poveste 0.17.2 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,7 +20,7 @@ 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.
23
+ > **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.
24
24
 
25
25
  Then create a `poveste.config.ts` in your project root:
26
26
 
@@ -47,10 +47,12 @@ Run `npx poveste --help` for the full list of options.
47
47
 
48
48
  ## Requirements
49
49
 
50
- Node `>=24.15.0` and Vite `^8.0.0`. See
50
+ Node `>=22.22.2` and Vite `^8.0.0`. See
51
51
  [supported versions](https://poveste.dev/guide/getting-started#supported-versions) for
52
52
  what proves each range.
53
53
 
54
+ On Node 24, use 24.15 or later: Poveste's DOM dependency (jsdom) accepts `^22.22.2 || ^24.15.0 || >=26`.
55
+
54
56
  ## Links
55
57
 
56
58
  - [Documentation](https://poveste.dev)
@@ -1,3 +1,4 @@
1
+ import { isError } from '../util/is-error.js';
1
2
  /*
2
3
  * Structured clone keeps an `Error`'s `name`, `message` and `stack` and drops every
3
4
  * own property beyond them, so the code frame a Vite transform error puts on `frame`
@@ -19,7 +20,7 @@ function isSerialized(value) {
19
20
  * story may legitimately throw, and it should arrive as the data it is.
20
21
  */
21
22
  function errorLike(value) {
22
- if (Error.isError(value)) {
23
+ if (isError(value)) {
23
24
  return true;
24
25
  }
25
26
  if (typeof value !== 'object' || value === null) {
@@ -3,6 +3,7 @@ import path, { relative } from 'pathe';
3
3
  import pc from 'picocolors';
4
4
  import { TEMP_PATH } from '../alias.js';
5
5
  import { createPath } from '../tree.js';
6
+ import { escapeRegExp } from '../util/escape-regexp.js';
6
7
  import { slash } from '../util/fs.js';
7
8
  import { globalsFromDefine } from './define-globals.js';
8
9
  import { createModuleServer } from './module-server.js';
@@ -24,7 +25,7 @@ export function useCollectStories(options, ctx) {
24
25
  // @TODO temporary fix for https://github.com/histoire-dev/histoire/issues/409
25
26
  /vite\w*\/dist\/client\/(client|env).mjs/,
26
27
  ...ctx.config.viteNodeInlineDeps ?? [],
27
- new RegExp(RegExp.escape(path.resolve(TEMP_PATH, 'plugins'))),
28
+ new RegExp(escapeRegExp(path.resolve(TEMP_PATH, 'plugins'))),
28
29
  ],
29
30
  transformMode: ctx.config.viteNodeTransformMode,
30
31
  });
@@ -1,7 +1,7 @@
1
- import { resolveConfig as resolveViteConfig } from 'vite';
1
+ import { loadConfigFromFile as loadViteConfigFromFile, resolveConfig as resolveViteConfig } from 'vite';
2
2
  import { processConfig, resolveConfig } from './config.js';
3
3
  import { viteCommand, viteMode } from './util/vite-mode.js';
4
- import { mergePovesteViteConfig } from './vite.js';
4
+ import { mergePovesteViteConfig, withoutIgnoredPlugins } from './vite.js';
5
5
  export async function createContext(options) {
6
6
  const config = await resolveConfig(process.cwd(), options.mode, options.configFile);
7
7
  const command = viteCommand(options.mode);
@@ -9,7 +9,15 @@ export async function createContext(options) {
9
9
  // `'development'` whatever the command, and `base` from this resolution is
10
10
  // what the hand-written index.html uses while the bundle uses the build's.
11
11
  // A user config setting `base` by mode would otherwise disagree with itself.
12
- const viteConfig = await resolveViteConfig({}, command, viteMode(options.mode));
12
+ // Through `viteIgnorePlugins`, as the dev server and the build are: an ignored
13
+ // plugin's `config` hook still runs in a resolution that skips it, and SvelteKit's
14
+ // sets a relative `base` here, which index.html then carries (#1200).
15
+ const userViteConfig = await loadViteConfigFromFile({ command, mode: viteMode(options.mode) });
16
+ const viteConfig = await resolveViteConfig({
17
+ ...userViteConfig?.config,
18
+ configFile: false,
19
+ plugins: await withoutIgnoredPlugins(userViteConfig?.config.plugins, config.viteIgnorePlugins),
20
+ }, command, viteMode(options.mode));
13
21
  const supportPlugins = config.plugins.flatMap(p => p.supportPlugin ? [p.supportPlugin] : []);
14
22
  const partial = {
15
23
  root: viteConfig.root,
@@ -2,6 +2,7 @@ export declare function createDomEnv(): {
2
2
  window: Window & typeof globalThis;
3
3
  destroy: () => void;
4
4
  pristineKeys: Set<string>;
5
+ storages: Storage[];
5
6
  };
6
7
  /** Returns a reused environment to the state a fresh one would have been in. */
7
- export declare function resetDomEnv(env: Pick<ReturnType<typeof createDomEnv>, 'window' | 'pristineKeys'>): void;
8
+ export declare function resetDomEnv(env: Pick<ReturnType<typeof createDomEnv>, 'window' | 'pristineKeys' | 'storages'>): void;
@@ -1,4 +1,5 @@
1
1
  import { JSDOM, VirtualConsole, } from 'jsdom';
2
+ import { isError } from '../util/is-error.js';
2
3
  import { populateGlobal } from './util.js';
3
4
  // jsdom error types — not exported by jsdom.
4
5
  const JSDOM_ERROR_CSS_PARSING = 'css-parsing';
@@ -26,7 +27,7 @@ function createVirtualConsole() {
26
27
  // `Error.isError` and not `instanceof`: the cause was constructed inside
27
28
  // jsdom's realm, where `instanceof Error` is false and the stack — the
28
29
  // only part that says which line threw — is discarded (#1093).
29
- globalThis.console.error((Error.isError(err.cause) ? err.cause.stack : undefined) ?? err.message);
30
+ globalThis.console.error((isError(err.cause) ? err.cause.stack : undefined) ?? err.message);
30
31
  }
31
32
  else {
32
33
  globalThis.console.error(err.message);
@@ -43,12 +44,31 @@ export function createDomEnv() {
43
44
  includeNodeLocations: false,
44
45
  contentType: 'text/html',
45
46
  });
47
+ const storageNames = ['localStorage', 'sessionStorage'];
48
+ // Before `populateGlobal`, which installs jsdom's own on older Node.
49
+ const storageDescriptors = storageNames.map(name => Object.getOwnPropertyDescriptor(globalThis, name));
46
50
  const { keys, originals } = populateGlobal(globalThis, dom.window, { bindFunctions: true });
51
+ // From Node 25 the global names are Node's experimental Web Storage, so
52
+ // `populateGlobal` leaves them, and every read warns without
53
+ // `--localstorage-file`: from a story during collection, and from the reset
54
+ // below (#1175). jsdom's take their place, as they already did on older Node.
55
+ // Descriptors rather than values, because reading Node's is what warns.
56
+ const storages = storageNames.map(name => dom.window[name]);
57
+ storageNames.forEach((name, index) => {
58
+ Object.defineProperty(globalThis, name, { configurable: true, enumerable: true, writable: true, value: storages[index] });
59
+ });
47
60
  function destroy() {
48
61
  keys.forEach(key => Reflect.deleteProperty(globalThis, key));
49
62
  originals.forEach((v, k) => {
50
63
  Reflect.set(globalThis, k, v);
51
64
  });
65
+ storageNames.forEach((name, index) => {
66
+ const descriptor = storageDescriptors[index];
67
+ if (descriptor)
68
+ Object.defineProperty(globalThis, name, descriptor);
69
+ else
70
+ Reflect.deleteProperty(globalThis, name);
71
+ });
52
72
  }
53
73
  window.ResizeObserver = window.ResizeObserver || class ResizeObserver {
54
74
  disconnect() { }
@@ -105,6 +125,7 @@ export function createDomEnv() {
105
125
  // `populateGlobal` does `global.window = global`, so `window` here is
106
126
  // `globalThis` — one surface, not two.
107
127
  pristineKeys: new Set(Object.keys(globalThis)),
128
+ storages,
108
129
  };
109
130
  }
110
131
  /** Returns a reused environment to the state a fresh one would have been in. */
@@ -119,14 +140,8 @@ export function resetDomEnv(env) {
119
140
  }
120
141
  doc.head?.replaceChildren();
121
142
  doc.body?.replaceChildren();
122
- try {
123
- // Only present when the key was proxied onto the global, which varies by
124
- // node version.
125
- env.window.localStorage?.clear();
126
- env.window.sessionStorage?.clear();
127
- }
128
- catch {
129
- // Unavailable for some jsdom urls.
143
+ for (const storage of env.storages) {
144
+ storage.clear();
130
145
  }
131
146
  // Additions only: the modules that installed the rest are cached, not re-run.
132
147
  for (const key of Object.keys(globalThis)) {
@@ -131,6 +131,17 @@ async function startServer(ctx, options, onOpen, close) {
131
131
  });
132
132
  let collecting = false;
133
133
  let didAllStoriesYet = false;
134
+ const updateFor = (url, timestamp) => ({
135
+ type: 'update',
136
+ updates: [
137
+ {
138
+ type: 'js-update',
139
+ acceptedPath: url,
140
+ path: url,
141
+ timestamp,
142
+ },
143
+ ],
144
+ });
134
145
  // Invalidate modules
135
146
  const invalidateModule = (id) => {
136
147
  const mod = server.moduleGraph.getModuleById(id);
@@ -141,18 +152,33 @@ async function startServer(ctx, options, onOpen, close) {
141
152
  // Send HMR update
142
153
  const timestamp = Date.now();
143
154
  mod.lastHMRTimestamp = timestamp;
144
- server.ws.send({
145
- type: 'update',
146
- updates: [
147
- {
148
- type: 'js-update',
149
- acceptedPath: mod.url,
150
- path: mod.url,
151
- timestamp,
152
- },
153
- ],
154
- });
155
+ server.ws.send(updateFor(mod.url, timestamp));
156
+ };
157
+ // Bumped before the list is invalidated, so the copy a client loads next
158
+ // carries the generation it reflects.
159
+ const invalidateStoryList = () => {
160
+ VirtualFiles.storiesGeneration.set(ctx, (VirtualFiles.storiesGeneration.get(ctx) ?? 0) + 1);
161
+ invalidateModule(VirtualFiles.RESOLVED_STORIES_ID);
162
+ invalidateModule(VirtualFiles.RESOLVED_SEARCH_TITLE_DATA_ID);
155
163
  };
164
+ // The update and `poveste:all-stories-loaded` reach only a page that has
165
+ // already evaluated the list and registered for them, and neither is
166
+ // replayed. A page that loaded the list mid-collection and was still loading
167
+ // when collection ended kept that partial list until a reload (#1218). By the
168
+ // time it mounts it is listening, so it is sent what it missed.
169
+ server.ws.on('poveste:mount', (data, client) => {
170
+ if (!didAllStoriesYet || data?.generation === (VirtualFiles.storiesGeneration.get(ctx) ?? 0)) {
171
+ return;
172
+ }
173
+ const timestamp = Date.now();
174
+ for (const id of [VirtualFiles.RESOLVED_STORIES_ID, VirtualFiles.RESOLVED_SEARCH_TITLE_DATA_ID]) {
175
+ const mod = server.moduleGraph.getModuleById(id);
176
+ if (mod) {
177
+ client.send(updateFor(mod.url, timestamp));
178
+ }
179
+ }
180
+ client.send('poveste:all-stories-loaded', {});
181
+ });
156
182
  onOpen('onStoryChange', onStoryChange(async (changedFile) => {
157
183
  if (changedFile && !didAllStoriesYet) {
158
184
  return;
@@ -227,16 +253,14 @@ async function startServer(ctx, options, onOpen, close) {
227
253
  return;
228
254
  }
229
255
  console.log(`Collect stories end ${pc.bold(pc.blue(Math.round(performance.now() - time)))}ms`);
230
- invalidateModule(VirtualFiles.RESOLVED_STORIES_ID);
231
- invalidateModule(VirtualFiles.RESOLVED_SEARCH_TITLE_DATA_ID);
256
+ invalidateStoryList();
232
257
  collecting = false;
233
258
  if (queued) {
234
259
  await collect();
235
260
  }
236
261
  }
237
262
  onOpen('onStoryListChange', onStoryListChange(() => {
238
- invalidateModule(VirtualFiles.RESOLVED_STORIES_ID);
239
- invalidateModule(VirtualFiles.RESOLVED_SEARCH_TITLE_DATA_ID);
263
+ invalidateStoryList();
240
264
  }));
241
265
  onOpen('onMarkdownListChange', onMarkdownListChange(() => {
242
266
  invalidateModule(VirtualFiles.RESOLVED_MARKDOWN_FILES);
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Escapes a string so a `RegExp` built from it matches that string literally.
3
+ *
4
+ * Not `RegExp.escape`: Node 22, inside the supported range since #1226, does not
5
+ * have it, and TypeScript's `ESNext` lib types it anyway, so the compiler says
6
+ * nothing when it is used.
7
+ */
8
+ export declare function escapeRegExp(value: string): string;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Escapes a string so a `RegExp` built from it matches that string literally.
3
+ *
4
+ * Not `RegExp.escape`: Node 22, inside the supported range since #1226, does not
5
+ * have it, and TypeScript's `ESNext` lib types it anyway, so the compiler says
6
+ * nothing when it is used.
7
+ */
8
+ export function escapeRegExp(value) {
9
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
10
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * `Error.isError` where the Node has it, which is from 24; Node 22, inside the
3
+ * supported range since #1226, does not. The fallback gives the same answer for
4
+ * what these call sites see: an error from another realm, jsdom's included, still
5
+ * reports `[object Error]`, and jsdom's `DOMException` fails both.
6
+ */
7
+ export declare function isError(value: unknown): value is Error;
@@ -0,0 +1,10 @@
1
+ const native = Error.isError;
2
+ /**
3
+ * `Error.isError` where the Node has it, which is from 24; Node 22, inside the
4
+ * supported range since #1226, does not. The fallback gives the same answer for
5
+ * what these call sites see: an error from another realm, jsdom's included, still
6
+ * reports `[object Error]`, and jsdom's `DOMException` fails both.
7
+ */
8
+ export function isError(value) {
9
+ return native ? native(value) : Object.prototype.toString.call(value) === '[object Error]';
10
+ }
@@ -1,8 +1,16 @@
1
+ import type { Plugin as VitePlugin } from 'vite';
1
2
  /**
2
3
  * Resolves `specifier` as Node would from `from`, to a real path, or `undefined`
3
4
  * when nothing answers.
4
5
  */
5
6
  export type Resolver = (specifier: string, from: string) => string | undefined;
7
+ /**
8
+ * Finds the package by walking `node_modules` up from `from`, then lets Node resolve
9
+ * the subpath from inside it, through its `exports`. Not `require.resolve` from `from`:
10
+ * Node folds NODE_PATH into every lookup, even with `paths`, and pnpm's bin shims
11
+ * point it at the whole store, so a Svelte book run through `poveste` "had" a Vue of
12
+ * its own and the chrome was collapsed onto a Vue Vite could not find (#1201).
13
+ */
6
14
  export declare const nodeResolver: Resolver;
7
15
  export interface VendoredAlias {
8
16
  /** The bare package the alias points at, `vue`. */
@@ -54,8 +62,8 @@ export declare function satisfiesCaret(version: string | undefined, range: strin
54
62
  * and making the chrome's pinned copies authoritative for a project's own story code
55
63
  * is a larger change than this defect asks for.
56
64
  *
57
- * Empty when the project has no Vue of its own, when the two already resolve to one
58
- * file, or when the project's Vue does not satisfy the range vendors pins — in each
65
+ * Empty when the project has no Vue of its own, or when the project's Vue does not
66
+ * satisfy the range vendors pins — in each
59
67
  * case the vendored copy is what the chrome should keep. Refusing on anything it
60
68
  * cannot read with certainty is deliberate: the failure of this function is a chrome
61
69
  * that keeps a Vue it can certainly run, and the failure of guessing is a chrome
@@ -71,4 +79,36 @@ export declare function satisfiesCaret(version: string | undefined, range: strin
71
79
  export declare function collapseVendoredVue(options: {
72
80
  root: string;
73
81
  resolve?: Resolver;
82
+ /** Where `@poveste/vendors` is resolved from: Poveste's own install unless a test says otherwise. */
83
+ vendorsFrom?: string;
74
84
  }): Record<string, string>;
85
+ /**
86
+ * Resolves `@poveste/vendors` from Poveste's own install, wherever it is imported.
87
+ *
88
+ * `@poveste/vendors` is excluded from pre-bundling, so the pre-bundled
89
+ * `@poveste/controls` keeps bare imports of it. Vite resolves an optimized entry's
90
+ * imports from the package's real location, but a shared chunk of one has no real
91
+ * location and resolves from `node_modules/.pvt-vite/deps` under the project. Since
92
+ * #593 split the controls into chunks, that only worked where npm or Yarn had
93
+ * hoisted `@poveste/vendors` to the project's `node_modules`. pnpm doesn't, and
94
+ * `poveste dev` rendered a blank page (#1201).
95
+ */
96
+ export declare function vendorsFromPoveste(): VitePlugin;
97
+ /**
98
+ * The chrome's vendored packages, as nested pre-bundle specifiers, for a book with no
99
+ * Vue of its own (Svelte, Solid, React).
100
+ *
101
+ * There the collapse has no project Vue to point at, and under pnpm `poveste-vue`
102
+ * and the `vue` that `poveste-vue-router` imports are one file in the store. Vite
103
+ * pre-bundled the first and served the second raw: one file, two module instances,
104
+ * and a book that rendered no story. Pre-bundling every vendored package together,
105
+ * reached through Poveste's own install, puts their Vue in one shared chunk (#1201).
106
+ *
107
+ * Empty wherever the collapse applies, or when the chain cannot be resolved from
108
+ * the project, where Vite would only warn about an include it cannot find.
109
+ */
110
+ export declare function chromePrebundles(options: {
111
+ root: string;
112
+ resolve?: Resolver;
113
+ vendorsFrom?: string;
114
+ }): string[];
@@ -1,15 +1,37 @@
1
- import { readFileSync, realpathSync } from 'node:fs';
1
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
2
2
  import { createRequire } from 'node:module';
3
- import { join } from 'pathe';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { dirname, join } from 'pathe';
5
+ // A file of this package's own, to resolve from.
6
+ const POVESTE_FILE = fileURLToPath(import.meta.url);
7
+ function packageName(specifier) {
8
+ const parts = specifier.split('/');
9
+ return specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
10
+ }
11
+ /**
12
+ * Finds the package by walking `node_modules` up from `from`, then lets Node resolve
13
+ * the subpath from inside it, through its `exports`. Not `require.resolve` from `from`:
14
+ * Node folds NODE_PATH into every lookup, even with `paths`, and pnpm's bin shims
15
+ * point it at the whole store, so a Svelte book run through `poveste` "had" a Vue of
16
+ * its own and the chrome was collapsed onto a Vue Vite could not find (#1201).
17
+ */
4
18
  export const nodeResolver = (specifier, from) => {
5
- try {
6
- // `createRequire` resolves relative to the *dirname* of what it is given, so a
7
- // directory has to be named as a file inside it or every lookup starts one
8
- // level too high.
9
- return realpathSync(createRequire(join(from, 'noop.js')).resolve(specifier));
10
- }
11
- catch {
12
- return undefined;
19
+ const name = packageName(specifier);
20
+ for (let dir = from;; dir = dirname(dir)) {
21
+ const candidate = join(dir, 'node_modules', name);
22
+ if (existsSync(join(candidate, 'package.json'))) {
23
+ try {
24
+ // From a file inside the package, whose own `node_modules` parent is the
25
+ // first place Node looks, so the lookup ends here before NODE_PATH.
26
+ return realpathSync(createRequire(join(candidate, 'package.json')).resolve(specifier));
27
+ }
28
+ catch {
29
+ return undefined;
30
+ }
31
+ }
32
+ if (dirname(dir) === dir) {
33
+ return undefined;
34
+ }
13
35
  }
14
36
  };
15
37
  /**
@@ -92,8 +114,8 @@ export function satisfiesCaret(version, range) {
92
114
  * and making the chrome's pinned copies authoritative for a project's own story code
93
115
  * is a larger change than this defect asks for.
94
116
  *
95
- * Empty when the project has no Vue of its own, when the two already resolve to one
96
- * file, or when the project's Vue does not satisfy the range vendors pins — in each
117
+ * Empty when the project has no Vue of its own, or when the project's Vue does not
118
+ * satisfy the range vendors pins — in each
97
119
  * case the vendored copy is what the chrome should keep. Refusing on anything it
98
120
  * cannot read with certainty is deliberate: the failure of this function is a chrome
99
121
  * that keeps a Vue it can certainly run, and the failure of guessing is a chrome
@@ -109,12 +131,13 @@ export function satisfiesCaret(version, range) {
109
131
  export function collapseVendoredVue(options) {
110
132
  const { root } = options;
111
133
  const resolve = options.resolve ?? nodeResolver;
112
- // From the vendors directory, not from the project: that is where the import
113
- // happens, and an isolated `node_modules` layout makes `poveste-vue` invisible
114
- // from the project root while the chrome resolves it perfectly well.
134
+ // Vendors from Poveste's own install, and `poveste-vue` from the vendors directory:
135
+ // that is where each import happens. An isolated `node_modules` layout, pnpm's,
136
+ // puts neither at the project root, so resolving vendors from there found nothing,
137
+ // nothing was collapsed, and a pnpm book had two Vues (#1201).
115
138
  // Through an exported subpath: vendors' `exports` map does not list
116
139
  // `./package.json`, so resolving that name directly fails.
117
- const vendorsEntry = resolve('@poveste/vendors/vue', root);
140
+ const vendorsEntry = resolve('@poveste/vendors/vue', options.vendorsFrom ?? dirname(POVESTE_FILE));
118
141
  const vendorsDir = vendorsEntry ? join(vendorsEntry, '..') : undefined;
119
142
  if (!vendorsDir) {
120
143
  return {};
@@ -127,7 +150,10 @@ export function collapseVendoredVue(options) {
127
150
  const [installedAs, { range }] = vueAlias;
128
151
  const vendored = resolve(installedAs, vendorsDir);
129
152
  const project = resolve('vue', root);
130
- if (!vendored || !project || vendored === project) {
153
+ // Even when they are one file, as pnpm makes them: Vite keys a pre-bundled dependency
154
+ // by the name it is imported under, so `poveste-vue` was served raw beside the
155
+ // pre-bundled `vue` — one file, two module instances, and no story (#1201).
156
+ if (!vendored || !project) {
131
157
  return {};
132
158
  }
133
159
  if (!satisfiesCaret(versionOf('vue/package.json', resolve, root), range)) {
@@ -155,3 +181,52 @@ function readJson(file) {
155
181
  return undefined;
156
182
  }
157
183
  }
184
+ /**
185
+ * Resolves `@poveste/vendors` from Poveste's own install, wherever it is imported.
186
+ *
187
+ * `@poveste/vendors` is excluded from pre-bundling, so the pre-bundled
188
+ * `@poveste/controls` keeps bare imports of it. Vite resolves an optimized entry's
189
+ * imports from the package's real location, but a shared chunk of one has no real
190
+ * location and resolves from `node_modules/.pvt-vite/deps` under the project. Since
191
+ * #593 split the controls into chunks, that only worked where npm or Yarn had
192
+ * hoisted `@poveste/vendors` to the project's `node_modules`. pnpm doesn't, and
193
+ * `poveste dev` rendered a blank page (#1201).
194
+ */
195
+ export function vendorsFromPoveste() {
196
+ return {
197
+ name: 'poveste:vendors-from-poveste',
198
+ enforce: 'pre',
199
+ resolveId(source, _importer, options) {
200
+ if (source !== '@poveste/vendors' && !source.startsWith('@poveste/vendors/')) {
201
+ return null;
202
+ }
203
+ return this.resolve(source, POVESTE_FILE, { ...options, skipSelf: true });
204
+ },
205
+ };
206
+ }
207
+ /**
208
+ * The chrome's vendored packages, as nested pre-bundle specifiers, for a book with no
209
+ * Vue of its own (Svelte, Solid, React).
210
+ *
211
+ * There the collapse has no project Vue to point at, and under pnpm `poveste-vue`
212
+ * and the `vue` that `poveste-vue-router` imports are one file in the store. Vite
213
+ * pre-bundled the first and served the second raw: one file, two module instances,
214
+ * and a book that rendered no story. Pre-bundling every vendored package together,
215
+ * reached through Poveste's own install, puts their Vue in one shared chunk (#1201).
216
+ *
217
+ * Empty wherever the collapse applies, or when the chain cannot be resolved from
218
+ * the project, where Vite would only warn about an include it cannot find.
219
+ */
220
+ export function chromePrebundles(options) {
221
+ const { root } = options;
222
+ const resolve = options.resolve ?? nodeResolver;
223
+ if (resolve('vue', root) || !resolve('poveste/package.json', root)) {
224
+ return [];
225
+ }
226
+ const vendorsEntry = resolve('@poveste/vendors/vue', options.vendorsFrom ?? dirname(POVESTE_FILE));
227
+ if (!vendorsEntry) {
228
+ return [];
229
+ }
230
+ const aliases = vendoredAliases(readJson(join(vendorsEntry, '..', 'package.json'))?.dependencies);
231
+ return Object.keys(aliases).map(installedAs => `poveste > @poveste/vendors > ${installedAs}`);
232
+ }
@@ -1,2 +1,8 @@
1
1
  import type { Context } from '../context.js';
2
+ /**
3
+ * How many times the story list has changed since the server started, per book.
4
+ * A client that loaded the list before a change, and missed the update that
5
+ * announced it, is told so when it mounts (#1218).
6
+ */
7
+ export declare const storiesGeneration: WeakMap<Context, number>;
2
8
  export declare function resolvedStories(ctx: Context): string;
@@ -1,5 +1,11 @@
1
1
  import { makeTree } from '../tree.js';
2
2
  import { jsString } from './codegen.js';
3
+ /**
4
+ * How many times the story list has changed since the server started, per book.
5
+ * A client that loaded the list before a change, and missed the update that
6
+ * announced it, is told so when it mounts (#1218).
7
+ */
8
+ export const storiesGeneration = new WeakMap();
3
9
  export function resolvedStories(ctx) {
4
10
  const resolvedStories = ctx.storyFiles.filter(s => !!s.story);
5
11
  const files = resolvedStories.map((file, index) => {
@@ -26,6 +32,7 @@ ${resolvedStories.map((file, index) => {
26
32
  }).filter(Boolean).join('\n')}
27
33
  export let files = [${files.map(file => `{${JSON.stringify(file).slice(1, -1)}, component: Comp${file.index}, source: () => import(${jsString(`virtual:story-source:${file.story.id}`)})}`).join(',\n')}]
28
34
  export let tree = ${JSON.stringify(makeTree(ctx.config, resolvedStories))}
35
+ export let generation = ${storiesGeneration.get(ctx) ?? 0}
29
36
  const handlers = []
30
37
  export function onUpdate (cb) {
31
38
  handlers.push(cb)
@@ -34,6 +41,7 @@ if (import.meta.hot) {
34
41
  import.meta.hot.accept(newModule => {
35
42
  files = newModule.files
36
43
  tree = newModule.tree
44
+ generation = newModule.generation
37
45
  handlers.forEach(h => {
38
46
  h(newModule.files, newModule.tree)
39
47
  newModule.onUpdate(h)
@@ -1,5 +1,11 @@
1
- import type { InlineConfig } from 'vite';
1
+ import type { PovesteConfig } from '@poveste/shared';
2
+ import type { InlineConfig, PluginOption } from 'vite';
2
3
  import type { Context } from './context.js';
4
+ /**
5
+ * The user's plugins, flattened one level as Vite's own options are, without the
6
+ * ones `viteIgnorePlugins` names.
7
+ */
8
+ export declare function withoutIgnoredPlugins(plugins: PluginOption[] | undefined, ignored: PovesteConfig['viteIgnorePlugins']): Promise<PluginOption[]>;
3
9
  export declare function mergePovesteViteConfig(viteConfig: InlineConfig, ctx: Pick<Context, 'config' | 'mode'>): Promise<InlineConfig>;
4
10
  export interface ViteConfigWithPlugins {
5
11
  viteConfig: InlineConfig;
package/dist/node/vite.js CHANGED
@@ -8,11 +8,29 @@ import { createMarkdownPlugins } from './markdown.js';
8
8
  import { optimizeEntries } from './optimize-entries.js';
9
9
  import { notifyStoryChange } from './stories.js';
10
10
  import { chromeCssScopePlugin, globalStylesPlugin, userCssScopePlugin, } from './style-isolation/index.js';
11
+ import { escapeRegExp } from './util/escape-regexp.js';
11
12
  import { applyHeadTransform } from './util/head.js';
12
13
  import { viteCommand, viteMode } from './util/vite-mode.js';
13
- import { collapseVendoredVue } from './vendors.js';
14
+ import { chromePrebundles, collapseVendoredVue, vendorsFromPoveste } from './vendors.js';
14
15
  import { createVirtualFilesPlugin } from './virtual/vite-plugin.js';
15
16
  const require = createRequire(import.meta.url);
17
+ /**
18
+ * The user's plugins, flattened one level as Vite's own options are, without the
19
+ * ones `viteIgnorePlugins` names.
20
+ */
21
+ export async function withoutIgnoredPlugins(plugins, ignored) {
22
+ const flat = [];
23
+ for (const pluginOption of plugins ?? []) {
24
+ const resolvedPluginOption = await pluginOption;
25
+ if (Array.isArray(resolvedPluginOption)) {
26
+ flat.push(...await Promise.all(resolvedPluginOption));
27
+ }
28
+ else {
29
+ flat.push(resolvedPluginOption);
30
+ }
31
+ }
32
+ return flat.filter(plugin => plugin && !('name' in plugin && ignored?.includes(plugin.name)));
33
+ }
16
34
  export async function mergePovesteViteConfig(viteConfig, ctx) {
17
35
  if (ctx.config.vite) {
18
36
  const command = viteCommand(ctx.mode);
@@ -26,24 +44,7 @@ export async function mergePovesteViteConfig(viteConfig, ctx) {
26
44
  viteConfig = mergeViteConfig(viteConfig, overrides);
27
45
  }
28
46
  }
29
- let flatPlugins = [];
30
- if (viteConfig.plugins) {
31
- for (const pluginOption of viteConfig.plugins) {
32
- const resolvedPluginOption = await pluginOption;
33
- if (Array.isArray(resolvedPluginOption)) {
34
- flatPlugins.push(...await Promise.all(resolvedPluginOption));
35
- }
36
- else {
37
- flatPlugins.push(resolvedPluginOption);
38
- }
39
- }
40
- flatPlugins = flatPlugins.filter(Boolean);
41
- }
42
- const ignoredPlugins = ctx.config.viteIgnorePlugins;
43
- if (ignoredPlugins) {
44
- flatPlugins = flatPlugins.filter(plugin => !(plugin && 'name' in plugin && ignoredPlugins.includes(plugin.name)));
45
- }
46
- viteConfig.plugins = flatPlugins;
47
+ viteConfig.plugins = await withoutIgnoredPlugins(viteConfig.plugins, ctx.config.viteIgnorePlugins);
47
48
  return viteConfig;
48
49
  }
49
50
  export async function getViteConfigWithPlugins(isServer, ctx) {
@@ -148,6 +149,10 @@ export async function getViteConfigWithPlugins(isServer, ctx) {
148
149
  config(_, { command }) {
149
150
  const collapsedVue = isServer || process.env['POVESTE_DEV'] ? {} : collapseVendoredVue({ root: ctx.root });
150
151
  return {
152
+ // The book is a single-page app whatever the project's framework says: a
153
+ // framework serving its own app sets `custom`, which turns off the fallback
154
+ // that serves `/story/...` (#1200).
155
+ appType: 'spa',
151
156
  resolve: {
152
157
  dedupe: [
153
158
  'vue',
@@ -188,6 +193,7 @@ export async function getViteConfigWithPlugins(isServer, ctx) {
188
193
  // bare `vue` was served raw beside the pre-bundled copy of the same
189
194
  // file: two module instances, and `RouterView` rendered nothing (#1134).
190
195
  ...(Object.keys(collapsedVue).length > 0 ? ['vue'] : []),
196
+ ...isServer || process.env['POVESTE_DEV'] ? [] : chromePrebundles({ root: ctx.root }),
191
197
  ],
192
198
  exclude: [
193
199
  'poveste',
@@ -328,6 +334,7 @@ export async function getViteConfigWithPlugins(isServer, ctx) {
328
334
  };
329
335
  },
330
336
  });
337
+ plugins.push(vendorsFromPoveste());
331
338
  plugins.push(createVirtualFilesPlugin(ctx, isServer));
332
339
  plugins.push(globalStylesPlugin({
333
340
  files: ctx.config.globalStyles ?? [],
@@ -355,7 +362,7 @@ export async function getViteConfigWithPlugins(isServer, ctx) {
355
362
  if (/\.(?:vue|js)(?:$|\?)/.test(id)) {
356
363
  const original = code;
357
364
  for (const [flag, value] of Object.entries(flags)) {
358
- code = code.replace(new RegExp(RegExp.escape(flag), 'g'), value);
365
+ code = code.replace(new RegExp(escapeRegExp(flag), 'g'), value);
359
366
  }
360
367
  if (original !== code)
361
368
  return code;
@@ -415,11 +422,11 @@ export async function getViteConfigWithPlugins(isServer, ctx) {
415
422
  ['vue', 'vue'],
416
423
  ].reduce((acc, [name, entry]) => {
417
424
  acc.push({
418
- find: new RegExp(`^${RegExp.escape(name)}$`),
425
+ find: new RegExp(`^${escapeRegExp(name)}$`),
419
426
  replacement: `@poveste/vendors/${entry}`,
420
427
  });
421
428
  acc.push({
422
- find: new RegExp(`^${RegExp.escape(name)}/`),
429
+ find: new RegExp(`^${escapeRegExp(name)}/`),
423
430
  replacement: `@poveste/vendors/${entry}/`,
424
431
  });
425
432
  return acc;
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "poveste",
3
3
  "type": "module",
4
- "version": "0.17.2",
5
- "description": "Interactive component playgrounds for Vue, Nuxt, Svelte, SvelteKit and Quasar — a drop-in fork of histoire, with Quasar support histoire does not ship.",
4
+ "version": "0.18.1",
5
+ "description": "Interactive component playgrounds for Vue, Nuxt, Svelte, SvelteKit and Quasar, plus Solid (renderer only) — a drop-in fork of histoire, with Quasar support histoire does not ship.",
6
6
  "author": {
7
7
  "name": "Sorin Gitlan",
8
8
  "url": "https://x.com/50rayn"
@@ -29,6 +29,8 @@
29
29
  "svelte",
30
30
  "nuxt",
31
31
  "sveltekit",
32
+ "solid",
33
+ "solidjs",
32
34
  "cli"
33
35
  ],
34
36
  "publishConfig": {
@@ -68,17 +70,17 @@
68
70
  "plugin.js"
69
71
  ],
70
72
  "engines": {
71
- "node": ">=24.15.0"
73
+ "node": ">=22.22.2"
72
74
  },
73
75
  "peerDependencies": {
74
76
  "vite": "^8.0.0"
75
77
  },
76
78
  "dependencies": {
77
79
  "@11ty/gray-matter": "^3.0.0",
78
- "@poveste/app": "^0.17.2",
79
- "@poveste/controls": "^0.17.2",
80
- "@poveste/shared": "^0.17.2",
81
- "@poveste/vendors": "^0.17.2",
80
+ "@poveste/app": "^0.18.1",
81
+ "@poveste/controls": "^0.18.1",
82
+ "@poveste/shared": "^0.18.1",
83
+ "@poveste/vendors": "^0.18.1",
82
84
  "change-case": "^5.4.4",
83
85
  "chokidar": "^5.0.0",
84
86
  "defu": "^6.1.4",