@barefootjs/jsx 0.5.3 → 0.6.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.
- package/dist/adapters/parsed-expr-emitter.d.ts +1 -1
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +1 -1
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.js +154 -19
- package/dist/ir-to-client-js/generate-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +30 -1
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/phases/provider-and-child-inits.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +10 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/expression-parser.test.ts +167 -13
- package/src/__tests__/ir-to-client-js/reactivity.test.ts +1 -0
- package/src/__tests__/staged-ir/06-multi-stage-soak.test.ts +18 -3
- package/src/adapters/parsed-expr-emitter.ts +7 -0
- package/src/expression-parser.ts +200 -17
- package/src/ir-to-client-js/emit-registration.ts +1 -1
- package/src/ir-to-client-js/generate-init.ts +16 -1
- package/src/ir-to-client-js/html-template.ts +156 -2
- package/src/ir-to-client-js/index.ts +1 -0
- package/src/ir-to-client-js/phases/provider-and-child-inits.ts +12 -1
- package/src/ir-to-client-js/types.ts +10 -0
|
@@ -398,25 +398,104 @@ describe('expression-parser', () => {
|
|
|
398
398
|
const result = parseExpression("items().filter(({name = 'anon'}) => name.startsWith('a'))")
|
|
399
399
|
expect(result.kind).toBe('higher-order')
|
|
400
400
|
if (result.kind === 'higher-order') {
|
|
401
|
-
// Predicate: `(_t.name ?? 'anon').startsWith('a')`.
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
}
|
|
401
|
+
// Predicate: `(_t.name ?? 'anon').startsWith('a')`. Since #1448
|
|
402
|
+
// Tier B, `.startsWith` lowers to an `array-method` node (it was
|
|
403
|
+
// a generic `call` before), so the receiver is on `.object`.
|
|
404
|
+
expect(result.predicate.kind).toBe('array-method')
|
|
405
|
+
if (result.predicate.kind === 'array-method') {
|
|
406
|
+
expect(result.predicate.method).toBe('startsWith')
|
|
407
|
+
expect(result.predicate.object.kind).toBe('logical')
|
|
408
|
+
if (result.predicate.object.kind === 'logical') {
|
|
409
|
+
expect(result.predicate.object.op).toBe('??')
|
|
410
|
+
expect(result.predicate.object.right.kind).toBe('literal')
|
|
411
|
+
if (result.predicate.object.right.kind === 'literal') {
|
|
412
|
+
expect(result.predicate.object.right.value).toBe('anon')
|
|
414
413
|
}
|
|
415
414
|
}
|
|
416
415
|
}
|
|
417
416
|
}
|
|
418
417
|
})
|
|
419
418
|
|
|
419
|
+
test('lowers .startsWith(search, position) — full arity (#1448 Tier B)', () => {
|
|
420
|
+
const result = parseExpression(`name().startsWith("world", 6)`)
|
|
421
|
+
expect(result.kind).toBe('array-method')
|
|
422
|
+
if (result.kind === 'array-method') {
|
|
423
|
+
expect(result.method).toBe('startsWith')
|
|
424
|
+
// Both the search string and the position survive as args so the
|
|
425
|
+
// adapter can emit the re-anchored test.
|
|
426
|
+
expect(result.args.length).toBe(2)
|
|
427
|
+
}
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
test('refuses .startsWith() with no search string (#1448 Tier B)', () => {
|
|
431
|
+
// JS coerces the missing argument to the string "undefined" — a
|
|
432
|
+
// degenerate result not worth lowering (mirrors `.includes()`).
|
|
433
|
+
const result = parseExpression(`name().startsWith()`)
|
|
434
|
+
expect(result.kind).toBe('unsupported')
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
test('lowers .replace(pattern, replacement, extra) — ignores 3rd+ arg (#1448 Tier B)', () => {
|
|
438
|
+
const result = parseExpression(`name().replace("o", "0", "extra")`)
|
|
439
|
+
expect(result.kind).toBe('array-method')
|
|
440
|
+
if (result.kind === 'array-method') {
|
|
441
|
+
expect(result.method).toBe('replace')
|
|
442
|
+
}
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
test('refuses .replace(pattern) with no replacement (#1448 Tier B)', () => {
|
|
446
|
+
// JS coerces the missing replacement to the string "undefined".
|
|
447
|
+
const result = parseExpression(`name().replace("o")`)
|
|
448
|
+
expect(result.kind).toBe('unsupported')
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
test('refuses .replace() with no arguments (#1448 Tier B)', () => {
|
|
452
|
+
// The zero-arg form is degenerate too (both pattern and
|
|
453
|
+
// replacement coerce to "undefined") — pin it alongside the
|
|
454
|
+
// one-arg case so neither regresses.
|
|
455
|
+
const result = parseExpression(`name().replace()`)
|
|
456
|
+
expect(result.kind).toBe('unsupported')
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
test('.replace with a non-regex unsupported arg surfaces its own reason (#1448 Tier B)', () => {
|
|
460
|
+
// An object-literal replacement must NOT be mislabelled as the
|
|
461
|
+
// deferred regex form — the guard only special-cases a
|
|
462
|
+
// regex-literal pattern.
|
|
463
|
+
const result = parseExpression(`name().replace("a", {x: 1})`)
|
|
464
|
+
expect(result.kind).toBe('unsupported')
|
|
465
|
+
if (result.kind === 'unsupported') {
|
|
466
|
+
expect(result.reason).not.toContain('regex form is deferred')
|
|
467
|
+
}
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
test('.replace(/re/, repl) refuses the regex form with the deferred reason (#1448 Tier B)', () => {
|
|
471
|
+
const result = parseExpression(`name().replace(/a/g, "b")`)
|
|
472
|
+
expect(result.kind).toBe('unsupported')
|
|
473
|
+
if (result.kind === 'unsupported') {
|
|
474
|
+
expect(result.reason).toContain('regex form is deferred')
|
|
475
|
+
}
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
test('lowers .repeat() and .repeat(n, extra) — full arity (#1448 Tier B)', () => {
|
|
479
|
+
// `.repeat()` is `repeat(0)` → "" in JS (not a RangeError); a
|
|
480
|
+
// second+ argument is ignored. Both stay on the lowering path.
|
|
481
|
+
for (const expr of [`name().repeat()`, `name().repeat(3, 4)`]) {
|
|
482
|
+
const result = parseExpression(expr)
|
|
483
|
+
expect(result.kind).toBe('array-method')
|
|
484
|
+
if (result.kind === 'array-method') {
|
|
485
|
+
expect(result.method).toBe('repeat')
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
test('lowers .padStart() / .padEnd(t, p, extra) — full arity (#1448 Tier B)', () => {
|
|
491
|
+
// `.padStart()` is `padStart(0)` → the receiver unchanged; a
|
|
492
|
+
// third+ argument is ignored. Both stay on the lowering path.
|
|
493
|
+
for (const expr of [`name().padStart()`, `name().padEnd(5, "0", "x")`]) {
|
|
494
|
+
const result = parseExpression(expr)
|
|
495
|
+
expect(result.kind).toBe('array-method')
|
|
496
|
+
}
|
|
497
|
+
})
|
|
498
|
+
|
|
420
499
|
test('lowers .filter(({label = `untitled-${suffix}`}) => label) — template-literal default (#1531)', () => {
|
|
421
500
|
const result = parseExpression('items().filter(({label = `untitled-${suffix}`}) => label)')
|
|
422
501
|
expect(result.kind).toBe('higher-order')
|
|
@@ -1358,3 +1437,78 @@ describe('expression-parser', () => {
|
|
|
1358
1437
|
})
|
|
1359
1438
|
})
|
|
1360
1439
|
})
|
|
1440
|
+
|
|
1441
|
+
// =============================================================================
|
|
1442
|
+
// Full-arity lowering for the array / string methods (#1448)
|
|
1443
|
+
// =============================================================================
|
|
1444
|
+
//
|
|
1445
|
+
// These methods lower at their full JS arity: zero-arg defaults
|
|
1446
|
+
// (`.join()` → `,`, `.slice()` → full copy) and JS-ignored trailing
|
|
1447
|
+
// arguments (`.trim(1)`, `.at(i, extra)`, `.slice(s, e, extra)`) are all
|
|
1448
|
+
// accepted. The only forms still refused are the ones whose EXTRA
|
|
1449
|
+
// argument changes the result and isn't lowered yet — the `fromIndex` of
|
|
1450
|
+
// `.includes`/`.indexOf`/`.lastIndexOf` and the variadic `.concat(a, b)`
|
|
1451
|
+
// — because silently dropping those would make SSR differ from the
|
|
1452
|
+
// client (worse than a build error).
|
|
1453
|
+
describe('expression-parser — array-method full-arity lowering (#1448)', () => {
|
|
1454
|
+
const supported: Array<[string, string]> = [
|
|
1455
|
+
// base forms
|
|
1456
|
+
['join(sep)', 'arr.join("-")'],
|
|
1457
|
+
['includes(x)', 'arr.includes(x)'],
|
|
1458
|
+
['indexOf(x)', 'arr.indexOf(x)'],
|
|
1459
|
+
['at(i)', 'arr.at(1)'],
|
|
1460
|
+
['concat(other)', 'arr.concat(b)'],
|
|
1461
|
+
['slice(start)', 'arr.slice(1)'],
|
|
1462
|
+
['slice(start, end)', 'arr.slice(1, 2)'],
|
|
1463
|
+
['trim()', 's.trim()'],
|
|
1464
|
+
// zero-arg defaults (every one of these escaping both its arm AND
|
|
1465
|
+
// the guard is the footgun the relaxation must NOT reintroduce)
|
|
1466
|
+
['join() → default ","', 'arr.join()'],
|
|
1467
|
+
['slice() → full copy', 'arr.slice()'],
|
|
1468
|
+
['at() → at(0)', 'arr.at()'],
|
|
1469
|
+
['concat() → shallow copy', 'arr.concat()'],
|
|
1470
|
+
['reverse() base', 'arr.reverse()'],
|
|
1471
|
+
['toReversed() base', 'arr.toReversed()'],
|
|
1472
|
+
['toLowerCase() base', 's.toLowerCase()'],
|
|
1473
|
+
['toUpperCase() base', 's.toUpperCase()'],
|
|
1474
|
+
['lastIndexOf(x) base', 'arr.lastIndexOf(x)'],
|
|
1475
|
+
// JS-ignored trailing arguments
|
|
1476
|
+
['slice(s, e, extra)', 'arr.slice(1, 2, 3)'],
|
|
1477
|
+
['at(i, extra)', 'arr.at(1, 2)'],
|
|
1478
|
+
['reverse(extra)', 'arr.reverse(1)'],
|
|
1479
|
+
['toReversed(extra)', 'arr.toReversed(1)'],
|
|
1480
|
+
['toLowerCase(extra)', 's.toLowerCase("x")'],
|
|
1481
|
+
['toUpperCase(extra)', 's.toUpperCase("x")'],
|
|
1482
|
+
['trim(extra)', 's.trim(1)'],
|
|
1483
|
+
]
|
|
1484
|
+
for (const [label, expr] of supported) {
|
|
1485
|
+
test(`${label} — lowers to array-method`, () => {
|
|
1486
|
+
expect(parseExpression(expr).kind).toBe('array-method')
|
|
1487
|
+
})
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// Still refused (the extra argument is meaningful and not yet
|
|
1491
|
+
// lowered), plus the degenerate zero-arg search forms (`includes()`
|
|
1492
|
+
// searches for `undefined` — refused rather than guessed).
|
|
1493
|
+
const refused: Array<[string, string]> = [
|
|
1494
|
+
['includes(x, fromIndex)', 'arr.includes(x, 1)'],
|
|
1495
|
+
['indexOf(x, fromIndex)', 'arr.indexOf(x, 1)'],
|
|
1496
|
+
['lastIndexOf(x, fromIndex)', 'arr.lastIndexOf(x, 1)'],
|
|
1497
|
+
['concat(a, b) variadic', 'arr.concat(a, b)'],
|
|
1498
|
+
['includes() zero-arg', 'arr.includes()'],
|
|
1499
|
+
['indexOf() zero-arg', 'arr.indexOf()'],
|
|
1500
|
+
]
|
|
1501
|
+
for (const [label, expr] of refused) {
|
|
1502
|
+
test(`${label} — refuses (meaningful extra arg, not yet lowered)`, () => {
|
|
1503
|
+
const result = parseExpression(expr)
|
|
1504
|
+
expect(result.kind).toBe('unsupported')
|
|
1505
|
+
if (result.kind === 'unsupported') {
|
|
1506
|
+
// Must NOT push `@client` (wrong remedy; doesn't work in
|
|
1507
|
+
// attribute / condition position), and must explain it's a
|
|
1508
|
+
// not-yet-lowered argument.
|
|
1509
|
+
expect(result.reason).not.toContain('@client')
|
|
1510
|
+
expect(result.reason).toContain('not yet lowered')
|
|
1511
|
+
}
|
|
1512
|
+
})
|
|
1513
|
+
}
|
|
1514
|
+
})
|
|
@@ -80,9 +80,24 @@ describe('Multi-stage soak (DeskCanvas-shape)', () => {
|
|
|
80
80
|
expect(clientJs).toMatch(/import\s+\{[^}]*useYjs[^}]*\}\s+from\s+['"]\.\/useYjs['"]/)
|
|
81
81
|
})
|
|
82
82
|
|
|
83
|
-
test('Module →
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
test('Module → Init: nodeTypes is forwarded to the deferred child via init', () => {
|
|
84
|
+
// The `<Flow>` render carries init-scope-only props (`data-yjs={yjs.id}`
|
|
85
|
+
// where `yjs` is an init-local; `nodes={items()}`). The module-scope
|
|
86
|
+
// template lambda can't supply those, so the child render is DEFERRED
|
|
87
|
+
// to a `data-bf-ph` placeholder and `upsertChild` (→ createComponent)
|
|
88
|
+
// creates `Flow` with the COMPLETE getter props. The inlinable
|
|
89
|
+
// `nodeTypes` is therefore forwarded through the init `upsertChild`
|
|
90
|
+
// getter — never dropped — rather than baked into the template lambda.
|
|
91
|
+
// (Pre-fix this child was rendered eagerly via `renderChild('Flow', {
|
|
92
|
+
// nodeTypes })` with the init-scope props silently dropped — the
|
|
93
|
+
// dropped-prop bug.)
|
|
94
|
+
const { templateBody, initBody } = compile(DESK_CANVAS_SHAPE, 'DeskCanvas.tsx')
|
|
95
|
+
// Template defers the child render with a placeholder, not renderChild.
|
|
96
|
+
expect(templateBody).toMatch(/data-bf-ph="s0"/)
|
|
97
|
+
expect(templateBody).not.toMatch(/renderChild/)
|
|
98
|
+
// Init forwards nodeTypes (and every other prop) with complete values.
|
|
99
|
+
expect(initBody).toMatch(/upsertChild\(__scope,\s*'Flow',\s*'s0'/)
|
|
100
|
+
expect(initBody).toMatch(/get nodeTypes\(\)\s*\{\s*return nodeTypes\s*\}/)
|
|
86
101
|
})
|
|
87
102
|
|
|
88
103
|
// TODO(#1138 P3 5/N): `useYjs(...)` (init-local initializer) leaks into
|
|
@@ -66,6 +66,13 @@ export type ArrayMethod =
|
|
|
66
66
|
| 'toLowerCase'
|
|
67
67
|
| 'toUpperCase'
|
|
68
68
|
| 'trim'
|
|
69
|
+
| 'split'
|
|
70
|
+
| 'startsWith'
|
|
71
|
+
| 'endsWith'
|
|
72
|
+
| 'replace'
|
|
73
|
+
| 'repeat'
|
|
74
|
+
| 'padStart'
|
|
75
|
+
| 'padEnd'
|
|
69
76
|
|
|
70
77
|
/**
|
|
71
78
|
* Method names handled by the dedicated `sortMethod()` dispatcher
|
package/src/expression-parser.ts
CHANGED
|
@@ -46,6 +46,13 @@ export type ParsedExpr =
|
|
|
46
46
|
| 'toLowerCase'
|
|
47
47
|
| 'toUpperCase'
|
|
48
48
|
| 'trim'
|
|
49
|
+
| 'split'
|
|
50
|
+
| 'startsWith'
|
|
51
|
+
| 'endsWith'
|
|
52
|
+
| 'replace'
|
|
53
|
+
| 'repeat'
|
|
54
|
+
| 'padStart'
|
|
55
|
+
| 'padEnd'
|
|
49
56
|
object: ParsedExpr
|
|
50
57
|
args: ParsedExpr[]
|
|
51
58
|
}
|
|
@@ -225,12 +232,46 @@ const UNSUPPORTED_METHODS = new Set([
|
|
|
225
232
|
// unsupported array methods above get), pointing users at the
|
|
226
233
|
// `/* @client */` escape hatch. Each name drops off as its lowering
|
|
227
234
|
// lands. See #1448 "Unsupported string methods" Tier B / Tier C.
|
|
228
|
-
|
|
229
|
-
|
|
235
|
+
// `split` is no longer here — `String.prototype.split(sep)` lowers
|
|
236
|
+
// via the `array-method` IR + `bf_split` (Go) / `bf->split` (Mojo),
|
|
237
|
+
// returning an array that composes with `.join()` / `.map()` / etc.
|
|
238
|
+
// See #1448 Tier B.
|
|
239
|
+
// `startsWith` / `endsWith` are no longer here — both lower via the
|
|
240
|
+
// `array-method` IR + `bf_starts_with` / `bf_ends_with` (Go) and
|
|
241
|
+
// `bf->starts_with` / `bf->ends_with` (Mojo). See #1448 Tier B.
|
|
242
|
+
// `replace` is no longer here — the string-pattern form lowers via
|
|
243
|
+
// the `array-method` IR + `bf_replace` (Go) / `bf->replace` (Mojo);
|
|
244
|
+
// the regex-pattern form is refused at the parse arm below (it would
|
|
245
|
+
// need the per-adapter regex-flavour decision). `replaceAll` stays
|
|
246
|
+
// refused. See #1448 Tier B.
|
|
247
|
+
// `repeat` is no longer here — `String.prototype.repeat(n)` lowers via
|
|
248
|
+
// the `array-method` IR + `bf_repeat` (Go) / `bf->repeat` (Mojo).
|
|
249
|
+
// See #1448 Tier B.
|
|
250
|
+
// `padStart` / `padEnd` are no longer here — both lower via the
|
|
251
|
+
// `array-method` IR + `bf_pad_start` / `bf_pad_end` (Go) and
|
|
252
|
+
// `bf->pad_start` / `bf->pad_end` (Mojo). See #1448 Tier B.
|
|
253
|
+
'replaceAll',
|
|
230
254
|
'charAt', 'charCodeAt', 'codePointAt', 'normalize',
|
|
231
255
|
'substring', 'substr', 'match', 'matchAll', 'search',
|
|
232
256
|
])
|
|
233
257
|
|
|
258
|
+
// Methods that lower at their single-argument form but whose EXTRA
|
|
259
|
+
// argument is meaningful and NOT yet lowered: the `fromIndex` of
|
|
260
|
+
// `.includes` / `.indexOf` / `.lastIndexOf` (the 2-arg form) and the
|
|
261
|
+
// additional arrays of a variadic `.concat(a, b, …)`. The relaxed
|
|
262
|
+
// per-method arms in `convertNode` accept every method's zero-arg
|
|
263
|
+
// defaults (`.join()` / `.slice()` / `.concat()` / `.at()`) and
|
|
264
|
+
// JS-ignored trailing arguments; this guard catches only the remaining
|
|
265
|
+
// meaningful-extra forms, refusing them with BF101 because silently
|
|
266
|
+
// dropping the argument would make the SSR output differ from the
|
|
267
|
+
// client. See #1448.
|
|
268
|
+
const LOWERED_ARRAY_METHODS = new Set([
|
|
269
|
+
'includes',
|
|
270
|
+
'indexOf',
|
|
271
|
+
'lastIndexOf',
|
|
272
|
+
'concat',
|
|
273
|
+
])
|
|
274
|
+
|
|
234
275
|
// =============================================================================
|
|
235
276
|
// Expression Parser
|
|
236
277
|
// =============================================================================
|
|
@@ -344,7 +385,10 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
344
385
|
// etc. later means widening the IR discriminator, not adding more
|
|
345
386
|
// branches to every adapter's call dispatch.
|
|
346
387
|
if (callee.kind === 'member' && !callee.computed) {
|
|
347
|
-
|
|
388
|
+
// `.join()` / `.join(sep)` — JS defaults the separator to `,` when
|
|
389
|
+
// omitted and ignores any extra arguments. Accept every arity; the
|
|
390
|
+
// adapters supply the default separator and read only `args[0]`.
|
|
391
|
+
if (callee.property === 'join') {
|
|
348
392
|
return { kind: 'array-method', method: 'join', object: callee.object, args }
|
|
349
393
|
}
|
|
350
394
|
// `.includes(x)` — shared between `Array.prototype.includes` and
|
|
@@ -369,20 +413,28 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
369
413
|
// element). Go has `bf_at` registered already (see runtime
|
|
370
414
|
// FuncMap); Mojo's `bf->at` wraps the same arithmetic.
|
|
371
415
|
// See #1448 Tier A.
|
|
372
|
-
|
|
416
|
+
// `.at(i)` — JS ignores any argument past the first, and `.at()`
|
|
417
|
+
// with no argument is `.at(0)` (the first element). Accept every
|
|
418
|
+
// arity; the adapters read `args[0]` (defaulting the index to 0).
|
|
419
|
+
if (callee.property === 'at') {
|
|
373
420
|
return { kind: 'array-method', method: 'at', object: callee.object, args }
|
|
374
421
|
}
|
|
375
|
-
// `.concat(other)` —
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
|
|
422
|
+
// `.concat()` / `.concat(other)` — `.concat()` returns a shallow
|
|
423
|
+
// copy (indistinguishable from the receiver in an SSR snapshot),
|
|
424
|
+
// and `.concat(other)` merges the two arrays. Go uses `bf_concat`
|
|
425
|
+
// (reflect-based append into `[]any`); Mojo uses `bf->concat`
|
|
426
|
+
// (Perl list builder). The VARIADIC form (`.concat(a, b, …)`) is
|
|
427
|
+
// not lowered yet — it's refused by the guard below rather than
|
|
428
|
+
// silently dropping the extra arrays.
|
|
429
|
+
if (callee.property === 'concat' && args.length <= 1) {
|
|
380
430
|
return { kind: 'array-method', method: 'concat', object: callee.object, args }
|
|
381
431
|
}
|
|
382
|
-
// `.slice(start)` / `.slice(start, end)` —
|
|
383
|
-
// through `bf_slice` (Go) / `bf->slice` (Mojo)
|
|
384
|
-
//
|
|
385
|
-
|
|
432
|
+
// `.slice()` / `.slice(start)` / `.slice(start, end)` — route
|
|
433
|
+
// through `bf_slice` (Go) / `bf->slice` (Mojo). A missing `start`
|
|
434
|
+
// defaults to 0 (full copy), a missing / undef `end` means "to
|
|
435
|
+
// length", and JS ignores any third+ argument. Accept every arity;
|
|
436
|
+
// the adapters read only `args[0]` / `args[1]`.
|
|
437
|
+
if (callee.property === 'slice') {
|
|
386
438
|
return { kind: 'array-method', method: 'slice', object: callee.object, args }
|
|
387
439
|
}
|
|
388
440
|
// `.reverse()` and `.toReversed()` — both zero-arg shapes
|
|
@@ -390,7 +442,8 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
390
442
|
// of state, so JS's mutate-and-return-receiver (`reverse`)
|
|
391
443
|
// vs return-new-array (`toReversed`) distinction has no
|
|
392
444
|
// template-level meaning; both produce a new reversed array.
|
|
393
|
-
|
|
445
|
+
// JS takes no argument and ignores any that are passed.
|
|
446
|
+
if (callee.property === 'reverse' || callee.property === 'toReversed') {
|
|
394
447
|
return { kind: 'array-method', method: callee.property, object: callee.object, args }
|
|
395
448
|
}
|
|
396
449
|
// `.toLowerCase()` — string-only (the IR carries a value-builtin
|
|
@@ -398,20 +451,150 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
398
451
|
// label is a misnomer for string methods but the mechanical
|
|
399
452
|
// pipeline matches). Go uses the existing `bf_lower` helper;
|
|
400
453
|
// Mojo uses Perl's native `lc`. See #1448 Tier A.
|
|
401
|
-
if (callee.property === 'toLowerCase'
|
|
454
|
+
if (callee.property === 'toLowerCase') {
|
|
402
455
|
return { kind: 'array-method', method: 'toLowerCase', object: callee.object, args }
|
|
403
456
|
}
|
|
404
457
|
// `.toUpperCase()` — Go uses the existing `bf_upper` helper;
|
|
405
458
|
// Mojo uses Perl's native `uc`.
|
|
406
|
-
if (callee.property === 'toUpperCase'
|
|
459
|
+
if (callee.property === 'toUpperCase') {
|
|
407
460
|
return { kind: 'array-method', method: 'toUpperCase', object: callee.object, args }
|
|
408
461
|
}
|
|
409
462
|
// `.trim()` — Go uses the existing `bf_trim` helper; Mojo uses
|
|
410
463
|
// a new `bf->trim` method that mirrors JS's "strip leading +
|
|
411
464
|
// trailing whitespace" semantic via a Perl regex.
|
|
412
|
-
if (callee.property === 'trim'
|
|
465
|
+
if (callee.property === 'trim') {
|
|
413
466
|
return { kind: 'array-method', method: 'trim', object: callee.object, args }
|
|
414
467
|
}
|
|
468
|
+
// `.split()` / `.split(sep)` / `.split(sep, limit)` — string →
|
|
469
|
+
// array, full JS arity. `.split()` (no separator) returns the
|
|
470
|
+
// whole string as a single element; `.split(sep)` splits on the
|
|
471
|
+
// (literal) separator; the optional `limit` caps the number of
|
|
472
|
+
// pieces. JS ignores a third+ argument. Go uses `bf_split`
|
|
473
|
+
// (`strings.Split`, optional limit, normalised to `[]any`) and
|
|
474
|
+
// `bf_arr` for the no-separator whole-string case; Mojo uses
|
|
475
|
+
// `bf->split`. The regex-separator form stays refused (the parser
|
|
476
|
+
// never reaches here for it — a regex literal arg is `unsupported`
|
|
477
|
+
// and propagates). See #1448 Tier B.
|
|
478
|
+
if (callee.property === 'split') {
|
|
479
|
+
return { kind: 'array-method', method: 'split', object: callee.object, args }
|
|
480
|
+
}
|
|
481
|
+
// Arity guard for the forms whose EXTRA argument changes the
|
|
482
|
+
// result and is not yet lowered: the `fromIndex` of `.includes` /
|
|
483
|
+
// `.indexOf` / `.lastIndexOf` (the 2-arg form), and the additional
|
|
484
|
+
// arrays of a variadic `.concat(a, b, …)`. Silently dropping those
|
|
485
|
+
// would make the SSR output *differ* from the client (worse than a
|
|
486
|
+
// build error), so they refuse with BF101 until lowered. (The
|
|
487
|
+
// single-argument forms, zero-arg defaults, and JS-ignored
|
|
488
|
+
// trailing arguments of every method are accepted by the relaxed
|
|
489
|
+
// arms above.) See #1448.
|
|
490
|
+
if (LOWERED_ARRAY_METHODS.has(callee.property)) {
|
|
491
|
+
const argName = callee.property === 'concat' ? 'other' : 'x'
|
|
492
|
+
const detail =
|
|
493
|
+
callee.property === 'concat'
|
|
494
|
+
? 'the variadic `.concat(a, b, …)` form'
|
|
495
|
+
: `\`.${callee.property}(…)\` with ${args.length} argument(s)`
|
|
496
|
+
return {
|
|
497
|
+
kind: 'unsupported',
|
|
498
|
+
raw,
|
|
499
|
+
reason: `${detail} is not yet lowered to the Go/Mojo template adapters. Use the single-argument \`.${callee.property}(${argName})\` form, or pre-compute the value before the template.`,
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
// `.startsWith(search, position?)` / `.endsWith(search, endPosition?)`
|
|
503
|
+
// — string → boolean, full JS arity. Go uses `bf_starts_with` /
|
|
504
|
+
// `bf_ends_with` (wrapping `strings.HasPrefix` / `strings.HasSuffix`,
|
|
505
|
+
// with an optional position that re-anchors the comparison); Mojo
|
|
506
|
+
// uses `bf->starts_with` / `bf->ends_with` (substr comparison). JS
|
|
507
|
+
// ignores a third+ argument. The zero-arg form (`.startsWith()`) is
|
|
508
|
+
// refused: JS coerces the missing search to the literal string
|
|
509
|
+
// "undefined", a degenerate result not worth lowering (mirrors the
|
|
510
|
+
// `.includes()` zero-arg refusal). See #1448 Tier B.
|
|
511
|
+
if (callee.property === 'startsWith' || callee.property === 'endsWith') {
|
|
512
|
+
if (args.length === 0) {
|
|
513
|
+
return {
|
|
514
|
+
kind: 'unsupported',
|
|
515
|
+
raw,
|
|
516
|
+
reason: `\`.${callee.property}()\` with no search string is not lowered — JS coerces the missing argument to the string "undefined", a degenerate result. Pass an explicit search string, or pre-compute the value before the template.`,
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return { kind: 'array-method', method: callee.property, object: callee.object, args }
|
|
520
|
+
}
|
|
521
|
+
// `.replace(pattern, replacement)` — string-pattern form,
|
|
522
|
+
// replacing the FIRST occurrence (JS semantics for a string
|
|
523
|
+
// pattern). Go uses `bf_replace` (`strings.Replace` with n=1);
|
|
524
|
+
// Mojo uses `bf->replace` (index/substr splice, no regex). A
|
|
525
|
+
// regex-literal pattern parses as `unsupported` (convertNode has
|
|
526
|
+
// no regex arm), so it's refused explicitly here rather than
|
|
527
|
+
// emitting a broken `.Replace` — the Perl `s///` vs Go
|
|
528
|
+
// `regexp.ReplaceAllString` flavour gap is the open design
|
|
529
|
+
// question in #1448. `replaceAll` stays refused entirely.
|
|
530
|
+
//
|
|
531
|
+
// Full JS arity: a third+ argument is ignored (the adapter reads
|
|
532
|
+
// only the pattern + replacement). The one- and zero-argument
|
|
533
|
+
// forms are refused: JS coerces the missing replacement (and
|
|
534
|
+
// pattern) to the literal string "undefined", a degenerate result
|
|
535
|
+
// (mirrors the `.includes()` / `.startsWith()` zero-arg refusal).
|
|
536
|
+
if (callee.property === 'replace') {
|
|
537
|
+
if (args.length < 2) {
|
|
538
|
+
return {
|
|
539
|
+
kind: 'unsupported',
|
|
540
|
+
raw,
|
|
541
|
+
reason: `\`.replace(${args.length === 0 ? '' : 'pattern'})\` needs both a pattern and a replacement — JS coerces the missing argument to the string "undefined", a degenerate result. Pass both arguments, or pre-compute the value before the template.`,
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
// A regex-literal pattern is the deferred form (the Perl `s///`
|
|
545
|
+
// vs Go `regexp.ReplaceAllString` flavour gap, #1448) — detect it
|
|
546
|
+
// on the TS node so the message is accurate. Any OTHER unsupported
|
|
547
|
+
// pattern/replacement (an object literal, an unsupported call, …)
|
|
548
|
+
// surfaces ITS OWN reason rather than being mislabelled as the
|
|
549
|
+
// regex form.
|
|
550
|
+
const patternNode = node.arguments[0]
|
|
551
|
+
if (patternNode && ts.isRegularExpressionLiteral(patternNode)) {
|
|
552
|
+
return {
|
|
553
|
+
kind: 'unsupported',
|
|
554
|
+
raw,
|
|
555
|
+
reason:
|
|
556
|
+
'String.prototype.replace supports only a string pattern + string replacement (the regex form is deferred); use a string pattern or wrap the expression in /* @client */',
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const badArg =
|
|
560
|
+
args[0].kind === 'unsupported'
|
|
561
|
+
? args[0]
|
|
562
|
+
: args[1].kind === 'unsupported'
|
|
563
|
+
? args[1]
|
|
564
|
+
: undefined
|
|
565
|
+
if (badArg && badArg.kind === 'unsupported') {
|
|
566
|
+
return { kind: 'unsupported', raw, reason: badArg.reason }
|
|
567
|
+
}
|
|
568
|
+
return { kind: 'array-method', method: 'replace', object: callee.object, args }
|
|
569
|
+
}
|
|
570
|
+
// `.repeat(n)` — string → string (the receiver concatenated `n`
|
|
571
|
+
// times). Go uses `bf_repeat` (`strings.Repeat`, clamping a
|
|
572
|
+
// negative count to "" instead of panicking); Mojo uses
|
|
573
|
+
// `bf->repeat` (Perl's `x` operator). JS throws RangeError for a
|
|
574
|
+
// negative count, but SSR templates degrade to the empty string
|
|
575
|
+
// rather than crashing the render. See #1448 Tier B.
|
|
576
|
+
// Full JS arity: `.repeat()` (no count) is `repeat(0)` → "" (JS
|
|
577
|
+
// coerces the missing count to 0, not a RangeError), and a
|
|
578
|
+
// second+ argument is ignored. The adapter supplies the `0` for
|
|
579
|
+
// the no-argument form. See #1448 Tier B.
|
|
580
|
+
if (callee.property === 'repeat') {
|
|
581
|
+
return { kind: 'array-method', method: 'repeat', object: callee.object, args }
|
|
582
|
+
}
|
|
583
|
+
// `.padStart(target, pad?)` / `.padEnd(target, pad?)` — string →
|
|
584
|
+
// string, padded to `target` length with `pad` (default a single
|
|
585
|
+
// space) repeated + truncated to fill. Go uses `bf_pad_start` /
|
|
586
|
+
// `bf_pad_end`; Mojo uses `bf->pad_start` / `bf->pad_end`. Both
|
|
587
|
+
// count length in code points (Go runes / Perl chars) so they
|
|
588
|
+
// stay byte-equal — this differs from JS's UTF-16-unit length
|
|
589
|
+
// only for astral-plane receivers, which are vanishingly rare in
|
|
590
|
+
// numeric / space padding. See #1448 Tier B.
|
|
591
|
+
// Full JS arity: `.padStart()` (no target) is `padStart(0)` → the
|
|
592
|
+
// receiver unchanged (JS coerces the missing target to 0), and a
|
|
593
|
+
// third+ argument is ignored. The adapter supplies the `0` for the
|
|
594
|
+
// no-argument form and reads only target + padString.
|
|
595
|
+
if (callee.property === 'padStart' || callee.property === 'padEnd') {
|
|
596
|
+
return { kind: 'array-method', method: callee.property, object: callee.object, args }
|
|
597
|
+
}
|
|
415
598
|
// `.sort(cmp)` / `.toSorted(cmp)` (#1448 Tier B). The comparator
|
|
416
599
|
// is extracted into a structured `SortComparator` at parse time;
|
|
417
600
|
// unrecognised shapes fall through to `unsupported` so adapters
|
|
@@ -168,7 +168,7 @@ export function emitRegistrationAndHydration(
|
|
|
168
168
|
// transformation runs at this layer (#1277).
|
|
169
169
|
const csrInlinableConstants = csrInlinableConstantsFromCtx(ctx)
|
|
170
170
|
const templateHtml = generateCsrTemplate(
|
|
171
|
-
_ir.root, csrInlinableConstants, ctx, undefined, restSpreadNames, ctx.propsObjectName, unsafeLocalNames
|
|
171
|
+
_ir.root, csrInlinableConstants, ctx, undefined, restSpreadNames, ctx.propsObjectName, unsafeLocalNames, ctx.deferredChildSlots
|
|
172
172
|
)
|
|
173
173
|
if (templateHtml) {
|
|
174
174
|
defParts.push(`template: (${PROPS_PARAM}) => \`${templateHtml}\``)
|
|
@@ -14,7 +14,8 @@ import { PROPS_PARAM } from './utils'
|
|
|
14
14
|
import { buildReferencesGraph } from './build-references'
|
|
15
15
|
import { computePropUsage } from './compute-prop-usage'
|
|
16
16
|
import { IMPORT_PLACEHOLDER, MODULE_CONSTANTS_PLACEHOLDER } from './imports'
|
|
17
|
-
import { emitRegistrationAndHydration } from './emit-registration'
|
|
17
|
+
import { emitRegistrationAndHydration, csrInlinableConstantsFromCtx } from './emit-registration'
|
|
18
|
+
import { computeDeferredChildSlots } from './html-template'
|
|
18
19
|
import { emitChildComponentImports } from './child-components'
|
|
19
20
|
import { classifyLocalDeclarations } from './init-declarations'
|
|
20
21
|
import { emitModuleLevelDeclarations, resolveFinalImports } from './emit-module-level'
|
|
@@ -55,6 +56,20 @@ export function generateInitFunction(
|
|
|
55
56
|
// duplicate warnings (#1247).
|
|
56
57
|
const inlinability = buildInlinableConstants(ctx, graph, ir.root)
|
|
57
58
|
|
|
59
|
+
// Decide which direct child components must defer their render to init
|
|
60
|
+
// because a forwarded prop resolves to an init-scope-only / non-inlinable
|
|
61
|
+
// local (dropped-prop fix). The child-init phase reads this set to emit
|
|
62
|
+
// `upsertChild` instead of `initChild`; `emitRegistrationAndHydration`
|
|
63
|
+
// reads it to emit a `data-bf-ph` placeholder instead of
|
|
64
|
+
// `renderChild(...)`. Computed here, once `unsafeLocalNames` is known.
|
|
65
|
+
ctx.deferredChildSlots = computeDeferredChildSlots(
|
|
66
|
+
ir.root,
|
|
67
|
+
ctx,
|
|
68
|
+
csrInlinableConstantsFromCtx(ctx),
|
|
69
|
+
inlinability.unsafeLocalNames,
|
|
70
|
+
ctx.propsObjectName,
|
|
71
|
+
)
|
|
72
|
+
|
|
58
73
|
// --- Emission: declarative phase pipeline. Each entry in `PHASES`
|
|
59
74
|
// declares its inputs (dependsOn) and emission action (run); the
|
|
60
75
|
// stable topological execution preserves the legacy by-position
|