@barefootjs/xslate 0.16.0 → 0.17.1
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/analysis/component-tree.d.ts +30 -0
- package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
- package/dist/adapter/emit-context.d.ts +94 -0
- package/dist/adapter/emit-context.d.ts.map +1 -0
- package/dist/adapter/expr/array-method.d.ts +73 -0
- package/dist/adapter/expr/array-method.d.ts.map +1 -0
- package/dist/adapter/expr/emitters.d.ts +96 -0
- package/dist/adapter/expr/emitters.d.ts.map +1 -0
- package/dist/adapter/expr/operand.d.ts +25 -0
- package/dist/adapter/expr/operand.d.ts.map +1 -0
- package/dist/adapter/index.js +1412 -1323
- package/dist/adapter/lib/constants.d.ts +22 -0
- package/dist/adapter/lib/constants.d.ts.map +1 -0
- package/dist/adapter/lib/ir-scope.d.ts +29 -0
- package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
- package/dist/adapter/lib/kolon-naming.d.ts +22 -0
- package/dist/adapter/lib/kolon-naming.d.ts.map +1 -0
- package/dist/adapter/lib/types.d.ts +27 -0
- package/dist/adapter/lib/types.d.ts.map +1 -0
- package/dist/adapter/memo/seed.d.ts +38 -0
- package/dist/adapter/memo/seed.d.ts.map +1 -0
- package/dist/adapter/props/prop-classes.d.ts +32 -0
- package/dist/adapter/props/prop-classes.d.ts.map +1 -0
- package/dist/adapter/spread/spread-codegen.d.ts +69 -0
- package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
- package/dist/adapter/value/parsed-literal.d.ts +32 -0
- package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
- package/dist/adapter/xslate-adapter.d.ts +38 -92
- package/dist/adapter/xslate-adapter.d.ts.map +1 -1
- package/dist/build.js +1412 -1323
- package/dist/index.js +1412 -1323
- package/lib/BarefootJS/Backend/Xslate.pm +1 -1
- package/package.json +3 -3
- package/src/__tests__/query-href.test.ts +96 -0
- package/src/__tests__/xslate-adapter.test.ts +200 -8
- package/src/adapter/analysis/component-tree.ts +123 -0
- package/src/adapter/emit-context.ts +104 -0
- package/src/adapter/expr/array-method.ts +332 -0
- package/src/adapter/expr/emitters.ts +548 -0
- package/src/adapter/expr/operand.ts +35 -0
- package/src/adapter/lib/constants.ts +34 -0
- package/src/adapter/lib/ir-scope.ts +53 -0
- package/src/adapter/lib/kolon-naming.ts +27 -0
- package/src/adapter/lib/types.ts +30 -0
- package/src/adapter/memo/seed.ts +78 -0
- package/src/adapter/props/prop-classes.ts +64 -0
- package/src/adapter/spread/spread-codegen.ts +179 -0
- package/src/adapter/value/parsed-literal.ts +80 -0
- package/src/adapter/xslate-adapter.ts +190 -1236
- package/src/test-render.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/xslate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"description": "Text::Xslate (Kolon) adapter for BarefootJS — compiles IR to .tx templates and ships the Xslate rendering backend; runs under any PSGI/Plack app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -55,14 +55,14 @@
|
|
|
55
55
|
"directory": "packages/adapter-xslate"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@barefootjs/shared": "0.
|
|
58
|
+
"@barefootjs/shared": "0.17.1"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@barefootjs/jsx": ">=0.2.0"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
65
|
-
"@barefootjs/jsx": "0.
|
|
65
|
+
"@barefootjs/jsx": "0.17.1",
|
|
66
66
|
"typescript": "^5.0.0"
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `queryHref(base, { … })` → `$bf.query(...)` lowering for the Xslate adapter
|
|
3
|
+
* (#2042). Parity with the go-template / Mojo lowering: the call + object literal
|
|
4
|
+
* are structured IR, so it lowers directly. The shared `query` runtime helper
|
|
5
|
+
* (BarefootJS.pm) includes a pair iff its guard is truthy AND its value is a
|
|
6
|
+
* non-empty string, so a plain `key: v` passes guard `1` and a conditional
|
|
7
|
+
* `key: cond ? v : undefined` passes the lowered condition.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, test, expect } from 'bun:test'
|
|
10
|
+
import { compileJSX, type ComponentIR } from '@barefootjs/jsx'
|
|
11
|
+
import { XslateAdapter } from '../adapter/xslate-adapter'
|
|
12
|
+
|
|
13
|
+
function template(src: string): string {
|
|
14
|
+
const a = new XslateAdapter()
|
|
15
|
+
const r = compileJSX(src.trimStart(), 'T.tsx', { adapter: a, outputIR: true })
|
|
16
|
+
const ir = JSON.parse(r.files.find(f => f.type === 'ir')!.content) as ComponentIR
|
|
17
|
+
return a.generate(ir).template
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('queryHref → $bf.query (Xslate, #2042)', () => {
|
|
21
|
+
test('a plain value passes guard 1', () => {
|
|
22
|
+
const t = template(`
|
|
23
|
+
'use client'
|
|
24
|
+
import { queryHref } from '@barefootjs/client'
|
|
25
|
+
export function P(props: { base: string; tag: string }) {
|
|
26
|
+
return <a href={queryHref(props.base, { tag: props.tag })}>x</a>
|
|
27
|
+
}
|
|
28
|
+
`)
|
|
29
|
+
expect(t).toContain("$bf.query($base, 1, 'tag', $tag)")
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('a conditional include passes the lowered condition as the guard', () => {
|
|
33
|
+
const t = template(`
|
|
34
|
+
'use client'
|
|
35
|
+
import { queryHref } from '@barefootjs/client'
|
|
36
|
+
export function P(props: { base: string; sort: string; tag: string }) {
|
|
37
|
+
return <a href={queryHref(props.base, { sort: props.sort !== 'date' ? props.sort : undefined, tag: props.tag })}>x</a>
|
|
38
|
+
}
|
|
39
|
+
`)
|
|
40
|
+
// Kolon uses `!=` for string inequality (no `ne` operator).
|
|
41
|
+
expect(t).toContain("$bf.query($base, ($sort != 'date'), 'sort', $sort, 1, 'tag', $tag)")
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// A bare-value guard (`flag ? v : undefined`) is JS *string* truthiness — `'0'`
|
|
45
|
+
// is a truthy string in JS but false under Perl's `unless`. The lowering must
|
|
46
|
+
// normalise it to a non-empty-string test so SSR matches the client / go (where
|
|
47
|
+
// `lowerUrlGuard` emits `ne <value> ""`). Kolon renders the `!== ''` test as
|
|
48
|
+
// `!= ''` (its string inequality), matching the comparison guard above.
|
|
49
|
+
test('a bare-value guard is normalised to a non-empty-string test', () => {
|
|
50
|
+
const t = template(`
|
|
51
|
+
'use client'
|
|
52
|
+
import { queryHref } from '@barefootjs/client'
|
|
53
|
+
export function P(props: { base: string; flag: string; val: string }) {
|
|
54
|
+
return <a href={queryHref(props.base, { q: props.flag ? props.val : undefined })}>x</a>
|
|
55
|
+
}
|
|
56
|
+
`)
|
|
57
|
+
expect(t).toContain("$bf.query($base, ($flag != ''), 'q', $val)")
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// An array value (`{ tag: props.tags }`) lowers to the bare slice expression;
|
|
61
|
+
// the shared Perl `query` helper detects the arrayref at runtime and appends
|
|
62
|
+
// one pair per non-empty member (#2048). No adapter-side change beyond passing
|
|
63
|
+
// the value through.
|
|
64
|
+
test('an array value passes the slice expression for the helper to append', () => {
|
|
65
|
+
const t = template(`
|
|
66
|
+
'use client'
|
|
67
|
+
import { queryHref } from '@barefootjs/client'
|
|
68
|
+
export function P(props: { base: string; tags: string[] }) {
|
|
69
|
+
return <a href={queryHref(props.base, { tag: props.tags })}>x</a>
|
|
70
|
+
}
|
|
71
|
+
`)
|
|
72
|
+
expect(t).toContain("$bf.query($base, 1, 'tag', $tags)")
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('an aliased import is recognised', () => {
|
|
76
|
+
const t = template(`
|
|
77
|
+
'use client'
|
|
78
|
+
import { queryHref as qh } from '@barefootjs/client'
|
|
79
|
+
export function P(props: { base: string; tag: string }) {
|
|
80
|
+
return <a href={qh(props.base, { tag: props.tag })}>x</a>
|
|
81
|
+
}
|
|
82
|
+
`)
|
|
83
|
+
expect(t).toContain("$bf.query($base, 1, 'tag', $tag)")
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('a dynamic (non-literal) params object falls back (no $bf.query)', () => {
|
|
87
|
+
const t = template(`
|
|
88
|
+
'use client'
|
|
89
|
+
import { queryHref } from '@barefootjs/client'
|
|
90
|
+
export function P(props: { base: string; q: Record<string, string> }) {
|
|
91
|
+
return <a href={queryHref(props.base, props.q)}>x</a>
|
|
92
|
+
}
|
|
93
|
+
`)
|
|
94
|
+
expect(t).not.toContain('.query')
|
|
95
|
+
})
|
|
96
|
+
})
|
|
@@ -101,10 +101,29 @@ runAdapterConformanceTests({
|
|
|
101
101
|
// Tagged-template-literal call in a className — same family, same
|
|
102
102
|
// refusal (BF101).
|
|
103
103
|
'tagged-template-classname': [{ code: 'BF101', severity: 'error' }],
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
104
|
+
// #2038: a filter predicate whose body contains a NESTED callback call
|
|
105
|
+
// (`t => !picked().some(p => …)` / `t => picked().find(p => …)`). Kolon
|
|
106
|
+
// has no inline `grep` form, so `XslateFilterEmitter.callbackMethod` used
|
|
107
|
+
// to degrade the inner call to its receiver, silently changing predicate
|
|
108
|
+
// semantics — the compiler is loud instead of lossy. (Mojo is pinned only
|
|
109
|
+
// for the `.find` variant: it lowers a nested `.some` to a real inline
|
|
110
|
+
// Perl `grep`.) The `/* @client */` twin
|
|
111
|
+
// (`filter-nested-callback-predicate-client`) has no pin here: it must
|
|
112
|
+
// render clean on every adapter, which asserts the suppression contract.
|
|
113
|
+
// https://github.com/piconic-ai/barefootjs/issues/2038
|
|
114
|
+
'filter-nested-callback-predicate': [{ code: 'BF101', severity: 'error' }],
|
|
115
|
+
'filter-nested-find-predicate': [{ code: 'BF101', severity: 'error' }],
|
|
116
|
+
// NB: TOP-LEVEL `.find` / `.findIndex` / `.findLast` / `.findLastIndex`
|
|
117
|
+
// (text position) are NOT pinned here — unlike mojo (which refuses them),
|
|
118
|
+
// Xslate lowers them to `$bf.find` / `find_index` / `find_last` /
|
|
119
|
+
// `find_last_index` via the same Kolon-lambda mechanism as `.filter` /
|
|
120
|
+
// `.every` / `.some`, so they render. Only the NESTED-in-a-predicate form
|
|
121
|
+
// above is refused (#2038).
|
|
122
|
+
// #2073 follow-up: a function-reference `.map(format)` callback has no
|
|
123
|
+
// arrow body to serialize — not a CALLBACK_METHODS shape — so the
|
|
124
|
+
// UNSUPPORTED_METHODS gate refuses it with BF101 rather than emitting
|
|
125
|
+
// a broken template.
|
|
126
|
+
'array-map-function-reference': [{ code: 'BF101', severity: 'error' }],
|
|
108
127
|
},
|
|
109
128
|
// Template-primitive registry parity: same V1 surface as mojo, so the
|
|
110
129
|
// same two cases stay skipped (bespoke user import + customSerialize
|
|
@@ -113,11 +132,15 @@ runAdapterConformanceTests({
|
|
|
113
132
|
TemplatePrimitiveCaseId.USER_IMPORT_VIA_CONST,
|
|
114
133
|
TemplatePrimitiveCaseId.NO_DOUBLE_REWRITE_OF_PROPS_OBJECT,
|
|
115
134
|
]),
|
|
116
|
-
//
|
|
117
|
-
//
|
|
135
|
+
// `client-only` / `client-only-loop-with-sibling-cond` /
|
|
136
|
+
// `filter-nested-callback-predicate-client` are no longer skipped —
|
|
137
|
+
// `renderLoop` now emits the `$bf.comment("loop:<id>")` boundary pair
|
|
138
|
+
// for clientOnly loops (Hono / Go parity), so mapArray() can locate
|
|
139
|
+
// its insertion anchor at hydration time (#872 / #1087).
|
|
118
140
|
skipMarkerConformance: new Set([
|
|
119
|
-
|
|
120
|
-
|
|
141
|
+
// Same as Hono / Mojo: `/* @client */` markers on TodoApp's keyed
|
|
142
|
+
// `.map` intentionally elide a slot id from the SSR template that
|
|
143
|
+
// the IR still declares (s6). See hono-adapter.test for the contract.
|
|
121
144
|
'todo-app',
|
|
122
145
|
// #1467 Phase 2e: same `/* @client */` keyed-map elision (data-table).
|
|
123
146
|
'data-table',
|
|
@@ -291,3 +314,172 @@ export function C() {
|
|
|
291
314
|
expect(deferred.template).not.toContain('data-x')
|
|
292
315
|
})
|
|
293
316
|
})
|
|
317
|
+
|
|
318
|
+
// #2018 P2: higher-order predicates lower through the runtime evaluator
|
|
319
|
+
// (`$bf.*_eval`), isomorphic with the Go / Mojo `*_eval` helpers. A predicate
|
|
320
|
+
// the evaluator can't model (a method-call predicate) falls back to the Kolon
|
|
321
|
+
// lambda runtime call. Template-text pins guard against silent divergence.
|
|
322
|
+
describe('XslateAdapter - #2075 searchParams()-derived memo seeding', () => {
|
|
323
|
+
test('seeds an aliased scalar derived memo from the canonical reader', () => {
|
|
324
|
+
const { template } = compileAndGenerate(`
|
|
325
|
+
'use client'
|
|
326
|
+
import { createMemo, createSearchParams } from '@barefootjs/client'
|
|
327
|
+
export function SortStatus() {
|
|
328
|
+
const [sp] = createSearchParams()
|
|
329
|
+
const sort = createMemo(() => sp().get('sort') ?? 'date')
|
|
330
|
+
return <p>sort: {sort()}</p>
|
|
331
|
+
}
|
|
332
|
+
`)
|
|
333
|
+
expect(template).toContain(": my $sort = ($searchParams.get('sort') // 'date');")
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
// The Kolon lambda param and the `$bf` runtime object are
|
|
337
|
+
// lowering-internal, not out-of-scope template vars (#2075).
|
|
338
|
+
test('seeds a filter memo chained off the derived memo', () => {
|
|
339
|
+
const { template } = compileAndGenerate(`
|
|
340
|
+
'use client'
|
|
341
|
+
import { createMemo, createSearchParams } from '@barefootjs/client'
|
|
342
|
+
export function TaggedList(props: { items: { title: string; tags: string[] }[] }) {
|
|
343
|
+
const [searchParams] = createSearchParams()
|
|
344
|
+
const tag = createMemo(() => searchParams().get('tag') ?? '')
|
|
345
|
+
const visible = createMemo(() => props.items.filter((p) => !tag() || p.tags.includes(tag())))
|
|
346
|
+
return <ul>{visible().map((p) => <li key={p.title}>{p.title}</li>)}</ul>
|
|
347
|
+
}
|
|
348
|
+
`)
|
|
349
|
+
expect(template).toContain(": my $tag = ($searchParams.get('tag') // '');")
|
|
350
|
+
expect(template).toContain(': my $visible = $bf.filter($items,')
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
// The seed-scope guard used to scan the LOWERED
|
|
354
|
+
// Kolon string, allowing every arrow-callback param tree-wide. That let an
|
|
355
|
+
// outer, unbound `p` (shadowed only inside the callback) slip past the
|
|
356
|
+
// guard as if it were the callback's own bound `$p` — emitting a bogus
|
|
357
|
+
// seed line. The guard now walks the parsed SOURCE tree with proper
|
|
358
|
+
// lexical scoping (`freeIdentifiers`), so this shape seeds nothing and
|
|
359
|
+
// falls back to the null/ssr-defaults path.
|
|
360
|
+
test('an outer unbound `p` shadowed only inside the callback does not seed', () => {
|
|
361
|
+
const { template } = compileAndGenerate(`
|
|
362
|
+
'use client'
|
|
363
|
+
import { createMemo } from '@barefootjs/client'
|
|
364
|
+
export function C(props: { items: { ok: boolean }[] }) {
|
|
365
|
+
const visible = createMemo(() => props.items.filter((p) => p.ok) && p)
|
|
366
|
+
return <div>{String(visible())}</div>
|
|
367
|
+
}
|
|
368
|
+
`)
|
|
369
|
+
expect(template).not.toContain('my $visible')
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
// An out-of-scope bare `_` reference must not seed either — the old
|
|
373
|
+
// unconditional `allowed.add('_')` / `allowed.add('bf')` masked this.
|
|
374
|
+
test('an out-of-scope bare `_` reference does not seed', () => {
|
|
375
|
+
const { template } = compileAndGenerate(`
|
|
376
|
+
'use client'
|
|
377
|
+
import { createMemo } from '@barefootjs/client'
|
|
378
|
+
export function C(props: { count: number }) {
|
|
379
|
+
const doubled = createMemo(() => props.count * 2 + _)
|
|
380
|
+
return <div>{doubled()}</div>
|
|
381
|
+
}
|
|
382
|
+
`)
|
|
383
|
+
expect(template).not.toContain('my $doubled')
|
|
384
|
+
})
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
describe('XslateAdapter - #2073 value-producing .map(cb)', () => {
|
|
388
|
+
// The blog-showcase shape (#1938/#1939): a value-returning `.map` (string
|
|
389
|
+
// projection, not JSX) lowers through the evaluator — `$bf.map_eval`
|
|
390
|
+
// projects each element (no flatten) and composes through `$bf.join`.
|
|
391
|
+
test('.map(t => `#${t}`).join(" ") emits $bf.map_eval composed into $bf.join', () => {
|
|
392
|
+
const { template } = compileAndGenerate(`
|
|
393
|
+
function TagLine({ tags }: { tags: string[] }) {
|
|
394
|
+
return <p>{tags.map((t) => \`#\${t}\`).join(' ')}</p>
|
|
395
|
+
}
|
|
396
|
+
export { TagLine }
|
|
397
|
+
`)
|
|
398
|
+
expect(template).toContain("$bf.join($bf.map_eval($tags,")
|
|
399
|
+
expect(template).toContain('"kind":"template-literal"')
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
test('.map(u => u.name) emits $bf.map_eval with the field projection', () => {
|
|
403
|
+
const { template } = compileAndGenerate(`
|
|
404
|
+
function NameList({ users }: { users: { name: string }[] }) {
|
|
405
|
+
return <div>{users.map((u) => u.name).join(', ')}</div>
|
|
406
|
+
}
|
|
407
|
+
export { NameList }
|
|
408
|
+
`)
|
|
409
|
+
expect(template).toContain('$bf.map_eval($users,')
|
|
410
|
+
expect(template).toContain('"property":"name"')
|
|
411
|
+
})
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
describe('XslateAdapter - higher-order predicate lowering (#2018 P2)', () => {
|
|
415
|
+
test('a serializable predicate lowers to $bf.filter_eval with the JSON body + env', () => {
|
|
416
|
+
// A standalone `.filter().length` exercises the higher-order emitter (the
|
|
417
|
+
// `.filter().map()` form is a loop-hoist with an inline `: if`, handled by
|
|
418
|
+
// renderLoop, not this emitter).
|
|
419
|
+
const { template } = compileAndGenerate(`
|
|
420
|
+
function A({ items }: { items: { done: boolean }[] }) {
|
|
421
|
+
return <div>{items.filter(x => x.done).length}</div>
|
|
422
|
+
}
|
|
423
|
+
export { A }
|
|
424
|
+
`)
|
|
425
|
+
expect(template).toContain('$bf.filter_eval(')
|
|
426
|
+
expect(template).toContain('"property":"done"')
|
|
427
|
+
expect(template).toContain("'x'")
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
test('.find / .findLast share $bf.find_eval, distinguished by the forward flag', () => {
|
|
431
|
+
const find = compileAndGenerate(`
|
|
432
|
+
function A({ items }: { items: { done: boolean }[] }) {
|
|
433
|
+
return <div>{items.find(x => x.done) ? 'y' : 'n'}</div>
|
|
434
|
+
}
|
|
435
|
+
export { A }
|
|
436
|
+
`).template
|
|
437
|
+
expect(find).toContain('$bf.find_eval(')
|
|
438
|
+
expect(find).toContain(', 1, {})')
|
|
439
|
+
|
|
440
|
+
const findLast = compileAndGenerate(`
|
|
441
|
+
function A({ items }: { items: { done: boolean }[] }) {
|
|
442
|
+
return <div>{items.findLast(x => x.done) ? 'y' : 'n'}</div>
|
|
443
|
+
}
|
|
444
|
+
export { A }
|
|
445
|
+
`).template
|
|
446
|
+
expect(findLast).toContain('$bf.find_eval(')
|
|
447
|
+
expect(findLast).toContain(', 0, {})')
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
test('.includes() in a predicate now lowers via the evaluator, not the Kolon-lambda fallback', () => {
|
|
451
|
+
// #2075: `.includes(x)` joined the evaluator's `array-method` surface
|
|
452
|
+
// (shared with the Perl `Evaluator.pm` runtime), so a predicate built
|
|
453
|
+
// from it routes through `$bf.every_eval` like any other pure predicate.
|
|
454
|
+
const { template } = compileAndGenerate(`
|
|
455
|
+
function A({ items }: { items: { name: string }[] }) {
|
|
456
|
+
return <div>{items.every(x => x.name.includes('a')) ? 'y' : 'n'}</div>
|
|
457
|
+
}
|
|
458
|
+
export { A }
|
|
459
|
+
`)
|
|
460
|
+
expect(template).toContain('$bf.every_eval(')
|
|
461
|
+
expect(template).toContain('"method":"includes"')
|
|
462
|
+
expect(template).not.toContain('-> $x {')
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
test('a method-call predicate outside the evaluator surface falls back to the Kolon-lambda runtime call', () => {
|
|
466
|
+
const { template } = compileAndGenerate(`
|
|
467
|
+
function A({ items }: { items: { name: string }[] }) {
|
|
468
|
+
return <div>{items.every(x => x.name.toUpperCase() === 'A') ? 'y' : 'n'}</div>
|
|
469
|
+
}
|
|
470
|
+
export { A }
|
|
471
|
+
`)
|
|
472
|
+
// `.toUpperCase()` is outside the evaluator's `array-method` gate (only
|
|
473
|
+
// `includes` is recognized there), so the predicate keeps the
|
|
474
|
+
// `-> $x { … }` lambda form passed to the runtime `$bf.every`.
|
|
475
|
+
expect(template).not.toContain('every_eval')
|
|
476
|
+
expect(template).toContain('$bf.every(')
|
|
477
|
+
expect(template).toContain('-> $x {')
|
|
478
|
+
})
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
// #2038 nested-callback-predicate loudness is pinned at the shared
|
|
482
|
+
// conformance layer: `filter-nested-callback-predicate` /
|
|
483
|
+
// `filter-nested-find-predicate` (BF101 via `expectedDiagnostics` above) and
|
|
484
|
+
// `filter-nested-callback-predicate-client` (the `/* @client */` suppression
|
|
485
|
+
// twin, which must render clean).
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component-tree analysis for the Text::Xslate (Kolon) template adapter.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `xslate-adapter.ts` (domain-module refactor, issue #2018
|
|
5
|
+
* track D). Pure functions over the IR — they read no adapter instance
|
|
6
|
+
* state. `collectImportedLoopChildComponentErrors` returns its diagnostics
|
|
7
|
+
* instead of pushing onto the adapter's error list, so the adapter stays the
|
|
8
|
+
* sole owner of `errors`.
|
|
9
|
+
*
|
|
10
|
+
* SHARED CANDIDATE: `hasClientInteractivity` is byte-identical to the Mojo
|
|
11
|
+
* adapter's copy and adapter-agnostic; the BF103 loop-child check is the same
|
|
12
|
+
* structural walk in both, differing only in the Perl/Kolon diagnostic text —
|
|
13
|
+
* both are groundwork for a shared Perl-family codegen module (issue #2018
|
|
14
|
+
* track D).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
ComponentIR,
|
|
19
|
+
IRNode,
|
|
20
|
+
IRComponent,
|
|
21
|
+
IRElement,
|
|
22
|
+
IRFragment,
|
|
23
|
+
IRConditional,
|
|
24
|
+
IRLoop,
|
|
25
|
+
IRIfStatement,
|
|
26
|
+
IRProvider,
|
|
27
|
+
IRAsync,
|
|
28
|
+
CompilerError,
|
|
29
|
+
} from '@barefootjs/jsx'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Whether the component needs the client runtime — it owns reactive state
|
|
33
|
+
* (signals / effects / onMount) or the analyzer flagged it as needing init.
|
|
34
|
+
*/
|
|
35
|
+
export function hasClientInteractivity(ir: ComponentIR): boolean {
|
|
36
|
+
return (
|
|
37
|
+
ir.metadata.signals.length > 0 ||
|
|
38
|
+
ir.metadata.effects.length > 0 ||
|
|
39
|
+
ir.metadata.onMounts.length > 0 ||
|
|
40
|
+
(ir.metadata.clientAnalysis?.needsInit ?? false)
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build a `BF103` diagnostic for every component reference inside a loop body
|
|
46
|
+
* whose name is imported from a relative-path module. Mirror of the Go
|
|
47
|
+
* adapter's check — the Xslate adapter has the same cross-template-registration
|
|
48
|
+
* constraint at request time. Returns the diagnostics so the caller pushes
|
|
49
|
+
* them onto its own error list.
|
|
50
|
+
*/
|
|
51
|
+
export function collectImportedLoopChildComponentErrors(
|
|
52
|
+
ir: ComponentIR,
|
|
53
|
+
componentName: string,
|
|
54
|
+
): CompilerError[] {
|
|
55
|
+
const errors: CompilerError[] = []
|
|
56
|
+
const relativeImports = new Set<string>()
|
|
57
|
+
for (const imp of ir.metadata.templateImports ?? ir.metadata.imports ?? []) {
|
|
58
|
+
if (!imp.source.startsWith('./') && !imp.source.startsWith('../')) continue
|
|
59
|
+
if (imp.isTypeOnly) continue
|
|
60
|
+
for (const spec of imp.specifiers) {
|
|
61
|
+
relativeImports.add(spec.alias ?? spec.name)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (relativeImports.size === 0) return errors
|
|
65
|
+
|
|
66
|
+
const loc = { file: componentName + '.tsx', start: { line: 1, column: 0 }, end: { line: 1, column: 0 } }
|
|
67
|
+
const visit = (node: IRNode, inLoop: boolean): void => {
|
|
68
|
+
switch (node.type) {
|
|
69
|
+
case 'component': {
|
|
70
|
+
const comp = node as IRComponent
|
|
71
|
+
if (inLoop && relativeImports.has(comp.name)) {
|
|
72
|
+
errors.push({
|
|
73
|
+
code: 'BF103',
|
|
74
|
+
severity: 'error',
|
|
75
|
+
message: `Component <${comp.name}> is imported from a sibling module and used inside a loop. The Xslate adapter emits a cross-template call; the child template must be registered alongside the parent at render time.`,
|
|
76
|
+
loc: comp.loc ?? loc,
|
|
77
|
+
suggestion: {
|
|
78
|
+
message:
|
|
79
|
+
`Options:\n` +
|
|
80
|
+
` 1. Compile '${comp.name}' (its source file) with the same adapter and register the resulting Xslate template alongside the parent at render time.\n` +
|
|
81
|
+
` 2. Inline <${comp.name}> directly inside the loop body so no cross-file template lookup is needed.\n` +
|
|
82
|
+
` 3. Mark the loop position as @client-only so the template is materialised on the client instead of at SSR time.`,
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
for (const child of comp.children) visit(child, inLoop)
|
|
87
|
+
break
|
|
88
|
+
}
|
|
89
|
+
case 'element':
|
|
90
|
+
for (const child of (node as IRElement).children) visit(child, inLoop)
|
|
91
|
+
break
|
|
92
|
+
case 'fragment':
|
|
93
|
+
for (const child of (node as IRFragment).children) visit(child, inLoop)
|
|
94
|
+
break
|
|
95
|
+
case 'conditional': {
|
|
96
|
+
const cond = node as IRConditional
|
|
97
|
+
visit(cond.whenTrue, inLoop)
|
|
98
|
+
if (cond.whenFalse) visit(cond.whenFalse, inLoop)
|
|
99
|
+
break
|
|
100
|
+
}
|
|
101
|
+
case 'loop':
|
|
102
|
+
for (const child of (node as IRLoop).children) visit(child, true)
|
|
103
|
+
break
|
|
104
|
+
case 'if-statement': {
|
|
105
|
+
const stmt = node as IRIfStatement
|
|
106
|
+
visit(stmt.consequent, inLoop)
|
|
107
|
+
if (stmt.alternate) visit(stmt.alternate, inLoop)
|
|
108
|
+
break
|
|
109
|
+
}
|
|
110
|
+
case 'provider':
|
|
111
|
+
for (const child of (node as IRProvider).children) visit(child, inLoop)
|
|
112
|
+
break
|
|
113
|
+
case 'async': {
|
|
114
|
+
const a = node as IRAsync
|
|
115
|
+
visit(a.fallback, inLoop)
|
|
116
|
+
for (const child of a.children) visit(child, inLoop)
|
|
117
|
+
break
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
visit(ir.root, false)
|
|
122
|
+
return errors
|
|
123
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract the extracted expression-emitter modules depend on instead of
|
|
3
|
+
* the concrete `XslateAdapter`.
|
|
4
|
+
*
|
|
5
|
+
* The Xslate adapter's top-level expression lowering is mutually recursive
|
|
6
|
+
* with the adapter's own const/record resolution and its filter-predicate
|
|
7
|
+
* emitter, so the extracted `XslateTopLevelEmitter` still needs to call back
|
|
8
|
+
* into shared per-compile state and recursive entry points. `XslateEmitContext`
|
|
9
|
+
* is that seam: the emitter takes a `XslateEmitContext` built by the adapter's
|
|
10
|
+
* private `emitCtx` getter (the adapter does NOT `implements` this interface,
|
|
11
|
+
* so the wrapped members stay private and off its exported public type —
|
|
12
|
+
* matching the Go adapter's `emitCtx`). The emitter depends on this narrow
|
|
13
|
+
* interface rather than the full ~2.5k-line class, so the coupling is explicit
|
|
14
|
+
* and it's unit-testable against a stub.
|
|
15
|
+
*
|
|
16
|
+
* Keep this surface minimal: add a member only when an extracted module
|
|
17
|
+
* genuinely needs it, so the seam documents the real cross-module coupling
|
|
18
|
+
* rather than re-exposing the whole adapter.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { ParsedExpr, CompilerError, IRMetadata } from '@barefootjs/jsx'
|
|
22
|
+
|
|
23
|
+
export interface XslateEmitContext {
|
|
24
|
+
/**
|
|
25
|
+
* (#1922) Local binding names the request-scoped `searchParams()` env signal
|
|
26
|
+
* is imported under. Non-empty enables the env-signal method-call lowering.
|
|
27
|
+
*/
|
|
28
|
+
readonly _searchParamsLocals: Set<string>
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Inline a module-scope pure string-literal const by name as the resolved
|
|
32
|
+
* literal value, or null when the name is not such a const.
|
|
33
|
+
*/
|
|
34
|
+
_resolveModuleStringConst(name: string): string | null
|
|
35
|
+
|
|
36
|
+
/** Resolve a literal const (`const totalPages = 5`) to its Kolon value, or null. */
|
|
37
|
+
_resolveLiteralConst(name: string): string | null
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a static property access on a module object-literal const
|
|
41
|
+
* (`variantClasses.ghost`) to its Kolon value at compile time, or null.
|
|
42
|
+
*/
|
|
43
|
+
_resolveStaticRecordLiteral(objectName: string, key: string): string | null
|
|
44
|
+
|
|
45
|
+
/** Record a BF101 unsupported-expression diagnostic. */
|
|
46
|
+
_recordExprBF101(message: string, reason?: string): void
|
|
47
|
+
|
|
48
|
+
/** Lower a filter/predicate body to its Kolon form, bound to `param`. */
|
|
49
|
+
_renderKolonFilterExprPublic(expr: ParsedExpr, param: string): string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The contract the extracted object-literal / conditional-spread lowering
|
|
54
|
+
* (`spread/spread-codegen.ts`) depends on. The spread lowering recurses into
|
|
55
|
+
* the core expression lowering and records its own BF101 diagnostics, so it
|
|
56
|
+
* needs the recursive entry point plus the per-compile bookkeeping the
|
|
57
|
+
* adapter owns. Declared separately from `XslateEmitContext` so each
|
|
58
|
+
* extracted module's real coupling is documented precisely.
|
|
59
|
+
*/
|
|
60
|
+
export interface XslateSpreadContext {
|
|
61
|
+
/** Component name, for diagnostic source locations. */
|
|
62
|
+
readonly componentName: string
|
|
63
|
+
|
|
64
|
+
/** Per-compile diagnostic list the spread lowering appends to. */
|
|
65
|
+
readonly errors: CompilerError[]
|
|
66
|
+
|
|
67
|
+
/** Local-constant metadata, for resolving `Record[key]` spread values. */
|
|
68
|
+
readonly localConstants: IRMetadata['localConstants']
|
|
69
|
+
|
|
70
|
+
/** Prop params, for classifying a bare-identifier index as a prop. */
|
|
71
|
+
readonly propsParams: { name: string }[]
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Lower a JS expression to its Kolon form (the core recursive entry).
|
|
75
|
+
*
|
|
76
|
+
* When the IR already carries a structured `ParsedExpr` tree, pass it as
|
|
77
|
+
* `preParsed` so the converter threads it straight through instead of
|
|
78
|
+
* re-parsing `expr` — mirrors go-template's
|
|
79
|
+
* `convertExpressionToGo(jsExpr, out?, preParsed?)`. With `preParsed` set,
|
|
80
|
+
* `expr` is unused for parsing (the converter derives any diagnostic text
|
|
81
|
+
* from the tree), so callers may pass `''`.
|
|
82
|
+
*/
|
|
83
|
+
convertExpressionToKolon(expr: string, preParsed?: ParsedExpr): string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The contract the extracted in-template memo / context seeding
|
|
88
|
+
* (`memo/seed.ts`) depends on. The seed lowering recurses into the core
|
|
89
|
+
* expression lowering to compute a derived signal/memo value or a context
|
|
90
|
+
* default; that recursive entry is its only adapter coupling.
|
|
91
|
+
*/
|
|
92
|
+
export interface XslateMemoContext {
|
|
93
|
+
/**
|
|
94
|
+
* Lower a JS expression to its Kolon form (the core recursive entry).
|
|
95
|
+
*
|
|
96
|
+
* When the IR already carries a structured `ParsedExpr` tree, pass it as
|
|
97
|
+
* `preParsed` so the converter threads it straight through instead of
|
|
98
|
+
* re-parsing `expr` — mirrors go-template's
|
|
99
|
+
* `convertExpressionToGo(jsExpr, out?, preParsed?)`. With `preParsed` set,
|
|
100
|
+
* `expr` is unused for parsing (the converter derives any diagnostic text
|
|
101
|
+
* from the tree), so callers may pass `''`.
|
|
102
|
+
*/
|
|
103
|
+
convertExpressionToKolon(expr: string, preParsed?: ParsedExpr): string
|
|
104
|
+
}
|