@wular/pnext 0.0.4 → 0.0.6
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 +11 -11
- package/bin/pnext +1 -1
- package/config/lint/base.js +7 -7
- package/config/ts/base.json +2 -4
- package/config/ts/react.json +2 -6
- package/package.json +23 -2
- package/reference/compat.md +1 -1
- package/reference/config.md +2 -2
- package/reference/css.md +13 -9
- package/reference/dev.md +1 -1
- package/reference/metadata.md +7 -7
- package/reference/navigation.md +35 -28
- package/reference/performance.md +70 -70
- package/reference/rendering.md +17 -17
- package/reference/routing.md +17 -17
- package/reference/typegen.md +12 -8
- package/src/api/cache.ts +36 -37
- package/src/api/client-cache.ts +2 -2
- package/src/api/client-navigation.ts +113 -109
- package/src/api/dynamic.tsx +58 -55
- package/src/api/link.tsx +49 -52
- package/src/api/navigation.ts +59 -58
- package/src/api/server.ts +152 -144
- package/src/api/suspense.ts +4 -4
- package/src/cli/adapters/vercel-warm.ts +126 -121
- package/src/cli/adapters/vercel.ts +437 -443
- package/src/cli/analyze.ts +333 -144
- package/src/cli/{named-bin.ts → boot/named-bin.ts} +37 -37
- package/src/cli/{boot-trace.ts → boot/trace.ts} +10 -10
- package/src/cli/build.ts +976 -978
- package/src/cli/create.ts +71 -63
- package/src/cli/dev.ts +116 -116
- package/src/cli/index.ts +88 -85
- package/src/cli/migrate/package-json.ts +73 -71
- package/src/cli/migrate/report.ts +32 -33
- package/src/cli/migrate/{index.ts → run.ts} +48 -49
- package/src/cli/migrate/scan.ts +46 -46
- package/src/cli/migrate/spinner.ts +9 -9
- package/src/cli/migrate/tsconfig.ts +35 -35
- package/src/cli/{server-entry.ts → serve/entry.ts} +57 -57
- package/src/cli/{request-pipeline.ts → serve/pipeline.ts} +386 -394
- package/src/cli/{serve-ui.ts → serve/ui.ts} +47 -47
- package/src/cli/start.ts +65 -66
- package/src/{typegen.ts → cli/typegen.ts} +59 -59
- package/src/client/build.ts +794 -768
- package/src/client/chunk-fold.ts +245 -240
- package/src/client/{paths.ts → chunk-name.ts} +6 -6
- package/src/client/entry.ts +162 -147
- package/src/client/prebuilt.ts +231 -223
- package/src/client/profile.ts +29 -29
- package/src/client/react-compiler.ts +20 -15
- package/src/client/{compat-surface.ts → react-tier.ts} +64 -64
- package/src/client/reference-stub.ts +51 -51
- package/src/client/reference.ts +19 -19
- package/src/{api → client}/router/events.ts +17 -17
- package/src/{api → client}/router/history.ts +16 -16
- package/src/{api → client}/router/hub.ts +52 -53
- package/src/{api/router.ts → client/router/index.ts} +61 -60
- package/src/{api → client}/router/policies.ts +24 -24
- package/src/{api → client}/router/runtime.ts +1988 -1992
- package/src/{api → client}/router/types.ts +93 -98
- package/src/compat/actions/client-plugin.ts +42 -43
- package/src/compat/actions/client-stub.ts +14 -14
- package/src/compat/actions/{action-client.ts → client.ts} +194 -193
- package/src/compat/actions/config.ts +63 -64
- package/src/compat/actions/detect.ts +68 -68
- package/src/compat/actions/discovery.ts +105 -105
- package/src/compat/actions/{action-dispatch.ts → dispatch.ts} +277 -274
- package/src/compat/actions/early-submit.ts +1 -1
- package/src/compat/actions/endpoint.ts +198 -198
- package/src/compat/actions/extensions.ts +811 -0
- package/src/compat/actions/flight.ts +23 -23
- package/src/compat/actions/form-state.ts +34 -34
- package/src/compat/actions/hoist.ts +382 -236
- package/src/compat/actions/ids.ts +7 -12
- package/src/compat/actions/index.ts +8 -8
- package/src/compat/actions/instances.ts +46 -46
- package/src/compat/actions/origin.ts +47 -48
- package/src/compat/actions/protocol.ts +22 -22
- package/src/compat/actions/registry.ts +19 -19
- package/src/compat/{misc/action-return.ts → actions/return.ts} +60 -57
- package/src/compat/actions/rewrite.ts +125 -125
- package/src/compat/actions/{action-router.ts → router.ts} +10 -10
- package/src/compat/actions/serve.ts +132 -136
- package/src/compat/actions/server-tag.ts +4 -4
- package/src/compat/actions/{action-shared.ts → shared.ts} +34 -34
- package/src/compat/actions/unrecognized-error.ts +4 -4
- package/src/compat/{index.ts → aliases.ts} +112 -109
- package/src/compat/bundler/bun-externals.ts +18 -18
- package/src/compat/bundler/cjs-exports.ts +271 -223
- package/src/compat/bundler/config.ts +116 -111
- package/src/compat/bundler/externals.ts +12 -12
- package/src/compat/bundler/import-meta-url.ts +19 -19
- package/src/compat/bundler/modularize-imports.ts +36 -33
- package/src/compat/bundler/new-url-asset.ts +22 -24
- package/src/compat/bundler/optimize-package-imports.ts +111 -107
- package/src/compat/bundler/polyfill.ts +28 -28
- package/src/compat/bundler/react-compiler.ts +35 -29
- package/src/compat/bundler/react-profiler.tsx +11 -11
- package/src/compat/bundler/relay-transform.ts +48 -47
- package/src/compat/bundler/require-context.ts +119 -113
- package/src/compat/bundler/resolve-extensions.ts +19 -19
- package/src/compat/bundler/source-cache.ts +25 -25
- package/src/compat/bundler/static-imports.ts +7 -7
- package/src/compat/bundler/symlink-imports.ts +46 -46
- package/src/compat/bundler/tsconfig-paths.ts +13 -15
- package/src/compat/bundler/wasm.ts +58 -60
- package/src/compat/bundler/webpack-loaders.ts +254 -241
- package/src/compat/bundler/worker.ts +101 -104
- package/src/compat/cache/build-flags.ts +29 -29
- package/src/compat/cache/build-prerender-errors.ts +40 -44
- package/src/compat/cache/custom-handler.ts +60 -53
- package/src/compat/cache/fetch-patch.ts +240 -240
- package/src/compat/cache/handler.ts +27 -31
- package/src/compat/cache/modern-handler.ts +148 -126
- package/src/compat/cache/resume-data-cache.ts +47 -45
- package/src/compat/cache/revalidate.ts +233 -232
- package/src/compat/cache/runtime-error.ts +35 -35
- package/src/compat/cache/use-cache-transform.ts +442 -417
- package/src/compat/cache/use-cache.ts +650 -614
- package/src/compat/cache-control.ts +140 -142
- package/src/compat/client/base-path.ts +21 -20
- package/src/compat/client/css-order.ts +18 -18
- package/src/compat/client/errors/bare-boundary.ts +11 -11
- package/src/compat/client/errors/control-flow.ts +23 -23
- package/src/compat/client/errors/error-boundary.ts +61 -61
- package/src/compat/client/errors/global-error.ts +101 -91
- package/src/compat/client/errors/install.ts +71 -72
- package/src/compat/client/errors/lazy.ts +23 -23
- package/src/compat/client/errors/primitive-throw.ts +47 -45
- package/src/compat/client/errors/soft-refresh.ts +4 -4
- package/src/compat/client/link-status.ts +33 -33
- package/src/compat/client/{nav-compat-runtime.ts → nav-runtime.ts} +20 -20
- package/src/compat/client/{nav-compat.ts → nav.ts} +12 -13
- package/src/compat/client/navigation-scroll.ts +63 -63
- package/src/compat/client/optimistic-routing.ts +93 -88
- package/src/compat/client/prefetch-cache.ts +23 -24
- package/src/compat/client/route-announcer.ts +35 -35
- package/src/compat/client/segment-cache-policy.ts +20 -20
- package/src/compat/client/segment-cache.ts +309 -315
- package/src/compat/client/segment-prefetch.ts +127 -132
- package/src/compat/client/trailing-slash.ts +5 -4
- package/src/compat/css/chunking.ts +113 -116
- package/src/compat/css/inline-css.ts +21 -21
- package/src/compat/css/lightningcss.ts +37 -38
- package/src/compat/css/modules.ts +161 -175
- package/src/compat/css/nonce.ts +7 -7
- package/src/compat/css/sass-plugin.ts +18 -21
- package/src/compat/css/sass.ts +150 -152
- package/src/compat/css/styled-jsx-runtime.ts +27 -27
- package/src/compat/css/styled-jsx.ts +21 -21
- package/src/compat/edge-runtime.ts +27 -27
- package/src/compat/{adapter → export}/build-complete.ts +72 -75
- package/src/compat/export/client.ts +29 -31
- package/src/compat/export/{index.ts → emit.ts} +111 -110
- package/src/compat/export/standalone.ts +62 -54
- package/src/compat/image-optimizer/cache.ts +51 -49
- package/src/compat/image-optimizer/detect.ts +52 -52
- package/src/compat/image-optimizer/{index.ts → optimize.ts} +189 -194
- package/src/compat/image-optimizer/source.ts +77 -80
- package/src/compat/lifecycle/after-scope.ts +26 -26
- package/src/compat/lifecycle/after.ts +48 -45
- package/src/compat/lifecycle/error-funnel.ts +98 -102
- package/src/compat/lifecycle/error-serialize.ts +30 -26
- package/src/compat/lifecycle/error-ui.ts +67 -32
- package/src/compat/lifecycle/instrumentation-client.ts +36 -38
- package/src/compat/lifecycle/instrumentation.ts +85 -85
- package/src/compat/lifecycle/node-console.ts +11 -11
- package/src/compat/lifecycle/testmode.ts +160 -132
- package/src/compat/mdx/compile.ts +60 -57
- package/src/compat/mdx/plugin.ts +11 -11
- package/src/compat/mdx/{next-mdx-stub.ts → stub.ts} +5 -5
- package/src/compat/{metadata-route-artifacts.ts → metadata-artifacts.ts} +195 -191
- package/src/compat/metadata.ts +75 -81
- package/src/compat/next/cache.ts +68 -69
- package/src/compat/next/canonical-url.ts +9 -9
- package/src/compat/next/client-cache.ts +19 -19
- package/src/compat/next/client-navigation.ts +112 -116
- package/src/compat/next/client-only.ts +1 -1
- package/src/compat/next/client-script.tsx +99 -93
- package/src/compat/next/client-server.ts +10 -10
- package/src/compat/next/config-loader.ts +176 -173
- package/src/compat/next/config.ts +7 -7
- package/src/compat/next/constants.cjs +6 -6
- package/src/compat/next/constants.ts +6 -6
- package/src/compat/next/custom-server.ts +26 -24
- package/src/compat/next/dist/client/components/app-router-headers.ts +21 -21
- package/src/compat/next/dist/server/app-render/work-unit-async-storage.external.cjs +3 -4
- package/src/compat/next/dist/server/web/spec-extension/revalidate.ts +1 -1
- package/src/compat/next/dist/server/web/spec-extension/unstable-cache.ts +1 -1
- package/src/compat/next/dist/server/web/spec-extension/unstable-no-store.ts +1 -1
- package/src/compat/next/dynamic.tsx +21 -18
- package/src/compat/next/error.tsx +52 -52
- package/src/compat/next/font/cache.ts +74 -74
- package/src/compat/next/font/google.ts +2 -2
- package/src/compat/next/font/index.ts +1 -1
- package/src/compat/next/font/local.ts +3 -3
- package/src/compat/next/font/runtime-client.ts +17 -15
- package/src/compat/next/font/runtime.ts +443 -408
- package/src/compat/next/font/shared.ts +120 -108
- package/src/compat/next/form.tsx +63 -63
- package/src/compat/next/head.tsx +2 -2
- package/src/compat/next/headers.ts +103 -95
- package/src/compat/next/image/client.tsx +220 -0
- package/src/compat/next/image/config.ts +56 -58
- package/src/compat/next/image/optimizer.ts +40 -36
- package/src/compat/next/image/patterns.ts +37 -40
- package/src/compat/next/{image-props.ts → image/props.ts} +208 -202
- package/src/compat/next/image/shared.ts +65 -57
- package/src/compat/next/image/static-metadata.ts +98 -107
- package/src/compat/next/image/validate.ts +79 -88
- package/src/compat/next/image.tsx +19 -23
- package/src/compat/next/index.ts +1 -1
- package/src/compat/next/legacy-image.tsx +59 -60
- package/src/compat/next/{link-validation-transform.ts → link-transform.ts} +95 -96
- package/src/compat/next/link.tsx +158 -158
- package/src/compat/next/navigation.cjs +12 -3
- package/src/compat/next/navigation.ts +48 -50
- package/src/compat/next/offline.ts +27 -27
- package/src/compat/next/og.ts +121 -124
- package/src/compat/next/preferred-region.ts +13 -14
- package/src/compat/next/redirects.ts +58 -56
- package/src/compat/next/resource-hints.ts +73 -76
- package/src/compat/next/rewrites.ts +130 -133
- package/src/compat/next/root-params.ts +45 -45
- package/src/compat/next/{optimistic-route-state.ts → route-state.ts} +52 -52
- package/src/compat/next/router.cjs +4 -2
- package/src/compat/next/router.ts +58 -61
- package/src/compat/next/script.tsx +108 -108
- package/src/compat/next/server-only.ts +1 -1
- package/src/compat/next/server.ts +19 -19
- package/src/compat/next/svgr.ts +18 -17
- package/src/compat/next/telemetry.ts +24 -24
- package/src/compat/next/{image-usage.ts → usage.ts} +70 -39
- package/src/compat/next/user-agent.ts +53 -49
- package/src/compat/next/web-vitals.ts +22 -24
- package/src/compat/otel/api.ts +41 -41
- package/src/compat/otel/client-trace-metadata.ts +25 -27
- package/src/compat/otel/fetch-span.ts +29 -29
- package/src/compat/otel/tracer.ts +331 -331
- package/src/compat/pages/api.ts +456 -0
- package/src/compat/pages/client-plugin.ts +36 -36
- package/src/compat/pages/router-state.ts +34 -34
- package/src/compat/pages/{index.ts → router.ts} +130 -135
- package/src/compat/ppr/io.ts +12 -12
- package/src/compat/ppr/missing-root-params.ts +34 -38
- package/src/compat/ppr/root-params-scan.ts +66 -66
- package/src/compat/ppr/root-params-transform.ts +24 -26
- package/src/compat/ppr/root-params.ts +30 -30
- package/src/compat/ppr/segment-config-incompat.ts +6 -7
- package/src/compat/protocol.ts +71 -70
- package/src/compat/react/action-state.ts +47 -48
- package/src/compat/react/client-lite.ts +15 -15
- package/src/compat/react/client.ts +4 -4
- package/src/compat/react/compiler-runtime.ts +11 -11
- package/src/compat/react/dom-client.ts +44 -44
- package/src/compat/react/dom-react-server.ts +10 -16
- package/src/compat/react/dom-server.ts +10 -10
- package/src/compat/react/dom.ts +52 -52
- package/src/compat/react/hooks-extra.ts +34 -35
- package/src/compat/react/parity.ts +64 -61
- package/src/compat/react/preact.ts +81 -82
- package/src/compat/react/react-server.ts +28 -28
- package/src/compat/react/router-shim.ts +1 -1
- package/src/compat/react/server-component-use.ts +8 -8
- package/src/compat/react/server-inserted-html.ts +30 -31
- package/src/compat/react/server.ts +53 -55
- package/src/compat/react/use.ts +32 -32
- package/src/compat/react/view-transition.ts +20 -20
- package/src/compat/register/actions.ts +35 -824
- package/src/compat/register/boot.ts +41 -41
- package/src/compat/register/build-tier.ts +5 -5
- package/src/compat/register/build.ts +74 -70
- package/src/compat/register/bundler.ts +161 -156
- package/src/compat/register/cache.ts +20 -20
- package/src/compat/register/client-errors.ts +3 -3
- package/src/compat/register/config.ts +6 -6
- package/src/compat/register/css-extras.ts +30 -34
- package/src/compat/register/edge-runtime.ts +3 -3
- package/src/compat/register/errors.ts +10 -12
- package/src/compat/register/export.ts +16 -16
- package/src/compat/register/font.ts +11 -11
- package/src/compat/register/hooks.ts +2 -2
- package/src/compat/register/image.ts +40 -40
- package/src/compat/register/index.ts +59 -62
- package/src/compat/register/instrumentation-client.ts +10 -10
- package/src/compat/register/lifecycle.ts +25 -28
- package/src/compat/register/mdx.ts +10 -10
- package/src/compat/register/middleware.ts +226 -16
- package/src/compat/register/otel.ts +80 -88
- package/src/compat/register/pages-api.ts +10 -463
- package/src/compat/register/ppr.ts +16 -16
- package/src/compat/register/protocol.ts +17 -18
- package/src/compat/register/proxy.ts +49 -51
- package/src/compat/register/render.ts +79 -76
- package/src/compat/register/routing.ts +162 -159
- package/src/compat/register/segment.ts +24 -1897
- package/src/compat/register/static-image.ts +3 -3
- package/src/compat/register/{misc.ts → taint.ts} +9 -9
- package/src/compat/register/typed-routes.ts +14 -14
- package/src/compat/register/{usecache.ts → use-cache.ts} +39 -39
- package/src/compat/register/validation.ts +18 -18
- package/src/compat/segment/loading-boundary.ts +43 -45
- package/src/compat/segment/page-slot.ts +69 -69
- package/src/compat/segment/serve.ts +1884 -0
- package/src/compat/segment/tree.ts +113 -112
- package/src/compat/segment/vary-key.ts +38 -38
- package/src/compat/segment/vary-params.ts +138 -142
- package/src/compat/static-params.ts +14 -12
- package/src/compat/tsconfig-defaults.ts +87 -91
- package/src/compat/typecheck/{index.ts → check.ts} +234 -212
- package/src/compat/typecheck/worker.ts +15 -12
- package/src/compat/typed-routes/{index.ts → generate.ts} +36 -36
- package/src/compat/typed-routes/manifest.ts +174 -170
- package/src/compat/typed-routes/typegen.ts +127 -110
- package/src/compat/validation/errors.ts +16 -19
- package/src/compat/validation/prerender-diagnostics.ts +521 -504
- package/src/compat/validation/{index.ts → validate.ts} +647 -648
- package/src/compat-bootstrap.ts +16 -16
- package/src/config.ts +69 -69
- package/src/css/build.ts +230 -227
- package/src/css/postcss.ts +79 -80
- package/src/css/worker.ts +14 -15
- package/src/dev/client-actions.ts +10 -10
- package/src/dev/client-chunk-store.ts +27 -27
- package/src/dev/{client-key-cache.ts → restart/client-key.ts} +76 -76
- package/src/dev/{restart-cache.ts → restart/enabled.ts} +1 -1
- package/src/dev/{global-css-cache.ts → restart/global-css.ts} +91 -83
- package/src/dev/{node-module-bundle-cache.ts → restart/node-modules.ts} +24 -24
- package/src/dev/{route-bundle-key-cache.ts → restart/route-bundle-key.ts} +53 -53
- package/src/dev/{route-facts-cache.ts → restart/route-facts.ts} +82 -82
- package/src/dev/server.ts +804 -820
- package/src/env.ts +46 -43
- package/src/extensions.ts +491 -478
- package/src/index.ts +8 -8
- package/src/internal.ts +20 -23
- package/src/{islands → render}/boundary-error.ts +3 -3
- package/src/render/hooks.ts +71 -71
- package/src/render/island-context.ts +14 -14
- package/src/render/metadata.ts +310 -310
- package/src/{ppr-postpone.ts → render/postpone.ts} +5 -5
- package/src/{ppr.ts → render/ppr.ts} +244 -245
- package/src/render/renderer.ts +2076 -2082
- package/src/render/resource-hints.ts +16 -17
- package/src/render/slots.tsx +224 -235
- package/src/{islands → render}/static-children.ts +9 -12
- package/src/{islands → render}/static-slots.ts +37 -37
- package/src/{cache/context.ts → request/cache.ts} +20 -20
- package/src/request/context.ts +107 -107
- package/src/{dynamic/source.ts → resolve/dynamic.ts} +139 -139
- package/src/resolve/engine.ts +90 -77
- package/src/resolve/imports.ts +475 -463
- package/src/resolve/scan-facts.ts +318 -296
- package/src/resolve/source-text.ts +37 -37
- package/src/{dynamic → resolve}/tree-shake.ts +132 -128
- package/src/routing/forwarded.ts +19 -19
- package/src/routing/handler.ts +84 -91
- package/src/routing/href.ts +69 -70
- package/src/routing/{metadata.ts → metadata-files.ts} +403 -401
- package/src/{proxy.ts → routing/proxy.ts} +306 -312
- package/src/routing/{request-runtime.ts → request-environment.ts} +10 -10
- package/src/routing/routes.ts +827 -815
- package/src/routing/slots.ts +164 -160
- package/src/runtime/loader.ts +952 -0
- package/src/{dev → runtime}/module-cache.ts +309 -287
- package/src/{dev → runtime}/module-generations.ts +9 -9
- package/src/{dev → runtime}/module-transform.ts +81 -72
- package/src/{dev/imports.ts → runtime/modules.ts} +934 -847
- package/src/runtime/{server.ts → vendor-build.ts} +848 -1696
- package/src/runtime/vendor.ts +425 -404
- package/src/styles.d.ts +9 -0
- package/src/types.ts +320 -335
- package/src/utils/ansi.ts +5 -5
- package/src/utils/{source.ts → code.ts} +15 -12
- package/src/utils/content-type.ts +3 -3
- package/src/utils/decode.ts +2 -2
- package/src/utils/dev-profile.ts +13 -13
- package/src/utils/error-log.ts +6 -6
- package/src/utils/esbuild.ts +18 -18
- package/src/utils/fs-cache.ts +13 -13
- package/src/utils/fs.ts +57 -49
- package/src/utils/html.ts +20 -24
- package/src/utils/native-require.ts +8 -8
- package/src/utils/serialize.ts +139 -146
- package/src/utils/verbose.ts +18 -18
- package/src/cli/analyze-print.ts +0 -181
- package/src/compat/middleware/manifest.ts +0 -210
- package/src/compat/next/image-client.tsx +0 -215
- package/src/compat/next/link-usage.ts +0 -29
- package/src/css/index.ts +0 -2
- package/src/render/index.ts +0 -1
- package/src/style-modules.d.ts +0 -9
package/src/cli/build.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { copyFile, mkdir, rename, writeFile } from 'node:fs/promises'
|
|
2
|
-
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs'
|
|
3
|
-
import { createRequire } from 'node:module'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import { fileURLToPath } from 'node:url'
|
|
6
|
-
import { writeVercelOutput } from './adapters/vercel'
|
|
7
|
-
import { startWarmChild } from './adapters/vercel-warm'
|
|
8
|
-
import { devOutSegment, loadConfig, pathToFileHref } from '../config'
|
|
9
|
-
import { bootstrapCompat } from '../compat-bootstrap'
|
|
1
|
+
import { copyFile, mkdir, rename, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { writeVercelOutput } from './adapters/vercel'
|
|
7
|
+
import { startWarmChild } from './adapters/vercel-warm'
|
|
8
|
+
import { devOutSegment, loadConfig, pathToFileHref } from '../config'
|
|
9
|
+
import { bootstrapCompat } from '../compat-bootstrap'
|
|
10
10
|
import {
|
|
11
11
|
buildParallelPhaseError,
|
|
12
12
|
clearBuildParallelPhases,
|
|
@@ -18,13 +18,13 @@ import {
|
|
|
18
18
|
getRenderExtensions,
|
|
19
19
|
runInitHooks,
|
|
20
20
|
withRouteRuntime,
|
|
21
|
-
} from '../extensions'
|
|
22
|
-
import { buildClientEntries, emitStaticClientChunks, startClientSources } from '../client/build'
|
|
23
|
-
import { beginSourceScope, endSourceScope, sourceCacheStats } from '../resolve/source-text'
|
|
24
|
-
import { flushDevModuleCaches } from '../
|
|
25
|
-
import { scanFactsStats } from '../resolve/scan-facts'
|
|
26
|
-
import { clientEntryName } from '../client/
|
|
27
|
-
import { registerServerRuntime, serverBundleTargetForRuntime } from '../runtime/
|
|
21
|
+
} from '../extensions'
|
|
22
|
+
import { buildClientEntries, emitStaticClientChunks, startClientSources } from '../client/build'
|
|
23
|
+
import { beginSourceScope, endSourceScope, sourceCacheStats } from '../resolve/source-text'
|
|
24
|
+
import { flushDevModuleCaches } from '../runtime/module-cache'
|
|
25
|
+
import { scanFactsStats } from '../resolve/scan-facts'
|
|
26
|
+
import { clientEntryName } from '../client/chunk-name'
|
|
27
|
+
import { registerServerRuntime, serverBundleTargetForRuntime } from '../runtime/loader'
|
|
28
28
|
import {
|
|
29
29
|
buildClientReferenceCss,
|
|
30
30
|
buildGlobalCss,
|
|
@@ -33,8 +33,8 @@ import {
|
|
|
33
33
|
prepareRouteCssChunks,
|
|
34
34
|
registerCssRuntime,
|
|
35
35
|
warmCssPipeline,
|
|
36
|
-
} from '../css'
|
|
37
|
-
import { ensureEmptyDir, listFiles, readText, toPosixPath, writeText } from '../utils/fs'
|
|
36
|
+
} from '../css/build'
|
|
37
|
+
import { ensureEmptyDir, listFiles, readText, toPosixPath, writeText } from '../utils/fs'
|
|
38
38
|
import {
|
|
39
39
|
discoverStaticMetadataFiles,
|
|
40
40
|
metadataRouteHandlerModule,
|
|
@@ -45,8 +45,8 @@ import {
|
|
|
45
45
|
staticRouteMetadataKey,
|
|
46
46
|
withDynamicMetadataRoutes,
|
|
47
47
|
type StaticMetadataFile,
|
|
48
|
-
} from '../routing/metadata'
|
|
49
|
-
import { readModuleMetadata, readModuleViewport } from '../render/metadata'
|
|
48
|
+
} from '../routing/metadata-files'
|
|
49
|
+
import { readModuleMetadata, readModuleViewport } from '../render/metadata'
|
|
50
50
|
import {
|
|
51
51
|
defaultNotFoundDocument,
|
|
52
52
|
pprShellPath,
|
|
@@ -57,33 +57,33 @@ import {
|
|
|
57
57
|
renderPartialShell,
|
|
58
58
|
renderSubShell,
|
|
59
59
|
staticParamsFor,
|
|
60
|
-
} from '../render'
|
|
60
|
+
} from '../render/renderer'
|
|
61
61
|
import {
|
|
62
62
|
abortActivePrerenderScopes,
|
|
63
63
|
beginShellSourceTracking,
|
|
64
64
|
cacheComponents,
|
|
65
65
|
endShellSourceTracking,
|
|
66
|
-
} from '../ppr'
|
|
67
|
-
import { handleRouteModule, staticRouteParams, type RouteHandlerModule } from '../routing/handler'
|
|
68
|
-
import { runWithCacheScope } from '../cache
|
|
69
|
-
import { devServerModuleHref, setEmitCompiledSpecifiersManifest } from '../
|
|
66
|
+
} from '../render/ppr'
|
|
67
|
+
import { handleRouteModule, staticRouteParams, type RouteHandlerModule } from '../routing/handler'
|
|
68
|
+
import { runWithCacheScope } from '../request/cache'
|
|
69
|
+
import { devServerModuleHref, setEmitCompiledSpecifiersManifest } from '../runtime/modules'
|
|
70
70
|
import {
|
|
71
71
|
beginDynamicBailoutProbe,
|
|
72
72
|
endDynamicBailoutProbe,
|
|
73
73
|
runWithWorkUnit,
|
|
74
|
-
} from '../request/context'
|
|
75
|
-
import { findProxyFile, proxyExternalLoadTarget, validateProxyFiles } from '../proxy'
|
|
76
|
-
import { setRequestRuntime } from '../routing/request-
|
|
74
|
+
} from '../request/context'
|
|
75
|
+
import { findProxyFile, proxyExternalLoadTarget, validateProxyFiles } from '../routing/proxy'
|
|
76
|
+
import { setRequestRuntime } from '../routing/request-environment'
|
|
77
77
|
import {
|
|
78
78
|
addClientEntryReason,
|
|
79
79
|
findLayouts,
|
|
80
80
|
materializeRouteFacts,
|
|
81
81
|
scanRoutes,
|
|
82
|
-
} from '../routing/routes'
|
|
83
|
-
import { interceptionMarkerLevels } from '../routing/slots'
|
|
84
|
-
import { writeTypegen } from '
|
|
85
|
-
import { createVerboseLogger, type VerboseLogger } from '../utils/verbose'
|
|
86
|
-
import { bold, cyan, dim, green } from '../utils/ansi'
|
|
82
|
+
} from '../routing/routes'
|
|
83
|
+
import { interceptionMarkerLevels } from '../routing/slots'
|
|
84
|
+
import { writeTypegen } from './typegen'
|
|
85
|
+
import { createVerboseLogger, type VerboseLogger } from '../utils/verbose'
|
|
86
|
+
import { bold, cyan, dim, green } from '../utils/ansi'
|
|
87
87
|
import type {
|
|
88
88
|
ActionManifestEntry,
|
|
89
89
|
BuildManifest,
|
|
@@ -92,123 +92,123 @@ import type {
|
|
|
92
92
|
StaticFileMetadata,
|
|
93
93
|
StaticModuleMetadata,
|
|
94
94
|
StaticRouteMetadata,
|
|
95
|
-
} from '../types'
|
|
95
|
+
} from '../types'
|
|
96
96
|
|
|
97
97
|
/** Mutable accumulator handed to compat build steps as their `manifest` ctx. */
|
|
98
98
|
interface BuildStepState {
|
|
99
|
-
actions: ActionManifestEntry[]
|
|
99
|
+
actions: ActionManifestEntry[]
|
|
100
100
|
/** Root-relative 'use server' module paths — known from discovery, so the
|
|
101
101
|
* client stage keys off this instead of waiting for their compile. */
|
|
102
|
-
actionSources?: string[]
|
|
102
|
+
actionSources?: string[]
|
|
103
103
|
/** First-party files that import a node_modules action module (absolute paths) — see
|
|
104
104
|
* ActionDiscovery.actionImporters; route.sourceFiles never reaches into node_modules itself. */
|
|
105
|
-
actionImporters?: string[]
|
|
105
|
+
actionImporters?: string[]
|
|
106
106
|
/** Work a step handed back rather than finishing inline; awaited before the
|
|
107
107
|
* build manifest is written, so it lands under the client stage. */
|
|
108
|
-
deferred?: Promise<void
|
|
108
|
+
deferred?: Promise<void>
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
type CacheLifeStash = ReturnType<typeof getBuildExtensions>['compat'] extends {
|
|
112
|
-
takeCacheLifeStash: () => infer T
|
|
112
|
+
takeCacheLifeStash: () => infer T
|
|
113
113
|
}
|
|
114
114
|
? NonNullable<T>
|
|
115
|
-
: never
|
|
116
|
-
type SegmentMeta = ReturnType<ReturnType<typeof getBuildExtensions>['compat']['buildSegmentMeta']
|
|
115
|
+
: never
|
|
116
|
+
type SegmentMeta = ReturnType<ReturnType<typeof getBuildExtensions>['compat']['buildSegmentMeta']>
|
|
117
117
|
|
|
118
118
|
function nextCompatEnabled(config: Awaited<ReturnType<typeof loadConfig>>) {
|
|
119
|
-
return getCompatModeExtensions().nextEnabled(config)
|
|
119
|
+
return getCompatModeExtensions().nextEnabled(config)
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
function reactCompatEnabled(config: Awaited<ReturnType<typeof loadConfig>>) {
|
|
123
|
-
return getCompatModeExtensions().reactEnabled(config)
|
|
123
|
+
return getCompatModeExtensions().reactEnabled(config)
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
function buildCompat() {
|
|
127
|
-
return getBuildExtensions().compat
|
|
127
|
+
return getBuildExtensions().compat
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
interface BuildOptions {
|
|
131
|
-
adapter?: 'vercel'
|
|
132
|
-
verbose?: boolean
|
|
131
|
+
adapter?: 'vercel'
|
|
132
|
+
verbose?: boolean
|
|
133
133
|
/**
|
|
134
134
|
* `next build --debug-build-paths <glob>` parity: restrict the build to the route files matching
|
|
135
135
|
* the (comma-separated) pattern(s), relative to the project root.
|
|
136
136
|
*/
|
|
137
|
-
debugBuildPaths?: string
|
|
137
|
+
debugBuildPaths?: string
|
|
138
138
|
/**
|
|
139
139
|
* `next build --experimental-build-mode compile|generate` parity: `compile`
|
|
140
140
|
* bundles without prerendering; `generate` runs the prerender/export pass
|
|
141
141
|
* with Next-shaped page-data output (and fails with Next's exact
|
|
142
142
|
* blocking-prerender diagnostics under cacheComponents).
|
|
143
143
|
*/
|
|
144
|
-
buildMode?: 'compile' | 'generate'
|
|
144
|
+
buildMode?: 'compile' | 'generate'
|
|
145
145
|
/** `next build --debug-prerender`: unminified prerender stacks + codeframes. */
|
|
146
|
-
debugPrerender?: boolean
|
|
146
|
+
debugPrerender?: boolean
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
export async function buildProject(root?: string, options: BuildOptions = {}) {
|
|
150
|
-
clearBuildParallelPhases()
|
|
150
|
+
clearBuildParallelPhases()
|
|
151
151
|
// One read per source for the whole build (route-fact walk, action discovery,
|
|
152
152
|
// client loader), released here so nothing survives the build.
|
|
153
|
-
beginSourceScope()
|
|
153
|
+
beginSourceScope()
|
|
154
154
|
// Only the vercel adapter's trace step reads the per-artifact specifier
|
|
155
155
|
// sidecars; every other build path pays nothing for them.
|
|
156
|
-
const restoreSpecifiersManifest = setEmitCompiledSpecifiersManifest(options.adapter === 'vercel')
|
|
156
|
+
const restoreSpecifiersManifest = setEmitCompiledSpecifiersManifest(options.adapter === 'vercel')
|
|
157
157
|
try {
|
|
158
|
-
return await runBuild(root, options)
|
|
158
|
+
return await runBuild(root, options)
|
|
159
159
|
} catch (error) {
|
|
160
160
|
// A parallel phase that already failed (a type error) is the root cause of
|
|
161
161
|
// whatever broke downstream; report it instead of the symptom.
|
|
162
|
-
const phaseError = buildParallelPhaseError()
|
|
163
|
-
throw phaseError ?? error
|
|
162
|
+
const phaseError = buildParallelPhaseError()
|
|
163
|
+
throw phaseError ?? error
|
|
164
164
|
} finally {
|
|
165
|
-
endSourceScope()
|
|
166
|
-
restoreSpecifiersManifest()
|
|
167
|
-
flushDevModuleCaches()
|
|
165
|
+
endSourceScope()
|
|
166
|
+
restoreSpecifiersManifest()
|
|
167
|
+
flushDevModuleCaches()
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
172
|
-
const verbose = options.verbose ?? false
|
|
173
|
-
const log = createVerboseLogger(verbose, 'build')
|
|
174
|
-
const startedAt = performance.now()
|
|
172
|
+
const verbose = options.verbose ?? false
|
|
173
|
+
const log = createVerboseLogger(verbose, 'build')
|
|
174
|
+
const startedAt = performance.now()
|
|
175
175
|
|
|
176
176
|
console.log(
|
|
177
177
|
`${cyan('▲')} ${bold('pnext')} ${dim('— Creating an optimized production build ...\n')}`,
|
|
178
|
-
)
|
|
179
|
-
const config = await loadConfig(root)
|
|
178
|
+
)
|
|
179
|
+
const config = await loadConfig(root)
|
|
180
180
|
// Load Tailwind on the CSS worker while the steps below run, so the first
|
|
181
181
|
// stylesheet doesn't pay its cold boot.
|
|
182
|
-
warmCssPipeline(config)
|
|
182
|
+
warmCssPipeline(config)
|
|
183
183
|
// Compat plugin loader: the single gated seam that populates the core
|
|
184
184
|
// extension registries when compat is enabled (no-op for pure-core apps).
|
|
185
185
|
// A build compiles immediately, so it takes both tiers up front.
|
|
186
|
-
await bootstrapCompat(config)
|
|
187
|
-
log.log(`config loaded — out ${path.relative(config.root, config.outPath) || '.'}`)
|
|
186
|
+
await bootstrapCompat(config)
|
|
187
|
+
log.log(`config loaded — out ${path.relative(config.root, config.outPath) || '.'}`)
|
|
188
188
|
// Run registered init hooks (compat installs the Next fetch-cache patch so prerenders observe
|
|
189
189
|
// force-cache / next: { revalidate, tags }). No-op for pure-core apps. `build: true` keeps
|
|
190
190
|
// server-boot-only hooks out of the build - Next never runs register() during `next build`, so
|
|
191
191
|
// build prerenders must see a noop tracer.
|
|
192
|
-
runInitHooks(config, { build: true })
|
|
192
|
+
runInitHooks(config, { build: true })
|
|
193
193
|
// The vercel adapter's warm pass runs in its own process; start it here so
|
|
194
194
|
// its boot overlaps the build and only the warming itself lands on the
|
|
195
195
|
// adapter step.
|
|
196
|
-
const warm = options.adapter === 'vercel' ? startWarmChild(config) : undefined
|
|
196
|
+
const warm = options.adapter === 'vercel' ? startWarmChild(config) : undefined
|
|
197
197
|
await log.step('prepare output directory', async () => {
|
|
198
198
|
// Build-owned outputs only: `<outRoot>/dev` belongs to a possibly-running
|
|
199
199
|
// dev server and must survive.
|
|
200
|
-
await ensureEmptyDir(config.outPath, [devOutSegment])
|
|
201
|
-
await copyPublicDir(config.publicPath, path.join(config.outPath, 'public'))
|
|
202
|
-
})
|
|
200
|
+
await ensureEmptyDir(config.outPath, [devOutSegment])
|
|
201
|
+
await copyPublicDir(config.publicPath, path.join(config.outPath, 'public'))
|
|
202
|
+
})
|
|
203
203
|
// Prebundled server entry for `pnext start`: framework-only, independent of the
|
|
204
204
|
// app build, so it runs in a child process for the whole build — its bundling
|
|
205
205
|
// heap never stacks on the build's peak RSS and its wall hides under the build.
|
|
206
206
|
// Best-effort — a failure only costs start time. Awaited before the summary.
|
|
207
|
-
const serverEntryDone = import('./
|
|
207
|
+
const serverEntryDone = import('./serve/entry')
|
|
208
208
|
.then(entry => entry.emitServerEntryChild(config.outPath))
|
|
209
209
|
.catch((error: Error) => {
|
|
210
|
-
console.warn(`pnext build: server entry bundling skipped — ${error.message}`)
|
|
211
|
-
})
|
|
210
|
+
console.warn(`pnext build: server entry bundling skipped — ${error.message}`)
|
|
211
|
+
})
|
|
212
212
|
// The document-level stylesheets run their postcss/Tailwind pass on the CSS
|
|
213
213
|
// worker, so they overlap with the route scan below instead of serializing
|
|
214
214
|
// ahead of it. Awaited before prepareRouteCssChunks — route CSS still builds
|
|
@@ -217,50 +217,50 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
217
217
|
.then(() => buildGlobalCss(config, { verbose }))
|
|
218
218
|
// 404/not-found documents reference their own CSS chunk; emit it alongside
|
|
219
219
|
// the global chunk so the synthetic not-found routes don't 404 their styles.
|
|
220
|
-
.then(() => buildNotFoundCss(config, { verbose }))
|
|
220
|
+
.then(() => buildNotFoundCss(config, { verbose }))
|
|
221
221
|
// Nothing awaits it until below; park the rejection so a CSS failure reports
|
|
222
222
|
// at that await instead of as an unhandled rejection mid-scan.
|
|
223
|
-
documentCss.catch(() => undefined)
|
|
223
|
+
documentCss.catch(() => undefined)
|
|
224
224
|
// experimental.nextScriptWorkers: copy the Partytown library so `worker`
|
|
225
225
|
// scripts (rewritten to type="text/partytown") can load it from
|
|
226
226
|
// /_next/static/~partytown/. Tolerates the package being absent. Nothing
|
|
227
227
|
// downstream reads it, so it runs beside the scan instead of ahead of it.
|
|
228
|
-
const partytownLib = log.step('partytown lib', () => copyPartytownLib(config))
|
|
229
|
-
partytownLib.catch(() => undefined)
|
|
228
|
+
const partytownLib = log.step('partytown lib', () => copyPartytownLib(config))
|
|
229
|
+
partytownLib.catch(() => undefined)
|
|
230
230
|
|
|
231
|
-
let routes = await log.step('scan routes', () => scanRoutes(config.appPath))
|
|
232
|
-
log.log(`found ${routes.length} route${routes.length === 1 ? '' : 's'}`)
|
|
231
|
+
let routes = await log.step('scan routes', () => scanRoutes(config.appPath))
|
|
232
|
+
log.log(`found ${routes.length} route${routes.length === 1 ? '' : 's'}`)
|
|
233
233
|
if (options.debugBuildPaths) {
|
|
234
|
-
routes = filterDebugBuildRoutes(config.root, routes, options.debugBuildPaths)
|
|
234
|
+
routes = filterDebugBuildRoutes(config.root, routes, options.debugBuildPaths)
|
|
235
235
|
log.log(
|
|
236
236
|
`--debug-build-paths ${options.debugBuildPaths} — ${routes.length} route${routes.length === 1 ? '' : 's'} kept`,
|
|
237
|
-
)
|
|
237
|
+
)
|
|
238
238
|
}
|
|
239
239
|
// The page routes' server modules are the bulk of the adapter's warm pass and
|
|
240
240
|
// depend on nothing but the scan; hand them over now so the child compiles
|
|
241
241
|
// them under the client stage instead of after it.
|
|
242
|
-
warm?.prewarm(routes.filter(route => route.kind === 'page').map(route => route.file))
|
|
242
|
+
warm?.prewarm(routes.filter(route => route.kind === 'page').map(route => route.file))
|
|
243
243
|
// Compat build steps (action discovery/bundling + server-reference manifest)
|
|
244
244
|
// populate their output onto this accumulator. Pure-core registers no steps.
|
|
245
|
-
const buildState: BuildStepState = { actions: [] }
|
|
246
|
-
const stepContext = { config, routes, manifest: buildState, log }
|
|
247
|
-
const { steps } = getBuildExtensions()
|
|
245
|
+
const buildState: BuildStepState = { actions: [] }
|
|
246
|
+
const stepContext = { config, routes, manifest: buildState, log }
|
|
247
|
+
const { steps } = getBuildExtensions()
|
|
248
248
|
// Steps that declare themselves route-fact-independent (action discovery) run UNDER the scan below
|
|
249
249
|
// rather than after it, which would make both strictly serial.
|
|
250
250
|
const earlySteps = runBuildSteps(
|
|
251
251
|
steps.filter(step => step.early),
|
|
252
252
|
stepContext,
|
|
253
|
-
)
|
|
253
|
+
)
|
|
254
254
|
// Nothing awaits it until the client-entry set is computed; park the rejection
|
|
255
255
|
// so a discovery failure reports there instead of as an unhandled rejection.
|
|
256
|
-
earlySteps.catch(() => undefined)
|
|
256
|
+
earlySteps.catch(() => undefined)
|
|
257
257
|
// The table arrives with its content facts deferred (dev boots on paths
|
|
258
258
|
// alone); a build needs all of them, and needs their scan errors up front.
|
|
259
259
|
// The client stage needs nothing but each route's client file list, so it
|
|
260
260
|
// starts here, route by route as the facts land: the transform chain and the
|
|
261
261
|
// React Compiler run on oxc's threadpool under the rest of the build instead
|
|
262
262
|
// of inside the client stage's own wall.
|
|
263
|
-
const clientSources = startClientSources(config)
|
|
263
|
+
const clientSources = startClientSources(config)
|
|
264
264
|
// Unpaced on purpose: the walk holds the loop for ~0.55 s and action
|
|
265
265
|
// discovery beside it cannot resume, but pacing it (setImmediate per route)
|
|
266
266
|
// measures a wash — discovery's 0.6 s of starvation is hidden entirely inside
|
|
@@ -269,27 +269,25 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
269
269
|
'scan route facts',
|
|
270
270
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
271
271
|
async () => materializeRouteFacts(routes, route => clientSources.warmRoutes([route])),
|
|
272
|
-
)
|
|
272
|
+
)
|
|
273
273
|
// Render-time extensions read the route table from the request runtime (e.g.
|
|
274
274
|
// compat's static-sibling route state); publish it for the prerender pass the
|
|
275
275
|
// same way the serve handlers do.
|
|
276
|
-
setRequestRuntime({ config, routes, dev: false })
|
|
276
|
+
setRequestRuntime({ config, routes, dev: false })
|
|
277
277
|
// Compute the compat CSS-chunk plan across all routes up front (Next's CSS chunking needs the
|
|
278
278
|
// global picture); buildRouteCss then emits one asset per planned slice. No-op for pure-core apps.
|
|
279
279
|
// The plan is derived from the route table alone, so it does NOT wait on the document stylesheets
|
|
280
280
|
// - those are awaited just before the first route stylesheet is emitted, which lets a Tailwind
|
|
281
281
|
// pass run under the build steps and the client bundles instead of ahead of them.
|
|
282
|
-
prepareRouteCssChunks(routes)
|
|
282
|
+
prepareRouteCssChunks(routes)
|
|
283
283
|
// output:'export' with trailingSlash:false lays prerendered pages out flat
|
|
284
284
|
// (`/a.html` rather than `/a/index.html`). Only meaningful under compat (the
|
|
285
285
|
// Next config carries `output`); pure-core apps always use the dir layout.
|
|
286
286
|
const flatExportLayout =
|
|
287
|
-
nextCompatEnabled(config) &&
|
|
288
|
-
buildCompat().nextOutputExport() &&
|
|
289
|
-
config.trailingSlash !== true;
|
|
287
|
+
nextCompatEnabled(config) && buildCompat().nextOutputExport() && config.trailingSlash !== true
|
|
290
288
|
// The proxy bundle is independent of the remaining build steps; overlap them.
|
|
291
|
-
const proxyBuild = log.step('proxy module', () => buildProxyModule(config))
|
|
292
|
-
proxyBuild.catch(() => undefined)
|
|
289
|
+
const proxyBuild = log.step('proxy module', () => buildProxyModule(config))
|
|
290
|
+
proxyBuild.catch(() => undefined)
|
|
293
291
|
// Gate steps (validation) run here so their diagnostics still precede any
|
|
294
292
|
// failure the client stage or the not-found prerender would raise for the
|
|
295
293
|
// same broken app; every other step is a manifest write nothing downstream
|
|
@@ -297,27 +295,27 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
297
295
|
await runBuildSteps(
|
|
298
296
|
steps.filter(step => !step.early && step.gate),
|
|
299
297
|
stepContext,
|
|
300
|
-
)
|
|
298
|
+
)
|
|
301
299
|
// Static metadata depends on the scan, not on action discovery — collect and
|
|
302
300
|
// copy it while the early steps finish rather than after them.
|
|
303
|
-
const staticFiles: Record<string, StaticFileMetadata> = {}
|
|
301
|
+
const staticFiles: Record<string, StaticFileMetadata> = {}
|
|
304
302
|
const staticMetadata = (async () => {
|
|
305
303
|
const files = await log.step('discover static metadata files', () =>
|
|
306
304
|
Promise.resolve(discoverStaticMetadataFiles(config.appPath)),
|
|
307
|
-
)
|
|
305
|
+
)
|
|
308
306
|
await log.step('static metadata files', () =>
|
|
309
307
|
copyStaticMetadataFiles(config, staticFiles, files),
|
|
310
|
-
)
|
|
311
|
-
return files
|
|
312
|
-
})()
|
|
313
|
-
staticMetadata.catch(() => undefined)
|
|
308
|
+
)
|
|
309
|
+
return files
|
|
310
|
+
})()
|
|
311
|
+
staticMetadata.catch(() => undefined)
|
|
314
312
|
// Action discovery arms the client-stub set buildClientEntries consumes, so
|
|
315
313
|
// this is the point the early steps have to have landed by. What the step
|
|
316
314
|
// deferred is not part of that arming and is awaited further below.
|
|
317
|
-
await earlySteps
|
|
318
|
-
const deferredSteps = buildState.deferred ?? Promise.resolve()
|
|
319
|
-
deferredSteps.catch(() => undefined)
|
|
320
|
-
const staticMetadataFiles = await staticMetadata
|
|
315
|
+
await earlySteps
|
|
316
|
+
const deferredSteps = buildState.deferred ?? Promise.resolve()
|
|
317
|
+
deferredSteps.catch(() => undefined)
|
|
318
|
+
const staticMetadataFiles = await staticMetadata
|
|
321
319
|
// Warnings only - nothing downstream reads the result. Started here and awaited after the client
|
|
322
320
|
// stage so it costs the build nothing, and still prints before the first prerendered route line.
|
|
323
321
|
const metadataWarnings = nextCompatEnabled(config)
|
|
@@ -328,36 +326,38 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
328
326
|
staticMetadataFiles,
|
|
329
327
|
}),
|
|
330
328
|
)
|
|
331
|
-
: undefined
|
|
332
|
-
metadataWarnings?.catch(() => undefined)
|
|
329
|
+
: undefined
|
|
330
|
+
metadataWarnings?.catch(() => undefined)
|
|
333
331
|
const staticModuleMetadata = await log.step('core module metadata', () =>
|
|
334
332
|
collectStaticModuleMetadata(config, routes),
|
|
335
|
-
)
|
|
333
|
+
)
|
|
336
334
|
|
|
337
|
-
const actionSources = buildState.actionSources ?? buildState.actions.map(a => a.sourceKey)
|
|
335
|
+
const actionSources = buildState.actionSources ?? buildState.actions.map(a => a.sourceKey)
|
|
338
336
|
// realpath, not resolve: a hybrid app's routes are scanned through the pages-compat mirror, whose
|
|
339
337
|
// `source-app` is a symlink to the real app dir - the two spellings of one file must compare equal.
|
|
340
|
-
const actionFiles = new Set(
|
|
338
|
+
const actionFiles = new Set(
|
|
339
|
+
actionSources.map(source => realFilePath(path.resolve(config.root, source))),
|
|
340
|
+
)
|
|
341
341
|
// A node_modules action module never lands in route.sourceFiles itself (that walk stops at the
|
|
342
342
|
// package boundary), so a route reaching one only through a first-party importer is matched here.
|
|
343
|
-
const actionImporters = new Set((buildState.actionImporters ?? []).map(realFilePath))
|
|
343
|
+
const actionImporters = new Set((buildState.actionImporters ?? []).map(realFilePath))
|
|
344
344
|
for (const route of routes) {
|
|
345
345
|
if (
|
|
346
346
|
route.kind === 'page' &&
|
|
347
347
|
route.sourceFiles.some(file => {
|
|
348
|
-
const real = realFilePath(file)
|
|
349
|
-
return actionFiles.has(real) || actionImporters.has(real)
|
|
348
|
+
const real = realFilePath(file)
|
|
349
|
+
return actionFiles.has(real) || actionImporters.has(real)
|
|
350
350
|
})
|
|
351
351
|
) {
|
|
352
|
-
addClientEntryReason(route, 'actions')
|
|
352
|
+
addClientEntryReason(route, 'actions')
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
355
|
const clientRoutes = routes.filter(
|
|
356
356
|
route =>
|
|
357
357
|
route.kind !== 'handler' &&
|
|
358
358
|
(route.client || route.clientReferences.length > 0 || route.needsRouterEntry),
|
|
359
|
-
)
|
|
360
|
-
for (const route of clientRoutes) route.clientEntry = `assets/${clientEntryName(route)}.js
|
|
359
|
+
)
|
|
360
|
+
for (const route of clientRoutes) route.clientEntry = `assets/${clientEntryName(route)}.js`
|
|
361
361
|
// A prerenderable 404 boots the server graph; the client bundles are esbuild.
|
|
362
362
|
// Both need only the stub set and the entry names decided just above, so run
|
|
363
363
|
// them side by side instead of paying the render after the bundle. Result is
|
|
@@ -369,8 +369,8 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
369
369
|
documentCss,
|
|
370
370
|
staticMetadataFiles,
|
|
371
371
|
staticModuleMetadata,
|
|
372
|
-
})
|
|
373
|
-
notFoundDocuments.catch(() => undefined)
|
|
372
|
+
})
|
|
373
|
+
notFoundDocuments.catch(() => undefined)
|
|
374
374
|
const clientBundles = log.step(
|
|
375
375
|
`client bundles (${clientRoutes.length} route${clientRoutes.length === 1 ? '' : 's'})`,
|
|
376
376
|
() =>
|
|
@@ -382,45 +382,43 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
382
382
|
hasServerActions: actionSources.length > 0,
|
|
383
383
|
pipeline: clientSources,
|
|
384
384
|
}),
|
|
385
|
-
)
|
|
386
|
-
clientBundles.catch(() => undefined)
|
|
385
|
+
)
|
|
386
|
+
clientBundles.catch(() => undefined)
|
|
387
387
|
// The manifest-writing steps and typegen depend on nothing the client stage produces, so they run
|
|
388
388
|
// under it instead of ahead of it. Awaited first so a step failure still reports before a
|
|
389
389
|
// bundling error.
|
|
390
390
|
const remainingSteps = runBuildSteps(
|
|
391
391
|
steps.filter(step => !step.early && !step.gate),
|
|
392
392
|
stepContext,
|
|
393
|
-
).then(() =>
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
await
|
|
398
|
-
await
|
|
399
|
-
const proxyModule = await proxyBuild;
|
|
400
|
-
await partytownLib;
|
|
393
|
+
).then(() => log.step('typegen', () => writeTypegen(config, routes)))
|
|
394
|
+
remainingSteps.catch(() => undefined)
|
|
395
|
+
await remainingSteps
|
|
396
|
+
await clientBundles
|
|
397
|
+
const proxyModule = await proxyBuild
|
|
398
|
+
await partytownLib
|
|
401
399
|
// Standalone static chunks (compat: the no-module polyfills chunk) live
|
|
402
400
|
// outside the esbuild entry graph, so emit them regardless of whether any
|
|
403
401
|
// route produced a client bundle.
|
|
404
|
-
await emitStaticClientChunks(config, path.join(config.outPath, 'public', 'assets'))
|
|
402
|
+
await emitStaticClientChunks(config, path.join(config.outPath, 'public', 'assets'))
|
|
405
403
|
// All three source consumers have run by here; the split says how much of the
|
|
406
404
|
// app was read once and shared rather than read per consumer.
|
|
407
405
|
if (verbose) {
|
|
408
|
-
const sources = sourceCacheStats()
|
|
406
|
+
const sources = sourceCacheStats()
|
|
409
407
|
log.log(
|
|
410
408
|
`source cache — ${sources.reads} reads, ${sources.hits} shared, ${sources.files} files · parses ${JSON.stringify(scanFactsStats())}`,
|
|
411
|
-
)
|
|
409
|
+
)
|
|
412
410
|
}
|
|
413
|
-
await metadataWarnings
|
|
411
|
+
await metadataWarnings
|
|
414
412
|
|
|
415
413
|
// A throwing after() during a prerender must fail the whole build (Next
|
|
416
414
|
// exits 1), but with `prerenderEarlyExit: false` the errors for every route
|
|
417
415
|
// are collected first — so record it and keep prerendering, then fail at the
|
|
418
416
|
// end once all routes have logged their prerender-error lines.
|
|
419
|
-
let hadAfterPrerenderError = false
|
|
417
|
+
let hadAfterPrerenderError = false
|
|
420
418
|
// `dynamic = 'error'` routes that read dynamic data can't be rendered
|
|
421
419
|
// statically; Next fails the build naming each offending route + API. Collect
|
|
422
420
|
// them all (matching prerenderEarlyExit:false) and fail once at the end.
|
|
423
|
-
let hadDynamicErrorFailure = false
|
|
421
|
+
let hadDynamicErrorFailure = false
|
|
424
422
|
|
|
425
423
|
// A handler route whose logical output path doubles as a parent directory for a descendant
|
|
426
424
|
// route's static output cannot be written as a plain file AND host a child directory on one
|
|
@@ -428,28 +426,28 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
428
426
|
// the file and the directory would collide). The ancestor keeps the physical file so it serves
|
|
429
427
|
// statically; each descendant records its prerender-manifest entry but skips the physical copy
|
|
430
428
|
// and serves dynamically. Descendants are detected up-front so the outcome is order-independent.
|
|
431
|
-
const descendantHandlerIds = collectDescendantHandlerIds(routes)
|
|
429
|
+
const descendantHandlerIds = collectDescendantHandlerIds(routes)
|
|
432
430
|
|
|
433
|
-
const isCompile = options.buildMode === 'compile'
|
|
434
|
-
const isGenerate = options.buildMode === 'generate'
|
|
435
|
-
const debugPrerender = options.debugPrerender ?? false
|
|
431
|
+
const isCompile = options.buildMode === 'compile'
|
|
432
|
+
const isGenerate = options.buildMode === 'generate'
|
|
433
|
+
const debugPrerender = options.debugPrerender ?? false
|
|
436
434
|
// Record the build inputs the serving runtime needs (compat: --debug-prerender
|
|
437
435
|
// selects the shape of the runtime 'use cache' error log). No-op for core.
|
|
438
|
-
buildCompat().recordBuildFlags(config.outPath, debugPrerender)
|
|
436
|
+
buildCompat().recordBuildFlags(config.outPath, debugPrerender)
|
|
439
437
|
// Generate mode: Next prints the page-data banner, then ONLY prerender diagnostics until the
|
|
440
438
|
// route table - the cache-components-errors suites capture everything after this line and
|
|
441
439
|
// inline-snapshot it. Per-path lines are buffered and printed after a `Route (app)` header, which
|
|
442
440
|
// ends the suites' capture window.
|
|
443
|
-
const generateRouteLines: string[] = []
|
|
441
|
+
const generateRouteLines: string[] = []
|
|
444
442
|
/** Route ids that prerendered a partial (cache-components) shell — Next's `◐`. */
|
|
445
|
-
const partialPrerenders = new Set<string>()
|
|
443
|
+
const partialPrerenders = new Set<string>()
|
|
446
444
|
/** Routes that failed generate-mode prerender diagnostics, in scan order. */
|
|
447
|
-
const generateFailures: { route: string; omitErrorLine: boolean }[] = []
|
|
448
|
-
if (isGenerate) console.log(' Collecting page data ...')
|
|
445
|
+
const generateFailures: { route: string; omitErrorLine: boolean }[] = []
|
|
446
|
+
if (isGenerate) console.log(' Collecting page data ...')
|
|
449
447
|
|
|
450
448
|
// Route stylesheets build strictly after the document ones; this is the first
|
|
451
449
|
// point that needs them, so everything above ran alongside the CSS worker.
|
|
452
|
-
await documentCss
|
|
450
|
+
await documentCss
|
|
453
451
|
|
|
454
452
|
for (const route of routes) {
|
|
455
453
|
if (route.dynamicErrorApi) {
|
|
@@ -457,19 +455,19 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
457
455
|
`Error: Route ${toNextRoutePattern(route.route || '/')} with \`dynamic = "error"\` ` +
|
|
458
456
|
`couldn't be rendered statically because it used ${route.dynamicErrorApi}. ` +
|
|
459
457
|
`See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering`,
|
|
460
|
-
)
|
|
461
|
-
hadDynamicErrorFailure = true
|
|
462
|
-
continue
|
|
458
|
+
)
|
|
459
|
+
hadDynamicErrorFailure = true
|
|
460
|
+
continue
|
|
463
461
|
}
|
|
464
462
|
if (route.kind === 'handler') {
|
|
465
|
-
if (isCompile) continue
|
|
466
|
-
if (!(await staticRouteHandlerCandidate(route))) continue
|
|
467
|
-
const dynamicUsage = await staticHandlerDynamicUsage(route)
|
|
463
|
+
if (isCompile) continue
|
|
464
|
+
if (!(await staticRouteHandlerCandidate(route))) continue
|
|
465
|
+
const dynamicUsage = await staticHandlerDynamicUsage(route)
|
|
468
466
|
if (dynamicUsage) {
|
|
469
467
|
console.log(
|
|
470
468
|
`Caught Error: Dynamic server usage: Route ${route.route || '/'} couldn't be rendered statically because it used \`${dynamicUsage}\`.`,
|
|
471
|
-
)
|
|
472
|
-
continue
|
|
469
|
+
)
|
|
470
|
+
continue
|
|
473
471
|
}
|
|
474
472
|
// A single handler prerender failure must not kill the whole build:
|
|
475
473
|
// skip the static copy and let the route serve dynamically at runtime.
|
|
@@ -481,22 +479,22 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
481
479
|
skipPhysicalWrite: descendantHandlerIds.has(route.id),
|
|
482
480
|
}),
|
|
483
481
|
),
|
|
484
|
-
)
|
|
482
|
+
)
|
|
485
483
|
} catch (error) {
|
|
486
|
-
rethrowIfProgrammingError(error)
|
|
487
|
-
if (isAfterPrerenderError(error)) hadAfterPrerenderError = true
|
|
488
|
-
warnSkippedStatic(route.route, error instanceof Error ? error.message : String(error))
|
|
484
|
+
rethrowIfProgrammingError(error)
|
|
485
|
+
if (isAfterPrerenderError(error)) hadAfterPrerenderError = true
|
|
486
|
+
warnSkippedStatic(route.route, error instanceof Error ? error.message : String(error))
|
|
489
487
|
}
|
|
490
|
-
continue
|
|
488
|
+
continue
|
|
491
489
|
}
|
|
492
490
|
|
|
493
|
-
await buildRouteCss(config, route, { verbose })
|
|
491
|
+
await buildRouteCss(config, route, { verbose })
|
|
494
492
|
for (const reference of route.clientReferences) {
|
|
495
|
-
await buildClientReferenceCss(config, reference, { verbose })
|
|
493
|
+
await buildClientReferenceCss(config, reference, { verbose })
|
|
496
494
|
}
|
|
497
495
|
|
|
498
496
|
// Compile mode bundles only — every prerender/export step is generate's.
|
|
499
|
-
if (isCompile) continue
|
|
497
|
+
if (isCompile) continue
|
|
500
498
|
|
|
501
499
|
// Generate mode fails a cacheComponents route whose prerender would block,
|
|
502
500
|
// with Next's exact diagnostic block (owner stack + codeframe under
|
|
@@ -505,26 +503,26 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
505
503
|
if (isGenerate && cacheComponents() && !route.client && !route.interception) {
|
|
506
504
|
// Next names the route by its source pattern (`/use-cache-params/[slug]`),
|
|
507
505
|
// not pnext's `:param` form — the diagnostics quote it verbatim.
|
|
508
|
-
const diagnosticRoute = toNextRoutePattern(route.route || '/')
|
|
506
|
+
const diagnosticRoute = toNextRoutePattern(route.route || '/')
|
|
509
507
|
const diagnostic = buildCompat().diagnoseCacheComponentsPrerender({
|
|
510
508
|
route: diagnosticRoute,
|
|
511
509
|
pageFile: route.file,
|
|
512
510
|
appPath: config.appPath,
|
|
513
511
|
debugPrerender,
|
|
514
|
-
})
|
|
512
|
+
})
|
|
515
513
|
if (diagnostic) {
|
|
516
|
-
console.error(diagnostic)
|
|
514
|
+
console.error(diagnostic)
|
|
517
515
|
generateFailures.push({
|
|
518
516
|
route: diagnosticRoute,
|
|
519
517
|
omitErrorLine: buildCompat().diagnosticLeadsWithErrorLine(diagnostic),
|
|
520
|
-
})
|
|
521
|
-
continue
|
|
518
|
+
})
|
|
519
|
+
continue
|
|
522
520
|
}
|
|
523
521
|
}
|
|
524
522
|
|
|
525
523
|
if (route.ppr) {
|
|
526
|
-
await log.step(`ppr shell ${route.route}`, () => buildPprShell(config, route))
|
|
527
|
-
continue
|
|
524
|
+
await log.step(`ppr shell ${route.route}`, () => buildPprShell(config, route))
|
|
525
|
+
continue
|
|
528
526
|
}
|
|
529
527
|
|
|
530
528
|
// Under the global cacheComponents flag every page is a PPR candidate:
|
|
@@ -538,35 +536,35 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
538
536
|
) {
|
|
539
537
|
const built = await log.step(`cache-components shell ${route.route}`, () =>
|
|
540
538
|
buildCacheComponentsShell(config, route),
|
|
541
|
-
)
|
|
539
|
+
)
|
|
542
540
|
if (built) {
|
|
543
541
|
// Next marks a partially prerendered route with a distinct glyph in its build output, and
|
|
544
542
|
// suites grep for it. That glyph is for a PARTIAL prerender: a shell with dynamic holes (or
|
|
545
543
|
// blocked metadata), or a param FALLBACK shell whose params only resolve per request. A
|
|
546
544
|
// hole-less shell of a fully-known route keeps Next's static marker.
|
|
547
545
|
const paramFallback =
|
|
548
|
-
(route.params.length > 0 || Boolean(route.catchAll)) && !route.hasStaticParams
|
|
546
|
+
(route.params.length > 0 || Boolean(route.catchAll)) && !route.hasStaticParams
|
|
549
547
|
if (paramFallback || (route.pprHoles?.length ?? 0) > 0 || route.pprMetadata) {
|
|
550
|
-
partialPrerenders.add(route.id)
|
|
548
|
+
partialPrerenders.add(route.id)
|
|
551
549
|
}
|
|
552
|
-
if (isGenerate) generateRouteLines.push(` ◐ ${route.route || '/'}`)
|
|
553
|
-
continue
|
|
550
|
+
if (isGenerate) generateRouteLines.push(` ◐ ${route.route || '/'}`)
|
|
551
|
+
continue
|
|
554
552
|
}
|
|
555
553
|
}
|
|
556
554
|
// A whole-client page cannot consume hanging dynamic params during a shell
|
|
557
555
|
// prerender. Keep it runtime-only until that boundary can postpone.
|
|
558
|
-
if (cacheComponents() && route.client && (route.params.length > 0 || route.catchAll)) continue
|
|
556
|
+
if (cacheComponents() && route.client && (route.params.length > 0 || route.catchAll)) continue
|
|
559
557
|
|
|
560
558
|
// Segment config gates prerendering: force-dynamic / revalidate 0 /
|
|
561
559
|
// force-no-store routes are never prebuilt; force-static prerenders even
|
|
562
560
|
// when the route reads request data (with an empty synthetic request,
|
|
563
561
|
// Next-style).
|
|
564
|
-
const segmentConfig = route.segmentConfig
|
|
565
|
-
const forceStatic = segmentConfig?.dynamic === 'force-static'
|
|
562
|
+
const segmentConfig = route.segmentConfig
|
|
563
|
+
const forceStatic = segmentConfig?.dynamic === 'force-static'
|
|
566
564
|
if (forceStatic && segmentConfig?.runtime === 'edge') {
|
|
567
565
|
console.warn(
|
|
568
566
|
`Page "${route.route}" is using runtime = 'edge' which is currently incompatible with dynamic = 'force-static'. Please remove either "runtime" or "force-static" for correct behavior`,
|
|
569
|
-
)
|
|
567
|
+
)
|
|
570
568
|
}
|
|
571
569
|
const configForcesDynamic =
|
|
572
570
|
segmentConfig?.dynamic === 'force-dynamic' ||
|
|
@@ -576,9 +574,9 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
576
574
|
// absent from its prerender manifest / static output); their data
|
|
577
575
|
// stability comes from the fetch cache, not prebuilt HTML. force-static
|
|
578
576
|
// keeps the existing (warned-about) prerender behavior.
|
|
579
|
-
(segmentConfig?.runtime === 'edge' && !forceStatic)
|
|
577
|
+
(segmentConfig?.runtime === 'edge' && !forceStatic)
|
|
580
578
|
|
|
581
|
-
let staticParams: Awaited<ReturnType<typeof staticParamsFor>> | null = null
|
|
579
|
+
let staticParams: Awaited<ReturnType<typeof staticParamsFor>> | null = null
|
|
582
580
|
if (!configForcesDynamic && route.hasStaticParams) {
|
|
583
581
|
try {
|
|
584
582
|
staticParams = await log.step(`static params ${route.route}`, () =>
|
|
@@ -591,24 +589,24 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
591
589
|
prerender: true,
|
|
592
590
|
})
|
|
593
591
|
.then(result => result.value),
|
|
594
|
-
)
|
|
592
|
+
)
|
|
595
593
|
} catch (error) {
|
|
596
|
-
rethrowIfProgrammingError(error)
|
|
597
|
-
if (!isAfterPrerenderError(error)) throw error
|
|
594
|
+
rethrowIfProgrammingError(error)
|
|
595
|
+
if (!isAfterPrerenderError(error)) throw error
|
|
598
596
|
// Next reports a generateStaticParams failure with this exact prefix
|
|
599
597
|
// (naming the route in `[param]` form), followed by the underlying
|
|
600
598
|
// error (the thrown after() message).
|
|
601
|
-
console.error(`Failed to collect page data for ${toNextRoutePattern(route.route)}`)
|
|
602
|
-
console.error(error instanceof Error ? (error.stack ?? error.message) : String(error))
|
|
603
|
-
hadAfterPrerenderError = true
|
|
604
|
-
continue
|
|
599
|
+
console.error(`Failed to collect page data for ${toNextRoutePattern(route.route)}`)
|
|
600
|
+
console.error(error instanceof Error ? (error.stack ?? error.message) : String(error))
|
|
601
|
+
hadAfterPrerenderError = true
|
|
602
|
+
continue
|
|
605
603
|
}
|
|
606
604
|
}
|
|
607
605
|
// Dynamic request APIs normally skip page prerendering. An unstable_cache
|
|
608
606
|
// boundary is the exception: Next executes the fill during prerender so a
|
|
609
607
|
// request API read inside it can fail the build at the exact call site.
|
|
610
608
|
const validateUnstableCacheScope =
|
|
611
|
-
!configForcesDynamic && route.usesRequest && !forceStatic && (await usesUnstableCache(route))
|
|
609
|
+
!configForcesDynamic && route.usesRequest && !forceStatic && (await usesUnstableCache(route))
|
|
612
610
|
const paramSets = staticParams
|
|
613
611
|
? staticParams.paths
|
|
614
612
|
: validateUnstableCacheScope
|
|
@@ -617,10 +615,10 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
617
615
|
? []
|
|
618
616
|
: route.mode === 'static' && route.params.length === 0 && !route.catchAll
|
|
619
617
|
? [{}]
|
|
620
|
-
: []
|
|
618
|
+
: []
|
|
621
619
|
// Partial sets included: dynamicParams=false checks match governed params
|
|
622
620
|
// against these at request time.
|
|
623
|
-
if (staticParams) route.prerenderedParams = staticParams.allSets
|
|
621
|
+
if (staticParams) route.prerenderedParams = staticParams.allSets
|
|
624
622
|
// NEXT_DEBUG_BUILD parity: Next logs why a route that would otherwise have
|
|
625
623
|
// been prerendered fell back to dynamic (the e2e suite greps this line).
|
|
626
624
|
// This branch covers the request-API bailout (headers()/cookies() in the
|
|
@@ -635,7 +633,7 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
635
633
|
route.params.length === 0 &&
|
|
636
634
|
!route.catchAll
|
|
637
635
|
) {
|
|
638
|
-
logStaticBailout(route.route || '/', await requestApiBailoutReason(route))
|
|
636
|
+
logStaticBailout(route.route || '/', await requestApiBailoutReason(route))
|
|
639
637
|
}
|
|
640
638
|
if (paramSets.length === 0) {
|
|
641
639
|
// Next prerenders every non-force-dynamic page and only marks it dynamic once a request API
|
|
@@ -651,12 +649,11 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
651
649
|
!route.catchAll &&
|
|
652
650
|
(await schedulesDeferredWork(route))
|
|
653
651
|
) {
|
|
654
|
-
await probeDeferredDynamicUsage(config, route, staticMetadataFiles, staticModuleMetadata)
|
|
652
|
+
await probeDeferredDynamicUsage(config, route, staticMetadataFiles, staticModuleMetadata)
|
|
655
653
|
}
|
|
656
|
-
continue
|
|
654
|
+
continue
|
|
657
655
|
}
|
|
658
|
-
if (!forceStatic && (await needsRequestOriginForMetadataImages(config.appPath, route)))
|
|
659
|
-
continue;
|
|
656
|
+
if (!forceStatic && (await needsRequestOriginForMetadataImages(config.appPath, route))) continue
|
|
660
657
|
|
|
661
658
|
await log.step(
|
|
662
659
|
`prerender ${route.route} (${paramSets.length} page${paramSets.length === 1 ? '' : 's'})`,
|
|
@@ -666,15 +663,15 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
666
663
|
// generateStaticParams value, so a page sees `sticks%20%26%20stones`,
|
|
667
664
|
// never the raw `sticks & stones` (prerender-encoding suite). File
|
|
668
665
|
// layout and dynamicParams matching keep the raw values.
|
|
669
|
-
const params = encodeStaticRouteParams(rawParams)
|
|
670
|
-
const routePath = fillRoutePath(route.route, rawParams)
|
|
671
|
-
const file = staticHtmlPath(config.outPath, routePath, flatExportLayout)
|
|
672
|
-
const collision = staticOutputCollision(config.outPath, file)
|
|
666
|
+
const params = encodeStaticRouteParams(rawParams)
|
|
667
|
+
const routePath = fillRoutePath(route.route, rawParams)
|
|
668
|
+
const file = staticHtmlPath(config.outPath, routePath, flatExportLayout)
|
|
669
|
+
const collision = staticOutputCollision(config.outPath, file)
|
|
673
670
|
if (collision) {
|
|
674
|
-
warnSkippedStatic(routePath, collision)
|
|
675
|
-
continue
|
|
671
|
+
warnSkippedStatic(routePath, collision)
|
|
672
|
+
continue
|
|
676
673
|
}
|
|
677
|
-
const url = new URL(`http://pnext.local${routePath}`)
|
|
674
|
+
const url = new URL(`http://pnext.local${routePath}`)
|
|
678
675
|
// A single page prerender failure must not kill the whole build:
|
|
679
676
|
// skip the static copy and let the route serve dynamically.
|
|
680
677
|
try {
|
|
@@ -682,71 +679,74 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
682
679
|
// stashes its effective revalidate/expire/stale (stashCacheLife
|
|
683
680
|
// reads the work-unit scope). We read + persist it below so a pure
|
|
684
681
|
// static HIT can re-emit the SWR headers from start.ts.
|
|
685
|
-
let cacheLife: CacheLifeStash | undefined
|
|
686
|
-
let fontLinkHeader: string | undefined
|
|
687
|
-
let prerenderVary: BuildResponseVary | undefined
|
|
682
|
+
let cacheLife: CacheLifeStash | undefined
|
|
683
|
+
let fontLinkHeader: string | undefined
|
|
684
|
+
let prerenderVary: BuildResponseVary | undefined
|
|
688
685
|
const rendered = await runWithWorkUnit('render', async () => {
|
|
689
686
|
const tracked = await buildCompat().withVaryParamsTracking(() =>
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
687
|
+
getRenderExtensions().collectRenderMeta(
|
|
688
|
+
() =>
|
|
689
|
+
runWithNextBuildPhase(() =>
|
|
690
|
+
withRouteRuntime(route.segmentConfig?.runtime, () =>
|
|
691
|
+
renderPageWithStatus({
|
|
692
|
+
config,
|
|
693
|
+
route,
|
|
694
|
+
params,
|
|
695
|
+
url,
|
|
696
|
+
// Empty request so request APIs (cookies/headers) resolve
|
|
697
|
+
// to empty values instead of failing the prerender.
|
|
698
|
+
...(route.usesRequest ? { request: new Request(url) } : {}),
|
|
699
|
+
staticMetadataFiles,
|
|
700
|
+
staticModuleMetadata,
|
|
701
|
+
resolveDynamicMetadataRoutes: true,
|
|
702
|
+
}),
|
|
703
|
+
),
|
|
706
704
|
),
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
705
|
+
{
|
|
706
|
+
fetchCache: segmentConfig?.fetchCache,
|
|
707
|
+
route: routePath || '/',
|
|
708
|
+
prerender: true,
|
|
709
|
+
},
|
|
710
|
+
),
|
|
711
|
+
)
|
|
712
|
+
const result = tracked.value
|
|
713
|
+
prerenderVary = tracked.vary
|
|
714
|
+
cacheLife = buildCompat().takeCacheLifeStash()
|
|
714
715
|
// A static prerender never runs the response finalizer that flushes
|
|
715
716
|
// font preloads to the `Link` header, so bake it into the manifest
|
|
716
717
|
// headers here (must read the work unit before it unwinds).
|
|
717
|
-
fontLinkHeader = buildCompat().takeFontLinkHeader()
|
|
718
|
-
return result
|
|
719
|
-
})
|
|
718
|
+
fontLinkHeader = buildCompat().takeFontLinkHeader()
|
|
719
|
+
return result
|
|
720
|
+
})
|
|
720
721
|
// Explicit no-store signals (fetch no-store/no-cache, revalidate
|
|
721
722
|
// 0, unstable_noStore) keep the route dynamic — unless the
|
|
722
723
|
// segment config explicitly opts into static/ISR output.
|
|
723
724
|
const optedStatic =
|
|
724
725
|
forceStatic ||
|
|
725
726
|
typeof segmentConfig?.revalidate === 'number' ||
|
|
726
|
-
segmentConfig?.revalidate === false
|
|
727
|
+
segmentConfig?.revalidate === false
|
|
727
728
|
if (rendered.noStore && !optedStatic) {
|
|
728
|
-
if (process.env.NEXT_DEBUG_BUILD)
|
|
729
|
-
|
|
730
|
-
continue;
|
|
729
|
+
if (process.env.NEXT_DEBUG_BUILD) logStaticBailout(routePath || '/', 'no-store fetch')
|
|
730
|
+
continue
|
|
731
731
|
}
|
|
732
|
-
await writeText(file, rendered.value.html)
|
|
733
|
-
recordPrerenderVary(route, routePath || '/', prerenderVary)
|
|
732
|
+
await writeText(file, rendered.value.html)
|
|
733
|
+
recordPrerenderVary(route, routePath || '/', prerenderVary)
|
|
734
734
|
// Prerendered paths print like Next's build output; tooling (and
|
|
735
735
|
// the Next e2e suite) greps for them. Generate mode defers them
|
|
736
736
|
// below the `Route (app)` header so the diagnostics capture window
|
|
737
737
|
// stays clean.
|
|
738
|
-
const routeLine = `${dim(' ○')} ${routePath || '/'}
|
|
739
|
-
if (isGenerate) generateRouteLines.push(routeLine)
|
|
740
|
-
else console.log(routeLine)
|
|
738
|
+
const routeLine = `${dim(' ○')} ${routePath || '/'}`
|
|
739
|
+
if (isGenerate) generateRouteLines.push(routeLine)
|
|
740
|
+
else console.log(routeLine)
|
|
741
741
|
await emitConcreteNextPageArtifacts(config, route, routePath || '/', {
|
|
742
742
|
html: rendered.value.html,
|
|
743
743
|
status: rendered.value.status,
|
|
744
|
-
})
|
|
745
|
-
const relative = toPosixPath(path.relative(path.join(config.outPath, 'public'), file))
|
|
744
|
+
})
|
|
745
|
+
const relative = toPosixPath(path.relative(path.join(config.outPath, 'public'), file))
|
|
746
746
|
const revalidateSeconds = combineRevalidate(
|
|
747
747
|
segmentConfig?.revalidate,
|
|
748
748
|
rendered.revalidateSeconds,
|
|
749
|
-
)
|
|
749
|
+
)
|
|
750
750
|
// A `use cache` render stashes its effective cacheLife; the SWR
|
|
751
751
|
// cache-control + x-nextjs-stale-time normally come from a response
|
|
752
752
|
// finalizer, but a pure static HIT never re-renders, so we bake the
|
|
@@ -754,7 +754,7 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
754
754
|
// start.ts can re-emit them on the HIT. The work-unit stash is empty
|
|
755
755
|
// when the build prerender ran without a request work unit; the
|
|
756
756
|
// render cache meta carries the same windows, so fall back to it.
|
|
757
|
-
const defaultExpireTime = buildCompat().defaultExpireTimeSeconds()
|
|
757
|
+
const defaultExpireTime = buildCompat().defaultExpireTimeSeconds()
|
|
758
758
|
const effectiveCacheLife: CacheLifeStash | undefined =
|
|
759
759
|
cacheLife ??
|
|
760
760
|
(rendered.expireSeconds !== undefined || rendered.staleSeconds !== undefined
|
|
@@ -772,15 +772,15 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
772
772
|
revalidateSeconds,
|
|
773
773
|
expireSeconds: defaultExpireTime,
|
|
774
774
|
}
|
|
775
|
-
: undefined)
|
|
776
|
-
const cacheLifeHeaders = cacheLifeResponseHeaders(effectiveCacheLife)
|
|
775
|
+
: undefined)
|
|
776
|
+
const cacheLifeHeaders = cacheLifeResponseHeaders(effectiveCacheLife)
|
|
777
777
|
const headers: [string, string][] = [
|
|
778
778
|
...(rendered.value.location
|
|
779
779
|
? [['location', rendered.value.location] as [string, string]]
|
|
780
780
|
: []),
|
|
781
781
|
...(fontLinkHeader ? [['link', fontLinkHeader] as [string, string]] : []),
|
|
782
782
|
...cacheLifeHeaders,
|
|
783
|
-
]
|
|
783
|
+
]
|
|
784
784
|
staticFiles[relative] = {
|
|
785
785
|
status: rendered.value.status,
|
|
786
786
|
headers,
|
|
@@ -794,15 +794,15 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
794
794
|
? { staleSeconds: effectiveCacheLife.staleSeconds }
|
|
795
795
|
: {}),
|
|
796
796
|
...(rendered.tags.length > 0 ? { tags: rendered.tags } : {}),
|
|
797
|
-
}
|
|
797
|
+
}
|
|
798
798
|
} catch (error) {
|
|
799
|
-
rethrowIfProgrammingError(error)
|
|
800
|
-
if (isAfterPrerenderError(error)) hadAfterPrerenderError = true
|
|
801
|
-
warnSkippedStatic(routePath, error instanceof Error ? error.message : String(error))
|
|
799
|
+
rethrowIfProgrammingError(error)
|
|
800
|
+
if (isAfterPrerenderError(error)) hadAfterPrerenderError = true
|
|
801
|
+
warnSkippedStatic(routePath, error instanceof Error ? error.message : String(error))
|
|
802
802
|
}
|
|
803
803
|
}
|
|
804
804
|
},
|
|
805
|
-
)
|
|
805
|
+
)
|
|
806
806
|
}
|
|
807
807
|
|
|
808
808
|
// Generate-mode prerender diagnostics fail the pass with Next's exact
|
|
@@ -812,34 +812,34 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
812
812
|
for (const failed of generateFailures) {
|
|
813
813
|
console.error(
|
|
814
814
|
buildCompat().prerenderFailureFooter(failed.route, debugPrerender, failed.omitErrorLine),
|
|
815
|
-
)
|
|
815
|
+
)
|
|
816
816
|
}
|
|
817
|
-
console.error('Next.js build worker exited with code: 1 and signal: null')
|
|
818
|
-
process.exit(1)
|
|
817
|
+
console.error('Next.js build worker exited with code: 1 and signal: null')
|
|
818
|
+
process.exit(1)
|
|
819
819
|
}
|
|
820
820
|
if (isGenerate) {
|
|
821
|
-
console.log('\nRoute (app)')
|
|
822
|
-
for (const line of generateRouteLines) console.log(line)
|
|
821
|
+
console.log('\nRoute (app)')
|
|
822
|
+
for (const line of generateRouteLines) console.log(line)
|
|
823
823
|
}
|
|
824
824
|
|
|
825
825
|
// Fail the build after every route has been given the chance to log its
|
|
826
826
|
// prerender error (matching `prerenderEarlyExit: false`).
|
|
827
827
|
if (hadAfterPrerenderError) {
|
|
828
|
-
throw new Error('Build failed because an error was thrown inside `after()` while prerendering.')
|
|
828
|
+
throw new Error('Build failed because an error was thrown inside `after()` while prerendering.')
|
|
829
829
|
}
|
|
830
830
|
if (hadDynamicErrorFailure) {
|
|
831
831
|
throw new Error(
|
|
832
832
|
'Build failed because a route with `dynamic = "error"` read dynamic data during prerendering.',
|
|
833
|
-
)
|
|
833
|
+
)
|
|
834
834
|
}
|
|
835
835
|
|
|
836
836
|
const interceptionPrerenders = isCompile
|
|
837
837
|
? new Map<string, string[]>()
|
|
838
|
-
: await logInterceptionPrerenders(config, routes)
|
|
838
|
+
: await logInterceptionPrerenders(config, routes)
|
|
839
839
|
|
|
840
|
-
const fallback404 = await notFoundDocuments
|
|
841
|
-
await emitNextNotFoundArtifacts(config, fallback404)
|
|
842
|
-
await emitProxyServerArtifacts(config, proxyModule)
|
|
840
|
+
const fallback404 = await notFoundDocuments
|
|
841
|
+
await emitNextNotFoundArtifacts(config, fallback404)
|
|
842
|
+
await emitProxyServerArtifacts(config, proxyModule)
|
|
843
843
|
|
|
844
844
|
// Next always prerenders the built-in `/_not-found` and `/_global-error`
|
|
845
845
|
// pseudo-routes for an app-router build (even when the app ships no custom
|
|
@@ -855,25 +855,25 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
855
855
|
// it and the prerender manifest omits the route. The hybrid pages+app
|
|
856
856
|
// materializer synthesizes an app/layout.js, so inspect the ORIGINAL app
|
|
857
857
|
// (the shim's `source-app` symlink) when it exists.
|
|
858
|
-
const sourceApp = path.join(config.appPath, '..', 'source-app')
|
|
859
|
-
const layoutRoot = existsSync(sourceApp) ? sourceApp : config.appPath
|
|
858
|
+
const sourceApp = path.join(config.appPath, '..', 'source-app')
|
|
859
|
+
const layoutRoot = existsSync(sourceApp) ? sourceApp : config.appPath
|
|
860
860
|
const hasTopLevelRootLayout = ['tsx', 'ts', 'jsx', 'js'].some(ext =>
|
|
861
861
|
existsSync(path.join(layoutRoot, `layout.${ext}`)),
|
|
862
|
-
)
|
|
862
|
+
)
|
|
863
863
|
const pseudos = hasTopLevelRootLayout
|
|
864
864
|
? ['_not-found.html', '_global-error.html']
|
|
865
|
-
: ['_not-found.html']
|
|
865
|
+
: ['_not-found.html']
|
|
866
866
|
for (const pseudo of pseudos) {
|
|
867
867
|
if (!(pseudo in staticFiles)) {
|
|
868
|
-
staticFiles[pseudo] = { status: 200, headers: [], kind: 'page' }
|
|
868
|
+
staticFiles[pseudo] = { status: 200, headers: [], kind: 'page' }
|
|
869
869
|
}
|
|
870
870
|
}
|
|
871
871
|
}
|
|
872
872
|
|
|
873
873
|
// The action modules' server compile ran under the client stage; the manifest
|
|
874
874
|
// carries its entries, so this is where it has to have landed.
|
|
875
|
-
await deferredSteps
|
|
876
|
-
const actions = buildState.actions
|
|
875
|
+
await deferredSteps
|
|
876
|
+
const actions = buildState.actions
|
|
877
877
|
const manifest: BuildManifest = {
|
|
878
878
|
version: 0,
|
|
879
879
|
root: config.root,
|
|
@@ -892,22 +892,22 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
892
892
|
: {}),
|
|
893
893
|
...(proxyModule ? { proxyModule } : {}),
|
|
894
894
|
...(actions.length > 0 ? { actions } : {}),
|
|
895
|
-
}
|
|
896
|
-
await writeBuildManifest(config.outPath, manifest)
|
|
897
|
-
log.log(`wrote manifest.json (${routes.length} route${routes.length === 1 ? '' : 's'})`)
|
|
898
|
-
await log.step('server entry', () => serverEntryDone)
|
|
895
|
+
}
|
|
896
|
+
await writeBuildManifest(config.outPath, manifest)
|
|
897
|
+
log.log(`wrote manifest.json (${routes.length} route${routes.length === 1 ? '' : 's'})`)
|
|
898
|
+
await log.step('server entry', () => serverEntryDone)
|
|
899
899
|
for (const hook of getBuildExtensions().completeHooks) {
|
|
900
|
-
await hook({ config, manifest, log })
|
|
900
|
+
await hook({ config, manifest, log })
|
|
901
901
|
}
|
|
902
902
|
if (options.adapter === 'vercel')
|
|
903
903
|
await log.step('vercel adapter output', () =>
|
|
904
904
|
writeVercelOutput(config, manifest, { verbose, warm }),
|
|
905
|
-
)
|
|
905
|
+
)
|
|
906
906
|
|
|
907
907
|
// Bundling is done; the build metric stops here. Phases that ran alongside it
|
|
908
908
|
// (typecheck) are awaited next and reported on their own lines.
|
|
909
|
-
const buildDurationMs = performance.now() - startedAt
|
|
910
|
-
const phases = await settleBuildParallelPhases()
|
|
909
|
+
const buildDurationMs = performance.now() - startedAt
|
|
910
|
+
const phases = await settleBuildParallelPhases()
|
|
911
911
|
|
|
912
912
|
printBuildSummary(
|
|
913
913
|
config,
|
|
@@ -918,23 +918,23 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
918
918
|
interceptionPrerenders,
|
|
919
919
|
partialPrerenders,
|
|
920
920
|
phases,
|
|
921
|
-
)
|
|
922
|
-
return manifest
|
|
921
|
+
)
|
|
922
|
+
return manifest
|
|
923
923
|
}
|
|
924
924
|
|
|
925
925
|
/** Run build steps in registration order, each timed under its own label. */
|
|
926
926
|
async function runBuildSteps(steps: BuildStep[], ctx: BuildStepContext) {
|
|
927
927
|
for (const [index, step] of steps.entries()) {
|
|
928
|
-
await ctx.log.step(`build step ${step.name || index}`, () => step(ctx))
|
|
928
|
+
await ctx.log.step(`build step ${step.name || index}`, () => step(ctx))
|
|
929
929
|
}
|
|
930
930
|
}
|
|
931
931
|
|
|
932
932
|
/** Await every background phase, returning each one's own elapsed time. */
|
|
933
933
|
async function settleBuildParallelPhases() {
|
|
934
|
-
const phases = getBuildExtensions().parallelPhases
|
|
935
|
-
const timings: { name: string; durationMs: number }[] = []
|
|
936
|
-
for (const phase of phases) timings.push({ name: phase.name, durationMs: await phase.run })
|
|
937
|
-
return timings
|
|
934
|
+
const phases = getBuildExtensions().parallelPhases
|
|
935
|
+
const timings: { name: string; durationMs: number }[] = []
|
|
936
|
+
for (const phase of phases) timings.push({ name: phase.name, durationMs: await phase.run })
|
|
937
|
+
return timings
|
|
938
938
|
}
|
|
939
939
|
|
|
940
940
|
/**
|
|
@@ -948,33 +948,35 @@ async function logInterceptionPrerenders(
|
|
|
948
948
|
config: Awaited<ReturnType<typeof loadConfig>>,
|
|
949
949
|
routes: RouteManifestEntry[],
|
|
950
950
|
): Promise<Map<string, string[]>> {
|
|
951
|
-
const prerenders = new Map<string, string[]>()
|
|
951
|
+
const prerenders = new Map<string, string[]>()
|
|
952
952
|
for (const route of routes) {
|
|
953
|
-
if (route.kind !== 'page' || !route.interception) continue
|
|
954
|
-
if (!routeFileExportsStaticParams(route.file)) continue
|
|
955
|
-
let paths: string[]
|
|
953
|
+
if (route.kind !== 'page' || !route.interception) continue
|
|
954
|
+
if (!routeFileExportsStaticParams(route.file)) continue
|
|
955
|
+
let paths: string[]
|
|
956
956
|
try {
|
|
957
|
-
const staticParams = await staticParamsFor(config, route)
|
|
957
|
+
const staticParams = await staticParamsFor(config, route)
|
|
958
958
|
paths = staticParams.paths.map(params =>
|
|
959
959
|
interceptionDisplayRoute(route, fillRoutePath(route.route, params)),
|
|
960
|
-
)
|
|
960
|
+
)
|
|
961
961
|
} catch (error) {
|
|
962
|
-
rethrowIfProgrammingError(error)
|
|
963
|
-
warnSkippedStatic(route.route, error instanceof Error ? error.message : String(error))
|
|
964
|
-
continue
|
|
962
|
+
rethrowIfProgrammingError(error)
|
|
963
|
+
warnSkippedStatic(route.route, error instanceof Error ? error.message : String(error))
|
|
964
|
+
continue
|
|
965
965
|
}
|
|
966
|
-
if (paths.length === 0) continue
|
|
967
|
-
prerenders.set(route.id, paths)
|
|
968
|
-
for (const routePath of paths) console.log(`${dim(' ○')} ${routePath}`)
|
|
966
|
+
if (paths.length === 0) continue
|
|
967
|
+
prerenders.set(route.id, paths)
|
|
968
|
+
for (const routePath of paths) console.log(`${dim(' ○')} ${routePath}`)
|
|
969
969
|
}
|
|
970
|
-
return prerenders
|
|
970
|
+
return prerenders
|
|
971
971
|
}
|
|
972
972
|
|
|
973
973
|
function routeFileExportsStaticParams(file: string): boolean {
|
|
974
|
-
if (!existsSync(file)) return false
|
|
975
|
-
const source = readTextSync(file)
|
|
976
|
-
return
|
|
977
|
-
/\bexport\s+
|
|
974
|
+
if (!existsSync(file)) return false
|
|
975
|
+
const source = readTextSync(file)
|
|
976
|
+
return (
|
|
977
|
+
/\bexport\s+(?:async\s+)?function\s+generateStaticParams\b/.test(source) ||
|
|
978
|
+
/\bexport\s+const\s+generateStaticParams\b/.test(source)
|
|
979
|
+
)
|
|
978
980
|
}
|
|
979
981
|
|
|
980
982
|
/**
|
|
@@ -985,39 +987,39 @@ function routeFileExportsStaticParams(file: string): boolean {
|
|
|
985
987
|
* marker is re-inserted at the segment the rewind landed on.
|
|
986
988
|
*/
|
|
987
989
|
function interceptionDisplayRoute(route: RouteManifestEntry, filled?: string): string {
|
|
988
|
-
const interception = route.interception
|
|
989
|
-
if (!interception) return route.route
|
|
990
|
-
const base = interception.base === '/' ? [] : interception.base.split('/').filter(Boolean)
|
|
991
|
-
const levels = interceptionMarkerLevels(interception.marker)
|
|
992
|
-
const target = (filled ?? toNextRoutePattern(route.route)).split('/').filter(Boolean)
|
|
990
|
+
const interception = route.interception
|
|
991
|
+
if (!interception) return route.route
|
|
992
|
+
const base = interception.base === '/' ? [] : interception.base.split('/').filter(Boolean)
|
|
993
|
+
const levels = interceptionMarkerLevels(interception.marker)
|
|
994
|
+
const target = (filled ?? toNextRoutePattern(route.route)).split('/').filter(Boolean)
|
|
993
995
|
// `route.route` is `<kept base>/<segments below the marker>`; the marker dir
|
|
994
996
|
// itself is the first segment past what the rewind kept.
|
|
995
|
-
const cut = Number.isFinite(levels) ? Math.max(0, base.length - levels) : 0
|
|
996
|
-
const below = target.slice(cut)
|
|
997
|
-
const segments = [...base, `${interception.marker}${below[0] ?? ''}`, ...below.slice(1)]
|
|
998
|
-
return `/${segments.join('/')}
|
|
997
|
+
const cut = Number.isFinite(levels) ? Math.max(0, base.length - levels) : 0
|
|
998
|
+
const below = target.slice(cut)
|
|
999
|
+
const segments = [...base, `${interception.marker}${below[0] ?? ''}`, ...below.slice(1)]
|
|
1000
|
+
return `/${segments.join('/')}`
|
|
999
1001
|
}
|
|
1000
1002
|
|
|
1001
1003
|
async function writeBuildManifest(outPath: string, manifest: BuildManifest) {
|
|
1002
|
-
assertManifestServerArtifacts(outPath, manifest)
|
|
1003
|
-
const file = path.join(outPath, 'manifest.json')
|
|
1004
|
-
const temporary = path.join(outPath, `.manifest-${process.pid}.tmp`)
|
|
1005
|
-
await writeFile(temporary, `${JSON.stringify(manifest, null, 2)}\n`)
|
|
1006
|
-
await rename(temporary, file)
|
|
1004
|
+
assertManifestServerArtifacts(outPath, manifest)
|
|
1005
|
+
const file = path.join(outPath, 'manifest.json')
|
|
1006
|
+
const temporary = path.join(outPath, `.manifest-${process.pid}.tmp`)
|
|
1007
|
+
await writeFile(temporary, `${JSON.stringify(manifest, null, 2)}\n`)
|
|
1008
|
+
await rename(temporary, file)
|
|
1007
1009
|
}
|
|
1008
1010
|
|
|
1009
1011
|
function assertManifestServerArtifacts(outPath: string, manifest: BuildManifest) {
|
|
1010
1012
|
const artifacts = [
|
|
1011
1013
|
...(manifest.proxyModule ? [manifest.proxyModule] : []),
|
|
1012
1014
|
...(manifest.actions?.map(action => action.modulePath) ?? []),
|
|
1013
|
-
]
|
|
1015
|
+
]
|
|
1014
1016
|
for (const artifact of artifacts) {
|
|
1015
|
-
const file = path.resolve(outPath, artifact)
|
|
1017
|
+
const file = path.resolve(outPath, artifact)
|
|
1016
1018
|
if (file !== outPath && !file.startsWith(`${outPath}${path.sep}`)) {
|
|
1017
|
-
throw new Error(`Build manifest server artifact escapes output directory: ${artifact}`)
|
|
1019
|
+
throw new Error(`Build manifest server artifact escapes output directory: ${artifact}`)
|
|
1018
1020
|
}
|
|
1019
1021
|
if (!existsSync(file)) {
|
|
1020
|
-
throw new Error(`Build manifest server artifact is missing: ${artifact}`)
|
|
1022
|
+
throw new Error(`Build manifest server artifact is missing: ${artifact}`)
|
|
1021
1023
|
}
|
|
1022
1024
|
}
|
|
1023
1025
|
}
|
|
@@ -1032,18 +1034,17 @@ function printBuildSummary(
|
|
|
1032
1034
|
partialPrerenders = new Set<string>(),
|
|
1033
1035
|
parallelPhases: { name: string; durationMs: number }[] = [],
|
|
1034
1036
|
) {
|
|
1035
|
-
const pages = routes.filter(route => route.kind !== 'handler').length
|
|
1036
|
-
const handlers = routes.length - pages
|
|
1037
|
-
const parts = [`${pages} ${pages === 1 ? 'page' : 'pages'}`]
|
|
1038
|
-
if (handlers > 0)
|
|
1039
|
-
|
|
1040
|
-
const outDir = path.relative(config.root, config.outPath) || '.';
|
|
1037
|
+
const pages = routes.filter(route => route.kind !== 'handler').length
|
|
1038
|
+
const handlers = routes.length - pages
|
|
1039
|
+
const parts = [`${pages} ${pages === 1 ? 'page' : 'pages'}`]
|
|
1040
|
+
if (handlers > 0) parts.push(`${handlers} ${handlers === 1 ? 'route handler' : 'route handlers'}`)
|
|
1041
|
+
const outDir = path.relative(config.root, config.outPath) || '.'
|
|
1041
1042
|
|
|
1042
1043
|
// Route table: ○ static, ● SSG (params), ◐ partial prerender, ƒ dynamic.
|
|
1043
|
-
console.log('')
|
|
1044
|
-
console.log('Route (app)')
|
|
1044
|
+
console.log('')
|
|
1045
|
+
console.log('Route (app)')
|
|
1045
1046
|
for (const route of routes) {
|
|
1046
|
-
const intercepted = interceptionPrerenders.get(route.id)
|
|
1047
|
+
const intercepted = interceptionPrerenders.get(route.id)
|
|
1047
1048
|
const marker =
|
|
1048
1049
|
route.kind === 'handler'
|
|
1049
1050
|
? Object.values(staticFiles).some(file => file.routeId === route.id)
|
|
@@ -1057,36 +1058,36 @@ function printBuildSummary(
|
|
|
1057
1058
|
? 'ƒ'
|
|
1058
1059
|
: route.hasStaticParams
|
|
1059
1060
|
? '●'
|
|
1060
|
-
: '○'
|
|
1061
|
+
: '○'
|
|
1061
1062
|
// An interception route is named by its own directory path (marker kept),
|
|
1062
1063
|
// never by the target route it resolves to — which is a real route of its own.
|
|
1063
|
-
const label = route.interception ? interceptionDisplayRoute(route) : route.route || '/'
|
|
1064
|
+
const label = route.interception ? interceptionDisplayRoute(route) : route.route || '/'
|
|
1064
1065
|
// Next names a dynamic route by its source pattern (`/[dyn]`), not pnext's
|
|
1065
1066
|
// `:param` form; compat apps' e2e output is matched against Next's.
|
|
1066
|
-
console.log(` ${marker} ${nextCompatEnabled(config) ? toNextRoutePattern(label) : label}`)
|
|
1067
|
+
console.log(` ${marker} ${nextCompatEnabled(config) ? toNextRoutePattern(label) : label}`)
|
|
1067
1068
|
}
|
|
1068
1069
|
for (const file of staticMetadataSummaryFiles(staticFiles)) {
|
|
1069
|
-
console.log(` ${dim('○')} /${file}`)
|
|
1070
|
+
console.log(` ${dim('○')} /${file}`)
|
|
1070
1071
|
}
|
|
1071
1072
|
if (hasMiddleware) {
|
|
1072
|
-
console.log('')
|
|
1073
|
-
console.log(` ${dim('ƒ')} Middleware`)
|
|
1073
|
+
console.log('')
|
|
1074
|
+
console.log(` ${dim('ƒ')} Middleware`)
|
|
1074
1075
|
}
|
|
1075
|
-
console.log('')
|
|
1076
|
+
console.log('')
|
|
1076
1077
|
console.log(
|
|
1077
1078
|
`${green('✓')} ${bold('Build complete')} ${dim(`in ${formatBuildDuration(durationMs)}`)}`,
|
|
1078
|
-
)
|
|
1079
|
+
)
|
|
1079
1080
|
// Phases that ran alongside the build are their own metric, never folded into
|
|
1080
1081
|
// the build time — a typecheck fully hidden behind bundling costs 0 wall.
|
|
1081
1082
|
for (const phase of parallelPhases) {
|
|
1082
1083
|
console.log(
|
|
1083
1084
|
`${green('✓')} ${bold(`${phase.name} complete`)} ${dim(`in ${formatBuildDuration(phase.durationMs)}`)}`,
|
|
1084
|
-
)
|
|
1085
|
+
)
|
|
1085
1086
|
}
|
|
1086
|
-
console.log(` ${dim(`${parts.join(', ')} → ${outDir}`)}`)
|
|
1087
|
-
console.log('')
|
|
1088
|
-
console.log(` Run ${cyan('pnext start')} to serve the production build.`)
|
|
1089
|
-
console.log(` ${cyan('pnext analyze')} to analyze the build.`)
|
|
1087
|
+
console.log(` ${dim(`${parts.join(', ')} → ${outDir}`)}`)
|
|
1088
|
+
console.log('')
|
|
1089
|
+
console.log(` Run ${cyan('pnext start')} to serve the production build.`)
|
|
1090
|
+
console.log(` ${cyan('pnext analyze')} to analyze the build.`)
|
|
1090
1091
|
}
|
|
1091
1092
|
|
|
1092
1093
|
function staticMetadataSummaryFiles(staticFiles: Record<string, StaticFileMetadata>) {
|
|
@@ -1098,15 +1099,15 @@ function staticMetadataSummaryFiles(staticFiles: Record<string, StaticFileMetada
|
|
|
1098
1099
|
),
|
|
1099
1100
|
)
|
|
1100
1101
|
.map(([file]) => file)
|
|
1101
|
-
.sort()
|
|
1102
|
+
.sort()
|
|
1102
1103
|
}
|
|
1103
1104
|
|
|
1104
1105
|
function formatBuildDuration(durationMs: number) {
|
|
1105
|
-
const totalSeconds = Math.max(0, durationMs) / 1000
|
|
1106
|
-
if (totalSeconds < 60) return `${totalSeconds.toFixed(totalSeconds < 10 ? 2 : 1)}s
|
|
1107
|
-
const minutes = Math.floor(totalSeconds / 60)
|
|
1108
|
-
const seconds = Math.round(totalSeconds % 60)
|
|
1109
|
-
return `${minutes}m ${seconds}s
|
|
1106
|
+
const totalSeconds = Math.max(0, durationMs) / 1000
|
|
1107
|
+
if (totalSeconds < 60) return `${totalSeconds.toFixed(totalSeconds < 10 ? 2 : 1)}s`
|
|
1108
|
+
const minutes = Math.floor(totalSeconds / 60)
|
|
1109
|
+
const seconds = Math.round(totalSeconds % 60)
|
|
1110
|
+
return `${minutes}m ${seconds}s`
|
|
1110
1111
|
}
|
|
1111
1112
|
|
|
1112
1113
|
/**
|
|
@@ -1117,14 +1118,14 @@ function formatBuildDuration(durationMs: number) {
|
|
|
1117
1118
|
* start.ts on a static HIT). Mirrors the static-prerender loop's capture.
|
|
1118
1119
|
*/
|
|
1119
1120
|
interface ShellCacheMeta {
|
|
1120
|
-
cacheLife?: CacheLifeStash
|
|
1121
|
-
tags: string[]
|
|
1121
|
+
cacheLife?: CacheLifeStash
|
|
1122
|
+
tags: string[]
|
|
1122
1123
|
/** Prerendered `Link` header (font preloads + react-dom resource hints). */
|
|
1123
|
-
linkHeader?: string
|
|
1124
|
+
linkHeader?: string
|
|
1124
1125
|
}
|
|
1125
1126
|
|
|
1126
1127
|
/** A baked partial/fallback shell (renderPartialShell's non-null result). */
|
|
1127
|
-
type PrebuiltShell = NonNullable<Awaited<ReturnType<typeof renderPartialShell
|
|
1128
|
+
type PrebuiltShell = NonNullable<Awaited<ReturnType<typeof renderPartialShell>>>
|
|
1128
1129
|
|
|
1129
1130
|
/**
|
|
1130
1131
|
* Render a shell (partial/fallback) inside a work unit + render cache-meta scope
|
|
@@ -1135,32 +1136,36 @@ async function renderShellWithCacheMeta(
|
|
|
1135
1136
|
config: Awaited<ReturnType<typeof loadConfig>>,
|
|
1136
1137
|
route: RouteManifestEntry,
|
|
1137
1138
|
render: () => Promise<PrebuiltShell | null>,
|
|
1138
|
-
): Promise<{
|
|
1139
|
+
): Promise<{
|
|
1140
|
+
prebuilt: PrebuiltShell | null
|
|
1141
|
+
meta: ShellCacheMeta
|
|
1142
|
+
vary: BuildResponseVary | undefined
|
|
1143
|
+
}> {
|
|
1139
1144
|
// A shell prerender resolves its `use cache` scopes asynchronously and the cacheLife-to-work-unit
|
|
1140
1145
|
// stash is not in scope during that propagation. collectRenderMeta's render cache-meta IS, so the
|
|
1141
1146
|
// effective revalidate/expire/stale windows come back on the result.
|
|
1142
|
-
let linkHeader: string | undefined
|
|
1147
|
+
let linkHeader: string | undefined
|
|
1143
1148
|
// A BAKED segment is rendered here, outside any request, so without this scope its param accesses
|
|
1144
1149
|
// are never tracked and the artifact ships with no vary set - which keys every prerendered route
|
|
1145
1150
|
// on its exact URL and re-fetches the shared shell for every param value.
|
|
1146
1151
|
const tracked = await buildCompat().withVaryParamsTracking(async () =>
|
|
1147
1152
|
runWithWorkUnit('render', async () => {
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1153
|
+
const rendered = await getRenderExtensions().collectRenderMeta(
|
|
1154
|
+
() => runWithNextBuildPhase(render),
|
|
1155
|
+
{
|
|
1156
|
+
fetchCache: route.segmentConfig?.fetchCache,
|
|
1157
|
+
route: route.route || '/',
|
|
1158
|
+
prerender: true,
|
|
1159
|
+
},
|
|
1160
|
+
)
|
|
1161
|
+
// Font preloads + react-dom preload() hints recorded during the shell
|
|
1162
|
+
// render become the route's serve-time `Link` header (a PPR resume never
|
|
1163
|
+
// re-runs the components that emitted them). Read before the unit unwinds.
|
|
1164
|
+
linkHeader = buildCompat().takeFontLinkHeader()
|
|
1165
|
+
return rendered
|
|
1161
1166
|
}),
|
|
1162
|
-
)
|
|
1163
|
-
const result = tracked.value
|
|
1167
|
+
)
|
|
1168
|
+
const result = tracked.value
|
|
1164
1169
|
const cacheLife: CacheLifeStash | undefined =
|
|
1165
1170
|
result.revalidateSeconds !== undefined ||
|
|
1166
1171
|
result.expireSeconds !== undefined ||
|
|
@@ -1172,7 +1177,7 @@ async function renderShellWithCacheMeta(
|
|
|
1172
1177
|
...(result.expireSeconds !== undefined ? { expireSeconds: result.expireSeconds } : {}),
|
|
1173
1178
|
...(result.staleSeconds !== undefined ? { staleSeconds: result.staleSeconds } : {}),
|
|
1174
1179
|
}
|
|
1175
|
-
: undefined
|
|
1180
|
+
: undefined
|
|
1176
1181
|
return {
|
|
1177
1182
|
prebuilt: result.value,
|
|
1178
1183
|
meta: {
|
|
@@ -1181,7 +1186,7 @@ async function renderShellWithCacheMeta(
|
|
|
1181
1186
|
...(linkHeader ? { linkHeader } : {}),
|
|
1182
1187
|
},
|
|
1183
1188
|
vary: tracked.vary,
|
|
1184
|
-
}
|
|
1189
|
+
}
|
|
1185
1190
|
}
|
|
1186
1191
|
|
|
1187
1192
|
/**
|
|
@@ -1190,7 +1195,7 @@ async function renderShellWithCacheMeta(
|
|
|
1190
1195
|
* client prefetches: such data is not worth prefetching, so its boundary stays a hole the
|
|
1191
1196
|
* navigation fills live.
|
|
1192
1197
|
*/
|
|
1193
|
-
const DYNAMIC_EXPIRE_SECONDS = 300
|
|
1198
|
+
const DYNAMIC_EXPIRE_SECONDS = 300
|
|
1194
1199
|
|
|
1195
1200
|
/**
|
|
1196
1201
|
* True when the route's own sources declare a cacheLife that expires inside the dynamic-expire
|
|
@@ -1199,18 +1204,18 @@ const DYNAMIC_EXPIRE_SECONDS = 300;
|
|
|
1199
1204
|
*/
|
|
1200
1205
|
function routeDeclaresShortLivedCache(route: RouteManifestEntry): boolean {
|
|
1201
1206
|
for (const file of route.sourceFiles) {
|
|
1202
|
-
if (!existsSync(file)) continue
|
|
1203
|
-
const source = readTextSync(file)
|
|
1207
|
+
if (!existsSync(file)) continue
|
|
1208
|
+
const source = readTextSync(file)
|
|
1204
1209
|
for (const literal of source.matchAll(/cacheLife\s*\(\s*\{[^}]*\bexpire\s*:\s*([^,}]+)/gs)) {
|
|
1205
|
-
const value = staticNumberExpression(literal[1]!)
|
|
1206
|
-
if (value !== undefined && value < DYNAMIC_EXPIRE_SECONDS) return true
|
|
1210
|
+
const value = staticNumberExpression(literal[1]!)
|
|
1211
|
+
if (value !== undefined && value < DYNAMIC_EXPIRE_SECONDS) return true
|
|
1207
1212
|
}
|
|
1208
1213
|
for (const named of source.matchAll(/cacheLife\s*\(\s*['"]([^'"]+)['"]\s*\)/g)) {
|
|
1209
|
-
const expire = builtInCacheLifeExpire(named[1]!)
|
|
1210
|
-
if (expire !== undefined && expire < DYNAMIC_EXPIRE_SECONDS) return true
|
|
1214
|
+
const expire = builtInCacheLifeExpire(named[1]!)
|
|
1215
|
+
if (expire !== undefined && expire < DYNAMIC_EXPIRE_SECONDS) return true
|
|
1211
1216
|
}
|
|
1212
1217
|
}
|
|
1213
|
-
return false
|
|
1218
|
+
return false
|
|
1214
1219
|
}
|
|
1215
1220
|
|
|
1216
1221
|
/**
|
|
@@ -1225,15 +1230,15 @@ async function renderPrefetchBody(
|
|
|
1225
1230
|
route: RouteManifestEntry,
|
|
1226
1231
|
render: () => Promise<PrebuiltShell | null>,
|
|
1227
1232
|
): Promise<{ prebuilt: PrebuiltShell; vary: BuildResponseVary | undefined } | undefined> {
|
|
1228
|
-
if (!routeDeclaresShortLivedCache(route)) return undefined
|
|
1233
|
+
if (!routeDeclaresShortLivedCache(route)) return undefined
|
|
1229
1234
|
try {
|
|
1230
|
-
const { prebuilt, vary } = await renderShellWithCacheMeta(config, route, render)
|
|
1231
|
-
return prebuilt ? { prebuilt, vary } : undefined
|
|
1235
|
+
const { prebuilt, vary } = await renderShellWithCacheMeta(config, route, render)
|
|
1236
|
+
return prebuilt ? { prebuilt, vary } : undefined
|
|
1232
1237
|
} catch {
|
|
1233
1238
|
// Best-effort: the document shell doubles as the prefetch body, as before.
|
|
1234
|
-
return undefined
|
|
1239
|
+
return undefined
|
|
1235
1240
|
} finally {
|
|
1236
|
-
abortActivePrerenderScopes()
|
|
1241
|
+
abortActivePrerenderScopes()
|
|
1237
1242
|
}
|
|
1238
1243
|
}
|
|
1239
1244
|
|
|
@@ -1241,29 +1246,29 @@ async function buildPprShell(
|
|
|
1241
1246
|
config: Awaited<ReturnType<typeof loadConfig>>,
|
|
1242
1247
|
route: RouteManifestEntry,
|
|
1243
1248
|
) {
|
|
1244
|
-
const url = new URL(`http://pnext.local${route.route}`)
|
|
1249
|
+
const url = new URL(`http://pnext.local${route.route}`)
|
|
1245
1250
|
// Render under the production-build phase so build-phase sentinels
|
|
1246
1251
|
// (process.env.NEXT_PHASE) resolve to their buildtime value in the shell.
|
|
1247
1252
|
const { prebuilt, meta, vary } = await renderShellWithCacheMeta(config, route, () =>
|
|
1248
1253
|
renderPartialShell({ config, route, url }),
|
|
1249
|
-
)
|
|
1254
|
+
)
|
|
1250
1255
|
if (!prebuilt) {
|
|
1251
1256
|
// Dynamic data escaped every Suspense boundary — there is no static shell to
|
|
1252
1257
|
// bake, so drop the PPR flag and let the route render fully dynamically.
|
|
1253
|
-
route.ppr = false
|
|
1254
|
-
return
|
|
1255
|
-
}
|
|
1256
|
-
route.pprHoles = prebuilt.holes
|
|
1257
|
-
route.pprMetadata = prebuilt.metadataDynamic || undefined
|
|
1258
|
-
persistRouteCacheLife(route, meta.cacheLife)
|
|
1259
|
-
recordRouteCacheTags(route, meta.tags)
|
|
1260
|
-
if (meta.linkHeader) route.linkHeader = meta.linkHeader
|
|
1261
|
-
const file = pprShellPath(config.outPath, route.id)
|
|
1262
|
-
await mkdir(path.dirname(file), { recursive: true })
|
|
1263
|
-
await writeText(file, prebuilt.shell)
|
|
1258
|
+
route.ppr = false
|
|
1259
|
+
return
|
|
1260
|
+
}
|
|
1261
|
+
route.pprHoles = prebuilt.holes
|
|
1262
|
+
route.pprMetadata = prebuilt.metadataDynamic || undefined
|
|
1263
|
+
persistRouteCacheLife(route, meta.cacheLife)
|
|
1264
|
+
recordRouteCacheTags(route, meta.tags)
|
|
1265
|
+
if (meta.linkHeader) route.linkHeader = meta.linkHeader
|
|
1266
|
+
const file = pprShellPath(config.outPath, route.id)
|
|
1267
|
+
await mkdir(path.dirname(file), { recursive: true })
|
|
1268
|
+
await writeText(file, prebuilt.shell)
|
|
1264
1269
|
const prefetchBody = (await renderPrefetchBody(config, route, () =>
|
|
1265
1270
|
renderPartialShell({ config, route, url, prefetchShell: true }),
|
|
1266
|
-
)) ?? { prebuilt, vary }
|
|
1271
|
+
)) ?? { prebuilt, vary }
|
|
1267
1272
|
await emitSegmentArtifacts(
|
|
1268
1273
|
config,
|
|
1269
1274
|
route,
|
|
@@ -1275,25 +1280,25 @@ async function buildPprShell(
|
|
|
1275
1280
|
// renderPartialShell against the PATTERN url: a route-level, params-hanging
|
|
1276
1281
|
// render, so an empty tracked set is honest (see buildVaryTrusted).
|
|
1277
1282
|
true,
|
|
1278
|
-
)
|
|
1283
|
+
)
|
|
1279
1284
|
}
|
|
1280
1285
|
|
|
1281
1286
|
/** Union shell-prerender cache tags onto the route (PPR-shell tag staleness). */
|
|
1282
1287
|
function recordRouteCacheTags(route: RouteManifestEntry, tags: readonly string[]): void {
|
|
1283
|
-
if (tags.length === 0) return
|
|
1284
|
-
route.cacheTags = [...new Set([...(route.cacheTags ?? []), ...tags])]
|
|
1288
|
+
if (tags.length === 0) return
|
|
1289
|
+
route.cacheTags = [...new Set([...(route.cacheTags ?? []), ...tags])]
|
|
1285
1290
|
}
|
|
1286
1291
|
|
|
1287
1292
|
/** Persist a shell render's effective cacheLife on the route so the static-serve
|
|
1288
1293
|
* path (start.ts) can re-emit the SWR cache-control + x-nextjs-stale-time headers
|
|
1289
1294
|
* on a pure static HIT/MISS (the header finalizer only fires on a live render). */
|
|
1290
1295
|
function persistRouteCacheLife(route: RouteManifestEntry, life: CacheLifeStash | undefined): void {
|
|
1291
|
-
if (!life) return
|
|
1296
|
+
if (!life) return
|
|
1292
1297
|
route.cacheLife = {
|
|
1293
1298
|
...(life.revalidateSeconds !== undefined ? { revalidateSeconds: life.revalidateSeconds } : {}),
|
|
1294
1299
|
...(life.expireSeconds !== undefined ? { expireSeconds: life.expireSeconds } : {}),
|
|
1295
1300
|
...(life.staleSeconds !== undefined ? { staleSeconds: life.staleSeconds } : {}),
|
|
1296
|
-
}
|
|
1301
|
+
}
|
|
1297
1302
|
}
|
|
1298
1303
|
|
|
1299
1304
|
/**
|
|
@@ -1317,14 +1322,12 @@ async function emitSegmentArtifacts(
|
|
|
1317
1322
|
* otherwise defends against. Never pass `true` for a CONCRETE-param render. */
|
|
1318
1323
|
fallbackParamsRender = false,
|
|
1319
1324
|
): Promise<void> {
|
|
1320
|
-
const isStatic = route.mode === 'static' && !postponed
|
|
1321
|
-
const compat = buildCompat()
|
|
1325
|
+
const isStatic = route.mode === 'static' && !postponed
|
|
1326
|
+
const compat = buildCompat()
|
|
1322
1327
|
const staleTime =
|
|
1323
1328
|
staleTimeFromRouteSources(route) ??
|
|
1324
|
-
(isStatic
|
|
1325
|
-
|
|
1326
|
-
: compat.defaultDynamicStaleTimeSeconds);
|
|
1327
|
-
const bodySizeBytes = Buffer.byteLength(body);
|
|
1329
|
+
(isStatic ? compat.defaultStaticStaleTimeSeconds : compat.defaultDynamicStaleTimeSeconds)
|
|
1330
|
+
const bodySizeBytes = Buffer.byteLength(body)
|
|
1328
1331
|
const tree = compat.buildRootTreePrefetch({
|
|
1329
1332
|
pathname: route.route,
|
|
1330
1333
|
isStatic,
|
|
@@ -1332,7 +1335,8 @@ async function emitSegmentArtifacts(
|
|
|
1332
1335
|
routeId: route.id,
|
|
1333
1336
|
bodySizeBytes,
|
|
1334
1337
|
postponed,
|
|
1335
|
-
runtimePrefetch:
|
|
1338
|
+
runtimePrefetch:
|
|
1339
|
+
(route.segmentConfig as { prefetch?: unknown } | undefined)?.prefetch === 'allow-runtime',
|
|
1336
1340
|
// The route's <title> is dynamic and ships as its own `/_head` response;
|
|
1337
1341
|
// announcing it in the TREE lets the client fetch the head before the body
|
|
1338
1342
|
// (Next's response order) instead of learning it from the body response.
|
|
@@ -1342,7 +1346,7 @@ async function emitSegmentArtifacts(
|
|
|
1342
1346
|
...(route.pprMetadata === true
|
|
1343
1347
|
? { headOutlined: true, ...(/[:*]/.test(route.route) ? { headFirst: true } : {}) }
|
|
1344
1348
|
: {}),
|
|
1345
|
-
})
|
|
1349
|
+
})
|
|
1346
1350
|
const meta = compat.buildSegmentMeta({
|
|
1347
1351
|
status: statusOverride ?? 200,
|
|
1348
1352
|
staleTime,
|
|
@@ -1350,11 +1354,11 @@ async function emitSegmentArtifacts(
|
|
|
1350
1354
|
bodySizeBytes,
|
|
1351
1355
|
prefetchHints: { [route.route]: tree.tree.prefetchHints },
|
|
1352
1356
|
...(vary && buildVaryTrusted(route, vary, fallbackParamsRender) ? { vary } : {}),
|
|
1353
|
-
})
|
|
1354
|
-
await mkdir(compat.segmentDir(config.outPath, route.id), { recursive: true })
|
|
1355
|
-
await writeText(compat.treeSegmentFile(config.outPath, route.id), JSON.stringify(tree))
|
|
1356
|
-
await writeText(compat.bodySegmentFile(config.outPath, route.id), body)
|
|
1357
|
-
await writeText(compat.segmentMetaFile(config.outPath, route.id), JSON.stringify(meta))
|
|
1357
|
+
})
|
|
1358
|
+
await mkdir(compat.segmentDir(config.outPath, route.id), { recursive: true })
|
|
1359
|
+
await writeText(compat.treeSegmentFile(config.outPath, route.id), JSON.stringify(tree))
|
|
1360
|
+
await writeText(compat.bodySegmentFile(config.outPath, route.id), body)
|
|
1361
|
+
await writeText(compat.segmentMetaFile(config.outPath, route.id), JSON.stringify(meta))
|
|
1358
1362
|
await emitNextSegmentArtifacts(
|
|
1359
1363
|
config.root,
|
|
1360
1364
|
route,
|
|
@@ -1362,7 +1366,7 @@ async function emitSegmentArtifacts(
|
|
|
1362
1366
|
compat.rootTreePrefetchText(tree, 'flight'),
|
|
1363
1367
|
meta,
|
|
1364
1368
|
segmentMetaHeaders(cacheMeta),
|
|
1365
|
-
)
|
|
1369
|
+
)
|
|
1366
1370
|
}
|
|
1367
1371
|
|
|
1368
1372
|
/**
|
|
@@ -1387,14 +1391,14 @@ function recordPrerenderVary(
|
|
|
1387
1391
|
routePath: string,
|
|
1388
1392
|
vary: BuildResponseVary | undefined,
|
|
1389
1393
|
): void {
|
|
1390
|
-
if (!vary || !buildVaryTrusted(route, vary, false)) return
|
|
1391
|
-
if (vary.params.length === 0 && !vary.search) return
|
|
1392
|
-
route.prerenderVary ??= {}
|
|
1394
|
+
if (!vary || !buildVaryTrusted(route, vary, false)) return
|
|
1395
|
+
if (vary.params.length === 0 && !vary.search) return
|
|
1396
|
+
route.prerenderVary ??= {}
|
|
1393
1397
|
route.prerenderVary[routePath] = {
|
|
1394
1398
|
vary: buildCompat().varyNamesFor(vary, 'body'),
|
|
1395
1399
|
layoutVary: buildCompat().varyNamesFor(vary, 'layout'),
|
|
1396
1400
|
pageVary: buildCompat().varyNamesFor(vary, 'page'),
|
|
1397
|
-
}
|
|
1401
|
+
}
|
|
1398
1402
|
}
|
|
1399
1403
|
|
|
1400
1404
|
function buildVaryTrusted(
|
|
@@ -1402,9 +1406,9 @@ function buildVaryTrusted(
|
|
|
1402
1406
|
vary: BuildResponseVary,
|
|
1403
1407
|
fallbackParamsRender: boolean,
|
|
1404
1408
|
): boolean {
|
|
1405
|
-
if (vary.params.length > 0 || vary.search) return true
|
|
1406
|
-
if (fallbackParamsRender) return true
|
|
1407
|
-
return route.params.length === 0 && !route.catchAll
|
|
1409
|
+
if (vary.params.length > 0 || vary.search) return true
|
|
1410
|
+
if (fallbackParamsRender) return true
|
|
1411
|
+
return route.params.length === 0 && !route.catchAll
|
|
1408
1412
|
}
|
|
1409
1413
|
|
|
1410
1414
|
/**
|
|
@@ -1415,11 +1419,11 @@ function buildVaryTrusted(
|
|
|
1415
1419
|
* tags). Empty when the route declared no cacheLife and no tags.
|
|
1416
1420
|
*/
|
|
1417
1421
|
function segmentMetaHeaders(cacheMeta: ShellCacheMeta | undefined): Record<string, string> {
|
|
1418
|
-
const headers: Record<string, string> = {}
|
|
1419
|
-
if (!cacheMeta) return headers
|
|
1420
|
-
for (const [key, value] of cacheLifeResponseHeaders(cacheMeta.cacheLife)) headers[key] = value
|
|
1421
|
-
if (cacheMeta.tags.length > 0) headers['x-next-cache-tags'] = cacheMeta.tags.join(',')
|
|
1422
|
-
return headers
|
|
1422
|
+
const headers: Record<string, string> = {}
|
|
1423
|
+
if (!cacheMeta) return headers
|
|
1424
|
+
for (const [key, value] of cacheLifeResponseHeaders(cacheMeta.cacheLife)) headers[key] = value
|
|
1425
|
+
if (cacheMeta.tags.length > 0) headers['x-next-cache-tags'] = cacheMeta.tags.join(',')
|
|
1426
|
+
return headers
|
|
1423
1427
|
}
|
|
1424
1428
|
|
|
1425
1429
|
async function emitNextSegmentArtifacts(
|
|
@@ -1430,9 +1434,9 @@ async function emitNextSegmentArtifacts(
|
|
|
1430
1434
|
meta: SegmentMeta,
|
|
1431
1435
|
headers: Record<string, string> = {},
|
|
1432
1436
|
): Promise<void> {
|
|
1433
|
-
const appPath = nextAppRoutePath(route.route)
|
|
1434
|
-
const appFile = path.join(root, '.next', 'server', 'app', appPath)
|
|
1435
|
-
const segmentsDir = `${appFile}.segments
|
|
1437
|
+
const appPath = nextAppRoutePath(route.route)
|
|
1438
|
+
const appFile = path.join(root, '.next', 'server', 'app', appPath)
|
|
1439
|
+
const segmentsDir = `${appFile}.segments`
|
|
1436
1440
|
const nextMeta = {
|
|
1437
1441
|
status: meta.status,
|
|
1438
1442
|
// Next's page `.meta` carries the route's response headers (x-nextjs-stale-
|
|
@@ -1445,22 +1449,22 @@ async function emitNextSegmentArtifacts(
|
|
|
1445
1449
|
...(meta.segmentSizes ? { segmentSizes: meta.segmentSizes } : {}),
|
|
1446
1450
|
...(meta.inlinedSegmentPaths ? { inlinedSegmentPaths: meta.inlinedSegmentPaths } : {}),
|
|
1447
1451
|
...(meta.prefetchHints ? { prefetchHints: meta.prefetchHints } : {}),
|
|
1448
|
-
}
|
|
1452
|
+
}
|
|
1449
1453
|
|
|
1450
|
-
await mkdir(segmentsDir, { recursive: true })
|
|
1454
|
+
await mkdir(segmentsDir, { recursive: true })
|
|
1451
1455
|
// A baked shell is stored OPEN (no closing tags) so the serve path can stream resumed holes into
|
|
1452
1456
|
// it. Next's equivalent file is instead the finished document whenever nothing was postponed, and
|
|
1453
1457
|
// suites tell a complete prerender from an incomplete shell by testing for the `</html>` tail.
|
|
1454
1458
|
// Close a hole-free shell here; a postponed one stays open, exactly as Next leaves its partials.
|
|
1455
|
-
await writeText(`${appFile}.html`, meta.postponed ? body : `${body}\n </body></html>`)
|
|
1456
|
-
await writeText(`${appFile}.meta`, JSON.stringify(nextMeta))
|
|
1457
|
-
await writeText(path.join(segmentsDir, '_tree.segment.rsc'), tree)
|
|
1458
|
-
await writeText(path.join(segmentsDir, '_full.segment.rsc'), body)
|
|
1459
|
-
await writeText(path.join(segmentsDir, '_index.segment.rsc'), body)
|
|
1459
|
+
await writeText(`${appFile}.html`, meta.postponed ? body : `${body}\n </body></html>`)
|
|
1460
|
+
await writeText(`${appFile}.meta`, JSON.stringify(nextMeta))
|
|
1461
|
+
await writeText(path.join(segmentsDir, '_tree.segment.rsc'), tree)
|
|
1462
|
+
await writeText(path.join(segmentsDir, '_full.segment.rsc'), body)
|
|
1463
|
+
await writeText(path.join(segmentsDir, '_index.segment.rsc'), body)
|
|
1460
1464
|
|
|
1461
|
-
const routeSegment = appPath === 'index' ? '__PAGE__' : appPath
|
|
1462
|
-
await writeNestedSegment(segmentsDir, `${routeSegment}.segment.rsc`, body)
|
|
1463
|
-
await writeNestedSegment(segmentsDir, path.join(routeSegment, '__PAGE__.segment.rsc'), body)
|
|
1465
|
+
const routeSegment = appPath === 'index' ? '__PAGE__' : appPath
|
|
1466
|
+
await writeNestedSegment(segmentsDir, `${routeSegment}.segment.rsc`, body)
|
|
1467
|
+
await writeNestedSegment(segmentsDir, path.join(routeSegment, '__PAGE__.segment.rsc'), body)
|
|
1464
1468
|
}
|
|
1465
1469
|
|
|
1466
1470
|
/**
|
|
@@ -1479,11 +1483,11 @@ async function emitConcreteNextPageArtifacts(
|
|
|
1479
1483
|
routePath: string,
|
|
1480
1484
|
rendered: { html: string; status: number; postponed?: boolean },
|
|
1481
1485
|
): Promise<void> {
|
|
1482
|
-
if (!config.compat?.next || !cacheComponents()) return
|
|
1483
|
-
const compat = buildCompat()
|
|
1484
|
-
const postponed = rendered.postponed ?? false
|
|
1485
|
-
const bodySizeBytes = Buffer.byteLength(rendered.html)
|
|
1486
|
-
const staleTime = staleTimeFromRouteSources(route) ?? compat.defaultStaticStaleTimeSeconds
|
|
1486
|
+
if (!config.compat?.next || !cacheComponents()) return
|
|
1487
|
+
const compat = buildCompat()
|
|
1488
|
+
const postponed = rendered.postponed ?? false
|
|
1489
|
+
const bodySizeBytes = Buffer.byteLength(rendered.html)
|
|
1490
|
+
const staleTime = staleTimeFromRouteSources(route) ?? compat.defaultStaticStaleTimeSeconds
|
|
1487
1491
|
const tree = compat.buildRootTreePrefetch({
|
|
1488
1492
|
pathname: routePath,
|
|
1489
1493
|
isStatic: !postponed,
|
|
@@ -1491,17 +1495,17 @@ async function emitConcreteNextPageArtifacts(
|
|
|
1491
1495
|
routeId: route.id,
|
|
1492
1496
|
bodySizeBytes,
|
|
1493
1497
|
postponed,
|
|
1494
|
-
})
|
|
1498
|
+
})
|
|
1495
1499
|
const meta = compat.buildSegmentMeta({
|
|
1496
1500
|
status: rendered.status,
|
|
1497
1501
|
staleTime,
|
|
1498
1502
|
postponed,
|
|
1499
1503
|
bodySizeBytes,
|
|
1500
|
-
})
|
|
1501
|
-
const appFile = path.join(config.root, '.next', 'server', 'app', nextAppRoutePath(routePath))
|
|
1502
|
-
const segmentsDir = `${appFile}.segments
|
|
1503
|
-
await mkdir(segmentsDir, { recursive: true })
|
|
1504
|
-
await writeText(`${appFile}.html`, rendered.html)
|
|
1504
|
+
})
|
|
1505
|
+
const appFile = path.join(config.root, '.next', 'server', 'app', nextAppRoutePath(routePath))
|
|
1506
|
+
const segmentsDir = `${appFile}.segments`
|
|
1507
|
+
await mkdir(segmentsDir, { recursive: true })
|
|
1508
|
+
await writeText(`${appFile}.html`, rendered.html)
|
|
1505
1509
|
await writeText(
|
|
1506
1510
|
`${appFile}.meta`,
|
|
1507
1511
|
JSON.stringify({
|
|
@@ -1512,46 +1516,46 @@ async function emitConcreteNextPageArtifacts(
|
|
|
1512
1516
|
segmentPaths: meta.segmentPaths,
|
|
1513
1517
|
...(meta.segmentSizes ? { segmentSizes: meta.segmentSizes } : {}),
|
|
1514
1518
|
}),
|
|
1515
|
-
)
|
|
1519
|
+
)
|
|
1516
1520
|
await writeText(
|
|
1517
1521
|
path.join(segmentsDir, '_tree.segment.rsc'),
|
|
1518
1522
|
compat.rootTreePrefetchText(tree, 'flight'),
|
|
1519
|
-
)
|
|
1523
|
+
)
|
|
1520
1524
|
}
|
|
1521
1525
|
|
|
1522
1526
|
function staleTimeFromRouteSources(route: RouteManifestEntry): number | undefined {
|
|
1523
|
-
let found: number | undefined
|
|
1527
|
+
let found: number | undefined
|
|
1524
1528
|
for (const file of route.sourceFiles) {
|
|
1525
|
-
if (!existsSync(file)) continue
|
|
1526
|
-
const source = readTextSync(file)
|
|
1529
|
+
if (!existsSync(file)) continue
|
|
1530
|
+
const source = readTextSync(file)
|
|
1527
1531
|
for (const literal of source.matchAll(/cacheLife\s*\(\s*\{[^}]*\bstale\s*:\s*([^,}]+)/gs)) {
|
|
1528
|
-
const value = staticNumberExpression(literal[1]!)
|
|
1529
|
-
if (value !== undefined) found = minDefined(found, value)
|
|
1532
|
+
const value = staticNumberExpression(literal[1]!)
|
|
1533
|
+
if (value !== undefined) found = minDefined(found, value)
|
|
1530
1534
|
}
|
|
1531
1535
|
for (const named of source.matchAll(/cacheLife\s*\(\s*['"]([^'"]+)['"]\s*\)/g)) {
|
|
1532
|
-
const namedValue = builtInCacheLifeStale(named[1]!)
|
|
1533
|
-
if (namedValue !== undefined) found = minDefined(found, namedValue)
|
|
1536
|
+
const namedValue = builtInCacheLifeStale(named[1]!)
|
|
1537
|
+
if (namedValue !== undefined) found = minDefined(found, namedValue)
|
|
1534
1538
|
}
|
|
1535
1539
|
}
|
|
1536
|
-
return found
|
|
1540
|
+
return found
|
|
1537
1541
|
}
|
|
1538
1542
|
|
|
1539
1543
|
function readTextSync(file: string): string {
|
|
1540
1544
|
try {
|
|
1541
|
-
return existsSync(file) && statSync(file).isFile() ? readFileSync(file, 'utf8') : ''
|
|
1545
|
+
return existsSync(file) && statSync(file).isFile() ? readFileSync(file, 'utf8') : ''
|
|
1542
1546
|
} catch {
|
|
1543
|
-
return ''
|
|
1547
|
+
return ''
|
|
1544
1548
|
}
|
|
1545
1549
|
}
|
|
1546
1550
|
|
|
1547
1551
|
function staticNumberExpression(expression: string): number | undefined {
|
|
1548
|
-
const trimmed = expression.trim()
|
|
1549
|
-
if (/^\d+(?:\.\d+)?$/.test(trimmed)) return Number(trimmed)
|
|
1550
|
-
const product = trimmed.split('*').map(part => part.trim())
|
|
1552
|
+
const trimmed = expression.trim()
|
|
1553
|
+
if (/^\d+(?:\.\d+)?$/.test(trimmed)) return Number(trimmed)
|
|
1554
|
+
const product = trimmed.split('*').map(part => part.trim())
|
|
1551
1555
|
if (product.length > 1 && product.every(part => /^\d+(?:\.\d+)?$/.test(part))) {
|
|
1552
|
-
return product.reduce((value, part) => value * Number(part), 1)
|
|
1556
|
+
return product.reduce((value, part) => value * Number(part), 1)
|
|
1553
1557
|
}
|
|
1554
|
-
return undefined
|
|
1558
|
+
return undefined
|
|
1555
1559
|
}
|
|
1556
1560
|
|
|
1557
1561
|
function builtInCacheLifeStale(profile: string): number | undefined {
|
|
@@ -1561,16 +1565,16 @@ function builtInCacheLifeStale(profile: string): number | undefined {
|
|
|
1561
1565
|
// (their client stale window is below the runtime threshold), so their
|
|
1562
1566
|
// staleness must not shorten the route's prefetch expiry — only the
|
|
1563
1567
|
// longer-lived caches that actually land in the prerender count.
|
|
1564
|
-
return undefined
|
|
1568
|
+
return undefined
|
|
1565
1569
|
case 'default':
|
|
1566
1570
|
case 'minutes':
|
|
1567
1571
|
case 'hours':
|
|
1568
1572
|
case 'days':
|
|
1569
1573
|
case 'weeks':
|
|
1570
1574
|
case 'max':
|
|
1571
|
-
return 300
|
|
1575
|
+
return 300
|
|
1572
1576
|
default:
|
|
1573
|
-
return undefined
|
|
1577
|
+
return undefined
|
|
1574
1578
|
}
|
|
1575
1579
|
}
|
|
1576
1580
|
|
|
@@ -1578,37 +1582,37 @@ function builtInCacheLifeStale(profile: string): number | undefined {
|
|
|
1578
1582
|
function builtInCacheLifeExpire(profile: string): number | undefined {
|
|
1579
1583
|
switch (profile) {
|
|
1580
1584
|
case 'seconds':
|
|
1581
|
-
return 60
|
|
1585
|
+
return 60
|
|
1582
1586
|
case 'minutes':
|
|
1583
|
-
return 3600
|
|
1587
|
+
return 3600
|
|
1584
1588
|
case 'hours':
|
|
1585
|
-
return 86400
|
|
1589
|
+
return 86400
|
|
1586
1590
|
case 'days':
|
|
1587
|
-
return 604800
|
|
1591
|
+
return 604800
|
|
1588
1592
|
case 'weeks':
|
|
1589
|
-
return 2592000
|
|
1593
|
+
return 2592000
|
|
1590
1594
|
case 'default':
|
|
1591
1595
|
case 'max':
|
|
1592
|
-
return 31536000
|
|
1596
|
+
return 31536000
|
|
1593
1597
|
default:
|
|
1594
|
-
return undefined
|
|
1598
|
+
return undefined
|
|
1595
1599
|
}
|
|
1596
1600
|
}
|
|
1597
1601
|
|
|
1598
1602
|
function minDefined(a: number | undefined, b: number | undefined): number | undefined {
|
|
1599
|
-
if (a === undefined) return b
|
|
1600
|
-
if (b === undefined) return a
|
|
1601
|
-
return Math.min(a, b)
|
|
1603
|
+
if (a === undefined) return b
|
|
1604
|
+
if (b === undefined) return a
|
|
1605
|
+
return Math.min(a, b)
|
|
1602
1606
|
}
|
|
1603
1607
|
|
|
1604
1608
|
async function writeNestedSegment(root: string, relative: string, body: string): Promise<void> {
|
|
1605
|
-
const file = path.join(root, relative)
|
|
1606
|
-
await mkdir(path.dirname(file), { recursive: true })
|
|
1607
|
-
await writeText(file, body)
|
|
1609
|
+
const file = path.join(root, relative)
|
|
1610
|
+
await mkdir(path.dirname(file), { recursive: true })
|
|
1611
|
+
await writeText(file, body)
|
|
1608
1612
|
}
|
|
1609
1613
|
|
|
1610
1614
|
function nextAppRoutePath(route: string): string {
|
|
1611
|
-
return route.replace(/^\/+/, '') || 'index'
|
|
1615
|
+
return route.replace(/^\/+/, '') || 'index'
|
|
1612
1616
|
}
|
|
1613
1617
|
|
|
1614
1618
|
/**
|
|
@@ -1622,15 +1626,15 @@ function nextAppRoutePath(route: string): string {
|
|
|
1622
1626
|
function shellRendersContentOutsideHoles(shell: string): boolean {
|
|
1623
1627
|
// A shell is a PARTIAL document: it usually stops mid-stream with no closing
|
|
1624
1628
|
// `</body>`, so read from the body tag to whichever comes first.
|
|
1625
|
-
const open = /<body[^>]*>/.exec(shell)
|
|
1626
|
-
if (!open) return false
|
|
1627
|
-
const rest = shell.slice(open.index + open[0].length)
|
|
1628
|
-
const body = rest.split('</body>')[0] ?? ''
|
|
1629
|
+
const open = /<body[^>]*>/.exec(shell)
|
|
1630
|
+
if (!open) return false
|
|
1631
|
+
const rest = shell.slice(open.index + open[0].length)
|
|
1632
|
+
const body = rest.split('</body>')[0] ?? ''
|
|
1629
1633
|
const outside = stripBalanced(body, 'pnext-suspense')
|
|
1630
1634
|
.replace(/<(script|template|style)[^>]*>[\s\S]*?<\/\1>/g, '')
|
|
1631
|
-
.replace(/<!--[\s\S]*?-->/g, '')
|
|
1632
|
-
if (/<(img|svg|video|canvas|input)[\s>]/.test(outside)) return true
|
|
1633
|
-
return outside.replace(/<[^>]*>/g, '').trim().length > 0
|
|
1635
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
1636
|
+
if (/<(img|svg|video|canvas|input)[\s>]/.test(outside)) return true
|
|
1637
|
+
return outside.replace(/<[^>]*>/g, '').trim().length > 0
|
|
1634
1638
|
}
|
|
1635
1639
|
|
|
1636
1640
|
/**
|
|
@@ -1640,40 +1644,40 @@ function shellRendersContentOutsideHoles(shell: string): boolean {
|
|
|
1640
1644
|
* (nothing to show, stays blocking) and a route whose whole body is one hole that still paints.
|
|
1641
1645
|
*/
|
|
1642
1646
|
function shellHolesRenderFallbackContent(shell: string): boolean {
|
|
1643
|
-
const open = /<body[^>]*>/.exec(shell)
|
|
1644
|
-
if (!open) return false
|
|
1647
|
+
const open = /<body[^>]*>/.exec(shell)
|
|
1648
|
+
if (!open) return false
|
|
1645
1649
|
const body = (shell.slice(open.index + open[0].length).split('</body>')[0] ?? '')
|
|
1646
1650
|
.replace(/<(script|template|style)[^>]*>[\s\S]*?<\/\1>/g, '')
|
|
1647
|
-
.replace(/<!--[\s\S]*?-->/g, '')
|
|
1648
|
-
const outside = stripBalanced(body, 'pnext-suspense')
|
|
1649
|
-
const text = (h: string) => h.replace(/<[^>]*>/g, '').trim().length
|
|
1650
|
-
const media = (h: string) => /<(img|svg|video|canvas|input)[\s>]/.test(h)
|
|
1651
|
-
return text(body) - text(outside) > 0 || (media(body) && !media(outside))
|
|
1651
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
1652
|
+
const outside = stripBalanced(body, 'pnext-suspense')
|
|
1653
|
+
const text = (h: string) => h.replace(/<[^>]*>/g, '').trim().length
|
|
1654
|
+
const media = (h: string) => /<(img|svg|video|canvas|input)[\s>]/.test(h)
|
|
1655
|
+
return text(body) - text(outside) > 0 || (media(body) && !media(outside))
|
|
1652
1656
|
}
|
|
1653
1657
|
|
|
1654
1658
|
/** Remove every balanced `<tag>…</tag>` region (nesting-aware) from `html`. */
|
|
1655
1659
|
function stripBalanced(html: string, tag: string): string {
|
|
1656
|
-
const open = new RegExp(`<${tag}[\\s>]`, 'g')
|
|
1657
|
-
const boundary = new RegExp(`<${tag}[\\s>]|</${tag}>`, 'g')
|
|
1658
|
-
let out = ''
|
|
1659
|
-
let cursor = 0
|
|
1660
|
+
const open = new RegExp(`<${tag}[\\s>]`, 'g')
|
|
1661
|
+
const boundary = new RegExp(`<${tag}[\\s>]|</${tag}>`, 'g')
|
|
1662
|
+
let out = ''
|
|
1663
|
+
let cursor = 0
|
|
1660
1664
|
for (let start = open.exec(html); start; start = open.exec(html)) {
|
|
1661
|
-
if (start.index < cursor) continue
|
|
1662
|
-
out += html.slice(cursor, start.index)
|
|
1663
|
-
boundary.lastIndex = start.index + 1
|
|
1664
|
-
let depth = 1
|
|
1665
|
-
let end = html.length
|
|
1665
|
+
if (start.index < cursor) continue
|
|
1666
|
+
out += html.slice(cursor, start.index)
|
|
1667
|
+
boundary.lastIndex = start.index + 1
|
|
1668
|
+
let depth = 1
|
|
1669
|
+
let end = html.length
|
|
1666
1670
|
for (let hit = boundary.exec(html); hit; hit = boundary.exec(html)) {
|
|
1667
|
-
depth += hit[0].startsWith('</') ? -1 : 1
|
|
1671
|
+
depth += hit[0].startsWith('</') ? -1 : 1
|
|
1668
1672
|
if (depth === 0) {
|
|
1669
|
-
end = hit.index + hit[0].length
|
|
1670
|
-
break
|
|
1673
|
+
end = hit.index + hit[0].length
|
|
1674
|
+
break
|
|
1671
1675
|
}
|
|
1672
1676
|
}
|
|
1673
|
-
cursor = end
|
|
1674
|
-
open.lastIndex = cursor
|
|
1677
|
+
cursor = end
|
|
1678
|
+
open.lastIndex = cursor
|
|
1675
1679
|
}
|
|
1676
|
-
return out + html.slice(cursor)
|
|
1680
|
+
return out + html.slice(cursor)
|
|
1677
1681
|
}
|
|
1678
1682
|
|
|
1679
1683
|
/**
|
|
@@ -1694,24 +1698,24 @@ async function buildCacheComponentsShell(
|
|
|
1694
1698
|
config: Awaited<ReturnType<typeof loadConfig>>,
|
|
1695
1699
|
route: RouteManifestEntry,
|
|
1696
1700
|
): Promise<boolean> {
|
|
1697
|
-
const url = new URL(`http://pnext.local${route.route}`)
|
|
1698
|
-
const hasDynamicParams = route.params.length > 0 || Boolean(route.catchAll)
|
|
1701
|
+
const url = new URL(`http://pnext.local${route.route}`)
|
|
1702
|
+
const hasDynamicParams = route.params.length > 0 || Boolean(route.catchAll)
|
|
1699
1703
|
// Build-phase so buildtime sentinels resolve correctly in the shell.
|
|
1700
|
-
beginShellSourceTracking()
|
|
1701
|
-
let renderResult: Awaited<ReturnType<typeof renderShellWithCacheMeta
|
|
1704
|
+
beginShellSourceTracking()
|
|
1705
|
+
let renderResult: Awaited<ReturnType<typeof renderShellWithCacheMeta>>
|
|
1702
1706
|
try {
|
|
1703
1707
|
renderResult = await renderShellWithCacheMeta(config, route, () =>
|
|
1704
1708
|
hasDynamicParams
|
|
1705
1709
|
? renderFallbackShell({ config, route, url })
|
|
1706
1710
|
: renderPartialShell({ config, route, url }),
|
|
1707
|
-
)
|
|
1711
|
+
)
|
|
1708
1712
|
} finally {
|
|
1709
1713
|
// Belt-and-braces: force-abort any prerender scope this route left armed so a
|
|
1710
1714
|
// stray cacheSignal-bound timer can never keep the build's event loop alive.
|
|
1711
|
-
abortActivePrerenderScopes()
|
|
1715
|
+
abortActivePrerenderScopes()
|
|
1712
1716
|
}
|
|
1713
|
-
const { prebuilt, meta } = renderResult
|
|
1714
|
-
const shellSources = endShellSourceTracking()
|
|
1717
|
+
const { prebuilt, meta } = renderResult
|
|
1718
|
+
const shellSources = endShellSourceTracking()
|
|
1715
1719
|
// A base fallback shell provides NO partial-shell value when it is either null (dynamic data
|
|
1716
1720
|
// escaped every boundary) or params-only-empty (its only holes come from `params`, which are
|
|
1717
1721
|
// URL-derived and resolve at request time). Such a base shell is never served as a postponed
|
|
@@ -1744,35 +1748,37 @@ async function buildCacheComponentsShell(
|
|
|
1744
1748
|
(shellRendersContentOutsideHoles(prebuilt.shell) ||
|
|
1745
1749
|
shellHolesRenderFallbackContent(prebuilt.shell))
|
|
1746
1750
|
) &&
|
|
1747
|
-
!(hasDynamicParams && routeSourcesUseRequestApi(config.root, route)))
|
|
1751
|
+
!(hasDynamicParams && routeSourcesUseRequestApi(config.root, route)))
|
|
1748
1752
|
|
|
1749
1753
|
if (!baseShellIsEmpty) {
|
|
1750
|
-
route.ppr = true
|
|
1751
|
-
route.pprHoles = prebuilt.holes
|
|
1752
|
-
route.pprMetadata = prebuilt.metadataDynamic || undefined
|
|
1753
|
-
persistRouteCacheLife(route, meta.cacheLife)
|
|
1754
|
-
recordRouteCacheTags(route, meta.tags)
|
|
1755
|
-
if (meta.linkHeader) route.linkHeader = meta.linkHeader
|
|
1756
|
-
const file = pprShellPath(config.outPath, route.id)
|
|
1757
|
-
await mkdir(path.dirname(file), { recursive: true })
|
|
1758
|
-
await writeText(file, prebuilt.shell)
|
|
1754
|
+
route.ppr = true
|
|
1755
|
+
route.pprHoles = prebuilt.holes
|
|
1756
|
+
route.pprMetadata = prebuilt.metadataDynamic || undefined
|
|
1757
|
+
persistRouteCacheLife(route, meta.cacheLife)
|
|
1758
|
+
recordRouteCacheTags(route, meta.tags)
|
|
1759
|
+
if (meta.linkHeader) route.linkHeader = meta.linkHeader
|
|
1760
|
+
const file = pprShellPath(config.outPath, route.id)
|
|
1761
|
+
await mkdir(path.dirname(file), { recursive: true })
|
|
1762
|
+
await writeText(file, prebuilt.shell)
|
|
1759
1763
|
// A param-free shell that resolved through http-access fallback recovery (a
|
|
1760
1764
|
// notFound()/forbidden()/unauthorized() above the boundary) must carry the fallback STATUS in
|
|
1761
1765
|
// its `.meta`, not 200. The shell prerender does not surface it, so probe with a blocking render
|
|
1762
1766
|
// - the same pattern as the buildSubShells full-param probe.
|
|
1763
|
-
let fallbackStatus: number | undefined
|
|
1767
|
+
let fallbackStatus: number | undefined
|
|
1764
1768
|
if (!hasDynamicParams && routeSourcesUseHttpFallback(config.root, route)) {
|
|
1765
1769
|
try {
|
|
1766
1770
|
const probe = await runWithWorkUnit('render', () =>
|
|
1767
1771
|
getRenderExtensions()
|
|
1768
1772
|
.collectRenderMeta(
|
|
1769
1773
|
() =>
|
|
1770
|
-
runWithNextBuildPhase(() =>
|
|
1774
|
+
runWithNextBuildPhase(() =>
|
|
1775
|
+
renderPageWithStatus({ config, route, params: {}, url }),
|
|
1776
|
+
),
|
|
1771
1777
|
{ route: route.route || '/', prerender: true },
|
|
1772
1778
|
)
|
|
1773
1779
|
.then(result => result.value),
|
|
1774
|
-
)
|
|
1775
|
-
if (probe.status !== 200) fallbackStatus = probe.status
|
|
1780
|
+
)
|
|
1781
|
+
if (probe.status !== 200) fallbackStatus = probe.status
|
|
1776
1782
|
} catch {
|
|
1777
1783
|
// Probe render failed — the shell itself is unaffected.
|
|
1778
1784
|
}
|
|
@@ -1781,7 +1787,7 @@ async function buildCacheComponentsShell(
|
|
|
1781
1787
|
hasDynamicParams
|
|
1782
1788
|
? renderFallbackShell({ config, route, url, prefetchShell: true })
|
|
1783
1789
|
: renderPartialShell({ config, route, url, prefetchShell: true }),
|
|
1784
|
-
)) ?? { prebuilt, vary: renderResult.vary }
|
|
1790
|
+
)) ?? { prebuilt, vary: renderResult.vary }
|
|
1785
1791
|
await emitSegmentArtifacts(
|
|
1786
1792
|
config,
|
|
1787
1793
|
route,
|
|
@@ -1793,13 +1799,13 @@ async function buildCacheComponentsShell(
|
|
|
1793
1799
|
// renderFallbackShell/renderPartialShell against the PATTERN url: a
|
|
1794
1800
|
// route-level, params-hanging render (see buildVaryTrusted).
|
|
1795
1801
|
true,
|
|
1796
|
-
)
|
|
1802
|
+
)
|
|
1797
1803
|
|
|
1798
1804
|
// Descending-specificity sub-shells from generateStaticParams prefixes.
|
|
1799
1805
|
if (hasDynamicParams && route.hasStaticParams) {
|
|
1800
|
-
await buildSubShells(config, route)
|
|
1806
|
+
await buildSubShells(config, route)
|
|
1801
1807
|
}
|
|
1802
|
-
return true
|
|
1808
|
+
return true
|
|
1803
1809
|
}
|
|
1804
1810
|
|
|
1805
1811
|
// The base shell is empty, but a route whose generateStaticParams cover a PARTIAL prefix still
|
|
@@ -1821,21 +1827,21 @@ async function buildCacheComponentsShell(
|
|
|
1821
1827
|
routeSourcesUseRequestApi(config.root, route) &&
|
|
1822
1828
|
(await hasFullStaticParamSet(config, route))))
|
|
1823
1829
|
) {
|
|
1824
|
-
route.ppr = true
|
|
1825
|
-
route.pprMetadata = prebuilt?.metadataDynamic || undefined
|
|
1830
|
+
route.ppr = true
|
|
1831
|
+
route.pprMetadata = prebuilt?.metadataDynamic || undefined
|
|
1826
1832
|
if (prebuilt) {
|
|
1827
|
-
persistRouteCacheLife(route, meta.cacheLife)
|
|
1828
|
-
recordRouteCacheTags(route, meta.tags)
|
|
1829
|
-
if (meta.linkHeader) route.linkHeader = meta.linkHeader
|
|
1833
|
+
persistRouteCacheLife(route, meta.cacheLife)
|
|
1834
|
+
recordRouteCacheTags(route, meta.tags)
|
|
1835
|
+
if (meta.linkHeader) route.linkHeader = meta.linkHeader
|
|
1830
1836
|
}
|
|
1831
|
-
await buildSubShells(config, route)
|
|
1832
|
-
return true
|
|
1837
|
+
await buildSubShells(config, route)
|
|
1838
|
+
return true
|
|
1833
1839
|
}
|
|
1834
1840
|
|
|
1835
1841
|
// No partial-prefix sub-shells either: fall through to the normal
|
|
1836
1842
|
// static/dynamic build (full generateStaticParams sets still prerender;
|
|
1837
1843
|
// uncovered params render dynamically).
|
|
1838
|
-
return false
|
|
1844
|
+
return false
|
|
1839
1845
|
}
|
|
1840
1846
|
|
|
1841
1847
|
/**
|
|
@@ -1848,20 +1854,20 @@ async function hasPartialStaticPrefix(
|
|
|
1848
1854
|
config: Awaited<ReturnType<typeof loadConfig>>,
|
|
1849
1855
|
route: RouteManifestEntry,
|
|
1850
1856
|
): Promise<boolean> {
|
|
1851
|
-
if (!route.hasStaticParams) return false
|
|
1852
|
-
let staticParams: Awaited<ReturnType<typeof staticParamsFor
|
|
1857
|
+
if (!route.hasStaticParams) return false
|
|
1858
|
+
let staticParams: Awaited<ReturnType<typeof staticParamsFor>>
|
|
1853
1859
|
try {
|
|
1854
|
-
staticParams = await staticParamsFor(config, route)
|
|
1860
|
+
staticParams = await staticParamsFor(config, route)
|
|
1855
1861
|
} catch {
|
|
1856
|
-
return false
|
|
1862
|
+
return false
|
|
1857
1863
|
}
|
|
1858
|
-
const paramKeys = [...route.params, ...(route.catchAll ? [route.catchAll] : [])]
|
|
1864
|
+
const paramKeys = [...route.params, ...(route.catchAll ? [route.catchAll] : [])]
|
|
1859
1865
|
for (const set of staticParams.allSets) {
|
|
1860
|
-
let filled = 0
|
|
1861
|
-
while (filled < paramKeys.length && set[paramKeys[filled]!] !== undefined) filled
|
|
1862
|
-
if (filled > 0 && filled < paramKeys.length) return true
|
|
1866
|
+
let filled = 0
|
|
1867
|
+
while (filled < paramKeys.length && set[paramKeys[filled]!] !== undefined) filled++
|
|
1868
|
+
if (filled > 0 && filled < paramKeys.length) return true
|
|
1863
1869
|
}
|
|
1864
|
-
return false
|
|
1870
|
+
return false
|
|
1865
1871
|
}
|
|
1866
1872
|
|
|
1867
1873
|
/**
|
|
@@ -1873,16 +1879,16 @@ async function hasFullStaticParamSet(
|
|
|
1873
1879
|
config: Awaited<ReturnType<typeof loadConfig>>,
|
|
1874
1880
|
route: RouteManifestEntry,
|
|
1875
1881
|
): Promise<boolean> {
|
|
1876
|
-
if (!route.hasStaticParams) return false
|
|
1877
|
-
let staticParams: Awaited<ReturnType<typeof staticParamsFor
|
|
1882
|
+
if (!route.hasStaticParams) return false
|
|
1883
|
+
let staticParams: Awaited<ReturnType<typeof staticParamsFor>>
|
|
1878
1884
|
try {
|
|
1879
|
-
staticParams = await staticParamsFor(config, route)
|
|
1885
|
+
staticParams = await staticParamsFor(config, route)
|
|
1880
1886
|
} catch {
|
|
1881
|
-
return false
|
|
1887
|
+
return false
|
|
1882
1888
|
}
|
|
1883
|
-
const paramKeys = [...route.params, ...(route.catchAll ? [route.catchAll] : [])]
|
|
1884
|
-
if (paramKeys.length === 0) return false
|
|
1885
|
-
return staticParams.allSets.some(set => paramKeys.every(key => set[key] !== undefined))
|
|
1889
|
+
const paramKeys = [...route.params, ...(route.catchAll ? [route.catchAll] : [])]
|
|
1890
|
+
if (paramKeys.length === 0) return false
|
|
1891
|
+
return staticParams.allSets.some(set => paramKeys.every(key => set[key] !== undefined))
|
|
1886
1892
|
}
|
|
1887
1893
|
|
|
1888
1894
|
/**
|
|
@@ -1895,34 +1901,34 @@ async function buildSubShells(
|
|
|
1895
1901
|
config: Awaited<ReturnType<typeof loadConfig>>,
|
|
1896
1902
|
route: RouteManifestEntry,
|
|
1897
1903
|
): Promise<void> {
|
|
1898
|
-
let staticParams: Awaited<ReturnType<typeof staticParamsFor
|
|
1904
|
+
let staticParams: Awaited<ReturnType<typeof staticParamsFor>>
|
|
1899
1905
|
try {
|
|
1900
|
-
staticParams = await staticParamsFor(config, route)
|
|
1906
|
+
staticParams = await staticParamsFor(config, route)
|
|
1901
1907
|
} catch {
|
|
1902
|
-
return
|
|
1908
|
+
return
|
|
1903
1909
|
}
|
|
1904
|
-
route.prerenderedParams = staticParams.allSets
|
|
1905
|
-
const paramKeys = [...route.params, ...(route.catchAll ? [route.catchAll] : [])]
|
|
1910
|
+
route.prerenderedParams = staticParams.allSets
|
|
1911
|
+
const paramKeys = [...route.params, ...(route.catchAll ? [route.catchAll] : [])]
|
|
1906
1912
|
// Distinct leading prefixes AND full param sets. A partial prefix fills keys
|
|
1907
1913
|
// [0..k) (k < len) and leaves the rest hanging → a sub-shell (postponed). A
|
|
1908
1914
|
// FULL set fills every param → a fully-prerendered shell with no holes (not
|
|
1909
1915
|
// postponed) that the request path serves for that exact URL.
|
|
1910
|
-
const prefixes = new Map<string, Record<string, RouteParamValue>>()
|
|
1916
|
+
const prefixes = new Map<string, Record<string, RouteParamValue>>()
|
|
1911
1917
|
for (const set of staticParams.allSets) {
|
|
1912
|
-
let filled = 0
|
|
1913
|
-
while (filled < paramKeys.length && set[paramKeys[filled]!] !== undefined) filled
|
|
1914
|
-
if (filled === 0) continue
|
|
1915
|
-
const concrete: Record<string, RouteParamValue> = {}
|
|
1916
|
-
for (let i = 0; i < filled; i++) concrete[paramKeys[i]!] = set[paramKeys[i]!]
|
|
1917
|
-
const key = subShellKey(concrete)
|
|
1918
|
-
if (!prefixes.has(key)) prefixes.set(key, concrete)
|
|
1918
|
+
let filled = 0
|
|
1919
|
+
while (filled < paramKeys.length && set[paramKeys[filled]!] !== undefined) filled++
|
|
1920
|
+
if (filled === 0) continue
|
|
1921
|
+
const concrete: Record<string, RouteParamValue> = {}
|
|
1922
|
+
for (let i = 0; i < filled; i++) concrete[paramKeys[i]!] = set[paramKeys[i]!]!
|
|
1923
|
+
const key = subShellKey(concrete)
|
|
1924
|
+
if (!prefixes.has(key)) prefixes.set(key, concrete)
|
|
1919
1925
|
}
|
|
1920
|
-
if (prefixes.size === 0) return
|
|
1926
|
+
if (prefixes.size === 0) return
|
|
1921
1927
|
|
|
1922
|
-
const subShells: NonNullable<RouteManifestEntry['pprSubShells']> = []
|
|
1928
|
+
const subShells: NonNullable<RouteManifestEntry['pprSubShells']> = []
|
|
1923
1929
|
for (const [key, concreteParams] of prefixes) {
|
|
1924
|
-
const routePath = fillRoutePath(route.route, concreteParams)
|
|
1925
|
-
const url = new URL(`http://pnext.local${routePath}`)
|
|
1930
|
+
const routePath = fillRoutePath(route.route, concreteParams)
|
|
1931
|
+
const url = new URL(`http://pnext.local${routePath}`)
|
|
1926
1932
|
// Collect cache meta so a sub-shell's cache tags (its concrete-param
|
|
1927
1933
|
// prefix lets `use cache` scopes fill that the fallback shell postponed)
|
|
1928
1934
|
// join the route's tag set for PPR-shell tag staleness.
|
|
@@ -1934,13 +1940,13 @@ async function buildSubShells(
|
|
|
1934
1940
|
),
|
|
1935
1941
|
{ fetchCache: route.segmentConfig?.fetchCache, route: routePath || '/', prerender: true },
|
|
1936
1942
|
),
|
|
1937
|
-
)
|
|
1938
|
-
recordRouteCacheTags(route, tags)
|
|
1939
|
-
if (!prebuilt) continue
|
|
1940
|
-
const file = pprSubShellPath(config.outPath, route.id, key)
|
|
1941
|
-
await mkdir(path.dirname(file), { recursive: true })
|
|
1942
|
-
await writeText(file, prebuilt.shell)
|
|
1943
|
-
subShells.push({ key, concreteParams, holes: prebuilt.holes })
|
|
1943
|
+
)
|
|
1944
|
+
recordRouteCacheTags(route, tags)
|
|
1945
|
+
if (!prebuilt) continue
|
|
1946
|
+
const file = pprSubShellPath(config.outPath, route.id, key)
|
|
1947
|
+
await mkdir(path.dirname(file), { recursive: true })
|
|
1948
|
+
await writeText(file, prebuilt.shell)
|
|
1949
|
+
subShells.push({ key, concreteParams, holes: prebuilt.holes })
|
|
1944
1950
|
// A FULL param set is a concrete prerendered path - emit Next's `.{html,meta}` plus tree-segment
|
|
1945
1951
|
// artifacts for it. The sub-shell is the prerendered html (dynamic holes stay excluded, so a
|
|
1946
1952
|
// dynamic <head> never leaks into the static copy); a blocking probe render supplies the real
|
|
@@ -1963,7 +1969,7 @@ async function buildSubShells(
|
|
|
1963
1969
|
{ route: routePath || '/', prerender: true },
|
|
1964
1970
|
)
|
|
1965
1971
|
.then(result => result.value),
|
|
1966
|
-
)
|
|
1972
|
+
)
|
|
1967
1973
|
// Dynamic metadata counts: the head resumes at request time, so the shell is partial even
|
|
1968
1974
|
// with zero body holes. The sub-shell render may not re-flag metadataDynamic once the
|
|
1969
1975
|
// fallback shell recorded it, so the route-level flag joins in. http-access fallback recovery
|
|
@@ -1973,12 +1979,12 @@ async function buildSubShells(
|
|
|
1973
1979
|
prebuilt.holes.length > 0 ||
|
|
1974
1980
|
prebuilt.metadataDynamic ||
|
|
1975
1981
|
route.pprMetadata === true ||
|
|
1976
|
-
routeSourcesHaveDynamicHead(route)
|
|
1982
|
+
routeSourcesHaveDynamicHead(route)
|
|
1977
1983
|
await emitConcreteNextPageArtifacts(config, route, routePath, {
|
|
1978
1984
|
html: postponed ? prebuilt.shell : `${prebuilt.shell}\n </body></html>`,
|
|
1979
1985
|
status: probe.status,
|
|
1980
1986
|
postponed,
|
|
1981
|
-
})
|
|
1987
|
+
})
|
|
1982
1988
|
} catch {
|
|
1983
1989
|
// Probe render failed — the sub-shell itself is unaffected.
|
|
1984
1990
|
}
|
|
@@ -1986,8 +1992,8 @@ async function buildSubShells(
|
|
|
1986
1992
|
}
|
|
1987
1993
|
subShells.sort(
|
|
1988
1994
|
(a, b) => Object.keys(b.concreteParams).length - Object.keys(a.concreteParams).length,
|
|
1989
|
-
)
|
|
1990
|
-
if (subShells.length > 0) route.pprSubShells = subShells
|
|
1995
|
+
)
|
|
1996
|
+
if (subShells.length > 0) route.pprSubShells = subShells
|
|
1991
1997
|
}
|
|
1992
1998
|
|
|
1993
1999
|
/**
|
|
@@ -2000,12 +2006,12 @@ function routeSourcesUseHttpFallback(root: string, route: RouteManifestEntry): b
|
|
|
2000
2006
|
// App files only: `sourceFiles` also carries framework entries (pnext's own
|
|
2001
2007
|
// compat sources name these APIs), which would blocking-probe every route
|
|
2002
2008
|
// that imports next/server.
|
|
2003
|
-
if (!file.startsWith(root) || file.includes(`${path.sep}node_modules${path.sep}`)) continue
|
|
2004
|
-
if (!existsSync(file)) continue
|
|
2005
|
-
const source = readTextSync(file)
|
|
2006
|
-
if (/\b(?:notFound|forbidden|unauthorized)\s*\(\s*\)/.test(source)) return true
|
|
2009
|
+
if (!file.startsWith(root) || file.includes(`${path.sep}node_modules${path.sep}`)) continue
|
|
2010
|
+
if (!existsSync(file)) continue
|
|
2011
|
+
const source = readTextSync(file)
|
|
2012
|
+
if (/\b(?:notFound|forbidden|unauthorized)\s*\(\s*\)/.test(source)) return true
|
|
2007
2013
|
}
|
|
2008
|
-
return false
|
|
2014
|
+
return false
|
|
2009
2015
|
}
|
|
2010
2016
|
|
|
2011
2017
|
/**
|
|
@@ -2021,17 +2027,17 @@ function routeSourcesUseHttpFallback(root: string, route: RouteManifestEntry): b
|
|
|
2021
2027
|
*/
|
|
2022
2028
|
function routeSourcesUseRequestApi(root: string, route: RouteManifestEntry): boolean {
|
|
2023
2029
|
for (const file of route.sourceFiles) {
|
|
2024
|
-
if (!file.startsWith(root) || file.includes(`${path.sep}node_modules${path.sep}`)) continue
|
|
2025
|
-
if (!existsSync(file)) continue
|
|
2030
|
+
if (!file.startsWith(root) || file.includes(`${path.sep}node_modules${path.sep}`)) continue
|
|
2031
|
+
if (!existsSync(file)) continue
|
|
2026
2032
|
// Comments are stripped first: fixtures routinely SAY "no connection()" in
|
|
2027
2033
|
// prose above a page that has none (`//` inside a string only costs a
|
|
2028
2034
|
// false negative, which is the safe direction here).
|
|
2029
2035
|
const source = readTextSync(file)
|
|
2030
2036
|
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
2031
|
-
.replace(/(^|[^:])\/\/.*$/gm, '$1')
|
|
2032
|
-
if (/\b(?:connection|cookies|headers|draftMode)\s*\(\s*\)/.test(source)) return true
|
|
2037
|
+
.replace(/(^|[^:])\/\/.*$/gm, '$1')
|
|
2038
|
+
if (/\b(?:connection|cookies|headers|draftMode)\s*\(\s*\)/.test(source)) return true
|
|
2033
2039
|
}
|
|
2034
|
-
return false
|
|
2040
|
+
return false
|
|
2035
2041
|
}
|
|
2036
2042
|
|
|
2037
2043
|
/**
|
|
@@ -2041,19 +2047,19 @@ function routeSourcesUseRequestApi(root: string, route: RouteManifestEntry): boo
|
|
|
2041
2047
|
*/
|
|
2042
2048
|
function routeSourcesHaveDynamicHead(route: RouteManifestEntry): boolean {
|
|
2043
2049
|
for (const file of route.sourceFiles) {
|
|
2044
|
-
if (!existsSync(file)) continue
|
|
2045
|
-
const source = readTextSync(file)
|
|
2050
|
+
if (!existsSync(file)) continue
|
|
2051
|
+
const source = readTextSync(file)
|
|
2046
2052
|
const match =
|
|
2047
2053
|
/export\s+(?:async\s+)?function\s+(?:generateMetadata|generateViewport)\s*\([^)]*\)\s*(?::[^{]+)?\{/.exec(
|
|
2048
2054
|
source,
|
|
2049
|
-
)
|
|
2050
|
-
if (!match) continue
|
|
2051
|
-
const body = source.slice(match.index + match[0].length)
|
|
2055
|
+
)
|
|
2056
|
+
if (!match) continue
|
|
2057
|
+
const body = source.slice(match.index + match[0].length)
|
|
2052
2058
|
if (/\bawait\b/.test(body.split('\nexport ')[0] ?? body) && !body.includes('use cache')) {
|
|
2053
|
-
return true
|
|
2059
|
+
return true
|
|
2054
2060
|
}
|
|
2055
2061
|
}
|
|
2056
|
-
return false
|
|
2062
|
+
return false
|
|
2057
2063
|
}
|
|
2058
2064
|
|
|
2059
2065
|
/**
|
|
@@ -2070,38 +2076,38 @@ function filterDebugBuildRoutes(
|
|
|
2070
2076
|
.split(',')
|
|
2071
2077
|
.map(pattern => pattern.trim())
|
|
2072
2078
|
.filter(pattern => pattern.length > 0 && !pattern.startsWith('!'))
|
|
2073
|
-
.map(debugBuildPathMatcher)
|
|
2074
|
-
if (matchers.length === 0) return routes
|
|
2079
|
+
.map(debugBuildPathMatcher)
|
|
2080
|
+
if (matchers.length === 0) return routes
|
|
2075
2081
|
return routes.filter(route => {
|
|
2076
|
-
const relative = toPosixPath(path.relative(root, route.file))
|
|
2077
|
-
return matchers.some(matcher => matcher(relative))
|
|
2078
|
-
})
|
|
2082
|
+
const relative = toPosixPath(path.relative(root, route.file))
|
|
2083
|
+
return matchers.some(matcher => matcher(relative))
|
|
2084
|
+
})
|
|
2079
2085
|
}
|
|
2080
2086
|
|
|
2081
2087
|
function debugBuildPathMatcher(pattern: string): (file: string) => boolean {
|
|
2082
2088
|
if (!/[*?]/.test(pattern)) {
|
|
2083
|
-
return file => file === pattern || file.endsWith(`/${pattern}`)
|
|
2089
|
+
return file => file === pattern || file.endsWith(`/${pattern}`)
|
|
2084
2090
|
}
|
|
2085
2091
|
const regex = new RegExp(
|
|
2086
2092
|
`^${pattern
|
|
2087
2093
|
.split(/(\*\*\/|\*\*|\*|\?)/)
|
|
2088
2094
|
.map(part => {
|
|
2089
|
-
if (part === '**/') return '(?:.*/)?'
|
|
2090
|
-
if (part === '**') return '.*'
|
|
2091
|
-
if (part === '*') return '[^/]*'
|
|
2092
|
-
if (part === '?') return '[^/]'
|
|
2093
|
-
return part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
2095
|
+
if (part === '**/') return '(?:.*/)?'
|
|
2096
|
+
if (part === '**') return '.*'
|
|
2097
|
+
if (part === '*') return '[^/]*'
|
|
2098
|
+
if (part === '?') return '[^/]'
|
|
2099
|
+
return part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
2094
2100
|
})
|
|
2095
2101
|
.join('')}$`,
|
|
2096
|
-
)
|
|
2097
|
-
return file => regex.test(file)
|
|
2102
|
+
)
|
|
2103
|
+
return file => regex.test(file)
|
|
2098
2104
|
}
|
|
2099
2105
|
|
|
2100
2106
|
/** Stable on-disk signature for a sub-shell's concrete-param prefix. */
|
|
2101
2107
|
function subShellKey(concrete: Record<string, RouteParamValue>): string {
|
|
2102
2108
|
return Object.entries(concrete)
|
|
2103
2109
|
.map(([k, v]) => `${k}-${(Array.isArray(v) ? v.join('_') : v).replace(/[^a-zA-Z0-9_]/g, '_')}`)
|
|
2104
|
-
.join('.')
|
|
2110
|
+
.join('.')
|
|
2105
2111
|
}
|
|
2106
2112
|
|
|
2107
2113
|
/**
|
|
@@ -2110,16 +2116,16 @@ function subShellKey(concrete: Record<string, RouteParamValue>): string {
|
|
|
2110
2116
|
* (next/server etc.), so start must load this build-time artifact instead.
|
|
2111
2117
|
*/
|
|
2112
2118
|
async function buildProxyModule(config: Awaited<ReturnType<typeof loadConfig>>) {
|
|
2113
|
-
await validateProxyFiles(config)
|
|
2114
|
-
const file = findProxyFile(config)
|
|
2115
|
-
if (!file) return undefined
|
|
2119
|
+
await validateProxyFiles(config)
|
|
2120
|
+
const file = findProxyFile(config)
|
|
2121
|
+
if (!file) return undefined
|
|
2116
2122
|
const href = await devServerModuleHref(config, file, 'prod', {
|
|
2117
2123
|
conditionTarget: 'edge',
|
|
2118
2124
|
externalLoadTarget: proxyExternalLoadTarget(file),
|
|
2119
2125
|
reactServerLayer: true,
|
|
2120
|
-
})
|
|
2121
|
-
const compiled = fileURLToPath(href)
|
|
2122
|
-
return toPosixPath(path.relative(config.outPath, compiled))
|
|
2126
|
+
})
|
|
2127
|
+
const compiled = fileURLToPath(href)
|
|
2128
|
+
return toPosixPath(path.relative(config.outPath, compiled))
|
|
2123
2129
|
}
|
|
2124
2130
|
|
|
2125
2131
|
/**
|
|
@@ -2136,14 +2142,14 @@ async function renderNotFoundDocuments({
|
|
|
2136
2142
|
staticMetadataFiles,
|
|
2137
2143
|
staticModuleMetadata,
|
|
2138
2144
|
}: {
|
|
2139
|
-
config: Awaited<ReturnType<typeof loadConfig
|
|
2140
|
-
log: VerboseLogger
|
|
2141
|
-
skip: boolean
|
|
2142
|
-
documentCss: Promise<unknown
|
|
2143
|
-
staticMetadataFiles: StaticMetadataFile[]
|
|
2144
|
-
staticModuleMetadata: Record<string, StaticModuleMetadata
|
|
2145
|
+
config: Awaited<ReturnType<typeof loadConfig>>
|
|
2146
|
+
log: VerboseLogger
|
|
2147
|
+
skip: boolean
|
|
2148
|
+
documentCss: Promise<unknown>
|
|
2149
|
+
staticMetadataFiles: StaticMetadataFile[]
|
|
2150
|
+
staticModuleMetadata: Record<string, StaticModuleMetadata>
|
|
2145
2151
|
}): Promise<string | undefined> {
|
|
2146
|
-
if (skip) return undefined
|
|
2152
|
+
if (skip) return undefined
|
|
2147
2153
|
const render = async () => {
|
|
2148
2154
|
const response = await renderGlobalNotFoundResponse({
|
|
2149
2155
|
config,
|
|
@@ -2151,29 +2157,29 @@ async function renderNotFoundDocuments({
|
|
|
2151
2157
|
staticMetadataFiles,
|
|
2152
2158
|
staticModuleMetadata,
|
|
2153
2159
|
resolveDynamicMetadataRoutes: true,
|
|
2154
|
-
})
|
|
2155
|
-
return response.text()
|
|
2156
|
-
}
|
|
2160
|
+
})
|
|
2161
|
+
return response.text()
|
|
2162
|
+
}
|
|
2157
2163
|
// The document links the not-found stylesheet chunk buildNotFoundCss emits.
|
|
2158
|
-
await documentCss
|
|
2164
|
+
await documentCss
|
|
2159
2165
|
|
|
2160
2166
|
if (await shouldBuildGlobalNotFound(config)) {
|
|
2161
2167
|
await log.step('global not-found', async () =>
|
|
2162
2168
|
writeText(path.join(config.outPath, 'public', '404.html'), await render()),
|
|
2163
|
-
)
|
|
2169
|
+
)
|
|
2164
2170
|
}
|
|
2165
2171
|
// Next always prerenders the built-in `/_not-found` document for a compat
|
|
2166
2172
|
// build even without a prerenderable not-found file (not-found/default reads
|
|
2167
2173
|
// `.next/server/app/_not-found.html`). When public/404.html was written, the
|
|
2168
2174
|
// .next artifacts are copied from it; otherwise they need a document here.
|
|
2169
|
-
if (!config.compat?.next) return undefined
|
|
2170
|
-
if (existsSync(path.join(config.outPath, 'public', '404.html'))) return undefined
|
|
2175
|
+
if (!config.compat?.next) return undefined
|
|
2176
|
+
if (existsSync(path.join(config.outPath, 'public', '404.html'))) return undefined
|
|
2171
2177
|
// Nothing prerenderable to render into the document: either the app authored no not-found.* at
|
|
2172
2178
|
// all, or it authored one and declared it force-dynamic, in which case Next leaves `/_not-found`
|
|
2173
2179
|
// dynamic and prerenders no custom 404 either. Emit the standalone default rather than boot the
|
|
2174
2180
|
// whole server graph for an artifact that is never served - the app's *runtime* 404, still
|
|
2175
2181
|
// dynamic, is what users see.
|
|
2176
|
-
return defaultNotFoundDocument()
|
|
2182
|
+
return defaultNotFoundDocument()
|
|
2177
2183
|
}
|
|
2178
2184
|
|
|
2179
2185
|
/**
|
|
@@ -2186,9 +2192,9 @@ async function shouldBuildGlobalNotFound(config: Awaited<ReturnType<typeof loadC
|
|
|
2186
2192
|
path.join(config.appPath, `global-not-found.${ext}`),
|
|
2187
2193
|
path.join(config.appPath, `not-found.${ext}`),
|
|
2188
2194
|
])
|
|
2189
|
-
.find(candidate => existsSync(candidate))
|
|
2190
|
-
if (!file) return false
|
|
2191
|
-
return !/\bexport\s+const\s+dynamic\s*=\s*['"]force-dynamic['"]/.test(await readText(file))
|
|
2195
|
+
.find(candidate => existsSync(candidate))
|
|
2196
|
+
if (!file) return false
|
|
2197
|
+
return !/\bexport\s+const\s+dynamic\s*=\s*['"]force-dynamic['"]/.test(await readText(file))
|
|
2192
2198
|
}
|
|
2193
2199
|
|
|
2194
2200
|
/**
|
|
@@ -2202,99 +2208,99 @@ async function emitProxyServerArtifacts(
|
|
|
2202
2208
|
config: Awaited<ReturnType<typeof loadConfig>>,
|
|
2203
2209
|
proxyModule: string | undefined,
|
|
2204
2210
|
) {
|
|
2205
|
-
if (!config.compat?.next || !proxyModule) return
|
|
2206
|
-
const compiled = path.resolve(config.outPath, proxyModule)
|
|
2207
|
-
if (!existsSync(compiled)) return
|
|
2211
|
+
if (!config.compat?.next || !proxyModule) return
|
|
2212
|
+
const compiled = path.resolve(config.outPath, proxyModule)
|
|
2213
|
+
if (!existsSync(compiled)) return
|
|
2208
2214
|
|
|
2209
|
-
const serverDir = path.join(config.root, '.next', 'server')
|
|
2210
|
-
await mkdir(serverDir, { recursive: true })
|
|
2211
|
-
const middlewareFile = path.join(serverDir, 'middleware.js')
|
|
2212
|
-
await copyFile(compiled, middlewareFile)
|
|
2215
|
+
const serverDir = path.join(config.root, '.next', 'server')
|
|
2216
|
+
await mkdir(serverDir, { recursive: true })
|
|
2217
|
+
const middlewareFile = path.join(serverDir, 'middleware.js')
|
|
2218
|
+
await copyFile(compiled, middlewareFile)
|
|
2213
2219
|
|
|
2214
2220
|
// NFT files are relative to the trace file's directory and must all exist on
|
|
2215
2221
|
// disk. The self-contained bundle is the only traced output.
|
|
2216
|
-
const files = ['middleware.js'].filter(name => existsSync(path.join(serverDir, name)))
|
|
2222
|
+
const files = ['middleware.js'].filter(name => existsSync(path.join(serverDir, name)))
|
|
2217
2223
|
await writeFile(
|
|
2218
2224
|
path.join(serverDir, 'middleware.js.nft.json'),
|
|
2219
2225
|
`${JSON.stringify({ version: 1, files })}\n`,
|
|
2220
|
-
)
|
|
2226
|
+
)
|
|
2221
2227
|
}
|
|
2222
2228
|
|
|
2223
2229
|
async function emitNextNotFoundArtifacts(
|
|
2224
2230
|
config: Awaited<ReturnType<typeof loadConfig>>,
|
|
2225
2231
|
fallback404?: string,
|
|
2226
2232
|
) {
|
|
2227
|
-
if (!config.compat?.next) return
|
|
2228
|
-
const source = path.join(config.outPath, 'public', '404.html')
|
|
2229
|
-
const html = existsSync(source) ? readFileSync(source, 'utf8') : fallback404
|
|
2230
|
-
if (html === undefined) return
|
|
2231
|
-
|
|
2232
|
-
const serverDir = path.join(config.root, '.next', 'server')
|
|
2233
|
-
const pagesDir = path.join(serverDir, 'pages')
|
|
2234
|
-
const notFoundDir = path.join(serverDir, 'app', '_not-found')
|
|
2235
|
-
await mkdir(pagesDir, { recursive: true })
|
|
2236
|
-
await mkdir(notFoundDir, { recursive: true })
|
|
2237
|
-
await writeFile(path.join(pagesDir, '404.html'), html)
|
|
2233
|
+
if (!config.compat?.next) return
|
|
2234
|
+
const source = path.join(config.outPath, 'public', '404.html')
|
|
2235
|
+
const html = existsSync(source) ? readFileSync(source, 'utf8') : fallback404
|
|
2236
|
+
if (html === undefined) return
|
|
2237
|
+
|
|
2238
|
+
const serverDir = path.join(config.root, '.next', 'server')
|
|
2239
|
+
const pagesDir = path.join(serverDir, 'pages')
|
|
2240
|
+
const notFoundDir = path.join(serverDir, 'app', '_not-found')
|
|
2241
|
+
await mkdir(pagesDir, { recursive: true })
|
|
2242
|
+
await mkdir(notFoundDir, { recursive: true })
|
|
2243
|
+
await writeFile(path.join(pagesDir, '404.html'), html)
|
|
2238
2244
|
// Next's app-router prerender output for the built-in /_not-found route. The
|
|
2239
2245
|
// .rsc mirror carries the document body like the segment artifacts do
|
|
2240
2246
|
// (not-found/default greps both for the noindex marker).
|
|
2241
|
-
await writeFile(path.join(serverDir, 'app', '_not-found.html'), html)
|
|
2242
|
-
await writeFile(path.join(serverDir, 'app', '_not-found.rsc'), html)
|
|
2247
|
+
await writeFile(path.join(serverDir, 'app', '_not-found.html'), html)
|
|
2248
|
+
await writeFile(path.join(serverDir, 'app', '_not-found.rsc'), html)
|
|
2243
2249
|
|
|
2244
|
-
const pagesManifestFile = path.join(serverDir, 'pages-manifest.json')
|
|
2245
|
-
let pagesManifest: Record<string, string> = {}
|
|
2250
|
+
const pagesManifestFile = path.join(serverDir, 'pages-manifest.json')
|
|
2251
|
+
let pagesManifest: Record<string, string> = {}
|
|
2246
2252
|
try {
|
|
2247
|
-
pagesManifest = JSON.parse(readFileSync(pagesManifestFile, 'utf8')) as Record<string, string
|
|
2253
|
+
pagesManifest = JSON.parse(readFileSync(pagesManifestFile, 'utf8')) as Record<string, string>
|
|
2248
2254
|
} catch {
|
|
2249
2255
|
// First compat build has no pages manifest yet.
|
|
2250
2256
|
}
|
|
2251
|
-
pagesManifest['/404'] = 'pages/404.html'
|
|
2252
|
-
await writeFile(pagesManifestFile, `${JSON.stringify(pagesManifest, null, 2)}\n`)
|
|
2257
|
+
pagesManifest['/404'] = 'pages/404.html'
|
|
2258
|
+
await writeFile(pagesManifestFile, `${JSON.stringify(pagesManifest, null, 2)}\n`)
|
|
2253
2259
|
|
|
2254
|
-
const clientReferenceManifest = 'page_client-reference-manifest.js'
|
|
2255
|
-
await writeFile(path.join(notFoundDir, clientReferenceManifest), 'self.__RSC_MANIFEST={}\n')
|
|
2260
|
+
const clientReferenceManifest = 'page_client-reference-manifest.js'
|
|
2261
|
+
await writeFile(path.join(notFoundDir, clientReferenceManifest), 'self.__RSC_MANIFEST={}\n')
|
|
2256
2262
|
await writeFile(
|
|
2257
2263
|
path.join(notFoundDir, 'page.js.nft.json'),
|
|
2258
2264
|
`${JSON.stringify({ version: 1, files: [clientReferenceManifest] }, null, 2)}\n`,
|
|
2259
|
-
)
|
|
2265
|
+
)
|
|
2260
2266
|
}
|
|
2261
2267
|
|
|
2262
2268
|
async function collectStaticModuleMetadata(
|
|
2263
2269
|
config: Awaited<ReturnType<typeof loadConfig>>,
|
|
2264
2270
|
routes: RouteManifestEntry[],
|
|
2265
2271
|
) {
|
|
2266
|
-
const metadata: Record<string, StaticModuleMetadata> = {}
|
|
2267
|
-
if (config.compat?.next) return metadata
|
|
2272
|
+
const metadata: Record<string, StaticModuleMetadata> = {}
|
|
2273
|
+
if (config.compat?.next) return metadata
|
|
2268
2274
|
|
|
2269
|
-
const files = new Set<string>()
|
|
2275
|
+
const files = new Set<string>()
|
|
2270
2276
|
for (const route of routes) {
|
|
2271
|
-
if (route.kind !== 'page') continue
|
|
2277
|
+
if (route.kind !== 'page') continue
|
|
2272
2278
|
for (const file of findLayouts(config.appPath, route.file)) {
|
|
2273
|
-
if (existsSync(file)) files.add(file)
|
|
2279
|
+
if (existsSync(file)) files.add(file)
|
|
2274
2280
|
}
|
|
2275
|
-
files.add(route.file)
|
|
2281
|
+
files.add(route.file)
|
|
2276
2282
|
}
|
|
2277
|
-
if (files.size === 0) return metadata
|
|
2283
|
+
if (files.size === 0) return metadata
|
|
2278
2284
|
|
|
2279
|
-
registerServerRuntime(config, [...files])
|
|
2280
|
-
registerCssRuntime()
|
|
2285
|
+
registerServerRuntime(config, [...files])
|
|
2286
|
+
registerCssRuntime()
|
|
2281
2287
|
for (const file of files) {
|
|
2282
2288
|
const href = reactCompatEnabled(config)
|
|
2283
2289
|
? await devServerModuleHref(config, file, 'build')
|
|
2284
|
-
: pathToFileHref(file)
|
|
2290
|
+
: pathToFileHref(file)
|
|
2285
2291
|
const module = (await import(href)) as {
|
|
2286
|
-
metadata?: Parameters<typeof readModuleMetadata>[0]['metadata']
|
|
2287
|
-
viewport?: Parameters<typeof readModuleViewport>[0]['viewport']
|
|
2288
|
-
}
|
|
2289
|
-
const entry: StaticModuleMetadata = {}
|
|
2290
|
-
const routeMetadata = await readModuleMetadata(module)
|
|
2291
|
-
const viewport = await readModuleViewport(module)
|
|
2292
|
-
if (routeMetadata) entry.metadata = routeMetadata
|
|
2293
|
-
if (viewport) entry.viewport = viewport
|
|
2294
|
-
if (entry.metadata || entry.viewport) metadata[file] = entry
|
|
2292
|
+
metadata?: Parameters<typeof readModuleMetadata>[0]['metadata']
|
|
2293
|
+
viewport?: Parameters<typeof readModuleViewport>[0]['viewport']
|
|
2294
|
+
}
|
|
2295
|
+
const entry: StaticModuleMetadata = {}
|
|
2296
|
+
const routeMetadata = await readModuleMetadata(module)
|
|
2297
|
+
const viewport = await readModuleViewport(module)
|
|
2298
|
+
if (routeMetadata) entry.metadata = routeMetadata
|
|
2299
|
+
if (viewport) entry.viewport = viewport
|
|
2300
|
+
if (entry.metadata || entry.viewport) metadata[file] = entry
|
|
2295
2301
|
}
|
|
2296
2302
|
|
|
2297
|
-
return metadata
|
|
2303
|
+
return metadata
|
|
2298
2304
|
}
|
|
2299
2305
|
|
|
2300
2306
|
async function collectStaticRouteMetadata(
|
|
@@ -2302,14 +2308,14 @@ async function collectStaticRouteMetadata(
|
|
|
2302
2308
|
routes: RouteManifestEntry[],
|
|
2303
2309
|
staticMetadataFiles: StaticMetadataFile[],
|
|
2304
2310
|
) {
|
|
2305
|
-
const metadata: Record<string, StaticRouteMetadata> = {}
|
|
2306
|
-
if (config.compat?.next) return metadata
|
|
2311
|
+
const metadata: Record<string, StaticRouteMetadata> = {}
|
|
2312
|
+
if (config.compat?.next) return metadata
|
|
2307
2313
|
|
|
2308
2314
|
for (const route of routes) {
|
|
2309
|
-
if (route.kind !== 'page' || route.interception) continue
|
|
2310
|
-
const paramSets = route.prerenderedParams ?? (route.params.length === 0 ? [{}] : [])
|
|
2315
|
+
if (route.kind !== 'page' || route.interception) continue
|
|
2316
|
+
const paramSets = route.prerenderedParams ?? (route.params.length === 0 ? [{}] : [])
|
|
2311
2317
|
for (const params of paramSets) {
|
|
2312
|
-
const routePath = fillRoutePath(route.route, params)
|
|
2318
|
+
const routePath = fillRoutePath(route.route, params)
|
|
2313
2319
|
const base = route.interception
|
|
2314
2320
|
? staticMetadataForRouteFromFiles(
|
|
2315
2321
|
staticMetadataFiles,
|
|
@@ -2317,19 +2323,19 @@ async function collectStaticRouteMetadata(
|
|
|
2317
2323
|
route.file,
|
|
2318
2324
|
routePath,
|
|
2319
2325
|
)
|
|
2320
|
-
: staticMetadataForPathFromFiles(staticMetadataFiles, routePath)
|
|
2326
|
+
: staticMetadataForPathFromFiles(staticMetadataFiles, routePath)
|
|
2321
2327
|
const resolved = await withDynamicMetadataRoutes(
|
|
2322
2328
|
base,
|
|
2323
2329
|
config.appPath,
|
|
2324
2330
|
route.file,
|
|
2325
2331
|
routePath,
|
|
2326
2332
|
file => importMetadataModule(config, file),
|
|
2327
|
-
)
|
|
2328
|
-
metadata[staticRouteMetadataKey(routePath)] = resolved
|
|
2333
|
+
)
|
|
2334
|
+
metadata[staticRouteMetadataKey(routePath)] = resolved
|
|
2329
2335
|
}
|
|
2330
2336
|
}
|
|
2331
2337
|
|
|
2332
|
-
return metadata
|
|
2338
|
+
return metadata
|
|
2333
2339
|
}
|
|
2334
2340
|
|
|
2335
2341
|
async function importMetadataModule(
|
|
@@ -2338,8 +2344,8 @@ async function importMetadataModule(
|
|
|
2338
2344
|
): Promise<Record<string, unknown>> {
|
|
2339
2345
|
const href = reactCompatEnabled(config)
|
|
2340
2346
|
? await devServerModuleHref(config, file, 'build')
|
|
2341
|
-
: pathToFileHref(file)
|
|
2342
|
-
return import(href) as Promise<Record<string, unknown
|
|
2347
|
+
: pathToFileHref(file)
|
|
2348
|
+
return import(href) as Promise<Record<string, unknown>>
|
|
2343
2349
|
}
|
|
2344
2350
|
|
|
2345
2351
|
async function buildStaticRouteHandler(
|
|
@@ -2347,8 +2353,8 @@ async function buildStaticRouteHandler(
|
|
|
2347
2353
|
route: RouteManifestEntry,
|
|
2348
2354
|
options: { skipPhysicalWrite?: boolean } = {},
|
|
2349
2355
|
) {
|
|
2350
|
-
const skipPhysicalWrite = options.skipPhysicalWrite ?? false
|
|
2351
|
-
registerServerRuntime(config, route.sourceFiles)
|
|
2356
|
+
const skipPhysicalWrite = options.skipPhysicalWrite ?? false
|
|
2357
|
+
registerServerRuntime(config, route.sourceFiles)
|
|
2352
2358
|
// Compat mode loads the compiled module (aliases baked in) so bare next/*
|
|
2353
2359
|
// imports (next/headers etc.) resolve to the compat layer, not an installed
|
|
2354
2360
|
// next package. Same pattern as page prerenders.
|
|
@@ -2356,16 +2362,16 @@ async function buildStaticRouteHandler(
|
|
|
2356
2362
|
? await devServerModuleHref(config, route.file, 'build', {
|
|
2357
2363
|
conditionTarget: serverBundleTargetForRuntime(route.segmentConfig?.runtime),
|
|
2358
2364
|
})
|
|
2359
|
-
: pathToFileHref(route.file)
|
|
2360
|
-
const imported = (await import(moduleHref)) as Parameters<typeof metadataRouteHandlerModule>[0]
|
|
2361
|
-
const routeModule = metadataRouteHandlerModule(imported, route) ?? imported
|
|
2365
|
+
: pathToFileHref(route.file)
|
|
2366
|
+
const imported = (await import(moduleHref)) as Parameters<typeof metadataRouteHandlerModule>[0]
|
|
2367
|
+
const routeModule = metadataRouteHandlerModule(imported, route) ?? imported
|
|
2362
2368
|
const module = (
|
|
2363
2369
|
nextCompatEnabled(config)
|
|
2364
2370
|
? buildCompat().normalizeStaticParamsModule(routeModule as Record<string, unknown>)
|
|
2365
2371
|
: routeModule
|
|
2366
|
-
) as RouteHandlerModule
|
|
2367
|
-
const paramSets = await staticRouteParams(route, module)
|
|
2368
|
-
const staticFiles: Record<string, StaticFileMetadata> = {}
|
|
2372
|
+
) as RouteHandlerModule
|
|
2373
|
+
const paramSets = await staticRouteParams(route, module)
|
|
2374
|
+
const staticFiles: Record<string, StaticFileMetadata> = {}
|
|
2369
2375
|
|
|
2370
2376
|
// A generated-param metadata route (`generateSitemaps` / `generateImageMetadata`)
|
|
2371
2377
|
// knows every id ahead of time, so each expanded pathname is a prerendered
|
|
@@ -2375,26 +2381,24 @@ async function buildStaticRouteHandler(
|
|
|
2375
2381
|
if (route.metadataRoute?.generatedParam && paramSets.length > 0) {
|
|
2376
2382
|
route.metadataRoute.generatedRoutes = paramSets.map(params =>
|
|
2377
2383
|
fillRoutePath(route.route, params, route.metadataRoute?.generatedParam),
|
|
2378
|
-
)
|
|
2384
|
+
)
|
|
2379
2385
|
}
|
|
2380
2386
|
|
|
2381
2387
|
// force-static handlers see a canonicalized, origin-normalized request (no
|
|
2382
2388
|
// search/headers/cookies); Next reports the URL against a fixed base host.
|
|
2383
2389
|
const requestBase =
|
|
2384
|
-
route.segmentConfig?.dynamic === 'force-static'
|
|
2385
|
-
? 'http://localhost:3000'
|
|
2386
|
-
: 'http://pnext.local';
|
|
2390
|
+
route.segmentConfig?.dynamic === 'force-static' ? 'http://localhost:3000' : 'http://pnext.local'
|
|
2387
2391
|
for (const params of paramSets) {
|
|
2388
|
-
const routePath = fillRoutePath(route.route, params, route.metadataRoute?.generatedParam)
|
|
2389
|
-
const file = staticRouteHandlerPath(config.outPath, routePath)
|
|
2392
|
+
const routePath = fillRoutePath(route.route, params, route.metadataRoute?.generatedParam)
|
|
2393
|
+
const file = staticRouteHandlerPath(config.outPath, routePath)
|
|
2390
2394
|
// A descendant handler intentionally forgoes the physical file (its ancestor
|
|
2391
2395
|
// owns public/<parent>), so don't treat the guaranteed collision as a skip —
|
|
2392
2396
|
// still render it to capture the prerender-manifest metadata below.
|
|
2393
2397
|
if (!skipPhysicalWrite) {
|
|
2394
|
-
const collision = staticOutputCollision(config.outPath, file)
|
|
2398
|
+
const collision = staticOutputCollision(config.outPath, file)
|
|
2395
2399
|
if (collision) {
|
|
2396
|
-
warnSkippedStatic(routePath, collision)
|
|
2397
|
-
continue
|
|
2400
|
+
warnSkippedStatic(routePath, collision)
|
|
2401
|
+
continue
|
|
2398
2402
|
}
|
|
2399
2403
|
}
|
|
2400
2404
|
// Wrap in a work unit (like the page prerender path) so an after()
|
|
@@ -2421,123 +2425,124 @@ async function buildStaticRouteHandler(
|
|
|
2421
2425
|
dynamicError: route.segmentConfig?.dynamic === 'error',
|
|
2422
2426
|
},
|
|
2423
2427
|
),
|
|
2424
|
-
)
|
|
2428
|
+
)
|
|
2425
2429
|
// Cache-components only prerenders handlers that settle synchronously or
|
|
2426
2430
|
// in a microtask. A later task (including a delayed response stream)
|
|
2427
2431
|
// depends on runtime work and must not produce a static file.
|
|
2428
|
-
const early = await settleBeforeNextTask(prerender)
|
|
2429
|
-
const completed = early ?? (await prerender)
|
|
2432
|
+
const early = await settleBeforeNextTask(prerender)
|
|
2433
|
+
const completed = early ?? (await prerender)
|
|
2430
2434
|
const rendered =
|
|
2431
2435
|
early ??
|
|
2432
|
-
(route.segmentConfig?.dynamic === 'force-static' ||
|
|
2433
|
-
(await handlerHasEntirelyCachedIo(route))
|
|
2436
|
+
(route.segmentConfig?.dynamic === 'force-static' || (await handlerHasEntirelyCachedIo(route))
|
|
2434
2437
|
? completed
|
|
2435
|
-
: undefined)
|
|
2436
|
-
if (!rendered) continue
|
|
2437
|
-
const response = rendered.value
|
|
2438
|
+
: undefined)
|
|
2439
|
+
if (!rendered) continue
|
|
2440
|
+
const response = rendered.value
|
|
2438
2441
|
// A 5xx means the handler threw during prerender (handleRouteModule funnels
|
|
2439
2442
|
// uncaught throws to an empty 500). Don't bake a broken response into static
|
|
2440
2443
|
// output — surface it to the caller's skip-and-warn path so the route serves
|
|
2441
2444
|
// dynamically instead.
|
|
2442
2445
|
if (response.status >= 500) {
|
|
2443
|
-
throw new Error(`route handler prerender returned ${response.status}`)
|
|
2446
|
+
throw new Error(`route handler prerender returned ${response.status}`)
|
|
2444
2447
|
}
|
|
2445
|
-
if (rendered.noStore && !staticRouteHandlerExplicitlyCached(route)) continue
|
|
2446
|
-
const body = await settleBeforeNextTask(response.clone().arrayBuffer())
|
|
2447
|
-
if (!body) continue
|
|
2448
|
-
const relative = toPosixPath(path.relative(path.join(config.outPath, 'public'), file))
|
|
2448
|
+
if (rendered.noStore && !staticRouteHandlerExplicitlyCached(route)) continue
|
|
2449
|
+
const body = await settleBeforeNextTask(response.clone().arrayBuffer())
|
|
2450
|
+
if (!body) continue
|
|
2451
|
+
const relative = toPosixPath(path.relative(path.join(config.outPath, 'public'), file))
|
|
2449
2452
|
// Descendant handlers record their manifest entry (so the synthesized
|
|
2450
2453
|
// prerender-manifest lists them) but skip the on-disk copy that would
|
|
2451
2454
|
// collide with the ancestor's file; they serve dynamically at runtime.
|
|
2452
2455
|
if (!skipPhysicalWrite) {
|
|
2453
|
-
await mkdir(path.dirname(file), { recursive: true })
|
|
2454
|
-
await writeFile(file, new Uint8Array(body))
|
|
2456
|
+
await mkdir(path.dirname(file), { recursive: true })
|
|
2457
|
+
await writeFile(file, new Uint8Array(body))
|
|
2455
2458
|
}
|
|
2456
2459
|
const revalidateSeconds = combineRevalidate(
|
|
2457
2460
|
route.segmentConfig?.revalidate,
|
|
2458
2461
|
rendered.revalidateSeconds,
|
|
2459
|
-
)
|
|
2462
|
+
)
|
|
2460
2463
|
staticFiles[relative] = {
|
|
2461
2464
|
status: response.status,
|
|
2462
2465
|
headers: [...response.headers.entries()],
|
|
2463
2466
|
routeId: route.id,
|
|
2464
2467
|
...(revalidateSeconds !== undefined ? { revalidateSeconds } : {}),
|
|
2465
2468
|
...(rendered.tags.length > 0 ? { tags: rendered.tags } : {}),
|
|
2466
|
-
}
|
|
2469
|
+
}
|
|
2467
2470
|
}
|
|
2468
2471
|
|
|
2469
|
-
return staticFiles
|
|
2472
|
+
return staticFiles
|
|
2470
2473
|
}
|
|
2471
2474
|
|
|
2472
2475
|
function settleBeforeNextTask<T>(value: Promise<T>): Promise<T | undefined> {
|
|
2473
2476
|
return new Promise((resolve, reject) => {
|
|
2474
|
-
let settled = false
|
|
2477
|
+
let settled = false
|
|
2475
2478
|
const timer = setTimeout(() => {
|
|
2476
|
-
if (!settled) resolve(undefined)
|
|
2477
|
-
}, 0)
|
|
2479
|
+
if (!settled) resolve(undefined)
|
|
2480
|
+
}, 0)
|
|
2478
2481
|
void value.then(
|
|
2479
2482
|
result => {
|
|
2480
|
-
settled = true
|
|
2481
|
-
clearTimeout(timer)
|
|
2482
|
-
resolve(result)
|
|
2483
|
+
settled = true
|
|
2484
|
+
clearTimeout(timer)
|
|
2485
|
+
resolve(result)
|
|
2483
2486
|
},
|
|
2484
2487
|
(error: unknown) => {
|
|
2485
|
-
settled = true
|
|
2486
|
-
clearTimeout(timer)
|
|
2487
|
-
reject(error instanceof Error ? error : new Error(String(error)))
|
|
2488
|
+
settled = true
|
|
2489
|
+
clearTimeout(timer)
|
|
2490
|
+
reject(error instanceof Error ? error : new Error(String(error)))
|
|
2488
2491
|
},
|
|
2489
|
-
)
|
|
2490
|
-
})
|
|
2492
|
+
)
|
|
2493
|
+
})
|
|
2491
2494
|
}
|
|
2492
2495
|
|
|
2493
2496
|
async function handlerHasEntirelyCachedIo(route: RouteManifestEntry): Promise<boolean> {
|
|
2494
2497
|
// Synthetic slot routes carry a phantom `page.tsx` anchor that never exists
|
|
2495
2498
|
// on disk — source sniffing must skip them or the build ENOENTs.
|
|
2496
|
-
if (route.synthetic || !existsSync(route.file)) return false
|
|
2497
|
-
const source = await readText(route.file)
|
|
2499
|
+
if (route.synthetic || !existsSync(route.file)) return false
|
|
2500
|
+
const source = await readText(route.file)
|
|
2498
2501
|
// A metadata route handler exports a `default` function rather than `GET`; pnext wraps it into the
|
|
2499
2502
|
// handler internally. When that default is a top-level `use cache` function every read inside it is
|
|
2500
2503
|
// cached, so the whole route is prerenderable. Without this the GET sniff below returns false and
|
|
2501
2504
|
// the route stays runtime-only.
|
|
2502
|
-
if (route.metadataRoute && metadataDefaultUsesCache(source)) return true
|
|
2503
|
-
const getStart = source.search(/\bexport\s+(?:async\s+)?function\s+GET\s*\(/)
|
|
2504
|
-
if (getStart === -1) return false
|
|
2505
|
-
const afterGet = source.slice(getStart)
|
|
2506
|
-
const nextDeclaration = afterGet.slice(1).search(/^(?:async\s+)?(?:function|const|let)\s+/m)
|
|
2507
|
-
const body = nextDeclaration === -1 ? afterGet : afterGet.slice(0, nextDeclaration + 1)
|
|
2505
|
+
if (route.metadataRoute && metadataDefaultUsesCache(source)) return true
|
|
2506
|
+
const getStart = source.search(/\bexport\s+(?:async\s+)?function\s+GET\s*\(/)
|
|
2507
|
+
if (getStart === -1) return false
|
|
2508
|
+
const afterGet = source.slice(getStart)
|
|
2509
|
+
const nextDeclaration = afterGet.slice(1).search(/^(?:async\s+)?(?:function|const|let)\s+/m)
|
|
2510
|
+
const body = nextDeclaration === -1 ? afterGet : afterGet.slice(0, nextDeclaration + 1)
|
|
2508
2511
|
// Reading the wall clock straight in the GET body - outside any `use cache` scope - is dynamic
|
|
2509
2512
|
// under cacheComponents: Next taints such a route as dynamic and never prerenders it. Prebuilding
|
|
2510
2513
|
// it would serve a build-time body that is already stale by the first request, and the background
|
|
2511
2514
|
// regen would race a subsequent read. Empty-parens only: `new Date(ms)` is deterministic.
|
|
2512
2515
|
if (/\bnew\s+Date\s*\(\s*\)/.test(body) || /\bDate\s*\.\s*now\s*\(/.test(body)) {
|
|
2513
|
-
return false
|
|
2516
|
+
return false
|
|
2514
2517
|
}
|
|
2515
|
-
const awaited = [...body.matchAll(/\bawait\s+([A-Za-z_$][\w$]*)\s*\(/g)].map(
|
|
2516
|
-
|
|
2517
|
-
);
|
|
2518
|
-
if (awaited.length === 0) return false;
|
|
2518
|
+
const awaited = [...body.matchAll(/\bawait\s+([A-Za-z_$][\w$]*)\s*\(/g)].map(match => match[1]!)
|
|
2519
|
+
if (awaited.length === 0) return false
|
|
2519
2520
|
|
|
2520
2521
|
// `await import(...)` is module evaluation, not request IO: a module that top-level-awaits still
|
|
2521
2522
|
// resolves the same for every request, so the route stays prerenderable. It only settles in a later
|
|
2522
2523
|
// task the FIRST time, which is exactly the build's prerender, so the awaited-call check forgives it.
|
|
2523
|
-
const cached = new Set<string>(['import'])
|
|
2524
|
-
for (const match of source.matchAll(
|
|
2525
|
-
|
|
2524
|
+
const cached = new Set<string>(['import'])
|
|
2525
|
+
for (const match of source.matchAll(
|
|
2526
|
+
/\bconst\s+([A-Za-z_$][\w$]*)\s*=\s*([A-Za-z_$][\w$]*)\s*\(/g,
|
|
2527
|
+
)) {
|
|
2528
|
+
if (match[2] === 'cache' || match[2] === 'unstable_cache') cached.add(match[1]!)
|
|
2526
2529
|
}
|
|
2527
2530
|
for (const match of source.matchAll(
|
|
2528
2531
|
/\b(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*\{\s*['"]use cache['"]/g,
|
|
2529
2532
|
)) {
|
|
2530
|
-
cached.add(match[1]!)
|
|
2533
|
+
cached.add(match[1]!)
|
|
2531
2534
|
}
|
|
2532
2535
|
for (const name of awaited) {
|
|
2533
|
-
const start = source.search(
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
const
|
|
2538
|
-
|
|
2536
|
+
const start = source.search(
|
|
2537
|
+
new RegExp(`\\b(?:const\\s+${name}\\s*=|function\\s+${name}\\s*\\()`),
|
|
2538
|
+
)
|
|
2539
|
+
if (start === -1) continue
|
|
2540
|
+
const tail = source.slice(start)
|
|
2541
|
+
const next = tail.slice(1).search(/^(?:async\s+)?(?:function|const|let)\s+/m)
|
|
2542
|
+
const declaration = next === -1 ? tail : tail.slice(0, next + 1)
|
|
2543
|
+
if (/\bcache\s*:\s*['"]force-cache['"]/.test(declaration)) cached.add(name)
|
|
2539
2544
|
}
|
|
2540
|
-
return awaited.every(name => cached.has(name))
|
|
2545
|
+
return awaited.every(name => cached.has(name))
|
|
2541
2546
|
}
|
|
2542
2547
|
|
|
2543
2548
|
/**
|
|
@@ -2548,54 +2553,51 @@ async function handlerHasEntirelyCachedIo(route: RouteManifestEntry): Promise<bo
|
|
|
2548
2553
|
function metadataDefaultUsesCache(source: string): boolean {
|
|
2549
2554
|
return /\bexport\s+default\s+(?:async\s+)?function\b[^(]*\([^)]*\)\s*(?::[^{]*)?\{\s*(?:\/\/[^\n]*\n\s*|\/\*[\s\S]*?\*\/\s*)*['"]use cache['"]/.test(
|
|
2550
2555
|
source,
|
|
2551
|
-
)
|
|
2556
|
+
)
|
|
2552
2557
|
}
|
|
2553
2558
|
|
|
2554
2559
|
async function usesUnstableCache(route: RouteManifestEntry): Promise<boolean> {
|
|
2555
|
-
if (route.synthetic || !existsSync(route.file)) return false
|
|
2556
|
-
const source = await readText(route.file)
|
|
2557
|
-
const localNames: string[] = []
|
|
2558
|
-
const dynamicNames: string[] = []
|
|
2560
|
+
if (route.synthetic || !existsSync(route.file)) return false
|
|
2561
|
+
const source = await readText(route.file)
|
|
2562
|
+
const localNames: string[] = []
|
|
2563
|
+
const dynamicNames: string[] = []
|
|
2559
2564
|
for (const match of source.matchAll(
|
|
2560
2565
|
/\bimport\s*\{([^}]*)\}\s*from\s*['"]next\/(cache|headers|server)(?:\.js)?['"]/g,
|
|
2561
2566
|
)) {
|
|
2562
2567
|
for (const item of match[1]!.split(',')) {
|
|
2563
|
-
const [imported, local] = item.trim().split(/\s+as\s+/)
|
|
2568
|
+
const [imported, local] = item.trim().split(/\s+as\s+/)
|
|
2564
2569
|
if (match[2] === 'cache' && imported === 'unstable_cache') {
|
|
2565
|
-
localNames.push(local ?? imported)
|
|
2570
|
+
localNames.push(local ?? imported)
|
|
2566
2571
|
} else if (
|
|
2567
2572
|
(match[2] === 'headers' && (imported === 'cookies' || imported === 'headers')) ||
|
|
2568
2573
|
(match[2] === 'server' && imported === 'connection')
|
|
2569
2574
|
) {
|
|
2570
|
-
dynamicNames.push(local ?? imported)
|
|
2575
|
+
dynamicNames.push(local ?? imported)
|
|
2571
2576
|
}
|
|
2572
2577
|
}
|
|
2573
2578
|
}
|
|
2574
|
-
if (dynamicNames.length === 0) return false
|
|
2575
|
-
const dynamicCall = dynamicNames.join('|')
|
|
2579
|
+
if (dynamicNames.length === 0) return false
|
|
2580
|
+
const dynamicCall = dynamicNames.join('|')
|
|
2576
2581
|
return localNames.some(name =>
|
|
2577
2582
|
new RegExp(
|
|
2578
2583
|
`\\b${name}\\s*\\(\\s*(?:async\\s*)?(?:\\([^)]*\\)|[A-Za-z_$][\\w$]*)?\\s*=>[\\s\\S]{0,1000}?\\b(?:${dynamicCall})\\s*\\(`,
|
|
2579
2584
|
).test(source),
|
|
2580
|
-
)
|
|
2585
|
+
)
|
|
2581
2586
|
}
|
|
2582
2587
|
|
|
2583
2588
|
async function staticHandlerDynamicUsage(route: RouteManifestEntry): Promise<string | undefined> {
|
|
2584
|
-
if (!route.usesRequest) return undefined
|
|
2585
|
-
if (
|
|
2586
|
-
|
|
2587
|
-
route.segmentConfig?.dynamic === 'error'
|
|
2588
|
-
) {
|
|
2589
|
-
return undefined;
|
|
2589
|
+
if (!route.usesRequest) return undefined
|
|
2590
|
+
if (route.segmentConfig?.dynamic === 'force-static' || route.segmentConfig?.dynamic === 'error') {
|
|
2591
|
+
return undefined
|
|
2590
2592
|
}
|
|
2591
|
-
if (route.synthetic || !existsSync(route.file)) return undefined
|
|
2592
|
-
const source = await readText(route.file)
|
|
2593
|
-
if (/\b(?:request|req)\.url\b/.test(source)) return 'request.url'
|
|
2594
|
-
if (/\bnextUrl\s*\.\s*toString\s*\(/.test(source)) return 'nextUrl.toString'
|
|
2595
|
-
if (/\bheaders\s*\(/.test(source)) return 'headers()'
|
|
2596
|
-
if (/\bcookies\s*\(/.test(source)) return 'cookies()'
|
|
2597
|
-
if (/\bconnection\s*\(/.test(source)) return 'connection()'
|
|
2598
|
-
return undefined
|
|
2593
|
+
if (route.synthetic || !existsSync(route.file)) return undefined
|
|
2594
|
+
const source = await readText(route.file)
|
|
2595
|
+
if (/\b(?:request|req)\.url\b/.test(source)) return 'request.url'
|
|
2596
|
+
if (/\bnextUrl\s*\.\s*toString\s*\(/.test(source)) return 'nextUrl.toString'
|
|
2597
|
+
if (/\bheaders\s*\(/.test(source)) return 'headers()'
|
|
2598
|
+
if (/\bcookies\s*\(/.test(source)) return 'cookies()'
|
|
2599
|
+
if (/\bconnection\s*\(/.test(source)) return 'connection()'
|
|
2600
|
+
return undefined
|
|
2599
2601
|
}
|
|
2600
2602
|
|
|
2601
2603
|
/**
|
|
@@ -2605,55 +2607,55 @@ async function staticHandlerDynamicUsage(route: RouteManifestEntry): Promise<str
|
|
|
2605
2607
|
* cleanly. The ancestor keeps its physical file; descendants skip theirs.
|
|
2606
2608
|
*/
|
|
2607
2609
|
function collectDescendantHandlerIds(routes: RouteManifestEntry[]): Set<string> {
|
|
2608
|
-
const ids = new Set<string>()
|
|
2609
|
-
const paths = routes.map(route => ({ route, norm: (route.route || '/').replace(/\/+$/, '') }))
|
|
2610
|
+
const ids = new Set<string>()
|
|
2611
|
+
const paths = routes.map(route => ({ route, norm: (route.route || '/').replace(/\/+$/, '') }))
|
|
2610
2612
|
for (const { route, norm } of paths) {
|
|
2611
|
-
if (route.kind !== 'handler') continue
|
|
2613
|
+
if (route.kind !== 'handler') continue
|
|
2612
2614
|
const hasAncestor = paths.some(
|
|
2613
2615
|
other =>
|
|
2614
2616
|
other.route !== route &&
|
|
2615
2617
|
other.norm.length > 0 &&
|
|
2616
2618
|
norm !== other.norm &&
|
|
2617
2619
|
norm.startsWith(`${other.norm}/`),
|
|
2618
|
-
)
|
|
2619
|
-
if (hasAncestor) ids.add(route.id)
|
|
2620
|
+
)
|
|
2621
|
+
if (hasAncestor) ids.add(route.id)
|
|
2620
2622
|
}
|
|
2621
|
-
return ids
|
|
2623
|
+
return ids
|
|
2622
2624
|
}
|
|
2623
2625
|
|
|
2624
2626
|
async function staticRouteHandlerCandidate(route: RouteManifestEntry) {
|
|
2625
|
-
const config = route.segmentConfig
|
|
2626
|
-
if (config?.dynamic === 'force-dynamic') return false
|
|
2627
|
-
if (config?.revalidate === 0) return false
|
|
2628
|
-
if (config?.fetchCache === 'force-no-store') return false
|
|
2627
|
+
const config = route.segmentConfig
|
|
2628
|
+
if (config?.dynamic === 'force-dynamic') return false
|
|
2629
|
+
if (config?.revalidate === 0) return false
|
|
2630
|
+
if (config?.fetchCache === 'force-no-store') return false
|
|
2629
2631
|
// A handler that calls revalidatePath/revalidateTag performs a side effect
|
|
2630
2632
|
// per request; it can never be prerendered (Next keeps it dynamic even with a
|
|
2631
2633
|
// `revalidate` export), so serving a cached body would drop the revalidation.
|
|
2632
|
-
if (route.handlerUsesRevalidationApi) return false
|
|
2633
|
-
if (route.metadataRoute) return !route.usesRequest
|
|
2634
|
+
if (route.handlerUsesRevalidationApi) return false
|
|
2635
|
+
if (route.metadataRoute) return !route.usesRequest
|
|
2634
2636
|
// `dynamic = 'error'` deliberately exercises a static render so request API
|
|
2635
2637
|
// reads can fail at their call sites instead of turning the handler dynamic.
|
|
2636
|
-
if (config?.dynamic === 'error') return true
|
|
2637
|
-
if (config?.dynamic === 'force-static') return true
|
|
2638
|
+
if (config?.dynamic === 'error') return true
|
|
2639
|
+
if (config?.dynamic === 'force-static') return true
|
|
2638
2640
|
// A `revalidate` export or `generateStaticParams` opts a handler into static
|
|
2639
2641
|
// (ISR) output even if it reads request data — Next prerenders it with a
|
|
2640
2642
|
// canonical request. Truly dynamic access (cookies/headers) would surface at
|
|
2641
2643
|
// runtime, but reading e.g. `req.nextUrl.pathname` is prerender-safe.
|
|
2642
|
-
if (staticRouteHandlerExplicitlyCached(route) || route.hasStaticParams) return true
|
|
2643
|
-
if (route.usesRequest) return false
|
|
2644
|
+
if (staticRouteHandlerExplicitlyCached(route) || route.hasStaticParams) return true
|
|
2645
|
+
if (route.usesRequest) return false
|
|
2644
2646
|
// Since Next 15, GET route handlers are dynamic by default: only an explicit opt-in prerenders one.
|
|
2645
2647
|
// Under cacheComponents every request-independent handler is a prerender candidate again, EXCEPT
|
|
2646
2648
|
// one that performs uncached IO (time/randomness outside any `use cache` scope): baking it would
|
|
2647
2649
|
// freeze `new Date()` at build time while its `use cache` parts must keep live SWR semantics.
|
|
2648
|
-
if (!cacheComponents() || route.mode !== 'static') return false
|
|
2649
|
-
if (!handlerUsesUncachedIo(route)) return true
|
|
2650
|
+
if (!cacheComponents() || route.mode !== 'static') return false
|
|
2651
|
+
if (!handlerUsesUncachedIo(route)) return true
|
|
2650
2652
|
// The uncached-IO sniff is textual, so it also trips on time/randomness that
|
|
2651
2653
|
// sits inside a helper the handler only ever reaches through an
|
|
2652
2654
|
// `unstable_cache()` wrapper (cache-components routes `/routes/io-cached`).
|
|
2653
2655
|
// The call-graph check is authoritative there: when EVERY awaited call in GET
|
|
2654
2656
|
// resolves to a cached wrapper the IO is cached after all. A handler that
|
|
2655
2657
|
// also awaits the raw helper (`/routes/io-mixed`) still fails it.
|
|
2656
|
-
return handlerHasEntirelyCachedIo(route)
|
|
2658
|
+
return handlerHasEntirelyCachedIo(route)
|
|
2657
2659
|
}
|
|
2658
2660
|
|
|
2659
2661
|
/**
|
|
@@ -2663,83 +2665,81 @@ async function staticRouteHandlerCandidate(route: RouteManifestEntry) {
|
|
|
2663
2665
|
* cacheComponents, so it must render per-request.
|
|
2664
2666
|
*/
|
|
2665
2667
|
function handlerUsesUncachedIo(route: RouteManifestEntry): boolean {
|
|
2666
|
-
const source = readTextSync(route.file)
|
|
2667
|
-
if (!source) return false
|
|
2668
|
-
return /\bnew Date\s*\(|\bDate\.now\s*\(|\bMath\.random\s*\(/.test(
|
|
2669
|
-
stripUseCacheBodies(source),
|
|
2670
|
-
);
|
|
2668
|
+
const source = readTextSync(route.file)
|
|
2669
|
+
if (!source) return false
|
|
2670
|
+
return /\bnew Date\s*\(|\bDate\.now\s*\(|\bMath\.random\s*\(/.test(stripUseCacheBodies(source))
|
|
2671
2671
|
}
|
|
2672
2672
|
|
|
2673
2673
|
/** Blank out every function body whose prologue opens with a 'use cache' directive. */
|
|
2674
2674
|
function stripUseCacheBodies(source: string): string {
|
|
2675
|
-
let out = source
|
|
2675
|
+
let out = source
|
|
2676
2676
|
for (;;) {
|
|
2677
|
-
const directive = /\{\s*(['"])use cache(?:\s*:\s*[\w-]+)?\1\s*;?/.exec(out)
|
|
2678
|
-
if (!directive) break
|
|
2679
|
-
const bodyOpen = directive.index + 1
|
|
2680
|
-
let balance = 1
|
|
2681
|
-
let end = out.length
|
|
2677
|
+
const directive = /\{\s*(['"])use cache(?:\s*:\s*[\w-]+)?\1\s*;?/.exec(out)
|
|
2678
|
+
if (!directive) break
|
|
2679
|
+
const bodyOpen = directive.index + 1
|
|
2680
|
+
let balance = 1
|
|
2681
|
+
let end = out.length
|
|
2682
2682
|
for (let i = bodyOpen; i < out.length; i += 1) {
|
|
2683
|
-
const ch = out[i]
|
|
2684
|
-
if (ch === '{') balance += 1
|
|
2683
|
+
const ch = out[i]
|
|
2684
|
+
if (ch === '{') balance += 1
|
|
2685
2685
|
else if (ch === '}') {
|
|
2686
|
-
balance -= 1
|
|
2686
|
+
balance -= 1
|
|
2687
2687
|
if (balance === 0) {
|
|
2688
|
-
end = i
|
|
2689
|
-
break
|
|
2688
|
+
end = i
|
|
2689
|
+
break
|
|
2690
2690
|
}
|
|
2691
2691
|
}
|
|
2692
2692
|
}
|
|
2693
|
-
out = `${out.slice(0, bodyOpen)}${out.slice(end)}
|
|
2693
|
+
out = `${out.slice(0, bodyOpen)}${out.slice(end)}`
|
|
2694
2694
|
}
|
|
2695
|
-
return out
|
|
2695
|
+
return out
|
|
2696
2696
|
}
|
|
2697
2697
|
|
|
2698
2698
|
function staticRouteHandlerExplicitlyCached(route: RouteManifestEntry) {
|
|
2699
|
-
const config = route.segmentConfig
|
|
2699
|
+
const config = route.segmentConfig
|
|
2700
2700
|
return (
|
|
2701
2701
|
config?.dynamic === 'force-static' ||
|
|
2702
2702
|
config?.revalidate === false ||
|
|
2703
2703
|
(typeof config?.revalidate === 'number' && config.revalidate > 0)
|
|
2704
|
-
)
|
|
2704
|
+
)
|
|
2705
2705
|
}
|
|
2706
2706
|
|
|
2707
2707
|
async function needsRequestOriginForMetadataImages(appPath: string, route: RouteManifestEntry) {
|
|
2708
|
-
if (!(await routeNullsMetadataBase(route))) return false
|
|
2709
|
-
return routeMetadataImageFiles(appPath, route.file)
|
|
2708
|
+
if (!(await routeNullsMetadataBase(route))) return false
|
|
2709
|
+
return routeMetadataImageFiles(appPath, route.file)
|
|
2710
2710
|
}
|
|
2711
2711
|
|
|
2712
2712
|
async function routeNullsMetadataBase(route: RouteManifestEntry) {
|
|
2713
|
-
const sourceFiles = route.sourceFiles.filter(file => /\.(tsx?|jsx?|mjs|cjs)$/.test(file))
|
|
2713
|
+
const sourceFiles = route.sourceFiles.filter(file => /\.(tsx?|jsx?|mjs|cjs)$/.test(file))
|
|
2714
2714
|
for (const file of sourceFiles) {
|
|
2715
2715
|
try {
|
|
2716
2716
|
if (/\bmetadataBase\s*:\s*null\b|\bmetadataBase\s*=\s*null\b/.test(await readText(file))) {
|
|
2717
|
-
return true
|
|
2717
|
+
return true
|
|
2718
2718
|
}
|
|
2719
2719
|
} catch {
|
|
2720
2720
|
// Ignore generated/virtual paths that are not readable at build time.
|
|
2721
2721
|
}
|
|
2722
2722
|
}
|
|
2723
|
-
return false
|
|
2723
|
+
return false
|
|
2724
2724
|
}
|
|
2725
2725
|
|
|
2726
2726
|
function routeMetadataImageFiles(appPath: string, routeFile: string) {
|
|
2727
|
-
let dir = path.dirname(routeFile)
|
|
2728
|
-
const root = path.resolve(appPath)
|
|
2727
|
+
let dir = path.dirname(routeFile)
|
|
2728
|
+
const root = path.resolve(appPath)
|
|
2729
2729
|
while (dir.startsWith(root)) {
|
|
2730
2730
|
if (existsSync(dir)) {
|
|
2731
2731
|
for (const entry of readdirSync(dir)) {
|
|
2732
2732
|
if (
|
|
2733
2733
|
/^(opengraph-image|twitter-image)\d*\.(tsx?|jsx?|mjs|png|jpe?g|gif|webp|svg)$/.test(entry)
|
|
2734
2734
|
) {
|
|
2735
|
-
return true
|
|
2735
|
+
return true
|
|
2736
2736
|
}
|
|
2737
2737
|
}
|
|
2738
2738
|
}
|
|
2739
|
-
if (dir === root) break
|
|
2740
|
-
dir = path.dirname(dir)
|
|
2739
|
+
if (dir === root) break
|
|
2740
|
+
dir = path.dirname(dir)
|
|
2741
2741
|
}
|
|
2742
|
-
return false
|
|
2742
|
+
return false
|
|
2743
2743
|
}
|
|
2744
2744
|
|
|
2745
2745
|
async function copyStaticMetadataFiles(
|
|
@@ -2748,32 +2748,32 @@ async function copyStaticMetadataFiles(
|
|
|
2748
2748
|
metadataFiles = discoverStaticMetadataFiles(config.appPath),
|
|
2749
2749
|
) {
|
|
2750
2750
|
for (const metadataFile of metadataFiles) {
|
|
2751
|
-
const target = staticMetadataOutputFile(config.outPath, metadataFile)
|
|
2752
|
-
const collision = staticOutputCollision(config.outPath, target)
|
|
2751
|
+
const target = staticMetadataOutputFile(config.outPath, metadataFile)
|
|
2752
|
+
const collision = staticOutputCollision(config.outPath, target)
|
|
2753
2753
|
if (collision) {
|
|
2754
|
-
warnSkippedStatic(`/${metadataFile.outputPath}`, collision)
|
|
2755
|
-
continue
|
|
2754
|
+
warnSkippedStatic(`/${metadataFile.outputPath}`, collision)
|
|
2755
|
+
continue
|
|
2756
2756
|
}
|
|
2757
|
-
await mkdir(path.dirname(target), { recursive: true })
|
|
2758
|
-
await copyFile(metadataFile.file, target)
|
|
2757
|
+
await mkdir(path.dirname(target), { recursive: true })
|
|
2758
|
+
await copyFile(metadataFile.file, target)
|
|
2759
2759
|
staticFiles[metadataFile.outputPath] = {
|
|
2760
2760
|
status: 200,
|
|
2761
2761
|
headers: [
|
|
2762
2762
|
['content-type', metadataFile.contentType],
|
|
2763
2763
|
['cache-control', staticMetadataCacheControl],
|
|
2764
2764
|
],
|
|
2765
|
-
}
|
|
2765
|
+
}
|
|
2766
2766
|
}
|
|
2767
2767
|
}
|
|
2768
2768
|
|
|
2769
2769
|
export function staticHtmlPath(outPath: string, routePath: string, flatLayout = false) {
|
|
2770
|
-
if (routePath === '/') return path.join(outPath, 'public', 'index.html')
|
|
2771
|
-
const normalized = routePath.replace(/^\/|\/$/g, '')
|
|
2770
|
+
if (routePath === '/') return path.join(outPath, 'public', 'index.html')
|
|
2771
|
+
const normalized = routePath.replace(/^\/|\/$/g, '')
|
|
2772
2772
|
// output:'export' with trailingSlash:false lays pages out flat (`/a.html`);
|
|
2773
2773
|
// otherwise (and always for trailingSlash:true) each page gets its own dir
|
|
2774
2774
|
// (`/a/index.html`).
|
|
2775
|
-
if (flatLayout) return safePublicPath(outPath, `${normalized}.html`)
|
|
2776
|
-
return safePublicPath(outPath, normalized, 'index.html')
|
|
2775
|
+
if (flatLayout) return safePublicPath(outPath, `${normalized}.html`)
|
|
2776
|
+
return safePublicPath(outPath, normalized, 'index.html')
|
|
2777
2777
|
}
|
|
2778
2778
|
|
|
2779
2779
|
// Percent-encode generateStaticParams values the way Next encodes them into
|
|
@@ -2787,7 +2787,7 @@ function encodeStaticRouteParams(
|
|
|
2787
2787
|
key,
|
|
2788
2788
|
Array.isArray(value) ? value.map(encodeURIComponent) : encodeURIComponent(value),
|
|
2789
2789
|
]),
|
|
2790
|
-
)
|
|
2790
|
+
)
|
|
2791
2791
|
}
|
|
2792
2792
|
|
|
2793
2793
|
function fillRoutePath(
|
|
@@ -2795,51 +2795,51 @@ function fillRoutePath(
|
|
|
2795
2795
|
params: Record<string, RouteParamValue>,
|
|
2796
2796
|
generatedParam?: string,
|
|
2797
2797
|
) {
|
|
2798
|
-
let value = route
|
|
2798
|
+
let value = route
|
|
2799
2799
|
for (const [key, param] of Object.entries(params)) {
|
|
2800
|
-
const segment = Array.isArray(param) ? param.join('/') : param
|
|
2801
|
-
value = value.replace(`:${key}*`, segment).replace(`:${key}`, segment)
|
|
2800
|
+
const segment = Array.isArray(param) ? param.join('/') : param
|
|
2801
|
+
value = value.replace(`:${key}*`, segment).replace(`:${key}`, segment)
|
|
2802
2802
|
}
|
|
2803
2803
|
if (generatedParam && params[generatedParam] !== undefined) {
|
|
2804
|
-
const param = params[generatedParam]
|
|
2805
|
-
const segment = Array.isArray(param) ? param.join('/') : param
|
|
2806
|
-
value = value.replace(':id', segment)
|
|
2804
|
+
const param = params[generatedParam]
|
|
2805
|
+
const segment = Array.isArray(param) ? param.join('/') : param
|
|
2806
|
+
value = value.replace(':id', segment)
|
|
2807
2807
|
}
|
|
2808
|
-
return value
|
|
2808
|
+
return value
|
|
2809
2809
|
}
|
|
2810
2810
|
|
|
2811
2811
|
async function runWithNextBuildPhase<T>(task: () => Promise<T>): Promise<T> {
|
|
2812
2812
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
2813
|
-
const previous = process.env.NEXT_PHASE
|
|
2813
|
+
const previous = process.env.NEXT_PHASE
|
|
2814
2814
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
2815
|
-
process.env.NEXT_PHASE = 'phase-production-build'
|
|
2815
|
+
process.env.NEXT_PHASE = 'phase-production-build'
|
|
2816
2816
|
try {
|
|
2817
|
-
return await task()
|
|
2817
|
+
return await task()
|
|
2818
2818
|
} finally {
|
|
2819
2819
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
2820
|
-
if (previous === undefined) delete process.env.NEXT_PHASE
|
|
2820
|
+
if (previous === undefined) delete process.env.NEXT_PHASE
|
|
2821
2821
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
2822
|
-
else process.env.NEXT_PHASE = previous
|
|
2822
|
+
else process.env.NEXT_PHASE = previous
|
|
2823
2823
|
}
|
|
2824
2824
|
}
|
|
2825
2825
|
|
|
2826
2826
|
/**
|
|
2827
|
-
* The `use cache` SWR response headers for a stashed cacheLife, mirroring the register-
|
|
2827
|
+
* The `use cache` SWR response headers for a stashed cacheLife, mirroring the register/use-cache.ts
|
|
2828
2828
|
* finalizer. Baked into the manifest so a pure static HIT can re-emit them from start.ts - the
|
|
2829
2829
|
* finalizer only fires on a live render. Returns [] when no cacheLife applies.
|
|
2830
2830
|
*/
|
|
2831
2831
|
function cacheLifeResponseHeaders(life: CacheLifeStash | undefined): [string, string][] {
|
|
2832
|
-
if (!life) return []
|
|
2833
|
-
const headers: [string, string][] = []
|
|
2834
|
-
const { revalidateSeconds, expireSeconds, staleSeconds } = life
|
|
2832
|
+
if (!life) return []
|
|
2833
|
+
const headers: [string, string][] = []
|
|
2834
|
+
const { revalidateSeconds, expireSeconds, staleSeconds } = life
|
|
2835
2835
|
if (revalidateSeconds !== undefined && expireSeconds !== undefined) {
|
|
2836
|
-
const swr = Math.max(0, expireSeconds - revalidateSeconds)
|
|
2837
|
-
headers.push(['cache-control', `s-maxage=${revalidateSeconds}, stale-while-revalidate=${swr}`])
|
|
2836
|
+
const swr = Math.max(0, expireSeconds - revalidateSeconds)
|
|
2837
|
+
headers.push(['cache-control', `s-maxage=${revalidateSeconds}, stale-while-revalidate=${swr}`])
|
|
2838
2838
|
}
|
|
2839
2839
|
if (staleSeconds !== undefined) {
|
|
2840
|
-
headers.push(['x-nextjs-stale-time', String(staleSeconds)])
|
|
2840
|
+
headers.push(['x-nextjs-stale-time', String(staleSeconds)])
|
|
2841
2841
|
}
|
|
2842
|
-
return headers
|
|
2842
|
+
return headers
|
|
2843
2843
|
}
|
|
2844
2844
|
|
|
2845
2845
|
/** Effective ISR TTL: the lowest of the segment `revalidate` export and any data-cache revalidate used during the render. */
|
|
@@ -2850,13 +2850,13 @@ function combineRevalidate(
|
|
|
2850
2850
|
const candidates = [
|
|
2851
2851
|
...(typeof segmentRevalidate === 'number' && segmentRevalidate > 0 ? [segmentRevalidate] : []),
|
|
2852
2852
|
...(collected !== undefined && collected > 0 ? [collected] : []),
|
|
2853
|
-
]
|
|
2854
|
-
if (candidates.length === 0) return undefined
|
|
2855
|
-
return Math.min(...candidates)
|
|
2853
|
+
]
|
|
2854
|
+
if (candidates.length === 0) return undefined
|
|
2855
|
+
return Math.min(...candidates)
|
|
2856
2856
|
}
|
|
2857
2857
|
|
|
2858
2858
|
function staticRouteHandlerPath(outPath: string, routePath: string) {
|
|
2859
|
-
return safePublicPath(outPath, routePath.replace(/^\/+/, '') || 'index')
|
|
2859
|
+
return safePublicPath(outPath, routePath.replace(/^\/+/, '') || 'index')
|
|
2860
2860
|
}
|
|
2861
2861
|
|
|
2862
2862
|
/**
|
|
@@ -2866,21 +2866,21 @@ function staticRouteHandlerPath(outPath: string, routePath: string) {
|
|
|
2866
2866
|
* route serves dynamically (start's static lookup only matches real files, then falls through).
|
|
2867
2867
|
*/
|
|
2868
2868
|
function staticOutputCollision(outPath: string, file: string): string | null {
|
|
2869
|
-
const publicPath = path.join(outPath, 'public')
|
|
2869
|
+
const publicPath = path.join(outPath, 'public')
|
|
2870
2870
|
if (existsSync(file) && statSync(file).isDirectory()) {
|
|
2871
|
-
return `${toPosixPath(path.relative(publicPath, file))} already exists as a directory
|
|
2871
|
+
return `${toPosixPath(path.relative(publicPath, file))} already exists as a directory`
|
|
2872
2872
|
}
|
|
2873
|
-
let dir = path.dirname(file)
|
|
2873
|
+
let dir = path.dirname(file)
|
|
2874
2874
|
while (dir !== publicPath && dir !== path.dirname(dir)) {
|
|
2875
2875
|
if (existsSync(dir)) {
|
|
2876
2876
|
if (!statSync(dir).isDirectory()) {
|
|
2877
|
-
return `${toPosixPath(path.relative(publicPath, dir))} already exists as a file
|
|
2877
|
+
return `${toPosixPath(path.relative(publicPath, dir))} already exists as a file`
|
|
2878
2878
|
}
|
|
2879
|
-
break
|
|
2879
|
+
break
|
|
2880
2880
|
}
|
|
2881
|
-
dir = path.dirname(dir)
|
|
2881
|
+
dir = path.dirname(dir)
|
|
2882
2882
|
}
|
|
2883
|
-
return null
|
|
2883
|
+
return null
|
|
2884
2884
|
}
|
|
2885
2885
|
|
|
2886
2886
|
/**
|
|
@@ -2896,7 +2896,7 @@ function rethrowIfProgrammingError(error: unknown): void {
|
|
|
2896
2896
|
error instanceof SyntaxError ||
|
|
2897
2897
|
(error instanceof TypeError && isBuildInternalStack(error))
|
|
2898
2898
|
) {
|
|
2899
|
-
throw error
|
|
2899
|
+
throw error
|
|
2900
2900
|
}
|
|
2901
2901
|
}
|
|
2902
2902
|
|
|
@@ -2905,46 +2905,44 @@ function rethrowIfProgrammingError(error: unknown): void {
|
|
|
2905
2905
|
// internals rather than app code, so a genuine render-time `TypeError` still
|
|
2906
2906
|
// degrades to skip-and-warn.
|
|
2907
2907
|
function isBuildInternalStack(error: Error): boolean {
|
|
2908
|
-
const stack = error.stack ?? ''
|
|
2909
|
-
const firstFrame = stack.split('\n').find(line => /^\s*at\s/.test(line)) ?? ''
|
|
2910
|
-
return /packages[/\\]pnext[/\\]src[/\\]/.test(firstFrame)
|
|
2908
|
+
const stack = error.stack ?? ''
|
|
2909
|
+
const firstFrame = stack.split('\n').find(line => /^\s*at\s/.test(line)) ?? ''
|
|
2910
|
+
return /packages[/\\]pnext[/\\]src[/\\]/.test(firstFrame)
|
|
2911
2911
|
}
|
|
2912
2912
|
|
|
2913
2913
|
// Next's NEXT_DEBUG_BUILD static-bailout diagnostic, grepped verbatim by the
|
|
2914
2914
|
// e2e suites ("should output debug info for static bailouts").
|
|
2915
2915
|
function logStaticBailout(routePath: string, reason: string) {
|
|
2916
|
-
console.log(
|
|
2917
|
-
`Static generation failed due to dynamic usage on ${routePath}, reason: ${reason}`,
|
|
2918
|
-
);
|
|
2916
|
+
console.log(`Static generation failed due to dynamic usage on ${routePath}, reason: ${reason}`)
|
|
2919
2917
|
}
|
|
2920
2918
|
|
|
2921
2919
|
// Best-effort label for WHICH request API kept the route dynamic (Next names
|
|
2922
2920
|
// the first dynamic API hit during its build attempt, e.g. "headers").
|
|
2923
2921
|
async function requestApiBailoutReason(route: RouteManifestEntry): Promise<string> {
|
|
2924
2922
|
for (const file of ownSourceFiles(route)) {
|
|
2925
|
-
let source: string
|
|
2923
|
+
let source: string
|
|
2926
2924
|
try {
|
|
2927
|
-
source = await readText(file)
|
|
2925
|
+
source = await readText(file)
|
|
2928
2926
|
} catch {
|
|
2929
|
-
continue
|
|
2927
|
+
continue
|
|
2930
2928
|
}
|
|
2931
|
-
const match = /\b(headers|cookies|draftMode|connection)\s*\(/.exec(source)
|
|
2932
|
-
if (match?.[1]) return match[1]
|
|
2929
|
+
const match = /\b(headers|cookies|draftMode|connection)\s*\(/.exec(source)
|
|
2930
|
+
if (match?.[1]) return match[1]
|
|
2933
2931
|
}
|
|
2934
|
-
return 'dynamic usage'
|
|
2932
|
+
return 'dynamic usage'
|
|
2935
2933
|
}
|
|
2936
2934
|
|
|
2937
2935
|
// The route's own source files (the closure also carries framework/compat
|
|
2938
2936
|
// modules, whose bodies mention every request API and every scheduler).
|
|
2939
2937
|
function ownSourceFiles(route: RouteManifestEntry): string[] {
|
|
2940
|
-
const frameworkRoot = path.resolve(fileURLToPath(import.meta.url), '..', '..')
|
|
2938
|
+
const frameworkRoot = path.resolve(fileURLToPath(import.meta.url), '..', '..')
|
|
2941
2939
|
return route.sourceFiles.filter(file => {
|
|
2942
|
-
const resolved = path.resolve(file)
|
|
2940
|
+
const resolved = path.resolve(file)
|
|
2943
2941
|
return (
|
|
2944
2942
|
!resolved.includes(`${path.sep}node_modules${path.sep}`) &&
|
|
2945
2943
|
!resolved.startsWith(frameworkRoot + path.sep)
|
|
2946
|
-
)
|
|
2947
|
-
})
|
|
2944
|
+
)
|
|
2945
|
+
})
|
|
2948
2946
|
}
|
|
2949
2947
|
|
|
2950
2948
|
/**
|
|
@@ -2954,15 +2952,15 @@ function ownSourceFiles(route: RouteManifestEntry): string[] {
|
|
|
2954
2952
|
*/
|
|
2955
2953
|
async function schedulesDeferredWork(route: RouteManifestEntry): Promise<boolean> {
|
|
2956
2954
|
for (const file of ownSourceFiles(route)) {
|
|
2957
|
-
let source: string
|
|
2955
|
+
let source: string
|
|
2958
2956
|
try {
|
|
2959
|
-
source = await readText(file)
|
|
2957
|
+
source = await readText(file)
|
|
2960
2958
|
} catch {
|
|
2961
|
-
continue
|
|
2959
|
+
continue
|
|
2962
2960
|
}
|
|
2963
|
-
if (/\b(?:setTimeout|setImmediate|queueMicrotask)\s*\(/.test(source)) return true
|
|
2961
|
+
if (/\b(?:setTimeout|setImmediate|queueMicrotask)\s*\(/.test(source)) return true
|
|
2964
2962
|
}
|
|
2965
|
-
return false
|
|
2963
|
+
return false
|
|
2966
2964
|
}
|
|
2967
2965
|
|
|
2968
2966
|
/**
|
|
@@ -2971,33 +2969,33 @@ async function schedulesDeferredWork(route: RouteManifestEntry): Promise<boolean
|
|
|
2971
2969
|
* what surfaces is the genuine error - this only decides who catches it.
|
|
2972
2970
|
*/
|
|
2973
2971
|
function captureDeferredFailures(record: (error: unknown) => void) {
|
|
2974
|
-
const globals = globalThis as unknown as Record<string, (...args: unknown[]) => unknown
|
|
2975
|
-
const originals = new Map<string, (...args: unknown[]) => unknown>()
|
|
2972
|
+
const globals = globalThis as unknown as Record<string, (...args: unknown[]) => unknown>
|
|
2973
|
+
const originals = new Map<string, (...args: unknown[]) => unknown>()
|
|
2976
2974
|
for (const name of ['setTimeout', 'setImmediate', 'queueMicrotask']) {
|
|
2977
|
-
const original = globals[name]
|
|
2978
|
-
if (typeof original !== 'function') continue
|
|
2979
|
-
originals.set(name, original)
|
|
2975
|
+
const original = globals[name]
|
|
2976
|
+
if (typeof original !== 'function') continue
|
|
2977
|
+
originals.set(name, original)
|
|
2980
2978
|
globals[name] = function patched(this: unknown, callback: unknown, ...rest: unknown[]) {
|
|
2981
|
-
if (typeof callback !== 'function') return original.call(this, callback, ...rest)
|
|
2979
|
+
if (typeof callback !== 'function') return original.call(this, callback, ...rest)
|
|
2982
2980
|
const guarded = function guardedCallback(this: unknown, ...args: unknown[]) {
|
|
2983
2981
|
try {
|
|
2984
|
-
const value = (callback as (...inner: unknown[]) => unknown).apply(this, args)
|
|
2985
|
-
if (isThenable(value)) void value.then(undefined, record)
|
|
2986
|
-
return value
|
|
2982
|
+
const value = (callback as (...inner: unknown[]) => unknown).apply(this, args)
|
|
2983
|
+
if (isThenable(value)) void value.then(undefined, record)
|
|
2984
|
+
return value
|
|
2987
2985
|
} catch (error) {
|
|
2988
|
-
record(error)
|
|
2986
|
+
record(error)
|
|
2989
2987
|
}
|
|
2990
|
-
}
|
|
2991
|
-
return original.call(this, guarded, ...rest)
|
|
2992
|
-
}
|
|
2988
|
+
}
|
|
2989
|
+
return original.call(this, guarded, ...rest)
|
|
2990
|
+
}
|
|
2993
2991
|
}
|
|
2994
2992
|
return () => {
|
|
2995
|
-
for (const [name, original] of originals) globals[name] = original
|
|
2996
|
-
}
|
|
2993
|
+
for (const [name, original] of originals) globals[name] = original
|
|
2994
|
+
}
|
|
2997
2995
|
}
|
|
2998
2996
|
|
|
2999
2997
|
function isThenable(value: unknown): value is PromiseLike<unknown> {
|
|
3000
|
-
return typeof (value as { then?: unknown } | null)?.then === 'function'
|
|
2998
|
+
return typeof (value as { then?: unknown } | null)?.then === 'function'
|
|
3001
2999
|
}
|
|
3002
3000
|
|
|
3003
3001
|
/**
|
|
@@ -3012,10 +3010,10 @@ async function probeDeferredDynamicUsage(
|
|
|
3012
3010
|
staticMetadataFiles: StaticMetadataFile[],
|
|
3013
3011
|
staticModuleMetadata: Record<string, StaticModuleMetadata>,
|
|
3014
3012
|
): Promise<void> {
|
|
3015
|
-
const routePath = toNextRoutePattern(route.route || '/')
|
|
3016
|
-
const failures: unknown[] = []
|
|
3017
|
-
const restore = captureDeferredFailures(error => failures.push(error))
|
|
3018
|
-
beginDynamicBailoutProbe(routePath)
|
|
3013
|
+
const routePath = toNextRoutePattern(route.route || '/')
|
|
3014
|
+
const failures: unknown[] = []
|
|
3015
|
+
const restore = captureDeferredFailures(error => failures.push(error))
|
|
3016
|
+
beginDynamicBailoutProbe(routePath)
|
|
3019
3017
|
try {
|
|
3020
3018
|
await runWithWorkUnit('render', () =>
|
|
3021
3019
|
getRenderExtensions().collectRenderMeta(
|
|
@@ -3034,37 +3032,37 @@ async function probeDeferredDynamicUsage(
|
|
|
3034
3032
|
),
|
|
3035
3033
|
{ route: route.route || '/', prerender: true },
|
|
3036
3034
|
),
|
|
3037
|
-
)
|
|
3035
|
+
)
|
|
3038
3036
|
// Give a 0ms timer scheduled during the render its turn before we stop
|
|
3039
3037
|
// listening; anything slower keeps the route dynamic without a report.
|
|
3040
|
-
await new Promise(resolve => setTimeout(resolve, 0))
|
|
3038
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
3041
3039
|
} catch {
|
|
3042
3040
|
// Ordinary bailout (or an unrelated prerender failure): the route just
|
|
3043
3041
|
// stays dynamic, exactly as it already did without the probe.
|
|
3044
|
-
return
|
|
3042
|
+
return
|
|
3045
3043
|
} finally {
|
|
3046
|
-
endDynamicBailoutProbe()
|
|
3047
|
-
restore()
|
|
3044
|
+
endDynamicBailoutProbe()
|
|
3045
|
+
restore()
|
|
3048
3046
|
}
|
|
3049
3047
|
for (const failure of failures) {
|
|
3050
|
-
if (!(failure instanceof Error) || failure.name !== 'DynamicServerError') continue
|
|
3048
|
+
if (!(failure instanceof Error) || failure.name !== 'DynamicServerError') continue
|
|
3051
3049
|
console.error(
|
|
3052
3050
|
`Error occurred prerendering page "${routePath}". Read more: https://nextjs.org/docs/messages/prerender-error`,
|
|
3053
|
-
)
|
|
3054
|
-
console.error(`${failure.name}: ${failure.message}`)
|
|
3051
|
+
)
|
|
3052
|
+
console.error(`${failure.name}: ${failure.message}`)
|
|
3055
3053
|
}
|
|
3056
3054
|
}
|
|
3057
3055
|
|
|
3058
3056
|
function warnSkippedStatic(routePath: string, reason: string) {
|
|
3059
3057
|
console.warn(
|
|
3060
3058
|
`pnext build: skipping static output for ${routePath} (${reason}); the route will be served dynamically`,
|
|
3061
|
-
)
|
|
3059
|
+
)
|
|
3062
3060
|
}
|
|
3063
3061
|
|
|
3064
3062
|
// pnext routes carry Express-style `:param` / `:...catchAll` segments; Next's
|
|
3065
3063
|
// build diagnostics name routes in `[param]` / `[...catchAll]` form.
|
|
3066
3064
|
function toNextRoutePattern(route: string): string {
|
|
3067
|
-
return route.replace(/:\.\.\.([^/]+)/g, '[...$1]').replace(/:([^/]+)/g, '[$1]')
|
|
3065
|
+
return route.replace(/:\.\.\.([^/]+)/g, '[...$1]').replace(/:([^/]+)/g, '[$1]')
|
|
3068
3066
|
}
|
|
3069
3067
|
|
|
3070
3068
|
// A throwing after() during a prerender is tagged by the compat after() runtime
|
|
@@ -3075,26 +3073,26 @@ function isAfterPrerenderError(error: unknown): boolean {
|
|
|
3075
3073
|
typeof error === 'object' &&
|
|
3076
3074
|
error !== null &&
|
|
3077
3075
|
(error as Record<symbol, unknown>)[Symbol.for('pnext.afterPrerenderError')] === true
|
|
3078
|
-
)
|
|
3076
|
+
)
|
|
3079
3077
|
}
|
|
3080
3078
|
|
|
3081
3079
|
/** Symlink-resolved path, for comparing two spellings of one file. Missing files keep their path. */
|
|
3082
3080
|
function realFilePath(file: string): string {
|
|
3083
3081
|
try {
|
|
3084
|
-
return realpathSync.native(file)
|
|
3082
|
+
return realpathSync.native(file)
|
|
3085
3083
|
} catch {
|
|
3086
|
-
return path.resolve(file)
|
|
3084
|
+
return path.resolve(file)
|
|
3087
3085
|
}
|
|
3088
3086
|
}
|
|
3089
3087
|
|
|
3090
3088
|
function safePublicPath(outPath: string, ...segments: string[]) {
|
|
3091
|
-
const publicPath = path.join(outPath, 'public')
|
|
3092
|
-
const file = path.join(publicPath, ...segments)
|
|
3093
|
-
const relative = path.relative(publicPath, file)
|
|
3089
|
+
const publicPath = path.join(outPath, 'public')
|
|
3090
|
+
const file = path.join(publicPath, ...segments)
|
|
3091
|
+
const relative = path.relative(publicPath, file)
|
|
3094
3092
|
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
3095
|
-
throw new Error(`Static output path escapes public directory: ${segments.join('/')}`)
|
|
3093
|
+
throw new Error(`Static output path escapes public directory: ${segments.join('/')}`)
|
|
3096
3094
|
}
|
|
3097
|
-
return file
|
|
3095
|
+
return file
|
|
3098
3096
|
}
|
|
3099
3097
|
|
|
3100
3098
|
/**
|
|
@@ -3104,45 +3102,45 @@ function safePublicPath(outPath: string, ...segments: string[]) {
|
|
|
3104
3102
|
* points here, and the 404 is the user's to resolve by installing partytown.
|
|
3105
3103
|
*/
|
|
3106
3104
|
async function copyPartytownLib(config: Awaited<ReturnType<typeof loadConfig>>) {
|
|
3107
|
-
if (!nextCompatEnabled(config)) return
|
|
3108
|
-
if (!buildCompat().nextScriptWorkersEnabled()) return
|
|
3105
|
+
if (!nextCompatEnabled(config)) return
|
|
3106
|
+
if (!buildCompat().nextScriptWorkersEnabled()) return
|
|
3109
3107
|
|
|
3110
|
-
const libDir = resolvePartytownLibDir(config.root)
|
|
3108
|
+
const libDir = resolvePartytownLibDir(config.root)
|
|
3111
3109
|
if (!libDir) {
|
|
3112
3110
|
console.warn(
|
|
3113
3111
|
'pnext build: experimental.nextScriptWorkers is enabled but no Partytown package was found ' +
|
|
3114
3112
|
'(@qwik.dev/partytown or @builder.io/partytown); worker scripts will 404 the library until it is installed',
|
|
3115
|
-
)
|
|
3116
|
-
return
|
|
3113
|
+
)
|
|
3114
|
+
return
|
|
3117
3115
|
}
|
|
3118
|
-
const target = path.join(config.outPath, 'public', '_next', 'static', '~partytown')
|
|
3119
|
-
await mkdir(target, { recursive: true })
|
|
3120
|
-
await copyPublicDir(libDir, target)
|
|
3116
|
+
const target = path.join(config.outPath, 'public', '_next', 'static', '~partytown')
|
|
3117
|
+
await mkdir(target, { recursive: true })
|
|
3118
|
+
await copyPublicDir(libDir, target)
|
|
3121
3119
|
}
|
|
3122
3120
|
|
|
3123
3121
|
// Locate the Partytown package `lib/` dir from the app root, preferring the
|
|
3124
3122
|
// current `@qwik.dev/partytown` over the legacy `@builder.io/partytown`. Returns
|
|
3125
3123
|
// null when neither resolves.
|
|
3126
3124
|
function resolvePartytownLibDir(root: string): string | null {
|
|
3127
|
-
const requireFromRoot = createRequire(path.join(root, 'package.json'))
|
|
3125
|
+
const requireFromRoot = createRequire(path.join(root, 'package.json'))
|
|
3128
3126
|
for (const pkg of ['@qwik.dev/partytown', '@builder.io/partytown']) {
|
|
3129
3127
|
try {
|
|
3130
|
-
const pkgJson = requireFromRoot.resolve(`${pkg}/package.json`)
|
|
3131
|
-
const libDir = path.join(path.dirname(pkgJson), 'lib')
|
|
3132
|
-
if (existsSync(libDir) && statSync(libDir).isDirectory()) return libDir
|
|
3128
|
+
const pkgJson = requireFromRoot.resolve(`${pkg}/package.json`)
|
|
3129
|
+
const libDir = path.join(path.dirname(pkgJson), 'lib')
|
|
3130
|
+
if (existsSync(libDir) && statSync(libDir).isDirectory()) return libDir
|
|
3133
3131
|
} catch {
|
|
3134
3132
|
// Package not installed; try the next candidate.
|
|
3135
3133
|
}
|
|
3136
3134
|
}
|
|
3137
|
-
return null
|
|
3135
|
+
return null
|
|
3138
3136
|
}
|
|
3139
3137
|
|
|
3140
3138
|
async function copyPublicDir(from: string, to: string) {
|
|
3141
|
-
const files = await listFiles(from)
|
|
3139
|
+
const files = await listFiles(from)
|
|
3142
3140
|
for (const file of files) {
|
|
3143
|
-
const relative = toPosixPath(path.relative(from, file))
|
|
3144
|
-
const target = path.join(to, relative)
|
|
3145
|
-
await mkdir(path.dirname(target), { recursive: true })
|
|
3146
|
-
await copyFile(file, target)
|
|
3141
|
+
const relative = toPosixPath(path.relative(from, file))
|
|
3142
|
+
const target = path.join(to, relative)
|
|
3143
|
+
await mkdir(path.dirname(target), { recursive: true })
|
|
3144
|
+
await copyFile(file, target)
|
|
3147
3145
|
}
|
|
3148
3146
|
}
|