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 CHANGED
@@ -47,7 +47,7 @@ Run `npx poveste --help` for the full list of options.
47
47
 
48
48
  ## Requirements
49
49
 
50
- Node `^22.22.2 || ^24.15.0 || >=26.0.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
 
package/bin.mjs CHANGED
@@ -1,4 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict'
3
3
 
4
- import './dist/node/bin.js'
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
- 'A story file needs a <Story> in it: https://poveste.dev/guide/',
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
- 'Write a story file and run this again: https://poveste.dev/guide/',
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
+ }
@@ -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
- // Story collection runs in a Node worker. Externalised dependencies are loaded by
2
- // native Node ESM, so Vite never applies `define` to them — and a bare compile-time
3
- // feature flag is just an undefined identifier there. vue-i18n reading
4
- // `__VUE_PROD_DEVTOOLS__` was the reported case (#284); anything with build-time
5
- // flags fails the same way.
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
- // Vite `define` values are JS expression *source*. A plugin-injected flag arrives
20
- // already parsed (`false`), while a user's `define: { X: 'false' }` arrives as the
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,2 @@
1
+ export declare function serializeError(error: unknown, seen?: WeakSet<Error>): unknown;
2
+ export declare function deserializeError(value: unknown): unknown;
@@ -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 { serveInvoke } from './rpc.js';
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
- // Published layout: `poveste` and scoped `@poveste/*` packages. These
18
- // MUST be inlined so collection transforms them and resolves their
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 = new Tinypool({
44
- filename: new URL('./worker.js', import.meta.url).href,
45
- // WebContainer compatibility (Stackblitz)
46
- useAtomics: typeof process.versions['webcontainer'] !== 'string',
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
- function createChannel() {
55
- const channel = new MessageChannel();
56
- const port = channel.port2;
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.broadcastMessage({
68
- kind: 'hst:invalidate',
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: MessagePort, invoke: Invoke): void;
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: MessagePort): Invoke;
10
+ export declare function invokeOver(port: PortLike): Invoke;
@@ -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, over the
3
- * `MessagePort` each task brings. It replaced birpc (#344): after #167 the
4
- * channel carries a single method, and a dependency that ships in `poveste` was
5
- * three majors behind to carry it.
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 ({ id, name, data }) => {
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', ({ id, result, error }) => {
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
- if (error) {
35
- call.reject(error);
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
- * Runs a transformed module with `require`, `exports`, `module`, `__filename`
33
- * and `__dirname` in scope, as vite-node did. Vite's own evaluator passes only
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
- // `import.meta.env` as vite-node gave it: `process.env`, with the three flags as
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
- import type { ServerStory, ServerStoryFile } from '@poveste/shared';
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
- // Cleanup module cache
31
- parentPort?.on('message', (message) => {
32
- if (message?.kind === 'hst:invalidate') {
33
- invalidate(message.file);
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
- export default async (payload) => {
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
- // Here rather than broadcast from the main thread: the pool dispatches tasks
40
- // on a different port from `broadcastMessage`, and nothing orders the two, so
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
+ }
@@ -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
- const SHORT_HEX = /^#([a-f\d]){3,4}$/i;
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*`;
@@ -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").PlatformPath;
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 parsed = parseColor(colors[color][key]);
9
- if (!parsed) {
10
- throw new Error(`[poveste] theme.colors.${color}.${key} is not a colour poveste can read: ${JSON.stringify(colors[color][key])}`);
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}: ${parsed.color.join(' ')};`;
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
- // Two copies reach a book. A consumer's stories resolve their own pinia, which
117
- // imports `@vue/devtools-api` by name; Poveste's own stores use
118
- // `@poveste/vendors`, which keeps the real module as `devtools-api.js` so that
119
- // `poveste dev` still registers them. Both are stubbed here.
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'` is what keeps dev registering — the vendored chunk is the
122
- // real module until a book is built.
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 `b-pinia` and `b-vue-router` import different
137
- // ones of the two the package exports.
138
- return id === STUB || VENDORED.test(id)
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.15.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": "^22.22.2 || ^24.15.0 || >=26.0.0"
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
- "@akryum/tinypool": "^0.3.1",
77
- "@poveste/app": "^0.15.0",
78
- "@poveste/controls": "^0.15.0",
79
- "@poveste/shared": "^0.15.0",
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.2.0",
87
- "jsdom": "^30.0.1",
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.0"
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": "^22.10.1",
106
+ "@types/node": "^26.6.3",
107
107
  "@types/picomatch": "^4.0.3",
108
108
  "typescript": "6.0.3",
109
- "vite": "^8.2.0",
110
- "vitest": "^5.0.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,7 +0,0 @@
1
- declare const _default: {
2
- plugins: {
3
- name: string;
4
- defaultConfig(): void;
5
- }[];
6
- };
7
- export default _default;
@@ -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
- };