poveste 0.20.0 → 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.
Files changed (51) hide show
  1. package/README.md +14 -7
  2. package/dist/node/build.js +6 -2
  3. package/dist/node/collect/runner.d.ts +15 -4
  4. package/dist/node/collect/runner.js +52 -5
  5. package/dist/node/collect/worker.js +2 -1
  6. package/dist/node/docgen/build.d.ts +2 -2
  7. package/dist/node/docgen/build.js +7 -4
  8. package/dist/node/docgen/components-of.d.ts +17 -5
  9. package/dist/node/docgen/components-of.js +27 -7
  10. package/dist/node/docgen/engine.d.ts +8 -0
  11. package/dist/node/docgen/engine.js +3 -2
  12. package/dist/node/docgen/jsx/extractor.d.ts +8 -0
  13. package/dist/node/docgen/jsx/extractor.js +79 -0
  14. package/dist/node/docgen/jsx/walk.d.ts +20 -0
  15. package/dist/node/docgen/jsx/walk.js +187 -0
  16. package/dist/node/docgen/protocol.d.ts +2 -0
  17. package/dist/node/docgen/public.d.ts +5 -0
  18. package/dist/node/docgen/public.js +3 -0
  19. package/dist/node/docgen/reexports.d.ts +27 -0
  20. package/dist/node/docgen/reexports.js +187 -0
  21. package/dist/node/docgen/service.d.ts +16 -3
  22. package/dist/node/docgen/service.js +87 -8
  23. package/dist/node/docgen/typed.d.ts +51 -0
  24. package/dist/node/docgen/typed.js +130 -0
  25. package/dist/node/docgen/typescript.d.ts +14 -0
  26. package/dist/node/docgen/typescript.js +81 -0
  27. package/dist/node/histoire-imports.d.ts +10 -0
  28. package/dist/node/histoire-imports.js +52 -0
  29. package/dist/node/server.js +8 -5
  30. package/dist/node/stories.d.ts +15 -1
  31. package/dist/node/stories.js +53 -7
  32. package/dist/node/style-isolation/consumer-postcss.d.ts +18 -0
  33. package/dist/node/style-isolation/consumer-postcss.js +54 -0
  34. package/dist/node/style-isolation/entry-css-merger.d.ts +12 -0
  35. package/dist/node/style-isolation/entry-css-merger.js +23 -2
  36. package/dist/node/vendors.d.ts +21 -9
  37. package/dist/node/vendors.js +32 -12
  38. package/dist/node/virtual/component-docs.d.ts +9 -0
  39. package/dist/node/virtual/component-docs.js +33 -0
  40. package/dist/node/virtual/index.d.ts +12 -11
  41. package/dist/node/virtual/index.js +8 -6
  42. package/dist/node/virtual/resolved-config.d.ts +0 -24
  43. package/dist/node/virtual/resolved-config.js +1 -29
  44. package/dist/node/virtual/resolved-stories.js +7 -5
  45. package/dist/node/virtual/story-icons.d.ts +20 -0
  46. package/dist/node/virtual/story-icons.js +77 -0
  47. package/dist/node/virtual/vite-plugin.js +7 -1
  48. package/dist/node/vite.js +23 -3
  49. package/docgen.d.ts +1 -0
  50. package/docgen.js +1 -0
  51. package/package.json +19 -9
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 histoire does not support. Solid and React
9
- are supported as renderers only: their stories render, and they have no controls
10
- panel.
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, renderer only
24
- pnpm i -D poveste @poveste/plugin-react # React 19, renderer only
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 and no hint, and for a plugin that's new in that release, its empty `0.0.1` placeholder. 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 ignores the camelCase spelling), or with exact versions.
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, use 24.15 or later: Poveste's DOM dependency (jsdom) accepts `^22.22.2 || ^24.15.0 || >=26`.
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
 
@@ -124,7 +124,7 @@ export async function build(ctx) {
124
124
  // Same wrapper the dev path uses for these exact calls (server.ts).
125
125
  await wrapLogError('destroyCollectStories', () => destroyCollectStories());
126
126
  }
127
- ctx.componentDocs = await extractComponentDocs(ctx, server.moduleGraph);
127
+ ctx.componentDocs = await extractComponentDocs(ctx, server);
128
128
  const counts = buildCounts(ctx.storyFiles);
129
129
  const { viteConfig: buildViteConfigRaw } = await getViteConfigWithPlugins(false, ctx);
130
130
  const buildViteConfig = mergeViteConfig(buildViteConfigRaw, {
@@ -241,6 +241,10 @@ export async function build(ctx) {
241
241
  // returns whatever the minifier left — `codegen-vue.spec.ts` fails
242
242
  // on three cases with this on (#329).
243
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,
244
248
  // Don't build in SSR mode
245
249
  ssr: false,
246
250
  });
@@ -250,7 +254,7 @@ export async function build(ctx) {
250
254
  const isolate = ctx.config.isolateStyles !== false;
251
255
  buildPlugins.push(userCssScopePlugin({ enabled: isolate }));
252
256
  buildPlugins.push(chromeCssScopePlugin({ enabled: isolate }));
253
- buildPlugins.push(entryCssMergerPlugin({ isolateStyles: isolate }));
257
+ buildPlugins.push(entryCssMergerPlugin({ isolateStyles: isolate, minify: true }));
254
258
  for (const cb of changeViteConfigCallbacks) {
255
259
  await cb(buildViteConfig);
256
260
  }
@@ -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 an externalised module added to `globalThis` when Node ran it. Node
35
- * caches that module, so once a worker's reset deletes them nothing installs them
36
- * again. Vue's development build sets `__VUE_HMR_RUNTIME__` this way, and a story
37
- * compiled with HMR code reads it at its top level (#1320).
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 an externalised module added to `globalThis` when Node ran it. Node
72
- * caches that module, so once a worker's reset deletes them nothing installs them
73
- * again. Vue's development build sets `__VUE_HMR_RUNTIME__` this way, and a story
74
- * compiled with HMR code reads it at its top level (#1320).
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], createRequire(filename), cjsExports, moduleProxy, filename, dirname(filename));
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);
@@ -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();
@@ -63,6 +63,7 @@ if (!parentPort) {
63
63
  // The port it answers over is the one tasks arrive on, so nothing is handed over
64
64
  // per story.
65
65
  const _invoke = invokeOver(parentPort);
66
+ stubStylesheetsForNode();
66
67
  serveTasks(parentPort, { invalidate, invalidateAll, collect });
67
68
  async function collect(payload) {
68
69
  const startTime = performance.now();
@@ -1,8 +1,8 @@
1
- import type { ModuleGraph } from 'vite';
1
+ import type { ViteDevServer } from 'vite';
2
2
  import type { Context } from '../context.js';
3
3
  import type { StoryDocsResult } from './protocol.js';
4
4
  /**
5
5
  * The build's pass (#1159): with no reader to wait for, every story's named
6
6
  * components are extracted once, after collection, into what the book ships.
7
7
  */
8
- export declare function extractComponentDocs(ctx: Context, moduleGraph: ModuleGraph): Promise<Record<string, StoryDocsResult['components']>>;
8
+ export declare function extractComponentDocs(ctx: Context, server: Pick<ViteDevServer, 'moduleGraph' | 'pluginContainer'>): Promise<Record<string, StoryDocsResult['components']>>;
@@ -1,17 +1,18 @@
1
- import { componentsOfStory } from './components-of.js';
1
+ import { componentsOfStory, storyOf } from './components-of.js';
2
2
  import { createDocgenService } from './service.js';
3
3
  /**
4
4
  * The build's pass (#1159): with no reader to wait for, every story's named
5
5
  * components are extracted once, after collection, into what the book ships.
6
6
  */
7
- export async function extractComponentDocs(ctx, moduleGraph) {
7
+ export async function extractComponentDocs(ctx, server) {
8
8
  const docgen = createDocgenService({
9
9
  root: ctx.root,
10
10
  plugins: ctx.config.plugins,
11
11
  enabled: ctx.config.autoDocs !== false,
12
12
  bookOptions: typeof ctx.config.autoDocs === 'object' ? ctx.config.autoDocs : undefined,
13
13
  collected: Promise.resolve(),
14
- componentsOf: storyId => componentsOfStory(ctx.storyFiles, moduleGraph, storyId),
14
+ storyOf: storyId => storyOf(ctx.storyFiles, storyId),
15
+ componentsOf: storyId => componentsOfStory(ctx.storyFiles, server, storyId),
15
16
  });
16
17
  if (!docgen.enabled) {
17
18
  return {};
@@ -27,8 +28,10 @@ export async function extractComponentDocs(ctx, moduleGraph) {
27
28
  const { components } = await docgen.request(storyId);
28
29
  // A built book is published, so an extractor's error stays on the build
29
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.
30
33
  const read = Object.entries(components).filter(([file, result]) => {
31
- if (!result.doc) {
34
+ if (!result.doc && result.error !== undefined) {
32
35
  failed.set(file, result.error);
33
36
  }
34
37
  return result.doc;
@@ -1,7 +1,19 @@
1
- import type { ServerStoryFile } from '@poveste/shared';
2
- import type { ModuleGraph } from 'vite';
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
+ }
3
14
  /**
4
- * The files a story's own module imports, as collection resolved them: the
5
- * components that story names. Other story files are left out.
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.
6
18
  */
7
- export declare function componentsOfStory(storyFiles: ServerStoryFile[], moduleGraph: ModuleGraph, storyId: string): string[];
19
+ export declare function componentsOfStory(storyFiles: ServerStoryFile[], server: Pick<ViteDevServer, 'moduleGraph' | 'pluginContainer'>, storyId: string): Promise<StoryComponents>;
@@ -1,17 +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
+ }
1
9
  /**
2
- * The files a story's own module imports, as collection resolved them: the
3
- * components that story names. Other story files are left out.
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.
4
13
  */
5
- export function componentsOfStory(storyFiles, moduleGraph, storyId) {
6
- const storyFile = storyFiles.find(file => file.story?.id === storyId);
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
+ }
7
27
  const storyPaths = new Set(storyFiles.map(file => file.path));
8
- const files = new Set();
9
- for (const mod of moduleGraph.getModulesByFile(storyFile?.path ?? '') ?? []) {
28
+ const files = new Set(traced.files);
29
+ for (const mod of server.moduleGraph.getModulesByFile(storyFile) ?? []) {
10
30
  for (const imported of mod.importedModules) {
11
31
  if (imported.file && !storyPaths.has(imported.file)) {
12
32
  files.add(imported.file);
13
33
  }
14
34
  }
15
35
  }
16
- return [...files];
36
+ return { files: [...files], unresolved: traced.unresolved };
17
37
  }
@@ -5,11 +5,19 @@ export interface EngineExtractorSpec {
5
5
  module: string;
6
6
  options?: unknown;
7
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
+ */
8
12
  export type ExtractResult = {
9
13
  doc: ComponentDoc;
10
14
  } | {
11
15
  doc?: undefined;
12
16
  error: string;
17
+ name?: string;
18
+ } | {
19
+ doc?: undefined;
20
+ error?: undefined;
13
21
  };
14
22
  export interface DocgenEngineOptions {
15
23
  root: string;
@@ -61,10 +61,11 @@ export function createDocgenEngine(options) {
61
61
  try {
62
62
  const extractor = await extractorFor(name);
63
63
  const doc = await extractor.extract(file);
64
- results[file] = doc ? { doc } : { error: 'no component found' };
64
+ results[file] = doc ? { doc } : {};
65
65
  }
66
66
  catch (error) {
67
- results[file] = { error: describeError(error, options.root) };
67
+ const component = error?.component;
68
+ results[file] = { error: describeError(error, options.root), ...typeof component === 'string' ? { name: component } : {} };
68
69
  }
69
70
  await recycleIfNeeded();
70
71
  }
@@ -0,0 +1,8 @@
1
+ import type { DocgenExtractor, DocgenExtractorContext } from '@poveste/shared';
2
+ import type { JsxDialect } from './walk.js';
3
+ /**
4
+ * The JSX frameworks' extractor, run in the docgen worker: one language service
5
+ * over the book's tsconfig, read through a dialect. A plugin's extractor module
6
+ * is this with its dialect bound.
7
+ */
8
+ export declare function createJsxExtractor(context: DocgenExtractorContext, dialect: JsxDialect): DocgenExtractor;
@@ -0,0 +1,79 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { ambientFiles, loadFromBook, readTsconfig } from '../typed.js';
3
+ import { documentStory } from './walk.js';
4
+ function compilerOptions(typescript, parsed, dialect) {
5
+ return {
6
+ ...parsed
7
+ ? parsed.options
8
+ : {
9
+ jsx: typescript.JsxEmit.Preserve,
10
+ jsxImportSource: dialect.jsxImportSource,
11
+ module: typescript.ModuleKind.ESNext,
12
+ moduleResolution: typescript.ModuleResolutionKind.Bundler,
13
+ target: typescript.ScriptTarget.ESNext,
14
+ strict: true,
15
+ skipLibCheck: true,
16
+ },
17
+ // A `.story.jsx` is read whatever the book's tsconfig says, and nothing is emitted.
18
+ allowJs: true,
19
+ noEmit: true,
20
+ };
21
+ }
22
+ /**
23
+ * The JSX frameworks' extractor, run in the docgen worker: one language service
24
+ * over the book's tsconfig, read through a dialect. A plugin's extractor module
25
+ * is this with its dialect bound.
26
+ */
27
+ export function createJsxExtractor(context, dialect) {
28
+ const typescript = loadFromBook(context.root, 'typescript', 'reads JSX components\' types');
29
+ const options = (context.options ?? {});
30
+ const parsed = readTsconfig(typescript, context.root);
31
+ const settings = compilerOptions(typescript, parsed, dialect);
32
+ // The requested stories and the book's declaration files, not every source it includes:
33
+ // the program grows with the components asked for, not with the book.
34
+ const files = new Set(ambientFiles(parsed));
35
+ const versions = new Map();
36
+ let projectVersion = 0;
37
+ const service = typescript.createLanguageService({
38
+ getProjectVersion: () => String(projectVersion),
39
+ getCompilationSettings: () => settings,
40
+ getScriptFileNames: () => [...files],
41
+ getScriptVersion: file => String(versions.get(file) ?? 0),
42
+ getScriptSnapshot(file) {
43
+ const text = typescript.sys.readFile(file);
44
+ return text === undefined ? undefined : typescript.ScriptSnapshot.fromString(text);
45
+ },
46
+ getCurrentDirectory: () => context.root,
47
+ getDefaultLibFileName: compilerOptions => typescript.getDefaultLibFilePath(compilerOptions),
48
+ fileExists: typescript.sys.fileExists,
49
+ readFile: typescript.sys.readFile,
50
+ readDirectory: typescript.sys.readDirectory,
51
+ directoryExists: typescript.sys.directoryExists,
52
+ getDirectories: typescript.sys.getDirectories,
53
+ });
54
+ function program() {
55
+ const current = service.getProgram();
56
+ if (!current) {
57
+ throw new Error('TypeScript built no program for the book');
58
+ }
59
+ return current;
60
+ }
61
+ return {
62
+ extract(file) {
63
+ if (!files.has(file)) {
64
+ files.add(file);
65
+ projectVersion++;
66
+ }
67
+ return documentStory(typescript, program(), file, dialect, options);
68
+ },
69
+ update(file) {
70
+ if (!existsSync(file)) {
71
+ files.delete(file);
72
+ }
73
+ versions.set(file, (versions.get(file) ?? 0) + 1);
74
+ projectVersion++;
75
+ },
76
+ sources: () => service.getProgram()?.getSourceFiles().map(sourceFile => sourceFile.fileName).filter(file => !file.includes('/node_modules/')) ?? [],
77
+ dispose: () => service.dispose(),
78
+ };
79
+ }
@@ -0,0 +1,20 @@
1
+ import type { ComponentDoc } from '@poveste/shared';
2
+ import type ts from 'typescript';
3
+ import type { DocgenOptions } from '../typed.js';
4
+ /** What differs between JSX frameworks, as data, so Solid and React share one walker (#1110, #1161). */
5
+ export interface JsxDialect {
6
+ /** Where the compiler finds the JSX types when the book has no tsconfig: `solid-js`, `react`. */
7
+ jsxImportSource: string;
8
+ /** Calls that merge an object of defaults into the props parameter: `mergeProps`. */
9
+ defaultCalls: string[];
10
+ /** A prop whose printed type matches is a slot: `JSX.Element`. */
11
+ slotType: RegExp;
12
+ /** The framework's types that give a component `children`: `ParentProps`, `PropsWithChildren`. */
13
+ childrenTypes: string[];
14
+ }
15
+ /**
16
+ * The component a story file's default export names in its `component` field,
17
+ * documented from the type of its first parameter. Nothing when the story names none,
18
+ * or names one `exclude` leaves out.
19
+ */
20
+ export declare function documentStory(typescript: typeof ts, program: ts.Program, file: string, dialect: JsxDialect, options?: DocgenOptions): ComponentDoc | undefined;