@barefootjs/erb 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/erb",
3
- "version": "0.33.4",
3
+ "version": "0.34.0",
4
4
  "description": "ERB (Embedded Ruby) adapter for BarefootJS — compiles IR to .erb templates and ships the Ruby rendering backend; runs under any Rack app (Sinatra, Rails)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -54,7 +54,7 @@
54
54
  "directory": "packages/adapter-erb"
55
55
  },
56
56
  "dependencies": {
57
- "@barefootjs/shared": "0.33.4"
57
+ "@barefootjs/shared": "0.34.0"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "@barefootjs/jsx": ">=0.2.0",
@@ -71,9 +71,9 @@
71
71
  },
72
72
  "devDependencies": {
73
73
  "@barefootjs/adapter-tests": "0.1.0",
74
- "@barefootjs/jsx": "0.33.4",
75
- "@barefootjs/vite": "0.33.4",
76
- "@barefootjs/client": "0.33.4",
74
+ "@barefootjs/jsx": "0.34.0",
75
+ "@barefootjs/vite": "0.34.0",
76
+ "@barefootjs/client": "0.34.0",
77
77
  "typescript": "^5.0.0",
78
78
  "vite": "^6.0.0"
79
79
  }
@@ -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 ErbEmitContext {
24
24
  /**
@@ -27,6 +27,17 @@ export interface ErbEmitContext {
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 `ErbTopLevelEmitter`'s `lowering` seam (#2843)
33
+ * so a registered call — the built-in `queryHref`, or any userland plugin —
34
+ * is recognised no matter where it sits in an expression tree (a ternary
35
+ * branch, a template-literal interpolation, …), not only when it's the call
36
+ * the adapter's own top-level conversion entry point
37
+ * (`convertExpressionToRuby`) 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.
@@ -86,8 +86,6 @@ import {
86
86
  lookupStaticRecordLiteral,
87
87
  searchParamsLocalNames,
88
88
  prepareLoweringMatchers,
89
- queryHrefArgs,
90
- isValidHelperId,
91
89
  sortComparatorFromArrow,
92
90
  isDangerousInnerHtmlAttr,
93
91
  resolveDangerousInnerHtml,
@@ -96,6 +94,7 @@ import {
96
94
  resolveStaticLoopSource,
97
95
  derivesScopeFromSlot,
98
96
  BindingScope,
97
+ buildImportAliasMap,
99
98
  } from '@barefootjs/jsx'
100
99
  import { isAriaBooleanAttr, isBooleanResultExpr, isExplicitStringCall } from './boolean-result.ts'
101
100
  import type { ParsedExpr, LoweringMatcher, LoopBindingPathSegment, EscapeKind } from '@barefootjs/jsx'
@@ -259,6 +258,17 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
259
258
  * nullish-attribute omission.
260
259
  */
261
260
  private nullableOptionalProps: Set<string> = new Set()
261
+ /**
262
+ * Local alias -> declared/exported name for imported components (#2822,
263
+ * the SSR-side counterpart of #2777's client-JS registry-key fix). A
264
+ * child referenced under an import alias (`import { Foo as Bar }`,
265
+ * `<Bar/>`) must build its cross-template `render_child` call against
266
+ * the child's own declared name (`Foo`, what `foo.tsx` registers its ERB
267
+ * partial as) — never the caller-local binding. Built once per compile
268
+ * from `ir.metadata.imports` via the shared `buildImportAliasMap`
269
+ * (`@barefootjs/jsx`) and read by `toTemplateName`.
270
+ */
271
+ private importAliases: Map<string, string> = new Map()
262
272
 
263
273
  constructor(options: ErbAdapterOptions = {}) {
264
274
  super()
@@ -288,6 +298,7 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
288
298
  this._searchParamsLocals = searchParamsLocalNames(ir.metadata)
289
299
  this._loweringMatchers = prepareLoweringMatchers(ir.metadata)
290
300
  this.localConstants = ir.metadata.localConstants ?? []
301
+ this.importAliases = buildImportAliasMap(ir.metadata.imports ?? [])
291
302
  this.scope = BindingScope.EMPTY
292
303
  this.errors = []
293
304
  this.childrenCaptureCounter = 0
@@ -404,7 +415,7 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
404
415
  */
405
416
  parseUndefinedAlternateTernary(
406
417
  expr: string,
407
- ): { condition: string; consequent: string } | null {
418
+ ): { condition: string; consequent: string; testParsed: ParsedExpr; consequentParsed: ParsedExpr } | null {
408
419
  const parsed = parseExpression(expr.trim())
409
420
  if (parsed?.kind !== 'conditional') return null
410
421
  const alt = parsed.alternate
@@ -412,13 +423,16 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
412
423
  (alt.kind === 'identifier' && (alt.name === 'undefined' || alt.name === 'null')) ||
413
424
  (alt.kind === 'literal' && (alt.value === null || alt.value === undefined))
414
425
  if (!isUndef) return null
415
- // Serialise the parsed sub-expressions back to JS source rather than
416
- // slicing `expr` text `indexOf('?')` / `lastIndexOf(':')` would
417
- // mis-split when the consequent itself contains `?` / `:` inside a
418
- // string or nested ternary (`cond ? 'a:b' : undefined`).
426
+ // `condition`/`consequent` are DEBUG text only (`exprToString` renders an
427
+ // unsupported nested shape like an object literal as non-reparseable
428
+ // `[UNSUPPORTED: …]` text) callers that need to re-lower the
429
+ // sub-expression must use `testParsed`/`consequentParsed` (the actual
430
+ // parsed trees) as `preParsed`, not re-parse these strings.
419
431
  return {
420
432
  condition: exprToString(parsed.test),
421
433
  consequent: exprToString(parsed.consequent),
434
+ testParsed: parsed.test,
435
+ consequentParsed: parsed.consequent,
422
436
  }
423
437
  }
424
438
 
@@ -1323,8 +1337,12 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
1323
1337
  private childrenCaptureCounter = 0
1324
1338
 
1325
1339
  private toTemplateName(componentName: string): string {
1340
+ // Resolve an import alias (`import { Foo as Bar }`, `<Bar/>`) back to
1341
+ // the child's own declared name BEFORE snake-casing (#2822) — `Bar`
1342
+ // has no `foo.tsx`-registered partial; only `Foo` does.
1343
+ const declaredName = this.importAliases.get(componentName) ?? componentName
1326
1344
  // Convert PascalCase to snake_case for ERB template naming
1327
- return componentName
1345
+ return declaredName
1328
1346
  .replace(/([A-Z])/g, '_$1')
1329
1347
  .toLowerCase()
1330
1348
  .replace(/^_/, '')
@@ -1484,8 +1502,8 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
1484
1502
  {
1485
1503
  const m = this.parseUndefinedAlternateTernary(value.expr)
1486
1504
  if (m) {
1487
- const cond = this.convertExpressionToRuby(m.condition)
1488
- const val = this.convertExpressionToRuby(m.consequent)
1505
+ const cond = this.convertExpressionToRuby('', m.testParsed)
1506
+ const val = this.convertExpressionToRuby('', m.consequentParsed)
1489
1507
  return `<% if bf.truthy?(${cond}) %>${name}="<%= bf.h(${val}) %>"<% end %>`
1490
1508
  }
1491
1509
  }
@@ -1802,7 +1820,7 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
1802
1820
  const hasTaggedTemplate = /[A-Za-z_$][\w$]*\s*`/.test(probe)
1803
1821
  if (!startsAsObjectLiteral && !hasTaggedTemplate) return false
1804
1822
  const parsed = parseExpression(expr.trim())
1805
- const support = isSupported(parsed)
1823
+ const support = isSupported(parsed, { loweringMatchers: this._loweringMatchers })
1806
1824
  if (parsed.kind !== 'unsupported' && support.supported) return false
1807
1825
  const reason = support.reason ?? (parsed.kind === 'unsupported' ? parsed.reason : undefined)
1808
1826
  const reasonLine = reason ? `\n${reason}` : ''
@@ -1829,6 +1847,7 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
1829
1847
  private get emitCtx(): ErbEmitContext {
1830
1848
  return {
1831
1849
  _searchParamsLocals: this._searchParamsLocals,
1850
+ _loweringMatchers: this._loweringMatchers,
1832
1851
  resolveModuleStringConst: (name) => this.resolveModuleStringConst(name),
1833
1852
  resolveLiteralConst: (name) => this.resolveLiteralConst(name),
1834
1853
  resolveStaticRecordLiteral: (o, k) => this.resolveStaticRecordLiteral(o, k),
@@ -1885,42 +1904,16 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
1885
1904
  parsed = parseExpression(trimmed)
1886
1905
  }
1887
1906
 
1888
- // Registered call lowerings, including the built-in `queryHref` plugin,
1889
- // which lowers `queryHref(base, { … })` to a neutral `guard-list` on
1890
- // the `query` helper → `bf.query(base, <triples>)`. Recognised before
1891
- // the support gate because the object-literal arg is otherwise
1892
- // `unsupported` (BF101). The `bf.query` helper includes a pair iff its
1893
- // guard is truthy AND its value is a non-empty string (the client's
1894
- // `if (value)`): a plain `key: v` passes guard `true`, a conditional
1895
- // `key: cond ? v : undefined` passes the lowered cond. Only the
1896
- // `query` helper renders to `bf.query`; another guard-list helper must
1897
- // not be silently mis-rendered as a query.
1898
- if (parsed.kind === 'call') {
1899
- for (const matcher of this._loweringMatchers) {
1900
- const node = matcher(parsed.callee, parsed.args)
1901
- if (node?.kind === 'guard-list' && node.helper === 'query') {
1902
- const argsRuby = queryHrefArgs(node, n => this.renderParsedExprToRuby(n))
1903
- return `bf.query(${argsRuby.join(', ')})`
1904
- }
1905
- // Generic `helper-call` (#2069) — the neutral vocabulary's escape
1906
- // hatch for a userland `LoweringPlugin` that lowers to a single
1907
- // runtime-helper invocation. `bf.<helper>(args…)` mirrors the
1908
- // `query` helper's own naming convention exactly: the framework
1909
- // renders the call, the plugin author registers `<helper>` as a
1910
- // Ruby-callable method on their own `bf` helper object — same
1911
- // contract as `bf.query` itself, just not built in.
1912
- if (node?.kind === 'helper-call' && isValidHelperId(node.helper)) {
1913
- const argsX = node.args.map(a => this.renderParsedExprToRuby(a))
1914
- return `bf.${node.helper}(${argsX.join(', ')})`
1915
- }
1916
- }
1917
- }
1918
-
1919
1907
  // `pos` distinguishes a derived-seed RHS (an assignment, checked via
1920
1908
  // `isSupportedValue`) from every other, genuinely rendered call site —
1921
1909
  // the rendered gate would otherwise re-refuse a tree the seed plan
1922
1910
  // already classified `derived` at value position (#2696 review).
1923
- const support = pos === 'value' ? isSupportedValue(parsed) : isSupported(parsed)
1911
+ // Registered call lowerings (#2057) including the built-in `queryHref`
1912
+ // plugin — are consulted by this gate no matter where the call sits in
1913
+ // the tree (`ErbTopLevelEmitter`'s `lowering` seam does the actual
1914
+ // rendering below, via `emitParsedExpr`'s shared `call` case).
1915
+ const supportOpts = { loweringMatchers: this._loweringMatchers }
1916
+ const support = pos === 'value' ? isSupportedValue(parsed, supportOpts) : isSupported(parsed, supportOpts)
1924
1917
  if (!support.supported) {
1925
1918
  this.errors.push({
1926
1919
  code: 'BF101',
@@ -46,6 +46,10 @@ import {
46
46
  identifierPath,
47
47
  matchSearchParamsMethodCall,
48
48
  sortComparatorFromArrow,
49
+ type LoweringEmitter,
50
+ type LoweringNode,
51
+ queryHrefArgs,
52
+ isValidHelperId,
49
53
  } from '@barefootjs/jsx'
50
54
 
51
55
  import type { ErbEmitContext } from '../emit-context.ts'
@@ -346,6 +350,35 @@ export class ErbTopLevelEmitter implements ParsedExprEmitter {
346
350
  this.ctx = ctx
347
351
  }
348
352
 
353
+ /**
354
+ * Registered-lowering seam (#2843): `emitParsedExpr`'s shared `call` case
355
+ * tries every matcher here BEFORE `call()` itself, so a registered call
356
+ * (the built-in `queryHref`, or any userland plugin) is recognised no
357
+ * matter where it sits in the tree. `render` is what used to live inline
358
+ * in `ErbAdapter.convertExpressionToRuby` before the object-literal
359
+ * support-gate refusal (`checkSupport`'s `call` arm, now itself
360
+ * registry-aware) made the pre-gate special case unnecessary.
361
+ */
362
+ get lowering(): LoweringEmitter {
363
+ return {
364
+ matchers: this.ctx._loweringMatchers,
365
+ render: (node: LoweringNode, emit: (e: ParsedExpr) => string): string | null => {
366
+ // `query` guard-list — `queryHref`-shaped.
367
+ if (node.kind === 'guard-list' && node.helper === 'query') {
368
+ const qArgs = queryHrefArgs(node, emit)
369
+ return `bf.query(${qArgs.join(', ')})`
370
+ }
371
+ // Generic `helper-call` (#2069) — a userland `LoweringPlugin`'s
372
+ // single runtime-helper invocation; `bf.<helper>(args…)` mirrors
373
+ // the `query` helper's own naming convention.
374
+ if (node.kind === 'helper-call' && isValidHelperId(node.helper)) {
375
+ return `bf.${node.helper}(${node.args.map(emit).join(', ')})`
376
+ }
377
+ return null
378
+ },
379
+ }
380
+ }
381
+
349
382
  identifier(name: string): string {
350
383
  // `undefined` / `null` nested inside a larger expression tree (e.g.
351
384
  // `props.isActive ? 'page' : undefined`) — the top-level short-circuits
@@ -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
+ // `ErbTopLevelEmitter`'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' }],
@@ -56,4 +60,11 @@ export const conformancePins: ConformancePins = {
56
60
  // `rich-prop-client-read` above.
57
61
  'jsx-element-prop-ternary': [{ code: 'BF021', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2667' }],
58
62
  'jsx-element-prop-array': [{ code: 'BF021', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2667' }],
63
+ // #2771: a reactive primitive invoked through a namespace import
64
+ // (`import * as bf from '@barefootjs/client'`, `bf.createSignal(...)`)
65
+ // that the analyzer's checker-less fast path cannot recognize refuses
66
+ // loudly (BF013) instead of silently dropping the declaration — fired
67
+ // in the shared analyzer pass ahead of any adapter's `adapter.generate()`,
68
+ // so all nine adapters (including Hono) pin this identically.
69
+ 'namespace-import-primitive': [{ code: 'BF013', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2771' }],
59
70
  }
@@ -15,4 +15,7 @@ import type { RenderDivergences } from '@barefootjs/jsx'
15
15
  // at value position and the runtime evaluator's `object-literal` case
16
16
  // now merges it, so the seed classifies `derived` and SSRs identically
17
17
  // to Hono.
18
- export const renderDivergences: RenderDivergences = {}
18
+ export const renderDivergences: RenderDivergences = {
19
+ 'aliased-loop-source':
20
+ 'A `.map()` loop whose source is a local const alias of a signal getter (`const items__alias = items`) throws `undefined method \'length\' for nil (NoMethodError)` on real Ruby ERB — 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.',
21
+ }