@barefootjs/jsx 0.26.1 → 0.26.3
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 +662 -223
- package/dist/ir-to-client-js/build-references.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts +2 -0
- 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/loop.d.ts +12 -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.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.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +19 -1
- package/dist/ir-to-client-js/html-template.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/types.d.ts +4 -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 +96 -32
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +94 -0
- package/src/__tests__/client-js-generation.test.ts +10 -3
- package/src/__tests__/compiler-stress-1244.test.ts +12 -1
- package/src/__tests__/event-delegation-scope-2367.test.ts +108 -0
- package/src/__tests__/flatmap-segments.test.ts +80 -0
- package/src/__tests__/map-arbitrary-body.test.ts +226 -0
- package/src/__tests__/map-body-no-silent-divergence.test.ts +360 -0
- package/src/__tests__/map-multi-return-body.test.ts +172 -0
- 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 +3 -2
- package/src/ir-to-client-js/collect-elements.ts +3 -3
- package/src/ir-to-client-js/control-flow/plan/branch-loop.ts +2 -0
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +7 -2
- 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 +16 -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 +17 -2
- package/src/ir-to-client-js/control-flow/plan/loop.ts +12 -0
- package/src/ir-to-client-js/control-flow/shared.ts +2 -1
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +9 -2
- package/src/ir-to-client-js/control-flow.ts +11 -0
- package/src/ir-to-client-js/html-template.ts +133 -37
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +22 -10
- package/src/ir-to-client-js/types.ts +4 -3
- package/src/jsx-to-ir.ts +690 -164
- 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 +110 -35
|
@@ -253,6 +253,16 @@ export class TestAdapter extends JsxAdapter {
|
|
|
253
253
|
// parsed as a block statement (matches hono-adapter behavior).
|
|
254
254
|
const safeChildren = children.startsWith('{') ? `<>${children}</>` : children
|
|
255
255
|
|
|
256
|
+
// A `.map()` callback preamble (a Stage-2 value-only `const`, or a Stage-3
|
|
257
|
+
// arbitrary array-builder) runs verbatim in the block body. Use the typed
|
|
258
|
+
// carrier so raw JSX leaves stay intact for the JSX runtime; `{out}`-style
|
|
259
|
+
// element-array children render natively. (Without this the preamble was
|
|
260
|
+
// dropped and its identifiers rendered unbound.)
|
|
261
|
+
const preamble = loop.preamble?.ssrText
|
|
262
|
+
if (preamble) {
|
|
263
|
+
return `{${loop.array}.map((${loop.param}${indexParam}) => { ${preamble} return ${safeChildren} })}`
|
|
264
|
+
}
|
|
265
|
+
|
|
256
266
|
return `{${loop.array}.map((${loop.param}${indexParam}) => ${safeChildren})}`
|
|
257
267
|
}
|
|
258
268
|
|
package/src/analyzer-context.ts
CHANGED
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
DeclinedReactiveFactory,
|
|
26
26
|
} from './types.ts'
|
|
27
27
|
import { type ExcludeRange, collectAllTypeRanges, reconstructWithoutTypes } from './strip-types.ts'
|
|
28
|
+
import type { CallbackBodyAcceptor } from './adapters/interface.ts'
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* Deferred info for BF043 (props destructuring warning).
|
|
@@ -199,6 +200,15 @@ export interface AnalyzerContext {
|
|
|
199
200
|
|
|
200
201
|
// Errors
|
|
201
202
|
errors: CompilerError[]
|
|
203
|
+
/**
|
|
204
|
+
* Capability of the adapter this component is being compiled for: can its
|
|
205
|
+
* runtime render an off-subset callback body (`filter`/`sort`/… predicate)
|
|
206
|
+
* verbatim? Set from `TemplateAdapter.acceptsCallbackBody`; undefined for
|
|
207
|
+
* direct analyzer callers (no adapter) and DSL adapters. Consulted at the
|
|
208
|
+
* Phase-1 callback-lowering sites so a JS-runtime target isn't rejected for
|
|
209
|
+
* a body it could run. See `spec/callback-fidelity.md`.
|
|
210
|
+
*/
|
|
211
|
+
acceptsCallbackBody?: CallbackBodyAcceptor
|
|
202
212
|
|
|
203
213
|
// Directive
|
|
204
214
|
hasUseClientDirective: boolean
|
|
@@ -232,11 +242,13 @@ export interface AnalyzerContext {
|
|
|
232
242
|
|
|
233
243
|
export function createAnalyzerContext(
|
|
234
244
|
sourceFile: ts.SourceFile,
|
|
235
|
-
filePath: string
|
|
245
|
+
filePath: string,
|
|
246
|
+
acceptsCallbackBody?: CallbackBodyAcceptor
|
|
236
247
|
): AnalyzerContext {
|
|
237
248
|
return {
|
|
238
249
|
sourceFile,
|
|
239
250
|
filePath,
|
|
251
|
+
acceptsCallbackBody,
|
|
240
252
|
|
|
241
253
|
componentName: null,
|
|
242
254
|
componentNode: null,
|
|
@@ -300,6 +312,22 @@ export function createAnalyzerContext(
|
|
|
300
312
|
} catch {
|
|
301
313
|
ownSourceFile = undefined
|
|
302
314
|
}
|
|
315
|
+
// Test-gated trust-boundary assertion (write-side string rule,
|
|
316
|
+
// CLAUDE.md): getJS output is spliced into emitted artifacts, so a
|
|
317
|
+
// JSX-bearing node here is a leak by construction — mixed content must
|
|
318
|
+
// travel as structured segments, never as raw text. Every getJS call
|
|
319
|
+
// site shares this contract; the env gate keeps the subtree walk off
|
|
320
|
+
// the production hot path (the trichotomy harness enables it). Scoped
|
|
321
|
+
// to error-free compiles: a compile that already refused loudly may
|
|
322
|
+
// take degraded fallback paths whose artifacts are gated by the error —
|
|
323
|
+
// the invariant this trips on is the SILENT leak.
|
|
324
|
+
if (process.env.BF_ASSERT_NO_JSX_IN_GETJS === '1' && this.errors.length === 0 && nodeContainsJsx(node)) {
|
|
325
|
+
throw new Error(
|
|
326
|
+
'getJS() called on a JSX-bearing node — raw JSX must never be spliced ' +
|
|
327
|
+
'into emitted output. Carry mixed content as structured segments ' +
|
|
328
|
+
'(MapCallbackPreamble / FlatMapCallback) instead.'
|
|
329
|
+
)
|
|
330
|
+
}
|
|
303
331
|
if (ownSourceFile && ownSourceFile !== sourceFile) {
|
|
304
332
|
return node.getText(ownSourceFile)
|
|
305
333
|
}
|
|
@@ -308,6 +336,12 @@ export function createAnalyzerContext(
|
|
|
308
336
|
}
|
|
309
337
|
}
|
|
310
338
|
|
|
339
|
+
/** Subtree JSX check for the test-gated getJS assertion above. */
|
|
340
|
+
function nodeContainsJsx(node: ts.Node): boolean {
|
|
341
|
+
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) return true
|
|
342
|
+
return ts.forEachChild(node, nodeContainsJsx) ?? false
|
|
343
|
+
}
|
|
344
|
+
|
|
311
345
|
// =============================================================================
|
|
312
346
|
// Source Location Helper
|
|
313
347
|
// =============================================================================
|
package/src/analyzer.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import ts from 'typescript'
|
|
10
10
|
import type { ImportSpecifier, TypeInfo, ParamInfo, ReactiveFactoryInfo, DeclinedReactiveFactory, RequiredFactoryImport, FactoryRenameSite, SourceLocation } from './types.ts'
|
|
11
11
|
import { parseExpression, parseBlockBodyTolerant, foldBlockToExpr } from './expression-parser.ts'
|
|
12
|
+
import type { CallbackBodyAcceptor } from './adapters/interface.ts'
|
|
12
13
|
import { rewriteBarePropRefs } from './prop-rewrite.ts'
|
|
13
14
|
import { incrementCounter } from './instrumentation.ts'
|
|
14
15
|
import {
|
|
@@ -158,7 +159,8 @@ export function analyzeComponent(
|
|
|
158
159
|
source: string,
|
|
159
160
|
filePath: string,
|
|
160
161
|
targetComponentName?: string,
|
|
161
|
-
program?: ts.Program
|
|
162
|
+
program?: ts.Program,
|
|
163
|
+
acceptsCallbackBody?: CallbackBodyAcceptor
|
|
162
164
|
): AnalyzerContext {
|
|
163
165
|
incrementCounter('filesAnalyzed')
|
|
164
166
|
// Track whether the caller supplied a shared ts.Program. Used downstream
|
|
@@ -229,7 +231,7 @@ export function analyzeComponent(
|
|
|
229
231
|
}
|
|
230
232
|
}
|
|
231
233
|
|
|
232
|
-
const ctx = createAnalyzerContext(sourceFile, filePath)
|
|
234
|
+
const ctx = createAnalyzerContext(sourceFile, filePath, acceptsCallbackBody)
|
|
233
235
|
ctx.checker = checker
|
|
234
236
|
|
|
235
237
|
// Reactive-factory prescan results (#931, #2325 cross-file + object-
|
|
@@ -2077,18 +2079,77 @@ function extractSingleJsxReturn(
|
|
|
2077
2079
|
return jsxReturn
|
|
2078
2080
|
}
|
|
2079
2081
|
|
|
2080
|
-
type JsxReturnNode = ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment
|
|
2082
|
+
export type JsxReturnNode = ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment
|
|
2083
|
+
|
|
2084
|
+
/**
|
|
2085
|
+
* The branch structure extracted from a multi-return JSX body — an if/else-if
|
|
2086
|
+
* chain or a `switch`. Shared by the helper-function fold and (Stage 2 of
|
|
2087
|
+
* `spec/callback-fidelity.md`) the `.map()` callback-body fold.
|
|
2088
|
+
*/
|
|
2089
|
+
export interface MultiReturnJsxBranches {
|
|
2090
|
+
branches: Array<{
|
|
2091
|
+
condition: ts.Expression
|
|
2092
|
+
jsxReturn: JsxReturnNode | null
|
|
2093
|
+
/**
|
|
2094
|
+
* For a `switch` with fallthrough (`case 'a': case 'b': return X`), the
|
|
2095
|
+
* additional case expressions that share this branch's return. The fold
|
|
2096
|
+
* OR-joins them with `condition` into `disc === a || disc === b`. Only set
|
|
2097
|
+
* on switch-sourced branches.
|
|
2098
|
+
*/
|
|
2099
|
+
extraCaseConditions?: ts.Expression[]
|
|
2100
|
+
}>
|
|
2101
|
+
fallback: JsxReturnNode | null
|
|
2102
|
+
switchDiscriminant?: ts.Expression
|
|
2103
|
+
/**
|
|
2104
|
+
* Leading `const` / `let` declarations before the branches, collected only
|
|
2105
|
+
* when the caller passes `allowPreamble` (the `.map()` body fold — Stage 2 of
|
|
2106
|
+
* `spec/callback-fidelity.md`). A JS-runtime adapter folds them into a
|
|
2107
|
+
* per-iteration preamble; a DSL adapter can't carry a loop-local into a
|
|
2108
|
+
* conditional branch template, so the `.map()` lowering refuses (BF021 +
|
|
2109
|
+
* `/* @client *\/`) rather than render wrong output. Empty for the
|
|
2110
|
+
* helper-function inliner, which passes no `allowPreamble` and still bails on
|
|
2111
|
+
* any variable statement.
|
|
2112
|
+
*/
|
|
2113
|
+
preamble?: ts.VariableStatement[]
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
/**
|
|
2117
|
+
* A leading-`const` preamble is only safe to fold when every branch (and the
|
|
2118
|
+
* fallback) returns real JSX. If any returns `null`, the loop item can be null
|
|
2119
|
+
* and the compiler routes the loop through the anchored conditional-item
|
|
2120
|
+
* runtime (`mapArrayAnchored`), where a preamble local computed before the
|
|
2121
|
+
* conditional interacts badly with the per-item hydration anchor — observed as
|
|
2122
|
+
* a keyed-reactivity break on a real component (the pivot-table demo, whose
|
|
2123
|
+
* `.map()` body is `const f = …; if (!f) return null; return <div key={fid}>`
|
|
2124
|
+
* and which carries a workaround note about exactly this compiler behavior).
|
|
2125
|
+
* When unsafe, bail so the body falls through to the single-return path
|
|
2126
|
+
* unchanged. Stage 2 of `spec/callback-fidelity.md`.
|
|
2127
|
+
*/
|
|
2128
|
+
function preambleUnsafe(
|
|
2129
|
+
preamble: ts.VariableStatement[],
|
|
2130
|
+
branches: Array<{ jsxReturn: JsxReturnNode | null }>,
|
|
2131
|
+
fallback: JsxReturnNode | null,
|
|
2132
|
+
): boolean {
|
|
2133
|
+
if (preamble.length === 0) return false
|
|
2134
|
+
return fallback === null || branches.some(b => b.jsxReturn === null)
|
|
2135
|
+
}
|
|
2081
2136
|
|
|
2082
2137
|
/**
|
|
2083
2138
|
* Extract conditional branches from a multi-return JSX helper function body.
|
|
2084
2139
|
* Supports if/else if chains and switch statements where every branch
|
|
2085
2140
|
* returns JSX or null. Returns null for unsupported patterns.
|
|
2086
2141
|
*/
|
|
2087
|
-
function extractMultiReturnJsxBranches(
|
|
2088
|
-
body: ts.Block
|
|
2089
|
-
|
|
2090
|
-
|
|
2142
|
+
export function extractMultiReturnJsxBranches(
|
|
2143
|
+
body: ts.Block,
|
|
2144
|
+
allowPreamble = false,
|
|
2145
|
+
): MultiReturnJsxBranches | null {
|
|
2146
|
+
const branches: Array<{
|
|
2147
|
+
condition: ts.Expression
|
|
2148
|
+
jsxReturn: JsxReturnNode | null
|
|
2149
|
+
extraCaseConditions?: ts.Expression[]
|
|
2150
|
+
}> = []
|
|
2091
2151
|
let fallback: JsxReturnNode | null = null
|
|
2152
|
+
const preamble: ts.VariableStatement[] = []
|
|
2092
2153
|
|
|
2093
2154
|
const stmts = body.statements
|
|
2094
2155
|
for (let i = 0; i < stmts.length; i++) {
|
|
@@ -2125,7 +2186,8 @@ function extractMultiReturnJsxBranches(
|
|
|
2125
2186
|
return null
|
|
2126
2187
|
}
|
|
2127
2188
|
if (branches.length === 0) return null
|
|
2128
|
-
|
|
2189
|
+
if (preambleUnsafe(preamble, branches, fallback)) return null
|
|
2190
|
+
return { branches, fallback, preamble }
|
|
2129
2191
|
}
|
|
2130
2192
|
break
|
|
2131
2193
|
}
|
|
@@ -2151,23 +2213,49 @@ function extractMultiReturnJsxBranches(
|
|
|
2151
2213
|
const hasDefault = stmt.caseBlock.clauses.some(c => ts.isDefaultClause(c))
|
|
2152
2214
|
if (!hasDefault) return null
|
|
2153
2215
|
|
|
2216
|
+
// Case labels that fall through to the next clause's body — an empty
|
|
2217
|
+
// `case 'a':` immediately before `case 'b': return X` — share that
|
|
2218
|
+
// return. Accumulate their expressions and attach them to the branch of
|
|
2219
|
+
// the clause that finally carries the body. Stage 2 of
|
|
2220
|
+
// spec/callback-fidelity.md.
|
|
2221
|
+
let pendingCases: ts.Expression[] = []
|
|
2154
2222
|
for (const clause of stmt.caseBlock.clauses) {
|
|
2223
|
+
if (ts.isCaseClause(clause) && clause.statements.length === 0) {
|
|
2224
|
+
pendingCases.push(clause.expression)
|
|
2225
|
+
continue
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2155
2228
|
const jsxReturn = findJsxReturnInCaseClause(clause)
|
|
2156
2229
|
const nullReturn = findNullReturnInCaseClause(clause)
|
|
2157
2230
|
if (!jsxReturn && !nullReturn) return null
|
|
2231
|
+
// Reject clauses that carry extra statements around the return (a
|
|
2232
|
+
// local `const`, an expression statement, …): folding drops them, the
|
|
2233
|
+
// same branch-local-preamble gap as `isDirectReturnBlock`. A bare
|
|
2234
|
+
// `break` is fine. (#2377 review.)
|
|
2235
|
+
if (!caseClauseIsDirectReturn(clause)) return null
|
|
2158
2236
|
|
|
2159
2237
|
if (ts.isCaseClause(clause)) {
|
|
2160
2238
|
branches.push({
|
|
2161
2239
|
condition: clause.expression,
|
|
2162
2240
|
jsxReturn: jsxReturn ?? null,
|
|
2241
|
+
extraCaseConditions: pendingCases.length > 0 ? pendingCases : undefined,
|
|
2163
2242
|
})
|
|
2243
|
+
pendingCases = []
|
|
2164
2244
|
} else {
|
|
2245
|
+
// `default`. A fallthrough INTO default (`case 'a': default:`) would
|
|
2246
|
+
// need the accumulated cases to also hit the default body — an
|
|
2247
|
+
// unusual shape; bail rather than guess.
|
|
2248
|
+
if (pendingCases.length > 0) return null
|
|
2165
2249
|
fallback = jsxReturn ?? null
|
|
2166
2250
|
}
|
|
2167
2251
|
}
|
|
2252
|
+
// Trailing empty case labels with no following body (`case 'z':` last,
|
|
2253
|
+
// no return anywhere after) — nothing to attach them to; bail.
|
|
2254
|
+
if (pendingCases.length > 0) return null
|
|
2168
2255
|
|
|
2169
2256
|
if (branches.length === 0) return null
|
|
2170
|
-
|
|
2257
|
+
if (preambleUnsafe(preamble, branches, fallback)) return null
|
|
2258
|
+
return { branches, fallback, switchDiscriminant: stmt.expression, preamble }
|
|
2171
2259
|
}
|
|
2172
2260
|
|
|
2173
2261
|
// Trailing return <jsx> or return null — this is the fallback
|
|
@@ -2183,16 +2271,36 @@ function extractMultiReturnJsxBranches(
|
|
|
2183
2271
|
continue
|
|
2184
2272
|
}
|
|
2185
2273
|
|
|
2186
|
-
//
|
|
2187
|
-
//
|
|
2188
|
-
|
|
2274
|
+
// Leading `const`/`let` before any branch — collected as a preamble when
|
|
2275
|
+
// the caller allows it (the `.map()` body fold; Stage 2 of
|
|
2276
|
+
// spec/callback-fidelity.md). The JS-runtime fold emits it once per
|
|
2277
|
+
// iteration ahead of the conditional; a DSL target refuses (BF021 +
|
|
2278
|
+
// `/* @client */`) since it can't carry the loop-local into a branch
|
|
2279
|
+
// template. Without `allowPreamble`, or once branches have started, reject:
|
|
2280
|
+
// the helper-function inliner has no place to emit it, and a mid-chain
|
|
2281
|
+
// local referenced in a later condition/JSX would become undefined.
|
|
2282
|
+
if (ts.isVariableStatement(stmt)) {
|
|
2283
|
+
// Only `const` / `let` are collectable — a `var` hoists to the enclosing
|
|
2284
|
+
// function scope with different (redeclaration / TDZ-free) semantics that
|
|
2285
|
+
// the per-iteration preamble emit wouldn't preserve, so reject it.
|
|
2286
|
+
// (#2379 review.)
|
|
2287
|
+
const declFlags = stmt.declarationList.flags
|
|
2288
|
+
const isConstOrLet =
|
|
2289
|
+
(declFlags & ts.NodeFlags.Const) !== 0 || (declFlags & ts.NodeFlags.Let) !== 0
|
|
2290
|
+
if (allowPreamble && isConstOrLet && branches.length === 0 && fallback === null) {
|
|
2291
|
+
preamble.push(stmt)
|
|
2292
|
+
continue
|
|
2293
|
+
}
|
|
2294
|
+
return null
|
|
2295
|
+
}
|
|
2189
2296
|
|
|
2190
2297
|
// Any other statement type → unsupported pattern
|
|
2191
2298
|
return null
|
|
2192
2299
|
}
|
|
2193
2300
|
|
|
2194
2301
|
if (branches.length === 0) return null
|
|
2195
|
-
|
|
2302
|
+
if (preambleUnsafe(preamble, branches, fallback)) return null
|
|
2303
|
+
return { branches, fallback, preamble }
|
|
2196
2304
|
}
|
|
2197
2305
|
|
|
2198
2306
|
/**
|
|
@@ -2208,18 +2316,17 @@ function isDirectReturnBlock(node: ts.Statement): boolean {
|
|
|
2208
2316
|
for (const stmt of node.statements) {
|
|
2209
2317
|
if (ts.isReturnStatement(stmt)) {
|
|
2210
2318
|
returnCount++
|
|
2211
|
-
|
|
2212
|
-
ts.isIfStatement(stmt) ||
|
|
2213
|
-
ts.isSwitchStatement(stmt) ||
|
|
2214
|
-
ts.isForStatement(stmt) ||
|
|
2215
|
-
ts.isForOfStatement(stmt) ||
|
|
2216
|
-
ts.isForInStatement(stmt) ||
|
|
2217
|
-
ts.isWhileStatement(stmt) ||
|
|
2218
|
-
ts.isDoStatement(stmt) ||
|
|
2219
|
-
ts.isTryStatement(stmt)
|
|
2220
|
-
) {
|
|
2221
|
-
return false
|
|
2319
|
+
continue
|
|
2222
2320
|
}
|
|
2321
|
+
// Any non-return statement — a local `const`/`let`, an expression
|
|
2322
|
+
// statement, or nested control flow (if/switch/for/while/try) — means
|
|
2323
|
+
// this is not a *direct* return block. Folding it into an
|
|
2324
|
+
// `IRConditional` branch would silently DROP the statement: the neutral
|
|
2325
|
+
// IR has no per-branch carrier for a local, so a branch-local
|
|
2326
|
+
// `const x = …; return <A>{x}</A>` renders `x` undefined (ReferenceError
|
|
2327
|
+
// at SSR/hydration). Bail so the caller falls back conservatively.
|
|
2328
|
+
// (#2377 review; Stage 2 of spec/callback-fidelity.md.)
|
|
2329
|
+
return false
|
|
2223
2330
|
}
|
|
2224
2331
|
return returnCount === 1
|
|
2225
2332
|
}
|
|
@@ -2253,6 +2360,37 @@ function findJsxReturnInCaseClause(
|
|
|
2253
2360
|
return null
|
|
2254
2361
|
}
|
|
2255
2362
|
|
|
2363
|
+
/**
|
|
2364
|
+
* True when a `switch` case/default clause contains exactly one `return` and
|
|
2365
|
+
* nothing else that would be dropped by the fold (a trailing `break` is
|
|
2366
|
+
* allowed). Rejects clauses with a local `const`/`let`, an expression
|
|
2367
|
+
* statement, or nested control flow — those have no per-branch preamble
|
|
2368
|
+
* carrier in the folded `IRConditional`. (#2377 review; Stage 2 of
|
|
2369
|
+
* spec/callback-fidelity.md.)
|
|
2370
|
+
*/
|
|
2371
|
+
function caseClauseIsDirectReturn(clause: ts.CaseClause | ts.DefaultClause): boolean {
|
|
2372
|
+
let returnCount = 0
|
|
2373
|
+
let seenReturn = false
|
|
2374
|
+
for (const stmt of clause.statements) {
|
|
2375
|
+
if (ts.isReturnStatement(stmt)) {
|
|
2376
|
+
returnCount++
|
|
2377
|
+
seenReturn = true
|
|
2378
|
+
continue
|
|
2379
|
+
}
|
|
2380
|
+
if (ts.isBreakStatement(stmt)) {
|
|
2381
|
+
// A `break` BEFORE the return makes the return unreachable — at runtime
|
|
2382
|
+
// the case exits and the callback falls through to `undefined`, so
|
|
2383
|
+
// folding it as if it returned JSX would render the wrong branch. Only a
|
|
2384
|
+
// trailing break (after the return; dead but harmless) is allowed.
|
|
2385
|
+
// (#2378 review.)
|
|
2386
|
+
if (!seenReturn) return false
|
|
2387
|
+
continue
|
|
2388
|
+
}
|
|
2389
|
+
return false
|
|
2390
|
+
}
|
|
2391
|
+
return returnCount === 1
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2256
2394
|
function findNullReturnInCaseClause(
|
|
2257
2395
|
clause: ts.CaseClause | ts.DefaultClause
|
|
2258
2396
|
): boolean {
|
package/src/compiler.ts
CHANGED
|
@@ -119,7 +119,7 @@ function compileMultipleComponents(
|
|
|
119
119
|
const program = options.program ?? (needsTypeBasedDetection(source) ? createProgramForFile(source, filePath)?.program : undefined)
|
|
120
120
|
|
|
121
121
|
for (const componentName of componentNames) {
|
|
122
|
-
const ctx = analyzeComponent(source, filePath, componentName, program)
|
|
122
|
+
const ctx = analyzeComponent(source, filePath, componentName, program, adapter.acceptsCallbackBody)
|
|
123
123
|
|
|
124
124
|
if (!ctx.jsxReturn) {
|
|
125
125
|
errors.push(...ctx.errors)
|
|
@@ -556,7 +556,7 @@ export function compileJSX(
|
|
|
556
556
|
}
|
|
557
557
|
|
|
558
558
|
// Single component flow
|
|
559
|
-
const ctx = analyzeComponent(compileSource, filePath, undefined, options.program)
|
|
559
|
+
const ctx = analyzeComponent(compileSource, filePath, undefined, options.program, options.adapter.acceptsCallbackBody)
|
|
560
560
|
|
|
561
561
|
if (!ctx.jsxReturn) {
|
|
562
562
|
errors.push(...ctx.errors)
|
package/src/expression-parser.ts
CHANGED
|
@@ -364,25 +364,22 @@ export interface SupportResult {
|
|
|
364
364
|
// evaluate JS at runtime via hono/jsx) so this set only constrains
|
|
365
365
|
// the template-language adapters.
|
|
366
366
|
const UNSUPPORTED_METHODS = new Set([
|
|
367
|
-
// Higher-order array methods.
|
|
368
|
-
// `
|
|
369
|
-
//
|
|
370
|
-
// `
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
// `
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
// message. The rest stay refused — see #1448 Tier C for the design
|
|
384
|
-
// questions. `forEach` carries a tailored reason (see
|
|
385
|
-
// `UNSUPPORTED_METHOD_REASONS`).
|
|
367
|
+
// Higher-order array methods. `filter`, `every`, `some`, `find`,
|
|
368
|
+
// `findIndex`, `findLast`, `findLastIndex`, `reduce`, `reduceRight`
|
|
369
|
+
// and `flatMap` *with an arrow-callback argument* are recognized by
|
|
370
|
+
// `asCallbackMethodCall` and lower via the runtime evaluator
|
|
371
|
+
// (`serializeParsedExpr`) in the `call` arm ABOVE this gate — an
|
|
372
|
+
// off-subset callback body refuses there with a "complex callback"
|
|
373
|
+
// reason, not here. They stay listed in this set so the shapes that
|
|
374
|
+
// recognition can't catch — a BARE method reference (`arr.filter`
|
|
375
|
+
// uncalled) or a function-reference callback (no arrow) — still refuse
|
|
376
|
+
// loudly. (`reduce` / `reduceRight` are NOT folded into a structured
|
|
377
|
+
// `array-method` / `ReduceOp` form: #2018 P5 leaves them as a generic
|
|
378
|
+
// `call` whose reducer body + initial value serialize to the
|
|
379
|
+
// evaluator.) `map` is intercepted as an IRLoop when its callback
|
|
380
|
+
// returns JSX, and as a `CALLBACK_METHODS` evaluator lowering
|
|
381
|
+
// (`map_eval`, #2073) when it returns a value. `forEach` carries a
|
|
382
|
+
// tailored reason (see `UNSUPPORTED_METHOD_REASONS`).
|
|
386
383
|
// `flat` is no longer here — `.flat(depth?)` lowers via the
|
|
387
384
|
// `array-method` IR (structured `FlatDepth`) + `bf_flat` (Go) /
|
|
388
385
|
// `bf->flat` (Mojo). `flatMap` stays listed as a fallback: the
|
|
@@ -395,6 +392,17 @@ const UNSUPPORTED_METHODS = new Set([
|
|
|
395
392
|
// `IRLoop` upstream. See #1448.
|
|
396
393
|
'filter', 'map', 'reduce', 'reduceRight', 'every', 'some',
|
|
397
394
|
'forEach', 'flatMap',
|
|
395
|
+
// `fill` has no template lowering on any DSL adapter (it mutates the
|
|
396
|
+
// receiver in place and returns it — no ParsedExpr / evaluator form).
|
|
397
|
+
// Before it was listed here `isSupported` reported it "supported" and
|
|
398
|
+
// the adapters emitted a raw `.fill(...)` method call with no build
|
|
399
|
+
// diagnostic — the same silent footgun the string methods below
|
|
400
|
+
// documented before they were gated: it only surfaced as a crash at
|
|
401
|
+
// template-render time. Listing it makes the build fail loudly with
|
|
402
|
+
// BF101, pointing users at the `/* @client */` escape (a JS-runtime
|
|
403
|
+
// adapter still runs it verbatim, since those skip `isSupported`).
|
|
404
|
+
// See spec/callback-fidelity.md.
|
|
405
|
+
'fill',
|
|
398
406
|
// #1448 Tier A — Array methods. Each method PR adds the lowering
|
|
399
407
|
// (typically a new `array-method` variant or runtime helper) and
|
|
400
408
|
// removes its row here. See packages/adapter-tests/fixtures/methods/.
|
|
@@ -35,6 +35,7 @@ import type {
|
|
|
35
35
|
ReferenceSource,
|
|
36
36
|
ReferencesGraph,
|
|
37
37
|
} from '../types.ts'
|
|
38
|
+
import { preambleAnalysisText } from '../types.ts'
|
|
38
39
|
import type { IRVisitor as WalkerVisitor } from './walker.ts'
|
|
39
40
|
import { attrValueToString } from './utils.ts'
|
|
40
41
|
import type { ClientJsContext } from './types.ts'
|
|
@@ -151,7 +152,7 @@ export function buildReferencesGraph(ctx: ClientJsContext, irRoot: IRNode): Refe
|
|
|
151
152
|
}
|
|
152
153
|
if (elem.filterPredicate) addExprEdges(ROOT_SOURCE, elem.filterPredicate.raw, 'template-closure')
|
|
153
154
|
if (elem.sortComparator) addExprEdges(ROOT_SOURCE, elem.sortComparator.raw, 'template-closure')
|
|
154
|
-
if (elem.
|
|
155
|
+
if (elem.preamble) addExprEdges(ROOT_SOURCE, preambleAnalysisText(elem.preamble), 'template-closure')
|
|
155
156
|
for (const attr of elem.bindings.reactiveAttrs) {
|
|
156
157
|
addExprEdges(ROOT_SOURCE, attr.expression, 'template-closure')
|
|
157
158
|
}
|
|
@@ -316,7 +317,7 @@ export function buildReferencesGraph(ctx: ClientJsContext, irRoot: IRNode): Refe
|
|
|
316
317
|
addExprEdges(ROOT_SOURCE, l.array, 'template-closure')
|
|
317
318
|
if (l.filterPredicate) addExprEdges(ROOT_SOURCE, l.filterPredicate.raw, 'template-closure')
|
|
318
319
|
if (l.sortComparator) addExprEdges(ROOT_SOURCE, l.sortComparator.raw, 'template-closure')
|
|
319
|
-
if (l.
|
|
320
|
+
if (l.preamble) addExprEdges(ROOT_SOURCE, preambleAnalysisText(l.preamble), 'template-closure')
|
|
320
321
|
descend()
|
|
321
322
|
if (l.childComponent) walkChildComponent(l.childComponent)
|
|
322
323
|
if (l.nestedComponents) {
|
|
@@ -393,7 +393,7 @@ export function collectInnerLoops(
|
|
|
393
393
|
objectIteration: n.objectIteration,
|
|
394
394
|
containerSlotId: scope.parentSlotId,
|
|
395
395
|
template,
|
|
396
|
-
|
|
396
|
+
preamble: n.preamble,
|
|
397
397
|
refsOuterParam: refsOuter,
|
|
398
398
|
childComponents,
|
|
399
399
|
insideConditional: !flat && scope.insideCond ? true : undefined,
|
|
@@ -776,7 +776,7 @@ export function collectElements(
|
|
|
776
776
|
raw: l.sortComparator.raw,
|
|
777
777
|
} : undefined,
|
|
778
778
|
chainOrder: l.chainOrder,
|
|
779
|
-
|
|
779
|
+
preamble: l.preamble,
|
|
780
780
|
})
|
|
781
781
|
// Don't descend — loop-scoped variables are only available inside the iteration.
|
|
782
782
|
},
|
|
@@ -1111,7 +1111,7 @@ function collectBranchLoops(
|
|
|
1111
1111
|
objectIteration: n.objectIteration,
|
|
1112
1112
|
template: childTemplate,
|
|
1113
1113
|
containerSlotId: containerSlot,
|
|
1114
|
-
|
|
1114
|
+
preamble: n.preamble,
|
|
1115
1115
|
nestedComponents: useElementReconciliation ? n.nestedComponents : undefined,
|
|
1116
1116
|
bindings: branchBindings,
|
|
1117
1117
|
innerLoops: useElementReconciliation ? innerLoopsCollected : undefined,
|
|
@@ -21,6 +21,8 @@ import type { ReactiveEffectsPlan } from './reactive-effects.ts'
|
|
|
21
21
|
|
|
22
22
|
export interface BranchPlainLoopPlan {
|
|
23
23
|
kind: 'plain'
|
|
24
|
+
/** See LoopPlanCommon.rowConstruction — required on every variant. */
|
|
25
|
+
rowConstruction: 'string-template' | 'dom-ops'
|
|
24
26
|
/**
|
|
25
27
|
* Variable suffix for the container lookup — e.g. for slotId `s4` the
|
|
26
28
|
* stringifier emits `__loop_s4` and queries via `$(__branchScope, 's4')`.
|
|
@@ -16,6 +16,7 @@ import { buildBranchCompositePlan } from './build-composite-loop.ts'
|
|
|
16
16
|
import { buildBranchLoopDelegationPlan } from './build-event-delegation.ts'
|
|
17
17
|
import { buildReactiveEffectsPlan } from './build-reactive-effects.ts'
|
|
18
18
|
import { destructureLoopParam, loopKeyFn, buildChildRefBindings } from '../shared.ts'
|
|
19
|
+
import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
|
|
19
20
|
import type {
|
|
20
21
|
BranchCompositeLoopPlan,
|
|
21
22
|
BranchLoopPlan,
|
|
@@ -44,6 +45,7 @@ export function buildBranchLoopPlan(loop: BranchLoop, profileComponentName?: str
|
|
|
44
45
|
|
|
45
46
|
const plan: BranchPlainLoopPlan = {
|
|
46
47
|
kind: 'plain',
|
|
48
|
+
rowConstruction: 'string-template',
|
|
47
49
|
containerSlotId,
|
|
48
50
|
containerVar,
|
|
49
51
|
markerId: loop.markerId,
|
|
@@ -54,8 +56,11 @@ export function buildBranchLoopPlan(loop: BranchLoop, profileComponentName?: str
|
|
|
54
56
|
indexParam: loop.index || '__idx',
|
|
55
57
|
// Wrap loop-param references to signal-accessor form so the preamble
|
|
56
58
|
// matches the template literal's already-wrapped reads (#1065).
|
|
57
|
-
mapPreambleWrapped: loop.
|
|
58
|
-
?
|
|
59
|
+
mapPreambleWrapped: loop.preamble
|
|
60
|
+
? renderPreamble(loop.preamble, {
|
|
61
|
+
transformJs: (t) => wrapLoopParamAsAccessor(t, loop.param, loop.paramBindings),
|
|
62
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: loop.param, bindings: loop.paramBindings }], undefined, true),
|
|
63
|
+
})
|
|
59
64
|
: '',
|
|
60
65
|
template: loop.template,
|
|
61
66
|
reactiveEffects: hasReactiveEffects
|
|
@@ -60,6 +60,10 @@ export function buildComponentLoopPlan(elem: TopLevelLoop, profileComponentName?
|
|
|
60
60
|
|
|
61
61
|
return {
|
|
62
62
|
kind: 'component',
|
|
63
|
+
// Rows are createComponent-driven: a lowered HTML-string leaf passed as a
|
|
64
|
+
// prop would diverge from SSR (which passes real JSX elements). The plan
|
|
65
|
+
// dispatcher refuses a JSX-bearing preamble on this variant.
|
|
66
|
+
rowConstruction: 'dom-ops',
|
|
63
67
|
containerVar: `_${varSlotId(elem.slotId)}`,
|
|
64
68
|
markerId: elem.markerId,
|
|
65
69
|
arrayExpr: buildChainedArrayExpr(elem),
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from '../shared.ts'
|
|
24
24
|
import { buildReactiveEffectsPlan } from './build-reactive-effects.ts'
|
|
25
25
|
import { buildInnerLoopsPlan } from './build-inner-loop.ts'
|
|
26
|
+
import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
|
|
26
27
|
import type { CompositeLoopPlan } from './types.ts'
|
|
27
28
|
|
|
28
29
|
/** @internal — prefer `buildLoopPlan`. */
|
|
@@ -36,6 +37,7 @@ export function buildTopLevelCompositePlan(elem: TopLevelLoop, profileComponentN
|
|
|
36
37
|
|
|
37
38
|
return {
|
|
38
39
|
kind: 'composite',
|
|
40
|
+
rowConstruction: 'string-template',
|
|
39
41
|
containerVar: `_${varSlotId(elem.slotId)}`,
|
|
40
42
|
markerId: elem.markerId,
|
|
41
43
|
arrayExpr: buildChainedArrayExpr(elem),
|
|
@@ -43,7 +45,12 @@ export function buildTopLevelCompositePlan(elem: TopLevelLoop, profileComponentN
|
|
|
43
45
|
paramHead,
|
|
44
46
|
paramUnwrap,
|
|
45
47
|
indexParam: elem.index || '__idx',
|
|
46
|
-
mapPreambleWrapped: elem.
|
|
48
|
+
mapPreambleWrapped: elem.preamble
|
|
49
|
+
? renderPreamble(elem.preamble, {
|
|
50
|
+
transformJs: wrap,
|
|
51
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined, true),
|
|
52
|
+
})
|
|
53
|
+
: '',
|
|
47
54
|
template: elem.template,
|
|
48
55
|
outerComps: filterCondCompsOut(outerCompsByDepth, elem.bindings.conditionals),
|
|
49
56
|
outerEvents: elem.bindings.events.filter(ev => ev.nestedLoops.length === 0),
|
|
@@ -87,6 +94,7 @@ export function buildBranchCompositePlan(loop: BranchLoop, cv: string, profileCo
|
|
|
87
94
|
|
|
88
95
|
return {
|
|
89
96
|
kind: 'composite',
|
|
97
|
+
rowConstruction: 'string-template',
|
|
90
98
|
containerVar: `__loop_${cv}`,
|
|
91
99
|
markerId: loop.markerId,
|
|
92
100
|
// Chain `.filter()` / `.toSorted()` onto the source array so the mapArray
|
|
@@ -97,7 +105,12 @@ export function buildBranchCompositePlan(loop: BranchLoop, cv: string, profileCo
|
|
|
97
105
|
paramHead,
|
|
98
106
|
paramUnwrap,
|
|
99
107
|
indexParam: loop.index || '__idx',
|
|
100
|
-
mapPreambleWrapped: loop.
|
|
108
|
+
mapPreambleWrapped: loop.preamble
|
|
109
|
+
? renderPreamble(loop.preamble, {
|
|
110
|
+
transformJs: wrap,
|
|
111
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: loop.param, bindings: loop.paramBindings }], undefined, true),
|
|
112
|
+
})
|
|
113
|
+
: '',
|
|
101
114
|
template: loop.template,
|
|
102
115
|
outerComps: filterCondCompsOut(outerCompsByDepth, loop.bindings.conditionals),
|
|
103
116
|
outerEvents: childEvents.filter(ev => ev.nestedLoops.length === 0),
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import type { TopLevelLoop, BranchLoop, LoopChildEvent } from '../../types.ts'
|
|
13
13
|
import { buildChainedArrayExpr, varSlotId, substituteLoopBindings } from '../../utils.ts'
|
|
14
|
+
import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
|
|
14
15
|
import type {
|
|
15
16
|
EventDelegationPlan,
|
|
16
17
|
ItemLookup,
|
|
@@ -35,7 +36,11 @@ export function buildDynamicLoopDelegationPlan(
|
|
|
35
36
|
paramBindings: elem.paramBindings,
|
|
36
37
|
key: elem.key,
|
|
37
38
|
index: elem.index,
|
|
38
|
-
mapPreamble: elem.
|
|
39
|
+
mapPreamble: elem.preamble
|
|
40
|
+
? renderPreamble(elem.preamble, {
|
|
41
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined, true),
|
|
42
|
+
})
|
|
43
|
+
: null,
|
|
39
44
|
}),
|
|
40
45
|
}
|
|
41
46
|
}
|
|
@@ -60,7 +65,11 @@ export function buildBranchLoopDelegationPlan(
|
|
|
60
65
|
paramBindings: loop.paramBindings,
|
|
61
66
|
key: loop.key,
|
|
62
67
|
index: loop.index,
|
|
63
|
-
mapPreamble: loop.
|
|
68
|
+
mapPreamble: loop.preamble
|
|
69
|
+
? renderPreamble(loop.preamble, {
|
|
70
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: loop.param, bindings: loop.paramBindings }], undefined, true),
|
|
71
|
+
})
|
|
72
|
+
: null,
|
|
64
73
|
}),
|
|
65
74
|
}
|
|
66
75
|
}
|
|
@@ -86,7 +95,11 @@ export function buildStaticArrayDelegationPlan(
|
|
|
86
95
|
// array too (#1434).
|
|
87
96
|
arrayExpr: buildChainedArrayExpr(elem),
|
|
88
97
|
param: elem.param,
|
|
89
|
-
mapPreamble: elem.
|
|
98
|
+
mapPreamble: elem.preamble
|
|
99
|
+
? renderPreamble(elem.preamble, {
|
|
100
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined, true),
|
|
101
|
+
})
|
|
102
|
+
: null,
|
|
90
103
|
offset: elem.offset ?? null,
|
|
91
104
|
indexParam: elem.index ?? null,
|
|
92
105
|
},
|