@barefootjs/jsx 0.17.1 → 0.18.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/interface.d.ts +20 -4
- package/dist/adapters/interface.d.ts.map +1 -1
- package/dist/adapters/parsed-expr-emitter.d.ts +3 -1
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/analyzer-context.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/augment-inherited-props.d.ts +19 -0
- package/dist/augment-inherited-props.d.ts.map +1 -1
- package/dist/compiler.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +1 -0
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +19 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +815 -251
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/compute-inlinability.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +9 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts +19 -0
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-registration.d.ts +4 -2
- package/dist/ir-to-client-js/emit-registration.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +41 -0
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +22 -1
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/loop-destructure.d.ts +55 -18
- package/dist/loop-destructure.d.ts.map +1 -1
- package/dist/lowering-registry.d.ts +13 -0
- package/dist/lowering-registry.d.ts.map +1 -1
- package/dist/relocate.d.ts +28 -0
- package/dist/relocate.d.ts.map +1 -1
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/types.d.ts +68 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +284 -12
- package/src/__tests__/augment-inherited-props.test.ts +96 -0
- package/src/__tests__/compiler-runtime-contract.test.ts +11 -1
- package/src/__tests__/compiler-stress-1244.test.ts +13 -4
- package/src/__tests__/csr-substitution-safety-divergence.test.ts +137 -0
- package/src/__tests__/destructured-map-params.test.ts +11 -1
- package/src/__tests__/expression-parser.test.ts +63 -2
- package/src/__tests__/ir-sort-comparator.test.ts +261 -0
- package/src/__tests__/loop-destructure.test.ts +313 -0
- package/src/__tests__/loop-hoisted-template.test.ts +235 -0
- package/src/__tests__/props-destructuring.test.ts +110 -0
- package/src/__tests__/serialize-parsed-expr.test.ts +70 -3
- package/src/__tests__/ssr-defaults.test.ts +20 -0
- package/src/__tests__/staged-ir/11-template-primitive-registry.test.ts +231 -1
- package/src/__tests__/tagged-template-interleave.test.ts +268 -0
- package/src/__tests__/unsupported-expression.test.ts +194 -7
- package/src/adapters/interface.ts +20 -4
- package/src/adapters/parsed-expr-emitter.ts +19 -2
- package/src/analyzer-context.ts +20 -0
- package/src/analyzer.ts +74 -1
- package/src/augment-inherited-props.ts +139 -9
- package/src/compiler.ts +4 -0
- package/src/expression-parser.ts +237 -56
- package/src/index.ts +23 -1
- package/src/ir-to-client-js/collect-elements.ts +15 -1
- package/src/ir-to-client-js/compute-inlinability.ts +6 -1
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +1 -0
- package/src/ir-to-client-js/control-flow/plan/loop.ts +9 -0
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +30 -8
- package/src/ir-to-client-js/control-flow/stringify/template-parse.ts +30 -0
- package/src/ir-to-client-js/emit-registration.ts +4 -2
- package/src/ir-to-client-js/html-template.ts +198 -1
- package/src/ir-to-client-js/index.ts +1 -0
- package/src/ir-to-client-js/types.ts +22 -0
- package/src/jsx-to-ir.ts +409 -24
- package/src/loop-destructure.ts +89 -36
- package/src/lowering-registry.ts +16 -0
- package/src/relocate.ts +201 -14
- package/src/ssr-defaults.ts +34 -32
- package/src/types.ts +65 -0
package/src/expression-parser.ts
CHANGED
|
@@ -99,18 +99,40 @@ export type ParsedExpr =
|
|
|
99
99
|
object: ParsedExpr
|
|
100
100
|
args: ParsedExpr[]
|
|
101
101
|
}
|
|
102
|
-
// `.flat(depth?)` (#1448 Tier C). The flatten depth
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
102
|
+
// `.flat(depth?)` (#1448 Tier C, dynamic depth #2094). The flatten depth
|
|
103
|
+
// is validated at parse time — a literal integer / `Infinity` /
|
|
104
|
+
// unary-minus-literal normalises into a structured `FlatDepth` and the
|
|
105
|
+
// literal never reaches `args`, so adapters fold via a runtime helper
|
|
106
|
+
// instead of re-inspecting the depth argument. A non-literal depth that
|
|
107
|
+
// itself parses to a SUPPORTED `ParsedExpr` (a numeric prop, signal read,
|
|
108
|
+
// arithmetic, …) becomes a DYNAMIC depth: `depthExpr` carries that
|
|
109
|
+
// expression and `flatDepth` is a meaningless placeholder (`1`) — every
|
|
110
|
+
// consumer must check `depthExpr` first. A depth expression that itself
|
|
111
|
+
// doesn't resolve (an `unsupported` shape) still refuses with BF101 (the
|
|
112
|
+
// depth must be *expressible*, even if not known until render time). See
|
|
113
|
+
// the `.flat` arm in `convertNode`.
|
|
114
|
+
//
|
|
115
|
+
// Runtime coercion contract for a dynamic depth (JS `ToIntegerOrInfinity`,
|
|
116
|
+
// pinned by the `flat_dynamic` golden-vector cases): truncate toward
|
|
117
|
+
// zero; a NaN-producing value coerces to `0`; negative coerces to `0`
|
|
118
|
+
// (JS: negative depth never recurses, same as `0` — a shallow copy);
|
|
119
|
+
// `Infinity` / a huge finite value flattens fully. This is intentionally
|
|
120
|
+
// NOT the same runtime helper as the literal-depth path: the literal path
|
|
121
|
+
// pre-normalises `Infinity` to a `-1` *sentinel* meaning "flatten fully"
|
|
122
|
+
// (see `FlatDepth`'s doc), and a genuinely dynamic value of `-1` means
|
|
123
|
+
// the OPPOSITE (JS: no flatten) — reusing one helper for both would
|
|
124
|
+
// silently invert that case. Each adapter therefore routes a dynamic
|
|
125
|
+
// depth through a SEPARATE runtime helper (Go: `bf_flat_dynamic` /
|
|
126
|
+
// `FlatDynamicDepth`, distinct from the literal path's `bf_flat` / `Flat`)
|
|
127
|
+
// that performs the coercion above from scratch, never reusing the
|
|
128
|
+
// literal path's `-1`-means-infinite convention.
|
|
108
129
|
| {
|
|
109
130
|
kind: 'array-method'
|
|
110
131
|
method: 'flat'
|
|
111
132
|
object: ParsedExpr
|
|
112
133
|
args: []
|
|
113
134
|
flatDepth: FlatDepth
|
|
135
|
+
depthExpr?: ParsedExpr
|
|
114
136
|
}
|
|
115
137
|
| { kind: 'unsupported'; raw: string; reason: string }
|
|
116
138
|
|
|
@@ -795,6 +817,7 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
795
817
|
if (callee.property === 'flat') {
|
|
796
818
|
const depthNode = node.arguments[0]
|
|
797
819
|
let flatDepth: FlatDepth
|
|
820
|
+
let depthExpr: ParsedExpr | undefined
|
|
798
821
|
if (depthNode === undefined) {
|
|
799
822
|
flatDepth = 1
|
|
800
823
|
} else if (ts.isIdentifier(depthNode) && depthNode.text === 'Infinity') {
|
|
@@ -811,16 +834,34 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
811
834
|
n = -Number(depthNode.operand.text)
|
|
812
835
|
}
|
|
813
836
|
if (n === undefined || Number.isNaN(n)) {
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
837
|
+
// Not a literal — try a DYNAMIC depth: an expression that
|
|
838
|
+
// itself resolves to a supported `ParsedExpr` (a numeric prop,
|
|
839
|
+
// signal read, arithmetic, …) can be lowered through a runtime
|
|
840
|
+
// helper that coerces it at render time (JS
|
|
841
|
+
// `ToIntegerOrInfinity`; see the `depthExpr` doc on the
|
|
842
|
+
// `array-method`/`flat` type). Still refuse with BF101 when
|
|
843
|
+
// the depth expression itself is outside the supported
|
|
844
|
+
// surface — the depth must be *expressible*, even if not
|
|
845
|
+
// known until render time.
|
|
846
|
+
const parsedDepth = convertNode(depthNode, raw)
|
|
847
|
+
if (checkSupport(parsedDepth).supported) {
|
|
848
|
+
depthExpr = parsedDepth
|
|
849
|
+
flatDepth = 1 // unused placeholder — consumers must check `depthExpr` first
|
|
850
|
+
} else {
|
|
851
|
+
return {
|
|
852
|
+
kind: 'unsupported',
|
|
853
|
+
raw,
|
|
854
|
+
reason: `\`.flat(depth)\` needs a literal integer, \`Infinity\`, or a supported dynamic depth expression — this depth can't be resolved. Use a literal depth, a supported expression (prop/signal/arithmetic), or pre-compute the value before the template.`,
|
|
855
|
+
}
|
|
818
856
|
}
|
|
857
|
+
} else {
|
|
858
|
+
const truncated = Math.trunc(n)
|
|
859
|
+
flatDepth = truncated < 0 ? 0 : truncated
|
|
819
860
|
}
|
|
820
|
-
const truncated = Math.trunc(n)
|
|
821
|
-
flatDepth = truncated < 0 ? 0 : truncated
|
|
822
861
|
}
|
|
823
|
-
return
|
|
862
|
+
return depthExpr !== undefined
|
|
863
|
+
? { kind: 'array-method', method: 'flat', object: callee.object, args: [], flatDepth, depthExpr }
|
|
864
|
+
: { kind: 'array-method', method: 'flat', object: callee.object, args: [], flatDepth }
|
|
824
865
|
}
|
|
825
866
|
// `.toLowerCase()` — string-only (the IR carries a value-builtin
|
|
826
867
|
// tag, not a receiver-type discriminator, so the `array-method`
|
|
@@ -1869,6 +1910,7 @@ function validateRestUsage(
|
|
|
1869
1910
|
case 'array-method':
|
|
1870
1911
|
walk(e.object)
|
|
1871
1912
|
for (const a of e.args) walk(a)
|
|
1913
|
+
if (e.method === 'flat' && e.depthExpr) walk(e.depthExpr)
|
|
1872
1914
|
return
|
|
1873
1915
|
case 'literal':
|
|
1874
1916
|
case 'unsupported':
|
|
@@ -1981,6 +2023,7 @@ function collectIdentifiers(expr: ParsedExpr, out: Set<string>): void {
|
|
|
1981
2023
|
case 'array-method':
|
|
1982
2024
|
collectIdentifiers(expr.object, out)
|
|
1983
2025
|
expr.args.forEach(e => collectIdentifiers(e, out))
|
|
2026
|
+
if (expr.method === 'flat' && expr.depthExpr) collectIdentifiers(expr.depthExpr, out)
|
|
1984
2027
|
return
|
|
1985
2028
|
case 'literal':
|
|
1986
2029
|
case 'regex':
|
|
@@ -2078,8 +2121,16 @@ function substituteDestructuredFields(
|
|
|
2078
2121
|
case 'array-method':
|
|
2079
2122
|
if (e.method === 'flat') {
|
|
2080
2123
|
// `flatDepth` is a normalised literal — no destructure refs to
|
|
2081
|
-
// substitute.
|
|
2082
|
-
|
|
2124
|
+
// substitute. `depthExpr` (a dynamic depth) DOES need the same
|
|
2125
|
+
// substitution as any other value position.
|
|
2126
|
+
return {
|
|
2127
|
+
kind: 'array-method',
|
|
2128
|
+
method: 'flat',
|
|
2129
|
+
object: walk(e.object),
|
|
2130
|
+
args: [],
|
|
2131
|
+
flatDepth: e.flatDepth,
|
|
2132
|
+
...(e.depthExpr ? { depthExpr: walk(e.depthExpr) } : {}),
|
|
2133
|
+
}
|
|
2083
2134
|
}
|
|
2084
2135
|
return { kind: 'array-method', method: e.method, object: walk(e.object), args: e.args.map(walk) }
|
|
2085
2136
|
case 'literal':
|
|
@@ -2202,6 +2253,16 @@ function checkSupport(expr: ParsedExpr): SupportResult {
|
|
|
2202
2253
|
const argSupport = checkSupport(arg)
|
|
2203
2254
|
if (!argSupport.supported) return argSupport
|
|
2204
2255
|
}
|
|
2256
|
+
// A dynamic `.flat(depth)` carries its depth expression outside
|
|
2257
|
+
// `args` (see the `depthExpr` doc) — check it too. The parser
|
|
2258
|
+
// already gated this at parse time (only a SUPPORTED depth
|
|
2259
|
+
// expression becomes `depthExpr`), so this re-check is defensive:
|
|
2260
|
+
// it keeps `isSupported` total for any `array-method`/`flat` node
|
|
2261
|
+
// regardless of how it was constructed (e.g. by a rewrite walker).
|
|
2262
|
+
if (expr.method === 'flat' && expr.depthExpr) {
|
|
2263
|
+
const depthSupport = checkSupport(expr.depthExpr)
|
|
2264
|
+
if (!depthSupport.supported) return depthSupport
|
|
2265
|
+
}
|
|
2205
2266
|
return { supported: true, level: 'L2' }
|
|
2206
2267
|
}
|
|
2207
2268
|
|
|
@@ -2338,6 +2399,31 @@ function checkSupport(expr: ParsedExpr): SupportResult {
|
|
|
2338
2399
|
case 'logical': {
|
|
2339
2400
|
const leftSupport = checkSupport(expr.left)
|
|
2340
2401
|
if (!leftSupport.supported) return leftSupport
|
|
2402
|
+
|
|
2403
|
+
// `x ?? {}` — admit an EMPTY object-literal fallback as the right
|
|
2404
|
+
// operand of `??` only. A standalone object literal stays refused
|
|
2405
|
+
// (the `object-literal` case above), and a non-empty one here still
|
|
2406
|
+
// falls through to the general `checkSupport(expr.right)` refusal
|
|
2407
|
+
// below. This is deliberately narrow, scoped to the one shape that
|
|
2408
|
+
// needs it (chart's `<Ctx.Provider value={{ config: props.config ??
|
|
2409
|
+
// {} }}>`):
|
|
2410
|
+
// - `??` only, not `&&` / `||` — a `{}` operand there plays a
|
|
2411
|
+
// different (always-truthy) role that each adapter's
|
|
2412
|
+
// boolean/filter dispatcher already handles by treating an
|
|
2413
|
+
// object-literal leaf as an unconditional-truthy sentinel; that
|
|
2414
|
+
// path doesn't need a real emitted value the way `??`'s value
|
|
2415
|
+
// position does.
|
|
2416
|
+
// - EMPTY object literal only (`properties.length === 0`) — the
|
|
2417
|
+
// one caller of this shape never needs populated fallback keys,
|
|
2418
|
+
// and restricting to empty means every adapter's `objectLiteral`
|
|
2419
|
+
// value-emitter only has to special-case the zero-property case
|
|
2420
|
+
// (emit its language's empty dict/hashref literal) rather than
|
|
2421
|
+
// also lowering arbitrary member values through the general
|
|
2422
|
+
// expression pipeline for SSR/CSR parity.
|
|
2423
|
+
if (expr.op === '??' && expr.right.kind === 'object-literal' && expr.right.properties.length === 0) {
|
|
2424
|
+
return { supported: true, level: 'L4' }
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2341
2427
|
const rightSupport = checkSupport(expr.right)
|
|
2342
2428
|
if (!rightSupport.supported) return rightSupport
|
|
2343
2429
|
|
|
@@ -2397,7 +2483,11 @@ export function containsHigherOrder(expr: ParsedExpr): boolean {
|
|
|
2397
2483
|
case 'array-literal':
|
|
2398
2484
|
return expr.elements.some(containsHigherOrder)
|
|
2399
2485
|
case 'array-method':
|
|
2400
|
-
return
|
|
2486
|
+
return (
|
|
2487
|
+
containsHigherOrder(expr.object) ||
|
|
2488
|
+
expr.args.some(containsHigherOrder) ||
|
|
2489
|
+
(expr.method === 'flat' && expr.depthExpr !== undefined && containsHigherOrder(expr.depthExpr))
|
|
2490
|
+
)
|
|
2401
2491
|
default:
|
|
2402
2492
|
return false
|
|
2403
2493
|
}
|
|
@@ -2750,7 +2840,10 @@ function usesPerPath(name: string, expr: ParsedExpr): { min: number; max: number
|
|
|
2750
2840
|
case 'array-literal':
|
|
2751
2841
|
return sum(e.elements)
|
|
2752
2842
|
case 'array-method':
|
|
2753
|
-
|
|
2843
|
+
if (e.method === 'flat') {
|
|
2844
|
+
return add(walk(e.object), e.depthExpr ? walk(e.depthExpr) : { min: 0, max: 0 })
|
|
2845
|
+
}
|
|
2846
|
+
return add(walk(e.object), sum(e.args))
|
|
2754
2847
|
case 'object-literal':
|
|
2755
2848
|
return sum(e.properties.map(p => p.value))
|
|
2756
2849
|
case 'arrow':
|
|
@@ -2826,7 +2919,14 @@ function inlineBinding(
|
|
|
2826
2919
|
return { kind: 'array-literal', elements: e.elements.map(el => walk(el, enclosing)) }
|
|
2827
2920
|
case 'array-method':
|
|
2828
2921
|
if (e.method === 'flat') {
|
|
2829
|
-
return {
|
|
2922
|
+
return {
|
|
2923
|
+
kind: 'array-method',
|
|
2924
|
+
method: 'flat',
|
|
2925
|
+
object: walk(e.object, enclosing),
|
|
2926
|
+
args: [],
|
|
2927
|
+
flatDepth: e.flatDepth,
|
|
2928
|
+
...(e.depthExpr ? { depthExpr: walk(e.depthExpr, enclosing) } : {}),
|
|
2929
|
+
}
|
|
2830
2930
|
}
|
|
2831
2931
|
return { kind: 'array-method', method: e.method, object: walk(e.object, enclosing), args: e.args.map(a => walk(a, enclosing)) }
|
|
2832
2932
|
case 'object-literal':
|
|
@@ -3042,8 +3142,12 @@ export function exprToString(expr: ParsedExpr): string {
|
|
|
3042
3142
|
return `[${expr.elements.map(exprToString).join(', ')}]`
|
|
3043
3143
|
case 'array-method':
|
|
3044
3144
|
if (expr.method === 'flat') {
|
|
3045
|
-
// Preserve the normalised depth so diagnostics don't
|
|
3046
|
-
// print `.flat()` for a `.flat(2)` / `.flat(Infinity)`
|
|
3145
|
+
// Preserve the normalised / dynamic depth so diagnostics don't
|
|
3146
|
+
// misleadingly print `.flat()` for a `.flat(2)` / `.flat(Infinity)`
|
|
3147
|
+
// / `.flat(depthProp)` source.
|
|
3148
|
+
if (expr.depthExpr) {
|
|
3149
|
+
return `${exprToString(expr.object)}.flat(${exprToString(expr.depthExpr)})`
|
|
3150
|
+
}
|
|
3047
3151
|
const d = expr.flatDepth
|
|
3048
3152
|
const depthSrc = d === 'infinity' ? 'Infinity' : String(d)
|
|
3049
3153
|
return `${exprToString(expr.object)}.flat(${d === 1 ? '' : depthSrc})`
|
|
@@ -3115,8 +3219,12 @@ export function stringifyParsedExpr(expr: ParsedExpr): string {
|
|
|
3115
3219
|
return `[${expr.elements.map(stringifyParsedExpr).join(', ')}]`
|
|
3116
3220
|
case 'array-method':
|
|
3117
3221
|
if (expr.method === 'flat') {
|
|
3118
|
-
// Round-trip the normalised depth back to JS for the
|
|
3119
|
-
// path: `'infinity'` → `Infinity`, `1` is left implicit
|
|
3222
|
+
// Round-trip the normalised / dynamic depth back to JS for the
|
|
3223
|
+
// CSR / Hono path: `'infinity'` → `Infinity`, `1` is left implicit
|
|
3224
|
+
// (`.flat()`), a dynamic depth round-trips its own expression.
|
|
3225
|
+
if (expr.depthExpr) {
|
|
3226
|
+
return `${stringifyParsedExpr(expr.object)}.flat(${stringifyParsedExpr(expr.depthExpr)})`
|
|
3227
|
+
}
|
|
3120
3228
|
const d = expr.flatDepth
|
|
3121
3229
|
const depthSrc = d === 'infinity' ? 'Infinity' : String(d)
|
|
3122
3230
|
return `${stringifyParsedExpr(expr.object)}.flat(${d === 1 ? '' : depthSrc})`
|
|
@@ -3185,9 +3293,12 @@ export function materializeGetterCalls(expr: ParsedExpr, names: ReadonlySet<stri
|
|
|
3185
3293
|
return { kind: 'array-literal', elements: expr.elements.map(rw) }
|
|
3186
3294
|
case 'array-method':
|
|
3187
3295
|
// `flat`'s `args` is always `[]` (the depth is carried structurally in
|
|
3188
|
-
// `flatDepth`, not `args`) — still rewrite `object
|
|
3189
|
-
// `args.map` that every
|
|
3190
|
-
|
|
3296
|
+
// `flatDepth` / `depthExpr`, not `args`) — still rewrite `object` (and
|
|
3297
|
+
// `depthExpr`, when dynamic), just skip the `args.map` that every
|
|
3298
|
+
// other method needs.
|
|
3299
|
+
if (expr.method === 'flat') {
|
|
3300
|
+
return { ...expr, object: rw(expr.object), ...(expr.depthExpr ? { depthExpr: rw(expr.depthExpr) } : {}) }
|
|
3301
|
+
}
|
|
3191
3302
|
return { ...expr, object: rw(expr.object), args: expr.args.map(rw) }
|
|
3192
3303
|
case 'object-literal':
|
|
3193
3304
|
return {
|
|
@@ -3240,30 +3351,36 @@ export function serializeParsedExpr(expr: ParsedExpr): string | null {
|
|
|
3240
3351
|
*/
|
|
3241
3352
|
export function freeVarsInBody(body: ParsedExpr, params: ReadonlySet<string>): string[] {
|
|
3242
3353
|
const found = new Set<string>()
|
|
3243
|
-
|
|
3354
|
+
// `bound` starts as the outer callback's own `params` and grows when the
|
|
3355
|
+
// walk descends into a nested `map`/`filter` callback arrow (#2094) — a
|
|
3356
|
+
// NESTED arrow's own params shadow the outer ones for its body only,
|
|
3357
|
+
// mirroring {@link freeIdentifiers}'s lexical scoping. Before #2094 a
|
|
3358
|
+
// serializable body never contained a nested arrow, so this parameter
|
|
3359
|
+
// didn't need to change per call; it does now.
|
|
3360
|
+
const visit = (e: ParsedExpr, bound: ReadonlySet<string>): void => {
|
|
3244
3361
|
switch (e.kind) {
|
|
3245
3362
|
case 'identifier':
|
|
3246
|
-
if (!
|
|
3363
|
+
if (!bound.has(e.name)) found.add(e.name)
|
|
3247
3364
|
return
|
|
3248
3365
|
case 'binary':
|
|
3249
3366
|
case 'logical':
|
|
3250
|
-
visit(e.left)
|
|
3251
|
-
visit(e.right)
|
|
3367
|
+
visit(e.left, bound)
|
|
3368
|
+
visit(e.right, bound)
|
|
3252
3369
|
return
|
|
3253
3370
|
case 'unary':
|
|
3254
|
-
visit(e.argument)
|
|
3371
|
+
visit(e.argument, bound)
|
|
3255
3372
|
return
|
|
3256
3373
|
case 'conditional':
|
|
3257
|
-
visit(e.test)
|
|
3258
|
-
visit(e.consequent)
|
|
3259
|
-
visit(e.alternate)
|
|
3374
|
+
visit(e.test, bound)
|
|
3375
|
+
visit(e.consequent, bound)
|
|
3376
|
+
visit(e.alternate, bound)
|
|
3260
3377
|
return
|
|
3261
3378
|
case 'member':
|
|
3262
|
-
visit(e.object)
|
|
3379
|
+
visit(e.object, bound)
|
|
3263
3380
|
return
|
|
3264
3381
|
case 'index-access':
|
|
3265
|
-
visit(e.object)
|
|
3266
|
-
visit(e.index)
|
|
3382
|
+
visit(e.object, bound)
|
|
3383
|
+
visit(e.index, bound)
|
|
3267
3384
|
return
|
|
3268
3385
|
case 'call':
|
|
3269
3386
|
// A builtin callee (`String`/`Number`/`Boolean`, or `Math.<fn>`) is
|
|
@@ -3271,42 +3388,52 @@ export function freeVarsInBody(body: ParsedExpr, params: ReadonlySet<string>): s
|
|
|
3271
3388
|
// captured free var. Visiting it would add `Math` / `String` to the
|
|
3272
3389
|
// env, making the adapter emit an undefined `$Math` / `.Math` base_env
|
|
3273
3390
|
// entry (Copilot review #2031). Skip the callee identifier in that
|
|
3274
|
-
// case; the arguments are still real references and are visited
|
|
3275
|
-
|
|
3276
|
-
|
|
3391
|
+
// case; the arguments are still real references and are visited
|
|
3392
|
+
// (a nested `.map(cb)` / `.filter(cb)` callback call's `args[0]` is
|
|
3393
|
+
// the callback `arrow`, visited below with its own params bound).
|
|
3394
|
+
if (evalBuiltinCalleeName(e.callee) === null) visit(e.callee, bound)
|
|
3395
|
+
e.args.forEach((a) => visit(a, bound))
|
|
3277
3396
|
return
|
|
3278
3397
|
case 'template-literal':
|
|
3279
|
-
for (const p of e.parts) if (p.type === 'expression') visit(p.expr)
|
|
3398
|
+
for (const p of e.parts) if (p.type === 'expression') visit(p.expr, bound)
|
|
3280
3399
|
return
|
|
3281
3400
|
case 'array-literal':
|
|
3282
|
-
e.elements.forEach(visit)
|
|
3401
|
+
e.elements.forEach((el) => visit(el, bound))
|
|
3283
3402
|
return
|
|
3284
3403
|
case 'object-literal':
|
|
3285
3404
|
// Object *values* are references; keys are not. (Shorthand `{ x }`
|
|
3286
3405
|
// carries the ref on its `value` identifier, which is visited here.)
|
|
3287
|
-
for (const p of e.properties) visit(p.value)
|
|
3406
|
+
for (const p of e.properties) visit(p.value, bound)
|
|
3288
3407
|
return
|
|
3289
3408
|
case 'array-method':
|
|
3290
|
-
//
|
|
3291
|
-
// `object` (the receiver) and `args` (
|
|
3292
|
-
// positions serialized, so visit both when
|
|
3293
|
-
// with
|
|
3294
|
-
//
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3409
|
+
// `.includes(x)` / `.join(sep?)` are serializable ({@link
|
|
3410
|
+
// toEvalNode}); their `object` (the receiver) and `args` (needle /
|
|
3411
|
+
// separator) are the value positions serialized, so visit both when
|
|
3412
|
+
// the tree reaches here with either method. A nested `.map`/`.filter`
|
|
3413
|
+
// reaches this walk as a `call` (above), not an `array-method` — see
|
|
3414
|
+
// {@link asCallbackMethodCall}. Every other `array-method` is
|
|
3415
|
+
// non-serializable and doesn't occur in a serializable body.
|
|
3416
|
+
if (e.method === 'includes' || e.method === 'join') {
|
|
3417
|
+
visit(e.object, bound)
|
|
3418
|
+
e.args.forEach((a) => visit(a, bound))
|
|
3298
3419
|
}
|
|
3299
3420
|
return
|
|
3421
|
+
// A nested callback arrow (the `.map`/`.filter` callback argument,
|
|
3422
|
+
// #2094): its own params shadow the outer bound set for its body only.
|
|
3423
|
+
case 'arrow': {
|
|
3424
|
+
const inner = e.params.length === 0 ? bound : new Set([...bound, ...e.params])
|
|
3425
|
+
visit(e.body, inner)
|
|
3426
|
+
return
|
|
3427
|
+
}
|
|
3300
3428
|
// Non-serializable kinds don't occur in a serializable body
|
|
3301
3429
|
// (serializeParsedExpr returns null for them); nothing to collect.
|
|
3302
3430
|
case 'literal':
|
|
3303
|
-
case 'arrow':
|
|
3304
3431
|
case 'regex':
|
|
3305
3432
|
case 'unsupported':
|
|
3306
3433
|
return
|
|
3307
3434
|
}
|
|
3308
3435
|
}
|
|
3309
|
-
visit(body)
|
|
3436
|
+
visit(body, params)
|
|
3310
3437
|
return [...found].sort()
|
|
3311
3438
|
}
|
|
3312
3439
|
|
|
@@ -3374,6 +3501,7 @@ export function freeIdentifiers(expr: ParsedExpr): Set<string> | null {
|
|
|
3374
3501
|
case 'array-method':
|
|
3375
3502
|
if (!visit(e.object, bound)) return false
|
|
3376
3503
|
for (const a of e.args) if (!visit(a, bound)) return false
|
|
3504
|
+
if (e.method === 'flat' && e.depthExpr && !visit(e.depthExpr, bound)) return false
|
|
3377
3505
|
return true
|
|
3378
3506
|
case 'object-literal':
|
|
3379
3507
|
for (const p of e.properties) if (!visit(p.value, bound)) return false
|
|
@@ -3478,6 +3606,44 @@ function toEvalNode(e: ParsedExpr): Record<string, unknown> | null {
|
|
|
3478
3606
|
return object && index ? { kind: 'index-access', object, index } : null
|
|
3479
3607
|
}
|
|
3480
3608
|
case 'call': {
|
|
3609
|
+
// A nested `.map(cb)` / `.filter(cb)` callback call (#2094) — the
|
|
3610
|
+
// SAME recognition `callbackMethod()` dispatch uses. Only these two
|
|
3611
|
+
// widen into the evaluator's surface: they are order-preserving,
|
|
3612
|
+
// per-element, and produce a bounded result, so the pure gate can
|
|
3613
|
+
// still reason about them structurally. `sort` / `reduce` /
|
|
3614
|
+
// `reduceRight` / `flat` / `flatMap` / `every` / `some` / `find*`
|
|
3615
|
+
// nested INSIDE an eval body stay refused — `asCallbackMethodCall`
|
|
3616
|
+
// recognizes them too, but they fall through to the generic
|
|
3617
|
+
// non-builtin-callee refusal below because `cb.method` doesn't match
|
|
3618
|
+
// either case here.
|
|
3619
|
+
const cb = asCallbackMethodCall(e)
|
|
3620
|
+
if (cb && (cb.method === 'map' || cb.method === 'filter')) {
|
|
3621
|
+
const object = toEvalNode(cb.object)
|
|
3622
|
+
if (!object) return null
|
|
3623
|
+
const body = toEvalNode(cb.arrow.body)
|
|
3624
|
+
if (!body) return null
|
|
3625
|
+
// `cb.arrow.params` is always plain identifier names (the `ParsedExpr`
|
|
3626
|
+
// `arrow` variant only carries destructure-rewritten / plain params —
|
|
3627
|
+
// an array-binding-pattern param stays `unsupported` upstream and
|
|
3628
|
+
// never reaches here). 1- and 2-param arrows are both supported for
|
|
3629
|
+
// `map`/`filter` (element, and index when declared); the Go/eval-cases
|
|
3630
|
+
// reference pins the 2-param contract.
|
|
3631
|
+
//
|
|
3632
|
+
// Re-serialized in the SAME `call` / `member` / `arrow` shape
|
|
3633
|
+
// `asCallbackMethodCall` recognizes — NOT a bespoke wrapper — because
|
|
3634
|
+
// the `eval-vectors.json` golden corpus carries the genuine
|
|
3635
|
+
// `ParsedExpr` `parseExpression` produces (unfiltered by
|
|
3636
|
+
// `toEvalNode`; see `eval-generate.ts`), which is exactly this
|
|
3637
|
+
// shape. Using the same shape here means the compiled-template
|
|
3638
|
+
// embedded body and the golden-vector corpus reach the evaluator in
|
|
3639
|
+
// one encoding, so a single `EvalNode`/`evaluate` recognition path
|
|
3640
|
+
// (Go `eval.go`, `eval-reference.ts`) covers both.
|
|
3641
|
+
return {
|
|
3642
|
+
kind: 'call',
|
|
3643
|
+
callee: { kind: 'member', object, property: cb.method, computed: false },
|
|
3644
|
+
args: [{ kind: 'arrow', params: cb.arrow.params, body }],
|
|
3645
|
+
}
|
|
3646
|
+
}
|
|
3481
3647
|
// The evaluator executes only the builtin allowlist; a non-builtin callee
|
|
3482
3648
|
// would evaluate to nil at runtime, so refuse it here (the purity gate).
|
|
3483
3649
|
if (evalBuiltinCalleeName(e.callee) === null) return null
|
|
@@ -3523,18 +3689,33 @@ function toEvalNode(e: ParsedExpr): Record<string, unknown> | null {
|
|
|
3523
3689
|
return { kind: 'object-literal', properties }
|
|
3524
3690
|
}
|
|
3525
3691
|
case 'array-method': {
|
|
3526
|
-
// `.includes(x)` is
|
|
3692
|
+
// `.includes(x)` is one `array-method` the evaluator executes
|
|
3527
3693
|
// (Go `eval.go` / Perl `Evaluator.pm`, includes support): the
|
|
3528
3694
|
// receiver-type dispatch (array SameValueZero membership vs string
|
|
3529
3695
|
// substring) happens at evaluator runtime, same as the SSR template
|
|
3530
|
-
// lowering's `bf_includes` / `$bf->includes`.
|
|
3531
|
-
// `array-method` (`join`, `slice`, `flat`, …) is outside the
|
|
3532
|
-
// evaluator's surface and refuses below.
|
|
3696
|
+
// lowering's `bf_includes` / `$bf->includes`.
|
|
3533
3697
|
if (e.method === 'includes' && e.args.length === 1) {
|
|
3534
3698
|
const object = toEvalNode(e.object)
|
|
3535
3699
|
const arg = toEvalNode(e.args[0])
|
|
3536
3700
|
return object && arg ? { kind: 'array-method', method: 'includes', object, args: [arg] } : null
|
|
3537
3701
|
}
|
|
3702
|
+
// `.join(sep?)` (#2094) — 0 or 1 serializable separator arg. Lets a
|
|
3703
|
+
// nested `.map`/`.filter` chain feed straight into `.join(' ')` inside
|
|
3704
|
+
// an eval body (the #1938 blog-showcase shape:
|
|
3705
|
+
// `p.tags.map(t => '#' + t)` is joined by the OUTER `.join`, which is
|
|
3706
|
+
// already the caller-visible template expression, not inside the eval
|
|
3707
|
+
// body — this arm is for a `.join` that itself appears NESTED inside
|
|
3708
|
+
// an eval body, e.g. a `.map(p => p.tags.join(','))` projection).
|
|
3709
|
+
if (e.method === 'join' && e.args.length <= 1) {
|
|
3710
|
+
const object = toEvalNode(e.object)
|
|
3711
|
+
if (!object) return null
|
|
3712
|
+
if (e.args.length === 0) return { kind: 'array-method', method: 'join', object, args: [] }
|
|
3713
|
+
const sep = toEvalNode(e.args[0])
|
|
3714
|
+
return sep ? { kind: 'array-method', method: 'join', object, args: [sep] } : null
|
|
3715
|
+
}
|
|
3716
|
+
// Every other `array-method` (`slice`, `flat`, `flatMap` [handled as
|
|
3717
|
+
// its own `call` shape when it has an arrow, so not reached here],
|
|
3718
|
+
// …) is outside the evaluator's surface and refuses.
|
|
3538
3719
|
return null
|
|
3539
3720
|
}
|
|
3540
3721
|
// Outside the evaluator's pure-expression surface — refuse so the caller
|
package/src/index.ts
CHANGED
|
@@ -26,6 +26,9 @@ export type {
|
|
|
26
26
|
IRConditional,
|
|
27
27
|
IRLoop,
|
|
28
28
|
IRLoopChildComponent,
|
|
29
|
+
LoopParamBinding,
|
|
30
|
+
LoopBindingPathSegment,
|
|
31
|
+
RestExcludeKey,
|
|
29
32
|
IRComponent,
|
|
30
33
|
IRFragment,
|
|
31
34
|
IRSlot,
|
|
@@ -50,6 +53,8 @@ export type {
|
|
|
50
53
|
TypeDefinition,
|
|
51
54
|
SourceLocation,
|
|
52
55
|
CompilerError,
|
|
56
|
+
ConformancePin,
|
|
57
|
+
ConformancePins,
|
|
53
58
|
} from './types.ts'
|
|
54
59
|
|
|
55
60
|
// Analyzer
|
|
@@ -89,6 +94,7 @@ export {
|
|
|
89
94
|
getLoweringPlugins,
|
|
90
95
|
prepareLoweringMatchers,
|
|
91
96
|
matchLoweringCall,
|
|
97
|
+
isValidHelperId,
|
|
92
98
|
__resetLoweringPluginsForTest,
|
|
93
99
|
type LoweringPlugin,
|
|
94
100
|
type LoweringNode,
|
|
@@ -260,6 +266,22 @@ export interface BuildOptions {
|
|
|
260
266
|
* Forwarded to `compileJSX` as `CompileOptions.localImportPrefixes`.
|
|
261
267
|
*/
|
|
262
268
|
localImportPrefixes?: string[]
|
|
269
|
+
/**
|
|
270
|
+
* How the CLI produces `barefoot.js` (the client runtime bundle):
|
|
271
|
+
* - `'treeshake'` (default) — bundle only the runtime exports this
|
|
272
|
+
* project's compiled client JS actually imports, plus a small
|
|
273
|
+
* always-kept public mount API (`render`, `hydrate`, etc.).
|
|
274
|
+
* - `'full'` — copy the entire prebuilt runtime bundle verbatim.
|
|
275
|
+
* See `@barefootjs/cli`'s `runtime-treeshake.ts` for the collector and
|
|
276
|
+
* `ALWAYS_KEEP_RUNTIME_EXPORTS` for the always-kept names.
|
|
277
|
+
*/
|
|
278
|
+
runtimeBundle?: 'treeshake' | 'full'
|
|
279
|
+
/**
|
|
280
|
+
* Extra `@barefootjs/client*` export names to force-keep in `barefoot.js`
|
|
281
|
+
* under `runtimeBundle: 'treeshake'` — for names only ever referenced
|
|
282
|
+
* from hand-written page scripts the CLI never compiles.
|
|
283
|
+
*/
|
|
284
|
+
runtimeKeep?: string[]
|
|
263
285
|
}
|
|
264
286
|
|
|
265
287
|
// AttrValue constructors
|
|
@@ -286,7 +308,7 @@ export type { StyleObjectEntry } from './expression-parser.ts'
|
|
|
286
308
|
export type { ParsedExpr, ObjectLiteralProperty, ParsedStatement, SortComparator, SortKey, FlatDepth, SupportLevel, SupportResult, TemplatePart } from './expression-parser.ts'
|
|
287
309
|
export { buildLoopChainExpr } from './loop-chain.ts'
|
|
288
310
|
export type { LoopChainInputs } from './loop-chain.ts'
|
|
289
|
-
export { isLowerableObjectRestDestructure } from './loop-destructure.ts'
|
|
311
|
+
export { isLowerableLoopDestructure, isLowerableObjectRestDestructure } from './loop-destructure.ts'
|
|
290
312
|
|
|
291
313
|
// Debug analysis
|
|
292
314
|
export {
|
|
@@ -6,7 +6,7 @@ import { type IRNode, type IRElement, type IRComponent, type IRLoop, type IRProp
|
|
|
6
6
|
import type { ClientJsContext, ConditionalBranchChildComponent, ConditionalBranchReactiveAttr, BranchLoop, ConditionalBranchTextEffect, ConditionalElement, LoopChildBindings, LoopChildBranchSummary, LoopChildConditional, LoopOffset, NestedLoop } from './types.ts'
|
|
7
7
|
import { attrValueToString, freeIdsFromRefs, quotePropName, PROPS_PARAM } from './utils.ts'
|
|
8
8
|
import { classifyReactivity, decideWrapForAttr, decideWrapForChildProp, decideWrapFromAstFlags, collectEventHandlersFromIR, collectConditionalBranchEvents, collectConditionalBranchRefs, collectConditionalBranchChildComponents, collectLoopChildEventsWithNesting, collectLoopChildReactiveAttrs, collectLoopChildReactiveTexts, collectLoopChildRefs, emptyLoopChildBindings } from './reactivity.ts'
|
|
9
|
-
import { irToHtmlTemplate, irToPlaceholderTemplate, irChildrenToJsExpr } from './html-template.ts'
|
|
9
|
+
import { irToHtmlTemplate, irToPlaceholderTemplate, irChildrenToJsExpr, buildLoopSkeletonTemplate } from './html-template.ts'
|
|
10
10
|
import { expandDynamicPropValue, expandConstantForReactivity } from './prop-handling.ts'
|
|
11
11
|
import { walkIR, stopAt } from './walker.ts'
|
|
12
12
|
import { buildLoopChainExpr } from '../loop-chain.ts'
|
|
@@ -671,6 +671,7 @@ export function collectElements(
|
|
|
671
671
|
|
|
672
672
|
let template = ''
|
|
673
673
|
let staticItemTemplate: string | undefined
|
|
674
|
+
let skeletonTemplate: string | undefined
|
|
674
675
|
if (l.childComponent) {
|
|
675
676
|
template = '' // childComponent path uses createComponent directly
|
|
676
677
|
// CSR materialize fallback (#1268): when the loop array references an
|
|
@@ -714,6 +715,18 @@ export function collectElements(
|
|
|
714
715
|
staticItemTemplate = useElementReconciliation
|
|
715
716
|
? irToPlaceholderTemplate(l.children[0], buildRestSpreadNames(ctx), 0)
|
|
716
717
|
: irToHtmlTemplate(l.children[0], buildRestSpreadNames(ctx), 0)
|
|
718
|
+
} else if (!useElementReconciliation && !l.bodyIsMultiRoot && !l.bodyIsItemConditional) {
|
|
719
|
+
// Hoisted shared-template fast path (perf): only for the plain
|
|
720
|
+
// `mapArray` shape — single-root, dynamic array, no element
|
|
721
|
+
// reconciliation. `buildLoopSkeletonTemplate` re-derives safety
|
|
722
|
+
// from the raw IR (spread attrs, conditionals, unslotted dynamic
|
|
723
|
+
// expressions, …) and returns `null` for anything it can't prove
|
|
724
|
+
// safe; the plan builder (`build-loop.ts`) falls back to the
|
|
725
|
+
// per-row `template` above whenever this stays `undefined`.
|
|
726
|
+
skeletonTemplate = buildLoopSkeletonTemplate(l.children[0], {
|
|
727
|
+
reactiveAttrKeys: new Set(bindings.reactiveAttrs.map(a => `${a.childSlotId}::${a.attrName}`)),
|
|
728
|
+
reactiveTextSlotIds: new Set(bindings.reactiveTexts.map(t => t.slotId)),
|
|
729
|
+
}) ?? undefined
|
|
717
730
|
}
|
|
718
731
|
}
|
|
719
732
|
|
|
@@ -732,6 +745,7 @@ export function collectElements(
|
|
|
732
745
|
iterationShape: l.iterationShape,
|
|
733
746
|
template,
|
|
734
747
|
staticItemTemplate,
|
|
748
|
+
skeletonTemplate,
|
|
735
749
|
childEventHandlers: childHandlers,
|
|
736
750
|
bindings,
|
|
737
751
|
childComponent: l.childComponent,
|
|
@@ -64,7 +64,12 @@ export function buildEnvFromCtx(ctx: ClientJsContext): RelocateEnv {
|
|
|
64
64
|
effects: ctx.effects,
|
|
65
65
|
onMounts: ctx.onMounts,
|
|
66
66
|
initStatements: ctx.initStatements,
|
|
67
|
-
imports
|
|
67
|
+
// Real component imports (#2069) — `buildRelocateEnvFromIR` calls
|
|
68
|
+
// `prepareLoweringMatchers(metadata)` on this reconstructed object,
|
|
69
|
+
// and plugin `prepare()` resolves local import names from
|
|
70
|
+
// `metadata.imports`. `[]` here would silently disable every
|
|
71
|
+
// import-aware LoweringPlugin for the client-JS inline-safety gate.
|
|
72
|
+
imports: ctx.imports,
|
|
68
73
|
templateImports: [],
|
|
69
74
|
namedExports: [],
|
|
70
75
|
localFunctions: ctx.localFunctions,
|
|
@@ -102,6 +102,7 @@ export function buildPlainLoopPlan(elem: TopLevelLoop, profileComponentName?: st
|
|
|
102
102
|
indexParam: elem.index || '__idx',
|
|
103
103
|
mapPreambleWrapped: elem.mapPreamble ? wrap(elem.mapPreamble) : '',
|
|
104
104
|
template: elem.template,
|
|
105
|
+
skeletonTemplate: elem.skeletonTemplate,
|
|
105
106
|
reactiveEffects: hasReactive ? buildLoopReactiveEffectsPlan(elem, profileComponentName) : null,
|
|
106
107
|
childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings),
|
|
107
108
|
bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false,
|
|
@@ -94,6 +94,15 @@ interface PlainLoopVariant extends DynamicLoopCommon {
|
|
|
94
94
|
mapPreambleWrapped: string
|
|
95
95
|
/** HTML template string for one item. */
|
|
96
96
|
template: string
|
|
97
|
+
/**
|
|
98
|
+
* Shared once-per-loop skeleton template (perf) — see
|
|
99
|
+
* `buildLoopSkeletonTemplate` (html-template.ts). When present, the
|
|
100
|
+
* stringifier declares it once (before the `mapArray` call) and clones
|
|
101
|
+
* from it instead of re-parsing `template`'s interpolated innerHTML per
|
|
102
|
+
* row. `undefined` means the loop body wasn't proven safe to hoist; the
|
|
103
|
+
* stringifier falls back to the legacy per-row `emitTemplateCloneInline`.
|
|
104
|
+
*/
|
|
105
|
+
skeletonTemplate?: string
|
|
97
106
|
/** Resolved reactive-effects plan — null forces the single-line renderItem shape. */
|
|
98
107
|
reactiveEffects: ReactiveEffectsPlan | null
|
|
99
108
|
/**
|