@barefootjs/jsx 0.29.0 → 0.30.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/child-scope.d.ts +17 -0
- package/dist/adapters/child-scope.d.ts.map +1 -0
- package/dist/adapters/loop-bound-names.d.ts.map +1 -1
- package/dist/analyzer-context.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +990 -367
- package/dist/ir-to-client-js/collect-elements.d.ts +5 -25
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts +66 -2
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts +92 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts +134 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts +46 -11
- package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts +7 -0
- package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +2 -3
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/declaration-emit.d.ts +6 -0
- package/dist/ir-to-client-js/plan/declaration-emit.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts +1 -10
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +12 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/prop-rewrite.d.ts +24 -7
- package/dist/prop-rewrite.d.ts.map +1 -1
- package/dist/rich-type-evidence.d.ts.map +1 -1
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/to-locale-date-lowering.d.ts.map +1 -1
- package/dist/types.d.ts +78 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +6 -24
- package/src/__tests__/branch-loop-plain.test.ts +28 -45
- package/src/__tests__/csr-substitution-safety-divergence.test.ts +9 -3
- package/src/__tests__/csr-template-scope-soundness.test.ts +167 -0
- package/src/__tests__/early-return-branch-switch.test.ts +96 -0
- package/src/__tests__/form-control-value-ssr.test.ts +102 -0
- package/src/__tests__/lazy-conditional.test.ts +190 -0
- package/src/__tests__/lazy-preamble.test.ts +297 -0
- package/src/__tests__/lazy-row-eligibility.test.ts +97 -28
- package/src/__tests__/loop-item-root-scope.test.ts +213 -0
- package/src/__tests__/map-multi-return-body.test.ts +30 -3
- package/src/__tests__/nested-loop-conditional.test.ts +27 -6
- package/src/__tests__/preamble-attr-reactivity.test.ts +217 -0
- package/src/__tests__/preamble-declarations.test.ts +207 -0
- package/src/__tests__/ssr-defaults.test.ts +44 -0
- package/src/adapters/child-scope.ts +22 -0
- package/src/adapters/loop-bound-names.ts +6 -0
- package/src/analyzer-context.ts +43 -0
- package/src/analyzer.ts +33 -3
- package/src/index.ts +1 -0
- package/src/ir-to-client-js/collect-elements.ts +51 -10
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +1 -1
- package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +2 -2
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +3 -3
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +2 -2
- package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +147 -5
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +2 -2
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +1 -0
- package/src/ir-to-client-js/control-flow/plan/lazy-conditional.ts +151 -0
- package/src/ir-to-client-js/control-flow/plan/lazy-preamble.ts +287 -0
- package/src/ir-to-client-js/control-flow/plan/lazy-row-eligibility.ts +85 -22
- package/src/ir-to-client-js/control-flow/plan/reactive-effects.ts +7 -0
- package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +133 -35
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +24 -4
- package/src/ir-to-client-js/csr-substitute.ts +6 -1
- package/src/ir-to-client-js/emit-registration.ts +1 -1
- package/src/ir-to-client-js/html-template.ts +51 -40
- package/src/ir-to-client-js/index.ts +1 -1
- package/src/ir-to-client-js/plan/build-declaration-emit.ts +2 -0
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +1 -1
- package/src/ir-to-client-js/plan/declaration-emit.ts +6 -0
- package/src/ir-to-client-js/reactivity.ts +25 -0
- package/src/ir-to-client-js/stringify/declaration-emit.ts +9 -4
- package/src/ir-to-client-js/types.ts +12 -0
- package/src/jsx-to-ir.ts +409 -24
- package/src/prop-rewrite.ts +149 -19
- package/src/rich-type-evidence.ts +6 -7
- package/src/ssr-defaults.ts +7 -2
- package/src/to-locale-date-lowering.ts +8 -2
- package/src/types.ts +80 -0
package/src/jsx-to-ir.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
type MapCallbackPreamble,
|
|
30
30
|
type PreambleSegment,
|
|
31
31
|
type PreambleRegionSource,
|
|
32
|
+
type PreambleValueDeclaration,
|
|
32
33
|
tsxSourceText,
|
|
33
34
|
type SourceLocation,
|
|
34
35
|
type TypeInfo,
|
|
@@ -945,6 +946,36 @@ function buildIRRoot(analyzer: AnalyzerContext): IRNode | null {
|
|
|
945
946
|
// build an if-statement chain instead of a single node
|
|
946
947
|
if (analyzer.conditionalReturns.length > 0) {
|
|
947
948
|
const ctx = createTransformContext(analyzer)
|
|
949
|
+
// #2463: a chain whose conditions are ALL reactive (signal/memo reads)
|
|
950
|
+
// is semantically the root ternary, and the IRIfStatement contract
|
|
951
|
+
// ("client JS handles all branches and switches at runtime") demands
|
|
952
|
+
// the same insert() plan the ternary gets. Lower it through the
|
|
953
|
+
// conditional path + synthetic scope wrapper. Gated to chains with no
|
|
954
|
+
// branch-local scope variables — those carry #1409/#1414 machinery
|
|
955
|
+
// (branch consts, branch-scoped signals) that the conditional plan
|
|
956
|
+
// has no seat for, so they stay on the IRIfStatement path; a static
|
|
957
|
+
// or prop-conditioned chain stays there too (its branches cannot
|
|
958
|
+
// change after render without a re-render, so no runtime switch is
|
|
959
|
+
// owed). The eligibility check is analysis-only (no transforms run),
|
|
960
|
+
// so falling through to the statement path never skews slot ids.
|
|
961
|
+
// `exprCallsReactiveGetters` (signal/memo getter CALLS), not
|
|
962
|
+
// `isReactiveExpression`: bare destructured-prop reads also count as
|
|
963
|
+
// "reactive" to the latter, and prop-conditioned chains must stay on
|
|
964
|
+
// the IRIfStatement path — their branches only change on re-render,
|
|
965
|
+
// and converting them would churn every #1401-family emission.
|
|
966
|
+
const allReactiveNoLocals =
|
|
967
|
+
analyzer.jsxReturn != null &&
|
|
968
|
+
analyzer.conditionalReturns.every(cr =>
|
|
969
|
+
cr.scopeVariables.length === 0 &&
|
|
970
|
+
exprCallsReactiveGetters(cr.condition, ctx),
|
|
971
|
+
)
|
|
972
|
+
if (allReactiveNoLocals) {
|
|
973
|
+
const chain = buildIfStatementChain(analyzer, ctx, { asConditional: true })
|
|
974
|
+
if (!chain) return null
|
|
975
|
+
// Degenerate transforms can fall a link back to statement mode; the
|
|
976
|
+
// wrapper is owed only to a conditional root (the ternary shape).
|
|
977
|
+
return chain.type === 'conditional' ? wrapInScopeElement(chain) : chain
|
|
978
|
+
}
|
|
948
979
|
return buildIfStatementChain(analyzer, ctx)
|
|
949
980
|
}
|
|
950
981
|
|
|
@@ -1231,6 +1262,84 @@ function transformJsxElement(
|
|
|
1231
1262
|
return transformHtmlElement(node, ctx, tagName)
|
|
1232
1263
|
}
|
|
1233
1264
|
|
|
1265
|
+
/**
|
|
1266
|
+
* SSR projection of a controlled form-control `value` (#2464 / #2465).
|
|
1267
|
+
*
|
|
1268
|
+
* `value` is not an attribute on `<textarea>` (its initial value is its
|
|
1269
|
+
* element content) and not an attribute on `<select>` (the selection is
|
|
1270
|
+
* `selected` on the matching `<option>`). Emitting it verbatim ships
|
|
1271
|
+
* invalid HTML that browsers ignore, so no-JS and pre-hydration users see
|
|
1272
|
+
* an empty textarea / the wrong option until the client `.value` effect
|
|
1273
|
+
* snaps it.
|
|
1274
|
+
*
|
|
1275
|
+
* Lowering (shared IR, so every adapter inherits it):
|
|
1276
|
+
* - the `value` attr is marked `clientOnly` — SSR skips it, the existing
|
|
1277
|
+
* hydrate-time `.value` property binding is unchanged;
|
|
1278
|
+
* - `<textarea>` with no children gains a NON-reactive expression child
|
|
1279
|
+
* (the initial value as element content — updates keep flowing through
|
|
1280
|
+
* the `.value` effect, deliberately not a live text slot);
|
|
1281
|
+
* - `<select>` distributes `selected={(value) === 'opt'}` onto each
|
|
1282
|
+
* statically-valued `<option>` (incl. under `<optgroup>`/fragments) —
|
|
1283
|
+
* the exact per-option comparison shape the `select-option-selected`
|
|
1284
|
+
* fixture already proves across every adapter. Options rendered by a
|
|
1285
|
+
* dynamic loop can't be statically distributed and are left to the
|
|
1286
|
+
* hydrate-time effect (tracked with #2466 on the #2464 thread).
|
|
1287
|
+
*/
|
|
1288
|
+
function lowerFormControlValueSsr(
|
|
1289
|
+
tagName: string,
|
|
1290
|
+
attrs: IRAttribute[],
|
|
1291
|
+
children: IRNode[],
|
|
1292
|
+
): void {
|
|
1293
|
+
if (tagName !== 'textarea' && tagName !== 'select') return
|
|
1294
|
+
const valueAttr = attrs.find(a => a.name === 'value')
|
|
1295
|
+
if (!valueAttr || valueAttr.clientOnly || valueAttr.value.kind !== 'expression') return
|
|
1296
|
+
const { expr, templateExpr } = valueAttr.value
|
|
1297
|
+
|
|
1298
|
+
valueAttr.clientOnly = true
|
|
1299
|
+
|
|
1300
|
+
if (tagName === 'textarea') {
|
|
1301
|
+
if (children.length > 0) return
|
|
1302
|
+
children.push({
|
|
1303
|
+
type: 'expression',
|
|
1304
|
+
expr,
|
|
1305
|
+
// The client-side registration template interpolates slotless
|
|
1306
|
+
// expression children RAW (no text-slot `escapeText` wrapper), so a
|
|
1307
|
+
// value containing `</textarea>` would break out of the element on
|
|
1308
|
+
// CSR mount. Escape in the client-only template variant; `expr`
|
|
1309
|
+
// stays clean for the SSR adapters, whose template engines (and
|
|
1310
|
+
// hono/jsx) already escape text children natively.
|
|
1311
|
+
templateExpr: `escapeText(${templateExpr ?? expr})`,
|
|
1312
|
+
typeInfo: null,
|
|
1313
|
+
reactive: false,
|
|
1314
|
+
slotId: null,
|
|
1315
|
+
loc: valueAttr.loc,
|
|
1316
|
+
origin: { phase: 'ssr', scope: 'template', effect: 'pure' },
|
|
1317
|
+
})
|
|
1318
|
+
return
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
const selectedFor = (optValue: string): AttrValue =>
|
|
1322
|
+
AttrValueOf.expression(
|
|
1323
|
+
`(${expr}) === ${JSON.stringify(optValue)}`,
|
|
1324
|
+
templateExpr !== undefined
|
|
1325
|
+
? { templateExpr: `(${templateExpr}) === ${JSON.stringify(optValue)}` }
|
|
1326
|
+
: undefined,
|
|
1327
|
+
)
|
|
1328
|
+
const distribute = (nodes: IRNode[]): void => {
|
|
1329
|
+
for (const n of nodes) {
|
|
1330
|
+
if (n.type === 'element' && n.tag === 'option') {
|
|
1331
|
+
if (n.attrs.some(a => a.name === 'selected')) continue
|
|
1332
|
+
const optValue = n.attrs.find(a => a.name === 'value')
|
|
1333
|
+
if (!optValue || optValue.value.kind !== 'literal') continue
|
|
1334
|
+
n.attrs.push({ name: 'selected', value: selectedFor(optValue.value.value), loc: n.loc })
|
|
1335
|
+
} else if (n.type === 'fragment' || (n.type === 'element' && n.tag === 'optgroup')) {
|
|
1336
|
+
distribute(n.children)
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
distribute(children)
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1234
1343
|
function transformHtmlElement(
|
|
1235
1344
|
node: ts.JsxElement,
|
|
1236
1345
|
ctx: TransformContext,
|
|
@@ -1246,6 +1355,7 @@ function transformHtmlElement(
|
|
|
1246
1355
|
ctx.isRoot = false
|
|
1247
1356
|
|
|
1248
1357
|
const children = transformChildren(node.children, ctx)
|
|
1358
|
+
lowerFormControlValueSsr(tagName, attrs, children)
|
|
1249
1359
|
|
|
1250
1360
|
// Determine if this element needs a slot ID
|
|
1251
1361
|
// Elements need slotIds if they have: events, dynamic children, reactive attributes, or refs
|
|
@@ -1301,6 +1411,8 @@ function transformSelfClosingElement(
|
|
|
1301
1411
|
}
|
|
1302
1412
|
|
|
1303
1413
|
const { attrs, events, ref } = processAttributes(node.attributes, ctx)
|
|
1414
|
+
const selfClosingChildren: IRNode[] = []
|
|
1415
|
+
lowerFormControlValueSsr(tagName, attrs, selfClosingChildren)
|
|
1304
1416
|
|
|
1305
1417
|
// Elements need slotIds if they have events, reactive attributes, or refs
|
|
1306
1418
|
const needsSlot = events.length > 0 || hasReactiveAttributes(attrs, ctx) || ref !== null
|
|
@@ -1315,7 +1427,7 @@ function transformSelfClosingElement(
|
|
|
1315
1427
|
attrs,
|
|
1316
1428
|
events,
|
|
1317
1429
|
ref,
|
|
1318
|
-
children:
|
|
1430
|
+
children: selfClosingChildren,
|
|
1319
1431
|
slotId,
|
|
1320
1432
|
needsScope,
|
|
1321
1433
|
loc: getSourceLocation(node, ctx.sourceFile, ctx.filePath),
|
|
@@ -3794,6 +3906,27 @@ function branchHasNoElement(node: IRNode): boolean {
|
|
|
3794
3906
|
* element ternaries (which always render exactly one element and stay on the
|
|
3795
3907
|
* legacy `mapArray` path).
|
|
3796
3908
|
*/
|
|
3909
|
+
/**
|
|
3910
|
+
* Tag every component that is a loop item root — a DIRECT member of the
|
|
3911
|
+
* loop body, unwrapping through nested conditionals (`cond ? <A/> : <B/>`,
|
|
3912
|
+
* `cond && <A/>`) but NOT through elements or fragments. Mirrors the Hono
|
|
3913
|
+
* reference adapter's `renderConditional` ctx-forwarding exactly:
|
|
3914
|
+
* `isLoopItemRoot` threads through nested conditional branches (a bare
|
|
3915
|
+
* ternary/logical loop body, arbitrarily nested), but `renderElement` /
|
|
3916
|
+
* `renderFragment` never forward it to their children — a component nested
|
|
3917
|
+
* inside a plain element or fragment is never a loop item root, even if
|
|
3918
|
+
* that element/fragment is itself the loop's row. See #2444.
|
|
3919
|
+
*/
|
|
3920
|
+
function tagLoopItemRootComponents(nodes: readonly IRNode[]): void {
|
|
3921
|
+
for (const node of nodes) {
|
|
3922
|
+
if (node.type === 'component') {
|
|
3923
|
+
node.loopItemRoot = true
|
|
3924
|
+
} else if (node.type === 'conditional') {
|
|
3925
|
+
tagLoopItemRootComponents([node.whenTrue, node.whenFalse])
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3929
|
+
|
|
3797
3930
|
function loopBodyItemConditional(children: IRNode[]): IRConditional | null {
|
|
3798
3931
|
const real = children.filter(
|
|
3799
3932
|
(c) => !(c.type === 'text' && typeof c.value === 'string' && !c.value.trim())
|
|
@@ -4229,20 +4362,29 @@ function transformMapCall(
|
|
|
4229
4362
|
// verbatim.
|
|
4230
4363
|
preamble = preambleFromValueStatements(pre, ctx)
|
|
4231
4364
|
|
|
4232
|
-
//
|
|
4233
|
-
//
|
|
4234
|
-
//
|
|
4235
|
-
//
|
|
4236
|
-
//
|
|
4237
|
-
//
|
|
4238
|
-
//
|
|
4239
|
-
|
|
4365
|
+
// Stage 2 of spec/callback-fidelity.md, narrowed by #2447. This used
|
|
4366
|
+
// to refuse EVERY branch preamble on a DSL target, on the premise
|
|
4367
|
+
// that a loop-local can't be carried into a conditional branch
|
|
4368
|
+
// template. That premise stopped holding once `declarations` lower
|
|
4369
|
+
// to a per-row local: the fold emits the conditional INSIDE the loop
|
|
4370
|
+
// body, so a local declared ahead of it is in scope in both arms —
|
|
4371
|
+
// `{{$label := bf_upper .Kind}}{{if .On}}…{{$label}}…{{else}}…`. What
|
|
4372
|
+
// remains true is the original hazard for a preamble that CANNOT be
|
|
4373
|
+
// declared: nothing lowers, both branches read an unassigned name.
|
|
4374
|
+
// So the refusal now keys off the same condition as the single-return
|
|
4375
|
+
// gate below, not off the presence of branches.
|
|
4376
|
+
if (
|
|
4377
|
+
!preamble.declarations &&
|
|
4378
|
+
!isClientOnly &&
|
|
4379
|
+
!(ctx.analyzer.acceptsCallbackBody?.('map') ?? false)
|
|
4380
|
+
) {
|
|
4240
4381
|
ctx.analyzer.errors.push(
|
|
4241
4382
|
createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN, loc, {
|
|
4242
4383
|
message:
|
|
4243
|
-
'A .map() callback body with a
|
|
4244
|
-
'
|
|
4245
|
-
'
|
|
4384
|
+
'A .map() callback body with a preamble before its branches cannot be ' +
|
|
4385
|
+
'lowered to a template: the preamble is not a sequence of value ' +
|
|
4386
|
+
'declarations, so this backend has no per-row local to carry into the ' +
|
|
4387
|
+
'branches.',
|
|
4246
4388
|
suggestion: {
|
|
4247
4389
|
message: 'Add /* @client */ to evaluate this expression on the client only',
|
|
4248
4390
|
},
|
|
@@ -4381,6 +4523,37 @@ function transformMapCall(
|
|
|
4381
4523
|
}
|
|
4382
4524
|
if (valueStmts.length > 0) {
|
|
4383
4525
|
preamble = preambleFromValueStatements(valueStmts, ctx)
|
|
4526
|
+
|
|
4527
|
+
// #2447. A DSL adapter cannot execute `preamble.segments` (JS
|
|
4528
|
+
// text), so it lowers `declarations` — one per-row local each —
|
|
4529
|
+
// and the row markup reads them as locals. When the preamble
|
|
4530
|
+
// isn't fully declarable there is nothing to lower, and the
|
|
4531
|
+
// adapter used to emit the row anyway, reading names it never
|
|
4532
|
+
// assigned: the class rendered empty, on every DSL backend, with
|
|
4533
|
+
// no diagnostic. Refuse loudly instead, with the same
|
|
4534
|
+
// `/* @client */` escape as the branch-preamble, array-builder
|
|
4535
|
+
// and flatMap gates above.
|
|
4536
|
+
if (
|
|
4537
|
+
!preamble.declarations &&
|
|
4538
|
+
!isClientOnly &&
|
|
4539
|
+
!(ctx.analyzer.acceptsCallbackBody?.('map') ?? false)
|
|
4540
|
+
) {
|
|
4541
|
+
ctx.analyzer.errors.push(
|
|
4542
|
+
createError(
|
|
4543
|
+
ErrorCodes.UNSUPPORTED_JSX_PATTERN,
|
|
4544
|
+
getSourceLocation(body, ctx.sourceFile, ctx.filePath),
|
|
4545
|
+
{
|
|
4546
|
+
message:
|
|
4547
|
+
'A .map() callback preamble that is not a sequence of value ' +
|
|
4548
|
+
'declarations cannot be lowered to a template: this backend can ' +
|
|
4549
|
+
'declare a per-row local, but cannot run arbitrary statements per row.',
|
|
4550
|
+
suggestion: {
|
|
4551
|
+
message: 'Add /* @client */ to evaluate this expression on the client only',
|
|
4552
|
+
},
|
|
4553
|
+
},
|
|
4554
|
+
)
|
|
4555
|
+
)
|
|
4556
|
+
}
|
|
4384
4557
|
}
|
|
4385
4558
|
}
|
|
4386
4559
|
}
|
|
@@ -4605,6 +4778,15 @@ function transformMapCall(
|
|
|
4605
4778
|
preamble = undefined
|
|
4606
4779
|
}
|
|
4607
4780
|
|
|
4781
|
+
// A component that is a DIRECT member of the loop body (the row root, or
|
|
4782
|
+
// one of several flatMap-sibling roots) owns its own per-row identity and
|
|
4783
|
+
// gets a freshly randomized `bf-s` rather than deriving one from parent
|
|
4784
|
+
// scope + slot. Tag it here — the single place that knows which nodes are
|
|
4785
|
+
// direct loop-body members — so every backend can consult
|
|
4786
|
+
// `derivesScopeFromSlot()` instead of re-deriving this fact from a mutable
|
|
4787
|
+
// "am I inside a loop" flag.
|
|
4788
|
+
tagLoopItemRootComponents(children)
|
|
4789
|
+
|
|
4608
4790
|
// Extract childComponent info if the loop body is a single component
|
|
4609
4791
|
// This enables createComponent-based rendering with proper prop passing
|
|
4610
4792
|
let childComponent: IRLoopChildComponent | undefined
|
|
@@ -4675,6 +4857,19 @@ function transformMapCall(
|
|
|
4675
4857
|
? collectPreambleRegions(children, new Set(preamble.declaredNames), ctx)
|
|
4676
4858
|
: undefined
|
|
4677
4859
|
|
|
4860
|
+
// #2447 follow-up — the ATTRIBUTE twin of the regions above. `class={cls}`
|
|
4861
|
+
// where the preamble declares `cls` froze at its row-construction value for
|
|
4862
|
+
// the same reason `{cells}` did, and for one extra reason on top: an
|
|
4863
|
+
// attribute only gets wired at all if its element carries a slot id, and
|
|
4864
|
+
// `hasReactiveAttributes` (which decides that at element-build time, before
|
|
4865
|
+
// any loop preamble is known) scores a bare local as non-reactive. So the
|
|
4866
|
+
// slot has to be granted HERE, in the same after-the-fact pass the regions
|
|
4867
|
+
// use — after which `collectLoopChildReactiveAttrs` classifies the attr and
|
|
4868
|
+
// the row effect re-runs the preamble ahead of the write.
|
|
4869
|
+
if (preamble && !isStaticArray) {
|
|
4870
|
+
markPreambleAttrSlots(children, new Set(preamble.declaredNames), ctx)
|
|
4871
|
+
}
|
|
4872
|
+
|
|
4678
4873
|
// Collect nested components for both static and dynamic arrays.
|
|
4679
4874
|
// Static arrays: needed for initChild hydration.
|
|
4680
4875
|
// Dynamic arrays with native root + component descendants: enables
|
|
@@ -5061,6 +5256,72 @@ function collectPreambleRegions(
|
|
|
5061
5256
|
return regions
|
|
5062
5257
|
}
|
|
5063
5258
|
|
|
5259
|
+
/**
|
|
5260
|
+
* Grant a slot id to every loop-body element carrying an attribute that reads
|
|
5261
|
+
* a `.map()` preamble local (#2447 follow-up).
|
|
5262
|
+
*
|
|
5263
|
+
* Only the slot id is decided here; whether the attribute is *wired* is the
|
|
5264
|
+
* client-JS classifier's call (`collectLoopChildReactiveAttrs`, which takes
|
|
5265
|
+
* the same `declaredNames` set). Splitting it that way keeps this pass free of
|
|
5266
|
+
* reactivity policy — it removes the one obstacle the classifier cannot remove
|
|
5267
|
+
* for itself, since `hasReactiveAttributes` already ran when the element was
|
|
5268
|
+
* built and a slot id cannot be granted from the client-JS pass (the SSR
|
|
5269
|
+
* template is rendered from this same IR and would not carry the `bf` marker).
|
|
5270
|
+
*
|
|
5271
|
+
* Mirrors `collectPreambleRegions`' traversal exactly, conditional arms
|
|
5272
|
+
* included — an arm element gets its slot id here too, which costs nothing
|
|
5273
|
+
* when the arm's own wiring declines to use it.
|
|
5274
|
+
*/
|
|
5275
|
+
function markPreambleAttrSlots(
|
|
5276
|
+
nodes: IRNode[],
|
|
5277
|
+
declared: ReadonlySet<string>,
|
|
5278
|
+
ctx: TransformContext,
|
|
5279
|
+
): void {
|
|
5280
|
+
const readsDeclared = (attr: IRAttribute): boolean => {
|
|
5281
|
+
if (attr.name === 'key') return false
|
|
5282
|
+
const value = attr.value
|
|
5283
|
+
if (value.kind !== 'expression' && value.kind !== 'template') return false
|
|
5284
|
+
const refs = attr.freeIdentifiers ?? extractFreeIdentifiersFromText(attrValueText(value))
|
|
5285
|
+
for (const r of refs) if (declared.has(r)) return true
|
|
5286
|
+
return false
|
|
5287
|
+
}
|
|
5288
|
+
const visit = (list: IRNode[]): void => {
|
|
5289
|
+
for (const node of list) {
|
|
5290
|
+
switch (node.type) {
|
|
5291
|
+
case 'element':
|
|
5292
|
+
if (!node.slotId && node.attrs.some(readsDeclared)) node.slotId = generateSlotId(ctx)
|
|
5293
|
+
visit(node.children)
|
|
5294
|
+
break
|
|
5295
|
+
case 'fragment':
|
|
5296
|
+
visit(node.children)
|
|
5297
|
+
break
|
|
5298
|
+
case 'conditional':
|
|
5299
|
+
visit([node.whenTrue, ...(node.whenFalse ? [node.whenFalse] : [])])
|
|
5300
|
+
break
|
|
5301
|
+
}
|
|
5302
|
+
}
|
|
5303
|
+
}
|
|
5304
|
+
visit(nodes)
|
|
5305
|
+
}
|
|
5306
|
+
|
|
5307
|
+
/**
|
|
5308
|
+
* Expression-bearing source text of an attribute value, for the
|
|
5309
|
+
* free-identifier scan. Only used as a FALLBACK — an attribute normally
|
|
5310
|
+
* carries `freeIdentifiers` from its own AST walk, and this reconstruction
|
|
5311
|
+
* exists for the few IR producers that don't populate it. Static string parts
|
|
5312
|
+
* are dropped so a literal word can never be mistaken for an identifier.
|
|
5313
|
+
*/
|
|
5314
|
+
function attrValueText(value: AttrValue): string {
|
|
5315
|
+
if (value.kind === 'expression') return value.expr
|
|
5316
|
+
if (value.kind !== 'template') return ''
|
|
5317
|
+
const out: string[] = []
|
|
5318
|
+
for (const p of value.parts) {
|
|
5319
|
+
if (p.type === 'ternary') out.push(p.condition, p.whenTrue, p.whenFalse)
|
|
5320
|
+
else if (p.type === 'lookup') out.push(p.key)
|
|
5321
|
+
}
|
|
5322
|
+
return out.join(' ')
|
|
5323
|
+
}
|
|
5324
|
+
|
|
5064
5325
|
/**
|
|
5065
5326
|
* Collect the names a binding introduces, recursing through object/array
|
|
5066
5327
|
* destructuring patterns (`const { id: k } = r`, `const [k, ...rest] = xs`) so
|
|
@@ -5118,9 +5379,50 @@ function preambleFromValueStatements(
|
|
|
5118
5379
|
declaredNames: [...declared],
|
|
5119
5380
|
// Value-only preambles accumulate no JSX, so no child needs the array join.
|
|
5120
5381
|
builderNames: [],
|
|
5382
|
+
declarations: neutralPreambleDeclarations(statements, ctx) ?? undefined,
|
|
5121
5383
|
}
|
|
5122
5384
|
}
|
|
5123
5385
|
|
|
5386
|
+
/**
|
|
5387
|
+
* The DSL SSR half of a value preamble (#2447): every statement as a neutral
|
|
5388
|
+
* {@link PreambleValueDeclaration}, or `null` if any statement is not one.
|
|
5389
|
+
*
|
|
5390
|
+
* All-or-nothing — see `MapCallbackPreamble.declarations`. `null` is what the
|
|
5391
|
+
* Phase-1 gate turns into a loud refusal, so every rejection here must be a
|
|
5392
|
+
* shape a template language genuinely cannot express per row, not a shape this
|
|
5393
|
+
* function merely hasn't got round to.
|
|
5394
|
+
*/
|
|
5395
|
+
function neutralPreambleDeclarations(
|
|
5396
|
+
statements: readonly ts.Statement[],
|
|
5397
|
+
ctx: TransformContext,
|
|
5398
|
+
): PreambleValueDeclaration[] | null {
|
|
5399
|
+
const out: PreambleValueDeclaration[] = []
|
|
5400
|
+
for (const stmt of statements) {
|
|
5401
|
+
// A non-declaration statement (assignment, `for`, a call for its side
|
|
5402
|
+
// effects) has no per-row template form at all.
|
|
5403
|
+
if (!ts.isVariableStatement(stmt)) return null
|
|
5404
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
5405
|
+
// A destructuring binding declares several names from one initializer;
|
|
5406
|
+
// no adapter has a single per-row local form for that.
|
|
5407
|
+
if (!ts.isIdentifier(decl.name)) return null
|
|
5408
|
+
if (!decl.initializer) return null
|
|
5409
|
+
const valueParsed = tsNodeToParsedExpr(decl.initializer)
|
|
5410
|
+
// The same subset gate every other DSL-lowered expression passes
|
|
5411
|
+
// through. An adapter that additionally cannot emit a supported shape
|
|
5412
|
+
// (Go has no array-literal form, say) still records its own BF101 from
|
|
5413
|
+
// its own emitter — this only decides whether the shape is expressible
|
|
5414
|
+
// in principle.
|
|
5415
|
+
if (!isSupported(valueParsed).supported) return null
|
|
5416
|
+
out.push({
|
|
5417
|
+
name: decl.name.text,
|
|
5418
|
+
valueParsed,
|
|
5419
|
+
raw: decl.initializer.getText(ctx.sourceFile),
|
|
5420
|
+
})
|
|
5421
|
+
}
|
|
5422
|
+
}
|
|
5423
|
+
return out.length > 0 ? out : null
|
|
5424
|
+
}
|
|
5425
|
+
|
|
5124
5426
|
/** Drop the trailing separator space from the final js segment. */
|
|
5125
5427
|
function trimPreambleSegments(segments: PreambleSegment[]): PreambleSegment[] {
|
|
5126
5428
|
const last = segments[segments.length - 1]
|
|
@@ -5772,6 +6074,11 @@ function tryResolveTemplateSpanFromConst(
|
|
|
5772
6074
|
): IRTemplatePart[] | null {
|
|
5773
6075
|
// ${IDENT}
|
|
5774
6076
|
if (ts.isIdentifier(expr)) {
|
|
6077
|
+
// #2222-family: inside a loop callback the name may be the loop's
|
|
6078
|
+
// item/index binding shadowing a same-named const — resolving the
|
|
6079
|
+
// const would bake the outer value into every row. Fall back to
|
|
6080
|
+
// the bare-expression path, which sees the loop binding.
|
|
6081
|
+
if (ctx.loopParams.has(expr.text)) return null
|
|
5775
6082
|
const constInfo = findLocalConst(expr.text, ctx.analyzer)
|
|
5776
6083
|
if (!constInfo) return null
|
|
5777
6084
|
const ast = parseConstInitializer(constInfo)
|
|
@@ -5785,6 +6092,10 @@ function tryResolveTemplateSpanFromConst(
|
|
|
5785
6092
|
// ${IDENT[KEY]}
|
|
5786
6093
|
if (ts.isElementAccessExpression(expr)) {
|
|
5787
6094
|
if (!ts.isIdentifier(expr.expression)) return null
|
|
6095
|
+
// Same loop-shadowing guard as the ${IDENT} arm: `tone[k]` inside
|
|
6096
|
+
// `items.map((tone) => …)` must read the row's `tone`, not a
|
|
6097
|
+
// same-named module/component record const.
|
|
6098
|
+
if (ctx.loopParams.has(expr.expression.text)) return null
|
|
5788
6099
|
const constInfo = findLocalConst(expr.expression.text, ctx.analyzer)
|
|
5789
6100
|
if (!constInfo) return null
|
|
5790
6101
|
const ast = parseConstInitializer(constInfo)
|
|
@@ -5968,6 +6279,30 @@ function tryResolveIdentifierAsTemplateLiteral(
|
|
|
5968
6279
|
return [{ type: 'string', value: ast.text }]
|
|
5969
6280
|
}
|
|
5970
6281
|
|
|
6282
|
+
// `const cls = variantClasses[variant]` (#2477): the initializer IS the
|
|
6283
|
+
// #2300 element-access shape, just not wrapped in a template literal.
|
|
6284
|
+
// Route it through the same span resolver so the one-hop const
|
|
6285
|
+
// (`className={cls}`) lowers to the identical `lookup` part as the
|
|
6286
|
+
// inline form (`className={variantClasses[variant]}`, the #2300 arm in
|
|
6287
|
+
// `getAttributeValue`) and the template-literal hop
|
|
6288
|
+
// (`const cls = \`${variantClasses[variant]}\``, the arm below) — both
|
|
6289
|
+
// of which every adapter already renders. Without this arm the attr
|
|
6290
|
+
// fell through to a bare-expression `cls`, which JSX-runtime SSR
|
|
6291
|
+
// (Hono) evaluates fine but every template backend emits as a
|
|
6292
|
+
// reference to a variable the template never defines, rendering
|
|
6293
|
+
// `class=""` with zero diagnostics. Same dynamic-key guard as the
|
|
6294
|
+
// inline arm: a static-key index (`paths['icon']`) stays on the
|
|
6295
|
+
// bare-expression path (jsx-to-ir regression pin — adapters resolve a
|
|
6296
|
+
// constant-key index already, and it must remain a plain `expression`
|
|
6297
|
+
// attr, not a single-case `lookup`).
|
|
6298
|
+
if (
|
|
6299
|
+
ts.isElementAccessExpression(ast) &&
|
|
6300
|
+
!ts.isStringLiteralLike(ast.argumentExpression) &&
|
|
6301
|
+
!ts.isNumericLiteral(ast.argumentExpression)
|
|
6302
|
+
) {
|
|
6303
|
+
return tryResolveTemplateSpanFromConst(ast, ctx)
|
|
6304
|
+
}
|
|
6305
|
+
|
|
5971
6306
|
if (!ts.isTemplateExpression(ast)) return null
|
|
5972
6307
|
|
|
5973
6308
|
// We require at least one structurally-meaningful span (a string
|
|
@@ -6433,8 +6768,20 @@ function processComponentProps(
|
|
|
6433
6768
|
// (Hono) keep using `expr`. Boolean-attr is also promoted to
|
|
6434
6769
|
// shorthand (`<X disabled />` → `disabled={true}`).
|
|
6435
6770
|
if (value.kind === 'template') {
|
|
6436
|
-
|
|
6771
|
+
// Collapse using the raw part values, but also carry the
|
|
6772
|
+
// template-variant projection (parts' `templateValue` /
|
|
6773
|
+
// `templateCondition` / `templateKey`, i.e. destructured-prop refs
|
|
6774
|
+
// rewritten to `_p.X`) as `templateExpr`. Without it, the
|
|
6775
|
+
// `templateExpr` derivation below walks the ORIGINAL initializer AST
|
|
6776
|
+
// — for a const-resolved template (`className={classes}`) that AST is
|
|
6777
|
+
// the bare `classes` identifier with no prop refs, so the rewrite
|
|
6778
|
+
// no-ops and the module-scope registration template leaks bare
|
|
6779
|
+
// destructured props into `renderChild(...)` (#2468).
|
|
6780
|
+
const collapsed = templatePartsToJsString(value.parts)
|
|
6781
|
+
const collapsedTemplate = templatePartsToJsString(value.parts, { useTemplate: true })
|
|
6782
|
+
value = AttrValueOf.expression(collapsed, {
|
|
6437
6783
|
parts: value.parts,
|
|
6784
|
+
...(collapsedTemplate !== collapsed && { templateExpr: collapsedTemplate }),
|
|
6438
6785
|
})
|
|
6439
6786
|
} else if (value.kind === 'boolean-attr') {
|
|
6440
6787
|
value = AttrValueOf.booleanShorthand()
|
|
@@ -6479,18 +6826,20 @@ function processComponentProps(
|
|
|
6479
6826
|
* needs to be collapsed into an `expression` for component-prop forwarding —
|
|
6480
6827
|
* component props are runtime JS values, not HTML attribute bodies.
|
|
6481
6828
|
*/
|
|
6482
|
-
function templatePartsToJsString(parts: readonly IRTemplatePart[]): string {
|
|
6829
|
+
function templatePartsToJsString(parts: readonly IRTemplatePart[], opts?: { useTemplate?: boolean }): string {
|
|
6483
6830
|
let result = '`'
|
|
6484
6831
|
for (const part of parts) {
|
|
6485
6832
|
if (part.type === 'string') {
|
|
6486
|
-
result += part.value
|
|
6833
|
+
result += (opts?.useTemplate && part.templateValue) ? part.templateValue : part.value
|
|
6487
6834
|
} else if (part.type === 'ternary') {
|
|
6488
|
-
|
|
6835
|
+
const cond = (opts?.useTemplate && part.templateCondition) ? part.templateCondition : part.condition
|
|
6836
|
+
result += `\${${cond} ? '${part.whenTrue}' : '${part.whenFalse}'}`
|
|
6489
6837
|
} else if (part.type === 'lookup') {
|
|
6838
|
+
const key = (opts?.useTemplate && part.templateKey) ? part.templateKey : part.key
|
|
6490
6839
|
const obj = '{' + Object.entries(part.cases).map(
|
|
6491
6840
|
([k, v]) => `${JSON.stringify(k)}: ${JSON.stringify(v)}`
|
|
6492
6841
|
).join(', ') + '}'
|
|
6493
|
-
result += `\${(${obj})[${
|
|
6842
|
+
result += `\${(${obj})[${key}]}`
|
|
6494
6843
|
}
|
|
6495
6844
|
}
|
|
6496
6845
|
result += '`'
|
|
@@ -6875,14 +7224,21 @@ function replaceBranchLocalRefs(
|
|
|
6875
7224
|
*/
|
|
6876
7225
|
function buildIfStatementChain(
|
|
6877
7226
|
analyzer: AnalyzerContext,
|
|
6878
|
-
ctx: TransformContext
|
|
6879
|
-
|
|
7227
|
+
ctx: TransformContext,
|
|
7228
|
+
opts?: { asConditional?: boolean }
|
|
7229
|
+
): IRIfStatement | IRConditional | null {
|
|
6880
7230
|
const conditionalReturns = analyzer.conditionalReturns
|
|
7231
|
+
// #2463: conditional mode builds the same IRConditional links the root
|
|
7232
|
+
// ternary produces (reactive insert() plan). The caller wraps the chain
|
|
7233
|
+
// in the synthetic display:contents scope element, so branches must NOT
|
|
7234
|
+
// claim the root scope themselves — mirror the ternary path's
|
|
7235
|
+
// `ctx.isRoot = false`.
|
|
7236
|
+
const asConditional = opts?.asConditional === true
|
|
6881
7237
|
|
|
6882
7238
|
// Start with the final return (else case) if it exists
|
|
6883
7239
|
let alternate: IRNode | null = null
|
|
6884
7240
|
if (analyzer.jsxReturn) {
|
|
6885
|
-
ctx.isRoot =
|
|
7241
|
+
ctx.isRoot = !asConditional
|
|
6886
7242
|
alternate = transformNode(analyzer.jsxReturn, ctx)
|
|
6887
7243
|
}
|
|
6888
7244
|
|
|
@@ -7010,8 +7366,9 @@ function buildIfStatementChain(
|
|
|
7010
7366
|
}
|
|
7011
7367
|
|
|
7012
7368
|
// Transform the JSX return in the then branch
|
|
7013
|
-
// Reset isRoot so each branch gets needsScope=true
|
|
7014
|
-
|
|
7369
|
+
// Reset isRoot so each branch gets needsScope=true (statement mode);
|
|
7370
|
+
// in conditional mode (#2463) the synthetic wrapper carries the scope.
|
|
7371
|
+
ctx.isRoot = !asConditional
|
|
7015
7372
|
let consequent: IRNode | null
|
|
7016
7373
|
try {
|
|
7017
7374
|
consequent = transformNode(condReturn.jsxReturn, ctx)
|
|
@@ -7062,6 +7419,33 @@ function buildIfStatementChain(
|
|
|
7062
7419
|
analyzer.filePath
|
|
7063
7420
|
)
|
|
7064
7421
|
|
|
7422
|
+
if (asConditional && alternate) {
|
|
7423
|
+
// #2463: emit the same node shape `transformConditional` builds for
|
|
7424
|
+
// the root ternary, so the downstream insert() plan, slot wrapping,
|
|
7425
|
+
// and template substitution all apply unchanged. Eligibility
|
|
7426
|
+
// (checked by the caller) guarantees a reactive condition and no
|
|
7427
|
+
// branch scopeVariables.
|
|
7428
|
+
const conditional: IRConditional = {
|
|
7429
|
+
type: 'conditional',
|
|
7430
|
+
condition,
|
|
7431
|
+
templateCondition,
|
|
7432
|
+
conditionType: null,
|
|
7433
|
+
reactive: true,
|
|
7434
|
+
whenTrue: consequent,
|
|
7435
|
+
whenFalse: alternate,
|
|
7436
|
+
slotId: generateSlotId(ctx),
|
|
7437
|
+
loc,
|
|
7438
|
+
origin: {
|
|
7439
|
+
phase: 'tick',
|
|
7440
|
+
scope: 'template',
|
|
7441
|
+
effect: 'pure',
|
|
7442
|
+
freeRefs: resolveFreeRefs(condReturn.condition, makeBindingEnv(ctx)),
|
|
7443
|
+
},
|
|
7444
|
+
}
|
|
7445
|
+
alternate = conditional
|
|
7446
|
+
continue
|
|
7447
|
+
}
|
|
7448
|
+
|
|
7065
7449
|
// Create the if statement node
|
|
7066
7450
|
const ifStmt: IRIfStatement = {
|
|
7067
7451
|
type: 'if-statement',
|
|
@@ -7077,6 +7461,7 @@ function buildIfStatementChain(
|
|
|
7077
7461
|
alternate = ifStmt
|
|
7078
7462
|
}
|
|
7079
7463
|
|
|
7080
|
-
// The final result
|
|
7081
|
-
|
|
7464
|
+
// The final result: the first link of the chain (an IRIfStatement in
|
|
7465
|
+
// statement mode, an IRConditional in #2463's conditional mode).
|
|
7466
|
+
return alternate as IRIfStatement | IRConditional | null
|
|
7082
7467
|
}
|