@barefootjs/jsx 0.17.0 → 0.18.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/adapters/env-signal.d.ts +42 -7
- package/dist/adapters/env-signal.d.ts.map +1 -1
- package/dist/adapters/interface.d.ts +20 -4
- package/dist/adapters/interface.d.ts.map +1 -1
- package/dist/adapters/parsed-expr-emitter.d.ts +3 -1
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/analyzer-context.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/augment-inherited-props.d.ts +19 -0
- package/dist/augment-inherited-props.d.ts.map +1 -1
- package/dist/compiler.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +48 -2
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +24 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1728 -969
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/compute-inlinability.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +9 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts +19 -0
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-registration.d.ts +4 -2
- package/dist/ir-to-client-js/emit-registration.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +41 -0
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +22 -1
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/loop-destructure.d.ts +55 -18
- package/dist/loop-destructure.d.ts.map +1 -1
- package/dist/lowering-registry.d.ts +13 -0
- package/dist/lowering-registry.d.ts.map +1 -1
- package/dist/relocate.d.ts +28 -0
- package/dist/relocate.d.ts.map +1 -1
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/ssr-seed-plan.d.ts +84 -0
- package/dist/ssr-seed-plan.d.ts.map +1 -0
- package/dist/types.d.ts +79 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +284 -12
- package/src/__tests__/augment-inherited-props.test.ts +96 -0
- package/src/__tests__/compiler-runtime-contract.test.ts +11 -1
- package/src/__tests__/compiler-stress-1244.test.ts +13 -4
- package/src/__tests__/csr-substitution-safety-divergence.test.ts +137 -0
- package/src/__tests__/destructured-map-params.test.ts +11 -1
- package/src/__tests__/expression-parser.test.ts +74 -3
- package/src/__tests__/free-identifiers.test.ts +55 -0
- package/src/__tests__/ir-sort-comparator.test.ts +261 -0
- package/src/__tests__/loop-destructure.test.ts +313 -0
- package/src/__tests__/loop-hoisted-template.test.ts +235 -0
- package/src/__tests__/materialize-getter-calls.test.ts +58 -0
- package/src/__tests__/props-destructuring.test.ts +110 -0
- package/src/__tests__/serialize-parsed-expr.test.ts +88 -2
- package/src/__tests__/ssr-defaults.test.ts +20 -0
- package/src/__tests__/ssr-seed-plan.test.ts +212 -0
- package/src/__tests__/staged-ir/11-template-primitive-registry.test.ts +231 -1
- package/src/__tests__/tagged-template-interleave.test.ts +268 -0
- package/src/__tests__/unsupported-expression.test.ts +194 -7
- package/src/adapters/env-signal.ts +57 -9
- package/src/adapters/interface.ts +20 -4
- package/src/adapters/parsed-expr-emitter.ts +19 -2
- package/src/analyzer-context.ts +20 -0
- package/src/analyzer.ts +74 -1
- package/src/augment-inherited-props.ts +139 -9
- package/src/compiler.ts +10 -1
- package/src/expression-parser.ts +421 -50
- package/src/index.ts +30 -3
- package/src/ir-to-client-js/collect-elements.ts +15 -1
- package/src/ir-to-client-js/compute-inlinability.ts +6 -1
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +1 -0
- package/src/ir-to-client-js/control-flow/plan/loop.ts +9 -0
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +30 -8
- package/src/ir-to-client-js/control-flow/stringify/template-parse.ts +30 -0
- package/src/ir-to-client-js/emit-registration.ts +4 -2
- package/src/ir-to-client-js/html-template.ts +198 -1
- package/src/ir-to-client-js/index.ts +1 -0
- package/src/ir-to-client-js/types.ts +22 -0
- package/src/jsx-to-ir.ts +409 -24
- package/src/loop-destructure.ts +89 -36
- package/src/lowering-registry.ts +16 -0
- package/src/relocate.ts +201 -14
- package/src/ssr-defaults.ts +34 -32
- package/src/ssr-seed-plan.ts +146 -0
- package/src/types.ts +76 -0
package/src/index.ts
CHANGED
|
@@ -12,6 +12,10 @@ export type { CompileResult, CompileOptions, CompileOptionsWithAdapter, FileOutp
|
|
|
12
12
|
export { extractSsrDefaults } from './ssr-defaults.ts'
|
|
13
13
|
export type { SsrDefault } from './ssr-defaults.ts'
|
|
14
14
|
|
|
15
|
+
// Backend-neutral SSR seed plan (in-template derived signal/memo seeding)
|
|
16
|
+
export { computeSsrSeedPlan } from './ssr-seed-plan.ts'
|
|
17
|
+
export type { SsrSeedPlan, SsrSeedStep } from './ssr-seed-plan.ts'
|
|
18
|
+
|
|
15
19
|
// Pure IR types
|
|
16
20
|
export type {
|
|
17
21
|
ComponentIR,
|
|
@@ -22,6 +26,9 @@ export type {
|
|
|
22
26
|
IRConditional,
|
|
23
27
|
IRLoop,
|
|
24
28
|
IRLoopChildComponent,
|
|
29
|
+
LoopParamBinding,
|
|
30
|
+
LoopBindingPathSegment,
|
|
31
|
+
RestExcludeKey,
|
|
25
32
|
IRComponent,
|
|
26
33
|
IRFragment,
|
|
27
34
|
IRSlot,
|
|
@@ -46,6 +53,8 @@ export type {
|
|
|
46
53
|
TypeDefinition,
|
|
47
54
|
SourceLocation,
|
|
48
55
|
CompilerError,
|
|
56
|
+
ConformancePin,
|
|
57
|
+
ConformancePins,
|
|
49
58
|
} from './types.ts'
|
|
50
59
|
|
|
51
60
|
// Analyzer
|
|
@@ -77,13 +86,15 @@ export type { JsxAdapterConfig } from './adapters/jsx-adapter.ts'
|
|
|
77
86
|
export { rewriteImportsForTemplate } from './adapters/template-imports.ts'
|
|
78
87
|
export { emitParsedExpr } from './adapters/parsed-expr-emitter.ts'
|
|
79
88
|
export type { ParsedExprEmitter, HigherOrderMethod, ArrayMethod, SortMethod, LiteralType } from './adapters/parsed-expr-emitter.ts'
|
|
80
|
-
export { importsSearchParams, searchParamsLocalNames, queryHrefLocalNames, matchSearchParamsMethodCall } from './adapters/env-signal.ts'
|
|
89
|
+
export { importsSearchParams, searchParamsLocalNames, envSignalLocalNames, envSignalReaderFor, ENV_SIGNAL_READERS, queryHrefLocalNames, matchSearchParamsMethodCall } from './adapters/env-signal.ts'
|
|
90
|
+
export type { EnvSignalReader } from './adapters/env-signal.ts'
|
|
81
91
|
export { matchQueryHrefCall, queryHrefArgs, type QueryHrefCall, type QueryHrefTriple } from './query-href-lowering.ts'
|
|
82
92
|
export {
|
|
83
93
|
registerLoweringPlugin,
|
|
84
94
|
getLoweringPlugins,
|
|
85
95
|
prepareLoweringMatchers,
|
|
86
96
|
matchLoweringCall,
|
|
97
|
+
isValidHelperId,
|
|
87
98
|
__resetLoweringPluginsForTest,
|
|
88
99
|
type LoweringPlugin,
|
|
89
100
|
type LoweringNode,
|
|
@@ -255,6 +266,22 @@ export interface BuildOptions {
|
|
|
255
266
|
* Forwarded to `compileJSX` as `CompileOptions.localImportPrefixes`.
|
|
256
267
|
*/
|
|
257
268
|
localImportPrefixes?: string[]
|
|
269
|
+
/**
|
|
270
|
+
* How the CLI produces `barefoot.js` (the client runtime bundle):
|
|
271
|
+
* - `'treeshake'` (default) — bundle only the runtime exports this
|
|
272
|
+
* project's compiled client JS actually imports, plus a small
|
|
273
|
+
* always-kept public mount API (`render`, `hydrate`, etc.).
|
|
274
|
+
* - `'full'` — copy the entire prebuilt runtime bundle verbatim.
|
|
275
|
+
* See `@barefootjs/cli`'s `runtime-treeshake.ts` for the collector and
|
|
276
|
+
* `ALWAYS_KEEP_RUNTIME_EXPORTS` for the always-kept names.
|
|
277
|
+
*/
|
|
278
|
+
runtimeBundle?: 'treeshake' | 'full'
|
|
279
|
+
/**
|
|
280
|
+
* Extra `@barefootjs/client*` export names to force-keep in `barefoot.js`
|
|
281
|
+
* under `runtimeBundle: 'treeshake'` — for names only ever referenced
|
|
282
|
+
* from hand-written page scripts the CLI never compiles.
|
|
283
|
+
*/
|
|
284
|
+
runtimeKeep?: string[]
|
|
258
285
|
}
|
|
259
286
|
|
|
260
287
|
// AttrValue constructors
|
|
@@ -276,12 +303,12 @@ export {
|
|
|
276
303
|
export { ErrorCodes, createError, formatError, generateCodeFrame } from './errors.ts'
|
|
277
304
|
|
|
278
305
|
// Expression Parser
|
|
279
|
-
export { parseExpression, tsNodeToParsedExpr, asCallbackMethodCall, CALLBACK_METHODS, sortComparatorFromArrow, serializeParsedExpr, freeVarsInBody, isSupported, exprToString, stringifyParsedExpr, identifierPath, parseBlockBody, parseBlockBodyTolerant, foldBlockToExpr, predicateTernaryToLogical, containsHigherOrder, extractArrowBodyExpression, parseStyleObjectEntries, parseProviderObjectLiteral, type ProviderObjectMember, type FoldBlockOptions } from './expression-parser.ts'
|
|
306
|
+
export { parseExpression, tsNodeToParsedExpr, asCallbackMethodCall, CALLBACK_METHODS, sortComparatorFromArrow, serializeParsedExpr, freeVarsInBody, freeIdentifiers, materializeGetterCalls, isSupported, exprToString, stringifyParsedExpr, identifierPath, parseBlockBody, parseBlockBodyTolerant, foldBlockToExpr, predicateTernaryToLogical, containsHigherOrder, extractArrowBodyExpression, parseStyleObjectEntries, parseProviderObjectLiteral, type ProviderObjectMember, type FoldBlockOptions } from './expression-parser.ts'
|
|
280
307
|
export type { StyleObjectEntry } from './expression-parser.ts'
|
|
281
308
|
export type { ParsedExpr, ObjectLiteralProperty, ParsedStatement, SortComparator, SortKey, FlatDepth, SupportLevel, SupportResult, TemplatePart } from './expression-parser.ts'
|
|
282
309
|
export { buildLoopChainExpr } from './loop-chain.ts'
|
|
283
310
|
export type { LoopChainInputs } from './loop-chain.ts'
|
|
284
|
-
export { isLowerableObjectRestDestructure } from './loop-destructure.ts'
|
|
311
|
+
export { isLowerableLoopDestructure, isLowerableObjectRestDestructure } from './loop-destructure.ts'
|
|
285
312
|
|
|
286
313
|
// Debug analysis
|
|
287
314
|
export {
|
|
@@ -6,7 +6,7 @@ import { type IRNode, type IRElement, type IRComponent, type IRLoop, type IRProp
|
|
|
6
6
|
import type { ClientJsContext, ConditionalBranchChildComponent, ConditionalBranchReactiveAttr, BranchLoop, ConditionalBranchTextEffect, ConditionalElement, LoopChildBindings, LoopChildBranchSummary, LoopChildConditional, LoopOffset, NestedLoop } from './types.ts'
|
|
7
7
|
import { attrValueToString, freeIdsFromRefs, quotePropName, PROPS_PARAM } from './utils.ts'
|
|
8
8
|
import { classifyReactivity, decideWrapForAttr, decideWrapForChildProp, decideWrapFromAstFlags, collectEventHandlersFromIR, collectConditionalBranchEvents, collectConditionalBranchRefs, collectConditionalBranchChildComponents, collectLoopChildEventsWithNesting, collectLoopChildReactiveAttrs, collectLoopChildReactiveTexts, collectLoopChildRefs, emptyLoopChildBindings } from './reactivity.ts'
|
|
9
|
-
import { irToHtmlTemplate, irToPlaceholderTemplate, irChildrenToJsExpr } from './html-template.ts'
|
|
9
|
+
import { irToHtmlTemplate, irToPlaceholderTemplate, irChildrenToJsExpr, buildLoopSkeletonTemplate } from './html-template.ts'
|
|
10
10
|
import { expandDynamicPropValue, expandConstantForReactivity } from './prop-handling.ts'
|
|
11
11
|
import { walkIR, stopAt } from './walker.ts'
|
|
12
12
|
import { buildLoopChainExpr } from '../loop-chain.ts'
|
|
@@ -671,6 +671,7 @@ export function collectElements(
|
|
|
671
671
|
|
|
672
672
|
let template = ''
|
|
673
673
|
let staticItemTemplate: string | undefined
|
|
674
|
+
let skeletonTemplate: string | undefined
|
|
674
675
|
if (l.childComponent) {
|
|
675
676
|
template = '' // childComponent path uses createComponent directly
|
|
676
677
|
// CSR materialize fallback (#1268): when the loop array references an
|
|
@@ -714,6 +715,18 @@ export function collectElements(
|
|
|
714
715
|
staticItemTemplate = useElementReconciliation
|
|
715
716
|
? irToPlaceholderTemplate(l.children[0], buildRestSpreadNames(ctx), 0)
|
|
716
717
|
: irToHtmlTemplate(l.children[0], buildRestSpreadNames(ctx), 0)
|
|
718
|
+
} else if (!useElementReconciliation && !l.bodyIsMultiRoot && !l.bodyIsItemConditional) {
|
|
719
|
+
// Hoisted shared-template fast path (perf): only for the plain
|
|
720
|
+
// `mapArray` shape — single-root, dynamic array, no element
|
|
721
|
+
// reconciliation. `buildLoopSkeletonTemplate` re-derives safety
|
|
722
|
+
// from the raw IR (spread attrs, conditionals, unslotted dynamic
|
|
723
|
+
// expressions, …) and returns `null` for anything it can't prove
|
|
724
|
+
// safe; the plan builder (`build-loop.ts`) falls back to the
|
|
725
|
+
// per-row `template` above whenever this stays `undefined`.
|
|
726
|
+
skeletonTemplate = buildLoopSkeletonTemplate(l.children[0], {
|
|
727
|
+
reactiveAttrKeys: new Set(bindings.reactiveAttrs.map(a => `${a.childSlotId}::${a.attrName}`)),
|
|
728
|
+
reactiveTextSlotIds: new Set(bindings.reactiveTexts.map(t => t.slotId)),
|
|
729
|
+
}) ?? undefined
|
|
717
730
|
}
|
|
718
731
|
}
|
|
719
732
|
|
|
@@ -732,6 +745,7 @@ export function collectElements(
|
|
|
732
745
|
iterationShape: l.iterationShape,
|
|
733
746
|
template,
|
|
734
747
|
staticItemTemplate,
|
|
748
|
+
skeletonTemplate,
|
|
735
749
|
childEventHandlers: childHandlers,
|
|
736
750
|
bindings,
|
|
737
751
|
childComponent: l.childComponent,
|
|
@@ -64,7 +64,12 @@ export function buildEnvFromCtx(ctx: ClientJsContext): RelocateEnv {
|
|
|
64
64
|
effects: ctx.effects,
|
|
65
65
|
onMounts: ctx.onMounts,
|
|
66
66
|
initStatements: ctx.initStatements,
|
|
67
|
-
imports
|
|
67
|
+
// Real component imports (#2069) — `buildRelocateEnvFromIR` calls
|
|
68
|
+
// `prepareLoweringMatchers(metadata)` on this reconstructed object,
|
|
69
|
+
// and plugin `prepare()` resolves local import names from
|
|
70
|
+
// `metadata.imports`. `[]` here would silently disable every
|
|
71
|
+
// import-aware LoweringPlugin for the client-JS inline-safety gate.
|
|
72
|
+
imports: ctx.imports,
|
|
68
73
|
templateImports: [],
|
|
69
74
|
namedExports: [],
|
|
70
75
|
localFunctions: ctx.localFunctions,
|
|
@@ -102,6 +102,7 @@ export function buildPlainLoopPlan(elem: TopLevelLoop, profileComponentName?: st
|
|
|
102
102
|
indexParam: elem.index || '__idx',
|
|
103
103
|
mapPreambleWrapped: elem.mapPreamble ? wrap(elem.mapPreamble) : '',
|
|
104
104
|
template: elem.template,
|
|
105
|
+
skeletonTemplate: elem.skeletonTemplate,
|
|
105
106
|
reactiveEffects: hasReactive ? buildLoopReactiveEffectsPlan(elem, profileComponentName) : null,
|
|
106
107
|
childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings),
|
|
107
108
|
bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false,
|
|
@@ -94,6 +94,15 @@ interface PlainLoopVariant extends DynamicLoopCommon {
|
|
|
94
94
|
mapPreambleWrapped: string
|
|
95
95
|
/** HTML template string for one item. */
|
|
96
96
|
template: string
|
|
97
|
+
/**
|
|
98
|
+
* Shared once-per-loop skeleton template (perf) — see
|
|
99
|
+
* `buildLoopSkeletonTemplate` (html-template.ts). When present, the
|
|
100
|
+
* stringifier declares it once (before the `mapArray` call) and clones
|
|
101
|
+
* from it instead of re-parsing `template`'s interpolated innerHTML per
|
|
102
|
+
* row. `undefined` means the loop body wasn't proven safe to hoist; the
|
|
103
|
+
* stringifier falls back to the legacy per-row `emitTemplateCloneInline`.
|
|
104
|
+
*/
|
|
105
|
+
skeletonTemplate?: string
|
|
97
106
|
/** Resolved reactive-effects plan — null forces the single-line renderItem shape. */
|
|
98
107
|
reactiveEffects: ReactiveEffectsPlan | null
|
|
99
108
|
/**
|
|
@@ -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 =
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
*
|
|
106
|
-
*
|
|
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
|
-
|
|
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
|