@barefootjs/mojolicious 0.31.4 → 0.31.6

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::Mojo;
2
- our $VERSION = "0.31.3";
2
+ our $VERSION = "0.31.5";
3
3
  use Mojo::Base -base, -signatures;
4
4
 
5
5
  use Mojo::ByteStream qw(b);
@@ -1,5 +1,5 @@
1
1
  package Mojolicious::Plugin::BarefootJS::DevReload;
2
- our $VERSION = "0.31.3";
2
+ our $VERSION = "0.31.5";
3
3
  use Mojo::Base 'Mojolicious::Plugin', -signatures;
4
4
 
5
5
  =head1 NAME
@@ -1,5 +1,5 @@
1
1
  package Mojolicious::Plugin::BarefootJS;
2
- our $VERSION = "0.31.3";
2
+ our $VERSION = "0.31.5";
3
3
  use Mojo::Base 'Mojolicious::Plugin', -signatures;
4
4
 
5
5
  use Mojo::File qw(path);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/mojolicious",
3
- "version": "0.31.4",
3
+ "version": "0.31.6",
4
4
  "description": "Mojolicious EP template adapter for BarefootJS - generates .html.ep files from IR",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -52,7 +52,7 @@
52
52
  "directory": "packages/adapter-mojolicious"
53
53
  },
54
54
  "dependencies": {
55
- "@barefootjs/shared": "0.31.4"
55
+ "@barefootjs/shared": "0.31.6"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "@barefootjs/jsx": ">=0.2.0",
@@ -70,9 +70,9 @@
70
70
  },
71
71
  "devDependencies": {
72
72
  "@barefootjs/adapter-tests": "0.1.0",
73
- "@barefootjs/jsx": "0.31.4",
74
- "@barefootjs/vite": "0.31.4",
75
- "@barefootjs/client": "0.31.4",
73
+ "@barefootjs/jsx": "0.31.6",
74
+ "@barefootjs/vite": "0.31.6",
75
+ "@barefootjs/client": "0.31.6",
76
76
  "vite": "^6.0.0"
77
77
  }
78
78
  }
@@ -65,6 +65,7 @@ import {
65
65
  dangerousInnerHtmlDiagnostic,
66
66
  resolveStaticLoopSource,
67
67
  derivesScopeFromSlot,
68
+ BindingScope,
68
69
  } from '@barefootjs/jsx'
69
70
  import { isAriaBooleanAttr, isBooleanResultExpr } from './boolean-result.ts'
70
71
  import type { ParsedExpr, LoweringMatcher } from '@barefootjs/jsx'
@@ -238,14 +239,22 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
238
239
  */
239
240
  private localConstants: IRMetadata['localConstants'] = []
240
241
  /**
241
- * Names currently bound by an enclosing loop body — the `my $<param>` and
242
- * `my $<index>` bindings `renderLoop` introduces — ref-counted so nested
243
- * loops compose. `resolveModuleStringConst` consults this so a loop
242
+ * The one canonical, position-accurate "names bound by an enclosing loop
243
+ * callback" service (#2482 Stage 2)replaces the ref-counted
244
+ * `Map<string, number>` this adapter used to push/pop around
245
+ * `renderLoop`'s body (the `my $<param>` / `my $<index>` bindings it
246
+ * introduces). `resolveModuleStringConst` consults this so a loop
244
247
  * variable whose name happens to match a module string const is NOT
245
248
  * inlined as the const literal (mirrors the Go adapter's loop-param /
246
- * loop-var shadowing guards). (#1749 review)
249
+ * loop-var shadowing guards). (#1749 review) Threaded via
250
+ * save/restore-by-reference around `renderChildren(loop.children)` in
251
+ * `renderLoop` (immutable — no ref-count bookkeeping), mirroring the
252
+ * Stage 1a/1b `ctx.scope` precedent in `jsx-to-ir.ts`. `IRLoop` already
253
+ * structurally satisfies `LoopBindingSource`
254
+ * (`param`/`index`/`paramBindings`/`preamble`), so `renderLoop` passes
255
+ * the loop node straight to `enterLoopRow`.
247
256
  */
248
- private loopBoundNames: Map<string, number> = new Map()
257
+ private scope: BindingScope = BindingScope.EMPTY
249
258
  /**
250
259
  * Prop names whose value is `undef` in the template body when the caller
251
260
  * omits them — so a bare-reference attribute should be dropped rather
@@ -293,7 +302,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
293
302
  this._searchParamsLocals = searchParamsLocalNames(ir.metadata)
294
303
  this._loweringMatchers = prepareLoweringMatchers(ir.metadata)
295
304
  this.localConstants = ir.metadata.localConstants ?? []
296
- this.loopBoundNames.clear()
305
+ this.scope = BindingScope.EMPTY
297
306
  this.errors = []
298
307
  this.childrenCaptureCounter = 0
299
308
 
@@ -388,7 +397,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
388
397
  // Inside a `.map()` callback, a param that shares a boolean prop's name
389
398
  // is the ROW binding, not the prop — route it through plain string
390
399
  // emission instead of `bf->bool_str` (#2488).
391
- if (this.loopBoundNames.has(bare)) return false
400
+ if (this.scope.isBound(bare)) return false
392
401
  return this.booleanTypedProps.has(bare)
393
402
  }
394
403
 
@@ -423,16 +432,15 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
423
432
  * function-scope consts never reach the per-render stash, so a bare
424
433
  * `$totalPages` faults under strict mode.
425
434
  *
426
- * The `loopBoundNames` guard also covers the #2221 hazard (a loop
427
- * callback's own param shadowing this outer const's name): unlike the
428
- * Twig-family adapters' coarse, whole-component `collectLoopBoundNames(ir)`
429
- * static set, this adapter's `loopBoundNames` is a LIVE ref-counted map
430
- * `renderLoop` populates/depopulates as it descends/ascends into each
431
- * loop body (#1749) — so it's already scope-precise for this call site;
432
- * no separate `staticLoopSourceBoundNames`-style field is needed here.
435
+ * The threaded scope's shadow guard also covers the #2221 hazard (a loop
436
+ * callback's own param shadowing this outer const's name): `this.scope`
437
+ * is position-accurate (#2482 Stage 2) — pushed/popped by reference as
438
+ * `renderLoop` descends/ascends into each loop body so it's already
439
+ * scope-precise for this call site; no separate coarse whole-component
440
+ * field is needed here.
433
441
  */
434
442
  private resolveLiteralConst(name: string): string | null {
435
- if (this.loopBoundNames?.has?.(name)) return null
443
+ if (this.scope.isBound(name)) return null
436
444
  const c = (this.localConstants ?? []).find(lc => lc.name === name)
437
445
  if (c?.value === undefined) return null
438
446
  const v = c.value.trim()
@@ -443,8 +451,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
443
451
  }
444
452
 
445
453
  private resolveStaticRecordLiteral(objectName: string, key: string): string | null {
446
- if (this.loopBoundNames?.has?.(objectName)) return null
447
- const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants)
454
+ const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants, name => this.scope.isBound(name))
448
455
  if (!hit) return null
449
456
  return hit.kind === 'number'
450
457
  ? hit.text
@@ -454,7 +461,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
454
461
  private resolveModuleStringConst(name: string): string | null {
455
462
  // A loop body introduces `my $<param>` / `my $<index>` bindings that
456
463
  // shadow a module const of the same name — never inline inside one.
457
- if (this.loopBoundNames.has(name)) return null
464
+ if (this.scope.isBound(name)) return null
458
465
  const value = this.moduleStringConsts.get(name)
459
466
  if (value === undefined) return null
460
467
  return `'${value.replace(/[\\']/g, m => `\\${m}`)}'`
@@ -902,10 +909,10 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
902
909
  // `Object.entries(props.tags).filter(...)`) still refuses below.
903
910
  // `isNameShadowed` guards a DIFFERENT, enclosing loop's own callback
904
911
  // param shadowing this identifier (fable review) — reuses the same
905
- // live `loopBoundNames` ref-counted tracking `resolveModuleStringConst`
906
- // already consults for this hazard class (#1749).
912
+ // threaded, position-accurate `this.scope` `resolveModuleStringConst`
913
+ // already consults for this hazard class (#1749/#2482 Stage 2).
907
914
  const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
908
- isNameShadowed: name => this.loopBoundNames.has(name),
915
+ isNameShadowed: this.scope.asShadowPredicate(),
909
916
  })
910
917
  const staticArray = staticItems !== null ? staticValueToPerl(staticItems) : null
911
918
 
@@ -921,6 +928,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
921
928
  suggestion: {
922
929
  message:
923
930
  'Pre-compute the array server-side and pass it as a prop, or mark the loop position as @client-only so it runs in JS on the client.',
931
+ escape: [{ kind: 'prop-precompute' }, { kind: 'client-directive' }],
924
932
  },
925
933
  })
926
934
  }
@@ -951,26 +959,21 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
951
959
  const indexVar = loop.iterationShape === 'keys'
952
960
  ? `$${param}`
953
961
  : loop.index ? `$${loop.index}` : '$_i'
954
- // Names this loop binds in body scope. Guard module-const inlining for
955
- // the whole body (children + key + filter) so a same-named loop variable
956
- // isn't replaced by the const literal (#1749 review). Ref-counted for
957
- // nested loops; released after the body lines are assembled below.
958
- const loopBound = loop.objectIteration === 'entries'
959
- ? [param, loop.index ?? '_k']
960
- : loop.objectIteration === 'keys' || loop.objectIteration === 'values' || loop.iterationShape === 'keys'
961
- ? [param]
962
- : supportableDestructure
963
- ? ['__bf_item', ...(loop.paramBindings ?? []).map(b => b.name), loop.index ?? '_i']
964
- : [param, loop.index ?? '_i']
965
- // A `.map()` callback preamble lowers to one per-row `my` local per
966
- // declaration (#2447). Loop-binding the names keeps a same-named module
967
- // const from inlining over the local the loop just declared — the same
968
- // hazard class `resolveModuleStringConst` already consults this map for.
962
+ // This loop's row scope. Guards module-const inlining for the whole
963
+ // body (children + key + filter) so a same-named loop variable isn't
964
+ // replaced by the const literal (#1749 review). `IRLoop` already
965
+ // structurally satisfies `LoopBindingSource`
966
+ // (`param`/`index`/`paramBindings`/`preamble`) `enterLoopRow(loop)`
967
+ // binds exactly what this renderLoop's header + preamble locals
968
+ // introduce (#2482 Stage 2 replaces the ref-counted `Map` this
969
+ // adapter used to carry). Restored by reference (immutable — no
970
+ // ref-count bookkeeping) at the matching pop below, once the WHOLE
971
+ // body (including the filter predicate) has been rendered except for
972
+ // one temporary drop-to-outer window around the hoisted sort-comparator
973
+ // emission below, since that runs OUTSIDE this loop.
969
974
  const preambleDecls = loop.preamble?.declarations ?? []
970
- for (const d of preambleDecls) loopBound.push(d.name)
971
- for (const n of loopBound) {
972
- this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1)
973
- }
975
+ const prevScope = this.scope
976
+ this.scope = prevScope.enterLoopRow(loop)
974
977
  const prevLoopKeyDepth = this.currentLoopKeyDepth
975
978
  this.currentLoopKeyDepth = loop.depth
976
979
  const renderedChildren = this.renderChildren(loop.children)
@@ -995,18 +998,15 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
995
998
  // `bf->sort_eval`; fall back to the structured `bf->sort` for a
996
999
  // comparator the evaluator can't model (e.g. `localeCompare`).
997
1000
  //
998
- // The hoisted sort runs OUTSIDE this loop, so this loop's bound names
1001
+ // The hoisted sort runs OUTSIDE this loop, so this loop's row scope
999
1002
  // must not shadow the comparator's captured free vars while emitting the
1000
1003
  // env — otherwise a captured var that happens to share a loop-param name
1001
1004
  // is blocked from inlining its module const and renders as an undefined
1002
- // `$name` (strict-mode fault, Copilot review #2035). Drop this loop's
1003
- // bound names for the sort emit, then restore (a nested loop's outer
1004
- // bindings, ref-counted, stay in effect).
1005
- for (const n of loopBound) {
1006
- const c = (this.loopBoundNames.get(n) ?? 1) - 1
1007
- if (c <= 0) this.loopBoundNames.delete(n)
1008
- else this.loopBoundNames.set(n, c)
1009
- }
1005
+ // `$name` (strict-mode fault, Copilot review #2035). Drop back to the
1006
+ // outer (pre-row) scope for the sort emit, then restore the row scope
1007
+ // below (a nested loop's own outer bindings stay in effect throughout,
1008
+ // since `prevScope` already carries them).
1009
+ this.scope = prevScope
1010
1010
  const sortEmit = (e: ParsedExpr) => this.convertExpressionToPerl('', e)
1011
1011
  // `loop.sortComparator` is the generic `IRLoopSort` (#2018 P5): serialize
1012
1012
  // the arrow body for the evaluator (eval-first), recover the structured
@@ -1030,9 +1030,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
1030
1030
  )
1031
1031
  sorted = rawArray
1032
1032
  }
1033
- for (const n of loopBound) {
1034
- this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1)
1035
- }
1033
+ this.scope = prevScope.enterLoopRow(loop)
1036
1034
  lines.push(`% my $${sortedHoist} = ${sorted};`)
1037
1035
  }
1038
1036
  if (loop.objectIteration) {
@@ -1121,12 +1119,8 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
1121
1119
  lines.push(children)
1122
1120
  }
1123
1121
 
1124
- // Body fully rendered — release the loop-bound names.
1125
- for (const n of loopBound) {
1126
- const c = (this.loopBoundNames.get(n) ?? 1) - 1
1127
- if (c <= 0) this.loopBoundNames.delete(n)
1128
- else this.loopBoundNames.set(n, c)
1129
- }
1122
+ // Body fully rendered — restore the outer (pre-row) scope.
1123
+ this.scope = prevScope
1130
1124
 
1131
1125
  lines.push(`% }`)
1132
1126
  lines.push(`<%== bf->comment("/loop:${loop.markerId}") %>`)
@@ -1395,7 +1389,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
1395
1389
  // Inside a `.map()` callback, a param that shadows a nullable
1396
1390
  // optional prop's name is the row binding, not the prop — skip the
1397
1391
  // spurious `defined` guard (#2488).
1398
- !this.loopBoundNames.has(normalizedBareId)
1392
+ !this.scope.isBound(normalizedBareId)
1399
1393
  ) {
1400
1394
  const perl = this.convertExpressionToPerl(value.expr)
1401
1395
  const body =
@@ -1527,14 +1521,14 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
1527
1521
  // lowering. Only function-scope (`!isModule`) consts whose value is
1528
1522
  // NOT itself a bare identifier (loop guard) are considered.
1529
1523
  //
1530
- // `loopBoundNames` guard (#2221): an enclosing `.map()` callback's
1524
+ // `this.scope` shadow guard (#2221): an enclosing `.map()` callback's
1531
1525
  // own param can shadow this outer const's name (`.map((sizeAttrs)
1532
1526
  // => <li {...sizeAttrs} />)`) — without the guard this forwarded
1533
1527
  // the OUTER const's hashref at every iteration instead of the
1534
- // per-item `$sizeAttrs` value. Same live ref-counted map
1535
- // `resolveLiteralConst` / `resolveStaticRecordLiteral` already
1528
+ // per-item `$sizeAttrs` value. Same threaded, position-accurate
1529
+ // scope `resolveLiteralConst` / `resolveStaticRecordLiteral` already
1536
1530
  // consult for this hazard class (#1749).
1537
- if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed) && !this.loopBoundNames.has(trimmed)) {
1531
+ if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed) && !this.scope.isBound(trimmed)) {
1538
1532
  const localConst = this.localConstants.find(
1539
1533
  c => c.name === trimmed && !c.isModule,
1540
1534
  )
@@ -13,6 +13,9 @@ export const conformancePins: ConformancePins = {
13
13
  // JS-runtime target runs it, a DSL adapter surfaces BF021 + `/* @client */`.
14
14
  // See spec/callback-fidelity.md.
15
15
  'filter-typeof-predicate': [{ code: 'BF021', severity: 'error' }],
16
+ // Array-builder `.map()` body (imperative `push`-into-array preamble):
17
+ // BF021, with a verified `/* @client */` escape — `map-array-builder-
18
+ // body-client` (#2613). See that fixture's docstring.
16
19
  'map-array-builder-body': [{ code: 'BF021', severity: 'error' }],
17
20
  'map-array-builder-escaping': [{ code: 'BF021', severity: 'error' }],
18
21
  // `.fill(value)` mutates the receiver in place — no template lowering
@@ -70,14 +73,22 @@ export const conformancePins: ConformancePins = {
70
73
  // check in `renderLoop`. This was always true; it was simply
71
74
  // unreachable before because BF104 refused the destructure shape first.
72
75
  'static-array-from-props': [
73
- { code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2321' },
76
+ {
77
+ code: 'BF101',
78
+ severity: 'error',
79
+ issue: 'https://github.com/piconic-ai/barefootjs/issues/2321',
80
+ },
74
81
  ],
75
82
  // The BF101 above fires; BF104 no longer does (see above), and BF103
76
83
  // (sibling-imported `<Tag>` child component in the loop body) no longer
77
84
  // does either now that the conformance harness passes
78
85
  // `siblingTemplatesRegistered: true` (#2205).
79
86
  'static-array-from-props-with-component': [
80
- { code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2321' },
87
+ {
88
+ code: 'BF101',
89
+ severity: 'error',
90
+ issue: 'https://github.com/piconic-ai/barefootjs/issues/2321',
91
+ },
81
92
  ],
82
93
  // #1310 / #2087: rest destructure in .map() callback. All four shapes
83
94
  // now lower via #2087 Phase B's `segments`-walking accessor:
@@ -107,9 +118,7 @@ export const conformancePins: ConformancePins = {
107
118
  // NOT pinned: Mojo lowers it to a real inline Perl `grep` and must
108
119
  // render to Hono parity instead.
109
120
  // Faithful lowering tracked: https://github.com/piconic-ai/barefootjs/issues/2320 (successor to #2038)
110
- 'filter-nested-find-predicate': [
111
- { code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2320' },
112
- ],
121
+ 'filter-nested-find-predicate': [{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2320' }],
113
122
  // #1467 demo-corpus context providers (`radio-group`, `accordion`,
114
123
  // `dialog`, `popover`, `select`, `dropdown-menu`, `combobox`,
115
124
  // `command`) are no longer pinned — an object-literal provider value