@barefootjs/jsx 0.33.4 → 0.34.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/child-scope.d.ts +13 -0
- package/dist/adapters/child-scope.d.ts.map +1 -1
- package/dist/adapters/parsed-expr-emitter.d.ts +17 -0
- package/dist/adapters/parsed-expr-emitter.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/expression-parser.d.ts +17 -4
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1399 -1173
- 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/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/loop.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/lowering-registry.d.ts +12 -0
- package/dist/lowering-registry.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 +6 -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__/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__/lowering-value-position.test.ts +232 -0
- package/src/__tests__/markup-prop-brand.test.ts +165 -26
- package/src/__tests__/namespace-import-primitive.test.ts +244 -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/adapters/parsed-expr-emitter.ts +43 -0
- package/src/analyzer.ts +130 -0
- package/src/errors.ts +14 -1
- package/src/expression-parser.ts +63 -29
- package/src/index.ts +9 -1
- 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/shared.ts +12 -5
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +8 -2
- 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 +141 -70
- 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 +200 -42
- package/src/lowering-registry.ts +21 -0
- package/src/prop-rewrite.ts +23 -5
- package/src/props-binding.ts +74 -18
- package/src/types.ts +6 -5
|
@@ -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) {
|
|
@@ -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 {
|
|
@@ -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`)
|
|
@@ -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
|
|
@@ -15,6 +15,7 @@ import { datePlugin, DATE_METHODS } from '../date-lowering.ts'
|
|
|
15
15
|
import { toLocaleDatePlugin, foldedArgToClientJs } from '../to-locale-date-lowering.ts'
|
|
16
16
|
import { tsNodeToParsedExpr } from '../expression-parser.ts'
|
|
17
17
|
import type { LoweringMatcher } from '../lowering-registry.ts'
|
|
18
|
+
import { rewriteBarePropRefs } from '../prop-rewrite.ts'
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Profile mode (#1690, SR3/SR4): the id appended to a DOM-binding effect so the
|
|
@@ -127,28 +128,43 @@ export function emitAttrUpdate(target: string, attrName: string, expression: str
|
|
|
127
128
|
/**
|
|
128
129
|
* Rewrite destructured prop names in an expression to `(props.xxx ?? default)`.
|
|
129
130
|
* Only applies when the component uses destructured props (not props.xxx style).
|
|
131
|
+
*
|
|
132
|
+
* Delegates the actual walk to `rewriteBarePropRefs` (`prop-rewrite.ts`) — the
|
|
133
|
+
* one AST walk in the compiler that already distinguishes a VALUE reference
|
|
134
|
+
* from an object-literal key, a property-access name, a shorthand property,
|
|
135
|
+
* or a name shadowed by an inner binding (`items.map((tag) => tag.x)`).
|
|
136
|
+
* This function used to run its own `\btag\b`-style regex over the raw
|
|
137
|
+
* expression text with no such distinction, so `queryHref(base, { tag: tag })`
|
|
138
|
+
* rewrote the KEY too (`{ _p.tag: _p.tag }`, a syntax error) — #2741. Only
|
|
139
|
+
* the default-value `(_p.x ?? <default>)` wrapping is specific to this call
|
|
140
|
+
* site, supplied via `replacementFor` rather than a second copy of the walk.
|
|
130
141
|
*/
|
|
131
142
|
export function rewriteDestructuredPropsInExpr(expr: string, ctx: ClientJsContext): string {
|
|
132
143
|
if (ctx.propsObjectName) return expr
|
|
133
144
|
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
145
|
+
const propNames = new Set<string>()
|
|
146
|
+
const propAliases = new Map<string, string>()
|
|
147
|
+
const propsByName = new Map<string, ClientJsContext['propsParams'][number]>()
|
|
137
148
|
for (const prop of ctx.propsParams) {
|
|
138
149
|
if (prop.name === 'children') continue
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
150
|
+
propNames.add(prop.name)
|
|
151
|
+
propsByName.set(prop.name, prop)
|
|
142
152
|
// `_p` is always keyed by the caller-facing name (#2524 CSR half).
|
|
143
|
-
|
|
144
|
-
const defaultVal = prop.defaultValue
|
|
145
|
-
const replacement = defaultVal
|
|
146
|
-
? `(${PROPS_PARAM}.${callerKey} ?? ${prop.defaultContainsArrow ? `(${defaultVal})` : defaultVal})`
|
|
147
|
-
: `${PROPS_PARAM}.${callerKey}`
|
|
148
|
-
result = result.replace(new RegExp(`(?<![-.])\\b${prop.name}\\b`, 'g'), replacement)
|
|
153
|
+
if (prop.sourceName && prop.sourceName !== prop.name) propAliases.set(prop.name, prop.sourceName)
|
|
149
154
|
}
|
|
155
|
+
if (propNames.size === 0) return expr
|
|
150
156
|
|
|
151
|
-
|
|
157
|
+
const replacementFor = (localName: string, callerKey: string): string => {
|
|
158
|
+
const defaultVal = propsByName.get(localName)?.defaultValue
|
|
159
|
+
if (!defaultVal) return `${PROPS_PARAM}.${callerKey}`
|
|
160
|
+
const defaultContainsArrow = propsByName.get(localName)?.defaultContainsArrow
|
|
161
|
+
return `(${PROPS_PARAM}.${callerKey} ?? ${defaultContainsArrow ? `(${defaultVal})` : defaultVal})`
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Parenthesized so an object-literal or arrow-function expression parses
|
|
165
|
+
// standalone — same convention `applyScopedPropRefRewrite` itself uses.
|
|
166
|
+
const sourceFile = ts.createSourceFile('__bf_reactive_expr.ts', `(${expr}\n)`, ts.ScriptTarget.Latest, true)
|
|
167
|
+
return rewriteBarePropRefs(expr, sourceFile, propNames, undefined, propAliases, replacementFor) ?? expr
|
|
152
168
|
}
|
|
153
169
|
|
|
154
170
|
/**
|
|
@@ -223,7 +223,7 @@ export function emitRegistrationAndHydration(
|
|
|
223
223
|
// (#2651) — see `generateCsrTemplate`'s identical derivation below
|
|
224
224
|
// for why this is reused as-is rather than re-derived.
|
|
225
225
|
const markupSlotIds = new Set(ctx.dynamicElements.map(e => e.slotId))
|
|
226
|
-
const templateHtml = irToComponentTemplate(_ir.root, inlinableConstants, restSpreadNames, ctx.propsObjectName, markupSlotIds)
|
|
226
|
+
const templateHtml = irToComponentTemplate(_ir.root, inlinableConstants, restSpreadNames, ctx.propsObjectName, markupSlotIds, ctx.restPropsName)
|
|
227
227
|
if (templateHtml) {
|
|
228
228
|
defParts.push(buildTemplateDefPart(ctx, templateHtml))
|
|
229
229
|
}
|