@barefootjs/erb 0.31.3 → 0.31.5
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/erb-adapter.d.ts +16 -10
- package/dist/adapter/erb-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +16 -35
- package/dist/index.js +16 -35
- package/dist/vite.js +241 -198
- package/package.json +5 -5
- package/src/adapter/erb-adapter.ts +53 -60
- package/src/adapter/expr/emitters.ts +5 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/erb",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.5",
|
|
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.31.
|
|
57
|
+
"@barefootjs/shared": "0.31.5"
|
|
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.31.
|
|
75
|
-
"@barefootjs/vite": "0.31.
|
|
76
|
-
"@barefootjs/client": "0.31.
|
|
74
|
+
"@barefootjs/jsx": "0.31.5",
|
|
75
|
+
"@barefootjs/vite": "0.31.5",
|
|
76
|
+
"@barefootjs/client": "0.31.5",
|
|
77
77
|
"typescript": "^5.0.0",
|
|
78
78
|
"vite": "^6.0.0"
|
|
79
79
|
}
|
|
@@ -94,6 +94,7 @@ import {
|
|
|
94
94
|
dangerousInnerHtmlDiagnostic,
|
|
95
95
|
resolveStaticLoopSource,
|
|
96
96
|
derivesScopeFromSlot,
|
|
97
|
+
BindingScope,
|
|
97
98
|
} from '@barefootjs/jsx'
|
|
98
99
|
import { isAriaBooleanAttr, isBooleanResultExpr, isExplicitStringCall } from './boolean-result.ts'
|
|
99
100
|
import type { ParsedExpr, LoweringMatcher, LoopBindingPathSegment } from '@barefootjs/jsx'
|
|
@@ -248,17 +249,23 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
|
|
|
248
249
|
*/
|
|
249
250
|
private localConstants: IRMetadata['localConstants'] = []
|
|
250
251
|
/**
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
* for TWO things in the ERB adapter
|
|
255
|
-
* only used it to guard
|
|
256
|
-
* fundamental `v[:name]` vs
|
|
257
|
-
* `ErbTopLevelEmitter.identifier` —
|
|
258
|
-
* `isLoopBoundName` docstring for why ERB's
|
|
259
|
-
* where Perl's uniform `$name` sigil does
|
|
252
|
+
* The one canonical, position-accurate "names bound by an enclosing loop
|
|
253
|
+
* callback" service (#2482 Stage 2) — replaces the ref-counted
|
|
254
|
+
* `Map<string, number>` this adapter used to push/pop around
|
|
255
|
+
* `renderLoop`'s body. Load-bearing for TWO things in the ERB adapter
|
|
256
|
+
* (more than the Mojo original, which only used it to guard
|
|
257
|
+
* const-inlining): it also decides the fundamental `v[:name]` vs
|
|
258
|
+
* bare-Ruby-local rendering choice in `ErbTopLevelEmitter.identifier` —
|
|
259
|
+
* see `emit-context.ts`'s `isLoopBoundName` docstring for why ERB's
|
|
260
|
+
* two-locals model needs this where Perl's uniform `$name` sigil does
|
|
261
|
+
* not. Threaded via save/restore-by-reference around
|
|
262
|
+
* `renderChildren(loop.children)` in `renderLoop` (immutable — no
|
|
263
|
+
* ref-count bookkeeping), mirroring the Stage 1a/1b `ctx.scope`
|
|
264
|
+
* precedent in `jsx-to-ir.ts`. `IRLoop` already structurally satisfies
|
|
265
|
+
* `LoopBindingSource` (`param`/`index`/`paramBindings`/`preamble`), so
|
|
266
|
+
* `renderLoop` passes the loop node straight to `enterLoopRow`.
|
|
260
267
|
*/
|
|
261
|
-
private
|
|
268
|
+
private scope: BindingScope = BindingScope.EMPTY
|
|
262
269
|
/**
|
|
263
270
|
* Prop names whose value is `nil` in the template body when the caller
|
|
264
271
|
* omits them — so a bare-reference attribute should be dropped rather
|
|
@@ -298,7 +305,7 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
|
|
|
298
305
|
this._searchParamsLocals = searchParamsLocalNames(ir.metadata)
|
|
299
306
|
this._loweringMatchers = prepareLoweringMatchers(ir.metadata)
|
|
300
307
|
this.localConstants = ir.metadata.localConstants ?? []
|
|
301
|
-
this.
|
|
308
|
+
this.scope = BindingScope.EMPTY
|
|
302
309
|
this.errors = []
|
|
303
310
|
this.childrenCaptureCounter = 0
|
|
304
311
|
|
|
@@ -440,7 +447,7 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
|
|
|
440
447
|
* would read nil.
|
|
441
448
|
*/
|
|
442
449
|
private resolveLiteralConst(name: string): string | null {
|
|
443
|
-
if (this.
|
|
450
|
+
if (this.scope.isBound(name)) return null
|
|
444
451
|
const c = (this.localConstants ?? []).find(lc => lc.name === name)
|
|
445
452
|
if (c?.value === undefined) return null
|
|
446
453
|
const v = c.value.trim()
|
|
@@ -451,8 +458,7 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
|
|
|
451
458
|
}
|
|
452
459
|
|
|
453
460
|
private resolveStaticRecordLiteral(objectName: string, key: string): string | null {
|
|
454
|
-
|
|
455
|
-
const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants)
|
|
461
|
+
const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants, name => this.scope.isBound(name))
|
|
456
462
|
if (!hit) return null
|
|
457
463
|
return hit.kind === 'number' ? hit.text : rubyStringLiteral(hit.text)
|
|
458
464
|
}
|
|
@@ -460,7 +466,7 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
|
|
|
460
466
|
private resolveModuleStringConst(name: string): string | null {
|
|
461
467
|
// A loop body introduces block-param bindings that shadow a module
|
|
462
468
|
// const of the same name — never inline inside one.
|
|
463
|
-
if (this.
|
|
469
|
+
if (this.scope.isBound(name)) return null
|
|
464
470
|
const value = this.moduleStringConsts.get(name)
|
|
465
471
|
if (value === undefined) return null
|
|
466
472
|
return rubyStringLiteral(value)
|
|
@@ -469,7 +475,7 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
|
|
|
469
475
|
/** Whether `name` currently names a loop-bound Ruby local. See
|
|
470
476
|
* `ErbEmitContext.isLoopBoundName`'s docstring. */
|
|
471
477
|
private isLoopBoundName(name: string): boolean {
|
|
472
|
-
return this.
|
|
478
|
+
return this.scope.isBound(name)
|
|
473
479
|
}
|
|
474
480
|
|
|
475
481
|
// ===========================================================================
|
|
@@ -957,15 +963,17 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
|
|
|
957
963
|
// `unsupported` object-literal path (BF101, "Expression not
|
|
958
964
|
// supported"), which is what this loop-specific check now also does
|
|
959
965
|
// deliberately, up front, for both shapes.
|
|
966
|
+
// Canonical, position-accurate predicate (#2482 Stage 2) — the
|
|
967
|
+
// enclosing loop scope's own membership.
|
|
960
968
|
const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
|
|
961
|
-
isNameShadowed:
|
|
969
|
+
isNameShadowed: this.scope.asShadowPredicate(),
|
|
962
970
|
})
|
|
963
971
|
const staticArray = staticItems !== null ? staticValueToRuby(staticItems) : null
|
|
964
972
|
|
|
965
973
|
if (staticArray === null && loop.arrayParsed?.kind === 'identifier') {
|
|
966
974
|
const arrayName = loop.arrayParsed.name
|
|
967
975
|
const isUnresolvableLocalConst =
|
|
968
|
-
!this.
|
|
976
|
+
!this.scope.isBound(arrayName) &&
|
|
969
977
|
this.resolveModuleStringConst(arrayName) === null &&
|
|
970
978
|
this.resolveLiteralConst(arrayName) === null &&
|
|
971
979
|
this.localConstants.some(c => c.name === arrayName && !c.isModule)
|
|
@@ -994,29 +1002,22 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
|
|
|
994
1002
|
const indexVar = loop.iterationShape === 'keys'
|
|
995
1003
|
? rubyLocal(param)
|
|
996
1004
|
: rubyLocal(loop.index ?? '_i')
|
|
997
|
-
//
|
|
998
|
-
//
|
|
999
|
-
//
|
|
1000
|
-
//
|
|
1001
|
-
//
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
//
|
|
1010
|
-
// declaration (#2447). The names must be loop-bound for the whole body,
|
|
1011
|
-
// or `ErbTopLevelEmitter.identifier` renders each read as `v[:cls]` —
|
|
1012
|
-
// a vars-Hash key nothing ever seeds, i.e. the empty attribute this
|
|
1013
|
-
// fixes. Phase 1 guarantees `declarations` is present or the loop was
|
|
1014
|
-
// already refused, so there is no partial-lowering case here.
|
|
1005
|
+
// This loop's row scope. Guards module-const inlining (and, in ERB, the
|
|
1006
|
+
// fundamental v[:name]-vs-local rendering choice) for the whole body
|
|
1007
|
+
// (children + key + filter) so a same-named loop variable isn't
|
|
1008
|
+
// replaced by the const literal / a vars-Hash read. `IRLoop` already
|
|
1009
|
+
// structurally satisfies `LoopBindingSource`
|
|
1010
|
+
// (`param`/`index`/`paramBindings`/`preamble`) — `enterLoopRow(loop)`
|
|
1011
|
+
// binds exactly what this renderLoop's header + preamble locals
|
|
1012
|
+
// introduce (#2482 Stage 2 — replaces the ref-counted `Map` this
|
|
1013
|
+
// adapter used to carry). Restored by reference (immutable — no
|
|
1014
|
+
// ref-count bookkeeping) at the matching pop below, once the WHOLE
|
|
1015
|
+
// body (including the filter predicate) has been rendered — except for
|
|
1016
|
+
// one temporary drop-to-outer window around the hoisted sort-comparator
|
|
1017
|
+
// emission below, since that runs OUTSIDE this loop.
|
|
1015
1018
|
const preambleDecls = loop.preamble?.declarations ?? []
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1)
|
|
1019
|
-
}
|
|
1019
|
+
const prevScope = this.scope
|
|
1020
|
+
this.scope = prevScope.enterLoopRow(loop)
|
|
1020
1021
|
const prevLoopKeyDepth = this.currentLoopKeyDepth
|
|
1021
1022
|
this.currentLoopKeyDepth = loop.depth
|
|
1022
1023
|
const renderedChildren = this.renderChildren(loop.children)
|
|
@@ -1042,18 +1043,16 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
|
|
|
1042
1043
|
// fall back to the structured `bf.sort` for a comparator the
|
|
1043
1044
|
// evaluator can't model (e.g. `localeCompare`).
|
|
1044
1045
|
//
|
|
1045
|
-
// The hoisted sort runs OUTSIDE this loop, so this loop's
|
|
1046
|
+
// The hoisted sort runs OUTSIDE this loop, so this loop's row scope
|
|
1046
1047
|
// must not shadow the comparator's captured free vars while emitting
|
|
1047
1048
|
// the env — otherwise a captured var that happens to share a
|
|
1048
1049
|
// loop-param name is blocked from inlining its module const / from
|
|
1049
1050
|
// reading `v[:name]` and instead resolves to the (out-of-scope) loop
|
|
1050
|
-
// local. Drop
|
|
1051
|
-
// restore (a nested loop's outer
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
else this.loopBoundNames.set(n, c)
|
|
1056
|
-
}
|
|
1051
|
+
// local. Drop back to the outer (pre-row) scope for the sort emit,
|
|
1052
|
+
// then restore the row scope below (a nested loop's own outer
|
|
1053
|
+
// bindings stay in effect throughout, since `prevScope` already
|
|
1054
|
+
// carries them).
|
|
1055
|
+
this.scope = prevScope
|
|
1057
1056
|
const sortEmit = (e: ParsedExpr) => this.convertExpressionToRuby('', e)
|
|
1058
1057
|
const sortArrow = loop.sortComparator.arrow
|
|
1059
1058
|
let sorted: string | null = null
|
|
@@ -1074,9 +1073,7 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
|
|
|
1074
1073
|
)
|
|
1075
1074
|
sorted = rawArray
|
|
1076
1075
|
}
|
|
1077
|
-
|
|
1078
|
-
this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1)
|
|
1079
|
-
}
|
|
1076
|
+
this.scope = prevScope.enterLoopRow(loop)
|
|
1080
1077
|
lines.push(`<%- ${sortedHoist} = ${sorted} -%>`)
|
|
1081
1078
|
}
|
|
1082
1079
|
if (loop.objectIteration) {
|
|
@@ -1184,12 +1181,8 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
|
|
|
1184
1181
|
lines.push(children)
|
|
1185
1182
|
}
|
|
1186
1183
|
|
|
1187
|
-
// Body fully rendered —
|
|
1188
|
-
|
|
1189
|
-
const c = (this.loopBoundNames.get(n) ?? 1) - 1
|
|
1190
|
-
if (c <= 0) this.loopBoundNames.delete(n)
|
|
1191
|
-
else this.loopBoundNames.set(n, c)
|
|
1192
|
-
}
|
|
1184
|
+
// Body fully rendered — restore the outer (pre-row) scope.
|
|
1185
|
+
this.scope = prevScope
|
|
1193
1186
|
|
|
1194
1187
|
lines.push(`<%- end -%>`)
|
|
1195
1188
|
lines.push(`<%= bf.comment("/loop:${loop.markerId}") %>`)
|
|
@@ -1558,11 +1551,11 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
|
|
|
1558
1551
|
// function-scope (`!isModule`) consts whose value is NOT itself a
|
|
1559
1552
|
// bare identifier (loop guard) are considered.
|
|
1560
1553
|
//
|
|
1561
|
-
// `
|
|
1562
|
-
// param can shadow this outer const's name (`.map((attrs) => <p
|
|
1554
|
+
// `this.scope` shadow guard (#2489): an enclosing `.map()` callback's
|
|
1555
|
+
// own param can shadow this outer const's name (`.map((attrs) => <p
|
|
1563
1556
|
// {...attrs} />)`) — without the guard this forwarded the OUTER
|
|
1564
1557
|
// const's value at every iteration instead of the per-item value.
|
|
1565
|
-
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed) && !this.
|
|
1558
|
+
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed) && !this.scope.isBound(trimmed)) {
|
|
1566
1559
|
const localConst = this.localConstants.find(
|
|
1567
1560
|
c => c.name === trimmed && !c.isModule,
|
|
1568
1561
|
)
|
|
@@ -352,11 +352,11 @@ export class ErbTopLevelEmitter implements ParsedExprEmitter {
|
|
|
352
352
|
if (name === 'undefined' || name === 'null') return 'nil'
|
|
353
353
|
// A loop-bound name (this identifier resolves to a bare Ruby local
|
|
354
354
|
// introduced by an enclosing loop, not a vars-Hash entry) takes
|
|
355
|
-
// priority over const inlining — mirrors the Mojo adapter's
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
//
|
|
355
|
+
// priority over const inlining — mirrors the Mojo adapter's threaded
|
|
356
|
+
// scope shadow guard, but here it ALSO decides the fundamental
|
|
357
|
+
// v[:name]-vs-bare-local rendering, not just the const fast path
|
|
358
|
+
// (ERB's two-locals variable model needs this distinction; Perl's
|
|
359
|
+
// uniform `$name` sigil does not — see `ErbEmitContext`).
|
|
360
360
|
if (this.ctx.isLoopBoundName(name)) return rubyLocal(name)
|
|
361
361
|
// Module pure-string const (e.g. `const baseClasses = '...'` used in a
|
|
362
362
|
// className template literal): inline the literal value rather than
|