poveste 0.8.2 → 0.10.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/dist/node/bin.js CHANGED
@@ -13,21 +13,52 @@ process.env.POVESTE = 'true';
13
13
  // and take it away, which Vike does — and a failed build then ran forever with
14
14
  // its error already printed (#405).
15
15
  //
16
- // So failure is reported and exited here, deliberately. `exitCode` alone is not
17
- // enough — teardown does not get every handle back, and after a failed build a
18
- // `Timeout` and three `MessagePort`s are still open, so waiting for an empty
19
- // event loop is what hung in the first place. But exiting on the same tick as
20
- // the error would truncate it: writes to a piped stderr are asynchronous, and a
21
- // framed compiler error is long.
16
+ // So failure is reported here, the exit code set, and the process forced out if
17
+ // it does not leave on its own. Closing the collection ports (#426) was not
18
+ // enough on its own: a failed build still hangs in the nuxt4 and sveltekit books,
19
+ // holding `FSEventWrap` and `FSReqCallback` handles — watchers nothing closes on
20
+ // the error path. And `dev` and `preview` hold a listening server by design, so
21
+ // for them no amount of leak-fixing would ever let the loop drain.
22
22
  //
23
- // So both: the exit code is set immediately, and the hard exit is a timer that
24
- // does not itself hold the loop open. A process with nothing leaked exits on its
25
- // own, flushing normally; one that leaked is cut loose a moment later, by which
26
- // time the error has been written.
23
+ // The timer is unref'd, so it never fires for a process that ends by itself and
24
+ // costs nothing when there is nothing to force. It is 500ms rather than
25
+ // immediate because exiting on the same tick as `console.error` truncates it:
26
+ // writes to a piped stderr are asynchronous, and a framed compiler error is long.
27
+ //
28
+ // It does mean a leak reads as a clean failure from the outside, so it hides the
29
+ // class of bug #426 was. `POVESTE_NO_FORCE_EXIT` below is how that stays visible
30
+ // without removing it — CI sets it for one step, where a leak is a hang again.
31
+ //
32
+ // Asserting on the reported handle list instead was tried and does not work: a
33
+ // clean failed build leaves transient ones (`Immediate`, `FSReqCallback`), and the
34
+ // allowance needed to go green has to include `FSReqCallback` — most of what the
35
+ // sveltekit leak shows as. Whether the loop drains is the question (#433).
36
+ /**
37
+ * A build asked to prove it drains: the forced exit below is skipped and what is
38
+ * still holding the loop open is printed.
39
+ *
40
+ * The exit code cannot carry that on its own. Forcing the exit makes a process
41
+ * that leaks look exactly like one that does not, which is what left the CI
42
+ * guard's hang branch unreachable and the #426 class of defect undetectable
43
+ * (#433). Under this flag a leak is a hang again, which is measurable, and the
44
+ * handle list says what to go and close.
45
+ *
46
+ * For CI, not for users — a leak should not be the difference between a build
47
+ * that ends and one that does not.
48
+ */
49
+ function proveItDrains() {
50
+ return !!process.env.POVESTE_NO_FORCE_EXIT;
51
+ }
27
52
  function run(command) {
28
53
  return command().then(() => undefined, (error) => {
29
54
  console.error(error);
30
55
  process.exitCode = 1;
56
+ if (proveItDrains()) {
57
+ setImmediate(() => {
58
+ console.error(`POVESTE_ACTIVE_HANDLES: ${JSON.stringify(process.getActiveResourcesInfo())}`);
59
+ });
60
+ return;
61
+ }
31
62
  setTimeout(() => process.exit(1), 500).unref();
32
63
  });
33
64
  }
@@ -54,6 +54,13 @@ export async function build(ctx) {
54
54
  optimizeDeps: { include: [], noDiscovery: true },
55
55
  }));
56
56
  await server.pluginContainer.buildStart({});
57
+ // Cleanup a plugin registers runs however this ends. `onBuildEnd` cannot be that
58
+ // channel: "the build ended" and "the build ended well" are different events, and
59
+ // plugins publish results from the latter (#434).
60
+ const cleanupCallbacks = [];
61
+ const onCleanup = (cb) => {
62
+ cleanupCallbacks.push(cb);
63
+ };
57
64
  // Everything from here is wrapped so the server is closed on the way out
58
65
  // whichever way that is — it is the other handle that kept a failed build
59
66
  // alive (#405). `serverClosed` keeps the existing order rather than moving the
@@ -70,7 +77,7 @@ export async function build(ctx) {
70
77
  for (const plugin of ctx.config.plugins) {
71
78
  if (plugin.onBuild) {
72
79
  const api = new BuildPluginApi(ctx, plugin, moduleLoader);
73
- await plugin.onBuild(api);
80
+ await plugin.onBuild(api, onCleanup);
74
81
  changeViteConfigCallbacks.push(...api.changeViteConfigCallbacks);
75
82
  buildEndCallbacks.push(...api.buildEndCallbacks);
76
83
  previewStoryCallbacks.push(...api.previewStoryCallbacks);
@@ -285,6 +292,9 @@ export async function build(ctx) {
285
292
  if (!serverClosed) {
286
293
  await wrapLogError('server.close', () => server.close());
287
294
  }
295
+ for (const cb of cleanupCallbacks) {
296
+ await wrapLogError('plugin.onBuild.onCleanup', () => cb());
297
+ }
288
298
  }
289
299
  }
290
300
  function generateBaseHtml(head, body, ctx) {
@@ -0,0 +1,8 @@
1
+ import type { Awaitable } from '@poveste/shared';
2
+ export declare function onPovesteCleanup(cb: () => Awaitable<void>): void;
3
+ /**
4
+ * Runs every registered callback and forgets them, so a `poveste dev` restart does
5
+ * not close the same thing twice. Logged rather than thrown: a teardown that fails
6
+ * must not replace whatever failure brought us here.
7
+ */
8
+ export declare function runPovesteCleanups(): Promise<void>;
@@ -0,0 +1,28 @@
1
+ import { wrapLogError } from './util/log.js';
2
+ /**
3
+ * Teardown for things a plugin opens while the config is being resolved, before
4
+ * any command hook has run.
5
+ *
6
+ * `onBuild`'s own `onCleanup` covers what a plugin opens during a build. It cannot
7
+ * cover what already existed: `@poveste/plugin-nuxt` loads Nuxt in `defaultConfig`,
8
+ * so between that and the `onBuild` loop there is a window — finding the stories,
9
+ * resolving the Vite config, starting the collection server — where a failure left
10
+ * Nuxt running with nothing registered to close it (#434).
11
+ *
12
+ * Registered here, its lifetime starts where the resource does.
13
+ */
14
+ const callbacks = [];
15
+ export function onPovesteCleanup(cb) {
16
+ callbacks.push(cb);
17
+ }
18
+ /**
19
+ * Runs every registered callback and forgets them, so a `poveste dev` restart does
20
+ * not close the same thing twice. Logged rather than thrown: a teardown that fails
21
+ * must not replace whatever failure brought us here.
22
+ */
23
+ export async function runPovesteCleanups() {
24
+ const pending = callbacks.splice(0, callbacks.length);
25
+ for (const cb of pending) {
26
+ await wrapLogError('poveste.cleanup', () => cb());
27
+ }
28
+ }
@@ -79,18 +79,23 @@ export function useCollectStories(options, ctx) {
79
79
  });
80
80
  }
81
81
  async function executeStoryFile(storyFile) {
82
+ // The channel belongs to this execution, and nothing used to close it. On a
83
+ // build that fails, the executions still in flight are abandoned mid-run and
84
+ // their main-thread ports stay open and listening — enough live handles to
85
+ // keep the process alive with nothing left to do (#426).
86
+ let channel;
82
87
  try {
83
- const { workerPort } = createChannel();
88
+ channel = createChannel();
84
89
  const payload = {
85
90
  root: server.config.root,
86
91
  base: server.config.base,
87
92
  storyFile,
88
- port: workerPort,
93
+ port: channel.workerPort,
89
94
  defineGlobals,
90
95
  };
91
96
  const { storyData } = await threadPool.run(payload, {
92
97
  transferList: [
93
- workerPort,
98
+ channel.workerPort,
94
99
  ],
95
100
  });
96
101
  if (storyData.length === 0) {
@@ -125,6 +130,9 @@ export function useCollectStories(options, ctx) {
125
130
  throw e;
126
131
  }
127
132
  }
133
+ finally {
134
+ channel?.port.close();
135
+ }
128
136
  }
129
137
  async function destroy() {
130
138
  await threadPool.destroy();
@@ -1,9 +1,17 @@
1
1
  import { build } from '../build.js';
2
+ import { runPovesteCleanups } from '../cleanup.js';
2
3
  import { createContext } from '../context.js';
3
4
  export async function buildCommand(options) {
4
- const ctx = await createContext({
5
- configFile: options.config,
6
- mode: 'build',
7
- });
8
- await build(ctx);
5
+ // Plugins open things while the config is resolved — before any hook here has
6
+ // run — so the teardown has to span the command, not one phase of it (#434).
7
+ try {
8
+ const ctx = await createContext({
9
+ configFile: options.config,
10
+ mode: 'build',
11
+ });
12
+ await build(ctx);
13
+ }
14
+ finally {
15
+ await runPovesteCleanups();
16
+ }
9
17
  }
@@ -1,54 +1,62 @@
1
1
  import chokidar from 'chokidar';
2
2
  import pc from 'picocolors';
3
+ import { runPovesteCleanups } from '../cleanup.js';
3
4
  import { resolveConfigFile } from '../config.js';
4
5
  import { createContext } from '../context.js';
5
6
  import { createServer } from '../server.js';
6
7
  import { resolvePort } from './port.js';
7
8
  export async function devCommand(options) {
8
- const port = resolvePort(options.port, 'dev');
9
- let stop;
10
- async function start() {
11
- const ctx = await createContext({
12
- configFile: options.config,
13
- mode: 'dev',
14
- });
15
- const { server, viteConfigFile, close } = await createServer(ctx, {
16
- port,
17
- open: options.open,
18
- host: options.host,
19
- });
20
- server.printUrls();
21
- // Poveste config watcher
22
- let watcher;
23
- if (viteConfigFile) {
24
- watcher = chokidar.watch(viteConfigFile, {
9
+ // Plugins open things while the config is resolved — before any hook here has
10
+ // run — so the teardown has to span the command, not one phase of it (#434).
11
+ try {
12
+ const port = resolvePort(options.port, 'dev');
13
+ let stop;
14
+ async function start() {
15
+ const ctx = await createContext({
16
+ configFile: options.config,
17
+ mode: 'dev',
18
+ });
19
+ const { server, viteConfigFile, close } = await createServer(ctx, {
20
+ port,
21
+ open: options.open,
22
+ host: options.host,
23
+ });
24
+ server.printUrls();
25
+ // Poveste config watcher
26
+ let watcher;
27
+ if (viteConfigFile) {
28
+ watcher = chokidar.watch(viteConfigFile, {
29
+ ignoreInitial: true,
30
+ });
31
+ watcher.on('change', () => {
32
+ restart('Vite');
33
+ });
34
+ }
35
+ return async () => {
36
+ await watcher?.close();
37
+ await close();
38
+ };
39
+ }
40
+ async function restart(source) {
41
+ if (stop) {
42
+ console.log(pc.blue(`${source} config changed, restarting...`));
43
+ await stop();
44
+ stop = null; // Don't call stop again until new start() is done
45
+ stop = await start();
46
+ }
47
+ }
48
+ stop = await start();
49
+ const configFile = await resolveConfigFile(undefined, options.config);
50
+ if (configFile) {
51
+ const watcher = chokidar.watch(configFile, {
25
52
  ignoreInitial: true,
26
53
  });
27
54
  watcher.on('change', () => {
28
- restart('Vite');
55
+ restart('Poveste');
29
56
  });
30
57
  }
31
- return async () => {
32
- await watcher?.close();
33
- await close();
34
- };
35
- }
36
- async function restart(source) {
37
- if (stop) {
38
- console.log(pc.blue(`${source} config changed, restarting...`));
39
- await stop();
40
- stop = null; // Don't call stop again until new start() is done
41
- stop = await start();
42
- }
43
58
  }
44
- stop = await start();
45
- const configFile = await resolveConfigFile(undefined, options.config);
46
- if (configFile) {
47
- const watcher = chokidar.watch(configFile, {
48
- ignoreInitial: true,
49
- });
50
- watcher.on('change', () => {
51
- restart('Poveste');
52
- });
59
+ finally {
60
+ await runPovesteCleanups();
53
61
  }
54
62
  }
@@ -1,20 +1,28 @@
1
+ import { runPovesteCleanups } from '../cleanup.js';
1
2
  import { createContext } from '../context.js';
2
3
  import { startPreview } from '../preview.js';
3
4
  import { resolvePort } from './port.js';
4
5
  export async function previewCommand(options) {
5
- const port = resolvePort(options.port, 'preview');
6
- const ctx = await createContext({
7
- mode: 'build',
8
- });
9
- for (const plugin of ctx.config.plugins) {
10
- if (plugin.onPreview) {
11
- await plugin.onPreview();
6
+ // Plugins open things while the config is resolved — before any hook here has
7
+ // run — so the teardown has to span the command, not one phase of it (#434).
8
+ try {
9
+ const port = resolvePort(options.port, 'preview');
10
+ const ctx = await createContext({
11
+ mode: 'build',
12
+ });
13
+ for (const plugin of ctx.config.plugins) {
14
+ if (plugin.onPreview) {
15
+ await plugin.onPreview();
16
+ }
12
17
  }
18
+ const { printUrls } = await startPreview({
19
+ port,
20
+ host: options.host,
21
+ open: options.open,
22
+ }, ctx);
23
+ printUrls();
24
+ }
25
+ finally {
26
+ await runPovesteCleanups();
13
27
  }
14
- const { printUrls } = await startPreview({
15
- port,
16
- host: options.host,
17
- open: options.open,
18
- }, ctx);
19
- printUrls();
20
28
  }
@@ -1,3 +1,4 @@
1
+ export { onPovesteCleanup } from './cleanup.js';
1
2
  export { defaultColors } from './colors.js';
2
3
  export * from './config.js';
3
4
  export * from './plugin.js';
@@ -1,7 +1,8 @@
1
+ // A plugin that generates story source needs to reference poveste's own
2
+ // vendored copies of Vue and the controls from inside that generated code.
3
+ export { onPovesteCleanup } from './cleanup.js';
1
4
  export { defaultColors } from './colors.js';
2
5
  export * from './config.js';
3
6
  export * from './plugin.js';
4
- // A plugin that generates story source needs to reference poveste's own
5
- // vendored copies of Vue and the controls from inside that generated code.
6
7
  export { getInjectedImport } from './util/vendors.js';
7
8
  export * from '@poveste/shared';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "poveste",
3
3
  "type": "module",
4
- "version": "0.8.2",
4
+ "version": "0.10.0",
5
5
  "description": "Interactive component playgrounds for Vue, Svelte and Nuxt — a maintained, drop-in successor to histoire",
6
6
  "author": {
7
7
  "name": "Sorin Gitlan"
@@ -98,10 +98,10 @@
98
98
  "sirv": "^3.0.0",
99
99
  "unhead": "^3.1.0",
100
100
  "vite-node": "^6.0.0",
101
- "@poveste/app": "^0.8.2",
102
- "@poveste/controls": "^0.8.2",
103
- "@poveste/shared": "^0.8.2",
104
- "@poveste/vendors": "^0.8.2"
101
+ "@poveste/app": "^0.10.0",
102
+ "@poveste/controls": "^0.10.0",
103
+ "@poveste/shared": "^0.10.0",
104
+ "@poveste/vendors": "^0.10.0"
105
105
  },
106
106
  "devDependencies": {
107
107
  "@types/fs-extra": "^11.0.4",