@barefootjs/test 0.26.0 → 0.26.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/index.js CHANGED
@@ -188803,6 +188803,15 @@ var EVAL_MATH_METHODS = new Set([
188803
188803
  "ceil",
188804
188804
  "round"
188805
188805
  ]);
188806
+ function identifierPath(callee) {
188807
+ if (callee.kind === "identifier")
188808
+ return callee.name;
188809
+ if (callee.kind === "member" && !callee.computed) {
188810
+ const head = identifierPath(callee.object);
188811
+ return head ? `${head}.${callee.property}` : null;
188812
+ }
188813
+ return null;
188814
+ }
188806
188815
 
188807
188816
  // ../jsx/src/prop-rewrite.ts
188808
188817
  var import_typescript5 = __toESM(require_typescript(), 1);
@@ -198789,12 +198798,37 @@ function collectClassTokens(attrValue, resolved, ctx) {
198789
198798
  return [];
198790
198799
  const isDynamic = attrValue.kind === "expression" || attrValue.kind === "spread";
198791
198800
  if (isDynamic) {
198801
+ if (attrValue.kind === "expression" && attrValue.parsed?.kind === "conditional") {
198802
+ const value2 = resolveParsedClass(attrValue.parsed, ctx);
198803
+ return value2 === null ? [] : splitClassTokens(value2);
198804
+ }
198792
198805
  const value = resolveClassValue(resolved, ctx);
198793
198806
  if (value !== null)
198794
198807
  return splitClassTokens(value);
198795
198808
  }
198796
198809
  return splitClassTokens(resolved);
198797
198810
  }
198811
+ function resolveParsedClass(expr, ctx) {
198812
+ switch (expr.kind) {
198813
+ case "literal":
198814
+ return expr.literalType === "string" ? String(expr.value) : null;
198815
+ case "identifier":
198816
+ return ctx.cmap.get(expr.name) ?? ctx.defaults.get(expr.name) ?? null;
198817
+ case "member": {
198818
+ const path = identifierPath(expr);
198819
+ return path === null ? null : ctx.cmap.get(path) ?? null;
198820
+ }
198821
+ case "conditional": {
198822
+ const whenTrue = resolveParsedClass(expr.consequent, ctx);
198823
+ const whenFalse = resolveParsedClass(expr.alternate, ctx);
198824
+ if (whenTrue === null && whenFalse === null)
198825
+ return null;
198826
+ return [whenTrue, whenFalse].filter((v) => v !== null).join(" ");
198827
+ }
198828
+ default:
198829
+ return null;
198830
+ }
198831
+ }
198798
198832
  function substituteInterpolations(value, ctx) {
198799
198833
  return value.replace(/\$\{([^}]+)\}/g, (raw, expr) => {
198800
198834
  const trimmed = expr.trim();
@@ -198844,6 +198878,14 @@ function resolveTemplateAttr(tl, ctx) {
198844
198878
  function resolveConstants(constants3) {
198845
198879
  const resolved = new Map;
198846
198880
  for (const c of constants3) {
198881
+ if (c.parsed?.kind === "object-literal") {
198882
+ for (const prop of c.parsed.properties) {
198883
+ const value2 = resolveObjectPropValue(prop.value, resolved);
198884
+ if (value2 !== null)
198885
+ resolved.set(`${c.name}.${prop.key}`, value2);
198886
+ }
198887
+ continue;
198888
+ }
198847
198889
  if (!c.value)
198848
198890
  continue;
198849
198891
  if (c.valueBranches) {
@@ -198868,6 +198910,31 @@ function resolveConstants(constants3) {
198868
198910
  }
198869
198911
  return resolved;
198870
198912
  }
198913
+ function resolveObjectPropValue(expr, resolved) {
198914
+ if (expr.kind === "literal" && expr.literalType === "string") {
198915
+ return String(expr.value);
198916
+ }
198917
+ if (expr.kind === "identifier") {
198918
+ return resolved.get(expr.name) ?? null;
198919
+ }
198920
+ if (expr.kind === "template-literal") {
198921
+ const strs = [];
198922
+ for (const part of expr.parts) {
198923
+ if (part.type === "string") {
198924
+ strs.push(part.value);
198925
+ continue;
198926
+ }
198927
+ if (part.expr.kind !== "identifier")
198928
+ return null;
198929
+ const value = resolved.get(part.expr.name);
198930
+ if (value === undefined)
198931
+ return null;
198932
+ strs.push(value);
198933
+ }
198934
+ return strs.join("");
198935
+ }
198936
+ return null;
198937
+ }
198871
198938
  function tryResolve(raw, resolved) {
198872
198939
  const value = raw.trim();
198873
198940
  if (value.startsWith("'") && value.endsWith("'") && !value.slice(1, -1).includes("'")) {
@@ -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;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"}
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,EAEX,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"}
@@ -7,17 +7,25 @@
7
7
  * (from ternary initializers), those are used directly instead of
8
8
  * re-parsing from the string representation.
9
9
  */
10
+ import type { ParsedExpr } from '@barefootjs/jsx';
10
11
  /**
11
12
  * Build a map of constant name → resolved string value.
12
13
  * Resolves string literals, template literals, array.join() patterns,
13
14
  * and plain identifier references. When `valueBranches` is present
14
15
  * (from ternary initializers), each branch is resolved and merged
15
- * with union semantics. Record lookups, function expressions, and
16
- * other complex values are skipped.
16
+ * with union semantics. An object-literal const with string-literal,
17
+ * template-literal, or bare-identifier-valued properties (`const rowClass
18
+ * = { active: \`${base} row-active\`, plain: base }`) additionally seeds
19
+ * member-path
20
+ * keys (`rowClass.active` → `'row row-active'`) so a member-access
21
+ * className (`className={rowClass.active}`, or a ternary arm) resolves
22
+ * through the same lookup (#2354, template-literal values #2360).
23
+ * Function expressions and other complex property values are skipped.
17
24
  */
18
25
  export declare function resolveConstants(constants: Array<{
19
26
  name: string;
20
27
  value?: string;
21
28
  valueBranches?: string[];
29
+ parsed?: ParsedExpr;
22
30
  }>): Map<string, string>;
23
31
  //# sourceMappingURL=resolve-constants.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"resolve-constants.d.ts","sourceRoot":"","sources":["../src/resolve-constants.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,GAC3E,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CA2BrB"}
1
+ {"version":3,"file":"resolve-constants.d.ts","sourceRoot":"","sources":["../src/resolve-constants.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAEjD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,CAAC,EAAE,UAAU,CAAA;CAAE,CAAC,GAChG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CA0CrB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/test",
3
- "version": "0.26.0",
3
+ "version": "0.26.2",
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.26.0"
42
+ "@barefootjs/jsx": "0.26.2"
43
43
  },
44
44
  "devDependencies": {
45
45
  "typescript": "^5.0.0"
@@ -18,8 +18,9 @@ import type {
18
18
  IRProvider,
19
19
  IRAsync,
20
20
  IRMetadata,
21
+ ParsedExpr,
21
22
  } from '@barefootjs/jsx'
22
- import { resolveSetters, buildLocalFunctionSetterMap, type SetterRef, type FnSetterResolution } from '@barefootjs/jsx'
23
+ import { resolveSetters, buildLocalFunctionSetterMap, identifierPath, type SetterRef, type FnSetterResolution } from '@barefootjs/jsx'
23
24
 
24
25
  type IRAttribute = IRElement['attrs'][number]
25
26
  type AttrValue = IRAttribute['value']
@@ -500,8 +501,17 @@ function splitClassTokens(value: string): string[] {
500
501
  * - `string` spans → literal text, with `${ident}` interpolations
501
502
  * substituted from resolved consts / literal prop defaults
502
503
  * 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`.
504
+ * and literal prop defaults. A bare `className={cond ? a : b}` ternary
505
+ * (no backticks) is walked structurally through the attr's `parsed`
506
+ * tree — the `expression`-kind analogue of the `template` branch's
507
+ * `ternary` part handling — so both arms union with the same semantics,
508
+ * and an unresolvable ternary yields `[]` rather than leaking its own
509
+ * operator tokens (`?`/`:`) and member fragments as class names (#2354).
510
+ * A non-ternary expression keeps the historical fallback: a resolved
511
+ * value splits into tokens, otherwise the raw source passes through
512
+ * (an unresolvable bare identifier surfaces as its name — a proxy some
513
+ * tests assert on). Anything still carrying a `${...}` interpolation is
514
+ * dropped by `splitClassTokens`.
505
515
  */
506
516
  function collectClassTokens(attrValue: AttrValue, resolved: string | boolean | null, ctx: ConvertContext): string[] {
507
517
  if (attrValue.kind === 'template') {
@@ -519,12 +529,53 @@ function collectClassTokens(attrValue: AttrValue, resolved: string | boolean | n
519
529
 
520
530
  const isDynamic = attrValue.kind === 'expression' || attrValue.kind === 'spread'
521
531
  if (isDynamic) {
532
+ // Bare `className={cond ? a : b}`: no backticks, so it never becomes a
533
+ // structured `template` attr. Walk the parsed ternary and union both
534
+ // arms — resolving identifier / member-access branches through the
535
+ // same cmap as everything else. A CSS class can never contain the
536
+ // `?`/`:` operator tokens the old raw-source fallback leaked here, so
537
+ // an unresolvable ternary suppresses to `[]` instead (#2354).
538
+ if (attrValue.kind === 'expression' && attrValue.parsed?.kind === 'conditional') {
539
+ const value = resolveParsedClass(attrValue.parsed, ctx)
540
+ return value === null ? [] : splitClassTokens(value)
541
+ }
522
542
  const value = resolveClassValue(resolved, ctx)
523
543
  if (value !== null) return splitClassTokens(value)
524
544
  }
525
545
  return splitClassTokens(resolved)
526
546
  }
527
547
 
548
+ /**
549
+ * Reduce a parsed className expression to its resolved class string, or
550
+ * `null` when no branch resolves. Handles the shapes a className ternary
551
+ * reaches for: string literals, local-const / prop-default identifiers,
552
+ * object-property member access (`rowClass.active`, resolved through the
553
+ * member-path keys `resolveConstants` seeds), and nested ternaries. Both
554
+ * arms of a ternary union (matching the `template` ternary part and the
555
+ * intermediate-const `valueBranches` handling), since the IR can't pick a
556
+ * concrete arm without the runtime condition. (#2354)
557
+ */
558
+ function resolveParsedClass(expr: ParsedExpr, ctx: ConvertContext): string | null {
559
+ switch (expr.kind) {
560
+ case 'literal':
561
+ return expr.literalType === 'string' ? String(expr.value) : null
562
+ case 'identifier':
563
+ return ctx.cmap.get(expr.name) ?? ctx.defaults.get(expr.name) ?? null
564
+ case 'member': {
565
+ const path = identifierPath(expr)
566
+ return path === null ? null : ctx.cmap.get(path) ?? null
567
+ }
568
+ case 'conditional': {
569
+ const whenTrue = resolveParsedClass(expr.consequent, ctx)
570
+ const whenFalse = resolveParsedClass(expr.alternate, ctx)
571
+ if (whenTrue === null && whenFalse === null) return null
572
+ return [whenTrue, whenFalse].filter((v): v is string => v !== null).join(' ')
573
+ }
574
+ default:
575
+ return null
576
+ }
577
+ }
578
+
528
579
  /**
529
580
  * Replace `${ident}` interpolations with a resolved const or a literal
530
581
  * prop default; unresolvable spans are kept verbatim so the caller's
@@ -8,20 +8,43 @@
8
8
  * re-parsing from the string representation.
9
9
  */
10
10
 
11
+ import type { ParsedExpr } from '@barefootjs/jsx'
12
+
11
13
  /**
12
14
  * Build a map of constant name → resolved string value.
13
15
  * Resolves string literals, template literals, array.join() patterns,
14
16
  * and plain identifier references. When `valueBranches` is present
15
17
  * (from ternary initializers), each branch is resolved and merged
16
- * with union semantics. Record lookups, function expressions, and
17
- * other complex values are skipped.
18
+ * with union semantics. An object-literal const with string-literal,
19
+ * template-literal, or bare-identifier-valued properties (`const rowClass
20
+ * = { active: \`${base} row-active\`, plain: base }`) additionally seeds
21
+ * member-path
22
+ * keys (`rowClass.active` → `'row row-active'`) so a member-access
23
+ * className (`className={rowClass.active}`, or a ternary arm) resolves
24
+ * through the same lookup (#2354, template-literal values #2360).
25
+ * Function expressions and other complex property values are skipped.
18
26
  */
19
27
  export function resolveConstants(
20
- constants: Array<{ name: string; value?: string; valueBranches?: string[] }>
28
+ constants: Array<{ name: string; value?: string; valueBranches?: string[]; parsed?: ParsedExpr }>
21
29
  ): Map<string, string> {
22
30
  const resolved = new Map<string, string>()
23
31
 
24
32
  for (const c of constants) {
33
+ // Object-literal const: expose each resolvable property as a
34
+ // `name.key` member-path key. The object's OWN bare identifier
35
+ // (`c.name` referenced directly, e.g. `className={rowClass}`) stays
36
+ // unresolved — an object is not a class string — matching prior
37
+ // behavior; this is unrelated to a resolvable identifier-valued
38
+ // PROPERTY (`{ plain: base }`), handled by resolveObjectPropValue
39
+ // below (Copilot review, PR #2361).
40
+ if (c.parsed?.kind === 'object-literal') {
41
+ for (const prop of c.parsed.properties) {
42
+ const value = resolveObjectPropValue(prop.value, resolved)
43
+ if (value !== null) resolved.set(`${c.name}.${prop.key}`, value)
44
+ }
45
+ continue
46
+ }
47
+
25
48
  if (!c.value) continue
26
49
 
27
50
  // When the analyzer provides structured branch info, resolve each
@@ -47,6 +70,56 @@ export function resolveConstants(
47
70
  return resolved
48
71
  }
49
72
 
73
+ /**
74
+ * Resolve one object-literal property's value to a string, for the
75
+ * `name.key` member-path seeding above. Supports the same shapes as
76
+ * `tryResolve` — string literal, template literal, plain identifier
77
+ * reference — but walks the already-structured `ParsedExpr` (available
78
+ * here, unlike `tryResolve`'s raw-string input) instead of re-parsing
79
+ * with a regex, and is stricter about "unresolvable": `tryResolve`'s
80
+ * template-literal branch folds an unresolved `${...}` to `''` (its
81
+ * caller only ever uses that for the whole-constant case, where a
82
+ * partial string is an acceptable degrade), but a property value here
83
+ * returns `null` for the whole property instead — seeding `name.key`
84
+ * with a misleadingly partial class string would be worse than not
85
+ * seeding it at all. A bare identifier property (`{ plain: base }`) and
86
+ * an interpolated identifier inside a template literal both resolve
87
+ * against `resolved` — safe because `resolveConstants` processes
88
+ * `constants` in declaration order, so a module-scope const referenced
89
+ * inside a later object literal's property is already in the map
90
+ * (#2360). Anything else (a nested object/array, a call, a binary
91
+ * expression, ...) is left unresolved, same posture as `tryResolve`
92
+ * skipping "complex values".
93
+ */
94
+ function resolveObjectPropValue(expr: ParsedExpr, resolved: Map<string, string>): string | null {
95
+ if (expr.kind === 'literal' && expr.literalType === 'string') {
96
+ return String(expr.value)
97
+ }
98
+ if (expr.kind === 'identifier') {
99
+ return resolved.get(expr.name) ?? null
100
+ }
101
+ if (expr.kind === 'template-literal') {
102
+ const strs: string[] = []
103
+ for (const part of expr.parts) {
104
+ if (part.type === 'string') {
105
+ strs.push(part.value)
106
+ continue
107
+ }
108
+ // An interpolation that isn't a plain identifier reference (a
109
+ // conditional, a member access, ...), or one that IS an identifier
110
+ // but doesn't resolve, makes the whole template unresolved — don't
111
+ // fold it to '' and seed a partial, misleadingly "resolved" string
112
+ // (Copilot review, PR #2361).
113
+ if (part.expr.kind !== 'identifier') return null
114
+ const value = resolved.get(part.expr.name)
115
+ if (value === undefined) return null
116
+ strs.push(value)
117
+ }
118
+ return strs.join('')
119
+ }
120
+ return null
121
+ }
122
+
50
123
  function tryResolve(raw: string, resolved: Map<string, string>): string | null {
51
124
  const value = raw.trim()
52
125