@weasel-js/forge 1.9.2 → 1.9.4

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 (48) hide show
  1. package/README.md +22 -0
  2. package/dist/{FrameController-CpcY0DNB.d.ts → FrameController-BQjKhdwk.d.ts} +2 -36
  3. package/dist/chunk-5DXUNLV2.js +6 -0
  4. package/dist/chunk-5DXUNLV2.js.map +1 -0
  5. package/dist/{chunk-MGQWCPYE.js → chunk-5KI5YSQ5.js} +4 -5
  6. package/dist/chunk-5KI5YSQ5.js.map +1 -0
  7. package/dist/{chunk-Q2EZXJFE.js → chunk-B3PPHJTS.js} +59 -45
  8. package/dist/chunk-B3PPHJTS.js.map +1 -0
  9. package/dist/{chunk-CJX3W5HY.js → chunk-FRUA5WJF.js} +4 -3
  10. package/dist/chunk-FRUA5WJF.js.map +1 -0
  11. package/dist/{chunk-GZPQMGXJ.js → chunk-JHL4ERDA.js} +4 -3
  12. package/dist/chunk-JHL4ERDA.js.map +1 -0
  13. package/dist/{chunk-JVRHBI3V.js → chunk-MQSFPRF6.js} +20 -44
  14. package/dist/chunk-MQSFPRF6.js.map +1 -0
  15. package/dist/chunk-TXLWQFF6.js +10 -0
  16. package/dist/chunk-TXLWQFF6.js.map +1 -0
  17. package/dist/{chunk-BGOHALHO.js → chunk-ZZCCIXEJ.js} +11 -90
  18. package/dist/chunk-ZZCCIXEJ.js.map +1 -0
  19. package/dist/{config-BfsYw5lg.d.ts → config-Dfu-NKxd.d.ts} +27 -4
  20. package/dist/csf/preview-api.js +2 -2
  21. package/dist/frame/index.d.ts +2 -2
  22. package/dist/frame/index.js +6 -40
  23. package/dist/frame/index.js.map +1 -1
  24. package/dist/index.d.ts +3 -3
  25. package/dist/index.js +3 -3
  26. package/dist/index.js.map +1 -1
  27. package/dist/shell/index.css +61 -61
  28. package/dist/shell/index.css.map +1 -1
  29. package/dist/shell/index.d.ts +5 -13
  30. package/dist/shell/index.js +345 -332
  31. package/dist/shell/index.js.map +1 -1
  32. package/dist/test/index.d.ts +4 -2
  33. package/dist/test/index.js +7 -7
  34. package/dist/test/index.js.map +1 -1
  35. package/dist/test/play.d.ts +3 -3
  36. package/dist/test/play.js.map +1 -1
  37. package/dist/vite/index.d.ts +3 -1
  38. package/dist/vite/index.js +365 -101
  39. package/dist/vite/index.js.map +1 -1
  40. package/package.json +5 -5
  41. package/dist/chunk-BGOHALHO.js.map +0 -1
  42. package/dist/chunk-CJX3W5HY.js.map +0 -1
  43. package/dist/chunk-GZPQMGXJ.js.map +0 -1
  44. package/dist/chunk-JVRHBI3V.js.map +0 -1
  45. package/dist/chunk-KJGXJD3U.js +0 -15
  46. package/dist/chunk-KJGXJD3U.js.map +0 -1
  47. package/dist/chunk-MGQWCPYE.js.map +0 -1
  48. package/dist/chunk-Q2EZXJFE.js.map +0 -1
@@ -1,19 +1,21 @@
1
+ import { DEFAULT_TITLE } from '../chunk-5DXUNLV2.js';
1
2
  import { isGallery } from '../chunk-WEQSLPKT.js';
2
3
  import { storyNameFromExport, storyId } from '../chunk-HRGPFQBW.js';
3
4
  import { readFileSync, existsSync, mkdirSync, writeFileSync, globSync, statSync } from 'fs';
4
- import { resolve, sep, relative, matchesGlob, dirname, join, basename } from 'path';
5
+ import { resolve, sep, relative, dirname, join, basename, matchesGlob } from 'path';
5
6
  import { parse } from '@babel/parser';
6
7
  import { fileURLToPath } from 'url';
7
8
 
8
9
  // src/vite/html.ts
9
- function html(entry, mode = "serve") {
10
+ var escaped = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
11
+ function html(entry, mode = "serve", title = DEFAULT_TITLE) {
10
12
  const src = `${mode === "serve" ? "/@id/" : ""}virtual:forge/${entry}`;
11
13
  return `<!doctype html>
12
14
  <html lang="en">
13
15
  <head>
14
16
  <meta charset="utf-8" />
15
17
  <meta name="viewport" content="width=device-width, initial-scale=1" />
16
- <title>weaselforge</title>
18
+ <title>${escaped(title)}</title>
17
19
  </head>
18
20
  <body>
19
21
  <div id="root"></div>
@@ -26,13 +28,13 @@ function html(entry, mode = "serve") {
26
28
  // src/vite/build.ts
27
29
  var DIR = "node_modules/.forge";
28
30
  var PAGES = { index: "shell-entry.js", frame: "frame-entry.js" };
29
- function writePages(root) {
31
+ function writePages(root, title) {
30
32
  const dir = join(root, DIR);
31
33
  mkdirSync(dir, { recursive: true });
32
34
  return Object.fromEntries(
33
35
  Object.entries(PAGES).map(([name, entry]) => {
34
36
  const file = join(dir, `${name}.html`);
35
- writeFileSync(file, html(entry, "build"));
37
+ writeFileSync(file, html(entry, "build", title));
36
38
  return [name, file];
37
39
  })
38
40
  );
@@ -47,34 +49,6 @@ function hoistPages(bundle, emit) {
47
49
  emit({ type: "asset", fileName: `${name}.html`, source: up(asset.source) });
48
50
  }
49
51
  }
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
52
  var keyOf = (file, local) => `${file}#${local}`;
79
53
  var fileOfKey = (key) => key.slice(0, key.lastIndexOf("#"));
80
54
  var SCRIPT = /\.[cm]?[jt]sx?$/;
@@ -365,11 +339,59 @@ function calleeOf(node, bindings, depth = 0) {
365
339
  return void 0;
366
340
  }
367
341
  }
342
+ var LITERALS = /* @__PURE__ */ new Set([
343
+ "StringLiteral",
344
+ "NumericLiteral",
345
+ "BooleanLiteral",
346
+ "NullLiteral",
347
+ "BigIntLiteral",
348
+ "RegExpLiteral",
349
+ "TemplateLiteral",
350
+ "ArrayExpression",
351
+ "ArrowFunctionExpression",
352
+ "FunctionExpression",
353
+ "FunctionDeclaration",
354
+ "ClassExpression",
355
+ "JSXElement",
356
+ "JSXFragment"
357
+ ]);
358
+ function isLiteralValue(node, bindings, depth = 0) {
359
+ if (!node || depth > 16) return false;
360
+ switch (node.type) {
361
+ case "TSSatisfiesExpression":
362
+ case "TSAsExpression":
363
+ case "TSTypeAssertion":
364
+ case "TSNonNullExpression":
365
+ case "ParenthesizedExpression":
366
+ return isLiteralValue(node.expression, bindings, depth + 1);
367
+ case "Identifier":
368
+ return isLiteralValue(bindings.get(node.name), bindings, depth + 1);
369
+ case "ObjectExpression":
370
+ return node.properties.every((p) => p.type !== "SpreadElement");
371
+ default:
372
+ return LITERALS.has(node.type);
373
+ }
374
+ }
368
375
  function stringLiteral(node) {
369
376
  if (node.type === "StringLiteral") return node.value;
370
377
  if (node.type === "TemplateLiteral" && node.expressions.length === 0) return node.quasis.map((q) => q.value.cooked ?? q.value.raw).join("");
371
378
  return void 0;
372
379
  }
380
+ function stringOf(node, bindings, depth = 0) {
381
+ if (!node || depth > 16) return void 0;
382
+ switch (node.type) {
383
+ case "TSSatisfiesExpression":
384
+ case "TSAsExpression":
385
+ case "TSTypeAssertion":
386
+ case "TSNonNullExpression":
387
+ case "ParenthesizedExpression":
388
+ return stringOf(node.expression, bindings, depth + 1);
389
+ case "Identifier":
390
+ return stringOf(bindings.get(node.name), bindings, depth + 1);
391
+ default:
392
+ return stringLiteral(node);
393
+ }
394
+ }
373
395
  function isDefaultExport(stmt) {
374
396
  if (stmt.type === "ExportDefaultDeclaration") return true;
375
397
  return stmt.type === "ExportNamedDeclaration" && stmt.specifiers.some((s) => s.type === "ExportSpecifier" && exportedName(s.exported) === "default");
@@ -416,10 +438,58 @@ function stringProp(obj, key) {
416
438
  return value?.type === "StringLiteral" ? value.value : void 0;
417
439
  }
418
440
 
441
+ // src/vite/nativeExports.ts
442
+ var isFn = (node) => node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression" || node?.type === "FunctionDeclaration";
443
+ var foreignFrom = (known) => ({
444
+ value: (name) => known.values[name],
445
+ call: (name) => known.calls[name],
446
+ reexport: (name) => known.reexports[name]
447
+ });
448
+ function returnsStory(statements, scope, depth) {
449
+ return statements.some((stmt) => {
450
+ if (stmt.type === "ReturnStatement") return mayBeStory(stmt.argument ?? void 0, scope, depth);
451
+ if (stmt.type === "BlockStatement") return returnsStory(stmt.body, scope, depth);
452
+ if (stmt.type !== "IfStatement") return false;
453
+ return returnsStory([stmt.consequent, ...stmt.alternate ? [stmt.alternate] : []], scope, depth);
454
+ });
455
+ }
456
+ function mayReturnStory(fn, scope, depth = 0) {
457
+ return fn.body.type === "BlockStatement" ? returnsStory(fn.body.body, scope, depth + 1) : mayBeStory(fn.body, scope, depth + 1);
458
+ }
459
+ function mayBeStory(node, scope, depth = 0) {
460
+ if (!node) return true;
461
+ if (depth > 16) return false;
462
+ switch (node.type) {
463
+ case "TSSatisfiesExpression":
464
+ case "TSAsExpression":
465
+ case "TSTypeAssertion":
466
+ case "TSNonNullExpression":
467
+ case "ParenthesizedExpression":
468
+ return mayBeStory(node.expression, scope, depth + 1);
469
+ case "Identifier":
470
+ return scope.bindings.has(node.name) ? mayBeStory(scope.bindings.get(node.name), scope, depth + 1) : scope.foreign?.value(node.name) ?? true;
471
+ case "CallExpression": {
472
+ if (node.callee.type !== "Identifier") return false;
473
+ if (scope.wrappers.has(node.callee.name)) return true;
474
+ const fn = scope.bindings.get(node.callee.name);
475
+ if (isFn(fn)) return mayReturnStory(fn, scope, depth);
476
+ return !scope.bindings.has(node.callee.name) && (scope.foreign?.call(node.callee.name) ?? false);
477
+ }
478
+ case "ConditionalExpression":
479
+ return mayBeStory(node.consequent, scope, depth + 1) || mayBeStory(node.alternate, scope, depth + 1);
480
+ case "LogicalExpression":
481
+ return mayBeStory(node.left, scope, depth + 1) || mayBeStory(node.right, scope, depth + 1);
482
+ case "ObjectExpression":
483
+ return node.properties.some((p) => p.type === "SpreadElement" && mayBeStory(p.argument, scope, depth + 1));
484
+ default:
485
+ return !isLiteralValue(node, scope.bindings);
486
+ }
487
+ }
488
+
419
489
  // src/vite/indexFile.ts
420
- function indexFile(code, file, autoTitle2, wrappers = /* @__PURE__ */ new Set()) {
490
+ function indexFile(code, file, autoTitle2, wrappers = /* @__PURE__ */ new Set(), foreign) {
421
491
  try {
422
- return indexProgram(parseFile(code, file), file, autoTitle2, wrappers);
492
+ return indexProgram(parseFile(code, file), file, autoTitle2, wrappers, foreign);
423
493
  } catch (err) {
424
494
  throw new Error(`${file}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
425
495
  }
@@ -464,11 +534,13 @@ function scan(program, extra) {
464
534
  const decl = stmt.type === "ExportNamedDeclaration" ? stmt.declaration : stmt;
465
535
  if (decl?.type === "VariableDeclaration") {
466
536
  for (const d of decl.declarations) if (d.id.type === "Identifier" && d.init) bindings.set(d.id.name, d.init);
537
+ } else if (decl?.type === "FunctionDeclaration" && decl.id) {
538
+ bindings.set(decl.id.name, decl);
467
539
  }
468
540
  }
469
541
  return { bindings, wrappers };
470
542
  }
471
- function indexProgram(program, file, autoTitle2, extra) {
543
+ function indexProgram(program, file, autoTitle2, extra, foreign) {
472
544
  const { bindings, wrappers } = scan(program, extra);
473
545
  let metaNode;
474
546
  let metaDoc;
@@ -486,7 +558,7 @@ function indexProgram(program, file, autoTitle2, extra) {
486
558
  if (d.id.type === "Identifier") exported.push({ name: d.id.name, node: d.init ?? void 0, doc: docOf(d) ?? doc });
487
559
  }
488
560
  } else if (decl?.type === "FunctionDeclaration" && decl.id) {
489
- exported.push({ name: decl.id.name, node: void 0, doc });
561
+ exported.push({ name: decl.id.name, node: decl, doc });
490
562
  }
491
563
  for (const spec of stmt.specifiers) {
492
564
  if (spec.type !== "ExportSpecifier" || spec.exportKind === "type") continue;
@@ -495,12 +567,13 @@ function indexProgram(program, file, autoTitle2, extra) {
495
567
  if (name === "default") {
496
568
  metaNode = node;
497
569
  metaDoc ??= doc;
498
- } else exported.push({ name, node, doc });
570
+ } else if (stmt.source) exported.push({ name, node, doc, from: true });
571
+ else exported.push({ name, node: node ?? (spec.local.type === "Identifier" ? spec.local : void 0), doc });
499
572
  }
500
573
  }
501
574
  if (!metaNode && !program.body.some(isDefaultExport)) return [];
502
575
  const meta = objectOf(metaNode, bindings, wrappers);
503
- const title = stringProp(meta, "title") ?? autoTitle2;
576
+ const title = stringOf(prop(meta, "title"), bindings) ?? autoTitle2;
504
577
  const include = storyFilter(meta, "includeStories");
505
578
  const exclude = storyFilter(meta, "excludeStories");
506
579
  const componentNode = prop(meta, "component");
@@ -508,7 +581,7 @@ function indexProgram(program, file, autoTitle2, extra) {
508
581
  const metaIsolate = isolateOf(meta, bindings, wrappers, "default");
509
582
  const metaTags = tagsOf(meta);
510
583
  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 }) => {
584
+ 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
585
  const spec = objectOf(node, bindings, wrappers);
513
586
  const isolate = isolateOf(spec, bindings, wrappers, name) ?? metaIsolate;
514
587
  const tags = combineTags(metaTags, tagsOf(spec));
@@ -569,36 +642,105 @@ function storyFilter(obj, key) {
569
642
  }
570
643
  return null;
571
644
  }
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;
645
+ function storyGlobOf(path, stories) {
646
+ return stories.find((pattern) => matchesGlob(path, pattern.replace(/^\.\//, "")));
647
+ }
648
+
649
+ // src/vite/autoTitle.ts
650
+ var posix = (path) => path.split(sep).join("/");
651
+ var dotted = (path) => /^\.{1,2}([/\\]|$)/.test(path) ? path : `./${path}`;
652
+ function globDirectory(pattern) {
653
+ const segments = pattern.split("/");
654
+ const magic = segments.findIndex((segment) => /^!|(^|[^\\])[*?[{(]/.test(segment));
655
+ return magic === -1 ? dirname(pattern) : segments.slice(0, magic).join("/");
656
+ }
657
+ function titleParts(parts) {
658
+ const last = parts[parts.length - 1];
659
+ if (last === void 0) return parts;
660
+ const stripped = last.replace(/(?:[.](?:story|stories))?([.][^.]+)$/i, "");
661
+ if (parts.length === 1) return [stripped];
662
+ const nextToLast = parts[parts.length - 2];
663
+ if (stripped && nextToLast && stripped.toLowerCase() === nextToLast.toLowerCase()) return [...parts.slice(0, -2), stripped];
664
+ if (stripped && (/^(story|stories)([.][^.]+)$/i.test(last) || /^index$/i.test(stripped))) return parts.slice(0, -1);
665
+ return [...parts.slice(0, -1), stripped];
666
+ }
667
+ var titleFrom = (importPath, directory) => titleParts(importPath.replace(directory, "").split("/").filter(Boolean)).join("/");
668
+ function autoTitle(file, root, stories) {
669
+ const path = posix(relative(root, file));
670
+ const importPath = dotted(path);
671
+ const pattern = storyGlobOf(path, stories);
672
+ if (pattern === void 0) return titleFrom(importPath, ".");
673
+ const directory = dotted(posix(relative(root, resolve(root, globDirectory(pattern))))).replace(/\/$/, "");
674
+ return titleFrom(importPath, directory);
675
+ }
676
+
677
+ // src/vite/foreignStories.ts
678
+ var MAX_DEPTH = 8;
679
+ async function settled(read, ask) {
680
+ const known = { values: {}, calls: {}, reexports: {} };
681
+ const asked = /* @__PURE__ */ new Set();
682
+ for (; ; ) {
683
+ const fresh = [];
684
+ const answer = (kind) => (name) => {
685
+ if (!asked.has(`${kind}:${name}`)) fresh.push([kind, name]);
686
+ return known[kind][name];
687
+ };
688
+ const result = read({ value: answer("values"), call: answer("calls"), reexport: answer("reexports") });
689
+ if (fresh.length === 0) return { result, known };
690
+ for (const [kind, name] of fresh) {
691
+ asked.add(`${kind}:${name}`);
692
+ const found = await ask(kind, name);
693
+ if (found !== void 0) known[kind][name] = found;
578
694
  }
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
695
  }
582
- return [];
583
696
  }
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 });
697
+ async function foreignStories(file, wrappers, own, trace, read) {
698
+ const answers = /* @__PURE__ */ new Map();
699
+ const exported = async (at, name, called, depth) => {
700
+ const key = await trace.declaring(at, name);
701
+ if (!key) return void 0;
702
+ if (own.has(key)) return called && own.get(key) === "story";
703
+ const memo = `${key}:${called}`;
704
+ if (!answers.has(memo)) {
705
+ answers.set(memo, depth > MAX_DEPTH ? Promise.resolve(void 0) : declared(key, called, depth));
596
706
  }
707
+ return answers.get(memo);
708
+ };
709
+ const declared = async (key, called, depth) => {
710
+ const at = key.slice(0, key.lastIndexOf("#"));
711
+ const local = key.slice(at.length + 1);
712
+ let scope;
713
+ try {
714
+ scope = scan(parseFile(read(at), at), /* @__PURE__ */ new Set());
715
+ } catch {
716
+ return void 0;
717
+ }
718
+ const node = scope.bindings.get(local);
719
+ if (!node) return void 0;
720
+ const { result } = await settled(
721
+ (foreign) => called ? isFn(node) && mayReturnStory(node, { ...scope, foreign }) : mayBeStory(node, { ...scope, foreign }),
722
+ asking(at, depth + 1)
723
+ );
724
+ return result;
597
725
  };
726
+ const asking = (at, depth) => async (kind, name) => {
727
+ const mod = trace.info(at);
728
+ const exp = mod?.exports.get(name);
729
+ const from = kind === "reexports" ? exp?.kind === "from" ? exp : void 0 : mod?.imports.get(name);
730
+ if (!from || from.imported === "*") return void 0;
731
+ const target = await trace.target(from.spec, at);
732
+ return target ? exported(target, from.imported, kind === "calls", depth) : void 0;
733
+ };
734
+ try {
735
+ const program = parseFile(read(file), file);
736
+ return (await settled((foreign) => void indexProgram(program, file, "", wrappers, foreign), asking(file, 0))).known;
737
+ } catch {
738
+ return { values: {}, calls: {}, reexports: {} };
739
+ }
598
740
  }
599
741
 
600
- // src/vite/wrappers.ts
601
- function createWrapperResolver(read) {
742
+ // src/vite/trace.ts
743
+ function createTracer(read) {
602
744
  const modules = /* @__PURE__ */ new Map();
603
745
  const info = (file) => {
604
746
  if (!modules.has(file)) {
@@ -612,18 +754,20 @@ function createWrapperResolver(read) {
612
754
  }
613
755
  return modules.get(file) ?? null;
614
756
  };
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;
757
+ return (resolve4) => {
758
+ const trace = {
759
+ reads: /* @__PURE__ */ new Set(),
760
+ unresolved: false,
761
+ info,
762
+ async target(spec, importer) {
763
+ const id = (await resolve4(spec, importer).catch(() => null))?.split("?")[0];
764
+ if (!id) trace.unresolved = true;
765
+ return id && !id.startsWith("\0") ? id : null;
766
+ },
767
+ declaring: (at, name) => declaring(at, name, 0)
624
768
  };
625
- const declaring = async (at, name, depth = 0) => {
626
- reads.add(at);
769
+ const declaring = async (at, name, depth) => {
770
+ trace.reads.add(at);
627
771
  const mod = info(at);
628
772
  if (!mod || depth > 32) return null;
629
773
  const exp = mod.exports.get(name);
@@ -631,38 +775,121 @@ function createWrapperResolver(read) {
631
775
  const imp = mod.imports.get(exp.local);
632
776
  if (!imp) return `${at}#${exp.local}`;
633
777
  if (imp.imported === "*") return null;
634
- const next = await target(imp.spec, at);
778
+ const next = await trace.target(imp.spec, at);
635
779
  return next ? declaring(next, imp.imported, depth + 1) : null;
636
780
  }
637
781
  if (exp?.kind === "from") {
638
- const next = await target(exp.spec, at);
782
+ const next = await trace.target(exp.spec, at);
639
783
  return next ? declaring(next, exp.imported, depth + 1) : null;
640
784
  }
641
785
  if (name === "default") return null;
642
786
  for (const spec of mod.stars) {
643
- const next = await target(spec, at);
787
+ const next = await trace.target(spec, at);
644
788
  const found = next ? await declaring(next, name, depth + 1) : null;
645
789
  if (found) return found;
646
790
  }
647
791
  return null;
648
792
  };
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)
653
- );
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);
793
+ return trace;
794
+ };
795
+ }
796
+
797
+ // src/vite/wrappers.ts
798
+ var NO_FOREIGN = { values: {}, calls: {}, reexports: {} };
799
+ function sameResolution(a, b) {
800
+ const key = (r) => JSON.stringify([[...r?.wrappers ?? []].sort(), r?.foreign ?? NO_FOREIGN]);
801
+ return key(a) === key(b);
802
+ }
803
+ function createWrapperResolver(read) {
804
+ const tracer = createTracer(read);
805
+ return async (file, callees, resolve4) => {
806
+ const trace = tracer(resolve4);
807
+ const wrappers = /* @__PURE__ */ new Set();
808
+ const direct = [...trace.info(file)?.imports.values() ?? []].some((imp) => imp.spec === "@weasel-js/forge");
809
+ if (callees.length === 0 && !direct) return { wrappers, foreign: NO_FOREIGN, reads: trace.reads, unresolved: false };
810
+ const forge2 = await trace.target("@weasel-js/forge", file);
811
+ const own = /* @__PURE__ */ new Map();
812
+ if (forge2) {
813
+ for (const name of ["meta", "story"]) {
814
+ const key = await trace.declaring(forge2, name);
815
+ if (key) own.set(key, name);
816
+ }
817
+ for (const callee of callees) {
818
+ const from = await trace.target(callee.spec, file);
819
+ const key = from ? await trace.declaring(from, callee.imported) : null;
820
+ if (key && own.has(key)) wrappers.add(callee.local);
821
+ }
822
+ }
823
+ const foreign = forge2 ? await foreignStories(file, wrappers, own, trace, read) : NO_FOREIGN;
824
+ return { wrappers, foreign, reads: trace.reads, unresolved: trace.unresolved };
825
+ };
826
+ }
827
+
828
+ // src/vite/indexStory.ts
829
+ function indexStory(code, file, source, resolved) {
830
+ const foreign = resolved ? foreignFrom(resolved.foreign) : void 0;
831
+ return indexFile(code, file, autoTitle(file, source.root, source.stories), resolved?.wrappers, foreign);
832
+ }
833
+ async function indexStoryResolved(code, file, source, resolve4, decide = createWrapperResolver((path) => readFileSync(path, "utf8"))) {
834
+ const resolution = await decide(file, foreignCallees(code, file), resolve4);
835
+ return { entries: indexStory(code, file, source, resolution), resolution };
836
+ }
837
+ function ownEntries(realms) {
838
+ const here = fileURLToPath(import.meta.url);
839
+ for (let dir = dirname(here); dir !== dirname(dir); dir = dirname(dir)) {
840
+ const manifest = join(dir, "package.json");
841
+ if (!existsSync(manifest) || JSON.parse(readFileSync(manifest, "utf8")).name !== "@weasel-js/forge") {
842
+ continue;
843
+ }
844
+ const source = !relative(join(dir, "src"), here).startsWith("..");
845
+ return realms.map((realm) => join(dir, source ? `src/${realm}/index.ts` : `dist/${realm}/index.js`));
846
+ }
847
+ return [];
848
+ }
849
+
850
+ // src/vite/shellTitle.ts
851
+ function shellTitle(code, file) {
852
+ let program;
853
+ try {
854
+ program = parseFile(code, file);
855
+ } catch {
856
+ return void 0;
857
+ }
858
+ const bindings = /* @__PURE__ */ new Map();
859
+ let config;
860
+ for (const stmt of program.body) {
861
+ if (stmt.type === "VariableDeclaration") {
862
+ for (const d of stmt.declarations) if (d.id.type === "Identifier" && d.init) bindings.set(d.id.name, d.init);
863
+ }
864
+ if (stmt.type === "ExportDefaultDeclaration") config = stmt.declaration;
865
+ if (stmt.type !== "ExportNamedDeclaration" || stmt.source) continue;
866
+ for (const spec of stmt.specifiers) {
867
+ if (spec.type === "ExportSpecifier" && exportedName(spec.exported) === "default") config = spec.local;
868
+ }
869
+ }
870
+ const wrapper = calleeOf(config, bindings);
871
+ return stringOf(prop(objectOf(config, bindings, new Set(wrapper ? [wrapper] : [])), "title"), bindings);
872
+ }
873
+ var SHIMS = [
874
+ [/^@?storybook\/preview-api$/, "@weasel-js/forge/preview-api"],
875
+ [/^@?storybook\/test$/, "@weasel-js/forge/play"]
876
+ ];
877
+ function storybookShims() {
878
+ return {
879
+ name: "weaselforge:storybook-shims",
880
+ enforce: "pre",
881
+ resolveId(id) {
882
+ const hit = SHIMS.find(([pattern]) => pattern.test(id));
883
+ if (!hit) return void 0;
884
+ return this.resolve(hit[1], fileURLToPath(import.meta.url), { skipSelf: true });
658
885
  }
659
- return { wrappers, reads, unresolved };
660
886
  };
661
887
  }
662
888
 
663
889
  // src/vite/plugin.ts
664
890
  var PREFIX = "virtual:forge/";
665
891
  var MODULES = /* @__PURE__ */ new Set(["index.js", "importers.js", "deps.js", "frame-config.js", "shell-config.js", "shell-entry.js", "frame-entry.js"]);
892
+ var SCRIPT2 = /\.[cm]?[jt]sx?$/;
666
893
  var PAGES2 = { "/": "shell-entry.js", "/index.html": "shell-entry.js", "/frame.html": "frame-entry.js" };
667
894
  function forge(options) {
668
895
  let root = process.cwd();
@@ -682,9 +909,9 @@ function forge(options) {
682
909
  };
683
910
  const matches = (file) => {
684
911
  const path = relative(root, file).split(sep).join("/");
685
- return !path.split("/").includes("node_modules") && options.stories.some((pattern) => matchesGlob(path, pattern));
912
+ return !path.split("/").includes("node_modules") && storyGlobOf(path, options.stories) !== void 0;
686
913
  };
687
- const read = (file) => indexFile(readFileSync(file, "utf8"), file, autoTitle(file, root, options.stories), wrappersOf.get(file)?.wrappers);
914
+ const read = (file) => indexStory(readFileSync(file, "utf8"), file, { root, stories: options.stories }, wrappersOf.get(file));
688
915
  const logError = (err) => logger?.error(`[forge] ${err instanceof Error ? err.message : String(err)}`);
689
916
  const files = () => {
690
917
  byFile ??= new Map(
@@ -711,9 +938,9 @@ function forge(options) {
711
938
  } catch {
712
939
  continue;
713
940
  }
714
- const before = wrappersOf.get(file)?.wrappers ?? /* @__PURE__ */ new Set();
941
+ const before = wrappersOf.get(file);
715
942
  wrappersOf.set(file, next);
716
- if (before.size === next.wrappers.size && [...before].every((name) => next.wrappers.has(name))) continue;
943
+ if (sameResolution(before, next)) continue;
717
944
  try {
718
945
  current.set(file, read(file));
719
946
  } catch (err) {
@@ -722,6 +949,15 @@ function forge(options) {
722
949
  }
723
950
  };
724
951
  const depGraph = () => deps ??= createDepGraph({ root, read: (file) => readFileSync(file, "utf8") });
952
+ const titleUnder = (at) => {
953
+ if (!options.shellConfig) return void 0;
954
+ const file = resolve(at, options.shellConfig);
955
+ try {
956
+ return shellTitle(readFileSync(file, "utf8"), file);
957
+ } catch {
958
+ return void 0;
959
+ }
960
+ };
725
961
  const configModule = (path) => path ? `export { default } from ${JSON.stringify(resolve(root, path))};
726
962
  ` : "export default {};\n";
727
963
  const modules = {
@@ -756,8 +992,9 @@ import.meta.hot?.on('forge:index', (next) => workshop.setIndex(next));
756
992
  // Read from source on first request, so the graph costs nothing until the page is up.
757
993
  import('virtual:forge/deps.js').then((m) => workshop.setDependencies(m.default));
758
994
  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.
995
+ // An edit to a story file, or to a module one imports, reaches here as an update of the importers module, whose
996
+ // fresh import() URLs carry the new timestamps; the files it reaches arrive just before, so the reload waits for
997
+ // the importers that can fetch them.
761
998
  const stale = new Set();
762
999
  import.meta.hot?.on('forge:story', ({ file }) => stale.add(file));
763
1000
  import.meta.hot?.accept('virtual:forge/importers.js', (next) => {
@@ -769,7 +1006,8 @@ import.meta.hot?.accept('virtual:forge/importers.js', (next) => {
769
1006
  `,
770
1007
  "frame-entry.js": () => `import { mountFrame } from '@weasel-js/forge/frame';
771
1008
  import '@weasel-js/forge/frame.css';
772
- import index from 'virtual:forge/index.js';
1009
+ ${(options.frameStyles ?? []).map((path) => `import ${JSON.stringify(resolve(root, path))};
1010
+ `).join("")}import index from 'virtual:forge/index.js';
773
1011
  import importers from 'virtual:forge/importers.js';
774
1012
  import setup from 'virtual:forge/frame-config.js';
775
1013
 
@@ -801,13 +1039,28 @@ import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
801
1039
  depsPending = null;
802
1040
  });
803
1041
  }
1042
+ const storiesReaching = (changed) => {
1043
+ const current = files();
1044
+ const seen = /* @__PURE__ */ new Set();
1045
+ const reached = /* @__PURE__ */ new Set();
1046
+ const climb = (mod) => {
1047
+ if (seen.has(mod)) return;
1048
+ seen.add(mod);
1049
+ if (mod.file && current.has(mod.file)) reached.add(mod.file);
1050
+ if (mod.isSelfAccepting && !(mod.file && SCRIPT2.test(mod.file))) return;
1051
+ for (const importer of mod.importers) climb(importer);
1052
+ };
1053
+ for (const mod of changed) climb(mod);
1054
+ return [...reached];
1055
+ };
804
1056
  async function reindex(server, file, event) {
805
1057
  const current = files();
806
- if (event === "change" && current.has(file)) server.ws.send({ type: "custom", event: "forge:story", data: { file } });
807
1058
  const own = event === "add" ? matches(file) : current.has(file);
808
1059
  const through = [...wrappersOf].filter(([story, { reads, unresolved }]) => story !== file && (reads.has(file) || event === "add" && unresolved)).map(([story]) => story);
809
1060
  if (!own && through.length === 0) return false;
810
1061
  const before = JSON.stringify(index());
1062
+ const importable = () => JSON.stringify([...current].filter(([, entries]) => entries.length > 0).map(([path]) => path));
1063
+ const importableBefore = importable();
811
1064
  if (own && event === "unlink") {
812
1065
  current.delete(file);
813
1066
  wrappersOf.delete(file);
@@ -825,6 +1078,8 @@ import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
825
1078
  if (JSON.stringify(next) === before) return false;
826
1079
  invalidate(server, ["index.js", "importers.js"]);
827
1080
  server.ws.send({ type: "custom", event: "forge:index", data: next });
1081
+ const importers = server.environments.client.moduleGraph.getModuleById(`\0${PREFIX}importers.js`);
1082
+ if (importers && importable() !== importableBefore) await server.environments.client.reloadModule(importers);
828
1083
  return true;
829
1084
  }
830
1085
  return [
@@ -840,7 +1095,7 @@ import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
840
1095
  ];
841
1096
  const optimizeDeps = { entries };
842
1097
  if (command !== "build") return { optimizeDeps };
843
- return { optimizeDeps, build: { rolldownOptions: { input: writePages(at) } } };
1098
+ return { optimizeDeps, build: { rolldownOptions: { input: writePages(at, titleUnder(at)) } } };
844
1099
  },
845
1100
  generateBundle: {
846
1101
  order: "post",
@@ -874,12 +1129,16 @@ import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
874
1129
  return `export default ${depsSent};
875
1130
  `;
876
1131
  },
1132
+ hotUpdate({ modules: modules2, server }) {
1133
+ if (this.environment.name !== "client") return;
1134
+ for (const file of storiesReaching(modules2)) server.ws.send({ type: "custom", event: "forge:story", data: { file } });
1135
+ },
877
1136
  configureServer(server) {
878
1137
  server.watcher.add([...files().keys()]);
879
1138
  for (const event of ["add", "change", "unlink"]) {
880
1139
  server.watcher.on(event, (path) => {
881
1140
  const file = resolve(path);
882
- const added = event === "add" && /\.[cm]?[jt]sx?$/.test(file) && !file.split(sep).includes("node_modules");
1141
+ const added = event === "add" && SCRIPT2.test(file) && !file.split(sep).includes("node_modules");
883
1142
  if (added) depGraph().reset();
884
1143
  const read2 = added || event !== "add" && depGraph().invalidate(file);
885
1144
  reindex(server, file, event).then((indexed) => {
@@ -894,7 +1153,7 @@ import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
894
1153
  const page = path === prefix ? "/" : path.startsWith(`${prefix}/`) ? path.slice(prefix.length) : null;
895
1154
  const entry = req.method === "GET" && page !== null ? PAGES2[page] : void 0;
896
1155
  if (!entry) return next();
897
- server.transformIndexHtml(url, html(entry)).then(
1156
+ server.transformIndexHtml(url, html(entry, "serve", titleUnder(root))).then(
898
1157
  (doc) => {
899
1158
  res.statusCode = 200;
900
1159
  res.setHeader("Content-Type", "text/html");
@@ -914,7 +1173,7 @@ function forgeTest(options) {
914
1173
  if (id.startsWith("\0") || id.includes("?")) return false;
915
1174
  const path = relative(root, id).split(sep).join("/");
916
1175
  if (path.startsWith("../") || path.split("/").includes("node_modules")) return false;
917
- return options.stories.some((pattern) => matchesGlob(path, pattern));
1176
+ return storyGlobOf(path, options.stories) !== void 0;
918
1177
  };
919
1178
  return [
920
1179
  ...options.storybookShims === false ? [] : [storybookShims()],
@@ -933,9 +1192,14 @@ function forgeTest(options) {
933
1192
  configResolved(config) {
934
1193
  root = config.root;
935
1194
  },
936
- transform(code, id) {
1195
+ async transform(code, id) {
937
1196
  if (!matches(id)) return void 0;
938
- const entries = indexFile(code, id, autoTitle(id, root, options.stories));
1197
+ const { entries } = await indexStoryResolved(
1198
+ code,
1199
+ id,
1200
+ { root, stories: options.stories },
1201
+ async (spec, importer) => (await this.resolve(spec, importer))?.id ?? null
1202
+ );
939
1203
  const title = entries[0]?.title;
940
1204
  if (title === void 0) return void 0;
941
1205
  const q = (value) => JSON.stringify(value);