@barefootjs/mojolicious 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.
@@ -1,5 +1,5 @@
1
1
  package BarefootJS::Backend::Mojo;
2
- our $VERSION = "0.31.2";
2
+ our $VERSION = "0.31.4";
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.2";
2
+ our $VERSION = "0.31.4";
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.2";
2
+ our $VERSION = "0.31.4";
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.3",
3
+ "version": "0.31.5",
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.3"
55
+ "@barefootjs/shared": "0.31.5"
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.3",
74
- "@barefootjs/vite": "0.31.3",
75
- "@barefootjs/client": "0.31.3",
73
+ "@barefootjs/jsx": "0.31.5",
74
+ "@barefootjs/vite": "0.31.5",
75
+ "@barefootjs/client": "0.31.5",
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
 
@@ -951,26 +958,21 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
951
958
  const indexVar = loop.iterationShape === 'keys'
952
959
  ? `$${param}`
953
960
  : 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.
961
+ // This loop's row scope. Guards module-const inlining for the whole
962
+ // body (children + key + filter) so a same-named loop variable isn't
963
+ // replaced by the const literal (#1749 review). `IRLoop` already
964
+ // structurally satisfies `LoopBindingSource`
965
+ // (`param`/`index`/`paramBindings`/`preamble`) `enterLoopRow(loop)`
966
+ // binds exactly what this renderLoop's header + preamble locals
967
+ // introduce (#2482 Stage 2 replaces the ref-counted `Map` this
968
+ // adapter used to carry). Restored by reference (immutable — no
969
+ // ref-count bookkeeping) at the matching pop below, once the WHOLE
970
+ // body (including the filter predicate) has been rendered except for
971
+ // one temporary drop-to-outer window around the hoisted sort-comparator
972
+ // emission below, since that runs OUTSIDE this loop.
969
973
  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
- }
974
+ const prevScope = this.scope
975
+ this.scope = prevScope.enterLoopRow(loop)
974
976
  const prevLoopKeyDepth = this.currentLoopKeyDepth
975
977
  this.currentLoopKeyDepth = loop.depth
976
978
  const renderedChildren = this.renderChildren(loop.children)
@@ -995,18 +997,15 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
995
997
  // `bf->sort_eval`; fall back to the structured `bf->sort` for a
996
998
  // comparator the evaluator can't model (e.g. `localeCompare`).
997
999
  //
998
- // The hoisted sort runs OUTSIDE this loop, so this loop's bound names
1000
+ // The hoisted sort runs OUTSIDE this loop, so this loop's row scope
999
1001
  // must not shadow the comparator's captured free vars while emitting the
1000
1002
  // env — otherwise a captured var that happens to share a loop-param name
1001
1003
  // 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
- }
1004
+ // `$name` (strict-mode fault, Copilot review #2035). Drop back to the
1005
+ // outer (pre-row) scope for the sort emit, then restore the row scope
1006
+ // below (a nested loop's own outer bindings stay in effect throughout,
1007
+ // since `prevScope` already carries them).
1008
+ this.scope = prevScope
1010
1009
  const sortEmit = (e: ParsedExpr) => this.convertExpressionToPerl('', e)
1011
1010
  // `loop.sortComparator` is the generic `IRLoopSort` (#2018 P5): serialize
1012
1011
  // the arrow body for the evaluator (eval-first), recover the structured
@@ -1030,9 +1029,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
1030
1029
  )
1031
1030
  sorted = rawArray
1032
1031
  }
1033
- for (const n of loopBound) {
1034
- this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1)
1035
- }
1032
+ this.scope = prevScope.enterLoopRow(loop)
1036
1033
  lines.push(`% my $${sortedHoist} = ${sorted};`)
1037
1034
  }
1038
1035
  if (loop.objectIteration) {
@@ -1121,12 +1118,8 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
1121
1118
  lines.push(children)
1122
1119
  }
1123
1120
 
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
- }
1121
+ // Body fully rendered — restore the outer (pre-row) scope.
1122
+ this.scope = prevScope
1130
1123
 
1131
1124
  lines.push(`% }`)
1132
1125
  lines.push(`<%== bf->comment("/loop:${loop.markerId}") %>`)
@@ -1395,7 +1388,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
1395
1388
  // Inside a `.map()` callback, a param that shadows a nullable
1396
1389
  // optional prop's name is the row binding, not the prop — skip the
1397
1390
  // spurious `defined` guard (#2488).
1398
- !this.loopBoundNames.has(normalizedBareId)
1391
+ !this.scope.isBound(normalizedBareId)
1399
1392
  ) {
1400
1393
  const perl = this.convertExpressionToPerl(value.expr)
1401
1394
  const body =
@@ -1527,14 +1520,14 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
1527
1520
  // lowering. Only function-scope (`!isModule`) consts whose value is
1528
1521
  // NOT itself a bare identifier (loop guard) are considered.
1529
1522
  //
1530
- // `loopBoundNames` guard (#2221): an enclosing `.map()` callback's
1523
+ // `this.scope` shadow guard (#2221): an enclosing `.map()` callback's
1531
1524
  // own param can shadow this outer const's name (`.map((sizeAttrs)
1532
1525
  // => <li {...sizeAttrs} />)`) — without the guard this forwarded
1533
1526
  // 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
1527
+ // per-item `$sizeAttrs` value. Same threaded, position-accurate
1528
+ // scope `resolveLiteralConst` / `resolveStaticRecordLiteral` already
1536
1529
  // consult for this hazard class (#1749).
1537
- if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed) && !this.loopBoundNames.has(trimmed)) {
1530
+ if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed) && !this.scope.isBound(trimmed)) {
1538
1531
  const localConst = this.localConstants.find(
1539
1532
  c => c.name === trimmed && !c.isModule,
1540
1533
  )