@barefootjs/jsx 0.27.0 → 0.28.1

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.
Files changed (65) hide show
  1. package/dist/analyzer.d.ts.map +1 -1
  2. package/dist/compiler.d.ts.map +1 -1
  3. package/dist/errors.d.ts +1 -0
  4. package/dist/errors.d.ts.map +1 -1
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +955 -273
  8. package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts +6 -0
  9. package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts.map +1 -1
  10. package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts +2 -1
  11. package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts.map +1 -1
  12. package/dist/ir-to-client-js/control-flow/plan/build-insert.d.ts +7 -0
  13. package/dist/ir-to-client-js/control-flow/plan/build-insert.d.ts.map +1 -1
  14. package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts +107 -0
  15. package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts.map +1 -0
  16. package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts +9 -1
  17. package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts +203 -0
  19. package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts.map +1 -0
  20. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +10 -0
  21. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
  22. package/dist/ir-to-client-js/control-flow/stringify/branch-loop.d.ts.map +1 -1
  23. package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts +98 -0
  24. package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -0
  25. package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts +5 -0
  26. package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
  27. package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
  28. package/dist/ir-to-client-js/control-flow.d.ts.map +1 -1
  29. package/dist/ir-to-client-js/imports.d.ts +26 -2
  30. package/dist/ir-to-client-js/imports.d.ts.map +1 -1
  31. package/dist/value-references.d.ts +48 -0
  32. package/dist/value-references.d.ts.map +1 -0
  33. package/package.json +2 -2
  34. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +162 -36
  35. package/src/__tests__/client-js-generation.test.ts +4 -1
  36. package/src/__tests__/composite-branch-loop.test.ts +12 -3
  37. package/src/__tests__/conditional-mapArray-key.test.ts +7 -1
  38. package/src/__tests__/create-selector.test.ts +21 -7
  39. package/src/__tests__/ir-to-client-js/imports.test.ts +98 -1
  40. package/src/__tests__/lazy-row-eligibility.test.ts +692 -0
  41. package/src/__tests__/loop-branch-bare-expression-reactive-text.test.ts +98 -0
  42. package/src/__tests__/loop-fallback-wrap.test.ts +31 -12
  43. package/src/__tests__/loop-hoisted-template.test.ts +10 -6
  44. package/src/__tests__/static-loop-csr-materialize.test.ts +6 -1
  45. package/src/__tests__/type-only-import-leak.test.ts +60 -0
  46. package/src/analyzer.ts +3 -0
  47. package/src/compiler.ts +6 -2
  48. package/src/errors.ts +11 -0
  49. package/src/index.ts +4 -0
  50. package/src/ir-to-client-js/collect-elements.ts +48 -13
  51. package/src/ir-to-client-js/control-flow/plan/branch-loop.ts +6 -0
  52. package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +35 -12
  53. package/src/ir-to-client-js/control-flow/plan/build-insert.ts +9 -2
  54. package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +305 -0
  55. package/src/ir-to-client-js/control-flow/plan/build-loop.ts +44 -12
  56. package/src/ir-to-client-js/control-flow/plan/lazy-row-eligibility.ts +440 -0
  57. package/src/ir-to-client-js/control-flow/plan/loop.ts +10 -0
  58. package/src/ir-to-client-js/control-flow/stringify/branch-loop.ts +22 -0
  59. package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +478 -0
  60. package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +6 -1
  61. package/src/ir-to-client-js/control-flow/stringify/loop.ts +23 -0
  62. package/src/ir-to-client-js/control-flow.ts +7 -2
  63. package/src/ir-to-client-js/imports.ts +53 -4
  64. package/src/jsx-to-ir.ts +18 -1
  65. package/src/value-references.ts +108 -0
@@ -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', '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', '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', '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", "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", "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", "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__ */";
@@ -17,6 +17,30 @@ export declare function detectUsedImports(code: string): Set<string>;
17
17
  * compiler-emitted /runtime subpath).
18
18
  */
19
19
  export declare function collectUserDomImports(ir: ComponentIR): string[];
20
+ /**
21
+ * Build the "is this local name used as a value in the generated code?"
22
+ * test used to decide which imported specifiers survive into the client
23
+ * bundle. Prefers a real value-reference set over the historical
24
+ * `\bname\b` text scan (#2432: an object key or string literal that
25
+ * merely spells an imported name used to emit a phantom import). Falls
26
+ * back to a substring scan when the generated text cannot be parsed
27
+ * cleanly — a partial parse would under-report references and DROP a
28
+ * needed import. The reference set is computed at most once per call.
29
+ *
30
+ * The fallback is a plain `includes()`, not a `\bname\b` regex: `\b` is
31
+ * defined over `[A-Za-z0-9_]`, so a `$`-prefixed name (`$fetch`, as
32
+ * exported by `ofetch`) or a non-ASCII local both sit outside a word
33
+ * boundary and would never match — silently dropping the import, the one
34
+ * failure direction this helper must never take. Worse, splicing
35
+ * `localName` straight into `new RegExp(...)` treated `$` as the
36
+ * end-of-input anchor, so `\b$fetch\b` couldn't match `$fetch` at all.
37
+ * `includes()` is deliberately COARSER than a word-boundary scan (it
38
+ * matches `helper` inside `helperFoo` too) — that's fine here: the
39
+ * fallback's only job is "never under-report", and over-keeping an
40
+ * import whose binding already exists is harmless, while dropping one is
41
+ * fatal.
42
+ */
43
+ export declare function makeValueUsageTest(generatedCode: string): (localName: string) => boolean;
20
44
  /**
21
45
  * Collect external (non-DOM, non-component) imports that are used in generated code.
22
46
  * These are third-party libraries like @barefootjs/form, zod, etc. that need to be
@@ -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;AAItD,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,WAAW,EAAE,wBAAwB,EAC7H,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,EAIxG,YAAY,EAAE,WAAW,EAEzB,WAAW,EAAE,SAAS,EAItB,MAAM,EAIN,YAAY,CACJ,CAAA;AAEV,gDAAgD;AAChD,eAAO,MAAM,qBAAqB,wqBAA4B,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,CAiB/D;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,EAAE,mBAAmB,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAmCvH"}
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"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Single door for "is this identifier a VALUE reference in emitted JS".
3
+ *
4
+ * Replaces `\bname\b` text scans at the import-emission sites (#2432): a
5
+ * regex scan can't tell a genuine value reference (`paperColor({ ... })`)
6
+ * from an object key or string literal that merely spells an imported
7
+ * name (`{ Theme: 'テーマ' }`). That false match used to make
8
+ * `collectExternalImports` re-emit a per-specifier type-only import
9
+ * (`import { paperColor, type Theme } from '../lib/theme'`) as a VALUE
10
+ * import, which the CLI's relative-import inliner then placed in the IIFE's
11
+ * `return { … }` with no binding — `ReferenceError: Theme is not defined`
12
+ * at load, killing the whole page's client JS.
13
+ *
14
+ * `packages/cli`'s `detectStrippedReferences` (in `resolve-imports.ts`)
15
+ * shares the same classifier for its own dangling-reference scan, so the
16
+ * two "is this a real use" checks in the pipeline can never drift apart.
17
+ */
18
+ import ts from 'typescript';
19
+ /**
20
+ * Identifier-position classifier: returns `true` when `id` is being USED
21
+ * as a value, `false` when it's a declaration name, property key, member-
22
+ * access name, or other non-reference slot.
23
+ *
24
+ * A ShorthandPropertyAssignment (`{ Theme }`) intentionally counts as a
25
+ * reference — it reads the binding, it doesn't just spell its name.
26
+ *
27
+ * Caveat: this is a syntactic test, not a scope analysis. If a local
28
+ * function parameter happens to share a name with an imported binding,
29
+ * 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`).
34
+ */
35
+ export declare function isValueReferenceIdentifier(id: ts.Identifier): boolean;
36
+ /**
37
+ * Parse `code` and collect the text of every identifier that is a VALUE
38
+ * reference per `isValueReferenceIdentifier`.
39
+ *
40
+ * Returns `null` when the text did not parse cleanly. `null` means
41
+ * "cannot answer" — callers MUST fall back to their previous (regex-scan)
42
+ * behaviour rather than treating it as an empty set. Narrowing on a
43
+ * partial parse would DROP a needed import, which is the failure
44
+ * direction we must never take (a phantom missing-import build failure
45
+ * is recoverable; a silently dead client bundle is not).
46
+ */
47
+ export declare function collectValueReferencedNames(code: string): Set<string> | null;
48
+ //# sourceMappingURL=value-references.d.ts.map
@@ -0,0 +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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/jsx",
3
- "version": "0.27.0",
3
+ "version": "0.28.1",
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.27.0"
56
+ "@barefootjs/shared": "0.28.1"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "@barefootjs/client": ">=0.2.0",
@@ -4380,7 +4380,7 @@ export function Example(_p, __bfKey) { return createComponent('Example', _p, __b
4380
4380
  `;
4381
4381
 
4382
4382
  exports[`docs/core/adapters/hono-adapter.md doc-examples L190 — (no label) 1`] = `
4383
- "import { $, createComponent, createEffect, createSignal, escapeAttr, escapeText, escapeTextOrNode, hydrate, lazySlots, mapArray } from '@barefootjs/client/runtime'
4383
+ "import { $, createComponent, createEffect, createSignal, escapeAttr, escapeText, escapeTextOrNode, hydrate, lazySlots, mapArrayLazy, textOrNode } from '@barefootjs/client/runtime'
4384
4384
 
4385
4385
  export function initTodoItem(__scope, _p = {}) {
4386
4386
  if (!__scope) return
@@ -4431,11 +4431,32 @@ export function initExample(__scope, _p = {}) {
4431
4431
 
4432
4432
  const __tpl_l0 = document.createElement('template')
4433
4433
  __tpl_l0.innerHTML = \`<li data-key=""><!--bf:s0--><!--/--></li>\`
4434
- mapArray(() => items(), _s1, (item) => String(item), (item, __idx, __existing) => {
4435
- const __el = __existing ?? __tpl_l0.content.firstElementChild.cloneNode(true)
4436
- const __bfw_s0 = lazySlots(__el, [{ id: 's0', kind: 'text', path: __existing ? [] : [0] }])
4437
- createEffect(() => { __bfw_s0('s0', String(item())) })
4438
- return __el
4434
+ const __lzp_l0 = [[0]]
4435
+ const __lzs_l0 = [{ id: 's0', kind: 'text', path: [] }]
4436
+ const __lzsc_l0 = [{ id: 's0', kind: 'text', path: __lzp_l0[0] }]
4437
+ const __lzc_l0 = (__e) => {
4438
+ return [null]
4439
+ }
4440
+ mapArrayLazy(() => items(), _s1, (item) => String(item), {
4441
+ createRow: (__e, __idx) => {
4442
+ const item = () => __e.item
4443
+ const __el = __tpl_l0.content.firstElementChild.cloneNode(true)
4444
+ const __r = __e.refs = [lazySlots(__el, __lzsc_l0)]
4445
+ const __l = __e.last = []
4446
+ { const __x = item()
4447
+ __r[0]('s0', textOrNode(__x))
4448
+ __l[0] = __x }
4449
+ return __el
4450
+ },
4451
+ applyItem: (__e) => {
4452
+ const item = () => __e.item
4453
+ const __r = __e.refs ?? (__e.refs = __lzc_l0(__e))
4454
+ const __l = __e.last ?? (__e.last = [])
4455
+ const __d = __r[0] ?? (__r[0] = lazySlots(__e.primaryEl, __lzs_l0))
4456
+ { const __x = item()
4457
+ if (!(0 in __l) || !Object.is(__l[0], __x)) __d('s0', textOrNode(__x))
4458
+ __l[0] = __x }
4459
+ },
4439
4460
  }, 'l0')
4440
4461
 
4441
4462
  }
@@ -4494,7 +4515,7 @@ export function Counter(_p, __bfKey) { return createComponent('Counter', _p, __b
4494
4515
  `;
4495
4516
 
4496
4517
  exports[`docs/core/adapters/go-template-adapter.md doc-examples L168 — (no label) 1`] = `
4497
- "import { $, createComponent, createEffect, createSignal, escapeAttr, escapeText, escapeTextOrNode, hydrate, lazySlots, mapArray } from '@barefootjs/client/runtime'
4518
+ "import { $, createComponent, createEffect, createSignal, escapeAttr, escapeText, escapeTextOrNode, hydrate, lazySlots, mapArrayLazy, textOrNode } from '@barefootjs/client/runtime'
4498
4519
 
4499
4520
  export function initTodoItem(__scope, _p = {}) {
4500
4521
  if (!__scope) return
@@ -4545,11 +4566,32 @@ export function initExample(__scope, _p = {}) {
4545
4566
 
4546
4567
  const __tpl_l0 = document.createElement('template')
4547
4568
  __tpl_l0.innerHTML = \`<li data-key=""><!--bf:s0--><!--/--></li>\`
4548
- mapArray(() => items(), _s1, (item) => String(item), (item, __idx, __existing) => {
4549
- const __el = __existing ?? __tpl_l0.content.firstElementChild.cloneNode(true)
4550
- const __bfw_s0 = lazySlots(__el, [{ id: 's0', kind: 'text', path: __existing ? [] : [0] }])
4551
- createEffect(() => { __bfw_s0('s0', String(item())) })
4552
- return __el
4569
+ const __lzp_l0 = [[0]]
4570
+ const __lzs_l0 = [{ id: 's0', kind: 'text', path: [] }]
4571
+ const __lzsc_l0 = [{ id: 's0', kind: 'text', path: __lzp_l0[0] }]
4572
+ const __lzc_l0 = (__e) => {
4573
+ return [null]
4574
+ }
4575
+ mapArrayLazy(() => items(), _s1, (item) => String(item), {
4576
+ createRow: (__e, __idx) => {
4577
+ const item = () => __e.item
4578
+ const __el = __tpl_l0.content.firstElementChild.cloneNode(true)
4579
+ const __r = __e.refs = [lazySlots(__el, __lzsc_l0)]
4580
+ const __l = __e.last = []
4581
+ { const __x = item()
4582
+ __r[0]('s0', textOrNode(__x))
4583
+ __l[0] = __x }
4584
+ return __el
4585
+ },
4586
+ applyItem: (__e) => {
4587
+ const item = () => __e.item
4588
+ const __r = __e.refs ?? (__e.refs = __lzc_l0(__e))
4589
+ const __l = __e.last ?? (__e.last = [])
4590
+ const __d = __r[0] ?? (__r[0] = lazySlots(__e.primaryEl, __lzs_l0))
4591
+ { const __x = item()
4592
+ if (!(0 in __l) || !Object.is(__l[0], __x)) __d('s0', textOrNode(__x))
4593
+ __l[0] = __x }
4594
+ },
4553
4595
  }, 'l0')
4554
4596
 
4555
4597
  }
@@ -4559,7 +4601,7 @@ export function Example(_p, __bfKey) { return createComponent('Example', _p, __b
4559
4601
  `;
4560
4602
 
4561
4603
  exports[`docs/core/adapters/go-template-adapter.md doc-examples L180 — (no label) 1`] = `
4562
- "import { $, createComponent, createEffect, createSignal, escapeAttr, escapeText, escapeTextOrNode, hydrate, lazySlots, mapArray } from '@barefootjs/client/runtime'
4604
+ "import { $, createComponent, createEffect, createSignal, escapeAttr, escapeText, escapeTextOrNode, hydrate, lazySlots, mapArrayLazy, textOrNode } from '@barefootjs/client/runtime'
4563
4605
 
4564
4606
  export function initTodoItem(__scope, _p = {}) {
4565
4607
  if (!__scope) return
@@ -4610,11 +4652,32 @@ export function initExample(__scope, _p = {}) {
4610
4652
 
4611
4653
  const __tpl_l0 = document.createElement('template')
4612
4654
  __tpl_l0.innerHTML = \`<li data-key=""><!--bf:s0--><!--/--></li>\`
4613
- mapArray(() => items().filter(item => item.active), _s1, (item) => String(item.id), (item, __idx, __existing) => {
4614
- const __el = __existing ?? __tpl_l0.content.firstElementChild.cloneNode(true)
4615
- const __bfw_s0 = lazySlots(__el, [{ id: 's0', kind: 'text', path: __existing ? [] : [0] }])
4616
- createEffect(() => { __bfw_s0('s0', String(item().name)) })
4617
- return __el
4655
+ const __lzp_l0 = [[0]]
4656
+ const __lzs_l0 = [{ id: 's0', kind: 'text', path: [] }]
4657
+ const __lzsc_l0 = [{ id: 's0', kind: 'text', path: __lzp_l0[0] }]
4658
+ const __lzc_l0 = (__e) => {
4659
+ return [null]
4660
+ }
4661
+ mapArrayLazy(() => items().filter(item => item.active), _s1, (item) => String(item.id), {
4662
+ createRow: (__e, __idx) => {
4663
+ const item = () => __e.item
4664
+ const __el = __tpl_l0.content.firstElementChild.cloneNode(true)
4665
+ const __r = __e.refs = [lazySlots(__el, __lzsc_l0)]
4666
+ const __l = __e.last = []
4667
+ { const __x = item().name
4668
+ __r[0]('s0', textOrNode(__x))
4669
+ __l[0] = __x }
4670
+ return __el
4671
+ },
4672
+ applyItem: (__e) => {
4673
+ const item = () => __e.item
4674
+ const __r = __e.refs ?? (__e.refs = __lzc_l0(__e))
4675
+ const __l = __e.last ?? (__e.last = [])
4676
+ const __d = __r[0] ?? (__r[0] = lazySlots(__e.primaryEl, __lzs_l0))
4677
+ { const __x = item().name
4678
+ if (!(0 in __l) || !Object.is(__l[0], __x)) __d('s0', textOrNode(__x))
4679
+ __l[0] = __x }
4680
+ },
4618
4681
  }, 'l0')
4619
4682
 
4620
4683
  }
@@ -5032,7 +5095,7 @@ export function StatementExample(_p, __bfKey) { return createComponent('Statemen
5032
5095
  `;
5033
5096
 
5034
5097
  exports[`docs/core/advanced/compiler-internals.md doc-examples L126 — (no label) 1`] = `
5035
- "import { $, createComponent, createEffect, createSignal, escapeAttr, escapeText, escapeTextOrNode, hydrate, lazySlots, mapArray } from '@barefootjs/client/runtime'
5098
+ "import { $, createComponent, createEffect, createSignal, escapeAttr, escapeText, escapeTextOrNode, hydrate, lazySlots, mapArrayLazy, textOrNode } from '@barefootjs/client/runtime'
5036
5099
 
5037
5100
  export function initTodoItem(__scope, _p = {}) {
5038
5101
  if (!__scope) return
@@ -5083,11 +5146,32 @@ export function initExample(__scope, _p = {}) {
5083
5146
 
5084
5147
  const __tpl_l0 = document.createElement('template')
5085
5148
  __tpl_l0.innerHTML = \`<li data-key=""><!--bf:s0--><!--/--></li>\`
5086
- mapArray(() => todos().filter(t => !t.done).toSorted((a, b) => a.date - b.date), _s1, (t) => String(t.id), (t, __idx, __existing) => {
5087
- const __el = __existing ?? __tpl_l0.content.firstElementChild.cloneNode(true)
5088
- const __bfw_s0 = lazySlots(__el, [{ id: 's0', kind: 'text', path: __existing ? [] : [0] }])
5089
- createEffect(() => { __bfw_s0('s0', String(t().name)) })
5090
- return __el
5149
+ const __lzp_l0 = [[0]]
5150
+ const __lzs_l0 = [{ id: 's0', kind: 'text', path: [] }]
5151
+ const __lzsc_l0 = [{ id: 's0', kind: 'text', path: __lzp_l0[0] }]
5152
+ const __lzc_l0 = (__e) => {
5153
+ return [null]
5154
+ }
5155
+ mapArrayLazy(() => todos().filter(t => !t.done).toSorted((a, b) => a.date - b.date), _s1, (t) => String(t.id), {
5156
+ createRow: (__e, __idx) => {
5157
+ const t = () => __e.item
5158
+ const __el = __tpl_l0.content.firstElementChild.cloneNode(true)
5159
+ const __r = __e.refs = [lazySlots(__el, __lzsc_l0)]
5160
+ const __l = __e.last = []
5161
+ { const __x = t().name
5162
+ __r[0]('s0', textOrNode(__x))
5163
+ __l[0] = __x }
5164
+ return __el
5165
+ },
5166
+ applyItem: (__e) => {
5167
+ const t = () => __e.item
5168
+ const __r = __e.refs ?? (__e.refs = __lzc_l0(__e))
5169
+ const __l = __e.last ?? (__e.last = [])
5170
+ const __d = __r[0] ?? (__r[0] = lazySlots(__e.primaryEl, __lzs_l0))
5171
+ { const __x = t().name
5172
+ if (!(0 in __l) || !Object.is(__l[0], __x)) __d('s0', textOrNode(__x))
5173
+ __l[0] = __x }
5174
+ },
5091
5175
  }, 'l0')
5092
5176
 
5093
5177
  }
@@ -5159,7 +5243,7 @@ export function StatementExample(_p, __bfKey) { return createComponent('Statemen
5159
5243
  `;
5160
5244
 
5161
5245
  exports[`docs/core/advanced/performance.md doc-examples L67 — ✅ Stable key — DOM nodes reused when list changes 1`] = `
5162
- "import { $, createComponent, createEffect, createSignal, escapeAttr, escapeText, escapeTextOrNode, hydrate, lazySlots, mapArray } from '@barefootjs/client/runtime'
5246
+ "import { $, createComponent, createEffect, createSignal, escapeAttr, escapeText, escapeTextOrNode, hydrate, lazySlots, mapArrayLazy, textOrNode } from '@barefootjs/client/runtime'
5163
5247
 
5164
5248
  export function initTodoItem(__scope, _p = {}) {
5165
5249
  if (!__scope) return
@@ -5210,11 +5294,32 @@ export function initExample(__scope, _p = {}) {
5210
5294
 
5211
5295
  const __tpl_l0 = document.createElement('template')
5212
5296
  __tpl_l0.innerHTML = \`<li data-key=""><!--bf:s0--><!--/--></li>\`
5213
- mapArray(() => items(), _s1, (item) => String(item.id), (item, __idx, __existing) => {
5214
- const __el = __existing ?? __tpl_l0.content.firstElementChild.cloneNode(true)
5215
- const __bfw_s0 = lazySlots(__el, [{ id: 's0', kind: 'text', path: __existing ? [] : [0] }])
5216
- createEffect(() => { __bfw_s0('s0', String(item().name)) })
5217
- return __el
5297
+ const __lzp_l0 = [[0]]
5298
+ const __lzs_l0 = [{ id: 's0', kind: 'text', path: [] }]
5299
+ const __lzsc_l0 = [{ id: 's0', kind: 'text', path: __lzp_l0[0] }]
5300
+ const __lzc_l0 = (__e) => {
5301
+ return [null]
5302
+ }
5303
+ mapArrayLazy(() => items(), _s1, (item) => String(item.id), {
5304
+ createRow: (__e, __idx) => {
5305
+ const item = () => __e.item
5306
+ const __el = __tpl_l0.content.firstElementChild.cloneNode(true)
5307
+ const __r = __e.refs = [lazySlots(__el, __lzsc_l0)]
5308
+ const __l = __e.last = []
5309
+ { const __x = item().name
5310
+ __r[0]('s0', textOrNode(__x))
5311
+ __l[0] = __x }
5312
+ return __el
5313
+ },
5314
+ applyItem: (__e) => {
5315
+ const item = () => __e.item
5316
+ const __r = __e.refs ?? (__e.refs = __lzc_l0(__e))
5317
+ const __l = __e.last ?? (__e.last = [])
5318
+ const __d = __r[0] ?? (__r[0] = lazySlots(__e.primaryEl, __lzs_l0))
5319
+ { const __x = item().name
5320
+ if (!(0 in __l) || !Object.is(__l[0], __x)) __d('s0', textOrNode(__x))
5321
+ __l[0] = __x }
5322
+ },
5218
5323
  }, 'l0')
5219
5324
 
5220
5325
  }
@@ -5224,7 +5329,7 @@ export function Example(_p, __bfKey) { return createComponent('Example', _p, __b
5224
5329
  `;
5225
5330
 
5226
5331
  exports[`docs/core/advanced/performance.md doc-examples L70 — ❌ Index key — DOM nodes recreated on reorder 1`] = `
5227
- "import { $, createComponent, createEffect, createSignal, escapeAttr, escapeText, escapeTextOrNode, hydrate, lazySlots, mapArray } from '@barefootjs/client/runtime'
5332
+ "import { $, createComponent, createEffect, createSignal, escapeAttr, escapeText, escapeTextOrNode, hydrate, lazySlots, mapArrayLazy, textOrNode } from '@barefootjs/client/runtime'
5228
5333
 
5229
5334
  export function initTodoItem(__scope, _p = {}) {
5230
5335
  if (!__scope) return
@@ -5275,11 +5380,32 @@ export function initExample(__scope, _p = {}) {
5275
5380
 
5276
5381
  const __tpl_l0 = document.createElement('template')
5277
5382
  __tpl_l0.innerHTML = \`<li data-key=""><!--bf:s0--><!--/--></li>\`
5278
- mapArray(() => items(), _s1, (item, i) => String(i), (item, i, __existing) => {
5279
- const __el = __existing ?? __tpl_l0.content.firstElementChild.cloneNode(true)
5280
- const __bfw_s0 = lazySlots(__el, [{ id: 's0', kind: 'text', path: __existing ? [] : [0] }])
5281
- createEffect(() => { __bfw_s0('s0', String(item().name)) })
5282
- return __el
5383
+ const __lzp_l0 = [[0]]
5384
+ const __lzs_l0 = [{ id: 's0', kind: 'text', path: [] }]
5385
+ const __lzsc_l0 = [{ id: 's0', kind: 'text', path: __lzp_l0[0] }]
5386
+ const __lzc_l0 = (__e) => {
5387
+ return [null]
5388
+ }
5389
+ mapArrayLazy(() => items(), _s1, (item, i) => String(i), {
5390
+ createRow: (__e, i) => {
5391
+ const item = () => __e.item
5392
+ const __el = __tpl_l0.content.firstElementChild.cloneNode(true)
5393
+ const __r = __e.refs = [lazySlots(__el, __lzsc_l0)]
5394
+ const __l = __e.last = []
5395
+ { const __x = item().name
5396
+ __r[0]('s0', textOrNode(__x))
5397
+ __l[0] = __x }
5398
+ return __el
5399
+ },
5400
+ applyItem: (__e) => {
5401
+ const item = () => __e.item
5402
+ const __r = __e.refs ?? (__e.refs = __lzc_l0(__e))
5403
+ const __l = __e.last ?? (__e.last = [])
5404
+ const __d = __r[0] ?? (__r[0] = lazySlots(__e.primaryEl, __lzs_l0))
5405
+ { const __x = item().name
5406
+ if (!(0 in __l) || !Object.is(__l[0], __x)) __d('s0', textOrNode(__x))
5407
+ __l[0] = __x }
5408
+ },
5283
5409
  }, 'l0')
5284
5410
 
5285
5411
  }
@@ -2638,7 +2638,10 @@ describe('Client JS generation', () => {
2638
2638
  const js = result.files.find(f => f.type === 'clientJs')!.content
2639
2639
  // The branch arm's mapArray call must live inside a
2640
2640
  // createDisposableEffect that is pushed into __disposers.
2641
- expect(js).toMatch(/__disposers\.push\(createDisposableEffect\(\(\)\s*=>\s*\{[\s\S]*?mapArray\(/)
2641
+ // `mapArray(` or `mapArrayLazy(` — a lazy-row-eligible branch loop
2642
+ // (spec/slot-unification.md §9.4) uses the latter and has exactly the
2643
+ // same disposal obligation.
2644
+ expect(js).toMatch(/__disposers\.push\(createDisposableEffect\(\(\)\s*=>\s*\{[\s\S]*?mapArray(Lazy)?\(/)
2642
2645
  })
2643
2646
 
2644
2647
  test('nested conditional inside a conditional branch is wrapped in createDisposableEffect', () => {
@@ -53,7 +53,10 @@ describe('composite loops inside conditional branches (#724)', () => {
53
53
  const js = clientJs!.content
54
54
 
55
55
  // Should use mapArray inside the branch's bindEvents
56
- expect(js).toContain('mapArray(')
56
+ // `mapArray(` or `mapArrayLazy(` — a lazy-row-eligible branch loop
57
+ // (spec/slot-unification.md §9.4) uses the latter; either satisfies
58
+ // "keyed reconciliation, not a composite/component shape".
59
+ expect(js).toMatch(/\bmapArray(Lazy)?\(/)
57
60
 
58
61
  // After O-1's mode-collapse PR, child component init goes through the
59
62
  // runtime `upsertChild` helper instead of an emit-side
@@ -96,7 +99,10 @@ describe('composite loops inside conditional branches (#724)', () => {
96
99
  const js = clientJs!.content
97
100
 
98
101
  // Should use mapArray for per-item reactivity
99
- expect(js).toContain('mapArray(')
102
+ // `mapArray(` or `mapArrayLazy(` — a lazy-row-eligible branch loop
103
+ // (spec/slot-unification.md §9.4) uses the latter; either satisfies
104
+ // "keyed reconciliation, not a composite/component shape".
105
+ expect(js).toMatch(/\bmapArray(Lazy)?\(/)
100
106
 
101
107
  // Should NOT use createComponent inside the init body (no child
102
108
  // components in this fixture). The CLI also emits a callable shim
@@ -147,7 +153,10 @@ describe('composite loops inside conditional branches (#724)', () => {
147
153
  const js = clientJs!.content
148
154
 
149
155
  // Should use mapArray for the loop
150
- expect(js).toContain('mapArray(')
156
+ // `mapArray(` or `mapArrayLazy(` — a lazy-row-eligible branch loop
157
+ // (spec/slot-unification.md §9.4) uses the latter; either satisfies
158
+ // "keyed reconciliation, not a composite/component shape".
159
+ expect(js).toMatch(/\bmapArray(Lazy)?\(/)
151
160
 
152
161
  // Should have event delegation (addEventListener + closest pattern)
153
162
  expect(js).toContain(".addEventListener('click', (__bfEvt) => {")
@@ -29,11 +29,17 @@ function clientJs(source: string, file = 'CondKey.tsx'): string {
29
29
  return cjs!.content
30
30
  }
31
31
 
32
+ /**
33
+ * Reconciler call sites, whichever family they use. A lazy-row-eligible loop
34
+ * (spec/slot-unification.md §9.4) emits `mapArrayLazy(`; everything else keeps
35
+ * `mapArray(`. The key expression this suite asserts on is the same third
36
+ * argument in both shapes, so match the family, not one member of it.
37
+ */
32
38
  function mapArrayCalls(content: string): string[] {
33
39
  return content
34
40
  .split('\n')
35
41
  .map((ln) => ln.trim())
36
- .filter((ln) => ln.startsWith('mapArray('))
42
+ .filter((ln) => ln.startsWith('mapArray(') || ln.startsWith('mapArrayLazy('))
37
43
  }
38
44
 
39
45
  describe('mapArray key extraction across conditional branches (#1098)', () => {
@@ -7,9 +7,14 @@
7
7
  * `isSelected(row.id)` as reactive with no analyzer changes — the callee's
8
8
  * type carries the brand, not the call's return type. These tests pin:
9
9
  * - the classic js-framework-benchmark row compiles with `isSelected(row.id)`
10
- * wired to a single per-row `createEffect` (not the legacy
11
- * `selected() === row.id` shape, which would still work but subscribes
12
- * every row to the raw signal)
10
+ * wired through the lazy row graph — ZERO emitted per-row effects, the
11
+ * loop-level effect living in `mapArrayLazy` (spec §9). Until §9.3a
12
+ * was lifted this shape was gate-INELIGIBLE (an opaque local whose call
13
+ * is the reactive read could not be primed) and compiled to one per-row
14
+ * `createEffect`; the runtime re-subscribe seam replaced that, so the
15
+ * loop is emitted like any other. Either way it is never
16
+ * the legacy `selected() === row.id` shape, which subscribes every row
17
+ * to the raw signal.
13
18
  * - the loop-param accessor wrap applies only to the call ARGUMENT
14
19
  * (`row.id` -> `row().id`), leaving the `isSelected` callee untouched
15
20
  * - the hoisted skeleton fast path (#2223) still fires for a selector-driven
@@ -36,7 +41,7 @@ function compile(source: string, filename: string): { clientJs: string; template
36
41
  }
37
42
 
38
43
  describe('createSelector (perf, #2143 gap 5)', () => {
39
- test('isSelected(row.id) in a loop body compiles to one per-row createEffect', () => {
44
+ test('isSelected(row.id) in a loop body compiles through the lazy row graph', () => {
40
45
  const source = `
41
46
  'use client'
42
47
  import { createSignal, createSelector } from '@barefootjs/client'
@@ -66,12 +71,21 @@ describe('createSelector (perf, #2143 gap 5)', () => {
66
71
  // The callee itself is never rewritten to an accessor call.
67
72
  expect(clientJs).not.toContain('isSelected()(')
68
73
 
69
- // Exactly one createEffect wraps the selector-driven class attribute
70
- // not the O(rows) `selected() === row.id` shape.
74
+ // The selector-driven class attribute costs NO emitted effect at all:
75
+ // the loop is lazy-eligible, so its one loop-level effect lives inside
76
+ // `mapArrayLazy` rather than in the emitted row body. It is also never
77
+ // the O(rows) `selected() === row.id` shape.
71
78
  const effectCount = (clientJs.match(/createEffect\(/g) ?? []).length
72
- expect(effectCount).toBe(1)
79
+ expect(effectCount).toBe(0)
80
+ expect(clientJs).toContain('mapArrayLazy(')
73
81
  expect(clientJs).not.toContain('selected() ===')
74
82
 
83
+ // `isSelected` is an opaque local (its CALL is the reactive read). Nothing
84
+ // in the emission marks that: the runtime's re-subscribe seam applies to
85
+ // every loop-level outer effect, so the compiler has no judgement to
86
+ // encode here (§9.3a).
87
+ expect(clientJs).not.toContain('outerNeedsResubscribe')
88
+
75
89
  // The hoisted skeleton fast path (#2223) still fires: a single-line
76
90
  // renderItem clones from a once-per-loop template.
77
91
  expect(clientJs).toMatch(/const __tpl_\w+ = document\.createElement\('template'\)/)
@@ -1,5 +1,5 @@
1
1
  import { describe, test, expect } from 'bun:test'
2
- import { collectExternalImports } from '../../ir-to-client-js/imports'
2
+ import { collectExternalImports, collectUserDomImports } from '../../ir-to-client-js/imports'
3
3
  import type { ComponentIR, ImportInfo, SourceLocation } from '../../types'
4
4
 
5
5
  const dummyLoc: SourceLocation = { file: 'test.tsx', start: { line: 1, column: 0 }, end: { line: 1, column: 0 } }
@@ -229,4 +229,101 @@ describe('collectExternalImports', () => {
229
229
  "import { z } from 'zod'",
230
230
  ])
231
231
  })
232
+
233
+ // #2432: `collectExternalImports` used a `\bname\b` text scan to decide
234
+ // which specifiers survive into the client bundle. That scan never
235
+ // consulted the per-specifier `isTypeOnly` flag, so a per-specifier
236
+ // `import { paperColor, type Theme } from '../lib/theme'` re-emitted
237
+ // `Theme` as a VALUE import whenever the word "Theme" merely appeared in
238
+ // the generated code (e.g. as an object key) — the CLI's relative-import
239
+ // inliner then put `Theme` in the IIFE's `return { … }` with no binding,
240
+ // producing `ReferenceError: Theme is not defined` at load.
241
+ test('per-specifier type-only specifier is not emitted, while its value sibling is (#2432)', () => {
242
+ const ir: ReturnType<typeof makeIR> = makeIR([{
243
+ source: '../lib/theme',
244
+ specifiers: [
245
+ { name: 'paperColor', alias: null, isDefault: false, isNamespace: false },
246
+ { name: 'Theme', alias: null, isDefault: false, isNamespace: false, isTypeOnly: true },
247
+ ],
248
+ isTypeOnly: false,
249
+ loc: dummyLoc,
250
+ }])
251
+ const code = "const labels = { Theme: 'テーマ' };\nconst c = paperColor({ paper: '#fff' });"
252
+ const result = collectExternalImports(ir, code)
253
+ expect(result).toEqual(["import { paperColor } from '../lib/theme'"])
254
+ })
255
+
256
+ test('a value specifier that appears ONLY as an object key is not emitted (#2432)', () => {
257
+ const ir = makeIR([makeImport('./utils', ['helper'])])
258
+ const code = 'const o = { helper: 1 };'
259
+ const result = collectExternalImports(ir, code)
260
+ expect(result).toEqual([])
261
+ })
262
+
263
+ test('a value specifier that appears ONLY inside a string literal is not emitted (#2432)', () => {
264
+ const ir = makeIR([makeImport('./utils', ['helper'])])
265
+ const code = "const s = 'helper is a function';"
266
+ const result = collectExternalImports(ir, code)
267
+ expect(result).toEqual([])
268
+ })
269
+
270
+ test('a value specifier that appears ONLY as a property-access name is not emitted (#2432)', () => {
271
+ const ir = makeIR([makeImport('./utils', ['helper'])])
272
+ const code = 'obj.helper();'
273
+ const result = collectExternalImports(ir, code)
274
+ expect(result).toEqual([])
275
+ })
276
+
277
+ test('a value specifier used via shorthand property IS emitted (#2432)', () => {
278
+ const ir = makeIR([makeImport('./utils', ['helper'])])
279
+ const code = 'const o = { helper };'
280
+ const result = collectExternalImports(ir, code)
281
+ expect(result).toEqual(["import { helper } from './utils'"])
282
+ })
283
+
284
+ test('unparseable generated text falls back to the text scan (#2432)', () => {
285
+ const ir = makeIR([makeImport('some-lib', ['helper'])])
286
+ const code = 'MyComponent helper()'
287
+ const result = collectExternalImports(ir, code)
288
+ expect(result).toEqual(["import { helper } from 'some-lib'"])
289
+ })
290
+
291
+ // These two pin the fallback branch specifically (generated text that does
292
+ // NOT parse cleanly, so `makeValueUsageTest` falls back to a substring
293
+ // scan rather than the AST-based value-reference set). #2432: the
294
+ // fallback used to be a `new RegExp(`\\b${localName}\\b`)` scan, which
295
+ // silently DROPPED the import for either shape below — `\b` isn't defined
296
+ // for `$` or non-ASCII characters, and `$` is also a regex metacharacter
297
+ // (end-of-input anchor) when spliced unescaped into `new RegExp(...)`, so
298
+ // `\b$fetch\b` could never match `$fetch` at all. Dropping the import is
299
+ // the one failure direction this helper must never take.
300
+ test('fallback matches a $-prefixed specifier (#2432)', () => {
301
+ const ir = makeIR([makeImport('ofetch', ['$fetch'])])
302
+ const code = 'MyComponent $fetch()'
303
+ const result = collectExternalImports(ir, code)
304
+ expect(result).toEqual(["import { $fetch } from 'ofetch'"])
305
+ })
306
+
307
+ test('fallback matches a non-ASCII specifier (#2432)', () => {
308
+ const ir = makeIR([makeImport('some-lib', ['日本語'])])
309
+ const code = 'MyComponent 日本語()'
310
+ const result = collectExternalImports(ir, code)
311
+ expect(result).toEqual(["import { 日本語 } from 'some-lib'"])
312
+ })
313
+ })
314
+
315
+ describe('collectUserDomImports', () => {
316
+ test('per-specifier type-only specifier is not emitted from the runtime subpath (#2432)', () => {
317
+ const ir = makeIR([{
318
+ source: '@barefootjs/client',
319
+ specifiers: [
320
+ { name: 'createSignal', alias: null, isDefault: false, isNamespace: false },
321
+ { name: 'Signal', alias: null, isDefault: false, isNamespace: false, isTypeOnly: true },
322
+ ],
323
+ isTypeOnly: false,
324
+ loc: dummyLoc,
325
+ }])
326
+ const result = collectUserDomImports(ir)
327
+ expect(result).toEqual(['createSignal'])
328
+ })
232
329
  })