@barefootjs/cli 0.2.0 → 0.3.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
  });
@@ -7147,7 +7147,7 @@ function transformExpressionInner(expr, ctx2, node, isClientOnly) {
7147
7147
  }
7148
7148
  const ir = transformJsxExpression(expr, ctx2, isClientOnly);
7149
7149
  if (ir !== null) {
7150
- if (isClientOnly && ir.type === "conditional") {
7150
+ if ((isClientOnly || shouldAutoDeferReactiveBrand(expr, ctx2)) && ir.type === "conditional") {
7151
7151
  ir.clientOnly = true;
7152
7152
  if (!ir.slotId) {
7153
7153
  ir.slotId = generateSlotId(ctx2);
@@ -8524,7 +8524,7 @@ function processAttributes(attributes, ctx2) {
8524
8524
  value = { ...value, templateExpr: rewritten };
8525
8525
  }
8526
8526
  }
8527
- if (hasLeadingClientDirective(attr.initializer.expression, ctx2.sourceFile)) {
8527
+ if (hasLeadingClientDirective(attr.initializer.expression, ctx2.sourceFile) || shouldAutoDeferReactiveBrand(attr.initializer.expression, ctx2)) {
8528
8528
  clientOnly = true;
8529
8529
  }
8530
8530
  }
@@ -8927,6 +8927,13 @@ function isReactiveExpression(expr, ctx2, astNode) {
8927
8927
  }
8928
8928
  return false;
8929
8929
  }
8930
+ function shouldAutoDeferReactiveBrand(expr, ctx2) {
8931
+ const checker = ctx2.analyzer.checker;
8932
+ if (!checker) return false;
8933
+ if (!containsReactiveExpression(expr, checker)) return false;
8934
+ if (isSignalOrMemoReference(ctx2.getJS(expr), ctx2)) return false;
8935
+ return true;
8936
+ }
8930
8937
  function isSignalOrMemoReference(expr, ctx2, visited) {
8931
8938
  for (const { pattern } of ctx2.patterns.signals) {
8932
8939
  if (pattern.test(expr)) return true;
@@ -9026,10 +9033,10 @@ function hasDynamicContent(children) {
9026
9033
  function inferExpressionType(_node, _ctx) {
9027
9034
  return null;
9028
9035
  }
9029
- function replaceBranchLocalRefs(text, branchNames, resolve7) {
9036
+ function replaceBranchLocalRefs(text, branchNames, resolve10) {
9030
9037
  if (branchNames.length === 0) return text;
9031
9038
  const pattern = new RegExp(`(?<![\\w$])(${branchNames.join("|")})(?![\\w$])`, "g");
9032
- return replaceInExprContexts(text, pattern, (_match, name) => resolve7(name));
9039
+ return replaceInExprContexts(text, pattern, (_match, name) => resolve10(name));
9033
9040
  }
9034
9041
  function buildIfStatementChain(analyzer, ctx2) {
9035
9042
  const conditionalReturns = analyzer.conditionalReturns;
@@ -17476,13 +17483,51 @@ function makeIdRefRegex(name) {
17476
17483
  }
17477
17484
  function buildLocalFunctionSetterMap(meta, setterToSignal) {
17478
17485
  const setterPatterns = [...setterToSignal.keys()].map((s) => ({ name: s, re: makeIdCallRegex(s) }));
17479
- const result = /* @__PURE__ */ new Map();
17480
- for (const fn of meta.localFunctions) {
17486
+ const bodies = /* @__PURE__ */ new Map();
17487
+ for (const fn of meta.localFunctions) bodies.set(fn.name, fn.body);
17488
+ for (const c of meta.localConstants) {
17489
+ if (c.containsArrow && c.value) bodies.set(c.name, c.value);
17490
+ }
17491
+ const fnNamePatterns = [...bodies.keys()].map((n) => ({ name: n, re: makeIdCallRegex(n) }));
17492
+ const directSetters = /* @__PURE__ */ new Map();
17493
+ const directCalls = /* @__PURE__ */ new Map();
17494
+ for (const [name, body] of bodies) {
17481
17495
  const setters = [];
17482
- for (const { name, re } of setterPatterns) {
17483
- if (re.test(fn.body)) setters.push(name);
17496
+ for (const { name: setter, re } of setterPatterns) {
17497
+ if (re.test(body)) setters.push(setter);
17498
+ }
17499
+ directSetters.set(name, setters);
17500
+ const calls = [];
17501
+ for (const { name: callee, re } of fnNamePatterns) {
17502
+ if (callee !== name && re.test(body)) calls.push(callee);
17503
+ }
17504
+ directCalls.set(name, calls);
17505
+ }
17506
+ const resolve10 = (name, stack) => {
17507
+ const out = [];
17508
+ const seen = /* @__PURE__ */ new Set();
17509
+ for (const setter of directSetters.get(name) ?? []) {
17510
+ if (!seen.has(setter)) {
17511
+ out.push({ setter, chain: [] });
17512
+ seen.add(setter);
17513
+ }
17514
+ }
17515
+ for (const callee of directCalls.get(name) ?? []) {
17516
+ if (stack.has(callee)) continue;
17517
+ const sub2 = resolve10(callee, /* @__PURE__ */ new Set([...stack, callee]));
17518
+ for (const r of sub2) {
17519
+ if (!seen.has(r.setter)) {
17520
+ out.push({ setter: r.setter, chain: [callee, ...r.chain] });
17521
+ seen.add(r.setter);
17522
+ }
17523
+ }
17484
17524
  }
17485
- if (setters.length > 0) result.set(fn.name, setters);
17525
+ return out;
17526
+ };
17527
+ const result = /* @__PURE__ */ new Map();
17528
+ for (const name of bodies.keys()) {
17529
+ const resolved = resolve10(name, /* @__PURE__ */ new Set([name]));
17530
+ if (resolved.length > 0) result.set(name, resolved);
17486
17531
  }
17487
17532
  return result;
17488
17533
  }
@@ -17574,12 +17619,16 @@ function resolveSetters(handler, setterToSignal, fnSetters) {
17574
17619
  }
17575
17620
  }
17576
17621
  }
17577
- for (const [fnName, setters] of fnSetters) {
17622
+ for (const [fnName, resolutions] of fnSetters) {
17578
17623
  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);
17624
+ for (const r of resolutions) {
17625
+ if (!seen.has(r.setter)) {
17626
+ refs.push({
17627
+ setter: r.setter,
17628
+ signal: setterToSignal.get(r.setter) ?? null,
17629
+ via: [fnName, ...r.chain]
17630
+ });
17631
+ seen.add(r.setter);
17583
17632
  }
17584
17633
  }
17585
17634
  }
@@ -17613,7 +17662,7 @@ function formatEventSummary(summary, graph) {
17613
17662
  lines.push("");
17614
17663
  lines.push(` ${event.elementContext}`);
17615
17664
  const setterParts = event.setterCalls.map((s) => {
17616
- const chain = s.via ? `${s.via} -> ${s.setter}` : s.setter;
17665
+ const chain = s.via && s.via.length > 0 ? `${s.via.join(" -> ")} -> ${s.setter}` : s.setter;
17617
17666
  return chain;
17618
17667
  });
17619
17668
  const setterStr = setterParts.length > 0 ? setterParts.join(", ") : event.handler;
@@ -17883,6 +17932,95 @@ function buildUpdateEntry(consumer, graph, visited) {
17883
17932
  }
17884
17933
  return { name: consumer, kind: "effect", label: consumer, children: [] };
17885
17934
  }
17935
+ function buildWhyUpdate(source, filePath, bindingLabel, componentName) {
17936
+ const { graph, ir } = buildComponentAnalysis(source, filePath, componentName);
17937
+ const matches = graph.domBindings.filter(
17938
+ (d) => d.label === bindingLabel || d.slotId === bindingLabel
17939
+ );
17940
+ if (matches.length === 0) return null;
17941
+ if (matches.length > 1) {
17942
+ return {
17943
+ binding: bindingLabel,
17944
+ expression: null,
17945
+ deps: [],
17946
+ ambiguous: matches.map((d) => ({ label: d.label, slotId: d.slotId }))
17947
+ };
17948
+ }
17949
+ const binding = matches[0];
17950
+ const setterToSignal = /* @__PURE__ */ new Map();
17951
+ for (const s of ir.metadata.signals) {
17952
+ if (s.setter) setterToSignal.set(s.setter, s.getter);
17953
+ }
17954
+ const fnSetters = buildLocalFunctionSetterMap(ir.metadata, setterToSignal);
17955
+ const events = collectEventBindings(ir.root, setterToSignal, fnSetters);
17956
+ const deps = [];
17957
+ const visited = /* @__PURE__ */ new Set();
17958
+ function traceDep(name) {
17959
+ if (visited.has(name)) return;
17960
+ visited.add(name);
17961
+ const signal = graph.signals.find((s) => s.name === name);
17962
+ if (signal) {
17963
+ const changedBy = [];
17964
+ for (const ev of events) {
17965
+ for (const sc of ev.setterCalls) {
17966
+ if (sc.signal === name) {
17967
+ changedBy.push({
17968
+ handler: ev.eventName,
17969
+ setter: sc.setter,
17970
+ elementContext: ev.elementContext,
17971
+ via: sc.via
17972
+ });
17973
+ }
17974
+ }
17975
+ }
17976
+ deps.push({ name, kind: "signal", dependsOn: [], changedBy });
17977
+ return;
17978
+ }
17979
+ const memo = graph.memos.find((m) => m.name === name);
17980
+ if (memo) {
17981
+ deps.push({ name, kind: "memo", dependsOn: memo.deps, changedBy: [] });
17982
+ for (const dep of memo.deps) traceDep(dep);
17983
+ }
17984
+ }
17985
+ for (const dep of binding.deps) traceDep(dep);
17986
+ const stableId = binding.type === "attribute" ? binding.label : binding.slotId;
17987
+ return {
17988
+ binding: stableId,
17989
+ expression: binding.expression ?? null,
17990
+ deps,
17991
+ ...binding.classification === "fallback" && { classification: binding.classification },
17992
+ ...binding.wrapReason && { wrapReason: binding.wrapReason }
17993
+ };
17994
+ }
17995
+ function formatWhyUpdate(result) {
17996
+ const lines = [];
17997
+ lines.push(`${result.binding} updates because:`);
17998
+ if (result.expression) {
17999
+ lines.push(` ${result.expression}`);
18000
+ }
18001
+ if (result.classification === "fallback") {
18002
+ lines.push("");
18003
+ lines.push(`note: this is a fallback-wrapped binding (${result.wrapReason ?? "unknown"})`);
18004
+ lines.push(" the compiler could not statically prove reactivity \u2014 deps are determined at runtime");
18005
+ }
18006
+ for (const dep of result.deps) {
18007
+ lines.push("");
18008
+ if (dep.kind === "memo") {
18009
+ lines.push(`${dep.name} depends on:`);
18010
+ for (const d of dep.dependsOn) lines.push(` ${d}`);
18011
+ } else {
18012
+ lines.push(`${dep.name} changes from:`);
18013
+ if (dep.changedBy.length === 0) {
18014
+ lines.push(" (no event handlers found)");
18015
+ }
18016
+ for (const src of dep.changedBy) {
18017
+ const chain = src.via && src.via.length > 0 ? `${src.elementContext} ${src.handler} -> ${src.via.join(" -> ")} -> ${src.setter}` : `${src.elementContext} ${src.handler} -> ${src.setter}`;
18018
+ lines.push(` ${chain}`);
18019
+ }
18020
+ }
18021
+ }
18022
+ return lines.join("\n");
18023
+ }
17886
18024
  function formatComponentGraph(graph) {
17887
18025
  const lines = [];
17888
18026
  lines.push(`${graph.componentName} (${graph.sourceFile})`);
@@ -18054,6 +18192,145 @@ function formatSignalTrace(traces) {
18054
18192
  }
18055
18193
  }).join("\n");
18056
18194
  }
18195
+ function describeFallback(binding) {
18196
+ const isEventHandler = binding.type === "event" || binding.type === "attribute" && /^on[A-Z]/.test(binding.label.split(".").pop() ?? "");
18197
+ const reason = describeFallbackReason(binding.wrapReason, binding.type, isEventHandler);
18198
+ 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";
18199
+ 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";
18200
+ return {
18201
+ label: binding.label,
18202
+ expression: binding.expression ?? "(expression not captured)",
18203
+ reason,
18204
+ runtimeDeps,
18205
+ suggestion,
18206
+ loc: binding.loc ? { file: binding.loc.file, line: binding.loc.start.line } : void 0,
18207
+ isEventHandler
18208
+ };
18209
+ }
18210
+ function describeFallbackReason(wrapReason, bindingType, isEventHandler) {
18211
+ 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";
18212
+ switch (wrapReason) {
18213
+ case "fallback-function-calls":
18214
+ return isEventHandler ? `function call in ${context} (event handler prop)` : `opaque function call in ${context} \u2014 the compiler cannot prove it is reactive or pure`;
18215
+ case "fallback-getter-calls":
18216
+ return `call pattern resembles a signal getter in ${context}, but is not a recognized signal`;
18217
+ case "string-reactive":
18218
+ return `string-level match found a signal/memo name in ${context}`;
18219
+ case "props-access":
18220
+ return `props.xxx reference in ${context} \u2014 reactive via prop forwarding`;
18221
+ case "proven-reactive":
18222
+ return `statically proven reactive in ${context}`;
18223
+ default:
18224
+ return `unknown fallback trigger in ${context}`;
18225
+ }
18226
+ }
18227
+ function formatFallbackExplanations(componentName, fallbacks) {
18228
+ const lines = [];
18229
+ if (fallbacks.length === 0) {
18230
+ lines.push(`${componentName} \u2014 no fallback-wrapped expressions.`);
18231
+ return lines.join("\n");
18232
+ }
18233
+ lines.push(`${componentName} \u2014 ${fallbacks.length} fallback-wrapped expression(s)`);
18234
+ for (const f of fallbacks) {
18235
+ const ex = describeFallback(f);
18236
+ lines.push("");
18237
+ if (ex.loc) {
18238
+ const locFile = ex.loc.file.split("/").pop() ?? ex.loc.file;
18239
+ lines.push(` ${locFile}:${ex.loc.line}`);
18240
+ }
18241
+ lines.push(` ${ex.label} fallback:`);
18242
+ lines.push(` expression: ${ex.expression}`);
18243
+ lines.push(` reason: ${ex.reason}`);
18244
+ lines.push(` runtime deps: ${ex.runtimeDeps}`);
18245
+ lines.push(` suggestion: ${ex.suggestion}`);
18246
+ }
18247
+ return lines.join("\n");
18248
+ }
18249
+ function buildComponentSummary(source, filePath, componentName) {
18250
+ const { graph, ir } = buildComponentAnalysis(source, filePath, componentName);
18251
+ const meta = ir.metadata;
18252
+ const clientNeeds = analyzeClientNeeds(ir);
18253
+ const hasReactiveState = meta.signals.length > 0 || meta.memos.length > 0 || meta.effects.length > 0;
18254
+ const needsClient = clientNeeds.needsInit && hasReactiveState;
18255
+ let loopCount = 0;
18256
+ countNodeType(ir.root, "loop", () => {
18257
+ loopCount++;
18258
+ });
18259
+ let conditionalCount = 0;
18260
+ countNodeType(ir.root, "conditional", () => {
18261
+ conditionalCount++;
18262
+ });
18263
+ const eventHandlers = graph.domBindings.filter((d) => d.type === "event").length;
18264
+ const textBindings = graph.domBindings.filter((d) => d.type === "text").length;
18265
+ const attrBindings = graph.domBindings.filter((d) => d.type === "attribute").length;
18266
+ const fallbacks = graph.domBindings.filter((d) => d.classification === "fallback").length;
18267
+ let clientBundle = null;
18268
+ if (needsClient) {
18269
+ const base = filePath.replace(/\.[^.]+$/, "").split("/").pop() ?? meta.componentName;
18270
+ clientBundle = `${base}.client.js`;
18271
+ }
18272
+ return {
18273
+ componentName: graph.componentName,
18274
+ sourceFile: graph.sourceFile,
18275
+ hydrated: needsClient,
18276
+ clientBundle,
18277
+ signals: graph.signals.length,
18278
+ memos: graph.memos.length,
18279
+ effects: graph.effects.length,
18280
+ loops: loopCount,
18281
+ eventHandlers,
18282
+ dynamicTextBindings: textBindings,
18283
+ dynamicAttributes: attrBindings,
18284
+ conditionals: conditionalCount,
18285
+ fallbacks
18286
+ };
18287
+ }
18288
+ function countNodeType(node, targetType, cb) {
18289
+ if (node.type === targetType) cb();
18290
+ switch (node.type) {
18291
+ case "element":
18292
+ case "fragment":
18293
+ case "provider":
18294
+ for (const child of node.children) countNodeType(child, targetType, cb);
18295
+ break;
18296
+ case "component":
18297
+ for (const child of node.children) countNodeType(child, targetType, cb);
18298
+ break;
18299
+ case "conditional":
18300
+ countNodeType(node.whenTrue, targetType, cb);
18301
+ countNodeType(node.whenFalse, targetType, cb);
18302
+ break;
18303
+ case "loop":
18304
+ for (const child of node.children) countNodeType(child, targetType, cb);
18305
+ break;
18306
+ case "if-statement":
18307
+ countNodeType(node.consequent, targetType, cb);
18308
+ if (node.alternate) countNodeType(node.alternate, targetType, cb);
18309
+ break;
18310
+ case "async":
18311
+ countNodeType(node.fallback, targetType, cb);
18312
+ for (const child of node.children) countNodeType(child, targetType, cb);
18313
+ break;
18314
+ }
18315
+ }
18316
+ function formatComponentSummary(summary) {
18317
+ const lines = [];
18318
+ lines.push(summary.componentName);
18319
+ lines.push(` hydrated: ${summary.hydrated ? "yes" : "no"}`);
18320
+ if (summary.clientBundle) {
18321
+ lines.push(` client bundle: ${summary.clientBundle}`);
18322
+ }
18323
+ lines.push(` signals: ${summary.signals}`);
18324
+ lines.push(` memos: ${summary.memos}`);
18325
+ if (summary.effects > 0) lines.push(` effects: ${summary.effects}`);
18326
+ lines.push(` loops: ${summary.loops}`);
18327
+ lines.push(` event handlers: ${summary.eventHandlers}`);
18328
+ lines.push(` dynamic text bindings: ${summary.dynamicTextBindings}`);
18329
+ lines.push(` dynamic attributes: ${summary.dynamicAttributes}`);
18330
+ if (summary.conditionals > 0) lines.push(` conditionals: ${summary.conditionals}`);
18331
+ if (summary.fallbacks > 0) lines.push(` fallbacks: ${summary.fallbacks}`);
18332
+ return lines.join("\n");
18333
+ }
18057
18334
  function inferWrapReasonForAttrLike(hasStringReactive, hasPropsRef, flags) {
18058
18335
  if (hasPropsRef) return "props-access";
18059
18336
  if (hasStringReactive) return "string-reactive";
@@ -18284,6 +18561,7 @@ var init_debug = __esm({
18284
18561
  init_analyzer();
18285
18562
  init_jsx_to_ir();
18286
18563
  init_compiler();
18564
+ init_ir_to_client_js();
18287
18565
  init_reactivity();
18288
18566
  }
18289
18567
  });
@@ -18304,18 +18582,22 @@ __export(src_exports, {
18304
18582
  applyCssLayerPrefix: () => applyCssLayerPrefix,
18305
18583
  buildComponentAnalysis: () => buildComponentAnalysis,
18306
18584
  buildComponentGraph: () => buildComponentGraph,
18585
+ buildComponentSummary: () => buildComponentSummary,
18307
18586
  buildEventSummary: () => buildEventSummary,
18308
18587
  buildGraphFromIR: () => buildGraphFromIR,
18588
+ buildLocalFunctionSetterMap: () => buildLocalFunctionSetterMap,
18309
18589
  buildLoopChainExpr: () => buildLoopChainExpr,
18310
18590
  buildLoopSummary: () => buildLoopSummary,
18311
18591
  buildMetadata: () => buildMetadata,
18312
18592
  buildSourceMapFromIR: () => buildSourceMapFromIR,
18593
+ buildWhyUpdate: () => buildWhyUpdate,
18313
18594
  combineParentChildClientJs: () => combineParentChildClientJs,
18314
18595
  compileJSX: () => compileJSX,
18315
18596
  containsHigherOrder: () => containsHigherOrder,
18316
18597
  createError: () => createError,
18317
18598
  createProgramForCorpus: () => createProgramForCorpus,
18318
18599
  createProgramForFile: () => createProgramForFile,
18600
+ describeFallback: () => describeFallback,
18319
18601
  disableCompilerInstrumentation: () => disableCompilerInstrumentation,
18320
18602
  emitAttrValue: () => emitAttrValue,
18321
18603
  emitIRNode: () => emitIRNode,
@@ -18326,12 +18608,15 @@ __export(src_exports, {
18326
18608
  extractSsrDefaults: () => extractSsrDefaults,
18327
18609
  findReachableNames: () => findReachableNames,
18328
18610
  formatComponentGraph: () => formatComponentGraph,
18611
+ formatComponentSummary: () => formatComponentSummary,
18329
18612
  formatError: () => formatError,
18330
18613
  formatEventSummary: () => formatEventSummary,
18614
+ formatFallbackExplanations: () => formatFallbackExplanations,
18331
18615
  formatLoopSummary: () => formatLoopSummary,
18332
18616
  formatParamWithType: () => formatParamWithType,
18333
18617
  formatSignalTrace: () => formatSignalTrace,
18334
18618
  formatUpdatePath: () => formatUpdatePath,
18619
+ formatWhyUpdate: () => formatWhyUpdate,
18335
18620
  generateClientJs: () => generateClientJs,
18336
18621
  generateClientJsWithSourceMap: () => generateClientJsWithSourceMap,
18337
18622
  generateCodeFrame: () => generateCodeFrame,
@@ -18345,10 +18630,12 @@ __export(src_exports, {
18345
18630
  jsxToIR: () => jsxToIR,
18346
18631
  listComponentFunctions: () => listComponentFunctions,
18347
18632
  listExportedComponents: () => listComponentFunctions,
18633
+ makeIdCallRegex: () => makeIdCallRegex,
18348
18634
  needsTypeBasedDetection: () => needsTypeBasedDetection,
18349
18635
  parseBlockBody: () => parseBlockBody,
18350
18636
  parseExpression: () => parseExpression,
18351
18637
  resetCompilerCounters: () => resetCompilerCounters,
18638
+ resolveSetters: () => resolveSetters,
18352
18639
  rewriteImportsForTemplate: () => rewriteImportsForTemplate,
18353
18640
  stringifyParsedExpr: () => stringifyParsedExpr,
18354
18641
  traceUpdatePath: () => traceUpdatePath
@@ -20134,7 +20421,7 @@ async function writeBuildId(outDir, result) {
20134
20421
  }
20135
20422
  async function watch(config, options = {}) {
20136
20423
  const { debounceMs = 100, signal } = options;
20137
- const { watch: fsWatch } = await import("node:fs/promises");
20424
+ const { watch: fsWatch2 } = await import("node:fs/promises");
20138
20425
  const initial = await build(config);
20139
20426
  let cachedProgram = initial.sharedProgram;
20140
20427
  console.log("");
@@ -20212,7 +20499,7 @@ async function watch(config, options = {}) {
20212
20499
  };
20213
20500
  const watchRoot = async (root, recursive) => {
20214
20501
  try {
20215
- const iter = fsWatch(root, { recursive, signal });
20502
+ const iter = fsWatch2(root, { recursive, signal });
20216
20503
  for await (const event of iter) {
20217
20504
  if (!isRelevant(root, event.filename)) continue;
20218
20505
  schedule();
@@ -23588,7 +23875,7 @@ async function select(args2) {
23588
23875
  output.write(`\x1B[33m?\x1B[0m \x1B[1m${args2.message}\x1B[0m
23589
23876
  `);
23590
23877
  render(true);
23591
- return new Promise((resolve7, reject) => {
23878
+ return new Promise((resolve10, reject) => {
23592
23879
  readline.emitKeypressEvents(input);
23593
23880
  input.setRawMode?.(true);
23594
23881
  input.resume();
@@ -23633,7 +23920,7 @@ async function select(args2) {
23633
23920
  output.write(`\x1B[${totalLines}A`);
23634
23921
  output.write(`\u2714 ${args2.message} \x1B[1;32m${shortLabel}\x1B[0m
23635
23922
  `);
23636
- resolve7(args2.options[cursor].value);
23923
+ resolve10(args2.options[cursor].value);
23637
23924
  return;
23638
23925
  }
23639
23926
  };
@@ -24532,325 +24819,268 @@ var init_scaffold_layout = __esm({
24532
24819
  }
24533
24820
  });
24534
24821
 
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);
24822
+ // src/lib/preview/compile.ts
24823
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
24824
+ import { existsSync as existsSync12 } from "node:fs";
24825
+ import { execFileSync } from "node:child_process";
24826
+ import { resolve as resolve6, relative as relative3 } from "node:path";
24827
+ import { build as build2 } from "esbuild";
24828
+ async function compile(options) {
24829
+ const { assets, previewsPath, previewNames, componentName, liveReload } = options;
24830
+ const { rootDir, srcComponentsDir, tokensCss, globalsCss, runtimeStandalone, uno } = assets;
24831
+ const DIST_DIR = resolve6(rootDir, ".preview-dist");
24832
+ const MODULES_DIR = resolve6(DIST_DIR, "_modules");
24833
+ await mkdir2(MODULES_DIR, { recursive: true });
24834
+ console.log("Generating CSS...");
24835
+ await writeFile2(resolve6(DIST_DIR, "globals.css"), tokensCss + "\n" + globalsCss);
24836
+ console.log("Generated: .preview-dist/globals.css");
24837
+ console.log("Generating UnoCSS...");
24838
+ let unoConfigPath = uno.configPath;
24839
+ if (uno.configIsBundled) {
24840
+ unoConfigPath = resolve6(DIST_DIR, "uno.config.ts");
24841
+ await writeFile2(unoConfigPath, await readFile2(uno.configPath, "utf-8"));
24842
+ }
24843
+ execFileSync(uno.bin, [
24844
+ ...uno.globs,
24845
+ "--config",
24846
+ unoConfigPath,
24847
+ "-o",
24848
+ resolve6(DIST_DIR, "uno.css")
24849
+ ], { cwd: uno.cwd, stdio: "inherit" });
24850
+ console.log("Generated: .preview-dist/uno.css");
24851
+ const previewSource = await readFile2(previewsPath, "utf-8");
24852
+ const previewSiblings = [
24853
+ ...previewSource.matchAll(/from\s*['"]\.\.\/([a-z][a-z0-9-]*)(?:\/[^'"]*)?['"]/g)
24854
+ ].map((m) => m[1]);
24855
+ const componentFiles = resolveDependenciesFromSource(
24856
+ [componentName, ...previewSiblings],
24857
+ srcComponentsDir
24858
+ ).map((name) => resolve6(srcComponentsDir, name, "index.tsx")).filter(existsSync12);
24859
+ console.log(`Compiling ${componentFiles.length + 1} files...`);
24860
+ const allFiles = [...componentFiles, previewsPath];
24861
+ const adapter = new PreviewCsrAdapter();
24862
+ const previewKey = relative3(rootDir, previewsPath).replace(/\.tsx$/, "");
24863
+ const clientJsByKey = /* @__PURE__ */ new Map();
24864
+ let previewProducedClientJs = false;
24865
+ for (const filePath of allFiles) {
24866
+ const source = await readFile2(filePath, "utf-8");
24867
+ const isPreview = filePath === previewsPath;
24868
+ const result = compileJSX(source, filePath, { adapter });
24869
+ const errors = result.errors.filter((e) => e.severity === "error");
24870
+ const warnings = result.errors.filter((e) => e.severity === "warning");
24871
+ for (const w of warnings) console.warn(formatError(w, source, { projectDir: rootDir }));
24872
+ if (errors.length > 0) {
24873
+ for (const e of errors) console.error(formatError(e, source, { projectDir: rootDir }));
24874
+ if (isPreview) {
24875
+ throw new Error(`Preview compilation failed for ${previewKey} (see errors above).`);
24876
+ }
24877
+ continue;
24565
24878
  }
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 {
24879
+ const clientJs = result.files.find((f) => f.type === "clientJs")?.content;
24880
+ if (!clientJs) continue;
24881
+ const key = relative3(rootDir, filePath).replace(/\.tsx$/, "");
24882
+ clientJsByKey.set(key, clientJs);
24883
+ if (isPreview) previewProducedClientJs = true;
24578
24884
  }
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);
24885
+ if (!previewProducedClientJs) {
24886
+ throw new Error(
24887
+ `Preview ${previewKey} produced no client JS. Each preview function must return a single root element (wrap multiple roots in <>...</>).`
24888
+ );
24584
24889
  }
24585
- await runPreview(component);
24890
+ const combined = combineParentChildClientJs(clientJsByKey);
24891
+ const allModules = new Map([...clientJsByKey, ...combined]);
24892
+ for (const [key, content] of allModules) {
24893
+ const safeName = key.replace(/[/\\]/g, "__") + ".js";
24894
+ await writeFile2(resolve6(MODULES_DIR, safeName), content);
24895
+ }
24896
+ const previewModuleFile = previewKey.replace(/[/\\]/g, "__") + ".js";
24897
+ const entrySource = generateEntryScript(previewModuleFile, previewNames);
24898
+ const entryPath = resolve6(DIST_DIR, "_entry.js");
24899
+ await writeFile2(entryPath, entrySource);
24900
+ console.log("Bundling for browser...");
24901
+ await build2({
24902
+ entryPoints: [entryPath],
24903
+ outfile: resolve6(DIST_DIR, "_bundle.js"),
24904
+ bundle: true,
24905
+ format: "esm",
24906
+ platform: "browser",
24907
+ minify: false,
24908
+ sourcemap: "inline",
24909
+ absWorkingDir: rootDir,
24910
+ alias: { "@barefootjs/client/runtime": runtimeStandalone },
24911
+ define: { "process.env.NODE_ENV": '"development"' }
24912
+ });
24913
+ console.log("Generated: .preview-dist/_bundle.js");
24914
+ await writeFile2(resolve6(DIST_DIR, "index.html"), generateHTML(componentName, liveReload));
24915
+ console.log("Generated: .preview-dist/index.html");
24916
+ return { distDir: DIST_DIR };
24586
24917
  }
24587
- var init_preview = __esm({
24588
- "src/commands/preview.ts"() {
24589
- "use strict";
24590
- init_scaffold_layout();
24591
- }
24592
- });
24918
+ function generateEntryScript(previewModuleFile, previewNames) {
24919
+ const namesJson = JSON.stringify(previewNames);
24920
+ return `import { render } from '@barefootjs/client/runtime'
24921
+ import './_modules/${previewModuleFile}'
24922
+
24923
+ const previews = ${namesJson}
24924
+ const app = document.getElementById('preview-root')
24925
+
24926
+ for (const name of previews) {
24927
+ const section = document.createElement('div')
24928
+ section.className = 'preview-section'
24929
+ section.dataset.preview = name
24930
+
24931
+ const title = document.createElement('div')
24932
+ title.className = 'preview-title'
24933
+ title.textContent = name.replace(/([a-z])([A-Z])/g, '$1 $2')
24934
+ section.appendChild(title)
24935
+
24936
+ const content = document.createElement('div')
24937
+ section.appendChild(content)
24938
+ app.appendChild(section)
24593
24939
 
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
24940
  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;
24941
+ render(content, name, {})
24942
+ } catch (err) {
24943
+ content.textContent = 'Render error: ' + (err && err.message || err)
24944
+ console.error('[preview]', name, err)
24645
24945
  }
24646
24946
  }
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
- ];
24947
+ `;
24653
24948
  }
24654
- function resolveTokensCss(projectDir, tokensRelDir) {
24655
- for (const p of candidateCssPaths(projectDir, tokensRelDir)) {
24656
- if (existsSync13(p)) return p;
24657
- }
24658
- return void 0;
24949
+ function generateHTML(componentName, liveReload = false) {
24950
+ const displayName = componentName.charAt(0).toUpperCase() + componentName.slice(1);
24951
+ return `<!DOCTYPE html>
24952
+ <html lang="en">
24953
+ <head>
24954
+ <meta charset="UTF-8" />
24955
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
24956
+ <title>${displayName} \u2014 Preview</title>
24957
+ <link rel="stylesheet" href="/globals.css" />
24958
+ <link rel="stylesheet" href="/uno.css" />
24959
+ <style>
24960
+ body {
24961
+ padding: 2rem;
24962
+ font-family: system-ui, -apple-system, sans-serif;
24963
+ }
24964
+ .preview-section {
24965
+ margin-bottom: 2rem;
24966
+ padding: 1.5rem;
24967
+ border: 1px solid var(--border);
24968
+ border-radius: var(--radius);
24969
+ }
24970
+ .preview-title {
24971
+ font-size: 0.875rem;
24972
+ font-weight: 500;
24973
+ color: var(--muted-foreground);
24974
+ margin-bottom: 1rem;
24975
+ text-transform: uppercase;
24976
+ letter-spacing: 0.05em;
24977
+ }
24978
+ h1 {
24979
+ font-size: 1.5rem;
24980
+ font-weight: 600;
24981
+ margin-bottom: 1.5rem;
24982
+ }
24983
+ #bf-theme-toggle {
24984
+ position: fixed;
24985
+ bottom: 1rem;
24986
+ right: 1rem;
24987
+ z-index: 9999;
24988
+ width: 2.5rem;
24989
+ height: 2.5rem;
24990
+ border-radius: var(--radius);
24991
+ border: 1px solid var(--border);
24992
+ background: var(--card);
24993
+ color: var(--foreground);
24994
+ display: flex;
24995
+ align-items: center;
24996
+ justify-content: center;
24997
+ cursor: pointer;
24998
+ box-shadow: 0 1px 3px rgba(0,0,0,.1);
24999
+ }
25000
+ #bf-theme-toggle:hover { background: var(--accent); }
25001
+ #bf-theme-toggle .sun { display: none; }
25002
+ #bf-theme-toggle .moon { display: block; }
25003
+ .dark #bf-theme-toggle .sun { display: block; }
25004
+ .dark #bf-theme-toggle .moon { display: none; }
25005
+ </style>
25006
+ </head>
25007
+ <body>
25008
+ <h1>${displayName}</h1>
25009
+ <div id="preview-root"></div>
25010
+ <button id="bf-theme-toggle" type="button" aria-label="Toggle dark mode"
25011
+ onclick="var r=document.documentElement;r.classList.add('theme-transition');r.classList.toggle('dark');setTimeout(function(){r.classList.remove('theme-transition')},300)">
25012
+ <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">
25013
+ <circle cx="12" cy="12" r="4"></circle>
25014
+ <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>
25015
+ </svg>
25016
+ <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">
25017
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
25018
+ </svg>
25019
+ </button>
25020
+ <script type="module" src="/_bundle.js"></script>
25021
+ ${liveReload ? LIVE_RELOAD_SCRIPT : ""}
25022
+ </body>
25023
+ </html>`;
24659
25024
  }
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;
25025
+ var EMPTY_OUTPUT, PreviewCsrAdapter, LIVE_RELOAD_SCRIPT;
25026
+ var init_compile = __esm({
25027
+ "src/lib/preview/compile.ts"() {
25028
+ "use strict";
25029
+ init_src2();
25030
+ init_dependency_resolver();
25031
+ EMPTY_OUTPUT = Object.freeze({
25032
+ template: "",
25033
+ sections: Object.freeze({ imports: "", types: "", component: "", defaultExport: "" }),
25034
+ extension: ".tsx"
25035
+ });
25036
+ PreviewCsrAdapter = class extends BaseAdapter {
25037
+ name = "csr";
25038
+ extension = ".tsx";
25039
+ acceptsTemplateCall = () => true;
25040
+ generate() {
25041
+ return EMPTY_OUTPUT;
25042
+ }
25043
+ renderNode() {
25044
+ return "";
24673
25045
  }
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);
24690
- }
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;
24706
- }
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;
24765
- }
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;
24774
- }
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;
24798
- }
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)"
24838
- }
24839
- };
25046
+ renderElement() {
25047
+ return "";
25048
+ }
25049
+ renderExpression() {
25050
+ return "";
25051
+ }
25052
+ renderConditional() {
25053
+ return "";
25054
+ }
25055
+ renderLoop() {
25056
+ return "";
25057
+ }
25058
+ renderComponent() {
25059
+ return "";
25060
+ }
25061
+ renderScopeMarker() {
25062
+ return "";
25063
+ }
25064
+ renderSlotMarker() {
25065
+ return "";
25066
+ }
25067
+ renderCondMarker() {
25068
+ return "";
25069
+ }
25070
+ };
25071
+ LIVE_RELOAD_SCRIPT = `<script>
25072
+ (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()})()
25073
+ </script>`;
24840
25074
  }
24841
25075
  });
24842
25076
 
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";
25077
+ // src/lib/tokens.ts
25078
+ import { readFile as readFile3 } from "node:fs/promises";
25079
+ import { existsSync as existsSync13 } from "node:fs";
25080
+ import { resolve as resolve7, dirname as dirname4 } from "node:path";
24851
25081
  import { fileURLToPath as fileURLToPath4 } from "node:url";
24852
25082
  async function loadTokens(jsonPath) {
24853
- const content = await readFile2(jsonPath, "utf-8");
25083
+ const content = await readFile3(jsonPath, "utf-8");
24854
25084
  return JSON.parse(content);
24855
25085
  }
24856
25086
  function mergeTokenSets(...sets) {
@@ -24874,26 +25104,64 @@ function mergeTokenSets(...sets) {
24874
25104
  function mergeTokenArray(base, ext) {
24875
25105
  for (const token of ext) {
24876
25106
  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
- }
25107
+ if (idx >= 0) base[idx] = token;
25108
+ else base.push(token);
25109
+ }
25110
+ }
25111
+ function generateCSS(tokenSet) {
25112
+ const rootLines = [];
25113
+ const darkLines = [];
25114
+ function addSection(label, tokens) {
25115
+ if (tokens.length === 0) return;
25116
+ rootLines.push(` /* \u2500\u2500 ${label} ${"\u2500".repeat(Math.max(1, 50 - label.length))} */`);
25117
+ for (const t of tokens) rootLines.push(` --${t.name}: ${t.value};`);
25118
+ rootLines.push("");
25119
+ }
25120
+ function addColorSection(tokens) {
25121
+ if (tokens.length === 0) return;
25122
+ rootLines.push(` /* \u2500\u2500 Colors (OKLCH, neutral theme) ${"\u2500".repeat(15)} */`);
25123
+ for (const t of tokens) rootLines.push(` --${t.name}: ${t.value};`);
25124
+ rootLines.push("");
25125
+ for (const t of tokens.filter((t2) => t2.dark)) {
25126
+ darkLines.push(` --${t.name}: ${t.dark};`);
25127
+ }
25128
+ }
25129
+ addSection("Typography", [...tokenSet.typography.fontFamily, ...tokenSet.typography.letterSpacing]);
25130
+ addSection("Spacing scale", tokenSet.spacing);
25131
+ addSection("Border radius", tokenSet.borderRadius);
25132
+ addSection("Transitions", [...tokenSet.transitions.duration, ...tokenSet.transitions.easing]);
25133
+ addSection("Layout", tokenSet.layout);
25134
+ addColorSection(tokenSet.colors);
25135
+ addSection("Shadows", tokenSet.shadows);
25136
+ const header = `/**
25137
+ * AUTO-GENERATED \u2014 Do not edit manually.
25138
+ * Generated from tokens.json.
25139
+ *
25140
+ * BarefootJS Design Tokens
25141
+ */`;
25142
+ let css = `${header}
25143
+
25144
+ :root {
25145
+ ${rootLines.join("\n")}}
25146
+ `;
25147
+ if (darkLines.length > 0) css += `
25148
+ .dark {
25149
+ ${darkLines.join("\n")}
25150
+ }
25151
+ `;
25152
+ return css;
24883
25153
  }
24884
25154
  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;
25155
+ const monorepoTokens = resolve7(ctx2.root, "site/shared/tokens/tokens.json");
25156
+ if (existsSync13(monorepoTokens)) return monorepoTokens;
25157
+ const bundledTokens = resolve7(dirname4(thisFile2), "tokens.json");
25158
+ if (existsSync13(bundledTokens)) return bundledTokens;
24889
25159
  return null;
24890
25160
  }
24891
25161
  async function loadTokenSet(ctx2) {
24892
25162
  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
- }
25163
+ const userTokens = resolve7(ctx2.projectDir, ctx2.config.paths.tokens, "tokens.json");
25164
+ if (await fileExists(userTokens)) return loadTokens(userTokens);
24897
25165
  }
24898
25166
  const basePath = findBaseTokensJson(ctx2);
24899
25167
  if (!basePath) {
@@ -24902,1045 +25170,1679 @@ async function loadTokenSet(ctx2) {
24902
25170
  );
24903
25171
  }
24904
25172
  const base = await loadTokens(basePath);
24905
- const uiJsonPath = resolve6(ctx2.root, "site/ui/tokens.json");
25173
+ const uiJsonPath = resolve7(ctx2.root, "site/ui/tokens.json");
24906
25174
  if (await fileExists(uiJsonPath)) {
24907
- const ext = await loadTokens(uiJsonPath);
24908
- return mergeTokenSets(base, ext);
25175
+ return mergeTokenSets(base, await loadTokens(uiJsonPath));
24909
25176
  }
24910
25177
  return base;
24911
25178
  }
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);
25179
+ async function loadTokensCss(ctx2) {
25180
+ return generateCSS(await loadTokenSet(ctx2));
24964
25181
  }
24965
- var thisFile2, CATEGORY_NAMES;
25182
+ var thisFile2;
24966
25183
  var init_tokens = __esm({
24967
- "src/commands/tokens.ts"() {
25184
+ "src/lib/tokens.ts"() {
24968
25185
  "use strict";
24969
25186
  init_runtime();
24970
25187
  thisFile2 = fileURLToPath4(import.meta.url);
24971
- CATEGORY_NAMES = [
24972
- "typography",
24973
- "spacing",
24974
- "borderRadius",
24975
- "transitions",
24976
- "layout",
24977
- "colors",
24978
- "shadows"
24979
- ];
24980
25188
  }
24981
25189
  });
24982
25190
 
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"));
24990
- }
24991
- function toPascalCase(kebab) {
24992
- return kebab.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join("");
25191
+ // src/lib/preview/errors.ts
25192
+ var PreviewError;
25193
+ var init_errors2 = __esm({
25194
+ "src/lib/preview/errors.ts"() {
25195
+ "use strict";
25196
+ PreviewError = class extends Error {
25197
+ };
25198
+ }
25199
+ });
25200
+
25201
+ // src/lib/preview/assets.ts
25202
+ import { existsSync as existsSync14 } from "node:fs";
25203
+ import { readFile as readFile4 } from "node:fs/promises";
25204
+ import { resolve as resolve8, dirname as dirname5 } from "node:path";
25205
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
25206
+ function firstExisting(...candidates) {
25207
+ return candidates.find((c) => !!c && existsSync14(c));
24993
25208
  }
24994
- function toCamelCase(kebab) {
24995
- const pascal = toPascalCase(kebab);
24996
- return pascal[0].toLowerCase() + pascal.slice(1);
25209
+ function unoBinCandidates(dir) {
25210
+ return ["unocss", "unocss.cmd", "unocss.CMD"].map((n) => resolve8(dir, "node_modules/.bin", n));
24997
25211
  }
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}`;
25212
+ async function resolvePreviewAssets(ctx2) {
25213
+ const monorepo = ctx2.config === null;
25214
+ const projectDir = ctx2.projectDir;
25215
+ const rootDir = projectDir ?? ctx2.root;
25216
+ const srcComponentsDir = monorepo ? resolve8(ctx2.root, "ui/components/ui") : resolve8(projectDir, ctx2.config.paths.components);
25217
+ const tokensCss = await loadTokensCss(ctx2);
25218
+ const globalsPath = firstExisting(
25219
+ projectDir && resolve8(projectDir, "styles/globals.css"),
25220
+ projectDir && resolve8(projectDir, "globals.css"),
25221
+ projectDir && resolve8(projectDir, "app/globals.css"),
25222
+ monorepo ? resolve8(ctx2.root, "site/ui/styles/globals.css") : void 0,
25223
+ resolve8(assetDir, "preview-globals.css")
25224
+ );
25225
+ const globalsCss = globalsPath ? await readFile4(globalsPath, "utf-8") : "";
25226
+ const bundledUnoConfig = resolve8(assetDir, "preview-uno.config.ts");
25227
+ const configPath = firstExisting(
25228
+ projectDir && resolve8(projectDir, "uno.config.ts"),
25229
+ projectDir && resolve8(projectDir, "uno.config.js"),
25230
+ monorepo ? resolve8(ctx2.root, "site/ui/uno.config.ts") : void 0,
25231
+ bundledUnoConfig
25232
+ );
25233
+ if (!configPath) {
25234
+ throw new PreviewError(
25235
+ "No UnoCSS config found and the bundled default is missing \u2014 reinstall @barefootjs/cli."
25236
+ );
25237
+ }
25238
+ const unoCwd = monorepo ? resolve8(ctx2.root, "site/ui") : rootDir;
25239
+ const globs = monorepo ? ["../../ui/components/**/*.tsx", "./**/*.tsx", "./dist/**/*.tsx"] : [resolve8(srcComponentsDir, "**/*.tsx")];
25240
+ const unoBin = firstExisting(
25241
+ ...unoBinCandidates(rootDir),
25242
+ ...monorepo ? unoBinCandidates(resolve8(ctx2.root, "site/ui")) : [],
25243
+ ...monorepo ? unoBinCandidates(ctx2.root) : []
25244
+ );
25245
+ if (!unoBin) {
25246
+ throw new PreviewError(
25247
+ "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.)"
25248
+ );
25249
+ }
25250
+ const runtimeStandalone = firstExisting(
25251
+ monorepo ? resolve8(ctx2.root, "packages/client/dist/runtime/standalone.js") : void 0,
25252
+ resolve8(rootDir, "node_modules/@barefootjs/client/dist/runtime/standalone.js"),
25253
+ resolve8(rootDir, "node_modules/@barefootjs/client/dist/runtime/index.js")
25254
+ );
25255
+ if (!runtimeStandalone) {
25256
+ throw new PreviewError(
25257
+ "The @barefootjs/client runtime was not found. Install @barefootjs/client in your project (its dist must include runtime/standalone.js)."
25258
+ );
25259
+ }
25007
25260
  return {
25008
- componentCode,
25009
- testCode,
25010
- componentPath: `${basePath}/index.tsx`,
25011
- testPath: `${basePath}/index.test.tsx`
25261
+ rootDir,
25262
+ srcComponentsDir,
25263
+ tokensCss,
25264
+ globalsCss,
25265
+ runtimeStandalone,
25266
+ uno: { bin: unoBin, cwd: unoCwd, configPath, configIsBundled: configPath === bundledUnoConfig, globs }
25012
25267
  };
25013
25268
  }
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
- }
25269
+ var assetDir;
25270
+ var init_assets = __esm({
25271
+ "src/lib/preview/assets.ts"() {
25272
+ "use strict";
25273
+ init_tokens();
25274
+ init_errors2();
25275
+ assetDir = dirname5(fileURLToPath5(import.meta.url));
25276
+ }
25277
+ });
25278
+
25279
+ // src/lib/preview-generate.ts
25280
+ function toPascalCase(kebab) {
25281
+ return kebab.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join("");
25282
+ }
25283
+ function toKebabCase(pascal) {
25284
+ return pascal.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
25285
+ }
25286
+ function capitalize2(s) {
25287
+ return s.charAt(0).toUpperCase() + s.slice(1);
25288
+ }
25289
+ function inferVariantPropName(typeName, props) {
25290
+ const match = props.find((p) => p.type === typeName);
25291
+ return match ? match.name : null;
25292
+ }
25293
+ function findExternalTags(code, knownNames) {
25294
+ const external = [];
25295
+ for (const m of code.matchAll(/<([A-Z][a-zA-Z0-9]*)/g)) {
25296
+ if (!knownNames.has(m[1]) && !external.includes(m[1])) {
25297
+ external.push(m[1]);
25022
25298
  }
25023
- imports.push({ from: `../${name}`, names });
25024
25299
  }
25025
- return imports;
25300
+ return external;
25026
25301
  }
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'`);
25302
+ function resolveExternalTagImport(tag, parentModuleName, parentPascalName) {
25303
+ if (tag.endsWith("Icon")) {
25304
+ return { from: "../icon", name: tag };
25034
25305
  }
25035
- lines.push(`import type { Child } from '../../../types'`);
25036
- for (const imp of imports) {
25037
- lines.push(`import { ${imp.names.join(", ")} } from '${imp.from}'`);
25306
+ if (tag.startsWith(parentPascalName)) {
25307
+ return { from: `../${parentModuleName}`, name: tag };
25038
25308
  }
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
- }
25309
+ return { from: `../${toKebabCase(tag)}`, name: tag };
25310
+ }
25311
+ function emitJsxReturn(lines, jsx, indent = " ") {
25312
+ const jsxLines = jsx.split("\n");
25313
+ const tagLines = jsxLines.filter((l) => /^\s*<[A-Za-z]/.test(l));
25314
+ if (tagLines.length === 0) {
25315
+ lines.push(`${indent}return ${jsx}`);
25316
+ return;
25045
25317
  }
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(`}`);
25318
+ const minIndent = Math.min(...tagLines.map((l) => l.match(/^(\s*)/)?.[1].length ?? 0));
25319
+ const rootElements = tagLines.filter((l) => (l.match(/^(\s*)/)?.[1].length ?? 0) === minIndent);
25320
+ const needsFragment = rootElements.length > 1 && !jsx.trim().startsWith("<>");
25321
+ if (jsxLines.length === 1 && !needsFragment) {
25322
+ const singleLineRoots = (jsx.match(/<(?![/])[A-Za-z]/g) ?? []).length;
25323
+ if (singleLineRoots > 1) {
25324
+ lines.push(`${indent}return (<>${jsx}</>)`);
25325
+ } else {
25326
+ lines.push(`${indent}return ${jsx}`);
25327
+ }
25065
25328
  } 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(`}`);
25329
+ lines.push(`${indent}return (`);
25330
+ if (needsFragment) lines.push(`${indent} <>`);
25331
+ for (const l of jsxLines) {
25332
+ lines.push(`${indent} ${needsFragment ? " " : ""}${l}`);
25333
+ }
25334
+ if (needsFragment) lines.push(`${indent} </>`);
25335
+ lines.push(`${indent})`);
25078
25336
  }
25079
- lines.push(``);
25080
- lines.push(`export { ${pascalName} }`);
25081
- lines.push(`export type { ${pascalName}Props }`);
25082
- lines.push(``);
25083
- return lines.join("\n");
25084
25337
  }
25085
- function generateTestCode(componentName, needsClient, importSource) {
25086
- const pascalName = toPascalCase(componentName);
25087
- const varName = toCamelCase(componentName) + "Source";
25338
+ function splitExampleCode(code) {
25339
+ const lines = code.split("\n");
25340
+ const firstJsxLine = lines.findIndex((l) => l.trim().startsWith("<"));
25341
+ if (firstJsxLine <= 0) {
25342
+ return { statements: [], jsx: code };
25343
+ }
25344
+ return {
25345
+ statements: lines.slice(0, firstJsxLine).filter((l) => l.trim() !== ""),
25346
+ jsx: lines.slice(firstJsxLine).join("\n")
25347
+ };
25348
+ }
25349
+ function isSimpleJsx(code) {
25350
+ const trimmed = code.trim();
25351
+ return trimmed.startsWith("<") && !trimmed.includes("createSignal");
25352
+ }
25353
+ function generatePreview(meta, componentsBasePath = "ui/components/ui") {
25354
+ const pascalName = toPascalCase(meta.name);
25355
+ const hasVariants = meta.variants != null && Object.keys(meta.variants).length > 0;
25356
+ const hasSubComponents = meta.subComponents != null && meta.subComponents.length > 0;
25357
+ let needsClient = meta.stateful || meta.tags.includes("stateful");
25358
+ const exampleCode = hasSubComponents && meta.examples.length > 0 ? meta.examples[0].code : "";
25359
+ if (exampleCode.includes("createSignal")) {
25360
+ needsClient = true;
25361
+ }
25362
+ const needsCreateSignalImport = exampleCode.includes("createSignal");
25088
25363
  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(``);
25364
+ const previewNames = [];
25365
+ lines.push("// Auto-generated preview. Customize by editing this file.");
25107
25366
  if (needsClient) {
25108
- lines.push(` test('isClient is true', () => {`);
25109
- lines.push(` expect(result.isClient).toBe(true)`);
25110
- lines.push(` })`);
25367
+ lines.push('"use client"');
25111
25368
  }
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";
25369
+ lines.push("");
25370
+ if (needsCreateSignalImport) {
25371
+ lines.push("import { createSignal } from '@barefootjs/client'");
25132
25372
  }
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);
25373
+ const subNames = [];
25374
+ if (hasSubComponents) {
25375
+ for (const sub2 of meta.subComponents) {
25376
+ subNames.push(sub2.name);
25377
+ }
25147
25378
  }
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);
25379
+ const importsBySource = /* @__PURE__ */ new Map();
25380
+ const moduleNames = [pascalName, ...subNames];
25381
+ if (hasSubComponents && exampleCode) {
25382
+ const allTags = [...exampleCode.matchAll(/<([A-Z][a-zA-Z0-9]*)/g)].map((m) => m[1]);
25383
+ const usedFromModule = [...new Set(allTags.filter((t) => moduleNames.includes(t)))];
25384
+ if (usedFromModule.length > 0) {
25385
+ importsBySource.set(`../${meta.name}`, usedFromModule);
25386
+ }
25387
+ const knownNames = new Set(moduleNames);
25388
+ for (const tag of findExternalTags(exampleCode, knownNames)) {
25389
+ const resolved = resolveExternalTagImport(tag, meta.name, pascalName);
25390
+ const list = importsBySource.get(resolved.from) ?? [];
25391
+ if (!list.includes(resolved.name)) list.push(resolved.name);
25392
+ importsBySource.set(resolved.from, list);
25393
+ }
25394
+ } else {
25395
+ importsBySource.set(`../${meta.name}`, moduleNames);
25159
25396
  }
25160
- const testAbsPath = path19.join(writeRoot, result.testPath);
25161
- const testDir = path19.dirname(testAbsPath);
25162
- if (!existsSync16(testDir)) {
25163
- mkdirSync4(testDir, { recursive: true });
25397
+ for (const [from, names] of importsBySource) {
25398
+ lines.push(`import { ${names.join(", ")} } from '${from}'`);
25164
25399
  }
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();
25400
+ lines.push("");
25401
+ if (hasSubComponents) {
25402
+ generateMultiComponent(lines, previewNames, meta, pascalName, subNames);
25403
+ } else if (needsClient && hasVariants) {
25404
+ generateStatefulWithVariants(lines, previewNames, meta, pascalName);
25405
+ } else if (needsClient) {
25406
+ generateStateful(lines, previewNames, meta, pascalName);
25407
+ } else if (hasVariants) {
25408
+ generateStatelessWithVariants(lines, previewNames, meta, pascalName);
25409
+ } else {
25410
+ generateStatelessSimple(lines, previewNames, meta, pascalName);
25183
25411
  }
25184
- });
25185
-
25186
- // src/lib/parse-component.ts
25187
- function parseComponent(source) {
25412
+ lines.push("");
25188
25413
  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)
25414
+ code: lines.join("\n"),
25415
+ previewNames,
25416
+ filePath: `${componentsBasePath}/${meta.name}/index.preview.tsx`
25198
25417
  };
25199
25418
  }
25200
- function detectUseClient(source) {
25201
- const firstLine = source.split("\n")[0].trim();
25202
- return firstLine === '"use client"' || firstLine === "'use client'";
25419
+ function generateStatelessWithVariants(lines, previewNames, meta, pascalName) {
25420
+ previewNames.push("Default");
25421
+ lines.push("export function Default() {");
25422
+ lines.push(` return <${pascalName}>${meta.title}</${pascalName}>`);
25423
+ lines.push("}");
25424
+ lines.push("");
25425
+ for (const [typeName, values] of Object.entries(meta.variants)) {
25426
+ const propName = inferVariantPropName(typeName, meta.props);
25427
+ if (!propName) continue;
25428
+ const funcName = capitalize2(propName) + "s";
25429
+ previewNames.push(funcName);
25430
+ lines.push(`export function ${funcName}() {`);
25431
+ lines.push(" return (");
25432
+ lines.push(' <div className="flex flex-wrap items-center gap-4">');
25433
+ for (const value of values) {
25434
+ lines.push(` <${pascalName} ${propName}="${value}">${capitalize2(value)}</${pascalName}>`);
25435
+ }
25436
+ lines.push(" </div>");
25437
+ lines.push(" )");
25438
+ lines.push("}");
25439
+ lines.push("");
25440
+ }
25441
+ }
25442
+ function generateStatelessSimple(lines, previewNames, meta, pascalName) {
25443
+ previewNames.push("Default");
25444
+ lines.push("export function Default() {");
25445
+ const simpleExample = meta.examples.find((e) => isSimpleJsx(e.code));
25446
+ if (simpleExample) {
25447
+ emitJsxReturn(lines, simpleExample.code);
25448
+ } else {
25449
+ const hasChildren = meta.props.some((p) => p.name === "children");
25450
+ if (hasChildren) {
25451
+ lines.push(` return <${pascalName}>${meta.title}</${pascalName}>`);
25452
+ } else {
25453
+ lines.push(` return <${pascalName} />`);
25454
+ }
25455
+ }
25456
+ lines.push("}");
25457
+ lines.push("");
25458
+ }
25459
+ function generateStateful(lines, previewNames, meta, pascalName) {
25460
+ previewNames.push("Default");
25461
+ lines.push("export function Default() {");
25462
+ lines.push(" return (");
25463
+ lines.push(' <div className="flex gap-4">');
25464
+ lines.push(` <${pascalName} />`);
25465
+ const defaultStateProp = meta.props.find(
25466
+ (p) => p.name === "defaultChecked" || p.name === "defaultPressed" || p.name === "defaultValue" || p.name === "defaultOpen"
25467
+ );
25468
+ if (defaultStateProp) {
25469
+ lines.push(` <${pascalName} ${defaultStateProp.name} />`);
25470
+ }
25471
+ if (meta.props.some((p) => p.name === "disabled")) {
25472
+ lines.push(` <${pascalName} disabled />`);
25473
+ }
25474
+ lines.push(" </div>");
25475
+ lines.push(" )");
25476
+ lines.push("}");
25477
+ lines.push("");
25478
+ }
25479
+ function generateStatefulWithVariants(lines, previewNames, meta, pascalName) {
25480
+ generateStateful(lines, previewNames, meta, pascalName);
25481
+ const hasChildren = meta.props.some((p) => p.name === "children");
25482
+ for (const [typeName, values] of Object.entries(meta.variants)) {
25483
+ const propName = inferVariantPropName(typeName, meta.props);
25484
+ if (!propName) continue;
25485
+ const funcName = capitalize2(propName) + "s";
25486
+ previewNames.push(funcName);
25487
+ lines.push(`export function ${funcName}() {`);
25488
+ lines.push(" return (");
25489
+ lines.push(' <div className="flex flex-wrap items-center gap-4">');
25490
+ for (const value of values) {
25491
+ if (hasChildren) {
25492
+ lines.push(` <${pascalName} ${propName}="${value}">${capitalize2(value)}</${pascalName}>`);
25493
+ } else {
25494
+ lines.push(` <${pascalName} ${propName}="${value}" />`);
25495
+ }
25496
+ }
25497
+ lines.push(" </div>");
25498
+ lines.push(" )");
25499
+ lines.push("}");
25500
+ lines.push("");
25501
+ }
25502
+ }
25503
+ function generateMultiComponent(lines, previewNames, meta, pascalName, _subNames) {
25504
+ previewNames.push("Default");
25505
+ if (meta.examples.length > 0) {
25506
+ const example = meta.examples[0];
25507
+ const { statements, jsx } = splitExampleCode(example.code);
25508
+ lines.push("export function Default() {");
25509
+ for (const stmt of statements) {
25510
+ lines.push(` ${stmt}`);
25511
+ }
25512
+ const definedNames = new Set(
25513
+ 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]])
25514
+ );
25515
+ const handlerRefs = [...new Set([...jsx.matchAll(/\b(handle[A-Z]\w*)\b/g)].map((m) => m[1]))];
25516
+ for (const h of handlerRefs) {
25517
+ if (!definedNames.has(h)) {
25518
+ lines.push(` const ${h} = () => {}`);
25519
+ }
25520
+ }
25521
+ if (statements.length > 0 || handlerRefs.length > 0) {
25522
+ lines.push("");
25523
+ }
25524
+ emitJsxReturn(lines, jsx);
25525
+ lines.push("}");
25526
+ } else {
25527
+ lines.push("export function Default() {");
25528
+ lines.push(" return (");
25529
+ lines.push(` <${pascalName}>`);
25530
+ for (const sub2 of meta.subComponents) {
25531
+ const hasChildrenProp = sub2.props.some((p) => p.name === "children");
25532
+ const label = sub2.name.replace(pascalName, "") || sub2.name;
25533
+ if (hasChildrenProp) {
25534
+ lines.push(` <${sub2.name}>${label}</${sub2.name}>`);
25535
+ } else {
25536
+ lines.push(` <${sub2.name} />`);
25537
+ }
25538
+ }
25539
+ lines.push(` </${pascalName}>`);
25540
+ lines.push(" )");
25541
+ lines.push("}");
25542
+ }
25543
+ lines.push("");
25544
+ }
25545
+ var init_preview_generate = __esm({
25546
+ "src/lib/preview-generate.ts"() {
25547
+ "use strict";
25548
+ }
25549
+ });
25550
+
25551
+ // src/lib/preview/run.ts
25552
+ import { resolve as resolve9, relative as relative4 } from "node:path";
25553
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync15 } from "node:fs";
25554
+ async function runPreview(componentName, ctx2, opts = {}) {
25555
+ const assets = await resolvePreviewAssets(ctx2);
25556
+ const previewsPath = resolve9(assets.srcComponentsDir, componentName, "index.preview.tsx");
25557
+ if (!existsSync15(previewsPath)) {
25558
+ try {
25559
+ const meta = loadComponent(ctx2.metaDir, componentName);
25560
+ const result = generatePreview(meta);
25561
+ writeFileSync4(previewsPath, result.code);
25562
+ console.log(`Auto-generated preview: ${relative4(assets.rootDir, previewsPath)}`);
25563
+ } catch {
25564
+ throw new PreviewError(
25565
+ `Preview file not found and auto-generation failed for "${componentName}".
25566
+ Run: bf gen preview ${componentName}`
25567
+ );
25568
+ }
25569
+ }
25570
+ const source = readFileSync6(previewsPath, "utf-8");
25571
+ const previewNames = [
25572
+ ...source.matchAll(/export\s+(?:async\s+)?function\s+(\w+)/g),
25573
+ ...source.matchAll(/export\s+const\s+(\w+)\s*=/g)
25574
+ ].map((m) => m[1]);
25575
+ if (previewNames.length === 0) {
25576
+ throw new PreviewError("No exported preview functions found in the preview file.");
25577
+ }
25578
+ console.log(`Found ${previewNames.length} previews: ${previewNames.join(", ")}`);
25579
+ return compile({ assets, previewsPath, previewNames, componentName, liveReload: opts.liveReload });
25580
+ }
25581
+ var init_run = __esm({
25582
+ "src/lib/preview/run.ts"() {
25583
+ "use strict";
25584
+ init_compile();
25585
+ init_assets();
25586
+ init_errors2();
25587
+ init_meta_loader();
25588
+ init_preview_generate();
25589
+ init_errors2();
25590
+ }
25591
+ });
25592
+
25593
+ // src/lib/preview/serve.ts
25594
+ import { createServer } from "node:http";
25595
+ import { createReadStream, existsSync as existsSync16, statSync as statSync2 } from "node:fs";
25596
+ import { join, extname, normalize } from "node:path";
25597
+ function startPreviewServer(distDir, port) {
25598
+ let reloadToken = String(Date.now());
25599
+ const server = createServer((req, res) => {
25600
+ const url = (req.url ?? "/").split("?")[0];
25601
+ if (url === "/__preview_reload") {
25602
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
25603
+ res.setHeader("Cache-Control", "no-store");
25604
+ res.end(reloadToken);
25605
+ return;
25606
+ }
25607
+ const rel = decodeURIComponent(url === "/" ? "/index.html" : url);
25608
+ const filePath = normalize(join(distDir, rel));
25609
+ if (!filePath.startsWith(distDir) || !existsSync16(filePath) || statSync2(filePath).isDirectory()) {
25610
+ res.statusCode = 404;
25611
+ res.end("Not found");
25612
+ return;
25613
+ }
25614
+ res.setHeader("Content-Type", MIME[extname(filePath)] ?? "application/octet-stream");
25615
+ res.setHeader("Cache-Control", "no-store");
25616
+ createReadStream(filePath).pipe(res);
25617
+ });
25618
+ server.listen(port);
25619
+ return {
25620
+ url: `http://localhost:${port}`,
25621
+ bumpReload() {
25622
+ reloadToken = String(Date.now());
25623
+ },
25624
+ close() {
25625
+ server.close();
25626
+ }
25627
+ };
25628
+ }
25629
+ var MIME;
25630
+ var init_serve = __esm({
25631
+ "src/lib/preview/serve.ts"() {
25632
+ "use strict";
25633
+ MIME = {
25634
+ ".html": "text/html; charset=utf-8",
25635
+ ".js": "text/javascript; charset=utf-8",
25636
+ ".css": "text/css; charset=utf-8",
25637
+ ".json": "application/json; charset=utf-8",
25638
+ ".svg": "image/svg+xml",
25639
+ ".map": "application/json; charset=utf-8"
25640
+ };
25641
+ }
25642
+ });
25643
+
25644
+ // src/commands/preview.ts
25645
+ var preview_exports = {};
25646
+ __export(preview_exports, {
25647
+ run: () => run8
25648
+ });
25649
+ import { existsSync as existsSync17, readdirSync as readdirSync5, watch as fsWatch } from "fs";
25650
+ import path16 from "path";
25651
+ function parseArgs(args2) {
25652
+ const out = { serve: false, watch: false, help: false, port: DEFAULT_PORT };
25653
+ for (let i = 0; i < args2.length; i++) {
25654
+ const a = args2[i];
25655
+ if (a === "-h" || a === "--help") out.help = true;
25656
+ else if (a === "--serve") out.serve = true;
25657
+ else if (a === "--watch") out.watch = true;
25658
+ else if (a === "--port") out.port = parseInt(args2[++i] ?? "", 10);
25659
+ else if (a.startsWith("--port=")) out.port = parseInt(a.slice("--port=".length), 10);
25660
+ else if (!a.startsWith("-") && out.component === void 0) out.component = a;
25661
+ }
25662
+ if (out.watch) out.serve = true;
25663
+ return out;
25664
+ }
25665
+ function listPreviewableComponents(ctx2) {
25666
+ const { writeRoot, componentsBasePath } = resolveScaffoldLayout(ctx2);
25667
+ const componentsDir = path16.join(writeRoot, componentsBasePath);
25668
+ if (!existsSync17(componentsDir)) return [];
25669
+ const names = [];
25670
+ for (const name of readdirSync5(componentsDir)) {
25671
+ const previewFile = path16.join(componentsDir, name, "index.preview.tsx");
25672
+ if (existsSync17(previewFile)) names.push(name);
25673
+ }
25674
+ return names.sort();
25675
+ }
25676
+ async function run8(args2, ctx2) {
25677
+ const opts = parseArgs(args2);
25678
+ if (opts.help) {
25679
+ console.log(HELP);
25680
+ return;
25681
+ }
25682
+ if (!opts.component) {
25683
+ const available = listPreviewableComponents(ctx2);
25684
+ if (ctx2.jsonFlag) {
25685
+ console.log(JSON.stringify({ previewable: available }, null, 2));
25686
+ return;
25687
+ }
25688
+ if (available.length === 0) {
25689
+ console.error("No previewable components found.");
25690
+ console.error("Generate one with: bf gen preview <component>");
25691
+ process.exit(1);
25692
+ }
25693
+ console.log(`${available.length} previewable component(s):`);
25694
+ for (const name of available) console.log(` ${name}`);
25695
+ console.log();
25696
+ console.log("Open one with: bf preview <component>");
25697
+ return;
25698
+ }
25699
+ if (!Number.isInteger(opts.port) || opts.port < 1 || opts.port > 65535) {
25700
+ console.error(`Invalid --port: must be an integer between 1 and 65535.`);
25701
+ process.exit(1);
25702
+ }
25703
+ const component = opts.component;
25704
+ const runOpts = { liveReload: opts.watch };
25705
+ let result;
25706
+ try {
25707
+ result = await runPreview(component, ctx2, runOpts);
25708
+ } catch (err) {
25709
+ if (err instanceof PreviewError) {
25710
+ console.error(`Error: ${err.message}`);
25711
+ process.exit(1);
25712
+ }
25713
+ throw err;
25714
+ }
25715
+ const relDir = path16.relative(process.cwd(), result.distDir);
25716
+ console.log(`
25717
+ \u2713 Preview built \u2192 ${relDir}/`);
25718
+ if (!opts.serve) {
25719
+ console.log(`
25720
+ npx serve ${relDir}`);
25721
+ return;
25722
+ }
25723
+ const server = startPreviewServer(result.distDir, opts.port);
25724
+ console.log(`
25725
+ Serving ${server.url}`);
25726
+ if (!opts.watch) {
25727
+ console.log(" Press Ctrl+C to stop.");
25728
+ await new Promise((resolve10) => process.on("SIGINT", resolve10));
25729
+ server.close();
25730
+ return;
25731
+ }
25732
+ console.log(" Watching for changes \u2014 edit a component and save. Press Ctrl+C to stop.");
25733
+ let rebuilding = false;
25734
+ let pending = false;
25735
+ let timer;
25736
+ const rebuild = async () => {
25737
+ if (rebuilding) {
25738
+ pending = true;
25739
+ return;
25740
+ }
25741
+ rebuilding = true;
25742
+ console.log("\nChange detected \u2014 rebuilding...");
25743
+ try {
25744
+ await runPreview(component, ctx2, runOpts);
25745
+ server.bumpReload();
25746
+ console.log("\u2713 Rebuilt");
25747
+ } catch (err) {
25748
+ const msg = err instanceof PreviewError ? err.message : err.message;
25749
+ console.error(`\u2717 Rebuild failed: ${msg}`);
25750
+ } finally {
25751
+ rebuilding = false;
25752
+ if (pending) {
25753
+ pending = false;
25754
+ void rebuild();
25755
+ }
25756
+ }
25757
+ };
25758
+ const schedule = () => {
25759
+ clearTimeout(timer);
25760
+ timer = setTimeout(() => void rebuild(), 150);
25761
+ };
25762
+ const { writeRoot, componentsBasePath } = resolveScaffoldLayout(ctx2);
25763
+ const watchTargets = [
25764
+ path16.join(writeRoot, componentsBasePath),
25765
+ // Monorepo token/CSS sources
25766
+ path16.join(ctx2.root, "site/ui/styles"),
25767
+ path16.join(ctx2.root, "site/ui/tokens.json"),
25768
+ path16.join(ctx2.root, "site/shared/tokens"),
25769
+ // Project token/CSS sources
25770
+ ctx2.projectDir && path16.join(ctx2.projectDir, "styles"),
25771
+ ctx2.projectDir && path16.join(ctx2.projectDir, "globals.css"),
25772
+ ctx2.projectDir && path16.join(ctx2.projectDir, "uno.config.ts"),
25773
+ ctx2.projectDir && ctx2.config?.paths.tokens && path16.join(ctx2.projectDir, ctx2.config.paths.tokens)
25774
+ ].filter((t) => !!t && existsSync17(t));
25775
+ const watchers = watchTargets.map(
25776
+ (target) => fsWatch(target, { recursive: true }, schedule)
25777
+ );
25778
+ await new Promise((resolve10) => process.on("SIGINT", resolve10));
25779
+ for (const w of watchers) w.close();
25780
+ server.close();
25781
+ }
25782
+ var DEFAULT_PORT, HELP;
25783
+ var init_preview = __esm({
25784
+ "src/commands/preview.ts"() {
25785
+ "use strict";
25786
+ init_scaffold_layout();
25787
+ init_run();
25788
+ init_serve();
25789
+ DEFAULT_PORT = 4321;
25790
+ HELP = `Usage: bf preview [component] [options]
25791
+
25792
+ bf preview List previewable components
25793
+ bf preview <component> Build a static preview into .preview-dist/
25794
+
25795
+ Options:
25796
+ --serve Serve the build on a local server and print its URL
25797
+ --watch Rebuild on source changes and live-reload (implies --serve)
25798
+ --port <number> Server port for --serve/--watch (default ${DEFAULT_PORT})
25799
+ -h, --help Show this help`;
25800
+ }
25801
+ });
25802
+
25803
+ // src/commands/tokens-apply.ts
25804
+ var tokens_apply_exports = {};
25805
+ __export(tokens_apply_exports, {
25806
+ applyCssOverrides: () => applyCssOverrides,
25807
+ applyTokenOverrides: () => applyTokenOverrides,
25808
+ parseStudioUrl: () => parseStudioUrl,
25809
+ resolveTokensCss: () => resolveTokensCss,
25810
+ run: () => run9
25811
+ });
25812
+ import { existsSync as existsSync18, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
25813
+ import path17 from "path";
25814
+ async function run9(args2, ctx2) {
25815
+ const url = args2[0];
25816
+ if (!url) {
25817
+ console.error("Error: tokens apply requires a Studio URL.");
25818
+ console.error("Usage: bf tokens apply <url>");
25819
+ process.exit(1);
25820
+ }
25821
+ const projectDir = ctx2.projectDir ?? process.cwd();
25822
+ const tokensRelDir = ctx2.config?.paths.tokens ?? "tokens";
25823
+ const studioConfig = parseStudioUrl(url);
25824
+ if (!studioConfig) {
25825
+ console.error("Error: could not decode Studio config from the URL (no `?c=` param or malformed payload).");
25826
+ process.exit(1);
25827
+ }
25828
+ const cssPath = resolveTokensCss(projectDir, tokensRelDir);
25829
+ if (!cssPath) {
25830
+ console.error("Error: tokens.css not found. Checked:");
25831
+ for (const p of candidateCssPaths(projectDir, tokensRelDir)) {
25832
+ console.error(` - ${path17.relative(projectDir, p)}`);
25833
+ }
25834
+ console.error(" Run `npm create barefootjs@latest` to scaffold a project first.");
25835
+ process.exit(1);
25836
+ }
25837
+ applyCssOverrides(cssPath, studioConfig);
25838
+ console.log(` Patched ${path17.relative(projectDir, cssPath)}`);
25839
+ const tokensJsonPath = path17.join(projectDir, tokensRelDir, "tokens.json");
25840
+ if (existsSync18(tokensJsonPath)) {
25841
+ applyTokenOverrides(tokensJsonPath, studioConfig);
25842
+ console.log(` Patched ${path17.relative(projectDir, tokensJsonPath)}`);
25843
+ }
25844
+ }
25845
+ function parseStudioUrl(url) {
25846
+ try {
25847
+ const parsed = new URL(url);
25848
+ const encoded = parsed.searchParams.get("c");
25849
+ if (!encoded) return void 0;
25850
+ const json = atob(decodeURIComponent(encoded));
25851
+ return JSON.parse(json);
25852
+ } catch {
25853
+ return void 0;
25854
+ }
25855
+ }
25856
+ function candidateCssPaths(projectDir, tokensRelDir) {
25857
+ return [
25858
+ path17.join(projectDir, tokensRelDir, "tokens.css"),
25859
+ path17.join(projectDir, "public", "tokens.css"),
25860
+ path17.join(projectDir, "static", "tokens.css")
25861
+ ];
25203
25862
  }
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);
25863
+ function resolveTokensCss(projectDir, tokensRelDir) {
25864
+ for (const p of candidateCssPaths(projectDir, tokensRelDir)) {
25865
+ if (existsSync18(p)) return p;
25214
25866
  }
25215
- return descLines.join(" ").replace(/\s+/g, " ").trim();
25867
+ return void 0;
25216
25868
  }
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 });
25869
+ function buildBlockOverrides(config) {
25870
+ const root = {};
25871
+ const dark = {};
25872
+ if (config.spacing) root["--spacing"] = config.spacing;
25873
+ if (config.radius) root["--radius"] = config.radius;
25874
+ if (config.font) {
25875
+ root["--font-sans"] = FONT_MAP[config.font] ?? config.font;
25228
25876
  }
25229
- return examples;
25877
+ if (config.style) {
25878
+ const preset = SHADOW_PRESETS[config.style];
25879
+ if (preset) {
25880
+ for (const [name, value] of Object.entries(preset)) {
25881
+ root[`--${name}`] = value;
25882
+ }
25883
+ }
25884
+ }
25885
+ if (config.tokens) {
25886
+ for (const [name, values] of Object.entries(config.tokens)) {
25887
+ if (values.light !== void 0) root[`--${name}`] = values.light;
25888
+ if (values.dark !== void 0) dark[`--${name}`] = values.dark;
25889
+ }
25890
+ }
25891
+ return { root, dark };
25230
25892
  }
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);
25893
+ function applyCssOverrides(cssPath, config) {
25894
+ const overrides = buildBlockOverrides(config);
25895
+ let css = readFileSync7(cssPath, "utf-8");
25896
+ css = patchBlock(css, /:root\s*\{/, overrides.root);
25897
+ css = patchBlock(css, /\.dark\s*\{/, overrides.dark);
25898
+ writeFileSync5(cssPath, css);
25235
25899
  }
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] === "}") {
25900
+ function patchBlock(css, openRe, overrides) {
25901
+ if (Object.keys(overrides).length === 0) return css;
25902
+ const openMatch = openRe.exec(css);
25903
+ if (!openMatch) return css;
25904
+ const blockStart = openMatch.index + openMatch[0].length;
25905
+ let depth = 1;
25906
+ let blockEnd = -1;
25907
+ for (let i = blockStart; i < css.length; i++) {
25908
+ const ch = css[i];
25909
+ if (ch === "{") depth++;
25910
+ else if (ch === "}") {
25244
25911
  depth--;
25245
25912
  if (depth === 0) {
25246
- bodyEnd = i;
25913
+ blockEnd = i;
25247
25914
  break;
25248
25915
  }
25249
25916
  }
25250
25917
  }
25251
- const body = source.slice(bodyStart + 1, bodyEnd);
25252
- return parsePropsBody(body);
25918
+ if (blockEnd === -1) return css;
25919
+ let block = css.slice(blockStart, blockEnd);
25920
+ const toAppend = [];
25921
+ for (const [name, value] of Object.entries(overrides)) {
25922
+ const re = new RegExp(`(${escapeRegex2(name)}\\s*:\\s*)[^;]+(;)`);
25923
+ if (re.test(block)) {
25924
+ block = block.replace(re, `$1${value}$2`);
25925
+ } else {
25926
+ toAppend.push([name, value]);
25927
+ }
25928
+ }
25929
+ if (toAppend.length > 0) {
25930
+ const lines = toAppend.map(([n, v]) => ` ${n}: ${v};`).join("\n");
25931
+ const trailing = block.match(/\s*$/)?.[0] ?? "";
25932
+ block = block.slice(0, block.length - trailing.length) + `
25933
+
25934
+ /* \u2500\u2500 Studio overrides \u2500\u2500 */
25935
+ ${lines}
25936
+ `;
25937
+ }
25938
+ return css.slice(0, blockStart) + block + css.slice(blockEnd);
25253
25939
  }
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
- });
25940
+ function escapeRegex2(s) {
25941
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
25942
+ }
25943
+ function applyTokenOverrides(tokensJsonPath, config) {
25944
+ const raw = readFileSync7(tokensJsonPath, "utf-8");
25945
+ const tokensData = JSON.parse(raw);
25946
+ if (config.tokens) {
25947
+ for (const [name, values] of Object.entries(config.tokens)) {
25948
+ applyColorOverride(tokensData, name, values);
25949
+ }
25275
25950
  }
25276
- return props;
25951
+ if (config.spacing) {
25952
+ applySimpleOverride(tokensData, "--spacing", config.spacing);
25953
+ }
25954
+ if (config.radius) {
25955
+ applySimpleOverride(tokensData, "--radius", config.radius);
25956
+ }
25957
+ if (config.font) {
25958
+ const fontValue = FONT_MAP[config.font] || config.font;
25959
+ applySimpleOverride(tokensData, "--font-sans", fontValue);
25960
+ }
25961
+ if (config.style) {
25962
+ applyShadowPreset(tokensData, config.style);
25963
+ }
25964
+ writeFileSync5(tokensJsonPath, JSON.stringify(tokensData, null, 2) + "\n");
25277
25965
  }
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);
25966
+ function applyColorOverride(tokensData, name, values) {
25967
+ const varName = `--${name}`;
25968
+ if (Array.isArray(tokensData.colors)) {
25969
+ for (const token of tokensData.colors) {
25970
+ if (token.name === varName || token.name === name) {
25971
+ if (values.light) token.value = values.light;
25972
+ if (values.dark) token.dark = values.dark;
25973
+ return;
25974
+ }
25975
+ }
25286
25976
  }
25287
- return descLines.join(" ").trim();
25977
+ if (Array.isArray(tokensData.tokens)) {
25978
+ for (const token of tokensData.tokens) {
25979
+ if (token.name === varName || token.name === name) {
25980
+ if (values.light) token.value = values.light;
25981
+ if (values.dark) token.dark = values.dark;
25982
+ return;
25983
+ }
25984
+ }
25985
+ }
25986
+ }
25987
+ function applySimpleOverride(tokensData, name, value) {
25988
+ const bareName = name.startsWith("--") ? name.slice(2) : name;
25989
+ const sections = [
25990
+ tokensData.colors,
25991
+ tokensData.spacing,
25992
+ tokensData.borderRadius,
25993
+ tokensData.shadows,
25994
+ tokensData.layout
25995
+ ];
25996
+ if (tokensData.typography) {
25997
+ for (const arr of Object.values(tokensData.typography)) {
25998
+ if (Array.isArray(arr)) sections.push(arr);
25999
+ }
26000
+ }
26001
+ for (const arr of sections) {
26002
+ if (!Array.isArray(arr)) continue;
26003
+ for (const token of arr) {
26004
+ if (token.name === bareName || token.name === name) {
26005
+ token.value = value;
26006
+ return;
26007
+ }
26008
+ }
26009
+ }
26010
+ }
26011
+ function applyShadowPreset(tokensData, styleName) {
26012
+ const shadows = SHADOW_PRESETS[styleName];
26013
+ if (!shadows) return;
26014
+ for (const [name, value] of Object.entries(shadows)) {
26015
+ applySimpleOverride(tokensData, name, value);
26016
+ }
26017
+ }
26018
+ var FONT_MAP, SHADOW_PRESETS;
26019
+ var init_tokens_apply = __esm({
26020
+ "src/commands/tokens-apply.ts"() {
26021
+ "use strict";
26022
+ FONT_MAP = {
26023
+ system: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif',
26024
+ inter: '"Inter", sans-serif',
26025
+ "noto-sans": '"Noto Sans", sans-serif',
26026
+ "nunito-sans": '"Nunito Sans", sans-serif',
26027
+ figtree: '"Figtree", sans-serif'
26028
+ };
26029
+ SHADOW_PRESETS = {
26030
+ Sharp: {
26031
+ "shadow-sm": "0 1px 2px 0 rgb(0 0 0 / 0.04)",
26032
+ "shadow": "0 1px 2px 0 rgb(0 0 0 / 0.06)",
26033
+ "shadow-md": "0 2px 4px -1px rgb(0 0 0 / 0.08)",
26034
+ "shadow-lg": "0 4px 8px -2px rgb(0 0 0 / 0.1)"
26035
+ },
26036
+ Soft: {
26037
+ "shadow-sm": "0 1px 3px 0 rgb(0 0 0 / 0.06)",
26038
+ "shadow": "0 2px 6px 0 rgb(0 0 0 / 0.08), 0 1px 3px -1px rgb(0 0 0 / 0.06)",
26039
+ "shadow-md": "0 6px 12px -2px rgb(0 0 0 / 0.08), 0 3px 6px -3px rgb(0 0 0 / 0.06)",
26040
+ "shadow-lg": "0 12px 24px -4px rgb(0 0 0 / 0.08), 0 6px 10px -5px rgb(0 0 0 / 0.06)"
26041
+ },
26042
+ Compact: {
26043
+ "shadow-sm": "none",
26044
+ "shadow": "none",
26045
+ "shadow-md": "none",
26046
+ "shadow-lg": "0 1px 2px 0 rgb(0 0 0 / 0.05)"
26047
+ }
26048
+ };
26049
+ }
26050
+ });
26051
+
26052
+ // src/commands/tokens.ts
26053
+ var tokens_exports = {};
26054
+ __export(tokens_exports, {
26055
+ run: () => run10
26056
+ });
26057
+ function flattenTokens(tokenSet, category) {
26058
+ const result = [];
26059
+ function add(cat, tokens) {
26060
+ if (category && category !== cat) return;
26061
+ result.push(...tokens);
26062
+ }
26063
+ add("typography", [...tokenSet.typography.fontFamily, ...tokenSet.typography.letterSpacing]);
26064
+ add("spacing", tokenSet.spacing);
26065
+ add("borderRadius", tokenSet.borderRadius);
26066
+ add("transitions", [...tokenSet.transitions.duration, ...tokenSet.transitions.easing]);
26067
+ add("layout", tokenSet.layout);
26068
+ add("colors", tokenSet.colors);
26069
+ add("shadows", tokenSet.shadows);
26070
+ return result;
25288
26071
  }
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 });
26072
+ function printTokens(tokens, jsonFlag2) {
26073
+ if (jsonFlag2) {
26074
+ console.log(JSON.stringify(tokens, null, 2));
26075
+ return;
25298
26076
  }
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 });
26077
+ if (tokens.length === 0) {
26078
+ console.log("No tokens found.");
26079
+ return;
25308
26080
  }
25309
- return subs;
26081
+ const nameWidth = Math.max(25, ...tokens.map((t) => t.name.length + 4));
26082
+ const header = `${"NAME".padEnd(nameWidth)}VALUE`;
26083
+ console.log(header);
26084
+ console.log("-".repeat(header.length + 20));
26085
+ for (const t of tokens) {
26086
+ const name = `--${t.name}`;
26087
+ const dark = t.dark;
26088
+ const darkSuffix = dark ? ` (dark: ${dark})` : "";
26089
+ console.log(`${name.padEnd(nameWidth)}${t.value}${darkSuffix}`);
26090
+ }
26091
+ console.log(`
26092
+ ${tokens.length} token(s)`);
25310
26093
  }
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, ""));
26094
+ async function run10(args2, ctx2) {
26095
+ let category;
26096
+ const catIdx = args2.indexOf("--category");
26097
+ if (catIdx >= 0 && args2[catIdx + 1]) {
26098
+ const val = args2[catIdx + 1];
26099
+ if (!CATEGORY_NAMES.includes(val)) {
26100
+ console.error(`Unknown category: ${val}`);
26101
+ console.error(`Available: ${CATEGORY_NAMES.join(", ")}`);
26102
+ process.exit(1);
25320
26103
  }
26104
+ category = val;
25321
26105
  }
25322
- return variants;
26106
+ const tokenSet = await loadTokenSet(ctx2);
26107
+ const tokens = flattenTokens(tokenSet, category);
26108
+ printTokens(tokens, ctx2.jsonFlag);
25323
26109
  }
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
- }
26110
+ var CATEGORY_NAMES;
26111
+ var init_tokens2 = __esm({
26112
+ "src/commands/tokens.ts"() {
26113
+ "use strict";
26114
+ init_tokens();
26115
+ CATEGORY_NAMES = [
26116
+ "typography",
26117
+ "spacing",
26118
+ "borderRadius",
26119
+ "transitions",
26120
+ "layout",
26121
+ "colors",
26122
+ "shadows"
26123
+ ];
25332
26124
  }
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 || [])];
26125
+ });
26126
+
26127
+ // src/lib/scaffold.ts
26128
+ import { readFileSync as readFileSync8, existsSync as existsSync19 } from "fs";
26129
+ import path18 from "path";
26130
+ function loadMeta(metaDir, name) {
26131
+ const filePath = path18.join(metaDir, `${name}.json`);
26132
+ if (!existsSync19(filePath)) return null;
26133
+ return JSON.parse(readFileSync8(filePath, "utf-8"));
26134
+ }
26135
+ function toPascalCase2(kebab) {
26136
+ return kebab.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join("");
26137
+ }
26138
+ function toCamelCase(kebab) {
26139
+ const pascal = toPascalCase2(kebab);
26140
+ return pascal[0].toLowerCase() + pascal.slice(1);
26141
+ }
26142
+ function scaffold(componentName, useComponents, metaDir, componentsBasePath = "ui/components/ui", options = {}) {
26143
+ const metas = useComponents.map((name) => ({ name, meta: loadMeta(metaDir, name) }));
26144
+ const found = metas.filter((m) => m.meta !== null);
26145
+ const notFound = metas.filter((m) => m.meta === null).map((m) => m.name);
26146
+ const needsClient = found.some((m) => m.meta.stateful);
26147
+ const imports = buildImports(found);
26148
+ const componentCode = generateComponentCode(componentName, imports, needsClient, notFound);
26149
+ const testCode = generateTestCode(componentName, needsClient, options.testImportSource ?? "bun:test");
26150
+ const basePath = `${componentsBasePath}/${componentName}`;
25337
26151
  return {
25338
- role: roles.size > 0 ? [...roles].join(", ") : void 0,
25339
- ariaAttributes: ariaAttrs,
25340
- dataAttributes: dataAttrs
26152
+ componentCode,
26153
+ testCode,
26154
+ componentPath: `${basePath}/index.tsx`,
26155
+ testPath: `${basePath}/index.test.tsx`
25341
26156
  };
25342
26157
  }
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);
26158
+ function buildImports(components) {
26159
+ const imports = [];
26160
+ for (const { name, meta } of components) {
26161
+ const names = [toPascalCase2(name)];
26162
+ if (meta.subComponents) {
26163
+ for (const sub2 of meta.subComponents) {
26164
+ names.push(sub2.name);
25356
26165
  }
25357
- } else {
25358
- external.push(specifier);
25359
26166
  }
26167
+ imports.push({ from: `../${name}`, names });
25360
26168
  }
25361
- return {
25362
- internal: [...new Set(internal)],
25363
- external: [...new Set(external)]
25364
- };
26169
+ return imports;
25365
26170
  }
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
- }
26171
+ function generateComponentCode(componentName, imports, needsClient, notFound) {
26172
+ const pascalName = toPascalCase2(componentName);
26173
+ const lines = [];
26174
+ if (needsClient) {
26175
+ lines.push(`"use client"`);
26176
+ lines.push(``);
26177
+ lines.push(`import { createSignal } from '@barefootjs/client'`);
25386
26178
  }
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";
26179
+ lines.push(`import type { Child } from '../../../types'`);
26180
+ for (const imp of imports) {
26181
+ lines.push(`import { ${imp.names.join(", ")} } from '${imp.from}'`);
26182
+ }
26183
+ if (notFound.length > 0) {
26184
+ lines.push(``);
26185
+ lines.push(`// WARNING: These components were not found in ui/meta/:`);
26186
+ for (const name of notFound) {
26187
+ lines.push(`// - ${name}`);
26188
+ }
25401
26189
  }
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
26190
  lines.push(``);
25424
- lines.push(`const ${varName} = readFileSync(resolve(__dirname, '${relativePath}'), 'utf-8')`);
26191
+ lines.push(`interface ${pascalName}Props {`);
26192
+ lines.push(` /** Additional CSS classes. */`);
26193
+ lines.push(` className?: string`);
26194
+ lines.push(` /** Children to render. */`);
26195
+ lines.push(` children?: Child`);
26196
+ lines.push(`}`);
25425
26197
  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
- }
26198
+ if (needsClient) {
26199
+ lines.push(`function ${pascalName}(props: ${pascalName}Props) {`);
26200
+ lines.push(` // TODO: Add signals`);
26201
+ lines.push(` // const [value, setValue] = createSignal(...)`);
26202
+ lines.push(``);
26203
+ lines.push(` return (`);
26204
+ lines.push(` <div data-slot="${componentName}">`);
26205
+ lines.push(` {/* TODO: Compose components */}`);
26206
+ lines.push(` </div>`);
26207
+ lines.push(` )`);
26208
+ lines.push(`}`);
26209
+ } else {
26210
+ lines.push(`function ${pascalName}({`);
26211
+ lines.push(` className = '',`);
26212
+ lines.push(` children,`);
26213
+ lines.push(` ...props`);
26214
+ lines.push(`}: ${pascalName}Props) {`);
26215
+ lines.push(` return (`);
26216
+ lines.push(` <div data-slot="${componentName}" className={className}>`);
26217
+ lines.push(` {/* TODO: Compose components */}`);
26218
+ lines.push(` {children}`);
26219
+ lines.push(` </div>`);
26220
+ lines.push(` )`);
26221
+ lines.push(`}`);
25434
26222
  }
25435
- return lines.join("\n") + "\n";
26223
+ lines.push(``);
26224
+ lines.push(`export { ${pascalName} }`);
26225
+ lines.push(`export type { ${pascalName}Props }`);
26226
+ lines.push(``);
26227
+ return lines.join("\n");
25436
26228
  }
25437
- function generateDescribeBlock(source, parsed, componentName, varName, fileName, multiComponent) {
26229
+ function generateTestCode(componentName, needsClient, importSource) {
26230
+ const pascalName = toPascalCase2(componentName);
26231
+ const varName = toCamelCase(componentName) + "Source";
25438
26232
  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})`);
26233
+ lines.push(`import { describe, test, expect } from '${importSource}'`);
26234
+ lines.push(`import { readFileSync } from 'fs'`);
26235
+ lines.push(`import { resolve } from 'path'`);
26236
+ lines.push(`import { renderToTest } from '@barefootjs/test'`);
26237
+ lines.push(``);
26238
+ lines.push(`const ${varName} = readFileSync(resolve(__dirname, 'index.tsx'), 'utf-8')`);
26239
+ lines.push(``);
26240
+ lines.push(`describe('${pascalName}', () => {`);
26241
+ lines.push(` const result = renderToTest(${varName}, '${componentName}.tsx')`);
25443
26242
  lines.push(``);
25444
26243
  lines.push(` test('has no compiler errors', () => {`);
25445
26244
  lines.push(` expect(result.errors).toEqual([])`);
25446
26245
  lines.push(` })`);
25447
26246
  lines.push(``);
25448
- lines.push(` test('componentName is ${componentName}', () => {`);
25449
- lines.push(` expect(result.componentName).toBe('${componentName}')`);
26247
+ lines.push(` test('componentName is ${pascalName}', () => {`);
26248
+ lines.push(` expect(result.componentName).toBe('${pascalName}')`);
25450
26249
  lines.push(` })`);
25451
26250
  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([])`);
26251
+ if (needsClient) {
26252
+ lines.push(` test('isClient is true', () => {`);
26253
+ lines.push(` expect(result.isClient).toBe(true)`);
25461
26254
  lines.push(` })`);
25462
26255
  }
25463
26256
  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
- }
26257
+ lines.push(` test('renders as <div>', () => {`);
26258
+ lines.push(` expect(result.root.tag).toBe('div')`);
26259
+ lines.push(` })`);
26260
+ lines.push(``);
26261
+ lines.push(` test('has data-slot=${componentName}', () => {`);
26262
+ lines.push(` expect(result.root.props['data-slot']).toBe('${componentName}')`);
26263
+ lines.push(` })`);
26264
+ lines.push(``);
25540
26265
  lines.push(` test('toStructure() shows expected tree', () => {`);
25541
26266
  lines.push(` const structure = result.toStructure()`);
25542
26267
  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
26268
  lines.push(` })`);
25550
26269
  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());
26270
+ lines.push(``);
26271
+ return lines.join("\n");
25635
26272
  }
25636
- var ON_PROP_BY_EVENT;
25637
- var init_test_template = __esm({
25638
- "src/lib/test-template.ts"() {
26273
+ var init_scaffold = __esm({
26274
+ "src/lib/scaffold.ts"() {
25639
26275
  "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
26276
  }
25648
26277
  });
25649
26278
 
25650
- // src/commands/gen-test.ts
25651
- var gen_test_exports = {};
25652
- __export(gen_test_exports, {
25653
- run: () => run12
26279
+ // src/commands/gen-component.ts
26280
+ var gen_component_exports = {};
26281
+ __export(gen_component_exports, {
26282
+ run: () => run11
25654
26283
  });
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]");
26284
+ import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync4, existsSync as existsSync20 } from "fs";
26285
+ import path19 from "path";
26286
+ function run11(args2, ctx2) {
26287
+ if (args2.length < 1) {
26288
+ console.error("Usage: bf gen component <component-name> [use-component1] [use-component2] ...");
26289
+ console.error("Example: bf gen component settings-form input switch button");
25665
26290
  process.exit(1);
25666
26291
  }
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}`);
26292
+ const [componentName, ...useComponents] = args2;
26293
+ const { writeRoot, componentsBasePath } = resolveScaffoldLayout(ctx2);
26294
+ const pm = detectPackageManager(ctx2.projectDir ?? ctx2.root);
26295
+ const runner = testRunnerFor(pm);
26296
+ const result = scaffold(componentName, useComponents, ctx2.metaDir, componentsBasePath, {
26297
+ testImportSource: runner.importSource
26298
+ });
26299
+ const componentAbsPath = path19.join(writeRoot, result.componentPath);
26300
+ if (existsSync20(componentAbsPath)) {
26301
+ console.error(`Error: ${result.componentPath} already exists. Delete it first or choose a different name.`);
25673
26302
  process.exit(1);
25674
26303
  }
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);
26304
+ const testAbsPath = path19.join(writeRoot, result.testPath);
26305
+ const testDir = path19.dirname(testAbsPath);
26306
+ if (!existsSync20(testDir)) {
26307
+ mkdirSync4(testDir, { recursive: true });
25689
26308
  }
25690
- writeFileSync6(testPath, content);
25691
- const rel = path21.relative(ctx2.projectDir ?? ctx2.root, testPath);
25692
- console.log(`Created: ${rel}`);
26309
+ writeFileSync6(componentAbsPath, result.componentCode);
26310
+ writeFileSync6(testAbsPath, result.testCode);
26311
+ const testCmd = commandsFor(pm).test(result.testPath);
26312
+ console.log(`Created:`);
26313
+ console.log(` ${result.componentPath}`);
26314
+ console.log(` ${result.testPath}`);
25693
26315
  console.log(``);
25694
- console.log(`Next: ${commandsFor(pm).test(rel)}`);
26316
+ console.log(`Next steps:`);
26317
+ console.log(` 1. Implement the component in ${result.componentPath}`);
26318
+ console.log(` 2. ${testCmd}`);
26319
+ console.log(` 3. bf gen test ${componentName} (regenerate richer test)`);
25695
26320
  }
25696
- var init_gen_test = __esm({
25697
- "src/commands/gen-test.ts"() {
26321
+ var init_gen_component = __esm({
26322
+ "src/commands/gen-component.ts"() {
25698
26323
  "use strict";
25699
- init_resolve_source();
25700
- init_test_template();
26324
+ init_scaffold();
26325
+ init_scaffold_layout();
25701
26326
  init_pm();
25702
26327
  }
25703
26328
  });
25704
26329
 
25705
- // src/lib/preview-generate.ts
25706
- function toPascalCase2(kebab) {
25707
- return kebab.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join("");
26330
+ // src/lib/parse-component.ts
26331
+ function parseComponent(source) {
26332
+ return {
26333
+ useClient: detectUseClient(source),
26334
+ description: extractTopLevelDescription(source),
26335
+ examples: extractExamples2(source),
26336
+ props: extractMainProps2(source),
26337
+ subComponents: extractSubComponents2(source),
26338
+ variants: extractVariants2(source),
26339
+ accessibility: extractAccessibility2(source),
26340
+ dependencies: extractDependencies3(source),
26341
+ exportedNames: extractExportedNames(source)
26342
+ };
25708
26343
  }
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();
26344
+ function detectUseClient(source) {
26345
+ const firstLine = source.split("\n")[0].trim();
26346
+ return firstLine === '"use client"' || firstLine === "'use client'";
25711
26347
  }
25712
- function capitalize2(s) {
25713
- return s.charAt(0).toUpperCase() + s.slice(1);
26348
+ function extractTopLevelDescription(source) {
26349
+ const match = source.match(/^(?:"use client"\n+)?\/\*\*\n([\s\S]*?)\*\//m);
26350
+ if (!match) return "";
26351
+ const block = match[1];
26352
+ const lines = block.split("\n");
26353
+ const descLines = [];
26354
+ for (const line of lines) {
26355
+ const cleaned = line.replace(/^\s*\*\s?/, "").trim();
26356
+ if (cleaned.startsWith("@")) break;
26357
+ descLines.push(cleaned);
26358
+ }
26359
+ return descLines.join(" ").replace(/\s+/g, " ").trim();
25714
26360
  }
25715
- function inferVariantPropName(typeName, props) {
25716
- const match = props.find((p) => p.type === typeName);
25717
- return match ? match.name : null;
26361
+ function extractExamples2(source) {
26362
+ const match = source.match(/^(?:"use client"\n+)?\/\*\*\n([\s\S]*?)\*\//m);
26363
+ if (!match) return [];
26364
+ const block = match[1];
26365
+ const examples = [];
26366
+ const exampleRegex = /@example\s+(.*?)(?:\n\s*\*\s*```tsx?\n([\s\S]*?)```)/g;
26367
+ let m;
26368
+ while ((m = exampleRegex.exec(block)) !== null) {
26369
+ const title = m[1].trim();
26370
+ const code = m[2].split("\n").map((l) => l.replace(/^\s*\*\s?/, "")).join("\n").trim();
26371
+ examples.push({ title, code });
26372
+ }
26373
+ return examples;
25718
26374
  }
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]);
26375
+ function extractMainProps2(source) {
26376
+ const match = source.match(/interface\s+(\w+Props)\s+(?:extends\s+[\w<>,\s]+\s+)?\{/);
26377
+ if (!match) return [];
26378
+ return extractPropsFromInterface(source, match.index);
26379
+ }
26380
+ function extractPropsFromInterface(source, startIndex) {
26381
+ const bodyStart = source.indexOf("{", startIndex);
26382
+ if (bodyStart === -1) return [];
26383
+ let depth = 0;
26384
+ let bodyEnd = bodyStart;
26385
+ for (let i = bodyStart; i < source.length; i++) {
26386
+ if (source[i] === "{") depth++;
26387
+ else if (source[i] === "}") {
26388
+ depth--;
26389
+ if (depth === 0) {
26390
+ bodyEnd = i;
26391
+ break;
26392
+ }
25724
26393
  }
25725
26394
  }
25726
- return external;
26395
+ const body = source.slice(bodyStart + 1, bodyEnd);
26396
+ return parsePropsBody(body);
25727
26397
  }
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 };
26398
+ function parsePropsBody(body) {
26399
+ const props = [];
26400
+ const propRegex = /(?:\/\*\*\s*([\s\S]*?)\s*\*\/\s*)?([\w]+)(\??):\s*([^\n]+)/g;
26401
+ let m;
26402
+ while ((m = propRegex.exec(body)) !== null) {
26403
+ const jsdoc = m[1] || "";
26404
+ const name = m[2];
26405
+ const optional = m[3] === "?";
26406
+ const rawType = m[4].trim();
26407
+ if (name.startsWith("__")) continue;
26408
+ const type = rawType.replace(/;?\s*$/, "").trim();
26409
+ const description = extractPropDescription(jsdoc);
26410
+ const defaultMatch = jsdoc.match(/@default\s+(.+?)(?:\s*$|\s*\*)/m);
26411
+ const defaultValue = defaultMatch ? defaultMatch[1].trim().replace(/^['"]|['"]$/g, "") : void 0;
26412
+ props.push({
26413
+ name,
26414
+ type,
26415
+ required: !optional && defaultValue === void 0,
26416
+ default: defaultValue,
26417
+ description
26418
+ });
26419
+ }
26420
+ return props;
26421
+ }
26422
+ function extractPropDescription(jsdoc) {
26423
+ if (!jsdoc) return "";
26424
+ const lines = jsdoc.split("\n");
26425
+ const descLines = [];
26426
+ for (const line of lines) {
26427
+ const cleaned = line.replace(/^\s*\*?\s*/, "").trim();
26428
+ if (cleaned.startsWith("@")) break;
26429
+ if (cleaned) descLines.push(cleaned);
26430
+ }
26431
+ return descLines.join(" ").trim();
26432
+ }
26433
+ function extractSubComponents2(source) {
26434
+ const exportedNames = extractExportedNames(source);
26435
+ if (exportedNames.length <= 1) return [];
26436
+ const subs = [];
26437
+ const interfaceRegex = /interface\s+(\w+Props)\s+(?:extends\s+[\w<>,\s]+\s+)?\{/g;
26438
+ const interfaces = [];
26439
+ let im;
26440
+ while ((im = interfaceRegex.exec(source)) !== null) {
26441
+ interfaces.push({ name: im[1], index: im.index });
26442
+ }
26443
+ for (let i = 1; i < interfaces.length; i++) {
26444
+ const iface = interfaces[i];
26445
+ const componentName = iface.name.replace(/Props$/, "");
26446
+ if (!exportedNames.includes(componentName)) continue;
26447
+ const beforeInterface = source.slice(Math.max(0, iface.index - 300), iface.index);
26448
+ const jsdocMatch = beforeInterface.match(/\/\*\*\s*([\s\S]*?)\s*\*\/\s*$/);
26449
+ const description = jsdocMatch ? extractPropDescription(jsdocMatch[1]) : "";
26450
+ const props = extractPropsFromInterface(source, iface.index);
26451
+ subs.push({ name: componentName, description, props });
26452
+ }
26453
+ return subs;
26454
+ }
26455
+ function extractVariants2(source) {
26456
+ const variants = {};
26457
+ const typeRegex = /type\s+(\w+(?:Variant|Size|Orientation|Side|Position))\s*=\s*([^\n]+)/g;
26458
+ let m;
26459
+ while ((m = typeRegex.exec(source)) !== null) {
26460
+ const name = m[1];
26461
+ const values = m[2].match(/'([^']+)'/g);
26462
+ if (values) {
26463
+ variants[name] = values.map((v) => v.replace(/'/g, ""));
26464
+ }
26465
+ }
26466
+ return variants;
26467
+ }
26468
+ function extractAccessibility2(source) {
26469
+ const roleMatches = source.match(/role[={"]+([^"}\s]+)/g);
26470
+ const roles = /* @__PURE__ */ new Set();
26471
+ if (roleMatches) {
26472
+ for (const rm of roleMatches) {
26473
+ const val = rm.match(/role[={"]+([^"}\s]+)/);
26474
+ if (val) roles.add(val[1]);
26475
+ }
25733
26476
  }
26477
+ const ariaMatches = source.match(/aria-[\w]+/g);
26478
+ const ariaAttrs = [...new Set(ariaMatches || [])];
26479
+ const dataMatches = source.match(/data-(?:state|slot|orientation|value|disabled|side)[\w-]*/g);
26480
+ const dataAttrs = [...new Set(dataMatches || [])];
25734
26481
  return {
25735
- statements: lines.slice(0, firstJsxLine).filter((l) => l.trim() !== ""),
25736
- jsx: lines.slice(firstJsxLine).join("\n")
26482
+ role: roles.size > 0 ? [...roles].join(", ") : void 0,
26483
+ ariaAttributes: ariaAttrs,
26484
+ dataAttributes: dataAttrs
25737
26485
  };
25738
26486
  }
25739
- function isSimpleJsx(code) {
25740
- const trimmed = code.trim();
25741
- return trimmed.startsWith("<") && !trimmed.includes("createSignal");
26487
+ function extractDependencies3(source) {
26488
+ const internal = [];
26489
+ const external = [];
26490
+ const importRegex = /import\s+(?:type\s+)?(?:\{[^}]*\}|[\w]+)\s+from\s+['"]([^'"]+)['"]/g;
26491
+ let m;
26492
+ while ((m = importRegex.exec(source)) !== null) {
26493
+ const specifier = m[1];
26494
+ if (m[0].includes("import type")) continue;
26495
+ if (specifier.startsWith("./") || specifier.startsWith("../")) {
26496
+ const parts = specifier.split("/");
26497
+ const name = parts[parts.length - 1].replace(/\.tsx?$/, "");
26498
+ if (name !== "types" && name !== "index") {
26499
+ internal.push(name);
26500
+ }
26501
+ } else {
26502
+ external.push(specifier);
26503
+ }
26504
+ }
26505
+ return {
26506
+ internal: [...new Set(internal)],
26507
+ external: [...new Set(external)]
26508
+ };
26509
+ }
26510
+ function extractExportedNames(source) {
26511
+ const names = [];
26512
+ const seen = /* @__PURE__ */ new Set();
26513
+ const add = (n) => {
26514
+ if (!n) return;
26515
+ const trimmed = n.trim();
26516
+ if (!trimmed || seen.has(trimmed)) return;
26517
+ if (trimmed.startsWith("type ")) return;
26518
+ seen.add(trimmed);
26519
+ names.push(trimmed);
26520
+ };
26521
+ const braceRegex = /export\s+\{([^}]+)\}/g;
26522
+ let bm;
26523
+ while ((bm = braceRegex.exec(source)) !== null) {
26524
+ for (const part of bm[1].split(",")) {
26525
+ const trimmed = part.trim();
26526
+ if (!trimmed || trimmed.startsWith("type ")) continue;
26527
+ const asMatch = trimmed.match(/\s+as\s+(\w+)$/);
26528
+ add(asMatch ? asMatch[1] : trimmed);
26529
+ }
26530
+ }
26531
+ const fnRegex = /export\s+(?:default\s+)?(?:async\s+)?function\s+(\w+)/g;
26532
+ let fm;
26533
+ while ((fm = fnRegex.exec(source)) !== null) add(fm[1]);
26534
+ const varRegex = /export\s+(?:const|let|var)\s+(\w+)\s*[=:]/g;
26535
+ let vm;
26536
+ while ((vm = varRegex.exec(source)) !== null) add(vm[1]);
26537
+ const defaultRegex = /export\s+default\s+(\w+)\s*;?\s*$/m;
26538
+ const dm = defaultRegex.exec(source);
26539
+ if (dm) add(dm[1]);
26540
+ return names;
25742
26541
  }
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;
26542
+ var init_parse_component = __esm({
26543
+ "src/lib/parse-component.ts"() {
26544
+ "use strict";
25751
26545
  }
25752
- const needsCreateSignalImport = exampleCode.includes("createSignal");
26546
+ });
26547
+
26548
+ // src/lib/test-template.ts
26549
+ import { readFileSync as readFileSync9 } from "fs";
26550
+ import path20 from "path";
26551
+ function generateTestTemplate(componentPath, options = {}) {
26552
+ const importSource = options.importSource ?? "bun:test";
26553
+ const source = readFileSync9(componentPath, "utf-8");
26554
+ const parsed = parseComponent(source);
26555
+ const fileName = path20.basename(componentPath);
26556
+ const baseName = fileName.replace(/\.tsx$/, "");
26557
+ const relativePath = fileName;
26558
+ const varName = toCamelCase2(baseName) + "Source";
26559
+ const exportedNames = parsed.exportedNames;
26560
+ const mainComponent = exportedNames[0];
26561
+ const hasSubComponents = exportedNames.length > 1;
25753
26562
  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'");
26563
+ lines.push(`import { describe, test, expect } from '${importSource}'`);
26564
+ lines.push(`import { readFileSync } from 'fs'`);
26565
+ lines.push(`import { resolve } from 'path'`);
26566
+ lines.push(`import { renderToTest } from '@barefootjs/test'`);
26567
+ lines.push(``);
26568
+ lines.push(`const ${varName} = readFileSync(resolve(__dirname, '${relativePath}'), 'utf-8')`);
26569
+ lines.push(``);
26570
+ if (mainComponent) {
26571
+ lines.push(...generateDescribeBlock(source, parsed, mainComponent, varName, fileName, hasSubComponents));
25762
26572
  }
25763
- const componentImports = [pascalName];
25764
- const subNames = [];
25765
26573
  if (hasSubComponents) {
25766
- for (const sub2 of meta.subComponents) {
25767
- componentImports.push(sub2.name);
25768
- subNames.push(sub2.name);
26574
+ for (let i = 1; i < exportedNames.length; i++) {
26575
+ lines.push(``);
26576
+ lines.push(...generateDescribeBlock(source, parsed, exportedNames[i], varName, fileName, true));
25769
26577
  }
25770
26578
  }
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)}'`);
26579
+ return lines.join("\n") + "\n";
26580
+ }
26581
+ function generateDescribeBlock(source, parsed, componentName, varName, fileName, multiComponent) {
26582
+ const lines = [];
26583
+ const renderArg = multiComponent ? `, '${componentName}'` : "";
26584
+ const funcInfo = analyzeFunction(source, componentName);
26585
+ lines.push(`describe('${componentName}', () => {`);
26586
+ lines.push(` const result = renderToTest(${varName}, '${fileName}'${renderArg})`);
26587
+ lines.push(``);
26588
+ lines.push(` test('has no compiler errors', () => {`);
26589
+ lines.push(` expect(result.errors).toEqual([])`);
26590
+ lines.push(` })`);
26591
+ lines.push(``);
26592
+ lines.push(` test('componentName is ${componentName}', () => {`);
26593
+ lines.push(` expect(result.componentName).toBe('${componentName}')`);
26594
+ lines.push(` })`);
26595
+ lines.push(``);
26596
+ if (funcInfo.signals.length > 0) {
26597
+ lines.push(` test('has expected signals', () => {`);
26598
+ for (const sig of funcInfo.signals) {
26599
+ lines.push(` expect(result.signals).toContain('${sig}')`);
25776
26600
  }
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);
26601
+ lines.push(` })`);
25787
26602
  } else {
25788
- generateStatelessSimple(lines, previewNames, meta, pascalName);
26603
+ lines.push(` test('no signals (stateless)', () => {`);
26604
+ lines.push(` expect(result.signals).toEqual([])`);
26605
+ lines.push(` })`);
25789
26606
  }
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}>`);
26607
+ lines.push(``);
26608
+ if (funcInfo.rootTag) {
26609
+ const isComponentRoot = /^[A-Z]/.test(funcInfo.rootTag);
26610
+ lines.push(` test('renders as <${funcInfo.rootTag}>', () => {`);
26611
+ if (funcInfo.hasConditionalReturn) {
26612
+ lines.push(` // Component has conditional return (e.g., asChild branch)`);
26613
+ const finder = isComponentRoot ? `result.find({ componentName: '${funcInfo.rootTag}' })` : `result.find({ tag: '${funcInfo.rootTag}' })`;
26614
+ lines.push(` expect(${finder}).not.toBeNull()`);
26615
+ } else if (isComponentRoot) {
26616
+ lines.push(` expect(result.root.componentName).toBe('${funcInfo.rootTag}')`);
26617
+ } else {
26618
+ lines.push(` expect(result.root.tag).toBe('${funcInfo.rootTag}')`);
25813
26619
  }
25814
- lines.push(" </div>");
25815
- lines.push(" )");
25816
- lines.push("}");
25817
- lines.push("");
26620
+ lines.push(` })`);
26621
+ lines.push(``);
25818
26622
  }
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}`);
26623
+ if (funcInfo.dataSlot) {
26624
+ lines.push(` test('has data-slot=${funcInfo.dataSlot}', () => {`);
26625
+ if (funcInfo.hasConditionalReturn) {
26626
+ lines.push(` const el = result.find({ tag: '${funcInfo.rootTag}' })!`);
26627
+ lines.push(` expect(el.props['data-slot']).toBe('${funcInfo.dataSlot}')`);
25828
26628
  } else {
25829
- lines.push(" return (");
25830
- for (const l of jsxLines) {
25831
- lines.push(` ${l}`);
25832
- }
25833
- lines.push(" )");
26629
+ lines.push(` expect(result.root.props['data-slot']).toBe('${funcInfo.dataSlot}')`);
25834
26630
  }
25835
- } else {
25836
- const hasChildren = meta.props.some((p) => p.name === "children");
25837
- if (hasChildren) {
25838
- lines.push(` return <${pascalName}>${meta.title}</${pascalName}>`);
26631
+ lines.push(` })`);
26632
+ lines.push(``);
26633
+ }
26634
+ if (funcInfo.role) {
26635
+ lines.push(` test('has role=${funcInfo.role}', () => {`);
26636
+ lines.push(` const el = result.find({ role: '${funcInfo.role}' })`);
26637
+ lines.push(` expect(el).not.toBeNull()`);
26638
+ lines.push(` })`);
26639
+ lines.push(``);
26640
+ }
26641
+ const ariaAttrs = funcInfo.ariaAttributes;
26642
+ if (ariaAttrs.length > 0) {
26643
+ lines.push(` test('has ARIA attributes', () => {`);
26644
+ const findTarget = funcInfo.role ? `result.find({ role: '${funcInfo.role}' })!` : funcInfo.rootTag ? `result.find({ tag: '${funcInfo.rootTag}' })!` : "result.root";
26645
+ lines.push(` const el = ${findTarget}`);
26646
+ for (const attr of ariaAttrs) {
26647
+ const shortName = attr.replace("aria-", "");
26648
+ lines.push(` expect(el.aria).toHaveProperty('${shortName}')`);
26649
+ }
26650
+ lines.push(` })`);
26651
+ lines.push(``);
26652
+ }
26653
+ if (funcInfo.hasDataState) {
26654
+ lines.push(` test('has data-state attribute', () => {`);
26655
+ if (funcInfo.hasConditionalReturn) {
26656
+ lines.push(` const el = result.find({ tag: '${funcInfo.rootTag}' })!`);
26657
+ lines.push(` expect(el.dataState).not.toBeNull()`);
25839
26658
  } else {
25840
- lines.push(` return <${pascalName} />`);
26659
+ lines.push(` expect(result.root.dataState).not.toBeNull()`);
25841
26660
  }
26661
+ lines.push(` })`);
26662
+ lines.push(``);
25842
26663
  }
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} />`);
26664
+ if (funcInfo.events.length > 0) {
26665
+ lines.push(` test('has event handlers', () => {`);
26666
+ lines.push(` const all = result.findAll({})`);
26667
+ for (const event of funcInfo.events) {
26668
+ const onProp = ON_PROP_BY_EVENT[event];
26669
+ lines.push(` expect(`);
26670
+ lines.push(` all.some(n => n.events.includes('${event}') || n.props['${onProp}'] != null),`);
26671
+ lines.push(` ).toBe(true)`);
26672
+ }
26673
+ lines.push(` })`);
26674
+ lines.push(``);
25857
26675
  }
25858
- if (meta.props.some((p) => p.name === "disabled")) {
25859
- lines.push(` <${pascalName} disabled />`);
26676
+ if (funcInfo.childComponents.length > 0) {
26677
+ lines.push(` test('contains child components', () => {`);
26678
+ for (const child of funcInfo.childComponents) {
26679
+ lines.push(` expect(result.find({ componentName: '${child}' })).not.toBeNull()`);
26680
+ }
26681
+ lines.push(` })`);
26682
+ lines.push(``);
25860
26683
  }
25861
- lines.push(" </div>");
25862
- lines.push(" )");
25863
- lines.push("}");
25864
- lines.push("");
26684
+ lines.push(` test('toStructure() shows expected tree', () => {`);
26685
+ lines.push(` const structure = result.toStructure()`);
26686
+ lines.push(` expect(structure.length).toBeGreaterThan(0)`);
26687
+ if (funcInfo.rootTag) {
26688
+ lines.push(` expect(structure).toContain('${funcInfo.rootTag}')`);
26689
+ }
26690
+ if (funcInfo.role) {
26691
+ lines.push(` expect(structure).toContain('[role=${funcInfo.role}]')`);
26692
+ }
26693
+ lines.push(` })`);
26694
+ lines.push(`})`);
26695
+ return lines;
25865
26696
  }
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
- }
26697
+ function analyzeFunction(source, componentName) {
26698
+ const funcRegex = new RegExp(`function\\s+${componentName}\\s*\\(`);
26699
+ const funcMatch = funcRegex.exec(source);
26700
+ let funcBody = source;
26701
+ if (!funcMatch) {
26702
+ const aliasMatch = source.match(new RegExp(`const\\s+${componentName}\\s*=\\s*(\\w+)`));
26703
+ if (aliasMatch) {
26704
+ return analyzeFunction(source, aliasMatch[1]);
25883
26705
  }
25884
- lines.push(" </div>");
25885
- lines.push(" )");
25886
- lines.push("}");
25887
- lines.push("");
25888
26706
  }
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} = () => {}`);
26707
+ if (funcMatch) {
26708
+ const parenStart = source.indexOf("(", funcMatch.index);
26709
+ let parenDepth = 0;
26710
+ let parenEnd = parenStart;
26711
+ for (let i = parenStart; i < source.length; i++) {
26712
+ if (source[i] === "(") parenDepth++;
26713
+ else if (source[i] === ")") {
26714
+ parenDepth--;
26715
+ if (parenDepth === 0) {
26716
+ parenEnd = i;
26717
+ break;
26718
+ }
25906
26719
  }
25907
26720
  }
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}`);
26721
+ let depth = 0;
26722
+ let bodyStart = -1;
26723
+ for (let i = parenEnd + 1; i < source.length; i++) {
26724
+ if (source[i] === "{") {
26725
+ if (bodyStart === -1) bodyStart = i;
26726
+ depth++;
26727
+ } else if (source[i] === "}") {
26728
+ depth--;
26729
+ if (depth === 0) {
26730
+ funcBody = source.slice(bodyStart, i + 1);
26731
+ break;
26732
+ }
25918
26733
  }
25919
- lines.push(" )");
25920
26734
  }
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
- }
26735
+ }
26736
+ const signals = [];
26737
+ const signalRegex = /const\s+\[(\w+),\s*\w+\]\s*=\s*createSignal/g;
26738
+ let sm;
26739
+ while ((sm = signalRegex.exec(funcBody)) !== null) {
26740
+ signals.push(sm[1]);
26741
+ }
26742
+ const returnMatches = [...funcBody.matchAll(/return\s*\(?\s*<(\w+)/g)];
26743
+ const rootTag = returnMatches.length > 0 ? returnMatches[returnMatches.length - 1][1] : null;
26744
+ const allSlots = [...funcBody.matchAll(/data-slot="([^"]+)"/g)].map((m) => m[1]);
26745
+ const expectedSlot = componentName.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
26746
+ const dataSlot = allSlots.find((s) => s === expectedSlot) || allSlots[0] || null;
26747
+ const roleMatch = funcBody.match(/role="([^"]+)"/);
26748
+ const role = roleMatch ? roleMatch[1] : null;
26749
+ const ariaMatches = funcBody.match(/aria-[\w]+(?==)/g);
26750
+ const ariaAttributes = [...new Set(ariaMatches || [])].filter((a) => a !== "aria-invalid");
26751
+ const hasDataState = /data-state=/.test(funcBody);
26752
+ const hasConditionalReturn = /if\s*\(.*\)\s*\{?\s*return/.test(funcBody);
26753
+ const events = [];
26754
+ for (const [event, onProp] of Object.entries(ON_PROP_BY_EVENT)) {
26755
+ if (new RegExp(`${onProp}=`).test(funcBody)) events.push(event);
26756
+ }
26757
+ const childCompRegex = /<([A-Z][A-Za-z]+)[\s/>]/g;
26758
+ const childComponents = [];
26759
+ let cm;
26760
+ while ((cm = childCompRegex.exec(funcBody)) !== null) {
26761
+ if (!childComponents.includes(cm[1])) {
26762
+ childComponents.push(cm[1]);
25934
26763
  }
25935
- lines.push(` </${pascalName}>`);
25936
- lines.push(" )");
25937
- lines.push("}");
25938
26764
  }
25939
- lines.push("");
26765
+ return {
26766
+ signals,
26767
+ rootTag,
26768
+ dataSlot,
26769
+ role,
26770
+ ariaAttributes,
26771
+ hasDataState,
26772
+ hasConditionalReturn,
26773
+ events,
26774
+ childComponents
26775
+ };
25940
26776
  }
25941
- var init_preview_generate = __esm({
25942
- "src/lib/preview-generate.ts"() {
26777
+ function toCamelCase2(kebab) {
26778
+ return kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
26779
+ }
26780
+ var ON_PROP_BY_EVENT;
26781
+ var init_test_template = __esm({
26782
+ "src/lib/test-template.ts"() {
26783
+ "use strict";
26784
+ init_parse_component();
26785
+ ON_PROP_BY_EVENT = {
26786
+ click: "onClick",
26787
+ input: "onInput",
26788
+ change: "onChange",
26789
+ keydown: "onKeyDown"
26790
+ };
26791
+ }
26792
+ });
26793
+
26794
+ // src/commands/gen-test.ts
26795
+ var gen_test_exports = {};
26796
+ __export(gen_test_exports, {
26797
+ run: () => run12
26798
+ });
26799
+ import { existsSync as existsSync21, writeFileSync as writeFileSync7 } from "fs";
26800
+ import path21 from "path";
26801
+ function run12(args2, ctx2) {
26802
+ const positional = args2.filter((a) => !a.startsWith("-"));
26803
+ const flagSet = new Set(args2.filter((a) => a.startsWith("-")));
26804
+ const writeToStdout = flagSet.has("--stdout");
26805
+ const force = flagSet.has("--force") || flagSet.has("-f");
26806
+ const componentName = positional[0];
26807
+ if (!componentName) {
26808
+ console.error("Error: Component name required. Usage: bf gen test <component> [--stdout] [--force]");
26809
+ process.exit(1);
26810
+ }
26811
+ const searched = [];
26812
+ const resolved = resolveComponentSource(componentName, ctx2, searched);
26813
+ if (!resolved) {
26814
+ console.error(`Error: Cannot find component "${componentName}".`);
26815
+ console.error("Looked in:");
26816
+ for (const p of searched) console.error(` - ${p}`);
26817
+ process.exit(1);
26818
+ }
26819
+ const pm = detectPackageManager(ctx2.projectDir ?? ctx2.root);
26820
+ const runner = testRunnerFor(pm);
26821
+ const content = generateTestTemplate(resolved.filePath, { importSource: runner.importSource });
26822
+ if (writeToStdout) {
26823
+ console.log(content);
26824
+ return;
26825
+ }
26826
+ const dir = path21.dirname(resolved.filePath);
26827
+ const base = path21.basename(resolved.filePath, path21.extname(resolved.filePath));
26828
+ const testPath = path21.join(dir, `${base}.test.tsx`);
26829
+ if (existsSync21(testPath) && !force) {
26830
+ const rel2 = path21.relative(ctx2.projectDir ?? ctx2.root, testPath);
26831
+ console.error(`Error: ${rel2} already exists. Pass --force to overwrite, or --stdout to preview.`);
26832
+ process.exit(1);
26833
+ }
26834
+ writeFileSync7(testPath, content);
26835
+ const rel = path21.relative(ctx2.projectDir ?? ctx2.root, testPath);
26836
+ console.log(`Created: ${rel}`);
26837
+ console.log(``);
26838
+ console.log(`Next: ${commandsFor(pm).test(rel)}`);
26839
+ }
26840
+ var init_gen_test = __esm({
26841
+ "src/commands/gen-test.ts"() {
25943
26842
  "use strict";
26843
+ init_resolve_source();
26844
+ init_test_template();
26845
+ init_pm();
25944
26846
  }
25945
26847
  });
25946
26848
 
@@ -25949,7 +26851,7 @@ var gen_preview_exports = {};
25949
26851
  __export(gen_preview_exports, {
25950
26852
  run: () => run13
25951
26853
  });
25952
- import { existsSync as existsSync18, writeFileSync as writeFileSync7, mkdirSync as mkdirSync5 } from "fs";
26854
+ import { existsSync as existsSync22, writeFileSync as writeFileSync8, mkdirSync as mkdirSync5 } from "fs";
25953
26855
  import path22 from "path";
25954
26856
  async function run13(args2, ctx2) {
25955
26857
  const force = args2.includes("--force");
@@ -25962,15 +26864,15 @@ async function run13(args2, ctx2) {
25962
26864
  const { writeRoot, componentsBasePath } = resolveScaffoldLayout(ctx2);
25963
26865
  const result = generatePreview(meta, componentsBasePath);
25964
26866
  const absPath = path22.join(writeRoot, result.filePath);
25965
- if (existsSync18(absPath) && !force) {
26867
+ if (existsSync22(absPath) && !force) {
25966
26868
  console.error(`Error: ${result.filePath} already exists. Use --force to overwrite.`);
25967
26869
  process.exit(1);
25968
26870
  }
25969
26871
  const dir = path22.dirname(absPath);
25970
- if (!existsSync18(dir)) {
26872
+ if (!existsSync22(dir)) {
25971
26873
  mkdirSync5(dir, { recursive: true });
25972
26874
  }
25973
- writeFileSync7(absPath, result.code);
26875
+ writeFileSync8(absPath, result.code);
25974
26876
  console.log(`Generated ${result.filePath}`);
25975
26877
  console.log(`Previews: ${result.previewNames.join(", ")}`);
25976
26878
  }
@@ -25988,7 +26890,7 @@ var debug_graph_exports = {};
25988
26890
  __export(debug_graph_exports, {
25989
26891
  run: () => run14
25990
26892
  });
25991
- import { readFileSync as readFileSync9 } from "fs";
26893
+ import { readFileSync as readFileSync10 } from "fs";
25992
26894
  async function run14(args2, ctx2) {
25993
26895
  const componentName = args2[0];
25994
26896
  if (!componentName) {
@@ -26005,7 +26907,7 @@ async function run14(args2, ctx2) {
26005
26907
  for (const p of searched) console.error(` - ${p}`);
26006
26908
  process.exit(1);
26007
26909
  }
26008
- const source = readFileSync9(resolved.filePath, "utf-8");
26910
+ const source = readFileSync10(resolved.filePath, "utf-8");
26009
26911
  const graph = buildComponentGraph2(source, resolved.filePath, resolved.componentName);
26010
26912
  if (ctx2.jsonFlag) {
26011
26913
  console.log(JSON.stringify(graphToJSON2(graph), null, 2));
@@ -26025,7 +26927,7 @@ var debug_trace_exports = {};
26025
26927
  __export(debug_trace_exports, {
26026
26928
  run: () => run15
26027
26929
  });
26028
- import { readFileSync as readFileSync10 } from "fs";
26930
+ import { readFileSync as readFileSync11 } from "fs";
26029
26931
  async function run15(args2, ctx2) {
26030
26932
  const componentName = args2[0];
26031
26933
  const targetName = args2[1];
@@ -26043,7 +26945,7 @@ async function run15(args2, ctx2) {
26043
26945
  for (const p of searched) console.error(` - ${p}`);
26044
26946
  process.exit(1);
26045
26947
  }
26046
- const source = readFileSync10(resolved.filePath, "utf-8");
26948
+ const source = readFileSync11(resolved.filePath, "utf-8");
26047
26949
  const graph = buildComponentGraph2(source, resolved.filePath, resolved.componentName);
26048
26950
  const path23 = traceUpdatePath2(graph, targetName);
26049
26951
  if (!path23) {
@@ -26075,7 +26977,7 @@ var debug_fallbacks_exports = {};
26075
26977
  __export(debug_fallbacks_exports, {
26076
26978
  run: () => run16
26077
26979
  });
26078
- import { readFileSync as readFileSync11 } from "fs";
26980
+ import { readFileSync as readFileSync12 } from "fs";
26079
26981
  async function run16(args2, ctx2) {
26080
26982
  const componentName = args2[0];
26081
26983
  if (!componentName) {
@@ -26083,7 +26985,7 @@ async function run16(args2, ctx2) {
26083
26985
  console.error("Usage: bf debug fallbacks <component> [--json]");
26084
26986
  process.exit(1);
26085
26987
  }
26086
- const { buildComponentGraph: buildComponentGraph2 } = await Promise.resolve().then(() => (init_src2(), src_exports));
26988
+ const { buildComponentGraph: buildComponentGraph2, describeFallback: describeFallback2, formatFallbackExplanations: formatFallbackExplanations2 } = await Promise.resolve().then(() => (init_src2(), src_exports));
26087
26989
  const searched = [];
26088
26990
  const resolved = resolveComponentSource(componentName, ctx2, searched);
26089
26991
  if (!resolved) {
@@ -26092,45 +26994,35 @@ async function run16(args2, ctx2) {
26092
26994
  for (const p of searched) console.error(` - ${p}`);
26093
26995
  process.exit(1);
26094
26996
  }
26095
- const source = readFileSync11(resolved.filePath, "utf-8");
26997
+ const source = readFileSync12(resolved.filePath, "utf-8");
26096
26998
  const graph = buildComponentGraph2(source, resolved.filePath, resolved.componentName);
26097
- const fallbacks = graph.domBindings.filter((d) => d.classification === "fallback");
26999
+ const isEventHandlerProp = (d) => d.type === "attribute" && /^on[A-Z]/.test(d.label.split(".").pop() ?? "");
27000
+ const fallbacks = graph.domBindings.filter((d) => d.classification === "fallback" && !isEventHandlerProp(d));
26098
27001
  if (ctx2.jsonFlag) {
26099
27002
  console.log(JSON.stringify({
26100
27003
  componentName: graph.componentName,
26101
27004
  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
- }))
27005
+ fallbacks: fallbacks.map((f) => {
27006
+ const ex = describeFallback2(f);
27007
+ return {
27008
+ label: f.label,
27009
+ slotId: f.slotId,
27010
+ deps: f.deps,
27011
+ type: f.type,
27012
+ classification: f.classification,
27013
+ ...f.expression !== void 0 && { expression: f.expression },
27014
+ ...f.wrapReason !== void 0 && { wrapReason: f.wrapReason },
27015
+ reason: ex.reason,
27016
+ runtimeDeps: ex.runtimeDeps,
27017
+ suggestion: ex.suggestion,
27018
+ isEventHandler: ex.isEventHandler,
27019
+ ...ex.loc && { loc: ex.loc }
27020
+ };
27021
+ })
26111
27022
  }, null, 2));
26112
27023
  return;
26113
27024
  }
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.");
27025
+ console.log(formatFallbackExplanations2(graph.componentName, fallbacks));
26134
27026
  }
26135
27027
  var init_debug_fallbacks = __esm({
26136
27028
  "src/commands/debug-fallbacks.ts"() {
@@ -26144,7 +27036,7 @@ var debug_signals_exports = {};
26144
27036
  __export(debug_signals_exports, {
26145
27037
  run: () => run17
26146
27038
  });
26147
- import { readFileSync as readFileSync12 } from "fs";
27039
+ import { readFileSync as readFileSync13 } from "fs";
26148
27040
  async function run17(args2, ctx2) {
26149
27041
  const componentName = args2[0];
26150
27042
  if (!componentName) {
@@ -26161,7 +27053,7 @@ async function run17(args2, ctx2) {
26161
27053
  for (const p of searched) console.error(` - ${p}`);
26162
27054
  process.exit(1);
26163
27055
  }
26164
- const source = readFileSync12(resolved.filePath, "utf-8");
27056
+ const source = readFileSync13(resolved.filePath, "utf-8");
26165
27057
  const graph = buildComponentGraph2(source, resolved.filePath, resolved.componentName);
26166
27058
  const trace = generateStaticTrace2(graph);
26167
27059
  if (ctx2.jsonFlag) {
@@ -26184,7 +27076,7 @@ var debug_events_exports = {};
26184
27076
  __export(debug_events_exports, {
26185
27077
  run: () => run18
26186
27078
  });
26187
- import { readFileSync as readFileSync13 } from "fs";
27079
+ import { readFileSync as readFileSync14 } from "fs";
26188
27080
  async function run18(args2, ctx2) {
26189
27081
  const componentName = args2[0];
26190
27082
  if (!componentName) {
@@ -26201,7 +27093,7 @@ async function run18(args2, ctx2) {
26201
27093
  for (const p of searched) console.error(` - ${p}`);
26202
27094
  process.exit(1);
26203
27095
  }
26204
- const source = readFileSync13(resolved.filePath, "utf-8");
27096
+ const source = readFileSync14(resolved.filePath, "utf-8");
26205
27097
  const summary = buildEventSummary2(source, resolved.filePath, resolved.componentName);
26206
27098
  if (ctx2.jsonFlag) {
26207
27099
  console.log(JSON.stringify({ componentName: summary.componentName, sourceFile: summary.sourceFile, events: summary.events }, null, 2));
@@ -26221,7 +27113,7 @@ var debug_loops_exports = {};
26221
27113
  __export(debug_loops_exports, {
26222
27114
  run: () => run19
26223
27115
  });
26224
- import { readFileSync as readFileSync14 } from "fs";
27116
+ import { readFileSync as readFileSync15 } from "fs";
26225
27117
  async function run19(args2, ctx2) {
26226
27118
  const componentName = args2[0];
26227
27119
  if (!componentName) {
@@ -26238,7 +27130,7 @@ async function run19(args2, ctx2) {
26238
27130
  for (const p of searched) console.error(` - ${p}`);
26239
27131
  process.exit(1);
26240
27132
  }
26241
- const source = readFileSync14(resolved.filePath, "utf-8");
27133
+ const source = readFileSync15(resolved.filePath, "utf-8");
26242
27134
  const summary = buildLoopSummary2(source, resolved.filePath, resolved.componentName);
26243
27135
  if (ctx2.jsonFlag) {
26244
27136
  console.log(JSON.stringify(summary, null, 2));
@@ -26253,6 +27145,101 @@ var init_debug_loops = __esm({
26253
27145
  }
26254
27146
  });
26255
27147
 
27148
+ // src/commands/debug-why-update.ts
27149
+ var debug_why_update_exports = {};
27150
+ __export(debug_why_update_exports, {
27151
+ run: () => run20
27152
+ });
27153
+ import { readFileSync as readFileSync16 } from "fs";
27154
+ async function run20(args2, ctx2) {
27155
+ const componentName = args2[0];
27156
+ const bindingLabel = args2[1];
27157
+ if (!componentName || !bindingLabel) {
27158
+ console.error("Error: Component name and binding label required.");
27159
+ console.error("Usage: bf debug why-update <component> <binding> [--json]");
27160
+ console.error(' binding: attribute name (e.g. "style"), slot ID (e.g. "s0"),');
27161
+ console.error(' or component prop (e.g. "Button.disabled")');
27162
+ process.exit(1);
27163
+ }
27164
+ const { buildWhyUpdate: buildWhyUpdate2, formatWhyUpdate: formatWhyUpdate2, buildComponentGraph: buildComponentGraph2 } = await Promise.resolve().then(() => (init_src2(), src_exports));
27165
+ const searched = [];
27166
+ const resolved = resolveComponentSource(componentName, ctx2, searched);
27167
+ if (!resolved) {
27168
+ console.error(`Error: Cannot find component "${componentName}".`);
27169
+ console.error("Looked in:");
27170
+ for (const p of searched) console.error(` - ${p}`);
27171
+ process.exit(1);
27172
+ }
27173
+ const source = readFileSync16(resolved.filePath, "utf-8");
27174
+ const result = buildWhyUpdate2(source, resolved.filePath, bindingLabel, resolved.componentName);
27175
+ if (!result) {
27176
+ const graph = buildComponentGraph2(source, resolved.filePath, resolved.componentName);
27177
+ console.error(`Error: Binding "${bindingLabel}" not found in ${graph.componentName}.`);
27178
+ const available = graph.domBindings.map(
27179
+ (d) => d.type === "attribute" ? d.label : d.slotId
27180
+ );
27181
+ if (available.length > 0) {
27182
+ console.error(`Available bindings: ${[...new Set(available)].join(", ")}`);
27183
+ }
27184
+ process.exit(1);
27185
+ }
27186
+ if (result.ambiguous) {
27187
+ console.error(`Error: "${bindingLabel}" matches multiple bindings. Disambiguate with a slot ID:`);
27188
+ for (const m of result.ambiguous) {
27189
+ console.error(` ${m.slotId} (${m.label})`);
27190
+ }
27191
+ process.exit(1);
27192
+ }
27193
+ if (ctx2.jsonFlag) {
27194
+ console.log(JSON.stringify(result, null, 2));
27195
+ return;
27196
+ }
27197
+ console.log(formatWhyUpdate2(result));
27198
+ }
27199
+ var init_debug_why_update = __esm({
27200
+ "src/commands/debug-why-update.ts"() {
27201
+ "use strict";
27202
+ init_resolve_source();
27203
+ }
27204
+ });
27205
+
27206
+ // src/commands/debug-summary.ts
27207
+ var debug_summary_exports = {};
27208
+ __export(debug_summary_exports, {
27209
+ run: () => run21
27210
+ });
27211
+ import { readFileSync as readFileSync17 } from "fs";
27212
+ async function run21(args2, ctx2) {
27213
+ const componentName = args2[0];
27214
+ if (!componentName) {
27215
+ console.error("Error: Component name required.");
27216
+ console.error("Usage: bf debug summary <component> [--json]");
27217
+ process.exit(1);
27218
+ }
27219
+ const { buildComponentSummary: buildComponentSummary2, formatComponentSummary: formatComponentSummary2 } = await Promise.resolve().then(() => (init_src2(), src_exports));
27220
+ const searched = [];
27221
+ const resolved = resolveComponentSource(componentName, ctx2, searched);
27222
+ if (!resolved) {
27223
+ console.error(`Error: Cannot find component "${componentName}".`);
27224
+ console.error("Looked in:");
27225
+ for (const p of searched) console.error(` - ${p}`);
27226
+ process.exit(1);
27227
+ }
27228
+ const source = readFileSync17(resolved.filePath, "utf-8");
27229
+ const summary = buildComponentSummary2(source, resolved.filePath, resolved.componentName);
27230
+ if (ctx2.jsonFlag) {
27231
+ console.log(JSON.stringify(summary, null, 2));
27232
+ return;
27233
+ }
27234
+ console.log(formatComponentSummary2(summary));
27235
+ }
27236
+ var init_debug_summary = __esm({
27237
+ "src/commands/debug-summary.ts"() {
27238
+ "use strict";
27239
+ init_resolve_source();
27240
+ }
27241
+ });
27242
+
26256
27243
  // src/context.ts
26257
27244
  import { existsSync as existsSync2 } from "fs";
26258
27245
  import path from "path";
@@ -26350,6 +27337,8 @@ Debug:
26350
27337
  debug trace <component> <signal> Trace update propagation for a signal/memo
26351
27338
  debug events <component> Show event handlers and their update paths
26352
27339
  debug loops <component> Show loop bindings grouped by source collection
27340
+ debug why-update <component> <binding> Explain why a binding updates
27341
+ debug summary <component> Show hydration and size summary
26353
27342
  debug fallbacks <component> Show wrap-by-default fallback bindings (#937)
26354
27343
  debug signals <component> Show signal initialization trace
26355
27344
 
@@ -26369,60 +27358,60 @@ Workflow:
26369
27358
  }
26370
27359
  switch (command) {
26371
27360
  case "build": {
26372
- const { run: run20 } = await Promise.resolve().then(() => (init_build2(), build_exports));
26373
- await run20(filteredArgs.slice(1), ctx);
27361
+ const { run: run22 } = await Promise.resolve().then(() => (init_build2(), build_exports));
27362
+ await run22(filteredArgs.slice(1), ctx);
26374
27363
  break;
26375
27364
  }
26376
27365
  case "init": {
26377
- const { run: run20 } = await Promise.resolve().then(() => (init_init(), init_exports));
26378
- await run20(filteredArgs.slice(1), ctx);
27366
+ const { run: run22 } = await Promise.resolve().then(() => (init_init(), init_exports));
27367
+ await run22(filteredArgs.slice(1), ctx);
26379
27368
  break;
26380
27369
  }
26381
27370
  case "add": {
26382
- const { run: run20 } = await Promise.resolve().then(() => (init_add(), add_exports));
26383
- await run20(filteredArgs.slice(1), ctx);
27371
+ const { run: run22 } = await Promise.resolve().then(() => (init_add(), add_exports));
27372
+ await run22(filteredArgs.slice(1), ctx);
26384
27373
  break;
26385
27374
  }
26386
27375
  case "search": {
26387
- const { run: run20 } = await Promise.resolve().then(() => (init_search(), search_exports));
26388
- await run20(filteredArgs.slice(1), ctx);
27376
+ const { run: run22 } = await Promise.resolve().then(() => (init_search(), search_exports));
27377
+ await run22(filteredArgs.slice(1), ctx);
26389
27378
  break;
26390
27379
  }
26391
27380
  case "docs": {
26392
- const { run: run20 } = await Promise.resolve().then(() => (init_docs(), docs_exports));
26393
- run20(filteredArgs.slice(1), ctx);
27381
+ const { run: run22 } = await Promise.resolve().then(() => (init_docs(), docs_exports));
27382
+ run22(filteredArgs.slice(1), ctx);
26394
27383
  break;
26395
27384
  }
26396
27385
  case "guide": {
26397
- const { run: run20 } = await Promise.resolve().then(() => (init_guide(), guide_exports));
26398
- run20(filteredArgs.slice(1), ctx);
27386
+ const { run: run22 } = await Promise.resolve().then(() => (init_guide(), guide_exports));
27387
+ run22(filteredArgs.slice(1), ctx);
26399
27388
  break;
26400
27389
  }
26401
27390
  case "preview": {
26402
- const { run: run20 } = await Promise.resolve().then(() => (init_preview(), preview_exports));
26403
- await run20(filteredArgs.slice(1), ctx);
27391
+ const { run: run22 } = await Promise.resolve().then(() => (init_preview(), preview_exports));
27392
+ await run22(filteredArgs.slice(1), ctx);
26404
27393
  break;
26405
27394
  }
26406
27395
  case "tokens": {
26407
27396
  if (sub === "apply") {
26408
- const { run: run20 } = await Promise.resolve().then(() => (init_tokens_apply(), tokens_apply_exports));
26409
- await run20(rest, ctx);
27397
+ const { run: run22 } = await Promise.resolve().then(() => (init_tokens_apply(), tokens_apply_exports));
27398
+ await run22(rest, ctx);
26410
27399
  } else {
26411
- const { run: run20 } = await Promise.resolve().then(() => (init_tokens(), tokens_exports));
26412
- await run20(filteredArgs.slice(1), ctx);
27400
+ const { run: run22 } = await Promise.resolve().then(() => (init_tokens2(), tokens_exports));
27401
+ await run22(filteredArgs.slice(1), ctx);
26413
27402
  }
26414
27403
  break;
26415
27404
  }
26416
27405
  case "gen": {
26417
27406
  if (sub === "component") {
26418
- const { run: run20 } = await Promise.resolve().then(() => (init_gen_component(), gen_component_exports));
26419
- run20(rest, ctx);
27407
+ const { run: run22 } = await Promise.resolve().then(() => (init_gen_component(), gen_component_exports));
27408
+ run22(rest, ctx);
26420
27409
  } else if (sub === "test") {
26421
- const { run: run20 } = await Promise.resolve().then(() => (init_gen_test(), gen_test_exports));
26422
- run20(rest, ctx);
27410
+ const { run: run22 } = await Promise.resolve().then(() => (init_gen_test(), gen_test_exports));
27411
+ run22(rest, ctx);
26423
27412
  } else if (sub === "preview") {
26424
- const { run: run20 } = await Promise.resolve().then(() => (init_gen_preview(), gen_preview_exports));
26425
- await run20(rest, ctx);
27413
+ const { run: run22 } = await Promise.resolve().then(() => (init_gen_preview(), gen_preview_exports));
27414
+ await run22(rest, ctx);
26426
27415
  } else {
26427
27416
  console.error("Usage: bf gen <component|test|preview> ...");
26428
27417
  process.exit(1);
@@ -26431,33 +27420,39 @@ switch (command) {
26431
27420
  }
26432
27421
  case "debug": {
26433
27422
  if (sub === "graph") {
26434
- const { run: run20 } = await Promise.resolve().then(() => (init_debug_graph(), debug_graph_exports));
26435
- await run20(rest, ctx);
27423
+ const { run: run22 } = await Promise.resolve().then(() => (init_debug_graph(), debug_graph_exports));
27424
+ await run22(rest, ctx);
26436
27425
  } else if (sub === "trace") {
26437
- const { run: run20 } = await Promise.resolve().then(() => (init_debug_trace(), debug_trace_exports));
26438
- await run20(rest, ctx);
27426
+ const { run: run22 } = await Promise.resolve().then(() => (init_debug_trace(), debug_trace_exports));
27427
+ await run22(rest, ctx);
26439
27428
  } else if (sub === "fallbacks") {
26440
- const { run: run20 } = await Promise.resolve().then(() => (init_debug_fallbacks(), debug_fallbacks_exports));
26441
- await run20(rest, ctx);
27429
+ const { run: run22 } = await Promise.resolve().then(() => (init_debug_fallbacks(), debug_fallbacks_exports));
27430
+ await run22(rest, ctx);
26442
27431
  } else if (sub === "signals") {
26443
- const { run: run20 } = await Promise.resolve().then(() => (init_debug_signals(), debug_signals_exports));
26444
- await run20(rest, ctx);
27432
+ const { run: run22 } = await Promise.resolve().then(() => (init_debug_signals(), debug_signals_exports));
27433
+ await run22(rest, ctx);
26445
27434
  } else if (sub === "events") {
26446
- const { run: run20 } = await Promise.resolve().then(() => (init_debug_events(), debug_events_exports));
26447
- await run20(rest, ctx);
27435
+ const { run: run22 } = await Promise.resolve().then(() => (init_debug_events(), debug_events_exports));
27436
+ await run22(rest, ctx);
26448
27437
  } else if (sub === "loops") {
26449
- const { run: run20 } = await Promise.resolve().then(() => (init_debug_loops(), debug_loops_exports));
26450
- await run20(rest, ctx);
27438
+ const { run: run22 } = await Promise.resolve().then(() => (init_debug_loops(), debug_loops_exports));
27439
+ await run22(rest, ctx);
27440
+ } else if (sub === "why-update") {
27441
+ const { run: run22 } = await Promise.resolve().then(() => (init_debug_why_update(), debug_why_update_exports));
27442
+ await run22(rest, ctx);
27443
+ } else if (sub === "summary") {
27444
+ const { run: run22 } = await Promise.resolve().then(() => (init_debug_summary(), debug_summary_exports));
27445
+ await run22(rest, ctx);
26451
27446
  } else {
26452
- console.error("Usage: bf debug <graph|trace|fallbacks|signals|events|loops> ...");
27447
+ console.error("Usage: bf debug <graph|trace|fallbacks|signals|events|loops|why-update|summary> ...");
26453
27448
  process.exit(1);
26454
27449
  }
26455
27450
  break;
26456
27451
  }
26457
27452
  case "meta": {
26458
27453
  if (sub === "extract") {
26459
- const { run: run20 } = await Promise.resolve().then(() => (init_meta_extract(), meta_extract_exports));
26460
- await run20(rest, ctx);
27454
+ const { run: run22 } = await Promise.resolve().then(() => (init_meta_extract(), meta_extract_exports));
27455
+ await run22(rest, ctx);
26461
27456
  } else {
26462
27457
  console.error("Usage: bf meta extract");
26463
27458
  process.exit(1);