@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
|
@@ -0,0 +1,3453 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { builtinModules, createRequire } from 'node:module';
|
|
3
|
+
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
4
|
+
import { copyFile, mkdir, readFile, rm, stat, symlink, writeFile } from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { build, transform, type Loader, type Metafile, type OnLoadResult, type OnResolveResult, type Plugin } from 'esbuild';
|
|
8
|
+
import {
|
|
9
|
+
applyBundledSourceTransforms,
|
|
10
|
+
applyServerSourcePreTransforms,
|
|
11
|
+
applyServerSourceTransforms,
|
|
12
|
+
extraLoadableExtensions,
|
|
13
|
+
getAssetExtensions,
|
|
14
|
+
getBundlerExtensions,
|
|
15
|
+
getCssExtensions,
|
|
16
|
+
getImportAliasExtensions,
|
|
17
|
+
onLoadableExtensionsChanged,
|
|
18
|
+
serverDefineOptions,
|
|
19
|
+
sourceNeedsServerTransforms,
|
|
20
|
+
type ServerEsbuildPluginOptions,
|
|
21
|
+
} from '../extensions';
|
|
22
|
+
import {
|
|
23
|
+
frameworkRuntimeAliasEntries,
|
|
24
|
+
pathToFileHref,
|
|
25
|
+
pnextAliases,
|
|
26
|
+
type CompatAliasTarget,
|
|
27
|
+
type ResolvedConfig,
|
|
28
|
+
} from '../config';
|
|
29
|
+
import {
|
|
30
|
+
rewriteDynamicCallTargets,
|
|
31
|
+
rewriteLiteralDynamicCalls,
|
|
32
|
+
sourceHasDynamicImport,
|
|
33
|
+
} from '../dynamic/source';
|
|
34
|
+
import { cacheRoot, devArtifactUsable, noteDevArtifactWritten } from '../dev/module-cache';
|
|
35
|
+
import { readNodeModuleBundle, writeNodeModuleBundle } from '../dev/node-module-bundle-cache';
|
|
36
|
+
import { setBasePathPrefix, setTrailingSlashUrls } from '../routing/href';
|
|
37
|
+
import {
|
|
38
|
+
getExternalPackagePolicy,
|
|
39
|
+
isEsmModuleEntry,
|
|
40
|
+
isEsmModuleFile,
|
|
41
|
+
isPackageSubpathUnexported,
|
|
42
|
+
resolveExternalLoadTarget,
|
|
43
|
+
resolveImport,
|
|
44
|
+
resolveLinkedPackageSpecifier,
|
|
45
|
+
resolveNestedPackageFromImporter,
|
|
46
|
+
resolveVendorPackageSpecifier,
|
|
47
|
+
clearVendorPackageResolutions,
|
|
48
|
+
clearProvidedEntryResolutions,
|
|
49
|
+
provideEntryResolution,
|
|
50
|
+
workspacePackageRoots,
|
|
51
|
+
} from '../resolve/imports';
|
|
52
|
+
import { clearNodeModulesResolutionCache } from '../resolve/engine';
|
|
53
|
+
import { cachedExistsSync, clearNodeModulesFsCache } from '../utils/fs-cache';
|
|
54
|
+
import {
|
|
55
|
+
clientReferenceExportNames,
|
|
56
|
+
clientReferenceModuleSource,
|
|
57
|
+
hasUseClientDirective,
|
|
58
|
+
} from '../client/reference-stub';
|
|
59
|
+
import { reactCompatEnabled } from '../render/hooks';
|
|
60
|
+
import { escapeRegex } from '../utils/source';
|
|
61
|
+
import { writeFileAtomic } from '../utils/fs';
|
|
62
|
+
import {
|
|
63
|
+
addCommonJsNamedExports,
|
|
64
|
+
canonicalVendorCode,
|
|
65
|
+
noteVendorContentId,
|
|
66
|
+
vendorContentId,
|
|
67
|
+
cjsEntryMayHaveNamedExports,
|
|
68
|
+
cjsNamedExportFacade,
|
|
69
|
+
heavyProfRow,
|
|
70
|
+
clearVendorPipeline,
|
|
71
|
+
copyBrowserReadyEsmDist,
|
|
72
|
+
dropVendorBundle,
|
|
73
|
+
dropVendorGroup,
|
|
74
|
+
nextVendorTraceSeq,
|
|
75
|
+
outsideVendorSlot,
|
|
76
|
+
trackPreplanBuild,
|
|
77
|
+
rewriteEmittedRefs,
|
|
78
|
+
vendorBundle,
|
|
79
|
+
vendorBundleMemHit,
|
|
80
|
+
vendorTraceEnabled,
|
|
81
|
+
vendorTraceRow,
|
|
82
|
+
verifyVendorArtifact,
|
|
83
|
+
type VendorBuildPlan,
|
|
84
|
+
type VendorGroupMember,
|
|
85
|
+
type VendorGroupPlan,
|
|
86
|
+
} from './vendor';
|
|
87
|
+
|
|
88
|
+
const require = createRequire(import.meta.url);
|
|
89
|
+
|
|
90
|
+
// B4/B5: extra esbuild resolve conditions layered onto the server (RSC) vendor
|
|
91
|
+
// bundle. Core always applies `react-server` (the server graph IS the RSC
|
|
92
|
+
// layer). Compat can add `next-js` (only when cacheComponents is on) via the
|
|
93
|
+
// setter. Kept as a module-level seam so core carries no static edge into
|
|
94
|
+
// compat's config reader.
|
|
95
|
+
export type ServerBundleTarget =
|
|
96
|
+
| CompatAliasTarget
|
|
97
|
+
| 'edge'
|
|
98
|
+
| 'pages-api'
|
|
99
|
+
| 'pages-api-edge'
|
|
100
|
+
| 'pages'
|
|
101
|
+
| 'pages-edge';
|
|
102
|
+
|
|
103
|
+
let extraServerBundleConditions: (target: ServerBundleTarget) => string[] = () => [];
|
|
104
|
+
|
|
105
|
+
/** Install compat-driven extra vendor-bundle conditions (B5 `next-js`). */
|
|
106
|
+
export function setServerBundleConditions(
|
|
107
|
+
factory: ((target: ServerBundleTarget) => string[]) | undefined,
|
|
108
|
+
): void {
|
|
109
|
+
extraServerBundleConditions = factory ?? (() => []);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function serverBundleTargetForRuntime(runtime: string | undefined): ServerBundleTarget {
|
|
113
|
+
return runtime === 'edge' || runtime === 'experimental-edge' ? 'edge' : 'server';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function pagesApiBundleTargetForRuntime(runtime: string | undefined): ServerBundleTarget {
|
|
117
|
+
return runtime === 'edge' || runtime === 'experimental-edge' ? 'pages-api-edge' : 'pages-api';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Pages-router PAGE SSR layer: Next compiles it in the server bundle with `node` (or
|
|
122
|
+
* `edge-light`/`browser` for edge) export conditions but WITHOUT `react-server` - a pages page is
|
|
123
|
+
* not part of the RSC layer, so `library/react`-style react-server splits must resolve to `default`.
|
|
124
|
+
*/
|
|
125
|
+
export function pagesBundleTargetForRuntime(runtime: string | undefined): ServerBundleTarget {
|
|
126
|
+
return runtime === 'edge' || runtime === 'experimental-edge' ? 'pages-edge' : 'pages';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function serverBundleConditions(target: ServerBundleTarget): string[] {
|
|
130
|
+
// Preserve the original client-target conditions (module/import) to avoid
|
|
131
|
+
// regressing the browser SSR vendor bundle; only the RSC/server layer gains
|
|
132
|
+
// `react-server`. Compat-added conditions (next-js) prepend for either target.
|
|
133
|
+
const base =
|
|
134
|
+
target === 'client'
|
|
135
|
+
? ['module', 'import']
|
|
136
|
+
: target === 'edge'
|
|
137
|
+
? ['react-server', 'edge-light', 'browser', 'module', 'import']
|
|
138
|
+
: target === 'pages-api-edge'
|
|
139
|
+
? ['edge-light', 'browser', 'module', 'import']
|
|
140
|
+
: target === 'pages-api'
|
|
141
|
+
? ['node', 'require', 'import']
|
|
142
|
+
: target === 'pages-edge'
|
|
143
|
+
? ['edge-light', 'browser', 'module', 'import']
|
|
144
|
+
: target === 'pages'
|
|
145
|
+
? ['node', 'module', 'import']
|
|
146
|
+
: ['react-server', 'node', 'module', 'import'];
|
|
147
|
+
return [...new Set([...extraServerBundleConditions(target), ...base])];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function serverBundleRequireConditions(target: ServerBundleTarget): string[] {
|
|
151
|
+
const base =
|
|
152
|
+
target === 'client'
|
|
153
|
+
? ['module', 'require']
|
|
154
|
+
: target === 'edge'
|
|
155
|
+
? ['react-server', 'edge-light', 'browser', 'module', 'require']
|
|
156
|
+
: target === 'pages-api-edge'
|
|
157
|
+
? ['edge-light', 'browser', 'module', 'require']
|
|
158
|
+
: target === 'pages-api'
|
|
159
|
+
? ['node', 'require']
|
|
160
|
+
: target === 'pages-edge'
|
|
161
|
+
? ['edge-light', 'browser', 'module', 'require']
|
|
162
|
+
: target === 'pages'
|
|
163
|
+
? ['node', 'module', 'require']
|
|
164
|
+
: ['react-server', 'node', 'module', 'require'];
|
|
165
|
+
return [...new Set([...extraServerBundleConditions(target), ...base])];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const runtimeConfigs = new Map<string, RuntimeConfig>();
|
|
169
|
+
const aliasCache = new WeakMap<ResolvedConfig, Map<string, string>>();
|
|
170
|
+
const packageRootCache = new WeakMap<ResolvedConfig, string[]>();
|
|
171
|
+
const registeredSignatures = new WeakMap<ResolvedConfig, Set<string>>();
|
|
172
|
+
const rootPathCache = new Map<string, string[]>();
|
|
173
|
+
const runtimeConfigLookupCache = new Map<string, RuntimeConfig | null>();
|
|
174
|
+
const firstAliasCache = new Map<string, string | null>();
|
|
175
|
+
const transformCache = new Map<string, TransformCacheEntry>();
|
|
176
|
+
const nodeModuleTransformCache = new Map<string, TransformCacheEntry>();
|
|
177
|
+
// Freshness of generated `.pnext-require.cjs` sidecars (see inlineModuleScopeRequires).
|
|
178
|
+
const requireSidecarCache = new Map<string, { mtimeMs: number; size: number }>();
|
|
179
|
+
const registeredLoadRoots = new Set<string>();
|
|
180
|
+
let resolveRegistered = false;
|
|
181
|
+
let nodeModulesLoadRegistered = false;
|
|
182
|
+
// A live Bun.plugin forces every later import through the plugin pipeline (and
|
|
183
|
+
// pnext's own src through transformSource), so importing the compat graph with
|
|
184
|
+
// the plugins already up costs ~5x. build/start load compat before they ever
|
|
185
|
+
// register a runtime; dev arms the plugins on the first request instead.
|
|
186
|
+
let pluginsDeferred = false;
|
|
187
|
+
const deferredPluginRoots = new Set<string>();
|
|
188
|
+
|
|
189
|
+
const packageJsxLoaders = {
|
|
190
|
+
'.js': 'jsx',
|
|
191
|
+
'.mjs': 'jsx',
|
|
192
|
+
'.cjs': 'jsx',
|
|
193
|
+
} satisfies Record<string, Loader>;
|
|
194
|
+
|
|
195
|
+
interface RuntimeConfig {
|
|
196
|
+
root: string;
|
|
197
|
+
roots: Set<string>;
|
|
198
|
+
aliases: Map<string, string>;
|
|
199
|
+
/** Full resolved config, so compile sites can invoke config-aware extensions. */
|
|
200
|
+
resolved: ResolvedConfig;
|
|
201
|
+
missingImportError: (specifier: string) => string | undefined;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
interface TransformCacheEntry {
|
|
205
|
+
mtimeMs: number;
|
|
206
|
+
size: number;
|
|
207
|
+
code: string;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function registerServerRuntime(config: ResolvedConfig, sourceFiles: string[] = []) {
|
|
211
|
+
// Server-rendered Links emit canonical (slashed) hrefs under trailingSlash.
|
|
212
|
+
setTrailingSlashUrls(Boolean(config.trailingSlash));
|
|
213
|
+
// File-convention metadata asset hrefs (og-image, manifest) carry the
|
|
214
|
+
// basePath prefix; core render reads it through the href seam.
|
|
215
|
+
setBasePathPrefix(typeof config.basePath === 'string' ? config.basePath : '');
|
|
216
|
+
if (typeof Bun === 'undefined') return;
|
|
217
|
+
const sourceRoots = [...new Set(sourceFiles.map(file => sourceRootForFile(config, file)))].sort();
|
|
218
|
+
const signature = sourceRoots.join('\0');
|
|
219
|
+
let registered = registeredSignatures.get(config);
|
|
220
|
+
if (!registered) {
|
|
221
|
+
registered = new Set();
|
|
222
|
+
registeredSignatures.set(config, registered);
|
|
223
|
+
}
|
|
224
|
+
if (registered.has(signature)) return;
|
|
225
|
+
registered.add(signature);
|
|
226
|
+
|
|
227
|
+
const aliases = aliasesForConfig(config);
|
|
228
|
+
const roots = [
|
|
229
|
+
...rootPaths(config.appPath),
|
|
230
|
+
...rootPaths(path.join(config.outPath, 'cache', 'server')),
|
|
231
|
+
// The pnext src root, so the framework's own source transforms too.
|
|
232
|
+
...rootPaths(path.join(import.meta.dirname, '..')),
|
|
233
|
+
...sourceRoots.flatMap(rootPaths),
|
|
234
|
+
];
|
|
235
|
+
const key = rootPaths(config.appPath).join('\0');
|
|
236
|
+
const missingImportError = (specifier: string) =>
|
|
237
|
+
getImportAliasExtensions().missingImportError(config, specifier);
|
|
238
|
+
const existing = runtimeConfigs.get(key);
|
|
239
|
+
if (existing) {
|
|
240
|
+
for (const root of roots) existing.roots.add(root);
|
|
241
|
+
for (const [specifier, target] of aliases) existing.aliases.set(specifier, target);
|
|
242
|
+
existing.resolved = config;
|
|
243
|
+
existing.missingImportError = missingImportError;
|
|
244
|
+
} else {
|
|
245
|
+
runtimeConfigs.set(key, {
|
|
246
|
+
root: config.root,
|
|
247
|
+
roots: new Set(roots),
|
|
248
|
+
aliases: new Map(aliases),
|
|
249
|
+
resolved: config,
|
|
250
|
+
missingImportError,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
runtimeConfigLookupCache.clear();
|
|
254
|
+
firstAliasCache.clear();
|
|
255
|
+
if (pluginsDeferred) {
|
|
256
|
+
for (const root of roots) deferredPluginRoots.add(root);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
registerResolvePlugin();
|
|
260
|
+
registerNodeModulesLoadPlugin();
|
|
261
|
+
for (const root of roots) registerLoadPlugin(root);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Hold the Bun module plugins back until installServerRuntimePlugins() runs, so
|
|
266
|
+
* whatever the process still has to import of its own code loads natively.
|
|
267
|
+
*/
|
|
268
|
+
export function deferServerRuntimePlugins(): void {
|
|
269
|
+
pluginsDeferred = true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Arm the plugins deferServerRuntimePlugins() held back. Idempotent, cheap. */
|
|
273
|
+
export function installServerRuntimePlugins(): void {
|
|
274
|
+
if (!pluginsDeferred) return;
|
|
275
|
+
pluginsDeferred = false;
|
|
276
|
+
registerResolvePlugin();
|
|
277
|
+
registerNodeModulesLoadPlugin();
|
|
278
|
+
for (const root of deferredPluginRoots) registerLoadPlugin(root);
|
|
279
|
+
deferredPluginRoots.clear();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function clearServerRuntimeCaches() {
|
|
283
|
+
clearVendorPipeline();
|
|
284
|
+
clearVendorNativeCaches();
|
|
285
|
+
vendorEntryResolutions.clear();
|
|
286
|
+
clearVendorPackageResolutions();
|
|
287
|
+
vendorLayerRecords.clear();
|
|
288
|
+
vendorSharedEntries.clear();
|
|
289
|
+
vendorSharedLoaded.clear();
|
|
290
|
+
vendorReuseVerdicts.clear();
|
|
291
|
+
vendorReuseLoaded.clear();
|
|
292
|
+
vendorReuseProbes.clear();
|
|
293
|
+
vendorLayerProbeMemo.clear();
|
|
294
|
+
// The marker memo tracks files under `.pnext`; a wipe takes them with it.
|
|
295
|
+
esmDistPackages.clear();
|
|
296
|
+
clearProvidedEntryResolutions();
|
|
297
|
+
clearNodeModulesResolutionCache();
|
|
298
|
+
clearNodeModulesFsCache();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function resolveRuntimeTarget(target: string) {
|
|
302
|
+
return path.isAbsolute(target) ? target : require.resolve(target);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function aliasesForConfig(config: ResolvedConfig) {
|
|
306
|
+
let aliases = aliasCache.get(config);
|
|
307
|
+
if (!aliases) {
|
|
308
|
+
aliases = new Map<string, string>();
|
|
309
|
+
for (const [specifier, target] of Object.entries(coreAliases(config, 'server'))) {
|
|
310
|
+
aliases.set(specifier, resolveRuntimeTarget(target));
|
|
311
|
+
}
|
|
312
|
+
aliasCache.set(config, aliases);
|
|
313
|
+
}
|
|
314
|
+
return aliases;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function coreAliases(config: ResolvedConfig, target: CompatAliasTarget): Record<string, string> {
|
|
318
|
+
return {
|
|
319
|
+
...frameworkRuntimeAliasEntries(),
|
|
320
|
+
...pnextAliases(target),
|
|
321
|
+
...getImportAliasExtensions().aliases(config, target),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const aliasSpecifierFilter =
|
|
326
|
+
/^(?:@wular\/pnext\/cache|preact(?:\/|$)|react(?:-dom)?(?:\/|$)|react-compiler-runtime$|next(?:\/|$)|server-only$|client-only$|@vercel\/og$)/;
|
|
327
|
+
|
|
328
|
+
function registerResolvePlugin() {
|
|
329
|
+
if (resolveRegistered) return;
|
|
330
|
+
resolveRegistered = true;
|
|
331
|
+
Bun.plugin({
|
|
332
|
+
name: 'pnext-server-runtime-resolve',
|
|
333
|
+
setup(plugin) {
|
|
334
|
+
plugin.onResolve({ filter: aliasSpecifierFilter }, args => {
|
|
335
|
+
const runtime = runtimeConfigForFile(args.importer);
|
|
336
|
+
const resolved = runtime?.aliases.get(args.path) ?? firstAliasForSpecifier(args.path);
|
|
337
|
+
const target = resolved ? serverRequireAlias(args.path, resolved, args.kind) : undefined;
|
|
338
|
+
if (target) return { path: target };
|
|
339
|
+
const message = (runtime ?? firstRuntimeConfig())?.missingImportError(args.path);
|
|
340
|
+
if (message) throw new Error(message);
|
|
341
|
+
return undefined;
|
|
342
|
+
});
|
|
343
|
+
plugin.onResolve({ filter: /^[^./].*/ }, ({ path: specifier, importer }) => {
|
|
344
|
+
const runtime = runtimeConfigForFile(importer);
|
|
345
|
+
const target = runtime && resolveExternalLoadTarget({
|
|
346
|
+
root: runtime.root,
|
|
347
|
+
fromFile: importer,
|
|
348
|
+
specifier,
|
|
349
|
+
target: 'server',
|
|
350
|
+
});
|
|
351
|
+
if (!target) return undefined;
|
|
352
|
+
return { path: target };
|
|
353
|
+
});
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function registerLoadPlugin(root: string) {
|
|
359
|
+
if (registeredLoadRoots.has(root)) return;
|
|
360
|
+
registeredLoadRoots.add(root);
|
|
361
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
362
|
+
if (process.env.PNEXT_DEBUG_LOADPLUGIN) {
|
|
363
|
+
console.error(`[loadplugin] register root=${root} extras=${currentLoadExtras().join(',')}`);
|
|
364
|
+
}
|
|
365
|
+
loadPluginExtras.set(root, currentLoadExtras());
|
|
366
|
+
Bun.plugin({
|
|
367
|
+
name: `pnext-server-runtime-load-${hashRoot(root)}`,
|
|
368
|
+
setup(plugin) {
|
|
369
|
+
plugin.onLoad({ filter: rootFilter(root) }, async ({ path: file }) => ({
|
|
370
|
+
contents: await transformSource(file),
|
|
371
|
+
loader: 'js',
|
|
372
|
+
}));
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// The load plugin's filter regex latches the page extensions known at registration time. A root registered
|
|
378
|
+
// BEFORE compat adds `mdx`/`md` (boot order shifts under load) would let those modules fall through to Bun's
|
|
379
|
+
// file loader, whose default export is the file path and renders as a bogus JSX tag - so a late registration
|
|
380
|
+
// extends every latched root with an extras-only plugin.
|
|
381
|
+
const loadPluginExtras = new Map<string, string[]>();
|
|
382
|
+
|
|
383
|
+
function currentLoadExtras(): string[] {
|
|
384
|
+
return extraLoadableExtensions().filter(ext => /^[a-z0-9]+$/i.test(ext));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
onLoadableExtensionsChanged(() => {
|
|
388
|
+
for (const [root, seen] of loadPluginExtras) {
|
|
389
|
+
const fresh = currentLoadExtras().filter(ext => !seen.includes(ext));
|
|
390
|
+
if (fresh.length === 0) continue;
|
|
391
|
+
loadPluginExtras.set(root, [...seen, ...fresh]);
|
|
392
|
+
const exts = fresh.map(escapeRegex).join('|');
|
|
393
|
+
const filter = new RegExp(
|
|
394
|
+
`^${escapeRegex(root)}(?!.*\\/node_modules\\/)(?:/.*)?\\.(?:${exts})$`,
|
|
395
|
+
);
|
|
396
|
+
Bun.plugin({
|
|
397
|
+
name: `pnext-server-runtime-load-extras-${hashRoot(root)}-${fresh.join('-')}`,
|
|
398
|
+
setup(plugin) {
|
|
399
|
+
plugin.onLoad({ filter }, async ({ path: file }) => ({
|
|
400
|
+
contents: await transformSource(file),
|
|
401
|
+
loader: 'js',
|
|
402
|
+
}));
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
function registerNodeModulesLoadPlugin() {
|
|
409
|
+
if (nodeModulesLoadRegistered) return;
|
|
410
|
+
nodeModulesLoadRegistered = true;
|
|
411
|
+
Bun.plugin({
|
|
412
|
+
name: 'pnext-server-node-modules-load',
|
|
413
|
+
setup(plugin) {
|
|
414
|
+
plugin.onLoad(
|
|
415
|
+
{
|
|
416
|
+
filter:
|
|
417
|
+
/\/node_modules\/.*(?:\.mjs|\.esm\.js|\/(?:es|esm|dist\/esm|build\/modern)\/.*\.js)$/,
|
|
418
|
+
},
|
|
419
|
+
async ({ path: file }) => {
|
|
420
|
+
const code = await transformNodeModuleSource(file);
|
|
421
|
+
return { contents: code, loader: 'js' };
|
|
422
|
+
},
|
|
423
|
+
);
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function runtimeConfigForFile(file: string | undefined) {
|
|
429
|
+
if (!file) return undefined;
|
|
430
|
+
const key = path.resolve(file);
|
|
431
|
+
if (runtimeConfigLookupCache.has(key)) {
|
|
432
|
+
return runtimeConfigLookupCache.get(key) ?? undefined;
|
|
433
|
+
}
|
|
434
|
+
for (const config of runtimeConfigs.values()) {
|
|
435
|
+
for (const root of config.roots) {
|
|
436
|
+
if (isInside(root, file)) {
|
|
437
|
+
runtimeConfigLookupCache.set(key, config);
|
|
438
|
+
return config;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
runtimeConfigLookupCache.set(key, null);
|
|
443
|
+
return undefined;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function sourceRootForFile(config: ResolvedConfig, file: string) {
|
|
447
|
+
const dir = path.dirname(file);
|
|
448
|
+
if (isInside(config.appPath, dir)) return config.appPath;
|
|
449
|
+
return packageRootsForConfig(config).find(root => isInside(root, dir)) ?? dir;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function packageRootsForConfig(config: ResolvedConfig) {
|
|
453
|
+
let roots = packageRootCache.get(config);
|
|
454
|
+
if (!roots) {
|
|
455
|
+
roots = workspacePackageRoots(config.workspaceRoot);
|
|
456
|
+
packageRootCache.set(config, roots);
|
|
457
|
+
}
|
|
458
|
+
return roots;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function firstRuntimeConfig() {
|
|
462
|
+
for (const config of runtimeConfigs.values()) return config;
|
|
463
|
+
return undefined;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function firstAliasForSpecifier(specifier: string) {
|
|
467
|
+
if (firstAliasCache.has(specifier)) return firstAliasCache.get(specifier) ?? undefined;
|
|
468
|
+
for (const config of runtimeConfigs.values()) {
|
|
469
|
+
const target = config.aliases.get(specifier);
|
|
470
|
+
if (target) {
|
|
471
|
+
firstAliasCache.set(specifier, target);
|
|
472
|
+
return target;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
firstAliasCache.set(specifier, null);
|
|
476
|
+
return undefined;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function transformSource(file: string) {
|
|
480
|
+
const fileStat = await stat(file);
|
|
481
|
+
const cached = transformCache.get(file);
|
|
482
|
+
if (cached?.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) {
|
|
483
|
+
return cached.code;
|
|
484
|
+
}
|
|
485
|
+
// The in-memory cache is empty on every restart, so a restarted server
|
|
486
|
+
// re-transforms the whole materialized module set on the request path.
|
|
487
|
+
const diskCache = transformCacheFile(file, fileStat);
|
|
488
|
+
if (diskCache) {
|
|
489
|
+
const code = await readFile(diskCache, 'utf8').catch(() => undefined);
|
|
490
|
+
if (code !== undefined) {
|
|
491
|
+
transformCache.set(file, { mtimeMs: fileStat.mtimeMs, size: fileStat.size, code });
|
|
492
|
+
return code;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const runtime = runtimeConfigForFile(file);
|
|
497
|
+
let raw = await readFile(file, 'utf8');
|
|
498
|
+
// Raw markdown compiles through the MDX pipeline first; materialized
|
|
499
|
+
// cache/server copies were already compiled in place (dev/imports) and would
|
|
500
|
+
// be corrupted by a second pass.
|
|
501
|
+
if (/\.mdx?$/.test(file) && !file.includes(`${path.sep}cache${path.sep}server${path.sep}`)) {
|
|
502
|
+
const { compileMdx } = await import('../compat/mdx/compile');
|
|
503
|
+
raw = (await compileMdx(raw, file, runtime?.root ?? path.dirname(file))).code;
|
|
504
|
+
}
|
|
505
|
+
const source = await inlineModuleScopeRequires(
|
|
506
|
+
rewriteServerSource(raw, file, {
|
|
507
|
+
root: runtime?.root,
|
|
508
|
+
}),
|
|
509
|
+
file,
|
|
510
|
+
runtime?.root,
|
|
511
|
+
);
|
|
512
|
+
const result = await transform(source, {
|
|
513
|
+
loader: esbuildLoader(file),
|
|
514
|
+
jsx: 'automatic',
|
|
515
|
+
jsxImportSource: 'preact',
|
|
516
|
+
sourcefile: file,
|
|
517
|
+
target: 'es2022',
|
|
518
|
+
...serverDefineOptions(),
|
|
519
|
+
});
|
|
520
|
+
transformCache.set(file, {
|
|
521
|
+
mtimeMs: fileStat.mtimeMs,
|
|
522
|
+
size: fileStat.size,
|
|
523
|
+
code: result.code,
|
|
524
|
+
});
|
|
525
|
+
if (diskCache) {
|
|
526
|
+
await mkdir(path.dirname(diskCache), { recursive: true }).catch(() => undefined);
|
|
527
|
+
void writeFileAtomic(diskCache, result.code).catch(() => undefined);
|
|
528
|
+
}
|
|
529
|
+
return result.code;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
533
|
+
const transformDiskCacheDisabled = process.env.PNEXT_TRANSFORM_DISK_CACHE === '0';
|
|
534
|
+
const materializedSegment = `${path.sep}cache${path.sep}server${path.sep}`;
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Where a transform of `file` is remembered across restarts. Only for the
|
|
538
|
+
* materialized `cache/server` modules: they are hash-named build output under
|
|
539
|
+
* our own cache root, so the key (path + mtime + size + the define/extension
|
|
540
|
+
* inputs of the transform) is complete, and the whole tree is discarded when
|
|
541
|
+
* a build or a config change rewrites it.
|
|
542
|
+
*/
|
|
543
|
+
function transformCacheFile(file: string, fileStat: { mtimeMs: number; size: number }) {
|
|
544
|
+
if (transformDiskCacheDisabled) return undefined;
|
|
545
|
+
const at = file.lastIndexOf(materializedSegment);
|
|
546
|
+
if (at === -1) return undefined;
|
|
547
|
+
const cacheRoot = file.slice(0, at + materializedSegment.length);
|
|
548
|
+
const key = `${file}\0${fileStat.mtimeMs}\0${fileStat.size}\0${currentLoadExtras().join(',')}\0${JSON.stringify(serverDefineOptions())}`;
|
|
549
|
+
return path.join(cacheRoot, 'transform', `${hashBundleSpecifier(key)}.js`);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// A synchronous `require('./x')` of an app source module fails at runtime with `require() async
|
|
553
|
+
// module ... is unsupported`: the load plugin's onLoad hook makes every app `[jt]sx?` module an
|
|
554
|
+
// ASYNC Bun module, and Bun cannot `require` one. Bun does not route relative `require-call`s
|
|
555
|
+
// through onResolve plugins (only bare specifiers reach them), so the alias trick cannot intercept
|
|
556
|
+
// them at resolve time. Instead each such require is rewritten to a sibling
|
|
557
|
+
// `<x>.pnext-require.cjs` compiled to CommonJS: a real `.cjs` on disk is deliberately excluded by
|
|
558
|
+
// rootFilter, so Bun loads it natively and synchronously.
|
|
559
|
+
//
|
|
560
|
+
// Only requires that resolve to a rootFilter-matched app module are rewritten (a
|
|
561
|
+
// `.cjs`/`.json`/node_modules require already loads synchronously). The sidecar is a distinct module
|
|
562
|
+
// instance from the module's ESM copy - acceptable here, since the require path was an outright
|
|
563
|
+
// crash before and such modules are typically leaf value modules.
|
|
564
|
+
//
|
|
565
|
+
// Both the source form and the BUILT form are matched: the server bundle keeps such a require
|
|
566
|
+
// unbundled and emits esbuild's `__require("file:///abs/path/...")` shim call, which loads the very
|
|
567
|
+
// same async app module at runtime.
|
|
568
|
+
const REQUIRE_CALL = /\b(?:__)?require\(\s*(['"])((?:\.\.?\/|\/|file:\/\/)[^'"]+)\1\s*\)/g;
|
|
569
|
+
|
|
570
|
+
async function inlineModuleScopeRequires(
|
|
571
|
+
source: string,
|
|
572
|
+
file: string,
|
|
573
|
+
root: string | undefined,
|
|
574
|
+
): Promise<string> {
|
|
575
|
+
if (!root || !source.includes('require(')) return source;
|
|
576
|
+
const filter = rootFilter(root);
|
|
577
|
+
const sidecars = new Map<string, string>();
|
|
578
|
+
const pending: Promise<void>[] = [];
|
|
579
|
+
const seen = new Set<string>();
|
|
580
|
+
for (const match of source.matchAll(REQUIRE_CALL)) {
|
|
581
|
+
const specifier = match[2]!;
|
|
582
|
+
if (seen.has(specifier)) continue;
|
|
583
|
+
seen.add(specifier);
|
|
584
|
+
const target = requireCallTarget(root, file, specifier);
|
|
585
|
+
if (!target || !filter.test(target)) continue;
|
|
586
|
+
pending.push(
|
|
587
|
+
requireCjsSidecar(target).then(sidecar => {
|
|
588
|
+
if (sidecar) sidecars.set(specifier, sidecar);
|
|
589
|
+
}),
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
if (pending.length === 0) return source;
|
|
593
|
+
await Promise.all(pending);
|
|
594
|
+
if (sidecars.size === 0) return source;
|
|
595
|
+
return source.replace(REQUIRE_CALL, (whole, quote: string, specifier: string) => {
|
|
596
|
+
const sidecar = sidecars.get(specifier);
|
|
597
|
+
return sidecar ? `require(${quote}${sidecar}${quote})` : whole;
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/** The module a rewritable `require(...)` call resolves to, if any. */
|
|
602
|
+
function requireCallTarget(root: string, file: string, specifier: string): string | undefined {
|
|
603
|
+
if (specifier.startsWith('file://')) {
|
|
604
|
+
try {
|
|
605
|
+
return resolveImport(root, file, fileURLToPath(specifier));
|
|
606
|
+
} catch {
|
|
607
|
+
return undefined;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return resolveImport(root, file, specifier);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Compile an app module to a sibling `.pnext-require.cjs` (CommonJS), reusing a
|
|
614
|
+
// mtime/size-cached copy. Returns the sidecar path, or undefined on any failure
|
|
615
|
+
// (the original require is then left as-is). The module runs through the same
|
|
616
|
+
// server source pipeline as a normal load so the CJS copy behaves like the ESM
|
|
617
|
+
// one; nested module-scope requires inside it are NOT recursively inlined.
|
|
618
|
+
async function requireCjsSidecar(target: string): Promise<string | undefined> {
|
|
619
|
+
try {
|
|
620
|
+
const sidecar = requireCjsSidecarPath(target);
|
|
621
|
+
const fileStat = await stat(target);
|
|
622
|
+
const cached = requireSidecarCache.get(target);
|
|
623
|
+
if (cached?.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return sidecar;
|
|
624
|
+
const runtime = runtimeConfigForFile(target);
|
|
625
|
+
const source = rewriteServerSource(await readFile(target, 'utf8'), target, {
|
|
626
|
+
root: runtime?.root,
|
|
627
|
+
});
|
|
628
|
+
const result = await transform(source, {
|
|
629
|
+
loader: esbuildLoader(target),
|
|
630
|
+
jsx: 'automatic',
|
|
631
|
+
jsxImportSource: 'preact',
|
|
632
|
+
sourcefile: target,
|
|
633
|
+
target: 'es2022',
|
|
634
|
+
format: 'cjs',
|
|
635
|
+
...serverDefineOptions(),
|
|
636
|
+
});
|
|
637
|
+
await writeFile(sidecar, result.code);
|
|
638
|
+
requireSidecarCache.set(target, { mtimeMs: fileStat.mtimeMs, size: fileStat.size });
|
|
639
|
+
return sidecar;
|
|
640
|
+
} catch {
|
|
641
|
+
return undefined;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function requireCjsSidecarPath(target: string): string {
|
|
646
|
+
return `${target.replace(/\.[^./\\]+$/, '')}.pnext-require.cjs`;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async function transformNodeModuleSource(file: string) {
|
|
650
|
+
const fileStat = await stat(file);
|
|
651
|
+
const cached = nodeModuleTransformCache.get(file);
|
|
652
|
+
if (cached?.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) {
|
|
653
|
+
return cached.code;
|
|
654
|
+
}
|
|
655
|
+
// The in-memory cache is empty on every restart, so a restarted server
|
|
656
|
+
// re-BUNDLES each of these packages on the request path.
|
|
657
|
+
const record = nodeModuleBundleRecordFile(file, fileStat);
|
|
658
|
+
const reused = record ? await readNodeModuleBundle(record) : undefined;
|
|
659
|
+
if (reused !== undefined) {
|
|
660
|
+
nodeModuleTransformCache.set(file, { mtimeMs: fileStat.mtimeMs, size: fileStat.size, code: reused });
|
|
661
|
+
return reused;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const { code, inputs } = await bundleNodeModuleSource(file);
|
|
665
|
+
nodeModuleTransformCache.set(file, {
|
|
666
|
+
mtimeMs: fileStat.mtimeMs,
|
|
667
|
+
size: fileStat.size,
|
|
668
|
+
code,
|
|
669
|
+
});
|
|
670
|
+
if (record) void writeNodeModuleBundle(record, inputs, code);
|
|
671
|
+
return code;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Where a bundle of `file` is remembered across restarts. The entry's own stat is in the name, but
|
|
676
|
+
* the build inlines the package's relative graph too, so the record carries every esbuild input and
|
|
677
|
+
* is only reused while all of them still stat the same. A record that cannot be validated is a miss.
|
|
678
|
+
*/
|
|
679
|
+
function nodeModuleBundleRecordFile(file: string, fileStat: { mtimeMs: number; size: number }) {
|
|
680
|
+
if (transformDiskCacheDisabled) return undefined;
|
|
681
|
+
const outPath = (runtimeConfigForFile(file) ?? firstRuntimeConfig())?.resolved.outPath;
|
|
682
|
+
if (!outPath) return undefined;
|
|
683
|
+
const key = `${file}\0${fileStat.mtimeMs}\0${fileStat.size}\0${currentLoadExtras().join(',')}\0${JSON.stringify(serverDefineOptions())}`;
|
|
684
|
+
return path.join(cacheRoot(outPath), 'node-module-bundle', `${hashBundleSpecifier(key)}.json`);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
async function bundleNodeModuleSource(file: string) {
|
|
688
|
+
const resolved = runtimeConfigForFile(file)?.resolved;
|
|
689
|
+
const extensionPlugins = resolved ? getBundlerExtensions().serverEsbuildPlugins(resolved) : [];
|
|
690
|
+
const result = await build({
|
|
691
|
+
// The marker entry only resolves when the extension chain is present.
|
|
692
|
+
entryPoints: [
|
|
693
|
+
(resolved
|
|
694
|
+
? getBundlerExtensions().serverBundleEntry(file, path.dirname(file), file)
|
|
695
|
+
: undefined) ?? file,
|
|
696
|
+
],
|
|
697
|
+
bundle: true,
|
|
698
|
+
write: false,
|
|
699
|
+
format: 'esm',
|
|
700
|
+
platform: 'neutral',
|
|
701
|
+
target: 'es2022',
|
|
702
|
+
loader: packageJsxLoaders,
|
|
703
|
+
jsx: 'automatic',
|
|
704
|
+
jsxImportSource: 'preact',
|
|
705
|
+
packages: 'external',
|
|
706
|
+
logLevel: 'silent',
|
|
707
|
+
metafile: true,
|
|
708
|
+
...serverDefineOptions(),
|
|
709
|
+
plugins: [
|
|
710
|
+
serverAssetPlugin(),
|
|
711
|
+
...extensionPlugins,
|
|
712
|
+
runtimeAliasBuildPlugin(undefined, 'server', { bundleRequireAliases: true }),
|
|
713
|
+
],
|
|
714
|
+
});
|
|
715
|
+
const output = result.outputFiles[0];
|
|
716
|
+
if (!output) throw new Error(`Failed to bundle ${file}`);
|
|
717
|
+
// Packages can ship 'use cache' functions too (Next allows it); everything
|
|
718
|
+
// else in the source chain was already settled by the bundle above (imports
|
|
719
|
+
// resolved by the plugins, constants inlined by esbuild `define`).
|
|
720
|
+
return {
|
|
721
|
+
code: applyBundledSourceTransforms(output.text, file),
|
|
722
|
+
inputs: Object.keys(result.metafile.inputs).map(input => path.resolve(input)),
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* The one vendor-compile entry point. Every demand - a module compile's import scan, an esbuild
|
|
728
|
+
* resolve callback, a transitive dependency of another vendor bundle - arrives here and is scheduled
|
|
729
|
+
* by the pipeline in `./vendor`. `nested` marks demands raised from inside a running vendor build;
|
|
730
|
+
* see `withVendorSlot` for why they must not queue.
|
|
731
|
+
*/
|
|
732
|
+
export async function externalServerPackageHref(
|
|
733
|
+
config: ResolvedConfig,
|
|
734
|
+
specifier: string,
|
|
735
|
+
target: CompatAliasTarget,
|
|
736
|
+
resolveDir = config.root,
|
|
737
|
+
conditionTarget: ServerBundleTarget = target,
|
|
738
|
+
nested = false,
|
|
739
|
+
) {
|
|
740
|
+
let plan = vendorBuildPlan(config, specifier, target, resolveDir, conditionTarget);
|
|
741
|
+
const aliased = await crossLayerReusePlan(config, specifier, resolveDir, target, conditionTarget, plan);
|
|
742
|
+
if (aliased) plan = aliased;
|
|
743
|
+
if (vendorTraceEnabled()) {
|
|
744
|
+
vendorTraceRow({
|
|
745
|
+
kind: 'request',
|
|
746
|
+
specifier,
|
|
747
|
+
resolveDir,
|
|
748
|
+
target,
|
|
749
|
+
conditionTarget,
|
|
750
|
+
memHit: vendorBundleMemHit(plan.key),
|
|
751
|
+
atMs: performance.now(),
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
let file = await vendorBundle(plan, nested);
|
|
755
|
+
// A `pnext build` wiping the output directory under a running server
|
|
756
|
+
// deletes vendor bundles; re-write instead of handing out a dead path.
|
|
757
|
+
if (!cachedExistsSync(file) || !(await compiledModuleUsable(file))) {
|
|
758
|
+
dropVendorBundle(plan.key, file);
|
|
759
|
+
if (plan.group) dropVendorGroup(plan.group.key);
|
|
760
|
+
file = await vendorBundle(plan, nested);
|
|
761
|
+
}
|
|
762
|
+
return pathToFileHref(file);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Artifact identity: what the bundle is compiled FROM, never who asked. Keying on the importer's
|
|
767
|
+
* directory (what `require.resolve` fell back to when a package publishes no `require` condition)
|
|
768
|
+
* split one package into one build per importing directory.
|
|
769
|
+
*/
|
|
770
|
+
function vendorArtifactKey(
|
|
771
|
+
config: ResolvedConfig,
|
|
772
|
+
target: CompatAliasTarget,
|
|
773
|
+
conditionTarget: ServerBundleTarget,
|
|
774
|
+
source: string,
|
|
775
|
+
) {
|
|
776
|
+
return `${config.outPath}\0${target}\0${conditionTarget}\0${source}`;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function vendorArtifactFile(config: ResolvedConfig, key: string) {
|
|
780
|
+
return path.join(config.outPath, 'cache', 'server', 'vendor', `${hashBundleSpecifier(key)}.mjs`);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* The file this specifier names - the artifact's identity. Resolved with `import` on top of the
|
|
785
|
+
* layer's conditions because that is what esbuild adds for an ESM entry point: without it a package
|
|
786
|
+
* publishing only an `import` condition resolves to nothing, and the key falls back to the
|
|
787
|
+
* importer's directory - the duplicate-build shape this path exists to remove.
|
|
788
|
+
*/
|
|
789
|
+
function resolveVendorEntry(
|
|
790
|
+
config: ResolvedConfig,
|
|
791
|
+
specifier: string,
|
|
792
|
+
resolveDir: string,
|
|
793
|
+
conditionTarget: ServerBundleTarget,
|
|
794
|
+
) {
|
|
795
|
+
if (path.isAbsolute(specifier)) return path.resolve(specifier);
|
|
796
|
+
// Every demand builds a plan before dedup can see it, so this resolve runs
|
|
797
|
+
// once per DEMAND, not once per artifact — and the same specifier arrives
|
|
798
|
+
// from hundreds of importers. The answer is pure in its inputs.
|
|
799
|
+
const key = `${config.root}\0${conditionTarget}\0${resolveDir}\0${specifier}`;
|
|
800
|
+
if (vendorEntryResolutions.has(key)) return vendorEntryResolutions.get(key);
|
|
801
|
+
const entry = resolveVendorPackageSpecifier(
|
|
802
|
+
config.root,
|
|
803
|
+
path.join(resolveDir, 'pnext-resolve.ts'),
|
|
804
|
+
specifier,
|
|
805
|
+
[...serverBundleConditions(conditionTarget), 'import'],
|
|
806
|
+
);
|
|
807
|
+
vendorEntryResolutions.set(key, entry);
|
|
808
|
+
return entry;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const vendorEntryResolutions = new Map<string, string | undefined>();
|
|
812
|
+
|
|
813
|
+
function vendorBuildPlan(
|
|
814
|
+
config: ResolvedConfig,
|
|
815
|
+
specifier: string,
|
|
816
|
+
target: CompatAliasTarget,
|
|
817
|
+
resolveDir: string,
|
|
818
|
+
conditionTarget: ServerBundleTarget,
|
|
819
|
+
): VendorBuildPlan {
|
|
820
|
+
const entry = resolveVendorEntry(config, specifier, resolveDir, conditionTarget);
|
|
821
|
+
// An unresolvable specifier has no artifact identity to key on; keep the old
|
|
822
|
+
// importer-scoped key so the demand still compiles (and still dedups per dir).
|
|
823
|
+
const key = vendorArtifactKey(
|
|
824
|
+
config,
|
|
825
|
+
target,
|
|
826
|
+
conditionTarget,
|
|
827
|
+
entry ?? `${path.resolve(resolveDir)}\0${specifier}`,
|
|
828
|
+
);
|
|
829
|
+
const file = vendorArtifactFile(config, key);
|
|
830
|
+
return {
|
|
831
|
+
key,
|
|
832
|
+
file,
|
|
833
|
+
prepare: () =>
|
|
834
|
+
prepareVendorArtifact(config, specifier, resolveDir, conditionTarget, file, target, entry),
|
|
835
|
+
buildOne: nested =>
|
|
836
|
+
writeExternalServerPackageBundle(
|
|
837
|
+
config,
|
|
838
|
+
specifier,
|
|
839
|
+
target,
|
|
840
|
+
resolveDir,
|
|
841
|
+
file,
|
|
842
|
+
conditionTarget,
|
|
843
|
+
nested,
|
|
844
|
+
),
|
|
845
|
+
group: entry
|
|
846
|
+
? vendorGroupPlan(config, { specifier, entry, file }, target, resolveDir, conditionTarget)
|
|
847
|
+
: undefined,
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* A vendor artifact this process just wrote is trusted for the rest of the run.
|
|
853
|
+
* Without the note, a cache folder that started without a marker (every cold
|
|
854
|
+
* boot) distrusts what was just written, and `externalServerPackageHref`'s
|
|
855
|
+
* liveness check rebuilds every bundle a second time.
|
|
856
|
+
*/
|
|
857
|
+
async function writeVendorArtifact(file: string, code: string) {
|
|
858
|
+
const id = await publishVendorContent(file, code);
|
|
859
|
+
if (id === undefined) {
|
|
860
|
+
await writeFileAtomic(file, code);
|
|
861
|
+
noteDevArtifactWritten(file);
|
|
862
|
+
}
|
|
863
|
+
return id;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// --------------------------------------------------------------------------
|
|
867
|
+
// content-addressed artifacts and the cross-layer verdict
|
|
868
|
+
// --------------------------------------------------------------------------
|
|
869
|
+
|
|
870
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
871
|
+
const vendorContentDisabled = () => process.env.PNEXT_VENDOR_CONTENT === '0';
|
|
872
|
+
|
|
873
|
+
function vendorContentFile(vendorDir: string, id: string) {
|
|
874
|
+
return path.join(vendorDir, `${id}.content.mjs`);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/** Entries only: a chunk reaches its entry by relative path, so it stays put. */
|
|
878
|
+
async function publishVendorContent(file: string, code: string) {
|
|
879
|
+
const vendorDir = path.dirname(file);
|
|
880
|
+
if (vendorContentDisabled() || path.basename(vendorDir) !== 'vendor') return undefined;
|
|
881
|
+
const id = hashBundleSpecifier(canonicalVendorCode(code));
|
|
882
|
+
const content = vendorContentFile(vendorDir, id);
|
|
883
|
+
if (!cachedExistsSync(content)) await writeFileAtomic(content, code);
|
|
884
|
+
noteDevArtifactWritten(content);
|
|
885
|
+
await linkVendorArtifact(file, content);
|
|
886
|
+
noteVendorContentId(file, id);
|
|
887
|
+
noteDevArtifactWritten(file);
|
|
888
|
+
return id;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/** The layer-keyed path becomes an alias to the shared bytes. */
|
|
892
|
+
async function linkVendorArtifact(file: string, content: string) {
|
|
893
|
+
await rm(file, { force: true }).catch(() => undefined);
|
|
894
|
+
try {
|
|
895
|
+
await symlink(path.basename(content), file);
|
|
896
|
+
} catch {
|
|
897
|
+
await copyFile(content, file);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/** What one layer's build of an entry saw, for the cross-layer comparison. */
|
|
902
|
+
interface VendorLayerRecord {
|
|
903
|
+
inputs: string;
|
|
904
|
+
externals: string;
|
|
905
|
+
blocked: boolean;
|
|
906
|
+
id: string;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Keyed by output path exactly as `vendorArtifactKey` is: a verdict holds for
|
|
910
|
+
// ONE app's conditions, aliases and defines, and several run in one process.
|
|
911
|
+
const vendorLayerRecords = new Map<string, Map<string, VendorLayerRecord>>();
|
|
912
|
+
/** Entries proven layer-independent -> the content id both layers resolve to. */
|
|
913
|
+
const vendorSharedEntries = new Map<string, Map<string, string>>();
|
|
914
|
+
const vendorSharedLoaded = new Set<string>();
|
|
915
|
+
|
|
916
|
+
function vendorSharedFor(config: ResolvedConfig) {
|
|
917
|
+
let shared = vendorSharedEntries.get(config.outPath);
|
|
918
|
+
if (!shared) {
|
|
919
|
+
shared = new Map();
|
|
920
|
+
vendorSharedEntries.set(config.outPath, shared);
|
|
921
|
+
}
|
|
922
|
+
return shared;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/** Only these two layers were ever the duplicated half. */
|
|
926
|
+
function vendorLayerKey(target: CompatAliasTarget, conditionTarget: ServerBundleTarget) {
|
|
927
|
+
if (target === 'server' && conditionTarget === 'server') return 'server';
|
|
928
|
+
if (target === 'client' && conditionTarget === 'client') return 'client';
|
|
929
|
+
return undefined;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function vendorSharedFile(config: ResolvedConfig) {
|
|
933
|
+
return path.join(config.outPath, 'cache', 'server', 'vendor', 'layer-shared.json');
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/** Verdicts from earlier runs, so reuse lands on the first demand of this one. */
|
|
937
|
+
function loadVendorSharedEntries(config: ResolvedConfig) {
|
|
938
|
+
if (vendorSharedLoaded.has(config.outPath)) return;
|
|
939
|
+
vendorSharedLoaded.add(config.outPath);
|
|
940
|
+
try {
|
|
941
|
+
const stored = JSON.parse(readFileSync(vendorSharedFile(config), 'utf8')) as Record<string, string>;
|
|
942
|
+
const shared = vendorSharedFor(config);
|
|
943
|
+
for (const [entry, id] of Object.entries(stored)) shared.set(entry, id);
|
|
944
|
+
} catch {
|
|
945
|
+
// No verdicts yet; they are written as builds settle.
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function persistVendorSharedEntries(config: ResolvedConfig) {
|
|
950
|
+
void writeFileAtomic(
|
|
951
|
+
vendorSharedFile(config),
|
|
952
|
+
JSON.stringify(Object.fromEntries(vendorSharedFor(config))),
|
|
953
|
+
).catch(() => undefined);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/** Offline verdict: externals compare as content ids, so this is a fixpoint. */
|
|
957
|
+
function recordVendorLayerBuild(
|
|
958
|
+
config: ResolvedConfig,
|
|
959
|
+
entry: string | undefined,
|
|
960
|
+
target: CompatAliasTarget,
|
|
961
|
+
conditionTarget: ServerBundleTarget,
|
|
962
|
+
metafile: Metafile | undefined,
|
|
963
|
+
code: string,
|
|
964
|
+
id: string | undefined,
|
|
965
|
+
) {
|
|
966
|
+
const layer = entry && id && metafile && vendorLayerKey(target, conditionTarget);
|
|
967
|
+
if (!layer || !entry || !id || !metafile) return;
|
|
968
|
+
loadVendorSharedEntries(config);
|
|
969
|
+
const externals = new Set<string>();
|
|
970
|
+
let blocked = /['"]use (?:client|server|cache)['"]/.test(code);
|
|
971
|
+
for (const meta of Object.values(metafile.inputs)) {
|
|
972
|
+
for (const reference of meta.imports) {
|
|
973
|
+
if (!reference.external) continue;
|
|
974
|
+
const target_ = reference.path.startsWith('file:')
|
|
975
|
+
? fileURLToPath(reference.path)
|
|
976
|
+
: undefined;
|
|
977
|
+
// A shim outside the vendor tree is this layer's own alias target: the
|
|
978
|
+
// artifact is layer-specific whatever else matches.
|
|
979
|
+
if (target_ && path.basename(path.dirname(target_)) !== 'vendor') blocked = true;
|
|
980
|
+
externals.add(target_ ? (vendorContentId(target_) ?? target_) : reference.path);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
const record: VendorLayerRecord = {
|
|
984
|
+
inputs: Object.keys(metafile.inputs).sort().join('\n'),
|
|
985
|
+
externals: [...externals].sort().join('\n'),
|
|
986
|
+
blocked,
|
|
987
|
+
id,
|
|
988
|
+
};
|
|
989
|
+
const recordKey = `${config.outPath}\0${entry}`;
|
|
990
|
+
let byLayer = vendorLayerRecords.get(recordKey);
|
|
991
|
+
if (!byLayer) {
|
|
992
|
+
byLayer = new Map();
|
|
993
|
+
vendorLayerRecords.set(recordKey, byLayer);
|
|
994
|
+
}
|
|
995
|
+
byLayer.set(layer, record);
|
|
996
|
+
const server = byLayer.get('server');
|
|
997
|
+
const client = byLayer.get('client');
|
|
998
|
+
if (!server || !client || server.blocked || client.blocked) return;
|
|
999
|
+
if (server.inputs !== client.inputs || server.externals !== client.externals) return;
|
|
1000
|
+
vendorSharedFor(config).set(entry, server.id);
|
|
1001
|
+
persistVendorSharedEntries(config);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/** A proven-shared entry's content file, or undefined to compile as usual. */
|
|
1005
|
+
function reusableVendorContent(
|
|
1006
|
+
config: ResolvedConfig,
|
|
1007
|
+
entry: string | undefined,
|
|
1008
|
+
target: CompatAliasTarget,
|
|
1009
|
+
conditionTarget: ServerBundleTarget,
|
|
1010
|
+
) {
|
|
1011
|
+
if (vendorContentDisabled() || !entry || !vendorLayerKey(target, conditionTarget)) return undefined;
|
|
1012
|
+
loadVendorSharedEntries(config);
|
|
1013
|
+
const id = vendorSharedFor(config).get(entry);
|
|
1014
|
+
if (!id) return undefined;
|
|
1015
|
+
const content = vendorContentFile(path.join(config.outPath, 'cache', 'server', 'vendor'), id);
|
|
1016
|
+
return existsSync(content) ? content : undefined;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// --------------------------------------------------------------------------
|
|
1020
|
+
// pre-build cross-layer reuse verdict (PNEXT_VENDOR_REUSE=0 to disable)
|
|
1021
|
+
// --------------------------------------------------------------------------
|
|
1022
|
+
|
|
1023
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
1024
|
+
const vendorReuseEnabled = () => process.env.PNEXT_VENDOR_REUSE !== '0';
|
|
1025
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
1026
|
+
const vendorReuseExtEnabled = () => process.env.PNEXT_VENDOR_REUSE_EXT !== '0';
|
|
1027
|
+
|
|
1028
|
+
/** Alias targets that differ by layer (compat/index.ts); prefix-matches subpaths. */
|
|
1029
|
+
const VENDOR_LAYER_ALIASES = new Set([
|
|
1030
|
+
'react',
|
|
1031
|
+
'react-dom',
|
|
1032
|
+
'next/cache',
|
|
1033
|
+
'next/navigation',
|
|
1034
|
+
'next/script',
|
|
1035
|
+
'next/server',
|
|
1036
|
+
'server-only',
|
|
1037
|
+
'client-only',
|
|
1038
|
+
]);
|
|
1039
|
+
|
|
1040
|
+
function isVendorLayerAlias(external: string) {
|
|
1041
|
+
return (
|
|
1042
|
+
VENDOR_LAYER_ALIASES.has(external) ||
|
|
1043
|
+
VENDOR_LAYER_ALIASES.has(external.split('/').slice(0, 2).join('/'))
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
interface VendorReuseVerdict {
|
|
1048
|
+
stat: string;
|
|
1049
|
+
conds: string;
|
|
1050
|
+
reusable: boolean;
|
|
1051
|
+
reason?: string;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const vendorReuseVerdicts = new Map<string, Map<string, VendorReuseVerdict>>();
|
|
1055
|
+
const vendorReuseLoaded = new Set<string>();
|
|
1056
|
+
const vendorReuseProbes = new Map<string, Promise<boolean>>();
|
|
1057
|
+
|
|
1058
|
+
function vendorReuseFile(config: ResolvedConfig) {
|
|
1059
|
+
return path.join(config.outPath, 'cache', 'server', 'vendor', 'reuse-verdicts.json');
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function vendorReuseFor(config: ResolvedConfig) {
|
|
1063
|
+
let verdicts = vendorReuseVerdicts.get(config.outPath);
|
|
1064
|
+
if (!verdicts) {
|
|
1065
|
+
verdicts = new Map();
|
|
1066
|
+
vendorReuseVerdicts.set(config.outPath, verdicts);
|
|
1067
|
+
}
|
|
1068
|
+
return verdicts;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/** Verdicts persist per checkout: probes are paid once, restarts read the store. */
|
|
1072
|
+
function loadVendorReuseVerdicts(config: ResolvedConfig) {
|
|
1073
|
+
if (vendorReuseLoaded.has(config.outPath)) return;
|
|
1074
|
+
vendorReuseLoaded.add(config.outPath);
|
|
1075
|
+
try {
|
|
1076
|
+
const stored = JSON.parse(readFileSync(vendorReuseFile(config), 'utf8')) as Record<
|
|
1077
|
+
string,
|
|
1078
|
+
VendorReuseVerdict
|
|
1079
|
+
>;
|
|
1080
|
+
const verdicts = vendorReuseFor(config);
|
|
1081
|
+
for (const [key, verdict] of Object.entries(stored)) verdicts.set(key, verdict);
|
|
1082
|
+
} catch {
|
|
1083
|
+
// No store yet; verdicts are written as probes settle.
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function persistVendorReuseVerdicts(config: ResolvedConfig) {
|
|
1088
|
+
void writeFileAtomic(
|
|
1089
|
+
vendorReuseFile(config),
|
|
1090
|
+
JSON.stringify(Object.fromEntries(vendorReuseFor(config))),
|
|
1091
|
+
).catch(() => undefined);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/** Probe results shared across verdict pairs — node_modules bytes are immutable per process. */
|
|
1095
|
+
const vendorLayerProbeMemo = new Map<string, Promise<VendorLayerProbe>>();
|
|
1096
|
+
|
|
1097
|
+
type VendorLayerProbe = Awaited<ReturnType<typeof probeVendorLayer>>;
|
|
1098
|
+
|
|
1099
|
+
function probeVendorLayerMemo(config: ResolvedConfig, entry: string, conditions: string[]) {
|
|
1100
|
+
if (!vendorReuseExtEnabled()) return probeVendorLayer(config, entry, conditions);
|
|
1101
|
+
const key = `${config.outPath}\0${entry}\0${conditions.join(',')}`;
|
|
1102
|
+
let probe = vendorLayerProbeMemo.get(key);
|
|
1103
|
+
if (!probe) {
|
|
1104
|
+
probe = probeVendorLayer(config, entry, conditions);
|
|
1105
|
+
probe.catch(() => vendorLayerProbeMemo.delete(key));
|
|
1106
|
+
vendorLayerProbeMemo.set(key, probe);
|
|
1107
|
+
}
|
|
1108
|
+
return probe;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
/** Metafile-only probe of one layer's view of an entry (never writes, no slots). */
|
|
1112
|
+
async function probeVendorLayer(config: ResolvedConfig, entry: string, conditions: string[]) {
|
|
1113
|
+
const result = await build({
|
|
1114
|
+
entryPoints: [entry],
|
|
1115
|
+
absWorkingDir: config.root,
|
|
1116
|
+
bundle: true,
|
|
1117
|
+
write: false,
|
|
1118
|
+
metafile: true,
|
|
1119
|
+
format: 'esm',
|
|
1120
|
+
platform: 'neutral',
|
|
1121
|
+
target: 'es2022',
|
|
1122
|
+
conditions,
|
|
1123
|
+
mainFields: ['module', 'main'],
|
|
1124
|
+
loader: { '.js': 'jsx', '.mjs': 'jsx', '.cjs': 'jsx', '.css': 'empty' },
|
|
1125
|
+
jsx: 'automatic',
|
|
1126
|
+
jsxImportSource: 'preact',
|
|
1127
|
+
logLevel: 'silent',
|
|
1128
|
+
packages: 'external',
|
|
1129
|
+
});
|
|
1130
|
+
const externals = new Set<string>();
|
|
1131
|
+
for (const meta of Object.values(result.metafile.inputs)) {
|
|
1132
|
+
for (const reference of meta.imports) if (reference.external) externals.add(reference.path);
|
|
1133
|
+
}
|
|
1134
|
+
return {
|
|
1135
|
+
inputs: Object.keys(result.metafile.inputs).sort().join('\n'),
|
|
1136
|
+
externals: [...externals].sort(),
|
|
1137
|
+
directive: /['"]use (?:client|server|cache)['"]/.test(
|
|
1138
|
+
result.outputFiles.map(file => file.text).join(''),
|
|
1139
|
+
),
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
async function computeVendorReuseVerdict(
|
|
1144
|
+
config: ResolvedConfig,
|
|
1145
|
+
serverEntry: string,
|
|
1146
|
+
clientEntry: string,
|
|
1147
|
+
otherTarget: ServerBundleTarget = 'client',
|
|
1148
|
+
): Promise<{ reusable: boolean; reason?: string }> {
|
|
1149
|
+
const tp = performance.now();
|
|
1150
|
+
const done = (v: { reusable: boolean; reason?: string }) => {
|
|
1151
|
+
heavyProfRow({ k: 'reuse-probe', entry: serverEntry, other: otherTarget, ms: performance.now() - tp, ...v });
|
|
1152
|
+
return v;
|
|
1153
|
+
};
|
|
1154
|
+
// A finished server build already proved itself layer-specific: skip both probes.
|
|
1155
|
+
if (vendorReuseExtEnabled()) {
|
|
1156
|
+
const recorded = vendorLayerRecords.get(`${config.outPath}\0${serverEntry}`)?.get('server');
|
|
1157
|
+
if (recorded?.blocked) return done({ reusable: false, reason: 'recorded-blocked' });
|
|
1158
|
+
}
|
|
1159
|
+
const [server, client] = await Promise.all([
|
|
1160
|
+
probeVendorLayerMemo(config, serverEntry, serverBundleConditions('server')),
|
|
1161
|
+
probeVendorLayerMemo(config, clientEntry, serverBundleConditions(otherTarget)),
|
|
1162
|
+
]);
|
|
1163
|
+
if (server.inputs !== client.inputs) return done({ reusable: false, reason: 'inputs-differ' });
|
|
1164
|
+
if (server.externals.join('\n') !== client.externals.join('\n')) {
|
|
1165
|
+
return done({ reusable: false, reason: 'externals-differ' });
|
|
1166
|
+
}
|
|
1167
|
+
const aliasHit = server.externals.filter(isVendorLayerAlias);
|
|
1168
|
+
if (aliasHit.length > 0) return done({ reusable: false, reason: `layer-alias: ${aliasHit.join(',')}` });
|
|
1169
|
+
if (server.directive || client.directive) return done({ reusable: false, reason: 'directive' });
|
|
1170
|
+
return done({ reusable: true });
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/** Whether the client demand may resolve to the server-layer artifact. */
|
|
1174
|
+
function crossLayerReuseVerdict(
|
|
1175
|
+
config: ResolvedConfig,
|
|
1176
|
+
serverEntry: string,
|
|
1177
|
+
clientEntry: string,
|
|
1178
|
+
otherTarget: ServerBundleTarget = 'client',
|
|
1179
|
+
): Promise<boolean> {
|
|
1180
|
+
loadVendorReuseVerdicts(config);
|
|
1181
|
+
const key =
|
|
1182
|
+
otherTarget === 'client'
|
|
1183
|
+
? `${serverEntry}\0${clientEntry}`
|
|
1184
|
+
: `${otherTarget}:${serverEntry}\0${clientEntry}`;
|
|
1185
|
+
const stats = [statSync(serverEntry, { throwIfNoEntry: false }), statSync(clientEntry, { throwIfNoEntry: false })];
|
|
1186
|
+
if (stats.some(entryStat => !entryStat)) return Promise.resolve(false);
|
|
1187
|
+
const stat_ = stats.map(entryStat => `${entryStat!.mtimeMs}:${entryStat!.size}`).join('|');
|
|
1188
|
+
const conds = JSON.stringify([serverBundleConditions('server'), serverBundleConditions(otherTarget)]);
|
|
1189
|
+
const stored = vendorReuseFor(config).get(key);
|
|
1190
|
+
if (stored?.stat === stat_ && stored.conds === conds) {
|
|
1191
|
+
return Promise.resolve(stored.reusable);
|
|
1192
|
+
}
|
|
1193
|
+
const probeKey = `${config.outPath}\0${key}`;
|
|
1194
|
+
let probe = vendorReuseProbes.get(probeKey);
|
|
1195
|
+
if (!probe) {
|
|
1196
|
+
probe = computeVendorReuseVerdict(config, serverEntry, clientEntry, otherTarget)
|
|
1197
|
+
.catch(error => ({
|
|
1198
|
+
reusable: false,
|
|
1199
|
+
reason: `probe-failed: ${(error instanceof Error ? error.message : String(error)).slice(0, 160)}`,
|
|
1200
|
+
}))
|
|
1201
|
+
.then(verdict => {
|
|
1202
|
+
vendorReuseFor(config).set(key, { stat: stat_, conds, ...verdict });
|
|
1203
|
+
persistVendorReuseVerdicts(config);
|
|
1204
|
+
return verdict.reusable;
|
|
1205
|
+
})
|
|
1206
|
+
.finally(() => vendorReuseProbes.delete(probeKey));
|
|
1207
|
+
vendorReuseProbes.set(probeKey, probe);
|
|
1208
|
+
}
|
|
1209
|
+
return probe;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/**
|
|
1213
|
+
* Alias a client-layer vendor demand onto the server-layer plan when the two builds are proven
|
|
1214
|
+
* byte-equivalent. The swap happens BEFORE enqueue, so the demand lands on the server build's
|
|
1215
|
+
* `pendingBundles` slot through ordinary dedup - no new cross-layer wait edges enter the scheduler.
|
|
1216
|
+
* A server build that is neither on disk nor pending is never waited for.
|
|
1217
|
+
*/
|
|
1218
|
+
async function crossLayerReusePlan(
|
|
1219
|
+
config: ResolvedConfig,
|
|
1220
|
+
specifier: string,
|
|
1221
|
+
resolveDir: string,
|
|
1222
|
+
target: CompatAliasTarget,
|
|
1223
|
+
conditionTarget: ServerBundleTarget,
|
|
1224
|
+
clientPlan: VendorBuildPlan,
|
|
1225
|
+
): Promise<VendorBuildPlan | undefined> {
|
|
1226
|
+
if (!vendorReuseEnabled() || vendorContentDisabled()) return undefined;
|
|
1227
|
+
// An edge demand proven condition-independent lands on the server plan —
|
|
1228
|
+
// one build serves both, and the server artifact keeps client reuse alive.
|
|
1229
|
+
if (target === 'server' && conditionTarget === 'edge' && vendorReuseExtEnabled()) {
|
|
1230
|
+
if (vendorBundleMemHit(clientPlan.key) || existsSync(clientPlan.file)) return undefined;
|
|
1231
|
+
const edgeEntry = resolveVendorEntry(config, specifier, resolveDir, 'edge');
|
|
1232
|
+
const serverEntry = resolveVendorEntry(config, specifier, resolveDir, 'server');
|
|
1233
|
+
if (!edgeEntry || !serverEntry || edgeEntry !== serverEntry) return undefined;
|
|
1234
|
+
const reusable = await crossLayerReuseVerdict(config, serverEntry, edgeEntry, 'edge').catch(() => false);
|
|
1235
|
+
return reusable ? vendorBuildPlan(config, specifier, 'server', resolveDir, 'server') : undefined;
|
|
1236
|
+
}
|
|
1237
|
+
if (vendorLayerKey(target, conditionTarget) !== 'client') return undefined;
|
|
1238
|
+
// An in-flight or on-disk client artifact already settles cheaper than a probe.
|
|
1239
|
+
if (vendorBundleMemHit(clientPlan.key) || existsSync(clientPlan.file)) return undefined;
|
|
1240
|
+
const clientEntry = resolveVendorEntry(config, specifier, resolveDir, conditionTarget);
|
|
1241
|
+
const serverEntry = resolveVendorEntry(config, specifier, resolveDir, 'server');
|
|
1242
|
+
if (!clientEntry || !serverEntry) return undefined;
|
|
1243
|
+
const serverPlan = vendorBuildPlan(config, specifier, 'server', resolveDir, 'server');
|
|
1244
|
+
if (!vendorBundleMemHit(serverPlan.key) && !existsSync(serverPlan.file)) return undefined;
|
|
1245
|
+
const reusable = await crossLayerReuseVerdict(config, serverEntry, clientEntry).catch(() => false);
|
|
1246
|
+
return reusable ? serverPlan : undefined;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/** Cache hit and no-compile fast paths, in front of every build. */
|
|
1250
|
+
async function prepareVendorArtifact(
|
|
1251
|
+
config: ResolvedConfig,
|
|
1252
|
+
specifier: string,
|
|
1253
|
+
resolveDir: string,
|
|
1254
|
+
conditionTarget: ServerBundleTarget,
|
|
1255
|
+
file: string,
|
|
1256
|
+
target: CompatAliasTarget,
|
|
1257
|
+
entry: string | undefined,
|
|
1258
|
+
) {
|
|
1259
|
+
// Vendor bundles are keyed by resolved package path (version-specific under
|
|
1260
|
+
// pnpm), so an existing file is already current. Reuse it across dev restarts
|
|
1261
|
+
// instead of re-bundling the whole dependency graph.
|
|
1262
|
+
if (existsSync(file) && (await compiledModuleUsable(file))) return file;
|
|
1263
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
1264
|
+
// An earlier build proved this entry layer-independent: the other layer's
|
|
1265
|
+
// content IS this layer's artifact, so the demand settles without compiling.
|
|
1266
|
+
const shared = reusableVendorContent(config, entry, target, conditionTarget);
|
|
1267
|
+
if (shared && (await compiledModuleUsable(shared))) {
|
|
1268
|
+
await linkVendorArtifact(file, shared);
|
|
1269
|
+
noteDevArtifactWritten(file);
|
|
1270
|
+
return shared;
|
|
1271
|
+
}
|
|
1272
|
+
// A package that already ships a browser-ready ESM dist is copied, not compiled - the bundler would
|
|
1273
|
+
// only reproduce it. Sits in FRONT of the pass, never replaces it: anything the predicate rejects
|
|
1274
|
+
// falls through unchanged, and the copy is resolved exactly as the bundler would have. The copies
|
|
1275
|
+
// load natively now (rootFilter skips the vendor tree), so an app that configures `compiler.define`
|
|
1276
|
+
// must take the bundling path instead - that is where the defines are applied.
|
|
1277
|
+
if (vendorLoadsNatively() && serverDefineOptions().define) return undefined;
|
|
1278
|
+
const distDir = `${file.replace(/\.mjs$/, '')}.dist`;
|
|
1279
|
+
const distEntry = await copyBrowserReadyEsmDist(
|
|
1280
|
+
resolveVendorPackageSpecifier(
|
|
1281
|
+
config.root,
|
|
1282
|
+
path.join(resolveDir, 'pnext-resolve.ts'),
|
|
1283
|
+
specifier,
|
|
1284
|
+
serverBundleConditions(conditionTarget),
|
|
1285
|
+
) ?? '',
|
|
1286
|
+
distDir,
|
|
1287
|
+
);
|
|
1288
|
+
if (distEntry) {
|
|
1289
|
+
// Native loading means the `.js` copies need their own type marker: an app
|
|
1290
|
+
// without `"type": "module"` would otherwise have Bun read them as
|
|
1291
|
+
// CommonJS and choke on their `export` syntax.
|
|
1292
|
+
await markEsmDistPackage(distDir);
|
|
1293
|
+
noteDevArtifactWritten(distEntry);
|
|
1294
|
+
}
|
|
1295
|
+
return distEntry;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
/**
|
|
1299
|
+
* Bisect switch for the vendor-tree load-plugin exclusion. Deliberately NOT `PNEXT_VENDOR_NATIVE` -
|
|
1300
|
+
* that name already gates the plugin-free bundler, and sharing it would arm that opt-in path by
|
|
1301
|
+
* accident whenever this one was bisected.
|
|
1302
|
+
*/
|
|
1303
|
+
function vendorLoadsNatively() {
|
|
1304
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
1305
|
+
return process.env.PNEXT_VENDOR_DIST_NATIVE !== '0';
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// --------------------------------------------------------------------------
|
|
1309
|
+
// pre-planned vendor policy (PNEXT_VENDOR_PREPLAN=1 opt-in, server layer only)
|
|
1310
|
+
// --------------------------------------------------------------------------
|
|
1311
|
+
|
|
1312
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
1313
|
+
const vendorPreplanEnabled = () => process.env.PNEXT_VENDOR_PREPLAN === '1';
|
|
1314
|
+
|
|
1315
|
+
interface VendorPreplanVerdict {
|
|
1316
|
+
stat: string;
|
|
1317
|
+
conds: string;
|
|
1318
|
+
safe: boolean;
|
|
1319
|
+
reason?: string;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
const vendorPreplanVerdicts = new Map<string, Map<string, VendorPreplanVerdict>>();
|
|
1323
|
+
const vendorPreplanLoaded = new Set<string>();
|
|
1324
|
+
const vendorPreplanProbes = new Map<string, Promise<VendorPreplanVerdict>>();
|
|
1325
|
+
|
|
1326
|
+
const vendorPreplanStats = {
|
|
1327
|
+
preplanRefs: 0,
|
|
1328
|
+
fallbackRefs: 0,
|
|
1329
|
+
enqueued: 0,
|
|
1330
|
+
composedBuilds: 0,
|
|
1331
|
+
packagesRouted: 0,
|
|
1332
|
+
packagesRejected: 0,
|
|
1333
|
+
reasons: [] as string[],
|
|
1334
|
+
};
|
|
1335
|
+
|
|
1336
|
+
export function vendorPreplanReport() {
|
|
1337
|
+
return { ...vendorPreplanStats, reasons: vendorPreplanStats.reasons.slice(0, 60) };
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
1341
|
+
const vendorPreplanDebug = () => process.env.PNEXT_VENDOR_PREPLAN_DEBUG === '1';
|
|
1342
|
+
let preplanReportFlush: ReturnType<typeof setTimeout> | undefined;
|
|
1343
|
+
function schedulePreplanReport() {
|
|
1344
|
+
if (!vendorPreplanDebug()) return;
|
|
1345
|
+
if (preplanReportFlush) clearTimeout(preplanReportFlush);
|
|
1346
|
+
preplanReportFlush = setTimeout(() => {
|
|
1347
|
+
console.error(`pnext vendor preplan ${JSON.stringify(vendorPreplanReport())}`);
|
|
1348
|
+
console.error(`pnext vendor native ${JSON.stringify(vendorNativeReport())}`);
|
|
1349
|
+
}, 3000);
|
|
1350
|
+
preplanReportFlush.unref?.();
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
function vendorPreplanFile(config: ResolvedConfig) {
|
|
1354
|
+
return path.join(config.outPath, 'cache', 'server', 'vendor', 'preplan-verdicts.json');
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function vendorPreplanFor(config: ResolvedConfig) {
|
|
1358
|
+
let verdicts = vendorPreplanVerdicts.get(config.outPath);
|
|
1359
|
+
if (!verdicts) {
|
|
1360
|
+
verdicts = new Map();
|
|
1361
|
+
vendorPreplanVerdicts.set(config.outPath, verdicts);
|
|
1362
|
+
}
|
|
1363
|
+
return verdicts;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
/** Probe verdicts persist per checkout, reuse-verdict style. */
|
|
1367
|
+
function loadVendorPreplanVerdicts(config: ResolvedConfig) {
|
|
1368
|
+
if (vendorPreplanLoaded.has(config.outPath)) return;
|
|
1369
|
+
vendorPreplanLoaded.add(config.outPath);
|
|
1370
|
+
try {
|
|
1371
|
+
const stored = JSON.parse(readFileSync(vendorPreplanFile(config), 'utf8')) as Record<
|
|
1372
|
+
string,
|
|
1373
|
+
VendorPreplanVerdict
|
|
1374
|
+
>;
|
|
1375
|
+
const verdicts = vendorPreplanFor(config);
|
|
1376
|
+
for (const [key, verdict] of Object.entries(stored)) verdicts.set(key, verdict);
|
|
1377
|
+
} catch {
|
|
1378
|
+
// No store yet; verdicts are written as probes settle.
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
function persistVendorPreplanVerdicts(config: ResolvedConfig) {
|
|
1383
|
+
void writeFileAtomic(
|
|
1384
|
+
vendorPreplanFile(config),
|
|
1385
|
+
JSON.stringify(Object.fromEntries(vendorPreplanFor(config))),
|
|
1386
|
+
).catch(() => undefined);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/** Does this entry's graph change shape under `react-server` (or carry directives)? */
|
|
1390
|
+
function vendorPreplanEntryVerdict(
|
|
1391
|
+
config: ResolvedConfig,
|
|
1392
|
+
entry: string,
|
|
1393
|
+
conditions: string[],
|
|
1394
|
+
): Promise<VendorPreplanVerdict> | VendorPreplanVerdict {
|
|
1395
|
+
loadVendorPreplanVerdicts(config);
|
|
1396
|
+
const entryStat = statSync(entry, { throwIfNoEntry: false });
|
|
1397
|
+
if (!entryStat) return { stat: '', conds: '', safe: false, reason: 'entry missing' };
|
|
1398
|
+
const stat_ = `${entryStat.mtimeMs}:${entryStat.size}`;
|
|
1399
|
+
const conds = JSON.stringify(conditions);
|
|
1400
|
+
const stored = vendorPreplanFor(config).get(entry);
|
|
1401
|
+
if (stored?.stat === stat_ && stored.conds === conds) return stored;
|
|
1402
|
+
const probeKey = `${config.outPath}\0${entry}\0${conds}`;
|
|
1403
|
+
let probe = vendorPreplanProbes.get(probeKey);
|
|
1404
|
+
if (!probe) {
|
|
1405
|
+
probe = computeVendorPreplanVerdict(config, entry, conditions, stat_, conds)
|
|
1406
|
+
.catch(error => ({
|
|
1407
|
+
stat: stat_,
|
|
1408
|
+
conds,
|
|
1409
|
+
safe: false,
|
|
1410
|
+
reason: `probe-failed: ${(error instanceof Error ? error.message : String(error)).slice(0, 160)}`,
|
|
1411
|
+
}))
|
|
1412
|
+
.then(verdict => {
|
|
1413
|
+
vendorPreplanFor(config).set(entry, verdict);
|
|
1414
|
+
persistVendorPreplanVerdicts(config);
|
|
1415
|
+
return verdict;
|
|
1416
|
+
})
|
|
1417
|
+
.finally(() => vendorPreplanProbes.delete(probeKey));
|
|
1418
|
+
vendorPreplanProbes.set(probeKey, probe);
|
|
1419
|
+
}
|
|
1420
|
+
return probe;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
async function computeVendorPreplanVerdict(
|
|
1424
|
+
config: ResolvedConfig,
|
|
1425
|
+
entry: string,
|
|
1426
|
+
conditions: string[],
|
|
1427
|
+
stat_: string,
|
|
1428
|
+
conds: string,
|
|
1429
|
+
): Promise<VendorPreplanVerdict> {
|
|
1430
|
+
const [rsc, plain] = await Promise.all([
|
|
1431
|
+
probeVendorLayer(config, entry, conditions),
|
|
1432
|
+
probeVendorLayer(config, entry, conditions.filter(condition => condition !== 'react-server')),
|
|
1433
|
+
]);
|
|
1434
|
+
if (rsc.inputs !== plain.inputs) {
|
|
1435
|
+
return { stat: stat_, conds, safe: false, reason: 'react-server inputs differ' };
|
|
1436
|
+
}
|
|
1437
|
+
if (rsc.externals.join('\n') !== plain.externals.join('\n')) {
|
|
1438
|
+
return { stat: stat_, conds, safe: false, reason: 'react-server externals differ' };
|
|
1439
|
+
}
|
|
1440
|
+
if (rsc.directive || plain.directive) {
|
|
1441
|
+
return { stat: stat_, conds, safe: false, reason: 'directive' };
|
|
1442
|
+
}
|
|
1443
|
+
return { stat: stat_, conds, safe: true };
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
/**
|
|
1447
|
+
* May THIS package's build emit pre-planned refs? Any rejection routes the whole build to the awaited
|
|
1448
|
+
* plugin decisions - always available, always correct. ESM-only keeps the exec surface off the
|
|
1449
|
+
* fire-and-forget path: a CJS parent's artifact is imported (facade exec) the moment its build
|
|
1450
|
+
* returns, before any drain could cover its children.
|
|
1451
|
+
*/
|
|
1452
|
+
async function vendorPreplanPackageSafe(
|
|
1453
|
+
config: ResolvedConfig,
|
|
1454
|
+
packageName: string | undefined,
|
|
1455
|
+
entry: string | undefined,
|
|
1456
|
+
target: CompatAliasTarget,
|
|
1457
|
+
conditionTarget: ServerBundleTarget,
|
|
1458
|
+
): Promise<boolean> {
|
|
1459
|
+
if (!vendorPreplanEnabled() || target !== 'server' || conditionTarget !== 'server') return false;
|
|
1460
|
+
const reject = (reason: string) => {
|
|
1461
|
+
vendorPreplanStats.packagesRejected += 1;
|
|
1462
|
+
vendorPreplanStats.reasons.push(`${packageName ?? '?'}: ${reason}`);
|
|
1463
|
+
schedulePreplanReport();
|
|
1464
|
+
return false;
|
|
1465
|
+
};
|
|
1466
|
+
if (!packageName || !entry || !path.isAbsolute(entry)) return reject('unresolved entry');
|
|
1467
|
+
if (/\.[cm]?tsx?$/.test(entry)) return reject('ts entry');
|
|
1468
|
+
if (!isEsmModuleEntry(entry)) return reject('cjs entry');
|
|
1469
|
+
if (getExternalPackagePolicy().transpile(packageName)) return reject('transpilePackages');
|
|
1470
|
+
const verdict = await vendorPreplanEntryVerdict(config, entry, serverBundleConditions('server'));
|
|
1471
|
+
if (!verdict.safe) return reject(verdict.reason ?? 'react-server divergence');
|
|
1472
|
+
vendorPreplanStats.packagesRouted += 1;
|
|
1473
|
+
schedulePreplanReport();
|
|
1474
|
+
return true;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
/** Mirror of `externalServerPackageHref`'s liveness recheck, minus the await(er). */
|
|
1478
|
+
function enqueuePreplanBuild(plan: VendorBuildPlan) {
|
|
1479
|
+
vendorPreplanStats.enqueued += 1;
|
|
1480
|
+
trackPreplanBuild(
|
|
1481
|
+
(async () => {
|
|
1482
|
+
let file = await vendorBundle(plan, false);
|
|
1483
|
+
if (!cachedExistsSync(file) || !(await compiledModuleUsable(file))) {
|
|
1484
|
+
dropVendorBundle(plan.key, file);
|
|
1485
|
+
if (plan.group) dropVendorGroup(plan.group.key);
|
|
1486
|
+
file = await vendorBundle(plan, false);
|
|
1487
|
+
}
|
|
1488
|
+
})(),
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/**
|
|
1493
|
+
* The deterministic-href emit: classify the dep once from pure inputs, hand
|
|
1494
|
+
* back its artifact path pre-build, and enqueue the build without awaiting it.
|
|
1495
|
+
* `undefined` routes the reference to the awaited plugin decision unchanged.
|
|
1496
|
+
* Every emitted href is exactly what `externalServerPackageHref` would have
|
|
1497
|
+
* returned for this demand, so flag-on artifacts stay byte-identical.
|
|
1498
|
+
*/
|
|
1499
|
+
async function preplanDependencyDecision(
|
|
1500
|
+
config: ResolvedConfig,
|
|
1501
|
+
target: CompatAliasTarget,
|
|
1502
|
+
conditionTarget: ServerBundleTarget,
|
|
1503
|
+
specifier: string,
|
|
1504
|
+
resolveDir: string,
|
|
1505
|
+
): Promise<OnResolveResult | undefined> {
|
|
1506
|
+
const depName = packageNameFromSpecifier(specifier);
|
|
1507
|
+
if (!depName || !isPackageSpecifier(specifier)) return undefined;
|
|
1508
|
+
if (builtinModules.includes(depName) || isBunBuiltin(depName)) return undefined;
|
|
1509
|
+
if (getExternalPackagePolicy().external(depName)) return undefined;
|
|
1510
|
+
if (getExternalPackagePolicy().transpile(depName)) return undefined;
|
|
1511
|
+
const entry = resolveVendorEntry(config, specifier, resolveDir, conditionTarget);
|
|
1512
|
+
if (!entry || /\.[cm]?tsx?$/.test(entry) || !isEsmModuleEntry(entry)) return undefined;
|
|
1513
|
+
const plan = vendorBuildPlan(config, specifier, target, resolveDir, conditionTarget);
|
|
1514
|
+
if (cachedExistsSync(plan.file)) {
|
|
1515
|
+
if (!(await compiledModuleUsable(plan.file))) return undefined;
|
|
1516
|
+
} else {
|
|
1517
|
+
// Both prepare fast paths settle on a DIFFERENT file than `plan.file`, so
|
|
1518
|
+
// their demands keep the awaited path (which is nearly free for them).
|
|
1519
|
+
if (reusableVendorContent(config, entry, target, conditionTarget)) return undefined;
|
|
1520
|
+
if (!(vendorLoadsNatively() && serverDefineOptions().define)) {
|
|
1521
|
+
const distEntry = await copyBrowserReadyEsmDist(
|
|
1522
|
+
resolveVendorPackageSpecifier(
|
|
1523
|
+
config.root,
|
|
1524
|
+
path.join(resolveDir, 'pnext-resolve.ts'),
|
|
1525
|
+
specifier,
|
|
1526
|
+
serverBundleConditions(conditionTarget),
|
|
1527
|
+
) ?? '',
|
|
1528
|
+
`${plan.file.replace(/\.mjs$/, '')}.dist`,
|
|
1529
|
+
);
|
|
1530
|
+
if (distEntry) return undefined;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
enqueuePreplanBuild(plan);
|
|
1534
|
+
return { path: pathToFileHref(plan.file), external: true };
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
const esmDistPackages = new Set<string>();
|
|
1538
|
+
|
|
1539
|
+
async function markEsmDistPackage(distDir: string) {
|
|
1540
|
+
if (esmDistPackages.has(distDir)) return;
|
|
1541
|
+
esmDistPackages.add(distDir);
|
|
1542
|
+
const marker = path.join(distDir, 'package.json');
|
|
1543
|
+
if (existsSync(marker)) return;
|
|
1544
|
+
await writeFile(marker, '{"type":"module"}').catch(() => undefined);
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
/**
|
|
1548
|
+
* Can this compiled artifact be imported as-is? Two O(1) checks - the file itself, and the cache
|
|
1549
|
+
* folder's marker - replace the whole-graph walk that read back every compiled module and checked
|
|
1550
|
+
* each of its `cache/server/` edges. Both guard the same hazard: a `pnext build` wiping `.pnext`
|
|
1551
|
+
* under a running server can strand a dangling import, and Bun caches a failed load for the life of
|
|
1552
|
+
* the process.
|
|
1553
|
+
*/
|
|
1554
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
1555
|
+
export async function compiledModuleUsable(file: string): Promise<boolean> {
|
|
1556
|
+
return devArtifactUsable(file);
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
/**
|
|
1560
|
+
* The group this subpath belongs to. Same-package imports are inlined (externalizing would lose a
|
|
1561
|
+
* CommonJS entry's named exports), so a build per subpath re-parses the package's whole graph per
|
|
1562
|
+
* subpath; the subpaths demanded together become entries of one `splitting: true` build instead.
|
|
1563
|
+
*
|
|
1564
|
+
* Restricted to ESM entries: an entry esbuild has to convert from CommonJS goes through
|
|
1565
|
+
* `addCommonJsNamedExports`, whose export recovery reads the shape of a single-file bundle and does
|
|
1566
|
+
* not survive being split into chunks.
|
|
1567
|
+
*/
|
|
1568
|
+
function vendorGroupPlan(
|
|
1569
|
+
config: ResolvedConfig,
|
|
1570
|
+
member: VendorGroupMember,
|
|
1571
|
+
target: CompatAliasTarget,
|
|
1572
|
+
resolveDir: string,
|
|
1573
|
+
conditionTarget: ServerBundleTarget,
|
|
1574
|
+
): VendorGroupPlan | undefined {
|
|
1575
|
+
const packageName = packageNameFromSpecifier(member.specifier);
|
|
1576
|
+
const packageRoot = packageName && vendorPackageRoot(member.entry, packageName);
|
|
1577
|
+
if (!packageName || !packageRoot || !isEsmModuleEntry(member.entry)) return undefined;
|
|
1578
|
+
return {
|
|
1579
|
+
key: `${config.outPath}\0${target}\0${conditionTarget}\0${packageRoot}`,
|
|
1580
|
+
member,
|
|
1581
|
+
build: (members, external, nested) =>
|
|
1582
|
+
buildVendorGroup(
|
|
1583
|
+
config,
|
|
1584
|
+
packageName,
|
|
1585
|
+
members,
|
|
1586
|
+
external,
|
|
1587
|
+
target,
|
|
1588
|
+
resolveDir,
|
|
1589
|
+
conditionTarget,
|
|
1590
|
+
nested,
|
|
1591
|
+
),
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
/**
|
|
1596
|
+
* The installed package directory an entry belongs to — the grouping identity,
|
|
1597
|
+
* so a nested (non-hoisted) copy never shares a group with the hoisted one.
|
|
1598
|
+
*/
|
|
1599
|
+
function vendorPackageRoot(entry: string, packageName: string) {
|
|
1600
|
+
const marker = `${path.sep}node_modules${path.sep}`;
|
|
1601
|
+
const index = entry.lastIndexOf(marker);
|
|
1602
|
+
if (index < 0) return undefined;
|
|
1603
|
+
return path.join(entry.slice(0, index + marker.length), packageName);
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
/**
|
|
1607
|
+
* One esbuild pass for the subpaths demanded together, with the shared graph split into chunks so it
|
|
1608
|
+
* is parsed once instead of once per subpath. Siblings already on disk are externalized rather than
|
|
1609
|
+
* re-parsed, which keeps artifacts handed out earlier valid across a growth round.
|
|
1610
|
+
*/
|
|
1611
|
+
async function buildVendorGroup(
|
|
1612
|
+
config: ResolvedConfig,
|
|
1613
|
+
packageName: string,
|
|
1614
|
+
members: readonly VendorGroupMember[],
|
|
1615
|
+
external: readonly VendorGroupMember[],
|
|
1616
|
+
target: CompatAliasTarget,
|
|
1617
|
+
resolveDir: string,
|
|
1618
|
+
conditionTarget: ServerBundleTarget,
|
|
1619
|
+
nested: boolean,
|
|
1620
|
+
) {
|
|
1621
|
+
const built = members.filter(member => !existsSync(member.file));
|
|
1622
|
+
if (built.length === 0) return;
|
|
1623
|
+
// Only a sibling whose artifact is ON DISK can be externalized — one still
|
|
1624
|
+
// compiling (a solo build this round did not wait for) must be inlined, or
|
|
1625
|
+
// this bundle would import a file that is not there yet.
|
|
1626
|
+
const siblings = new Map(
|
|
1627
|
+
[...external, ...members]
|
|
1628
|
+
.filter(member => !built.includes(member) && existsSync(member.file))
|
|
1629
|
+
.map(member => [member.specifier, pathToFileHref(member.file)] as const),
|
|
1630
|
+
);
|
|
1631
|
+
const outdir = path.dirname(built[0]!.file);
|
|
1632
|
+
const context = { nestedExternalFiles: [] as string[] };
|
|
1633
|
+
const entryFiles = new Set(built.map(member => member.file));
|
|
1634
|
+
// The plugin-chain fallback builds from bare specifiers; hand the interop
|
|
1635
|
+
// plugin each member's already-resolved entry.
|
|
1636
|
+
for (const member of built) provideEntryResolution(member.specifier, resolveDir, member.entry);
|
|
1637
|
+
const entryPoints = built.map(member => ({
|
|
1638
|
+
in:
|
|
1639
|
+
getBundlerExtensions().serverBundleEntry(member.specifier, resolveDir, member.entry) ??
|
|
1640
|
+
member.specifier,
|
|
1641
|
+
out: path.basename(member.file, '.mjs'),
|
|
1642
|
+
}));
|
|
1643
|
+
const trace: VendorBuildTrace | undefined = vendorTraceEnabled()
|
|
1644
|
+
? { plugins: [], entry: built[0]!.entry }
|
|
1645
|
+
: undefined;
|
|
1646
|
+
const traceSeq = trace ? nextVendorTraceSeq() : 0;
|
|
1647
|
+
const traceStart = performance.now();
|
|
1648
|
+
const traceRow = (outputs: { text: string }[] | undefined, error?: unknown) => {
|
|
1649
|
+
if (!trace) return;
|
|
1650
|
+
traceVendorBuild({
|
|
1651
|
+
seq: traceSeq,
|
|
1652
|
+
specifier: packageName,
|
|
1653
|
+
resolveDir,
|
|
1654
|
+
target,
|
|
1655
|
+
conditionTarget,
|
|
1656
|
+
conditions: serverBundleConditions(conditionTarget),
|
|
1657
|
+
startMs: traceStart,
|
|
1658
|
+
outBytes: (outputs ?? []).reduce((sum, output) => sum + output.text.length, 0),
|
|
1659
|
+
trace,
|
|
1660
|
+
ok: !error,
|
|
1661
|
+
...(error
|
|
1662
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
1663
|
+
? { error: (error instanceof Error ? error.message : String(error)).slice(0, 300) }
|
|
1664
|
+
: {}),
|
|
1665
|
+
});
|
|
1666
|
+
};
|
|
1667
|
+
const preplan =
|
|
1668
|
+
vendorPreplanEnabled() &&
|
|
1669
|
+
(
|
|
1670
|
+
await Promise.all(
|
|
1671
|
+
built.map(member =>
|
|
1672
|
+
vendorPreplanPackageSafe(config, packageName, member.entry, target, conditionTarget),
|
|
1673
|
+
),
|
|
1674
|
+
)
|
|
1675
|
+
).every(Boolean);
|
|
1676
|
+
let outputFiles;
|
|
1677
|
+
try {
|
|
1678
|
+
outputFiles = await profileRuntimeStep(
|
|
1679
|
+
`vendor bundle ${packageName} [${target}/${conditionTarget}] (${built.length} entries)`,
|
|
1680
|
+
async () =>
|
|
1681
|
+
(await buildVendorNative({
|
|
1682
|
+
preplan,
|
|
1683
|
+
config,
|
|
1684
|
+
target,
|
|
1685
|
+
conditionTarget,
|
|
1686
|
+
resolveDir,
|
|
1687
|
+
bailKey: `${packageName}\0${target}\0${conditionTarget}\0${outdir}`,
|
|
1688
|
+
packageName,
|
|
1689
|
+
// Resolved entries, not the bare specifiers the plugin build uses:
|
|
1690
|
+
// `packages: 'external'` refuses to bundle a bare entry point.
|
|
1691
|
+
entryPoints: built.map(member => ({
|
|
1692
|
+
in: member.entry,
|
|
1693
|
+
out: path.basename(member.file, '.mjs'),
|
|
1694
|
+
})),
|
|
1695
|
+
outdir,
|
|
1696
|
+
splitting: true,
|
|
1697
|
+
siblings,
|
|
1698
|
+
ownEntries: new Map(built.map(member => [member.entry, member.file])),
|
|
1699
|
+
isEntry: output => entryFiles.has(output),
|
|
1700
|
+
nested,
|
|
1701
|
+
context,
|
|
1702
|
+
trace,
|
|
1703
|
+
})) ??
|
|
1704
|
+
(await buildVendorGroupWithPlugins(
|
|
1705
|
+
config,
|
|
1706
|
+
packageName,
|
|
1707
|
+
siblings,
|
|
1708
|
+
target,
|
|
1709
|
+
resolveDir,
|
|
1710
|
+
conditionTarget,
|
|
1711
|
+
outdir,
|
|
1712
|
+
entryPoints,
|
|
1713
|
+
context,
|
|
1714
|
+
trace,
|
|
1715
|
+
preplan,
|
|
1716
|
+
)),
|
|
1717
|
+
);
|
|
1718
|
+
} catch (error) {
|
|
1719
|
+
traceRow(undefined, error);
|
|
1720
|
+
throw error;
|
|
1721
|
+
}
|
|
1722
|
+
traceRow(outputFiles);
|
|
1723
|
+
await mkdir(path.join(outdir, 'chunks'), { recursive: true });
|
|
1724
|
+
// Chunks first: an entry must never be readable before what it imports is.
|
|
1725
|
+
for (const output of [...outputFiles].sort(
|
|
1726
|
+
(a, b) => Number(entryFiles.has(a.path)) - Number(entryFiles.has(b.path)),
|
|
1727
|
+
)) {
|
|
1728
|
+
const bundled = entryFiles.has(output.path)
|
|
1729
|
+
? addCommonJsNamedExports(output.text)
|
|
1730
|
+
: output.text;
|
|
1731
|
+
// Packages can ship 'use cache' functions too (Next allows it); client
|
|
1732
|
+
// bundles keep the source untouched.
|
|
1733
|
+
const code =
|
|
1734
|
+
target === 'server' ? applyBundledSourceTransforms(bundled, output.path) : bundled;
|
|
1735
|
+
await writeVendorArtifact(output.path, code);
|
|
1736
|
+
}
|
|
1737
|
+
await emitNestedExternalTraceCopies(built[0]!.file, context.nestedExternalFiles);
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// vendor plugin-callback attribution (PNEXT_VENDOR_PROFILE=1, dormant otherwise). Every
|
|
1741
|
+
// onResolve/onLoad is an IPC round trip esbuild's bundler blocks on, against the one JS thread that
|
|
1742
|
+
// answers them; counts separate "our callbacks are slow" from "we answer many questions".
|
|
1743
|
+
|
|
1744
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
1745
|
+
const vendorProfileEnabled = process.env.PNEXT_VENDOR_PROFILE === '1';
|
|
1746
|
+
const vendorProfileTotals = new Map<string, number>();
|
|
1747
|
+
|
|
1748
|
+
function vendorProfileCount(key: string, by = 1) {
|
|
1749
|
+
vendorProfileTotals.set(key, (vendorProfileTotals.get(key) ?? 0) + by);
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
export function vendorProfileReport() {
|
|
1753
|
+
return Object.fromEntries(
|
|
1754
|
+
[...vendorProfileTotals].sort((a, b) => b[1] - a[1]).map(([k, v]) => [k, Math.round(v)]),
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
// Printed once the callbacks go quiet, so one page's whole plugin chain lands
|
|
1759
|
+
// in a single line instead of interleaving with the build it is measuring.
|
|
1760
|
+
let vendorProfileFlush: ReturnType<typeof setTimeout> | undefined;
|
|
1761
|
+
function scheduleVendorProfileReport() {
|
|
1762
|
+
if (vendorProfileFlush) clearTimeout(vendorProfileFlush);
|
|
1763
|
+
vendorProfileFlush = setTimeout(() => {
|
|
1764
|
+
console.log(`pnext vendor profile ${JSON.stringify(vendorProfileReport())}`);
|
|
1765
|
+
}, 3000);
|
|
1766
|
+
vendorProfileFlush.unref?.();
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
function profiledVendorPlugins(plugins: Plugin[]): Plugin[] {
|
|
1770
|
+
if (!vendorProfileEnabled) return plugins;
|
|
1771
|
+
return plugins.map(plugin => ({
|
|
1772
|
+
name: plugin.name,
|
|
1773
|
+
setup(build) {
|
|
1774
|
+
const wrap =
|
|
1775
|
+
(kind: 'resolve' | 'load', register: (options: any, cb: any) => void) =>
|
|
1776
|
+
(options: any, callback: any) =>
|
|
1777
|
+
register(options, async (args: any) => {
|
|
1778
|
+
vendorProfileCount(`${plugin.name}:${kind}#`);
|
|
1779
|
+
const start = performance.now();
|
|
1780
|
+
try {
|
|
1781
|
+
return await (callback as (a: unknown) => unknown)(args);
|
|
1782
|
+
} finally {
|
|
1783
|
+
vendorProfileCount(`${plugin.name}:${kind}ms`, performance.now() - start);
|
|
1784
|
+
scheduleVendorProfileReport();
|
|
1785
|
+
}
|
|
1786
|
+
});
|
|
1787
|
+
return plugin.setup({
|
|
1788
|
+
...build,
|
|
1789
|
+
onResolve: wrap('resolve', build.onResolve.bind(build)),
|
|
1790
|
+
onLoad: wrap('load', build.onLoad.bind(build)),
|
|
1791
|
+
});
|
|
1792
|
+
},
|
|
1793
|
+
}));
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
/** The plugin chain, for whatever the plugin-free build could not express. */
|
|
1797
|
+
async function buildVendorGroupWithPlugins(
|
|
1798
|
+
config: ResolvedConfig,
|
|
1799
|
+
packageName: string,
|
|
1800
|
+
siblings: ReadonlyMap<string, string>,
|
|
1801
|
+
target: CompatAliasTarget,
|
|
1802
|
+
resolveDir: string,
|
|
1803
|
+
conditionTarget: ServerBundleTarget,
|
|
1804
|
+
outdir: string,
|
|
1805
|
+
entryPoints: { in: string; out: string }[],
|
|
1806
|
+
context: { nestedExternalFiles: string[] },
|
|
1807
|
+
trace?: VendorBuildTrace,
|
|
1808
|
+
preplan = false,
|
|
1809
|
+
) {
|
|
1810
|
+
const pluginStart = performance.now();
|
|
1811
|
+
const groupPlugins = profiledVendorPlugins([
|
|
1812
|
+
serverAssetPlugin(config),
|
|
1813
|
+
...getBundlerExtensions().serverEsbuildPlugins(
|
|
1814
|
+
config,
|
|
1815
|
+
vendorPluginOptions(config, target, conditionTarget),
|
|
1816
|
+
),
|
|
1817
|
+
runtimeAliasBuildPlugin(config, target, {
|
|
1818
|
+
bundleRequireAliases: true,
|
|
1819
|
+
reactServerLayer: target === 'server',
|
|
1820
|
+
}),
|
|
1821
|
+
vendorGroupSiblingPlugin(siblings),
|
|
1822
|
+
externalPackageDependencyPlugin(config, packageName, target, conditionTarget, context, preplan),
|
|
1823
|
+
externalBuiltinBuildPlugin(),
|
|
1824
|
+
]);
|
|
1825
|
+
if (trace) trace.plugins = groupPlugins.map(plugin => plugin.name);
|
|
1826
|
+
const result = await build({
|
|
1827
|
+
entryPoints,
|
|
1828
|
+
absWorkingDir: resolveDir,
|
|
1829
|
+
bundle: true,
|
|
1830
|
+
splitting: true,
|
|
1831
|
+
write: false,
|
|
1832
|
+
outdir,
|
|
1833
|
+
outExtension: { '.js': '.mjs' },
|
|
1834
|
+
chunkNames: 'chunks/[name]-[hash]',
|
|
1835
|
+
format: 'esm',
|
|
1836
|
+
platform: 'neutral',
|
|
1837
|
+
target: 'es2022',
|
|
1838
|
+
conditions: serverBundleConditions(conditionTarget),
|
|
1839
|
+
mainFields: ['module', 'main'],
|
|
1840
|
+
loader: packageJsxLoaders,
|
|
1841
|
+
jsx: 'automatic',
|
|
1842
|
+
jsxImportSource: 'preact',
|
|
1843
|
+
logLevel: 'silent',
|
|
1844
|
+
// Trace-only: the analyzer's per-build input set comes from the metafile.
|
|
1845
|
+
...(trace ? { metafile: true as const } : {}),
|
|
1846
|
+
...serverDefineOptions(),
|
|
1847
|
+
plugins: groupPlugins,
|
|
1848
|
+
});
|
|
1849
|
+
vendorNativeStats.pluginBuilds += 1;
|
|
1850
|
+
vendorNativeStats.pluginBuildMs += performance.now() - pluginStart;
|
|
1851
|
+
if (trace) trace.metafile = result.metafile;
|
|
1852
|
+
return result.outputFiles.map(output => ({ path: output.path, text: output.text }));
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
/** Siblings already compiled stay out of the graph, as plain externals. */
|
|
1856
|
+
function vendorGroupSiblingPlugin(siblings: ReadonlyMap<string, string>): Plugin {
|
|
1857
|
+
return {
|
|
1858
|
+
name: 'pnext-vendor-group-siblings',
|
|
1859
|
+
setup(build) {
|
|
1860
|
+
if (siblings.size === 0) return;
|
|
1861
|
+
build.onResolve({ filter: /^[^./].*/ }, args => {
|
|
1862
|
+
if (args.kind === 'entry-point') return undefined;
|
|
1863
|
+
const sibling = siblings.get(args.path);
|
|
1864
|
+
return sibling ? { path: sibling, external: true } : undefined;
|
|
1865
|
+
});
|
|
1866
|
+
},
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
// per-file client boundaries inside a package.
|
|
1871
|
+
//
|
|
1872
|
+
// A package ships its client half as ordinary files carrying a `'use client'` directive next to a
|
|
1873
|
+
// directive-less index that re-exports them. From the react-server graph such a file is a client
|
|
1874
|
+
// REFERENCE - never code the RSC pass runs - and the module pass already stubs the ones it resolves
|
|
1875
|
+
// itself. A vendor bundle reaches them mid-graph instead, where no resolve of ours ever sees the
|
|
1876
|
+
// specifier, so the directive was lost inside the bundle: the RSC pass then executed a client
|
|
1877
|
+
// component and the app got a SECOND copy of every module the client profile also bundles - two
|
|
1878
|
+
// `createContext` calls, so a provider rendered from the client graph is invisible to a consumer
|
|
1879
|
+
// rendered from this one.
|
|
1880
|
+
//
|
|
1881
|
+
// The scan is off wherever it cannot matter: client-target bundles (the SSR pass must run the real
|
|
1882
|
+
// component) and non-react apps. On the ones that remain it costs a read per input file, which
|
|
1883
|
+
// esbuild was going to make anyway. `PNEXT_VENDOR_CLIENT_BOUNDARY=0` removes the plugin, to bisect.
|
|
1884
|
+
|
|
1885
|
+
// No longer a plugin of its own: the scan is composed into the server chain's
|
|
1886
|
+
// single claiming onLoad (ServerEsbuildPluginOptions.vendorClientBoundary), so
|
|
1887
|
+
// it adds zero plugin-callback round trips. Reads are memoized per config —
|
|
1888
|
+
// the same node_modules bytes used to be re-read and re-decoded per build.
|
|
1889
|
+
const vendorClientBoundaryLoads = new WeakMap<
|
|
1890
|
+
ResolvedConfig,
|
|
1891
|
+
(file: string) => Promise<OnLoadResult | undefined>
|
|
1892
|
+
>();
|
|
1893
|
+
|
|
1894
|
+
function vendorClientBoundaryLoad(config: ResolvedConfig) {
|
|
1895
|
+
let load = vendorClientBoundaryLoads.get(config);
|
|
1896
|
+
if (load) return load;
|
|
1897
|
+
const memo = new Map<string, Promise<OnLoadResult | undefined>>();
|
|
1898
|
+
const scan = async (file: string): Promise<OnLoadResult | undefined> => {
|
|
1899
|
+
const source = await readFile(file, 'utf8').catch(() => undefined);
|
|
1900
|
+
if (source === undefined || !hasUseClientDirective(source)) return undefined;
|
|
1901
|
+
return {
|
|
1902
|
+
contents: clientReferenceModuleSource(
|
|
1903
|
+
file,
|
|
1904
|
+
await clientReferenceExportNames(config, file, source),
|
|
1905
|
+
{ inlineSymbol: true },
|
|
1906
|
+
),
|
|
1907
|
+
loader: 'js',
|
|
1908
|
+
resolveDir: path.dirname(file),
|
|
1909
|
+
};
|
|
1910
|
+
};
|
|
1911
|
+
// Only node_modules answers are cached: immutable for the process life, the
|
|
1912
|
+
// same assumption the vendor artifact cache makes. First-party files re-read.
|
|
1913
|
+
load = file => {
|
|
1914
|
+
if (!file.includes(`${path.sep}node_modules${path.sep}`)) return scan(file);
|
|
1915
|
+
let pending = memo.get(file);
|
|
1916
|
+
if (!pending) {
|
|
1917
|
+
pending = scan(file);
|
|
1918
|
+
memo.set(file, pending);
|
|
1919
|
+
}
|
|
1920
|
+
return pending;
|
|
1921
|
+
};
|
|
1922
|
+
vendorClientBoundaryLoads.set(config, load);
|
|
1923
|
+
return load;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
/**
|
|
1927
|
+
* Whether the react-server vendor pass scans this build's inputs at all. `target === 'server'` alone
|
|
1928
|
+
* is not the react-server graph: the PAGES router compiles under it too, and there `'use client'` is
|
|
1929
|
+
* inert - Next never runs an RSC pass for a pages route, so the file must be EXECUTED, not stubbed.
|
|
1930
|
+
* Stubbing it there turns a plain exported value into a truthy reference object, which crashes SSR.
|
|
1931
|
+
*/
|
|
1932
|
+
function vendorPluginOptions(
|
|
1933
|
+
config: ResolvedConfig,
|
|
1934
|
+
target: CompatAliasTarget,
|
|
1935
|
+
conditionTarget: ServerBundleTarget,
|
|
1936
|
+
): ServerEsbuildPluginOptions {
|
|
1937
|
+
const boundary =
|
|
1938
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
1939
|
+
process.env.PNEXT_VENDOR_CLIENT_BOUNDARY !== '0' &&
|
|
1940
|
+
target === 'server' &&
|
|
1941
|
+
!isPagesBundleTarget(conditionTarget) &&
|
|
1942
|
+
reactCompatEnabled(config)
|
|
1943
|
+
? vendorClientBoundaryLoad(config)
|
|
1944
|
+
: undefined;
|
|
1945
|
+
return { vendorClientBoundary: boundary, realPathEntries: true };
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
function isPagesBundleTarget(target: ServerBundleTarget) {
|
|
1949
|
+
return target === 'pages' || target === 'pages-api' || target === 'pages-api-edge';
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
// plugin-free vendor builds.
|
|
1953
|
+
//
|
|
1954
|
+
// The four core plugins above resolve by callback, and a callback is a JS bridge hop the bundler
|
|
1955
|
+
// waits on. Each behaviour leaves the callback in one of two ways - an esbuild-native option where
|
|
1956
|
+
// it is expressible, or a post-pass over the emitted chunks, where the metafile hands us the same
|
|
1957
|
+
// (importer, specifier, kind) triples the resolve callback used to see:
|
|
1958
|
+
//
|
|
1959
|
+
// builtins -> `packages: 'external'` covers them, nothing to do
|
|
1960
|
+
// asset stubs -> native `loader: empty`; an extension the map does not cover has no
|
|
1961
|
+
// loader, so the build fails and falls back - the safe direction
|
|
1962
|
+
// framework aliases -> external by default, post-pass repoints each one at its alias target
|
|
1963
|
+
// (require-kind separately, against the react-server layer map)
|
|
1964
|
+
// external packages -> external by default, post-pass runs the policy: nested-version pinning,
|
|
1965
|
+
// transitive vendor recursion, CommonJS facades
|
|
1966
|
+
//
|
|
1967
|
+
// What must be BUNDLED rather than externalized - a package's own subpaths, a default-only
|
|
1968
|
+
// CommonJS dependency, a cross-package asset - is named in `alias`, which overrides
|
|
1969
|
+
// `packages: 'external'` for exactly those specifiers and nothing else. That set is a property of
|
|
1970
|
+
// the package, so it is discovered once (`vendorPackageShape`) and every later build starts with it.
|
|
1971
|
+
// Anything still unexpressible bails to the plugin build; bailing is always available and always
|
|
1972
|
+
// correct, so this path never has to be complete - it has to be RIGHT, which is what the re-bundle
|
|
1973
|
+
// gate proves.
|
|
1974
|
+
//
|
|
1975
|
+
// Off by default: this pipeline is not spending its time in plugin callbacks, and two next-compat
|
|
1976
|
+
// behaviours still diverge - see `vendorNativeEnabled`.
|
|
1977
|
+
|
|
1978
|
+
/**
|
|
1979
|
+
* OFF by default: it is measurably slower AND not yet equivalent. `app-external` loses four
|
|
1980
|
+
* assertions with it on - `transpilePackages`, the react-server export condition, CJS tree-shaking
|
|
1981
|
+
* and an async external module all 500 - which the re-bundle gate cannot catch, because those
|
|
1982
|
+
* artifacts are structurally sound and semantically wrong.
|
|
1983
|
+
*/
|
|
1984
|
+
function vendorNativeEnabled() {
|
|
1985
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
1986
|
+
return process.env.PNEXT_VENDOR_NATIVE === '1';
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
/**
|
|
1990
|
+
* Artifacts whose plugin-free build did not work out. One bail is enough: the
|
|
1991
|
+
* cause is a property of the package (a require-kind alias to a CommonJS shim,
|
|
1992
|
+
* an asset needing real contents), not of the demand, so a retry pays for the
|
|
1993
|
+
* discarded build again and lands in the same place.
|
|
1994
|
+
*/
|
|
1995
|
+
const vendorNativeBailed = new Set<string>();
|
|
1996
|
+
|
|
1997
|
+
/** How the vendor builds of this process split between the two paths. */
|
|
1998
|
+
const vendorNativeStats = {
|
|
1999
|
+
attempts: 0,
|
|
2000
|
+
native: 0,
|
|
2001
|
+
bailed: 0,
|
|
2002
|
+
rounds: 0,
|
|
2003
|
+
buildMs: 0,
|
|
2004
|
+
pluginBuilds: 0,
|
|
2005
|
+
pluginBuildMs: 0,
|
|
2006
|
+
planMs: 0,
|
|
2007
|
+
verifyMs: 0,
|
|
2008
|
+
reasons: [] as string[],
|
|
2009
|
+
};
|
|
2010
|
+
|
|
2011
|
+
export function vendorNativeReport() {
|
|
2012
|
+
return { ...vendorNativeStats, reasons: vendorNativeStats.reasons.slice(0, 40) };
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
/**
|
|
2016
|
+
* What the asset plugin stubs, minus the extensions whose stub has CONTENT
|
|
2017
|
+
* (images, CSS modules). Those have no loader here on purpose: esbuild fails
|
|
2018
|
+
* the build, and failing is how they reach the plugin path.
|
|
2019
|
+
*/
|
|
2020
|
+
const vendorEmptyLoaders = {
|
|
2021
|
+
'.css': 'empty',
|
|
2022
|
+
'.scss': 'empty',
|
|
2023
|
+
'.sass': 'empty',
|
|
2024
|
+
'.less': 'empty',
|
|
2025
|
+
} satisfies Record<string, Loader>;
|
|
2026
|
+
|
|
2027
|
+
/** Discovery is a fixpoint; past this a package is not worth another try. */
|
|
2028
|
+
const MAX_VENDOR_NATIVE_ROUNDS = 2;
|
|
2029
|
+
|
|
2030
|
+
interface VendorNativeRequest {
|
|
2031
|
+
config: ResolvedConfig;
|
|
2032
|
+
target: CompatAliasTarget;
|
|
2033
|
+
conditionTarget: ServerBundleTarget;
|
|
2034
|
+
resolveDir: string;
|
|
2035
|
+
/** Cache key for the bail set — the artifact this build is producing. */
|
|
2036
|
+
bailKey: string;
|
|
2037
|
+
/** The package the entries belong to; its own subpaths are inlined. */
|
|
2038
|
+
packageName: string | undefined;
|
|
2039
|
+
entryPoints: { in: string; out: string }[] | string[];
|
|
2040
|
+
outdir?: string;
|
|
2041
|
+
splitting: boolean;
|
|
2042
|
+
/** Demanded siblings already on disk (group builds only). */
|
|
2043
|
+
siblings?: ReadonlyMap<string, string>;
|
|
2044
|
+
/** Entries THIS build emits, by resolved entry file: artifacts-to-be. */
|
|
2045
|
+
ownEntries?: ReadonlyMap<string, string>;
|
|
2046
|
+
/** Raised from inside another build: it may never wait on anything shared. */
|
|
2047
|
+
nested: boolean;
|
|
2048
|
+
/** Preplan-safe package: native build with pure ref decisions, no verify. */
|
|
2049
|
+
preplan?: boolean;
|
|
2050
|
+
/** Entry outputs, by emitted path; everything else is a shared chunk. */
|
|
2051
|
+
isEntry: (outputPath: string) => boolean;
|
|
2052
|
+
/** Where a solo build's single output lands, for the verification gate. */
|
|
2053
|
+
soloFile?: string;
|
|
2054
|
+
context: { nestedExternalFiles: string[] };
|
|
2055
|
+
/** Trace-only: filled with the producing pass's plugins/metafile. */
|
|
2056
|
+
trace?: VendorBuildTrace;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
/** A repointed reference: the specifier to emit, and the file it must exist at. */
|
|
2060
|
+
interface VendorRefTarget {
|
|
2061
|
+
specifier: string;
|
|
2062
|
+
file: string;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
/**
|
|
2066
|
+
* Run the plugin-free build, or return undefined to say "use the plugins". Every return path other than the
|
|
2067
|
+
* verified one is a bail, so a caller can treat undefined as "nothing happened" - no artifact is written here.
|
|
2068
|
+
*/
|
|
2069
|
+
async function buildVendorNative(request: VendorNativeRequest) {
|
|
2070
|
+
vendorNativeStats.attempts += 1;
|
|
2071
|
+
if (
|
|
2072
|
+
(!vendorNativeEnabled() && !request.preplan) ||
|
|
2073
|
+
vendorNativeBailed.has(request.bailKey)
|
|
2074
|
+
) {
|
|
2075
|
+
return undefined;
|
|
2076
|
+
}
|
|
2077
|
+
// A bail is remembered so the next demand does not pay for the same
|
|
2078
|
+
// discarded build — unless the cause was the fixpoint running out of rounds,
|
|
2079
|
+
// which the package shape this build just recorded has already moved on from.
|
|
2080
|
+
const bail = (why: string, permanent = true) => {
|
|
2081
|
+
if (permanent) vendorNativeBailed.add(request.bailKey);
|
|
2082
|
+
vendorNativeStats.bailed += 1;
|
|
2083
|
+
vendorNativeStats.reasons.push(`${request.packageName ?? '?'}: ${why}`);
|
|
2084
|
+
return undefined;
|
|
2085
|
+
};
|
|
2086
|
+
|
|
2087
|
+
const conditions = serverBundleConditions(request.conditionTarget);
|
|
2088
|
+
// What a package has to keep INSIDE its bundle is a property of the package,
|
|
2089
|
+
// not of one subpath's build, so it is discovered once and reused by every
|
|
2090
|
+
// later build of it — the fixpoint the §4e prototype ran app-wide, amortized
|
|
2091
|
+
// per package here.
|
|
2092
|
+
const shape = vendorPackageShape(request);
|
|
2093
|
+
// Only the FIRST build of a package discovers its shape; the rest wait for that answer instead of racing to
|
|
2094
|
+
// the same rounds. Off the slot, because a waiter that keeps its worker is exactly the inversion this is
|
|
2095
|
+
// here to fix. A nested build never waits - its own parent is holding a slot for it, and a cycle through two
|
|
2096
|
+
// packages would otherwise have nowhere to go.
|
|
2097
|
+
if (!request.nested && shape.discovery) {
|
|
2098
|
+
await outsideVendorSlot(() => shape.discovery!);
|
|
2099
|
+
}
|
|
2100
|
+
const pioneer = !shape.discovery;
|
|
2101
|
+
let settleDiscovery: (() => void) | undefined;
|
|
2102
|
+
if (pioneer) shape.discovery = new Promise<void>(resolve => (settleDiscovery = resolve));
|
|
2103
|
+
try {
|
|
2104
|
+
return await runVendorNativeRounds(request, shape, conditions, bail);
|
|
2105
|
+
} finally {
|
|
2106
|
+
if (pioneer) {
|
|
2107
|
+
shape.discovery = undefined;
|
|
2108
|
+
settleDiscovery?.();
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
async function runVendorNativeRounds(
|
|
2114
|
+
request: VendorNativeRequest,
|
|
2115
|
+
shape: VendorPackageShape,
|
|
2116
|
+
conditions: string[],
|
|
2117
|
+
bail: (why: string, permanent?: boolean) => undefined,
|
|
2118
|
+
) {
|
|
2119
|
+
const { config, resolveDir } = request;
|
|
2120
|
+
|
|
2121
|
+
for (let round = 1; round <= MAX_VENDOR_NATIVE_ROUNDS; round += 1) {
|
|
2122
|
+
let result;
|
|
2123
|
+
vendorNativeStats.rounds += 1;
|
|
2124
|
+
const buildStart = performance.now();
|
|
2125
|
+
const nativePlugins = getBundlerExtensions().serverEsbuildPlugins(
|
|
2126
|
+
config,
|
|
2127
|
+
vendorPluginOptions(config, request.target, request.conditionTarget),
|
|
2128
|
+
);
|
|
2129
|
+
if (request.trace) request.trace.plugins = nativePlugins.map(plugin => plugin.name);
|
|
2130
|
+
try {
|
|
2131
|
+
result = await build({
|
|
2132
|
+
entryPoints: (request.entryPoints as (string | { in: string; out: string })[]).map(
|
|
2133
|
+
entry =>
|
|
2134
|
+
typeof entry === 'string'
|
|
2135
|
+
? (getBundlerExtensions().serverBundleEntry(entry, resolveDir, entry) ?? entry)
|
|
2136
|
+
: {
|
|
2137
|
+
...entry,
|
|
2138
|
+
in:
|
|
2139
|
+
getBundlerExtensions().serverBundleEntry(entry.in, resolveDir, entry.in) ??
|
|
2140
|
+
entry.in,
|
|
2141
|
+
},
|
|
2142
|
+
) as string[],
|
|
2143
|
+
absWorkingDir: resolveDir,
|
|
2144
|
+
bundle: true,
|
|
2145
|
+
...(request.splitting ? { splitting: true, chunkNames: 'chunks/[name]-[hash]' } : {}),
|
|
2146
|
+
write: false,
|
|
2147
|
+
...(request.outdir ? { outdir: request.outdir, outExtension: { '.js': '.mjs' } } : {}),
|
|
2148
|
+
format: 'esm',
|
|
2149
|
+
platform: 'neutral',
|
|
2150
|
+
target: 'es2022',
|
|
2151
|
+
conditions,
|
|
2152
|
+
mainFields: ['module', 'main'],
|
|
2153
|
+
loader: { ...packageJsxLoaders, ...vendorEmptyLoaders },
|
|
2154
|
+
jsx: 'automatic',
|
|
2155
|
+
jsxImportSource: 'preact',
|
|
2156
|
+
logLevel: 'silent',
|
|
2157
|
+
metafile: true,
|
|
2158
|
+
...serverDefineOptions(),
|
|
2159
|
+
// Every bare specifier out — the cheapest possible graph — except the
|
|
2160
|
+
// handful `alias` names, which is how a same-package subpath or a
|
|
2161
|
+
// default-only CommonJS dependency stays INSIDE without giving up
|
|
2162
|
+
// externalization for everything else.
|
|
2163
|
+
packages: 'external',
|
|
2164
|
+
...(shape.inline.size > 0 ? { alias: Object.fromEntries(shape.inline) } : {}),
|
|
2165
|
+
plugins: nativePlugins,
|
|
2166
|
+
});
|
|
2167
|
+
} catch (error) {
|
|
2168
|
+
vendorNativeStats.buildMs += performance.now() - buildStart;
|
|
2169
|
+
return bail((error instanceof Error ? error.message : String(error)).slice(0, 200));
|
|
2170
|
+
}
|
|
2171
|
+
vendorNativeStats.buildMs += performance.now() - buildStart;
|
|
2172
|
+
|
|
2173
|
+
const inputs = Object.keys(result.metafile.inputs);
|
|
2174
|
+
// An asset whose stub carries content, or one a configured loader rule
|
|
2175
|
+
// claims, is not what `loader: empty` produced. Hand it to the plugins.
|
|
2176
|
+
for (const input of inputs) {
|
|
2177
|
+
const file = path.resolve(resolveDir, input);
|
|
2178
|
+
if (!serverIgnoredAssetFilter.test(file)) continue;
|
|
2179
|
+
if (getAssetExtensions().hasLoaderRuleFor(file)) return bail(`loader rule for ${input}`);
|
|
2180
|
+
if (getCssExtensions().loadCssModuleForClient(file) !== undefined) {
|
|
2181
|
+
return bail(`css module ${input}`);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
// Preplan class is ESM-only across the GRAPH, not just the entry.
|
|
2185
|
+
if (request.preplan) {
|
|
2186
|
+
for (const input of inputs) {
|
|
2187
|
+
const file = path.resolve(resolveDir, input);
|
|
2188
|
+
if (serverIgnoredAssetFilter.test(file) || !cachedExistsSync(file)) continue;
|
|
2189
|
+
if (/\.[cm]?tsx?$/.test(file)) return bail(`ts input ${input}`);
|
|
2190
|
+
if (file.endsWith('.cjs') ||(/\.jsx?$/.test(file) && !isEsmModuleEntry(file))) {
|
|
2191
|
+
return bail(`cjs input ${input}`);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
// Off the slot: everything below is discovery, and the bundler that needed
|
|
2197
|
+
// the worker has already handed its output over. Nothing else in the
|
|
2198
|
+
// pipeline can do this — the plugin chain's discovery runs inside a resolve
|
|
2199
|
+
// callback, with the bundler still open behind it.
|
|
2200
|
+
const planStart = performance.now();
|
|
2201
|
+
const plan = await outsideVendorSlot(() =>
|
|
2202
|
+
planVendorNativeRefs(request, result.metafile, conditions),
|
|
2203
|
+
);
|
|
2204
|
+
vendorNativeStats.planMs += performance.now() - planStart;
|
|
2205
|
+
if (!plan) return bail('unrepresentable reference');
|
|
2206
|
+
if (plan.inline.size > 0) {
|
|
2207
|
+
// Record before deciding: even a bail teaches the package's shape, so the
|
|
2208
|
+
// next build of it starts where this one gave up.
|
|
2209
|
+
let grew = false;
|
|
2210
|
+
for (const [specifier, file] of plan.inline) {
|
|
2211
|
+
if (shape.inline.has(specifier)) continue;
|
|
2212
|
+
shape.inline.set(specifier, file);
|
|
2213
|
+
grew = true;
|
|
2214
|
+
}
|
|
2215
|
+
if (!grew) return bail('inline set stopped converging');
|
|
2216
|
+
if (round === MAX_VENDOR_NATIVE_ROUNDS) return bail('inlining unsettled', false);
|
|
2217
|
+
continue;
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
const outputs = result.outputFiles.map(output => ({
|
|
2221
|
+
path: request.soloFile ?? output.path,
|
|
2222
|
+
text: rewriteEmittedRefs(output.text, ref =>
|
|
2223
|
+
(ref.require ? plan.requires : plan.imports).get(ref.specifier)?.specifier,
|
|
2224
|
+
),
|
|
2225
|
+
}));
|
|
2226
|
+
const repointed = new Map(
|
|
2227
|
+
[...plan.imports.values(), ...plan.requires.values()].map(t => [t.specifier, t.file]),
|
|
2228
|
+
);
|
|
2229
|
+
// Preplan mode: the drain barrier + byte-eq gates stand in for the
|
|
2230
|
+
// re-bundle verification loop — its cost is one of the two this composes out.
|
|
2231
|
+
if (!request.preplan) {
|
|
2232
|
+
const verifyStart = performance.now();
|
|
2233
|
+
const chunks = new Map(
|
|
2234
|
+
outputs.filter(output => !request.isEntry(output.path)).map(o => [o.path, o.text]),
|
|
2235
|
+
);
|
|
2236
|
+
for (const output of outputs) {
|
|
2237
|
+
if (!request.isEntry(output.path)) continue;
|
|
2238
|
+
const failure = await verifyVendorArtifact(output.path, output.text, repointed, chunks);
|
|
2239
|
+
if (failure) {
|
|
2240
|
+
vendorNativeStats.verifyMs += performance.now() - verifyStart;
|
|
2241
|
+
return bail(`verification: ${failure}`);
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
vendorNativeStats.verifyMs += performance.now() - verifyStart;
|
|
2245
|
+
} else {
|
|
2246
|
+
vendorPreplanStats.composedBuilds += 1;
|
|
2247
|
+
schedulePreplanReport();
|
|
2248
|
+
}
|
|
2249
|
+
vendorNativeStats.native += 1;
|
|
2250
|
+
if (request.trace) request.trace.metafile = result.metafile;
|
|
2251
|
+
return outputs;
|
|
2252
|
+
}
|
|
2253
|
+
return undefined;
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
/**
|
|
2257
|
+
* What one package externalizes and what it keeps inside - discovered once and then reused, so the
|
|
2258
|
+
* rounds a package costs are paid by its FIRST build and no other. Seeded from the manifest: a
|
|
2259
|
+
* declared dependency is external before any build proves it, which keeps a package's own subpaths
|
|
2260
|
+
* (the ones `packages: 'external'` cannot tell apart from dependencies) bundled from the start.
|
|
2261
|
+
*/
|
|
2262
|
+
interface VendorPackageShape {
|
|
2263
|
+
/** Specifier -> file it must be bundled from, as an esbuild `alias` entry. */
|
|
2264
|
+
inline: Map<string, string>;
|
|
2265
|
+
/** Set while one build is discovering it; every other build awaits this. */
|
|
2266
|
+
discovery?: Promise<void>;
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
const vendorPackageShapes = new Map<string, VendorPackageShape>();
|
|
2270
|
+
|
|
2271
|
+
function vendorPackageShape(request: VendorNativeRequest): VendorPackageShape {
|
|
2272
|
+
const entry = (request.entryPoints as { in?: string }[])[0];
|
|
2273
|
+
const first = typeof entry === 'string' ? entry : entry?.in;
|
|
2274
|
+
// A workspace package resolves to its real source path, which has no
|
|
2275
|
+
// `node_modules` segment to key on — walk up to its manifest instead.
|
|
2276
|
+
const root =
|
|
2277
|
+
(first && request.packageName && vendorPackageRoot(first, request.packageName)) ??
|
|
2278
|
+
(first && nearestManifestDir(path.dirname(first)));
|
|
2279
|
+
if (!root) return { inline: new Map() };
|
|
2280
|
+
const key = `${root}\0${request.target}\0${request.conditionTarget}`;
|
|
2281
|
+
let shape = vendorPackageShapes.get(key);
|
|
2282
|
+
if (!shape) {
|
|
2283
|
+
shape = { inline: new Map() };
|
|
2284
|
+
vendorPackageShapes.set(key, shape);
|
|
2285
|
+
}
|
|
2286
|
+
return shape;
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
function nearestManifestDir(from: string) {
|
|
2290
|
+
let dir = from;
|
|
2291
|
+
while (true) {
|
|
2292
|
+
if (existsSync(path.join(dir, 'package.json'))) return dir;
|
|
2293
|
+
const parent = path.dirname(dir);
|
|
2294
|
+
if (parent === dir) return undefined;
|
|
2295
|
+
dir = parent;
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
/**
|
|
2300
|
+
* Decide what each external reference in the emitted code means - the work the resolve callbacks used
|
|
2301
|
+
* to do, off the bundler's critical path. Undefined when a reference cannot be expressed as a rewrite
|
|
2302
|
+
* (the caller then bails), or the set of specifiers that must be pulled back INSIDE the bundle.
|
|
2303
|
+
*/
|
|
2304
|
+
async function planVendorNativeRefs(
|
|
2305
|
+
request: VendorNativeRequest,
|
|
2306
|
+
metafile: Metafile,
|
|
2307
|
+
conditions: string[],
|
|
2308
|
+
) {
|
|
2309
|
+
const imports = new Map<string, VendorRefTarget>();
|
|
2310
|
+
const requires = new Map<string, VendorRefTarget>();
|
|
2311
|
+
const inline = new Map<string, string>();
|
|
2312
|
+
const settled = new Set<string>();
|
|
2313
|
+
|
|
2314
|
+
for (const [input, meta] of Object.entries(metafile.inputs)) {
|
|
2315
|
+
// Decisions are a property of the DIRECTORY an import sits in, never of the
|
|
2316
|
+
// file — so a package's several hundred modules all asking for `react`
|
|
2317
|
+
// decide once, and the answer is reused across builds.
|
|
2318
|
+
const importerFile = path.resolve(request.resolveDir, input);
|
|
2319
|
+
const importerDir = path.dirname(importerFile);
|
|
2320
|
+
for (const reference of meta.imports) {
|
|
2321
|
+
if (!reference.external) continue;
|
|
2322
|
+
const specifier = reference.path;
|
|
2323
|
+
const isRequire = reference.kind === 'require-call';
|
|
2324
|
+
const key = `${isRequire ? 'r' : 'i'}\0${specifier}\0${importerDir}`;
|
|
2325
|
+
if (settled.has(key)) continue;
|
|
2326
|
+
settled.add(key);
|
|
2327
|
+
|
|
2328
|
+
// Same package. A subpath THIS build already emits is repointed at that artifact; a sibling on disk at
|
|
2329
|
+
// its own. Everything else becomes its own vendor demand rather than being inlined here - inlining is
|
|
2330
|
+
// what makes a package's shared graph get re-parsed once per subpath, the cost this whole pipeline
|
|
2331
|
+
// exists to remove. Request-scoped, so it sits in front of the shared decision cache.
|
|
2332
|
+
if (packageNameFromSpecifier(specifier) === request.packageName) {
|
|
2333
|
+
const sibling = request.siblings?.get(specifier);
|
|
2334
|
+
if (sibling) {
|
|
2335
|
+
if (!record(imports, specifier, { specifier: sibling, file: fileURLToPath(sibling) })) {
|
|
2336
|
+
return undefined;
|
|
2337
|
+
}
|
|
2338
|
+
continue;
|
|
2339
|
+
}
|
|
2340
|
+
const own = ownEntryArtifact(request, specifier, importerDir, conditions);
|
|
2341
|
+
if (own) {
|
|
2342
|
+
if (!record(imports, specifier, own)) return undefined;
|
|
2343
|
+
continue;
|
|
2344
|
+
}
|
|
2345
|
+
// A subpath nothing demanded: it comes back inside, as the plugin build
|
|
2346
|
+
// would have had it. Demanding it instead can close a cycle back onto
|
|
2347
|
+
// the artifact being built right now, which no scheduler can settle.
|
|
2348
|
+
const resolved = resolveVendorPackageSpecifier(
|
|
2349
|
+
request.config.root,
|
|
2350
|
+
path.join(importerDir, 'pnext-resolve.ts'),
|
|
2351
|
+
specifier,
|
|
2352
|
+
conditions,
|
|
2353
|
+
);
|
|
2354
|
+
if (!resolved) return undefined;
|
|
2355
|
+
inline.set(specifier, resolved);
|
|
2356
|
+
continue;
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
const decision = await (request.preplan
|
|
2360
|
+
? preplanVendorRef(request, specifier, importerFile, isRequire, conditions)
|
|
2361
|
+
: decideVendorRef(request, specifier, importerFile, isRequire, conditions));
|
|
2362
|
+
if (!decision) return undefined;
|
|
2363
|
+
if (decision.kind === 'keep') continue;
|
|
2364
|
+
if (decision.kind === 'inline') {
|
|
2365
|
+
inline.set(specifier, decision.file);
|
|
2366
|
+
continue;
|
|
2367
|
+
}
|
|
2368
|
+
if (decision.nested) request.context.nestedExternalFiles.push(decision.nested);
|
|
2369
|
+
if (!record(isRequire ? requires : imports, specifier, decision.target)) return undefined;
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
return { imports, requires, inline };
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
/**
|
|
2376
|
+
* The artifact THIS build is already emitting for a same-package specifier, if it is one of its entries. It
|
|
2377
|
+
* is not on disk yet - it is written the moment this build returns - so it is repointed without an existence
|
|
2378
|
+
* check.
|
|
2379
|
+
*/
|
|
2380
|
+
function ownEntryArtifact(
|
|
2381
|
+
request: VendorNativeRequest,
|
|
2382
|
+
specifier: string,
|
|
2383
|
+
importerDir: string,
|
|
2384
|
+
conditions: string[],
|
|
2385
|
+
): VendorRefTarget | undefined {
|
|
2386
|
+
if (!request.ownEntries?.size) return undefined;
|
|
2387
|
+
const entry = resolveVendorPackageSpecifier(
|
|
2388
|
+
request.config.root,
|
|
2389
|
+
path.join(importerDir, 'pnext-resolve.ts'),
|
|
2390
|
+
specifier,
|
|
2391
|
+
conditions,
|
|
2392
|
+
);
|
|
2393
|
+
const artifact = entry && request.ownEntries.get(entry);
|
|
2394
|
+
return artifact ? { specifier: pathToFileHref(artifact), file: artifact } : undefined;
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
type VendorRefDecision =
|
|
2398
|
+
| { kind: 'keep' }
|
|
2399
|
+
/** Must be bundled: `file` is what an alias entry points the specifier at. */
|
|
2400
|
+
| { kind: 'inline'; file: string }
|
|
2401
|
+
| { kind: 'repoint'; target: VendorRefTarget; nested?: string };
|
|
2402
|
+
|
|
2403
|
+
/**
|
|
2404
|
+
* Decisions are pure in (layer, specifier, importing directory), and the same
|
|
2405
|
+
* handful of packages are imported from everywhere, so one answer serves every
|
|
2406
|
+
* build that meets the reference again. Without this the post-pass re-runs the
|
|
2407
|
+
* resolver work the plugin chain at least only did once per bundle.
|
|
2408
|
+
*/
|
|
2409
|
+
const vendorRefDecisions = new Map<string, Promise<VendorRefDecision | undefined>>();
|
|
2410
|
+
|
|
2411
|
+
/** Everything the plugin-free path learned about packages, forgotten. */
|
|
2412
|
+
function clearVendorNativeCaches() {
|
|
2413
|
+
vendorRefDecisions.clear();
|
|
2414
|
+
vendorNativeBailed.clear();
|
|
2415
|
+
vendorPackageShapes.clear();
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
/**
|
|
2419
|
+
* Preplan replacement for `decideVendorRef`: the same classification from pure inputs only - no awaited child
|
|
2420
|
+
* builds, no facade exec. Anything it cannot settle purely returns undefined, bailing the whole package to
|
|
2421
|
+
* the plugin path. Cached beside the plugin-era decisions under a distinct key prefix.
|
|
2422
|
+
*/
|
|
2423
|
+
function preplanVendorRef(
|
|
2424
|
+
request: VendorNativeRequest,
|
|
2425
|
+
specifier: string,
|
|
2426
|
+
importerFile: string,
|
|
2427
|
+
isRequire: boolean,
|
|
2428
|
+
conditions: string[],
|
|
2429
|
+
) {
|
|
2430
|
+
const importerDir = path.dirname(importerFile);
|
|
2431
|
+
const key = `p\0${request.target}\0${request.conditionTarget}\0${isRequire ? 'r' : 'i'}\0${specifier}\0${importerDir}`;
|
|
2432
|
+
let decision = vendorRefDecisions.get(key);
|
|
2433
|
+
if (!decision) {
|
|
2434
|
+
decision = preplanVendorRefUncached(request, specifier, importerFile, isRequire, conditions);
|
|
2435
|
+
vendorRefDecisions.set(key, decision);
|
|
2436
|
+
}
|
|
2437
|
+
return decision;
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
async function preplanVendorRefUncached(
|
|
2441
|
+
request: VendorNativeRequest,
|
|
2442
|
+
specifier: string,
|
|
2443
|
+
importerFile: string,
|
|
2444
|
+
isRequire: boolean,
|
|
2445
|
+
conditions: string[],
|
|
2446
|
+
): Promise<VendorRefDecision | undefined> {
|
|
2447
|
+
const { config, target } = request;
|
|
2448
|
+
const importerDir = path.dirname(importerFile);
|
|
2449
|
+
const keep = { kind: 'keep' } as const;
|
|
2450
|
+
const depName = packageNameFromSpecifier(specifier);
|
|
2451
|
+
if (depName && (builtinModules.includes(depName) || isBunBuiltin(depName))) return keep;
|
|
2452
|
+
// Require-of-external is the async-interop shape that 500ed; not this class.
|
|
2453
|
+
if (isRequire) return undefined;
|
|
2454
|
+
if (aliasSpecifierFilter.test(specifier)) {
|
|
2455
|
+
const resolved = coreAliases(config, target)[specifier] ?? firstAliasForSpecifier(specifier);
|
|
2456
|
+
if (!resolved) return undefined;
|
|
2457
|
+
if (!path.isAbsolute(resolved)) return keep;
|
|
2458
|
+
return { kind: 'repoint', target: { specifier: pathToFileHref(resolved), file: resolved } };
|
|
2459
|
+
}
|
|
2460
|
+
if (serverIgnoredAssetFilter.test(specifier)) {
|
|
2461
|
+
const asset = resolveVendorPackageSpecifier(
|
|
2462
|
+
config.root,
|
|
2463
|
+
path.join(importerDir, 'pnext-resolve.ts'),
|
|
2464
|
+
specifier,
|
|
2465
|
+
conditions,
|
|
2466
|
+
);
|
|
2467
|
+
return asset ? { kind: 'inline', file: asset } : undefined;
|
|
2468
|
+
}
|
|
2469
|
+
if (!depName || !isPackageSpecifier(specifier)) return undefined;
|
|
2470
|
+
if (getExternalPackagePolicy().external(depName)) {
|
|
2471
|
+
const fromImporter = resolveNestedPackageFromImporter(importerFile, specifier, conditions);
|
|
2472
|
+
const fromRoot = resolveVendorPackageSpecifier(
|
|
2473
|
+
config.root,
|
|
2474
|
+
path.join(config.root, 'pnext-resolve.ts'),
|
|
2475
|
+
specifier,
|
|
2476
|
+
conditions,
|
|
2477
|
+
);
|
|
2478
|
+
if (!fromImporter || fromImporter === fromRoot) return keep;
|
|
2479
|
+
return {
|
|
2480
|
+
kind: 'repoint',
|
|
2481
|
+
target: { specifier: fromImporter, file: fromImporter },
|
|
2482
|
+
nested: fromImporter,
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
const planned = await preplanDependencyDecision(
|
|
2486
|
+
config,
|
|
2487
|
+
target,
|
|
2488
|
+
request.conditionTarget,
|
|
2489
|
+
specifier,
|
|
2490
|
+
importerDir,
|
|
2491
|
+
);
|
|
2492
|
+
if (!planned?.path) return undefined;
|
|
2493
|
+
return {
|
|
2494
|
+
kind: 'repoint',
|
|
2495
|
+
target: { specifier: planned.path, file: fileURLToPath(planned.path) },
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
function decideVendorRef(
|
|
2500
|
+
request: VendorNativeRequest,
|
|
2501
|
+
specifier: string,
|
|
2502
|
+
importerFile: string,
|
|
2503
|
+
isRequire: boolean,
|
|
2504
|
+
conditions: string[],
|
|
2505
|
+
) {
|
|
2506
|
+
const importerDir = path.dirname(importerFile);
|
|
2507
|
+
const key = `${request.target}\0${request.conditionTarget}\0${isRequire ? 'r' : 'i'}\0${specifier}\0${importerDir}`;
|
|
2508
|
+
let decision = vendorRefDecisions.get(key);
|
|
2509
|
+
if (!decision) {
|
|
2510
|
+
decision = decideVendorRefUncached(request, specifier, importerFile, isRequire, conditions);
|
|
2511
|
+
vendorRefDecisions.set(key, decision);
|
|
2512
|
+
}
|
|
2513
|
+
return decision;
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
async function decideVendorRefUncached(
|
|
2517
|
+
request: VendorNativeRequest,
|
|
2518
|
+
specifier: string,
|
|
2519
|
+
importerFile: string,
|
|
2520
|
+
isRequire: boolean,
|
|
2521
|
+
conditions: string[],
|
|
2522
|
+
): Promise<VendorRefDecision | undefined> {
|
|
2523
|
+
const { config, target, conditionTarget } = request;
|
|
2524
|
+
const importerDir = path.dirname(importerFile);
|
|
2525
|
+
const keep = { kind: 'keep' } as const;
|
|
2526
|
+
const depName = packageNameFromSpecifier(specifier);
|
|
2527
|
+
if (depName && (builtinModules.includes(depName) || isBunBuiltin(depName))) return keep;
|
|
2528
|
+
|
|
2529
|
+
// Framework aliases. A require-call reads the react-server layer — a package
|
|
2530
|
+
// probing `'useState' in require('react')` must see false — so the two kinds
|
|
2531
|
+
// of reference to one specifier are repointed separately.
|
|
2532
|
+
if (aliasSpecifierFilter.test(specifier)) {
|
|
2533
|
+
const resolved = coreAliases(config, target)[specifier] ?? firstAliasForSpecifier(specifier);
|
|
2534
|
+
if (!resolved) return undefined;
|
|
2535
|
+
if (isRequire) {
|
|
2536
|
+
// A CommonJS shim swap (`next/navigation` -> navigation.cjs) is the one
|
|
2537
|
+
// alias the plugin BUNDLES; leave that build to the plugin.
|
|
2538
|
+
if (serverRequireAlias(specifier, resolved, 'require-call') !== resolved) return undefined;
|
|
2539
|
+
const layerAliases =
|
|
2540
|
+
target === 'server' ? getImportAliasExtensions().reactServerLayerAliases(config) : undefined;
|
|
2541
|
+
const layer = layerAliases?.[specifier] ?? resolved;
|
|
2542
|
+
const file = path.isAbsolute(layer) ? layer : require.resolve(layer);
|
|
2543
|
+
return { kind: 'repoint', target: { specifier: file, file } };
|
|
2544
|
+
}
|
|
2545
|
+
if (!path.isAbsolute(resolved)) return keep;
|
|
2546
|
+
return { kind: 'repoint', target: { specifier: pathToFileHref(resolved), file: resolved } };
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
// An asset the bundler externalized (a cross-package CSS import) has to come
|
|
2550
|
+
// back inside, where `loader: empty` stubs it.
|
|
2551
|
+
if (serverIgnoredAssetFilter.test(specifier)) {
|
|
2552
|
+
const asset = resolveVendorPackageSpecifier(
|
|
2553
|
+
config.root,
|
|
2554
|
+
path.join(importerDir, 'pnext-resolve.ts'),
|
|
2555
|
+
specifier,
|
|
2556
|
+
conditions,
|
|
2557
|
+
);
|
|
2558
|
+
return asset ? { kind: 'inline', file: asset } : undefined;
|
|
2559
|
+
}
|
|
2560
|
+
if (!depName || !isPackageSpecifier(specifier)) return undefined;
|
|
2561
|
+
|
|
2562
|
+
// B7: a serverExternalPackage stays external, pinned to the importing
|
|
2563
|
+
// package's own copy when that differs from the hoisted root version.
|
|
2564
|
+
if (getExternalPackagePolicy().external(depName)) {
|
|
2565
|
+
const fromImporter = resolveNestedPackageFromImporter(importerFile, specifier, conditions);
|
|
2566
|
+
const fromRoot = resolveVendorPackageSpecifier(
|
|
2567
|
+
config.root,
|
|
2568
|
+
path.join(config.root, 'pnext-resolve.ts'),
|
|
2569
|
+
specifier,
|
|
2570
|
+
conditions,
|
|
2571
|
+
);
|
|
2572
|
+
if (!fromImporter || fromImporter === fromRoot) return keep;
|
|
2573
|
+
return {
|
|
2574
|
+
kind: 'repoint',
|
|
2575
|
+
target: { specifier: fromImporter, file: fromImporter },
|
|
2576
|
+
nested: fromImporter,
|
|
2577
|
+
};
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
const resolvedEntry = resolveVendorPackageSpecifier(
|
|
2581
|
+
config.root,
|
|
2582
|
+
path.join(importerDir, 'pnext-resolve.ts'),
|
|
2583
|
+
specifier,
|
|
2584
|
+
conditions,
|
|
2585
|
+
);
|
|
2586
|
+
// A CommonJS dependency loses its named exports when externalized, so it is
|
|
2587
|
+
// inlined unless a verified facade can republish them.
|
|
2588
|
+
const commonJs = Boolean(resolvedEntry) && !isEsmModuleEntry(resolvedEntry!);
|
|
2589
|
+
if (commonJs && !(await cjsEntryMayHaveNamedExports(resolvedEntry!))) {
|
|
2590
|
+
return { kind: 'inline', file: resolvedEntry! };
|
|
2591
|
+
}
|
|
2592
|
+
let vendored: string;
|
|
2593
|
+
const tv = performance.now();
|
|
2594
|
+
try {
|
|
2595
|
+
vendored = await externalServerPackageHref(
|
|
2596
|
+
config,
|
|
2597
|
+
specifier,
|
|
2598
|
+
target,
|
|
2599
|
+
importerDir,
|
|
2600
|
+
conditionTarget,
|
|
2601
|
+
// Raised from inside a running build: must not queue behind it.
|
|
2602
|
+
true,
|
|
2603
|
+
);
|
|
2604
|
+
} catch (error) {
|
|
2605
|
+
if (isResolveFailure(error)) return keep;
|
|
2606
|
+
return undefined;
|
|
2607
|
+
}
|
|
2608
|
+
if (vendorTraceEnabled()) {
|
|
2609
|
+
vendorTraceRow({
|
|
2610
|
+
kind: 'edge',
|
|
2611
|
+
from: request.packageName ?? '?',
|
|
2612
|
+
fromTarget: target,
|
|
2613
|
+
fromCondition: conditionTarget,
|
|
2614
|
+
to: specifier,
|
|
2615
|
+
toResolveDir: importerDir,
|
|
2616
|
+
});
|
|
2617
|
+
}
|
|
2618
|
+
if (commonJs) {
|
|
2619
|
+
heavyProfRow({ k: 'dep-build-wait', site: 'ref', spec: specifier, ms: performance.now() - tv });
|
|
2620
|
+
const tf = performance.now();
|
|
2621
|
+
const facade = await cjsNamedExportFacade(fileURLToPath(vendored), vendored);
|
|
2622
|
+
heavyProfRow({ k: 'facade-wait', site: 'ref', spec: specifier, ms: performance.now() - tf });
|
|
2623
|
+
if (!facade) return { kind: 'inline', file: resolvedEntry! };
|
|
2624
|
+
return {
|
|
2625
|
+
kind: 'repoint',
|
|
2626
|
+
target: { specifier: pathToFileHref(facade), file: facade },
|
|
2627
|
+
};
|
|
2628
|
+
}
|
|
2629
|
+
return {
|
|
2630
|
+
kind: 'repoint',
|
|
2631
|
+
target: { specifier: vendored, file: fileURLToPath(vendored) },
|
|
2632
|
+
};
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
/**
|
|
2636
|
+
* Record one specifier's target. A chunk mixes importers, so a specifier that two importers resolve
|
|
2637
|
+
* DIFFERENTLY (a pinned nested version for one of them) cannot be rewritten in place - that is a bail, not a
|
|
2638
|
+
* choice.
|
|
2639
|
+
*/
|
|
2640
|
+
function record(
|
|
2641
|
+
targets: Map<string, VendorRefTarget>,
|
|
2642
|
+
specifier: string,
|
|
2643
|
+
target: VendorRefTarget,
|
|
2644
|
+
) {
|
|
2645
|
+
const existing = targets.get(specifier);
|
|
2646
|
+
if (existing) return existing.specifier === target.specifier;
|
|
2647
|
+
targets.set(specifier, target);
|
|
2648
|
+
return true;
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
async function writeExternalServerPackageBundle(
|
|
2652
|
+
config: ResolvedConfig,
|
|
2653
|
+
specifier: string,
|
|
2654
|
+
target: CompatAliasTarget,
|
|
2655
|
+
resolveDir: string,
|
|
2656
|
+
file: string,
|
|
2657
|
+
conditionTarget: ServerBundleTarget,
|
|
2658
|
+
nested: boolean,
|
|
2659
|
+
) {
|
|
2660
|
+
const context = { nestedExternalFiles: [] as string[] };
|
|
2661
|
+
// The layer is part of the label: the same specifier legitimately compiles
|
|
2662
|
+
// once per target, and a profile that cannot tell those apart reads as
|
|
2663
|
+
// duplication that is not there.
|
|
2664
|
+
const bundled = await profileRuntimeStep(`vendor bundle ${specifier} [${target}/${conditionTarget}]`, () =>
|
|
2665
|
+
bundleExternalPackage(
|
|
2666
|
+
config,
|
|
2667
|
+
specifier,
|
|
2668
|
+
target,
|
|
2669
|
+
resolveDir,
|
|
2670
|
+
conditionTarget,
|
|
2671
|
+
context,
|
|
2672
|
+
file,
|
|
2673
|
+
nested,
|
|
2674
|
+
),
|
|
2675
|
+
);
|
|
2676
|
+
// Packages can ship 'use cache' functions too (Next allows it); client
|
|
2677
|
+
// bundles keep the source untouched.
|
|
2678
|
+
const code =
|
|
2679
|
+
target === 'server' ? applyBundledSourceTransforms(bundled.code, specifier) : bundled.code;
|
|
2680
|
+
const id = await writeVendorArtifact(file, code);
|
|
2681
|
+
recordVendorLayerBuild(
|
|
2682
|
+
config,
|
|
2683
|
+
resolveVendorEntry(config, specifier, resolveDir, conditionTarget),
|
|
2684
|
+
target,
|
|
2685
|
+
conditionTarget,
|
|
2686
|
+
bundled.metafile,
|
|
2687
|
+
code,
|
|
2688
|
+
id,
|
|
2689
|
+
);
|
|
2690
|
+
// When this vendor bundle pins a transitive external to a nested version distinct from the hoisted root
|
|
2691
|
+
// copy, Node/webpack file tracing surfaces that dependency's source as a `.js` artifact in the server
|
|
2692
|
+
// output. Emit a `.js` trace copy of each such source so build tooling that scans `**/*.js` - never `.mjs` -
|
|
2693
|
+
// observes the bundled transitive too. These are trace artifacts only; nothing imports them.
|
|
2694
|
+
await emitNestedExternalTraceCopies(file, context.nestedExternalFiles);
|
|
2695
|
+
return file;
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
/** Trace-only side channel: what the esbuild pass that produced the code did. */
|
|
2699
|
+
interface VendorBuildTrace {
|
|
2700
|
+
plugins: string[];
|
|
2701
|
+
entry?: string;
|
|
2702
|
+
metafile?: Metafile;
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
/** Emit one JSONL build row from a finished (or failed) vendor esbuild pass. */
|
|
2706
|
+
function traceVendorBuild(row: {
|
|
2707
|
+
seq: number;
|
|
2708
|
+
specifier: string;
|
|
2709
|
+
resolveDir: string;
|
|
2710
|
+
target: string;
|
|
2711
|
+
conditionTarget: ServerBundleTarget;
|
|
2712
|
+
conditions: string[];
|
|
2713
|
+
startMs: number;
|
|
2714
|
+
outBytes: number;
|
|
2715
|
+
trace: VendorBuildTrace;
|
|
2716
|
+
ok: boolean;
|
|
2717
|
+
error?: string;
|
|
2718
|
+
}) {
|
|
2719
|
+
const inputs = row.trace.metafile?.inputs ?? {};
|
|
2720
|
+
const inputFiles = Object.keys(inputs);
|
|
2721
|
+
const endMs = performance.now();
|
|
2722
|
+
vendorTraceRow({
|
|
2723
|
+
seq: row.seq,
|
|
2724
|
+
specifier: row.specifier,
|
|
2725
|
+
resolveDir: row.resolveDir,
|
|
2726
|
+
target: row.target,
|
|
2727
|
+
conditionTarget: row.conditionTarget,
|
|
2728
|
+
entry: row.trace.entry,
|
|
2729
|
+
conditions: row.conditions,
|
|
2730
|
+
startMs: row.startMs,
|
|
2731
|
+
endMs,
|
|
2732
|
+
ms: endMs - row.startMs,
|
|
2733
|
+
inputCount: inputFiles.length,
|
|
2734
|
+
inputBytes: inputFiles.reduce((sum, input) => sum + (inputs[input]?.bytes ?? 0), 0),
|
|
2735
|
+
inputFiles,
|
|
2736
|
+
outBytes: row.outBytes,
|
|
2737
|
+
plugins: row.trace.plugins,
|
|
2738
|
+
ok: row.ok,
|
|
2739
|
+
...(row.error ? { error: row.error } : {}),
|
|
2740
|
+
});
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
async function bundleExternalPackage(
|
|
2744
|
+
config: ResolvedConfig,
|
|
2745
|
+
specifier: string,
|
|
2746
|
+
target: CompatAliasTarget,
|
|
2747
|
+
resolveDir: string,
|
|
2748
|
+
conditionTarget: ServerBundleTarget,
|
|
2749
|
+
context?: { nestedExternalFiles: string[] },
|
|
2750
|
+
/** The artifact this bundle becomes — the plugin-free path's identity. */
|
|
2751
|
+
file?: string,
|
|
2752
|
+
nested = false,
|
|
2753
|
+
) {
|
|
2754
|
+
if (!vendorTraceEnabled()) {
|
|
2755
|
+
return bundleExternalPackageImpl(
|
|
2756
|
+
config,
|
|
2757
|
+
specifier,
|
|
2758
|
+
target,
|
|
2759
|
+
resolveDir,
|
|
2760
|
+
conditionTarget,
|
|
2761
|
+
context,
|
|
2762
|
+
file,
|
|
2763
|
+
nested,
|
|
2764
|
+
);
|
|
2765
|
+
}
|
|
2766
|
+
const trace: VendorBuildTrace = { plugins: [] };
|
|
2767
|
+
const seq = nextVendorTraceSeq();
|
|
2768
|
+
const startMs = performance.now();
|
|
2769
|
+
const common = {
|
|
2770
|
+
seq,
|
|
2771
|
+
specifier,
|
|
2772
|
+
resolveDir,
|
|
2773
|
+
target,
|
|
2774
|
+
conditionTarget,
|
|
2775
|
+
conditions: serverBundleConditions(conditionTarget),
|
|
2776
|
+
startMs,
|
|
2777
|
+
trace,
|
|
2778
|
+
};
|
|
2779
|
+
try {
|
|
2780
|
+
const result = await bundleExternalPackageImpl(
|
|
2781
|
+
config,
|
|
2782
|
+
specifier,
|
|
2783
|
+
target,
|
|
2784
|
+
resolveDir,
|
|
2785
|
+
conditionTarget,
|
|
2786
|
+
context,
|
|
2787
|
+
file,
|
|
2788
|
+
nested,
|
|
2789
|
+
trace,
|
|
2790
|
+
);
|
|
2791
|
+
traceVendorBuild({ ...common, outBytes: result.code.length, ok: true });
|
|
2792
|
+
return result;
|
|
2793
|
+
} catch (error) {
|
|
2794
|
+
traceVendorBuild({
|
|
2795
|
+
...common,
|
|
2796
|
+
outBytes: 0,
|
|
2797
|
+
ok: false,
|
|
2798
|
+
error: (error instanceof Error ? error.message : String(error)).slice(0, 300),
|
|
2799
|
+
});
|
|
2800
|
+
throw error;
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
async function bundleExternalPackageImpl(
|
|
2805
|
+
config: ResolvedConfig,
|
|
2806
|
+
specifier: string,
|
|
2807
|
+
target: CompatAliasTarget,
|
|
2808
|
+
resolveDir: string,
|
|
2809
|
+
conditionTarget: ServerBundleTarget,
|
|
2810
|
+
context?: { nestedExternalFiles: string[] },
|
|
2811
|
+
/** The artifact this bundle becomes — the plugin-free path's identity. */
|
|
2812
|
+
file?: string,
|
|
2813
|
+
nested = false,
|
|
2814
|
+
trace?: VendorBuildTrace,
|
|
2815
|
+
) {
|
|
2816
|
+
const packageName = packageNameFromSpecifier(specifier);
|
|
2817
|
+
const resolvedEntry = resolveVendorPackageSpecifier(
|
|
2818
|
+
config.root,
|
|
2819
|
+
path.join(resolveDir, 'pnext-resolve.ts'),
|
|
2820
|
+
specifier,
|
|
2821
|
+
serverBundleConditions(conditionTarget),
|
|
2822
|
+
);
|
|
2823
|
+
const requiredEntry =
|
|
2824
|
+
resolvedEntry &&
|
|
2825
|
+
!isEsmModuleFile(resolvedEntry) &&
|
|
2826
|
+
getExternalPackagePolicy().esmExternals() === true
|
|
2827
|
+
? resolveVendorPackageSpecifier(
|
|
2828
|
+
config.root,
|
|
2829
|
+
path.join(resolveDir, 'pnext-resolve.ts'),
|
|
2830
|
+
specifier,
|
|
2831
|
+
serverBundleRequireConditions(conditionTarget),
|
|
2832
|
+
)
|
|
2833
|
+
: undefined;
|
|
2834
|
+
if (
|
|
2835
|
+
packageName &&
|
|
2836
|
+
resolvedEntry?.match(/\.[cm]?tsx?$/) &&
|
|
2837
|
+
!getExternalPackagePolicy().transpile(packageName)
|
|
2838
|
+
) {
|
|
2839
|
+
throw new Error(
|
|
2840
|
+
`${path.relative(config.root, resolvedEntry)}\nModule parse failed: Unexpected token`,
|
|
2841
|
+
);
|
|
2842
|
+
}
|
|
2843
|
+
const entry =
|
|
2844
|
+
requiredEntry ??
|
|
2845
|
+
resolveLinkedPackageSpecifier(
|
|
2846
|
+
config.root,
|
|
2847
|
+
path.join(resolveDir, 'pnext-resolve.ts'),
|
|
2848
|
+
specifier,
|
|
2849
|
+
serverBundleConditions(conditionTarget),
|
|
2850
|
+
) ??
|
|
2851
|
+
specifier;
|
|
2852
|
+
// `packages: 'external'` refuses to bundle a BARE entry point, so the
|
|
2853
|
+
// plugin-free path needs the resolved file — which is the same file esbuild
|
|
2854
|
+
// would have resolved the specifier to.
|
|
2855
|
+
const nativeEntry = path.isAbsolute(entry) ? entry : resolvedEntry;
|
|
2856
|
+
if (trace) trace.entry = nativeEntry ?? resolvedEntry;
|
|
2857
|
+
const preplan =
|
|
2858
|
+
vendorPreplanEnabled() &&
|
|
2859
|
+
(await vendorPreplanPackageSafe(
|
|
2860
|
+
config,
|
|
2861
|
+
packageName,
|
|
2862
|
+
nativeEntry ?? resolvedEntry,
|
|
2863
|
+
target,
|
|
2864
|
+
conditionTarget,
|
|
2865
|
+
));
|
|
2866
|
+
const native =
|
|
2867
|
+
file && nativeEntry
|
|
2868
|
+
? await buildVendorNative({
|
|
2869
|
+
preplan,
|
|
2870
|
+
config,
|
|
2871
|
+
target,
|
|
2872
|
+
conditionTarget,
|
|
2873
|
+
resolveDir,
|
|
2874
|
+
bailKey: file,
|
|
2875
|
+
packageName,
|
|
2876
|
+
entryPoints: [nativeEntry],
|
|
2877
|
+
splitting: false,
|
|
2878
|
+
ownEntries: new Map([[nativeEntry, file]]),
|
|
2879
|
+
isEntry: () => true,
|
|
2880
|
+
soloFile: file,
|
|
2881
|
+
nested,
|
|
2882
|
+
context: context ?? { nestedExternalFiles: [] },
|
|
2883
|
+
trace,
|
|
2884
|
+
})
|
|
2885
|
+
: undefined;
|
|
2886
|
+
if (native?.[0]) return { code: addCommonJsNamedExports(native[0].text), metafile: undefined };
|
|
2887
|
+
|
|
2888
|
+
let bundled: string;
|
|
2889
|
+
let metafile: Metafile | undefined;
|
|
2890
|
+
const pluginStart = performance.now();
|
|
2891
|
+
const vendorPlugins = profiledVendorPlugins([
|
|
2892
|
+
serverAssetPlugin(config),
|
|
2893
|
+
...getBundlerExtensions().serverEsbuildPlugins(
|
|
2894
|
+
config,
|
|
2895
|
+
vendorPluginOptions(config, target, conditionTarget),
|
|
2896
|
+
),
|
|
2897
|
+
runtimeAliasBuildPlugin(config, target, {
|
|
2898
|
+
bundleRequireAliases: true,
|
|
2899
|
+
// The server vendor bundle IS the react-server layer (client-target
|
|
2900
|
+
// bundles are the SSR pass and keep the full-hooks shim).
|
|
2901
|
+
reactServerLayer: target === 'server',
|
|
2902
|
+
}),
|
|
2903
|
+
externalPackageDependencyPlugin(config, specifier, target, conditionTarget, context, preplan),
|
|
2904
|
+
externalBuiltinBuildPlugin(),
|
|
2905
|
+
]);
|
|
2906
|
+
if (trace) trace.plugins = vendorPlugins.map(plugin => plugin.name);
|
|
2907
|
+
// Bare entry: hand the interop plugin the resolution already computed above.
|
|
2908
|
+
if (!path.isAbsolute(entry) && resolvedEntry) {
|
|
2909
|
+
provideEntryResolution(entry, resolveDir, resolvedEntry);
|
|
2910
|
+
}
|
|
2911
|
+
try {
|
|
2912
|
+
const result = await build({
|
|
2913
|
+
entryPoints: [
|
|
2914
|
+
getBundlerExtensions().serverBundleEntry(
|
|
2915
|
+
entry,
|
|
2916
|
+
resolveDir,
|
|
2917
|
+
path.isAbsolute(entry) ? entry : resolvedEntry,
|
|
2918
|
+
) ?? entry,
|
|
2919
|
+
],
|
|
2920
|
+
absWorkingDir: resolveDir,
|
|
2921
|
+
bundle: true,
|
|
2922
|
+
write: false,
|
|
2923
|
+
format: 'esm',
|
|
2924
|
+
platform: 'neutral',
|
|
2925
|
+
target: 'es2022',
|
|
2926
|
+
conditions: serverBundleConditions(conditionTarget),
|
|
2927
|
+
mainFields: ['module', 'main'],
|
|
2928
|
+
loader: packageJsxLoaders,
|
|
2929
|
+
jsx: 'automatic',
|
|
2930
|
+
jsxImportSource: 'preact',
|
|
2931
|
+
logLevel: 'silent',
|
|
2932
|
+
// The verdict is computed offline from this, after the build settles.
|
|
2933
|
+
metafile: true,
|
|
2934
|
+
...serverDefineOptions(),
|
|
2935
|
+
plugins: vendorPlugins,
|
|
2936
|
+
});
|
|
2937
|
+
const output = result.outputFiles[0];
|
|
2938
|
+
if (!output) throw new Error(`Failed to bundle ${specifier}`);
|
|
2939
|
+
vendorNativeStats.pluginBuilds += 1;
|
|
2940
|
+
vendorNativeStats.pluginBuildMs += performance.now() - pluginStart;
|
|
2941
|
+
bundled = output.text;
|
|
2942
|
+
metafile = result.metafile;
|
|
2943
|
+
if (trace) trace.metafile = result.metafile;
|
|
2944
|
+
} catch (error) {
|
|
2945
|
+
// A subpath the package deliberately withholds from this layer
|
|
2946
|
+
// (`"node": null`) is not a broken dependency: browser-only code reaches it
|
|
2947
|
+
// behind a `typeof window` guard that never runs on the server, and Next's
|
|
2948
|
+
// server compilers drop that branch before their resolver ever sees it.
|
|
2949
|
+
// Vending a module that throws Node's own error keeps the guarded import
|
|
2950
|
+
// out of the build while still failing loudly if anything evaluates it.
|
|
2951
|
+
if (
|
|
2952
|
+
!isPackageSubpathUnexported(
|
|
2953
|
+
config.root,
|
|
2954
|
+
path.join(resolveDir, 'pnext-resolve.ts'),
|
|
2955
|
+
specifier,
|
|
2956
|
+
serverBundleConditions(conditionTarget),
|
|
2957
|
+
)
|
|
2958
|
+
) {
|
|
2959
|
+
throw error;
|
|
2960
|
+
}
|
|
2961
|
+
return { code: unexportedSubpathModule(specifier), metafile: undefined };
|
|
2962
|
+
}
|
|
2963
|
+
return { code: addCommonJsNamedExports(bundled), metafile };
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
function unexportedSubpathModule(specifier: string) {
|
|
2967
|
+
const packageName = packageNameFromSpecifier(specifier);
|
|
2968
|
+
const subpath = packageName && specifier !== packageName ? `.${specifier.slice(packageName.length)}` : '.';
|
|
2969
|
+
const message = `Package subpath '${subpath}' is not defined by "exports" in ${packageName ?? specifier}`;
|
|
2970
|
+
return `throw Object.assign(new Error(${JSON.stringify(message)}), { code: 'ERR_PACKAGE_PATH_NOT_EXPORTED' });\n`;
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
function runtimeAliasBuildPlugin(
|
|
2974
|
+
config?: ResolvedConfig,
|
|
2975
|
+
target: CompatAliasTarget = 'server',
|
|
2976
|
+
options: { bundleRequireAliases?: boolean; reactServerLayer?: boolean } = {},
|
|
2977
|
+
): Plugin {
|
|
2978
|
+
const aliases = config ? coreAliases(config, target) : undefined;
|
|
2979
|
+
// A node_modules package bundled into the RSC (react-server) layer that requires 'react' must observe the
|
|
2980
|
+
// react-server subset - no client-hook dispatcher - exactly like the framework's own react-server modules,
|
|
2981
|
+
// so a package's `'useState' in require('react')` probe reads false on the server. ESM named imports stay
|
|
2982
|
+
// external and resolve at runtime against the full-hooks server shim (kept for pages/api back-compat), so
|
|
2983
|
+
// this narrow require-call override never breaks them.
|
|
2984
|
+
const reactServerLayerAliases =
|
|
2985
|
+
config && options.reactServerLayer
|
|
2986
|
+
? getImportAliasExtensions().reactServerLayerAliases(config)
|
|
2987
|
+
: undefined;
|
|
2988
|
+
return {
|
|
2989
|
+
name: 'pnext-runtime-build-alias',
|
|
2990
|
+
setup(build) {
|
|
2991
|
+
build.onResolve({ filter: /^\.\/preact$/ }, args => {
|
|
2992
|
+
// `./preact` is the compat react shim's private import. It also appears in MIRRORED copies of the
|
|
2993
|
+
// shim (build-cache outputs under node_modules paths) where no sibling preact file exists - those
|
|
2994
|
+
// must resolve to the single compat instance too. Only a REAL sibling ./preact.* opts out.
|
|
2995
|
+
const hasRealSibling = ['.ts', '.tsx', '.js', '.mjs', '.jsx'].some(ext =>
|
|
2996
|
+
existsSync(path.join(args.resolveDir, `preact${ext}`)),
|
|
2997
|
+
);
|
|
2998
|
+
const compatPreact = path.resolve(import.meta.dirname, '..', 'compat', 'react', 'preact.ts');
|
|
2999
|
+
if (hasRealSibling && path.resolve(args.resolveDir, 'preact.ts') !== compatPreact)
|
|
3000
|
+
return undefined;
|
|
3001
|
+
return { path: compatPreact };
|
|
3002
|
+
});
|
|
3003
|
+
build.onResolve({ filter: /^\.\.\/client\/errors\/primitive-throw$/ }, args => {
|
|
3004
|
+
const compatPreact = path.resolve(import.meta.dirname, '..', 'compat', 'react', 'preact.ts');
|
|
3005
|
+
if (path.resolve(args.importer) !== compatPreact) return undefined;
|
|
3006
|
+
return { path: path.resolve(import.meta.dirname, '..', 'compat', 'client', 'errors', 'primitive-throw.ts') };
|
|
3007
|
+
});
|
|
3008
|
+
build.onResolve({ filter: aliasSpecifierFilter }, args => {
|
|
3009
|
+
const resolved = aliases?.[args.path] ?? firstAliasForSpecifier(args.path);
|
|
3010
|
+
if (!resolved) return undefined;
|
|
3011
|
+
const requireTarget = serverRequireAlias(args.path, resolved, args.kind);
|
|
3012
|
+
if (requireTarget !== resolved) return { path: requireTarget };
|
|
3013
|
+
if (options.bundleRequireAliases && args.kind === 'require-call') {
|
|
3014
|
+
const layerTarget = serverRequireAlias(
|
|
3015
|
+
args.path,
|
|
3016
|
+
reactServerLayerAliases?.[args.path] ?? resolved,
|
|
3017
|
+
args.kind,
|
|
3018
|
+
);
|
|
3019
|
+
return { path: path.isAbsolute(layerTarget) ? layerTarget : require.resolve(layerTarget) };
|
|
3020
|
+
}
|
|
3021
|
+
return {
|
|
3022
|
+
path: path.isAbsolute(resolved) ? pathToFileHref(resolved) : resolved,
|
|
3023
|
+
external: true,
|
|
3024
|
+
};
|
|
3025
|
+
});
|
|
3026
|
+
},
|
|
3027
|
+
};
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
function serverRequireAlias(specifier: string, target: string, kind: string | undefined): string {
|
|
3031
|
+
if (kind !== 'require-call') return target;
|
|
3032
|
+
if (specifier === 'next/navigation' && target.endsWith(`${path.sep}navigation.ts`)) {
|
|
3033
|
+
return path.join(path.dirname(target), 'navigation.cjs');
|
|
3034
|
+
}
|
|
3035
|
+
if (specifier === 'next/router' && target.endsWith(`${path.sep}router.ts`)) {
|
|
3036
|
+
return path.join(path.dirname(target), 'router.cjs');
|
|
3037
|
+
}
|
|
3038
|
+
return target;
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
function externalPackageDependencyPlugin(
|
|
3042
|
+
config: ResolvedConfig,
|
|
3043
|
+
entrySpecifier: string,
|
|
3044
|
+
target: CompatAliasTarget,
|
|
3045
|
+
conditionTarget: ServerBundleTarget,
|
|
3046
|
+
context?: { nestedExternalFiles: string[] },
|
|
3047
|
+
preplan = false,
|
|
3048
|
+
): Plugin {
|
|
3049
|
+
const entryPackageName = packageNameFromSpecifier(entrySpecifier);
|
|
3050
|
+
return {
|
|
3051
|
+
name: 'pnext-external-package-dependencies',
|
|
3052
|
+
setup(build) {
|
|
3053
|
+
build.onResolve({ filter: /^[^./].*/ }, async args => {
|
|
3054
|
+
if (!isPackageSpecifier(args.path)) return undefined;
|
|
3055
|
+
if (packageNameFromSpecifier(args.path) === entryPackageName) return undefined;
|
|
3056
|
+
if (preplan && args.kind !== 'require-call') {
|
|
3057
|
+
const planned = await preplanDependencyDecision(
|
|
3058
|
+
config,
|
|
3059
|
+
target,
|
|
3060
|
+
conditionTarget,
|
|
3061
|
+
args.path,
|
|
3062
|
+
args.resolveDir,
|
|
3063
|
+
);
|
|
3064
|
+
if (planned) {
|
|
3065
|
+
vendorPreplanStats.preplanRefs += 1;
|
|
3066
|
+
return planned;
|
|
3067
|
+
}
|
|
3068
|
+
vendorPreplanStats.fallbackRefs += 1;
|
|
3069
|
+
}
|
|
3070
|
+
// Off the slot for the whole decision: it ends in a nested vendor
|
|
3071
|
+
// build, and holding a worker across that is what caps real
|
|
3072
|
+
// parallelism at dependency depth (`PNEXT_VENDOR_YIELD=0` pins the old
|
|
3073
|
+
// behaviour for bisecting). The resolver work in front of it is cheap
|
|
3074
|
+
// and off-slot too, exactly as the plugin-free path's post-pass runs it.
|
|
3075
|
+
const decision = await yieldVendorSlotWhile(() =>
|
|
3076
|
+
externalDependencyDecision(config, target, conditionTarget, args.path, args.resolveDir, args.importer),
|
|
3077
|
+
);
|
|
3078
|
+
if (decision?.vendored && vendorTraceEnabled()) {
|
|
3079
|
+
vendorTraceRow({
|
|
3080
|
+
kind: 'edge',
|
|
3081
|
+
from: entrySpecifier,
|
|
3082
|
+
fromTarget: target,
|
|
3083
|
+
fromCondition: conditionTarget,
|
|
3084
|
+
to: args.path,
|
|
3085
|
+
toResolveDir: args.resolveDir,
|
|
3086
|
+
});
|
|
3087
|
+
}
|
|
3088
|
+
if (decision?.nested && context) context.nestedExternalFiles.push(decision.nested);
|
|
3089
|
+
// eslint-disable-next-line @typescript-eslint/only-throw-error
|
|
3090
|
+
if (decision?.error) throw decision.error;
|
|
3091
|
+
return decision?.result;
|
|
3092
|
+
});
|
|
3093
|
+
},
|
|
3094
|
+
};
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
/** What one external reference resolves to, plus the effects the caller replays. */
|
|
3098
|
+
interface ExternalDependencyDecision {
|
|
3099
|
+
result: OnResolveResult | undefined;
|
|
3100
|
+
/** A transitive dep pinned to a nested copy, for the trace-copy pass. */
|
|
3101
|
+
nested?: string;
|
|
3102
|
+
/** A build failure, re-thrown per caller rather than cached as a value. */
|
|
3103
|
+
error?: unknown;
|
|
3104
|
+
/** Trace-only: the dep was deferred to a vendor bundle of its own. */
|
|
3105
|
+
vendored?: boolean;
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
/**
|
|
3109
|
+
* NOT memoized on (layer, specifier, importing directory), though it looks pure in them: the answer
|
|
3110
|
+
* also depends on what is on disk when it is asked - `externalServerPackageHref` re-checks its
|
|
3111
|
+
* artifact's liveness per demand, and the facade gate re-executes a bundle that may have been
|
|
3112
|
+
* replaced. Caching it lost `app-external` outright, so the seam stays as the plugin chain had it.
|
|
3113
|
+
*/
|
|
3114
|
+
async function externalDependencyDecision(
|
|
3115
|
+
config: ResolvedConfig,
|
|
3116
|
+
target: CompatAliasTarget,
|
|
3117
|
+
conditionTarget: ServerBundleTarget,
|
|
3118
|
+
specifier: string,
|
|
3119
|
+
resolveDir: string,
|
|
3120
|
+
importer: string | undefined,
|
|
3121
|
+
): Promise<ExternalDependencyDecision> {
|
|
3122
|
+
// B7: a serverExternalPackage transitive dep stays external (Node resolves it
|
|
3123
|
+
// from node_modules at runtime) instead of being bundled.
|
|
3124
|
+
const depName = packageNameFromSpecifier(specifier);
|
|
3125
|
+
if (depName && getExternalPackagePolicy().external(depName)) {
|
|
3126
|
+
// Transitive-version correctness: the runtime resolves a bare external from the VENDOR bundle's own
|
|
3127
|
+
// location, which walks up to the hoisted root copy - wrong when the importing package pulls a distinct
|
|
3128
|
+
// nested version (pnpm virtual store, npm nesting). Resolve the dep from the IMPORTING package's real
|
|
3129
|
+
// path here, at build time, and when that is a copy distinct from the hoisted root version, pin the
|
|
3130
|
+
// external to that exact resolved file so the correct version loads at runtime. A hoisted dep stays a
|
|
3131
|
+
// portable bare external. Bundling the nested copy inline instead regresses to a resolve ERROR for the
|
|
3132
|
+
// bundle's other bare externals, so keep it external and only pin the path.
|
|
3133
|
+
const conditions = serverBundleConditions(conditionTarget);
|
|
3134
|
+
const fromImporter = importer
|
|
3135
|
+
? resolveNestedPackageFromImporter(importer, specifier, conditions)
|
|
3136
|
+
: undefined;
|
|
3137
|
+
const fromRoot = resolveVendorPackageSpecifier(
|
|
3138
|
+
config.root,
|
|
3139
|
+
path.join(config.root, 'pnext-resolve.ts'),
|
|
3140
|
+
specifier,
|
|
3141
|
+
conditions,
|
|
3142
|
+
);
|
|
3143
|
+
if (fromImporter && fromImporter !== fromRoot) {
|
|
3144
|
+
return { result: { path: fromImporter, external: true }, nested: fromImporter };
|
|
3145
|
+
}
|
|
3146
|
+
return { result: { path: specifier, external: true } };
|
|
3147
|
+
}
|
|
3148
|
+
const resolvedEntry = resolveVendorPackageSpecifier(
|
|
3149
|
+
config.root,
|
|
3150
|
+
path.join(resolveDir, 'pnext-resolve.ts'),
|
|
3151
|
+
specifier,
|
|
3152
|
+
serverBundleConditions(conditionTarget),
|
|
3153
|
+
);
|
|
3154
|
+
// A CommonJS dependency loses its named exports when externalized (esbuild emits only `default` for a CJS
|
|
3155
|
+
// entry), so it is inlined here instead. `isEsmModuleEntry` first checks CONTENT, not just extension and
|
|
3156
|
+
// `package.json#type` - most "CJS" entries are plain-.js ESM and inline for no reason. What is left gets a
|
|
3157
|
+
// verified named-export facade when it publishes names, and is inlined only when it does not.
|
|
3158
|
+
const commonJs = Boolean(resolvedEntry) && !isEsmModuleEntry(resolvedEntry!);
|
|
3159
|
+
if (commonJs && !(await cjsEntryMayHaveNamedExports(resolvedEntry!))) return { result: undefined };
|
|
3160
|
+
let resolvedPackage: string;
|
|
3161
|
+
const tv = performance.now();
|
|
3162
|
+
try {
|
|
3163
|
+
resolvedPackage = await externalServerPackageHref(
|
|
3164
|
+
config,
|
|
3165
|
+
specifier,
|
|
3166
|
+
target,
|
|
3167
|
+
resolveDir,
|
|
3168
|
+
conditionTarget,
|
|
3169
|
+
// Raised from inside a running build: must not queue behind it.
|
|
3170
|
+
true,
|
|
3171
|
+
);
|
|
3172
|
+
} catch (error) {
|
|
3173
|
+
if (isResolveFailure(error)) return { result: { path: specifier, external: true } };
|
|
3174
|
+
return { result: undefined, error };
|
|
3175
|
+
}
|
|
3176
|
+
if (commonJs) {
|
|
3177
|
+
heavyProfRow({ k: 'dep-build-wait', site: 'plugin', spec: specifier, ms: performance.now() - tv });
|
|
3178
|
+
const tf = performance.now();
|
|
3179
|
+
const facade = await cjsNamedExportFacade(fileURLToPath(resolvedPackage), resolvedPackage);
|
|
3180
|
+
heavyProfRow({ k: 'facade-wait', site: 'plugin', spec: specifier, ms: performance.now() - tf });
|
|
3181
|
+
if (!facade) return { result: undefined };
|
|
3182
|
+
return { result: { path: pathToFileHref(facade), external: true }, vendored: true };
|
|
3183
|
+
}
|
|
3184
|
+
return { result: { path: resolvedPackage, external: true }, vendored: true };
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
/** `PNEXT_VENDOR_YIELD=0` keeps the slot across a nested dependency's build. */
|
|
3188
|
+
function yieldVendorSlotWhile<T>(task: () => Promise<T>) {
|
|
3189
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
3190
|
+
return process.env.PNEXT_VENDOR_YIELD === '0' ? task() : outsideVendorSlot(task);
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
function isResolveFailure(error: unknown) {
|
|
3194
|
+
const details = error instanceof Error ? `${error.message}\n${error.stack ?? ''}` : String(error);
|
|
3195
|
+
return details.includes('Could not resolve');
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
async function profileRuntimeStep<T>(label: string, task: () => Promise<T>) {
|
|
3199
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
3200
|
+
if (!process.env.PNEXT_DEV_PROFILE) return task();
|
|
3201
|
+
const start = performance.now();
|
|
3202
|
+
try {
|
|
3203
|
+
return await task();
|
|
3204
|
+
} finally {
|
|
3205
|
+
console.log(`dev-profile compat ${label} in ${formatDuration(performance.now() - start)}`);
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
function formatDuration(durationMs: number) {
|
|
3210
|
+
const ms = Math.max(0, durationMs);
|
|
3211
|
+
if (ms < 10) return `${ms.toFixed(1)}ms`;
|
|
3212
|
+
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
3213
|
+
return `${(ms / 1000).toFixed(ms < 10000 ? 2 : 1)}s`;
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3216
|
+
function externalBuiltinBuildPlugin(): Plugin {
|
|
3217
|
+
return {
|
|
3218
|
+
name: 'pnext-external-builtins',
|
|
3219
|
+
setup(build) {
|
|
3220
|
+
build.onResolve({ filter: /^[^./].*/ }, args => {
|
|
3221
|
+
const packageName = packageNameFromSpecifier(args.path);
|
|
3222
|
+
if (!packageName || (!builtinModules.includes(packageName) && !isBunBuiltin(packageName))) {
|
|
3223
|
+
return undefined;
|
|
3224
|
+
}
|
|
3225
|
+
return { path: args.path, external: true };
|
|
3226
|
+
});
|
|
3227
|
+
},
|
|
3228
|
+
};
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
function isBunBuiltin(specifier: string) {
|
|
3232
|
+
return specifier === 'bun' || specifier.startsWith('bun:');
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
function serverAssetPlugin(config?: ResolvedConfig): Plugin {
|
|
3236
|
+
return {
|
|
3237
|
+
name: 'pnext-empty-server-assets',
|
|
3238
|
+
setup(build) {
|
|
3239
|
+
build.onResolve({ filter: serverIgnoredAssetFilter }, args => {
|
|
3240
|
+
const resolved = resolveAssetPath(args.path, args.resolveDir);
|
|
3241
|
+
// A configured `turbopack.rules` loader chain for this extension (e.g.
|
|
3242
|
+
// `*.svg`) preempts the generic asset pipeline: defer so the compat
|
|
3243
|
+
// loader-rule plugin's onLoad runs the chain against the real file.
|
|
3244
|
+
if (getAssetExtensions().hasLoaderRuleFor(resolved)) {
|
|
3245
|
+
return undefined;
|
|
3246
|
+
}
|
|
3247
|
+
return { path: resolved, namespace: 'pnext-empty-server-asset' };
|
|
3248
|
+
});
|
|
3249
|
+
build.onLoad({ filter: /.*/, namespace: 'pnext-empty-server-asset' }, async args => ({
|
|
3250
|
+
contents: getCssExtensions().loadCssModuleForClient(args.path) ?? (isCssFile(args.path)
|
|
3251
|
+
? ''
|
|
3252
|
+
: isStaticImageFile(args.path)
|
|
3253
|
+
? await staticImageModuleSource(config, args.path)
|
|
3254
|
+
: 'export default "";'),
|
|
3255
|
+
loader: 'js',
|
|
3256
|
+
}));
|
|
3257
|
+
},
|
|
3258
|
+
};
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
/**
|
|
3262
|
+
* Whether `rewriteServerSource` can possibly change `source`. One regex scan
|
|
3263
|
+
* over the union of the core dynamic tokens and every registered transform's
|
|
3264
|
+
* own sniff tokens (see `withSniff`); most modules trigger nothing and skip the
|
|
3265
|
+
* whole chain.
|
|
3266
|
+
*/
|
|
3267
|
+
export function needsServerSourceRewrite(source: string) {
|
|
3268
|
+
return sourceHasDynamicImport(source) || sourceNeedsServerTransforms(source);
|
|
3269
|
+
}
|
|
3270
|
+
|
|
3271
|
+
export function rewriteServerSource(
|
|
3272
|
+
source: string,
|
|
3273
|
+
file: string,
|
|
3274
|
+
options: { nextFonts?: boolean; root?: string } = {},
|
|
3275
|
+
) {
|
|
3276
|
+
const root = options.root ?? path.dirname(file);
|
|
3277
|
+
const rewritten = sourceHasDynamicImport(source)
|
|
3278
|
+
? rewriteDynamicCallTargets(
|
|
3279
|
+
rewriteLiteralDynamicCalls(source, file),
|
|
3280
|
+
specifier => resolveImport(root, file, specifier),
|
|
3281
|
+
file,
|
|
3282
|
+
)
|
|
3283
|
+
: source;
|
|
3284
|
+
// Compat pre-transforms (next/font/root-params) run before generic server
|
|
3285
|
+
// transforms (use-cache/action tags). Pure-core apps register no transforms.
|
|
3286
|
+
if (options.nextFonts === false) return rewritten;
|
|
3287
|
+
if (!sourceNeedsServerTransforms(rewritten)) return rewritten;
|
|
3288
|
+
return applyServerSourceTransforms(
|
|
3289
|
+
applyServerSourcePreTransforms(rewritten, file, options.root),
|
|
3290
|
+
file,
|
|
3291
|
+
options.root,
|
|
3292
|
+
);
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
export function runtimeImportTarget(specifier: string, format: 'esm' | 'cjs' = 'esm') {
|
|
3296
|
+
if (isRelativeOrAbsoluteSpecifier(specifier)) return specifier;
|
|
3297
|
+
const target = firstAliasForSpecifier(specifier);
|
|
3298
|
+
if (!target) return specifier;
|
|
3299
|
+
return format === 'esm' ? pathToFileHref(target) : target;
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
export function runtimeServerImportTarget(specifier: string) {
|
|
3303
|
+
return runtimeImportTarget(specifier, 'esm');
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
function isRelativeOrAbsoluteSpecifier(specifier: string) {
|
|
3307
|
+
return specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('file:');
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
function isPackageSpecifier(specifier: string) {
|
|
3311
|
+
if (isRelativeOrAbsoluteSpecifier(specifier)) return false;
|
|
3312
|
+
if (specifier.startsWith('#')) return false;
|
|
3313
|
+
if (specifier.startsWith('node:')) return false;
|
|
3314
|
+
if (specifier.includes(':')) return false;
|
|
3315
|
+
const packageName = packageNameFromSpecifier(specifier);
|
|
3316
|
+
return Boolean(packageName && !builtinModules.includes(packageName));
|
|
3317
|
+
}
|
|
3318
|
+
|
|
3319
|
+
function isInside(root: string, file: string) {
|
|
3320
|
+
const relative = path.relative(root, file);
|
|
3321
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
function rootPaths(root: string) {
|
|
3325
|
+
const key = path.resolve(root);
|
|
3326
|
+
const cached = rootPathCache.get(key);
|
|
3327
|
+
if (cached) return cached;
|
|
3328
|
+
const paths = new Set([path.resolve(root)]);
|
|
3329
|
+
try {
|
|
3330
|
+
paths.add(realpathSync.native(root));
|
|
3331
|
+
} catch {
|
|
3332
|
+
// The dev cache root is registered before it exists.
|
|
3333
|
+
}
|
|
3334
|
+
const resolved = [...paths];
|
|
3335
|
+
rootPathCache.set(key, resolved);
|
|
3336
|
+
return resolved;
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
function rootFilter(root: string) {
|
|
3340
|
+
// Base source extensions plus any extra loadable extensions registered by
|
|
3341
|
+
// compat (e.g. mdx/md via pageExtensions). Without these, `.mdx` modules skip
|
|
3342
|
+
// the transform load hook and resolve as raw assets (default export = path).
|
|
3343
|
+
const extras = currentLoadExtras().map(escapeRegex);
|
|
3344
|
+
const exts = ['[jt]sx?', ...extras].join('|');
|
|
3345
|
+
// Exclude `next.config.{js,cjs,mjs,ts}`: it is loaded for its exported value (often CommonJS
|
|
3346
|
+
// `module.exports = ...`), not as part of the app's server module graph. Routing it through the
|
|
3347
|
+
// ES-module source transform drops the CJS exports, silently disabling next.config
|
|
3348
|
+
// rewrites/redirects.
|
|
3349
|
+
// Exclude the vendor tree under the cache root: the `*.dist/` copies copyBrowserReadyEsmDist
|
|
3350
|
+
// publishes are verbatim third-party ESM the copy predicate has already proved needs no server
|
|
3351
|
+
// rewrite, but they are `.js`, so each was read and re-transformed on every boot. Bun loads them
|
|
3352
|
+
// natively instead. Two clauses because the vendor tree is reachable from two registered roots.
|
|
3353
|
+
const vendorGuard = vendorLoadsNatively()
|
|
3354
|
+
? `${root.endsWith(`${path.sep}cache${path.sep}server`) ? '(?!\\/vendor\\/)' : ''}(?!.*\\/cache\\/server\\/vendor\\/)`
|
|
3355
|
+
: '';
|
|
3356
|
+
return new RegExp(
|
|
3357
|
+
`^${escapeRegex(root)}${vendorGuard}(?!.*\\/node_modules\\/)(?!\\/next\\.config\\.(?:js|cjs|mjs|ts)$)(?:/.*)?\\.(?:${exts})$`,
|
|
3358
|
+
);
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3361
|
+
function esbuildLoader(file: string) {
|
|
3362
|
+
if (file.endsWith('.tsx')) return 'tsx';
|
|
3363
|
+
if (file.endsWith('.ts')) return 'ts';
|
|
3364
|
+
// Workspace .js/.jsx may contain JSX (Next allows it); jsx is a js superset.
|
|
3365
|
+
return 'jsx';
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
function hashRoot(value: string) {
|
|
3369
|
+
let hash = 5381;
|
|
3370
|
+
for (const char of value) hash = ((hash << 5) + hash) ^ char.charCodeAt(0);
|
|
3371
|
+
return (hash >>> 0).toString(36);
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
function hashBundleSpecifier(value: string) {
|
|
3375
|
+
return createHash('sha256').update(value).digest('hex').slice(0, 16);
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
// Trace copies of nested transitive externals (a version distinct from the
|
|
3379
|
+
// hoisted root copy) written alongside the vendor bundle as `.js`, so file
|
|
3380
|
+
// tracing / server-bundle scans observe the bundled dependency the way
|
|
3381
|
+
// webpack's bundled transitive would appear. Trace-only: never imported.
|
|
3382
|
+
async function emitNestedExternalTraceCopies(vendorFile: string, sources: readonly string[]) {
|
|
3383
|
+
const dir = path.dirname(vendorFile);
|
|
3384
|
+
for (const source of new Set(sources)) {
|
|
3385
|
+
try {
|
|
3386
|
+
const content = await readFile(source, 'utf8');
|
|
3387
|
+
const out = path.join(dir, `${hashBundleSpecifier(source)}.trace.js`);
|
|
3388
|
+
if (!existsSync(out)) await writeFileAtomic(out, content);
|
|
3389
|
+
} catch {
|
|
3390
|
+
// A source that cannot be read is simply not traced.
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
|
|
3395
|
+
|
|
3396
|
+
const serverIgnoredAssetFilter =
|
|
3397
|
+
/\.(?:css|scss|sass|woff2?|ttf|otf|eot|png|jpe?g|gif|webp|avif|svg|ico|bmp)(?:$|[?#])/;
|
|
3398
|
+
|
|
3399
|
+
function isCssFile(file: string) {
|
|
3400
|
+
return file.endsWith('.css');
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3403
|
+
function isStaticImageFile(file: string) {
|
|
3404
|
+
return /\.(?:png|jpe?g|gif|webp|avif|svg|ico|bmp)(?:$|[?#])/.test(file);
|
|
3405
|
+
}
|
|
3406
|
+
|
|
3407
|
+
function resolveAssetPath(specifier: string, resolveDir: string) {
|
|
3408
|
+
const [sourcePath = '', hash = ''] = specifier.split('#', 2);
|
|
3409
|
+
const resolved = path.isAbsolute(sourcePath) ? sourcePath : path.resolve(resolveDir, sourcePath);
|
|
3410
|
+
return hash ? `${resolved}#${hash}` : resolved;
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3413
|
+
async function staticImageModuleSource(config: ResolvedConfig | undefined, file: string) {
|
|
3414
|
+
const [sourcePath = ''] = file.split('#', 1);
|
|
3415
|
+
const bytes = new Uint8Array(await readFile(sourcePath));
|
|
3416
|
+
const emitted: { relative: string; bytes: Uint8Array }[] = [];
|
|
3417
|
+
const emit = (relative: string, data: Uint8Array) => {
|
|
3418
|
+
emitted.push({ relative, bytes: data });
|
|
3419
|
+
return `/${relative}`;
|
|
3420
|
+
};
|
|
3421
|
+
const compat = await getAssetExtensions().staticAssetModule({ sourcePath, bytes, emit });
|
|
3422
|
+
const source = compat ?? coreStaticAssetModule(sourcePath, bytes, emit);
|
|
3423
|
+
if (config) {
|
|
3424
|
+
for (const asset of emitted) {
|
|
3425
|
+
const target = path.join(config.outPath, 'public', ...asset.relative.split('/'));
|
|
3426
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
3427
|
+
if (!existsSync(target)) await Bun.write(target, asset.bytes);
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
return source;
|
|
3431
|
+
}
|
|
3432
|
+
|
|
3433
|
+
// Core's generic static-asset module: emit the file under a hashed media URL
|
|
3434
|
+
// and export it as the default (a plain URL string) when no compat override is
|
|
3435
|
+
// registered. next/image compat replaces this with a full StaticImageData shape.
|
|
3436
|
+
function coreStaticAssetModule(
|
|
3437
|
+
sourcePath: string,
|
|
3438
|
+
bytes: Uint8Array,
|
|
3439
|
+
emit: (relative: string, bytes: Uint8Array) => string,
|
|
3440
|
+
): string {
|
|
3441
|
+
const ext = path.extname(sourcePath).toLowerCase() || '.bin';
|
|
3442
|
+
const hash = createHash('sha256').update(bytes).digest('hex').slice(0, 8);
|
|
3443
|
+
const base = path.basename(sourcePath, path.extname(sourcePath)).replace(/[^A-Za-z0-9_-]+/g, '-');
|
|
3444
|
+
const relative = getAssetExtensions().staticAssetRelativePath({ sourcePath, hash, base, ext });
|
|
3445
|
+
const src = emit(relative, bytes);
|
|
3446
|
+
return `const src = ${JSON.stringify(src)};\nexport default src;\nexport { src };\n`;
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3449
|
+
function packageNameFromSpecifier(specifier: string) {
|
|
3450
|
+
const normalized = specifier.startsWith('node:') ? specifier.slice(5) : specifier;
|
|
3451
|
+
const parts = normalized.split('/');
|
|
3452
|
+
return normalized.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
|
|
3453
|
+
}
|