poveste 0.18.0 → 0.18.2

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,9 @@
1
+ import './iterator.js';
1
2
  export declare function createDomEnv(): {
2
3
  window: Window & typeof globalThis;
3
4
  destroy: () => void;
4
5
  pristineKeys: Set<string>;
6
+ storages: Storage[];
5
7
  };
6
8
  /** 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;
9
+ export declare function resetDomEnv(env: Pick<ReturnType<typeof createDomEnv>, 'window' | 'pristineKeys' | 'storages'>): void;
@@ -1,5 +1,11 @@
1
+ /* eslint-disable perfectionist/sort-imports -- order is load order here */
2
+ // First, and kept first: it defines `Iterator` where the runtime lacks it, and
3
+ // jsdom extends `Iterator` while it loads (#1238).
4
+ import './iterator.js';
1
5
  import { JSDOM, VirtualConsole, } from 'jsdom';
6
+ import { isError } from '../util/is-error.js';
2
7
  import { populateGlobal } from './util.js';
8
+ /* eslint-enable perfectionist/sort-imports */
3
9
  // jsdom error types — not exported by jsdom.
4
10
  const JSDOM_ERROR_CSS_PARSING = 'css-parsing';
5
11
  const JSDOM_ERROR_UNHANDLED_EXCEPTION = 'unhandled-exception';
@@ -26,7 +32,7 @@ function createVirtualConsole() {
26
32
  // `Error.isError` and not `instanceof`: the cause was constructed inside
27
33
  // jsdom's realm, where `instanceof Error` is false and the stack — the
28
34
  // only part that says which line threw — is discarded (#1093).
29
- globalThis.console.error((Error.isError(err.cause) ? err.cause.stack : undefined) ?? err.message);
35
+ globalThis.console.error((isError(err.cause) ? err.cause.stack : undefined) ?? err.message);
30
36
  }
31
37
  else {
32
38
  globalThis.console.error(err.message);
@@ -43,12 +49,31 @@ export function createDomEnv() {
43
49
  includeNodeLocations: false,
44
50
  contentType: 'text/html',
45
51
  });
52
+ const storageNames = ['localStorage', 'sessionStorage'];
53
+ // Before `populateGlobal`, which installs jsdom's own on older Node.
54
+ const storageDescriptors = storageNames.map(name => Object.getOwnPropertyDescriptor(globalThis, name));
46
55
  const { keys, originals } = populateGlobal(globalThis, dom.window, { bindFunctions: true });
56
+ // From Node 25 the global names are Node's experimental Web Storage, so
57
+ // `populateGlobal` leaves them, and every read warns without
58
+ // `--localstorage-file`: from a story during collection, and from the reset
59
+ // below (#1175). jsdom's take their place, as they already did on older Node.
60
+ // Descriptors rather than values, because reading Node's is what warns.
61
+ const storages = storageNames.map(name => dom.window[name]);
62
+ storageNames.forEach((name, index) => {
63
+ Object.defineProperty(globalThis, name, { configurable: true, enumerable: true, writable: true, value: storages[index] });
64
+ });
47
65
  function destroy() {
48
66
  keys.forEach(key => Reflect.deleteProperty(globalThis, key));
49
67
  originals.forEach((v, k) => {
50
68
  Reflect.set(globalThis, k, v);
51
69
  });
70
+ storageNames.forEach((name, index) => {
71
+ const descriptor = storageDescriptors[index];
72
+ if (descriptor)
73
+ Object.defineProperty(globalThis, name, descriptor);
74
+ else
75
+ Reflect.deleteProperty(globalThis, name);
76
+ });
52
77
  }
53
78
  window.ResizeObserver = window.ResizeObserver || class ResizeObserver {
54
79
  disconnect() { }
@@ -105,6 +130,7 @@ export function createDomEnv() {
105
130
  // `populateGlobal` does `global.window = global`, so `window` here is
106
131
  // `globalThis` — one surface, not two.
107
132
  pristineKeys: new Set(Object.keys(globalThis)),
133
+ storages,
108
134
  };
109
135
  }
110
136
  /** Returns a reused environment to the state a fresh one would have been in. */
@@ -119,14 +145,8 @@ export function resetDomEnv(env) {
119
145
  }
120
146
  doc.head?.replaceChildren();
121
147
  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.
148
+ for (const storage of env.storages) {
149
+ storage.clear();
130
150
  }
131
151
  // Additions only: the modules that installed the rest are cached, not re-run.
132
152
  for (const key of Object.keys(globalThis)) {
@@ -0,0 +1,2 @@
1
+ /** Defines `globalThis.Iterator` and its helpers where the runtime has neither. */
2
+ export declare function installIterator(): void;
@@ -0,0 +1,213 @@
1
+ // StackBlitz's WebContainer reports Node 22.22.3 but has no global `Iterator`, and
2
+ // jsdom 30.1 extends it when it loads, so every story failed to collect there
3
+ // (#1238). Real Node has had it since 22.0 and never runs this.
4
+ //
5
+ // Vendored rather than core-js: core-js and core-js-pure both carry a postinstall,
6
+ // and pnpm 11 and 12 fail an install outright on a dependency's unapproved build
7
+ // script. The full ES2025 helper set rather than only the `find` jsdom calls
8
+ // today, so the next helper jsdom reaches for is already here.
9
+ const IteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()));
10
+ function callable(fn, name) {
11
+ if (typeof fn !== 'function') {
12
+ throw new TypeError(`Iterator.prototype.${name} expects a function`);
13
+ }
14
+ return fn;
15
+ }
16
+ function count(value, name) {
17
+ const n = Number(value);
18
+ if (Number.isNaN(n)) {
19
+ throw new RangeError(`Iterator.prototype.${name} expects a number`);
20
+ }
21
+ const whole = Math.trunc(n);
22
+ if (whole < 0) {
23
+ throw new RangeError(`Iterator.prototype.${name} expects a non-negative number`);
24
+ }
25
+ return whole;
26
+ }
27
+ // Closing after a callback threw keeps the callback's error, as the spec does.
28
+ function closeQuietly(iterator) {
29
+ try {
30
+ iterator.return?.();
31
+ }
32
+ catch { }
33
+ }
34
+ function guarded(iterator, run) {
35
+ try {
36
+ return run();
37
+ }
38
+ catch (error) {
39
+ closeQuietly(iterator);
40
+ throw error;
41
+ }
42
+ }
43
+ /**
44
+ * A lazy helper. `step` turns each value into what to yield, and `stop` ends it
45
+ * before the next read. The underlying iterator is closed whenever this one ends
46
+ * before it does.
47
+ */
48
+ function lazy(iterator, step, stop = () => false) {
49
+ const next = iterator.next;
50
+ return (function* () {
51
+ let exhausted = false;
52
+ try {
53
+ for (let index = 0; !stop(); index++) {
54
+ const result = next.call(iterator);
55
+ if (result.done) {
56
+ exhausted = true;
57
+ return;
58
+ }
59
+ yield* guarded(iterator, () => step(result.value, index));
60
+ }
61
+ }
62
+ finally {
63
+ if (!exhausted) {
64
+ closeQuietly(iterator);
65
+ }
66
+ }
67
+ })();
68
+ }
69
+ /** An eager helper: `visit` returns true to stop, which closes the iterator. */
70
+ function each(iterator, visit) {
71
+ const next = iterator.next;
72
+ for (let index = 0;; index++) {
73
+ const result = next.call(iterator);
74
+ if (result.done) {
75
+ return;
76
+ }
77
+ if (guarded(iterator, () => visit(result.value, index))) {
78
+ closeQuietly(iterator);
79
+ return;
80
+ }
81
+ }
82
+ }
83
+ function flattenable(value, name) {
84
+ if (value === null || (typeof value !== 'object' && typeof value !== 'function')) {
85
+ throw new TypeError(`Iterator.${name} expects an iterator or an iterable`);
86
+ }
87
+ const method = Reflect.get(value, Symbol.iterator);
88
+ const inner = method === undefined ? value : method.call(value);
89
+ return { [Symbol.iterator]: () => inner };
90
+ }
91
+ const helpers = {
92
+ map(fn) {
93
+ const mapper = callable(fn, 'map');
94
+ return lazy(this, (value, index) => [mapper(value, index)]);
95
+ },
96
+ filter(fn) {
97
+ const predicate = callable(fn, 'filter');
98
+ return lazy(this, (value, index) => (predicate(value, index) ? [value] : []));
99
+ },
100
+ take(limit) {
101
+ let remaining = count(limit, 'take');
102
+ return lazy(this, (value) => {
103
+ remaining--;
104
+ return [value];
105
+ }, () => remaining === 0);
106
+ },
107
+ drop(limit) {
108
+ let remaining = count(limit, 'drop');
109
+ return lazy(this, (value) => {
110
+ if (remaining > 0) {
111
+ remaining--;
112
+ return [];
113
+ }
114
+ return [value];
115
+ });
116
+ },
117
+ flatMap(fn) {
118
+ const mapper = callable(fn, 'flatMap');
119
+ return lazy(this, (value, index) => flattenable(mapper(value, index), 'prototype.flatMap'));
120
+ },
121
+ reduce(fn, ...initial) {
122
+ const reducer = callable(fn, 'reduce');
123
+ let accumulator = initial[0];
124
+ let started = initial.length > 0;
125
+ each(this, (value, index) => {
126
+ if (started) {
127
+ accumulator = reducer(accumulator, value, index);
128
+ }
129
+ else {
130
+ accumulator = value;
131
+ started = true;
132
+ }
133
+ return false;
134
+ });
135
+ if (!started) {
136
+ throw new TypeError('Iterator.prototype.reduce of an empty iterator with no initial value');
137
+ }
138
+ return accumulator;
139
+ },
140
+ toArray() {
141
+ const out = [];
142
+ each(this, (value) => {
143
+ out.push(value);
144
+ return false;
145
+ });
146
+ return out;
147
+ },
148
+ forEach(fn) {
149
+ const visit = callable(fn, 'forEach');
150
+ each(this, (value, index) => {
151
+ visit(value, index);
152
+ return false;
153
+ });
154
+ },
155
+ some(fn) {
156
+ const predicate = callable(fn, 'some');
157
+ let found = false;
158
+ each(this, (value, index) => {
159
+ found = Boolean(predicate(value, index));
160
+ return found;
161
+ });
162
+ return found;
163
+ },
164
+ every(fn) {
165
+ const predicate = callable(fn, 'every');
166
+ let all = true;
167
+ each(this, (value, index) => {
168
+ all = Boolean(predicate(value, index));
169
+ return !all;
170
+ });
171
+ return all;
172
+ },
173
+ find(fn) {
174
+ const predicate = callable(fn, 'find');
175
+ let found;
176
+ each(this, (value, index) => {
177
+ if (predicate(value, index)) {
178
+ found = value;
179
+ return true;
180
+ }
181
+ return false;
182
+ });
183
+ return found;
184
+ },
185
+ };
186
+ /** Defines `globalThis.Iterator` and its helpers where the runtime has neither. */
187
+ export function installIterator() {
188
+ if (typeof Reflect.get(globalThis, 'Iterator') !== 'undefined') {
189
+ return;
190
+ }
191
+ function Iterator() {
192
+ if (new.target === undefined || new.target === Iterator) {
193
+ throw new TypeError('Iterator is an abstract class and cannot be constructed directly');
194
+ }
195
+ }
196
+ Iterator.prototype = IteratorPrototype;
197
+ Object.defineProperty(IteratorPrototype, 'constructor', { value: Iterator, writable: true, configurable: true });
198
+ Object.defineProperty(Iterator, 'from', {
199
+ value(value) {
200
+ const iterator = (typeof value === 'string' ? value : flattenable(value, 'from'))[Symbol.iterator]();
201
+ return Object.prototype.isPrototypeOf.call(IteratorPrototype, iterator) ? iterator : lazy(iterator, item => [item]);
202
+ },
203
+ writable: true,
204
+ configurable: true,
205
+ });
206
+ for (const [name, fn] of Object.entries(helpers)) {
207
+ if (!(name in IteratorPrototype)) {
208
+ Object.defineProperty(IteratorPrototype, name, { value: fn, writable: true, configurable: true });
209
+ }
210
+ }
211
+ Object.defineProperty(globalThis, 'Iterator', { value: Iterator, writable: true, configurable: true });
212
+ }
213
+ installIterator();
@@ -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)
package/dist/node/vite.js CHANGED
@@ -8,9 +8,10 @@ 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);
16
17
  /**
@@ -192,6 +193,7 @@ export async function getViteConfigWithPlugins(isServer, ctx) {
192
193
  // bare `vue` was served raw beside the pre-bundled copy of the same
193
194
  // file: two module instances, and `RouterView` rendered nothing (#1134).
194
195
  ...(Object.keys(collapsedVue).length > 0 ? ['vue'] : []),
196
+ ...isServer || process.env['POVESTE_DEV'] ? [] : chromePrebundles({ root: ctx.root }),
195
197
  ],
196
198
  exclude: [
197
199
  'poveste',
@@ -332,6 +334,7 @@ export async function getViteConfigWithPlugins(isServer, ctx) {
332
334
  };
333
335
  },
334
336
  });
337
+ plugins.push(vendorsFromPoveste());
335
338
  plugins.push(createVirtualFilesPlugin(ctx, isServer));
336
339
  plugins.push(globalStylesPlugin({
337
340
  files: ctx.config.globalStyles ?? [],
@@ -359,7 +362,7 @@ export async function getViteConfigWithPlugins(isServer, ctx) {
359
362
  if (/\.(?:vue|js)(?:$|\?)/.test(id)) {
360
363
  const original = code;
361
364
  for (const [flag, value] of Object.entries(flags)) {
362
- code = code.replace(new RegExp(RegExp.escape(flag), 'g'), value);
365
+ code = code.replace(new RegExp(escapeRegExp(flag), 'g'), value);
363
366
  }
364
367
  if (original !== code)
365
368
  return code;
@@ -419,11 +422,11 @@ export async function getViteConfigWithPlugins(isServer, ctx) {
419
422
  ['vue', 'vue'],
420
423
  ].reduce((acc, [name, entry]) => {
421
424
  acc.push({
422
- find: new RegExp(`^${RegExp.escape(name)}$`),
425
+ find: new RegExp(`^${escapeRegExp(name)}$`),
423
426
  replacement: `@poveste/vendors/${entry}`,
424
427
  });
425
428
  acc.push({
426
- find: new RegExp(`^${RegExp.escape(name)}/`),
429
+ find: new RegExp(`^${escapeRegExp(name)}/`),
427
430
  replacement: `@poveste/vendors/${entry}/`,
428
431
  });
429
432
  return acc;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "poveste",
3
3
  "type": "module",
4
- "version": "0.18.0",
4
+ "version": "0.18.2",
5
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",
@@ -70,17 +70,17 @@
70
70
  "plugin.js"
71
71
  ],
72
72
  "engines": {
73
- "node": ">=24.15.0"
73
+ "node": ">=22.22.2"
74
74
  },
75
75
  "peerDependencies": {
76
76
  "vite": "^8.0.0"
77
77
  },
78
78
  "dependencies": {
79
79
  "@11ty/gray-matter": "^3.0.0",
80
- "@poveste/app": "^0.18.0",
81
- "@poveste/controls": "^0.18.0",
82
- "@poveste/shared": "^0.18.0",
83
- "@poveste/vendors": "^0.18.0",
80
+ "@poveste/app": "^0.18.2",
81
+ "@poveste/controls": "^0.18.2",
82
+ "@poveste/shared": "^0.18.2",
83
+ "@poveste/vendors": "^0.18.2",
84
84
  "change-case": "^5.4.4",
85
85
  "chokidar": "^5.0.0",
86
86
  "defu": "^6.1.4",