@barefootjs/jsx 0.26.2 → 0.26.4
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/interface.d.ts +29 -0
- package/dist/adapters/interface.d.ts.map +1 -1
- package/dist/adapters/jsx-adapter.d.ts +9 -0
- package/dist/adapters/jsx-adapter.d.ts.map +1 -1
- package/dist/adapters/loop-bound-names.d.ts.map +1 -1
- package/dist/adapters/parsed-expr-emitter.d.ts +0 -10
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/adapters/test-adapter.d.ts.map +1 -1
- package/dist/analyzer-context.d.ts +11 -1
- package/dist/analyzer-context.d.ts.map +1 -1
- package/dist/analyzer.d.ts +40 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.js +1086 -269
- package/dist/ir-to-client-js/build-references.d.ts.map +1 -1
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts +14 -1
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.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/build-composite-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts +16 -1
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +39 -0
- 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 +12 -1
- package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts +19 -0
- 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/html-template.d.ts +67 -1
- 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/plan/build-static-array-child-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +29 -4
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/loop-destructure.d.ts.map +1 -1
- package/dist/strip-types.d.ts +18 -0
- package/dist/strip-types.d.ts.map +1 -1
- package/dist/types.d.ts +143 -32
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +277 -33
- package/src/__tests__/client-js-generation.test.ts +18 -5
- package/src/__tests__/compiler-runtime-contract.test.ts +4 -4
- package/src/__tests__/compiler-stress-1244.test.ts +12 -1
- package/src/__tests__/delegated-handler-preamble.test.ts +153 -0
- package/src/__tests__/event-delegation-scope-2367.test.ts +108 -0
- package/src/__tests__/flatmap-segments.test.ts +262 -0
- package/src/__tests__/map-arbitrary-body.test.ts +226 -0
- package/src/__tests__/map-body-no-silent-divergence.test.ts +405 -0
- package/src/__tests__/map-multi-return-body.test.ts +172 -0
- package/src/__tests__/preamble-region-patch.test.ts +150 -0
- package/src/__tests__/static-loop-csr-materialize.test.ts +3 -2
- package/src/__tests__/unsupported-expression.test.ts +20 -2
- package/src/adapters/interface.ts +40 -0
- package/src/adapters/jsx-adapter.ts +10 -0
- package/src/adapters/loop-bound-names.ts +6 -2
- package/src/adapters/parsed-expr-emitter.ts +5 -10
- package/src/adapters/test-adapter.ts +10 -0
- package/src/analyzer-context.ts +35 -1
- package/src/analyzer.ts +162 -24
- package/src/compiler.ts +2 -2
- package/src/expression-parser.ts +27 -19
- package/src/ir-to-client-js/build-references.ts +19 -2
- package/src/ir-to-client-js/collect-elements.ts +69 -13
- package/src/ir-to-client-js/control-flow/plan/branch-loop.ts +14 -1
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +18 -5
- package/src/ir-to-client-js/control-flow/plan/build-component-loop.ts +4 -0
- package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +15 -2
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +32 -3
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +26 -3
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +53 -2
- package/src/ir-to-client-js/control-flow/plan/event-delegation.ts +16 -1
- package/src/ir-to-client-js/control-flow/plan/loop.ts +40 -0
- package/src/ir-to-client-js/control-flow/shared.ts +28 -2
- package/src/ir-to-client-js/control-flow/stringify/branch-loop.ts +25 -3
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +76 -21
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +65 -1
- package/src/ir-to-client-js/control-flow.ts +11 -0
- package/src/ir-to-client-js/html-template.ts +244 -39
- package/src/ir-to-client-js/imports.ts +1 -1
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +22 -10
- package/src/ir-to-client-js/reactivity.ts +6 -0
- package/src/ir-to-client-js/types.ts +28 -3
- package/src/jsx-to-ir.ts +1059 -167
- package/src/loop-destructure.ts +9 -5
- package/src/rich-type-refusal.ts +6 -2
- package/src/strip-types.ts +47 -0
- package/src/types.ts +159 -35
package/src/jsx-to-ir.ts
CHANGED
|
@@ -26,7 +26,10 @@ import {
|
|
|
26
26
|
type LoopBindingPathSegment,
|
|
27
27
|
type RestExcludeKey,
|
|
28
28
|
type FlatMapCallback,
|
|
29
|
-
type
|
|
29
|
+
type MapCallbackPreamble,
|
|
30
|
+
type PreambleSegment,
|
|
31
|
+
type PreambleRegionSource,
|
|
32
|
+
tsxSourceText,
|
|
30
33
|
type SourceLocation,
|
|
31
34
|
type TypeInfo,
|
|
32
35
|
type OriginInfo,
|
|
@@ -48,11 +51,13 @@ import {
|
|
|
48
51
|
import { resolveFreeRefs, isNameBound as isNameBoundInEnv, type BindingEnvironment } from './free-refs.ts'
|
|
49
52
|
import { computeFileScope } from './ir-to-client-js/component-scope.ts'
|
|
50
53
|
import { createTemplateAwareStringProtector } from './ir-to-client-js/html-template.ts'
|
|
54
|
+
import { extractFreeIdentifiersFromText } from './ir-to-client-js/csr-substitute.ts'
|
|
51
55
|
import { datePlugin, DATE_METHODS } from './date-lowering.ts'
|
|
52
56
|
import { toLocaleDatePlugin, foldedArgToClientJs } from './to-locale-date-lowering.ts'
|
|
53
57
|
import type { LoweringMatcher } from './lowering-registry.ts'
|
|
54
|
-
import { extractFreeIdentifiersFromNode, initializerShapeContainsJsx } from './analyzer.ts'
|
|
58
|
+
import { extractFreeIdentifiersFromNode, initializerShapeContainsJsx, extractMultiReturnJsxBranches, type MultiReturnJsxBranches } from './analyzer.ts'
|
|
55
59
|
import { iterateJsTokens, replaceInExprContexts } from './scanner/js-scanner.ts'
|
|
60
|
+
import { reconstructAsSegments } from './strip-types.ts'
|
|
56
61
|
import { toHTMLAttrName, decodeEntities } from '@barefootjs/shared'
|
|
57
62
|
|
|
58
63
|
// =============================================================================
|
|
@@ -909,8 +914,12 @@ function attachParsedExpressions(node: IRNode, analyzer: AnalyzerContext, bound:
|
|
|
909
914
|
for (const nested of node.nestedComponents ?? []) {
|
|
910
915
|
for (const child of nested.children) attachParsedExpressions(child, analyzer, loopBound)
|
|
911
916
|
}
|
|
912
|
-
for (const
|
|
913
|
-
attachParsedExpressions(
|
|
917
|
+
for (const seg of node.flatMapCallback?.segments ?? []) {
|
|
918
|
+
if (seg.kind === 'jsx') attachParsedExpressions(seg.ir, analyzer, loopBound)
|
|
919
|
+
}
|
|
920
|
+
// Preamble leaves (array-builder bodies) are nested IR the same way.
|
|
921
|
+
for (const seg of node.preamble?.segments ?? []) {
|
|
922
|
+
if (seg.kind === 'jsx') attachParsedExpressions(seg.ir, analyzer, loopBound)
|
|
914
923
|
}
|
|
915
924
|
break
|
|
916
925
|
}
|
|
@@ -2119,100 +2128,132 @@ function transformMultiReturnJsxFunctionCall(
|
|
|
2119
2128
|
|
|
2120
2129
|
try {
|
|
2121
2130
|
const loc = getSourceLocation(callExpr, ctx.sourceFile, ctx.filePath)
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2131
|
+
return foldMultiReturnBranches(info, ctx, loc, substitutedGetJS)
|
|
2132
|
+
} finally {
|
|
2133
|
+
ctx.getJS = originalCtxGetJS
|
|
2134
|
+
ctx.analyzer.getJS = originalAnalyzerGetJS
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
/**
|
|
2139
|
+
* Fold an extracted multi-return branch structure (if/else-if chain or switch)
|
|
2140
|
+
* into a right-nested chain of `IRConditional` nodes, terminating in the
|
|
2141
|
+
* fallback (or `null`). Shared by the helper-function inliner
|
|
2142
|
+
* (`transformMultiReturnJsxFunctionCall`, which swaps `ctx.getJS` for a
|
|
2143
|
+
* param-substituting variant first) and the `.map()` callback-body fold
|
|
2144
|
+
* (Stage 2 of `spec/callback-fidelity.md`), which folds branches in the
|
|
2145
|
+
* loop-body context where the item binding is already in scope — no
|
|
2146
|
+
* substitution. `getText` stringifies condition / discriminant expressions
|
|
2147
|
+
* (the substituting variant for the former, `ctx.getJS` for the latter);
|
|
2148
|
+
* branch JSX is transformed via `transformNode`, which reads the (possibly
|
|
2149
|
+
* swapped) `ctx.getJS`.
|
|
2150
|
+
*/
|
|
2151
|
+
function foldMultiReturnBranches(
|
|
2152
|
+
info: MultiReturnJsxBranches,
|
|
2153
|
+
ctx: TransformContext,
|
|
2154
|
+
loc: SourceLocation,
|
|
2155
|
+
getText: (node: ts.Node) => string,
|
|
2156
|
+
): IRNode {
|
|
2157
|
+
const nullExpr: IRExpression = {
|
|
2158
|
+
type: 'expression',
|
|
2159
|
+
expr: 'null',
|
|
2160
|
+
typeInfo: { kind: 'primitive', raw: 'null', primitive: 'null' },
|
|
2161
|
+
reactive: false,
|
|
2162
|
+
slotId: null,
|
|
2163
|
+
loc,
|
|
2164
|
+
origin: { phase: 'tick', scope: 'template', effect: 'pure', freeRefs: [] },
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
// Build the conditional chain from bottom up (last branch → first branch)
|
|
2168
|
+
let result: IRNode = info.fallback
|
|
2169
|
+
? (transformNode(info.fallback, ctx) ?? nullExpr)
|
|
2170
|
+
: nullExpr
|
|
2171
|
+
|
|
2172
|
+
for (let i = info.branches.length - 1; i >= 0; i--) {
|
|
2173
|
+
const branch = info.branches[i]
|
|
2174
|
+
|
|
2175
|
+
// A switch fallthrough (`case 'a': case 'b': return X`) yields one branch
|
|
2176
|
+
// covering several case labels — OR-join them into `disc === a ||
|
|
2177
|
+
// disc === b`. A plain if-branch or a single case is just `[condition]`.
|
|
2178
|
+
const caseConds = info.switchDiscriminant
|
|
2179
|
+
? [branch.condition, ...(branch.extraCaseConditions ?? [])]
|
|
2180
|
+
: [branch.condition]
|
|
2181
|
+
|
|
2182
|
+
// Build condition text (`getText` applies param substitution when the
|
|
2183
|
+
// helper-function inliner supplies a substituting variant).
|
|
2184
|
+
let conditionText: string
|
|
2185
|
+
if (info.switchDiscriminant) {
|
|
2186
|
+
const discText = getText(info.switchDiscriminant)
|
|
2187
|
+
// Parenthesize both operands so a low-precedence case expression
|
|
2188
|
+
// (`case a ?? b:`, a ternary, …) keeps strict-equality semantics rather
|
|
2189
|
+
// than binding as `(disc === a) ?? b`. (#2377 review.)
|
|
2190
|
+
conditionText = caseConds.map(c => `(${discText}) === (${getText(c)})`).join(' || ')
|
|
2191
|
+
} else {
|
|
2192
|
+
conditionText = getText(branch.condition)
|
|
2130
2193
|
}
|
|
2131
2194
|
|
|
2132
|
-
//
|
|
2133
|
-
|
|
2134
|
-
|
|
2195
|
+
// For switch-sourced conditions, merge freeRefs/reactivity from the
|
|
2196
|
+
// discriminant and every case expression so prop rewrites and reactivity
|
|
2197
|
+
// detection cover the full `disc === a || disc === b` condition.
|
|
2198
|
+
const env = makeBindingEnv(ctx)
|
|
2199
|
+
const caseFreeRefs = caseConds.flatMap(c => resolveFreeRefs(c, env))
|
|
2200
|
+
const discFreeRefs = info.switchDiscriminant
|
|
2201
|
+
? resolveFreeRefs(info.switchDiscriminant, env)
|
|
2202
|
+
: []
|
|
2203
|
+
const conditionOrigin: OriginInfo = {
|
|
2204
|
+
phase: 'tick',
|
|
2205
|
+
scope: 'template',
|
|
2206
|
+
effect: 'pure',
|
|
2207
|
+
freeRefs: [...discFreeRefs, ...caseFreeRefs],
|
|
2208
|
+
}
|
|
2209
|
+
const reactive = isReactiveExpression(conditionText, ctx, branch.condition)
|
|
2210
|
+
|| isReactiveOrigin(conditionOrigin)
|
|
2211
|
+
const loopParamReactive = !reactive && referencesLoopParam(conditionText, ctx)
|
|
2212
|
+
const callsReactive = caseConds.some(c => exprCallsReactiveGetters(c, ctx))
|
|
2213
|
+
|| (info.switchDiscriminant ? exprCallsReactiveGetters(info.switchDiscriminant, ctx) : false)
|
|
2214
|
+
const hasCalls = caseConds.some(c => exprHasFunctionCalls(c))
|
|
2215
|
+
|| (info.switchDiscriminant ? exprHasFunctionCalls(info.switchDiscriminant) : false)
|
|
2216
|
+
const needsSlot = reactive || loopParamReactive || callsReactive || hasCalls
|
|
2217
|
+
const slotId = needsSlot ? generateSlotId(ctx) : null
|
|
2218
|
+
|
|
2219
|
+
const whenTrue = branch.jsxReturn
|
|
2220
|
+
? (transformNode(branch.jsxReturn, ctx) ?? nullExpr)
|
|
2135
2221
|
: nullExpr
|
|
2136
2222
|
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
}
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
// both the discriminant and case expression so prop rewrites and
|
|
2152
|
-
// reactivity detection cover the full `disc === case` condition.
|
|
2153
|
-
const env = makeBindingEnv(ctx)
|
|
2154
|
-
const caseFreeRefs = resolveFreeRefs(branch.condition, env)
|
|
2155
|
-
const discFreeRefs = info.switchDiscriminant
|
|
2156
|
-
? resolveFreeRefs(info.switchDiscriminant, env)
|
|
2157
|
-
: []
|
|
2158
|
-
const conditionOrigin: OriginInfo = {
|
|
2159
|
-
phase: 'tick',
|
|
2160
|
-
scope: 'template',
|
|
2161
|
-
effect: 'pure',
|
|
2162
|
-
freeRefs: [...discFreeRefs, ...caseFreeRefs],
|
|
2163
|
-
}
|
|
2164
|
-
const reactive = isReactiveExpression(conditionText, ctx, branch.condition)
|
|
2165
|
-
|| isReactiveOrigin(conditionOrigin)
|
|
2166
|
-
const loopParamReactive = !reactive && referencesLoopParam(conditionText, ctx)
|
|
2167
|
-
const callsReactive = exprCallsReactiveGetters(branch.condition, ctx)
|
|
2168
|
-
|| (info.switchDiscriminant ? exprCallsReactiveGetters(info.switchDiscriminant, ctx) : false)
|
|
2169
|
-
const hasCalls = exprHasFunctionCalls(branch.condition)
|
|
2170
|
-
|| (info.switchDiscriminant ? exprHasFunctionCalls(info.switchDiscriminant) : false)
|
|
2171
|
-
const needsSlot = reactive || loopParamReactive || callsReactive || hasCalls
|
|
2172
|
-
const slotId = needsSlot ? generateSlotId(ctx) : null
|
|
2173
|
-
|
|
2174
|
-
const whenTrue = branch.jsxReturn
|
|
2175
|
-
? (transformNode(branch.jsxReturn, ctx) ?? nullExpr)
|
|
2176
|
-
: nullExpr
|
|
2177
|
-
|
|
2178
|
-
// For switch conditions, build templateCondition from both parts
|
|
2179
|
-
let templateCondition: string | undefined
|
|
2180
|
-
if (info.switchDiscriminant) {
|
|
2181
|
-
const discRewritten = rewriteBarePropRefs(
|
|
2182
|
-
substitutedGetJS(info.switchDiscriminant), info.switchDiscriminant, ctx
|
|
2183
|
-
)
|
|
2184
|
-
const caseRewritten = rewriteBarePropRefs(
|
|
2185
|
-
substitutedGetJS(branch.condition), branch.condition, ctx
|
|
2186
|
-
)
|
|
2187
|
-
const discPart = discRewritten ?? substitutedGetJS(info.switchDiscriminant)
|
|
2188
|
-
const casePart = caseRewritten ?? substitutedGetJS(branch.condition)
|
|
2189
|
-
templateCondition = `${discPart} === ${casePart}`
|
|
2190
|
-
} else {
|
|
2191
|
-
templateCondition = rewriteBarePropRefs(conditionText, branch.condition, ctx)
|
|
2192
|
-
}
|
|
2193
|
-
|
|
2194
|
-
const conditional: IRConditional = {
|
|
2195
|
-
type: 'conditional',
|
|
2196
|
-
condition: conditionText,
|
|
2197
|
-
templateCondition,
|
|
2198
|
-
conditionType: null,
|
|
2199
|
-
reactive,
|
|
2200
|
-
whenTrue,
|
|
2201
|
-
whenFalse: result,
|
|
2202
|
-
slotId,
|
|
2203
|
-
callsReactiveGetters: callsReactive || undefined,
|
|
2204
|
-
hasFunctionCalls: hasCalls || undefined,
|
|
2205
|
-
loc,
|
|
2206
|
-
origin: conditionOrigin,
|
|
2207
|
-
}
|
|
2208
|
-
result = conditional
|
|
2223
|
+
// For switch conditions, build templateCondition from the discriminant and
|
|
2224
|
+
// every (prop-rewritten) case label, OR-joined for fallthrough.
|
|
2225
|
+
let templateCondition: string | undefined
|
|
2226
|
+
if (info.switchDiscriminant) {
|
|
2227
|
+
const discRewritten = rewriteBarePropRefs(
|
|
2228
|
+
getText(info.switchDiscriminant), info.switchDiscriminant, ctx
|
|
2229
|
+
)
|
|
2230
|
+
const discPart = discRewritten ?? getText(info.switchDiscriminant)
|
|
2231
|
+
templateCondition = caseConds.map(c => {
|
|
2232
|
+
const casePart = rewriteBarePropRefs(getText(c), c, ctx) ?? getText(c)
|
|
2233
|
+
return `(${discPart}) === (${casePart})`
|
|
2234
|
+
}).join(' || ')
|
|
2235
|
+
} else {
|
|
2236
|
+
templateCondition = rewriteBarePropRefs(conditionText, branch.condition, ctx)
|
|
2209
2237
|
}
|
|
2210
2238
|
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2239
|
+
const conditional: IRConditional = {
|
|
2240
|
+
type: 'conditional',
|
|
2241
|
+
condition: conditionText,
|
|
2242
|
+
templateCondition,
|
|
2243
|
+
conditionType: null,
|
|
2244
|
+
reactive,
|
|
2245
|
+
whenTrue,
|
|
2246
|
+
whenFalse: result,
|
|
2247
|
+
slotId,
|
|
2248
|
+
callsReactiveGetters: callsReactive || undefined,
|
|
2249
|
+
hasFunctionCalls: hasCalls || undefined,
|
|
2250
|
+
loc,
|
|
2251
|
+
origin: conditionOrigin,
|
|
2252
|
+
}
|
|
2253
|
+
result = conditional
|
|
2215
2254
|
}
|
|
2255
|
+
|
|
2256
|
+
return result
|
|
2216
2257
|
}
|
|
2217
2258
|
|
|
2218
2259
|
function transformConditional(
|
|
@@ -3597,6 +3638,99 @@ function checkLoopKey(
|
|
|
3597
3638
|
* item; multi-root bodies (e.g. `<><path/><path/></>`) need per-item
|
|
3598
3639
|
* boundary markers and multi-root template cloning (#1212).
|
|
3599
3640
|
*/
|
|
3641
|
+
/**
|
|
3642
|
+
* Recognize a flatMap PROJECTION body: a bare map-like call
|
|
3643
|
+
* (`it.tags.map(...)` / nested `.flatMap(...)`) as the whole body — either
|
|
3644
|
+
* an expression body (parenthesized or not) or a block whose ONLY statement
|
|
3645
|
+
* is `return <call>`. Anything carrying additional statements does not
|
|
3646
|
+
* qualify (it rides the structured-segments carrier instead). Returns the
|
|
3647
|
+
* call expression to lower as the loop's nested-loop child, or null.
|
|
3648
|
+
*/
|
|
3649
|
+
function flatMapProjectionCall(body: ts.ConciseBody): ts.CallExpression | null {
|
|
3650
|
+
let expr: ts.Expression | undefined
|
|
3651
|
+
if (ts.isBlock(body)) {
|
|
3652
|
+
const real = body.statements
|
|
3653
|
+
if (real.length !== 1 || !ts.isReturnStatement(real[0]) || !real[0].expression) return null
|
|
3654
|
+
expr = real[0].expression
|
|
3655
|
+
} else {
|
|
3656
|
+
expr = body
|
|
3657
|
+
}
|
|
3658
|
+
while (ts.isParenthesizedExpression(expr)) expr = expr.expression
|
|
3659
|
+
if (!ts.isCallExpression(expr)) return null
|
|
3660
|
+
if (!getMapLikeMethod(expr)) return null
|
|
3661
|
+
// The descriptor synthesis renders the inner callback's raw params into
|
|
3662
|
+
// the client accessor (`.map((tag, i) => ({ k, h }))`), which is only
|
|
3663
|
+
// sound for plain-identifier params — a destructure pattern rides the
|
|
3664
|
+
// segments carrier instead.
|
|
3665
|
+
const cb = expr.arguments[0]
|
|
3666
|
+
if (!cb || (!ts.isArrowFunction(cb) && !ts.isFunctionExpression(cb))) return null
|
|
3667
|
+
for (const p of cb.parameters) {
|
|
3668
|
+
if (!ts.isIdentifier(p.name)) return null
|
|
3669
|
+
}
|
|
3670
|
+
// The client descriptor is one element per flattened leaf, so the inner
|
|
3671
|
+
// callback must produce a single element root (a JSX element, or a ternary
|
|
3672
|
+
// whose branches are elements). Fragments / array literals ride the
|
|
3673
|
+
// segments carrier.
|
|
3674
|
+
let innerBody: ts.Node = cb.body
|
|
3675
|
+
if (ts.isBlock(innerBody)) {
|
|
3676
|
+
const ret = innerBody.statements.find(
|
|
3677
|
+
(s): s is ts.ReturnStatement => ts.isReturnStatement(s) && s.expression != null,
|
|
3678
|
+
)
|
|
3679
|
+
if (innerBody.statements.length !== 1 || !ret?.expression) return null
|
|
3680
|
+
innerBody = ret.expression
|
|
3681
|
+
}
|
|
3682
|
+
while (ts.isParenthesizedExpression(innerBody)) innerBody = innerBody.expression
|
|
3683
|
+
const isElementish = (n: ts.Node): boolean => {
|
|
3684
|
+
let m = n
|
|
3685
|
+
while (ts.isParenthesizedExpression(m)) m = m.expression
|
|
3686
|
+
if (ts.isJsxElement(m) || ts.isJsxSelfClosingElement(m)) return leafIsWirelessElement(m)
|
|
3687
|
+
if (ts.isConditionalExpression(m)) return isElementish(m.whenTrue) && isElementish(m.whenFalse)
|
|
3688
|
+
return false
|
|
3689
|
+
}
|
|
3690
|
+
if (!isElementish(innerBody)) return null
|
|
3691
|
+
return expr
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
/**
|
|
3695
|
+
* Syntactic pre-check that a projection leaf carries no per-element wiring —
|
|
3696
|
+
* no event handlers, spreads, component tags, or nested JSX-bearing loops.
|
|
3697
|
+
* Runs BEFORE the leaf is transformed (the accept/reject decision must not
|
|
3698
|
+
* leave transform side effects — slot ids, collected events — behind when
|
|
3699
|
+
* the shape falls back to the segments carrier, whose own transform would
|
|
3700
|
+
* then double-register them).
|
|
3701
|
+
*/
|
|
3702
|
+
function leafIsWirelessElement(el: ts.JsxElement | ts.JsxSelfClosingElement): boolean {
|
|
3703
|
+
let ok = true
|
|
3704
|
+
const visit = (n: ts.Node): void => {
|
|
3705
|
+
if (!ok) return
|
|
3706
|
+
if (ts.isJsxOpeningElement(n) || ts.isJsxSelfClosingElement(n)) {
|
|
3707
|
+
// Only a lowercase plain identifier (or a namespaced name like
|
|
3708
|
+
// `svg:path`) is an intrinsic element. A member/this tag
|
|
3709
|
+
// (`<icons.Tag/>`, `<this.Tag/>`) is a component per JSX semantics
|
|
3710
|
+
// regardless of case — the case test alone would let it through.
|
|
3711
|
+
const tagNode = n.tagName
|
|
3712
|
+
const isIntrinsic = ts.isIdentifier(tagNode)
|
|
3713
|
+
? !/^[A-Z]/.test(tagNode.text)
|
|
3714
|
+
: ts.isJsxNamespacedName(tagNode)
|
|
3715
|
+
if (!isIntrinsic) { ok = false; return }
|
|
3716
|
+
for (const attr of n.attributes.properties) {
|
|
3717
|
+
if (ts.isJsxSpreadAttribute(attr)) { ok = false; return }
|
|
3718
|
+
if (ts.isJsxAttribute(attr)) {
|
|
3719
|
+
const name = attr.name.getText()
|
|
3720
|
+
if (/^on[A-Z]/.test(name)) { ok = false; return }
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
if (ts.isCallExpression(n) && getMapLikeMethod(n) && containsJsxInExpression(n)) {
|
|
3725
|
+
ok = false
|
|
3726
|
+
return
|
|
3727
|
+
}
|
|
3728
|
+
ts.forEachChild(n, visit)
|
|
3729
|
+
}
|
|
3730
|
+
visit(el)
|
|
3731
|
+
return ok
|
|
3732
|
+
}
|
|
3733
|
+
|
|
3600
3734
|
function loopBodyIsMultiRoot(children: IRNode[]): boolean {
|
|
3601
3735
|
const real = children.filter(
|
|
3602
3736
|
(c) => !(c.type === 'text' && typeof c.value === 'string' && !c.value.trim())
|
|
@@ -3680,6 +3814,10 @@ function transformMapCall(
|
|
|
3680
3814
|
// Capture nesting depth before we register this map's own params.
|
|
3681
3815
|
// ctx.loopParams is populated by the *outer* map; if non-empty we are inside one.
|
|
3682
3816
|
const isNested = ctx.loopParams.size > 0
|
|
3817
|
+
// Diagnostic count at entry — the structural net at the scalar fallthrough
|
|
3818
|
+
// de-dups against refusals fired DURING this call (leaf-wiring, DSL gates),
|
|
3819
|
+
// never against unrelated diagnostics recorded before it.
|
|
3820
|
+
const diagCountAtEntry = ctx.analyzer.errors.length
|
|
3683
3821
|
// This loop's own depth (0 = outermost) is however many enclosing
|
|
3684
3822
|
// loops are already active, captured before `ctx.loopDepth` below is
|
|
3685
3823
|
// bumped for THIS loop's own descendants.
|
|
@@ -3705,9 +3843,10 @@ function transformMapCall(
|
|
|
3705
3843
|
let filterPredicate: FilterPredicateResult | undefined
|
|
3706
3844
|
let sortComparator: IRLoopSort | undefined
|
|
3707
3845
|
let chainOrder: 'filter-sort' | 'sort-filter' | undefined
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3846
|
+
// Structured pre-return statements of a block-body callback (Stage 3 root
|
|
3847
|
+
// cure): JS text + compiled JSX leaves as segments, plus the raw TSX for
|
|
3848
|
+
// JSX-runtime SSR and the declared names (D5 key guard, {out} join).
|
|
3849
|
+
let preamble: MapCallbackPreamble | undefined
|
|
3711
3850
|
let iterationShape: 'entries' | 'keys' | undefined
|
|
3712
3851
|
let objectIteration: 'entries' | 'keys' | 'values' | undefined
|
|
3713
3852
|
|
|
@@ -3761,7 +3900,12 @@ function transformMapCall(
|
|
|
3761
3900
|
// Handle sort comparator extraction
|
|
3762
3901
|
const sortExtraction = extractSortComparator(sortInfo.callback, sortInfo.method, ctx)
|
|
3763
3902
|
if (isClientOnly || !sortExtraction.result) {
|
|
3764
|
-
|
|
3903
|
+
// Off-subset comparator: keep it in the array string for client / SSR
|
|
3904
|
+
// evaluation. Only raise the diagnostic when the target adapter's runtime
|
|
3905
|
+
// can't run the comparator body verbatim (DSL); a JS-runtime adapter runs
|
|
3906
|
+
// it, so rejecting it would be a universal error for a DSL-only limit.
|
|
3907
|
+
// See spec/callback-fidelity.md.
|
|
3908
|
+
if (!isClientOnly && sortExtraction.unsupportedReason && !(ctx.analyzer.acceptsCallbackBody?.('sort') ?? false)) {
|
|
3765
3909
|
ctx.analyzer.errors.push(
|
|
3766
3910
|
createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN,
|
|
3767
3911
|
getSourceLocation(sortInfo.callback, ctx.sourceFile, ctx.filePath),
|
|
@@ -3784,7 +3928,11 @@ function transformMapCall(
|
|
|
3784
3928
|
chainOrder = 'filter-sort'
|
|
3785
3929
|
const filterExtraction = extractFilterPredicate(innerFilter.callback, ctx)
|
|
3786
3930
|
if (isClientOnly || !filterExtraction.result) {
|
|
3787
|
-
|
|
3931
|
+
// Off-subset predicate: keep it in the array string for client / SSR
|
|
3932
|
+
// evaluation. Only raise the diagnostic when the target adapter's runtime
|
|
3933
|
+
// can't run the predicate body verbatim (DSL); a JS-runtime adapter runs
|
|
3934
|
+
// it. See spec/callback-fidelity.md.
|
|
3935
|
+
if (!isClientOnly && filterExtraction.unsupportedReason && !(ctx.analyzer.acceptsCallbackBody?.('filter') ?? false)) {
|
|
3788
3936
|
ctx.analyzer.errors.push(
|
|
3789
3937
|
createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN,
|
|
3790
3938
|
getSourceLocation(innerFilter.callback, ctx.sourceFile, ctx.filePath),
|
|
@@ -3818,7 +3966,11 @@ function transformMapCall(
|
|
|
3818
3966
|
const filterExtraction = extractFilterPredicate(filterInfo.callback, ctx)
|
|
3819
3967
|
|
|
3820
3968
|
if (isClientOnly || !filterExtraction.result) {
|
|
3821
|
-
|
|
3969
|
+
// Off-subset predicate: keep it in the array string for client / SSR
|
|
3970
|
+
// evaluation. Only raise the diagnostic when the target adapter's runtime
|
|
3971
|
+
// can't run the predicate body verbatim (DSL); a JS-runtime adapter runs
|
|
3972
|
+
// it. See spec/callback-fidelity.md.
|
|
3973
|
+
if (!isClientOnly && filterExtraction.unsupportedReason && !(ctx.analyzer.acceptsCallbackBody?.('filter') ?? false)) {
|
|
3822
3974
|
ctx.analyzer.errors.push(
|
|
3823
3975
|
createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN,
|
|
3824
3976
|
getSourceLocation(filterInfo.callback, ctx.sourceFile, ctx.filePath),
|
|
@@ -3841,7 +3993,12 @@ function transformMapCall(
|
|
|
3841
3993
|
chainOrder = 'sort-filter'
|
|
3842
3994
|
const sortExtraction = extractSortComparator(innerSort.callback, innerSort.method, ctx)
|
|
3843
3995
|
if (isClientOnly || !sortExtraction.result) {
|
|
3844
|
-
|
|
3996
|
+
// Off-subset comparator: keep it in the array string for client / SSR
|
|
3997
|
+
// evaluation. Only raise the diagnostic when the target adapter's runtime
|
|
3998
|
+
// can't run the comparator body verbatim (DSL); a JS-runtime adapter runs
|
|
3999
|
+
// it, so rejecting it would be a universal error for a DSL-only limit.
|
|
4000
|
+
// See spec/callback-fidelity.md.
|
|
4001
|
+
if (!isClientOnly && sortExtraction.unsupportedReason && !(ctx.analyzer.acceptsCallbackBody?.('sort') ?? false)) {
|
|
3845
4002
|
ctx.analyzer.errors.push(
|
|
3846
4003
|
createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN,
|
|
3847
4004
|
getSourceLocation(innerSort.callback, ctx.sourceFile, ctx.filePath),
|
|
@@ -4032,10 +4189,58 @@ function transformMapCall(
|
|
|
4032
4189
|
// flatMap arrow with array literal: items.flatMap(item => [<A/>, <B/>])
|
|
4033
4190
|
children = transformArrayLiteralChildren(body, ctx)
|
|
4034
4191
|
} else if (ts.isBlock(body)) {
|
|
4192
|
+
// Multi-return JSX block body (if/else-if chain or a `switch`, including
|
|
4193
|
+
// fallthrough case labels, optionally preceded by a `const`/`let`
|
|
4194
|
+
// preamble) — fold to a nested IRConditional so the loop renders each
|
|
4195
|
+
// branch's compiled template. Tried BEFORE the single-return path:
|
|
4196
|
+
// otherwise the trailing `return <fallback/>` would claim the
|
|
4197
|
+
// single-return arm and the leading `if (...) return <A/>` would leak
|
|
4198
|
+
// verbatim into the map preamble (the silent-verbatim-leak this fixes).
|
|
4199
|
+
// Stage 2 of spec/callback-fidelity.md. A branch-local `const` (inside a
|
|
4200
|
+
// branch block/case) or a statement-level nested loop is not folded
|
|
4201
|
+
// (extractMultiReturnJsxBranches returns null) and falls through below.
|
|
4202
|
+
const multiReturn = method !== 'flatMap' ? extractMultiReturnJsxBranches(body, true) : null
|
|
4203
|
+
if (multiReturn && multiReturn.branches.length > 0) {
|
|
4204
|
+
const loc = getSourceLocation(body, ctx.sourceFile, ctx.filePath)
|
|
4205
|
+
children = [foldMultiReturnBranches(multiReturn, ctx, loc, ctx.getJS)]
|
|
4206
|
+
|
|
4207
|
+
const pre = multiReturn.preamble ?? []
|
|
4208
|
+
if (pre.length > 0) {
|
|
4209
|
+
// Emit the leading const/let preamble once per iteration, ahead of
|
|
4210
|
+
// the conditional — the same carrier the single-return path uses. A
|
|
4211
|
+
// JS-runtime adapter (and the browser under /* @client */) runs it
|
|
4212
|
+
// verbatim.
|
|
4213
|
+
preamble = preambleFromValueStatements(pre, ctx)
|
|
4214
|
+
|
|
4215
|
+
// A DSL adapter can't carry a loop-local `const` into a conditional
|
|
4216
|
+
// branch template, so it would render the branches with the local
|
|
4217
|
+
// undefined (silent divergence). Refuse instead — adapter-gated like
|
|
4218
|
+
// the filter/sort sites: a JS-runtime target folds and runs it, a DSL
|
|
4219
|
+
// target errors with the /* @client */ escape (which renders the map
|
|
4220
|
+
// client-only, where the browser runs the preamble). Stage 2 of
|
|
4221
|
+
// spec/callback-fidelity.md.
|
|
4222
|
+
if (!isClientOnly && !(ctx.analyzer.acceptsCallbackBody?.('map') ?? false)) {
|
|
4223
|
+
ctx.analyzer.errors.push(
|
|
4224
|
+
createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN, loc, {
|
|
4225
|
+
message:
|
|
4226
|
+
'A .map() callback body with a `const`/`let` preamble before its ' +
|
|
4227
|
+
'branches cannot be lowered to a template: the loop-local binding ' +
|
|
4228
|
+
'cannot be carried into a conditional branch on this backend.',
|
|
4229
|
+
suggestion: {
|
|
4230
|
+
message: 'Add /* @client */ to evaluate this expression on the client only',
|
|
4231
|
+
},
|
|
4232
|
+
})
|
|
4233
|
+
)
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
4236
|
+
}
|
|
4237
|
+
|
|
4035
4238
|
// Block body: (item) => { const label = ...; return <div>{label}</div> }
|
|
4036
|
-
const returnStmt =
|
|
4037
|
-
|
|
4038
|
-
|
|
4239
|
+
const returnStmt = children.length === 0
|
|
4240
|
+
? body.statements.find(
|
|
4241
|
+
(s): s is ts.ReturnStatement => ts.isReturnStatement(s) && s.expression != null
|
|
4242
|
+
)
|
|
4243
|
+
: undefined
|
|
4039
4244
|
if (returnStmt && returnStmt.expression) {
|
|
4040
4245
|
let returnExpr = returnStmt.expression
|
|
4041
4246
|
while (ts.isParenthesizedExpression(returnExpr)) {
|
|
@@ -4047,42 +4252,202 @@ function transformMapCall(
|
|
|
4047
4252
|
children = [transformed]
|
|
4048
4253
|
}
|
|
4049
4254
|
}
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
const
|
|
4053
|
-
|
|
4054
|
-
|
|
4255
|
+
// Stage 3 of spec/callback-fidelity.md — an arbitrary `.map()` body that
|
|
4256
|
+
// *constructs* JSX in a statement before its `return` (the classic
|
|
4257
|
+
// `const out = []; for (const c of it.cells) out.push(<td>{c}</td>);
|
|
4258
|
+
// return <tr>{out}</tr>` array-builder) cannot be lowered to a template.
|
|
4259
|
+
// The preamble collector below reconstructs each statement with
|
|
4260
|
+
// `ctx.getJS`, which strips *types* but not JSX, so the raw `<td>{c}</td>`
|
|
4261
|
+
// would splice verbatim into the emitted client bundle — invalid JS, a
|
|
4262
|
+
// silent syntax-error leak with no diagnostic. Refuse loudly instead of
|
|
4263
|
+
// leaking. (A later Stage-3 PR renders such bodies verbatim on JS
|
|
4264
|
+
// runtimes; until then it is a build error on every backend.)
|
|
4265
|
+
// Scan only statements the preamble collector below would reach — i.e.
|
|
4266
|
+
// those *before* `returnStmt` (it `break`s at the return). JSX in
|
|
4267
|
+
// unreachable post-return dead code is never spliced into the preamble,
|
|
4268
|
+
// so it is not part of the leak and must not trip the refusal.
|
|
4269
|
+
let jsxPreambleStmt: ts.Statement | undefined
|
|
4055
4270
|
for (const stmt of body.statements) {
|
|
4056
4271
|
if (stmt === returnStmt) break
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
templatePreambleStmts.push(tjs.endsWith(';') ? tjs : tjs + ';')
|
|
4062
|
-
typedPreambleStmts.push(ts.endsWith(';') ? ts : ts + ';')
|
|
4063
|
-
if (js !== ts) hasTypeDiff = true
|
|
4064
|
-
if (js !== tjs) hasTemplateDiff = true
|
|
4272
|
+
if (containsJsxInExpression(stmt)) {
|
|
4273
|
+
jsxPreambleStmt = stmt
|
|
4274
|
+
break
|
|
4275
|
+
}
|
|
4065
4276
|
}
|
|
4066
|
-
if (
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4277
|
+
if (jsxPreambleStmt) {
|
|
4278
|
+
// Stage 3 / D4 (spec/callback-fidelity.md). A JS-runtime adapter runs
|
|
4279
|
+
// the callback body verbatim — each JSX leaf lowers to a template-
|
|
4280
|
+
// literal HTML string, the imperative control flow runs as-is, and the
|
|
4281
|
+
// element-array child ({out}) is joined into the row. A DSL template
|
|
4282
|
+
// runtime can't do this, so it refuses with the /* @client */ escape
|
|
4283
|
+
// (which renders the loop client-only, where the browser — always JS —
|
|
4284
|
+
// runs the same body). Mirrors the const-preamble gate above and the
|
|
4285
|
+
// Stage-1 filter/sort sites (all consult `acceptsCallbackBody`).
|
|
4286
|
+
const jsRuntime = isClientOnly || (ctx.analyzer.acceptsCallbackBody?.('map') ?? false)
|
|
4287
|
+
if (children.length === 0) {
|
|
4288
|
+
// The return shape produced no loop children (a bare identifier
|
|
4289
|
+
// `return out`, a ternary, any non-element return): there is no
|
|
4290
|
+
// single template root to host the built elements, so the loop IR
|
|
4291
|
+
// can't exist and the fallback would splice the raw callback —
|
|
4292
|
+
// JSX included — verbatim into an expression anchor (a silent
|
|
4293
|
+
// leak on EVERY tier, /* @client */ included). Refuse loudly with
|
|
4294
|
+
// restructuring guidance; the return-shape holes pinned by
|
|
4295
|
+
// map-body-no-silent-divergence.test.ts close here.
|
|
4296
|
+
ctx.analyzer.errors.push(
|
|
4297
|
+
createError(
|
|
4298
|
+
ErrorCodes.UNSUPPORTED_JSX_PATTERN,
|
|
4299
|
+
getSourceLocation(jsxPreambleStmt, ctx.sourceFile, ctx.filePath),
|
|
4300
|
+
{
|
|
4301
|
+
message:
|
|
4302
|
+
'A .map() callback that builds JSX in a statement before its ' +
|
|
4303
|
+
'`return` must return a single JSX element that embeds the ' +
|
|
4304
|
+
'built array — this return shape has no element root to host it.',
|
|
4305
|
+
suggestion: {
|
|
4306
|
+
message:
|
|
4307
|
+
'Wrap the result in one element root, e.g. `return <tr key={item.id}>{out}</tr>`.',
|
|
4308
|
+
},
|
|
4309
|
+
}
|
|
4310
|
+
)
|
|
4311
|
+
)
|
|
4312
|
+
} else if (!jsRuntime) {
|
|
4313
|
+
ctx.analyzer.errors.push(
|
|
4314
|
+
createError(
|
|
4315
|
+
ErrorCodes.UNSUPPORTED_JSX_PATTERN,
|
|
4316
|
+
getSourceLocation(jsxPreambleStmt, ctx.sourceFile, ctx.filePath),
|
|
4317
|
+
{
|
|
4318
|
+
message:
|
|
4319
|
+
'A .map() callback that builds JSX in a statement before its ' +
|
|
4320
|
+
'`return` (for example pushing elements into an array in a loop) ' +
|
|
4321
|
+
'cannot be lowered to a template on this backend.',
|
|
4322
|
+
suggestion: {
|
|
4323
|
+
message: 'Add /* @client */ to render this loop on the client only',
|
|
4324
|
+
},
|
|
4325
|
+
}
|
|
4326
|
+
)
|
|
4327
|
+
)
|
|
4328
|
+
} else {
|
|
4329
|
+
const collected = buildPreambleSegments(body.statements, returnStmt, ctx)
|
|
4330
|
+
if (collected.refusalNode) {
|
|
4331
|
+
// A leaf carries wiring the verbatim path can't render (event
|
|
4332
|
+
// handler, component, nested loop, reactive slot, spread), or
|
|
4333
|
+
// sits inside a template literal (a segment boundary there would
|
|
4334
|
+
// split the literal's lexical state for downstream text
|
|
4335
|
+
// transforms). Refuse loudly (D-E) rather than emit a
|
|
4336
|
+
// silently-degraded node.
|
|
4337
|
+
ctx.analyzer.errors.push(
|
|
4338
|
+
createError(
|
|
4339
|
+
ErrorCodes.UNSUPPORTED_JSX_PATTERN,
|
|
4340
|
+
getSourceLocation(collected.refusalNode, ctx.sourceFile, ctx.filePath),
|
|
4341
|
+
{
|
|
4342
|
+
message:
|
|
4343
|
+
'A JSX element built in a .map() callback preamble cannot carry ' +
|
|
4344
|
+
'event handlers, components, nested loops, or reactive expressions, ' +
|
|
4345
|
+
'and cannot sit inside a template literal — the verbatim render ' +
|
|
4346
|
+
'path builds it once, with no reactive wiring.',
|
|
4347
|
+
suggestion: {
|
|
4348
|
+
message:
|
|
4349
|
+
'Return the element directly (not through a preamble variable), ' +
|
|
4350
|
+
'or add /* @client */ to render the loop on the client only.',
|
|
4351
|
+
},
|
|
4352
|
+
}
|
|
4353
|
+
)
|
|
4354
|
+
)
|
|
4355
|
+
} else {
|
|
4356
|
+
preamble = collected.preamble
|
|
4357
|
+
}
|
|
4070
4358
|
}
|
|
4071
|
-
|
|
4072
|
-
|
|
4359
|
+
} else {
|
|
4360
|
+
const valueStmts: ts.Statement[] = []
|
|
4361
|
+
for (const stmt of body.statements) {
|
|
4362
|
+
if (stmt === returnStmt) break
|
|
4363
|
+
valueStmts.push(stmt)
|
|
4364
|
+
}
|
|
4365
|
+
if (valueStmts.length > 0) {
|
|
4366
|
+
preamble = preambleFromValueStatements(valueStmts, ctx)
|
|
4073
4367
|
}
|
|
4074
4368
|
}
|
|
4075
4369
|
}
|
|
4076
4370
|
|
|
4077
4371
|
// flatMap block body fallback: compile JSX inline when children
|
|
4078
|
-
// couldn't be extracted via the standard single-return path.
|
|
4079
|
-
|
|
4372
|
+
// couldn't be extracted via the standard single-return path. A pure
|
|
4373
|
+
// single-`return <call>` projection is NOT taken here — it lowers to
|
|
4374
|
+
// neutral IR below (nested-loop child), which DSL adapters templatize.
|
|
4375
|
+
if (method === 'flatMap' && children.length === 0 && !flatMapProjectionCall(body)) {
|
|
4080
4376
|
flatMapCallback = buildFlatMapCallback(callback, body, ctx)
|
|
4081
4377
|
}
|
|
4082
4378
|
} else {
|
|
4083
4379
|
tryTransformRenderableBody(body)
|
|
4084
4380
|
}
|
|
4085
4381
|
|
|
4382
|
+
// flatMap PROJECTION body — the canonical `flatMap(it => it.tags.map(tag
|
|
4383
|
+
// => <li key={...}/>))`, as an expression body or a single-`return` block
|
|
4384
|
+
// (parenthesized or not). This is a pure nested-loop projection, so it
|
|
4385
|
+
// lowers to NEUTRAL IR — an inner IRLoop as the loop's only child — which
|
|
4386
|
+
// every SSR adapter can templatize (nested `{{range}}` on DSL backends),
|
|
4387
|
+
// per spec/callback-fidelity.md's fidelity table ("single expr" row) and
|
|
4388
|
+
// the array-literal flatMap precedent. Only bodies carrying STATEMENTS
|
|
4389
|
+
// (early returns, consts) fall through to the segments carrier below and
|
|
4390
|
+
// its DSL gate. The client reconciles the flattened leaves through the
|
|
4391
|
+
// descriptor mapArray path, synthesized from this same neutral IR
|
|
4392
|
+
// (`renderFlatMapProjectionClientBody`); the leaf `key` renders as the
|
|
4393
|
+
// usual nested-loop `data-key-1` on both the SSR and client string
|
|
4394
|
+
// sides, and the flattened reconciliation identity (`data-key`) is
|
|
4395
|
+
// stamped by mapArray from the inner loop's `key` field.
|
|
4396
|
+
if (method === 'flatMap' && children.length === 0 && !flatMapCallback) {
|
|
4397
|
+
const projection = flatMapProjectionCall(body)
|
|
4398
|
+
if (projection) {
|
|
4399
|
+
const transformed = transformJsxExpression(projection, ctx, isClientOnly)
|
|
4400
|
+
if (transformed && transformed.type === 'loop') {
|
|
4401
|
+
children = [transformed]
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
4404
|
+
}
|
|
4405
|
+
|
|
4406
|
+
// flatMap EXPRESSION body containing JSX in a non-projection shape —
|
|
4407
|
+
// e.g. a call wrapping JSX. The block-body form already rides the
|
|
4408
|
+
// structured-segments carrier in the isBlock branch above; an unbraced
|
|
4409
|
+
// body is the same shape minus the braces, so route it through the same
|
|
4410
|
+
// door. Without this the dispatch falls to the IRExpression scalar path,
|
|
4411
|
+
// which splices the raw callback — JSX included — verbatim into the
|
|
4412
|
+
// client bundle: a silent SyntaxError that kills the whole component's
|
|
4413
|
+
// hydration.
|
|
4414
|
+
if (method === 'flatMap' && children.length === 0 && !flatMapCallback && !ts.isBlock(body)) {
|
|
4415
|
+
flatMapCallback = buildFlatMapCallback(callback, body, ctx)
|
|
4416
|
+
}
|
|
4417
|
+
|
|
4418
|
+
// A flatMapCallback carries the WHOLE body (statements included) as
|
|
4419
|
+
// structured segments — the generic statements-before-return `preamble`
|
|
4420
|
+
// the block-body path may have collected above is a duplicate carrier.
|
|
4421
|
+
// Worse, it isn't one: spliced into a mapArray renderItem it re-runs the
|
|
4422
|
+
// body's control flow against the wrong binding shape (the audited
|
|
4423
|
+
// `if (t().tags.length > maxTags) return [];` renderItem residue).
|
|
4424
|
+
// The segments carrier is the single door; drop the shadow copy.
|
|
4425
|
+
if (flatMapCallback) preamble = undefined
|
|
4426
|
+
|
|
4427
|
+
// Adapter gate (spec/callback-fidelity.md fidelity model): a flatMap body
|
|
4428
|
+
// carried as structured segments runs verbatim on a JS runtime; a DSL
|
|
4429
|
+
// template runtime cannot execute it at SSR — pre-gate, e.g. the Go
|
|
4430
|
+
// adapter emitted `{{range …}}{{end}}` with an EMPTY body (silent
|
|
4431
|
+
// divergence, the exact failure class the sound-or-loud invariant
|
|
4432
|
+
// forbids). Refuse loudly with the /* @client */ escape, mirroring the
|
|
4433
|
+
// const-preamble and array-builder gates above.
|
|
4434
|
+
if (flatMapCallback && !isClientOnly && !(ctx.analyzer.acceptsCallbackBody?.('flatMap') ?? false)) {
|
|
4435
|
+
ctx.analyzer.errors.push(
|
|
4436
|
+
createError(
|
|
4437
|
+
ErrorCodes.UNSUPPORTED_JSX_PATTERN,
|
|
4438
|
+
getSourceLocation(body, ctx.sourceFile, ctx.filePath),
|
|
4439
|
+
{
|
|
4440
|
+
message:
|
|
4441
|
+
'A .flatMap() callback body with statements or a nested projection ' +
|
|
4442
|
+
'cannot be lowered to a template on this backend.',
|
|
4443
|
+
suggestion: {
|
|
4444
|
+
message: 'Add /* @client */ to render this loop on the client only',
|
|
4445
|
+
},
|
|
4446
|
+
}
|
|
4447
|
+
)
|
|
4448
|
+
)
|
|
4449
|
+
}
|
|
4450
|
+
|
|
4086
4451
|
// Unregister loop params
|
|
4087
4452
|
if (paramBindings) {
|
|
4088
4453
|
for (const b of paramBindings) ctx.loopParams.delete(b.name)
|
|
@@ -4097,6 +4462,43 @@ function transformMapCall(
|
|
|
4097
4462
|
// fall back to treating the entire expression as an IRExpression — unless
|
|
4098
4463
|
// flatMap already built a compiled callback (flatMapCallback).
|
|
4099
4464
|
if (children.length === 0 && !flatMapCallback) {
|
|
4465
|
+
// Structural net (sound-or-loud, spec/callback-fidelity.md): a callback
|
|
4466
|
+
// body that carries an INLINE JSX literal but produced no loop lowering
|
|
4467
|
+
// must never fall through to the IRExpression scalar path — `ctx.getJS`
|
|
4468
|
+
// strips types, not JSX, so the raw `<li …>` would splice verbatim into
|
|
4469
|
+
// the emitted client bundle as a silent SyntaxError. Any recognizer gap
|
|
4470
|
+
// for a JSX-bearing shape becomes a loud diagnostic here instead of a
|
|
4471
|
+
// leak. (A JSX-helper CALL — `map(t => renderItem(t))` — carries no
|
|
4472
|
+
// inline JSX literal and legitimately stays on the reactive-text path.)
|
|
4473
|
+
const cb = node.arguments[0]
|
|
4474
|
+
const cbBody = cb && (ts.isArrowFunction(cb) || ts.isFunctionExpression(cb)) ? cb.body : undefined
|
|
4475
|
+
// Entry-count gate: a more specific refusal already fired for THIS call
|
|
4476
|
+
// (e.g. the flatMap leaf-wiring check) — don't stack the generic message
|
|
4477
|
+
// on top of it. Comparing against the count captured at entry (not
|
|
4478
|
+
// `=== 0`) keeps the net armed when an unrelated diagnostic was recorded
|
|
4479
|
+
// earlier in the file — a warning elsewhere must never silence the leak
|
|
4480
|
+
// guard for this callback.
|
|
4481
|
+
if (cbBody && containsJsxInExpression(cbBody) && ctx.analyzer.errors.length === diagCountAtEntry) {
|
|
4482
|
+
ctx.analyzer.errors.push(
|
|
4483
|
+
createError(
|
|
4484
|
+
ErrorCodes.UNSUPPORTED_JSX_PATTERN,
|
|
4485
|
+
getSourceLocation(cbBody, ctx.sourceFile, ctx.filePath),
|
|
4486
|
+
{
|
|
4487
|
+
message:
|
|
4488
|
+
`A .${method}() callback that builds JSX in this shape cannot be ` +
|
|
4489
|
+
'compiled — the JSX would leak verbatim into the client bundle. ' +
|
|
4490
|
+
'Recognized bodies: a JSX element/fragment, a ternary or ' +
|
|
4491
|
+
'&& / || / ?? expression, an array literal (flatMap), or a block ' +
|
|
4492
|
+
'body whose return the compiler can lower.',
|
|
4493
|
+
suggestion: {
|
|
4494
|
+
message:
|
|
4495
|
+
'Restructure the callback to return the JSX element directly ' +
|
|
4496
|
+
'(or via a block body with a plain `return`).',
|
|
4497
|
+
},
|
|
4498
|
+
}
|
|
4499
|
+
)
|
|
4500
|
+
)
|
|
4501
|
+
}
|
|
4100
4502
|
return null
|
|
4101
4503
|
}
|
|
4102
4504
|
|
|
@@ -4122,6 +4524,70 @@ function transformMapCall(
|
|
|
4122
4524
|
? extractItemConditionalKey(itemConditional!)
|
|
4123
4525
|
: (children.length > 0 ? extractLoopKey(children[0]) : null)
|
|
4124
4526
|
|
|
4527
|
+
// Stage 3 / D5 (spec/callback-fidelity.md) — the keyFn is hoisted: `mapArray`
|
|
4528
|
+
// computes it from the raw item BEFORE the callback body runs, so the key must
|
|
4529
|
+
// be derivable from the item (and index), never from a value the preamble
|
|
4530
|
+
// computes. A key that reads a preamble-declared local would compile to an
|
|
4531
|
+
// unbound keyFn; refuse instead.
|
|
4532
|
+
const declaredNameSet = preamble && preamble.declaredNames.length > 0
|
|
4533
|
+
? new Set(preamble.declaredNames)
|
|
4534
|
+
: undefined
|
|
4535
|
+
if (key && declaredNameSet) {
|
|
4536
|
+
const keyRefs = extractFreeIdentifiersFromText(key)
|
|
4537
|
+
const usesLocal = [...keyRefs].some((r) => declaredNameSet.has(r))
|
|
4538
|
+
if (usesLocal) {
|
|
4539
|
+
ctx.analyzer.errors.push(
|
|
4540
|
+
createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN, getSourceLocation(node, ctx.sourceFile, ctx.filePath), {
|
|
4541
|
+
message:
|
|
4542
|
+
'A .map() loop key must be derivable from the loop item — it is ' +
|
|
4543
|
+
'evaluated before the callback body runs, so it cannot reference a ' +
|
|
4544
|
+
'value computed in the callback preamble.',
|
|
4545
|
+
suggestion: {
|
|
4546
|
+
message: 'Derive the key directly from the loop item (e.g. key={item.id}).',
|
|
4547
|
+
},
|
|
4548
|
+
})
|
|
4549
|
+
)
|
|
4550
|
+
}
|
|
4551
|
+
}
|
|
4552
|
+
|
|
4553
|
+
// Stage 3 / D4 — flag element-array children ({out}) built by the preamble so
|
|
4554
|
+
// Phase-2 string emission joins them instead of `String([])`-collapsing them.
|
|
4555
|
+
// Scoped to builderNames (leaf accumulators), NOT all declared locals — a
|
|
4556
|
+
// value-only `{label}` keeps its historical plain interpolation.
|
|
4557
|
+
if (preamble && preamble.builderNames.length > 0) {
|
|
4558
|
+
flagArrayChildExpressions(children, new Set(preamble.builderNames))
|
|
4559
|
+
}
|
|
4560
|
+
|
|
4561
|
+
// Stage 3 root cure — a JSX-building preamble feeding a COMPONENT root is
|
|
4562
|
+
// refused: the component loop's rows are createComponent-driven ('dom-ops'
|
|
4563
|
+
// row construction), and a lowered HTML-string leaf passed as a prop would
|
|
4564
|
+
// silently diverge from SSR, where the same prop is a real JSX element.
|
|
4565
|
+
if (
|
|
4566
|
+
preamble &&
|
|
4567
|
+
preamble.builderNames.length > 0 &&
|
|
4568
|
+
children.length === 1 &&
|
|
4569
|
+
children[0].type === 'component'
|
|
4570
|
+
) {
|
|
4571
|
+
ctx.analyzer.errors.push(
|
|
4572
|
+
createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN, getSourceLocation(node, ctx.sourceFile, ctx.filePath), {
|
|
4573
|
+
message:
|
|
4574
|
+
'A .map() callback that builds JSX in its preamble cannot return a ' +
|
|
4575
|
+
'component: the built elements would reach the component as raw HTML ' +
|
|
4576
|
+
'strings on the client but as JSX elements at SSR (a silent divergence).',
|
|
4577
|
+
suggestion: {
|
|
4578
|
+
message:
|
|
4579
|
+
'Return a plain element root that embeds the array, or move the ' +
|
|
4580
|
+
'building logic inside the component.',
|
|
4581
|
+
},
|
|
4582
|
+
})
|
|
4583
|
+
)
|
|
4584
|
+
// Refused = not carried (the old "do NOT collect" principle): codegen
|
|
4585
|
+
// still runs on an errored compile, and the dom-ops backstop invariant
|
|
4586
|
+
// must stay a FUTURE-variant tripwire, not re-fire on shapes Phase 1
|
|
4587
|
+
// already refused loudly.
|
|
4588
|
+
preamble = undefined
|
|
4589
|
+
}
|
|
4590
|
+
|
|
4125
4591
|
// Extract childComponent info if the loop body is a single component
|
|
4126
4592
|
// This enables createComponent-based rendering with proper prop passing
|
|
4127
4593
|
let childComponent: IRLoopChildComponent | undefined
|
|
@@ -4181,6 +4647,17 @@ function transformMapCall(
|
|
|
4181
4647
|
// already handles correctly.
|
|
4182
4648
|
&& !objectIteration
|
|
4183
4649
|
|
|
4650
|
+
// #2389 patch-on-update — a signal-backed (non-static) loop whose body has
|
|
4651
|
+
// a preamble may reference a preamble-declared local directly as a child
|
|
4652
|
+
// expression (`{cells}`, `{stateLabel}`). `mapArray` reuses the same DOM
|
|
4653
|
+
// node on a same-key update, re-running only the wired text/attr slots —
|
|
4654
|
+
// this child has neither, so without a dedicated region-patch effect it
|
|
4655
|
+
// freezes at its mount-time value. Static arrays are never recreated via
|
|
4656
|
+
// signals, so there is nothing to patch; skip them entirely.
|
|
4657
|
+
const preambleRegions = preamble && !isStaticArray
|
|
4658
|
+
? collectPreambleRegions(children, new Set(preamble.declaredNames), ctx)
|
|
4659
|
+
: undefined
|
|
4660
|
+
|
|
4184
4661
|
// Collect nested components for both static and dynamic arrays.
|
|
4185
4662
|
// Static arrays: needed for initChild hydration.
|
|
4186
4663
|
// Dynamic arrays with native root + component descendants: enables reconcileElements
|
|
@@ -4222,11 +4699,10 @@ function transformMapCall(
|
|
|
4222
4699
|
objectIteration,
|
|
4223
4700
|
depth,
|
|
4224
4701
|
clientOnly: isClientOnly || undefined,
|
|
4225
|
-
|
|
4226
|
-
|
|
4702
|
+
preamble,
|
|
4703
|
+
preambleRegions: preambleRegions && preambleRegions.length > 0 ? preambleRegions : undefined,
|
|
4227
4704
|
paramType,
|
|
4228
4705
|
indexType,
|
|
4229
|
-
typedMapPreamble,
|
|
4230
4706
|
paramBindings,
|
|
4231
4707
|
arrayFreeIdentifiers: extractFreeIdentifiersFromNode(arrayExpr),
|
|
4232
4708
|
flatMapCallback,
|
|
@@ -4269,67 +4745,483 @@ function containsJsx(node: ts.Node): boolean {
|
|
|
4269
4745
|
}
|
|
4270
4746
|
|
|
4271
4747
|
/**
|
|
4272
|
-
* Build a FlatMapCallback for complex flatMap
|
|
4273
|
-
*
|
|
4274
|
-
*
|
|
4275
|
-
*
|
|
4748
|
+
* Build a FlatMapCallback for complex flatMap bodies (conditional returns,
|
|
4749
|
+
* variable-assigned JSX, etc.). Accepts a block body OR an expression body
|
|
4750
|
+
* (`t => t.tags.map(tag => <li/>)`) — segment reconstruction and the SSR
|
|
4751
|
+
* rawBody are position-based, so both shapes flow through identically (a
|
|
4752
|
+
* block's text keeps its braces; an expression's text is a valid arrow body
|
|
4753
|
+
* as-is). Walks the callback body AST and carries it as structured segments
|
|
4754
|
+
* — JS text (types stripped) interleaved with compiled JSX-leaf IR — never
|
|
4755
|
+
* as a sentinel-bearing string (the write-side rule in CLAUDE.md; same shape
|
|
4756
|
+
* as the map-preamble carrier).
|
|
4276
4757
|
*/
|
|
4277
4758
|
function buildFlatMapCallback(
|
|
4278
4759
|
callback: ts.ArrowFunction | ts.FunctionExpression,
|
|
4279
|
-
body: ts.Block,
|
|
4760
|
+
body: ts.Block | ts.Expression,
|
|
4280
4761
|
ctx: TransformContext,
|
|
4281
4762
|
): FlatMapCallback | undefined {
|
|
4282
4763
|
if (!containsJsx(body)) return undefined
|
|
4283
4764
|
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
const
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
const
|
|
4292
|
-
function collectJsx(n: ts.Node): void {
|
|
4765
|
+
// Collect all JSX leaves — don't descend into a JSX node (transformNode
|
|
4766
|
+
// compiles its interior). A leaf under a template literal is refused (a
|
|
4767
|
+
// segment boundary there would split the literal's lexical state), same as
|
|
4768
|
+
// the map-preamble collector.
|
|
4769
|
+
const leafSpans: Array<{ start: number; end: number }> = []
|
|
4770
|
+
const leafIrs: IRNode[] = []
|
|
4771
|
+
let refusalNode: ts.Node | undefined
|
|
4772
|
+
const collectJsx = (n: ts.Node, underTemplate: boolean): void => {
|
|
4293
4773
|
if (ts.isJsxElement(n) || ts.isJsxSelfClosingElement(n) || ts.isJsxFragment(n)) {
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
})
|
|
4774
|
+
if (underTemplate) refusalNode ??= n
|
|
4775
|
+
leafSpans.push({ start: n.getStart(ctx.sourceFile), end: n.getEnd() })
|
|
4776
|
+
const ir = transformNode(n as ts.Expression, ctx)
|
|
4777
|
+
leafIrs.push(ir ?? { type: 'text', value: '', loc: getSourceLocation(n, ctx.sourceFile, ctx.filePath) })
|
|
4299
4778
|
return
|
|
4300
4779
|
}
|
|
4301
|
-
n.
|
|
4780
|
+
const inTemplate = underTemplate || ts.isTemplateExpression(n) || ts.isTaggedTemplateExpression(n)
|
|
4781
|
+
n.forEachChild((c) => collectJsx(c, inTemplate))
|
|
4302
4782
|
}
|
|
4303
|
-
collectJsx(body)
|
|
4304
|
-
|
|
4305
|
-
if (jsxNodes.length === 0) return undefined
|
|
4783
|
+
collectJsx(body, false)
|
|
4306
4784
|
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4785
|
+
if (leafSpans.length === 0) return undefined
|
|
4786
|
+
if (refusalNode) {
|
|
4787
|
+
ctx.analyzer.errors.push(
|
|
4788
|
+
createError(
|
|
4789
|
+
ErrorCodes.UNSUPPORTED_JSX_PATTERN,
|
|
4790
|
+
getSourceLocation(refusalNode, ctx.sourceFile, ctx.filePath),
|
|
4791
|
+
{
|
|
4792
|
+
message:
|
|
4793
|
+
'A JSX element inside a template literal in a .flatMap() callback ' +
|
|
4794
|
+
'body cannot be compiled.',
|
|
4795
|
+
suggestion: { message: 'Build the element outside the template literal.' },
|
|
4796
|
+
}
|
|
4797
|
+
)
|
|
4798
|
+
)
|
|
4799
|
+
return undefined
|
|
4800
|
+
}
|
|
4315
4801
|
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4802
|
+
// A leaf that carries wiring the descriptor render path can't honour —
|
|
4803
|
+
// an event handler, a component, a nested loop, or a spread — would be
|
|
4804
|
+
// silently dead DOM on the client (the leaf renders as a keyed HTML
|
|
4805
|
+
// string, patched wholesale on change, with no per-slot wiring). Refuse
|
|
4806
|
+
// loudly (sound-or-loud), mirroring `preambleFragmentNeedsWiring` for map
|
|
4807
|
+
// preambles — but deliberately NOT refusing reactive expressions: whole-
|
|
4808
|
+
// leaf patching covers those.
|
|
4809
|
+
for (const leafIr of leafIrs) {
|
|
4810
|
+
// The descriptor path renders each leaf as ONE keyed element — the
|
|
4811
|
+
// renderItem adopts `template.content.firstElementChild` and `patchLeaf`
|
|
4812
|
+
// patches a single element root. A fragment (or any non-element) root
|
|
4813
|
+
// would silently drop siblings client-side while SSR renders them all —
|
|
4814
|
+
// the exact divergence class this carrier forbids. Refuse loudly.
|
|
4815
|
+
if (leafIr.type !== 'element') {
|
|
4816
|
+
const loc = 'loc' in leafIr && leafIr.loc
|
|
4817
|
+
? leafIr.loc
|
|
4818
|
+
: getSourceLocation(body, ctx.sourceFile, ctx.filePath)
|
|
4819
|
+
ctx.analyzer.errors.push(
|
|
4820
|
+
createError(
|
|
4821
|
+
ErrorCodes.UNSUPPORTED_JSX_PATTERN,
|
|
4822
|
+
loc,
|
|
4823
|
+
{
|
|
4824
|
+
message:
|
|
4825
|
+
'A JSX leaf produced by a .flatMap() callback must be a single ' +
|
|
4826
|
+
'element — a fragment or non-element root cannot ride the keyed ' +
|
|
4827
|
+
'descriptor path (each leaf hydrates and patches as one element).',
|
|
4828
|
+
suggestion: {
|
|
4829
|
+
message: 'Wrap the leaf content in a single keyed element.',
|
|
4830
|
+
},
|
|
4831
|
+
}
|
|
4832
|
+
)
|
|
4833
|
+
)
|
|
4834
|
+
return undefined
|
|
4835
|
+
}
|
|
4836
|
+
if (flatMapLeafNeedsWiring(leafIr)) {
|
|
4837
|
+
const loc = 'loc' in leafIr && leafIr.loc
|
|
4838
|
+
? leafIr.loc
|
|
4839
|
+
: getSourceLocation(body, ctx.sourceFile, ctx.filePath)
|
|
4840
|
+
ctx.analyzer.errors.push(
|
|
4841
|
+
createError(
|
|
4842
|
+
ErrorCodes.UNSUPPORTED_JSX_PATTERN,
|
|
4843
|
+
loc,
|
|
4844
|
+
{
|
|
4845
|
+
message:
|
|
4846
|
+
'A JSX element produced by a .flatMap() callback cannot carry ' +
|
|
4847
|
+
'event handlers, components, nested loops, or spreads — the ' +
|
|
4848
|
+
'leaf renders as a keyed HTML string with no per-element wiring.',
|
|
4849
|
+
suggestion: {
|
|
4850
|
+
message:
|
|
4851
|
+
'Restructure so the interactive element lives in a .map() body — ' +
|
|
4852
|
+
'the descriptor path has no per-element wiring on any backend, ' +
|
|
4853
|
+
'so /* @client */ does not lift this.',
|
|
4854
|
+
},
|
|
4855
|
+
}
|
|
4856
|
+
)
|
|
4857
|
+
)
|
|
4858
|
+
return undefined
|
|
4859
|
+
}
|
|
4321
4860
|
}
|
|
4322
|
-
compiledBody += bodyText.slice(lastEnd)
|
|
4323
4861
|
|
|
4324
|
-
|
|
4862
|
+
const pieces = reconstructAsSegments(body, ctx.sourceFile, ctx.analyzer.typeExcludeRanges, leafSpans)
|
|
4863
|
+
const segments: PreambleSegment[] = pieces.map((piece) => {
|
|
4864
|
+
if ('marker' in piece) return { kind: 'jsx', ir: leafIrs[piece.marker] }
|
|
4865
|
+
// Template variant per segment, like buildPreambleSegments: the hydrate
|
|
4866
|
+
// registration template is module scope (`template: (_p) => ...`), so a
|
|
4867
|
+
// bare destructured-prop reference must rewrite to `_p.xxx` there —
|
|
4868
|
+
// `applyPropsRewrite` can't do it (destructured components have no props
|
|
4869
|
+
// object name).
|
|
4870
|
+
const tpl = rewriteBarePropRefs(piece.js, body, ctx)
|
|
4871
|
+
return tpl !== undefined && tpl !== piece.js
|
|
4872
|
+
? { kind: 'js', text: piece.js, templateText: tpl }
|
|
4873
|
+
: { kind: 'js', text: piece.js }
|
|
4874
|
+
})
|
|
4875
|
+
|
|
4325
4876
|
const paramsText = callback.parameters.map(p => p.getText(ctx.sourceFile)).join(', ')
|
|
4326
4877
|
|
|
4327
4878
|
return {
|
|
4328
4879
|
params: `(${paramsText})`,
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4880
|
+
segments,
|
|
4881
|
+
rawBody: tsxSourceText(body.getText(ctx.sourceFile)),
|
|
4882
|
+
}
|
|
4883
|
+
}
|
|
4884
|
+
|
|
4885
|
+
/**
|
|
4886
|
+
* Stage 3 / D4 (spec/callback-fidelity.md) — the collected JSX-bearing preamble
|
|
4887
|
+
* of an arbitrary `.map()` callback (an imperative array-builder), structured.
|
|
4888
|
+
*/
|
|
4889
|
+
interface PreambleCollection {
|
|
4890
|
+
preamble: MapCallbackPreamble
|
|
4891
|
+
/** Set when a leaf carries wiring the verbatim path can't render (D-E). */
|
|
4892
|
+
refusalNode?: ts.Node
|
|
4893
|
+
}
|
|
4894
|
+
|
|
4895
|
+
/**
|
|
4896
|
+
* Does a flatMap segment leaf carry wiring the descriptor render path can't
|
|
4897
|
+
* honour? Narrower than {@link preambleFragmentNeedsWiring}: reactive
|
|
4898
|
+
* expressions are ALLOWED (the client renders each leaf as a keyed
|
|
4899
|
+
* `{ k, h }` descriptor and patches the whole leaf when its HTML changes),
|
|
4900
|
+
* but events, components, nested loops, and spreads have no wiring on that
|
|
4901
|
+
* path and would be silently dead.
|
|
4902
|
+
*/
|
|
4903
|
+
function flatMapLeafNeedsWiring(ir: IRNode): boolean {
|
|
4904
|
+
switch (ir.type) {
|
|
4905
|
+
case 'component':
|
|
4906
|
+
case 'loop':
|
|
4907
|
+
return true
|
|
4908
|
+
case 'element':
|
|
4909
|
+
if (ir.events.length > 0) return true
|
|
4910
|
+
if (ir.attrs.some((a) => a.name.startsWith('...'))) return true
|
|
4911
|
+
return ir.children.some(flatMapLeafNeedsWiring)
|
|
4912
|
+
case 'conditional':
|
|
4913
|
+
return (
|
|
4914
|
+
flatMapLeafNeedsWiring(ir.whenTrue) ||
|
|
4915
|
+
(ir.whenFalse ? flatMapLeafNeedsWiring(ir.whenFalse) : false)
|
|
4916
|
+
)
|
|
4917
|
+
case 'fragment':
|
|
4918
|
+
return ir.children.some(flatMapLeafNeedsWiring)
|
|
4919
|
+
default:
|
|
4920
|
+
return false
|
|
4921
|
+
}
|
|
4922
|
+
}
|
|
4923
|
+
|
|
4924
|
+
/**
|
|
4925
|
+
* Does a compiled preamble JSX leaf carry wiring the verbatim render path can't
|
|
4926
|
+
* honour? The path builds each leaf once as an interpolated HTML string with no
|
|
4927
|
+
* reactive effects, event listeners, or child reconciliation — so an event
|
|
4928
|
+
* handler, a component, a nested loop, a reactive slot, or a spread would be
|
|
4929
|
+
* silently dropped. Refuse instead (D-E), never diverge silently.
|
|
4930
|
+
*/
|
|
4931
|
+
function preambleFragmentNeedsWiring(ir: IRNode): boolean {
|
|
4932
|
+
switch (ir.type) {
|
|
4933
|
+
case 'component':
|
|
4934
|
+
case 'loop':
|
|
4935
|
+
return true
|
|
4936
|
+
case 'element':
|
|
4937
|
+
if (ir.events.length > 0) return true
|
|
4938
|
+
// JSX spread (`<x {...rest} />`) keeps `name === '...'` as its marker.
|
|
4939
|
+
if (ir.attrs.some((a) => a.name.startsWith('...'))) return true
|
|
4940
|
+
return ir.children.some(preambleFragmentNeedsWiring)
|
|
4941
|
+
case 'expression':
|
|
4942
|
+
return ir.reactive === true
|
|
4943
|
+
case 'conditional':
|
|
4944
|
+
return (
|
|
4945
|
+
preambleFragmentNeedsWiring(ir.whenTrue) ||
|
|
4946
|
+
(ir.whenFalse ? preambleFragmentNeedsWiring(ir.whenFalse) : false)
|
|
4947
|
+
)
|
|
4948
|
+
case 'fragment':
|
|
4949
|
+
return ir.children.some(preambleFragmentNeedsWiring)
|
|
4950
|
+
default:
|
|
4951
|
+
return false
|
|
4952
|
+
}
|
|
4953
|
+
}
|
|
4954
|
+
|
|
4955
|
+
/**
|
|
4956
|
+
* Stage 3 / D4 — walk loop-body children and flag each `IRExpression` child
|
|
4957
|
+
* whose free identifiers intersect the preamble-declared names (e.g. `{out}`).
|
|
4958
|
+
* Phase-2 string emission then joins the array instead of `String([])`-collapsing
|
|
4959
|
+
* it. JSX SSR adapters ignore the flag (their JSX runtime renders arrays).
|
|
4960
|
+
*/
|
|
4961
|
+
function flagArrayChildExpressions(nodes: IRNode[], declared: ReadonlySet<string>): void {
|
|
4962
|
+
for (const node of nodes) {
|
|
4963
|
+
switch (node.type) {
|
|
4964
|
+
case 'expression': {
|
|
4965
|
+
// Only the bare `{out}` shape — the whole expression is a single
|
|
4966
|
+
// identifier that is itself a preamble-declared array. A broader
|
|
4967
|
+
// expression (`{out.length}`, `{f(out)}`) must NOT get the array-join
|
|
4968
|
+
// coercion: it would change the value, and the join ternary reads the
|
|
4969
|
+
// expression twice, so a call could double-fire its side effects.
|
|
4970
|
+
const name = node.expr.trim()
|
|
4971
|
+
const refs = extractFreeIdentifiersFromText(node.expr)
|
|
4972
|
+
if (refs.size === 1 && refs.has(name) && declared.has(name)) {
|
|
4973
|
+
node.joinArrayChild = true
|
|
4974
|
+
}
|
|
4975
|
+
break
|
|
4976
|
+
}
|
|
4977
|
+
case 'element':
|
|
4978
|
+
case 'fragment':
|
|
4979
|
+
flagArrayChildExpressions(node.children, declared)
|
|
4980
|
+
break
|
|
4981
|
+
case 'conditional':
|
|
4982
|
+
flagArrayChildExpressions(
|
|
4983
|
+
[node.whenTrue, ...(node.whenFalse ? [node.whenFalse] : [])],
|
|
4984
|
+
declared,
|
|
4985
|
+
)
|
|
4986
|
+
break
|
|
4987
|
+
}
|
|
4988
|
+
}
|
|
4989
|
+
}
|
|
4990
|
+
|
|
4991
|
+
/**
|
|
4992
|
+
* #2389 patch-on-update — walk loop-body children and classify each
|
|
4993
|
+
* `IRExpression` whose free identifiers intersect `declared` (the enclosing
|
|
4994
|
+
* `.map()` preamble's `declaredNames`) as a preamble-patched region. Unlike
|
|
4995
|
+
* {@link flagArrayChildExpressions} (which only recognizes the exact bare
|
|
4996
|
+
* `{out}` shape for the array-join coercion), this is deliberately broader —
|
|
4997
|
+
* ANY expression reading a preamble local needs the region-patch effect,
|
|
4998
|
+
* whether it's a builder array (`{cells}`, `joinArrayChild` already set by
|
|
4999
|
+
* `flagArrayChildExpressions`, called earlier for the same `children`) or a
|
|
5000
|
+
* plain value (`{stateLabel}`).
|
|
5001
|
+
*
|
|
5002
|
+
* Reuses an existing `slotId` when the node already has one (e.g. it also
|
|
5003
|
+
* reads the loop param and so already qualified via the ordinary reactive-
|
|
5004
|
+
* text path) rather than allocating a second slot — `node.preambleRegion`
|
|
5005
|
+
* is the single source of truth downstream, so `collectLoopChildReactiveTexts`
|
|
5006
|
+
* can unconditionally defer to it.
|
|
5007
|
+
*/
|
|
5008
|
+
function collectPreambleRegions(
|
|
5009
|
+
nodes: IRNode[],
|
|
5010
|
+
declared: ReadonlySet<string>,
|
|
5011
|
+
ctx: TransformContext,
|
|
5012
|
+
): PreambleRegionSource[] {
|
|
5013
|
+
const regions: PreambleRegionSource[] = []
|
|
5014
|
+
const visit = (list: IRNode[]): void => {
|
|
5015
|
+
for (const node of list) {
|
|
5016
|
+
switch (node.type) {
|
|
5017
|
+
case 'expression': {
|
|
5018
|
+
const refs = extractFreeIdentifiersFromText(node.expr)
|
|
5019
|
+
const usesPreambleLocal = [...refs].some((r) => declared.has(r))
|
|
5020
|
+
if (usesPreambleLocal) {
|
|
5021
|
+
if (!node.slotId) node.slotId = generateSlotId(ctx)
|
|
5022
|
+
node.preambleRegion = true
|
|
5023
|
+
node.reactive = true
|
|
5024
|
+
regions.push({
|
|
5025
|
+
slotId: node.slotId,
|
|
5026
|
+
expr: node.expr,
|
|
5027
|
+
joinArrayChild: node.joinArrayChild || undefined,
|
|
5028
|
+
})
|
|
5029
|
+
}
|
|
5030
|
+
break
|
|
5031
|
+
}
|
|
5032
|
+
case 'element':
|
|
5033
|
+
case 'fragment':
|
|
5034
|
+
visit(node.children)
|
|
5035
|
+
break
|
|
5036
|
+
case 'conditional':
|
|
5037
|
+
visit([node.whenTrue, ...(node.whenFalse ? [node.whenFalse] : [])])
|
|
5038
|
+
break
|
|
5039
|
+
}
|
|
5040
|
+
}
|
|
5041
|
+
}
|
|
5042
|
+
visit(nodes)
|
|
5043
|
+
return regions
|
|
5044
|
+
}
|
|
5045
|
+
|
|
5046
|
+
/**
|
|
5047
|
+
* Collect the names a binding introduces, recursing through object/array
|
|
5048
|
+
* destructuring patterns (`const { id: k } = r`, `const [k, ...rest] = xs`) so
|
|
5049
|
+
* the D5 key-derivability guard sees every preamble-scoped local — mirrors
|
|
5050
|
+
* `collectParamBindingNames`.
|
|
5051
|
+
*/
|
|
5052
|
+
function collectBindingNames(name: ts.BindingName, out: Set<string>): void {
|
|
5053
|
+
if (ts.isIdentifier(name)) {
|
|
5054
|
+
out.add(name.text)
|
|
5055
|
+
return
|
|
5056
|
+
}
|
|
5057
|
+
// Object / array binding pattern; array holes are OmittedExpression, skipped.
|
|
5058
|
+
for (const el of name.elements) {
|
|
5059
|
+
if (ts.isBindingElement(el)) collectBindingNames(el.name, out)
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
5062
|
+
|
|
5063
|
+
/** Names a preamble statement declares (const/let/var/function). */
|
|
5064
|
+
function collectPreambleDeclaredNames(stmt: ts.Statement, out: Set<string>): void {
|
|
5065
|
+
if (ts.isVariableStatement(stmt)) {
|
|
5066
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
5067
|
+
collectBindingNames(decl.name, out)
|
|
5068
|
+
}
|
|
5069
|
+
} else if (ts.isFunctionDeclaration(stmt) && stmt.name) {
|
|
5070
|
+
out.add(stmt.name.text)
|
|
5071
|
+
}
|
|
5072
|
+
}
|
|
5073
|
+
|
|
5074
|
+
/**
|
|
5075
|
+
* Build a {@link MapCallbackPreamble} from value-only (JSX-free) pre-return
|
|
5076
|
+
* statements — the Stage-2 fold preamble and the plain block-body preamble.
|
|
5077
|
+
* One `js` segment per statement, semicolon-normalized and space-joined
|
|
5078
|
+
* exactly as the former string carriers were.
|
|
5079
|
+
*/
|
|
5080
|
+
function preambleFromValueStatements(
|
|
5081
|
+
statements: readonly ts.Statement[],
|
|
5082
|
+
ctx: TransformContext,
|
|
5083
|
+
): MapCallbackPreamble {
|
|
5084
|
+
const segments: PreambleSegment[] = []
|
|
5085
|
+
const typedParts: string[] = []
|
|
5086
|
+
const declared = new Set<string>()
|
|
5087
|
+
for (const stmt of statements) {
|
|
5088
|
+
collectPreambleDeclaredNames(stmt, declared)
|
|
5089
|
+
const js0 = ctx.getJS(stmt)
|
|
5090
|
+
const tjs0 = ctx.getTemplateJS(stmt)
|
|
5091
|
+
const raw0 = stmt.getText(ctx.sourceFile)
|
|
5092
|
+
const js = (js0.endsWith(';') ? js0 : js0 + ';') + ' '
|
|
5093
|
+
const tjs = (tjs0.endsWith(';') ? tjs0 : tjs0 + ';') + ' '
|
|
5094
|
+
typedParts.push(raw0.endsWith(';') ? raw0 : raw0 + ';')
|
|
5095
|
+
segments.push(tjs !== js ? { kind: 'js', text: js, templateText: tjs } : { kind: 'js', text: js })
|
|
5096
|
+
}
|
|
5097
|
+
return {
|
|
5098
|
+
segments: trimPreambleSegments(segments),
|
|
5099
|
+
ssrText: tsxSourceText(typedParts.join(' ')),
|
|
5100
|
+
declaredNames: [...declared],
|
|
5101
|
+
// Value-only preambles accumulate no JSX, so no child needs the array join.
|
|
5102
|
+
builderNames: [],
|
|
5103
|
+
}
|
|
5104
|
+
}
|
|
5105
|
+
|
|
5106
|
+
/** Drop the trailing separator space from the final js segment. */
|
|
5107
|
+
function trimPreambleSegments(segments: PreambleSegment[]): PreambleSegment[] {
|
|
5108
|
+
const last = segments[segments.length - 1]
|
|
5109
|
+
if (last?.kind === 'js') {
|
|
5110
|
+
const text = last.text.trimEnd()
|
|
5111
|
+
const templateText = last.templateText?.trimEnd()
|
|
5112
|
+
segments[segments.length - 1] = templateText !== undefined
|
|
5113
|
+
? { kind: 'js', text, templateText }
|
|
5114
|
+
: { kind: 'js', text }
|
|
5115
|
+
}
|
|
5116
|
+
return segments
|
|
5117
|
+
}
|
|
5118
|
+
|
|
5119
|
+
/**
|
|
5120
|
+
* Collect an arbitrary `.map()` callback's pre-return statements as structured
|
|
5121
|
+
* segments: JS text between JSX leaves becomes `js` segments (types stripped via
|
|
5122
|
+
* the same span-walk as `reconstructWithoutTypes`), each top-level JSX leaf
|
|
5123
|
+
* becomes a `jsx` segment carrying its compiled IR. No sentinel string ever
|
|
5124
|
+
* exists. Each leaf is checked against {@link preambleFragmentNeedsWiring}; a
|
|
5125
|
+
* leaf inside a template literal is refused (a segment boundary there would
|
|
5126
|
+
* split the literal's lexical state for downstream per-segment text
|
|
5127
|
+
* transforms).
|
|
5128
|
+
*/
|
|
5129
|
+
function buildPreambleSegments(
|
|
5130
|
+
statements: ts.NodeArray<ts.Statement>,
|
|
5131
|
+
returnStmt: ts.Statement | undefined,
|
|
5132
|
+
ctx: TransformContext,
|
|
5133
|
+
): PreambleCollection {
|
|
5134
|
+
const segments: PreambleSegment[] = []
|
|
5135
|
+
const typedParts: string[] = []
|
|
5136
|
+
const declared = new Set<string>()
|
|
5137
|
+
const builders = new Set<string>()
|
|
5138
|
+
let refusalNode: ts.Node | undefined
|
|
5139
|
+
|
|
5140
|
+
// Which local does this leaf accumulate into? The `push`/`unshift` receiver
|
|
5141
|
+
// (`out.push(<td/>)`) or the declaration target of a leaf-bearing
|
|
5142
|
+
// initializer (`const out = xs.map(x => <td/>)`). Only these names get the
|
|
5143
|
+
// `{out}` array-join child emission — a value-only local must keep its
|
|
5144
|
+
// plain interpolation (pinned by client-js-generation.test.ts #520).
|
|
5145
|
+
const recordBuilderTarget = (leaf: ts.Node, stmt: ts.Statement): void => {
|
|
5146
|
+
for (let n: ts.Node | undefined = leaf.parent; n && n !== stmt.parent; n = n.parent) {
|
|
5147
|
+
if (
|
|
5148
|
+
ts.isCallExpression(n) &&
|
|
5149
|
+
ts.isPropertyAccessExpression(n.expression) &&
|
|
5150
|
+
(n.expression.name.text === 'push' || n.expression.name.text === 'unshift') &&
|
|
5151
|
+
ts.isIdentifier(n.expression.expression)
|
|
5152
|
+
) {
|
|
5153
|
+
builders.add(n.expression.expression.text)
|
|
5154
|
+
return
|
|
5155
|
+
}
|
|
5156
|
+
if (ts.isVariableDeclaration(n) && ts.isIdentifier(n.name)) {
|
|
5157
|
+
builders.add(n.name.text)
|
|
5158
|
+
return
|
|
5159
|
+
}
|
|
5160
|
+
}
|
|
5161
|
+
}
|
|
5162
|
+
|
|
5163
|
+
for (const stmt of statements) {
|
|
5164
|
+
if (stmt === returnStmt) break
|
|
5165
|
+
collectPreambleDeclaredNames(stmt, declared)
|
|
5166
|
+
|
|
5167
|
+
// Top-level JSX leaves in this statement — don't descend into a JSX node
|
|
5168
|
+
// (transformNode compiles its interior). Mirrors buildFlatMapCallback.
|
|
5169
|
+
const leafSpans: Array<{ start: number; end: number }> = []
|
|
5170
|
+
const leafIrs: IRNode[] = []
|
|
5171
|
+
const collect = (n: ts.Node, underTemplate: boolean): void => {
|
|
5172
|
+
if (ts.isJsxElement(n) || ts.isJsxSelfClosingElement(n) || ts.isJsxFragment(n)) {
|
|
5173
|
+
if (underTemplate) refusalNode ??= n
|
|
5174
|
+
recordBuilderTarget(n, stmt)
|
|
5175
|
+
leafSpans.push({ start: n.getStart(ctx.sourceFile), end: n.getEnd() })
|
|
5176
|
+
const ir = transformNode(n as ts.Expression, ctx)
|
|
5177
|
+
if (ir && preambleFragmentNeedsWiring(ir)) refusalNode ??= n
|
|
5178
|
+
leafIrs.push(ir ?? { type: 'text', value: '', loc: getSourceLocation(n, ctx.sourceFile, ctx.filePath) })
|
|
5179
|
+
return
|
|
5180
|
+
}
|
|
5181
|
+
const inTemplate = underTemplate || ts.isTemplateExpression(n) || ts.isTaggedTemplateExpression(n)
|
|
5182
|
+
n.forEachChild((c) => collect(c, inTemplate))
|
|
5183
|
+
}
|
|
5184
|
+
collect(stmt, false)
|
|
5185
|
+
|
|
5186
|
+
const raw0 = stmt.getText(ctx.sourceFile)
|
|
5187
|
+
typedParts.push(raw0.endsWith(';') ? raw0 : raw0 + ';')
|
|
5188
|
+
|
|
5189
|
+
if (leafSpans.length === 0) {
|
|
5190
|
+
const js0 = ctx.getJS(stmt)
|
|
5191
|
+
const tjs0 = ctx.getTemplateJS(stmt)
|
|
5192
|
+
const js = (js0.endsWith(';') ? js0 : js0 + ';') + ' '
|
|
5193
|
+
const tjs = (tjs0.endsWith(';') ? tjs0 : tjs0 + ';') + ' '
|
|
5194
|
+
segments.push(tjs !== js ? { kind: 'js', text: js, templateText: tjs } : { kind: 'js', text: js })
|
|
5195
|
+
continue
|
|
5196
|
+
}
|
|
5197
|
+
|
|
5198
|
+
const pieces = reconstructAsSegments(stmt, ctx.sourceFile, ctx.analyzer.typeExcludeRanges, leafSpans)
|
|
5199
|
+
for (const piece of pieces) {
|
|
5200
|
+
if ('marker' in piece) {
|
|
5201
|
+
segments.push({ kind: 'jsx', ir: leafIrs[piece.marker] })
|
|
5202
|
+
} else {
|
|
5203
|
+
// Per-segment template variant: the destructured-prop rewrite is a text
|
|
5204
|
+
// transform; leaf refusal above guarantees segment boundaries never
|
|
5205
|
+
// split a string/template literal's lexical state.
|
|
5206
|
+
const tpl = rewriteBarePropRefs(piece.js, stmt, ctx)
|
|
5207
|
+
segments.push(tpl !== undefined && tpl !== piece.js
|
|
5208
|
+
? { kind: 'js', text: piece.js, templateText: tpl }
|
|
5209
|
+
: { kind: 'js', text: piece.js })
|
|
5210
|
+
}
|
|
5211
|
+
}
|
|
5212
|
+
// Statement separator + semicolon normalization (the raw text may omit `;`).
|
|
5213
|
+
const sep = raw0.endsWith(';') ? ' ' : '; '
|
|
5214
|
+
segments.push({ kind: 'js', text: sep })
|
|
5215
|
+
}
|
|
5216
|
+
|
|
5217
|
+
return {
|
|
5218
|
+
preamble: {
|
|
5219
|
+
segments: trimPreambleSegments(segments),
|
|
5220
|
+
ssrText: tsxSourceText(typedParts.join(' ')),
|
|
5221
|
+
declaredNames: [...declared],
|
|
5222
|
+
builderNames: [...builders],
|
|
5223
|
+
},
|
|
5224
|
+
refusalNode,
|
|
4333
5225
|
}
|
|
4334
5226
|
}
|
|
4335
5227
|
|