poveste 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin.mjs +19 -1
- package/dist/node/build-counts.d.ts +11 -1
- package/dist/node/build-counts.js +24 -3
- package/dist/node/build.js +2 -2
- package/dist/node/collect/define-globals.js +7 -22
- package/dist/node/collect/error.d.ts +2 -0
- package/dist/node/collect/error.js +70 -0
- package/dist/node/collect/index.js +15 -43
- package/dist/node/collect/module-server.d.ts +4 -0
- package/dist/node/collect/module-server.js +3 -3
- package/dist/node/collect/pool.d.ts +14 -0
- package/dist/node/collect/pool.js +106 -0
- package/dist/node/collect/rpc.d.ts +7 -3
- package/dist/node/collect/rpc.js +26 -11
- package/dist/node/collect/runner.js +4 -14
- package/dist/node/collect/task.d.ts +14 -0
- package/dist/node/collect/task.js +81 -0
- package/dist/node/collect/types.d.ts +10 -0
- package/dist/node/collect/types.js +1 -0
- package/dist/node/collect/worker.d.ts +1 -14
- package/dist/node/collect/worker.js +25 -16
- package/dist/node/colors.js +8 -1
- package/dist/node/plugin.d.ts +1 -1
- package/dist/node/virtual/resolved-theme.js +10 -4
- package/dist/node/vite.js +12 -13
- package/node-floor.mjs +87 -0
- package/package.json +15 -14
- package/dist/node/__tests__/dev-cleanup/poveste.config.d.ts +0 -7
- package/dist/node/__tests__/dev-cleanup/poveste.config.js +0 -15
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ 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
|
|
package/bin.mjs
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict'
|
|
3
3
|
|
|
4
|
-
import '
|
|
4
|
+
import process from 'node:process'
|
|
5
|
+
import { supportedNode } from './node-floor.mjs'
|
|
6
|
+
|
|
7
|
+
// Before the CLI is imported, not inside it: on a Node below the floor the import
|
|
8
|
+
// itself is one of the things that can fail, and it fails naming neither Node nor
|
|
9
|
+
// Poveste — story collection loads jsdom, which answers `webidl.util
|
|
10
|
+
// .markAsUncloneable is not a function` on Node 20 (#913).
|
|
11
|
+
//
|
|
12
|
+
// Imported dynamically for the same reason. A static import is parsed with this
|
|
13
|
+
// module, before a line of it runs, so syntax `dist/` may carry would win the race
|
|
14
|
+
// against the message explaining it.
|
|
15
|
+
if (supportedNode()) {
|
|
16
|
+
import('./dist/node/bin.js').catch((error) => {
|
|
17
|
+
// `exitCode`, not `exit`: the error is the last thing written, and forcing the
|
|
18
|
+
// process out on the same tick truncates it on a pipe.
|
|
19
|
+
process.exitCode = 1
|
|
20
|
+
console.error(error)
|
|
21
|
+
})
|
|
22
|
+
}
|
|
@@ -53,4 +53,14 @@ export declare function builtSomething(counts: BuildCounts): boolean;
|
|
|
53
53
|
* Documents count as content: a book of `.story.md` pages with no variants has
|
|
54
54
|
* something in it, and telling its author nothing was found would be wrong.
|
|
55
55
|
*/
|
|
56
|
-
export declare function buildSummary(counts: BuildCounts, seconds: number, storyMatch: string[]): string[];
|
|
56
|
+
export declare function buildSummary(counts: BuildCounts, seconds: number, storyMatch: string[], guideUrl?: string): string[];
|
|
57
|
+
/**
|
|
58
|
+
* The page that shows a story in the framework this book uses.
|
|
59
|
+
*
|
|
60
|
+
* The message used to send everyone to `/guide/`, which explains what a story is
|
|
61
|
+
* for a second time rather than showing one — two hops of misdirection for a
|
|
62
|
+
* reader who has just been told they have no stories (#905). Nuxt and Quasar
|
|
63
|
+
* write Vue stories, SvelteKit writes Svelte ones, so the plugin names map onto
|
|
64
|
+
* the two pages that exist.
|
|
65
|
+
*/
|
|
66
|
+
export declare function storyGuideUrl(pluginNames: string[]): string;
|
|
@@ -60,7 +60,7 @@ export function builtSomething(counts) {
|
|
|
60
60
|
* Documents count as content: a book of `.story.md` pages with no variants has
|
|
61
61
|
* something in it, and telling its author nothing was found would be wrong.
|
|
62
62
|
*/
|
|
63
|
-
export function buildSummary(counts, seconds, storyMatch) {
|
|
63
|
+
export function buildSummary(counts, seconds, storyMatch, guideUrl = 'https://poveste.dev/guide/getting-started') {
|
|
64
64
|
const took = `in ${seconds}s`;
|
|
65
65
|
const globs = storyMatch.join(', ');
|
|
66
66
|
if (!builtSomething(counts)) {
|
|
@@ -68,12 +68,12 @@ export function buildSummary(counts, seconds, storyMatch) {
|
|
|
68
68
|
const files = `${counts.empty.length} file${counts.empty.length === 1 ? '' : 's'}`;
|
|
69
69
|
return [
|
|
70
70
|
`Built 0 stories ${took} — ${files} matched ${globs} and produced no story`,
|
|
71
|
-
|
|
71
|
+
`A story file needs a <Story> in it: ${guideUrl}`,
|
|
72
72
|
];
|
|
73
73
|
}
|
|
74
74
|
return [
|
|
75
75
|
`Built 0 stories ${took} — nothing matched ${globs}`,
|
|
76
|
-
|
|
76
|
+
`Write a story file and run this again: ${guideUrl}`,
|
|
77
77
|
];
|
|
78
78
|
}
|
|
79
79
|
const stories = `${counts.stories} stor${counts.stories === 1 ? 'y' : 'ies'}`;
|
|
@@ -81,3 +81,24 @@ export function buildSummary(counts, seconds, storyMatch) {
|
|
|
81
81
|
const docs = counts.docs ? ` and ${counts.docs} document${counts.docs === 1 ? '' : 's'}` : '';
|
|
82
82
|
return [`✅ Built ${stories} (${variants})${docs} ${took}`];
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* The page that shows a story in the framework this book uses.
|
|
86
|
+
*
|
|
87
|
+
* The message used to send everyone to `/guide/`, which explains what a story is
|
|
88
|
+
* for a second time rather than showing one — two hops of misdirection for a
|
|
89
|
+
* reader who has just been told they have no stories (#905). Nuxt and Quasar
|
|
90
|
+
* write Vue stories, SvelteKit writes Svelte ones, so the plugin names map onto
|
|
91
|
+
* the two pages that exist.
|
|
92
|
+
*/
|
|
93
|
+
export function storyGuideUrl(pluginNames) {
|
|
94
|
+
const vue = ['@poveste/plugin-vue', '@poveste/plugin-nuxt', '@poveste/plugin-quasar'];
|
|
95
|
+
const svelte = ['@poveste/plugin-svelte'];
|
|
96
|
+
if (pluginNames.some(name => svelte.includes(name))) {
|
|
97
|
+
return 'https://poveste.dev/guide/svelte/stories';
|
|
98
|
+
}
|
|
99
|
+
if (pluginNames.some(name => vue.includes(name))) {
|
|
100
|
+
return 'https://poveste.dev/guide/vue/stories';
|
|
101
|
+
}
|
|
102
|
+
// A book with neither, such as vanilla: the framework list is the next step.
|
|
103
|
+
return 'https://poveste.dev/guide/getting-started';
|
|
104
|
+
}
|
package/dist/node/build.js
CHANGED
|
@@ -5,7 +5,7 @@ import { join } from 'pathe';
|
|
|
5
5
|
import pc from 'picocolors';
|
|
6
6
|
import { createServer as createViteServer, mergeConfig as mergeViteConfig, build as viteBuild, } from 'vite';
|
|
7
7
|
import { APP_PATH } from './alias.js';
|
|
8
|
-
import { buildCounts, buildSummary, builtSomething } from './build-counts.js';
|
|
8
|
+
import { buildCounts, buildSummary, builtSomething, storyGuideUrl } from './build-counts.js';
|
|
9
9
|
import { getSerializedStoryData } from './build-serialize.js';
|
|
10
10
|
import { useCollectStories } from './collect/index.js';
|
|
11
11
|
import { useModuleLoader } from './load.js';
|
|
@@ -286,7 +286,7 @@ export async function build(ctx) {
|
|
|
286
286
|
// Green only when something was built. A tick over nothing is the claim
|
|
287
287
|
// #624 is about, and it is the first command the guide asks for.
|
|
288
288
|
const paint = builtSomething(counts) ? pc.green : pc.yellow;
|
|
289
|
-
for (const line of buildSummary(counts, Math.round(duration / 1000 * 100) / 100, ctx.config.storyMatch)) {
|
|
289
|
+
for (const line of buildSummary(counts, Math.round(duration / 1000 * 100) / 100, ctx.config.storyMatch, storyGuideUrl(ctx.config.plugins.map(plugin => plugin.name)))) {
|
|
290
290
|
console.log(paint(line));
|
|
291
291
|
}
|
|
292
292
|
// Render
|
|
@@ -1,26 +1,11 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// The fix is to give the collection environment the same values a real build would
|
|
8
|
-
// substitute, taken from Vite's own resolved `define` rather than a list of library
|
|
9
|
-
// names this package would have to keep current.
|
|
10
|
-
// Only `__FLAG__`-shaped keys are seeded. Every flag this exists for follows that
|
|
11
|
-
// convention — `__VUE_OPTIONS_API__`, `__VUE_PROD_DEVTOOLS__`, `__VUE_I18N_LEGACY_API__`,
|
|
12
|
-
// `__INTLIFY_PROD_DEVTOOLS__` — and narrow is the right failure here. A browser-targeted
|
|
13
|
-
// config shimming a Node global (`define: { process: '{"env":{}}' }` is a real shape)
|
|
14
|
-
// would otherwise replace the collection worker's own `process`, surfacing far from the
|
|
15
|
-
// config that caused it. Refusing to evaluate arbitrary expressions while letting
|
|
16
|
-
// arbitrary identifiers overwrite the runtime would be a guard in name only.
|
|
17
|
-
// `process.env.NODE_ENV` and friends fail the same test, being member expressions.
|
|
1
|
+
// Externalised deps load through native Node ESM, which Vite never applies `define`
|
|
2
|
+
// to, so a compile-time flag is an undefined identifier in a collection worker —
|
|
3
|
+
// vue-i18n reading `__VUE_PROD_DEVTOOLS__` was the reported case (#284).
|
|
4
|
+
// Narrow on purpose: a browser-targeted `define: { process: '{"env":{}}' }` would
|
|
5
|
+
// otherwise overwrite the worker's own `process`, far from the config that caused it.
|
|
18
6
|
const FEATURE_FLAG = /^__[A-Z0-9_]+__$/;
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
// string `"false"` — and seeding that string would make the flag *truthy*, silently
|
|
22
|
-
// inverting it. So strings are parsed as literals, and anything that is not a literal
|
|
23
|
-
// is skipped: evaluating arbitrary expressions here is not something this should do.
|
|
7
|
+
// `define` values are expression *source*, so a user's `define: { X: 'false' }` arrives
|
|
8
|
+
// as the string `"false"` and seeding it verbatim would invert the flag.
|
|
24
9
|
export function globalsFromDefine(define) {
|
|
25
10
|
const globals = {};
|
|
26
11
|
for (const [key, value] of Object.entries(define ?? {})) {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Structured clone keeps an `Error`'s `name`, `message` and `stack` and drops every
|
|
3
|
+
* own property beyond them, so the code frame a Vite transform error puts on `frame`
|
|
4
|
+
* never once survived the worker boundary (#1020).
|
|
5
|
+
*/
|
|
6
|
+
const MARK = 'pvt:error';
|
|
7
|
+
function isSerialized(value) {
|
|
8
|
+
return typeof value === 'object' && value !== null && value.kind === MARK;
|
|
9
|
+
}
|
|
10
|
+
// A property the clone rejects takes the whole message down with it, which is worse
|
|
11
|
+
// than dropping the property.
|
|
12
|
+
function cloneable(value) {
|
|
13
|
+
try {
|
|
14
|
+
structuredClone(value);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/*
|
|
22
|
+
* `seen` breaks a cause that points back into its own chain. `structuredClone`
|
|
23
|
+
* carries a cycle happily; this walk is the only thing here that cannot, and it
|
|
24
|
+
* runs on the path that reports a failure — so overflowing the stack would lose
|
|
25
|
+
* the error it was called to describe and replace it with its own.
|
|
26
|
+
*/
|
|
27
|
+
export function serializeError(error, seen = new WeakSet()) {
|
|
28
|
+
if (!(error instanceof Error)) {
|
|
29
|
+
return cloneable(error) ? error : String(error);
|
|
30
|
+
}
|
|
31
|
+
if (seen.has(error)) {
|
|
32
|
+
// Named and described, without the cause that leads back here.
|
|
33
|
+
return { kind: MARK, name: error.name, message: error.message, stack: error.stack, props: {} };
|
|
34
|
+
}
|
|
35
|
+
seen.add(error);
|
|
36
|
+
const props = {};
|
|
37
|
+
for (const key of Object.getOwnPropertyNames(error)) {
|
|
38
|
+
if (key === 'name' || key === 'message' || key === 'stack') {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const value = error[key];
|
|
42
|
+
if (key === 'cause') {
|
|
43
|
+
props[key] = serializeError(value, seen);
|
|
44
|
+
}
|
|
45
|
+
else if (cloneable(value)) {
|
|
46
|
+
props[key] = value;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { kind: MARK, name: error.name, message: error.message, stack: error.stack, props };
|
|
50
|
+
}
|
|
51
|
+
export function deserializeError(value) {
|
|
52
|
+
if (!isSerialized(value)) {
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
const error = new Error(value.message);
|
|
56
|
+
error.name = value.name;
|
|
57
|
+
// Assigning `undefined` would erase the stack `new Error` just gave this one.
|
|
58
|
+
if (value.stack !== undefined) {
|
|
59
|
+
error.stack = value.stack;
|
|
60
|
+
}
|
|
61
|
+
for (const [key, prop] of Object.entries(value.props)) {
|
|
62
|
+
Object.defineProperty(error, key, {
|
|
63
|
+
value: key === 'cause' ? deserializeError(prop) : prop,
|
|
64
|
+
writable: true,
|
|
65
|
+
enumerable: true,
|
|
66
|
+
configurable: true,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return error;
|
|
70
|
+
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { cpus } from 'node:os';
|
|
2
|
-
import { MessageChannel } from 'node:worker_threads';
|
|
3
|
-
import Tinypool from '@akryum/tinypool';
|
|
4
2
|
import { escapeRegExp } from '@poveste/shared';
|
|
5
3
|
import path, { relative } from 'pathe';
|
|
6
4
|
import pc from 'picocolors';
|
|
@@ -9,16 +7,14 @@ import { createPath } from '../tree.js';
|
|
|
9
7
|
import { slash } from '../util/fs.js';
|
|
10
8
|
import { globalsFromDefine } from './define-globals.js';
|
|
11
9
|
import { createModuleServer } from './module-server.js';
|
|
12
|
-
import {
|
|
10
|
+
import { createPool } from './pool.js';
|
|
11
|
+
import { INVALIDATE, INVALIDATE_ALL } from './task.js';
|
|
13
12
|
export function useCollectStories(options, ctx) {
|
|
14
13
|
const { server, mainServer } = options;
|
|
15
14
|
const node = createModuleServer(server, {
|
|
16
15
|
inline: [
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// `virtual:` imports; otherwise they're loaded via native Node ESM,
|
|
20
|
-
// which throws ERR_UNSUPPORTED_ESM_URL_SCHEME on `virtual:` and breaks
|
|
21
|
-
// story collection for any fresh npm install.
|
|
16
|
+
// Must be inlined: loaded through native Node ESM instead, their `virtual:`
|
|
17
|
+
// imports throw ERR_UNSUPPORTED_ESM_URL_SCHEME on any fresh npm install.
|
|
22
18
|
/\/poveste\/dist/,
|
|
23
19
|
/\/poveste\/client/,
|
|
24
20
|
/@poveste\/[\w-]+\/dist/,
|
|
@@ -33,64 +29,44 @@ export function useCollectStories(options, ctx) {
|
|
|
33
29
|
],
|
|
34
30
|
transformMode: ctx.config.viteNodeTransformMode,
|
|
35
31
|
});
|
|
36
|
-
// Same values a real build substitutes, so externalised deps see their flags.
|
|
37
32
|
const defineGlobals = globalsFromDefine(server.config.define);
|
|
38
33
|
const maxThreads = ctx.config.collectMaxThreads ?? cpus().length;
|
|
39
34
|
const threadsCount = ctx.mode === 'dev'
|
|
40
35
|
? Math.max(Math.min(maxThreads, Math.floor(cpus().length / 2)), 1)
|
|
41
36
|
: Math.max(Math.min(maxThreads, cpus().length - 1), 1);
|
|
42
37
|
console.log(pc.blue(`Using ${threadsCount} thread${threadsCount === 1 ? '' : 's'} for story collection`));
|
|
43
|
-
const threadPool =
|
|
44
|
-
filename: new URL('./worker.js', import.meta.url)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
minThreads: threadsCount,
|
|
48
|
-
maxThreads: threadsCount,
|
|
38
|
+
const threadPool = createPool({
|
|
39
|
+
filename: new URL('./worker.js', import.meta.url),
|
|
40
|
+
threads: threadsCount,
|
|
41
|
+
invoke: (name, data) => node.invoke(name, data),
|
|
49
42
|
});
|
|
50
43
|
function clearCache() {
|
|
51
44
|
server.moduleGraph.invalidateAll();
|
|
52
45
|
node.clearCache();
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const workerPort = channel.port1;
|
|
58
|
-
serveInvoke(port, (name, data) => node.invoke(name, data));
|
|
59
|
-
return {
|
|
60
|
-
port,
|
|
61
|
-
workerPort,
|
|
62
|
-
};
|
|
46
|
+
// Both of those make every module read again, and a worker told nothing would
|
|
47
|
+
// discover that one at a time, dropping each as some story asks for it — which
|
|
48
|
+
// is how one run ends up holding two instances of a module.
|
|
49
|
+
threadPool.broadcast({ kind: INVALIDATE_ALL });
|
|
63
50
|
}
|
|
64
51
|
if (mainServer) {
|
|
65
52
|
mainServer.watcher.on('change', (file) => {
|
|
66
53
|
file = slash(file);
|
|
67
|
-
threadPool.
|
|
68
|
-
kind:
|
|
54
|
+
threadPool.broadcast({
|
|
55
|
+
kind: INVALIDATE,
|
|
69
56
|
file,
|
|
70
57
|
});
|
|
71
58
|
});
|
|
72
59
|
}
|
|
73
60
|
let destroying = false;
|
|
74
61
|
async function executeStoryFile(storyFile) {
|
|
75
|
-
// The channel belongs to this execution, and nothing used to close it. On a
|
|
76
|
-
// build that fails, the executions still in flight are abandoned mid-run and
|
|
77
|
-
// their main-thread ports stay open and listening — enough live handles to
|
|
78
|
-
// keep the process alive with nothing left to do (#426).
|
|
79
|
-
let channel;
|
|
80
62
|
try {
|
|
81
|
-
channel = createChannel();
|
|
82
63
|
const payload = {
|
|
83
64
|
root: server.config.root,
|
|
84
65
|
base: server.config.base,
|
|
85
66
|
storyFile,
|
|
86
|
-
port: channel.workerPort,
|
|
87
67
|
defineGlobals,
|
|
88
68
|
};
|
|
89
|
-
const { storyData } = await threadPool.run(payload
|
|
90
|
-
transferList: [
|
|
91
|
-
channel.workerPort,
|
|
92
|
-
],
|
|
93
|
-
});
|
|
69
|
+
const { storyData } = await threadPool.run(payload);
|
|
94
70
|
const finalData = storyData[0];
|
|
95
71
|
if (!finalData) {
|
|
96
72
|
console.warn(pc.yellow(`⚠️ No story found for ${storyFile.path}`));
|
|
@@ -99,7 +75,6 @@ export function useCollectStories(options, ctx) {
|
|
|
99
75
|
else if (storyData.length > 1) {
|
|
100
76
|
console.warn(pc.yellow(`⚠️ Multiple stories not supported: ${storyFile.path}`));
|
|
101
77
|
}
|
|
102
|
-
// Default props
|
|
103
78
|
if (ctx.config.defaultStoryProps) {
|
|
104
79
|
for (const [key, value] of Object.entries(ctx.config.defaultStoryProps)) {
|
|
105
80
|
if (Reflect.get(finalData, key) == null) {
|
|
@@ -132,9 +107,6 @@ export function useCollectStories(options, ctx) {
|
|
|
132
107
|
throw e;
|
|
133
108
|
}
|
|
134
109
|
}
|
|
135
|
-
finally {
|
|
136
|
-
channel?.port.close();
|
|
137
|
-
}
|
|
138
110
|
}
|
|
139
111
|
async function destroy() {
|
|
140
112
|
destroying = true;
|
|
@@ -7,6 +7,10 @@ export interface ModuleServerOptions {
|
|
|
7
7
|
web?: RegExp[];
|
|
8
8
|
} | undefined;
|
|
9
9
|
}
|
|
10
|
+
export declare function isInternalRequest(id: string): boolean;
|
|
11
|
+
/** A request id as the transformed code wrote it, back to a module id. */
|
|
12
|
+
export declare function normalizeRequestId(id: string, base?: string): string;
|
|
13
|
+
export declare function matches(id: string, patterns: (string | RegExp)[] | undefined): boolean;
|
|
10
14
|
export interface ModuleServer {
|
|
11
15
|
invoke: (name: string, data: unknown[]) => Promise<unknown>;
|
|
12
16
|
clearCache: () => void;
|
|
@@ -37,11 +37,11 @@ function withTrailingSlash(path) {
|
|
|
37
37
|
function cleanUrl(url) {
|
|
38
38
|
return url.replace(/[?#].*$/, '');
|
|
39
39
|
}
|
|
40
|
-
function isInternalRequest(id) {
|
|
40
|
+
export function isInternalRequest(id) {
|
|
41
41
|
return /^\/?@vite\/(?:client|env)$/.test(id);
|
|
42
42
|
}
|
|
43
43
|
/** A request id as the transformed code wrote it, back to a module id. */
|
|
44
|
-
function normalizeRequestId(id, base) {
|
|
44
|
+
export function normalizeRequestId(id, base) {
|
|
45
45
|
if (base && base !== '/' && id.startsWith(withTrailingSlash(base))) {
|
|
46
46
|
id = `/${id.slice(base.length)}`;
|
|
47
47
|
}
|
|
@@ -93,7 +93,7 @@ function toFilePath(id, root) {
|
|
|
93
93
|
exists,
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
|
-
function matches(id, patterns) {
|
|
96
|
+
export function matches(id, patterns) {
|
|
97
97
|
return (patterns ?? []).some(pattern => typeof pattern === 'string'
|
|
98
98
|
? MODULE_DIRECTORIES.some(dir => id.includes(join(dir, pattern)))
|
|
99
99
|
: pattern.test(id));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Invoke } from './runner.js';
|
|
2
|
+
export interface PoolOptions {
|
|
3
|
+
filename: string | URL;
|
|
4
|
+
threads: number;
|
|
5
|
+
/** Answers the `invoke` calls workers make while they run. */
|
|
6
|
+
invoke: Invoke;
|
|
7
|
+
}
|
|
8
|
+
export interface Pool<P, R> {
|
|
9
|
+
run: (payload: P) => Promise<R>;
|
|
10
|
+
/** Reaches every worker, busy ones included. */
|
|
11
|
+
broadcast: (message: unknown) => void;
|
|
12
|
+
destroy: () => Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export declare function createPool<P, R>(options: PoolOptions): Pool<P, R>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Worker } from 'node:worker_threads';
|
|
2
|
+
import { deserializeError } from './error.js';
|
|
3
|
+
import { serveInvoke } from './rpc.js';
|
|
4
|
+
import { DONE, FAILED, TASK } from './task.js';
|
|
5
|
+
export function createPool(options) {
|
|
6
|
+
const queue = [];
|
|
7
|
+
const idle = [];
|
|
8
|
+
const workers = [];
|
|
9
|
+
let destroyed = false;
|
|
10
|
+
let nextId = 0;
|
|
11
|
+
for (let i = 0; i < options.threads; i++) {
|
|
12
|
+
const worker = new Worker(options.filename, { stdout: false, stderr: false });
|
|
13
|
+
const handle = { worker, current: undefined };
|
|
14
|
+
serveInvoke(worker, options.invoke);
|
|
15
|
+
worker.on('message', (message) => {
|
|
16
|
+
if (message?.kind !== DONE && message?.kind !== FAILED) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const task = handle.current;
|
|
20
|
+
// A late or repeated answer is for a task this worker has already finished,
|
|
21
|
+
// and without the id it would settle whichever one it is running now.
|
|
22
|
+
if (!task || message.id !== task.id) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
handle.current = undefined;
|
|
26
|
+
if (message.kind === DONE) {
|
|
27
|
+
task.resolve(message.result);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
task.reject(deserializeError(message.error));
|
|
31
|
+
}
|
|
32
|
+
release(handle);
|
|
33
|
+
});
|
|
34
|
+
// A worker that dies takes its task with it. Without this the story's
|
|
35
|
+
// promise never settles and the build hangs rather than failing.
|
|
36
|
+
worker.on('error', (error) => {
|
|
37
|
+
const task = handle.current;
|
|
38
|
+
handle.current = undefined;
|
|
39
|
+
task?.reject(error);
|
|
40
|
+
drop(handle);
|
|
41
|
+
});
|
|
42
|
+
worker.on('exit', () => {
|
|
43
|
+
const task = handle.current;
|
|
44
|
+
handle.current = undefined;
|
|
45
|
+
task?.reject(new Error('[poveste] a collection worker exited before its story finished'));
|
|
46
|
+
drop(handle);
|
|
47
|
+
});
|
|
48
|
+
workers.push(handle);
|
|
49
|
+
idle.push(handle);
|
|
50
|
+
}
|
|
51
|
+
function drop(handle) {
|
|
52
|
+
remove(workers, handle);
|
|
53
|
+
remove(idle, handle);
|
|
54
|
+
// Nothing is left to run the queue, so fail it rather than let it wait.
|
|
55
|
+
if (workers.length === 0) {
|
|
56
|
+
for (const task of queue.splice(0)) {
|
|
57
|
+
task.reject(new Error('[poveste] every collection worker is gone'));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function remove(list, handle) {
|
|
62
|
+
const at = list.indexOf(handle);
|
|
63
|
+
if (at !== -1) {
|
|
64
|
+
list.splice(at, 1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function release(handle) {
|
|
68
|
+
const next = queue.shift();
|
|
69
|
+
if (next) {
|
|
70
|
+
dispatch(handle, next);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
idle.push(handle);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function dispatch(handle, task) {
|
|
77
|
+
handle.current = task;
|
|
78
|
+
handle.worker.postMessage({ kind: TASK, id: task.id, payload: task.payload });
|
|
79
|
+
}
|
|
80
|
+
function run(payload) {
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
if (destroyed) {
|
|
83
|
+
reject(new Error('[poveste] the collection pool is destroyed'));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const task = { id: nextId++, payload, resolve, reject };
|
|
87
|
+
const handle = idle.shift();
|
|
88
|
+
if (handle) {
|
|
89
|
+
dispatch(handle, task);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
queue.push(task);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function broadcast(message) {
|
|
97
|
+
for (const handle of workers) {
|
|
98
|
+
handle.worker.postMessage(message);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function destroy() {
|
|
102
|
+
destroyed = true;
|
|
103
|
+
await Promise.all(workers.slice().map(handle => handle.worker.terminate()));
|
|
104
|
+
}
|
|
105
|
+
return { run, broadcast, destroy };
|
|
106
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import type { MessagePort } from 'node:worker_threads';
|
|
2
1
|
import type { Invoke } from './runner.js';
|
|
2
|
+
/** A `MessagePort` or a `Worker` — the two ends this runs over. */
|
|
3
|
+
export interface PortLike {
|
|
4
|
+
on: (event: 'message', listener: (value: any) => void) => unknown;
|
|
5
|
+
postMessage: (value: any) => void;
|
|
6
|
+
}
|
|
3
7
|
/** Answers `invoke` requests arriving on `port`. */
|
|
4
|
-
export declare function serveInvoke(port:
|
|
8
|
+
export declare function serveInvoke(port: PortLike, invoke: Invoke): void;
|
|
5
9
|
/** An `invoke` that sends its calls over `port`. */
|
|
6
|
-
export declare function invokeOver(port:
|
|
10
|
+
export declare function invokeOver(port: PortLike): Invoke;
|
package/dist/node/collect/rpc.js
CHANGED
|
@@ -1,21 +1,29 @@
|
|
|
1
|
+
import { deserializeError, serializeError } from './error.js';
|
|
1
2
|
/*
|
|
2
|
-
* The one call a collection worker makes to the main thread
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* The one call a collection worker makes to the main thread. It replaced birpc
|
|
4
|
+
* (#344): after #167 the channel carries a single method, and a dependency that
|
|
5
|
+
* ships in `poveste` was three majors behind to carry it.
|
|
6
|
+
*
|
|
7
|
+
* Task dispatch shares the channel (#1020), so both sides tag what they send.
|
|
6
8
|
*/
|
|
7
9
|
// What birpc waited before rejecting a call, kept so a stuck transform still
|
|
8
10
|
// fails the story rather than holding its worker forever.
|
|
9
11
|
const TIMEOUT_MS = 60_000;
|
|
12
|
+
const REQUEST = 'pvt:invoke';
|
|
13
|
+
const RESPONSE = 'pvt:invoked';
|
|
10
14
|
/** Answers `invoke` requests arriving on `port`. */
|
|
11
15
|
export function serveInvoke(port, invoke) {
|
|
12
|
-
port.on('message', async (
|
|
16
|
+
port.on('message', async (message) => {
|
|
17
|
+
if (message?.kind !== REQUEST) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const { id, name, data } = message;
|
|
13
21
|
let response;
|
|
14
22
|
try {
|
|
15
|
-
response = { id, result: await invoke(name, data) };
|
|
23
|
+
response = { kind: RESPONSE, id, result: await invoke(name, data) };
|
|
16
24
|
}
|
|
17
25
|
catch (error) {
|
|
18
|
-
response = { id, error };
|
|
26
|
+
response = { kind: RESPONSE, id, error: serializeError(error) };
|
|
19
27
|
}
|
|
20
28
|
port.postMessage(response);
|
|
21
29
|
});
|
|
@@ -24,15 +32,22 @@ export function serveInvoke(port, invoke) {
|
|
|
24
32
|
export function invokeOver(port) {
|
|
25
33
|
let nextId = 0;
|
|
26
34
|
const pending = new Map();
|
|
27
|
-
port.on('message', (
|
|
35
|
+
port.on('message', (message) => {
|
|
36
|
+
if (message?.kind !== RESPONSE) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const { id, result, error } = message;
|
|
28
40
|
const call = pending.get(id);
|
|
29
41
|
if (!call) {
|
|
30
42
|
return;
|
|
31
43
|
}
|
|
32
44
|
pending.delete(id);
|
|
33
45
|
clearTimeout(call.timeout);
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
// Presence rather than truthiness: `serveInvoke` sets the key only when the
|
|
47
|
+
// call threw, and a thrown `undefined` or `''` read as no error at all —
|
|
48
|
+
// resolving the runner with `undefined` where it expects a module.
|
|
49
|
+
if ('error' in message) {
|
|
50
|
+
call.reject(deserializeError(error));
|
|
36
51
|
}
|
|
37
52
|
else {
|
|
38
53
|
call.resolve(result);
|
|
@@ -46,6 +61,6 @@ export function invokeOver(port) {
|
|
|
46
61
|
}, TIMEOUT_MS);
|
|
47
62
|
timeout.unref();
|
|
48
63
|
pending.set(id, { resolve, reject, timeout });
|
|
49
|
-
port.postMessage({ id, name, data });
|
|
64
|
+
port.postMessage({ kind: REQUEST, id, name, data });
|
|
50
65
|
});
|
|
51
66
|
}
|
|
@@ -19,22 +19,13 @@ function exportAll(exports, source) {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
-
/**
|
|
23
|
-
* Loads an externalised dependency the way vite-node did, which collection has
|
|
24
|
-
* always relied on: a CommonJS package's named exports are read through its
|
|
25
|
-
* default export, so `import { x } from 'cjs-package'` works whether or not Node's
|
|
26
|
-
* lexer found `x`.
|
|
27
|
-
*/
|
|
22
|
+
/** As vite-node did: a CJS package's named exports are read through its default. */
|
|
28
23
|
class InteropEvaluator {
|
|
29
24
|
// The runner offsets source maps by the lines a wrapper adds, which match Vite's own.
|
|
30
25
|
startOffset = new ESModulesEvaluator().startOffset;
|
|
31
26
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* its import bindings, so a CommonJS dependency a book inlines through
|
|
35
|
-
* `viteNodeInlineDeps` threw `exports is not defined` (#167). What a module
|
|
36
|
-
* assigns to `exports` or `module.exports` becomes its default export and its
|
|
37
|
-
* named exports.
|
|
27
|
+
* Vite's own evaluator passes only import bindings, so a CJS dependency inlined
|
|
28
|
+
* through `viteNodeInlineDeps` threw `exports is not defined` (#167).
|
|
38
29
|
*/
|
|
39
30
|
async runInlinedModule(context, code, module) {
|
|
40
31
|
const exports = context[ssrModuleExportsKey];
|
|
@@ -102,8 +93,7 @@ class InteropEvaluator {
|
|
|
102
93
|
});
|
|
103
94
|
}
|
|
104
95
|
}
|
|
105
|
-
//
|
|
106
|
-
// booleans. The runner's default throws on any key the transform did not replace.
|
|
96
|
+
// The runner's own default throws on any key the transform did not replace.
|
|
107
97
|
const env = new Proxy(process.env, {
|
|
108
98
|
get: (target, key) => typeof key !== 'string'
|
|
109
99
|
? undefined
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PortLike } from './rpc.js';
|
|
2
|
+
import type { Payload, ReturnData } from './types.js';
|
|
3
|
+
export declare const TASK = "pvt:task";
|
|
4
|
+
export declare const DONE = "pvt:done";
|
|
5
|
+
export declare const FAILED = "pvt:failed";
|
|
6
|
+
export declare const INVALIDATE = "hst:invalidate";
|
|
7
|
+
export declare const INVALIDATE_ALL = "pvt:invalidate-all";
|
|
8
|
+
export interface TaskHandlers {
|
|
9
|
+
invalidate: (file: string) => void;
|
|
10
|
+
invalidateAll: () => void;
|
|
11
|
+
collect: (payload: Payload) => Promise<ReturnData>;
|
|
12
|
+
}
|
|
13
|
+
/** Answers task dispatch and invalidation arriving on `port`. */
|
|
14
|
+
export declare function serveTasks(port: PortLike, handlers: TaskHandlers): void;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { serializeError } from './error.js';
|
|
2
|
+
/*
|
|
3
|
+
* The task half of what a collection worker answers on its port (#1020). Split
|
|
4
|
+
* from `worker.ts` because that module throws outside a worker thread, which
|
|
5
|
+
* leaves this — where a mis-routed message loses a story — unreachable from a test.
|
|
6
|
+
*/
|
|
7
|
+
export const TASK = 'pvt:task';
|
|
8
|
+
export const DONE = 'pvt:done';
|
|
9
|
+
export const FAILED = 'pvt:failed';
|
|
10
|
+
/* The watcher's, and older than the rest — the wire value is what `index.ts`
|
|
11
|
+
has always sent. Shared so a rename cannot land on one end only: an unknown
|
|
12
|
+
kind is dropped in silence by design, so a typo here stops invalidation
|
|
13
|
+
without failing anything. */
|
|
14
|
+
export const INVALIDATE = 'hst:invalidate';
|
|
15
|
+
export const INVALIDATE_ALL = 'pvt:invalidate-all';
|
|
16
|
+
/** Answers task dispatch and invalidation arriving on `port`. */
|
|
17
|
+
export function serveTasks(port, handlers) {
|
|
18
|
+
// Invalidations that arrived while a story was running, held until none is.
|
|
19
|
+
const deferred = new Set();
|
|
20
|
+
let dropEverything = false;
|
|
21
|
+
// A count rather than a flag: nothing here stops a second task arriving before
|
|
22
|
+
// the first settles — the pool sends one at a time, and the spec beside this
|
|
23
|
+
// one drives two deliberately — and under a flag the second task's own drain
|
|
24
|
+
// would invalidate modules the first is still holding, which is the thing this
|
|
25
|
+
// whole file is arranging not to do.
|
|
26
|
+
let running = 0;
|
|
27
|
+
port.on('message', (message) => {
|
|
28
|
+
// Sent once per collection pass, when the server clears its own cache. Every
|
|
29
|
+
// module is read again either way; applying it here rather than one module at
|
|
30
|
+
// a time as each is next asked for is what stops a run straddling the two.
|
|
31
|
+
if (message?.kind === INVALIDATE_ALL) {
|
|
32
|
+
if (running > 0) {
|
|
33
|
+
dropEverything = true;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
handlers.invalidateAll();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (message?.kind === INVALIDATE) {
|
|
40
|
+
// Applied mid-story it re-evaluates modules that story is already using,
|
|
41
|
+
// and the two halves of a framework then disagree about which instance is
|
|
42
|
+
// current: Vue injects a key its provider never used, Svelte reads a
|
|
43
|
+
// component context outside any component. The invalidation is for the
|
|
44
|
+
// *next* run of the file anyway, so it waits for one.
|
|
45
|
+
if (running > 0) {
|
|
46
|
+
deferred.add(message.file);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
handlers.invalidate(message.file);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (message?.kind !== TASK) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const { id } = message;
|
|
56
|
+
// Only with the graph to itself: a task starting beside another cannot take
|
|
57
|
+
// modules out from under it.
|
|
58
|
+
if (running === 0) {
|
|
59
|
+
if (dropEverything) {
|
|
60
|
+
dropEverything = false;
|
|
61
|
+
deferred.clear();
|
|
62
|
+
handlers.invalidateAll();
|
|
63
|
+
}
|
|
64
|
+
for (const file of deferred) {
|
|
65
|
+
handlers.invalidate(file);
|
|
66
|
+
}
|
|
67
|
+
deferred.clear();
|
|
68
|
+
}
|
|
69
|
+
running++;
|
|
70
|
+
const settle = () => {
|
|
71
|
+
running--;
|
|
72
|
+
};
|
|
73
|
+
handlers.collect(message.payload).then((result) => {
|
|
74
|
+
settle();
|
|
75
|
+
port.postMessage({ kind: DONE, id, result });
|
|
76
|
+
}, (error) => {
|
|
77
|
+
settle();
|
|
78
|
+
port.postMessage({ kind: FAILED, id, error: serializeError(error) });
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ServerStory, ServerStoryFile } from '@poveste/shared';
|
|
2
|
+
export interface Payload {
|
|
3
|
+
root: string;
|
|
4
|
+
base: string;
|
|
5
|
+
storyFile: ServerStoryFile;
|
|
6
|
+
defineGlobals?: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
export interface ReturnData {
|
|
9
|
+
storyData: ServerStory[];
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,14 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import type { MessagePort } from 'node:worker_threads';
|
|
3
|
-
export interface Payload {
|
|
4
|
-
root: string;
|
|
5
|
-
base: string;
|
|
6
|
-
port: MessagePort;
|
|
7
|
-
storyFile: ServerStoryFile;
|
|
8
|
-
defineGlobals?: Record<string, unknown>;
|
|
9
|
-
}
|
|
10
|
-
export interface ReturnData {
|
|
11
|
-
storyData: ServerStory[];
|
|
12
|
-
}
|
|
13
|
-
declare const _default: (payload: Payload) => Promise<ReturnData>;
|
|
14
|
-
export default _default;
|
|
1
|
+
export {};
|
|
@@ -7,13 +7,13 @@ import { EvaluatedModules } from 'vite/module-runner';
|
|
|
7
7
|
import { createDomEnv, resetDomEnv } from '../dom/env.js';
|
|
8
8
|
import { invokeOver } from './rpc.js';
|
|
9
9
|
import { createRunner } from './runner.js';
|
|
10
|
+
import { serveTasks } from './task.js';
|
|
10
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
12
|
const _evaluatedModules = new EvaluatedModules();
|
|
12
13
|
let _runner;
|
|
13
14
|
// Worker-lifetime: externalised runtimes cache the DOM they first saw, so one
|
|
14
15
|
// per story broke re-collection.
|
|
15
16
|
let _domEnv;
|
|
16
|
-
let _invoke;
|
|
17
17
|
// A virtual module is cached under the id the plugin resolved it to, not the one
|
|
18
18
|
// it was asked for, so both forms have to go.
|
|
19
19
|
function invalidate(file) {
|
|
@@ -27,28 +27,38 @@ function invalidate(file) {
|
|
|
27
27
|
_evaluatedModules.invalidateModule(node);
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
// The whole graph, dropped at a task boundary. The server clears its own cache
|
|
31
|
+
// once per pass, so this is re-read in that pass anyway — what changes is whether
|
|
32
|
+
// a story can be halfway through when its turn comes.
|
|
33
|
+
//
|
|
34
|
+
// Everything, with nothing held back. Vitest skips its own `dist` from the reset
|
|
35
|
+
// it does between test files, and the same shape here re-broke Svelte: keeping a
|
|
36
|
+
// module while dropping what it imports leaves it holding the dropped instances.
|
|
37
|
+
// Vitest's skipped set imports only itself; the plugin runtime imports the
|
|
38
|
+
// framework, which is in this graph.
|
|
39
|
+
function invalidateAll() {
|
|
40
|
+
for (const node of _evaluatedModules.idToModuleMap.values()) {
|
|
41
|
+
_evaluatedModules.invalidateModule(node);
|
|
34
42
|
}
|
|
35
|
-
}
|
|
36
|
-
|
|
43
|
+
}
|
|
44
|
+
if (!parentPort) {
|
|
45
|
+
throw new Error('[poveste] the collection worker was started outside a worker thread');
|
|
46
|
+
}
|
|
47
|
+
// The port it answers over is the one tasks arrive on, so nothing is handed over
|
|
48
|
+
// per story.
|
|
49
|
+
const _invoke = invokeOver(parentPort);
|
|
50
|
+
serveTasks(parentPort, { invalidate, invalidateAll, collect });
|
|
51
|
+
async function collect(payload) {
|
|
37
52
|
const startTime = performance.now();
|
|
38
53
|
process.env['HST_COLLECT'] = 'true';
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
// a busy worker could start the task first and read its own stale cache. A
|
|
42
|
-
// story being re-executed is being re-read by definition (#557).
|
|
54
|
+
// A story being re-executed is being re-read by definition (#557), and one
|
|
55
|
+
// re-collected without an intervening watcher event gets no broadcast at all.
|
|
43
56
|
invalidate(payload.storyFile.moduleId);
|
|
44
57
|
// Before any module runs: an externalised dep reads these at import time.
|
|
45
58
|
for (const [key, value] of Object.entries(payload.defineGlobals ?? {})) {
|
|
46
59
|
;
|
|
47
60
|
globalThis[key] = value;
|
|
48
61
|
}
|
|
49
|
-
_invoke = invokeOver(payload.port);
|
|
50
|
-
// One runner for the worker's lifetime, reaching the server through whichever
|
|
51
|
-
// port the current task brought.
|
|
52
62
|
const runner = _runner ?? (_runner = createRunner((name, data) => _invoke(name, data), _evaluatedModules));
|
|
53
63
|
if (_domEnv) {
|
|
54
64
|
resetDomEnv(_domEnv);
|
|
@@ -58,7 +68,6 @@ export default async (payload) => {
|
|
|
58
68
|
}
|
|
59
69
|
const el = window.document.createElement('div');
|
|
60
70
|
const beforeExecuteTime = performance.now();
|
|
61
|
-
// Mount app to collect stories/variants
|
|
62
71
|
const { run } = (await runner.import(resolve(__dirname, './run.js')));
|
|
63
72
|
const afterExecuteTime = performance.now();
|
|
64
73
|
const storyData = [];
|
|
@@ -81,4 +90,4 @@ export default async (payload) => {
|
|
|
81
90
|
return {
|
|
82
91
|
storyData,
|
|
83
92
|
};
|
|
84
|
-
}
|
|
93
|
+
}
|
package/dist/node/colors.js
CHANGED
|
@@ -280,7 +280,14 @@ export const defaultColors = {
|
|
|
280
280
|
},
|
|
281
281
|
};
|
|
282
282
|
const HEX = /^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i;
|
|
283
|
-
|
|
283
|
+
/*
|
|
284
|
+
* Four groups, not one group repeated. `([a-f\d]){3,4}` captures only the last
|
|
285
|
+
* character it matched, so the expansion below received `#0f0` as one group and
|
|
286
|
+
* built `#00undefinedundefined` — which `HEX` then rejected, and every shorthand
|
|
287
|
+
* colour a config used came back unreadable (#955).
|
|
288
|
+
*/
|
|
289
|
+
// eslint-disable-next-line regexp/optimal-quantifier-concatenation -- `{1,2}` here is the bug above
|
|
290
|
+
const SHORT_HEX = /^#([a-f\d])([a-f\d])([a-f\d])([a-f\d])?$/i;
|
|
284
291
|
const VALUE = `(?:\\d+|\\d*\\.\\d+)%?`;
|
|
285
292
|
const SEP = `(?:\\s*,\\s*|\\s+)`;
|
|
286
293
|
const ALPHA_SEP = `\\s*[,/]\\s*`;
|
package/dist/node/plugin.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export declare class BasePluginApi implements PluginApiBase {
|
|
|
11
11
|
colors: import("picocolors/types.js").Colors & {
|
|
12
12
|
createColors: (enabled?: boolean) => import("picocolors/types.js").Colors;
|
|
13
13
|
};
|
|
14
|
-
path: import("path")
|
|
14
|
+
path: typeof import("node:path");
|
|
15
15
|
fs: typeof fs;
|
|
16
16
|
constructor(ctx: Context, plugin: Plugin, moduleLoader: ModuleLoader,
|
|
17
17
|
/** Absent where nothing should watch, which is a build. */
|
|
@@ -5,11 +5,17 @@ export function resolvedTheme(ctx) {
|
|
|
5
5
|
const colors = ctx.config.theme?.colors ?? {};
|
|
6
6
|
for (const color in colors) {
|
|
7
7
|
for (const key in colors[color]) {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
const value = colors[color][key];
|
|
9
|
+
// Parsed to be rejected, not to be rebuilt. What goes into the variable is
|
|
10
|
+
// what the config wrote: `parseColor` splits a colour into channels and
|
|
11
|
+
// drops both the alpha and the space it was written in, so rebuilding from
|
|
12
|
+
// its parts silently turned `#10b98180` opaque and `hsl(160 84% 39%)` into
|
|
13
|
+
// `rgb(160 84% 39%)` — which is not a colour, so the declaration was thrown
|
|
14
|
+
// away and the token fell back (#955).
|
|
15
|
+
if (!parseColor(value)) {
|
|
16
|
+
throw new Error(`[poveste] theme.colors.${color}.${key} is not a colour poveste can read: ${JSON.stringify(value)}`);
|
|
11
17
|
}
|
|
12
|
-
css += `--_poveste-color-${color}-${key}: ${
|
|
18
|
+
css += `--_poveste-color-${color}-${key}: ${value};`;
|
|
13
19
|
}
|
|
14
20
|
}
|
|
15
21
|
css += '}';
|
package/dist/node/vite.js
CHANGED
|
@@ -113,16 +113,18 @@ export async function getViteConfigWithPlugins(isServer, ctx) {
|
|
|
113
113
|
// published book is Poveste's own stores, so the 200 KB buys them nothing
|
|
114
114
|
// (#791).
|
|
115
115
|
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
// `@
|
|
119
|
-
//
|
|
116
|
+
// One rule reaches both copies now. A consumer's stories resolve their own
|
|
117
|
+
// pinia and Poveste's stores resolve its own, and since #347 both import
|
|
118
|
+
// `@vue/devtools-api` by name rather than one of them carrying it inside a
|
|
119
|
+
// prebundled chunk — so the name is the whole of it.
|
|
120
120
|
//
|
|
121
|
-
// `apply: 'build'`
|
|
122
|
-
// real module
|
|
121
|
+
// `apply: 'build'` keeps dev registering. Note that it is not what makes the
|
|
122
|
+
// real module reach a dev browser: Vite's dep optimiser bakes it into a chunk
|
|
123
|
+
// before this plugin sees the id, and flipping this to `'serve'` leaves dev
|
|
124
|
+
// serving it regardless. Measured while rewriting the spec that used to claim
|
|
125
|
+
// otherwise.
|
|
123
126
|
if (!isServer) {
|
|
124
127
|
const STUB = '\0poveste:devtools-api';
|
|
125
|
-
const VENDORED = /[\\/](?:poveste-vendors|@poveste[\\/]vendors)[\\/]dist[\\/]client[\\/]devtools-api\.js(?:\?.*)?$/;
|
|
126
128
|
plugins.push({
|
|
127
129
|
name: 'poveste-devtools-api-stub',
|
|
128
130
|
apply: 'build',
|
|
@@ -133,9 +135,9 @@ export async function getViteConfigWithPlugins(isServer, ctx) {
|
|
|
133
135
|
load(id) {
|
|
134
136
|
// Not an empty module: pinia and vue-router *call* this, so the export
|
|
135
137
|
// has to exist or the book throws where it used to register nothing.
|
|
136
|
-
// Both spellings, because
|
|
137
|
-
//
|
|
138
|
-
return id === STUB
|
|
138
|
+
// Both spellings, because pinia and vue-router import different ones of
|
|
139
|
+
// the two the package exports.
|
|
140
|
+
return id === STUB
|
|
139
141
|
? 'export function setupDevtoolsPlugin() {}\nexport { setupDevtoolsPlugin as setupDevToolsPlugin }\n'
|
|
140
142
|
: undefined;
|
|
141
143
|
},
|
|
@@ -389,11 +391,8 @@ export async function getViteConfigWithPlugins(isServer, ctx) {
|
|
|
389
391
|
resolve: {
|
|
390
392
|
alias: [
|
|
391
393
|
...[
|
|
392
|
-
['floating-vue/dist/style.css', 'node_modules/floating-vue/dist/style.css'],
|
|
393
|
-
['floating-vue', 'floating-vue'],
|
|
394
394
|
['@iconify/vue', 'iconify'],
|
|
395
395
|
['pinia', 'pinia'],
|
|
396
|
-
['scroll-into-view-if-needed', 'scroll'],
|
|
397
396
|
['vue-router', 'vue-router'],
|
|
398
397
|
['@vueuse/core', 'vue-use'],
|
|
399
398
|
['vue', 'vue'],
|
package/node-floor.mjs
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// The Node check the CLI makes before it loads anything.
|
|
2
|
+
//
|
|
3
|
+
// `engines.node` is an install-time signal, and #901 is where it works against
|
|
4
|
+
// us: npm answers a Node its range rejects by resolving *backwards*, and the
|
|
5
|
+
// seventeen versions published before this package declared the field (0.1.0 to
|
|
6
|
+
// 0.6.1) accept every Node, so that is where it lands. #907 made the range
|
|
7
|
+
// contiguous, which removes the realistic case; it cannot remove the class,
|
|
8
|
+
// because nothing can be added to a version already published and deprecation
|
|
9
|
+
// does not stop npm choosing one.
|
|
10
|
+
//
|
|
11
|
+
// So the run-time signal is the one that is ours. Vite has carried this for
|
|
12
|
+
// years and its wording is the model (#913).
|
|
13
|
+
//
|
|
14
|
+
// Its own file, in plain ES2015, imported by `bin.mjs` before the CLI: the
|
|
15
|
+
// failure it exists for can be the import of `dist/`, and syntax this file
|
|
16
|
+
// cannot parse on an old Node would be the same defect wearing a different hat.
|
|
17
|
+
|
|
18
|
+
import { readFileSync, writeSync } from 'node:fs'
|
|
19
|
+
import process from 'node:process'
|
|
20
|
+
import { fileURLToPath } from 'node:url'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The floor of a `>=x.y.z` range, or `null` for any other shape.
|
|
24
|
+
*
|
|
25
|
+
* One `>=` floor is the only shape `engines.node` may take — `scripts/checks/
|
|
26
|
+
* versions.ts` holds every published package to it — so anything else means the
|
|
27
|
+
* manifest has drifted, and the CLI says nothing rather than turning our own
|
|
28
|
+
* drift into a reader's failed command.
|
|
29
|
+
*/
|
|
30
|
+
export function floorOf(range) {
|
|
31
|
+
const match = /^>=(\d+)\.(\d+)\.(\d+)$/.exec(String(range ?? '').trim())
|
|
32
|
+
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isBelow(version, floor) {
|
|
36
|
+
const parts = String(version).split('.').map(Number)
|
|
37
|
+
for (let i = 0; i < 3; i++) {
|
|
38
|
+
const part = parts[i] ?? 0
|
|
39
|
+
if (part !== floor[i]) {
|
|
40
|
+
return part < floor[i]
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Vite's wording, because a reader may well have seen it already. */
|
|
47
|
+
export function unsupportedNodeMessage(version, range) {
|
|
48
|
+
return `You are using Node.js ${version}. Poveste requires Node.js version ${range}. Please upgrade your Node.js version.`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function nodeProblem(version, range) {
|
|
52
|
+
const floor = floorOf(range)
|
|
53
|
+
return floor && isBelow(version, floor) ? unsupportedNodeMessage(version, range) : null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Whether this Node may run the CLI, saying so on stderr when it may not.
|
|
58
|
+
*
|
|
59
|
+
* `writeSync` and `process.exitCode`, not `console.error` and `process.exit`: a
|
|
60
|
+
* write to a piped stderr is asynchronous and `process.exit` does not wait for it.
|
|
61
|
+
* `src/node/bin.ts` delays its own forced exit 500ms for that hazard, after a
|
|
62
|
+
* framed compiler error came out cut in half.
|
|
63
|
+
*
|
|
64
|
+
* Measured here, this message survived both shapes — 112 bytes is small enough to
|
|
65
|
+
* land in one write. So this is the shape that cannot lose it rather than a fix for
|
|
66
|
+
* an observed loss, and it costs nothing: there is nothing to flush and nothing
|
|
67
|
+
* left to do but exit.
|
|
68
|
+
*/
|
|
69
|
+
export function supportedNode(manifestUrl = new URL('./package.json', import.meta.url)) {
|
|
70
|
+
let range
|
|
71
|
+
try {
|
|
72
|
+
range = JSON.parse(readFileSync(fileURLToPath(manifestUrl), 'utf8')).engines?.node
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// A manifest this cannot read is not the reader's problem to be told about.
|
|
76
|
+
return true
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const problem = nodeProblem(process.versions.node, range)
|
|
80
|
+
if (!problem) {
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
writeSync(2, `${problem}\n`)
|
|
85
|
+
process.exitCode = 1
|
|
86
|
+
return false
|
|
87
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "poveste",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.16.0",
|
|
5
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.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Sorin Gitlan",
|
|
@@ -62,29 +62,29 @@
|
|
|
62
62
|
"client.js",
|
|
63
63
|
"dist",
|
|
64
64
|
"index.d.ts",
|
|
65
|
+
"node-floor.mjs",
|
|
65
66
|
"plugin.d.ts",
|
|
66
67
|
"plugin.js"
|
|
67
68
|
],
|
|
68
69
|
"engines": {
|
|
69
|
-
"node": "
|
|
70
|
+
"node": ">=22.22.2"
|
|
70
71
|
},
|
|
71
72
|
"peerDependencies": {
|
|
72
73
|
"vite": "^8.0.0"
|
|
73
74
|
},
|
|
74
75
|
"dependencies": {
|
|
75
76
|
"@11ty/gray-matter": "^3.0.0",
|
|
76
|
-
"@
|
|
77
|
-
"@poveste/
|
|
78
|
-
"@poveste/
|
|
79
|
-
"@poveste/
|
|
80
|
-
"@poveste/vendors": "^0.15.0",
|
|
77
|
+
"@poveste/app": "^0.16.0",
|
|
78
|
+
"@poveste/controls": "^0.16.0",
|
|
79
|
+
"@poveste/shared": "^0.16.0",
|
|
80
|
+
"@poveste/vendors": "^0.16.0",
|
|
81
81
|
"change-case": "^5.4.4",
|
|
82
82
|
"chokidar": "^5.0.0",
|
|
83
83
|
"defu": "^6.1.4",
|
|
84
84
|
"diacritics": "^1.3.0",
|
|
85
85
|
"es-module-lexer": "^3.0.2",
|
|
86
|
-
"fs-extra": "^11.
|
|
87
|
-
"jsdom": "^30.
|
|
86
|
+
"fs-extra": "^11.4.1",
|
|
87
|
+
"jsdom": "^30.1.1",
|
|
88
88
|
"lightningcss": "^1.32.0",
|
|
89
89
|
"markdown-it": "^15.0.2",
|
|
90
90
|
"markdown-it-anchor": "^10.0.0",
|
|
@@ -97,22 +97,23 @@
|
|
|
97
97
|
"sade": "^1.8.1",
|
|
98
98
|
"shiki": "^4.4.3",
|
|
99
99
|
"tinyglobby": "^0.2.17",
|
|
100
|
-
"unhead": "^3.1
|
|
100
|
+
"unhead": "^3.4.1"
|
|
101
101
|
},
|
|
102
102
|
"devDependencies": {
|
|
103
103
|
"@types/diacritics": "^1.3.3",
|
|
104
104
|
"@types/fs-extra": "^11.0.4",
|
|
105
105
|
"@types/jsdom": "^30.0.0",
|
|
106
|
-
"@types/node": "^
|
|
106
|
+
"@types/node": "^26.6.3",
|
|
107
107
|
"@types/picomatch": "^4.0.3",
|
|
108
108
|
"typescript": "6.0.3",
|
|
109
|
-
"vite": "^8.
|
|
110
|
-
"vitest": "^5.0.
|
|
109
|
+
"vite": "^8.3.1",
|
|
110
|
+
"vitest": "^5.0.2"
|
|
111
111
|
},
|
|
112
112
|
"scripts": {
|
|
113
113
|
"build": "rimraf dist && tsc",
|
|
114
114
|
"watch": "tsc -w --sourceMap",
|
|
115
115
|
"test": "vitest run",
|
|
116
|
-
"test:dev": "vitest"
|
|
116
|
+
"test:dev": "vitest",
|
|
117
|
+
"typecheck:server": "tsc -p tsconfig.server.json --noEmit"
|
|
117
118
|
}
|
|
118
119
|
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { appendFileSync } from 'node:fs';
|
|
2
|
-
import { onPovesteCleanup } from 'poveste';
|
|
3
|
-
const marker = process.env['POVESTE_CLEANUP_MARKER'] ?? '';
|
|
4
|
-
// Registers the way @poveste/plugin-nuxt does, while the config resolves, and
|
|
5
|
-
// leaves a line behind each time the cleanup runs.
|
|
6
|
-
// `.ts`, not `.js`: the restart specs edit this file, and a `.js` config can be
|
|
7
|
-
// served from Node's module cache, so the edit would never be read.
|
|
8
|
-
export default {
|
|
9
|
-
plugins: [{
|
|
10
|
-
name: 'cleanup-marker',
|
|
11
|
-
defaultConfig() {
|
|
12
|
-
onPovesteCleanup(() => appendFileSync(marker, 'ran\n'));
|
|
13
|
-
},
|
|
14
|
-
}],
|
|
15
|
-
};
|