@wular/pnext 0.1.6 → 0.1.8
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/bin/pnext +9 -2
- package/dist/server/_chunks/aliases-pycbtjt1.js +46 -0
- package/dist/server/_chunks/analyze-4d9e00g9.js +449 -0
- package/dist/server/_chunks/boot-bhemypxz.js +38 -0
- package/dist/server/_chunks/build-c991dv65.js +86 -0
- package/dist/server/_chunks/build-kh5e3x48.js +3341 -0
- package/dist/server/_chunks/build-tier-0a96kmx3.js +2456 -0
- package/dist/server/_chunks/compile-040r79gj.js +22 -0
- package/dist/server/_chunks/config-0tvz03a6.js +24 -0
- package/dist/server/_chunks/config-27rj7830.js +36 -0
- package/dist/server/_chunks/create-9rvwshsp.js +135 -0
- package/dist/server/_chunks/dev-ex9exynt.js +220 -0
- package/dist/server/_chunks/esbuild-yyktfrtt.js +15 -0
- package/dist/server/_chunks/generate-myw9hef7.js +22 -0
- package/dist/server/_chunks/index-0r5eqfc7.js +279 -0
- package/dist/server/_chunks/index-0yjv5v47.js +105 -0
- package/dist/server/_chunks/index-11xpbfej.js +17 -0
- package/dist/server/_chunks/index-141vcwk6.js +109 -0
- package/dist/server/_chunks/index-1pzm1hdy.js +4130 -0
- package/dist/server/_chunks/index-29pq0kax.js +590 -0
- package/dist/server/_chunks/index-2vz0wzdn.js +59 -0
- package/dist/server/_chunks/index-2wfv4kvx.js +182 -0
- package/dist/server/_chunks/index-38xpdxwd.js +194 -0
- package/dist/server/_chunks/index-3e075kny.js +128 -0
- package/dist/server/_chunks/index-3ycf5ckv.js +472 -0
- package/dist/server/_chunks/index-4399bvd2.js +18 -0
- package/dist/server/_chunks/index-4da76t62.js +297 -0
- package/dist/server/_chunks/index-4t92hdxd.js +172 -0
- package/dist/server/_chunks/index-4zzebszd.js +10 -0
- package/dist/server/_chunks/index-52k54ykj.js +95 -0
- package/dist/server/_chunks/index-5hr4062s.js +70 -0
- package/dist/server/_chunks/index-68m3mwbd.js +84 -0
- package/dist/server/_chunks/index-6b009mt0.js +56 -0
- package/dist/server/_chunks/index-6gex3pfa.js +15 -0
- package/dist/server/_chunks/index-6kp9r0d9.js +11 -0
- package/dist/server/_chunks/index-6q7zvrap.js +134 -0
- package/dist/server/_chunks/index-71885zaq.js +284 -0
- package/dist/server/_chunks/index-73z4esab.js +38 -0
- package/dist/server/_chunks/index-79ngvam8.js +60 -0
- package/dist/server/_chunks/index-7wc9n8be.js +25 -0
- package/dist/server/_chunks/index-8ct7rqy3.js +198 -0
- package/dist/server/_chunks/index-8kcxdfya.js +206 -0
- package/dist/server/_chunks/index-9ch50vtq.js +18 -0
- package/dist/server/_chunks/index-9f49w76j.js +213 -0
- package/dist/server/_chunks/index-9hz3s0m8.js +851 -0
- package/dist/server/_chunks/index-akkrvjn6.js +535 -0
- package/dist/server/_chunks/index-b6wtkdp4.js +24 -0
- package/dist/server/_chunks/index-b7j671dj.js +207 -0
- package/dist/server/_chunks/index-bcws5eh2.js +35 -0
- package/dist/server/_chunks/index-bzd2ef7t.js +10 -0
- package/dist/server/_chunks/index-cjgqs13g.js +65 -0
- package/dist/server/_chunks/index-ckkdpht5.js +41 -0
- package/dist/server/_chunks/index-cn86z041.js +296 -0
- package/dist/server/_chunks/index-cvr4z22m.js +92 -0
- package/dist/server/_chunks/index-cyfednwb.js +87 -0
- package/dist/server/_chunks/index-d158y67j.js +152 -0
- package/dist/server/_chunks/index-d1jsmhhk.js +955 -0
- package/dist/server/_chunks/index-dg0f05zs.js +199 -0
- package/dist/server/_chunks/index-dgr390q7.js +9438 -0
- package/dist/server/_chunks/index-drwv61cc.js +185 -0
- package/dist/server/_chunks/index-e3a6yx84.js +18 -0
- package/dist/server/_chunks/index-e88tyv35.js +45 -0
- package/dist/server/_chunks/index-en7n9f7m.js +22 -0
- package/dist/server/_chunks/index-entm8bmz.js +28 -0
- package/dist/server/_chunks/index-evhd413j.js +57 -0
- package/dist/server/_chunks/index-ezgj0tr1.js +88 -0
- package/dist/server/_chunks/index-fjv96re1.js +5295 -0
- package/dist/server/_chunks/index-fn1chkkd.js +214 -0
- package/dist/server/_chunks/index-fp8r0sqs.js +64 -0
- package/dist/server/_chunks/index-gcmj16xy.js +92 -0
- package/dist/server/_chunks/index-gkndxkxf.js +736 -0
- package/dist/server/_chunks/index-grta8ap3.js +238 -0
- package/dist/server/_chunks/index-h2a70tg4.js +7 -0
- package/dist/server/_chunks/index-h4xy648y.js +51 -0
- package/dist/server/_chunks/index-hdbwtdv2.js +352 -0
- package/dist/server/_chunks/index-he6n2bvp.js +1270 -0
- package/dist/server/_chunks/index-j0m1jxqa.js +1253 -0
- package/dist/server/_chunks/index-j2zrcp1k.js +66 -0
- package/dist/server/_chunks/index-j681c48a.js +2530 -0
- package/dist/server/_chunks/index-jbj7ehef.js +50 -0
- package/dist/server/_chunks/index-jcsjq3d1.js +20 -0
- package/dist/server/_chunks/index-jks0x0a1.js +14 -0
- package/dist/server/_chunks/index-jn9rbhrx.js +1599 -0
- package/dist/server/_chunks/index-jt8e1pje.js +12 -0
- package/dist/server/_chunks/index-kdhxpwyq.js +483 -0
- package/dist/server/_chunks/index-knp0s912.js +7 -0
- package/dist/server/_chunks/index-kqyy1d4f.js +50 -0
- package/dist/server/_chunks/index-kresp2n8.js +445 -0
- package/dist/server/_chunks/index-m63czgq5.js +590 -0
- package/dist/server/_chunks/index-ms9v9tnx.js +31 -0
- package/dist/server/_chunks/index-n4hxpng2.js +11 -0
- package/dist/server/_chunks/index-n629dy38.js +1071 -0
- package/dist/server/_chunks/index-nf3df4jy.js +266 -0
- package/dist/server/_chunks/index-nqh3xhdc.js +57 -0
- package/dist/server/_chunks/index-pym1cezq.js +39 -0
- package/dist/server/_chunks/index-q3r9z8mq.js +213 -0
- package/dist/server/_chunks/index-q738kkhj.js +100 -0
- package/dist/server/_chunks/index-qcrvt6er.js +112 -0
- package/dist/server/_chunks/index-qjj6gddn.js +54 -0
- package/dist/server/_chunks/index-qpwsgt3a.js +158 -0
- package/dist/server/_chunks/index-r97ptz1q.js +97 -0
- package/dist/server/_chunks/index-ras0sdab.js +26 -0
- package/dist/server/_chunks/index-rcs322t1.js +19 -0
- package/dist/server/_chunks/index-rgj5mx84.js +101 -0
- package/dist/server/_chunks/index-rjegyfx4.js +1435 -0
- package/dist/server/_chunks/index-rt9qq1va.js +4 -0
- package/dist/server/_chunks/index-rthrsed5.js +356 -0
- package/dist/server/_chunks/index-sq3kvz3g.js +200 -0
- package/dist/server/_chunks/index-stbk3jhn.js +31 -0
- package/dist/server/_chunks/index-tf4j19ge.js +130 -0
- package/dist/server/_chunks/index-tgcakdxc.js +125 -0
- package/dist/server/_chunks/index-tgsfjmep.js +74 -0
- package/dist/server/_chunks/index-tq1f96v7.js +51 -0
- package/dist/server/_chunks/index-tq3b39a5.js +107 -0
- package/dist/server/_chunks/index-typcbcsv.js +819 -0
- package/dist/server/_chunks/index-v1ags1na.js +132 -0
- package/dist/server/_chunks/index-vmr35jjw.js +170 -0
- package/dist/server/_chunks/index-vvq7rqtn.js +329 -0
- package/dist/server/_chunks/index-vz0km5qk.js +33 -0
- package/dist/server/_chunks/index-w6h8m6vp.js +138 -0
- package/dist/server/_chunks/index-web361b8.js +138 -0
- package/dist/server/_chunks/index-wm36rj4v.js +311 -0
- package/dist/server/_chunks/index-x0y005nh.js +283 -0
- package/dist/server/_chunks/index-x2m17v9c.js +6166 -0
- package/dist/server/_chunks/index-x3yn4ywm.js +22 -0
- package/dist/server/_chunks/index-x9c55pzy.js +871 -0
- package/dist/server/_chunks/index-xh1nbk46.js +1299 -0
- package/dist/server/_chunks/index-xvj80c3a.js +30 -0
- package/dist/server/_chunks/index-xywjg2vc.js +100 -0
- package/dist/server/_chunks/index-y4d02znn.js +66 -0
- package/dist/server/_chunks/index-y7mzvq2q.js +110 -0
- package/dist/server/_chunks/index-ye9hmsvf.js +47 -0
- package/dist/server/_chunks/index-yhxkxghe.js +56 -0
- package/dist/server/_chunks/index-yrjkqtfj.js +86 -0
- package/dist/server/_chunks/index-z88qv16t.js +11 -0
- package/dist/server/_chunks/index-zfrwq472.js +154 -0
- package/dist/server/_chunks/index-zqpqyg8p.js +666 -0
- package/dist/server/_chunks/loader-5en7wpt5.js +80 -0
- package/dist/server/_chunks/modules-t92favpj.js +76 -0
- package/dist/server/_chunks/pipeline-4t9q43ht.js +836 -0
- package/dist/server/_chunks/production-385bw8df.js +55 -0
- package/dist/server/_chunks/proxy-config-60gprphe.js +23 -0
- package/dist/server/_chunks/proxy-gfhmktbk.js +35 -0
- package/dist/server/_chunks/router-xnrva921.js +501 -0
- package/dist/server/_chunks/routes-ht868pdc.js +66 -0
- package/dist/server/_chunks/run-d40d63ag.js +697 -0
- package/dist/server/_chunks/server-2p8fgqq2.js +1839 -0
- package/dist/server/_chunks/tsconfig-defaults-bcn34eqb.js +21 -0
- package/dist/server/_chunks/typegen-dvtcjtaw.js +37 -0
- package/dist/server/api/client-cache.js +14 -0
- package/dist/server/cli/index.js +375 -0
- package/dist/server/cli/start.js +158 -0
- package/dist/server/client/reference.js +16 -0
- package/dist/server/compat/next/cache.js +56 -0
- package/dist/server/compat/next/canonical-url.js +13 -0
- package/dist/server/compat/next/client-cache.js +57 -0
- package/dist/server/compat/next/client-navigation.js +232 -0
- package/dist/server/compat/next/client-only.js +1 -0
- package/dist/server/compat/next/client-script.js +178 -0
- package/dist/server/compat/next/client-server.js +34 -0
- package/dist/server/compat/next/config-headers.js +34 -0
- package/dist/server/compat/next/config-loader.js +25 -0
- package/dist/server/compat/next/config.js +10 -0
- package/dist/server/compat/next/constants.js +18 -0
- package/dist/server/compat/next/custom-server.js +10 -0
- package/dist/server/compat/next/dist/client/components/app-router-headers.js +48 -0
- package/dist/server/compat/next/dist/server/web/spec-extension/revalidate.js +27 -0
- package/dist/server/compat/next/dist/server/web/spec-extension/unstable-cache.js +25 -0
- package/dist/server/compat/next/dist/server/web/spec-extension/unstable-no-store.js +25 -0
- package/dist/server/compat/next/dynamic.js +21 -0
- package/dist/server/compat/next/error.js +112 -0
- package/dist/server/compat/next/font/cache.js +16 -0
- package/dist/server/compat/next/font/google.js +3 -0
- package/dist/server/compat/next/font/index.js +1 -0
- package/dist/server/compat/next/font/local.js +24 -0
- package/dist/server/compat/next/font/runtime-client.js +33 -0
- package/dist/server/compat/next/font/runtime.js +43 -0
- package/dist/server/compat/next/font/shared.js +22 -0
- package/dist/server/compat/next/form.js +129 -0
- package/dist/server/compat/next/head.js +10 -0
- package/dist/server/compat/next/headers.js +30 -0
- package/dist/server/compat/next/image/client.js +11 -0
- package/dist/server/compat/next/image/config.js +18 -0
- package/dist/server/compat/next/image/optimizer.js +25 -0
- package/dist/server/compat/next/image/patterns.js +12 -0
- package/dist/server/compat/next/image/props.js +16 -0
- package/dist/server/compat/next/image/shared.js +86 -0
- package/dist/server/compat/next/image/static-metadata.js +32 -0
- package/dist/server/compat/next/image/validate.js +8 -0
- package/dist/server/compat/next/image.js +59 -0
- package/dist/server/compat/next/index.js +1 -0
- package/dist/server/compat/next/legacy-image.js +78 -0
- package/dist/server/compat/next/legacy-request-apis.js +24 -0
- package/dist/server/compat/next/link-transform.js +15 -0
- package/dist/server/compat/next/link.js +14 -0
- package/dist/server/compat/next/navigation.js +191 -0
- package/dist/server/compat/next/offline.js +45 -0
- package/dist/server/compat/next/og.js +8 -0
- package/dist/server/compat/next/preferred-region.js +8 -0
- package/dist/server/compat/next/redirects.js +32 -0
- package/dist/server/compat/next/resource-hints.js +30 -0
- package/dist/server/compat/next/rewrites.js +43 -0
- package/dist/server/compat/next/root-params.js +121 -0
- package/dist/server/compat/next/route-state.js +23 -0
- package/dist/server/compat/next/router.js +103 -0
- package/dist/server/compat/next/script.js +37 -0
- package/dist/server/compat/next/server-only.js +1 -0
- package/dist/server/compat/next/server.js +131 -0
- package/dist/server/compat/next/svgr.js +14 -0
- package/dist/server/compat/next/telemetry.js +10 -0
- package/dist/server/compat/next/usage.js +13 -0
- package/dist/server/compat/next/user-agent.js +12 -0
- package/dist/server/compat/next/web-vitals.js +35 -0
- package/dist/server/compat/react/action-state.js +11 -0
- package/dist/server/compat/react/client-lite.js +83 -0
- package/dist/server/compat/react/client.js +131 -0
- package/dist/server/compat/react/compiler-runtime.js +42 -0
- package/dist/server/compat/react/dom-client.js +106 -0
- package/dist/server/compat/react/dom-react-server.js +37 -0
- package/dist/server/compat/react/dom-server.js +40 -0
- package/dist/server/compat/react/dom.js +52 -0
- package/dist/server/compat/react/hooks-extra.js +12 -0
- package/dist/server/compat/react/internals.js +14 -0
- package/dist/server/compat/react/parity.js +11 -0
- package/dist/server/compat/react/preact.js +113 -0
- package/dist/server/compat/react/react-server.js +133 -0
- package/dist/server/compat/react/router-shim.js +8 -0
- package/dist/server/compat/react/server-component-use.js +14 -0
- package/dist/server/compat/react/server-inserted-html.js +14 -0
- package/dist/server/compat/react/server.js +194 -0
- package/dist/server/compat/react/style-values.js +10 -0
- package/dist/server/compat/react/use.js +10 -0
- package/dist/server/compat/react/view-transition.js +12 -0
- package/dist/server/generation.json +1 -0
- package/package.json +4 -1
- package/reference/compat.md +1 -1
- package/reference/config.md +2 -2
- package/reference/dev.md +1 -1
- package/reference/getting-started.md +1 -1
- package/reference/rendering.md +4 -4
- package/src/api/dynamic.tsx +86 -4
- package/src/api/server.ts +24 -6
- package/src/cli/adapters/vercel-warm.ts +327 -62
- package/src/cli/adapters/vercel.ts +164 -1054
- package/src/cli/analyze.ts +3 -2
- package/src/cli/build/cache.ts +6 -6
- package/src/cli/build.ts +78 -45
- package/src/cli/index.ts +21 -3
- package/src/cli/serve/immutable.ts +31 -0
- package/src/cli/serve/pipeline.ts +97 -93
- package/src/cli/serve/standalone-worker.ts +8 -0
- package/src/cli/serve/standalone.ts +703 -0
- package/src/cli/start.ts +31 -8
- package/src/client/build.ts +1 -1
- package/src/client/chunk-fold.ts +19 -6
- package/src/client/react-tier.ts +3 -2
- package/src/client/reference-stub.ts +23 -36
- package/src/compat/actions/client-plugin.ts +4 -2
- package/src/compat/actions/config.ts +6 -5
- package/src/compat/actions/extensions.ts +5 -5
- package/src/compat/aliases.ts +8 -1
- package/src/compat/bundler/config.ts +88 -0
- package/src/compat/bundler/optimize-package-imports.ts +86 -26
- package/src/compat/bundler/relay-transform.ts +15 -3
- package/src/compat/bundler/require-context.ts +2 -1
- package/src/compat/bundler/webpack-loaders.ts +3 -2
- package/src/compat/bundler/worker.ts +2 -2
- package/src/compat/cache-control.ts +16 -8
- package/src/compat/css/inline-css.ts +1 -1
- package/src/compat/css/modules.ts +18 -5
- package/src/compat/export/build-complete.ts +1 -1
- package/src/compat/export/emit.ts +4 -4
- package/src/compat/export/standalone.ts +30 -65
- package/src/compat/image-optimizer/optimize.ts +1 -1
- package/src/compat/lifecycle/error-funnel.ts +1 -1
- package/src/compat/lifecycle/instrumentation-client.ts +4 -3
- package/src/compat/lifecycle/instrumentation.ts +117 -24
- package/src/compat/lifecycle/testmode.ts +3 -2
- package/src/compat/metadata-artifacts.ts +1 -1
- package/src/compat/next/config-loader.ts +140 -13
- package/src/compat/next/dynamic.tsx +10 -23
- package/src/compat/next/font/cache.ts +2 -1
- package/src/compat/next/font/runtime.ts +23 -3
- package/src/compat/next/og.ts +3 -3
- package/src/compat/next/usage.ts +3 -2
- package/src/compat/pages/api.ts +83 -14
- package/src/compat/pages/router.ts +30 -14
- package/src/compat/register/bundler.ts +3 -1
- package/src/compat/register/image.ts +1 -1
- package/src/compat/register/middleware.ts +1 -1
- package/src/compat/register/otel.ts +5 -9
- package/src/compat/register/render.ts +3 -1
- package/src/compat/register/use-cache.ts +3 -1
- package/src/compat/release.ts +46 -0
- package/src/compat/segment/serve.ts +1 -1
- package/src/compat/typecheck/check.ts +6 -4
- package/src/compat/validation/validate.ts +10 -7
- package/src/config.ts +76 -9
- package/src/css/assets.ts +213 -0
- package/src/css/build.ts +190 -247
- package/src/dev/client-chunk-store.ts +2 -1
- package/src/dev/restart/client-key.ts +2 -1
- package/src/dev/restart/global-css.ts +2 -1
- package/src/dev/restart/route-bundle-key.ts +2 -1
- package/src/dev/restart/route-facts.ts +2 -1
- package/src/dev/server.ts +34 -14
- package/src/extensions.ts +4 -0
- package/src/out-paths.ts +10 -0
- package/src/render/hooks.ts +13 -0
- package/src/render/renderer.ts +53 -33
- package/src/request/context.ts +9 -0
- package/src/resolve/dynamic.ts +7 -1
- package/src/resolve/engine.ts +2 -12
- package/src/resolve/imports.ts +12 -52
- package/src/resolve/package-json.ts +56 -0
- package/src/resolve/scan-facts.ts +250 -1
- package/src/routing/href.ts +10 -3
- package/src/routing/match.ts +287 -0
- package/src/routing/metadata-files.ts +20 -6
- package/src/routing/proxy-config.ts +137 -0
- package/src/routing/proxy.ts +35 -134
- package/src/routing/routes.ts +22 -281
- package/src/runtime/fingerprint.ts +23 -8
- package/src/runtime/load.ts +170 -0
- package/src/runtime/loader.ts +218 -44
- package/src/runtime/module-cache.ts +15 -1
- package/src/runtime/modules.ts +405 -82
- package/src/runtime/prebundle.ts +101 -0
- package/src/runtime/production.ts +456 -0
- package/src/runtime/release-graph.ts +342 -0
- package/src/runtime/vendor-build.ts +750 -17
- package/src/runtime/vendor.ts +16 -3
- package/src/types.ts +4 -3
- package/src/utils/directive.ts +40 -0
- package/src/utils/fs.ts +95 -4
- package/src/utils/resource-query.ts +12 -0
- package/src/cli/serve/entry.ts +0 -45
|
@@ -1,28 +1,21 @@
|
|
|
1
|
-
import { copyFile,
|
|
2
|
-
import {
|
|
3
|
-
import { builtinModules } from 'node:module'
|
|
1
|
+
import { copyFile, cp, mkdir, readFile, readdir, rename, rm } from 'node:fs/promises'
|
|
2
|
+
import { constants, existsSync, readdirSync } from 'node:fs'
|
|
4
3
|
import path from 'node:path'
|
|
5
|
-
import {
|
|
6
|
-
import { DEFAULT_OUT_DIR, pathToFileHref } from '../../config'
|
|
4
|
+
import { pathToFileHref } from '../../config'
|
|
7
5
|
import type { ResolvedConfig } from '../../config'
|
|
8
|
-
import { globalCssSources } from '../../css/build'
|
|
9
|
-
import { getImportAliasExtensions } from '../../extensions'
|
|
10
6
|
import { nextCompatEnabled } from '../../render/hooks'
|
|
11
|
-
import { importSpecifiers } from '../../resolve/scan-facts'
|
|
12
|
-
import { compiledSpecifiersManifestSuffix } from '../../runtime/modules'
|
|
13
|
-
import { resolveImport, workspacePackageRoots } from '../../resolve/imports'
|
|
14
7
|
import { findProxyFile, proxyRoutePatterns, type ProxyModule } from '../../routing/proxy'
|
|
15
8
|
import { cacheRoot } from '../boot/named-bin'
|
|
16
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
immutableAssetPath,
|
|
11
|
+
immutableAssetPrefixes,
|
|
12
|
+
immutableCacheControl,
|
|
13
|
+
} from '../serve/immutable'
|
|
17
14
|
import { escapeRegex } from '../../utils/code'
|
|
18
15
|
import { listFiles, toPosixPath, writeText } from '../../utils/fs'
|
|
19
16
|
import { createVerboseLogger, type VerboseLogger } from '../../utils/verbose'
|
|
20
17
|
import type { BuildManifest, StaticFileMetadata } from '../../types'
|
|
21
18
|
|
|
22
|
-
// Vercel unpacks a function's `.func` directory here at runtime. Baked
|
|
23
|
-
// absolute paths in the build output are rewritten to this root.
|
|
24
|
-
const RUNTIME_ROOT = '/var/task'
|
|
25
|
-
|
|
26
19
|
// The single catch-all server function: the same `pnext start` pipeline,
|
|
27
20
|
// running on Vercel's Bun runtime (`"bunVersion": "1.x"` in vercel.json).
|
|
28
21
|
const SERVER_FUNCTION = '_pnext'
|
|
@@ -38,114 +31,9 @@ const FUNCTION_PLATFORM = {
|
|
|
38
31
|
libc: 'glibc',
|
|
39
32
|
} as const
|
|
40
33
|
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
const prunedDirNames = [
|
|
45
|
-
'node_modules',
|
|
46
|
-
'.git',
|
|
47
|
-
'.next',
|
|
48
|
-
'.pnext',
|
|
49
|
-
'.turbo',
|
|
50
|
-
'.cache',
|
|
51
|
-
'.vercel',
|
|
52
|
-
'.tmp',
|
|
53
|
-
]
|
|
54
|
-
|
|
55
|
-
// Files nothing reads at request time. Sourcemaps only decorate stack traces,
|
|
56
|
-
// and Bun strips types rather than resolving declarations — neither can change
|
|
57
|
-
// what the function serves, and together they are a fifth of a typical closure.
|
|
58
|
-
// Markdown is deliberately absent: an mdx app imports it as a module.
|
|
59
|
-
const droppedFileSuffixes = [
|
|
60
|
-
'.map',
|
|
61
|
-
'.d.ts',
|
|
62
|
-
'.d.mts',
|
|
63
|
-
'.d.cts',
|
|
64
|
-
'.DS_Store',
|
|
65
|
-
// The compile-time specifier sidecars (see traceNodeModulesClosure below):
|
|
66
|
-
// build-time metadata the trace consumes, never read at request time.
|
|
67
|
-
compiledSpecifiersManifestSuffix,
|
|
68
|
-
]
|
|
69
|
-
|
|
70
|
-
/** What the function tree ships, resolved once from config. */
|
|
71
|
-
interface PackRules {
|
|
72
|
-
prunedDirs: Set<string>
|
|
73
|
-
droppedSuffixes: string[]
|
|
74
|
-
/** Absolute sources exempt from the prune list — the app's own build output. */
|
|
75
|
-
keepPaths: Set<string>
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Test seam: the packing suite builds one app both ways and diffs the trees,
|
|
79
|
-
// so it needs the unpruned closure on demand. Never off for a real deployment.
|
|
80
|
-
let packPruningEnabled = true
|
|
81
|
-
|
|
82
|
-
/** @internal Test-only. Returns a restore function. */
|
|
83
|
-
export function setPackPruningEnabled(enabled: boolean) {
|
|
84
|
-
const previous = packPruningEnabled
|
|
85
|
-
packPruningEnabled = enabled
|
|
86
|
-
return () => {
|
|
87
|
-
packPruningEnabled = previous
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function packRules(config: ResolvedConfig): PackRules {
|
|
92
|
-
if (!packPruningEnabled) {
|
|
93
|
-
return {
|
|
94
|
-
// The two the function could never resolve through stay pruned even
|
|
95
|
-
// here: a nested store or object database is not a packing choice.
|
|
96
|
-
prunedDirs: new Set(['node_modules', '.git']),
|
|
97
|
-
droppedSuffixes: [],
|
|
98
|
-
keepPaths: new Set([path.resolve(config.outPath)]),
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
const keep = new Set(config.adapter?.keep ?? [])
|
|
102
|
-
const extra = config.adapter?.exclude ?? []
|
|
103
|
-
const isSuffix = (entry: string) => entry.startsWith('.') && !prunedDirNames.includes(entry)
|
|
104
|
-
return {
|
|
105
|
-
prunedDirs: new Set(
|
|
106
|
-
[...prunedDirNames, ...extra.filter(entry => !isSuffix(entry))].filter(
|
|
107
|
-
name => !keep.has(name),
|
|
108
|
-
),
|
|
109
|
-
),
|
|
110
|
-
droppedSuffixes: [...droppedFileSuffixes, ...extra.filter(isSuffix)].filter(
|
|
111
|
-
suffix => !keep.has(suffix),
|
|
112
|
-
),
|
|
113
|
-
// The app's outDir shares its name with the prune list; it holds the
|
|
114
|
-
// compiled module cache the function serves from and must survive. The
|
|
115
|
-
// default out root goes with it: a `distDir` app moved its output away, but
|
|
116
|
-
// the next.config bundle the function imports at boot still lives there.
|
|
117
|
-
keepPaths: new Set([path.resolve(config.outPath), path.resolve(config.root, DEFAULT_OUT_DIR)]),
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* A keep path and everything under it. Prefix, not equality: the compiled module cache mirrors the
|
|
123
|
-
* source layout, so a dependency's artifacts land in `<outDir>/cache/server/<profile>/node_modules/
|
|
124
|
-
* ...`. That segment names a source tree, not a package store, and pruning it by name ships the
|
|
125
|
-
* importer without the module it imports — the function then 500s on the first request that reaches
|
|
126
|
-
* it. Only the app's own build output is exempt; a real `node_modules` beside it still prunes.
|
|
127
|
-
*/
|
|
128
|
-
function withinKeepPath(pack: PackRules, source: string) {
|
|
129
|
-
const resolved = path.resolve(source)
|
|
130
|
-
for (const keep of pack.keepPaths) {
|
|
131
|
-
if (resolved === keep || resolved.startsWith(`${keep}${path.sep}`)) return true
|
|
132
|
-
}
|
|
133
|
-
return false
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* `isLink` is load-bearing, not a detail: the exemption covers real directories the build output
|
|
138
|
-
* owns, never a link out of it. The proxy shim links `node_modules` into `.pnext`, and following
|
|
139
|
-
* that would pull the whole store into the function.
|
|
140
|
-
*/
|
|
141
|
-
function shipsDir(pack: PackRules, name: string, source: string, isLink = false) {
|
|
142
|
-
if (!pack.prunedDirs.has(name)) return true
|
|
143
|
-
return !isLink && withinKeepPath(pack, source)
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function shipsFile(pack: PackRules, name: string) {
|
|
147
|
-
return !pack.droppedSuffixes.some(suffix => name.endsWith(suffix))
|
|
148
|
-
}
|
|
34
|
+
// Image optimization's native stack: Vercel optimizes images itself, as Next's builder drops these
|
|
35
|
+
// (next/dist/build/collect-build-traces.js).
|
|
36
|
+
const functionDroppedPackages = [/^sharp$/, /^@img\/sharp-libvips/]
|
|
149
37
|
|
|
150
38
|
interface VercelConfig {
|
|
151
39
|
version: 3
|
|
@@ -156,55 +44,36 @@ interface VercelConfig {
|
|
|
156
44
|
methods?: string[]
|
|
157
45
|
headers?: Record<string, string>
|
|
158
46
|
continue?: boolean
|
|
47
|
+
important?: boolean
|
|
159
48
|
}
|
|
160
49
|
| { handle: 'filesystem' }
|
|
161
50
|
)[]
|
|
162
51
|
overrides?: Record<string, { path?: string; contentType?: string }>
|
|
163
52
|
}
|
|
164
53
|
|
|
54
|
+
/** `.vercel/output`: the standalone dir as the server function, its `static/` as the CDN tree. */
|
|
165
55
|
export async function writeVercelOutput(
|
|
166
56
|
config: ResolvedConfig,
|
|
167
57
|
manifest: BuildManifest,
|
|
168
|
-
options: { verbose?: boolean
|
|
58
|
+
options: { verbose?: boolean } = {},
|
|
169
59
|
) {
|
|
170
60
|
const log = createVerboseLogger(options.verbose ?? false, 'vercel')
|
|
171
61
|
const outputPath = path.join(config.root, '.vercel', 'output')
|
|
62
|
+
const staticPath = path.join(config.outPath, 'static')
|
|
172
63
|
const staticFiles = manifest.staticFiles ?? {}
|
|
173
|
-
// Warming runs in its own process (see ./vercel-warm) and normally started
|
|
174
|
-
// with the build; a caller that skipped that only loses the overlap.
|
|
175
|
-
const warm = options.warm ?? startWarmChild(config)
|
|
176
64
|
await log.step('prepare output directory', () => emptyDir(outputPath))
|
|
177
65
|
|
|
178
|
-
// The compiled set is already final once the child reaches its handler
|
|
179
|
-
// phase, so `onCompiled` resolves this ahead of full completion and
|
|
180
|
-
// writeServerFunction starts tracing while the child's handler imports
|
|
181
|
-
// still run. `finish` itself always fires `onCompiled` before it resolves,
|
|
182
|
-
// so this promise never hangs on it.
|
|
183
|
-
let resolveWarmedModules: (modules: string[]) => void
|
|
184
|
-
const warmedModulesEarly = new Promise<string[]>(resolve => {
|
|
185
|
-
resolveWarmedModules = resolve
|
|
186
|
-
})
|
|
187
|
-
const warmSettled = warm.finish(log, modules => resolveWarmedModules(modules))
|
|
188
|
-
// Nothing awaits it until writeServerFunction's copy step; park the
|
|
189
|
-
// rejection so it never surfaces as unhandled in between (it never actually
|
|
190
|
-
// rejects — warmWithRestarts catches everything itself — but the gap
|
|
191
|
-
// between here and that await is otherwise unguarded).
|
|
192
|
-
warmSettled.catch(() => undefined)
|
|
193
|
-
const warmedModules = await log.step('warm module cache (compiled)', () => warmedModulesEarly)
|
|
194
|
-
|
|
195
66
|
const functionPath = path.join(outputPath, 'functions', `${SERVER_FUNCTION}.func`)
|
|
196
|
-
await writeServerFunction(config, manifest, functionPath,
|
|
67
|
+
await writeServerFunction(config.outPath, manifest, functionPath, log)
|
|
197
68
|
|
|
198
|
-
// After the warm pass, never before: it is what emits next/font bytes under public/, and a fully
|
|
199
|
-
// dynamic app renders nothing at build, so copying earlier ships a CDN with no fonts.
|
|
200
69
|
await log.step('copy static files', () =>
|
|
201
70
|
copyStaticFiles(
|
|
202
|
-
|
|
71
|
+
staticPath,
|
|
203
72
|
path.join(outputPath, 'static'),
|
|
204
73
|
relative => !staticFiles[relative] || canServeStaticOnVercel(staticFiles[relative]),
|
|
205
74
|
),
|
|
206
75
|
)
|
|
207
|
-
const overrides = await staticOverrides(
|
|
76
|
+
const overrides = await staticOverrides(staticPath, staticFiles)
|
|
208
77
|
|
|
209
78
|
const routes: NonNullable<VercelConfig['routes']> = [
|
|
210
79
|
// next-compat documents reference the build output under Next's static path (assetPathname),
|
|
@@ -213,7 +82,7 @@ export async function writeVercelOutput(
|
|
|
213
82
|
// server function: served, but at function cost with no edge cache.
|
|
214
83
|
...compatStaticRewrite(config, outputPath),
|
|
215
84
|
// Every content-hashed build asset, exactly the set `pnext start` serves immutable.
|
|
216
|
-
await
|
|
85
|
+
...(await immutableAssetRoutes(staticPath, manifest.publicAssets)),
|
|
217
86
|
// Proxy-matched paths go to the server function before the CDN filesystem
|
|
218
87
|
// check — `pnext start` runs the proxy ahead of static files too.
|
|
219
88
|
...(await proxyRoutes(config)),
|
|
@@ -281,141 +150,38 @@ async function proxyRoutes(config: ResolvedConfig) {
|
|
|
281
150
|
const proxyFile = findProxyFile(config)
|
|
282
151
|
if (!proxyFile) return []
|
|
283
152
|
const proxyModule = (await import(pathToFileHref(proxyFile))) as ProxyModule
|
|
284
|
-
|
|
153
|
+
// Percent-encoded paths and repeated slashes go to the function, which decodes or 308s them.
|
|
154
|
+
return [...proxyRoutePatterns(proxyModule.config), '^.*(?:%|//).*$'].map(src => ({
|
|
285
155
|
src,
|
|
286
156
|
dest: `/${SERVER_FUNCTION}`,
|
|
287
157
|
}))
|
|
288
158
|
}
|
|
289
159
|
|
|
290
160
|
async function writeServerFunction(
|
|
291
|
-
|
|
161
|
+
standalonePath: string,
|
|
292
162
|
manifest: BuildManifest,
|
|
293
163
|
functionPath: string,
|
|
294
|
-
warmedModules: string[],
|
|
295
|
-
// Resolves once the warm child fully exits (handlers included). The trace below only needs the
|
|
296
|
-
// compiled set `warmedModules` already carries - it reasons from static specifiers, never from
|
|
297
|
-
// handler execution - so it starts immediately; only the tree copy further down, which must ship
|
|
298
|
-
// whatever handler imports wrote to the build cache, waits on this.
|
|
299
|
-
warmSettled: Promise<unknown>,
|
|
300
164
|
log: VerboseLogger,
|
|
301
165
|
) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const framework = await resolveFrameworkPackage(workspaceRoot)
|
|
307
|
-
const { root: pnextRoot, inWorkspace: pnextInWorkspace, targetRel: pnextTargetRel } = framework
|
|
308
|
-
const closure = await traceNodeModulesClosure(
|
|
309
|
-
config,
|
|
310
|
-
manifest,
|
|
311
|
-
functionPath,
|
|
312
|
-
framework,
|
|
313
|
-
warmedModules,
|
|
314
|
-
pack,
|
|
315
|
-
log,
|
|
166
|
+
await mkdir(path.dirname(functionPath), { recursive: true })
|
|
167
|
+
// A copy-on-write clone where the filesystem has one; links stay links.
|
|
168
|
+
await log.step('copy standalone', () =>
|
|
169
|
+
cp(standalonePath, functionPath, { recursive: true, verbatimSymlinks: true }),
|
|
316
170
|
)
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
return path.join(functionPath, path.relative(workspaceRoot, file))
|
|
324
|
-
}
|
|
325
|
-
return undefined
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Replicate the workspace layout the build ran in: app source (the renderer reads convention files
|
|
329
|
-
// and global css imports from it at request time), the build output, and the workspace packages the
|
|
330
|
-
// server can actually reach. Unrelated workspace trees never ship.
|
|
331
|
-
const appRel = path.relative(workspaceRoot, config.root)
|
|
332
|
-
const packageRoots = workspacePackageRoots(workspaceRoot).map(root => path.resolve(root))
|
|
333
|
-
const neededRoots = new Set<string>()
|
|
334
|
-
for (const file of closure.tracedFiles) {
|
|
335
|
-
const root = packageRoots.find(candidate => isInsideDir(candidate, file))
|
|
336
|
-
if (root) neededRoots.add(root)
|
|
337
|
-
}
|
|
338
|
-
for (const relative of closure.workspacePackages) {
|
|
339
|
-
neededRoots.add(path.resolve(workspaceRoot, relative))
|
|
340
|
-
}
|
|
341
|
-
// The renderer re-resolves the root layout's global css imports from source
|
|
342
|
-
// at request time; the packages holding those css files must be present.
|
|
343
|
-
for (const file of globalCssSources(config)) {
|
|
344
|
-
const root = packageRoots.find(candidate => isInsideDir(candidate, file))
|
|
345
|
-
if (root) neededRoots.add(root)
|
|
346
|
-
}
|
|
347
|
-
// Tracing sees what the *server* loaded, which is not everything the shipped source can still ask
|
|
348
|
-
// for: the app's own sources travel with the function and a compat compile at request time resolves
|
|
349
|
-
// their imports for real. A workspace package one of them names but no traced module reached would
|
|
350
|
-
// not be here, so close over the declared workspace dependencies too.
|
|
351
|
-
addWorkspaceDependencies(neededRoots, packageRoots, [path.resolve(config.root), ...neededRoots])
|
|
352
|
-
neededRoots.delete(path.resolve(config.root))
|
|
353
|
-
neededRoots.delete(pnextRoot)
|
|
354
|
-
|
|
355
|
-
// The trace above never needed the handler phase, but the copy below ships
|
|
356
|
-
// config.outPath verbatim — handler imports can still be writing vendor
|
|
357
|
-
// bundles into it. Most of this wait is already gone by the time the trace
|
|
358
|
-
// and the roots above finish; whatever remains is genuine handler-phase work.
|
|
359
|
-
await log.step('await handler warm-up', () => warmSettled)
|
|
360
|
-
|
|
361
|
-
// Every tree lands in its own subdirectory and the copies are io-bound, so
|
|
362
|
-
// the whole replica goes out as one concurrent step instead of tree by tree.
|
|
363
|
-
const roots = neededRoots.size + 2
|
|
364
|
-
await log.step(`copy function tree (${closure.packageCount} packages, ${roots} roots)`, () =>
|
|
365
|
-
Promise.all([
|
|
366
|
-
closure.copy(),
|
|
367
|
-
// `public` is served by the CDN, never from the function; the rest are
|
|
368
|
-
// named again because the app root is the one tree whose build output
|
|
369
|
-
// ships, so it cannot rely on the depth-wise prune alone.
|
|
370
|
-
copyTree(config.root, path.join(functionPath, appRel), pack, [
|
|
371
|
-
'public',
|
|
372
|
-
'.vercel',
|
|
373
|
-
'.next',
|
|
374
|
-
'.turbo',
|
|
375
|
-
]),
|
|
376
|
-
copyPackageTree(pnextRoot, path.join(functionPath, pnextTargetRel), pack),
|
|
377
|
-
...[...neededRoots].map(packageRoot =>
|
|
378
|
-
copyPackageTree(
|
|
379
|
-
packageRoot,
|
|
380
|
-
path.join(functionPath, path.relative(workspaceRoot, packageRoot)),
|
|
381
|
-
pack,
|
|
382
|
-
),
|
|
383
|
-
),
|
|
384
|
-
...['package.json', 'tsconfig.json', 'bunfig.toml']
|
|
385
|
-
.map(name => path.join(workspaceRoot, name))
|
|
386
|
-
.filter(file => existsSync(file))
|
|
387
|
-
.map(file => copyFile(file, path.join(functionPath, path.basename(file)))),
|
|
388
|
-
]),
|
|
389
|
-
)
|
|
390
|
-
|
|
391
|
-
// Baked absolute paths (compiled cache imports, manifest file paths) point
|
|
392
|
-
// at the build machine's workspace; rewrite them to the runtime root.
|
|
393
|
-
await log.step('rewrite baked paths', () =>
|
|
394
|
-
rewriteBakedPaths(path.join(functionPath, appRel, path.relative(config.root, config.outPath)), [
|
|
395
|
-
...(pnextInWorkspace
|
|
396
|
-
? []
|
|
397
|
-
: [[pnextRoot, toPosixPath(path.join(RUNTIME_ROOT, pnextTargetRel))] as const]),
|
|
398
|
-
[workspaceRoot, RUNTIME_ROOT] as const,
|
|
399
|
-
]),
|
|
400
|
-
)
|
|
401
|
-
|
|
402
|
-
// Bun's runtime onResolve plugins never see bare specifiers, so the compat aliases the dev pipeline
|
|
403
|
-
// applies during compilation cannot be applied at runtime for raw-loaded sources (proxy, handlers).
|
|
404
|
-
// Bun does honor tsconfig paths for bare imports, so map the aliased specifiers to their shims in
|
|
405
|
-
// every shipped tsconfig.
|
|
406
|
-
await log.step('inject compat tsconfig paths', () =>
|
|
407
|
-
injectCompatPaths(config, functionPath, pnextRoot, replicaPathFor),
|
|
171
|
+
const modules = path.join(functionPath, 'node_modules')
|
|
172
|
+
const packages = await packageDirs(modules)
|
|
173
|
+
await Promise.all(
|
|
174
|
+
[...packages]
|
|
175
|
+
.filter(([name]) => functionDroppedPackages.some(pattern => pattern.test(name)))
|
|
176
|
+
.map(([, dirs]) => Promise.all(dirs.map(dir => rm(dir, { recursive: true, force: true })))),
|
|
408
177
|
)
|
|
178
|
+
await log.step('platform natives', () => replaceHostNatives(packages, log))
|
|
409
179
|
|
|
410
|
-
const startEntry = toPosixPath(path.join(pnextTargetRel, 'src/cli/start.ts'))
|
|
411
180
|
await writeText(
|
|
412
181
|
path.join(functionPath, 'index.mjs'),
|
|
413
|
-
`import
|
|
414
|
-
import { createRequestHandler } from ${JSON.stringify(`./${startEntry}`)};
|
|
182
|
+
`import { createRequestHandler } from './server/entry.js';
|
|
415
183
|
|
|
416
|
-
const handlerPromise = createRequestHandler({
|
|
417
|
-
root: path.join(import.meta.dirname, ${JSON.stringify(appRel)}),
|
|
418
|
-
});
|
|
184
|
+
const handlerPromise = createRequestHandler({ root: import.meta.dirname });
|
|
419
185
|
|
|
420
186
|
async function handler(request) {
|
|
421
187
|
return (await handlerPromise)(request);
|
|
@@ -455,726 +221,108 @@ export default Object.assign(handler, { fetch: handler });
|
|
|
455
221
|
)
|
|
456
222
|
}
|
|
457
223
|
|
|
458
|
-
/**
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
for (const name of Object.keys(manifest[field] ?? {})) {
|
|
476
|
-
const root = byName.get(name)
|
|
477
|
-
if (!root || seen.has(root)) continue
|
|
478
|
-
seen.add(root)
|
|
479
|
-
roots.add(root)
|
|
480
|
-
queue.push(root)
|
|
224
|
+
/** Package name -> the shipped directories holding it (a store layout can carry several). */
|
|
225
|
+
async function packageDirs(modules: string) {
|
|
226
|
+
const found = new Map<string, string[]>()
|
|
227
|
+
const visit = async (dir: string, depth: number) => {
|
|
228
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => [])
|
|
229
|
+
for (const entry of entries) {
|
|
230
|
+
if (!entry.isDirectory()) continue
|
|
231
|
+
const full = path.join(dir, entry.name)
|
|
232
|
+
if (entry.name === '.bun' || entry.name === '.pnpm') {
|
|
233
|
+
for (const store of await readdir(full, { withFileTypes: true }).catch(() => []))
|
|
234
|
+
if (store.isDirectory()) await visit(path.join(full, store.name, 'node_modules'), 0)
|
|
235
|
+
} else if (entry.name.startsWith('@') && depth === 0) {
|
|
236
|
+
await visit(full, 1)
|
|
237
|
+
} else if (existsSync(path.join(full, 'package.json'))) {
|
|
238
|
+
const name = depth === 1 ? `${path.basename(dir)}/${entry.name}` : entry.name
|
|
239
|
+
found.set(name, [...(found.get(name) ?? []), full])
|
|
240
|
+
await visit(path.join(full, 'node_modules'), 0)
|
|
481
241
|
}
|
|
482
242
|
}
|
|
483
243
|
}
|
|
244
|
+
await visit(modules, 0)
|
|
245
|
+
return found
|
|
484
246
|
}
|
|
485
247
|
|
|
486
|
-
interface
|
|
248
|
+
interface PackageJson {
|
|
487
249
|
name?: string
|
|
488
|
-
|
|
250
|
+
version?: string
|
|
251
|
+
os?: string[]
|
|
252
|
+
cpu?: string[]
|
|
253
|
+
libc?: string[]
|
|
489
254
|
optionalDependencies?: Record<string, string>
|
|
490
255
|
}
|
|
491
256
|
|
|
492
|
-
const manifests = new Map<string, PackageManifest | undefined>()
|
|
493
|
-
|
|
494
|
-
function packageManifest(root: string): PackageManifest | undefined {
|
|
495
|
-
const cached = manifests.get(root)
|
|
496
|
-
if (cached !== undefined || manifests.has(root)) return cached
|
|
497
|
-
let parsed: PackageManifest | undefined
|
|
498
|
-
try {
|
|
499
|
-
parsed = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8')) as PackageManifest
|
|
500
|
-
} catch {
|
|
501
|
-
parsed = undefined // not a package, or unreadable
|
|
502
|
-
}
|
|
503
|
-
manifests.set(root, parsed)
|
|
504
|
-
return parsed
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Clones a tree with hardlinks instead of byte copies - the output is packaging-only, so sharing
|
|
508
|
-
// inodes is safe as long as nothing writes in place (rewriteBakedPaths unlinks before writing).
|
|
509
|
-
// Exclusions apply to direct children only; node_modules and .git are pruned at any depth. Symlinks
|
|
510
|
-
// are dereferenced so the replica is self-contained; the visited set breaks symlink cycles.
|
|
511
|
-
async function copyTree(
|
|
512
|
-
from: string,
|
|
513
|
-
to: string,
|
|
514
|
-
pack: PackRules,
|
|
515
|
-
excludeNames: string[] = [],
|
|
516
|
-
visited = new Set<string>([realDir(from) ?? path.resolve(from)]),
|
|
517
|
-
) {
|
|
518
|
-
const exclude = new Set(excludeNames)
|
|
519
|
-
const clone = directoryCloneEnabled ? await directoryCloner() : null
|
|
520
|
-
// Nothing to skip at this level: the whole tree clones in one syscall.
|
|
521
|
-
if (exclude.size === 0 && !existsSync(to)) {
|
|
522
|
-
await mkdir(path.dirname(to), { recursive: true })
|
|
523
|
-
if (clone?.(from, to)) return normalizeClone(to, from, pack, visited)
|
|
524
|
-
}
|
|
525
|
-
// The build output is still live while the walk runs — typegen rewrites
|
|
526
|
-
// `.pnext/types` under it — so a directory can vanish between the parent's
|
|
527
|
-
// readdir and this one. It was never part of the shipped closure; skip it.
|
|
528
|
-
const entries = readDirEntries(from)
|
|
529
|
-
if (!entries) return
|
|
530
|
-
await mkdir(to, { recursive: true })
|
|
531
|
-
await Promise.all(
|
|
532
|
-
entries.map(async entry => {
|
|
533
|
-
const source = path.join(from, entry.name)
|
|
534
|
-
const target = path.join(to, entry.name)
|
|
535
|
-
// Matched by name before type: a symlinked `node_modules` must be pruned
|
|
536
|
-
// as the directory it points at, not followed.
|
|
537
|
-
if (exclude.has(entry.name) || !shipsDir(pack, entry.name, source, entry.isSymbolicLink()))
|
|
538
|
-
return
|
|
539
|
-
if (entry.isSymbolicLink()) {
|
|
540
|
-
const link = resolveLinkTarget(source)
|
|
541
|
-
if (!link) return
|
|
542
|
-
if (link.isDirectory) {
|
|
543
|
-
if (visited.has(link.path)) return
|
|
544
|
-
visited.add(link.path)
|
|
545
|
-
return copyTree(link.path, target, pack, [], visited)
|
|
546
|
-
}
|
|
547
|
-
return shipsFile(pack, entry.name) ? linkOrCopyFile(link.path, target) : undefined
|
|
548
|
-
}
|
|
549
|
-
// One syscall for the whole subtree beats one per file — the excluded
|
|
550
|
-
// names are stripped from the copy afterwards.
|
|
551
|
-
if (entry.isDirectory() && clone?.(source, target)) {
|
|
552
|
-
return normalizeClone(target, source, pack, visited)
|
|
553
|
-
}
|
|
554
|
-
if (entry.isDirectory()) return copyTree(source, target, pack, [], visited)
|
|
555
|
-
if (entry.isFile() && shipsFile(pack, entry.name)) return linkOrCopyFile(source, target)
|
|
556
|
-
}),
|
|
557
|
-
)
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
/** Directory entries, or undefined if the directory vanished under the walk. */
|
|
561
|
-
function readDirEntries(dir: string) {
|
|
562
|
-
try {
|
|
563
|
-
return readdirSync(dir, { withFileTypes: true })
|
|
564
|
-
} catch (error) {
|
|
565
|
-
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined
|
|
566
|
-
throw error
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
/**
|
|
571
|
-
* Directory-level copy-on-write clone, when the platform has one: macOS `clonefile` copies a whole
|
|
572
|
-
* hierarchy in a single call and, like a hardlink, shares the underlying blocks. Resolved once and
|
|
573
|
-
* never retried - no cloner just means the hardlink walk below does the work.
|
|
574
|
-
*/
|
|
575
|
-
// Test seam: the equivalence suite builds the same app both ways and diffs the trees.
|
|
576
|
-
let directoryCloneEnabled = true
|
|
577
|
-
|
|
578
|
-
/** @internal Test-only. Returns a restore function. */
|
|
579
|
-
export function setDirectoryCloneEnabled(enabled: boolean) {
|
|
580
|
-
const previous = directoryCloneEnabled
|
|
581
|
-
directoryCloneEnabled = enabled
|
|
582
|
-
return () => {
|
|
583
|
-
directoryCloneEnabled = previous
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
let cloner: ((from: string, to: string) => boolean) | null | undefined
|
|
588
|
-
async function directoryCloner() {
|
|
589
|
-
if (cloner !== undefined) return cloner
|
|
590
|
-
cloner = null
|
|
591
|
-
if (process.platform === 'darwin') {
|
|
592
|
-
try {
|
|
593
|
-
const { dlopen } = await import('bun:ffi')
|
|
594
|
-
const { symbols } = dlopen('libSystem.B.dylib', {
|
|
595
|
-
clonefile: { args: ['cstring', 'cstring', 'i32'], returns: 'i32' },
|
|
596
|
-
})
|
|
597
|
-
cloner = (from, to) =>
|
|
598
|
-
symbols.clonefile(Buffer.from(`${from}\0`), Buffer.from(`${to}\0`), 0) === 0
|
|
599
|
-
} catch {
|
|
600
|
-
// no clonefile here; the per-file walk stays correct, just slower
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
return cloner
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
/**
|
|
607
|
-
* A clone is verbatim, so it still holds what the walk would have skipped:
|
|
608
|
-
* drop the pruned entries and dereference inner symlinks, which would otherwise
|
|
609
|
-
* point outside the function at runtime. Mirrors the walk's rules exactly, so
|
|
610
|
-
* both paths emit the same tree. Reading directory entries costs nothing next
|
|
611
|
-
* to the per-file copies this replaces.
|
|
612
|
-
*/
|
|
613
|
-
async function normalizeClone(
|
|
614
|
-
dir: string,
|
|
615
|
-
// Where `dir` was cloned from. Carried so the keep-path exemption reads the same here as in the
|
|
616
|
-
// walk: the clone's own paths are inside the function and would never match a source keep path.
|
|
617
|
-
from: string,
|
|
618
|
-
pack: PackRules,
|
|
619
|
-
visited: Set<string>,
|
|
620
|
-
) {
|
|
621
|
-
const stack: [string, string][] = [[dir, from]]
|
|
622
|
-
const pending: Promise<unknown>[] = []
|
|
623
|
-
while (stack.length > 0) {
|
|
624
|
-
const [current, currentFrom] = stack.pop()!
|
|
625
|
-
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
626
|
-
const full = path.join(current, entry.name)
|
|
627
|
-
const source = path.join(currentFrom, entry.name)
|
|
628
|
-
// Matched by name before type, as the walk does: the pages-compat shim
|
|
629
|
-
// links `node_modules` at the app root, and following it would pull the
|
|
630
|
-
// whole store into the function.
|
|
631
|
-
if (!shipsDir(pack, entry.name, source, entry.isSymbolicLink())) {
|
|
632
|
-
rmSync(full, { recursive: true, force: true })
|
|
633
|
-
} else if (entry.isSymbolicLink()) {
|
|
634
|
-
const link = resolveLinkTarget(full)
|
|
635
|
-
rmSync(full, { force: true })
|
|
636
|
-
if (!link) continue
|
|
637
|
-
if (link.isDirectory) {
|
|
638
|
-
if (visited.has(link.path)) continue
|
|
639
|
-
visited.add(link.path)
|
|
640
|
-
pending.push(copyTree(link.path, full, pack, [], visited))
|
|
641
|
-
} else if (shipsFile(pack, entry.name)) {
|
|
642
|
-
pending.push(linkOrCopyFile(link.path, full))
|
|
643
|
-
}
|
|
644
|
-
} else if (entry.isDirectory()) {
|
|
645
|
-
stack.push([full, source])
|
|
646
|
-
} else if (!shipsFile(pack, entry.name)) {
|
|
647
|
-
rmSync(full, { force: true })
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
await Promise.all(pending)
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
interface FrameworkPackage {
|
|
655
|
-
root: string
|
|
656
|
-
packageName: string
|
|
657
|
-
/** Declared runtime dependencies — the only packages it can load eagerly. */
|
|
658
|
-
dependencies: string[]
|
|
659
|
-
/** Lazily loaded feature deps (og, mdx, sass, image optimization). */
|
|
660
|
-
optionalDependencies: string[]
|
|
661
|
-
inWorkspace: boolean
|
|
662
|
-
/** Where the framework ships inside the function, relative to its root. */
|
|
663
|
-
targetRel: string
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// The framework itself ships with the function. Inside a workspace it keeps its
|
|
667
|
-
// workspace-relative location so baked paths line up; when consumed from
|
|
668
|
-
// outside the workspace (e.g. installed from a registry) it lands in the
|
|
669
|
-
// function's node_modules under its package name.
|
|
670
|
-
async function resolveFrameworkPackage(workspaceRoot: string): Promise<FrameworkPackage> {
|
|
671
|
-
const root = path.resolve(import.meta.dirname, '../../..')
|
|
672
|
-
const packageJson = await readPackageJson(root)
|
|
673
|
-
const packageName = packageJson?.name ?? 'pnext'
|
|
674
|
-
const inWorkspace = isInsideDir(workspaceRoot, root) && !root.includes('node_modules')
|
|
675
|
-
return {
|
|
676
|
-
root,
|
|
677
|
-
packageName,
|
|
678
|
-
// Never devDependencies: their tooling (typescript, eslint, …) is
|
|
679
|
-
// reachable from build-time sources only and must not enter the function.
|
|
680
|
-
dependencies: Object.keys(packageJson?.dependencies ?? {}),
|
|
681
|
-
optionalDependencies: Object.keys(packageJson?.optionalDependencies ?? {}),
|
|
682
|
-
inWorkspace,
|
|
683
|
-
targetRel: inWorkspace
|
|
684
|
-
? path.relative(workspaceRoot, root)
|
|
685
|
-
: path.join('node_modules', packageName),
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// Build-time directories a package that publishes its whole tree still keeps.
|
|
690
|
-
const packageCopyExcludes = [
|
|
691
|
-
'node_modules',
|
|
692
|
-
'.cache',
|
|
693
|
-
'.vercel',
|
|
694
|
-
'.next',
|
|
695
|
-
'.pnext',
|
|
696
|
-
'.turbo',
|
|
697
|
-
'test',
|
|
698
|
-
'tests',
|
|
699
|
-
'__tests__',
|
|
700
|
-
]
|
|
701
|
-
|
|
702
|
-
/**
|
|
703
|
-
* Copies a workspace package the way npm would publish it: a `files` list is the package's own
|
|
704
|
-
* statement of what it ships, so it beats guessing at build-time directory names. Glob entries fall
|
|
705
|
-
* back to the exclude list - matching npm's glob semantics is not worth it for a packaging copy.
|
|
706
|
-
*/
|
|
707
|
-
async function copyPackageTree(from: string, to: string, pack: PackRules) {
|
|
708
|
-
const packageJson = await readPackageJson(from)
|
|
709
|
-
const files = packageJson?.files
|
|
710
|
-
if (!files?.length || files.some(entry => /[*?[\]{}!]/.test(entry))) {
|
|
711
|
-
return copyTree(from, to, pack, packageCopyExcludes)
|
|
712
|
-
}
|
|
713
|
-
await mkdir(to, { recursive: true })
|
|
714
|
-
// npm always publishes these two regardless of `files`; the runtime resolves
|
|
715
|
-
// through both (tsconfig for Bun's `paths`/jsx settings). Listed entries need
|
|
716
|
-
// no exclude list — `files` already said what ships — which also lets each
|
|
717
|
-
// directory clone whole.
|
|
718
|
-
await Promise.all(
|
|
719
|
-
[...files, 'package.json', 'tsconfig.json'].map(async name => {
|
|
720
|
-
const source = path.join(from, name)
|
|
721
|
-
const stats = statSync(source, { throwIfNoEntry: false })
|
|
722
|
-
if (!stats) return
|
|
723
|
-
if (stats.isDirectory()) return copyTree(source, path.join(to, name), pack)
|
|
724
|
-
if (!shipsFile(pack, name)) return
|
|
725
|
-
return linkOrCopyFile(source, path.join(to, name))
|
|
726
|
-
}),
|
|
727
|
-
)
|
|
728
|
-
}
|
|
729
|
-
|
|
730
257
|
async function readPackageJson(dir: string) {
|
|
731
|
-
const file = path.join(dir, 'package.json')
|
|
732
|
-
if (!existsSync(file)) return undefined
|
|
733
|
-
return JSON.parse(await readFile(file, 'utf8')) as {
|
|
734
|
-
name?: string
|
|
735
|
-
version?: string
|
|
736
|
-
files?: string[]
|
|
737
|
-
os?: string[]
|
|
738
|
-
cpu?: string[]
|
|
739
|
-
libc?: string[]
|
|
740
|
-
dependencies?: Record<string, string>
|
|
741
|
-
optionalDependencies?: Record<string, string>
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
// The closure hardlinks ~13k files: ~2.9s of a ~3.1s build. Test seam so suites
|
|
746
|
-
// asserting only config shape can skip it. Never off for a real deployment.
|
|
747
|
-
let dependencyClosureEnabled = true
|
|
748
|
-
|
|
749
|
-
/** @internal Test-only. Returns a restore function. */
|
|
750
|
-
export function setDependencyClosureEnabled(enabled: boolean) {
|
|
751
|
-
const previous = dependencyClosureEnabled
|
|
752
|
-
dependencyClosureEnabled = enabled
|
|
753
|
-
return () => {
|
|
754
|
-
dependencyClosureEnabled = previous
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
function skipDependencyClosure() {
|
|
759
|
-
return !dependencyClosureEnabled
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
async function linkOrCopyFile(from: string, to: string) {
|
|
763
258
|
try {
|
|
764
|
-
await
|
|
765
|
-
} catch {
|
|
766
|
-
// Same race as the walk above: a source that vanished mid-copy was never
|
|
767
|
-
// part of the closure, and nothing else here is worth failing the build for.
|
|
768
|
-
await copyFile(from, to).catch(error => {
|
|
769
|
-
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
|
770
|
-
})
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
// Where a symlink really points, and whether that is a directory. Dangling
|
|
775
|
-
// links are ordinary in a real node_modules (removed package, unmet optional
|
|
776
|
-
// dep, interrupted install), so an unreadable target is skipped rather than
|
|
777
|
-
// failing the build.
|
|
778
|
-
function resolveLinkTarget(link: string) {
|
|
779
|
-
const resolved = realDir(link) ?? realFile(link)
|
|
780
|
-
if (!resolved) return undefined
|
|
781
|
-
try {
|
|
782
|
-
return { path: resolved, isDirectory: statSync(resolved).isDirectory() }
|
|
259
|
+
return JSON.parse(await readFile(path.join(dir, 'package.json'), 'utf8')) as PackageJson
|
|
783
260
|
} catch {
|
|
784
261
|
return undefined
|
|
785
262
|
}
|
|
786
263
|
}
|
|
787
264
|
|
|
788
|
-
function realFile(file: string) {
|
|
789
|
-
try {
|
|
790
|
-
return realpathSync(file)
|
|
791
|
-
} catch {
|
|
792
|
-
return undefined
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
function isInsideDir(root: string, file: string) {
|
|
797
|
-
const relative = path.relative(root, file)
|
|
798
|
-
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
/**
|
|
802
|
-
* Whether a compat alias resolves to framework source (react, next/*) rather than to a real package.
|
|
803
|
-
* Containment, not a `node_modules` substring: an installed framework lives under node_modules
|
|
804
|
-
* itself, and a substring test there classifies every next/* shim as a package to ship.
|
|
805
|
-
* @internal Exported for tests.
|
|
806
|
-
*/
|
|
807
|
-
export function isFrameworkOwnedAliasTarget(frameworkRoot: string, target: string) {
|
|
808
|
-
if (!path.isAbsolute(target) || !isInsideDir(frameworkRoot, target)) return false
|
|
809
|
-
// A nested store under the framework root is still a real package.
|
|
810
|
-
return !path.relative(frameworkRoot, target).split(path.sep).includes('node_modules')
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
function frameworkOwnedAliases(config: ResolvedConfig, frameworkRoot: string) {
|
|
814
|
-
return Object.entries(getImportAliasExtensions().aliases(config, 'server')).filter(([, target]) =>
|
|
815
|
-
isFrameworkOwnedAliasTarget(frameworkRoot, target),
|
|
816
|
-
)
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
async function injectCompatPaths(
|
|
820
|
-
config: ResolvedConfig,
|
|
821
|
-
functionPath: string,
|
|
822
|
-
frameworkRoot: string,
|
|
823
|
-
replicaPathFor: (file: string) => string | undefined,
|
|
824
|
-
) {
|
|
825
|
-
const aliasTargets = frameworkOwnedAliases(config, frameworkRoot)
|
|
826
|
-
.map(([specifier, target]) => [specifier, replicaPathFor(target)] as const)
|
|
827
|
-
.filter((entry): entry is readonly [string, string] => Boolean(entry[1]))
|
|
828
|
-
if (aliasTargets.length === 0) return
|
|
829
|
-
|
|
830
|
-
for (const file of walkTsconfigFiles(functionPath)) {
|
|
831
|
-
let parsed: { compilerOptions?: { baseUrl?: string; paths?: Record<string, string[]> } }
|
|
832
|
-
try {
|
|
833
|
-
const source = stripJsonComments(await readFile(file, 'utf8')).replace(/,\s*([}\]])/g, '$1')
|
|
834
|
-
parsed = JSON.parse(source) as typeof parsed
|
|
835
|
-
} catch (error) {
|
|
836
|
-
console.warn(`vercel adapter: could not update ${file}:`, error)
|
|
837
|
-
continue
|
|
838
|
-
}
|
|
839
|
-
const dir = path.dirname(file)
|
|
840
|
-
const compilerOptions = (parsed.compilerOptions ??= {})
|
|
841
|
-
const paths = (compilerOptions.paths ??= {})
|
|
842
|
-
for (const [specifier, target] of aliasTargets) {
|
|
843
|
-
if (paths[specifier]) continue
|
|
844
|
-
const relative = toPosixPath(path.relative(dir, target))
|
|
845
|
-
paths[specifier] = [relative.startsWith('.') ? relative : `./${relative}`]
|
|
846
|
-
}
|
|
847
|
-
compilerOptions.baseUrl ??= '.'
|
|
848
|
-
// The replica shares inodes with the original tsconfig; never write
|
|
849
|
-
// through the hardlink.
|
|
850
|
-
await rm(file)
|
|
851
|
-
await writeFile(file, `${JSON.stringify(parsed, null, 2)}\n`)
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
// Minimal JSONC support for tsconfig files: strips // and /* */ comments
|
|
856
|
-
// outside strings (a "$schema": "https://..." value must survive).
|
|
857
|
-
function stripJsonComments(source: string) {
|
|
858
|
-
let result = ''
|
|
859
|
-
let inString = false
|
|
860
|
-
for (let index = 0; index < source.length; index++) {
|
|
861
|
-
const char = source[index]!
|
|
862
|
-
if (inString) {
|
|
863
|
-
result += char
|
|
864
|
-
if (char === '\\') {
|
|
865
|
-
result += source[++index] ?? ''
|
|
866
|
-
} else if (char === '"') {
|
|
867
|
-
inString = false
|
|
868
|
-
}
|
|
869
|
-
continue
|
|
870
|
-
}
|
|
871
|
-
if (char === '"') {
|
|
872
|
-
inString = true
|
|
873
|
-
result += char
|
|
874
|
-
continue
|
|
875
|
-
}
|
|
876
|
-
if (char === '/' && source[index + 1] === '/') {
|
|
877
|
-
while (index < source.length && source[index] !== '\n') index++
|
|
878
|
-
result += '\n'
|
|
879
|
-
continue
|
|
880
|
-
}
|
|
881
|
-
if (char === '/' && source[index + 1] === '*') {
|
|
882
|
-
index += 2
|
|
883
|
-
while (index < source.length && !(source[index] === '*' && source[index + 1] === '/')) index++
|
|
884
|
-
index++
|
|
885
|
-
continue
|
|
886
|
-
}
|
|
887
|
-
result += char
|
|
888
|
-
}
|
|
889
|
-
return result
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
const bakedPathFileFilter = /\.(?:m?js|cjs|jsx|tsx?|json|html|css)$/
|
|
893
|
-
|
|
894
|
-
// The build output is thousands of small files and only a few hundred carry a
|
|
895
|
-
// baked path, so the pass is io-latency-bound: run it in bounded-concurrency
|
|
896
|
-
// batches instead of one file at a time.
|
|
897
|
-
const BAKED_PATH_CONCURRENCY = 64
|
|
898
|
-
|
|
899
|
-
async function rewriteBakedPaths(
|
|
900
|
-
dir: string,
|
|
901
|
-
replacements: readonly (readonly [string, string])[],
|
|
902
|
-
) {
|
|
903
|
-
if (!existsSync(dir)) return
|
|
904
|
-
const files = (await listFiles(dir)).filter(file => bakedPathFileFilter.test(file))
|
|
905
|
-
for (let i = 0; i < files.length; i += BAKED_PATH_CONCURRENCY) {
|
|
906
|
-
await Promise.all(
|
|
907
|
-
files.slice(i, i + BAKED_PATH_CONCURRENCY).map(async file => {
|
|
908
|
-
const source = await readFile(file, 'utf8')
|
|
909
|
-
let rewritten = source
|
|
910
|
-
for (const [from, to] of replacements) {
|
|
911
|
-
if (rewritten.includes(from)) rewritten = rewritten.replaceAll(from, to)
|
|
912
|
-
}
|
|
913
|
-
if (rewritten === source) return
|
|
914
|
-
// The replica is hardlinked to the real build output; unlink before
|
|
915
|
-
// writing so the rewrite never reaches the original through a shared
|
|
916
|
-
// inode.
|
|
917
|
-
await rm(file)
|
|
918
|
-
await writeFile(file, rewritten)
|
|
919
|
-
}),
|
|
920
|
-
)
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
|
|
924
265
|
/**
|
|
925
|
-
*
|
|
926
|
-
*
|
|
927
|
-
*
|
|
928
|
-
* way, just cheaper when it is there.
|
|
266
|
+
* Native bindings resolved here are built for the build host; the function needs the ones for its
|
|
267
|
+
* own platform. Each is replaced by its owner's build for the function platform, whole: a binding
|
|
268
|
+
* package is its binary.
|
|
929
269
|
*/
|
|
930
|
-
async function
|
|
931
|
-
const
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
// Ship only the node_modules packages the server can actually reach at request time: bare imports
|
|
949
|
-
// traced from the modules the runtime loads, plus the framework's declared runtime dependencies,
|
|
950
|
-
// expanded through their dependency closures. Nothing reachable only from build-time code enters the
|
|
951
|
-
// function - the full monorepo node_modules does not fit Vercel's size limit. Returns the closure
|
|
952
|
-
// plus the deferred copy so the caller can order the io.
|
|
953
|
-
async function traceNodeModulesClosure(
|
|
954
|
-
config: ResolvedConfig,
|
|
955
|
-
manifest: BuildManifest,
|
|
956
|
-
functionPath: string,
|
|
957
|
-
framework: FrameworkPackage,
|
|
958
|
-
warmedModules: string[],
|
|
959
|
-
pack: PackRules,
|
|
960
|
-
log: VerboseLogger,
|
|
961
|
-
) {
|
|
962
|
-
const workspaceRoot = config.workspaceRoot
|
|
963
|
-
const specifiers = new Set<string>()
|
|
964
|
-
const tracedFiles = new Set<string>()
|
|
965
|
-
// Compat-aliased specifiers whose target lives in the framework source
|
|
966
|
-
// (react, next/*) never resolve from node_modules at runtime — shipping
|
|
967
|
-
// them would drag in the full next/react trees. Aliases that point into
|
|
968
|
-
// node_modules (the pinned preact family) still need their packages.
|
|
969
|
-
const frameworkAliasedSpecifiers = new Set(
|
|
970
|
-
frameworkOwnedAliases(config, path.resolve(framework.root)).map(([specifier]) => specifier),
|
|
971
|
-
)
|
|
972
|
-
await log.step('trace runtime imports', async () => {
|
|
973
|
-
// Only code Bun imports raw at request time needs real node_modules: route handlers, the proxy,
|
|
974
|
-
// the config chain, and the compiled modules the warm step just wrote. Pages resolve through that
|
|
975
|
-
// compiled cache, whose externals are vendored, so their dependencies never load raw.
|
|
976
|
-
const queue: string[] = []
|
|
977
|
-
const seen = tracedFiles
|
|
978
|
-
const enqueue = (file: string | undefined) => {
|
|
979
|
-
if (!file) return
|
|
980
|
-
const resolved = path.resolve(file)
|
|
981
|
-
if (seen.has(resolved) || !existsSync(resolved)) return
|
|
982
|
-
seen.add(resolved)
|
|
983
|
-
queue.push(resolved)
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
for (const route of manifest.routes) {
|
|
987
|
-
if (route.kind === 'handler') enqueue(route.file)
|
|
988
|
-
}
|
|
989
|
-
enqueue(findProxyFile(config) ?? undefined)
|
|
990
|
-
enqueue(path.join(config.root, 'pnext.config.ts'))
|
|
991
|
-
enqueue(path.join(config.root, 'next.config.js'))
|
|
992
|
-
for (const file of warmedModules) enqueue(file)
|
|
993
|
-
|
|
994
|
-
const visit = async (file: string) => {
|
|
995
|
-
if (!scriptFilePattern.test(file)) return
|
|
996
|
-
const found = await artifactSpecifiers(file)
|
|
997
|
-
if (found === null) return
|
|
998
|
-
for (const specifier of found) {
|
|
999
|
-
if (frameworkAliasedSpecifiers.has(specifier)) continue
|
|
1000
|
-
// Compiled modules import each other — and whatever the compiler left
|
|
1001
|
-
// external — by absolute href. Follow the ones inside the build output
|
|
1002
|
-
// and ship the packages the rest point into.
|
|
1003
|
-
const absolute = absolutePathFromSpecifier(specifier)
|
|
1004
|
-
if (absolute) {
|
|
1005
|
-
const name = packageNameFromPath(absolute)
|
|
1006
|
-
if (name) specifiers.add(name)
|
|
1007
|
-
else if (isInsideDir(config.outPath, absolute)) enqueue(absolute)
|
|
1008
|
-
continue
|
|
1009
|
-
}
|
|
1010
|
-
if (/^(?:node:|bun$|bun:|data:)/.test(specifier)) continue
|
|
1011
|
-
// resolveImport covers relative paths, tsconfig aliases, package imports and workspace
|
|
1012
|
-
// packages - workspace source gets traced further. Bare package specifiers are also recorded
|
|
1013
|
-
// so the closure ships (or workspace-links) their node_modules entries; workspace packages
|
|
1014
|
-
// need the link even when their source is traced.
|
|
1015
|
-
const resolved = resolveImport(config.root, file, specifier, config.workspaceRoot)
|
|
1016
|
-
if (
|
|
1017
|
-
resolved &&
|
|
1018
|
-
isInsideDir(workspaceRoot, resolved) &&
|
|
1019
|
-
!resolved.includes('node_modules')
|
|
1020
|
-
) {
|
|
1021
|
-
enqueue(resolved)
|
|
1022
|
-
}
|
|
1023
|
-
const name = packageNameFromSpecifier(specifier)
|
|
1024
|
-
if (name) specifiers.add(name)
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
// A level's reads are independent, so the walk advances a level at a time, bounded so a wide
|
|
1029
|
-
// graph cannot exhaust the fd table.
|
|
1030
|
-
const READ_BATCH = 64
|
|
1031
|
-
for (let frontier = queue.splice(0); frontier.length > 0; frontier = queue.splice(0)) {
|
|
1032
|
-
for (let index = 0; index < frontier.length; index += READ_BATCH) {
|
|
1033
|
-
await Promise.all(frontier.slice(index, index + READ_BATCH).map(visit))
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
log.log(`traced ${seen.size} runtime modules`)
|
|
1037
|
-
})
|
|
1038
|
-
|
|
1039
|
-
const packageDirs = new Map<string, string>()
|
|
1040
|
-
const workspaceLinks = new Map<string, string>()
|
|
1041
|
-
// Native bindings resolved here are built for the build host; the function
|
|
1042
|
-
// needs the ones for its own platform, fetched below.
|
|
1043
|
-
const hostNatives = new Map<string, { dir: string; owner?: PackageVersion }>()
|
|
1044
|
-
// The framework ships whole, so what it can load at runtime is exactly what it declares as a
|
|
1045
|
-
// runtime dependency - resolved from its own root, which is not the app's when it comes from a
|
|
1046
|
-
// registry. Its optional deps back compat features that only load when the app uses them, so they
|
|
1047
|
-
// ship only where the app declares them too.
|
|
1048
|
-
const appDependencies = await declaredDependencies(config.root)
|
|
1049
|
-
const queue: { name: string; from: string; owner?: PackageVersion }[] = [
|
|
1050
|
-
...framework.dependencies.map(name => ({ name, from: framework.root })),
|
|
1051
|
-
...framework.optionalDependencies
|
|
1052
|
-
.filter(name => appDependencies.has(name))
|
|
1053
|
-
.map(name => ({ name, from: framework.root })),
|
|
1054
|
-
...[...specifiers].map(name => ({ name, from: workspaceRoot })),
|
|
1055
|
-
]
|
|
1056
|
-
const seen = new Set<string>()
|
|
1057
|
-
|
|
1058
|
-
while (queue.length > 0) {
|
|
1059
|
-
const { name, from, owner } = queue.shift()!
|
|
1060
|
-
if (seen.has(name)) continue
|
|
1061
|
-
seen.add(name)
|
|
1062
|
-
// The framework is copied to its own location by the caller; copying it
|
|
1063
|
-
// again under its package name collides with that (and with the workspace
|
|
1064
|
-
// link it needs when it lives in the workspace).
|
|
1065
|
-
if (name === framework.packageName) {
|
|
1066
|
-
if (framework.inWorkspace) workspaceLinks.set(name, framework.targetRel)
|
|
1067
|
-
continue
|
|
1068
|
-
}
|
|
1069
|
-
const dir = resolvePackageDir(name, [from, workspaceRoot, config.root])
|
|
1070
|
-
if (!dir) continue
|
|
1071
|
-
|
|
1072
|
-
// Workspace packages ship at their workspace-relative path; link them so
|
|
1073
|
-
// bare imports resolve to the same real path the compiled cache uses.
|
|
1074
|
-
const workspaceRelative = path.relative(workspaceRoot, dir)
|
|
1075
|
-
if (!workspaceRelative.startsWith('..') && !workspaceRelative.startsWith('node_modules')) {
|
|
1076
|
-
workspaceLinks.set(name, workspaceRelative)
|
|
1077
|
-
continue
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
const packageJson = await readPackageJson(dir)
|
|
1081
|
-
// `os`/`cpu` are npm's own declaration that a package is platform-bound;
|
|
1082
|
-
// one that already fits the function's platform ships as-is.
|
|
1083
|
-
const platformBound = Boolean(packageJson?.os ?? packageJson?.cpu)
|
|
1084
|
-
if (packageJson && platformBound && !matchesFunctionPlatform(packageJson)) {
|
|
1085
|
-
hostNatives.set(name, { dir, owner })
|
|
1086
|
-
continue
|
|
1087
|
-
}
|
|
1088
|
-
packageDirs.set(name, dir)
|
|
1089
|
-
if (!packageJson) continue
|
|
1090
|
-
const self = { name, version: packageJson.version ?? '' }
|
|
1091
|
-
// Peer dependencies are deliberately not expanded — they fan out to whole
|
|
1092
|
-
// ecosystems (react, next, ...) that are only shipped when something
|
|
1093
|
-
// actually imports them.
|
|
1094
|
-
for (const dependency of [
|
|
1095
|
-
...Object.keys(packageJson.dependencies ?? {}),
|
|
1096
|
-
...Object.keys(packageJson.optionalDependencies ?? {}),
|
|
1097
|
-
]) {
|
|
1098
|
-
queue.push({ name: dependency, from: dir, owner: self })
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
if (hostNatives.size > 0 && !skipDependencyClosure()) {
|
|
1103
|
-
const targets = await log.step(
|
|
1104
|
-
`resolve ${FUNCTION_PLATFORM.os}-${FUNCTION_PLATFORM.cpu} natives (${hostNatives.size} packages)`,
|
|
1105
|
-
() => platformNativePackages(hostNatives, log),
|
|
1106
|
-
)
|
|
1107
|
-
for (const [name, dir] of targets) packageDirs.set(name, dir)
|
|
270
|
+
async function replaceHostNatives(packages: Map<string, string[]>, log: VerboseLogger) {
|
|
271
|
+
const manifests = new Map<string, PackageJson>()
|
|
272
|
+
for (const [name, dirs] of packages) {
|
|
273
|
+
const json = await readPackageJson(dirs[0]!)
|
|
274
|
+
if (json) manifests.set(name, json)
|
|
275
|
+
}
|
|
276
|
+
const hostNatives = new Map<
|
|
277
|
+
string,
|
|
278
|
+
{ dirs: string[]; owner?: { name: string; version: string } }
|
|
279
|
+
>()
|
|
280
|
+
for (const [name, json] of manifests) {
|
|
281
|
+
if (!(json.os ?? json.cpu) || matchesFunctionPlatform(json)) continue
|
|
282
|
+
const owner = [...manifests].find(([, candidate]) => candidate.optionalDependencies?.[name])
|
|
283
|
+
hostNatives.set(name, {
|
|
284
|
+
dirs: packages.get(name)!,
|
|
285
|
+
owner: owner && { name: owner[0], version: owner[1].version ?? '' },
|
|
286
|
+
})
|
|
1108
287
|
}
|
|
1109
|
-
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1112
|
-
const
|
|
1113
|
-
|
|
1114
|
-
await
|
|
1115
|
-
|
|
1116
|
-
const target = path.join(functionPath, 'node_modules', name)
|
|
1117
|
-
await mkdir(path.dirname(target), { recursive: true })
|
|
1118
|
-
await copyTree(dir, target, pack)
|
|
1119
|
-
}),
|
|
1120
|
-
)
|
|
288
|
+
if (hostNatives.size === 0) return
|
|
289
|
+
const targets = await platformNativePackages(hostNatives, manifests, log)
|
|
290
|
+
for (const [name, dir] of targets) {
|
|
291
|
+
for (const host of hostNatives.get(name)?.dirs ?? []) {
|
|
292
|
+
const target = path.join(path.dirname(host), name.split('/').at(-1)!)
|
|
293
|
+
await rm(host, { recursive: true, force: true })
|
|
294
|
+
await cp(dir, target, { recursive: true, dereference: true })
|
|
1121
295
|
}
|
|
1122
|
-
for (const [name, workspaceRelative] of workspaceLinks) {
|
|
1123
|
-
const linkPath = path.join(functionPath, 'node_modules', name)
|
|
1124
|
-
await mkdir(path.dirname(linkPath), { recursive: true })
|
|
1125
|
-
const target = path.relative(
|
|
1126
|
-
path.dirname(linkPath),
|
|
1127
|
-
path.join(functionPath, workspaceRelative),
|
|
1128
|
-
)
|
|
1129
|
-
if (!existsSync(linkPath)) await symlink(target, linkPath, 'dir')
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
return {
|
|
1134
|
-
tracedFiles,
|
|
1135
|
-
workspacePackages: [...workspaceLinks.values()],
|
|
1136
|
-
packageCount: packageDirs.size,
|
|
1137
|
-
copy,
|
|
1138
296
|
}
|
|
1139
297
|
}
|
|
1140
298
|
|
|
1141
|
-
interface PackageVersion {
|
|
1142
|
-
name: string
|
|
1143
|
-
version: string
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
299
|
/**
|
|
1147
|
-
*
|
|
1148
|
-
*
|
|
1149
|
-
*
|
|
1150
|
-
*
|
|
1151
|
-
* unavailable (offline CI, private registry), so packaging never hard-fails on a network hiccup.
|
|
300
|
+
* The function platform's builds, from the registry: one cross-platform install per distinct set of
|
|
301
|
+
* owning packages, cached by content in the user cache dir. Falls back to the host copies when the
|
|
302
|
+
* install is unavailable (offline CI, private registry), so packaging never hard-fails on a network
|
|
303
|
+
* hiccup.
|
|
1152
304
|
*/
|
|
1153
305
|
async function platformNativePackages(
|
|
1154
|
-
hostNatives: Map<string, {
|
|
306
|
+
hostNatives: Map<string, { dirs: string[]; owner?: { name: string; version: string } }>,
|
|
307
|
+
manifests: Map<string, PackageJson>,
|
|
1155
308
|
log: VerboseLogger,
|
|
1156
309
|
) {
|
|
1157
|
-
// Install the package that *owns* the binding: only it lists every
|
|
1158
|
-
// platform's build in its optional deps.
|
|
1159
310
|
const owners = new Map<string, string>()
|
|
1160
|
-
for (const [name, {
|
|
1161
|
-
const spec = owner ?? { name, version: (
|
|
311
|
+
for (const [name, { owner }] of hostNatives) {
|
|
312
|
+
const spec = owner ?? { name, version: manifests.get(name)?.version ?? '' }
|
|
1162
313
|
if (spec.version) owners.set(spec.name, spec.version)
|
|
1163
314
|
}
|
|
1164
315
|
const specs = [...owners].map(([name, version]) => `${name}@${version}`).sort()
|
|
1165
|
-
|
|
1166
|
-
if (specs.length === 0) return fallback()
|
|
1167
|
-
|
|
316
|
+
if (specs.length === 0) return new Map<string, string>()
|
|
1168
317
|
try {
|
|
1169
318
|
const installed = await crossPlatformInstall(specs)
|
|
1170
319
|
const matches = new Map<string, string>()
|
|
1171
320
|
for (const [name, dir] of installed) {
|
|
1172
|
-
const
|
|
1173
|
-
if (
|
|
321
|
+
const json = await readPackageJson(dir)
|
|
322
|
+
if (json && (json.os ?? json.cpu) && matchesFunctionPlatform(json)) matches.set(name, dir)
|
|
1174
323
|
}
|
|
1175
|
-
if (matches.size === 0)
|
|
324
|
+
if (matches.size === 0)
|
|
1176
325
|
throw new Error(`no ${FUNCTION_PLATFORM.os} builds in ${specs.join(' ')}`)
|
|
1177
|
-
}
|
|
1178
326
|
return matches
|
|
1179
327
|
} catch (error) {
|
|
1180
328
|
console.warn(
|
|
@@ -1183,7 +331,7 @@ async function platformNativePackages(
|
|
|
1183
331
|
error,
|
|
1184
332
|
)
|
|
1185
333
|
log.log('falling back to host native packages')
|
|
1186
|
-
return
|
|
334
|
+
return new Map<string, string>()
|
|
1187
335
|
}
|
|
1188
336
|
}
|
|
1189
337
|
|
|
@@ -1259,88 +407,6 @@ async function crossPlatformInstall(specs: string[]) {
|
|
|
1259
407
|
return installed
|
|
1260
408
|
}
|
|
1261
409
|
|
|
1262
|
-
const scriptFilePattern = /\.(?:m?js|cjs|jsx|tsx?)$/
|
|
1263
|
-
|
|
1264
|
-
// Directories the shipped tsconfigs can never live in; pruned during the walk —
|
|
1265
|
-
// a naive recursive listing would crawl the function's own node_modules.
|
|
1266
|
-
const skippedScanDirs = new Set(['node_modules', '.git', '.next', '.pnext', '.turbo', '.vercel'])
|
|
1267
|
-
|
|
1268
|
-
function* walkTsconfigFiles(root: string): Generator<string> {
|
|
1269
|
-
const stack = [root]
|
|
1270
|
-
while (stack.length > 0) {
|
|
1271
|
-
const dir = stack.pop()!
|
|
1272
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1273
|
-
const full = path.join(dir, entry.name)
|
|
1274
|
-
if (entry.isDirectory()) {
|
|
1275
|
-
if (!skippedScanDirs.has(entry.name)) stack.push(full)
|
|
1276
|
-
} else if (entry.isFile() && entry.name === 'tsconfig.json') {
|
|
1277
|
-
yield full
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
/** Every dependency name an app's own package.json declares. */
|
|
1284
|
-
async function declaredDependencies(root: string) {
|
|
1285
|
-
const packageJson = (await readPackageJson(root)) as
|
|
1286
|
-
Record<string, Record<string, string> | undefined> | undefined
|
|
1287
|
-
return new Set(
|
|
1288
|
-
['dependencies', 'optionalDependencies', 'devDependencies'].flatMap(field =>
|
|
1289
|
-
Object.keys(packageJson?.[field] ?? {}),
|
|
1290
|
-
),
|
|
1291
|
-
)
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
/** Absolute path behind an `import`, whether written as a path or a file URL. */
|
|
1295
|
-
function absolutePathFromSpecifier(specifier: string) {
|
|
1296
|
-
if (specifier.startsWith('file://')) return fileURLToPath(specifier)
|
|
1297
|
-
return path.isAbsolute(specifier) ? specifier : undefined
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
/** The package an absolute path belongs to, if it points inside node_modules. */
|
|
1301
|
-
function packageNameFromPath(file: string) {
|
|
1302
|
-
const parts = toPosixPath(file).split('/node_modules/')
|
|
1303
|
-
const tail = parts.at(-1)
|
|
1304
|
-
if (parts.length < 2 || !tail) return undefined
|
|
1305
|
-
const segments = tail.split('/')
|
|
1306
|
-
return segments[0]?.startsWith('@') ? segments.slice(0, 2).join('/') : segments[0]
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
function packageNameFromSpecifier(specifier: string) {
|
|
1310
|
-
if (/^[./#]|^file:|^node:|^data:/.test(specifier)) return undefined
|
|
1311
|
-
if (specifier === 'bun' || specifier.startsWith('bun:')) return undefined
|
|
1312
|
-
if (specifier.includes(':')) return undefined
|
|
1313
|
-
// Scoped names need a real scope — `@/env`-style tsconfig aliases are not
|
|
1314
|
-
// packages.
|
|
1315
|
-
if (specifier.startsWith('@') && !/^@[^/]+\/[^/]+/.test(specifier)) return undefined
|
|
1316
|
-
const parts = specifier.split('/')
|
|
1317
|
-
const name = specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]
|
|
1318
|
-
if (!name || builtinModules.includes(name)) return undefined
|
|
1319
|
-
return name
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
function resolvePackageDir(name: string, fromDirs: string[]) {
|
|
1323
|
-
for (const from of fromDirs) {
|
|
1324
|
-
let dir = path.resolve(from)
|
|
1325
|
-
while (true) {
|
|
1326
|
-
const candidate = path.join(dir, 'node_modules', name)
|
|
1327
|
-
if (existsSync(path.join(candidate, 'package.json'))) return realDir(candidate)
|
|
1328
|
-
const parent = path.dirname(dir)
|
|
1329
|
-
if (parent === dir) break
|
|
1330
|
-
dir = parent
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
return undefined
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
function realDir(dir: string) {
|
|
1337
|
-
try {
|
|
1338
|
-
return statSync(dir).isDirectory() ? realpathSync(dir) : undefined
|
|
1339
|
-
} catch {
|
|
1340
|
-
return undefined
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
410
|
async function copyStaticFiles(
|
|
1345
411
|
from: string,
|
|
1346
412
|
to: string,
|
|
@@ -1355,7 +421,9 @@ async function copyStaticFiles(
|
|
|
1355
421
|
)
|
|
1356
422
|
await Promise.all([...directories].map(dir => mkdir(dir, { recursive: true })))
|
|
1357
423
|
await Promise.all(
|
|
1358
|
-
files.map(file =>
|
|
424
|
+
files.map(file =>
|
|
425
|
+
copyFile(file, path.join(to, path.relative(from, file)), constants.COPYFILE_FICLONE),
|
|
426
|
+
),
|
|
1359
427
|
)
|
|
1360
428
|
}
|
|
1361
429
|
|
|
@@ -1379,14 +447,50 @@ async function staticOverrides(
|
|
|
1379
447
|
return overrides
|
|
1380
448
|
}
|
|
1381
449
|
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
const
|
|
1385
|
-
|
|
1386
|
-
|
|
450
|
+
// `important` lets route headers win over the CDN's own static-file cache-control, as Next's builder does.
|
|
451
|
+
async function immutableAssetRoutes(publicDir: string, publicAssets: string[] = []) {
|
|
452
|
+
const app = new Set(publicAssets)
|
|
453
|
+
const built =
|
|
454
|
+
publicAssets.length > 0
|
|
455
|
+
? (await listFiles(publicDir))
|
|
456
|
+
.map(file => toPosixPath(path.relative(publicDir, file)))
|
|
457
|
+
.filter(relative => !app.has(relative) && immutableAssetPath(relative))
|
|
458
|
+
: []
|
|
459
|
+
const patterns = immutableAssetPatterns(built, publicAssets)
|
|
460
|
+
return routeSources(patterns, source => `^/(?:${source})`).map(src => ({
|
|
461
|
+
src,
|
|
1387
462
|
headers: { 'cache-control': immutableCacheControl },
|
|
1388
463
|
continue: true,
|
|
464
|
+
important: true,
|
|
465
|
+
}))
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Whole immutable prefixes that hold no app public file; under the others, each build file collapsed
|
|
470
|
+
* to its topmost folder below the prefix that holds no app public file, else its exact name.
|
|
471
|
+
*/
|
|
472
|
+
function immutableAssetPatterns(built: string[], publicAssets: string[]) {
|
|
473
|
+
const appDirs = new Set(publicAssets.flatMap(ancestorDirs))
|
|
474
|
+
const prefixes = [...new Set(immutableAssetPrefixes())]
|
|
475
|
+
const patterns = prefixes.filter(prefix => !appDirs.has(prefix)).map(escapeRegex)
|
|
476
|
+
for (const relative of built) {
|
|
477
|
+
const prefix = prefixes.find(prefix => relative.startsWith(prefix))
|
|
478
|
+
if (!prefix || !appDirs.has(prefix)) continue
|
|
479
|
+
const dir = ancestorDirs(relative).find(dir => dir.length > prefix.length && !appDirs.has(dir))
|
|
480
|
+
patterns.push(dir ? escapeRegex(requestPath(dir)) : `${escapeRegex(requestPath(relative))}$`)
|
|
1389
481
|
}
|
|
482
|
+
return [...new Set(patterns)]
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** A public-relative path as browsers request it (the WHATWG path percent-encode set). */
|
|
486
|
+
function requestPath(relative: string) {
|
|
487
|
+
return relative.replace(/[\0-\x20"#<>?`{}\x7f-\u{10ffff}]/gu, char => encodeURIComponent(char))
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** `a/b/c.js` -> `['a/', 'a/b/']`. */
|
|
491
|
+
function ancestorDirs(relative: string) {
|
|
492
|
+
const parts = relative.split('/').slice(0, -1)
|
|
493
|
+
return parts.map((_, index) => `${parts.slice(0, index + 1).join('/')}/`)
|
|
1390
494
|
}
|
|
1391
495
|
|
|
1392
496
|
// Headers a CDN route cannot replay; a file that sets one stays on the server function.
|
|
@@ -1434,23 +538,29 @@ function staticHeaderRoutes(staticFiles: Record<string, StaticFileMetadata>) {
|
|
|
1434
538
|
groups.set(key, group)
|
|
1435
539
|
group.patterns.push(servedPathPattern(relative))
|
|
1436
540
|
}
|
|
1437
|
-
return [...groups.values()].flatMap(({ headers, patterns }) =>
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
const last = sources.length - 1
|
|
1441
|
-
if (last >= 0 && sources[last]!.length + pattern.length < maxRouteSourceLength) {
|
|
1442
|
-
sources[last] += `|${pattern}`
|
|
1443
|
-
} else {
|
|
1444
|
-
sources.push(pattern)
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
return sources.map(source => ({
|
|
1448
|
-
src: `^/(?:${source})$`,
|
|
541
|
+
return [...groups.values()].flatMap(({ headers, patterns }) =>
|
|
542
|
+
routeSources(patterns, source => `^/(?:${source})$`).map(src => ({
|
|
543
|
+
src,
|
|
1449
544
|
methods: ['GET', 'HEAD'],
|
|
1450
545
|
headers,
|
|
1451
546
|
continue: true,
|
|
1452
|
-
|
|
1453
|
-
|
|
547
|
+
important: true,
|
|
548
|
+
})),
|
|
549
|
+
)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** Joins patterns into as few `wrap`ped route sources as fit `maxRouteSourceLength`. */
|
|
553
|
+
function routeSources(patterns: string[], wrap: (alternation: string) => string) {
|
|
554
|
+
const sources: string[] = []
|
|
555
|
+
for (const pattern of patterns) {
|
|
556
|
+
const last = sources.length - 1
|
|
557
|
+
if (last >= 0 && wrap(`${sources[last]}|${pattern}`).length <= maxRouteSourceLength) {
|
|
558
|
+
sources[last] += `|${pattern}`
|
|
559
|
+
} else {
|
|
560
|
+
sources.push(pattern)
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return sources.map(wrap)
|
|
1454
564
|
}
|
|
1455
565
|
|
|
1456
566
|
/** A static file's request paths: `a/index.html` also answers `/a` and `/a/`. */
|