@weasel-js/forge 1.9.4 → 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 (48) hide show
  1. package/README.md +12 -4
  2. package/dist/{FrameController-BQjKhdwk.d.ts → FrameController-UeTY7ubF.d.ts} +25 -1
  3. package/dist/{chunk-B3PPHJTS.js → chunk-4NM3RTYK.js} +14 -14
  4. package/dist/chunk-4NM3RTYK.js.map +1 -0
  5. package/dist/{chunk-JHL4ERDA.js → chunk-DFKCLZKS.js} +3 -3
  6. package/dist/chunk-DFKCLZKS.js.map +1 -0
  7. package/dist/{chunk-FRUA5WJF.js → chunk-DKWHGSBZ.js} +3 -3
  8. package/dist/{chunk-FRUA5WJF.js.map → chunk-DKWHGSBZ.js.map} +1 -1
  9. package/dist/{chunk-ZZCCIXEJ.js → chunk-ENXU34PI.js} +16 -25
  10. package/dist/chunk-ENXU34PI.js.map +1 -0
  11. package/dist/{chunk-TXLWQFF6.js → chunk-FL2MUPUL.js} +2 -2
  12. package/dist/chunk-FL2MUPUL.js.map +1 -0
  13. package/dist/{chunk-5KI5YSQ5.js → chunk-GB6PRJET.js} +3 -3
  14. package/dist/{chunk-5KI5YSQ5.js.map → chunk-GB6PRJET.js.map} +1 -1
  15. package/dist/{chunk-MQSFPRF6.js → chunk-V4RI7GWD.js} +37 -19
  16. package/dist/chunk-V4RI7GWD.js.map +1 -0
  17. package/dist/{chunk-HRGPFQBW.js → chunk-ZAWXMUGZ.js} +6 -3
  18. package/dist/chunk-ZAWXMUGZ.js.map +1 -0
  19. package/dist/{config-Dfu-NKxd.d.ts → config-Cz7ZyGF-.d.ts} +5 -5
  20. package/dist/csf/preview-api.d.ts +5 -2
  21. package/dist/csf/preview-api.js +2 -2
  22. package/dist/csf/preview-api.js.map +1 -1
  23. package/dist/frame/index.css +19 -2
  24. package/dist/frame/index.css.map +1 -1
  25. package/dist/frame/index.d.ts +2 -2
  26. package/dist/frame/index.js +5 -5
  27. package/dist/frame/index.js.map +1 -1
  28. package/dist/index.d.ts +4 -4
  29. package/dist/index.js +3 -3
  30. package/dist/shell/index.css +36 -5
  31. package/dist/shell/index.css.map +1 -1
  32. package/dist/shell/index.d.ts +7 -6
  33. package/dist/shell/index.js +398 -274
  34. package/dist/shell/index.js.map +1 -1
  35. package/dist/test/index.d.ts +1 -1
  36. package/dist/test/index.js +6 -6
  37. package/dist/test/play.d.ts +3 -5
  38. package/dist/test/play.js.map +1 -1
  39. package/dist/vite/index.d.ts +16 -1
  40. package/dist/vite/index.js +403 -78
  41. package/dist/vite/index.js.map +1 -1
  42. package/package.json +10 -5
  43. package/dist/chunk-B3PPHJTS.js.map +0 -1
  44. package/dist/chunk-HRGPFQBW.js.map +0 -1
  45. package/dist/chunk-JHL4ERDA.js.map +0 -1
  46. package/dist/chunk-MQSFPRF6.js.map +0 -1
  47. package/dist/chunk-TXLWQFF6.js.map +0 -1
  48. package/dist/chunk-ZZCCIXEJ.js.map +0 -1
@@ -1,10 +1,12 @@
1
1
  import { DEFAULT_TITLE } from '../chunk-5DXUNLV2.js';
2
2
  import { isGallery } from '../chunk-WEQSLPKT.js';
3
- import { storyNameFromExport, storyId } from '../chunk-HRGPFQBW.js';
4
- import { readFileSync, existsSync, mkdirSync, writeFileSync, globSync, statSync } from 'fs';
5
- import { resolve, sep, relative, dirname, join, basename, matchesGlob } 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';
6
6
  import { parse } from '@babel/parser';
7
+ import { execFile } from 'child_process';
7
8
  import { fileURLToPath } from 'url';
9
+ import { tmpdir } from 'os';
8
10
 
9
11
  // src/vite/html.ts
10
12
  var escaped = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -79,7 +81,7 @@ function createDepGraph({ root, read }) {
79
81
  reset() {
80
82
  resolutions.clear();
81
83
  },
82
- async build(entries, resolve4) {
84
+ async build(entries, resolve5) {
83
85
  const resolveFrom = (spec, importer) => {
84
86
  let byImporter = resolutions.get(importer);
85
87
  if (!byImporter) {
@@ -88,7 +90,7 @@ function createDepGraph({ root, read }) {
88
90
  }
89
91
  let pending = byImporter.get(spec);
90
92
  if (!pending) {
91
- pending = resolve4(spec, importer).then(
93
+ pending = resolve5(spec, importer).then(
92
94
  (id) => {
93
95
  const file = id?.split("?")[0];
94
96
  return file && !file.startsWith("\0") && SCRIPT.test(file) && !file.split(/[\\/]/).includes("node_modules") ? file : null;
@@ -318,6 +320,92 @@ function namesIn(node) {
318
320
  walk(node);
319
321
  return out;
320
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
+ }
321
409
  function parseFile(code, file) {
322
410
  const plugins = ["typescript", "decorators-legacy", .../\.[jt]sx$/.test(file) ? ["jsx"] : []];
323
411
  return parse(code, { sourceType: "module", plugins }).program;
@@ -647,7 +735,7 @@ function storyGlobOf(path, stories) {
647
735
  }
648
736
 
649
737
  // src/vite/autoTitle.ts
650
- var posix = (path) => path.split(sep).join("/");
738
+ var posix2 = (path) => path.split(sep).join("/");
651
739
  var dotted = (path) => /^\.{1,2}([/\\]|$)/.test(path) ? path : `./${path}`;
652
740
  function globDirectory(pattern) {
653
741
  const segments = pattern.split("/");
@@ -666,11 +754,11 @@ function titleParts(parts) {
666
754
  }
667
755
  var titleFrom = (importPath, directory) => titleParts(importPath.replace(directory, "").split("/").filter(Boolean)).join("/");
668
756
  function autoTitle(file, root, stories) {
669
- const path = posix(relative(root, file));
757
+ const path = posix2(relative(root, file));
670
758
  const importPath = dotted(path);
671
759
  const pattern = storyGlobOf(path, stories);
672
760
  if (pattern === void 0) return titleFrom(importPath, ".");
673
- const directory = dotted(posix(relative(root, resolve(root, globDirectory(pattern))))).replace(/\/$/, "");
761
+ const directory = dotted(posix2(relative(root, resolve(root, globDirectory(pattern))))).replace(/\/$/, "");
674
762
  return titleFrom(importPath, directory);
675
763
  }
676
764
 
@@ -754,13 +842,13 @@ function createTracer(read) {
754
842
  }
755
843
  return modules.get(file) ?? null;
756
844
  };
757
- return (resolve4) => {
845
+ return (resolve5) => {
758
846
  const trace = {
759
847
  reads: /* @__PURE__ */ new Set(),
760
848
  unresolved: false,
761
849
  info,
762
850
  async target(spec, importer) {
763
- const id = (await resolve4(spec, importer).catch(() => null))?.split("?")[0];
851
+ const id = (await resolve5(spec, importer).catch(() => null))?.split("?")[0];
764
852
  if (!id) trace.unresolved = true;
765
853
  return id && !id.startsWith("\0") ? id : null;
766
854
  },
@@ -802,8 +890,8 @@ function sameResolution(a, b) {
802
890
  }
803
891
  function createWrapperResolver(read) {
804
892
  const tracer = createTracer(read);
805
- return async (file, callees, resolve4) => {
806
- const trace = tracer(resolve4);
893
+ return async (file, callees, resolve5) => {
894
+ const trace = tracer(resolve5);
807
895
  const wrappers = /* @__PURE__ */ new Set();
808
896
  const direct = [...trace.info(file)?.imports.values() ?? []].some((imp) => imp.spec === "@weasel-js/forge");
809
897
  if (callees.length === 0 && !direct) return { wrappers, foreign: NO_FOREIGN, reads: trace.reads, unresolved: false };
@@ -830,8 +918,8 @@ function indexStory(code, file, source, resolved) {
830
918
  const foreign = resolved ? foreignFrom(resolved.foreign) : void 0;
831
919
  return indexFile(code, file, autoTitle(file, source.root, source.stories), resolved?.wrappers, foreign);
832
920
  }
833
- async function indexStoryResolved(code, file, source, resolve4, decide = createWrapperResolver((path) => readFileSync(path, "utf8"))) {
834
- const resolution = await decide(file, foreignCallees(code, file), resolve4);
921
+ async function indexStoryResolved(code, file, source, resolve5, decide = createWrapperResolver((path) => readFileSync(path, "utf8"))) {
922
+ const resolution = await decide(file, foreignCallees(code, file), resolve5);
835
923
  return { entries: indexStory(code, file, source, resolution), resolution };
836
924
  }
837
925
  function ownEntries(realms) {
@@ -847,6 +935,241 @@ function ownEntries(realms) {
847
935
  return [];
848
936
  }
849
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)
1169
+ );
1170
+ });
1171
+ }
1172
+
850
1173
  // src/vite/shellTitle.ts
851
1174
  function shellTitle(code, file) {
852
1175
  let program;
@@ -885,12 +1208,57 @@ function storybookShims() {
885
1208
  }
886
1209
  };
887
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
+ }
888
1257
 
889
1258
  // src/vite/plugin.ts
890
1259
  var PREFIX = "virtual:forge/";
891
1260
  var MODULES = /* @__PURE__ */ new Set(["index.js", "importers.js", "deps.js", "frame-config.js", "shell-config.js", "shell-entry.js", "frame-entry.js"]);
892
1261
  var SCRIPT2 = /\.[cm]?[jt]sx?$/;
893
- var PAGES2 = { "/": "shell-entry.js", "/index.html": "shell-entry.js", "/frame.html": "frame-entry.js" };
894
1262
  function forge(options) {
895
1263
  let root = process.cwd();
896
1264
  let base = "/";
@@ -949,6 +1317,17 @@ function forge(options) {
949
1317
  }
950
1318
  };
951
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
+ }
952
1331
  const titleUnder = (at) => {
953
1332
  if (!options.shellConfig) return void 0;
954
1333
  const file = resolve(at, options.shellConfig);
@@ -972,50 +1351,8 @@ ${lines.join("\n")}
972
1351
  },
973
1352
  "frame-config.js": () => configModule(options.frameConfig),
974
1353
  "shell-config.js": () => configModule(options.shellConfig),
975
- "shell-entry.js": () => `import { mountWorkshop } from '@weasel-js/forge/shell';
976
- import '@weasel-js/labkit/styles.css';
977
- import '@weasel-js/forge/shell.css';
978
- import index from 'virtual:forge/index.js';
979
- import importers from 'virtual:forge/importers.js';
980
- import config from 'virtual:forge/shell-config.js';
981
- import setup from 'virtual:forge/frame-config.js';
982
-
983
- const workshop = mountWorkshop({
984
- index,
985
- importers,
986
- setup,
987
- config,
988
- frameUrl: ${JSON.stringify(`${base}frame.html`)},
989
- stories: ${JSON.stringify(options.stories)},
990
- });
991
- import.meta.hot?.on('forge:index', (next) => workshop.setIndex(next));
992
- // Read from source on first request, so the graph costs nothing until the page is up.
993
- import('virtual:forge/deps.js').then((m) => workshop.setDependencies(m.default));
994
- import.meta.hot?.on('forge:deps', (next) => workshop.setDependencies(next));
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.
998
- const stale = new Set();
999
- import.meta.hot?.on('forge:story', ({ file }) => stale.add(file));
1000
- import.meta.hot?.accept('virtual:forge/importers.js', (next) => {
1001
- if (!next) return;
1002
- workshop.setImporters(next.default);
1003
- for (const file of stale) workshop.reloadStory(file);
1004
- stale.clear();
1005
- });
1006
- `,
1007
- "frame-entry.js": () => `import { mountFrame } from '@weasel-js/forge/frame';
1008
- import '@weasel-js/forge/frame.css';
1009
- ${(options.frameStyles ?? []).map((path) => `import ${JSON.stringify(resolve(root, path))};
1010
- `).join("")}import index from 'virtual:forge/index.js';
1011
- import importers from 'virtual:forge/importers.js';
1012
- import setup from 'virtual:forge/frame-config.js';
1013
-
1014
- mountFrame({ index, importers, setup });
1015
- // The importers module is shared with the workshop page, which accepts a story edit in place; a frame document
1016
- // showing the old module reloads instead, and without this the edit would dead-end here and reload every page.
1017
- import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
1018
- `
1354
+ "shell-entry.js": () => shellEntry(base, options.stories),
1355
+ "frame-entry.js": () => frameEntry((options.frameStyles ?? []).map((path) => resolve(root, path)))
1019
1356
  };
1020
1357
  const invalidate = (server, names) => {
1021
1358
  for (const env of Object.values(server.environments)) {
@@ -1027,9 +1364,9 @@ import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
1027
1364
  };
1028
1365
  function redeps(server) {
1029
1366
  if (depsSent === null || depsPending) return;
1030
- 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;
1031
1368
  depsPending = Promise.resolve().then(async () => {
1032
- const graph = await depGraph().build(index(), resolve4);
1369
+ const graph = await graphOf(resolve5);
1033
1370
  const json = JSON.stringify(graph);
1034
1371
  if (json === depsSent) return;
1035
1372
  depsSent = json;
@@ -1084,6 +1421,7 @@ import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
1084
1421
  }
1085
1422
  return [
1086
1423
  ...options.storybookShims === false ? [] : [storybookShims()],
1424
+ ...options.propControls === false ? [] : [propControls(() => [...files().keys()], options.propControls === true ? void 0 : options.propControls)],
1087
1425
  {
1088
1426
  name: "weaselforge",
1089
1427
  config(config, { command }) {
@@ -1111,6 +1449,7 @@ import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
1111
1449
  wrappersOf.clear();
1112
1450
  settledAll = null;
1113
1451
  deps = null;
1452
+ history = null;
1114
1453
  depsSent = null;
1115
1454
  },
1116
1455
  resolveId(id) {
@@ -1124,7 +1463,7 @@ import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
1124
1463
  await settledAll;
1125
1464
  }
1126
1465
  if (name !== "deps.js") return modules[name]?.();
1127
- 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);
1128
1467
  depsSent = JSON.stringify(graph);
1129
1468
  return `export default ${depsSent};
1130
1469
  `;
@@ -1135,6 +1474,7 @@ import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
1135
1474
  },
1136
1475
  configureServer(server) {
1137
1476
  server.watcher.add([...files().keys()]);
1477
+ void warnWhenWatchIsSilent(server);
1138
1478
  for (const event of ["add", "change", "unlink"]) {
1139
1479
  server.watcher.on(event, (path) => {
1140
1480
  const file = resolve(path);
@@ -1146,22 +1486,7 @@ import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
1146
1486
  }, logError);
1147
1487
  });
1148
1488
  }
1149
- server.middlewares.use((req, res, next) => {
1150
- const url = req.url ?? "/";
1151
- const path = url.split("?")[0] ?? "/";
1152
- const prefix = base.endsWith("/") ? base.slice(0, -1) : base;
1153
- const page = path === prefix ? "/" : path.startsWith(`${prefix}/`) ? path.slice(prefix.length) : null;
1154
- const entry = req.method === "GET" && page !== null ? PAGES2[page] : void 0;
1155
- if (!entry) return next();
1156
- server.transformIndexHtml(url, html(entry, "serve", titleUnder(root))).then(
1157
- (doc) => {
1158
- res.statusCode = 200;
1159
- res.setHeader("Content-Type", "text/html");
1160
- res.end(doc);
1161
- },
1162
- (err) => next(err)
1163
- );
1164
- });
1489
+ servePages(server, base, () => titleUnder(root));
1165
1490
  }
1166
1491
  }
1167
1492
  ];