@barefootjs/jsx 0.32.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/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/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 +129 -40
- 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__/expression-parser.test.ts +43 -6
- 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/expression-parser.ts +200 -89
- package/src/index.ts +2 -2
- package/src/jsx-to-ir.ts +4 -1
- 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/expression-parser.ts
CHANGED
|
@@ -68,16 +68,19 @@ export type ParsedExpr =
|
|
|
68
68
|
// `unsupported`.
|
|
69
69
|
| { kind: 'regex'; raw: string }
|
|
70
70
|
| { kind: 'array-literal'; elements: ParsedExpr[] }
|
|
71
|
-
// Object literal `{ a: 1, b: x }` / shorthand `{ a }
|
|
72
|
-
//
|
|
73
|
-
// Perl hashref) can emit
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
// `
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
71
|
+
// Object literal `{ a: 1, b: x }` / shorthand `{ a }` / spread
|
|
72
|
+
// `{ ...t, a: 1 }` (#2696 Step 2). Carried so an adapter that lowers an
|
|
73
|
+
// object *value* (Go `map[string]interface{}`, Perl hashref) can emit
|
|
74
|
+
// from structure instead of re-parsing the source with
|
|
75
|
+
// `ts.createSourceFile`. `properties` is an ORDER-PRESERVING list of
|
|
76
|
+
// `prop` / `spread` entries — order carries JS's override semantics
|
|
77
|
+
// (`{...t, k: v}` vs `{k: v, ...t}` differ in which value wins a shared
|
|
78
|
+
// key), so it is one list, never split into separate spread/prop
|
|
79
|
+
// arrays. A computed key, method, or getter/setter still falls through
|
|
80
|
+
// to `unsupported` (unchanged). `raw` is the original expression
|
|
81
|
+
// string — the same value the old `unsupported` fallback carried — so
|
|
82
|
+
// an adapter that does not yet consume `properties` stays byte-identical
|
|
83
|
+
// by emitting it exactly as it emits `unsupported`. Extending the type
|
|
81
84
|
// adds a TS compile error in every exhaustive `ParsedExpr` switch, the
|
|
82
85
|
// same drift defence used for `array-literal` / `array-method`.
|
|
83
86
|
| { kind: 'object-literal'; properties: ObjectLiteralProperty[]; raw: string }
|
|
@@ -234,25 +237,45 @@ const _arrayMethodRegistryIsExhaustive: MissingFromArrayMethodRegistry extends n
|
|
|
234
237
|
void _arrayMethodRegistryIsExhaustive
|
|
235
238
|
|
|
236
239
|
/**
|
|
237
|
-
* One
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
* `
|
|
240
|
+
* One entry of an `object-literal` `ParsedExpr`, in SOURCE ORDER — a `prop`
|
|
241
|
+
* (`{ a: 1 }` / shorthand `{ a }`) or a `spread` (`{ ...t }`, #2696 Step 2).
|
|
242
|
+
* Kept as one discriminated-union list rather than separate prop/spread
|
|
243
|
+
* arrays because JS's override semantics are POSITIONAL — `{ ...t, a: 1 }`
|
|
244
|
+
* and `{ a: 1, ...t }` merge in opposite directions — and only a single
|
|
245
|
+
* ordered list can carry that. Every direct consumer of `properties`
|
|
246
|
+
* (emitters, `freeVarsInBody`, `inlineBinding`, the rewrite walkers,
|
|
247
|
+
* `objectLiteralTo*` adapter helpers, …) is exhaustive over `kind`, so a
|
|
248
|
+
* TS compile error surfaces any site this Step 2 change didn't already
|
|
249
|
+
* update.
|
|
242
250
|
*/
|
|
243
|
-
export type ObjectLiteralProperty =
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
251
|
+
export type ObjectLiteralProperty =
|
|
252
|
+
| {
|
|
253
|
+
kind: 'prop'
|
|
254
|
+
// The resolved (non-computed) property name — for `{ a: 1 }` and
|
|
255
|
+
// shorthand `{ a }` it is `a`; for `{ 'a-b': 1 }` it is `a-b`.
|
|
256
|
+
// Computed keys (`{ [k]: 1 }`) are not represented; such literals
|
|
257
|
+
// fall through to `unsupported` at parse time.
|
|
258
|
+
key: string
|
|
259
|
+
// The syntactic kind of the key, since `key` normalises all three to a
|
|
260
|
+
// string and so loses the distinction. A consumer that must treat a
|
|
261
|
+
// numeric key (`{ 1: 'a' }`) differently from a same-text string key
|
|
262
|
+
// (`{ '1': 'a' }`) reads this; most consumers ignore it. `identifier`
|
|
263
|
+
// for shorthand.
|
|
264
|
+
keyKind?: 'identifier' | 'string' | 'numeric'
|
|
265
|
+
// Shorthand `{ a }` (the value is the identifier `a`) vs explicit
|
|
266
|
+
// `{ a: <value> }`. The `value` already carries the resolved tree
|
|
267
|
+
// either way; this flag is kept for re-stringification fidelity.
|
|
268
|
+
shorthand: boolean
|
|
269
|
+
value: ParsedExpr
|
|
270
|
+
}
|
|
271
|
+
| {
|
|
272
|
+
kind: 'spread'
|
|
273
|
+
// The spread SOURCE expression (`t` in `{ ...t }`) — carried, not
|
|
274
|
+
// pre-flattened, so a consumer that can't merge structurally
|
|
275
|
+
// (`checkSupport`, the walkers) can still recurse into it uniformly
|
|
276
|
+
// with every other value-position `ParsedExpr`.
|
|
277
|
+
expr: ParsedExpr
|
|
278
|
+
}
|
|
256
279
|
|
|
257
280
|
/**
|
|
258
281
|
* One comparison key inside a sort comparator. A simple
|
|
@@ -1000,7 +1023,7 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
1000
1023
|
// surface — the depth must be *expressible*, even if not
|
|
1001
1024
|
// known until render time.
|
|
1002
1025
|
const parsedDepth = convertNode(depthNode, raw)
|
|
1003
|
-
if (checkSupport(parsedDepth).supported) {
|
|
1026
|
+
if (checkSupport(parsedDepth, 'rendered').supported) {
|
|
1004
1027
|
depthExpr = parsedDepth
|
|
1005
1028
|
flatDepth = 1 // unused placeholder — consumers must check `depthExpr` first
|
|
1006
1029
|
} else {
|
|
@@ -1215,23 +1238,25 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
1215
1238
|
return { kind: 'array-literal', elements }
|
|
1216
1239
|
}
|
|
1217
1240
|
|
|
1218
|
-
// Object literal: { a: 1, b: x, shorthand }.
|
|
1219
|
-
//
|
|
1220
|
-
//
|
|
1221
|
-
// getter/setter) falls through to the generic `unsupported`
|
|
1222
|
-
// below, exactly as before this kind existed.
|
|
1241
|
+
// Object literal: { a: 1, b: x, shorthand, ...spread }. Every property
|
|
1242
|
+
// must be a non-computed `key: value`, a shorthand `{ key }`, or a
|
|
1243
|
+
// spread `{ ...expr }` (#2696 Step 2). Anything else (computed key,
|
|
1244
|
+
// method, getter/setter) falls through to the generic `unsupported`
|
|
1245
|
+
// fallback below, exactly as before this kind existed.
|
|
1223
1246
|
if (ts.isObjectLiteralExpression(node)) {
|
|
1224
1247
|
const properties: ObjectLiteralProperty[] = []
|
|
1225
1248
|
for (const prop of node.properties) {
|
|
1226
1249
|
if (ts.isPropertyAssignment(prop)) {
|
|
1227
1250
|
const k = objectLiteralKeyName(prop.name)
|
|
1228
1251
|
if (k === null) return { kind: 'unsupported', raw, reason: `Unsupported syntax: ${ts.SyntaxKind[node.kind]}` }
|
|
1229
|
-
properties.push({ key: k.key, keyKind: k.keyKind, shorthand: false, value: convertNode(prop.initializer, raw) })
|
|
1252
|
+
properties.push({ kind: 'prop', key: k.key, keyKind: k.keyKind, shorthand: false, value: convertNode(prop.initializer, raw) })
|
|
1230
1253
|
} else if (ts.isShorthandPropertyAssignment(prop)) {
|
|
1231
1254
|
const key = prop.name.text
|
|
1232
|
-
properties.push({ key, keyKind: 'identifier', shorthand: true, value: { kind: 'identifier', name: key } })
|
|
1255
|
+
properties.push({ kind: 'prop', key, keyKind: 'identifier', shorthand: true, value: { kind: 'identifier', name: key } })
|
|
1256
|
+
} else if (ts.isSpreadAssignment(prop)) {
|
|
1257
|
+
properties.push({ kind: 'spread', expr: convertNode(prop.expression, raw) })
|
|
1233
1258
|
} else {
|
|
1234
|
-
//
|
|
1259
|
+
// Computed key, method, getter/setter — not a plain map.
|
|
1235
1260
|
return { kind: 'unsupported', raw, reason: `Unsupported syntax: ${ts.SyntaxKind[node.kind]}` }
|
|
1236
1261
|
}
|
|
1237
1262
|
}
|
|
@@ -2362,25 +2387,69 @@ function getUnaryOperatorString(op: ts.PrefixUnaryOperator): string {
|
|
|
2362
2387
|
// =============================================================================
|
|
2363
2388
|
|
|
2364
2389
|
/**
|
|
2365
|
-
* Check if a parsed expression is supported for SSR template conversion
|
|
2390
|
+
* Check if a parsed expression is supported for SSR template conversion at
|
|
2391
|
+
* a RENDERED position — a standalone template expression (`{expr}`, an
|
|
2392
|
+
* attribute value, a condition test, …) whose value is what actually
|
|
2393
|
+
* reaches the page. See {@link isSupportedValue} for the sibling VALUE-
|
|
2394
|
+
* position entry point, and `checkSupport`'s `pos` parameter for why the two
|
|
2395
|
+
* never converge partway through a tree.
|
|
2366
2396
|
*/
|
|
2367
2397
|
export function isSupported(expr: ParsedExpr): SupportResult {
|
|
2368
|
-
return checkSupport(expr)
|
|
2398
|
+
return checkSupport(expr, 'rendered')
|
|
2369
2399
|
}
|
|
2370
2400
|
|
|
2371
|
-
|
|
2401
|
+
/**
|
|
2402
|
+
* Check if a parsed expression is supported at a VALUE position — a
|
|
2403
|
+
* signal/memo initializer (`computeSsrSeedPlan`'s classify step: an
|
|
2404
|
+
* assignment, never a render). The one behavioural difference from
|
|
2405
|
+
* {@link isSupported}: an `object-literal` anywhere in the tree is admitted
|
|
2406
|
+
* when every property value is itself supported, instead of being refused
|
|
2407
|
+
* outright.
|
|
2408
|
+
*/
|
|
2409
|
+
export function isSupportedValue(expr: ParsedExpr): SupportResult {
|
|
2410
|
+
return checkSupport(expr, 'value')
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
/**
|
|
2414
|
+
* Where in the expression tree a node sits, for the ONE shape whose support
|
|
2415
|
+
* depends on it: `object-literal` (refused at `rendered`, admitted at `value`
|
|
2416
|
+
* when every property value is itself supported — see the `object-literal`
|
|
2417
|
+
* case). `pos` is set ONLY by the two entry points ({@link isSupported} /
|
|
2418
|
+
* {@link isSupportedValue}) and propagates UNCHANGED through every recursive
|
|
2419
|
+
* call, with no per-shape forcing — forcing a container's contents to
|
|
2420
|
+
* `value` would let an object literal survive a `.map()`/`array-method`/
|
|
2421
|
+
* array-literal reachable from a RENDERED entry point and reach a string/
|
|
2422
|
+
* equality position (`{[{a:1}]}`, `[{a:1}].join(',')`, `.includes({a:1})`)
|
|
2423
|
+
* that no adapter renders identically to JS — a new silent-divergence class,
|
|
2424
|
+
* not a capability. Pure inheritance keeps `isSupported`'s behaviour
|
|
2425
|
+
* byte-identical to before `pos` existed; only a `value`-ENTERED tree (a
|
|
2426
|
+
* signal/memo initializer) ever admits a populated object literal.
|
|
2427
|
+
*/
|
|
2428
|
+
type ExprPos = 'rendered' | 'value'
|
|
2429
|
+
|
|
2430
|
+
function checkSupport(expr: ParsedExpr, pos: ExprPos): SupportResult {
|
|
2372
2431
|
switch (expr.kind) {
|
|
2373
2432
|
case 'unsupported':
|
|
2374
2433
|
return { supported: false, reason: expr.reason }
|
|
2375
2434
|
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2435
|
+
case 'object-literal': {
|
|
2436
|
+
// See `ExprPos`'s doc: refused at `rendered` (byte-identical reason to
|
|
2437
|
+
// the pre-`pos` behaviour, Roadmap A-1); admitted at `value` when
|
|
2438
|
+
// every property value is itself supported. Property values inherit
|
|
2439
|
+
// `pos`, which is already `value` here — written as `pos`, not the
|
|
2440
|
+
// literal `'value'`, for consistency with every other recursive call.
|
|
2441
|
+
if (pos !== 'value') {
|
|
2442
|
+
return { supported: false, reason: 'Unsupported syntax: ObjectLiteralExpression' }
|
|
2443
|
+
}
|
|
2444
|
+
for (const prop of expr.properties) {
|
|
2445
|
+
// A spread entry's SOURCE must itself be supported at `value`
|
|
2446
|
+
// position — the same criterion as an explicit property's value,
|
|
2447
|
+
// since the spread source is read (not rendered) at merge time.
|
|
2448
|
+
const propSupport = checkSupport(prop.kind === 'spread' ? prop.expr : prop.value, pos)
|
|
2449
|
+
if (!propSupport.supported) return propSupport
|
|
2450
|
+
}
|
|
2451
|
+
return { supported: true, level: 'L2' }
|
|
2452
|
+
}
|
|
2384
2453
|
|
|
2385
2454
|
case 'identifier':
|
|
2386
2455
|
return { supported: true, level: 'L1' }
|
|
@@ -2396,13 +2465,14 @@ function checkSupport(expr: ParsedExpr): SupportResult {
|
|
|
2396
2465
|
return { supported: false, reason: 'Standalone arrow functions / regex literals are not supported' }
|
|
2397
2466
|
|
|
2398
2467
|
case 'array-literal': {
|
|
2399
|
-
// Array literal is lowerable iff every element is
|
|
2400
|
-
//
|
|
2401
|
-
//
|
|
2402
|
-
//
|
|
2403
|
-
// see which adapter is consuming
|
|
2468
|
+
// Array literal is lowerable iff every element is; elements inherit
|
|
2469
|
+
// `pos` (see `ExprPos`'s doc). Adapters that don't have an
|
|
2470
|
+
// array-literal form in their template language (Go templates) still
|
|
2471
|
+
// need to refuse it — they do so in their own `arrayLiteral` emitter
|
|
2472
|
+
// method, not here, because we can't see which adapter is consuming
|
|
2473
|
+
// the IR at this point.
|
|
2404
2474
|
for (const el of expr.elements) {
|
|
2405
|
-
const elSupport = checkSupport(el)
|
|
2475
|
+
const elSupport = checkSupport(el, pos)
|
|
2406
2476
|
if (!elSupport.supported) return elSupport
|
|
2407
2477
|
}
|
|
2408
2478
|
return { supported: true, level: 'L2' }
|
|
@@ -2421,20 +2491,22 @@ function checkSupport(expr: ParsedExpr): SupportResult {
|
|
|
2421
2491
|
`String.prototype.${expr.method} supports only a string pattern + string replacement (the regex form is deferred); use a string pattern or wrap the expression in /* @client */`,
|
|
2422
2492
|
}
|
|
2423
2493
|
}
|
|
2424
|
-
|
|
2494
|
+
// Receiver/args inherit `pos` (see `ExprPos`'s doc).
|
|
2495
|
+
const objSupport = checkSupport(expr.object, pos)
|
|
2425
2496
|
if (!objSupport.supported) return objSupport
|
|
2426
2497
|
for (const arg of expr.args) {
|
|
2427
|
-
const argSupport = checkSupport(arg)
|
|
2498
|
+
const argSupport = checkSupport(arg, pos)
|
|
2428
2499
|
if (!argSupport.supported) return argSupport
|
|
2429
2500
|
}
|
|
2430
2501
|
// A dynamic `.flat(depth)` carries its depth expression outside
|
|
2431
|
-
// `args` (see the `depthExpr` doc) — check it too
|
|
2432
|
-
// already gated this at parse time (only a
|
|
2433
|
-
// expression becomes `depthExpr`), so this re-check
|
|
2434
|
-
// it keeps `isSupported` total for
|
|
2435
|
-
// regardless of how it was constructed
|
|
2502
|
+
// `args` (see the `depthExpr` doc) — check it too, same inherited
|
|
2503
|
+
// `pos`. The parser already gated this at parse time (only a
|
|
2504
|
+
// SUPPORTED depth expression becomes `depthExpr`), so this re-check
|
|
2505
|
+
// is defensive: it keeps `isSupported`/`isSupportedValue` total for
|
|
2506
|
+
// any `array-method`/`flat` node regardless of how it was constructed
|
|
2507
|
+
// (e.g. by a rewrite walker).
|
|
2436
2508
|
if (expr.method === 'flat' && expr.depthExpr) {
|
|
2437
|
-
const depthSupport = checkSupport(expr.depthExpr)
|
|
2509
|
+
const depthSupport = checkSupport(expr.depthExpr, pos)
|
|
2438
2510
|
if (!depthSupport.supported) return depthSupport
|
|
2439
2511
|
}
|
|
2440
2512
|
return { supported: true, level: 'L2' }
|
|
@@ -2453,11 +2525,16 @@ function checkSupport(expr: ParsedExpr): SupportResult {
|
|
|
2453
2525
|
// `grep`, Go's `len (bf_filter_eval …)`) or surfaces BF101 at its
|
|
2454
2526
|
// predicate fallback's exact degrade points (#2038) — a blanket refusal
|
|
2455
2527
|
// here would break the faithful shapes (#1443 PR4).
|
|
2528
|
+
//
|
|
2529
|
+
// Receiver, callback body, and trailing args inherit `pos` (see
|
|
2530
|
+
// `ExprPos`'s doc) — a RENDERED `.map(t => ({...}))` stays refused
|
|
2531
|
+
// (the body value is what ends up rendered), while a VALUE-entered
|
|
2532
|
+
// one (a signal/memo initializer) admits it.
|
|
2456
2533
|
const cb = asCallbackMethodCall(expr)
|
|
2457
2534
|
if (cb) {
|
|
2458
|
-
const objSupport = checkSupport(cb.object)
|
|
2535
|
+
const objSupport = checkSupport(cb.object, pos)
|
|
2459
2536
|
if (!objSupport.supported) return objSupport
|
|
2460
|
-
const bodySupport = checkSupport(cb.arrow.body)
|
|
2537
|
+
const bodySupport = checkSupport(cb.arrow.body, pos)
|
|
2461
2538
|
if (!bodySupport.supported) {
|
|
2462
2539
|
return {
|
|
2463
2540
|
supported: false,
|
|
@@ -2466,14 +2543,15 @@ function checkSupport(expr: ParsedExpr): SupportResult {
|
|
|
2466
2543
|
}
|
|
2467
2544
|
}
|
|
2468
2545
|
for (const rest of cb.args) {
|
|
2469
|
-
const restSupport = checkSupport(rest)
|
|
2546
|
+
const restSupport = checkSupport(rest, pos)
|
|
2470
2547
|
if (!restSupport.supported) return restSupport
|
|
2471
2548
|
}
|
|
2472
2549
|
return { supported: true, level: 'L5' }
|
|
2473
2550
|
}
|
|
2474
2551
|
|
|
2475
|
-
// Check if callee is supported
|
|
2476
|
-
|
|
2552
|
+
// Check if callee is supported. Not container contents — inherits
|
|
2553
|
+
// the CURRENT `pos` (see `ExprPos`'s doc).
|
|
2554
|
+
const calleeSupport = checkSupport(expr.callee, pos)
|
|
2477
2555
|
if (!calleeSupport.supported) {
|
|
2478
2556
|
return calleeSupport
|
|
2479
2557
|
}
|
|
@@ -2502,9 +2580,11 @@ function checkSupport(expr: ParsedExpr): SupportResult {
|
|
|
2502
2580
|
return { supported: true, level: 'L1' }
|
|
2503
2581
|
}
|
|
2504
2582
|
|
|
2505
|
-
// Other function calls - check args
|
|
2583
|
+
// Other function calls - check args. Not container contents of a
|
|
2584
|
+
// recognised callback/array-method (that path returned above) —
|
|
2585
|
+
// inherits the CURRENT `pos` (see `ExprPos`'s doc).
|
|
2506
2586
|
for (const arg of expr.args) {
|
|
2507
|
-
const argSupport = checkSupport(arg)
|
|
2587
|
+
const argSupport = checkSupport(arg, pos)
|
|
2508
2588
|
if (!argSupport.supported) {
|
|
2509
2589
|
return argSupport
|
|
2510
2590
|
}
|
|
@@ -2513,7 +2593,8 @@ function checkSupport(expr: ParsedExpr): SupportResult {
|
|
|
2513
2593
|
}
|
|
2514
2594
|
|
|
2515
2595
|
case 'member': {
|
|
2516
|
-
|
|
2596
|
+
// Not container contents — inherits the CURRENT `pos`.
|
|
2597
|
+
const objSupport = checkSupport(expr.object, pos)
|
|
2517
2598
|
if (!objSupport.supported) {
|
|
2518
2599
|
return objSupport
|
|
2519
2600
|
}
|
|
@@ -2527,18 +2608,21 @@ function checkSupport(expr: ParsedExpr): SupportResult {
|
|
|
2527
2608
|
case 'index-access': {
|
|
2528
2609
|
// `arr[index]` — supported when both the receiver and the index
|
|
2529
2610
|
// expression are themselves supported (the index is typically a
|
|
2530
|
-
// loop variable or arithmetic over one). #1897 (data-table).
|
|
2531
|
-
|
|
2611
|
+
// loop variable or arithmetic over one). #1897 (data-table). Neither
|
|
2612
|
+
// is container contents — both inherit the CURRENT `pos`.
|
|
2613
|
+
const objSupport = checkSupport(expr.object, pos)
|
|
2532
2614
|
if (!objSupport.supported) return objSupport
|
|
2533
|
-
const indexSupport = checkSupport(expr.index)
|
|
2615
|
+
const indexSupport = checkSupport(expr.index, pos)
|
|
2534
2616
|
if (!indexSupport.supported) return indexSupport
|
|
2535
2617
|
return { supported: true, level: 'L2' }
|
|
2536
2618
|
}
|
|
2537
2619
|
|
|
2538
2620
|
case 'binary': {
|
|
2539
|
-
|
|
2621
|
+
// Operands inherit the CURRENT `pos` (see `ExprPos`'s doc) — a binary
|
|
2622
|
+
// operand isn't container contents.
|
|
2623
|
+
const leftSupport = checkSupport(expr.left, pos)
|
|
2540
2624
|
if (!leftSupport.supported) return leftSupport
|
|
2541
|
-
const rightSupport = checkSupport(expr.right)
|
|
2625
|
+
const rightSupport = checkSupport(expr.right, pos)
|
|
2542
2626
|
if (!rightSupport.supported) return rightSupport
|
|
2543
2627
|
|
|
2544
2628
|
// Comparison operators are L3
|
|
@@ -2555,7 +2639,8 @@ function checkSupport(expr: ParsedExpr): SupportResult {
|
|
|
2555
2639
|
}
|
|
2556
2640
|
|
|
2557
2641
|
case 'unary': {
|
|
2558
|
-
|
|
2642
|
+
// Inherits the CURRENT `pos` — not container contents.
|
|
2643
|
+
const argSupport = checkSupport(expr.argument, pos)
|
|
2559
2644
|
if (!argSupport.supported) return argSupport
|
|
2560
2645
|
|
|
2561
2646
|
// Negation is L4
|
|
@@ -2571,7 +2656,8 @@ function checkSupport(expr: ParsedExpr): SupportResult {
|
|
|
2571
2656
|
}
|
|
2572
2657
|
|
|
2573
2658
|
case 'logical': {
|
|
2574
|
-
|
|
2659
|
+
// Operands inherit the CURRENT `pos` — not container contents.
|
|
2660
|
+
const leftSupport = checkSupport(expr.left, pos)
|
|
2575
2661
|
if (!leftSupport.supported) return leftSupport
|
|
2576
2662
|
|
|
2577
2663
|
// `x ?? {}` — admit an EMPTY object-literal fallback as the right
|
|
@@ -2598,27 +2684,35 @@ function checkSupport(expr: ParsedExpr): SupportResult {
|
|
|
2598
2684
|
return { supported: true, level: 'L4' }
|
|
2599
2685
|
}
|
|
2600
2686
|
|
|
2601
|
-
const rightSupport = checkSupport(expr.right)
|
|
2687
|
+
const rightSupport = checkSupport(expr.right, pos)
|
|
2602
2688
|
if (!rightSupport.supported) return rightSupport
|
|
2603
2689
|
|
|
2604
2690
|
return { supported: true, level: 'L4' }
|
|
2605
2691
|
}
|
|
2606
2692
|
|
|
2607
2693
|
case 'conditional': {
|
|
2608
|
-
|
|
2694
|
+
// Branches inherit the CURRENT `pos` (see `ExprPos`'s doc): whichever
|
|
2695
|
+
// branch is taken becomes the ternary's own value, so at a RENDERED
|
|
2696
|
+
// position both branches are themselves rendered — `cond ? {a:1} :
|
|
2697
|
+
// {b:2}` stays refused there, not admitted just for being a
|
|
2698
|
+
// sub-expression.
|
|
2699
|
+
const testSupport = checkSupport(expr.test, pos)
|
|
2609
2700
|
if (!testSupport.supported) return testSupport
|
|
2610
|
-
const consSupport = checkSupport(expr.consequent)
|
|
2701
|
+
const consSupport = checkSupport(expr.consequent, pos)
|
|
2611
2702
|
if (!consSupport.supported) return consSupport
|
|
2612
|
-
const altSupport = checkSupport(expr.alternate)
|
|
2703
|
+
const altSupport = checkSupport(expr.alternate, pos)
|
|
2613
2704
|
if (!altSupport.supported) return altSupport
|
|
2614
2705
|
|
|
2615
2706
|
return { supported: true, level: 'L4' }
|
|
2616
2707
|
}
|
|
2617
2708
|
|
|
2618
2709
|
case 'template-literal': {
|
|
2710
|
+
// Interpolated parts inherit the CURRENT `pos` — not container
|
|
2711
|
+
// contents (a template literal always stringifies its holes, so an
|
|
2712
|
+
// object-literal hole has no sensible rendering either way).
|
|
2619
2713
|
for (const part of expr.parts) {
|
|
2620
2714
|
if (part.type === 'expression') {
|
|
2621
|
-
const partSupport = checkSupport(part.expr)
|
|
2715
|
+
const partSupport = checkSupport(part.expr, pos)
|
|
2622
2716
|
if (!partSupport.supported) return partSupport
|
|
2623
2717
|
}
|
|
2624
2718
|
}
|
|
@@ -2935,7 +3029,7 @@ function isPureInit(e: ParsedExpr, pureCallNames?: ReadonlySet<string>): boolean
|
|
|
2935
3029
|
case 'array-literal':
|
|
2936
3030
|
return e.elements.every(pure)
|
|
2937
3031
|
case 'object-literal':
|
|
2938
|
-
return e.properties.every(p => pure(p.value))
|
|
3032
|
+
return e.properties.every(p => pure(p.kind === 'spread' ? p.expr : p.value))
|
|
2939
3033
|
case 'call':
|
|
2940
3034
|
// A zero-arg reactive getter read (`filter()`, `count()`) is idempotent;
|
|
2941
3035
|
// any other call may be effectful or non-deterministic.
|
|
@@ -3019,7 +3113,7 @@ function usesPerPath(name: string, expr: ParsedExpr): { min: number; max: number
|
|
|
3019
3113
|
}
|
|
3020
3114
|
return add(walk(e.object), sum(e.args))
|
|
3021
3115
|
case 'object-literal':
|
|
3022
|
-
return sum(e.properties.map(p => p.value))
|
|
3116
|
+
return sum(e.properties.map(p => (p.kind === 'spread' ? p.expr : p.value)))
|
|
3023
3117
|
case 'arrow':
|
|
3024
3118
|
// A callback body may run any number of times (per element, or never).
|
|
3025
3119
|
return walk(e.body).max > 0 ? { min: 0, max: Number.POSITIVE_INFINITY } : { min: 0, max: 0 }
|
|
@@ -3115,7 +3209,9 @@ export function inlineBinding(
|
|
|
3115
3209
|
case 'object-literal':
|
|
3116
3210
|
return {
|
|
3117
3211
|
kind: 'object-literal',
|
|
3118
|
-
properties: e.properties.map(p =>
|
|
3212
|
+
properties: e.properties.map(p =>
|
|
3213
|
+
p.kind === 'spread' ? { ...p, expr: walk(p.expr, enclosing) } : { ...p, value: walk(p.value, enclosing) },
|
|
3214
|
+
),
|
|
3119
3215
|
raw: e.raw,
|
|
3120
3216
|
}
|
|
3121
3217
|
case 'literal':
|
|
@@ -3487,7 +3583,7 @@ export function materializeGetterCalls(expr: ParsedExpr, names: ReadonlySet<stri
|
|
|
3487
3583
|
return {
|
|
3488
3584
|
kind: 'object-literal',
|
|
3489
3585
|
raw: expr.raw,
|
|
3490
|
-
properties: expr.properties.map(p => ({ ...p, value: rw(p.value) })),
|
|
3586
|
+
properties: expr.properties.map(p => (p.kind === 'spread' ? { ...p, expr: rw(p.expr) } : { ...p, value: rw(p.value) })),
|
|
3491
3587
|
}
|
|
3492
3588
|
case 'arrow':
|
|
3493
3589
|
return { kind: 'arrow', params: expr.params, body: rw(expr.body) }
|
|
@@ -3586,7 +3682,8 @@ export function freeVarsInBody(body: ParsedExpr, params: ReadonlySet<string>): s
|
|
|
3586
3682
|
case 'object-literal':
|
|
3587
3683
|
// Object *values* are references; keys are not. (Shorthand `{ x }`
|
|
3588
3684
|
// carries the ref on its `value` identifier, which is visited here.)
|
|
3589
|
-
|
|
3685
|
+
// A spread's source expression is likewise a reference.
|
|
3686
|
+
for (const p of e.properties) visit(p.kind === 'spread' ? p.expr : p.value, bound)
|
|
3590
3687
|
return
|
|
3591
3688
|
case 'array-method':
|
|
3592
3689
|
// `.includes(x)` / `.join(sep?)` are serializable ({@link
|
|
@@ -3693,7 +3790,7 @@ export function freeIdentifiers(expr: ParsedExpr): Set<string> | null {
|
|
|
3693
3790
|
if (e.method === 'flat' && e.depthExpr && !visit(e.depthExpr, bound)) return false
|
|
3694
3791
|
return true
|
|
3695
3792
|
case 'object-literal':
|
|
3696
|
-
for (const p of e.properties) if (!visit(p.value, bound)) return false
|
|
3793
|
+
for (const p of e.properties) if (!visit(p.kind === 'spread' ? p.expr : p.value, bound)) return false
|
|
3697
3794
|
return true
|
|
3698
3795
|
case 'arrow': {
|
|
3699
3796
|
const inner = new Set(bound)
|
|
@@ -3869,11 +3966,25 @@ function toEvalNode(e: ParsedExpr): Record<string, unknown> | null {
|
|
|
3869
3966
|
return { kind: 'array-literal', elements }
|
|
3870
3967
|
}
|
|
3871
3968
|
case 'object-literal': {
|
|
3969
|
+
// Each entry carries its own `kind` tag (`prop` | `spread`) — the SAME
|
|
3970
|
+
// discriminant `ObjectLiteralProperty` uses — so every backend
|
|
3971
|
+
// evaluator (Go `eval.go`, Perl `Evaluator.pm`, Python, Ruby, PHP,
|
|
3972
|
+
// Rust) decodes this exactly like the raw `ParsedExpr` shape the
|
|
3973
|
+
// `eval-vectors.json` golden corpus carries (#2696 Step 2): a `prop`
|
|
3974
|
+
// sets one key; a `spread` evaluates its `expr` and shallow-merges the
|
|
3975
|
+
// result's own keys (later entries win; a null/undefined/non-object
|
|
3976
|
+
// spread source is a no-op).
|
|
3872
3977
|
const properties: Record<string, unknown>[] = []
|
|
3873
3978
|
for (const p of e.properties) {
|
|
3979
|
+
if (p.kind === 'spread') {
|
|
3980
|
+
const spreadExpr = toEvalNode(p.expr)
|
|
3981
|
+
if (!spreadExpr) return null
|
|
3982
|
+
properties.push({ kind: 'spread', expr: spreadExpr })
|
|
3983
|
+
continue
|
|
3984
|
+
}
|
|
3874
3985
|
const value = toEvalNode(p.value)
|
|
3875
3986
|
if (!value) return null
|
|
3876
|
-
properties.push({ key: p.key, value })
|
|
3987
|
+
properties.push({ kind: 'prop', key: p.key, value })
|
|
3877
3988
|
}
|
|
3878
3989
|
return { kind: 'object-literal', properties }
|
|
3879
3990
|
}
|
package/src/index.ts
CHANGED
|
@@ -93,7 +93,7 @@ export type {
|
|
|
93
93
|
export { JsxAdapter } from './adapters/jsx-adapter.ts'
|
|
94
94
|
export type { JsxAdapterConfig } from './adapters/jsx-adapter.ts'
|
|
95
95
|
export { rewriteImportsForTemplate, rewriteDynamicImportsInSource } from './adapters/template-imports.ts'
|
|
96
|
-
export { emitParsedExpr, groupBinaryOperand, isStringTypedOperand, isStringConcatBinary } from './adapters/parsed-expr-emitter.ts'
|
|
96
|
+
export { emitParsedExpr, groupBinaryOperand, groupObjectLiteralSegments, isStringTypedOperand, isStringConcatBinary } from './adapters/parsed-expr-emitter.ts'
|
|
97
97
|
export type { ParsedExprEmitter, HigherOrderMethod, ArrayMethod, SortMethod, LiteralType } from './adapters/parsed-expr-emitter.ts'
|
|
98
98
|
export { collectLoopBoundNames } from './adapters/loop-bound-names.ts'
|
|
99
99
|
export { derivesScopeFromSlot } from './adapters/child-scope.ts'
|
|
@@ -211,7 +211,7 @@ export { ErrorCodes, createError, formatError, generateCodeFrame } from './error
|
|
|
211
211
|
export { isValueReferenceIdentifier, collectValueReferencedNames } from './value-references.ts'
|
|
212
212
|
|
|
213
213
|
// Expression Parser
|
|
214
|
-
export { parseExpression, tsNodeToParsedExpr, asCallbackMethodCall, CALLBACK_METHODS, sortComparatorFromArrow, serializeParsedExpr, freeVarsInBody, freeIdentifiers, materializeGetterCalls, isSupported, exprToString, stringifyParsedExpr, identifierPath, parseBlockBody, parseBlockBodyTolerant, foldBlockToExpr, predicateTernaryToLogical, containsHigherOrder, extractArrowBodyExpression, parseStyleObjectEntries, hasUnsafeStyleValue, parseProviderObjectLiteral, type ProviderObjectMember, type FoldBlockOptions } from './expression-parser.ts'
|
|
214
|
+
export { parseExpression, tsNodeToParsedExpr, asCallbackMethodCall, CALLBACK_METHODS, sortComparatorFromArrow, serializeParsedExpr, freeVarsInBody, freeIdentifiers, materializeGetterCalls, isSupported, isSupportedValue, exprToString, stringifyParsedExpr, identifierPath, parseBlockBody, parseBlockBodyTolerant, foldBlockToExpr, predicateTernaryToLogical, containsHigherOrder, extractArrowBodyExpression, parseStyleObjectEntries, hasUnsafeStyleValue, parseProviderObjectLiteral, type ProviderObjectMember, type FoldBlockOptions } from './expression-parser.ts'
|
|
215
215
|
export type { StyleObjectEntry } from './expression-parser.ts'
|
|
216
216
|
export { PARSED_EXPR_KINDS, ARRAY_METHOD_NAMES } from './expression-parser.ts'
|
|
217
217
|
export type { ParsedExpr, ObjectLiteralProperty, ParsedStatement, SortComparator, SortKey, FlatDepth, SupportLevel, SupportResult, TemplatePart } from './expression-parser.ts'
|
package/src/jsx-to-ir.ts
CHANGED
|
@@ -3296,7 +3296,10 @@ function resolveCallbackMethodFunctionReferences(
|
|
|
3296
3296
|
...(e.method === 'flat' && e.depthExpr ? { depthExpr: visit(e.depthExpr, bound) } : {}),
|
|
3297
3297
|
} as ParsedExpr
|
|
3298
3298
|
case 'object-literal':
|
|
3299
|
-
return {
|
|
3299
|
+
return {
|
|
3300
|
+
...e,
|
|
3301
|
+
properties: e.properties.map(p => (p.kind === 'spread' ? { ...p, expr: visit(p.expr, bound) } : { ...p, value: visit(p.value, bound) })),
|
|
3302
|
+
}
|
|
3300
3303
|
case 'arrow': {
|
|
3301
3304
|
const inner = e.params.length === 0 ? bound : new Set([...bound, ...e.params])
|
|
3302
3305
|
return { ...e, body: visit(e.body, inner) }
|
|
@@ -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.
|