poveste 0.9.0 → 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
@@ -26,12 +26,39 @@ process.env.POVESTE = 'true';
26
26
  // writes to a piped stderr are asynchronous, and a framed compiler error is long.
27
27
  //
28
28
  // It does mean a leak reads as a clean failure from the outside, so it hides the
29
- // class of bug #426 was. Removing it again needs those watcher handles closed
30
- // first, and a guard that measures the resource list rather than the exit code.
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
+ }
31
52
  function run(command) {
32
53
  return command().then(() => undefined, (error) => {
33
54
  console.error(error);
34
55
  process.exitCode = 1;
56
+ if (proveItDrains()) {
57
+ setImmediate(() => {
58
+ console.error(`POVESTE_ACTIVE_HANDLES: ${JSON.stringify(process.getActiveResourcesInfo())}`);
59
+ });
60
+ return;
61
+ }
35
62
  setTimeout(() => process.exit(1), 500).unref();
36
63
  });
37
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
+ }
@@ -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.9.0",
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.9.0",
102
- "@poveste/controls": "^0.9.0",
103
- "@poveste/shared": "^0.9.0",
104
- "@poveste/vendors": "^0.9.0"
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",