@barefootjs/test 0.17.0 → 0.17.1
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/index.js +77 -36
- package/dist/ir-to-test-node.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/ir-to-test-node.ts +124 -32
package/dist/index.js
CHANGED
|
@@ -187348,6 +187348,7 @@ function tsNodeToParsedExpr(node) {
|
|
|
187348
187348
|
}
|
|
187349
187349
|
var CALLBACK_METHODS = new Set([
|
|
187350
187350
|
"filter",
|
|
187351
|
+
"map",
|
|
187351
187352
|
"every",
|
|
187352
187353
|
"some",
|
|
187353
187354
|
"find",
|
|
@@ -195464,6 +195465,9 @@ var JS_BUILTINS = new Set([
|
|
|
195464
195465
|
var ENV_SIGNAL_CLIENT_FACTORY = {
|
|
195465
195466
|
search: "createSearchParams"
|
|
195466
195467
|
};
|
|
195468
|
+
var ENV_SIGNAL_READERS = new Map([
|
|
195469
|
+
["search", { key: "search", canonicalName: "searchParams", methods: new Set(["get"]) }]
|
|
195470
|
+
]);
|
|
195467
195471
|
function queryHrefLocalNames(metadata) {
|
|
195468
195472
|
const names = new Set;
|
|
195469
195473
|
for (const imp of metadata.imports) {
|
|
@@ -195668,9 +195672,12 @@ var import_typescript15 = __toESM(require_typescript(), 1);
|
|
|
195668
195672
|
var import_typescript16 = __toESM(require_typescript(), 1);
|
|
195669
195673
|
var UNRESOLVED = Symbol("unresolved");
|
|
195670
195674
|
var NO_RETURN = Symbol("no-return");
|
|
195675
|
+
|
|
195676
|
+
// ../jsx/src/augment-inherited-props.ts
|
|
195677
|
+
var import_typescript17 = __toESM(require_typescript(), 1);
|
|
195671
195678
|
// ../jsx/src/shared-program.ts
|
|
195672
195679
|
init_path();
|
|
195673
|
-
var
|
|
195680
|
+
var import_typescript18 = __toESM(require_typescript(), 1);
|
|
195674
195681
|
// ../jsx/src/adapters/interface.ts
|
|
195675
195682
|
class BaseAdapter {
|
|
195676
195683
|
renderChildren(children) {
|
|
@@ -196184,9 +196191,9 @@ function registerBuiltinLoweringPlugins() {
|
|
|
196184
196191
|
registerLoweringPlugin(plugin);
|
|
196185
196192
|
}
|
|
196186
196193
|
// ../jsx/src/combine-client-js.ts
|
|
196187
|
-
var import_typescript18 = __toESM(require_typescript(), 1);
|
|
196188
|
-
// ../jsx/src/debug.ts
|
|
196189
196194
|
var import_typescript19 = __toESM(require_typescript(), 1);
|
|
196195
|
+
// ../jsx/src/debug.ts
|
|
196196
|
+
var import_typescript20 = __toESM(require_typescript(), 1);
|
|
196190
196197
|
function escapeForIdBoundary(name) {
|
|
196191
196198
|
return name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
196192
196199
|
}
|
|
@@ -196278,8 +196285,6 @@ function resolveSetters(handler, setterToSignal, fnSetters) {
|
|
|
196278
196285
|
return refs;
|
|
196279
196286
|
}
|
|
196280
196287
|
// ../jsx/src/profiler.ts
|
|
196281
|
-
var import_typescript20 = __toESM(require_typescript(), 1);
|
|
196282
|
-
// ../jsx/src/augment-inherited-props.ts
|
|
196283
196288
|
var import_typescript21 = __toESM(require_typescript(), 1);
|
|
196284
196289
|
|
|
196285
196290
|
// ../jsx/src/index.ts
|
|
@@ -196378,6 +196383,7 @@ function irNodeToTestNode(node, constantMap, metadata) {
|
|
|
196378
196383
|
const cmap = constantMap ?? new Map;
|
|
196379
196384
|
const setterToSignal = new Map;
|
|
196380
196385
|
const fnSetters = new Map;
|
|
196386
|
+
const defaults = new Map;
|
|
196381
196387
|
if (metadata) {
|
|
196382
196388
|
for (const s of metadata.signals) {
|
|
196383
196389
|
if (s.setter)
|
|
@@ -196386,8 +196392,24 @@ function irNodeToTestNode(node, constantMap, metadata) {
|
|
|
196386
196392
|
for (const [k, v] of buildLocalFunctionSetterMap(metadata, setterToSignal)) {
|
|
196387
196393
|
fnSetters.set(k, v);
|
|
196388
196394
|
}
|
|
196395
|
+
for (const p of metadata.propsParams) {
|
|
196396
|
+
const lit = p.defaultValue !== undefined ? parseLiteralDefault(p.defaultValue) : null;
|
|
196397
|
+
if (lit !== null)
|
|
196398
|
+
defaults.set(p.name, lit);
|
|
196399
|
+
}
|
|
196400
|
+
}
|
|
196401
|
+
return convert(node, { cmap, setterToSignal, fnSetters, defaults });
|
|
196402
|
+
}
|
|
196403
|
+
function parseLiteralDefault(js) {
|
|
196404
|
+
const v = js.trim();
|
|
196405
|
+
if (v.startsWith("'") && v.endsWith("'") && !v.slice(1, -1).includes("'") || v.startsWith('"') && v.endsWith('"') && !v.slice(1, -1).includes('"')) {
|
|
196406
|
+
return v.slice(1, -1);
|
|
196389
196407
|
}
|
|
196390
|
-
|
|
196408
|
+
if (/^-?\d+(\.\d+)?$/.test(v))
|
|
196409
|
+
return v;
|
|
196410
|
+
if (v === "true" || v === "false")
|
|
196411
|
+
return v;
|
|
196412
|
+
return null;
|
|
196391
196413
|
}
|
|
196392
196414
|
function convert(node, ctx) {
|
|
196393
196415
|
switch (node.type) {
|
|
@@ -196396,7 +196418,7 @@ function convert(node, ctx) {
|
|
|
196396
196418
|
case "text":
|
|
196397
196419
|
return convertText(node);
|
|
196398
196420
|
case "expression":
|
|
196399
|
-
return convertExpression(node);
|
|
196421
|
+
return convertExpression(node, ctx);
|
|
196400
196422
|
case "conditional":
|
|
196401
196423
|
return convertConditional(node, ctx);
|
|
196402
196424
|
case "loop":
|
|
@@ -196426,19 +196448,9 @@ function convertElement(node, ctx) {
|
|
|
196426
196448
|
let dataState = null;
|
|
196427
196449
|
let classes = [];
|
|
196428
196450
|
for (const attr of node.attrs) {
|
|
196429
|
-
const value = resolveAttrValue(attr);
|
|
196451
|
+
const value = resolveAttrValue(attr, ctx);
|
|
196430
196452
|
if (attr.name === "className" || attr.name === "class") {
|
|
196431
|
-
|
|
196432
|
-
const isDynamic = attr.value.kind === "expression" || attr.value.kind === "template" || attr.value.kind === "spread";
|
|
196433
|
-
if (isDynamic && ctx.cmap.size > 0) {
|
|
196434
|
-
const resolved = resolveClassValue(value, ctx.cmap);
|
|
196435
|
-
if (resolved !== null) {
|
|
196436
|
-
classes = resolved.split(/\s+/).filter(Boolean);
|
|
196437
|
-
continue;
|
|
196438
|
-
}
|
|
196439
|
-
}
|
|
196440
|
-
classes = value.split(/\s+/).filter(Boolean);
|
|
196441
|
-
}
|
|
196453
|
+
classes = collectClassTokens(attr.value, value, ctx);
|
|
196442
196454
|
continue;
|
|
196443
196455
|
}
|
|
196444
196456
|
if (attr.name === "role") {
|
|
@@ -196501,12 +196513,13 @@ function convertText(node) {
|
|
|
196501
196513
|
componentName: null
|
|
196502
196514
|
});
|
|
196503
196515
|
}
|
|
196504
|
-
function convertExpression(node) {
|
|
196516
|
+
function convertExpression(node, ctx) {
|
|
196517
|
+
const text = ctx.defaults.get(node.expr.trim()) ?? node.expr;
|
|
196505
196518
|
return new TestNode({
|
|
196506
196519
|
tag: null,
|
|
196507
196520
|
type: "expression",
|
|
196508
196521
|
children: [],
|
|
196509
|
-
text
|
|
196522
|
+
text,
|
|
196510
196523
|
props: {},
|
|
196511
196524
|
classes: [],
|
|
196512
196525
|
role: null,
|
|
@@ -196571,7 +196584,7 @@ function convertComponent(node, ctx) {
|
|
|
196571
196584
|
props[prop.name] = prop.value.expr;
|
|
196572
196585
|
break;
|
|
196573
196586
|
case "template":
|
|
196574
|
-
props[prop.name] = resolveTemplateAttr(prop.value);
|
|
196587
|
+
props[prop.name] = resolveTemplateAttr(prop.value, ctx);
|
|
196575
196588
|
break;
|
|
196576
196589
|
case "boolean-shorthand":
|
|
196577
196590
|
case "boolean-attr":
|
|
@@ -196711,41 +196724,69 @@ function refsToHandler(refs) {
|
|
|
196711
196724
|
}
|
|
196712
196725
|
return { setters, via };
|
|
196713
196726
|
}
|
|
196714
|
-
function
|
|
196715
|
-
|
|
196716
|
-
|
|
196727
|
+
function splitClassTokens(value) {
|
|
196728
|
+
return value.split(/\s+/).filter((t) => t && !t.includes("${"));
|
|
196729
|
+
}
|
|
196730
|
+
function collectClassTokens(attrValue, resolved, ctx) {
|
|
196731
|
+
if (attrValue.kind === "template") {
|
|
196732
|
+
const joined = attrValue.parts.map((part) => {
|
|
196733
|
+
if (part.type === "string")
|
|
196734
|
+
return substituteInterpolations(part.value, ctx);
|
|
196735
|
+
if (part.type === "ternary")
|
|
196736
|
+
return `${part.whenTrue} ${part.whenFalse}`;
|
|
196737
|
+
return Object.values(part.cases).join(" ");
|
|
196738
|
+
}).join("");
|
|
196739
|
+
return splitClassTokens(joined);
|
|
196740
|
+
}
|
|
196741
|
+
if (typeof resolved !== "string")
|
|
196742
|
+
return [];
|
|
196743
|
+
const isDynamic = attrValue.kind === "expression" || attrValue.kind === "spread";
|
|
196744
|
+
if (isDynamic) {
|
|
196745
|
+
const value = resolveClassValue(resolved, ctx);
|
|
196746
|
+
if (value !== null)
|
|
196747
|
+
return splitClassTokens(value);
|
|
196748
|
+
}
|
|
196749
|
+
return splitClassTokens(resolved);
|
|
196750
|
+
}
|
|
196751
|
+
function substituteInterpolations(value, ctx) {
|
|
196752
|
+
return value.replace(/\$\{([^}]+)\}/g, (raw, expr) => {
|
|
196753
|
+
const trimmed = expr.trim();
|
|
196754
|
+
return ctx.cmap.get(trimmed) ?? ctx.defaults.get(trimmed) ?? raw;
|
|
196755
|
+
});
|
|
196756
|
+
}
|
|
196757
|
+
function resolveClassValue(value, ctx) {
|
|
196758
|
+
if (ctx.cmap.has(value)) {
|
|
196759
|
+
return ctx.cmap.get(value);
|
|
196760
|
+
}
|
|
196761
|
+
if (ctx.defaults.has(value)) {
|
|
196762
|
+
return ctx.defaults.get(value);
|
|
196717
196763
|
}
|
|
196718
196764
|
if (value.startsWith("`") && value.endsWith("`")) {
|
|
196719
|
-
|
|
196720
|
-
return inner.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
196721
|
-
const trimmed = expr.trim();
|
|
196722
|
-
return cmap.get(trimmed) ?? "";
|
|
196723
|
-
});
|
|
196765
|
+
return substituteInterpolations(value.slice(1, -1), ctx);
|
|
196724
196766
|
}
|
|
196725
196767
|
return null;
|
|
196726
196768
|
}
|
|
196727
|
-
function resolveAttrValue(attr) {
|
|
196769
|
+
function resolveAttrValue(attr, ctx) {
|
|
196728
196770
|
switch (attr.value.kind) {
|
|
196729
196771
|
case "boolean-attr":
|
|
196730
196772
|
return true;
|
|
196731
196773
|
case "literal":
|
|
196732
196774
|
return attr.value.value;
|
|
196733
196775
|
case "expression":
|
|
196734
|
-
return attr.value.expr;
|
|
196735
196776
|
case "spread":
|
|
196736
|
-
return attr.value.expr;
|
|
196777
|
+
return ctx.defaults.get(attr.value.expr.trim()) ?? attr.value.expr;
|
|
196737
196778
|
case "template":
|
|
196738
|
-
return resolveTemplateAttr(attr.value);
|
|
196779
|
+
return resolveTemplateAttr(attr.value, ctx);
|
|
196739
196780
|
case "boolean-shorthand":
|
|
196740
196781
|
return true;
|
|
196741
196782
|
case "jsx-children":
|
|
196742
196783
|
return null;
|
|
196743
196784
|
}
|
|
196744
196785
|
}
|
|
196745
|
-
function resolveTemplateAttr(tl) {
|
|
196786
|
+
function resolveTemplateAttr(tl, ctx) {
|
|
196746
196787
|
return tl.parts.map((part) => {
|
|
196747
196788
|
if (part.type === "string")
|
|
196748
|
-
return part.value;
|
|
196789
|
+
return substituteInterpolations(part.value, ctx);
|
|
196749
196790
|
if (part.type === "ternary")
|
|
196750
196791
|
return `{${part.condition}}`;
|
|
196751
196792
|
return Object.values(part.cases).join(" ");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ir-to-test-node.d.ts","sourceRoot":"","sources":["../src/ir-to-test-node.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,MAAM,EAYN,UAAU,EACX,MAAM,iBAAiB,CAAA;AAMxB,OAAO,EAAE,QAAQ,EAAqB,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"ir-to-test-node.d.ts","sourceRoot":"","sources":["../src/ir-to-test-node.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,MAAM,EAYN,UAAU,EACX,MAAM,iBAAiB,CAAA;AAMxB,OAAO,EAAE,QAAQ,EAAqB,MAAM,gBAAgB,CAAA;AAU5D,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,CAAC,EAAE,UAAU,GAAG,QAAQ,CAuBjH"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/test",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"description": "Test utilities for BarefootJS - IR-based component testing without a browser",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"directory": "packages/test"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@barefootjs/jsx": "0.17.
|
|
42
|
+
"@barefootjs/jsx": "0.17.1"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"typescript": "^5.0.0"
|
package/src/ir-to-test-node.ts
CHANGED
|
@@ -30,12 +30,15 @@ interface ConvertContext {
|
|
|
30
30
|
cmap: Map<string, string>
|
|
31
31
|
setterToSignal: Map<string, string>
|
|
32
32
|
fnSetters: Map<string, FnSetterResolution[]>
|
|
33
|
+
/** Prop name → literal destructure-default value (`{ size = 'md' }` → `size: 'md'`). */
|
|
34
|
+
defaults: Map<string, string>
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
export function irNodeToTestNode(node: IRNode, constantMap?: Map<string, string>, metadata?: IRMetadata): TestNode {
|
|
36
38
|
const cmap = constantMap ?? new Map<string, string>()
|
|
37
39
|
const setterToSignal = new Map<string, string>()
|
|
38
40
|
const fnSetters = new Map<string, FnSetterResolution[]>()
|
|
41
|
+
const defaults = new Map<string, string>()
|
|
39
42
|
if (metadata) {
|
|
40
43
|
for (const s of metadata.signals) {
|
|
41
44
|
if (s.setter) setterToSignal.set(s.setter, s.getter)
|
|
@@ -43,8 +46,35 @@ export function irNodeToTestNode(node: IRNode, constantMap?: Map<string, string>
|
|
|
43
46
|
for (const [k, v] of buildLocalFunctionSetterMap(metadata, setterToSignal)) {
|
|
44
47
|
fnSetters.set(k, v)
|
|
45
48
|
}
|
|
49
|
+
// renderToTest models the component compiled with NO incoming props,
|
|
50
|
+
// so a literal destructure default IS the statically-known value of
|
|
51
|
+
// that prop (#2069). Non-literal defaults (arrows, objects, computed
|
|
52
|
+
// expressions) stay unresolved — surfacing a stale expression string
|
|
53
|
+
// would be worse than the raw reference.
|
|
54
|
+
for (const p of metadata.propsParams) {
|
|
55
|
+
const lit = p.defaultValue !== undefined ? parseLiteralDefault(p.defaultValue) : null
|
|
56
|
+
if (lit !== null) defaults.set(p.name, lit)
|
|
57
|
+
}
|
|
46
58
|
}
|
|
47
|
-
return convert(node, { cmap, setterToSignal, fnSetters })
|
|
59
|
+
return convert(node, { cmap, setterToSignal, fnSetters, defaults })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parse a `ParamInfo.defaultValue` JS-text into a plain string when it is
|
|
64
|
+
* a self-contained literal: quoted string, number, or boolean. Returns
|
|
65
|
+
* null for anything else.
|
|
66
|
+
*/
|
|
67
|
+
function parseLiteralDefault(js: string): string | null {
|
|
68
|
+
const v = js.trim()
|
|
69
|
+
if (
|
|
70
|
+
(v.startsWith("'") && v.endsWith("'") && !v.slice(1, -1).includes("'")) ||
|
|
71
|
+
(v.startsWith('"') && v.endsWith('"') && !v.slice(1, -1).includes('"'))
|
|
72
|
+
) {
|
|
73
|
+
return v.slice(1, -1)
|
|
74
|
+
}
|
|
75
|
+
if (/^-?\d+(\.\d+)?$/.test(v)) return v
|
|
76
|
+
if (v === 'true' || v === 'false') return v
|
|
77
|
+
return null
|
|
48
78
|
}
|
|
49
79
|
|
|
50
80
|
function convert(node: IRNode, ctx: ConvertContext): TestNode {
|
|
@@ -54,7 +84,7 @@ function convert(node: IRNode, ctx: ConvertContext): TestNode {
|
|
|
54
84
|
case 'text':
|
|
55
85
|
return convertText(node)
|
|
56
86
|
case 'expression':
|
|
57
|
-
return convertExpression(node)
|
|
87
|
+
return convertExpression(node, ctx)
|
|
58
88
|
case 'conditional':
|
|
59
89
|
return convertConditional(node, ctx)
|
|
60
90
|
case 'loop':
|
|
@@ -90,20 +120,10 @@ function convertElement(node: IRElement, ctx: ConvertContext): TestNode {
|
|
|
90
120
|
let classes: string[] = []
|
|
91
121
|
|
|
92
122
|
for (const attr of node.attrs) {
|
|
93
|
-
const value = resolveAttrValue(attr)
|
|
123
|
+
const value = resolveAttrValue(attr, ctx)
|
|
94
124
|
|
|
95
125
|
if (attr.name === 'className' || attr.name === 'class') {
|
|
96
|
-
|
|
97
|
-
const isDynamic = attr.value.kind === 'expression' || attr.value.kind === 'template' || attr.value.kind === 'spread'
|
|
98
|
-
if (isDynamic && ctx.cmap.size > 0) {
|
|
99
|
-
const resolved = resolveClassValue(value, ctx.cmap)
|
|
100
|
-
if (resolved !== null) {
|
|
101
|
-
classes = resolved.split(/\s+/).filter(Boolean)
|
|
102
|
-
continue
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
classes = value.split(/\s+/).filter(Boolean)
|
|
106
|
-
}
|
|
126
|
+
classes = collectClassTokens(attr.value, value, ctx)
|
|
107
127
|
continue
|
|
108
128
|
}
|
|
109
129
|
|
|
@@ -178,12 +198,20 @@ function convertText(node: IRText): TestNode {
|
|
|
178
198
|
// Expression
|
|
179
199
|
// ---------------------------------------------------------------------------
|
|
180
200
|
|
|
181
|
-
function convertExpression(node: IRExpression): TestNode {
|
|
201
|
+
function convertExpression(node: IRExpression, ctx: ConvertContext): TestNode {
|
|
202
|
+
// A bare reference to a defaulted prop (`<div>{label}</div>` with
|
|
203
|
+
// `{ label = 'Hello' }`) resolves to its literal default, so
|
|
204
|
+
// `findByText('Hello')` sees the zero-props render (#2069). Prop refs
|
|
205
|
+
// are flagged reactive (they update on parent re-render), so this
|
|
206
|
+
// keys off defaults-map membership, not the reactive flag: signal /
|
|
207
|
+
// memo reads are call expressions (`count()`), never bare identifiers,
|
|
208
|
+
// and keep their source text — wiring is the assertion surface there.
|
|
209
|
+
const text = ctx.defaults.get(node.expr.trim()) ?? node.expr
|
|
182
210
|
return new TestNode({
|
|
183
211
|
tag: null,
|
|
184
212
|
type: 'expression',
|
|
185
213
|
children: [],
|
|
186
|
-
text
|
|
214
|
+
text,
|
|
187
215
|
props: {},
|
|
188
216
|
classes: [],
|
|
189
217
|
role: null,
|
|
@@ -265,7 +293,7 @@ function convertComponent(node: IRComponent, ctx: ConvertContext): TestNode {
|
|
|
265
293
|
props[prop.name] = prop.value.expr
|
|
266
294
|
break
|
|
267
295
|
case 'template':
|
|
268
|
-
props[prop.name] = resolveTemplateAttr(prop.value)
|
|
296
|
+
props[prop.name] = resolveTemplateAttr(prop.value, ctx)
|
|
269
297
|
break
|
|
270
298
|
case 'boolean-shorthand':
|
|
271
299
|
case 'boolean-attr':
|
|
@@ -450,19 +478,77 @@ function refsToHandler(refs: SetterRef[]): EventHandler {
|
|
|
450
478
|
return { setters, via }
|
|
451
479
|
}
|
|
452
480
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
481
|
+
/**
|
|
482
|
+
* Split a resolved className value into tokens, dropping any span that
|
|
483
|
+
* still carries an unresolved runtime interpolation (`${className}`,
|
|
484
|
+
* `foo-${x}`). Those are dynamic passthroughs the IR can't evaluate —
|
|
485
|
+
* they aren't real class tokens, and leaking them verbatim pollutes
|
|
486
|
+
* `.classes` for exact-match assertions.
|
|
487
|
+
*/
|
|
488
|
+
function splitClassTokens(value: string): string[] {
|
|
489
|
+
return value.split(/\s+/).filter(t => t && !t.includes('${'))
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Resolve a className attribute value into its class tokens.
|
|
494
|
+
*
|
|
495
|
+
* For a structured template attr the parts are walked directly so class
|
|
496
|
+
* collection keeps union semantics per part kind:
|
|
497
|
+
* - `lookup` (`${MAP[KEY]}`) → every case's tokens (PR #2000)
|
|
498
|
+
* - `ternary` (`cond ? 'on' : 'off'`) → both branches' tokens,
|
|
499
|
+
* matching the intermediate-const `valueBranches` union (#525)
|
|
500
|
+
* - `string` spans → literal text, with `${ident}` interpolations
|
|
501
|
+
* substituted from resolved consts / literal prop defaults
|
|
502
|
+
* For expression attrs the value resolves through local consts (cmap)
|
|
503
|
+
* and literal prop defaults. Anything still carrying a `${...}`
|
|
504
|
+
* interpolation is dropped by `splitClassTokens`.
|
|
505
|
+
*/
|
|
506
|
+
function collectClassTokens(attrValue: AttrValue, resolved: string | boolean | null, ctx: ConvertContext): string[] {
|
|
507
|
+
if (attrValue.kind === 'template') {
|
|
508
|
+
const joined = attrValue.parts
|
|
509
|
+
.map(part => {
|
|
510
|
+
if (part.type === 'string') return substituteInterpolations(part.value, ctx)
|
|
511
|
+
if (part.type === 'ternary') return `${part.whenTrue} ${part.whenFalse}`
|
|
512
|
+
return Object.values(part.cases).join(' ')
|
|
513
|
+
})
|
|
514
|
+
.join('')
|
|
515
|
+
return splitClassTokens(joined)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (typeof resolved !== 'string') return []
|
|
519
|
+
|
|
520
|
+
const isDynamic = attrValue.kind === 'expression' || attrValue.kind === 'spread'
|
|
521
|
+
if (isDynamic) {
|
|
522
|
+
const value = resolveClassValue(resolved, ctx)
|
|
523
|
+
if (value !== null) return splitClassTokens(value)
|
|
524
|
+
}
|
|
525
|
+
return splitClassTokens(resolved)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Replace `${ident}` interpolations with a resolved const or a literal
|
|
530
|
+
* prop default; unresolvable spans are kept verbatim so the caller's
|
|
531
|
+
* token filter can drop them.
|
|
532
|
+
*/
|
|
533
|
+
function substituteInterpolations(value: string, ctx: ConvertContext): string {
|
|
534
|
+
return value.replace(/\$\{([^}]+)\}/g, (raw, expr) => {
|
|
535
|
+
const trimmed = expr.trim()
|
|
536
|
+
return ctx.cmap.get(trimmed) ?? ctx.defaults.get(trimmed) ?? raw
|
|
537
|
+
})
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function resolveClassValue(value: string, ctx: ConvertContext): string | null {
|
|
541
|
+
// Simple identifier lookup: a local const or a literal prop default.
|
|
542
|
+
if (ctx.cmap.has(value)) {
|
|
543
|
+
return ctx.cmap.get(value)!
|
|
544
|
+
}
|
|
545
|
+
if (ctx.defaults.has(value)) {
|
|
546
|
+
return ctx.defaults.get(value)!
|
|
457
547
|
}
|
|
458
548
|
|
|
459
549
|
// Template literal: `...${var}...`
|
|
460
550
|
if (value.startsWith('`') && value.endsWith('`')) {
|
|
461
|
-
|
|
462
|
-
return inner.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
463
|
-
const trimmed = expr.trim()
|
|
464
|
-
return cmap.get(trimmed) ?? ''
|
|
465
|
-
})
|
|
551
|
+
return substituteInterpolations(value.slice(1, -1), ctx)
|
|
466
552
|
}
|
|
467
553
|
|
|
468
554
|
return null
|
|
@@ -472,18 +558,22 @@ function resolveClassValue(value: string, cmap: Map<string, string>): string | n
|
|
|
472
558
|
// Attribute value resolution
|
|
473
559
|
// ---------------------------------------------------------------------------
|
|
474
560
|
|
|
475
|
-
function resolveAttrValue(attr: IRAttribute): string | boolean | null {
|
|
561
|
+
function resolveAttrValue(attr: IRAttribute, ctx: ConvertContext): string | boolean | null {
|
|
476
562
|
switch (attr.value.kind) {
|
|
477
563
|
case 'boolean-attr':
|
|
478
564
|
return true
|
|
479
565
|
case 'literal':
|
|
480
566
|
return attr.value.value
|
|
481
567
|
case 'expression':
|
|
482
|
-
return attr.value.expr
|
|
483
568
|
case 'spread':
|
|
484
|
-
|
|
569
|
+
// A bare reference to a defaulted prop (`type={type}` with
|
|
570
|
+
// `{ type = 'button' }`) resolves to its literal default —
|
|
571
|
+
// renderToTest models the zero-props render, where the default
|
|
572
|
+
// IS the value (#2069). Anything non-bare keeps the expression
|
|
573
|
+
// text (the wiring-visible representation).
|
|
574
|
+
return ctx.defaults.get(attr.value.expr.trim()) ?? attr.value.expr
|
|
485
575
|
case 'template':
|
|
486
|
-
return resolveTemplateAttr(attr.value)
|
|
576
|
+
return resolveTemplateAttr(attr.value, ctx)
|
|
487
577
|
case 'boolean-shorthand':
|
|
488
578
|
return true
|
|
489
579
|
case 'jsx-children':
|
|
@@ -491,10 +581,12 @@ function resolveAttrValue(attr: IRAttribute): string | boolean | null {
|
|
|
491
581
|
}
|
|
492
582
|
}
|
|
493
583
|
|
|
494
|
-
function resolveTemplateAttr(tl: TemplateAttr): string {
|
|
584
|
+
function resolveTemplateAttr(tl: TemplateAttr, ctx: ConvertContext): string {
|
|
495
585
|
return tl.parts
|
|
496
586
|
.map(part => {
|
|
497
|
-
|
|
587
|
+
// `${ident}` interpolations against a literal prop default
|
|
588
|
+
// resolve like local consts do (#2069).
|
|
589
|
+
if (part.type === 'string') return substituteInterpolations(part.value, ctx)
|
|
498
590
|
if (part.type === 'ternary') return `{${part.condition}}`
|
|
499
591
|
// `lookup` (`Record<T, string>[key]`) — the test framework
|
|
500
592
|
// doesn't render against a specific key, so concatenate every
|