@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
package/src/analyzer.ts
CHANGED
|
@@ -3058,9 +3058,25 @@ function collectConstant(
|
|
|
3058
3058
|
const value = defaultValueExpr ? `${baseValue} ?? ${defaultValueExpr}` : baseValue
|
|
3059
3059
|
const containsArrow = el.initializer ? nodeContainsArrow(el.initializer) : false
|
|
3060
3060
|
const freeIdentifiers = el.initializer ? extractFreeIdentifiersFromNode(el.initializer) : new Set([propsName])
|
|
3061
|
+
// Structured parse of `value`, same as the plain-identifier branch
|
|
3062
|
+
// below (#2724 review finding): without this, a RENAMED destructure
|
|
3063
|
+
// (`const { items: rows } = props`) left `parsed` undefined forever,
|
|
3064
|
+
// so `isArrayExprDirectPropRef`'s member-access recognition (which
|
|
3065
|
+
// reads `.parsed`, not `.name`) never saw it — `rows` stayed
|
|
3066
|
+
// classified `isStaticArray: true` even though it's `props.items`
|
|
3067
|
+
// itself. Since #2835, the name-only branch reads `boundPropNames`
|
|
3068
|
+
// (empty for a whole-`props`-param component) rather than the
|
|
3069
|
+
// type-member-name set, so this `.parsed` is the only path for the
|
|
3070
|
+
// un-renamed case too, not just the rename case. `parseExpression`
|
|
3071
|
+
// naturally keeps a defaulted destructure (`value` ends in
|
|
3072
|
+
// `?? <default>`) OUT of the
|
|
3073
|
+
// `kind: 'member'` shape (it parses as a logical-or instead), which
|
|
3074
|
+
// is what keeps that case correctly static per #2806.
|
|
3075
|
+
const parsed = parseExpression(`(${value.trim()})`)
|
|
3061
3076
|
ctx.localConstants.push({
|
|
3062
3077
|
name: localName,
|
|
3063
3078
|
value,
|
|
3079
|
+
parsed,
|
|
3064
3080
|
declarationKind,
|
|
3065
3081
|
isExported,
|
|
3066
3082
|
type: null,
|
|
@@ -3889,6 +3905,11 @@ function validateContext(ctx: AnalyzerContext): void {
|
|
|
3889
3905
|
// failures before factory inlining landed (#931).
|
|
3890
3906
|
validateReactiveFactoryCalls(ctx)
|
|
3891
3907
|
|
|
3908
|
+
// BF013: flag a reactive primitive invoked through a namespace import
|
|
3909
|
+
// that the analyzer could not resolve (#2771) — same "silently dropped,
|
|
3910
|
+
// ReferenceError at hydrate" failure shape as BF011.
|
|
3911
|
+
validateNamespaceQualifiedPrimitives(ctx)
|
|
3912
|
+
|
|
3892
3913
|
// BF003: a `"use client"` file may not import a component from a
|
|
3893
3914
|
// non-`"use client"` source file (#1501). Server-side knowledge must
|
|
3894
3915
|
// not transitively cross into the client bundle, and hydration-marker
|
|
@@ -5864,6 +5885,115 @@ export function validateReactiveFactoryCalls(ctx: AnalyzerContext): void {
|
|
|
5864
5885
|
}
|
|
5865
5886
|
}
|
|
5866
5887
|
|
|
5888
|
+
/**
|
|
5889
|
+
* BF013 (#2771): a reactive primitive called through a namespace import
|
|
5890
|
+
* (`import * as bf from '@barefootjs/client'`, `bf.createSignal(...)`)
|
|
5891
|
+
* that `resolvePrimitiveKind`'s checker-based slow path could not resolve
|
|
5892
|
+
* — no shared `ts.Program` was supplied for this compile. Fast-path
|
|
5893
|
+
* recognition only matches a bare identifier callee, so the call is
|
|
5894
|
+
* silently dropped from `ctx.signals`/`ctx.memos`/etc. while every
|
|
5895
|
+
* reference to its result survives in the emitted output, throwing
|
|
5896
|
+
* ReferenceError at hydrate.
|
|
5897
|
+
*
|
|
5898
|
+
* Gated on non-recognition, not on the namespace-import shape itself:
|
|
5899
|
+
* when a checker IS available (vite always supplies one via
|
|
5900
|
+
* `corpus-program.ts`), `resolvePrimitiveKind` already resolves this
|
|
5901
|
+
* shape soundly end-to-end, and this pass must not flag what the
|
|
5902
|
+
* checker-aided path already handles correctly.
|
|
5903
|
+
*
|
|
5904
|
+
* Scoped to the component's own top-level statements, mirroring
|
|
5905
|
+
* `validateReactiveFactoryCalls` above — a namespace-qualified call
|
|
5906
|
+
* inside a handler/effect body is emitted verbatim and runs fine at
|
|
5907
|
+
* hydrate (the namespace import itself is re-emitted, #2767 follow-up),
|
|
5908
|
+
* so walking into nested bodies would produce false refusals.
|
|
5909
|
+
*/
|
|
5910
|
+
function validateNamespaceQualifiedPrimitives(ctx: AnalyzerContext): void {
|
|
5911
|
+
if (!ctx.componentNode) return
|
|
5912
|
+
const body = ts.isFunctionDeclaration(ctx.componentNode)
|
|
5913
|
+
? ctx.componentNode.body
|
|
5914
|
+
: (ts.isBlock(ctx.componentNode.body) ? ctx.componentNode.body : null)
|
|
5915
|
+
if (!body) return
|
|
5916
|
+
|
|
5917
|
+
for (const stmt of body.statements) {
|
|
5918
|
+
const calls: ts.CallExpression[] = []
|
|
5919
|
+
if (ts.isVariableStatement(stmt)) {
|
|
5920
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
5921
|
+
let init = decl.initializer
|
|
5922
|
+
// `const [n] = bf.createSignal(0)[0]` — vanishingly rare, but the
|
|
5923
|
+
// element-access wrapper must not hide the call underneath it.
|
|
5924
|
+
if (init && ts.isElementAccessExpression(init)) init = init.expression
|
|
5925
|
+
if (init && ts.isCallExpression(init)) calls.push(init)
|
|
5926
|
+
}
|
|
5927
|
+
} else if (ts.isExpressionStatement(stmt) && ts.isCallExpression(stmt.expression)) {
|
|
5928
|
+
calls.push(stmt.expression)
|
|
5929
|
+
}
|
|
5930
|
+
|
|
5931
|
+
for (const call of calls) {
|
|
5932
|
+
const callee = call.expression
|
|
5933
|
+
if (!ts.isPropertyAccessExpression(callee) || !ts.isIdentifier(callee.expression)) continue
|
|
5934
|
+
const primitive = callee.name.text
|
|
5935
|
+
if (!(primitive in PRIMITIVE_CANONICAL_NAMES)) continue
|
|
5936
|
+
if (resolvePrimitiveKind(call, ctx) !== null) continue // checker resolved it — sound, no diagnostic
|
|
5937
|
+
const ns = callee.expression.text
|
|
5938
|
+
if (!isClientNamespaceImportName(ns, ctx)) continue // import identity, not the bare name
|
|
5939
|
+
if (isNamespaceNameShadowedAtComponentTopLevel(ns, body, ctx)) continue
|
|
5940
|
+
|
|
5941
|
+
const loc = getSourceLocation(stmt, ctx.sourceFile, ctx.filePath)
|
|
5942
|
+
ctx.errors.push(
|
|
5943
|
+
createError(ErrorCodes.PRIMITIVE_VIA_NAMESPACE_IMPORT, loc, {
|
|
5944
|
+
severity: 'error',
|
|
5945
|
+
message:
|
|
5946
|
+
`'${ns}.${primitive}(...)' calls a @barefootjs/client reactive primitive through the namespace ` +
|
|
5947
|
+
`import 'import * as ${ns}' and was not recognized by the analyzer. The declaration is dropped ` +
|
|
5948
|
+
`from the compiled output and every reference to it throws ReferenceError at hydrate.`,
|
|
5949
|
+
suggestion: {
|
|
5950
|
+
message:
|
|
5951
|
+
`Import the primitive by name: import { ${primitive} } from '@barefootjs/client' and call ` +
|
|
5952
|
+
`${primitive}(...) directly. (A shared ts.Program passed via CompileOptions.program lets the ` +
|
|
5953
|
+
`analyzer resolve the namespace through the TypeChecker; without one the named form is required.)`,
|
|
5954
|
+
escape: [{ kind: 'rewrite' }],
|
|
5955
|
+
},
|
|
5956
|
+
})
|
|
5957
|
+
)
|
|
5958
|
+
}
|
|
5959
|
+
}
|
|
5960
|
+
}
|
|
5961
|
+
|
|
5962
|
+
function isClientNamespaceImportName(name: string, ctx: AnalyzerContext): boolean {
|
|
5963
|
+
return ctx.imports.some(
|
|
5964
|
+
imp =>
|
|
5965
|
+
imp.source === '@barefootjs/client' &&
|
|
5966
|
+
!imp.isTypeOnly &&
|
|
5967
|
+
imp.specifiers.some(s => s.isNamespace && s.name === name)
|
|
5968
|
+
)
|
|
5969
|
+
}
|
|
5970
|
+
|
|
5971
|
+
/**
|
|
5972
|
+
* A component-top-level binding that shadows the namespace import name
|
|
5973
|
+
* (a local `const bf = ...`/function `bf`, or a prop param/props-object
|
|
5974
|
+
* named `bf`) means `bf.createSignal` in THIS component isn't the client
|
|
5975
|
+
* namespace at all — refusing would be a false positive. Does not model a
|
|
5976
|
+
* shadow introduced in a nested block above the call site (same accepted
|
|
5977
|
+
* posture as the checker-aided fast path).
|
|
5978
|
+
*/
|
|
5979
|
+
function isNamespaceNameShadowedAtComponentTopLevel(
|
|
5980
|
+
name: string,
|
|
5981
|
+
body: ts.Block,
|
|
5982
|
+
ctx: AnalyzerContext
|
|
5983
|
+
): boolean {
|
|
5984
|
+
if (ctx.propsObjectName === name) return true
|
|
5985
|
+
if (ctx.propsParams.some(p => p.name === name)) return true
|
|
5986
|
+
for (const stmt of body.statements) {
|
|
5987
|
+
if (ts.isFunctionDeclaration(stmt) && stmt.name?.text === name) return true
|
|
5988
|
+
if (ts.isVariableStatement(stmt)) {
|
|
5989
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
5990
|
+
if (ts.isIdentifier(decl.name) && decl.name.text === name) return true
|
|
5991
|
+
}
|
|
5992
|
+
}
|
|
5993
|
+
}
|
|
5994
|
+
return false
|
|
5995
|
+
}
|
|
5996
|
+
|
|
5867
5997
|
/**
|
|
5868
5998
|
* Object-destructure half of `validateReactiveFactoryCalls` (#2325). Split
|
|
5869
5999
|
* out so the tuple path above stays a straight read of the pre-#2325 logic
|
package/src/errors.ts
CHANGED
|
@@ -19,8 +19,18 @@ export const ErrorCodes = {
|
|
|
19
19
|
MISSING_USE_CLIENT: 'BF001',
|
|
20
20
|
CLIENT_IMPORTING_SERVER: 'BF003',
|
|
21
21
|
|
|
22
|
-
// Signal/Memo errors (BF011-BF019)
|
|
22
|
+
// Signal/Memo errors (BF011-BF019). BF012 is retired (see
|
|
23
|
+
// `invalid-signal-usage.audit.test.ts`) — BF013 is the next free slot.
|
|
23
24
|
SIGNAL_OUTSIDE_COMPONENT: 'BF011',
|
|
25
|
+
// A reactive primitive (createSignal/createMemo/createEffect/onMount/
|
|
26
|
+
// onCleanup/createSearchParams) called through a namespace import
|
|
27
|
+
// (`import * as bf from '@barefootjs/client'`, `bf.createSignal(...)`)
|
|
28
|
+
// that the analyzer could not resolve — no shared `ts.Program` was
|
|
29
|
+
// supplied, so `resolvePrimitiveKind`'s checker-based slow path never
|
|
30
|
+
// ran. Same failure shape as BF011: the declaration is silently dropped
|
|
31
|
+
// from the compiled output and every reference to it throws
|
|
32
|
+
// ReferenceError at hydrate. #2771.
|
|
33
|
+
PRIMITIVE_VIA_NAMESPACE_IMPORT: 'BF013',
|
|
24
34
|
|
|
25
35
|
// JSX errors (BF021-BF029). BF022 was retired (see
|
|
26
36
|
// `invalid-jsx-attribute.audit.test.ts`) and BF026 is reserved by
|
|
@@ -150,6 +160,9 @@ const errorMessages: Record<ErrorCode, string> = {
|
|
|
150
160
|
'Module-level reactive declaration (createSignal / createMemo) is not allowed. ' +
|
|
151
161
|
'The downstream codegen drops the declaration silently and every reference becomes a ReferenceError at SSR and at hydrate. ' +
|
|
152
162
|
'Move the declaration inside a component function so each mount gets its own state.',
|
|
163
|
+
[ErrorCodes.PRIMITIVE_VIA_NAMESPACE_IMPORT]:
|
|
164
|
+
"Reactive primitive called through a namespace import of '@barefootjs/client' (import * as ns) is not recognized; " +
|
|
165
|
+
'the declaration is dropped and its references throw ReferenceError at hydrate. Import the primitive by name instead.',
|
|
153
166
|
|
|
154
167
|
[ErrorCodes.UNSUPPORTED_JSX_PATTERN]: 'Unsupported JSX pattern',
|
|
155
168
|
[ErrorCodes.MISSING_KEY_IN_LIST]:
|
package/src/expression-parser.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import ts from 'typescript'
|
|
10
|
+
import { loweringNodeChildren, type LoweringMatcher } from './lowering-registry.ts'
|
|
10
11
|
|
|
11
12
|
// =============================================================================
|
|
12
13
|
// Parsed Expression Types
|
|
@@ -1049,7 +1050,7 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
1049
1050
|
// surface — the depth must be *expressible*, even if not
|
|
1050
1051
|
// known until render time.
|
|
1051
1052
|
const parsedDepth = convertNode(depthNode, raw)
|
|
1052
|
-
if (checkSupport(parsedDepth, 'rendered').supported) {
|
|
1053
|
+
if (checkSupport(parsedDepth, 'rendered', []).supported) {
|
|
1053
1054
|
depthExpr = parsedDepth
|
|
1054
1055
|
flatDepth = 1 // unused placeholder — consumers must check `depthExpr` first
|
|
1055
1056
|
} else {
|
|
@@ -2419,9 +2420,20 @@ function getUnaryOperatorString(op: ts.PrefixUnaryOperator): string {
|
|
|
2419
2420
|
* reaches the page. See {@link isSupportedValue} for the sibling VALUE-
|
|
2420
2421
|
* position entry point, and `checkSupport`'s `pos` parameter for why the two
|
|
2421
2422
|
* never converge partway through a tree.
|
|
2423
|
+
*
|
|
2424
|
+
* `opts.loweringMatchers` (#2843), when given, lets a call recognised by a
|
|
2425
|
+
* registered lowering plugin (`queryHref`, or any userland plugin) admit
|
|
2426
|
+
* its params regardless of nesting position — a ternary branch, a
|
|
2427
|
+
* template-literal interpolation, a binary operand, … — matching the
|
|
2428
|
+
* top-level attribute path, which already consulted the same registry
|
|
2429
|
+
* before this gate ever ran. Omit it (every existing caller does) for
|
|
2430
|
+
* byte-identical prior behaviour.
|
|
2422
2431
|
*/
|
|
2423
|
-
export function isSupported(
|
|
2424
|
-
|
|
2432
|
+
export function isSupported(
|
|
2433
|
+
expr: ParsedExpr,
|
|
2434
|
+
opts?: { loweringMatchers?: readonly LoweringMatcher[] },
|
|
2435
|
+
): SupportResult {
|
|
2436
|
+
return checkSupport(expr, 'rendered', opts?.loweringMatchers ?? [])
|
|
2425
2437
|
}
|
|
2426
2438
|
|
|
2427
2439
|
/**
|
|
@@ -2430,10 +2442,13 @@ export function isSupported(expr: ParsedExpr): SupportResult {
|
|
|
2430
2442
|
* assignment, never a render). The one behavioural difference from
|
|
2431
2443
|
* {@link isSupported}: an `object-literal` anywhere in the tree is admitted
|
|
2432
2444
|
* when every property value is itself supported, instead of being refused
|
|
2433
|
-
* outright.
|
|
2445
|
+
* outright. See {@link isSupported}'s doc for `opts.loweringMatchers`.
|
|
2434
2446
|
*/
|
|
2435
|
-
export function isSupportedValue(
|
|
2436
|
-
|
|
2447
|
+
export function isSupportedValue(
|
|
2448
|
+
expr: ParsedExpr,
|
|
2449
|
+
opts?: { loweringMatchers?: readonly LoweringMatcher[] },
|
|
2450
|
+
): SupportResult {
|
|
2451
|
+
return checkSupport(expr, 'value', opts?.loweringMatchers ?? [])
|
|
2437
2452
|
}
|
|
2438
2453
|
|
|
2439
2454
|
/**
|
|
@@ -2453,7 +2468,7 @@ export function isSupportedValue(expr: ParsedExpr): SupportResult {
|
|
|
2453
2468
|
*/
|
|
2454
2469
|
type ExprPos = 'rendered' | 'value'
|
|
2455
2470
|
|
|
2456
|
-
function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
2471
|
+
function checkSupport(expr: ParsedExpr, pos: ExprPos, matchers: readonly LoweringMatcher[]): SupportResult {
|
|
2457
2472
|
switch (expr.kind) {
|
|
2458
2473
|
case 'unsupported':
|
|
2459
2474
|
return { supported: false, reason: expr.reason }
|
|
@@ -2471,7 +2486,7 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2471
2486
|
// A spread entry's SOURCE must itself be supported at `value`
|
|
2472
2487
|
// position — the same criterion as an explicit property's value,
|
|
2473
2488
|
// since the spread source is read (not rendered) at merge time.
|
|
2474
|
-
const propSupport = checkSupport(prop.kind === 'spread' ? prop.expr : prop.value, pos)
|
|
2489
|
+
const propSupport = checkSupport(prop.kind === 'spread' ? prop.expr : prop.value, pos, matchers)
|
|
2475
2490
|
if (!propSupport.supported) return propSupport
|
|
2476
2491
|
}
|
|
2477
2492
|
return { supported: true, level: 'L2' }
|
|
@@ -2498,7 +2513,7 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2498
2513
|
// method, not here, because we can't see which adapter is consuming
|
|
2499
2514
|
// the IR at this point.
|
|
2500
2515
|
for (const el of expr.elements) {
|
|
2501
|
-
const elSupport = checkSupport(el, pos)
|
|
2516
|
+
const elSupport = checkSupport(el, pos, matchers)
|
|
2502
2517
|
if (!elSupport.supported) return elSupport
|
|
2503
2518
|
}
|
|
2504
2519
|
return { supported: true, level: 'L2' }
|
|
@@ -2518,10 +2533,10 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2518
2533
|
}
|
|
2519
2534
|
}
|
|
2520
2535
|
// Receiver/args inherit `pos` (see `ExprPos`'s doc).
|
|
2521
|
-
const objSupport = checkSupport(expr.object, pos)
|
|
2536
|
+
const objSupport = checkSupport(expr.object, pos, matchers)
|
|
2522
2537
|
if (!objSupport.supported) return objSupport
|
|
2523
2538
|
for (const arg of expr.args) {
|
|
2524
|
-
const argSupport = checkSupport(arg, pos)
|
|
2539
|
+
const argSupport = checkSupport(arg, pos, matchers)
|
|
2525
2540
|
if (!argSupport.supported) return argSupport
|
|
2526
2541
|
}
|
|
2527
2542
|
// A dynamic `.flat(depth)` carries its depth expression outside
|
|
@@ -2532,7 +2547,7 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2532
2547
|
// any `array-method`/`flat` node regardless of how it was constructed
|
|
2533
2548
|
// (e.g. by a rewrite walker).
|
|
2534
2549
|
if (expr.method === 'flat' && expr.depthExpr) {
|
|
2535
|
-
const depthSupport = checkSupport(expr.depthExpr, pos)
|
|
2550
|
+
const depthSupport = checkSupport(expr.depthExpr, pos, matchers)
|
|
2536
2551
|
if (!depthSupport.supported) return depthSupport
|
|
2537
2552
|
}
|
|
2538
2553
|
return { supported: true, level: 'L2' }
|
|
@@ -2540,6 +2555,25 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2540
2555
|
|
|
2541
2556
|
|
|
2542
2557
|
case 'call': {
|
|
2558
|
+
// #2843: a call a registered lowering plugin recognises (`queryHref`,
|
|
2559
|
+
// or any userland plugin) is admitted regardless of where it sits in
|
|
2560
|
+
// the tree — the neutral node's OWN children (`loweringNodeChildren`)
|
|
2561
|
+
// are re-checked at the current `pos` instead of the call's raw
|
|
2562
|
+
// `args`, so e.g. `queryHref`'s params object literal (refused at
|
|
2563
|
+
// `rendered` position by the `object-literal` case above) doesn't
|
|
2564
|
+
// reach that refusal at all: the plugin already took responsibility
|
|
2565
|
+
// for it. Tried before `asCallbackMethodCall` and the generic per-arg
|
|
2566
|
+
// loop below, matching the precedence the top-level attribute path
|
|
2567
|
+
// already gives the registry.
|
|
2568
|
+
for (const matcher of matchers) {
|
|
2569
|
+
const node = matcher(expr.callee, expr.args)
|
|
2570
|
+
if (!node) continue
|
|
2571
|
+
for (const child of loweringNodeChildren(node)) {
|
|
2572
|
+
const childSupport = checkSupport(child, pos, matchers)
|
|
2573
|
+
if (!childSupport.supported) return childSupport
|
|
2574
|
+
}
|
|
2575
|
+
return { supported: true, level: 'L2' }
|
|
2576
|
+
}
|
|
2543
2577
|
// Higher-order callback methods (`.filter`/`.sort`/`.reduce`/… with an
|
|
2544
2578
|
// arrow argument) lower via the runtime evaluator (#2018). Supported iff
|
|
2545
2579
|
// the receiver and the callback BODY are supported. Recognised before the
|
|
@@ -2558,9 +2592,9 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2558
2592
|
// one (a signal/memo initializer) admits it.
|
|
2559
2593
|
const cb = asCallbackMethodCall(expr)
|
|
2560
2594
|
if (cb) {
|
|
2561
|
-
const objSupport = checkSupport(cb.object, pos)
|
|
2595
|
+
const objSupport = checkSupport(cb.object, pos, matchers)
|
|
2562
2596
|
if (!objSupport.supported) return objSupport
|
|
2563
|
-
const bodySupport = checkSupport(cb.arrow.body, pos)
|
|
2597
|
+
const bodySupport = checkSupport(cb.arrow.body, pos, matchers)
|
|
2564
2598
|
if (!bodySupport.supported) {
|
|
2565
2599
|
return {
|
|
2566
2600
|
supported: false,
|
|
@@ -2569,7 +2603,7 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2569
2603
|
}
|
|
2570
2604
|
}
|
|
2571
2605
|
for (const rest of cb.args) {
|
|
2572
|
-
const restSupport = checkSupport(rest, pos)
|
|
2606
|
+
const restSupport = checkSupport(rest, pos, matchers)
|
|
2573
2607
|
if (!restSupport.supported) return restSupport
|
|
2574
2608
|
}
|
|
2575
2609
|
return { supported: true, level: 'L5' }
|
|
@@ -2577,7 +2611,7 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2577
2611
|
|
|
2578
2612
|
// Check if callee is supported. Not container contents — inherits
|
|
2579
2613
|
// the CURRENT `pos` (see `ExprPos`'s doc).
|
|
2580
|
-
const calleeSupport = checkSupport(expr.callee, pos)
|
|
2614
|
+
const calleeSupport = checkSupport(expr.callee, pos, matchers)
|
|
2581
2615
|
if (!calleeSupport.supported) {
|
|
2582
2616
|
return calleeSupport
|
|
2583
2617
|
}
|
|
@@ -2610,7 +2644,7 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2610
2644
|
// recognised callback/array-method (that path returned above) —
|
|
2611
2645
|
// inherits the CURRENT `pos` (see `ExprPos`'s doc).
|
|
2612
2646
|
for (const arg of expr.args) {
|
|
2613
|
-
const argSupport = checkSupport(arg, pos)
|
|
2647
|
+
const argSupport = checkSupport(arg, pos, matchers)
|
|
2614
2648
|
if (!argSupport.supported) {
|
|
2615
2649
|
return argSupport
|
|
2616
2650
|
}
|
|
@@ -2620,7 +2654,7 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2620
2654
|
|
|
2621
2655
|
case 'member': {
|
|
2622
2656
|
// Not container contents — inherits the CURRENT `pos`.
|
|
2623
|
-
const objSupport = checkSupport(expr.object, pos)
|
|
2657
|
+
const objSupport = checkSupport(expr.object, pos, matchers)
|
|
2624
2658
|
if (!objSupport.supported) {
|
|
2625
2659
|
return objSupport
|
|
2626
2660
|
}
|
|
@@ -2636,9 +2670,9 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2636
2670
|
// expression are themselves supported (the index is typically a
|
|
2637
2671
|
// loop variable or arithmetic over one). #1897 (data-table). Neither
|
|
2638
2672
|
// is container contents — both inherit the CURRENT `pos`.
|
|
2639
|
-
const objSupport = checkSupport(expr.object, pos)
|
|
2673
|
+
const objSupport = checkSupport(expr.object, pos, matchers)
|
|
2640
2674
|
if (!objSupport.supported) return objSupport
|
|
2641
|
-
const indexSupport = checkSupport(expr.index, pos)
|
|
2675
|
+
const indexSupport = checkSupport(expr.index, pos, matchers)
|
|
2642
2676
|
if (!indexSupport.supported) return indexSupport
|
|
2643
2677
|
return { supported: true, level: 'L2' }
|
|
2644
2678
|
}
|
|
@@ -2646,9 +2680,9 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2646
2680
|
case 'binary': {
|
|
2647
2681
|
// Operands inherit the CURRENT `pos` (see `ExprPos`'s doc) — a binary
|
|
2648
2682
|
// operand isn't container contents.
|
|
2649
|
-
const leftSupport = checkSupport(expr.left, pos)
|
|
2683
|
+
const leftSupport = checkSupport(expr.left, pos, matchers)
|
|
2650
2684
|
if (!leftSupport.supported) return leftSupport
|
|
2651
|
-
const rightSupport = checkSupport(expr.right, pos)
|
|
2685
|
+
const rightSupport = checkSupport(expr.right, pos, matchers)
|
|
2652
2686
|
if (!rightSupport.supported) return rightSupport
|
|
2653
2687
|
|
|
2654
2688
|
// Comparison operators are L3
|
|
@@ -2666,7 +2700,7 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2666
2700
|
|
|
2667
2701
|
case 'unary': {
|
|
2668
2702
|
// Inherits the CURRENT `pos` — not container contents.
|
|
2669
|
-
const argSupport = checkSupport(expr.argument, pos)
|
|
2703
|
+
const argSupport = checkSupport(expr.argument, pos, matchers)
|
|
2670
2704
|
if (!argSupport.supported) return argSupport
|
|
2671
2705
|
|
|
2672
2706
|
// Negation is L4
|
|
@@ -2683,7 +2717,7 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2683
2717
|
|
|
2684
2718
|
case 'logical': {
|
|
2685
2719
|
// Operands inherit the CURRENT `pos` — not container contents.
|
|
2686
|
-
const leftSupport = checkSupport(expr.left, pos)
|
|
2720
|
+
const leftSupport = checkSupport(expr.left, pos, matchers)
|
|
2687
2721
|
if (!leftSupport.supported) return leftSupport
|
|
2688
2722
|
|
|
2689
2723
|
// `x ?? {}` — admit an EMPTY object-literal fallback as the right
|
|
@@ -2710,7 +2744,7 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2710
2744
|
return { supported: true, level: 'L4' }
|
|
2711
2745
|
}
|
|
2712
2746
|
|
|
2713
|
-
const rightSupport = checkSupport(expr.right, pos)
|
|
2747
|
+
const rightSupport = checkSupport(expr.right, pos, matchers)
|
|
2714
2748
|
if (!rightSupport.supported) return rightSupport
|
|
2715
2749
|
|
|
2716
2750
|
return { supported: true, level: 'L4' }
|
|
@@ -2722,11 +2756,11 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2722
2756
|
// position both branches are themselves rendered — `cond ? {a:1} :
|
|
2723
2757
|
// {b:2}` stays refused there, not admitted just for being a
|
|
2724
2758
|
// sub-expression.
|
|
2725
|
-
const testSupport = checkSupport(expr.test, pos)
|
|
2759
|
+
const testSupport = checkSupport(expr.test, pos, matchers)
|
|
2726
2760
|
if (!testSupport.supported) return testSupport
|
|
2727
|
-
const consSupport = checkSupport(expr.consequent, pos)
|
|
2761
|
+
const consSupport = checkSupport(expr.consequent, pos, matchers)
|
|
2728
2762
|
if (!consSupport.supported) return consSupport
|
|
2729
|
-
const altSupport = checkSupport(expr.alternate, pos)
|
|
2763
|
+
const altSupport = checkSupport(expr.alternate, pos, matchers)
|
|
2730
2764
|
if (!altSupport.supported) return altSupport
|
|
2731
2765
|
|
|
2732
2766
|
return { supported: true, level: 'L4' }
|
|
@@ -2738,7 +2772,7 @@ function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
|
2738
2772
|
// object-literal hole has no sensible rendering either way).
|
|
2739
2773
|
for (const part of expr.parts) {
|
|
2740
2774
|
if (part.type === 'expression') {
|
|
2741
|
-
const partSupport = checkSupport(part.expr, pos)
|
|
2775
|
+
const partSupport = checkSupport(part.expr, pos, matchers)
|
|
2742
2776
|
if (!partSupport.supported) return partSupport
|
|
2743
2777
|
}
|
|
2744
2778
|
}
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,12 @@ export { extractSsrDefaults, deriveStashFromDefaults } from './ssr-defaults.ts'
|
|
|
15
15
|
export { propsDestructureBinding, buildPropAliasMap, isIdentifierName } from './props-binding.ts'
|
|
16
16
|
export type { SsrDefault } from './ssr-defaults.ts'
|
|
17
17
|
|
|
18
|
+
// Import-alias resolution (#2777 client-JS registry key; #2822 SSR cross-template
|
|
19
|
+
// call names) — a local alias -> declared/exported name map built from
|
|
20
|
+
// `ir.metadata.imports`. DSL adapters build this once per compile and look up
|
|
21
|
+
// `aliasMap.get(comp.name) ?? comp.name` at each cross-template-call-name site.
|
|
22
|
+
export { buildImportAliasMap } from './ir-to-client-js/component-scope.ts'
|
|
23
|
+
|
|
18
24
|
// Backend-neutral SSR seed plan (in-template derived signal/memo seeding)
|
|
19
25
|
export { computeSsrSeedPlan } from './ssr-seed-plan.ts'
|
|
20
26
|
export type { SsrSeedPlan, SsrSeedStep } from './ssr-seed-plan.ts'
|
|
@@ -63,6 +69,7 @@ export type {
|
|
|
63
69
|
ConformancePin,
|
|
64
70
|
ConformancePins,
|
|
65
71
|
RenderDivergences,
|
|
72
|
+
ImportInfo,
|
|
66
73
|
} from './types.ts'
|
|
67
74
|
|
|
68
75
|
// Analyzer
|
|
@@ -94,7 +101,7 @@ export { JsxAdapter } from './adapters/jsx-adapter.ts'
|
|
|
94
101
|
export type { JsxAdapterConfig } from './adapters/jsx-adapter.ts'
|
|
95
102
|
export { rewriteImportsForTemplate, rewriteDynamicImportsInSource } from './adapters/template-imports.ts'
|
|
96
103
|
export { emitParsedExpr, groupBinaryOperand, groupObjectLiteralSegments, isStringTypedOperand, isStringConcatBinary } from './adapters/parsed-expr-emitter.ts'
|
|
97
|
-
export type { ParsedExprEmitter, HigherOrderMethod, ArrayMethod, SortMethod, LiteralType } from './adapters/parsed-expr-emitter.ts'
|
|
104
|
+
export type { ParsedExprEmitter, LoweringEmitter, HigherOrderMethod, ArrayMethod, SortMethod, LiteralType } from './adapters/parsed-expr-emitter.ts'
|
|
98
105
|
export { collectLoopBoundNames } from './adapters/loop-bound-names.ts'
|
|
99
106
|
export { derivesScopeFromSlot } from './adapters/child-scope.ts'
|
|
100
107
|
export { evaluateStaticLiteral, isFullyStaticLiteral, resolveStaticLoopSource } from './static-literal.ts'
|
|
@@ -107,6 +114,7 @@ export {
|
|
|
107
114
|
prepareLoweringMatchers,
|
|
108
115
|
matchLoweringCall,
|
|
109
116
|
isValidHelperId,
|
|
117
|
+
loweringNodeChildren,
|
|
110
118
|
__resetLoweringPluginsForTest,
|
|
111
119
|
type LoweringPlugin,
|
|
112
120
|
type LoweringNode,
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { IRNode } from '../types.ts'
|
|
12
12
|
import type { ClientJsContext, ConditionalElement } from './types.ts'
|
|
13
|
+
import { resolveImportedComponentName } from './component-scope.ts'
|
|
13
14
|
|
|
14
15
|
/** Emit child-component import marker lines for every component name
|
|
15
16
|
* reachable from the current component's IR (minus siblings). */
|
|
@@ -39,7 +40,13 @@ export function emitChildComponentImports(
|
|
|
39
40
|
}
|
|
40
41
|
for (const childName of childComponentNames) {
|
|
41
42
|
if (!siblingSet.has(childName)) {
|
|
42
|
-
|
|
43
|
+
// #2777 — resolve an import alias (`import { Foo as Bar }`, `<Bar/>`)
|
|
44
|
+
// to the declared name (`Foo`) the child's own module registers
|
|
45
|
+
// under, so `packages/vite`'s child-name index (keyed by exported
|
|
46
|
+
// names) and `combine-client-js.ts`'s `hydrate('Name')` scan both
|
|
47
|
+
// find it. `siblingSet` is checked against the LOCAL name above —
|
|
48
|
+
// a same-file sibling is never reached through an import alias.
|
|
49
|
+
lines.push(`import '/* @bf-child:${resolveImportedComponentName(childName)} */'`)
|
|
43
50
|
}
|
|
44
51
|
}
|
|
45
52
|
}
|
|
@@ -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
|