poveste 0.13.1 → 0.15.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 (80) hide show
  1. package/README.md +4 -2
  2. package/client.d.ts +9 -1
  3. package/dist/node/__tests__/dev-cleanup/poveste.config.d.ts +7 -0
  4. package/dist/node/__tests__/dev-cleanup/poveste.config.js +15 -0
  5. package/dist/node/alias.js +1 -1
  6. package/dist/node/bin.js +5 -5
  7. package/dist/node/build-counts.d.ts +56 -0
  8. package/dist/node/build-counts.js +83 -0
  9. package/dist/node/build-serialize.d.ts +3 -3
  10. package/dist/node/build.js +56 -28
  11. package/dist/node/builtin-plugins/vanilla-support/MountStory.js +9 -9
  12. package/dist/node/builtin-plugins/vanilla-support/RenderStory.d.ts +1 -1
  13. package/dist/node/builtin-plugins/vanilla-support/RenderStory.js +5 -5
  14. package/dist/node/builtin-plugins/vanilla-support/collect.js +2 -2
  15. package/dist/node/builtin-plugins/vanilla-support/types.d.ts +2 -2
  16. package/dist/node/builtin-plugins/vanilla-support/vanilla.story.js +4 -4
  17. package/dist/node/collect/index.js +41 -39
  18. package/dist/node/collect/module-server.d.ts +14 -0
  19. package/dist/node/collect/module-server.js +268 -0
  20. package/dist/node/collect/rpc.d.ts +6 -0
  21. package/dist/node/collect/rpc.js +51 -0
  22. package/dist/node/collect/runner.d.ts +4 -0
  23. package/dist/node/collect/runner.js +130 -0
  24. package/dist/node/collect/worker.js +22 -25
  25. package/dist/node/colors.d.ts +4 -4
  26. package/dist/node/colors.js +6 -6
  27. package/dist/node/commands/dev.d.ts +13 -1
  28. package/dist/node/commands/dev.js +106 -45
  29. package/dist/node/config.d.ts +2 -2
  30. package/dist/node/config.js +50 -20
  31. package/dist/node/context.d.ts +2 -2
  32. package/dist/node/context.js +9 -4
  33. package/dist/node/dom/env.js +11 -8
  34. package/dist/node/dom/util.js +1 -1
  35. package/dist/node/load.js +10 -13
  36. package/dist/node/markdown.d.ts +3 -4
  37. package/dist/node/markdown.js +85 -36
  38. package/dist/node/optimize-entries.js +2 -2
  39. package/dist/node/plugin.d.ts +13 -8
  40. package/dist/node/plugin.js +24 -7
  41. package/dist/node/preview.d.ts +3 -3
  42. package/dist/node/preview.js +2 -2
  43. package/dist/node/search.d.ts +8 -2
  44. package/dist/node/server.d.ts +9 -4
  45. package/dist/node/server.js +70 -30
  46. package/dist/node/stories.d.ts +5 -4
  47. package/dist/node/stories.js +10 -6
  48. package/dist/node/style-isolation/entry-css-merger.js +2 -3
  49. package/dist/node/style-isolation/global-styles.js +1 -0
  50. package/dist/node/style-isolation/scope-chrome.js +2 -2
  51. package/dist/node/tree.js +29 -21
  52. package/dist/node/util/find-up.d.ts +1 -1
  53. package/dist/node/util/managed-watches.d.ts +14 -0
  54. package/dist/node/util/managed-watches.js +39 -0
  55. package/dist/node/util/unregister.d.ts +2 -0
  56. package/dist/node/util/unregister.js +9 -0
  57. package/dist/node/util/vendors.js +1 -3
  58. package/dist/node/util/watch-ignore.js +3 -3
  59. package/dist/node/virtual/resolved-config.js +4 -5
  60. package/dist/node/virtual/resolved-generated-setup-code.d.ts +1 -1
  61. package/dist/node/virtual/resolved-generated-setup-code.js +1 -1
  62. package/dist/node/virtual/resolved-markdown-files.js +1 -1
  63. package/dist/node/virtual/resolved-support-plugins-client.js +1 -1
  64. package/dist/node/virtual/resolved-support-plugins-collect.js +1 -1
  65. package/dist/node/virtual/resolved-theme.js +8 -3
  66. package/dist/node/virtual/story-source.d.ts +1 -1
  67. package/dist/node/virtual/story-source.js +2 -1
  68. package/dist/node/virtual/story.d.ts +1 -1
  69. package/dist/node/virtual/story.js +1 -0
  70. package/dist/node/virtual/util.d.ts +1 -1
  71. package/dist/node/virtual/vite-plugin.js +2 -0
  72. package/dist/node/vite.d.ts +1 -1
  73. package/dist/node/vite.js +50 -12
  74. package/package.json +22 -24
  75. package/plugin.d.ts +11 -0
  76. package/plugin.js +5 -0
  77. package/dist/node/builtin-plugins/pinceau-tokens-style.d.ts +0 -1
  78. package/dist/node/builtin-plugins/pinceau-tokens-style.js +0 -85
  79. package/dist/node/builtin-plugins/pinceau-tokens.d.ts +0 -6
  80. package/dist/node/builtin-plugins/pinceau-tokens.js +0 -424
package/README.md CHANGED
@@ -20,6 +20,8 @@ pnpm i -D poveste @poveste/plugin-nuxt # Nuxt 4
20
20
  pnpm i -D poveste @poveste/plugin-quasar # Quasar
21
21
  ```
22
22
 
23
+ > **Installed an older version than you expected?** For about a day after a release, pnpm installs the previous one — `(x.y.z is available)`, no error. That is pnpm's release-age cooldown, not a broken publish. Use `npm`, wait it out, or pass `--config.minimum-release-age=0` (kebab-case — pnpm 12 silently ignores the camelCase spelling). Asking for the exact version does not help: pnpm 12 refuses one inside the window too.
24
+
23
25
  Then create a `poveste.config.ts` in your project root:
24
26
 
25
27
  ```ts
@@ -46,11 +48,11 @@ Run `npx poveste --help` for the full list of options.
46
48
  ## Requirements
47
49
 
48
50
  Node `^22.22.2 || ^24.15.0 || >=26.0.0` and Vite `^8.0.0`. See
49
- [supported versions](https://poveste.dev/guide/getting-started.html#supported-versions) for
51
+ [supported versions](https://poveste.dev/guide/getting-started#supported-versions) for
50
52
  what proves each range.
51
53
 
52
54
  ## Links
53
55
 
54
56
  - [Documentation](https://poveste.dev)
55
- - [Migrating from histoire](https://poveste.dev/guide/migration-from-histoire.html)
57
+ - [Migrating from histoire](https://poveste.dev/guide/migration-from-histoire)
56
58
  - [GitHub](https://github.com/poveste-dev/poveste)
package/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @deprecated
2
+ * @deprecated Use {@link logEvent} instead. This forwards to it and warns.
3
3
  */
4
4
  export function hstEvent(name: string, argument): void
5
5
 
@@ -15,5 +15,13 @@ export function logEvent(name: string, argument): void
15
15
  */
16
16
  export function isCollecting(): boolean
17
17
 
18
+ /**
19
+ * Switches the book between light and dark, or to `value` when one is given.
20
+ * Returns the mode it ended up in.
21
+ */
18
22
  export function toggleDark(value?: boolean): boolean
23
+
24
+ /**
25
+ * Whether the book is currently in dark mode.
26
+ */
19
27
  export function isDark(): boolean
@@ -0,0 +1,7 @@
1
+ declare const _default: {
2
+ plugins: {
3
+ name: string;
4
+ defaultConfig(): void;
5
+ }[];
6
+ };
7
+ export default _default;
@@ -0,0 +1,15 @@
1
+ import { appendFileSync } from 'node:fs';
2
+ import { onPovesteCleanup } from 'poveste';
3
+ const marker = process.env['POVESTE_CLEANUP_MARKER'] ?? '';
4
+ // Registers the way @poveste/plugin-nuxt does, while the config resolves, and
5
+ // leaves a line behind each time the cleanup runs.
6
+ // `.ts`, not `.js`: the restart specs edit this file, and a `.js` config can be
7
+ // served from Node's module cache, so the edit would never be read.
8
+ export default {
9
+ plugins: [{
10
+ name: 'cleanup-marker',
11
+ defaultConfig() {
12
+ onPovesteCleanup(() => appendFileSync(marker, 'ran\n'));
13
+ },
14
+ }],
15
+ };
@@ -1,5 +1,5 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import path from 'pathe';
3
3
  const require = createRequire(import.meta.url);
4
- export const APP_PATH = path.join(path.dirname(require.resolve('@poveste/app/package.json')), process.env.POVESTE_DEV ? 'src' : 'dist');
4
+ export const APP_PATH = path.join(path.dirname(require.resolve('@poveste/app/package.json')), process.env['POVESTE_DEV'] ? 'src' : 'dist');
5
5
  export const TEMP_PATH = path.join(process.cwd(), 'node_modules', '.poveste');
package/dist/node/bin.js CHANGED
@@ -4,9 +4,9 @@ import { dirname, resolve } from 'pathe';
4
4
  import sade from 'sade';
5
5
  const __dirname = dirname(fileURLToPath(import.meta.url));
6
6
  const { version } = JSON.parse(fs.readFileSync(resolve(__dirname, '../../package.json'), 'utf8'));
7
- process.env.NODE_ENV = process.argv.includes('build') || process.argv.includes('preview') ? 'production' : 'development';
8
- process.env.HISTOIRE = 'true';
9
- process.env.POVESTE = 'true';
7
+ process.env['NODE_ENV'] = process.argv.includes('build') || process.argv.includes('preview') ? 'production' : 'development';
8
+ process.env['HISTOIRE'] = 'true';
9
+ process.env['POVESTE'] = 'true';
10
10
  // sade does not await an action, so a rejected command was terminated only by
11
11
  // Node's default handling of the unhandled rejection it became. That is not the
12
12
  // CLI's to rely on: any dependency can install an `unhandledRejection` listener
@@ -15,7 +15,7 @@ process.env.POVESTE = 'true';
15
15
  //
16
16
  // So failure is reported here, the exit code set, and the process forced out if
17
17
  // it does not leave on its own. Closing the collection ports (#426) was not
18
- // enough on its own: a failed build still hangs in the nuxt4 and sveltekit books,
18
+ // enough on its own: a failed build still hangs in the nuxt and sveltekit books,
19
19
  // holding `FSEventWrap` and `FSReqCallback` handles — watchers nothing closes on
20
20
  // the error path. And `dev` and `preview` hold a listening server by design, so
21
21
  // for them no amount of leak-fixing would ever let the loop drain.
@@ -47,7 +47,7 @@ process.env.POVESTE = 'true';
47
47
  * that ends and one that does not.
48
48
  */
49
49
  function proveItDrains() {
50
- return !!process.env.POVESTE_NO_FORCE_EXIT;
50
+ return !!process.env['POVESTE_NO_FORCE_EXIT'];
51
51
  }
52
52
  function run(command) {
53
53
  return command().then(() => undefined, (error) => {
@@ -0,0 +1,56 @@
1
+ import type { ServerStoryFile } from '@poveste/shared';
2
+ export interface BuildCounts {
3
+ /** Story files that produced a story with variants. */
4
+ stories: number;
5
+ variants: number;
6
+ /** Standalone `.story.md` pages, which the book's home page calls Documents. */
7
+ docs: number;
8
+ /** Relative paths of the files that produced neither, so the warning can name them. */
9
+ empty: string[];
10
+ }
11
+ /**
12
+ * Splits the collected story files three ways, the way the book's own home page
13
+ * does: stories, documents, and files that produced nothing.
14
+ *
15
+ * "No variants" is not the question. A standalone `.story.md` is collected into a
16
+ * story with `docsOnly: true` and `variants: []` by construction (`markdown.ts`),
17
+ * so counting empty by variants alone reported every documentation page in the
18
+ * book as a problem — three of the four `examples/vue` warned about are
19
+ * `SHARED_STORY_TITLES` entries that a required spec fails if you delete them,
20
+ * which left the one real empty file hidden inside them (#670).
21
+ *
22
+ * Documents are counted rather than folded in with stories because `HomeView.vue`
23
+ * already makes exactly this split, and a build that reports 60 stories over a
24
+ * home page reading 57 Stories and 3 Documents disagrees with itself.
25
+ */
26
+ export declare function buildCounts(storyFiles: ServerStoryFile[]): BuildCounts;
27
+ /**
28
+ * Whether the build produced anything a reader can open.
29
+ *
30
+ * Stated once because two callers ask it: the message below, and the colour
31
+ * `build.ts` prints that message in. Reading it back off the message instead —
32
+ * one line means green — ties the colour to the shape of an unrelated decision,
33
+ * and a second success line would turn a good build yellow.
34
+ */
35
+ export declare function builtSomething(counts: BuildCounts): boolean;
36
+ /**
37
+ * What a finished build says it produced.
38
+ *
39
+ * A separate function because the interesting case is a claim rather than a
40
+ * number: `✅ Built 0 stories (0 variants)` is true of a working build in a
41
+ * project with nothing to build, and it is what a first-time reader gets from
42
+ * the first command the guide tells them to run (#624). Nothing in it says
43
+ * where the build looked, so there is no next step in it either.
44
+ *
45
+ * Nothing built is two states, not one, and the likelier is the second: a
46
+ * reader who followed the guide has a story file and has not put a `<Story>`
47
+ * in it yet. Those files matched, so "nothing matched" is false of them, and
48
+ * "write a story file" asks for the one they already wrote.
49
+ *
50
+ * Not an error. A book with no stories is a valid thing to build — scaffolding
51
+ * is exactly that state — so this changes what is said, not the exit code.
52
+ *
53
+ * Documents count as content: a book of `.story.md` pages with no variants has
54
+ * something in it, and telling its author nothing was found would be wrong.
55
+ */
56
+ export declare function buildSummary(counts: BuildCounts, seconds: number, storyMatch: string[]): string[];
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Splits the collected story files three ways, the way the book's own home page
3
+ * does: stories, documents, and files that produced nothing.
4
+ *
5
+ * "No variants" is not the question. A standalone `.story.md` is collected into a
6
+ * story with `docsOnly: true` and `variants: []` by construction (`markdown.ts`),
7
+ * so counting empty by variants alone reported every documentation page in the
8
+ * book as a problem — three of the four `examples/vue` warned about are
9
+ * `SHARED_STORY_TITLES` entries that a required spec fails if you delete them,
10
+ * which left the one real empty file hidden inside them (#670).
11
+ *
12
+ * Documents are counted rather than folded in with stories because `HomeView.vue`
13
+ * already makes exactly this split, and a build that reports 60 stories over a
14
+ * home page reading 57 Stories and 3 Documents disagrees with itself.
15
+ */
16
+ export function buildCounts(storyFiles) {
17
+ const counts = { stories: 0, variants: 0, docs: 0, empty: [] };
18
+ for (const file of storyFiles) {
19
+ if (file.story?.docsOnly) {
20
+ counts.docs++;
21
+ }
22
+ else if (file.story?.variants.length) {
23
+ counts.stories++;
24
+ counts.variants += file.story.variants.length;
25
+ }
26
+ else {
27
+ counts.empty.push(file.relativePath);
28
+ }
29
+ }
30
+ return counts;
31
+ }
32
+ /**
33
+ * Whether the build produced anything a reader can open.
34
+ *
35
+ * Stated once because two callers ask it: the message below, and the colour
36
+ * `build.ts` prints that message in. Reading it back off the message instead —
37
+ * one line means green — ties the colour to the shape of an unrelated decision,
38
+ * and a second success line would turn a good build yellow.
39
+ */
40
+ export function builtSomething(counts) {
41
+ return counts.stories > 0 || counts.docs > 0;
42
+ }
43
+ /**
44
+ * What a finished build says it produced.
45
+ *
46
+ * A separate function because the interesting case is a claim rather than a
47
+ * number: `✅ Built 0 stories (0 variants)` is true of a working build in a
48
+ * project with nothing to build, and it is what a first-time reader gets from
49
+ * the first command the guide tells them to run (#624). Nothing in it says
50
+ * where the build looked, so there is no next step in it either.
51
+ *
52
+ * Nothing built is two states, not one, and the likelier is the second: a
53
+ * reader who followed the guide has a story file and has not put a `<Story>`
54
+ * in it yet. Those files matched, so "nothing matched" is false of them, and
55
+ * "write a story file" asks for the one they already wrote.
56
+ *
57
+ * Not an error. A book with no stories is a valid thing to build — scaffolding
58
+ * is exactly that state — so this changes what is said, not the exit code.
59
+ *
60
+ * Documents count as content: a book of `.story.md` pages with no variants has
61
+ * something in it, and telling its author nothing was found would be wrong.
62
+ */
63
+ export function buildSummary(counts, seconds, storyMatch) {
64
+ const took = `in ${seconds}s`;
65
+ const globs = storyMatch.join(', ');
66
+ if (!builtSomething(counts)) {
67
+ if (counts.empty.length) {
68
+ const files = `${counts.empty.length} file${counts.empty.length === 1 ? '' : 's'}`;
69
+ return [
70
+ `Built 0 stories ${took} — ${files} matched ${globs} and produced no story`,
71
+ 'A story file needs a <Story> in it: https://poveste.dev/guide/',
72
+ ];
73
+ }
74
+ return [
75
+ `Built 0 stories ${took} — nothing matched ${globs}`,
76
+ 'Write a story file and run this again: https://poveste.dev/guide/',
77
+ ];
78
+ }
79
+ const stories = `${counts.stories} stor${counts.stories === 1 ? 'y' : 'ies'}`;
80
+ const variants = `${counts.variants} variant${counts.variants === 1 ? '' : 's'}`;
81
+ const docs = counts.docs ? ` and ${counts.docs} document${counts.docs === 1 ? '' : 's'}` : '';
82
+ return [`✅ Built ${stories} (${variants})${docs} ${took}`];
83
+ }
@@ -3,9 +3,9 @@ import type { Context } from './context.js';
3
3
  interface SerializedStory extends Omit<ServerStory, 'docsText'> {
4
4
  relativePath: string;
5
5
  supportPluginId: string;
6
- treePath?: string[];
7
- virtual?: boolean;
8
- markdownFile?: SerializedMarkdownFile;
6
+ treePath?: string[] | undefined;
7
+ virtual?: boolean | undefined;
8
+ markdownFile: SerializedMarkdownFile | null;
9
9
  }
10
10
  interface SerializedMarkdownFile {
11
11
  id: string;
@@ -5,6 +5,7 @@ import { join } from 'pathe';
5
5
  import pc from 'picocolors';
6
6
  import { createServer as createViteServer, mergeConfig as mergeViteConfig, build as viteBuild, } from 'vite';
7
7
  import { APP_PATH } from './alias.js';
8
+ import { buildCounts, buildSummary, builtSomething } from './build-counts.js';
8
9
  import { getSerializedStoryData } from './build-serialize.js';
9
10
  import { useCollectStories } from './collect/index.js';
10
11
  import { useModuleLoader } from './load.js';
@@ -57,6 +58,17 @@ export async function build(ctx) {
57
58
  // bundle built from it.
58
59
  mode: viteMode(ctx.mode),
59
60
  optimizeDeps: { include: [], noDiscovery: true },
61
+ plugins: [{
62
+ // Collection reads each file once, so this server needs no watcher, and
63
+ // one leaked from it: a transform still in flight when a failed build
64
+ // closes the server re-adds a file, Vite's chokidar reopens on `add`, and
65
+ // the native watcher that starts is never closed (#434). A `config` hook
66
+ // cannot say this, because merging a config skips `null`.
67
+ name: 'poveste:collect-without-watcher',
68
+ configResolved(config) {
69
+ Object.assign(config.server, { watch: null });
70
+ },
71
+ }],
60
72
  }));
61
73
  await server.pluginContainer.buildStart({});
62
74
  // Cleanup a plugin registers runs however this ends. `onBuildEnd` cannot be that
@@ -105,9 +117,7 @@ export async function build(ctx) {
105
117
  // Same wrapper the dev path uses for these exact calls (server.ts).
106
118
  await wrapLogError('destroyCollectStories', () => destroyCollectStories());
107
119
  }
108
- const storyCount = ctx.storyFiles.reduce((sum, file) => sum + (file.story?.variants.length ? 1 : 0), 0);
109
- const variantCount = ctx.storyFiles.reduce((sum, file) => sum + (file.story?.variants.length ?? 0), 0);
110
- const emptyStoryCount = ctx.storyFiles.length - storyCount;
120
+ const counts = buildCounts(ctx.storyFiles);
111
121
  const { viteConfig: buildViteConfigRaw } = await getViteConfigWithPlugins(false, ctx);
112
122
  const buildViteConfig = mergeViteConfig(buildViteConfigRaw, {
113
123
  // Vite picks the `.env` file from `mode`, not from NODE_ENV — this said
@@ -115,7 +125,7 @@ export async function build(ctx) {
115
125
  mode: viteMode(ctx.mode),
116
126
  build: {
117
127
  lib: false,
118
- rollupOptions: {
128
+ rolldownOptions: {
119
129
  // Named, not an array. With an array the bundler derives `[name]` from the
120
130
  // path, and `APP_PATH` is absolute — so any framework whose `entryFileNames`
121
131
  // refuses a path-shaped `[name]` fails the build outright (#369, and
@@ -137,14 +147,15 @@ export async function build(ctx) {
137
147
  },
138
148
  },
139
149
  });
150
+ const buildPlugins = buildViteConfig.plugins ??= [];
140
151
  // For @vite/plugin-vue: Always put our vite server
141
152
  // Disable template inlining
142
153
  // (so that we no longer need defineExpose)
143
154
  // Nuxt: replaces the Nuxt vite dev server
144
- buildViteConfig.plugins.push({
155
+ buildPlugins.push({
145
156
  name: 'poveste-vue-plugin-override',
146
157
  config(config) {
147
- const vuePlugin = config.plugins.find((p) => p.name === 'vite:vue');
158
+ const vuePlugin = config.plugins?.find((p) => p.name === 'vite:vue');
148
159
  if (vuePlugin) {
149
160
  // @ts-expect-error vue plugin use function form
150
161
  const original = vuePlugin.configureServer.bind(vuePlugin);
@@ -165,14 +176,16 @@ export async function build(ctx) {
165
176
  }
166
177
  },
167
178
  });
168
- buildViteConfig.plugins.push({
179
+ buildPlugins.push({
169
180
  name: 'poveste-build-config-override',
170
181
  enforce: 'post',
171
182
  config(config) {
183
+ const build = config.build ??= {};
184
+ const rolldownOptions = build.rolldownOptions ??= {};
172
185
  // Don't externalize
173
- config.build.rollupOptions.external = [];
186
+ rolldownOptions.external = [];
174
187
  // Force chunk strategy
175
- config.build.rollupOptions.output = {
188
+ rolldownOptions.output = {
176
189
  manualChunks(id) {
177
190
  // Vite's runtime helpers (`\0vite/preload-helper.js` and friends) are
178
191
  // virtual modules, so the node_modules routing below never sees them
@@ -197,6 +210,7 @@ export async function build(ctx) {
197
210
  }
198
211
  return 'vendor';
199
212
  }
213
+ return undefined;
200
214
  },
201
215
  };
202
216
  // A framework that declares Vite environments gives each one its own
@@ -205,72 +219,86 @@ export async function build(ctx) {
205
219
  // while poveste kept looking in `outDir` (#369). Only the client environment
206
220
  // builds the book. Where no framework declared environments there is nothing to
207
221
  // override, and where one did this puts the book back where poveste reads it.
208
- if (config.environments?.client?.build) {
209
- config.environments.client.build.outDir = ctx.config.outDir;
222
+ if (config.environments?.['client']?.build) {
223
+ config.environments['client'].build.outDir = ctx.config.outDir;
210
224
  }
211
225
  // Force vite build options
212
- Object.assign(config.build, {
226
+ Object.assign(build, {
213
227
  outDir: ctx.config.outDir,
214
228
  emptyOutDir: true,
215
229
  // Re-merged per-entry by entry-css-merger.
216
230
  cssCodeSplit: true,
217
231
  // Not an oversight: the source pane prints story code by reading
218
232
  // functions back at runtime, and `Function.prototype.toString()`
219
- // returns whatever the minifier left — `codegen-vue3.spec.ts` fails
233
+ // returns whatever the minifier left — `codegen-vue.spec.ts` fails
220
234
  // on three cases with this on (#329).
221
235
  minify: false,
222
236
  // Don't build in SSR mode
223
237
  ssr: false,
224
238
  });
225
- config.define.__HST_COLLECT__ = false;
239
+ config.define = { ...config.define, __HST_COLLECT__: false };
226
240
  },
227
241
  });
228
242
  const isolate = ctx.config.isolateStyles !== false;
229
- buildViteConfig.plugins.push(userCssScopePlugin({ enabled: isolate }));
230
- buildViteConfig.plugins.push(chromeCssScopePlugin({ enabled: isolate }));
231
- buildViteConfig.plugins.push(entryCssMergerPlugin({ isolateStyles: isolate }));
243
+ buildPlugins.push(userCssScopePlugin({ enabled: isolate }));
244
+ buildPlugins.push(chromeCssScopePlugin({ enabled: isolate }));
245
+ buildPlugins.push(entryCssMergerPlugin({ isolateStyles: isolate }));
232
246
  for (const cb of changeViteConfigCallbacks) {
233
- console.log('vite config hook', cb);
234
247
  await cb(buildViteConfig);
235
248
  }
236
249
  const results = await viteBuild(buildViteConfig);
237
250
  const result = Array.isArray(results) ? results[0] : results;
238
- function findEntryCss(entryName) {
239
- return result.output.find(o => o.type === 'asset' && o.fileName === `${entryName}.css`);
251
+ if (!result) {
252
+ throw new Error('[poveste] the book build produced no output');
240
253
  }
254
+ const findEntryCss = (entryName) => result.output.find(o => o.type === 'asset' && o.fileName === `${entryName}.css`);
241
255
  const mainStyleOutput = findEntryCss('bundle-main')
242
- ?? result.output.find(o => o.name === 'style.css' && o.type === 'asset');
256
+ ?? result.output.find(o => o.type === 'asset' && o.names.includes('style.css'));
243
257
  const sandboxStyleOutput = findEntryCss('bundle-sandbox') ?? mainStyleOutput;
244
258
  // Preload
245
- const preloadOutputs = result.output.filter(o => PRELOAD_MODULES.includes(o.name) && o.type === 'chunk');
259
+ const preloadOutputs = result.output.filter(o => o.type === 'chunk' && PRELOAD_MODULES.includes(o.name));
246
260
  const preloadHtml = generateScriptLinks(preloadOutputs.map(o => o.fileName), 'preload', ctx);
247
261
  // Prefetch
248
- const prefetchOutputs = result.output.filter(o => PREFETCHED_MODULES.includes(o.name) && o.type === 'chunk');
262
+ const prefetchOutputs = result.output.filter(o => o.type === 'chunk' && PREFETCHED_MODULES.includes(o.name));
249
263
  const prefetchHtml = generateScriptLinks(prefetchOutputs.map(o => o.fileName), 'prefetch', ctx);
250
264
  // Index
251
- const indexOutput = result.output.find(o => o.name === 'bundle-main' && o.type === 'chunk');
265
+ const indexOutput = result.output.find(o => o.type === 'chunk' && o.name === 'bundle-main');
266
+ const sandboxOutput = result.output.find(o => o.type === 'chunk' && o.name === 'bundle-sandbox');
267
+ if (!indexOutput || !sandboxOutput || !mainStyleOutput || !sandboxStyleOutput) {
268
+ const missing = { 'bundle-main.js': indexOutput, 'bundle-sandbox.js': sandboxOutput, 'bundle-main.css': mainStyleOutput };
269
+ throw new Error(`[poveste] the book build produced no ${Object.entries(missing).filter(([, output]) => !output).map(([name]) => name).join(', ')}`);
270
+ }
252
271
  let indexHtml = generateEntryHtml(indexOutput.fileName, mainStyleOutput.fileName, {
253
272
  HEAD: `${preloadHtml}${prefetchHtml}`,
254
273
  }, ctx);
255
274
  indexHtml = await applyHeadTransform(indexHtml, ctx.config.head);
256
275
  await writeFile('index.html', indexHtml, ctx);
257
276
  // Sandbox
258
- const sandboxOutput = result.output.find(o => o.name === 'bundle-sandbox' && o.type === 'chunk');
259
277
  let sandboxHtml = generateEntryHtml(sandboxOutput.fileName, sandboxStyleOutput.fileName, {}, ctx);
260
278
  sandboxHtml = await applyHeadTransform(sandboxHtml, ctx.config.head);
261
279
  await writeFile('__sandbox.html', sandboxHtml, ctx);
262
280
  await writeFile('poveste.json', JSON.stringify(getSerializedStoryData(ctx), null, 2), ctx);
263
281
  const duration = performance.now() - startTime;
264
- if (emptyStoryCount) {
265
- console.warn(pc.yellow(`⚠️ ${emptyStoryCount} empty story file${emptyStoryCount === 1 ? '' : 's'}`));
282
+ const { empty } = counts;
283
+ if (empty.length) {
284
+ console.warn(pc.yellow(`⚠️ ${empty.length} empty story file${empty.length === 1 ? '' : 's'}: ${empty.join(', ')}`));
285
+ }
286
+ // Green only when something was built. A tick over nothing is the claim
287
+ // #624 is about, and it is the first command the guide asks for.
288
+ const paint = builtSomething(counts) ? pc.green : pc.yellow;
289
+ for (const line of buildSummary(counts, Math.round(duration / 1000 * 100) / 100, ctx.config.storyMatch)) {
290
+ console.log(paint(line));
266
291
  }
267
- console.log(pc.green(`✅ Built ${storyCount} stor${storyCount === 1 ? 'y' : 'ies'} (${variantCount} variant${variantCount === 1 ? '' : 's'}) in ${Math.round(duration / 1000 * 100) / 100}s`));
268
292
  // Render
269
293
  if (previewStoryCallbacks.length) {
270
294
  const { baseUrl, close } = await startPreview({}, ctx);
271
295
  try {
272
296
  for (const storyFile of ctx.storyFiles) {
273
297
  const story = storyFile.story;
298
+ // An empty story file collects no story, and there is nothing to render.
299
+ if (!story) {
300
+ continue;
301
+ }
274
302
  for (const variant of story.variants) {
275
303
  const query = new URLSearchParams();
276
304
  query.append('storyId', story.id);
@@ -8,7 +8,7 @@ export default _defineComponent({
8
8
  },
9
9
  },
10
10
  setup(props) {
11
- const options = props.story.file.component;
11
+ const options = props.story.file?.component;
12
12
  let rawVariants = [];
13
13
  if (options.onMount) {
14
14
  rawVariants = [{
@@ -19,16 +19,16 @@ export default _defineComponent({
19
19
  }];
20
20
  }
21
21
  else {
22
- rawVariants = options.variants;
22
+ rawVariants = options.variants ?? [];
23
23
  }
24
- for (const index in props.story.variants) {
24
+ for (const [index, variant] of props.story.variants.entries()) {
25
25
  const rawVariant = rawVariants[index];
26
- Object.assign(props.story.variants[index], {
27
- slots: () => ({ default: rawVariant.onMount, controls: rawVariant.onMountControls }),
28
- source: rawVariant.source ?? options.source,
29
- responsiveDisabled: rawVariant.responsiveDisabled ?? options.responsiveDisabled,
30
- autoPropsDisabled: rawVariant.autoPropsDisabled ?? options.autoPropsDisabled,
31
- setupApp: rawVariant.setupApp ?? options.setupApp,
26
+ Object.assign(variant, {
27
+ slots: () => ({ default: rawVariant?.onMount, controls: rawVariant?.onMountControls }),
28
+ source: rawVariant?.source ?? options.source,
29
+ responsiveDisabled: rawVariant?.responsiveDisabled ?? options.responsiveDisabled,
30
+ autoPropsDisabled: rawVariant?.autoPropsDisabled ?? options.autoPropsDisabled,
31
+ setupApp: rawVariant?.setupApp ?? options.setupApp,
32
32
  configReady: true,
33
33
  });
34
34
  }
@@ -14,7 +14,7 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
14
14
  default: string;
15
15
  };
16
16
  }>, {
17
- sandbox: import("vue").Ref<HTMLDivElement, HTMLDivElement>;
17
+ sandbox: import("vue").Ref<HTMLDivElement | undefined, HTMLDivElement | undefined>;
18
18
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
19
19
  ready: () => true;
20
20
  }, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
@@ -26,7 +26,7 @@ export default _defineComponent({
26
26
  setup(props, { emit }) {
27
27
  const sandbox = _ref();
28
28
  let mounting = false;
29
- let app;
29
+ let app = null;
30
30
  let appHooks;
31
31
  async function unmountVariant() {
32
32
  if (app) {
@@ -36,7 +36,7 @@ export default _defineComponent({
36
36
  await hook();
37
37
  }
38
38
  }
39
- app.el.parentNode.removeChild(app.el);
39
+ app.el.remove();
40
40
  app = null;
41
41
  }
42
42
  }
@@ -86,9 +86,9 @@ export default _defineComponent({
86
86
  appHooks.onUnmount.push(cb);
87
87
  },
88
88
  };
89
- const onMount = props.variant.slots()[props.slotName];
90
- await onMount(api);
91
- sandbox.value.appendChild(app.el);
89
+ const onMount = Reflect.get(props.variant.slots?.() ?? {}, props.slotName);
90
+ await onMount?.(api);
91
+ sandbox.value?.appendChild(app.el);
92
92
  emit('ready');
93
93
  }
94
94
  _onMounted(async () => {
@@ -10,7 +10,7 @@ export async function run({ file, storyData }) {
10
10
  }];
11
11
  }
12
12
  else {
13
- rawVariants = options.variants;
13
+ rawVariants = options.variants ?? [];
14
14
  }
15
15
  const story = {
16
16
  id: options.id ?? file.id,
@@ -20,7 +20,7 @@ export async function run({ file, storyData }) {
20
20
  icon: options.icon,
21
21
  iconColor: options.iconColor,
22
22
  docsOnly: options.docsOnly ?? false,
23
- variants: null,
23
+ variants: [],
24
24
  };
25
25
  const variants = rawVariants.map((v, index) => ({
26
26
  id: v.id ?? `${story.id}-${index}`,
@@ -12,8 +12,8 @@ export interface MountApi {
12
12
  onUnmount: (cb: () => unknown) => Awaitable<void>;
13
13
  }
14
14
  export interface VanillaApi {
15
- onMount?: (api: MountApi) => Awaitable<void>;
16
- onMountControls?: (api: MountApi) => Awaitable<void>;
15
+ onMount?: ((api: MountApi) => Awaitable<void>) | undefined;
16
+ onMountControls?: ((api: MountApi) => Awaitable<void>) | undefined;
17
17
  }
18
18
  export type VanillaStorySetupHandler = (payload: {
19
19
  app: App;
@@ -5,20 +5,20 @@ export default {
5
5
  {
6
6
  title: 'Hello',
7
7
  onMount: ({ el, state, onUpdate }) => {
8
- state.count = 0;
8
+ state['count'] = 0;
9
9
  const div = document.createElement('div');
10
10
  div.innerHTML = '<h1>Hello World</h1>';
11
11
  el.appendChild(div);
12
12
  const counter = document.createElement('pre');
13
- counter.innerHTML = `Counter: ${state.counter}`;
13
+ counter.innerHTML = `Counter: ${state['counter']}`;
14
14
  div.appendChild(counter);
15
15
  onUpdate(() => {
16
- counter.innerHTML = `Counter: ${state.count}`;
16
+ counter.innerHTML = `Counter: ${state['count']}`;
17
17
  });
18
18
  },
19
19
  onMountControls: ({ el, state, onUnmount }) => {
20
20
  const add = () => {
21
- state.count++;
21
+ state['count']++;
22
22
  };
23
23
  Object.assign(el.style, {
24
24
  padding: '8px',