@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.
@@ -1,5 +1,5 @@
1
1
  package BarefootJS::Backend::Xslate;
2
- our $VERSION = "0.33.3";
2
+ our $VERSION = "0.33.6";
3
3
  use strict;
4
4
  use warnings;
5
5
  use utf8;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/xslate",
3
- "version": "0.33.4",
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.33.4"
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.33.4",
76
- "@barefootjs/vite": "0.33.4",
77
- "@barefootjs/client": "0.33.4",
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 componentName
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
- const cond = this.convertExpressionToKolon(m.condition)
1385
- const val = this.convertExpressionToKolon(m.consequent)
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
- // Registered call lowerings (#2057) including the built-in `queryHref`
1719
- // plugin (#2042), which lowers `queryHref(base, { })` to a neutral
1720
- // `guard-list` on the `query` helper `$bf.query(base, <triples>)`.
1721
- // Recognised before the support gate because the object-literal arg is
1722
- // otherwise `unsupported` (BF101). The `query` helper includes a pair iff its
1723
- // guard is truthy AND its value is a non-empty string (the client's
1724
- // `if (value)`): a plain `key: v` passes guard `1`, a conditional
1725
- // `key: cond ? v : undefined` passes the lowered cond. Only the `query`
1726
- // helper renders to `$bf.query`; another guard-list helper must not be
1727
- // silently mis-rendered as a query.
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 support = pos === 'value' ? isSupportedValue(parsed) : isSupported(parsed)
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
- // Serialise the parsed sub-expressions back to JS source rather than
1822
- // slicing `expr` text `indexOf('?')` / `lastIndexOf(':')` would
1823
- // mis-split when the consequent itself contains `?` / `:` inside a
1824
- // string or nested ternary (`cond ? 'a:b' : undefined`).
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
 
@@ -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
+ }