@barefootjs/jsx 0.17.0 → 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/env-signal.d.ts +42 -7
- package/dist/adapters/env-signal.d.ts.map +1 -1
- 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 +48 -2
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +24 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1728 -969
- 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/ssr-seed-plan.d.ts +84 -0
- package/dist/ssr-seed-plan.d.ts.map +1 -0
- package/dist/types.d.ts +79 -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 +74 -3
- package/src/__tests__/free-identifiers.test.ts +55 -0
- 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__/materialize-getter-calls.test.ts +58 -0
- package/src/__tests__/props-destructuring.test.ts +110 -0
- package/src/__tests__/serialize-parsed-expr.test.ts +88 -2
- package/src/__tests__/ssr-defaults.test.ts +20 -0
- package/src/__tests__/ssr-seed-plan.test.ts +212 -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/env-signal.ts +57 -9
- 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 +10 -1
- package/src/expression-parser.ts +421 -50
- package/src/index.ts +30 -3
- 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/ssr-seed-plan.ts +146 -0
- package/src/types.ts +76 -0
package/src/jsx-to-ir.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
type AttrValue,
|
|
24
24
|
type IRTemplatePart,
|
|
25
25
|
type LoopParamBinding,
|
|
26
|
+
type LoopBindingPathSegment,
|
|
26
27
|
type RestExcludeKey,
|
|
27
28
|
type FlatMapCallback,
|
|
28
29
|
type FlatMapJsxFragment,
|
|
@@ -34,7 +35,8 @@ import {
|
|
|
34
35
|
} from './types.ts'
|
|
35
36
|
import { type AnalyzerContext, type MultiReturnJsxInfo, getSourceLocation, collectReactiveGetterNames } from './analyzer-context.ts'
|
|
36
37
|
import { parseExpression, isSupported, parseBlockBody, foldBlockToExpr, predicateTernaryToLogical, tsNodeToParsedExpr, sortComparatorFromArrow, stringifyParsedExpr, cssKebabCase, type ParsedExpr } from './expression-parser.ts'
|
|
37
|
-
import type { IRLoopSort } from './types.ts'
|
|
38
|
+
import type { IRLoopSort, FunctionInfo } from './types.ts'
|
|
39
|
+
import { formatParamWithType } from './module-exports.ts'
|
|
38
40
|
import { createError, ErrorCodes, internalInvariant } from './errors.ts'
|
|
39
41
|
import { CLIENT_BUILTIN_SOURCE, isClientBuiltinName, type ClientBuiltinTag } from './builtins.ts'
|
|
40
42
|
import { containsReactiveExpression } from './reactivity-checker.ts'
|
|
@@ -1568,6 +1570,12 @@ function transformExpressionInner(
|
|
|
1568
1570
|
node: ts.JsxExpression,
|
|
1569
1571
|
isClientOnly: boolean,
|
|
1570
1572
|
): IRNode | null {
|
|
1573
|
+
// #2092: `{cn\`base ${tone()}\`}` in JSX-child position — same desugar
|
|
1574
|
+
// as the attribute path (`getAttributeValue`), applied first so every
|
|
1575
|
+
// check below (JSX-constant inlining, the shared dispatcher, the
|
|
1576
|
+
// scalar fallback's `ctx.getJS`) sees the untagged template literal.
|
|
1577
|
+
expr = tryDesugarInterleaveTaggedTemplate(expr, ctx)
|
|
1578
|
+
|
|
1571
1579
|
// Check for bare signal/memo identifier (BF044)
|
|
1572
1580
|
checkBareSignalOrMemoIdentifier(expr, ctx)
|
|
1573
1581
|
|
|
@@ -2440,35 +2448,57 @@ type SortExtractionResult = {
|
|
|
2440
2448
|
* (eval-first) and the client's JS round-trip. Accepted catalogue: subtraction
|
|
2441
2449
|
* (`a.f - b.f`, `a - b`, reverse for desc), `.localeCompare`, and the
|
|
2442
2450
|
* relational-ternary sign forms; any of them `||`-chained for multi-key.
|
|
2451
|
+
*
|
|
2452
|
+
* A bare identifier callback (`.sort(byPrice)`, #2090) is resolved one hop
|
|
2453
|
+
* through {@link resolveSortComparatorIdentifier} — a module- or
|
|
2454
|
+
* component-scope `const byPrice = (a, b) => …` or `function byPrice(a, b)
|
|
2455
|
+
* {…}` — before falling into the same arrow + catalogue gate below, so a
|
|
2456
|
+
* resolved reference is byte-for-byte equivalent to inlining it. Alias
|
|
2457
|
+
* chains (`const c2 = c1`) and imported/prop identifiers are NOT followed —
|
|
2458
|
+
* they surface a distinct "could not be resolved" BF021.
|
|
2443
2459
|
*/
|
|
2444
2460
|
function extractSortComparator(
|
|
2445
2461
|
callback: ts.Expression,
|
|
2446
2462
|
_method: 'sort' | 'toSorted',
|
|
2447
2463
|
ctx: TransformContext
|
|
2448
2464
|
): SortExtractionResult {
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2465
|
+
// Surface the OUTER callback source — users see the string they wrote
|
|
2466
|
+
// (for an identifier callback, `ctx.getJS` returns just the bare name).
|
|
2467
|
+
const outerRaw = ctx.getJS(callback)
|
|
2468
|
+
const unsupported = (): SortExtractionResult => ({
|
|
2469
|
+
result: null,
|
|
2470
|
+
unsupportedReason:
|
|
2471
|
+
`Sort comparator '${outerRaw}' is not a supported shape. Accepted:\n` +
|
|
2472
|
+
` (a, b) => a - b\n` +
|
|
2473
|
+
` (a, b) => a.field - b.field\n` +
|
|
2474
|
+
` (a, b) => a.localeCompare(b)\n` +
|
|
2475
|
+
` (a, b) => a.field.localeCompare(b.field)\n` +
|
|
2476
|
+
` (a, b) => a.field > b.field ? 1 : a.field < b.field ? -1 : 0\n` +
|
|
2477
|
+
` any of the above '||'-chained for multi-key tie-breaks\n` +
|
|
2478
|
+
`(reverse the operands for descending order).`,
|
|
2479
|
+
})
|
|
2480
|
+
|
|
2481
|
+
let resolvedNode: ts.Expression = callback
|
|
2482
|
+
if (ts.isIdentifier(callback)) {
|
|
2483
|
+
const resolved = resolveSortComparatorIdentifier(callback.text, ctx)
|
|
2484
|
+
if (!resolved) {
|
|
2485
|
+
return {
|
|
2486
|
+
result: null,
|
|
2487
|
+
unsupportedReason:
|
|
2488
|
+
`Sort comparator '${outerRaw}' could not be resolved to a local function — ` +
|
|
2489
|
+
`declare it in the same file or inline it.`,
|
|
2490
|
+
}
|
|
2463
2491
|
}
|
|
2492
|
+
resolvedNode = resolved
|
|
2464
2493
|
}
|
|
2465
|
-
|
|
2494
|
+
|
|
2495
|
+
if (!ts.isArrowFunction(resolvedNode) && !ts.isFunctionExpression(resolvedNode)) {
|
|
2466
2496
|
return {
|
|
2467
2497
|
result: null,
|
|
2468
2498
|
unsupportedReason: 'Sort comparator must be an arrow function or function expression',
|
|
2469
2499
|
}
|
|
2470
2500
|
}
|
|
2471
|
-
const arrow = tsNodeToParsedExpr(
|
|
2501
|
+
const arrow = tsNodeToParsedExpr(resolvedNode)
|
|
2472
2502
|
if (arrow.kind !== 'arrow' || arrow.params.length !== 2) return unsupported()
|
|
2473
2503
|
// Gate on the same catalogue the localeCompare fallback recovers, so the
|
|
2474
2504
|
// hoist decision (and thus the SSR/client split) is byte-for-byte unchanged.
|
|
@@ -2483,6 +2513,47 @@ function extractSortComparator(
|
|
|
2483
2513
|
}
|
|
2484
2514
|
}
|
|
2485
2515
|
|
|
2516
|
+
/**
|
|
2517
|
+
* Resolve a bare-identifier sort comparator callback (`.sort(byPrice)`,
|
|
2518
|
+
* #2090) to its underlying arrow / function-expression node, ONE HOP only
|
|
2519
|
+
* — no alias chains (`const c2 = c1` is not followed; `c2` resolves to the
|
|
2520
|
+
* identifier `c1`, not a function, and is left unresolved).
|
|
2521
|
+
*
|
|
2522
|
+
* A name bound BOTH as a const and as a `function` declaration is refused
|
|
2523
|
+
* outright. In valid JS that collision only occurs across scopes (a
|
|
2524
|
+
* same-scope redeclaration is a syntax error), and `FunctionInfo` does not
|
|
2525
|
+
* carry the source scope — component-body `function` declarations are
|
|
2526
|
+
* hoisted to module scope for client emission, so its `isModule` reflects
|
|
2527
|
+
* EMISSION placement, not lexical position. Picking either binding would
|
|
2528
|
+
* risk compiling the comparator the call site can't actually see (Copilot
|
|
2529
|
+
* review on #2091), so the ambiguity resolves to the loud unresolved
|
|
2530
|
+
* BF021 instead of a guess. Within a single kind, the existing
|
|
2531
|
+
* shadowing-aware lookups apply (`findLocalConst` / `findLocalFunction`:
|
|
2532
|
+
* component scope beats module scope, last in source order); a binding
|
|
2533
|
+
* that isn't an arrow / function expression fails resolution without any
|
|
2534
|
+
* cross-kind fallback.
|
|
2535
|
+
*
|
|
2536
|
+
* Returns null when the name doesn't resolve to a local arrow /
|
|
2537
|
+
* function-expression — covers the cross-kind ambiguity, a non-function
|
|
2538
|
+
* const, an import, a prop, or a name with no local binding at all. The
|
|
2539
|
+
* caller surfaces BF021 either way; the specific message (off-catalogue
|
|
2540
|
+
* vs. unresolved) is decided by the caller, not here.
|
|
2541
|
+
*/
|
|
2542
|
+
function resolveSortComparatorIdentifier(name: string, ctx: TransformContext): ts.Expression | null {
|
|
2543
|
+
const constInfo = findLocalConst(name, ctx)
|
|
2544
|
+
const fnInfo = findLocalFunction(name, ctx)
|
|
2545
|
+
if (constInfo && fnInfo) return null
|
|
2546
|
+
if (constInfo) {
|
|
2547
|
+
const ast = parseConstInitializer(constInfo)
|
|
2548
|
+
return ast && (ts.isArrowFunction(ast) || ts.isFunctionExpression(ast)) ? ast : null
|
|
2549
|
+
}
|
|
2550
|
+
if (fnInfo) {
|
|
2551
|
+
const ast = parseFunctionInfoAsExpr(fnInfo)
|
|
2552
|
+
return ast && (ts.isArrowFunction(ast) || ts.isFunctionExpression(ast)) ? ast : null
|
|
2553
|
+
}
|
|
2554
|
+
return null
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2486
2557
|
/**
|
|
2487
2558
|
* Result type for extractFilterPredicate. The predicate is always an expression:
|
|
2488
2559
|
* a block body is normalized to one via `foldBlockToExpr` +
|
|
@@ -2650,7 +2721,11 @@ function extractLoopParamBindings(
|
|
|
2650
2721
|
: `${prefix}[${JSON.stringify(key)}]`
|
|
2651
2722
|
}
|
|
2652
2723
|
|
|
2653
|
-
const walk = (
|
|
2724
|
+
const walk = (
|
|
2725
|
+
p: ts.ArrayBindingPattern | ts.ObjectBindingPattern,
|
|
2726
|
+
prefix: string,
|
|
2727
|
+
segments: readonly LoopBindingPathSegment[],
|
|
2728
|
+
): void => {
|
|
2654
2729
|
if (unsupported) return
|
|
2655
2730
|
if (ts.isArrayBindingPattern(p)) {
|
|
2656
2731
|
const elements = p.elements
|
|
@@ -2676,14 +2751,16 @@ function extractLoopParamBindings(
|
|
|
2676
2751
|
name: el.name.text,
|
|
2677
2752
|
path: prefix,
|
|
2678
2753
|
rest: { kind: 'array', from: index },
|
|
2754
|
+
segments,
|
|
2679
2755
|
})
|
|
2680
2756
|
return
|
|
2681
2757
|
}
|
|
2682
2758
|
const path = `${prefix}[${index}]`
|
|
2759
|
+
const nextSegments = [...segments, { kind: 'index', index } as const]
|
|
2683
2760
|
if (ts.isIdentifier(el.name)) {
|
|
2684
|
-
bindings.push({ name: el.name.text, path })
|
|
2761
|
+
bindings.push({ name: el.name.text, path, segments: nextSegments })
|
|
2685
2762
|
} else {
|
|
2686
|
-
walk(el.name, path)
|
|
2763
|
+
walk(el.name, path, nextSegments)
|
|
2687
2764
|
}
|
|
2688
2765
|
}
|
|
2689
2766
|
return
|
|
@@ -2712,6 +2789,7 @@ function extractLoopParamBindings(
|
|
|
2712
2789
|
name: el.name.text,
|
|
2713
2790
|
path: prefix,
|
|
2714
2791
|
rest: { kind: 'object', exclude: collectedKeys },
|
|
2792
|
+
segments,
|
|
2715
2793
|
})
|
|
2716
2794
|
return
|
|
2717
2795
|
}
|
|
@@ -2728,18 +2806,20 @@ function extractLoopParamBindings(
|
|
|
2728
2806
|
unsupported = true
|
|
2729
2807
|
return
|
|
2730
2808
|
}
|
|
2731
|
-
|
|
2809
|
+
const keyIsIdent = isIdent(keyText)
|
|
2810
|
+
collectedKeys.push({ key: keyText, isIdent: keyIsIdent })
|
|
2732
2811
|
const path = appendDotAccess(prefix, keyText)
|
|
2812
|
+
const nextSegments = [...segments, { kind: 'field', key: keyText, isIdent: keyIsIdent } as const]
|
|
2733
2813
|
if (ts.isIdentifier(el.name)) {
|
|
2734
|
-
bindings.push({ name: el.name.text, path })
|
|
2814
|
+
bindings.push({ name: el.name.text, path, segments: nextSegments })
|
|
2735
2815
|
} else {
|
|
2736
|
-
walk(el.name, path)
|
|
2816
|
+
walk(el.name, path, nextSegments)
|
|
2737
2817
|
}
|
|
2738
2818
|
}
|
|
2739
2819
|
}
|
|
2740
2820
|
|
|
2741
2821
|
if (ts.isArrayBindingPattern(pattern) || ts.isObjectBindingPattern(pattern)) {
|
|
2742
|
-
walk(pattern, '')
|
|
2822
|
+
walk(pattern, '', [])
|
|
2743
2823
|
if (unsupported) return { unsupported: true }
|
|
2744
2824
|
return bindings
|
|
2745
2825
|
}
|
|
@@ -4043,6 +4123,13 @@ function getAttributeValue(attr: ts.JsxAttribute, ctx: TransformContext): AttrVa
|
|
|
4043
4123
|
}
|
|
4044
4124
|
}
|
|
4045
4125
|
|
|
4126
|
+
// #2092: `className={cn\`base ${tone()}\`}` — a tagged template whose
|
|
4127
|
+
// tag resolves to a recognized interleave function desugars to the
|
|
4128
|
+
// equivalent untagged template literal, so every check below (static
|
|
4129
|
+
// style object, template-literal parts, ternary, generic expression)
|
|
4130
|
+
// sees it exactly as if the user had written the untagged form.
|
|
4131
|
+
expr = tryDesugarInterleaveTaggedTemplate(expr, ctx)
|
|
4132
|
+
|
|
4046
4133
|
// BF062: AwaitExpression in attribute position
|
|
4047
4134
|
if (ts.isAwaitExpression(expr)) {
|
|
4048
4135
|
ctx.analyzer.errors.push(
|
|
@@ -4281,6 +4368,22 @@ function findLocalConst(name: string, ctx: TransformContext) {
|
|
|
4281
4368
|
return pool[pool.length - 1]
|
|
4282
4369
|
}
|
|
4283
4370
|
|
|
4371
|
+
/**
|
|
4372
|
+
* Resolve a `function` declaration name with the same shadowing-aware
|
|
4373
|
+
* lookup as {@link findLocalConst} — a component-scope declaration wins
|
|
4374
|
+
* over a module-scope one of the same name, and among several
|
|
4375
|
+
* component-scope declarations the last in source order wins.
|
|
4376
|
+
*
|
|
4377
|
+
* Returns undefined when no local function matches.
|
|
4378
|
+
*/
|
|
4379
|
+
function findLocalFunction(name: string, ctx: TransformContext) {
|
|
4380
|
+
const matches = ctx.analyzer.localFunctions.filter(f => f.name === name)
|
|
4381
|
+
if (matches.length === 0) return undefined
|
|
4382
|
+
const fnScoped = matches.filter(f => !f.isModule)
|
|
4383
|
+
const pool = fnScoped.length > 0 ? fnScoped : matches
|
|
4384
|
+
return pool[pool.length - 1]
|
|
4385
|
+
}
|
|
4386
|
+
|
|
4284
4387
|
/**
|
|
4285
4388
|
* Detect a PascalCase JSX tag that is really a *dynamic tag* local
|
|
4286
4389
|
* (`const Tag = children.tag`) rather than a component reference.
|
|
@@ -4463,6 +4566,288 @@ function astText(node: ts.Node): string {
|
|
|
4463
4566
|
return node.getText(node.getSourceFile())
|
|
4464
4567
|
}
|
|
4465
4568
|
|
|
4569
|
+
/**
|
|
4570
|
+
* Re-parse a `FunctionInfo` (a `function foo(...) {...}` declaration
|
|
4571
|
+
* collected by the analyzer) into an anonymous `ts.FunctionExpression`, for
|
|
4572
|
+
* the same reason `parseConstInitializer` re-parses a const's initializer
|
|
4573
|
+
* text — the analyzer stores source text, not a live AST node tied to
|
|
4574
|
+
* `ctx.sourceFile`.
|
|
4575
|
+
*
|
|
4576
|
+
* Wraps as `const __bf_resolve_fn__ = function(params) body` (an
|
|
4577
|
+
* EXPRESSION position) rather than reparsing a standalone `function foo() {}`
|
|
4578
|
+
* statement, because `convertNode` in expression-parser.ts only recognizes
|
|
4579
|
+
* `ts.isArrowFunction` / `ts.isFunctionExpression` — not
|
|
4580
|
+
* `ts.isFunctionDeclaration`. The returned node's parameters/body are then
|
|
4581
|
+
* structurally identical to a function-expression sort comparator, so it
|
|
4582
|
+
* flows through the existing `tsNodeToParsedExpr` → `sortComparatorFromArrow`
|
|
4583
|
+
* path unchanged. Uses `typedParams`/`typedBody` when present (verbatim
|
|
4584
|
+
* source, may carry type annotations that don't affect `ts.isIdentifier(p.name)`
|
|
4585
|
+
* checks downstream) and falls back to reconstructing from `params`/`body`.
|
|
4586
|
+
*
|
|
4587
|
+
* Returns null when the source doesn't parse cleanly (e.g. no body).
|
|
4588
|
+
* Memoized per `FunctionInfo` object identity — mirrors
|
|
4589
|
+
* `constInitializerCache`.
|
|
4590
|
+
*/
|
|
4591
|
+
const functionInfoExprCache = new WeakMap<object, ts.Expression | null>()
|
|
4592
|
+
|
|
4593
|
+
function parseFunctionInfoAsExpr(fn: FunctionInfo): ts.Expression | null {
|
|
4594
|
+
const cached = functionInfoExprCache.get(fn as object)
|
|
4595
|
+
if (cached !== undefined) return cached
|
|
4596
|
+
const result = parseFunctionInfoAsExprImpl(fn)
|
|
4597
|
+
functionInfoExprCache.set(fn as object, result)
|
|
4598
|
+
return result
|
|
4599
|
+
}
|
|
4600
|
+
|
|
4601
|
+
function parseFunctionInfoAsExprImpl(fn: FunctionInfo): ts.Expression | null {
|
|
4602
|
+
if (!fn.body) return null
|
|
4603
|
+
const params = fn.typedParams !== undefined
|
|
4604
|
+
? fn.typedParams
|
|
4605
|
+
: fn.params.map(formatParamWithType).join(', ')
|
|
4606
|
+
const body = fn.typedBody ?? fn.body
|
|
4607
|
+
const wrapped = `const __bf_resolve_fn__ = function(${params}) ${body}`
|
|
4608
|
+
const sf = ts.createSourceFile(
|
|
4609
|
+
'__bf_resolve_fn.ts',
|
|
4610
|
+
wrapped,
|
|
4611
|
+
ts.ScriptTarget.Latest,
|
|
4612
|
+
/* setParentNodes */ true,
|
|
4613
|
+
ts.ScriptKind.TS,
|
|
4614
|
+
)
|
|
4615
|
+
const stmt = sf.statements[0]
|
|
4616
|
+
if (!stmt || !ts.isVariableStatement(stmt)) return null
|
|
4617
|
+
const decl = stmt.declarationList.declarations[0]
|
|
4618
|
+
if (!decl?.initializer) return null
|
|
4619
|
+
return decl.initializer
|
|
4620
|
+
}
|
|
4621
|
+
|
|
4622
|
+
// =============================================================================
|
|
4623
|
+
// Tagged-Template Interleave-Tag Desugaring (#2092, Refs #2069)
|
|
4624
|
+
// =============================================================================
|
|
4625
|
+
|
|
4626
|
+
/**
|
|
4627
|
+
* Recognize + desugar the classname-tag idiom:
|
|
4628
|
+
*
|
|
4629
|
+
* function cn(parts: TemplateStringsArray, ...args: unknown[]): string {
|
|
4630
|
+
* return parts.reduce<string>((acc, p, i) => acc + p + (args[i] ?? ''), '')
|
|
4631
|
+
* }
|
|
4632
|
+
* className={cn`base ${tone()}`}
|
|
4633
|
+
*
|
|
4634
|
+
* When `expr` is a top-level `ts.TaggedTemplateExpression` whose tag is an
|
|
4635
|
+
* identifier resolving ONE HOP through same-file scope (`findLocalConst` /
|
|
4636
|
+
* `findLocalFunction`, same shadowing preference as #2090's sort-comparator
|
|
4637
|
+
* resolution) to a function structurally proven to be an "interleave tag"
|
|
4638
|
+
* (see {@link isInterleaveTagFunction}), this rewrites the tagged template
|
|
4639
|
+
* to the equivalent UNTAGGED template literal — each span wrapped in
|
|
4640
|
+
* `(span) ?? ''` — and returns the new node. The entire existing pipeline
|
|
4641
|
+
* (dep analysis, template-literal parts, adapter emit, client-JS binding)
|
|
4642
|
+
* then processes the rewritten node exactly as if the user had written an
|
|
4643
|
+
* untagged template literal directly; no adapter changes, no new IR node.
|
|
4644
|
+
*
|
|
4645
|
+
* Returns `expr` UNCHANGED when the tag doesn't resolve, resolves to
|
|
4646
|
+
* something other than an arrow/function expression, or resolves but its
|
|
4647
|
+
* body isn't the exact interleave-reduce shape (imported tags, computed
|
|
4648
|
+
* tags like `obj.cn`, non-rest signatures, a tag that joins with `-`,
|
|
4649
|
+
* etc.) — today's opaque-leaf behavior (adapter BF101) is preserved
|
|
4650
|
+
* byte-for-byte for every other case.
|
|
4651
|
+
*
|
|
4652
|
+
* Top-level only (#2092 scope): a tagged template nested inside a ternary
|
|
4653
|
+
* or map callback is not rewritten by this call — only the two call sites
|
|
4654
|
+
* that receive an attribute value / JSX-child expression directly.
|
|
4655
|
+
*/
|
|
4656
|
+
function tryDesugarInterleaveTaggedTemplate(
|
|
4657
|
+
expr: ts.Expression,
|
|
4658
|
+
ctx: TransformContext,
|
|
4659
|
+
): ts.Expression {
|
|
4660
|
+
if (!ts.isTaggedTemplateExpression(expr)) return expr
|
|
4661
|
+
if (!ts.isIdentifier(expr.tag)) return expr
|
|
4662
|
+
|
|
4663
|
+
const resolvedTag = resolveInterleaveTagIdentifier(expr.tag.text, ctx)
|
|
4664
|
+
if (!resolvedTag) return expr
|
|
4665
|
+
if (!isInterleaveTagFunction(resolvedTag)) return expr
|
|
4666
|
+
|
|
4667
|
+
const rewritten = buildUntaggedTemplateLiteral(expr, ctx)
|
|
4668
|
+
return rewritten ?? expr
|
|
4669
|
+
}
|
|
4670
|
+
|
|
4671
|
+
/**
|
|
4672
|
+
* Resolve a bare-identifier tag reference one hop through same-file scope
|
|
4673
|
+
* — a `const cn = (parts, ...args) => …` or `function cn(parts, ...args)
|
|
4674
|
+
* {…}` — reusing the exact `findLocalConst` / `findLocalFunction` lookup
|
|
4675
|
+
* (and its shadowing preference) that #2090 established for sort
|
|
4676
|
+
* comparators. Alias chains and imported/prop identifiers are NOT
|
|
4677
|
+
* followed, matching that precedent. Returns null when nothing resolves.
|
|
4678
|
+
*/
|
|
4679
|
+
function resolveInterleaveTagIdentifier(name: string, ctx: TransformContext): ts.Expression | null {
|
|
4680
|
+
const constInfo = findLocalConst(name, ctx)
|
|
4681
|
+
const fnInfo = findLocalFunction(name, ctx)
|
|
4682
|
+
// A name bound BOTH as a const and as a `function` declaration is the
|
|
4683
|
+
// same cross-kind ambiguity `resolveSortComparatorIdentifier` refuses:
|
|
4684
|
+
// it can only occur across scopes, and `FunctionInfo.isModule` reflects
|
|
4685
|
+
// emission placement rather than lexical position, so picking either
|
|
4686
|
+
// binding could desugar a tag the call site can't actually see. Refuse —
|
|
4687
|
+
// the node stays opaque and keeps today's adapter BF101 (Copilot review
|
|
4688
|
+
// on #2093).
|
|
4689
|
+
if (constInfo && fnInfo) return null
|
|
4690
|
+
if (constInfo) {
|
|
4691
|
+
const ast = parseConstInitializer(constInfo)
|
|
4692
|
+
return ast && (ts.isArrowFunction(ast) || ts.isFunctionExpression(ast)) ? ast : null
|
|
4693
|
+
}
|
|
4694
|
+
if (fnInfo) {
|
|
4695
|
+
const ast = parseFunctionInfoAsExpr(fnInfo)
|
|
4696
|
+
return ast && (ts.isArrowFunction(ast) || ts.isFunctionExpression(ast)) ? ast : null
|
|
4697
|
+
}
|
|
4698
|
+
return null
|
|
4699
|
+
}
|
|
4700
|
+
|
|
4701
|
+
/**
|
|
4702
|
+
* Structural catalogue gate for an "interleave tag" — the classname-cn
|
|
4703
|
+
* idiom's tag function. Kept tight per #2092: a resolved tag that doesn't
|
|
4704
|
+
* match EXACTLY this shape is left unrecognized (caller leaves the node
|
|
4705
|
+
* untouched).
|
|
4706
|
+
*
|
|
4707
|
+
* Signature: exactly 2 params, first a plain (non-rest) identifier
|
|
4708
|
+
* (`parts`), second a REST identifier param (`...args`). Type annotations
|
|
4709
|
+
* are irrelevant — only the parsed structure is matched.
|
|
4710
|
+
*
|
|
4711
|
+
* Body (after the existing block-body → single-`return` folding that
|
|
4712
|
+
* `tsNodeToParsedExpr` already performs):
|
|
4713
|
+
*
|
|
4714
|
+
* parts.reduce((acc, p, i) => acc + p + (args[i] ?? ''), '')
|
|
4715
|
+
*
|
|
4716
|
+
* — receiver is the first param; the reduce callback has 3 plain
|
|
4717
|
+
* identifier params; the callback body is a left-assoc `+` chain
|
|
4718
|
+
* `(acc + p) + X` (any parenthesization — `tsNodeToParsedExpr` already
|
|
4719
|
+
* discards parens) with `acc`/`p` the callback's first two params in
|
|
4720
|
+
* that order; `X` is `args[i] ?? ''`, optionally wrapped in `String(...)`,
|
|
4721
|
+
* with `args` the rest param and `i` the callback's third param; the
|
|
4722
|
+
* reduce init arg is the empty string literal.
|
|
4723
|
+
*/
|
|
4724
|
+
function isInterleaveTagFunction(fn: ts.Expression): boolean {
|
|
4725
|
+
if (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn)) return false
|
|
4726
|
+
if (fn.parameters.length !== 2) return false
|
|
4727
|
+
const [partsParam, argsParam] = fn.parameters
|
|
4728
|
+
if (!ts.isIdentifier(partsParam.name) || partsParam.dotDotDotToken) return false
|
|
4729
|
+
if (!ts.isIdentifier(argsParam.name) || !argsParam.dotDotDotToken) return false
|
|
4730
|
+
|
|
4731
|
+
const parsed = tsNodeToParsedExpr(fn)
|
|
4732
|
+
if (parsed.kind !== 'arrow') return false
|
|
4733
|
+
return isInterleaveReduceCall(parsed.body, partsParam.name.text, argsParam.name.text)
|
|
4734
|
+
}
|
|
4735
|
+
|
|
4736
|
+
/** Match `<partsName>.reduce((acc, p, i) => …, '')`. */
|
|
4737
|
+
function isInterleaveReduceCall(body: ParsedExpr, partsName: string, argsName: string): boolean {
|
|
4738
|
+
if (body.kind !== 'call' || body.args.length !== 2) return false
|
|
4739
|
+
const { callee, args } = body
|
|
4740
|
+
if (callee.kind !== 'member' || callee.computed || callee.property !== 'reduce') return false
|
|
4741
|
+
if (callee.object.kind !== 'identifier' || callee.object.name !== partsName) return false
|
|
4742
|
+
|
|
4743
|
+
const [callback, init] = args
|
|
4744
|
+
if (init.kind !== 'literal' || init.literalType !== 'string' || init.value !== '') return false
|
|
4745
|
+
if (callback.kind !== 'arrow' || callback.params.length !== 3) return false
|
|
4746
|
+
|
|
4747
|
+
const [acc, p, i] = callback.params
|
|
4748
|
+
return isInterleaveReduceCallbackBody(callback.body, acc, p, i, argsName)
|
|
4749
|
+
}
|
|
4750
|
+
|
|
4751
|
+
/** Match `(acc + p) + X` where X is the per-span interleave expression. */
|
|
4752
|
+
function isInterleaveReduceCallbackBody(
|
|
4753
|
+
body: ParsedExpr,
|
|
4754
|
+
acc: string,
|
|
4755
|
+
p: string,
|
|
4756
|
+
i: string,
|
|
4757
|
+
argsName: string,
|
|
4758
|
+
): boolean {
|
|
4759
|
+
if (body.kind !== 'binary' || body.op !== '+') return false
|
|
4760
|
+
const { left, right } = body
|
|
4761
|
+
if (left.kind !== 'binary' || left.op !== '+') return false
|
|
4762
|
+
if (left.left.kind !== 'identifier' || left.left.name !== acc) return false
|
|
4763
|
+
if (left.right.kind !== 'identifier' || left.right.name !== p) return false
|
|
4764
|
+
return isInterleaveSpanExpr(right, i, argsName)
|
|
4765
|
+
}
|
|
4766
|
+
|
|
4767
|
+
/** Match `args[i] ?? ''`, optionally wrapped in `String(...)`. */
|
|
4768
|
+
function isInterleaveSpanExpr(expr: ParsedExpr, i: string, argsName: string): boolean {
|
|
4769
|
+
let inner = expr
|
|
4770
|
+
if (
|
|
4771
|
+
inner.kind === 'call' &&
|
|
4772
|
+
inner.args.length === 1 &&
|
|
4773
|
+
inner.callee.kind === 'identifier' &&
|
|
4774
|
+
inner.callee.name === 'String'
|
|
4775
|
+
) {
|
|
4776
|
+
inner = inner.args[0]
|
|
4777
|
+
}
|
|
4778
|
+
if (inner.kind !== 'logical' || inner.op !== '??') return false
|
|
4779
|
+
if (inner.right.kind !== 'literal' || inner.right.literalType !== 'string' || inner.right.value !== '') {
|
|
4780
|
+
return false
|
|
4781
|
+
}
|
|
4782
|
+
const idx = inner.left
|
|
4783
|
+
if (idx.kind !== 'index-access') return false
|
|
4784
|
+
if (idx.object.kind !== 'identifier' || idx.object.name !== argsName) return false
|
|
4785
|
+
if (idx.index.kind !== 'identifier' || idx.index.name !== i) return false
|
|
4786
|
+
return true
|
|
4787
|
+
}
|
|
4788
|
+
|
|
4789
|
+
/**
|
|
4790
|
+
* Build the untagged template literal equivalent to a recognized
|
|
4791
|
+
* interleave-tag call: `cn\`base ${tone()}\`` → `` `base ${(tone()) ?? ''}` ``.
|
|
4792
|
+
*
|
|
4793
|
+
* Each literal chunk uses its RAW source text (`rawText`, falling back to
|
|
4794
|
+
* the cooked `text` only if `rawText` is unexpectedly absent) so escapes
|
|
4795
|
+
* (a literal backtick, `${`, or backslash inside a chunk) survive
|
|
4796
|
+
* verbatim — pasting the COOKED text back into new template source would
|
|
4797
|
+
* mis-parse or silently change meaning. Each span expression uses
|
|
4798
|
+
* `ctx.getJS` (type-stripped, matching every other raw-text extraction in
|
|
4799
|
+
* this file) wrapped as `${(<expr>) ?? ''}`.
|
|
4800
|
+
*
|
|
4801
|
+
* The assembled text is re-parsed the same way `parseConstInitializer`
|
|
4802
|
+
* re-parses a const initializer (wrapped in `const __bf_… = (…)` so the
|
|
4803
|
+
* result lands in expression position). Returns null if the reparse
|
|
4804
|
+
* doesn't produce a clean template-literal expression (defensive; should
|
|
4805
|
+
* not happen for well-formed input).
|
|
4806
|
+
*/
|
|
4807
|
+
function buildUntaggedTemplateLiteral(
|
|
4808
|
+
node: ts.TaggedTemplateExpression,
|
|
4809
|
+
ctx: TransformContext,
|
|
4810
|
+
): ts.Expression | null {
|
|
4811
|
+
const template = node.template
|
|
4812
|
+
|
|
4813
|
+
let text: string
|
|
4814
|
+
if (ts.isNoSubstitutionTemplateLiteral(template)) {
|
|
4815
|
+
text = '`' + (template.rawText ?? template.text) + '`'
|
|
4816
|
+
} else {
|
|
4817
|
+
let body = template.head.rawText ?? template.head.text
|
|
4818
|
+
for (const span of template.templateSpans) {
|
|
4819
|
+
const spanText = ctx.getJS(span.expression)
|
|
4820
|
+
body += '${(' + spanText + ') ?? \'\'}'
|
|
4821
|
+
body += span.literal.rawText ?? span.literal.text
|
|
4822
|
+
}
|
|
4823
|
+
text = '`' + body + '`'
|
|
4824
|
+
}
|
|
4825
|
+
|
|
4826
|
+
const wrapped = `const __bf_resolve_tagged__ = (${text})`
|
|
4827
|
+
// ScriptKind.TSX (unlike the const/function-resolution re-parses above,
|
|
4828
|
+
// which parse comparator/tag FUNCTION BODIES): the span expressions are
|
|
4829
|
+
// verbatim attribute-position text from a .tsx component, so they were
|
|
4830
|
+
// originally parsed under TSX rules — re-parsing them as plain TS can
|
|
4831
|
+
// mis-parse or reject valid TSX span syntax and silently skip the
|
|
4832
|
+
// rewrite (Copilot review on #2093).
|
|
4833
|
+
const sf = ts.createSourceFile(
|
|
4834
|
+
'__bf_resolve_tagged.tsx',
|
|
4835
|
+
wrapped,
|
|
4836
|
+
ts.ScriptTarget.Latest,
|
|
4837
|
+
/* setParentNodes */ true,
|
|
4838
|
+
ts.ScriptKind.TSX,
|
|
4839
|
+
)
|
|
4840
|
+
const stmt = sf.statements[0]
|
|
4841
|
+
if (!stmt || !ts.isVariableStatement(stmt)) return null
|
|
4842
|
+
const decl = stmt.declarationList.declarations[0]
|
|
4843
|
+
if (!decl?.initializer) return null
|
|
4844
|
+
const result = ts.isParenthesizedExpression(decl.initializer)
|
|
4845
|
+
? decl.initializer.expression
|
|
4846
|
+
: decl.initializer
|
|
4847
|
+
if (!ts.isTemplateExpression(result) && !ts.isNoSubstitutionTemplateLiteral(result)) return null
|
|
4848
|
+
return result
|
|
4849
|
+
}
|
|
4850
|
+
|
|
4466
4851
|
/**
|
|
4467
4852
|
* Parse a conditional (ternary) expression into structured form.
|
|
4468
4853
|
* Only parses simple ternaries with string literal branches.
|