poveste 0.19.1 → 0.21.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.
- package/README.md +14 -7
- package/dist/node/build.js +22 -4
- package/dist/node/collect/index.js +21 -2
- package/dist/node/collect/module-server.js +9 -3
- package/dist/node/collect/runner.d.ts +15 -4
- package/dist/node/collect/runner.js +52 -5
- package/dist/node/collect/types.d.ts +2 -0
- package/dist/node/collect/worker.js +28 -6
- package/dist/node/config-validation.js +3 -0
- package/dist/node/context.d.ts +3 -0
- package/dist/node/docgen/build.d.ts +8 -0
- package/dist/node/docgen/build.js +52 -0
- package/dist/node/docgen/components-of.d.ts +19 -0
- package/dist/node/docgen/components-of.js +37 -0
- package/dist/node/docgen/engine.d.ts +58 -0
- package/dist/node/docgen/engine.js +91 -0
- package/dist/node/docgen/jsx/extractor.d.ts +8 -0
- package/dist/node/docgen/jsx/extractor.js +79 -0
- package/dist/node/docgen/jsx/walk.d.ts +20 -0
- package/dist/node/docgen/jsx/walk.js +187 -0
- package/dist/node/docgen/protocol.d.ts +34 -0
- package/dist/node/docgen/protocol.js +1 -0
- package/dist/node/docgen/public.d.ts +5 -0
- package/dist/node/docgen/public.js +3 -0
- package/dist/node/docgen/reexports.d.ts +27 -0
- package/dist/node/docgen/reexports.js +187 -0
- package/dist/node/docgen/service.d.ts +65 -0
- package/dist/node/docgen/service.js +207 -0
- package/dist/node/docgen/typed.d.ts +51 -0
- package/dist/node/docgen/typed.js +130 -0
- package/dist/node/docgen/typescript.d.ts +14 -0
- package/dist/node/docgen/typescript.js +81 -0
- package/dist/node/docgen/worker.d.ts +1 -0
- package/dist/node/docgen/worker.js +20 -0
- package/dist/node/histoire-imports.d.ts +10 -0
- package/dist/node/histoire-imports.js +52 -0
- package/dist/node/server.js +31 -0
- package/dist/node/stories.d.ts +16 -1
- package/dist/node/stories.js +77 -11
- package/dist/node/style-isolation/consumer-postcss.d.ts +18 -0
- package/dist/node/style-isolation/consumer-postcss.js +54 -0
- package/dist/node/style-isolation/entry-css-merger.d.ts +12 -0
- package/dist/node/style-isolation/entry-css-merger.js +23 -2
- package/dist/node/util/head.d.ts +8 -1
- package/dist/node/util/head.js +13 -3
- package/dist/node/util/vendors.d.ts +5 -0
- package/dist/node/util/vendors.js +11 -0
- package/dist/node/vendors.d.ts +21 -9
- package/dist/node/vendors.js +32 -12
- package/dist/node/virtual/component-docs.d.ts +9 -0
- package/dist/node/virtual/component-docs.js +33 -0
- package/dist/node/virtual/index.d.ts +12 -9
- package/dist/node/virtual/index.js +9 -5
- package/dist/node/virtual/resolved-config.js +2 -1
- package/dist/node/virtual/resolved-stories.js +7 -5
- package/dist/node/virtual/story-icons.d.ts +20 -0
- package/dist/node/virtual/story-icons.js +77 -0
- package/dist/node/virtual/story-source.js +2 -1
- package/dist/node/virtual/vite-plugin.js +13 -0
- package/dist/node/vite.js +25 -5
- package/docgen.d.ts +1 -0
- package/docgen.js +1 -0
- package/package.json +24 -14
package/README.md
CHANGED
|
@@ -5,9 +5,14 @@
|
|
|
5
5
|
Poveste builds a browsable *book* of stories from your components, powered by
|
|
6
6
|
[Vite](https://vite.dev). It is a drop-in fork of
|
|
7
7
|
[histoire](https://github.com/histoire-dev/histoire), covering Vue, Nuxt, Svelte,
|
|
8
|
-
SvelteKit and Quasar — the last of which
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
SvelteKit and Quasar — the last of which no released version of histoire includes — and
|
|
9
|
+
Solid and React, whose stories render with no controls panel yet.
|
|
10
|
+
|
|
11
|
+
In every one of those books, each story's **Docs** tab also shows a props, slots and
|
|
12
|
+
events table read from its components' types: the ones a Vue or Svelte story imports, or
|
|
13
|
+
the one a Solid or React story names in its `component` field. That needs `typescript` 6
|
|
14
|
+
or earlier in the book, plus `svelte2tsx` in a Svelte book and `@types/react` in a React
|
|
15
|
+
one.
|
|
11
16
|
|
|
12
17
|
<img src="https://raw.githubusercontent.com/poveste-dev/poveste/main/screenshot.png" alt="A poveste book: the story list, a variant grid, and the controls panel" width="900">
|
|
13
18
|
|
|
@@ -20,11 +25,11 @@ pnpm i -D poveste @poveste/plugin-vue # Vue 3
|
|
|
20
25
|
pnpm i -D poveste @poveste/plugin-svelte # Svelte 5 and SvelteKit
|
|
21
26
|
pnpm i -D poveste @poveste/plugin-vue @poveste/plugin-nuxt # Nuxt 4
|
|
22
27
|
pnpm i -D poveste @poveste/plugin-vue @poveste/plugin-quasar # Quasar
|
|
23
|
-
pnpm i -D poveste @poveste/plugin-solid # Solid,
|
|
24
|
-
pnpm i -D poveste @poveste/plugin-react # React 19,
|
|
28
|
+
pnpm i -D poveste @poveste/plugin-solid # Solid, no controls yet
|
|
29
|
+
pnpm i -D poveste @poveste/plugin-react # React 19, no controls yet
|
|
25
30
|
```
|
|
26
31
|
|
|
27
|
-
> **Installed an older version than you expected?** For about a day after a release, pnpm installs the previous one with no error
|
|
32
|
+
> **Installed an older version than you expected?** For about a day after a release, pnpm installs the previous one with no error, and for a plugin that's new in that release, its empty `0.0.1` placeholder. pnpm 11.5.3 added `(x.y.z is available)` after the version it installed; pnpm 12.14.0 said nothing. That's pnpm's release-age cooldown, not a broken publish. `package.json` keeps the old `^x.y.z`, so waiting doesn't fix it: install again with `npm`, with `--config.minimum-release-age=0` (kebab-case: pnpm 12.4.0 ignores the camelCase spelling, which 12.14.0 accepts), or with exact versions.
|
|
28
33
|
|
|
29
34
|
Then create a `poveste.config.ts` in your project root. For a Vue project it is this:
|
|
30
35
|
|
|
@@ -57,7 +62,9 @@ Node `>=22.22.2` and Vite `^8.0.0`. See
|
|
|
57
62
|
[supported versions](https://poveste.dev/guide/getting-started#supported-versions) for
|
|
58
63
|
what proves each range.
|
|
59
64
|
|
|
60
|
-
On Node 24,
|
|
65
|
+
On Node 24, prefer 24.15 or later. Poveste's DOM dependency, jsdom, and three of its own dependencies declare `^22.22.2 || ^24.15.0 || >=26`. On 24.13.0 npm warns `EBADENGINE` for each of them and installs anyway, and a Vue book built there, but nothing tests Poveste on a Node 24 before 24.15.
|
|
66
|
+
|
|
67
|
+
Auto-docs, the props, slots and events table in Vue, Nuxt, Quasar, Svelte, SvelteKit, Solid and React books, needs `typescript` 6 or earlier in the book: `npm i -D typescript@6`. A Svelte or SvelteKit book also needs `svelte2tsx`, and a React book `@types/react`. Without them, or with TypeScript 7, it turns itself off and says so once. See [`autoDocs`](https://poveste.dev/reference/config#autodocs).
|
|
61
68
|
|
|
62
69
|
## Links
|
|
63
70
|
|
package/dist/node/build.js
CHANGED
|
@@ -8,13 +8,14 @@ import { APP_PATH } from './alias.js';
|
|
|
8
8
|
import { buildCounts, buildSummary, builtSomething, storyGuideUrl } from './build-counts.js';
|
|
9
9
|
import { getSerializedStoryData } from './build-serialize.js';
|
|
10
10
|
import { useCollectStories } from './collect/index.js';
|
|
11
|
+
import { extractComponentDocs } from './docgen/build.js';
|
|
11
12
|
import { useModuleLoader } from './load.js';
|
|
12
13
|
import { createMarkdownFilesWatcher } from './markdown.js';
|
|
13
14
|
import { BuildPluginApi } from './plugin.js';
|
|
14
15
|
import { startPreview } from './preview.js';
|
|
15
16
|
import { findAllStories } from './stories.js';
|
|
16
17
|
import { chromeCssScopePlugin, entryCssMergerPlugin, userCssScopePlugin, } from './style-isolation/index.js';
|
|
17
|
-
import { applyHeadTransform } from './util/head.js';
|
|
18
|
+
import { applyHeadTransform, defaultDescription } from './util/head.js';
|
|
18
19
|
import { wrapLogError } from './util/log.js';
|
|
19
20
|
import { viteMode } from './util/vite-mode.js';
|
|
20
21
|
import { getViteConfigWithPlugins } from './vite.js';
|
|
@@ -38,6 +39,12 @@ const APP_ONLY_VENDORS = [
|
|
|
38
39
|
/[\\/]node_modules[\\/]shiki[\\/]/,
|
|
39
40
|
/[\\/]node_modules[\\/]@shikijs[\\/]/,
|
|
40
41
|
];
|
|
42
|
+
// Left to the bundler wherever they are installed, rather than sent to `vendor`.
|
|
43
|
+
// `@poveste/controls` loads three controls lazily, and in a book installed from
|
|
44
|
+
// npm `vendor` took them in, so every reader downloaded them before anything
|
|
45
|
+
// rendered. The workspace book never showed it: linked source has no
|
|
46
|
+
// `node_modules` in its path (#1343).
|
|
47
|
+
const LEFT_TO_BUNDLER = ['@poveste/app', '@poveste/controls'];
|
|
41
48
|
const PREFETCHED_MODULES = [
|
|
42
49
|
'StoryView',
|
|
43
50
|
'reactivity',
|
|
@@ -117,6 +124,7 @@ export async function build(ctx) {
|
|
|
117
124
|
// Same wrapper the dev path uses for these exact calls (server.ts).
|
|
118
125
|
await wrapLogError('destroyCollectStories', () => destroyCollectStories());
|
|
119
126
|
}
|
|
127
|
+
ctx.componentDocs = await extractComponentDocs(ctx, server);
|
|
120
128
|
const counts = buildCounts(ctx.storyFiles);
|
|
121
129
|
const { viteConfig: buildViteConfigRaw } = await getViteConfigWithPlugins(false, ctx);
|
|
122
130
|
const buildViteConfig = mergeViteConfig(buildViteConfigRaw, {
|
|
@@ -198,7 +206,7 @@ export async function build(ctx) {
|
|
|
198
206
|
if (id.replace(/^\0/, '').startsWith('vite/')) {
|
|
199
207
|
return 'app-runtime';
|
|
200
208
|
}
|
|
201
|
-
if (!id.includes(
|
|
209
|
+
if (!LEFT_TO_BUNDLER.some(name => id.includes(name)) && id.includes('node_modules')) {
|
|
202
210
|
if (APP_ONLY_VENDORS.some(test => test.test(id))) {
|
|
203
211
|
return 'highlighter';
|
|
204
212
|
}
|
|
@@ -233,6 +241,10 @@ export async function build(ctx) {
|
|
|
233
241
|
// returns whatever the minifier left — `codegen-vue.spec.ts` fails
|
|
234
242
|
// on three cases with this on (#329).
|
|
235
243
|
minify: false,
|
|
244
|
+
// CSS too, whatever a book's own config says: Vite's minifier strips the
|
|
245
|
+
// comments story CSS is found by before it is wrapped in `@scope`. The
|
|
246
|
+
// entry-css-merger minifies instead, after wrapping (#1441).
|
|
247
|
+
cssMinify: false,
|
|
236
248
|
// Don't build in SSR mode
|
|
237
249
|
ssr: false,
|
|
238
250
|
});
|
|
@@ -242,7 +254,7 @@ export async function build(ctx) {
|
|
|
242
254
|
const isolate = ctx.config.isolateStyles !== false;
|
|
243
255
|
buildPlugins.push(userCssScopePlugin({ enabled: isolate }));
|
|
244
256
|
buildPlugins.push(chromeCssScopePlugin({ enabled: isolate }));
|
|
245
|
-
buildPlugins.push(entryCssMergerPlugin({ isolateStyles: isolate }));
|
|
257
|
+
buildPlugins.push(entryCssMergerPlugin({ isolateStyles: isolate, minify: true }));
|
|
246
258
|
for (const cb of changeViteConfigCallbacks) {
|
|
247
259
|
await cb(buildViteConfig);
|
|
248
260
|
}
|
|
@@ -271,13 +283,19 @@ export async function build(ctx) {
|
|
|
271
283
|
let indexHtml = generateEntryHtml(indexOutput.fileName, mainStyleOutput.fileName, {
|
|
272
284
|
HEAD: `${preloadHtml}${prefetchHtml}`,
|
|
273
285
|
}, ctx);
|
|
274
|
-
indexHtml = await applyHeadTransform(indexHtml, ctx.config.head);
|
|
286
|
+
indexHtml = await applyHeadTransform(indexHtml, ctx.config.head, defaultDescription(ctx.config.theme.title ?? 'Poveste'));
|
|
275
287
|
await writeFile('index.html', indexHtml, ctx);
|
|
276
288
|
// Sandbox
|
|
277
289
|
let sandboxHtml = generateEntryHtml(sandboxOutput.fileName, sandboxStyleOutput.fileName, {}, ctx);
|
|
278
290
|
sandboxHtml = await applyHeadTransform(sandboxHtml, ctx.config.head);
|
|
279
291
|
await writeFile('__sandbox.html', sandboxHtml, ctx);
|
|
280
292
|
await writeFile('poveste.json', JSON.stringify(getSerializedStoryData(ctx), null, 2), ctx);
|
|
293
|
+
// A host's SPA fallback answers a missing /robots.txt with index.html, which a
|
|
294
|
+
// crawler reads as a malformed robots file rather than as none (#316). One in
|
|
295
|
+
// the public directory is the book's own, and was copied here already.
|
|
296
|
+
if (!await fs.pathExists(join(ctx.config.outDir, 'robots.txt'))) {
|
|
297
|
+
await writeFile('robots.txt', 'User-agent: *\nAllow: /\n', ctx);
|
|
298
|
+
}
|
|
281
299
|
const duration = performance.now() - startTime;
|
|
282
300
|
const { empty } = counts;
|
|
283
301
|
if (empty.length) {
|
|
@@ -5,6 +5,7 @@ import { TEMP_PATH } from '../alias.js';
|
|
|
5
5
|
import { createPath } from '../tree.js';
|
|
6
6
|
import { escapeRegExp } from '../util/escape-regexp.js';
|
|
7
7
|
import { slash } from '../util/fs.js';
|
|
8
|
+
import { NOOP_ID, SETUP_ID } from '../virtual/index.js';
|
|
8
9
|
import { globalsFromDefine } from './define-globals.js';
|
|
9
10
|
import { strayLayoutKeys } from './layout-keys.js';
|
|
10
11
|
import { createModuleServer } from './module-server.js';
|
|
@@ -41,7 +42,16 @@ export function useCollectStories(options, ctx) {
|
|
|
41
42
|
threads: threadsCount,
|
|
42
43
|
invoke: (name, data) => node.invoke(name, data),
|
|
43
44
|
});
|
|
45
|
+
// Resolved once: a change to the config restarts the server.
|
|
46
|
+
let setupFile;
|
|
47
|
+
function resolveSetupFile() {
|
|
48
|
+
return setupFile ??= server.pluginContainer.resolveId(SETUP_ID, undefined, { ssr: true })
|
|
49
|
+
.then(resolved => resolved && resolved.id !== NOOP_ID ? resolved.id : undefined);
|
|
50
|
+
}
|
|
51
|
+
// Every story the setup file reaches fails with it, so it is reported once a pass.
|
|
52
|
+
let setupFailureReported = false;
|
|
44
53
|
function clearCache() {
|
|
54
|
+
setupFailureReported = false;
|
|
45
55
|
server.moduleGraph.invalidateAll();
|
|
46
56
|
node.clearCache();
|
|
47
57
|
// Both of those make every module read again, and a worker told nothing would
|
|
@@ -66,6 +76,7 @@ export function useCollectStories(options, ctx) {
|
|
|
66
76
|
base: server.config.base,
|
|
67
77
|
storyFile,
|
|
68
78
|
defineGlobals,
|
|
79
|
+
setupFile: await resolveSetupFile(),
|
|
69
80
|
};
|
|
70
81
|
const { storyData } = await threadPool.run(payload);
|
|
71
82
|
const finalData = storyData[0];
|
|
@@ -101,12 +112,20 @@ export function useCollectStories(options, ctx) {
|
|
|
101
112
|
catch (e) {
|
|
102
113
|
// Work `destroy` cut short fails with whatever the pool or a closing server
|
|
103
114
|
// throws. That is the shutdown, not the story, so it is not reported (#878).
|
|
104
|
-
|
|
115
|
+
// A build destroys only once a story has failed, which already threw.
|
|
116
|
+
if (destroying) {
|
|
105
117
|
return;
|
|
106
118
|
}
|
|
107
119
|
// A Vite transform error carries the code frame on `frame`.
|
|
108
120
|
const error = (e instanceof Error ? e : new Error(String(e)));
|
|
109
|
-
|
|
121
|
+
const detail = error.frame ? `${pc.bold(error.message)}\n${error.frame}` : error.stack;
|
|
122
|
+
if (!error.setupFile) {
|
|
123
|
+
console.error(pc.red(`Error while collecting story ${storyFile.path}:\n${detail}`));
|
|
124
|
+
}
|
|
125
|
+
else if (!setupFailureReported) {
|
|
126
|
+
setupFailureReported = true;
|
|
127
|
+
console.error(pc.red(`Error while loading the setup file ${error.setupFile}, so no story that needs it was collected:\n${detail}`));
|
|
128
|
+
}
|
|
110
129
|
if (options.throws) {
|
|
111
130
|
throw e;
|
|
112
131
|
}
|
|
@@ -152,6 +152,13 @@ export function createModuleServer(server, options) {
|
|
|
152
152
|
? true
|
|
153
153
|
: [...options.inline, ...(Array.isArray(noExternal) ? noExternal : noExternal ? [noExternal] : [])];
|
|
154
154
|
const externalizeCache = new Map();
|
|
155
|
+
// Each entry keeps the module's invalidation time it was transformed under, in
|
|
156
|
+
// Vite's clock rather than `Date.now()`. That clock adds a millisecond for every
|
|
157
|
+
// call within one, so a burst of transforms runs it ahead of the wall clock, and
|
|
158
|
+
// `invalidateAll` then stamps every module seconds in the future. Against
|
|
159
|
+
// `Date.now()` the whole cache read as stale until real time caught up, each
|
|
160
|
+
// re-transform pushed the clock further ahead, and one reload re-collected a
|
|
161
|
+
// large book a hundred times over (#1352).
|
|
155
162
|
const fetchCache = new Map();
|
|
156
163
|
function getTransformMode(id) {
|
|
157
164
|
const path = cleanUrl(id);
|
|
@@ -227,12 +234,11 @@ export function createModuleServer(server, options) {
|
|
|
227
234
|
const moduleNode = server.moduleGraph.getModuleById(id) ?? [...server.moduleGraph.getModulesByFile(path) ?? []][0];
|
|
228
235
|
const invalidatedAt = moduleNode ? Math.max(moduleNode.lastHMRTimestamp, moduleNode.lastInvalidationTimestamp) : 0;
|
|
229
236
|
const cached = fetchCache.get(path);
|
|
230
|
-
if (cached &&
|
|
237
|
+
if (cached && invalidatedAt <= cached.invalidatedAt) {
|
|
231
238
|
// Unchanged since the runner last read it: it keeps what it has. A runner
|
|
232
239
|
// that never had it gets the code, with nothing of its own to invalidate.
|
|
233
240
|
return cachedByRunner ? { cache: true } : { ...cached.result, invalidate: false };
|
|
234
241
|
}
|
|
235
|
-
const timestamp = Date.now();
|
|
236
242
|
const result = await transform(id, getTransformMode(id));
|
|
237
243
|
if (!result) {
|
|
238
244
|
throw new Error(`[poveste] could not transform ${id}${importer ? `, imported from ${importer}` : ''}`);
|
|
@@ -244,7 +250,7 @@ export function createModuleServer(server, options) {
|
|
|
244
250
|
url: id,
|
|
245
251
|
invalidate: true,
|
|
246
252
|
};
|
|
247
|
-
fetchCache.set(path, {
|
|
253
|
+
fetchCache.set(path, { invalidatedAt, result: fetched });
|
|
248
254
|
return fetched;
|
|
249
255
|
}
|
|
250
256
|
/** The two calls a `ModuleRunner` makes, as its transport's `invoke` carries them. */
|
|
@@ -31,11 +31,22 @@ export type Invoke = (name: string, data: unknown[]) => Promise<unknown>;
|
|
|
31
31
|
*/
|
|
32
32
|
export declare function filenameOf(module: Readonly<EvaluatedModuleNode>, root: string): string;
|
|
33
33
|
/**
|
|
34
|
-
* The keys
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
34
|
+
* The keys a module Node ran itself added to `globalThis`. Node caches that module,
|
|
35
|
+
* so once a worker's reset deletes them nothing installs them again. Vue's
|
|
36
|
+
* development build sets `__VUE_HMR_RUNTIME__` this way, and a story compiled with
|
|
37
|
+
* HMR code reads it at its top level (#1320). Node runs a module either as an
|
|
38
|
+
* external, or when an inlined CommonJS module requires it: an installed Nuxt or
|
|
39
|
+
* Quasar book's `vue.cjs.prod.js` is inlined and requires `@vue/runtime-dom` (#1404).
|
|
38
40
|
*/
|
|
39
41
|
export declare const globalsFromExternals: Set<string>;
|
|
42
|
+
/**
|
|
43
|
+
* Lets a dependency Node runs natively import its own stylesheet. A `type: module`
|
|
44
|
+
* package's `.js` is externalised, and @primer/react 38's `dist` imports `.css`
|
|
45
|
+
* files, which Node has no format for (#1392). Nothing in collection reads a
|
|
46
|
+
* stylesheet, so for this thread it is an empty module; the book still gets it from
|
|
47
|
+
* Vite. Checked rather than assumed, because a runtime that is not Node, such as
|
|
48
|
+
* StackBlitz's, may not have the hook.
|
|
49
|
+
*/
|
|
50
|
+
export declare function stubStylesheetsForNode(): void;
|
|
40
51
|
/** A runner over a module server, reached through `invoke`. `root` is the Vite root. */
|
|
41
52
|
export declare function createRunner(invoke: Invoke, root: string, evaluatedModules?: EvaluatedModules): ModuleRunner;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
+
import * as nodeModule from 'node:module';
|
|
2
3
|
import { createRequire } from 'node:module';
|
|
3
4
|
import { dirname, isAbsolute, join } from 'node:path';
|
|
4
5
|
import process from 'node:process';
|
|
@@ -68,13 +69,40 @@ export function filenameOf(module, root) {
|
|
|
68
69
|
return join(root, 'virtual-module');
|
|
69
70
|
}
|
|
70
71
|
/**
|
|
71
|
-
* The keys
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
72
|
+
* The keys a module Node ran itself added to `globalThis`. Node caches that module,
|
|
73
|
+
* so once a worker's reset deletes them nothing installs them again. Vue's
|
|
74
|
+
* development build sets `__VUE_HMR_RUNTIME__` this way, and a story compiled with
|
|
75
|
+
* HMR code reads it at its top level (#1320). Node runs a module either as an
|
|
76
|
+
* external, or when an inlined CommonJS module requires it: an installed Nuxt or
|
|
77
|
+
* Quasar book's `vue.cjs.prod.js` is inlined and requires `@vue/runtime-dom` (#1404).
|
|
75
78
|
*/
|
|
76
79
|
export const globalsFromExternals = new Set();
|
|
77
80
|
const ranExternals = new Set();
|
|
81
|
+
const requiredModules = new Set();
|
|
82
|
+
/** The `require` an inlined module gets, recording what each module it loads first installs. */
|
|
83
|
+
function requireRecordingGlobals(filename) {
|
|
84
|
+
const nodeRequire = createRequire(filename);
|
|
85
|
+
const recording = (id) => {
|
|
86
|
+
const resolved = nodeRequire.resolve(id);
|
|
87
|
+
if (requiredModules.has(resolved)) {
|
|
88
|
+
return nodeRequire(id);
|
|
89
|
+
}
|
|
90
|
+
const before = new Set(Object.keys(globalThis));
|
|
91
|
+
try {
|
|
92
|
+
return nodeRequire(id);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
requiredModules.add(resolved);
|
|
96
|
+
for (const key of Object.keys(globalThis)) {
|
|
97
|
+
if (!before.has(key)) {
|
|
98
|
+
globalsFromExternals.add(key);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
// `resolve`, `cache` and the rest, as the module expects of `require`.
|
|
104
|
+
return Object.assign(recording, nodeRequire);
|
|
105
|
+
}
|
|
78
106
|
async function importRecordingGlobals(file) {
|
|
79
107
|
const before = new Set(Object.keys(globalThis));
|
|
80
108
|
const imported = await import(file);
|
|
@@ -86,6 +114,25 @@ async function importRecordingGlobals(file) {
|
|
|
86
114
|
}
|
|
87
115
|
return imported;
|
|
88
116
|
}
|
|
117
|
+
const STYLESHEET = /\.(?:css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:[?#]|$)/;
|
|
118
|
+
/**
|
|
119
|
+
* Lets a dependency Node runs natively import its own stylesheet. A `type: module`
|
|
120
|
+
* package's `.js` is externalised, and @primer/react 38's `dist` imports `.css`
|
|
121
|
+
* files, which Node has no format for (#1392). Nothing in collection reads a
|
|
122
|
+
* stylesheet, so for this thread it is an empty module; the book still gets it from
|
|
123
|
+
* Vite. Checked rather than assumed, because a runtime that is not Node, such as
|
|
124
|
+
* StackBlitz's, may not have the hook.
|
|
125
|
+
*/
|
|
126
|
+
export function stubStylesheetsForNode() {
|
|
127
|
+
if (typeof nodeModule.registerHooks !== 'function') {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
nodeModule.registerHooks({
|
|
131
|
+
load: (url, context, nextLoad) => STYLESHEET.test(url)
|
|
132
|
+
? { format: 'module', source: 'export default {}', shortCircuit: true }
|
|
133
|
+
: nextLoad(url, context),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
89
136
|
/** As vite-node did: a CJS package's named exports are read through its default. */
|
|
90
137
|
class InteropEvaluator {
|
|
91
138
|
root;
|
|
@@ -146,7 +193,7 @@ class InteropEvaluator {
|
|
|
146
193
|
// own scope it redeclared it, a syntax error that failed every story of the book
|
|
147
194
|
// (#1301). The closing brace goes on a new line: the code may end in a line comment.
|
|
148
195
|
const run = new AsyncFunction(ssrModuleExportsKey, ssrImportMetaKey, ssrImportKey, ssrDynamicImportKey, ssrExportAllKey, ssrExportNameKey, 'require', 'exports', 'module', '__filename', '__dirname', `"use strict";{\n${code}\n}`);
|
|
149
|
-
await run(context[ssrModuleExportsKey], context[ssrImportMetaKey], context[ssrImportKey], context[ssrDynamicImportKey], context[ssrExportAllKey], context[ssrExportNameKey],
|
|
196
|
+
await run(context[ssrModuleExportsKey], context[ssrImportMetaKey], context[ssrImportKey], context[ssrDynamicImportKey], context[ssrExportAllKey], context[ssrExportNameKey], requireRecordingGlobals(filename), cjsExports, moduleProxy, filename, dirname(filename));
|
|
150
197
|
}
|
|
151
198
|
async runExternalModule(file) {
|
|
152
199
|
const imported = ranExternals.has(file) ? await import(file) : await importRecordingGlobals(file);
|
|
@@ -4,6 +4,8 @@ export interface Payload {
|
|
|
4
4
|
base: string;
|
|
5
5
|
storyFile: ServerStoryFile;
|
|
6
6
|
defineGlobals?: Record<string, unknown>;
|
|
7
|
+
/** The book's setup file as the collect server resolved it, when it has one. */
|
|
8
|
+
setupFile?: string | undefined;
|
|
7
9
|
}
|
|
8
10
|
export interface ReturnData {
|
|
9
11
|
storyData: ServerStory[];
|
|
@@ -6,7 +6,7 @@ import pc from 'picocolors';
|
|
|
6
6
|
import { EvaluatedModules } from 'vite/module-runner';
|
|
7
7
|
import { createDomEnv, resetDomEnv } from '../dom/env.js';
|
|
8
8
|
import { invokeOver } from './rpc.js';
|
|
9
|
-
import { createRunner, globalsFromExternals } from './runner.js';
|
|
9
|
+
import { createRunner, globalsFromExternals, stubStylesheetsForNode } from './runner.js';
|
|
10
10
|
import { serveTasks } from './task.js';
|
|
11
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
12
|
const _evaluatedModules = new EvaluatedModules();
|
|
@@ -41,12 +41,29 @@ function invalidateAll() {
|
|
|
41
41
|
_evaluatedModules.invalidateModule(node);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
// A framework's collect module imports the setup file, so a setup file that fails
|
|
45
|
+
// to load fails every story with an error that names neither of them (#1324). The
|
|
46
|
+
// runner keeps a module that failed as its rejected promise, which is how it is told
|
|
47
|
+
// apart from a story that failed on its own. Matched on the fetched file, because
|
|
48
|
+
// the module's id is however its importer wrote it, root-relative inside the root.
|
|
49
|
+
async function blameSetupFile(error, setupFile) {
|
|
50
|
+
const nodes = setupFile
|
|
51
|
+
? [..._evaluatedModules.idToModuleMap.values()].filter(node => node.meta && 'file' in node.meta && node.meta.file === setupFile)
|
|
52
|
+
: [];
|
|
53
|
+
const failed = await Promise.all(nodes.map(node => node.promise?.then(() => false, () => true)));
|
|
54
|
+
if (!failed.includes(true)) {
|
|
55
|
+
return error;
|
|
56
|
+
}
|
|
57
|
+
const blamed = typeof error === 'object' && error !== null ? error : new Error(String(error));
|
|
58
|
+
return Object.assign(blamed, { setupFile });
|
|
59
|
+
}
|
|
44
60
|
if (!parentPort) {
|
|
45
61
|
throw new Error('[poveste] the collection worker was started outside a worker thread');
|
|
46
62
|
}
|
|
47
63
|
// The port it answers over is the one tasks arrive on, so nothing is handed over
|
|
48
64
|
// per story.
|
|
49
65
|
const _invoke = invokeOver(parentPort);
|
|
66
|
+
stubStylesheetsForNode();
|
|
50
67
|
serveTasks(parentPort, { invalidate, invalidateAll, collect });
|
|
51
68
|
async function collect(payload) {
|
|
52
69
|
const startTime = performance.now();
|
|
@@ -73,11 +90,16 @@ async function collect(payload) {
|
|
|
73
90
|
const { run } = (await runner.import(resolve(__dirname, './run.js')));
|
|
74
91
|
const afterExecuteTime = performance.now();
|
|
75
92
|
const storyData = [];
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
93
|
+
try {
|
|
94
|
+
await run({
|
|
95
|
+
file: payload.storyFile,
|
|
96
|
+
storyData,
|
|
97
|
+
el,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
throw await blameSetupFile(error, payload.setupFile);
|
|
102
|
+
}
|
|
81
103
|
const afterRunTime = performance.now();
|
|
82
104
|
if (payload.storyFile.markdownFile) {
|
|
83
105
|
const el = document.createElement('div');
|
|
@@ -7,6 +7,9 @@ const RULES = [
|
|
|
7
7
|
{ path: 'setupCode', kind: 'string[]' },
|
|
8
8
|
{ path: 'globalStyles', kind: 'string[]' },
|
|
9
9
|
{ path: 'isolateStyles', kind: 'boolean' },
|
|
10
|
+
{ path: 'autoDocs', kind: ['boolean', 'object'] },
|
|
11
|
+
{ path: 'autoDocs.allow', kind: 'string[]' },
|
|
12
|
+
{ path: 'autoDocs.exclude', kind: 'string[]' },
|
|
10
13
|
{ path: 'autoApplyContrastColor', kind: 'boolean' },
|
|
11
14
|
{ path: 'defaultBackgroundColor', kind: 'string' },
|
|
12
15
|
{ path: 'sandboxDarkClass', kind: 'string' },
|
package/dist/node/context.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ConfigMode, FinalSupportPlugin, PluginCommand, PovesteConfig, ServerMarkdownFile, ServerStoryFile } from '@poveste/shared';
|
|
2
2
|
import type { ResolvedConfig } from 'vite';
|
|
3
|
+
import type { StoryDocsResult } from './docgen/protocol.js';
|
|
3
4
|
export interface Context {
|
|
4
5
|
root: string;
|
|
5
6
|
config: PovesteConfig;
|
|
@@ -9,6 +10,8 @@ export interface Context {
|
|
|
9
10
|
supportPlugins: FinalSupportPlugin[];
|
|
10
11
|
markdownFiles: ServerMarkdownFile[];
|
|
11
12
|
registeredCommands: PluginCommand[];
|
|
13
|
+
/** What `poveste build` extracted for auto-docs, by story id (#1159). */
|
|
14
|
+
componentDocs?: Record<string, StoryDocsResult['components']>;
|
|
12
15
|
}
|
|
13
16
|
export interface CreateContextOptions {
|
|
14
17
|
mode: Context['mode'];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ViteDevServer } from 'vite';
|
|
2
|
+
import type { Context } from '../context.js';
|
|
3
|
+
import type { StoryDocsResult } from './protocol.js';
|
|
4
|
+
/**
|
|
5
|
+
* The build's pass (#1159): with no reader to wait for, every story's named
|
|
6
|
+
* components are extracted once, after collection, into what the book ships.
|
|
7
|
+
*/
|
|
8
|
+
export declare function extractComponentDocs(ctx: Context, server: Pick<ViteDevServer, 'moduleGraph' | 'pluginContainer'>): Promise<Record<string, StoryDocsResult['components']>>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { componentsOfStory, storyOf } from './components-of.js';
|
|
2
|
+
import { createDocgenService } from './service.js';
|
|
3
|
+
/**
|
|
4
|
+
* The build's pass (#1159): with no reader to wait for, every story's named
|
|
5
|
+
* components are extracted once, after collection, into what the book ships.
|
|
6
|
+
*/
|
|
7
|
+
export async function extractComponentDocs(ctx, server) {
|
|
8
|
+
const docgen = createDocgenService({
|
|
9
|
+
root: ctx.root,
|
|
10
|
+
plugins: ctx.config.plugins,
|
|
11
|
+
enabled: ctx.config.autoDocs !== false,
|
|
12
|
+
bookOptions: typeof ctx.config.autoDocs === 'object' ? ctx.config.autoDocs : undefined,
|
|
13
|
+
collected: Promise.resolve(),
|
|
14
|
+
storyOf: storyId => storyOf(ctx.storyFiles, storyId),
|
|
15
|
+
componentsOf: storyId => componentsOfStory(ctx.storyFiles, server, storyId),
|
|
16
|
+
});
|
|
17
|
+
if (!docgen.enabled) {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
const docs = {};
|
|
21
|
+
const failed = new Map();
|
|
22
|
+
try {
|
|
23
|
+
for (const storyFile of ctx.storyFiles) {
|
|
24
|
+
const storyId = storyFile.story?.id;
|
|
25
|
+
if (!storyId) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const { components } = await docgen.request(storyId);
|
|
29
|
+
// A built book is published, so an extractor's error stays on the build
|
|
30
|
+
// machine: the component keeps its runtime props, as with auto-docs off (#1382).
|
|
31
|
+
// A result with neither is a file with nothing to document, such as a JSX
|
|
32
|
+
// story that names no component, and is not a failure.
|
|
33
|
+
const read = Object.entries(components).filter(([file, result]) => {
|
|
34
|
+
if (!result.doc && result.error !== undefined) {
|
|
35
|
+
failed.set(file, result.error);
|
|
36
|
+
}
|
|
37
|
+
return result.doc;
|
|
38
|
+
});
|
|
39
|
+
if (read.length) {
|
|
40
|
+
docs[storyId] = Object.fromEntries(read);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
await docgen.dispose();
|
|
46
|
+
}
|
|
47
|
+
if (failed.size) {
|
|
48
|
+
const [file, error] = [...failed][0];
|
|
49
|
+
console.warn(`[poveste] auto-docs could not read ${failed.size} component${failed.size === 1 ? '' : 's'}, which show their runtime props instead. The first, ${file}: ${error}`);
|
|
50
|
+
}
|
|
51
|
+
return docs;
|
|
52
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ServerStory, ServerStoryFile } from '@poveste/shared';
|
|
2
|
+
import type { ViteDevServer } from 'vite';
|
|
3
|
+
/** The story's own file, absolute, the framework collection gave it, and its own `autoDocs`. */
|
|
4
|
+
export declare function storyOf(storyFiles: ServerStoryFile[], storyId: string): {
|
|
5
|
+
file: string;
|
|
6
|
+
supportPluginId: string;
|
|
7
|
+
autoDocs?: ServerStory['autoDocs'];
|
|
8
|
+
} | undefined;
|
|
9
|
+
/** The components a story names, and how many capitalised imports reached none. */
|
|
10
|
+
export interface StoryComponents {
|
|
11
|
+
files: string[];
|
|
12
|
+
unresolved: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The files a story names: those its `component` field lists, when it has one; else
|
|
16
|
+
* what its own module imports, as collection resolved it, and the components its
|
|
17
|
+
* imports reach through re-exports (#1432). Other story files are left out.
|
|
18
|
+
*/
|
|
19
|
+
export declare function componentsOfStory(storyFiles: ServerStoryFile[], server: Pick<ViteDevServer, 'moduleGraph' | 'pluginContainer'>, storyId: string): Promise<StoryComponents>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute } from 'pathe';
|
|
3
|
+
import { storyComponents } from './reexports.js';
|
|
4
|
+
/** The story's own file, absolute, the framework collection gave it, and its own `autoDocs`. */
|
|
5
|
+
export function storyOf(storyFiles, storyId) {
|
|
6
|
+
const storyFile = storyFiles.find(file => file.story?.id === storyId);
|
|
7
|
+
return storyFile && { file: storyFile.path, supportPluginId: storyFile.supportPluginId, autoDocs: storyFile.story?.autoDocs };
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The files a story names: those its `component` field lists, when it has one; else
|
|
11
|
+
* what its own module imports, as collection resolved it, and the components its
|
|
12
|
+
* imports reach through re-exports (#1432). Other story files are left out.
|
|
13
|
+
*/
|
|
14
|
+
export async function componentsOfStory(storyFiles, server, storyId) {
|
|
15
|
+
const storyFile = storyOf(storyFiles, storyId)?.file;
|
|
16
|
+
if (!storyFile) {
|
|
17
|
+
return { files: [], unresolved: 0 };
|
|
18
|
+
}
|
|
19
|
+
const resolve = async (specifier, importer) => {
|
|
20
|
+
const id = (await server.pluginContainer.resolveId(specifier, importer, { ssr: true }))?.id.split('?')[0];
|
|
21
|
+
return id && isAbsolute(id) && existsSync(id) ? id : undefined;
|
|
22
|
+
};
|
|
23
|
+
const traced = await storyComponents(storyFile, resolve).catch(() => ({ files: [], named: false, unresolved: 0 }));
|
|
24
|
+
if (traced.named) {
|
|
25
|
+
return { files: traced.files, unresolved: traced.unresolved };
|
|
26
|
+
}
|
|
27
|
+
const storyPaths = new Set(storyFiles.map(file => file.path));
|
|
28
|
+
const files = new Set(traced.files);
|
|
29
|
+
for (const mod of server.moduleGraph.getModulesByFile(storyFile) ?? []) {
|
|
30
|
+
for (const imported of mod.importedModules) {
|
|
31
|
+
if (imported.file && !storyPaths.has(imported.file)) {
|
|
32
|
+
files.add(imported.file);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { files: [...files], unresolved: traced.unresolved };
|
|
37
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ComponentDoc, DocgenExtractorModule } from '@poveste/shared';
|
|
2
|
+
export interface EngineExtractorSpec {
|
|
3
|
+
/** The plugin's name, which keys its extractor. */
|
|
4
|
+
name: string;
|
|
5
|
+
module: string;
|
|
6
|
+
options?: unknown;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* An empty result is a file the extractor found nothing to document in, such as a story naming no component.
|
|
10
|
+
* A failure's `name` is the component's, when the extractor knew it and the file is not the component's own.
|
|
11
|
+
*/
|
|
12
|
+
export type ExtractResult = {
|
|
13
|
+
doc: ComponentDoc;
|
|
14
|
+
} | {
|
|
15
|
+
doc?: undefined;
|
|
16
|
+
error: string;
|
|
17
|
+
name?: string;
|
|
18
|
+
} | {
|
|
19
|
+
doc?: undefined;
|
|
20
|
+
error?: undefined;
|
|
21
|
+
};
|
|
22
|
+
export interface DocgenEngineOptions {
|
|
23
|
+
root: string;
|
|
24
|
+
extractors: EngineExtractorSpec[];
|
|
25
|
+
/**
|
|
26
|
+
* Dispose every extractor once the heap passes this share of its limit, and
|
|
27
|
+
* build afresh on the next request. Storybook recycles at 0.7, and laziness
|
|
28
|
+
* alone did not cap its memory (storybookjs/storybook#35260).
|
|
29
|
+
*/
|
|
30
|
+
heapRatio?: number;
|
|
31
|
+
heapLimit?: number;
|
|
32
|
+
heapUsed?: () => number;
|
|
33
|
+
load?: (module: string) => Promise<DocgenExtractorModule>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* What a reader is shown of an extractor's failure: its first line, with the
|
|
37
|
+
* book's root made relative and any other absolute path cut to its file name.
|
|
38
|
+
* Node's `MODULE_NOT_FOUND` message carries a require stack of absolute paths,
|
|
39
|
+
* which a built book published under every component (#1382).
|
|
40
|
+
*/
|
|
41
|
+
export declare function describeError(error: unknown, root: string): string;
|
|
42
|
+
/** The extraction loop that runs inside the docgen worker, kept free of the thread. */
|
|
43
|
+
/** `import()` takes a URL, and an absolute Windows path is not one: `D:` reads as its scheme. */
|
|
44
|
+
export declare function importable(module: string): string;
|
|
45
|
+
export declare function createDocgenEngine(options: DocgenEngineOptions): {
|
|
46
|
+
stats: {
|
|
47
|
+
created: number;
|
|
48
|
+
recycled: number;
|
|
49
|
+
};
|
|
50
|
+
extract(requests: {
|
|
51
|
+
name: string;
|
|
52
|
+
file: string;
|
|
53
|
+
}[]): Promise<Record<string, ExtractResult>>;
|
|
54
|
+
/** Every live extractor's sources, for the dev server to watch. */
|
|
55
|
+
sources(): Promise<string[]>;
|
|
56
|
+
update(file: string): Promise<void>;
|
|
57
|
+
dispose: () => Promise<void>;
|
|
58
|
+
};
|