@barefootjs/jsx 0.33.3 → 0.33.6
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/child-scope.d.ts +13 -0
- package/dist/adapters/child-scope.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1392 -1117
- package/dist/ir-to-client-js/build-references.d.ts.map +1 -1
- package/dist/ir-to-client-js/child-components.d.ts.map +1 -1
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/component-scope.d.ts +20 -0
- package/dist/ir-to-client-js/component-scope.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-component-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +2 -4
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/shared.d.ts +4 -3
- package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/component-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.d.ts.map +1 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts +35 -9
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts +10 -0
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +49 -2
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/index.d.ts.map +1 -1
- package/dist/ir-to-client-js/rewrite-props-object.d.ts +12 -1
- package/dist/ir-to-client-js/rewrite-props-object.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/prop-rewrite.d.ts +5 -2
- package/dist/prop-rewrite.d.ts.map +1 -1
- package/dist/props-binding.d.ts +49 -11
- package/dist/props-binding.d.ts.map +1 -1
- package/dist/types.d.ts +19 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +15 -15
- package/src/__tests__/aliased-import-child-registry-2777.test.ts +112 -0
- package/src/__tests__/date-lowering.test.ts +18 -4
- package/src/__tests__/fragment-body-loop-key.test.ts +95 -0
- package/src/__tests__/inner-loop-mathml-namespace.test.ts +1 -1
- package/src/__tests__/inner-loop-svg-namespace.test.ts +4 -4
- package/src/__tests__/issue-2724-prop-alias-loop-not-static.test.ts +355 -0
- package/src/__tests__/issue-2737-props-object-alias-template.test.ts +86 -0
- package/src/__tests__/issue-2756-input-skeleton-bails.test.ts +123 -0
- package/src/__tests__/issue-2778-getter-alias-csr-template.test.ts +113 -0
- package/src/__tests__/issue-2806-rest-bag-template-rewrite.test.ts +136 -0
- package/src/__tests__/issue-2835-props-type-member-name-collision.test.ts +259 -0
- package/src/__tests__/loop-item-root-scope.test.ts +7 -3
- package/src/__tests__/markup-prop-brand.test.ts +165 -26
- package/src/__tests__/multi-root-loop-body.test.ts +7 -3
- package/src/__tests__/namespace-import-primitive.test.ts +244 -0
- package/src/__tests__/preamble-declarations.test.ts +42 -0
- package/src/__tests__/rewrite-destructured-props.test.ts +53 -0
- package/src/__tests__/static-loop-csr-materialize.test.ts +2 -2
- package/src/adapters/child-scope.ts +23 -0
- package/src/analyzer.ts +130 -0
- package/src/errors.ts +14 -1
- package/src/index.ts +7 -0
- package/src/ir-to-client-js/build-references.ts +7 -0
- package/src/ir-to-client-js/child-components.ts +8 -1
- package/src/ir-to-client-js/collect-elements.ts +10 -38
- package/src/ir-to-client-js/component-scope.ts +60 -0
- package/src/ir-to-client-js/compute-inlinability.ts +46 -11
- package/src/ir-to-client-js/control-flow/plan/build-component-loop.ts +19 -1
- package/src/ir-to-client-js/control-flow/plan/loop.ts +2 -4
- package/src/ir-to-client-js/control-flow/shared.ts +12 -5
- package/src/ir-to-client-js/control-flow/stringify/component-loop.ts +7 -0
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +8 -2
- package/src/ir-to-client-js/control-flow.ts +12 -0
- package/src/ir-to-client-js/csr-substitute.ts +82 -16
- package/src/ir-to-client-js/emit-reactive.ts +29 -13
- package/src/ir-to-client-js/emit-registration.ts +1 -1
- package/src/ir-to-client-js/html-template.ts +194 -82
- package/src/ir-to-client-js/imports.ts +4 -0
- package/src/ir-to-client-js/index.ts +34 -23
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +7 -2
- package/src/ir-to-client-js/rewrite-props-object.ts +14 -0
- package/src/jsx-to-ir.ts +318 -54
- package/src/prop-rewrite.ts +23 -5
- package/src/props-binding.ts +74 -18
- package/src/types.ts +19 -5
|
@@ -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, buildLoopRowScope, anyNameIn } from './reactivity.ts'
|
|
9
|
-
import { irToHtmlTemplate, irToPlaceholderTemplate, irChildrenToJsExpr, buildLoopSkeletonTemplate, computeSkeletonSlotPaths, renderFlatMapClientBody, renderFlatMapProjectionClientBody, flatMapCallbackHasKeyedLeaf, type SkeletonSlotPaths } from './html-template.ts'
|
|
9
|
+
import { irToHtmlTemplate, irToPlaceholderTemplate, irChildrenToJsExpr, jsxChildrenPropGetterExpr, buildLoopSkeletonTemplate, computeSkeletonSlotPaths, renderFlatMapClientBody, renderFlatMapProjectionClientBody, flatMapCallbackHasKeyedLeaf, type SkeletonSlotPaths } from './html-template.ts'
|
|
10
10
|
import { detectRootNamespaceWrapTag } from './control-flow/stringify/template-parse.ts'
|
|
11
11
|
import { expandDynamicPropValue, expandConstantForReactivity, resolveRestSpreadOrigin, resolveRestSpreadNames } from './prop-handling.ts'
|
|
12
12
|
import { extractFreeIdentifiersFromText } from './csr-substitute.ts'
|
|
@@ -475,29 +475,6 @@ function jsxChildrenContainComponent(nodes: IRNode[]): boolean {
|
|
|
475
475
|
return false
|
|
476
476
|
}
|
|
477
477
|
|
|
478
|
-
/**
|
|
479
|
-
* Narrow gate for branding a `jsx-children` getter's value with `bfMarkup()`
|
|
480
|
-
* (#2651). The constructor (`jsx-to-ir.ts`'s `processComponentProps`) always
|
|
481
|
-
* stores exactly one node here (`AttrValueOf.jsxChildren([...])`), so
|
|
482
|
-
* `nodes[0]` is the whole payload; `irChildrenToJsExpr` (`html-template.ts`)
|
|
483
|
-
* turns a single `'element'` node into ONE HTML-string template literal —
|
|
484
|
-
* the same shape the `renderChild` / `irToComponentTemplateWithOpts` /
|
|
485
|
-
* `generateCsrTemplateWithOpts` "jsx-children" doors always produce (they
|
|
486
|
-
* join every child into one string regardless of shape), and the shape
|
|
487
|
-
* this fixture's `header={<strong>Title</strong>}` exercises.
|
|
488
|
-
*
|
|
489
|
-
* Deliberately narrow — NOT a general "does this reduce to one string"
|
|
490
|
-
* check: a `'fragment'` node (`header={<>text<strong/></>}`, multiple
|
|
491
|
-
* children) or a `'conditional'` node reduces through `irChildrenToJsExpr`
|
|
492
|
-
* to an array literal or a nested ternary of un-escaped-vs-escaped parts,
|
|
493
|
-
* for which `escapeTextOrNode`/`escapeTextOrMarkup` have no (array) or an
|
|
494
|
-
* unproven (ternary) contract. Those shapes are left unbranded — see
|
|
495
|
-
* #2651's door inventory — rather than guessed at here.
|
|
496
|
-
*/
|
|
497
|
-
function isSingleElementJsxChildren(nodes: IRNode[]): boolean {
|
|
498
|
-
return nodes.length === 1 && nodes[0].type === 'element'
|
|
499
|
-
}
|
|
500
|
-
|
|
501
478
|
/** Build propsExpr for a child component from its IR props. */
|
|
502
479
|
function buildComponentPropsExpr(props: IRProp[], ctx: ClientJsContext): string {
|
|
503
480
|
const knownSpreadProp = props.find(p => {
|
|
@@ -526,19 +503,13 @@ function buildComponentPropsExpr(props: IRProp[], ctx: ClientJsContext): string
|
|
|
526
503
|
}
|
|
527
504
|
switch (prop.value.kind) {
|
|
528
505
|
case 'jsx-children': {
|
|
529
|
-
const jsxExpr = irChildrenToJsExpr(prop.value.children)
|
|
530
506
|
if (jsxChildrenContainComponent(prop.value.children)) {
|
|
507
|
+
const jsxExpr = irChildrenToJsExpr(prop.value.children)
|
|
531
508
|
propsForInit.push(`get ${quotePropName(prop.name)}() { return __slot(() => ${jsxExpr}) }`)
|
|
532
|
-
} else if (prop.name !== 'children' && isSingleElementJsxChildren(prop.value.children)) {
|
|
533
|
-
// Brand (#2651): `jsxExpr` is the single HTML-string template
|
|
534
|
-
// literal `irChildrenToJsExpr` builds for a lone 'element' node —
|
|
535
|
-
// see `isSingleElementJsxChildren`'s docstring for why only this
|
|
536
|
-
// shape is branded here. Excludes an explicit `children={<jsx/>}`
|
|
537
|
-
// prop (out of scope, unchanged) — the child's `{children}`
|
|
538
|
-
// interpolation is a bare passthrough with no unwrap call, so a
|
|
539
|
-
// branded object there would stringify to `[object Object]`.
|
|
540
|
-
propsForInit.push(`get ${quotePropName(prop.name)}() { return bfMarkup(${jsxExpr}) }`)
|
|
541
509
|
} else {
|
|
510
|
+
// Brand via the shared door (#2651/#2702) — see
|
|
511
|
+
// `jsxChildrenPropGetterExpr`'s docstring.
|
|
512
|
+
const jsxExpr = jsxChildrenPropGetterExpr(prop.value.children, prop.name)
|
|
542
513
|
propsForInit.push(`get ${quotePropName(prop.name)}() { return ${jsxExpr} }`)
|
|
543
514
|
}
|
|
544
515
|
break
|
|
@@ -772,10 +743,11 @@ export function collectElements(
|
|
|
772
743
|
// passed), matching the plain-element path's `staticItemTemplate`.
|
|
773
744
|
if (l.isStaticArray && l.children[0]) {
|
|
774
745
|
// `l.children[0]` is the loop's row-root component — already
|
|
775
|
-
// tagged `loopItemRoot` in the IR, so `
|
|
776
|
-
//
|
|
777
|
-
//
|
|
778
|
-
//
|
|
746
|
+
// tagged `loopItemRoot` in the IR, so `renderChildScopeArgs`
|
|
747
|
+
// passes its slotId with the `loopItemRoot` flag (#2833) instead
|
|
748
|
+
// of deriving its own scope from the parent slot. Each iteration
|
|
749
|
+
// still owns a distinct scope identified by `data-key`, mirroring
|
|
750
|
+
// the SSR template's renderChild emit.
|
|
779
751
|
staticItemTemplate = irToHtmlTemplate(l.children[0], resolveRestSpreadNames(ctx), 0, undefined, undefined)
|
|
780
752
|
}
|
|
781
753
|
} else if (l.children[0] && !projectionInner) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ImportInfo } from '../types.ts'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* File-scoped component name disambiguation for the runtime registry.
|
|
3
5
|
*
|
|
@@ -63,8 +65,66 @@ export function setActiveComponentScope(scope: { fileScope: string; nonExportedS
|
|
|
63
65
|
_activeScope = scope
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
/**
|
|
69
|
+
* #2777 — a client component referenced under an import alias
|
|
70
|
+
* (`import { Foo as Bar } from './Foo'`, `<Bar/>`) must resolve its
|
|
71
|
+
* registry key to the DECLARED/exported name (`Foo`, what the child's own
|
|
72
|
+
* module registers under via `hydrate('Foo', ...)`), not the caller-local
|
|
73
|
+
* binding (`Bar`) — the registry is keyed by string name, so a mismatch
|
|
74
|
+
* here left `initChild('Bar', ...)` unable to find `Foo`'s registration
|
|
75
|
+
* and hydration silently never ran.
|
|
76
|
+
*
|
|
77
|
+
* A local alias → exported name map, active per client-JS generation the
|
|
78
|
+
* same way `_activeScope` is (module-level state, installed/cleared by
|
|
79
|
+
* `generateClientJsWithSourceMap`) rather than threaded through every
|
|
80
|
+
* recursive emitter — same rationale as `_activeScope` above.
|
|
81
|
+
*/
|
|
82
|
+
let _activeImportAliases: ReadonlyMap<string, string> | null = null
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Build a local-alias → exported-name map from a component's parsed
|
|
86
|
+
* imports. Only VALUE, non-default, non-namespace, ALIASED specifiers
|
|
87
|
+
* produce an entry — a bare `import { Foo }` needs no rewrite (`name`
|
|
88
|
+
* already equals what's registered), and default/namespace imports can't
|
|
89
|
+
* be resolved this way (a default's exported name isn't recorded here,
|
|
90
|
+
* and a namespace member reference isn't an aliased specifier at all).
|
|
91
|
+
*/
|
|
92
|
+
export function buildImportAliasMap(imports: readonly ImportInfo[]): Map<string, string> {
|
|
93
|
+
const aliases = new Map<string, string>()
|
|
94
|
+
for (const imp of imports) {
|
|
95
|
+
if (imp.isTypeOnly) continue
|
|
96
|
+
for (const spec of imp.specifiers) {
|
|
97
|
+
if (spec.isTypeOnly || spec.isDefault || spec.isNamespace || spec.alias === null) continue
|
|
98
|
+
aliases.set(spec.alias, spec.name)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return aliases
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function setActiveImportAliases(aliases: ReadonlyMap<string, string> | null): void {
|
|
105
|
+
_activeImportAliases = aliases
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Resolve a local JSX tag name to the name the referenced component's own
|
|
110
|
+
* module registers under (`hydrate(<name>, ...)`) — the caller-facing
|
|
111
|
+
* alias if imported under one, otherwise the name unchanged. Used
|
|
112
|
+
* anywhere a name needs to reach the runtime registry WITHOUT also going
|
|
113
|
+
* through the non-exported-sibling rewrite below (the `@bf-child:` marker
|
|
114
|
+
* comment, which must never carry a same-file sibling's hash).
|
|
115
|
+
*/
|
|
116
|
+
export function resolveImportedComponentName(name: string): string {
|
|
117
|
+
return _activeImportAliases?.get(name) ?? name
|
|
118
|
+
}
|
|
119
|
+
|
|
66
120
|
/** Resolve a component name to its registry key under the active file scope. */
|
|
67
121
|
export function nameForRegistryRef(name: string): string {
|
|
122
|
+
// An aliased IMPORT always wins over the non-exported-sibling rewrite:
|
|
123
|
+
// `import { Foo as Bar } from './Foo'` plus a private same-file
|
|
124
|
+
// `function Foo() {}` must still resolve `<Bar/>` to the imported
|
|
125
|
+
// `Foo`, not hash the sibling's `Foo` key onto it.
|
|
126
|
+
const imported = _activeImportAliases?.get(name)
|
|
127
|
+
if (imported !== undefined) return imported
|
|
68
128
|
if (!_activeScope) return name
|
|
69
129
|
if (!_activeScope.fileScope) return name
|
|
70
130
|
if (!_activeScope.nonExportedSiblings.has(name)) return name
|
|
@@ -32,7 +32,7 @@ import type { RelocateEnv, RelocateDecision } from '../relocate.ts'
|
|
|
32
32
|
import { createError, ErrorCodes } from '../errors.ts'
|
|
33
33
|
import { attrValueToString } from './utils.ts'
|
|
34
34
|
import { walkIR, type IRVisitor } from './walker.ts'
|
|
35
|
-
import { buildSignalMemoEnv, csrSubstitute, type CsrEnv, type CsrSubstitution } from './csr-substitute.ts'
|
|
35
|
+
import { buildSignalMemoEnv, csrSubstitute, resolveGetterAliases, type CsrEnv, type CsrSubstitution } from './csr-substitute.ts'
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Build a `RelocateEnv` from a live `ClientJsContext`. The IR-keyed env
|
|
@@ -237,6 +237,13 @@ export function computeInlinability(
|
|
|
237
237
|
const signalGetters = new Set(ctx.signals.map(s => s.getter))
|
|
238
238
|
const signalSetters = new Set(ctx.signals.filter(s => s.setter).map(s => s.setter!))
|
|
239
239
|
const memoNames = new Set(ctx.memos.map(m => m.name))
|
|
240
|
+
// A bare alias-hop chain that ultimately names a signal/memo getter
|
|
241
|
+
// (`const items__alias = items`, #2778) is `reactive-read` the same as
|
|
242
|
+
// a direct reference — computed once here and threaded to both the
|
|
243
|
+
// per-const classification below AND `populateCsrInlinable`, so the
|
|
244
|
+
// two passes can't independently reach different verdicts for the
|
|
245
|
+
// same alias.
|
|
246
|
+
const getterAliases = resolveGetterAliases(ctx.localConstants, (n) => signalGetters.has(n) || memoNames.has(n))
|
|
240
247
|
|
|
241
248
|
// RelocateEnv is built once per component from the live ClientJsContext.
|
|
242
249
|
// The adapter capabilities (`templatePrimitives` / `acceptsTemplateCall`)
|
|
@@ -253,6 +260,7 @@ export function computeInlinability(
|
|
|
253
260
|
signalSetters,
|
|
254
261
|
memoNames,
|
|
255
262
|
env,
|
|
263
|
+
getterAliases,
|
|
256
264
|
)
|
|
257
265
|
constants.set(c.name, status)
|
|
258
266
|
decisionsByName.set(c.name, decisions)
|
|
@@ -267,7 +275,7 @@ export function computeInlinability(
|
|
|
267
275
|
// transformation. The map lives on `ClientJsContext` — not on the
|
|
268
276
|
// cross-adapter `ConstantInfo` IR — so SSR adapters don't see CSR
|
|
269
277
|
// substitution semantics they have no use for.
|
|
270
|
-
populateCsrInlinable(ctx, env)
|
|
278
|
+
populateCsrInlinable(ctx, env, getterAliases)
|
|
271
279
|
|
|
272
280
|
return { constants, functions, decisionsByName, templateRiskyNames }
|
|
273
281
|
}
|
|
@@ -289,20 +297,26 @@ export function computeInlinability(
|
|
|
289
297
|
* - the value contains an arrow / function expression (identity per render)
|
|
290
298
|
* - it's a system construct (`createContext`, `new WeakMap`)
|
|
291
299
|
* - it's a JSX literal (handled by jsx-inline routing)
|
|
300
|
+
* - it's a bare alias-hop chain naming a signal/memo getter (`getterAliases`)
|
|
292
301
|
* - the substituted form fails `isInlinableInTemplate` (would re-execute
|
|
293
302
|
* a non-pure call at template-eval time — #1138)
|
|
294
303
|
*/
|
|
295
|
-
function populateCsrInlinable(ctx: ClientJsContext, relocateEnv: RelocateEnv): void {
|
|
304
|
+
function populateCsrInlinable(ctx: ClientJsContext, relocateEnv: RelocateEnv, getterAliases: ReadonlyMap<string, string>): void {
|
|
296
305
|
if (ctx.localConstants.length === 0) return
|
|
297
306
|
|
|
298
|
-
// Base env: signal getters + memo calls in raw (props.X) form
|
|
299
|
-
//
|
|
300
|
-
// `
|
|
301
|
-
//
|
|
302
|
-
// `
|
|
307
|
+
// Base env: signal getters + memo calls in raw (props.X) form, PLUS
|
|
308
|
+
// `getterAliases`'s call-kind entries (`buildSignalMemoEnv`'s own
|
|
309
|
+
// `localConstants` param, #2778) — the same call-kind entry an
|
|
310
|
+
// alias's origin already has, so `items__alias()` substitutes exactly
|
|
311
|
+
// like `items()` rather than through this function's generic
|
|
312
|
+
// identifier-inlining path below (see the pre-mark loop's comment for
|
|
313
|
+
// why that path must never see these names). We keep the substitution
|
|
314
|
+
// map raw so the post-substitution `isInlinableInTemplate` check sees
|
|
315
|
+
// bridged prop references (the form `useYjs(props.X)`) and rejects them
|
|
316
|
+
// via `hasCallWithBridgedArg` (#1138). The final `_p.X` rewrite happens
|
|
303
317
|
// when the template emitter calls `csrSubstitute` on the IR text —
|
|
304
318
|
// by then it's safe because we already know the form is inline-safe.
|
|
305
|
-
const baseEnv = buildSignalMemoEnv(ctx.signals, ctx.memos, ctx.propsObjectName)
|
|
319
|
+
const baseEnv = buildSignalMemoEnv(ctx.signals, ctx.memos, ctx.propsObjectName, ctx.localConstants)
|
|
306
320
|
const constSubs = new Map<string, CsrSubstitution>()
|
|
307
321
|
const finalised = new Set<string>()
|
|
308
322
|
|
|
@@ -314,8 +328,18 @@ function populateCsrInlinable(ctx: ClientJsContext, relocateEnv: RelocateEnv): v
|
|
|
314
328
|
// Pre-mark consts that are structurally ineligible — they record
|
|
315
329
|
// `null` in `ctx.csrInlinable`. Mirrors `classifyConstantInitial`
|
|
316
330
|
// for the kinds that have no value to substitute.
|
|
331
|
+
//
|
|
332
|
+
// A `getterAliases` name is pre-marked `null` here too — NOT because
|
|
333
|
+
// it can't be substituted (it already IS, as a call-kind entry in
|
|
334
|
+
// `baseEnv` above) but because this function's generic path below
|
|
335
|
+
// would otherwise resolve it through `isInlinableInTemplate`, whose
|
|
336
|
+
// signal-getter FALLBACK verdict (`rewrittenAs: 'undefined'`, meant
|
|
337
|
+
// for a getter referenced bare and uncalled) would freeze in as an
|
|
338
|
+
// identifier-kind override that wins over `baseEnv`'s correct
|
|
339
|
+
// call-kind entry (`buildEnvWithConsts`'s Map spread puts `constSubs`
|
|
340
|
+
// last) — the literal `(undefined)()` this issue is about (#2778).
|
|
317
341
|
for (const c of ctx.localConstants) {
|
|
318
|
-
if (c.isJsx || !c.value || c.containsArrow || c.systemConstructKind) {
|
|
342
|
+
if (c.isJsx || !c.value || c.containsArrow || c.systemConstructKind || getterAliases.has(c.name)) {
|
|
319
343
|
ctx.csrInlinable.set(c.name, null)
|
|
320
344
|
finalised.add(c.name)
|
|
321
345
|
}
|
|
@@ -371,7 +395,7 @@ function populateCsrInlinable(ctx: ClientJsContext, relocateEnv: RelocateEnv): v
|
|
|
371
395
|
// stay out of the template lambda. The relocate output
|
|
372
396
|
// (`rewrittenValue`) is the bridged form (bare destructured prop
|
|
373
397
|
// refs lifted to `_p.X`); we store that so the emit-time
|
|
374
|
-
// `
|
|
398
|
+
// `rewritePropsObjectRef` doesn't need to redo the AST work. The
|
|
375
399
|
// free identifiers are re-extracted from the bridged form so
|
|
376
400
|
// the post-substitution unsafe-name check stays exact.
|
|
377
401
|
const inlineResult = isInlinableInTemplate(rewritten, relocateEnv)
|
|
@@ -598,11 +622,22 @@ function classifyConstantInitial(
|
|
|
598
622
|
signalSetters: Set<string>,
|
|
599
623
|
memoNames: Set<string>,
|
|
600
624
|
env: RelocateEnv,
|
|
625
|
+
getterAliases: ReadonlyMap<string, string>,
|
|
601
626
|
): { status: ConstantInlinability; decisions: RelocateDecision[] } {
|
|
602
627
|
if (c.isJsx) return { status: { kind: 'jsx-inline' }, decisions: [] }
|
|
603
628
|
if (!c.value) return { status: { kind: 'placeholder-let' }, decisions: [] }
|
|
604
629
|
if (c.containsArrow) return { status: { kind: 'arrow-literal' }, decisions: [] }
|
|
605
630
|
if (c.systemConstructKind) return { status: { kind: 'system-construct' }, decisions: [] }
|
|
631
|
+
// A multi-hop alias chain to a getter (`const a = items; const b = a`)
|
|
632
|
+
// has `b`'s OWN free id as `a` — an `init-local`, not a getter — so the
|
|
633
|
+
// direct free-id check just below only catches the one-hop case. Without
|
|
634
|
+
// this, `b` falls through to the stage-2 `isInlinableInTemplate` check,
|
|
635
|
+
// which resolves `a` to `init-local fallback` and (since `b` is
|
|
636
|
+
// template-risky) turns a now-correctly-substituted reference into a
|
|
637
|
+
// spurious BF061 error (#2778). `getterAliases` already walked the full
|
|
638
|
+
// chain, so checking it here makes every hop agree with the one-hop case
|
|
639
|
+
// without re-deriving the walk.
|
|
640
|
+
if (getterAliases.has(c.name)) return { status: { kind: 'reactive-read' }, decisions: [] }
|
|
606
641
|
|
|
607
642
|
const freeIds = c.freeIdentifiers
|
|
608
643
|
if (freeIds) {
|
|
@@ -26,9 +26,10 @@ import {
|
|
|
26
26
|
isTextOnlyConditional,
|
|
27
27
|
buildChildRefBindings,
|
|
28
28
|
} from '../shared.ts'
|
|
29
|
-
import { irChildrenToJsExpr } from '../../html-template.ts'
|
|
29
|
+
import { irChildrenToJsExpr, renderPreamble } from '../../html-template.ts'
|
|
30
30
|
import { buildReactiveEffectsPlan } from './build-reactive-effects.ts'
|
|
31
31
|
import type { ComponentLoopPlan, NestedComponentInit } from './types.ts'
|
|
32
|
+
import { internalInvariant } from '../../../errors.ts'
|
|
32
33
|
|
|
33
34
|
/** @internal — prefer `buildLoopPlan`. */
|
|
34
35
|
export function buildComponentLoopPlan(elem: TopLevelLoop, profileComponentName?: string): ComponentLoopPlan {
|
|
@@ -37,6 +38,22 @@ export function buildComponentLoopPlan(elem: TopLevelLoop, profileComponentName?
|
|
|
37
38
|
const keyExpr = wrapLoopParamAsAccessor(elem.key || '__idx', elem.param, elem.paramBindings)
|
|
38
39
|
const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(elem.param, elem.paramBindings)
|
|
39
40
|
|
|
41
|
+
// A component-root loop's preamble is JS-only by construction: Phase 1
|
|
42
|
+
// (jsx-to-ir.ts) already refuses and strips any preamble with a JSX leaf
|
|
43
|
+
// (`builderNames.length > 0`) on this shape, because a lowered HTML-string
|
|
44
|
+
// leaf passed as a prop would diverge from SSR's real JSX elements — see
|
|
45
|
+
// `rowConstruction: 'dom-ops'` below. `renderLeaf` is therefore never
|
|
46
|
+
// reachable here; if it ever fires, a Phase-1 refusal for the new shape
|
|
47
|
+
// that let a JSX-bearing preamble through is missing, not this line.
|
|
48
|
+
const mapPreambleWrapped = elem.preamble
|
|
49
|
+
? renderPreamble(elem.preamble, {
|
|
50
|
+
transformJs: text => wrapLoopParamAsAccessor(text, elem.param, elem.paramBindings),
|
|
51
|
+
renderLeaf: () => {
|
|
52
|
+
internalInvariant(false, 'component-root loop received a JSX-bearing preamble — Phase 1 should have refused it')
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
: ''
|
|
56
|
+
|
|
40
57
|
// Only init components at loopDepth 0 — inner-loop components are handled by their own loop
|
|
41
58
|
const outerNestedComps = (elem.nestedComponents ?? []).filter(c => !c.loopDepth)
|
|
42
59
|
const nestedComps: NestedComponentInit[] = outerNestedComps.map(comp => {
|
|
@@ -64,6 +81,7 @@ export function buildComponentLoopPlan(elem: TopLevelLoop, profileComponentName?
|
|
|
64
81
|
// prop would diverge from SSR (which passes real JSX elements). The plan
|
|
65
82
|
// dispatcher refuses a JSX-bearing preamble on this variant.
|
|
66
83
|
rowConstruction: 'dom-ops',
|
|
84
|
+
mapPreambleWrapped,
|
|
67
85
|
containerVar: `_${varSlotId(elem.slotId)}`,
|
|
68
86
|
markerId: elem.markerId,
|
|
69
87
|
arrayExpr: buildChainedArrayExpr(elem),
|
|
@@ -77,6 +77,8 @@ interface DynamicLoopCommon extends LoopPlanCommon {
|
|
|
77
77
|
paramHead: string
|
|
78
78
|
/** Statement to unwrap a destructured param at body entry. Empty when not needed. */
|
|
79
79
|
paramUnwrap: string
|
|
80
|
+
/** Pre-render preamble line (already wrapped with loop param accessor). Empty when none. */
|
|
81
|
+
mapPreambleWrapped: string
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
/**
|
|
@@ -117,8 +119,6 @@ export interface LoopChildRefBinding {
|
|
|
117
119
|
*/
|
|
118
120
|
interface PlainLoopVariant extends DynamicLoopCommon {
|
|
119
121
|
kind: 'plain'
|
|
120
|
-
/** Pre-render preamble line (already wrapped with loop param accessor). Empty when none. */
|
|
121
|
-
mapPreambleWrapped: string
|
|
122
122
|
/** HTML template string for one item. */
|
|
123
123
|
template: string
|
|
124
124
|
/**
|
|
@@ -225,8 +225,6 @@ interface ComponentLoopVariant extends DynamicLoopCommon {
|
|
|
225
225
|
*/
|
|
226
226
|
interface CompositeLoopVariant extends DynamicLoopCommon {
|
|
227
227
|
kind: 'composite'
|
|
228
|
-
/** Wrapped mapPreamble line, hoisted before the SSR/CSR split. Empty when none. */
|
|
229
|
-
mapPreambleWrapped: string
|
|
230
228
|
/** Inner template HTML for the loop body (single item). */
|
|
231
229
|
template: string
|
|
232
230
|
/** Outer-level child components (depth 0), with `insideConditional` ones already filtered out. */
|
|
@@ -16,7 +16,7 @@ import type { LoopChildEvent, LoopChildRef, TopLevelLoop, NestedLoop, CollectedL
|
|
|
16
16
|
import type { IRLoopChildComponent, LoopParamBinding } from '../../types.ts'
|
|
17
17
|
import { preambleAnalysisText } from '../../types.ts'
|
|
18
18
|
import { quotePropName, wrapLoopParamAsAccessor, irChildrenFreeIds, attrValueToString } from '../utils.ts'
|
|
19
|
-
import { irChildrenToJsExpr } from '../html-template.ts'
|
|
19
|
+
import { irChildrenToJsExpr, jsxChildrenPropGetterExpr } from '../html-template.ts'
|
|
20
20
|
import { emitListenerBlock } from './stringify/event-listener.ts'
|
|
21
21
|
import { nameForRegistryRef } from '../component-scope.ts'
|
|
22
22
|
import { BF_SCOPE, BF_HOST, BF_AT } from '@barefootjs/shared'
|
|
@@ -285,7 +285,13 @@ export function buildComponentPropsExpr(
|
|
|
285
285
|
case 'boolean-attr':
|
|
286
286
|
return `get ${quotePropName(p.name)}() { return true }`
|
|
287
287
|
case 'jsx-children': {
|
|
288
|
-
|
|
288
|
+
// Brand via the shared door (#2651/#2702) — see
|
|
289
|
+
// `jsxChildrenPropGetterExpr`'s docstring. This builder is shared
|
|
290
|
+
// by loop and conditional-branch component reconciliation, so a
|
|
291
|
+
// named jsx-children prop on a component instantiated in either
|
|
292
|
+
// context used to reach its getter unbranded (pullfrog review, PR
|
|
293
|
+
// #2820).
|
|
294
|
+
const childExpr = jsxChildrenPropGetterExpr(p.value.children, p.name)
|
|
289
295
|
return `get ${quotePropName(p.name)}() { return ${wrap(childExpr)} }`
|
|
290
296
|
}
|
|
291
297
|
case 'expression':
|
|
@@ -360,9 +366,10 @@ function emitEventSetup(
|
|
|
360
366
|
* e.g. `qsa(__el, ${buildCompSelector(c)})`.
|
|
361
367
|
*
|
|
362
368
|
* - With `slotId`: primary `(bf-h, bf-m)` against the enclosing scope's
|
|
363
|
-
* `__scopeId`, with a `[bf-s$="_<slot>"]` fallback for
|
|
364
|
-
*
|
|
365
|
-
*
|
|
369
|
+
* `__scopeId`, with a `[bf-s$="_<slot>"]` fallback for a DSL adapter's SSR
|
|
370
|
+
* output that doesn't stamp `(bf-h, bf-m)` on this shape (CSR's own
|
|
371
|
+
* `renderChild` stamps both, loop item root included, since #2833). See
|
|
372
|
+
* `spec/compiler.md` "Slot identity".
|
|
366
373
|
* - Without `slotId`: `bf-s^="<Name>_"` name-prefix for top-level lookup.
|
|
367
374
|
*/
|
|
368
375
|
export function buildCompSelector(comp: { slotId?: string | null; name: string }): string {
|
|
@@ -49,11 +49,18 @@ export function stringifyComponentLoop(lines: string[], plan: ComponentLoopPlan)
|
|
|
49
49
|
nestedComps,
|
|
50
50
|
childConditionalEffects,
|
|
51
51
|
profileLoopId,
|
|
52
|
+
mapPreambleWrapped,
|
|
52
53
|
} = plan
|
|
53
54
|
|
|
54
55
|
const loopBfId = profileLoopId ? `, ${JSON.stringify(profileLoopId)}` : ''
|
|
55
56
|
lines.push(` mapArray(() => ${arrayExpr}, ${containerVar}, ${keyFn}, (${paramHead}, ${indexParam}, __existing) => {`)
|
|
56
57
|
if (paramUnwrap) lines.push(` ${paramUnwrap}`)
|
|
58
|
+
// The preamble's consts are referenced by componentPropsExpr's getters
|
|
59
|
+
// below, in BOTH the `initChild` (hydration-reuse) and `createComponent`
|
|
60
|
+
// (fresh row) branches — a row rebuild and a hydration-reused row must
|
|
61
|
+
// read the same preamble-computed value, so it can't be hoisted into only
|
|
62
|
+
// one branch.
|
|
63
|
+
if (mapPreambleWrapped) lines.push(` ${mapPreambleWrapped}`)
|
|
57
64
|
|
|
58
65
|
const scopedComp = nameForRegistryRef(componentName)
|
|
59
66
|
|
|
@@ -421,7 +421,12 @@ export function stringifyStaticLoop(lines: string[], plan: StaticLoopPlan): void
|
|
|
421
421
|
// insert them in order. `__iterEl` is the first root (the one reactive
|
|
422
422
|
// bindings attach to); the rest land alongside it via insertBefore.
|
|
423
423
|
lines.push(` const __mtpl = document.createElement('template')`)
|
|
424
|
-
|
|
424
|
+
// Materialize runs during init, after the parent's own template
|
|
425
|
+
// evaluation (and its ambient parent-scope id) has already unwound —
|
|
426
|
+
// `withParentScope` re-establishes it just for this row's HTML so
|
|
427
|
+
// `renderChild` inside it stamps `bf-h` with the same `__scopeId` the
|
|
428
|
+
// static init's own selector interpolates (#2833).
|
|
429
|
+
lines.push(` __mtpl.innerHTML = withParentScope(__scopeId, () => \`${itemHtml}\`)`)
|
|
425
430
|
lines.push(` const __anchor = ${containerVar}.children[${childIndexExpr}] ?? null`)
|
|
426
431
|
lines.push(` let __first = null`)
|
|
427
432
|
lines.push(` let __sib = __mtpl.content${childPath}.firstElementChild`)
|
|
@@ -435,7 +440,8 @@ export function stringifyStaticLoop(lines: string[], plan: StaticLoopPlan): void
|
|
|
435
440
|
lines.push(` __iterEl = __first`)
|
|
436
441
|
} else {
|
|
437
442
|
lines.push(` const __tpl = document.createElement('template')`)
|
|
438
|
-
|
|
443
|
+
// See the multi-root branch's comment above (#2833).
|
|
444
|
+
lines.push(` __tpl.innerHTML = withParentScope(__scopeId, () => \`${itemHtml}\`)`)
|
|
439
445
|
lines.push(` const __cloned = __tpl.content${childPath}`)
|
|
440
446
|
lines.push(` if (__cloned) {`)
|
|
441
447
|
lines.push(` const __anchor = ${containerVar}.children[${childIndexExpr}] ?? null`)
|
|
@@ -86,6 +86,18 @@ export function emitLoopUpdates(lines: string[], ctx: ClientJsContext, unsafeLoc
|
|
|
86
86
|
!(elem.preamble && elem.preamble.builderNames.length > 0 && plan.rowConstruction === 'dom-ops'),
|
|
87
87
|
`loop variant '${plan.kind}' declares dom-ops row construction but received a JSX-bearing preamble — add a Phase-1 refusal (or wire renderPreamble support) for this shape`,
|
|
88
88
|
)
|
|
89
|
+
// #2797's hole, generalized: a JS-only preamble (no JSX leaf, so the
|
|
90
|
+
// check above doesn't fire) can still be silently dropped if a variant
|
|
91
|
+
// just never reads `elem.preamble` at all — which is exactly what
|
|
92
|
+
// happened to the 'component' variant before it grew
|
|
93
|
+
// `mapPreambleWrapped`. A variant that doesn't declare that field can't
|
|
94
|
+
// be checked here (nothing to read), so this only catches a variant
|
|
95
|
+
// that declares it but leaves it unpopulated for a preamble that has
|
|
96
|
+
// names to declare.
|
|
97
|
+
internalInvariant(
|
|
98
|
+
!(elem.preamble && elem.preamble.declaredNames.length > 0 && 'mapPreambleWrapped' in plan && plan.mapPreambleWrapped === ''),
|
|
99
|
+
`loop variant '${plan.kind}' has a preamble with declared names (${elem.preamble?.declaredNames.join(', ')}) but its plan's mapPreambleWrapped is empty — wire it up or the emitted call site references a name nothing declares`,
|
|
100
|
+
)
|
|
89
101
|
stringifyLoop(lines, plan)
|
|
90
102
|
emitLoopEventDelegation(lines, elem, plan.kind, ctx.profile ? ctx.componentName : undefined)
|
|
91
103
|
}
|
|
@@ -25,10 +25,11 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import ts from 'typescript'
|
|
28
|
-
import {
|
|
28
|
+
import { inferDefaultValue } from './utils.ts'
|
|
29
29
|
import { extractFreeIdentifiersFromNode } from '../analyzer.ts'
|
|
30
|
-
import type { MemoInfo, SignalInfo } from '../types.ts'
|
|
30
|
+
import type { ConstantInfo, MemoInfo, SignalInfo } from '../types.ts'
|
|
31
31
|
import type { BindingScope } from '../scope/binding-scope.ts'
|
|
32
|
+
import { resolveAliasOrigin } from '../props-binding.ts'
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* CSR-substituted const value: the const's initializer with every
|
|
@@ -321,23 +322,34 @@ function csrSubstituteOnce(
|
|
|
321
322
|
// lets `isInlinableInTemplate` see bridged prop references and
|
|
322
323
|
// reject calls like `useYjs(props.X)` from the inline path (#1138).
|
|
323
324
|
// Callers that want the emit-form (`_p.X`) apply the rewrite via
|
|
324
|
-
// `
|
|
325
|
+
// `rewritePropsObjectRef` (rewrite-props-object.ts) after the
|
|
326
|
+
// inline-safety check — an AST walk, not a regex, so it survives a
|
|
327
|
+
// parenthesised receiver (`(props).label`, produced when `props`
|
|
328
|
+
// itself was just constant-inlined under a local alias) and skips
|
|
329
|
+
// non-value positions a regex can't distinguish (#2737).
|
|
325
330
|
|
|
326
331
|
// Compute free identifiers of the rewritten form by re-parsing. Avoids
|
|
327
332
|
// having to track analytically through nested substitutions, which is
|
|
328
333
|
// exactly the bookkeeping that made the legacy code drift from emission.
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
334
|
+
//
|
|
335
|
+
// `extractFreeIdentifiersFromText` only sees `rewritten` in isolation —
|
|
336
|
+
// it has no way to know about `enclosingScope`, the loop-row bindings
|
|
337
|
+
// introduced OUTSIDE this expression's own AST (`isBound`'s second half,
|
|
338
|
+
// above). Without filtering by it too, a name legitimately shadowed by
|
|
339
|
+
// the enclosing loop row (never substituted, since `isBound` already
|
|
340
|
+
// skipped it during `visit`) still comes back as "free" here and trips
|
|
341
|
+
// the caller's `unsafeLocalNames` check — the exact same identifier the
|
|
342
|
+
// substitution step correctly left alone gets the UNSAFE sentinel
|
|
343
|
+
// anyway. Filtering by the same `enclosingScope.isBound` predicate keeps
|
|
344
|
+
// the two checks in agreement, matching this function's own docstring
|
|
345
|
+
// contract above (#2814 review).
|
|
346
|
+
const freeIdentifiers = extractFreeIdentifiersFromText(rewritten)
|
|
347
|
+
if (enclosingScope) {
|
|
348
|
+
for (const name of freeIdentifiers) {
|
|
349
|
+
if (enclosingScope.isBound(name)) freeIdentifiers.delete(name)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return { rewritten, freeIdentifiers }
|
|
341
353
|
}
|
|
342
354
|
|
|
343
355
|
export function extractFreeIdentifiersFromText(text: string): Set<string> {
|
|
@@ -430,6 +442,50 @@ export function extractMemoBodyExpr(computation: string): string {
|
|
|
430
442
|
return `(() => ${body})()`
|
|
431
443
|
}
|
|
432
444
|
|
|
445
|
+
/**
|
|
446
|
+
* Which local consts are bare alias-hop chains that ultimately name a
|
|
447
|
+
* substitutable signal/memo getter (`isGetter`) — e.g. `const
|
|
448
|
+
* items__alias = items` where `items` is a signal getter. Returns
|
|
449
|
+
* alias name → origin getter name (the alias itself is never a key of
|
|
450
|
+
* its own map, so `origin !== alias` always holds for entries returned).
|
|
451
|
+
*
|
|
452
|
+
* A bare identifier alias of a getter is NOT an "inlinable const" in the
|
|
453
|
+
* ordinary sense (#2778): `populateCsrInlinable`'s generic path resolves
|
|
454
|
+
* a const's initializer through `csrSubstitute`/relocate, and `items`
|
|
455
|
+
* used bare (not called) is a `signal-getter` there, which relocate can
|
|
456
|
+
* only answer with its own `undefined` FALLBACK (correct when `items`
|
|
457
|
+
* itself appears bare, wrong once frozen as `items__alias`'s permanent
|
|
458
|
+
* value — a wrong substitution, not a missing one, so the #2468 scope
|
|
459
|
+
* gate can't see it either since `undefined` is a real in-scope global).
|
|
460
|
+
* Resolving the alias to its origin BEFORE that generic path runs, and
|
|
461
|
+
* registering it as the origin's own call-kind entry, means
|
|
462
|
+
* `items__alias()` is substituted by the exact mechanism that already
|
|
463
|
+
* substitutes `items()` correctly — one call-kind entry, not a second
|
|
464
|
+
* "identifier that means undefined-unless-called" special case.
|
|
465
|
+
*
|
|
466
|
+
* Reuses `resolveAliasOrigin` (`props-binding.ts`) — the same hop-walker
|
|
467
|
+
* `resolveRestSpreadOriginCore` uses for the props/rest alias question —
|
|
468
|
+
* rather than a second alias-chain walker for this different terminal
|
|
469
|
+
* set (a signal/memo NAME here, `restPropsName`/`propsObjectName` there).
|
|
470
|
+
*/
|
|
471
|
+
export function resolveGetterAliases(
|
|
472
|
+
localConstants: readonly ConstantInfo[],
|
|
473
|
+
isGetter: (name: string) => boolean,
|
|
474
|
+
): Map<string, string> {
|
|
475
|
+
const constantValues = new Map<string, string | undefined>()
|
|
476
|
+
for (const c of localConstants) {
|
|
477
|
+
if (c.isModule) continue
|
|
478
|
+
constantValues.set(c.name, c.value)
|
|
479
|
+
}
|
|
480
|
+
const aliases = new Map<string, string>()
|
|
481
|
+
for (const c of localConstants) {
|
|
482
|
+
if (c.isModule || isGetter(c.name)) continue
|
|
483
|
+
const origin = resolveAliasOrigin(constantValues, c.name, (current) => (isGetter(current) ? current : null))
|
|
484
|
+
if (origin !== null && origin !== c.name) aliases.set(c.name, origin)
|
|
485
|
+
}
|
|
486
|
+
return aliases
|
|
487
|
+
}
|
|
488
|
+
|
|
433
489
|
/**
|
|
434
490
|
* Build the CSR substitution env from the live `ClientJsContext`.
|
|
435
491
|
*
|
|
@@ -442,11 +498,18 @@ export function extractMemoBodyExpr(computation: string): string {
|
|
|
442
498
|
* in `compute-inlinability.ts`). Inlinable-const substitutions are
|
|
443
499
|
* layered into a copy of this env at `generateCsrTemplate`'s entry,
|
|
444
500
|
* reading from `ClientJsContext.csrInlinable`.
|
|
501
|
+
*
|
|
502
|
+
* `localConstants` is consulted ONLY to resolve bare getter aliases
|
|
503
|
+
* (`resolveGetterAliases`, #2778) — a name that is itself a signal/memo
|
|
504
|
+
* getter always wins the `substitutions.has` check there, so `signals`/
|
|
505
|
+
* `memos` stay the single source of truth for what a getter name means;
|
|
506
|
+
* this just extends which SOURCE NAMES reach that same meaning.
|
|
445
507
|
*/
|
|
446
508
|
export function buildSignalMemoEnv(
|
|
447
509
|
signals: readonly SignalInfo[],
|
|
448
510
|
memos: readonly MemoInfo[],
|
|
449
511
|
propsObjectName: string | null,
|
|
512
|
+
localConstants: readonly ConstantInfo[] = [],
|
|
450
513
|
): CsrEnv {
|
|
451
514
|
const substitutions = new Map<string, CsrSubstitution>()
|
|
452
515
|
for (const s of signals) {
|
|
@@ -474,6 +537,9 @@ export function buildSignalMemoEnv(
|
|
|
474
537
|
freeIdentifiers: m.computationFreeIdentifiers ?? new Set(),
|
|
475
538
|
})
|
|
476
539
|
}
|
|
540
|
+
for (const [alias, origin] of resolveGetterAliases(localConstants, (n) => substitutions.has(n))) {
|
|
541
|
+
substitutions.set(alias, substitutions.get(origin)!)
|
|
542
|
+
}
|
|
477
543
|
return { substitutions, propsObjectName }
|
|
478
544
|
}
|
|
479
545
|
|
|
@@ -481,7 +547,7 @@ export function buildSignalMemoEnv(
|
|
|
481
547
|
* Add the `?? <default>` SSR fallback to a signal's initial value when
|
|
482
548
|
* the value is a bare `propsName.X` reference. Returns the value in raw
|
|
483
549
|
* (props.X) form — the `propsObjectName → _p` rewrite is deferred to
|
|
484
|
-
* `
|
|
550
|
+
* `rewritePropsObjectRef` at emit time so the post-substitution
|
|
485
551
|
* `isInlinableInTemplate` check still sees bridged-arg shapes (#1138).
|
|
486
552
|
*
|
|
487
553
|
* The `??` fallback prevents literal `undefined` from leaking into the
|