@weasel-js/forge 1.9.3 → 1.10.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.
Files changed (55) hide show
  1. package/README.md +34 -4
  2. package/dist/{FrameController-CpcY0DNB.d.ts → FrameController-UeTY7ubF.d.ts} +26 -36
  3. package/dist/{chunk-Q2EZXJFE.js → chunk-4NM3RTYK.js} +70 -56
  4. package/dist/chunk-4NM3RTYK.js.map +1 -0
  5. package/dist/chunk-5DXUNLV2.js +6 -0
  6. package/dist/chunk-5DXUNLV2.js.map +1 -0
  7. package/dist/{chunk-GZPQMGXJ.js → chunk-DFKCLZKS.js} +5 -4
  8. package/dist/chunk-DFKCLZKS.js.map +1 -0
  9. package/dist/{chunk-CJX3W5HY.js → chunk-DKWHGSBZ.js} +4 -3
  10. package/dist/chunk-DKWHGSBZ.js.map +1 -0
  11. package/dist/{chunk-BGOHALHO.js → chunk-ENXU34PI.js} +23 -111
  12. package/dist/chunk-ENXU34PI.js.map +1 -0
  13. package/dist/chunk-FL2MUPUL.js +10 -0
  14. package/dist/chunk-FL2MUPUL.js.map +1 -0
  15. package/dist/{chunk-MGQWCPYE.js → chunk-GB6PRJET.js} +4 -5
  16. package/dist/chunk-GB6PRJET.js.map +1 -0
  17. package/dist/{chunk-JVRHBI3V.js → chunk-V4RI7GWD.js} +53 -59
  18. package/dist/chunk-V4RI7GWD.js.map +1 -0
  19. package/dist/{chunk-HRGPFQBW.js → chunk-ZAWXMUGZ.js} +6 -3
  20. package/dist/chunk-ZAWXMUGZ.js.map +1 -0
  21. package/dist/{config-BfsYw5lg.d.ts → config-Cz7ZyGF-.d.ts} +28 -5
  22. package/dist/csf/preview-api.d.ts +5 -2
  23. package/dist/csf/preview-api.js +2 -2
  24. package/dist/csf/preview-api.js.map +1 -1
  25. package/dist/frame/index.css +19 -2
  26. package/dist/frame/index.css.map +1 -1
  27. package/dist/frame/index.d.ts +2 -2
  28. package/dist/frame/index.js +7 -41
  29. package/dist/frame/index.js.map +1 -1
  30. package/dist/index.d.ts +4 -4
  31. package/dist/index.js +3 -3
  32. package/dist/index.js.map +1 -1
  33. package/dist/shell/index.css +93 -62
  34. package/dist/shell/index.css.map +1 -1
  35. package/dist/shell/index.d.ts +10 -17
  36. package/dist/shell/index.js +724 -587
  37. package/dist/shell/index.js.map +1 -1
  38. package/dist/test/index.d.ts +4 -2
  39. package/dist/test/index.js +8 -8
  40. package/dist/test/index.js.map +1 -1
  41. package/dist/test/play.d.ts +2 -4
  42. package/dist/test/play.js.map +1 -1
  43. package/dist/vite/index.d.ts +19 -2
  44. package/dist/vite/index.js +751 -162
  45. package/dist/vite/index.js.map +1 -1
  46. package/package.json +10 -5
  47. package/dist/chunk-BGOHALHO.js.map +0 -1
  48. package/dist/chunk-CJX3W5HY.js.map +0 -1
  49. package/dist/chunk-GZPQMGXJ.js.map +0 -1
  50. package/dist/chunk-HRGPFQBW.js.map +0 -1
  51. package/dist/chunk-JVRHBI3V.js.map +0 -1
  52. package/dist/chunk-KJGXJD3U.js +0 -15
  53. package/dist/chunk-KJGXJD3U.js.map +0 -1
  54. package/dist/chunk-MGQWCPYE.js.map +0 -1
  55. package/dist/chunk-Q2EZXJFE.js.map +0 -1
@@ -1,19 +1,23 @@
1
+ import { DEFAULT_TITLE } from '../chunk-5DXUNLV2.js';
1
2
  import { isGallery } from '../chunk-WEQSLPKT.js';
2
- import { storyNameFromExport, storyId } from '../chunk-HRGPFQBW.js';
3
- import { readFileSync, existsSync, mkdirSync, writeFileSync, globSync, statSync } from 'fs';
4
- import { resolve, sep, relative, matchesGlob, dirname, join, basename } from 'path';
3
+ import { PROP_ARG_TYPES_EXPORT, storyNameFromExport, storyId } from '../chunk-ZAWXMUGZ.js';
4
+ import { readFileSync, mkdtempSync, existsSync, mkdirSync, writeFileSync, globSync, statSync, watch, rmSync } from 'fs';
5
+ import { resolve, sep, relative, join, dirname, basename, matchesGlob, posix } from 'path';
5
6
  import { parse } from '@babel/parser';
7
+ import { execFile } from 'child_process';
6
8
  import { fileURLToPath } from 'url';
9
+ import { tmpdir } from 'os';
7
10
 
8
11
  // src/vite/html.ts
9
- function html(entry, mode = "serve") {
12
+ var escaped = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
13
+ function html(entry, mode = "serve", title = DEFAULT_TITLE) {
10
14
  const src = `${mode === "serve" ? "/@id/" : ""}virtual:forge/${entry}`;
11
15
  return `<!doctype html>
12
16
  <html lang="en">
13
17
  <head>
14
18
  <meta charset="utf-8" />
15
19
  <meta name="viewport" content="width=device-width, initial-scale=1" />
16
- <title>weaselforge</title>
20
+ <title>${escaped(title)}</title>
17
21
  </head>
18
22
  <body>
19
23
  <div id="root"></div>
@@ -26,13 +30,13 @@ function html(entry, mode = "serve") {
26
30
  // src/vite/build.ts
27
31
  var DIR = "node_modules/.forge";
28
32
  var PAGES = { index: "shell-entry.js", frame: "frame-entry.js" };
29
- function writePages(root) {
33
+ function writePages(root, title) {
30
34
  const dir = join(root, DIR);
31
35
  mkdirSync(dir, { recursive: true });
32
36
  return Object.fromEntries(
33
37
  Object.entries(PAGES).map(([name, entry]) => {
34
38
  const file = join(dir, `${name}.html`);
35
- writeFileSync(file, html(entry, "build"));
39
+ writeFileSync(file, html(entry, "build", title));
36
40
  return [name, file];
37
41
  })
38
42
  );
@@ -47,34 +51,6 @@ function hoistPages(bundle, emit) {
47
51
  emit({ type: "asset", fileName: `${name}.html`, source: up(asset.source) });
48
52
  }
49
53
  }
50
- var posix = (path) => path.split(sep).join("/");
51
- var dotted = (path) => /^\.{1,2}([/\\]|$)/.test(path) ? path : `./${path}`;
52
- function globDirectory(pattern) {
53
- const segments = pattern.split("/");
54
- const magic = segments.findIndex((segment) => /^!|(^|[^\\])[*?[{(]/.test(segment));
55
- return magic === -1 ? dirname(pattern) : segments.slice(0, magic).join("/");
56
- }
57
- function titleParts(parts) {
58
- const last = parts[parts.length - 1];
59
- if (last === void 0) return parts;
60
- const stripped = last.replace(/(?:[.](?:story|stories))?([.][^.]+)$/i, "");
61
- if (parts.length === 1) return [stripped];
62
- const nextToLast = parts[parts.length - 2];
63
- if (stripped && nextToLast && stripped.toLowerCase() === nextToLast.toLowerCase()) return [...parts.slice(0, -2), stripped];
64
- if (stripped && (/^(story|stories)([.][^.]+)$/i.test(last) || /^index$/i.test(stripped))) return parts.slice(0, -1);
65
- return [...parts.slice(0, -1), stripped];
66
- }
67
- var titleFrom = (importPath, directory) => titleParts(importPath.replace(directory, "").split("/").filter(Boolean)).join("/");
68
- function autoTitle(file, root, stories) {
69
- const path = posix(relative(root, file));
70
- const importPath = dotted(path);
71
- for (const pattern of stories) {
72
- if (!matchesGlob(path, pattern.replace(/^\.\//, ""))) continue;
73
- const directory = dotted(posix(relative(root, resolve(root, globDirectory(pattern))))).replace(/\/$/, "");
74
- return titleFrom(importPath, directory);
75
- }
76
- return titleFrom(importPath, ".");
77
- }
78
54
  var keyOf = (file, local) => `${file}#${local}`;
79
55
  var fileOfKey = (key) => key.slice(0, key.lastIndexOf("#"));
80
56
  var SCRIPT = /\.[cm]?[jt]sx?$/;
@@ -105,7 +81,7 @@ function createDepGraph({ root, read }) {
105
81
  reset() {
106
82
  resolutions.clear();
107
83
  },
108
- async build(entries, resolve4) {
84
+ async build(entries, resolve5) {
109
85
  const resolveFrom = (spec, importer) => {
110
86
  let byImporter = resolutions.get(importer);
111
87
  if (!byImporter) {
@@ -114,7 +90,7 @@ function createDepGraph({ root, read }) {
114
90
  }
115
91
  let pending = byImporter.get(spec);
116
92
  if (!pending) {
117
- pending = resolve4(spec, importer).then(
93
+ pending = resolve5(spec, importer).then(
118
94
  (id) => {
119
95
  const file = id?.split("?")[0];
120
96
  return file && !file.startsWith("\0") && SCRIPT.test(file) && !file.split(/[\\/]/).includes("node_modules") ? file : null;
@@ -344,6 +320,92 @@ function namesIn(node) {
344
320
  walk(node);
345
321
  return out;
346
322
  }
323
+ var runGit = (args, cwd) => new Promise((done, fail) => {
324
+ execFile(
325
+ "git",
326
+ ["-c", "core.quotepath=off", ...args],
327
+ { cwd, maxBuffer: 1 << 30 },
328
+ (error, stdout) => error ? fail(error) : done(stdout)
329
+ );
330
+ });
331
+ var LOG_FORMAT = "--format=%x00%H%x09%aI";
332
+ function fileDates(log, paths) {
333
+ const out = /* @__PURE__ */ new Map();
334
+ const names = new Map(paths.map((path) => [path, path]));
335
+ let commit = "";
336
+ let date = "";
337
+ for (const line of log.split("\n")) {
338
+ if (names.size === 0) break;
339
+ if (line.startsWith("\0")) {
340
+ [commit = "", date = ""] = line.slice(1).split(" ");
341
+ continue;
342
+ }
343
+ const [status, ...files] = line.split(" ");
344
+ const name = files.at(-1);
345
+ const path = name === void 0 ? void 0 : names.get(name);
346
+ if (!status || name === void 0 || path === void 0) continue;
347
+ out.set(path, { created: date, modified: out.get(path)?.modified ?? date, commit });
348
+ const renamed = status.startsWith("R") ? files[0] : void 0;
349
+ if (status === "A" || renamed !== void 0) names.delete(name);
350
+ if (renamed !== void 0) names.set(renamed, path);
351
+ }
352
+ return out;
353
+ }
354
+ function versionOf(named) {
355
+ const tag = named.trim().split(/[~^]/)[0] ?? "";
356
+ return /(?:^v?|@)(\d+\.\d+\.\d+[-+.\w]*)$/.exec(tag)?.[1] ?? null;
357
+ }
358
+ function packageOf(file, top) {
359
+ for (let dir = dirname(file); dir.startsWith(top); dir = dirname(dir)) {
360
+ const manifest = join(dir, "package.json");
361
+ if (existsSync(manifest)) {
362
+ try {
363
+ const name = JSON.parse(readFileSync(manifest, "utf8")).name;
364
+ if (typeof name === "string") return name;
365
+ } catch {
366
+ }
367
+ }
368
+ if (dir === top) break;
369
+ }
370
+ return null;
371
+ }
372
+ function createHistoryReader(root, git = runGit) {
373
+ let cached = null;
374
+ async function load(sources) {
375
+ const [top = "", prefix = ""] = (await git(["rev-parse", "--show-toplevel", "--show-prefix"], root)).split("\n");
376
+ const inRepo = new Map(sources.map((source) => [posix.normalize(posix.join(prefix, source)), source]));
377
+ const log = await git(["log", "--name-status", "-M", LOG_FORMAT], top);
378
+ const dates = fileDates(log, [...inRepo.keys()]);
379
+ const byPackage = /* @__PURE__ */ new Map();
380
+ for (const path of dates.keys()) {
381
+ const name = packageOf(resolve(top, path), top);
382
+ byPackage.set(name, [...byPackage.get(name) ?? [], path]);
383
+ }
384
+ const history = {};
385
+ for (const [name, paths] of byPackage) {
386
+ const refs = ["v[0-9]*", "[0-9]*", ...name === null ? [] : [`${name}@[0-9]*`]].map((ref) => `--refs=${ref}`);
387
+ const commits = paths.map((path) => dates.get(path).commit);
388
+ const named = (await git(["name-rev", "--tags", "--name-only", ...refs, ...commits], top)).split("\n");
389
+ paths.forEach((path, i) => {
390
+ const { created, modified } = dates.get(path);
391
+ history[inRepo.get(path)] = { created, modified, introduced: versionOf(named[i] ?? "") };
392
+ });
393
+ }
394
+ return history;
395
+ }
396
+ return {
397
+ async read(sources) {
398
+ try {
399
+ const head = (await git(["rev-parse", "HEAD"], root)).trim();
400
+ const key = [...sources].sort().join("\n");
401
+ if (cached?.head !== head || cached.sources !== key) cached = { head, sources: key, history: await load(sources) };
402
+ return cached.history;
403
+ } catch {
404
+ return {};
405
+ }
406
+ }
407
+ };
408
+ }
347
409
  function parseFile(code, file) {
348
410
  const plugins = ["typescript", "decorators-legacy", .../\.[jt]sx$/.test(file) ? ["jsx"] : []];
349
411
  return parse(code, { sourceType: "module", plugins }).program;
@@ -365,11 +427,59 @@ function calleeOf(node, bindings, depth = 0) {
365
427
  return void 0;
366
428
  }
367
429
  }
430
+ var LITERALS = /* @__PURE__ */ new Set([
431
+ "StringLiteral",
432
+ "NumericLiteral",
433
+ "BooleanLiteral",
434
+ "NullLiteral",
435
+ "BigIntLiteral",
436
+ "RegExpLiteral",
437
+ "TemplateLiteral",
438
+ "ArrayExpression",
439
+ "ArrowFunctionExpression",
440
+ "FunctionExpression",
441
+ "FunctionDeclaration",
442
+ "ClassExpression",
443
+ "JSXElement",
444
+ "JSXFragment"
445
+ ]);
446
+ function isLiteralValue(node, bindings, depth = 0) {
447
+ if (!node || depth > 16) return false;
448
+ switch (node.type) {
449
+ case "TSSatisfiesExpression":
450
+ case "TSAsExpression":
451
+ case "TSTypeAssertion":
452
+ case "TSNonNullExpression":
453
+ case "ParenthesizedExpression":
454
+ return isLiteralValue(node.expression, bindings, depth + 1);
455
+ case "Identifier":
456
+ return isLiteralValue(bindings.get(node.name), bindings, depth + 1);
457
+ case "ObjectExpression":
458
+ return node.properties.every((p) => p.type !== "SpreadElement");
459
+ default:
460
+ return LITERALS.has(node.type);
461
+ }
462
+ }
368
463
  function stringLiteral(node) {
369
464
  if (node.type === "StringLiteral") return node.value;
370
465
  if (node.type === "TemplateLiteral" && node.expressions.length === 0) return node.quasis.map((q) => q.value.cooked ?? q.value.raw).join("");
371
466
  return void 0;
372
467
  }
468
+ function stringOf(node, bindings, depth = 0) {
469
+ if (!node || depth > 16) return void 0;
470
+ switch (node.type) {
471
+ case "TSSatisfiesExpression":
472
+ case "TSAsExpression":
473
+ case "TSTypeAssertion":
474
+ case "TSNonNullExpression":
475
+ case "ParenthesizedExpression":
476
+ return stringOf(node.expression, bindings, depth + 1);
477
+ case "Identifier":
478
+ return stringOf(bindings.get(node.name), bindings, depth + 1);
479
+ default:
480
+ return stringLiteral(node);
481
+ }
482
+ }
373
483
  function isDefaultExport(stmt) {
374
484
  if (stmt.type === "ExportDefaultDeclaration") return true;
375
485
  return stmt.type === "ExportNamedDeclaration" && stmt.specifiers.some((s) => s.type === "ExportSpecifier" && exportedName(s.exported) === "default");
@@ -416,10 +526,58 @@ function stringProp(obj, key) {
416
526
  return value?.type === "StringLiteral" ? value.value : void 0;
417
527
  }
418
528
 
529
+ // src/vite/nativeExports.ts
530
+ var isFn = (node) => node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression" || node?.type === "FunctionDeclaration";
531
+ var foreignFrom = (known) => ({
532
+ value: (name) => known.values[name],
533
+ call: (name) => known.calls[name],
534
+ reexport: (name) => known.reexports[name]
535
+ });
536
+ function returnsStory(statements, scope, depth) {
537
+ return statements.some((stmt) => {
538
+ if (stmt.type === "ReturnStatement") return mayBeStory(stmt.argument ?? void 0, scope, depth);
539
+ if (stmt.type === "BlockStatement") return returnsStory(stmt.body, scope, depth);
540
+ if (stmt.type !== "IfStatement") return false;
541
+ return returnsStory([stmt.consequent, ...stmt.alternate ? [stmt.alternate] : []], scope, depth);
542
+ });
543
+ }
544
+ function mayReturnStory(fn, scope, depth = 0) {
545
+ return fn.body.type === "BlockStatement" ? returnsStory(fn.body.body, scope, depth + 1) : mayBeStory(fn.body, scope, depth + 1);
546
+ }
547
+ function mayBeStory(node, scope, depth = 0) {
548
+ if (!node) return true;
549
+ if (depth > 16) return false;
550
+ switch (node.type) {
551
+ case "TSSatisfiesExpression":
552
+ case "TSAsExpression":
553
+ case "TSTypeAssertion":
554
+ case "TSNonNullExpression":
555
+ case "ParenthesizedExpression":
556
+ return mayBeStory(node.expression, scope, depth + 1);
557
+ case "Identifier":
558
+ return scope.bindings.has(node.name) ? mayBeStory(scope.bindings.get(node.name), scope, depth + 1) : scope.foreign?.value(node.name) ?? true;
559
+ case "CallExpression": {
560
+ if (node.callee.type !== "Identifier") return false;
561
+ if (scope.wrappers.has(node.callee.name)) return true;
562
+ const fn = scope.bindings.get(node.callee.name);
563
+ if (isFn(fn)) return mayReturnStory(fn, scope, depth);
564
+ return !scope.bindings.has(node.callee.name) && (scope.foreign?.call(node.callee.name) ?? false);
565
+ }
566
+ case "ConditionalExpression":
567
+ return mayBeStory(node.consequent, scope, depth + 1) || mayBeStory(node.alternate, scope, depth + 1);
568
+ case "LogicalExpression":
569
+ return mayBeStory(node.left, scope, depth + 1) || mayBeStory(node.right, scope, depth + 1);
570
+ case "ObjectExpression":
571
+ return node.properties.some((p) => p.type === "SpreadElement" && mayBeStory(p.argument, scope, depth + 1));
572
+ default:
573
+ return !isLiteralValue(node, scope.bindings);
574
+ }
575
+ }
576
+
419
577
  // src/vite/indexFile.ts
420
- function indexFile(code, file, autoTitle2, wrappers = /* @__PURE__ */ new Set()) {
578
+ function indexFile(code, file, autoTitle2, wrappers = /* @__PURE__ */ new Set(), foreign) {
421
579
  try {
422
- return indexProgram(parseFile(code, file), file, autoTitle2, wrappers);
580
+ return indexProgram(parseFile(code, file), file, autoTitle2, wrappers, foreign);
423
581
  } catch (err) {
424
582
  throw new Error(`${file}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
425
583
  }
@@ -464,11 +622,13 @@ function scan(program, extra) {
464
622
  const decl = stmt.type === "ExportNamedDeclaration" ? stmt.declaration : stmt;
465
623
  if (decl?.type === "VariableDeclaration") {
466
624
  for (const d of decl.declarations) if (d.id.type === "Identifier" && d.init) bindings.set(d.id.name, d.init);
625
+ } else if (decl?.type === "FunctionDeclaration" && decl.id) {
626
+ bindings.set(decl.id.name, decl);
467
627
  }
468
628
  }
469
629
  return { bindings, wrappers };
470
630
  }
471
- function indexProgram(program, file, autoTitle2, extra) {
631
+ function indexProgram(program, file, autoTitle2, extra, foreign) {
472
632
  const { bindings, wrappers } = scan(program, extra);
473
633
  let metaNode;
474
634
  let metaDoc;
@@ -486,7 +646,7 @@ function indexProgram(program, file, autoTitle2, extra) {
486
646
  if (d.id.type === "Identifier") exported.push({ name: d.id.name, node: d.init ?? void 0, doc: docOf(d) ?? doc });
487
647
  }
488
648
  } else if (decl?.type === "FunctionDeclaration" && decl.id) {
489
- exported.push({ name: decl.id.name, node: void 0, doc });
649
+ exported.push({ name: decl.id.name, node: decl, doc });
490
650
  }
491
651
  for (const spec of stmt.specifiers) {
492
652
  if (spec.type !== "ExportSpecifier" || spec.exportKind === "type") continue;
@@ -495,12 +655,13 @@ function indexProgram(program, file, autoTitle2, extra) {
495
655
  if (name === "default") {
496
656
  metaNode = node;
497
657
  metaDoc ??= doc;
498
- } else exported.push({ name, node, doc });
658
+ } else if (stmt.source) exported.push({ name, node, doc, from: true });
659
+ else exported.push({ name, node: node ?? (spec.local.type === "Identifier" ? spec.local : void 0), doc });
499
660
  }
500
661
  }
501
662
  if (!metaNode && !program.body.some(isDefaultExport)) return [];
502
663
  const meta = objectOf(metaNode, bindings, wrappers);
503
- const title = stringProp(meta, "title") ?? autoTitle2;
664
+ const title = stringOf(prop(meta, "title"), bindings) ?? autoTitle2;
504
665
  const include = storyFilter(meta, "includeStories");
505
666
  const exclude = storyFilter(meta, "excludeStories");
506
667
  const componentNode = prop(meta, "component");
@@ -508,7 +669,7 @@ function indexProgram(program, file, autoTitle2, extra) {
508
669
  const metaIsolate = isolateOf(meta, bindings, wrappers, "default");
509
670
  const metaTags = tagsOf(meta);
510
671
  const native = wrappers.has(calleeOf(metaNode, bindings) ?? "");
511
- return exported.filter(({ name }) => name !== "__namedExportsOrder").filter(({ name }) => (!include || include(name)) && (!exclude || !exclude(name))).map(({ name, node, doc }) => {
672
+ return exported.filter(({ name }) => name !== "__namedExportsOrder").filter(({ name }) => (!include || include(name)) && (!exclude || !exclude(name))).filter((e) => !native || (e.from ? foreign?.reexport(e.name) ?? true : mayBeStory(e.node, { bindings, wrappers, foreign }))).map(({ name, node, doc }) => {
512
673
  const spec = objectOf(node, bindings, wrappers);
513
674
  const isolate = isolateOf(spec, bindings, wrappers, name) ?? metaIsolate;
514
675
  const tags = combineTags(metaTags, tagsOf(spec));
@@ -569,36 +730,105 @@ function storyFilter(obj, key) {
569
730
  }
570
731
  return null;
571
732
  }
572
- function ownEntries(realms) {
573
- const here = fileURLToPath(import.meta.url);
574
- for (let dir = dirname(here); dir !== dirname(dir); dir = dirname(dir)) {
575
- const manifest = join(dir, "package.json");
576
- if (!existsSync(manifest) || JSON.parse(readFileSync(manifest, "utf8")).name !== "@weasel-js/forge") {
577
- continue;
733
+ function storyGlobOf(path, stories) {
734
+ return stories.find((pattern) => matchesGlob(path, pattern.replace(/^\.\//, "")));
735
+ }
736
+
737
+ // src/vite/autoTitle.ts
738
+ var posix2 = (path) => path.split(sep).join("/");
739
+ var dotted = (path) => /^\.{1,2}([/\\]|$)/.test(path) ? path : `./${path}`;
740
+ function globDirectory(pattern) {
741
+ const segments = pattern.split("/");
742
+ const magic = segments.findIndex((segment) => /^!|(^|[^\\])[*?[{(]/.test(segment));
743
+ return magic === -1 ? dirname(pattern) : segments.slice(0, magic).join("/");
744
+ }
745
+ function titleParts(parts) {
746
+ const last = parts[parts.length - 1];
747
+ if (last === void 0) return parts;
748
+ const stripped = last.replace(/(?:[.](?:story|stories))?([.][^.]+)$/i, "");
749
+ if (parts.length === 1) return [stripped];
750
+ const nextToLast = parts[parts.length - 2];
751
+ if (stripped && nextToLast && stripped.toLowerCase() === nextToLast.toLowerCase()) return [...parts.slice(0, -2), stripped];
752
+ if (stripped && (/^(story|stories)([.][^.]+)$/i.test(last) || /^index$/i.test(stripped))) return parts.slice(0, -1);
753
+ return [...parts.slice(0, -1), stripped];
754
+ }
755
+ var titleFrom = (importPath, directory) => titleParts(importPath.replace(directory, "").split("/").filter(Boolean)).join("/");
756
+ function autoTitle(file, root, stories) {
757
+ const path = posix2(relative(root, file));
758
+ const importPath = dotted(path);
759
+ const pattern = storyGlobOf(path, stories);
760
+ if (pattern === void 0) return titleFrom(importPath, ".");
761
+ const directory = dotted(posix2(relative(root, resolve(root, globDirectory(pattern))))).replace(/\/$/, "");
762
+ return titleFrom(importPath, directory);
763
+ }
764
+
765
+ // src/vite/foreignStories.ts
766
+ var MAX_DEPTH = 8;
767
+ async function settled(read, ask) {
768
+ const known = { values: {}, calls: {}, reexports: {} };
769
+ const asked = /* @__PURE__ */ new Set();
770
+ for (; ; ) {
771
+ const fresh = [];
772
+ const answer = (kind) => (name) => {
773
+ if (!asked.has(`${kind}:${name}`)) fresh.push([kind, name]);
774
+ return known[kind][name];
775
+ };
776
+ const result = read({ value: answer("values"), call: answer("calls"), reexport: answer("reexports") });
777
+ if (fresh.length === 0) return { result, known };
778
+ for (const [kind, name] of fresh) {
779
+ asked.add(`${kind}:${name}`);
780
+ const found = await ask(kind, name);
781
+ if (found !== void 0) known[kind][name] = found;
578
782
  }
579
- const source = !relative(join(dir, "src"), here).startsWith("..");
580
- return realms.map((realm) => join(dir, source ? `src/${realm}/index.ts` : `dist/${realm}/index.js`));
581
783
  }
582
- return [];
583
784
  }
584
- var SHIMS = [
585
- [/^@?storybook\/preview-api$/, "@weasel-js/forge/preview-api"],
586
- [/^@?storybook\/test$/, "@weasel-js/forge/play"]
587
- ];
588
- function storybookShims() {
589
- return {
590
- name: "weaselforge:storybook-shims",
591
- enforce: "pre",
592
- resolveId(id) {
593
- const hit = SHIMS.find(([pattern]) => pattern.test(id));
594
- if (!hit) return void 0;
595
- return this.resolve(hit[1], fileURLToPath(import.meta.url), { skipSelf: true });
785
+ async function foreignStories(file, wrappers, own, trace, read) {
786
+ const answers = /* @__PURE__ */ new Map();
787
+ const exported = async (at, name, called, depth) => {
788
+ const key = await trace.declaring(at, name);
789
+ if (!key) return void 0;
790
+ if (own.has(key)) return called && own.get(key) === "story";
791
+ const memo = `${key}:${called}`;
792
+ if (!answers.has(memo)) {
793
+ answers.set(memo, depth > MAX_DEPTH ? Promise.resolve(void 0) : declared(key, called, depth));
596
794
  }
795
+ return answers.get(memo);
597
796
  };
797
+ const declared = async (key, called, depth) => {
798
+ const at = key.slice(0, key.lastIndexOf("#"));
799
+ const local = key.slice(at.length + 1);
800
+ let scope;
801
+ try {
802
+ scope = scan(parseFile(read(at), at), /* @__PURE__ */ new Set());
803
+ } catch {
804
+ return void 0;
805
+ }
806
+ const node = scope.bindings.get(local);
807
+ if (!node) return void 0;
808
+ const { result } = await settled(
809
+ (foreign) => called ? isFn(node) && mayReturnStory(node, { ...scope, foreign }) : mayBeStory(node, { ...scope, foreign }),
810
+ asking(at, depth + 1)
811
+ );
812
+ return result;
813
+ };
814
+ const asking = (at, depth) => async (kind, name) => {
815
+ const mod = trace.info(at);
816
+ const exp = mod?.exports.get(name);
817
+ const from = kind === "reexports" ? exp?.kind === "from" ? exp : void 0 : mod?.imports.get(name);
818
+ if (!from || from.imported === "*") return void 0;
819
+ const target = await trace.target(from.spec, at);
820
+ return target ? exported(target, from.imported, kind === "calls", depth) : void 0;
821
+ };
822
+ try {
823
+ const program = parseFile(read(file), file);
824
+ return (await settled((foreign) => void indexProgram(program, file, "", wrappers, foreign), asking(file, 0))).known;
825
+ } catch {
826
+ return { values: {}, calls: {}, reexports: {} };
827
+ }
598
828
  }
599
829
 
600
- // src/vite/wrappers.ts
601
- function createWrapperResolver(read) {
830
+ // src/vite/trace.ts
831
+ function createTracer(read) {
602
832
  const modules = /* @__PURE__ */ new Map();
603
833
  const info = (file) => {
604
834
  if (!modules.has(file)) {
@@ -612,18 +842,20 @@ function createWrapperResolver(read) {
612
842
  }
613
843
  return modules.get(file) ?? null;
614
844
  };
615
- return async (file, callees, resolve4) => {
616
- const reads = /* @__PURE__ */ new Set();
617
- const wrappers = /* @__PURE__ */ new Set();
618
- let unresolved = false;
619
- if (callees.length === 0) return { wrappers, reads, unresolved };
620
- const target = async (spec, importer) => {
621
- const id = (await resolve4(spec, importer).catch(() => null))?.split("?")[0];
622
- if (!id) unresolved = true;
623
- return id && !id.startsWith("\0") ? id : null;
845
+ return (resolve5) => {
846
+ const trace = {
847
+ reads: /* @__PURE__ */ new Set(),
848
+ unresolved: false,
849
+ info,
850
+ async target(spec, importer) {
851
+ const id = (await resolve5(spec, importer).catch(() => null))?.split("?")[0];
852
+ if (!id) trace.unresolved = true;
853
+ return id && !id.startsWith("\0") ? id : null;
854
+ },
855
+ declaring: (at, name) => declaring(at, name, 0)
624
856
  };
625
- const declaring = async (at, name, depth = 0) => {
626
- reads.add(at);
857
+ const declaring = async (at, name, depth) => {
858
+ trace.reads.add(at);
627
859
  const mod = info(at);
628
860
  if (!mod || depth > 32) return null;
629
861
  const exp = mod.exports.get(name);
@@ -631,39 +863,402 @@ function createWrapperResolver(read) {
631
863
  const imp = mod.imports.get(exp.local);
632
864
  if (!imp) return `${at}#${exp.local}`;
633
865
  if (imp.imported === "*") return null;
634
- const next = await target(imp.spec, at);
866
+ const next = await trace.target(imp.spec, at);
635
867
  return next ? declaring(next, imp.imported, depth + 1) : null;
636
868
  }
637
869
  if (exp?.kind === "from") {
638
- const next = await target(exp.spec, at);
870
+ const next = await trace.target(exp.spec, at);
639
871
  return next ? declaring(next, exp.imported, depth + 1) : null;
640
872
  }
641
873
  if (name === "default") return null;
642
874
  for (const spec of mod.stars) {
643
- const next = await target(spec, at);
875
+ const next = await trace.target(spec, at);
644
876
  const found = next ? await declaring(next, name, depth + 1) : null;
645
877
  if (found) return found;
646
878
  }
647
879
  return null;
648
880
  };
649
- const forge2 = await target("@weasel-js/forge", file);
650
- if (!forge2) return { wrappers, reads, unresolved };
651
- const own = new Set(
652
- (await Promise.all(["meta", "story"].map((name) => declaring(forge2, name)))).filter((key) => key !== null)
881
+ return trace;
882
+ };
883
+ }
884
+
885
+ // src/vite/wrappers.ts
886
+ var NO_FOREIGN = { values: {}, calls: {}, reexports: {} };
887
+ function sameResolution(a, b) {
888
+ const key = (r) => JSON.stringify([[...r?.wrappers ?? []].sort(), r?.foreign ?? NO_FOREIGN]);
889
+ return key(a) === key(b);
890
+ }
891
+ function createWrapperResolver(read) {
892
+ const tracer = createTracer(read);
893
+ return async (file, callees, resolve5) => {
894
+ const trace = tracer(resolve5);
895
+ const wrappers = /* @__PURE__ */ new Set();
896
+ const direct = [...trace.info(file)?.imports.values() ?? []].some((imp) => imp.spec === "@weasel-js/forge");
897
+ if (callees.length === 0 && !direct) return { wrappers, foreign: NO_FOREIGN, reads: trace.reads, unresolved: false };
898
+ const forge2 = await trace.target("@weasel-js/forge", file);
899
+ const own = /* @__PURE__ */ new Map();
900
+ if (forge2) {
901
+ for (const name of ["meta", "story"]) {
902
+ const key = await trace.declaring(forge2, name);
903
+ if (key) own.set(key, name);
904
+ }
905
+ for (const callee of callees) {
906
+ const from = await trace.target(callee.spec, file);
907
+ const key = from ? await trace.declaring(from, callee.imported) : null;
908
+ if (key && own.has(key)) wrappers.add(callee.local);
909
+ }
910
+ }
911
+ const foreign = forge2 ? await foreignStories(file, wrappers, own, trace, read) : NO_FOREIGN;
912
+ return { wrappers, foreign, reads: trace.reads, unresolved: trace.unresolved };
913
+ };
914
+ }
915
+
916
+ // src/vite/indexStory.ts
917
+ function indexStory(code, file, source, resolved) {
918
+ const foreign = resolved ? foreignFrom(resolved.foreign) : void 0;
919
+ return indexFile(code, file, autoTitle(file, source.root, source.stories), resolved?.wrappers, foreign);
920
+ }
921
+ async function indexStoryResolved(code, file, source, resolve5, decide = createWrapperResolver((path) => readFileSync(path, "utf8"))) {
922
+ const resolution = await decide(file, foreignCallees(code, file), resolve5);
923
+ return { entries: indexStory(code, file, source, resolution), resolution };
924
+ }
925
+ function ownEntries(realms) {
926
+ const here = fileURLToPath(import.meta.url);
927
+ for (let dir = dirname(here); dir !== dirname(dir); dir = dirname(dir)) {
928
+ const manifest = join(dir, "package.json");
929
+ if (!existsSync(manifest) || JSON.parse(readFileSync(manifest, "utf8")).name !== "@weasel-js/forge") {
930
+ continue;
931
+ }
932
+ const source = !relative(join(dir, "src"), here).startsWith("..");
933
+ return realms.map((realm) => join(dir, source ? `src/${realm}/index.ts` : `dist/${realm}/index.js`));
934
+ }
935
+ return [];
936
+ }
937
+
938
+ // src/vite/entries.ts
939
+ function shellEntry(base, stories) {
940
+ return `import { mountWorkshop } from '@weasel-js/forge/shell';
941
+ import '@weasel-js/labkit/styles.css';
942
+ import '@weasel-js/forge/shell.css';
943
+ import index from 'virtual:forge/index.js';
944
+ import importers from 'virtual:forge/importers.js';
945
+ import config from 'virtual:forge/shell-config.js';
946
+ import setup from 'virtual:forge/frame-config.js';
947
+
948
+ const workshop = mountWorkshop({
949
+ index,
950
+ importers,
951
+ setup,
952
+ config,
953
+ frameUrl: ${JSON.stringify(`${base}frame.html`)},
954
+ stories: ${JSON.stringify(stories)},
955
+ });
956
+ import.meta.hot?.on('forge:index', (next) => workshop.setIndex(next));
957
+ // Read from source on first request, so the graph costs nothing until the page is up.
958
+ import('virtual:forge/deps.js').then((m) => workshop.setDependencies(m.default));
959
+ import.meta.hot?.on('forge:deps', (next) => workshop.setDependencies(next));
960
+ // An edit to a story file, or to a module one imports, reaches here as an update of the importers module, whose
961
+ // fresh import() URLs carry the new timestamps; the files it reaches arrive just before, so the reload waits for
962
+ // the importers that can fetch them.
963
+ const stale = new Set();
964
+ import.meta.hot?.on('forge:story', ({ file }) => stale.add(file));
965
+ import.meta.hot?.accept('virtual:forge/importers.js', (next) => {
966
+ if (!next) return;
967
+ workshop.setImporters(next.default);
968
+ for (const file of stale) workshop.reloadStory(file);
969
+ stale.clear();
970
+ });
971
+ `;
972
+ }
973
+ function frameEntry(styles) {
974
+ return `import { mountFrame } from '@weasel-js/forge/frame';
975
+ import '@weasel-js/forge/frame.css';
976
+ ${styles.map((path) => `import ${JSON.stringify(path)};
977
+ `).join("")}import index from 'virtual:forge/index.js';
978
+ import importers from 'virtual:forge/importers.js';
979
+ import setup from 'virtual:forge/frame-config.js';
980
+
981
+ mountFrame({ index, importers, setup });
982
+ // The importers module is shared with the workshop page, which accepts a story edit in place; a frame document
983
+ // showing the old module reloads instead, and without this the edit would dead-end here and reload every page.
984
+ import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
985
+ `;
986
+ }
987
+ var PLATFORM = /[\\/]node_modules[\\/](@types[\\/]react(-dom)?|typescript)[\\/]/;
988
+ async function createPropReader(root, stories, { skip } = {}) {
989
+ const skipped = (file) => PLATFORM.test(file) || skip?.(file) === true;
990
+ let tsm;
991
+ try {
992
+ tsm = (await import('typescript')).default;
993
+ } catch {
994
+ return null;
995
+ }
996
+ const { sys } = tsm;
997
+ const configFile = tsm.findConfigFile(root, sys.fileExists);
998
+ const read = configFile ? tsm.readConfigFile(configFile, sys.readFile) : void 0;
999
+ const parsed = read?.config ? tsm.parseJsonConfigFileContent(read.config, { ...sys, readDirectory: () => [] }, dirname(configFile)) : void 0;
1000
+ const options = {
1001
+ jsx: tsm.JsxEmit.ReactJSX,
1002
+ moduleResolution: tsm.ModuleResolutionKind.Bundler,
1003
+ module: tsm.ModuleKind.ESNext,
1004
+ target: tsm.ScriptTarget.ESNext,
1005
+ strict: true,
1006
+ ...parsed?.options,
1007
+ allowJs: true,
1008
+ noEmit: true,
1009
+ skipLibCheck: true
1010
+ };
1011
+ const host = {
1012
+ getScriptFileNames: () => [...stories()],
1013
+ getScriptVersion: (file) => String(sys.getModifiedTime?.(file)?.getTime() ?? 0),
1014
+ getScriptSnapshot: (file) => {
1015
+ const text = sys.readFile(file);
1016
+ return text === void 0 ? void 0 : tsm.ScriptSnapshot.fromString(text);
1017
+ },
1018
+ getCurrentDirectory: () => root,
1019
+ getCompilationSettings: () => options,
1020
+ getDefaultLibFileName: tsm.getDefaultLibFilePath,
1021
+ fileExists: sys.fileExists,
1022
+ readFile: sys.readFile,
1023
+ readDirectory: sys.readDirectory,
1024
+ directoryExists: sys.directoryExists,
1025
+ getDirectories: sys.getDirectories
1026
+ };
1027
+ const service = tsm.createLanguageService(host, tsm.createDocumentRegistry());
1028
+ return {
1029
+ read(file) {
1030
+ const program = service.getProgram();
1031
+ const source = program?.getSourceFile(file);
1032
+ if (!program || !source) return null;
1033
+ const checker = program.getTypeChecker();
1034
+ const props = componentProps(tsm, checker, source);
1035
+ return props ? propTable(tsm, checker, props, source, skipped) : null;
1036
+ }
1037
+ };
1038
+ }
1039
+ function componentProps(tsm, checker, source) {
1040
+ const module = checker.getSymbolAtLocation(source);
1041
+ const exported = module && checker.tryGetMemberInModuleExports("default", module);
1042
+ if (!exported) return null;
1043
+ const meta = checker.getTypeOfSymbolAtLocation(
1044
+ exported.flags & tsm.SymbolFlags.Alias ? checker.getAliasedSymbol(exported) : exported,
1045
+ source
1046
+ );
1047
+ const named = meta.getProperty("component");
1048
+ if (!named) return null;
1049
+ const component = checker.getNonNullableType(checker.getTypeOfSymbolAtLocation(named, source));
1050
+ for (const part of component.isUnion() ? component.types : [component]) {
1051
+ const [call] = part.getCallSignatures();
1052
+ const [param] = call?.getParameters() ?? [];
1053
+ if (call) return param ? checker.getTypeOfSymbolAtLocation(param, source) : null;
1054
+ const instance = part.getConstructSignatures()[0]?.getReturnType();
1055
+ const held = instance?.getProperty("props");
1056
+ if (held) return checker.getTypeOfSymbolAtLocation(held, source);
1057
+ }
1058
+ return null;
1059
+ }
1060
+ function propTable(tsm, checker, props, at, skipped) {
1061
+ const argTypes = {};
1062
+ const sources = /* @__PURE__ */ new Set();
1063
+ for (const prop2 of checker.getPropertiesOfType(checker.getApparentType(props))) {
1064
+ const files = (prop2.getDeclarations() ?? []).map((declaration) => declaration.getSourceFile().fileName);
1065
+ if (files.length > 0 && files.every(skipped)) continue;
1066
+ const argType = controlFor(
1067
+ tsm,
1068
+ checker.getNonNullableType(checker.getTypeOfSymbolAtLocation(prop2, at)),
1069
+ () => writtenOrder(tsm, checker, prop2)
1070
+ );
1071
+ if (!argType) continue;
1072
+ const description = tsm.displayPartsToString(prop2.getDocumentationComment(checker)).replace(/\s+/g, " ").trim();
1073
+ argTypes[prop2.getName()] = description ? { ...argType, description } : argType;
1074
+ for (const file of files) if (!/[\\/]node_modules[\\/]/.test(file)) sources.add(file);
1075
+ }
1076
+ return { argTypes, sources: [...sources] };
1077
+ }
1078
+ function controlFor(tsm, type, written) {
1079
+ const { TypeFlags } = tsm;
1080
+ const parts = type.isUnion() ? type.types : [type];
1081
+ const every = (flags) => parts.every((part) => part.flags & flags);
1082
+ if (every(TypeFlags.BooleanLike)) return { control: { type: "boolean" } };
1083
+ if (type.flags & TypeFlags.String) return { control: { type: "text" } };
1084
+ if (type.flags & TypeFlags.Number) return { control: { type: "number" } };
1085
+ if (parts.length > 1 && every(TypeFlags.StringLiteral)) {
1086
+ const options = parts.map((part) => part.value);
1087
+ const order = written();
1088
+ const same = order?.length === options.length && options.every((option) => order.includes(option));
1089
+ return { options: same ? order : options };
1090
+ }
1091
+ return null;
1092
+ }
1093
+ function writtenOrder(tsm, checker, prop2) {
1094
+ const [declaration, ...others] = prop2.getDeclarations() ?? [];
1095
+ if (!declaration || others.length > 0) return null;
1096
+ if (!tsm.isPropertySignature(declaration) && !tsm.isPropertyDeclaration(declaration)) return null;
1097
+ const seen = /* @__PURE__ */ new Set();
1098
+ const literals = (node) => {
1099
+ if (!node || seen.has(node)) return null;
1100
+ seen.add(node);
1101
+ if (tsm.isParenthesizedTypeNode(node)) return literals(node.type);
1102
+ if (tsm.isLiteralTypeNode(node)) {
1103
+ if (node.literal.kind === tsm.SyntaxKind.NullKeyword) return [];
1104
+ return tsm.isStringLiteralLike(node.literal) ? [node.literal.text] : null;
1105
+ }
1106
+ if (node.kind === tsm.SyntaxKind.UndefinedKeyword) return [];
1107
+ if (tsm.isUnionTypeNode(node)) {
1108
+ const parts = node.types.map(literals);
1109
+ return parts.every((part) => part !== null) ? [...new Set(parts.flat())] : null;
1110
+ }
1111
+ if (tsm.isTypeReferenceNode(node) && !node.typeArguments) {
1112
+ const named = checker.getSymbolAtLocation(node.typeName);
1113
+ const target = named && named.flags & tsm.SymbolFlags.Alias ? checker.getAliasedSymbol(named) : named;
1114
+ const [alias, ...rest] = target?.getDeclarations() ?? [];
1115
+ if (alias && rest.length === 0 && tsm.isTypeAliasDeclaration(alias) && !alias.typeParameters) {
1116
+ return literals(alias.type);
1117
+ }
1118
+ }
1119
+ return null;
1120
+ };
1121
+ return literals(declaration.type);
1122
+ }
1123
+
1124
+ // src/vite/propControls.ts
1125
+ function propControls(stories, options = {}) {
1126
+ let root = process.cwd();
1127
+ let reader = null;
1128
+ return {
1129
+ name: "weaselforge:prop-controls",
1130
+ configResolved(config) {
1131
+ root = config.root;
1132
+ reader = null;
1133
+ },
1134
+ async transform(code, id) {
1135
+ if (!stories().includes(id)) return void 0;
1136
+ const props = await (reader ??= createPropReader(root, stories, options));
1137
+ let table = null;
1138
+ try {
1139
+ table = props?.read(id) ?? null;
1140
+ } catch (err) {
1141
+ this.warn(`could not read the props for ${id}: ${err instanceof Error ? err.message : String(err)}`);
1142
+ }
1143
+ if (!table || Object.keys(table.argTypes).length === 0) return void 0;
1144
+ for (const source of table.sources) this.addWatchFile(source);
1145
+ return { code: `${code}
1146
+ export const ${PROP_ARG_TYPES_EXPORT} = ${JSON.stringify(table.argTypes)};
1147
+ `, map: null };
1148
+ }
1149
+ };
1150
+ }
1151
+
1152
+ // src/vite/servePages.ts
1153
+ var PAGES2 = { "/": "shell-entry.js", "/index.html": "shell-entry.js", "/frame.html": "frame-entry.js" };
1154
+ function servePages(server, base, title) {
1155
+ server.middlewares.use((req, res, next) => {
1156
+ const url = req.url ?? "/";
1157
+ const path = url.split("?")[0] ?? "/";
1158
+ const prefix = base.endsWith("/") ? base.slice(0, -1) : base;
1159
+ const page = path === prefix ? "/" : path.startsWith(`${prefix}/`) ? path.slice(prefix.length) : null;
1160
+ const entry = req.method === "GET" && page !== null ? PAGES2[page] : void 0;
1161
+ if (!entry) return next();
1162
+ server.transformIndexHtml(url, html(entry, "serve", title())).then(
1163
+ (doc) => {
1164
+ res.statusCode = 200;
1165
+ res.setHeader("Content-Type", "text/html");
1166
+ res.end(doc);
1167
+ },
1168
+ (err) => next(err)
653
1169
  );
654
- for (const callee of callees) {
655
- const from = await target(callee.spec, file);
656
- const key = from ? await declaring(from, callee.imported) : null;
657
- if (key && own.has(key)) wrappers.add(callee.local);
1170
+ });
1171
+ }
1172
+
1173
+ // src/vite/shellTitle.ts
1174
+ function shellTitle(code, file) {
1175
+ let program;
1176
+ try {
1177
+ program = parseFile(code, file);
1178
+ } catch {
1179
+ return void 0;
1180
+ }
1181
+ const bindings = /* @__PURE__ */ new Map();
1182
+ let config;
1183
+ for (const stmt of program.body) {
1184
+ if (stmt.type === "VariableDeclaration") {
1185
+ for (const d of stmt.declarations) if (d.id.type === "Identifier" && d.init) bindings.set(d.id.name, d.init);
1186
+ }
1187
+ if (stmt.type === "ExportDefaultDeclaration") config = stmt.declaration;
1188
+ if (stmt.type !== "ExportNamedDeclaration" || stmt.source) continue;
1189
+ for (const spec of stmt.specifiers) {
1190
+ if (spec.type === "ExportSpecifier" && exportedName(spec.exported) === "default") config = spec.local;
1191
+ }
1192
+ }
1193
+ const wrapper = calleeOf(config, bindings);
1194
+ return stringOf(prop(objectOf(config, bindings, new Set(wrapper ? [wrapper] : [])), "title"), bindings);
1195
+ }
1196
+ var SHIMS = [
1197
+ [/^@?storybook\/preview-api$/, "@weasel-js/forge/preview-api"],
1198
+ [/^@?storybook\/test$/, "@weasel-js/forge/play"]
1199
+ ];
1200
+ function storybookShims() {
1201
+ return {
1202
+ name: "weaselforge:storybook-shims",
1203
+ enforce: "pre",
1204
+ resolveId(id) {
1205
+ const hit = SHIMS.find(([pattern]) => pattern.test(id));
1206
+ if (!hit) return void 0;
1207
+ return this.resolve(hit[1], fileURLToPath(import.meta.url), { skipSelf: true });
658
1208
  }
659
- return { wrappers, reads, unresolved };
660
1209
  };
661
1210
  }
1211
+ var WAIT_MS = 5e3;
1212
+ var REWRITE_MS = 250;
1213
+ var watchDir = (dir, onEvent) => {
1214
+ const watcher = watch(dir, onEvent);
1215
+ watcher.on("error", () => {
1216
+ });
1217
+ watcher.unref();
1218
+ return () => watcher.close();
1219
+ };
1220
+ function fileEventsArrive({ waitMs = WAIT_MS, watch = watchDir } = {}) {
1221
+ const dir = mkdtempSync(join(tmpdir(), "forge-watch-"));
1222
+ return new Promise((resolve5) => {
1223
+ let settled2 = false;
1224
+ let writes = 0;
1225
+ let stop;
1226
+ let rewrite;
1227
+ let deadline;
1228
+ const settle = (arrived) => {
1229
+ if (settled2) return;
1230
+ settled2 = true;
1231
+ clearInterval(rewrite);
1232
+ clearTimeout(deadline);
1233
+ stop?.();
1234
+ rmSync(dir, { recursive: true, force: true });
1235
+ resolve5(arrived);
1236
+ };
1237
+ const write = () => writeFileSync(join(dir, "probe"), String(writes++));
1238
+ try {
1239
+ stop = watch(dir, () => settle(true));
1240
+ write();
1241
+ } catch {
1242
+ settle(false);
1243
+ return;
1244
+ }
1245
+ rewrite = setInterval(write, REWRITE_MS);
1246
+ deadline = setTimeout(() => settle(false), waitMs);
1247
+ rewrite.unref();
1248
+ deadline.unref();
1249
+ });
1250
+ }
1251
+ var SILENT = `[forge] a file written ${WAIT_MS / 1e3} s ago has not been reported by the OS file watcher, so an edit will not reach the page until file events arrive again. \`server.watch: { usePolling: true }\` serves edits without them. On macOS, look for \`fseventsd\` held at 100% CPU.`;
1252
+ async function warnWhenWatchIsSilent(server, probe = fileEventsArrive) {
1253
+ const { watch } = server.config.server;
1254
+ if (watch === null || watch?.usePolling) return;
1255
+ if (!await probe()) server.config.logger.warn(SILENT);
1256
+ }
662
1257
 
663
1258
  // src/vite/plugin.ts
664
1259
  var PREFIX = "virtual:forge/";
665
1260
  var MODULES = /* @__PURE__ */ new Set(["index.js", "importers.js", "deps.js", "frame-config.js", "shell-config.js", "shell-entry.js", "frame-entry.js"]);
666
- var PAGES2 = { "/": "shell-entry.js", "/index.html": "shell-entry.js", "/frame.html": "frame-entry.js" };
1261
+ var SCRIPT2 = /\.[cm]?[jt]sx?$/;
667
1262
  function forge(options) {
668
1263
  let root = process.cwd();
669
1264
  let base = "/";
@@ -682,9 +1277,9 @@ function forge(options) {
682
1277
  };
683
1278
  const matches = (file) => {
684
1279
  const path = relative(root, file).split(sep).join("/");
685
- return !path.split("/").includes("node_modules") && options.stories.some((pattern) => matchesGlob(path, pattern));
1280
+ return !path.split("/").includes("node_modules") && storyGlobOf(path, options.stories) !== void 0;
686
1281
  };
687
- const read = (file) => indexFile(readFileSync(file, "utf8"), file, autoTitle(file, root, options.stories), wrappersOf.get(file)?.wrappers);
1282
+ const read = (file) => indexStory(readFileSync(file, "utf8"), file, { root, stories: options.stories }, wrappersOf.get(file));
688
1283
  const logError = (err) => logger?.error(`[forge] ${err instanceof Error ? err.message : String(err)}`);
689
1284
  const files = () => {
690
1285
  byFile ??= new Map(
@@ -711,9 +1306,9 @@ function forge(options) {
711
1306
  } catch {
712
1307
  continue;
713
1308
  }
714
- const before = wrappersOf.get(file)?.wrappers ?? /* @__PURE__ */ new Set();
1309
+ const before = wrappersOf.get(file);
715
1310
  wrappersOf.set(file, next);
716
- if (before.size === next.wrappers.size && [...before].every((name) => next.wrappers.has(name))) continue;
1311
+ if (sameResolution(before, next)) continue;
717
1312
  try {
718
1313
  current.set(file, read(file));
719
1314
  } catch (err) {
@@ -722,6 +1317,26 @@ function forge(options) {
722
1317
  }
723
1318
  };
724
1319
  const depGraph = () => deps ??= createDepGraph({ root, read: (file) => readFileSync(file, "utf8") });
1320
+ let history = null;
1321
+ async function graphOf(resolveImport) {
1322
+ const graph = await depGraph().build(index(), resolveImport);
1323
+ const sources = Object.values(graph).flatMap((entry) => entry.source === null ? [] : [entry.source]);
1324
+ const histories = await (history ??= createHistoryReader(root)).read(sources);
1325
+ for (const entry of Object.values(graph)) {
1326
+ const found = entry.source === null ? void 0 : histories[entry.source];
1327
+ if (found) entry.history = found;
1328
+ }
1329
+ return graph;
1330
+ }
1331
+ const titleUnder = (at) => {
1332
+ if (!options.shellConfig) return void 0;
1333
+ const file = resolve(at, options.shellConfig);
1334
+ try {
1335
+ return shellTitle(readFileSync(file, "utf8"), file);
1336
+ } catch {
1337
+ return void 0;
1338
+ }
1339
+ };
725
1340
  const configModule = (path) => path ? `export { default } from ${JSON.stringify(resolve(root, path))};
726
1341
  ` : "export default {};\n";
727
1342
  const modules = {
@@ -736,48 +1351,8 @@ ${lines.join("\n")}
736
1351
  },
737
1352
  "frame-config.js": () => configModule(options.frameConfig),
738
1353
  "shell-config.js": () => configModule(options.shellConfig),
739
- "shell-entry.js": () => `import { mountWorkshop } from '@weasel-js/forge/shell';
740
- import '@weasel-js/labkit/styles.css';
741
- import '@weasel-js/forge/shell.css';
742
- import index from 'virtual:forge/index.js';
743
- import importers from 'virtual:forge/importers.js';
744
- import config from 'virtual:forge/shell-config.js';
745
- import setup from 'virtual:forge/frame-config.js';
746
-
747
- const workshop = mountWorkshop({
748
- index,
749
- importers,
750
- setup,
751
- config,
752
- frameUrl: ${JSON.stringify(`${base}frame.html`)},
753
- stories: ${JSON.stringify(options.stories)},
754
- });
755
- import.meta.hot?.on('forge:index', (next) => workshop.setIndex(next));
756
- // Read from source on first request, so the graph costs nothing until the page is up.
757
- import('virtual:forge/deps.js').then((m) => workshop.setDependencies(m.default));
758
- import.meta.hot?.on('forge:deps', (next) => workshop.setDependencies(next));
759
- // A story edit reaches here as an update of the importers module, whose fresh import() URLs carry the new
760
- // timestamps; the file it names arrives just before, so the reload waits for the importers that can fetch it.
761
- const stale = new Set();
762
- import.meta.hot?.on('forge:story', ({ file }) => stale.add(file));
763
- import.meta.hot?.accept('virtual:forge/importers.js', (next) => {
764
- if (!next) return;
765
- workshop.setImporters(next.default);
766
- for (const file of stale) workshop.reloadStory(file);
767
- stale.clear();
768
- });
769
- `,
770
- "frame-entry.js": () => `import { mountFrame } from '@weasel-js/forge/frame';
771
- import '@weasel-js/forge/frame.css';
772
- import index from 'virtual:forge/index.js';
773
- import importers from 'virtual:forge/importers.js';
774
- import setup from 'virtual:forge/frame-config.js';
775
-
776
- mountFrame({ index, importers, setup });
777
- // The importers module is shared with the workshop page, which accepts a story edit in place; a frame document
778
- // showing the old module reloads instead, and without this the edit would dead-end here and reload every page.
779
- import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
780
- `
1354
+ "shell-entry.js": () => shellEntry(base, options.stories),
1355
+ "frame-entry.js": () => frameEntry((options.frameStyles ?? []).map((path) => resolve(root, path)))
781
1356
  };
782
1357
  const invalidate = (server, names) => {
783
1358
  for (const env of Object.values(server.environments)) {
@@ -789,9 +1364,9 @@ import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
789
1364
  };
790
1365
  function redeps(server) {
791
1366
  if (depsSent === null || depsPending) return;
792
- const resolve4 = async (spec, importer) => (await server.environments.client.pluginContainer.resolveId(spec, importer))?.id ?? null;
1367
+ const resolve5 = async (spec, importer) => (await server.environments.client.pluginContainer.resolveId(spec, importer))?.id ?? null;
793
1368
  depsPending = Promise.resolve().then(async () => {
794
- const graph = await depGraph().build(index(), resolve4);
1369
+ const graph = await graphOf(resolve5);
795
1370
  const json = JSON.stringify(graph);
796
1371
  if (json === depsSent) return;
797
1372
  depsSent = json;
@@ -801,13 +1376,28 @@ import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
801
1376
  depsPending = null;
802
1377
  });
803
1378
  }
1379
+ const storiesReaching = (changed) => {
1380
+ const current = files();
1381
+ const seen = /* @__PURE__ */ new Set();
1382
+ const reached = /* @__PURE__ */ new Set();
1383
+ const climb = (mod) => {
1384
+ if (seen.has(mod)) return;
1385
+ seen.add(mod);
1386
+ if (mod.file && current.has(mod.file)) reached.add(mod.file);
1387
+ if (mod.isSelfAccepting && !(mod.file && SCRIPT2.test(mod.file))) return;
1388
+ for (const importer of mod.importers) climb(importer);
1389
+ };
1390
+ for (const mod of changed) climb(mod);
1391
+ return [...reached];
1392
+ };
804
1393
  async function reindex(server, file, event) {
805
1394
  const current = files();
806
- if (event === "change" && current.has(file)) server.ws.send({ type: "custom", event: "forge:story", data: { file } });
807
1395
  const own = event === "add" ? matches(file) : current.has(file);
808
1396
  const through = [...wrappersOf].filter(([story, { reads, unresolved }]) => story !== file && (reads.has(file) || event === "add" && unresolved)).map(([story]) => story);
809
1397
  if (!own && through.length === 0) return false;
810
1398
  const before = JSON.stringify(index());
1399
+ const importable = () => JSON.stringify([...current].filter(([, entries]) => entries.length > 0).map(([path]) => path));
1400
+ const importableBefore = importable();
811
1401
  if (own && event === "unlink") {
812
1402
  current.delete(file);
813
1403
  wrappersOf.delete(file);
@@ -825,10 +1415,13 @@ import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
825
1415
  if (JSON.stringify(next) === before) return false;
826
1416
  invalidate(server, ["index.js", "importers.js"]);
827
1417
  server.ws.send({ type: "custom", event: "forge:index", data: next });
1418
+ const importers = server.environments.client.moduleGraph.getModuleById(`\0${PREFIX}importers.js`);
1419
+ if (importers && importable() !== importableBefore) await server.environments.client.reloadModule(importers);
828
1420
  return true;
829
1421
  }
830
1422
  return [
831
1423
  ...options.storybookShims === false ? [] : [storybookShims()],
1424
+ ...options.propControls === false ? [] : [propControls(() => [...files().keys()], options.propControls === true ? void 0 : options.propControls)],
832
1425
  {
833
1426
  name: "weaselforge",
834
1427
  config(config, { command }) {
@@ -840,7 +1433,7 @@ import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
840
1433
  ];
841
1434
  const optimizeDeps = { entries };
842
1435
  if (command !== "build") return { optimizeDeps };
843
- return { optimizeDeps, build: { rolldownOptions: { input: writePages(at) } } };
1436
+ return { optimizeDeps, build: { rolldownOptions: { input: writePages(at, titleUnder(at)) } } };
844
1437
  },
845
1438
  generateBundle: {
846
1439
  order: "post",
@@ -856,6 +1449,7 @@ import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
856
1449
  wrappersOf.clear();
857
1450
  settledAll = null;
858
1451
  deps = null;
1452
+ history = null;
859
1453
  depsSent = null;
860
1454
  },
861
1455
  resolveId(id) {
@@ -869,17 +1463,22 @@ import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
869
1463
  await settledAll;
870
1464
  }
871
1465
  if (name !== "deps.js") return modules[name]?.();
872
- const graph = await depGraph().build(index(), async (spec, importer) => (await this.resolve(spec, importer))?.id ?? null);
1466
+ const graph = await graphOf(async (spec, importer) => (await this.resolve(spec, importer))?.id ?? null);
873
1467
  depsSent = JSON.stringify(graph);
874
1468
  return `export default ${depsSent};
875
1469
  `;
876
1470
  },
1471
+ hotUpdate({ modules: modules2, server }) {
1472
+ if (this.environment.name !== "client") return;
1473
+ for (const file of storiesReaching(modules2)) server.ws.send({ type: "custom", event: "forge:story", data: { file } });
1474
+ },
877
1475
  configureServer(server) {
878
1476
  server.watcher.add([...files().keys()]);
1477
+ void warnWhenWatchIsSilent(server);
879
1478
  for (const event of ["add", "change", "unlink"]) {
880
1479
  server.watcher.on(event, (path) => {
881
1480
  const file = resolve(path);
882
- const added = event === "add" && /\.[cm]?[jt]sx?$/.test(file) && !file.split(sep).includes("node_modules");
1481
+ const added = event === "add" && SCRIPT2.test(file) && !file.split(sep).includes("node_modules");
883
1482
  if (added) depGraph().reset();
884
1483
  const read2 = added || event !== "add" && depGraph().invalidate(file);
885
1484
  reindex(server, file, event).then((indexed) => {
@@ -887,22 +1486,7 @@ import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
887
1486
  }, logError);
888
1487
  });
889
1488
  }
890
- server.middlewares.use((req, res, next) => {
891
- const url = req.url ?? "/";
892
- const path = url.split("?")[0] ?? "/";
893
- const prefix = base.endsWith("/") ? base.slice(0, -1) : base;
894
- const page = path === prefix ? "/" : path.startsWith(`${prefix}/`) ? path.slice(prefix.length) : null;
895
- const entry = req.method === "GET" && page !== null ? PAGES2[page] : void 0;
896
- if (!entry) return next();
897
- server.transformIndexHtml(url, html(entry)).then(
898
- (doc) => {
899
- res.statusCode = 200;
900
- res.setHeader("Content-Type", "text/html");
901
- res.end(doc);
902
- },
903
- (err) => next(err)
904
- );
905
- });
1489
+ servePages(server, base, () => titleUnder(root));
906
1490
  }
907
1491
  }
908
1492
  ];
@@ -914,7 +1498,7 @@ function forgeTest(options) {
914
1498
  if (id.startsWith("\0") || id.includes("?")) return false;
915
1499
  const path = relative(root, id).split(sep).join("/");
916
1500
  if (path.startsWith("../") || path.split("/").includes("node_modules")) return false;
917
- return options.stories.some((pattern) => matchesGlob(path, pattern));
1501
+ return storyGlobOf(path, options.stories) !== void 0;
918
1502
  };
919
1503
  return [
920
1504
  ...options.storybookShims === false ? [] : [storybookShims()],
@@ -933,9 +1517,14 @@ function forgeTest(options) {
933
1517
  configResolved(config) {
934
1518
  root = config.root;
935
1519
  },
936
- transform(code, id) {
1520
+ async transform(code, id) {
937
1521
  if (!matches(id)) return void 0;
938
- const entries = indexFile(code, id, autoTitle(id, root, options.stories));
1522
+ const { entries } = await indexStoryResolved(
1523
+ code,
1524
+ id,
1525
+ { root, stories: options.stories },
1526
+ async (spec, importer) => (await this.resolve(spec, importer))?.id ?? null
1527
+ );
939
1528
  const title = entries[0]?.title;
940
1529
  if (title === void 0) return void 0;
941
1530
  const q = (value) => JSON.stringify(value);