@barefootjs/jsx 0.28.1 → 0.29.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/index.js CHANGED
@@ -14046,7 +14046,7 @@ function emitTemplateCloneLines(template, indent) {
14046
14046
  ];
14047
14047
  }
14048
14048
  function emitLoopItemElementSetup(lines, opts) {
14049
- const { template, bodyIsMultiRoot, indent, singleRootLayout } = opts;
14049
+ const { template, bodyIsMultiRoot, indent, singleRootLayout, mountRow } = opts;
14050
14050
  const innerIndent = indent + " ";
14051
14051
  if (bodyIsMultiRoot) {
14052
14052
  lines.push(`${indent}let __el, __extras`);
@@ -14057,18 +14057,21 @@ function emitLoopItemElementSetup(lines, opts) {
14057
14057
  lines.push(ln);
14058
14058
  }
14059
14059
  lines.push(`${innerIndent}__el.__bfExtras = __extras`);
14060
+ if (mountRow)
14061
+ lines.push(`${innerIndent}mountRowRoot(__el)`);
14060
14062
  lines.push(`${indent}}`);
14061
14063
  return;
14062
14064
  }
14063
14065
  if (singleRootLayout === "inline") {
14064
14066
  const cloneExpr = emitTemplateCloneInline(template);
14065
- lines.push(`${indent}const __el = __existing ?? (() => { ${cloneExpr} })()`);
14067
+ const clone = `__existing ?? (() => { ${cloneExpr} })()`;
14068
+ lines.push(`${indent}const __el = ${mountRow ? `__existing ?? mountRowRoot((() => { ${cloneExpr} })())` : clone}`);
14066
14069
  return;
14067
14070
  }
14068
- lines.push(`${indent}const __el = __existing ?? (() => {`);
14071
+ lines.push(`${indent}const __el = __existing ?? ${mountRow ? "mountRowRoot(" : ""}(() => {`);
14069
14072
  for (const ln of emitTemplateCloneLines(template, innerIndent))
14070
14073
  lines.push(ln);
14071
- lines.push(`${indent}})()`);
14074
+ lines.push(`${indent}})()${mountRow ? ")" : ""}`);
14072
14075
  }
14073
14076
  function emitMultiRootTemplateCloneLines(template, indent, varEl, varExtras) {
14074
14077
  const isSvg = multiRootTemplateNeedsSvgWrap(template);
@@ -15296,6 +15299,10 @@ function isValueReferenceIdentifier(id) {
15296
15299
  if ((ts13.isMethodDeclaration(parent) || ts13.isGetAccessorDeclaration(parent) || ts13.isSetAccessorDeclaration(parent)) && parent.name === id) {
15297
15300
  return false;
15298
15301
  }
15302
+ if (ts13.isPropertyDeclaration(parent) && parent.name === id)
15303
+ return false;
15304
+ if (ts13.isMetaProperty(parent) && parent.name === id)
15305
+ return false;
15299
15306
  if (ts13.isVariableDeclaration(parent) && parent.name === id)
15300
15307
  return false;
15301
15308
  if (ts13.isFunctionDeclaration(parent) && parent.name === id)
@@ -15369,6 +15376,7 @@ var RUNTIME_IMPORT_CANDIDATES = [
15369
15376
  "registerTemplate",
15370
15377
  "initChild",
15371
15378
  "upsertChild",
15379
+ "mountRowRoot",
15372
15380
  "createPortal",
15373
15381
  "provideContext",
15374
15382
  "createContext",
@@ -20227,7 +20235,8 @@ function stringifyCompositeLoop(lines, plan) {
20227
20235
  template,
20228
20236
  bodyIsMultiRoot,
20229
20237
  indent: bodyIndent,
20230
- singleRootLayout: "multiline"
20238
+ singleRootLayout: "multiline",
20239
+ mountRow: true
20231
20240
  });
20232
20241
  emitComponentAndEventSetup(lines, bodyIndent, "__el", compsArr, eventsArr, loopParam, loopParamBindings, bodyIsMultiRoot);
20233
20242
  if (innerLoops.length > 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"composite-loop.d.ts","sourceRoot":"","sources":["../../../../src/ir-to-client-js/control-flow/stringify/composite-loop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAOH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAEzD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,iBAAiB,GAAG,IAAI,CAuGrF"}
1
+ {"version":3,"file":"composite-loop.d.ts","sourceRoot":"","sources":["../../../../src/ir-to-client-js/control-flow/stringify/composite-loop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAOH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAEzD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,iBAAiB,GAAG,IAAI,CA2GrF"}
@@ -125,6 +125,20 @@ export declare function emitLoopItemElementSetup(lines: string[], opts: {
125
125
  indent: string;
126
126
  /** Single-root layout: 'inline' (plain / branch-plain) or 'multiline' (composite). */
127
127
  singleRootLayout: 'inline' | 'multiline';
128
+ /**
129
+ * Emit `mountRowRoot(__el)` on the FRESH branch, connecting the row at the
130
+ * mount point `mapArray` handed down before the body's tail runs.
131
+ *
132
+ * Only bodies that initialise something inside the row need it — the tail
133
+ * is where `useContext` would otherwise resolve against a detached element
134
+ * and fall through to the global last-writer-wins store. A row with no
135
+ * nested init has nothing to resolve, so plain loops leave this off and
136
+ * their emission (and the `mapArrayLazy` measurements) are untouched.
137
+ *
138
+ * Never on the hydration branch: that row came from SSR markup and is in
139
+ * the document already.
140
+ */
141
+ mountRow?: boolean;
128
142
  }): void;
129
143
  /**
130
144
  * Multi-root template clone for loop bodies that emit a JSX Fragment with
@@ -1 +1 @@
1
- {"version":3,"file":"template-parse.d.ts","sourceRoot":"","sources":["../../../../src/ir-to-client-js/control-flow/stringify/template-parse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAkBH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAsB3D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAIvE;AAkDD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKhE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAKvH;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,GAAG,MAAM,CAIjF;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAajF;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,MAAM,EAAE,EACf,IAAI,EAAE;IACJ,QAAQ,EAAE,MAAM,CAAA;IAChB,eAAe,EAAE,OAAO,CAAA;IACxB,MAAM,EAAE,MAAM,CAAA;IACd,sFAAsF;IACtF,gBAAgB,EAAE,QAAQ,GAAG,WAAW,CAAA;CACzC,GACA,IAAI,CAuBN;AAED;;;;;;;;;GASG;AACH,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAChB,MAAM,EAAE,CAeV"}
1
+ {"version":3,"file":"template-parse.d.ts","sourceRoot":"","sources":["../../../../src/ir-to-client-js/control-flow/stringify/template-parse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAkBH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAsB3D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAIvE;AAkDD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKhE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAKvH;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,GAAG,MAAM,CAIjF;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAajF;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,MAAM,EAAE,EACf,IAAI,EAAE;IACJ,QAAQ,EAAE,MAAM,CAAA;IAChB,eAAe,EAAE,OAAO,CAAA;IACxB,MAAM,EAAE,MAAM,CAAA;IACd,sFAAsF;IACtF,gBAAgB,EAAE,QAAQ,GAAG,WAAW,CAAA;IACxC;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,GACA,IAAI,CA4BN;AAED;;;;;;;;;GASG;AACH,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAChB,MAAM,EAAE,CAeV"}
@@ -2,9 +2,9 @@
2
2
  * Import detection and DOM import management.
3
3
  */
4
4
  import type { ComponentIR } from '../types.ts';
5
- export declare const RUNTIME_IMPORT_CANDIDATES: readonly ['createSignal', 'createMemo', 'createEffect', 'onCleanup', 'onMount', 'hydrate', 'insert', 'getLoopChildren', 'getLoopNodes', 'mapArray', 'mapArrayAnchored', 'mapArrayLazy', 'patchLeaf', 'createDisposableEffect', 'createComponent', 'renderChild', 'registerComponent', 'registerTemplate', 'initChild', 'upsertChild', 'createPortal', 'provideContext', 'createContext', 'useContext', 'forwardProps', 'applyRestAttrs', 'splitProps', 'spreadAttrs', 'styleToCss', 'escapeAttr', 'escapeText', 'escapeTextOrNode', 'qsa', 'qsaItem', 'qsaChildScope', 'qsaChildScopes', 'upsertChildItem', '__slot', '__bfSlot', '__bfText', 'claimSlots', 'lazySlots', 'lazyClaimSlots', 'textOrNode', 'beginTurn', 'endTurn', 'date', 'formatDate'];
5
+ export declare const RUNTIME_IMPORT_CANDIDATES: readonly ['createSignal', 'createMemo', 'createEffect', 'onCleanup', 'onMount', 'hydrate', 'insert', 'getLoopChildren', 'getLoopNodes', 'mapArray', 'mapArrayAnchored', 'mapArrayLazy', 'patchLeaf', 'createDisposableEffect', 'createComponent', 'renderChild', 'registerComponent', 'registerTemplate', 'initChild', 'upsertChild', 'mountRowRoot', 'createPortal', 'provideContext', 'createContext', 'useContext', 'forwardProps', 'applyRestAttrs', 'splitProps', 'spreadAttrs', 'styleToCss', 'escapeAttr', 'escapeText', 'escapeTextOrNode', 'qsa', 'qsaItem', 'qsaChildScope', 'qsaChildScopes', 'upsertChildItem', '__slot', '__bfSlot', '__bfText', 'claimSlots', 'lazySlots', 'lazyClaimSlots', 'textOrNode', 'beginTurn', 'endTurn', 'date', 'formatDate'];
6
6
  /** @deprecated Use RUNTIME_IMPORT_CANDIDATES */
7
- export declare const DOM_IMPORT_CANDIDATES: readonly ["createSignal", "createMemo", "createEffect", "onCleanup", "onMount", "hydrate", "insert", "getLoopChildren", "getLoopNodes", "mapArray", "mapArrayAnchored", "mapArrayLazy", "patchLeaf", "createDisposableEffect", "createComponent", "renderChild", "registerComponent", "registerTemplate", "initChild", "upsertChild", "createPortal", "provideContext", "createContext", "useContext", "forwardProps", "applyRestAttrs", "splitProps", "spreadAttrs", "styleToCss", "escapeAttr", "escapeText", "escapeTextOrNode", "qsa", "qsaItem", "qsaChildScope", "qsaChildScopes", "upsertChildItem", "__slot", "__bfSlot", "__bfText", "claimSlots", "lazySlots", "lazyClaimSlots", "textOrNode", "beginTurn", "endTurn", "date", "formatDate"];
7
+ export declare const DOM_IMPORT_CANDIDATES: readonly ["createSignal", "createMemo", "createEffect", "onCleanup", "onMount", "hydrate", "insert", "getLoopChildren", "getLoopNodes", "mapArray", "mapArrayAnchored", "mapArrayLazy", "patchLeaf", "createDisposableEffect", "createComponent", "renderChild", "registerComponent", "registerTemplate", "initChild", "upsertChild", "mountRowRoot", "createPortal", "provideContext", "createContext", "useContext", "forwardProps", "applyRestAttrs", "splitProps", "spreadAttrs", "styleToCss", "escapeAttr", "escapeText", "escapeTextOrNode", "qsa", "qsaItem", "qsaChildScope", "qsaChildScopes", "upsertChildItem", "__slot", "__bfSlot", "__bfText", "claimSlots", "lazySlots", "lazyClaimSlots", "textOrNode", "beginTurn", "endTurn", "date", "formatDate"];
8
8
  export declare const RUNTIME_MODULE = "@barefootjs/client/runtime";
9
9
  export declare const IMPORT_PLACEHOLDER = "/* __BAREFOOTJS_DOM_IMPORTS__ */";
10
10
  export declare const MODULE_CONSTANTS_PLACEHOLDER = "/* __MODULE_LEVEL_CONSTANTS__ */";
@@ -1 +1 @@
1
- {"version":3,"file":"imports.d.ts","sourceRoot":"","sources":["../../src/ir-to-client-js/imports.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAU,MAAM,aAAa,CAAA;AAKtD,eAAO,MAAM,yBAAyB,YACpC,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,SAAS,EACpE,SAAS,EAAE,QAAQ,EAAE,iBAAiB,EAAE,cAAc,EAAE,UAAU,EAAE,kBAAkB,EAAE,cAAc,EAAE,WAAW,EAAE,wBAAwB,EAC7I,iBAAiB,EAAE,aAAa,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,WAAW,EAAE,aAAa,EACrG,cAAc,EACd,gBAAgB,EAAE,eAAe,EAAE,YAAY,EAC/C,cAAc,EAAE,gBAAgB,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,kBAAkB,EAC3H,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAUxG,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,YAAY,EAEzD,WAAW,EAAE,SAAS,EAItB,MAAM,EAIN,YAAY,CACJ,CAAA;AAEV,gDAAgD;AAChD,eAAO,MAAM,qBAAqB,wtBAA4B,CAAA;AAE9D,eAAO,MAAM,cAAc,+BAA+B,CAAA;AAE1D,eAAO,MAAM,kBAAkB,qCAAqC,CAAA;AACpE,eAAO,MAAM,4BAA4B,qCAAqC,CAAA;AAE9E;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAqB3D;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,WAAW,GAAG,MAAM,EAAE,CAoB/D;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,MAAM,GAAG,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAWxF;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,EAAE,mBAAmB,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAsCvH"}
1
+ {"version":3,"file":"imports.d.ts","sourceRoot":"","sources":["../../src/ir-to-client-js/imports.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAU,MAAM,aAAa,CAAA;AAKtD,eAAO,MAAM,yBAAyB,YACpC,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,SAAS,EACpE,SAAS,EAAE,QAAQ,EAAE,iBAAiB,EAAE,cAAc,EAAE,UAAU,EAAE,kBAAkB,EAAE,cAAc,EAAE,WAAW,EAAE,wBAAwB,EAC7I,iBAAiB,EAAE,aAAa,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,WAAW,EAAE,aAAa,EAKrG,cAAc,EACd,cAAc,EACd,gBAAgB,EAAE,eAAe,EAAE,YAAY,EAC/C,cAAc,EAAE,gBAAgB,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,kBAAkB,EAC3H,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAUxG,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,YAAY,EAEzD,WAAW,EAAE,SAAS,EAItB,MAAM,EAIN,YAAY,CACJ,CAAA;AAEV,gDAAgD;AAChD,eAAO,MAAM,qBAAqB,wuBAA4B,CAAA;AAE9D,eAAO,MAAM,cAAc,+BAA+B,CAAA;AAE1D,eAAO,MAAM,kBAAkB,qCAAqC,CAAA;AACpE,eAAO,MAAM,4BAA4B,qCAAqC,CAAA;AAE9E;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAqB3D;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,WAAW,GAAG,MAAM,EAAE,CAoB/D;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,MAAM,GAAG,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAWxF;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,EAAE,mBAAmB,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAsCvH"}
@@ -24,13 +24,26 @@ import ts from 'typescript';
24
24
  * A ShorthandPropertyAssignment (`{ Theme }`) intentionally counts as a
25
25
  * reference — it reads the binding, it doesn't just spell its name.
26
26
  *
27
+ * CONTRACT: this classifies identifier positions in **JavaScript** source.
28
+ * Both current callers parse with `ts.ScriptKind.JS` —
29
+ * `collectValueReferencedNames` below, and `detectStrippedReferences` in
30
+ * `packages/cli/src/lib/resolve-imports.ts`, which parses the assembled
31
+ * bundle. TypeScript-only positions are deliberately NOT handled: an
32
+ * identifier in a type position (`const x: Foo = …`, a
33
+ * `TypeReferenceNode`) is still reported as a value reference, and so are
34
+ * `interface` / `type` / `enum` declaration names. Do not point this at
35
+ * TypeScript source — it would over-report there.
36
+ *
27
37
  * Caveat: this is a syntactic test, not a scope analysis. If a local
28
38
  * function parameter happens to share a name with an imported binding,
29
39
  * references inside that function's body will count as references to
30
- * the import (false positive). Acceptable: over-counting a reference
31
- * just means we keep an import we didn't strictly need, which is a
32
- * strict improvement over the alternative failure direction (dropping a
33
- * needed import and producing a `ReferenceError`).
40
+ * the import (false positive). For the import-emission caller
41
+ * (`makeValueUsageTest` in `ir-to-client-js/imports.ts`), over-counting is
42
+ * the safe direction an extra import whose binding exists is harmless.
43
+ * It is NOT safe for the BF053 caller (`detectStrippedReferences`): there,
44
+ * an over-reported reference to a stripped binding is a false build
45
+ * error on legal code. That asymmetry is why every exclusion branch below
46
+ * matters — each one is a case that would otherwise misfire BF053.
34
47
  */
35
48
  export declare function isValueReferenceIdentifier(id: ts.Identifier): boolean;
36
49
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"value-references.d.ts","sourceRoot":"","sources":["../src/value-references.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,MAAM,YAAY,CAAA;AAE3B;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,0BAA0B,CAAC,EAAE,EAAE,EAAE,CAAC,UAAU,GAAG,OAAO,CAgCrE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CA0B5E"}
1
+ {"version":3,"file":"value-references.d.ts","sourceRoot":"","sources":["../src/value-references.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,MAAM,YAAY,CAAA;AAE3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,0BAA0B,CAAC,EAAE,EAAE,EAAE,CAAC,UAAU,GAAG,OAAO,CAyCrE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CA0B5E"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/jsx",
3
- "version": "0.28.1",
3
+ "version": "0.29.0",
4
4
  "description": "JSX compiler for BarefootJS - transforms JSX to server HTML + client JS",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -53,7 +53,7 @@
53
53
  "directory": "packages/jsx"
54
54
  },
55
55
  "dependencies": {
56
- "@barefootjs/shared": "0.28.1"
56
+ "@barefootjs/shared": "0.29.0"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "@barefootjs/client": ">=0.2.0",
@@ -310,6 +310,24 @@ describe('collectExternalImports', () => {
310
310
  const result = collectExternalImports(ir, code)
311
311
  expect(result).toEqual(["import { 日本語 } from 'some-lib'"])
312
312
  })
313
+
314
+ // #2432 follow-up: a class field name is a member key, not a read — the
315
+ // same treatment as an object-literal key above.
316
+ test('a specifier that appears ONLY as a class field name is not emitted (#2432 follow-up)', () => {
317
+ const ir = makeIR([makeImport('./utils', ['helper'])])
318
+ const code = 'class Widget { helper = 1 }'
319
+ const result = collectExternalImports(ir, code)
320
+ expect(result).toEqual([])
321
+ })
322
+
323
+ // #2432 follow-up: a COMPUTED class field name (`[helper]`) IS a read of
324
+ // the binding — pins that the class-field exclusion didn't over-exclude.
325
+ test('a specifier that appears as a COMPUTED class field name IS emitted (#2432 follow-up)', () => {
326
+ const ir = makeIR([makeImport('./utils', ['helper'])])
327
+ const code = 'class Widget { [helper] = 1 }'
328
+ const result = collectExternalImports(ir, code)
329
+ expect(result).toEqual(["import { helper } from './utils'"])
330
+ })
313
331
  })
314
332
 
315
333
  describe('collectUserDomImports', () => {
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Which code creates a loop row's root element, and whether that row is
3
+ * connected before its body's tail runs.
4
+ *
5
+ * The "connect before init" invariant exists because `useContext` resolves by
6
+ * walking `parentElement`: a child that initialises inside a detached row finds
7
+ * no ancestors and falls through to the global last-writer-wins context store,
8
+ * silently reading whichever provider on the page wrote last. So the creation
9
+ * site has to know its destination in advance, and there are two ways to tell
10
+ * it — a PLACEHOLDER already in the tree (`mountAt`, child slots), or the
11
+ * AMBIENT row mount point.
12
+ *
13
+ * Both work because a runtime FUNCTION creates the element and can take an
14
+ * argument. A row whose root is a template clone written inline in the emitted
15
+ * body has no such function, so the compiler emits `mountRowRoot(clone)` to
16
+ * consume the same ambient point — but only for the variant that initialises
17
+ * anything inside the row. A row with no nested init has nothing that could
18
+ * resolve wrongly, and skipping it there keeps the high-volume `mapArrayLazy`
19
+ * emission untouched.
20
+ *
21
+ * This matrix pins that split against compiled output rather than
22
+ * recollection. `mountsRow` is the load-bearing column: if it ever flips to
23
+ * false for a shape that initialises a child, that shape has silently gone
24
+ * back to initialising detached, and nothing else in the suite would say so —
25
+ * the wrong value it produces is only wrong when a second provider of the same
26
+ * context happens to be on the page.
27
+ */
28
+ import { describe, test, expect } from 'bun:test'
29
+ import { compileJSX } from '../compiler'
30
+ import { TestAdapter } from '../adapters/test-adapter'
31
+
32
+ interface RowShape {
33
+ /** How the row's root element comes into existence. */
34
+ rowRoot: 'createComponent' | 'template-clone'
35
+ /**
36
+ * Does a child inside the row reach its `init` through a placeholder slot
37
+ * (`upsertChild` / `upsertChildItem`)?
38
+ *
39
+ * Narrower than "something initialises in this row" — a conditional child
40
+ * arrives via `insert` + `initChild` and reads false here. Read it alongside
41
+ * `mountsRow`, never as a substitute.
42
+ *
43
+ * `initChild` on the row ROOT is deliberately excluded: that is the
44
+ * hydration branch adopting an SSR-rendered row, which is in the document by
45
+ * construction and never part of this problem.
46
+ */
47
+ nestedChildInit: boolean
48
+ /** Multi-root (Fragment) body — the sibling-walk lookup path. */
49
+ multiRoot: boolean
50
+ /** Is the fresh row connected before the body's tail runs? */
51
+ mountsRow: boolean
52
+ /** Which list runtime the loop compiles to. */
53
+ loopRuntime: string
54
+ }
55
+
56
+ function classify(loopBody: string): RowShape {
57
+ const source = `"use client"
58
+ import { createSignal } from '@barefootjs/client'
59
+ import { Chip } from './chip'
60
+ function T() {
61
+ const [items] = createSignal([{ id: '1', name: 'a' }])
62
+ return <ul>{items().map((it) => ${loopBody})}</ul>
63
+ }
64
+ export { T }`
65
+ const result = compileJSX(source, 'T.tsx', { adapter: new TestAdapter() })
66
+ const hard = result.errors.filter((e) => e.severity !== 'warning')
67
+ if (hard.length > 0) throw new Error(`unexpected compile error: ${hard.map((e) => e.code).join(',')}`)
68
+ const js = result.files.find((f) => f.type === 'clientJs')?.content ?? ''
69
+
70
+ // Slice from the list call to the end of the emitted init function.
71
+ // Everything the row does at setup time is in there, and the top-level
72
+ // `createComponent('T', …)` export sits after it, so it cannot be mistaken
73
+ // for a row root. Starting at the call also excludes the import line, so
74
+ // `mountsRow` reflects a real call site rather than the import of one.
75
+ const call = /mapArray(?:Lazy|Anchored)?\(/.exec(js)
76
+ if (!call) throw new Error('no list runtime call emitted')
77
+ const hydrateAt = js.indexOf('\nhydrate(')
78
+ const region = js.slice(call.index, hydrateAt === -1 ? undefined : hydrateAt)
79
+
80
+ return {
81
+ rowRoot: /cloneNode\(/.test(region) ? 'template-clone' : 'createComponent',
82
+ nestedChildInit: /upsertChild\(|upsertChildItem\(/.test(region),
83
+ multiRoot: /__bfExtras|upsertChildItem\(|qsaItem\(/.test(region),
84
+ mountsRow: /mountRowRoot\(/.test(region),
85
+ loopRuntime: /mapArray(?:Lazy|Anchored)?/.exec(region)?.[0] ?? 'unknown',
86
+ }
87
+ }
88
+
89
+ describe('loop row creation paths — who makes the row root, and when it is connected', () => {
90
+ test('component row root: the runtime creates it, so it takes the mount point itself', () => {
91
+ // `<Chip/>` IS the row. `createComponent` consumes the ambient point in its
92
+ // own step 7b, so no separate call is emitted.
93
+ expect(classify('<Chip key={it.id} id={it.id} />')).toEqual({
94
+ rowRoot: 'createComponent',
95
+ nestedChildInit: false,
96
+ multiRoot: false,
97
+ mountsRow: false,
98
+ loopRuntime: 'mapArray',
99
+ })
100
+ })
101
+
102
+ test('markup row root with no component inside: a clone, and deliberately not mounted', () => {
103
+ // Nothing initialises inside this row, so there is no `useContext` call
104
+ // during setup to resolve wrongly — and this is the high-volume path, so
105
+ // leaving it alone keeps the `mapArrayLazy` emission and the measurements
106
+ // taken against it untouched. `mountsRow: false` here is a decision.
107
+ expect(classify('<li key={it.id}>{it.name}</li>')).toEqual({
108
+ rowRoot: 'template-clone',
109
+ nestedChildInit: false,
110
+ multiRoot: false,
111
+ mountsRow: false,
112
+ loopRuntime: 'mapArrayLazy',
113
+ })
114
+ })
115
+
116
+ test('markup row root with a nested component: clone, mounted before the tail', () => {
117
+ // The row root is user markup, so it is not and cannot become a component;
118
+ // `mountRowRoot` is what gives it a destination. Flip `mountsRow` to false
119
+ // and `upsertChild` goes back to running the child's `init` against a
120
+ // detached element.
121
+ expect(classify('<li key={it.id}><Chip id={it.id} /></li>')).toEqual({
122
+ rowRoot: 'template-clone',
123
+ nestedChildInit: true,
124
+ multiRoot: false,
125
+ mountsRow: true,
126
+ loopRuntime: 'mapArray',
127
+ })
128
+ })
129
+
130
+ test('two nested components in one markup row: same case, one mount', () => {
131
+ expect(classify('<li key={it.id}><Chip id={it.id} /><Chip id={it.name} /></li>')).toEqual({
132
+ rowRoot: 'template-clone',
133
+ nestedChildInit: true,
134
+ multiRoot: false,
135
+ mountsRow: true,
136
+ loopRuntime: 'mapArray',
137
+ })
138
+ })
139
+
140
+ test('Fragment row with a nested component: mounted, and the multi-root lookup copes', () => {
141
+ // A Fragment root can never be a component, so this shape only became
142
+ // reachable for a mount point once the clone path got one. It is also the
143
+ // multi-root lookup, whose sibling walk in `qsa-item.ts` used to end with
144
+ // `return` and skip the `__bfExtras` stash the moment the primary was
145
+ // attached — a `break` now, so an attached primary still finds its extras.
146
+ expect(classify('<><Chip id={it.id} /><span>{it.name}</span></>')).toEqual({
147
+ rowRoot: 'template-clone',
148
+ nestedChildInit: true,
149
+ multiRoot: true,
150
+ mountsRow: true,
151
+ loopRuntime: 'mapArray',
152
+ })
153
+ })
154
+
155
+ test('conditional child in a markup row: mounted, though it inits through `insert`', () => {
156
+ // `nestedChildInit` is false because this child arrives via `insert` +
157
+ // `initChild` rather than `upsertChild` — but it is still a child
158
+ // initialising inside the row, and the emission is keyed on the loop
159
+ // variant (composite), not on which call the child arrives through. This
160
+ // row is why the two columns have to be read separately.
161
+ expect(classify('<li key={it.id}>{it.name ? <Chip id={it.id} /> : null}</li>')).toEqual({
162
+ rowRoot: 'template-clone',
163
+ nestedChildInit: false,
164
+ multiRoot: false,
165
+ mountsRow: true,
166
+ loopRuntime: 'mapArray',
167
+ })
168
+ })
169
+ })
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Tests for `packages/jsx/src/value-references.ts` — the single-door
3
+ * "is this identifier a VALUE reference" classifier shared by
4
+ * `collectExternalImports` (jsx) and `detectStrippedReferences` (cli).
5
+ *
6
+ * There was no direct test file for this module before this one. It was
7
+ * added as a follow-up to #2432: a Copilot review on the original PR
8
+ * flagged that `ts.PropertyDeclaration` (class field) names were not
9
+ * excluded, so a class field like `helper` in `class C { helper = 1 }`
10
+ * was misclassified as a value reference. Harmless for the jsx-side
11
+ * caller (an extra kept import), but a false BF053 build error for the
12
+ * cli-side caller when the same name is a stripped import binding.
13
+ */
14
+
15
+ import { describe, test, expect } from 'bun:test'
16
+ import { collectValueReferencedNames } from '../value-references'
17
+
18
+ describe('collectValueReferencedNames', () => {
19
+ test('class field name only is NOT a reference', () => {
20
+ const result = collectValueReferencedNames('class C { helper = 1 }')
21
+ expect(result).not.toBeNull()
22
+ expect(result?.has('helper')).toBe(false)
23
+ })
24
+
25
+ test('static class field name only is NOT a reference', () => {
26
+ const result = collectValueReferencedNames('class C { static helper = 1 }')
27
+ expect(result).not.toBeNull()
28
+ expect(result?.has('helper')).toBe(false)
29
+ })
30
+
31
+ test('computed class field name IS a reference (does not over-exclude)', () => {
32
+ const result = collectValueReferencedNames('class C { [helper] = 1 }')
33
+ expect(result).not.toBeNull()
34
+ expect(result?.has('helper')).toBe(true)
35
+ })
36
+
37
+ test('class method name is NOT a reference', () => {
38
+ const result = collectValueReferencedNames('class C { helper() {} }')
39
+ expect(result).not.toBeNull()
40
+ expect(result?.has('helper')).toBe(false)
41
+ })
42
+
43
+ test('object-literal key is NOT a reference', () => {
44
+ const result = collectValueReferencedNames('const o = { helper: 1 };')
45
+ expect(result).not.toBeNull()
46
+ expect(result?.has('helper')).toBe(false)
47
+ })
48
+
49
+ test('property access name is NOT a reference', () => {
50
+ const result = collectValueReferencedNames('obj.helper();')
51
+ expect(result).not.toBeNull()
52
+ expect(result?.has('helper')).toBe(false)
53
+ })
54
+
55
+ test('shorthand property IS a reference (reads the binding)', () => {
56
+ const result = collectValueReferencedNames('const o = { helper };')
57
+ expect(result).not.toBeNull()
58
+ expect(result?.has('helper')).toBe(true)
59
+ })
60
+
61
+ test('a genuine call IS a reference', () => {
62
+ const result = collectValueReferencedNames('helper();')
63
+ expect(result).not.toBeNull()
64
+ expect(result?.has('helper')).toBe(true)
65
+ })
66
+
67
+ test('new.target: "target" is NOT a reference', () => {
68
+ const result = collectValueReferencedNames('function f() { return new.target; }')
69
+ expect(result).not.toBeNull()
70
+ expect(result?.has('target')).toBe(false)
71
+ })
72
+
73
+ // Both halves of the MetaProperty exclusion are pinned: `new.target`
74
+ // above, and `import.meta` here — the one Copilot flagged as uncovered
75
+ // on #2439. `import.meta.url` also incidentally covers the
76
+ // property-access half (`url` is excluded too).
77
+ test('import.meta: "meta" is NOT a reference', () => {
78
+ const result = collectValueReferencedNames('const u = import.meta.url;')
79
+ expect(result).not.toBeNull()
80
+ expect(result?.has('meta')).toBe(false)
81
+ expect(result?.has('url')).toBe(false)
82
+ })
83
+
84
+ test('unparseable text returns null ("cannot answer")', () => {
85
+ const result = collectValueReferencedNames('const x = ;;; {{{ ]]]')
86
+ expect(result).toBeNull()
87
+ })
88
+ })
@@ -119,6 +119,10 @@ export function stringifyCompositeLoop(lines: string[], plan: CompositeLoopPlan)
119
119
  bodyIsMultiRoot,
120
120
  indent: bodyIndent,
121
121
  singleRootLayout: 'multiline',
122
+ // Composite is exactly the variant that initialises something inside the
123
+ // row — nested components, inner loops, or both — so it is exactly the
124
+ // variant whose tail needs the row already connected.
125
+ mountRow: true,
122
126
  })
123
127
  emitComponentAndEventSetup(lines, bodyIndent, '__el', compsArr, eventsArr, loopParam, loopParamBindings, bodyIsMultiRoot)
124
128
  if (innerLoops.length > 0) {
@@ -251,9 +251,23 @@ export function emitLoopItemElementSetup(
251
251
  indent: string
252
252
  /** Single-root layout: 'inline' (plain / branch-plain) or 'multiline' (composite). */
253
253
  singleRootLayout: 'inline' | 'multiline'
254
+ /**
255
+ * Emit `mountRowRoot(__el)` on the FRESH branch, connecting the row at the
256
+ * mount point `mapArray` handed down before the body's tail runs.
257
+ *
258
+ * Only bodies that initialise something inside the row need it — the tail
259
+ * is where `useContext` would otherwise resolve against a detached element
260
+ * and fall through to the global last-writer-wins store. A row with no
261
+ * nested init has nothing to resolve, so plain loops leave this off and
262
+ * their emission (and the `mapArrayLazy` measurements) are untouched.
263
+ *
264
+ * Never on the hydration branch: that row came from SSR markup and is in
265
+ * the document already.
266
+ */
267
+ mountRow?: boolean
254
268
  },
255
269
  ): void {
256
- const { template, bodyIsMultiRoot, indent, singleRootLayout } = opts
270
+ const { template, bodyIsMultiRoot, indent, singleRootLayout, mountRow } = opts
257
271
  const innerIndent = indent + ' '
258
272
  if (bodyIsMultiRoot) {
259
273
  lines.push(`${indent}let __el, __extras`)
@@ -264,17 +278,22 @@ export function emitLoopItemElementSetup(
264
278
  lines.push(ln)
265
279
  }
266
280
  lines.push(`${innerIndent}__el.__bfExtras = __extras`)
281
+ // After the stash: `mountRowRoot` attaches the primary, and an attached
282
+ // primary makes `itemRootElements`' sibling walk the first thing a lookup
283
+ // sees — it must find the stash already in place behind it.
284
+ if (mountRow) lines.push(`${innerIndent}mountRowRoot(__el)`)
267
285
  lines.push(`${indent}}`)
268
286
  return
269
287
  }
270
288
  if (singleRootLayout === 'inline') {
271
289
  const cloneExpr = emitTemplateCloneInline(template)
272
- lines.push(`${indent}const __el = __existing ?? (() => { ${cloneExpr} })()`)
290
+ const clone = `__existing ?? (() => { ${cloneExpr} })()`
291
+ lines.push(`${indent}const __el = ${mountRow ? `__existing ?? mountRowRoot((() => { ${cloneExpr} })())` : clone}`)
273
292
  return
274
293
  }
275
- lines.push(`${indent}const __el = __existing ?? (() => {`)
294
+ lines.push(`${indent}const __el = __existing ?? ${mountRow ? 'mountRowRoot(' : ''}(() => {`)
276
295
  for (const ln of emitTemplateCloneLines(template, innerIndent)) lines.push(ln)
277
- lines.push(`${indent}})()`)
296
+ lines.push(`${indent}})()${mountRow ? ')' : ''}`)
278
297
  }
279
298
 
280
299
  /**
@@ -11,6 +11,11 @@ export const RUNTIME_IMPORT_CANDIDATES = [
11
11
  'createSignal', 'createMemo', 'createEffect', 'onCleanup', 'onMount',
12
12
  'hydrate', 'insert', 'getLoopChildren', 'getLoopNodes', 'mapArray', 'mapArrayAnchored', 'mapArrayLazy', 'patchLeaf', 'createDisposableEffect',
13
13
  'createComponent', 'renderChild', 'registerComponent', 'registerTemplate', 'initChild', 'upsertChild',
14
+ // Connects a template-clone loop row before the body's tail runs, so a child
15
+ // that inits inside it resolves context against real ancestors rather than
16
+ // falling through to the global store. The clone-root counterpart of the
17
+ // mount point `createComponent` consumes for component-root rows.
18
+ 'mountRowRoot',
14
19
  'createPortal',
15
20
  'provideContext', 'createContext', 'useContext',
16
21
  'forwardProps', 'applyRestAttrs', 'splitProps', 'spreadAttrs', 'styleToCss', 'escapeAttr', 'escapeText', 'escapeTextOrNode',
@@ -26,13 +26,26 @@ import ts from 'typescript'
26
26
  * A ShorthandPropertyAssignment (`{ Theme }`) intentionally counts as a
27
27
  * reference — it reads the binding, it doesn't just spell its name.
28
28
  *
29
+ * CONTRACT: this classifies identifier positions in **JavaScript** source.
30
+ * Both current callers parse with `ts.ScriptKind.JS` —
31
+ * `collectValueReferencedNames` below, and `detectStrippedReferences` in
32
+ * `packages/cli/src/lib/resolve-imports.ts`, which parses the assembled
33
+ * bundle. TypeScript-only positions are deliberately NOT handled: an
34
+ * identifier in a type position (`const x: Foo = …`, a
35
+ * `TypeReferenceNode`) is still reported as a value reference, and so are
36
+ * `interface` / `type` / `enum` declaration names. Do not point this at
37
+ * TypeScript source — it would over-report there.
38
+ *
29
39
  * Caveat: this is a syntactic test, not a scope analysis. If a local
30
40
  * function parameter happens to share a name with an imported binding,
31
41
  * references inside that function's body will count as references to
32
- * the import (false positive). Acceptable: over-counting a reference
33
- * just means we keep an import we didn't strictly need, which is a
34
- * strict improvement over the alternative failure direction (dropping a
35
- * needed import and producing a `ReferenceError`).
42
+ * the import (false positive). For the import-emission caller
43
+ * (`makeValueUsageTest` in `ir-to-client-js/imports.ts`), over-counting is
44
+ * the safe direction an extra import whose binding exists is harmless.
45
+ * It is NOT safe for the BF053 caller (`detectStrippedReferences`): there,
46
+ * an over-reported reference to a stripped binding is a false build
47
+ * error on legal code. That asymmetry is why every exclusion branch below
48
+ * matters — each one is a case that would otherwise misfire BF053.
36
49
  */
37
50
  export function isValueReferenceIdentifier(id: ts.Identifier): boolean {
38
51
  const parent = id.parent
@@ -47,6 +60,15 @@ export function isValueReferenceIdentifier(id: ts.Identifier): boolean {
47
60
  ) {
48
61
  return false
49
62
  }
63
+ // Class field name (`class C { helper = 1 }`, including `static helper
64
+ // = 1`): the name is a member key, not a read. The `parent.name === id`
65
+ // guard is what preserves the computed case — in `class C { [helper] =
66
+ // 1 }` the name is a ComputedPropertyName, not `id` itself, so `helper`
67
+ // (inside the brackets) still falls through and counts as a reference.
68
+ if (ts.isPropertyDeclaration(parent) && parent.name === id) return false
69
+ // `new.target` / `import.meta`: `target`/`meta` sits in keyword
70
+ // position, not a binding.
71
+ if (ts.isMetaProperty(parent) && parent.name === id) return false
50
72
  if (ts.isVariableDeclaration(parent) && parent.name === id) return false
51
73
  if (ts.isFunctionDeclaration(parent) && parent.name === id) return false
52
74
  if (ts.isFunctionExpression(parent) && parent.name === id) return false