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 +4 -2
- package/dist/node/collect/error.js +2 -1
- package/dist/node/collect/index.js +2 -1
- package/dist/node/context.js +11 -3
- package/dist/node/dom/env.d.ts +2 -1
- package/dist/node/dom/env.js +24 -9
- package/dist/node/server.js +39 -15
- package/dist/node/util/escape-regexp.d.ts +8 -0
- package/dist/node/util/escape-regexp.js +10 -0
- package/dist/node/util/is-error.d.ts +7 -0
- package/dist/node/util/is-error.js +10 -0
- package/dist/node/vendors.d.ts +42 -2
- package/dist/node/vendors.js +92 -17
- package/dist/node/virtual/resolved-stories.d.ts +6 -0
- package/dist/node/virtual/resolved-stories.js +8 -0
- package/dist/node/vite.d.ts +7 -1
- package/dist/node/vite.js +29 -22
- package/package.json +9 -7
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
|
|
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 `>=
|
|
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 (
|
|
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(
|
|
28
|
+
new RegExp(escapeRegExp(path.resolve(TEMP_PATH, 'plugins'))),
|
|
28
29
|
],
|
|
29
30
|
transformMode: ctx.config.viteNodeTransformMode,
|
|
30
31
|
});
|
package/dist/node/context.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
package/dist/node/dom/env.d.ts
CHANGED
|
@@ -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;
|
package/dist/node/dom/env.js
CHANGED
|
@@ -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((
|
|
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
|
-
|
|
123
|
-
|
|
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)) {
|
package/dist/node/server.js
CHANGED
|
@@ -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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/node/vendors.d.ts
CHANGED
|
@@ -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
|
|
58
|
-
*
|
|
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[];
|
package/dist/node/vendors.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
96
|
-
*
|
|
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
|
-
//
|
|
113
|
-
// happens
|
|
114
|
-
//
|
|
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',
|
|
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
|
-
|
|
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.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import type {
|
|
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
|
-
|
|
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(
|
|
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(`^${
|
|
425
|
+
find: new RegExp(`^${escapeRegExp(name)}$`),
|
|
419
426
|
replacement: `@poveste/vendors/${entry}`,
|
|
420
427
|
});
|
|
421
428
|
acc.push({
|
|
422
|
-
find: new RegExp(`^${
|
|
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.
|
|
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": ">=
|
|
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.
|
|
79
|
-
"@poveste/controls": "^0.
|
|
80
|
-
"@poveste/shared": "^0.
|
|
81
|
-
"@poveste/vendors": "^0.
|
|
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",
|