@barefootjs/jsx 0.31.0 → 0.31.2
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 +11 -0
- package/dist/adapters/interface.d.ts.map +1 -1
- package/dist/adapters/jsx-adapter.d.ts +92 -1
- package/dist/adapters/jsx-adapter.d.ts.map +1 -1
- package/dist/adapters/test-adapter.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/compiler.d.ts.map +1 -1
- package/dist/css-layer-prefixer.d.ts +16 -0
- package/dist/css-layer-prefixer.d.ts.map +1 -1
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/html-types.d.ts +19 -0
- package/dist/html-types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1402 -1124
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/phases/props-event-handlers.d.ts.map +1 -1
- package/dist/ir-to-client-js/phases/props-extraction.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +6 -4
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/jsx-runtime/index.d.ts +2 -8
- package/dist/jsx-runtime/index.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/module-exports.d.ts +9 -1
- package/dist/module-exports.d.ts.map +1 -1
- package/dist/prop-rewrite.d.ts +7 -2
- package/dist/prop-rewrite.d.ts.map +1 -1
- package/dist/props-binding.d.ts +40 -0
- package/dist/props-binding.d.ts.map +1 -0
- package/dist/relocate.d.ts +9 -0
- package/dist/relocate.d.ts.map +1 -1
- package/dist/ssr-defaults.d.ts +43 -0
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/template-parts.d.ts +53 -0
- package/dist/template-parts.d.ts.map +1 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/adapter-output.test.ts +8 -4
- package/src/__tests__/aliased-destructured-prop-csr.test.ts +112 -0
- package/src/__tests__/css-layer-prefixer.test.ts +72 -0
- package/src/__tests__/form-control-value-ssr.test.ts +48 -3
- package/src/__tests__/memo-deps-comments.test.ts +99 -0
- package/src/__tests__/multi-return-sibling-diagnostic.test.ts +241 -0
- package/src/__tests__/ssr-defaults.test.ts +124 -1
- package/src/__tests__/staged-ir/08-relocate-unit.test.ts +1 -0
- package/src/__tests__/staged-ir/11-template-primitive-registry.test.ts +1 -0
- package/src/adapters/interface.ts +11 -0
- package/src/adapters/jsx-adapter.ts +266 -4
- package/src/adapters/test-adapter.ts +13 -10
- package/src/analyzer.ts +50 -8
- package/src/compiler.ts +119 -18
- package/src/css-layer-prefixer.ts +80 -24
- package/src/errors.ts +18 -0
- package/src/html-types.ts +24 -0
- package/src/index.ts +7 -1
- package/src/ir-to-client-js/collect-elements.ts +4 -1
- package/src/ir-to-client-js/emit-reactive.ts +4 -2
- package/src/ir-to-client-js/phases/props-event-handlers.ts +4 -3
- package/src/ir-to-client-js/phases/props-extraction.ts +7 -4
- package/src/ir-to-client-js/plan/build-declaration-emit.ts +6 -3
- package/src/ir-to-client-js/utils.ts +5 -24
- package/src/jsx-runtime/index.ts +2 -7
- package/src/jsx-to-ir.ts +75 -42
- package/src/module-exports.ts +11 -2
- package/src/prop-rewrite.ts +25 -5
- package/src/props-binding.ts +70 -0
- package/src/relocate.ts +19 -2
- package/src/ssr-defaults.ts +70 -0
- package/src/template-parts.ts +81 -0
- package/src/types.ts +10 -0
|
@@ -7,6 +7,7 @@ import ts from 'typescript'
|
|
|
7
7
|
import type { AttrValue, IRTemplatePart, LoopParamBinding, FreeReference, IRNode } from '../types.ts'
|
|
8
8
|
import type { TopLevelLoop, BranchLoop, LoopOffset } from './types.ts'
|
|
9
9
|
import { buildLoopChainExpr } from '../loop-chain.ts'
|
|
10
|
+
import { templatePartsToJsExpr } from '../template-parts.ts'
|
|
10
11
|
import {
|
|
11
12
|
iterateJsTokens,
|
|
12
13
|
isIdentifierLikeToken,
|
|
@@ -73,31 +74,11 @@ export function profileBindingId(componentName: string | undefined, slotId: stri
|
|
|
73
74
|
* Convert a `template` variant's parts into a JS template-literal string.
|
|
74
75
|
* Shared by both `attrValueToString` and any consumer that wants to flatten
|
|
75
76
|
* a structured template into JS-level concatenation.
|
|
77
|
+
*
|
|
78
|
+
* Re-exported from the single renderer in `../template-parts.ts` — the
|
|
79
|
+
* client bundle is plain JS, so it never passes `typed`.
|
|
76
80
|
*/
|
|
77
|
-
export
|
|
78
|
-
let result = '`'
|
|
79
|
-
for (const part of parts) {
|
|
80
|
-
if (part.type === 'string') {
|
|
81
|
-
result += (opts?.useTemplate && part.templateValue) ? part.templateValue : part.value
|
|
82
|
-
} else if (part.type === 'ternary') {
|
|
83
|
-
const cond = (opts?.useTemplate && part.templateCondition) ? part.templateCondition : part.condition
|
|
84
|
-
result += `\${${cond} ? '${part.whenTrue}' : '${part.whenFalse}'}`
|
|
85
|
-
} else if (part.type === 'lookup') {
|
|
86
|
-
// `${MAP[KEY]}` was structurally captured at IR time so SSR
|
|
87
|
-
// adapters could emit a switch. For client-side JS we rebuild
|
|
88
|
-
// the equivalent runtime indexed lookup against the resolved
|
|
89
|
-
// cases — keeps the JSX runtime path semantically identical to
|
|
90
|
-
// the original `${variantClasses[variant]}` source.
|
|
91
|
-
const key = (opts?.useTemplate && part.templateKey) ? part.templateKey : part.key
|
|
92
|
-
const obj = '{' + Object.entries(part.cases).map(
|
|
93
|
-
([k, v]) => `${JSON.stringify(k)}: ${JSON.stringify(v)}`
|
|
94
|
-
).join(', ') + '}'
|
|
95
|
-
result += `\${(${obj})[${key}]}`
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
result += '`'
|
|
99
|
-
return result
|
|
100
|
-
}
|
|
81
|
+
export { templatePartsToJsExpr }
|
|
101
82
|
|
|
102
83
|
/**
|
|
103
84
|
* Flatten an `AttrValue` to its raw string form, suitable for HTML attribute
|
package/src/jsx-runtime/index.ts
CHANGED
|
@@ -45,6 +45,7 @@ import type {
|
|
|
45
45
|
SVGBaseAttributes,
|
|
46
46
|
SVGPresentationAttributes,
|
|
47
47
|
SVGMarkerReferenceAttributes,
|
|
48
|
+
SVGSVGAttributes,
|
|
48
49
|
} from '../html-types.ts'
|
|
49
50
|
|
|
50
51
|
// Stub function types (for type checking only - no runtime implementation)
|
|
@@ -208,13 +209,7 @@ export declare namespace JSX {
|
|
|
208
209
|
// SVG (basic support).
|
|
209
210
|
// Each entry uses `SVGBaseAttributes` so `ref` can be narrowed per-tag.
|
|
210
211
|
// See `SVGBaseAttributes` JSDoc for why plain intersection doesn't work.
|
|
211
|
-
svg:
|
|
212
|
-
viewBox?: string
|
|
213
|
-
xmlns?: string
|
|
214
|
-
width?: number | string
|
|
215
|
-
height?: number | string
|
|
216
|
-
ref?: (element: SVGSVGElement) => void
|
|
217
|
-
}
|
|
212
|
+
svg: SVGSVGAttributes
|
|
218
213
|
path: SVGBaseAttributes & SVGPresentationAttributes & SVGMarkerReferenceAttributes & {
|
|
219
214
|
d?: string
|
|
220
215
|
pathLength?: number | string
|
package/src/jsx-to-ir.ts
CHANGED
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
rewriteBarePropRefs as rewriteBarePropRefsCore,
|
|
50
50
|
collectAstPropRefs,
|
|
51
51
|
} from './prop-rewrite.ts'
|
|
52
|
+
import { buildPropAliasMap } from './props-binding.ts'
|
|
52
53
|
import { resolveFreeRefs, isNameBound as isNameBoundInEnv, type BindingEnvironment } from './free-refs.ts'
|
|
53
54
|
import { computeFileScope } from './ir-to-client-js/component-scope.ts'
|
|
54
55
|
import { createTemplateAwareStringProtector } from './ir-to-client-js/html-template.ts'
|
|
@@ -59,6 +60,7 @@ import type { LoweringMatcher } from './lowering-registry.ts'
|
|
|
59
60
|
import { extractFreeIdentifiersFromNode, initializerShapeContainsJsx, extractMultiReturnJsxBranches, type MultiReturnJsxBranches } from './analyzer.ts'
|
|
60
61
|
import { iterateJsTokens, replaceInExprContexts } from './scanner/js-scanner.ts'
|
|
61
62
|
import { reconstructAsSegments } from './strip-types.ts'
|
|
63
|
+
import { templatePartsToJsExpr } from './template-parts.ts'
|
|
62
64
|
import { toHTMLAttrName, decodeEntities } from '@barefootjs/shared'
|
|
63
65
|
|
|
64
66
|
// =============================================================================
|
|
@@ -91,6 +93,15 @@ interface TransformContext {
|
|
|
91
93
|
_moduleClientSignalNames?: Set<string>
|
|
92
94
|
/** Cached set of destructured prop names for AST-based rewriting */
|
|
93
95
|
_destructuredPropNames?: Set<string> | null
|
|
96
|
+
/**
|
|
97
|
+
* Cached local-name → caller-facing-key (`sourceName ?? name`) map for
|
|
98
|
+
* `_destructuredPropNames`, entries only for names that actually rename
|
|
99
|
+
* (`{ n: count }` → `count` → `n`). Built alongside `_destructuredPropNames`
|
|
100
|
+
* so both stay in lockstep with the same shadow-filtered eligible set —
|
|
101
|
+
* consumed by `rewriteBarePropRefsCore` to emit `_p.<caller>` instead of
|
|
102
|
+
* `_p.<local>` (#2524 CSR half: `_p` is always caller-keyed).
|
|
103
|
+
*/
|
|
104
|
+
_destructuredPropAliases?: Map<string, string> | null
|
|
94
105
|
/** Active loop parameter names for slotId assignment to loop-param-dependent expressions */
|
|
95
106
|
loopParams: Set<string>
|
|
96
107
|
/**
|
|
@@ -536,7 +547,8 @@ function rewriteBarePropRefs(text: string, expr: ts.Node, ctx: TransformContext)
|
|
|
536
547
|
// `_branchScopePropDeps` at branch entry; here we just walk `expr`
|
|
537
548
|
// for references to those locals and union the matching dep sets.
|
|
538
549
|
const extraPropRefs = collectBranchLocalPropRefsViaSubstitution(expr, ctx)
|
|
539
|
-
|
|
550
|
+
const propAliases = getDestructuredPropAliases(ctx)
|
|
551
|
+
return rewriteBarePropRefsCore(dateLowered, expr, propNames, extraPropRefs, propAliases ?? undefined)
|
|
540
552
|
}
|
|
541
553
|
|
|
542
554
|
/**
|
|
@@ -618,14 +630,26 @@ function getDestructuredPropNames(ctx: TransformContext): Set<string> | null {
|
|
|
618
630
|
if (!isDestructureFromProps) shadowed.add(c.name)
|
|
619
631
|
}
|
|
620
632
|
}
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
.filter(n => !shadowed.has(n))
|
|
633
|
+
const eligible = ctx.analyzer.propsParams.filter(p => !shadowed.has(p.name))
|
|
634
|
+
const names = eligible.map(p => p.name)
|
|
624
635
|
ctx._destructuredPropNames = names.length > 0 ? new Set(names) : null
|
|
636
|
+
ctx._destructuredPropAliases = buildPropAliasMap(eligible) ?? null
|
|
625
637
|
}
|
|
626
638
|
return ctx._destructuredPropNames ?? null
|
|
627
639
|
}
|
|
628
640
|
|
|
641
|
+
/**
|
|
642
|
+
* Companion to `getDestructuredPropNames`: the local-name → caller-key
|
|
643
|
+
* alias map for the SAME shadow-filtered eligible set, populated as a
|
|
644
|
+
* side effect of that call. Always call `getDestructuredPropNames` first
|
|
645
|
+
* (or accept it may return an empty cache) — the two caches are written
|
|
646
|
+
* together in one pass.
|
|
647
|
+
*/
|
|
648
|
+
function getDestructuredPropAliases(ctx: TransformContext): Map<string, string> | null {
|
|
649
|
+
if (ctx._destructuredPropNames === undefined) getDestructuredPropNames(ctx)
|
|
650
|
+
return ctx._destructuredPropAliases ?? null
|
|
651
|
+
}
|
|
652
|
+
|
|
629
653
|
function createTransformContext(analyzer: AnalyzerContext): TransformContext {
|
|
630
654
|
return {
|
|
631
655
|
analyzer,
|
|
@@ -1278,12 +1302,21 @@ function transformJsxElement(
|
|
|
1278
1302
|
* - `<textarea>` with no children gains a NON-reactive expression child
|
|
1279
1303
|
* (the initial value as element content — updates keep flowing through
|
|
1280
1304
|
* the `.value` effect, deliberately not a live text slot);
|
|
1281
|
-
* - `<select>` distributes `selected={(value) ===
|
|
1282
|
-
*
|
|
1283
|
-
* the exact per-option comparison shape the
|
|
1284
|
-
* fixture already proves across every adapter.
|
|
1285
|
-
*
|
|
1286
|
-
*
|
|
1305
|
+
* - `<select>` distributes `selected={(value) === optValue}` onto each
|
|
1306
|
+
* `<option>` (incl. under `<optgroup>`/fragments, and under a `.map()`
|
|
1307
|
+
* loop body) — the exact per-option comparison shape the
|
|
1308
|
+
* `select-option-selected` fixture already proves across every adapter.
|
|
1309
|
+
* A literal `optValue` (e.g. `value="banana"`) compares by
|
|
1310
|
+
* `JSON.stringify`; an expression `optValue` (a static dynamic value, or
|
|
1311
|
+
* a loop row reading its item — e.g. `value={o.id}`) compares against
|
|
1312
|
+
* the expression text directly. For a loop row this makes `selected` an
|
|
1313
|
+
* ordinary per-item reactive attribute like any other (`o.id`, the row's
|
|
1314
|
+
* text) — it rides the SAME loop-plan machinery
|
|
1315
|
+
* (`collectLoopChildReactiveAttrs` → `emitAttrUpdate`, which special-cases
|
|
1316
|
+
* `selected` as a boolean DOM PROPERTY write, not just an HTML attribute)
|
|
1317
|
+
* that already reruns per row on item change and per outer-signal change,
|
|
1318
|
+
* so selectedness is recomputed instead of staying attached to whichever
|
|
1319
|
+
* physical `<option>` a reorder happened to rewrite in place (#2466).
|
|
1287
1320
|
*/
|
|
1288
1321
|
function lowerFormControlValueSsr(
|
|
1289
1322
|
tagName: string,
|
|
@@ -1318,21 +1351,47 @@ function lowerFormControlValueSsr(
|
|
|
1318
1351
|
return
|
|
1319
1352
|
}
|
|
1320
1353
|
|
|
1321
|
-
const
|
|
1354
|
+
const selectedForLiteral = (optValue: string): AttrValue =>
|
|
1322
1355
|
AttrValueOf.expression(
|
|
1323
1356
|
`(${expr}) === ${JSON.stringify(optValue)}`,
|
|
1324
1357
|
templateExpr !== undefined
|
|
1325
1358
|
? { templateExpr: `(${templateExpr}) === ${JSON.stringify(optValue)}` }
|
|
1326
1359
|
: undefined,
|
|
1327
1360
|
)
|
|
1361
|
+
// An expression-valued `option value` — the shape every `.map()` loop row
|
|
1362
|
+
// uses (`value={o.id}`) since the whole point of the loop is a per-item
|
|
1363
|
+
// value. Compares the controlled value directly against the option's own
|
|
1364
|
+
// value EXPRESSION TEXT (never JSON-stringified — it is JS, not a string
|
|
1365
|
+
// literal). Inside a loop row this expression reads both the row item
|
|
1366
|
+
// (`o.id`) and the outer controlled signal (`expr`, e.g. `val()`), which
|
|
1367
|
+
// is exactly the "reads both" shape `collectLoopChildReactiveAttrs` /
|
|
1368
|
+
// `classifyLazyBinding` already know how to place into both `applyItem`
|
|
1369
|
+
// (row changed) and `applyOuter` (controlled value changed) — no new loop
|
|
1370
|
+
// machinery, just one more per-row reactive attribute (#2466).
|
|
1371
|
+
const selectedForExpr = (optExpr: string, optTemplateExpr: string | undefined): AttrValue =>
|
|
1372
|
+
AttrValueOf.expression(
|
|
1373
|
+
`(${expr}) === (${optExpr})`,
|
|
1374
|
+
templateExpr !== undefined || optTemplateExpr !== undefined
|
|
1375
|
+
? { templateExpr: `(${templateExpr ?? expr}) === (${optTemplateExpr ?? optExpr})` }
|
|
1376
|
+
: undefined,
|
|
1377
|
+
)
|
|
1328
1378
|
const distribute = (nodes: IRNode[]): void => {
|
|
1329
1379
|
for (const n of nodes) {
|
|
1330
1380
|
if (n.type === 'element' && n.tag === 'option') {
|
|
1331
1381
|
if (n.attrs.some(a => a.name === 'selected')) continue
|
|
1332
1382
|
const optValue = n.attrs.find(a => a.name === 'value')
|
|
1333
|
-
if (!optValue
|
|
1334
|
-
|
|
1335
|
-
|
|
1383
|
+
if (!optValue) continue
|
|
1384
|
+
if (optValue.value.kind === 'literal') {
|
|
1385
|
+
n.attrs.push({ name: 'selected', value: selectedForLiteral(optValue.value.value), loc: n.loc })
|
|
1386
|
+
} else if (optValue.value.kind === 'expression') {
|
|
1387
|
+
const selected = selectedForExpr(optValue.value.expr, optValue.value.templateExpr)
|
|
1388
|
+
n.attrs.push({ name: 'selected', value: selected, loc: n.loc })
|
|
1389
|
+
}
|
|
1390
|
+
} else if (
|
|
1391
|
+
n.type === 'fragment' ||
|
|
1392
|
+
n.type === 'loop' ||
|
|
1393
|
+
(n.type === 'element' && n.tag === 'optgroup')
|
|
1394
|
+
) {
|
|
1336
1395
|
distribute(n.children)
|
|
1337
1396
|
}
|
|
1338
1397
|
}
|
|
@@ -6777,8 +6836,8 @@ function processComponentProps(
|
|
|
6777
6836
|
// the bare `classes` identifier with no prop refs, so the rewrite
|
|
6778
6837
|
// no-ops and the module-scope registration template leaks bare
|
|
6779
6838
|
// destructured props into `renderChild(...)` (#2468).
|
|
6780
|
-
const collapsed =
|
|
6781
|
-
const collapsedTemplate =
|
|
6839
|
+
const collapsed = templatePartsToJsExpr(value.parts)
|
|
6840
|
+
const collapsedTemplate = templatePartsToJsExpr(value.parts, { useTemplate: true })
|
|
6782
6841
|
value = AttrValueOf.expression(collapsed, {
|
|
6783
6842
|
parts: value.parts,
|
|
6784
6843
|
...(collapsedTemplate !== collapsed && { templateExpr: collapsedTemplate }),
|
|
@@ -6820,32 +6879,6 @@ function processComponentProps(
|
|
|
6820
6879
|
return props
|
|
6821
6880
|
}
|
|
6822
6881
|
|
|
6823
|
-
/**
|
|
6824
|
-
* Flatten a structured template-literal's parts back into a JS expression
|
|
6825
|
-
* string. Used at IR construction time when a structured `template` variant
|
|
6826
|
-
* needs to be collapsed into an `expression` for component-prop forwarding —
|
|
6827
|
-
* component props are runtime JS values, not HTML attribute bodies.
|
|
6828
|
-
*/
|
|
6829
|
-
function templatePartsToJsString(parts: readonly IRTemplatePart[], opts?: { useTemplate?: boolean }): string {
|
|
6830
|
-
let result = '`'
|
|
6831
|
-
for (const part of parts) {
|
|
6832
|
-
if (part.type === 'string') {
|
|
6833
|
-
result += (opts?.useTemplate && part.templateValue) ? part.templateValue : part.value
|
|
6834
|
-
} else if (part.type === 'ternary') {
|
|
6835
|
-
const cond = (opts?.useTemplate && part.templateCondition) ? part.templateCondition : part.condition
|
|
6836
|
-
result += `\${${cond} ? '${part.whenTrue}' : '${part.whenFalse}'}`
|
|
6837
|
-
} else if (part.type === 'lookup') {
|
|
6838
|
-
const key = (opts?.useTemplate && part.templateKey) ? part.templateKey : part.key
|
|
6839
|
-
const obj = '{' + Object.entries(part.cases).map(
|
|
6840
|
-
([k, v]) => `${JSON.stringify(k)}: ${JSON.stringify(v)}`
|
|
6841
|
-
).join(', ') + '}'
|
|
6842
|
-
result += `\${(${obj})[${key}]}`
|
|
6843
|
-
}
|
|
6844
|
-
}
|
|
6845
|
-
result += '`'
|
|
6846
|
-
return result
|
|
6847
|
-
}
|
|
6848
|
-
|
|
6849
6882
|
// =============================================================================
|
|
6850
6883
|
// Helpers
|
|
6851
6884
|
// =============================================================================
|
package/src/module-exports.ts
CHANGED
|
@@ -21,10 +21,19 @@ export function generateModuleExports(
|
|
|
21
21
|
ir: ComponentIR,
|
|
22
22
|
extraInlineExported: ReadonlySet<string> = new Set(),
|
|
23
23
|
rewriteRelativeImport?: (importPath: string) => string,
|
|
24
|
+
options?: {
|
|
25
|
+
/**
|
|
26
|
+
* Skip `export const` / `export function` value declarations — the
|
|
27
|
+
* adapter already emitted them inside its module-scope section, in
|
|
28
|
+
* source order (see `TemplateSections.moduleConstantsIncludeExports`).
|
|
29
|
+
* `export { … } [from '…']` specifier blocks are still emitted.
|
|
30
|
+
*/
|
|
31
|
+
skipValueDeclarations?: boolean
|
|
32
|
+
},
|
|
24
33
|
): string | null {
|
|
25
34
|
const lines: string[] = []
|
|
26
35
|
|
|
27
|
-
for (const constant of ir.metadata.localConstants) {
|
|
36
|
+
for (const constant of options?.skipValueDeclarations ? [] : ir.metadata.localConstants) {
|
|
28
37
|
if (!constant.isExported) continue
|
|
29
38
|
const keyword = constant.declarationKind ?? 'const'
|
|
30
39
|
if (!constant.value) {
|
|
@@ -38,7 +47,7 @@ export function generateModuleExports(
|
|
|
38
47
|
lines.push(`export ${keyword} ${constant.name} = ${constant.value}`)
|
|
39
48
|
}
|
|
40
49
|
|
|
41
|
-
for (const func of ir.metadata.localFunctions) {
|
|
50
|
+
for (const func of options?.skipValueDeclarations ? [] : ir.metadata.localFunctions) {
|
|
42
51
|
if (!func.isExported) continue
|
|
43
52
|
// Prefer the source-verbatim signature so type predicates and explicit
|
|
44
53
|
// `:unknown` parameter annotations survive — see FunctionInfo.typedParams
|
package/src/prop-rewrite.ts
CHANGED
|
@@ -135,7 +135,11 @@ export function collectAstPropRefs(
|
|
|
135
135
|
* Returns null when `text` does not parse cleanly as an expression —
|
|
136
136
|
* the caller falls back to the legacy regex rewrite.
|
|
137
137
|
*/
|
|
138
|
-
function applyScopedPropRefRewrite(
|
|
138
|
+
function applyScopedPropRefRewrite(
|
|
139
|
+
text: string,
|
|
140
|
+
propRefs: Set<string>,
|
|
141
|
+
propAliases?: ReadonlyMap<string, string>,
|
|
142
|
+
): string | null {
|
|
139
143
|
// Wrap in parens so object literals and arrows parse as expressions.
|
|
140
144
|
const prefix = '('
|
|
141
145
|
const sf = ts.createSourceFile('__bf_prop_rewrite.ts', `${prefix}${text}\n)`, ts.ScriptTarget.Latest, true)
|
|
@@ -149,11 +153,15 @@ function applyScopedPropRefRewrite(text: string, propRefs: Set<string>): string
|
|
|
149
153
|
const start = n.getStart(sf) - prefix.length
|
|
150
154
|
const end = n.getEnd() - prefix.length
|
|
151
155
|
if (start < 0 || end > text.length) return
|
|
156
|
+
// `_p` is always keyed by the caller-facing name (`sourceName ?? name`
|
|
157
|
+
// — #2524 CSR half); the local binding (`n.text`) only survives on the
|
|
158
|
+
// left of a shorthand expansion.
|
|
159
|
+
const callerKey = propAliases?.get(n.text) ?? n.text
|
|
152
160
|
if (parent && ts.isShorthandPropertyAssignment(parent) && parent.name === n) {
|
|
153
|
-
edits.push({ start, end, replacement: `${n.text}: ${PROPS_PARAM}.${
|
|
161
|
+
edits.push({ start, end, replacement: `${n.text}: ${PROPS_PARAM}.${callerKey}` })
|
|
154
162
|
return
|
|
155
163
|
}
|
|
156
|
-
edits.push({ start, end, replacement: `${PROPS_PARAM}.${
|
|
164
|
+
edits.push({ start, end, replacement: `${PROPS_PARAM}.${callerKey}` })
|
|
157
165
|
})
|
|
158
166
|
|
|
159
167
|
if (edits.length === 0) return text
|
|
@@ -177,11 +185,14 @@ function applyScopedPropRefRewrite(text: string, propRefs: Set<string>): string
|
|
|
177
185
|
export function applyRegexPropRefRewrite(
|
|
178
186
|
text: string,
|
|
179
187
|
propRefs: Iterable<string>,
|
|
188
|
+
propAliases?: ReadonlyMap<string, string>,
|
|
180
189
|
): string {
|
|
181
190
|
const { protect, restore } = createTemplateAwareStringProtector()
|
|
182
191
|
let result = protect(text)
|
|
183
192
|
|
|
184
193
|
for (const propName of propRefs) {
|
|
194
|
+
// `_p` is always keyed by the caller-facing name (#2524 CSR half).
|
|
195
|
+
const callerKey = propAliases?.get(propName) ?? propName
|
|
185
196
|
const pattern = new RegExp(`(?<!${PROPS_PARAM}\\.)(?<!['"\\w.-])\\b${propName}\\b(?![a-zA-Z0-9_$])`, 'g')
|
|
186
197
|
result = result.replace(pattern, (match, offset, str) => {
|
|
187
198
|
// Skip object literal keys: preceded by { or , and followed by :
|
|
@@ -190,7 +201,7 @@ export function applyRegexPropRefRewrite(
|
|
|
190
201
|
const before = str.slice(0, offset)
|
|
191
202
|
if (/[{,]\s*$/.test(before)) return match
|
|
192
203
|
}
|
|
193
|
-
return `${PROPS_PARAM}.${
|
|
204
|
+
return `${PROPS_PARAM}.${callerKey}`
|
|
194
205
|
})
|
|
195
206
|
}
|
|
196
207
|
|
|
@@ -209,12 +220,18 @@ export function applyRegexPropRefRewrite(
|
|
|
209
220
|
* `text` was produced by inlining a branch-local whose initializer
|
|
210
221
|
* references the prop). The rewrite only touches genuine value
|
|
211
222
|
* references, so passing an over-broad set is safe.
|
|
223
|
+
* @param propAliases - Local name → caller-facing key (`sourceName ?? name`)
|
|
224
|
+
* for aliased destructured props (`{ n: count }` → `count` → `n`).
|
|
225
|
+
* `_p` is always keyed by the caller-facing name (#2524 CSR half); a name
|
|
226
|
+
* absent from this map emits `_p.<name>` unchanged (the un-aliased case,
|
|
227
|
+
* where `sourceName ?? name` is an identity).
|
|
212
228
|
*/
|
|
213
229
|
export function rewriteBarePropRefs(
|
|
214
230
|
text: string,
|
|
215
231
|
node: ts.Node,
|
|
216
232
|
propNames: Set<string>,
|
|
217
233
|
extraPropRefs?: ReadonlySet<string>,
|
|
234
|
+
propAliases?: ReadonlyMap<string, string>,
|
|
218
235
|
): string | undefined {
|
|
219
236
|
// Walk AST to find which prop names are actually used as value references
|
|
220
237
|
const foundPropRefs = new Set<string>()
|
|
@@ -225,5 +242,8 @@ export function rewriteBarePropRefs(
|
|
|
225
242
|
}
|
|
226
243
|
}
|
|
227
244
|
if (foundPropRefs.size === 0) return undefined
|
|
228
|
-
return
|
|
245
|
+
return (
|
|
246
|
+
applyScopedPropRefRewrite(text, foundPropRefs, propAliases) ??
|
|
247
|
+
applyRegexPropRefRewrite(text, foundPropRefs, propAliases)
|
|
248
|
+
)
|
|
229
249
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
import type { ParamInfo } from './types.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Authoritative IdentifierName classification for a destructure-pattern
|
|
6
|
+
* property key, built on TS's own `isIdentifierStart` / `isIdentifierPart`
|
|
7
|
+
* primitives (Unicode-aware, stays aligned with what TS itself accepts as
|
|
8
|
+
* a bare property key). Mirrors the `isIdent` precedent in
|
|
9
|
+
* `jsx-to-ir.ts` (#1244) — a source key like `data-key` or `aria-label`
|
|
10
|
+
* can't be emitted as a bare `key: local` destructure and must be quoted
|
|
11
|
+
* (`"data-key": local`).
|
|
12
|
+
*/
|
|
13
|
+
export function isIdentifierName(key: string): boolean {
|
|
14
|
+
if (key.length === 0) return false
|
|
15
|
+
for (let i = 0; i < key.length; ) {
|
|
16
|
+
const cp = key.codePointAt(i)!
|
|
17
|
+
const ok = i === 0
|
|
18
|
+
? ts.isIdentifierStart(cp, ts.ScriptTarget.Latest)
|
|
19
|
+
: ts.isIdentifierPart(cp, ts.ScriptTarget.Latest)
|
|
20
|
+
if (!ok) return false
|
|
21
|
+
i += cp > 0xFFFF ? 2 : 1
|
|
22
|
+
}
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The single destructure-binding renderer for a props param, shared by
|
|
28
|
+
* every JSX-runtime SSR adapter (Hono, TestAdapter). The caller-facing
|
|
29
|
+
* key is `sourceName ?? name` (ParamInfo's own rule) — `name` is only
|
|
30
|
+
* ever the LOCAL binding. Emits the plain shorthand when they match
|
|
31
|
+
* (byte-identical to the pre-rename-aware form); emits a `key: local`
|
|
32
|
+
* rename otherwise (b4f5075). This also covers the `class` → `className`
|
|
33
|
+
* rename: a source prop literally named `class` can only reach
|
|
34
|
+
* `propsParams` via an aliased destructure (`{ class: className }` —
|
|
35
|
+
* `class` is a reserved word, so it can never be an un-aliased binding),
|
|
36
|
+
* which sets `sourceName: 'class'` and takes the rename branch
|
|
37
|
+
* (`class: className`), not a bare `className`.
|
|
38
|
+
*
|
|
39
|
+
* One exported implementation, two consumers, zero drift — the
|
|
40
|
+
* hono/test-adapter pair carrying private copies is exactly the
|
|
41
|
+
* lockstep-rule duplication #2460/#2524 were about.
|
|
42
|
+
*/
|
|
43
|
+
export function propsDestructureBinding(p: ParamInfo): string {
|
|
44
|
+
const callerKey = p.sourceName ?? p.name
|
|
45
|
+
const localName = p.name
|
|
46
|
+
const binding = callerKey === localName
|
|
47
|
+
? localName
|
|
48
|
+
: `${isIdentifierName(callerKey) ? callerKey : JSON.stringify(callerKey)}: ${localName}`
|
|
49
|
+
return p.defaultValue ? `${binding} = ${p.defaultValue}` : binding
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Local-name → caller-facing-key map for prop-reference rewrites —
|
|
54
|
+
* entries only for `ParamInfo`s that actually rename (`sourceName` set,
|
|
55
|
+
* see its docstring in `types.ts`). `_p` is always keyed by the
|
|
56
|
+
* caller-facing name (#2524 CSR half); an un-aliased prop leaves no
|
|
57
|
+
* entry, so `map?.get(name) ?? name` degrades to an identity there.
|
|
58
|
+
* Returns `undefined` when nothing renames, so callers can
|
|
59
|
+
* short-circuit.
|
|
60
|
+
*/
|
|
61
|
+
export function buildPropAliasMap(params: readonly ParamInfo[]): Map<string, string> | undefined {
|
|
62
|
+
let map: Map<string, string> | undefined
|
|
63
|
+
for (const p of params) {
|
|
64
|
+
if (p.sourceName) {
|
|
65
|
+
if (!map) map = new Map()
|
|
66
|
+
map.set(p.name, p.sourceName)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return map
|
|
70
|
+
}
|
package/src/relocate.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { Scope, BindingKind, IRMetadata } from './types.ts'
|
|
|
15
15
|
import { isVisibleIn } from './types.ts'
|
|
16
16
|
import type { AnalyzerContext } from './analyzer-context.ts'
|
|
17
17
|
import { PROPS_PARAM } from './ir-to-client-js/utils.ts'
|
|
18
|
+
import { buildPropAliasMap } from './props-binding.ts'
|
|
18
19
|
import type {
|
|
19
20
|
TemplatePrimitiveRegistry,
|
|
20
21
|
TemplateCallAcceptor,
|
|
@@ -42,6 +43,15 @@ export interface RelocateEnv {
|
|
|
42
43
|
* `TransformContext._destructuredPropNames`.
|
|
43
44
|
*/
|
|
44
45
|
propsForLift: Set<string>
|
|
46
|
+
/**
|
|
47
|
+
* Local prop name → caller-facing key (`sourceName ?? name`), entries
|
|
48
|
+
* only for `ParamInfo`s that rename (`{ n: count }` → `count` → `n`).
|
|
49
|
+
* `_p` is always keyed by the caller-facing name (#2524 CSR half) — the
|
|
50
|
+
* `lift-to-prop` action reads this so `count` lifts to `_p.n`, not
|
|
51
|
+
* `_p.count`. A name absent from this map is un-aliased, so
|
|
52
|
+
* `propSourceNames.get(name) ?? name` degrades to an identity there.
|
|
53
|
+
*/
|
|
54
|
+
propSourceNames: ReadonlyMap<string, string>
|
|
45
55
|
/**
|
|
46
56
|
* Name of the props parameter (e.g. `props`). Used to detect
|
|
47
57
|
* `props.X` member access at lift sites — those are not free refs
|
|
@@ -179,8 +189,10 @@ function decideAction(
|
|
|
179
189
|
if (env.propsObjectName !== null && name === env.propsObjectName) {
|
|
180
190
|
return { action: 'lift-to-prop', rewrittenAs: PROPS_PARAM }
|
|
181
191
|
}
|
|
182
|
-
// Lift `name` → `_p
|
|
183
|
-
|
|
192
|
+
// Lift `name` → `_p.<caller-facing key>` — `_p` is always keyed by
|
|
193
|
+
// the caller-facing name (#2524 CSR half), not the local binding.
|
|
194
|
+
const callerKey = env.propSourceNames.get(name) ?? name
|
|
195
|
+
return { action: 'lift-to-prop', rewrittenAs: `${PROPS_PARAM}.${callerKey}` }
|
|
184
196
|
}
|
|
185
197
|
|
|
186
198
|
if ((kind === 'init-local' || kind === 'sub-init-local') && toScope === 'template') {
|
|
@@ -860,6 +872,10 @@ function buildRelocateEnvFromFields(src: EnvFields): RelocateEnv {
|
|
|
860
872
|
if (kind === 'prop') propsForLift.add(name)
|
|
861
873
|
}
|
|
862
874
|
|
|
875
|
+
// propSourceNames: local prop name → caller-facing key, entries only
|
|
876
|
+
// for `ParamInfo`s that actually rename. See `RelocateEnv.propSourceNames`.
|
|
877
|
+
const propSourceNames = buildPropAliasMap(src.propsParams) ?? new Map<string, string>()
|
|
878
|
+
|
|
863
879
|
// aliasTargets (#2069 R2): one-hop alias resolution table for
|
|
864
880
|
// `isCallAcceptedByAdapter`. A const whose FINAL resolved binding kind
|
|
865
881
|
// is `init-local` or `module-local` (i.e. not a signal/memo/prop-alias
|
|
@@ -884,6 +900,7 @@ function buildRelocateEnvFromFields(src: EnvFields): RelocateEnv {
|
|
|
884
900
|
bindings,
|
|
885
901
|
inlinable: new Map(), // populated by compute-inlinability after analyzer runs
|
|
886
902
|
propsForLift,
|
|
903
|
+
propSourceNames,
|
|
887
904
|
propsObjectName,
|
|
888
905
|
allowFallback: true,
|
|
889
906
|
aliasTargets,
|
package/src/ssr-defaults.ts
CHANGED
|
@@ -62,6 +62,76 @@ export interface SsrDefault {
|
|
|
62
62
|
isRestProps?: boolean
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* TS twin of the runtime `derive*FromDefaults` family that ships in every
|
|
67
|
+
* OTHER SSR runtime port (Ruby's `BarefootJS::Context.derive_vars_from_defaults`
|
|
68
|
+
* — `packages/adapter-erb/lib/barefoot_js.rb:337-360` — plus Python's
|
|
69
|
+
* `barefootjs.runtime._derive_stash_from_defaults`, PHP's
|
|
70
|
+
* `Barefoot\BarefootJS::deriveStashFromDefaults`, Perl's
|
|
71
|
+
* `BarefootJS::_derive_stash_from_defaults`, and Rust's
|
|
72
|
+
* `barefootjs::manifest::derive_stash_from_defaults`). TypeScript had no such
|
|
73
|
+
* function — every adapter-tests conformance harness (and 3 production
|
|
74
|
+
* integration sites) either discarded `SsrDefault.propName` outright or keyed
|
|
75
|
+
* its seeding loop off the LOCAL template-var name instead of the
|
|
76
|
+
* caller-facing prop key, which is exactly the #2157 defect class (and its
|
|
77
|
+
* #2524 SSR-seeding recurrence) restated at
|
|
78
|
+
* `packages/adapter-erb/src/test-render.ts:276-287`. Any harness or
|
|
79
|
+
* integration deriving template-stash vars from an `extractSsrDefaults(...)`
|
|
80
|
+
* map MUST route through this function (or its runtime-language twin, when
|
|
81
|
+
* one is reachable) instead of hand-flattening `SsrDefault.value` and
|
|
82
|
+
* merging raw caller props over it — that flattening is precisely what
|
|
83
|
+
* silently drops the rename.
|
|
84
|
+
*
|
|
85
|
+
* Semantics (mirrors `derive_vars_from_defaults`'s observable behavior,
|
|
86
|
+
* including its edge cases — though not always the identical mechanism;
|
|
87
|
+
* e.g. Python's `_derive_stash_from_defaults` checks `props.get(prop_name)
|
|
88
|
+
* is not None` with no separate `in` membership test, relying on `dict.get`
|
|
89
|
+
* defaulting a missing key to `None` — behaviorally identical to this
|
|
90
|
+
* function's explicit `propName in props && props[propName] != null` for a
|
|
91
|
+
* plain dict/object, just expressed differently):
|
|
92
|
+
* - A non-object entry (a bare JSON value some callers may still pass,
|
|
93
|
+
* e.g. a manifest round-tripped through a generic JSON domain) is used
|
|
94
|
+
* AS-IS.
|
|
95
|
+
* - `isRestProps` entries: prefer `props[<this entry's own key>]` when the
|
|
96
|
+
* caller supplied one (checked via `in`, so an explicit `undefined` /
|
|
97
|
+
* `null` value still counts as "supplied" — the rest bag is a
|
|
98
|
+
* caller-assembled aggregate, not a single scalar with a meaningful
|
|
99
|
+
* "absent" state), else the static `value` fallback (normally `{}`).
|
|
100
|
+
* - Otherwise: prefer `props[propName]` when `propName` is set AND the
|
|
101
|
+
* caller supplied a NON-NULLISH (`!= null`, so `undefined` and `null`
|
|
102
|
+
* both fall through — mirrors every other port's "present and defined"
|
|
103
|
+
* check) value for it, else the static `value` fallback. `propName`-less
|
|
104
|
+
* entries (signal / memo locals) always use the static value — the
|
|
105
|
+
* caller cannot override them by construction.
|
|
106
|
+
*/
|
|
107
|
+
export function deriveStashFromDefaults(
|
|
108
|
+
defaults: Record<string, SsrDefault>,
|
|
109
|
+
props: Record<string, unknown>,
|
|
110
|
+
): Record<string, unknown> {
|
|
111
|
+
const extra: Record<string, unknown> = {}
|
|
112
|
+
for (const [name, d] of Object.entries(defaults)) {
|
|
113
|
+
if (d === null || typeof d !== 'object') {
|
|
114
|
+
// Defensive: every ENTRY `extractSsrDefaults` itself emits is always
|
|
115
|
+
// the `{ value, propName?, isRestProps? }` shape, but a caller may
|
|
116
|
+
// feed this a manifest round-tripped through a generic JSON domain
|
|
117
|
+
// (mirrors every runtime port's own `ref($d) eq 'HASH'` /
|
|
118
|
+
// `d.is_a?(Hash)` / `isinstance(d, dict)` guard).
|
|
119
|
+
extra[name] = d
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
if (d.isRestProps) {
|
|
123
|
+
extra[name] = name in props ? props[name] : d.value
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
if (d.propName !== undefined && d.propName in props && props[d.propName] != null) {
|
|
127
|
+
extra[name] = props[d.propName]
|
|
128
|
+
} else {
|
|
129
|
+
extra[name] = d.value
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return extra
|
|
133
|
+
}
|
|
134
|
+
|
|
65
135
|
const UNRESOLVED = Symbol('unresolved')
|
|
66
136
|
type EvalResult = unknown | typeof UNRESOLVED
|
|
67
137
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single renderer for a structured `template` variant's parts back into
|
|
3
|
+
* JS template-literal source.
|
|
4
|
+
*
|
|
5
|
+
* Three sites used to carry byte-identical copies of this loop — the IR-time
|
|
6
|
+
* component-prop collapse (`jsx-to-ir.ts`), the client-JS emitter
|
|
7
|
+
* (`ir-to-client-js/utils.ts`), and the JSX adapters' attribute renderer. They
|
|
8
|
+
* have to agree: the collapse's output is what a JSX adapter emits verbatim
|
|
9
|
+
* for a component prop, so a divergence between any two of them is a silent
|
|
10
|
+
* SSR/CSR mismatch. One door, three callers.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { IRTemplatePart } from './types.ts'
|
|
14
|
+
|
|
15
|
+
export interface TemplatePartsToJsOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Prefer each part's prop-rewritten projection (`templateValue` /
|
|
18
|
+
* `templateCondition` / `templateKey`, i.e. destructured props rewritten
|
|
19
|
+
* to `_p.X`) when present. Used by the client-JS / module-registration
|
|
20
|
+
* template emitters, which run outside the component's destructured scope.
|
|
21
|
+
*/
|
|
22
|
+
useTemplate?: boolean
|
|
23
|
+
/**
|
|
24
|
+
* Emit TypeScript type annotations. Only adapters whose output is
|
|
25
|
+
* type-checked as .tsx set this — see `JsxAdapterConfig.preserveTypes`.
|
|
26
|
+
* The neutral (untyped) form is what DSL adapters' expression pipelines
|
|
27
|
+
* and the client-JS bundle consume, so it must stay plain JS.
|
|
28
|
+
*/
|
|
29
|
+
typed?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Render one `lookup` part — `${MAP[KEY]}` structurally captured at IR time
|
|
34
|
+
* so DSL adapters can emit a switch — as the equivalent runtime indexed
|
|
35
|
+
* access against the resolved cases.
|
|
36
|
+
*
|
|
37
|
+
* Under `typed`, the inlined object literal is annotated
|
|
38
|
+
* `as Record<string, string>` (#2565). The IR's `key` is the TYPE-STRIPPED
|
|
39
|
+
* source text of the index expression, so a narrowing assertion written at
|
|
40
|
+
* the use site — `strokePaths[name as keyof typeof strokePaths]` — is already
|
|
41
|
+
* gone by the time the record's cases are folded in here. That leaves the
|
|
42
|
+
* literal's exact key set indexed by the binding's unnarrowed union, which
|
|
43
|
+
* fails TS7053 ("expression of type 'IconName' can't be used to index type
|
|
44
|
+
* '{ check: string; … }'") in any consumer that type-checks its compiled
|
|
45
|
+
* templates. Widening the literal to a string index signature restores the
|
|
46
|
+
* assertion's effect without reconstructing its text, which may name types
|
|
47
|
+
* the emitted template never declares (`keyof typeof strokePaths` where
|
|
48
|
+
* `strokePaths` was localised into a component body). Purely a type-level
|
|
49
|
+
* annotation — the runtime expression is identical either way.
|
|
50
|
+
*/
|
|
51
|
+
export function lookupPartToJsExpr(
|
|
52
|
+
part: Extract<IRTemplatePart, { type: 'lookup' }>,
|
|
53
|
+
opts?: TemplatePartsToJsOptions,
|
|
54
|
+
): string {
|
|
55
|
+
const key = (opts?.useTemplate && part.templateKey) ? part.templateKey : part.key
|
|
56
|
+
const obj = '{' + Object.entries(part.cases).map(
|
|
57
|
+
([k, v]) => `${JSON.stringify(k)}: ${JSON.stringify(v)}`
|
|
58
|
+
).join(', ') + '}'
|
|
59
|
+
const typed = opts?.typed ? ' as Record<string, string>' : ''
|
|
60
|
+
return `(${obj}${typed})[${key}]`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Convert a `template` variant's parts into a JS template-literal string. */
|
|
64
|
+
export function templatePartsToJsExpr(
|
|
65
|
+
parts: readonly IRTemplatePart[],
|
|
66
|
+
opts?: TemplatePartsToJsOptions,
|
|
67
|
+
): string {
|
|
68
|
+
let result = '`'
|
|
69
|
+
for (const part of parts) {
|
|
70
|
+
if (part.type === 'string') {
|
|
71
|
+
result += (opts?.useTemplate && part.templateValue) ? part.templateValue : part.value
|
|
72
|
+
} else if (part.type === 'ternary') {
|
|
73
|
+
const cond = (opts?.useTemplate && part.templateCondition) ? part.templateCondition : part.condition
|
|
74
|
+
result += `\${${cond} ? '${part.whenTrue}' : '${part.whenFalse}'}`
|
|
75
|
+
} else if (part.type === 'lookup') {
|
|
76
|
+
result += `\${${lookupPartToJsExpr(part, opts)}}`
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
result += '`'
|
|
80
|
+
return result
|
|
81
|
+
}
|