forgebook 0.2.0 → 0.3.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 (94) hide show
  1. package/README.md +113 -5
  2. package/dist/bin/forgebook/agentLoopCommands.mjs +3 -0
  3. package/dist/bin/forgebook/agentText.d.ts +2 -2
  4. package/dist/bin/forgebook/agentText.js +15 -13
  5. package/dist/bin/forgebook/agents.md +21 -6
  6. package/dist/bin/forgebook/mcpTools.js +1 -1
  7. package/dist/bin/forgebook/newStoriesFile.js +7 -2
  8. package/dist/bin/forgebook-docs/collectDocs.js +1 -1
  9. package/dist/src/argsProps.d.ts +32 -0
  10. package/dist/src/argsProps.js +69 -0
  11. package/dist/src/catalogue.d.ts +48 -5
  12. package/dist/src/catalogue.js +47 -8
  13. package/dist/src/frame/mountFrame.js +26 -7
  14. package/dist/src/frame/touchCaret.d.ts +43 -0
  15. package/dist/src/frame/touchCaret.js +195 -0
  16. package/dist/src/frame/touchEvents.d.ts +3 -2
  17. package/dist/src/frame/touchEvents.js +17 -18
  18. package/dist/src/frame/touchLayer.js +48 -7
  19. package/dist/src/propGroups.d.ts +66 -3
  20. package/dist/src/propGroups.js +67 -13
  21. package/dist/src/shell/ApiReference.d.ts +9 -4
  22. package/dist/src/shell/ApiReference.js +47 -39
  23. package/dist/src/shell/AudienceSections.js +1 -1
  24. package/dist/src/shell/ComponentPage.d.ts +5 -2
  25. package/dist/src/shell/ComponentPage.js +4 -18
  26. package/dist/src/shell/DocBlocks.js +1 -1
  27. package/dist/src/shell/PropRows.d.ts +42 -0
  28. package/dist/src/shell/PropRows.js +60 -0
  29. package/dist/src/shell/PropsTab.d.ts +14 -10
  30. package/dist/src/shell/PropsTab.js +18 -58
  31. package/dist/src/shell/RequiredFirstSwitch.d.ts +16 -0
  32. package/dist/src/shell/RequiredFirstSwitch.js +8 -0
  33. package/dist/src/shell/Stage.js +11 -23
  34. package/dist/src/shell/StoryCoverage.js +4 -1
  35. package/dist/src/shell/docPage.css +6 -0
  36. package/dist/src/shell/dockTabs.d.ts +7 -5
  37. package/dist/src/shell/dockTabs.js +4 -4
  38. package/dist/src/shell/reportedPanel.d.ts +12 -0
  39. package/dist/src/shell/reportedPanel.js +19 -0
  40. package/dist/src/shell/shell.css +56 -1
  41. package/dist/src/shell/strings.d.ts +2 -0
  42. package/dist/src/shell/strings.js +2 -0
  43. package/dist/src/shell/usePanelDocs.d.ts +11 -0
  44. package/dist/src/shell/usePanelDocs.js +29 -0
  45. package/dist/src/shell/useRequiredFirst.d.ts +27 -0
  46. package/dist/src/shell/useRequiredFirst.js +67 -0
  47. package/dist/src/storyCheck.d.ts +2 -0
  48. package/dist/src/storyCheck.js +2 -1
  49. package/dist/src/storyGlobals.js +2 -1
  50. package/dist/src/storyTree.d.ts +11 -1
  51. package/dist/src/storyTree.js +3 -1
  52. package/dist/src/viewState.d.ts +7 -0
  53. package/dist/src/viewState.js +13 -0
  54. package/dist/src/vite/agent.types.d.ts +12 -0
  55. package/dist/src/vite/agentApi.js +6 -1
  56. package/dist/src/vite/agentRoutes.js +18 -4
  57. package/dist/src/vite/builtTexts.d.ts +2 -2
  58. package/dist/src/vite/builtTexts.js +7 -6
  59. package/dist/src/vite/configFile.d.ts +3 -2
  60. package/dist/src/vite/configFile.js +63 -14
  61. package/dist/src/vite/docPagesModule.d.ts +10 -7
  62. package/dist/src/vite/docPagesModule.js +14 -11
  63. package/dist/src/vite/docsExtractor.d.ts +6 -0
  64. package/dist/src/vite/docsExtractor.js +13 -0
  65. package/dist/src/vite/fileAccess.d.ts +75 -0
  66. package/dist/src/vite/fileAccess.js +199 -0
  67. package/dist/src/vite/forgebookPlugin.d.ts +1 -1
  68. package/dist/src/vite/forgebookPlugin.js +67 -98
  69. package/dist/src/vite/framesServer.js +21 -5
  70. package/dist/src/vite/generatedModules.d.ts +23 -0
  71. package/dist/src/vite/generatedModules.js +58 -0
  72. package/dist/src/vite/hotSnippets.js +7 -3
  73. package/dist/src/vite/indexStoryFile.d.ts +2 -2
  74. package/dist/src/vite/indexStoryFile.js +2 -2
  75. package/dist/src/vite/indexedParameters.d.ts +1 -1
  76. package/dist/src/vite/indexedParameters.js +21 -1
  77. package/dist/src/vite/originGuard.d.ts +14 -4
  78. package/dist/src/vite/originGuard.js +15 -6
  79. package/dist/src/vite/pages.d.ts +3 -2
  80. package/dist/src/vite/pages.js +23 -16
  81. package/dist/src/vite/pluginIds.d.ts +24 -2
  82. package/dist/src/vite/pluginIds.js +44 -6
  83. package/dist/src/vite/pluginModules.d.ts +39 -10
  84. package/dist/src/vite/pluginModules.js +67 -22
  85. package/dist/src/vite/prebundled.js +2 -1
  86. package/dist/src/vite/searchModule.d.ts +6 -4
  87. package/dist/src/vite/searchModule.js +8 -7
  88. package/dist/src/vite/storyFilesModule.d.ts +59 -27
  89. package/dist/src/vite/storyFilesModule.js +88 -58
  90. package/dist/src/vite/storyPatterns.d.ts +39 -0
  91. package/dist/src/vite/storyPatterns.js +100 -0
  92. package/dist/src/vite/textQueries.d.ts +7 -1
  93. package/dist/src/vite/textQueries.js +7 -1
  94. package/package.json +4 -4
@@ -1,5 +1,6 @@
1
1
  /**
2
- * The modules that answer a `?forgebook-docs-pages` import: every page a pattern matches as one module with each page's
2
+ * The modules that answer a group of docs pages, a `?forgebook-docs-pages` import's pattern or a catalogue's `docs`
3
+ * list (`DOCS_PAGES_SPECIFIER_PREFIX`): every page the group lists as one module with each page's
3
4
  * index, and the loader of the page itself, and each page's own module.
4
5
  *
5
6
  * @remarks
@@ -15,7 +16,7 @@ import { codeLiteral } from "../codeLiteral.js";
15
16
  import { readLinkedFile } from "./docPageLinks.js";
16
17
  import { compiledDocPage, compiledHome, docPageIndex } from "./docPageMarkdown.js";
17
18
  import { pluginFences } from "./pluginConfig.js";
18
- import { PAGES_SEARCH_PREFIX, storyFileKeys, storyFilesGroupText } from "./storyFilesModule.js";
19
+ import { EVERY_FILE, PAGES_SEARCH_PREFIX, storyFileKeys, storyFilesGroupText } from "./storyFilesModule.js";
19
20
  import { DOCS_PAGE_QUERY } from "./textQueries.js";
20
21
  import { VITE_PATHS } from "./vitePaths.js";
21
22
  /** Reads a linked page, watching it: its title makes the link's address, so a new title renews the page that links. */
@@ -26,15 +27,16 @@ const watchedRead = (watch) => (file) => {
26
27
  return text;
27
28
  };
28
29
  /**
29
- * Every page a pattern matches, indexed.
30
+ * Every page a group's patterns list, indexed.
30
31
  *
31
32
  * @param root - the declaring module's folder.
32
- * @param pattern - the pattern.
33
+ * @param patterns - the patterns, `!` ones included (`listedFiles`).
33
34
  * @param watch - adds a file whose edit renews the module that read it.
34
35
  * @param fences - the fences the catalogue's plugins draw, which are not code; none by default.
36
+ * @param isKept - whether a listed page may be read; every one by default.
35
37
  */
36
- export const indexedDocPages = (root, pattern, watch, fences = []) => {
37
- return storyFileKeys(root, [pattern]).map((key) => {
38
+ export const indexedDocPages = (root, patterns, watch, fences = [], isKept = EVERY_FILE) => {
39
+ return storyFileKeys(root, patterns, isKept).map((key) => {
38
40
  const file = resolve(root, key);
39
41
  watch(file);
40
42
  return { key, file, index: docPageIndex(file, readFileSync(file, "utf8"), watchedRead(watch), fences) };
@@ -43,20 +45,21 @@ export const indexedDocPages = (root, pattern, watch, fences = []) => {
43
45
  /** An object literal of a key per page, each value written by `value`. */
44
46
  const objectOf = (keys, value) => `{${keys.map((key) => `${codeLiteral(key)}: ${value(key)}`).join(",\n")}}`;
45
47
  /**
46
- * The code of the module for one pattern.
48
+ * The code of the module for one group.
47
49
  *
48
50
  * @param root - the declaring module's folder.
49
- * @param pattern - the pattern before `?forgebook-docs-pages`.
51
+ * @param patterns - the group's patterns.
50
52
  * @param watch - adds a file whose edit renews the module (each page: its index is in the module).
51
53
  * @param isServing - whether a dev server runs, whose tools read a page's Markdown.
52
54
  * @param plugins - the catalogue's plugins, whose fences a page may draw; none by default.
55
+ * @param isKept - whether a listed page may be read; every one by default.
53
56
  */
54
- export const docPagesModule = (root, pattern, watch, isServing, plugins = []) => {
55
- const pages = indexedDocPages(root, pattern, watch, pluginFences(plugins));
57
+ export const docPagesModule = (root, patterns, watch, isServing, plugins = [], isKept = EVERY_FILE) => {
58
+ const pages = indexedDocPages(root, patterns, watch, pluginFences(plugins), isKept);
56
59
  const keys = pages.map((page) => page.key);
57
60
  const index = Object.fromEntries(pages.map((page) => [page.key, page.index]));
58
61
  const page = (key) => codeLiteral(`${VITE_PATHS.resolve(root, key)}${DOCS_PAGE_QUERY}`);
59
- const search = codeLiteral(`${PAGES_SEARCH_PREFIX}${storyFilesGroupText(root, pattern)}`);
62
+ const search = codeLiteral(`${PAGES_SEARCH_PREFIX}${storyFilesGroupText(root, patterns)}`);
60
63
  return [
61
64
  "export default {",
62
65
  `index: ${codeLiteral(index)},`,
@@ -101,6 +101,12 @@ export type DocsExtractorOptions = {
101
101
  * looks keeps them, renewed after each edit rather than built again (seconds, in a large monorepo).
102
102
  */
103
103
  export declare const IDLE_MS: number;
104
+ /**
105
+ * The extractor of a catalogue that reads no component docs (`componentDocs: false`): it starts no worker and builds no
106
+ * TypeScript program, answers every story file as one whose meta names no component, and every group's usage with why
107
+ * there is none.
108
+ */
109
+ export declare const NO_DOCS_EXTRACTOR: DocsExtractor;
104
110
  /**
105
111
  * A docs extractor with its worker, started on its first request.
106
112
  *
@@ -62,6 +62,19 @@ const codeStampOf = (docsModule) => {
62
62
  return undefined;
63
63
  }
64
64
  };
65
+ /**
66
+ * The extractor of a catalogue that reads no component docs (`componentDocs: false`): it starts no worker and builds no
67
+ * TypeScript program, answers every story file as one whose meta names no component, and every group's usage with why
68
+ * there is none.
69
+ */
70
+ export const NO_DOCS_EXTRACTOR = {
71
+ extract: () => Promise.resolve({ docs: { name: null } }),
72
+ usage: () => Promise.resolve("The catalogue reads no component docs (componentDocs: false)"),
73
+ warm: () => undefined,
74
+ changed: () => undefined,
75
+ listed: () => undefined,
76
+ close: () => Promise.resolve()
77
+ };
65
78
  /**
66
79
  * A docs extractor with its worker, started on its first request.
67
80
  *
@@ -0,0 +1,75 @@
1
+ import { type ResolvedConfig, type ViteDevServer } from "vite";
2
+ import type { IsFileKept } from "./storyFilesModule.ts";
3
+ /**
4
+ * Whether Vite refuses a listed file whatever `server.fs.allow` says: `deny`, or a path it will not trust (a `:` in it).
5
+ * Vite offers no check of this step alone, so this asks its whole check with an `allow` that covers the file.
6
+ *
7
+ * @param config - the dev server's configuration.
8
+ * @param file - the file, absolute in Vite's form.
9
+ */
10
+ export declare const isFileRefused: (config: ResolvedConfig, file: string) => boolean;
11
+ /**
12
+ * Whether a dev server may read the file a query's id names. An id Vite resolved is normalised; one with a `..` came
13
+ * from an address unresolved, and Vite's check reads a path as written, so `/project/../secret` would pass for a file
14
+ * of `/project`.
15
+ *
16
+ * @param config - the dev server's configuration.
17
+ * @param id - the module's id, the file and then its query.
18
+ */
19
+ export declare const isQueriedFileReadable: (config: ResolvedConfig, id: string) => boolean;
20
+ /** What a plugin's dev server lets a client read: `devServerAccess` makes it. */
21
+ export type DevServerAccess = {
22
+ /**
23
+ * Starts on a dev server; until then (and in a build, which has none) nothing is refused.
24
+ *
25
+ * @param server - the dev server.
26
+ * @param sources - the module that imports the configuration's sources; none without a configuration.
27
+ */
28
+ readonly serve: (server: ViteDevServer, sources: string | undefined) => void;
29
+ /**
30
+ * Whether a resolver's importer is a module, not the stand-in Vite gives an address asked for directly.
31
+ *
32
+ * @param importer - the importer the resolver was given.
33
+ */
34
+ readonly isModule: (importer: string | undefined) => importer is string;
35
+ /**
36
+ * Notes that a module imports a group, whose id is now served.
37
+ *
38
+ * @param id - the group module's id.
39
+ */
40
+ readonly noteImported: (id: string) => void;
41
+ /** Whether a file a group lists may be read; says so once when not, as nothing else would tell why it is missing. */
42
+ readonly isKept: IsFileKept;
43
+ /**
44
+ * Holds a request for a group no import has named until the configuration's sources have been read.
45
+ *
46
+ * @param url - the request's address.
47
+ */
48
+ readonly settle: (url: string) => Promise<void>;
49
+ /**
50
+ * What the plugin's load answers for a file's query `server.fs` refuses: an empty module and not Vite's 404, as Vite
51
+ * would then read a path with a `..` as written, past the check that refused it. Asked of every id, since an address
52
+ * can name an id directly, past the plugin's `resolveId`.
53
+ *
54
+ * @param id - the module's id.
55
+ * @returns the module, or none when the client may read the id.
56
+ * @throws for a group no module of the project imports, with Vite's code for a missing file.
57
+ */
58
+ readonly refusal: (id: string) => {
59
+ readonly code: string;
60
+ readonly moduleType: "js";
61
+ } | undefined;
62
+ };
63
+ /** What a plugin's dev server lets a client read. */
64
+ export declare const devServerAccess: () => DevServerAccess;
65
+ /**
66
+ * The plugin's hook for Vitest (`configureVitest`), whose module cache keeps each module's transform on disk between
67
+ * runs: a module read from it resolves no import, so a group it names would stay refused from the second run on.
68
+ */
69
+ export declare const VITEST_HOOKS: {
70
+ configureVitest: (context: {
71
+ readonly defineCacheKeyGenerator?: (generator: (module: {
72
+ readonly sourceCode: string;
73
+ }) => false | undefined) => void;
74
+ }) => void;
75
+ };
@@ -0,0 +1,199 @@
1
+ /**
2
+ * What a dev server's plugin may read for a client, by Vite's own `server.fs` and by what the project imports: a
3
+ * plugin's answer never passes through Vite's check, so each read an address can name asks here first.
4
+ *
5
+ * @remarks
6
+ * Vite's rule is two steps. The first refuses a file whatever else holds (`server.fs.deny`, a path it will not trust).
7
+ * Past it, a file is served when `allow` covers it or a module of the project imports it. A group of story files or
8
+ * docs pages is what a module of the project imports, so it keeps to the first step alone: a catalogue's `../src` may be
9
+ * outside `allow`, and Vite serves its files all the same. A query names one file in the address, so it asks both steps:
10
+ * Vite has recorded the file by then if a module imports it.
11
+ *
12
+ * A group's id holds its patterns, so an id alone must never choose what is read: any address could name a folder to
13
+ * list and read, and the imports the group's module writes would make Vite serve every file listed, past `allow`.
14
+ *
15
+ * A group is named by a module's import, which the plugin sees when Vite transforms that module. A client may ask before
16
+ * that: a tab left open across a restart holds its modules and asks for a search index first. So a request for a group
17
+ * no import has named waits, before Vite starts on it, for the server to read what the configuration's sources import,
18
+ * the same modules a page would load: inside the group's load the reading could wait for a request that waits for it. A
19
+ * cache of transforms would hide the import for good, and Vitest keeps one on disk between runs: a module that names a
20
+ * group stays out of it.
21
+ *
22
+ * A group no module names answers as a missing file does: Vite answers 404, keeps no module to renew and raises no
23
+ * overlay on the pages open. A module's `export default null` would read as a group, and fail far from here in whatever
24
+ * reads it.
25
+ *
26
+ * @module
27
+ */
28
+ import { realpathSync } from "node:fs";
29
+ import { join } from "node:path";
30
+ import { isCSSRequest, isFileLoadingAllowed } from "vite";
31
+ import { isGroupId, namesGroup, namingGroupOf } from "./pluginIds.js";
32
+ import { CODE_QUERY, DOCS_PAGE_QUERY, DOCS_QUERY, HOME_QUERY, MARKDOWN_QUERY, PAGE_QUERY } from "./textQueries.js";
33
+ import { VITE_PATHS } from "./vitePaths.js";
34
+ /** The queries the plugin answers with a file's text, so each read keeps to what the dev server may serve. */
35
+ const FILE_QUERIES = [
36
+ DOCS_QUERY,
37
+ CODE_QUERY,
38
+ PAGE_QUERY,
39
+ MARKDOWN_QUERY,
40
+ DOCS_PAGE_QUERY,
41
+ HOME_QUERY
42
+ ];
43
+ /**
44
+ * A listed file's real path, in Vite's form. Vite refuses a Windows short name (`RUNNER~1`), which could dodge `deny`,
45
+ * and a system's temporary folder is often written with one: the long name lets the file be judged by what it is. So
46
+ * does a link's target, which is the file that would be read.
47
+ *
48
+ * @param file - the file, absolute.
49
+ */
50
+ const realFile = (file) => {
51
+ try {
52
+ return VITE_PATHS.normalize(realpathSync.native(file));
53
+ }
54
+ catch {
55
+ return file;
56
+ }
57
+ };
58
+ /**
59
+ * Whether Vite refuses a listed file whatever `server.fs.allow` says: `deny`, or a path it will not trust (a `:` in it).
60
+ * Vite offers no check of this step alone, so this asks its whole check with an `allow` that covers the file.
61
+ *
62
+ * @param config - the dev server's configuration.
63
+ * @param file - the file, absolute in Vite's form.
64
+ */
65
+ export const isFileRefused = (config, file) => {
66
+ const real = realFile(file);
67
+ const fs = { ...config.server.fs, allow: [VITE_PATHS.dirname(real)] };
68
+ return !isFileLoadingAllowed({ ...config, server: { ...config.server, fs } }, real);
69
+ };
70
+ /**
71
+ * Whether a dev server may read the file a query's id names. An id Vite resolved is normalised; one with a `..` came
72
+ * from an address unresolved, and Vite's check reads a path as written, so `/project/../secret` would pass for a file
73
+ * of `/project`.
74
+ *
75
+ * @param config - the dev server's configuration.
76
+ * @param id - the module's id, the file and then its query.
77
+ */
78
+ export const isQueriedFileReadable = (config, id) => {
79
+ const file = id.slice(0, id.lastIndexOf("?"));
80
+ return VITE_PATHS.normalize(file) === file && isFileLoadingAllowed(config, file);
81
+ };
82
+ /**
83
+ * Whether reading a module can show an import of a group: not a group (reading one waits for this reading) nor a file's
84
+ * query, which reads no import; not a style sheet or a pre-bundled dependency, which import none.
85
+ *
86
+ * @param environment - the environment the module is read in.
87
+ * @param module - the module.
88
+ */
89
+ const mayNameGroup = (environment, module) => {
90
+ const { id, url } = module;
91
+ return (id !== null &&
92
+ !isCSSRequest(url) &&
93
+ !isGroupId(id) &&
94
+ !FILE_QUERIES.some((query) => id.endsWith(query)) &&
95
+ environment.depsOptimizer?.isOptimizedDepUrl(url) !== true);
96
+ };
97
+ /**
98
+ * Reads a module and what it imports, for the imports the plugin sees on the way.
99
+ *
100
+ * @param environment - the environment to read them in.
101
+ * @param entry - the module's address.
102
+ * @returns whether each could be read: one that could not may name a group once it is fixed.
103
+ */
104
+ const readImports = async (environment, entry) => {
105
+ const seen = new Set([entry]);
106
+ const read = async (url) => {
107
+ try {
108
+ await environment.transformRequest(url);
109
+ }
110
+ catch {
111
+ return false;
112
+ }
113
+ const module = await environment.moduleGraph.getModuleByUrl(url);
114
+ const next = [...(module?.importedModules ?? [])].filter((imported) => !seen.has(imported.url) && mayNameGroup(environment, imported));
115
+ for (const imported of next)
116
+ seen.add(imported.url);
117
+ const reads = await Promise.all(next.map((imported) => read(imported.url)));
118
+ return reads.every(Boolean);
119
+ };
120
+ return read(entry);
121
+ };
122
+ /**
123
+ * The id of the group a request's address names, when it names one: any spelling this misses reaches the plugin's load,
124
+ * which refuses it all the same.
125
+ *
126
+ * @param url - the request's address.
127
+ */
128
+ const groupIdOf = (url) => {
129
+ try {
130
+ const path = decodeURI(url.split("?")[0] ?? "");
131
+ const at = path.indexOf("virtual:forgebook-");
132
+ return at === -1 ? undefined : `\0${path.slice(at)}`;
133
+ }
134
+ catch {
135
+ return undefined;
136
+ }
137
+ };
138
+ /** Vite's code for a file it could not load: its server answers 404, with no overlay, and a runner fails with the text. */
139
+ const MISSING_MODULE = "ERR_LOAD_URL";
140
+ /** What a plugin's dev server lets a client read. */
141
+ export const devServerAccess = () => {
142
+ let devServer;
143
+ let addressImporter;
144
+ let sourcesEntry;
145
+ /** The reading of the sources: once, as an edit that names a new group is seen when Vite transforms it. */
146
+ let sourcesRead;
147
+ const imported = new Set();
148
+ return {
149
+ serve: (server, sources) => {
150
+ devServer = server;
151
+ addressImporter = join(server.config.root, "index.html");
152
+ sourcesEntry = sources;
153
+ sourcesRead = undefined;
154
+ },
155
+ isModule: (importer) => importer !== undefined && importer !== addressImporter,
156
+ noteImported: (id) => {
157
+ imported.add(namingGroupOf(id));
158
+ },
159
+ isKept: (file) => {
160
+ if (devServer === undefined || !isFileRefused(devServer.config, file))
161
+ return true;
162
+ devServer.config.logger.warnOnce(`Forgebook leaves out ${file}: Vite's server.fs refuses it`);
163
+ return false;
164
+ },
165
+ settle: async (url) => {
166
+ const id = groupIdOf(url);
167
+ if (id === undefined || devServer === undefined || sourcesEntry === undefined)
168
+ return;
169
+ if (imported.has(namingGroupOf(id)))
170
+ return;
171
+ sourcesRead ??= readImports(devServer.environments.client, sourcesEntry).then((isRead) => {
172
+ if (!isRead)
173
+ sourcesRead = undefined;
174
+ });
175
+ await sourcesRead;
176
+ },
177
+ refusal: (id) => {
178
+ if (devServer === undefined)
179
+ return undefined;
180
+ if (!isGroupId(id))
181
+ return FILE_QUERIES.some((query) => id.endsWith(query)) &&
182
+ !isQueriedFileReadable(devServer.config, id)
183
+ ? { code: "export default null;", moduleType: "js" }
184
+ : undefined;
185
+ if (imported.has(namingGroupOf(id)))
186
+ return undefined;
187
+ throw Object.assign(new Error(`Forgebook serves a group of files only when a module imports it: no module of the project imports ${id.slice(1)}`), { code: MISSING_MODULE });
188
+ }
189
+ };
190
+ };
191
+ /**
192
+ * The plugin's hook for Vitest (`configureVitest`), whose module cache keeps each module's transform on disk between
193
+ * runs: a module read from it resolves no import, so a group it names would stay refused from the second run on.
194
+ */
195
+ export const VITEST_HOOKS = {
196
+ configureVitest: (context) => {
197
+ context.defineCacheKeyGenerator?.(({ sourceCode }) => (namesGroup(sourceCode) ? false : undefined));
198
+ }
199
+ };
@@ -1,4 +1,4 @@
1
- import { type Plugin } from "vite";
1
+ import type { Plugin } from "vite";
2
2
  import type { FrameworkAdapter } from "./adapter.ts";
3
3
  import { type ResolvedConfig } from "./configFile.ts";
4
4
  /** The plugin's options. */