@barefootjs/xslate 0.15.2 → 0.17.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/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 +63 -0
- package/dist/adapter/expr/array-method.d.ts.map +1 -0
- package/dist/adapter/expr/emitters.d.ts +97 -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 +1428 -1324
- 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 +34 -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 +1428 -1324
- package/dist/index.js +1428 -1324
- 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 +94 -2
- package/src/adapter/analysis/component-tree.ts +123 -0
- package/src/adapter/emit-context.ts +104 -0
- package/src/adapter/expr/array-method.ts +311 -0
- package/src/adapter/expr/emitters.ts +530 -0
- package/src/adapter/expr/operand.ts +35 -0
- package/src/adapter/lib/constants.ts +34 -0
- package/src/adapter/lib/ir-scope.ts +64 -0
- package/src/adapter/lib/kolon-naming.ts +27 -0
- package/src/adapter/lib/types.ts +30 -0
- package/src/adapter/memo/seed.ts +125 -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 +176 -1233
- 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.0",
|
|
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.0"
|
|
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.0",
|
|
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
|
+
})
|
|
@@ -88,8 +88,12 @@ runAdapterConformanceTests({
|
|
|
88
88
|
// #1467 Phase 2e: `data-table` is no longer pinned here — it
|
|
89
89
|
// compiles clean now (`selected()[index]` → `index-access`,
|
|
90
90
|
// `.toFixed(2)` → `$bf.to_fixed`, `/* @client */` memo SSR-folded)
|
|
91
|
-
// and
|
|
92
|
-
// divergence
|
|
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.
|
|
93
97
|
// `style-3-signals` / `style-object-dynamic` no longer pinned — a
|
|
94
98
|
// `style={{ … }}` object literal now lowers to a CSS string with dynamic
|
|
95
99
|
// values interpolated (`background-color:<: $color :>;padding:8px`) via
|
|
@@ -254,3 +258,91 @@ export function Item({ on }: { on?: boolean }) {
|
|
|
254
258
|
expect(count).toBe(2)
|
|
255
259
|
})
|
|
256
260
|
})
|
|
261
|
+
|
|
262
|
+
// =============================================================================
|
|
263
|
+
// #1966 — `/* @client */` defers ATTRIBUTE bindings (not just child/text)
|
|
264
|
+
// =============================================================================
|
|
265
|
+
//
|
|
266
|
+
// `renderAttributes` skips SSR emission for `attr.clientOnly`, so a
|
|
267
|
+
// deferred attribute predicate is omitted from the Xslate template (and the
|
|
268
|
+
// unsupported-expression lowering is never reached → no BF101/BF102). The
|
|
269
|
+
// client runtime sets the attribute on hydrate. Mirrors the Go pins.
|
|
270
|
+
describe('XslateAdapter - #1966 @client defers attribute bindings', () => {
|
|
271
|
+
function compileAttr(attrExpr: string) {
|
|
272
|
+
const adapter = new XslateAdapter()
|
|
273
|
+
const ir = compileToIR(`
|
|
274
|
+
"use client"
|
|
275
|
+
import { createSignal } from "@barefootjs/client"
|
|
276
|
+
export function C() {
|
|
277
|
+
const [sel] = createSignal(0)
|
|
278
|
+
const pred = (n: number) => sel() === n
|
|
279
|
+
return <div data-x={${attrExpr}}>hi</div>
|
|
280
|
+
}
|
|
281
|
+
`)
|
|
282
|
+
const template = adapter.generate(ir).template ?? ''
|
|
283
|
+
const errors = (adapter as unknown as { errors: { code: string }[] }).errors ?? []
|
|
284
|
+
return { errors, template }
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
test('bare emits data-x; @client omits it from SSR', () => {
|
|
288
|
+
expect(compileAttr('pred(1)').template).toContain('data-x')
|
|
289
|
+
const deferred = compileAttr('/* @client */ pred(1)')
|
|
290
|
+
expect(deferred.errors).toEqual([])
|
|
291
|
+
expect(deferred.template).not.toContain('data-x')
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
// #2018 P2: higher-order predicates lower through the runtime evaluator
|
|
296
|
+
// (`$bf.*_eval`), isomorphic with the Go / Mojo `*_eval` helpers. A predicate
|
|
297
|
+
// the evaluator can't model (a method-call predicate) falls back to the Kolon
|
|
298
|
+
// lambda runtime call. Template-text pins guard against silent divergence.
|
|
299
|
+
describe('XslateAdapter - higher-order predicate lowering (#2018 P2)', () => {
|
|
300
|
+
test('a serializable predicate lowers to $bf.filter_eval with the JSON body + env', () => {
|
|
301
|
+
// A standalone `.filter().length` exercises the higher-order emitter (the
|
|
302
|
+
// `.filter().map()` form is a loop-hoist with an inline `: if`, handled by
|
|
303
|
+
// renderLoop, not this emitter).
|
|
304
|
+
const { template } = compileAndGenerate(`
|
|
305
|
+
function A({ items }: { items: { done: boolean }[] }) {
|
|
306
|
+
return <div>{items.filter(x => x.done).length}</div>
|
|
307
|
+
}
|
|
308
|
+
export { A }
|
|
309
|
+
`)
|
|
310
|
+
expect(template).toContain('$bf.filter_eval(')
|
|
311
|
+
expect(template).toContain('"property":"done"')
|
|
312
|
+
expect(template).toContain("'x'")
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
test('.find / .findLast share $bf.find_eval, distinguished by the forward flag', () => {
|
|
316
|
+
const find = compileAndGenerate(`
|
|
317
|
+
function A({ items }: { items: { done: boolean }[] }) {
|
|
318
|
+
return <div>{items.find(x => x.done) ? 'y' : 'n'}</div>
|
|
319
|
+
}
|
|
320
|
+
export { A }
|
|
321
|
+
`).template
|
|
322
|
+
expect(find).toContain('$bf.find_eval(')
|
|
323
|
+
expect(find).toContain(', 1, {})')
|
|
324
|
+
|
|
325
|
+
const findLast = compileAndGenerate(`
|
|
326
|
+
function A({ items }: { items: { done: boolean }[] }) {
|
|
327
|
+
return <div>{items.findLast(x => x.done) ? 'y' : 'n'}</div>
|
|
328
|
+
}
|
|
329
|
+
export { A }
|
|
330
|
+
`).template
|
|
331
|
+
expect(findLast).toContain('$bf.find_eval(')
|
|
332
|
+
expect(findLast).toContain(', 0, {})')
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
test('a method-call predicate falls back to the Kolon-lambda runtime call', () => {
|
|
336
|
+
const { template } = compileAndGenerate(`
|
|
337
|
+
function A({ items }: { items: { name: string }[] }) {
|
|
338
|
+
return <div>{items.every(x => x.name.includes('a')) ? 'y' : 'n'}</div>
|
|
339
|
+
}
|
|
340
|
+
export { A }
|
|
341
|
+
`)
|
|
342
|
+
// No evaluator helper — the unsupported predicate keeps the `-> $x { … }`
|
|
343
|
+
// lambda form passed to the runtime `$bf.every`.
|
|
344
|
+
expect(template).not.toContain('every_eval')
|
|
345
|
+
expect(template).toContain('$bf.every(')
|
|
346
|
+
expect(template).toContain('-> $x {')
|
|
347
|
+
})
|
|
348
|
+
})
|
|
@@ -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
|
+
}
|