@barefootjs/xslate 0.33.4 → 0.34.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/emit-context.d.ts +11 -1
- package/dist/adapter/emit-context.d.ts.map +1 -1
- package/dist/adapter/expr/emitters.d.ts +11 -1
- package/dist/adapter/expr/emitters.d.ts.map +1 -1
- package/dist/adapter/index.js +33 -23
- package/dist/adapter/xslate-adapter.d.ts +16 -0
- package/dist/adapter/xslate-adapter.d.ts.map +1 -1
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +38 -25
- package/dist/render-divergences.d.ts.map +1 -1
- package/dist/vite.js +197 -135
- package/lib/BarefootJS/Backend/Xslate.pm +1 -1
- package/package.json +5 -5
- package/src/adapter/emit-context.ts +12 -1
- package/src/adapter/expr/emitters.ts +33 -0
- package/src/adapter/xslate-adapter.ts +52 -43
- package/src/conformance-pins.ts +11 -0
- package/src/render-divergences.ts +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/xslate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.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,7 +55,7 @@
|
|
|
55
55
|
"directory": "packages/adapter-xslate"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@barefootjs/shared": "0.
|
|
58
|
+
"@barefootjs/shared": "0.34.0"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@barefootjs/jsx": ">=0.2.0",
|
|
@@ -72,9 +72,9 @@
|
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
75
|
-
"@barefootjs/jsx": "0.
|
|
76
|
-
"@barefootjs/vite": "0.
|
|
77
|
-
"@barefootjs/client": "0.
|
|
75
|
+
"@barefootjs/jsx": "0.34.0",
|
|
76
|
+
"@barefootjs/vite": "0.34.0",
|
|
77
|
+
"@barefootjs/client": "0.34.0",
|
|
78
78
|
"typescript": "^5.0.0",
|
|
79
79
|
"vite": "^6.0.0"
|
|
80
80
|
}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* rather than re-exposing the whole adapter.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import type { ParsedExpr, CompilerError, IRMetadata } from '@barefootjs/jsx'
|
|
21
|
+
import type { ParsedExpr, CompilerError, IRMetadata, LoweringMatcher } from '@barefootjs/jsx'
|
|
22
22
|
|
|
23
23
|
export interface XslateEmitContext {
|
|
24
24
|
/**
|
|
@@ -27,6 +27,17 @@ export interface XslateEmitContext {
|
|
|
27
27
|
*/
|
|
28
28
|
readonly _searchParamsLocals: Set<string>
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Registered lowering-plugin matchers (#2057), bound to this component's
|
|
32
|
+
* metadata at init. Read by `XslateTopLevelEmitter`'s `lowering` seam
|
|
33
|
+
* (#2843) so a registered call — the built-in `queryHref`, or any
|
|
34
|
+
* userland plugin — is recognised no matter where it sits in an
|
|
35
|
+
* expression tree (a ternary branch, a template-literal interpolation, …),
|
|
36
|
+
* not only when it's the call the adapter's own top-level conversion
|
|
37
|
+
* entry point (`convertExpressionToKolon`) is asked to lower directly.
|
|
38
|
+
*/
|
|
39
|
+
readonly _loweringMatchers: readonly LoweringMatcher[]
|
|
40
|
+
|
|
30
41
|
/**
|
|
31
42
|
* Inline a module-scope pure string-literal const by name as the resolved
|
|
32
43
|
* literal value, or null when the name is not such a const.
|
|
@@ -15,6 +15,8 @@ import { groupBinaryOperand,
|
|
|
15
15
|
groupObjectLiteralSegments,
|
|
16
16
|
isStringConcatBinary,
|
|
17
17
|
type ParsedExprEmitter,
|
|
18
|
+
type LoweringEmitter,
|
|
19
|
+
type LoweringNode,
|
|
18
20
|
type HigherOrderMethod,
|
|
19
21
|
type ArrayMethod,
|
|
20
22
|
type LiteralType,
|
|
@@ -25,6 +27,8 @@ import { groupBinaryOperand,
|
|
|
25
27
|
identifierPath,
|
|
26
28
|
matchSearchParamsMethodCall,
|
|
27
29
|
sortComparatorFromArrow,
|
|
30
|
+
queryHrefArgs,
|
|
31
|
+
isValidHelperId,
|
|
28
32
|
} from '@barefootjs/jsx'
|
|
29
33
|
|
|
30
34
|
import type { XslateEmitContext } from '../emit-context.ts'
|
|
@@ -267,6 +271,35 @@ export class XslateTopLevelEmitter implements ParsedExprEmitter {
|
|
|
267
271
|
this.ctx = ctx
|
|
268
272
|
}
|
|
269
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Registered-lowering seam (#2843): `emitParsedExpr`'s shared `call` case
|
|
276
|
+
* tries every matcher here BEFORE `call()` itself, so a registered call
|
|
277
|
+
* (the built-in `queryHref`, or any userland plugin) is recognised no
|
|
278
|
+
* matter where it sits in the tree. `render` is what used to live inline
|
|
279
|
+
* in `XslateAdapter.convertExpressionToKolon` before the object-literal
|
|
280
|
+
* support-gate refusal (`checkSupport`'s `call` arm, now itself
|
|
281
|
+
* registry-aware) made the pre-gate special case unnecessary.
|
|
282
|
+
*/
|
|
283
|
+
get lowering(): LoweringEmitter {
|
|
284
|
+
return {
|
|
285
|
+
matchers: this.ctx._loweringMatchers,
|
|
286
|
+
render: (node: LoweringNode, emit: (e: ParsedExpr) => string): string | null => {
|
|
287
|
+
// `query` guard-list — `queryHref`-shaped.
|
|
288
|
+
if (node.kind === 'guard-list' && node.helper === 'query') {
|
|
289
|
+
const qArgs = queryHrefArgs(node, emit)
|
|
290
|
+
return `$bf.query(${qArgs.join(', ')})`
|
|
291
|
+
}
|
|
292
|
+
// Generic `helper-call` (#2069) — a userland `LoweringPlugin`'s
|
|
293
|
+
// single runtime-helper invocation; `$bf.<helper>(args…)` mirrors
|
|
294
|
+
// the `query` helper's own naming convention.
|
|
295
|
+
if (node.kind === 'helper-call' && isValidHelperId(node.helper)) {
|
|
296
|
+
return `$bf.${node.helper}(${node.args.map(emit).join(', ')})`
|
|
297
|
+
}
|
|
298
|
+
return null
|
|
299
|
+
},
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
270
303
|
identifier(name: string): string {
|
|
271
304
|
// `undefined` / `null` nested inside a larger expression tree —
|
|
272
305
|
// Kolon `nil` (#1897).
|
|
@@ -70,8 +70,6 @@ import {
|
|
|
70
70
|
lookupStaticRecordLiteral,
|
|
71
71
|
searchParamsLocalNames,
|
|
72
72
|
prepareLoweringMatchers,
|
|
73
|
-
queryHrefArgs,
|
|
74
|
-
isValidHelperId,
|
|
75
73
|
sortComparatorFromArrow,
|
|
76
74
|
isLowerableLoopDestructure,
|
|
77
75
|
isDangerousInnerHtmlAttr,
|
|
@@ -81,6 +79,7 @@ import {
|
|
|
81
79
|
resolveStaticLoopSource,
|
|
82
80
|
derivesScopeFromSlot,
|
|
83
81
|
BindingScope,
|
|
82
|
+
buildImportAliasMap,
|
|
84
83
|
} from '@barefootjs/jsx'
|
|
85
84
|
import { isAriaBooleanAttr, isBooleanResultExpr } from './boolean-result.ts'
|
|
86
85
|
import ts from 'typescript'
|
|
@@ -258,6 +257,20 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
258
257
|
*/
|
|
259
258
|
private nullableOptionalProps: Set<string> = new Set()
|
|
260
259
|
|
|
260
|
+
/**
|
|
261
|
+
* Local alias -> declared/exported name for imported components (#2822,
|
|
262
|
+
* the SSR-side counterpart of #2777's client-JS registry-key fix). A
|
|
263
|
+
* child referenced under an import alias (`import { Foo as Bar }`,
|
|
264
|
+
* `<Bar/>`) must build its cross-template call against the child's own
|
|
265
|
+
* declared name (`Foo`, what `foo.tsx` registers its Kolon partial as) —
|
|
266
|
+
* never the caller-local binding. Xslate's unresolved-reference case
|
|
267
|
+
* silently drops the child rather than raising, so this was the worst
|
|
268
|
+
* variant of the bug class. Built once per compile from
|
|
269
|
+
* `ir.metadata.imports` via the shared `buildImportAliasMap`
|
|
270
|
+
* (`@barefootjs/jsx`) and read by `toTemplateName`.
|
|
271
|
+
*/
|
|
272
|
+
private importAliases: Map<string, string> = new Map()
|
|
273
|
+
|
|
261
274
|
constructor(options: XslateAdapterOptions = {}) {
|
|
262
275
|
super()
|
|
263
276
|
this.options = {
|
|
@@ -287,6 +300,7 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
287
300
|
this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants)
|
|
288
301
|
this._searchParamsLocals = searchParamsLocalNames(ir.metadata)
|
|
289
302
|
this._loweringMatchers = prepareLoweringMatchers(ir.metadata)
|
|
303
|
+
this.importAliases = buildImportAliasMap(ir.metadata.imports ?? [])
|
|
290
304
|
this.errors = []
|
|
291
305
|
this.childrenCaptureCounter = 0
|
|
292
306
|
|
|
@@ -1229,8 +1243,14 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1229
1243
|
private presenceVarCounter = 0
|
|
1230
1244
|
|
|
1231
1245
|
private toTemplateName(componentName: string): string {
|
|
1246
|
+
// Resolve an import alias (`import { Foo as Bar }`, `<Bar/>`) back to
|
|
1247
|
+
// the child's own declared name BEFORE snake-casing (#2822) — `Bar`
|
|
1248
|
+
// has no `foo.tsx`-registered partial; only `Foo` does. Xslate's
|
|
1249
|
+
// unresolved-reference case silently drops the child rather than
|
|
1250
|
+
// raising, so this mismatch was the worst variant of the bug class.
|
|
1251
|
+
const declaredName = this.importAliases.get(componentName) ?? componentName
|
|
1232
1252
|
// Convert PascalCase to snake_case for template naming.
|
|
1233
|
-
return
|
|
1253
|
+
return declaredName
|
|
1234
1254
|
.replace(/([A-Z])/g, '_$1')
|
|
1235
1255
|
.toLowerCase()
|
|
1236
1256
|
.replace(/^_/, '')
|
|
@@ -1381,8 +1401,13 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1381
1401
|
{
|
|
1382
1402
|
const m = this.parseUndefinedAlternateTernary(value.expr)
|
|
1383
1403
|
if (m) {
|
|
1384
|
-
|
|
1385
|
-
|
|
1404
|
+
// Pass the PARSED sub-trees through as `preParsed` (#2843 review)
|
|
1405
|
+
// rather than re-parsing `m.condition`/`m.consequent` — those are
|
|
1406
|
+
// `exprToString` debug text, which renders any nested
|
|
1407
|
+
// `object-literal` (e.g. a registered call's params, `queryHref`'s
|
|
1408
|
+
// `{ tag }`) as a non-reparseable `[UNSUPPORTED: …]` placeholder.
|
|
1409
|
+
const cond = this.convertExpressionToKolon('', m.testParsed)
|
|
1410
|
+
const val = this.convertExpressionToKolon('', m.consequentParsed)
|
|
1386
1411
|
return `\n: if (${cond}) {\n${name}="<: ${val} :>"\n: }\n`
|
|
1387
1412
|
}
|
|
1388
1413
|
}
|
|
@@ -1637,7 +1662,7 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1637
1662
|
const hasTaggedTemplate = /[A-Za-z_$][\w$]*\s*`/.test(probe)
|
|
1638
1663
|
if (!startsAsObjectLiteral && !hasTaggedTemplate) return false
|
|
1639
1664
|
const parsed = parseExpression(expr.trim())
|
|
1640
|
-
const support = isSupported(parsed)
|
|
1665
|
+
const support = isSupported(parsed, { loweringMatchers: this._loweringMatchers })
|
|
1641
1666
|
if (parsed.kind !== 'unsupported' && support.supported) return false
|
|
1642
1667
|
const reason = support.reason ?? (parsed.kind === 'unsupported' ? parsed.reason : undefined)
|
|
1643
1668
|
const reasonLine = reason ? `\n${reason}` : ''
|
|
@@ -1664,6 +1689,7 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1664
1689
|
private get emitCtx(): XslateEmitContext {
|
|
1665
1690
|
return {
|
|
1666
1691
|
_searchParamsLocals: this._searchParamsLocals,
|
|
1692
|
+
_loweringMatchers: this._loweringMatchers,
|
|
1667
1693
|
_resolveModuleStringConst: (name) => this._resolveModuleStringConst(name),
|
|
1668
1694
|
_resolveLiteralConst: (name) => this._resolveLiteralConst(name),
|
|
1669
1695
|
_resolveStaticRecordLiteral: (o, k) => this._resolveStaticRecordLiteral(o, k),
|
|
@@ -1715,42 +1741,22 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1715
1741
|
parsed = parseExpression(trimmed)
|
|
1716
1742
|
}
|
|
1717
1743
|
|
|
1718
|
-
//
|
|
1719
|
-
// plugin
|
|
1720
|
-
//
|
|
1721
|
-
//
|
|
1722
|
-
//
|
|
1723
|
-
//
|
|
1724
|
-
//
|
|
1725
|
-
// `
|
|
1726
|
-
//
|
|
1727
|
-
//
|
|
1728
|
-
if (parsed.kind === 'call') {
|
|
1729
|
-
for (const matcher of this._loweringMatchers) {
|
|
1730
|
-
const node = matcher(parsed.callee, parsed.args)
|
|
1731
|
-
if (node?.kind === 'guard-list' && node.helper === 'query') {
|
|
1732
|
-
const qArgs = queryHrefArgs(node, n => this.renderParsedExprToKolon(n))
|
|
1733
|
-
return `$bf.query(${qArgs.join(', ')})`
|
|
1734
|
-
}
|
|
1735
|
-
// Generic `helper-call` (#2069) — the neutral vocabulary's escape
|
|
1736
|
-
// hatch for a userland `LoweringPlugin` that lowers to a single
|
|
1737
|
-
// runtime-helper invocation. `$bf.<helper>(args…)` mirrors the
|
|
1738
|
-
// `query` helper's own naming convention exactly: the framework
|
|
1739
|
-
// renders the call, the plugin author registers `<helper>` as a
|
|
1740
|
-
// Kolon-callable method on the `$bf` vars entry in their own
|
|
1741
|
-
// runtime — same contract as `$bf.query` itself, just not built in.
|
|
1742
|
-
if (node?.kind === 'helper-call' && isValidHelperId(node.helper)) {
|
|
1743
|
-
const argsX = node.args.map(a => this.renderParsedExprToKolon(a))
|
|
1744
|
-
return `$bf.${node.helper}(${argsX.join(', ')})`
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1744
|
+
// #2843: a registered lowering plugin's call (the built-in `queryHref`,
|
|
1745
|
+
// or any userland plugin) is recognised no matter where it sits in the
|
|
1746
|
+
// tree — a ternary branch, a template-literal interpolation, … — not
|
|
1747
|
+
// only when `parsed.kind === 'call'` directly. That recognition now
|
|
1748
|
+
// lives in `XslateTopLevelEmitter`'s `lowering` seam, consulted by
|
|
1749
|
+
// `emitParsedExpr`'s shared `call` dispatch; the support gate below is
|
|
1750
|
+
// passed `this._loweringMatchers` so a matched call's params (e.g.
|
|
1751
|
+
// `queryHref`'s object literal, otherwise `unsupported` at `rendered`
|
|
1752
|
+
// position) are admitted wherever the call is nested.
|
|
1753
|
+
//
|
|
1749
1754
|
// `pos` distinguishes a derived-seed RHS (an assignment, checked via
|
|
1750
1755
|
// `isSupportedValue`) from every other, genuinely rendered call site —
|
|
1751
1756
|
// the rendered gate would otherwise re-refuse a tree the seed plan
|
|
1752
1757
|
// already classified `derived` at value position (#2696 review).
|
|
1753
|
-
const
|
|
1758
|
+
const supportOpts = { loweringMatchers: this._loweringMatchers }
|
|
1759
|
+
const support = pos === 'value' ? isSupportedValue(parsed, supportOpts) : isSupported(parsed, supportOpts)
|
|
1754
1760
|
if (!support.supported) {
|
|
1755
1761
|
this.errors.push({
|
|
1756
1762
|
code: 'BF101',
|
|
@@ -1810,7 +1816,7 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1810
1816
|
*/
|
|
1811
1817
|
parseUndefinedAlternateTernary(
|
|
1812
1818
|
expr: string,
|
|
1813
|
-
): { condition: string; consequent: string } | null {
|
|
1819
|
+
): { condition: string; consequent: string; testParsed: ParsedExpr; consequentParsed: ParsedExpr } | null {
|
|
1814
1820
|
const parsed = parseExpression(expr.trim())
|
|
1815
1821
|
if (parsed?.kind !== 'conditional') return null
|
|
1816
1822
|
const alt = parsed.alternate
|
|
@@ -1818,13 +1824,16 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1818
1824
|
(alt.kind === 'identifier' && (alt.name === 'undefined' || alt.name === 'null')) ||
|
|
1819
1825
|
(alt.kind === 'literal' && (alt.value === null || alt.value === undefined))
|
|
1820
1826
|
if (!isUndef) return null
|
|
1821
|
-
//
|
|
1822
|
-
//
|
|
1823
|
-
//
|
|
1824
|
-
//
|
|
1827
|
+
// `condition`/`consequent` are DEBUG text only (`exprToString` renders an
|
|
1828
|
+
// unsupported nested shape like an object literal as non-reparseable
|
|
1829
|
+
// `[UNSUPPORTED: …]` text) — callers that need to re-lower the
|
|
1830
|
+
// sub-expression must use `testParsed`/`consequentParsed` (the actual
|
|
1831
|
+
// parsed trees) as `preParsed`, not re-parse these strings.
|
|
1825
1832
|
return {
|
|
1826
1833
|
condition: exprToString(parsed.test),
|
|
1827
1834
|
consequent: exprToString(parsed.consequent),
|
|
1835
|
+
testParsed: parsed.test,
|
|
1836
|
+
consequentParsed: parsed.consequent,
|
|
1828
1837
|
}
|
|
1829
1838
|
}
|
|
1830
1839
|
|
package/src/conformance-pins.ts
CHANGED
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
import type { ConformancePins } from '@barefootjs/jsx'
|
|
11
11
|
|
|
12
12
|
export const conformancePins: ConformancePins = {
|
|
13
|
+
// #2843: graduated — a registered lowering call inside a ternary
|
|
14
|
+
// attribute branch (or any nested value position) is now recognised via
|
|
15
|
+
// `XslateTopLevelEmitter`'s `lowering` seam + the registry-aware support
|
|
16
|
+
// gate, matching the direct-call attribute path exactly.
|
|
13
17
|
'filter-typeof-predicate': [{ code: 'BF021', severity: 'error' }],
|
|
14
18
|
'map-array-builder-body': [{ code: 'BF021', severity: 'error' }],
|
|
15
19
|
'map-array-builder-escaping': [{ code: 'BF021', severity: 'error' }],
|
|
@@ -63,4 +67,11 @@ export const conformancePins: ConformancePins = {
|
|
|
63
67
|
// `rich-prop-client-read` above.
|
|
64
68
|
'jsx-element-prop-ternary': [{ code: 'BF021', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2667' }],
|
|
65
69
|
'jsx-element-prop-array': [{ code: 'BF021', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2667' }],
|
|
70
|
+
// #2771: a reactive primitive invoked through a namespace import
|
|
71
|
+
// (`import * as bf from '@barefootjs/client'`, `bf.createSignal(...)`)
|
|
72
|
+
// that the analyzer's checker-less fast path cannot recognize refuses
|
|
73
|
+
// loudly (BF013) instead of silently dropping the declaration — fired
|
|
74
|
+
// in the shared analyzer pass ahead of any adapter's `adapter.generate()`,
|
|
75
|
+
// so all nine adapters (including Hono) pin this identically.
|
|
76
|
+
'namespace-import-primitive': [{ code: 'BF013', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2771' }],
|
|
66
77
|
}
|
|
@@ -22,4 +22,7 @@ import type { RenderDivergences } from '@barefootjs/jsx'
|
|
|
22
22
|
// the real name off that capture — the same in-template recompute the other
|
|
23
23
|
// six template-stash backends already had. Keep the file even when the set
|
|
24
24
|
// is empty — the next divergence lands here, not in a re-created file.
|
|
25
|
-
export const renderDivergences: RenderDivergences = {
|
|
25
|
+
export const renderDivergences: RenderDivergences = {
|
|
26
|
+
'aliased-loop-source':
|
|
27
|
+
'A `.map()` loop whose source is a local const alias of a signal getter (`const items__alias = items`) SSRs an empty `<ul>` on real Text::Xslate — the seeded loop data is keyed by the signal\'s real name (`items`), and the alias hop is never resolved when deciding what to seed under `items__alias`. This is the SSR-side twin of #2778 (fixed for the CSR client-JS template in the same PR that added this fixture) — that fix only touches client-JS emission, not SSR data-seeding. Tracked at https://github.com/piconic-ai/barefootjs/issues/2813; graduate by resolving the alias hop at SSR-seeding time using the same `resolveAliasOrigin`/`resolveGetterAliases` mechanism #2778 introduced, rather than a third alias-hop walker.',
|
|
28
|
+
}
|