@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/dist/adapter/emit-context.d.ts +11 -1
- package/dist/adapter/emit-context.d.ts.map +1 -1
- package/dist/adapter/erb-adapter.d.ts +14 -0
- package/dist/adapter/erb-adapter.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/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/package.json +5 -5
- package/src/adapter/emit-context.ts +12 -1
- package/src/adapter/erb-adapter.ts +36 -43
- package/src/adapter/expr/emitters.ts +33 -0
- 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/erb",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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.
|
|
75
|
-
"@barefootjs/vite": "0.
|
|
76
|
-
"@barefootjs/client": "0.
|
|
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
|
-
//
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
//
|
|
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
|
|
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.
|
|
1488
|
-
const val = this.convertExpressionToRuby(m.
|
|
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
|
-
|
|
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
|
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
|
+
// `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
|
+
}
|