@wular/pnext 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/bin/pnext +1 -1
- package/config/lint/base.js +7 -7
- package/config/ts/base.json +2 -4
- package/config/ts/react.json +2 -6
- package/package.json +23 -2
- package/reference/compat.md +1 -1
- package/reference/config.md +2 -2
- package/reference/css.md +13 -9
- package/reference/dev.md +1 -1
- package/reference/metadata.md +7 -7
- package/reference/navigation.md +35 -28
- package/reference/performance.md +70 -70
- package/reference/rendering.md +17 -17
- package/reference/routing.md +17 -17
- package/reference/typegen.md +12 -8
- package/src/api/cache.ts +36 -37
- package/src/api/client-cache.ts +2 -2
- package/src/api/client-navigation.ts +113 -109
- package/src/api/dynamic.tsx +58 -55
- package/src/api/link.tsx +49 -52
- package/src/api/navigation.ts +59 -58
- package/src/api/server.ts +152 -144
- package/src/api/suspense.ts +4 -4
- package/src/cli/adapters/vercel-warm.ts +126 -121
- package/src/cli/adapters/vercel.ts +437 -443
- package/src/cli/analyze.ts +333 -144
- package/src/cli/{named-bin.ts → boot/named-bin.ts} +37 -37
- package/src/cli/{boot-trace.ts → boot/trace.ts} +10 -10
- package/src/cli/build.ts +976 -978
- package/src/cli/create.ts +71 -63
- package/src/cli/dev.ts +116 -116
- package/src/cli/index.ts +88 -85
- package/src/cli/migrate/package-json.ts +73 -71
- package/src/cli/migrate/report.ts +32 -33
- package/src/cli/migrate/{index.ts → run.ts} +48 -49
- package/src/cli/migrate/scan.ts +46 -46
- package/src/cli/migrate/spinner.ts +9 -9
- package/src/cli/migrate/tsconfig.ts +35 -35
- package/src/cli/{server-entry.ts → serve/entry.ts} +57 -57
- package/src/cli/{request-pipeline.ts → serve/pipeline.ts} +386 -394
- package/src/cli/{serve-ui.ts → serve/ui.ts} +47 -47
- package/src/cli/start.ts +65 -66
- package/src/{typegen.ts → cli/typegen.ts} +59 -59
- package/src/client/build.ts +794 -768
- package/src/client/chunk-fold.ts +245 -240
- package/src/client/{paths.ts → chunk-name.ts} +6 -6
- package/src/client/entry.ts +162 -147
- package/src/client/prebuilt.ts +231 -223
- package/src/client/profile.ts +29 -29
- package/src/client/react-compiler.ts +20 -15
- package/src/client/{compat-surface.ts → react-tier.ts} +64 -64
- package/src/client/reference-stub.ts +51 -51
- package/src/client/reference.ts +19 -19
- package/src/{api → client}/router/events.ts +17 -17
- package/src/{api → client}/router/history.ts +16 -16
- package/src/{api → client}/router/hub.ts +52 -53
- package/src/{api/router.ts → client/router/index.ts} +61 -60
- package/src/{api → client}/router/policies.ts +24 -24
- package/src/{api → client}/router/runtime.ts +1988 -1992
- package/src/{api → client}/router/types.ts +93 -98
- package/src/compat/actions/client-plugin.ts +42 -43
- package/src/compat/actions/client-stub.ts +14 -14
- package/src/compat/actions/{action-client.ts → client.ts} +194 -193
- package/src/compat/actions/config.ts +63 -64
- package/src/compat/actions/detect.ts +68 -68
- package/src/compat/actions/discovery.ts +105 -105
- package/src/compat/actions/{action-dispatch.ts → dispatch.ts} +277 -274
- package/src/compat/actions/early-submit.ts +1 -1
- package/src/compat/actions/endpoint.ts +198 -198
- package/src/compat/actions/extensions.ts +811 -0
- package/src/compat/actions/flight.ts +23 -23
- package/src/compat/actions/form-state.ts +34 -34
- package/src/compat/actions/hoist.ts +382 -236
- package/src/compat/actions/ids.ts +7 -12
- package/src/compat/actions/index.ts +8 -8
- package/src/compat/actions/instances.ts +46 -46
- package/src/compat/actions/origin.ts +47 -48
- package/src/compat/actions/protocol.ts +22 -22
- package/src/compat/actions/registry.ts +19 -19
- package/src/compat/{misc/action-return.ts → actions/return.ts} +60 -57
- package/src/compat/actions/rewrite.ts +125 -125
- package/src/compat/actions/{action-router.ts → router.ts} +10 -10
- package/src/compat/actions/serve.ts +132 -136
- package/src/compat/actions/server-tag.ts +4 -4
- package/src/compat/actions/{action-shared.ts → shared.ts} +34 -34
- package/src/compat/actions/unrecognized-error.ts +4 -4
- package/src/compat/{index.ts → aliases.ts} +112 -109
- package/src/compat/bundler/bun-externals.ts +18 -18
- package/src/compat/bundler/cjs-exports.ts +271 -223
- package/src/compat/bundler/config.ts +116 -111
- package/src/compat/bundler/externals.ts +12 -12
- package/src/compat/bundler/import-meta-url.ts +19 -19
- package/src/compat/bundler/modularize-imports.ts +36 -33
- package/src/compat/bundler/new-url-asset.ts +22 -24
- package/src/compat/bundler/optimize-package-imports.ts +111 -107
- package/src/compat/bundler/polyfill.ts +28 -28
- package/src/compat/bundler/react-compiler.ts +35 -29
- package/src/compat/bundler/react-profiler.tsx +11 -11
- package/src/compat/bundler/relay-transform.ts +48 -47
- package/src/compat/bundler/require-context.ts +119 -113
- package/src/compat/bundler/resolve-extensions.ts +19 -19
- package/src/compat/bundler/source-cache.ts +25 -25
- package/src/compat/bundler/static-imports.ts +7 -7
- package/src/compat/bundler/symlink-imports.ts +46 -46
- package/src/compat/bundler/tsconfig-paths.ts +13 -15
- package/src/compat/bundler/wasm.ts +58 -60
- package/src/compat/bundler/webpack-loaders.ts +254 -241
- package/src/compat/bundler/worker.ts +101 -104
- package/src/compat/cache/build-flags.ts +29 -29
- package/src/compat/cache/build-prerender-errors.ts +40 -44
- package/src/compat/cache/custom-handler.ts +60 -53
- package/src/compat/cache/fetch-patch.ts +240 -240
- package/src/compat/cache/handler.ts +27 -31
- package/src/compat/cache/modern-handler.ts +148 -126
- package/src/compat/cache/resume-data-cache.ts +47 -45
- package/src/compat/cache/revalidate.ts +233 -232
- package/src/compat/cache/runtime-error.ts +35 -35
- package/src/compat/cache/use-cache-transform.ts +442 -417
- package/src/compat/cache/use-cache.ts +650 -614
- package/src/compat/cache-control.ts +140 -142
- package/src/compat/client/base-path.ts +21 -20
- package/src/compat/client/css-order.ts +18 -18
- package/src/compat/client/errors/bare-boundary.ts +11 -11
- package/src/compat/client/errors/control-flow.ts +23 -23
- package/src/compat/client/errors/error-boundary.ts +61 -61
- package/src/compat/client/errors/global-error.ts +101 -91
- package/src/compat/client/errors/install.ts +71 -72
- package/src/compat/client/errors/lazy.ts +23 -23
- package/src/compat/client/errors/primitive-throw.ts +47 -45
- package/src/compat/client/errors/soft-refresh.ts +4 -4
- package/src/compat/client/link-status.ts +33 -33
- package/src/compat/client/{nav-compat-runtime.ts → nav-runtime.ts} +20 -20
- package/src/compat/client/{nav-compat.ts → nav.ts} +12 -13
- package/src/compat/client/navigation-scroll.ts +63 -63
- package/src/compat/client/optimistic-routing.ts +93 -88
- package/src/compat/client/prefetch-cache.ts +23 -24
- package/src/compat/client/route-announcer.ts +35 -35
- package/src/compat/client/segment-cache-policy.ts +20 -20
- package/src/compat/client/segment-cache.ts +309 -315
- package/src/compat/client/segment-prefetch.ts +127 -132
- package/src/compat/client/trailing-slash.ts +5 -4
- package/src/compat/css/chunking.ts +113 -116
- package/src/compat/css/inline-css.ts +21 -21
- package/src/compat/css/lightningcss.ts +37 -38
- package/src/compat/css/modules.ts +161 -175
- package/src/compat/css/nonce.ts +7 -7
- package/src/compat/css/sass-plugin.ts +18 -21
- package/src/compat/css/sass.ts +150 -152
- package/src/compat/css/styled-jsx-runtime.ts +27 -27
- package/src/compat/css/styled-jsx.ts +21 -21
- package/src/compat/edge-runtime.ts +27 -27
- package/src/compat/{adapter → export}/build-complete.ts +72 -75
- package/src/compat/export/client.ts +29 -31
- package/src/compat/export/{index.ts → emit.ts} +111 -110
- package/src/compat/export/standalone.ts +62 -54
- package/src/compat/image-optimizer/cache.ts +51 -49
- package/src/compat/image-optimizer/detect.ts +52 -52
- package/src/compat/image-optimizer/{index.ts → optimize.ts} +189 -194
- package/src/compat/image-optimizer/source.ts +77 -80
- package/src/compat/lifecycle/after-scope.ts +26 -26
- package/src/compat/lifecycle/after.ts +48 -45
- package/src/compat/lifecycle/error-funnel.ts +98 -102
- package/src/compat/lifecycle/error-serialize.ts +30 -26
- package/src/compat/lifecycle/error-ui.ts +67 -32
- package/src/compat/lifecycle/instrumentation-client.ts +36 -38
- package/src/compat/lifecycle/instrumentation.ts +85 -85
- package/src/compat/lifecycle/node-console.ts +11 -11
- package/src/compat/lifecycle/testmode.ts +160 -132
- package/src/compat/mdx/compile.ts +60 -57
- package/src/compat/mdx/plugin.ts +11 -11
- package/src/compat/mdx/{next-mdx-stub.ts → stub.ts} +5 -5
- package/src/compat/{metadata-route-artifacts.ts → metadata-artifacts.ts} +195 -191
- package/src/compat/metadata.ts +75 -81
- package/src/compat/next/cache.ts +68 -69
- package/src/compat/next/canonical-url.ts +9 -9
- package/src/compat/next/client-cache.ts +19 -19
- package/src/compat/next/client-navigation.ts +112 -116
- package/src/compat/next/client-only.ts +1 -1
- package/src/compat/next/client-script.tsx +99 -93
- package/src/compat/next/client-server.ts +10 -10
- package/src/compat/next/config-loader.ts +176 -173
- package/src/compat/next/config.ts +7 -7
- package/src/compat/next/constants.cjs +6 -6
- package/src/compat/next/constants.ts +6 -6
- package/src/compat/next/custom-server.ts +26 -24
- package/src/compat/next/dist/client/components/app-router-headers.ts +21 -21
- package/src/compat/next/dist/server/app-render/work-unit-async-storage.external.cjs +3 -4
- package/src/compat/next/dist/server/web/spec-extension/revalidate.ts +1 -1
- package/src/compat/next/dist/server/web/spec-extension/unstable-cache.ts +1 -1
- package/src/compat/next/dist/server/web/spec-extension/unstable-no-store.ts +1 -1
- package/src/compat/next/dynamic.tsx +21 -18
- package/src/compat/next/error.tsx +52 -52
- package/src/compat/next/font/cache.ts +74 -74
- package/src/compat/next/font/google.ts +2 -2
- package/src/compat/next/font/index.ts +1 -1
- package/src/compat/next/font/local.ts +3 -3
- package/src/compat/next/font/runtime-client.ts +17 -15
- package/src/compat/next/font/runtime.ts +443 -408
- package/src/compat/next/font/shared.ts +120 -108
- package/src/compat/next/form.tsx +63 -63
- package/src/compat/next/head.tsx +2 -2
- package/src/compat/next/headers.ts +103 -95
- package/src/compat/next/image/client.tsx +220 -0
- package/src/compat/next/image/config.ts +56 -58
- package/src/compat/next/image/optimizer.ts +40 -36
- package/src/compat/next/image/patterns.ts +37 -40
- package/src/compat/next/{image-props.ts → image/props.ts} +208 -202
- package/src/compat/next/image/shared.ts +65 -57
- package/src/compat/next/image/static-metadata.ts +98 -107
- package/src/compat/next/image/validate.ts +79 -88
- package/src/compat/next/image.tsx +19 -23
- package/src/compat/next/index.ts +1 -1
- package/src/compat/next/legacy-image.tsx +59 -60
- package/src/compat/next/{link-validation-transform.ts → link-transform.ts} +95 -96
- package/src/compat/next/link.tsx +158 -158
- package/src/compat/next/navigation.cjs +12 -3
- package/src/compat/next/navigation.ts +48 -50
- package/src/compat/next/offline.ts +27 -27
- package/src/compat/next/og.ts +121 -124
- package/src/compat/next/preferred-region.ts +13 -14
- package/src/compat/next/redirects.ts +58 -56
- package/src/compat/next/resource-hints.ts +73 -76
- package/src/compat/next/rewrites.ts +130 -133
- package/src/compat/next/root-params.ts +45 -45
- package/src/compat/next/{optimistic-route-state.ts → route-state.ts} +52 -52
- package/src/compat/next/router.cjs +4 -2
- package/src/compat/next/router.ts +58 -61
- package/src/compat/next/script.tsx +108 -108
- package/src/compat/next/server-only.ts +1 -1
- package/src/compat/next/server.ts +19 -19
- package/src/compat/next/svgr.ts +18 -17
- package/src/compat/next/telemetry.ts +24 -24
- package/src/compat/next/{image-usage.ts → usage.ts} +70 -39
- package/src/compat/next/user-agent.ts +53 -49
- package/src/compat/next/web-vitals.ts +22 -24
- package/src/compat/otel/api.ts +41 -41
- package/src/compat/otel/client-trace-metadata.ts +25 -27
- package/src/compat/otel/fetch-span.ts +29 -29
- package/src/compat/otel/tracer.ts +331 -331
- package/src/compat/pages/api.ts +456 -0
- package/src/compat/pages/client-plugin.ts +36 -36
- package/src/compat/pages/router-state.ts +34 -34
- package/src/compat/pages/{index.ts → router.ts} +130 -135
- package/src/compat/ppr/io.ts +12 -12
- package/src/compat/ppr/missing-root-params.ts +34 -38
- package/src/compat/ppr/root-params-scan.ts +66 -66
- package/src/compat/ppr/root-params-transform.ts +24 -26
- package/src/compat/ppr/root-params.ts +30 -30
- package/src/compat/ppr/segment-config-incompat.ts +6 -7
- package/src/compat/protocol.ts +71 -70
- package/src/compat/react/action-state.ts +47 -48
- package/src/compat/react/client-lite.ts +15 -15
- package/src/compat/react/client.ts +4 -4
- package/src/compat/react/compiler-runtime.ts +11 -11
- package/src/compat/react/dom-client.ts +44 -44
- package/src/compat/react/dom-react-server.ts +10 -16
- package/src/compat/react/dom-server.ts +10 -10
- package/src/compat/react/dom.ts +52 -52
- package/src/compat/react/hooks-extra.ts +34 -35
- package/src/compat/react/parity.ts +64 -61
- package/src/compat/react/preact.ts +81 -82
- package/src/compat/react/react-server.ts +28 -28
- package/src/compat/react/router-shim.ts +1 -1
- package/src/compat/react/server-component-use.ts +8 -8
- package/src/compat/react/server-inserted-html.ts +30 -31
- package/src/compat/react/server.ts +53 -55
- package/src/compat/react/use.ts +32 -32
- package/src/compat/react/view-transition.ts +20 -20
- package/src/compat/register/actions.ts +35 -824
- package/src/compat/register/boot.ts +41 -41
- package/src/compat/register/build-tier.ts +5 -5
- package/src/compat/register/build.ts +74 -70
- package/src/compat/register/bundler.ts +161 -156
- package/src/compat/register/cache.ts +20 -20
- package/src/compat/register/client-errors.ts +3 -3
- package/src/compat/register/config.ts +6 -6
- package/src/compat/register/css-extras.ts +30 -34
- package/src/compat/register/edge-runtime.ts +3 -3
- package/src/compat/register/errors.ts +10 -12
- package/src/compat/register/export.ts +16 -16
- package/src/compat/register/font.ts +11 -11
- package/src/compat/register/hooks.ts +2 -2
- package/src/compat/register/image.ts +40 -40
- package/src/compat/register/index.ts +59 -62
- package/src/compat/register/instrumentation-client.ts +10 -10
- package/src/compat/register/lifecycle.ts +25 -28
- package/src/compat/register/mdx.ts +10 -10
- package/src/compat/register/middleware.ts +226 -16
- package/src/compat/register/otel.ts +80 -88
- package/src/compat/register/pages-api.ts +10 -463
- package/src/compat/register/ppr.ts +16 -16
- package/src/compat/register/protocol.ts +17 -18
- package/src/compat/register/proxy.ts +49 -51
- package/src/compat/register/render.ts +79 -76
- package/src/compat/register/routing.ts +162 -159
- package/src/compat/register/segment.ts +24 -1897
- package/src/compat/register/static-image.ts +3 -3
- package/src/compat/register/{misc.ts → taint.ts} +9 -9
- package/src/compat/register/typed-routes.ts +14 -14
- package/src/compat/register/{usecache.ts → use-cache.ts} +39 -39
- package/src/compat/register/validation.ts +18 -18
- package/src/compat/segment/loading-boundary.ts +43 -45
- package/src/compat/segment/page-slot.ts +69 -69
- package/src/compat/segment/serve.ts +1884 -0
- package/src/compat/segment/tree.ts +113 -112
- package/src/compat/segment/vary-key.ts +38 -38
- package/src/compat/segment/vary-params.ts +138 -142
- package/src/compat/static-params.ts +14 -12
- package/src/compat/tsconfig-defaults.ts +87 -91
- package/src/compat/typecheck/{index.ts → check.ts} +234 -212
- package/src/compat/typecheck/worker.ts +15 -12
- package/src/compat/typed-routes/{index.ts → generate.ts} +36 -36
- package/src/compat/typed-routes/manifest.ts +174 -170
- package/src/compat/typed-routes/typegen.ts +127 -110
- package/src/compat/validation/errors.ts +16 -19
- package/src/compat/validation/prerender-diagnostics.ts +521 -504
- package/src/compat/validation/{index.ts → validate.ts} +647 -648
- package/src/compat-bootstrap.ts +16 -16
- package/src/config.ts +69 -69
- package/src/css/build.ts +230 -227
- package/src/css/postcss.ts +79 -80
- package/src/css/worker.ts +14 -15
- package/src/dev/client-actions.ts +10 -10
- package/src/dev/client-chunk-store.ts +27 -27
- package/src/dev/{client-key-cache.ts → restart/client-key.ts} +76 -76
- package/src/dev/{restart-cache.ts → restart/enabled.ts} +1 -1
- package/src/dev/{global-css-cache.ts → restart/global-css.ts} +91 -83
- package/src/dev/{node-module-bundle-cache.ts → restart/node-modules.ts} +24 -24
- package/src/dev/{route-bundle-key-cache.ts → restart/route-bundle-key.ts} +53 -53
- package/src/dev/{route-facts-cache.ts → restart/route-facts.ts} +82 -82
- package/src/dev/server.ts +804 -820
- package/src/env.ts +46 -43
- package/src/extensions.ts +491 -478
- package/src/index.ts +8 -8
- package/src/internal.ts +20 -23
- package/src/{islands → render}/boundary-error.ts +3 -3
- package/src/render/hooks.ts +71 -71
- package/src/render/island-context.ts +14 -14
- package/src/render/metadata.ts +310 -310
- package/src/{ppr-postpone.ts → render/postpone.ts} +5 -5
- package/src/{ppr.ts → render/ppr.ts} +244 -245
- package/src/render/renderer.ts +2076 -2082
- package/src/render/resource-hints.ts +16 -17
- package/src/render/slots.tsx +224 -235
- package/src/{islands → render}/static-children.ts +9 -12
- package/src/{islands → render}/static-slots.ts +37 -37
- package/src/{cache/context.ts → request/cache.ts} +20 -20
- package/src/request/context.ts +107 -107
- package/src/{dynamic/source.ts → resolve/dynamic.ts} +139 -139
- package/src/resolve/engine.ts +90 -77
- package/src/resolve/imports.ts +475 -463
- package/src/resolve/scan-facts.ts +318 -296
- package/src/resolve/source-text.ts +37 -37
- package/src/{dynamic → resolve}/tree-shake.ts +132 -128
- package/src/routing/forwarded.ts +19 -19
- package/src/routing/handler.ts +84 -91
- package/src/routing/href.ts +69 -70
- package/src/routing/{metadata.ts → metadata-files.ts} +403 -401
- package/src/{proxy.ts → routing/proxy.ts} +306 -312
- package/src/routing/{request-runtime.ts → request-environment.ts} +10 -10
- package/src/routing/routes.ts +827 -815
- package/src/routing/slots.ts +164 -160
- package/src/runtime/loader.ts +952 -0
- package/src/{dev → runtime}/module-cache.ts +309 -287
- package/src/{dev → runtime}/module-generations.ts +9 -9
- package/src/{dev → runtime}/module-transform.ts +81 -72
- package/src/{dev/imports.ts → runtime/modules.ts} +934 -847
- package/src/runtime/{server.ts → vendor-build.ts} +848 -1696
- package/src/runtime/vendor.ts +425 -404
- package/src/styles.d.ts +9 -0
- package/src/types.ts +320 -335
- package/src/utils/ansi.ts +5 -5
- package/src/utils/{source.ts → code.ts} +15 -12
- package/src/utils/content-type.ts +3 -3
- package/src/utils/decode.ts +2 -2
- package/src/utils/dev-profile.ts +13 -13
- package/src/utils/error-log.ts +6 -6
- package/src/utils/esbuild.ts +18 -18
- package/src/utils/fs-cache.ts +13 -13
- package/src/utils/fs.ts +57 -49
- package/src/utils/html.ts +20 -24
- package/src/utils/native-require.ts +8 -8
- package/src/utils/serialize.ts +139 -146
- package/src/utils/verbose.ts +18 -18
- package/src/cli/analyze-print.ts +0 -181
- package/src/compat/middleware/manifest.ts +0 -210
- package/src/compat/next/image-client.tsx +0 -215
- package/src/compat/next/link-usage.ts +0 -29
- package/src/css/index.ts +0 -2
- package/src/render/index.ts +0 -1
- package/src/style-modules.d.ts +0 -9
package/src/routing/routes.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import path from 'node:path'
|
|
2
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
3
|
-
import { listFiles, listFilesSync, readDirListing, toPosixPath, withDirCache } from '../utils/fs'
|
|
4
|
-
import { safeDecode } from '../utils/decode'
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
3
|
+
import { listFiles, listFilesSync, readDirListing, toPosixPath, withDirCache } from '../utils/fs'
|
|
4
|
+
import { safeDecode } from '../utils/decode'
|
|
5
5
|
import {
|
|
6
6
|
dynamicCallsFromSource,
|
|
7
7
|
pnextDynamicImportNames,
|
|
8
8
|
rewriteLiteralDynamicCalls,
|
|
9
|
-
} from '../dynamic
|
|
9
|
+
} from '../resolve/dynamic'
|
|
10
10
|
import {
|
|
11
11
|
commonJsModuleHasDefaultExport,
|
|
12
12
|
packageNameForFile,
|
|
@@ -14,10 +14,10 @@ import {
|
|
|
14
14
|
resolveImport,
|
|
15
15
|
resolveModuleAlias,
|
|
16
16
|
resolvePackageSpecifier,
|
|
17
|
-
} from '../resolve/imports'
|
|
18
|
-
import { dynamicCallFacts, scanFacts, type DynamicCallFact } from '../resolve/scan-facts'
|
|
19
|
-
import { readSourceSync } from '../resolve/source-text'
|
|
20
|
-
import { globalCssSourcesForPaths } from '../css'
|
|
17
|
+
} from '../resolve/imports'
|
|
18
|
+
import { dynamicCallFacts, scanFacts, type DynamicCallFact } from '../resolve/scan-facts'
|
|
19
|
+
import { readSourceSync } from '../resolve/source-text'
|
|
20
|
+
import { globalCssSourcesForPaths } from '../css/build'
|
|
21
21
|
import {
|
|
22
22
|
childrenDefaultFile,
|
|
23
23
|
interceptionMarkerLevels,
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
isSlotSegment,
|
|
27
27
|
slotConventionFile,
|
|
28
28
|
slotDirectoriesIn,
|
|
29
|
-
} from './slots'
|
|
29
|
+
} from './slots'
|
|
30
30
|
import type {
|
|
31
31
|
ClientEntryReason,
|
|
32
32
|
MetadataRouteEntry,
|
|
@@ -37,13 +37,13 @@ import type {
|
|
|
37
37
|
RouteMode,
|
|
38
38
|
RouteParamValue,
|
|
39
39
|
RouteSegmentConfig,
|
|
40
|
-
} from '../types'
|
|
40
|
+
} from '../types'
|
|
41
41
|
import {
|
|
42
42
|
clientReferenceId,
|
|
43
43
|
ssrClientReference,
|
|
44
44
|
type ClientDynamicReference,
|
|
45
45
|
type ClientReference,
|
|
46
|
-
} from '../client/reference'
|
|
46
|
+
} from '../client/reference'
|
|
47
47
|
import {
|
|
48
48
|
alwaysClientEntryReasons,
|
|
49
49
|
classifyRouteDependencies,
|
|
@@ -53,7 +53,7 @@ import {
|
|
|
53
53
|
getCssExtensions,
|
|
54
54
|
sourceClientEntryReasons,
|
|
55
55
|
sourceUsesRegisteredRequestApi,
|
|
56
|
-
} from '../extensions'
|
|
56
|
+
} from '../extensions'
|
|
57
57
|
|
|
58
58
|
// Single source of truth for the page/convention extension list. `.tsx`/`.ts` stay first so an all-.tsx app
|
|
59
59
|
// resolves on the first candidate and produces the identical file as before; .js/.jsx/.mjs are additive
|
|
@@ -61,153 +61,153 @@ import {
|
|
|
61
61
|
// pageExtensions()/routeHandlerExtensions() LAZILY, computed on first use and memoized, so the compat
|
|
62
62
|
// registration that runs at bootstrap - before scanRoutes - is honored; a module-level const regex is baked
|
|
63
63
|
// at import time, which is too early.
|
|
64
|
-
const BASE_PAGE_EXTENSIONS = ['tsx', 'ts', 'jsx', 'js', 'mjs'] as const
|
|
65
|
-
const BASE_ROUTE_HANDLER_EXTENSIONS = ['ts', 'tsx', 'js', 'mjs'] as const
|
|
64
|
+
const BASE_PAGE_EXTENSIONS = ['tsx', 'ts', 'jsx', 'js', 'mjs'] as const
|
|
65
|
+
const BASE_ROUTE_HANDLER_EXTENSIONS = ['ts', 'tsx', 'js', 'mjs'] as const
|
|
66
66
|
|
|
67
67
|
/** Base + compat-registered page extensions, de-duplicated in order. */
|
|
68
68
|
function pageExtensions(): string[] {
|
|
69
|
-
return [...new Set([...BASE_PAGE_EXTENSIONS, ...extraPageExtensions()])]
|
|
69
|
+
return [...new Set([...BASE_PAGE_EXTENSIONS, ...extraPageExtensions()])]
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/** Route-handler extensions (route.*); compat page extensions do not apply. */
|
|
73
73
|
function routeHandlerExtensions(): readonly string[] {
|
|
74
|
-
return BASE_ROUTE_HANDLER_EXTENSIONS
|
|
74
|
+
return BASE_ROUTE_HANDLER_EXTENSIONS
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
/** Union of page + route-handler extensions (metadata + trailing-file regexes). */
|
|
78
78
|
function allFileExtensions(): string[] {
|
|
79
|
-
return [...new Set([...pageExtensions(), ...routeHandlerExtensions()])]
|
|
79
|
+
return [...new Set([...pageExtensions(), ...routeHandlerExtensions()])]
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
const componentConventions = ['loading', 'error', 'not-found'] as const
|
|
82
|
+
const componentConventions = ['loading', 'error', 'not-found'] as const
|
|
83
83
|
// Extra boundary conventions (e.g. Next's authInterrupts forbidden/unauthorized)
|
|
84
84
|
// are registered by compat via the routeConventions seam and merged in here.
|
|
85
85
|
function boundaryConventions(): string[] {
|
|
86
|
-
return [...componentConventions, ...extraBoundaryConventionNames()]
|
|
86
|
+
return [...componentConventions, ...extraBoundaryConventionNames()]
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
const globalErrorBase = 'global-error'
|
|
89
|
+
const globalErrorBase = 'global-error'
|
|
90
90
|
|
|
91
91
|
// The extension list is only known after compat bootstrap runs (which may
|
|
92
92
|
// registerPageExtensions), and scanRoutes always runs after bootstrap. Each
|
|
93
93
|
// pattern is computed on first use and memoized so the compat additions land
|
|
94
94
|
// while repeat calls stay cheap. Kept as accessors (not module-level consts)
|
|
95
95
|
// precisely to defer baking past bootstrap.
|
|
96
|
-
let memoPageFile: RegExp | undefined
|
|
97
|
-
let memoRouteFilePattern: RegExp | undefined
|
|
98
|
-
let memoDefaultConventionPattern: RegExp | undefined
|
|
99
|
-
let memoMetadataCodeFilePattern: RegExp | undefined
|
|
100
|
-
let memoPageOrRouteTrailing: RegExp | undefined
|
|
101
|
-
let memoSlotConventionPattern: RegExp | undefined
|
|
96
|
+
let memoPageFile: RegExp | undefined
|
|
97
|
+
let memoRouteFilePattern: RegExp | undefined
|
|
98
|
+
let memoDefaultConventionPattern: RegExp | undefined
|
|
99
|
+
let memoMetadataCodeFilePattern: RegExp | undefined
|
|
100
|
+
let memoPageOrRouteTrailing: RegExp | undefined
|
|
101
|
+
let memoSlotConventionPattern: RegExp | undefined
|
|
102
102
|
|
|
103
103
|
// Memos must not outlive a later registerPageExtensions call: Pages-compat
|
|
104
104
|
// materialization scans routes during config resolution, BEFORE
|
|
105
105
|
// registerMdxExtensions runs, and a pattern baked then would lock `.mdx` out.
|
|
106
|
-
let memoExtensionsKey: string | undefined
|
|
106
|
+
let memoExtensionsKey: string | undefined
|
|
107
107
|
function freshMemos(): void {
|
|
108
|
-
const key = extraPageExtensions().join(',')
|
|
109
|
-
if (key === memoExtensionsKey) return
|
|
110
|
-
memoExtensionsKey = key
|
|
111
|
-
memoPageFile = undefined
|
|
112
|
-
memoRouteFilePattern = undefined
|
|
113
|
-
memoDefaultConventionPattern = undefined
|
|
114
|
-
memoMetadataCodeFilePattern = undefined
|
|
115
|
-
memoPageOrRouteTrailing = undefined
|
|
116
|
-
memoSlotConventionPattern = undefined
|
|
108
|
+
const key = extraPageExtensions().join(',')
|
|
109
|
+
if (key === memoExtensionsKey) return
|
|
110
|
+
memoExtensionsKey = key
|
|
111
|
+
memoPageFile = undefined
|
|
112
|
+
memoRouteFilePattern = undefined
|
|
113
|
+
memoDefaultConventionPattern = undefined
|
|
114
|
+
memoMetadataCodeFilePattern = undefined
|
|
115
|
+
memoPageOrRouteTrailing = undefined
|
|
116
|
+
memoSlotConventionPattern = undefined
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
function pageFile(): RegExp {
|
|
120
|
-
freshMemos()
|
|
121
|
-
return (memoPageFile ??= extensionRegex('page', pageExtensions()))
|
|
120
|
+
freshMemos()
|
|
121
|
+
return (memoPageFile ??= extensionRegex('page', pageExtensions()))
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
function routeFilePattern(): RegExp {
|
|
125
|
-
freshMemos()
|
|
126
|
-
return (memoRouteFilePattern ??= extensionRegex('route', routeHandlerExtensions()))
|
|
125
|
+
freshMemos()
|
|
126
|
+
return (memoRouteFilePattern ??= extensionRegex('route', routeHandlerExtensions()))
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
function defaultConventionPattern(): RegExp {
|
|
130
|
-
freshMemos()
|
|
131
|
-
return (memoDefaultConventionPattern ??= extensionRegex('default', pageExtensions()))
|
|
130
|
+
freshMemos()
|
|
131
|
+
return (memoDefaultConventionPattern ??= extensionRegex('default', pageExtensions()))
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
function metadataCodeFilePattern(): RegExp {
|
|
135
|
-
freshMemos()
|
|
135
|
+
freshMemos()
|
|
136
136
|
return (memoMetadataCodeFilePattern ??= new RegExp(
|
|
137
137
|
`(^|/)(robots|sitemap|manifest|icon\\d*|apple-icon\\d*|opengraph-image\\d*|twitter-image\\d*)\\.(${allFileExtensions().join(
|
|
138
138
|
'|',
|
|
139
139
|
)})$`,
|
|
140
|
-
))
|
|
140
|
+
))
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
function pageOrRouteTrailing(): RegExp {
|
|
144
|
-
freshMemos()
|
|
144
|
+
freshMemos()
|
|
145
145
|
return (memoPageOrRouteTrailing ??= new RegExp(
|
|
146
146
|
`/?(page|route|layout|template|loading|error|not-found|forbidden|unauthorized|default|global-error)\\.(${allFileExtensions().join(
|
|
147
147
|
'|',
|
|
148
148
|
)})$`,
|
|
149
|
-
))
|
|
149
|
+
))
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
function slotConventionPattern(): RegExp {
|
|
153
|
-
freshMemos()
|
|
153
|
+
freshMemos()
|
|
154
154
|
return (memoSlotConventionPattern ??= new RegExp(
|
|
155
155
|
`(^|/)(page|layout|template|loading|error|default|not-found)\\.(${pageExtensions().join('|')})$`,
|
|
156
|
-
))
|
|
156
|
+
))
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
function extensionRegex(base: string, extensions: readonly string[]) {
|
|
160
|
-
return new RegExp(`(^|/)${escapeRegex(base)}\\.(${extensions.join('|')})$`)
|
|
160
|
+
return new RegExp(`(^|/)${escapeRegex(base)}\\.(${extensions.join('|')})$`)
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
/** Nearest existing file named `<base>.<ext>` for the convention extensions. */
|
|
164
164
|
function conventionFileName(dir: string, base: string) {
|
|
165
|
-
const { files } = readDirListing(dir)
|
|
165
|
+
const { files } = readDirListing(dir)
|
|
166
166
|
for (const extension of pageExtensions()) {
|
|
167
|
-
if (files.has(`${base}.${extension}`)) return path.join(dir, `${base}.${extension}`)
|
|
167
|
+
if (files.has(`${base}.${extension}`)) return path.join(dir, `${base}.${extension}`)
|
|
168
168
|
}
|
|
169
|
-
return undefined
|
|
169
|
+
return undefined
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
interface RouteParts {
|
|
173
|
-
route: string
|
|
174
|
-
pattern: string
|
|
175
|
-
params: string[]
|
|
176
|
-
catchAll?: string
|
|
177
|
-
catchAllOptional?: boolean
|
|
178
|
-
interception?: Omit<RouteInterception, 'slotDir'
|
|
173
|
+
route: string
|
|
174
|
+
pattern: string
|
|
175
|
+
params: string[]
|
|
176
|
+
catchAll?: string
|
|
177
|
+
catchAllOptional?: boolean
|
|
178
|
+
interception?: Omit<RouteInterception, 'slotDir'>
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
interface ModuleEdge {
|
|
182
|
-
file: string
|
|
183
|
-
exports: string[]
|
|
184
|
-
dynamic?: ClientDynamicReference
|
|
182
|
+
file: string
|
|
183
|
+
exports: string[]
|
|
184
|
+
dynamic?: ClientDynamicReference
|
|
185
185
|
/** `export * from` the target: its named exports are re-exported by this module. */
|
|
186
|
-
star?: boolean
|
|
186
|
+
star?: boolean
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
interface ScanContext {
|
|
190
|
-
root: string
|
|
191
|
-
appPath: string
|
|
192
|
-
sources: Map<string, string
|
|
193
|
-
edges: Map<string, ModuleEdge[]
|
|
194
|
-
requestImportUsage: Map<string, boolean
|
|
190
|
+
root: string
|
|
191
|
+
appPath: string
|
|
192
|
+
sources: Map<string, string>
|
|
193
|
+
edges: Map<string, ModuleEdge[]>
|
|
194
|
+
requestImportUsage: Map<string, boolean>
|
|
195
195
|
/**
|
|
196
196
|
* Per-FILE facts, path-keyed. Every route re-walks its whole source closure and each visit
|
|
197
197
|
* re-derived these from the text. Same lifetime and staleness rules as `sources`.
|
|
198
198
|
*/
|
|
199
|
-
exists: Map<string, boolean
|
|
200
|
-
useClient: Map<string, boolean
|
|
201
|
-
usesLink: Map<string, boolean
|
|
202
|
-
usesNavigation: Map<string, boolean
|
|
203
|
-
entryReasons: Map<string, ClientEntryReason[]
|
|
199
|
+
exists: Map<string, boolean>
|
|
200
|
+
useClient: Map<string, boolean>
|
|
201
|
+
usesLink: Map<string, boolean>
|
|
202
|
+
usesNavigation: Map<string, boolean>
|
|
203
|
+
entryReasons: Map<string, ClientEntryReason[]>
|
|
204
204
|
/** Root-layout CSS closure — itself a content walk, so it resolves with the first CSS fact. */
|
|
205
|
-
globalCssImports(): Set<string
|
|
205
|
+
globalCssImports(): Set<string>
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
function createScanContext(appPath: string, globalCss?: () => Set<string>): ScanContext {
|
|
209
|
-
const root = rootFromAppPath(appPath)
|
|
210
|
-
let memo: Set<string> | undefined
|
|
209
|
+
const root = rootFromAppPath(appPath)
|
|
210
|
+
let memo: Set<string> | undefined
|
|
211
211
|
return {
|
|
212
212
|
root,
|
|
213
213
|
appPath,
|
|
@@ -221,20 +221,20 @@ function createScanContext(appPath: string, globalCss?: () => Set<string>): Scan
|
|
|
221
221
|
entryReasons: new Map(),
|
|
222
222
|
globalCssImports:
|
|
223
223
|
globalCss ?? (() => (memo ??= new Set(globalCssSourcesForPaths(root, appPath)))),
|
|
224
|
-
}
|
|
224
|
+
}
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
/** `existsSync` once per path per scan — the closure walk asks the same paths per route. */
|
|
228
228
|
function fileExists(context: ScanContext, file: string) {
|
|
229
|
-
let hit = context.exists.get(file)
|
|
229
|
+
let hit = context.exists.get(file)
|
|
230
230
|
if (hit === undefined) {
|
|
231
|
-
hit = existsSync(file)
|
|
232
|
-
context.exists.set(file, hit)
|
|
231
|
+
hit = existsSync(file)
|
|
232
|
+
context.exists.set(file, hit)
|
|
233
233
|
}
|
|
234
|
-
return hit
|
|
234
|
+
return hit
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
const routeHandlerMethods = ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'] as const
|
|
237
|
+
const routeHandlerMethods = ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'] as const
|
|
238
238
|
|
|
239
239
|
/**
|
|
240
240
|
* The entry fields that need file CONTENT. Boot never reads them: the route table is built from path
|
|
@@ -242,21 +242,21 @@ const routeHandlerMethods = ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'O
|
|
|
242
242
|
* compile - through one memoized scan-facts pass per file, which that compile then re-uses.
|
|
243
243
|
*/
|
|
244
244
|
export interface RouteFacts {
|
|
245
|
-
mode: RouteMode
|
|
246
|
-
hasStaticParams: boolean
|
|
247
|
-
usesRequest: boolean
|
|
248
|
-
handlerUsesRevalidationApi?: true
|
|
249
|
-
dynamicErrorApi?: string
|
|
250
|
-
client: boolean
|
|
251
|
-
clientReferences: ClientReference[]
|
|
252
|
-
cssImports: string[]
|
|
253
|
-
sourceFiles: string[]
|
|
254
|
-
stream?: RouteManifestEntry['stream']
|
|
255
|
-
maxDuration?: number
|
|
256
|
-
ppr?: boolean
|
|
257
|
-
segmentConfig?: RouteSegmentConfig
|
|
258
|
-
needsRouterEntry?: true
|
|
259
|
-
clientEntryReasons?: ClientEntryReason[]
|
|
245
|
+
mode: RouteMode
|
|
246
|
+
hasStaticParams: boolean
|
|
247
|
+
usesRequest: boolean
|
|
248
|
+
handlerUsesRevalidationApi?: true
|
|
249
|
+
dynamicErrorApi?: string
|
|
250
|
+
client: boolean
|
|
251
|
+
clientReferences: ClientReference[]
|
|
252
|
+
cssImports: string[]
|
|
253
|
+
sourceFiles: string[]
|
|
254
|
+
stream?: RouteManifestEntry['stream']
|
|
255
|
+
maxDuration?: number
|
|
256
|
+
ppr?: boolean
|
|
257
|
+
segmentConfig?: RouteSegmentConfig
|
|
258
|
+
needsRouterEntry?: true
|
|
259
|
+
clientEntryReasons?: ClientEntryReason[]
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
const deferredFields = [
|
|
@@ -275,21 +275,21 @@ const deferredFields = [
|
|
|
275
275
|
'segmentConfig',
|
|
276
276
|
'needsRouterEntry',
|
|
277
277
|
'clientEntryReasons',
|
|
278
|
-
] as const satisfies readonly (keyof RouteFacts)[]
|
|
278
|
+
] as const satisfies readonly (keyof RouteFacts)[]
|
|
279
279
|
|
|
280
280
|
/** Path-only entry fields; everything in RouteFacts is layered on lazily. */
|
|
281
|
-
type RoutePathEntry = Omit<RouteManifestEntry, keyof RouteFacts
|
|
281
|
+
type RoutePathEntry = Omit<RouteManifestEntry, keyof RouteFacts>
|
|
282
282
|
|
|
283
|
-
const resolvedRoutes = new WeakSet<RouteManifestEntry>()
|
|
284
|
-
let factsVersion = 0
|
|
283
|
+
const resolvedRoutes = new WeakSet<RouteManifestEntry>()
|
|
284
|
+
let factsVersion = 0
|
|
285
285
|
|
|
286
286
|
/** Bumped each time a route resolves its deferred facts (dev re-keys watch roots off it). */
|
|
287
287
|
export function routeFactsVersion() {
|
|
288
|
-
return factsVersion
|
|
288
|
+
return factsVersion
|
|
289
289
|
}
|
|
290
290
|
|
|
291
291
|
export function routeFactsResolved(route: RouteManifestEntry) {
|
|
292
|
-
return resolvedRoutes.has(route)
|
|
292
|
+
return resolvedRoutes.has(route)
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
/**
|
|
@@ -302,10 +302,10 @@ export function materializeRouteFacts<T extends RouteManifestEntry[]>(
|
|
|
302
302
|
onRoute?: (route: RouteManifestEntry) => void,
|
|
303
303
|
): T {
|
|
304
304
|
for (const route of routes) {
|
|
305
|
-
void route.sourceFiles
|
|
306
|
-
onRoute?.(route)
|
|
305
|
+
void route.sourceFiles
|
|
306
|
+
onRoute?.(route)
|
|
307
307
|
}
|
|
308
|
-
return routes
|
|
308
|
+
return routes
|
|
309
309
|
}
|
|
310
310
|
|
|
311
311
|
/**
|
|
@@ -318,10 +318,10 @@ export async function materializeRouteFactsPaced<T extends RouteManifestEntry[]>
|
|
|
318
318
|
pause: () => Promise<void>,
|
|
319
319
|
): Promise<T> {
|
|
320
320
|
for (const route of routes) {
|
|
321
|
-
await pause()
|
|
322
|
-
void route.sourceFiles
|
|
321
|
+
await pause()
|
|
322
|
+
void route.sourceFiles
|
|
323
323
|
}
|
|
324
|
-
return routes
|
|
324
|
+
return routes
|
|
325
325
|
}
|
|
326
326
|
|
|
327
327
|
/**
|
|
@@ -330,14 +330,14 @@ export async function materializeRouteFactsPaced<T extends RouteManifestEntry[]>
|
|
|
330
330
|
* still has the content it was recorded with; `save` is called once, with the facts a fresh walk produced.
|
|
331
331
|
*/
|
|
332
332
|
export interface RouteFactsStore {
|
|
333
|
-
load(routeId: string): RouteFacts | undefined
|
|
334
|
-
save(routeId: string, facts: RouteFacts): void
|
|
333
|
+
load(routeId: string): RouteFacts | undefined
|
|
334
|
+
save(routeId: string, facts: RouteFacts): void
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
-
let factsStore: RouteFactsStore | undefined
|
|
337
|
+
let factsStore: RouteFactsStore | undefined
|
|
338
338
|
|
|
339
339
|
export function setRouteFactsStore(store: RouteFactsStore | undefined) {
|
|
340
|
-
factsStore = store
|
|
340
|
+
factsStore = store
|
|
341
341
|
}
|
|
342
342
|
|
|
343
343
|
function withDeferredFacts(
|
|
@@ -345,81 +345,81 @@ function withDeferredFacts(
|
|
|
345
345
|
base: RoutePathEntry,
|
|
346
346
|
compute: () => RouteFacts,
|
|
347
347
|
): RouteManifestEntry {
|
|
348
|
-
const entry = base as RouteManifestEntry
|
|
349
|
-
let facts: RouteFacts | undefined
|
|
348
|
+
const entry = base as RouteManifestEntry
|
|
349
|
+
let facts: RouteFacts | undefined
|
|
350
350
|
// Consumers still assign some of these (the build marks needsRouterEntry), so
|
|
351
351
|
// each field keeps a setter that wins over the computed value without forcing
|
|
352
352
|
// the scan.
|
|
353
|
-
const overrides = new Map<string, unknown>()
|
|
353
|
+
const overrides = new Map<string, unknown>()
|
|
354
354
|
|
|
355
355
|
for (const field of deferredFields) {
|
|
356
356
|
Object.defineProperty(entry, field, {
|
|
357
357
|
enumerable: true,
|
|
358
358
|
configurable: true,
|
|
359
359
|
get() {
|
|
360
|
-
if (overrides.has(field)) return overrides.get(field)
|
|
360
|
+
if (overrides.has(field)) return overrides.get(field)
|
|
361
361
|
if (!facts) {
|
|
362
362
|
// Route ids repeat across apps ('page:/' everywhere), so the app the
|
|
363
363
|
// facts were derived in is part of the key - otherwise one app's
|
|
364
364
|
// record answers another app's lookup in the same process.
|
|
365
|
-
const key = `${appPath}\0${base.kind}:${base.id}
|
|
366
|
-
const stored = factsStore?.load(key)
|
|
367
|
-
facts = stored ?? withDirCache(compute)
|
|
368
|
-
if (!stored) factsStore?.save(key, facts)
|
|
369
|
-
resolvedRoutes.add(entry)
|
|
370
|
-
factsVersion += 1
|
|
365
|
+
const key = `${appPath}\0${base.kind}:${base.id}`
|
|
366
|
+
const stored = factsStore?.load(key)
|
|
367
|
+
facts = stored ?? withDirCache(compute)
|
|
368
|
+
if (!stored) factsStore?.save(key, facts)
|
|
369
|
+
resolvedRoutes.add(entry)
|
|
370
|
+
factsVersion += 1
|
|
371
371
|
}
|
|
372
|
-
return facts[field]
|
|
372
|
+
return facts[field]
|
|
373
373
|
},
|
|
374
374
|
set(value: unknown) {
|
|
375
|
-
overrides.set(field, value)
|
|
375
|
+
overrides.set(field, value)
|
|
376
376
|
},
|
|
377
|
-
})
|
|
377
|
+
})
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
-
return entry
|
|
380
|
+
return entry
|
|
381
381
|
}
|
|
382
382
|
|
|
383
383
|
export async function scanRoutes(appPath: string): Promise<RouteManifestEntry[]> {
|
|
384
|
-
const files = await listFiles(appPath)
|
|
385
|
-
return withDirCache(() => buildRouteTable(appPath, files))
|
|
384
|
+
const files = await listFiles(appPath)
|
|
385
|
+
return withDirCache(() => buildRouteTable(appPath, files))
|
|
386
386
|
}
|
|
387
387
|
|
|
388
388
|
/** The path pass: file paths in, route table out — no file content is read. */
|
|
389
389
|
function buildRouteTable(appPath: string, files: string[]): RouteManifestEntry[] {
|
|
390
|
-
const context = createScanContext(appPath)
|
|
391
|
-
const routes: RouteManifestEntry[] = []
|
|
392
|
-
const slotPageFiles: { file: string; relative: string }[] = []
|
|
393
|
-
const defaultPageFiles: { file: string; relative: string }[] = []
|
|
390
|
+
const context = createScanContext(appPath)
|
|
391
|
+
const routes: RouteManifestEntry[] = []
|
|
392
|
+
const slotPageFiles: { file: string; relative: string }[] = []
|
|
393
|
+
const defaultPageFiles: { file: string; relative: string }[] = []
|
|
394
394
|
// Parallel/intercepting-route semantics only exist on client navigations, so
|
|
395
395
|
// when the app uses them every page ships the router runtime. Both facts are
|
|
396
396
|
// path-shaped, but the whole table must be walked before they are known —
|
|
397
397
|
// hence a holder the per-route needsRouterEntry closures read later.
|
|
398
|
-
const table = { parallelRoutes: false }
|
|
398
|
+
const table = { parallelRoutes: false }
|
|
399
399
|
|
|
400
400
|
for (const file of files) {
|
|
401
|
-
const relative = toPosixPath(path.relative(appPath, file))
|
|
402
|
-
const routeFile = path.join(appPath, relative)
|
|
401
|
+
const relative = toPosixPath(path.relative(appPath, file))
|
|
402
|
+
const routeFile = path.join(appPath, relative)
|
|
403
403
|
if (
|
|
404
404
|
defaultConventionPattern().test(relative) &&
|
|
405
405
|
!relative.split('/').some(segment => segment.startsWith('@'))
|
|
406
406
|
) {
|
|
407
|
-
defaultPageFiles.push({ file, relative })
|
|
407
|
+
defaultPageFiles.push({ file, relative })
|
|
408
408
|
}
|
|
409
409
|
// App segments beginning with `_` are private (Next ignores them); the
|
|
410
410
|
// route is dropped so requesting it 404s. A directory literally named
|
|
411
411
|
// `%5Ffoo` decodes to a routable `/_foo` (handled in routeParts), so it is
|
|
412
412
|
// NOT private. Applies to pages, handlers, and metadata routes alike.
|
|
413
|
-
if (isPrivateAppPath(relative)) continue
|
|
413
|
+
if (isPrivateAppPath(relative)) continue
|
|
414
414
|
if (!pageFile().test(relative) && !routeFilePattern().test(relative)) {
|
|
415
|
-
if (!metadataCodeFilePattern().test(relative)) continue
|
|
415
|
+
if (!metadataCodeFilePattern().test(relative)) continue
|
|
416
416
|
// The one content read boot cannot defer: a metadata file's generateSitemaps/
|
|
417
417
|
// generateImageMetadata export decides whether its URL carries an id segment,
|
|
418
418
|
// so it shapes the PATTERN. Bounded to metadata files (a handful per app) and
|
|
419
419
|
// to their own source — no module-graph walk.
|
|
420
|
-
const source = readSource(context, file)
|
|
421
|
-
const metadataParts = dynamicMetadataRouteParts(relative, source)
|
|
422
|
-
if (!metadataParts) continue
|
|
420
|
+
const source = readSource(context, file)
|
|
421
|
+
const metadataParts = dynamicMetadataRouteParts(relative, source)
|
|
422
|
+
if (!metadataParts) continue
|
|
423
423
|
routes.push(
|
|
424
424
|
withDeferredFacts(
|
|
425
425
|
appPath,
|
|
@@ -436,38 +436,38 @@ function buildRouteTable(appPath: string, files: string[]): RouteManifestEntry[]
|
|
|
436
436
|
},
|
|
437
437
|
() => metadataRouteFacts(context, routeFile, metadataParts),
|
|
438
438
|
),
|
|
439
|
-
)
|
|
440
|
-
continue
|
|
439
|
+
)
|
|
440
|
+
continue
|
|
441
441
|
}
|
|
442
442
|
// Pages inside `@slot` dirs primarily render as slot content of their
|
|
443
443
|
// owning segment; they also derive standalone entries (synthetic slot URLs
|
|
444
444
|
// and slot interception targets) in a second pass below.
|
|
445
445
|
if (relative.split('/').some(segment => segment.startsWith('@'))) {
|
|
446
|
-
if (pageFile().test(relative)) slotPageFiles.push({ file, relative })
|
|
447
|
-
continue
|
|
446
|
+
if (pageFile().test(relative)) slotPageFiles.push({ file, relative })
|
|
447
|
+
continue
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
-
const kind = pageFile().test(relative) ? 'page' : 'handler'
|
|
451
|
-
const routeDir = relative.replace(pageOrRouteTrailing(), '')
|
|
452
|
-
const parts = routeParts(routeDir)
|
|
453
|
-
const conventions = conventionPaths(appPath, routeFile, kind)
|
|
454
|
-
const templatePaths = conventions.templateFiles.filter(item => existsSync(item))
|
|
450
|
+
const kind = pageFile().test(relative) ? 'page' : 'handler'
|
|
451
|
+
const routeDir = relative.replace(pageOrRouteTrailing(), '')
|
|
452
|
+
const parts = routeParts(routeDir)
|
|
453
|
+
const conventions = conventionPaths(appPath, routeFile, kind)
|
|
454
|
+
const templatePaths = conventions.templateFiles.filter(item => existsSync(item))
|
|
455
455
|
// authInterrupt boundary files (forbidden/unauthorized) are only discovered
|
|
456
456
|
// when compat registered those conventions via the routeConventions seam.
|
|
457
|
-
const extraBoundaries = new Set(extraBoundaryConventionNames())
|
|
457
|
+
const extraBoundaries = new Set(extraBoundaryConventionNames())
|
|
458
458
|
const forbiddenPaths =
|
|
459
459
|
kind === 'page' && extraBoundaries.has('forbidden')
|
|
460
460
|
? findConventionFiles(appPath, routeFile, 'forbidden').filter(item => existsSync(item))
|
|
461
|
-
: []
|
|
461
|
+
: []
|
|
462
462
|
const unauthorizedPaths =
|
|
463
463
|
kind === 'page' && extraBoundaries.has('unauthorized')
|
|
464
464
|
? findConventionFiles(appPath, routeFile, 'unauthorized').filter(item => existsSync(item))
|
|
465
|
-
: []
|
|
465
|
+
: []
|
|
466
466
|
|
|
467
467
|
// An interception entry's route/pattern is the TARGET path it responds
|
|
468
468
|
// to; it only matches soft navigations (see matchInterception), renders
|
|
469
469
|
// per request, and never prerenders.
|
|
470
|
-
const interception = parts.interception
|
|
470
|
+
const interception = parts.interception
|
|
471
471
|
routes.push(
|
|
472
472
|
withDeferredFacts(
|
|
473
473
|
appPath,
|
|
@@ -488,22 +488,22 @@ function buildRouteTable(appPath: string, files: string[]): RouteManifestEntry[]
|
|
|
488
488
|
},
|
|
489
489
|
() => pageOrHandlerFacts(context, { kind, routeFile, parts, conventions, table }),
|
|
490
490
|
),
|
|
491
|
-
)
|
|
491
|
+
)
|
|
492
492
|
}
|
|
493
493
|
|
|
494
|
-
appendSlotDerivedRoutes(context, routes, slotPageFiles, table)
|
|
495
|
-
appendDefaultDerivedRoutes(context, routes, defaultPageFiles, table)
|
|
496
|
-
table.parallelRoutes = routes.some(route => route.slotDirs?.length || route.interception)
|
|
494
|
+
appendSlotDerivedRoutes(context, routes, slotPageFiles, table)
|
|
495
|
+
appendDefaultDerivedRoutes(context, routes, defaultPageFiles, table)
|
|
496
|
+
table.parallelRoutes = routes.some(route => route.slotDirs?.length || route.interception)
|
|
497
497
|
|
|
498
|
-
return routes.sort((a, b) => routeRank(a).localeCompare(routeRank(b)))
|
|
498
|
+
return routes.sort((a, b) => routeRank(a).localeCompare(routeRank(b)))
|
|
499
499
|
}
|
|
500
500
|
|
|
501
501
|
/** Convention files reachable from a route file — all decided by paths on disk. */
|
|
502
502
|
function conventionPaths(appPath: string, routeFile: string, kind: 'page' | 'handler') {
|
|
503
503
|
if (kind !== 'page') {
|
|
504
|
-
return { layoutFiles: [], templateFiles: [], specialFiles: [], slotDirs: [], slotFiles: [] }
|
|
504
|
+
return { layoutFiles: [], templateFiles: [], specialFiles: [], slotDirs: [], slotFiles: [] }
|
|
505
505
|
}
|
|
506
|
-
const slotDirs = chainSlotDirs(appPath, routeFile)
|
|
506
|
+
const slotDirs = chainSlotDirs(appPath, routeFile)
|
|
507
507
|
return {
|
|
508
508
|
layoutFiles: findLayouts(appPath, routeFile),
|
|
509
509
|
templateFiles: findConventionFiles(appPath, routeFile, 'template'),
|
|
@@ -512,14 +512,14 @@ function conventionPaths(appPath: string, routeFile: string, kind: 'page' | 'han
|
|
|
512
512
|
),
|
|
513
513
|
slotDirs,
|
|
514
514
|
slotFiles: slotTreeFiles(slotDirs),
|
|
515
|
-
}
|
|
515
|
+
}
|
|
516
516
|
}
|
|
517
517
|
|
|
518
|
-
type ConventionPaths = ReturnType<typeof conventionPaths
|
|
518
|
+
type ConventionPaths = ReturnType<typeof conventionPaths>
|
|
519
519
|
|
|
520
520
|
/** Set once the whole table is known; the closures below read it, never at boot. */
|
|
521
521
|
interface TableFlags {
|
|
522
|
-
parallelRoutes: boolean
|
|
522
|
+
parallelRoutes: boolean
|
|
523
523
|
}
|
|
524
524
|
|
|
525
525
|
function metadataRouteFacts(
|
|
@@ -527,19 +527,22 @@ function metadataRouteFacts(
|
|
|
527
527
|
routeFile: string,
|
|
528
528
|
parts: DynamicMetadataRouteParts,
|
|
529
529
|
): RouteFacts {
|
|
530
|
-
const source = readSource(context, routeFile)
|
|
531
|
-
const sourceFiles = collectSourceFiles(context, [routeFile])
|
|
532
|
-
const dependency = routeDependencyClassification(context, 'handler', sourceFiles)
|
|
530
|
+
const source = readSource(context, routeFile)
|
|
531
|
+
const sourceFiles = collectSourceFiles(context, [routeFile])
|
|
532
|
+
const dependency = routeDependencyClassification(context, 'handler', sourceFiles)
|
|
533
533
|
const usesRequest =
|
|
534
534
|
metadataRouteUsesRequest(source) ||
|
|
535
535
|
usesRevalidationApis(source) ||
|
|
536
536
|
dependency.usesRequest === true ||
|
|
537
|
-
usesRequestImportDataForFiles(
|
|
537
|
+
usesRequestImportDataForFiles(
|
|
538
|
+
context,
|
|
539
|
+
sourceFiles.filter(file => !isAssetFile(file)),
|
|
540
|
+
)
|
|
538
541
|
const segmentConfig = segmentConfigFromEntries(
|
|
539
542
|
context.appPath,
|
|
540
543
|
[{ file: routeFile, source }],
|
|
541
544
|
parts,
|
|
542
|
-
)
|
|
545
|
+
)
|
|
543
546
|
return {
|
|
544
547
|
mode: inferRouteMode(parts, usesRequest, segmentConfig),
|
|
545
548
|
hasStaticParams: Boolean(parts.metadataRoute.generatedParam),
|
|
@@ -550,67 +553,69 @@ function metadataRouteFacts(
|
|
|
550
553
|
sourceFiles,
|
|
551
554
|
maxDuration: maxDurationFromSources([source]),
|
|
552
555
|
...(segmentConfig ? { segmentConfig } : {}),
|
|
553
|
-
}
|
|
556
|
+
}
|
|
554
557
|
}
|
|
555
558
|
|
|
556
559
|
function pageOrHandlerFacts(
|
|
557
560
|
context: ScanContext,
|
|
558
561
|
options: {
|
|
559
|
-
kind: 'page' | 'handler'
|
|
560
|
-
routeFile: string
|
|
561
|
-
parts: RouteParts
|
|
562
|
-
conventions: ConventionPaths
|
|
563
|
-
table: TableFlags
|
|
562
|
+
kind: 'page' | 'handler'
|
|
563
|
+
routeFile: string
|
|
564
|
+
parts: RouteParts
|
|
565
|
+
conventions: ConventionPaths
|
|
566
|
+
table: TableFlags
|
|
564
567
|
},
|
|
565
568
|
): RouteFacts {
|
|
566
|
-
const { appPath } = context
|
|
567
|
-
const { kind, routeFile, parts, conventions, table } = options
|
|
568
|
-
const { layoutFiles, templateFiles, specialFiles, slotFiles } = conventions
|
|
569
|
-
const source = readScanSource(context, routeFile)
|
|
569
|
+
const { appPath } = context
|
|
570
|
+
const { kind, routeFile, parts, conventions, table } = options
|
|
571
|
+
const { layoutFiles, templateFiles, specialFiles, slotFiles } = conventions
|
|
572
|
+
const source = readScanSource(context, routeFile)
|
|
570
573
|
const layoutSources = [...layoutFiles, ...templateFiles, ...specialFiles]
|
|
571
574
|
.filter(item => existsSync(item))
|
|
572
|
-
.map(item => readScanSource(context, item))
|
|
573
|
-
const entryFiles = [routeFile, ...layoutFiles, ...templateFiles, ...specialFiles, ...slotFiles]
|
|
574
|
-
const sourceFiles = collectSourceFiles(context, entryFiles)
|
|
575
|
-
const dependency = routeDependencyClassification(context, kind, sourceFiles)
|
|
576
|
-
const handlerUsesRevalidationApi = kind === 'handler' && usesRevalidationApis(source)
|
|
575
|
+
.map(item => readScanSource(context, item))
|
|
576
|
+
const entryFiles = [routeFile, ...layoutFiles, ...templateFiles, ...specialFiles, ...slotFiles]
|
|
577
|
+
const sourceFiles = collectSourceFiles(context, entryFiles)
|
|
578
|
+
const dependency = routeDependencyClassification(context, kind, sourceFiles)
|
|
579
|
+
const handlerUsesRevalidationApi = kind === 'handler' && usesRevalidationApis(source)
|
|
577
580
|
const usesRequest =
|
|
578
581
|
kind === 'handler'
|
|
579
|
-
? routeHandlerUsesRequest(source) ||
|
|
580
|
-
|
|
582
|
+
? routeHandlerUsesRequest(source) ||
|
|
583
|
+
handlerUsesRevalidationApi ||
|
|
584
|
+
dependency.usesRequest === true
|
|
585
|
+
: [source, ...layoutSources].some(usesRequestData) || dependency.usesRequest === true
|
|
581
586
|
// Layout sources only (leaf-first, page first): segment config exports in
|
|
582
587
|
// templates/special files have no route-level meaning.
|
|
583
|
-
const existingLayoutFiles = layoutFiles.filter(item => existsSync(item))
|
|
588
|
+
const existingLayoutFiles = layoutFiles.filter(item => existsSync(item))
|
|
584
589
|
const configEntries = [
|
|
585
590
|
{ file: routeFile, source },
|
|
586
591
|
...[...existingLayoutFiles].reverse().map(item => ({
|
|
587
592
|
file: item,
|
|
588
593
|
source: readScanSource(context, item),
|
|
589
594
|
})),
|
|
590
|
-
]
|
|
591
|
-
const segmentConfig = segmentConfigFromEntries(appPath, configEntries, parts)
|
|
595
|
+
]
|
|
596
|
+
const segmentConfig = segmentConfigFromEntries(appPath, configEntries, parts)
|
|
592
597
|
if (segmentConfig?.dynamicParamsFalse?.length) {
|
|
593
598
|
// `dynamicParams = false` only 404s unlisted params when the route is fully statically enumerable -
|
|
594
599
|
// every dynamic segment must be generated by a generateStaticParams somewhere in the chain. If a dynamic
|
|
595
600
|
// segment has no static params, the route renders on demand and the ancestor's dynamicParams config no
|
|
596
601
|
// longer restricts it (Next treats the whole path as dynamic).
|
|
597
|
-
const allDynamicParams = [...parts.params, ...(parts.catchAll ? [parts.catchAll] : [])]
|
|
598
|
-
const coveredParams = new Set<string>()
|
|
602
|
+
const allDynamicParams = [...parts.params, ...(parts.catchAll ? [parts.catchAll] : [])]
|
|
603
|
+
const coveredParams = new Set<string>()
|
|
599
604
|
for (const entry of configEntries) {
|
|
600
|
-
if (!hasStaticParamsExport(entry.source)) continue
|
|
601
|
-
const covered = paramOfOwnSegment(appPath, entry.file, allDynamicParams)
|
|
602
|
-
if (covered) coveredParams.add(covered)
|
|
605
|
+
if (!hasStaticParamsExport(entry.source)) continue
|
|
606
|
+
const covered = paramOfOwnSegment(appPath, entry.file, allDynamicParams)
|
|
607
|
+
if (covered) coveredParams.add(covered)
|
|
603
608
|
}
|
|
604
609
|
if (allDynamicParams.some(param => !coveredParams.has(param))) {
|
|
605
|
-
delete segmentConfig.dynamicParamsFalse
|
|
610
|
+
delete segmentConfig.dynamicParamsFalse
|
|
606
611
|
} else if (hasStaticParamsExport(source)) {
|
|
607
|
-
segmentConfig.strictDynamicParams = true
|
|
612
|
+
segmentConfig.strictDynamicParams = true
|
|
608
613
|
}
|
|
609
614
|
}
|
|
610
615
|
const hasStaticParams =
|
|
611
616
|
hasStaticParamsExport(source) ||
|
|
612
617
|
(kind === 'page' &&
|
|
613
|
-
existingLayoutFiles.map(item => readScanSource(context, item)).some(hasStaticParamsExport))
|
|
618
|
+
existingLayoutFiles.map(item => readScanSource(context, item)).some(hasStaticParamsExport))
|
|
614
619
|
// `dynamic = 'error'` forbids dynamic data: a static reference to one is a
|
|
615
620
|
// build failure (enforced in the prerender pass). Restricted to routes with
|
|
616
621
|
// no dynamic params and no generateStaticParams: those prerender a single
|
|
@@ -625,15 +630,15 @@ function pageOrHandlerFacts(
|
|
|
625
630
|
!parts.catchAll &&
|
|
626
631
|
!hasStaticParams
|
|
627
632
|
? dynamicApiLabel(kind === 'handler' ? [source] : [source, ...layoutSources], kind)
|
|
628
|
-
: undefined
|
|
633
|
+
: undefined
|
|
629
634
|
const ppr =
|
|
630
635
|
kind === 'page' &&
|
|
631
636
|
usesRequest &&
|
|
632
637
|
parts.params.length === 0 &&
|
|
633
638
|
!parts.catchAll &&
|
|
634
|
-
exportsExperimentalPpr([source, ...layoutSources])
|
|
639
|
+
exportsExperimentalPpr([source, ...layoutSources])
|
|
635
640
|
|
|
636
|
-
const directClient = kind === 'page' && hasUseClientDirective(routeFile, source)
|
|
641
|
+
const directClient = kind === 'page' && hasUseClientDirective(routeFile, source)
|
|
637
642
|
const clientReferences =
|
|
638
643
|
kind === 'page'
|
|
639
644
|
? collectClientReferences(
|
|
@@ -642,14 +647,16 @@ function pageOrHandlerFacts(
|
|
|
642
647
|
? [...layoutFiles, ...templateFiles, ...specialFiles, ...slotFiles]
|
|
643
648
|
: entryFiles,
|
|
644
649
|
)
|
|
645
|
-
: []
|
|
650
|
+
: []
|
|
646
651
|
const client =
|
|
647
652
|
directClient ||
|
|
648
|
-
clientReferences.some(
|
|
653
|
+
clientReferences.some(
|
|
654
|
+
reference => reference.file === routeFile && reference.exportName === 'default',
|
|
655
|
+
)
|
|
649
656
|
const stream =
|
|
650
657
|
kind === 'page'
|
|
651
658
|
? routeStreamMetadata({ context, layoutFiles, templateFiles, specialFiles })
|
|
652
|
-
: undefined
|
|
659
|
+
: undefined
|
|
653
660
|
const cssImports =
|
|
654
661
|
kind === 'page'
|
|
655
662
|
? collectCssImports(
|
|
@@ -657,10 +664,10 @@ function pageOrHandlerFacts(
|
|
|
657
664
|
cssEntryOrder({ routeFile, layoutFiles, templateFiles, specialFiles, slotFiles }),
|
|
658
665
|
{ route: true },
|
|
659
666
|
)
|
|
660
|
-
: []
|
|
661
|
-
if (kind === 'page') attachLazyReferenceCss(context, clientReferences)
|
|
667
|
+
: []
|
|
668
|
+
if (kind === 'page') attachLazyReferenceCss(context, clientReferences)
|
|
662
669
|
|
|
663
|
-
const interception = parts.interception
|
|
670
|
+
const interception = parts.interception
|
|
664
671
|
return {
|
|
665
672
|
mode: interception ? 'dynamic' : inferRouteMode(parts, usesRequest, segmentConfig),
|
|
666
673
|
hasStaticParams: interception ? false : hasStaticParams,
|
|
@@ -684,7 +691,7 @@ function pageOrHandlerFacts(
|
|
|
684
691
|
slotInterceptor: false,
|
|
685
692
|
table,
|
|
686
693
|
}),
|
|
687
|
-
}
|
|
694
|
+
}
|
|
688
695
|
}
|
|
689
696
|
|
|
690
697
|
/**
|
|
@@ -699,38 +706,38 @@ function pageOrHandlerFacts(
|
|
|
699
706
|
function routerEntryFact(
|
|
700
707
|
context: ScanContext,
|
|
701
708
|
options: {
|
|
702
|
-
kind: 'page' | 'handler'
|
|
703
|
-
client: boolean
|
|
704
|
-
clientReferences: ClientReference[]
|
|
705
|
-
sourceFiles: string[]
|
|
706
|
-
slotInterceptor: boolean
|
|
707
|
-
table: TableFlags
|
|
709
|
+
kind: 'page' | 'handler'
|
|
710
|
+
client: boolean
|
|
711
|
+
clientReferences: ClientReference[]
|
|
712
|
+
sourceFiles: string[]
|
|
713
|
+
slotInterceptor: boolean
|
|
714
|
+
table: TableFlags
|
|
708
715
|
},
|
|
709
716
|
): { needsRouterEntry?: true; clientEntryReasons?: ClientEntryReason[] } {
|
|
710
|
-
if (options.kind !== 'page') return {}
|
|
711
|
-
const reasons: ClientEntryReason[] = []
|
|
717
|
+
if (options.kind !== 'page') return {}
|
|
718
|
+
const reasons: ClientEntryReason[] = []
|
|
712
719
|
// Slot interceptors never render standalone (the origin page hosts them);
|
|
713
720
|
// children interceptors render directly and need the runtime.
|
|
714
|
-
if (options.table.parallelRoutes && !options.slotInterceptor) reasons.push('parallel-routes')
|
|
721
|
+
if (options.table.parallelRoutes && !options.slotInterceptor) reasons.push('parallel-routes')
|
|
715
722
|
// Compat parity: Next ships its router bootstrap on every app page, so a page
|
|
716
723
|
// with no client code of its own still hydrates (and runs
|
|
717
724
|
// instrumentation-client before hydration).
|
|
718
|
-
reasons.push(...alwaysClientEntryReasons())
|
|
725
|
+
reasons.push(...alwaysClientEntryReasons())
|
|
719
726
|
// A page with client code of its own already ships an entry, so the <Link> scan only exists to catch the
|
|
720
727
|
// pages that otherwise would not. The remaining reasons gate feature regions INSIDE the entry (the action
|
|
721
728
|
// runtime), so they are scanned for every page - a page's inline `'use server'` action is usually handed to
|
|
722
729
|
// an island it also renders.
|
|
723
730
|
if (!options.client && options.clientReferences.length === 0) {
|
|
724
|
-
if (routeUsesLinkComponent(context, options.sourceFiles)) reasons.push('link')
|
|
731
|
+
if (routeUsesLinkComponent(context, options.sourceFiles)) reasons.push('link')
|
|
725
732
|
} else if (routeUsesNavigation(context, options.sourceFiles)) {
|
|
726
733
|
// The complement of the branch above: a page that already ships an entry
|
|
727
734
|
// never needed a reason before, so nothing here changes whether the entry
|
|
728
735
|
// exists — it decides whether that entry carries the router at all.
|
|
729
|
-
reasons.push('router-api')
|
|
736
|
+
reasons.push('router-api')
|
|
730
737
|
}
|
|
731
|
-
reasons.push(...routeClientEntryReasons(context, options.sourceFiles))
|
|
732
|
-
if (reasons.length === 0) return {}
|
|
733
|
-
return { needsRouterEntry: true, clientEntryReasons: [...new Set(reasons)] }
|
|
738
|
+
reasons.push(...routeClientEntryReasons(context, options.sourceFiles))
|
|
739
|
+
if (reasons.length === 0) return {}
|
|
740
|
+
return { needsRouterEntry: true, clientEntryReasons: [...new Set(reasons)] }
|
|
734
741
|
}
|
|
735
742
|
|
|
736
743
|
/**
|
|
@@ -739,19 +746,21 @@ function routerEntryFact(
|
|
|
739
746
|
* discovery has run). Keeps `needsRouterEntry` in step with the reason list.
|
|
740
747
|
*/
|
|
741
748
|
export function addClientEntryReason(route: RouteManifestEntry, reason: ClientEntryReason): void {
|
|
742
|
-
const reasons = route.clientEntryReasons ?? []
|
|
743
|
-
if (!reasons.includes(reason)) route.clientEntryReasons = [...reasons, reason]
|
|
744
|
-
route.needsRouterEntry = true
|
|
749
|
+
const reasons = route.clientEntryReasons ?? []
|
|
750
|
+
if (!reasons.includes(reason)) route.clientEntryReasons = [...reasons, reason]
|
|
751
|
+
route.needsRouterEntry = true
|
|
745
752
|
}
|
|
746
753
|
|
|
747
754
|
function routeStreamMetadata(options: {
|
|
748
|
-
context: ScanContext
|
|
749
|
-
layoutFiles: string[]
|
|
750
|
-
templateFiles: string[]
|
|
751
|
-
specialFiles: string[]
|
|
755
|
+
context: ScanContext
|
|
756
|
+
layoutFiles: string[]
|
|
757
|
+
templateFiles: string[]
|
|
758
|
+
specialFiles: string[]
|
|
752
759
|
}) {
|
|
753
|
-
const existingSpecialFiles = options.specialFiles.filter(item => existsSync(item))
|
|
754
|
-
const hasLoadingBoundary = existingSpecialFiles.some(file =>
|
|
760
|
+
const existingSpecialFiles = options.specialFiles.filter(item => existsSync(item))
|
|
761
|
+
const hasLoadingBoundary = existingSpecialFiles.some(file =>
|
|
762
|
+
/(^|[/\\])loading\.[^.]+$/.test(file),
|
|
763
|
+
)
|
|
755
764
|
// Compute the ancestor client-reference closure for EVERY page, not just loading.js routes. Streaming
|
|
756
765
|
// isolates each Suspense boundary and drops client-provider context from ancestor layouts, so the
|
|
757
766
|
// render-time stream gate keys off this list - a route with in-page user Suspense boundaries but no
|
|
@@ -759,45 +768,45 @@ function routeStreamMetadata(options: {
|
|
|
759
768
|
const ancestorClientReferences = collectClientReferences(options.context, [
|
|
760
769
|
...options.layoutFiles,
|
|
761
770
|
...options.templateFiles,
|
|
762
|
-
])
|
|
771
|
+
])
|
|
763
772
|
return {
|
|
764
773
|
...(hasLoadingBoundary ? { hasLoadingBoundary } : {}),
|
|
765
774
|
...(ancestorClientReferences.length > 0
|
|
766
775
|
? { ancestorClientReferences: ancestorClientReferences.map(reference => reference.id) }
|
|
767
776
|
: {}),
|
|
768
|
-
}
|
|
777
|
+
}
|
|
769
778
|
}
|
|
770
779
|
|
|
771
780
|
function hasStaticParamsExport(source: string) {
|
|
772
|
-
const searchable = stripCommentsAndStrings(source)
|
|
781
|
+
const searchable = stripCommentsAndStrings(source)
|
|
773
782
|
return (
|
|
774
783
|
/\bexport\s+(?:async\s+)?function\s+(?:params|generateStaticParams)\b/.test(searchable) ||
|
|
775
784
|
/\bexport\s+const\s+(?:params|generateStaticParams)\b/.test(searchable)
|
|
776
|
-
)
|
|
785
|
+
)
|
|
777
786
|
}
|
|
778
787
|
|
|
779
788
|
/** All `@slot` directories on the walk from the app root to the route's dir. */
|
|
780
789
|
function chainSlotDirs(appPath: string, routeFilePath: string) {
|
|
781
|
-
const dirs: string[] = []
|
|
782
|
-
let dir = path.dirname(routeFilePath)
|
|
790
|
+
const dirs: string[] = []
|
|
791
|
+
let dir = path.dirname(routeFilePath)
|
|
783
792
|
while (dir.startsWith(appPath)) {
|
|
784
|
-
dirs.push(...slotDirectoriesIn(dir))
|
|
785
|
-
if (dir === appPath) break
|
|
786
|
-
dir = path.dirname(dir)
|
|
793
|
+
dirs.push(...slotDirectoriesIn(dir))
|
|
794
|
+
if (dir === appPath) break
|
|
795
|
+
dir = path.dirname(dir)
|
|
787
796
|
}
|
|
788
|
-
return dirs.reverse()
|
|
797
|
+
return dirs.reverse()
|
|
789
798
|
}
|
|
790
799
|
|
|
791
800
|
/** Every convention file in the given slot trees (recursive). */
|
|
792
801
|
function slotTreeFiles(slotDirs: string[]) {
|
|
793
|
-
const files: string[] = []
|
|
794
|
-
const pattern = slotConventionPattern()
|
|
802
|
+
const files: string[] = []
|
|
803
|
+
const pattern = slotConventionPattern()
|
|
795
804
|
for (const dir of slotDirs) {
|
|
796
805
|
for (const file of listFilesSync(dir)) {
|
|
797
|
-
if (pattern.test(toPosixPath(file))) files.push(file)
|
|
806
|
+
if (pattern.test(toPosixPath(file))) files.push(file)
|
|
798
807
|
}
|
|
799
808
|
}
|
|
800
|
-
return files
|
|
809
|
+
return files
|
|
801
810
|
}
|
|
802
811
|
|
|
803
812
|
/**
|
|
@@ -814,19 +823,19 @@ function appendSlotDerivedRoutes(
|
|
|
814
823
|
slotPages: { file: string; relative: string }[],
|
|
815
824
|
table: TableFlags,
|
|
816
825
|
) {
|
|
817
|
-
const appPath = context.appPath
|
|
818
|
-
const patterns = new Set(routes.filter(route => !route.interception).map(route => route.pattern))
|
|
826
|
+
const appPath = context.appPath
|
|
827
|
+
const patterns = new Set(routes.filter(route => !route.interception).map(route => route.pattern))
|
|
819
828
|
const interceptionKeys = new Set(
|
|
820
829
|
routes
|
|
821
830
|
.filter(route => route.interception)
|
|
822
831
|
.map(route => `${route.pattern}#${route.interception!.base}`),
|
|
823
|
-
)
|
|
832
|
+
)
|
|
824
833
|
|
|
825
834
|
const candidates = slotPages
|
|
826
835
|
.map(({ file, relative }) => {
|
|
827
|
-
const segments = relative.split('/')
|
|
828
|
-
const slotIndex = segments.findIndex(isSlotSegment)
|
|
829
|
-
const slotDir = path.join(appPath, ...segments.slice(0, slotIndex + 1))
|
|
836
|
+
const segments = relative.split('/')
|
|
837
|
+
const slotIndex = segments.findIndex(isSlotSegment)
|
|
838
|
+
const slotDir = path.join(appPath, ...segments.slice(0, slotIndex + 1))
|
|
830
839
|
return {
|
|
831
840
|
file,
|
|
832
841
|
relative,
|
|
@@ -836,18 +845,18 @@ function appendSlotDerivedRoutes(
|
|
|
836
845
|
innerNames: segments
|
|
837
846
|
.slice(slotIndex + 1, -1)
|
|
838
847
|
.filter(name => !isSlotSegment(name) && !isGroupSegment(name)),
|
|
839
|
-
}
|
|
848
|
+
}
|
|
840
849
|
})
|
|
841
850
|
// Deepest owner first: a URL reachable through several slots anchors its
|
|
842
851
|
// layout chain at the most specific segment.
|
|
843
|
-
.sort((a, b) => b.ownerDir.length - a.ownerDir.length)
|
|
852
|
+
.sort((a, b) => b.ownerDir.length - a.ownerDir.length)
|
|
844
853
|
|
|
845
854
|
for (const candidate of candidates) {
|
|
846
|
-
const { parts } = candidate
|
|
855
|
+
const { parts } = candidate
|
|
847
856
|
if (parts.interception) {
|
|
848
|
-
const key = `${parts.pattern}#${parts.interception.base}
|
|
849
|
-
if (interceptionKeys.has(key)) continue
|
|
850
|
-
interceptionKeys.add(key)
|
|
857
|
+
const key = `${parts.pattern}#${parts.interception.base}`
|
|
858
|
+
if (interceptionKeys.has(key)) continue
|
|
859
|
+
interceptionKeys.add(key)
|
|
851
860
|
// Never rendered directly: a matching soft navigation renders the
|
|
852
861
|
// CURRENT page (the from-path's entry) and this slot resolves the
|
|
853
862
|
// interceptor during slot rendering. Kept minimal on purpose.
|
|
@@ -877,14 +886,14 @@ function appendSlotDerivedRoutes(
|
|
|
877
886
|
}),
|
|
878
887
|
}),
|
|
879
888
|
),
|
|
880
|
-
)
|
|
881
|
-
continue
|
|
889
|
+
)
|
|
890
|
+
continue
|
|
882
891
|
}
|
|
883
892
|
|
|
884
|
-
if (patterns.has(parts.pattern)) continue
|
|
885
|
-
patterns.add(parts.pattern)
|
|
893
|
+
if (patterns.has(parts.pattern)) continue
|
|
894
|
+
patterns.add(parts.pattern)
|
|
886
895
|
|
|
887
|
-
const childrenDefault = childrenDefaultFile(candidate.ownerDir, candidate.innerNames)
|
|
896
|
+
const childrenDefault = childrenDefaultFile(candidate.ownerDir, candidate.innerNames)
|
|
888
897
|
// The anchor only positions the entry in the app tree (layouts, templates,
|
|
889
898
|
// boundaries, slot chain). Prefer a real file: the children default, then an
|
|
890
899
|
// explicit `@children` page, then the owner's own page. A non-existent
|
|
@@ -894,24 +903,22 @@ function appendSlotDerivedRoutes(
|
|
|
894
903
|
childrenDefault ??
|
|
895
904
|
slotConventionFile(path.join(candidate.ownerDir, '@children'), 'page') ??
|
|
896
905
|
slotConventionFile(candidate.ownerDir, 'page') ??
|
|
897
|
-
candidate.file
|
|
898
|
-
const layoutFiles = findLayouts(appPath, anchor)
|
|
899
|
-
const templateFiles = findConventionFiles(appPath, anchor, 'template')
|
|
906
|
+
candidate.file
|
|
907
|
+
const layoutFiles = findLayouts(appPath, anchor)
|
|
908
|
+
const templateFiles = findConventionFiles(appPath, anchor, 'template')
|
|
900
909
|
const specialFiles = boundaryConventions().flatMap(name =>
|
|
901
910
|
findConventionFiles(appPath, anchor, name),
|
|
902
|
-
)
|
|
903
|
-
const slotDirs = chainSlotDirs(appPath, anchor)
|
|
904
|
-
const slotFiles = slotTreeFiles(slotDirs)
|
|
905
|
-
const extraBoundaries = new Set(extraBoundaryConventionNames())
|
|
906
|
-
const forbiddenPaths =
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
: [];
|
|
914
|
-
const templatePaths = templateFiles.filter(item => existsSync(item));
|
|
911
|
+
)
|
|
912
|
+
const slotDirs = chainSlotDirs(appPath, anchor)
|
|
913
|
+
const slotFiles = slotTreeFiles(slotDirs)
|
|
914
|
+
const extraBoundaries = new Set(extraBoundaryConventionNames())
|
|
915
|
+
const forbiddenPaths = extraBoundaries.has('forbidden')
|
|
916
|
+
? findConventionFiles(appPath, anchor, 'forbidden').filter(item => existsSync(item))
|
|
917
|
+
: []
|
|
918
|
+
const unauthorizedPaths = extraBoundaries.has('unauthorized')
|
|
919
|
+
? findConventionFiles(appPath, anchor, 'unauthorized').filter(item => existsSync(item))
|
|
920
|
+
: []
|
|
921
|
+
const templatePaths = templateFiles.filter(item => existsSync(item))
|
|
915
922
|
|
|
916
923
|
routes.push(
|
|
917
924
|
withDeferredFacts(
|
|
@@ -944,7 +951,7 @@ function appendSlotDerivedRoutes(
|
|
|
944
951
|
table,
|
|
945
952
|
}),
|
|
946
953
|
),
|
|
947
|
-
)
|
|
954
|
+
)
|
|
948
955
|
}
|
|
949
956
|
}
|
|
950
957
|
|
|
@@ -957,7 +964,7 @@ const emptyDerivedFacts = {
|
|
|
957
964
|
clientReferences: [],
|
|
958
965
|
cssImports: [],
|
|
959
966
|
sourceFiles: [],
|
|
960
|
-
} satisfies RouteFacts
|
|
967
|
+
} satisfies RouteFacts
|
|
961
968
|
|
|
962
969
|
/**
|
|
963
970
|
* Content facts of a synthetic slot / default entry. Its tree is the convention
|
|
@@ -967,22 +974,22 @@ function derivedRouteFacts(
|
|
|
967
974
|
context: ScanContext,
|
|
968
975
|
options: {
|
|
969
976
|
/** Files that lead the entry tree (children default) or ARE it (a default.tsx). */
|
|
970
|
-
leadFiles: string[]
|
|
971
|
-
layoutFiles: string[]
|
|
972
|
-
templateFiles: string[]
|
|
973
|
-
specialFiles: string[]
|
|
974
|
-
slotFiles: string[]
|
|
977
|
+
leadFiles: string[]
|
|
978
|
+
layoutFiles: string[]
|
|
979
|
+
templateFiles: string[]
|
|
980
|
+
specialFiles: string[]
|
|
981
|
+
slotFiles: string[]
|
|
975
982
|
/** File whose `'use client'` directive decides the entry's own `client` flag. */
|
|
976
|
-
clientFile?: string
|
|
983
|
+
clientFile?: string
|
|
977
984
|
/** Lead file also participates in CSS order as the route file (default entries). */
|
|
978
|
-
leadIsRouteFile?: boolean
|
|
979
|
-
table: TableFlags
|
|
985
|
+
leadIsRouteFile?: boolean
|
|
986
|
+
table: TableFlags
|
|
980
987
|
},
|
|
981
988
|
): RouteFacts {
|
|
982
|
-
const { leadFiles, layoutFiles, templateFiles, specialFiles, slotFiles } = options
|
|
983
|
-
const entryFiles = [...leadFiles, ...layoutFiles, ...templateFiles, ...specialFiles, ...slotFiles]
|
|
984
|
-
const sourceFiles = collectSourceFiles(context, entryFiles)
|
|
985
|
-
const clientReferences = collectClientReferences(context, entryFiles)
|
|
989
|
+
const { leadFiles, layoutFiles, templateFiles, specialFiles, slotFiles } = options
|
|
990
|
+
const entryFiles = [...leadFiles, ...layoutFiles, ...templateFiles, ...specialFiles, ...slotFiles]
|
|
991
|
+
const sourceFiles = collectSourceFiles(context, entryFiles)
|
|
992
|
+
const clientReferences = collectClientReferences(context, entryFiles)
|
|
986
993
|
const cssImports = collectCssImports(
|
|
987
994
|
context,
|
|
988
995
|
cssEntryOrder({
|
|
@@ -993,9 +1000,9 @@ function derivedRouteFacts(
|
|
|
993
1000
|
slotFiles,
|
|
994
1001
|
}),
|
|
995
1002
|
{ route: true },
|
|
996
|
-
)
|
|
997
|
-
attachLazyReferenceCss(context, clientReferences)
|
|
998
|
-
const client = options.clientFile ? isClientModule(context, options.clientFile) : false
|
|
1003
|
+
)
|
|
1004
|
+
attachLazyReferenceCss(context, clientReferences)
|
|
1005
|
+
const client = options.clientFile ? isClientModule(context, options.clientFile) : false
|
|
999
1006
|
|
|
1000
1007
|
return {
|
|
1001
1008
|
...emptyDerivedFacts,
|
|
@@ -1011,7 +1018,7 @@ function derivedRouteFacts(
|
|
|
1011
1018
|
slotInterceptor: false,
|
|
1012
1019
|
table: options.table,
|
|
1013
1020
|
}),
|
|
1014
|
-
}
|
|
1021
|
+
}
|
|
1015
1022
|
}
|
|
1016
1023
|
|
|
1017
1024
|
/**
|
|
@@ -1029,35 +1036,33 @@ function appendDefaultDerivedRoutes(
|
|
|
1029
1036
|
defaultPages: { file: string; relative: string }[],
|
|
1030
1037
|
table: TableFlags,
|
|
1031
1038
|
) {
|
|
1032
|
-
const appPath = context.appPath
|
|
1033
|
-
const patterns = new Set(routes.filter(route => !route.interception).map(route => route.pattern))
|
|
1039
|
+
const appPath = context.appPath
|
|
1040
|
+
const patterns = new Set(routes.filter(route => !route.interception).map(route => route.pattern))
|
|
1034
1041
|
|
|
1035
1042
|
for (const { file, relative } of defaultPages) {
|
|
1036
|
-
const dir = path.dirname(file)
|
|
1043
|
+
const dir = path.dirname(file)
|
|
1037
1044
|
// A sibling page.tsx means the segment already has a normal entry.
|
|
1038
|
-
if (conventionFileName(dir, 'page')) continue
|
|
1039
|
-
const parts = routeParts(relative.replace(pageOrRouteTrailing(), ''))
|
|
1040
|
-
if (parts.interception) continue
|
|
1041
|
-
if (patterns.has(parts.pattern)) continue
|
|
1042
|
-
patterns.add(parts.pattern)
|
|
1043
|
-
|
|
1044
|
-
const layoutFiles = findLayouts(appPath, file)
|
|
1045
|
-
const templateFiles = findConventionFiles(appPath, file, 'template')
|
|
1045
|
+
if (conventionFileName(dir, 'page')) continue
|
|
1046
|
+
const parts = routeParts(relative.replace(pageOrRouteTrailing(), ''))
|
|
1047
|
+
if (parts.interception) continue
|
|
1048
|
+
if (patterns.has(parts.pattern)) continue
|
|
1049
|
+
patterns.add(parts.pattern)
|
|
1050
|
+
|
|
1051
|
+
const layoutFiles = findLayouts(appPath, file)
|
|
1052
|
+
const templateFiles = findConventionFiles(appPath, file, 'template')
|
|
1046
1053
|
const specialFiles = boundaryConventions().flatMap(name =>
|
|
1047
1054
|
findConventionFiles(appPath, file, name),
|
|
1048
|
-
)
|
|
1049
|
-
const slotDirs = chainSlotDirs(appPath, file)
|
|
1050
|
-
const slotFiles = slotTreeFiles(slotDirs)
|
|
1051
|
-
const extraBoundaries = new Set(extraBoundaryConventionNames())
|
|
1052
|
-
const forbiddenPaths =
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
: [];
|
|
1060
|
-
const templatePaths = templateFiles.filter(item => existsSync(item));
|
|
1055
|
+
)
|
|
1056
|
+
const slotDirs = chainSlotDirs(appPath, file)
|
|
1057
|
+
const slotFiles = slotTreeFiles(slotDirs)
|
|
1058
|
+
const extraBoundaries = new Set(extraBoundaryConventionNames())
|
|
1059
|
+
const forbiddenPaths = extraBoundaries.has('forbidden')
|
|
1060
|
+
? findConventionFiles(appPath, file, 'forbidden').filter(item => existsSync(item))
|
|
1061
|
+
: []
|
|
1062
|
+
const unauthorizedPaths = extraBoundaries.has('unauthorized')
|
|
1063
|
+
? findConventionFiles(appPath, file, 'unauthorized').filter(item => existsSync(item))
|
|
1064
|
+
: []
|
|
1065
|
+
const templatePaths = templateFiles.filter(item => existsSync(item))
|
|
1061
1066
|
|
|
1062
1067
|
routes.push(
|
|
1063
1068
|
withDeferredFacts(
|
|
@@ -1088,12 +1093,12 @@ function appendDefaultDerivedRoutes(
|
|
|
1088
1093
|
table,
|
|
1089
1094
|
}),
|
|
1090
1095
|
),
|
|
1091
|
-
)
|
|
1096
|
+
)
|
|
1092
1097
|
}
|
|
1093
1098
|
}
|
|
1094
1099
|
|
|
1095
1100
|
export function findLayouts(appPath: string, routeFilePath: string) {
|
|
1096
|
-
return findConventionFiles(appPath, routeFilePath, 'layout')
|
|
1101
|
+
return findConventionFiles(appPath, routeFilePath, 'layout')
|
|
1097
1102
|
}
|
|
1098
1103
|
|
|
1099
1104
|
/**
|
|
@@ -1105,9 +1110,9 @@ export function findLayouts(appPath: string, routeFilePath: string) {
|
|
|
1105
1110
|
* Returns [] when the app has no root not-found file (built-in fallback).
|
|
1106
1111
|
*/
|
|
1107
1112
|
export async function collectNotFoundCss(appPath: string): Promise<string[]> {
|
|
1108
|
-
const notFoundFile = conventionFileName(appPath, 'not-found')
|
|
1109
|
-
if (!notFoundFile) return []
|
|
1110
|
-
return collectFileCss(appPath, [notFoundFile])
|
|
1113
|
+
const notFoundFile = conventionFileName(appPath, 'not-found')
|
|
1114
|
+
if (!notFoundFile) return []
|
|
1115
|
+
return collectFileCss(appPath, [notFoundFile])
|
|
1111
1116
|
}
|
|
1112
1117
|
|
|
1113
1118
|
/**
|
|
@@ -1123,13 +1128,13 @@ export async function collectFileCss(
|
|
|
1123
1128
|
files: string[],
|
|
1124
1129
|
options: { includeGlobalCss?: boolean } = {},
|
|
1125
1130
|
): Promise<string[]> {
|
|
1126
|
-
const existing = files.filter(file => existsSync(file))
|
|
1127
|
-
if (existing.length === 0) return []
|
|
1131
|
+
const existing = files.filter(file => existsSync(file))
|
|
1132
|
+
if (existing.length === 0) return []
|
|
1128
1133
|
const context = createScanContext(
|
|
1129
1134
|
appPath,
|
|
1130
1135
|
options.includeGlobalCss ? () => new Set<string>() : undefined,
|
|
1131
|
-
)
|
|
1132
|
-
return collectCssImports(context, existing)
|
|
1136
|
+
)
|
|
1137
|
+
return collectCssImports(context, existing)
|
|
1133
1138
|
}
|
|
1134
1139
|
|
|
1135
1140
|
/**
|
|
@@ -1140,52 +1145,52 @@ export async function collectFileCss(
|
|
|
1140
1145
|
* byte-identical behavior.
|
|
1141
1146
|
*/
|
|
1142
1147
|
export function findConventionFiles(appPath: string, routeFilePath: string, name: string) {
|
|
1143
|
-
const base = conventionBase(name)
|
|
1144
|
-
const files: string[] = []
|
|
1145
|
-
let dir = path.dirname(routeFilePath)
|
|
1148
|
+
const base = conventionBase(name)
|
|
1149
|
+
const files: string[] = []
|
|
1150
|
+
let dir = path.dirname(routeFilePath)
|
|
1146
1151
|
|
|
1147
1152
|
while (dir.startsWith(appPath)) {
|
|
1148
|
-
files.push(conventionFileName(dir, base) ?? path.join(dir, `${base}.tsx`))
|
|
1149
|
-
if (dir === appPath) break
|
|
1150
|
-
dir = path.dirname(dir)
|
|
1153
|
+
files.push(conventionFileName(dir, base) ?? path.join(dir, `${base}.tsx`))
|
|
1154
|
+
if (dir === appPath) break
|
|
1155
|
+
dir = path.dirname(dir)
|
|
1151
1156
|
}
|
|
1152
1157
|
|
|
1153
|
-
return files.reverse()
|
|
1158
|
+
return files.reverse()
|
|
1154
1159
|
}
|
|
1155
1160
|
|
|
1156
1161
|
function conventionBase(name: string) {
|
|
1157
|
-
const match = new RegExp(`\\.(${pageExtensions().join('|')})$`).exec(name)
|
|
1158
|
-
return match ? name.slice(0, -match[0].length) : name
|
|
1162
|
+
const match = new RegExp(`\\.(${pageExtensions().join('|')})$`).exec(name)
|
|
1163
|
+
return match ? name.slice(0, -match[0].length) : name
|
|
1159
1164
|
}
|
|
1160
1165
|
|
|
1161
1166
|
/** App-root `global-error.*` file, if present (BuildManifest.globalErrorFile). */
|
|
1162
1167
|
export function findGlobalError(appPath: string) {
|
|
1163
|
-
return conventionFileName(appPath, globalErrorBase)
|
|
1168
|
+
return conventionFileName(appPath, globalErrorBase)
|
|
1164
1169
|
}
|
|
1165
1170
|
|
|
1166
1171
|
export function matchRoute(routes: RouteManifestEntry[], pathname: string) {
|
|
1167
1172
|
// Trailing-slash tolerance: '/route/' matches the same entry as '/route'
|
|
1168
1173
|
// (Next redirects these away; static file serving already accepts both).
|
|
1169
|
-
const normalized = normalizePathname(pathname)
|
|
1174
|
+
const normalized = normalizePathname(pathname)
|
|
1170
1175
|
for (const route of routes) {
|
|
1171
1176
|
// Interception entries never match a plain (hard) request; they apply
|
|
1172
1177
|
// only through matchInterception on soft navigations.
|
|
1173
|
-
if (route.interception) continue
|
|
1174
|
-
const match = routeRegex(route).exec(normalized)
|
|
1175
|
-
if (!match) continue
|
|
1176
|
-
return { route, params: routeMatchParams(route, match) }
|
|
1178
|
+
if (route.interception) continue
|
|
1179
|
+
const match = routeRegex(route).exec(normalized)
|
|
1180
|
+
if (!match) continue
|
|
1181
|
+
return { route, params: routeMatchParams(route, match) }
|
|
1177
1182
|
}
|
|
1178
|
-
return null
|
|
1183
|
+
return null
|
|
1179
1184
|
}
|
|
1180
1185
|
|
|
1181
1186
|
export function normalizePathname(pathname: string) {
|
|
1182
|
-
return pathname.length > 1 ? pathname.replace(/\/+$/, '') || '/' : pathname
|
|
1187
|
+
return pathname.length > 1 ? pathname.replace(/\/+$/, '') || '/' : pathname
|
|
1183
1188
|
}
|
|
1184
1189
|
|
|
1185
1190
|
function routeMatchParams(route: RouteManifestEntry, match: RegExpExecArray) {
|
|
1186
|
-
const params: Record<string, RouteParamValue> = {}
|
|
1191
|
+
const params: Record<string, RouteParamValue> = {}
|
|
1187
1192
|
route.params.forEach((name, index) => {
|
|
1188
|
-
const decoded = safeDecode(match[index + 1] ?? '')
|
|
1193
|
+
const decoded = safeDecode(match[index + 1] ?? '')
|
|
1189
1194
|
// A runtime request whose segment IS the dynamic placeholder for its own
|
|
1190
1195
|
// param (`[slug]`, whether sent raw or as `%5Bslug%5D`) is Next's "params
|
|
1191
1196
|
// placeholder": it must surface the ENCODED placeholder (`%5Bslug%5D`), not
|
|
@@ -1194,20 +1199,20 @@ function routeMatchParams(route: RouteManifestEntry, match: RegExpExecArray) {
|
|
|
1194
1199
|
// boundary); Next keeps it URL-encoded to render at runtime instead.
|
|
1195
1200
|
// Fallback-shell generation injects its placeholder params through a
|
|
1196
1201
|
// separate build path, so this only affects live requests.
|
|
1197
|
-
params[name] = decoded === `[${name}]` ? encodeURIComponent(decoded) : decoded
|
|
1198
|
-
})
|
|
1202
|
+
params[name] = decoded === `[${name}]` ? encodeURIComponent(decoded) : decoded
|
|
1203
|
+
})
|
|
1199
1204
|
if (route.catchAll) {
|
|
1200
1205
|
params[route.catchAll] = (match[route.params.length + 1] ?? '')
|
|
1201
1206
|
.split('/')
|
|
1202
1207
|
.filter(Boolean)
|
|
1203
|
-
.map(safeDecode)
|
|
1208
|
+
.map(safeDecode)
|
|
1204
1209
|
}
|
|
1205
|
-
return params
|
|
1210
|
+
return params
|
|
1206
1211
|
}
|
|
1207
1212
|
|
|
1208
1213
|
export interface InterceptionRouteMatch {
|
|
1209
|
-
route: RouteManifestEntry
|
|
1210
|
-
params: Record<string, RouteParamValue
|
|
1214
|
+
route: RouteManifestEntry
|
|
1215
|
+
params: Record<string, RouteParamValue>
|
|
1211
1216
|
}
|
|
1212
1217
|
|
|
1213
1218
|
/**
|
|
@@ -1222,34 +1227,34 @@ export function matchInterception(
|
|
|
1222
1227
|
pathname: string,
|
|
1223
1228
|
fromPath: string | undefined,
|
|
1224
1229
|
): InterceptionRouteMatch | null {
|
|
1225
|
-
if (!fromPath) return null
|
|
1226
|
-
const target = normalizePathname(pathname)
|
|
1227
|
-
const from = normalizePathname(fromPath)
|
|
1228
|
-
if (target === from) return null
|
|
1229
|
-
let best: InterceptionRouteMatch | null = null
|
|
1230
|
-
let bestDepth = -1
|
|
1230
|
+
if (!fromPath) return null
|
|
1231
|
+
const target = normalizePathname(pathname)
|
|
1232
|
+
const from = normalizePathname(fromPath)
|
|
1233
|
+
if (target === from) return null
|
|
1234
|
+
let best: InterceptionRouteMatch | null = null
|
|
1235
|
+
let bestDepth = -1
|
|
1231
1236
|
for (const route of routes) {
|
|
1232
|
-
const interception = route.interception
|
|
1233
|
-
if (!interception) continue
|
|
1234
|
-
const match = routeRegex(route).exec(target)
|
|
1235
|
-
if (!match) continue
|
|
1236
|
-
const basePattern = interception.basePattern ? `/${interception.basePattern}` : ''
|
|
1237
|
-
if (!new RegExp(`^${basePattern}(?:/.*)?$`).test(from)) continue
|
|
1238
|
-
const depth = interception.base.split('/').filter(Boolean).length
|
|
1239
|
-
if (depth <= bestDepth) continue
|
|
1240
|
-
bestDepth = depth
|
|
1241
|
-
best = { route, params: routeMatchParams(route, match) }
|
|
1237
|
+
const interception = route.interception
|
|
1238
|
+
if (!interception) continue
|
|
1239
|
+
const match = routeRegex(route).exec(target)
|
|
1240
|
+
if (!match) continue
|
|
1241
|
+
const basePattern = interception.basePattern ? `/${interception.basePattern}` : ''
|
|
1242
|
+
if (!new RegExp(`^${basePattern}(?:/.*)?$`).test(from)) continue
|
|
1243
|
+
const depth = interception.base.split('/').filter(Boolean).length
|
|
1244
|
+
if (depth <= bestDepth) continue
|
|
1245
|
+
bestDepth = depth
|
|
1246
|
+
best = { route, params: routeMatchParams(route, match) }
|
|
1242
1247
|
}
|
|
1243
|
-
return best
|
|
1248
|
+
return best
|
|
1244
1249
|
}
|
|
1245
1250
|
|
|
1246
1251
|
export interface RouteRenderSelection {
|
|
1247
|
-
route: RouteManifestEntry
|
|
1248
|
-
params: Record<string, RouteParamValue
|
|
1252
|
+
route: RouteManifestEntry
|
|
1253
|
+
params: Record<string, RouteParamValue>
|
|
1249
1254
|
/** Pathname the children tree renders from (normalized). */
|
|
1250
|
-
childrenPath: string
|
|
1255
|
+
childrenPath: string
|
|
1251
1256
|
/** Internal pathname after rewrites, used to resolve interception slots. */
|
|
1252
|
-
targetPath: string
|
|
1257
|
+
targetPath: string
|
|
1253
1258
|
}
|
|
1254
1259
|
|
|
1255
1260
|
/**
|
|
@@ -1263,9 +1268,9 @@ export function selectRouteForRequest(
|
|
|
1263
1268
|
pathname: string,
|
|
1264
1269
|
nav?: import('../types').NavState,
|
|
1265
1270
|
): RouteRenderSelection | null {
|
|
1266
|
-
const target = normalizePathname(pathname)
|
|
1271
|
+
const target = normalizePathname(pathname)
|
|
1267
1272
|
if (nav) {
|
|
1268
|
-
const intercepted = matchInterception(routes, target, nav.children)
|
|
1273
|
+
const intercepted = matchInterception(routes, target, nav.children)
|
|
1269
1274
|
if (intercepted) {
|
|
1270
1275
|
if (!intercepted.route.interception?.slotDir) {
|
|
1271
1276
|
return {
|
|
@@ -1273,22 +1278,24 @@ export function selectRouteForRequest(
|
|
|
1273
1278
|
params: intercepted.params,
|
|
1274
1279
|
childrenPath: target,
|
|
1275
1280
|
targetPath: target,
|
|
1276
|
-
}
|
|
1281
|
+
}
|
|
1277
1282
|
}
|
|
1278
|
-
const host = hostMatch(routes, nav.children)
|
|
1279
|
-
if (host) return { ...host, targetPath: target }
|
|
1283
|
+
const host = hostMatch(routes, nav.children)
|
|
1284
|
+
if (host) return { ...host, targetPath: target }
|
|
1280
1285
|
}
|
|
1281
1286
|
}
|
|
1282
|
-
const matched = matchRoute(routes, target)
|
|
1283
|
-
if (!matched) return null
|
|
1287
|
+
const matched = matchRoute(routes, target)
|
|
1288
|
+
if (!matched) return null
|
|
1284
1289
|
if (nav && matched.route.synthetic && nav.children) {
|
|
1285
|
-
const primary = matchRoute(
|
|
1290
|
+
const primary = matchRoute(
|
|
1291
|
+
routes.filter(route => !route.synthetic),
|
|
1292
|
+
target,
|
|
1293
|
+
)
|
|
1286
1294
|
const syntheticOwner = matched.route.syntheticSlotDir
|
|
1287
1295
|
? path.dirname(matched.route.syntheticSlotDir)
|
|
1288
|
-
: undefined
|
|
1289
|
-
const primaryFromOwner =
|
|
1290
|
-
? path.relative(syntheticOwner, primary.route.file)
|
|
1291
|
-
: undefined;
|
|
1296
|
+
: undefined
|
|
1297
|
+
const primaryFromOwner =
|
|
1298
|
+
primary && syntheticOwner ? path.relative(syntheticOwner, primary.route.file) : undefined
|
|
1292
1299
|
if (
|
|
1293
1300
|
primary &&
|
|
1294
1301
|
primaryFromOwner !== undefined &&
|
|
@@ -1300,16 +1307,16 @@ export function selectRouteForRequest(
|
|
|
1300
1307
|
params: primary.params,
|
|
1301
1308
|
childrenPath: target,
|
|
1302
1309
|
targetPath: target,
|
|
1303
|
-
}
|
|
1310
|
+
}
|
|
1304
1311
|
}
|
|
1305
|
-
const host = hostMatch(routes, nav.children)
|
|
1312
|
+
const host = hostMatch(routes, nav.children)
|
|
1306
1313
|
if (
|
|
1307
1314
|
host &&
|
|
1308
1315
|
host.route !== matched.route &&
|
|
1309
1316
|
matched.route.syntheticSlotDir &&
|
|
1310
1317
|
host.route.slotDirs?.includes(matched.route.syntheticSlotDir)
|
|
1311
1318
|
) {
|
|
1312
|
-
return { ...host, targetPath: target }
|
|
1319
|
+
return { ...host, targetPath: target }
|
|
1313
1320
|
}
|
|
1314
1321
|
}
|
|
1315
1322
|
return {
|
|
@@ -1317,7 +1324,7 @@ export function selectRouteForRequest(
|
|
|
1317
1324
|
params: matched.params,
|
|
1318
1325
|
childrenPath: target,
|
|
1319
1326
|
targetPath: target,
|
|
1320
|
-
}
|
|
1327
|
+
}
|
|
1321
1328
|
}
|
|
1322
1329
|
|
|
1323
1330
|
/**
|
|
@@ -1326,135 +1333,133 @@ export function selectRouteForRequest(
|
|
|
1326
1333
|
* parallel-route state (URI-encoded JSON).
|
|
1327
1334
|
*/
|
|
1328
1335
|
export function parseNavState(request: Request): NavState | undefined {
|
|
1329
|
-
if (!request.headers.get('x-pnext-soft-nav')) return undefined
|
|
1330
|
-
const raw = request.headers.get('x-pnext-nav-state')
|
|
1331
|
-
if (!raw) return {}
|
|
1336
|
+
if (!request.headers.get('x-pnext-soft-nav')) return undefined
|
|
1337
|
+
const raw = request.headers.get('x-pnext-nav-state')
|
|
1338
|
+
if (!raw) return {}
|
|
1332
1339
|
try {
|
|
1333
|
-
const parsed = JSON.parse(decodeURIComponent(raw)) as unknown
|
|
1334
|
-
if (parsed && typeof parsed === 'object') return parsed
|
|
1340
|
+
const parsed = JSON.parse(decodeURIComponent(raw)) as unknown
|
|
1341
|
+
if (parsed && typeof parsed === 'object') return parsed
|
|
1335
1342
|
} catch {
|
|
1336
1343
|
// Malformed state degrades to a plain soft navigation.
|
|
1337
1344
|
}
|
|
1338
|
-
return {}
|
|
1345
|
+
return {}
|
|
1339
1346
|
}
|
|
1340
1347
|
|
|
1341
1348
|
function hostMatch(
|
|
1342
1349
|
routes: RouteManifestEntry[],
|
|
1343
1350
|
fromPath: string | undefined,
|
|
1344
1351
|
): RouteRenderSelection | null {
|
|
1345
|
-
if (!fromPath) return null
|
|
1346
|
-
const from = normalizePathname(fromPath)
|
|
1347
|
-
const host = matchRoute(routes, from)
|
|
1348
|
-
if (host?.route.kind !== 'page') return null
|
|
1349
|
-
return { route: host.route, params: host.params, childrenPath: from, targetPath: from }
|
|
1352
|
+
if (!fromPath) return null
|
|
1353
|
+
const from = normalizePathname(fromPath)
|
|
1354
|
+
const host = matchRoute(routes, from)
|
|
1355
|
+
if (host?.route.kind !== 'page') return null
|
|
1356
|
+
return { route: host.route, params: host.params, childrenPath: from, targetPath: from }
|
|
1350
1357
|
}
|
|
1351
1358
|
|
|
1352
|
-
const routeRegexCache = new WeakMap<RouteManifestEntry, RegExp>()
|
|
1359
|
+
const routeRegexCache = new WeakMap<RouteManifestEntry, RegExp>()
|
|
1353
1360
|
|
|
1354
1361
|
function routeRegex(route: RouteManifestEntry) {
|
|
1355
|
-
let regex = routeRegexCache.get(route)
|
|
1362
|
+
let regex = routeRegexCache.get(route)
|
|
1356
1363
|
if (!regex) {
|
|
1357
|
-
regex = new RegExp(`^${route.pattern}$`)
|
|
1358
|
-
routeRegexCache.set(route, regex)
|
|
1364
|
+
regex = new RegExp(`^${route.pattern}$`)
|
|
1365
|
+
routeRegexCache.set(route, regex)
|
|
1359
1366
|
}
|
|
1360
|
-
return regex
|
|
1367
|
+
return regex
|
|
1361
1368
|
}
|
|
1362
1369
|
|
|
1363
1370
|
interface RouteSegmentToken {
|
|
1364
|
-
route: string
|
|
1371
|
+
route: string
|
|
1365
1372
|
/** Regex piece; absent for the optional catch-all (assembled specially). */
|
|
1366
|
-
pattern?: string
|
|
1367
|
-
param?: string
|
|
1368
|
-
catchAll?: string
|
|
1369
|
-
catchAllOptional?: boolean
|
|
1373
|
+
pattern?: string
|
|
1374
|
+
param?: string
|
|
1375
|
+
catchAll?: string
|
|
1376
|
+
catchAllOptional?: boolean
|
|
1370
1377
|
}
|
|
1371
1378
|
|
|
1372
1379
|
function segmentTokenOf(bare: string): RouteSegmentToken {
|
|
1373
|
-
const optionalCatchAllMatch = /^\[\[\.\.\.([^\]]+)\]\]$/.exec(bare)
|
|
1380
|
+
const optionalCatchAllMatch = /^\[\[\.\.\.([^\]]+)\]\]$/.exec(bare)
|
|
1374
1381
|
if (optionalCatchAllMatch?.[1]) {
|
|
1375
|
-
const name = optionalCatchAllMatch[1]
|
|
1376
|
-
return { route: `:${name}*`, catchAll: name, catchAllOptional: true }
|
|
1382
|
+
const name = optionalCatchAllMatch[1]
|
|
1383
|
+
return { route: `:${name}*`, catchAll: name, catchAllOptional: true }
|
|
1377
1384
|
}
|
|
1378
|
-
const catchAllMatch = /^\[\.\.\.([^\]]+)\]$/.exec(bare)
|
|
1385
|
+
const catchAllMatch = /^\[\.\.\.([^\]]+)\]$/.exec(bare)
|
|
1379
1386
|
if (catchAllMatch?.[1]) {
|
|
1380
|
-
return { route: `:${catchAllMatch[1]}*`, pattern: '(.*)', catchAll: catchAllMatch[1] }
|
|
1387
|
+
return { route: `:${catchAllMatch[1]}*`, pattern: '(.*)', catchAll: catchAllMatch[1] }
|
|
1381
1388
|
}
|
|
1382
|
-
const paramMatch = /^\[([^\]]+)\]$/.exec(bare)
|
|
1389
|
+
const paramMatch = /^\[([^\]]+)\]$/.exec(bare)
|
|
1383
1390
|
if (paramMatch?.[1]) {
|
|
1384
|
-
return { route: `:${paramMatch[1]}`, pattern: '([^/]+)', param: paramMatch[1] }
|
|
1391
|
+
return { route: `:${paramMatch[1]}`, pattern: '([^/]+)', param: paramMatch[1] }
|
|
1385
1392
|
}
|
|
1386
|
-
const literal = decodeLiteralSegment(bare)
|
|
1387
|
-
return { route: literal, pattern: escapeRegex(literal) }
|
|
1393
|
+
const literal = decodeLiteralSegment(bare)
|
|
1394
|
+
return { route: literal, pattern: escapeRegex(literal) }
|
|
1388
1395
|
}
|
|
1389
1396
|
|
|
1390
1397
|
// A path is private when any of its directory segments begins with `_`, except
|
|
1391
1398
|
// route groups `(name)`, slots `@slot`, and `%5F`-encoded segments (which
|
|
1392
1399
|
// decode to a routable leading underscore). The filename segment is excluded.
|
|
1393
1400
|
function isPrivateAppPath(relative: string): boolean {
|
|
1394
|
-
const segments = relative.split('/')
|
|
1401
|
+
const segments = relative.split('/')
|
|
1395
1402
|
return segments
|
|
1396
1403
|
.slice(0, -1)
|
|
1397
|
-
.some(
|
|
1398
|
-
segment => segment.startsWith('_') && !isGroupSegment(segment) && !isSlotSegment(segment),
|
|
1399
|
-
);
|
|
1404
|
+
.some(segment => segment.startsWith('_') && !isGroupSegment(segment) && !isSlotSegment(segment))
|
|
1400
1405
|
}
|
|
1401
1406
|
|
|
1402
1407
|
// A literal (non-dynamic) path segment: `%5F` decodes to `_` so a directory
|
|
1403
1408
|
// named `%5Ffoo` on disk routes as `/_foo`. Other percent escapes are left
|
|
1404
1409
|
// as-is (the router decodes params at match time).
|
|
1405
1410
|
function decodeLiteralSegment(segment: string): string {
|
|
1406
|
-
return segment.replace(/%5[Ff]/g, '_')
|
|
1411
|
+
return segment.replace(/%5[Ff]/g, '_')
|
|
1407
1412
|
}
|
|
1408
1413
|
|
|
1409
1414
|
function routeParts(routeDir: string): RouteParts {
|
|
1410
|
-
const segments = routeDir.split('/').filter(Boolean)
|
|
1411
|
-
let tokens: RouteSegmentToken[] = []
|
|
1412
|
-
let interception: RouteParts['interception']
|
|
1415
|
+
const segments = routeDir.split('/').filter(Boolean)
|
|
1416
|
+
let tokens: RouteSegmentToken[] = []
|
|
1417
|
+
let interception: RouteParts['interception']
|
|
1413
1418
|
|
|
1414
1419
|
for (const segment of segments) {
|
|
1415
1420
|
// Route group `(name)`: organizational only. `@slot`: no path segment.
|
|
1416
|
-
if (isGroupSegment(segment) || isSlotSegment(segment)) continue
|
|
1421
|
+
if (isGroupSegment(segment) || isSlotSegment(segment)) continue
|
|
1417
1422
|
|
|
1418
|
-
const marker = interceptionMarkerOf(segment)
|
|
1419
|
-
let bare = segment
|
|
1423
|
+
const marker = interceptionMarkerOf(segment)
|
|
1424
|
+
let bare = segment
|
|
1420
1425
|
if (marker) {
|
|
1421
1426
|
// The interception target rewinds `levels` URL segments from the
|
|
1422
1427
|
// marker's position; the segments before the marker form the base the
|
|
1423
1428
|
// interceptor renders within.
|
|
1424
|
-
bare = segment.slice(marker.length)
|
|
1425
|
-
const base = tokens.slice()
|
|
1426
|
-
const levels = interceptionMarkerLevels(marker)
|
|
1429
|
+
bare = segment.slice(marker.length)
|
|
1430
|
+
const base = tokens.slice()
|
|
1431
|
+
const levels = interceptionMarkerLevels(marker)
|
|
1427
1432
|
tokens =
|
|
1428
1433
|
levels === Number.POSITIVE_INFINITY
|
|
1429
1434
|
? []
|
|
1430
|
-
: tokens.slice(0, Math.max(0, tokens.length - levels))
|
|
1435
|
+
: tokens.slice(0, Math.max(0, tokens.length - levels))
|
|
1431
1436
|
interception = {
|
|
1432
1437
|
marker,
|
|
1433
1438
|
base: `/${base.map(token => token.route).join('/')}`.replace(/\/$/, '') || '/',
|
|
1434
1439
|
basePattern: base.map(token => token.pattern ?? '(.*)').join('/'),
|
|
1435
|
-
}
|
|
1440
|
+
}
|
|
1436
1441
|
}
|
|
1437
1442
|
|
|
1438
|
-
tokens.push(segmentTokenOf(bare))
|
|
1443
|
+
tokens.push(segmentTokenOf(bare))
|
|
1439
1444
|
}
|
|
1440
1445
|
|
|
1441
|
-
const params = tokens.filter(token => token.param).map(token => token.param!)
|
|
1442
|
-
const catchAllToken = tokens.find(token => token.catchAll)
|
|
1443
|
-
const routeSegments = tokens.map(token => token.route)
|
|
1446
|
+
const params = tokens.filter(token => token.param).map(token => token.param!)
|
|
1447
|
+
const catchAllToken = tokens.find(token => token.catchAll)
|
|
1448
|
+
const routeSegments = tokens.map(token => token.route)
|
|
1444
1449
|
const patternSegments = tokens
|
|
1445
1450
|
.filter(token => !token.catchAllOptional)
|
|
1446
|
-
.map(token => token.pattern!)
|
|
1451
|
+
.map(token => token.pattern!)
|
|
1447
1452
|
|
|
1448
|
-
const route = `/${routeSegments.join('/')}`.replace(/\/$/, '') || '/'
|
|
1449
|
-
let pattern: string
|
|
1453
|
+
const route = `/${routeSegments.join('/')}`.replace(/\/$/, '') || '/'
|
|
1454
|
+
let pattern: string
|
|
1450
1455
|
if (catchAllToken?.catchAllOptional) {
|
|
1451
1456
|
// `[[...slug]]` matches the base path and any nested path: the whole
|
|
1452
1457
|
// trailing group (including its leading slash) is optional.
|
|
1453
|
-
const prefix = patternSegments.length > 0 ? `/${patternSegments.join('/')}` : ''
|
|
1454
|
-
pattern = `${prefix}(?:/(.*))
|
|
1455
|
-
if (pattern === '(?:/(.*))?') pattern = '/?(?:(.*))?'
|
|
1458
|
+
const prefix = patternSegments.length > 0 ? `/${patternSegments.join('/')}` : ''
|
|
1459
|
+
pattern = `${prefix}(?:/(.*))?`
|
|
1460
|
+
if (pattern === '(?:/(.*))?') pattern = '/?(?:(.*))?'
|
|
1456
1461
|
} else {
|
|
1457
|
-
pattern = route === '/' ? '/' : `/${patternSegments.join('/')}
|
|
1462
|
+
pattern = route === '/' ? '/' : `/${patternSegments.join('/')}`
|
|
1458
1463
|
}
|
|
1459
1464
|
return {
|
|
1460
1465
|
route,
|
|
@@ -1463,59 +1468,59 @@ function routeParts(routeDir: string): RouteParts {
|
|
|
1463
1468
|
catchAll: catchAllToken?.catchAll,
|
|
1464
1469
|
catchAllOptional: Boolean(catchAllToken?.catchAllOptional),
|
|
1465
1470
|
interception,
|
|
1466
|
-
}
|
|
1471
|
+
}
|
|
1467
1472
|
}
|
|
1468
1473
|
|
|
1469
1474
|
interface DynamicMetadataRouteParts extends RouteParts {
|
|
1470
|
-
metadataRoute: MetadataRouteEntry
|
|
1475
|
+
metadataRoute: MetadataRouteEntry
|
|
1471
1476
|
}
|
|
1472
1477
|
|
|
1473
1478
|
function dynamicMetadataRouteParts(
|
|
1474
1479
|
relative: string,
|
|
1475
1480
|
source: string,
|
|
1476
1481
|
): DynamicMetadataRouteParts | undefined {
|
|
1477
|
-
const name = path.basename(relative)
|
|
1478
|
-
const dir = path.dirname(relative) === '.' ? '' : path.dirname(relative)
|
|
1479
|
-
const root = dir === ''
|
|
1480
|
-
const extension = path.extname(name)
|
|
1481
|
-
const base = name.slice(0, -extension.length)
|
|
1482
|
-
const kind = dynamicMetadataKind(base, root)
|
|
1483
|
-
if (!kind) return undefined
|
|
1484
|
-
|
|
1485
|
-
const prefix = routeParts(dir)
|
|
1486
|
-
const generatedParam = metadataGeneratedParam(kind, source)
|
|
1487
|
-
const final = dynamicMetadataFinalSegment(kind, base, dir, Boolean(generatedParam))
|
|
1482
|
+
const name = path.basename(relative)
|
|
1483
|
+
const dir = path.dirname(relative) === '.' ? '' : path.dirname(relative)
|
|
1484
|
+
const root = dir === ''
|
|
1485
|
+
const extension = path.extname(name)
|
|
1486
|
+
const base = name.slice(0, -extension.length)
|
|
1487
|
+
const kind = dynamicMetadataKind(base, root)
|
|
1488
|
+
if (!kind) return undefined
|
|
1489
|
+
|
|
1490
|
+
const prefix = routeParts(dir)
|
|
1491
|
+
const generatedParam = metadataGeneratedParam(kind, source)
|
|
1492
|
+
const final = dynamicMetadataFinalSegment(kind, base, dir, Boolean(generatedParam))
|
|
1488
1493
|
return {
|
|
1489
1494
|
...appendRouteParts(prefix, final.route, final.pattern, generatedParam ? [generatedParam] : []),
|
|
1490
1495
|
metadataRoute: {
|
|
1491
1496
|
kind,
|
|
1492
1497
|
...(generatedParam ? { generatedParam } : {}),
|
|
1493
1498
|
},
|
|
1494
|
-
}
|
|
1499
|
+
}
|
|
1495
1500
|
}
|
|
1496
1501
|
|
|
1497
1502
|
function dynamicMetadataKind(base: string, root: boolean): MetadataRouteKind | undefined {
|
|
1498
|
-
if (base === 'robots') return root ? 'robots' : undefined
|
|
1499
|
-
if (base === 'sitemap') return 'sitemap'
|
|
1500
|
-
if (base === 'manifest') return root ? 'manifest' : undefined
|
|
1501
|
-
if (/^icon\d*$/.test(base)) return 'icon'
|
|
1502
|
-
if (/^apple-icon\d*$/.test(base)) return 'apple-icon'
|
|
1503
|
-
if (/^opengraph-image\d*$/.test(base)) return 'opengraph-image'
|
|
1504
|
-
if (/^twitter-image\d*$/.test(base)) return 'twitter-image'
|
|
1505
|
-
return undefined
|
|
1503
|
+
if (base === 'robots') return root ? 'robots' : undefined
|
|
1504
|
+
if (base === 'sitemap') return 'sitemap'
|
|
1505
|
+
if (base === 'manifest') return root ? 'manifest' : undefined
|
|
1506
|
+
if (/^icon\d*$/.test(base)) return 'icon'
|
|
1507
|
+
if (/^apple-icon\d*$/.test(base)) return 'apple-icon'
|
|
1508
|
+
if (/^opengraph-image\d*$/.test(base)) return 'opengraph-image'
|
|
1509
|
+
if (/^twitter-image\d*$/.test(base)) return 'twitter-image'
|
|
1510
|
+
return undefined
|
|
1506
1511
|
}
|
|
1507
1512
|
|
|
1508
1513
|
function metadataGeneratedParam(kind: MetadataRouteKind, source: string) {
|
|
1509
1514
|
if (kind === 'sitemap' && /\bexport\s+(?:async\s+)?function\s+generateSitemaps\b/.test(source)) {
|
|
1510
|
-
return '__metadata_id__'
|
|
1515
|
+
return '__metadata_id__'
|
|
1511
1516
|
}
|
|
1512
1517
|
if (
|
|
1513
1518
|
metadataImageKind(kind) &&
|
|
1514
1519
|
/\bexport\s+(?:async\s+)?function\s+generateImageMetadata\b/.test(source)
|
|
1515
1520
|
) {
|
|
1516
|
-
return '__metadata_id__'
|
|
1521
|
+
return '__metadata_id__'
|
|
1517
1522
|
}
|
|
1518
|
-
return undefined
|
|
1523
|
+
return undefined
|
|
1519
1524
|
}
|
|
1520
1525
|
|
|
1521
1526
|
function dynamicMetadataFinalSegment(
|
|
@@ -1524,31 +1529,31 @@ function dynamicMetadataFinalSegment(
|
|
|
1524
1529
|
relativeDir: string,
|
|
1525
1530
|
generated: boolean,
|
|
1526
1531
|
) {
|
|
1527
|
-
if (kind === 'robots') return { route: 'robots.txt', pattern: 'robots\\.txt' }
|
|
1532
|
+
if (kind === 'robots') return { route: 'robots.txt', pattern: 'robots\\.txt' }
|
|
1528
1533
|
if (kind === 'manifest')
|
|
1529
|
-
return { route: 'manifest.webmanifest', pattern: 'manifest\\.webmanifest' }
|
|
1534
|
+
return { route: 'manifest.webmanifest', pattern: 'manifest\\.webmanifest' }
|
|
1530
1535
|
if (kind === 'sitemap') {
|
|
1531
1536
|
return generated
|
|
1532
1537
|
? { route: 'sitemap/:id.xml', pattern: 'sitemap/([^/]+)\\.xml' }
|
|
1533
|
-
: { route: 'sitemap.xml', pattern: 'sitemap\\.xml' }
|
|
1538
|
+
: { route: 'sitemap.xml', pattern: 'sitemap\\.xml' }
|
|
1534
1539
|
}
|
|
1535
1540
|
|
|
1536
|
-
const suffix = metadataRouteSuffix(relativeDir)
|
|
1537
|
-
const imageBase = suffix ? `${base}-${suffix}` : base
|
|
1541
|
+
const suffix = metadataRouteSuffix(relativeDir)
|
|
1542
|
+
const imageBase = suffix ? `${base}-${suffix}` : base
|
|
1538
1543
|
return generated
|
|
1539
1544
|
? { route: `${imageBase}/:id`, pattern: `${escapeRegex(imageBase)}/([^/]+)` }
|
|
1540
|
-
: { route: imageBase, pattern: escapeRegex(imageBase) }
|
|
1545
|
+
: { route: imageBase, pattern: escapeRegex(imageBase) }
|
|
1541
1546
|
}
|
|
1542
1547
|
|
|
1543
1548
|
function appendRouteParts(parts: RouteParts, route: string, pattern: string, params: string[]) {
|
|
1544
|
-
const prefixRoute = parts.route === '/' ? '' : parts.route
|
|
1545
|
-
const prefixPattern = parts.pattern === '/' ? '' : parts.pattern
|
|
1549
|
+
const prefixRoute = parts.route === '/' ? '' : parts.route
|
|
1550
|
+
const prefixPattern = parts.pattern === '/' ? '' : parts.pattern
|
|
1546
1551
|
return {
|
|
1547
1552
|
...parts,
|
|
1548
1553
|
route: `${prefixRoute}/${route}`.replace(/\/$/, '') || '/',
|
|
1549
1554
|
pattern: `${prefixPattern}/${pattern}`,
|
|
1550
1555
|
params: [...parts.params, ...params],
|
|
1551
|
-
}
|
|
1556
|
+
}
|
|
1552
1557
|
}
|
|
1553
1558
|
|
|
1554
1559
|
function metadataImageKind(kind: MetadataRouteKind) {
|
|
@@ -1557,28 +1562,28 @@ function metadataImageKind(kind: MetadataRouteKind) {
|
|
|
1557
1562
|
kind === 'apple-icon' ||
|
|
1558
1563
|
kind === 'opengraph-image' ||
|
|
1559
1564
|
kind === 'twitter-image'
|
|
1560
|
-
)
|
|
1565
|
+
)
|
|
1561
1566
|
}
|
|
1562
1567
|
|
|
1563
1568
|
function metadataRouteUsesRequest(source: string) {
|
|
1564
1569
|
// routeHandlerUsesRequest already consults the compat usageDetection seam
|
|
1565
1570
|
// (next/headers, next/server connection(), ...).
|
|
1566
|
-
return routeHandlerUsesRequest(source)
|
|
1571
|
+
return routeHandlerUsesRequest(source)
|
|
1567
1572
|
}
|
|
1568
1573
|
|
|
1569
1574
|
function metadataRouteSuffix(relativeDir: string) {
|
|
1570
|
-
if (!relativeDir) return ''
|
|
1571
|
-
const segments = relativeDir.split('/').filter(Boolean)
|
|
1572
|
-
if (!segments.some(segment => isGroupSegment(segment) || isSlotSegment(segment))) return ''
|
|
1573
|
-
return djb2Hash(`/${relativeDir}`).toString(36).slice(0, 6)
|
|
1575
|
+
if (!relativeDir) return ''
|
|
1576
|
+
const segments = relativeDir.split('/').filter(Boolean)
|
|
1577
|
+
if (!segments.some(segment => isGroupSegment(segment) || isSlotSegment(segment))) return ''
|
|
1578
|
+
return djb2Hash(`/${relativeDir}`).toString(36).slice(0, 6)
|
|
1574
1579
|
}
|
|
1575
1580
|
|
|
1576
1581
|
function djb2Hash(value: string) {
|
|
1577
|
-
let hash = 5381
|
|
1582
|
+
let hash = 5381
|
|
1578
1583
|
for (let i = 0; i < value.length; i++) {
|
|
1579
|
-
hash = ((hash << 5) + hash + value.charCodeAt(i)) & 0xffffffff
|
|
1584
|
+
hash = ((hash << 5) + hash + value.charCodeAt(i)) & 0xffffffff
|
|
1580
1585
|
}
|
|
1581
|
-
return hash >>> 0
|
|
1586
|
+
return hash >>> 0
|
|
1582
1587
|
}
|
|
1583
1588
|
|
|
1584
1589
|
function inferRouteMode(
|
|
@@ -1586,72 +1591,72 @@ function inferRouteMode(
|
|
|
1586
1591
|
usesRequest: boolean,
|
|
1587
1592
|
config?: RouteSegmentConfig,
|
|
1588
1593
|
): RouteMode {
|
|
1589
|
-
if (config?.dynamic === 'force-dynamic') return 'dynamic'
|
|
1590
|
-
if (config?.revalidate === 0) return 'dynamic'
|
|
1591
|
-
if (config?.fetchCache === 'force-no-store') return 'dynamic'
|
|
1592
|
-
if (config?.dynamic === 'force-static') return 'static'
|
|
1593
|
-
if (config?.dynamic === 'error') return 'static'
|
|
1594
|
-
if (usesRequest) return 'dynamic'
|
|
1595
|
-
if (parts.params.length > 0 || parts.catchAll) return 'dynamic'
|
|
1596
|
-
return 'static'
|
|
1594
|
+
if (config?.dynamic === 'force-dynamic') return 'dynamic'
|
|
1595
|
+
if (config?.revalidate === 0) return 'dynamic'
|
|
1596
|
+
if (config?.fetchCache === 'force-no-store') return 'dynamic'
|
|
1597
|
+
if (config?.dynamic === 'force-static') return 'static'
|
|
1598
|
+
if (config?.dynamic === 'error') return 'static'
|
|
1599
|
+
if (usesRequest) return 'dynamic'
|
|
1600
|
+
if (parts.params.length > 0 || parts.catchAll) return 'dynamic'
|
|
1601
|
+
return 'static'
|
|
1597
1602
|
}
|
|
1598
1603
|
|
|
1599
1604
|
interface ParsedSegmentConfig {
|
|
1600
|
-
dynamic?: RouteSegmentConfig['dynamic']
|
|
1601
|
-
revalidate?: number | false
|
|
1602
|
-
fetchCache?: RouteSegmentConfig['fetchCache']
|
|
1603
|
-
dynamicParams?: boolean
|
|
1604
|
-
runtime?: string
|
|
1605
|
-
prefetch?: RouteSegmentConfig['prefetch']
|
|
1606
|
-
unstableInstant?: boolean
|
|
1605
|
+
dynamic?: RouteSegmentConfig['dynamic']
|
|
1606
|
+
revalidate?: number | false
|
|
1607
|
+
fetchCache?: RouteSegmentConfig['fetchCache']
|
|
1608
|
+
dynamicParams?: boolean
|
|
1609
|
+
runtime?: string
|
|
1610
|
+
prefetch?: RouteSegmentConfig['prefetch']
|
|
1611
|
+
unstableInstant?: boolean
|
|
1607
1612
|
/** Literal but invalid `export const revalidate` value (build error in Next). */
|
|
1608
|
-
invalidRevalidate?: string
|
|
1613
|
+
invalidRevalidate?: string
|
|
1609
1614
|
}
|
|
1610
1615
|
|
|
1611
1616
|
function parseSegmentConfig(source: string): ParsedSegmentConfig {
|
|
1612
|
-
const searchable = stripComments(source)
|
|
1613
|
-
const parsed: ParsedSegmentConfig = {}
|
|
1617
|
+
const searchable = stripComments(source)
|
|
1618
|
+
const parsed: ParsedSegmentConfig = {}
|
|
1614
1619
|
const dynamicMatch =
|
|
1615
1620
|
/^\s*export\s+const\s+dynamic\s*=\s*['"](auto|force-dynamic|error|force-static)['"]/m.exec(
|
|
1616
1621
|
searchable,
|
|
1617
|
-
)
|
|
1618
|
-
if (dynamicMatch) parsed.dynamic = dynamicMatch[1] as RouteSegmentConfig['dynamic']
|
|
1622
|
+
)
|
|
1623
|
+
if (dynamicMatch) parsed.dynamic = dynamicMatch[1] as RouteSegmentConfig['dynamic']
|
|
1619
1624
|
const revalidateMatch = /^\s*export\s+const\s+revalidate\s*=\s*(\d+|false)\s*;?\s*$/m.exec(
|
|
1620
1625
|
searchable,
|
|
1621
|
-
)
|
|
1626
|
+
)
|
|
1622
1627
|
if (revalidateMatch) {
|
|
1623
|
-
parsed.revalidate = revalidateMatch[1] === 'false' ? false : Number(revalidateMatch[1])
|
|
1628
|
+
parsed.revalidate = revalidateMatch[1] === 'false' ? false : Number(revalidateMatch[1])
|
|
1624
1629
|
} else {
|
|
1625
1630
|
// Invalid literal values (strings, negatives) are a build error in Next.
|
|
1626
1631
|
const invalid = /^\s*export\s+const\s+revalidate\s*=\s*(['"]([^'"]*)['"]|-\d+)/m.exec(
|
|
1627
1632
|
searchable,
|
|
1628
|
-
)
|
|
1629
|
-
if (invalid) parsed.invalidRevalidate = invalid[2] ?? invalid[1]
|
|
1633
|
+
)
|
|
1634
|
+
if (invalid) parsed.invalidRevalidate = invalid[2] ?? invalid[1]
|
|
1630
1635
|
}
|
|
1631
1636
|
const fetchCacheMatch =
|
|
1632
1637
|
/^\s*export\s+const\s+fetchCache\s*=\s*['"](auto|default-cache|only-cache|force-cache|force-no-store|default-no-store|only-no-store)['"]/m.exec(
|
|
1633
1638
|
searchable,
|
|
1634
|
-
)
|
|
1635
|
-
if (fetchCacheMatch) parsed.fetchCache = fetchCacheMatch[1] as RouteSegmentConfig['fetchCache']
|
|
1639
|
+
)
|
|
1640
|
+
if (fetchCacheMatch) parsed.fetchCache = fetchCacheMatch[1] as RouteSegmentConfig['fetchCache']
|
|
1636
1641
|
const dynamicParamsMatch = /^\s*export\s+const\s+dynamicParams\s*=\s*(true|false)/m.exec(
|
|
1637
1642
|
searchable,
|
|
1638
|
-
)
|
|
1639
|
-
if (dynamicParamsMatch) parsed.dynamicParams = dynamicParamsMatch[1] === 'true'
|
|
1640
|
-
const runtimeMatch = /^\s*export\s+const\s+runtime\s*=\s*['"](edge|nodejs)['"]/m.exec(searchable)
|
|
1641
|
-
if (runtimeMatch) parsed.runtime = runtimeMatch[1]
|
|
1643
|
+
)
|
|
1644
|
+
if (dynamicParamsMatch) parsed.dynamicParams = dynamicParamsMatch[1] === 'true'
|
|
1645
|
+
const runtimeMatch = /^\s*export\s+const\s+runtime\s*=\s*['"](edge|nodejs)['"]/m.exec(searchable)
|
|
1646
|
+
if (runtimeMatch) parsed.runtime = runtimeMatch[1]
|
|
1642
1647
|
const prefetchMatch =
|
|
1643
1648
|
/^\s*export\s+const\s+prefetch\s*=\s*['"](allow-runtime|partial|unstable_eager)['"]/m.exec(
|
|
1644
1649
|
searchable,
|
|
1645
|
-
)
|
|
1646
|
-
if (prefetchMatch) parsed.prefetch = prefetchMatch[1] as RouteSegmentConfig['prefetch']
|
|
1650
|
+
)
|
|
1651
|
+
if (prefetchMatch) parsed.prefetch = prefetchMatch[1] as RouteSegmentConfig['prefetch']
|
|
1647
1652
|
// `unstable_instant = true | false | {…samples}` — an object literal opts in
|
|
1648
1653
|
// like `true` (its samples refine the runtime-prefetch render, they don't
|
|
1649
1654
|
// gate it).
|
|
1650
1655
|
const instantMatch = /^\s*export\s+const\s+unstable_instant\s*=\s*(true|false|\{)/m.exec(
|
|
1651
1656
|
searchable,
|
|
1652
|
-
)
|
|
1653
|
-
if (instantMatch) parsed.unstableInstant = instantMatch[1] !== 'false'
|
|
1654
|
-
return parsed
|
|
1657
|
+
)
|
|
1658
|
+
if (instantMatch) parsed.unstableInstant = instantMatch[1] !== 'false'
|
|
1659
|
+
return parsed
|
|
1655
1660
|
}
|
|
1656
1661
|
|
|
1657
1662
|
/**
|
|
@@ -1670,54 +1675,54 @@ function segmentConfigFromEntries(
|
|
|
1670
1675
|
entries: { file: string; source: string }[],
|
|
1671
1676
|
parts: RouteParts,
|
|
1672
1677
|
): RouteSegmentConfig | undefined {
|
|
1673
|
-
const config: RouteSegmentConfig = {}
|
|
1674
|
-
let lowestRevalidate: number | undefined
|
|
1675
|
-
let sawRevalidateFalse = false
|
|
1676
|
-
const dynamicParamsByName = new Map<string, boolean>()
|
|
1677
|
-
const allParams = [...parts.params, ...(parts.catchAll ? [parts.catchAll] : [])]
|
|
1678
|
+
const config: RouteSegmentConfig = {}
|
|
1679
|
+
let lowestRevalidate: number | undefined
|
|
1680
|
+
let sawRevalidateFalse = false
|
|
1681
|
+
const dynamicParamsByName = new Map<string, boolean>()
|
|
1682
|
+
const allParams = [...parts.params, ...(parts.catchAll ? [parts.catchAll] : [])]
|
|
1678
1683
|
|
|
1679
1684
|
for (const entry of entries) {
|
|
1680
|
-
const parsed = parseSegmentConfig(entry.source)
|
|
1685
|
+
const parsed = parseSegmentConfig(entry.source)
|
|
1681
1686
|
if (parsed.invalidRevalidate !== undefined) {
|
|
1682
1687
|
throw new Error(
|
|
1683
1688
|
`Invalid revalidate value "${parsed.invalidRevalidate}" on "${parts.route || '/'}", must be a non-negative number or false`,
|
|
1684
|
-
)
|
|
1689
|
+
)
|
|
1685
1690
|
}
|
|
1686
1691
|
if (parsed.dynamic) {
|
|
1687
|
-
if (parsed.dynamic === 'force-dynamic') config.dynamic = 'force-dynamic'
|
|
1688
|
-
else if (!config.dynamic) config.dynamic = parsed.dynamic
|
|
1692
|
+
if (parsed.dynamic === 'force-dynamic') config.dynamic = 'force-dynamic'
|
|
1693
|
+
else if (!config.dynamic) config.dynamic = parsed.dynamic
|
|
1689
1694
|
}
|
|
1690
1695
|
if (parsed.revalidate !== undefined) {
|
|
1691
|
-
if (parsed.revalidate === false) sawRevalidateFalse = true
|
|
1696
|
+
if (parsed.revalidate === false) sawRevalidateFalse = true
|
|
1692
1697
|
else {
|
|
1693
1698
|
lowestRevalidate =
|
|
1694
1699
|
lowestRevalidate === undefined
|
|
1695
1700
|
? parsed.revalidate
|
|
1696
|
-
: Math.min(lowestRevalidate, parsed.revalidate)
|
|
1701
|
+
: Math.min(lowestRevalidate, parsed.revalidate)
|
|
1697
1702
|
}
|
|
1698
1703
|
}
|
|
1699
|
-
if (parsed.fetchCache && !config.fetchCache) config.fetchCache = parsed.fetchCache
|
|
1700
|
-
if (parsed.runtime && !config.runtime) config.runtime = parsed.runtime
|
|
1701
|
-
if (parsed.prefetch && !config.prefetch) config.prefetch = parsed.prefetch
|
|
1704
|
+
if (parsed.fetchCache && !config.fetchCache) config.fetchCache = parsed.fetchCache
|
|
1705
|
+
if (parsed.runtime && !config.runtime) config.runtime = parsed.runtime
|
|
1706
|
+
if (parsed.prefetch && !config.prefetch) config.prefetch = parsed.prefetch
|
|
1702
1707
|
// Leaf-most declaration wins (entries are page-first): a page's explicit
|
|
1703
1708
|
// `unstable_instant = false` overrides a layout's opt-in.
|
|
1704
1709
|
if (parsed.unstableInstant !== undefined && config.unstableInstant === undefined) {
|
|
1705
|
-
config.unstableInstant = parsed.unstableInstant
|
|
1710
|
+
config.unstableInstant = parsed.unstableInstant
|
|
1706
1711
|
}
|
|
1707
1712
|
if (parsed.dynamicParams !== undefined) {
|
|
1708
|
-
const param = paramOfOwnSegment(appPath, entry.file, allParams)
|
|
1713
|
+
const param = paramOfOwnSegment(appPath, entry.file, allParams)
|
|
1709
1714
|
if (param !== undefined && !dynamicParamsByName.has(param)) {
|
|
1710
|
-
dynamicParamsByName.set(param, parsed.dynamicParams)
|
|
1715
|
+
dynamicParamsByName.set(param, parsed.dynamicParams)
|
|
1711
1716
|
}
|
|
1712
1717
|
}
|
|
1713
1718
|
}
|
|
1714
1719
|
|
|
1715
|
-
if (lowestRevalidate !== undefined) config.revalidate = lowestRevalidate
|
|
1716
|
-
else if (sawRevalidateFalse) config.revalidate = false
|
|
1717
|
-
const governed = allParams.filter(param => dynamicParamsByName.get(param) === false)
|
|
1718
|
-
if (governed.length > 0) config.dynamicParamsFalse = governed
|
|
1720
|
+
if (lowestRevalidate !== undefined) config.revalidate = lowestRevalidate
|
|
1721
|
+
else if (sawRevalidateFalse) config.revalidate = false
|
|
1722
|
+
const governed = allParams.filter(param => dynamicParamsByName.get(param) === false)
|
|
1723
|
+
if (governed.length > 0) config.dynamicParamsFalse = governed
|
|
1719
1724
|
|
|
1720
|
-
return Object.keys(config).length > 0 ? config : undefined
|
|
1725
|
+
return Object.keys(config).length > 0 ? config : undefined
|
|
1721
1726
|
}
|
|
1722
1727
|
|
|
1723
1728
|
/**
|
|
@@ -1726,32 +1731,32 @@ function segmentConfigFromEntries(
|
|
|
1726
1731
|
* `locale` but not a `[slug]` below it).
|
|
1727
1732
|
*/
|
|
1728
1733
|
function paramOfOwnSegment(appPath: string, file: string, params: string[]) {
|
|
1729
|
-
const segments = toPosixPath(path.relative(appPath, path.dirname(file))).split('/')
|
|
1730
|
-
const own = segments[segments.length - 1]
|
|
1734
|
+
const segments = toPosixPath(path.relative(appPath, path.dirname(file))).split('/')
|
|
1735
|
+
const own = segments[segments.length - 1]
|
|
1731
1736
|
return params.find(
|
|
1732
1737
|
param => own === `[${param}]` || own === `[...${param}]` || own === `[[...${param}]]`,
|
|
1733
|
-
)
|
|
1738
|
+
)
|
|
1734
1739
|
}
|
|
1735
1740
|
|
|
1736
1741
|
// The parse is memoized per source, so the thousands of directive checks a
|
|
1737
1742
|
// scan makes cost one parse per file.
|
|
1738
1743
|
function hasUseClientDirective(file: string, source: string) {
|
|
1739
|
-
return scanFacts(file, source).useClient
|
|
1744
|
+
return scanFacts(file, source).useClient
|
|
1740
1745
|
}
|
|
1741
1746
|
|
|
1742
1747
|
function isClientModule(context: ScanContext, file: string) {
|
|
1743
|
-
const key = path.resolve(file)
|
|
1744
|
-
let client = context.useClient.get(key)
|
|
1748
|
+
const key = path.resolve(file)
|
|
1749
|
+
let client = context.useClient.get(key)
|
|
1745
1750
|
if (client === undefined) {
|
|
1746
|
-
client = hasUseClientDirective(file, readSource(context, file))
|
|
1747
|
-
context.useClient.set(key, client)
|
|
1751
|
+
client = hasUseClientDirective(file, readSource(context, file))
|
|
1752
|
+
context.useClient.set(key, client)
|
|
1748
1753
|
}
|
|
1749
|
-
return client
|
|
1754
|
+
return client
|
|
1750
1755
|
}
|
|
1751
1756
|
|
|
1752
1757
|
function collectClientReferences(context: ScanContext, entryFiles: string[]) {
|
|
1753
|
-
const references = new Map<string, ClientReference>()
|
|
1754
|
-
const visited = new Set<string>()
|
|
1758
|
+
const references = new Map<string, ClientReference>()
|
|
1759
|
+
const visited = new Set<string>()
|
|
1755
1760
|
|
|
1756
1761
|
for (const file of entryFiles) {
|
|
1757
1762
|
// A convention module may be a bare re-export of a client module
|
|
@@ -1761,23 +1766,23 @@ function collectClientReferences(context: ScanContext, entryFiles: string[]) {
|
|
|
1761
1766
|
// an island (the client bundle resolves the re-export chain naturally).
|
|
1762
1767
|
if (fileExists(context, file)) {
|
|
1763
1768
|
if (!isClientModule(context, file) && defaultReexportsClientModule(context, file)) {
|
|
1764
|
-
addClientReference(references, clientReference(file, 'default'))
|
|
1769
|
+
addClientReference(references, clientReference(file, 'default'))
|
|
1765
1770
|
}
|
|
1766
1771
|
}
|
|
1767
|
-
collectFromFile(context, file, references, visited)
|
|
1772
|
+
collectFromFile(context, file, references, visited)
|
|
1768
1773
|
}
|
|
1769
1774
|
|
|
1770
|
-
const collected = [...references.values()]
|
|
1775
|
+
const collected = [...references.values()]
|
|
1771
1776
|
// Webpack eagerly initializes side-effect-only client modules (a bare `import './client-only'`) ONLY once
|
|
1772
1777
|
// at least one client reference actually renders. A route whose sole client references are
|
|
1773
1778
|
// side-effect-only executes nothing on the client, so drop them - otherwise their side effects would
|
|
1774
1779
|
// wrongly run.
|
|
1775
|
-
const hasRenderedReference = collected.some(reference => !reference.sideEffect)
|
|
1780
|
+
const hasRenderedReference = collected.some(reference => !reference.sideEffect)
|
|
1776
1781
|
const filtered = hasRenderedReference
|
|
1777
1782
|
? collected
|
|
1778
|
-
: collected.filter(reference => !reference.sideEffect)
|
|
1783
|
+
: collected.filter(reference => !reference.sideEffect)
|
|
1779
1784
|
|
|
1780
|
-
return filtered.sort((a, b) => a.id.localeCompare(b.id))
|
|
1785
|
+
return filtered.sort((a, b) => a.id.localeCompare(b.id))
|
|
1781
1786
|
}
|
|
1782
1787
|
|
|
1783
1788
|
/**
|
|
@@ -1792,34 +1797,34 @@ function defaultReexportsClientModule(
|
|
|
1792
1797
|
file: string,
|
|
1793
1798
|
visited = new Set<string>(),
|
|
1794
1799
|
): boolean {
|
|
1795
|
-
const key = path.resolve(file)
|
|
1796
|
-
if (visited.has(key)) return false
|
|
1797
|
-
visited.add(key)
|
|
1798
|
-
const source = readSource(context, file)
|
|
1799
|
-
const specifiers: string[] = []
|
|
1800
|
+
const key = path.resolve(file)
|
|
1801
|
+
if (visited.has(key)) return false
|
|
1802
|
+
visited.add(key)
|
|
1803
|
+
const source = readSource(context, file)
|
|
1804
|
+
const specifiers: string[] = []
|
|
1800
1805
|
for (const edge of scanFacts(file, source).imports) {
|
|
1801
1806
|
// `export *` never carries a default, so only the named form counts.
|
|
1802
|
-
if (edge.reexport && edge.exports.includes('default')) specifiers.push(edge.specifier)
|
|
1807
|
+
if (edge.reexport && edge.exports.includes('default')) specifiers.push(edge.specifier)
|
|
1803
1808
|
}
|
|
1804
1809
|
// `import X from 'spec'; export default X` and the namespace form
|
|
1805
1810
|
// `import * as NS from 'spec'; export default NS.default` (the pages-compat
|
|
1806
1811
|
// materializer emits the latter) re-export a default just the same.
|
|
1807
|
-
const defaultExpr = /export\s+default\s+([A-Za-z_$][\w$]*)(\s*\.\s*default)?\s*;?/.exec(source)
|
|
1812
|
+
const defaultExpr = /export\s+default\s+([A-Za-z_$][\w$]*)(\s*\.\s*default)?\s*;?/.exec(source)
|
|
1808
1813
|
if (defaultExpr) {
|
|
1809
|
-
const binding = defaultExpr[1]
|
|
1810
|
-
const namespaced = Boolean(defaultExpr[2])
|
|
1814
|
+
const binding = defaultExpr[1]!
|
|
1815
|
+
const namespaced = Boolean(defaultExpr[2])
|
|
1811
1816
|
const importBinding = namespaced
|
|
1812
1817
|
? new RegExp(`import\\s*\\*\\s*as\\s+${binding}\\s+from\\s+['"]([^'"]+)['"]`).exec(source)
|
|
1813
|
-
: new RegExp(`import\\s+${binding}\\s+from\\s+['"]([^'"]+)['"]`).exec(source)
|
|
1814
|
-
if (importBinding?.[1]) specifiers.push(importBinding[1])
|
|
1818
|
+
: new RegExp(`import\\s+${binding}\\s+from\\s+['"]([^'"]+)['"]`).exec(source)
|
|
1819
|
+
if (importBinding?.[1]) specifiers.push(importBinding[1])
|
|
1815
1820
|
}
|
|
1816
1821
|
for (const specifier of specifiers) {
|
|
1817
|
-
const resolved = resolveModuleEdge(context.root, file, specifier)
|
|
1818
|
-
if (!resolved || !fileExists(context, resolved)) continue
|
|
1819
|
-
if (isClientModule(context, resolved)) return true
|
|
1820
|
-
if (defaultReexportsClientModule(context, resolved, visited)) return true
|
|
1822
|
+
const resolved = resolveModuleEdge(context.root, file, specifier)
|
|
1823
|
+
if (!resolved || !fileExists(context, resolved)) continue
|
|
1824
|
+
if (isClientModule(context, resolved)) return true
|
|
1825
|
+
if (defaultReexportsClientModule(context, resolved, visited)) return true
|
|
1821
1826
|
}
|
|
1822
|
-
return false
|
|
1827
|
+
return false
|
|
1823
1828
|
}
|
|
1824
1829
|
|
|
1825
1830
|
function collectFromFile(
|
|
@@ -1828,11 +1833,11 @@ function collectFromFile(
|
|
|
1828
1833
|
references: Map<string, ClientReference>,
|
|
1829
1834
|
visited: Set<string>,
|
|
1830
1835
|
) {
|
|
1831
|
-
if (visited.has(file) || !fileExists(context, file)) return
|
|
1832
|
-
visited.add(file)
|
|
1836
|
+
if (visited.has(file) || !fileExists(context, file)) return
|
|
1837
|
+
visited.add(file)
|
|
1833
1838
|
|
|
1834
1839
|
if (isClientModule(context, file)) {
|
|
1835
|
-
addClientReference(references, clientReference(file, 'default'))
|
|
1840
|
+
addClientReference(references, clientReference(file, 'default'))
|
|
1836
1841
|
}
|
|
1837
1842
|
|
|
1838
1843
|
for (const imported of moduleEdges(context, file)) {
|
|
@@ -1840,18 +1845,18 @@ function collectFromFile(
|
|
|
1840
1845
|
// `export * from './client'` re-exports the target's named exports (never
|
|
1841
1846
|
// its default), so each of them is a client reference of this module.
|
|
1842
1847
|
if (imported.star) {
|
|
1843
|
-
const importedSource = readSource(context, imported.file)
|
|
1848
|
+
const importedSource = readSource(context, imported.file)
|
|
1844
1849
|
for (const exportName of scanFacts(imported.file, importedSource).exportNames) {
|
|
1845
1850
|
if (exportName !== 'default') {
|
|
1846
|
-
addClientReference(references, clientReference(imported.file, exportName))
|
|
1851
|
+
addClientReference(references, clientReference(imported.file, exportName))
|
|
1847
1852
|
}
|
|
1848
1853
|
}
|
|
1849
|
-
continue
|
|
1854
|
+
continue
|
|
1850
1855
|
}
|
|
1851
1856
|
if (imported.exports.includes('*')) {
|
|
1852
1857
|
throw new Error(
|
|
1853
1858
|
`${file} namespace-imports Client Component module ${imported.file}. Import the client component by default or named export instead.`,
|
|
1854
|
-
)
|
|
1859
|
+
)
|
|
1855
1860
|
}
|
|
1856
1861
|
// A bare side-effect import binds no exports, but the module still belongs in the client bundle:
|
|
1857
1862
|
// webpack eagerly initializes every client reference module once any of them renders, so its top-level
|
|
@@ -1861,36 +1866,36 @@ function collectFromFile(
|
|
|
1861
1866
|
addClientReference(references, {
|
|
1862
1867
|
...clientReference(imported.file, '*side-effect*'),
|
|
1863
1868
|
sideEffect: true,
|
|
1864
|
-
})
|
|
1865
|
-
continue
|
|
1869
|
+
})
|
|
1870
|
+
continue
|
|
1866
1871
|
}
|
|
1867
1872
|
for (const exportName of imported.exports) {
|
|
1868
1873
|
addClientReference(references, {
|
|
1869
1874
|
...clientReference(imported.file, exportName),
|
|
1870
1875
|
dynamic: imported.dynamic,
|
|
1871
|
-
})
|
|
1876
|
+
})
|
|
1872
1877
|
}
|
|
1873
|
-
continue
|
|
1878
|
+
continue
|
|
1874
1879
|
}
|
|
1875
1880
|
|
|
1876
1881
|
if (imported.dynamic?.load === 'visible') {
|
|
1877
1882
|
console.warn(
|
|
1878
1883
|
`${file} uses dynamic({ load: 'visible' }) for ${imported.file}, but the target is a Server Component. It will render on the server instead of loading on visibility.`,
|
|
1879
|
-
)
|
|
1884
|
+
)
|
|
1880
1885
|
}
|
|
1881
1886
|
|
|
1882
|
-
collectFromFile(context, imported.file, references, visited)
|
|
1887
|
+
collectFromFile(context, imported.file, references, visited)
|
|
1883
1888
|
}
|
|
1884
1889
|
}
|
|
1885
1890
|
|
|
1886
1891
|
function addClientReference(references: Map<string, ClientReference>, reference: ClientReference) {
|
|
1887
|
-
const existing = references.get(reference.id)
|
|
1892
|
+
const existing = references.get(reference.id)
|
|
1888
1893
|
if (!existing) {
|
|
1889
|
-
references.set(reference.id, reference)
|
|
1890
|
-
return
|
|
1894
|
+
references.set(reference.id, reference)
|
|
1895
|
+
return
|
|
1891
1896
|
}
|
|
1892
1897
|
|
|
1893
|
-
if (!reference.dynamic) references.set(reference.id, reference)
|
|
1898
|
+
if (!reference.dynamic) references.set(reference.id, reference)
|
|
1894
1899
|
}
|
|
1895
1900
|
|
|
1896
1901
|
// CSS cascade order across a route's entry files: outer->inner layouts, then
|
|
@@ -1899,12 +1904,12 @@ function addClientReference(references: Map<string, ClientReference>, reference:
|
|
|
1899
1904
|
// so a layout's stylesheet is emitted before (and thus loses specificity ties
|
|
1900
1905
|
// to) the page's.
|
|
1901
1906
|
function cssEntryOrder(options: {
|
|
1902
|
-
routeFile?: string
|
|
1903
|
-
layoutFiles: string[]
|
|
1904
|
-
templateFiles: string[]
|
|
1905
|
-
specialFiles: string[]
|
|
1906
|
-
slotFiles: string[]
|
|
1907
|
-
leadFiles?: string[]
|
|
1907
|
+
routeFile?: string
|
|
1908
|
+
layoutFiles: string[]
|
|
1909
|
+
templateFiles: string[]
|
|
1910
|
+
specialFiles: string[]
|
|
1911
|
+
slotFiles: string[]
|
|
1912
|
+
leadFiles?: string[]
|
|
1908
1913
|
}) {
|
|
1909
1914
|
return [
|
|
1910
1915
|
...(options.leadFiles ?? []),
|
|
@@ -1913,7 +1918,7 @@ function cssEntryOrder(options: {
|
|
|
1913
1918
|
...(options.routeFile ? [options.routeFile] : []),
|
|
1914
1919
|
...options.specialFiles,
|
|
1915
1920
|
...options.slotFiles,
|
|
1916
|
-
]
|
|
1921
|
+
]
|
|
1917
1922
|
}
|
|
1918
1923
|
|
|
1919
1924
|
function collectCssImports(
|
|
@@ -1921,46 +1926,47 @@ function collectCssImports(
|
|
|
1921
1926
|
entryFiles: string[],
|
|
1922
1927
|
options: { route?: boolean } = {},
|
|
1923
1928
|
) {
|
|
1924
|
-
const imports = new Set<string>()
|
|
1925
|
-
const visited = new Set<string>()
|
|
1929
|
+
const imports = new Set<string>()
|
|
1930
|
+
const visited = new Set<string>()
|
|
1926
1931
|
|
|
1927
1932
|
for (const file of entryFiles) {
|
|
1928
|
-
collectCssFromFile(context, file, imports, visited)
|
|
1933
|
+
collectCssFromFile(context, file, imports, visited)
|
|
1929
1934
|
}
|
|
1930
1935
|
|
|
1931
1936
|
// Preserve source import order (a Set keeps insertion order): the emitted
|
|
1932
1937
|
// stylesheet's cascade depends on it — layouts outer->inner, then page, then
|
|
1933
1938
|
// components in import order. Sorting alphabetically would scramble the
|
|
1934
1939
|
// cascade and flip which rule wins.
|
|
1935
|
-
if (!options.route || !getCssExtensions().deferRootNotFoundCss()) return [...imports]
|
|
1940
|
+
if (!options.route || !getCssExtensions().deferRootNotFoundCss()) return [...imports]
|
|
1936
1941
|
|
|
1937
1942
|
const rootNotFound = entryFiles.find(
|
|
1938
|
-
file =>
|
|
1939
|
-
|
|
1940
|
-
|
|
1943
|
+
file =>
|
|
1944
|
+
path.dirname(file) === context.appPath && /^not-found\.[^.]+$/.test(path.basename(file)),
|
|
1945
|
+
)
|
|
1946
|
+
if (!rootNotFound) return [...imports]
|
|
1941
1947
|
|
|
1942
|
-
const rootNotFoundImports = new Set<string>()
|
|
1943
|
-
collectCssFromFile(context, rootNotFound, rootNotFoundImports, new Set())
|
|
1944
|
-
if (rootNotFoundImports.size === 0) return [...imports]
|
|
1948
|
+
const rootNotFoundImports = new Set<string>()
|
|
1949
|
+
collectCssFromFile(context, rootNotFound, rootNotFoundImports, new Set())
|
|
1950
|
+
if (rootNotFoundImports.size === 0) return [...imports]
|
|
1945
1951
|
|
|
1946
|
-
const matchedImports = new Set<string>()
|
|
1952
|
+
const matchedImports = new Set<string>()
|
|
1947
1953
|
for (const file of entryFiles) {
|
|
1948
|
-
if (file === rootNotFound) continue
|
|
1949
|
-
collectCssFromFile(context, file, matchedImports, new Set())
|
|
1954
|
+
if (file === rootNotFound) continue
|
|
1955
|
+
collectCssFromFile(context, file, matchedImports, new Set())
|
|
1950
1956
|
}
|
|
1951
1957
|
|
|
1952
|
-
return [...imports].filter(file => !rootNotFoundImports.has(file) || matchedImports.has(file))
|
|
1958
|
+
return [...imports].filter(file => !rootNotFoundImports.has(file) || matchedImports.has(file))
|
|
1953
1959
|
}
|
|
1954
1960
|
|
|
1955
1961
|
function collectSourceFiles(context: ScanContext, entryFiles: string[]) {
|
|
1956
|
-
const files = new Set<string>()
|
|
1957
|
-
const visited = new Set<string>()
|
|
1962
|
+
const files = new Set<string>()
|
|
1963
|
+
const visited = new Set<string>()
|
|
1958
1964
|
|
|
1959
1965
|
for (const file of entryFiles) {
|
|
1960
|
-
collectSourceFile(context, file, files, visited)
|
|
1966
|
+
collectSourceFile(context, file, files, visited)
|
|
1961
1967
|
}
|
|
1962
1968
|
|
|
1963
|
-
return [...files].sort()
|
|
1969
|
+
return [...files].sort()
|
|
1964
1970
|
}
|
|
1965
1971
|
|
|
1966
1972
|
function collectSourceFile(
|
|
@@ -1969,13 +1975,13 @@ function collectSourceFile(
|
|
|
1969
1975
|
files: Set<string>,
|
|
1970
1976
|
visited: Set<string>,
|
|
1971
1977
|
) {
|
|
1972
|
-
if (visited.has(file) || !fileExists(context, file)) return
|
|
1973
|
-
visited.add(file)
|
|
1974
|
-
files.add(file)
|
|
1975
|
-
if (isAssetFile(file)) return
|
|
1978
|
+
if (visited.has(file) || !fileExists(context, file)) return
|
|
1979
|
+
visited.add(file)
|
|
1980
|
+
files.add(file)
|
|
1981
|
+
if (isAssetFile(file)) return
|
|
1976
1982
|
|
|
1977
1983
|
for (const imported of moduleEdges(context, file)) {
|
|
1978
|
-
collectSourceFile(context, imported.file, files, visited)
|
|
1984
|
+
collectSourceFile(context, imported.file, files, visited)
|
|
1979
1985
|
}
|
|
1980
1986
|
}
|
|
1981
1987
|
|
|
@@ -1985,41 +1991,41 @@ function collectCssFromFile(
|
|
|
1985
1991
|
imports: Set<string>,
|
|
1986
1992
|
visited: Set<string>,
|
|
1987
1993
|
) {
|
|
1988
|
-
if (visited.has(file) || !fileExists(context, file)) return
|
|
1989
|
-
visited.add(file)
|
|
1994
|
+
if (visited.has(file) || !fileExists(context, file)) return
|
|
1995
|
+
visited.add(file)
|
|
1990
1996
|
|
|
1991
1997
|
if (isCssFile(file)) {
|
|
1992
|
-
if (!context.globalCssImports().has(path.resolve(file))) imports.add(file)
|
|
1993
|
-
return
|
|
1998
|
+
if (!context.globalCssImports().has(path.resolve(file))) imports.add(file)
|
|
1999
|
+
return
|
|
1994
2000
|
}
|
|
1995
2001
|
|
|
1996
|
-
const clientFile = isClientModule(context, file)
|
|
2002
|
+
const clientFile = isClientModule(context, file)
|
|
1997
2003
|
for (const imported of moduleEdges(context, file)) {
|
|
1998
2004
|
if (isCssFile(imported.file)) {
|
|
1999
|
-
if (!context.globalCssImports().has(path.resolve(imported.file))) imports.add(imported.file)
|
|
2000
|
-
continue
|
|
2005
|
+
if (!context.globalCssImports().has(path.resolve(imported.file))) imports.add(imported.file)
|
|
2006
|
+
continue
|
|
2001
2007
|
}
|
|
2002
2008
|
|
|
2003
2009
|
// CSS behind a non-SSR island boundary ships with the island
|
|
2004
2010
|
// (assets/<reference-id>.css) instead of the route chunk, so it loads
|
|
2005
2011
|
// when the component does. Boundaries only exist in server files:
|
|
2006
2012
|
// dynamic() inside a client subtree mounts without the island runtime.
|
|
2007
|
-
if (!clientFile &&
|
|
2013
|
+
if (!clientFile && isLazyClientEdge(context, imported)) continue
|
|
2008
2014
|
|
|
2009
|
-
collectCssFromFile(context, imported.file, imports, visited)
|
|
2015
|
+
collectCssFromFile(context, imported.file, imports, visited)
|
|
2010
2016
|
}
|
|
2011
2017
|
}
|
|
2012
2018
|
|
|
2013
2019
|
function isLazyClientEdge(context: ScanContext, edge: ModuleEdge) {
|
|
2014
|
-
if (!edge.dynamic || ssrClientReference({ dynamic: edge.dynamic })) return false
|
|
2015
|
-
return isClientModule(context, edge.file)
|
|
2020
|
+
if (!edge.dynamic || ssrClientReference({ dynamic: edge.dynamic })) return false
|
|
2021
|
+
return isClientModule(context, edge.file)
|
|
2016
2022
|
}
|
|
2017
2023
|
|
|
2018
2024
|
function attachLazyReferenceCss(context: ScanContext, references: ClientReference[]) {
|
|
2019
2025
|
for (const reference of references) {
|
|
2020
|
-
if (ssrClientReference(reference)) continue
|
|
2021
|
-
const css = collectCssImports(context, [reference.file])
|
|
2022
|
-
if (css.length > 0) reference.cssImports = css
|
|
2026
|
+
if (ssrClientReference(reference)) continue
|
|
2027
|
+
const css = collectCssImports(context, [reference.file])
|
|
2028
|
+
if (css.length > 0) reference.cssImports = css
|
|
2023
2029
|
}
|
|
2024
2030
|
}
|
|
2025
2031
|
|
|
@@ -2032,41 +2038,37 @@ function attachLazyReferenceCss(context: ScanContext, references: ClientReferenc
|
|
|
2032
2038
|
* scans would miss it and, e.g., prerender a revalidateTag route handler.
|
|
2033
2039
|
* Relative specifiers only: bare imports never carry segment config.
|
|
2034
2040
|
*/
|
|
2035
|
-
function readScanSource(
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
visited
|
|
2039
|
-
|
|
2040
|
-
const
|
|
2041
|
-
if (visited.has(key)) return '';
|
|
2042
|
-
visited.add(key);
|
|
2043
|
-
const source = readSource(context, file);
|
|
2044
|
-
const parts = [source];
|
|
2041
|
+
function readScanSource(context: ScanContext, file: string, visited = new Set<string>()): string {
|
|
2042
|
+
const key = path.resolve(file)
|
|
2043
|
+
if (visited.has(key)) return ''
|
|
2044
|
+
visited.add(key)
|
|
2045
|
+
const source = readSource(context, file)
|
|
2046
|
+
const parts = [source]
|
|
2045
2047
|
for (const edge of scanFacts(file, source).imports) {
|
|
2046
|
-
if (!edge.reexport || !edge.specifier.startsWith('.')) continue
|
|
2047
|
-
const resolved = resolveModuleEdge(context.root, file, edge.specifier)
|
|
2048
|
-
if (!resolved || !existsSync(resolved)) continue
|
|
2049
|
-
parts.push(readScanSource(context, resolved, visited))
|
|
2048
|
+
if (!edge.reexport || !edge.specifier.startsWith('.')) continue
|
|
2049
|
+
const resolved = resolveModuleEdge(context.root, file, edge.specifier)
|
|
2050
|
+
if (!resolved || !existsSync(resolved)) continue
|
|
2051
|
+
parts.push(readScanSource(context, resolved, visited))
|
|
2050
2052
|
}
|
|
2051
|
-
return parts.join('\n')
|
|
2053
|
+
return parts.join('\n')
|
|
2052
2054
|
}
|
|
2053
2055
|
|
|
2054
2056
|
function readSource(context: ScanContext, file: string) {
|
|
2055
|
-
const key = path.resolve(file)
|
|
2056
|
-
let source = context.sources.get(key)
|
|
2057
|
+
const key = path.resolve(file)
|
|
2058
|
+
let source = context.sources.get(key)
|
|
2057
2059
|
if (source === undefined) {
|
|
2058
2060
|
// Raw text comes from the build-scoped cache (a no-op outside a build), so
|
|
2059
2061
|
// the client loader and action discovery don't re-read what this walk reads.
|
|
2060
|
-
source = rewriteLiteralDynamicCalls(readSourceSync(key), file)
|
|
2061
|
-
context.sources.set(key, source)
|
|
2062
|
+
source = rewriteLiteralDynamicCalls(readSourceSync(key), file)
|
|
2063
|
+
context.sources.set(key, source)
|
|
2062
2064
|
}
|
|
2063
|
-
return source
|
|
2065
|
+
return source
|
|
2064
2066
|
}
|
|
2065
2067
|
|
|
2066
2068
|
// True when any of the route's source files imports a Link component
|
|
2067
2069
|
// (next/link in compat, or the core @wular/pnext/link). Matches the module
|
|
2068
2070
|
// specifier so it survives aliasing/renamed imports of the default export.
|
|
2069
|
-
const linkSpecifiers = new Set(['next/link', '@wular/pnext/link'])
|
|
2071
|
+
const linkSpecifiers = new Set(['next/link', '@wular/pnext/link'])
|
|
2070
2072
|
|
|
2071
2073
|
// Every module specifier through which a route's graph can reach a soft
|
|
2072
2074
|
// navigation: Link, the client router hooks, and compat's navigation surface.
|
|
@@ -2079,7 +2081,7 @@ const navigationSpecifiers = new Set([
|
|
|
2079
2081
|
'next/router',
|
|
2080
2082
|
'@wular/pnext/navigation',
|
|
2081
2083
|
'@wular/pnext/navigation/client',
|
|
2082
|
-
])
|
|
2084
|
+
])
|
|
2083
2085
|
|
|
2084
2086
|
// The root `@wular/pnext` barrel also re-exports navigation: a NAMED import of
|
|
2085
2087
|
// one of these counts, anything else (types, cache, config helpers) does not.
|
|
@@ -2097,72 +2099,72 @@ const rootNavigationExports = new Set([
|
|
|
2097
2099
|
'useRoute',
|
|
2098
2100
|
'useRouter',
|
|
2099
2101
|
'useSearchParams',
|
|
2100
|
-
])
|
|
2102
|
+
])
|
|
2101
2103
|
|
|
2102
2104
|
function importReachesNavigation(edge: { specifier: string; exports: string[] }) {
|
|
2103
|
-
if (navigationSpecifiers.has(edge.specifier)) return true
|
|
2104
|
-
if (edge.specifier !== '@wular/pnext') return false
|
|
2105
|
-
return edge.exports.length === 0 || edge.exports.some(name => rootNavigationExports.has(name))
|
|
2105
|
+
if (navigationSpecifiers.has(edge.specifier)) return true
|
|
2106
|
+
if (edge.specifier !== '@wular/pnext') return false
|
|
2107
|
+
return edge.exports.length === 0 || edge.exports.some(name => rootNavigationExports.has(name))
|
|
2106
2108
|
}
|
|
2107
2109
|
|
|
2108
2110
|
function routeUsesLinkComponent(context: ScanContext, files: string[]) {
|
|
2109
2111
|
for (const file of files) {
|
|
2110
|
-
if (!fileExists(context, file) || isAssetFile(file)) continue
|
|
2111
|
-
const key = path.resolve(file)
|
|
2112
|
-
let uses = context.usesLink.get(key)
|
|
2112
|
+
if (!fileExists(context, file) || isAssetFile(file)) continue
|
|
2113
|
+
const key = path.resolve(file)
|
|
2114
|
+
let uses = context.usesLink.get(key)
|
|
2113
2115
|
if (uses === undefined) {
|
|
2114
|
-
const facts = scanFacts(file, readSource(context, file))
|
|
2115
|
-
uses = facts.imports.some(edge => linkSpecifiers.has(edge.specifier))
|
|
2116
|
-
context.usesLink.set(key, uses)
|
|
2116
|
+
const facts = scanFacts(file, readSource(context, file))
|
|
2117
|
+
uses = facts.imports.some(edge => linkSpecifiers.has(edge.specifier))
|
|
2118
|
+
context.usesLink.set(key, uses)
|
|
2117
2119
|
}
|
|
2118
|
-
if (uses) return true
|
|
2120
|
+
if (uses) return true
|
|
2119
2121
|
}
|
|
2120
|
-
return false
|
|
2122
|
+
return false
|
|
2121
2123
|
}
|
|
2122
2124
|
|
|
2123
2125
|
/** True when any file in the route's closure can reach a soft navigation. */
|
|
2124
2126
|
function routeUsesNavigation(context: ScanContext, files: string[]) {
|
|
2125
2127
|
for (const file of files) {
|
|
2126
|
-
if (!fileExists(context, file) || isAssetFile(file)) continue
|
|
2127
|
-
const key = path.resolve(file)
|
|
2128
|
-
let uses = context.usesNavigation.get(key)
|
|
2128
|
+
if (!fileExists(context, file) || isAssetFile(file)) continue
|
|
2129
|
+
const key = path.resolve(file)
|
|
2130
|
+
let uses = context.usesNavigation.get(key)
|
|
2129
2131
|
if (uses === undefined) {
|
|
2130
|
-
const facts = scanFacts(file, readSource(context, file))
|
|
2131
|
-
uses = facts.imports.some(importReachesNavigation)
|
|
2132
|
-
context.usesNavigation.set(key, uses)
|
|
2132
|
+
const facts = scanFacts(file, readSource(context, file))
|
|
2133
|
+
uses = facts.imports.some(importReachesNavigation)
|
|
2134
|
+
context.usesNavigation.set(key, uses)
|
|
2133
2135
|
}
|
|
2134
|
-
if (uses) return true
|
|
2136
|
+
if (uses) return true
|
|
2135
2137
|
}
|
|
2136
|
-
return false
|
|
2138
|
+
return false
|
|
2137
2139
|
}
|
|
2138
2140
|
|
|
2139
2141
|
function routeClientEntryReasons(context: ScanContext, files: string[]): ClientEntryReason[] {
|
|
2140
|
-
const reasons: ClientEntryReason[] = []
|
|
2142
|
+
const reasons: ClientEntryReason[] = []
|
|
2141
2143
|
for (const file of files) {
|
|
2142
|
-
if (!fileExists(context, file) || isAssetFile(file)) continue
|
|
2143
|
-
const key = path.resolve(file)
|
|
2144
|
-
let fileReasons = context.entryReasons.get(key)
|
|
2144
|
+
if (!fileExists(context, file) || isAssetFile(file)) continue
|
|
2145
|
+
const key = path.resolve(file)
|
|
2146
|
+
let fileReasons = context.entryReasons.get(key)
|
|
2145
2147
|
if (fileReasons === undefined) {
|
|
2146
|
-
fileReasons = sourceClientEntryReasons(readSource(context, file))
|
|
2147
|
-
context.entryReasons.set(key, fileReasons)
|
|
2148
|
+
fileReasons = sourceClientEntryReasons(readSource(context, file))
|
|
2149
|
+
context.entryReasons.set(key, fileReasons)
|
|
2148
2150
|
}
|
|
2149
|
-
reasons.push(...fileReasons)
|
|
2151
|
+
reasons.push(...fileReasons)
|
|
2150
2152
|
}
|
|
2151
|
-
return reasons
|
|
2153
|
+
return reasons
|
|
2152
2154
|
}
|
|
2153
2155
|
|
|
2154
2156
|
function usesRequestImportDataForFiles(context: ScanContext, files: string[]) {
|
|
2155
2157
|
for (const file of files) {
|
|
2156
|
-
const key = path.resolve(file)
|
|
2157
|
-
let usesRequestImport = context.requestImportUsage.get(key)
|
|
2158
|
+
const key = path.resolve(file)
|
|
2159
|
+
let usesRequestImport = context.requestImportUsage.get(key)
|
|
2158
2160
|
if (usesRequestImport === undefined) {
|
|
2159
|
-
usesRequestImport = sourceUsesRegisteredRequestApi(readSource(context, file))
|
|
2160
|
-
context.requestImportUsage.set(key, usesRequestImport)
|
|
2161
|
+
usesRequestImport = sourceUsesRegisteredRequestApi(readSource(context, file))
|
|
2162
|
+
context.requestImportUsage.set(key, usesRequestImport)
|
|
2161
2163
|
}
|
|
2162
|
-
if (usesRequestImport) return true
|
|
2164
|
+
if (usesRequestImport) return true
|
|
2163
2165
|
}
|
|
2164
2166
|
|
|
2165
|
-
return false
|
|
2167
|
+
return false
|
|
2166
2168
|
}
|
|
2167
2169
|
|
|
2168
2170
|
function routeDependencyClassification(
|
|
@@ -2172,18 +2174,18 @@ function routeDependencyClassification(
|
|
|
2172
2174
|
) {
|
|
2173
2175
|
const sources = files
|
|
2174
2176
|
.filter(file => !isAssetFile(file))
|
|
2175
|
-
.map(file => ({ file, source: readSource(context, file) }))
|
|
2176
|
-
return classifyRouteDependencies({ kind, files: sources })
|
|
2177
|
+
.map(file => ({ file, source: readSource(context, file) }))
|
|
2178
|
+
return classifyRouteDependencies({ kind, files: sources })
|
|
2177
2179
|
}
|
|
2178
2180
|
|
|
2179
2181
|
function moduleEdges(context: ScanContext, file: string) {
|
|
2180
|
-
const key = path.resolve(file)
|
|
2181
|
-
let edges = context.edges.get(key)
|
|
2182
|
+
const key = path.resolve(file)
|
|
2183
|
+
let edges = context.edges.get(key)
|
|
2182
2184
|
if (!edges) {
|
|
2183
|
-
edges = moduleEdgesFromSource(context.root, file, readSource(context, file))
|
|
2184
|
-
context.edges.set(key, edges)
|
|
2185
|
+
edges = moduleEdgesFromSource(context.root, file, readSource(context, file))
|
|
2186
|
+
context.edges.set(key, edges)
|
|
2185
2187
|
}
|
|
2186
|
-
return edges
|
|
2188
|
+
return edges
|
|
2187
2189
|
}
|
|
2188
2190
|
|
|
2189
2191
|
// Compat pins framework specifiers (`next/form`, ...) to its own absolute files, and the bundler's
|
|
@@ -2199,55 +2201,55 @@ function resolveModuleEdge(root: string, file: string, specifier: string) {
|
|
|
2199
2201
|
getBundlerExtensions().resolveRouteDependency(root, file, specifier) ??
|
|
2200
2202
|
resolveScopedClientBoundaryPackage(root, file, specifier) ??
|
|
2201
2203
|
getCssExtensions().resolveCssDependency(root, file, specifier)
|
|
2202
|
-
)
|
|
2204
|
+
)
|
|
2203
2205
|
}
|
|
2204
2206
|
|
|
2205
|
-
const scopedClientBoundaryPackages = new Set(['@next/third-parties'])
|
|
2207
|
+
const scopedClientBoundaryPackages = new Set(['@next/third-parties'])
|
|
2206
2208
|
|
|
2207
2209
|
function resolveScopedClientBoundaryPackage(root: string, file: string, specifier: string) {
|
|
2208
|
-
if (!scopedClientBoundaryPackages.has(packageNameOfSpecifier(specifier) ?? '')) return undefined
|
|
2209
|
-
return resolvePackageSpecifier(root, file, specifier, ['import', 'default'])
|
|
2210
|
+
if (!scopedClientBoundaryPackages.has(packageNameOfSpecifier(specifier) ?? '')) return undefined
|
|
2211
|
+
return resolvePackageSpecifier(root, file, specifier, ['import', 'default'])
|
|
2210
2212
|
}
|
|
2211
2213
|
|
|
2212
2214
|
function moduleEdgesFromSource(root: string, file: string, source: string) {
|
|
2213
|
-
const dynamicNames = pnextDynamicImportNames(source, file)
|
|
2214
|
-
assertSupportedDynamicCalls(file, source, dynamicNames)
|
|
2215
|
+
const dynamicNames = pnextDynamicImportNames(source, file)
|
|
2216
|
+
assertSupportedDynamicCalls(file, source, dynamicNames)
|
|
2215
2217
|
// Edges carry their byte offset so the final list follows true source order:
|
|
2216
2218
|
// CSS injection order is a source-order property — a side-effect
|
|
2217
2219
|
// `import './a.css'` written before a component import must be emitted
|
|
2218
2220
|
// before that component's CSS.
|
|
2219
|
-
const ordered: (ModuleEdge & { index: number })[] = []
|
|
2220
|
-
const facts = scanFacts(file, source)
|
|
2221
|
+
const ordered: (ModuleEdge & { index: number })[] = []
|
|
2222
|
+
const facts = scanFacts(file, source)
|
|
2221
2223
|
|
|
2222
2224
|
for (const edge of facts.imports) {
|
|
2223
|
-
const resolved = resolveModuleEdge(root, file, edge.specifier)
|
|
2224
|
-
if (!resolved) continue
|
|
2225
|
+
const resolved = resolveModuleEdge(root, file, edge.specifier)
|
|
2226
|
+
if (!resolved) continue
|
|
2225
2227
|
ordered.push({
|
|
2226
2228
|
file: resolved,
|
|
2227
2229
|
exports: [...edge.exports],
|
|
2228
2230
|
index: edge.index,
|
|
2229
2231
|
...(edge.star ? { star: true } : {}),
|
|
2230
|
-
})
|
|
2232
|
+
})
|
|
2231
2233
|
}
|
|
2232
2234
|
|
|
2233
2235
|
if (scopedClientBoundaryPackages.has(packageNameForFile(file) ?? '')) {
|
|
2234
2236
|
for (const call of facts.requires) {
|
|
2235
|
-
const resolved = resolveModuleEdge(root, file, call.specifier)
|
|
2236
|
-
if (!resolved) continue
|
|
2237
|
+
const resolved = resolveModuleEdge(root, file, call.specifier)
|
|
2238
|
+
if (!resolved) continue
|
|
2237
2239
|
const exports = commonJsModuleHasDefaultExport(readFileSync(resolved, 'utf8'), resolved)
|
|
2238
2240
|
? ['default']
|
|
2239
|
-
: []
|
|
2240
|
-
ordered.push({ file: resolved, exports, index: call.index })
|
|
2241
|
+
: []
|
|
2242
|
+
ordered.push({ file: resolved, exports, index: call.index })
|
|
2241
2243
|
}
|
|
2242
2244
|
}
|
|
2243
2245
|
|
|
2244
|
-
ordered.sort((a, b) => a.index - b.index)
|
|
2245
|
-
const imports: ModuleEdge[] = ordered.map(({ index: _index, ...edge }) => edge)
|
|
2246
|
+
ordered.sort((a, b) => a.index - b.index)
|
|
2247
|
+
const imports: ModuleEdge[] = ordered.map(({ index: _index, ...edge }) => edge)
|
|
2246
2248
|
|
|
2247
2249
|
for (const imported of dynamicImportEdgesFromSource(root, file, source, dynamicNames))
|
|
2248
|
-
imports.push(imported)
|
|
2250
|
+
imports.push(imported)
|
|
2249
2251
|
|
|
2250
|
-
return imports
|
|
2252
|
+
return imports
|
|
2251
2253
|
}
|
|
2252
2254
|
|
|
2253
2255
|
function assertSupportedDynamicCalls(file: string, source: string, dynamicNames: Set<string>) {
|
|
@@ -2255,13 +2257,13 @@ function assertSupportedDynamicCalls(file: string, source: string, dynamicNames:
|
|
|
2255
2257
|
if (!/\bimport\s*\(/.test(call.source)) {
|
|
2256
2258
|
throw new Error(
|
|
2257
2259
|
`${file} uses dynamic() without a literal import. PNext supports dynamic('./module') or dynamic(() => import('./module')).`,
|
|
2258
|
-
)
|
|
2260
|
+
)
|
|
2259
2261
|
}
|
|
2260
2262
|
|
|
2261
2263
|
if (!/\bimport\s*\(\s*['"][^'"]+['"]\s*\)/.test(call.source)) {
|
|
2262
2264
|
throw new Error(
|
|
2263
2265
|
`${file} uses a non-literal dynamic import. PNext supports dynamic('./module') or dynamic(() => import('./module')).`,
|
|
2264
|
-
)
|
|
2266
|
+
)
|
|
2265
2267
|
}
|
|
2266
2268
|
}
|
|
2267
2269
|
}
|
|
@@ -2272,35 +2274,35 @@ function dynamicImportEdgesFromSource(
|
|
|
2272
2274
|
source: string,
|
|
2273
2275
|
dynamicNames: Set<string>,
|
|
2274
2276
|
) {
|
|
2275
|
-
const imports: { file: string; exports: string[]; dynamic: ClientDynamicReference }[] = []
|
|
2276
|
-
if (dynamicNames.size === 0) return imports
|
|
2277
|
+
const imports: { file: string; exports: string[]; dynamic: ClientDynamicReference }[] = []
|
|
2278
|
+
if (dynamicNames.size === 0) return imports
|
|
2277
2279
|
|
|
2278
|
-
const optionObjects = dynamicOptionObjects(source)
|
|
2280
|
+
const optionObjects = dynamicOptionObjects(source)
|
|
2279
2281
|
for (const fact of dynamicCallFacts(source, file)) {
|
|
2280
|
-
if (!dynamicNames.has(fact.name)) continue
|
|
2281
|
-
const resolved = resolveModuleEdge(root, file, fact.specifier)
|
|
2282
|
-
if (!resolved) continue
|
|
2282
|
+
if (!dynamicNames.has(fact.name)) continue
|
|
2283
|
+
const resolved = resolveModuleEdge(root, file, fact.specifier)
|
|
2284
|
+
if (!resolved) continue
|
|
2283
2285
|
imports.push({
|
|
2284
2286
|
file: resolved,
|
|
2285
2287
|
exports: [fact.exportName],
|
|
2286
2288
|
dynamic: dynamicOptionsForFact(source, fact, optionObjects),
|
|
2287
|
-
})
|
|
2289
|
+
})
|
|
2288
2290
|
}
|
|
2289
2291
|
|
|
2290
|
-
return imports
|
|
2292
|
+
return imports
|
|
2291
2293
|
}
|
|
2292
2294
|
|
|
2293
2295
|
function dynamicOptionObjects(source: string) {
|
|
2294
|
-
const options = new Map<string, ClientDynamicReference>()
|
|
2295
|
-
const pattern = /const\s+([A-Za-z_$][\w$]*)\s*=\s*({[\s\S]*?})\s*(?:as\s+const\s*)?;/g
|
|
2296
|
-
let match: RegExpExecArray | null
|
|
2296
|
+
const options = new Map<string, ClientDynamicReference>()
|
|
2297
|
+
const pattern = /const\s+([A-Za-z_$][\w$]*)\s*=\s*({[\s\S]*?})\s*(?:as\s+const\s*)?;/g
|
|
2298
|
+
let match: RegExpExecArray | null
|
|
2297
2299
|
|
|
2298
2300
|
while ((match = pattern.exec(source))) {
|
|
2299
|
-
const [, name, objectSource] = match
|
|
2300
|
-
if (name && objectSource) options.set(name, dynamicOptionsFromSource(objectSource))
|
|
2301
|
+
const [, name, objectSource] = match
|
|
2302
|
+
if (name && objectSource) options.set(name, dynamicOptionsFromSource(objectSource))
|
|
2301
2303
|
}
|
|
2302
2304
|
|
|
2303
|
-
return options
|
|
2305
|
+
return options
|
|
2304
2306
|
}
|
|
2305
2307
|
|
|
2306
2308
|
function dynamicOptionsForFact(
|
|
@@ -2308,73 +2310,80 @@ function dynamicOptionsForFact(
|
|
|
2308
2310
|
fact: DynamicCallFact,
|
|
2309
2311
|
optionObjects: Map<string, ClientDynamicReference>,
|
|
2310
2312
|
) {
|
|
2311
|
-
if (fact.optionsStart === undefined || fact.optionsEnd === undefined) return {}
|
|
2312
|
-
const options = source.slice(fact.optionsStart, fact.optionsEnd)
|
|
2313
|
-
if (/^[A-Za-z_$][\w$]*$/.test(options)) return optionObjects.get(options) ?? {}
|
|
2314
|
-
return dynamicOptionsFromSource(options)
|
|
2313
|
+
if (fact.optionsStart === undefined || fact.optionsEnd === undefined) return {}
|
|
2314
|
+
const options = source.slice(fact.optionsStart, fact.optionsEnd)
|
|
2315
|
+
if (/^[A-Za-z_$][\w$]*$/.test(options)) return optionObjects.get(options) ?? {}
|
|
2316
|
+
return dynamicOptionsFromSource(options)
|
|
2315
2317
|
}
|
|
2316
2318
|
|
|
2317
2319
|
function dynamicOptionsFromSource(source?: string): ClientDynamicReference {
|
|
2318
|
-
if (!source) return {}
|
|
2320
|
+
if (!source) return {}
|
|
2319
2321
|
return {
|
|
2320
2322
|
...dynamicLoadFromSource(source),
|
|
2321
2323
|
...dynamicRootMarginFromSource(source),
|
|
2322
2324
|
...dynamicSsrFromSource(source),
|
|
2323
2325
|
...dynamicThresholdFromSource(source),
|
|
2324
|
-
}
|
|
2326
|
+
}
|
|
2325
2327
|
}
|
|
2326
2328
|
|
|
2327
2329
|
function dynamicLoadFromSource(source: string): ClientDynamicReference {
|
|
2328
|
-
const match = /\bload\s*:\s*['"](render|visible)['"]/.exec(source)
|
|
2329
|
-
return match?.[1] === 'render' || match?.[1] === 'visible' ? { load: match[1] } : {}
|
|
2330
|
+
const match = /\bload\s*:\s*['"](render|visible)['"]/.exec(source)
|
|
2331
|
+
return match?.[1] === 'render' || match?.[1] === 'visible' ? { load: match[1] } : {}
|
|
2330
2332
|
}
|
|
2331
2333
|
|
|
2332
2334
|
function dynamicRootMarginFromSource(source: string): ClientDynamicReference {
|
|
2333
|
-
const match = /\brootMargin\s*:\s*['"]([^'"]+)['"]/.exec(source)
|
|
2334
|
-
return match?.[1] ? { rootMargin: match[1] } : {}
|
|
2335
|
+
const match = /\brootMargin\s*:\s*['"]([^'"]+)['"]/.exec(source)
|
|
2336
|
+
return match?.[1] ? { rootMargin: match[1] } : {}
|
|
2335
2337
|
}
|
|
2336
2338
|
|
|
2337
2339
|
function dynamicSsrFromSource(source: string): ClientDynamicReference {
|
|
2338
|
-
const match = /\bssr\s*:\s*(true|false)\b/.exec(source)
|
|
2339
|
-
return match?.[1] ? { ssr: match[1] === 'true' } : {}
|
|
2340
|
+
const match = /\bssr\s*:\s*(true|false)\b/.exec(source)
|
|
2341
|
+
return match?.[1] ? { ssr: match[1] === 'true' } : {}
|
|
2340
2342
|
}
|
|
2341
2343
|
|
|
2342
2344
|
function dynamicThresholdFromSource(source: string): ClientDynamicReference {
|
|
2343
|
-
const numberMatch = /\bthreshold\s*:\s*(-?\d+(?:\.\d+)?)/.exec(source)
|
|
2344
|
-
if (numberMatch?.[1]) return { threshold: Number(numberMatch[1]) }
|
|
2345
|
+
const numberMatch = /\bthreshold\s*:\s*(-?\d+(?:\.\d+)?)/.exec(source)
|
|
2346
|
+
if (numberMatch?.[1]) return { threshold: Number(numberMatch[1]) }
|
|
2345
2347
|
|
|
2346
|
-
const arrayMatch = /\bthreshold\s*:\s*\[([^\]]*)\]/.exec(source)
|
|
2347
|
-
if (!arrayMatch?.[1]) return {}
|
|
2348
|
+
const arrayMatch = /\bthreshold\s*:\s*\[([^\]]*)\]/.exec(source)
|
|
2349
|
+
if (!arrayMatch?.[1]) return {}
|
|
2348
2350
|
const threshold = arrayMatch[1]
|
|
2349
2351
|
.split(',')
|
|
2350
2352
|
.map(value => Number(value.trim()))
|
|
2351
|
-
.filter(Number.isFinite)
|
|
2352
|
-
return threshold.length > 0 ? { threshold } : {}
|
|
2353
|
+
.filter(Number.isFinite)
|
|
2354
|
+
return threshold.length > 0 ? { threshold } : {}
|
|
2353
2355
|
}
|
|
2354
2356
|
|
|
2355
2357
|
function isCssFile(file: string) {
|
|
2356
|
-
return
|
|
2358
|
+
return (
|
|
2359
|
+
file.endsWith('.css') ||
|
|
2360
|
+
getCssExtensions()
|
|
2361
|
+
.extraCssExtensions()
|
|
2362
|
+
.some(ext => file.endsWith(ext))
|
|
2363
|
+
)
|
|
2357
2364
|
}
|
|
2358
2365
|
|
|
2359
2366
|
function isAssetFile(file: string) {
|
|
2360
2367
|
return (
|
|
2361
2368
|
/\.(css|svg|png|jpe?g|gif|webp|ico|woff2?)$/.test(file) ||
|
|
2362
|
-
getCssExtensions()
|
|
2363
|
-
|
|
2369
|
+
getCssExtensions()
|
|
2370
|
+
.extraCssExtensions()
|
|
2371
|
+
.some(ext => file.endsWith(ext))
|
|
2372
|
+
)
|
|
2364
2373
|
}
|
|
2365
2374
|
|
|
2366
2375
|
function exportsExperimentalPpr(sources: string[]) {
|
|
2367
2376
|
return sources.some(source =>
|
|
2368
2377
|
/\bexport\s+const\s+experimental_ppr\s*=\s*true\b/.test(stripComments(source)),
|
|
2369
|
-
)
|
|
2378
|
+
)
|
|
2370
2379
|
}
|
|
2371
2380
|
|
|
2372
2381
|
function usesRequestData(source: string) {
|
|
2373
|
-
const searchable = stripJsxText(stripCommentsAndStrings(source))
|
|
2382
|
+
const searchable = stripJsxText(stripCommentsAndStrings(source))
|
|
2374
2383
|
// Core knows only its own generic request-data markers; the next/* import
|
|
2375
2384
|
// detectors (next/headers, next/navigation hooks, next/server connection())
|
|
2376
2385
|
// are registered by compat via the usageDetection seam.
|
|
2377
|
-
return /\b(?:request|searchParams)\b/.test(searchable) || sourceUsesRegisteredRequestApi(source)
|
|
2386
|
+
return /\b(?:request|searchParams)\b/.test(searchable) || sourceUsesRegisteredRequestApi(source)
|
|
2378
2387
|
}
|
|
2379
2388
|
|
|
2380
2389
|
/**
|
|
@@ -2384,7 +2393,10 @@ function usesRequestData(source: string) {
|
|
|
2384
2393
|
* left untouched so a real usage between JSX tags is still seen.
|
|
2385
2394
|
*/
|
|
2386
2395
|
function stripJsxText(source: string) {
|
|
2387
|
-
return source.replace(
|
|
2396
|
+
return source.replace(
|
|
2397
|
+
/>([^<>(){}=;\n]*)</g,
|
|
2398
|
+
(_match, text: string) => `>${' '.repeat(text.length)}<`,
|
|
2399
|
+
)
|
|
2388
2400
|
}
|
|
2389
2401
|
|
|
2390
2402
|
/**
|
|
@@ -2399,27 +2411,27 @@ function stripJsxText(source: string) {
|
|
|
2399
2411
|
* referenced (the route can prerender).
|
|
2400
2412
|
*/
|
|
2401
2413
|
function dynamicApiLabel(sources: string[], kind: 'page' | 'handler'): string | undefined {
|
|
2402
|
-
const joined = sources.map(stripCommentsAndStrings).join('\n')
|
|
2403
|
-
if (/\bconnection\s*\(/.test(joined)) return '`connection()`'
|
|
2404
|
-
if (/\bcookies\s*\(/.test(joined)) return '`cookies()`'
|
|
2405
|
-
if (/\bheaders\s*\(/.test(joined)) return '`headers()`'
|
|
2406
|
-
if (/\bdraftMode\s*\(/.test(joined)) return '`draftMode()`'
|
|
2414
|
+
const joined = sources.map(stripCommentsAndStrings).join('\n')
|
|
2415
|
+
if (/\bconnection\s*\(/.test(joined)) return '`connection()`'
|
|
2416
|
+
if (/\bcookies\s*\(/.test(joined)) return '`cookies()`'
|
|
2417
|
+
if (/\bheaders\s*\(/.test(joined)) return '`headers()`'
|
|
2418
|
+
if (/\bdraftMode\s*\(/.test(joined)) return '`draftMode()`'
|
|
2407
2419
|
if (kind === 'handler') {
|
|
2408
|
-
if (/\.formData\b/.test(joined)) return '`request.formData`'
|
|
2409
|
-
if (/\bnextUrl\b/.test(joined)) return '`nextUrl.toString`'
|
|
2410
|
-
if (/\brequest\.url\b|\breq\.url\b/.test(joined)) return '`request.url`'
|
|
2420
|
+
if (/\.formData\b/.test(joined)) return '`request.formData`'
|
|
2421
|
+
if (/\bnextUrl\b/.test(joined)) return '`nextUrl.toString`'
|
|
2422
|
+
if (/\brequest\.url\b|\breq\.url\b/.test(joined)) return '`request.url`'
|
|
2411
2423
|
} else if (/\bsearchParams\b/.test(joined)) {
|
|
2412
|
-
return '`await searchParams`, `searchParams.then`, or similar'
|
|
2424
|
+
return '`await searchParams`, `searchParams.then`, or similar'
|
|
2413
2425
|
}
|
|
2414
|
-
return undefined
|
|
2426
|
+
return undefined
|
|
2415
2427
|
}
|
|
2416
2428
|
|
|
2417
2429
|
function maxDurationFromSources(sources: string[]) {
|
|
2418
2430
|
const durations = sources
|
|
2419
2431
|
.map(source => /\bexport\s+const\s+maxDuration\s*=\s*(\d+)\b/.exec(source)?.[1])
|
|
2420
2432
|
.filter((value): value is string => Boolean(value))
|
|
2421
|
-
.map(value => Number(value))
|
|
2422
|
-
return durations.length > 0 ? Math.max(...durations) : undefined
|
|
2433
|
+
.map(value => Number(value))
|
|
2434
|
+
return durations.length > 0 ? Math.max(...durations) : undefined
|
|
2423
2435
|
}
|
|
2424
2436
|
|
|
2425
2437
|
// On-demand revalidation calls make a handler dynamic in Next: caching the
|
|
@@ -2427,78 +2439,78 @@ function maxDurationFromSources(sources: string[]) {
|
|
|
2427
2439
|
function usesRevalidationApis(source: string) {
|
|
2428
2440
|
return /\b(?:revalidatePath|revalidateTag|updateTag|expirePath|expireTag|unstable_expirePath|unstable_expireTag)\s*\(/.test(
|
|
2429
2441
|
stripCommentsAndStrings(source),
|
|
2430
|
-
)
|
|
2442
|
+
)
|
|
2431
2443
|
}
|
|
2432
2444
|
|
|
2433
2445
|
/** Sentinel for a destructured first parameter (no identifier to track). */
|
|
2434
|
-
const DESTRUCTURED_PARAM = '<destructured>'
|
|
2446
|
+
const DESTRUCTURED_PARAM = '<destructured>'
|
|
2435
2447
|
|
|
2436
2448
|
function routeHandlerUsesRequest(source: string) {
|
|
2437
|
-
const withoutComments = stripComments(source)
|
|
2438
|
-
const searchable = stripCommentsAndStrings(source)
|
|
2449
|
+
const withoutComments = stripComments(source)
|
|
2450
|
+
const searchable = stripCommentsAndStrings(source)
|
|
2439
2451
|
// Compat's usageDetection (next/headers import, etc.) marks a handler dynamic.
|
|
2440
|
-
if (sourceUsesRegisteredRequestApi(source)) return true
|
|
2452
|
+
if (sourceUsesRegisteredRequestApi(source)) return true
|
|
2441
2453
|
for (const method of routeHandlerMethods) {
|
|
2442
2454
|
for (const params of exportedFunctionParams(withoutComments, method)) {
|
|
2443
|
-
const name = firstParameterName(params)
|
|
2455
|
+
const name = firstParameterName(params)
|
|
2444
2456
|
// A destructured request parameter (`GET({ nextUrl })`) reads the request
|
|
2445
2457
|
// in the signature itself — there is no binding to count references for.
|
|
2446
|
-
if (name === DESTRUCTURED_PARAM) return true
|
|
2447
|
-
if (name && referencesName(searchable, name) > 1) return true
|
|
2458
|
+
if (name === DESTRUCTURED_PARAM) return true
|
|
2459
|
+
if (name && referencesName(searchable, name) > 1) return true
|
|
2448
2460
|
}
|
|
2449
2461
|
}
|
|
2450
|
-
return false
|
|
2462
|
+
return false
|
|
2451
2463
|
}
|
|
2452
2464
|
|
|
2453
2465
|
function exportedFunctionParams(source: string, method: string) {
|
|
2454
|
-
const params: string[] = []
|
|
2466
|
+
const params: string[] = []
|
|
2455
2467
|
const functionPattern = new RegExp(
|
|
2456
2468
|
`\\bexport\\s+(?:async\\s+)?function\\s+${method}\\s*\\(([^)]*)\\)`,
|
|
2457
2469
|
'g',
|
|
2458
|
-
)
|
|
2459
|
-
let match: RegExpExecArray | null
|
|
2470
|
+
)
|
|
2471
|
+
let match: RegExpExecArray | null
|
|
2460
2472
|
while ((match = functionPattern.exec(source))) {
|
|
2461
|
-
params.push(match[1] ?? '')
|
|
2473
|
+
params.push(match[1] ?? '')
|
|
2462
2474
|
}
|
|
2463
2475
|
|
|
2464
2476
|
const arrowPattern = new RegExp(
|
|
2465
2477
|
`\\bexport\\s+const\\s+${method}\\s*(?::[^=]+)?=\\s*(?:async\\s*)?(?:\\(([^)]*)\\)|([A-Za-z_$][\\w$]*))\\s*=>`,
|
|
2466
2478
|
'g',
|
|
2467
|
-
)
|
|
2479
|
+
)
|
|
2468
2480
|
while ((match = arrowPattern.exec(source))) {
|
|
2469
|
-
params.push(match[1] ?? match[2] ?? '')
|
|
2481
|
+
params.push(match[1] ?? match[2] ?? '')
|
|
2470
2482
|
}
|
|
2471
2483
|
|
|
2472
|
-
return params
|
|
2484
|
+
return params
|
|
2473
2485
|
}
|
|
2474
2486
|
|
|
2475
2487
|
function firstParameterName(params: string) {
|
|
2476
|
-
const first = params.split(',')[0]?.trim()
|
|
2477
|
-
if (!first) return undefined
|
|
2478
|
-
if (first.startsWith('{') || first.startsWith('[')) return DESTRUCTURED_PARAM
|
|
2488
|
+
const first = params.split(',')[0]?.trim()
|
|
2489
|
+
if (!first) return undefined
|
|
2490
|
+
if (first.startsWith('{') || first.startsWith('[')) return DESTRUCTURED_PARAM
|
|
2479
2491
|
const name =
|
|
2480
|
-
/^\.{3}\s*([A-Za-z_$][\w$]*)/.exec(first)?.[1] ?? /^([A-Za-z_$][\w$]*)/.exec(first)?.[1]
|
|
2481
|
-
return name
|
|
2492
|
+
/^\.{3}\s*([A-Za-z_$][\w$]*)/.exec(first)?.[1] ?? /^([A-Za-z_$][\w$]*)/.exec(first)?.[1]
|
|
2493
|
+
return name
|
|
2482
2494
|
}
|
|
2483
2495
|
|
|
2484
2496
|
function referencesName(source: string, name: string) {
|
|
2485
|
-
const pattern = new RegExp(`\\b${escapeRegex(name)}\\b`, 'g')
|
|
2486
|
-
return [...source.matchAll(pattern)].length
|
|
2497
|
+
const pattern = new RegExp(`\\b${escapeRegex(name)}\\b`, 'g')
|
|
2498
|
+
return [...source.matchAll(pattern)].length
|
|
2487
2499
|
}
|
|
2488
2500
|
|
|
2489
2501
|
function stripCommentsAndStrings(source: string) {
|
|
2490
|
-
return stripComments(source).replace(/(['"`])(?:\\[\s\S]|(?!\1)[^\\])*\1/g, '')
|
|
2502
|
+
return stripComments(source).replace(/(['"`])(?:\\[\s\S]|(?!\1)[^\\])*\1/g, '')
|
|
2491
2503
|
}
|
|
2492
2504
|
|
|
2493
2505
|
function stripComments(source: string) {
|
|
2494
|
-
return source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1')
|
|
2506
|
+
return source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1')
|
|
2495
2507
|
}
|
|
2496
2508
|
|
|
2497
2509
|
function rootFromAppPath(appPath: string) {
|
|
2498
|
-
const normalized = toPosixPath(appPath)
|
|
2510
|
+
const normalized = toPosixPath(appPath)
|
|
2499
2511
|
return normalized.endsWith('/src/app')
|
|
2500
2512
|
? path.dirname(path.dirname(appPath))
|
|
2501
|
-
: path.dirname(appPath)
|
|
2513
|
+
: path.dirname(appPath)
|
|
2502
2514
|
}
|
|
2503
2515
|
|
|
2504
2516
|
function clientReference(file: string, exportName: string): ClientReference {
|
|
@@ -2506,25 +2518,25 @@ function clientReference(file: string, exportName: string): ClientReference {
|
|
|
2506
2518
|
id: `c-${clientReferenceId(file, exportName)}`,
|
|
2507
2519
|
file,
|
|
2508
2520
|
exportName,
|
|
2509
|
-
}
|
|
2521
|
+
}
|
|
2510
2522
|
}
|
|
2511
2523
|
|
|
2512
2524
|
function routeId(route: string) {
|
|
2513
|
-
if (route === '/') return 'index'
|
|
2525
|
+
if (route === '/') return 'index'
|
|
2514
2526
|
return route
|
|
2515
2527
|
.split('/')
|
|
2516
2528
|
.filter(Boolean)
|
|
2517
2529
|
.map(segment => {
|
|
2518
2530
|
if (segment.startsWith(':') && segment.endsWith('*'))
|
|
2519
|
-
return `catchall-${sanitizeIdPart(segment.slice(1, -1))}
|
|
2520
|
-
if (segment.startsWith(':')) return `param-${sanitizeIdPart(segment.slice(1))}
|
|
2521
|
-
return sanitizeIdPart(segment)
|
|
2531
|
+
return `catchall-${sanitizeIdPart(segment.slice(1, -1))}`
|
|
2532
|
+
if (segment.startsWith(':')) return `param-${sanitizeIdPart(segment.slice(1))}`
|
|
2533
|
+
return sanitizeIdPart(segment)
|
|
2522
2534
|
})
|
|
2523
|
-
.join('-')
|
|
2535
|
+
.join('-')
|
|
2524
2536
|
}
|
|
2525
2537
|
|
|
2526
2538
|
function sanitizeIdPart(value: string) {
|
|
2527
|
-
return value.replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'route'
|
|
2539
|
+
return value.replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'route'
|
|
2528
2540
|
}
|
|
2529
2541
|
|
|
2530
2542
|
function routeRank(route: RouteManifestEntry) {
|
|
@@ -2536,10 +2548,10 @@ function routeRank(route: RouteManifestEntry) {
|
|
|
2536
2548
|
.split('/')
|
|
2537
2549
|
.filter(Boolean)
|
|
2538
2550
|
.map(segment => (segment.startsWith(':') ? (segment.endsWith('*') ? '2' : '1') : '0'))
|
|
2539
|
-
.join('')
|
|
2540
|
-
return `${spec}:${route.synthetic ? 1 : 0}:${route.route}
|
|
2551
|
+
.join('')
|
|
2552
|
+
return `${spec}:${route.synthetic ? 1 : 0}:${route.route}`
|
|
2541
2553
|
}
|
|
2542
2554
|
|
|
2543
2555
|
function escapeRegex(value: string) {
|
|
2544
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
2556
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
2545
2557
|
}
|