diff --git a/dist/commands/build/index.js b/dist/commands/build/index.js index 4b50ac4..cd64ca2 100644 --- a/dist/commands/build/index.js +++ b/dist/commands/build/index.js @@ -5,6 +5,7 @@ import { rmSync, writeFileSync } from "fs"; import bunextInit from "../../functions/bunext-init"; import { v4 } from "uuid"; import { updatePageLastmodRecord } from "../../utils/page-lastmod-record"; +import disposeBundlerContexts from "../../functions/bundler/dispose-bundler-contexts"; const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR, BUNDLER_CTX_MAP_JSON, SSR_BUNDLER_CTX_MAP_JSON, BUNX_CWD_MODULE_CACHE_DIR, BUILD_INFO_JSON, } = grabDirNames(); export default function () { return new Command("build") @@ -21,6 +22,7 @@ export default function () { catch (error) { } global.BUNEXT_SKIPPED_BROWSER_MODULES = new Set(); await bunextInit({ build_only: true }); + await disposeBundlerContexts({ stop_service: true }); // A failed bundle must fail the command: CI and scripts rely on // the exit code instead of grepping logs. if (global.BUNEXT_BUILD_FAILED) { diff --git a/dist/functions/bundler/dispose-bundler-contexts.d.ts b/dist/functions/bundler/dispose-bundler-contexts.d.ts index d3e581e..6b463d7 100644 --- a/dist/functions/bundler/dispose-bundler-contexts.d.ts +++ b/dist/functions/bundler/dispose-bundler-contexts.d.ts @@ -1,6 +1,15 @@ +type Params = { + /** + * Also stop esbuild's service process. Only safe when no other esbuild + * call can be in flight (e.g. before exiting, or right after the startup + * build). The service restarts on the next esbuild call. + */ + stop_service?: boolean; +}; /** * Disposes the hydration and SSR esbuild contexts, releasing the incremental * build state esbuild keeps in memory for them. The bundlers re-create the * contexts on their next run. */ -export default function disposeBundlerContexts(): Promise; +export default function disposeBundlerContexts(params?: Params): Promise; +export {}; diff --git a/dist/functions/bundler/dispose-bundler-contexts.js b/dist/functions/bundler/dispose-bundler-contexts.js index 1aa725f..9473869 100644 --- a/dist/functions/bundler/dispose-bundler-contexts.js +++ b/dist/functions/bundler/dispose-bundler-contexts.js @@ -1,9 +1,10 @@ +import * as esbuild from "esbuild"; /** * Disposes the hydration and SSR esbuild contexts, releasing the incremental * build state esbuild keeps in memory for them. The bundlers re-create the * contexts on their next run. */ -export default async function disposeBundlerContexts() { +export default async function disposeBundlerContexts(params) { try { await global.BUNEXT_BUNDLER_CTX?.dispose(); } @@ -14,4 +15,13 @@ export default async function disposeBundlerContexts() { } catch (error) { } global.BUNEXT_SSR_BUNDLER_CTX = undefined; + if (params?.stop_service) { + // Exiting while the service is still running can leave it orphaned + // with an unanswered ping, which crashes it with "all goroutines are + // asleep - deadlock!". + try { + await esbuild.stop(); + } + catch (error) { } + } } diff --git a/dist/functions/bunext-init.js b/dist/functions/bunext-init.js index 0645e8e..eb0e0ef 100644 --- a/dist/functions/bunext-init.js +++ b/dist/functions/bunext-init.js @@ -89,7 +89,7 @@ export default async function bunextInit(params) { await allPagesESBuildContextBundler({ start: true }); // The server only needs the artifact maps; don't keep // esbuild's build state alive for the rest of the process. - await disposeBundlerContexts(); + await disposeBundlerContexts({ stop_service: true }); } cron(); } diff --git a/package.json b/package.json index 91dd4f8..d907ca6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@moduletrace/bunext", - "version": "1.3.2", + "version": "1.3.3", "main": "dist/index.js", "module": "index.ts", "dependencies": { diff --git a/src/commands/build/index.ts b/src/commands/build/index.ts index ee08b8f..b07e03e 100644 --- a/src/commands/build/index.ts +++ b/src/commands/build/index.ts @@ -6,6 +6,7 @@ import bunextInit from "../../functions/bunext-init"; import type { BuildDataType } from "../../types"; import { v4 } from "uuid"; import { updatePageLastmodRecord } from "../../utils/page-lastmod-record"; +import disposeBundlerContexts from "../../functions/bundler/dispose-bundler-contexts"; const { HYDRATION_DST_DIR, @@ -33,6 +34,8 @@ export default function () { await bunextInit({ build_only: true }); + await disposeBundlerContexts({ stop_service: true }); + // A failed bundle must fail the command: CI and scripts rely on // the exit code instead of grepping logs. if (global.BUNEXT_BUILD_FAILED) { diff --git a/src/functions/bundler/dispose-bundler-contexts.ts b/src/functions/bundler/dispose-bundler-contexts.ts index 81d82da..0911d8a 100644 --- a/src/functions/bundler/dispose-bundler-contexts.ts +++ b/src/functions/bundler/dispose-bundler-contexts.ts @@ -1,9 +1,20 @@ +import * as esbuild from "esbuild"; + +type Params = { + /** + * Also stop esbuild's service process. Only safe when no other esbuild + * call can be in flight (e.g. before exiting, or right after the startup + * build). The service restarts on the next esbuild call. + */ + stop_service?: boolean; +}; + /** * Disposes the hydration and SSR esbuild contexts, releasing the incremental * build state esbuild keeps in memory for them. The bundlers re-create the * contexts on their next run. */ -export default async function disposeBundlerContexts() { +export default async function disposeBundlerContexts(params?: Params) { try { await global.BUNEXT_BUNDLER_CTX?.dispose(); } catch (error) {} @@ -13,4 +24,13 @@ export default async function disposeBundlerContexts() { await global.BUNEXT_SSR_BUNDLER_CTX?.dispose(); } catch (error) {} global.BUNEXT_SSR_BUNDLER_CTX = undefined; + + if (params?.stop_service) { + // Exiting while the service is still running can leave it orphaned + // with an unanswered ping, which crashes it with "all goroutines are + // asleep - deadlock!". + try { + await esbuild.stop(); + } catch (error) {} + } } diff --git a/src/functions/bunext-init.ts b/src/functions/bunext-init.ts index 4811ec9..cc0710e 100644 --- a/src/functions/bunext-init.ts +++ b/src/functions/bunext-init.ts @@ -165,7 +165,7 @@ export default async function bunextInit(params?: Params) { await allPagesESBuildContextBundler({ start: true }); // The server only needs the artifact maps; don't keep // esbuild's build state alive for the rest of the process. - await disposeBundlerContexts(); + await disposeBundlerContexts({ stop_service: true }); } cron();