@wular/pnext 0.0.3 → 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 -1961
- 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 -1887
- 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} +152 -143
- package/src/resolve/engine.ts +90 -77
- package/src/resolve/imports.ts +475 -463
- package/src/resolve/scan-facts.ts +442 -186
- 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 +833 -848
- 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/dev/server.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
2
|
-
import { createHash } from 'node:crypto'
|
|
3
|
-
import { readFile, readdir, rm, stat, watch } from 'node:fs/promises'
|
|
4
|
-
import path from 'node:path'
|
|
1
|
+
import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { readFile, readdir, rm, stat, watch } from 'node:fs/promises'
|
|
4
|
+
import path from 'node:path'
|
|
5
5
|
import {
|
|
6
6
|
DEFERRED_DYNAMIC_SIDECAR,
|
|
7
7
|
buildClientDynamicChunk,
|
|
@@ -9,16 +9,16 @@ import {
|
|
|
9
9
|
deferredDynamicRefById,
|
|
10
10
|
prebuiltRuntimeDir,
|
|
11
11
|
registerDeferredDynamicRef,
|
|
12
|
-
} from '../client/build'
|
|
13
|
-
import { drainPreplanBuilds } from '../runtime/vendor'
|
|
14
|
-
import { devClientCacheKey } from './client-key
|
|
15
|
-
import { clientChunkStoreDir, contentAddressDir, sweepClientChunkStore } from './client-chunk-store'
|
|
16
|
-
import { restartCacheEnabled } from './restart
|
|
17
|
-
import { installRouteFactsCache } from './route-facts
|
|
18
|
-
import { installGlobalCssCache } from './global-css
|
|
19
|
-
import { clientEntryName } from '../client/
|
|
20
|
-
import { bootstrapCompat, bootstrapCompatBoot } from '../compat-bootstrap'
|
|
21
|
-
import type { ResolvedConfig } from '../config'
|
|
12
|
+
} from '../client/build'
|
|
13
|
+
import { drainPreplanBuilds } from '../runtime/vendor'
|
|
14
|
+
import { devClientCacheKey } from './restart/client-key'
|
|
15
|
+
import { clientChunkStoreDir, contentAddressDir, sweepClientChunkStore } from './client-chunk-store'
|
|
16
|
+
import { restartCacheEnabled } from './restart/enabled'
|
|
17
|
+
import { installRouteFactsCache } from './restart/route-facts'
|
|
18
|
+
import { installGlobalCssCache } from './restart/global-css'
|
|
19
|
+
import { clientEntryName } from '../client/chunk-name'
|
|
20
|
+
import { bootstrapCompat, bootstrapCompatBoot } from '../compat-bootstrap'
|
|
21
|
+
import type { ResolvedConfig } from '../config'
|
|
22
22
|
import {
|
|
23
23
|
buildClientReferenceCss,
|
|
24
24
|
buildGlobalCss,
|
|
@@ -26,20 +26,20 @@ import {
|
|
|
26
26
|
clearGlobalCssSourceCache,
|
|
27
27
|
isCssFile,
|
|
28
28
|
warmCssPipeline,
|
|
29
|
-
} from '../css'
|
|
29
|
+
} from '../css/build'
|
|
30
30
|
import {
|
|
31
31
|
deferServerRuntimePlugins,
|
|
32
32
|
installServerRuntimePlugins,
|
|
33
33
|
registerServerRuntime,
|
|
34
34
|
serverBundleTargetForRuntime,
|
|
35
|
-
} from '../runtime/
|
|
36
|
-
import { applyProxyResponse, createProxyRunner, validateProxyFiles } from '../proxy'
|
|
37
|
-
import { renderGlobalNotFoundResponse, renderPageResponse } from '../render'
|
|
38
|
-
import { getFontExtensions } from '../render/hooks'
|
|
39
|
-
import { clearMetadataFileCaches, metadataRouteHandlerModule } from '../routing/metadata'
|
|
40
|
-
import { handleRouteModule, type RouteHandlerModule } from '../routing/handler'
|
|
41
|
-
import { malformedUrlResponse, trailingSlashRedirect } from '../routing/href'
|
|
42
|
-
import { withForwardedHeaders } from '../routing/forwarded'
|
|
35
|
+
} from '../runtime/loader'
|
|
36
|
+
import { applyProxyResponse, createProxyRunner, validateProxyFiles } from '../routing/proxy'
|
|
37
|
+
import { renderGlobalNotFoundResponse, renderPageResponse } from '../render/renderer'
|
|
38
|
+
import { getFontExtensions } from '../render/hooks'
|
|
39
|
+
import { clearMetadataFileCaches, metadataRouteHandlerModule } from '../routing/metadata-files'
|
|
40
|
+
import { handleRouteModule, type RouteHandlerModule } from '../routing/handler'
|
|
41
|
+
import { malformedUrlResponse, trailingSlashRedirect } from '../routing/href'
|
|
42
|
+
import { withForwardedHeaders } from '../routing/forwarded'
|
|
43
43
|
import {
|
|
44
44
|
findLayouts,
|
|
45
45
|
matchRoute,
|
|
@@ -49,9 +49,9 @@ import {
|
|
|
49
49
|
routeFactsVersion,
|
|
50
50
|
scanRoutes,
|
|
51
51
|
selectRouteForRequest,
|
|
52
|
-
} from '../routing/routes'
|
|
53
|
-
import { writeTypegen } from '../typegen'
|
|
54
|
-
import { runWithCacheScope } from '../cache
|
|
52
|
+
} from '../routing/routes'
|
|
53
|
+
import { writeTypegen } from '../cli/typegen'
|
|
54
|
+
import { runWithCacheScope } from '../request/cache'
|
|
55
55
|
import {
|
|
56
56
|
finalizeResponse,
|
|
57
57
|
getAssetExtensions,
|
|
@@ -62,7 +62,7 @@ import {
|
|
|
62
62
|
runInitHooks,
|
|
63
63
|
runRequestWarmHooks,
|
|
64
64
|
withRouteRuntime,
|
|
65
|
-
} from '../extensions'
|
|
65
|
+
} from '../extensions'
|
|
66
66
|
import {
|
|
67
67
|
flushWorkUnit,
|
|
68
68
|
flushWorkUnitOnClose,
|
|
@@ -70,18 +70,18 @@ import {
|
|
|
70
70
|
runWithWorkUnit,
|
|
71
71
|
setPhase,
|
|
72
72
|
setWorkUnitRoute,
|
|
73
|
-
} from '../request/context'
|
|
74
|
-
import { setRequestRuntime } from '../routing/request-
|
|
75
|
-
import { ensureDir, listFiles, writeText } from '../utils/fs'
|
|
76
|
-
import { contentType } from '../utils/content-type'
|
|
73
|
+
} from '../request/context'
|
|
74
|
+
import { setRequestRuntime } from '../routing/request-environment'
|
|
75
|
+
import { ensureDir, listFiles, writeText } from '../utils/fs'
|
|
76
|
+
import { contentType } from '../utils/content-type'
|
|
77
77
|
import {
|
|
78
78
|
flushDevProfileLines,
|
|
79
79
|
formatProfileDuration,
|
|
80
80
|
recordDevProfileLine,
|
|
81
|
-
} from '../utils/dev-profile'
|
|
82
|
-
import { clearResolverFsCache } from '../resolve/engine'
|
|
83
|
-
import { clearAppTreeResolutions, workspacePackageRoots } from '../resolve/imports'
|
|
84
|
-
import type { RouteManifestEntry, RouteParamValue } from '../types'
|
|
81
|
+
} from '../utils/dev-profile'
|
|
82
|
+
import { clearResolverFsCache } from '../resolve/engine'
|
|
83
|
+
import { clearAppTreeResolutions, workspacePackageRoots } from '../resolve/imports'
|
|
84
|
+
import type { RouteManifestEntry, RouteParamValue } from '../types'
|
|
85
85
|
import {
|
|
86
86
|
clearDevRouteBundleKeys,
|
|
87
87
|
devClientModuleHref,
|
|
@@ -89,60 +89,61 @@ import {
|
|
|
89
89
|
devRouteModuleLoaders,
|
|
90
90
|
devServerModuleHref,
|
|
91
91
|
warmDevModulePipeline,
|
|
92
|
-
} from '
|
|
93
|
-
import { ssrClientReference } from '../client/reference'
|
|
94
|
-
import { cacheRoot, setDevWatcherFreshness } from '
|
|
95
|
-
import { markBoot } from '../cli/boot
|
|
92
|
+
} from '../runtime/modules'
|
|
93
|
+
import { ssrClientReference } from '../client/reference'
|
|
94
|
+
import { cacheRoot, setDevWatcherFreshness } from '../runtime/module-cache'
|
|
95
|
+
import { markBoot } from '../cli/boot/trace'
|
|
96
|
+
import { formatDuration } from '../utils/verbose'
|
|
96
97
|
|
|
97
98
|
interface DevServerOptions {
|
|
98
|
-
config: ResolvedConfig
|
|
99
|
-
port: number
|
|
100
|
-
hostname: string
|
|
99
|
+
config: ResolvedConfig
|
|
100
|
+
port: number
|
|
101
|
+
hostname: string
|
|
101
102
|
/**
|
|
102
103
|
* Compile the entry route in the background as soon as the server is up, so the
|
|
103
104
|
* expensive cold build (esbuild cold start + Tailwind subprocess) overlaps with
|
|
104
105
|
* the browser launching instead of blocking the first click. The build caches
|
|
105
106
|
* dedup, so if the real request arrives mid-warm it awaits the same promise.
|
|
106
107
|
*/
|
|
107
|
-
warm?: boolean
|
|
108
|
+
warm?: boolean
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
const clientBuilds = new Map<string, Promise<string>>()
|
|
111
|
-
const clientChunks = new Map<string, string>()
|
|
111
|
+
const clientBuilds = new Map<string, Promise<string>>()
|
|
112
|
+
const clientChunks = new Map<string, string>()
|
|
112
113
|
|
|
113
114
|
// esbuild.stop() (the memory watchdog) can leave an in-flight build's promise permanently unsettled;
|
|
114
115
|
// anything awaiting it hangs and the dedup map keeps re-serving the poisoned promise. Waiters race against
|
|
115
116
|
// this registry so a service restart fails them fast instead - failed builds drop from the caches and the
|
|
116
117
|
// next request retries against the respawned service.
|
|
117
|
-
const clientBuildStopWaiters = new Set<(err: Error) => void>()
|
|
118
|
+
const clientBuildStopWaiters = new Set<(err: Error) => void>()
|
|
118
119
|
export function failInFlightClientBuilds(reason: string) {
|
|
119
|
-
const err = new Error(reason)
|
|
120
|
-
for (const reject of clientBuildStopWaiters) reject(err)
|
|
121
|
-
clientBuildStopWaiters.clear()
|
|
122
|
-
clientBuilds.clear()
|
|
120
|
+
const err = new Error(reason)
|
|
121
|
+
for (const reject of clientBuildStopWaiters) reject(err)
|
|
122
|
+
clientBuildStopWaiters.clear()
|
|
123
|
+
clientBuilds.clear()
|
|
123
124
|
}
|
|
124
125
|
// Client out-dirs whose chunk list is already in clientChunks — the dir name is
|
|
125
126
|
// a content key, so it never needs a second readdir.
|
|
126
|
-
const indexedClientDirs = new Set<string>()
|
|
127
|
+
const indexedClientDirs = new Set<string>()
|
|
127
128
|
// Layout chains per route file: findLayouts + an existsSync per level, which a
|
|
128
129
|
// warm request would otherwise redo every time.
|
|
129
|
-
const routeLayoutFiles = new Map<string, string[]>()
|
|
130
|
+
const routeLayoutFiles = new Map<string, string[]>()
|
|
130
131
|
// Keyed by app (outPath), not by asset path or route id alone: several dev
|
|
131
132
|
// servers for different apps share one process in tests and monorepo tooling,
|
|
132
133
|
// and a bare `/assets/global.css` or route id collides between them.
|
|
133
|
-
const assetBuilds = new Map<string, Promise<unknown>>()
|
|
134
|
-
const routeCacheKeys = new Map<string, Promise<string>>()
|
|
135
|
-
const appKey = (outPath: string, key: string) => `${outPath}\0${key}
|
|
134
|
+
const assetBuilds = new Map<string, Promise<unknown>>()
|
|
135
|
+
const routeCacheKeys = new Map<string, Promise<string>>()
|
|
136
|
+
const appKey = (outPath: string, key: string) => `${outPath}\0${key}`
|
|
136
137
|
function clearAppKeyed(map: Map<string, unknown>, outPath: string) {
|
|
137
|
-
for (const key of map.keys()) if (key.startsWith(`${outPath}\0`)) map.delete(key)
|
|
138
|
+
for (const key of map.keys()) if (key.startsWith(`${outPath}\0`)) map.delete(key)
|
|
138
139
|
}
|
|
139
140
|
// Routes whose bundles this process has already compiled, so the "Compiling"
|
|
140
141
|
// banner prints once per cold route (like Next.js) and not on warm hits.
|
|
141
142
|
// Cleared on reload.
|
|
142
|
-
const compiledRoutes = new Set<string>()
|
|
143
|
+
const compiledRoutes = new Set<string>()
|
|
143
144
|
|
|
144
145
|
interface DevEventClient {
|
|
145
|
-
controller: ReadableStreamDefaultController<Uint8Array
|
|
146
|
+
controller: ReadableStreamDefaultController<Uint8Array>
|
|
146
147
|
}
|
|
147
148
|
|
|
148
149
|
/**
|
|
@@ -151,27 +152,27 @@ interface DevEventClient {
|
|
|
151
152
|
* by route or source path need telling.
|
|
152
153
|
*/
|
|
153
154
|
function invalidateDevCaches(config: ResolvedConfig, changed: string[], structural: boolean) {
|
|
154
|
-
devModuleGraph(config).invalidate(changed)
|
|
155
|
+
devModuleGraph(config).invalidate(changed)
|
|
155
156
|
// oxc caches fs lookups (including misses) until cleared, so a file added or
|
|
156
157
|
// deleted since the last resolve stays invisible without this. A plain edit
|
|
157
158
|
// changes no lookup answer.
|
|
158
159
|
if (structural) {
|
|
159
|
-
clearResolverFsCache()
|
|
160
|
-
clearAppTreeResolutions()
|
|
161
|
-
clearMetadataFileCaches()
|
|
162
|
-
routeLayoutFiles.clear()
|
|
160
|
+
clearResolverFsCache()
|
|
161
|
+
clearAppTreeResolutions()
|
|
162
|
+
clearMetadataFileCaches()
|
|
163
|
+
routeLayoutFiles.clear()
|
|
163
164
|
}
|
|
164
165
|
// Route CSS is derived from the whole source tree (Tailwind scans it), and
|
|
165
166
|
// the client cache key is a content hash whose inputs just moved.
|
|
166
|
-
clearGlobalCssSourceCache()
|
|
167
|
-
clearDevRouteBundleKeys()
|
|
168
|
-
clearAppKeyed(assetBuilds, config.outPath)
|
|
169
|
-
clearAppKeyed(routeCacheKeys, config.outPath)
|
|
170
|
-
indexedClientDirs.clear()
|
|
171
|
-
compiledRoutes.clear()
|
|
167
|
+
clearGlobalCssSourceCache()
|
|
168
|
+
clearDevRouteBundleKeys()
|
|
169
|
+
clearAppKeyed(assetBuilds, config.outPath)
|
|
170
|
+
clearAppKeyed(routeCacheKeys, config.outPath)
|
|
171
|
+
indexedClientDirs.clear()
|
|
172
|
+
compiledRoutes.clear()
|
|
172
173
|
// Long sessions accumulate one generation per save; sweep past the keep
|
|
173
174
|
// window here too, not just at boot. Best-effort and off the reload path.
|
|
174
|
-
void evictStaleClientCaches(config.outPath)
|
|
175
|
+
void evictStaleClientCaches(config.outPath)
|
|
175
176
|
}
|
|
176
177
|
|
|
177
178
|
// Every edit writes a fresh content-keyed generation under cache/client/ and
|
|
@@ -180,26 +181,26 @@ function invalidateDevCaches(config: ResolvedConfig, changed: string[], structur
|
|
|
180
181
|
// just a cache miss. Runs off the boot path.
|
|
181
182
|
async function evictStaleClientCaches(outPath: string) {
|
|
182
183
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
183
|
-
const keep = Number(process.env.PNEXT_DEV_CLIENT_CACHE_KEEP || 32)
|
|
184
|
-
if (!Number.isFinite(keep) || keep <= 0) return
|
|
185
|
-
const clientRoot = path.join(outPath, 'cache', 'client')
|
|
184
|
+
const keep = Number(process.env.PNEXT_DEV_CLIENT_CACHE_KEEP || 32)
|
|
185
|
+
if (!Number.isFinite(keep) || keep <= 0) return
|
|
186
|
+
const clientRoot = path.join(outPath, 'cache', 'client')
|
|
186
187
|
try {
|
|
187
|
-
const entries = await readdir(clientRoot)
|
|
188
|
-
if (entries.length <= keep) return
|
|
188
|
+
const entries = await readdir(clientRoot)
|
|
189
|
+
if (entries.length <= keep) return
|
|
189
190
|
const dated = await Promise.all(
|
|
190
191
|
entries.map(async entry => {
|
|
191
|
-
const full = path.join(clientRoot, entry)
|
|
192
|
-
const info = await stat(full).catch(() => null)
|
|
193
|
-
return { full, mtime: info?.mtimeMs ?? 0 }
|
|
192
|
+
const full = path.join(clientRoot, entry)
|
|
193
|
+
const info = await stat(full).catch(() => null)
|
|
194
|
+
return { full, mtime: info?.mtimeMs ?? 0 }
|
|
194
195
|
}),
|
|
195
|
-
)
|
|
196
|
-
dated.sort((a, b) => b.mtime - a.mtime)
|
|
196
|
+
)
|
|
197
|
+
dated.sort((a, b) => b.mtime - a.mtime)
|
|
197
198
|
await Promise.allSettled(
|
|
198
199
|
dated.slice(keep).map(entry => rm(entry.full, { recursive: true, force: true })),
|
|
199
|
-
)
|
|
200
|
+
)
|
|
200
201
|
// Evicted generations may have been the last link to a store entry; sweep
|
|
201
202
|
// after removal so orphaned store files (nlink back down to 1) don't linger.
|
|
202
|
-
await sweepClientChunkStore(clientChunkStoreDir(outPath))
|
|
203
|
+
await sweepClientChunkStore(clientChunkStoreDir(outPath))
|
|
203
204
|
} catch {
|
|
204
205
|
// ignore: eviction is best-effort; worst case is disk growth, not breakage
|
|
205
206
|
}
|
|
@@ -209,80 +210,80 @@ async function evictStaleClientCaches(outPath: string) {
|
|
|
209
210
|
// cache into a `.cache-stale-*` dir on every boot. Nothing creates them now.
|
|
210
211
|
async function removeLeakedStaleCaches(outPath: string) {
|
|
211
212
|
try {
|
|
212
|
-
const entries = await readdir(outPath)
|
|
213
|
+
const entries = await readdir(outPath)
|
|
213
214
|
await Promise.allSettled(
|
|
214
215
|
entries
|
|
215
216
|
.filter(entry => entry.startsWith('.cache-stale-'))
|
|
216
217
|
.map(entry => rm(path.join(outPath, entry), { recursive: true, force: true })),
|
|
217
|
-
)
|
|
218
|
+
)
|
|
218
219
|
} catch {
|
|
219
220
|
// ignore: leftover stale dirs are only a disk-space nuisance
|
|
220
221
|
}
|
|
221
222
|
}
|
|
222
223
|
|
|
223
224
|
export async function startDevServer(options: DevServerOptions) {
|
|
224
|
-
const { config, port, hostname } = options
|
|
225
|
+
const { config, port, hostname } = options
|
|
225
226
|
// FSEvents replays writes that landed just before the watch started, so a server
|
|
226
227
|
// booted right after a checkout or scaffold reloads itself once for files it has
|
|
227
228
|
// already read. Nothing is compiled before the watcher, so anything this old is
|
|
228
229
|
// already in hand.
|
|
229
|
-
const bootTime = Date.now()
|
|
230
|
+
const bootTime = Date.now()
|
|
230
231
|
// Spawn the CSS worker first: Tailwind's cold boot then runs off the event
|
|
231
232
|
// loop, alongside everything below, instead of on the first page's stylesheet.
|
|
232
|
-
warmCssPipeline(config, { dev: true })
|
|
233
|
-
markBoot('boot:css-warm')
|
|
233
|
+
warmCssPipeline(config, { dev: true })
|
|
234
|
+
markBoot('boot:css-warm')
|
|
234
235
|
// Compat plugin loader: the single gated seam that populates the core
|
|
235
236
|
// extension registries when compat is enabled (no-op for pure-core apps).
|
|
236
237
|
// Boot takes the cheap tier only — route conventions, aliases, proxy names,
|
|
237
238
|
// lifecycle hooks; the implementation graph loads on the first request.
|
|
238
|
-
await bootstrapCompatBoot(config)
|
|
239
|
-
markBoot('boot:compat')
|
|
239
|
+
await bootstrapCompatBoot(config)
|
|
240
|
+
markBoot('boot:compat')
|
|
240
241
|
// The compat implementation graph still has to load (first request / warmup).
|
|
241
242
|
// Registering the module plugins now would put that whole import through the
|
|
242
243
|
// plugin pipeline; arm them once compat is in, like build and start do.
|
|
243
|
-
deferServerRuntimePlugins()
|
|
244
|
-
registerServerRuntime(config)
|
|
244
|
+
deferServerRuntimePlugins()
|
|
245
|
+
registerServerRuntime(config)
|
|
245
246
|
// Compat installs the Next fetch-cache patch here (no-op for pure-core apps).
|
|
246
|
-
runInitHooks(config)
|
|
247
|
+
runInitHooks(config)
|
|
247
248
|
// No boot sweep: the compiled cache is content-addressed and persists across
|
|
248
249
|
// restarts, so validity is a name miss plus the
|
|
249
250
|
// cache marker, not a wipe.
|
|
250
|
-
void removeLeakedStaleCaches(config.outPath)
|
|
251
|
-
void evictStaleClientCaches(config.outPath)
|
|
251
|
+
void removeLeakedStaleCaches(config.outPath)
|
|
252
|
+
void evictStaleClientCaches(config.outPath)
|
|
252
253
|
// Route facts are the biggest single thing a restart's first page used to re-derive. Installed
|
|
253
254
|
// before the route table so the first touch - whoever forces it - reads the previous process's
|
|
254
255
|
// walk instead of repeating it.
|
|
255
|
-
installRouteFactsCache(config)
|
|
256
|
+
installRouteFactsCache(config)
|
|
256
257
|
// Same deal for the root layout's CSS graph: 320 files read to find one
|
|
257
258
|
// stylesheet, and both the warm tier and the render force it at Ready+0.
|
|
258
|
-
installGlobalCssCache(config)
|
|
259
|
-
markBoot('boot:registry')
|
|
260
|
-
let routes = await scanRoutes(config.appPath)
|
|
261
|
-
markBoot('boot:scanRoutes')
|
|
262
|
-
await validateProxyFiles(config)
|
|
263
|
-
let proxyRunner = createProxyRunner(config)
|
|
264
|
-
markBoot('boot:proxy')
|
|
265
|
-
let devImportVersion = String(Date.now())
|
|
259
|
+
installGlobalCssCache(config)
|
|
260
|
+
markBoot('boot:registry')
|
|
261
|
+
let routes = await scanRoutes(config.appPath)
|
|
262
|
+
markBoot('boot:scanRoutes')
|
|
263
|
+
await validateProxyFiles(config)
|
|
264
|
+
let proxyRunner = createProxyRunner(config)
|
|
265
|
+
markBoot('boot:proxy')
|
|
266
|
+
let devImportVersion = String(Date.now())
|
|
266
267
|
// The proxy runs serially in front of every request, so its compile+import is
|
|
267
268
|
// pure critical path when the first request pays it. Started here it overlaps
|
|
268
269
|
// the CSS worker warmup, typegen and the first route's own module pass.
|
|
269
|
-
if (restartCacheEnabled()) proxyRunner.warm({ dev: true, devImportVersion })
|
|
270
|
-
markBoot('boot:proxy-warm')
|
|
270
|
+
if (restartCacheEnabled()) proxyRunner.warm({ dev: true, devImportVersion })
|
|
271
|
+
markBoot('boot:proxy-warm')
|
|
271
272
|
// Publish the live routing state the compat request interceptors (action
|
|
272
273
|
// dispatch, rewrites) + client-action discovery read. Compat re-runs action
|
|
273
274
|
// discovery lazily whenever the dev import version changes (see below), so the
|
|
274
275
|
// registry + client-stub set that startup/reload armed inline stay current.
|
|
275
|
-
publishRuntime()
|
|
276
|
-
const clients = new Set<DevEventClient>()
|
|
276
|
+
publishRuntime()
|
|
277
|
+
const clients = new Set<DevEventClient>()
|
|
277
278
|
// Bumped on every rebuild broadcast and stamped on each stream's `ready`, so a
|
|
278
279
|
// client that reconnects (bfcache restore) can tell it missed one and reload.
|
|
279
|
-
let generation = 0
|
|
280
|
-
let watcher: DevWatcher | undefined
|
|
280
|
+
let generation = 0
|
|
281
|
+
let watcher: DevWatcher | undefined
|
|
281
282
|
/** Requests currently being served — background work defers to them. */
|
|
282
|
-
let inFlight = 0
|
|
283
|
+
let inFlight = 0
|
|
283
284
|
|
|
284
285
|
function publishRuntime() {
|
|
285
|
-
setRequestRuntime({ config, routes, dev: true, devImportVersion })
|
|
286
|
+
setRequestRuntime({ config, routes, dev: true, devImportVersion })
|
|
286
287
|
}
|
|
287
288
|
|
|
288
289
|
// Typegen's route walk is the longest CPU run dev does outside a compile, and the only one with no
|
|
@@ -290,25 +291,25 @@ export async function startDevServer(options: DevServerOptions) {
|
|
|
290
291
|
// Bounded: a dev server under continuous load would otherwise never emit the .d.ts at all, so a
|
|
291
292
|
// route waits out requests for at most this long and then takes its turn regardless.
|
|
292
293
|
// `PNEXT_DEV_TYPEGEN_PACE=0` restores the uninterrupted walk.
|
|
293
|
-
const TYPEGEN_MAX_WAIT_MS = 3_000
|
|
294
|
+
const TYPEGEN_MAX_WAIT_MS = 3_000
|
|
294
295
|
|
|
295
296
|
async function typegenPause() {
|
|
296
297
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
297
|
-
if (process.env.PNEXT_DEV_TYPEGEN_PACE === '0') return
|
|
298
|
-
const deadline = performance.now() + TYPEGEN_MAX_WAIT_MS
|
|
298
|
+
if (process.env.PNEXT_DEV_TYPEGEN_PACE === '0') return
|
|
299
|
+
const deadline = performance.now() + TYPEGEN_MAX_WAIT_MS
|
|
299
300
|
do {
|
|
300
|
-
await new Promise(resolve => setTimeout(resolve, inFlight > 0 ? 25 : 0))
|
|
301
|
-
} while (inFlight > 0 && performance.now() < deadline)
|
|
301
|
+
await new Promise(resolve => setTimeout(resolve, inFlight > 0 ? 25 : 0))
|
|
302
|
+
} while (inFlight > 0 && performance.now() < deadline)
|
|
302
303
|
}
|
|
303
304
|
|
|
304
305
|
async function typegenInBackground() {
|
|
305
|
-
const generation = routes
|
|
306
|
+
const generation = routes
|
|
306
307
|
try {
|
|
307
|
-
await writeTypegen(config, await materializeRouteFactsPaced(generation, typegenPause))
|
|
308
|
+
await writeTypegen(config, await materializeRouteFactsPaced(generation, typegenPause))
|
|
308
309
|
} catch (error) {
|
|
309
|
-
if (generation === routes) logDevPreloadError('typegen', error)
|
|
310
|
+
if (generation === routes) logDevPreloadError('typegen', error)
|
|
310
311
|
}
|
|
311
|
-
if (generation === routes) syncWatchRoots()
|
|
312
|
+
if (generation === routes) syncWatchRoots()
|
|
312
313
|
}
|
|
313
314
|
|
|
314
315
|
async function reload(change: DevChange) {
|
|
@@ -317,31 +318,31 @@ export async function startDevServer(options: DevServerOptions) {
|
|
|
317
318
|
// graph has never seen.
|
|
318
319
|
const structural =
|
|
319
320
|
change.renamed &&
|
|
320
|
-
change.files.some(file => !existsSync(file) || !devModuleGraph(config).knows(file))
|
|
321
|
-
invalidateDevCaches(config, change.files, structural)
|
|
321
|
+
change.files.some(file => !existsSync(file) || !devModuleGraph(config).knows(file))
|
|
322
|
+
invalidateDevCaches(config, change.files, structural)
|
|
322
323
|
// A burst that only touched plain stylesheets cannot have moved the module graph, the route
|
|
323
324
|
// table, the proxy or the client bundles - only the built CSS assets. The page swaps its <link>s
|
|
324
325
|
// in place instead of navigating.
|
|
325
326
|
if (isCssOnlyChange(change.files)) {
|
|
326
|
-
generation
|
|
327
|
-
broadcast(clients, 'css-update')
|
|
328
|
-
return
|
|
327
|
+
generation++
|
|
328
|
+
broadcast(clients, 'css-update')
|
|
329
|
+
return
|
|
329
330
|
}
|
|
330
331
|
// The route table is built from file paths only, so only an added, removed
|
|
331
332
|
// or renamed file can change it — a plain save never does.
|
|
332
|
-
if (structural) routes = await scanRoutes(config.appPath)
|
|
333
|
-
await validateProxyFiles(config)
|
|
334
|
-
proxyRunner = createProxyRunner(config)
|
|
335
|
-
devImportVersion = `${Date.now()}
|
|
336
|
-
proxyRunner.warm({ dev: true, devImportVersion })
|
|
333
|
+
if (structural) routes = await scanRoutes(config.appPath)
|
|
334
|
+
await validateProxyFiles(config)
|
|
335
|
+
proxyRunner = createProxyRunner(config)
|
|
336
|
+
devImportVersion = `${Date.now()}`
|
|
337
|
+
proxyRunner.warm({ dev: true, devImportVersion })
|
|
337
338
|
// Republish with the bumped version so compat re-discovers actions before
|
|
338
339
|
// the next action request / client build.
|
|
339
|
-
publishRuntime()
|
|
340
|
-
void typegenInBackground()
|
|
341
|
-
watchedFactsVersion = -1
|
|
342
|
-
syncWatchRoots()
|
|
343
|
-
generation
|
|
344
|
-
broadcast(clients, 'reload')
|
|
340
|
+
publishRuntime()
|
|
341
|
+
void typegenInBackground()
|
|
342
|
+
watchedFactsVersion = -1
|
|
343
|
+
syncWatchRoots()
|
|
344
|
+
generation++
|
|
345
|
+
broadcast(clients, 'reload')
|
|
345
346
|
}
|
|
346
347
|
|
|
347
348
|
// A save's visibility must not wait out the coalescing window: the moment the
|
|
@@ -349,23 +350,23 @@ export async function startDevServer(options: DevServerOptions) {
|
|
|
349
350
|
// from it, so a request racing the debounced reload already compiles fresh.
|
|
350
351
|
// The heavy reload work (proxy, typegen, route scan, CSS) stays debounced.
|
|
351
352
|
function eagerInvalidate(file: string) {
|
|
352
|
-
devModuleGraph(config).invalidate([file])
|
|
353
|
-
clearDevRouteBundleKeys()
|
|
354
|
-
clearAppKeyed(routeCacheKeys, config.outPath)
|
|
353
|
+
devModuleGraph(config).invalidate([file])
|
|
354
|
+
clearDevRouteBundleKeys()
|
|
355
|
+
clearAppKeyed(routeCacheKeys, config.outPath)
|
|
355
356
|
}
|
|
356
357
|
|
|
357
358
|
// Watch roots outside app/ come from route sourceFiles, which only exist once
|
|
358
359
|
// a route has resolved its deferred facts — so they are re-derived whenever
|
|
359
360
|
// another route materializes (its first compile), not once at boot.
|
|
360
|
-
let watchedFactsVersion = -1
|
|
361
|
+
let watchedFactsVersion = -1
|
|
361
362
|
function syncWatchRoots() {
|
|
362
|
-
if (watchedFactsVersion === routeFactsVersion()) return
|
|
363
|
-
watchedFactsVersion = routeFactsVersion()
|
|
364
|
-
watcher = refreshWatcher(config, routes, watcher, reload, bootTime, eagerInvalidate)
|
|
363
|
+
if (watchedFactsVersion === routeFactsVersion()) return
|
|
364
|
+
watchedFactsVersion = routeFactsVersion()
|
|
365
|
+
watcher = refreshWatcher(config, routes, watcher, reload, bootTime, eagerInvalidate)
|
|
365
366
|
}
|
|
366
367
|
|
|
367
|
-
syncWatchRoots()
|
|
368
|
-
markBoot('boot:watcher')
|
|
368
|
+
syncWatchRoots()
|
|
369
|
+
markBoot('boot:watcher')
|
|
369
370
|
|
|
370
371
|
const server = Bun.serve({
|
|
371
372
|
hostname,
|
|
@@ -374,381 +375,373 @@ export async function startDevServer(options: DevServerOptions) {
|
|
|
374
375
|
fetch(request, server) {
|
|
375
376
|
// One work unit spans the whole request; its after-queue flushes once the
|
|
376
377
|
// response fully closes (stream end, redirect, notFound, error, abort).
|
|
377
|
-
inFlight
|
|
378
|
+
inFlight++
|
|
378
379
|
return runWithWorkUnit('render', () => handleDevRequest(request, server)).finally(() => {
|
|
379
|
-
inFlight
|
|
380
|
-
})
|
|
380
|
+
inFlight--
|
|
381
|
+
})
|
|
381
382
|
},
|
|
382
|
-
})
|
|
383
|
+
})
|
|
383
384
|
|
|
384
385
|
async function handleDevRequest(
|
|
385
386
|
request: Request,
|
|
386
387
|
server: { timeout(request: Request, seconds: number): void },
|
|
387
388
|
): Promise<Response> {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
389
|
+
const prologueStart = performance.now()
|
|
390
|
+
// The compat implementation graph loads here, on the first request, not at
|
|
391
|
+
// boot — everything a request touches (interceptors, render, compile) is
|
|
392
|
+
// registered by the time this resolves.
|
|
393
|
+
await bootstrapCompat(config, { serve: true })
|
|
394
|
+
installServerRuntimePlugins()
|
|
395
|
+
const unit = getWorkUnit()
|
|
396
|
+
const badRequest = malformedUrlResponse(request)
|
|
397
|
+
if (badRequest) return badRequest
|
|
398
|
+
request = withForwardedHeaders(request)
|
|
399
|
+
let url = new URL(request.url)
|
|
400
|
+
const profile = devRequestProfile(request, url)
|
|
401
|
+
if (profile) logDevProfile(profile, 'prologue (bootstrap/headers/url)', prologueStart)
|
|
402
|
+
let pageLog = pendingDevPageLoadLog(routes, request, url)
|
|
403
|
+
// Work this request wants started, but only once its own response is out.
|
|
404
|
+
let afterResponse: (() => void) | undefined
|
|
405
|
+
const logAbort = () => {
|
|
406
|
+
const pending = pageLog
|
|
407
|
+
pageLog = undefined
|
|
408
|
+
logDevPageAbort(pending)
|
|
409
|
+
}
|
|
410
|
+
request.signal.addEventListener('abort', logAbort, { once: true })
|
|
411
|
+
const finish = async (response: Response): Promise<Response> => {
|
|
412
|
+
const pending = pageLog
|
|
413
|
+
pageLog = undefined
|
|
414
|
+
request.signal.removeEventListener('abort', logAbort)
|
|
415
|
+
const finalized = await profileDevStep(profile, 'finalize response', () =>
|
|
416
|
+
finalizeResponse(
|
|
417
|
+
response,
|
|
418
|
+
{ method: request.method, url: new URL(request.url), headers: request.headers },
|
|
419
|
+
{ routeKind: unit?.routeKind ?? 'html', routeMode: unit?.routeMode },
|
|
420
|
+
),
|
|
421
|
+
)
|
|
422
|
+
const logged = logDevPageResponse(pending, finalized)
|
|
423
|
+
// A timer, not a bare call: the response is only handed to Bun when this returns, so anything
|
|
424
|
+
// started synchronously here still races it. That is also why the watch-root sync moved in -
|
|
425
|
+
// a request that compiled a route just materialized its source graph, and re-deriving the
|
|
426
|
+
// roots from it is a synchronous walk the document does not need.
|
|
427
|
+
const tasks = afterResponse
|
|
428
|
+
afterResponse = undefined
|
|
429
|
+
setTimeout(() => {
|
|
430
|
+
tasks?.()
|
|
431
|
+
syncWatchRoots()
|
|
432
|
+
}, 0)
|
|
433
|
+
if (profile) flushDevProfileLines()
|
|
434
|
+
return flushWorkUnitOnClose(logged, unit, request.signal)
|
|
435
|
+
}
|
|
435
436
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
437
|
+
try {
|
|
438
|
+
const initialPrefetchResponse = maybeDevPagePrefetchResponse(routes, url.pathname, request)
|
|
439
|
+
if (initialPrefetchResponse) return finish(initialPrefetchResponse)
|
|
440
|
+
|
|
441
|
+
let proxyResponse: Response | undefined
|
|
442
|
+
const proxyResult = await profileDevStep(profile, 'proxy', () =>
|
|
443
|
+
proxyRunner(request, { dev: true, devImportVersion }),
|
|
444
|
+
)
|
|
445
|
+
if (proxyResult instanceof Response) return finish(proxyResult)
|
|
446
|
+
if (proxyResult) {
|
|
447
|
+
request = proxyResult.request
|
|
448
|
+
proxyResponse = proxyResult.response
|
|
449
|
+
url = new URL(request.url)
|
|
450
|
+
pageLog = pendingDevPageLoadLog(routes, request, url, pageLog?.start)
|
|
451
|
+
|
|
452
|
+
const rewrittenPrefetchResponse = maybeDevPagePrefetchResponse(
|
|
453
|
+
routes,
|
|
454
|
+
url.pathname,
|
|
455
|
+
request,
|
|
456
|
+
)
|
|
457
|
+
if (rewrittenPrefetchResponse)
|
|
458
|
+
return finish(applyProxyResponse(rewrittenPrefetchResponse, proxyResponse))
|
|
459
|
+
}
|
|
459
460
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
461
|
+
if (url.pathname === '/__pnext/events') {
|
|
462
|
+
server.timeout(request, 0)
|
|
463
|
+
return finish(eventStream(clients, generation))
|
|
464
|
+
}
|
|
464
465
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
if (!found)
|
|
483
|
-
return finish(
|
|
484
|
-
applyProxyResponse(new Response('not found', { status: 404 }), proxyResponse),
|
|
485
|
-
);
|
|
486
|
-
const file = await profileDevStep(profile, `client dyn chunk ${dynChunkMatch[1]}`, () =>
|
|
487
|
-
buildDevDynamicChunk(config, found.route, found.reference, devImportVersion),
|
|
488
|
-
);
|
|
489
|
-
return finish(
|
|
490
|
-
applyProxyResponse(
|
|
491
|
-
devResponse(await readFile(file), 'text/javascript; charset=utf-8'),
|
|
492
|
-
proxyResponse,
|
|
493
|
-
),
|
|
494
|
-
);
|
|
495
|
-
}
|
|
496
|
-
const dynSharedChunk = /^\/__pnext\/client-dyn\/chunks\/(.+\.js)$/.exec(url.pathname);
|
|
497
|
-
if (dynSharedChunk?.[1]) {
|
|
498
|
-
const chunkFile = dynChunkFiles.get(dynSharedChunk[1]);
|
|
499
|
-
if (chunkFile && existsSync(chunkFile))
|
|
500
|
-
return finish(
|
|
501
|
-
applyProxyResponse(devChunkResponse(await readFile(chunkFile)), proxyResponse),
|
|
502
|
-
);
|
|
466
|
+
const assetResponse = await profileDevStep(profile, 'built asset lookup', () =>
|
|
467
|
+
maybeBuiltAsset(config, routes, url.pathname),
|
|
468
|
+
)
|
|
469
|
+
if (assetResponse) return finish(applyProxyResponse(assetResponse, proxyResponse))
|
|
470
|
+
|
|
471
|
+
const clientChunkResponse = await profileDevStep(profile, 'client chunk lookup', () =>
|
|
472
|
+
maybeDevClientChunk(config, routes, url.pathname),
|
|
473
|
+
)
|
|
474
|
+
if (clientChunkResponse) return finish(applyProxyResponse(clientChunkResponse, proxyResponse))
|
|
475
|
+
|
|
476
|
+
const runtimeResponse = maybePrebuiltRuntime(config, url.pathname)
|
|
477
|
+
if (runtimeResponse) return finish(applyProxyResponse(runtimeResponse, proxyResponse))
|
|
478
|
+
|
|
479
|
+
const dynChunkMatch = /^\/__pnext\/client-dyn\/([A-Za-z0-9-]+)\.js$/.exec(url.pathname)
|
|
480
|
+
if (dynChunkMatch?.[1]) {
|
|
481
|
+
const found = findDeferredDynamicReference(routes, dynChunkMatch[1])
|
|
482
|
+
if (!found)
|
|
503
483
|
return finish(
|
|
504
484
|
applyProxyResponse(new Response('not found', { status: 404 }), proxyResponse),
|
|
505
|
-
)
|
|
506
|
-
}
|
|
485
|
+
)
|
|
486
|
+
const file = await profileDevStep(profile, `client dyn chunk ${dynChunkMatch[1]}`, () =>
|
|
487
|
+
buildDevDynamicChunk(config, found.route, found.reference, devImportVersion),
|
|
488
|
+
)
|
|
489
|
+
return finish(
|
|
490
|
+
applyProxyResponse(
|
|
491
|
+
devResponse(await readFile(file), 'text/javascript; charset=utf-8'),
|
|
492
|
+
proxyResponse,
|
|
493
|
+
),
|
|
494
|
+
)
|
|
495
|
+
}
|
|
496
|
+
const dynSharedChunk = /^\/__pnext\/client-dyn\/chunks\/(.+\.js)$/.exec(url.pathname)
|
|
497
|
+
if (dynSharedChunk?.[1]) {
|
|
498
|
+
const chunkFile = dynChunkFiles.get(dynSharedChunk[1])
|
|
499
|
+
if (chunkFile && existsSync(chunkFile))
|
|
500
|
+
return finish(
|
|
501
|
+
applyProxyResponse(devChunkResponse(await readFile(chunkFile)), proxyResponse),
|
|
502
|
+
)
|
|
503
|
+
return finish(applyProxyResponse(new Response('not found', { status: 404 }), proxyResponse))
|
|
504
|
+
}
|
|
507
505
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
return finish(
|
|
517
|
-
applyProxyResponse(new Response('not found', { status: 404 }), proxyResponse),
|
|
518
|
-
);
|
|
519
|
-
const file = await profileDevStep(profile, `client build ${route.route}`, () =>
|
|
520
|
-
buildDevClient(config, route),
|
|
521
|
-
);
|
|
506
|
+
const clientMatch = /^\/__pnext\/client\/(.+)\.js$/.exec(url.pathname)
|
|
507
|
+
if (clientMatch?.[1]) {
|
|
508
|
+
const route = routes.find(
|
|
509
|
+
item =>
|
|
510
|
+
item.id === clientMatch[1] &&
|
|
511
|
+
(item.client || item.clientReferences.length > 0 || item.needsRouterEntry),
|
|
512
|
+
)
|
|
513
|
+
if (!route)
|
|
522
514
|
return finish(
|
|
523
|
-
applyProxyResponse(
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
515
|
+
applyProxyResponse(new Response('not found', { status: 404 }), proxyResponse),
|
|
516
|
+
)
|
|
517
|
+
const file = await profileDevStep(profile, `client build ${route.route}`, () =>
|
|
518
|
+
buildDevClient(config, route),
|
|
519
|
+
)
|
|
520
|
+
return finish(
|
|
521
|
+
applyProxyResponse(
|
|
522
|
+
devResponse(
|
|
523
|
+
await profileDevStep(profile, 'client read', () => readFile(file)),
|
|
524
|
+
'text/javascript; charset=utf-8',
|
|
529
525
|
),
|
|
530
|
-
|
|
531
|
-
|
|
526
|
+
proxyResponse,
|
|
527
|
+
),
|
|
528
|
+
)
|
|
529
|
+
}
|
|
532
530
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
)
|
|
550
|
-
if (emitted) {
|
|
551
|
-
setWorkUnitRoute('static-asset', 'static');
|
|
552
|
-
return finish(applyProxyResponse(emitted, proxyResponse));
|
|
553
|
-
}
|
|
531
|
+
const staticResponse = await profileDevStep(profile, 'static lookup', () =>
|
|
532
|
+
maybeStaticFile(config.publicPath, url.pathname),
|
|
533
|
+
)
|
|
534
|
+
if (staticResponse) {
|
|
535
|
+
setWorkUnitRoute('static-asset', 'static')
|
|
536
|
+
return finish(applyProxyResponse(staticResponse, proxyResponse))
|
|
537
|
+
}
|
|
538
|
+
// Emitted assets land in the build output's public dir, not the project public dir.
|
|
539
|
+
if (
|
|
540
|
+
getAssetExtensions()
|
|
541
|
+
.staticAssetPublicPrefixes()
|
|
542
|
+
.some(prefix => url.pathname.startsWith(prefix))
|
|
543
|
+
) {
|
|
544
|
+
const emitted = await maybeStaticFile(path.join(config.outPath, 'public'), url.pathname)
|
|
545
|
+
if (emitted) {
|
|
546
|
+
setWorkUnitRoute('static-asset', 'static')
|
|
547
|
+
return finish(applyProxyResponse(emitted, proxyResponse))
|
|
554
548
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
// Standalone chunks a bundler extension supplies (compat: the
|
|
571
|
-
// no-module polyfills chunk). The prod build writes them to disk; dev
|
|
572
|
-
// has no client out-dir for them, so serve the contents directly.
|
|
573
|
-
const staticChunk = maybeStaticClientChunk(config, staticAssetPathname);
|
|
574
|
-
if (staticChunk) {
|
|
575
|
-
setWorkUnitRoute('static-asset', 'static');
|
|
576
|
-
return finish(applyProxyResponse(staticChunk, proxyResponse));
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
setWorkUnitRoute('static-asset', 'static');
|
|
580
|
-
return finish(
|
|
581
|
-
applyProxyResponse(
|
|
582
|
-
new Response('Not Found', {
|
|
583
|
-
status: 404,
|
|
584
|
-
headers: { 'content-type': 'text/plain' },
|
|
585
|
-
}),
|
|
586
|
-
proxyResponse,
|
|
587
|
-
),
|
|
588
|
-
);
|
|
549
|
+
}
|
|
550
|
+
const staticAssetPathname = getNextStaticAssetPathname(
|
|
551
|
+
url.pathname,
|
|
552
|
+
config.basePath,
|
|
553
|
+
config.assetPrefix,
|
|
554
|
+
)
|
|
555
|
+
if (staticAssetPathname) {
|
|
556
|
+
const outPublicStatic = await maybeStaticFile(
|
|
557
|
+
path.join(config.outPath, 'public'),
|
|
558
|
+
staticAssetPathname,
|
|
559
|
+
)
|
|
560
|
+
if (outPublicStatic) {
|
|
561
|
+
setWorkUnitRoute('static-asset', 'static')
|
|
562
|
+
return finish(applyProxyResponse(outPublicStatic, proxyResponse))
|
|
589
563
|
}
|
|
590
564
|
|
|
591
|
-
//
|
|
592
|
-
//
|
|
593
|
-
//
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
for (const [index, interceptor] of getRequestExtensions().interceptors.entries()) {
|
|
599
|
-
const result = await profileDevStep(
|
|
600
|
-
profile,
|
|
601
|
-
`interceptor ${interceptor.name || index}`,
|
|
602
|
-
() => interceptor(request, { config }),
|
|
603
|
-
);
|
|
604
|
-
if (result instanceof Response) return finish(applyProxyResponse(result, proxyResponse));
|
|
605
|
-
if (result) {
|
|
606
|
-
request = result.request;
|
|
607
|
-
url = new URL(request.url);
|
|
608
|
-
}
|
|
565
|
+
// Standalone chunks a bundler extension supplies (compat: the
|
|
566
|
+
// no-module polyfills chunk). The prod build writes them to disk; dev
|
|
567
|
+
// has no client out-dir for them, so serve the contents directly.
|
|
568
|
+
const staticChunk = maybeStaticClientChunk(config, staticAssetPathname)
|
|
569
|
+
if (staticChunk) {
|
|
570
|
+
setWorkUnitRoute('static-asset', 'static')
|
|
571
|
+
return finish(applyProxyResponse(staticChunk, proxyResponse))
|
|
609
572
|
}
|
|
610
573
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
574
|
+
setWorkUnitRoute('static-asset', 'static')
|
|
575
|
+
return finish(
|
|
576
|
+
applyProxyResponse(
|
|
577
|
+
new Response('Not Found', {
|
|
578
|
+
status: 404,
|
|
579
|
+
headers: { 'content-type': 'text/plain' },
|
|
580
|
+
}),
|
|
581
|
+
proxyResponse,
|
|
582
|
+
),
|
|
583
|
+
)
|
|
584
|
+
}
|
|
618
585
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
586
|
+
// The compat request interceptors run before route matching (registration order: action dispatch -
|
|
587
|
+
// POSTs to a page URL with the action id - then next.config rewrites). A Response short-circuits
|
|
588
|
+
// (wrapped with the proxy response); a `{ request }` swaps the request (a rewrite) and continues.
|
|
589
|
+
// The render keeps the ORIGINAL request URL as canonical; only matching and lookup follow the
|
|
590
|
+
// rewritten url. Pure-core apps register no interceptors.
|
|
591
|
+
const canonicalRequest = request
|
|
592
|
+
const canonicalUrl = new URL(request.url)
|
|
593
|
+
for (const [index, interceptor] of getRequestExtensions().interceptors.entries()) {
|
|
594
|
+
const result = await profileDevStep(
|
|
595
|
+
profile,
|
|
596
|
+
`interceptor ${interceptor.name || index}`,
|
|
597
|
+
() => interceptor(request, { config }),
|
|
598
|
+
)
|
|
599
|
+
if (result instanceof Response) return finish(applyProxyResponse(result, proxyResponse))
|
|
600
|
+
if (result) {
|
|
601
|
+
request = result.request
|
|
602
|
+
url = new URL(request.url)
|
|
632
603
|
}
|
|
604
|
+
}
|
|
633
605
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
setPhase('handler');
|
|
637
|
-
return finish(
|
|
638
|
-
applyProxyResponse(
|
|
639
|
-
await profileDevStep(profile, `handler ${matched.route.route}`, () =>
|
|
640
|
-
handleRoute(
|
|
641
|
-
config,
|
|
642
|
-
matched.route,
|
|
643
|
-
canonicalRequest,
|
|
644
|
-
matched.params,
|
|
645
|
-
devImportVersion,
|
|
646
|
-
),
|
|
647
|
-
),
|
|
648
|
-
proxyResponse,
|
|
649
|
-
),
|
|
650
|
-
);
|
|
651
|
-
}
|
|
606
|
+
const canonicalRedirect = trailingSlashRedirect(config, url, request.method.toUpperCase())
|
|
607
|
+
if (canonicalRedirect) return finish(applyProxyResponse(canonicalRedirect, proxyResponse))
|
|
652
608
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
// still has to receive and parse the document before it asks, and by then the build is running (or,
|
|
660
|
-
// on a warm hit, already memoized). The hydration chunk is a separate fetch on the same terms.
|
|
661
|
-
const eagerClient = !deferClientStage();
|
|
662
|
-
if (eagerClient) preloadDevClient(config, matched.route, profile);
|
|
663
|
-
const preloadAssets = () => {
|
|
664
|
-
preloadDevPageAssets(config, matched.route, profile);
|
|
665
|
-
if (!eagerClient) preloadDevClient(config, matched.route, profile);
|
|
666
|
-
};
|
|
667
|
-
afterResponse = preloadAssets;
|
|
668
|
-
if (!deferAssetPreload()) {
|
|
669
|
-
afterResponse = undefined;
|
|
670
|
-
preloadAssets();
|
|
671
|
-
}
|
|
672
|
-
const layoutFiles = profileDevSyncStep(profile, 'find layouts', () =>
|
|
673
|
-
devLayoutFiles(config, matched.route.file),
|
|
674
|
-
);
|
|
675
|
-
let loaders: Awaited<ReturnType<typeof devRouteModuleLoaders>> | undefined;
|
|
676
|
-
preloadDevClientReferences(config, matched.route, profile);
|
|
677
|
-
try {
|
|
678
|
-
loaders = await profileDevStep(
|
|
679
|
-
profile,
|
|
680
|
-
`route module loaders ${matched.route.route}`,
|
|
681
|
-
() => devRouteModuleLoaders(config, matched.route, layoutFiles),
|
|
682
|
-
);
|
|
683
|
-
} catch (error) {
|
|
684
|
-
logDevRouteLoaderFallback(matched.route, error);
|
|
685
|
-
}
|
|
686
|
-
// Second chance for the font stage: on most apps the fonts are declared
|
|
687
|
-
// by the root LAYOUT, which the route bundle brings in — so this is
|
|
688
|
-
// where they first exist. Memoized, so the earlier call is not repeated.
|
|
689
|
-
void preloadDevFonts(config, profile);
|
|
609
|
+
const nav = parseNavState(request)
|
|
610
|
+
const matched = profileDevSyncStep(profile, 'match route', () =>
|
|
611
|
+
selectRouteForRequest(routes, url.pathname, nav),
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
if (!matched) {
|
|
690
615
|
return finish(
|
|
691
616
|
applyProxyResponse(
|
|
692
|
-
await
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
state: nav,
|
|
717
|
-
childrenPath: matched.childrenPath,
|
|
718
|
-
targetPath: matched.targetPath,
|
|
719
|
-
},
|
|
720
|
-
}
|
|
721
|
-
: {}),
|
|
722
|
-
}),
|
|
617
|
+
await renderGlobalNotFoundResponse({
|
|
618
|
+
config,
|
|
619
|
+
url,
|
|
620
|
+
request: canonicalRequest,
|
|
621
|
+
dev: true,
|
|
622
|
+
devImportVersion,
|
|
623
|
+
}),
|
|
624
|
+
proxyResponse,
|
|
625
|
+
),
|
|
626
|
+
)
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (matched.route.kind === 'handler') {
|
|
630
|
+
setWorkUnitRoute('route-handler')
|
|
631
|
+
setPhase('handler')
|
|
632
|
+
return finish(
|
|
633
|
+
applyProxyResponse(
|
|
634
|
+
await profileDevStep(profile, `handler ${matched.route.route}`, () =>
|
|
635
|
+
handleRoute(
|
|
636
|
+
config,
|
|
637
|
+
matched.route,
|
|
638
|
+
canonicalRequest,
|
|
639
|
+
matched.params,
|
|
640
|
+
devImportVersion,
|
|
723
641
|
),
|
|
724
642
|
),
|
|
725
643
|
proxyResponse,
|
|
726
644
|
),
|
|
727
|
-
)
|
|
645
|
+
)
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
setWorkUnitRoute('html', matched.route.mode === 'static' ? 'static' : 'dynamic')
|
|
649
|
+
pageRequestsSeen++
|
|
650
|
+
noteDevCompileStart(matched.route)
|
|
651
|
+
noteDevRouteServed(config, matched.route)
|
|
652
|
+
// Stylesheets are a SEPARATE browser fetch - the HTML never awaits them - so building them here only
|
|
653
|
+
// takes cores off the import the response is blocked on. Handed to `finish` instead: the browser
|
|
654
|
+
// still has to receive and parse the document before it asks, and by then the build is running (or,
|
|
655
|
+
// on a warm hit, already memoized). The hydration chunk is a separate fetch on the same terms.
|
|
656
|
+
const eagerClient = !deferClientStage()
|
|
657
|
+
if (eagerClient) preloadDevClient(config, matched.route, profile)
|
|
658
|
+
const preloadAssets = () => {
|
|
659
|
+
preloadDevPageAssets(config, matched.route, profile)
|
|
660
|
+
if (!eagerClient) preloadDevClient(config, matched.route, profile)
|
|
661
|
+
}
|
|
662
|
+
afterResponse = preloadAssets
|
|
663
|
+
if (!deferAssetPreload()) {
|
|
664
|
+
afterResponse = undefined
|
|
665
|
+
preloadAssets()
|
|
666
|
+
}
|
|
667
|
+
const layoutFiles = profileDevSyncStep(profile, 'find layouts', () =>
|
|
668
|
+
devLayoutFiles(config, matched.route.file),
|
|
669
|
+
)
|
|
670
|
+
let loaders: Awaited<ReturnType<typeof devRouteModuleLoaders>> | undefined
|
|
671
|
+
preloadDevClientReferences(config, matched.route, profile)
|
|
672
|
+
try {
|
|
673
|
+
loaders = await profileDevStep(profile, `route module loaders ${matched.route.route}`, () =>
|
|
674
|
+
devRouteModuleLoaders(config, matched.route, layoutFiles),
|
|
675
|
+
)
|
|
728
676
|
} catch (error) {
|
|
729
|
-
|
|
730
|
-
// The error funnel (compat classifies + reports) fires from this single
|
|
731
|
-
// catch, then dev rethrows so Bun surfaces its own overlay/500.
|
|
732
|
-
await reportRequestError(
|
|
733
|
-
error,
|
|
734
|
-
{ method: request.method, url: request.url, headers: request.headers },
|
|
735
|
-
{ phase: unit?.phase, routeKind: unit?.routeKind },
|
|
736
|
-
);
|
|
737
|
-
logDevPageError(pageLog);
|
|
738
|
-
flushWorkUnit(unit);
|
|
739
|
-
throw error;
|
|
677
|
+
logDevRouteLoaderFallback(matched.route, error)
|
|
740
678
|
}
|
|
679
|
+
// Second chance for the font stage: on most apps the fonts are declared
|
|
680
|
+
// by the root LAYOUT, which the route bundle brings in — so this is
|
|
681
|
+
// where they first exist. Memoized, so the earlier call is not repeated.
|
|
682
|
+
void preloadDevFonts(config, profile)
|
|
683
|
+
return finish(
|
|
684
|
+
applyProxyResponse(
|
|
685
|
+
await profileDevStep(profile, `render page ${matched.route.route}`, () =>
|
|
686
|
+
withRouteRuntime(matched.route.segmentConfig?.runtime, () =>
|
|
687
|
+
renderPageResponse({
|
|
688
|
+
config,
|
|
689
|
+
route: matched.route,
|
|
690
|
+
params: matched.params,
|
|
691
|
+
// The render keeps the ORIGINAL requested URL as canonical
|
|
692
|
+
// (usePathname/useSearchParams); a rewrite only steered matching.
|
|
693
|
+
url: canonicalUrl,
|
|
694
|
+
// Non-action POSTs to a page render the page (Next's MPA
|
|
695
|
+
// fallback for plain form posts / followed 307s), not a 405.
|
|
696
|
+
request:
|
|
697
|
+
request.method.toUpperCase() === 'POST'
|
|
698
|
+
? new Request(canonicalRequest.url, { headers: canonicalRequest.headers })
|
|
699
|
+
: canonicalRequest,
|
|
700
|
+
dev: true,
|
|
701
|
+
devImportVersion,
|
|
702
|
+
layoutFiles,
|
|
703
|
+
moduleLoader: loaders?.moduleLoader,
|
|
704
|
+
clientModuleLoader: loaders?.clientModuleLoader,
|
|
705
|
+
...(nav
|
|
706
|
+
? {
|
|
707
|
+
nav: {
|
|
708
|
+
soft: true,
|
|
709
|
+
state: nav,
|
|
710
|
+
childrenPath: matched.childrenPath,
|
|
711
|
+
targetPath: matched.targetPath,
|
|
712
|
+
},
|
|
713
|
+
}
|
|
714
|
+
: {}),
|
|
715
|
+
}),
|
|
716
|
+
),
|
|
717
|
+
),
|
|
718
|
+
proxyResponse,
|
|
719
|
+
),
|
|
720
|
+
)
|
|
721
|
+
} catch (error) {
|
|
722
|
+
request.signal.removeEventListener('abort', logAbort)
|
|
723
|
+
// The error funnel (compat classifies + reports) fires from this single
|
|
724
|
+
// catch, then dev rethrows so Bun surfaces its own overlay/500.
|
|
725
|
+
await reportRequestError(
|
|
726
|
+
error,
|
|
727
|
+
{ method: request.method, url: request.url, headers: request.headers },
|
|
728
|
+
{ phase: unit?.phase, routeKind: unit?.routeKind },
|
|
729
|
+
)
|
|
730
|
+
logDevPageError(pageLog)
|
|
731
|
+
flushWorkUnit(unit)
|
|
732
|
+
throw error
|
|
733
|
+
}
|
|
741
734
|
}
|
|
742
735
|
|
|
743
736
|
const warmup = options.warm
|
|
744
737
|
? afterReady(() => warmDev(config, routes)).catch(error => logDevPreloadError('warm', error))
|
|
745
|
-
: undefined
|
|
738
|
+
: undefined
|
|
746
739
|
// Typegen is the one consumer that needs EVERY route's content facts (mode,
|
|
747
740
|
// hydrated), so it runs last and off the critical path — after the entry
|
|
748
741
|
// route's warm compile. The .d.ts is for the editor, not for serving.
|
|
749
|
-
void Promise.resolve(warmup).then(typegenInBackground)
|
|
742
|
+
void Promise.resolve(warmup).then(typegenInBackground)
|
|
750
743
|
|
|
751
|
-
return Object.assign(server, { warmup })
|
|
744
|
+
return Object.assign(server, { warmup })
|
|
752
745
|
}
|
|
753
746
|
|
|
754
747
|
/**
|
|
@@ -757,7 +750,7 @@ export async function startDevServer(options: DevServerOptions) {
|
|
|
757
750
|
* runs after it: warmup can never push the banner out.
|
|
758
751
|
*/
|
|
759
752
|
function afterReady<T>(task: () => Promise<T>) {
|
|
760
|
-
return new Promise<T>(resolve => setTimeout(() => resolve(task()), 0))
|
|
753
|
+
return new Promise<T>(resolve => setTimeout(() => resolve(task()), 0))
|
|
761
754
|
}
|
|
762
755
|
|
|
763
756
|
async function warmDev(config: ResolvedConfig, routes: RouteManifestEntry[]) {
|
|
@@ -765,48 +758,48 @@ async function warmDev(config: ResolvedConfig, routes: RouteManifestEntry[]) {
|
|
|
765
758
|
// plus the compile pipeline's fixed first-use costs. Both are memoized at
|
|
766
759
|
// their source, so a request that beats us here joins the same promises
|
|
767
760
|
// instead of starting a second copy.
|
|
768
|
-
await bootstrapCompat(config, { serve: true })
|
|
769
|
-
installServerRuntimePlugins()
|
|
770
|
-
runRequestWarmHooks(config)
|
|
771
|
-
await warmDevModulePipeline(config)
|
|
772
|
-
await warmDevRoutes(config, routes)
|
|
761
|
+
await bootstrapCompat(config, { serve: true })
|
|
762
|
+
installServerRuntimePlugins()
|
|
763
|
+
runRequestWarmHooks(config)
|
|
764
|
+
await warmDevModulePipeline(config)
|
|
765
|
+
await warmDevRoutes(config, routes)
|
|
773
766
|
}
|
|
774
767
|
|
|
775
768
|
// Warm what this developer actually opens, not `/` (core spec change 6). The
|
|
776
769
|
// routes they hit are recorded next to the compiled cache, so the first boot of
|
|
777
770
|
// a checkout compiles nothing and every later one warms the last routes served —
|
|
778
771
|
// which, with the cache persisted, is usually a disk read rather than a compile.
|
|
779
|
-
const WARM_ROUTES = 3
|
|
772
|
+
const WARM_ROUTES = 3
|
|
780
773
|
|
|
781
774
|
// Page requests served since boot. The boot warmer reads it to stand down once
|
|
782
775
|
// the developer has asked for something concrete (see warmDevRoutes).
|
|
783
|
-
let pageRequestsSeen = 0
|
|
776
|
+
let pageRequestsSeen = 0
|
|
784
777
|
|
|
785
778
|
async function warmDevRoutes(config: ResolvedConfig, routes: RouteManifestEntry[]) {
|
|
786
779
|
const wanted = readDevRouteHistory(config)
|
|
787
780
|
.map(id => routes.find(route => route.id === id && route.kind === 'page'))
|
|
788
781
|
.filter((route): route is RouteManifestEntry => Boolean(route))
|
|
789
|
-
.slice(0, WARM_ROUTES)
|
|
790
|
-
if (wanted.length === 0) return
|
|
782
|
+
.slice(0, WARM_ROUTES)
|
|
783
|
+
if (wanted.length === 0) return
|
|
791
784
|
// Every route is warmed on its own, in history order, and the whole pass stops the moment a real
|
|
792
785
|
// page request arrives: a request is the developer saying which route they want, which beats the
|
|
793
786
|
// history guess outright, and warming a *different* route beside it only steals CPU from the page
|
|
794
787
|
// they are waiting on (a save landing on in-flight warm work invalidates it and the page pays for
|
|
795
788
|
// it twice). The stand-down covers the stylesheet too, not just the route loop: a page request
|
|
796
789
|
// that arrived first is blocked on its own import, and global.css is a build it does not consume.
|
|
797
|
-
if (deferAssetPreload() && pageRequestsSeen > 0) return
|
|
798
|
-
await buildDevAsset(config, '/assets/global.css', () => buildGlobalCss(config, { dev: true }))
|
|
790
|
+
if (deferAssetPreload() && pageRequestsSeen > 0) return
|
|
791
|
+
await buildDevAsset(config, '/assets/global.css', () => buildGlobalCss(config, { dev: true }))
|
|
799
792
|
for (const route of wanted) {
|
|
800
|
-
if (pageRequestsSeen > 0) return
|
|
801
|
-
await warmDevRoute(config, route).catch(() => undefined)
|
|
793
|
+
if (pageRequestsSeen > 0) return
|
|
794
|
+
await warmDevRoute(config, route).catch(() => undefined)
|
|
802
795
|
}
|
|
803
796
|
}
|
|
804
797
|
|
|
805
798
|
async function warmDevRoute(config: ResolvedConfig, route: RouteManifestEntry) {
|
|
806
|
-
noteDevCompileStart(route)
|
|
807
|
-
const start = performance.now()
|
|
808
|
-
const layoutFiles = devLayoutFiles(config, route.file)
|
|
809
|
-
const hasClient = route.client || route.clientReferences.length > 0
|
|
799
|
+
noteDevCompileStart(route)
|
|
800
|
+
const start = performance.now()
|
|
801
|
+
const layoutFiles = devLayoutFiles(config, route.file)
|
|
802
|
+
const hasClient = route.client || route.clientReferences.length > 0
|
|
810
803
|
await Promise.allSettled([
|
|
811
804
|
route.cssImports.length > 0
|
|
812
805
|
? buildDevAsset(config, `/assets/${route.id}.css`, () =>
|
|
@@ -817,8 +810,8 @@ async function warmDevRoute(config: ResolvedConfig, route: RouteManifestEntry) {
|
|
|
817
810
|
hasClient ? buildDevClient(config, route) : Promise.resolve(),
|
|
818
811
|
warmDevClientReferences(config, route),
|
|
819
812
|
warmDevPageModule(config, route),
|
|
820
|
-
])
|
|
821
|
-
noteDevCompileDone(route, performance.now() - start)
|
|
813
|
+
])
|
|
814
|
+
noteDevCompileDone(route, performance.now() - start)
|
|
822
815
|
}
|
|
823
816
|
|
|
824
817
|
/**
|
|
@@ -829,13 +822,13 @@ async function warmDevRoute(config: ResolvedConfig, route: RouteManifestEntry) {
|
|
|
829
822
|
async function warmDevPageModule(config: ResolvedConfig, route: RouteManifestEntry) {
|
|
830
823
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
831
824
|
if (process.env.PNEXT_DEV_WARM_PAGE_MODULE === '0' || !route.client || route.kind !== 'page') {
|
|
832
|
-
return
|
|
825
|
+
return
|
|
833
826
|
}
|
|
834
827
|
const href = await devServerModuleHref(config, route.file, undefined, {
|
|
835
828
|
conditionTarget: serverBundleTargetForRuntime(route.segmentConfig?.runtime),
|
|
836
|
-
})
|
|
837
|
-
await drainPreplanBuilds()
|
|
838
|
-
await import(href)
|
|
829
|
+
})
|
|
830
|
+
await drainPreplanBuilds()
|
|
831
|
+
await import(href)
|
|
839
832
|
}
|
|
840
833
|
|
|
841
834
|
/**
|
|
@@ -846,49 +839,49 @@ async function warmDevPageModule(config: ResolvedConfig, route: RouteManifestEnt
|
|
|
846
839
|
*/
|
|
847
840
|
async function warmDevClientReferences(config: ResolvedConfig, route: RouteManifestEntry) {
|
|
848
841
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
849
|
-
if (process.env.PNEXT_DEV_WARM_CLIENT_REFS === '0') return
|
|
850
|
-
const target = serverBundleTargetForRuntime(route.segmentConfig?.runtime)
|
|
842
|
+
if (process.env.PNEXT_DEV_WARM_CLIENT_REFS === '0') return
|
|
843
|
+
const target = serverBundleTargetForRuntime(route.segmentConfig?.runtime)
|
|
851
844
|
const files = new Set(
|
|
852
845
|
route.clientReferences.filter(ssrClientReference).map(reference => reference.file),
|
|
853
|
-
)
|
|
846
|
+
)
|
|
854
847
|
await Promise.allSettled(
|
|
855
848
|
// The version argument is unused by devClientModuleHref (artifacts are named
|
|
856
849
|
// by content), so there is nothing to keep in sync with the render's call.
|
|
857
850
|
[...files].map(file => devClientModuleHref(config, file, undefined, target)),
|
|
858
|
-
)
|
|
851
|
+
)
|
|
859
852
|
}
|
|
860
853
|
|
|
861
|
-
const HISTORY_LIMIT = 8
|
|
862
|
-
let history: string[] | undefined
|
|
863
|
-
let historyWrite: Timer | undefined
|
|
854
|
+
const HISTORY_LIMIT = 8
|
|
855
|
+
let history: string[] | undefined
|
|
856
|
+
let historyWrite: Timer | undefined
|
|
864
857
|
|
|
865
858
|
function historyFile(config: ResolvedConfig) {
|
|
866
|
-
return path.join(cacheRoot(config.outPath), 'warm.json')
|
|
859
|
+
return path.join(cacheRoot(config.outPath), 'warm.json')
|
|
867
860
|
}
|
|
868
861
|
|
|
869
862
|
function readDevRouteHistory(config: ResolvedConfig): string[] {
|
|
870
|
-
if (history) return history
|
|
863
|
+
if (history) return history
|
|
871
864
|
try {
|
|
872
|
-
const parsed = JSON.parse(readFileSync(historyFile(config), 'utf8')) as unknown
|
|
873
|
-
history = Array.isArray(parsed) ? parsed.filter(id => typeof id === 'string') : []
|
|
865
|
+
const parsed = JSON.parse(readFileSync(historyFile(config), 'utf8')) as unknown
|
|
866
|
+
history = Array.isArray(parsed) ? parsed.filter(id => typeof id === 'string') : []
|
|
874
867
|
} catch {
|
|
875
|
-
history = []
|
|
868
|
+
history = []
|
|
876
869
|
}
|
|
877
|
-
return history
|
|
870
|
+
return history
|
|
878
871
|
}
|
|
879
872
|
|
|
880
873
|
/** Remember a served route, most recent first — the next boot's warm list. */
|
|
881
874
|
function noteDevRouteServed(config: ResolvedConfig, route: RouteManifestEntry) {
|
|
882
|
-
if (route.kind !== 'page') return
|
|
883
|
-
const seen = readDevRouteHistory(config)
|
|
884
|
-
if (seen[0] === route.id) return
|
|
885
|
-
history = [route.id, ...seen.filter(id => id !== route.id)].slice(0, HISTORY_LIMIT)
|
|
886
|
-
if (historyWrite) return
|
|
875
|
+
if (route.kind !== 'page') return
|
|
876
|
+
const seen = readDevRouteHistory(config)
|
|
877
|
+
if (seen[0] === route.id) return
|
|
878
|
+
history = [route.id, ...seen.filter(id => id !== route.id)].slice(0, HISTORY_LIMIT)
|
|
879
|
+
if (historyWrite) return
|
|
887
880
|
historyWrite = setTimeout(() => {
|
|
888
|
-
historyWrite = undefined
|
|
889
|
-
void writeText(historyFile(config), JSON.stringify(history ?? [])).catch(() => undefined)
|
|
890
|
-
}, 500)
|
|
891
|
-
historyWrite.unref?.()
|
|
881
|
+
historyWrite = undefined
|
|
882
|
+
void writeText(historyFile(config), JSON.stringify(history ?? [])).catch(() => undefined)
|
|
883
|
+
}, 500)
|
|
884
|
+
historyWrite.unref?.()
|
|
892
885
|
}
|
|
893
886
|
|
|
894
887
|
function logDevRouteLoaderFallback(route: RouteManifestEntry, error: unknown) {
|
|
@@ -897,21 +890,21 @@ function logDevRouteLoaderFallback(route: RouteManifestEntry, error: unknown) {
|
|
|
897
890
|
`PNext dev route bundle failed for ${route.route}; falling back to per-module loading.`,
|
|
898
891
|
error instanceof Error ? (error.stack ?? error.message) : String(error),
|
|
899
892
|
].join('\n'),
|
|
900
|
-
)
|
|
893
|
+
)
|
|
901
894
|
}
|
|
902
895
|
|
|
903
896
|
interface DevRequestProfile {
|
|
904
|
-
label: string
|
|
905
|
-
start: number
|
|
897
|
+
label: string
|
|
898
|
+
start: number
|
|
906
899
|
}
|
|
907
900
|
|
|
908
901
|
function devRequestProfile(request: Request, url: URL): DevRequestProfile | undefined {
|
|
909
902
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
910
|
-
if (!process.env.PNEXT_DEV_PROFILE) return undefined
|
|
903
|
+
if (!process.env.PNEXT_DEV_PROFILE) return undefined
|
|
911
904
|
return {
|
|
912
905
|
label: `${request.method} ${url.pathname}`,
|
|
913
906
|
start: performance.now(),
|
|
914
|
-
}
|
|
907
|
+
}
|
|
915
908
|
}
|
|
916
909
|
|
|
917
910
|
async function profileDevStep<T>(
|
|
@@ -919,12 +912,12 @@ async function profileDevStep<T>(
|
|
|
919
912
|
label: string,
|
|
920
913
|
task: () => Promise<T>,
|
|
921
914
|
) {
|
|
922
|
-
if (!profile) return task()
|
|
923
|
-
const start = performance.now()
|
|
915
|
+
if (!profile) return task()
|
|
916
|
+
const start = performance.now()
|
|
924
917
|
try {
|
|
925
|
-
return await task()
|
|
918
|
+
return await task()
|
|
926
919
|
} finally {
|
|
927
|
-
logDevProfile(profile, label, start)
|
|
920
|
+
logDevProfile(profile, label, start)
|
|
928
921
|
}
|
|
929
922
|
}
|
|
930
923
|
|
|
@@ -933,22 +926,22 @@ function profileDevSyncStep<T>(
|
|
|
933
926
|
label: string,
|
|
934
927
|
task: () => T,
|
|
935
928
|
) {
|
|
936
|
-
if (!profile) return task()
|
|
937
|
-
const start = performance.now()
|
|
929
|
+
if (!profile) return task()
|
|
930
|
+
const start = performance.now()
|
|
938
931
|
try {
|
|
939
|
-
return task()
|
|
932
|
+
return task()
|
|
940
933
|
} finally {
|
|
941
|
-
logDevProfile(profile, label, start)
|
|
934
|
+
logDevProfile(profile, label, start)
|
|
942
935
|
}
|
|
943
936
|
}
|
|
944
937
|
|
|
945
938
|
function logDevProfile(profile: DevRequestProfile, label: string, start: number) {
|
|
946
|
-
const end = performance.now()
|
|
939
|
+
const end = performance.now()
|
|
947
940
|
// Both offsets, not just the end: stages on this path run as concurrent
|
|
948
941
|
// workflows, and overlap is only readable from where each one STARTED.
|
|
949
942
|
recordDevProfileLine(
|
|
950
943
|
`dev-profile ${profile.label} ${label} in ${formatProfileDuration(end - start)} (@${formatProfileDuration(start - profile.start)}..+${formatProfileDuration(end - profile.start)})`,
|
|
951
|
-
)
|
|
944
|
+
)
|
|
952
945
|
}
|
|
953
946
|
|
|
954
947
|
async function handleRoute(
|
|
@@ -960,22 +953,22 @@ async function handleRoute(
|
|
|
960
953
|
) {
|
|
961
954
|
const routeHref = await devServerModuleHref(config, route.file, devImportVersion, {
|
|
962
955
|
conditionTarget: serverBundleTargetForRuntime(route.segmentConfig?.runtime),
|
|
963
|
-
})
|
|
964
|
-
await drainPreplanBuilds()
|
|
956
|
+
})
|
|
957
|
+
await drainPreplanBuilds()
|
|
965
958
|
const imported = (await import(routeHref)) as RouteHandlerModule &
|
|
966
|
-
Parameters<typeof metadataRouteHandlerModule>[0]
|
|
967
|
-
const module = (metadataRouteHandlerModule(imported, route) ?? imported) as RouteHandlerModule
|
|
959
|
+
Parameters<typeof metadataRouteHandlerModule>[0]
|
|
960
|
+
const module = (metadataRouteHandlerModule(imported, route) ?? imported) as RouteHandlerModule
|
|
968
961
|
return withRouteRuntime(route.segmentConfig?.runtime, () =>
|
|
969
962
|
runWithCacheScope(() => handleRouteModule(module, request, params, { routeFile: route.file })),
|
|
970
|
-
)
|
|
963
|
+
)
|
|
971
964
|
}
|
|
972
965
|
|
|
973
966
|
function routeClientCacheKey(config: ResolvedConfig, route: RouteManifestEntry) {
|
|
974
|
-
const existing = routeCacheKeys.get(route.id)
|
|
975
|
-
if (existing) return existing
|
|
976
|
-
const key = devClientCacheKey(config, route, Boolean(config.compat?.next))
|
|
977
|
-
routeCacheKeys.set(route.id, key)
|
|
978
|
-
return key
|
|
967
|
+
const existing = routeCacheKeys.get(route.id)
|
|
968
|
+
if (existing) return existing
|
|
969
|
+
const key = devClientCacheKey(config, route, Boolean(config.compat?.next))
|
|
970
|
+
routeCacheKeys.set(route.id, key)
|
|
971
|
+
return key
|
|
979
972
|
}
|
|
980
973
|
|
|
981
974
|
function preloadDevClient(
|
|
@@ -983,14 +976,12 @@ function preloadDevClient(
|
|
|
983
976
|
route: RouteManifestEntry,
|
|
984
977
|
profile: DevRequestProfile | undefined,
|
|
985
978
|
) {
|
|
986
|
-
if (!route.client && route.clientReferences.length === 0 && !route.needsRouterEntry) return
|
|
987
|
-
void profileDevStep(profile, `client preload ${route.route}`, () =>
|
|
988
|
-
buildDevClient(config, route),
|
|
989
|
-
)
|
|
979
|
+
if (!route.client && route.clientReferences.length === 0 && !route.needsRouterEntry) return
|
|
980
|
+
void profileDevStep(profile, `client preload ${route.route}`, () => buildDevClient(config, route))
|
|
990
981
|
.catch(error => logDevPreloadError(`client build for ${route.route}`, error))
|
|
991
982
|
.finally(() => {
|
|
992
|
-
if (profile) flushDevProfileLines()
|
|
993
|
-
})
|
|
983
|
+
if (profile) flushDevProfileLines()
|
|
984
|
+
})
|
|
994
985
|
}
|
|
995
986
|
|
|
996
987
|
/**
|
|
@@ -1005,17 +996,17 @@ function preloadDevClientReferences(
|
|
|
1005
996
|
profile: DevRequestProfile | undefined,
|
|
1006
997
|
) {
|
|
1007
998
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
1008
|
-
if (process.env.PNEXT_DEV_PRELOAD_REFS === '0') return
|
|
999
|
+
if (process.env.PNEXT_DEV_PRELOAD_REFS === '0') return
|
|
1009
1000
|
if (route.clientReferences.length > 0) {
|
|
1010
1001
|
void profileDevStep(profile, `client references preload ${route.route}`, () =>
|
|
1011
1002
|
warmDevClientReferences(config, route),
|
|
1012
|
-
).catch(error => logDevPreloadError(`client references for ${route.route}`, error))
|
|
1003
|
+
).catch(error => logDevPreloadError(`client references for ${route.route}`, error))
|
|
1013
1004
|
}
|
|
1014
1005
|
void profileDevStep(profile, `page module preload ${route.route}`, () =>
|
|
1015
1006
|
warmDevPageModule(config, route),
|
|
1016
1007
|
)
|
|
1017
1008
|
.then(() => preloadDevFonts(config, profile))
|
|
1018
|
-
.catch(error => logDevPreloadError(`page module for ${route.route}`, error))
|
|
1009
|
+
.catch(error => logDevPreloadError(`page module for ${route.route}`, error))
|
|
1019
1010
|
}
|
|
1020
1011
|
|
|
1021
1012
|
/**
|
|
@@ -1027,19 +1018,19 @@ function preloadDevClientReferences(
|
|
|
1027
1018
|
function preloadDevFonts(config: ResolvedConfig, profile: DevRequestProfile | undefined) {
|
|
1028
1019
|
return profileDevStep(profile, 'font prewarm', () =>
|
|
1029
1020
|
getFontExtensions().prewarmFontAssets(config, { dev: true }),
|
|
1030
|
-
).catch(error => logDevPreloadError('font prewarm', error))
|
|
1021
|
+
).catch(error => logDevPreloadError('font prewarm', error))
|
|
1031
1022
|
}
|
|
1032
1023
|
|
|
1033
1024
|
/** Bisect seam: `PNEXT_DEV_CLIENT_STAGE=eager` restores the in-request firing. */
|
|
1034
1025
|
function deferClientStage() {
|
|
1035
1026
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
1036
|
-
return process.env.PNEXT_DEV_CLIENT_STAGE !== 'eager'
|
|
1027
|
+
return process.env.PNEXT_DEV_CLIENT_STAGE !== 'eager'
|
|
1037
1028
|
}
|
|
1038
1029
|
|
|
1039
1030
|
/** Bisect seam: `PNEXT_DEV_ASSET_PRELOAD=eager` restores the pre-request firing. */
|
|
1040
1031
|
function deferAssetPreload() {
|
|
1041
1032
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
1042
|
-
return process.env.PNEXT_DEV_ASSET_PRELOAD !== 'eager'
|
|
1033
|
+
return process.env.PNEXT_DEV_ASSET_PRELOAD !== 'eager'
|
|
1043
1034
|
}
|
|
1044
1035
|
|
|
1045
1036
|
function preloadDevPageAssets(
|
|
@@ -1049,23 +1040,23 @@ function preloadDevPageAssets(
|
|
|
1049
1040
|
) {
|
|
1050
1041
|
void profileDevStep(profile, 'asset preload /assets/global.css', () =>
|
|
1051
1042
|
buildDevAsset(config, '/assets/global.css', () => buildGlobalCss(config, { dev: true })),
|
|
1052
|
-
).catch(error => logDevPreloadError('global css build', error))
|
|
1043
|
+
).catch(error => logDevPreloadError('global css build', error))
|
|
1053
1044
|
|
|
1054
|
-
if (route.cssImports.length === 0) return
|
|
1045
|
+
if (route.cssImports.length === 0) return
|
|
1055
1046
|
void profileDevStep(profile, `asset preload /assets/${route.id}.css`, () =>
|
|
1056
1047
|
buildDevAsset(config, `/assets/${route.id}.css`, () =>
|
|
1057
1048
|
buildRouteCss(config, route, { dev: true }),
|
|
1058
1049
|
),
|
|
1059
|
-
).catch(error => logDevPreloadError(`route css build for ${route.route}`, error))
|
|
1050
|
+
).catch(error => logDevPreloadError(`route css build for ${route.route}`, error))
|
|
1060
1051
|
}
|
|
1061
1052
|
|
|
1062
1053
|
/** The route's existing layout chain, memoized until a structural save. */
|
|
1063
1054
|
function devLayoutFiles(config: ResolvedConfig, routeFile: string) {
|
|
1064
|
-
const cached = routeLayoutFiles.get(routeFile)
|
|
1065
|
-
if (cached) return cached
|
|
1066
|
-
const files = findLayouts(config.appPath, routeFile).filter(file => existsSync(file))
|
|
1067
|
-
routeLayoutFiles.set(routeFile, files)
|
|
1068
|
-
return files
|
|
1055
|
+
const cached = routeLayoutFiles.get(routeFile)
|
|
1056
|
+
if (cached) return cached
|
|
1057
|
+
const files = findLayouts(config.appPath, routeFile).filter(file => existsSync(file))
|
|
1058
|
+
routeLayoutFiles.set(routeFile, files)
|
|
1059
|
+
return files
|
|
1069
1060
|
}
|
|
1070
1061
|
|
|
1071
1062
|
function logDevPreloadError(label: string, error: unknown) {
|
|
@@ -1074,7 +1065,7 @@ function logDevPreloadError(label: string, error: unknown) {
|
|
|
1074
1065
|
`PNext dev preload failed for ${label}.`,
|
|
1075
1066
|
error instanceof Error ? (error.stack ?? error.message) : String(error),
|
|
1076
1067
|
].join('\n'),
|
|
1077
|
-
)
|
|
1068
|
+
)
|
|
1078
1069
|
}
|
|
1079
1070
|
|
|
1080
1071
|
/**
|
|
@@ -1086,18 +1077,18 @@ function findDeferredDynamicReference(routes: RouteManifestEntry[], id: string)
|
|
|
1086
1077
|
for (const route of routes) {
|
|
1087
1078
|
const reference = route.clientReferences.find(
|
|
1088
1079
|
item => item.id === id && item.dynamic && !ssrClientReference(item),
|
|
1089
|
-
)
|
|
1090
|
-
if (reference) return { route, reference }
|
|
1080
|
+
)
|
|
1081
|
+
if (reference) return { route, reference }
|
|
1091
1082
|
}
|
|
1092
|
-
const registered = deferredDynamicRefById(id)
|
|
1093
|
-
if (registered) return { route: undefined, reference: { id, ...registered } }
|
|
1094
|
-
return undefined
|
|
1083
|
+
const registered = deferredDynamicRefById(id)
|
|
1084
|
+
if (registered) return { route: undefined, reference: { id, ...registered } }
|
|
1085
|
+
return undefined
|
|
1095
1086
|
}
|
|
1096
1087
|
|
|
1097
1088
|
// On-demand dynamic chunk builds: keyed by (id, dev import version) so a save
|
|
1098
1089
|
// invalidates; chunk files index feeds /__pnext/client-dyn/chunks/ requests.
|
|
1099
|
-
const dynChunkBuilds = new Map<string, Promise<string>>()
|
|
1100
|
-
const dynChunkFiles = new Map<string, string>()
|
|
1090
|
+
const dynChunkBuilds = new Map<string, Promise<string>>()
|
|
1091
|
+
const dynChunkFiles = new Map<string, string>()
|
|
1101
1092
|
|
|
1102
1093
|
async function buildDevDynamicChunk(
|
|
1103
1094
|
config: ResolvedConfig,
|
|
@@ -1105,33 +1096,33 @@ async function buildDevDynamicChunk(
|
|
|
1105
1096
|
reference: { id: string; file: string; exportName: string },
|
|
1106
1097
|
devImportVersion: string,
|
|
1107
1098
|
) {
|
|
1108
|
-
const key = `${reference.id}\0${devImportVersion}
|
|
1109
|
-
const existing = dynChunkBuilds.get(key)
|
|
1110
|
-
if (existing) return existing
|
|
1099
|
+
const key = `${reference.id}\0${devImportVersion}`
|
|
1100
|
+
const existing = dynChunkBuilds.get(key)
|
|
1101
|
+
if (existing) return existing
|
|
1111
1102
|
const outDir = path.join(
|
|
1112
1103
|
config.outPath,
|
|
1113
1104
|
'cache',
|
|
1114
1105
|
'client-dyn',
|
|
1115
1106
|
`${reference.id}-${createHash('sha1').update(devImportVersion).digest('hex').slice(0, 8)}`,
|
|
1116
|
-
)
|
|
1107
|
+
)
|
|
1117
1108
|
const build = (async () => {
|
|
1118
|
-
const outfile = path.join(outDir, `${reference.id}.js`)
|
|
1109
|
+
const outfile = path.join(outDir, `${reference.id}.js`)
|
|
1119
1110
|
if (!existsSync(outfile)) {
|
|
1120
|
-
await buildClientDynamicChunk({ config, route, reference, outDir })
|
|
1111
|
+
await buildClientDynamicChunk({ config, route, reference, outDir })
|
|
1121
1112
|
}
|
|
1122
|
-
const chunksDir = path.join(outDir, 'chunks')
|
|
1113
|
+
const chunksDir = path.join(outDir, 'chunks')
|
|
1123
1114
|
if (existsSync(chunksDir)) {
|
|
1124
1115
|
for (const entry of await readdir(chunksDir)) {
|
|
1125
|
-
if (entry.endsWith('.js')) dynChunkFiles.set(entry, path.join(chunksDir, entry))
|
|
1116
|
+
if (entry.endsWith('.js')) dynChunkFiles.set(entry, path.join(chunksDir, entry))
|
|
1126
1117
|
}
|
|
1127
1118
|
}
|
|
1128
|
-
return outfile
|
|
1119
|
+
return outfile
|
|
1129
1120
|
})().catch(error => {
|
|
1130
|
-
dynChunkBuilds.delete(key)
|
|
1131
|
-
throw error
|
|
1132
|
-
})
|
|
1133
|
-
dynChunkBuilds.set(key, build)
|
|
1134
|
-
return build
|
|
1121
|
+
dynChunkBuilds.delete(key)
|
|
1122
|
+
throw error
|
|
1123
|
+
})
|
|
1124
|
+
dynChunkBuilds.set(key, build)
|
|
1125
|
+
return build
|
|
1135
1126
|
}
|
|
1136
1127
|
|
|
1137
1128
|
/** A cached entry never re-ran the pipeline rewrite: recover its chunk-id map. */
|
|
@@ -1139,9 +1130,9 @@ async function loadDeferredDynamicSidecar(outDir: string) {
|
|
|
1139
1130
|
try {
|
|
1140
1131
|
const parsed = JSON.parse(
|
|
1141
1132
|
await readFile(path.join(outDir, DEFERRED_DYNAMIC_SIDECAR), 'utf8'),
|
|
1142
|
-
) as Record<string, { file: string; exportName: string }
|
|
1133
|
+
) as Record<string, { file: string; exportName: string }>
|
|
1143
1134
|
for (const [id, ref] of Object.entries(parsed)) {
|
|
1144
|
-
if (ref?.file && ref.exportName) registerDeferredDynamicRef(id, ref)
|
|
1135
|
+
if (ref?.file && ref.exportName) registerDeferredDynamicRef(id, ref)
|
|
1145
1136
|
}
|
|
1146
1137
|
} catch {
|
|
1147
1138
|
// No sidecar: the entry has no deferred dynamic refs.
|
|
@@ -1149,55 +1140,55 @@ async function loadDeferredDynamicSidecar(outDir: string) {
|
|
|
1149
1140
|
}
|
|
1150
1141
|
|
|
1151
1142
|
async function buildDevClient(config: ResolvedConfig, route: RouteManifestEntry) {
|
|
1152
|
-
const cacheKey = await routeClientCacheKey(config, route)
|
|
1153
|
-
const outDir = path.join(config.outPath, 'cache', 'client', cacheKey)
|
|
1154
|
-
const outFile = path.join(outDir, `${clientEntryName(route)}.js`)
|
|
1143
|
+
const cacheKey = await routeClientCacheKey(config, route)
|
|
1144
|
+
const outDir = path.join(config.outPath, 'cache', 'client', cacheKey)
|
|
1145
|
+
const outFile = path.join(outDir, `${clientEntryName(route)}.js`)
|
|
1155
1146
|
|
|
1156
1147
|
// Dedup in-flight builds first. A single page view fires up to three requests
|
|
1157
1148
|
// for the same route's client bundle — the background preload, the entry, and
|
|
1158
1149
|
// each chunk — and they must share one build. Two esbuild runs into the same
|
|
1159
1150
|
// outDir would race nameSharedClientChunks' rename and fail with ENOENT.
|
|
1160
|
-
const existing = clientBuilds.get(outDir)
|
|
1161
|
-
if (existing) return existing
|
|
1151
|
+
const existing = clientBuilds.get(outDir)
|
|
1152
|
+
if (existing) return existing
|
|
1162
1153
|
|
|
1163
1154
|
if (existsSync(outFile)) {
|
|
1164
1155
|
// Index chunks so chunk requests skip the rebuild fallback — once per outDir,
|
|
1165
1156
|
// not once per request: the dir name is a content key, so its chunk list is
|
|
1166
1157
|
// fixed for as long as it exists.
|
|
1167
1158
|
if (!indexedClientDirs.has(outDir)) {
|
|
1168
|
-
await indexClientChunks(outDir)
|
|
1169
|
-
await loadDeferredDynamicSidecar(outDir)
|
|
1170
|
-
indexedClientDirs.add(outDir)
|
|
1159
|
+
await indexClientChunks(outDir)
|
|
1160
|
+
await loadDeferredDynamicSidecar(outDir)
|
|
1161
|
+
indexedClientDirs.add(outDir)
|
|
1171
1162
|
}
|
|
1172
|
-
return outFile
|
|
1163
|
+
return outFile
|
|
1173
1164
|
}
|
|
1174
1165
|
|
|
1175
1166
|
// Register the promise synchronously (no await between the get above and this
|
|
1176
1167
|
// set) so concurrent callers can't both slip past the dedup check.
|
|
1177
1168
|
const inner = (async () => {
|
|
1178
|
-
await ensureDir(outDir)
|
|
1179
|
-
const file = await buildClientEntry({ config, route, outDir, dev: true })
|
|
1169
|
+
await ensureDir(outDir)
|
|
1170
|
+
const file = await buildClientEntry({ config, route, outDir, dev: true })
|
|
1180
1171
|
// Dedup this generation's chunk bytes against the shared store before serving
|
|
1181
1172
|
//: identical chunks recur across generations, and this is
|
|
1182
1173
|
// the one place all of a generation's output exists before requests read it.
|
|
1183
|
-
await contentAddressDir(outDir, clientChunkStoreDir(config.outPath))
|
|
1184
|
-
await indexClientChunks(outDir)
|
|
1185
|
-
indexedClientDirs.add(outDir)
|
|
1186
|
-
return file
|
|
1187
|
-
})()
|
|
1188
|
-
let unregister: () => void = () => undefined
|
|
1174
|
+
await contentAddressDir(outDir, clientChunkStoreDir(config.outPath))
|
|
1175
|
+
await indexClientChunks(outDir)
|
|
1176
|
+
indexedClientDirs.add(outDir)
|
|
1177
|
+
return file
|
|
1178
|
+
})()
|
|
1179
|
+
let unregister: () => void = () => undefined
|
|
1189
1180
|
const stopSignal = new Promise<never>((_, reject) => {
|
|
1190
|
-
clientBuildStopWaiters.add(reject)
|
|
1191
|
-
unregister = () => void clientBuildStopWaiters.delete(reject)
|
|
1192
|
-
})
|
|
1181
|
+
clientBuildStopWaiters.add(reject)
|
|
1182
|
+
unregister = () => void clientBuildStopWaiters.delete(reject)
|
|
1183
|
+
})
|
|
1193
1184
|
// If the watchdog wins the race, `inner` may settle later with no listener.
|
|
1194
|
-
inner.catch(() => undefined)
|
|
1185
|
+
inner.catch(() => undefined)
|
|
1195
1186
|
const build = Promise.race([inner, stopSignal]).finally(() => {
|
|
1196
|
-
unregister()
|
|
1197
|
-
clientBuilds.delete(outDir)
|
|
1198
|
-
})
|
|
1199
|
-
clientBuilds.set(outDir, build)
|
|
1200
|
-
return build
|
|
1187
|
+
unregister()
|
|
1188
|
+
clientBuilds.delete(outDir)
|
|
1189
|
+
})
|
|
1190
|
+
clientBuilds.set(outDir, build)
|
|
1191
|
+
return build
|
|
1201
1192
|
}
|
|
1202
1193
|
|
|
1203
1194
|
async function maybeDevClientChunk(
|
|
@@ -1205,24 +1196,24 @@ async function maybeDevClientChunk(
|
|
|
1205
1196
|
routes: RouteManifestEntry[],
|
|
1206
1197
|
pathname: string,
|
|
1207
1198
|
) {
|
|
1208
|
-
const chunkMatch = /^\/__pnext\/client\/chunks\/(.+\.js)$/.exec(pathname)
|
|
1209
|
-
if (!chunkMatch?.[1]) return null
|
|
1210
|
-
const cached = clientChunks.get(chunkMatch[1])
|
|
1199
|
+
const chunkMatch = /^\/__pnext\/client\/chunks\/(.+\.js)$/.exec(pathname)
|
|
1200
|
+
if (!chunkMatch?.[1]) return null
|
|
1201
|
+
const cached = clientChunks.get(chunkMatch[1])
|
|
1211
1202
|
if (cached && existsSync(cached)) {
|
|
1212
|
-
return devChunkResponse(await readFile(cached))
|
|
1203
|
+
return devChunkResponse(await readFile(cached))
|
|
1213
1204
|
}
|
|
1214
1205
|
|
|
1215
1206
|
for (const route of routes) {
|
|
1216
|
-
if (!route.client && route.clientReferences.length === 0) continue
|
|
1217
|
-
const cacheKey = await routeClientCacheKey(config, route)
|
|
1218
|
-
const outDir = path.join(config.outPath, 'cache', 'client', cacheKey)
|
|
1219
|
-
await buildDevClient(config, route)
|
|
1220
|
-
const file = path.join(outDir, 'chunks', chunkMatch[1])
|
|
1221
|
-
if (!isInside(path.join(outDir, 'chunks'), file) || !existsSync(file)) continue
|
|
1222
|
-
return devChunkResponse(await readFile(file))
|
|
1207
|
+
if (!route.client && route.clientReferences.length === 0) continue
|
|
1208
|
+
const cacheKey = await routeClientCacheKey(config, route)
|
|
1209
|
+
const outDir = path.join(config.outPath, 'cache', 'client', cacheKey)
|
|
1210
|
+
await buildDevClient(config, route)
|
|
1211
|
+
const file = path.join(outDir, 'chunks', chunkMatch[1])
|
|
1212
|
+
if (!isInside(path.join(outDir, 'chunks'), file) || !existsSync(file)) continue
|
|
1213
|
+
return devChunkResponse(await readFile(file))
|
|
1223
1214
|
}
|
|
1224
1215
|
|
|
1225
|
-
return null
|
|
1216
|
+
return null
|
|
1226
1217
|
}
|
|
1227
1218
|
|
|
1228
1219
|
/**
|
|
@@ -1231,38 +1222,38 @@ async function maybeDevClientChunk(
|
|
|
1231
1222
|
* exists to make cheap - so it needs its own route rather than the built-asset lookup.
|
|
1232
1223
|
*/
|
|
1233
1224
|
function maybePrebuiltRuntime(config: ResolvedConfig, pathname: string) {
|
|
1234
|
-
const match = /^\/__pnext\/runtime\/([0-9a-f]+)\/(.+\.js)$/.exec(pathname)
|
|
1235
|
-
if (!match?.[1] || !match[2]) return null
|
|
1236
|
-
const dir = prebuiltRuntimeDir(config, match[1])
|
|
1237
|
-
const file = path.join(dir, match[2])
|
|
1238
|
-
if (!isInside(dir, file) || !existsSync(file)) return null
|
|
1239
|
-
return devChunkResponse(readFileSync(file))
|
|
1225
|
+
const match = /^\/__pnext\/runtime\/([0-9a-f]+)\/(.+\.js)$/.exec(pathname)
|
|
1226
|
+
if (!match?.[1] || !match[2]) return null
|
|
1227
|
+
const dir = prebuiltRuntimeDir(config, match[1])
|
|
1228
|
+
const file = path.join(dir, match[2])
|
|
1229
|
+
if (!isInside(dir, file) || !existsSync(file)) return null
|
|
1230
|
+
return devChunkResponse(readFileSync(file))
|
|
1240
1231
|
}
|
|
1241
1232
|
|
|
1242
1233
|
/** Serve an extension-supplied standalone chunk by its `/_next/static/chunks/<name>` path. */
|
|
1243
1234
|
function maybeStaticClientChunk(config: ResolvedConfig, staticAssetPathname: string) {
|
|
1244
|
-
const match = /^\/_next\/static\/chunks\/([^/]+\.js)$/.exec(staticAssetPathname)
|
|
1245
|
-
if (!match?.[1]) return null
|
|
1235
|
+
const match = /^\/_next\/static\/chunks\/([^/]+\.js)$/.exec(staticAssetPathname)
|
|
1236
|
+
if (!match?.[1]) return null
|
|
1246
1237
|
const chunk = getBundlerExtensions()
|
|
1247
1238
|
.staticClientChunks(config)
|
|
1248
|
-
.find(item => item.name === match[1])
|
|
1249
|
-
return chunk ? devChunkResponse(chunk.contents) : null
|
|
1239
|
+
.find(item => item.name === match[1])
|
|
1240
|
+
return chunk ? devChunkResponse(chunk.contents) : null
|
|
1250
1241
|
}
|
|
1251
1242
|
|
|
1252
1243
|
async function indexClientChunks(outDir: string) {
|
|
1253
|
-
const chunksDir = path.join(outDir, 'chunks')
|
|
1254
|
-
if (!existsSync(chunksDir)) return
|
|
1244
|
+
const chunksDir = path.join(outDir, 'chunks')
|
|
1245
|
+
if (!existsSync(chunksDir)) return
|
|
1255
1246
|
for (const file of await listFiles(chunksDir)) {
|
|
1256
|
-
if (file.endsWith('.js')) clientChunks.set(path.basename(file), file)
|
|
1247
|
+
if (file.endsWith('.js')) clientChunks.set(path.basename(file), file)
|
|
1257
1248
|
}
|
|
1258
1249
|
}
|
|
1259
1250
|
|
|
1260
1251
|
async function maybeStaticFile(publicPath: string, pathname: string) {
|
|
1261
|
-
const filePath = path.join(publicPath, pathname.replace(/^\/+/, ''))
|
|
1262
|
-
if (!isInside(publicPath, filePath) || !existsSync(filePath)) return null
|
|
1263
|
-
const fileStat = await stat(filePath)
|
|
1264
|
-
if (!fileStat.isFile()) return null
|
|
1265
|
-
return devResponse(await readFile(filePath), contentType(filePath))
|
|
1252
|
+
const filePath = path.join(publicPath, pathname.replace(/^\/+/, ''))
|
|
1253
|
+
if (!isInside(publicPath, filePath) || !existsSync(filePath)) return null
|
|
1254
|
+
const fileStat = await stat(filePath)
|
|
1255
|
+
if (!fileStat.isFile()) return null
|
|
1256
|
+
return devResponse(await readFile(filePath), contentType(filePath))
|
|
1266
1257
|
}
|
|
1267
1258
|
|
|
1268
1259
|
function getNextStaticAssetPathname(
|
|
@@ -1270,30 +1261,30 @@ function getNextStaticAssetPathname(
|
|
|
1270
1261
|
basePath: string | undefined,
|
|
1271
1262
|
assetPrefix: string | undefined,
|
|
1272
1263
|
) {
|
|
1273
|
-
if (pathname.startsWith('/_next/static/')) return pathname
|
|
1274
|
-
const normalizedBasePath = normalizePathPrefix(basePath)
|
|
1264
|
+
if (pathname.startsWith('/_next/static/')) return pathname
|
|
1265
|
+
const normalizedBasePath = normalizePathPrefix(basePath)
|
|
1275
1266
|
if (normalizedBasePath && pathname.startsWith(`${normalizedBasePath}/_next/static/`)) {
|
|
1276
|
-
return pathname.slice(normalizedBasePath.length)
|
|
1267
|
+
return pathname.slice(normalizedBasePath.length)
|
|
1277
1268
|
}
|
|
1278
|
-
const normalizedAssetPrefix = normalizePathPrefix(assetPrefix)
|
|
1269
|
+
const normalizedAssetPrefix = normalizePathPrefix(assetPrefix)
|
|
1279
1270
|
if (normalizedAssetPrefix && pathname.startsWith(`${normalizedAssetPrefix}/_next/static/`)) {
|
|
1280
|
-
return pathname.slice(normalizedAssetPrefix.length)
|
|
1271
|
+
return pathname.slice(normalizedAssetPrefix.length)
|
|
1281
1272
|
}
|
|
1282
|
-
return null
|
|
1273
|
+
return null
|
|
1283
1274
|
}
|
|
1284
1275
|
|
|
1285
1276
|
function normalizePathPrefix(prefix: string | undefined): string | null {
|
|
1286
|
-
if (!prefix) return null
|
|
1287
|
-
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(prefix)) return null
|
|
1288
|
-
const normalized = `/${prefix.replace(/^\/+|\/+$/g, '')}
|
|
1289
|
-
if (normalized === '/') return null
|
|
1290
|
-
return normalized
|
|
1277
|
+
if (!prefix) return null
|
|
1278
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(prefix)) return null
|
|
1279
|
+
const normalized = `/${prefix.replace(/^\/+|\/+$/g, '')}`
|
|
1280
|
+
if (normalized === '/') return null
|
|
1281
|
+
return normalized
|
|
1291
1282
|
}
|
|
1292
1283
|
|
|
1293
1284
|
function findClientReferenceCss(routes: RouteManifestEntry[], id: string) {
|
|
1294
1285
|
return routes
|
|
1295
1286
|
.flatMap(route => route.clientReferences)
|
|
1296
|
-
.find(reference => reference.id === id && reference.cssImports?.length)
|
|
1287
|
+
.find(reference => reference.id === id && reference.cssImports?.length)
|
|
1297
1288
|
}
|
|
1298
1289
|
|
|
1299
1290
|
async function maybeBuiltAsset(
|
|
@@ -1301,50 +1292,50 @@ async function maybeBuiltAsset(
|
|
|
1301
1292
|
routes: RouteManifestEntry[],
|
|
1302
1293
|
pathname: string,
|
|
1303
1294
|
) {
|
|
1304
|
-
if (!pathname.startsWith('/assets/')) return null
|
|
1295
|
+
if (!pathname.startsWith('/assets/')) return null
|
|
1305
1296
|
if (pathname === '/assets/global.css') {
|
|
1306
|
-
await buildDevAsset(config, pathname, () => buildGlobalCss(config, { dev: true }))
|
|
1297
|
+
await buildDevAsset(config, pathname, () => buildGlobalCss(config, { dev: true }))
|
|
1307
1298
|
} else {
|
|
1308
|
-
const cssMatch = /^\/assets\/(.+)\.css$/.exec(pathname)
|
|
1309
|
-
const route = cssMatch?.[1] ? routes.find(item => item.id === cssMatch[1]) : undefined
|
|
1299
|
+
const cssMatch = /^\/assets\/(.+)\.css$/.exec(pathname)
|
|
1300
|
+
const route = cssMatch?.[1] ? routes.find(item => item.id === cssMatch[1]) : undefined
|
|
1310
1301
|
// Island CSS is only ever requested for a route that already rendered, so
|
|
1311
1302
|
// the resolved routes are searched first — scanning the rest would compile
|
|
1312
1303
|
// the whole app to answer one stylesheet request.
|
|
1313
1304
|
const reference =
|
|
1314
1305
|
!route && cssMatch?.[1]
|
|
1315
|
-
? findClientReferenceCss(routes.filter(routeFactsResolved), cssMatch[1]) ??
|
|
1316
|
-
findClientReferenceCss(routes, cssMatch[1])
|
|
1317
|
-
: undefined
|
|
1306
|
+
? (findClientReferenceCss(routes.filter(routeFactsResolved), cssMatch[1]) ??
|
|
1307
|
+
findClientReferenceCss(routes, cssMatch[1]))
|
|
1308
|
+
: undefined
|
|
1318
1309
|
if (route) {
|
|
1319
|
-
await buildDevAsset(config, pathname, () => buildRouteCss(config, route, { dev: true }))
|
|
1310
|
+
await buildDevAsset(config, pathname, () => buildRouteCss(config, route, { dev: true }))
|
|
1320
1311
|
} else if (reference) {
|
|
1321
1312
|
await buildDevAsset(config, pathname, () =>
|
|
1322
1313
|
buildClientReferenceCss(config, reference, { dev: true }),
|
|
1323
|
-
)
|
|
1314
|
+
)
|
|
1324
1315
|
}
|
|
1325
1316
|
}
|
|
1326
1317
|
|
|
1327
|
-
const outPath = config.outPath
|
|
1328
|
-
const filePath = path.join(outPath, 'cache', pathname.replace(/^\/+/, ''))
|
|
1329
|
-
if (!isInside(path.join(outPath, 'cache'), filePath) || !existsSync(filePath)) return null
|
|
1330
|
-
const fileStat = await stat(filePath)
|
|
1331
|
-
if (!fileStat.isFile()) return null
|
|
1318
|
+
const outPath = config.outPath
|
|
1319
|
+
const filePath = path.join(outPath, 'cache', pathname.replace(/^\/+/, ''))
|
|
1320
|
+
if (!isInside(path.join(outPath, 'cache'), filePath) || !existsSync(filePath)) return null
|
|
1321
|
+
const fileStat = await stat(filePath)
|
|
1322
|
+
if (!fileStat.isFile()) return null
|
|
1332
1323
|
return new Response(await readFile(filePath), {
|
|
1333
1324
|
headers: { 'content-type': contentType(filePath) },
|
|
1334
|
-
})
|
|
1325
|
+
})
|
|
1335
1326
|
}
|
|
1336
1327
|
|
|
1337
1328
|
function buildDevAsset(config: ResolvedConfig, pathname: string, build: () => Promise<unknown>) {
|
|
1338
|
-
const key = appKey(config.outPath, pathname)
|
|
1339
|
-
const existing = assetBuilds.get(key)
|
|
1340
|
-
if (existing) return existing
|
|
1329
|
+
const key = appKey(config.outPath, pathname)
|
|
1330
|
+
const existing = assetBuilds.get(key)
|
|
1331
|
+
if (existing) return existing
|
|
1341
1332
|
// Build each asset once per dev version (reload() clears it); drop on failure to allow retry.
|
|
1342
1333
|
const next = build().catch(error => {
|
|
1343
|
-
assetBuilds.delete(key)
|
|
1344
|
-
throw error
|
|
1345
|
-
})
|
|
1346
|
-
assetBuilds.set(key, next)
|
|
1347
|
-
return next
|
|
1334
|
+
assetBuilds.delete(key)
|
|
1335
|
+
throw error
|
|
1336
|
+
})
|
|
1337
|
+
assetBuilds.set(key, next)
|
|
1338
|
+
return next
|
|
1348
1339
|
}
|
|
1349
1340
|
|
|
1350
1341
|
function maybeDevPagePrefetchResponse(
|
|
@@ -1352,25 +1343,25 @@ function maybeDevPagePrefetchResponse(
|
|
|
1352
1343
|
pathname: string,
|
|
1353
1344
|
request: Request,
|
|
1354
1345
|
) {
|
|
1355
|
-
if (!isDevPagePrefetchRequest(request)) return null
|
|
1356
|
-
const matched = matchRoute(routes, pathname)
|
|
1357
|
-
if (matched?.route.kind !== 'page') return null
|
|
1358
|
-
return devPagePrefetchResponse()
|
|
1346
|
+
if (!isDevPagePrefetchRequest(request)) return null
|
|
1347
|
+
const matched = matchRoute(routes, pathname)
|
|
1348
|
+
if (matched?.route.kind !== 'page') return null
|
|
1349
|
+
return devPagePrefetchResponse()
|
|
1359
1350
|
}
|
|
1360
1351
|
|
|
1361
1352
|
export function isDevPagePrefetchRequest(request: Request) {
|
|
1362
|
-
if (request.method.toUpperCase() !== 'GET') return false
|
|
1353
|
+
if (request.method.toUpperCase() !== 'GET') return false
|
|
1363
1354
|
return (
|
|
1364
1355
|
headerHasPrefetchToken(request.headers.get('sec-purpose')) ||
|
|
1365
1356
|
headerHasPrefetchToken(request.headers.get('purpose')) ||
|
|
1366
1357
|
getRouterProtocolExtensions()
|
|
1367
1358
|
.prefetchRequestHeaders()
|
|
1368
1359
|
.some(header => request.headers.get(header) === '1')
|
|
1369
|
-
)
|
|
1360
|
+
)
|
|
1370
1361
|
}
|
|
1371
1362
|
|
|
1372
1363
|
function headerHasPrefetchToken(value: string | null) {
|
|
1373
|
-
return Boolean(value?.split(/[;,\s]+/).some(token => token.toLowerCase() === 'prefetch'))
|
|
1364
|
+
return Boolean(value?.split(/[;,\s]+/).some(token => token.toLowerCase() === 'prefetch'))
|
|
1374
1365
|
}
|
|
1375
1366
|
|
|
1376
1367
|
function devPagePrefetchResponse() {
|
|
@@ -1380,22 +1371,22 @@ function devPagePrefetchResponse() {
|
|
|
1380
1371
|
'cache-control': 'no-store',
|
|
1381
1372
|
'x-pnext-dev-prefetch': 'skipped',
|
|
1382
1373
|
},
|
|
1383
|
-
})
|
|
1374
|
+
})
|
|
1384
1375
|
}
|
|
1385
1376
|
|
|
1386
1377
|
interface PendingDevPageLoadLog {
|
|
1387
|
-
method: string
|
|
1388
|
-
pathname: string
|
|
1389
|
-
route: string
|
|
1390
|
-
start: number
|
|
1378
|
+
method: string
|
|
1379
|
+
pathname: string
|
|
1380
|
+
route: string
|
|
1381
|
+
start: number
|
|
1391
1382
|
}
|
|
1392
1383
|
|
|
1393
1384
|
interface DevPageLoadLog {
|
|
1394
|
-
method: string
|
|
1395
|
-
pathname: string
|
|
1396
|
-
route: string
|
|
1397
|
-
status: number
|
|
1398
|
-
durationMs: number
|
|
1385
|
+
method: string
|
|
1386
|
+
pathname: string
|
|
1387
|
+
route: string
|
|
1388
|
+
status: number
|
|
1389
|
+
durationMs: number
|
|
1399
1390
|
}
|
|
1400
1391
|
|
|
1401
1392
|
function pendingDevPageLoadLog(
|
|
@@ -1404,57 +1395,57 @@ function pendingDevPageLoadLog(
|
|
|
1404
1395
|
url: URL,
|
|
1405
1396
|
start = performance.now(),
|
|
1406
1397
|
): PendingDevPageLoadLog | undefined {
|
|
1407
|
-
const matched = matchRoute(routes, url.pathname)
|
|
1408
|
-
if (matched?.route.kind !== 'page') return undefined
|
|
1398
|
+
const matched = matchRoute(routes, url.pathname)
|
|
1399
|
+
if (matched?.route.kind !== 'page') return undefined
|
|
1409
1400
|
return {
|
|
1410
1401
|
method: request.method,
|
|
1411
1402
|
pathname: url.pathname,
|
|
1412
1403
|
route: matched.route.route,
|
|
1413
1404
|
start,
|
|
1414
|
-
}
|
|
1405
|
+
}
|
|
1415
1406
|
}
|
|
1416
1407
|
|
|
1417
1408
|
function logDevPageResponse(pending: PendingDevPageLoadLog | undefined, response: Response) {
|
|
1418
|
-
if (!pending) return response
|
|
1409
|
+
if (!pending) return response
|
|
1419
1410
|
logDevPageLoad({
|
|
1420
1411
|
method: pending.method,
|
|
1421
1412
|
pathname: pending.pathname,
|
|
1422
1413
|
route: pending.route,
|
|
1423
1414
|
status: response.status,
|
|
1424
1415
|
durationMs: performance.now() - pending.start,
|
|
1425
|
-
})
|
|
1426
|
-
return response
|
|
1416
|
+
})
|
|
1417
|
+
return response
|
|
1427
1418
|
}
|
|
1428
1419
|
|
|
1429
1420
|
function logDevPageError(pending: PendingDevPageLoadLog | undefined) {
|
|
1430
|
-
if (!pending) return
|
|
1421
|
+
if (!pending) return
|
|
1431
1422
|
logDevPageLoad({
|
|
1432
1423
|
method: pending.method,
|
|
1433
1424
|
pathname: pending.pathname,
|
|
1434
1425
|
route: pending.route,
|
|
1435
1426
|
status: 500,
|
|
1436
1427
|
durationMs: performance.now() - pending.start,
|
|
1437
|
-
})
|
|
1428
|
+
})
|
|
1438
1429
|
}
|
|
1439
1430
|
|
|
1440
1431
|
function logDevPageAbort(pending: PendingDevPageLoadLog | undefined) {
|
|
1441
|
-
if (!pending) return
|
|
1432
|
+
if (!pending) return
|
|
1442
1433
|
logDevPageLoad({
|
|
1443
1434
|
method: pending.method,
|
|
1444
1435
|
pathname: pending.pathname,
|
|
1445
1436
|
route: pending.route,
|
|
1446
1437
|
status: 499,
|
|
1447
1438
|
durationMs: performance.now() - pending.start,
|
|
1448
|
-
})
|
|
1439
|
+
})
|
|
1449
1440
|
}
|
|
1450
1441
|
|
|
1451
1442
|
// Print a "Compiling" banner the first time a route is hit in this process,
|
|
1452
1443
|
// so the several-second cold esbuild + Tailwind build isn't a silent stall. The
|
|
1453
1444
|
// timed `page GET ... in Xs` line follows once the response resolves.
|
|
1454
1445
|
function noteDevCompileStart(route: RouteManifestEntry) {
|
|
1455
|
-
if (compiledRoutes.has(route.file)) return
|
|
1456
|
-
compiledRoutes.add(route.file)
|
|
1457
|
-
console.log(`${cyan('○')} ${dim('Compiling')} ${cyan(route.route)} ${dim('...')}`)
|
|
1446
|
+
if (compiledRoutes.has(route.file)) return
|
|
1447
|
+
compiledRoutes.add(route.file)
|
|
1448
|
+
console.log(`${cyan('○')} ${dim('Compiling')} ${cyan(route.route)} ${dim('...')}`)
|
|
1458
1449
|
}
|
|
1459
1450
|
|
|
1460
1451
|
// Completion line for the warm compile, since no request log fires before the
|
|
@@ -1462,15 +1453,15 @@ function noteDevCompileStart(route: RouteManifestEntry) {
|
|
|
1462
1453
|
function noteDevCompileDone(route: RouteManifestEntry, durationMs: number) {
|
|
1463
1454
|
console.log(
|
|
1464
1455
|
`${green('✓')} ${dim('Compiled')} ${cyan(route.route)} ${dim('in')} ${durationLabel(durationMs)}`,
|
|
1465
|
-
)
|
|
1456
|
+
)
|
|
1466
1457
|
}
|
|
1467
1458
|
|
|
1468
1459
|
function logDevPageLoad(entry: DevPageLoadLog) {
|
|
1469
|
-
console.log(formatDevPageLoadLog(entry))
|
|
1460
|
+
console.log(formatDevPageLoadLog(entry))
|
|
1470
1461
|
}
|
|
1471
1462
|
|
|
1472
1463
|
export function formatDevPageLoadLog(entry: DevPageLoadLog) {
|
|
1473
|
-
const route = entry.route === entry.pathname ? '' : dim(`(${entry.route})`)
|
|
1464
|
+
const route = entry.route === entry.pathname ? '' : dim(`(${entry.route})`)
|
|
1474
1465
|
return [
|
|
1475
1466
|
dim('page'),
|
|
1476
1467
|
dim(entry.method),
|
|
@@ -1481,54 +1472,47 @@ export function formatDevPageLoadLog(entry: DevPageLoadLog) {
|
|
|
1481
1472
|
durationLabel(entry.durationMs),
|
|
1482
1473
|
]
|
|
1483
1474
|
.filter(Boolean)
|
|
1484
|
-
.join(' ')
|
|
1475
|
+
.join(' ')
|
|
1485
1476
|
}
|
|
1486
1477
|
|
|
1487
1478
|
function statusLabel(status: number) {
|
|
1488
|
-
const label = String(status)
|
|
1489
|
-
if (status >= 500) return red(label)
|
|
1490
|
-
if (status >= 400) return yellow(label)
|
|
1491
|
-
if (status >= 300) return cyan(label)
|
|
1492
|
-
return green(label)
|
|
1479
|
+
const label = String(status)
|
|
1480
|
+
if (status >= 500) return red(label)
|
|
1481
|
+
if (status >= 400) return yellow(label)
|
|
1482
|
+
if (status >= 300) return cyan(label)
|
|
1483
|
+
return green(label)
|
|
1493
1484
|
}
|
|
1494
1485
|
|
|
1495
1486
|
function durationLabel(durationMs: number) {
|
|
1496
|
-
const label = formatDuration(durationMs)
|
|
1497
|
-
if (durationMs >= 1000) return yellow(label)
|
|
1498
|
-
if (durationMs >= 250) return cyan(label)
|
|
1499
|
-
return green(label)
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
function formatDuration(durationMs: number) {
|
|
1503
|
-
const ms = Math.max(0, durationMs);
|
|
1504
|
-
if (ms < 10) return `${ms.toFixed(1)}ms`;
|
|
1505
|
-
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
1506
|
-
return `${(ms / 1000).toFixed(ms < 10000 ? 2 : 1)}s`;
|
|
1487
|
+
const label = formatDuration(durationMs)
|
|
1488
|
+
if (durationMs >= 1000) return yellow(label)
|
|
1489
|
+
if (durationMs >= 250) return cyan(label)
|
|
1490
|
+
return green(label)
|
|
1507
1491
|
}
|
|
1508
1492
|
|
|
1509
1493
|
function dim(value: string) {
|
|
1510
|
-
return color(2, 22, value)
|
|
1494
|
+
return color(2, 22, value)
|
|
1511
1495
|
}
|
|
1512
1496
|
|
|
1513
1497
|
function red(value: string) {
|
|
1514
|
-
return color(31, 39, value)
|
|
1498
|
+
return color(31, 39, value)
|
|
1515
1499
|
}
|
|
1516
1500
|
|
|
1517
1501
|
function yellow(value: string) {
|
|
1518
|
-
return color(33, 39, value)
|
|
1502
|
+
return color(33, 39, value)
|
|
1519
1503
|
}
|
|
1520
1504
|
|
|
1521
1505
|
function green(value: string) {
|
|
1522
|
-
return color(32, 39, value)
|
|
1506
|
+
return color(32, 39, value)
|
|
1523
1507
|
}
|
|
1524
1508
|
|
|
1525
1509
|
function cyan(value: string) {
|
|
1526
|
-
return color(36, 39, value)
|
|
1510
|
+
return color(36, 39, value)
|
|
1527
1511
|
}
|
|
1528
1512
|
|
|
1529
1513
|
function color(open: number, close: number, value: string) {
|
|
1530
|
-
if (!process.stdout.isTTY) return value
|
|
1531
|
-
return `\x1b[${open}m${value}\x1b[${close}m
|
|
1514
|
+
if (!process.stdout.isTTY) return value
|
|
1515
|
+
return `\x1b[${open}m${value}\x1b[${close}m`
|
|
1532
1516
|
}
|
|
1533
1517
|
|
|
1534
1518
|
function devResponse(body: BodyInit, contentType: string) {
|
|
@@ -1537,7 +1521,7 @@ function devResponse(body: BodyInit, contentType: string) {
|
|
|
1537
1521
|
'content-type': contentType,
|
|
1538
1522
|
'cache-control': 'no-store',
|
|
1539
1523
|
},
|
|
1540
|
-
})
|
|
1524
|
+
})
|
|
1541
1525
|
}
|
|
1542
1526
|
|
|
1543
1527
|
// Client chunk filenames are content-hashed (chunks/[name]-[hash].js), so they
|
|
@@ -1550,17 +1534,17 @@ function devChunkResponse(body: BodyInit) {
|
|
|
1550
1534
|
'content-type': 'text/javascript; charset=utf-8',
|
|
1551
1535
|
'cache-control': 'public, max-age=31536000, immutable',
|
|
1552
1536
|
},
|
|
1553
|
-
})
|
|
1537
|
+
})
|
|
1554
1538
|
}
|
|
1555
1539
|
|
|
1556
1540
|
function isInside(root: string, file: string) {
|
|
1557
|
-
const relative = path.relative(root, file)
|
|
1558
|
-
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
|
|
1541
|
+
const relative = path.relative(root, file)
|
|
1542
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
|
|
1559
1543
|
}
|
|
1560
1544
|
|
|
1561
1545
|
interface DevWatcher {
|
|
1562
|
-
rootsKey: string
|
|
1563
|
-
stop(): void
|
|
1546
|
+
rootsKey: string
|
|
1547
|
+
stop(): void
|
|
1564
1548
|
}
|
|
1565
1549
|
|
|
1566
1550
|
function refreshWatcher(
|
|
@@ -1571,35 +1555,35 @@ function refreshWatcher(
|
|
|
1571
1555
|
bootTime: number,
|
|
1572
1556
|
onEvent: (file: string) => void,
|
|
1573
1557
|
) {
|
|
1574
|
-
const roots = devWatchRoots(config, routes)
|
|
1575
|
-
const rootsKey = roots.join('\0')
|
|
1576
|
-
if (current?.rootsKey === rootsKey) return current
|
|
1577
|
-
current?.stop()
|
|
1578
|
-
return watchRoots(roots, rootsKey, config.outPath, onChange, bootTime, onEvent)
|
|
1558
|
+
const roots = devWatchRoots(config, routes)
|
|
1559
|
+
const rootsKey = roots.join('\0')
|
|
1560
|
+
if (current?.rootsKey === rootsKey) return current
|
|
1561
|
+
current?.stop()
|
|
1562
|
+
return watchRoots(roots, rootsKey, config.outPath, onChange, bootTime, onEvent)
|
|
1579
1563
|
}
|
|
1580
1564
|
|
|
1581
1565
|
function devWatchRoots(config: ResolvedConfig, routes: RouteManifestEntry[]) {
|
|
1582
|
-
const packageRoots = workspacePackageRoots(config.workspaceRoot)
|
|
1583
|
-
const roots = new Set([config.appPath])
|
|
1566
|
+
const packageRoots = workspacePackageRoots(config.workspaceRoot)
|
|
1567
|
+
const roots = new Set([config.appPath])
|
|
1584
1568
|
|
|
1585
1569
|
// Reading sourceFiles off an unresolved route would scan the whole app at
|
|
1586
1570
|
// boot — exactly what the deferred route table exists to avoid.
|
|
1587
1571
|
for (const route of routes.filter(routeFactsResolved)) {
|
|
1588
1572
|
for (const file of route.sourceFiles) {
|
|
1589
|
-
if (isInside(config.appPath, file)) continue
|
|
1590
|
-
const packageRoot = packageRoots.find(root => isInside(root, file))
|
|
1591
|
-
if (packageRoot) roots.add(packageWatchRoot(packageRoot, file))
|
|
1573
|
+
if (isInside(config.appPath, file)) continue
|
|
1574
|
+
const packageRoot = packageRoots.find(root => isInside(root, file))
|
|
1575
|
+
if (packageRoot) roots.add(packageWatchRoot(packageRoot, file))
|
|
1592
1576
|
else {
|
|
1593
1577
|
// An app that is not a declared workspace member has no package root,
|
|
1594
1578
|
// which used to leave its own components/, lib/ and styles/ unwatched:
|
|
1595
1579
|
// the server served fresh content but the open page was never told.
|
|
1596
|
-
const projectRoot = projectWatchRoot(config.root, file)
|
|
1597
|
-
if (projectRoot) roots.add(projectRoot)
|
|
1580
|
+
const projectRoot = projectWatchRoot(config.root, file)
|
|
1581
|
+
if (projectRoot) roots.add(projectRoot)
|
|
1598
1582
|
}
|
|
1599
1583
|
}
|
|
1600
1584
|
}
|
|
1601
1585
|
|
|
1602
|
-
return [...roots].sort()
|
|
1586
|
+
return [...roots].sort()
|
|
1603
1587
|
}
|
|
1604
1588
|
|
|
1605
1589
|
/**
|
|
@@ -1608,18 +1592,18 @@ function devWatchRoots(config: ResolvedConfig, routes: RouteManifestEntry[]) {
|
|
|
1608
1592
|
* directories and files sitting directly in the root are not covered.
|
|
1609
1593
|
*/
|
|
1610
1594
|
function projectWatchRoot(root: string, file: string) {
|
|
1611
|
-
if (!isInside(root, file)) return undefined
|
|
1612
|
-
const [segment, ...rest] = path.relative(root, file).split(path.sep)
|
|
1613
|
-
if (!segment || rest.length === 0) return undefined
|
|
1614
|
-
if (segment === 'node_modules' || segment.startsWith('.')) return undefined
|
|
1615
|
-
return path.join(root, segment)
|
|
1595
|
+
if (!isInside(root, file)) return undefined
|
|
1596
|
+
const [segment, ...rest] = path.relative(root, file).split(path.sep)
|
|
1597
|
+
if (!segment || rest.length === 0) return undefined
|
|
1598
|
+
if (segment === 'node_modules' || segment.startsWith('.')) return undefined
|
|
1599
|
+
return path.join(root, segment)
|
|
1616
1600
|
}
|
|
1617
1601
|
|
|
1618
1602
|
function packageWatchRoot(packageRoot: string, file: string) {
|
|
1619
|
-
const relative = path.relative(packageRoot, file)
|
|
1620
|
-
const [firstSegment] = relative.split(path.sep)
|
|
1621
|
-
if (!firstSegment || firstSegment === '..' || path.isAbsolute(relative)) return packageRoot
|
|
1622
|
-
return path.join(packageRoot, firstSegment)
|
|
1603
|
+
const relative = path.relative(packageRoot, file)
|
|
1604
|
+
const [firstSegment] = relative.split(path.sep)
|
|
1605
|
+
if (!firstSegment || firstSegment === '..' || path.isAbsolute(relative)) return packageRoot
|
|
1606
|
+
return path.join(packageRoot, firstSegment)
|
|
1623
1607
|
}
|
|
1624
1608
|
|
|
1625
1609
|
/**
|
|
@@ -1632,19 +1616,19 @@ function isCssOnlyChange(files: string[]) {
|
|
|
1632
1616
|
return (
|
|
1633
1617
|
files.length > 0 &&
|
|
1634
1618
|
files.every(file => isCssFile(file) && !/\.module\.[^.]+$/.test(file) && existsSync(file))
|
|
1635
|
-
)
|
|
1619
|
+
)
|
|
1636
1620
|
}
|
|
1637
1621
|
|
|
1638
1622
|
/** What a watcher burst touched. */
|
|
1639
1623
|
interface DevChange {
|
|
1640
|
-
files: string[]
|
|
1624
|
+
files: string[]
|
|
1641
1625
|
/** At least one event was a create/delete/rename (an atomic save is one too). */
|
|
1642
|
-
renamed: boolean
|
|
1626
|
+
renamed: boolean
|
|
1643
1627
|
}
|
|
1644
1628
|
|
|
1645
1629
|
// Once any watch root fails (recursive watch unsupported), the graph goes back
|
|
1646
1630
|
// to re-stating on every request for the life of the process.
|
|
1647
|
-
let recursiveWatchBroken = false
|
|
1631
|
+
let recursiveWatchBroken = false
|
|
1648
1632
|
|
|
1649
1633
|
function watchRoots(
|
|
1650
1634
|
roots: string[],
|
|
@@ -1654,73 +1638,73 @@ function watchRoots(
|
|
|
1654
1638
|
bootTime: number,
|
|
1655
1639
|
onEvent: (file: string) => void,
|
|
1656
1640
|
): DevWatcher {
|
|
1657
|
-
const controller = new AbortController()
|
|
1658
|
-
let pending: Timer | undefined
|
|
1659
|
-
let running = false
|
|
1660
|
-
let batch: DevChange = { files: [], renamed: false }
|
|
1641
|
+
const controller = new AbortController()
|
|
1642
|
+
let pending: Timer | undefined
|
|
1643
|
+
let running = false
|
|
1644
|
+
let batch: DevChange = { files: [], renamed: false }
|
|
1661
1645
|
|
|
1662
1646
|
async function flush() {
|
|
1663
|
-
if (running || batch.files.length === 0) return
|
|
1664
|
-
const change = batch
|
|
1665
|
-
batch = { files: [], renamed: false }
|
|
1666
|
-
running = true
|
|
1647
|
+
if (running || batch.files.length === 0) return
|
|
1648
|
+
const change = batch
|
|
1649
|
+
batch = { files: [], renamed: false }
|
|
1650
|
+
running = true
|
|
1667
1651
|
try {
|
|
1668
|
-
await onChange(change)
|
|
1652
|
+
await onChange(change)
|
|
1669
1653
|
} finally {
|
|
1670
|
-
running = false
|
|
1654
|
+
running = false
|
|
1671
1655
|
}
|
|
1672
|
-
if (batch.files.length > 0) await flush()
|
|
1656
|
+
if (batch.files.length > 0) await flush()
|
|
1673
1657
|
}
|
|
1674
1658
|
|
|
1675
1659
|
function schedule(file: string, renamed: boolean) {
|
|
1676
|
-
if (predatesBoot(file, bootTime)) return
|
|
1677
|
-
onEvent(file)
|
|
1678
|
-
batch.files.push(file)
|
|
1679
|
-
batch.renamed ||= renamed
|
|
1680
|
-
if (pending) clearTimeout(pending)
|
|
1660
|
+
if (predatesBoot(file, bootTime)) return
|
|
1661
|
+
onEvent(file)
|
|
1662
|
+
batch.files.push(file)
|
|
1663
|
+
batch.renamed ||= renamed
|
|
1664
|
+
if (pending) clearTimeout(pending)
|
|
1681
1665
|
// The coalescing window exists so one save that lands as several events costs one recompile. A
|
|
1682
1666
|
// stylesheet-only burst has no recompile to coalesce, so it waits a tenth as long; a JS file
|
|
1683
1667
|
// arriving late just triggers the ordinary reload behind it.
|
|
1684
1668
|
pending = setTimeout(
|
|
1685
1669
|
() => {
|
|
1686
|
-
pending = undefined
|
|
1687
|
-
void flush()
|
|
1670
|
+
pending = undefined
|
|
1671
|
+
void flush()
|
|
1688
1672
|
},
|
|
1689
1673
|
isCssOnlyChange(batch.files) ? 4 : 40,
|
|
1690
|
-
)
|
|
1674
|
+
)
|
|
1691
1675
|
}
|
|
1692
1676
|
|
|
1693
1677
|
for (const root of roots) {
|
|
1694
1678
|
void watchRoot(root, controller.signal, schedule, () => {
|
|
1695
|
-
recursiveWatchBroken = true
|
|
1696
|
-
setDevWatcherFreshness(outPath, false)
|
|
1697
|
-
})
|
|
1679
|
+
recursiveWatchBroken = true
|
|
1680
|
+
setDevWatcherFreshness(outPath, false)
|
|
1681
|
+
})
|
|
1698
1682
|
}
|
|
1699
|
-
if (!recursiveWatchBroken) setDevWatcherFreshness(outPath, true)
|
|
1683
|
+
if (!recursiveWatchBroken) setDevWatcherFreshness(outPath, true)
|
|
1700
1684
|
|
|
1701
1685
|
return {
|
|
1702
1686
|
rootsKey,
|
|
1703
1687
|
stop() {
|
|
1704
|
-
if (pending) clearTimeout(pending)
|
|
1705
|
-
controller.abort()
|
|
1688
|
+
if (pending) clearTimeout(pending)
|
|
1689
|
+
controller.abort()
|
|
1706
1690
|
},
|
|
1707
|
-
}
|
|
1691
|
+
}
|
|
1708
1692
|
}
|
|
1709
1693
|
|
|
1710
1694
|
/** A replayed pre-boot event: still on disk, unchanged since before the server read it. */
|
|
1711
1695
|
function predatesBoot(file: string, bootTime: number) {
|
|
1712
|
-
const info = statSync(file, { throwIfNoEntry: false })
|
|
1713
|
-
return Boolean(info?.isFile() && info.mtimeMs <= bootTime)
|
|
1696
|
+
const info = statSync(file, { throwIfNoEntry: false })
|
|
1697
|
+
return Boolean(info?.isFile() && info.mtimeMs <= bootTime)
|
|
1714
1698
|
}
|
|
1715
1699
|
|
|
1716
1700
|
// Output and dependency churn is not a save: .pnext (the server's own cache
|
|
1717
1701
|
// persists + typegen land there and used to reload the world on every warm
|
|
1718
1702
|
// request), other dot dirs, and node_modules.
|
|
1719
|
-
const ignoredWatchSegment = /(?:^|[\\/])(?:node_modules|\.[^\\/]+)(?:[\\/]|$)
|
|
1703
|
+
const ignoredWatchSegment = /(?:^|[\\/])(?:node_modules|\.[^\\/]+)(?:[\\/]|$)/
|
|
1720
1704
|
|
|
1721
1705
|
/** @internal Test-only: is this root-relative watch event output/dependency churn? */
|
|
1722
1706
|
export function isIgnoredWatchPath(relativeFile: string) {
|
|
1723
|
-
return ignoredWatchSegment.test(relativeFile)
|
|
1707
|
+
return ignoredWatchSegment.test(relativeFile)
|
|
1724
1708
|
}
|
|
1725
1709
|
|
|
1726
1710
|
async function watchRoot(
|
|
@@ -1730,50 +1714,50 @@ async function watchRoot(
|
|
|
1730
1714
|
onBroken: () => void,
|
|
1731
1715
|
) {
|
|
1732
1716
|
try {
|
|
1733
|
-
const watcher = watch(root, { recursive: true, signal })
|
|
1717
|
+
const watcher = watch(root, { recursive: true, signal })
|
|
1734
1718
|
for await (const event of watcher) {
|
|
1735
1719
|
// No filename (rare, platform-dependent) means we cannot scope the
|
|
1736
1720
|
// invalidation: treat it as structural so everything is re-derived.
|
|
1737
|
-
if (!event.filename) onChange(root, true)
|
|
1721
|
+
if (!event.filename) onChange(root, true)
|
|
1738
1722
|
else if (!ignoredWatchSegment.test(event.filename))
|
|
1739
|
-
onChange(path.resolve(root, event.filename), event.eventType === 'rename')
|
|
1723
|
+
onChange(path.resolve(root, event.filename), event.eventType === 'rename')
|
|
1740
1724
|
}
|
|
1741
1725
|
} catch (error) {
|
|
1742
|
-
if (signal.aborted || (error instanceof Error && error.name === 'AbortError')) return
|
|
1726
|
+
if (signal.aborted || (error instanceof Error && error.name === 'AbortError')) return
|
|
1743
1727
|
// Some platforms do not support recursive watch. Dev still works without
|
|
1744
1728
|
// live reload, but the graph must then re-stat on every request.
|
|
1745
|
-
onBroken()
|
|
1729
|
+
onBroken()
|
|
1746
1730
|
}
|
|
1747
1731
|
}
|
|
1748
1732
|
|
|
1749
1733
|
function eventStream(clients: Set<DevEventClient>, generation: number) {
|
|
1750
|
-
let client: DevEventClient | undefined
|
|
1734
|
+
let client: DevEventClient | undefined
|
|
1751
1735
|
const stream = new ReadableStream<Uint8Array>({
|
|
1752
1736
|
start(controller) {
|
|
1753
|
-
client = { controller }
|
|
1754
|
-
clients.add(client)
|
|
1755
|
-
controller.enqueue(new TextEncoder().encode(`event: ready\ndata: ${generation}\n\n`))
|
|
1737
|
+
client = { controller }
|
|
1738
|
+
clients.add(client)
|
|
1739
|
+
controller.enqueue(new TextEncoder().encode(`event: ready\ndata: ${generation}\n\n`))
|
|
1756
1740
|
},
|
|
1757
1741
|
cancel() {
|
|
1758
|
-
if (client) clients.delete(client)
|
|
1742
|
+
if (client) clients.delete(client)
|
|
1759
1743
|
},
|
|
1760
|
-
})
|
|
1744
|
+
})
|
|
1761
1745
|
return new Response(stream, {
|
|
1762
1746
|
headers: {
|
|
1763
1747
|
'content-type': 'text/event-stream',
|
|
1764
1748
|
'cache-control': 'no-cache',
|
|
1765
1749
|
connection: 'keep-alive',
|
|
1766
1750
|
},
|
|
1767
|
-
})
|
|
1751
|
+
})
|
|
1768
1752
|
}
|
|
1769
1753
|
|
|
1770
1754
|
function broadcast(clients: Set<DevEventClient>, event: string) {
|
|
1771
|
-
const payload = new TextEncoder().encode(`event: ${event}\ndata: ${Date.now()}\n\n`)
|
|
1755
|
+
const payload = new TextEncoder().encode(`event: ${event}\ndata: ${Date.now()}\n\n`)
|
|
1772
1756
|
for (const client of clients) {
|
|
1773
1757
|
try {
|
|
1774
|
-
client.controller.enqueue(payload)
|
|
1758
|
+
client.controller.enqueue(payload)
|
|
1775
1759
|
} catch {
|
|
1776
|
-
clients.delete(client)
|
|
1760
|
+
clients.delete(client)
|
|
1777
1761
|
}
|
|
1778
1762
|
}
|
|
1779
1763
|
}
|