@barefootjs/jsx 0.5.2 → 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/combine-client-js.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 +330 -70
- package/dist/ir-to-client-js/collect-elements.d.ts +26 -14
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts +8 -3
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- 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/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.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/plan/static-array-child-init.d.ts +3 -3
- package/dist/ir-to-client-js/plan/static-array-child-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +36 -4
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +19 -1
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +203 -203
- package/src/__tests__/child-components-in-map.test.ts +333 -0
- package/src/__tests__/combine-client-js.test.ts +47 -0
- package/src/__tests__/dangerously-set-inner-html.test.ts +82 -0
- 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/__tests__/static-loop-csr-materialize.test.ts +6 -4
- package/src/__tests__/text-slot-escaping.test.ts +56 -0
- package/src/adapters/parsed-expr-emitter.ts +7 -0
- package/src/combine-client-js.ts +66 -22
- package/src/expression-parser.ts +200 -17
- package/src/ir-to-client-js/collect-elements.ts +170 -32
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +1 -1
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +2 -1
- package/src/ir-to-client-js/control-flow/plan/event-delegation.ts +8 -3
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +3 -3
- package/src/ir-to-client-js/emit-reactive.ts +9 -0
- 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 +238 -12
- package/src/ir-to-client-js/imports.ts +1 -1
- 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/plan/build-static-array-child-init.ts +4 -8
- package/src/ir-to-client-js/plan/static-array-child-init.ts +3 -3
- package/src/ir-to-client-js/types.ts +37 -4
- package/src/ir-to-client-js/utils.ts +41 -1
|
@@ -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
|
|
@@ -203,8 +203,9 @@ describe('#1247 — static-loop CSR self-heal', () => {
|
|
|
203
203
|
// the inner `it` param).
|
|
204
204
|
expect(clientJs).toMatch(/items\.map\(\(it, i\) =>/)
|
|
205
205
|
// The inner per-item HTML references the inner destructured param
|
|
206
|
-
// directly (`${it}` in the template
|
|
207
|
-
|
|
206
|
+
// directly (`${escapeText(it)}` in the template — text slots are
|
|
207
|
+
// HTML-escaped, #1694), not via a `__bfItem` accessor.
|
|
208
|
+
expect(clientJs).toMatch(/\$\{escapeText\(it\)\}/)
|
|
208
209
|
expect(clientJs).not.toMatch(/__bfItem\(\)/)
|
|
209
210
|
})
|
|
210
211
|
|
|
@@ -335,7 +336,8 @@ describe('#1247 — static-loop CSR self-heal', () => {
|
|
|
335
336
|
// Preamble is NOT duplicated inside the materialize branch.
|
|
336
337
|
expect(materializeBlock).not.toMatch(/const count = users\.length/)
|
|
337
338
|
// The cloned template still references `count` — now resolved by the
|
|
338
|
-
// forEach-scope `const` introduced just above.
|
|
339
|
-
|
|
339
|
+
// forEach-scope `const` introduced just above. Text slots are
|
|
340
|
+
// HTML-escaped (#1694), so it appears as `${escapeText(String(count))}`.
|
|
341
|
+
expect(materializeBlock).toMatch(/\$\{escapeText\(String\(count\)\)\}/)
|
|
340
342
|
})
|
|
341
343
|
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text-slot HTML-escaping emit shape (#1694 + follow-up).
|
|
3
|
+
*
|
|
4
|
+
* Pins which interpolations the client template wraps in `escapeText`:
|
|
5
|
+
* - a plain text slot (`{stringValue}`) IS escaped — it becomes the
|
|
6
|
+
* slot's text content under `innerHTML`;
|
|
7
|
+
* - a branch-slot expression (Child-position value inside a conditional
|
|
8
|
+
* `template()` arrow) is routed through `__bfSlot` and must NOT be
|
|
9
|
+
* wrapped in `escapeText`. `__bfSlot` returns raw `<!--bf-slot:N-->`
|
|
10
|
+
* markers for live nodes; escaping the whole call corrupts them and
|
|
11
|
+
* drops slotted content (the regression that broke `e2e-site-ui`).
|
|
12
|
+
* `__bfSlot` escapes its own plain-string path internally instead.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, test, expect } from 'bun:test'
|
|
16
|
+
import { compileJSX } from '../compiler'
|
|
17
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
18
|
+
|
|
19
|
+
const adapter = new TestAdapter()
|
|
20
|
+
|
|
21
|
+
function getClientJs(source: string, filename: string): string {
|
|
22
|
+
const result = compileJSX(source, filename, { adapter })
|
|
23
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
24
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
25
|
+
expect(clientJs).toBeDefined()
|
|
26
|
+
return clientJs!.content
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('text-slot escaping', () => {
|
|
30
|
+
test('a plain text slot is wrapped in escapeText', () => {
|
|
31
|
+
const clientJs = getClientJs(
|
|
32
|
+
`'use client'
|
|
33
|
+
export function Label({ text }: { text: string }) {
|
|
34
|
+
return <span>{text}</span>
|
|
35
|
+
}`,
|
|
36
|
+
'Label.tsx',
|
|
37
|
+
)
|
|
38
|
+
expect(clientJs).toMatch(/<!--bf:\w+-->\$\{escapeText\(_p\.text\)\}<!--\/-->/)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('a branch-slot expression is NOT wrapped in escapeText', () => {
|
|
42
|
+
const clientJs = getClientJs(
|
|
43
|
+
`'use client'
|
|
44
|
+
import { createSignal } from '@barefootjs/client'
|
|
45
|
+
export function Branch({ show }: { show: boolean }) {
|
|
46
|
+
const [t] = createSignal('hi')
|
|
47
|
+
return <div>{show ? <span>{t()}</span> : null}</div>
|
|
48
|
+
}`,
|
|
49
|
+
'Branch.tsx',
|
|
50
|
+
)
|
|
51
|
+
// The branch value goes through __bfSlot (raw markers preserved)…
|
|
52
|
+
expect(clientJs).toMatch(/\$\{__bfSlot\(/)
|
|
53
|
+
// …and must never be double-wrapped by the text escape.
|
|
54
|
+
expect(clientJs).not.toMatch(/escapeText\(\s*__bfSlot/)
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -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/combine-client-js.ts
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* into the parent's file, eliminating the need for separate HTTP requests.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import ts from 'typescript'
|
|
12
|
+
|
|
11
13
|
const CHILD_PLACEHOLDER_RE = /import '\/\* @bf-child:(\w+) \*\/'/g
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -101,33 +103,75 @@ function parseAndMerge(
|
|
|
101
103
|
otherImports: string[],
|
|
102
104
|
codeSections: string[]
|
|
103
105
|
): void {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
106
|
+
// Parse the client JS so we only ever treat *real* top-level
|
|
107
|
+
// `ImportDeclaration` statements as imports. The predecessor matched
|
|
108
|
+
// raw lines beginning with `import `, which also caught `import …`
|
|
109
|
+
// lines that merely live *inside a string / template literal value*
|
|
110
|
+
// (e.g. a data module exporting a code snippet). Tearing such a line
|
|
111
|
+
// out of its string relocated the component's real runtime import into
|
|
112
|
+
// the literal and left `hydrate` undefined at call time. See
|
|
113
|
+
// piconic-ai/barefootjs#1702.
|
|
114
|
+
// Parent pointers aren't needed here — we only read `statements` and each
|
|
115
|
+
// import's `getStart`/`getEnd` — so skip building them to keep the per-chunk
|
|
116
|
+
// parse cheap when combining many files.
|
|
117
|
+
const sourceFile = ts.createSourceFile(
|
|
118
|
+
'combine.js',
|
|
119
|
+
content,
|
|
120
|
+
ts.ScriptTarget.Latest,
|
|
121
|
+
/*setParentNodes*/ false,
|
|
122
|
+
ts.ScriptKind.JS,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
// Character spans of the top-level imports to strip from the emitted
|
|
126
|
+
// code, so everything that isn't an import (including literals whose
|
|
127
|
+
// contents look like imports) is preserved verbatim.
|
|
128
|
+
const importSpans: Array<[number, number]> = []
|
|
129
|
+
|
|
130
|
+
for (const stmt of sourceFile.statements) {
|
|
131
|
+
if (!ts.isImportDeclaration(stmt)) continue
|
|
132
|
+
const start = stmt.getStart(sourceFile)
|
|
133
|
+
const end = stmt.getEnd()
|
|
134
|
+
importSpans.push([start, end])
|
|
135
|
+
|
|
136
|
+
const stmtText = content.slice(start, end)
|
|
137
|
+
// `@bf-child:` placeholders are resolved by inlining elsewhere; drop
|
|
138
|
+
// them entirely (neither merged nor kept as code).
|
|
139
|
+
if (stmtText.includes('@bf-child:')) continue
|
|
140
|
+
|
|
141
|
+
const clause = stmt.importClause
|
|
142
|
+
const bindings = clause?.namedBindings
|
|
143
|
+
const specifier = ts.isStringLiteral(stmt.moduleSpecifier)
|
|
144
|
+
? stmt.moduleSpecifier.text
|
|
145
|
+
: ''
|
|
146
|
+
if (clause && !clause.name && bindings && ts.isNamedImports(bindings)) {
|
|
147
|
+
// Pure named import (`import { a, b as c } from '…'`) — merge by source.
|
|
148
|
+
if (!importsBySource.has(specifier)) {
|
|
149
|
+
importsBySource.set(specifier, new Set())
|
|
150
|
+
}
|
|
151
|
+
const set = importsBySource.get(specifier)!
|
|
152
|
+
for (const el of bindings.elements) {
|
|
153
|
+
const name = el.propertyName
|
|
154
|
+
? `${el.propertyName.text} as ${el.name.text}`
|
|
155
|
+
: el.name.text
|
|
156
|
+
set.add(name)
|
|
124
157
|
}
|
|
125
158
|
} else {
|
|
126
|
-
|
|
159
|
+
// default / namespace / side-effect import — keep verbatim.
|
|
160
|
+
if (!otherImports.includes(stmtText)) {
|
|
161
|
+
otherImports.push(stmtText)
|
|
162
|
+
}
|
|
127
163
|
}
|
|
128
164
|
}
|
|
129
165
|
|
|
130
|
-
|
|
166
|
+
// Reconstruct the code with the import spans removed.
|
|
167
|
+
let code = ''
|
|
168
|
+
let cursor = 0
|
|
169
|
+
for (const [start, end] of importSpans) {
|
|
170
|
+
code += content.slice(cursor, start)
|
|
171
|
+
cursor = end
|
|
172
|
+
}
|
|
173
|
+
code += content.slice(cursor)
|
|
174
|
+
code = code.trim()
|
|
131
175
|
if (code) {
|
|
132
176
|
codeSections.push(code)
|
|
133
177
|
}
|