@barefootjs/xslate 0.17.0 → 0.18.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/adapter/expr/array-method.d.ts +13 -1
- package/dist/adapter/expr/array-method.d.ts.map +1 -1
- package/dist/adapter/expr/emitters.d.ts +13 -10
- package/dist/adapter/expr/emitters.d.ts.map +1 -1
- package/dist/adapter/index.js +132 -85
- package/dist/adapter/lib/ir-scope.d.ts +0 -5
- package/dist/adapter/lib/ir-scope.d.ts.map +1 -1
- package/dist/adapter/memo/seed.d.ts +14 -14
- package/dist/adapter/memo/seed.d.ts.map +1 -1
- package/dist/adapter/xslate-adapter.d.ts +30 -0
- package/dist/adapter/xslate-adapter.d.ts.map +1 -1
- package/dist/build.js +132 -85
- package/dist/conformance-pins.d.ts +10 -0
- package/dist/conformance-pins.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +151 -85
- package/lib/BarefootJS/Backend/Xslate.pm +1 -1
- package/package.json +3 -3
- package/src/__tests__/xslate-adapter.test.ts +138 -91
- package/src/adapter/expr/array-method.ts +43 -1
- package/src/adapter/expr/emitters.ts +53 -23
- package/src/adapter/lib/ir-scope.ts +0 -11
- package/src/adapter/memo/seed.ts +25 -72
- package/src/adapter/xslate-adapter.ts +254 -52
- package/src/conformance-pins.ts +112 -0
- package/src/index.ts +1 -0
|
@@ -13,13 +13,11 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { describe, test, expect } from 'bun:test'
|
|
16
|
-
import {
|
|
17
|
-
runAdapterConformanceTests,
|
|
18
|
-
TemplatePrimitiveCaseId,
|
|
19
|
-
} from '@barefootjs/adapter-tests'
|
|
16
|
+
import { runAdapterConformanceTests } from '@barefootjs/adapter-tests'
|
|
20
17
|
import { XslateAdapter } from '../adapter'
|
|
21
18
|
import { renderXslateComponent, XslateNotAvailableError } from '../test-render'
|
|
22
19
|
import { compileJSX, type ComponentIR } from '@barefootjs/jsx'
|
|
20
|
+
import { conformancePins } from '../conformance-pins'
|
|
23
21
|
|
|
24
22
|
runAdapterConformanceTests({
|
|
25
23
|
name: 'xslate',
|
|
@@ -33,91 +31,26 @@ runAdapterConformanceTests({
|
|
|
33
31
|
// body children (TableCell) now receive `_bf_slot` for deterministic
|
|
34
32
|
// parent-scope-derived IDs matching Hono.
|
|
35
33
|
// Per-fixture build-time contracts for shapes the Xslate adapter
|
|
36
|
-
// intentionally refuses to lower.
|
|
37
|
-
// gates are shared code paths in
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
'static-array-from-props': [{ code: 'BF104', severity: 'error' }],
|
|
53
|
-
// Both BF103 (imported child) and BF104 (destructure) fire.
|
|
54
|
-
'static-array-from-props-with-component': [
|
|
55
|
-
{ code: 'BF103', severity: 'error' },
|
|
56
|
-
{ code: 'BF104', severity: 'error' },
|
|
57
|
-
],
|
|
58
|
-
// Rest-destructure `.map()` callbacks — the object-rest shape read via
|
|
59
|
-
// member access (`rest-destructure-object-in-map`) now lowers via Kolon
|
|
60
|
-
// `: my` binding locals (`$rest` aliases the item). The other three stay
|
|
61
|
-
// refused: rest SPREAD needs a residual object, array-index / nested paths
|
|
62
|
-
// can't unpack a tuple (same surface as mojo).
|
|
63
|
-
'rest-destructure-object-spread-in-map': [{ code: 'BF104', severity: 'error' }],
|
|
64
|
-
'rest-destructure-array-in-map': [{ code: 'BF104', severity: 'error' }],
|
|
65
|
-
'rest-destructure-nested-in-map': [{ code: 'BF104', severity: 'error' }],
|
|
66
|
-
// XSLATE-SPECIFIC (mojo passes this): the site/ui Button auto-infers a
|
|
67
|
-
// `<Slot>` sibling that spreads `{...props}` / `{...children.props}`
|
|
68
|
-
// onto its root element. Kolon hashref method args can't splat a
|
|
69
|
-
// runtime hash into named entries (no `%$h`-into-call-args form), so
|
|
70
|
-
// the adapter refuses the spread with BF101 rather than emit a broken
|
|
71
|
-
// render_child call. Mojo's EP `%= include` accepts a flat stash, so it
|
|
72
|
-
// lowers the same shape; this is a genuine engine divergence, pinned
|
|
73
|
-
// declaratively here.
|
|
74
|
-
'button': [{ code: 'BF101', severity: 'error' }],
|
|
75
|
-
// `kbd` auto-infers the same `<Slot>` `{...props}` spread as `button`
|
|
76
|
-
// above — refused with BF101 for the identical Kolon engine reason, not a
|
|
77
|
-
// render-mismatch (so it's pinned here, not in `skipJsx`).
|
|
78
|
-
'kbd': [{ code: 'BF101', severity: 'error' }],
|
|
79
|
-
// #1467 demo-corpus context providers (`radio-group`, `select`,
|
|
80
|
-
// `dropdown-menu`, `combobox`, `command`) are no longer pinned — an
|
|
81
|
-
// object-literal provider value (`{ value: currentValue,
|
|
82
|
-
// onValueChange: (v) => {…} }`) lowers to a Kolon hashref via
|
|
83
|
-
// `parseProviderObjectLiteral` (#1897): getter members snapshot
|
|
84
|
-
// their body's SSR value, handler / function-shaped members lower
|
|
85
|
-
// to `nil`. The command demo's `ref={(el) => {…}}` function prop on
|
|
86
|
-
// an imported component is skipped at SSR like `on*` handlers.
|
|
87
|
-
//
|
|
88
|
-
// #1467 Phase 2e: `data-table` is no longer pinned here — it
|
|
89
|
-
// compiles clean now (`selected()[index]` → `index-access`,
|
|
90
|
-
// `.toFixed(2)` → `$bf.to_fixed`, `/* @client */` memo SSR-folded)
|
|
91
|
-
// and renders to Hono parity on real Text::Xslate. The keyed-loop
|
|
92
|
-
// scope-ID divergence (#1896) was fixed by the body-children
|
|
93
|
-
// `inLoop` reset (loop-item children get `_bf_slot`); data-table is
|
|
94
|
-
// off `skipJsx` entirely and only kept in `skipMarkerConformance`
|
|
95
|
-
// below for the shared `/* @client */` keyed-map slot-id elision
|
|
96
|
-
// contract (same as `todo-app`), not a render or BF101 gap.
|
|
97
|
-
// `style-3-signals` / `style-object-dynamic` no longer pinned — a
|
|
98
|
-
// `style={{ … }}` object literal now lowers to a CSS string with dynamic
|
|
99
|
-
// values interpolated (`background-color:<: $color :>;padding:8px`) via
|
|
100
|
-
// `tryLowerStyleObject` (#1322).
|
|
101
|
-
// Tagged-template-literal call in a className — same family, same
|
|
102
|
-
// refusal (BF101).
|
|
103
|
-
'tagged-template-classname': [{ code: 'BF101', severity: 'error' }],
|
|
104
|
-
// NB: `.find` / `.findIndex` / `.findLast` / `.findLastIndex` are NOT
|
|
105
|
-
// pinned here — unlike mojo (which refuses them), Xslate lowers them to
|
|
106
|
-
// `$bf.find` / `find_index` / `find_last` / `find_last_index` via the same
|
|
107
|
-
// Kolon-lambda mechanism as `.filter` / `.every` / `.some`, so they render.
|
|
108
|
-
},
|
|
109
|
-
// Template-primitive registry parity: same V1 surface as mojo, so the
|
|
110
|
-
// same two cases stay skipped (bespoke user import + customSerialize
|
|
111
|
-
// can't render server-side without user-supplied helper mappings).
|
|
112
|
-
skipTemplatePrimitives: new Set([
|
|
113
|
-
TemplatePrimitiveCaseId.USER_IMPORT_VIA_CONST,
|
|
114
|
-
TemplatePrimitiveCaseId.NO_DOUBLE_REWRITE_OF_PROPS_OBJECT,
|
|
115
|
-
]),
|
|
116
|
-
// Loop boundary markers for `@client` loops aren't emitted by the
|
|
117
|
-
// Xslate adapter yet (ported from mojo, which skips the same set).
|
|
34
|
+
// intentionally refuses to lower. Lives in `../conformance-pins` —
|
|
35
|
+
// mirrors mojo's set (the lowering gates are shared code paths in
|
|
36
|
+
// the ported adapter).
|
|
37
|
+
expectedDiagnostics: conformancePins,
|
|
38
|
+
// Template-primitive registry: `USER_IMPORT_VIA_CONST` and
|
|
39
|
+
// `NO_DOUBLE_REWRITE_OF_PROPS_OBJECT` now pass (#2069) — a bespoke user
|
|
40
|
+
// import can never be added to the string-keyed registry, but the
|
|
41
|
+
// shared `RelocateEnv.loweringMatchers` acceptance path recognises it
|
|
42
|
+
// via a `LoweringPlugin` the case setup registers around the compile
|
|
43
|
+
// (see `packages/adapter-tests/src/cases/template-primitives.ts`). No
|
|
44
|
+
// skips left, so `skipTemplatePrimitives` is omitted entirely.
|
|
45
|
+
// `client-only` / `client-only-loop-with-sibling-cond` /
|
|
46
|
+
// `filter-nested-callback-predicate-client` are no longer skipped —
|
|
47
|
+
// `renderLoop` now emits the `$bf.comment("loop:<id>")` boundary pair
|
|
48
|
+
// for clientOnly loops (Hono / Go parity), so mapArray() can locate
|
|
49
|
+
// its insertion anchor at hydration time (#872 / #1087).
|
|
118
50
|
skipMarkerConformance: new Set([
|
|
119
|
-
|
|
120
|
-
|
|
51
|
+
// Same as Hono / Mojo: `/* @client */` markers on TodoApp's keyed
|
|
52
|
+
// `.map` intentionally elide a slot id from the SSR template that
|
|
53
|
+
// the IR still declares (s6). See hono-adapter.test for the contract.
|
|
121
54
|
'todo-app',
|
|
122
55
|
// #1467 Phase 2e: same `/* @client */` keyed-map elision (data-table).
|
|
123
56
|
'data-table',
|
|
@@ -296,6 +229,98 @@ export function C() {
|
|
|
296
229
|
// (`$bf.*_eval`), isomorphic with the Go / Mojo `*_eval` helpers. A predicate
|
|
297
230
|
// the evaluator can't model (a method-call predicate) falls back to the Kolon
|
|
298
231
|
// lambda runtime call. Template-text pins guard against silent divergence.
|
|
232
|
+
describe('XslateAdapter - #2075 searchParams()-derived memo seeding', () => {
|
|
233
|
+
test('seeds an aliased scalar derived memo from the canonical reader', () => {
|
|
234
|
+
const { template } = compileAndGenerate(`
|
|
235
|
+
'use client'
|
|
236
|
+
import { createMemo, createSearchParams } from '@barefootjs/client'
|
|
237
|
+
export function SortStatus() {
|
|
238
|
+
const [sp] = createSearchParams()
|
|
239
|
+
const sort = createMemo(() => sp().get('sort') ?? 'date')
|
|
240
|
+
return <p>sort: {sort()}</p>
|
|
241
|
+
}
|
|
242
|
+
`)
|
|
243
|
+
expect(template).toContain(": my $sort = ($searchParams.get('sort') // 'date');")
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
// The Kolon lambda param and the `$bf` runtime object are
|
|
247
|
+
// lowering-internal, not out-of-scope template vars (#2075).
|
|
248
|
+
test('seeds a filter memo chained off the derived memo', () => {
|
|
249
|
+
const { template } = compileAndGenerate(`
|
|
250
|
+
'use client'
|
|
251
|
+
import { createMemo, createSearchParams } from '@barefootjs/client'
|
|
252
|
+
export function TaggedList(props: { items: { title: string; tags: string[] }[] }) {
|
|
253
|
+
const [searchParams] = createSearchParams()
|
|
254
|
+
const tag = createMemo(() => searchParams().get('tag') ?? '')
|
|
255
|
+
const visible = createMemo(() => props.items.filter((p) => !tag() || p.tags.includes(tag())))
|
|
256
|
+
return <ul>{visible().map((p) => <li key={p.title}>{p.title}</li>)}</ul>
|
|
257
|
+
}
|
|
258
|
+
`)
|
|
259
|
+
expect(template).toContain(": my $tag = ($searchParams.get('tag') // '');")
|
|
260
|
+
expect(template).toContain(': my $visible = $bf.filter($items,')
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
// The seed-scope guard used to scan the LOWERED
|
|
264
|
+
// Kolon string, allowing every arrow-callback param tree-wide. That let an
|
|
265
|
+
// outer, unbound `p` (shadowed only inside the callback) slip past the
|
|
266
|
+
// guard as if it were the callback's own bound `$p` — emitting a bogus
|
|
267
|
+
// seed line. The guard now walks the parsed SOURCE tree with proper
|
|
268
|
+
// lexical scoping (`freeIdentifiers`), so this shape seeds nothing and
|
|
269
|
+
// falls back to the null/ssr-defaults path.
|
|
270
|
+
test('an outer unbound `p` shadowed only inside the callback does not seed', () => {
|
|
271
|
+
const { template } = compileAndGenerate(`
|
|
272
|
+
'use client'
|
|
273
|
+
import { createMemo } from '@barefootjs/client'
|
|
274
|
+
export function C(props: { items: { ok: boolean }[] }) {
|
|
275
|
+
const visible = createMemo(() => props.items.filter((p) => p.ok) && p)
|
|
276
|
+
return <div>{String(visible())}</div>
|
|
277
|
+
}
|
|
278
|
+
`)
|
|
279
|
+
expect(template).not.toContain('my $visible')
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
// An out-of-scope bare `_` reference must not seed either — the old
|
|
283
|
+
// unconditional `allowed.add('_')` / `allowed.add('bf')` masked this.
|
|
284
|
+
test('an out-of-scope bare `_` reference does not seed', () => {
|
|
285
|
+
const { template } = compileAndGenerate(`
|
|
286
|
+
'use client'
|
|
287
|
+
import { createMemo } from '@barefootjs/client'
|
|
288
|
+
export function C(props: { count: number }) {
|
|
289
|
+
const doubled = createMemo(() => props.count * 2 + _)
|
|
290
|
+
return <div>{doubled()}</div>
|
|
291
|
+
}
|
|
292
|
+
`)
|
|
293
|
+
expect(template).not.toContain('my $doubled')
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
describe('XslateAdapter - #2073 value-producing .map(cb)', () => {
|
|
298
|
+
// The blog-showcase shape (#1938/#1939): a value-returning `.map` (string
|
|
299
|
+
// projection, not JSX) lowers through the evaluator — `$bf.map_eval`
|
|
300
|
+
// projects each element (no flatten) and composes through `$bf.join`.
|
|
301
|
+
test('.map(t => `#${t}`).join(" ") emits $bf.map_eval composed into $bf.join', () => {
|
|
302
|
+
const { template } = compileAndGenerate(`
|
|
303
|
+
function TagLine({ tags }: { tags: string[] }) {
|
|
304
|
+
return <p>{tags.map((t) => \`#\${t}\`).join(' ')}</p>
|
|
305
|
+
}
|
|
306
|
+
export { TagLine }
|
|
307
|
+
`)
|
|
308
|
+
expect(template).toContain("$bf.join($bf.map_eval($tags,")
|
|
309
|
+
expect(template).toContain('"kind":"template-literal"')
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
test('.map(u => u.name) emits $bf.map_eval with the field projection', () => {
|
|
313
|
+
const { template } = compileAndGenerate(`
|
|
314
|
+
function NameList({ users }: { users: { name: string }[] }) {
|
|
315
|
+
return <div>{users.map((u) => u.name).join(', ')}</div>
|
|
316
|
+
}
|
|
317
|
+
export { NameList }
|
|
318
|
+
`)
|
|
319
|
+
expect(template).toContain('$bf.map_eval($users,')
|
|
320
|
+
expect(template).toContain('"property":"name"')
|
|
321
|
+
})
|
|
322
|
+
})
|
|
323
|
+
|
|
299
324
|
describe('XslateAdapter - higher-order predicate lowering (#2018 P2)', () => {
|
|
300
325
|
test('a serializable predicate lowers to $bf.filter_eval with the JSON body + env', () => {
|
|
301
326
|
// A standalone `.filter().length` exercises the higher-order emitter (the
|
|
@@ -332,17 +357,39 @@ export { A }
|
|
|
332
357
|
expect(findLast).toContain(', 0, {})')
|
|
333
358
|
})
|
|
334
359
|
|
|
335
|
-
test('a
|
|
360
|
+
test('.includes() in a predicate now lowers via the evaluator, not the Kolon-lambda fallback', () => {
|
|
361
|
+
// #2075: `.includes(x)` joined the evaluator's `array-method` surface
|
|
362
|
+
// (shared with the Perl `Evaluator.pm` runtime), so a predicate built
|
|
363
|
+
// from it routes through `$bf.every_eval` like any other pure predicate.
|
|
336
364
|
const { template } = compileAndGenerate(`
|
|
337
365
|
function A({ items }: { items: { name: string }[] }) {
|
|
338
366
|
return <div>{items.every(x => x.name.includes('a')) ? 'y' : 'n'}</div>
|
|
339
367
|
}
|
|
340
368
|
export { A }
|
|
341
369
|
`)
|
|
342
|
-
|
|
343
|
-
|
|
370
|
+
expect(template).toContain('$bf.every_eval(')
|
|
371
|
+
expect(template).toContain('"method":"includes"')
|
|
372
|
+
expect(template).not.toContain('-> $x {')
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
test('a method-call predicate outside the evaluator surface falls back to the Kolon-lambda runtime call', () => {
|
|
376
|
+
const { template } = compileAndGenerate(`
|
|
377
|
+
function A({ items }: { items: { name: string }[] }) {
|
|
378
|
+
return <div>{items.every(x => x.name.toUpperCase() === 'A') ? 'y' : 'n'}</div>
|
|
379
|
+
}
|
|
380
|
+
export { A }
|
|
381
|
+
`)
|
|
382
|
+
// `.toUpperCase()` is outside the evaluator's `array-method` gate (only
|
|
383
|
+
// `includes` is recognized there), so the predicate keeps the
|
|
384
|
+
// `-> $x { … }` lambda form passed to the runtime `$bf.every`.
|
|
344
385
|
expect(template).not.toContain('every_eval')
|
|
345
386
|
expect(template).toContain('$bf.every(')
|
|
346
387
|
expect(template).toContain('-> $x {')
|
|
347
388
|
})
|
|
348
389
|
})
|
|
390
|
+
|
|
391
|
+
// #2038 nested-callback-predicate loudness is pinned at the shared
|
|
392
|
+
// conformance layer: `filter-nested-callback-predicate` /
|
|
393
|
+
// `filter-nested-find-predicate` (BF101 via `expectedDiagnostics` above) and
|
|
394
|
+
// `filter-nested-callback-predicate-client` (the `/* @client */` suppression
|
|
395
|
+
// twin, which must render clean).
|
|
@@ -287,6 +287,27 @@ export function renderFlatMapEval(
|
|
|
287
287
|
return `$bf.flat_map_eval(${recv}, '${escapePerlSingleQuote(json)}', '${param}', ${env})`
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Emit a value-producing `.map(cb)` via the runtime evaluator (#2073): the
|
|
292
|
+
* projection body serializes to JSON and `$bf.map_eval` projects each element,
|
|
293
|
+
* one result per element (no flatten — the JS `.map` contract). Composes
|
|
294
|
+
* through the array-method chain (`.map(cb).join(' ')`). Returns null when
|
|
295
|
+
* the projection is outside the evaluator surface (→ caller refuses with
|
|
296
|
+
* BF101). The JSX-returning `.map` is an IRLoop upstream and never reaches
|
|
297
|
+
* this emit.
|
|
298
|
+
*/
|
|
299
|
+
export function renderMapEval(
|
|
300
|
+
recv: string,
|
|
301
|
+
body: ParsedExpr,
|
|
302
|
+
param: string,
|
|
303
|
+
emit: (e: ParsedExpr) => string,
|
|
304
|
+
): string | null {
|
|
305
|
+
const json = serializeParsedExpr(body)
|
|
306
|
+
if (json === null) return null
|
|
307
|
+
const env = emitEvalEnvArg(body, [param], emit)
|
|
308
|
+
return `$bf.map_eval(${recv}, '${escapePerlSingleQuote(json)}', '${param}', ${env})`
|
|
309
|
+
}
|
|
310
|
+
|
|
290
311
|
/**
|
|
291
312
|
* Shared Kolon emit for `.sort(cmp)` / `.toSorted(cmp)`. Used by both the
|
|
292
313
|
* filter-context emitter and the top-level emitter, plus the loop-array
|
|
@@ -305,7 +326,28 @@ export function renderSortMethod(recv: string, c: SortComparator): string {
|
|
|
305
326
|
}
|
|
306
327
|
|
|
307
328
|
// `.flat(depth?)` → `$bf.flat($recv, $depth)`.
|
|
308
|
-
export function renderFlatMethod(
|
|
329
|
+
export function renderFlatMethod(
|
|
330
|
+
recv: string,
|
|
331
|
+
depth: FlatDepth | { expr: ParsedExpr },
|
|
332
|
+
emit: (e: ParsedExpr) => string,
|
|
333
|
+
): string {
|
|
334
|
+
if (typeof depth === 'object') {
|
|
335
|
+
// Dynamic depth (#2094): routed to a SEPARATE runtime helper
|
|
336
|
+
// (`$bf.flat_dynamic`), not `$bf.flat`. The literal-depth path below
|
|
337
|
+
// already uses `-1` as a compile-time SENTINEL baked into the template
|
|
338
|
+
// source meaning "the source literally said `Infinity`". A genuinely
|
|
339
|
+
// dynamic depth that happens to evaluate to `-1` at render time means
|
|
340
|
+
// the OPPOSITE in real JS (`[1,[2]].flat(-1)` never recurses — same as
|
|
341
|
+
// `.flat(0)`). Since both paths would otherwise hand the same
|
|
342
|
+
// literal-looking argument to one shared helper, that helper couldn't
|
|
343
|
+
// tell which case it's in — so `flat_dynamic` performs its own
|
|
344
|
+
// `ToIntegerOrInfinity`-style coercion (truncate toward zero; negative
|
|
345
|
+
// -> 0; NaN/non-numeric -> 0; +Infinity or a huge finite value ->
|
|
346
|
+
// flatten fully) and then delegates to `flat`. See `sub flat_dynamic`
|
|
347
|
+
// in BarefootJS.pm and the Go reference (`FlatDynamicDepth`/
|
|
348
|
+
// `coerceFlatDepth` in bf.go).
|
|
349
|
+
return `$bf.flat_dynamic(${recv}, ${emit(depth.expr)})`
|
|
350
|
+
}
|
|
309
351
|
const d = depth === 'infinity' ? -1 : depth
|
|
310
352
|
return `$bf.flat(${recv}, ${d})`
|
|
311
353
|
}
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
renderPredicateEval,
|
|
36
36
|
renderFlatMethod,
|
|
37
37
|
renderFlatMapEval,
|
|
38
|
+
renderMapEval,
|
|
38
39
|
} from './array-method.ts'
|
|
39
40
|
|
|
40
41
|
/**
|
|
@@ -62,18 +63,20 @@ const PREDICATE_METHODS = new Set<HigherOrderMethod>([
|
|
|
62
63
|
* filters. Higher-order predicates are emitted using Kolon's own scalar
|
|
63
64
|
* comparison operators (which delegate to Perl semantics).
|
|
64
65
|
*
|
|
65
|
-
* NOTE: Kolon has no `grep { } @{...}` form, so nested higher-order
|
|
66
|
-
* (`x.tags.filter(...).
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* use; richer nested shapes fall back to the helper or surface as BF101 via
|
|
70
|
-
* the top-level emitter.
|
|
66
|
+
* NOTE: Kolon has no `grep { } @{...}` form, so a nested higher-order call
|
|
67
|
+
* (`x.tags.filter(...)`, `other.some(...)`) inside a predicate has no faithful
|
|
68
|
+
* scalar lowering here. Such predicates surface BF101 via `onUnsupported`
|
|
69
|
+
* (#2038) instead of silently degrading to the callback's receiver.
|
|
71
70
|
*/
|
|
72
71
|
export class XslateFilterEmitter implements ParsedExprEmitter {
|
|
73
72
|
constructor(
|
|
74
73
|
private readonly param: string,
|
|
75
74
|
private readonly localVarMap: Map<string, string>,
|
|
76
75
|
private readonly isStringName: (n: string) => boolean = () => false,
|
|
76
|
+
// Records a BF101 for predicate shapes this emitter can only degrade
|
|
77
|
+
// (#2038). Optional so emitter construction stays possible without an
|
|
78
|
+
// adapter; a missing hook keeps the old silent-degrade emit.
|
|
79
|
+
private readonly onUnsupported?: (message: string, reason?: string) => void,
|
|
77
80
|
) {}
|
|
78
81
|
|
|
79
82
|
identifier(name: string): string {
|
|
@@ -155,12 +158,15 @@ export class XslateFilterEmitter implements ParsedExprEmitter {
|
|
|
155
158
|
_restArgs: ParsedExpr[],
|
|
156
159
|
emit: (e: ParsedExpr) => string,
|
|
157
160
|
): string {
|
|
158
|
-
//
|
|
159
|
-
// form
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
|
|
161
|
+
// A nested callback method inside a filter predicate has no Kolon scalar
|
|
162
|
+
// form. The pre-#2038 behavior degraded it to its receiver, which silently
|
|
163
|
+
// changes predicate semantics (`!other.some(r => …)` collapses to
|
|
164
|
+
// `!other`), so surface BF101 instead. The receiver emit is kept only so
|
|
165
|
+
// the template stays syntactically valid while the build fails.
|
|
166
|
+
this.onUnsupported?.(
|
|
167
|
+
`Filter predicate contains a nested '.${method}(...)' callback, which has no Kolon scalar form`,
|
|
168
|
+
`Rewrite the predicate without a nested callback method, or add /* @client */ for client-only evaluation (no SSR).`,
|
|
169
|
+
)
|
|
164
170
|
return emit(object)
|
|
165
171
|
}
|
|
166
172
|
|
|
@@ -177,8 +183,12 @@ export class XslateFilterEmitter implements ParsedExprEmitter {
|
|
|
177
183
|
return renderArrayMethod(method, object, args, emit)
|
|
178
184
|
}
|
|
179
185
|
|
|
180
|
-
flatMethod(
|
|
181
|
-
|
|
186
|
+
flatMethod(
|
|
187
|
+
object: ParsedExpr,
|
|
188
|
+
depth: FlatDepth | { expr: ParsedExpr },
|
|
189
|
+
emit: (e: ParsedExpr) => string,
|
|
190
|
+
): string {
|
|
191
|
+
return renderFlatMethod(emit(object), depth, emit)
|
|
182
192
|
}
|
|
183
193
|
|
|
184
194
|
conditional(_test: ParsedExpr, _consequent: ParsedExpr, _alternate: ParsedExpr): string {
|
|
@@ -398,6 +408,18 @@ export class XslateTopLevelEmitter implements ParsedExprEmitter {
|
|
|
398
408
|
return "''"
|
|
399
409
|
}
|
|
400
410
|
|
|
411
|
+
// Value-producing `.map(cb)` (#2073): serialize the projection body +
|
|
412
|
+
// emit `$bf.map_eval`. (The JSX-returning `.map` is an IRLoop upstream.)
|
|
413
|
+
if (method === 'map') {
|
|
414
|
+
const evalForm = renderMapEval(recv, body, params[0], emit)
|
|
415
|
+
if (evalForm !== null) return evalForm
|
|
416
|
+
this.ctx._recordExprBF101(
|
|
417
|
+
`'.map(...)' projection is outside the Xslate adapter's evaluable surface`,
|
|
418
|
+
`Pre-compute the projected array, or move this position to a '/* @client */' boundary.`,
|
|
419
|
+
)
|
|
420
|
+
return "''"
|
|
421
|
+
}
|
|
422
|
+
|
|
401
423
|
// Unknown callback method (should not arrive — CALLBACK_METHODS is closed).
|
|
402
424
|
void object
|
|
403
425
|
return recv
|
|
@@ -468,8 +490,12 @@ export class XslateTopLevelEmitter implements ParsedExprEmitter {
|
|
|
468
490
|
return renderArrayMethod(method, object, args, emit)
|
|
469
491
|
}
|
|
470
492
|
|
|
471
|
-
flatMethod(
|
|
472
|
-
|
|
493
|
+
flatMethod(
|
|
494
|
+
object: ParsedExpr,
|
|
495
|
+
depth: FlatDepth | { expr: ParsedExpr },
|
|
496
|
+
emit: (e: ParsedExpr) => string,
|
|
497
|
+
): string {
|
|
498
|
+
return renderFlatMethod(emit(object), depth, emit)
|
|
473
499
|
}
|
|
474
500
|
|
|
475
501
|
conditional(
|
|
@@ -519,12 +545,16 @@ export class XslateTopLevelEmitter implements ParsedExprEmitter {
|
|
|
519
545
|
return "''"
|
|
520
546
|
}
|
|
521
547
|
|
|
522
|
-
objectLiteral(
|
|
523
|
-
//
|
|
524
|
-
//
|
|
525
|
-
//
|
|
526
|
-
//
|
|
527
|
-
//
|
|
528
|
-
|
|
548
|
+
objectLiteral(properties: ObjectLiteralProperty[], _raw: string, _emit: (e: ParsedExpr) => string): string {
|
|
549
|
+
// The shared `isSupported` gate only ever lets this dispatcher see an
|
|
550
|
+
// object literal as the EMPTY (`?? {}`) fallback operand of `??`
|
|
551
|
+
// (expression-parser.ts, `logical` case) — any other object literal is
|
|
552
|
+
// refused before reaching here. Emit Kolon's real empty hashref
|
|
553
|
+
// literal, matching the `'{}'` convention `objectLiteralToKolonHashref`
|
|
554
|
+
// already uses for the zero-property case in the spread path. A
|
|
555
|
+
// populated literal is structurally unreachable given the gate, but
|
|
556
|
+
// still degrades safely to the pre-existing empty-string sentinel
|
|
557
|
+
// rather than silently dropping keys.
|
|
558
|
+
return properties.length === 0 ? '{}' : "''"
|
|
529
559
|
}
|
|
530
560
|
}
|
|
@@ -51,14 +51,3 @@ export function collectRootScopeNodes(node: IRNode): Set<IRNode> {
|
|
|
51
51
|
visit(node)
|
|
52
52
|
return out
|
|
53
53
|
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* True when every `$var` the lowered Kolon expression references is already in
|
|
57
|
-
* scope — guards in-template memo seeding against an out-of-scope binding. (#1297)
|
|
58
|
-
*/
|
|
59
|
-
export function referencedVarsAreAvailable(expr: string, available: ReadonlySet<string>): boolean {
|
|
60
|
-
for (const m of expr.matchAll(/\$([A-Za-z_]\w*)/g)) {
|
|
61
|
-
if (!available.has(m[1])) return false
|
|
62
|
-
}
|
|
63
|
-
return true
|
|
64
|
-
}
|
package/src/adapter/memo/seed.ts
CHANGED
|
@@ -13,13 +13,10 @@ import {
|
|
|
13
13
|
type ComponentIR,
|
|
14
14
|
type ContextConsumer,
|
|
15
15
|
collectContextConsumers,
|
|
16
|
-
|
|
17
|
-
isSupported,
|
|
18
|
-
parseExpression,
|
|
16
|
+
computeSsrSeedPlan,
|
|
19
17
|
} from '@barefootjs/jsx'
|
|
20
18
|
|
|
21
19
|
import type { XslateMemoContext } from '../emit-context.ts'
|
|
22
|
-
import { referencedVarsAreAvailable } from '../lib/ir-scope.ts'
|
|
23
20
|
|
|
24
21
|
/** Kolon literal for a context-consumer's `createContext` default. */
|
|
25
22
|
export function contextDefaultKolon(c: ContextConsumer): string {
|
|
@@ -49,77 +46,33 @@ export function generateContextConsumerSeed(ir: ComponentIR): string {
|
|
|
49
46
|
}
|
|
50
47
|
|
|
51
48
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* this
|
|
56
|
-
*
|
|
57
|
-
*
|
|
49
|
+
* Emit `: my $<name> = <kolon>;` line-statements for every `derived` step of
|
|
50
|
+
* the backend-neutral SSR seed plan — the scope/availability/ordering
|
|
51
|
+
* analysis lives in `computeSsrSeedPlan` (packages/jsx/src/ssr-seed-plan.ts);
|
|
52
|
+
* this only lowers each step's expression to Kolon and applies the
|
|
53
|
+
* backend-specific emit guards: skip an empty lowering, skip a lowering that
|
|
54
|
+
* references no `$var` at all (a constant init/body — e.g. a `derived` step
|
|
55
|
+
* with empty `frees` — keeps the existing static ssr-defaults seed instead),
|
|
56
|
+
* and skip a self-referencing lowering (Kolon's `my` shadows the RHS, so
|
|
57
|
+
* `: my $x = … $x …` would read the just-declared undefined lexical rather
|
|
58
|
+
* than the render var — the plan rules out SOURCE-level self-refs, but a
|
|
59
|
+
* lowered canonical name could still collide, so this stays as the cheap
|
|
60
|
+
* defense it always was). `env-reader` and `opaque` steps emit nothing (the
|
|
61
|
+
* runtime supplies the reader, or the adapter's ssr-defaults path already
|
|
62
|
+
* covers it). (#1297, #2075)
|
|
58
63
|
*/
|
|
59
64
|
export function generateDerivedMemoSeed(ctx: XslateMemoContext, ir: ComponentIR): string {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const available = new Set<string>(ir.metadata.propsParams.map(p => p.name))
|
|
65
|
+
// Package G attached this to metadata at compile time; the `??` fallback
|
|
66
|
+
// only covers hand-built metadata in older tests that predate the attached
|
|
67
|
+
// plan — same shared function, so there's no divergence from the compiler.
|
|
68
|
+
const plan = ir.metadata.ssrSeedPlan ?? computeSsrSeedPlan(ir.metadata)
|
|
65
69
|
const lines: string[] = []
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
for (const signal of signals) {
|
|
73
|
-
const kolon = tryLowerToKolon(ctx, signal.initialValue, available)
|
|
74
|
-
// Kolon can't express `: my $x = … $x …` — declaring `my $x` makes the
|
|
75
|
-
// RHS `$x` an undefined lexical rather than the render var. A same-name
|
|
76
|
-
// signal (`createSignal(props.x ?? d)`, getter == prop) is just the prop
|
|
77
|
-
// with a default, which the harness already seeds correctly from the
|
|
78
|
-
// passed prop — skip the in-template seed for it. (Different-name
|
|
79
|
-
// prop-derived signals like toggle's `on` from `defaultOn` are unaffected.)
|
|
80
|
-
const refsSelf = kolon !== null && new RegExp(`\\$${signal.getter}\\b`).test(kolon)
|
|
81
|
-
if (kolon !== null && !refsSelf) lines.push(`: my $${signal.getter} = ${kolon};`)
|
|
82
|
-
available.add(signal.getter)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
for (const memo of memos) {
|
|
86
|
-
// Seed every memo whose body lowers cleanly — not just the ones whose
|
|
87
|
-
// static SSR default is null. A statically-foldable prop-derived memo
|
|
88
|
-
// (`createMemo(() => props.disabled ?? false)` → default `false`)
|
|
89
|
-
// still depends on the per-call prop: the static stash seed bakes in
|
|
90
|
-
// the absent-prop fold, so a caller passing `disabled => 1` would
|
|
91
|
-
// render the default branch (#1897, select's disabled item). The
|
|
92
|
-
// in-template recomputation reads the prop lexical already in scope;
|
|
93
|
-
// block-bodied arrows / out-of-scope references fall back to the
|
|
94
|
-
// static ssr-defaults seed. Same self-reference guard as the signal
|
|
95
|
-
// loop above — Kolon's `my` shadows the render var on the RHS.
|
|
96
|
-
const body = extractArrowBodyExpression(memo.computation)
|
|
97
|
-
if (body !== null) {
|
|
98
|
-
const kolon = tryLowerToKolon(ctx, body, available)
|
|
99
|
-
const refsSelf = kolon !== null && new RegExp(`\\$${memo.name}\\b`).test(kolon)
|
|
100
|
-
if (kolon !== null && !refsSelf) lines.push(`: my $${memo.name} = ${kolon};`)
|
|
101
|
-
}
|
|
102
|
-
available.add(memo.name)
|
|
70
|
+
for (const step of plan.steps) {
|
|
71
|
+
if (step.kind !== 'derived') continue
|
|
72
|
+
const kolon = ctx.convertExpressionToKolon(step.expr, step.parsed)
|
|
73
|
+
if (kolon === '' || !/\$[A-Za-z_]\w*/.test(kolon)) continue
|
|
74
|
+
if (new RegExp(`\\$${step.name}\\b`).test(kolon)) continue
|
|
75
|
+
lines.push(`: my $${step.name} = ${kolon};`)
|
|
103
76
|
}
|
|
104
77
|
return lines.length > 0 ? lines.join('\n') + '\n' : ''
|
|
105
78
|
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Lower a signal init / memo body to Kolon for an in-template SSR seed, or
|
|
109
|
-
* `null` when it shouldn't be seeded this way: not a supported shape
|
|
110
|
-
* (`isSupported` pre-check, so object/array literals don't fail the build),
|
|
111
|
-
* references no in-scope var (a constant — keep ssr-defaults seeding), or
|
|
112
|
-
* references an out-of-scope binding. (#1297)
|
|
113
|
-
*/
|
|
114
|
-
export function tryLowerToKolon(
|
|
115
|
-
ctx: XslateMemoContext,
|
|
116
|
-
expr: string,
|
|
117
|
-
available: ReadonlySet<string>,
|
|
118
|
-
): string | null {
|
|
119
|
-
const trimmed = expr.trim()
|
|
120
|
-
if (!trimmed) return null
|
|
121
|
-
if (!isSupported(parseExpression(trimmed)).supported) return null
|
|
122
|
-
const kolon = ctx.convertExpressionToKolon(trimmed)
|
|
123
|
-
if (kolon === '' || !/\$[A-Za-z_]\w*/.test(kolon)) return null
|
|
124
|
-
return referencedVarsAreAvailable(kolon, available) ? kolon : null
|
|
125
|
-
}
|