@wular/pnext 0.0.4 → 0.0.7
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 +22 -16
- package/bin/pnext +6 -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 +87 -22
- package/reference/config.md +40 -93
- package/reference/css.md +42 -33
- package/reference/dev.md +26 -33
- package/reference/env.md +18 -20
- package/reference/getting-started.md +132 -0
- package/reference/metadata.md +31 -46
- package/reference/navigation.md +65 -105
- package/reference/performance.md +21 -142
- package/reference/rendering.md +78 -75
- package/reference/routing.md +97 -107
- package/reference/typegen.md +27 -29
- package/src/api/cache.ts +36 -37
- package/src/api/client-cache.ts +2 -2
- package/src/api/client-navigation.ts +121 -114
- package/src/api/dynamic.tsx +58 -55
- package/src/api/link.tsx +52 -53
- 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 +72 -64
- package/src/cli/dev.ts +116 -116
- package/src/cli/index.ts +94 -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 +802 -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 +32 -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 +2022 -1995
- package/src/{api → client}/router/types.ts +98 -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 +86 -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 +2087 -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 +87 -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 +954 -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 +428 -406
- package/src/styles.d.ts +9 -0
- package/src/types.ts +324 -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/reference/overview.md +0 -35
- 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
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
// undefined/non-component default export; and the output:'export' extras (exportPathMap + app, a
|
|
11
11
|
// dynamic route without generateStaticParams, route handlers without static opt-ins, force-dynamic).
|
|
12
12
|
|
|
13
|
-
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
14
|
-
import path from 'node:path'
|
|
15
|
-
import type { ResolvedConfig } from '../../config'
|
|
16
|
-
import { extraPageExtensions } from '../../extensions'
|
|
17
|
-
import type { RouteManifestEntry } from '../../types'
|
|
18
|
-
import { getNextConfig } from '../next/config-loader'
|
|
13
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
14
|
+
import path from 'node:path'
|
|
15
|
+
import type { ResolvedConfig } from '../../config'
|
|
16
|
+
import { extraPageExtensions } from '../../extensions'
|
|
17
|
+
import type { RouteManifestEntry } from '../../types'
|
|
18
|
+
import { getNextConfig } from '../next/config-loader'
|
|
19
19
|
import {
|
|
20
20
|
conflictingAppAndPageMessage,
|
|
21
21
|
conflictingParallelPagesMessage,
|
|
@@ -30,9 +30,9 @@ import {
|
|
|
30
30
|
routeHandlerNotStaticWithExportMessage,
|
|
31
31
|
undefinedDefaultExportMessage,
|
|
32
32
|
unresolvedCodemodCommentMessage,
|
|
33
|
-
} from './errors'
|
|
33
|
+
} from './errors'
|
|
34
34
|
|
|
35
|
-
const BASE_PAGE_EXTENSIONS = ['tsx', 'ts', 'jsx', 'js', 'mjs'] as const
|
|
35
|
+
const BASE_PAGE_EXTENSIONS = ['tsx', 'ts', 'jsx', 'js', 'mjs'] as const
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* A build-error the validation pass surfaces. Carries only Next's message; the
|
|
@@ -40,8 +40,8 @@ const BASE_PAGE_EXTENSIONS = ['tsx', 'ts', 'jsx', 'js', 'mjs'] as const;
|
|
|
40
40
|
*/
|
|
41
41
|
export class PnextBuildValidationError extends Error {
|
|
42
42
|
constructor(message: string) {
|
|
43
|
-
super(message)
|
|
44
|
-
this.name = 'PnextBuildValidationError'
|
|
43
|
+
super(message)
|
|
44
|
+
this.name = 'PnextBuildValidationError'
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -51,61 +51,61 @@ export class PnextBuildValidationError extends Error {
|
|
|
51
51
|
* pass never yields and each file is read at most once.
|
|
52
52
|
*/
|
|
53
53
|
export function validateBuild(config: ResolvedConfig, routes: RouteManifestEntry[]): void {
|
|
54
|
-
sourceCache.clear()
|
|
55
|
-
const pageRoutes = routes.filter(route => route.kind === 'page' && !route.interception)
|
|
54
|
+
sourceCache.clear()
|
|
55
|
+
const pageRoutes = routes.filter(route => route.kind === 'page' && !route.interception)
|
|
56
56
|
|
|
57
57
|
// Warnings first: Next surfaces them while compiling, i.e. BEFORE any
|
|
58
58
|
// app-structure error aborts the build. An app that trips both (the
|
|
59
59
|
// invalid-reexport fixture has no root layout at all) must still show them.
|
|
60
|
-
warnBuildDiagnostics(config, routes)
|
|
61
|
-
validateRootLayout(config, pageRoutes)
|
|
62
|
-
validateAppPagesConflict(config)
|
|
63
|
-
validateParallelPageConflicts(config, pageRoutes)
|
|
64
|
-
validateMissingSlotDefaults(config, routes)
|
|
65
|
-
validateCodemodComments(routes)
|
|
66
|
-
validateDefaultExports(config, routes)
|
|
67
|
-
validateSearchParamsSuspense(config, pageRoutes)
|
|
68
|
-
validateUseCacheDirectives(config, routes)
|
|
69
|
-
validateUseCacheSearchParams(routes)
|
|
70
|
-
validateUseCacheHangingInputs(routes)
|
|
71
|
-
validateCacheComponentsSegmentConfigs(config, routes)
|
|
72
|
-
validateInstantStaticShells(config, routes)
|
|
73
|
-
validateClientSegmentConfigs(config, routes)
|
|
74
|
-
validateImageLoaderFile(config)
|
|
75
|
-
validateStaticImageImports(config, routes)
|
|
76
|
-
validateExportMode(config, routes)
|
|
60
|
+
warnBuildDiagnostics(config, routes)
|
|
61
|
+
validateRootLayout(config, pageRoutes)
|
|
62
|
+
validateAppPagesConflict(config)
|
|
63
|
+
validateParallelPageConflicts(config, pageRoutes)
|
|
64
|
+
validateMissingSlotDefaults(config, routes)
|
|
65
|
+
validateCodemodComments(routes)
|
|
66
|
+
validateDefaultExports(config, routes)
|
|
67
|
+
validateSearchParamsSuspense(config, pageRoutes)
|
|
68
|
+
validateUseCacheDirectives(config, routes)
|
|
69
|
+
validateUseCacheSearchParams(routes)
|
|
70
|
+
validateUseCacheHangingInputs(routes)
|
|
71
|
+
validateCacheComponentsSegmentConfigs(config, routes)
|
|
72
|
+
validateInstantStaticShells(config, routes)
|
|
73
|
+
validateClientSegmentConfigs(config, routes)
|
|
74
|
+
validateImageLoaderFile(config)
|
|
75
|
+
validateStaticImageImports(config, routes)
|
|
76
|
+
validateExportMode(config, routes)
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
function validateInstantStaticShells(config: ResolvedConfig, routes: RouteManifestEntry[]): void {
|
|
80
|
-
if (!cacheComponentsIsEnabled()) return
|
|
80
|
+
if (!cacheComponentsIsEnabled()) return
|
|
81
81
|
for (const route of routes) {
|
|
82
|
-
if (route.kind !== 'page') continue
|
|
83
|
-
const pageSource = readSource(route.file)
|
|
82
|
+
if (route.kind !== 'page') continue
|
|
83
|
+
const pageSource = readSource(route.file)
|
|
84
84
|
if (!pageSource || !/\bexport\s+const\s+unstable_instant\s*=\s*false\b/.test(pageSource))
|
|
85
|
-
continue
|
|
86
|
-
if (!/\bconnection\s*\(/.test(pageSource)) continue
|
|
85
|
+
continue
|
|
86
|
+
if (!/\bconnection\s*\(/.test(pageSource)) continue
|
|
87
87
|
const staticParent = layoutChain(config.appPath, route.file).some(file => {
|
|
88
|
-
const source = readSource(file)
|
|
89
|
-
return Boolean(source && /\bexport\s+const\s+unstable_instant\s*=\s*true\b/.test(source))
|
|
90
|
-
})
|
|
91
|
-
if (!staticParent) continue
|
|
88
|
+
const source = readSource(file)
|
|
89
|
+
return Boolean(source && /\bexport\s+const\s+unstable_instant\s*=\s*true\b/.test(source))
|
|
90
|
+
})
|
|
91
|
+
if (!staticParent) continue
|
|
92
92
|
throw new PnextBuildValidationError(
|
|
93
93
|
`Error occurred during prerendering page "${route.route || '/'}": Next.js encountered uncached data during prerendering.`,
|
|
94
|
-
)
|
|
94
|
+
)
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
function validateUseCacheHangingInputs(routes: RouteManifestEntry[]): void {
|
|
99
|
-
if (!cacheComponentsIsEnabled()) return
|
|
100
|
-
const failures: string[] = []
|
|
99
|
+
if (!cacheComponentsIsEnabled()) return
|
|
100
|
+
const failures: string[] = []
|
|
101
101
|
const timeout =
|
|
102
|
-
'Filling a cache during prerender timed out, likely because request-specific arguments such as params, searchParams, cookies() or dynamic data were used inside "use cache".'
|
|
102
|
+
'Filling a cache during prerender timed out, likely because request-specific arguments such as params, searchParams, cookies() or dynamic data were used inside "use cache".'
|
|
103
103
|
for (const route of routes) {
|
|
104
|
-
if (route.kind !== 'page') continue
|
|
105
|
-
const source = readSource(route.file)
|
|
106
|
-
if (!source || useCacheDirectives(source).length === 0) continue
|
|
107
|
-
const routePath = nextPagePath(route)
|
|
108
|
-
const thrown = /\bthrow\s+new\s+Error\(\s*['"]([^'"]+)['"]\s*\)/.exec(source)
|
|
104
|
+
if (route.kind !== 'page') continue
|
|
105
|
+
const source = readSource(route.file)
|
|
106
|
+
if (!source || useCacheDirectives(source).length === 0) continue
|
|
107
|
+
const routePath = nextPagePath(route)
|
|
108
|
+
const thrown = /\bthrow\s+new\s+Error\(\s*['"]([^'"]+)['"]\s*\)/.exec(source)
|
|
109
109
|
// Only a throw the PRERENDER would execute fails the build. A page that
|
|
110
110
|
// reads request data outside its cache scopes never prerenders, so its
|
|
111
111
|
// throw surfaces as a runtime error log instead (use-cache-runtime-error /
|
|
@@ -113,8 +113,8 @@ function validateUseCacheHangingInputs(routes: RouteManifestEntry[]): void {
|
|
|
113
113
|
if (thrown?.[1] && !readsRequestDataOutsideCache(source)) {
|
|
114
114
|
failures.push(
|
|
115
115
|
`Error occurred prerendering page "${routePath}". Read more: https://nextjs.org/docs/messages/prerender-error\nError: ${thrown[1]}`,
|
|
116
|
-
)
|
|
117
|
-
continue
|
|
116
|
+
)
|
|
117
|
+
continue
|
|
118
118
|
}
|
|
119
119
|
// Only an await of a hanging input INSIDE a cache scope hangs the fill; an
|
|
120
120
|
// `await params` outside the cached function resolves normally (e.g. under
|
|
@@ -122,7 +122,7 @@ function validateUseCacheHangingInputs(routes: RouteManifestEntry[]): void {
|
|
|
122
122
|
// values cross the cache boundary. Scope the heuristic to cache bodies.
|
|
123
123
|
// Private scopes never hang the build: they are excluded from prerenders
|
|
124
124
|
// entirely (evaluated per-request), so only shared cache bodies count.
|
|
125
|
-
const scoped = useCacheScopedSource(source).filter(scope => scope.kind !== 'private')
|
|
125
|
+
const scoped = useCacheScopedSource(source).filter(scope => scope.kind !== 'private')
|
|
126
126
|
// `await params` inside a cache scope only hangs when the params can never resolve during the
|
|
127
127
|
// prerender. With generateStaticParams the concrete prerenders pass resolved params (they join the
|
|
128
128
|
// cache key) and the fallback-shell prerender aborts the fill to a dynamic hole, so the heuristic
|
|
@@ -135,82 +135,82 @@ function validateUseCacheHangingInputs(routes: RouteManifestEntry[]): void {
|
|
|
135
135
|
const isHangingInput = (ident: string): boolean =>
|
|
136
136
|
ident === 'promise' ||
|
|
137
137
|
/^[A-Za-z_$][\w$]*Promise$/.test(ident) ||
|
|
138
|
-
(!route.hasStaticParams && ident === 'params')
|
|
138
|
+
(!route.hasStaticParams && ident === 'params')
|
|
139
139
|
const hangs = scoped.some(scope => {
|
|
140
|
-
const params = new Set(scope.params)
|
|
140
|
+
const params = new Set(scope.params)
|
|
141
141
|
return awaitedIdentifiers(scope.body).some(
|
|
142
142
|
ident => isHangingInput(ident) && !params.has(ident),
|
|
143
|
-
)
|
|
144
|
-
})
|
|
143
|
+
)
|
|
144
|
+
})
|
|
145
145
|
// The exemption above (a cached function awaiting its OWN parameter) only holds when the CALLER
|
|
146
146
|
// passes something that resolves during the prerender. Passing an uncached promise IN
|
|
147
147
|
// (`<Foo promise={fetchData()} />`, `indirection(getUncachedData())`) is the hanging-input
|
|
148
148
|
// violation: the fill waits on request-time IO a prerender never resolves. Detect it at the cached
|
|
149
149
|
// function's call sites. A value produced by ANOTHER cached function resolves from the cache
|
|
150
150
|
// during the prerender, so only calls into uncached code count.
|
|
151
|
-
const cachedNames = new Set(scoped.map(scope => scope.name).filter(name => name !== ''))
|
|
151
|
+
const cachedNames = new Set(scoped.map(scope => scope.name).filter(name => name !== ''))
|
|
152
152
|
// A REQUEST-BOUND promise (`cookies()`, `headers()`, a `next/root-params` accessor) is not a
|
|
153
153
|
// hanging input either: such a page never statically prerenders - the request API postpones the
|
|
154
154
|
// boundary and the route builds as runtime-prefetchable/dynamic - so the fill resolves from
|
|
155
155
|
// request data instead of hanging.
|
|
156
|
-
const exemptNames = new Set([...cachedNames, ...requestApiNames(source)])
|
|
156
|
+
const exemptNames = new Set([...cachedNames, ...requestApiNames(source)])
|
|
157
157
|
const receivesUncachedPromise = scoped.some(
|
|
158
158
|
scope =>
|
|
159
159
|
scope.name !== '' &&
|
|
160
160
|
scope.params.length > 0 &&
|
|
161
161
|
passesUncachedPromiseInto(source, scope.name, exemptNames),
|
|
162
|
-
)
|
|
163
|
-
if (!hangs && !receivesUncachedPromise) continue
|
|
162
|
+
)
|
|
163
|
+
if (!hangs && !receivesUncachedPromise) continue
|
|
164
164
|
failures.push(
|
|
165
165
|
`${timeout}\nError occurred prerendering page "${routePath}". Read more: https://nextjs.org/docs/messages/prerender-error`,
|
|
166
|
-
)
|
|
166
|
+
)
|
|
167
167
|
}
|
|
168
|
-
if (failures.length > 0) throw new PnextBuildValidationError(failures.join('\n\n'))
|
|
168
|
+
if (failures.length > 0) throw new PnextBuildValidationError(failures.join('\n\n'))
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
function validateUseCacheSearchParams(routes: RouteManifestEntry[]): void {
|
|
172
|
-
if (!cacheComponentsIsEnabled()) return
|
|
173
|
-
const failures: string[] = []
|
|
172
|
+
if (!cacheComponentsIsEnabled()) return
|
|
173
|
+
const failures: string[] = []
|
|
174
174
|
for (const route of routes) {
|
|
175
|
-
if (route.kind !== 'page') continue
|
|
176
|
-
const source = readSource(route.file)
|
|
177
|
-
if (!source || useCacheDirectives(source).length === 0) continue
|
|
175
|
+
if (route.kind !== 'page') continue
|
|
176
|
+
const source = readSource(route.file)
|
|
177
|
+
if (!source || useCacheDirectives(source).length === 0) continue
|
|
178
178
|
// Only an await INSIDE a shared cache scope is the E842 violation. A
|
|
179
179
|
// `use cache: private` scope MAY read searchParams (it is excluded from
|
|
180
180
|
// prerenders and evaluated per-request), and an await outside any cache
|
|
181
181
|
// body is an ordinary dynamic read.
|
|
182
|
-
const scoped = useCacheScopedSource(source).filter(scope => scope.kind !== 'private')
|
|
182
|
+
const scoped = useCacheScopedSource(source).filter(scope => scope.kind !== 'private')
|
|
183
183
|
// Awaiting `searchParams` that is the cached function's OWN parameter is the
|
|
184
184
|
// legal runtime-prefetch pattern (the page passes the searchParams promise
|
|
185
185
|
// into `publicCache(searchParams)` as a vary key). Only a closed-over
|
|
186
186
|
// page-level `searchParams` read is the E842 violation.
|
|
187
187
|
const violates = scoped.some(scope => {
|
|
188
|
-
const params = new Set(scope.params)
|
|
188
|
+
const params = new Set(scope.params)
|
|
189
189
|
return awaitedIdentifiers(scope.body).some(
|
|
190
190
|
ident => ident === 'searchParams' && !params.has(ident),
|
|
191
|
-
)
|
|
192
|
-
})
|
|
193
|
-
if (!violates) continue
|
|
194
|
-
const routePath = route.route || '/'
|
|
191
|
+
)
|
|
192
|
+
})
|
|
193
|
+
if (!violates) continue
|
|
194
|
+
const routePath = route.route || '/'
|
|
195
195
|
failures.push(
|
|
196
196
|
`Route ${routePath} used \`searchParams\` inside "use cache". Accessing dynamic request data inside a cache scope is not supported. ` +
|
|
197
197
|
'If you need some search params inside a cached function await `searchParams` outside of the cached function and pass only the required search params as arguments to the cached function. ' +
|
|
198
198
|
'See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache\n' +
|
|
199
199
|
`Error occurred prerendering page "${routePath}"`,
|
|
200
|
-
)
|
|
200
|
+
)
|
|
201
201
|
}
|
|
202
|
-
if (failures.length > 0) throw new PnextBuildValidationError(failures.join('\n\n'))
|
|
202
|
+
if (failures.length > 0) throw new PnextBuildValidationError(failures.join('\n\n'))
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
function validateClientSegmentConfigs(config: ResolvedConfig, routes: RouteManifestEntry[]): void {
|
|
206
206
|
for (const route of routes) {
|
|
207
|
-
if (route.kind !== 'page') continue
|
|
208
|
-
const source = readSource(route.file)
|
|
209
|
-
if (!source || !hasDirective(source, 'use client')) continue
|
|
207
|
+
if (route.kind !== 'page') continue
|
|
208
|
+
const source = readSource(route.file)
|
|
209
|
+
if (!source || !hasDirective(source, 'use client')) continue
|
|
210
210
|
if (/\bexport\s+const\s+unstable_instant\b/.test(source)) {
|
|
211
211
|
throw new PnextBuildValidationError(
|
|
212
212
|
`"unstable_instant" is a route segment config and can only be used when the segment is a Server Component module. Remove the "use client" directive`,
|
|
213
|
-
)
|
|
213
|
+
)
|
|
214
214
|
}
|
|
215
215
|
if (
|
|
216
216
|
getNextConfig().output === 'export' &&
|
|
@@ -218,54 +218,54 @@ function validateClientSegmentConfigs(config: ResolvedConfig, routes: RouteManif
|
|
|
218
218
|
) {
|
|
219
219
|
throw new PnextBuildValidationError(
|
|
220
220
|
`Page "/${appDirRelativeFile(config, route.file).replace(/\.[^.]+$/, '')}" cannot use both "use client" and export function "generateStaticParams()".`,
|
|
221
|
-
)
|
|
221
|
+
)
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
function validateImageLoaderFile(config: ResolvedConfig): void {
|
|
227
|
-
const images = getNextConfig().images
|
|
228
|
-
if (!images || typeof images !== 'object' || Array.isArray(images)) return
|
|
229
|
-
const loaderFile = (images as Record<string, unknown>).loaderFile
|
|
230
|
-
if (typeof loaderFile !== 'string' || loaderFile.length === 0) return
|
|
231
|
-
const file = path.resolve(config.root, loaderFile)
|
|
232
|
-
const source = readSource(file)
|
|
233
|
-
if (source && /\bexport\s+default\b|\bmodule\.exports\s*=/.test(source)) return
|
|
227
|
+
const images = getNextConfig().images
|
|
228
|
+
if (!images || typeof images !== 'object' || Array.isArray(images)) return
|
|
229
|
+
const loaderFile = (images as Record<string, unknown>).loaderFile
|
|
230
|
+
if (typeof loaderFile !== 'string' || loaderFile.length === 0) return
|
|
231
|
+
const file = path.resolve(config.root, loaderFile)
|
|
232
|
+
const source = readSource(file)
|
|
233
|
+
if (source && /\bexport\s+default\b|\bmodule\.exports\s*=/.test(source)) return
|
|
234
234
|
throw new PnextBuildValidationError(
|
|
235
235
|
'images.loaderFile detected but the file is missing default export.\n' +
|
|
236
236
|
'Read more: https://nextjs.org/docs/messages/invalid-images-config',
|
|
237
|
-
)
|
|
237
|
+
)
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
function validateStaticImageImports(config: ResolvedConfig, routes: RouteManifestEntry[]): void {
|
|
241
|
-
const seen = new Set<string>()
|
|
241
|
+
const seen = new Set<string>()
|
|
242
242
|
for (const route of routes) {
|
|
243
243
|
for (const file of routeFiles(route)) {
|
|
244
|
-
if (seen.has(file)) continue
|
|
245
|
-
seen.add(file)
|
|
246
|
-
if (!isAppSourceFile(config, file)) continue
|
|
247
|
-
const source = readSource(file)
|
|
248
|
-
if (!source) continue
|
|
244
|
+
if (seen.has(file)) continue
|
|
245
|
+
seen.add(file)
|
|
246
|
+
if (!isAppSourceFile(config, file)) continue
|
|
247
|
+
const source = readSource(file)
|
|
248
|
+
if (!source) continue
|
|
249
249
|
const imports = source.matchAll(
|
|
250
250
|
/\bimport\s+(?:[^'";]+?\s+from\s+)?['"]([^'"]+\.(?:png|jpe?g|gif|webp|avif|ico|bmp|svg))['"]/gi,
|
|
251
|
-
)
|
|
251
|
+
)
|
|
252
252
|
for (const match of imports) {
|
|
253
|
-
const specifier = match[1]
|
|
254
|
-
if (!specifier.startsWith('.')) continue
|
|
255
|
-
if (existsSync(path.resolve(path.dirname(file), specifier))) continue
|
|
253
|
+
const specifier = match[1]!
|
|
254
|
+
if (!specifier.startsWith('.')) continue
|
|
255
|
+
if (existsSync(path.resolve(path.dirname(file), specifier))) continue
|
|
256
256
|
throw new PnextBuildValidationError(
|
|
257
257
|
`Module not found: Can't resolve '${specifier}'\n./app/${appDirRelativeFile(config, file)}`,
|
|
258
|
-
)
|
|
258
|
+
)
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
function warnBuildDiagnostics(config: ResolvedConfig, routes: RouteManifestEntry[]): void {
|
|
265
|
-
const nextConfig = getNextConfig()
|
|
266
|
-
const experimental = nextConfig.experimental as Record<string, unknown> | undefined
|
|
265
|
+
const nextConfig = getNextConfig()
|
|
266
|
+
const experimental = nextConfig.experimental as Record<string, unknown> | undefined
|
|
267
267
|
if (hasBabelConfig(config.root) && experimental?.forceSwcTransforms !== true) {
|
|
268
|
-
console.warn('Disabled SWC as replacement for Babel because of custom Babel configuration')
|
|
268
|
+
console.warn('Disabled SWC as replacement for Babel because of custom Babel configuration')
|
|
269
269
|
}
|
|
270
270
|
if (
|
|
271
271
|
typeof nextConfig.outputFileTracingRoot !== 'string' &&
|
|
@@ -275,23 +275,23 @@ function warnBuildDiagnostics(config: ResolvedConfig, routes: RouteManifestEntry
|
|
|
275
275
|
typeof (nextConfig.turbopack as Record<string, unknown>).root === 'string'
|
|
276
276
|
)
|
|
277
277
|
) {
|
|
278
|
-
warnMultipleLockfiles(config.root)
|
|
278
|
+
warnMultipleLockfiles(config.root)
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
let warnedEdgeRuntime = false
|
|
281
|
+
let warnedEdgeRuntime = false
|
|
282
282
|
for (const file of new Set(routes.flatMap(route => routeFiles(route)))) {
|
|
283
|
-
if (!isAppSourceFile(config, file)) continue
|
|
284
|
-
const source = readSource(file)
|
|
285
|
-
if (!source) continue
|
|
283
|
+
if (!isAppSourceFile(config, file)) continue
|
|
284
|
+
const source = readSource(file)
|
|
285
|
+
if (!source) continue
|
|
286
286
|
if (!warnedEdgeRuntime && /\bruntime\s*=\s*['"](?:edge|experimental-edge)['"]/.test(source)) {
|
|
287
|
-
console.warn('The Edge Runtime is deprecated. You can use the "nodejs" runtime instead.')
|
|
288
|
-
warnedEdgeRuntime = true
|
|
287
|
+
console.warn('The Edge Runtime is deprecated. You can use the "nodejs" runtime instead.')
|
|
288
|
+
warnedEdgeRuntime = true
|
|
289
289
|
}
|
|
290
290
|
for (const key of ['runtime', 'preferredRegion'] as const) {
|
|
291
291
|
if (new RegExp(`\\bexport\\s*\\{[^}]*\\b${key}\\b[^}]*\\}\\s*from\\s*['"]`).test(source)) {
|
|
292
292
|
console.warn(
|
|
293
293
|
`Next.js can't recognize the exported \`${key}\` field in ${toPosix(path.relative(config.root, file))}`,
|
|
294
|
-
)
|
|
294
|
+
)
|
|
295
295
|
}
|
|
296
296
|
}
|
|
297
297
|
}
|
|
@@ -305,14 +305,14 @@ function hasBabelConfig(root: string): boolean {
|
|
|
305
305
|
'.babelrc.cjs',
|
|
306
306
|
'babel.config.js',
|
|
307
307
|
'babel.config.cjs',
|
|
308
|
-
].some(name => existsSync(path.join(root, name)))
|
|
308
|
+
].some(name => existsSync(path.join(root, name)))
|
|
309
309
|
}
|
|
310
310
|
|
|
311
311
|
function warnMultipleLockfiles(root: string): void {
|
|
312
312
|
// A configured `outputFileTracingRoot` is authoritative — Next stays silent.
|
|
313
|
-
if (typeof getNextConfig().outputFileTracingRoot === 'string') return
|
|
314
|
-
const lockfiles: string[] = []
|
|
315
|
-
let current = root
|
|
313
|
+
if (typeof getNextConfig().outputFileTracingRoot === 'string') return
|
|
314
|
+
const lockfiles: string[] = []
|
|
315
|
+
let current = root
|
|
316
316
|
while (true) {
|
|
317
317
|
for (const name of [
|
|
318
318
|
'package-lock.json',
|
|
@@ -321,25 +321,25 @@ function warnMultipleLockfiles(root: string): void {
|
|
|
321
321
|
'bun.lock',
|
|
322
322
|
'bun.lockb',
|
|
323
323
|
]) {
|
|
324
|
-
const file = path.join(current, name)
|
|
325
|
-
if (existsSync(file)) lockfiles.push(file)
|
|
324
|
+
const file = path.join(current, name)
|
|
325
|
+
if (existsSync(file)) lockfiles.push(file)
|
|
326
326
|
}
|
|
327
327
|
// A `pnpm-workspace.yaml` marks the definitive workspace root: stop the walk
|
|
328
328
|
// there (nothing above it belongs to this project), but still report what was
|
|
329
329
|
// collected — a lockfile written between the app and the workspace root is
|
|
330
330
|
// exactly the ambiguity Next warns about.
|
|
331
|
-
if (existsSync(path.join(current, 'pnpm-workspace.yaml'))) break
|
|
332
|
-
const parent = path.dirname(current)
|
|
333
|
-
if (parent === current) break
|
|
334
|
-
current = parent
|
|
331
|
+
if (existsSync(path.join(current, 'pnpm-workspace.yaml'))) break
|
|
332
|
+
const parent = path.dirname(current)
|
|
333
|
+
if (parent === current) break
|
|
334
|
+
current = parent
|
|
335
335
|
}
|
|
336
336
|
// The ambiguity is about DIRECTORIES: two lockfiles side by side in one
|
|
337
337
|
// directory still name a single root.
|
|
338
|
-
const dirs = [...new Set(lockfiles.map(file => path.dirname(file)))]
|
|
339
|
-
if (dirs.length < 2) return
|
|
338
|
+
const dirs = [...new Set(lockfiles.map(file => path.dirname(file)))]
|
|
339
|
+
if (dirs.length < 2) return
|
|
340
340
|
// Next selects the closest lockfile's directory as the root and lists the rest.
|
|
341
|
-
const selected = dirs[0]
|
|
342
|
-
const additional = lockfiles.filter(file => path.dirname(file) !== selected)
|
|
341
|
+
const selected = dirs[0]
|
|
342
|
+
const additional = lockfiles.filter(file => path.dirname(file) !== selected)
|
|
343
343
|
console.warn(
|
|
344
344
|
'⚠ Warning: Next.js inferred your workspace root, but it may not be correct.\n' +
|
|
345
345
|
` We detected multiple lockfiles and selected the directory of ${selected} as the root directory.\n` +
|
|
@@ -347,7 +347,7 @@ function warnMultipleLockfiles(root: string): void {
|
|
|
347
347
|
' See https://nextjs.org/docs/app/api-reference/config/next-config-js/output#caveats for more information.\n' +
|
|
348
348
|
' Detected additional lockfiles: \n' +
|
|
349
349
|
additional.map(file => ` * ${file}`).join('\n'),
|
|
350
|
-
)
|
|
350
|
+
)
|
|
351
351
|
}
|
|
352
352
|
|
|
353
353
|
/**
|
|
@@ -355,47 +355,47 @@ function warnMultipleLockfiles(root: string): void {
|
|
|
355
355
|
* is the route's whole graph, so a shared layout's modules appear in every route's list. A build
|
|
356
356
|
* never sees its sources change under it.
|
|
357
357
|
*/
|
|
358
|
-
const sourceCache = new Map<string, string | undefined>()
|
|
358
|
+
const sourceCache = new Map<string, string | undefined>()
|
|
359
359
|
|
|
360
360
|
function readSource(file: string): string | undefined {
|
|
361
|
-
if (sourceCache.has(file)) return sourceCache.get(file)
|
|
362
|
-
let source: string | undefined
|
|
361
|
+
if (sourceCache.has(file)) return sourceCache.get(file)
|
|
362
|
+
let source: string | undefined
|
|
363
363
|
try {
|
|
364
|
-
source = readFileSync(file, 'utf8')
|
|
364
|
+
source = readFileSync(file, 'utf8')
|
|
365
365
|
} catch {
|
|
366
|
-
source = undefined
|
|
366
|
+
source = undefined
|
|
367
367
|
}
|
|
368
|
-
sourceCache.set(file, source)
|
|
369
|
-
return source
|
|
368
|
+
sourceCache.set(file, source)
|
|
369
|
+
return source
|
|
370
370
|
}
|
|
371
371
|
|
|
372
372
|
function routeFiles(route: RouteManifestEntry): string[] {
|
|
373
|
-
return [...new Set([route.file, ...route.sourceFiles])]
|
|
373
|
+
return [...new Set([route.file, ...route.sourceFiles])]
|
|
374
374
|
}
|
|
375
375
|
|
|
376
376
|
function isAppSourceFile(config: ResolvedConfig, file: string): boolean {
|
|
377
|
-
if (isInside(config.outPath, file)) return false
|
|
378
|
-
const relative = path.relative(config.root, file)
|
|
379
|
-
if (relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative)) return true
|
|
377
|
+
if (isInside(config.outPath, file)) return false
|
|
378
|
+
const relative = path.relative(config.root, file)
|
|
379
|
+
if (relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative)) return true
|
|
380
380
|
return (
|
|
381
381
|
file.includes(`${path.sep}source-app${path.sep}`) ||
|
|
382
382
|
file.includes(`${path.sep}source-pages${path.sep}`)
|
|
383
|
-
)
|
|
383
|
+
)
|
|
384
384
|
}
|
|
385
385
|
|
|
386
386
|
function isInside(parent: string, file: string): boolean {
|
|
387
|
-
const relative = path.relative(parent, file)
|
|
388
|
-
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
|
|
387
|
+
const relative = path.relative(parent, file)
|
|
388
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
|
|
389
389
|
}
|
|
390
390
|
|
|
391
391
|
function hasDirective(source: string, directive: string): boolean {
|
|
392
|
-
return directivePrologue(source, 0).includes(directive)
|
|
392
|
+
return directivePrologue(source, 0).includes(directive)
|
|
393
393
|
}
|
|
394
394
|
|
|
395
395
|
/** Whether `cacheComponents` (or a legacy alias) is enabled in next.config. */
|
|
396
396
|
function cacheComponentsIsEnabled(): boolean {
|
|
397
|
-
const nextConfig = getNextConfig()
|
|
398
|
-
const experimental = nextConfig.experimental as Record<string, unknown> | undefined
|
|
397
|
+
const nextConfig = getNextConfig()
|
|
398
|
+
const experimental = nextConfig.experimental as Record<string, unknown> | undefined
|
|
399
399
|
return (
|
|
400
400
|
nextConfig.cacheComponents === true ||
|
|
401
401
|
experimental?.cacheComponents === true ||
|
|
@@ -403,7 +403,7 @@ function cacheComponentsIsEnabled(): boolean {
|
|
|
403
403
|
experimental?.dynamicIO === true ||
|
|
404
404
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
405
405
|
process.env.__NEXT_CACHE_COMPONENTS === 'true'
|
|
406
|
-
)
|
|
406
|
+
)
|
|
407
407
|
}
|
|
408
408
|
|
|
409
409
|
// Segment-config exports `cacheComponents` forbids: each forces a dynamic / caching / runtime disposition
|
|
@@ -418,9 +418,9 @@ const CACHE_COMPONENTS_FORBIDDEN_SEGMENT_CONFIGS = [
|
|
|
418
418
|
'dynamicParams',
|
|
419
419
|
'revalidate',
|
|
420
420
|
'fetchCache',
|
|
421
|
-
] as const
|
|
421
|
+
] as const
|
|
422
422
|
|
|
423
|
-
const SEGMENT_CONFIG_FILE = /(?:^|\/)(?:page|layout|route|default)\.(?:tsx|ts|jsx|js|mjs)
|
|
423
|
+
const SEGMENT_CONFIG_FILE = /(?:^|\/)(?:page|layout|route|default)\.(?:tsx|ts|jsx|js|mjs)$/
|
|
424
424
|
|
|
425
425
|
/**
|
|
426
426
|
* Under `cacheComponents`, error when a route segment (page/layout/route/
|
|
@@ -435,18 +435,18 @@ function validateCacheComponentsSegmentConfigs(
|
|
|
435
435
|
config: ResolvedConfig,
|
|
436
436
|
routes: RouteManifestEntry[],
|
|
437
437
|
): void {
|
|
438
|
-
if (!cacheComponentsIsEnabled()) return
|
|
439
|
-
const projectRoot = path.resolve(config.appPath, '..')
|
|
440
|
-
const seen = new Set<string>()
|
|
441
|
-
const failures: string[] = []
|
|
438
|
+
if (!cacheComponentsIsEnabled()) return
|
|
439
|
+
const projectRoot = path.resolve(config.appPath, '..')
|
|
440
|
+
const seen = new Set<string>()
|
|
441
|
+
const failures: string[] = []
|
|
442
442
|
for (const route of routes) {
|
|
443
443
|
for (const file of route.sourceFiles) {
|
|
444
|
-
if (seen.has(file)) continue
|
|
445
|
-
if (!SEGMENT_CONFIG_FILE.test(toPosix(file))) continue
|
|
446
|
-
seen.add(file)
|
|
447
|
-
if (!isProjectSourceFile(projectRoot, file)) continue
|
|
448
|
-
const source = readSource(file)
|
|
449
|
-
if (source === undefined) continue
|
|
444
|
+
if (seen.has(file)) continue
|
|
445
|
+
if (!SEGMENT_CONFIG_FILE.test(toPosix(file))) continue
|
|
446
|
+
seen.add(file)
|
|
447
|
+
if (!isProjectSourceFile(projectRoot, file)) continue
|
|
448
|
+
const source = readSource(file)
|
|
449
|
+
if (source === undefined) continue
|
|
450
450
|
// Skip pnext-generated pages-router wrappers. In a hybrid app the pages/
|
|
451
451
|
// materializer emits an app `page.js` that re-exports from a `source-pages`
|
|
452
452
|
// symlink and, for getServerSideProps pages, injects
|
|
@@ -454,12 +454,12 @@ function validateCacheComponentsSegmentConfigs(
|
|
|
454
454
|
// segment-config check is an app-router concern; a pages-router page uses
|
|
455
455
|
// getServerSideProps legitimately and never authored that config, so the
|
|
456
456
|
// synthetic wrapper must not be flagged.
|
|
457
|
-
if (isMaterializedPagesWrapper(source)) continue
|
|
458
|
-
let earliest: { key: string; index: number } | undefined
|
|
457
|
+
if (isMaterializedPagesWrapper(source)) continue
|
|
458
|
+
let earliest: { key: string; index: number } | undefined
|
|
459
459
|
for (const key of CACHE_COMPONENTS_FORBIDDEN_SEGMENT_CONFIGS) {
|
|
460
|
-
const match = new RegExp(`(?:^|[;\\n])\\s*export\\s+const\\s+${key}\\b`).exec(source)
|
|
460
|
+
const match = new RegExp(`(?:^|[;\\n])\\s*export\\s+const\\s+${key}\\b`).exec(source)
|
|
461
461
|
if (match && (earliest === undefined || match.index < earliest.index)) {
|
|
462
|
-
earliest = { key, index: match.index }
|
|
462
|
+
earliest = { key, index: match.index }
|
|
463
463
|
}
|
|
464
464
|
}
|
|
465
465
|
// `runtime` is forbidden under cacheComponents ONLY when it pins a non-`edge` runtime.
|
|
@@ -467,55 +467,56 @@ function validateCacheComponentsSegmentConfigs(
|
|
|
467
467
|
// "remove it" error, and that rejection propagates from a layout to every page under it.
|
|
468
468
|
// Under `experimental.useCache` ALONE there is no dynamic-shell opt-in, so EVERY runtime value -
|
|
469
469
|
// `edge` included - is rejected.
|
|
470
|
-
const runtimeMatch =
|
|
471
|
-
|
|
470
|
+
const runtimeMatch = /(?:^|[;\n])\s*export\s+const\s+runtime\s*=\s*['"]([^'"]+)['"]/.exec(
|
|
471
|
+
source,
|
|
472
|
+
)
|
|
472
473
|
if (
|
|
473
474
|
runtimeMatch &&
|
|
474
475
|
(runtimeMatch[1] !== 'edge' || useCacheOnlyIsEnabled()) &&
|
|
475
476
|
(earliest === undefined || runtimeMatch.index < earliest.index)
|
|
476
477
|
) {
|
|
477
|
-
earliest = { key: 'runtime', index: runtimeMatch.index }
|
|
478
|
+
earliest = { key: 'runtime', index: runtimeMatch.index }
|
|
478
479
|
}
|
|
479
|
-
if (!earliest) continue
|
|
480
|
+
if (!earliest) continue
|
|
480
481
|
// Under `experimental.useCache` ONLY (no cacheComponents alias), Next
|
|
481
482
|
// fails at webpack compile with a differently-worded error and a full
|
|
482
483
|
// webpack-errors block that the use-cache-segment-configs suite
|
|
483
484
|
// inline-snapshots — emit that exact block for the first offending file.
|
|
484
485
|
if (useCacheOnlyIsEnabled()) {
|
|
485
|
-
throw new PnextBuildValidationError(useCacheSegmentConfigBuildError(source, earliest.key))
|
|
486
|
+
throw new PnextBuildValidationError(useCacheSegmentConfigBuildError(source, earliest.key))
|
|
486
487
|
}
|
|
487
488
|
// Next prints the offending file on its own line ahead of the message. A layout is not a webpack
|
|
488
489
|
// entry - it is imported by every page beneath it, so its config error propagates and webpack attaches
|
|
489
490
|
// an "Import trace for requested module" block naming each importing page then the layout. Reproduce
|
|
490
491
|
// one trace block per importing page so the propagation suite's page-to-layout grep matches, which
|
|
491
492
|
// also proves the layout config reaches the pages.
|
|
492
|
-
let trace = ''
|
|
493
|
+
let trace = ''
|
|
493
494
|
if (/(?:^|\/)layout\.(?:tsx|ts|jsx|js|mjs)$/.test(toPosix(file))) {
|
|
494
|
-
const importers = new Set<string>()
|
|
495
|
+
const importers = new Set<string>()
|
|
495
496
|
for (const other of routes) {
|
|
496
|
-
if (other.kind !== 'page') continue
|
|
497
|
-
if (!other.sourceFiles.includes(file)) continue
|
|
498
|
-
importers.add(`./app/${appDirRelativeFile(config, other.file)}`)
|
|
497
|
+
if (other.kind !== 'page') continue
|
|
498
|
+
if (!other.sourceFiles.includes(file)) continue
|
|
499
|
+
importers.add(`./app/${appDirRelativeFile(config, other.file)}`)
|
|
499
500
|
}
|
|
500
|
-
const layoutRel = `./app/${appDirRelativeFile(config, file)}
|
|
501
|
+
const layoutRel = `./app/${appDirRelativeFile(config, file)}`
|
|
501
502
|
for (const importer of [...importers].sort()) {
|
|
502
|
-
trace += `\n\nImport trace for requested module:\n${importer}\n${layoutRel}
|
|
503
|
+
trace += `\n\nImport trace for requested module:\n${importer}\n${layoutRel}`
|
|
503
504
|
}
|
|
504
505
|
}
|
|
505
506
|
failures.push(
|
|
506
507
|
`./app/${appDirRelativeFile(config, file)}\n` +
|
|
507
508
|
incompatibleCacheComponentsSegmentConfigMessage(earliest.key) +
|
|
508
509
|
trace,
|
|
509
|
-
)
|
|
510
|
+
)
|
|
510
511
|
}
|
|
511
512
|
}
|
|
512
|
-
if (failures.length > 0) throw new PnextBuildValidationError(failures.join('\n\n'))
|
|
513
|
+
if (failures.length > 0) throw new PnextBuildValidationError(failures.join('\n\n'))
|
|
513
514
|
}
|
|
514
515
|
|
|
515
516
|
/** Enabled via `experimental.useCache` alone (no cacheComponents alias). */
|
|
516
517
|
function useCacheOnlyIsEnabled(): boolean {
|
|
517
|
-
const nextConfig = getNextConfig()
|
|
518
|
-
const experimental = nextConfig.experimental as Record<string, unknown> | undefined
|
|
518
|
+
const nextConfig = getNextConfig()
|
|
519
|
+
const experimental = nextConfig.experimental as Record<string, unknown> | undefined
|
|
519
520
|
return (
|
|
520
521
|
experimental?.useCache === true &&
|
|
521
522
|
nextConfig.cacheComponents !== true &&
|
|
@@ -523,7 +524,7 @@ function useCacheOnlyIsEnabled(): boolean {
|
|
|
523
524
|
experimental?.dynamicIO !== true &&
|
|
524
525
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
525
526
|
process.env.__NEXT_CACHE_COMPONENTS !== 'true'
|
|
526
|
-
)
|
|
527
|
+
)
|
|
527
528
|
}
|
|
528
529
|
|
|
529
530
|
/**
|
|
@@ -534,29 +535,29 @@ function useCacheOnlyIsEnabled(): boolean {
|
|
|
534
535
|
* after the final build-failed line.
|
|
535
536
|
*/
|
|
536
537
|
function useCacheSegmentConfigBuildError(source: string, key: string): string {
|
|
537
|
-
const lines = source.split('\n')
|
|
538
|
-
const exportRe = new RegExp(`\\bexport\\s+const\\s+${key}\\b`)
|
|
539
|
-
let line = 1
|
|
540
|
-
let column = 1
|
|
538
|
+
const lines = source.split('\n')
|
|
539
|
+
const exportRe = new RegExp(`\\bexport\\s+const\\s+${key}\\b`)
|
|
540
|
+
let line = 1
|
|
541
|
+
let column = 1
|
|
541
542
|
for (let i = 0; i < lines.length; i += 1) {
|
|
542
|
-
const match = new RegExp(`\\b${key}\\b`).exec(lines[i] ?? '')
|
|
543
|
+
const match = new RegExp(`\\b${key}\\b`).exec(lines[i] ?? '')
|
|
543
544
|
if (exportRe.test(lines[i] ?? '') && match) {
|
|
544
|
-
line = i + 1
|
|
545
|
-
column = match.index + 1
|
|
546
|
-
break
|
|
545
|
+
line = i + 1
|
|
546
|
+
column = match.index + 1
|
|
547
|
+
break
|
|
547
548
|
}
|
|
548
549
|
}
|
|
549
|
-
const last = Math.min(lines.length, line + 3)
|
|
550
|
-
const width = String(last).length
|
|
551
|
-
const gutter = ' '.repeat(width + 2)
|
|
552
|
-
const frame: string[] = [`${gutter},-[${line}:1]`]
|
|
550
|
+
const last = Math.min(lines.length, line + 3)
|
|
551
|
+
const width = String(last).length
|
|
552
|
+
const gutter = ' '.repeat(width + 2)
|
|
553
|
+
const frame: string[] = [`${gutter},-[${line}:1]`]
|
|
553
554
|
for (let n = line; n <= last; n += 1) {
|
|
554
|
-
frame.push(` ${String(n).padStart(width)} | ${lines[n - 1] ?? ''}`)
|
|
555
|
+
frame.push(` ${String(n).padStart(width)} | ${lines[n - 1] ?? ''}`)
|
|
555
556
|
if (n === line) {
|
|
556
|
-
frame.push(`${gutter}: ${' '.repeat(column - 1)}${'^'.repeat(key.length)}`)
|
|
557
|
+
frame.push(`${gutter}: ${' '.repeat(column - 1)}${'^'.repeat(key.length)}`)
|
|
557
558
|
}
|
|
558
559
|
}
|
|
559
|
-
frame.push(`${gutter}\`----`)
|
|
560
|
+
frame.push(`${gutter}\`----`)
|
|
560
561
|
return (
|
|
561
562
|
'Failed to compile.\n' +
|
|
562
563
|
'\n' +
|
|
@@ -569,54 +570,54 @@ function useCacheSegmentConfigBuildError(source: string, key: string): string {
|
|
|
569
570
|
'\n' +
|
|
570
571
|
'\n' +
|
|
571
572
|
'> Build failed because of webpack errors'
|
|
572
|
-
)
|
|
573
|
+
)
|
|
573
574
|
}
|
|
574
575
|
|
|
575
576
|
// A pnext-generated pages-router wrapper re-exports from the materializer's
|
|
576
577
|
// `source-pages` symlink; that import specifier is the stable marker separating
|
|
577
578
|
// synthetic wrappers from user-authored app segments.
|
|
578
579
|
function isMaterializedPagesWrapper(source: string): boolean {
|
|
579
|
-
return /\bfrom\s+['"][^'"]*source-pages[/\\]/.test(source)
|
|
580
|
+
return /\bfrom\s+['"][^'"]*source-pages[/\\]/.test(source)
|
|
580
581
|
}
|
|
581
582
|
|
|
582
583
|
function validateUseCacheDirectives(config: ResolvedConfig, routes: RouteManifestEntry[]): void {
|
|
583
|
-
const nextConfig = getNextConfig()
|
|
584
|
-
const experimental = nextConfig.experimental as Record<string, unknown> | undefined
|
|
584
|
+
const nextConfig = getNextConfig()
|
|
585
|
+
const experimental = nextConfig.experimental as Record<string, unknown> | undefined
|
|
585
586
|
const enabled =
|
|
586
587
|
nextConfig.cacheComponents === true ||
|
|
587
588
|
experimental?.cacheComponents === true ||
|
|
588
589
|
experimental?.useCache === true ||
|
|
589
590
|
experimental?.dynamicIO === true ||
|
|
590
591
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
591
|
-
process.env.__NEXT_CACHE_COMPONENTS === 'true'
|
|
592
|
-
const kinds = new Set<string>(['default', 'private', 'remote'])
|
|
593
|
-
const configured = nextConfig.cacheHandlers
|
|
592
|
+
process.env.__NEXT_CACHE_COMPONENTS === 'true'
|
|
593
|
+
const kinds = new Set<string>(['default', 'private', 'remote'])
|
|
594
|
+
const configured = nextConfig.cacheHandlers
|
|
594
595
|
if (configured && typeof configured === 'object' && !Array.isArray(configured)) {
|
|
595
|
-
for (const kind of Object.keys(configured)) kinds.add(kind)
|
|
596
|
+
for (const kind of Object.keys(configured)) kinds.add(kind)
|
|
596
597
|
}
|
|
597
598
|
|
|
598
|
-
const projectRoot = path.resolve(config.appPath, '..')
|
|
599
|
+
const projectRoot = path.resolve(config.appPath, '..')
|
|
599
600
|
// sourceFiles is the route's whole graph; shared modules repeat across every
|
|
600
601
|
// route, so visit each file once.
|
|
601
|
-
const seen = new Set<string>()
|
|
602
|
+
const seen = new Set<string>()
|
|
602
603
|
for (const route of routes) {
|
|
603
604
|
for (const file of route.sourceFiles) {
|
|
604
|
-
if (seen.has(file)) continue
|
|
605
|
-
seen.add(file)
|
|
606
|
-
if (!isProjectSourceFile(projectRoot, file)) continue
|
|
607
|
-
const source = readSource(file)
|
|
608
|
-
if (source === undefined) continue
|
|
605
|
+
if (seen.has(file)) continue
|
|
606
|
+
seen.add(file)
|
|
607
|
+
if (!isProjectSourceFile(projectRoot, file)) continue
|
|
608
|
+
const source = readSource(file)
|
|
609
|
+
if (source === undefined) continue
|
|
609
610
|
for (const directive of useCacheDirectives(source)) {
|
|
610
611
|
if (!enabled) {
|
|
611
612
|
throw new PnextBuildValidationError(
|
|
612
613
|
useCacheNotEnabledBuildError(`./app/${appDirRelativeFile(config, file)}`, source),
|
|
613
|
-
)
|
|
614
|
+
)
|
|
614
615
|
}
|
|
615
|
-
const kind = directive.kind
|
|
616
|
+
const kind = directive.kind
|
|
616
617
|
if (!kinds.has(kind)) {
|
|
617
618
|
throw new PnextBuildValidationError(
|
|
618
619
|
unknownCacheKindBuildError(`./app/${appDirRelativeFile(config, file)}`, source, kind),
|
|
619
|
-
)
|
|
620
|
+
)
|
|
620
621
|
}
|
|
621
622
|
}
|
|
622
623
|
}
|
|
@@ -624,24 +625,24 @@ function validateUseCacheDirectives(config: ResolvedConfig, routes: RouteManifes
|
|
|
624
625
|
}
|
|
625
626
|
|
|
626
627
|
function isProjectSourceFile(projectRoot: string, file: string): boolean {
|
|
627
|
-
const relative = path.relative(projectRoot, file)
|
|
628
|
+
const relative = path.relative(projectRoot, file)
|
|
628
629
|
return (
|
|
629
630
|
relative !== '' &&
|
|
630
631
|
!relative.startsWith('..') &&
|
|
631
632
|
!path.isAbsolute(relative) &&
|
|
632
633
|
!relative.split(path.sep).includes('node_modules')
|
|
633
|
-
)
|
|
634
|
+
)
|
|
634
635
|
}
|
|
635
636
|
|
|
636
637
|
export function useCacheDirectives(source: string): { kind: string }[] {
|
|
637
638
|
const directives = [
|
|
638
639
|
...directivePrologue(source, 0),
|
|
639
640
|
...functionBodies(source).flatMap(offset => directivePrologue(source, offset)),
|
|
640
|
-
]
|
|
641
|
+
]
|
|
641
642
|
return directives.flatMap(value => {
|
|
642
|
-
const match = /^use cache(?:\s*:\s*([\w-]+))?$/.exec(value)
|
|
643
|
-
return match ? [{ kind: match[1] ?? 'default' }] : []
|
|
644
|
-
})
|
|
643
|
+
const match = /^use cache(?:\s*:\s*([\w-]+))?$/.exec(value)
|
|
644
|
+
return match ? [{ kind: match[1] ?? 'default' }] : []
|
|
645
|
+
})
|
|
645
646
|
}
|
|
646
647
|
|
|
647
648
|
/**
|
|
@@ -656,28 +657,28 @@ export function useCacheDirectives(source: string): { kind: string }[] {
|
|
|
656
657
|
function useCacheScopedSource(
|
|
657
658
|
source: string,
|
|
658
659
|
): { kind: string; body: string; params: string[]; name: string }[] {
|
|
659
|
-
const kindOf = (value: string) => /^use cache(?:\s*:\s*([\w-]+))?$/.exec(value)
|
|
660
|
+
const kindOf = (value: string) => /^use cache(?:\s*:\s*([\w-]+))?$/.exec(value)
|
|
660
661
|
for (const directive of directivePrologue(source, 0)) {
|
|
661
662
|
// A module-level directive caches the whole file; there is no single cached
|
|
662
663
|
// function whose parameters could be legal vary keys, so `params` is empty.
|
|
663
|
-
const match = kindOf(directive)
|
|
664
|
-
if (match) return [{ kind: match[1] ?? 'default', body: source, params: [], name: '' }]
|
|
664
|
+
const match = kindOf(directive)
|
|
665
|
+
if (match) return [{ kind: match[1] ?? 'default', body: source, params: [], name: '' }]
|
|
665
666
|
}
|
|
666
|
-
const scopes: { kind: string; body: string; params: string[]; name: string }[] = []
|
|
667
|
+
const scopes: { kind: string; body: string; params: string[]; name: string }[] = []
|
|
667
668
|
for (const scope of functionScopes(source)) {
|
|
668
669
|
for (const directive of directivePrologue(source, scope.offset)) {
|
|
669
|
-
const match = kindOf(directive)
|
|
670
|
-
if (!match) continue
|
|
670
|
+
const match = kindOf(directive)
|
|
671
|
+
if (!match) continue
|
|
671
672
|
scopes.push({
|
|
672
673
|
kind: match[1] ?? 'default',
|
|
673
674
|
body: source.slice(scope.offset, functionBodyEnd(source, scope.offset)),
|
|
674
675
|
params: scope.params,
|
|
675
676
|
name: scope.name,
|
|
676
|
-
})
|
|
677
|
-
break
|
|
677
|
+
})
|
|
678
|
+
break
|
|
678
679
|
}
|
|
679
680
|
}
|
|
680
|
-
return scopes
|
|
681
|
+
return scopes
|
|
681
682
|
}
|
|
682
683
|
|
|
683
684
|
/**
|
|
@@ -693,22 +694,22 @@ function passesUncachedPromiseInto(
|
|
|
693
694
|
resolvableNames: ReadonlySet<string>,
|
|
694
695
|
): boolean {
|
|
695
696
|
const isUncached = (expression: string) =>
|
|
696
|
-
isUncachedPromiseExpression(expression, resolvableNames)
|
|
697
|
-
const pattern = new RegExp(`(<)?\\b${escapeRegExp(name)}\\b`, 'g')
|
|
697
|
+
isUncachedPromiseExpression(expression, resolvableNames)
|
|
698
|
+
const pattern = new RegExp(`(<)?\\b${escapeRegExp(name)}\\b`, 'g')
|
|
698
699
|
for (const match of source.matchAll(pattern)) {
|
|
699
|
-
const index = match.index
|
|
700
|
-
if (index === undefined || !isCodeOffset(source, index)) continue
|
|
701
|
-
const after = index + match[0].length
|
|
700
|
+
const index = match.index
|
|
701
|
+
if (index === undefined || !isCodeOffset(source, index)) continue
|
|
702
|
+
const after = index + match[0].length
|
|
702
703
|
if (match[1] === '<') {
|
|
703
|
-
if (jsxPropsPassUncachedPromise(source, after, isUncached)) return true
|
|
704
|
-
continue
|
|
704
|
+
if (jsxPropsPassUncachedPromise(source, after, isUncached)) return true
|
|
705
|
+
continue
|
|
705
706
|
}
|
|
706
|
-
const open = source.slice(after).search(/\S/)
|
|
707
|
-
if (open === -1 || source[after + open] !== '(') continue
|
|
708
|
-
const args = balancedFrom(source, after + open, '(', ')')
|
|
709
|
-
if (args !== undefined && splitTopLevelCommas(args).some(isUncached)) return true
|
|
707
|
+
const open = source.slice(after).search(/\S/)
|
|
708
|
+
if (open === -1 || source[after + open] !== '(') continue
|
|
709
|
+
const args = balancedFrom(source, after + open, '(', ')')
|
|
710
|
+
if (args !== undefined && splitTopLevelCommas(args).some(isUncached)) return true
|
|
710
711
|
}
|
|
711
|
-
return false
|
|
712
|
+
return false
|
|
712
713
|
}
|
|
713
714
|
|
|
714
715
|
/** Scan a JSX element's attributes (from just past `<Name`) for `prop={call()}`. */
|
|
@@ -718,15 +719,15 @@ function jsxPropsPassUncachedPromise(
|
|
|
718
719
|
isUncached: (expression: string) => boolean,
|
|
719
720
|
): boolean {
|
|
720
721
|
for (let cursor = offset; cursor < source.length; cursor++) {
|
|
721
|
-
const current = source[cursor]
|
|
722
|
-
if (current === '>') return false
|
|
723
|
-
if (current !== '{') continue
|
|
724
|
-
const expression = balancedFrom(source, cursor, '{', '}')
|
|
725
|
-
if (expression === undefined) return false
|
|
726
|
-
if (isUncached(expression)) return true
|
|
727
|
-
cursor += expression.length + 1
|
|
722
|
+
const current = source[cursor]!
|
|
723
|
+
if (current === '>') return false
|
|
724
|
+
if (current !== '{') continue
|
|
725
|
+
const expression = balancedFrom(source, cursor, '{', '}')
|
|
726
|
+
if (expression === undefined) return false
|
|
727
|
+
if (isUncached(expression)) return true
|
|
728
|
+
cursor += expression.length + 1
|
|
728
729
|
}
|
|
729
|
-
return false
|
|
730
|
+
return false
|
|
730
731
|
}
|
|
731
732
|
|
|
732
733
|
/** The text between `open` at `index` and its matching `close`. */
|
|
@@ -736,23 +737,23 @@ function balancedFrom(
|
|
|
736
737
|
open: string,
|
|
737
738
|
close: string,
|
|
738
739
|
): string | undefined {
|
|
739
|
-
let depth = 0
|
|
740
|
-
let quote: string | undefined
|
|
740
|
+
let depth = 0
|
|
741
|
+
let quote: string | undefined
|
|
741
742
|
for (let cursor = index; cursor < source.length; cursor++) {
|
|
742
|
-
const current = source[cursor]
|
|
743
|
+
const current = source[cursor]!
|
|
743
744
|
if (quote) {
|
|
744
|
-
if (current === '\\') cursor
|
|
745
|
-
else if (current === quote) quote = undefined
|
|
746
|
-
continue
|
|
745
|
+
if (current === '\\') cursor++
|
|
746
|
+
else if (current === quote) quote = undefined
|
|
747
|
+
continue
|
|
747
748
|
}
|
|
748
749
|
if (current === '"' || current === "'" || current === '`') {
|
|
749
|
-
quote = current
|
|
750
|
-
continue
|
|
750
|
+
quote = current
|
|
751
|
+
continue
|
|
751
752
|
}
|
|
752
|
-
if (current === open) depth
|
|
753
|
-
else if (current === close && --depth === 0) return source.slice(index + 1, cursor)
|
|
753
|
+
if (current === open) depth++
|
|
754
|
+
else if (current === close && --depth === 0) return source.slice(index + 1, cursor)
|
|
754
755
|
}
|
|
755
|
-
return undefined
|
|
756
|
+
return undefined
|
|
756
757
|
}
|
|
757
758
|
|
|
758
759
|
/** `foo()` / `a.b(…)` — an unawaited call whose promise crosses a cache boundary. */
|
|
@@ -760,10 +761,10 @@ function isUncachedPromiseExpression(
|
|
|
760
761
|
expression: string,
|
|
761
762
|
resolvableNames: ReadonlySet<string>,
|
|
762
763
|
): boolean {
|
|
763
|
-
const trimmed = expression.trim()
|
|
764
|
-
if (trimmed === '' || /^await\b/.test(trimmed)) return false
|
|
765
|
-
const call = /^([A-Za-z_$][\w$.]*)\s*\(/.exec(trimmed)
|
|
766
|
-
return call !== null && !resolvableNames.has(call[1]!)
|
|
764
|
+
const trimmed = expression.trim()
|
|
765
|
+
if (trimmed === '' || /^await\b/.test(trimmed)) return false
|
|
766
|
+
const call = /^([A-Za-z_$][\w$.]*)\s*\(/.exec(trimmed)
|
|
767
|
+
return call !== null && !resolvableNames.has(call[1]!)
|
|
767
768
|
}
|
|
768
769
|
|
|
769
770
|
/**
|
|
@@ -774,21 +775,21 @@ function isUncachedPromiseExpression(
|
|
|
774
775
|
* actual imports, so an app-local helper that happens to be named `cookies()` still counts as uncached IO.
|
|
775
776
|
*/
|
|
776
777
|
function requestApiNames(source: string): Set<string> {
|
|
777
|
-
const names = new Set<string>()
|
|
778
|
+
const names = new Set<string>()
|
|
778
779
|
const imports = source.matchAll(
|
|
779
780
|
/\bimport\s*\{([^}]*)\}\s*from\s*['"]next\/(?:headers|root-params)['"]/g,
|
|
780
|
-
)
|
|
781
|
+
)
|
|
781
782
|
for (const match of imports) {
|
|
782
783
|
for (const clause of match[1]!.split(',')) {
|
|
783
|
-
const local = /([A-Za-z_$][\w$]*)\s*$/.exec(clause.trim())
|
|
784
|
-
if (local) names.add(local[1]!)
|
|
784
|
+
const local = /([A-Za-z_$][\w$]*)\s*$/.exec(clause.trim())
|
|
785
|
+
if (local) names.add(local[1]!)
|
|
785
786
|
}
|
|
786
787
|
}
|
|
787
|
-
return names
|
|
788
|
+
return names
|
|
788
789
|
}
|
|
789
790
|
|
|
790
791
|
function escapeRegExp(value: string): string {
|
|
791
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
792
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
792
793
|
}
|
|
793
794
|
|
|
794
795
|
/**
|
|
@@ -800,8 +801,8 @@ function readsRequestDataOutsideCache(source: string): boolean {
|
|
|
800
801
|
const outside = useCacheScopedSource(source).reduce(
|
|
801
802
|
(rest, scope) => rest.replace(scope.body, ''),
|
|
802
803
|
source,
|
|
803
|
-
)
|
|
804
|
-
return /\b(?:connection|cookies|headers|draftMode)\s*\(\s*\)/.test(outside)
|
|
804
|
+
)
|
|
805
|
+
return /\b(?:connection|cookies|headers|draftMode)\s*\(\s*\)/.test(outside)
|
|
805
806
|
}
|
|
806
807
|
|
|
807
808
|
/**
|
|
@@ -810,39 +811,39 @@ function readsRequestDataOutsideCache(source: string): boolean {
|
|
|
810
811
|
* literals and comments; an unbalanced body yields the end of the source.
|
|
811
812
|
*/
|
|
812
813
|
function functionBodyEnd(source: string, offset: number): number {
|
|
813
|
-
let depth = 1
|
|
814
|
-
let quote: string | undefined
|
|
814
|
+
let depth = 1
|
|
815
|
+
let quote: string | undefined
|
|
815
816
|
for (let cursor = offset; cursor < source.length; cursor++) {
|
|
816
|
-
const current = source[cursor]
|
|
817
|
+
const current = source[cursor]!
|
|
817
818
|
if (quote) {
|
|
818
819
|
// Template literals are treated as opaque text (interpolations included):
|
|
819
820
|
// their braces are skipped rather than counted. Good enough for the
|
|
820
821
|
// heuristic; a nested template inside an interpolation may end the skip
|
|
821
822
|
// early, which at worst widens the scanned scope.
|
|
822
|
-
if (current === '\\') cursor
|
|
823
|
-
else if (current === quote) quote = undefined
|
|
824
|
-
continue
|
|
823
|
+
if (current === '\\') cursor++
|
|
824
|
+
else if (current === quote) quote = undefined
|
|
825
|
+
continue
|
|
825
826
|
}
|
|
826
827
|
if (current === '/' && source[cursor + 1] === '/') {
|
|
827
|
-
const newline = source.indexOf('\n', cursor + 2)
|
|
828
|
-
if (newline === -1) return source.length
|
|
829
|
-
cursor = newline
|
|
830
|
-
continue
|
|
828
|
+
const newline = source.indexOf('\n', cursor + 2)
|
|
829
|
+
if (newline === -1) return source.length
|
|
830
|
+
cursor = newline
|
|
831
|
+
continue
|
|
831
832
|
}
|
|
832
833
|
if (current === '/' && source[cursor + 1] === '*') {
|
|
833
|
-
const end = source.indexOf('*/', cursor + 2)
|
|
834
|
-
if (end === -1) return source.length
|
|
835
|
-
cursor = end + 1
|
|
836
|
-
continue
|
|
834
|
+
const end = source.indexOf('*/', cursor + 2)
|
|
835
|
+
if (end === -1) return source.length
|
|
836
|
+
cursor = end + 1
|
|
837
|
+
continue
|
|
837
838
|
}
|
|
838
839
|
if (current === '"' || current === "'" || current === '`') {
|
|
839
|
-
quote = current
|
|
840
|
-
continue
|
|
840
|
+
quote = current
|
|
841
|
+
continue
|
|
841
842
|
}
|
|
842
|
-
if (current === '{') depth
|
|
843
|
-
else if (current === '}' && --depth === 0) return cursor
|
|
843
|
+
if (current === '{') depth++
|
|
844
|
+
else if (current === '}' && --depth === 0) return cursor
|
|
844
845
|
}
|
|
845
|
-
return source.length
|
|
846
|
+
return source.length
|
|
846
847
|
}
|
|
847
848
|
|
|
848
849
|
/**
|
|
@@ -860,7 +861,7 @@ function useCacheNotEnabledBuildError(displayPath: string, source: string): stri
|
|
|
860
861
|
'To use "use cache", please enable the feature flag `cacheComponents` in your Next.js config.\n' +
|
|
861
862
|
' |\n' +
|
|
862
863
|
' | Read more: https://nextjs.org/docs/app/api-reference/directives/use-cache#usage',
|
|
863
|
-
)
|
|
864
|
+
)
|
|
864
865
|
}
|
|
865
866
|
|
|
866
867
|
/**
|
|
@@ -872,22 +873,18 @@ function unknownCacheKindBuildError(displayPath: string, source: string, kind: s
|
|
|
872
873
|
displayPath,
|
|
873
874
|
source,
|
|
874
875
|
`Unknown cache kind "${kind}". Please configure a cache handler for this kind in the \`cacheHandlers\` object in your Next.js config.`,
|
|
875
|
-
)
|
|
876
|
+
)
|
|
876
877
|
}
|
|
877
878
|
|
|
878
879
|
/** The shared `Failed to compile` block: message, swc code frame, import trace. */
|
|
879
|
-
function useCacheDirectiveBuildError(
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
const
|
|
885
|
-
const
|
|
886
|
-
const
|
|
887
|
-
const before = source.slice(0, index);
|
|
888
|
-
const line = before.split('\n').length;
|
|
889
|
-
const column = index - before.lastIndexOf('\n');
|
|
890
|
-
const frame = swcCodeFrame(source, line, column, span);
|
|
880
|
+
function useCacheDirectiveBuildError(displayPath: string, source: string, message: string): string {
|
|
881
|
+
const match = /(['"])use cache(?:\s*:\s*[\w-]+)?\1/.exec(source)
|
|
882
|
+
const index = match?.index ?? 0
|
|
883
|
+
const span = match?.[0].length ?? 11
|
|
884
|
+
const before = source.slice(0, index)
|
|
885
|
+
const line = before.split('\n').length
|
|
886
|
+
const column = index - before.lastIndexOf('\n')
|
|
887
|
+
const frame = swcCodeFrame(source, line, column, span)
|
|
891
888
|
return (
|
|
892
889
|
'Failed to compile.\n' +
|
|
893
890
|
'\n' +
|
|
@@ -901,7 +898,7 @@ function useCacheDirectiveBuildError(
|
|
|
901
898
|
'\n' +
|
|
902
899
|
'\n' +
|
|
903
900
|
'> Build failed because of webpack errors'
|
|
904
|
-
)
|
|
901
|
+
)
|
|
905
902
|
}
|
|
906
903
|
|
|
907
904
|
/**
|
|
@@ -910,47 +907,47 @@ function useCacheDirectiveBuildError(
|
|
|
910
907
|
* under the span, and a backtick footer.
|
|
911
908
|
*/
|
|
912
909
|
function swcCodeFrame(source: string, line: number, column: number, span: number): string {
|
|
913
|
-
const lines = source.split('\n')
|
|
914
|
-
const last = Math.min(lines.length, line + 3)
|
|
915
|
-
const width = String(last).length
|
|
916
|
-
const gutter = ' '.repeat(width + 2)
|
|
917
|
-
const out: string[] = [`${gutter},-[${line}:${column}]`]
|
|
910
|
+
const lines = source.split('\n')
|
|
911
|
+
const last = Math.min(lines.length, line + 3)
|
|
912
|
+
const width = String(last).length
|
|
913
|
+
const gutter = ' '.repeat(width + 2)
|
|
914
|
+
const out: string[] = [`${gutter},-[${line}:${column}]`]
|
|
918
915
|
for (let n = line; n <= last; n += 1) {
|
|
919
|
-
out.push(` ${String(n).padStart(width)} | ${lines[n - 1] ?? ''}`)
|
|
916
|
+
out.push(` ${String(n).padStart(width)} | ${lines[n - 1] ?? ''}`)
|
|
920
917
|
if (n === line) {
|
|
921
|
-
out.push(`${gutter}: ${' '.repeat(column - 1)}${'^'.repeat(span)}`)
|
|
918
|
+
out.push(`${gutter}: ${' '.repeat(column - 1)}${'^'.repeat(span)}`)
|
|
922
919
|
}
|
|
923
920
|
}
|
|
924
|
-
out.push(`${gutter}\`----`)
|
|
925
|
-
return out.join('\n')
|
|
921
|
+
out.push(`${gutter}\`----`)
|
|
922
|
+
return out.join('\n')
|
|
926
923
|
}
|
|
927
924
|
|
|
928
925
|
function directivePrologue(source: string, offset: number): string[] {
|
|
929
|
-
const directives: string[] = []
|
|
930
|
-
let cursor = offset
|
|
926
|
+
const directives: string[] = []
|
|
927
|
+
let cursor = offset
|
|
931
928
|
while (true) {
|
|
932
|
-
cursor = skipTrivia(source, cursor)
|
|
933
|
-
const quote = source[cursor]
|
|
934
|
-
if (quote !== '"' && quote !== "'") return directives
|
|
935
|
-
const end = stringEnd(source, cursor, quote)
|
|
936
|
-
if (end === -1) return directives
|
|
937
|
-
const value = source.slice(cursor + 1, end)
|
|
938
|
-
const afterString = end + 1
|
|
939
|
-
cursor = skipTrivia(source, afterString)
|
|
940
|
-
if (source[cursor] === ';') cursor
|
|
929
|
+
cursor = skipTrivia(source, cursor)
|
|
930
|
+
const quote = source[cursor]
|
|
931
|
+
if (quote !== '"' && quote !== "'") return directives
|
|
932
|
+
const end = stringEnd(source, cursor, quote)
|
|
933
|
+
if (end === -1) return directives
|
|
934
|
+
const value = source.slice(cursor + 1, end)
|
|
935
|
+
const afterString = end + 1
|
|
936
|
+
cursor = skipTrivia(source, afterString)
|
|
937
|
+
if (source[cursor] === ';') cursor++
|
|
941
938
|
else if (
|
|
942
939
|
cursor < source.length &&
|
|
943
940
|
source[cursor] !== '}' &&
|
|
944
941
|
!/[\r\n]/.test(source.slice(afterString, cursor))
|
|
945
942
|
) {
|
|
946
|
-
return directives
|
|
943
|
+
return directives
|
|
947
944
|
}
|
|
948
|
-
directives.push(value)
|
|
945
|
+
directives.push(value)
|
|
949
946
|
}
|
|
950
947
|
}
|
|
951
948
|
|
|
952
949
|
function functionBodies(source: string): number[] {
|
|
953
|
-
return functionScopes(source).map(scope => scope.offset)
|
|
950
|
+
return functionScopes(source).map(scope => scope.offset)
|
|
954
951
|
}
|
|
955
952
|
|
|
956
953
|
/**
|
|
@@ -961,19 +958,19 @@ function functionBodies(source: string): number[] {
|
|
|
961
958
|
* there - from closed-over request data, which is still flagged.
|
|
962
959
|
*/
|
|
963
960
|
function functionScopes(source: string): { offset: number; params: string[]; name: string }[] {
|
|
964
|
-
const scopes: { offset: number; params: string[]; name: string }[] = []
|
|
961
|
+
const scopes: { offset: number; params: string[]; name: string }[] = []
|
|
965
962
|
const pattern =
|
|
966
|
-
/\b(?:async\s+)?function(?:\s*\*)?(?:\s+[A-Za-z_$][\w$]*)?\s*(?:<[^>{}]*>)?\s*\([^)]*\)\s*(?::\s*[^={]+)?\{|\b(?:async\s+)?(?:[A-Za-z_$][\w$]*|\([^)]*\))\s*=>\s*\{/g
|
|
963
|
+
/\b(?:async\s+)?function(?:\s*\*)?(?:\s+[A-Za-z_$][\w$]*)?\s*(?:<[^>{}]*>)?\s*\([^)]*\)\s*(?::\s*[^={]+)?\{|\b(?:async\s+)?(?:[A-Za-z_$][\w$]*|\([^)]*\))\s*=>\s*\{/g
|
|
967
964
|
for (const match of source.matchAll(pattern)) {
|
|
968
965
|
if (match.index !== undefined && isCodeOffset(source, match.index)) {
|
|
969
966
|
scopes.push({
|
|
970
967
|
offset: match.index + match[0].length,
|
|
971
968
|
params: signatureParams(match[0]),
|
|
972
969
|
name: signatureName(source, match.index, match[0]),
|
|
973
|
-
})
|
|
970
|
+
})
|
|
974
971
|
}
|
|
975
972
|
}
|
|
976
|
-
return scopes
|
|
973
|
+
return scopes
|
|
977
974
|
}
|
|
978
975
|
|
|
979
976
|
/**
|
|
@@ -982,11 +979,11 @@ function functionScopes(source: string): { offset: number; params: string[]; nam
|
|
|
982
979
|
* a cached function's CALL SITES.
|
|
983
980
|
*/
|
|
984
981
|
function signatureName(source: string, index: number, signature: string): string {
|
|
985
|
-
const declared = /\bfunction(?:\s*\*)?\s+([A-Za-z_$][\w$]*)/.exec(signature)
|
|
986
|
-
if (declared) return declared[1]
|
|
987
|
-
const before = source.slice(Math.max(0, index - 200), index)
|
|
988
|
-
const assigned = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=;]*)?=\s*$/.exec(before)
|
|
989
|
-
return assigned?.[1] ?? ''
|
|
982
|
+
const declared = /\bfunction(?:\s*\*)?\s+([A-Za-z_$][\w$]*)/.exec(signature)
|
|
983
|
+
if (declared) return declared[1]!
|
|
984
|
+
const before = source.slice(Math.max(0, index - 200), index)
|
|
985
|
+
const assigned = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=;]*)?=\s*$/.exec(before)
|
|
986
|
+
return assigned?.[1] ?? ''
|
|
990
987
|
}
|
|
991
988
|
|
|
992
989
|
/**
|
|
@@ -997,131 +994,135 @@ function signatureName(source: string, index: number, signature: string): string
|
|
|
997
994
|
*/
|
|
998
995
|
export function signatureParams(signature: string): string[] {
|
|
999
996
|
// Arrow with a single unparenthesized parameter: `arg => {`.
|
|
1000
|
-
const bareArrow = /(?:^|[^\w$.])([A-Za-z_$][\w$]*)\s*=>\s*\{\s*$/.exec(signature)
|
|
1001
|
-
if (bareArrow && !signature.includes('(')) return [bareArrow[1]!]
|
|
1002
|
-
const open = signature.indexOf('(')
|
|
1003
|
-
if (open === -1) return []
|
|
997
|
+
const bareArrow = /(?:^|[^\w$.])([A-Za-z_$][\w$]*)\s*=>\s*\{\s*$/.exec(signature)
|
|
998
|
+
if (bareArrow && !signature.includes('(')) return [bareArrow[1]!]
|
|
999
|
+
const open = signature.indexOf('(')
|
|
1000
|
+
if (open === -1) return []
|
|
1004
1001
|
// The signatures `functionScopes` matches use `\([^)]*\)` for the parameter
|
|
1005
1002
|
// list, so the list never contains a nested `)` — the first `)` closes it.
|
|
1006
|
-
const close = signature.indexOf(')', open)
|
|
1007
|
-
if (close === -1) return []
|
|
1008
|
-
return paramBindingNames(signature.slice(open + 1, close))
|
|
1003
|
+
const close = signature.indexOf(')', open)
|
|
1004
|
+
if (close === -1) return []
|
|
1005
|
+
return paramBindingNames(signature.slice(open + 1, close))
|
|
1009
1006
|
}
|
|
1010
1007
|
|
|
1011
1008
|
/** Bound identifiers of a comma-separated parameter list. */
|
|
1012
1009
|
function paramBindingNames(paramList: string): string[] {
|
|
1013
|
-
const names: string[] = []
|
|
1010
|
+
const names: string[] = []
|
|
1014
1011
|
for (const raw of splitTopLevelCommas(paramList)) {
|
|
1015
|
-
const part = stripDefaultValue(raw)
|
|
1016
|
-
|
|
1012
|
+
const part = stripDefaultValue(raw)
|
|
1013
|
+
.trim()
|
|
1014
|
+
.replace(/^\.\.\.\s*/, '')
|
|
1015
|
+
if (!part) continue
|
|
1017
1016
|
if (part.startsWith('{') || part.startsWith('[')) {
|
|
1018
|
-
destructureBindings(firstBalancedGroup(part), names)
|
|
1017
|
+
destructureBindings(firstBalancedGroup(part), names)
|
|
1019
1018
|
} else {
|
|
1020
1019
|
// `name: Type` / `name?: Type` — the binding is the leading identifier.
|
|
1021
|
-
const ident = /^([A-Za-z_$][\w$]*)/.exec(part)
|
|
1022
|
-
if (ident) names.push(ident[1]!)
|
|
1020
|
+
const ident = /^([A-Za-z_$][\w$]*)/.exec(part)
|
|
1021
|
+
if (ident) names.push(ident[1]!)
|
|
1023
1022
|
}
|
|
1024
1023
|
}
|
|
1025
|
-
return names
|
|
1024
|
+
return names
|
|
1026
1025
|
}
|
|
1027
1026
|
|
|
1028
1027
|
/** Local bindings introduced by a destructuring pattern (`{…}` or `[…]`). */
|
|
1029
1028
|
function destructureBindings(pattern: string, names: string[]): void {
|
|
1030
|
-
const inner = pattern.slice(1, -1)
|
|
1029
|
+
const inner = pattern.slice(1, -1)
|
|
1031
1030
|
for (const raw of splitTopLevelCommas(inner)) {
|
|
1032
|
-
const entry = stripDefaultValue(raw)
|
|
1033
|
-
|
|
1031
|
+
const entry = stripDefaultValue(raw)
|
|
1032
|
+
.trim()
|
|
1033
|
+
.replace(/^\.\.\.\s*/, '')
|
|
1034
|
+
if (!entry) continue
|
|
1034
1035
|
// In a destructure entry a top-level `:` renames (`key: binding`); the local
|
|
1035
1036
|
// binding is on the RIGHT — the opposite of a parameter's `name: Type`.
|
|
1036
|
-
const colon = topLevelColonIndex(entry)
|
|
1037
|
-
const binding = (colon === -1 ? entry : entry.slice(colon + 1)).trim()
|
|
1037
|
+
const colon = topLevelColonIndex(entry)
|
|
1038
|
+
const binding = (colon === -1 ? entry : entry.slice(colon + 1)).trim()
|
|
1038
1039
|
if (binding.startsWith('{') || binding.startsWith('[')) {
|
|
1039
|
-
destructureBindings(firstBalancedGroup(binding), names)
|
|
1040
|
+
destructureBindings(firstBalancedGroup(binding), names)
|
|
1040
1041
|
} else {
|
|
1041
|
-
const ident = /^([A-Za-z_$][\w$]*)/.exec(binding)
|
|
1042
|
-
if (ident) names.push(ident[1]!)
|
|
1042
|
+
const ident = /^([A-Za-z_$][\w$]*)/.exec(binding)
|
|
1043
|
+
if (ident) names.push(ident[1]!)
|
|
1043
1044
|
}
|
|
1044
1045
|
}
|
|
1045
1046
|
}
|
|
1046
1047
|
|
|
1047
1048
|
/** Split `text` on commas that sit at bracket depth 0 (strings skipped). */
|
|
1048
1049
|
function splitTopLevelCommas(text: string): string[] {
|
|
1049
|
-
const parts: string[] = []
|
|
1050
|
-
let depth = 0
|
|
1051
|
-
let start = 0
|
|
1052
|
-
let quote: string | undefined
|
|
1050
|
+
const parts: string[] = []
|
|
1051
|
+
let depth = 0
|
|
1052
|
+
let start = 0
|
|
1053
|
+
let quote: string | undefined
|
|
1053
1054
|
for (let i = 0; i < text.length; i++) {
|
|
1054
|
-
const current = text[i]
|
|
1055
|
+
const current = text[i]!
|
|
1055
1056
|
if (quote) {
|
|
1056
|
-
if (current === '\\') i
|
|
1057
|
-
else if (current === quote) quote = undefined
|
|
1058
|
-
continue
|
|
1057
|
+
if (current === '\\') i++
|
|
1058
|
+
else if (current === quote) quote = undefined
|
|
1059
|
+
continue
|
|
1059
1060
|
}
|
|
1060
|
-
if (current === '"' || current === "'" || current === '`') quote = current
|
|
1061
|
-
else if (current === '{' || current === '[' || current === '(' || current === '<') depth
|
|
1062
|
-
else if (current === '}' || current === ']' || current === ')' || current === '>') depth
|
|
1061
|
+
if (current === '"' || current === "'" || current === '`') quote = current
|
|
1062
|
+
else if (current === '{' || current === '[' || current === '(' || current === '<') depth++
|
|
1063
|
+
else if (current === '}' || current === ']' || current === ')' || current === '>') depth--
|
|
1063
1064
|
else if (current === ',' && depth === 0) {
|
|
1064
|
-
parts.push(text.slice(start, i))
|
|
1065
|
-
start = i + 1
|
|
1065
|
+
parts.push(text.slice(start, i))
|
|
1066
|
+
start = i + 1
|
|
1066
1067
|
}
|
|
1067
1068
|
}
|
|
1068
|
-
parts.push(text.slice(start))
|
|
1069
|
-
return parts
|
|
1069
|
+
parts.push(text.slice(start))
|
|
1070
|
+
return parts
|
|
1070
1071
|
}
|
|
1071
1072
|
|
|
1072
1073
|
/** The first balanced `{…}`/`[…]` group in `text` (which starts with it). */
|
|
1073
1074
|
function firstBalancedGroup(text: string): string {
|
|
1074
|
-
const open = text[0]
|
|
1075
|
-
const close = open === '{' ? '}' : ']'
|
|
1076
|
-
let depth = 0
|
|
1077
|
-
let quote: string | undefined
|
|
1075
|
+
const open = text[0]
|
|
1076
|
+
const close = open === '{' ? '}' : ']'
|
|
1077
|
+
let depth = 0
|
|
1078
|
+
let quote: string | undefined
|
|
1078
1079
|
for (let i = 0; i < text.length; i++) {
|
|
1079
|
-
const current = text[i]
|
|
1080
|
+
const current = text[i]!
|
|
1080
1081
|
if (quote) {
|
|
1081
|
-
if (current === '\\') i
|
|
1082
|
-
else if (current === quote) quote = undefined
|
|
1083
|
-
continue
|
|
1082
|
+
if (current === '\\') i++
|
|
1083
|
+
else if (current === quote) quote = undefined
|
|
1084
|
+
continue
|
|
1084
1085
|
}
|
|
1085
|
-
if (current === '"' || current === "'" || current === '`') quote = current
|
|
1086
|
-
else if (current === open) depth
|
|
1087
|
-
else if (current === close && --depth === 0) return text.slice(0, i + 1)
|
|
1086
|
+
if (current === '"' || current === "'" || current === '`') quote = current
|
|
1087
|
+
else if (current === open) depth++
|
|
1088
|
+
else if (current === close && --depth === 0) return text.slice(0, i + 1)
|
|
1088
1089
|
}
|
|
1089
|
-
return text
|
|
1090
|
+
return text
|
|
1090
1091
|
}
|
|
1091
1092
|
|
|
1092
1093
|
/** Index of the first `:` at bracket depth 0, or -1. */
|
|
1093
1094
|
function topLevelColonIndex(text: string): number {
|
|
1094
|
-
let depth = 0
|
|
1095
|
-
let quote: string | undefined
|
|
1095
|
+
let depth = 0
|
|
1096
|
+
let quote: string | undefined
|
|
1096
1097
|
for (let i = 0; i < text.length; i++) {
|
|
1097
|
-
const current = text[i]
|
|
1098
|
+
const current = text[i]!
|
|
1098
1099
|
if (quote) {
|
|
1099
|
-
if (current === '\\') i
|
|
1100
|
-
else if (current === quote) quote = undefined
|
|
1101
|
-
continue
|
|
1100
|
+
if (current === '\\') i++
|
|
1101
|
+
else if (current === quote) quote = undefined
|
|
1102
|
+
continue
|
|
1102
1103
|
}
|
|
1103
|
-
if (current === '"' || current === "'" || current === '`') quote = current
|
|
1104
|
-
else if (current === '{' || current === '[' || current === '(' || current === '<') depth
|
|
1105
|
-
else if (current === '}' || current === ']' || current === ')' || current === '>') depth
|
|
1106
|
-
else if (current === ':' && depth === 0) return i
|
|
1104
|
+
if (current === '"' || current === "'" || current === '`') quote = current
|
|
1105
|
+
else if (current === '{' || current === '[' || current === '(' || current === '<') depth++
|
|
1106
|
+
else if (current === '}' || current === ']' || current === ')' || current === '>') depth--
|
|
1107
|
+
else if (current === ':' && depth === 0) return i
|
|
1107
1108
|
}
|
|
1108
|
-
return -1
|
|
1109
|
+
return -1
|
|
1109
1110
|
}
|
|
1110
1111
|
|
|
1111
1112
|
/** Drop a top-level `= default` suffix from a parameter/binding entry. */
|
|
1112
1113
|
function stripDefaultValue(entry: string): string {
|
|
1113
|
-
let depth = 0
|
|
1114
|
-
let quote: string | undefined
|
|
1114
|
+
let depth = 0
|
|
1115
|
+
let quote: string | undefined
|
|
1115
1116
|
for (let i = 0; i < entry.length; i++) {
|
|
1116
|
-
const current = entry[i]
|
|
1117
|
+
const current = entry[i]!
|
|
1117
1118
|
if (quote) {
|
|
1118
|
-
if (current === '\\') i
|
|
1119
|
-
else if (current === quote) quote = undefined
|
|
1120
|
-
continue
|
|
1119
|
+
if (current === '\\') i++
|
|
1120
|
+
else if (current === quote) quote = undefined
|
|
1121
|
+
continue
|
|
1121
1122
|
}
|
|
1122
|
-
if (current === '"' || current === "'" || current === '`') quote = current
|
|
1123
|
-
else if (current === '{' || current === '[' || current === '(' || current === '<') depth
|
|
1124
|
-
else if (current === '}' || current === ']' || current === ')' || current === '>') depth
|
|
1123
|
+
if (current === '"' || current === "'" || current === '`') quote = current
|
|
1124
|
+
else if (current === '{' || current === '[' || current === '(' || current === '<') depth++
|
|
1125
|
+
else if (current === '}' || current === ']' || current === ')' || current === '>') depth--
|
|
1125
1126
|
else if (
|
|
1126
1127
|
current === '=' &&
|
|
1127
1128
|
depth === 0 &&
|
|
@@ -1132,84 +1133,84 @@ function stripDefaultValue(entry: string): string {
|
|
|
1132
1133
|
entry[i - 1] !== '<' &&
|
|
1133
1134
|
entry[i - 1] !== '>'
|
|
1134
1135
|
) {
|
|
1135
|
-
return entry.slice(0, i)
|
|
1136
|
+
return entry.slice(0, i)
|
|
1136
1137
|
}
|
|
1137
1138
|
}
|
|
1138
|
-
return entry
|
|
1139
|
+
return entry
|
|
1139
1140
|
}
|
|
1140
1141
|
|
|
1141
1142
|
/** Identifiers directly awaited in `body` (the `X` of every `await X`). */
|
|
1142
1143
|
function awaitedIdentifiers(body: string): string[] {
|
|
1143
|
-
return [...body.matchAll(/\bawait\s+([A-Za-z_$][\w$]*)\b/g)].map(match => match[1]!)
|
|
1144
|
+
return [...body.matchAll(/\bawait\s+([A-Za-z_$][\w$]*)\b/g)].map(match => match[1]!)
|
|
1144
1145
|
}
|
|
1145
1146
|
|
|
1146
1147
|
function isCodeOffset(source: string, target: number): boolean {
|
|
1147
|
-
let quote: string | undefined
|
|
1148
|
+
let quote: string | undefined
|
|
1148
1149
|
for (let cursor = 0; cursor < target; cursor++) {
|
|
1149
|
-
const current = source[cursor]
|
|
1150
|
+
const current = source[cursor]!
|
|
1150
1151
|
if (quote) {
|
|
1151
|
-
if (current === '\\') cursor
|
|
1152
|
-
else if (current === quote) quote = undefined
|
|
1153
|
-
continue
|
|
1152
|
+
if (current === '\\') cursor++
|
|
1153
|
+
else if (current === quote) quote = undefined
|
|
1154
|
+
continue
|
|
1154
1155
|
}
|
|
1155
1156
|
if (current === '/' && source[cursor + 1] === '/') {
|
|
1156
|
-
const newline = source.indexOf('\n', cursor + 2)
|
|
1157
|
-
if (newline === -1 || newline >= target) return false
|
|
1158
|
-
cursor = newline
|
|
1159
|
-
continue
|
|
1157
|
+
const newline = source.indexOf('\n', cursor + 2)
|
|
1158
|
+
if (newline === -1 || newline >= target) return false
|
|
1159
|
+
cursor = newline
|
|
1160
|
+
continue
|
|
1160
1161
|
}
|
|
1161
1162
|
if (current === '/' && source[cursor + 1] === '*') {
|
|
1162
|
-
const end = source.indexOf('*/', cursor + 2)
|
|
1163
|
-
if (end === -1 || end >= target) return false
|
|
1164
|
-
cursor = end + 1
|
|
1165
|
-
continue
|
|
1163
|
+
const end = source.indexOf('*/', cursor + 2)
|
|
1164
|
+
if (end === -1 || end >= target) return false
|
|
1165
|
+
cursor = end + 1
|
|
1166
|
+
continue
|
|
1166
1167
|
}
|
|
1167
|
-
if (current === '"' || current === "'" || current === '`') quote = current
|
|
1168
|
+
if (current === '"' || current === "'" || current === '`') quote = current
|
|
1168
1169
|
}
|
|
1169
|
-
return quote === undefined
|
|
1170
|
+
return quote === undefined
|
|
1170
1171
|
}
|
|
1171
1172
|
|
|
1172
1173
|
function skipTrivia(source: string, start: number): number {
|
|
1173
|
-
let cursor = start
|
|
1174
|
+
let cursor = start
|
|
1174
1175
|
while (cursor < source.length) {
|
|
1175
1176
|
if (/\s/.test(source[cursor]!)) {
|
|
1176
|
-
cursor
|
|
1177
|
-
continue
|
|
1177
|
+
cursor++
|
|
1178
|
+
continue
|
|
1178
1179
|
}
|
|
1179
1180
|
if (source[cursor] === '/' && source[cursor + 1] === '/') {
|
|
1180
|
-
const newline = source.indexOf('\n', cursor + 2)
|
|
1181
|
-
cursor = newline === -1 ? source.length : newline + 1
|
|
1182
|
-
continue
|
|
1181
|
+
const newline = source.indexOf('\n', cursor + 2)
|
|
1182
|
+
cursor = newline === -1 ? source.length : newline + 1
|
|
1183
|
+
continue
|
|
1183
1184
|
}
|
|
1184
1185
|
if (source[cursor] === '/' && source[cursor + 1] === '*') {
|
|
1185
|
-
const end = source.indexOf('*/', cursor + 2)
|
|
1186
|
-
if (end === -1) return source.length
|
|
1187
|
-
cursor = end + 2
|
|
1188
|
-
continue
|
|
1186
|
+
const end = source.indexOf('*/', cursor + 2)
|
|
1187
|
+
if (end === -1) return source.length
|
|
1188
|
+
cursor = end + 2
|
|
1189
|
+
continue
|
|
1189
1190
|
}
|
|
1190
|
-
return cursor
|
|
1191
|
+
return cursor
|
|
1191
1192
|
}
|
|
1192
|
-
return cursor
|
|
1193
|
+
return cursor
|
|
1193
1194
|
}
|
|
1194
1195
|
|
|
1195
1196
|
function stringEnd(source: string, start: number, quote: string): number {
|
|
1196
1197
|
for (let cursor = start + 1; cursor < source.length; cursor++) {
|
|
1197
1198
|
if (source[cursor] === '\\') {
|
|
1198
|
-
cursor
|
|
1199
|
-
continue
|
|
1199
|
+
cursor++
|
|
1200
|
+
continue
|
|
1200
1201
|
}
|
|
1201
|
-
if (source[cursor] === quote) return cursor
|
|
1202
|
+
if (source[cursor] === quote) return cursor
|
|
1202
1203
|
}
|
|
1203
|
-
return -1
|
|
1204
|
+
return -1
|
|
1204
1205
|
}
|
|
1205
1206
|
|
|
1206
1207
|
function validateCodemodComments(routes: RouteManifestEntry[]): void {
|
|
1207
1208
|
for (const file of new Set(routes.map(route => route.file))) {
|
|
1208
|
-
const source = readSource(file)
|
|
1209
|
-
if (source === undefined) continue
|
|
1210
|
-
const match = /@next-codemod-error\s+([^\n*]+)/.exec(source)
|
|
1209
|
+
const source = readSource(file)
|
|
1210
|
+
if (source === undefined) continue
|
|
1211
|
+
const match = /@next-codemod-error\s+([^\n*]+)/.exec(source)
|
|
1211
1212
|
if (match?.[1]) {
|
|
1212
|
-
throw new PnextBuildValidationError(unresolvedCodemodCommentMessage(match[1].trim()))
|
|
1213
|
+
throw new PnextBuildValidationError(unresolvedCodemodCommentMessage(match[1].trim()))
|
|
1213
1214
|
}
|
|
1214
1215
|
}
|
|
1215
1216
|
}
|
|
@@ -1217,127 +1218,127 @@ function validateCodemodComments(routes: RouteManifestEntry[]): void {
|
|
|
1217
1218
|
// --- missing root layout ---------------------------------------------------
|
|
1218
1219
|
|
|
1219
1220
|
function validateRootLayout(config: ResolvedConfig, pageRoutes: RouteManifestEntry[]): void {
|
|
1220
|
-
if (pageRoutes.length === 0) return
|
|
1221
|
+
if (pageRoutes.length === 0) return
|
|
1221
1222
|
const hasRootLayout = pageExtensions().some(ext =>
|
|
1222
1223
|
existsSync(path.join(config.appPath, `layout.${ext}`)),
|
|
1223
|
-
)
|
|
1224
|
-
if (hasRootLayout) return
|
|
1224
|
+
)
|
|
1225
|
+
if (hasRootLayout) return
|
|
1225
1226
|
// No root `app/layout.*`. Next treats the topmost segment layout as the root
|
|
1226
1227
|
// and, when a page has an ancestor layout, does NOT error — the segment layout
|
|
1227
1228
|
// (or the synthesized builtin default `<html><body>{children}</body>`) supplies
|
|
1228
1229
|
// the document. Only a page with NO ancestor layout anywhere is a hard error.
|
|
1229
|
-
const orphan = pageRoutes.find(route => !hasAncestorLayout(config.appPath, route.file))
|
|
1230
|
-
if (!orphan) return
|
|
1230
|
+
const orphan = pageRoutes.find(route => !hasAncestorLayout(config.appPath, route.file))
|
|
1231
|
+
if (!orphan) return
|
|
1231
1232
|
throw new PnextBuildValidationError(
|
|
1232
1233
|
missingRootLayoutMessage(appDirRelativeFile(config, orphan.file)),
|
|
1233
|
-
)
|
|
1234
|
+
)
|
|
1234
1235
|
}
|
|
1235
1236
|
|
|
1236
1237
|
/** Whether any `layout.*` exists on the route file's directory chain (below appPath). */
|
|
1237
1238
|
function hasAncestorLayout(appPath: string, routeFile: string): boolean {
|
|
1238
|
-
let dir = path.dirname(routeFile)
|
|
1239
|
+
let dir = path.dirname(routeFile)
|
|
1239
1240
|
while (dir.startsWith(appPath)) {
|
|
1240
|
-
if (pageExtensions().some(ext => existsSync(path.join(dir, `layout.${ext}`)))) return true
|
|
1241
|
-
if (dir === appPath) break
|
|
1242
|
-
dir = path.dirname(dir)
|
|
1241
|
+
if (pageExtensions().some(ext => existsSync(path.join(dir, `layout.${ext}`)))) return true
|
|
1242
|
+
if (dir === appPath) break
|
|
1243
|
+
dir = path.dirname(dir)
|
|
1243
1244
|
}
|
|
1244
|
-
return false
|
|
1245
|
+
return false
|
|
1245
1246
|
}
|
|
1246
1247
|
|
|
1247
1248
|
function pageExtensions(): string[] {
|
|
1248
|
-
return [...new Set([...BASE_PAGE_EXTENSIONS, ...extraPageExtensions()])]
|
|
1249
|
+
return [...new Set([...BASE_PAGE_EXTENSIONS, ...extraPageExtensions()])]
|
|
1249
1250
|
}
|
|
1250
1251
|
|
|
1251
1252
|
// --- app/ vs pages/ conflict ----------------------------------------------
|
|
1252
1253
|
|
|
1253
1254
|
function validateAppPagesConflict(config: ResolvedConfig): void {
|
|
1254
|
-
const pagesDir = pagesDirFor(config)
|
|
1255
|
-
if (!pagesDir || !existsSync(pagesDir)) return
|
|
1256
|
-
const pagePaths = collectPagesRouterPaths(pagesDir)
|
|
1257
|
-
if (pagePaths.size === 0) return
|
|
1258
|
-
const appPaths = collectAppRouterPaths(path.join(config.root, 'app'))
|
|
1255
|
+
const pagesDir = pagesDirFor(config)
|
|
1256
|
+
if (!pagesDir || !existsSync(pagesDir)) return
|
|
1257
|
+
const pagePaths = collectPagesRouterPaths(pagesDir)
|
|
1258
|
+
if (pagePaths.size === 0) return
|
|
1259
|
+
const appPaths = collectAppRouterPaths(path.join(config.root, 'app'))
|
|
1259
1260
|
|
|
1260
|
-
const conflicts: { page: string; app: string }[] = []
|
|
1261
|
+
const conflicts: { page: string; app: string }[] = []
|
|
1261
1262
|
for (const [route, app] of appPaths) {
|
|
1262
|
-
const page = pagePaths.get(route)
|
|
1263
|
-
if (page) conflicts.push({ page, app })
|
|
1263
|
+
const page = pagePaths.get(route)
|
|
1264
|
+
if (page) conflicts.push({ page, app })
|
|
1264
1265
|
}
|
|
1265
|
-
if (conflicts.length === 0) return
|
|
1266
|
+
if (conflicts.length === 0) return
|
|
1266
1267
|
throw new PnextBuildValidationError(
|
|
1267
1268
|
conflictingAppAndPageMessage(dedupeConflicts(conflicts).sort(conflictSort)),
|
|
1268
|
-
)
|
|
1269
|
+
)
|
|
1269
1270
|
}
|
|
1270
1271
|
|
|
1271
1272
|
/** The pages/ directory sibling to the resolved app/ directory, if any. */
|
|
1272
1273
|
function pagesDirFor(config: ResolvedConfig): string | undefined {
|
|
1273
|
-
return path.join(config.root, 'pages')
|
|
1274
|
+
return path.join(config.root, 'pages')
|
|
1274
1275
|
}
|
|
1275
1276
|
|
|
1276
1277
|
function collectAppRouterPaths(appDir: string): Map<string, string> {
|
|
1277
|
-
const paths = new Map<string, string>()
|
|
1278
|
-
if (!existsSync(appDir)) return paths
|
|
1278
|
+
const paths = new Map<string, string>()
|
|
1279
|
+
if (!existsSync(appDir)) return paths
|
|
1279
1280
|
const walk = (dir: string, segments: string[]) => {
|
|
1280
1281
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1281
|
-
const full = path.join(dir, entry.name)
|
|
1282
|
+
const full = path.join(dir, entry.name)
|
|
1282
1283
|
if (entry.isDirectory()) {
|
|
1283
1284
|
if (entry.name.startsWith('@') || (entry.name.startsWith('(') && entry.name.includes('.')))
|
|
1284
|
-
continue
|
|
1285
|
+
continue
|
|
1285
1286
|
walk(
|
|
1286
1287
|
full,
|
|
1287
1288
|
entry.name.startsWith('(') && entry.name.endsWith(')')
|
|
1288
1289
|
? segments
|
|
1289
1290
|
: [...segments, entry.name],
|
|
1290
|
-
)
|
|
1291
|
-
continue
|
|
1291
|
+
)
|
|
1292
|
+
continue
|
|
1292
1293
|
}
|
|
1293
|
-
if (!/^page\.(?:tsx|ts|jsx|js|mjs)$/.test(entry.name)) continue
|
|
1294
|
-
const route = segments.length === 0 ? '/' : `/${segments.join('/')}
|
|
1295
|
-
paths.set(route, toPosix(path.relative(path.dirname(appDir), full)))
|
|
1294
|
+
if (!/^page\.(?:tsx|ts|jsx|js|mjs)$/.test(entry.name)) continue
|
|
1295
|
+
const route = segments.length === 0 ? '/' : `/${segments.join('/')}`
|
|
1296
|
+
paths.set(route, toPosix(path.relative(path.dirname(appDir), full)))
|
|
1296
1297
|
}
|
|
1297
|
-
}
|
|
1298
|
-
walk(appDir, [])
|
|
1299
|
-
return paths
|
|
1298
|
+
}
|
|
1299
|
+
walk(appDir, [])
|
|
1300
|
+
return paths
|
|
1300
1301
|
}
|
|
1301
1302
|
|
|
1302
1303
|
/** Public route paths declared under a pages/ directory (excludes api/ + _-files). */
|
|
1303
1304
|
function collectPagesRouterPaths(pagesDir: string): Map<string, string> {
|
|
1304
|
-
const paths = new Map<string, string>()
|
|
1305
|
+
const paths = new Map<string, string>()
|
|
1305
1306
|
const walk = (dir: string, prefix: string) => {
|
|
1306
1307
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1307
|
-
if (entry.name.startsWith('_')) continue
|
|
1308
|
-
const full = path.join(dir, entry.name)
|
|
1308
|
+
if (entry.name.startsWith('_')) continue
|
|
1309
|
+
const full = path.join(dir, entry.name)
|
|
1309
1310
|
if (entry.isDirectory()) {
|
|
1310
|
-
if (entry.name === 'api') continue
|
|
1311
|
-
walk(full, `${prefix}/${entry.name}`)
|
|
1312
|
-
continue
|
|
1311
|
+
if (entry.name === 'api') continue
|
|
1312
|
+
walk(full, `${prefix}/${entry.name}`)
|
|
1313
|
+
continue
|
|
1313
1314
|
}
|
|
1314
|
-
const match = /^(.*)\.(tsx|ts|jsx|js|mjs)$/.exec(entry.name)
|
|
1315
|
-
if (!match) continue
|
|
1316
|
-
const base = match[1]
|
|
1317
|
-
const rel = toPosix(path.relative(path.dirname(pagesDir), full))
|
|
1315
|
+
const match = /^(.*)\.(tsx|ts|jsx|js|mjs)$/.exec(entry.name)
|
|
1316
|
+
if (!match) continue
|
|
1317
|
+
const base = match[1]
|
|
1318
|
+
const rel = toPosix(path.relative(path.dirname(pagesDir), full))
|
|
1318
1319
|
if (base === 'index') {
|
|
1319
|
-
paths.set(prefix || '/', rel)
|
|
1320
|
+
paths.set(prefix || '/', rel)
|
|
1320
1321
|
} else {
|
|
1321
|
-
paths.set(`${prefix}/${base}`, rel)
|
|
1322
|
+
paths.set(`${prefix}/${base}`, rel)
|
|
1322
1323
|
}
|
|
1323
1324
|
}
|
|
1324
|
-
}
|
|
1325
|
-
walk(pagesDir, '')
|
|
1326
|
-
return paths
|
|
1325
|
+
}
|
|
1326
|
+
walk(pagesDir, '')
|
|
1327
|
+
return paths
|
|
1327
1328
|
}
|
|
1328
1329
|
|
|
1329
1330
|
function dedupeConflicts(conflicts: { page: string; app: string }[]) {
|
|
1330
|
-
const seen = new Set<string>()
|
|
1331
|
+
const seen = new Set<string>()
|
|
1331
1332
|
return conflicts.filter(conflict => {
|
|
1332
|
-
const key = `${conflict.page}\0${conflict.app}
|
|
1333
|
-
if (seen.has(key)) return false
|
|
1334
|
-
seen.add(key)
|
|
1335
|
-
return true
|
|
1336
|
-
})
|
|
1333
|
+
const key = `${conflict.page}\0${conflict.app}`
|
|
1334
|
+
if (seen.has(key)) return false
|
|
1335
|
+
seen.add(key)
|
|
1336
|
+
return true
|
|
1337
|
+
})
|
|
1337
1338
|
}
|
|
1338
1339
|
|
|
1339
1340
|
function conflictSort(a: { page: string; app: string }, b: { page: string; app: string }) {
|
|
1340
|
-
return a.page.localeCompare(b.page) || a.app.localeCompare(b.app)
|
|
1341
|
+
return a.page.localeCompare(b.page) || a.app.localeCompare(b.app)
|
|
1341
1342
|
}
|
|
1342
1343
|
|
|
1343
1344
|
// --- two parallel (route-group) pages on the same path ---------------------
|
|
@@ -1346,80 +1347,80 @@ function validateParallelPageConflicts(
|
|
|
1346
1347
|
config: ResolvedConfig,
|
|
1347
1348
|
pageRoutes: RouteManifestEntry[],
|
|
1348
1349
|
): void {
|
|
1349
|
-
const byPattern = new Map<string, RouteManifestEntry>()
|
|
1350
|
+
const byPattern = new Map<string, RouteManifestEntry>()
|
|
1350
1351
|
for (const route of pageRoutes) {
|
|
1351
|
-
if (route.synthetic) continue
|
|
1352
|
-
const existing = byPattern.get(route.pattern)
|
|
1352
|
+
if (route.synthetic) continue
|
|
1353
|
+
const existing = byPattern.get(route.pattern)
|
|
1353
1354
|
if (existing) {
|
|
1354
|
-
const labels = [routeGroupLabel(config, existing), routeGroupLabel(config, route)].sort()
|
|
1355
|
-
throw new PnextBuildValidationError(conflictingParallelPagesMessage(labels[0]!, labels[1]!))
|
|
1355
|
+
const labels = [routeGroupLabel(config, existing), routeGroupLabel(config, route)].sort()
|
|
1356
|
+
throw new PnextBuildValidationError(conflictingParallelPagesMessage(labels[0]!, labels[1]!))
|
|
1356
1357
|
}
|
|
1357
|
-
byPattern.set(route.pattern, route)
|
|
1358
|
+
byPattern.set(route.pattern, route)
|
|
1358
1359
|
}
|
|
1359
1360
|
}
|
|
1360
1361
|
|
|
1361
1362
|
/** The app-relative dir of the route's page file, as Next reports it. */
|
|
1362
1363
|
function routeGroupLabel(config: ResolvedConfig, route: RouteManifestEntry): string {
|
|
1363
|
-
const dir = path.dirname(appDirRelativeFile(config, route.file))
|
|
1364
|
-
return dir === '.' ? '/' : `/${dir}
|
|
1364
|
+
const dir = path.dirname(appDirRelativeFile(config, route.file))
|
|
1365
|
+
return dir === '.' ? '/' : `/${dir}`
|
|
1365
1366
|
}
|
|
1366
1367
|
|
|
1367
1368
|
// --- missing named-slot default on non-leaf segments -----------------------
|
|
1368
1369
|
|
|
1369
1370
|
function validateMissingSlotDefaults(config: ResolvedConfig, routes: RouteManifestEntry[]): void {
|
|
1370
|
-
const failures: string[] = []
|
|
1371
|
-
const interceptedTargets = interceptedTargetDirs(routes)
|
|
1371
|
+
const failures: string[] = []
|
|
1372
|
+
const interceptedTargets = interceptedTargetDirs(routes)
|
|
1372
1373
|
const visit = (dir: string): void => {
|
|
1373
1374
|
for (const entry of readDirEntries(dir)) {
|
|
1374
|
-
if (!entry.isDirectory()) continue
|
|
1375
|
-
const child = path.join(dir, entry.name)
|
|
1376
|
-
if (isSlotDir(entry.name)) continue
|
|
1377
|
-
visit(child)
|
|
1375
|
+
if (!entry.isDirectory()) continue
|
|
1376
|
+
const child = path.join(dir, entry.name)
|
|
1377
|
+
if (isSlotDir(entry.name)) continue
|
|
1378
|
+
visit(child)
|
|
1378
1379
|
}
|
|
1379
1380
|
|
|
1380
|
-
if (!hasNormalChildRoute(dir)) return
|
|
1381
|
-
if (interceptedTargets.has(dir)) return
|
|
1381
|
+
if (!hasNormalChildRoute(dir)) return
|
|
1382
|
+
if (interceptedTargets.has(dir)) return
|
|
1382
1383
|
for (const slot of readDirEntries(dir)) {
|
|
1383
|
-
if (!slot.isDirectory() || !isNamedSlotDir(slot.name)) continue
|
|
1384
|
-
const slotDir = path.join(dir, slot.name)
|
|
1385
|
-
if (hasConventionFile(slotDir, 'default')) continue
|
|
1384
|
+
if (!slot.isDirectory() || !isNamedSlotDir(slot.name)) continue
|
|
1385
|
+
const slotDir = path.join(dir, slot.name)
|
|
1386
|
+
if (hasConventionFile(slotDir, 'default')) continue
|
|
1386
1387
|
// A catch-all page inside the slot (`[...x]`/`[[...x]]`) matches any
|
|
1387
1388
|
// otherwise-unmatched path, so it doubles as the slot's fallback and
|
|
1388
1389
|
// Next does not require an explicit default.js (parallel-slot catch-all).
|
|
1389
|
-
if (slotHasCatchAllPage(slotDir)) continue
|
|
1390
|
-
failures.push(missingDefaultParallelRouteMessage(appPath(config, slotDir), slot.name))
|
|
1390
|
+
if (slotHasCatchAllPage(slotDir)) continue
|
|
1391
|
+
failures.push(missingDefaultParallelRouteMessage(appPath(config, slotDir), slot.name))
|
|
1391
1392
|
}
|
|
1392
|
-
}
|
|
1393
|
+
}
|
|
1393
1394
|
|
|
1394
|
-
visit(config.appPath)
|
|
1395
|
-
if (failures.length > 0) throw new PnextBuildValidationError(failures.join('\n\n'))
|
|
1395
|
+
visit(config.appPath)
|
|
1396
|
+
if (failures.length > 0) throw new PnextBuildValidationError(failures.join('\n\n'))
|
|
1396
1397
|
}
|
|
1397
1398
|
|
|
1398
1399
|
function interceptedTargetDirs(routes: RouteManifestEntry[]): Set<string> {
|
|
1399
1400
|
const patterns = new Set(
|
|
1400
1401
|
routes.filter(route => route.kind === 'page' && route.interception).map(route => route.pattern),
|
|
1401
|
-
)
|
|
1402
|
+
)
|
|
1402
1403
|
return new Set(
|
|
1403
1404
|
routes
|
|
1404
1405
|
.filter(route => route.kind === 'page' && !route.interception && patterns.has(route.pattern))
|
|
1405
1406
|
.map(route => path.dirname(route.file)),
|
|
1406
|
-
)
|
|
1407
|
+
)
|
|
1407
1408
|
}
|
|
1408
1409
|
|
|
1409
1410
|
function hasNormalChildRoute(dir: string, depth = 0): boolean {
|
|
1410
1411
|
for (const entry of readDirEntries(dir)) {
|
|
1411
|
-
if (!entry.isDirectory() || isSlotDir(entry.name)) continue
|
|
1412
|
-
const child = path.join(dir, entry.name)
|
|
1413
|
-
const nextDepth = isGroupDir(entry.name) ? depth : depth + 1
|
|
1412
|
+
if (!entry.isDirectory() || isSlotDir(entry.name)) continue
|
|
1413
|
+
const child = path.join(dir, entry.name)
|
|
1414
|
+
const nextDepth = isGroupDir(entry.name) ? depth : depth + 1
|
|
1414
1415
|
if (
|
|
1415
1416
|
nextDepth > 0 &&
|
|
1416
1417
|
(hasConventionFile(child, 'page') || hasConventionFile(child, 'default'))
|
|
1417
1418
|
) {
|
|
1418
|
-
return true
|
|
1419
|
+
return true
|
|
1419
1420
|
}
|
|
1420
|
-
if (hasNormalChildRoute(child, nextDepth)) return true
|
|
1421
|
+
if (hasNormalChildRoute(child, nextDepth)) return true
|
|
1421
1422
|
}
|
|
1422
|
-
return false
|
|
1423
|
+
return false
|
|
1423
1424
|
}
|
|
1424
1425
|
|
|
1425
1426
|
// Whether a slot subtree contains a catch-all page segment (`[...x]` or the
|
|
@@ -1427,106 +1428,106 @@ function hasNormalChildRoute(dir: string, depth = 0): boolean {
|
|
|
1427
1428
|
// (`(group)`) and nested normal segments are walked; other slot dirs are not.
|
|
1428
1429
|
function slotHasCatchAllPage(dir: string): boolean {
|
|
1429
1430
|
for (const entry of readDirEntries(dir)) {
|
|
1430
|
-
if (!entry.isDirectory()) continue
|
|
1431
|
-
const child = path.join(dir, entry.name)
|
|
1432
|
-
if (/^\[\[?\.\.\..+\]\]?$/.test(entry.name) && hasConventionFile(child, 'page')) return true
|
|
1433
|
-
if (slotHasCatchAllPage(child)) return true
|
|
1431
|
+
if (!entry.isDirectory()) continue
|
|
1432
|
+
const child = path.join(dir, entry.name)
|
|
1433
|
+
if (/^\[\[?\.\.\..+\]\]?$/.test(entry.name) && hasConventionFile(child, 'page')) return true
|
|
1434
|
+
if (slotHasCatchAllPage(child)) return true
|
|
1434
1435
|
}
|
|
1435
|
-
return false
|
|
1436
|
+
return false
|
|
1436
1437
|
}
|
|
1437
1438
|
|
|
1438
1439
|
function hasConventionFile(dir: string, name: string): boolean {
|
|
1439
|
-
return pageExtensions().some(ext => existsSync(path.join(dir, `${name}.${ext}`)))
|
|
1440
|
+
return pageExtensions().some(ext => existsSync(path.join(dir, `${name}.${ext}`)))
|
|
1440
1441
|
}
|
|
1441
1442
|
|
|
1442
1443
|
function readDirEntries(dir: string) {
|
|
1443
1444
|
try {
|
|
1444
|
-
return readdirSync(dir, { withFileTypes: true })
|
|
1445
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
1445
1446
|
} catch {
|
|
1446
|
-
return []
|
|
1447
|
+
return []
|
|
1447
1448
|
}
|
|
1448
1449
|
}
|
|
1449
1450
|
|
|
1450
1451
|
function isSlotDir(name: string): boolean {
|
|
1451
|
-
return name.startsWith('@')
|
|
1452
|
+
return name.startsWith('@')
|
|
1452
1453
|
}
|
|
1453
1454
|
|
|
1454
1455
|
function isNamedSlotDir(name: string): boolean {
|
|
1455
|
-
return isSlotDir(name) && name !== '@children'
|
|
1456
|
+
return isSlotDir(name) && name !== '@children'
|
|
1456
1457
|
}
|
|
1457
1458
|
|
|
1458
1459
|
function isGroupDir(name: string): boolean {
|
|
1459
|
-
return name.startsWith('(') && name.endsWith(')')
|
|
1460
|
+
return name.startsWith('(') && name.endsWith(')')
|
|
1460
1461
|
}
|
|
1461
1462
|
|
|
1462
1463
|
function appPath(config: ResolvedConfig, file: string): string {
|
|
1463
|
-
return `app/${appDirRelativeFile(config, file)}
|
|
1464
|
+
return `app/${appDirRelativeFile(config, file)}`
|
|
1464
1465
|
}
|
|
1465
1466
|
|
|
1466
1467
|
// --- undefined default export ----------------------------------------------
|
|
1467
1468
|
|
|
1468
1469
|
function validateDefaultExports(config: ResolvedConfig, routes: RouteManifestEntry[]): void {
|
|
1469
|
-
const failures: string[] = []
|
|
1470
|
-
const appPath = path.resolve(config.appPath)
|
|
1470
|
+
const failures: string[] = []
|
|
1471
|
+
const appPath = path.resolve(config.appPath)
|
|
1471
1472
|
for (const route of routes) {
|
|
1472
|
-
if (route.kind !== 'page' || route.interception || route.synthetic) continue
|
|
1473
|
+
if (route.kind !== 'page' || route.interception || route.synthetic) continue
|
|
1473
1474
|
for (const file of defaultExportFiles(route)) {
|
|
1474
1475
|
// MDX pages have no literal `export default` in source; the compiler emits one.
|
|
1475
|
-
if (/\.mdx?$/.test(file)) continue
|
|
1476
|
+
if (/\.mdx?$/.test(file)) continue
|
|
1476
1477
|
// A core-authored root layout exports only `metadata` — pnext synthesizes
|
|
1477
1478
|
// the <html>/<body> shell from it, so there is no component to default-export.
|
|
1478
1479
|
// Next requires one; under compat the shell still comes from core, so the
|
|
1479
1480
|
// layout is valid (SPEC change 9). Nested layouts keep Next's rule.
|
|
1480
|
-
if (isRootLayoutFile(appPath, file)) continue
|
|
1481
|
-
const source = readSource(file)
|
|
1482
|
-
if (source === undefined) continue
|
|
1481
|
+
if (isRootLayoutFile(appPath, file)) continue
|
|
1482
|
+
const source = readSource(file)
|
|
1483
|
+
if (source === undefined) continue
|
|
1483
1484
|
if (!hasDefaultExport(stripCommentsAndStrings(source))) {
|
|
1484
|
-
failures.push(undefinedDefaultExportMessage(componentRoutePath(route, file)))
|
|
1485
|
+
failures.push(undefinedDefaultExportMessage(componentRoutePath(route, file)))
|
|
1485
1486
|
}
|
|
1486
1487
|
}
|
|
1487
1488
|
}
|
|
1488
|
-
if (failures.length > 0) throw new PnextBuildValidationError([...new Set(failures)].join('\n'))
|
|
1489
|
+
if (failures.length > 0) throw new PnextBuildValidationError([...new Set(failures)].join('\n'))
|
|
1489
1490
|
}
|
|
1490
1491
|
|
|
1491
1492
|
function defaultExportFiles(route: RouteManifestEntry): string[] {
|
|
1492
|
-
const files = new Set<string>([route.file])
|
|
1493
|
-
const dir = path.dirname(route.file)
|
|
1493
|
+
const files = new Set<string>([route.file])
|
|
1494
|
+
const dir = path.dirname(route.file)
|
|
1494
1495
|
for (const ext of pageExtensions()) {
|
|
1495
|
-
files.add(path.join(dir, `layout.${ext}`))
|
|
1496
|
-
files.add(path.join(dir, `not-found.${ext}`))
|
|
1496
|
+
files.add(path.join(dir, `layout.${ext}`))
|
|
1497
|
+
files.add(path.join(dir, `not-found.${ext}`))
|
|
1497
1498
|
}
|
|
1498
|
-
return [...files].filter(file => existsSync(file))
|
|
1499
|
+
return [...files].filter(file => existsSync(file))
|
|
1499
1500
|
}
|
|
1500
1501
|
|
|
1501
1502
|
function isRootLayoutFile(appPath: string, file: string): boolean {
|
|
1502
1503
|
return (
|
|
1503
1504
|
path.basename(file).replace(/\.[^.]+$/, '') === 'layout' &&
|
|
1504
1505
|
path.resolve(path.dirname(file)) === appPath
|
|
1505
|
-
)
|
|
1506
|
+
)
|
|
1506
1507
|
}
|
|
1507
1508
|
|
|
1508
1509
|
function componentRoutePath(route: RouteManifestEntry, file: string): string {
|
|
1509
|
-
const convention = path.basename(file).replace(/\.(tsx|ts|jsx|js|mjs)$/, '')
|
|
1510
|
-
const routePath = route.route || '/'
|
|
1511
|
-
if (convention === 'page') return routePath === '/' ? '/page' : `${routePath}/page
|
|
1512
|
-
return `${routePath === '/' ? '' : routePath}/${convention}
|
|
1510
|
+
const convention = path.basename(file).replace(/\.(tsx|ts|jsx|js|mjs)$/, '')
|
|
1511
|
+
const routePath = route.route || '/'
|
|
1512
|
+
if (convention === 'page') return routePath === '/' ? '/page' : `${routePath}/page`
|
|
1513
|
+
return `${routePath === '/' ? '' : routePath}/${convention}`
|
|
1513
1514
|
}
|
|
1514
1515
|
|
|
1515
1516
|
function hasDefaultExport(searchable: string): boolean {
|
|
1516
|
-
return /\bexport\s+default\b/.test(searchable) || hasDefaultNamedExport(searchable)
|
|
1517
|
+
return /\bexport\s+default\b/.test(searchable) || hasDefaultNamedExport(searchable)
|
|
1517
1518
|
}
|
|
1518
1519
|
|
|
1519
1520
|
function hasDefaultNamedExport(searchable: string): boolean {
|
|
1520
1521
|
for (const match of searchable.matchAll(/\bexport\s*\{([^}]*)\}/g)) {
|
|
1521
|
-
const specifiers = match[1]?.split(',') ?? []
|
|
1522
|
-
if (specifiers.some(exportsDefaultSpecifier)) return true
|
|
1522
|
+
const specifiers = match[1]?.split(',') ?? []
|
|
1523
|
+
if (specifiers.some(exportsDefaultSpecifier)) return true
|
|
1523
1524
|
}
|
|
1524
|
-
return false
|
|
1525
|
+
return false
|
|
1525
1526
|
}
|
|
1526
1527
|
|
|
1527
1528
|
function exportsDefaultSpecifier(specifier: string): boolean {
|
|
1528
|
-
const parts = specifier.trim().split(/\s+as\s+/)
|
|
1529
|
-
return parts.length === 1 ? parts[0] === 'default' : parts.at(-1) === 'default'
|
|
1529
|
+
const parts = specifier.trim().split(/\s+as\s+/)
|
|
1530
|
+
return parts.length === 1 ? parts[0] === 'default' : parts.at(-1) === 'default'
|
|
1530
1531
|
}
|
|
1531
1532
|
|
|
1532
1533
|
// --- useSearchParams without Suspense --------------------------------------
|
|
@@ -1542,22 +1543,22 @@ function validateSearchParamsSuspense(
|
|
|
1542
1543
|
// force-dynamic, or request-data usage) reads useSearchParams on-demand, so
|
|
1543
1544
|
// no prerender-time CSR bailout occurs. Scope the check accordingly.
|
|
1544
1545
|
for (const route of pageRoutes) {
|
|
1545
|
-
if (!route.client) continue
|
|
1546
|
-
if (!isStaticallyPrerendered(route)) continue
|
|
1547
|
-
const source = readSource(route.file)
|
|
1548
|
-
if (source === undefined) continue
|
|
1549
|
-
const stripped = stripCommentsAndStrings(source)
|
|
1546
|
+
if (!route.client) continue
|
|
1547
|
+
if (!isStaticallyPrerendered(route)) continue
|
|
1548
|
+
const source = readSource(route.file)
|
|
1549
|
+
if (source === undefined) continue
|
|
1550
|
+
const stripped = stripCommentsAndStrings(source)
|
|
1550
1551
|
// This shape uses useSyncExternalStore's server snapshot to render a stable
|
|
1551
1552
|
// shell, then fills the URLSearchParams instance after hydration.
|
|
1552
|
-
if (/\buseSyncExternalStore\s*\(/.test(stripped)) continue
|
|
1553
|
-
if (!usesSearchParamsHook(stripped)) continue
|
|
1554
|
-
if (fileTreeHasSuspense(config, route)) continue
|
|
1555
|
-
throw new PnextBuildValidationError(missingSuspenseWithCsrBailoutMessage(route.route || '/'))
|
|
1553
|
+
if (/\buseSyncExternalStore\s*\(/.test(stripped)) continue
|
|
1554
|
+
if (!usesSearchParamsHook(stripped)) continue
|
|
1555
|
+
if (fileTreeHasSuspense(config, route)) continue
|
|
1556
|
+
throw new PnextBuildValidationError(missingSuspenseWithCsrBailoutMessage(route.route || '/'))
|
|
1556
1557
|
}
|
|
1557
1558
|
}
|
|
1558
1559
|
|
|
1559
1560
|
function usesSearchParamsHook(searchable: string): boolean {
|
|
1560
|
-
return /\buseSearchParams\s*\(/.test(searchable)
|
|
1561
|
+
return /\buseSearchParams\s*\(/.test(searchable)
|
|
1561
1562
|
}
|
|
1562
1563
|
|
|
1563
1564
|
/**
|
|
@@ -1570,73 +1571,71 @@ function usesSearchParamsHook(searchable: string): boolean {
|
|
|
1570
1571
|
* what triggers the bailout during prerender.
|
|
1571
1572
|
*/
|
|
1572
1573
|
function isStaticallyPrerendered(route: RouteManifestEntry): boolean {
|
|
1573
|
-
const config = route.segmentConfig
|
|
1574
|
+
const config = route.segmentConfig
|
|
1574
1575
|
const configForcesDynamic =
|
|
1575
1576
|
config?.dynamic === 'force-dynamic' ||
|
|
1576
1577
|
config?.revalidate === 0 ||
|
|
1577
|
-
config?.fetchCache === 'force-no-store'
|
|
1578
|
-
if (configForcesDynamic) return false
|
|
1578
|
+
config?.fetchCache === 'force-no-store'
|
|
1579
|
+
if (configForcesDynamic) return false
|
|
1579
1580
|
// Dynamic segments can only be prerendered when generateStaticParams
|
|
1580
1581
|
// enumerates them; otherwise the route renders on-demand per request.
|
|
1581
|
-
const isDynamicRoute = route.params.length > 0 || Boolean(route.catchAll)
|
|
1582
|
-
if (isDynamicRoute && !route.hasStaticParams) return false
|
|
1583
|
-
return true
|
|
1582
|
+
const isDynamicRoute = route.params.length > 0 || Boolean(route.catchAll)
|
|
1583
|
+
if (isDynamicRoute && !route.hasStaticParams) return false
|
|
1584
|
+
return true
|
|
1584
1585
|
}
|
|
1585
1586
|
|
|
1586
1587
|
function fileTreeHasSuspense(config: ResolvedConfig, route: RouteManifestEntry): boolean {
|
|
1587
1588
|
// A Suspense boundary anywhere on the layout chain (root->leaf) or in the page
|
|
1588
1589
|
// file itself satisfies the requirement. We scan the source text for a JSX
|
|
1589
1590
|
// <Suspense usage or a Suspense import, which is what the fixtures rely on.
|
|
1590
|
-
const files = [route.file, ...layoutChain(config.appPath, route.file)]
|
|
1591
|
+
const files = [route.file, ...layoutChain(config.appPath, route.file)]
|
|
1591
1592
|
for (const file of files) {
|
|
1592
|
-
const source = readSource(file)
|
|
1593
|
-
if (source === undefined) continue
|
|
1594
|
-
if (/<Suspense[\s/>]/.test(source) || /\bReact\.Suspense\b/.test(source)) return true
|
|
1593
|
+
const source = readSource(file)
|
|
1594
|
+
if (source === undefined) continue
|
|
1595
|
+
if (/<Suspense[\s/>]/.test(source) || /\bReact\.Suspense\b/.test(source)) return true
|
|
1595
1596
|
}
|
|
1596
|
-
return false
|
|
1597
|
+
return false
|
|
1597
1598
|
}
|
|
1598
1599
|
|
|
1599
1600
|
function layoutChain(appPath: string, routeFile: string): string[] {
|
|
1600
|
-
const files: string[] = []
|
|
1601
|
-
let dir = path.dirname(routeFile)
|
|
1601
|
+
const files: string[] = []
|
|
1602
|
+
let dir = path.dirname(routeFile)
|
|
1602
1603
|
while (dir.startsWith(appPath)) {
|
|
1603
1604
|
for (const ext of pageExtensions()) {
|
|
1604
|
-
const candidate = path.join(dir, `layout.${ext}`)
|
|
1605
|
-
if (existsSync(candidate)) files.push(candidate)
|
|
1605
|
+
const candidate = path.join(dir, `layout.${ext}`)
|
|
1606
|
+
if (existsSync(candidate)) files.push(candidate)
|
|
1606
1607
|
}
|
|
1607
|
-
if (dir === appPath) break
|
|
1608
|
-
dir = path.dirname(dir)
|
|
1608
|
+
if (dir === appPath) break
|
|
1609
|
+
dir = path.dirname(dir)
|
|
1609
1610
|
}
|
|
1610
|
-
return files
|
|
1611
|
+
return files
|
|
1611
1612
|
}
|
|
1612
1613
|
|
|
1613
1614
|
// --- output: 'export' mode -------------------------------------------------
|
|
1614
1615
|
|
|
1615
1616
|
function validateExportMode(config: ResolvedConfig, routes: RouteManifestEntry[]): void {
|
|
1616
|
-
const nextConfig = getNextConfig()
|
|
1617
|
-
if (nextConfig.output !== 'export') return
|
|
1617
|
+
const nextConfig = getNextConfig()
|
|
1618
|
+
if (nextConfig.output !== 'export') return
|
|
1618
1619
|
|
|
1619
1620
|
// exportPathMap is a pages-router-only config; using it with the app dir is a
|
|
1620
1621
|
// hard error.
|
|
1621
|
-
const hasAppPages = routes.some(route => route.kind === 'page' && !route.interception)
|
|
1622
|
+
const hasAppPages = routes.some(route => route.kind === 'page' && !route.interception)
|
|
1622
1623
|
if (hasAppPages && typeof nextConfig.exportPathMap === 'function') {
|
|
1623
|
-
throw new PnextBuildValidationError(exportPathMapWithAppDirMessage())
|
|
1624
|
+
throw new PnextBuildValidationError(exportPathMapWithAppDirMessage())
|
|
1624
1625
|
}
|
|
1625
1626
|
|
|
1626
1627
|
// Server actions need a server to receive them — Next fails the export build
|
|
1627
1628
|
// before any per-route diagnostics.
|
|
1628
1629
|
if (routesUseServerActions(routes)) {
|
|
1629
|
-
throw new PnextBuildValidationError('Server Actions are not supported with static export.')
|
|
1630
|
+
throw new PnextBuildValidationError('Server Actions are not supported with static export.')
|
|
1630
1631
|
}
|
|
1631
1632
|
|
|
1632
1633
|
if (routes.some(route => route.interception)) {
|
|
1633
|
-
throw new PnextBuildValidationError(
|
|
1634
|
-
'Intercepting routes are not supported with static export.',
|
|
1635
|
-
);
|
|
1634
|
+
throw new PnextBuildValidationError('Intercepting routes are not supported with static export.')
|
|
1636
1635
|
}
|
|
1637
1636
|
|
|
1638
1637
|
for (const route of routes) {
|
|
1639
|
-
if (route.interception || route.synthetic) continue
|
|
1638
|
+
if (route.interception || route.synthetic) continue
|
|
1640
1639
|
// force-dynamic is incompatible with a static export. Pages get Next's
|
|
1641
1640
|
// create-component-tree wording; handlers keep the route-level message.
|
|
1642
1641
|
if (route.segmentConfig?.dynamic === 'force-dynamic') {
|
|
@@ -1644,20 +1643,20 @@ function validateExportMode(config: ResolvedConfig, routes: RouteManifestEntry[]
|
|
|
1644
1643
|
route.kind === 'page'
|
|
1645
1644
|
? forceDynamicPageWithExportMessage()
|
|
1646
1645
|
: dynamicForceDynamicWithExportMessage(nextPagePath(route)),
|
|
1647
|
-
)
|
|
1646
|
+
)
|
|
1648
1647
|
}
|
|
1649
1648
|
if (route.kind === 'handler' && !routeHandlerStaticExportable(route)) {
|
|
1650
1649
|
throw new PnextBuildValidationError(
|
|
1651
1650
|
routeHandlerNotStaticWithExportMessage(nextPagePath(route)),
|
|
1652
|
-
)
|
|
1651
|
+
)
|
|
1653
1652
|
}
|
|
1654
1653
|
// A dynamic route (params/catch-all) must supply generateStaticParams to be
|
|
1655
1654
|
// exportable — there is no runtime to fill params in a static export.
|
|
1656
|
-
const isDynamicRoute = route.params.length > 0 || Boolean(route.catchAll)
|
|
1655
|
+
const isDynamicRoute = route.params.length > 0 || Boolean(route.catchAll)
|
|
1657
1656
|
if (route.kind === 'page' && isDynamicRoute && !route.hasStaticParams) {
|
|
1658
1657
|
throw new PnextBuildValidationError(
|
|
1659
1658
|
missingGenerateStaticParamsForExportMessage(nextPagePath(route)),
|
|
1660
|
-
)
|
|
1659
|
+
)
|
|
1661
1660
|
}
|
|
1662
1661
|
}
|
|
1663
1662
|
}
|
|
@@ -1665,21 +1664,21 @@ function validateExportMode(config: ResolvedConfig, routes: RouteManifestEntry[]
|
|
|
1665
1664
|
// Whether any route's source graph declares server actions ('use server' as a
|
|
1666
1665
|
// module prologue or an inline function directive).
|
|
1667
1666
|
function routesUseServerActions(routes: RouteManifestEntry[]): boolean {
|
|
1668
|
-
const seen = new Set<string>()
|
|
1667
|
+
const seen = new Set<string>()
|
|
1669
1668
|
for (const route of routes) {
|
|
1670
1669
|
for (const file of route.sourceFiles ?? []) {
|
|
1671
|
-
if (seen.has(file)) continue
|
|
1672
|
-
seen.add(file)
|
|
1673
|
-
const source = readSource(file)
|
|
1674
|
-
if (source === undefined) continue
|
|
1675
|
-
if (/(["'])use server\1\s*;?/.test(source)) return true
|
|
1670
|
+
if (seen.has(file)) continue
|
|
1671
|
+
seen.add(file)
|
|
1672
|
+
const source = readSource(file)
|
|
1673
|
+
if (source === undefined) continue
|
|
1674
|
+
if (/(["'])use server\1\s*;?/.test(source)) return true
|
|
1676
1675
|
}
|
|
1677
1676
|
}
|
|
1678
|
-
return false
|
|
1677
|
+
return false
|
|
1679
1678
|
}
|
|
1680
1679
|
|
|
1681
1680
|
function routeHandlerStaticExportable(route: RouteManifestEntry): boolean {
|
|
1682
|
-
const config = route.segmentConfig
|
|
1681
|
+
const config = route.segmentConfig
|
|
1683
1682
|
return (
|
|
1684
1683
|
config?.dynamic === 'force-static' ||
|
|
1685
1684
|
// `dynamic = 'error'` opts the handler into static rendering (dynamic API
|
|
@@ -1688,28 +1687,28 @@ function routeHandlerStaticExportable(route: RouteManifestEntry): boolean {
|
|
|
1688
1687
|
config?.revalidate === false ||
|
|
1689
1688
|
(typeof config?.revalidate === 'number' && config.revalidate > 0) ||
|
|
1690
1689
|
route.hasStaticParams
|
|
1691
|
-
)
|
|
1690
|
+
)
|
|
1692
1691
|
}
|
|
1693
1692
|
|
|
1694
1693
|
/** The route path in Next's page notation (`/blog/[slug]`, `/[...all]`). */
|
|
1695
1694
|
function nextPagePath(route: RouteManifestEntry): string {
|
|
1696
|
-
let value = route.route || '/'
|
|
1695
|
+
let value = route.route || '/'
|
|
1697
1696
|
if (route.catchAll) {
|
|
1698
|
-
const token = route.catchAllOptional ? `[[...${route.catchAll}]]` : `[...${route.catchAll}]
|
|
1699
|
-
value = value.replace(`:${route.catchAll}*`, token)
|
|
1697
|
+
const token = route.catchAllOptional ? `[[...${route.catchAll}]]` : `[...${route.catchAll}]`
|
|
1698
|
+
value = value.replace(`:${route.catchAll}*`, token)
|
|
1700
1699
|
}
|
|
1701
|
-
for (const param of route.params) value = value.replace(`:${param}`, `[${param}]`)
|
|
1702
|
-
return value
|
|
1700
|
+
for (const param of route.params) value = value.replace(`:${param}`, `[${param}]`)
|
|
1701
|
+
return value
|
|
1703
1702
|
}
|
|
1704
1703
|
|
|
1705
1704
|
// --- shared source scanning ------------------------------------------------
|
|
1706
1705
|
|
|
1707
1706
|
function appDirRelativeFile(config: ResolvedConfig, file: string): string {
|
|
1708
|
-
return toPosix(path.relative(config.appPath, file))
|
|
1707
|
+
return toPosix(path.relative(config.appPath, file))
|
|
1709
1708
|
}
|
|
1710
1709
|
|
|
1711
1710
|
function toPosix(file: string): string {
|
|
1712
|
-
return file.split(path.sep).join('/')
|
|
1711
|
+
return file.split(path.sep).join('/')
|
|
1713
1712
|
}
|
|
1714
1713
|
|
|
1715
1714
|
/**
|
|
@@ -1723,48 +1722,48 @@ function toPosix(file: string): string {
|
|
|
1723
1722
|
* opens first at the cursor, keeps them from interfering.
|
|
1724
1723
|
*/
|
|
1725
1724
|
function stripCommentsAndStrings(source: string): string {
|
|
1726
|
-
let out = ''
|
|
1727
|
-
let i = 0
|
|
1728
|
-
const n = source.length
|
|
1725
|
+
let out = ''
|
|
1726
|
+
let i = 0
|
|
1727
|
+
const n = source.length
|
|
1729
1728
|
while (i < n) {
|
|
1730
|
-
const ch = source[i]
|
|
1731
|
-
const next = source[i + 1]
|
|
1729
|
+
const ch = source[i]
|
|
1730
|
+
const next = source[i + 1]
|
|
1732
1731
|
// Block comment.
|
|
1733
1732
|
if (ch === '/' && next === '*') {
|
|
1734
|
-
const end = source.indexOf('*/', i + 2)
|
|
1735
|
-
out += ' '
|
|
1736
|
-
i = end === -1 ? n : end + 2
|
|
1737
|
-
continue
|
|
1733
|
+
const end = source.indexOf('*/', i + 2)
|
|
1734
|
+
out += ' '
|
|
1735
|
+
i = end === -1 ? n : end + 2
|
|
1736
|
+
continue
|
|
1738
1737
|
}
|
|
1739
1738
|
// Line comment.
|
|
1740
1739
|
if (ch === '/' && next === '/') {
|
|
1741
|
-
const end = source.indexOf('\n', i + 2)
|
|
1742
|
-
out += ' '
|
|
1743
|
-
i = end === -1 ? n : end
|
|
1744
|
-
continue
|
|
1740
|
+
const end = source.indexOf('\n', i + 2)
|
|
1741
|
+
out += ' '
|
|
1742
|
+
i = end === -1 ? n : end
|
|
1743
|
+
continue
|
|
1745
1744
|
}
|
|
1746
1745
|
// String / template literal.
|
|
1747
1746
|
if (ch === '"' || ch === "'" || ch === '`') {
|
|
1748
|
-
const quote = ch
|
|
1749
|
-
out += quote
|
|
1750
|
-
i += 1
|
|
1747
|
+
const quote = ch
|
|
1748
|
+
out += quote
|
|
1749
|
+
i += 1
|
|
1751
1750
|
while (i < n) {
|
|
1752
|
-
const c = source[i]
|
|
1751
|
+
const c = source[i]
|
|
1753
1752
|
if (c === '\\') {
|
|
1754
|
-
i += 2
|
|
1755
|
-
continue
|
|
1753
|
+
i += 2
|
|
1754
|
+
continue
|
|
1756
1755
|
}
|
|
1757
1756
|
if (c === quote) {
|
|
1758
|
-
i += 1
|
|
1759
|
-
break
|
|
1757
|
+
i += 1
|
|
1758
|
+
break
|
|
1760
1759
|
}
|
|
1761
|
-
i += 1
|
|
1760
|
+
i += 1
|
|
1762
1761
|
}
|
|
1763
|
-
out += quote
|
|
1764
|
-
continue
|
|
1762
|
+
out += quote
|
|
1763
|
+
continue
|
|
1765
1764
|
}
|
|
1766
|
-
out += ch
|
|
1767
|
-
i += 1
|
|
1765
|
+
out += ch
|
|
1766
|
+
i += 1
|
|
1768
1767
|
}
|
|
1769
|
-
return out
|
|
1768
|
+
return out
|
|
1770
1769
|
}
|