@barefootjs/jsx 0.32.0 → 0.33.1
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/dangerous-inner-html.d.ts.map +1 -1
- package/dist/adapters/parsed-expr-emitter.d.ts +22 -0
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +29 -6
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +274 -80
- package/dist/ir-to-client-js/collect-elements.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-child-arm.d.ts +8 -0
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.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/inner-loop.d.ts +12 -0
- package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.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/element-refs.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.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/phases/provider-and-child-inits.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +21 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/query-href-lowering.d.ts.map +1 -1
- package/dist/ssr-seed-plan.d.ts.map +1 -1
- package/dist/static-literal.d.ts.map +1 -1
- package/dist/to-locale-date-lowering.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +2 -4
- package/src/__tests__/child-components-in-map.test.ts +11 -3
- package/src/__tests__/client-js-generation.test.ts +37 -1
- package/src/__tests__/expression-parser.test.ts +43 -6
- package/src/__tests__/inline-jsx-callback.test.ts +55 -0
- package/src/__tests__/ir-jsx-props.test.ts +148 -0
- package/src/__tests__/issue-2705-branch-inner-loop-container.test.ts +91 -0
- package/src/__tests__/markup-prop-brand.test.ts +49 -0
- package/src/__tests__/nested-loop-conditional.test.ts +20 -11
- package/src/__tests__/return-through-local-var.test.ts +269 -0
- package/src/__tests__/serialize-parsed-expr.test.ts +17 -3
- package/src/__tests__/ssr-defaults.test.ts +41 -0
- package/src/__tests__/ssr-seed-plan.test.ts +12 -2
- package/src/adapters/dangerous-inner-html.ts +1 -0
- package/src/adapters/parsed-expr-emitter.ts +49 -3
- package/src/analyzer.ts +71 -0
- package/src/errors.ts +17 -1
- package/src/expression-parser.ts +200 -89
- package/src/index.ts +2 -2
- package/src/ir-to-client-js/collect-elements.ts +31 -20
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +19 -0
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +22 -3
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +5 -2
- package/src/ir-to-client-js/control-flow/plan/inner-loop.ts +12 -0
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +9 -0
- package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +7 -1
- package/src/ir-to-client-js/element-refs.ts +8 -0
- package/src/ir-to-client-js/emit-reactive.ts +91 -23
- package/src/ir-to-client-js/imports.ts +5 -0
- package/src/ir-to-client-js/index.ts +4 -0
- package/src/ir-to-client-js/phases/provider-and-child-inits.ts +5 -1
- package/src/ir-to-client-js/types.ts +21 -0
- package/src/jsx-to-ir.ts +161 -9
- package/src/query-href-lowering.ts +4 -0
- package/src/rich-type-refusal.ts +1 -1
- package/src/ssr-defaults.ts +51 -2
- package/src/ssr-seed-plan.ts +20 -3
- package/src/static-literal.ts +15 -1
- package/src/to-locale-date-lowering.ts +4 -0
package/src/jsx-to-ir.ts
CHANGED
|
@@ -1066,7 +1066,37 @@ function buildIRRoot(analyzer: AnalyzerContext): IRNode | null {
|
|
|
1066
1066
|
// scope; the inner IR must not double-mark a nested element as root.
|
|
1067
1067
|
ctx.isRoot = false
|
|
1068
1068
|
const ir = transformJsxExpression(jsxReturn, ctx)
|
|
1069
|
-
if (ir === null)
|
|
1069
|
+
if (ir === null) {
|
|
1070
|
+
// BF027 (#2720): a bare identifier at return position that names a
|
|
1071
|
+
// local `const`/`let` PROVEN to hold JSX (`jsxConstants` — pure JSX
|
|
1072
|
+
// literal; `inlineableJsxConsts` — a JSX-shaped ternary/`&&`/`||`/`??`)
|
|
1073
|
+
// is the "returned JSX through a local variable" shape. Return position
|
|
1074
|
+
// deliberately does not resolve identifiers the way JSX-child position
|
|
1075
|
+
// does (see the #547/#1409 inlining above), so the scalar-leaf fallback
|
|
1076
|
+
// would otherwise drop this component with zero files and zero
|
|
1077
|
+
// diagnostics. Scoped to identifiers already proven JSX-holding by
|
|
1078
|
+
// those two maps so an ordinary non-JSX return (`return 42`, `return
|
|
1079
|
+
// someHelperResult`) — including from a PascalCase-but-not-a-component
|
|
1080
|
+
// export the analyzer's syntactic component detector still matches —
|
|
1081
|
+
// stays silent exactly as before.
|
|
1082
|
+
if (
|
|
1083
|
+
ts.isIdentifier(jsxReturn) &&
|
|
1084
|
+
(analyzer.jsxConstants.has(jsxReturn.text) || analyzer.inlineableJsxConsts.has(jsxReturn.text))
|
|
1085
|
+
) {
|
|
1086
|
+
analyzer.errors.push(createError(
|
|
1087
|
+
ErrorCodes.RETURN_VALUE_NOT_JSX,
|
|
1088
|
+
getSourceLocation(jsxReturn, analyzer.sourceFile, analyzer.filePath),
|
|
1089
|
+
{
|
|
1090
|
+
message:
|
|
1091
|
+
`Component '${analyzer.componentName ?? '(unknown)'}' return value is not recognized ` +
|
|
1092
|
+
`as JSX — return the JSX expression directly instead of binding it to a local variable ` +
|
|
1093
|
+
`first (\`return ${jsxReturn.text}\` after \`const ${jsxReturn.text} = <jsx/>\` is not ` +
|
|
1094
|
+
`resolved at return position).`,
|
|
1095
|
+
},
|
|
1096
|
+
))
|
|
1097
|
+
}
|
|
1098
|
+
return null
|
|
1099
|
+
}
|
|
1070
1100
|
return wrapInScopeElement(ir)
|
|
1071
1101
|
}
|
|
1072
1102
|
|
|
@@ -3296,7 +3326,10 @@ function resolveCallbackMethodFunctionReferences(
|
|
|
3296
3326
|
...(e.method === 'flat' && e.depthExpr ? { depthExpr: visit(e.depthExpr, bound) } : {}),
|
|
3297
3327
|
} as ParsedExpr
|
|
3298
3328
|
case 'object-literal':
|
|
3299
|
-
return {
|
|
3329
|
+
return {
|
|
3330
|
+
...e,
|
|
3331
|
+
properties: e.properties.map(p => (p.kind === 'spread' ? { ...p, expr: visit(p.expr, bound) } : { ...p, value: visit(p.value, bound) })),
|
|
3332
|
+
}
|
|
3300
3333
|
case 'arrow': {
|
|
3301
3334
|
const inner = e.params.length === 0 ? bound : new Set([...bound, ...e.params])
|
|
3302
3335
|
return { ...e, body: visit(e.body, inner) }
|
|
@@ -6996,6 +7029,100 @@ function getStringValue(node: ts.Expression): string | null {
|
|
|
6996
7029
|
// Component Props Processing
|
|
6997
7030
|
// =============================================================================
|
|
6998
7031
|
|
|
7032
|
+
/**
|
|
7033
|
+
* Strip every legal-in-.tsx TRANSPARENT TS wrapper around an expression —
|
|
7034
|
+
* parens, `as`, `satisfies`, and postfix non-null `!` — repeatedly, so a
|
|
7035
|
+
* stack of them (`(x as any)!`) unwraps in one call. None of these change
|
|
7036
|
+
* the RUNTIME value; they are compile-time-only annotations TypeScript
|
|
7037
|
+
* erases, so a caller checking "is this JSX" (or "does this ternary/array
|
|
7038
|
+
* wrap JSX") must see through all of them to avoid false negatives (#2703
|
|
7039
|
+
* Copilot review on #2667: `header={cond ? (<a/> as any) : (<b/> as any)}`
|
|
7040
|
+
* unwrapped only parens, so the `as`-wrapped shape slipped past the naked-
|
|
7041
|
+
* wrapper refusal and still spliced raw JSX into the client bundle).
|
|
7042
|
+
* Angle-bracket type assertions (`<any>x`) are illegal in `.tsx` — the
|
|
7043
|
+
* `<` is always JSX — so there is no fourth wrapper kind to handle here.
|
|
7044
|
+
*/
|
|
7045
|
+
function unwrapTransparentTsWrappers(node: ts.Expression): ts.Expression {
|
|
7046
|
+
let n = node
|
|
7047
|
+
while (
|
|
7048
|
+
ts.isParenthesizedExpression(n) ||
|
|
7049
|
+
ts.isAsExpression(n) ||
|
|
7050
|
+
ts.isSatisfiesExpression(n) ||
|
|
7051
|
+
ts.isNonNullExpression(n)
|
|
7052
|
+
) {
|
|
7053
|
+
n = n.expression
|
|
7054
|
+
}
|
|
7055
|
+
return n
|
|
7056
|
+
}
|
|
7057
|
+
|
|
7058
|
+
/**
|
|
7059
|
+
* Whether `node` (a `ConditionalExpression` or `ArrayLiteralExpression`
|
|
7060
|
+
* already confirmed by the caller) has JSX syntax somewhere inside it —
|
|
7061
|
+
* a ternary arm that is (or recursively resolves to) a JSX element/
|
|
7062
|
+
* fragment, or an array element that is one. Used by #2667's naked-
|
|
7063
|
+
* wrapper refusal: a ternary/array prop initializer with NO JSX inside
|
|
7064
|
+
* (`disabled={cond ? a : b}`, `items={[a, b]}`) is ordinary and must keep
|
|
7065
|
+
* compiling exactly as before — only the JSX-carrying shape is refused.
|
|
7066
|
+
*
|
|
7067
|
+
* Deliberately narrow, mirroring `expressionContainsJsx`'s sibling scope
|
|
7068
|
+
* in `analyzer.ts`-adjacent code: only descends through transparent TS
|
|
7069
|
+
* wrappers (`unwrapTransparentTsWrappers`), `ConditionalExpression` arms,
|
|
7070
|
+
* and `ArrayLiteralExpression` elements (spread elements unwrapped too)
|
|
7071
|
+
* — the two wrapper shapes the issue names, each itself possibly
|
|
7072
|
+
* TS-wrapped (`(cond ? <a/> : <b/>) as any`, `cond ? (<a/> as any) : <b/>`,
|
|
7073
|
+
* #2703). It does not chase JSX through arbitrary call arguments, object
|
|
7074
|
+
* literals, or logical expressions; those are out of this refusal's
|
|
7075
|
+
* scope and stay on the pre-existing (working, non-JSX) expression path.
|
|
7076
|
+
*/
|
|
7077
|
+
function expressionWrapsJsx(node: ts.Expression): boolean {
|
|
7078
|
+
const n = unwrapTransparentTsWrappers(node)
|
|
7079
|
+
if (ts.isJsxElement(n) || ts.isJsxSelfClosingElement(n) || ts.isJsxFragment(n)) return true
|
|
7080
|
+
if (ts.isConditionalExpression(n)) {
|
|
7081
|
+
return expressionWrapsJsx(n.whenTrue) || expressionWrapsJsx(n.whenFalse)
|
|
7082
|
+
}
|
|
7083
|
+
if (ts.isArrayLiteralExpression(n)) {
|
|
7084
|
+
return n.elements.some((el) => expressionWrapsJsx(ts.isSpreadElement(el) ? el.expression : el))
|
|
7085
|
+
}
|
|
7086
|
+
return false
|
|
7087
|
+
}
|
|
7088
|
+
|
|
7089
|
+
/**
|
|
7090
|
+
* Refuse a component prop whose initializer is a ternary/array literally
|
|
7091
|
+
* wrapping JSX (#2667) — see `expressionWrapsJsx`'s docstring and the call
|
|
7092
|
+
* site above for why this can't fall through to the plain `expression`
|
|
7093
|
+
* AttrValue path (raw JSX syntax would splice into the emitted client
|
|
7094
|
+
* JS) and why the fragment-wrap escape is NOT offered here (BF021 message
|
|
7095
|
+
* below explains the unsoundness inline; see #2667's tracking issue for
|
|
7096
|
+
* the full door-inventory finding).
|
|
7097
|
+
*/
|
|
7098
|
+
function reportNakedJsxWrapperProp(
|
|
7099
|
+
ctx: TransformContext,
|
|
7100
|
+
attr: ts.JsxAttribute,
|
|
7101
|
+
propName: string,
|
|
7102
|
+
jsxExpr: ts.Expression,
|
|
7103
|
+
): void {
|
|
7104
|
+
const shape = ts.isConditionalExpression(jsxExpr) ? 'a ternary' : 'an array literal'
|
|
7105
|
+
ctx.analyzer.errors.push(
|
|
7106
|
+
createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN, getSourceLocation(attr, ctx.sourceFile, ctx.filePath), {
|
|
7107
|
+
message:
|
|
7108
|
+
`Prop '${propName}' is ${shape} wrapping JSX (${jsxExpr.getText(ctx.sourceFile)}). ` +
|
|
7109
|
+
`This shape is not compiled — only a JSX element/fragment given DIRECTLY as the prop value is.`,
|
|
7110
|
+
suggestion: {
|
|
7111
|
+
message:
|
|
7112
|
+
`Move the conditional/array out of the prop position: compute it in a local ` +
|
|
7113
|
+
`const and pass it as the component's children instead of a named prop ` +
|
|
7114
|
+
`(e.g. const ${propName} = ${jsxExpr.getText(ctx.sourceFile)}; <Comp>{${propName}}</Comp>). ` +
|
|
7115
|
+
`Wrapping the ternary/array in a fragment at the prop position ` +
|
|
7116
|
+
`(${propName}={<>{${jsxExpr.getText(ctx.sourceFile)}}</>}) is NOT a safe escape here: it compiles, ` +
|
|
7117
|
+
`but the child's own reactive prop getter receives the branch's HTML unbranded and re-escapes it as ` +
|
|
7118
|
+
`text on the child's very next reactive run, corrupting the DOM (a narrower gap #2651's door ` +
|
|
7119
|
+
`inventory left open — tracked separately).`,
|
|
7120
|
+
escape: [{ kind: 'rewrite' }],
|
|
7121
|
+
},
|
|
7122
|
+
}),
|
|
7123
|
+
)
|
|
7124
|
+
}
|
|
7125
|
+
|
|
6999
7126
|
function processComponentProps(
|
|
7000
7127
|
attributes: ts.JsxAttributes,
|
|
7001
7128
|
ctx: TransformContext
|
|
@@ -7012,14 +7139,14 @@ function processComponentProps(
|
|
|
7012
7139
|
|
|
7013
7140
|
const name = attr.name.getText(ctx.sourceFile)
|
|
7014
7141
|
|
|
7015
|
-
// JSX element/fragment as prop value: controls={<select />}
|
|
7016
|
-
// controls={(<div/>)}
|
|
7017
|
-
//
|
|
7142
|
+
// JSX element/fragment as prop value: controls={<select />},
|
|
7143
|
+
// controls={(<div/>)}, or controls={<div/> as any} (#2703 — the
|
|
7144
|
+
// entity is still directly JSX; `as`/`satisfies`/`!` are type-only
|
|
7145
|
+
// and erased, so this must classify identically to the bare form).
|
|
7146
|
+
// Carried as a `jsx-children` AttrValue variant so adapters render
|
|
7147
|
+
// the JSX inline rather than passing a string.
|
|
7018
7148
|
if (attr.initializer && ts.isJsxExpression(attr.initializer) && attr.initializer.expression) {
|
|
7019
|
-
|
|
7020
|
-
while (ts.isParenthesizedExpression(jsxExpr)) {
|
|
7021
|
-
jsxExpr = jsxExpr.expression
|
|
7022
|
-
}
|
|
7149
|
+
const jsxExpr = unwrapTransparentTsWrappers(attr.initializer.expression)
|
|
7023
7150
|
if (ts.isJsxElement(jsxExpr) || ts.isJsxSelfClosingElement(jsxExpr) || ts.isJsxFragment(jsxExpr)) {
|
|
7024
7151
|
const prevInsideComponentChildren = ctx.insideComponentChildren
|
|
7025
7152
|
ctx.insideComponentChildren = true
|
|
@@ -7034,6 +7161,31 @@ function processComponentProps(
|
|
|
7034
7161
|
continue
|
|
7035
7162
|
}
|
|
7036
7163
|
}
|
|
7164
|
+
|
|
7165
|
+
// #2667: a ternary or array LITERALLY WRAPPING JSX at this prop
|
|
7166
|
+
// position (`header={cond ? <a/> : <b/>}`, `header={[<a/>, <b/>]}`)
|
|
7167
|
+
// is neither the direct-element shape above nor a plain value
|
|
7168
|
+
// expression — it only classifies as `jsx-children` when the JSX is
|
|
7169
|
+
// hoisted behind a fragment (`header={<>{cond ? <a/> : <b/>}</>}`,
|
|
7170
|
+
// `unwrapHoistedFragment` above). Left undetected, this falls to the
|
|
7171
|
+
// plain `expression` path below, which stringifies the initializer's
|
|
7172
|
+
// SOURCE TEXT — literal JSX syntax spliced into the emitted client
|
|
7173
|
+
// JS (invalid at runtime; the #2651 door inventory's discovery).
|
|
7174
|
+
// Refuse loudly instead of guessing a lowering: the fragment-wrap
|
|
7175
|
+
// escape this diagnostic once considered recommending turns out to
|
|
7176
|
+
// be unsound for the same shape (`isSingleElementJsxChildren`'s
|
|
7177
|
+
// docstring in `ir-to-client-js/collect-elements.ts` — a
|
|
7178
|
+
// conditional-in-fragment reaches `initChild`'s getter UNbranded,
|
|
7179
|
+
// corrupting the child's DOM the moment its own reactive effect
|
|
7180
|
+
// first reads the prop), so only the children-passthrough escape is
|
|
7181
|
+
// offered.
|
|
7182
|
+
if (
|
|
7183
|
+
(ts.isConditionalExpression(jsxExpr) || ts.isArrayLiteralExpression(jsxExpr)) &&
|
|
7184
|
+
expressionWrapsJsx(jsxExpr)
|
|
7185
|
+
) {
|
|
7186
|
+
reportNakedJsxWrapperProp(ctx, attr, name, jsxExpr)
|
|
7187
|
+
continue
|
|
7188
|
+
}
|
|
7037
7189
|
}
|
|
7038
7190
|
|
|
7039
7191
|
let value = getAttributeValue(attr, ctx)
|
|
@@ -58,6 +58,10 @@ export function matchQueryHrefCall(
|
|
|
58
58
|
|
|
59
59
|
const triples: QueryHrefTriple[] = []
|
|
60
60
|
for (const p of obj.properties) {
|
|
61
|
+
// A spread (`{ ...extra, key: v }`, #2696 Step 2) has no static key to
|
|
62
|
+
// build a triple from — fall back to the generic lowering rather than
|
|
63
|
+
// dropping the spread's keys silently.
|
|
64
|
+
if (p.kind === 'spread') return null
|
|
61
65
|
const v = p.value
|
|
62
66
|
if (v.kind === 'conditional' && isOmitBranch(v.alternate)) {
|
|
63
67
|
triples.push({ guard: v.test, key: p.key, value: v.consequent })
|
package/src/rich-type-refusal.ts
CHANGED
|
@@ -348,7 +348,7 @@ function checkExpr(
|
|
|
348
348
|
for (const el of expr.elements) recurse(el)
|
|
349
349
|
break
|
|
350
350
|
case 'object-literal':
|
|
351
|
-
for (const prop of expr.properties) recurse(prop.value)
|
|
351
|
+
for (const prop of expr.properties) recurse(prop.kind === 'spread' ? prop.expr : prop.value)
|
|
352
352
|
break
|
|
353
353
|
case 'array-method':
|
|
354
354
|
recurse(expr.object)
|
package/src/ssr-defaults.ts
CHANGED
|
@@ -647,12 +647,28 @@ function evalNode(node: ts.Expression, ctx: EvalContext): EvalResult {
|
|
|
647
647
|
}
|
|
648
648
|
|
|
649
649
|
if (ts.isPropertyAccessExpression(node)) {
|
|
650
|
+
const baseResult = evalNode(node.expression, ctx)
|
|
650
651
|
// `props.X` / `props?.X` — read of a binding we know nothing about, so
|
|
651
652
|
// resolve to `undefined`. Chained access (`a.b.c`) collapses the same way
|
|
652
653
|
// because the base read is already undefined.
|
|
653
|
-
const baseResult = evalNode(node.expression, ctx)
|
|
654
654
|
if (baseResult === undefined) return undefined
|
|
655
|
-
|
|
655
|
+
if (baseResult === UNRESOLVED || baseResult === null || typeof baseResult !== 'object') {
|
|
656
|
+
return UNRESOLVED
|
|
657
|
+
}
|
|
658
|
+
// Own-property reads are only faithful for a plain object. An array (or
|
|
659
|
+
// any other non-plain object) exposes prototype members (`.map`,
|
|
660
|
+
// `.length`) that `hasOwnProperty` would wrongly resolve to `undefined`
|
|
661
|
+
// instead of the real function/value (#2698 review).
|
|
662
|
+
const proto = Object.getPrototypeOf(baseResult)
|
|
663
|
+
if (proto !== Object.prototype && proto !== null) return UNRESOLVED
|
|
664
|
+
// A resolved plain-object base — an object-literal value, e.g. a
|
|
665
|
+
// `.map()` callback param bound to one element (#2696's `t.a`). Missing
|
|
666
|
+
// key → JS `undefined`, mirroring `isElementAccessExpression`'s own
|
|
667
|
+
// missing-key handling above.
|
|
668
|
+
const key = node.name.text
|
|
669
|
+
return Object.prototype.hasOwnProperty.call(baseResult, key)
|
|
670
|
+
? (baseResult as Record<string, unknown>)[key]
|
|
671
|
+
: undefined
|
|
656
672
|
}
|
|
657
673
|
|
|
658
674
|
if (ts.isCallExpression(node)) {
|
|
@@ -665,6 +681,39 @@ function evalNode(node: ts.Expression, ctx: EvalContext): EvalResult {
|
|
|
665
681
|
) {
|
|
666
682
|
return ctx.bindings[node.expression.text]
|
|
667
683
|
}
|
|
684
|
+
// `<array>.map(cb)` — a single-param, expression-bodied arrow over a
|
|
685
|
+
// resolved array receiver; anything else (block body, multi-param,
|
|
686
|
+
// non-array receiver) stays `UNRESOLVED`, same conservative narrowing as
|
|
687
|
+
// every other arm here. A `derived` step with empty `frees` gets no
|
|
688
|
+
// in-template recompute (#2696), so this static value is what production
|
|
689
|
+
// actually reads.
|
|
690
|
+
if (
|
|
691
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
692
|
+
node.expression.name.text === 'map' &&
|
|
693
|
+
node.arguments.length === 1
|
|
694
|
+
) {
|
|
695
|
+
const arrow = node.arguments[0]
|
|
696
|
+
if (
|
|
697
|
+
ts.isArrowFunction(arrow) &&
|
|
698
|
+
arrow.parameters.length === 1 &&
|
|
699
|
+
ts.isIdentifier(arrow.parameters[0].name) &&
|
|
700
|
+
!ts.isBlock(arrow.body)
|
|
701
|
+
) {
|
|
702
|
+
const recv = evalNode(node.expression.expression, ctx)
|
|
703
|
+
if (Array.isArray(recv)) {
|
|
704
|
+
const paramName = (arrow.parameters[0].name as ts.Identifier).text
|
|
705
|
+
const mapped: unknown[] = []
|
|
706
|
+
for (const item of recv) {
|
|
707
|
+
const localBindings: Record<string, EvalResult> = { ...ctx.bindings, [paramName]: item }
|
|
708
|
+
const v = evalNode(arrow.body as ts.Expression, { ...ctx, bindings: localBindings })
|
|
709
|
+
if (v === UNRESOLVED) return UNRESOLVED
|
|
710
|
+
mapped.push(v === undefined ? null : v)
|
|
711
|
+
}
|
|
712
|
+
return mapped
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return UNRESOLVED
|
|
716
|
+
}
|
|
668
717
|
// `<array>.join(<sep?>)` — evaluate when the receiver resolves to an array
|
|
669
718
|
// and the separator (default `,`) is a string. Covers `stateClasses =
|
|
670
719
|
// [...].join(' ')` (#checkbox). Other array methods stay unresolved.
|
package/src/ssr-seed-plan.ts
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
extractArrowBodyExpression,
|
|
37
37
|
freeIdentifiers,
|
|
38
38
|
inlineBinding,
|
|
39
|
-
|
|
39
|
+
isSupportedValue,
|
|
40
40
|
parseExpression,
|
|
41
41
|
type ParsedExpr,
|
|
42
42
|
} from './expression-parser.ts'
|
|
@@ -81,6 +81,11 @@ export interface SsrSeedPlan {
|
|
|
81
81
|
* opaque). The scope check runs over the parsed SOURCE tree, so a shadowed
|
|
82
82
|
* name (`items.filter((p) => p.ok) && p`, where the trailing `p` is a
|
|
83
83
|
* different, unbound reference from the callback's own param) is rejected.
|
|
84
|
+
*
|
|
85
|
+
* Uses `isSupportedValue`, not `isSupported`: a signal/memo initializer is an
|
|
86
|
+
* ASSIGNMENT, never a render, so it's checked at VALUE position — this is
|
|
87
|
+
* what admits e.g. `createSignal([{ a: 'p' }].map(t => t.a).join(','))`
|
|
88
|
+
* (an object-literal array-method receiver) as `derived` instead of opaque.
|
|
84
89
|
*/
|
|
85
90
|
function classify(
|
|
86
91
|
name: string,
|
|
@@ -89,7 +94,7 @@ function classify(
|
|
|
89
94
|
parsed: ParsedExpr,
|
|
90
95
|
available: ReadonlySet<string>,
|
|
91
96
|
): SsrSeedStep {
|
|
92
|
-
if (!
|
|
97
|
+
if (!isSupportedValue(parsed).supported) return { kind: 'opaque', name, origin }
|
|
93
98
|
const frees = freeIdentifiers(parsed)
|
|
94
99
|
if (frees === null) return { kind: 'opaque', name, origin }
|
|
95
100
|
for (const free of frees) {
|
|
@@ -198,7 +203,19 @@ export function computeSsrSeedPlan(metadata: IRMetadata): SsrSeedPlan {
|
|
|
198
203
|
signal.getter,
|
|
199
204
|
'signal',
|
|
200
205
|
expr,
|
|
201
|
-
|
|
206
|
+
// Prefer the analyzer's already-parenthesised parse (`analyzer.ts`'s
|
|
207
|
+
// `parseExpression(\`(${signal.initialValue})\`)`, Roadmap A) over
|
|
208
|
+
// re-parsing the bare `expr` string here — re-parsing WITHOUT the
|
|
209
|
+
// wrap misreads a bare object-literal initializer
|
|
210
|
+
// (`createSignal({ ...base, done: true })`) as a block statement
|
|
211
|
+
// (`parseExpression`'s documented block-vs-expression-statement
|
|
212
|
+
// rule), silently opaquing an otherwise-derivable signal (#2696
|
|
213
|
+
// Step 2 follow-up: this only became observable once `object-
|
|
214
|
+
// literal` could classify `derived` at all). Falling back to a
|
|
215
|
+
// parenthesised re-parse (not the bare string) keeps a signal
|
|
216
|
+
// whose shape the analyzer's pass left unparsed (`signal.parsed`
|
|
217
|
+
// undefined) equally safe.
|
|
218
|
+
resolveThroughLocalConsts(signal.parsed ?? parseExpression(`(${expr})`), localConsts),
|
|
202
219
|
available,
|
|
203
220
|
),
|
|
204
221
|
)
|
package/src/static-literal.ts
CHANGED
|
@@ -60,9 +60,23 @@ export function evaluateStaticLiteral(
|
|
|
60
60
|
// Shorthand (`{ a }`) and explicit (`{ a: value }`) properties both
|
|
61
61
|
// carry their resolved tree in `value` (shorthand's is an
|
|
62
62
|
// `identifier`) — recursing here handles both uniformly: a shorthand
|
|
63
|
-
// property only resolves when `bindings` supplies it.
|
|
63
|
+
// property only resolves when `bindings` supplies it. A spread
|
|
64
|
+
// (`{ ...base, a: 1 }`, #2696 Step 2) resolves its source and merges
|
|
65
|
+
// in source order — the same shallow-merge, later-wins, null/undefined-
|
|
66
|
+
// is-a-no-op semantics as the runtime evaluator (`toEvalNode`'s
|
|
67
|
+
// `object-literal` case) and JS itself; a spread source that resolves
|
|
68
|
+
// to anything other than a plain object/null/undefined isn't
|
|
69
|
+
// statically mergeable here, so the whole literal declines.
|
|
64
70
|
const out: Record<string, unknown> = {}
|
|
65
71
|
for (const prop of expr.properties) {
|
|
72
|
+
if (prop.kind === 'spread') {
|
|
73
|
+
const resolved = evaluateStaticLiteral(prop.expr, bindings)
|
|
74
|
+
if (!resolved) return null
|
|
75
|
+
if (resolved.value === null || resolved.value === undefined) continue
|
|
76
|
+
if (typeof resolved.value !== 'object' || Array.isArray(resolved.value)) return null
|
|
77
|
+
Object.assign(out, resolved.value)
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
66
80
|
const resolved = evaluateStaticLiteral(prop.value, bindings)
|
|
67
81
|
if (!resolved) return null
|
|
68
82
|
out[prop.key] = resolved.value
|
|
@@ -504,6 +504,10 @@ export function matchToLocaleDateStringCall(
|
|
|
504
504
|
let tz: string | null = null
|
|
505
505
|
const probeOptions: Record<string, string> = {}
|
|
506
506
|
for (const prop of options.properties) {
|
|
507
|
+
// A spread (`{ ...opts, timeZone: 'UTC' }`, #2696 Step 2) isn't a
|
|
508
|
+
// literal-keyed entry this probe can read without evaluating the spread
|
|
509
|
+
// source — decline rather than silently skipping its keys.
|
|
510
|
+
if (prop.kind === 'spread') return null
|
|
507
511
|
if (prop.value.kind !== 'literal' || prop.value.literalType !== 'string') return null
|
|
508
512
|
const value = String(prop.value.value)
|
|
509
513
|
if (prop.key === 'timeZone') {
|