forgebook 0.3.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.
- package/README.md +2 -1
- package/dist/src/vite/fileAccess.d.ts +33 -9
- package/dist/src/vite/fileAccess.js +107 -17
- package/dist/src/vite/forgebookPlugin.js +10 -6
- package/dist/src/vite/pluginIds.d.ts +15 -0
- package/dist/src/vite/pluginIds.js +23 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1028,7 +1028,8 @@ its `frame-src`, in the preview's `frameDomains`).
|
|
|
1028
1028
|
read. Vite checks `Host`
|
|
1029
1029
|
first (`server.allowedHosts`, plain HTTP only), which stops a site that reaches `127.0.0.1` through its own domain.
|
|
1030
1030
|
- **Only the files your project names are listed or read.** A catalogue's `stories` and `docs`, and the groups a module
|
|
1031
|
-
of yours imports (`?forgebook-files`), are the only groups the server reads, whatever an address names
|
|
1031
|
+
of yours imports (`?forgebook-files`), are the only groups the server reads, whatever an address names: any other
|
|
1032
|
+
answers 404. A catalogue's tab left open across a restart gets its groups all the same. A query that
|
|
1032
1033
|
names one file (`?forgebook-code`, `?forgebook-markdown`) follows Vite's `server.fs`, as `?raw` does.
|
|
1033
1034
|
- **`server.fs.deny` wins over your globs.** A file it refuses stays out of the catalogue, its search and "Used in", with
|
|
1034
1035
|
a warning. A file your globs name outside `server.fs.allow` is listed and served, as Vite serves any file a module of
|
|
@@ -19,8 +19,13 @@ export declare const isFileRefused: (config: ResolvedConfig, file: string) => bo
|
|
|
19
19
|
export declare const isQueriedFileReadable: (config: ResolvedConfig, id: string) => boolean;
|
|
20
20
|
/** What a plugin's dev server lets a client read: `devServerAccess` makes it. */
|
|
21
21
|
export type DevServerAccess = {
|
|
22
|
-
/**
|
|
23
|
-
|
|
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;
|
|
24
29
|
/**
|
|
25
30
|
* Whether a resolver's importer is a module, not the stand-in Vite gives an address asked for directly.
|
|
26
31
|
*
|
|
@@ -36,16 +41,35 @@ export type DevServerAccess = {
|
|
|
36
41
|
/** Whether a file a group lists may be read; says so once when not, as nothing else would tell why it is missing. */
|
|
37
42
|
readonly isKept: IsFileKept;
|
|
38
43
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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`.
|
|
41
53
|
*
|
|
42
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.
|
|
43
57
|
*/
|
|
44
|
-
readonly
|
|
58
|
+
readonly refusal: (id: string) => {
|
|
59
|
+
readonly code: string;
|
|
60
|
+
readonly moduleType: "js";
|
|
61
|
+
} | undefined;
|
|
45
62
|
};
|
|
63
|
+
/** What a plugin's dev server lets a client read. */
|
|
64
|
+
export declare const devServerAccess: () => DevServerAccess;
|
|
46
65
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* @param renew - renews modules Vite keeps: a group refused before its first import would otherwise stay empty.
|
|
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.
|
|
50
68
|
*/
|
|
51
|
-
export declare const
|
|
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
|
+
};
|
|
@@ -12,12 +12,23 @@
|
|
|
12
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
13
|
* list and read, and the imports the group's module writes would make Vite serve every file listed, past `allow`.
|
|
14
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
|
+
*
|
|
15
26
|
* @module
|
|
16
27
|
*/
|
|
17
28
|
import { realpathSync } from "node:fs";
|
|
18
29
|
import { join } from "node:path";
|
|
19
|
-
import { isFileLoadingAllowed } from "vite";
|
|
20
|
-
import { isGroupId } from "./pluginIds.js";
|
|
30
|
+
import { isCSSRequest, isFileLoadingAllowed } from "vite";
|
|
31
|
+
import { isGroupId, namesGroup, namingGroupOf } from "./pluginIds.js";
|
|
21
32
|
import { CODE_QUERY, DOCS_PAGE_QUERY, DOCS_QUERY, HOME_QUERY, MARKDOWN_QUERY, PAGE_QUERY } from "./textQueries.js";
|
|
22
33
|
import { VITE_PATHS } from "./vitePaths.js";
|
|
23
34
|
/** The queries the plugin answers with a file's text, so each read keeps to what the dev server may serve. */
|
|
@@ -69,25 +80,81 @@ export const isQueriedFileReadable = (config, id) => {
|
|
|
69
80
|
return VITE_PATHS.normalize(file) === file && isFileLoadingAllowed(config, file);
|
|
70
81
|
};
|
|
71
82
|
/**
|
|
72
|
-
*
|
|
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.
|
|
73
125
|
*
|
|
74
|
-
* @param
|
|
126
|
+
* @param url - the request's address.
|
|
75
127
|
*/
|
|
76
|
-
|
|
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 = () => {
|
|
77
142
|
let devServer;
|
|
78
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;
|
|
79
147
|
const imported = new Set();
|
|
80
|
-
const refused = new Set();
|
|
81
148
|
return {
|
|
82
|
-
serve: (server) => {
|
|
149
|
+
serve: (server, sources) => {
|
|
83
150
|
devServer = server;
|
|
84
151
|
addressImporter = join(server.config.root, "index.html");
|
|
152
|
+
sourcesEntry = sources;
|
|
153
|
+
sourcesRead = undefined;
|
|
85
154
|
},
|
|
86
155
|
isModule: (importer) => importer !== undefined && importer !== addressImporter,
|
|
87
156
|
noteImported: (id) => {
|
|
88
|
-
imported.add(id);
|
|
89
|
-
if (refused.delete(id) && devServer !== undefined)
|
|
90
|
-
renew(devServer, [id]);
|
|
157
|
+
imported.add(namingGroupOf(id));
|
|
91
158
|
},
|
|
92
159
|
isKept: (file) => {
|
|
93
160
|
if (devServer === undefined || !isFileRefused(devServer.config, file))
|
|
@@ -95,15 +162,38 @@ export const devServerAccess = (renew) => {
|
|
|
95
162
|
devServer.config.logger.warnOnce(`Forgebook leaves out ${file}: Vite's server.fs refuses it`);
|
|
96
163
|
return false;
|
|
97
164
|
},
|
|
98
|
-
|
|
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) => {
|
|
99
178
|
if (devServer === undefined)
|
|
100
|
-
return
|
|
179
|
+
return undefined;
|
|
101
180
|
if (!isGroupId(id))
|
|
102
|
-
return
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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 });
|
|
107
188
|
}
|
|
108
189
|
};
|
|
109
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
|
+
};
|
|
@@ -33,7 +33,7 @@ import { currentCloseWatch } from "./closeWatch.js";
|
|
|
33
33
|
import { findConfigFile, loadConfigFile, packageResolver } from "./configFile.js";
|
|
34
34
|
import { codeStamp, diskCache } from "./diskCache.js";
|
|
35
35
|
import { createDocsExtractor, NO_DOCS_EXTRACTOR } from "./docsExtractor.js";
|
|
36
|
-
import { devServerAccess } from "./fileAccess.js";
|
|
36
|
+
import { devServerAccess, VITEST_HOOKS } from "./fileAccess.js";
|
|
37
37
|
import { FORCED_STATES_VISITOR } from "./forcedStates.js";
|
|
38
38
|
import { builtPlacement } from "./framesConfig.js";
|
|
39
39
|
import { serveFrames } from "./framesServer.js";
|
|
@@ -137,7 +137,7 @@ export const forgebookPlugin = (options = {}) => {
|
|
|
137
137
|
/** The groups' search indexes: any edit may change what a component's docs say. */
|
|
138
138
|
const searchModules = new Set();
|
|
139
139
|
/** What a client may read of a dev server. */
|
|
140
|
-
const access = devServerAccess(
|
|
140
|
+
const access = devServerAccess();
|
|
141
141
|
/** The errors the open frames report, for `forgebook errors` and the agents. */
|
|
142
142
|
const live = createLiveErrors();
|
|
143
143
|
/** Where the frames run: the dev server's second port once it listens, the configuration's choice in a build. */
|
|
@@ -392,7 +392,10 @@ export const forgebookPlugin = (options = {}) => {
|
|
|
392
392
|
resolved.logger.warn(`Forgebook's shell theme: ${problem}`);
|
|
393
393
|
},
|
|
394
394
|
configureServer: (server) => {
|
|
395
|
-
access.serve(server);
|
|
395
|
+
access.serve(server, config === undefined ? undefined : pageModulePath(config.dir, AGENT_SOURCES_FILE));
|
|
396
|
+
server.middlewares.use((request, _response, next) => {
|
|
397
|
+
access.settle(request.url ?? "/").then(() => next(), next);
|
|
398
|
+
});
|
|
396
399
|
// no person waits for a page in a test run
|
|
397
400
|
watching.serve(isTestRun ? undefined : server);
|
|
398
401
|
server.httpServer?.on("close", () => void docs?.close());
|
|
@@ -466,13 +469,14 @@ export const forgebookPlugin = (options = {}) => {
|
|
|
466
469
|
await docs?.close();
|
|
467
470
|
},
|
|
468
471
|
...storyFileHotHooks(isRedrawing, served.changedBy, watching),
|
|
472
|
+
...VITEST_HOOKS,
|
|
469
473
|
async load(id) {
|
|
470
474
|
const generated = await pagesModule(id);
|
|
471
475
|
if (generated !== undefined)
|
|
472
476
|
return generated;
|
|
473
|
-
|
|
474
|
-
if (
|
|
475
|
-
return
|
|
477
|
+
const refusal = access.refusal(id);
|
|
478
|
+
if (refusal !== undefined)
|
|
479
|
+
return refusal;
|
|
476
480
|
// in Vite's form, the text Vite's module graph knows a file by: a Node path on Windows (`C:\app\x.md`) is another
|
|
477
481
|
const watch = (file) => this.addWatchFile(VITE_PATHS.normalize(file));
|
|
478
482
|
const resolveImport = async (specifier, importer) => {
|
|
@@ -21,6 +21,21 @@ export declare const virtualId: (source: string) => string | undefined;
|
|
|
21
21
|
* @param id - the module's id.
|
|
22
22
|
*/
|
|
23
23
|
export declare const isGroupId: (id: string) => boolean;
|
|
24
|
+
/**
|
|
25
|
+
* The group a project's module names for a group's module: the story files of their "Used in", reach, search index and
|
|
26
|
+
* texts, the docs pages of their search index. Those read the same files and only their group's module imports them, so
|
|
27
|
+
* a client that already holds the group's module needs no other import to have been seen.
|
|
28
|
+
*
|
|
29
|
+
* @param id - a group's module's id.
|
|
30
|
+
*/
|
|
31
|
+
export declare const namingGroupOf: (id: string) => string;
|
|
32
|
+
/**
|
|
33
|
+
* Whether a module's text names a group. Its transform must run for the plugin to see the import, so a cache of
|
|
34
|
+
* transforms must not keep it.
|
|
35
|
+
*
|
|
36
|
+
* @param code - the module's text.
|
|
37
|
+
*/
|
|
38
|
+
export declare const namesGroup: (code: string) => boolean;
|
|
24
39
|
/** The resolved id of the configuration's data module. */
|
|
25
40
|
export declare const CONFIG_ID = "\0virtual:forgebook/config";
|
|
26
41
|
/** The resolved id of the design tokens' module. */
|
|
@@ -70,6 +70,29 @@ export const virtualId = (source) => {
|
|
|
70
70
|
* @param id - the module's id.
|
|
71
71
|
*/
|
|
72
72
|
export const isGroupId = (id) => VIRTUAL_PREFIXES.some((prefix) => id.startsWith(`\0${prefix}`));
|
|
73
|
+
/** The modules that read a group of story files' own files: named once its story files are. */
|
|
74
|
+
const STORY_FILES_READERS = [USAGE_PREFIX, REACH_PREFIX, SEARCH_PREFIX, TEXTS_PREFIX];
|
|
75
|
+
/**
|
|
76
|
+
* The group a project's module names for a group's module: the story files of their "Used in", reach, search index and
|
|
77
|
+
* texts, the docs pages of their search index. Those read the same files and only their group's module imports them, so
|
|
78
|
+
* a client that already holds the group's module needs no other import to have been seen.
|
|
79
|
+
*
|
|
80
|
+
* @param id - a group's module's id.
|
|
81
|
+
*/
|
|
82
|
+
export const namingGroupOf = (id) => {
|
|
83
|
+
const pagesSearch = `\0${PAGES_SEARCH_PREFIX}`;
|
|
84
|
+
if (id.startsWith(pagesSearch))
|
|
85
|
+
return `${DOCS_PAGES_PREFIX}${id.slice(pagesSearch.length)}`;
|
|
86
|
+
const reader = STORY_FILES_READERS.map((prefix) => `\0${prefix}`).find((mark) => id.startsWith(mark));
|
|
87
|
+
return reader === undefined ? id : `${FILES_PREFIX}${id.slice(reader.length)}`;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Whether a module's text names a group. Its transform must run for the plugin to see the import, so a cache of
|
|
91
|
+
* transforms must not keep it.
|
|
92
|
+
*
|
|
93
|
+
* @param code - the module's text.
|
|
94
|
+
*/
|
|
95
|
+
export const namesGroup = (code) => [FILES_QUERY, DOCS_PAGES_QUERY, ...VIRTUAL_PREFIXES].some((name) => code.includes(name));
|
|
73
96
|
/** The resolved id of the configuration's data module. */
|
|
74
97
|
export const CONFIG_ID = `\0${CONFIG_MODULE}`;
|
|
75
98
|
/** The resolved id of the design tokens' module. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forgebook",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "See every component in every theme, contrast, language and width: CSF stories, one frame per story, and a props table from the components' types.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -240,7 +240,7 @@
|
|
|
240
240
|
"lightningcss": "1.33.0",
|
|
241
241
|
"marked": "18.1.0",
|
|
242
242
|
"typescript-6": "npm:typescript@6.0.3",
|
|
243
|
-
"@forgebook/story-format": "0.2.
|
|
243
|
+
"@forgebook/story-format": "0.2.2"
|
|
244
244
|
},
|
|
245
245
|
"peerDependencies": {
|
|
246
246
|
"@testing-library/dom": "^10.4.0",
|
|
@@ -262,8 +262,8 @@
|
|
|
262
262
|
"sass-embedded": "1.106.1",
|
|
263
263
|
"vite": "8.3.5",
|
|
264
264
|
"vitest": "5.0.5",
|
|
265
|
-
"@forgebook/
|
|
266
|
-
"@forgebook/
|
|
265
|
+
"@forgebook/typescript-config": "0.0.0",
|
|
266
|
+
"@forgebook/tools": "0.0.0"
|
|
267
267
|
},
|
|
268
268
|
"peerDependenciesMeta": {
|
|
269
269
|
"@testing-library/dom": {
|