@barefootjs/cli 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -22,9 +22,9 @@ async function loadBuildConfig(configPath) {
22
22
  let importTarget = configPath;
23
23
  let cleanupPath = null;
24
24
  if (!isBun) {
25
- const { build: build2 } = await import("esbuild");
25
+ const { build: build3 } = await import("esbuild");
26
26
  importTarget = resolve(dirname(configPath), `.barefoot.config.${process.pid}.mjs`);
27
- await build2({
27
+ await build3({
28
28
  entryPoints: [configPath],
29
29
  outfile: importTarget,
30
30
  bundle: true,
@@ -36,8 +36,8 @@ async function loadBuildConfig(configPath) {
36
36
  // resolution still happens at runtime from the user's project.
37
37
  plugins: [{
38
38
  name: "externalize-non-barefoot",
39
- setup(build3) {
40
- build3.onResolve({ filter: /^[^./]/ }, (args2) => {
39
+ setup(build4) {
40
+ build4.onResolve({ filter: /^[^./]/ }, (args2) => {
41
41
  if (args2.path.startsWith("@barefootjs/")) return null;
42
42
  return { path: args2.path, external: true };
43
43
  });
@@ -1885,6 +1885,9 @@ function irToComponentTemplateWithOpts(node, opts) {
1885
1885
  }
1886
1886
  return `\${${transformExpr(node.expr, node.templateExpr)}}`;
1887
1887
  case "conditional": {
1888
+ if (node.clientOnly && node.slotId) {
1889
+ return `<!--bf-cond-start:${node.slotId}--><!--bf-cond-end:${node.slotId}-->`;
1890
+ }
1888
1891
  const trueBranch = recurse(node.whenTrue);
1889
1892
  const falseBranch = recurse(node.whenFalse);
1890
1893
  const trueHtml = node.slotId ? addCondAttrToTemplate(trueBranch, node.slotId) : trueBranch;
@@ -2103,6 +2106,9 @@ function generateCsrTemplateWithOpts(node, opts) {
2103
2106
  return `\${${expr}}`;
2104
2107
  }
2105
2108
  case "conditional": {
2109
+ if (node.clientOnly && node.slotId) {
2110
+ return `<!--bf-cond-start:${node.slotId}--><!--bf-cond-end:${node.slotId}-->`;
2111
+ }
2106
2112
  const trueBranch = recurse(node.whenTrue);
2107
2113
  const falseBranch = recurse(node.whenFalse);
2108
2114
  const trueHtml = node.slotId ? addCondAttrToTemplate(trueBranch, node.slotId) : trueBranch;
@@ -7147,7 +7153,7 @@ function transformExpressionInner(expr, ctx2, node, isClientOnly) {
7147
7153
  }
7148
7154
  const ir = transformJsxExpression(expr, ctx2, isClientOnly);
7149
7155
  if (ir !== null) {
7150
- if (isClientOnly && ir.type === "conditional") {
7156
+ if ((isClientOnly || shouldAutoDeferReactiveBrand(expr, ctx2)) && ir.type === "conditional") {
7151
7157
  ir.clientOnly = true;
7152
7158
  if (!ir.slotId) {
7153
7159
  ir.slotId = generateSlotId(ctx2);
@@ -8221,11 +8227,11 @@ function transformMapCall(node, ctx2, isClientOnly = false, method = "map") {
8221
8227
  if (stmt === returnStmt) break;
8222
8228
  const js = ctx2.getJS(stmt);
8223
8229
  const tjs = ctx2.getTemplateJS(stmt);
8224
- const ts19 = stmt.getText(ctx2.sourceFile);
8230
+ const ts20 = stmt.getText(ctx2.sourceFile);
8225
8231
  preambleStmts.push(js.endsWith(";") ? js : js + ";");
8226
8232
  templatePreambleStmts.push(tjs.endsWith(";") ? tjs : tjs + ";");
8227
- typedPreambleStmts.push(ts19.endsWith(";") ? ts19 : ts19 + ";");
8228
- if (js !== ts19) hasTypeDiff = true;
8233
+ typedPreambleStmts.push(ts20.endsWith(";") ? ts20 : ts20 + ";");
8234
+ if (js !== ts20) hasTypeDiff = true;
8229
8235
  if (js !== tjs) hasTemplateDiff = true;
8230
8236
  }
8231
8237
  if (preambleStmts.length > 0) {
@@ -8524,7 +8530,7 @@ function processAttributes(attributes, ctx2) {
8524
8530
  value = { ...value, templateExpr: rewritten };
8525
8531
  }
8526
8532
  }
8527
- if (hasLeadingClientDirective(attr.initializer.expression, ctx2.sourceFile)) {
8533
+ if (hasLeadingClientDirective(attr.initializer.expression, ctx2.sourceFile) || shouldAutoDeferReactiveBrand(attr.initializer.expression, ctx2)) {
8528
8534
  clientOnly = true;
8529
8535
  }
8530
8536
  }
@@ -8927,6 +8933,13 @@ function isReactiveExpression(expr, ctx2, astNode) {
8927
8933
  }
8928
8934
  return false;
8929
8935
  }
8936
+ function shouldAutoDeferReactiveBrand(expr, ctx2) {
8937
+ const checker = ctx2.analyzer.checker;
8938
+ if (!checker) return false;
8939
+ if (!containsReactiveExpression(expr, checker)) return false;
8940
+ if (isSignalOrMemoReference(ctx2.getJS(expr), ctx2)) return false;
8941
+ return true;
8942
+ }
8930
8943
  function isSignalOrMemoReference(expr, ctx2, visited) {
8931
8944
  for (const { pattern } of ctx2.patterns.signals) {
8932
8945
  if (pattern.test(expr)) return true;
@@ -9026,10 +9039,10 @@ function hasDynamicContent(children) {
9026
9039
  function inferExpressionType(_node, _ctx) {
9027
9040
  return null;
9028
9041
  }
9029
- function replaceBranchLocalRefs(text, branchNames, resolve7) {
9042
+ function replaceBranchLocalRefs(text, branchNames, resolve11) {
9030
9043
  if (branchNames.length === 0) return text;
9031
9044
  const pattern = new RegExp(`(?<![\\w$])(${branchNames.join("|")})(?![\\w$])`, "g");
9032
- return replaceInExprContexts(text, pattern, (_match, name) => resolve7(name));
9045
+ return replaceInExprContexts(text, pattern, (_match, name) => resolve11(name));
9033
9046
  }
9034
9047
  function buildIfStatementChain(analyzer, ctx2) {
9035
9048
  const conditionalReturns = analyzer.conditionalReturns;
@@ -17476,13 +17489,51 @@ function makeIdRefRegex(name) {
17476
17489
  }
17477
17490
  function buildLocalFunctionSetterMap(meta, setterToSignal) {
17478
17491
  const setterPatterns = [...setterToSignal.keys()].map((s) => ({ name: s, re: makeIdCallRegex(s) }));
17479
- const result = /* @__PURE__ */ new Map();
17480
- for (const fn of meta.localFunctions) {
17492
+ const bodies = /* @__PURE__ */ new Map();
17493
+ for (const fn of meta.localFunctions) bodies.set(fn.name, fn.body);
17494
+ for (const c of meta.localConstants) {
17495
+ if (c.containsArrow && c.value) bodies.set(c.name, c.value);
17496
+ }
17497
+ const fnNamePatterns = [...bodies.keys()].map((n) => ({ name: n, re: makeIdCallRegex(n) }));
17498
+ const directSetters = /* @__PURE__ */ new Map();
17499
+ const directCalls = /* @__PURE__ */ new Map();
17500
+ for (const [name, body] of bodies) {
17481
17501
  const setters = [];
17482
- for (const { name, re } of setterPatterns) {
17483
- if (re.test(fn.body)) setters.push(name);
17502
+ for (const { name: setter, re } of setterPatterns) {
17503
+ if (re.test(body)) setters.push(setter);
17504
+ }
17505
+ directSetters.set(name, setters);
17506
+ const calls = [];
17507
+ for (const { name: callee, re } of fnNamePatterns) {
17508
+ if (callee !== name && re.test(body)) calls.push(callee);
17509
+ }
17510
+ directCalls.set(name, calls);
17511
+ }
17512
+ const resolve11 = (name, stack) => {
17513
+ const out = [];
17514
+ const seen = /* @__PURE__ */ new Set();
17515
+ for (const setter of directSetters.get(name) ?? []) {
17516
+ if (!seen.has(setter)) {
17517
+ out.push({ setter, chain: [] });
17518
+ seen.add(setter);
17519
+ }
17520
+ }
17521
+ for (const callee of directCalls.get(name) ?? []) {
17522
+ if (stack.has(callee)) continue;
17523
+ const sub2 = resolve11(callee, /* @__PURE__ */ new Set([...stack, callee]));
17524
+ for (const r of sub2) {
17525
+ if (!seen.has(r.setter)) {
17526
+ out.push({ setter: r.setter, chain: [callee, ...r.chain] });
17527
+ seen.add(r.setter);
17528
+ }
17529
+ }
17484
17530
  }
17485
- if (setters.length > 0) result.set(fn.name, setters);
17531
+ return out;
17532
+ };
17533
+ const result = /* @__PURE__ */ new Map();
17534
+ for (const name of bodies.keys()) {
17535
+ const resolved = resolve11(name, /* @__PURE__ */ new Set([name]));
17536
+ if (resolved.length > 0) result.set(name, resolved);
17486
17537
  }
17487
17538
  return result;
17488
17539
  }
@@ -17574,12 +17625,16 @@ function resolveSetters(handler, setterToSignal, fnSetters) {
17574
17625
  }
17575
17626
  }
17576
17627
  }
17577
- for (const [fnName, setters] of fnSetters) {
17628
+ for (const [fnName, resolutions] of fnSetters) {
17578
17629
  if (trimmed === fnName || makeIdCallRegex(fnName).test(handler)) {
17579
- for (const setter of setters) {
17580
- if (!seen.has(setter)) {
17581
- refs.push({ setter, signal: setterToSignal.get(setter) ?? null, via: fnName });
17582
- seen.add(setter);
17630
+ for (const r of resolutions) {
17631
+ if (!seen.has(r.setter)) {
17632
+ refs.push({
17633
+ setter: r.setter,
17634
+ signal: setterToSignal.get(r.setter) ?? null,
17635
+ via: [fnName, ...r.chain]
17636
+ });
17637
+ seen.add(r.setter);
17583
17638
  }
17584
17639
  }
17585
17640
  }
@@ -17613,7 +17668,7 @@ function formatEventSummary(summary, graph) {
17613
17668
  lines.push("");
17614
17669
  lines.push(` ${event.elementContext}`);
17615
17670
  const setterParts = event.setterCalls.map((s) => {
17616
- const chain = s.via ? `${s.via} -> ${s.setter}` : s.setter;
17671
+ const chain = s.via && s.via.length > 0 ? `${s.via.join(" -> ")} -> ${s.setter}` : s.setter;
17617
17672
  return chain;
17618
17673
  });
17619
17674
  const setterStr = setterParts.length > 0 ? setterParts.join(", ") : event.handler;
@@ -17883,6 +17938,95 @@ function buildUpdateEntry(consumer, graph, visited) {
17883
17938
  }
17884
17939
  return { name: consumer, kind: "effect", label: consumer, children: [] };
17885
17940
  }
17941
+ function buildWhyUpdate(source, filePath, bindingLabel, componentName) {
17942
+ const { graph, ir } = buildComponentAnalysis(source, filePath, componentName);
17943
+ const matches = graph.domBindings.filter(
17944
+ (d) => d.label === bindingLabel || d.slotId === bindingLabel
17945
+ );
17946
+ if (matches.length === 0) return null;
17947
+ if (matches.length > 1) {
17948
+ return {
17949
+ binding: bindingLabel,
17950
+ expression: null,
17951
+ deps: [],
17952
+ ambiguous: matches.map((d) => ({ label: d.label, slotId: d.slotId }))
17953
+ };
17954
+ }
17955
+ const binding = matches[0];
17956
+ const setterToSignal = /* @__PURE__ */ new Map();
17957
+ for (const s of ir.metadata.signals) {
17958
+ if (s.setter) setterToSignal.set(s.setter, s.getter);
17959
+ }
17960
+ const fnSetters = buildLocalFunctionSetterMap(ir.metadata, setterToSignal);
17961
+ const events = collectEventBindings(ir.root, setterToSignal, fnSetters);
17962
+ const deps = [];
17963
+ const visited = /* @__PURE__ */ new Set();
17964
+ function traceDep(name) {
17965
+ if (visited.has(name)) return;
17966
+ visited.add(name);
17967
+ const signal = graph.signals.find((s) => s.name === name);
17968
+ if (signal) {
17969
+ const changedBy = [];
17970
+ for (const ev of events) {
17971
+ for (const sc of ev.setterCalls) {
17972
+ if (sc.signal === name) {
17973
+ changedBy.push({
17974
+ handler: ev.eventName,
17975
+ setter: sc.setter,
17976
+ elementContext: ev.elementContext,
17977
+ via: sc.via
17978
+ });
17979
+ }
17980
+ }
17981
+ }
17982
+ deps.push({ name, kind: "signal", dependsOn: [], changedBy });
17983
+ return;
17984
+ }
17985
+ const memo = graph.memos.find((m) => m.name === name);
17986
+ if (memo) {
17987
+ deps.push({ name, kind: "memo", dependsOn: memo.deps, changedBy: [] });
17988
+ for (const dep of memo.deps) traceDep(dep);
17989
+ }
17990
+ }
17991
+ for (const dep of binding.deps) traceDep(dep);
17992
+ const stableId = binding.type === "attribute" ? binding.label : binding.slotId;
17993
+ return {
17994
+ binding: stableId,
17995
+ expression: binding.expression ?? null,
17996
+ deps,
17997
+ ...binding.classification === "fallback" && { classification: binding.classification },
17998
+ ...binding.wrapReason && { wrapReason: binding.wrapReason }
17999
+ };
18000
+ }
18001
+ function formatWhyUpdate(result) {
18002
+ const lines = [];
18003
+ lines.push(`${result.binding} updates because:`);
18004
+ if (result.expression) {
18005
+ lines.push(` ${result.expression}`);
18006
+ }
18007
+ if (result.classification === "fallback") {
18008
+ lines.push("");
18009
+ lines.push(`note: this is a fallback-wrapped binding (${result.wrapReason ?? "unknown"})`);
18010
+ lines.push(" the compiler could not statically prove reactivity \u2014 deps are determined at runtime");
18011
+ }
18012
+ for (const dep of result.deps) {
18013
+ lines.push("");
18014
+ if (dep.kind === "memo") {
18015
+ lines.push(`${dep.name} depends on:`);
18016
+ for (const d of dep.dependsOn) lines.push(` ${d}`);
18017
+ } else {
18018
+ lines.push(`${dep.name} changes from:`);
18019
+ if (dep.changedBy.length === 0) {
18020
+ lines.push(" (no event handlers found)");
18021
+ }
18022
+ for (const src of dep.changedBy) {
18023
+ const chain = src.via && src.via.length > 0 ? `${src.elementContext} ${src.handler} -> ${src.via.join(" -> ")} -> ${src.setter}` : `${src.elementContext} ${src.handler} -> ${src.setter}`;
18024
+ lines.push(` ${chain}`);
18025
+ }
18026
+ }
18027
+ }
18028
+ return lines.join("\n");
18029
+ }
17886
18030
  function formatComponentGraph(graph) {
17887
18031
  const lines = [];
17888
18032
  lines.push(`${graph.componentName} (${graph.sourceFile})`);
@@ -18054,6 +18198,145 @@ function formatSignalTrace(traces) {
18054
18198
  }
18055
18199
  }).join("\n");
18056
18200
  }
18201
+ function describeFallback(binding) {
18202
+ const isEventHandler = binding.type === "event" || binding.type === "attribute" && /^on[A-Z]/.test(binding.label.split(".").pop() ?? "");
18203
+ const reason = describeFallbackReason(binding.wrapReason, binding.type, isEventHandler);
18204
+ const runtimeDeps = binding.deps.length > 0 ? binding.deps.join(", ") : isEventHandler ? "likely none (event handler captures values, does not track reactively)" : "unknown \u2014 subscribes to whatever signals it reads at runtime";
18205
+ const suggestion = isEventHandler ? "event handlers intentionally capture scope values; this fallback is typically safe to ignore" : binding.wrapReason === "fallback-function-calls" ? "inline the reactive source or wrap the result in createMemo so the compiler can prove the dependency" : binding.wrapReason === "fallback-getter-calls" ? "the call looks like a signal getter but is not a known signal; verify the function is pure or extract as createMemo" : "rewrite to use a known signal/memo reference so the compiler can statically prove reactivity";
18206
+ return {
18207
+ label: binding.label,
18208
+ expression: binding.expression ?? "(expression not captured)",
18209
+ reason,
18210
+ runtimeDeps,
18211
+ suggestion,
18212
+ loc: binding.loc ? { file: binding.loc.file, line: binding.loc.start.line } : void 0,
18213
+ isEventHandler
18214
+ };
18215
+ }
18216
+ function describeFallbackReason(wrapReason, bindingType, isEventHandler) {
18217
+ const context = bindingType === "attribute" ? "an attribute expression" : bindingType === "text" ? "a text interpolation" : bindingType === "conditional" ? "a conditional expression" : bindingType === "loop" ? "a loop array expression" : "an expression";
18218
+ switch (wrapReason) {
18219
+ case "fallback-function-calls":
18220
+ return isEventHandler ? `function call in ${context} (event handler prop)` : `opaque function call in ${context} \u2014 the compiler cannot prove it is reactive or pure`;
18221
+ case "fallback-getter-calls":
18222
+ return `call pattern resembles a signal getter in ${context}, but is not a recognized signal`;
18223
+ case "string-reactive":
18224
+ return `string-level match found a signal/memo name in ${context}`;
18225
+ case "props-access":
18226
+ return `props.xxx reference in ${context} \u2014 reactive via prop forwarding`;
18227
+ case "proven-reactive":
18228
+ return `statically proven reactive in ${context}`;
18229
+ default:
18230
+ return `unknown fallback trigger in ${context}`;
18231
+ }
18232
+ }
18233
+ function formatFallbackExplanations(componentName, fallbacks) {
18234
+ const lines = [];
18235
+ if (fallbacks.length === 0) {
18236
+ lines.push(`${componentName} \u2014 no fallback-wrapped expressions.`);
18237
+ return lines.join("\n");
18238
+ }
18239
+ lines.push(`${componentName} \u2014 ${fallbacks.length} fallback-wrapped expression(s)`);
18240
+ for (const f of fallbacks) {
18241
+ const ex = describeFallback(f);
18242
+ lines.push("");
18243
+ if (ex.loc) {
18244
+ const locFile = ex.loc.file.split("/").pop() ?? ex.loc.file;
18245
+ lines.push(` ${locFile}:${ex.loc.line}`);
18246
+ }
18247
+ lines.push(` ${ex.label} fallback:`);
18248
+ lines.push(` expression: ${ex.expression}`);
18249
+ lines.push(` reason: ${ex.reason}`);
18250
+ lines.push(` runtime deps: ${ex.runtimeDeps}`);
18251
+ lines.push(` suggestion: ${ex.suggestion}`);
18252
+ }
18253
+ return lines.join("\n");
18254
+ }
18255
+ function buildComponentSummary(source, filePath, componentName) {
18256
+ const { graph, ir } = buildComponentAnalysis(source, filePath, componentName);
18257
+ const meta = ir.metadata;
18258
+ const clientNeeds = analyzeClientNeeds(ir);
18259
+ const hasReactiveState = meta.signals.length > 0 || meta.memos.length > 0 || meta.effects.length > 0;
18260
+ const needsClient = clientNeeds.needsInit && hasReactiveState;
18261
+ let loopCount = 0;
18262
+ countNodeType(ir.root, "loop", () => {
18263
+ loopCount++;
18264
+ });
18265
+ let conditionalCount = 0;
18266
+ countNodeType(ir.root, "conditional", () => {
18267
+ conditionalCount++;
18268
+ });
18269
+ const eventHandlers = graph.domBindings.filter((d) => d.type === "event").length;
18270
+ const textBindings = graph.domBindings.filter((d) => d.type === "text").length;
18271
+ const attrBindings = graph.domBindings.filter((d) => d.type === "attribute").length;
18272
+ const fallbacks = graph.domBindings.filter((d) => d.classification === "fallback").length;
18273
+ let clientBundle = null;
18274
+ if (needsClient) {
18275
+ const base = filePath.replace(/\.[^.]+$/, "").split("/").pop() ?? meta.componentName;
18276
+ clientBundle = `${base}.client.js`;
18277
+ }
18278
+ return {
18279
+ componentName: graph.componentName,
18280
+ sourceFile: graph.sourceFile,
18281
+ hydrated: needsClient,
18282
+ clientBundle,
18283
+ signals: graph.signals.length,
18284
+ memos: graph.memos.length,
18285
+ effects: graph.effects.length,
18286
+ loops: loopCount,
18287
+ eventHandlers,
18288
+ dynamicTextBindings: textBindings,
18289
+ dynamicAttributes: attrBindings,
18290
+ conditionals: conditionalCount,
18291
+ fallbacks
18292
+ };
18293
+ }
18294
+ function countNodeType(node, targetType, cb) {
18295
+ if (node.type === targetType) cb();
18296
+ switch (node.type) {
18297
+ case "element":
18298
+ case "fragment":
18299
+ case "provider":
18300
+ for (const child of node.children) countNodeType(child, targetType, cb);
18301
+ break;
18302
+ case "component":
18303
+ for (const child of node.children) countNodeType(child, targetType, cb);
18304
+ break;
18305
+ case "conditional":
18306
+ countNodeType(node.whenTrue, targetType, cb);
18307
+ countNodeType(node.whenFalse, targetType, cb);
18308
+ break;
18309
+ case "loop":
18310
+ for (const child of node.children) countNodeType(child, targetType, cb);
18311
+ break;
18312
+ case "if-statement":
18313
+ countNodeType(node.consequent, targetType, cb);
18314
+ if (node.alternate) countNodeType(node.alternate, targetType, cb);
18315
+ break;
18316
+ case "async":
18317
+ countNodeType(node.fallback, targetType, cb);
18318
+ for (const child of node.children) countNodeType(child, targetType, cb);
18319
+ break;
18320
+ }
18321
+ }
18322
+ function formatComponentSummary(summary) {
18323
+ const lines = [];
18324
+ lines.push(summary.componentName);
18325
+ lines.push(` hydrated: ${summary.hydrated ? "yes" : "no"}`);
18326
+ if (summary.clientBundle) {
18327
+ lines.push(` client bundle: ${summary.clientBundle}`);
18328
+ }
18329
+ lines.push(` signals: ${summary.signals}`);
18330
+ lines.push(` memos: ${summary.memos}`);
18331
+ if (summary.effects > 0) lines.push(` effects: ${summary.effects}`);
18332
+ lines.push(` loops: ${summary.loops}`);
18333
+ lines.push(` event handlers: ${summary.eventHandlers}`);
18334
+ lines.push(` dynamic text bindings: ${summary.dynamicTextBindings}`);
18335
+ lines.push(` dynamic attributes: ${summary.dynamicAttributes}`);
18336
+ if (summary.conditionals > 0) lines.push(` conditionals: ${summary.conditionals}`);
18337
+ if (summary.fallbacks > 0) lines.push(` fallbacks: ${summary.fallbacks}`);
18338
+ return lines.join("\n");
18339
+ }
18057
18340
  function inferWrapReasonForAttrLike(hasStringReactive, hasPropsRef, flags) {
18058
18341
  if (hasPropsRef) return "props-access";
18059
18342
  if (hasStringReactive) return "string-reactive";
@@ -18284,6 +18567,7 @@ var init_debug = __esm({
18284
18567
  init_analyzer();
18285
18568
  init_jsx_to_ir();
18286
18569
  init_compiler();
18570
+ init_ir_to_client_js();
18287
18571
  init_reactivity();
18288
18572
  }
18289
18573
  });
@@ -18304,18 +18588,22 @@ __export(src_exports, {
18304
18588
  applyCssLayerPrefix: () => applyCssLayerPrefix,
18305
18589
  buildComponentAnalysis: () => buildComponentAnalysis,
18306
18590
  buildComponentGraph: () => buildComponentGraph,
18591
+ buildComponentSummary: () => buildComponentSummary,
18307
18592
  buildEventSummary: () => buildEventSummary,
18308
18593
  buildGraphFromIR: () => buildGraphFromIR,
18594
+ buildLocalFunctionSetterMap: () => buildLocalFunctionSetterMap,
18309
18595
  buildLoopChainExpr: () => buildLoopChainExpr,
18310
18596
  buildLoopSummary: () => buildLoopSummary,
18311
18597
  buildMetadata: () => buildMetadata,
18312
18598
  buildSourceMapFromIR: () => buildSourceMapFromIR,
18599
+ buildWhyUpdate: () => buildWhyUpdate,
18313
18600
  combineParentChildClientJs: () => combineParentChildClientJs,
18314
18601
  compileJSX: () => compileJSX,
18315
18602
  containsHigherOrder: () => containsHigherOrder,
18316
18603
  createError: () => createError,
18317
18604
  createProgramForCorpus: () => createProgramForCorpus,
18318
18605
  createProgramForFile: () => createProgramForFile,
18606
+ describeFallback: () => describeFallback,
18319
18607
  disableCompilerInstrumentation: () => disableCompilerInstrumentation,
18320
18608
  emitAttrValue: () => emitAttrValue,
18321
18609
  emitIRNode: () => emitIRNode,
@@ -18326,12 +18614,15 @@ __export(src_exports, {
18326
18614
  extractSsrDefaults: () => extractSsrDefaults,
18327
18615
  findReachableNames: () => findReachableNames,
18328
18616
  formatComponentGraph: () => formatComponentGraph,
18617
+ formatComponentSummary: () => formatComponentSummary,
18329
18618
  formatError: () => formatError,
18330
18619
  formatEventSummary: () => formatEventSummary,
18620
+ formatFallbackExplanations: () => formatFallbackExplanations,
18331
18621
  formatLoopSummary: () => formatLoopSummary,
18332
18622
  formatParamWithType: () => formatParamWithType,
18333
18623
  formatSignalTrace: () => formatSignalTrace,
18334
18624
  formatUpdatePath: () => formatUpdatePath,
18625
+ formatWhyUpdate: () => formatWhyUpdate,
18335
18626
  generateClientJs: () => generateClientJs,
18336
18627
  generateClientJsWithSourceMap: () => generateClientJsWithSourceMap,
18337
18628
  generateCodeFrame: () => generateCodeFrame,
@@ -18345,10 +18636,12 @@ __export(src_exports, {
18345
18636
  jsxToIR: () => jsxToIR,
18346
18637
  listComponentFunctions: () => listComponentFunctions,
18347
18638
  listExportedComponents: () => listComponentFunctions,
18639
+ makeIdCallRegex: () => makeIdCallRegex,
18348
18640
  needsTypeBasedDetection: () => needsTypeBasedDetection,
18349
18641
  parseBlockBody: () => parseBlockBody,
18350
18642
  parseExpression: () => parseExpression,
18351
18643
  resetCompilerCounters: () => resetCompilerCounters,
18644
+ resolveSetters: () => resolveSetters,
18352
18645
  rewriteImportsForTemplate: () => rewriteImportsForTemplate,
18353
18646
  stringifyParsedExpr: () => stringifyParsedExpr,
18354
18647
  traceUpdatePath: () => traceUpdatePath
@@ -19174,9 +19467,78 @@ var init_emit_ledger = __esm({
19174
19467
  }
19175
19468
  });
19176
19469
 
19470
+ // src/lib/assets-ignore.ts
19471
+ import { resolve as resolve5 } from "node:path";
19472
+ async function isCloudflareWorkersProject(projectDir) {
19473
+ for (const name of WRANGLER_CONFIG_NAMES) {
19474
+ if (await fileExists(resolve5(projectDir, name))) return true;
19475
+ }
19476
+ return false;
19477
+ }
19478
+ function collectServerOnlyAssets(input) {
19479
+ const entries = /* @__PURE__ */ new Set();
19480
+ entries.add(`${input.devSentinelSubdir}/`);
19481
+ entries.add(EMIT_LEDGER_FILENAME);
19482
+ entries.add(CACHE_FILENAME);
19483
+ if (input.hasExternals) entries.add("barefoot-externals.json");
19484
+ if (!input.clientOnly) {
19485
+ entries.add(`${input.templatesSubdir}/manifest.json`);
19486
+ for (const row of Object.values(input.manifest)) {
19487
+ if (row.markedTemplate) entries.add(row.markedTemplate);
19488
+ }
19489
+ }
19490
+ return [...entries].sort();
19491
+ }
19492
+ function stripManagedBlock(content) {
19493
+ const lines = content.split("\n");
19494
+ const begin = lines.indexOf(BLOCK_BEGIN);
19495
+ const end = lines.indexOf(BLOCK_END);
19496
+ if (begin !== -1 && end !== -1 && end > begin) {
19497
+ const kept = [...lines.slice(0, begin), ...lines.slice(end + 1)];
19498
+ return kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
19499
+ }
19500
+ return content.trim();
19501
+ }
19502
+ function buildManagedBlock(entries) {
19503
+ return [
19504
+ BLOCK_BEGIN,
19505
+ "# Server/build-only barefoot outputs \u2014 not browser-served. Regenerated on",
19506
+ "# every `bf build`; add your own entries outside this block.",
19507
+ ...entries,
19508
+ BLOCK_END
19509
+ ].join("\n");
19510
+ }
19511
+ async function writeAssetsIgnore(outDir, entries) {
19512
+ const path23 = resolve5(outDir, ASSETS_IGNORE_FILENAME);
19513
+ const existing = await fileExists(path23) ? await readText(path23) : "";
19514
+ const userContent = stripManagedBlock(existing);
19515
+ const block = buildManagedBlock(entries);
19516
+ const merged = userContent.length > 0 ? `${userContent}
19517
+
19518
+ ${block}
19519
+ ` : `${block}
19520
+ `;
19521
+ return writeIfChanged(path23, merged);
19522
+ }
19523
+ var ASSETS_IGNORE_FILENAME, BLOCK_BEGIN, BLOCK_END, WRANGLER_CONFIG_NAMES;
19524
+ var init_assets_ignore = __esm({
19525
+ "src/lib/assets-ignore.ts"() {
19526
+ "use strict";
19527
+ init_runtime();
19528
+ init_fs_utils();
19529
+ init_build_cache();
19530
+ init_emit_ledger();
19531
+ ASSETS_IGNORE_FILENAME = ".assetsignore";
19532
+ BLOCK_BEGIN = "# >>> barefoot managed block (generated by `bf build`) >>>";
19533
+ BLOCK_END = "# <<< barefoot managed block <<<";
19534
+ WRANGLER_CONFIG_NAMES = ["wrangler.toml", "wrangler.json", "wrangler.jsonc"];
19535
+ }
19536
+ });
19537
+
19177
19538
  // src/lib/build.ts
19539
+ import ts19 from "typescript";
19178
19540
  import { mkdir, readdir, stat, unlink } from "node:fs/promises";
19179
- import { resolve as resolve5, basename, relative as relative2, dirname as dirname3, isAbsolute as isAbsolute2 } from "node:path";
19541
+ import { resolve as resolve6, basename, relative as relative2, dirname as dirname3, isAbsolute as isAbsolute2 } from "node:path";
19180
19542
  import { fileURLToPath as fileURLToPath2 } from "node:url";
19181
19543
  import { build as esbuildBuild } from "esbuild";
19182
19544
  function detectMissingUseClient(content) {
@@ -19216,7 +19578,7 @@ async function discoverComponentFiles(dir, options) {
19216
19578
  return results;
19217
19579
  }
19218
19580
  for (const entry of entries) {
19219
- const fullPath = resolve5(dir, String(entry.name));
19581
+ const fullPath = resolve6(dir, String(entry.name));
19220
19582
  if (entry.isDirectory()) {
19221
19583
  if (skipDirs?.has(String(entry.name))) continue;
19222
19584
  results.push(...await discoverComponentFiles(fullPath, options));
@@ -19231,9 +19593,9 @@ function generateHash(content) {
19231
19593
  }
19232
19594
  function resolveBuildConfigFromTs(projectDir, tsConfig, overrides) {
19233
19595
  const componentDirs = (tsConfig.components ?? ["components"]).map(
19234
- (dir) => resolve5(projectDir, dir)
19596
+ (dir) => resolve6(projectDir, dir)
19235
19597
  );
19236
- const outDir = resolve5(projectDir, tsConfig.outDir ?? "dist");
19598
+ const outDir = resolve6(projectDir, tsConfig.outDir ?? "dist");
19237
19599
  return {
19238
19600
  projectDir,
19239
19601
  adapter: tsConfig.adapter,
@@ -19248,7 +19610,7 @@ function resolveBuildConfigFromTs(projectDir, tsConfig, overrides) {
19248
19610
  externals: tsConfig.externals,
19249
19611
  externalsBasePath: tsConfig.externalsBasePath,
19250
19612
  bundleEntries: tsConfig.bundleEntries?.map((e) => ({
19251
- entry: resolve5(projectDir, e.entry),
19613
+ entry: resolve6(projectDir, e.entry),
19252
19614
  outfile: e.outfile,
19253
19615
  externals: e.externals
19254
19616
  })),
@@ -19258,9 +19620,9 @@ function resolveBuildConfigFromTs(projectDir, tsConfig, overrides) {
19258
19620
  async function findCliPackageJson() {
19259
19621
  const here = dirname3(fileURLToPath2(import.meta.url));
19260
19622
  const candidates = [
19261
- resolve5(here, "../package.json"),
19623
+ resolve6(here, "../package.json"),
19262
19624
  // bundled dist/index.js
19263
- resolve5(here, "../../package.json")
19625
+ resolve6(here, "../../package.json")
19264
19626
  // source src/lib/build.ts
19265
19627
  ];
19266
19628
  for (const cand of candidates) {
@@ -19272,7 +19634,7 @@ async function findNearestLockfile(projectDir) {
19272
19634
  let dir = projectDir;
19273
19635
  while (true) {
19274
19636
  for (const name of LOCKFILE_NAMES) {
19275
- const candidate = resolve5(dir, name);
19637
+ const candidate = resolve6(dir, name);
19276
19638
  if (await fileExists(candidate)) return candidate;
19277
19639
  }
19278
19640
  const parent = dirname3(dir);
@@ -19293,9 +19655,9 @@ async function computeGlobalHash(config) {
19293
19655
  parts.push(await readText(cliPkgPath));
19294
19656
  }
19295
19657
  const configCandidates = [
19296
- resolve5(config.projectDir, "barefoot.config.ts"),
19297
- resolve5(config.projectDir, "barefoot.config.js"),
19298
- resolve5(config.projectDir, "barefoot.config.mjs")
19658
+ resolve6(config.projectDir, "barefoot.config.ts"),
19659
+ resolve6(config.projectDir, "barefoot.config.js"),
19660
+ resolve6(config.projectDir, "barefoot.config.mjs")
19299
19661
  ];
19300
19662
  for (const cand of configCandidates) {
19301
19663
  if (await fileExists(cand)) {
@@ -19316,9 +19678,9 @@ async function build(config, options = {}) {
19316
19678
  const templatesSubdir = layout?.templates ?? "components";
19317
19679
  const clientJsSubdir = layout?.clientJs ?? "components";
19318
19680
  const runtimeSubdir = layout?.runtime ?? clientJsSubdir;
19319
- const templatesOutDir = resolve5(config.outDir, templatesSubdir);
19320
- const clientJsOutDir = resolve5(config.outDir, clientJsSubdir);
19321
- const runtimeOutDir = resolve5(config.outDir, runtimeSubdir);
19681
+ const templatesOutDir = resolve6(config.outDir, templatesSubdir);
19682
+ const clientJsOutDir = resolve6(config.outDir, clientJsSubdir);
19683
+ const runtimeOutDir = resolve6(config.outDir, runtimeSubdir);
19322
19684
  await Promise.all([
19323
19685
  mkdir(templatesOutDir, { recursive: true }),
19324
19686
  mkdir(clientJsOutDir, { recursive: true }),
@@ -19332,14 +19694,14 @@ async function build(config, options = {}) {
19332
19694
  let anyOutputChanged = false;
19333
19695
  const loadedLedger = await loadEmitLedger(config.outDir, config.projectDir);
19334
19696
  const previousEmitEntries = loadedLedger?.entries ?? extractLedgerFromCache(onDiskCache);
19335
- const domPkgDir = resolve5(config.projectDir, "node_modules/@barefootjs/client");
19697
+ const domPkgDir = resolve6(config.projectDir, "node_modules/@barefootjs/client");
19336
19698
  const domDistCandidates = [
19337
- resolve5(config.projectDir, "../../packages/client/dist/runtime/standalone.js"),
19338
- resolve5(domPkgDir, "dist/runtime/standalone.js"),
19699
+ resolve6(config.projectDir, "../../packages/client/dist/runtime/standalone.js"),
19700
+ resolve6(domPkgDir, "dist/runtime/standalone.js"),
19339
19701
  // Legacy fallback for older @barefootjs/client dists that only shipped
19340
19702
  // the single runtime entry.
19341
- resolve5(config.projectDir, "../../packages/client/dist/runtime/index.js"),
19342
- resolve5(domPkgDir, "dist/runtime/index.js")
19703
+ resolve6(config.projectDir, "../../packages/client/dist/runtime/index.js"),
19704
+ resolve6(domPkgDir, "dist/runtime/index.js")
19343
19705
  ];
19344
19706
  let domDistFile = null;
19345
19707
  for (const candidate of domDistCandidates) {
@@ -19351,7 +19713,7 @@ async function build(config, options = {}) {
19351
19713
  }
19352
19714
  }
19353
19715
  if (domDistFile) {
19354
- const runtimeOutPath = resolve5(runtimeOutDir, "barefoot.js");
19716
+ const runtimeOutPath = resolve6(runtimeOutDir, "barefoot.js");
19355
19717
  let runtimeContent;
19356
19718
  if (config.minify) {
19357
19719
  runtimeContent = transpile(await readText(domDistFile), { loader: "js", minify: true });
@@ -19524,9 +19886,9 @@ async function build(config, options = {}) {
19524
19886
  if (!currentEmitSet.has(output)) orphanedOutputs.add(output);
19525
19887
  }
19526
19888
  }
19527
- const outDirAbs = resolve5(config.outDir);
19889
+ const outDirAbs = resolve6(config.outDir);
19528
19890
  for (const output of orphanedOutputs) {
19529
- const abs = resolve5(config.outDir, output);
19891
+ const abs = resolve6(config.outDir, output);
19530
19892
  const rel = relative2(outDirAbs, abs);
19531
19893
  const escapes = rel === "" || rel === "." || rel.startsWith("..") || isAbsolute2(rel);
19532
19894
  if (escapes) {
@@ -19554,7 +19916,7 @@ async function build(config, options = {}) {
19554
19916
  if (!entry.clientJs) continue;
19555
19917
  let raw = compiledClientJsByKey.get(name);
19556
19918
  if (!raw) {
19557
- const filePath = resolve5(config.outDir, entry.clientJs);
19919
+ const filePath = resolve6(config.outDir, entry.clientJs);
19558
19920
  try {
19559
19921
  raw = await readText(filePath);
19560
19922
  } catch {
@@ -19569,7 +19931,7 @@ async function build(config, options = {}) {
19569
19931
  for (const [name, content] of combined) {
19570
19932
  const entry = manifest[name];
19571
19933
  if (!entry?.clientJs) continue;
19572
- const filePath = resolve5(config.outDir, entry.clientJs);
19934
+ const filePath = resolve6(config.outDir, entry.clientJs);
19573
19935
  if (await writeIfChanged(filePath, content)) {
19574
19936
  anyOutputChanged = true;
19575
19937
  console.log(`Combined: ${entry.clientJs}`);
@@ -19622,10 +19984,10 @@ async function build(config, options = {}) {
19622
19984
  }
19623
19985
  }
19624
19986
  {
19625
- const runtimeAbs = resolve5(config.outDir, runtimeSubdir, "barefoot.js");
19987
+ const runtimeAbs = resolve6(config.outDir, runtimeSubdir, "barefoot.js");
19626
19988
  for (const [name, entry] of Object.entries(manifest)) {
19627
19989
  if (!entry.clientJs || name === "__barefoot__") continue;
19628
- const filePath = resolve5(config.outDir, entry.clientJs);
19990
+ const filePath = resolve6(config.outDir, entry.clientJs);
19629
19991
  let rel = relative2(dirname3(filePath), runtimeAbs);
19630
19992
  if (!rel.startsWith(".")) rel = "./" + rel;
19631
19993
  try {
@@ -19648,7 +20010,7 @@ async function build(config, options = {}) {
19648
20010
  if (config.minify) {
19649
20011
  for (const [name, entry] of Object.entries(manifest)) {
19650
20012
  if (!entry.clientJs || name === "__barefoot__") continue;
19651
- const filePath = resolve5(config.outDir, entry.clientJs);
20013
+ const filePath = resolve6(config.outDir, entry.clientJs);
19652
20014
  try {
19653
20015
  const content = await readText(filePath);
19654
20016
  if (content) {
@@ -19673,8 +20035,8 @@ async function build(config, options = {}) {
19673
20035
  });
19674
20036
  }
19675
20037
  if (!config.clientOnly) {
19676
- const manifestDir = resolve5(config.outDir, templatesSubdir);
19677
- const manifestPath = resolve5(manifestDir, "manifest.json");
20038
+ const manifestDir = resolve6(config.outDir, templatesSubdir);
20039
+ const manifestPath = resolve6(manifestDir, "manifest.json");
19678
20040
  const manifestContent = JSON.stringify(manifest, null, 2);
19679
20041
  if (await writeIfChanged(manifestPath, manifestContent)) {
19680
20042
  anyOutputChanged = true;
@@ -19702,6 +20064,18 @@ async function build(config, options = {}) {
19702
20064
  saveCache(config.outDir, nextCache),
19703
20065
  saveEmitLedger(config.outDir, config.projectDir, nextLedger)
19704
20066
  ]);
20067
+ if (await isCloudflareWorkersProject(config.projectDir)) {
20068
+ const ignored = collectServerOnlyAssets({
20069
+ devSentinelSubdir: DEV_SENTINEL_SUBDIR,
20070
+ templatesSubdir,
20071
+ manifest,
20072
+ hasExternals: !!config.externals && Object.keys(config.externals).length > 0,
20073
+ clientOnly: config.clientOnly
20074
+ });
20075
+ if (await writeAssetsIgnore(config.outDir, ignored)) {
20076
+ console.log(`Generated: ${ASSETS_IGNORE_FILENAME}`);
20077
+ }
20078
+ }
19705
20079
  return {
19706
20080
  compiledCount,
19707
20081
  skippedCount,
@@ -19712,6 +20086,28 @@ async function build(config, options = {}) {
19712
20086
  sharedProgram
19713
20087
  };
19714
20088
  }
20089
+ function extractBareImports(code) {
20090
+ const { importedFiles } = ts19.preProcessFile(code, true, true);
20091
+ const specifiers = /* @__PURE__ */ new Set();
20092
+ for (const { fileName } of importedFiles) {
20093
+ if (!fileName.startsWith(".") && !fileName.startsWith("/") && !fileName.includes("://")) {
20094
+ specifiers.add(fileName);
20095
+ }
20096
+ }
20097
+ return [...specifiers];
20098
+ }
20099
+ function unresolvedBareImports(code, externals) {
20100
+ const keys = /* @__PURE__ */ new Set([...Object.keys(externals), ...BF_CLIENT_DEDUP_KEYS]);
20101
+ const isResolved = (spec) => keys.has(spec) || [...keys].some((k) => k.endsWith("/") && spec.startsWith(k));
20102
+ return extractBareImports(code).filter((spec) => !isResolved(spec));
20103
+ }
20104
+ function rebundleExternalsFor(pkgName, externals) {
20105
+ return [.../* @__PURE__ */ new Set([
20106
+ ...Object.keys(externals).filter((k) => k !== pkgName),
20107
+ ...BF_CLIENT_DEDUP_KEYS,
20108
+ "@barefootjs/client/*"
20109
+ ])];
20110
+ }
19715
20111
  function vendorChunkFilename(pkgName) {
19716
20112
  const base = pkgName.includes("/") ? pkgName.split("/").pop() : pkgName;
19717
20113
  return `${base}.js`;
@@ -19720,7 +20116,7 @@ function effectiveNamesFor(entryPath, componentDirs) {
19720
20116
  const bn = basename(entryPath);
19721
20117
  if (componentDirs && componentDirs.length > 0) {
19722
20118
  for (const dir of componentDirs) {
19723
- const root = resolve5(dir);
20119
+ const root = resolve6(dir);
19724
20120
  if (entryPath !== root && entryPath.startsWith(root + "/")) {
19725
20121
  const rel = entryPath.slice(root.length + 1);
19726
20122
  const noExt2 = rel.replace(/\.[^.]+$/, "");
@@ -19734,14 +20130,14 @@ function effectiveNamesFor(entryPath, componentDirs) {
19734
20130
  function buildRelativeImportRewriter(sourcePath, outputPath, componentDirs, templatesOutDir) {
19735
20131
  const sourceDir = dirname3(sourcePath);
19736
20132
  const outputDir = dirname3(outputPath);
19737
- const resolvedComponentDirs = componentDirs.map((d) => resolve5(d));
20133
+ const resolvedComponentDirs = componentDirs.map((d) => resolve6(d));
19738
20134
  return (importPath) => {
19739
- const srcAbs = resolve5(sourceDir, importPath);
20135
+ const srcAbs = resolve6(sourceDir, importPath);
19740
20136
  let targetAbs = srcAbs;
19741
20137
  for (const componentDir of resolvedComponentDirs) {
19742
20138
  if (srcAbs === componentDir || srcAbs.startsWith(componentDir + "/")) {
19743
20139
  const relUnderComponentDir = srcAbs.slice(componentDir.length + 1);
19744
- targetAbs = relUnderComponentDir ? resolve5(templatesOutDir, relUnderComponentDir) : templatesOutDir;
20140
+ targetAbs = relUnderComponentDir ? resolve6(templatesOutDir, relUnderComponentDir) : templatesOutDir;
19745
20141
  break;
19746
20142
  }
19747
20143
  }
@@ -19794,7 +20190,7 @@ function mergeDuplicateNamedImports(content) {
19794
20190
  return out.join("\n");
19795
20191
  }
19796
20192
  async function resolvePkgBrowserEntry(pkgDir) {
19797
- const pkgJsonPath = resolve5(pkgDir, "package.json");
20193
+ const pkgJsonPath = resolve6(pkgDir, "package.json");
19798
20194
  if (!await fileExists(pkgJsonPath)) return null;
19799
20195
  const pkg = JSON.parse(await readText(pkgJsonPath));
19800
20196
  const browserCandidates = [
@@ -19803,7 +20199,7 @@ async function resolvePkgBrowserEntry(pkgDir) {
19803
20199
  pkg.jsdelivr
19804
20200
  ].filter((v) => typeof v === "string");
19805
20201
  for (const rel of browserCandidates) {
19806
- const abs = resolve5(pkgDir, rel);
20202
+ const abs = resolve6(pkgDir, rel);
19807
20203
  if (await fileExists(abs)) return { path: abs, isBrowserReady: true };
19808
20204
  }
19809
20205
  const fallbackCandidates = [
@@ -19811,7 +20207,7 @@ async function resolvePkgBrowserEntry(pkgDir) {
19811
20207
  pkg.main
19812
20208
  ].filter((v) => typeof v === "string");
19813
20209
  for (const rel of fallbackCandidates) {
19814
- const abs = resolve5(pkgDir, rel);
20210
+ const abs = resolve6(pkgDir, rel);
19815
20211
  if (await fileExists(abs)) return { path: abs, isBrowserReady: false };
19816
20212
  }
19817
20213
  return null;
@@ -19834,35 +20230,41 @@ async function processExternals(config, runtimeSubdir, runtimeOutDir) {
19834
20230
  continue;
19835
20231
  }
19836
20232
  if (isChunk) {
19837
- const pkgDir = resolve5(config.projectDir, "node_modules", pkgName);
20233
+ const pkgDir = resolve6(config.projectDir, "node_modules", pkgName);
19838
20234
  const entry = await resolvePkgBrowserEntry(pkgDir);
19839
20235
  if (!entry) {
19840
20236
  console.warn(`Warning: externals \u2014 could not resolve browser entry for "${pkgName}". Skipping.`);
19841
20237
  continue;
19842
20238
  }
19843
20239
  const wantRebundle = typeof spec === "object" && !("url" in spec) && spec.rebundle === true;
19844
- if (!entry.isBrowserReady && !wantRebundle) {
19845
- console.warn(
19846
- `Warning: externals \u2014 "${pkgName}" resolved via import/main entry (no umd/unpkg/jsdelivr found). The copied file may contain external imports that are not browser-ready. Set rebundle: true to re-bundle it into a self-contained ESM file.`
19847
- );
19848
- }
19849
20240
  const srcFile = entry.path;
19850
20241
  const filename = vendorChunkFilename(pkgName);
19851
- const destPath = resolve5(runtimeOutDir, filename);
20242
+ const destPath = resolve6(runtimeOutDir, filename);
19852
20243
  if (wantRebundle) {
19853
20244
  await esbuildBuild({
19854
20245
  entryPoints: [srcFile],
19855
20246
  outfile: destPath,
19856
20247
  format: "esm",
19857
20248
  bundle: true,
19858
- minify: config.minify ?? false
20249
+ minify: config.minify ?? false,
20250
+ external: rebundleExternalsFor(pkgName, config.externals)
19859
20251
  });
19860
20252
  anyChanged = true;
19861
20253
  console.log(`Generated (bundled): ${runtimeSubdir}/${filename}`);
19862
20254
  } else {
19863
- let content = await readBytes(srcFile);
20255
+ const bytes = await readBytes(srcFile);
20256
+ const text = new TextDecoder().decode(bytes);
20257
+ if (!entry.isBrowserReady) {
20258
+ const unresolved = unresolvedBareImports(text, config.externals);
20259
+ if (unresolved.length > 0) {
20260
+ console.warn(
20261
+ `Warning: externals \u2014 "${pkgName}" resolved via import/main entry (no umd/unpkg/jsdelivr found) and imports packages not in the importmap: ${unresolved.join(", ")}. These are not browser-ready. Set rebundle: true to re-bundle it into a self-contained ESM file.`
20262
+ );
20263
+ }
20264
+ }
20265
+ let content = bytes;
19864
20266
  if (config.minify) {
19865
- content = transpile(content instanceof Uint8Array ? new TextDecoder().decode(content) : content, { loader: "js", minify: true });
20267
+ content = transpile(text, { loader: "js", minify: true });
19866
20268
  }
19867
20269
  if (await writeIfChanged(destPath, content)) {
19868
20270
  anyChanged = true;
@@ -19887,7 +20289,7 @@ async function processExternals(config, runtimeSubdir, runtimeOutDir) {
19887
20289
  preloads,
19888
20290
  externals: allExternals
19889
20291
  };
19890
- const manifestPath = resolve5(config.outDir, "barefoot-externals.json");
20292
+ const manifestPath = resolve6(config.outDir, "barefoot-externals.json");
19891
20293
  if (await writeIfChanged(manifestPath, JSON.stringify(manifest, null, 2))) {
19892
20294
  anyChanged = true;
19893
20295
  console.log("Generated: barefoot-externals.json");
@@ -19899,8 +20301,8 @@ async function processBundleEntries(config, clientJsOutDir, clientJsSubdir, allE
19899
20301
  let anyChanged = false;
19900
20302
  for (const entry of config.bundleEntries) {
19901
20303
  const entryExternals = [...allExternals, ...entry.externals ?? []];
19902
- const outfilePath = resolve5(clientJsOutDir, entry.outfile);
19903
- const absEntry = resolve5(entry.entry);
20304
+ const outfilePath = resolve6(clientJsOutDir, entry.outfile);
20305
+ const absEntry = resolve6(entry.entry);
19904
20306
  const cacheKey = `${BUNDLE_KEY_PREFIX}${absEntry}`;
19905
20307
  const sourceContent = await readText(absEntry);
19906
20308
  const sourceHash = hashContent(sourceContent);
@@ -19940,7 +20342,7 @@ async function processBundleEntries(config, clientJsOutDir, clientJsSubdir, allE
19940
20342
  if (metafile) {
19941
20343
  for (const inputPath of Object.keys(metafile.inputs)) {
19942
20344
  if (externalsSet.has(inputPath)) continue;
19943
- const abs = resolve5(absWorkingDir, inputPath);
20345
+ const abs = resolve6(absWorkingDir, inputPath);
19944
20346
  if (abs === absEntry) continue;
19945
20347
  if (abs.includes("/node_modules/")) continue;
19946
20348
  if (await fileExists(abs)) {
@@ -19965,7 +20367,7 @@ async function collectRelativeImportDeps(entryPath, sourceContent) {
19965
20367
  for (const match of sourceContent.matchAll(RELATIVE_IMPORT_SCAN_RE)) {
19966
20368
  const rel = match[1];
19967
20369
  if (!rel.startsWith(".")) continue;
19968
- const base = resolve5(baseDir, rel);
20370
+ const base = resolve6(baseDir, rel);
19969
20371
  const candidates = [base, ...EXT_CANDIDATES.map((ext) => base + ext)];
19970
20372
  for (const cand of candidates) {
19971
20373
  if (seen.has(cand)) continue;
@@ -19999,7 +20401,7 @@ async function compileEntry(args2) {
19999
20401
  for (const depPath of await collectRelativeImportDeps(entryPath, sourceContent)) {
20000
20402
  deps[depPath] = hashContent(await readText(depPath));
20001
20403
  }
20002
- const presumedOutputPath = resolve5(
20404
+ const presumedOutputPath = resolve6(
20003
20405
  templatesOutDir,
20004
20406
  baseFileName.replace(/\.tsx?$/, config.adapter.extension)
20005
20407
  );
@@ -20065,7 +20467,7 @@ async function compileEntry(args2) {
20065
20467
  if (hasClientJs) {
20066
20468
  const rel = `${clientJsSubdir}/${clientJsFilename}`;
20067
20469
  outputs.push(rel);
20068
- const target = resolve5(clientJsOutDir, clientJsFilename);
20470
+ const target = resolve6(clientJsOutDir, clientJsFilename);
20069
20471
  await mkdir(dirname3(target), { recursive: true });
20070
20472
  if (await writeIfChanged(target, clientJsContent)) {
20071
20473
  wroteAny = true;
@@ -20082,7 +20484,7 @@ async function compileEntry(args2) {
20082
20484
  }
20083
20485
  const rel = `${templatesSubdir}/${outName}`;
20084
20486
  outputs.push(rel);
20085
- const target = resolve5(templatesOutDir, outName);
20487
+ const target = resolve6(templatesOutDir, outName);
20086
20488
  await mkdir(dirname3(target), { recursive: true });
20087
20489
  if (await writeIfChanged(target, outputContent)) {
20088
20490
  wroteAny = true;
@@ -20127,14 +20529,14 @@ async function compileEntry(args2) {
20127
20529
  }
20128
20530
  async function writeBuildId(outDir, result) {
20129
20531
  if (!result.changed) return;
20130
- const devDir = resolve5(outDir, DEV_SENTINEL_SUBDIR);
20532
+ const devDir = resolve6(outDir, DEV_SENTINEL_SUBDIR);
20131
20533
  await mkdir(devDir, { recursive: true });
20132
- const path23 = resolve5(devDir, DEV_SENTINEL_FILENAME);
20534
+ const path23 = resolve6(devDir, DEV_SENTINEL_FILENAME);
20133
20535
  await writeIfChanged(path23, String(Date.now()));
20134
20536
  }
20135
20537
  async function watch(config, options = {}) {
20136
20538
  const { debounceMs = 100, signal } = options;
20137
- const { watch: fsWatch } = await import("node:fs/promises");
20539
+ const { watch: fsWatch2 } = await import("node:fs/promises");
20138
20540
  const initial = await build(config);
20139
20541
  let cachedProgram = initial.sharedProgram;
20140
20542
  console.log("");
@@ -20212,7 +20614,7 @@ async function watch(config, options = {}) {
20212
20614
  };
20213
20615
  const watchRoot = async (root, recursive) => {
20214
20616
  try {
20215
- const iter = fsWatch(root, { recursive, signal });
20617
+ const iter = fsWatch2(root, { recursive, signal });
20216
20618
  for await (const event of iter) {
20217
20619
  if (!isRelevant(root, event.filename)) continue;
20218
20620
  schedule();
@@ -20272,6 +20674,7 @@ var init_build = __esm({
20272
20674
  init_build_cache();
20273
20675
  init_emit_ledger();
20274
20676
  init_fs_utils();
20677
+ init_assets_ignore();
20275
20678
  init_runtime();
20276
20679
  init_resolve_imports();
20277
20680
  BF001_TRIPWIRE_IMPORTS = /* @__PURE__ */ new Set([
@@ -23588,7 +23991,7 @@ async function select(args2) {
23588
23991
  output.write(`\x1B[33m?\x1B[0m \x1B[1m${args2.message}\x1B[0m
23589
23992
  `);
23590
23993
  render(true);
23591
- return new Promise((resolve7, reject) => {
23994
+ return new Promise((resolve11, reject) => {
23592
23995
  readline.emitKeypressEvents(input);
23593
23996
  input.setRawMode?.(true);
23594
23997
  input.resume();
@@ -23633,7 +24036,7 @@ async function select(args2) {
23633
24036
  output.write(`\x1B[${totalLines}A`);
23634
24037
  output.write(`\u2714 ${args2.message} \x1B[1;32m${shortLabel}\x1B[0m
23635
24038
  `);
23636
- resolve7(args2.options[cursor].value);
24039
+ resolve11(args2.options[cursor].value);
23637
24040
  return;
23638
24041
  }
23639
24042
  };
@@ -24532,325 +24935,268 @@ var init_scaffold_layout = __esm({
24532
24935
  }
24533
24936
  });
24534
24937
 
24535
- // src/commands/preview.ts
24536
- var preview_exports = {};
24537
- __export(preview_exports, {
24538
- run: () => run8
24539
- });
24540
- import { existsSync as existsSync12, readdirSync as readdirSync5 } from "fs";
24541
- import path16 from "path";
24542
- function listPreviewableComponents(ctx2) {
24543
- const { writeRoot, componentsBasePath } = resolveScaffoldLayout(ctx2);
24544
- const componentsDir = path16.join(writeRoot, componentsBasePath);
24545
- if (!existsSync12(componentsDir)) return [];
24546
- const names = [];
24547
- for (const name of readdirSync5(componentsDir)) {
24548
- const previewFile = path16.join(componentsDir, name, "index.preview.tsx");
24549
- if (existsSync12(previewFile)) names.push(name);
24550
- }
24551
- return names.sort();
24552
- }
24553
- async function run8(args2, ctx2) {
24554
- const component = args2[0];
24555
- if (!component) {
24556
- const available = listPreviewableComponents(ctx2);
24557
- if (ctx2.jsonFlag) {
24558
- console.log(JSON.stringify({ previewable: available }, null, 2));
24559
- return;
24560
- }
24561
- if (available.length === 0) {
24562
- console.error("No previewable components found.");
24563
- console.error("Generate one with: bf gen preview <component>");
24564
- process.exit(1);
24938
+ // src/lib/preview/compile.ts
24939
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
24940
+ import { existsSync as existsSync12 } from "node:fs";
24941
+ import { execFileSync } from "node:child_process";
24942
+ import { resolve as resolve7, relative as relative3 } from "node:path";
24943
+ import { build as build2 } from "esbuild";
24944
+ async function compile(options) {
24945
+ const { assets, previewsPath, previewNames, componentName, liveReload } = options;
24946
+ const { rootDir, srcComponentsDir, tokensCss, globalsCss, runtimeStandalone, uno } = assets;
24947
+ const DIST_DIR = resolve7(rootDir, ".preview-dist");
24948
+ const MODULES_DIR = resolve7(DIST_DIR, "_modules");
24949
+ await mkdir2(MODULES_DIR, { recursive: true });
24950
+ console.log("Generating CSS...");
24951
+ await writeFile2(resolve7(DIST_DIR, "globals.css"), tokensCss + "\n" + globalsCss);
24952
+ console.log("Generated: .preview-dist/globals.css");
24953
+ console.log("Generating UnoCSS...");
24954
+ let unoConfigPath = uno.configPath;
24955
+ if (uno.configIsBundled) {
24956
+ unoConfigPath = resolve7(DIST_DIR, "uno.config.ts");
24957
+ await writeFile2(unoConfigPath, await readFile2(uno.configPath, "utf-8"));
24958
+ }
24959
+ execFileSync(uno.bin, [
24960
+ ...uno.globs,
24961
+ "--config",
24962
+ unoConfigPath,
24963
+ "-o",
24964
+ resolve7(DIST_DIR, "uno.css")
24965
+ ], { cwd: uno.cwd, stdio: "inherit" });
24966
+ console.log("Generated: .preview-dist/uno.css");
24967
+ const previewSource = await readFile2(previewsPath, "utf-8");
24968
+ const previewSiblings = [
24969
+ ...previewSource.matchAll(/from\s*['"]\.\.\/([a-z][a-z0-9-]*)(?:\/[^'"]*)?['"]/g)
24970
+ ].map((m) => m[1]);
24971
+ const componentFiles = resolveDependenciesFromSource(
24972
+ [componentName, ...previewSiblings],
24973
+ srcComponentsDir
24974
+ ).map((name) => resolve7(srcComponentsDir, name, "index.tsx")).filter(existsSync12);
24975
+ console.log(`Compiling ${componentFiles.length + 1} files...`);
24976
+ const allFiles = [...componentFiles, previewsPath];
24977
+ const adapter = new PreviewCsrAdapter();
24978
+ const previewKey = relative3(rootDir, previewsPath).replace(/\.tsx$/, "");
24979
+ const clientJsByKey = /* @__PURE__ */ new Map();
24980
+ let previewProducedClientJs = false;
24981
+ for (const filePath of allFiles) {
24982
+ const source = await readFile2(filePath, "utf-8");
24983
+ const isPreview = filePath === previewsPath;
24984
+ const result = compileJSX(source, filePath, { adapter });
24985
+ const errors = result.errors.filter((e) => e.severity === "error");
24986
+ const warnings = result.errors.filter((e) => e.severity === "warning");
24987
+ for (const w of warnings) console.warn(formatError(w, source, { projectDir: rootDir }));
24988
+ if (errors.length > 0) {
24989
+ for (const e of errors) console.error(formatError(e, source, { projectDir: rootDir }));
24990
+ if (isPreview) {
24991
+ throw new Error(`Preview compilation failed for ${previewKey} (see errors above).`);
24992
+ }
24993
+ continue;
24565
24994
  }
24566
- console.log(`${available.length} previewable component(s):`);
24567
- for (const name of available) console.log(` ${name}`);
24568
- console.log();
24569
- console.log("Open one with: bf preview <component>");
24570
- return;
24571
- }
24572
- let runPreview = null;
24573
- try {
24574
- const specifier = ["..", "..", "..", "preview", "src", "index"].join("/");
24575
- const mod = await import(specifier);
24576
- runPreview = mod.runPreview;
24577
- } catch {
24995
+ const clientJs = result.files.find((f) => f.type === "clientJs")?.content;
24996
+ if (!clientJs) continue;
24997
+ const key = relative3(rootDir, filePath).replace(/\.tsx$/, "");
24998
+ clientJsByKey.set(key, clientJs);
24999
+ if (isPreview) previewProducedClientJs = true;
24578
25000
  }
24579
- if (!runPreview) {
24580
- console.error("bf preview is not available in the npm distribution yet.");
24581
- console.error("Tracking issue: https://github.com/piconic-ai/barefootjs/issues/885");
24582
- console.error("Workaround: run the barefootjs monorepo locally with bun.");
24583
- process.exit(1);
25001
+ if (!previewProducedClientJs) {
25002
+ throw new Error(
25003
+ `Preview ${previewKey} produced no client JS. Each preview function must return a single root element (wrap multiple roots in <>...</>).`
25004
+ );
24584
25005
  }
24585
- await runPreview(component);
25006
+ const combined = combineParentChildClientJs(clientJsByKey);
25007
+ const allModules = new Map([...clientJsByKey, ...combined]);
25008
+ for (const [key, content] of allModules) {
25009
+ const safeName = key.replace(/[/\\]/g, "__") + ".js";
25010
+ await writeFile2(resolve7(MODULES_DIR, safeName), content);
25011
+ }
25012
+ const previewModuleFile = previewKey.replace(/[/\\]/g, "__") + ".js";
25013
+ const entrySource = generateEntryScript(previewModuleFile, previewNames);
25014
+ const entryPath = resolve7(DIST_DIR, "_entry.js");
25015
+ await writeFile2(entryPath, entrySource);
25016
+ console.log("Bundling for browser...");
25017
+ await build2({
25018
+ entryPoints: [entryPath],
25019
+ outfile: resolve7(DIST_DIR, "_bundle.js"),
25020
+ bundle: true,
25021
+ format: "esm",
25022
+ platform: "browser",
25023
+ minify: false,
25024
+ sourcemap: "inline",
25025
+ absWorkingDir: rootDir,
25026
+ alias: { "@barefootjs/client/runtime": runtimeStandalone },
25027
+ define: { "process.env.NODE_ENV": '"development"' }
25028
+ });
25029
+ console.log("Generated: .preview-dist/_bundle.js");
25030
+ await writeFile2(resolve7(DIST_DIR, "index.html"), generateHTML(componentName, liveReload));
25031
+ console.log("Generated: .preview-dist/index.html");
25032
+ return { distDir: DIST_DIR };
24586
25033
  }
24587
- var init_preview = __esm({
24588
- "src/commands/preview.ts"() {
24589
- "use strict";
24590
- init_scaffold_layout();
24591
- }
24592
- });
25034
+ function generateEntryScript(previewModuleFile, previewNames) {
25035
+ const namesJson = JSON.stringify(previewNames);
25036
+ return `import { render } from '@barefootjs/client/runtime'
25037
+ import './_modules/${previewModuleFile}'
25038
+
25039
+ const previews = ${namesJson}
25040
+ const app = document.getElementById('preview-root')
25041
+
25042
+ for (const name of previews) {
25043
+ const section = document.createElement('div')
25044
+ section.className = 'preview-section'
25045
+ section.dataset.preview = name
25046
+
25047
+ const title = document.createElement('div')
25048
+ title.className = 'preview-title'
25049
+ title.textContent = name.replace(/([a-z])([A-Z])/g, '$1 $2')
25050
+ section.appendChild(title)
25051
+
25052
+ const content = document.createElement('div')
25053
+ section.appendChild(content)
25054
+ app.appendChild(section)
24593
25055
 
24594
- // src/commands/tokens-apply.ts
24595
- var tokens_apply_exports = {};
24596
- __export(tokens_apply_exports, {
24597
- applyCssOverrides: () => applyCssOverrides,
24598
- applyTokenOverrides: () => applyTokenOverrides,
24599
- parseStudioUrl: () => parseStudioUrl,
24600
- resolveTokensCss: () => resolveTokensCss,
24601
- run: () => run9
24602
- });
24603
- import { existsSync as existsSync13, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
24604
- import path17 from "path";
24605
- async function run9(args2, ctx2) {
24606
- const url = args2[0];
24607
- if (!url) {
24608
- console.error("Error: tokens apply requires a Studio URL.");
24609
- console.error("Usage: bf tokens apply <url>");
24610
- process.exit(1);
24611
- }
24612
- const projectDir = ctx2.projectDir ?? process.cwd();
24613
- const tokensRelDir = ctx2.config?.paths.tokens ?? "tokens";
24614
- const studioConfig = parseStudioUrl(url);
24615
- if (!studioConfig) {
24616
- console.error("Error: could not decode Studio config from the URL (no `?c=` param or malformed payload).");
24617
- process.exit(1);
24618
- }
24619
- const cssPath = resolveTokensCss(projectDir, tokensRelDir);
24620
- if (!cssPath) {
24621
- console.error("Error: tokens.css not found. Checked:");
24622
- for (const p of candidateCssPaths(projectDir, tokensRelDir)) {
24623
- console.error(` - ${path17.relative(projectDir, p)}`);
24624
- }
24625
- console.error(" Run `npm create barefootjs@latest` to scaffold a project first.");
24626
- process.exit(1);
24627
- }
24628
- applyCssOverrides(cssPath, studioConfig);
24629
- console.log(` Patched ${path17.relative(projectDir, cssPath)}`);
24630
- const tokensJsonPath = path17.join(projectDir, tokensRelDir, "tokens.json");
24631
- if (existsSync13(tokensJsonPath)) {
24632
- applyTokenOverrides(tokensJsonPath, studioConfig);
24633
- console.log(` Patched ${path17.relative(projectDir, tokensJsonPath)}`);
24634
- }
24635
- }
24636
- function parseStudioUrl(url) {
24637
25056
  try {
24638
- const parsed = new URL(url);
24639
- const encoded = parsed.searchParams.get("c");
24640
- if (!encoded) return void 0;
24641
- const json = atob(decodeURIComponent(encoded));
24642
- return JSON.parse(json);
24643
- } catch {
24644
- return void 0;
25057
+ render(content, name, {})
25058
+ } catch (err) {
25059
+ content.textContent = 'Render error: ' + (err && err.message || err)
25060
+ console.error('[preview]', name, err)
24645
25061
  }
24646
25062
  }
24647
- function candidateCssPaths(projectDir, tokensRelDir) {
24648
- return [
24649
- path17.join(projectDir, tokensRelDir, "tokens.css"),
24650
- path17.join(projectDir, "public", "tokens.css"),
24651
- path17.join(projectDir, "static", "tokens.css")
24652
- ];
24653
- }
24654
- function resolveTokensCss(projectDir, tokensRelDir) {
24655
- for (const p of candidateCssPaths(projectDir, tokensRelDir)) {
24656
- if (existsSync13(p)) return p;
24657
- }
24658
- return void 0;
25063
+ `;
24659
25064
  }
24660
- function buildBlockOverrides(config) {
24661
- const root = {};
24662
- const dark = {};
24663
- if (config.spacing) root["--spacing"] = config.spacing;
24664
- if (config.radius) root["--radius"] = config.radius;
24665
- if (config.font) {
24666
- root["--font-sans"] = FONT_MAP[config.font] ?? config.font;
24667
- }
24668
- if (config.style) {
24669
- const preset = SHADOW_PRESETS[config.style];
24670
- if (preset) {
24671
- for (const [name, value] of Object.entries(preset)) {
24672
- root[`--${name}`] = value;
24673
- }
24674
- }
24675
- }
24676
- if (config.tokens) {
24677
- for (const [name, values] of Object.entries(config.tokens)) {
24678
- if (values.light !== void 0) root[`--${name}`] = values.light;
24679
- if (values.dark !== void 0) dark[`--${name}`] = values.dark;
24680
- }
24681
- }
24682
- return { root, dark };
24683
- }
24684
- function applyCssOverrides(cssPath, config) {
24685
- const overrides = buildBlockOverrides(config);
24686
- let css = readFileSync6(cssPath, "utf-8");
24687
- css = patchBlock(css, /:root\s*\{/, overrides.root);
24688
- css = patchBlock(css, /\.dark\s*\{/, overrides.dark);
24689
- writeFileSync4(cssPath, css);
25065
+ function generateHTML(componentName, liveReload = false) {
25066
+ const displayName = componentName.charAt(0).toUpperCase() + componentName.slice(1);
25067
+ return `<!DOCTYPE html>
25068
+ <html lang="en">
25069
+ <head>
25070
+ <meta charset="UTF-8" />
25071
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
25072
+ <title>${displayName} \u2014 Preview</title>
25073
+ <link rel="stylesheet" href="/globals.css" />
25074
+ <link rel="stylesheet" href="/uno.css" />
25075
+ <style>
25076
+ body {
25077
+ padding: 2rem;
25078
+ font-family: system-ui, -apple-system, sans-serif;
25079
+ }
25080
+ .preview-section {
25081
+ margin-bottom: 2rem;
25082
+ padding: 1.5rem;
25083
+ border: 1px solid var(--border);
25084
+ border-radius: var(--radius);
25085
+ }
25086
+ .preview-title {
25087
+ font-size: 0.875rem;
25088
+ font-weight: 500;
25089
+ color: var(--muted-foreground);
25090
+ margin-bottom: 1rem;
25091
+ text-transform: uppercase;
25092
+ letter-spacing: 0.05em;
25093
+ }
25094
+ h1 {
25095
+ font-size: 1.5rem;
25096
+ font-weight: 600;
25097
+ margin-bottom: 1.5rem;
25098
+ }
25099
+ #bf-theme-toggle {
25100
+ position: fixed;
25101
+ bottom: 1rem;
25102
+ right: 1rem;
25103
+ z-index: 9999;
25104
+ width: 2.5rem;
25105
+ height: 2.5rem;
25106
+ border-radius: var(--radius);
25107
+ border: 1px solid var(--border);
25108
+ background: var(--card);
25109
+ color: var(--foreground);
25110
+ display: flex;
25111
+ align-items: center;
25112
+ justify-content: center;
25113
+ cursor: pointer;
25114
+ box-shadow: 0 1px 3px rgba(0,0,0,.1);
25115
+ }
25116
+ #bf-theme-toggle:hover { background: var(--accent); }
25117
+ #bf-theme-toggle .sun { display: none; }
25118
+ #bf-theme-toggle .moon { display: block; }
25119
+ .dark #bf-theme-toggle .sun { display: block; }
25120
+ .dark #bf-theme-toggle .moon { display: none; }
25121
+ </style>
25122
+ </head>
25123
+ <body>
25124
+ <h1>${displayName}</h1>
25125
+ <div id="preview-root"></div>
25126
+ <button id="bf-theme-toggle" type="button" aria-label="Toggle dark mode"
25127
+ onclick="var r=document.documentElement;r.classList.add('theme-transition');r.classList.toggle('dark');setTimeout(function(){r.classList.remove('theme-transition')},300)">
25128
+ <svg class="sun" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
25129
+ <circle cx="12" cy="12" r="4"></circle>
25130
+ <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"></path>
25131
+ </svg>
25132
+ <svg class="moon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
25133
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
25134
+ </svg>
25135
+ </button>
25136
+ <script type="module" src="/_bundle.js"></script>
25137
+ ${liveReload ? LIVE_RELOAD_SCRIPT : ""}
25138
+ </body>
25139
+ </html>`;
24690
25140
  }
24691
- function patchBlock(css, openRe, overrides) {
24692
- if (Object.keys(overrides).length === 0) return css;
24693
- const openMatch = openRe.exec(css);
24694
- if (!openMatch) return css;
24695
- const blockStart = openMatch.index + openMatch[0].length;
24696
- let depth = 1;
24697
- let blockEnd = -1;
24698
- for (let i = blockStart; i < css.length; i++) {
24699
- const ch = css[i];
24700
- if (ch === "{") depth++;
24701
- else if (ch === "}") {
24702
- depth--;
24703
- if (depth === 0) {
24704
- blockEnd = i;
24705
- break;
25141
+ var EMPTY_OUTPUT, PreviewCsrAdapter, LIVE_RELOAD_SCRIPT;
25142
+ var init_compile = __esm({
25143
+ "src/lib/preview/compile.ts"() {
25144
+ "use strict";
25145
+ init_src2();
25146
+ init_dependency_resolver();
25147
+ EMPTY_OUTPUT = Object.freeze({
25148
+ template: "",
25149
+ sections: Object.freeze({ imports: "", types: "", component: "", defaultExport: "" }),
25150
+ extension: ".tsx"
25151
+ });
25152
+ PreviewCsrAdapter = class extends BaseAdapter {
25153
+ name = "csr";
25154
+ extension = ".tsx";
25155
+ acceptsTemplateCall = () => true;
25156
+ generate() {
25157
+ return EMPTY_OUTPUT;
25158
+ }
25159
+ renderNode() {
25160
+ return "";
24706
25161
  }
24707
- }
24708
- }
24709
- if (blockEnd === -1) return css;
24710
- let block = css.slice(blockStart, blockEnd);
24711
- const toAppend = [];
24712
- for (const [name, value] of Object.entries(overrides)) {
24713
- const re = new RegExp(`(${escapeRegex2(name)}\\s*:\\s*)[^;]+(;)`);
24714
- if (re.test(block)) {
24715
- block = block.replace(re, `$1${value}$2`);
24716
- } else {
24717
- toAppend.push([name, value]);
24718
- }
24719
- }
24720
- if (toAppend.length > 0) {
24721
- const lines = toAppend.map(([n, v]) => ` ${n}: ${v};`).join("\n");
24722
- const trailing = block.match(/\s*$/)?.[0] ?? "";
24723
- block = block.slice(0, block.length - trailing.length) + `
24724
-
24725
- /* \u2500\u2500 Studio overrides \u2500\u2500 */
24726
- ${lines}
24727
- `;
24728
- }
24729
- return css.slice(0, blockStart) + block + css.slice(blockEnd);
24730
- }
24731
- function escapeRegex2(s) {
24732
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
24733
- }
24734
- function applyTokenOverrides(tokensJsonPath, config) {
24735
- const raw = readFileSync6(tokensJsonPath, "utf-8");
24736
- const tokensData = JSON.parse(raw);
24737
- if (config.tokens) {
24738
- for (const [name, values] of Object.entries(config.tokens)) {
24739
- applyColorOverride(tokensData, name, values);
24740
- }
24741
- }
24742
- if (config.spacing) {
24743
- applySimpleOverride(tokensData, "--spacing", config.spacing);
24744
- }
24745
- if (config.radius) {
24746
- applySimpleOverride(tokensData, "--radius", config.radius);
24747
- }
24748
- if (config.font) {
24749
- const fontValue = FONT_MAP[config.font] || config.font;
24750
- applySimpleOverride(tokensData, "--font-sans", fontValue);
24751
- }
24752
- if (config.style) {
24753
- applyShadowPreset(tokensData, config.style);
24754
- }
24755
- writeFileSync4(tokensJsonPath, JSON.stringify(tokensData, null, 2) + "\n");
24756
- }
24757
- function applyColorOverride(tokensData, name, values) {
24758
- const varName = `--${name}`;
24759
- if (Array.isArray(tokensData.colors)) {
24760
- for (const token of tokensData.colors) {
24761
- if (token.name === varName || token.name === name) {
24762
- if (values.light) token.value = values.light;
24763
- if (values.dark) token.dark = values.dark;
24764
- return;
25162
+ renderElement() {
25163
+ return "";
24765
25164
  }
24766
- }
24767
- }
24768
- if (Array.isArray(tokensData.tokens)) {
24769
- for (const token of tokensData.tokens) {
24770
- if (token.name === varName || token.name === name) {
24771
- if (values.light) token.value = values.light;
24772
- if (values.dark) token.dark = values.dark;
24773
- return;
25165
+ renderExpression() {
25166
+ return "";
24774
25167
  }
24775
- }
24776
- }
24777
- }
24778
- function applySimpleOverride(tokensData, name, value) {
24779
- const bareName = name.startsWith("--") ? name.slice(2) : name;
24780
- const sections = [
24781
- tokensData.colors,
24782
- tokensData.spacing,
24783
- tokensData.borderRadius,
24784
- tokensData.shadows,
24785
- tokensData.layout
24786
- ];
24787
- if (tokensData.typography) {
24788
- for (const arr of Object.values(tokensData.typography)) {
24789
- if (Array.isArray(arr)) sections.push(arr);
24790
- }
24791
- }
24792
- for (const arr of sections) {
24793
- if (!Array.isArray(arr)) continue;
24794
- for (const token of arr) {
24795
- if (token.name === bareName || token.name === name) {
24796
- token.value = value;
24797
- return;
25168
+ renderConditional() {
25169
+ return "";
24798
25170
  }
24799
- }
24800
- }
24801
- }
24802
- function applyShadowPreset(tokensData, styleName) {
24803
- const shadows = SHADOW_PRESETS[styleName];
24804
- if (!shadows) return;
24805
- for (const [name, value] of Object.entries(shadows)) {
24806
- applySimpleOverride(tokensData, name, value);
24807
- }
24808
- }
24809
- var FONT_MAP, SHADOW_PRESETS;
24810
- var init_tokens_apply = __esm({
24811
- "src/commands/tokens-apply.ts"() {
24812
- "use strict";
24813
- FONT_MAP = {
24814
- system: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif',
24815
- inter: '"Inter", sans-serif',
24816
- "noto-sans": '"Noto Sans", sans-serif',
24817
- "nunito-sans": '"Nunito Sans", sans-serif',
24818
- figtree: '"Figtree", sans-serif'
24819
- };
24820
- SHADOW_PRESETS = {
24821
- Sharp: {
24822
- "shadow-sm": "0 1px 2px 0 rgb(0 0 0 / 0.04)",
24823
- "shadow": "0 1px 2px 0 rgb(0 0 0 / 0.06)",
24824
- "shadow-md": "0 2px 4px -1px rgb(0 0 0 / 0.08)",
24825
- "shadow-lg": "0 4px 8px -2px rgb(0 0 0 / 0.1)"
24826
- },
24827
- Soft: {
24828
- "shadow-sm": "0 1px 3px 0 rgb(0 0 0 / 0.06)",
24829
- "shadow": "0 2px 6px 0 rgb(0 0 0 / 0.08), 0 1px 3px -1px rgb(0 0 0 / 0.06)",
24830
- "shadow-md": "0 6px 12px -2px rgb(0 0 0 / 0.08), 0 3px 6px -3px rgb(0 0 0 / 0.06)",
24831
- "shadow-lg": "0 12px 24px -4px rgb(0 0 0 / 0.08), 0 6px 10px -5px rgb(0 0 0 / 0.06)"
24832
- },
24833
- Compact: {
24834
- "shadow-sm": "none",
24835
- "shadow": "none",
24836
- "shadow-md": "none",
24837
- "shadow-lg": "0 1px 2px 0 rgb(0 0 0 / 0.05)"
25171
+ renderLoop() {
25172
+ return "";
25173
+ }
25174
+ renderComponent() {
25175
+ return "";
25176
+ }
25177
+ renderScopeMarker() {
25178
+ return "";
25179
+ }
25180
+ renderSlotMarker() {
25181
+ return "";
25182
+ }
25183
+ renderCondMarker() {
25184
+ return "";
24838
25185
  }
24839
25186
  };
25187
+ LIVE_RELOAD_SCRIPT = `<script>
25188
+ (function(){var seen;function poll(){fetch('/__preview_reload').then(function(r){return r.text()}).then(function(v){if(seen!==undefined&&v!==seen){location.reload();return}seen=v}).catch(function(){}).then(function(){setTimeout(poll,1000)})}poll()})()
25189
+ </script>`;
24840
25190
  }
24841
25191
  });
24842
25192
 
24843
- // src/commands/tokens.ts
24844
- var tokens_exports = {};
24845
- __export(tokens_exports, {
24846
- run: () => run10
24847
- });
24848
- import { readFile as readFile2 } from "node:fs/promises";
24849
- import { existsSync as existsSync14 } from "node:fs";
24850
- import { resolve as resolve6, dirname as dirname4 } from "node:path";
25193
+ // src/lib/tokens.ts
25194
+ import { readFile as readFile3 } from "node:fs/promises";
25195
+ import { existsSync as existsSync13 } from "node:fs";
25196
+ import { resolve as resolve8, dirname as dirname4 } from "node:path";
24851
25197
  import { fileURLToPath as fileURLToPath4 } from "node:url";
24852
25198
  async function loadTokens(jsonPath) {
24853
- const content = await readFile2(jsonPath, "utf-8");
25199
+ const content = await readFile3(jsonPath, "utf-8");
24854
25200
  return JSON.parse(content);
24855
25201
  }
24856
25202
  function mergeTokenSets(...sets) {
@@ -24874,26 +25220,64 @@ function mergeTokenSets(...sets) {
24874
25220
  function mergeTokenArray(base, ext) {
24875
25221
  for (const token of ext) {
24876
25222
  const idx = base.findIndex((t) => t.name === token.name);
24877
- if (idx >= 0) {
24878
- base[idx] = token;
24879
- } else {
24880
- base.push(token);
24881
- }
24882
- }
25223
+ if (idx >= 0) base[idx] = token;
25224
+ else base.push(token);
25225
+ }
25226
+ }
25227
+ function generateCSS(tokenSet) {
25228
+ const rootLines = [];
25229
+ const darkLines = [];
25230
+ function addSection(label, tokens) {
25231
+ if (tokens.length === 0) return;
25232
+ rootLines.push(` /* \u2500\u2500 ${label} ${"\u2500".repeat(Math.max(1, 50 - label.length))} */`);
25233
+ for (const t of tokens) rootLines.push(` --${t.name}: ${t.value};`);
25234
+ rootLines.push("");
25235
+ }
25236
+ function addColorSection(tokens) {
25237
+ if (tokens.length === 0) return;
25238
+ rootLines.push(` /* \u2500\u2500 Colors (OKLCH, neutral theme) ${"\u2500".repeat(15)} */`);
25239
+ for (const t of tokens) rootLines.push(` --${t.name}: ${t.value};`);
25240
+ rootLines.push("");
25241
+ for (const t of tokens.filter((t2) => t2.dark)) {
25242
+ darkLines.push(` --${t.name}: ${t.dark};`);
25243
+ }
25244
+ }
25245
+ addSection("Typography", [...tokenSet.typography.fontFamily, ...tokenSet.typography.letterSpacing]);
25246
+ addSection("Spacing scale", tokenSet.spacing);
25247
+ addSection("Border radius", tokenSet.borderRadius);
25248
+ addSection("Transitions", [...tokenSet.transitions.duration, ...tokenSet.transitions.easing]);
25249
+ addSection("Layout", tokenSet.layout);
25250
+ addColorSection(tokenSet.colors);
25251
+ addSection("Shadows", tokenSet.shadows);
25252
+ const header = `/**
25253
+ * AUTO-GENERATED \u2014 Do not edit manually.
25254
+ * Generated from tokens.json.
25255
+ *
25256
+ * BarefootJS Design Tokens
25257
+ */`;
25258
+ let css = `${header}
25259
+
25260
+ :root {
25261
+ ${rootLines.join("\n")}}
25262
+ `;
25263
+ if (darkLines.length > 0) css += `
25264
+ .dark {
25265
+ ${darkLines.join("\n")}
25266
+ }
25267
+ `;
25268
+ return css;
24883
25269
  }
24884
25270
  function findBaseTokensJson(ctx2) {
24885
- const monorepoTokens = resolve6(ctx2.root, "site/shared/tokens/tokens.json");
24886
- if (existsSync14(monorepoTokens)) return monorepoTokens;
24887
- const bundledTokens = resolve6(dirname4(thisFile2), "tokens.json");
24888
- if (existsSync14(bundledTokens)) return bundledTokens;
25271
+ const monorepoTokens = resolve8(ctx2.root, "site/shared/tokens/tokens.json");
25272
+ if (existsSync13(monorepoTokens)) return monorepoTokens;
25273
+ const bundledTokens = resolve8(dirname4(thisFile2), "tokens.json");
25274
+ if (existsSync13(bundledTokens)) return bundledTokens;
24889
25275
  return null;
24890
25276
  }
24891
25277
  async function loadTokenSet(ctx2) {
24892
25278
  if (ctx2.projectDir && ctx2.config?.paths.tokens) {
24893
- const userTokens = resolve6(ctx2.projectDir, ctx2.config.paths.tokens, "tokens.json");
24894
- if (await fileExists(userTokens)) {
24895
- return loadTokens(userTokens);
24896
- }
25279
+ const userTokens = resolve8(ctx2.projectDir, ctx2.config.paths.tokens, "tokens.json");
25280
+ if (await fileExists(userTokens)) return loadTokens(userTokens);
24897
25281
  }
24898
25282
  const basePath = findBaseTokensJson(ctx2);
24899
25283
  if (!basePath) {
@@ -24902,1045 +25286,1679 @@ async function loadTokenSet(ctx2) {
24902
25286
  );
24903
25287
  }
24904
25288
  const base = await loadTokens(basePath);
24905
- const uiJsonPath = resolve6(ctx2.root, "site/ui/tokens.json");
25289
+ const uiJsonPath = resolve8(ctx2.root, "site/ui/tokens.json");
24906
25290
  if (await fileExists(uiJsonPath)) {
24907
- const ext = await loadTokens(uiJsonPath);
24908
- return mergeTokenSets(base, ext);
25291
+ return mergeTokenSets(base, await loadTokens(uiJsonPath));
24909
25292
  }
24910
25293
  return base;
24911
25294
  }
24912
- function flattenTokens(tokenSet, category) {
24913
- const result = [];
24914
- function add(cat, tokens) {
24915
- if (category && category !== cat) return;
24916
- result.push(...tokens);
24917
- }
24918
- add("typography", [...tokenSet.typography.fontFamily, ...tokenSet.typography.letterSpacing]);
24919
- add("spacing", tokenSet.spacing);
24920
- add("borderRadius", tokenSet.borderRadius);
24921
- add("transitions", [...tokenSet.transitions.duration, ...tokenSet.transitions.easing]);
24922
- add("layout", tokenSet.layout);
24923
- add("colors", tokenSet.colors);
24924
- add("shadows", tokenSet.shadows);
24925
- return result;
24926
- }
24927
- function printTokens(tokens, jsonFlag2) {
24928
- if (jsonFlag2) {
24929
- console.log(JSON.stringify(tokens, null, 2));
24930
- return;
24931
- }
24932
- if (tokens.length === 0) {
24933
- console.log("No tokens found.");
24934
- return;
24935
- }
24936
- const nameWidth = Math.max(25, ...tokens.map((t) => t.name.length + 4));
24937
- const header = `${"NAME".padEnd(nameWidth)}VALUE`;
24938
- console.log(header);
24939
- console.log("-".repeat(header.length + 20));
24940
- for (const t of tokens) {
24941
- const name = `--${t.name}`;
24942
- const dark = t.dark;
24943
- const darkSuffix = dark ? ` (dark: ${dark})` : "";
24944
- console.log(`${name.padEnd(nameWidth)}${t.value}${darkSuffix}`);
24945
- }
24946
- console.log(`
24947
- ${tokens.length} token(s)`);
24948
- }
24949
- async function run10(args2, ctx2) {
24950
- let category;
24951
- const catIdx = args2.indexOf("--category");
24952
- if (catIdx >= 0 && args2[catIdx + 1]) {
24953
- const val = args2[catIdx + 1];
24954
- if (!CATEGORY_NAMES.includes(val)) {
24955
- console.error(`Unknown category: ${val}`);
24956
- console.error(`Available: ${CATEGORY_NAMES.join(", ")}`);
24957
- process.exit(1);
24958
- }
24959
- category = val;
24960
- }
24961
- const tokenSet = await loadTokenSet(ctx2);
24962
- const tokens = flattenTokens(tokenSet, category);
24963
- printTokens(tokens, ctx2.jsonFlag);
25295
+ async function loadTokensCss(ctx2) {
25296
+ return generateCSS(await loadTokenSet(ctx2));
24964
25297
  }
24965
- var thisFile2, CATEGORY_NAMES;
25298
+ var thisFile2;
24966
25299
  var init_tokens = __esm({
24967
- "src/commands/tokens.ts"() {
25300
+ "src/lib/tokens.ts"() {
24968
25301
  "use strict";
24969
25302
  init_runtime();
24970
25303
  thisFile2 = fileURLToPath4(import.meta.url);
24971
- CATEGORY_NAMES = [
24972
- "typography",
24973
- "spacing",
24974
- "borderRadius",
24975
- "transitions",
24976
- "layout",
24977
- "colors",
24978
- "shadows"
24979
- ];
24980
25304
  }
24981
25305
  });
24982
25306
 
24983
- // src/lib/scaffold.ts
24984
- import { readFileSync as readFileSync7, existsSync as existsSync15 } from "fs";
24985
- import path18 from "path";
24986
- function loadMeta(metaDir, name) {
24987
- const filePath = path18.join(metaDir, `${name}.json`);
24988
- if (!existsSync15(filePath)) return null;
24989
- return JSON.parse(readFileSync7(filePath, "utf-8"));
25307
+ // src/lib/preview/errors.ts
25308
+ var PreviewError;
25309
+ var init_errors2 = __esm({
25310
+ "src/lib/preview/errors.ts"() {
25311
+ "use strict";
25312
+ PreviewError = class extends Error {
25313
+ };
25314
+ }
25315
+ });
25316
+
25317
+ // src/lib/preview/assets.ts
25318
+ import { existsSync as existsSync14 } from "node:fs";
25319
+ import { readFile as readFile4 } from "node:fs/promises";
25320
+ import { resolve as resolve9, dirname as dirname5 } from "node:path";
25321
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
25322
+ function firstExisting(...candidates) {
25323
+ return candidates.find((c) => !!c && existsSync14(c));
25324
+ }
25325
+ function unoBinCandidates(dir) {
25326
+ return ["unocss", "unocss.cmd", "unocss.CMD"].map((n) => resolve9(dir, "node_modules/.bin", n));
25327
+ }
25328
+ async function resolvePreviewAssets(ctx2) {
25329
+ const monorepo = ctx2.config === null;
25330
+ const projectDir = ctx2.projectDir;
25331
+ const rootDir = projectDir ?? ctx2.root;
25332
+ const srcComponentsDir = monorepo ? resolve9(ctx2.root, "ui/components/ui") : resolve9(projectDir, ctx2.config.paths.components);
25333
+ const tokensCss = await loadTokensCss(ctx2);
25334
+ const globalsPath = firstExisting(
25335
+ projectDir && resolve9(projectDir, "styles/globals.css"),
25336
+ projectDir && resolve9(projectDir, "globals.css"),
25337
+ projectDir && resolve9(projectDir, "app/globals.css"),
25338
+ monorepo ? resolve9(ctx2.root, "site/ui/styles/globals.css") : void 0,
25339
+ resolve9(assetDir, "preview-globals.css")
25340
+ );
25341
+ const globalsCss = globalsPath ? await readFile4(globalsPath, "utf-8") : "";
25342
+ const bundledUnoConfig = resolve9(assetDir, "preview-uno.config.ts");
25343
+ const configPath = firstExisting(
25344
+ projectDir && resolve9(projectDir, "uno.config.ts"),
25345
+ projectDir && resolve9(projectDir, "uno.config.js"),
25346
+ monorepo ? resolve9(ctx2.root, "site/ui/uno.config.ts") : void 0,
25347
+ bundledUnoConfig
25348
+ );
25349
+ if (!configPath) {
25350
+ throw new PreviewError(
25351
+ "No UnoCSS config found and the bundled default is missing \u2014 reinstall @barefootjs/cli."
25352
+ );
25353
+ }
25354
+ const unoCwd = monorepo ? resolve9(ctx2.root, "site/ui") : rootDir;
25355
+ const globs = monorepo ? ["../../ui/components/**/*.tsx", "./**/*.tsx", "./dist/**/*.tsx"] : [resolve9(srcComponentsDir, "**/*.tsx")];
25356
+ const unoBin = firstExisting(
25357
+ ...unoBinCandidates(rootDir),
25358
+ ...monorepo ? unoBinCandidates(resolve9(ctx2.root, "site/ui")) : [],
25359
+ ...monorepo ? unoBinCandidates(ctx2.root) : []
25360
+ );
25361
+ if (!unoBin) {
25362
+ throw new PreviewError(
25363
+ "UnoCSS CLI not found. Install it in your project to generate preview styles:\n npm install -D unocss@^66 @unocss/cli@^66\n(v66 matches the bundled config's presetWind4; older majors won't generate the same utilities.)"
25364
+ );
25365
+ }
25366
+ const runtimeStandalone = firstExisting(
25367
+ monorepo ? resolve9(ctx2.root, "packages/client/dist/runtime/standalone.js") : void 0,
25368
+ resolve9(rootDir, "node_modules/@barefootjs/client/dist/runtime/standalone.js"),
25369
+ resolve9(rootDir, "node_modules/@barefootjs/client/dist/runtime/index.js")
25370
+ );
25371
+ if (!runtimeStandalone) {
25372
+ throw new PreviewError(
25373
+ "The @barefootjs/client runtime was not found. Install @barefootjs/client in your project (its dist must include runtime/standalone.js)."
25374
+ );
25375
+ }
25376
+ return {
25377
+ rootDir,
25378
+ srcComponentsDir,
25379
+ tokensCss,
25380
+ globalsCss,
25381
+ runtimeStandalone,
25382
+ uno: { bin: unoBin, cwd: unoCwd, configPath, configIsBundled: configPath === bundledUnoConfig, globs }
25383
+ };
24990
25384
  }
25385
+ var assetDir;
25386
+ var init_assets = __esm({
25387
+ "src/lib/preview/assets.ts"() {
25388
+ "use strict";
25389
+ init_tokens();
25390
+ init_errors2();
25391
+ assetDir = dirname5(fileURLToPath5(import.meta.url));
25392
+ }
25393
+ });
25394
+
25395
+ // src/lib/preview-generate.ts
24991
25396
  function toPascalCase(kebab) {
24992
25397
  return kebab.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join("");
24993
25398
  }
24994
- function toCamelCase(kebab) {
24995
- const pascal = toPascalCase(kebab);
24996
- return pascal[0].toLowerCase() + pascal.slice(1);
25399
+ function toKebabCase(pascal) {
25400
+ return pascal.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
24997
25401
  }
24998
- function scaffold(componentName, useComponents, metaDir, componentsBasePath = "ui/components/ui", options = {}) {
24999
- const metas = useComponents.map((name) => ({ name, meta: loadMeta(metaDir, name) }));
25000
- const found = metas.filter((m) => m.meta !== null);
25001
- const notFound = metas.filter((m) => m.meta === null).map((m) => m.name);
25002
- const needsClient = found.some((m) => m.meta.stateful);
25003
- const imports = buildImports(found);
25004
- const componentCode = generateComponentCode(componentName, imports, needsClient, notFound);
25005
- const testCode = generateTestCode(componentName, needsClient, options.testImportSource ?? "bun:test");
25006
- const basePath = `${componentsBasePath}/${componentName}`;
25007
- return {
25008
- componentCode,
25009
- testCode,
25010
- componentPath: `${basePath}/index.tsx`,
25011
- testPath: `${basePath}/index.test.tsx`
25012
- };
25402
+ function capitalize2(s) {
25403
+ return s.charAt(0).toUpperCase() + s.slice(1);
25013
25404
  }
25014
- function buildImports(components) {
25015
- const imports = [];
25016
- for (const { name, meta } of components) {
25017
- const names = [toPascalCase(name)];
25018
- if (meta.subComponents) {
25019
- for (const sub2 of meta.subComponents) {
25020
- names.push(sub2.name);
25021
- }
25405
+ function inferVariantPropName(typeName, props) {
25406
+ const match = props.find((p) => p.type === typeName);
25407
+ return match ? match.name : null;
25408
+ }
25409
+ function findExternalTags(code, knownNames) {
25410
+ const external = [];
25411
+ for (const m of code.matchAll(/<([A-Z][a-zA-Z0-9]*)/g)) {
25412
+ if (!knownNames.has(m[1]) && !external.includes(m[1])) {
25413
+ external.push(m[1]);
25022
25414
  }
25023
- imports.push({ from: `../${name}`, names });
25024
25415
  }
25025
- return imports;
25416
+ return external;
25026
25417
  }
25027
- function generateComponentCode(componentName, imports, needsClient, notFound) {
25028
- const pascalName = toPascalCase(componentName);
25029
- const lines = [];
25030
- if (needsClient) {
25031
- lines.push(`"use client"`);
25032
- lines.push(``);
25033
- lines.push(`import { createSignal } from '@barefootjs/client'`);
25418
+ function resolveExternalTagImport(tag, parentModuleName, parentPascalName) {
25419
+ if (tag.endsWith("Icon")) {
25420
+ return { from: "../icon", name: tag };
25034
25421
  }
25035
- lines.push(`import type { Child } from '../../../types'`);
25036
- for (const imp of imports) {
25037
- lines.push(`import { ${imp.names.join(", ")} } from '${imp.from}'`);
25422
+ if (tag.startsWith(parentPascalName)) {
25423
+ return { from: `../${parentModuleName}`, name: tag };
25038
25424
  }
25039
- if (notFound.length > 0) {
25040
- lines.push(``);
25041
- lines.push(`// WARNING: These components were not found in ui/meta/:`);
25042
- for (const name of notFound) {
25043
- lines.push(`// - ${name}`);
25044
- }
25425
+ return { from: `../${toKebabCase(tag)}`, name: tag };
25426
+ }
25427
+ function emitJsxReturn(lines, jsx, indent = " ") {
25428
+ const jsxLines = jsx.split("\n");
25429
+ const tagLines = jsxLines.filter((l) => /^\s*<[A-Za-z]/.test(l));
25430
+ if (tagLines.length === 0) {
25431
+ lines.push(`${indent}return ${jsx}`);
25432
+ return;
25045
25433
  }
25046
- lines.push(``);
25047
- lines.push(`interface ${pascalName}Props {`);
25048
- lines.push(` /** Additional CSS classes. */`);
25049
- lines.push(` className?: string`);
25050
- lines.push(` /** Children to render. */`);
25051
- lines.push(` children?: Child`);
25052
- lines.push(`}`);
25053
- lines.push(``);
25054
- if (needsClient) {
25055
- lines.push(`function ${pascalName}(props: ${pascalName}Props) {`);
25056
- lines.push(` // TODO: Add signals`);
25057
- lines.push(` // const [value, setValue] = createSignal(...)`);
25058
- lines.push(``);
25059
- lines.push(` return (`);
25060
- lines.push(` <div data-slot="${componentName}">`);
25061
- lines.push(` {/* TODO: Compose components */}`);
25062
- lines.push(` </div>`);
25063
- lines.push(` )`);
25064
- lines.push(`}`);
25434
+ const minIndent = Math.min(...tagLines.map((l) => l.match(/^(\s*)/)?.[1].length ?? 0));
25435
+ const rootElements = tagLines.filter((l) => (l.match(/^(\s*)/)?.[1].length ?? 0) === minIndent);
25436
+ const needsFragment = rootElements.length > 1 && !jsx.trim().startsWith("<>");
25437
+ if (jsxLines.length === 1 && !needsFragment) {
25438
+ const singleLineRoots = (jsx.match(/<(?![/])[A-Za-z]/g) ?? []).length;
25439
+ if (singleLineRoots > 1) {
25440
+ lines.push(`${indent}return (<>${jsx}</>)`);
25441
+ } else {
25442
+ lines.push(`${indent}return ${jsx}`);
25443
+ }
25065
25444
  } else {
25066
- lines.push(`function ${pascalName}({`);
25067
- lines.push(` className = '',`);
25068
- lines.push(` children,`);
25069
- lines.push(` ...props`);
25070
- lines.push(`}: ${pascalName}Props) {`);
25071
- lines.push(` return (`);
25072
- lines.push(` <div data-slot="${componentName}" className={className}>`);
25073
- lines.push(` {/* TODO: Compose components */}`);
25074
- lines.push(` {children}`);
25075
- lines.push(` </div>`);
25076
- lines.push(` )`);
25077
- lines.push(`}`);
25445
+ lines.push(`${indent}return (`);
25446
+ if (needsFragment) lines.push(`${indent} <>`);
25447
+ for (const l of jsxLines) {
25448
+ lines.push(`${indent} ${needsFragment ? " " : ""}${l}`);
25449
+ }
25450
+ if (needsFragment) lines.push(`${indent} </>`);
25451
+ lines.push(`${indent})`);
25078
25452
  }
25079
- lines.push(``);
25080
- lines.push(`export { ${pascalName} }`);
25081
- lines.push(`export type { ${pascalName}Props }`);
25082
- lines.push(``);
25083
- return lines.join("\n");
25084
25453
  }
25085
- function generateTestCode(componentName, needsClient, importSource) {
25086
- const pascalName = toPascalCase(componentName);
25087
- const varName = toCamelCase(componentName) + "Source";
25454
+ function splitExampleCode(code) {
25455
+ const lines = code.split("\n");
25456
+ const firstJsxLine = lines.findIndex((l) => l.trim().startsWith("<"));
25457
+ if (firstJsxLine <= 0) {
25458
+ return { statements: [], jsx: code };
25459
+ }
25460
+ return {
25461
+ statements: lines.slice(0, firstJsxLine).filter((l) => l.trim() !== ""),
25462
+ jsx: lines.slice(firstJsxLine).join("\n")
25463
+ };
25464
+ }
25465
+ function isSimpleJsx(code) {
25466
+ const trimmed = code.trim();
25467
+ return trimmed.startsWith("<") && !trimmed.includes("createSignal");
25468
+ }
25469
+ function generatePreview(meta, componentsBasePath = "ui/components/ui") {
25470
+ const pascalName = toPascalCase(meta.name);
25471
+ const hasVariants = meta.variants != null && Object.keys(meta.variants).length > 0;
25472
+ const hasSubComponents = meta.subComponents != null && meta.subComponents.length > 0;
25473
+ let needsClient = meta.stateful || meta.tags.includes("stateful");
25474
+ const exampleCode = hasSubComponents && meta.examples.length > 0 ? meta.examples[0].code : "";
25475
+ if (exampleCode.includes("createSignal")) {
25476
+ needsClient = true;
25477
+ }
25478
+ const needsCreateSignalImport = exampleCode.includes("createSignal");
25088
25479
  const lines = [];
25089
- lines.push(`import { describe, test, expect } from '${importSource}'`);
25090
- lines.push(`import { readFileSync } from 'fs'`);
25091
- lines.push(`import { resolve } from 'path'`);
25092
- lines.push(`import { renderToTest } from '@barefootjs/test'`);
25093
- lines.push(``);
25094
- lines.push(`const ${varName} = readFileSync(resolve(__dirname, 'index.tsx'), 'utf-8')`);
25095
- lines.push(``);
25096
- lines.push(`describe('${pascalName}', () => {`);
25097
- lines.push(` const result = renderToTest(${varName}, '${componentName}.tsx')`);
25098
- lines.push(``);
25099
- lines.push(` test('has no compiler errors', () => {`);
25100
- lines.push(` expect(result.errors).toEqual([])`);
25101
- lines.push(` })`);
25102
- lines.push(``);
25103
- lines.push(` test('componentName is ${pascalName}', () => {`);
25104
- lines.push(` expect(result.componentName).toBe('${pascalName}')`);
25105
- lines.push(` })`);
25106
- lines.push(``);
25480
+ const previewNames = [];
25481
+ lines.push("// Auto-generated preview. Customize by editing this file.");
25107
25482
  if (needsClient) {
25108
- lines.push(` test('isClient is true', () => {`);
25109
- lines.push(` expect(result.isClient).toBe(true)`);
25110
- lines.push(` })`);
25483
+ lines.push('"use client"');
25111
25484
  }
25112
- lines.push(``);
25113
- lines.push(` test('renders as <div>', () => {`);
25114
- lines.push(` expect(result.root.tag).toBe('div')`);
25115
- lines.push(` })`);
25116
- lines.push(``);
25117
- lines.push(` test('has data-slot=${componentName}', () => {`);
25118
- lines.push(` expect(result.root.props['data-slot']).toBe('${componentName}')`);
25119
- lines.push(` })`);
25120
- lines.push(``);
25121
- lines.push(` test('toStructure() shows expected tree', () => {`);
25122
- lines.push(` const structure = result.toStructure()`);
25123
- lines.push(` expect(structure.length).toBeGreaterThan(0)`);
25124
- lines.push(` })`);
25125
- lines.push(`})`);
25126
- lines.push(``);
25127
- return lines.join("\n");
25128
- }
25129
- var init_scaffold = __esm({
25130
- "src/lib/scaffold.ts"() {
25131
- "use strict";
25485
+ lines.push("");
25486
+ if (needsCreateSignalImport) {
25487
+ lines.push("import { createSignal } from '@barefootjs/client'");
25132
25488
  }
25133
- });
25134
-
25135
- // src/commands/gen-component.ts
25136
- var gen_component_exports = {};
25137
- __export(gen_component_exports, {
25138
- run: () => run11
25139
- });
25140
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync16 } from "fs";
25141
- import path19 from "path";
25142
- function run11(args2, ctx2) {
25143
- if (args2.length < 1) {
25144
- console.error("Usage: bf gen component <component-name> [use-component1] [use-component2] ...");
25145
- console.error("Example: bf gen component settings-form input switch button");
25146
- process.exit(1);
25489
+ const subNames = [];
25490
+ if (hasSubComponents) {
25491
+ for (const sub2 of meta.subComponents) {
25492
+ subNames.push(sub2.name);
25493
+ }
25147
25494
  }
25148
- const [componentName, ...useComponents] = args2;
25149
- const { writeRoot, componentsBasePath } = resolveScaffoldLayout(ctx2);
25150
- const pm = detectPackageManager(ctx2.projectDir ?? ctx2.root);
25151
- const runner = testRunnerFor(pm);
25152
- const result = scaffold(componentName, useComponents, ctx2.metaDir, componentsBasePath, {
25153
- testImportSource: runner.importSource
25154
- });
25155
- const componentAbsPath = path19.join(writeRoot, result.componentPath);
25156
- if (existsSync16(componentAbsPath)) {
25157
- console.error(`Error: ${result.componentPath} already exists. Delete it first or choose a different name.`);
25158
- process.exit(1);
25495
+ const importsBySource = /* @__PURE__ */ new Map();
25496
+ const moduleNames = [pascalName, ...subNames];
25497
+ if (hasSubComponents && exampleCode) {
25498
+ const allTags = [...exampleCode.matchAll(/<([A-Z][a-zA-Z0-9]*)/g)].map((m) => m[1]);
25499
+ const usedFromModule = [...new Set(allTags.filter((t) => moduleNames.includes(t)))];
25500
+ if (usedFromModule.length > 0) {
25501
+ importsBySource.set(`../${meta.name}`, usedFromModule);
25502
+ }
25503
+ const knownNames = new Set(moduleNames);
25504
+ for (const tag of findExternalTags(exampleCode, knownNames)) {
25505
+ const resolved = resolveExternalTagImport(tag, meta.name, pascalName);
25506
+ const list = importsBySource.get(resolved.from) ?? [];
25507
+ if (!list.includes(resolved.name)) list.push(resolved.name);
25508
+ importsBySource.set(resolved.from, list);
25509
+ }
25510
+ } else {
25511
+ importsBySource.set(`../${meta.name}`, moduleNames);
25159
25512
  }
25160
- const testAbsPath = path19.join(writeRoot, result.testPath);
25161
- const testDir = path19.dirname(testAbsPath);
25162
- if (!existsSync16(testDir)) {
25163
- mkdirSync4(testDir, { recursive: true });
25513
+ for (const [from, names] of importsBySource) {
25514
+ lines.push(`import { ${names.join(", ")} } from '${from}'`);
25164
25515
  }
25165
- writeFileSync5(componentAbsPath, result.componentCode);
25166
- writeFileSync5(testAbsPath, result.testCode);
25167
- const testCmd = commandsFor(pm).test(result.testPath);
25168
- console.log(`Created:`);
25169
- console.log(` ${result.componentPath}`);
25170
- console.log(` ${result.testPath}`);
25171
- console.log(``);
25172
- console.log(`Next steps:`);
25173
- console.log(` 1. Implement the component in ${result.componentPath}`);
25174
- console.log(` 2. ${testCmd}`);
25175
- console.log(` 3. bf gen test ${componentName} (regenerate richer test)`);
25176
- }
25177
- var init_gen_component = __esm({
25178
- "src/commands/gen-component.ts"() {
25179
- "use strict";
25180
- init_scaffold();
25181
- init_scaffold_layout();
25182
- init_pm();
25516
+ lines.push("");
25517
+ if (hasSubComponents) {
25518
+ generateMultiComponent(lines, previewNames, meta, pascalName, subNames);
25519
+ } else if (needsClient && hasVariants) {
25520
+ generateStatefulWithVariants(lines, previewNames, meta, pascalName);
25521
+ } else if (needsClient) {
25522
+ generateStateful(lines, previewNames, meta, pascalName);
25523
+ } else if (hasVariants) {
25524
+ generateStatelessWithVariants(lines, previewNames, meta, pascalName);
25525
+ } else {
25526
+ generateStatelessSimple(lines, previewNames, meta, pascalName);
25183
25527
  }
25184
- });
25185
-
25186
- // src/lib/parse-component.ts
25187
- function parseComponent(source) {
25528
+ lines.push("");
25188
25529
  return {
25189
- useClient: detectUseClient(source),
25190
- description: extractTopLevelDescription(source),
25191
- examples: extractExamples2(source),
25192
- props: extractMainProps2(source),
25193
- subComponents: extractSubComponents2(source),
25194
- variants: extractVariants2(source),
25195
- accessibility: extractAccessibility2(source),
25196
- dependencies: extractDependencies3(source),
25197
- exportedNames: extractExportedNames(source)
25530
+ code: lines.join("\n"),
25531
+ previewNames,
25532
+ filePath: `${componentsBasePath}/${meta.name}/index.preview.tsx`
25198
25533
  };
25199
25534
  }
25200
- function detectUseClient(source) {
25201
- const firstLine = source.split("\n")[0].trim();
25202
- return firstLine === '"use client"' || firstLine === "'use client'";
25535
+ function generateStatelessWithVariants(lines, previewNames, meta, pascalName) {
25536
+ previewNames.push("Default");
25537
+ lines.push("export function Default() {");
25538
+ lines.push(` return <${pascalName}>${meta.title}</${pascalName}>`);
25539
+ lines.push("}");
25540
+ lines.push("");
25541
+ for (const [typeName, values] of Object.entries(meta.variants)) {
25542
+ const propName = inferVariantPropName(typeName, meta.props);
25543
+ if (!propName) continue;
25544
+ const funcName = capitalize2(propName) + "s";
25545
+ previewNames.push(funcName);
25546
+ lines.push(`export function ${funcName}() {`);
25547
+ lines.push(" return (");
25548
+ lines.push(' <div className="flex flex-wrap items-center gap-4">');
25549
+ for (const value of values) {
25550
+ lines.push(` <${pascalName} ${propName}="${value}">${capitalize2(value)}</${pascalName}>`);
25551
+ }
25552
+ lines.push(" </div>");
25553
+ lines.push(" )");
25554
+ lines.push("}");
25555
+ lines.push("");
25556
+ }
25203
25557
  }
25204
- function extractTopLevelDescription(source) {
25205
- const match = source.match(/^(?:"use client"\n+)?\/\*\*\n([\s\S]*?)\*\//m);
25206
- if (!match) return "";
25207
- const block = match[1];
25208
- const lines = block.split("\n");
25209
- const descLines = [];
25210
- for (const line of lines) {
25211
- const cleaned = line.replace(/^\s*\*\s?/, "").trim();
25212
- if (cleaned.startsWith("@")) break;
25213
- descLines.push(cleaned);
25558
+ function generateStatelessSimple(lines, previewNames, meta, pascalName) {
25559
+ previewNames.push("Default");
25560
+ lines.push("export function Default() {");
25561
+ const simpleExample = meta.examples.find((e) => isSimpleJsx(e.code));
25562
+ if (simpleExample) {
25563
+ emitJsxReturn(lines, simpleExample.code);
25564
+ } else {
25565
+ const hasChildren = meta.props.some((p) => p.name === "children");
25566
+ if (hasChildren) {
25567
+ lines.push(` return <${pascalName}>${meta.title}</${pascalName}>`);
25568
+ } else {
25569
+ lines.push(` return <${pascalName} />`);
25570
+ }
25214
25571
  }
25215
- return descLines.join(" ").replace(/\s+/g, " ").trim();
25572
+ lines.push("}");
25573
+ lines.push("");
25216
25574
  }
25217
- function extractExamples2(source) {
25218
- const match = source.match(/^(?:"use client"\n+)?\/\*\*\n([\s\S]*?)\*\//m);
25219
- if (!match) return [];
25220
- const block = match[1];
25221
- const examples = [];
25222
- const exampleRegex = /@example\s+(.*?)(?:\n\s*\*\s*```tsx?\n([\s\S]*?)```)/g;
25223
- let m;
25224
- while ((m = exampleRegex.exec(block)) !== null) {
25225
- const title = m[1].trim();
25226
- const code = m[2].split("\n").map((l) => l.replace(/^\s*\*\s?/, "")).join("\n").trim();
25227
- examples.push({ title, code });
25575
+ function generateStateful(lines, previewNames, meta, pascalName) {
25576
+ previewNames.push("Default");
25577
+ lines.push("export function Default() {");
25578
+ lines.push(" return (");
25579
+ lines.push(' <div className="flex gap-4">');
25580
+ lines.push(` <${pascalName} />`);
25581
+ const defaultStateProp = meta.props.find(
25582
+ (p) => p.name === "defaultChecked" || p.name === "defaultPressed" || p.name === "defaultValue" || p.name === "defaultOpen"
25583
+ );
25584
+ if (defaultStateProp) {
25585
+ lines.push(` <${pascalName} ${defaultStateProp.name} />`);
25228
25586
  }
25229
- return examples;
25587
+ if (meta.props.some((p) => p.name === "disabled")) {
25588
+ lines.push(` <${pascalName} disabled />`);
25589
+ }
25590
+ lines.push(" </div>");
25591
+ lines.push(" )");
25592
+ lines.push("}");
25593
+ lines.push("");
25230
25594
  }
25231
- function extractMainProps2(source) {
25232
- const match = source.match(/interface\s+(\w+Props)\s+(?:extends\s+[\w<>,\s]+\s+)?\{/);
25233
- if (!match) return [];
25234
- return extractPropsFromInterface(source, match.index);
25595
+ function generateStatefulWithVariants(lines, previewNames, meta, pascalName) {
25596
+ generateStateful(lines, previewNames, meta, pascalName);
25597
+ const hasChildren = meta.props.some((p) => p.name === "children");
25598
+ for (const [typeName, values] of Object.entries(meta.variants)) {
25599
+ const propName = inferVariantPropName(typeName, meta.props);
25600
+ if (!propName) continue;
25601
+ const funcName = capitalize2(propName) + "s";
25602
+ previewNames.push(funcName);
25603
+ lines.push(`export function ${funcName}() {`);
25604
+ lines.push(" return (");
25605
+ lines.push(' <div className="flex flex-wrap items-center gap-4">');
25606
+ for (const value of values) {
25607
+ if (hasChildren) {
25608
+ lines.push(` <${pascalName} ${propName}="${value}">${capitalize2(value)}</${pascalName}>`);
25609
+ } else {
25610
+ lines.push(` <${pascalName} ${propName}="${value}" />`);
25611
+ }
25612
+ }
25613
+ lines.push(" </div>");
25614
+ lines.push(" )");
25615
+ lines.push("}");
25616
+ lines.push("");
25617
+ }
25235
25618
  }
25236
- function extractPropsFromInterface(source, startIndex) {
25237
- const bodyStart = source.indexOf("{", startIndex);
25238
- if (bodyStart === -1) return [];
25239
- let depth = 0;
25240
- let bodyEnd = bodyStart;
25241
- for (let i = bodyStart; i < source.length; i++) {
25242
- if (source[i] === "{") depth++;
25243
- else if (source[i] === "}") {
25244
- depth--;
25245
- if (depth === 0) {
25246
- bodyEnd = i;
25247
- break;
25619
+ function generateMultiComponent(lines, previewNames, meta, pascalName, _subNames) {
25620
+ previewNames.push("Default");
25621
+ if (meta.examples.length > 0) {
25622
+ const example = meta.examples[0];
25623
+ const { statements, jsx } = splitExampleCode(example.code);
25624
+ lines.push("export function Default() {");
25625
+ for (const stmt of statements) {
25626
+ lines.push(` ${stmt}`);
25627
+ }
25628
+ const definedNames = new Set(
25629
+ statements.flatMap((s) => [...s.matchAll(/\b(?:const|let|var)\s+(?:\[([^\]]+)\]|(\w+))/g)]).flatMap((m) => m[1] ? m[1].split(",").map((v) => v.trim()) : [m[2]])
25630
+ );
25631
+ const handlerRefs = [...new Set([...jsx.matchAll(/\b(handle[A-Z]\w*)\b/g)].map((m) => m[1]))];
25632
+ for (const h of handlerRefs) {
25633
+ if (!definedNames.has(h)) {
25634
+ lines.push(` const ${h} = () => {}`);
25635
+ }
25636
+ }
25637
+ if (statements.length > 0 || handlerRefs.length > 0) {
25638
+ lines.push("");
25639
+ }
25640
+ emitJsxReturn(lines, jsx);
25641
+ lines.push("}");
25642
+ } else {
25643
+ lines.push("export function Default() {");
25644
+ lines.push(" return (");
25645
+ lines.push(` <${pascalName}>`);
25646
+ for (const sub2 of meta.subComponents) {
25647
+ const hasChildrenProp = sub2.props.some((p) => p.name === "children");
25648
+ const label = sub2.name.replace(pascalName, "") || sub2.name;
25649
+ if (hasChildrenProp) {
25650
+ lines.push(` <${sub2.name}>${label}</${sub2.name}>`);
25651
+ } else {
25652
+ lines.push(` <${sub2.name} />`);
25248
25653
  }
25249
25654
  }
25655
+ lines.push(` </${pascalName}>`);
25656
+ lines.push(" )");
25657
+ lines.push("}");
25250
25658
  }
25251
- const body = source.slice(bodyStart + 1, bodyEnd);
25252
- return parsePropsBody(body);
25659
+ lines.push("");
25253
25660
  }
25254
- function parsePropsBody(body) {
25255
- const props = [];
25256
- const propRegex = /(?:\/\*\*\s*([\s\S]*?)\s*\*\/\s*)?([\w]+)(\??):\s*([^\n]+)/g;
25257
- let m;
25258
- while ((m = propRegex.exec(body)) !== null) {
25259
- const jsdoc = m[1] || "";
25260
- const name = m[2];
25261
- const optional = m[3] === "?";
25262
- const rawType = m[4].trim();
25263
- if (name.startsWith("__")) continue;
25264
- const type = rawType.replace(/;?\s*$/, "").trim();
25265
- const description = extractPropDescription(jsdoc);
25266
- const defaultMatch = jsdoc.match(/@default\s+(.+?)(?:\s*$|\s*\*)/m);
25267
- const defaultValue = defaultMatch ? defaultMatch[1].trim().replace(/^['"]|['"]$/g, "") : void 0;
25268
- props.push({
25269
- name,
25270
- type,
25271
- required: !optional && defaultValue === void 0,
25272
- default: defaultValue,
25273
- description
25274
- });
25661
+ var init_preview_generate = __esm({
25662
+ "src/lib/preview-generate.ts"() {
25663
+ "use strict";
25275
25664
  }
25276
- return props;
25665
+ });
25666
+
25667
+ // src/lib/preview/run.ts
25668
+ import { resolve as resolve10, relative as relative4 } from "node:path";
25669
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync15 } from "node:fs";
25670
+ async function runPreview(componentName, ctx2, opts = {}) {
25671
+ const assets = await resolvePreviewAssets(ctx2);
25672
+ const previewsPath = resolve10(assets.srcComponentsDir, componentName, "index.preview.tsx");
25673
+ if (!existsSync15(previewsPath)) {
25674
+ try {
25675
+ const meta = loadComponent(ctx2.metaDir, componentName);
25676
+ const result = generatePreview(meta);
25677
+ writeFileSync4(previewsPath, result.code);
25678
+ console.log(`Auto-generated preview: ${relative4(assets.rootDir, previewsPath)}`);
25679
+ } catch {
25680
+ throw new PreviewError(
25681
+ `Preview file not found and auto-generation failed for "${componentName}".
25682
+ Run: bf gen preview ${componentName}`
25683
+ );
25684
+ }
25685
+ }
25686
+ const source = readFileSync6(previewsPath, "utf-8");
25687
+ const previewNames = [
25688
+ ...source.matchAll(/export\s+(?:async\s+)?function\s+(\w+)/g),
25689
+ ...source.matchAll(/export\s+const\s+(\w+)\s*=/g)
25690
+ ].map((m) => m[1]);
25691
+ if (previewNames.length === 0) {
25692
+ throw new PreviewError("No exported preview functions found in the preview file.");
25693
+ }
25694
+ console.log(`Found ${previewNames.length} previews: ${previewNames.join(", ")}`);
25695
+ return compile({ assets, previewsPath, previewNames, componentName, liveReload: opts.liveReload });
25277
25696
  }
25278
- function extractPropDescription(jsdoc) {
25279
- if (!jsdoc) return "";
25280
- const lines = jsdoc.split("\n");
25281
- const descLines = [];
25282
- for (const line of lines) {
25283
- const cleaned = line.replace(/^\s*\*?\s*/, "").trim();
25284
- if (cleaned.startsWith("@")) break;
25285
- if (cleaned) descLines.push(cleaned);
25697
+ var init_run = __esm({
25698
+ "src/lib/preview/run.ts"() {
25699
+ "use strict";
25700
+ init_compile();
25701
+ init_assets();
25702
+ init_errors2();
25703
+ init_meta_loader();
25704
+ init_preview_generate();
25705
+ init_errors2();
25286
25706
  }
25287
- return descLines.join(" ").trim();
25707
+ });
25708
+
25709
+ // src/lib/preview/serve.ts
25710
+ import { createServer } from "node:http";
25711
+ import { createReadStream, existsSync as existsSync16, statSync as statSync2 } from "node:fs";
25712
+ import { join, extname, normalize } from "node:path";
25713
+ function startPreviewServer(distDir, port) {
25714
+ let reloadToken = String(Date.now());
25715
+ const server = createServer((req, res) => {
25716
+ const url = (req.url ?? "/").split("?")[0];
25717
+ if (url === "/__preview_reload") {
25718
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
25719
+ res.setHeader("Cache-Control", "no-store");
25720
+ res.end(reloadToken);
25721
+ return;
25722
+ }
25723
+ const rel = decodeURIComponent(url === "/" ? "/index.html" : url);
25724
+ const filePath = normalize(join(distDir, rel));
25725
+ if (!filePath.startsWith(distDir) || !existsSync16(filePath) || statSync2(filePath).isDirectory()) {
25726
+ res.statusCode = 404;
25727
+ res.end("Not found");
25728
+ return;
25729
+ }
25730
+ res.setHeader("Content-Type", MIME[extname(filePath)] ?? "application/octet-stream");
25731
+ res.setHeader("Cache-Control", "no-store");
25732
+ createReadStream(filePath).pipe(res);
25733
+ });
25734
+ server.listen(port);
25735
+ return {
25736
+ url: `http://localhost:${port}`,
25737
+ bumpReload() {
25738
+ reloadToken = String(Date.now());
25739
+ },
25740
+ close() {
25741
+ server.close();
25742
+ }
25743
+ };
25744
+ }
25745
+ var MIME;
25746
+ var init_serve = __esm({
25747
+ "src/lib/preview/serve.ts"() {
25748
+ "use strict";
25749
+ MIME = {
25750
+ ".html": "text/html; charset=utf-8",
25751
+ ".js": "text/javascript; charset=utf-8",
25752
+ ".css": "text/css; charset=utf-8",
25753
+ ".json": "application/json; charset=utf-8",
25754
+ ".svg": "image/svg+xml",
25755
+ ".map": "application/json; charset=utf-8"
25756
+ };
25757
+ }
25758
+ });
25759
+
25760
+ // src/commands/preview.ts
25761
+ var preview_exports = {};
25762
+ __export(preview_exports, {
25763
+ run: () => run8
25764
+ });
25765
+ import { existsSync as existsSync17, readdirSync as readdirSync5, watch as fsWatch } from "fs";
25766
+ import path16 from "path";
25767
+ function parseArgs(args2) {
25768
+ const out = { serve: false, watch: false, help: false, port: DEFAULT_PORT };
25769
+ for (let i = 0; i < args2.length; i++) {
25770
+ const a = args2[i];
25771
+ if (a === "-h" || a === "--help") out.help = true;
25772
+ else if (a === "--serve") out.serve = true;
25773
+ else if (a === "--watch") out.watch = true;
25774
+ else if (a === "--port") out.port = parseInt(args2[++i] ?? "", 10);
25775
+ else if (a.startsWith("--port=")) out.port = parseInt(a.slice("--port=".length), 10);
25776
+ else if (!a.startsWith("-") && out.component === void 0) out.component = a;
25777
+ }
25778
+ if (out.watch) out.serve = true;
25779
+ return out;
25780
+ }
25781
+ function listPreviewableComponents(ctx2) {
25782
+ const { writeRoot, componentsBasePath } = resolveScaffoldLayout(ctx2);
25783
+ const componentsDir = path16.join(writeRoot, componentsBasePath);
25784
+ if (!existsSync17(componentsDir)) return [];
25785
+ const names = [];
25786
+ for (const name of readdirSync5(componentsDir)) {
25787
+ const previewFile = path16.join(componentsDir, name, "index.preview.tsx");
25788
+ if (existsSync17(previewFile)) names.push(name);
25789
+ }
25790
+ return names.sort();
25791
+ }
25792
+ async function run8(args2, ctx2) {
25793
+ const opts = parseArgs(args2);
25794
+ if (opts.help) {
25795
+ console.log(HELP);
25796
+ return;
25797
+ }
25798
+ if (!opts.component) {
25799
+ const available = listPreviewableComponents(ctx2);
25800
+ if (ctx2.jsonFlag) {
25801
+ console.log(JSON.stringify({ previewable: available }, null, 2));
25802
+ return;
25803
+ }
25804
+ if (available.length === 0) {
25805
+ console.error("No previewable components found.");
25806
+ console.error("Generate one with: bf gen preview <component>");
25807
+ process.exit(1);
25808
+ }
25809
+ console.log(`${available.length} previewable component(s):`);
25810
+ for (const name of available) console.log(` ${name}`);
25811
+ console.log();
25812
+ console.log("Open one with: bf preview <component>");
25813
+ return;
25814
+ }
25815
+ if (!Number.isInteger(opts.port) || opts.port < 1 || opts.port > 65535) {
25816
+ console.error(`Invalid --port: must be an integer between 1 and 65535.`);
25817
+ process.exit(1);
25818
+ }
25819
+ const component = opts.component;
25820
+ const runOpts = { liveReload: opts.watch };
25821
+ let result;
25822
+ try {
25823
+ result = await runPreview(component, ctx2, runOpts);
25824
+ } catch (err) {
25825
+ if (err instanceof PreviewError) {
25826
+ console.error(`Error: ${err.message}`);
25827
+ process.exit(1);
25828
+ }
25829
+ throw err;
25830
+ }
25831
+ const relDir = path16.relative(process.cwd(), result.distDir);
25832
+ console.log(`
25833
+ \u2713 Preview built \u2192 ${relDir}/`);
25834
+ if (!opts.serve) {
25835
+ console.log(`
25836
+ npx serve ${relDir}`);
25837
+ return;
25838
+ }
25839
+ const server = startPreviewServer(result.distDir, opts.port);
25840
+ console.log(`
25841
+ Serving ${server.url}`);
25842
+ if (!opts.watch) {
25843
+ console.log(" Press Ctrl+C to stop.");
25844
+ await new Promise((resolve11) => process.on("SIGINT", resolve11));
25845
+ server.close();
25846
+ return;
25847
+ }
25848
+ console.log(" Watching for changes \u2014 edit a component and save. Press Ctrl+C to stop.");
25849
+ let rebuilding = false;
25850
+ let pending = false;
25851
+ let timer;
25852
+ const rebuild = async () => {
25853
+ if (rebuilding) {
25854
+ pending = true;
25855
+ return;
25856
+ }
25857
+ rebuilding = true;
25858
+ console.log("\nChange detected \u2014 rebuilding...");
25859
+ try {
25860
+ await runPreview(component, ctx2, runOpts);
25861
+ server.bumpReload();
25862
+ console.log("\u2713 Rebuilt");
25863
+ } catch (err) {
25864
+ const msg = err instanceof PreviewError ? err.message : err.message;
25865
+ console.error(`\u2717 Rebuild failed: ${msg}`);
25866
+ } finally {
25867
+ rebuilding = false;
25868
+ if (pending) {
25869
+ pending = false;
25870
+ void rebuild();
25871
+ }
25872
+ }
25873
+ };
25874
+ const schedule = () => {
25875
+ clearTimeout(timer);
25876
+ timer = setTimeout(() => void rebuild(), 150);
25877
+ };
25878
+ const { writeRoot, componentsBasePath } = resolveScaffoldLayout(ctx2);
25879
+ const watchTargets = [
25880
+ path16.join(writeRoot, componentsBasePath),
25881
+ // Monorepo token/CSS sources
25882
+ path16.join(ctx2.root, "site/ui/styles"),
25883
+ path16.join(ctx2.root, "site/ui/tokens.json"),
25884
+ path16.join(ctx2.root, "site/shared/tokens"),
25885
+ // Project token/CSS sources
25886
+ ctx2.projectDir && path16.join(ctx2.projectDir, "styles"),
25887
+ ctx2.projectDir && path16.join(ctx2.projectDir, "globals.css"),
25888
+ ctx2.projectDir && path16.join(ctx2.projectDir, "uno.config.ts"),
25889
+ ctx2.projectDir && ctx2.config?.paths.tokens && path16.join(ctx2.projectDir, ctx2.config.paths.tokens)
25890
+ ].filter((t) => !!t && existsSync17(t));
25891
+ const watchers = watchTargets.map(
25892
+ (target) => fsWatch(target, { recursive: true }, schedule)
25893
+ );
25894
+ await new Promise((resolve11) => process.on("SIGINT", resolve11));
25895
+ for (const w of watchers) w.close();
25896
+ server.close();
25897
+ }
25898
+ var DEFAULT_PORT, HELP;
25899
+ var init_preview = __esm({
25900
+ "src/commands/preview.ts"() {
25901
+ "use strict";
25902
+ init_scaffold_layout();
25903
+ init_run();
25904
+ init_serve();
25905
+ DEFAULT_PORT = 4321;
25906
+ HELP = `Usage: bf preview [component] [options]
25907
+
25908
+ bf preview List previewable components
25909
+ bf preview <component> Build a static preview into .preview-dist/
25910
+
25911
+ Options:
25912
+ --serve Serve the build on a local server and print its URL
25913
+ --watch Rebuild on source changes and live-reload (implies --serve)
25914
+ --port <number> Server port for --serve/--watch (default ${DEFAULT_PORT})
25915
+ -h, --help Show this help`;
25916
+ }
25917
+ });
25918
+
25919
+ // src/commands/tokens-apply.ts
25920
+ var tokens_apply_exports = {};
25921
+ __export(tokens_apply_exports, {
25922
+ applyCssOverrides: () => applyCssOverrides,
25923
+ applyTokenOverrides: () => applyTokenOverrides,
25924
+ parseStudioUrl: () => parseStudioUrl,
25925
+ resolveTokensCss: () => resolveTokensCss,
25926
+ run: () => run9
25927
+ });
25928
+ import { existsSync as existsSync18, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
25929
+ import path17 from "path";
25930
+ async function run9(args2, ctx2) {
25931
+ const url = args2[0];
25932
+ if (!url) {
25933
+ console.error("Error: tokens apply requires a Studio URL.");
25934
+ console.error("Usage: bf tokens apply <url>");
25935
+ process.exit(1);
25936
+ }
25937
+ const projectDir = ctx2.projectDir ?? process.cwd();
25938
+ const tokensRelDir = ctx2.config?.paths.tokens ?? "tokens";
25939
+ const studioConfig = parseStudioUrl(url);
25940
+ if (!studioConfig) {
25941
+ console.error("Error: could not decode Studio config from the URL (no `?c=` param or malformed payload).");
25942
+ process.exit(1);
25943
+ }
25944
+ const cssPath = resolveTokensCss(projectDir, tokensRelDir);
25945
+ if (!cssPath) {
25946
+ console.error("Error: tokens.css not found. Checked:");
25947
+ for (const p of candidateCssPaths(projectDir, tokensRelDir)) {
25948
+ console.error(` - ${path17.relative(projectDir, p)}`);
25949
+ }
25950
+ console.error(" Run `npm create barefootjs@latest` to scaffold a project first.");
25951
+ process.exit(1);
25952
+ }
25953
+ applyCssOverrides(cssPath, studioConfig);
25954
+ console.log(` Patched ${path17.relative(projectDir, cssPath)}`);
25955
+ const tokensJsonPath = path17.join(projectDir, tokensRelDir, "tokens.json");
25956
+ if (existsSync18(tokensJsonPath)) {
25957
+ applyTokenOverrides(tokensJsonPath, studioConfig);
25958
+ console.log(` Patched ${path17.relative(projectDir, tokensJsonPath)}`);
25959
+ }
25960
+ }
25961
+ function parseStudioUrl(url) {
25962
+ try {
25963
+ const parsed = new URL(url);
25964
+ const encoded = parsed.searchParams.get("c");
25965
+ if (!encoded) return void 0;
25966
+ const json = atob(decodeURIComponent(encoded));
25967
+ return JSON.parse(json);
25968
+ } catch {
25969
+ return void 0;
25970
+ }
25971
+ }
25972
+ function candidateCssPaths(projectDir, tokensRelDir) {
25973
+ return [
25974
+ path17.join(projectDir, tokensRelDir, "tokens.css"),
25975
+ path17.join(projectDir, "public", "tokens.css"),
25976
+ path17.join(projectDir, "static", "tokens.css")
25977
+ ];
25978
+ }
25979
+ function resolveTokensCss(projectDir, tokensRelDir) {
25980
+ for (const p of candidateCssPaths(projectDir, tokensRelDir)) {
25981
+ if (existsSync18(p)) return p;
25982
+ }
25983
+ return void 0;
25984
+ }
25985
+ function buildBlockOverrides(config) {
25986
+ const root = {};
25987
+ const dark = {};
25988
+ if (config.spacing) root["--spacing"] = config.spacing;
25989
+ if (config.radius) root["--radius"] = config.radius;
25990
+ if (config.font) {
25991
+ root["--font-sans"] = FONT_MAP[config.font] ?? config.font;
25992
+ }
25993
+ if (config.style) {
25994
+ const preset = SHADOW_PRESETS[config.style];
25995
+ if (preset) {
25996
+ for (const [name, value] of Object.entries(preset)) {
25997
+ root[`--${name}`] = value;
25998
+ }
25999
+ }
26000
+ }
26001
+ if (config.tokens) {
26002
+ for (const [name, values] of Object.entries(config.tokens)) {
26003
+ if (values.light !== void 0) root[`--${name}`] = values.light;
26004
+ if (values.dark !== void 0) dark[`--${name}`] = values.dark;
26005
+ }
26006
+ }
26007
+ return { root, dark };
26008
+ }
26009
+ function applyCssOverrides(cssPath, config) {
26010
+ const overrides = buildBlockOverrides(config);
26011
+ let css = readFileSync7(cssPath, "utf-8");
26012
+ css = patchBlock(css, /:root\s*\{/, overrides.root);
26013
+ css = patchBlock(css, /\.dark\s*\{/, overrides.dark);
26014
+ writeFileSync5(cssPath, css);
26015
+ }
26016
+ function patchBlock(css, openRe, overrides) {
26017
+ if (Object.keys(overrides).length === 0) return css;
26018
+ const openMatch = openRe.exec(css);
26019
+ if (!openMatch) return css;
26020
+ const blockStart = openMatch.index + openMatch[0].length;
26021
+ let depth = 1;
26022
+ let blockEnd = -1;
26023
+ for (let i = blockStart; i < css.length; i++) {
26024
+ const ch = css[i];
26025
+ if (ch === "{") depth++;
26026
+ else if (ch === "}") {
26027
+ depth--;
26028
+ if (depth === 0) {
26029
+ blockEnd = i;
26030
+ break;
26031
+ }
26032
+ }
26033
+ }
26034
+ if (blockEnd === -1) return css;
26035
+ let block = css.slice(blockStart, blockEnd);
26036
+ const toAppend = [];
26037
+ for (const [name, value] of Object.entries(overrides)) {
26038
+ const re = new RegExp(`(${escapeRegex2(name)}\\s*:\\s*)[^;]+(;)`);
26039
+ if (re.test(block)) {
26040
+ block = block.replace(re, `$1${value}$2`);
26041
+ } else {
26042
+ toAppend.push([name, value]);
26043
+ }
26044
+ }
26045
+ if (toAppend.length > 0) {
26046
+ const lines = toAppend.map(([n, v]) => ` ${n}: ${v};`).join("\n");
26047
+ const trailing = block.match(/\s*$/)?.[0] ?? "";
26048
+ block = block.slice(0, block.length - trailing.length) + `
26049
+
26050
+ /* \u2500\u2500 Studio overrides \u2500\u2500 */
26051
+ ${lines}
26052
+ `;
26053
+ }
26054
+ return css.slice(0, blockStart) + block + css.slice(blockEnd);
26055
+ }
26056
+ function escapeRegex2(s) {
26057
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
26058
+ }
26059
+ function applyTokenOverrides(tokensJsonPath, config) {
26060
+ const raw = readFileSync7(tokensJsonPath, "utf-8");
26061
+ const tokensData = JSON.parse(raw);
26062
+ if (config.tokens) {
26063
+ for (const [name, values] of Object.entries(config.tokens)) {
26064
+ applyColorOverride(tokensData, name, values);
26065
+ }
26066
+ }
26067
+ if (config.spacing) {
26068
+ applySimpleOverride(tokensData, "--spacing", config.spacing);
26069
+ }
26070
+ if (config.radius) {
26071
+ applySimpleOverride(tokensData, "--radius", config.radius);
26072
+ }
26073
+ if (config.font) {
26074
+ const fontValue = FONT_MAP[config.font] || config.font;
26075
+ applySimpleOverride(tokensData, "--font-sans", fontValue);
26076
+ }
26077
+ if (config.style) {
26078
+ applyShadowPreset(tokensData, config.style);
26079
+ }
26080
+ writeFileSync5(tokensJsonPath, JSON.stringify(tokensData, null, 2) + "\n");
26081
+ }
26082
+ function applyColorOverride(tokensData, name, values) {
26083
+ const varName = `--${name}`;
26084
+ if (Array.isArray(tokensData.colors)) {
26085
+ for (const token of tokensData.colors) {
26086
+ if (token.name === varName || token.name === name) {
26087
+ if (values.light) token.value = values.light;
26088
+ if (values.dark) token.dark = values.dark;
26089
+ return;
26090
+ }
26091
+ }
26092
+ }
26093
+ if (Array.isArray(tokensData.tokens)) {
26094
+ for (const token of tokensData.tokens) {
26095
+ if (token.name === varName || token.name === name) {
26096
+ if (values.light) token.value = values.light;
26097
+ if (values.dark) token.dark = values.dark;
26098
+ return;
26099
+ }
26100
+ }
26101
+ }
26102
+ }
26103
+ function applySimpleOverride(tokensData, name, value) {
26104
+ const bareName = name.startsWith("--") ? name.slice(2) : name;
26105
+ const sections = [
26106
+ tokensData.colors,
26107
+ tokensData.spacing,
26108
+ tokensData.borderRadius,
26109
+ tokensData.shadows,
26110
+ tokensData.layout
26111
+ ];
26112
+ if (tokensData.typography) {
26113
+ for (const arr of Object.values(tokensData.typography)) {
26114
+ if (Array.isArray(arr)) sections.push(arr);
26115
+ }
26116
+ }
26117
+ for (const arr of sections) {
26118
+ if (!Array.isArray(arr)) continue;
26119
+ for (const token of arr) {
26120
+ if (token.name === bareName || token.name === name) {
26121
+ token.value = value;
26122
+ return;
26123
+ }
26124
+ }
26125
+ }
26126
+ }
26127
+ function applyShadowPreset(tokensData, styleName) {
26128
+ const shadows = SHADOW_PRESETS[styleName];
26129
+ if (!shadows) return;
26130
+ for (const [name, value] of Object.entries(shadows)) {
26131
+ applySimpleOverride(tokensData, name, value);
26132
+ }
26133
+ }
26134
+ var FONT_MAP, SHADOW_PRESETS;
26135
+ var init_tokens_apply = __esm({
26136
+ "src/commands/tokens-apply.ts"() {
26137
+ "use strict";
26138
+ FONT_MAP = {
26139
+ system: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif',
26140
+ inter: '"Inter", sans-serif',
26141
+ "noto-sans": '"Noto Sans", sans-serif',
26142
+ "nunito-sans": '"Nunito Sans", sans-serif',
26143
+ figtree: '"Figtree", sans-serif'
26144
+ };
26145
+ SHADOW_PRESETS = {
26146
+ Sharp: {
26147
+ "shadow-sm": "0 1px 2px 0 rgb(0 0 0 / 0.04)",
26148
+ "shadow": "0 1px 2px 0 rgb(0 0 0 / 0.06)",
26149
+ "shadow-md": "0 2px 4px -1px rgb(0 0 0 / 0.08)",
26150
+ "shadow-lg": "0 4px 8px -2px rgb(0 0 0 / 0.1)"
26151
+ },
26152
+ Soft: {
26153
+ "shadow-sm": "0 1px 3px 0 rgb(0 0 0 / 0.06)",
26154
+ "shadow": "0 2px 6px 0 rgb(0 0 0 / 0.08), 0 1px 3px -1px rgb(0 0 0 / 0.06)",
26155
+ "shadow-md": "0 6px 12px -2px rgb(0 0 0 / 0.08), 0 3px 6px -3px rgb(0 0 0 / 0.06)",
26156
+ "shadow-lg": "0 12px 24px -4px rgb(0 0 0 / 0.08), 0 6px 10px -5px rgb(0 0 0 / 0.06)"
26157
+ },
26158
+ Compact: {
26159
+ "shadow-sm": "none",
26160
+ "shadow": "none",
26161
+ "shadow-md": "none",
26162
+ "shadow-lg": "0 1px 2px 0 rgb(0 0 0 / 0.05)"
26163
+ }
26164
+ };
26165
+ }
26166
+ });
26167
+
26168
+ // src/commands/tokens.ts
26169
+ var tokens_exports = {};
26170
+ __export(tokens_exports, {
26171
+ run: () => run10
26172
+ });
26173
+ function flattenTokens(tokenSet, category) {
26174
+ const result = [];
26175
+ function add(cat, tokens) {
26176
+ if (category && category !== cat) return;
26177
+ result.push(...tokens);
26178
+ }
26179
+ add("typography", [...tokenSet.typography.fontFamily, ...tokenSet.typography.letterSpacing]);
26180
+ add("spacing", tokenSet.spacing);
26181
+ add("borderRadius", tokenSet.borderRadius);
26182
+ add("transitions", [...tokenSet.transitions.duration, ...tokenSet.transitions.easing]);
26183
+ add("layout", tokenSet.layout);
26184
+ add("colors", tokenSet.colors);
26185
+ add("shadows", tokenSet.shadows);
26186
+ return result;
25288
26187
  }
25289
- function extractSubComponents2(source) {
25290
- const exportedNames = extractExportedNames(source);
25291
- if (exportedNames.length <= 1) return [];
25292
- const subs = [];
25293
- const interfaceRegex = /interface\s+(\w+Props)\s+(?:extends\s+[\w<>,\s]+\s+)?\{/g;
25294
- const interfaces = [];
25295
- let im;
25296
- while ((im = interfaceRegex.exec(source)) !== null) {
25297
- interfaces.push({ name: im[1], index: im.index });
26188
+ function printTokens(tokens, jsonFlag2) {
26189
+ if (jsonFlag2) {
26190
+ console.log(JSON.stringify(tokens, null, 2));
26191
+ return;
25298
26192
  }
25299
- for (let i = 1; i < interfaces.length; i++) {
25300
- const iface = interfaces[i];
25301
- const componentName = iface.name.replace(/Props$/, "");
25302
- if (!exportedNames.includes(componentName)) continue;
25303
- const beforeInterface = source.slice(Math.max(0, iface.index - 300), iface.index);
25304
- const jsdocMatch = beforeInterface.match(/\/\*\*\s*([\s\S]*?)\s*\*\/\s*$/);
25305
- const description = jsdocMatch ? extractPropDescription(jsdocMatch[1]) : "";
25306
- const props = extractPropsFromInterface(source, iface.index);
25307
- subs.push({ name: componentName, description, props });
26193
+ if (tokens.length === 0) {
26194
+ console.log("No tokens found.");
26195
+ return;
25308
26196
  }
25309
- return subs;
26197
+ const nameWidth = Math.max(25, ...tokens.map((t) => t.name.length + 4));
26198
+ const header = `${"NAME".padEnd(nameWidth)}VALUE`;
26199
+ console.log(header);
26200
+ console.log("-".repeat(header.length + 20));
26201
+ for (const t of tokens) {
26202
+ const name = `--${t.name}`;
26203
+ const dark = t.dark;
26204
+ const darkSuffix = dark ? ` (dark: ${dark})` : "";
26205
+ console.log(`${name.padEnd(nameWidth)}${t.value}${darkSuffix}`);
26206
+ }
26207
+ console.log(`
26208
+ ${tokens.length} token(s)`);
25310
26209
  }
25311
- function extractVariants2(source) {
25312
- const variants = {};
25313
- const typeRegex = /type\s+(\w+(?:Variant|Size|Orientation|Side|Position))\s*=\s*([^\n]+)/g;
25314
- let m;
25315
- while ((m = typeRegex.exec(source)) !== null) {
25316
- const name = m[1];
25317
- const values = m[2].match(/'([^']+)'/g);
25318
- if (values) {
25319
- variants[name] = values.map((v) => v.replace(/'/g, ""));
26210
+ async function run10(args2, ctx2) {
26211
+ let category;
26212
+ const catIdx = args2.indexOf("--category");
26213
+ if (catIdx >= 0 && args2[catIdx + 1]) {
26214
+ const val = args2[catIdx + 1];
26215
+ if (!CATEGORY_NAMES.includes(val)) {
26216
+ console.error(`Unknown category: ${val}`);
26217
+ console.error(`Available: ${CATEGORY_NAMES.join(", ")}`);
26218
+ process.exit(1);
25320
26219
  }
26220
+ category = val;
25321
26221
  }
25322
- return variants;
26222
+ const tokenSet = await loadTokenSet(ctx2);
26223
+ const tokens = flattenTokens(tokenSet, category);
26224
+ printTokens(tokens, ctx2.jsonFlag);
25323
26225
  }
25324
- function extractAccessibility2(source) {
25325
- const roleMatches = source.match(/role[={"]+([^"}\s]+)/g);
25326
- const roles = /* @__PURE__ */ new Set();
25327
- if (roleMatches) {
25328
- for (const rm of roleMatches) {
25329
- const val = rm.match(/role[={"]+([^"}\s]+)/);
25330
- if (val) roles.add(val[1]);
25331
- }
26226
+ var CATEGORY_NAMES;
26227
+ var init_tokens2 = __esm({
26228
+ "src/commands/tokens.ts"() {
26229
+ "use strict";
26230
+ init_tokens();
26231
+ CATEGORY_NAMES = [
26232
+ "typography",
26233
+ "spacing",
26234
+ "borderRadius",
26235
+ "transitions",
26236
+ "layout",
26237
+ "colors",
26238
+ "shadows"
26239
+ ];
25332
26240
  }
25333
- const ariaMatches = source.match(/aria-[\w]+/g);
25334
- const ariaAttrs = [...new Set(ariaMatches || [])];
25335
- const dataMatches = source.match(/data-(?:state|slot|orientation|value|disabled|side)[\w-]*/g);
25336
- const dataAttrs = [...new Set(dataMatches || [])];
26241
+ });
26242
+
26243
+ // src/lib/scaffold.ts
26244
+ import { readFileSync as readFileSync8, existsSync as existsSync19 } from "fs";
26245
+ import path18 from "path";
26246
+ function loadMeta(metaDir, name) {
26247
+ const filePath = path18.join(metaDir, `${name}.json`);
26248
+ if (!existsSync19(filePath)) return null;
26249
+ return JSON.parse(readFileSync8(filePath, "utf-8"));
26250
+ }
26251
+ function toPascalCase2(kebab) {
26252
+ return kebab.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join("");
26253
+ }
26254
+ function toCamelCase(kebab) {
26255
+ const pascal = toPascalCase2(kebab);
26256
+ return pascal[0].toLowerCase() + pascal.slice(1);
26257
+ }
26258
+ function scaffold(componentName, useComponents, metaDir, componentsBasePath = "ui/components/ui", options = {}) {
26259
+ const metas = useComponents.map((name) => ({ name, meta: loadMeta(metaDir, name) }));
26260
+ const found = metas.filter((m) => m.meta !== null);
26261
+ const notFound = metas.filter((m) => m.meta === null).map((m) => m.name);
26262
+ const needsClient = found.some((m) => m.meta.stateful);
26263
+ const imports = buildImports(found);
26264
+ const componentCode = generateComponentCode(componentName, imports, needsClient, notFound);
26265
+ const testCode = generateTestCode(componentName, needsClient, options.testImportSource ?? "bun:test");
26266
+ const basePath = `${componentsBasePath}/${componentName}`;
25337
26267
  return {
25338
- role: roles.size > 0 ? [...roles].join(", ") : void 0,
25339
- ariaAttributes: ariaAttrs,
25340
- dataAttributes: dataAttrs
26268
+ componentCode,
26269
+ testCode,
26270
+ componentPath: `${basePath}/index.tsx`,
26271
+ testPath: `${basePath}/index.test.tsx`
25341
26272
  };
25342
26273
  }
25343
- function extractDependencies3(source) {
25344
- const internal = [];
25345
- const external = [];
25346
- const importRegex = /import\s+(?:type\s+)?(?:\{[^}]*\}|[\w]+)\s+from\s+['"]([^'"]+)['"]/g;
25347
- let m;
25348
- while ((m = importRegex.exec(source)) !== null) {
25349
- const specifier = m[1];
25350
- if (m[0].includes("import type")) continue;
25351
- if (specifier.startsWith("./") || specifier.startsWith("../")) {
25352
- const parts = specifier.split("/");
25353
- const name = parts[parts.length - 1].replace(/\.tsx?$/, "");
25354
- if (name !== "types" && name !== "index") {
25355
- internal.push(name);
26274
+ function buildImports(components) {
26275
+ const imports = [];
26276
+ for (const { name, meta } of components) {
26277
+ const names = [toPascalCase2(name)];
26278
+ if (meta.subComponents) {
26279
+ for (const sub2 of meta.subComponents) {
26280
+ names.push(sub2.name);
25356
26281
  }
25357
- } else {
25358
- external.push(specifier);
25359
26282
  }
26283
+ imports.push({ from: `../${name}`, names });
25360
26284
  }
25361
- return {
25362
- internal: [...new Set(internal)],
25363
- external: [...new Set(external)]
25364
- };
26285
+ return imports;
25365
26286
  }
25366
- function extractExportedNames(source) {
25367
- const names = [];
25368
- const seen = /* @__PURE__ */ new Set();
25369
- const add = (n) => {
25370
- if (!n) return;
25371
- const trimmed = n.trim();
25372
- if (!trimmed || seen.has(trimmed)) return;
25373
- if (trimmed.startsWith("type ")) return;
25374
- seen.add(trimmed);
25375
- names.push(trimmed);
25376
- };
25377
- const braceRegex = /export\s+\{([^}]+)\}/g;
25378
- let bm;
25379
- while ((bm = braceRegex.exec(source)) !== null) {
25380
- for (const part of bm[1].split(",")) {
25381
- const trimmed = part.trim();
25382
- if (!trimmed || trimmed.startsWith("type ")) continue;
25383
- const asMatch = trimmed.match(/\s+as\s+(\w+)$/);
25384
- add(asMatch ? asMatch[1] : trimmed);
25385
- }
26287
+ function generateComponentCode(componentName, imports, needsClient, notFound) {
26288
+ const pascalName = toPascalCase2(componentName);
26289
+ const lines = [];
26290
+ if (needsClient) {
26291
+ lines.push(`"use client"`);
26292
+ lines.push(``);
26293
+ lines.push(`import { createSignal } from '@barefootjs/client'`);
25386
26294
  }
25387
- const fnRegex = /export\s+(?:default\s+)?(?:async\s+)?function\s+(\w+)/g;
25388
- let fm;
25389
- while ((fm = fnRegex.exec(source)) !== null) add(fm[1]);
25390
- const varRegex = /export\s+(?:const|let|var)\s+(\w+)\s*[=:]/g;
25391
- let vm;
25392
- while ((vm = varRegex.exec(source)) !== null) add(vm[1]);
25393
- const defaultRegex = /export\s+default\s+(\w+)\s*;?\s*$/m;
25394
- const dm = defaultRegex.exec(source);
25395
- if (dm) add(dm[1]);
25396
- return names;
25397
- }
25398
- var init_parse_component = __esm({
25399
- "src/lib/parse-component.ts"() {
25400
- "use strict";
26295
+ lines.push(`import type { Child } from '../../../types'`);
26296
+ for (const imp of imports) {
26297
+ lines.push(`import { ${imp.names.join(", ")} } from '${imp.from}'`);
26298
+ }
26299
+ if (notFound.length > 0) {
26300
+ lines.push(``);
26301
+ lines.push(`// WARNING: These components were not found in ui/meta/:`);
26302
+ for (const name of notFound) {
26303
+ lines.push(`// - ${name}`);
26304
+ }
25401
26305
  }
25402
- });
25403
-
25404
- // src/lib/test-template.ts
25405
- import { readFileSync as readFileSync8 } from "fs";
25406
- import path20 from "path";
25407
- function generateTestTemplate(componentPath, options = {}) {
25408
- const importSource = options.importSource ?? "bun:test";
25409
- const source = readFileSync8(componentPath, "utf-8");
25410
- const parsed = parseComponent(source);
25411
- const fileName = path20.basename(componentPath);
25412
- const baseName = fileName.replace(/\.tsx$/, "");
25413
- const relativePath = fileName;
25414
- const varName = toCamelCase2(baseName) + "Source";
25415
- const exportedNames = parsed.exportedNames;
25416
- const mainComponent = exportedNames[0];
25417
- const hasSubComponents = exportedNames.length > 1;
25418
- const lines = [];
25419
- lines.push(`import { describe, test, expect } from '${importSource}'`);
25420
- lines.push(`import { readFileSync } from 'fs'`);
25421
- lines.push(`import { resolve } from 'path'`);
25422
- lines.push(`import { renderToTest } from '@barefootjs/test'`);
25423
26306
  lines.push(``);
25424
- lines.push(`const ${varName} = readFileSync(resolve(__dirname, '${relativePath}'), 'utf-8')`);
26307
+ lines.push(`interface ${pascalName}Props {`);
26308
+ lines.push(` /** Additional CSS classes. */`);
26309
+ lines.push(` className?: string`);
26310
+ lines.push(` /** Children to render. */`);
26311
+ lines.push(` children?: Child`);
26312
+ lines.push(`}`);
25425
26313
  lines.push(``);
25426
- if (mainComponent) {
25427
- lines.push(...generateDescribeBlock(source, parsed, mainComponent, varName, fileName, hasSubComponents));
25428
- }
25429
- if (hasSubComponents) {
25430
- for (let i = 1; i < exportedNames.length; i++) {
25431
- lines.push(``);
25432
- lines.push(...generateDescribeBlock(source, parsed, exportedNames[i], varName, fileName, true));
25433
- }
26314
+ if (needsClient) {
26315
+ lines.push(`function ${pascalName}(props: ${pascalName}Props) {`);
26316
+ lines.push(` // TODO: Add signals`);
26317
+ lines.push(` // const [value, setValue] = createSignal(...)`);
26318
+ lines.push(``);
26319
+ lines.push(` return (`);
26320
+ lines.push(` <div data-slot="${componentName}">`);
26321
+ lines.push(` {/* TODO: Compose components */}`);
26322
+ lines.push(` </div>`);
26323
+ lines.push(` )`);
26324
+ lines.push(`}`);
26325
+ } else {
26326
+ lines.push(`function ${pascalName}({`);
26327
+ lines.push(` className = '',`);
26328
+ lines.push(` children,`);
26329
+ lines.push(` ...props`);
26330
+ lines.push(`}: ${pascalName}Props) {`);
26331
+ lines.push(` return (`);
26332
+ lines.push(` <div data-slot="${componentName}" className={className}>`);
26333
+ lines.push(` {/* TODO: Compose components */}`);
26334
+ lines.push(` {children}`);
26335
+ lines.push(` </div>`);
26336
+ lines.push(` )`);
26337
+ lines.push(`}`);
25434
26338
  }
25435
- return lines.join("\n") + "\n";
26339
+ lines.push(``);
26340
+ lines.push(`export { ${pascalName} }`);
26341
+ lines.push(`export type { ${pascalName}Props }`);
26342
+ lines.push(``);
26343
+ return lines.join("\n");
25436
26344
  }
25437
- function generateDescribeBlock(source, parsed, componentName, varName, fileName, multiComponent) {
26345
+ function generateTestCode(componentName, needsClient, importSource) {
26346
+ const pascalName = toPascalCase2(componentName);
26347
+ const varName = toCamelCase(componentName) + "Source";
25438
26348
  const lines = [];
25439
- const renderArg = multiComponent ? `, '${componentName}'` : "";
25440
- const funcInfo = analyzeFunction(source, componentName);
25441
- lines.push(`describe('${componentName}', () => {`);
25442
- lines.push(` const result = renderToTest(${varName}, '${fileName}'${renderArg})`);
26349
+ lines.push(`import { describe, test, expect } from '${importSource}'`);
26350
+ lines.push(`import { readFileSync } from 'fs'`);
26351
+ lines.push(`import { resolve } from 'path'`);
26352
+ lines.push(`import { renderToTest } from '@barefootjs/test'`);
26353
+ lines.push(``);
26354
+ lines.push(`const ${varName} = readFileSync(resolve(__dirname, 'index.tsx'), 'utf-8')`);
26355
+ lines.push(``);
26356
+ lines.push(`describe('${pascalName}', () => {`);
26357
+ lines.push(` const result = renderToTest(${varName}, '${componentName}.tsx')`);
25443
26358
  lines.push(``);
25444
26359
  lines.push(` test('has no compiler errors', () => {`);
25445
26360
  lines.push(` expect(result.errors).toEqual([])`);
25446
26361
  lines.push(` })`);
25447
26362
  lines.push(``);
25448
- lines.push(` test('componentName is ${componentName}', () => {`);
25449
- lines.push(` expect(result.componentName).toBe('${componentName}')`);
26363
+ lines.push(` test('componentName is ${pascalName}', () => {`);
26364
+ lines.push(` expect(result.componentName).toBe('${pascalName}')`);
25450
26365
  lines.push(` })`);
25451
26366
  lines.push(``);
25452
- if (funcInfo.signals.length > 0) {
25453
- lines.push(` test('has expected signals', () => {`);
25454
- for (const sig of funcInfo.signals) {
25455
- lines.push(` expect(result.signals).toContain('${sig}')`);
25456
- }
25457
- lines.push(` })`);
25458
- } else {
25459
- lines.push(` test('no signals (stateless)', () => {`);
25460
- lines.push(` expect(result.signals).toEqual([])`);
26367
+ if (needsClient) {
26368
+ lines.push(` test('isClient is true', () => {`);
26369
+ lines.push(` expect(result.isClient).toBe(true)`);
25461
26370
  lines.push(` })`);
25462
26371
  }
25463
26372
  lines.push(``);
25464
- if (funcInfo.rootTag) {
25465
- const isComponentRoot = /^[A-Z]/.test(funcInfo.rootTag);
25466
- lines.push(` test('renders as <${funcInfo.rootTag}>', () => {`);
25467
- if (funcInfo.hasConditionalReturn) {
25468
- lines.push(` // Component has conditional return (e.g., asChild branch)`);
25469
- const finder = isComponentRoot ? `result.find({ componentName: '${funcInfo.rootTag}' })` : `result.find({ tag: '${funcInfo.rootTag}' })`;
25470
- lines.push(` expect(${finder}).not.toBeNull()`);
25471
- } else if (isComponentRoot) {
25472
- lines.push(` expect(result.root.componentName).toBe('${funcInfo.rootTag}')`);
25473
- } else {
25474
- lines.push(` expect(result.root.tag).toBe('${funcInfo.rootTag}')`);
25475
- }
25476
- lines.push(` })`);
25477
- lines.push(``);
25478
- }
25479
- if (funcInfo.dataSlot) {
25480
- lines.push(` test('has data-slot=${funcInfo.dataSlot}', () => {`);
25481
- if (funcInfo.hasConditionalReturn) {
25482
- lines.push(` const el = result.find({ tag: '${funcInfo.rootTag}' })!`);
25483
- lines.push(` expect(el.props['data-slot']).toBe('${funcInfo.dataSlot}')`);
25484
- } else {
25485
- lines.push(` expect(result.root.props['data-slot']).toBe('${funcInfo.dataSlot}')`);
25486
- }
25487
- lines.push(` })`);
25488
- lines.push(``);
25489
- }
25490
- if (funcInfo.role) {
25491
- lines.push(` test('has role=${funcInfo.role}', () => {`);
25492
- lines.push(` const el = result.find({ role: '${funcInfo.role}' })`);
25493
- lines.push(` expect(el).not.toBeNull()`);
25494
- lines.push(` })`);
25495
- lines.push(``);
25496
- }
25497
- const ariaAttrs = funcInfo.ariaAttributes;
25498
- if (ariaAttrs.length > 0) {
25499
- lines.push(` test('has ARIA attributes', () => {`);
25500
- const findTarget = funcInfo.role ? `result.find({ role: '${funcInfo.role}' })!` : funcInfo.rootTag ? `result.find({ tag: '${funcInfo.rootTag}' })!` : "result.root";
25501
- lines.push(` const el = ${findTarget}`);
25502
- for (const attr of ariaAttrs) {
25503
- const shortName = attr.replace("aria-", "");
25504
- lines.push(` expect(el.aria).toHaveProperty('${shortName}')`);
25505
- }
25506
- lines.push(` })`);
25507
- lines.push(``);
25508
- }
25509
- if (funcInfo.hasDataState) {
25510
- lines.push(` test('has data-state attribute', () => {`);
25511
- if (funcInfo.hasConditionalReturn) {
25512
- lines.push(` const el = result.find({ tag: '${funcInfo.rootTag}' })!`);
25513
- lines.push(` expect(el.dataState).not.toBeNull()`);
25514
- } else {
25515
- lines.push(` expect(result.root.dataState).not.toBeNull()`);
25516
- }
25517
- lines.push(` })`);
25518
- lines.push(``);
25519
- }
25520
- if (funcInfo.events.length > 0) {
25521
- lines.push(` test('has event handlers', () => {`);
25522
- lines.push(` const all = result.findAll({})`);
25523
- for (const event of funcInfo.events) {
25524
- const onProp = ON_PROP_BY_EVENT[event];
25525
- lines.push(` expect(`);
25526
- lines.push(` all.some(n => n.events.includes('${event}') || n.props['${onProp}'] != null),`);
25527
- lines.push(` ).toBe(true)`);
25528
- }
25529
- lines.push(` })`);
25530
- lines.push(``);
25531
- }
25532
- if (funcInfo.childComponents.length > 0) {
25533
- lines.push(` test('contains child components', () => {`);
25534
- for (const child of funcInfo.childComponents) {
25535
- lines.push(` expect(result.find({ componentName: '${child}' })).not.toBeNull()`);
25536
- }
25537
- lines.push(` })`);
25538
- lines.push(``);
25539
- }
26373
+ lines.push(` test('renders as <div>', () => {`);
26374
+ lines.push(` expect(result.root.tag).toBe('div')`);
26375
+ lines.push(` })`);
26376
+ lines.push(``);
26377
+ lines.push(` test('has data-slot=${componentName}', () => {`);
26378
+ lines.push(` expect(result.root.props['data-slot']).toBe('${componentName}')`);
26379
+ lines.push(` })`);
26380
+ lines.push(``);
25540
26381
  lines.push(` test('toStructure() shows expected tree', () => {`);
25541
26382
  lines.push(` const structure = result.toStructure()`);
25542
26383
  lines.push(` expect(structure.length).toBeGreaterThan(0)`);
25543
- if (funcInfo.rootTag) {
25544
- lines.push(` expect(structure).toContain('${funcInfo.rootTag}')`);
25545
- }
25546
- if (funcInfo.role) {
25547
- lines.push(` expect(structure).toContain('[role=${funcInfo.role}]')`);
25548
- }
25549
26384
  lines.push(` })`);
25550
26385
  lines.push(`})`);
25551
- return lines;
25552
- }
25553
- function analyzeFunction(source, componentName) {
25554
- const funcRegex = new RegExp(`function\\s+${componentName}\\s*\\(`);
25555
- const funcMatch = funcRegex.exec(source);
25556
- let funcBody = source;
25557
- if (!funcMatch) {
25558
- const aliasMatch = source.match(new RegExp(`const\\s+${componentName}\\s*=\\s*(\\w+)`));
25559
- if (aliasMatch) {
25560
- return analyzeFunction(source, aliasMatch[1]);
25561
- }
25562
- }
25563
- if (funcMatch) {
25564
- const parenStart = source.indexOf("(", funcMatch.index);
25565
- let parenDepth = 0;
25566
- let parenEnd = parenStart;
25567
- for (let i = parenStart; i < source.length; i++) {
25568
- if (source[i] === "(") parenDepth++;
25569
- else if (source[i] === ")") {
25570
- parenDepth--;
25571
- if (parenDepth === 0) {
25572
- parenEnd = i;
25573
- break;
25574
- }
25575
- }
25576
- }
25577
- let depth = 0;
25578
- let bodyStart = -1;
25579
- for (let i = parenEnd + 1; i < source.length; i++) {
25580
- if (source[i] === "{") {
25581
- if (bodyStart === -1) bodyStart = i;
25582
- depth++;
25583
- } else if (source[i] === "}") {
25584
- depth--;
25585
- if (depth === 0) {
25586
- funcBody = source.slice(bodyStart, i + 1);
25587
- break;
25588
- }
25589
- }
25590
- }
25591
- }
25592
- const signals = [];
25593
- const signalRegex = /const\s+\[(\w+),\s*\w+\]\s*=\s*createSignal/g;
25594
- let sm;
25595
- while ((sm = signalRegex.exec(funcBody)) !== null) {
25596
- signals.push(sm[1]);
25597
- }
25598
- const returnMatches = [...funcBody.matchAll(/return\s*\(?\s*<(\w+)/g)];
25599
- const rootTag = returnMatches.length > 0 ? returnMatches[returnMatches.length - 1][1] : null;
25600
- const allSlots = [...funcBody.matchAll(/data-slot="([^"]+)"/g)].map((m) => m[1]);
25601
- const expectedSlot = componentName.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
25602
- const dataSlot = allSlots.find((s) => s === expectedSlot) || allSlots[0] || null;
25603
- const roleMatch = funcBody.match(/role="([^"]+)"/);
25604
- const role = roleMatch ? roleMatch[1] : null;
25605
- const ariaMatches = funcBody.match(/aria-[\w]+(?==)/g);
25606
- const ariaAttributes = [...new Set(ariaMatches || [])].filter((a) => a !== "aria-invalid");
25607
- const hasDataState = /data-state=/.test(funcBody);
25608
- const hasConditionalReturn = /if\s*\(.*\)\s*\{?\s*return/.test(funcBody);
25609
- const events = [];
25610
- for (const [event, onProp] of Object.entries(ON_PROP_BY_EVENT)) {
25611
- if (new RegExp(`${onProp}=`).test(funcBody)) events.push(event);
25612
- }
25613
- const childCompRegex = /<([A-Z][A-Za-z]+)[\s/>]/g;
25614
- const childComponents = [];
25615
- let cm;
25616
- while ((cm = childCompRegex.exec(funcBody)) !== null) {
25617
- if (!childComponents.includes(cm[1])) {
25618
- childComponents.push(cm[1]);
25619
- }
25620
- }
25621
- return {
25622
- signals,
25623
- rootTag,
25624
- dataSlot,
25625
- role,
25626
- ariaAttributes,
25627
- hasDataState,
25628
- hasConditionalReturn,
25629
- events,
25630
- childComponents
25631
- };
25632
- }
25633
- function toCamelCase2(kebab) {
25634
- return kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
26386
+ lines.push(``);
26387
+ return lines.join("\n");
25635
26388
  }
25636
- var ON_PROP_BY_EVENT;
25637
- var init_test_template = __esm({
25638
- "src/lib/test-template.ts"() {
26389
+ var init_scaffold = __esm({
26390
+ "src/lib/scaffold.ts"() {
25639
26391
  "use strict";
25640
- init_parse_component();
25641
- ON_PROP_BY_EVENT = {
25642
- click: "onClick",
25643
- input: "onInput",
25644
- change: "onChange",
25645
- keydown: "onKeyDown"
25646
- };
25647
26392
  }
25648
26393
  });
25649
26394
 
25650
- // src/commands/gen-test.ts
25651
- var gen_test_exports = {};
25652
- __export(gen_test_exports, {
25653
- run: () => run12
26395
+ // src/commands/gen-component.ts
26396
+ var gen_component_exports = {};
26397
+ __export(gen_component_exports, {
26398
+ run: () => run11
25654
26399
  });
25655
- import { existsSync as existsSync17, writeFileSync as writeFileSync6 } from "fs";
25656
- import path21 from "path";
25657
- function run12(args2, ctx2) {
25658
- const positional = args2.filter((a) => !a.startsWith("-"));
25659
- const flagSet = new Set(args2.filter((a) => a.startsWith("-")));
25660
- const writeToStdout = flagSet.has("--stdout");
25661
- const force = flagSet.has("--force") || flagSet.has("-f");
25662
- const componentName = positional[0];
25663
- if (!componentName) {
25664
- console.error("Error: Component name required. Usage: bf gen test <component> [--stdout] [--force]");
26400
+ import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync4, existsSync as existsSync20 } from "fs";
26401
+ import path19 from "path";
26402
+ function run11(args2, ctx2) {
26403
+ if (args2.length < 1) {
26404
+ console.error("Usage: bf gen component <component-name> [use-component1] [use-component2] ...");
26405
+ console.error("Example: bf gen component settings-form input switch button");
25665
26406
  process.exit(1);
25666
26407
  }
25667
- const searched = [];
25668
- const resolved = resolveComponentSource(componentName, ctx2, searched);
25669
- if (!resolved) {
25670
- console.error(`Error: Cannot find component "${componentName}".`);
25671
- console.error("Looked in:");
25672
- for (const p of searched) console.error(` - ${p}`);
26408
+ const [componentName, ...useComponents] = args2;
26409
+ const { writeRoot, componentsBasePath } = resolveScaffoldLayout(ctx2);
26410
+ const pm = detectPackageManager(ctx2.projectDir ?? ctx2.root);
26411
+ const runner = testRunnerFor(pm);
26412
+ const result = scaffold(componentName, useComponents, ctx2.metaDir, componentsBasePath, {
26413
+ testImportSource: runner.importSource
26414
+ });
26415
+ const componentAbsPath = path19.join(writeRoot, result.componentPath);
26416
+ if (existsSync20(componentAbsPath)) {
26417
+ console.error(`Error: ${result.componentPath} already exists. Delete it first or choose a different name.`);
25673
26418
  process.exit(1);
25674
26419
  }
25675
- const pm = detectPackageManager(ctx2.projectDir ?? ctx2.root);
25676
- const runner = testRunnerFor(pm);
25677
- const content = generateTestTemplate(resolved.filePath, { importSource: runner.importSource });
25678
- if (writeToStdout) {
25679
- console.log(content);
25680
- return;
25681
- }
25682
- const dir = path21.dirname(resolved.filePath);
25683
- const base = path21.basename(resolved.filePath, path21.extname(resolved.filePath));
25684
- const testPath = path21.join(dir, `${base}.test.tsx`);
25685
- if (existsSync17(testPath) && !force) {
25686
- const rel2 = path21.relative(ctx2.projectDir ?? ctx2.root, testPath);
25687
- console.error(`Error: ${rel2} already exists. Pass --force to overwrite, or --stdout to preview.`);
25688
- process.exit(1);
26420
+ const testAbsPath = path19.join(writeRoot, result.testPath);
26421
+ const testDir = path19.dirname(testAbsPath);
26422
+ if (!existsSync20(testDir)) {
26423
+ mkdirSync4(testDir, { recursive: true });
25689
26424
  }
25690
- writeFileSync6(testPath, content);
25691
- const rel = path21.relative(ctx2.projectDir ?? ctx2.root, testPath);
25692
- console.log(`Created: ${rel}`);
26425
+ writeFileSync6(componentAbsPath, result.componentCode);
26426
+ writeFileSync6(testAbsPath, result.testCode);
26427
+ const testCmd = commandsFor(pm).test(result.testPath);
26428
+ console.log(`Created:`);
26429
+ console.log(` ${result.componentPath}`);
26430
+ console.log(` ${result.testPath}`);
25693
26431
  console.log(``);
25694
- console.log(`Next: ${commandsFor(pm).test(rel)}`);
26432
+ console.log(`Next steps:`);
26433
+ console.log(` 1. Implement the component in ${result.componentPath}`);
26434
+ console.log(` 2. ${testCmd}`);
26435
+ console.log(` 3. bf gen test ${componentName} (regenerate richer test)`);
25695
26436
  }
25696
- var init_gen_test = __esm({
25697
- "src/commands/gen-test.ts"() {
26437
+ var init_gen_component = __esm({
26438
+ "src/commands/gen-component.ts"() {
25698
26439
  "use strict";
25699
- init_resolve_source();
25700
- init_test_template();
26440
+ init_scaffold();
26441
+ init_scaffold_layout();
25701
26442
  init_pm();
25702
26443
  }
25703
26444
  });
25704
26445
 
25705
- // src/lib/preview-generate.ts
25706
- function toPascalCase2(kebab) {
25707
- return kebab.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join("");
26446
+ // src/lib/parse-component.ts
26447
+ function parseComponent(source) {
26448
+ return {
26449
+ useClient: detectUseClient(source),
26450
+ description: extractTopLevelDescription(source),
26451
+ examples: extractExamples2(source),
26452
+ props: extractMainProps2(source),
26453
+ subComponents: extractSubComponents2(source),
26454
+ variants: extractVariants2(source),
26455
+ accessibility: extractAccessibility2(source),
26456
+ dependencies: extractDependencies3(source),
26457
+ exportedNames: extractExportedNames(source)
26458
+ };
25708
26459
  }
25709
- function toKebabCase(pascal) {
25710
- return pascal.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
26460
+ function detectUseClient(source) {
26461
+ const firstLine = source.split("\n")[0].trim();
26462
+ return firstLine === '"use client"' || firstLine === "'use client'";
25711
26463
  }
25712
- function capitalize2(s) {
25713
- return s.charAt(0).toUpperCase() + s.slice(1);
26464
+ function extractTopLevelDescription(source) {
26465
+ const match = source.match(/^(?:"use client"\n+)?\/\*\*\n([\s\S]*?)\*\//m);
26466
+ if (!match) return "";
26467
+ const block = match[1];
26468
+ const lines = block.split("\n");
26469
+ const descLines = [];
26470
+ for (const line of lines) {
26471
+ const cleaned = line.replace(/^\s*\*\s?/, "").trim();
26472
+ if (cleaned.startsWith("@")) break;
26473
+ descLines.push(cleaned);
26474
+ }
26475
+ return descLines.join(" ").replace(/\s+/g, " ").trim();
25714
26476
  }
25715
- function inferVariantPropName(typeName, props) {
25716
- const match = props.find((p) => p.type === typeName);
25717
- return match ? match.name : null;
26477
+ function extractExamples2(source) {
26478
+ const match = source.match(/^(?:"use client"\n+)?\/\*\*\n([\s\S]*?)\*\//m);
26479
+ if (!match) return [];
26480
+ const block = match[1];
26481
+ const examples = [];
26482
+ const exampleRegex = /@example\s+(.*?)(?:\n\s*\*\s*```tsx?\n([\s\S]*?)```)/g;
26483
+ let m;
26484
+ while ((m = exampleRegex.exec(block)) !== null) {
26485
+ const title = m[1].trim();
26486
+ const code = m[2].split("\n").map((l) => l.replace(/^\s*\*\s?/, "")).join("\n").trim();
26487
+ examples.push({ title, code });
26488
+ }
26489
+ return examples;
25718
26490
  }
25719
- function findExternalTags(code, knownNames) {
25720
- const external = [];
25721
- for (const m of code.matchAll(/<([A-Z][a-zA-Z]*)/g)) {
25722
- if (!knownNames.has(m[1]) && !external.includes(m[1])) {
25723
- external.push(m[1]);
26491
+ function extractMainProps2(source) {
26492
+ const match = source.match(/interface\s+(\w+Props)\s+(?:extends\s+[\w<>,\s]+\s+)?\{/);
26493
+ if (!match) return [];
26494
+ return extractPropsFromInterface(source, match.index);
26495
+ }
26496
+ function extractPropsFromInterface(source, startIndex) {
26497
+ const bodyStart = source.indexOf("{", startIndex);
26498
+ if (bodyStart === -1) return [];
26499
+ let depth = 0;
26500
+ let bodyEnd = bodyStart;
26501
+ for (let i = bodyStart; i < source.length; i++) {
26502
+ if (source[i] === "{") depth++;
26503
+ else if (source[i] === "}") {
26504
+ depth--;
26505
+ if (depth === 0) {
26506
+ bodyEnd = i;
26507
+ break;
26508
+ }
25724
26509
  }
25725
26510
  }
25726
- return external;
26511
+ const body = source.slice(bodyStart + 1, bodyEnd);
26512
+ return parsePropsBody(body);
25727
26513
  }
25728
- function splitExampleCode(code) {
25729
- const lines = code.split("\n");
25730
- const firstJsxLine = lines.findIndex((l) => l.trim().startsWith("<"));
25731
- if (firstJsxLine <= 0) {
25732
- return { statements: [], jsx: code };
26514
+ function parsePropsBody(body) {
26515
+ const props = [];
26516
+ const propRegex = /(?:\/\*\*\s*([\s\S]*?)\s*\*\/\s*)?([\w]+)(\??):\s*([^\n]+)/g;
26517
+ let m;
26518
+ while ((m = propRegex.exec(body)) !== null) {
26519
+ const jsdoc = m[1] || "";
26520
+ const name = m[2];
26521
+ const optional = m[3] === "?";
26522
+ const rawType = m[4].trim();
26523
+ if (name.startsWith("__")) continue;
26524
+ const type = rawType.replace(/;?\s*$/, "").trim();
26525
+ const description = extractPropDescription(jsdoc);
26526
+ const defaultMatch = jsdoc.match(/@default\s+(.+?)(?:\s*$|\s*\*)/m);
26527
+ const defaultValue = defaultMatch ? defaultMatch[1].trim().replace(/^['"]|['"]$/g, "") : void 0;
26528
+ props.push({
26529
+ name,
26530
+ type,
26531
+ required: !optional && defaultValue === void 0,
26532
+ default: defaultValue,
26533
+ description
26534
+ });
26535
+ }
26536
+ return props;
26537
+ }
26538
+ function extractPropDescription(jsdoc) {
26539
+ if (!jsdoc) return "";
26540
+ const lines = jsdoc.split("\n");
26541
+ const descLines = [];
26542
+ for (const line of lines) {
26543
+ const cleaned = line.replace(/^\s*\*?\s*/, "").trim();
26544
+ if (cleaned.startsWith("@")) break;
26545
+ if (cleaned) descLines.push(cleaned);
26546
+ }
26547
+ return descLines.join(" ").trim();
26548
+ }
26549
+ function extractSubComponents2(source) {
26550
+ const exportedNames = extractExportedNames(source);
26551
+ if (exportedNames.length <= 1) return [];
26552
+ const subs = [];
26553
+ const interfaceRegex = /interface\s+(\w+Props)\s+(?:extends\s+[\w<>,\s]+\s+)?\{/g;
26554
+ const interfaces = [];
26555
+ let im;
26556
+ while ((im = interfaceRegex.exec(source)) !== null) {
26557
+ interfaces.push({ name: im[1], index: im.index });
26558
+ }
26559
+ for (let i = 1; i < interfaces.length; i++) {
26560
+ const iface = interfaces[i];
26561
+ const componentName = iface.name.replace(/Props$/, "");
26562
+ if (!exportedNames.includes(componentName)) continue;
26563
+ const beforeInterface = source.slice(Math.max(0, iface.index - 300), iface.index);
26564
+ const jsdocMatch = beforeInterface.match(/\/\*\*\s*([\s\S]*?)\s*\*\/\s*$/);
26565
+ const description = jsdocMatch ? extractPropDescription(jsdocMatch[1]) : "";
26566
+ const props = extractPropsFromInterface(source, iface.index);
26567
+ subs.push({ name: componentName, description, props });
26568
+ }
26569
+ return subs;
26570
+ }
26571
+ function extractVariants2(source) {
26572
+ const variants = {};
26573
+ const typeRegex = /type\s+(\w+(?:Variant|Size|Orientation|Side|Position))\s*=\s*([^\n]+)/g;
26574
+ let m;
26575
+ while ((m = typeRegex.exec(source)) !== null) {
26576
+ const name = m[1];
26577
+ const values = m[2].match(/'([^']+)'/g);
26578
+ if (values) {
26579
+ variants[name] = values.map((v) => v.replace(/'/g, ""));
26580
+ }
26581
+ }
26582
+ return variants;
26583
+ }
26584
+ function extractAccessibility2(source) {
26585
+ const roleMatches = source.match(/role[={"]+([^"}\s]+)/g);
26586
+ const roles = /* @__PURE__ */ new Set();
26587
+ if (roleMatches) {
26588
+ for (const rm of roleMatches) {
26589
+ const val = rm.match(/role[={"]+([^"}\s]+)/);
26590
+ if (val) roles.add(val[1]);
26591
+ }
25733
26592
  }
26593
+ const ariaMatches = source.match(/aria-[\w]+/g);
26594
+ const ariaAttrs = [...new Set(ariaMatches || [])];
26595
+ const dataMatches = source.match(/data-(?:state|slot|orientation|value|disabled|side)[\w-]*/g);
26596
+ const dataAttrs = [...new Set(dataMatches || [])];
25734
26597
  return {
25735
- statements: lines.slice(0, firstJsxLine).filter((l) => l.trim() !== ""),
25736
- jsx: lines.slice(firstJsxLine).join("\n")
26598
+ role: roles.size > 0 ? [...roles].join(", ") : void 0,
26599
+ ariaAttributes: ariaAttrs,
26600
+ dataAttributes: dataAttrs
25737
26601
  };
25738
26602
  }
25739
- function isSimpleJsx(code) {
25740
- const trimmed = code.trim();
25741
- return trimmed.startsWith("<") && !trimmed.includes("createSignal");
26603
+ function extractDependencies3(source) {
26604
+ const internal = [];
26605
+ const external = [];
26606
+ const importRegex = /import\s+(?:type\s+)?(?:\{[^}]*\}|[\w]+)\s+from\s+['"]([^'"]+)['"]/g;
26607
+ let m;
26608
+ while ((m = importRegex.exec(source)) !== null) {
26609
+ const specifier = m[1];
26610
+ if (m[0].includes("import type")) continue;
26611
+ if (specifier.startsWith("./") || specifier.startsWith("../")) {
26612
+ const parts = specifier.split("/");
26613
+ const name = parts[parts.length - 1].replace(/\.tsx?$/, "");
26614
+ if (name !== "types" && name !== "index") {
26615
+ internal.push(name);
26616
+ }
26617
+ } else {
26618
+ external.push(specifier);
26619
+ }
26620
+ }
26621
+ return {
26622
+ internal: [...new Set(internal)],
26623
+ external: [...new Set(external)]
26624
+ };
26625
+ }
26626
+ function extractExportedNames(source) {
26627
+ const names = [];
26628
+ const seen = /* @__PURE__ */ new Set();
26629
+ const add = (n) => {
26630
+ if (!n) return;
26631
+ const trimmed = n.trim();
26632
+ if (!trimmed || seen.has(trimmed)) return;
26633
+ if (trimmed.startsWith("type ")) return;
26634
+ seen.add(trimmed);
26635
+ names.push(trimmed);
26636
+ };
26637
+ const braceRegex = /export\s+\{([^}]+)\}/g;
26638
+ let bm;
26639
+ while ((bm = braceRegex.exec(source)) !== null) {
26640
+ for (const part of bm[1].split(",")) {
26641
+ const trimmed = part.trim();
26642
+ if (!trimmed || trimmed.startsWith("type ")) continue;
26643
+ const asMatch = trimmed.match(/\s+as\s+(\w+)$/);
26644
+ add(asMatch ? asMatch[1] : trimmed);
26645
+ }
26646
+ }
26647
+ const fnRegex = /export\s+(?:default\s+)?(?:async\s+)?function\s+(\w+)/g;
26648
+ let fm;
26649
+ while ((fm = fnRegex.exec(source)) !== null) add(fm[1]);
26650
+ const varRegex = /export\s+(?:const|let|var)\s+(\w+)\s*[=:]/g;
26651
+ let vm;
26652
+ while ((vm = varRegex.exec(source)) !== null) add(vm[1]);
26653
+ const defaultRegex = /export\s+default\s+(\w+)\s*;?\s*$/m;
26654
+ const dm = defaultRegex.exec(source);
26655
+ if (dm) add(dm[1]);
26656
+ return names;
25742
26657
  }
25743
- function generatePreview(meta, componentsBasePath = "ui/components/ui") {
25744
- const pascalName = toPascalCase2(meta.name);
25745
- const hasVariants = meta.variants != null && Object.keys(meta.variants).length > 0;
25746
- const hasSubComponents = meta.subComponents != null && meta.subComponents.length > 0;
25747
- let needsClient = meta.stateful || meta.tags.includes("stateful");
25748
- const exampleCode = hasSubComponents && meta.examples.length > 0 ? meta.examples[0].code : "";
25749
- if (exampleCode.includes("createSignal")) {
25750
- needsClient = true;
26658
+ var init_parse_component = __esm({
26659
+ "src/lib/parse-component.ts"() {
26660
+ "use strict";
25751
26661
  }
25752
- const needsCreateSignalImport = exampleCode.includes("createSignal");
26662
+ });
26663
+
26664
+ // src/lib/test-template.ts
26665
+ import { readFileSync as readFileSync9 } from "fs";
26666
+ import path20 from "path";
26667
+ function generateTestTemplate(componentPath, options = {}) {
26668
+ const importSource = options.importSource ?? "bun:test";
26669
+ const source = readFileSync9(componentPath, "utf-8");
26670
+ const parsed = parseComponent(source);
26671
+ const fileName = path20.basename(componentPath);
26672
+ const baseName = fileName.replace(/\.tsx$/, "");
26673
+ const relativePath = fileName;
26674
+ const varName = toCamelCase2(baseName) + "Source";
26675
+ const exportedNames = parsed.exportedNames;
26676
+ const mainComponent = exportedNames[0];
26677
+ const hasSubComponents = exportedNames.length > 1;
25753
26678
  const lines = [];
25754
- const previewNames = [];
25755
- lines.push("// Auto-generated preview. Customize by editing this file.");
25756
- if (needsClient) {
25757
- lines.push('"use client"');
25758
- }
25759
- lines.push("");
25760
- if (needsCreateSignalImport) {
25761
- lines.push("import { createSignal } from '@barefootjs/client'");
26679
+ lines.push(`import { describe, test, expect } from '${importSource}'`);
26680
+ lines.push(`import { readFileSync } from 'fs'`);
26681
+ lines.push(`import { resolve } from 'path'`);
26682
+ lines.push(`import { renderToTest } from '@barefootjs/test'`);
26683
+ lines.push(``);
26684
+ lines.push(`const ${varName} = readFileSync(resolve(__dirname, '${relativePath}'), 'utf-8')`);
26685
+ lines.push(``);
26686
+ if (mainComponent) {
26687
+ lines.push(...generateDescribeBlock(source, parsed, mainComponent, varName, fileName, hasSubComponents));
25762
26688
  }
25763
- const componentImports = [pascalName];
25764
- const subNames = [];
25765
26689
  if (hasSubComponents) {
25766
- for (const sub2 of meta.subComponents) {
25767
- componentImports.push(sub2.name);
25768
- subNames.push(sub2.name);
26690
+ for (let i = 1; i < exportedNames.length; i++) {
26691
+ lines.push(``);
26692
+ lines.push(...generateDescribeBlock(source, parsed, exportedNames[i], varName, fileName, true));
25769
26693
  }
25770
26694
  }
25771
- lines.push(`import { ${componentImports.join(", ")} } from '../${meta.name}'`);
25772
- if (hasSubComponents && exampleCode) {
25773
- const knownNames = /* @__PURE__ */ new Set([pascalName, ...subNames]);
25774
- for (const tag of findExternalTags(exampleCode, knownNames)) {
25775
- lines.push(`import { ${tag} } from '../${toKebabCase(tag)}'`);
26695
+ return lines.join("\n") + "\n";
26696
+ }
26697
+ function generateDescribeBlock(source, parsed, componentName, varName, fileName, multiComponent) {
26698
+ const lines = [];
26699
+ const renderArg = multiComponent ? `, '${componentName}'` : "";
26700
+ const funcInfo = analyzeFunction(source, componentName);
26701
+ lines.push(`describe('${componentName}', () => {`);
26702
+ lines.push(` const result = renderToTest(${varName}, '${fileName}'${renderArg})`);
26703
+ lines.push(``);
26704
+ lines.push(` test('has no compiler errors', () => {`);
26705
+ lines.push(` expect(result.errors).toEqual([])`);
26706
+ lines.push(` })`);
26707
+ lines.push(``);
26708
+ lines.push(` test('componentName is ${componentName}', () => {`);
26709
+ lines.push(` expect(result.componentName).toBe('${componentName}')`);
26710
+ lines.push(` })`);
26711
+ lines.push(``);
26712
+ if (funcInfo.signals.length > 0) {
26713
+ lines.push(` test('has expected signals', () => {`);
26714
+ for (const sig of funcInfo.signals) {
26715
+ lines.push(` expect(result.signals).toContain('${sig}')`);
25776
26716
  }
25777
- }
25778
- lines.push("");
25779
- if (hasSubComponents) {
25780
- generateMultiComponent(lines, previewNames, meta, pascalName, subNames);
25781
- } else if (needsClient && hasVariants) {
25782
- generateStatefulWithVariants(lines, previewNames, meta, pascalName);
25783
- } else if (needsClient) {
25784
- generateStateful(lines, previewNames, meta, pascalName);
25785
- } else if (hasVariants) {
25786
- generateStatelessWithVariants(lines, previewNames, meta, pascalName);
26717
+ lines.push(` })`);
25787
26718
  } else {
25788
- generateStatelessSimple(lines, previewNames, meta, pascalName);
26719
+ lines.push(` test('no signals (stateless)', () => {`);
26720
+ lines.push(` expect(result.signals).toEqual([])`);
26721
+ lines.push(` })`);
25789
26722
  }
25790
- lines.push("");
25791
- return {
25792
- code: lines.join("\n"),
25793
- previewNames,
25794
- filePath: `${componentsBasePath}/${meta.name}/index.preview.tsx`
25795
- };
25796
- }
25797
- function generateStatelessWithVariants(lines, previewNames, meta, pascalName) {
25798
- previewNames.push("Default");
25799
- lines.push("export function Default() {");
25800
- lines.push(` return <${pascalName}>${meta.title}</${pascalName}>`);
25801
- lines.push("}");
25802
- lines.push("");
25803
- for (const [typeName, values] of Object.entries(meta.variants)) {
25804
- const propName = inferVariantPropName(typeName, meta.props);
25805
- if (!propName) continue;
25806
- const funcName = capitalize2(propName) + "s";
25807
- previewNames.push(funcName);
25808
- lines.push(`export function ${funcName}() {`);
25809
- lines.push(" return (");
25810
- lines.push(' <div className="flex flex-wrap items-center gap-4">');
25811
- for (const value of values) {
25812
- lines.push(` <${pascalName} ${propName}="${value}">${capitalize2(value)}</${pascalName}>`);
26723
+ lines.push(``);
26724
+ if (funcInfo.rootTag) {
26725
+ const isComponentRoot = /^[A-Z]/.test(funcInfo.rootTag);
26726
+ lines.push(` test('renders as <${funcInfo.rootTag}>', () => {`);
26727
+ if (funcInfo.hasConditionalReturn) {
26728
+ lines.push(` // Component has conditional return (e.g., asChild branch)`);
26729
+ const finder = isComponentRoot ? `result.find({ componentName: '${funcInfo.rootTag}' })` : `result.find({ tag: '${funcInfo.rootTag}' })`;
26730
+ lines.push(` expect(${finder}).not.toBeNull()`);
26731
+ } else if (isComponentRoot) {
26732
+ lines.push(` expect(result.root.componentName).toBe('${funcInfo.rootTag}')`);
26733
+ } else {
26734
+ lines.push(` expect(result.root.tag).toBe('${funcInfo.rootTag}')`);
25813
26735
  }
25814
- lines.push(" </div>");
25815
- lines.push(" )");
25816
- lines.push("}");
25817
- lines.push("");
26736
+ lines.push(` })`);
26737
+ lines.push(``);
25818
26738
  }
25819
- }
25820
- function generateStatelessSimple(lines, previewNames, meta, pascalName) {
25821
- previewNames.push("Default");
25822
- lines.push("export function Default() {");
25823
- const simpleExample = meta.examples.find((e) => isSimpleJsx(e.code));
25824
- if (simpleExample) {
25825
- const jsxLines = simpleExample.code.split("\n");
25826
- if (jsxLines.length === 1) {
25827
- lines.push(` return ${simpleExample.code}`);
26739
+ if (funcInfo.dataSlot) {
26740
+ lines.push(` test('has data-slot=${funcInfo.dataSlot}', () => {`);
26741
+ if (funcInfo.hasConditionalReturn) {
26742
+ lines.push(` const el = result.find({ tag: '${funcInfo.rootTag}' })!`);
26743
+ lines.push(` expect(el.props['data-slot']).toBe('${funcInfo.dataSlot}')`);
25828
26744
  } else {
25829
- lines.push(" return (");
25830
- for (const l of jsxLines) {
25831
- lines.push(` ${l}`);
25832
- }
25833
- lines.push(" )");
26745
+ lines.push(` expect(result.root.props['data-slot']).toBe('${funcInfo.dataSlot}')`);
25834
26746
  }
25835
- } else {
25836
- const hasChildren = meta.props.some((p) => p.name === "children");
25837
- if (hasChildren) {
25838
- lines.push(` return <${pascalName}>${meta.title}</${pascalName}>`);
26747
+ lines.push(` })`);
26748
+ lines.push(``);
26749
+ }
26750
+ if (funcInfo.role) {
26751
+ lines.push(` test('has role=${funcInfo.role}', () => {`);
26752
+ lines.push(` const el = result.find({ role: '${funcInfo.role}' })`);
26753
+ lines.push(` expect(el).not.toBeNull()`);
26754
+ lines.push(` })`);
26755
+ lines.push(``);
26756
+ }
26757
+ const ariaAttrs = funcInfo.ariaAttributes;
26758
+ if (ariaAttrs.length > 0) {
26759
+ lines.push(` test('has ARIA attributes', () => {`);
26760
+ const findTarget = funcInfo.role ? `result.find({ role: '${funcInfo.role}' })!` : funcInfo.rootTag ? `result.find({ tag: '${funcInfo.rootTag}' })!` : "result.root";
26761
+ lines.push(` const el = ${findTarget}`);
26762
+ for (const attr of ariaAttrs) {
26763
+ const shortName = attr.replace("aria-", "");
26764
+ lines.push(` expect(el.aria).toHaveProperty('${shortName}')`);
26765
+ }
26766
+ lines.push(` })`);
26767
+ lines.push(``);
26768
+ }
26769
+ if (funcInfo.hasDataState) {
26770
+ lines.push(` test('has data-state attribute', () => {`);
26771
+ if (funcInfo.hasConditionalReturn) {
26772
+ lines.push(` const el = result.find({ tag: '${funcInfo.rootTag}' })!`);
26773
+ lines.push(` expect(el.dataState).not.toBeNull()`);
25839
26774
  } else {
25840
- lines.push(` return <${pascalName} />`);
26775
+ lines.push(` expect(result.root.dataState).not.toBeNull()`);
25841
26776
  }
26777
+ lines.push(` })`);
26778
+ lines.push(``);
25842
26779
  }
25843
- lines.push("}");
25844
- lines.push("");
25845
- }
25846
- function generateStateful(lines, previewNames, meta, pascalName) {
25847
- previewNames.push("Default");
25848
- lines.push("export function Default() {");
25849
- lines.push(" return (");
25850
- lines.push(' <div className="flex gap-4">');
25851
- lines.push(` <${pascalName} />`);
25852
- const defaultStateProp = meta.props.find(
25853
- (p) => p.name === "defaultChecked" || p.name === "defaultPressed" || p.name === "defaultValue" || p.name === "defaultOpen"
25854
- );
25855
- if (defaultStateProp) {
25856
- lines.push(` <${pascalName} ${defaultStateProp.name} />`);
26780
+ if (funcInfo.events.length > 0) {
26781
+ lines.push(` test('has event handlers', () => {`);
26782
+ lines.push(` const all = result.findAll({})`);
26783
+ for (const event of funcInfo.events) {
26784
+ const onProp = ON_PROP_BY_EVENT[event];
26785
+ lines.push(` expect(`);
26786
+ lines.push(` all.some(n => n.events.includes('${event}') || n.props['${onProp}'] != null),`);
26787
+ lines.push(` ).toBe(true)`);
26788
+ }
26789
+ lines.push(` })`);
26790
+ lines.push(``);
25857
26791
  }
25858
- if (meta.props.some((p) => p.name === "disabled")) {
25859
- lines.push(` <${pascalName} disabled />`);
26792
+ if (funcInfo.childComponents.length > 0) {
26793
+ lines.push(` test('contains child components', () => {`);
26794
+ for (const child of funcInfo.childComponents) {
26795
+ lines.push(` expect(result.find({ componentName: '${child}' })).not.toBeNull()`);
26796
+ }
26797
+ lines.push(` })`);
26798
+ lines.push(``);
25860
26799
  }
25861
- lines.push(" </div>");
25862
- lines.push(" )");
25863
- lines.push("}");
25864
- lines.push("");
26800
+ lines.push(` test('toStructure() shows expected tree', () => {`);
26801
+ lines.push(` const structure = result.toStructure()`);
26802
+ lines.push(` expect(structure.length).toBeGreaterThan(0)`);
26803
+ if (funcInfo.rootTag) {
26804
+ lines.push(` expect(structure).toContain('${funcInfo.rootTag}')`);
26805
+ }
26806
+ if (funcInfo.role) {
26807
+ lines.push(` expect(structure).toContain('[role=${funcInfo.role}]')`);
26808
+ }
26809
+ lines.push(` })`);
26810
+ lines.push(`})`);
26811
+ return lines;
25865
26812
  }
25866
- function generateStatefulWithVariants(lines, previewNames, meta, pascalName) {
25867
- generateStateful(lines, previewNames, meta, pascalName);
25868
- const hasChildren = meta.props.some((p) => p.name === "children");
25869
- for (const [typeName, values] of Object.entries(meta.variants)) {
25870
- const propName = inferVariantPropName(typeName, meta.props);
25871
- if (!propName) continue;
25872
- const funcName = capitalize2(propName) + "s";
25873
- previewNames.push(funcName);
25874
- lines.push(`export function ${funcName}() {`);
25875
- lines.push(" return (");
25876
- lines.push(' <div className="flex flex-wrap items-center gap-4">');
25877
- for (const value of values) {
25878
- if (hasChildren) {
25879
- lines.push(` <${pascalName} ${propName}="${value}">${capitalize2(value)}</${pascalName}>`);
25880
- } else {
25881
- lines.push(` <${pascalName} ${propName}="${value}" />`);
25882
- }
26813
+ function analyzeFunction(source, componentName) {
26814
+ const funcRegex = new RegExp(`function\\s+${componentName}\\s*\\(`);
26815
+ const funcMatch = funcRegex.exec(source);
26816
+ let funcBody = source;
26817
+ if (!funcMatch) {
26818
+ const aliasMatch = source.match(new RegExp(`const\\s+${componentName}\\s*=\\s*(\\w+)`));
26819
+ if (aliasMatch) {
26820
+ return analyzeFunction(source, aliasMatch[1]);
25883
26821
  }
25884
- lines.push(" </div>");
25885
- lines.push(" )");
25886
- lines.push("}");
25887
- lines.push("");
25888
26822
  }
25889
- }
25890
- function generateMultiComponent(lines, previewNames, meta, pascalName, _subNames) {
25891
- previewNames.push("Default");
25892
- if (meta.examples.length > 0) {
25893
- const example = meta.examples[0];
25894
- const { statements, jsx } = splitExampleCode(example.code);
25895
- lines.push("export function Default() {");
25896
- for (const stmt of statements) {
25897
- lines.push(` ${stmt}`);
25898
- }
25899
- const definedNames = new Set(
25900
- statements.flatMap((s) => [...s.matchAll(/\b(?:const|let|var)\s+(?:\[([^\]]+)\]|(\w+))/g)]).flatMap((m) => m[1] ? m[1].split(",").map((v) => v.trim()) : [m[2]])
25901
- );
25902
- const handlerRefs = [...new Set([...jsx.matchAll(/\b(handle[A-Z]\w*)\b/g)].map((m) => m[1]))];
25903
- for (const h of handlerRefs) {
25904
- if (!definedNames.has(h)) {
25905
- lines.push(` const ${h} = () => {}`);
26823
+ if (funcMatch) {
26824
+ const parenStart = source.indexOf("(", funcMatch.index);
26825
+ let parenDepth = 0;
26826
+ let parenEnd = parenStart;
26827
+ for (let i = parenStart; i < source.length; i++) {
26828
+ if (source[i] === "(") parenDepth++;
26829
+ else if (source[i] === ")") {
26830
+ parenDepth--;
26831
+ if (parenDepth === 0) {
26832
+ parenEnd = i;
26833
+ break;
26834
+ }
25906
26835
  }
25907
26836
  }
25908
- if (statements.length > 0 || handlerRefs.length > 0) {
25909
- lines.push("");
25910
- }
25911
- const jsxLines = jsx.split("\n");
25912
- if (jsxLines.length === 1) {
25913
- lines.push(` return ${jsx}`);
25914
- } else {
25915
- lines.push(" return (");
25916
- for (const l of jsxLines) {
25917
- lines.push(` ${l}`);
26837
+ let depth = 0;
26838
+ let bodyStart = -1;
26839
+ for (let i = parenEnd + 1; i < source.length; i++) {
26840
+ if (source[i] === "{") {
26841
+ if (bodyStart === -1) bodyStart = i;
26842
+ depth++;
26843
+ } else if (source[i] === "}") {
26844
+ depth--;
26845
+ if (depth === 0) {
26846
+ funcBody = source.slice(bodyStart, i + 1);
26847
+ break;
26848
+ }
25918
26849
  }
25919
- lines.push(" )");
25920
26850
  }
25921
- lines.push("}");
25922
- } else {
25923
- lines.push("export function Default() {");
25924
- lines.push(" return (");
25925
- lines.push(` <${pascalName}>`);
25926
- for (const sub2 of meta.subComponents) {
25927
- const hasChildrenProp = sub2.props.some((p) => p.name === "children");
25928
- const label = sub2.name.replace(pascalName, "") || sub2.name;
25929
- if (hasChildrenProp) {
25930
- lines.push(` <${sub2.name}>${label}</${sub2.name}>`);
25931
- } else {
25932
- lines.push(` <${sub2.name} />`);
25933
- }
26851
+ }
26852
+ const signals = [];
26853
+ const signalRegex = /const\s+\[(\w+),\s*\w+\]\s*=\s*createSignal/g;
26854
+ let sm;
26855
+ while ((sm = signalRegex.exec(funcBody)) !== null) {
26856
+ signals.push(sm[1]);
26857
+ }
26858
+ const returnMatches = [...funcBody.matchAll(/return\s*\(?\s*<(\w+)/g)];
26859
+ const rootTag = returnMatches.length > 0 ? returnMatches[returnMatches.length - 1][1] : null;
26860
+ const allSlots = [...funcBody.matchAll(/data-slot="([^"]+)"/g)].map((m) => m[1]);
26861
+ const expectedSlot = componentName.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
26862
+ const dataSlot = allSlots.find((s) => s === expectedSlot) || allSlots[0] || null;
26863
+ const roleMatch = funcBody.match(/role="([^"]+)"/);
26864
+ const role = roleMatch ? roleMatch[1] : null;
26865
+ const ariaMatches = funcBody.match(/aria-[\w]+(?==)/g);
26866
+ const ariaAttributes = [...new Set(ariaMatches || [])].filter((a) => a !== "aria-invalid");
26867
+ const hasDataState = /data-state=/.test(funcBody);
26868
+ const hasConditionalReturn = /if\s*\(.*\)\s*\{?\s*return/.test(funcBody);
26869
+ const events = [];
26870
+ for (const [event, onProp] of Object.entries(ON_PROP_BY_EVENT)) {
26871
+ if (new RegExp(`${onProp}=`).test(funcBody)) events.push(event);
26872
+ }
26873
+ const childCompRegex = /<([A-Z][A-Za-z]+)[\s/>]/g;
26874
+ const childComponents = [];
26875
+ let cm;
26876
+ while ((cm = childCompRegex.exec(funcBody)) !== null) {
26877
+ if (!childComponents.includes(cm[1])) {
26878
+ childComponents.push(cm[1]);
25934
26879
  }
25935
- lines.push(` </${pascalName}>`);
25936
- lines.push(" )");
25937
- lines.push("}");
25938
26880
  }
25939
- lines.push("");
26881
+ return {
26882
+ signals,
26883
+ rootTag,
26884
+ dataSlot,
26885
+ role,
26886
+ ariaAttributes,
26887
+ hasDataState,
26888
+ hasConditionalReturn,
26889
+ events,
26890
+ childComponents
26891
+ };
25940
26892
  }
25941
- var init_preview_generate = __esm({
25942
- "src/lib/preview-generate.ts"() {
26893
+ function toCamelCase2(kebab) {
26894
+ return kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
26895
+ }
26896
+ var ON_PROP_BY_EVENT;
26897
+ var init_test_template = __esm({
26898
+ "src/lib/test-template.ts"() {
26899
+ "use strict";
26900
+ init_parse_component();
26901
+ ON_PROP_BY_EVENT = {
26902
+ click: "onClick",
26903
+ input: "onInput",
26904
+ change: "onChange",
26905
+ keydown: "onKeyDown"
26906
+ };
26907
+ }
26908
+ });
26909
+
26910
+ // src/commands/gen-test.ts
26911
+ var gen_test_exports = {};
26912
+ __export(gen_test_exports, {
26913
+ run: () => run12
26914
+ });
26915
+ import { existsSync as existsSync21, writeFileSync as writeFileSync7 } from "fs";
26916
+ import path21 from "path";
26917
+ function run12(args2, ctx2) {
26918
+ const positional = args2.filter((a) => !a.startsWith("-"));
26919
+ const flagSet = new Set(args2.filter((a) => a.startsWith("-")));
26920
+ const writeToStdout = flagSet.has("--stdout");
26921
+ const force = flagSet.has("--force") || flagSet.has("-f");
26922
+ const componentName = positional[0];
26923
+ if (!componentName) {
26924
+ console.error("Error: Component name required. Usage: bf gen test <component> [--stdout] [--force]");
26925
+ process.exit(1);
26926
+ }
26927
+ const searched = [];
26928
+ const resolved = resolveComponentSource(componentName, ctx2, searched);
26929
+ if (!resolved) {
26930
+ console.error(`Error: Cannot find component "${componentName}".`);
26931
+ console.error("Looked in:");
26932
+ for (const p of searched) console.error(` - ${p}`);
26933
+ process.exit(1);
26934
+ }
26935
+ const pm = detectPackageManager(ctx2.projectDir ?? ctx2.root);
26936
+ const runner = testRunnerFor(pm);
26937
+ const content = generateTestTemplate(resolved.filePath, { importSource: runner.importSource });
26938
+ if (writeToStdout) {
26939
+ console.log(content);
26940
+ return;
26941
+ }
26942
+ const dir = path21.dirname(resolved.filePath);
26943
+ const base = path21.basename(resolved.filePath, path21.extname(resolved.filePath));
26944
+ const testPath = path21.join(dir, `${base}.test.tsx`);
26945
+ if (existsSync21(testPath) && !force) {
26946
+ const rel2 = path21.relative(ctx2.projectDir ?? ctx2.root, testPath);
26947
+ console.error(`Error: ${rel2} already exists. Pass --force to overwrite, or --stdout to preview.`);
26948
+ process.exit(1);
26949
+ }
26950
+ writeFileSync7(testPath, content);
26951
+ const rel = path21.relative(ctx2.projectDir ?? ctx2.root, testPath);
26952
+ console.log(`Created: ${rel}`);
26953
+ console.log(``);
26954
+ console.log(`Next: ${commandsFor(pm).test(rel)}`);
26955
+ }
26956
+ var init_gen_test = __esm({
26957
+ "src/commands/gen-test.ts"() {
25943
26958
  "use strict";
26959
+ init_resolve_source();
26960
+ init_test_template();
26961
+ init_pm();
25944
26962
  }
25945
26963
  });
25946
26964
 
@@ -25949,7 +26967,7 @@ var gen_preview_exports = {};
25949
26967
  __export(gen_preview_exports, {
25950
26968
  run: () => run13
25951
26969
  });
25952
- import { existsSync as existsSync18, writeFileSync as writeFileSync7, mkdirSync as mkdirSync5 } from "fs";
26970
+ import { existsSync as existsSync22, writeFileSync as writeFileSync8, mkdirSync as mkdirSync5 } from "fs";
25953
26971
  import path22 from "path";
25954
26972
  async function run13(args2, ctx2) {
25955
26973
  const force = args2.includes("--force");
@@ -25962,15 +26980,15 @@ async function run13(args2, ctx2) {
25962
26980
  const { writeRoot, componentsBasePath } = resolveScaffoldLayout(ctx2);
25963
26981
  const result = generatePreview(meta, componentsBasePath);
25964
26982
  const absPath = path22.join(writeRoot, result.filePath);
25965
- if (existsSync18(absPath) && !force) {
26983
+ if (existsSync22(absPath) && !force) {
25966
26984
  console.error(`Error: ${result.filePath} already exists. Use --force to overwrite.`);
25967
26985
  process.exit(1);
25968
26986
  }
25969
26987
  const dir = path22.dirname(absPath);
25970
- if (!existsSync18(dir)) {
26988
+ if (!existsSync22(dir)) {
25971
26989
  mkdirSync5(dir, { recursive: true });
25972
26990
  }
25973
- writeFileSync7(absPath, result.code);
26991
+ writeFileSync8(absPath, result.code);
25974
26992
  console.log(`Generated ${result.filePath}`);
25975
26993
  console.log(`Previews: ${result.previewNames.join(", ")}`);
25976
26994
  }
@@ -25988,7 +27006,7 @@ var debug_graph_exports = {};
25988
27006
  __export(debug_graph_exports, {
25989
27007
  run: () => run14
25990
27008
  });
25991
- import { readFileSync as readFileSync9 } from "fs";
27009
+ import { readFileSync as readFileSync10 } from "fs";
25992
27010
  async function run14(args2, ctx2) {
25993
27011
  const componentName = args2[0];
25994
27012
  if (!componentName) {
@@ -26005,7 +27023,7 @@ async function run14(args2, ctx2) {
26005
27023
  for (const p of searched) console.error(` - ${p}`);
26006
27024
  process.exit(1);
26007
27025
  }
26008
- const source = readFileSync9(resolved.filePath, "utf-8");
27026
+ const source = readFileSync10(resolved.filePath, "utf-8");
26009
27027
  const graph = buildComponentGraph2(source, resolved.filePath, resolved.componentName);
26010
27028
  if (ctx2.jsonFlag) {
26011
27029
  console.log(JSON.stringify(graphToJSON2(graph), null, 2));
@@ -26025,7 +27043,7 @@ var debug_trace_exports = {};
26025
27043
  __export(debug_trace_exports, {
26026
27044
  run: () => run15
26027
27045
  });
26028
- import { readFileSync as readFileSync10 } from "fs";
27046
+ import { readFileSync as readFileSync11 } from "fs";
26029
27047
  async function run15(args2, ctx2) {
26030
27048
  const componentName = args2[0];
26031
27049
  const targetName = args2[1];
@@ -26043,7 +27061,7 @@ async function run15(args2, ctx2) {
26043
27061
  for (const p of searched) console.error(` - ${p}`);
26044
27062
  process.exit(1);
26045
27063
  }
26046
- const source = readFileSync10(resolved.filePath, "utf-8");
27064
+ const source = readFileSync11(resolved.filePath, "utf-8");
26047
27065
  const graph = buildComponentGraph2(source, resolved.filePath, resolved.componentName);
26048
27066
  const path23 = traceUpdatePath2(graph, targetName);
26049
27067
  if (!path23) {
@@ -26075,7 +27093,7 @@ var debug_fallbacks_exports = {};
26075
27093
  __export(debug_fallbacks_exports, {
26076
27094
  run: () => run16
26077
27095
  });
26078
- import { readFileSync as readFileSync11 } from "fs";
27096
+ import { readFileSync as readFileSync12 } from "fs";
26079
27097
  async function run16(args2, ctx2) {
26080
27098
  const componentName = args2[0];
26081
27099
  if (!componentName) {
@@ -26083,7 +27101,7 @@ async function run16(args2, ctx2) {
26083
27101
  console.error("Usage: bf debug fallbacks <component> [--json]");
26084
27102
  process.exit(1);
26085
27103
  }
26086
- const { buildComponentGraph: buildComponentGraph2 } = await Promise.resolve().then(() => (init_src2(), src_exports));
27104
+ const { buildComponentGraph: buildComponentGraph2, describeFallback: describeFallback2, formatFallbackExplanations: formatFallbackExplanations2 } = await Promise.resolve().then(() => (init_src2(), src_exports));
26087
27105
  const searched = [];
26088
27106
  const resolved = resolveComponentSource(componentName, ctx2, searched);
26089
27107
  if (!resolved) {
@@ -26092,45 +27110,35 @@ async function run16(args2, ctx2) {
26092
27110
  for (const p of searched) console.error(` - ${p}`);
26093
27111
  process.exit(1);
26094
27112
  }
26095
- const source = readFileSync11(resolved.filePath, "utf-8");
27113
+ const source = readFileSync12(resolved.filePath, "utf-8");
26096
27114
  const graph = buildComponentGraph2(source, resolved.filePath, resolved.componentName);
26097
- const fallbacks = graph.domBindings.filter((d) => d.classification === "fallback");
27115
+ const isEventHandlerProp = (d) => d.type === "attribute" && /^on[A-Z]/.test(d.label.split(".").pop() ?? "");
27116
+ const fallbacks = graph.domBindings.filter((d) => d.classification === "fallback" && !isEventHandlerProp(d));
26098
27117
  if (ctx2.jsonFlag) {
26099
27118
  console.log(JSON.stringify({
26100
27119
  componentName: graph.componentName,
26101
27120
  sourceFile: graph.sourceFile,
26102
- fallbacks: fallbacks.map((f) => ({
26103
- label: f.label,
26104
- slotId: f.slotId,
26105
- deps: f.deps,
26106
- type: f.type,
26107
- classification: f.classification,
26108
- ...f.expression !== void 0 && { expression: f.expression },
26109
- ...f.wrapReason !== void 0 && { wrapReason: f.wrapReason }
26110
- }))
27121
+ fallbacks: fallbacks.map((f) => {
27122
+ const ex = describeFallback2(f);
27123
+ return {
27124
+ label: f.label,
27125
+ slotId: f.slotId,
27126
+ deps: f.deps,
27127
+ type: f.type,
27128
+ classification: f.classification,
27129
+ ...f.expression !== void 0 && { expression: f.expression },
27130
+ ...f.wrapReason !== void 0 && { wrapReason: f.wrapReason },
27131
+ reason: ex.reason,
27132
+ runtimeDeps: ex.runtimeDeps,
27133
+ suggestion: ex.suggestion,
27134
+ isEventHandler: ex.isEventHandler,
27135
+ ...ex.loc && { loc: ex.loc }
27136
+ };
27137
+ })
26111
27138
  }, null, 2));
26112
27139
  return;
26113
27140
  }
26114
- if (fallbacks.length === 0) {
26115
- console.log(`${graph.componentName} \u2014 no fallback-wrapped expressions.`);
26116
- return;
26117
- }
26118
- console.log(`${graph.componentName} \u2014 ${fallbacks.length} fallback-wrapped expression(s)`);
26119
- const cells = fallbacks.map((f) => {
26120
- const id = f.type === "attribute" ? f.label : f.slotId;
26121
- return { f, cell: `${f.type} "${id}"` };
26122
- });
26123
- const width = cells.reduce((w, c) => Math.max(w, c.cell.length), 0);
26124
- for (const { f, cell } of cells) {
26125
- const expr = f.expression ?? "(expression not captured)";
26126
- const reason = f.wrapReason ? ` [${f.wrapReason}]` : "";
26127
- console.log(` ${cell.padEnd(width)} ~ ${expr}${reason}`);
26128
- }
26129
- console.log();
26130
- console.log("Fallback wraps run one createEffect per expression. Each subscribes to");
26131
- console.log("whatever signals it happens to read at runtime (possibly none).");
26132
- console.log("Rewrite the expression as a createMemo to make the dependency static,");
26133
- console.log("or inline a known-reactive source so the emitter can prove it.");
27141
+ console.log(formatFallbackExplanations2(graph.componentName, fallbacks));
26134
27142
  }
26135
27143
  var init_debug_fallbacks = __esm({
26136
27144
  "src/commands/debug-fallbacks.ts"() {
@@ -26144,7 +27152,7 @@ var debug_signals_exports = {};
26144
27152
  __export(debug_signals_exports, {
26145
27153
  run: () => run17
26146
27154
  });
26147
- import { readFileSync as readFileSync12 } from "fs";
27155
+ import { readFileSync as readFileSync13 } from "fs";
26148
27156
  async function run17(args2, ctx2) {
26149
27157
  const componentName = args2[0];
26150
27158
  if (!componentName) {
@@ -26161,7 +27169,7 @@ async function run17(args2, ctx2) {
26161
27169
  for (const p of searched) console.error(` - ${p}`);
26162
27170
  process.exit(1);
26163
27171
  }
26164
- const source = readFileSync12(resolved.filePath, "utf-8");
27172
+ const source = readFileSync13(resolved.filePath, "utf-8");
26165
27173
  const graph = buildComponentGraph2(source, resolved.filePath, resolved.componentName);
26166
27174
  const trace = generateStaticTrace2(graph);
26167
27175
  if (ctx2.jsonFlag) {
@@ -26184,7 +27192,7 @@ var debug_events_exports = {};
26184
27192
  __export(debug_events_exports, {
26185
27193
  run: () => run18
26186
27194
  });
26187
- import { readFileSync as readFileSync13 } from "fs";
27195
+ import { readFileSync as readFileSync14 } from "fs";
26188
27196
  async function run18(args2, ctx2) {
26189
27197
  const componentName = args2[0];
26190
27198
  if (!componentName) {
@@ -26201,7 +27209,7 @@ async function run18(args2, ctx2) {
26201
27209
  for (const p of searched) console.error(` - ${p}`);
26202
27210
  process.exit(1);
26203
27211
  }
26204
- const source = readFileSync13(resolved.filePath, "utf-8");
27212
+ const source = readFileSync14(resolved.filePath, "utf-8");
26205
27213
  const summary = buildEventSummary2(source, resolved.filePath, resolved.componentName);
26206
27214
  if (ctx2.jsonFlag) {
26207
27215
  console.log(JSON.stringify({ componentName: summary.componentName, sourceFile: summary.sourceFile, events: summary.events }, null, 2));
@@ -26221,7 +27229,7 @@ var debug_loops_exports = {};
26221
27229
  __export(debug_loops_exports, {
26222
27230
  run: () => run19
26223
27231
  });
26224
- import { readFileSync as readFileSync14 } from "fs";
27232
+ import { readFileSync as readFileSync15 } from "fs";
26225
27233
  async function run19(args2, ctx2) {
26226
27234
  const componentName = args2[0];
26227
27235
  if (!componentName) {
@@ -26238,7 +27246,7 @@ async function run19(args2, ctx2) {
26238
27246
  for (const p of searched) console.error(` - ${p}`);
26239
27247
  process.exit(1);
26240
27248
  }
26241
- const source = readFileSync14(resolved.filePath, "utf-8");
27249
+ const source = readFileSync15(resolved.filePath, "utf-8");
26242
27250
  const summary = buildLoopSummary2(source, resolved.filePath, resolved.componentName);
26243
27251
  if (ctx2.jsonFlag) {
26244
27252
  console.log(JSON.stringify(summary, null, 2));
@@ -26253,6 +27261,101 @@ var init_debug_loops = __esm({
26253
27261
  }
26254
27262
  });
26255
27263
 
27264
+ // src/commands/debug-why-update.ts
27265
+ var debug_why_update_exports = {};
27266
+ __export(debug_why_update_exports, {
27267
+ run: () => run20
27268
+ });
27269
+ import { readFileSync as readFileSync16 } from "fs";
27270
+ async function run20(args2, ctx2) {
27271
+ const componentName = args2[0];
27272
+ const bindingLabel = args2[1];
27273
+ if (!componentName || !bindingLabel) {
27274
+ console.error("Error: Component name and binding label required.");
27275
+ console.error("Usage: bf debug why-update <component> <binding> [--json]");
27276
+ console.error(' binding: attribute name (e.g. "style"), slot ID (e.g. "s0"),');
27277
+ console.error(' or component prop (e.g. "Button.disabled")');
27278
+ process.exit(1);
27279
+ }
27280
+ const { buildWhyUpdate: buildWhyUpdate2, formatWhyUpdate: formatWhyUpdate2, buildComponentGraph: buildComponentGraph2 } = await Promise.resolve().then(() => (init_src2(), src_exports));
27281
+ const searched = [];
27282
+ const resolved = resolveComponentSource(componentName, ctx2, searched);
27283
+ if (!resolved) {
27284
+ console.error(`Error: Cannot find component "${componentName}".`);
27285
+ console.error("Looked in:");
27286
+ for (const p of searched) console.error(` - ${p}`);
27287
+ process.exit(1);
27288
+ }
27289
+ const source = readFileSync16(resolved.filePath, "utf-8");
27290
+ const result = buildWhyUpdate2(source, resolved.filePath, bindingLabel, resolved.componentName);
27291
+ if (!result) {
27292
+ const graph = buildComponentGraph2(source, resolved.filePath, resolved.componentName);
27293
+ console.error(`Error: Binding "${bindingLabel}" not found in ${graph.componentName}.`);
27294
+ const available = graph.domBindings.map(
27295
+ (d) => d.type === "attribute" ? d.label : d.slotId
27296
+ );
27297
+ if (available.length > 0) {
27298
+ console.error(`Available bindings: ${[...new Set(available)].join(", ")}`);
27299
+ }
27300
+ process.exit(1);
27301
+ }
27302
+ if (result.ambiguous) {
27303
+ console.error(`Error: "${bindingLabel}" matches multiple bindings. Disambiguate with a slot ID:`);
27304
+ for (const m of result.ambiguous) {
27305
+ console.error(` ${m.slotId} (${m.label})`);
27306
+ }
27307
+ process.exit(1);
27308
+ }
27309
+ if (ctx2.jsonFlag) {
27310
+ console.log(JSON.stringify(result, null, 2));
27311
+ return;
27312
+ }
27313
+ console.log(formatWhyUpdate2(result));
27314
+ }
27315
+ var init_debug_why_update = __esm({
27316
+ "src/commands/debug-why-update.ts"() {
27317
+ "use strict";
27318
+ init_resolve_source();
27319
+ }
27320
+ });
27321
+
27322
+ // src/commands/debug-summary.ts
27323
+ var debug_summary_exports = {};
27324
+ __export(debug_summary_exports, {
27325
+ run: () => run21
27326
+ });
27327
+ import { readFileSync as readFileSync17 } from "fs";
27328
+ async function run21(args2, ctx2) {
27329
+ const componentName = args2[0];
27330
+ if (!componentName) {
27331
+ console.error("Error: Component name required.");
27332
+ console.error("Usage: bf debug summary <component> [--json]");
27333
+ process.exit(1);
27334
+ }
27335
+ const { buildComponentSummary: buildComponentSummary2, formatComponentSummary: formatComponentSummary2 } = await Promise.resolve().then(() => (init_src2(), src_exports));
27336
+ const searched = [];
27337
+ const resolved = resolveComponentSource(componentName, ctx2, searched);
27338
+ if (!resolved) {
27339
+ console.error(`Error: Cannot find component "${componentName}".`);
27340
+ console.error("Looked in:");
27341
+ for (const p of searched) console.error(` - ${p}`);
27342
+ process.exit(1);
27343
+ }
27344
+ const source = readFileSync17(resolved.filePath, "utf-8");
27345
+ const summary = buildComponentSummary2(source, resolved.filePath, resolved.componentName);
27346
+ if (ctx2.jsonFlag) {
27347
+ console.log(JSON.stringify(summary, null, 2));
27348
+ return;
27349
+ }
27350
+ console.log(formatComponentSummary2(summary));
27351
+ }
27352
+ var init_debug_summary = __esm({
27353
+ "src/commands/debug-summary.ts"() {
27354
+ "use strict";
27355
+ init_resolve_source();
27356
+ }
27357
+ });
27358
+
26256
27359
  // src/context.ts
26257
27360
  import { existsSync as existsSync2 } from "fs";
26258
27361
  import path from "path";
@@ -26272,9 +27375,9 @@ function findProjectConfig(startDir) {
26272
27375
  let dir = path.resolve(startDir);
26273
27376
  const { root: fsRoot } = path.parse(dir);
26274
27377
  while (true) {
26275
- const ts19 = path.join(dir, "barefoot.config.ts");
26276
- if (existsSync2(ts19)) {
26277
- return { dir, tsConfigPath: ts19 };
27378
+ const ts20 = path.join(dir, "barefoot.config.ts");
27379
+ if (existsSync2(ts20)) {
27380
+ return { dir, tsConfigPath: ts20 };
26278
27381
  }
26279
27382
  if (dir === fsRoot) return null;
26280
27383
  dir = path.dirname(dir);
@@ -26350,6 +27453,8 @@ Debug:
26350
27453
  debug trace <component> <signal> Trace update propagation for a signal/memo
26351
27454
  debug events <component> Show event handlers and their update paths
26352
27455
  debug loops <component> Show loop bindings grouped by source collection
27456
+ debug why-update <component> <binding> Explain why a binding updates
27457
+ debug summary <component> Show hydration and size summary
26353
27458
  debug fallbacks <component> Show wrap-by-default fallback bindings (#937)
26354
27459
  debug signals <component> Show signal initialization trace
26355
27460
 
@@ -26369,60 +27474,60 @@ Workflow:
26369
27474
  }
26370
27475
  switch (command) {
26371
27476
  case "build": {
26372
- const { run: run20 } = await Promise.resolve().then(() => (init_build2(), build_exports));
26373
- await run20(filteredArgs.slice(1), ctx);
27477
+ const { run: run22 } = await Promise.resolve().then(() => (init_build2(), build_exports));
27478
+ await run22(filteredArgs.slice(1), ctx);
26374
27479
  break;
26375
27480
  }
26376
27481
  case "init": {
26377
- const { run: run20 } = await Promise.resolve().then(() => (init_init(), init_exports));
26378
- await run20(filteredArgs.slice(1), ctx);
27482
+ const { run: run22 } = await Promise.resolve().then(() => (init_init(), init_exports));
27483
+ await run22(filteredArgs.slice(1), ctx);
26379
27484
  break;
26380
27485
  }
26381
27486
  case "add": {
26382
- const { run: run20 } = await Promise.resolve().then(() => (init_add(), add_exports));
26383
- await run20(filteredArgs.slice(1), ctx);
27487
+ const { run: run22 } = await Promise.resolve().then(() => (init_add(), add_exports));
27488
+ await run22(filteredArgs.slice(1), ctx);
26384
27489
  break;
26385
27490
  }
26386
27491
  case "search": {
26387
- const { run: run20 } = await Promise.resolve().then(() => (init_search(), search_exports));
26388
- await run20(filteredArgs.slice(1), ctx);
27492
+ const { run: run22 } = await Promise.resolve().then(() => (init_search(), search_exports));
27493
+ await run22(filteredArgs.slice(1), ctx);
26389
27494
  break;
26390
27495
  }
26391
27496
  case "docs": {
26392
- const { run: run20 } = await Promise.resolve().then(() => (init_docs(), docs_exports));
26393
- run20(filteredArgs.slice(1), ctx);
27497
+ const { run: run22 } = await Promise.resolve().then(() => (init_docs(), docs_exports));
27498
+ run22(filteredArgs.slice(1), ctx);
26394
27499
  break;
26395
27500
  }
26396
27501
  case "guide": {
26397
- const { run: run20 } = await Promise.resolve().then(() => (init_guide(), guide_exports));
26398
- run20(filteredArgs.slice(1), ctx);
27502
+ const { run: run22 } = await Promise.resolve().then(() => (init_guide(), guide_exports));
27503
+ run22(filteredArgs.slice(1), ctx);
26399
27504
  break;
26400
27505
  }
26401
27506
  case "preview": {
26402
- const { run: run20 } = await Promise.resolve().then(() => (init_preview(), preview_exports));
26403
- await run20(filteredArgs.slice(1), ctx);
27507
+ const { run: run22 } = await Promise.resolve().then(() => (init_preview(), preview_exports));
27508
+ await run22(filteredArgs.slice(1), ctx);
26404
27509
  break;
26405
27510
  }
26406
27511
  case "tokens": {
26407
27512
  if (sub === "apply") {
26408
- const { run: run20 } = await Promise.resolve().then(() => (init_tokens_apply(), tokens_apply_exports));
26409
- await run20(rest, ctx);
27513
+ const { run: run22 } = await Promise.resolve().then(() => (init_tokens_apply(), tokens_apply_exports));
27514
+ await run22(rest, ctx);
26410
27515
  } else {
26411
- const { run: run20 } = await Promise.resolve().then(() => (init_tokens(), tokens_exports));
26412
- await run20(filteredArgs.slice(1), ctx);
27516
+ const { run: run22 } = await Promise.resolve().then(() => (init_tokens2(), tokens_exports));
27517
+ await run22(filteredArgs.slice(1), ctx);
26413
27518
  }
26414
27519
  break;
26415
27520
  }
26416
27521
  case "gen": {
26417
27522
  if (sub === "component") {
26418
- const { run: run20 } = await Promise.resolve().then(() => (init_gen_component(), gen_component_exports));
26419
- run20(rest, ctx);
27523
+ const { run: run22 } = await Promise.resolve().then(() => (init_gen_component(), gen_component_exports));
27524
+ run22(rest, ctx);
26420
27525
  } else if (sub === "test") {
26421
- const { run: run20 } = await Promise.resolve().then(() => (init_gen_test(), gen_test_exports));
26422
- run20(rest, ctx);
27526
+ const { run: run22 } = await Promise.resolve().then(() => (init_gen_test(), gen_test_exports));
27527
+ run22(rest, ctx);
26423
27528
  } else if (sub === "preview") {
26424
- const { run: run20 } = await Promise.resolve().then(() => (init_gen_preview(), gen_preview_exports));
26425
- await run20(rest, ctx);
27529
+ const { run: run22 } = await Promise.resolve().then(() => (init_gen_preview(), gen_preview_exports));
27530
+ await run22(rest, ctx);
26426
27531
  } else {
26427
27532
  console.error("Usage: bf gen <component|test|preview> ...");
26428
27533
  process.exit(1);
@@ -26431,33 +27536,39 @@ switch (command) {
26431
27536
  }
26432
27537
  case "debug": {
26433
27538
  if (sub === "graph") {
26434
- const { run: run20 } = await Promise.resolve().then(() => (init_debug_graph(), debug_graph_exports));
26435
- await run20(rest, ctx);
27539
+ const { run: run22 } = await Promise.resolve().then(() => (init_debug_graph(), debug_graph_exports));
27540
+ await run22(rest, ctx);
26436
27541
  } else if (sub === "trace") {
26437
- const { run: run20 } = await Promise.resolve().then(() => (init_debug_trace(), debug_trace_exports));
26438
- await run20(rest, ctx);
27542
+ const { run: run22 } = await Promise.resolve().then(() => (init_debug_trace(), debug_trace_exports));
27543
+ await run22(rest, ctx);
26439
27544
  } else if (sub === "fallbacks") {
26440
- const { run: run20 } = await Promise.resolve().then(() => (init_debug_fallbacks(), debug_fallbacks_exports));
26441
- await run20(rest, ctx);
27545
+ const { run: run22 } = await Promise.resolve().then(() => (init_debug_fallbacks(), debug_fallbacks_exports));
27546
+ await run22(rest, ctx);
26442
27547
  } else if (sub === "signals") {
26443
- const { run: run20 } = await Promise.resolve().then(() => (init_debug_signals(), debug_signals_exports));
26444
- await run20(rest, ctx);
27548
+ const { run: run22 } = await Promise.resolve().then(() => (init_debug_signals(), debug_signals_exports));
27549
+ await run22(rest, ctx);
26445
27550
  } else if (sub === "events") {
26446
- const { run: run20 } = await Promise.resolve().then(() => (init_debug_events(), debug_events_exports));
26447
- await run20(rest, ctx);
27551
+ const { run: run22 } = await Promise.resolve().then(() => (init_debug_events(), debug_events_exports));
27552
+ await run22(rest, ctx);
26448
27553
  } else if (sub === "loops") {
26449
- const { run: run20 } = await Promise.resolve().then(() => (init_debug_loops(), debug_loops_exports));
26450
- await run20(rest, ctx);
27554
+ const { run: run22 } = await Promise.resolve().then(() => (init_debug_loops(), debug_loops_exports));
27555
+ await run22(rest, ctx);
27556
+ } else if (sub === "why-update") {
27557
+ const { run: run22 } = await Promise.resolve().then(() => (init_debug_why_update(), debug_why_update_exports));
27558
+ await run22(rest, ctx);
27559
+ } else if (sub === "summary") {
27560
+ const { run: run22 } = await Promise.resolve().then(() => (init_debug_summary(), debug_summary_exports));
27561
+ await run22(rest, ctx);
26451
27562
  } else {
26452
- console.error("Usage: bf debug <graph|trace|fallbacks|signals|events|loops> ...");
27563
+ console.error("Usage: bf debug <graph|trace|fallbacks|signals|events|loops|why-update|summary> ...");
26453
27564
  process.exit(1);
26454
27565
  }
26455
27566
  break;
26456
27567
  }
26457
27568
  case "meta": {
26458
27569
  if (sub === "extract") {
26459
- const { run: run20 } = await Promise.resolve().then(() => (init_meta_extract(), meta_extract_exports));
26460
- await run20(rest, ctx);
27570
+ const { run: run22 } = await Promise.resolve().then(() => (init_meta_extract(), meta_extract_exports));
27571
+ await run22(rest, ctx);
26461
27572
  } else {
26462
27573
  console.error("Usage: bf meta extract");
26463
27574
  process.exit(1);