@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,1310 @@
|
|
|
1
|
+
import { copyFile, link, mkdir, readFile, rename, rm, symlink, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from 'node:fs';
|
|
3
|
+
import { builtinModules } from 'node:module';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { pathToFileHref } from '../../config';
|
|
7
|
+
import type { ResolvedConfig } from '../../config';
|
|
8
|
+
import { globalCssSources } from '../../css';
|
|
9
|
+
import { getImportAliasExtensions } from '../../extensions';
|
|
10
|
+
import { importSpecifiers } from '../../resolve/scan-facts';
|
|
11
|
+
import { compiledSpecifiersManifestSuffix } from '../../dev/imports';
|
|
12
|
+
import { resolveImport, workspacePackageRoots } from '../../resolve/imports';
|
|
13
|
+
import { findProxyFile, proxyRoutePatterns, type ProxyModule } from '../../proxy';
|
|
14
|
+
import { cacheRoot } from '../named-bin';
|
|
15
|
+
import { startWarmChild, type WarmChild } from './vercel-warm';
|
|
16
|
+
import { listFiles, toPosixPath, writeText } from '../../utils/fs';
|
|
17
|
+
import { createVerboseLogger, type VerboseLogger } from '../../utils/verbose';
|
|
18
|
+
import type { BuildManifest, StaticFileMetadata } from '../../types';
|
|
19
|
+
|
|
20
|
+
// Vercel unpacks a function's `.func` directory here at runtime. Baked
|
|
21
|
+
// absolute paths in the build output are rewritten to this root.
|
|
22
|
+
const RUNTIME_ROOT = '/var/task';
|
|
23
|
+
|
|
24
|
+
// The single catch-all server function: the same `pnext start` pipeline,
|
|
25
|
+
// running on Vercel's Bun runtime (`"bunVersion": "1.x"` in vercel.json).
|
|
26
|
+
const SERVER_FUNCTION = '_pnext';
|
|
27
|
+
|
|
28
|
+
// The platform the function actually runs on, declared once: `architecture`
|
|
29
|
+
// goes into .vc-config.json and the same target selects the native packages
|
|
30
|
+
// shipped inside it, so the two can never drift. Vercel's default instruction
|
|
31
|
+
// set is x86_64 and its function runtime is glibc-based.
|
|
32
|
+
const FUNCTION_PLATFORM = {
|
|
33
|
+
architecture: 'x86_64',
|
|
34
|
+
os: 'linux',
|
|
35
|
+
cpu: 'x64',
|
|
36
|
+
libc: 'glibc',
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
// Directories that only ever hold build-tool output, pruned at *any* depth. Depth matters: pruning
|
|
40
|
+
// only a package's top level ships every nested one. node_modules and .git are here for the same
|
|
41
|
+
// reason - the function resolves through its own traced node_modules, never a nested store.
|
|
42
|
+
const prunedDirNames = [
|
|
43
|
+
'node_modules',
|
|
44
|
+
'.git',
|
|
45
|
+
'.next',
|
|
46
|
+
'.pnext',
|
|
47
|
+
'.turbo',
|
|
48
|
+
'.cache',
|
|
49
|
+
'.vercel',
|
|
50
|
+
'.tmp',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
// Files nothing reads at request time. Sourcemaps only decorate stack traces,
|
|
54
|
+
// and Bun strips types rather than resolving declarations — neither can change
|
|
55
|
+
// what the function serves, and together they are a fifth of a typical closure.
|
|
56
|
+
// Markdown is deliberately absent: an mdx app imports it as a module.
|
|
57
|
+
const droppedFileSuffixes = [
|
|
58
|
+
'.map',
|
|
59
|
+
'.d.ts',
|
|
60
|
+
'.d.mts',
|
|
61
|
+
'.d.cts',
|
|
62
|
+
'.DS_Store',
|
|
63
|
+
// The compile-time specifier sidecars (see traceNodeModulesClosure below):
|
|
64
|
+
// build-time metadata the trace consumes, never read at request time.
|
|
65
|
+
compiledSpecifiersManifestSuffix,
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
/** What the function tree ships, resolved once from config. */
|
|
69
|
+
interface PackRules {
|
|
70
|
+
prunedDirs: Set<string>;
|
|
71
|
+
droppedSuffixes: string[];
|
|
72
|
+
/** Absolute sources exempt from the prune list — the app's own build output. */
|
|
73
|
+
keepPaths: Set<string>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Test seam: the packing suite builds one app both ways and diffs the trees,
|
|
77
|
+
// so it needs the unpruned closure on demand. Never off for a real deployment.
|
|
78
|
+
let packPruningEnabled = true;
|
|
79
|
+
|
|
80
|
+
/** @internal Test-only. Returns a restore function. */
|
|
81
|
+
export function setPackPruningEnabled(enabled: boolean) {
|
|
82
|
+
const previous = packPruningEnabled;
|
|
83
|
+
packPruningEnabled = enabled;
|
|
84
|
+
return () => {
|
|
85
|
+
packPruningEnabled = previous;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function packRules(config: ResolvedConfig): PackRules {
|
|
90
|
+
if (!packPruningEnabled) {
|
|
91
|
+
return {
|
|
92
|
+
// The two the function could never resolve through stay pruned even
|
|
93
|
+
// here: a nested store or object database is not a packing choice.
|
|
94
|
+
prunedDirs: new Set(['node_modules', '.git']),
|
|
95
|
+
droppedSuffixes: [],
|
|
96
|
+
keepPaths: new Set([path.resolve(config.outPath)]),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const keep = new Set(config.adapter?.keep ?? []);
|
|
100
|
+
const extra = config.adapter?.exclude ?? [];
|
|
101
|
+
const isSuffix = (entry: string) => entry.startsWith('.') && !prunedDirNames.includes(entry);
|
|
102
|
+
return {
|
|
103
|
+
prunedDirs: new Set(
|
|
104
|
+
[...prunedDirNames, ...extra.filter(entry => !isSuffix(entry))].filter(
|
|
105
|
+
name => !keep.has(name),
|
|
106
|
+
),
|
|
107
|
+
),
|
|
108
|
+
droppedSuffixes: [...droppedFileSuffixes, ...extra.filter(isSuffix)].filter(
|
|
109
|
+
suffix => !keep.has(suffix),
|
|
110
|
+
),
|
|
111
|
+
// The app's outDir shares its name with the prune list; it holds the
|
|
112
|
+
// compiled module cache the function serves from and must survive.
|
|
113
|
+
keepPaths: new Set([path.resolve(config.outPath)]),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function shipsDir(pack: PackRules, name: string, source: string) {
|
|
118
|
+
return !pack.prunedDirs.has(name) || pack.keepPaths.has(path.resolve(source));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function shipsFile(pack: PackRules, name: string) {
|
|
122
|
+
return !pack.droppedSuffixes.some(suffix => name.endsWith(suffix));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface VercelConfig {
|
|
126
|
+
version: 3;
|
|
127
|
+
routes?: (
|
|
128
|
+
| { src: string; dest?: string; headers?: Record<string, string>; continue?: boolean }
|
|
129
|
+
| { handle: 'filesystem' }
|
|
130
|
+
)[];
|
|
131
|
+
overrides?: Record<string, { path?: string; contentType?: string }>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function writeVercelOutput(
|
|
135
|
+
config: ResolvedConfig,
|
|
136
|
+
manifest: BuildManifest,
|
|
137
|
+
options: { verbose?: boolean; warm?: WarmChild } = {},
|
|
138
|
+
) {
|
|
139
|
+
const log = createVerboseLogger(options.verbose ?? false, 'vercel');
|
|
140
|
+
const outputPath = path.join(config.root, '.vercel', 'output');
|
|
141
|
+
const staticFiles = manifest.staticFiles ?? {};
|
|
142
|
+
// Warming runs in its own process (see ./vercel-warm) and normally started
|
|
143
|
+
// with the build; a caller that skipped that only loses the overlap.
|
|
144
|
+
const warm = options.warm ?? startWarmChild(config);
|
|
145
|
+
await log.step('prepare output directory', () => emptyDir(outputPath));
|
|
146
|
+
|
|
147
|
+
await log.step('copy static files', () =>
|
|
148
|
+
copyStaticFiles(
|
|
149
|
+
path.join(config.outPath, 'public'),
|
|
150
|
+
path.join(outputPath, 'static'),
|
|
151
|
+
relative => !staticFiles[relative] || canServeStaticOnVercel(staticFiles[relative]),
|
|
152
|
+
),
|
|
153
|
+
);
|
|
154
|
+
const overrides = await staticOverrides(path.join(config.outPath, 'public'), staticFiles);
|
|
155
|
+
|
|
156
|
+
// The compiled set is already final once the child reaches its handler
|
|
157
|
+
// phase, so `onCompiled` resolves this ahead of full completion and
|
|
158
|
+
// writeServerFunction starts tracing while the child's handler imports
|
|
159
|
+
// still run. `finish` itself always fires `onCompiled` before it resolves,
|
|
160
|
+
// so this promise never hangs on it.
|
|
161
|
+
let resolveWarmedModules: (modules: string[]) => void;
|
|
162
|
+
const warmedModulesEarly = new Promise<string[]>(resolve => {
|
|
163
|
+
resolveWarmedModules = resolve;
|
|
164
|
+
});
|
|
165
|
+
const warmSettled = warm.finish(log, modules => resolveWarmedModules(modules));
|
|
166
|
+
// Nothing awaits it until writeServerFunction's copy step; park the
|
|
167
|
+
// rejection so it never surfaces as unhandled in between (it never actually
|
|
168
|
+
// rejects — warmWithRestarts catches everything itself — but the gap
|
|
169
|
+
// between here and that await is otherwise unguarded).
|
|
170
|
+
warmSettled.catch(() => undefined);
|
|
171
|
+
const warmedModules = await log.step('warm module cache (compiled)', () => warmedModulesEarly);
|
|
172
|
+
|
|
173
|
+
const functionPath = path.join(outputPath, 'functions', `${SERVER_FUNCTION}.func`);
|
|
174
|
+
await writeServerFunction(config, manifest, functionPath, warmedModules, warmSettled, log);
|
|
175
|
+
|
|
176
|
+
const routes: NonNullable<VercelConfig['routes']> = [
|
|
177
|
+
// Chunk and font filenames are content-hashed; serve them immutable.
|
|
178
|
+
{
|
|
179
|
+
src: '^/assets/(chunks|fonts)/.*',
|
|
180
|
+
headers: { 'cache-control': 'public, max-age=31536000, immutable' },
|
|
181
|
+
continue: true,
|
|
182
|
+
},
|
|
183
|
+
// Proxy-matched paths go to the server function before the CDN filesystem
|
|
184
|
+
// check — `pnext start` runs the proxy ahead of static files too.
|
|
185
|
+
...(await proxyRoutes(config)),
|
|
186
|
+
{ handle: 'filesystem' },
|
|
187
|
+
{ src: '^/.*$', dest: `/${SERVER_FUNCTION}` },
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
const vercelConfig: VercelConfig = {
|
|
191
|
+
version: 3,
|
|
192
|
+
routes,
|
|
193
|
+
...(Object.keys(overrides).length > 0 ? { overrides } : {}),
|
|
194
|
+
};
|
|
195
|
+
await writeText(
|
|
196
|
+
path.join(outputPath, 'config.json'),
|
|
197
|
+
`${JSON.stringify(vercelConfig, null, 2)}\n`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Unlinking the previous run's output (tens of thousands of hardlinks) must not block the build:
|
|
202
|
+
// rename it aside - which is what actually makes it invisible - and let a detached child do the
|
|
203
|
+
// unlinking. Nothing downstream depends on the bytes being gone, and anything an interrupted run
|
|
204
|
+
// leaves behind is swept on the next build.
|
|
205
|
+
async function emptyDir(dir: string) {
|
|
206
|
+
const parent = path.dirname(dir);
|
|
207
|
+
await mkdir(parent, { recursive: true });
|
|
208
|
+
const staleMarker = `${path.basename(dir)}.stale-`;
|
|
209
|
+
if (existsSync(dir)) {
|
|
210
|
+
await rename(
|
|
211
|
+
dir,
|
|
212
|
+
path.join(parent, `${staleMarker}${process.pid.toString(36)}-${Date.now().toString(36)}`),
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
const stale = readdirSync(parent)
|
|
216
|
+
.filter(entry => entry.startsWith(staleMarker))
|
|
217
|
+
.map(entry => path.join(parent, entry));
|
|
218
|
+
await mkdir(dir, { recursive: true });
|
|
219
|
+
if (stale.length > 0) {
|
|
220
|
+
Bun.spawn(['rm', '-rf', ...stale], { stdout: 'ignore', stderr: 'ignore' });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function proxyRoutes(config: ResolvedConfig) {
|
|
225
|
+
const proxyFile = findProxyFile(config);
|
|
226
|
+
if (!proxyFile) return [];
|
|
227
|
+
const proxyModule = (await import(pathToFileHref(proxyFile))) as ProxyModule;
|
|
228
|
+
return proxyRoutePatterns(proxyModule.config).map(src => ({
|
|
229
|
+
src,
|
|
230
|
+
dest: `/${SERVER_FUNCTION}`,
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function writeServerFunction(
|
|
235
|
+
config: ResolvedConfig,
|
|
236
|
+
manifest: BuildManifest,
|
|
237
|
+
functionPath: string,
|
|
238
|
+
warmedModules: string[],
|
|
239
|
+
// Resolves once the warm child fully exits (handlers included). The trace below only needs the
|
|
240
|
+
// compiled set `warmedModules` already carries - it reasons from static specifiers, never from
|
|
241
|
+
// handler execution - so it starts immediately; only the tree copy further down, which must ship
|
|
242
|
+
// whatever handler imports wrote to the build cache, waits on this.
|
|
243
|
+
warmSettled: Promise<unknown>,
|
|
244
|
+
log: VerboseLogger,
|
|
245
|
+
) {
|
|
246
|
+
const workspaceRoot = config.workspaceRoot;
|
|
247
|
+
await mkdir(functionPath, { recursive: true });
|
|
248
|
+
|
|
249
|
+
const pack = packRules(config);
|
|
250
|
+
const framework = await resolveFrameworkPackage(workspaceRoot);
|
|
251
|
+
const { root: pnextRoot, inWorkspace: pnextInWorkspace, targetRel: pnextTargetRel } = framework;
|
|
252
|
+
const closure = await traceNodeModulesClosure(
|
|
253
|
+
config,
|
|
254
|
+
manifest,
|
|
255
|
+
functionPath,
|
|
256
|
+
framework,
|
|
257
|
+
warmedModules,
|
|
258
|
+
pack,
|
|
259
|
+
log,
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
const replicaPathFor = (file: string) => {
|
|
263
|
+
if (isInsideDir(pnextRoot, file)) {
|
|
264
|
+
return path.join(functionPath, pnextTargetRel, path.relative(pnextRoot, file));
|
|
265
|
+
}
|
|
266
|
+
if (isInsideDir(workspaceRoot, file)) {
|
|
267
|
+
return path.join(functionPath, path.relative(workspaceRoot, file));
|
|
268
|
+
}
|
|
269
|
+
return undefined;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// Replicate the workspace layout the build ran in: app source (the renderer reads convention files
|
|
273
|
+
// and global css imports from it at request time), the build output, and the workspace packages the
|
|
274
|
+
// server can actually reach. Unrelated workspace trees never ship.
|
|
275
|
+
const appRel = path.relative(workspaceRoot, config.root);
|
|
276
|
+
const packageRoots = workspacePackageRoots(workspaceRoot).map(root => path.resolve(root));
|
|
277
|
+
const neededRoots = new Set<string>();
|
|
278
|
+
for (const file of closure.tracedFiles) {
|
|
279
|
+
const root = packageRoots.find(candidate => isInsideDir(candidate, file));
|
|
280
|
+
if (root) neededRoots.add(root);
|
|
281
|
+
}
|
|
282
|
+
for (const relative of closure.workspacePackages) {
|
|
283
|
+
neededRoots.add(path.resolve(workspaceRoot, relative));
|
|
284
|
+
}
|
|
285
|
+
// The renderer re-resolves the root layout's global css imports from source
|
|
286
|
+
// at request time; the packages holding those css files must be present.
|
|
287
|
+
for (const file of globalCssSources(config)) {
|
|
288
|
+
const root = packageRoots.find(candidate => isInsideDir(candidate, file));
|
|
289
|
+
if (root) neededRoots.add(root);
|
|
290
|
+
}
|
|
291
|
+
// Tracing sees what the *server* loaded, which is not everything the shipped source can still ask
|
|
292
|
+
// for: the app's own sources travel with the function and a compat compile at request time resolves
|
|
293
|
+
// their imports for real. A workspace package one of them names but no traced module reached would
|
|
294
|
+
// not be here, so close over the declared workspace dependencies too.
|
|
295
|
+
addWorkspaceDependencies(neededRoots, packageRoots, [path.resolve(config.root), ...neededRoots]);
|
|
296
|
+
neededRoots.delete(path.resolve(config.root));
|
|
297
|
+
neededRoots.delete(pnextRoot);
|
|
298
|
+
|
|
299
|
+
// The trace above never needed the handler phase, but the copy below ships
|
|
300
|
+
// config.outPath verbatim — handler imports can still be writing vendor
|
|
301
|
+
// bundles into it. Most of this wait is already gone by the time the trace
|
|
302
|
+
// and the roots above finish; whatever remains is genuine handler-phase work.
|
|
303
|
+
await log.step('await handler warm-up', () => warmSettled);
|
|
304
|
+
|
|
305
|
+
// Every tree lands in its own subdirectory and the copies are io-bound, so
|
|
306
|
+
// the whole replica goes out as one concurrent step instead of tree by tree.
|
|
307
|
+
const roots = neededRoots.size + 2;
|
|
308
|
+
await log.step(`copy function tree (${closure.packageCount} packages, ${roots} roots)`, () =>
|
|
309
|
+
Promise.all([
|
|
310
|
+
closure.copy(),
|
|
311
|
+
// `public` is served by the CDN, never from the function; the rest are
|
|
312
|
+
// named again because the app root is the one tree whose build output
|
|
313
|
+
// ships, so it cannot rely on the depth-wise prune alone.
|
|
314
|
+
copyTree(config.root, path.join(functionPath, appRel), pack, [
|
|
315
|
+
'public',
|
|
316
|
+
'.vercel',
|
|
317
|
+
'.next',
|
|
318
|
+
'.turbo',
|
|
319
|
+
]),
|
|
320
|
+
copyPackageTree(pnextRoot, path.join(functionPath, pnextTargetRel), pack),
|
|
321
|
+
...[...neededRoots].map(packageRoot =>
|
|
322
|
+
copyPackageTree(
|
|
323
|
+
packageRoot,
|
|
324
|
+
path.join(functionPath, path.relative(workspaceRoot, packageRoot)),
|
|
325
|
+
pack,
|
|
326
|
+
),
|
|
327
|
+
),
|
|
328
|
+
...['package.json', 'tsconfig.json', 'bunfig.toml']
|
|
329
|
+
.map(name => path.join(workspaceRoot, name))
|
|
330
|
+
.filter(file => existsSync(file))
|
|
331
|
+
.map(file => copyFile(file, path.join(functionPath, path.basename(file)))),
|
|
332
|
+
]),
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
// Baked absolute paths (compiled cache imports, manifest file paths) point
|
|
336
|
+
// at the build machine's workspace; rewrite them to the runtime root.
|
|
337
|
+
await log.step('rewrite baked paths', () =>
|
|
338
|
+
rewriteBakedPaths(path.join(functionPath, appRel, path.relative(config.root, config.outPath)), [
|
|
339
|
+
...(pnextInWorkspace
|
|
340
|
+
? []
|
|
341
|
+
: [[pnextRoot, toPosixPath(path.join(RUNTIME_ROOT, pnextTargetRel))] as const]),
|
|
342
|
+
[workspaceRoot, RUNTIME_ROOT] as const,
|
|
343
|
+
]),
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// Bun's runtime onResolve plugins never see bare specifiers, so the compat aliases the dev pipeline
|
|
347
|
+
// applies during compilation cannot be applied at runtime for raw-loaded sources (proxy, handlers).
|
|
348
|
+
// Bun does honor tsconfig paths for bare imports, so map the aliased specifiers to their shims in
|
|
349
|
+
// every shipped tsconfig.
|
|
350
|
+
await log.step('inject compat tsconfig paths', () =>
|
|
351
|
+
injectCompatPaths(config, functionPath, replicaPathFor),
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
const startEntry = toPosixPath(path.join(pnextTargetRel, 'src/cli/start.ts'));
|
|
355
|
+
await writeText(
|
|
356
|
+
path.join(functionPath, 'index.mjs'),
|
|
357
|
+
`import path from 'node:path';
|
|
358
|
+
import { createRequestHandler } from ${JSON.stringify(`./${startEntry}`)};
|
|
359
|
+
|
|
360
|
+
const handlerPromise = createRequestHandler({
|
|
361
|
+
root: path.join(import.meta.dirname, ${JSON.stringify(appRel)}),
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
async function handler(request) {
|
|
365
|
+
return (await handlerPromise)(request);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Callable for the Node-style launcher, \`fetch\` for the Bun runtime.
|
|
369
|
+
export default Object.assign(handler, { fetch: handler });
|
|
370
|
+
`,
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
const maxDuration = manifest.routes.reduce<number | undefined>(
|
|
374
|
+
(max, route) => (route.maxDuration ? Math.max(max ?? 0, route.maxDuration) : max),
|
|
375
|
+
undefined,
|
|
376
|
+
);
|
|
377
|
+
await writeText(
|
|
378
|
+
path.join(functionPath, '.vc-config.json'),
|
|
379
|
+
`${JSON.stringify(
|
|
380
|
+
{
|
|
381
|
+
// The Bun function runtime (project-level `bunVersion` does not apply
|
|
382
|
+
// to prebuilt Build Output API functions). Note: bun1.x functions have
|
|
383
|
+
// a 150 MiB uncompressed size limit vs 250 MiB for nodejs.
|
|
384
|
+
runtime: 'bun1.x',
|
|
385
|
+
handler: 'index.mjs',
|
|
386
|
+
launcherType: 'Nodejs',
|
|
387
|
+
architecture: FUNCTION_PLATFORM.architecture,
|
|
388
|
+
// The default export takes a web `Request` and returns a `Response`.
|
|
389
|
+
useWebApi: true,
|
|
390
|
+
supportsResponseStreaming: true,
|
|
391
|
+
...(maxDuration ? { maxDuration } : {}),
|
|
392
|
+
},
|
|
393
|
+
null,
|
|
394
|
+
2,
|
|
395
|
+
)}\n`,
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Grow `roots` with every workspace package transitively depended on by one of `from`. A workspace
|
|
401
|
+
* dependency is one whose name maps to a package root in this workspace - the workspace: protocol is
|
|
402
|
+
* the usual spelling, but a pinned version resolved through the workspace counts the same.
|
|
403
|
+
*/
|
|
404
|
+
function addWorkspaceDependencies(roots: Set<string>, packageRoots: string[], from: string[]) {
|
|
405
|
+
const byName = new Map<string, string>();
|
|
406
|
+
for (const root of packageRoots) {
|
|
407
|
+
const name = packageManifest(root)?.name;
|
|
408
|
+
if (name && !byName.has(name)) byName.set(name, root);
|
|
409
|
+
}
|
|
410
|
+
const queue = [...from];
|
|
411
|
+
const seen = new Set(queue);
|
|
412
|
+
while (queue.length > 0) {
|
|
413
|
+
const manifest = packageManifest(queue.pop()!);
|
|
414
|
+
if (!manifest) continue;
|
|
415
|
+
for (const field of ['dependencies', 'optionalDependencies'] as const) {
|
|
416
|
+
for (const name of Object.keys(manifest[field] ?? {})) {
|
|
417
|
+
const root = byName.get(name);
|
|
418
|
+
if (!root || seen.has(root)) continue;
|
|
419
|
+
seen.add(root);
|
|
420
|
+
roots.add(root);
|
|
421
|
+
queue.push(root);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
interface PackageManifest {
|
|
428
|
+
name?: string;
|
|
429
|
+
dependencies?: Record<string, string>;
|
|
430
|
+
optionalDependencies?: Record<string, string>;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const manifests = new Map<string, PackageManifest | undefined>();
|
|
434
|
+
|
|
435
|
+
function packageManifest(root: string): PackageManifest | undefined {
|
|
436
|
+
const cached = manifests.get(root);
|
|
437
|
+
if (cached !== undefined || manifests.has(root)) return cached;
|
|
438
|
+
let parsed: PackageManifest | undefined;
|
|
439
|
+
try {
|
|
440
|
+
parsed = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8')) as PackageManifest;
|
|
441
|
+
} catch {
|
|
442
|
+
parsed = undefined; // not a package, or unreadable
|
|
443
|
+
}
|
|
444
|
+
manifests.set(root, parsed);
|
|
445
|
+
return parsed;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Clones a tree with hardlinks instead of byte copies - the output is packaging-only, so sharing
|
|
449
|
+
// inodes is safe as long as nothing writes in place (rewriteBakedPaths unlinks before writing).
|
|
450
|
+
// Exclusions apply to direct children only; node_modules and .git are pruned at any depth. Symlinks
|
|
451
|
+
// are dereferenced so the replica is self-contained; the visited set breaks symlink cycles.
|
|
452
|
+
async function copyTree(
|
|
453
|
+
from: string,
|
|
454
|
+
to: string,
|
|
455
|
+
pack: PackRules,
|
|
456
|
+
excludeNames: string[] = [],
|
|
457
|
+
visited = new Set<string>([realDir(from) ?? path.resolve(from)]),
|
|
458
|
+
) {
|
|
459
|
+
const exclude = new Set(excludeNames);
|
|
460
|
+
const clone = directoryCloneEnabled ? await directoryCloner() : null;
|
|
461
|
+
// Nothing to skip at this level: the whole tree clones in one syscall.
|
|
462
|
+
if (exclude.size === 0 && !existsSync(to)) {
|
|
463
|
+
await mkdir(path.dirname(to), { recursive: true });
|
|
464
|
+
if (clone?.(from, to)) return normalizeClone(to, pack, visited);
|
|
465
|
+
}
|
|
466
|
+
// The build output is still live while the walk runs — typegen rewrites
|
|
467
|
+
// `.pnext/types` under it — so a directory can vanish between the parent's
|
|
468
|
+
// readdir and this one. It was never part of the shipped closure; skip it.
|
|
469
|
+
const entries = readDirEntries(from);
|
|
470
|
+
if (!entries) return;
|
|
471
|
+
await mkdir(to, { recursive: true });
|
|
472
|
+
await Promise.all(
|
|
473
|
+
entries.map(async entry => {
|
|
474
|
+
const source = path.join(from, entry.name);
|
|
475
|
+
const target = path.join(to, entry.name);
|
|
476
|
+
// Matched by name before type: a symlinked `node_modules` must be pruned
|
|
477
|
+
// as the directory it points at, not followed.
|
|
478
|
+
if (exclude.has(entry.name) || !shipsDir(pack, entry.name, source)) return;
|
|
479
|
+
if (entry.isSymbolicLink()) {
|
|
480
|
+
const link = resolveLinkTarget(source);
|
|
481
|
+
if (!link) return;
|
|
482
|
+
if (link.isDirectory) {
|
|
483
|
+
if (visited.has(link.path)) return;
|
|
484
|
+
visited.add(link.path);
|
|
485
|
+
return copyTree(link.path, target, pack, [], visited);
|
|
486
|
+
}
|
|
487
|
+
return shipsFile(pack, entry.name) ? linkOrCopyFile(link.path, target) : undefined;
|
|
488
|
+
}
|
|
489
|
+
// One syscall for the whole subtree beats one per file — the excluded
|
|
490
|
+
// names are stripped from the copy afterwards.
|
|
491
|
+
if (entry.isDirectory() && clone?.(source, target)) {
|
|
492
|
+
return normalizeClone(target, pack, visited);
|
|
493
|
+
}
|
|
494
|
+
if (entry.isDirectory()) return copyTree(source, target, pack, [], visited);
|
|
495
|
+
if (entry.isFile() && shipsFile(pack, entry.name)) return linkOrCopyFile(source, target);
|
|
496
|
+
}),
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Directory entries, or undefined if the directory vanished under the walk. */
|
|
501
|
+
function readDirEntries(dir: string) {
|
|
502
|
+
try {
|
|
503
|
+
return readdirSync(dir, { withFileTypes: true });
|
|
504
|
+
} catch (error) {
|
|
505
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
|
|
506
|
+
throw error;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Directory-level copy-on-write clone, when the platform has one: macOS `clonefile` copies a whole
|
|
512
|
+
* hierarchy in a single call and, like a hardlink, shares the underlying blocks. Resolved once and
|
|
513
|
+
* never retried - no cloner just means the hardlink walk below does the work.
|
|
514
|
+
*/
|
|
515
|
+
// Test seam: the equivalence suite builds the same app both ways and diffs the trees.
|
|
516
|
+
let directoryCloneEnabled = true;
|
|
517
|
+
|
|
518
|
+
/** @internal Test-only. Returns a restore function. */
|
|
519
|
+
export function setDirectoryCloneEnabled(enabled: boolean) {
|
|
520
|
+
const previous = directoryCloneEnabled;
|
|
521
|
+
directoryCloneEnabled = enabled;
|
|
522
|
+
return () => {
|
|
523
|
+
directoryCloneEnabled = previous;
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
let cloner: ((from: string, to: string) => boolean) | null | undefined;
|
|
528
|
+
async function directoryCloner() {
|
|
529
|
+
if (cloner !== undefined) return cloner;
|
|
530
|
+
cloner = null;
|
|
531
|
+
if (process.platform === 'darwin') {
|
|
532
|
+
try {
|
|
533
|
+
const { dlopen } = await import('bun:ffi');
|
|
534
|
+
const { symbols } = dlopen('libSystem.B.dylib', {
|
|
535
|
+
clonefile: { args: ['cstring', 'cstring', 'i32'], returns: 'i32' },
|
|
536
|
+
});
|
|
537
|
+
cloner = (from, to) =>
|
|
538
|
+
symbols.clonefile(Buffer.from(`${from}\0`), Buffer.from(`${to}\0`), 0) === 0;
|
|
539
|
+
} catch {
|
|
540
|
+
// no clonefile here; the per-file walk stays correct, just slower
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return cloner;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* A clone is verbatim, so it still holds what the walk would have skipped:
|
|
548
|
+
* drop the pruned entries and dereference inner symlinks, which would otherwise
|
|
549
|
+
* point outside the function at runtime. Mirrors the walk's rules exactly, so
|
|
550
|
+
* both paths emit the same tree. Reading directory entries costs nothing next
|
|
551
|
+
* to the per-file copies this replaces.
|
|
552
|
+
*/
|
|
553
|
+
async function normalizeClone(dir: string, pack: PackRules, visited: Set<string>) {
|
|
554
|
+
const stack = [dir];
|
|
555
|
+
const pending: Promise<unknown>[] = [];
|
|
556
|
+
while (stack.length > 0) {
|
|
557
|
+
const current = stack.pop()!;
|
|
558
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
559
|
+
const full = path.join(current, entry.name);
|
|
560
|
+
// Matched by name before type, as the walk does: the pages-compat shim
|
|
561
|
+
// links `node_modules` at the app root, and following it would pull the
|
|
562
|
+
// whole store into the function. The clone is already inside the
|
|
563
|
+
// function, so the walk's keep-path exemption cannot apply here.
|
|
564
|
+
if (pack.prunedDirs.has(entry.name)) {
|
|
565
|
+
rmSync(full, { recursive: true, force: true });
|
|
566
|
+
} else if (entry.isSymbolicLink()) {
|
|
567
|
+
const link = resolveLinkTarget(full);
|
|
568
|
+
rmSync(full, { force: true });
|
|
569
|
+
if (!link) continue;
|
|
570
|
+
if (link.isDirectory) {
|
|
571
|
+
if (visited.has(link.path)) continue;
|
|
572
|
+
visited.add(link.path);
|
|
573
|
+
pending.push(copyTree(link.path, full, pack, [], visited));
|
|
574
|
+
} else if (shipsFile(pack, entry.name)) {
|
|
575
|
+
pending.push(linkOrCopyFile(link.path, full));
|
|
576
|
+
}
|
|
577
|
+
} else if (entry.isDirectory()) {
|
|
578
|
+
stack.push(full);
|
|
579
|
+
} else if (!shipsFile(pack, entry.name)) {
|
|
580
|
+
rmSync(full, { force: true });
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
await Promise.all(pending);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
interface FrameworkPackage {
|
|
588
|
+
root: string;
|
|
589
|
+
packageName: string;
|
|
590
|
+
/** Declared runtime dependencies — the only packages it can load eagerly. */
|
|
591
|
+
dependencies: string[];
|
|
592
|
+
/** Lazily loaded feature deps (og, mdx, sass, image optimization). */
|
|
593
|
+
optionalDependencies: string[];
|
|
594
|
+
inWorkspace: boolean;
|
|
595
|
+
/** Where the framework ships inside the function, relative to its root. */
|
|
596
|
+
targetRel: string;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// The framework itself ships with the function. Inside a workspace it keeps its
|
|
600
|
+
// workspace-relative location so baked paths line up; when consumed from
|
|
601
|
+
// outside the workspace (e.g. installed from a registry) it lands in the
|
|
602
|
+
// function's node_modules under its package name.
|
|
603
|
+
async function resolveFrameworkPackage(workspaceRoot: string): Promise<FrameworkPackage> {
|
|
604
|
+
const root = path.resolve(import.meta.dirname, '../../..');
|
|
605
|
+
const packageJson = await readPackageJson(root);
|
|
606
|
+
const packageName = packageJson?.name ?? 'pnext';
|
|
607
|
+
const inWorkspace = isInsideDir(workspaceRoot, root) && !root.includes('node_modules');
|
|
608
|
+
return {
|
|
609
|
+
root,
|
|
610
|
+
packageName,
|
|
611
|
+
// Never devDependencies: their tooling (typescript, eslint, …) is
|
|
612
|
+
// reachable from build-time sources only and must not enter the function.
|
|
613
|
+
dependencies: Object.keys(packageJson?.dependencies ?? {}),
|
|
614
|
+
optionalDependencies: Object.keys(packageJson?.optionalDependencies ?? {}),
|
|
615
|
+
inWorkspace,
|
|
616
|
+
targetRel: inWorkspace
|
|
617
|
+
? path.relative(workspaceRoot, root)
|
|
618
|
+
: path.join('node_modules', packageName),
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Build-time directories a package that publishes its whole tree still keeps.
|
|
623
|
+
const packageCopyExcludes = [
|
|
624
|
+
'node_modules',
|
|
625
|
+
'.cache',
|
|
626
|
+
'.vercel',
|
|
627
|
+
'.next',
|
|
628
|
+
'.pnext',
|
|
629
|
+
'.turbo',
|
|
630
|
+
'test',
|
|
631
|
+
'tests',
|
|
632
|
+
'__tests__',
|
|
633
|
+
];
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Copies a workspace package the way npm would publish it: a `files` list is the package's own
|
|
637
|
+
* statement of what it ships, so it beats guessing at build-time directory names. Glob entries fall
|
|
638
|
+
* back to the exclude list - matching npm's glob semantics is not worth it for a packaging copy.
|
|
639
|
+
*/
|
|
640
|
+
async function copyPackageTree(from: string, to: string, pack: PackRules) {
|
|
641
|
+
const packageJson = await readPackageJson(from);
|
|
642
|
+
const files = packageJson?.files;
|
|
643
|
+
if (!files?.length || files.some(entry => /[*?[\]{}!]/.test(entry))) {
|
|
644
|
+
return copyTree(from, to, pack, packageCopyExcludes);
|
|
645
|
+
}
|
|
646
|
+
await mkdir(to, { recursive: true });
|
|
647
|
+
// npm always publishes these two regardless of `files`; the runtime resolves
|
|
648
|
+
// through both (tsconfig for Bun's `paths`/jsx settings). Listed entries need
|
|
649
|
+
// no exclude list — `files` already said what ships — which also lets each
|
|
650
|
+
// directory clone whole.
|
|
651
|
+
await Promise.all(
|
|
652
|
+
[...files, 'package.json', 'tsconfig.json'].map(async name => {
|
|
653
|
+
const source = path.join(from, name);
|
|
654
|
+
const stats = statSync(source, { throwIfNoEntry: false });
|
|
655
|
+
if (!stats) return;
|
|
656
|
+
if (stats.isDirectory()) return copyTree(source, path.join(to, name), pack);
|
|
657
|
+
if (!shipsFile(pack, name)) return;
|
|
658
|
+
return linkOrCopyFile(source, path.join(to, name));
|
|
659
|
+
}),
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
async function readPackageJson(dir: string) {
|
|
664
|
+
const file = path.join(dir, 'package.json');
|
|
665
|
+
if (!existsSync(file)) return undefined;
|
|
666
|
+
return JSON.parse(await readFile(file, 'utf8')) as {
|
|
667
|
+
name?: string;
|
|
668
|
+
version?: string;
|
|
669
|
+
files?: string[];
|
|
670
|
+
os?: string[];
|
|
671
|
+
cpu?: string[];
|
|
672
|
+
libc?: string[];
|
|
673
|
+
dependencies?: Record<string, string>;
|
|
674
|
+
optionalDependencies?: Record<string, string>;
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// The closure hardlinks ~13k files: ~2.9s of a ~3.1s build. Test seam so suites
|
|
679
|
+
// asserting only config shape can skip it. Never off for a real deployment.
|
|
680
|
+
let dependencyClosureEnabled = true;
|
|
681
|
+
|
|
682
|
+
/** @internal Test-only. Returns a restore function. */
|
|
683
|
+
export function setDependencyClosureEnabled(enabled: boolean) {
|
|
684
|
+
const previous = dependencyClosureEnabled;
|
|
685
|
+
dependencyClosureEnabled = enabled;
|
|
686
|
+
return () => {
|
|
687
|
+
dependencyClosureEnabled = previous;
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function skipDependencyClosure() {
|
|
692
|
+
return !dependencyClosureEnabled;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
async function linkOrCopyFile(from: string, to: string) {
|
|
696
|
+
try {
|
|
697
|
+
await link(from, to);
|
|
698
|
+
} catch {
|
|
699
|
+
// Same race as the walk above: a source that vanished mid-copy was never
|
|
700
|
+
// part of the closure, and nothing else here is worth failing the build for.
|
|
701
|
+
await copyFile(from, to).catch(error => {
|
|
702
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Where a symlink really points, and whether that is a directory. Dangling
|
|
708
|
+
// links are ordinary in a real node_modules (removed package, unmet optional
|
|
709
|
+
// dep, interrupted install), so an unreadable target is skipped rather than
|
|
710
|
+
// failing the build.
|
|
711
|
+
function resolveLinkTarget(link: string) {
|
|
712
|
+
const resolved = realDir(link) ?? realFile(link);
|
|
713
|
+
if (!resolved) return undefined;
|
|
714
|
+
try {
|
|
715
|
+
return { path: resolved, isDirectory: statSync(resolved).isDirectory() };
|
|
716
|
+
} catch {
|
|
717
|
+
return undefined;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function realFile(file: string) {
|
|
722
|
+
try {
|
|
723
|
+
return realpathSync(file);
|
|
724
|
+
} catch {
|
|
725
|
+
return undefined;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function isInsideDir(root: string, file: string) {
|
|
730
|
+
const relative = path.relative(root, file);
|
|
731
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async function injectCompatPaths(
|
|
735
|
+
config: ResolvedConfig,
|
|
736
|
+
functionPath: string,
|
|
737
|
+
replicaPathFor: (file: string) => string | undefined,
|
|
738
|
+
) {
|
|
739
|
+
const aliasTargets = Object.entries(getImportAliasExtensions().aliases(config, 'server'))
|
|
740
|
+
.filter(([, target]) => path.isAbsolute(target) && !target.includes('node_modules'))
|
|
741
|
+
.map(([specifier, target]) => [specifier, replicaPathFor(target)] as const)
|
|
742
|
+
.filter((entry): entry is readonly [string, string] => Boolean(entry[1]));
|
|
743
|
+
if (aliasTargets.length === 0) return;
|
|
744
|
+
|
|
745
|
+
for (const file of walkTsconfigFiles(functionPath)) {
|
|
746
|
+
let parsed: { compilerOptions?: { baseUrl?: string; paths?: Record<string, string[]> } };
|
|
747
|
+
try {
|
|
748
|
+
const source = stripJsonComments(await readFile(file, 'utf8')).replace(/,\s*([}\]])/g, '$1');
|
|
749
|
+
parsed = JSON.parse(source) as typeof parsed;
|
|
750
|
+
} catch (error) {
|
|
751
|
+
console.warn(`vercel adapter: could not update ${file}:`, error);
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
const dir = path.dirname(file);
|
|
755
|
+
const compilerOptions = (parsed.compilerOptions ??= {});
|
|
756
|
+
const paths = (compilerOptions.paths ??= {});
|
|
757
|
+
for (const [specifier, target] of aliasTargets) {
|
|
758
|
+
if (paths[specifier]) continue;
|
|
759
|
+
const relative = toPosixPath(path.relative(dir, target));
|
|
760
|
+
paths[specifier] = [relative.startsWith('.') ? relative : `./${relative}`];
|
|
761
|
+
}
|
|
762
|
+
compilerOptions.baseUrl ??= '.';
|
|
763
|
+
// The replica shares inodes with the original tsconfig; never write
|
|
764
|
+
// through the hardlink.
|
|
765
|
+
await rm(file);
|
|
766
|
+
await writeFile(file, `${JSON.stringify(parsed, null, 2)}\n`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Minimal JSONC support for tsconfig files: strips // and /* */ comments
|
|
771
|
+
// outside strings (a "$schema": "https://..." value must survive).
|
|
772
|
+
function stripJsonComments(source: string) {
|
|
773
|
+
let result = '';
|
|
774
|
+
let inString = false;
|
|
775
|
+
for (let index = 0; index < source.length; index++) {
|
|
776
|
+
const char = source[index]!;
|
|
777
|
+
if (inString) {
|
|
778
|
+
result += char;
|
|
779
|
+
if (char === '\\') {
|
|
780
|
+
result += source[++index] ?? '';
|
|
781
|
+
} else if (char === '"') {
|
|
782
|
+
inString = false;
|
|
783
|
+
}
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
if (char === '"') {
|
|
787
|
+
inString = true;
|
|
788
|
+
result += char;
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
if (char === '/' && source[index + 1] === '/') {
|
|
792
|
+
while (index < source.length && source[index] !== '\n') index++;
|
|
793
|
+
result += '\n';
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
if (char === '/' && source[index + 1] === '*') {
|
|
797
|
+
index += 2;
|
|
798
|
+
while (index < source.length && !(source[index] === '*' && source[index + 1] === '/'))
|
|
799
|
+
index++;
|
|
800
|
+
index++;
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
result += char;
|
|
804
|
+
}
|
|
805
|
+
return result;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const bakedPathFileFilter = /\.(?:m?js|cjs|jsx|tsx?|json|html|css)$/;
|
|
809
|
+
|
|
810
|
+
// The build output is thousands of small files and only a few hundred carry a
|
|
811
|
+
// baked path, so the pass is io-latency-bound: run it in bounded-concurrency
|
|
812
|
+
// batches instead of one file at a time.
|
|
813
|
+
const BAKED_PATH_CONCURRENCY = 64;
|
|
814
|
+
|
|
815
|
+
async function rewriteBakedPaths(
|
|
816
|
+
dir: string,
|
|
817
|
+
replacements: readonly (readonly [string, string])[],
|
|
818
|
+
) {
|
|
819
|
+
if (!existsSync(dir)) return;
|
|
820
|
+
const files = (await listFiles(dir)).filter(file => bakedPathFileFilter.test(file));
|
|
821
|
+
for (let i = 0; i < files.length; i += BAKED_PATH_CONCURRENCY) {
|
|
822
|
+
await Promise.all(
|
|
823
|
+
files.slice(i, i + BAKED_PATH_CONCURRENCY).map(async file => {
|
|
824
|
+
const source = await readFile(file, 'utf8');
|
|
825
|
+
let rewritten = source;
|
|
826
|
+
for (const [from, to] of replacements) {
|
|
827
|
+
if (rewritten.includes(from)) rewritten = rewritten.replaceAll(from, to);
|
|
828
|
+
}
|
|
829
|
+
if (rewritten === source) return;
|
|
830
|
+
// The replica is hardlinked to the real build output; unlink before
|
|
831
|
+
// writing so the rewrite never reaches the original through a shared
|
|
832
|
+
// inode.
|
|
833
|
+
await rm(file);
|
|
834
|
+
await writeFile(file, rewritten);
|
|
835
|
+
}),
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* An artifact's import specifiers, preferring the sidecar the compile step recorded over re-reading
|
|
842
|
+
* and re-parsing the file. Falls back to the parse whenever the sidecar is missing or unreadable - a
|
|
843
|
+
* stale cache hit from a prior dev build never wrote one - so the traced closure is identical either
|
|
844
|
+
* way, just cheaper when it is there.
|
|
845
|
+
*/
|
|
846
|
+
async function artifactSpecifiers(file: string): Promise<string[] | null> {
|
|
847
|
+
const sidecar = await readFile(`${file}${compiledSpecifiersManifestSuffix}`, 'utf8').catch(
|
|
848
|
+
() => null,
|
|
849
|
+
);
|
|
850
|
+
if (sidecar !== null) {
|
|
851
|
+
try {
|
|
852
|
+
const parsed: unknown = JSON.parse(sidecar);
|
|
853
|
+
if (Array.isArray(parsed) && parsed.every(entry => typeof entry === 'string')) {
|
|
854
|
+
return parsed;
|
|
855
|
+
}
|
|
856
|
+
} catch {
|
|
857
|
+
// malformed sidecar (partial write, foreign tool) — fall through to parse
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
const source = await readFile(file, 'utf8').catch(() => null);
|
|
861
|
+
return source === null ? null : importSpecifiers(source, file);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Ship only the node_modules packages the server can actually reach at request time: bare imports
|
|
865
|
+
// traced from the modules the runtime loads, plus the framework's declared runtime dependencies,
|
|
866
|
+
// expanded through their dependency closures. Nothing reachable only from build-time code enters the
|
|
867
|
+
// function - the full monorepo node_modules does not fit Vercel's size limit. Returns the closure
|
|
868
|
+
// plus the deferred copy so the caller can order the io.
|
|
869
|
+
async function traceNodeModulesClosure(
|
|
870
|
+
config: ResolvedConfig,
|
|
871
|
+
manifest: BuildManifest,
|
|
872
|
+
functionPath: string,
|
|
873
|
+
framework: FrameworkPackage,
|
|
874
|
+
warmedModules: string[],
|
|
875
|
+
pack: PackRules,
|
|
876
|
+
log: VerboseLogger,
|
|
877
|
+
) {
|
|
878
|
+
const workspaceRoot = config.workspaceRoot;
|
|
879
|
+
const specifiers = new Set<string>();
|
|
880
|
+
const tracedFiles = new Set<string>();
|
|
881
|
+
// Compat-aliased specifiers whose target lives in the framework source
|
|
882
|
+
// (react, next/*) never resolve from node_modules at runtime — shipping
|
|
883
|
+
// them would drag in the full next/react trees. Aliases that point into
|
|
884
|
+
// node_modules (the pinned preact family) still need their packages.
|
|
885
|
+
const frameworkAliasedSpecifiers = new Set(
|
|
886
|
+
Object.entries(getImportAliasExtensions().aliases(config, 'server'))
|
|
887
|
+
.filter(([, target]) => path.isAbsolute(target) && !target.includes('node_modules'))
|
|
888
|
+
.map(([specifier]) => specifier),
|
|
889
|
+
);
|
|
890
|
+
await log.step('trace runtime imports', async () => {
|
|
891
|
+
// Only code Bun imports raw at request time needs real node_modules: route handlers, the proxy,
|
|
892
|
+
// the config chain, and the compiled modules the warm step just wrote. Pages resolve through that
|
|
893
|
+
// compiled cache, whose externals are vendored, so their dependencies never load raw.
|
|
894
|
+
const queue: string[] = [];
|
|
895
|
+
const seen = tracedFiles;
|
|
896
|
+
const enqueue = (file: string | undefined) => {
|
|
897
|
+
if (!file) return;
|
|
898
|
+
const resolved = path.resolve(file);
|
|
899
|
+
if (seen.has(resolved) || !existsSync(resolved)) return;
|
|
900
|
+
seen.add(resolved);
|
|
901
|
+
queue.push(resolved);
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
for (const route of manifest.routes) {
|
|
905
|
+
if (route.kind === 'handler') enqueue(route.file);
|
|
906
|
+
}
|
|
907
|
+
enqueue(findProxyFile(config) ?? undefined);
|
|
908
|
+
enqueue(path.join(config.root, 'pnext.config.ts'));
|
|
909
|
+
enqueue(path.join(config.root, 'next.config.js'));
|
|
910
|
+
for (const file of warmedModules) enqueue(file);
|
|
911
|
+
|
|
912
|
+
const visit = async (file: string) => {
|
|
913
|
+
if (!scriptFilePattern.test(file)) return;
|
|
914
|
+
const found = await artifactSpecifiers(file);
|
|
915
|
+
if (found === null) return;
|
|
916
|
+
for (const specifier of found) {
|
|
917
|
+
if (frameworkAliasedSpecifiers.has(specifier)) continue;
|
|
918
|
+
// Compiled modules import each other — and whatever the compiler left
|
|
919
|
+
// external — by absolute href. Follow the ones inside the build output
|
|
920
|
+
// and ship the packages the rest point into.
|
|
921
|
+
const absolute = absolutePathFromSpecifier(specifier);
|
|
922
|
+
if (absolute) {
|
|
923
|
+
const name = packageNameFromPath(absolute);
|
|
924
|
+
if (name) specifiers.add(name);
|
|
925
|
+
else if (isInsideDir(config.outPath, absolute)) enqueue(absolute);
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
if (/^(?:node:|bun$|bun:|data:)/.test(specifier)) continue;
|
|
929
|
+
// resolveImport covers relative paths, tsconfig aliases, package imports and workspace
|
|
930
|
+
// packages - workspace source gets traced further. Bare package specifiers are also recorded
|
|
931
|
+
// so the closure ships (or workspace-links) their node_modules entries; workspace packages
|
|
932
|
+
// need the link even when their source is traced.
|
|
933
|
+
const resolved = resolveImport(config.root, file, specifier);
|
|
934
|
+
if (
|
|
935
|
+
resolved &&
|
|
936
|
+
isInsideDir(workspaceRoot, resolved) &&
|
|
937
|
+
!resolved.includes('node_modules')
|
|
938
|
+
) {
|
|
939
|
+
enqueue(resolved);
|
|
940
|
+
}
|
|
941
|
+
const name = packageNameFromSpecifier(specifier);
|
|
942
|
+
if (name) specifiers.add(name);
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
|
|
946
|
+
// A level's reads are independent, so the walk advances a level at a time, bounded so a wide
|
|
947
|
+
// graph cannot exhaust the fd table.
|
|
948
|
+
const READ_BATCH = 64;
|
|
949
|
+
for (let frontier = queue.splice(0); frontier.length > 0; frontier = queue.splice(0)) {
|
|
950
|
+
for (let index = 0; index < frontier.length; index += READ_BATCH) {
|
|
951
|
+
await Promise.all(frontier.slice(index, index + READ_BATCH).map(visit));
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
log.log(`traced ${seen.size} runtime modules`);
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
const packageDirs = new Map<string, string>();
|
|
958
|
+
const workspaceLinks = new Map<string, string>();
|
|
959
|
+
// Native bindings resolved here are built for the build host; the function
|
|
960
|
+
// needs the ones for its own platform, fetched below.
|
|
961
|
+
const hostNatives = new Map<string, { dir: string; owner?: PackageVersion }>();
|
|
962
|
+
// The framework ships whole, so what it can load at runtime is exactly what it declares as a
|
|
963
|
+
// runtime dependency - resolved from its own root, which is not the app's when it comes from a
|
|
964
|
+
// registry. Its optional deps back compat features that only load when the app uses them, so they
|
|
965
|
+
// ship only where the app declares them too.
|
|
966
|
+
const appDependencies = await declaredDependencies(config.root);
|
|
967
|
+
const queue: { name: string; from: string; owner?: PackageVersion }[] = [
|
|
968
|
+
...framework.dependencies.map(name => ({ name, from: framework.root })),
|
|
969
|
+
...framework.optionalDependencies
|
|
970
|
+
.filter(name => appDependencies.has(name))
|
|
971
|
+
.map(name => ({ name, from: framework.root })),
|
|
972
|
+
...[...specifiers].map(name => ({ name, from: workspaceRoot })),
|
|
973
|
+
];
|
|
974
|
+
const seen = new Set<string>();
|
|
975
|
+
|
|
976
|
+
while (queue.length > 0) {
|
|
977
|
+
const { name, from, owner } = queue.shift()!;
|
|
978
|
+
if (seen.has(name)) continue;
|
|
979
|
+
seen.add(name);
|
|
980
|
+
// The framework is copied to its own location by the caller; copying it
|
|
981
|
+
// again under its package name collides with that (and with the workspace
|
|
982
|
+
// link it needs when it lives in the workspace).
|
|
983
|
+
if (name === framework.packageName) {
|
|
984
|
+
if (framework.inWorkspace) workspaceLinks.set(name, framework.targetRel);
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
const dir = resolvePackageDir(name, [from, workspaceRoot, config.root]);
|
|
988
|
+
if (!dir) continue;
|
|
989
|
+
|
|
990
|
+
// Workspace packages ship at their workspace-relative path; link them so
|
|
991
|
+
// bare imports resolve to the same real path the compiled cache uses.
|
|
992
|
+
const workspaceRelative = path.relative(workspaceRoot, dir);
|
|
993
|
+
if (!workspaceRelative.startsWith('..') && !workspaceRelative.startsWith('node_modules')) {
|
|
994
|
+
workspaceLinks.set(name, workspaceRelative);
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
const packageJson = await readPackageJson(dir);
|
|
999
|
+
// `os`/`cpu` are npm's own declaration that a package is platform-bound;
|
|
1000
|
+
// one that already fits the function's platform ships as-is.
|
|
1001
|
+
const platformBound = Boolean(packageJson?.os ?? packageJson?.cpu);
|
|
1002
|
+
if (packageJson && platformBound && !matchesFunctionPlatform(packageJson)) {
|
|
1003
|
+
hostNatives.set(name, { dir, owner });
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
packageDirs.set(name, dir);
|
|
1007
|
+
if (!packageJson) continue;
|
|
1008
|
+
const self = { name, version: packageJson.version ?? '' };
|
|
1009
|
+
// Peer dependencies are deliberately not expanded — they fan out to whole
|
|
1010
|
+
// ecosystems (react, next, ...) that are only shipped when something
|
|
1011
|
+
// actually imports them.
|
|
1012
|
+
for (const dependency of [
|
|
1013
|
+
...Object.keys(packageJson.dependencies ?? {}),
|
|
1014
|
+
...Object.keys(packageJson.optionalDependencies ?? {}),
|
|
1015
|
+
]) {
|
|
1016
|
+
queue.push({ name: dependency, from: dir, owner: self });
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
if (hostNatives.size > 0 && !skipDependencyClosure()) {
|
|
1021
|
+
const targets = await log.step(
|
|
1022
|
+
`resolve ${FUNCTION_PLATFORM.os}-${FUNCTION_PLATFORM.cpu} natives (${hostNatives.size} packages)`,
|
|
1023
|
+
() => platformNativePackages(hostNatives, log),
|
|
1024
|
+
);
|
|
1025
|
+
for (const [name, dir] of targets) packageDirs.set(name, dir);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const copy = async () => {
|
|
1029
|
+
const entries = skipDependencyClosure() ? [] : [...packageDirs];
|
|
1030
|
+
const batchSize = 16;
|
|
1031
|
+
for (let index = 0; index < entries.length; index += batchSize) {
|
|
1032
|
+
await Promise.all(
|
|
1033
|
+
entries.slice(index, index + batchSize).map(async ([name, dir]) => {
|
|
1034
|
+
const target = path.join(functionPath, 'node_modules', name);
|
|
1035
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
1036
|
+
await copyTree(dir, target, pack);
|
|
1037
|
+
}),
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
for (const [name, workspaceRelative] of workspaceLinks) {
|
|
1041
|
+
const linkPath = path.join(functionPath, 'node_modules', name);
|
|
1042
|
+
await mkdir(path.dirname(linkPath), { recursive: true });
|
|
1043
|
+
const target = path.relative(
|
|
1044
|
+
path.dirname(linkPath),
|
|
1045
|
+
path.join(functionPath, workspaceRelative),
|
|
1046
|
+
);
|
|
1047
|
+
if (!existsSync(linkPath)) await symlink(target, linkPath, 'dir');
|
|
1048
|
+
}
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
return {
|
|
1052
|
+
tracedFiles,
|
|
1053
|
+
workspacePackages: [...workspaceLinks.values()],
|
|
1054
|
+
packageCount: packageDirs.size,
|
|
1055
|
+
copy,
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
interface PackageVersion {
|
|
1060
|
+
name: string;
|
|
1061
|
+
version: string;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* Native bindings for the function's platform, not the build host's. The host install only ever
|
|
1066
|
+
* carries its own platform's optional deps, so the right builds have to come from the registry: one
|
|
1067
|
+
* cross-platform install per distinct set of owning packages, cached by content in the user cache dir
|
|
1068
|
+
* (the build output is wiped every build). Falls back to the host copies when the install is
|
|
1069
|
+
* unavailable (offline CI, private registry), so packaging never hard-fails on a network hiccup.
|
|
1070
|
+
*/
|
|
1071
|
+
async function platformNativePackages(
|
|
1072
|
+
hostNatives: Map<string, { dir: string; owner?: PackageVersion }>,
|
|
1073
|
+
log: VerboseLogger,
|
|
1074
|
+
) {
|
|
1075
|
+
// Install the package that *owns* the binding: only it lists every
|
|
1076
|
+
// platform's build in its optional deps.
|
|
1077
|
+
const owners = new Map<string, string>();
|
|
1078
|
+
for (const [name, { dir, owner }] of hostNatives) {
|
|
1079
|
+
const spec = owner ?? { name, version: (await readPackageJson(dir))?.version ?? '' };
|
|
1080
|
+
if (spec.version) owners.set(spec.name, spec.version);
|
|
1081
|
+
}
|
|
1082
|
+
const specs = [...owners].map(([name, version]) => `${name}@${version}`).sort();
|
|
1083
|
+
const fallback = () => new Map([...hostNatives].map(([name, { dir }]) => [name, dir]));
|
|
1084
|
+
if (specs.length === 0) return fallback();
|
|
1085
|
+
|
|
1086
|
+
try {
|
|
1087
|
+
const installed = await crossPlatformInstall(specs);
|
|
1088
|
+
const matches = new Map<string, string>();
|
|
1089
|
+
for (const [name, dir] of installed) {
|
|
1090
|
+
const packageJson = await readPackageJson(dir);
|
|
1091
|
+
if (packageJson && matchesFunctionPlatform(packageJson)) matches.set(name, dir);
|
|
1092
|
+
}
|
|
1093
|
+
if (matches.size === 0) {
|
|
1094
|
+
throw new Error(`no ${FUNCTION_PLATFORM.os} builds in ${specs.join(' ')}`);
|
|
1095
|
+
}
|
|
1096
|
+
return matches;
|
|
1097
|
+
} catch (error) {
|
|
1098
|
+
console.warn(
|
|
1099
|
+
`vercel adapter: could not fetch ${FUNCTION_PLATFORM.os}-${FUNCTION_PLATFORM.cpu} native packages; ` +
|
|
1100
|
+
`the function will carry this machine's builds and fail at runtime:`,
|
|
1101
|
+
error,
|
|
1102
|
+
);
|
|
1103
|
+
log.log('falling back to host native packages');
|
|
1104
|
+
return fallback();
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function matchesFunctionPlatform(packageJson: {
|
|
1109
|
+
os?: string[];
|
|
1110
|
+
cpu?: string[];
|
|
1111
|
+
libc?: string[];
|
|
1112
|
+
}) {
|
|
1113
|
+
// npm's field semantics: an all-negated list excludes, anything else is an
|
|
1114
|
+
// allow-list (`os: ["!win32"]` still fits linux).
|
|
1115
|
+
const matches = (declared: string[] | undefined, value: string) => {
|
|
1116
|
+
if (!declared?.length) return true;
|
|
1117
|
+
if (declared.every(entry => entry.startsWith('!'))) return !declared.includes(`!${value}`);
|
|
1118
|
+
return declared.includes(value);
|
|
1119
|
+
};
|
|
1120
|
+
return (
|
|
1121
|
+
matches(packageJson.os, FUNCTION_PLATFORM.os) &&
|
|
1122
|
+
matches(packageJson.cpu, FUNCTION_PLATFORM.cpu) &&
|
|
1123
|
+
// Both a glibc and a musl build exist for the same os/cpu; Vercel's
|
|
1124
|
+
// function runtime is glibc.
|
|
1125
|
+
matches(packageJson.libc, FUNCTION_PLATFORM.libc)
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
/**
|
|
1130
|
+
* Installs `specs` for the function's platform into a content-addressed cache
|
|
1131
|
+
* directory and returns every package that landed. Bun's `--os`/`--cpu` pick
|
|
1132
|
+
* the target's optional deps instead of this machine's; its own download cache
|
|
1133
|
+
* makes a repeat install ~0.1s, and the directory cache makes it free.
|
|
1134
|
+
*/
|
|
1135
|
+
async function crossPlatformInstall(specs: string[]) {
|
|
1136
|
+
const key = Bun.hash
|
|
1137
|
+
.xxHash3(`${FUNCTION_PLATFORM.os}-${FUNCTION_PLATFORM.cpu}\0${specs.join('\0')}`)
|
|
1138
|
+
.toString(16);
|
|
1139
|
+
const dir = path.join(cacheRoot(), 'vercel-natives', key);
|
|
1140
|
+
const modules = path.join(dir, 'node_modules');
|
|
1141
|
+
if (!existsSync(modules)) {
|
|
1142
|
+
// Land under a temp name and rename: a concurrent build must never read a
|
|
1143
|
+
// half-installed tree.
|
|
1144
|
+
const staging = `${dir}.${process.pid.toString(36)}`;
|
|
1145
|
+
await rm(staging, { recursive: true, force: true });
|
|
1146
|
+
await mkdir(staging, { recursive: true });
|
|
1147
|
+
await writeText(path.join(staging, 'package.json'), '{"name":"pnext-natives"}\n');
|
|
1148
|
+
const install = Bun.spawn(
|
|
1149
|
+
[
|
|
1150
|
+
'bun',
|
|
1151
|
+
'add',
|
|
1152
|
+
'--no-save',
|
|
1153
|
+
'--ignore-scripts',
|
|
1154
|
+
'--silent',
|
|
1155
|
+
`--os=${FUNCTION_PLATFORM.os}`,
|
|
1156
|
+
`--cpu=${FUNCTION_PLATFORM.cpu}`,
|
|
1157
|
+
...specs,
|
|
1158
|
+
],
|
|
1159
|
+
{ cwd: staging, stdout: 'ignore', stderr: 'pipe' },
|
|
1160
|
+
);
|
|
1161
|
+
if ((await install.exited) !== 0) {
|
|
1162
|
+
const stderr = await new Response(install.stderr).text();
|
|
1163
|
+
await rm(staging, { recursive: true, force: true });
|
|
1164
|
+
throw new Error(stderr.trim() || 'bun add failed');
|
|
1165
|
+
}
|
|
1166
|
+
await rm(dir, { recursive: true, force: true });
|
|
1167
|
+
await rename(staging, dir);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
const installed = new Map<string, string>();
|
|
1171
|
+
for (const entry of readdirSync(modules)) {
|
|
1172
|
+
if (entry.startsWith('.')) continue;
|
|
1173
|
+
if (!entry.startsWith('@')) {
|
|
1174
|
+
installed.set(entry, path.join(modules, entry));
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
for (const scoped of readdirSync(path.join(modules, entry))) {
|
|
1178
|
+
installed.set(`${entry}/${scoped}`, path.join(modules, entry, scoped));
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
return installed;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const scriptFilePattern = /\.(?:m?js|cjs|jsx|tsx?)$/;
|
|
1185
|
+
|
|
1186
|
+
// Directories the shipped tsconfigs can never live in; pruned during the walk —
|
|
1187
|
+
// a naive recursive listing would crawl the function's own node_modules.
|
|
1188
|
+
const skippedScanDirs = new Set(['node_modules', '.git', '.next', '.pnext', '.turbo', '.vercel']);
|
|
1189
|
+
|
|
1190
|
+
function* walkTsconfigFiles(root: string): Generator<string> {
|
|
1191
|
+
const stack = [root];
|
|
1192
|
+
while (stack.length > 0) {
|
|
1193
|
+
const dir = stack.pop()!;
|
|
1194
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1195
|
+
const full = path.join(dir, entry.name);
|
|
1196
|
+
if (entry.isDirectory()) {
|
|
1197
|
+
if (!skippedScanDirs.has(entry.name)) stack.push(full);
|
|
1198
|
+
} else if (entry.isFile() && entry.name === 'tsconfig.json') {
|
|
1199
|
+
yield full;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/** Every dependency name an app's own package.json declares. */
|
|
1206
|
+
async function declaredDependencies(root: string) {
|
|
1207
|
+
const packageJson = (await readPackageJson(root)) as
|
|
1208
|
+
| Record<string, Record<string, string> | undefined>
|
|
1209
|
+
| undefined;
|
|
1210
|
+
return new Set(
|
|
1211
|
+
['dependencies', 'optionalDependencies', 'devDependencies'].flatMap(field =>
|
|
1212
|
+
Object.keys(packageJson?.[field] ?? {}),
|
|
1213
|
+
),
|
|
1214
|
+
);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/** Absolute path behind an `import`, whether written as a path or a file URL. */
|
|
1218
|
+
function absolutePathFromSpecifier(specifier: string) {
|
|
1219
|
+
if (specifier.startsWith('file://')) return fileURLToPath(specifier);
|
|
1220
|
+
return path.isAbsolute(specifier) ? specifier : undefined;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/** The package an absolute path belongs to, if it points inside node_modules. */
|
|
1224
|
+
function packageNameFromPath(file: string) {
|
|
1225
|
+
const parts = toPosixPath(file).split('/node_modules/');
|
|
1226
|
+
const tail = parts.at(-1);
|
|
1227
|
+
if (parts.length < 2 || !tail) return undefined;
|
|
1228
|
+
const segments = tail.split('/');
|
|
1229
|
+
return segments[0]?.startsWith('@') ? segments.slice(0, 2).join('/') : segments[0];
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
function packageNameFromSpecifier(specifier: string) {
|
|
1233
|
+
if (/^[./#]|^file:|^node:|^data:/.test(specifier)) return undefined;
|
|
1234
|
+
if (specifier === 'bun' || specifier.startsWith('bun:')) return undefined;
|
|
1235
|
+
if (specifier.includes(':')) return undefined;
|
|
1236
|
+
// Scoped names need a real scope — `@/env`-style tsconfig aliases are not
|
|
1237
|
+
// packages.
|
|
1238
|
+
if (specifier.startsWith('@') && !/^@[^/]+\/[^/]+/.test(specifier)) return undefined;
|
|
1239
|
+
const parts = specifier.split('/');
|
|
1240
|
+
const name = specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
|
|
1241
|
+
if (!name || builtinModules.includes(name)) return undefined;
|
|
1242
|
+
return name;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function resolvePackageDir(name: string, fromDirs: string[]) {
|
|
1246
|
+
for (const from of fromDirs) {
|
|
1247
|
+
let dir = path.resolve(from);
|
|
1248
|
+
while (true) {
|
|
1249
|
+
const candidate = path.join(dir, 'node_modules', name);
|
|
1250
|
+
if (existsSync(path.join(candidate, 'package.json'))) return realDir(candidate);
|
|
1251
|
+
const parent = path.dirname(dir);
|
|
1252
|
+
if (parent === dir) break;
|
|
1253
|
+
dir = parent;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
return undefined;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function realDir(dir: string) {
|
|
1260
|
+
try {
|
|
1261
|
+
return statSync(dir).isDirectory() ? realpathSync(dir) : undefined;
|
|
1262
|
+
} catch {
|
|
1263
|
+
return undefined;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
async function copyStaticFiles(
|
|
1268
|
+
from: string,
|
|
1269
|
+
to: string,
|
|
1270
|
+
shouldCopy: (relative: string) => boolean = () => true,
|
|
1271
|
+
) {
|
|
1272
|
+
const files = (await listFiles(from)).filter(file =>
|
|
1273
|
+
shouldCopy(toPosixPath(path.relative(from, file))),
|
|
1274
|
+
);
|
|
1275
|
+
// One mkdir per directory, not per file, then link them all at once.
|
|
1276
|
+
const directories = new Set(
|
|
1277
|
+
files.map(file => path.dirname(path.join(to, path.relative(from, file)))),
|
|
1278
|
+
);
|
|
1279
|
+
await Promise.all([...directories].map(dir => mkdir(dir, { recursive: true })));
|
|
1280
|
+
await Promise.all(
|
|
1281
|
+
files.map(file => linkOrCopyFile(file, path.join(to, path.relative(from, file)))),
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
async function staticOverrides(
|
|
1286
|
+
publicPath: string,
|
|
1287
|
+
staticFiles: Record<string, StaticFileMetadata>,
|
|
1288
|
+
) {
|
|
1289
|
+
const overrides: NonNullable<VercelConfig['overrides']> = {};
|
|
1290
|
+
for (const file of await listFiles(publicPath)) {
|
|
1291
|
+
const relative = toPosixPath(path.relative(publicPath, file));
|
|
1292
|
+
if (!relative.endsWith('/index.html')) continue;
|
|
1293
|
+
overrides[relative] = { path: relative.replace(/\/index\.html$/, '') };
|
|
1294
|
+
}
|
|
1295
|
+
for (const [relative, metadata] of Object.entries(staticFiles)) {
|
|
1296
|
+
if (!canServeStaticOnVercel(metadata)) continue;
|
|
1297
|
+
const contentType = metadata.headers.find(
|
|
1298
|
+
([name]) => name.toLowerCase() === 'content-type',
|
|
1299
|
+
)?.[1];
|
|
1300
|
+
if (contentType) overrides[relative] = { ...overrides[relative], contentType };
|
|
1301
|
+
}
|
|
1302
|
+
return overrides;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function canServeStaticOnVercel(metadata: StaticFileMetadata) {
|
|
1306
|
+
return (
|
|
1307
|
+
metadata.status === 200 &&
|
|
1308
|
+
metadata.headers.every(([name]) => name.toLowerCase() === 'content-type')
|
|
1309
|
+
);
|
|
1310
|
+
}
|