@barefootjs/test 0.25.0 → 0.26.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 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);
@@ -191421,6 +191430,37 @@ function extractFreeIdentifiersFromNode(node) {
191421
191430
  visit2(node);
191422
191431
  return ids;
191423
191432
  }
191433
+ function extractFreeTypeIdentifiersFromNode(node) {
191434
+ const ids = new Set;
191435
+ const boundTypeParams = new Set;
191436
+ function rootName(name) {
191437
+ return import_typescript8.default.isQualifiedName(name) ? rootName(name.left) : name;
191438
+ }
191439
+ function visit2(n) {
191440
+ if (import_typescript8.default.isTypeReferenceNode(n)) {
191441
+ const name = rootName(n.typeName).text;
191442
+ if (!boundTypeParams.has(name))
191443
+ ids.add(name);
191444
+ }
191445
+ if (import_typescript8.default.isTypeQueryNode(n)) {
191446
+ const name = rootName(n.exprName).text;
191447
+ if (!boundTypeParams.has(name))
191448
+ ids.add(name);
191449
+ }
191450
+ if (import_typescript8.default.isFunctionLike(n) && n.typeParameters && n.typeParameters.length > 0) {
191451
+ const names = n.typeParameters.map((p) => p.name.text);
191452
+ for (const p of names)
191453
+ boundTypeParams.add(p);
191454
+ import_typescript8.default.forEachChild(n, visit2);
191455
+ for (const p of names)
191456
+ boundTypeParams.delete(p);
191457
+ return;
191458
+ }
191459
+ import_typescript8.default.forEachChild(n, visit2);
191460
+ }
191461
+ visit2(node);
191462
+ return ids;
191463
+ }
191424
191464
  function initializerShapeContainsJsx(node) {
191425
191465
  let found = false;
191426
191466
  function visit2(n) {
@@ -192243,16 +192283,17 @@ function buildEntryImportIndex(sf, filePath) {
192243
192283
  continue;
192244
192284
  if (!import_typescript8.default.isStringLiteral(stmt.moduleSpecifier))
192245
192285
  continue;
192246
- if (stmt.importClause?.isTypeOnly)
192247
- continue;
192248
192286
  const src = stmt.moduleSpecifier.text;
192249
192287
  const targetKey = src.startsWith("./") || src.startsWith("../") ? resolveRelativeImportToFile(src, filePath) ?? "unresolved:" + src : src;
192288
+ const wholeTypeOnly = stmt.importClause?.isTypeOnly === true;
192250
192289
  const namedBindings = stmt.importClause?.namedBindings;
192251
192290
  if (namedBindings && import_typescript8.default.isNamedImports(namedBindings)) {
192252
192291
  for (const el of namedBindings.elements) {
192253
- if (el.isTypeOnly)
192254
- continue;
192255
- index.set(el.name.text, { targetKey, exportedName: (el.propertyName ?? el.name).text });
192292
+ index.set(el.name.text, {
192293
+ targetKey,
192294
+ exportedName: (el.propertyName ?? el.name).text,
192295
+ isTypeOnly: wholeTypeOnly || el.isTypeOnly
192296
+ });
192256
192297
  }
192257
192298
  }
192258
192299
  }
@@ -192282,6 +192323,9 @@ function collectEntryBindingNames(sf) {
192282
192323
  if ((import_typescript8.default.isFunctionDeclaration(node) || import_typescript8.default.isClassDeclaration(node) || import_typescript8.default.isEnumDeclaration(node)) && node.name) {
192283
192324
  names.add(node.name.text);
192284
192325
  }
192326
+ if ((import_typescript8.default.isTypeAliasDeclaration(node) || import_typescript8.default.isInterfaceDeclaration(node)) && node.name) {
192327
+ names.add(node.name.text);
192328
+ }
192285
192329
  if (import_typescript8.default.isFunctionLike(node)) {
192286
192330
  for (const p of node.parameters) {
192287
192331
  const out = [];
@@ -192477,7 +192521,11 @@ function prescanImportedReactiveFactories(entrySourceFile, filePath, result) {
192477
192521
  const required = [];
192478
192522
  const pending = [];
192479
192523
  let declinedEntry = null;
192480
- for (const ref of capture.importedRefs) {
192524
+ const allRefs = [
192525
+ ...capture.importedRefs.map((r) => ({ ...r, isTypeOnly: false })),
192526
+ ...capture.importedTypeRefs.map((r) => ({ ...r, isTypeOnly: true }))
192527
+ ];
192528
+ for (const ref of allRefs) {
192481
192529
  let specifier;
192482
192530
  let targetKey;
192483
192531
  if (ref.source.startsWith("./") || ref.source.startsWith("../")) {
@@ -192497,7 +192545,7 @@ function prescanImportedReactiveFactories(entrySourceFile, filePath, result) {
192497
192545
  targetKey = ref.source;
192498
192546
  }
192499
192547
  const existing = entryImportIndex.get(ref.localName);
192500
- if (existing && existing.targetKey === targetKey && existing.exportedName === ref.exportedName) {
192548
+ if (existing && existing.targetKey === targetKey && existing.exportedName === ref.exportedName && (ref.isTypeOnly || !existing.isTypeOnly)) {
192501
192549
  continue;
192502
192550
  }
192503
192551
  const planned = plannedInjections.get(ref.localName);
@@ -192511,7 +192559,7 @@ function prescanImportedReactiveFactories(entrySourceFile, filePath, result) {
192511
192559
  break;
192512
192560
  }
192513
192561
  pending.push([ref.localName, { targetKey, exportedName: ref.exportedName }]);
192514
- required.push({ localName: ref.localName, exportedName: ref.exportedName, specifier });
192562
+ required.push({ localName: ref.localName, exportedName: ref.exportedName, specifier, isTypeOnly: ref.isTypeOnly || undefined });
192515
192563
  }
192516
192564
  if (declinedEntry) {
192517
192565
  result.declined.set(spec.local, declinedEntry);
@@ -192531,6 +192579,8 @@ function prescanImportedReactiveFactories(entrySourceFile, filePath, result) {
192531
192579
  }
192532
192580
  function collectHelperModuleValueBindings(sf) {
192533
192581
  const local = new Set;
192582
+ const localTypes = new Set;
192583
+ const importedTypes = new Map;
192534
192584
  const imported = new Map;
192535
192585
  for (const stmt of sf.statements) {
192536
192586
  if (import_typescript8.default.isVariableStatement(stmt)) {
@@ -192546,35 +192596,41 @@ function collectHelperModuleValueBindings(sf) {
192546
192596
  local.add(stmt.name.text);
192547
192597
  continue;
192548
192598
  }
192599
+ if ((import_typescript8.default.isTypeAliasDeclaration(stmt) || import_typescript8.default.isInterfaceDeclaration(stmt)) && stmt.name) {
192600
+ localTypes.add(stmt.name.text);
192601
+ continue;
192602
+ }
192549
192603
  if (import_typescript8.default.isImportDeclaration(stmt)) {
192550
- if (stmt.importClause?.isTypeOnly)
192551
- continue;
192552
192604
  if (!import_typescript8.default.isStringLiteral(stmt.moduleSpecifier))
192553
192605
  continue;
192554
192606
  const src = stmt.moduleSpecifier.text;
192555
192607
  if (src === "@barefootjs/client" || src === "@barefootjs/client/runtime")
192556
192608
  continue;
192609
+ const wholeTypeOnly = stmt.importClause?.isTypeOnly === true;
192557
192610
  if (stmt.importClause?.name)
192558
- local.add(stmt.importClause.name.text);
192611
+ (wholeTypeOnly ? localTypes : local).add(stmt.importClause.name.text);
192559
192612
  const namedBindings = stmt.importClause?.namedBindings;
192560
192613
  if (namedBindings && import_typescript8.default.isNamedImports(namedBindings)) {
192561
192614
  for (const el of namedBindings.elements) {
192562
- if (el.isTypeOnly)
192563
- continue;
192564
- imported.set(el.name.text, { source: src, exportedName: (el.propertyName ?? el.name).text });
192615
+ const entry = { source: src, exportedName: (el.propertyName ?? el.name).text };
192616
+ if (wholeTypeOnly || el.isTypeOnly)
192617
+ importedTypes.set(el.name.text, entry);
192618
+ else
192619
+ imported.set(el.name.text, entry);
192565
192620
  }
192566
192621
  }
192567
192622
  if (namedBindings && import_typescript8.default.isNamespaceImport(namedBindings)) {
192568
- local.add(namedBindings.name.text);
192623
+ (wholeTypeOnly ? localTypes : local).add(namedBindings.name.text);
192569
192624
  }
192570
192625
  }
192571
192626
  }
192572
- return { local, imported };
192627
+ return { local, imported, localTypes, importedTypes };
192573
192628
  }
192574
192629
  function moduleCaptureCheck(fn, info, moduleBindings, selfName) {
192575
192630
  if (!fn.body)
192576
- return { captured: [], importedRefs: [] };
192631
+ return { captured: [], importedRefs: [], importedTypeRefs: [] };
192577
192632
  const free = extractFreeIdentifiersFromNode(fn.body);
192633
+ const freeTypes = extractFreeTypeIdentifiersFromNode(fn.body);
192578
192634
  const exclude = new Set(info.params);
192579
192635
  for (const b of info.localBindings)
192580
192636
  exclude.add(b);
@@ -192583,8 +192639,12 @@ function moduleCaptureCheck(fn, info, moduleBindings, selfName) {
192583
192639
  for (const p of REACTIVE_PRIMITIVES)
192584
192640
  exclude.add(p);
192585
192641
  exclude.add(selfName);
192642
+ if (fn.typeParameters)
192643
+ for (const p of fn.typeParameters)
192644
+ exclude.add(p.name.text);
192586
192645
  const captured = [];
192587
192646
  const importedRefs = [];
192647
+ const importedTypeRefs = [];
192588
192648
  for (const id of free) {
192589
192649
  if (exclude.has(id))
192590
192650
  continue;
@@ -192596,9 +192656,28 @@ function moduleCaptureCheck(fn, info, moduleBindings, selfName) {
192596
192656
  if (imp)
192597
192657
  importedRefs.push({ localName: id, source: imp.source, exportedName: imp.exportedName });
192598
192658
  }
192659
+ for (const id of freeTypes) {
192660
+ if (exclude.has(id))
192661
+ continue;
192662
+ if (free.has(id))
192663
+ continue;
192664
+ if (moduleBindings.localTypes.has(id) || moduleBindings.local.has(id)) {
192665
+ captured.push(id);
192666
+ continue;
192667
+ }
192668
+ const typeImp = moduleBindings.importedTypes.get(id);
192669
+ if (typeImp) {
192670
+ importedTypeRefs.push({ localName: id, source: typeImp.source, exportedName: typeImp.exportedName });
192671
+ continue;
192672
+ }
192673
+ const valueImp = moduleBindings.imported.get(id);
192674
+ if (valueImp)
192675
+ importedRefs.push({ localName: id, source: valueImp.source, exportedName: valueImp.exportedName });
192676
+ }
192599
192677
  captured.sort();
192600
192678
  importedRefs.sort((a, b) => a.localName < b.localName ? -1 : 1);
192601
- return { captured, importedRefs };
192679
+ importedTypeRefs.sort((a, b) => a.localName < b.localName ? -1 : 1);
192680
+ return { captured, importedRefs, importedTypeRefs };
192602
192681
  }
192603
192682
  function detectReactiveFactory(node, sourceFile, filePath) {
192604
192683
  if (!node.body || !node.name)
@@ -192937,20 +193016,44 @@ function rewriteFactoryCallsInSource(source, prescan) {
192937
193016
  if (edits.length === 0)
192938
193017
  return source;
192939
193018
  const importsBySpecifier = new Map;
193019
+ const typeImportsBySpecifier = new Map;
192940
193020
  for (const f of inlinedFactories) {
192941
193021
  for (const r of f.requiredImports ?? []) {
192942
- let names = importsBySpecifier.get(r.specifier);
193022
+ const bySpecifier = r.isTypeOnly ? typeImportsBySpecifier : importsBySpecifier;
193023
+ let names = bySpecifier.get(r.specifier);
192943
193024
  if (!names) {
192944
193025
  names = new Map;
192945
- importsBySpecifier.set(r.specifier, names);
193026
+ bySpecifier.set(r.specifier, names);
192946
193027
  }
192947
193028
  names.set(r.localName, r.exportedName);
192948
193029
  }
192949
193030
  }
192950
- if (importsBySpecifier.size > 0) {
192951
- const lines = [...importsBySpecifier].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([spec, names]) => {
193031
+ for (const [spec, typeNames] of typeImportsBySpecifier) {
193032
+ const valueNames = importsBySpecifier.get(spec);
193033
+ if (!valueNames)
193034
+ continue;
193035
+ for (const local of [...typeNames.keys()]) {
193036
+ if (valueNames.has(local))
193037
+ typeNames.delete(local);
193038
+ }
193039
+ if (typeNames.size === 0)
193040
+ typeImportsBySpecifier.delete(spec);
193041
+ }
193042
+ if (importsBySpecifier.size > 0 || typeImportsBySpecifier.size > 0) {
193043
+ const buildLine = (names, keyword, spec) => {
192952
193044
  const specifiers = [...names].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([local, exported]) => exported === local ? local : `${exported} as ${local}`);
192953
- return `import { ${specifiers.join(", ")} } from '${spec}'`;
193045
+ return `import ${keyword}{ ${specifiers.join(", ")} } from '${spec}'`;
193046
+ };
193047
+ const allSpecifiers = new Set([...importsBySpecifier.keys(), ...typeImportsBySpecifier.keys()]);
193048
+ const lines = [...allSpecifiers].sort().flatMap((spec) => {
193049
+ const out2 = [];
193050
+ const valueNames = importsBySpecifier.get(spec);
193051
+ if (valueNames)
193052
+ out2.push(buildLine(valueNames, "", spec));
193053
+ const typeNames = typeImportsBySpecifier.get(spec);
193054
+ if (typeNames)
193055
+ out2.push(buildLine(typeNames, "type ", spec));
193056
+ return out2;
192954
193057
  });
192955
193058
  const at = factoryImportInsertionOffset(sourceFile);
192956
193059
  edits.push({ start: at, end: at, replacement: at === 0 ? lines.join(`
@@ -193530,6 +193633,20 @@ function resolveFreeRefs(node, env) {
193530
193633
 
193531
193634
  // ../jsx/src/to-locale-date-lowering.ts
193532
193635
  var TO_LOCALE_TZ_RE = /^(?:UTC|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
193636
+ var tzProbeCache = new Map;
193637
+ function isBuildResolvableTimeZone(value) {
193638
+ const cached = tzProbeCache.get(value);
193639
+ if (cached !== undefined)
193640
+ return cached;
193641
+ let verified;
193642
+ try {
193643
+ verified = new Intl.DateTimeFormat("en-US", { timeZone: value }).resolvedOptions().timeZone === value;
193644
+ } catch {
193645
+ verified = false;
193646
+ }
193647
+ tzProbeCache.set(value, verified);
193648
+ return verified;
193649
+ }
193533
193650
  var PROBE_UTC = new Date(Date.UTC(2001, 1, 3));
193534
193651
  var formatCache = new Map;
193535
193652
  var namesCache = new Map;
@@ -193768,7 +193885,7 @@ function matchToLocaleDateStringCall(callee, args, metadata) {
193768
193885
  return null;
193769
193886
  const value = String(prop.value.value);
193770
193887
  if (prop.key === "timeZone") {
193771
- if (!TO_LOCALE_TZ_RE.test(value))
193888
+ if (!TO_LOCALE_TZ_RE.test(value) && !isBuildResolvableTimeZone(value))
193772
193889
  return null;
193773
193890
  tz = value;
193774
193891
  } else {
@@ -198681,12 +198798,37 @@ function collectClassTokens(attrValue, resolved, ctx) {
198681
198798
  return [];
198682
198799
  const isDynamic = attrValue.kind === "expression" || attrValue.kind === "spread";
198683
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
+ }
198684
198805
  const value = resolveClassValue(resolved, ctx);
198685
198806
  if (value !== null)
198686
198807
  return splitClassTokens(value);
198687
198808
  }
198688
198809
  return splitClassTokens(resolved);
198689
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
+ }
198690
198832
  function substituteInterpolations(value, ctx) {
198691
198833
  return value.replace(/\$\{([^}]+)\}/g, (raw, expr) => {
198692
198834
  const trimmed = expr.trim();
@@ -198736,6 +198878,14 @@ function resolveTemplateAttr(tl, ctx) {
198736
198878
  function resolveConstants(constants3) {
198737
198879
  const resolved = new Map;
198738
198880
  for (const c of constants3) {
198881
+ if (c.parsed?.kind === "object-literal") {
198882
+ for (const prop of c.parsed.properties) {
198883
+ if (prop.value.kind === "literal" && prop.value.literalType === "string") {
198884
+ resolved.set(`${c.name}.${prop.key}`, String(prop.value.value));
198885
+ }
198886
+ }
198887
+ continue;
198888
+ }
198739
198889
  if (!c.value)
198740
198890
  continue;
198741
198891
  if (c.valueBranches) {
@@ -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,23 @@
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-valued
17
+ * properties (`const rowClass = { active: 'row row-active', plain: 'row' }`)
18
+ * additionally seeds member-path keys (`rowClass.active` → `'row row-active'`)
19
+ * so a member-access className (`className={rowClass.active}`, or a
20
+ * ternary arm) resolves through the same lookup (#2354). Function
21
+ * expressions and other complex values are skipped.
17
22
  */
18
23
  export declare function resolveConstants(constants: Array<{
19
24
  name: string;
20
25
  value?: string;
21
26
  valueBranches?: string[];
27
+ parsed?: ParsedExpr;
22
28
  }>): Map<string, string>;
23
29
  //# 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;;;;;;;;;;;GAWG;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,CAuCrB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/test",
3
- "version": "0.25.0",
3
+ "version": "0.26.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.25.0"
42
+ "@barefootjs/jsx": "0.26.1"
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,38 @@
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-valued
19
+ * properties (`const rowClass = { active: 'row row-active', plain: 'row' }`)
20
+ * additionally seeds member-path keys (`rowClass.active` → `'row row-active'`)
21
+ * so a member-access className (`className={rowClass.active}`, or a
22
+ * ternary arm) resolves through the same lookup (#2354). Function
23
+ * expressions and other complex values are skipped.
18
24
  */
19
25
  export function resolveConstants(
20
- constants: Array<{ name: string; value?: string; valueBranches?: string[] }>
26
+ constants: Array<{ name: string; value?: string; valueBranches?: string[]; parsed?: ParsedExpr }>
21
27
  ): Map<string, string> {
22
28
  const resolved = new Map<string, string>()
23
29
 
24
30
  for (const c of constants) {
31
+ // Object-literal const: expose each string-valued property as a
32
+ // `name.key` member-path key. The bare identifier stays unresolved
33
+ // (an object is not a class string), matching prior behavior.
34
+ if (c.parsed?.kind === 'object-literal') {
35
+ for (const prop of c.parsed.properties) {
36
+ if (prop.value.kind === 'literal' && prop.value.literalType === 'string') {
37
+ resolved.set(`${c.name}.${prop.key}`, String(prop.value.value))
38
+ }
39
+ }
40
+ continue
41
+ }
42
+
25
43
  if (!c.value) continue
26
44
 
27
45
  // When the analyzer provides structured branch info, resolve each