forgebook 0.2.0 → 0.3.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 (94) hide show
  1. package/README.md +112 -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 +51 -0
  66. package/dist/src/vite/fileAccess.js +109 -0
  67. package/dist/src/vite/forgebookPlugin.d.ts +1 -1
  68. package/dist/src/vite/forgebookPlugin.js +61 -96
  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 +9 -2
  82. package/dist/src/vite/pluginIds.js +21 -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 +2 -2
@@ -0,0 +1,23 @@
1
+ import type { ConfigEnv } from "vite";
2
+ import type { ResolvedConfig } from "./configFile.ts";
3
+ /**
4
+ * A module the plugin generates for a catalogue's pages, when the id names one: an entry, the project's own stories'
5
+ * source, or a page's HTML (unless the folder has one of its own). The configuration's data waits for the frames'
6
+ * placement, so the plugin's `load` makes it.
7
+ *
8
+ * @param id - the module's id.
9
+ * @param config - the catalogue's configuration.
10
+ * @param command - whether a dev server runs, or a build: where the frame finds axe's script.
11
+ */
12
+ export declare const generatedModule: (id: string, config: ResolvedConfig, command: ConfigEnv["command"]) => {
13
+ code: string;
14
+ moduleType: "js";
15
+ } | string | undefined;
16
+ /**
17
+ * The id of a module the plugin generates for a catalogue's pages, when an import names one: a page or an entry by
18
+ * its address (`/index.html`, `/.forgebook-shell.ts`) or by its path, a source by its path.
19
+ *
20
+ * @param source - the import's specifier.
21
+ * @param dir - the configuration's folder.
22
+ */
23
+ export declare const generatedId: (source: string, dir: string) => string | undefined;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The modules the plugin generates for a catalogue's pages: the shell's and the frame's entries, the project's own
3
+ * stories' source, the agents' sources and the pages' HTML, and the ids imports name them by.
4
+ *
5
+ * @remarks
6
+ * The configuration's data is not among them: it waits for the frames' placement, which only the plugin knows.
7
+ *
8
+ * @module
9
+ */
10
+ import { existsSync } from "node:fs";
11
+ import { checksA11y } from "../catalogue.js";
12
+ import { axeAddress } from "./axeScript.js";
13
+ import { builtPlacement } from "./framesConfig.js";
14
+ import { agentSourcesModule, frameEntry, PAGE_ENTRIES, PAGE_FILES, pageHtml, pageModuleOf, pageModulePath, shellEntry, storiesSourceModule } from "./pages.js";
15
+ import { VITE_PATHS } from "./vitePaths.js";
16
+ /** The core's folder, where axe's script is found from. */
17
+ const PACKAGE_DIR = import.meta.dirname;
18
+ /**
19
+ * A module the plugin generates for a catalogue's pages, when the id names one: an entry, the project's own stories'
20
+ * source, or a page's HTML (unless the folder has one of its own). The configuration's data waits for the frames'
21
+ * placement, so the plugin's `load` makes it.
22
+ *
23
+ * @param id - the module's id.
24
+ * @param config - the catalogue's configuration.
25
+ * @param command - whether a dev server runs, or a build: where the frame finds axe's script.
26
+ */
27
+ export const generatedModule = (id, config, command) => {
28
+ const named = pageModuleOf(id, config.dir);
29
+ if (named === undefined)
30
+ return undefined;
31
+ if (named.kind === "entry") {
32
+ const code = named.page === "index.html"
33
+ ? shellEntry(config)
34
+ : frameEntry(config, checksA11y(config.lists) ? axeAddress(command, PACKAGE_DIR) : undefined);
35
+ return { code, moduleType: "js" };
36
+ }
37
+ if (named.kind === "stories-source")
38
+ return { code: storiesSourceModule(config), moduleType: "js" };
39
+ if (named.kind === "agent-sources")
40
+ return { code: agentSourcesModule(config), moduleType: "js" };
41
+ return existsSync(id) ? undefined : pageHtml(named.page, config, builtPlacement(config.frames));
42
+ };
43
+ /**
44
+ * The id of a module the plugin generates for a catalogue's pages, when an import names one: a page or an entry by
45
+ * its address (`/index.html`, `/.forgebook-shell.ts`) or by its path, a source by its path.
46
+ *
47
+ * @param source - the import's specifier.
48
+ * @param dir - the configuration's folder.
49
+ */
50
+ export const generatedId = (source, dir) => {
51
+ const byAddress = PAGE_FILES.find((page) => source === `/${PAGE_ENTRIES[page]}`);
52
+ if (byAddress !== undefined)
53
+ return pageModulePath(dir, PAGE_ENTRIES[byAddress]);
54
+ const pageByAddress = PAGE_FILES.find((page) => source === `/${page}`);
55
+ if (pageByAddress !== undefined)
56
+ return pageModulePath(dir, pageByAddress);
57
+ return pageModuleOf(source, dir) === undefined ? undefined : VITE_PATHS.normalize(source);
58
+ };
@@ -105,10 +105,14 @@ const readIndex = (root, pattern, key) => {
105
105
  };
106
106
  /** The patterns' modules a dev server serves, kept to tell an edit that changes a list. */
107
107
  export const servedPatterns = () => {
108
+ // by group, each story file as served: its key, the pattern that lists it, its index
108
109
  const served = new Map();
109
110
  return {
110
111
  keep: (id, files) => {
111
- served.set(id, new Map(files.map((file) => [file.file, { key: file.key, index: JSON.stringify(file.index) }])));
112
+ served.set(id, new Map(files.map((file) => [
113
+ file.file,
114
+ { key: file.key, pattern: file.pattern, index: JSON.stringify(file.index) }
115
+ ])));
112
116
  },
113
117
  changedBy: (file) => [...served]
114
118
  .filter(([id, files]) => {
@@ -116,8 +120,8 @@ export const servedPatterns = () => {
116
120
  const kept = files.get(VITE_PATHS.normalize(file));
117
121
  if (kept === undefined)
118
122
  return false;
119
- const { root, pattern } = storyFilesGroupOf(id.slice(FILES_PREFIX.length));
120
- const index = readIndex(root, pattern, kept.key);
123
+ const { root } = storyFilesGroupOf(id.slice(FILES_PREFIX.length));
124
+ const index = readIndex(root, kept.pattern, kept.key);
121
125
  return typeof index !== "string" && JSON.stringify(index) !== kept.index;
122
126
  })
123
127
  .map(([id]) => id)
@@ -17,8 +17,8 @@
17
17
  *
18
18
  * A value the index needs (the title, a name, `parameters.controls`, `group`, `height`, `main`, the meta's `alsoIn`,
19
19
  * `related`, `keywords`) is read when written plainly; written by code, it is left to the frame, which reads it when
20
- * the story loads. `touch`, `logic`, `device`, `design.figma` and the meta's `propGroups` are read by the index only
21
- * (`indexedParameters.ts`, `indexFields.ts`), so written by code they are a problem.
20
+ * the story loads. `touch`, `logic`, `device`, `requiredFirst`, `design.figma` and the meta's `propGroups` are read by
21
+ * the index only (`indexedParameters.ts`, `indexFields.ts`), so written by code they are a problem.
22
22
  * A `design.figma` is checked here too, so the pages never carry the checker: one that is not a link to a Figma file is
23
23
  * noted and left out, and the file keeps its stories. A title written by code, or none, is the file's path
24
24
  * (`autoTitle`, as Storybook's auto-title); one written by code is also noted, since the author meant another. A value written plainly but wrongly
@@ -17,8 +17,8 @@
17
17
  *
18
18
  * A value the index needs (the title, a name, `parameters.controls`, `group`, `height`, `main`, the meta's `alsoIn`,
19
19
  * `related`, `keywords`) is read when written plainly; written by code, it is left to the frame, which reads it when
20
- * the story loads. `touch`, `logic`, `device`, `design.figma` and the meta's `propGroups` are read by the index only
21
- * (`indexedParameters.ts`, `indexFields.ts`), so written by code they are a problem.
20
+ * the story loads. `touch`, `logic`, `device`, `requiredFirst`, `design.figma` and the meta's `propGroups` are read by
21
+ * the index only (`indexedParameters.ts`, `indexFields.ts`), so written by code they are a problem.
22
22
  * A `design.figma` is checked here too, so the pages never carry the checker: one that is not a link to a Figma file is
23
23
  * noted and left out, and the file keeps its stories. A title written by code, or none, is the file's path
24
24
  * (`autoTitle`, as Storybook's auto-title); one written by code is also noted, since the author meant another. A value written plainly but wrongly
@@ -1,7 +1,7 @@
1
1
  import type { StoryIndexEntry } from "@forgebook/story-format/source";
2
2
  import { type Fields } from "./indexFields.ts";
3
3
  /** What the index keeps of these parameters: each only when written. */
4
- export type IndexedParameters = Pick<StoryIndexEntry, "isTouch" | "isLogic" | "device">;
4
+ export type IndexedParameters = Pick<StoryIndexEntry, "isTouch" | "isLogic" | "device" | "requiredFirst">;
5
5
  /**
6
6
  * The parameters only the index reads, each only when written, or the first problem.
7
7
  *
@@ -1,4 +1,5 @@
1
1
  import { deviceSettingProblem, isDeviceOrientation } from "../deviceView.js";
2
+ import { REQUIRED_FIRST_LAYOUTS } from "../propGroups.js";
2
3
  import { keyName, parameter, unwrapped } from "./indexFields.js";
3
4
  /** Why `parameters.device` cannot be read as it is written. */
4
5
  const DEVICE_PROBLEM = '`parameters.device` must be written as a plain object (`{ frame: true, model: "iphone-16" }`)';
@@ -87,6 +88,21 @@ const deviceOf = (value) => {
87
88
  const problem = deviceSettingProblem(setting);
88
89
  return problem === undefined ? setting : new Error(`\`parameters.device\`: ${problem}`);
89
90
  };
91
+ /**
92
+ * `parameters.requiredFirst`: `undefined` when none, `"in-groups"`, `"section"` or `false` written plainly, or a
93
+ * problem otherwise.
94
+ *
95
+ * @param value - the parameter's value.
96
+ */
97
+ const requiredFirstOf = (value) => {
98
+ if (value === undefined)
99
+ return undefined;
100
+ const written = value.type === "Literal" ? value.value : undefined;
101
+ const layout = REQUIRED_FIRST_LAYOUTS.find((each) => each === written);
102
+ if (layout !== undefined)
103
+ return layout;
104
+ return new Error('`parameters.requiredFirst` must be written as a plain `"in-groups"`, `"section"` or `false`');
105
+ };
90
106
  /**
91
107
  * The parameters only the index reads, each only when written, or the first problem.
92
108
  *
@@ -102,9 +118,13 @@ export const indexedParameters = (fields) => {
102
118
  const device = deviceOf(parameter(fields, "device"));
103
119
  if (device instanceof Error)
104
120
  return device;
121
+ const requiredFirst = requiredFirstOf(parameter(fields, "requiredFirst"));
122
+ if (requiredFirst instanceof Error)
123
+ return requiredFirst;
105
124
  return {
106
125
  ...(touch === undefined ? {} : { isTouch: touch }),
107
126
  ...(logic === undefined ? {} : { isLogic: logic }),
108
- ...(device === undefined ? {} : { device })
127
+ ...(device === undefined ? {} : { device }),
128
+ ...(requiredFirst === undefined ? {} : { requiredFirst })
109
129
  };
110
130
  };
@@ -6,7 +6,13 @@
6
6
  * @remarks
7
7
  * Vite's CORS lets any page on `localhost` or `127.0.0.1` read the dev server, whatever its port: a frame on a second
8
8
  * port would read the catalogue's agent API and files as before. So the catalogue refuses a request another origin
9
- * sends, unless it is a navigation (a link, an embed), which shows a page but lets nobody read it.
9
+ * sends, unless it opens one of its pages (a link, an embed) or a public file, which shows it but lets nobody read it.
10
+ * No other address is opened for another origin's page: a server's route acts when opened (`/__open-in-editor` opens a
11
+ * file), and a module or a source file has no use as a page.
12
+ *
13
+ * Vite checks `Host` first, on both ports, against `server.allowedHosts`: a page that reaches `127.0.0.1` through its
14
+ * own domain (DNS rebinding) is not told apart here, since it is same-origin for its browser, and over plain HTTP on
15
+ * that domain its browser sends no `Sec-Fetch-*`.
10
16
  *
11
17
  * The frames' origin would otherwise serve everything the catalogue's does, and a story's script would read the
12
18
  * project from there. It serves a script or a `fetch` (whose body a script reads) only for a module that another
@@ -23,7 +29,10 @@
23
29
  */
24
30
  /** What a browser says of a request, from its headers. */
25
31
  export type RequestFacts = {
26
- /** Its path, without the query (`/frame.html`). */
32
+ /**
33
+ * Its path as an address parser reads it, without the query (`/frame.html`): the routes behind the guards read the
34
+ * same, so `//x/__forgebook/api/stories` or `/./__forgebook/api/stories` is the route it would reach.
35
+ */
27
36
  readonly path: string;
28
37
  /** Its query, with its `?` (`?raw`); empty without one. */
29
38
  readonly query: string;
@@ -44,13 +53,14 @@ export declare const FRAME_PAGE_PATH = "/frame.html";
44
53
  export type CatalogueAnswer = "serve" | "refuse" | "to-frames";
45
54
  /**
46
55
  * What the catalogue's origin does with a request: the frame page goes to the frames' origin, another origin's
47
- * request is refused, the rest is served.
56
+ * request is refused unless it opens a page or a public file, the rest is served.
48
57
  *
49
58
  * @param request - what the browser says of it.
50
59
  * @param framedPages - the server's routes that are pages made to be framed by another origin's page (a client's
51
60
  * inline preview), which only show a story and act on nothing.
61
+ * @param isPublic - whether a path names a file of the public folder; asked only of another origin's navigation.
52
62
  */
53
- export declare const catalogueAnswer: (request: RequestFacts, framedPages: ReadonlySet<string>) => CatalogueAnswer;
63
+ export declare const catalogueAnswer: (request: RequestFacts, framedPages: ReadonlySet<string>, isPublic?: (path: string) => boolean) => CatalogueAnswer;
54
64
  /** What the frames' origin does with a request: `ask-graph` serves it when another module imports it. */
55
65
  export type FramesAnswer = "serve" | "refuse" | "not-found" | "ask-graph";
56
66
  /** What the frames' origin knows of a request's path. */
@@ -6,7 +6,13 @@
6
6
  * @remarks
7
7
  * Vite's CORS lets any page on `localhost` or `127.0.0.1` read the dev server, whatever its port: a frame on a second
8
8
  * port would read the catalogue's agent API and files as before. So the catalogue refuses a request another origin
9
- * sends, unless it is a navigation (a link, an embed), which shows a page but lets nobody read it.
9
+ * sends, unless it opens one of its pages (a link, an embed) or a public file, which shows it but lets nobody read it.
10
+ * No other address is opened for another origin's page: a server's route acts when opened (`/__open-in-editor` opens a
11
+ * file), and a module or a source file has no use as a page.
12
+ *
13
+ * Vite checks `Host` first, on both ports, against `server.allowedHosts`: a page that reaches `127.0.0.1` through its
14
+ * own domain (DNS rebinding) is not told apart here, since it is same-origin for its browser, and over plain HTTP on
15
+ * that domain its browser sends no `Sec-Fetch-*`.
10
16
  *
11
17
  * The frames' origin would otherwise serve everything the catalogue's does, and a story's script would read the
12
18
  * project from there. It serves a script or a `fetch` (whose body a script reads) only for a module that another
@@ -41,6 +47,8 @@ const OPAQUE_DESTS = new Set([
41
47
  ]);
42
48
  /** The queries that ask Vite for a module made from a file (its text, its address…), never a file as it is. */
43
49
  const TRANSFORM_QUERY = /[?&](?:raw|import|inline|url|worker|worker_file|sharedworker)(?:[&=]|$)/;
50
+ /** The catalogue's pages, by their addresses: what another origin's page may open, with public files and framed routes. */
51
+ const CATALOGUE_PAGES = new Set(["/", "/index.html", FRAME_PAGE_PATH]);
44
52
  /** The dev server's own routes (`/__forgebook/api/`, `/__open-in-editor`): never a story's. */
45
53
  const SERVER_ROUTE_PREFIX = "/__";
46
54
  /**
@@ -88,18 +96,19 @@ const isAnyFromAnotherOrigin = (request) => request.site === undefined
88
96
  const isDocumentRequest = (request) => request.mode === "navigate" || DOCUMENT_DESTS.has(request.dest ?? "");
89
97
  /**
90
98
  * What the catalogue's origin does with a request: the frame page goes to the frames' origin, another origin's
91
- * request is refused, the rest is served.
99
+ * request is refused unless it opens a page or a public file, the rest is served.
92
100
  *
93
101
  * @param request - what the browser says of it.
94
102
  * @param framedPages - the server's routes that are pages made to be framed by another origin's page (a client's
95
103
  * inline preview), which only show a story and act on nothing.
104
+ * @param isPublic - whether a path names a file of the public folder; asked only of another origin's navigation.
96
105
  */
97
- export const catalogueAnswer = (request, framedPages) => {
106
+ export const catalogueAnswer = (request, framedPages, isPublic = () => false) => {
98
107
  if (isFromAnotherOrigin(request))
99
108
  return "refuse";
100
- const isFramedPage = framedPages.has(request.path) && isDocumentRequest(request);
101
- // a route of the server acts when it is opened (`/__open-in-editor` opens a file): not on another page's say
102
- if (request.path.startsWith(SERVER_ROUTE_PREFIX) && !isFramedPage && isAnyFromAnotherOrigin(request))
109
+ const isOpenable = () => CATALOGUE_PAGES.has(request.path) ||
110
+ (isDocumentRequest(request) && (framedPages.has(request.path) || isPublic(request.path)));
111
+ if (isAnyFromAnotherOrigin(request) && !isOpenable())
103
112
  return "refuse";
104
113
  return request.path === FRAME_PAGE_PATH ? "to-frames" : "serve";
105
114
  };
@@ -106,8 +106,9 @@ export declare const frameEntry: (config: ResolvedConfig, axe?: AxeAddress) => s
106
106
  */
107
107
  export declare const agentSourcesModule: (config: ResolvedConfig) => string;
108
108
  /**
109
- * The source of the project's own stories and docs pages: one `?forgebook-files` or `?forgebook-docs-pages` import per
110
- * glob, named after the catalogue.
109
+ * The source of the project's own stories and docs pages: its `stories` as one group of story files and its `docs` as
110
+ * one group of docs pages, each imported by its patterns (`FILES_SPECIFIER_PREFIX`), so a `!` pattern leaves files out
111
+ * of what all the others list and a file two patterns list comes once; named after the catalogue.
111
112
  *
112
113
  * @param config - the configuration.
113
114
  */
@@ -18,6 +18,7 @@ import { LOAD_RELOADS_KEY } from "../frame/staleModule.js";
18
18
  import { PREVIEWS_EVENT } from "../hotModules.js";
19
19
  import { PLAY_TOOLS_PACKAGES } from "./configFile.js";
20
20
  import { pluginFrameModule } from "./pluginConfig.js";
21
+ import { DOCS_PAGES_SPECIFIER_PREFIX, FILES_SPECIFIER_PREFIX, storyFilesGroupText } from "./storyFilesModule.js";
21
22
  import { HOME_QUERY } from "./textQueries.js";
22
23
  import { VITE_PATHS } from "./vitePaths.js";
23
24
  /** The pages, by their file name. */
@@ -402,22 +403,28 @@ export const agentSourcesModule = (config) => {
402
403
  return [...sources.imports, `export const sources = ${sources.list};`, ""].join("\n");
403
404
  };
404
405
  /**
405
- * The source of the project's own stories and docs pages: one `?forgebook-files` or `?forgebook-docs-pages` import per
406
- * glob, named after the catalogue.
406
+ * The source of the project's own stories and docs pages: its `stories` as one group of story files and its `docs` as
407
+ * one group of docs pages, each imported by its patterns (`FILES_SPECIFIER_PREFIX`), so a `!` pattern leaves files out
408
+ * of what all the others list and a file two patterns list comes once; named after the catalogue.
407
409
  *
408
410
  * @param config - the configuration.
409
411
  */
410
- export const storiesSourceModule = (config) => [
411
- ...config.stories.map((pattern, index) => `import files${index} from ${codeLiteral(`./${pattern.replace(/^\.\//, "")}?forgebook-files`)};`),
412
- ...config.docs.map((pattern, index) => `import docs${index} from ${codeLiteral(`./${pattern.replace(/^\.\//, "")}?forgebook-docs-pages`)};`),
413
- "export const source = {",
414
- ` id: ${codeLiteral(OWN_SOURCE_ID)},`,
415
- ` name: ${codeLiteral(config.title)},`,
416
- ` files: [${config.stories.map((_pattern, index) => `files${index}`).join(", ")}],`,
417
- ...(config.docs.length === 0
418
- ? []
419
- : [` docs: [${config.docs.map((_pattern, index) => `docs${index}`).join(", ")}],`]),
420
- " sheets: { required: [], theme: [], restyled: [] }",
421
- "};",
422
- ""
423
- ].join("\n");
412
+ export const storiesSourceModule = (config) => {
413
+ // the configuration's folder in Vite's form, as the plugin names an importer's: the patterns are relative to it
414
+ const folder = VITE_PATHS.normalize(config.dir);
415
+ const group = (prefix, patterns) => codeLiteral(`${prefix}${storyFilesGroupText(folder, patterns)}`);
416
+ const hasStories = config.stories.length > 0;
417
+ const hasDocs = config.docs.length > 0;
418
+ return [
419
+ ...(hasStories ? [`import files from ${group(FILES_SPECIFIER_PREFIX, config.stories)};`] : []),
420
+ ...(hasDocs ? [`import docs from ${group(DOCS_PAGES_SPECIFIER_PREFIX, config.docs)};`] : []),
421
+ "export const source = {",
422
+ ` id: ${codeLiteral(OWN_SOURCE_ID)},`,
423
+ ` name: ${codeLiteral(config.title)},`,
424
+ ` files: [${hasStories ? "files" : ""}],`,
425
+ ...(hasDocs ? [" docs: [docs],"] : []),
426
+ " sheets: { required: [], theme: [], restyled: [] }",
427
+ "};",
428
+ ""
429
+ ].join("\n");
430
+ };
@@ -1,5 +1,5 @@
1
- /** The id of a pattern's docs pages: virtual, with the declaring module's folder and the pattern after it. */
2
- export declare const DOCS_PAGES_PREFIX = "\0forgebook-docs-pages:";
1
+ /** The id of a group of docs pages: virtual, with the declaring module's folder and the patterns after it. */
2
+ export declare const DOCS_PAGES_PREFIX = "\0virtual:forgebook-docs-pages:";
3
3
  /**
4
4
  * The id of a pattern's module, when an import asks for one: its story files, or its docs pages.
5
5
  *
@@ -14,6 +14,13 @@ export declare const patternId: (source: string, folder: string) => string | nul
14
14
  * @param source - the import's specifier.
15
15
  */
16
16
  export declare const virtualId: (source: string) => string | undefined;
17
+ /**
18
+ * Whether an id names a group's module: one whose patterns are written in the id itself, so that whoever writes the id
19
+ * chooses what the plugin reads.
20
+ *
21
+ * @param id - the module's id.
22
+ */
23
+ export declare const isGroupId: (id: string) => boolean;
17
24
  /** The resolved id of the configuration's data module. */
18
25
  export declare const CONFIG_ID = "\0virtual:forgebook/config";
19
26
  /** The resolved id of the design tokens' module. */
@@ -6,20 +6,20 @@
6
6
  */
7
7
  import { CONFIG_MODULE, TOKENS_MODULE } from "./pages.js";
8
8
  import { FILES_PREFIX } from "./pluginModules.js";
9
- import { PAGES_SEARCH_PREFIX, REACH_PREFIX, SEARCH_PREFIX, storyFilesGroupText, TEXTS_PREFIX, USAGE_PREFIX } from "./storyFilesModule.js";
9
+ import { DOCS_PAGES_SPECIFIER_PREFIX, FILES_SPECIFIER_PREFIX, GROUP_SEPARATOR, PAGES_SEARCH_PREFIX, REACH_PREFIX, SEARCH_PREFIX, storyFilesGroupText, TEXTS_PREFIX, USAGE_PREFIX } from "./storyFilesModule.js";
10
10
  /** The query that asks for the story files a pattern matches (`../src/**\/*.stories.tsx?forgebook-files`). */
11
11
  const FILES_QUERY = "?forgebook-files";
12
12
  /** The query that asks for the docs pages a pattern matches (`./docs/*.md?forgebook-docs-pages`). */
13
13
  const DOCS_PAGES_QUERY = "?forgebook-docs-pages";
14
- /** The id of a pattern's docs pages: virtual, with the declaring module's folder and the pattern after it. */
15
- export const DOCS_PAGES_PREFIX = "\0forgebook-docs-pages:";
14
+ /** The id of a group of docs pages: virtual, with the declaring module's folder and the patterns after it. */
15
+ export const DOCS_PAGES_PREFIX = `\0${DOCS_PAGES_SPECIFIER_PREFIX}`;
16
16
  /**
17
17
  * The id of a `?forgebook-files` module: its prefix, then its group (`storyFilesGroupText`), which holds no `..`.
18
18
  *
19
19
  * @param folder - the importing module's folder.
20
20
  * @param pattern - the pattern as the import writes it.
21
21
  */
22
- const filesId = (folder, pattern) => `${FILES_PREFIX}${storyFilesGroupText(folder, pattern)}`;
22
+ const filesId = (folder, pattern) => `${FILES_PREFIX}${storyFilesGroupText(folder, [pattern])}`;
23
23
  /**
24
24
  * The id of a pattern's module, when an import asks for one: its story files, or its docs pages.
25
25
  *
@@ -27,15 +27,23 @@ const filesId = (folder, pattern) => `${FILES_PREFIX}${storyFilesGroupText(folde
27
27
  * @param folder - the importing module's folder.
28
28
  */
29
29
  export const patternId = (source, folder) => {
30
+ // the separator of a group's id: a folder that holds it would read back as patterns of its own
31
+ if (folder.includes(GROUP_SEPARATOR))
32
+ return null;
30
33
  if (source.endsWith(FILES_QUERY))
31
34
  return filesId(folder, source.slice(0, -FILES_QUERY.length));
32
35
  if (!source.endsWith(DOCS_PAGES_QUERY))
33
36
  return null;
34
37
  const pattern = source.slice(0, -DOCS_PAGES_QUERY.length);
35
- return `${DOCS_PAGES_PREFIX}${storyFilesGroupText(folder, pattern)}`;
38
+ return `${DOCS_PAGES_PREFIX}${storyFilesGroupText(folder, [pattern])}`;
36
39
  };
37
- /** The specifiers' prefixes of a group's virtual modules: its "Used in", its reach, its search indexes, its texts. */
40
+ /**
41
+ * The specifiers' prefixes of a group's virtual modules: the group named by its patterns (story files or docs pages),
42
+ * its "Used in", its reach, its search indexes, its texts.
43
+ */
38
44
  const VIRTUAL_PREFIXES = [
45
+ FILES_SPECIFIER_PREFIX,
46
+ DOCS_PAGES_SPECIFIER_PREFIX,
39
47
  USAGE_PREFIX,
40
48
  REACH_PREFIX,
41
49
  SEARCH_PREFIX,
@@ -55,6 +63,13 @@ export const virtualId = (source) => {
55
63
  return TOKENS_ID;
56
64
  return VIRTUAL_PREFIXES.some((prefix) => source.startsWith(prefix)) ? `\0${source}` : undefined;
57
65
  };
66
+ /**
67
+ * Whether an id names a group's module: one whose patterns are written in the id itself, so that whoever writes the id
68
+ * chooses what the plugin reads.
69
+ *
70
+ * @param id - the module's id.
71
+ */
72
+ export const isGroupId = (id) => VIRTUAL_PREFIXES.some((prefix) => id.startsWith(`\0${prefix}`));
58
73
  /** The resolved id of the configuration's data module. */
59
74
  export const CONFIG_ID = `\0${CONFIG_MODULE}`;
60
75
  /** The resolved id of the design tokens' module. */
@@ -11,9 +11,12 @@
11
11
  import type { DiskCache } from "./diskCache.ts";
12
12
  import type { DocsExtractor, DocsResult } from "./docsExtractor.ts";
13
13
  import type { PackJson } from "./packedModule.ts";
14
- import { type GroupLoaders, type IndexedStoryFile, type KeptIndex } from "./storyFilesModule.ts";
15
- /** The id of a pattern's module: virtual, with the declaring module's folder and the pattern after it. */
16
- export declare const FILES_PREFIX = "\0forgebook-files:";
14
+ import { type GroupLoaders, type IndexedStoryFile, type IsFileKept, type KeptIndex } from "./storyFilesModule.ts";
15
+ /**
16
+ * The id of a group of story files' module: virtual, with the declaring module's folder and the patterns after it,
17
+ * whether a `?forgebook-files` import or the group's specifier (`FILES_SPECIFIER_PREFIX`) names it.
18
+ */
19
+ export declare const FILES_PREFIX = "\0virtual:forgebook-files:";
17
20
  /** The resolved id of a group's "Used in" module: Rollup's mark of a virtual module, then its import's specifier. */
18
21
  export declare const USAGE_ID_PREFIX = "\0virtual:forgebook-usage:";
19
22
  /** The resolved id of a group's reach module, the same way. */
@@ -42,7 +45,7 @@ export declare const docsModule: (component: DocsResult, watch: (file: string) =
42
45
  moduleType: "js";
43
46
  };
44
47
  /**
45
- * The module of a `?forgebook-files` pattern: every matched story file's index and loaders; with the files, indexed,
48
+ * The module of a group of story files: every listed story file's index and loaders; with the files, indexed,
46
49
  * which the dev server keeps to tell an edit that changes the list.
47
50
  *
48
51
  * @param id - the module's id (`FILES_PREFIX`, then the group: `storyFilesGroupText`).
@@ -52,13 +55,35 @@ export declare const docsModule: (component: DocsResult, watch: (file: string) =
52
55
  * @param warm - starts reading the docs of a story file ahead of its request; none in a test run.
53
56
  * @param startTexts - a build's: starts reading the files' docs, code and pages, which the module imports from the
54
57
  * group's texts module (`builtTexts.ts`); none imports each from its query.
58
+ * @param isKept - whether a listed file may be read; every one by default.
55
59
  * @throws when a story file cannot be indexed.
56
60
  */
57
- export declare const patternModule: (id: string, watch: (file: string) => void, loaders: GroupLoaders, cache: DiskCache<KeptIndex> | undefined, warm: ((storyFile: string) => void) | undefined, startTexts?: (files: readonly IndexedStoryFile[]) => void) => {
61
+ export declare const patternModule: (id: string, watch: (file: string) => void, loaders: GroupLoaders, cache: DiskCache<KeptIndex> | undefined, warm: ((storyFile: string) => void) | undefined, startTexts?: (files: readonly IndexedStoryFile[]) => void, isKept?: IsFileKept) => {
58
62
  code: string;
59
63
  moduleType: "js";
60
64
  files: readonly IndexedStoryFile[];
61
65
  };
66
+ /**
67
+ * Whether a story file added or removed changes what a group of story files lists: listed now and not when its module
68
+ * was served, or the other way round. A file no pattern lists, or one a `!` pattern leaves out, changes nothing, so
69
+ * the pages need no reload for it.
70
+ *
71
+ * @param id - the group's module's id (`FILES_PREFIX`, then the group).
72
+ * @param listed - the files its module listed when served, absolute in Vite's form; none when it could not be made.
73
+ * @param file - the file added or removed.
74
+ * @param isKept - whether a listed file may be read; every one by default.
75
+ */
76
+ export declare const isListingChanged: (id: string, listed: ReadonlySet<string> | undefined, file: string, isKept?: IsFileKept) => boolean;
77
+ /**
78
+ * The modules a story file added or removed renews: the groups whose list it changes (`isListingChanged`) and, with
79
+ * them, the modules that read their files; none when it changes no list.
80
+ *
81
+ * @param served - the groups' modules, with the files each listed when served.
82
+ * @param file - the file added or removed.
83
+ * @param readers - the modules that read the groups' files: their "Used in" and search indexes.
84
+ * @param isKept - whether a listed file may be read; every one by default.
85
+ */
86
+ export declare const listingRenewals: (served: ReadonlyMap<string, ReadonlySet<string> | undefined>, file: string, readers: readonly string[], isKept?: IsFileKept) => readonly string[] | undefined;
62
87
  /**
63
88
  * A group's "Used in" module: for each component, the components that use it, packed once per title and list of
64
89
  * stories (`packedUsage`), which a page reads.
@@ -68,8 +93,9 @@ export declare const patternModule: (id: string, watch: (file: string) => void,
68
93
  * @param usage - reads the facts in the docs worker.
69
94
  * @param cache - the pattern's index kept between starts; none reads every file.
70
95
  * @param pack - writes the module's JSON as its value: as it is on a dev server, in a gzipped file of a build's.
96
+ * @param isKept - whether a listed file may be read; every one by default.
71
97
  */
72
- export declare const usageModule: (id: string, watch: (file: string) => void, usage: DocsExtractor["usage"], cache: DiskCache<KeptIndex> | undefined, pack: PackJson) => Promise<{
98
+ export declare const usageModule: (id: string, watch: (file: string) => void, usage: DocsExtractor["usage"], cache: DiskCache<KeptIndex> | undefined, pack: PackJson, isKept?: IsFileKept) => Promise<{
73
99
  code: string;
74
100
  moduleType: "js";
75
101
  }>;
@@ -82,8 +108,9 @@ export declare const usageModule: (id: string, watch: (file: string) => void, us
82
108
  * @param watch - adds a file whose edit renews the module (each story file).
83
109
  * @param usage - reads the facts in the docs worker.
84
110
  * @param cache - the pattern's index kept between starts; none reads every file.
111
+ * @param isKept - whether a listed file may be read; every one by default.
85
112
  */
86
- export declare const reachModule: (id: string, watch: (file: string) => void, usage: DocsExtractor["usage"], cache: DiskCache<KeptIndex> | undefined) => Promise<{
113
+ export declare const reachModule: (id: string, watch: (file: string) => void, usage: DocsExtractor["usage"], cache: DiskCache<KeptIndex> | undefined, isKept?: IsFileKept) => Promise<{
87
114
  code: string;
88
115
  moduleType: "js";
89
116
  }>;
@@ -103,13 +130,14 @@ export type SearchIndexModule = {
103
130
  *
104
131
  * @param id - the module's id.
105
132
  * @param watch - adds a file whose edit renews the module.
106
- * @param read - a story file's component docs, and how the module writes its JSON.
133
+ * @param read - a story file's component docs, how the module writes its JSON, and which listed files may be read.
107
134
  * @returns the index; none for another id.
108
135
  */
109
136
  export declare const searchIndexModule: (id: string, watch: (file: string) => void, read: {
110
137
  readonly docsOf: (storyFile: string) => Promise<DocsResult>;
111
138
  readonly isServing: boolean;
112
139
  readonly pack: PackJson;
140
+ readonly isKept: IsFileKept;
113
141
  }) => Promise<SearchIndexModule | undefined>;
114
142
  /** What a catalogue's search indexes read and tell: what `searchIndexes` makes. */
115
143
  export type SearchIndexes = {
@@ -130,11 +158,12 @@ export type SearchIndexes = {
130
158
  /**
131
159
  * The search's indexes of a plugin: a build reads each component's docs once, for its page and its index.
132
160
  *
133
- * @param read - reads a story file's component docs, says whether a dev server runs, and keeps each index's id with the
134
- * modules an edit renews (a docs pages' index with the docs pages').
161
+ * @param read - reads a story file's component docs, says whether a dev server runs, keeps each index's id with the
162
+ * modules an edit renews (a docs pages' index with the docs pages'), and says which listed files may be read.
135
163
  */
136
164
  export declare const searchIndexes: (read: {
137
165
  readonly extract: (storyFile: string) => Promise<DocsResult>;
138
166
  readonly isServing: () => boolean;
139
167
  readonly track: (id: string, isPages: boolean) => void;
168
+ readonly isKept: IsFileKept;
140
169
  }) => SearchIndexes;