@weasel-js/forge 1.6.1 → 1.7.1

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 (43) hide show
  1. package/README.md +35 -2
  2. package/dist/{FrameController-BspC9Zr5.d.ts → FrameController-RdGggdGA.d.ts} +34 -1
  3. package/dist/chunk-JNJGIUYU.js +77 -0
  4. package/dist/chunk-JNJGIUYU.js.map +1 -0
  5. package/dist/{chunk-CSAXZIMV.js → chunk-KIFCYY4F.js} +98 -11
  6. package/dist/chunk-KIFCYY4F.js.map +1 -0
  7. package/dist/{chunk-OLWZURU3.js → chunk-NYBMOSSR.js} +11 -12
  8. package/dist/chunk-NYBMOSSR.js.map +1 -0
  9. package/dist/{chunk-DIOLWCH7.js → chunk-SEK6EKHO.js} +38 -94
  10. package/dist/chunk-SEK6EKHO.js.map +1 -0
  11. package/dist/chunk-WEQSLPKT.js +9 -0
  12. package/dist/chunk-WEQSLPKT.js.map +1 -0
  13. package/dist/{chunk-2GXKNXTJ.js → chunk-Y3WZ2EWR.js} +2 -2
  14. package/dist/chunk-Y3WZ2EWR.js.map +1 -0
  15. package/dist/{chunk-JXIWZFD4.js → chunk-YKN3IAEX.js} +12 -10
  16. package/dist/chunk-YKN3IAEX.js.map +1 -0
  17. package/dist/{config-C5Vq5Kjj.d.ts → config-BJIgUhiI.d.ts} +16 -3
  18. package/dist/csf/preview-api.js +1 -1
  19. package/dist/frame/index.css +63 -7
  20. package/dist/frame/index.css.map +1 -1
  21. package/dist/frame/index.d.ts +7 -2
  22. package/dist/frame/index.js +11 -4
  23. package/dist/frame/index.js.map +1 -1
  24. package/dist/index.d.ts +14 -4
  25. package/dist/index.js +3 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/shell/index.css +165 -3
  28. package/dist/shell/index.css.map +1 -1
  29. package/dist/shell/index.d.ts +44 -31
  30. package/dist/shell/index.js +178 -202
  31. package/dist/shell/index.js.map +1 -1
  32. package/dist/test/index.d.ts +2 -1
  33. package/dist/test/index.js +5 -4
  34. package/dist/test/index.js.map +1 -1
  35. package/dist/vite/index.d.ts +7 -1
  36. package/dist/vite/index.js +367 -32
  37. package/dist/vite/index.js.map +1 -1
  38. package/package.json +10 -10
  39. package/dist/chunk-2GXKNXTJ.js.map +0 -1
  40. package/dist/chunk-CSAXZIMV.js.map +0 -1
  41. package/dist/chunk-DIOLWCH7.js.map +0 -1
  42. package/dist/chunk-JXIWZFD4.js.map +0 -1
  43. package/dist/chunk-OLWZURU3.js.map +0 -1
@@ -1,7 +1,8 @@
1
- import { F as FrameSetup } from '../FrameController-BspC9Zr5.js';
1
+ import { F as FrameSetup } from '../FrameController-RdGggdGA.js';
2
2
  import '@weasel-js/labkit/config';
3
3
  import 'react';
4
4
 
5
+ /** Options for `runStory`. */
5
6
  interface RunStoryOptions {
6
7
  /** The frame config (`frameConfig`). */
7
8
  setup?: FrameSetup;
@@ -1,8 +1,9 @@
1
- import { StoryTrial } from '../chunk-DIOLWCH7.js';
2
- import { loadStories } from '../chunk-OLWZURU3.js';
3
- import '../chunk-2GXKNXTJ.js';
1
+ import { StoryTrial } from '../chunk-SEK6EKHO.js';
2
+ import '../chunk-JNJGIUYU.js';
3
+ import { loadStories } from '../chunk-NYBMOSSR.js';
4
+ import '../chunk-Y3WZ2EWR.js';
4
5
  import '../chunk-HRGPFQBW.js';
5
- import '../chunk-JXIWZFD4.js';
6
+ import '../chunk-YKN3IAEX.js';
6
7
  import { withValueAtPath } from '@weasel-js/labkit/config';
7
8
  import { useState, useEffect } from 'react';
8
9
  import { createRoot } from 'react-dom/client';
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/test/runStory.tsx"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { withValueAtPath } from '@weasel-js/labkit/config';\nimport { type RefObject, useEffect, useState } from 'react';\nimport { createRoot, type Root } from 'react-dom/client';\nimport type { FrameSetup } from '../frame/FrameController';\nimport type { Viewport } from '../protocol/messages';\nimport { StoryTrial } from '../shell/StoryTrial';\nimport { loadStories } from '../story/load';\nimport type { LoadedStory } from '../story/types';\n\n/** Prefixes `error` with the phase it came from, keeping its stack. */\nfunction phased(phase: string, error: unknown): Error {\n const message = error instanceof Error ? error.message : String(error);\n const out = new Error(`${phase} fault: ${message}`, { cause: error });\n if (error instanceof Error && error.stack) out.stack = `${phase} fault: ${error.stack}`;\n return out;\n}\n\nconst settle = () =>\n new Promise<void>((resolve) => {\n requestAnimationFrame(() => setTimeout(resolve, 0));\n });\n\ninterface TestTrialProps {\n story: LoadedStory;\n setup: FrameSetup;\n hostRef: RefObject<HTMLElement | null>;\n /** The config in force after each commit, so `play` reads what the story last wrote. */\n onConfig: (config: unknown) => void;\n onRendered: () => void;\n onError: (error: unknown) => void;\n}\n\n/** The trial a story test runs in: its config and state, held the way a lab trial holds them. */\nfunction TestTrial({ story, setup, hostRef, onConfig, onRendered, onError }: TestTrialProps) {\n const [config, setConfigState] = useState<unknown>(() => story.config.defaults());\n const [state, setState] = useState<unknown>(null);\n useEffect(() => onConfig(config), [config, onConfig]);\n const ctx = {\n config,\n state,\n setConfig: (path: string, value: unknown) => setConfigState((prev: unknown) => withValueAtPath(prev, path, value)),\n setState,\n };\n return <StoryTrial story={story} setup={setup} ctx={ctx} hostRef={hostRef} onRendered={onRendered} onError={onError} />;\n}\n\nexport interface RunStoryOptions {\n /** The frame config (`frameConfig`). */\n setup?: FrameSetup;\n /** Resizes the page, for a story that names a viewport. Without it, stories run at whatever size the page is. */\n viewport?: (width: number, height: number) => Promise<void>;\n}\n\n/** Stops the story a timed-out test left mounted. */\nlet leftover: (() => unknown) | null = null;\n/** The page's size before any story resized it; a story with no viewport runs at this size. */\nlet pageSize: Viewport | null = null;\n\n/**\n * Renders one story in the test page the way the workshop does — defaults, no state, no globals — then plays it.\n * Throws on a render the story's boundary caught, or a play function that fails.\n */\nexport async function runStory(\n mod: Record<string, unknown>,\n exportName: string,\n file: string,\n autoTitle: string,\n { setup = {}, viewport }: RunStoryOptions = {},\n): Promise<void> {\n leftover?.();\n let story: LoadedStory | undefined;\n try {\n story = loadStories(mod, autoTitle, setup.parameters).find((s) => s.exportName === exportName);\n } catch (error) {\n throw phased('load', error);\n }\n if (!story) throw new Error(`load fault: ${file} has no story export \"${exportName}\"`);\n\n pageSize ??= { width: window.innerWidth, height: window.innerHeight };\n const size = story.viewport ?? pageSize;\n if (viewport && (window.innerWidth !== size.width || window.innerHeight !== size.height)) {\n await viewport(size.width, size.height);\n }\n\n const container = document.createElement('div');\n document.body.append(container);\n let root: Root | undefined;\n /** Returns what unmounting threw, having released everything else regardless. */\n const cleanup = (): { error: unknown } | null => {\n if (leftover === cleanup) leftover = null;\n let thrown: { error: unknown } | null = null;\n try {\n root?.unmount();\n } catch (error) {\n thrown = { error };\n } finally {\n root = undefined;\n container.remove();\n }\n return thrown;\n };\n leftover = cleanup;\n\n const hostRef: RefObject<HTMLElement | null> = { current: null };\n let config: unknown;\n let renderError: { error: unknown } | null = null;\n let markRendered: () => void = () => {};\n const rendered = new Promise<void>((resolve) => {\n markRendered = resolve;\n });\n const fail = (error: unknown) => {\n renderError ??= { error };\n // An error the boundary could not catch leaves nothing to announce `rendered`; the check below throws instead.\n markRendered();\n };\n const checkRender = () => {\n if (renderError) throw phased('render', renderError.error);\n };\n\n let failure: { error: unknown } | null = null;\n try {\n try {\n await setup.prepare?.(story);\n root = createRoot(container, { onUncaughtError: fail });\n root.render(\n <TestTrial\n story={story}\n setup={setup}\n hostRef={hostRef}\n onConfig={(next) => {\n config = next;\n }}\n onRendered={markRendered}\n onError={fail}\n />,\n );\n } catch (error) {\n throw phased('mount', error);\n }\n await rendered;\n await settle();\n checkRender();\n const host = hostRef.current;\n if (!host) throw new Error('render fault: the story host never mounted');\n if (story.play) {\n try {\n await story.play({ canvasElement: host, config, globals: {} });\n } catch (error) {\n throw phased('play', error);\n }\n await settle();\n checkRender();\n }\n } catch (error) {\n failure = { error };\n }\n const cleanupError = cleanup();\n if (failure) throw failure.error;\n if (cleanupError) throw phased('cleanup', cleanupError.error);\n}\n"]}
1
+ {"version":3,"sources":["../../src/test/runStory.tsx"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { withValueAtPath } from '@weasel-js/labkit/config';\nimport { type RefObject, useEffect, useState } from 'react';\nimport { createRoot, type Root } from 'react-dom/client';\nimport type { FrameSetup } from '../frame/FrameController';\nimport type { Viewport } from '../protocol/messages';\nimport { StoryTrial } from '../shell/StoryTrial';\nimport { loadStories } from '../story/load';\nimport type { LoadedStory } from '../story/types';\n\n/** Prefixes `error` with the phase it came from, keeping its stack. */\nfunction phased(phase: string, error: unknown): Error {\n const message = error instanceof Error ? error.message : String(error);\n const out = new Error(`${phase} fault: ${message}`, { cause: error });\n if (error instanceof Error && error.stack) out.stack = `${phase} fault: ${error.stack}`;\n return out;\n}\n\nconst settle = () =>\n new Promise<void>((resolve) => {\n requestAnimationFrame(() => setTimeout(resolve, 0));\n });\n\ninterface TestTrialProps {\n story: LoadedStory;\n setup: FrameSetup;\n hostRef: RefObject<HTMLElement | null>;\n /** The config in force after each commit, so `play` reads what the story last wrote. */\n onConfig: (config: unknown) => void;\n onRendered: () => void;\n onError: (error: unknown) => void;\n}\n\n/** The trial a story test runs in: its config and state, held the way a lab trial holds them. */\nfunction TestTrial({ story, setup, hostRef, onConfig, onRendered, onError }: TestTrialProps) {\n const [config, setConfigState] = useState<unknown>(() => story.config.defaults());\n const [state, setState] = useState<unknown>(null);\n useEffect(() => onConfig(config), [config, onConfig]);\n const ctx = {\n config,\n state,\n setConfig: (path: string, value: unknown) => setConfigState((prev: unknown) => withValueAtPath(prev, path, value)),\n setState,\n };\n return <StoryTrial story={story} setup={setup} ctx={ctx} hostRef={hostRef} onRendered={onRendered} onError={onError} />;\n}\n\n/** Options for `runStory`. */\nexport interface RunStoryOptions {\n /** The frame config (`frameConfig`). */\n setup?: FrameSetup;\n /** Resizes the page, for a story that names a viewport. Without it, stories run at whatever size the page is. */\n viewport?: (width: number, height: number) => Promise<void>;\n}\n\n/** Stops the story a timed-out test left mounted. */\nlet leftover: (() => unknown) | null = null;\n/** The page's size before any story resized it; a story with no viewport runs at this size. */\nlet pageSize: Viewport | null = null;\n\n/**\n * Renders one story in the test page the way the workshop does — defaults, no state, no globals — then plays it.\n * Throws on a render the story's boundary caught, or a play function that fails.\n */\nexport async function runStory(\n mod: Record<string, unknown>,\n exportName: string,\n file: string,\n autoTitle: string,\n { setup = {}, viewport }: RunStoryOptions = {},\n): Promise<void> {\n leftover?.();\n let story: LoadedStory | undefined;\n try {\n story = loadStories(mod, autoTitle, setup.parameters).find((s) => s.exportName === exportName);\n } catch (error) {\n throw phased('load', error);\n }\n if (!story) throw new Error(`load fault: ${file} has no story export \"${exportName}\"`);\n\n pageSize ??= { width: window.innerWidth, height: window.innerHeight };\n const size = story.viewport ?? pageSize;\n if (viewport && (window.innerWidth !== size.width || window.innerHeight !== size.height)) {\n await viewport(size.width, size.height);\n }\n\n const container = document.createElement('div');\n document.body.append(container);\n let root: Root | undefined;\n /** Returns what unmounting threw, having released everything else regardless. */\n const cleanup = (): { error: unknown } | null => {\n if (leftover === cleanup) leftover = null;\n let thrown: { error: unknown } | null = null;\n try {\n root?.unmount();\n } catch (error) {\n thrown = { error };\n } finally {\n root = undefined;\n container.remove();\n }\n return thrown;\n };\n leftover = cleanup;\n\n const hostRef: RefObject<HTMLElement | null> = { current: null };\n let config: unknown;\n let renderError: { error: unknown } | null = null;\n let markRendered: () => void = () => {};\n const rendered = new Promise<void>((resolve) => {\n markRendered = resolve;\n });\n const fail = (error: unknown) => {\n renderError ??= { error };\n // An error the boundary could not catch leaves nothing to announce `rendered`; the check below throws instead.\n markRendered();\n };\n const checkRender = () => {\n if (renderError) throw phased('render', renderError.error);\n };\n\n let failure: { error: unknown } | null = null;\n try {\n try {\n await setup.prepare?.(story);\n root = createRoot(container, { onUncaughtError: fail });\n root.render(\n <TestTrial\n story={story}\n setup={setup}\n hostRef={hostRef}\n onConfig={(next) => {\n config = next;\n }}\n onRendered={markRendered}\n onError={fail}\n />,\n );\n } catch (error) {\n throw phased('mount', error);\n }\n await rendered;\n await settle();\n checkRender();\n const host = hostRef.current;\n if (!host) throw new Error('render fault: the story host never mounted');\n if (story.play) {\n try {\n await story.play({ canvasElement: host, config, globals: {} });\n } catch (error) {\n throw phased('play', error);\n }\n await settle();\n checkRender();\n }\n } catch (error) {\n failure = { error };\n }\n const cleanupError = cleanup();\n if (failure) throw failure.error;\n if (cleanupError) throw phased('cleanup', cleanupError.error);\n}\n"]}
@@ -1,5 +1,6 @@
1
1
  import { Plugin } from 'vite';
2
2
 
3
+ /** Options for the `forge` vite plugin. */
3
4
  interface ForgeOptions {
4
5
  /** Globs of story files, relative to the vite root. */
5
6
  stories: string[];
@@ -7,11 +8,16 @@ interface ForgeOptions {
7
8
  frameConfig?: string;
8
9
  /** Path to the shell config module, relative to the vite root. Only the workshop page imports it. Optional. */
9
10
  shellConfig?: string;
10
- /** Alias Storybook's runtime modules to forge shims (Task 13). Default true. */
11
+ /** Alias Storybook's runtime modules to forge shims. Default true. */
11
12
  storybookShims?: boolean;
12
13
  }
14
+ /**
15
+ * The vite plugin that serves the workshop: it indexes the story files, serves the workshop at `/` and the frame
16
+ * document at `/frame.html`, pushes a new index to the open page as story files change, and builds both pages.
17
+ */
13
18
  declare function forge(options: ForgeOptions): Plugin[];
14
19
 
20
+ /** Options for the `forgeTest` vite plugin. */
15
21
  interface ForgeTestOptions {
16
22
  /** Globs of story files, relative to the vite root. Each matching file becomes a test file. */
17
23
  stories: string[];
@@ -1,8 +1,9 @@
1
+ import { isGallery } from '../chunk-WEQSLPKT.js';
1
2
  import { storyNameFromExport, storyId } from '../chunk-HRGPFQBW.js';
2
3
  import { existsSync, readFileSync, mkdirSync, writeFileSync, globSync, statSync } from 'fs';
3
- import { resolve, relative, sep, matchesGlob, dirname, join, basename } from 'path';
4
- import { fileURLToPath } from 'url';
4
+ import { resolve, sep, relative, matchesGlob, dirname, join, basename } from 'path';
5
5
  import { parse } from '@babel/parser';
6
+ import { fileURLToPath } from 'url';
6
7
 
7
8
  // src/vite/html.ts
8
9
  function html(entry, mode = "serve") {
@@ -74,6 +75,275 @@ function autoTitle(file, root, stories) {
74
75
  }
75
76
  return titleFrom(importPath, ".");
76
77
  }
78
+ var keyOf = (file, local) => `${file}#${local}`;
79
+ var fileOfKey = (key) => key.slice(0, key.lastIndexOf("#"));
80
+ var SCRIPT = /\.[cm]?[jt]sx?$/;
81
+ var DEFAULT_LOCAL = "*default*";
82
+ var TYPE_KEYS = /* @__PURE__ */ new Set(["typeAnnotation", "typeParameters", "typeArguments", "returnType", "superTypeParameters", "implements"]);
83
+ function createDepGraph({ root, read }) {
84
+ const modules = /* @__PURE__ */ new Map();
85
+ const resolutions = /* @__PURE__ */ new Map();
86
+ const info = (file) => {
87
+ if (!modules.has(file)) {
88
+ let parsed = null;
89
+ try {
90
+ parsed = moduleInfo(read(file), file);
91
+ } catch {
92
+ parsed = null;
93
+ }
94
+ modules.set(file, parsed);
95
+ }
96
+ return modules.get(file) ?? null;
97
+ };
98
+ return {
99
+ invalidate(file) {
100
+ const known = modules.has(file) || resolutions.has(file);
101
+ modules.delete(file);
102
+ resolutions.delete(file);
103
+ return known;
104
+ },
105
+ reset() {
106
+ resolutions.clear();
107
+ },
108
+ async build(entries, resolve4) {
109
+ const resolveFrom = (spec, importer) => {
110
+ let byImporter = resolutions.get(importer);
111
+ if (!byImporter) {
112
+ byImporter = /* @__PURE__ */ new Map();
113
+ resolutions.set(importer, byImporter);
114
+ }
115
+ let pending = byImporter.get(spec);
116
+ if (!pending) {
117
+ pending = resolve4(spec, importer).then(
118
+ (id) => {
119
+ const file = id?.split("?")[0];
120
+ return file && !file.startsWith("\0") && SCRIPT.test(file) && !file.split(/[\\/]/).includes("node_modules") ? file : null;
121
+ },
122
+ () => null
123
+ );
124
+ byImporter.set(spec, pending);
125
+ }
126
+ return pending;
127
+ };
128
+ const declaring = async (file, name, depth = 0) => {
129
+ const mod = info(file);
130
+ if (!mod || depth > 32) return null;
131
+ const exp = mod.exports.get(name);
132
+ if (exp?.kind === "local") {
133
+ const imp = mod.imports.get(exp.local);
134
+ return imp ? importKey(file, imp, depth + 1) : keyOf(file, exp.local);
135
+ }
136
+ if (exp?.kind === "from") {
137
+ const target = await resolveFrom(exp.spec, file);
138
+ return target ? declaring(target, exp.imported, depth + 1) : null;
139
+ }
140
+ if (name === "default") return null;
141
+ for (const spec of mod.stars) {
142
+ const target = await resolveFrom(spec, file);
143
+ const found = target ? await declaring(target, name, depth + 1) : null;
144
+ if (found) return found;
145
+ }
146
+ return null;
147
+ };
148
+ const importKey = async (importer, imp, depth = 0) => {
149
+ if (imp.imported === "*") return null;
150
+ const target = await resolveFrom(imp.spec, importer);
151
+ return target ? declaring(target, imp.imported, depth) : null;
152
+ };
153
+ const componentOf = async (entry) => {
154
+ const mod = info(entry.file);
155
+ if (!mod) return null;
156
+ const byTitle = entry.title.split("/").pop()?.replace(/\s+/g, "");
157
+ for (const local of [entry.componentName, byTitle]) {
158
+ const imp = local ? mod.imports.get(local) : void 0;
159
+ const key = imp ? await importKey(entry.file, imp) : null;
160
+ if (key) return key;
161
+ }
162
+ return null;
163
+ };
164
+ const titles = /* @__PURE__ */ new Map();
165
+ for (const entry of entries) {
166
+ if (isGallery(entry)) continue;
167
+ const list = titles.get(entry.title);
168
+ if (list) list.push(entry);
169
+ else titles.set(entry.title, [entry]);
170
+ }
171
+ const components = /* @__PURE__ */ new Map();
172
+ const byKey = /* @__PURE__ */ new Map();
173
+ for (const [title, list] of titles) {
174
+ let key = null;
175
+ for (const file of new Set(list.map((e) => e.file))) {
176
+ key = await componentOf(list.find((e) => e.file === file));
177
+ if (key) break;
178
+ }
179
+ components.set(title, key);
180
+ if (key) byKey.set(key, [...byKey.get(key) ?? [], title]);
181
+ }
182
+ const componentFiles = new Set([...byKey.keys()].map(fileOfKey));
183
+ const reached = async (key) => {
184
+ const out = /* @__PURE__ */ new Set();
185
+ const home = fileOfKey(key);
186
+ const homeDir = dirname(home);
187
+ const visited = /* @__PURE__ */ new Set();
188
+ const walkFile = async (file, roots) => {
189
+ const mod = info(file);
190
+ if (!mod) return;
191
+ const locals = /* @__PURE__ */ new Set();
192
+ if (roots === null) {
193
+ for (const name of mod.refs.keys()) locals.add(name);
194
+ for (const name of mod.imports.keys()) locals.add(name);
195
+ } else {
196
+ const queue = [...roots];
197
+ while (queue.length > 0) {
198
+ const name = queue.pop();
199
+ if (locals.has(name)) continue;
200
+ locals.add(name);
201
+ const here = keyOf(file, name);
202
+ if (here !== key && byKey.has(here)) {
203
+ out.add(here);
204
+ continue;
205
+ }
206
+ for (const ref of mod.refs.get(name) ?? []) {
207
+ if (mod.refs.has(ref) || mod.imports.has(ref)) queue.push(ref);
208
+ }
209
+ }
210
+ }
211
+ for (const name of locals) {
212
+ const imp = mod.imports.get(name);
213
+ if (!imp) continue;
214
+ const target = await importKey(file, imp);
215
+ if (target && byKey.has(target)) {
216
+ if (target !== key) out.add(target);
217
+ continue;
218
+ }
219
+ if (!imp.spec.startsWith(".")) continue;
220
+ const helper = await resolveFrom(imp.spec, file);
221
+ if (!helper || visited.has(helper) || componentFiles.has(helper)) continue;
222
+ if (relative(homeDir, helper).startsWith("..")) continue;
223
+ visited.add(helper);
224
+ await walkFile(helper, null);
225
+ }
226
+ };
227
+ visited.add(home);
228
+ await walkFile(home, [key.slice(home.length + 1)]);
229
+ return out;
230
+ };
231
+ const graph = {};
232
+ for (const [title, key] of components) {
233
+ graph[title] = { source: key ? relative(root, fileOfKey(key)).split(sep).join("/") : null, uses: [], usedBy: [] };
234
+ }
235
+ for (const [title, key] of components) {
236
+ if (!key) continue;
237
+ const uses = /* @__PURE__ */ new Set();
238
+ for (const target of await reached(key)) {
239
+ for (const other of byKey.get(target) ?? []) if (other !== title) uses.add(other);
240
+ }
241
+ const node = graph[title];
242
+ node.uses = [...uses].sort();
243
+ for (const other of node.uses) graph[other]?.usedBy.push(title);
244
+ }
245
+ for (const node of Object.values(graph)) node.usedBy.sort();
246
+ return Object.fromEntries(Object.entries(graph).sort(([a], [b]) => a.localeCompare(b)));
247
+ }
248
+ };
249
+ }
250
+ function moduleInfo(code, file) {
251
+ const plugins = ["typescript", "decorators-legacy", .../\.[cm]?[jt]sx$/.test(file) ? ["jsx"] : []];
252
+ const program = parse(code, { sourceType: "module", plugins }).program;
253
+ const imports = /* @__PURE__ */ new Map();
254
+ const exports = /* @__PURE__ */ new Map();
255
+ const stars = [];
256
+ const refs = /* @__PURE__ */ new Map();
257
+ const declare = (name, node) => refs.set(name, node ? namesIn(node) : /* @__PURE__ */ new Set());
258
+ const declareStatement = (decl) => {
259
+ if (!decl) return [];
260
+ switch (decl.type) {
261
+ case "FunctionDeclaration":
262
+ case "ClassDeclaration":
263
+ if (!decl.id) return [];
264
+ declare(decl.id.name, decl);
265
+ return [decl.id.name];
266
+ case "VariableDeclaration":
267
+ return decl.declarations.flatMap((d) => {
268
+ if (d.id.type !== "Identifier") return [];
269
+ declare(d.id.name, d.init);
270
+ return [d.id.name];
271
+ });
272
+ default:
273
+ return [];
274
+ }
275
+ };
276
+ for (const stmt of program.body) {
277
+ switch (stmt.type) {
278
+ case "ImportDeclaration":
279
+ if (stmt.importKind === "type") break;
280
+ for (const spec of stmt.specifiers) {
281
+ if (spec.type === "ImportSpecifier" && spec.importKind === "type") continue;
282
+ const imported = spec.type === "ImportNamespaceSpecifier" ? "*" : spec.type === "ImportDefaultSpecifier" ? "default" : nameOf(spec.imported);
283
+ imports.set(spec.local.name, { spec: stmt.source.value, imported });
284
+ }
285
+ break;
286
+ case "ExportNamedDeclaration":
287
+ if (stmt.exportKind === "type") break;
288
+ for (const name of declareStatement(stmt.declaration)) exports.set(name, { kind: "local", local: name });
289
+ for (const spec of stmt.specifiers) {
290
+ if (spec.type === "ExportSpecifier" && spec.exportKind === "type") continue;
291
+ if (spec.type !== "ExportSpecifier") continue;
292
+ const exported = nameOf(spec.exported);
293
+ const local = nameOf(spec.local);
294
+ exports.set(exported, stmt.source ? { kind: "from", spec: stmt.source.value, imported: local } : { kind: "local", local });
295
+ }
296
+ break;
297
+ case "ExportAllDeclaration":
298
+ if (stmt.exportKind !== "type") stars.push(stmt.source.value);
299
+ break;
300
+ case "ExportDefaultDeclaration": {
301
+ const decl = stmt.declaration;
302
+ const named = (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration") && decl.id ? decl.id.name : null;
303
+ if (named) declare(named, decl);
304
+ else if (decl.type === "Identifier") {
305
+ exports.set("default", { kind: "local", local: decl.name });
306
+ break;
307
+ } else declare(DEFAULT_LOCAL, decl);
308
+ exports.set("default", { kind: "local", local: named ?? DEFAULT_LOCAL });
309
+ break;
310
+ }
311
+ default:
312
+ declareStatement(stmt);
313
+ }
314
+ }
315
+ return { imports, exports, stars, refs };
316
+ }
317
+ function nameOf(node) {
318
+ return node.type === "Identifier" ? node.name : node.value;
319
+ }
320
+ function namesIn(node) {
321
+ const out = /* @__PURE__ */ new Set();
322
+ const walk = (value, key) => {
323
+ if (key !== void 0 && TYPE_KEYS.has(key)) return;
324
+ if (Array.isArray(value)) {
325
+ for (const item of value) walk(item);
326
+ return;
327
+ }
328
+ if (!value || typeof value !== "object" || typeof value.type !== "string") return;
329
+ const n = value;
330
+ if (n.type.startsWith("TS") && !("expression" in n)) return;
331
+ if (n.type === "Identifier" || n.type === "JSXIdentifier") {
332
+ out.add(n.name);
333
+ return;
334
+ }
335
+ for (const [k, child] of Object.entries(n)) {
336
+ if (k === "leadingComments" || k === "trailingComments" || k === "innerComments" || k === "loc") continue;
337
+ if ((n.type === "MemberExpression" || n.type === "OptionalMemberExpression") && k === "property" && !n.computed) continue;
338
+ if (n.type === "JSXMemberExpression" && k === "property") continue;
339
+ if ((n.type === "ObjectProperty" || n.type === "ObjectMethod" || n.type === "ClassMethod" || n.type === "ClassProperty") && k === "key" && !n.computed) continue;
340
+ if (n.type === "JSXAttribute" && k === "name") continue;
341
+ walk(child, k);
342
+ }
343
+ };
344
+ walk(node);
345
+ return out;
346
+ }
77
347
  function indexFile(code, file, autoTitle2) {
78
348
  try {
79
349
  const plugins = ["typescript", "decorators-legacy", .../\.[jt]sx$/.test(file) ? ["jsx"] : []];
@@ -119,7 +389,7 @@ function indexProgram(program, file, autoTitle2) {
119
389
  for (const spec of stmt.specifiers) {
120
390
  if (spec.type !== "ExportSpecifier" || spec.exportKind === "type") continue;
121
391
  const name = exportedName(spec.exported);
122
- const node = stmt.source ? void 0 : bindings.get(spec.local.name);
392
+ const node = stmt.source ? void 0 : bindings.get(exportedName(spec.local));
123
393
  if (name === "default") {
124
394
  metaNode = node;
125
395
  metaDoc ??= doc;
@@ -134,9 +404,11 @@ function indexProgram(program, file, autoTitle2) {
134
404
  const componentNode = prop(meta, "component");
135
405
  const componentName = componentNode?.type === "Identifier" ? componentNode.name : void 0;
136
406
  const metaIsolate = isolateOf(meta, bindings, wrappers, "default");
407
+ const metaTags = tagsOf(meta);
137
408
  return exported.filter(({ name }) => name !== "__namedExportsOrder").filter(({ name }) => (!include || include(name)) && (!exclude || !exclude(name))).map(({ name, node, doc }) => {
138
409
  const spec = objectOf(node, bindings, wrappers);
139
410
  const isolate = isolateOf(spec, bindings, wrappers, name) ?? metaIsolate;
411
+ const tags = combineTags(metaTags, tagsOf(spec));
140
412
  return {
141
413
  id: storyId(title, name),
142
414
  title,
@@ -146,7 +418,8 @@ function indexProgram(program, file, autoTitle2) {
146
418
  ...doc === void 0 ? {} : { description: doc },
147
419
  ...metaDoc === void 0 ? {} : { componentDescription: metaDoc },
148
420
  ...componentName === void 0 ? {} : { componentName },
149
- ...isolate === void 0 ? {} : { isolate }
421
+ ...isolate === void 0 ? {} : { isolate },
422
+ ...tags === void 0 ? {} : { tags }
150
423
  };
151
424
  });
152
425
  }
@@ -159,6 +432,20 @@ function isolateOf(obj, bindings, wrappers, exportName) {
159
432
  if (literal === void 0) throw new Error(`${exportName}: isolate must be a string literal`);
160
433
  return literal;
161
434
  }
435
+ function tagsOf(obj) {
436
+ const value = prop(obj, "tags");
437
+ if (value?.type !== "ArrayExpression") return void 0;
438
+ return value.elements.flatMap((e) => e ? stringLiteral(e) ?? [] : []);
439
+ }
440
+ function combineTags(meta, own) {
441
+ if (meta === void 0 && own === void 0) return void 0;
442
+ const out = new Set(meta);
443
+ for (const tag of own ?? []) {
444
+ if (tag.startsWith("!")) out.delete(tag.slice(1));
445
+ else out.add(tag);
446
+ }
447
+ return [...out];
448
+ }
162
449
  function stringLiteral(node) {
163
450
  if (node.type === "StringLiteral") return node.value;
164
451
  if (node.type === "TemplateLiteral" && node.expressions.length === 0) return node.quasis.map((q) => q.value.cooked ?? q.value.raw).join("");
@@ -221,6 +508,18 @@ function storyFilter(obj, key) {
221
508
  }
222
509
  return null;
223
510
  }
511
+ function ownEntries(realms) {
512
+ const here = fileURLToPath(import.meta.url);
513
+ for (let dir = dirname(here); dir !== dirname(dir); dir = dirname(dir)) {
514
+ const manifest = join(dir, "package.json");
515
+ if (!existsSync(manifest) || JSON.parse(readFileSync(manifest, "utf8")).name !== "@weasel-js/forge") {
516
+ continue;
517
+ }
518
+ const source = !relative(join(dir, "src"), here).startsWith("..");
519
+ return realms.map((realm) => join(dir, source ? `src/${realm}/index.ts` : `dist/${realm}/index.js`));
520
+ }
521
+ return [];
522
+ }
224
523
  var SHIMS = [
225
524
  [/^@?storybook\/preview-api$/, "@weasel-js/forge/preview-api"],
226
525
  [/^@?storybook\/test$/, "@weasel-js/forge/play"]
@@ -239,25 +538,16 @@ function storybookShims() {
239
538
 
240
539
  // src/vite/plugin.ts
241
540
  var PREFIX = "virtual:forge/";
242
- var MODULES = /* @__PURE__ */ new Set(["index.js", "importers.js", "frame-config.js", "shell-config.js", "shell-entry.js", "frame-entry.js"]);
541
+ var MODULES = /* @__PURE__ */ new Set(["index.js", "importers.js", "deps.js", "frame-config.js", "shell-config.js", "shell-entry.js", "frame-entry.js"]);
243
542
  var PAGES2 = { "/": "shell-entry.js", "/index.html": "shell-entry.js", "/frame.html": "frame-entry.js" };
244
- function ownEntries() {
245
- const here = fileURLToPath(import.meta.url);
246
- for (let dir = dirname(here); dir !== dirname(dir); dir = dirname(dir)) {
247
- const manifest = join(dir, "package.json");
248
- if (!existsSync(manifest) || JSON.parse(readFileSync(manifest, "utf8")).name !== "@weasel-js/forge") {
249
- continue;
250
- }
251
- const source = !relative(join(dir, "src"), here).startsWith("..");
252
- return ["shell", "frame"].map((realm) => join(dir, source ? `src/${realm}/index.ts` : `dist/${realm}/index.js`));
253
- }
254
- return [];
255
- }
256
543
  function forge(options) {
257
544
  let root = process.cwd();
258
545
  let base = "/";
259
546
  let byFile = null;
260
547
  let logger;
548
+ let deps = null;
549
+ let depsSent = null;
550
+ let depsPending = null;
261
551
  const glob = () => {
262
552
  const files2 = options.stories.flatMap(
263
553
  (pattern) => globSync(pattern, { cwd: root, exclude: (name) => basename(String(name)) === "node_modules" }).map((f) => resolve(root, f)).filter((f) => statSync(f).isFile()).sort()
@@ -284,6 +574,7 @@ function forge(options) {
284
574
  return byFile;
285
575
  };
286
576
  const index = () => [...files().values()].flat();
577
+ const depGraph = () => deps ??= createDepGraph({ root, read: (file) => readFileSync(file, "utf8") });
287
578
  const configModule = (path) => path ? `export { default } from ${JSON.stringify(resolve(root, path))};
288
579
  ` : "export default {};\n";
289
580
  const modules = {
@@ -315,6 +606,9 @@ const workshop = mountWorkshop({
315
606
  stories: ${JSON.stringify(options.stories)},
316
607
  });
317
608
  import.meta.hot?.on('forge:index', (next) => workshop.setIndex(next));
609
+ // Read from source on first request, so the graph costs nothing until the page is up.
610
+ import('virtual:forge/deps.js').then((m) => workshop.setDependencies(m.default));
611
+ import.meta.hot?.on('forge:deps', (next) => workshop.setDependencies(next));
318
612
  // A story edit reaches here as an update of the importers module, whose fresh import() URLs carry the new
319
613
  // timestamps; the file it names arrives just before, so the reload waits for the importers that can fetch it.
320
614
  const stale = new Set();
@@ -338,12 +632,34 @@ mountFrame({ index, importers, setup });
338
632
  import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
339
633
  `
340
634
  };
635
+ const invalidate = (server, names) => {
636
+ for (const env of Object.values(server.environments)) {
637
+ for (const name of names) {
638
+ const mod = env.moduleGraph.getModuleById(`\0${PREFIX}${name}`);
639
+ if (mod) env.moduleGraph.invalidateModule(mod);
640
+ }
641
+ }
642
+ };
643
+ function redeps(server) {
644
+ if (depsSent === null || depsPending) return;
645
+ const resolve4 = async (spec, importer) => (await server.environments.client.pluginContainer.resolveId(spec, importer))?.id ?? null;
646
+ depsPending = Promise.resolve().then(async () => {
647
+ const graph = await depGraph().build(index(), resolve4);
648
+ const json = JSON.stringify(graph);
649
+ if (json === depsSent) return;
650
+ depsSent = json;
651
+ invalidate(server, ["deps.js"]);
652
+ server.ws.send({ type: "custom", event: "forge:deps", data: graph });
653
+ }).catch(logError).finally(() => {
654
+ depsPending = null;
655
+ });
656
+ }
341
657
  function reindex(server, file, event) {
342
658
  const current = files();
343
659
  if (event === "change" && current.has(file)) server.ws.send({ type: "custom", event: "forge:story", data: { file } });
344
- if (event === "change" && !current.has(file)) return;
345
- if (event === "add" && !matches(file)) return;
346
- if (event === "unlink" && !current.has(file)) return;
660
+ if (event === "change" && !current.has(file)) return false;
661
+ if (event === "add" && !matches(file)) return false;
662
+ if (event === "unlink" && !current.has(file)) return false;
347
663
  const before = JSON.stringify(index());
348
664
  if (event === "unlink") {
349
665
  current.delete(file);
@@ -352,18 +668,14 @@ import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
352
668
  current.set(file, read(file));
353
669
  } catch (err) {
354
670
  logError(err);
355
- return;
671
+ return false;
356
672
  }
357
673
  }
358
674
  const next = index();
359
- if (JSON.stringify(next) === before) return;
360
- for (const env of Object.values(server.environments)) {
361
- for (const name of ["index.js", "importers.js"]) {
362
- const mod = env.moduleGraph.getModuleById(`\0${PREFIX}${name}`);
363
- if (mod) env.moduleGraph.invalidateModule(mod);
364
- }
365
- }
675
+ if (JSON.stringify(next) === before) return false;
676
+ invalidate(server, ["index.js", "importers.js"]);
366
677
  server.ws.send({ type: "custom", event: "forge:index", data: next });
678
+ return true;
367
679
  }
368
680
  return [
369
681
  ...options.storybookShims === false ? [] : [storybookShims()],
@@ -374,7 +686,7 @@ import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
374
686
  const entries = [
375
687
  ...options.stories,
376
688
  ...[options.frameConfig, options.shellConfig].flatMap((path) => path ? [resolve(at, path)] : []),
377
- ...ownEntries()
689
+ ...ownEntries(["shell", "frame"])
378
690
  ];
379
691
  const optimizeDeps = { entries };
380
692
  if (command !== "build") return { optimizeDeps };
@@ -391,18 +703,32 @@ import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
391
703
  base = config.base;
392
704
  logger = config.logger;
393
705
  byFile = null;
706
+ deps = null;
707
+ depsSent = null;
394
708
  },
395
709
  resolveId(id) {
396
710
  return id.startsWith(PREFIX) && MODULES.has(id.slice(PREFIX.length)) ? `\0${id}` : void 0;
397
711
  },
398
- load(id) {
712
+ async load(id) {
399
713
  if (!id.startsWith(`\0${PREFIX}`)) return void 0;
400
- return modules[id.slice(PREFIX.length + 1)]?.();
714
+ const name = id.slice(PREFIX.length + 1);
715
+ if (name !== "deps.js") return modules[name]?.();
716
+ const graph = await depGraph().build(index(), async (spec, importer) => (await this.resolve(spec, importer))?.id ?? null);
717
+ depsSent = JSON.stringify(graph);
718
+ return `export default ${depsSent};
719
+ `;
401
720
  },
402
721
  configureServer(server) {
403
722
  server.watcher.add([...files().keys()]);
404
723
  for (const event of ["add", "change", "unlink"]) {
405
- server.watcher.on(event, (file) => reindex(server, resolve(file), event));
724
+ server.watcher.on(event, (path) => {
725
+ const file = resolve(path);
726
+ const indexed = reindex(server, file, event);
727
+ const added = event === "add" && /\.[cm]?[jt]sx?$/.test(file) && !file.split(sep).includes("node_modules");
728
+ if (added) depGraph().reset();
729
+ const read2 = added || event !== "add" && depGraph().invalidate(file);
730
+ if (indexed || read2) redeps(server);
731
+ });
406
732
  }
407
733
  server.middlewares.use((req, res, next) => {
408
734
  const url = req.url ?? "/";
@@ -438,6 +764,15 @@ function forgeTest(options) {
438
764
  {
439
765
  name: "weaselforge:test",
440
766
  enforce: "pre",
767
+ config(config) {
768
+ const at = resolve(config.root ?? process.cwd());
769
+ const entries = [
770
+ ...options.stories,
771
+ ...options.frameConfig ? [resolve(at, options.frameConfig)] : [],
772
+ ...ownEntries(["test"])
773
+ ];
774
+ return { optimizeDeps: { entries } };
775
+ },
441
776
  configResolved(config) {
442
777
  root = config.root;
443
778
  },