@barefootjs/jsx 0.18.5 → 0.19.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 +77 -0
- package/dist/adapters/dangerous-inner-html.d.ts.map +1 -0
- package/dist/adapters/loop-bound-names.d.ts +22 -0
- package/dist/adapters/loop-bound-names.d.ts.map +1 -0
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +8 -0
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +15 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1002 -284
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts.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-child-arm.d.ts +9 -0
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.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/shared.d.ts +45 -3
- package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts +10 -0
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/skeleton-paths.d.ts +38 -0
- package/dist/ir-to-client-js/control-flow/stringify/skeleton-paths.d.ts.map +1 -0
- 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/csr-substitute.d.ts +23 -0
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +42 -0
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/phases/props-extraction.d.ts +0 -1
- package/dist/ir-to-client-js/phases/props-extraction.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/static-array-child-init.d.ts +20 -1
- package/dist/ir-to-client-js/plan/static-array-child-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/stringify/static-array-child-init.d.ts +1 -1
- package/dist/ir-to-client-js/types.d.ts +21 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/signal-init-eval.d.ts +82 -0
- package/dist/signal-init-eval.d.ts.map +1 -0
- package/dist/static-literal.d.ts +45 -0
- package/dist/static-literal.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +19 -13
- package/src/__tests__/client-js-generation.test.ts +40 -0
- package/src/__tests__/create-selector.test.ts +110 -0
- package/src/__tests__/csr-template-loop-shadowing.test.ts +214 -0
- package/src/__tests__/dangerous-inner-html-resolver.test.ts +197 -0
- package/src/__tests__/inner-loop-svg-namespace.test.ts +245 -0
- package/src/__tests__/loop-bound-names.test.ts +102 -0
- package/src/__tests__/loop-plan-classification.test.ts +2 -0
- package/src/__tests__/map-function-reference.test.ts +303 -0
- package/src/__tests__/nested-loop-index-param.test.ts +285 -0
- package/src/__tests__/props-destructuring.test.ts +61 -8
- package/src/__tests__/signal-init-eval.test.ts +138 -0
- package/src/__tests__/static-array-inner-loop-index-param.test.ts +180 -0
- package/src/__tests__/static-literal.test.ts +126 -0
- package/src/__tests__/string-concat-identifier.test.ts +55 -0
- package/src/adapters/dangerous-inner-html.ts +186 -0
- package/src/adapters/loop-bound-names.ts +80 -0
- package/src/adapters/parsed-expr-emitter.ts +9 -0
- package/src/analyzer.ts +36 -22
- package/src/expression-parser.ts +33 -0
- package/src/index.ts +20 -3
- package/src/ir-to-client-js/collect-elements.ts +15 -3
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +21 -4
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +13 -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-child-arm.ts +9 -0
- package/src/ir-to-client-js/control-flow/plan/loop.ts +9 -0
- package/src/ir-to-client-js/control-flow/shared.ts +103 -6
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +11 -2
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +5 -0
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +53 -10
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +25 -4
- package/src/ir-to-client-js/control-flow/stringify/skeleton-paths.ts +70 -0
- package/src/ir-to-client-js/control-flow/stringify/template-parse.ts +25 -1
- package/src/ir-to-client-js/csr-substitute.ts +53 -0
- package/src/ir-to-client-js/html-template.ts +235 -2
- package/src/ir-to-client-js/imports.ts +1 -0
- package/src/ir-to-client-js/phases/props-extraction.ts +5 -9
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +12 -1
- package/src/ir-to-client-js/plan/static-array-child-init.ts +20 -1
- package/src/ir-to-client-js/reactivity.ts +6 -0
- package/src/ir-to-client-js/stringify/static-array-child-init.ts +11 -6
- package/src/ir-to-client-js/types.ts +21 -0
- package/src/jsx-to-ir.ts +230 -39
- package/src/signal-init-eval.ts +165 -0
- package/src/static-literal.ts +128 -0
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
ImportInfo,
|
|
23
23
|
} from '../types.ts'
|
|
24
24
|
import type { CsrInlinabilityMap } from './csr-substitute.ts'
|
|
25
|
+
import type { SkeletonSlotPaths } from './html-template.ts'
|
|
25
26
|
|
|
26
27
|
export interface ClientJsContext {
|
|
27
28
|
componentName: string
|
|
@@ -404,6 +405,18 @@ export type ConditionalBranchConditional = ConditionalElement
|
|
|
404
405
|
export interface NestedLoop extends LoopCore {
|
|
405
406
|
kind: 'nested'
|
|
406
407
|
depth: number // 1 for first nesting level, 2 for second, etc.
|
|
408
|
+
/**
|
|
409
|
+
* Index parameter of the inner `.map()` callback (e.g. `i` from
|
|
410
|
+
* `.map((item, i) => ...)`), or `null` when the callback doesn't declare
|
|
411
|
+
* one. Mirrors `TopLevelLoop.index` / `BranchLoop.index` — added by #2218
|
|
412
|
+
* so a nested loop's `key`/reactive-text/reactive-attr/template/event/ref
|
|
413
|
+
* expressions that reference the index can be bound instead of throwing
|
|
414
|
+
* `ReferenceError` at runtime. The renderItem body only receives the
|
|
415
|
+
* index as a synthetic `__innerIdx<uid>` positional param (to avoid
|
|
416
|
+
* cross-depth name collisions), so emitters must alias it under this
|
|
417
|
+
* name when referenced — see `nestedLoopReferencesIndex` in `shared.ts`.
|
|
418
|
+
*/
|
|
419
|
+
index: string | null
|
|
407
420
|
containerSlotId: string | null // Slot ID of the parent element containing the loop (for hydration)
|
|
408
421
|
/** HTML template for a single inner loop item (for mapArray CSR rendering) */
|
|
409
422
|
template?: string
|
|
@@ -529,6 +542,14 @@ export interface TopLevelLoop extends LoopCore {
|
|
|
529
542
|
* proven covered by an effect).
|
|
530
543
|
*/
|
|
531
544
|
skeletonTemplate?: string
|
|
545
|
+
/**
|
|
546
|
+
* Compile-time child-index paths for `skeletonTemplate`'s dynamic slots
|
|
547
|
+
* (perf, #2143), computed by `computeSkeletonSlotPaths` from the same IR
|
|
548
|
+
* tree. `undefined` means "resolve every slot via `qsa`/`$t` at runtime"
|
|
549
|
+
* (skeletonTemplate is still hoisted/cloned — only the runtime lookups on
|
|
550
|
+
* top of the fresh clone become the fallback rather than the path chain).
|
|
551
|
+
*/
|
|
552
|
+
skeletonPaths?: SkeletonSlotPaths
|
|
532
553
|
childEventHandlers: string[] // Bare-identifier event handler names (for the reachability graph)
|
|
533
554
|
childComponent?: IRLoopChildComponent // For createComponent-based rendering
|
|
534
555
|
nestedComponents?: IRLoopChildComponent[] // For nested components in loop bodies
|
package/src/jsx-to-ir.ts
CHANGED
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
AttrValueOf,
|
|
35
35
|
} from './types.ts'
|
|
36
36
|
import { type AnalyzerContext, type MultiReturnJsxInfo, getSourceLocation, collectReactiveGetterNames } from './analyzer-context.ts'
|
|
37
|
-
import { parseExpression, isSupported, parseBlockBody, foldBlockToExpr, predicateTernaryToLogical, tsNodeToParsedExpr, sortComparatorFromArrow, stringifyParsedExpr, cssKebabCase, type ParsedExpr } from './expression-parser.ts'
|
|
37
|
+
import { parseExpression, isSupported, parseBlockBody, foldBlockToExpr, predicateTernaryToLogical, tsNodeToParsedExpr, sortComparatorFromArrow, stringifyParsedExpr, cssKebabCase, CALLBACK_METHODS, type ParsedExpr } from './expression-parser.ts'
|
|
38
38
|
import type { IRLoopSort, FunctionInfo } from './types.ts'
|
|
39
39
|
import { formatParamWithType } from './module-exports.ts'
|
|
40
40
|
import { createError, ErrorCodes, internalInvariant } from './errors.ts'
|
|
@@ -291,8 +291,21 @@ function exprHasFunctionCalls(expr: ts.Expression): boolean {
|
|
|
291
291
|
* Returns undefined if no rewriting is needed (SolidJS-style or no props).
|
|
292
292
|
*/
|
|
293
293
|
function rewriteBarePropRefs(text: string, expr: ts.Node, ctx: TransformContext): string | undefined {
|
|
294
|
-
|
|
294
|
+
let propNames = getDestructuredPropNames(ctx)
|
|
295
295
|
if (!propNames) return undefined
|
|
296
|
+
// #2222: a name bound as an enclosing loop callback's item/index param
|
|
297
|
+
// refers to the loop binding, not the prop, at THIS transform position —
|
|
298
|
+
// `ctx.loopParams` is the live loop-param set (destructured binding
|
|
299
|
+
// names and the index included), maintained by `transformMapCall` as it
|
|
300
|
+
// enters/leaves each callback, so this guard is scope-accurate rather
|
|
301
|
+
// than the coarse whole-component exclusion the SSR adapters use
|
|
302
|
+
// (#2221). Filter into a fresh set — `getDestructuredPropNames` caches
|
|
303
|
+
// its set on ctx and must not be mutated.
|
|
304
|
+
if (ctx.loopParams.size > 0) {
|
|
305
|
+
const filtered = new Set([...propNames].filter(n => !ctx.loopParams.has(n)))
|
|
306
|
+
if (filtered.size === 0) return undefined
|
|
307
|
+
propNames = filtered
|
|
308
|
+
}
|
|
296
309
|
// #1425: union any prop refs that `expr` reaches via branch-local
|
|
297
310
|
// text substitution. The AST walk in `rewriteBarePropRefsCore`
|
|
298
311
|
// sees only `expr`'s original AST, so a prop ref introduced via
|
|
@@ -591,20 +604,34 @@ function parseValueExpr(trimmed: string): ParsedExpr {
|
|
|
591
604
|
return parseExpression(trimmed.startsWith('{') ? `(${trimmed})` : trimmed)
|
|
592
605
|
}
|
|
593
606
|
|
|
607
|
+
/** Shared empty-set default for `attachParsedExpressions`/`resolveCallbackMethodFunctionReferences`'s `bound` param — avoids a fresh allocation per call when nothing is shadowed. */
|
|
608
|
+
const EMPTY_BOUND: ReadonlySet<string> = new Set()
|
|
609
|
+
|
|
594
610
|
/**
|
|
595
611
|
* Attach `parsed` (`parseExpression(expr.trim())`) to every `expression` node
|
|
596
612
|
* in the tree, so SSR adapters emit from the structured tree instead of each
|
|
597
613
|
* re-parsing the string at emit time. Best-effort: a node this walk misses (or
|
|
598
614
|
* an empty `expr`) simply has no `parsed`, and the adapter falls back to
|
|
599
615
|
* parsing — so under-coverage is safe, never a behavioural change.
|
|
616
|
+
*
|
|
617
|
+
* Every parse is run through {@link resolveCallbackMethodFunctionReferences}
|
|
618
|
+
* (#2206) so a bare-identifier `.map`/`.filter`/… callback (`tags.map(format)`)
|
|
619
|
+
* resolves to its declaration wherever it can appear — text/attr/prop/provider
|
|
620
|
+
* expressions and the loop-array expression alike — not just one hardcoded
|
|
621
|
+
* spot. `bound` carries the enclosing loop(s)' `param`/`index` names down
|
|
622
|
+
* into that resolution (Fable review, #2214) so a loop item variable that
|
|
623
|
+
* happens to share a name with a module-scope const/function shadows it,
|
|
624
|
+
* same as real JS scoping — resolution stays off within that loop's body.
|
|
600
625
|
*/
|
|
601
|
-
function attachParsedExpressions(node: IRNode): void {
|
|
626
|
+
function attachParsedExpressions(node: IRNode, analyzer: AnalyzerContext, bound: ReadonlySet<string> = EMPTY_BOUND): void {
|
|
627
|
+
const parse = (trimmed: string) => resolveCallbackMethodFunctionReferences(parseExpression(trimmed), analyzer, bound)
|
|
628
|
+
const parseValue = (trimmed: string) => resolveCallbackMethodFunctionReferences(parseValueExpr(trimmed), analyzer, bound)
|
|
602
629
|
if (node.type === 'expression') {
|
|
603
630
|
const trimmed = node.expr.trim()
|
|
604
|
-
if (trimmed) node.parsed =
|
|
631
|
+
if (trimmed) node.parsed = parse(trimmed)
|
|
605
632
|
} else if (node.type === 'conditional' || node.type === 'if-statement') {
|
|
606
633
|
const trimmed = node.condition.trim()
|
|
607
|
-
if (trimmed) node.parsedCondition =
|
|
634
|
+
if (trimmed) node.parsedCondition = parse(trimmed)
|
|
608
635
|
}
|
|
609
636
|
// Attach `parsed` to every expression-valued attribute / prop so adapters can
|
|
610
637
|
// lower from the tree instead of re-parsing the string. Element attrs,
|
|
@@ -615,23 +642,23 @@ function attachParsedExpressions(node: IRNode): void {
|
|
|
615
642
|
for (const attr of node.attrs) {
|
|
616
643
|
if (attr.value.kind === 'expression') {
|
|
617
644
|
const trimmed = attr.value.expr.trim()
|
|
618
|
-
if (trimmed) attr.value.parsed =
|
|
645
|
+
if (trimmed) attr.value.parsed = parseValue(trimmed)
|
|
619
646
|
} else if (attr.value.kind === 'spread') {
|
|
620
647
|
const trimmed = attr.value.expr.trim()
|
|
621
|
-
if (trimmed) attr.value.parsed =
|
|
648
|
+
if (trimmed) attr.value.parsed = parse(trimmed)
|
|
622
649
|
}
|
|
623
650
|
}
|
|
624
651
|
} else if (node.type === 'component') {
|
|
625
652
|
for (const prop of node.props) {
|
|
626
653
|
if (prop.value.kind === 'expression') {
|
|
627
654
|
const trimmed = prop.value.expr.trim()
|
|
628
|
-
if (trimmed) prop.value.parsed =
|
|
655
|
+
if (trimmed) prop.value.parsed = parseValue(trimmed)
|
|
629
656
|
}
|
|
630
657
|
}
|
|
631
658
|
} else if (node.type === 'provider') {
|
|
632
659
|
if (node.valueProp.value.kind === 'expression') {
|
|
633
660
|
const trimmed = node.valueProp.value.expr.trim()
|
|
634
|
-
if (trimmed) node.valueProp.value.parsed =
|
|
661
|
+
if (trimmed) node.valueProp.value.parsed = parseValue(trimmed)
|
|
635
662
|
}
|
|
636
663
|
}
|
|
637
664
|
switch (node.type) {
|
|
@@ -639,46 +666,54 @@ function attachParsedExpressions(node: IRNode): void {
|
|
|
639
666
|
case 'component':
|
|
640
667
|
case 'fragment':
|
|
641
668
|
case 'provider':
|
|
642
|
-
for (const child of node.children) attachParsedExpressions(child)
|
|
669
|
+
for (const child of node.children) attachParsedExpressions(child, analyzer, bound)
|
|
643
670
|
break
|
|
644
671
|
case 'async':
|
|
645
|
-
attachParsedExpressions(node.fallback)
|
|
646
|
-
for (const child of node.children) attachParsedExpressions(child)
|
|
672
|
+
attachParsedExpressions(node.fallback, analyzer, bound)
|
|
673
|
+
for (const child of node.children) attachParsedExpressions(child, analyzer, bound)
|
|
647
674
|
break
|
|
648
675
|
case 'loop': {
|
|
649
676
|
// Attach the parse of the SAME `array` string the adapters consume
|
|
650
677
|
// (the Go adapter's scalar-literal loop typing reads `loop.array` /
|
|
651
678
|
// `nested.loopArray`, which is exactly `loop.array`), so it can read
|
|
652
|
-
// the tree instead of re-parsing with `ts.createSourceFile`.
|
|
679
|
+
// the tree instead of re-parsing with `ts.createSourceFile`. The array
|
|
680
|
+
// expression itself is evaluated in the OUTER scope (before the loop's
|
|
681
|
+
// own `param`/`index` exist), so it parses against the unextended `bound`.
|
|
653
682
|
const trimmedArray = node.array.trim()
|
|
654
|
-
if (trimmedArray) node.arrayParsed =
|
|
655
|
-
|
|
683
|
+
if (trimmedArray) node.arrayParsed = parse(trimmedArray)
|
|
684
|
+
// The loop's item/index variables shadow any same-named module-scope
|
|
685
|
+
// const/function for everything inside the loop body (Fable review,
|
|
686
|
+
// #2214) — mirrors an arrow's own params in `resolveCallbackMethodFunctionReferences`.
|
|
687
|
+
const loopBound = new Set(bound)
|
|
688
|
+
loopBound.add(node.param)
|
|
689
|
+
if (node.index) loopBound.add(node.index)
|
|
690
|
+
for (const child of node.children) attachParsedExpressions(child, analyzer, loopBound)
|
|
656
691
|
// Loops also hold expression nodes off the main `children` array.
|
|
657
692
|
if (node.childComponent) {
|
|
658
|
-
for (const child of node.childComponent.children) attachParsedExpressions(child)
|
|
693
|
+
for (const child of node.childComponent.children) attachParsedExpressions(child, analyzer, loopBound)
|
|
659
694
|
}
|
|
660
695
|
for (const nested of node.nestedComponents ?? []) {
|
|
661
|
-
for (const child of nested.children) attachParsedExpressions(child)
|
|
696
|
+
for (const child of nested.children) attachParsedExpressions(child, analyzer, loopBound)
|
|
662
697
|
}
|
|
663
698
|
for (const frag of node.flatMapCallback?.fragments ?? []) {
|
|
664
|
-
attachParsedExpressions(frag.ir)
|
|
699
|
+
attachParsedExpressions(frag.ir, analyzer, loopBound)
|
|
665
700
|
}
|
|
666
701
|
break
|
|
667
702
|
}
|
|
668
703
|
case 'conditional':
|
|
669
|
-
attachParsedExpressions(node.whenTrue)
|
|
670
|
-
attachParsedExpressions(node.whenFalse)
|
|
704
|
+
attachParsedExpressions(node.whenTrue, analyzer, bound)
|
|
705
|
+
attachParsedExpressions(node.whenFalse, analyzer, bound)
|
|
671
706
|
break
|
|
672
707
|
case 'if-statement':
|
|
673
|
-
attachParsedExpressions(node.consequent)
|
|
674
|
-
if (node.alternate) attachParsedExpressions(node.alternate)
|
|
708
|
+
attachParsedExpressions(node.consequent, analyzer, bound)
|
|
709
|
+
if (node.alternate) attachParsedExpressions(node.alternate, analyzer, bound)
|
|
675
710
|
break
|
|
676
711
|
}
|
|
677
712
|
}
|
|
678
713
|
|
|
679
714
|
export function jsxToIR(analyzer: AnalyzerContext): IRNode | null {
|
|
680
715
|
const root = buildIRRoot(analyzer)
|
|
681
|
-
if (root) attachParsedExpressions(root)
|
|
716
|
+
if (root) attachParsedExpressions(root, analyzer)
|
|
682
717
|
return root
|
|
683
718
|
}
|
|
684
719
|
|
|
@@ -2632,8 +2667,8 @@ function extractSortComparator(
|
|
|
2632
2667
|
* vs. unresolved) is decided by the caller, not here.
|
|
2633
2668
|
*/
|
|
2634
2669
|
function resolveSortComparatorIdentifier(name: string, ctx: TransformContext): ts.Expression | null {
|
|
2635
|
-
const constInfo = findLocalConst(name, ctx)
|
|
2636
|
-
const fnInfo = findLocalFunction(name, ctx)
|
|
2670
|
+
const constInfo = findLocalConst(name, ctx.analyzer)
|
|
2671
|
+
const fnInfo = findLocalFunction(name, ctx.analyzer)
|
|
2637
2672
|
if (constInfo && fnInfo) return null
|
|
2638
2673
|
if (constInfo) {
|
|
2639
2674
|
const ast = parseConstInitializer(constInfo)
|
|
@@ -2646,6 +2681,149 @@ function resolveSortComparatorIdentifier(name: string, ctx: TransformContext): t
|
|
|
2646
2681
|
return null
|
|
2647
2682
|
}
|
|
2648
2683
|
|
|
2684
|
+
/**
|
|
2685
|
+
* Resolve a bare-identifier callback passed to a value-position higher-order
|
|
2686
|
+
* array method (`tags.map(format)`, `.filter`, `.sort`, … — any name in
|
|
2687
|
+
* {@link CALLBACK_METHODS}, #2206) to its underlying arrow / function-
|
|
2688
|
+
* expression node — the SAME one-hop, ambiguity-refuses lookup #2090
|
|
2689
|
+
* established for `.sort(fnref)` comparators (`resolveSortComparatorIdentifier`
|
|
2690
|
+
* above). Kept as its own small function (mirroring the existing
|
|
2691
|
+
* `resolveInterleaveTagIdentifier` precedent below) rather than sharing code
|
|
2692
|
+
* with the sort resolver, so each call site's refusal semantics stay
|
|
2693
|
+
* independently reviewable.
|
|
2694
|
+
*
|
|
2695
|
+
* Only needs `AnalyzerContext` (not a full `TransformContext`) because its
|
|
2696
|
+
* caller, {@link resolveCallbackMethodFunctionReferences}, runs as a
|
|
2697
|
+
* post-parse pass over an already-built `ParsedExpr` tree — no live
|
|
2698
|
+
* `ts.Node`/source-file binding is available or needed, since
|
|
2699
|
+
* `parseConstInitializer` / `parseFunctionInfoAsExpr` re-parse the
|
|
2700
|
+
* analyzer's stored declaration TEXT into a fresh, isolated source file.
|
|
2701
|
+
*/
|
|
2702
|
+
function resolveCallbackMethodFunctionReferenceIdentifier(name: string, analyzer: AnalyzerContext): ts.Expression | null {
|
|
2703
|
+
const constInfo = findLocalConst(name, analyzer)
|
|
2704
|
+
const fnInfo = findLocalFunction(name, analyzer)
|
|
2705
|
+
if (constInfo && fnInfo) return null
|
|
2706
|
+
if (constInfo) {
|
|
2707
|
+
const ast = parseConstInitializer(constInfo)
|
|
2708
|
+
return ast && (ts.isArrowFunction(ast) || ts.isFunctionExpression(ast)) ? ast : null
|
|
2709
|
+
}
|
|
2710
|
+
if (fnInfo) {
|
|
2711
|
+
const ast = parseFunctionInfoAsExpr(fnInfo)
|
|
2712
|
+
return ast && (ts.isArrowFunction(ast) || ts.isFunctionExpression(ast)) ? ast : null
|
|
2713
|
+
}
|
|
2714
|
+
return null
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
/**
|
|
2718
|
+
* Post-parse pass (#2206): walk a `ParsedExpr` tree and resolve every
|
|
2719
|
+
* `<recv>.<CALLBACK_METHODS method>(<bare identifier>, …rest)` call's
|
|
2720
|
+
* callback argument to its declaration, so `tags.map(format).join(' ')`
|
|
2721
|
+
* compiles exactly as if `format`'s body had been written inline
|
|
2722
|
+
* (`tags.map(t => '#' + t).join(' ')`) — the shape `asCallbackMethodCall`
|
|
2723
|
+
* (expression-parser.ts) already recognizes and every adapter's `map_eval`
|
|
2724
|
+
* (#2073) already lowers.
|
|
2725
|
+
*
|
|
2726
|
+
* Runs AFTER `parseExpression`, not threaded into it, because the
|
|
2727
|
+
* value-returning `.map(cb)` form is recognized generically downstream of
|
|
2728
|
+
* parsing (`asCallbackMethodCall`'s `arrow.kind !== 'arrow'` check), not
|
|
2729
|
+
* during it — `convertNode` already happily produces a generic `call` node
|
|
2730
|
+
* with an `identifier` arg for `tags.map(format)` with no special-casing
|
|
2731
|
+
* needed there. An unresolvable reference (import, non-function const,
|
|
2732
|
+
* alias chain, cross-kind ambiguity — see
|
|
2733
|
+
* {@link resolveCallbackMethodFunctionReferenceIdentifier}) or a resolved
|
|
2734
|
+
* body that can't fold to a single expression is left untouched: the
|
|
2735
|
+
* `identifier` arg passes through as-is, and `asCallbackMethodCall`'s
|
|
2736
|
+
* existing arrow-kind check still refuses it with the current BF101,
|
|
2737
|
+
* unchanged.
|
|
2738
|
+
*
|
|
2739
|
+
* A spliced-in resolved arrow's OWN body is not re-walked (`visit` returns
|
|
2740
|
+
* it as-is rather than recursing) — a bare-identifier callback declared
|
|
2741
|
+
* INSIDE another resolved declaration (`const outer = xs => xs.map(inner).join(',')`
|
|
2742
|
+
* used as `rows.map(outer)`) stays unresolved and BF101-refuses, same as
|
|
2743
|
+
* before #2206. This is a deliberate one-hop limit, not an oversight: naively
|
|
2744
|
+
* recursing risks an infinite loop on a self-referential declaration
|
|
2745
|
+
* (`const f = xs => xs.map(f)`), which would need a resolution stack to
|
|
2746
|
+
* detect — out of scope here. Fails safe (refuses loudly rather than
|
|
2747
|
+
* mis-rendering); see the pinned-refusal test in map-function-reference.test.ts.
|
|
2748
|
+
*
|
|
2749
|
+
|
|
2750
|
+
* `bound` tracks names that are lexically shadowed at the current walk
|
|
2751
|
+
* position — an enclosing arrow's own params (`rows.map(fn => fn.tags.map(fn)…)`
|
|
2752
|
+
* — so a bare identifier that refers to a PARAMETER rather than a same-file
|
|
2753
|
+
* const/function is never resolved against the const/function tables
|
|
2754
|
+
* (Fable review, #2214): it grows exactly like {@link freeVarsInBody}'s own
|
|
2755
|
+
* `bound` set, at the SAME `arrow` case. The initial call from
|
|
2756
|
+
* `attachParsedExpressions` seeds it with the enclosing loop's `param`/
|
|
2757
|
+
* `index` too (a loop's item variable shadows a same-named module const the
|
|
2758
|
+
* same way a callback arrow's param does — `origin.freeRefs` already tags
|
|
2759
|
+
* such a name `kind: 'render-item'`, this walk now honors it).
|
|
2760
|
+
*/
|
|
2761
|
+
function resolveCallbackMethodFunctionReferences(
|
|
2762
|
+
expr: ParsedExpr,
|
|
2763
|
+
analyzer: AnalyzerContext,
|
|
2764
|
+
bound: ReadonlySet<string> = EMPTY_BOUND,
|
|
2765
|
+
): ParsedExpr {
|
|
2766
|
+
function visit(e: ParsedExpr, bound: ReadonlySet<string>): ParsedExpr {
|
|
2767
|
+
switch (e.kind) {
|
|
2768
|
+
case 'literal':
|
|
2769
|
+
case 'identifier':
|
|
2770
|
+
case 'regex':
|
|
2771
|
+
case 'unsupported':
|
|
2772
|
+
return e
|
|
2773
|
+
case 'call': {
|
|
2774
|
+
const callee = visit(e.callee, bound)
|
|
2775
|
+
const args = e.args.map(a => visit(a, bound))
|
|
2776
|
+
if (
|
|
2777
|
+
callee.kind === 'member' && !callee.computed &&
|
|
2778
|
+
CALLBACK_METHODS.has(callee.property) &&
|
|
2779
|
+
args[0]?.kind === 'identifier' &&
|
|
2780
|
+
!bound.has(args[0].name)
|
|
2781
|
+
) {
|
|
2782
|
+
const resolved = resolveCallbackMethodFunctionReferenceIdentifier(args[0].name, analyzer)
|
|
2783
|
+
const arrow = resolved ? tsNodeToParsedExpr(resolved) : null
|
|
2784
|
+
if (arrow && arrow.kind === 'arrow') args[0] = arrow
|
|
2785
|
+
}
|
|
2786
|
+
return { ...e, callee, args }
|
|
2787
|
+
}
|
|
2788
|
+
case 'member':
|
|
2789
|
+
return { ...e, object: visit(e.object, bound) }
|
|
2790
|
+
case 'index-access':
|
|
2791
|
+
return { ...e, object: visit(e.object, bound), index: visit(e.index, bound) }
|
|
2792
|
+
case 'binary':
|
|
2793
|
+
case 'logical':
|
|
2794
|
+
return { ...e, left: visit(e.left, bound), right: visit(e.right, bound) }
|
|
2795
|
+
case 'unary':
|
|
2796
|
+
return { ...e, argument: visit(e.argument, bound) }
|
|
2797
|
+
case 'conditional':
|
|
2798
|
+
return { ...e, test: visit(e.test, bound), consequent: visit(e.consequent, bound), alternate: visit(e.alternate, bound) }
|
|
2799
|
+
case 'template-literal':
|
|
2800
|
+
return { ...e, parts: e.parts.map(p => (p.type === 'expression' ? { ...p, expr: visit(p.expr, bound) } : p)) }
|
|
2801
|
+
case 'array-literal':
|
|
2802
|
+
return { ...e, elements: e.elements.map(el => visit(el, bound)) }
|
|
2803
|
+
case 'array-method':
|
|
2804
|
+
// `e.args` is a fixed-length tuple for some `method` variants (e.g.
|
|
2805
|
+
// `flat`'s `args: []`) — `.map(visit)` preserves length/order (a
|
|
2806
|
+
// callback resolution never adds/removes args), so this is a
|
|
2807
|
+
// same-shape cast, not a structural change. TS can't verify the
|
|
2808
|
+
// per-`method` tuple-length invariant across a `.map`, hence the
|
|
2809
|
+
// whole-object cast rather than a narrower one.
|
|
2810
|
+
return {
|
|
2811
|
+
...e,
|
|
2812
|
+
object: visit(e.object, bound),
|
|
2813
|
+
args: e.args.map(a => visit(a, bound)),
|
|
2814
|
+
...(e.method === 'flat' && e.depthExpr ? { depthExpr: visit(e.depthExpr, bound) } : {}),
|
|
2815
|
+
} as ParsedExpr
|
|
2816
|
+
case 'object-literal':
|
|
2817
|
+
return { ...e, properties: e.properties.map(p => ({ ...p, value: visit(p.value, bound) })) }
|
|
2818
|
+
case 'arrow': {
|
|
2819
|
+
const inner = e.params.length === 0 ? bound : new Set([...bound, ...e.params])
|
|
2820
|
+
return { ...e, body: visit(e.body, inner) }
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
return visit(expr, bound)
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2649
2827
|
/**
|
|
2650
2828
|
* Result type for extractFilterPredicate. The predicate is always an expression:
|
|
2651
2829
|
* a block body is normalized to one via `foldBlockToExpr` +
|
|
@@ -3469,14 +3647,19 @@ function transformMapCall(
|
|
|
3469
3647
|
setArray(innerSort.array)
|
|
3470
3648
|
}
|
|
3471
3649
|
} else {
|
|
3472
|
-
// Simple filter().map()
|
|
3473
|
-
|
|
3474
|
-
|
|
3650
|
+
// Simple filter().map(). `setArray` (not a bare `array =`
|
|
3651
|
+
// assignment) so `templateArray` carries the `_p.`-rewritten form —
|
|
3652
|
+
// the module-scope `template:` lambda can't see init's destructured
|
|
3653
|
+
// prop locals, and a bare destructured name there threw
|
|
3654
|
+
// `ReferenceError` at runtime (#2222).
|
|
3655
|
+
setArray(filterInfo.array)
|
|
3475
3656
|
}
|
|
3476
3657
|
}
|
|
3477
3658
|
} else {
|
|
3478
|
-
|
|
3479
|
-
|
|
3659
|
+
// Plain `.map()` fallback — same `setArray` requirement as above
|
|
3660
|
+
// (#2222): this is the most common loop shape and was the primary
|
|
3661
|
+
// repro for the bare-destructured-name ReferenceError.
|
|
3662
|
+
setArray(chainSource)
|
|
3480
3663
|
}
|
|
3481
3664
|
|
|
3482
3665
|
// Get callback function
|
|
@@ -4434,7 +4617,7 @@ function tryResolveTemplateSpanFromConst(
|
|
|
4434
4617
|
): IRTemplatePart[] | null {
|
|
4435
4618
|
// ${IDENT}
|
|
4436
4619
|
if (ts.isIdentifier(expr)) {
|
|
4437
|
-
const constInfo = findLocalConst(expr.text, ctx)
|
|
4620
|
+
const constInfo = findLocalConst(expr.text, ctx.analyzer)
|
|
4438
4621
|
if (!constInfo) return null
|
|
4439
4622
|
const ast = parseConstInitializer(constInfo)
|
|
4440
4623
|
if (!ast) return null
|
|
@@ -4447,7 +4630,7 @@ function tryResolveTemplateSpanFromConst(
|
|
|
4447
4630
|
// ${IDENT[KEY]}
|
|
4448
4631
|
if (ts.isElementAccessExpression(expr)) {
|
|
4449
4632
|
if (!ts.isIdentifier(expr.expression)) return null
|
|
4450
|
-
const constInfo = findLocalConst(expr.expression.text, ctx)
|
|
4633
|
+
const constInfo = findLocalConst(expr.expression.text, ctx.analyzer)
|
|
4451
4634
|
if (!constInfo) return null
|
|
4452
4635
|
const ast = parseConstInitializer(constInfo)
|
|
4453
4636
|
if (!ast || !ts.isObjectLiteralExpression(ast)) return null
|
|
@@ -4504,8 +4687,8 @@ function tryResolveTemplateSpanFromConst(
|
|
|
4504
4687
|
*
|
|
4505
4688
|
* Returns undefined when no const matches.
|
|
4506
4689
|
*/
|
|
4507
|
-
function findLocalConst(name: string,
|
|
4508
|
-
const matches =
|
|
4690
|
+
function findLocalConst(name: string, analyzer: AnalyzerContext) {
|
|
4691
|
+
const matches = analyzer.localConstants.filter(c => c.name === name)
|
|
4509
4692
|
if (matches.length === 0) return undefined
|
|
4510
4693
|
// Prefer the innermost (non-module) binding; fall back to whatever's
|
|
4511
4694
|
// there if every match is at module scope.
|
|
@@ -4522,8 +4705,8 @@ function findLocalConst(name: string, ctx: TransformContext) {
|
|
|
4522
4705
|
*
|
|
4523
4706
|
* Returns undefined when no local function matches.
|
|
4524
4707
|
*/
|
|
4525
|
-
function findLocalFunction(name: string,
|
|
4526
|
-
const matches =
|
|
4708
|
+
function findLocalFunction(name: string, analyzer: AnalyzerContext) {
|
|
4709
|
+
const matches = analyzer.localFunctions.filter(f => f.name === name)
|
|
4527
4710
|
if (matches.length === 0) return undefined
|
|
4528
4711
|
const fnScoped = matches.filter(f => !f.isModule)
|
|
4529
4712
|
const pool = fnScoped.length > 0 ? fnScoped : matches
|
|
@@ -4613,7 +4796,15 @@ function tryResolveIdentifierAsTemplateLiteral(
|
|
|
4613
4796
|
ident: ts.Identifier,
|
|
4614
4797
|
ctx: TransformContext,
|
|
4615
4798
|
): IRTemplatePart[] | null {
|
|
4616
|
-
|
|
4799
|
+
// #2222/#2235: at this transform position the identifier may be an
|
|
4800
|
+
// enclosing loop callback's own (shadowing) parameter — folding the
|
|
4801
|
+
// outer const's literal into the IR here bakes the same hard-coded
|
|
4802
|
+
// value into EVERY adapter's output (e.g. `key={label}` inside
|
|
4803
|
+
// `.map((label) => ...)` becoming a constant duplicate key).
|
|
4804
|
+
// `ctx.loopParams` is the live loop-param set (destructured binding
|
|
4805
|
+
// names and index included), so the guard is scope-accurate.
|
|
4806
|
+
if (ctx.loopParams.has(ident.text)) return null
|
|
4807
|
+
const constInfo = findLocalConst(ident.text, ctx.analyzer)
|
|
4617
4808
|
if (!constInfo) return null
|
|
4618
4809
|
const ast = parseConstInitializer(constInfo)
|
|
4619
4810
|
if (!ast) return null
|
|
@@ -4823,8 +5014,8 @@ function tryDesugarInterleaveTaggedTemplate(
|
|
|
4823
5014
|
* followed, matching that precedent. Returns null when nothing resolves.
|
|
4824
5015
|
*/
|
|
4825
5016
|
function resolveInterleaveTagIdentifier(name: string, ctx: TransformContext): ts.Expression | null {
|
|
4826
|
-
const constInfo = findLocalConst(name, ctx)
|
|
4827
|
-
const fnInfo = findLocalFunction(name, ctx)
|
|
5017
|
+
const constInfo = findLocalConst(name, ctx.analyzer)
|
|
5018
|
+
const fnInfo = findLocalFunction(name, ctx.analyzer)
|
|
4828
5019
|
// A name bound BOTH as a const and as a `function` declaration is the
|
|
4829
5020
|
// same cross-kind ambiguity `resolveSortComparatorIdentifier` refuses:
|
|
4830
5021
|
// it can only occur across scopes, and `FunctionInfo.isModule` reflects
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TEST-HARNESS ONLY. Evaluate a signal initializer / prop-default source
|
|
3
|
+
* expression against a mock `props` object by EXECUTING it (`new
|
|
4
|
+
* Function`, with `undefined` shadowing the globals listed below), the
|
|
5
|
+
* same way the Hono/CSR conformance reference produces its value — by
|
|
6
|
+
* actually running the component. Never call this from a build path (`bf
|
|
7
|
+
* build`, `compileJSX`, any adapter's `generate`); it exists only so each
|
|
8
|
+
* adapter's `test-render.ts` conformance harness can seed a signal/
|
|
9
|
+
* prop-default for a non-JS-runtime SSR render (PHP/Ruby/Python/Perl/Go)
|
|
10
|
+
* that matches what Hono would compute. (#2209)
|
|
11
|
+
*
|
|
12
|
+
* NOT a security sandbox — the global shadowing below is a determinism
|
|
13
|
+
* tripwire (catches the common accidental-nondeterminism case, e.g.
|
|
14
|
+
* `Date.now()`), not containment: a sufficiently creative expression can
|
|
15
|
+
* still reach the real global scope (`new Function` bodies execute
|
|
16
|
+
* unlexically-scoped, so e.g. `[].constructor.constructor('return
|
|
17
|
+
* Date')()` recovers `Date` even though the bare name is shadowed, and
|
|
18
|
+
* `eval` can't be shadowed at all — JS forbids declaring or binding a
|
|
19
|
+
* parameter literally named `eval` in strict-mode code). Do not widen this
|
|
20
|
+
* module's use to anything but first-party fixture source. The actual
|
|
21
|
+
* trust boundary is that the evaluated text is first-party fixture
|
|
22
|
+
* source, already compiled by this same process — the conformance harness
|
|
23
|
+
* already executes fixture-derived code far more invasively (spawning
|
|
24
|
+
* `ruby`/`php`/`perl`/`go run` on generated programs). This module is
|
|
25
|
+
* never reachable from anything a real end user's untrusted input could
|
|
26
|
+
* influence.
|
|
27
|
+
*
|
|
28
|
+
* Why real execution instead of a hand-rolled evaluator: the initializer
|
|
29
|
+
* source is an arbitrary JS-subset expression over `props` (e.g. `(props.x
|
|
30
|
+
* ?? []).map(t => ({ ...t, editing: false }))`, #2209's actual repro) —
|
|
31
|
+
* every previous approach here was a regex/pattern-match over a small
|
|
32
|
+
* catalogue of recognized shapes (`props.x`, `props.x ?? default`, a bare
|
|
33
|
+
* literal), and #2209 is literally "the catalogue missed a shape" (the
|
|
34
|
+
* THIRD such miss on this codebase, per the superseded
|
|
35
|
+
* `evaluate-signal-init.test.ts`'s own #1672 pins). A hand-written
|
|
36
|
+
* evaluator over `ParsedExpr` would face the same drift, and can't
|
|
37
|
+
* represent object spread (`{ ...t, editing: false }`) without extending
|
|
38
|
+
* `ParsedExpr` — a production-compiler change that ripples into every
|
|
39
|
+
* adapter's exhaustive switch, disproportionate for a test-only need.
|
|
40
|
+
* `new Function` delegates parsing to the JS engine itself — CLAUDE.md's
|
|
41
|
+
* regex-parsing ban exists precisely to avoid the false-match/missed-shape
|
|
42
|
+
* failure mode this replaces.
|
|
43
|
+
*
|
|
44
|
+
* Shadowed as `undefined` (best-effort determinism, not containment — see
|
|
45
|
+
* above): `globalThis`, `window`, `document`, `Date`, `Math`, `crypto`,
|
|
46
|
+
* `performance`, `fetch`, `setTimeout`, `setInterval`, `require`,
|
|
47
|
+
* `process`, `Function`. (`eval` is deliberately absent from this list —
|
|
48
|
+
* it cannot be shadowed as a parameter name in strict-mode code; see
|
|
49
|
+
* above.)
|
|
50
|
+
*
|
|
51
|
+
* `props` bare-identifier destructured params (`createSignal(count)` where
|
|
52
|
+
* `count` is a destructured prop, not a `props.x` member) are NOT bound —
|
|
53
|
+
* the evaluator only exposes `props` — so such an initializer throws
|
|
54
|
+
* `ReferenceError` and falls back to "unset", matching every prior
|
|
55
|
+
* evaluator's behavior for that shape. Extending the environment with
|
|
56
|
+
* `ir.metadata.propsParams` bindings is a natural follow-up if a fixture
|
|
57
|
+
* ever needs it.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
const BLOCKED_GLOBALS = [
|
|
61
|
+
'globalThis',
|
|
62
|
+
'window',
|
|
63
|
+
'document',
|
|
64
|
+
'Date',
|
|
65
|
+
'Math',
|
|
66
|
+
'crypto',
|
|
67
|
+
'performance',
|
|
68
|
+
'fetch',
|
|
69
|
+
'setTimeout',
|
|
70
|
+
'setInterval',
|
|
71
|
+
'require',
|
|
72
|
+
'process',
|
|
73
|
+
'Function',
|
|
74
|
+
] as const
|
|
75
|
+
|
|
76
|
+
export type SignalInitEvalResult = { ok: true; value: unknown } | { ok: false }
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A value the harness's downstream language serializers (JSON/Python/PHP/
|
|
80
|
+
* Perl/Ruby literal builders) can actually marshal: `null`, a boolean, a
|
|
81
|
+
* number (including non-finite — several serializers already special-case
|
|
82
|
+
* NaN/Infinity), a string, a dense array with no holes and no `undefined`
|
|
83
|
+
* elements, or a plain object (`Object.prototype` or null prototype only —
|
|
84
|
+
* rejects class instances, functions, Maps/Sets, etc). Rejects genuine
|
|
85
|
+
* cycles; a shared (non-cyclic) reference appearing more than once — e.g.
|
|
86
|
+
* the same object at two array indices — is fine (JSON-equivalent
|
|
87
|
+
* behavior just duplicates it), so `ancestors` tracks only the current
|
|
88
|
+
* recursion path, not every value ever visited.
|
|
89
|
+
*/
|
|
90
|
+
function isTransportable(value: unknown, ancestors: Set<unknown> = new Set()): boolean {
|
|
91
|
+
if (value === null) return true
|
|
92
|
+
const t = typeof value
|
|
93
|
+
if (t === 'boolean' || t === 'number' || t === 'string') return true
|
|
94
|
+
if (t !== 'object') return false
|
|
95
|
+
if (ancestors.has(value)) return false // a real cycle (value is its own ancestor)
|
|
96
|
+
ancestors.add(value)
|
|
97
|
+
try {
|
|
98
|
+
if (Array.isArray(value)) {
|
|
99
|
+
// `Array.prototype.every` silently skips holes (`[1, , 3]`), so a
|
|
100
|
+
// sparse array would otherwise pass — compare against the own
|
|
101
|
+
// enumerable key count (holes aren't own keys) to catch that.
|
|
102
|
+
if (Object.keys(value).length !== value.length) return false
|
|
103
|
+
return value.every(el => el !== undefined && isTransportable(el, ancestors))
|
|
104
|
+
}
|
|
105
|
+
const proto = Object.getPrototypeOf(value)
|
|
106
|
+
if (proto !== Object.prototype && proto !== null) return false
|
|
107
|
+
return Object.values(value as Record<string, unknown>).every(v => isTransportable(v, ancestors))
|
|
108
|
+
} finally {
|
|
109
|
+
ancestors.delete(value)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Evaluate `expr` (a JS-subset source expression) against `props`. Returns
|
|
115
|
+
* `{ ok: false }` when the expression fails to parse, throws at evaluation
|
|
116
|
+
* time (e.g. a `ReferenceError` for an unbound identifier), or evaluates to
|
|
117
|
+
* something the downstream serializers can't marshal (see
|
|
118
|
+
* {@link isTransportable}).
|
|
119
|
+
*/
|
|
120
|
+
export function tryEvaluateSignalInit(
|
|
121
|
+
expr: string,
|
|
122
|
+
props?: Record<string, unknown>,
|
|
123
|
+
): SignalInitEvalResult {
|
|
124
|
+
const src = expr.trim()
|
|
125
|
+
if (src === '') return { ok: false }
|
|
126
|
+
let fn: (props: Record<string, unknown>, ...blocked: undefined[]) => unknown
|
|
127
|
+
try {
|
|
128
|
+
// `props` plus the blocked-globals shadows are the only bindings
|
|
129
|
+
// passed in — best-effort determinism, not containment; see the file
|
|
130
|
+
// docstring for the trust-boundary rationale (test-harness-only,
|
|
131
|
+
// never a build path).
|
|
132
|
+
fn = new Function(
|
|
133
|
+
'props',
|
|
134
|
+
...BLOCKED_GLOBALS,
|
|
135
|
+
`'use strict'; return (\n${src}\n);`,
|
|
136
|
+
) as typeof fn
|
|
137
|
+
} catch {
|
|
138
|
+
return { ok: false }
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const value = fn(props ?? {})
|
|
142
|
+
// `undefined` at the TOP level is a genuine, distinguishable result
|
|
143
|
+
// (e.g. an explicit `undefined` initializer, or `props.x` with no `x`
|
|
144
|
+
// and no `??` fallback) — not a marshal failure. `evaluateSignalInit`'s
|
|
145
|
+
// wrapper still collapses it to the "skip" outcome; callers that need
|
|
146
|
+
// the distinction use this function directly.
|
|
147
|
+
if (value === undefined) return { ok: true, value: undefined }
|
|
148
|
+
return isTransportable(value) ? { ok: true, value } : { ok: false }
|
|
149
|
+
} catch {
|
|
150
|
+
return { ok: false }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Drop-in replacement for the harnesses' former per-adapter regex
|
|
156
|
+
* evaluator: `null` means "could not evaluate, or evaluated to
|
|
157
|
+
* `undefined` — leave the signal/default unseeded", matching every prior
|
|
158
|
+
* evaluator's convention (an explicit JS `null` initializer also maps to
|
|
159
|
+
* `null` — the same "skip" outcome, since none of these harnesses
|
|
160
|
+
* distinguish "explicitly null" from "unset" downstream).
|
|
161
|
+
*/
|
|
162
|
+
export function evaluateSignalInit(expr: string, props?: Record<string, unknown>): unknown {
|
|
163
|
+
const result = tryEvaluateSignalInit(expr, props)
|
|
164
|
+
return result.ok && result.value !== undefined ? result.value : null
|
|
165
|
+
}
|