@barefootjs/jsx 0.32.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"ssr-seed-plan.d.ts","sourceRoot":"","sources":["../src/ssr-seed-plan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAGH,OAAO,EAAsB,KAAK,eAAe,EAAE,MAAM,0BAA0B,CAAA;AACnF,OAAO,EAML,KAAK,UAAU,EAChB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,GAC7D;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,GAC/G;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,CAAA;CAAE,CAAA;AAE/D,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,KAAK,EAAE,WAAW,EAAE,CAAA;CACrB;AA4FD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,UAAU,GAAG,WAAW,CAqDpE"}
1
+ {"version":3,"file":"ssr-seed-plan.d.ts","sourceRoot":"","sources":["../src/ssr-seed-plan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAGH,OAAO,EAAsB,KAAK,eAAe,EAAE,MAAM,0BAA0B,CAAA;AACnF,OAAO,EAML,KAAK,UAAU,EAChB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,GAC7D;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,GAC/G;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,CAAA;CAAE,CAAA;AAE/D,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,KAAK,EAAE,WAAW,EAAE,CAAA;CACrB;AAiGD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,UAAU,GAAG,WAAW,CAiEpE"}
@@ -1 +1 @@
1
- {"version":3,"file":"static-literal.d.ts","sourceRoot":"","sources":["../src/static-literal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE9C;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,UAAU,EAChB,QAAQ,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,GACtC;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CA+D3B;AAED,iFAAiF;AACjF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAE9D;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,UAAU,GAAG,SAAS,EACnC,cAAc,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,SAAS,EACvD,IAAI,CAAC,EAAE;IAAE,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAA;CAAE,GACpD,OAAO,EAAE,GAAG,IAAI,CAYlB"}
1
+ {"version":3,"file":"static-literal.d.ts","sourceRoot":"","sources":["../src/static-literal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE9C;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,UAAU,EAChB,QAAQ,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,GACtC;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CA6E3B;AAED,iFAAiF;AACjF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAE9D;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,UAAU,GAAG,SAAS,EACnC,cAAc,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,SAAS,EACvD,IAAI,CAAC,EAAE;IAAE,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAA;CAAE,GACpD,OAAO,EAAE,GAAG,IAAI,CAYlB"}
@@ -1 +1 @@
1
- {"version":3,"file":"to-locale-date-lowering.d.ts","sourceRoot":"","sources":["../src/to-locale-date-lowering.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAY,MAAM,YAAY,CAAA;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAI1E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,QAA4C,CAAA;AAKxE;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAWhE;AASD;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAA;IACf,sEAAsE;IACtE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;CACvB;AAmFD;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEtE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACnC,gBAAgB,GAAG,IAAI,CAOzB;AA0PD,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,SAAS,UAAU,EAAE,EAC3B,QAAQ,EAAE,UAAU,GACnB,YAAY,GAAG,IAAI,CAyErB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAmBtF;AAED,eAAO,MAAM,kBAAkB,EAAE,cAMhC,CAAA"}
1
+ {"version":3,"file":"to-locale-date-lowering.d.ts","sourceRoot":"","sources":["../src/to-locale-date-lowering.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAY,MAAM,YAAY,CAAA;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAI1E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,QAA4C,CAAA;AAKxE;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAWhE;AASD;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAA;IACf,sEAAsE;IACtE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;CACvB;AAmFD;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEtE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACnC,gBAAgB,GAAG,IAAI,CAOzB;AA0PD,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,SAAS,UAAU,EAAE,EAC3B,QAAQ,EAAE,UAAU,GACnB,YAAY,GAAG,IAAI,CA6ErB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAmBtF;AAED,eAAO,MAAM,kBAAkB,EAAE,cAMhC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/jsx",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "description": "JSX compiler for BarefootJS - transforms JSX to server HTML + client JS",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -49,7 +49,7 @@
49
49
  "directory": "packages/jsx"
50
50
  },
51
51
  "dependencies": {
52
- "@barefootjs/shared": "0.32.0"
52
+ "@barefootjs/shared": "0.33.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@barefootjs/client": ">=0.2.0",
@@ -1,6 +1,6 @@
1
1
  import { describe, test, expect } from 'bun:test'
2
2
  import ts from 'typescript'
3
- import { parseExpression, isSupported, exprToString, stringifyParsedExpr, parseBlockBody, foldBlockToExpr, extractArrowBodyExpression, parseStyleObjectEntries, parseProviderObjectLiteral, asCallbackMethodCall, containsHigherOrder } from '../expression-parser'
3
+ import { parseExpression, isSupported, isSupportedValue, exprToString, stringifyParsedExpr, parseBlockBody, foldBlockToExpr, extractArrowBodyExpression, parseStyleObjectEntries, parseProviderObjectLiteral, asCallbackMethodCall, containsHigherOrder } from '../expression-parser'
4
4
  import { collectAllTypeRanges, reconstructWithoutTypes } from '../strip-types'
5
5
 
6
6
  describe('expression-parser', () => {
@@ -395,12 +395,23 @@ describe('expression-parser', () => {
395
395
  expect((parseExpression('true') as { raw?: string }).raw).toBeUndefined()
396
396
  })
397
397
 
398
- test('falls through to unsupported for spread / computed-key object literals', () => {
399
- // A spread member is not a plain map property preserves the
400
- // pre-A-1 `unsupported` behaviour (byte-identical).
401
- expect(parseExpression('({ ...rest, a: 1 })').kind).toBe('unsupported')
402
- // Computed key `[k]: v` can't resolve to a static name.
398
+ test('falls through to unsupported for computed-key object literals; spread parses as object-literal (#2696 Step 2)', () => {
399
+ // Computed key `[k]: v` can't resolve to a static namestill
400
+ // unsupported.
403
401
  expect(parseExpression('({ [k]: 1 })').kind).toBe('unsupported')
402
+ // A spread member (#2696 Step 2) is now a structured `{ kind:
403
+ // 'spread', expr }` entry, in source order alongside any `prop`
404
+ // entries — no longer the pre-#2696 blanket `unsupported`.
405
+ const spread = parseExpression('({ ...rest, a: 1 })')
406
+ expect(spread).toMatchObject({
407
+ kind: 'object-literal',
408
+ properties: [
409
+ { kind: 'spread', expr: { kind: 'identifier', name: 'rest' } },
410
+ { kind: 'prop', key: 'a', shorthand: false, value: { kind: 'literal', value: 1 } },
411
+ ],
412
+ })
413
+ // A method / getter / setter still isn't a plain map — still refuses.
414
+ expect(parseExpression('({ f() { return 1 } })').kind).toBe('unsupported')
404
415
  })
405
416
 
406
417
  test('a bare `{ … }` at statement position is a block, not an object literal', () => {
@@ -1402,6 +1413,32 @@ describe('expression-parser', () => {
1402
1413
  expect(result.supported).toBe(false)
1403
1414
  })
1404
1415
 
1416
+ // #2696 review: `pos` propagates by pure inheritance, with no per-shape
1417
+ // forcing to `value` — a RENDERED entry point's behaviour must stay
1418
+ // byte-identical to before `isSupportedValue` existed, on every shape
1419
+ // that reaches a container-contents position (an array-literal element,
1420
+ // an array-method/callback receiver, a callback body). `isSupportedValue`
1421
+ // admits the identical trees, since the whole tree it's entered on is a
1422
+ // `value` position (a signal/memo initializer).
1423
+ test('a populated object-literal array-literal element is refused at RENDERED, supported at VALUE', () => {
1424
+ const expr = parseExpression('[{ a: 1 }]')
1425
+ expect(expr.kind).toBe('array-literal')
1426
+ expect(isSupported(expr).supported).toBe(false)
1427
+ expect(isSupportedValue(expr).supported).toBe(true)
1428
+ })
1429
+
1430
+ test('an array-of-objects .join() receiver is refused at RENDERED, supported at VALUE', () => {
1431
+ const expr = parseExpression("[{ a: 1 }].join(',')")
1432
+ expect(isSupported(expr).supported).toBe(false)
1433
+ expect(isSupportedValue(expr).supported).toBe(true)
1434
+ })
1435
+
1436
+ test('a .map() callback body returning an object literal is refused at RENDERED, supported at VALUE', () => {
1437
+ const expr = parseExpression('items.map(t => ({ a: t }))')
1438
+ expect(isSupported(expr).supported).toBe(false)
1439
+ expect(isSupportedValue(expr).supported).toBe(true)
1440
+ })
1441
+
1405
1442
  test('L5: filter() with simple predicate IS supported', () => {
1406
1443
  const expr = parseExpression('items().filter(x => x.done)')
1407
1444
  const result = isSupported(expr)
@@ -98,12 +98,26 @@ describe('serializeParsedExpr', () => {
98
98
  })
99
99
  })
100
100
 
101
- test('map body: object literal carries key + value (no keyKind / shorthand)', () => {
101
+ test('map body: object literal carries kind + key + value (no keyKind / shorthand)', () => {
102
+ // #2696 Step 2: each entry now carries its own `kind` tag ('prop' here —
103
+ // no source spread in this body) so the evaluator can distinguish a
104
+ // `prop` from a `spread` entry without re-deriving it from field shape.
102
105
  expect(evalJSON('({ id: item.id, n: item.n })')).toEqual({
103
106
  kind: 'object-literal',
104
107
  properties: [
105
- { key: 'id', value: { kind: 'member', object: { kind: 'identifier', name: 'item' }, property: 'id' } },
106
- { key: 'n', value: { kind: 'member', object: { kind: 'identifier', name: 'item' }, property: 'n' } },
108
+ { kind: 'prop', key: 'id', value: { kind: 'member', object: { kind: 'identifier', name: 'item' }, property: 'id' } },
109
+ { kind: 'prop', key: 'n', value: { kind: 'member', object: { kind: 'identifier', name: 'item' }, property: 'n' } },
110
+ ],
111
+ })
112
+ })
113
+
114
+ test('map body: object literal carries a spread entry as `{ kind: "spread", expr }`', () => {
115
+ // The todo-app motivating shape (#2696): `{ ...t, editing: false }`.
116
+ expect(evalJSON("({ ...t, editing: false })")).toEqual({
117
+ kind: 'object-literal',
118
+ properties: [
119
+ { kind: 'spread', expr: { kind: 'identifier', name: 't' } },
120
+ { kind: 'prop', key: 'editing', value: { kind: 'literal', value: false } },
107
121
  ],
108
122
  })
109
123
  })
@@ -458,6 +458,47 @@ describe('extractSsrDefaults', () => {
458
458
  // space mirrors Hono's empty-className interpolation).
459
459
  expect(defaults?.classes).toEqual({ value: 'a b c d tail' })
460
460
  })
461
+
462
+ // #2698 review: a property access on a non-plain-object base (an array
463
+ // element bound by `.map()`) must refuse (UNRESOLVED), not silently read
464
+ // `undefined` via `hasOwnProperty` — an array exposes prototype members
465
+ // (`.map`, `.filter`, …) that aren't own properties, so the old guard
466
+ // (`typeof !== 'object'`) let arrays through and misreported them as a
467
+ // real `undefined` value instead of an unrepresentable read.
468
+ test('.map() body property-access on an ARRAY element is UNRESOLVED, not undefined', () => {
469
+ const metadata = metadataFor(`
470
+ 'use client'
471
+ import { createSignal } from '@barefootjs/client'
472
+ function C() {
473
+ const [x] = createSignal([[1, 2], [3, 4]].map(t => t.foo))
474
+ return <p>{x()}</p>
475
+ }
476
+ `)
477
+
478
+ const defaults = extractSsrDefaults(metadata)
479
+ // UNRESOLVED aborts the whole `.map()` (not a per-element `null`) —
480
+ // resultToJsonable renders the abort as the same `null` a genuinely
481
+ // undefined value would, but the two paths reach it differently: this
482
+ // pin exists to keep the array case going through the abort path.
483
+ expect(defaults?.x).toEqual({ value: null })
484
+ })
485
+
486
+ test('.map() body property-access on a PLAIN-OBJECT element still resolves a missing key to undefined', () => {
487
+ const metadata = metadataFor(`
488
+ 'use client'
489
+ import { createSignal } from '@barefootjs/client'
490
+ function C() {
491
+ const [x] = createSignal([{ a: 1 }, {}].map(t => t.a))
492
+ return <p>{x()}</p>
493
+ }
494
+ `)
495
+
496
+ const defaults = extractSsrDefaults(metadata)
497
+ // Unlike the array case, a missing key on a plain object resolves that
498
+ // ONE element to `undefined` (→ `null`) and the `.map()` keeps going —
499
+ // it does not abort the whole computation.
500
+ expect(defaults?.x).toEqual({ value: [1, null] })
501
+ })
461
502
  })
462
503
 
463
504
  // TS twin of the Ruby/Python/PHP/Perl/Rust `derive*FromDefaults` runtime
@@ -158,7 +158,12 @@ describe('computeSsrSeedPlan', () => {
158
158
  if (label.kind === 'opaque') expect(label.origin).toBe('memo')
159
159
  })
160
160
 
161
- test('unsupported body (object literal) → opaque', () => {
161
+ test('object-literal body → derived (#2696: value-position object literal, isSupportedValue)', () => {
162
+ // Was `opaque` before `checkSupport` gained its `pos` parameter — a
163
+ // memo's WHOLE body is a value position (an assignment, never a
164
+ // render), so an object literal there is exactly the shape
165
+ // `isSupportedValue` now admits (every property value — here `n()` —
166
+ // is itself supported).
162
167
  const plan = planFor(`
163
168
  'use client'
164
169
  import { createMemo, createSignal } from '@barefootjs/client'
@@ -169,7 +174,12 @@ describe('computeSsrSeedPlan', () => {
169
174
  }
170
175
  `)
171
176
 
172
- expect(step(plan, 'obj').kind).toBe('opaque')
177
+ const obj = step(plan, 'obj')
178
+ expect(obj.kind).toBe('derived')
179
+ if (obj.kind === 'derived') {
180
+ expect(obj.origin).toBe('memo')
181
+ expect(obj.frees).toEqual(['n'])
182
+ }
173
183
  })
174
184
 
175
185
  test('literal signal init → derived with empty frees (constant-skip is emit-side)', () => {
@@ -122,6 +122,7 @@ function htmlPropValue(parsed: ParsedExpr): ParsedExpr | null {
122
122
  if (parsed.kind !== 'object-literal') return null
123
123
  if (parsed.properties.length !== 1) return null
124
124
  const [prop] = parsed.properties
125
+ if (prop.kind === 'spread') return null
125
126
  if (prop.key !== '__html') return null
126
127
  return prop.value
127
128
  }
@@ -163,9 +163,11 @@ export interface ParsedExprEmitter {
163
163
  arrow(params: string[], body: ParsedExpr, emit: (e: ParsedExpr) => string): string
164
164
  regex(raw: string): string
165
165
  arrayLiteral(elements: ParsedExpr[], emit: (e: ParsedExpr) => string): string
166
- // Emit an object literal `{ a: 1, b: x }`. `raw` is the original
167
- // expression string so an adapter that doesn't lower object values yet
168
- // can delegate to `unsupported(raw, )` and stay byte-identical.
166
+ // Emit an object literal `{ a: 1, b: x, ...rest }` (#2696 Step 2 adds
167
+ // spread entries `properties` is ORDER-PRESERVING, so an adapter must
168
+ // merge left-to-right, later entries winning, to match JS). `raw` is the
169
+ // original expression string so an adapter that doesn't lower object
170
+ // values yet can delegate to `unsupported(raw, …)` and stay byte-identical.
169
171
  objectLiteral(
170
172
  properties: ObjectLiteralProperty[],
171
173
  raw: string,
@@ -277,6 +279,50 @@ export function groupBinaryOperand(operand: ParsedExpr, emitted: string): string
277
279
  : emitted
278
280
  }
279
281
 
282
+ /**
283
+ * Split an `object-literal`'s (order-preserving) `properties` into rendered
284
+ * SEGMENTS an adapter's `objectLiteral()` folds with its own merge idiom
285
+ * (#2696 Step 2): each maximal run of consecutive `prop` entries collapses
286
+ * into ONE literal (via `literalOf`), and each `spread` entry becomes its
287
+ * own segment (its emitted source expression) — so `{ a: 1, b: 2, ...t, c: 3
288
+ * }` yields `[literal({a,b}), emit(t), literal({c})]`, in source order.
289
+ *
290
+ * A caller with NO spread entries at all should keep its pre-#2696 single
291
+ * `literalOf(properties)` call instead of this (this always returns at
292
+ * least one segment, but a caller folding 1-segment output through a merge
293
+ * call — `array_merge(x)`, `{}.merge(x)` — would be needlessly indirect
294
+ * for the common spread-free case).
295
+ *
296
+ * Shared here (not duplicated per adapter) because the grouping itself is
297
+ * backend-neutral structure — only `literalOf` (the target language's
298
+ * literal syntax) and the fold (the target language's merge idiom) are
299
+ * adapter-specific.
300
+ */
301
+ export function groupObjectLiteralSegments(
302
+ properties: readonly ObjectLiteralProperty[],
303
+ literalOf: (run: ReadonlyArray<Extract<ObjectLiteralProperty, { kind: 'prop' }>>) => string,
304
+ emit: (e: ParsedExpr) => string,
305
+ ): string[] {
306
+ const segments: string[] = []
307
+ let run: Extract<ObjectLiteralProperty, { kind: 'prop' }>[] = []
308
+ const flushRun = () => {
309
+ if (run.length > 0) {
310
+ segments.push(literalOf(run))
311
+ run = []
312
+ }
313
+ }
314
+ for (const p of properties) {
315
+ if (p.kind === 'spread') {
316
+ flushRun()
317
+ segments.push(emit(p.expr))
318
+ } else {
319
+ run.push(p)
320
+ }
321
+ }
322
+ flushRun()
323
+ return segments
324
+ }
325
+
280
326
  /**
281
327
  * Single point of dispatch from `ParsedExpr.kind` to the adapter's
282
328
  * method. Adapters call this once at their entry point; the recursion