@wular/pnext 0.0.1
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 +153 -0
- package/bin/pnext +67 -0
- package/config/lint/base.js +48 -0
- package/config/ts/base.json +26 -0
- package/config/ts/react.json +13 -0
- package/package.json +70 -0
- package/reference/compat.md +63 -0
- package/reference/config.md +120 -0
- package/reference/css.md +58 -0
- package/reference/dev.md +69 -0
- package/reference/env.md +40 -0
- package/reference/metadata.md +86 -0
- package/reference/navigation.md +149 -0
- package/reference/overview.md +35 -0
- package/reference/performance.md +97 -0
- package/reference/rendering.md +127 -0
- package/reference/routing.md +167 -0
- package/reference/typegen.md +64 -0
- package/src/api/cache.ts +80 -0
- package/src/api/client-cache.ts +9 -0
- package/src/api/client-navigation.ts +279 -0
- package/src/api/dynamic.tsx +102 -0
- package/src/api/link.tsx +119 -0
- package/src/api/navigation.ts +198 -0
- package/src/api/router/events.ts +53 -0
- package/src/api/router/history.ts +70 -0
- package/src/api/router/hub.ts +194 -0
- package/src/api/router/policies.ts +107 -0
- package/src/api/router/runtime.ts +5238 -0
- package/src/api/router/types.ts +299 -0
- package/src/api/router.ts +167 -0
- package/src/api/server.ts +323 -0
- package/src/api/suspense.ts +16 -0
- package/src/cache/context.ts +61 -0
- package/src/cli/adapters/vercel-warm.ts +375 -0
- package/src/cli/adapters/vercel.ts +1310 -0
- package/src/cli/analyze-print.ts +181 -0
- package/src/cli/analyze.ts +328 -0
- package/src/cli/boot-trace.ts +29 -0
- package/src/cli/build.ts +3114 -0
- package/src/cli/dev.ts +276 -0
- package/src/cli/index.ts +196 -0
- package/src/cli/named-bin.ts +119 -0
- package/src/cli/serve-ui.ts +160 -0
- package/src/cli/start.ts +1425 -0
- package/src/client/build.ts +2136 -0
- package/src/client/chunk-fold.ts +526 -0
- package/src/client/entry.ts +1525 -0
- package/src/client/paths.ts +22 -0
- package/src/client/prebuilt.ts +621 -0
- package/src/client/profile.ts +75 -0
- package/src/client/react-compiler.ts +94 -0
- package/src/client/reference-stub.ts +145 -0
- package/src/client/reference.ts +47 -0
- package/src/compat/actions/action-client.ts +531 -0
- package/src/compat/actions/action-dispatch.ts +676 -0
- package/src/compat/actions/action-router.ts +40 -0
- package/src/compat/actions/action-shared.ts +95 -0
- package/src/compat/actions/client-plugin.ts +135 -0
- package/src/compat/actions/client-stub.ts +65 -0
- package/src/compat/actions/config.ts +164 -0
- package/src/compat/actions/detect.ts +208 -0
- package/src/compat/actions/discovery.ts +244 -0
- package/src/compat/actions/early-submit.ts +40 -0
- package/src/compat/actions/endpoint.ts +602 -0
- package/src/compat/actions/flight.ts +52 -0
- package/src/compat/actions/form-state.ts +73 -0
- package/src/compat/actions/hoist.ts +485 -0
- package/src/compat/actions/ids.ts +39 -0
- package/src/compat/actions/index.ts +41 -0
- package/src/compat/actions/instances.ts +144 -0
- package/src/compat/actions/origin.ts +109 -0
- package/src/compat/actions/protocol.ts +125 -0
- package/src/compat/actions/registry.ts +74 -0
- package/src/compat/actions/rewrite.ts +282 -0
- package/src/compat/actions/serve.ts +414 -0
- package/src/compat/actions/server-tag.ts +21 -0
- package/src/compat/actions/unrecognized-error.ts +30 -0
- package/src/compat/adapter/build-complete.ts +257 -0
- package/src/compat/bundler/bun-externals.ts +53 -0
- package/src/compat/bundler/cjs-exports.ts +542 -0
- package/src/compat/bundler/config.ts +363 -0
- package/src/compat/bundler/externals.ts +34 -0
- package/src/compat/bundler/import-meta-url.ts +60 -0
- package/src/compat/bundler/modularize-imports.ts +119 -0
- package/src/compat/bundler/new-url-asset.ts +87 -0
- package/src/compat/bundler/optimize-package-imports.ts +273 -0
- package/src/compat/bundler/polyfill.ts +88 -0
- package/src/compat/bundler/react-compiler.ts +61 -0
- package/src/compat/bundler/react-profiler.tsx +25 -0
- package/src/compat/bundler/relay-transform.ts +116 -0
- package/src/compat/bundler/require-context.ts +281 -0
- package/src/compat/bundler/resolve-extensions.ts +75 -0
- package/src/compat/bundler/source-cache.ts +61 -0
- package/src/compat/bundler/static-imports.ts +25 -0
- package/src/compat/bundler/symlink-imports.ts +119 -0
- package/src/compat/bundler/tsconfig-paths.ts +50 -0
- package/src/compat/bundler/wasm.ts +153 -0
- package/src/compat/bundler/webpack-loaders.ts +685 -0
- package/src/compat/bundler/worker.ts +278 -0
- package/src/compat/cache/build-flags.ts +81 -0
- package/src/compat/cache/build-prerender-errors.ts +163 -0
- package/src/compat/cache/custom-handler.ts +159 -0
- package/src/compat/cache/fetch-patch.ts +745 -0
- package/src/compat/cache/handler.ts +100 -0
- package/src/compat/cache/modern-handler.ts +275 -0
- package/src/compat/cache/resume-data-cache.ts +143 -0
- package/src/compat/cache/revalidate.ts +759 -0
- package/src/compat/cache/runtime-error.ts +124 -0
- package/src/compat/cache/use-cache-transform.ts +961 -0
- package/src/compat/cache/use-cache.ts +1695 -0
- package/src/compat/cache-control.ts +269 -0
- package/src/compat/client/base-path.ts +64 -0
- package/src/compat/client/css-order.ts +36 -0
- package/src/compat/client/errors/control-flow.ts +92 -0
- package/src/compat/client/errors/error-boundary.ts +222 -0
- package/src/compat/client/errors/global-error.ts +238 -0
- package/src/compat/client/errors/install.ts +217 -0
- package/src/compat/client/errors/lazy.ts +53 -0
- package/src/compat/client/errors/primitive-throw.ts +126 -0
- package/src/compat/client/errors/soft-refresh.ts +14 -0
- package/src/compat/client/link-status.ts +86 -0
- package/src/compat/client/nav-compat-runtime.ts +57 -0
- package/src/compat/client/nav-compat.ts +42 -0
- package/src/compat/client/navigation-scroll.ts +154 -0
- package/src/compat/client/optimistic-routing.ts +206 -0
- package/src/compat/client/prefetch-cache.ts +111 -0
- package/src/compat/client/route-announcer.ts +72 -0
- package/src/compat/client/segment-cache-policy.ts +159 -0
- package/src/compat/client/segment-cache.ts +1077 -0
- package/src/compat/client/segment-prefetch.ts +375 -0
- package/src/compat/client/trailing-slash.ts +24 -0
- package/src/compat/css/chunking.ts +254 -0
- package/src/compat/css/inline-css.ts +73 -0
- package/src/compat/css/lightningcss.ts +90 -0
- package/src/compat/css/modules.ts +373 -0
- package/src/compat/css/nonce.ts +30 -0
- package/src/compat/css/sass-plugin.ts +65 -0
- package/src/compat/css/sass.ts +392 -0
- package/src/compat/css/styled-jsx-runtime.ts +80 -0
- package/src/compat/css/styled-jsx.ts +49 -0
- package/src/compat/edge-runtime.ts +71 -0
- package/src/compat/export/client.ts +112 -0
- package/src/compat/export/index.ts +272 -0
- package/src/compat/export/standalone.ts +207 -0
- package/src/compat/image-optimizer/cache.ts +119 -0
- package/src/compat/image-optimizer/detect.ts +143 -0
- package/src/compat/image-optimizer/index.ts +601 -0
- package/src/compat/image-optimizer/source.ts +243 -0
- package/src/compat/index.ts +458 -0
- package/src/compat/lifecycle/after-scope.ts +86 -0
- package/src/compat/lifecycle/after.ts +173 -0
- package/src/compat/lifecycle/error-funnel.ts +306 -0
- package/src/compat/lifecycle/error-serialize.ts +87 -0
- package/src/compat/lifecycle/error-ui.ts +167 -0
- package/src/compat/lifecycle/instrumentation-client.ts +138 -0
- package/src/compat/lifecycle/instrumentation.ts +277 -0
- package/src/compat/lifecycle/node-console.ts +19 -0
- package/src/compat/lifecycle/testmode.ts +263 -0
- package/src/compat/mdx/compile.ts +219 -0
- package/src/compat/mdx/next-mdx-stub.ts +46 -0
- package/src/compat/mdx/plugin.ts +37 -0
- package/src/compat/metadata-route-artifacts.ts +458 -0
- package/src/compat/metadata.ts +295 -0
- package/src/compat/middleware/manifest.ts +210 -0
- package/src/compat/misc/action-return.ts +173 -0
- package/src/compat/next/cache.ts +211 -0
- package/src/compat/next/canonical-url.ts +35 -0
- package/src/compat/next/client-cache.ts +57 -0
- package/src/compat/next/client-navigation.ts +313 -0
- package/src/compat/next/client-only.ts +3 -0
- package/src/compat/next/client-script.tsx +215 -0
- package/src/compat/next/client-server.ts +39 -0
- package/src/compat/next/config-loader.ts +569 -0
- package/src/compat/next/config.ts +29 -0
- package/src/compat/next/constants.cjs +6 -0
- package/src/compat/next/constants.ts +6 -0
- package/src/compat/next/custom-server.ts +236 -0
- package/src/compat/next/dist/client/components/app-router-headers.ts +32 -0
- package/src/compat/next/dist/server/app-render/work-unit-async-storage.external.cjs +38 -0
- package/src/compat/next/dist/server/web/spec-extension/revalidate.ts +1 -0
- package/src/compat/next/dist/server/web/spec-extension/unstable-cache.ts +1 -0
- package/src/compat/next/dist/server/web/spec-extension/unstable-no-store.ts +1 -0
- package/src/compat/next/dynamic.tsx +46 -0
- package/src/compat/next/error.tsx +148 -0
- package/src/compat/next/font/cache.ts +171 -0
- package/src/compat/next/font/google.ts +2 -0
- package/src/compat/next/font/index.ts +8 -0
- package/src/compat/next/font/local.ts +5 -0
- package/src/compat/next/font/runtime-client.ts +71 -0
- package/src/compat/next/font/runtime.ts +974 -0
- package/src/compat/next/font/shared.ts +281 -0
- package/src/compat/next/form.tsx +156 -0
- package/src/compat/next/head.tsx +10 -0
- package/src/compat/next/headers.ts +247 -0
- package/src/compat/next/image/config.ts +196 -0
- package/src/compat/next/image/optimizer.ts +96 -0
- package/src/compat/next/image/patterns.ts +103 -0
- package/src/compat/next/image/shared.ts +141 -0
- package/src/compat/next/image/static-metadata.ts +283 -0
- package/src/compat/next/image/validate.ts +269 -0
- package/src/compat/next/image-client.tsx +215 -0
- package/src/compat/next/image-props.ts +575 -0
- package/src/compat/next/image-usage.ts +102 -0
- package/src/compat/next/image.tsx +56 -0
- package/src/compat/next/index.ts +1 -0
- package/src/compat/next/legacy-image.tsx +97 -0
- package/src/compat/next/link-usage.ts +29 -0
- package/src/compat/next/link-validation-transform.ts +200 -0
- package/src/compat/next/link.tsx +466 -0
- package/src/compat/next/navigation.cjs +21 -0
- package/src/compat/next/navigation.ts +188 -0
- package/src/compat/next/offline.ts +51 -0
- package/src/compat/next/og.ts +324 -0
- package/src/compat/next/optimistic-route-state.ts +188 -0
- package/src/compat/next/preferred-region.ts +39 -0
- package/src/compat/next/redirects.ts +131 -0
- package/src/compat/next/resource-hints.ts +136 -0
- package/src/compat/next/rewrites.ts +350 -0
- package/src/compat/next/root-params.ts +142 -0
- package/src/compat/next/router.cjs +49 -0
- package/src/compat/next/router.ts +143 -0
- package/src/compat/next/script.tsx +355 -0
- package/src/compat/next/server-only.ts +3 -0
- package/src/compat/next/server.ts +28 -0
- package/src/compat/next/svgr.ts +58 -0
- package/src/compat/next/telemetry.ts +77 -0
- package/src/compat/next/user-agent.ts +100 -0
- package/src/compat/next/web-vitals.ts +56 -0
- package/src/compat/otel/api.ts +95 -0
- package/src/compat/otel/client-trace-metadata.ts +71 -0
- package/src/compat/otel/fetch-span.ts +77 -0
- package/src/compat/otel/tracer.ts +944 -0
- package/src/compat/pages/client-plugin.ts +108 -0
- package/src/compat/pages/index.ts +527 -0
- package/src/compat/pages/router-state.ts +94 -0
- package/src/compat/ppr/io.ts +38 -0
- package/src/compat/ppr/missing-root-params.ts +105 -0
- package/src/compat/ppr/root-params-scan.ts +164 -0
- package/src/compat/ppr/root-params-transform.ts +75 -0
- package/src/compat/ppr/root-params.ts +129 -0
- package/src/compat/ppr/segment-config-incompat.ts +34 -0
- package/src/compat/protocol.ts +202 -0
- package/src/compat/react/client.ts +59 -0
- package/src/compat/react/compiler-runtime.ts +60 -0
- package/src/compat/react/dom-client.ts +115 -0
- package/src/compat/react/dom-react-server.ts +20 -0
- package/src/compat/react/dom-server.ts +40 -0
- package/src/compat/react/dom.ts +154 -0
- package/src/compat/react/preact.ts +522 -0
- package/src/compat/react/react-server.ts +84 -0
- package/src/compat/react/router-shim.ts +26 -0
- package/src/compat/react/server-component-use.ts +48 -0
- package/src/compat/react/server-inserted-html.ts +87 -0
- package/src/compat/react/server.ts +156 -0
- package/src/compat/react/view-transition.ts +60 -0
- package/src/compat/register/actions.ts +875 -0
- package/src/compat/register/boot.ts +141 -0
- package/src/compat/register/build-tier.ts +11 -0
- package/src/compat/register/build.ts +182 -0
- package/src/compat/register/bundler.ts +587 -0
- package/src/compat/register/cache.ts +85 -0
- package/src/compat/register/client-errors.ts +18 -0
- package/src/compat/register/config.ts +17 -0
- package/src/compat/register/css-extras.ts +120 -0
- package/src/compat/register/edge-runtime.ts +6 -0
- package/src/compat/register/errors.ts +46 -0
- package/src/compat/register/export.ts +23 -0
- package/src/compat/register/font.ts +36 -0
- package/src/compat/register/hooks.ts +34 -0
- package/src/compat/register/image.ts +133 -0
- package/src/compat/register/index.ts +111 -0
- package/src/compat/register/instrumentation-client.ts +35 -0
- package/src/compat/register/lifecycle.ts +86 -0
- package/src/compat/register/mdx.ts +48 -0
- package/src/compat/register/middleware.ts +36 -0
- package/src/compat/register/misc.ts +44 -0
- package/src/compat/register/otel.ts +288 -0
- package/src/compat/register/pages-api.ts +473 -0
- package/src/compat/register/ppr.ts +56 -0
- package/src/compat/register/protocol.ts +57 -0
- package/src/compat/register/proxy.ts +127 -0
- package/src/compat/register/render.ts +268 -0
- package/src/compat/register/routing.ts +410 -0
- package/src/compat/register/segment.ts +1903 -0
- package/src/compat/register/static-image.ts +21 -0
- package/src/compat/register/typed-routes.ts +35 -0
- package/src/compat/register/usecache.ts +131 -0
- package/src/compat/register/validation.ts +56 -0
- package/src/compat/segment/loading-boundary.ts +113 -0
- package/src/compat/segment/page-slot.ts +200 -0
- package/src/compat/segment/tree.ts +481 -0
- package/src/compat/segment/vary-key.ts +102 -0
- package/src/compat/segment/vary-params.ts +551 -0
- package/src/compat/static-params.ts +33 -0
- package/src/compat/tsconfig-defaults.ts +301 -0
- package/src/compat/typecheck/index.ts +1481 -0
- package/src/compat/typecheck/worker.ts +26 -0
- package/src/compat/typed-routes/index.ts +92 -0
- package/src/compat/typed-routes/manifest.ts +356 -0
- package/src/compat/typed-routes/typegen.ts +566 -0
- package/src/compat/validation/errors.ts +159 -0
- package/src/compat/validation/index.ts +1770 -0
- package/src/compat/validation/prerender-diagnostics.ts +1508 -0
- package/src/compat-bootstrap.ts +67 -0
- package/src/config.ts +218 -0
- package/src/css/build.ts +697 -0
- package/src/css/index.ts +2 -0
- package/src/css/postcss.ts +236 -0
- package/src/css/worker.ts +34 -0
- package/src/dev/client-actions.ts +35 -0
- package/src/dev/client-chunk-store.ts +92 -0
- package/src/dev/client-key-cache.ts +178 -0
- package/src/dev/global-css-cache.ts +212 -0
- package/src/dev/imports.ts +2430 -0
- package/src/dev/module-cache.ts +721 -0
- package/src/dev/module-generations.ts +38 -0
- package/src/dev/module-transform.ts +188 -0
- package/src/dev/node-module-bundle-cache.ts +63 -0
- package/src/dev/restart-cache.ts +10 -0
- package/src/dev/route-bundle-key-cache.ts +154 -0
- package/src/dev/route-facts-cache.ts +223 -0
- package/src/dev/server.ts +1710 -0
- package/src/dynamic/source.ts +307 -0
- package/src/dynamic/tree-shake.ts +262 -0
- package/src/env.ts +92 -0
- package/src/extensions.ts +1898 -0
- package/src/index.ts +34 -0
- package/src/internal.ts +43 -0
- package/src/islands/boundary-error.ts +8 -0
- package/src/islands/static-children.ts +37 -0
- package/src/islands/static-slots.ts +106 -0
- package/src/ppr-postpone.ts +24 -0
- package/src/ppr.ts +784 -0
- package/src/proxy.ts +752 -0
- package/src/render/hooks.ts +384 -0
- package/src/render/index.ts +1 -0
- package/src/render/island-context.ts +47 -0
- package/src/render/metadata.ts +857 -0
- package/src/render/renderer.ts +7391 -0
- package/src/render/resource-hints.ts +44 -0
- package/src/render/slots.tsx +679 -0
- package/src/request/context.ts +396 -0
- package/src/resolve/engine.ts +219 -0
- package/src/resolve/imports.ts +1104 -0
- package/src/resolve/scan-facts.ts +474 -0
- package/src/resolve/source-text.ts +86 -0
- package/src/routing/forwarded.ts +41 -0
- package/src/routing/handler.ts +271 -0
- package/src/routing/href.ts +203 -0
- package/src/routing/metadata.ts +1018 -0
- package/src/routing/request-runtime.ts +43 -0
- package/src/routing/routes.ts +2560 -0
- package/src/routing/slots.ts +432 -0
- package/src/runtime/server.ts +3453 -0
- package/src/runtime/vendor.ts +1160 -0
- package/src/style-modules.d.ts +9 -0
- package/src/typegen.ts +151 -0
- package/src/types.ts +725 -0
- package/src/utils/ansi.ts +9 -0
- package/src/utils/content-type.ts +31 -0
- package/src/utils/decode.ts +7 -0
- package/src/utils/dev-profile.ts +31 -0
- package/src/utils/error-log.ts +29 -0
- package/src/utils/fs-cache.ts +31 -0
- package/src/utils/fs.ts +119 -0
- package/src/utils/html.ts +46 -0
- package/src/utils/serialize.ts +378 -0
- package/src/utils/source.ts +35 -0
- package/src/utils/verbose.ts +39 -0
- package/tsconfig.json +10 -0
package/src/cli/dev.ts
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { dim } from '../utils/ansi';
|
|
2
|
+
import { moduleGenerationStats } from '../dev/module-generations';
|
|
3
|
+
import { markBoot, printBootTrace } from './boot-trace';
|
|
4
|
+
import { browserHost, ensurePortFree, openBrowser, printServerReady } from './serve-ui';
|
|
5
|
+
|
|
6
|
+
export const devProcessTitle = 'pnext-dev';
|
|
7
|
+
export const devRestartEnv = 'PNEXT_DEV_RESTART';
|
|
8
|
+
|
|
9
|
+
const defaultMaxDevRssMb = 2048;
|
|
10
|
+
const defaultMaxEsbuildRssMb = 1024;
|
|
11
|
+
// A leaf edit supersedes a couple of generations, so this caps stale mass at roughly 300 MB -
|
|
12
|
+
// about one recycle per 75 leaf edits, and one per ~30 on wide-shared-module edits that rename
|
|
13
|
+
// most of a route graph.
|
|
14
|
+
const defaultMaxStaleModules = 150;
|
|
15
|
+
/** How long a recycle waits for in-flight responses before replacing the image. */
|
|
16
|
+
const drainTimeoutMs = 5_000;
|
|
17
|
+
|
|
18
|
+
export function nameDevProcess() {
|
|
19
|
+
process.title = devProcessTitle;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DevOptions {
|
|
23
|
+
root?: string;
|
|
24
|
+
port?: number;
|
|
25
|
+
hostname?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The dev server, running directly in the process bin/pnext exec'd - there is no supervisor.
|
|
30
|
+
* Resilience comes from self-replacement instead: a memory watchdog trip re-execs this process in
|
|
31
|
+
* place, which frees the module graph without paying a second process spawn on every boot.
|
|
32
|
+
*/
|
|
33
|
+
export async function dev(options: DevOptions = {}) {
|
|
34
|
+
nameDevProcess();
|
|
35
|
+
// Loaded lazily so config resolution and the server graph start in parallel.
|
|
36
|
+
const [{ loadConfig }, { startDevServer }] = await Promise.all([
|
|
37
|
+
import('../config'),
|
|
38
|
+
import('../dev/server'),
|
|
39
|
+
]);
|
|
40
|
+
markBoot('dev:imports');
|
|
41
|
+
const config = await loadConfig(options.root, { dev: true });
|
|
42
|
+
markBoot('dev:config');
|
|
43
|
+
const port = options.port ?? 3000;
|
|
44
|
+
const hostname = options.hostname ?? 'localhost';
|
|
45
|
+
// A memory-triggered restart can briefly race the previous listener's
|
|
46
|
+
// shutdown, so give the port a moment to free before failing.
|
|
47
|
+
await ensurePortFree(port, hostname, { attempts: 10 });
|
|
48
|
+
markBoot('dev:port');
|
|
49
|
+
const start = Date.now();
|
|
50
|
+
// Bisect switch, same role `PNEXT_DEV_RESTART_CACHE=0` plays for the restart
|
|
51
|
+
// caches: turns the boot warm-up off so a measurement can price it.
|
|
52
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
53
|
+
const warm = process.env.PNEXT_DEV_WARM !== '0';
|
|
54
|
+
const server = await startDevServer({ config, port, hostname, warm });
|
|
55
|
+
markBoot('dev:server');
|
|
56
|
+
const elapsedMs = Date.now() - start;
|
|
57
|
+
const url = `http://${browserHost(hostname)}:${server.port ?? port}`;
|
|
58
|
+
printServerReady({ mode: 'dev', hostname, port: server.port ?? port, elapsedMs });
|
|
59
|
+
printBootTrace();
|
|
60
|
+
registerShutdown(server);
|
|
61
|
+
watchServerMemory(server);
|
|
62
|
+
watchStaleModules(server);
|
|
63
|
+
watchEsbuildMemory();
|
|
64
|
+
// Hold the browser until the entry route has compiled, so it opens onto an
|
|
65
|
+
// already-built page instead of a multi-second cold compile.
|
|
66
|
+
if (server.warmup) console.log(dim('Warming up and opening the browser...'));
|
|
67
|
+
await server.warmup;
|
|
68
|
+
// A memory-triggered restart should refresh silently, not open another tab.
|
|
69
|
+
if (process.env[devRestartEnv] !== '1') openBrowser(url);
|
|
70
|
+
return server;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Every dev reload imports changed modules under fresh URLs, and the ES module
|
|
74
|
+
// registry never evicts, so a long session pins every stale graph in memory.
|
|
75
|
+
// Once RSS crosses the limit, trade a seconds-long restart (vendor bundles are
|
|
76
|
+
// preserved on disk) for getting the memory back.
|
|
77
|
+
function watchServerMemory(server: DevServerHandle) {
|
|
78
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
79
|
+
const limitMb = Number(process.env.PNEXT_DEV_MAX_RSS_MB || defaultMaxDevRssMb);
|
|
80
|
+
if (!Number.isFinite(limitMb) || limitMb <= 0) return;
|
|
81
|
+
const limitBytes = limitMb * 1024 * 1024;
|
|
82
|
+
const timer = setInterval(() => {
|
|
83
|
+
const rss = process.memoryUsage.rss();
|
|
84
|
+
if (rss < limitBytes) return;
|
|
85
|
+
clearInterval(timer);
|
|
86
|
+
const usedMb = Math.round(rss / (1024 * 1024));
|
|
87
|
+
console.log(
|
|
88
|
+
dim(
|
|
89
|
+
`Dev server reached ${usedMb}MB of memory (limit ${limitMb}MB, set PNEXT_DEV_MAX_RSS_MB to change); restarting to release stale modules...`,
|
|
90
|
+
),
|
|
91
|
+
);
|
|
92
|
+
void restartDevProcess(server);
|
|
93
|
+
}, 15_000);
|
|
94
|
+
timer.unref();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// The esbuild Go service self-drains its heap when idle, so two consecutive readings above the
|
|
98
|
+
// limit mean it is genuinely parked high. Stopping it is lossless - pnext only issues one-shot
|
|
99
|
+
// builds, never incremental contexts. A stop that races an in-flight build fails that one build;
|
|
100
|
+
// dev asset caches drop failed builds, so the next request retries.
|
|
101
|
+
function watchEsbuildMemory() {
|
|
102
|
+
if (process.platform === 'win32') return;
|
|
103
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
104
|
+
const limitMb = Number(process.env.PNEXT_DEV_MAX_ESBUILD_RSS_MB || defaultMaxEsbuildRssMb);
|
|
105
|
+
if (!Number.isFinite(limitMb) || limitMb <= 0) return;
|
|
106
|
+
let highTicks = 0;
|
|
107
|
+
const timer = setInterval(() => {
|
|
108
|
+
const rssMb = esbuildServiceRssMb();
|
|
109
|
+
highTicks = rssMb > limitMb ? highTicks + 1 : 0;
|
|
110
|
+
if (highTicks < 2) return;
|
|
111
|
+
highTicks = 0;
|
|
112
|
+
console.log(
|
|
113
|
+
dim(
|
|
114
|
+
`esbuild reached ${Math.round(rssMb)}MB of memory (limit ${limitMb}MB, set PNEXT_DEV_MAX_ESBUILD_RSS_MB to change); restarting it to release the heap...`,
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
void (async () => {
|
|
118
|
+
try {
|
|
119
|
+
const esbuild = await import('esbuild');
|
|
120
|
+
await esbuild.stop();
|
|
121
|
+
} catch {
|
|
122
|
+
// ignore: the service may already be gone
|
|
123
|
+
}
|
|
124
|
+
// stop() can strand in-flight build promises unsettled; fail their
|
|
125
|
+
// waiters so requests retry against the respawned service.
|
|
126
|
+
const { failInFlightClientBuilds } = await import('../dev/server');
|
|
127
|
+
failInFlightClientBuilds('esbuild service restarted by the memory watchdog');
|
|
128
|
+
})();
|
|
129
|
+
}, 15_000);
|
|
130
|
+
timer.unref();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Recycle on stale registry mass rather than on RSS. Every save renames the modules it changes and
|
|
135
|
+
* Bun's registry never unlinks the old ones, so a session accumulates superseded generations at a
|
|
136
|
+
* fixed rate that nothing in-process can free. RSS is a lagging proxy: it trips only after the
|
|
137
|
+
* growth has already cost save latency, and it also trips on a warm route set that is entirely
|
|
138
|
+
* live. The replacement re-warms from the content-keyed disk cache, so the cost is a restart, not
|
|
139
|
+
* a cold build.
|
|
140
|
+
*/
|
|
141
|
+
function watchStaleModules(server: DevServerHandle) {
|
|
142
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
143
|
+
const limit = Number(process.env.PNEXT_DEV_MAX_STALE_MODULES || defaultMaxStaleModules);
|
|
144
|
+
if (!Number.isFinite(limit) || limit <= 0) return;
|
|
145
|
+
const timer = setInterval(() => {
|
|
146
|
+
const { stale } = moduleGenerationStats();
|
|
147
|
+
if (stale < limit) return;
|
|
148
|
+
clearInterval(timer);
|
|
149
|
+
console.log(
|
|
150
|
+
dim(
|
|
151
|
+
`Dev server is holding ${stale} superseded module generations (limit ${limit}, set PNEXT_DEV_MAX_STALE_MODULES to change); restarting to release them...`,
|
|
152
|
+
),
|
|
153
|
+
);
|
|
154
|
+
void restartDevProcess(server);
|
|
155
|
+
}, 15_000);
|
|
156
|
+
timer.unref();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Our esbuild service children: [pid, RSS in MB] per process.
|
|
160
|
+
function esbuildServices(): [number, number][] {
|
|
161
|
+
const found: [number, number][] = [];
|
|
162
|
+
try {
|
|
163
|
+
const stdout = Bun.spawnSync(['ps', '-ax', '-o', 'pid=,ppid=,rss=,ucomm=']).stdout.toString();
|
|
164
|
+
for (const line of stdout.split('\n')) {
|
|
165
|
+
const [pid, ppid, rss, ...name] = line.trim().split(/\s+/);
|
|
166
|
+
if (Number(ppid) === process.pid && name.join(' ').includes('esbuild')) {
|
|
167
|
+
found.push([Number(pid), Number(rss) / 1024]);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
// ignore: no ps available means no monitoring, not a broken dev server
|
|
172
|
+
}
|
|
173
|
+
return found;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// RSS of our esbuild service, in MB (0 when not running).
|
|
177
|
+
function esbuildServiceRssMb() {
|
|
178
|
+
return esbuildServices().reduce((total, [, rssMb]) => total + rssMb, 0);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
interface DevServerHandle {
|
|
182
|
+
stop(closeActiveConnections?: boolean): unknown;
|
|
183
|
+
/** Resolves once nothing is being served (or the timeout elapses). */
|
|
184
|
+
drain?(timeoutMs: number): Promise<boolean>;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Replace this process with a fresh dev server. A real execve keeps the pid, the stdio and the
|
|
189
|
+
* shell's job control intact, so the restart is invisible from the terminal - and because the image
|
|
190
|
+
* is replaced, every byte of the stale module graph goes away. Bun exposes no exec, so libc's is
|
|
191
|
+
* called directly; where that is unavailable (Windows) the replacement is spawned instead, which
|
|
192
|
+
* frees the same memory but detaches the server from the shell's foreground job.
|
|
193
|
+
*/
|
|
194
|
+
async function restartDevProcess(server: DevServerHandle): Promise<void> {
|
|
195
|
+
try {
|
|
196
|
+
// Stop accepting, then let what is already being served finish: an exec
|
|
197
|
+
// mid-response would leave the browser with a truncated page and no error.
|
|
198
|
+
server.stop(false);
|
|
199
|
+
await server.drain?.(drainTimeoutMs);
|
|
200
|
+
server.stop(true);
|
|
201
|
+
} catch {
|
|
202
|
+
// ignore: we are being replaced regardless
|
|
203
|
+
}
|
|
204
|
+
// The esbuild service keeps its pipes across an exec, so it would outlive the
|
|
205
|
+
// image that owned it — with exactly the heap this restart is reclaiming.
|
|
206
|
+
for (const [pid] of esbuildServices()) {
|
|
207
|
+
try {
|
|
208
|
+
process.kill(pid, 'SIGTERM');
|
|
209
|
+
} catch {
|
|
210
|
+
// ignore: already gone
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const argv = [process.execPath, '--conditions=react-server', ...process.argv.slice(1)];
|
|
214
|
+
const env = { ...process.env, [devRestartEnv]: '1' };
|
|
215
|
+
try {
|
|
216
|
+
await execSelf(argv, env);
|
|
217
|
+
} catch {
|
|
218
|
+
// ignore: fall through to the spawn path
|
|
219
|
+
}
|
|
220
|
+
// Only reached when exec is unavailable or failed.
|
|
221
|
+
Bun.spawn(argv, { stdin: 'inherit', stdout: 'inherit', stderr: 'inherit', env });
|
|
222
|
+
process.exit(0);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// execve(2) via libc. argv/envp are NUL-terminated C string arrays, and the
|
|
226
|
+
// pointer array must stay reachable until the call returns (it never does on
|
|
227
|
+
// success).
|
|
228
|
+
async function execSelf(argv: string[], env: Record<string, string | undefined>): Promise<void> {
|
|
229
|
+
if (process.platform === 'win32') return;
|
|
230
|
+
const { dlopen, FFIType, ptr, suffix } = await import('bun:ffi');
|
|
231
|
+
const libc = process.platform === 'darwin' ? '/usr/lib/libSystem.B.dylib' : `libc.${suffix}.6`;
|
|
232
|
+
const { symbols } = dlopen(libc, {
|
|
233
|
+
execve: {
|
|
234
|
+
args: [FFIType.cstring, FFIType.ptr, FFIType.ptr],
|
|
235
|
+
returns: FFIType.i32,
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
const encoder = new TextEncoder();
|
|
239
|
+
const cString = (value: string) => encoder.encode(`${value}\0`);
|
|
240
|
+
const cArray = (values: string[]) => {
|
|
241
|
+
const cells = values.map(cString);
|
|
242
|
+
const array = new BigInt64Array(cells.length + 1);
|
|
243
|
+
cells.forEach((cell, index) => {
|
|
244
|
+
array[index] = BigInt(ptr(cell));
|
|
245
|
+
});
|
|
246
|
+
return { array, cells };
|
|
247
|
+
};
|
|
248
|
+
const args = cArray(argv);
|
|
249
|
+
const envp = cArray(
|
|
250
|
+
Object.entries(env)
|
|
251
|
+
.filter(([, value]) => value !== undefined)
|
|
252
|
+
.map(([key, value]) => `${key}=${value}`),
|
|
253
|
+
);
|
|
254
|
+
symbols.execve(cString(argv[0]!), ptr(args.array), ptr(envp.array));
|
|
255
|
+
// execve only returns on failure; keep the buffers alive until then.
|
|
256
|
+
void [args.cells, envp.cells];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Stop the server and run process `exit` handlers on a terminating signal,
|
|
260
|
+
// so killing the dev server never leaves orphaned subprocesses behind.
|
|
261
|
+
function registerShutdown(server: DevServerHandle) {
|
|
262
|
+
let closing = false;
|
|
263
|
+
const shutdown = () => {
|
|
264
|
+
if (closing) return;
|
|
265
|
+
closing = true;
|
|
266
|
+
try {
|
|
267
|
+
server.stop(true);
|
|
268
|
+
} catch {
|
|
269
|
+
// ignore: process is exiting regardless
|
|
270
|
+
}
|
|
271
|
+
process.exit(0);
|
|
272
|
+
};
|
|
273
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP'] as const) {
|
|
274
|
+
process.once(signal, shutdown);
|
|
275
|
+
}
|
|
276
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { existsSync, statSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { markBoot } from './boot-trace';
|
|
5
|
+
import { commandBinaryName, nameEsbuildProcess, namedBunBinary } from './named-bin';
|
|
6
|
+
|
|
7
|
+
const [, , command, ...args] = process.argv;
|
|
8
|
+
markBoot('cli:entry');
|
|
9
|
+
|
|
10
|
+
// A no-op once we already run under the command's name; otherwise it creates
|
|
11
|
+
// the hardlink so bin/pnext can exec it directly from the next run on.
|
|
12
|
+
namedBunBinary(commandBinaryName(command));
|
|
13
|
+
|
|
14
|
+
// Before any command lazily imports esbuild, which captures the binary path on
|
|
15
|
+
// module load.
|
|
16
|
+
nameEsbuildProcess();
|
|
17
|
+
markBoot('cli:naming');
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
if (command === 'dev') {
|
|
21
|
+
const { dev } = await import('./dev');
|
|
22
|
+
await dev({
|
|
23
|
+
root: positionalRoot(args),
|
|
24
|
+
port: optionNumber(args, '--port'),
|
|
25
|
+
hostname: optionString(args, '--hostname'),
|
|
26
|
+
});
|
|
27
|
+
} else if (command === 'build') {
|
|
28
|
+
const { buildProject } = await import('./build');
|
|
29
|
+
await buildProject(positionalRoot(args), {
|
|
30
|
+
adapter: adapterOption(args),
|
|
31
|
+
verbose: args.includes('--verbose'),
|
|
32
|
+
debugBuildPaths: optionString(args, '--debug-build-paths'),
|
|
33
|
+
buildMode: buildModeOption(args),
|
|
34
|
+
debugPrerender: args.includes('--debug-prerender'),
|
|
35
|
+
});
|
|
36
|
+
// Nothing here keeps the event loop alive on success, but a project's
|
|
37
|
+
// next.config can (e.g. a stray setInterval) and Next's own `next build`
|
|
38
|
+
// force-exits regardless of such handles. Do the same so `build` always
|
|
39
|
+
// terminates once buildProject resolves.
|
|
40
|
+
process.exit(0);
|
|
41
|
+
} else if (command === 'start') {
|
|
42
|
+
const { start } = await import('./start');
|
|
43
|
+
await start({
|
|
44
|
+
root: positionalRoot(args),
|
|
45
|
+
port: optionNumber(args, '--port'),
|
|
46
|
+
hostname: optionString(args, '--hostname'),
|
|
47
|
+
});
|
|
48
|
+
} else if (command === 'analyze') {
|
|
49
|
+
const { analyzeProject } = await import('./analyze');
|
|
50
|
+
const { route, root } = analyzePositionals(args);
|
|
51
|
+
const result = await analyzeProject(root, {
|
|
52
|
+
compression: analyzeCompression(args),
|
|
53
|
+
route,
|
|
54
|
+
});
|
|
55
|
+
if (args.includes('--json')) {
|
|
56
|
+
console.log(JSON.stringify(result, null, 2));
|
|
57
|
+
} else {
|
|
58
|
+
const { printAnalyzeResult } = await import('./analyze-print');
|
|
59
|
+
printAnalyzeResult(result, { route, files: args.includes('--files') });
|
|
60
|
+
}
|
|
61
|
+
} else if (command === 'typegen') {
|
|
62
|
+
const { typegenProject } = await import('../typegen');
|
|
63
|
+
const result = await typegenProject(positionalRoot(args));
|
|
64
|
+
console.log(
|
|
65
|
+
`Generated ${result.routes} route types, ${result.aliases} aliases, and ${result.checks} checks at ${result.file}`,
|
|
66
|
+
);
|
|
67
|
+
// Like `next typegen`, exit even when the loaded next.config leaves open
|
|
68
|
+
// handles (timers/connections) alive — a one-shot command must not hang.
|
|
69
|
+
process.exit(0);
|
|
70
|
+
} else {
|
|
71
|
+
printHelp();
|
|
72
|
+
process.exit(command ? 1 : 0);
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
const formatted = formatCliError(error);
|
|
76
|
+
console.error(formatted.message);
|
|
77
|
+
if (formatted.trace) console.log(`\n${formatted.trace}`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function formatCliError(error: unknown) {
|
|
82
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
83
|
+
const traceStart = message.indexOf('\npnext client import trace for route ');
|
|
84
|
+
if (traceStart === -1) return { message };
|
|
85
|
+
return {
|
|
86
|
+
message: message.slice(0, traceStart),
|
|
87
|
+
trace: message.slice(traceStart + 1),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function positionalRoot(args: string[]) {
|
|
92
|
+
return positionals(args)[0];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// The analyze command takes optional [route] and [directory] positionals. A
|
|
96
|
+
// leading-slash argument is the route filter unless it points at a directory
|
|
97
|
+
// that looks like a project root — system directories like /home or /run
|
|
98
|
+
// collide with real route names.
|
|
99
|
+
function analyzePositionals(args: string[]) {
|
|
100
|
+
let route: string | undefined;
|
|
101
|
+
let root: string | undefined;
|
|
102
|
+
for (const arg of positionals(args)) {
|
|
103
|
+
if (!route && arg.startsWith('/') && !isProjectDirectory(arg)) route = arg;
|
|
104
|
+
else root ??= arg;
|
|
105
|
+
}
|
|
106
|
+
return { route, root };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isProjectDirectory(candidate: string) {
|
|
110
|
+
if (!isDirectory(candidate)) return false;
|
|
111
|
+
return ['package.json', '.pnext', 'app', path.join('src', 'app')].some(entry =>
|
|
112
|
+
existsSync(path.join(candidate, entry)),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function positionals(args: string[]) {
|
|
117
|
+
const out: string[] = [];
|
|
118
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
119
|
+
const arg = args[index];
|
|
120
|
+
if (!arg) continue;
|
|
121
|
+
if (
|
|
122
|
+
arg === '--port' ||
|
|
123
|
+
arg === '--adapter' ||
|
|
124
|
+
arg === '--hostname' ||
|
|
125
|
+
arg === '--debug-build-paths' ||
|
|
126
|
+
arg === '--experimental-build-mode'
|
|
127
|
+
) {
|
|
128
|
+
index += 1;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (!arg.startsWith('--')) out.push(arg);
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isDirectory(candidate: string) {
|
|
137
|
+
try {
|
|
138
|
+
return statSync(candidate).isDirectory();
|
|
139
|
+
} catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function optionNumber(args: string[], name: string) {
|
|
145
|
+
const index = args.indexOf(name);
|
|
146
|
+
if (index === -1) return undefined;
|
|
147
|
+
const value = args[index + 1];
|
|
148
|
+
return value ? Number(value) : undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function optionString(args: string[], name: string) {
|
|
152
|
+
// Accept both `--name value` and `--name=value`.
|
|
153
|
+
const inline = args.find(arg => arg.startsWith(`${name}=`));
|
|
154
|
+
if (inline !== undefined) return inline.slice(name.length + 1);
|
|
155
|
+
const index = args.indexOf(name);
|
|
156
|
+
if (index === -1) return undefined;
|
|
157
|
+
return args[index + 1];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* `next build --experimental-build-mode compile|generate` parity. Both the
|
|
162
|
+
* space (`--experimental-build-mode generate`) and equals
|
|
163
|
+
* (`--experimental-build-mode=generate`) forms are accepted; `generate` runs
|
|
164
|
+
* the standard full build (idempotent over a prior `compile` run).
|
|
165
|
+
*/
|
|
166
|
+
function buildModeOption(args: string[]): 'compile' | 'generate' | undefined {
|
|
167
|
+
const value = optionString(args, '--experimental-build-mode');
|
|
168
|
+
if (value === 'compile' || value === 'generate') return value;
|
|
169
|
+
if (value !== undefined) {
|
|
170
|
+
console.error(
|
|
171
|
+
`error: option '--experimental-build-mode <mode>' argument '${value}' is invalid. Expected 'compile' or 'generate'.`,
|
|
172
|
+
);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function adapterOption(args: string[]) {
|
|
179
|
+
const index = args.indexOf('--adapter');
|
|
180
|
+
if (index === -1) return undefined;
|
|
181
|
+
const adapter = args[index + 1];
|
|
182
|
+
if (adapter === 'vercel') return adapter;
|
|
183
|
+
throw new Error(`Unsupported adapter: ${adapter ?? ''}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function analyzeCompression(args: string[]) {
|
|
187
|
+
return args.includes('--brotli') ? 'brotli' : 'gzip';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function printHelp() {
|
|
191
|
+
console.log(`pnext dev [directory] [--port 3000] [--hostname 127.0.0.1]
|
|
192
|
+
pnext build [directory] [--adapter vercel] [--verbose]
|
|
193
|
+
pnext start [directory] [--port 3000] [--hostname 127.0.0.1]
|
|
194
|
+
pnext analyze [route] [directory] [--brotli] [--files] [--json]
|
|
195
|
+
pnext typegen [directory]`);
|
|
196
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chmodSync,
|
|
3
|
+
constants as fsConstants,
|
|
4
|
+
copyFileSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
linkSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
} from 'node:fs';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
|
|
16
|
+
export const namedBinaryName = 'pnext';
|
|
17
|
+
|
|
18
|
+
// Keep in sync with the command case in bin/pnext, which resolves the same name.
|
|
19
|
+
const cliCommands = new Set(['dev', 'build', 'start', 'analyze', 'typegen']);
|
|
20
|
+
|
|
21
|
+
/** Process name for a CLI command: `pnext dev` runs as `pnext-dev`, etc. */
|
|
22
|
+
export function commandBinaryName(command: string | undefined) {
|
|
23
|
+
if (command && cliCommands.has(command)) return `${namedBinaryName}-${command}`;
|
|
24
|
+
return namedBinaryName;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Path to a bun executable whose file is literally named `name`.
|
|
29
|
+
*
|
|
30
|
+
* Activity Monitor and `ps -o ucomm` show the kernel's process name, set from the basename of the
|
|
31
|
+
* executable file at exec time - `argv0`, `exec -a` and `process.title` only change the argv that
|
|
32
|
+
* plain `ps` prints. So the running bun binary is hardlinked (or cloned/copied) into the user cache
|
|
33
|
+
* once per role and every process spawns through its role's link. Hardlinks share the inode, so the
|
|
34
|
+
* extra names are free.
|
|
35
|
+
*
|
|
36
|
+
* Falls back to the plain bun binary when the cache is unavailable: naming is cosmetic and must never
|
|
37
|
+
* break spawning.
|
|
38
|
+
*/
|
|
39
|
+
export function namedBunBinary(name: string = namedBinaryName): string {
|
|
40
|
+
if (process.platform === 'win32') return process.execPath;
|
|
41
|
+
if (path.basename(process.execPath) === name) return process.execPath;
|
|
42
|
+
return namedExecutable(process.execPath, `bun-${Bun.version}`, name);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Label esbuild's service process. The esbuild JS API spawns its own Go binary, so pnext never execs
|
|
47
|
+
* it - but esbuild honors `ESBUILD_BINARY_PATH`, so pointing that at a hardlink of the platform
|
|
48
|
+
* binary renames the service. The lib captures the variable at module load, so this must run before
|
|
49
|
+
* anything imports esbuild.
|
|
50
|
+
*/
|
|
51
|
+
export function nameEsbuildProcess() {
|
|
52
|
+
if (process.platform === 'win32') return;
|
|
53
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
54
|
+
if (process.env.ESBUILD_BINARY_PATH) return; // respect an explicit override
|
|
55
|
+
try {
|
|
56
|
+
const require = createRequire(import.meta.url);
|
|
57
|
+
const { version } = require('esbuild/package.json') as { version: string };
|
|
58
|
+
const binPath = require.resolve(`@esbuild/${process.platform}-${process.arch}/bin/esbuild`);
|
|
59
|
+
const named = namedExecutable(binPath, `esbuild-${version}`, 'pnext-esbuild');
|
|
60
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
61
|
+
if (named !== binPath) process.env.ESBUILD_BINARY_PATH = named;
|
|
62
|
+
} catch {
|
|
63
|
+
// unknown platform package or unavailable cache: esbuild resolves its own
|
|
64
|
+
// binary and just keeps its name
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function namedExecutable(sourcePath: string, versionDir: string, name: string): string {
|
|
69
|
+
try {
|
|
70
|
+
// One directory per tool version, so upgrades switch paths instead of
|
|
71
|
+
// serving a stale binary.
|
|
72
|
+
const binRoot = path.join(cacheRoot(), 'bin');
|
|
73
|
+
const dir = path.join(binRoot, versionDir);
|
|
74
|
+
const named = path.join(dir, name);
|
|
75
|
+
if (!existsSync(named)) {
|
|
76
|
+
mkdirSync(dir, { recursive: true });
|
|
77
|
+
// Land under a temp name and rename, so a concurrent pnext process can
|
|
78
|
+
// never exec a half-written binary.
|
|
79
|
+
const staging = path.join(dir, `.${name}-${process.pid}`);
|
|
80
|
+
rmSync(staging, { force: true });
|
|
81
|
+
try {
|
|
82
|
+
linkSync(sourcePath, staging);
|
|
83
|
+
} catch {
|
|
84
|
+
// Cross-volume or hardlink-restricted: clone when the filesystem
|
|
85
|
+
// supports it (free on APFS), else fall back to a full copy.
|
|
86
|
+
copyFileSync(sourcePath, staging, fsConstants.COPYFILE_FICLONE);
|
|
87
|
+
chmodSync(staging, 0o755);
|
|
88
|
+
}
|
|
89
|
+
renameSync(staging, named);
|
|
90
|
+
removeStaleVersions(binRoot, path.basename(dir));
|
|
91
|
+
}
|
|
92
|
+
return named;
|
|
93
|
+
} catch {
|
|
94
|
+
return sourcePath;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Best-effort sweep of binaries linked for previous versions of the same
|
|
99
|
+
// tool: keeping `bun-1.3.6` removes other `bun-*` dirs but leaves
|
|
100
|
+
// `esbuild-*` alone.
|
|
101
|
+
function removeStaleVersions(binRoot: string, keep: string) {
|
|
102
|
+
const toolPrefix = keep.slice(0, keep.indexOf('-') + 1);
|
|
103
|
+
try {
|
|
104
|
+
for (const entry of readdirSync(binRoot)) {
|
|
105
|
+
if (entry === keep || !entry.startsWith(toolPrefix)) continue;
|
|
106
|
+
rmSync(path.join(binRoot, entry), { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// ignore: stale versions only waste a hardlink's worth of space
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** pnext's user cache dir — survives the build wiping the output directory. */
|
|
114
|
+
export function cacheRoot() {
|
|
115
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
116
|
+
const xdg = process.env.XDG_CACHE_HOME;
|
|
117
|
+
if (xdg) return path.join(xdg, 'pnext');
|
|
118
|
+
return path.join(os.homedir(), '.cache', 'pnext');
|
|
119
|
+
}
|