@barefootjs/jsx 0.17.1 → 0.18.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 (79) hide show
  1. package/dist/adapters/interface.d.ts +20 -4
  2. package/dist/adapters/interface.d.ts.map +1 -1
  3. package/dist/adapters/parsed-expr-emitter.d.ts +3 -1
  4. package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
  5. package/dist/analyzer-context.d.ts.map +1 -1
  6. package/dist/analyzer.d.ts.map +1 -1
  7. package/dist/augment-inherited-props.d.ts +19 -0
  8. package/dist/augment-inherited-props.d.ts.map +1 -1
  9. package/dist/compiler.d.ts.map +1 -1
  10. package/dist/expression-parser.d.ts +1 -0
  11. package/dist/expression-parser.d.ts.map +1 -1
  12. package/dist/index.d.ts +19 -3
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +815 -251
  15. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  16. package/dist/ir-to-client-js/compute-inlinability.d.ts.map +1 -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/loop.d.ts +9 -0
  19. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
  20. package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
  21. package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts +19 -0
  22. package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts.map +1 -1
  23. package/dist/ir-to-client-js/emit-registration.d.ts +4 -2
  24. package/dist/ir-to-client-js/emit-registration.d.ts.map +1 -1
  25. package/dist/ir-to-client-js/html-template.d.ts +41 -0
  26. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  27. package/dist/ir-to-client-js/types.d.ts +22 -1
  28. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  29. package/dist/jsx-to-ir.d.ts.map +1 -1
  30. package/dist/loop-destructure.d.ts +55 -18
  31. package/dist/loop-destructure.d.ts.map +1 -1
  32. package/dist/lowering-registry.d.ts +13 -0
  33. package/dist/lowering-registry.d.ts.map +1 -1
  34. package/dist/relocate.d.ts +28 -0
  35. package/dist/relocate.d.ts.map +1 -1
  36. package/dist/ssr-defaults.d.ts.map +1 -1
  37. package/dist/types.d.ts +68 -0
  38. package/dist/types.d.ts.map +1 -1
  39. package/package.json +2 -2
  40. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +284 -12
  41. package/src/__tests__/augment-inherited-props.test.ts +96 -0
  42. package/src/__tests__/compiler-runtime-contract.test.ts +11 -1
  43. package/src/__tests__/compiler-stress-1244.test.ts +13 -4
  44. package/src/__tests__/csr-substitution-safety-divergence.test.ts +137 -0
  45. package/src/__tests__/destructured-map-params.test.ts +11 -1
  46. package/src/__tests__/expression-parser.test.ts +63 -2
  47. package/src/__tests__/ir-sort-comparator.test.ts +261 -0
  48. package/src/__tests__/loop-destructure.test.ts +313 -0
  49. package/src/__tests__/loop-hoisted-template.test.ts +235 -0
  50. package/src/__tests__/props-destructuring.test.ts +110 -0
  51. package/src/__tests__/serialize-parsed-expr.test.ts +70 -3
  52. package/src/__tests__/ssr-defaults.test.ts +20 -0
  53. package/src/__tests__/staged-ir/11-template-primitive-registry.test.ts +231 -1
  54. package/src/__tests__/tagged-template-interleave.test.ts +268 -0
  55. package/src/__tests__/unsupported-expression.test.ts +194 -7
  56. package/src/adapters/interface.ts +20 -4
  57. package/src/adapters/parsed-expr-emitter.ts +19 -2
  58. package/src/analyzer-context.ts +20 -0
  59. package/src/analyzer.ts +74 -1
  60. package/src/augment-inherited-props.ts +139 -9
  61. package/src/compiler.ts +4 -0
  62. package/src/expression-parser.ts +237 -56
  63. package/src/index.ts +23 -1
  64. package/src/ir-to-client-js/collect-elements.ts +15 -1
  65. package/src/ir-to-client-js/compute-inlinability.ts +6 -1
  66. package/src/ir-to-client-js/control-flow/plan/build-loop.ts +1 -0
  67. package/src/ir-to-client-js/control-flow/plan/loop.ts +9 -0
  68. package/src/ir-to-client-js/control-flow/stringify/loop.ts +30 -8
  69. package/src/ir-to-client-js/control-flow/stringify/template-parse.ts +30 -0
  70. package/src/ir-to-client-js/emit-registration.ts +4 -2
  71. package/src/ir-to-client-js/html-template.ts +198 -1
  72. package/src/ir-to-client-js/index.ts +1 -0
  73. package/src/ir-to-client-js/types.ts +22 -0
  74. package/src/jsx-to-ir.ts +409 -24
  75. package/src/loop-destructure.ts +89 -36
  76. package/src/lowering-registry.ts +16 -0
  77. package/src/relocate.ts +201 -14
  78. package/src/ssr-defaults.ts +34 -32
  79. package/src/types.ts +65 -0
@@ -27,7 +27,7 @@
27
27
  import { emitRefCall, varSlotId, profileBindingId } from '../../utils.ts'
28
28
  import { emitAttrUpdate } from '../../emit-reactive.ts'
29
29
  import { stringifyReactiveEffects } from './reactive-effects.ts'
30
- import { emitTemplateCloneInline, emitLoopItemElementSetup } from './template-parse.ts'
30
+ import { emitTemplateCloneInline, emitLoopItemElementSetup, emitHoistedTemplateDecl, hoistedCloneExpr } from './template-parse.ts'
31
31
  import { stringifyComponentLoop } from './component-loop.ts'
32
32
  import { stringifyCompositeLoop } from './composite-loop.ts'
33
33
  import type { LoopChildRefBinding, LoopPlan, PlainLoopPlan, StaticLoopPlan } from '../plan/types.ts'
@@ -117,6 +117,7 @@ export function stringifyPlainLoop(
117
117
  indexParam,
118
118
  mapPreambleWrapped,
119
119
  template,
120
+ skeletonTemplate,
120
121
  reactiveEffects,
121
122
  childRefs,
122
123
  bodyIsMultiRoot,
@@ -133,6 +134,21 @@ export function stringifyPlainLoop(
133
134
  return
134
135
  }
135
136
 
137
+ // Hoisted shared-template fast path (perf, see `buildLoopSkeletonTemplate`):
138
+ // declare the once-per-loop template BEFORE the `mapArray` call so every
139
+ // row clones from an already-parsed node instead of re-running
140
+ // `document.createElement('template')` + an `innerHTML` parse per row.
141
+ // `bodyIsMultiRoot` is re-checked defensively even though the plan builder
142
+ // only sets `skeletonTemplate` when it's already false.
143
+ const hoistedTpl = !bodyIsMultiRoot && skeletonTemplate ? skeletonTemplate : null
144
+ // Keyed off `markerId` (unique per loop, #1087), NOT `containerVar` —
145
+ // sibling `.map()` calls under the same parent share the container slot,
146
+ // so a container-derived name would collide ("has already been declared").
147
+ const tplVar = `__tpl_${markerId.replace(/[^A-Za-z0-9_$]/g, '_')}`
148
+ if (hoistedTpl) {
149
+ emitHoistedTemplateDecl(lines, topIndent, tplVar, hoistedTpl)
150
+ }
151
+
136
152
  // `childRefs` need `__el` as a handle to invoke the user's callback inside
137
153
  // the factory, so non-empty refs force the multi-line layout the same way
138
154
  // reactive effects do (#1244).
@@ -141,7 +157,9 @@ export function stringifyPlainLoop(
141
157
  // Single-line renderItem (no reactive effects, single root, no refs).
142
158
  const unwrapInline = paramUnwrap ? `${paramUnwrap} ` : ''
143
159
  const preamble = mapPreambleWrapped ? `${mapPreambleWrapped}; ` : ''
144
- const cloneExpr = emitTemplateCloneInline(template)
160
+ const cloneExpr = hoistedTpl
161
+ ? `return ${hoistedCloneExpr(tplVar, hoistedTpl)}`
162
+ : emitTemplateCloneInline(template)
145
163
  lines.push(
146
164
  `${topIndent}mapArray(() => ${arrayExpr}, ${containerVar}, ${keyFn}, (${paramHead}, ${indexParam}, __existing) => { ${unwrapInline}${preamble}if (__existing) return __existing; ${cloneExpr} }, '${markerId}'${loopBfId})`,
147
165
  )
@@ -153,12 +171,16 @@ export function stringifyPlainLoop(
153
171
  const bodyIndent = topIndent + ' '
154
172
  if (paramUnwrap) lines.push(`${bodyIndent}${paramUnwrap}`)
155
173
  if (mapPreambleWrapped) lines.push(`${bodyIndent}${mapPreambleWrapped}`)
156
- emitLoopItemElementSetup(lines, {
157
- template,
158
- bodyIsMultiRoot,
159
- indent: bodyIndent,
160
- singleRootLayout: 'inline',
161
- })
174
+ if (hoistedTpl) {
175
+ lines.push(`${bodyIndent}const __el = __existing ?? ${hoistedCloneExpr(tplVar, hoistedTpl)}`)
176
+ } else {
177
+ emitLoopItemElementSetup(lines, {
178
+ template,
179
+ bodyIsMultiRoot,
180
+ indent: bodyIndent,
181
+ singleRootLayout: 'inline',
182
+ })
183
+ }
162
184
  if (reactiveEffects !== null) {
163
185
  stringifyReactiveEffects(lines, reactiveEffects, { indent: bodyIndent, elVar: '__el', bodyIsMultiRoot })
164
186
  }
@@ -145,6 +145,36 @@ export function emitTemplateCloneInline(template: string): string {
145
145
  return `const __tpl = document.createElement('template'); __tpl.innerHTML = \`${template}\`; return __tpl.content.firstElementChild.cloneNode(true)`
146
146
  }
147
147
 
148
+ /**
149
+ * Emit the ONE-TIME declaration of a loop's hoisted shared template (perf):
150
+ * built once per loop, before the `mapArray` call, so every row clones from
151
+ * an already-parsed node instead of re-running `document.createElement
152
+ * ('template')` + an `innerHTML` parse per row. `skeletonTemplate` is the
153
+ * STATIC-ONLY skeleton produced by `buildLoopSkeletonTemplate` (dynamic attrs
154
+ * omitted, text markers empty) — never the per-row interpolated `template`.
155
+ *
156
+ * SVG namespace wrap mirrors `emitTemplateCloneLines` (#135 / #1088):
157
+ * `templateRootIsSvg` is re-checked against the skeleton (same root tag as
158
+ * the interpolated template, so the same wrap decision applies).
159
+ */
160
+ export function emitHoistedTemplateDecl(lines: string[], indent: string, tplVar: string, skeletonTemplate: string): void {
161
+ const isSvg = templateRootIsSvg(skeletonTemplate)
162
+ const html = isSvg ? `<svg>${skeletonTemplate}</svg>` : skeletonTemplate
163
+ lines.push(`${indent}const ${tplVar} = document.createElement('template')`)
164
+ lines.push(`${indent}${tplVar}.innerHTML = \`${html}\``)
165
+ }
166
+
167
+ /**
168
+ * Clone expression reading off a hoisted template variable declared via
169
+ * `emitHoistedTemplateDecl`, in place of the per-row
170
+ * `emitTemplateCloneInline` / `emitTemplateCloneLines` parse-and-clone.
171
+ */
172
+ export function hoistedCloneExpr(tplVar: string, skeletonTemplate: string): string {
173
+ return templateRootIsSvg(skeletonTemplate)
174
+ ? `${tplVar}.content.firstElementChild.firstElementChild.cloneNode(true)`
175
+ : `${tplVar}.content.firstElementChild.cloneNode(true)`
176
+ }
177
+
148
178
  /**
149
179
  * Multi-line variant for code paths that emit each line separately.
150
180
  * Returns three statements with no trailing newlines.
@@ -102,8 +102,10 @@ export function buildInlinableConstants(
102
102
  * — the AST substitution + chain resolution now lives upstream.
103
103
  *
104
104
  * Excludes constants whose entry is `null` (unsafe to inline) — those
105
- * stay in `unsafeLocalNames` and the CSR template's expression
106
- * substitution surfaces the UNSAFE sentinel for any reference to them.
105
+ * are also folded into the unsafe set `generateCsrTemplate` uses
106
+ * internally (see `mergeCsrNullUnsafe` in html-template.ts, #2106), so
107
+ * every caller of `generateCsrTemplate` gets a consistent
108
+ * substitute-or-fallback decision with no extra wiring required here.
107
109
  */
108
110
  export function csrInlinableConstantsFromCtx(ctx: ClientJsContext): Map<string, string> {
109
111
  const out = new Map<string, string>()
@@ -702,6 +702,139 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
702
702
  }
703
703
  }
704
704
 
705
+ /**
706
+ * Slots a top-level (`mapArray`-driven) loop body proves safe to hoist into a
707
+ * shared, once-per-loop template (perf: avoid per-row `document.createElement
708
+ * ('template')` + innerHTML parse + escapeText/escapeAttr, see
709
+ * `buildLoopSkeletonTemplate`).
710
+ */
711
+ export interface LoopSkeletonSafeSlots {
712
+ /** `"<childSlotId>::<attrName>"` pairs already covered by a loop-child reactive-attribute `createEffect`. */
713
+ reactiveAttrKeys: ReadonlySet<string>
714
+ /** Text-marker slot ids already covered by a loop-child reactive-text `createEffect`. */
715
+ reactiveTextSlotIds: ReadonlySet<string>
716
+ }
717
+
718
+ /**
719
+ * Build the STATIC skeleton of a top-level loop body — the shared template
720
+ * cloned once per row instead of re-parsed from a per-row interpolated
721
+ * `innerHTML` string (perf: create-heavy `.map()` loops, see
722
+ * spec/compiler.md "Loop emission shapes").
723
+ *
724
+ * The skeleton keeps every static element / attr / text verbatim, keeps `bf="sN"`
725
+ * marker attributes (needed for `qsa` / `$t` lookups), and keeps text-marker
726
+ * comments (`<!--bf:sN--><!--/-->`) but EMPTIES the interpolation between them.
727
+ * Every dynamic attribute is DROPPED entirely rather than interpolated — both
728
+ * forms rely on the loop-child `createEffect`s (already emitted alongside the
729
+ * clone, see `stringifyReactiveEffects`) to fill in the real value on their
730
+ * eager first run, so nothing is lost — UNLESS a dynamic attr/text isn't
731
+ * proven covered by one of those effects, in which case this function refuses
732
+ * (returns `null`) and the caller falls back to the per-row interpolated
733
+ * template (`irToHtmlTemplate`). `key` is special-cased to an always-empty
734
+ * `data-key=""` placeholder — `mapArray` stamps the real key onto freshly
735
+ * created elements itself (see `map-array.ts`), so the clone path never needs
736
+ * to bake one in.
737
+ *
738
+ * Refuses (returns `null`) on anything not proven safe: spread attrs,
739
+ * `dangerouslySetInnerHTML`, a dynamic attribute/text not present in `safe`,
740
+ * a bare (unslotted) dynamic expression (no DOM anchor to backfill later),
741
+ * conditionals, child components, nested loops, and provider/async/if-statement
742
+ * boundaries. Callers additionally gate on the loop shape itself (single-root,
743
+ * non-static, no `useElementReconciliation`) before invoking this — see
744
+ * `collect-elements.ts`'s `loop` visitor.
745
+ */
746
+ export function buildLoopSkeletonTemplate(node: IRNode, safe: LoopSkeletonSafeSlots): string | null {
747
+ switch (node.type) {
748
+ case 'element': {
749
+ const attrParts: string[] = []
750
+ for (const a of node.attrs) {
751
+ if (a.name === '...') return null
752
+ if (a.name === 'dangerouslySetInnerHTML') return null
753
+ if (a.name === 'key') {
754
+ attrParts.push(`${keyAttrName(0)}=""`)
755
+ continue
756
+ }
757
+ const v = a.value
758
+ switch (v.kind) {
759
+ case 'literal':
760
+ attrParts.push(`${toHtmlAttrName(a.name)}="${v.value}"`)
761
+ break
762
+ case 'boolean-attr':
763
+ attrParts.push(toHtmlAttrName(a.name))
764
+ break
765
+ case 'boolean-shorthand':
766
+ case 'jsx-children':
767
+ // Never legal on an intrinsic element in well-formed IR — emit nothing.
768
+ break
769
+ case 'expression':
770
+ case 'template': {
771
+ const attrKey = node.slotId ? `${node.slotId}::${a.name}` : null
772
+ if (!attrKey || !safe.reactiveAttrKeys.has(attrKey)) return null
773
+ // Covered by a loop-child createEffect — omit from the skeleton
774
+ // entirely; the effect's eager first run fills it in.
775
+ break
776
+ }
777
+ case 'spread':
778
+ return null
779
+ }
780
+ }
781
+
782
+ if (node.slotId) attrParts.push(`bf="${node.slotId}"`)
783
+
784
+ const attrs = attrParts.join(' ')
785
+ let children = ''
786
+ for (const child of node.children) {
787
+ const rendered = buildLoopSkeletonTemplate(child, safe)
788
+ if (rendered === null) return null
789
+ children += rendered
790
+ }
791
+
792
+ if (children || !VOID_ELEMENTS.has(node.tag)) {
793
+ return `<${node.tag}${attrs ? ' ' + attrs : ''}>${children}</${node.tag}>`
794
+ }
795
+ return `<${node.tag}${attrs ? ' ' + attrs : ''} />`
796
+ }
797
+
798
+ case 'text':
799
+ return node.value
800
+
801
+ case 'expression':
802
+ if (node.expr === 'null' || node.expr === 'undefined') return ''
803
+ if (!node.slotId) {
804
+ // No DOM anchor to backfill later — this is a one-time SSR-baked
805
+ // value with no corresponding createEffect. Can't safely omit.
806
+ return null
807
+ }
808
+ if (!safe.reactiveTextSlotIds.has(node.slotId)) return null
809
+ return `<!--bf:${node.slotId}--><!--/-->`
810
+
811
+ case 'fragment': {
812
+ let out = ''
813
+ for (const child of node.children) {
814
+ const rendered = buildLoopSkeletonTemplate(child, safe)
815
+ if (rendered === null) return null
816
+ out += rendered
817
+ }
818
+ return out
819
+ }
820
+
821
+ // Conditionals, child components, nested loops, and provider/async/
822
+ // if-statement/slot boundaries are all out of scope for the hoisted
823
+ // fast path — the caller falls back to `irToHtmlTemplate`.
824
+ case 'conditional':
825
+ case 'component':
826
+ case 'loop':
827
+ case 'if-statement':
828
+ case 'provider':
829
+ case 'async':
830
+ case 'slot':
831
+ return null
832
+
833
+ default:
834
+ return assertNever(node)
835
+ }
836
+ }
837
+
705
838
  /**
706
839
  * Generate an HTML template for composite element reconciliation.
707
840
  * Identical to irToHtmlTemplate except component nodes become placeholder
@@ -1382,7 +1515,71 @@ export function generateCsrTemplate(
1382
1515
  }
1383
1516
  }
1384
1517
  }
1385
- return generateCsrTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, csrEnv, insideLoop, unsafeLocalNames, deferredChildSlots, loopDepth: -1 })
1518
+ const effectiveUnsafeLocalNames = mergeCsrNullUnsafe(ctx, unsafeLocalNames)
1519
+ return generateCsrTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, csrEnv, insideLoop, unsafeLocalNames: effectiveUnsafeLocalNames, deferredChildSlots, loopDepth: -1 })
1520
+ }
1521
+
1522
+ /**
1523
+ * Fold `ctx.csrInlinable`'s null verdicts into `unsafeLocalNames`, so
1524
+ * `generateCsrTemplate`'s callers can't independently drift out of sync
1525
+ * with `ctx.csrInlinable` (#2106).
1526
+ *
1527
+ * The two "is this constant's value safe to inline into a module-scope
1528
+ * template" checks in `compute-inlinability.ts` can legitimately disagree:
1529
+ *
1530
+ * - Stage-2 classification (`classifyConstantInitial`) runs
1531
+ * `isInlinableInTemplate` on the RAW initializer text — deliberately,
1532
+ * so it can still see bridged prop args (`useYjs(props.X)`) for the
1533
+ * #1138 rejection. A call whose raw receiver is an identifier path
1534
+ * (`someModuleArray.includes(name)`) can pass this check (e.g. an
1535
+ * adapter with a broad `acceptsTemplateCall` accepts any
1536
+ * identifier-path callee) even though `name` is a bridged prop arg.
1537
+ * - `populateCsrInlinable` re-runs the same check on the
1538
+ * CSR-*substituted* form, where `someModuleArray` has already been
1539
+ * literal-inlined (`['a','b'].includes(name)`). The callee is no
1540
+ * longer an identifier path at all, so the adapter can't vouch for
1541
+ * it, and the bridged-arg rejection correctly fires — recorded as
1542
+ * `ctx.csrInlinable.get(name) === null`.
1543
+ *
1544
+ * `unsafeLocalNames` (passed in from the Stage-2-derived
1545
+ * `toLegacyInlinability` result) only reflects the first, looser verdict,
1546
+ * so a name `populateCsrInlinable` refused could still be missing from
1547
+ * it. Left alone, `transformExpr` below finds no CSR substitution for the
1548
+ * name (`inlinableConstants` — built from `ctx.csrInlinable` by the
1549
+ * caller — excludes null entries) AND no unsafe flag, so the bare,
1550
+ * module-scope-invisible identifier leaks straight into the emitted
1551
+ * template text (`ReferenceError` at template evaluation, #2106).
1552
+ *
1553
+ * `ctx.csrInlinable` is the ground truth for the CSR path — this makes it
1554
+ * the single source added to (never subtracted from) the effective
1555
+ * unsafe set used below: any post-substitution refusal is unsafe here,
1556
+ * full stop, regardless of what the looser Stage-2 check concluded.
1557
+ * Doing this once, inside `generateCsrTemplate` itself, means every
1558
+ * caller (the full-init path in `emit-registration.ts` AND the
1559
+ * template-only path in `index.ts`) gets the correction for free instead
1560
+ * of each needing to remember to fold `ctx.csrInlinable` in themselves.
1561
+ *
1562
+ * System-construct (`createContext()`, `new WeakMap()`) and JSX-inline
1563
+ * constants are exempted even though `populateCsrInlinable` also marks
1564
+ * them `null` — that's "not applicable" routing (module-scope singleton
1565
+ * referenced by name at runtime / already inlined at IR level), not an
1566
+ * unsafe reference, and `toLegacyInlinability` never treated them as
1567
+ * unsafe either.
1568
+ */
1569
+ function mergeCsrNullUnsafe(ctx: ClientJsContext, unsafeLocalNames: Set<string> | undefined): Set<string> | undefined {
1570
+ let merged: Set<string> | null = null
1571
+ // Built lazily on the first null verdict, so the common all-inlinable
1572
+ // component pays nothing; a Set keeps the merge linear in the number
1573
+ // of local constants instead of a per-name `.find()` scan.
1574
+ let exemptNames: Set<string> | null = null
1575
+ for (const [name, entry] of ctx.csrInlinable) {
1576
+ if (entry !== null || unsafeLocalNames?.has(name)) continue
1577
+ exemptNames ??= new Set(ctx.localConstants.filter((c) => c.isJsx || c.systemConstructKind).map((c) => c.name))
1578
+ if (exemptNames.has(name)) continue
1579
+ if (!merged) merged = new Set(unsafeLocalNames ?? [])
1580
+ merged.add(name)
1581
+ }
1582
+ return merged ?? unsafeLocalNames
1386
1583
  }
1387
1584
 
1388
1585
  /**
@@ -171,6 +171,7 @@ function createContext(
171
171
  initStatements: ir.metadata.initStatements ?? [],
172
172
  localFunctions: ir.metadata.localFunctions,
173
173
  localConstants: ir.metadata.localConstants,
174
+ imports: ir.metadata.imports,
174
175
  propsParams: ir.metadata.propsParams,
175
176
  propsObjectName: ir.metadata.propsObjectName,
176
177
  restPropsName: ir.metadata.restPropsName,
@@ -19,6 +19,7 @@ import type {
19
19
  ConstantInfo,
20
20
  ParamInfo,
21
21
  CompilerError,
22
+ ImportInfo,
22
23
  } from '../types.ts'
23
24
  import type { CsrInlinabilityMap } from './csr-substitute.ts'
24
25
 
@@ -53,6 +54,15 @@ export interface ClientJsContext {
53
54
  initStatements: InitStatementInfo[]
54
55
  localFunctions: FunctionInfo[]
55
56
  localConstants: ConstantInfo[]
57
+ /**
58
+ * Component-file import list, threaded through so `buildEnvFromCtx`'s
59
+ * reconstructed `IRMetadata`-shaped object carries REAL imports (#2069)
60
+ * — `prepareLoweringMatchers` needs the real list to resolve a
61
+ * `LoweringPlugin`'s local import names (e.g. `queryHrefLocalNames`);
62
+ * an empty `imports: []` would silently disable every import-aware
63
+ * plugin for the client-JS inline-safety gate.
64
+ */
65
+ imports: ImportInfo[]
56
66
  propsParams: ParamInfo[]
57
67
  propsObjectName: string | null
58
68
  restPropsName: string | null
@@ -497,6 +507,18 @@ export interface TopLevelLoop extends LoopCore {
497
507
  * that becomes `[]` in the CSR template substitution.
498
508
  */
499
509
  staticItemTemplate?: string
510
+ /**
511
+ * Shared once-per-loop skeleton template (perf), built by
512
+ * `buildLoopSkeletonTemplate` when the loop body is a statically-analyzable
513
+ * single-root element tree whose only dynamic parts are text/attribute
514
+ * slots already covered by a loop-child `createEffect`. Present only for
515
+ * dynamic (`mapArray`) top-level loops that pass the safety predicate in
516
+ * `collect-elements.ts`'s `loop` visitor — `undefined` means "fall back to
517
+ * the per-row interpolated `template` above" (conditionals, spread attrs,
518
+ * multi-root bodies, nested components/loops, or any dynamic attr/text not
519
+ * proven covered by an effect).
520
+ */
521
+ skeletonTemplate?: string
500
522
  childEventHandlers: string[] // Bare-identifier event handler names (for the reachability graph)
501
523
  childComponent?: IRLoopChildComponent // For createComponent-based rendering
502
524
  nestedComponents?: IRLoopChildComponent[] // For nested components in loop bodies