@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
|
@@ -13,36 +13,36 @@
|
|
|
13
13
|
// helper/component chain is resolved through local callsites, and ancestors are mapped to their JSX
|
|
14
14
|
// callsite positions - the same shape React's owner stack produces for these trees.
|
|
15
15
|
|
|
16
|
-
import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
17
|
-
import path from 'node:path'
|
|
16
|
+
import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
17
|
+
import path from 'node:path'
|
|
18
18
|
|
|
19
19
|
export interface PrerenderDiagnosticInput {
|
|
20
20
|
/** App-router pathname, e.g. '/dynamic-root'. */
|
|
21
|
-
route: string
|
|
21
|
+
route: string
|
|
22
22
|
/** Absolute path of the route's page file. */
|
|
23
|
-
pageFile: string
|
|
23
|
+
pageFile: string
|
|
24
24
|
/** Absolute path of the app/ directory (for app-relative display paths). */
|
|
25
|
-
appPath: string
|
|
25
|
+
appPath: string
|
|
26
26
|
/** `--debug-prerender`: unminified stacks + codeframes. */
|
|
27
|
-
debugPrerender: boolean
|
|
27
|
+
debugPrerender: boolean
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/** A single owner-stack frame within the page file. */
|
|
31
31
|
interface Frame {
|
|
32
|
-
name: string
|
|
33
|
-
line: number
|
|
34
|
-
col: number
|
|
32
|
+
name: string
|
|
33
|
+
line: number
|
|
34
|
+
col: number
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
interface Violation {
|
|
38
|
-
frames: Frame[]
|
|
38
|
+
frames: Frame[]
|
|
39
39
|
/** Codeframe anchor (the top frame's position). */
|
|
40
|
-
anchor: Frame
|
|
40
|
+
anchor: Frame
|
|
41
41
|
/**
|
|
42
42
|
* How many component elements wrap the violating JSX usage inside the page. Each wrapper adds one React
|
|
43
43
|
* internal frame to the minified component stack.
|
|
44
44
|
*/
|
|
45
|
-
wrapperDepth: number
|
|
45
|
+
wrapperDepth: number
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
@@ -54,10 +54,10 @@ interface Violation {
|
|
|
54
54
|
export function diagnoseCacheComponentsPrerender(
|
|
55
55
|
input: PrerenderDiagnosticInput,
|
|
56
56
|
): string | undefined {
|
|
57
|
-
const source = readSource(input.pageFile)
|
|
58
|
-
if (!source) return undefined
|
|
59
|
-
const displayPath = appRelativeDisplayPath(input.appPath, input.pageFile)
|
|
60
|
-
const layouts = layoutChainSources(input.appPath, input.pageFile)
|
|
57
|
+
const source = readSource(input.pageFile)
|
|
58
|
+
if (!source) return undefined
|
|
59
|
+
const displayPath = appRelativeDisplayPath(input.appPath, input.pageFile)
|
|
60
|
+
const layouts = layoutChainSources(input.appPath, input.pageFile)
|
|
61
61
|
|
|
62
62
|
// `use cache: private` nesting misuse fails the build before any other
|
|
63
63
|
// analysis — the nesting IS the violation.
|
|
@@ -66,29 +66,29 @@ export function diagnoseCacheComponentsPrerender(
|
|
|
66
66
|
displayPath,
|
|
67
67
|
input.route,
|
|
68
68
|
input.debugPrerender,
|
|
69
|
-
)
|
|
70
|
-
if (privateInUnstable) return privateInUnstable
|
|
69
|
+
)
|
|
70
|
+
if (privateInUnstable) return privateInUnstable
|
|
71
71
|
const privateInCache = detectPrivateInUseCache(
|
|
72
72
|
source,
|
|
73
73
|
displayPath,
|
|
74
74
|
input.route,
|
|
75
75
|
input.debugPrerender,
|
|
76
|
-
)
|
|
77
|
-
if (privateInCache) return privateInCache
|
|
76
|
+
)
|
|
77
|
+
if (privateInCache) return privateInCache
|
|
78
78
|
const nestedCache = detectNestedShortCacheLife(
|
|
79
79
|
source,
|
|
80
80
|
displayPath,
|
|
81
81
|
input.route,
|
|
82
82
|
input.debugPrerender,
|
|
83
|
-
)
|
|
84
|
-
if (nestedCache) return nestedCache
|
|
83
|
+
)
|
|
84
|
+
if (nestedCache) return nestedCache
|
|
85
85
|
|
|
86
86
|
// Request APIs inside a `use cache` scope fail the prerender with their own
|
|
87
87
|
// dedicated message (next-request-in-use-cache) before any blocking-dynamic
|
|
88
88
|
// analysis — the cache scope IS the violation, Suspense can't excuse it.
|
|
89
|
-
const useCache = detectUseCacheViolation(source)
|
|
89
|
+
const useCache = detectUseCacheViolation(source)
|
|
90
90
|
if (useCache) {
|
|
91
|
-
return useCacheBlock(input.route, displayPath, source, useCache, input.debugPrerender)
|
|
91
|
+
return useCacheBlock(input.route, displayPath, source, useCache, input.debugPrerender)
|
|
92
92
|
}
|
|
93
93
|
// Same violation, but the offending `use cache` component is imported from
|
|
94
94
|
// another module (third-party / ignore-listed code).
|
|
@@ -98,8 +98,8 @@ export function diagnoseCacheComponentsPrerender(
|
|
|
98
98
|
displayPath,
|
|
99
99
|
input.route,
|
|
100
100
|
input.debugPrerender,
|
|
101
|
-
)
|
|
102
|
-
if (importedUseCache) return importedUseCache
|
|
101
|
+
)
|
|
102
|
+
if (importedUseCache) return importedUseCache
|
|
103
103
|
|
|
104
104
|
// The page's own cache scope is an uncoverable dynamic hole (short-lived
|
|
105
105
|
// cache / fallback params) — the generic blocking-dynamic diagnostic.
|
|
@@ -109,8 +109,8 @@ export function diagnoseCacheComponentsPrerender(
|
|
|
109
109
|
input.route,
|
|
110
110
|
layouts,
|
|
111
111
|
input.debugPrerender,
|
|
112
|
-
)
|
|
113
|
-
if (pageHole) return pageHole
|
|
112
|
+
)
|
|
113
|
+
if (pageHole) return pageHole
|
|
114
114
|
|
|
115
115
|
// An unguarded Client Component reading a current-time value wins over an
|
|
116
116
|
// unguarded server dynamic access (sync-attribution precedence).
|
|
@@ -120,17 +120,22 @@ export function diagnoseCacheComponentsPrerender(
|
|
|
120
120
|
input.appPath,
|
|
121
121
|
input.route,
|
|
122
122
|
input.debugPrerender,
|
|
123
|
-
)
|
|
124
|
-
if (clientSyncIO) return clientSyncIO
|
|
123
|
+
)
|
|
124
|
+
if (clientSyncIO) return clientSyncIO
|
|
125
125
|
|
|
126
126
|
// A request API read synchronously during the prerender throws a real
|
|
127
127
|
// TypeError (the un-awaited promise has no such method) — reported as a
|
|
128
128
|
// runtime prerender error rather than a blocking-prerender diagnostic.
|
|
129
|
-
const syncRequestApi = detectSyncRequestApi(
|
|
130
|
-
|
|
129
|
+
const syncRequestApi = detectSyncRequestApi(
|
|
130
|
+
source,
|
|
131
|
+
displayPath,
|
|
132
|
+
input.route,
|
|
133
|
+
input.debugPrerender,
|
|
134
|
+
)
|
|
135
|
+
if (syncRequestApi) return syncRequestApi
|
|
131
136
|
|
|
132
|
-
const analysis = analyzePage(source)
|
|
133
|
-
const suspenseAboveBody = layouts.some(layoutWrapsBodyInSuspense)
|
|
137
|
+
const analysis = analyzePage(source)
|
|
138
|
+
const suspenseAboveBody = layouts.some(layoutWrapsBodyInSuspense)
|
|
134
139
|
|
|
135
140
|
// Precedence mirrors Next: a blocking dynamic access outside <Suspense> wins
|
|
136
141
|
// over the metadata/viewport variants; viewport wins over metadata.
|
|
@@ -139,25 +144,25 @@ export function diagnoseCacheComponentsPrerender(
|
|
|
139
144
|
.map(violation =>
|
|
140
145
|
blockingDynamicBlock(input.route, displayPath, source, violation, input.debugPrerender),
|
|
141
146
|
)
|
|
142
|
-
.join('\n')
|
|
147
|
+
.join('\n')
|
|
143
148
|
}
|
|
144
149
|
// Sync IO (Date/Math.random/crypto randomness) during a prerender fails the
|
|
145
150
|
// route with the unstable-value diagnostic regardless of Suspense wrapping.
|
|
146
|
-
const syncIO = detectSyncIO(source)
|
|
151
|
+
const syncIO = detectSyncIO(source)
|
|
147
152
|
if (syncIO) {
|
|
148
|
-
return syncIOBlock(input.route, displayPath, source, syncIO, input.debugPrerender)
|
|
153
|
+
return syncIOBlock(input.route, displayPath, source, syncIO, input.debugPrerender)
|
|
149
154
|
}
|
|
150
155
|
if (analysis.dynamicViewport && !suspenseAboveBody && !analysis.instantFalse) {
|
|
151
|
-
return viewportBlock(input.route)
|
|
156
|
+
return viewportBlock(input.route)
|
|
152
157
|
}
|
|
153
158
|
if (analysis.dynamicMetadata && !analysis.hasSuspendedDynamic) {
|
|
154
|
-
return metadataBlock(input.route)
|
|
159
|
+
return metadataBlock(input.route)
|
|
155
160
|
}
|
|
156
|
-
return undefined
|
|
161
|
+
return undefined
|
|
157
162
|
}
|
|
158
163
|
|
|
159
164
|
function prerenderErrorLine(route: string): string {
|
|
160
|
-
return `Error occurred prerendering page "${route}". Read more: https://nextjs.org/docs/messages/prerender-error
|
|
165
|
+
return `Error occurred prerendering page "${route}". Read more: https://nextjs.org/docs/messages/prerender-error`
|
|
161
166
|
}
|
|
162
167
|
|
|
163
168
|
/**
|
|
@@ -165,7 +170,7 @@ function prerenderErrorLine(route: string): string {
|
|
|
165
170
|
* thrown TypeError) print it before the serialized error, so the footer must not repeat it.
|
|
166
171
|
*/
|
|
167
172
|
export function diagnosticLeadsWithErrorLine(diagnostic: string): boolean {
|
|
168
|
-
return diagnostic.startsWith('Error occurred prerendering page "')
|
|
173
|
+
return diagnostic.startsWith('Error occurred prerendering page "')
|
|
169
174
|
}
|
|
170
175
|
|
|
171
176
|
/** The shared footer after the diagnostic blocks (debug vs minified variant). */
|
|
@@ -174,16 +179,16 @@ export function prerenderFailureFooter(
|
|
|
174
179
|
debugPrerender: boolean,
|
|
175
180
|
omitErrorLine = false,
|
|
176
181
|
): string {
|
|
177
|
-
const pagePath = `${route}/page
|
|
182
|
+
const pagePath = `${route}/page`
|
|
178
183
|
const summary = debugPrerender
|
|
179
184
|
? `> Export encountered errors on 1 path:\n\t${pagePath}: ${route}`
|
|
180
|
-
: `Export encountered an error on ${pagePath}: ${route}, exiting the build
|
|
185
|
+
: `Export encountered an error on ${pagePath}: ${route}, exiting the build.`
|
|
181
186
|
// Debug mode separates the error line from the export summary with a blank
|
|
182
187
|
// line; that blank line survives when the diagnostic already printed the
|
|
183
188
|
// error line itself.
|
|
184
|
-
if (omitErrorLine) return debugPrerender ? `\n${summary}` : summary
|
|
185
|
-
const errorLine = prerenderErrorLine(route)
|
|
186
|
-
return debugPrerender ? `${errorLine}\n\n${summary}` : `${errorLine}\n${summary}
|
|
189
|
+
if (omitErrorLine) return debugPrerender ? `\n${summary}` : summary
|
|
190
|
+
const errorLine = prerenderErrorLine(route)
|
|
191
|
+
return debugPrerender ? `${errorLine}\n\n${summary}` : `${errorLine}\n${summary}`
|
|
187
192
|
}
|
|
188
193
|
|
|
189
194
|
// --- message templates ------------------------------------------------------
|
|
@@ -198,18 +203,18 @@ const BLOCKING_DYNAMIC_WAYS = [
|
|
|
198
203
|
' https://nextjs.org/docs/messages/blocking-prerender-runtime#for-known-params-prerender',
|
|
199
204
|
' - [block] Set `export const unstable_instant = false` to silence this warning and allow a blocking route',
|
|
200
205
|
' https://nextjs.org/docs/messages/blocking-prerender-dynamic#allow-blocking-route',
|
|
201
|
-
].join('\n')
|
|
206
|
+
].join('\n')
|
|
202
207
|
|
|
203
208
|
function blockingDynamicIntro(route: string): string {
|
|
204
209
|
return (
|
|
205
210
|
`Error: Route "${route}": Next.js encountered uncached or runtime data during prerendering.\n\n` +
|
|
206
211
|
'`fetch(...)`, `cookies()`, `headers()`, `params`, `searchParams`, or `connection()` accessed outside of `<Suspense>` prevents the route from being prerendered, blocking the page load and leading to a slower user experience.\n\n' +
|
|
207
212
|
BLOCKING_DYNAMIC_WAYS
|
|
208
|
-
)
|
|
213
|
+
)
|
|
209
214
|
}
|
|
210
215
|
|
|
211
216
|
function debugTrailer(route: string): string {
|
|
212
|
-
return `To debug the issue, start the app in development mode by running \`next dev\`, then open "${route}" in your browser to investigate the error
|
|
217
|
+
return `To debug the issue, start the app in development mode by running \`next dev\`, then open "${route}" in your browser to investigate the error.`
|
|
213
218
|
}
|
|
214
219
|
|
|
215
220
|
function minifiedTrailer(route: string): string {
|
|
@@ -217,7 +222,7 @@ function minifiedTrailer(route: string): string {
|
|
|
217
222
|
'To get a more detailed stack trace and pinpoint the issue, try one of the following:\n' +
|
|
218
223
|
` - Start the app in development mode by running \`next dev\`, then open "${route}" in your browser to investigate the error.\n` +
|
|
219
224
|
' - Rerun the production build with `next build --debug-prerender` to generate better stack traces.'
|
|
220
|
-
)
|
|
225
|
+
)
|
|
221
226
|
}
|
|
222
227
|
|
|
223
228
|
function metadataBlock(route: string): string {
|
|
@@ -231,7 +236,7 @@ function metadataBlock(route: string): string {
|
|
|
231
236
|
' https://nextjs.org/docs/messages/blocking-prerender-metadata-dynamic#cache-the-metadata\n' +
|
|
232
237
|
' - [dynamic] Render a marker component that calls `await connection()` inside `<Suspense>` on the page\n' +
|
|
233
238
|
' https://nextjs.org/docs/messages/blocking-prerender-metadata-dynamic#mark-the-route-as-dynamic'
|
|
234
|
-
)
|
|
239
|
+
)
|
|
235
240
|
}
|
|
236
241
|
|
|
237
242
|
function viewportBlock(route: string): string {
|
|
@@ -245,7 +250,7 @@ function viewportBlock(route: string): string {
|
|
|
245
250
|
' https://nextjs.org/docs/messages/blocking-prerender-viewport-dynamic#cache-the-viewport-data\n' +
|
|
246
251
|
' - [block] Set `export const unstable_instant = false` to silence this warning and allow a blocking route\n' +
|
|
247
252
|
' https://nextjs.org/docs/messages/blocking-prerender-viewport-dynamic#allow-blocking-route'
|
|
248
|
-
)
|
|
253
|
+
)
|
|
249
254
|
}
|
|
250
255
|
|
|
251
256
|
/**
|
|
@@ -263,38 +268,38 @@ function blockingDynamicBlock(
|
|
|
263
268
|
violation: Violation,
|
|
264
269
|
debugPrerender: boolean,
|
|
265
270
|
): string {
|
|
266
|
-
const parts: string[] = [blockingDynamicIntro(route)]
|
|
271
|
+
const parts: string[] = [blockingDynamicIntro(route)]
|
|
267
272
|
if (debugPrerender) {
|
|
268
273
|
for (const frame of violation.frames) {
|
|
269
|
-
parts.push(` at ${frame.name} (webpack:///${displayPath}:${frame.line}:${frame.col})`)
|
|
274
|
+
parts.push(` at ${frame.name} (webpack:///${displayPath}:${frame.line}:${frame.col})`)
|
|
270
275
|
}
|
|
271
|
-
parts.push(codeFrame(source, violation.anchor.line, violation.anchor.col))
|
|
272
|
-
parts.push(debugTrailer(route))
|
|
276
|
+
parts.push(codeFrame(source, violation.anchor.line, violation.anchor.col))
|
|
277
|
+
parts.push(debugTrailer(route))
|
|
273
278
|
} else {
|
|
274
279
|
const dist = (count: number) =>
|
|
275
|
-
Array.from({ length: count }, () => ' at r (.next/server/chunks/main.js:1:1)')
|
|
280
|
+
Array.from({ length: count }, () => ' at r (.next/server/chunks/main.js:1:1)')
|
|
276
281
|
parts.push(
|
|
277
282
|
...dist(10 + violation.wrapperDepth),
|
|
278
283
|
' at main (<anonymous>)',
|
|
279
284
|
' at body (<anonymous>)',
|
|
280
285
|
' at html (<anonymous>)',
|
|
281
286
|
...dist(11),
|
|
282
|
-
)
|
|
283
|
-
parts.push(minifiedTrailer(route))
|
|
287
|
+
)
|
|
288
|
+
parts.push(minifiedTrailer(route))
|
|
284
289
|
}
|
|
285
|
-
return parts.join('\n')
|
|
290
|
+
return parts.join('\n')
|
|
286
291
|
}
|
|
287
292
|
|
|
288
293
|
// --- sync IO (unstable values) ---------------------------------------------
|
|
289
294
|
|
|
290
|
-
type SyncIOFamily = 'current-time' | 'random' | 'crypto'
|
|
295
|
+
type SyncIOFamily = 'current-time' | 'random' | 'crypto'
|
|
291
296
|
|
|
292
297
|
interface SyncIOFinding {
|
|
293
298
|
/** Message label, e.g. `Math.random()` or `require('node:crypto').randomBytes(size)`. */
|
|
294
|
-
label: string
|
|
295
|
-
family: SyncIOFamily
|
|
296
|
-
frames: Frame[]
|
|
297
|
-
anchor: Frame
|
|
299
|
+
label: string
|
|
300
|
+
family: SyncIOFamily
|
|
301
|
+
frames: Frame[]
|
|
302
|
+
anchor: Frame
|
|
298
303
|
}
|
|
299
304
|
|
|
300
305
|
const SYNC_IO_WAYS: Record<SyncIOFamily, string> = {
|
|
@@ -324,7 +329,7 @@ const SYNC_IO_WAYS: Record<SyncIOFamily, string> = {
|
|
|
324
329
|
' - [client] Render the value on the client with `"use client"`',
|
|
325
330
|
' https://nextjs.org/docs/messages/blocking-prerender-crypto#render-on-the-client',
|
|
326
331
|
].join('\n'),
|
|
327
|
-
}
|
|
332
|
+
}
|
|
328
333
|
|
|
329
334
|
/** Node crypto member -> message label ('' keeps the default `(...)` form). */
|
|
330
335
|
const NODE_CRYPTO_LABELS: Record<string, string> = {
|
|
@@ -336,13 +341,13 @@ const NODE_CRYPTO_LABELS: Record<string, string> = {
|
|
|
336
341
|
generateKeySync: "require('node:crypto').generateKeySync(...)",
|
|
337
342
|
generatePrimeSync: "require('node:crypto').generatePrimeSync(...)",
|
|
338
343
|
randomFillSync: "require('node:crypto').randomFillSync(...)",
|
|
339
|
-
}
|
|
344
|
+
}
|
|
340
345
|
|
|
341
346
|
interface SyncIOSite {
|
|
342
347
|
/** 0-based offset of the frame/caret anchor within the searched body. */
|
|
343
|
-
index: number
|
|
344
|
-
label: string
|
|
345
|
-
family: SyncIOFamily
|
|
348
|
+
index: number
|
|
349
|
+
label: string
|
|
350
|
+
family: SyncIOFamily
|
|
346
351
|
}
|
|
347
352
|
|
|
348
353
|
/**
|
|
@@ -354,59 +359,60 @@ interface SyncIOSite {
|
|
|
354
359
|
* owns the frame.
|
|
355
360
|
*/
|
|
356
361
|
function findSyncIOSite(body: string, nodeCrypto: boolean): SyncIOSite | undefined {
|
|
357
|
-
const sites: SyncIOSite[] = []
|
|
362
|
+
const sites: SyncIOSite[] = []
|
|
358
363
|
const add = (index: number, label: string, family: SyncIOFamily) =>
|
|
359
|
-
sites.push({ index, label, family })
|
|
364
|
+
sites.push({ index, label, family })
|
|
360
365
|
for (const m of body.matchAll(/\bDate\s*\.\s*(now)\s*\(/g)) {
|
|
361
|
-
add((m.index ?? 0) + m[0].lastIndexOf('now'), 'Date.now()', 'current-time')
|
|
366
|
+
add((m.index ?? 0) + m[0].lastIndexOf('now'), 'Date.now()', 'current-time')
|
|
362
367
|
}
|
|
363
|
-
for (const m of body.matchAll(/\bnew\s+Date\s*\(/g))
|
|
368
|
+
for (const m of body.matchAll(/\bnew\s+Date\s*\(/g))
|
|
369
|
+
add(m.index ?? 0, 'new Date()', 'current-time')
|
|
364
370
|
for (const m of body.matchAll(/(?<![.\w$])Date\s*\(/g)) {
|
|
365
|
-
const before = body.slice(0, m.index ?? 0)
|
|
366
|
-
if (/\bnew\s*$/.test(before)) continue
|
|
367
|
-
add(m.index ?? 0, 'Date()', 'current-time')
|
|
371
|
+
const before = body.slice(0, m.index ?? 0)
|
|
372
|
+
if (/\bnew\s*$/.test(before)) continue
|
|
373
|
+
add(m.index ?? 0, 'Date()', 'current-time')
|
|
368
374
|
}
|
|
369
375
|
for (const m of body.matchAll(/\bMath\s*\.\s*(random)\s*\(/g)) {
|
|
370
|
-
add((m.index ?? 0) + m[0].lastIndexOf('random'), 'Math.random()', 'random')
|
|
376
|
+
add((m.index ?? 0) + m[0].lastIndexOf('random'), 'Math.random()', 'random')
|
|
371
377
|
}
|
|
372
|
-
const cryptoMembers = Object.keys(NODE_CRYPTO_LABELS).join('|')
|
|
378
|
+
const cryptoMembers = Object.keys(NODE_CRYPTO_LABELS).join('|')
|
|
373
379
|
for (const m of body.matchAll(new RegExp(`\\bcrypto\\s*\\.\\s*(${cryptoMembers})\\s*\\(`, 'g'))) {
|
|
374
|
-
const member = m[1]
|
|
380
|
+
const member = m[1]!
|
|
375
381
|
if (nodeCrypto) {
|
|
376
|
-
const family: SyncIOFamily = member === 'getRandomValues' ? 'crypto' : 'random'
|
|
377
|
-
add(m.index ?? 0, NODE_CRYPTO_LABELS[member]!, family)
|
|
382
|
+
const family: SyncIOFamily = member === 'getRandomValues' ? 'crypto' : 'random'
|
|
383
|
+
add(m.index ?? 0, NODE_CRYPTO_LABELS[member]!, family)
|
|
378
384
|
} else if (member === 'getRandomValues' || member === 'randomUUID') {
|
|
379
385
|
add(
|
|
380
386
|
(m.index ?? 0) + m[0].lastIndexOf(member),
|
|
381
387
|
member === 'getRandomValues' ? 'crypto.getRandomValues()' : 'crypto.randomUUID()',
|
|
382
388
|
'crypto',
|
|
383
|
-
)
|
|
389
|
+
)
|
|
384
390
|
}
|
|
385
391
|
}
|
|
386
392
|
// Date.now() also matches the bare-Date scan guard above; the earliest anchor
|
|
387
393
|
// is the one Next reports. Dedupe overlapping candidates by taking the first.
|
|
388
|
-
sites.sort((a, b) => a.index - b.index)
|
|
389
|
-
return sites[0]
|
|
394
|
+
sites.sort((a, b) => a.index - b.index)
|
|
395
|
+
return sites[0]
|
|
390
396
|
}
|
|
391
397
|
|
|
392
398
|
/** Body has an awaited request/dynamic API BEFORE `index` (allows sync IO). */
|
|
393
399
|
function dynamicAccessBefore(body: string, index: number): boolean {
|
|
394
|
-
const before = body.slice(0, index)
|
|
395
|
-
return /\bawait\s+(?:connection|cookies|headers)\s*\(/.test(before)
|
|
400
|
+
const before = body.slice(0, index)
|
|
401
|
+
return /\bawait\s+(?:connection|cookies|headers)\s*\(/.test(before)
|
|
396
402
|
}
|
|
397
403
|
|
|
398
404
|
function hasUseCachePrologue(body: string): boolean {
|
|
399
|
-
return /^\s*(['"])use cache(?:\s*:\s*[\w-]+)?\1/.test(body)
|
|
405
|
+
return /^\s*(['"])use cache(?:\s*:\s*[\w-]+)?\1/.test(body)
|
|
400
406
|
}
|
|
401
407
|
|
|
402
408
|
/** A `use cache: private` directive prologue (private caches are dynamic holes). */
|
|
403
409
|
function hasPrivateCachePrologue(body: string): boolean {
|
|
404
|
-
return /^\s*(['"])use cache\s*:\s*private\1/.test(body)
|
|
410
|
+
return /^\s*(['"])use cache\s*:\s*private\1/.test(body)
|
|
405
411
|
}
|
|
406
412
|
|
|
407
413
|
/** A plain (non-private) `use cache` / `use cache: remote` prologue. */
|
|
408
414
|
function hasSharedCachePrologue(body: string): boolean {
|
|
409
|
-
return hasUseCachePrologue(body) && !hasPrivateCachePrologue(body)
|
|
415
|
+
return hasUseCachePrologue(body) && !hasPrivateCachePrologue(body)
|
|
410
416
|
}
|
|
411
417
|
|
|
412
418
|
/**
|
|
@@ -415,14 +421,14 @@ function hasSharedCachePrologue(body: string): boolean {
|
|
|
415
421
|
* which option triggered it, or undefined when the cache is prerenderable.
|
|
416
422
|
*/
|
|
417
423
|
function shortCacheLifeKind(body: string): 'expire' | 'revalidate' | undefined {
|
|
418
|
-
const call = /\bcacheLife\s*\(\s*\{([^}]*)\}\s*\)/.exec(body)
|
|
419
|
-
if (!call) return undefined
|
|
420
|
-
const options = call[1]
|
|
421
|
-
const expire = /\bexpire\s*:\s*(\d+)/.exec(options)
|
|
422
|
-
if (expire && Number(expire[1]) < 300) return 'expire'
|
|
423
|
-
const revalidate = /\brevalidate\s*:\s*(\d+)/.exec(options)
|
|
424
|
-
if (revalidate && Number(revalidate[1]) === 0) return 'revalidate'
|
|
425
|
-
return undefined
|
|
424
|
+
const call = /\bcacheLife\s*\(\s*\{([^}]*)\}\s*\)/.exec(body)
|
|
425
|
+
if (!call) return undefined
|
|
426
|
+
const options = call[1]!
|
|
427
|
+
const expire = /\bexpire\s*:\s*(\d+)/.exec(options)
|
|
428
|
+
if (expire && Number(expire[1]) < 300) return 'expire'
|
|
429
|
+
const revalidate = /\brevalidate\s*:\s*(\d+)/.exec(options)
|
|
430
|
+
if (revalidate && Number(revalidate[1]) === 0) return 'revalidate'
|
|
431
|
+
return undefined
|
|
426
432
|
}
|
|
427
433
|
|
|
428
434
|
/**
|
|
@@ -431,41 +437,41 @@ function shortCacheLifeKind(body: string): 'expire' | 'revalidate' | undefined {
|
|
|
431
437
|
* NOT excuse these (unlike blocking dynamic data).
|
|
432
438
|
*/
|
|
433
439
|
function detectSyncIO(source: string): SyncIOFinding | undefined {
|
|
434
|
-
if (/^\s*(['"])use client\1/.test(source)) return undefined
|
|
435
|
-
const functions = collectFunctions(source)
|
|
436
|
-
const byName = new Map(functions.map(fn => [fn.name, fn]))
|
|
437
|
-
const page = byName.get('Page') ?? functions.find(fn => isComponentName(fn.name))
|
|
438
|
-
if (!page) return undefined
|
|
439
|
-
const nodeCrypto = /['"]node:crypto['"]/.test(source)
|
|
440
|
+
if (/^\s*(['"])use client\1/.test(source)) return undefined
|
|
441
|
+
const functions = collectFunctions(source)
|
|
442
|
+
const byName = new Map(functions.map(fn => [fn.name, fn]))
|
|
443
|
+
const page = byName.get('Page') ?? functions.find(fn => isComponentName(fn.name))
|
|
444
|
+
if (!page) return undefined
|
|
445
|
+
const nodeCrypto = /['"]node:crypto['"]/.test(source)
|
|
440
446
|
for (const fn of functions) {
|
|
441
|
-
if (fn === page) continue
|
|
442
|
-
const body = source.slice(fn.bodyStart, fn.bodyEnd)
|
|
443
|
-
if (hasUseCachePrologue(body)) continue
|
|
444
|
-
const site = findSyncIOSite(body, nodeCrypto)
|
|
445
|
-
if (!site || dynamicAccessBefore(body, site.index)) continue
|
|
446
|
-
const anchorIndex = fn.bodyStart + site.index
|
|
447
|
-
const frames: Frame[] = [frameAt(source, anchorIndex, fn.name)]
|
|
448
|
-
let component = fn
|
|
447
|
+
if (fn === page) continue
|
|
448
|
+
const body = source.slice(fn.bodyStart, fn.bodyEnd)
|
|
449
|
+
if (hasUseCachePrologue(body)) continue
|
|
450
|
+
const site = findSyncIOSite(body, nodeCrypto)
|
|
451
|
+
if (!site || dynamicAccessBefore(body, site.index)) continue
|
|
452
|
+
const anchorIndex = fn.bodyStart + site.index
|
|
453
|
+
const frames: Frame[] = [frameAt(source, anchorIndex, fn.name)]
|
|
454
|
+
let component = fn
|
|
449
455
|
if (!isComponentName(fn.name)) {
|
|
450
456
|
// Helper: resolve the calling component for the middle frame.
|
|
451
457
|
const caller = functions.find(candidate => {
|
|
452
|
-
if (!isComponentName(candidate.name)) return false
|
|
453
|
-
const callerBody = source.slice(candidate.bodyStart, candidate.bodyEnd)
|
|
454
|
-
return new RegExp(`\\b${fn.name}\\s*\\(`).test(callerBody)
|
|
455
|
-
})
|
|
456
|
-
if (!caller) continue
|
|
457
|
-
const callerBody = source.slice(caller.bodyStart, caller.bodyEnd)
|
|
458
|
-
const callsite = new RegExp(`\\b${fn.name}\\s*\\(`).exec(callerBody)
|
|
459
|
-
frames.push(frameAt(source, caller.bodyStart + (callsite?.index ?? 0), caller.name))
|
|
460
|
-
component = caller
|
|
458
|
+
if (!isComponentName(candidate.name)) return false
|
|
459
|
+
const callerBody = source.slice(candidate.bodyStart, candidate.bodyEnd)
|
|
460
|
+
return new RegExp(`\\b${fn.name}\\s*\\(`).test(callerBody)
|
|
461
|
+
})
|
|
462
|
+
if (!caller) continue
|
|
463
|
+
const callerBody = source.slice(caller.bodyStart, caller.bodyEnd)
|
|
464
|
+
const callsite = new RegExp(`\\b${fn.name}\\s*\\(`).exec(callerBody)
|
|
465
|
+
frames.push(frameAt(source, caller.bodyStart + (callsite?.index ?? 0), caller.name))
|
|
466
|
+
component = caller
|
|
461
467
|
}
|
|
462
468
|
// Page frame: the JSX usage of the (calling) component.
|
|
463
|
-
const pageBody = source.slice(page.bodyStart, page.bodyEnd)
|
|
464
|
-
const usage = new RegExp(`<${component.name}\\b`).exec(pageBody)
|
|
465
|
-
if (usage) frames.push(frameAt(source, page.bodyStart + usage.index, page.name))
|
|
466
|
-
return { label: site.label, family: site.family, frames, anchor: frames[0]! }
|
|
469
|
+
const pageBody = source.slice(page.bodyStart, page.bodyEnd)
|
|
470
|
+
const usage = new RegExp(`<${component.name}\\b`).exec(pageBody)
|
|
471
|
+
if (usage) frames.push(frameAt(source, page.bodyStart + usage.index, page.name))
|
|
472
|
+
return { label: site.label, family: site.family, frames, anchor: frames[0]! }
|
|
467
473
|
}
|
|
468
|
-
return undefined
|
|
474
|
+
return undefined
|
|
469
475
|
}
|
|
470
476
|
|
|
471
477
|
// --- sync IO in an unguarded Client Component -------------------------------
|
|
@@ -477,7 +483,7 @@ const CLIENT_CURRENT_TIME_WAYS = [
|
|
|
477
483
|
' https://nextjs.org/docs/messages/blocking-prerender-current-time-client#move-into-effect-or-event-handler',
|
|
478
484
|
' - [measure] If the value is for telemetry, use a timing API such as `performance.now()`',
|
|
479
485
|
' https://nextjs.org/docs/messages/blocking-prerender-current-time-client#for-telemetry-use-a-timing-api',
|
|
480
|
-
].join('\n')
|
|
486
|
+
].join('\n')
|
|
481
487
|
|
|
482
488
|
/**
|
|
483
489
|
* A Client Component that reads a current-time value (`new Date()`/`Date.now()`)
|
|
@@ -494,41 +500,46 @@ function detectClientSyncIO(
|
|
|
494
500
|
route: string,
|
|
495
501
|
debugPrerender: boolean,
|
|
496
502
|
): string | undefined {
|
|
497
|
-
const functions = collectFunctions(source)
|
|
498
|
-
const page =
|
|
499
|
-
|
|
500
|
-
|
|
503
|
+
const functions = collectFunctions(source)
|
|
504
|
+
const page =
|
|
505
|
+
functions.find(fn => fn.name === 'Page') ?? functions.find(fn => isComponentName(fn.name))
|
|
506
|
+
if (!page) return undefined
|
|
507
|
+
const pageBody = source.slice(page.bodyStart, page.bodyEnd)
|
|
501
508
|
for (const imp of source.matchAll(
|
|
502
509
|
/import\s+(?:([\w$]+)\s*,?\s*)?(?:\{([^}]*)\})?\s*from\s*(['"])([^'"]+)\3/g,
|
|
503
510
|
)) {
|
|
504
|
-
const specifier = imp[4]
|
|
505
|
-
const names: string[] = []
|
|
506
|
-
if (imp[1]) names.push(imp[1].trim())
|
|
507
|
-
if (imp[2])
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
+
const specifier = imp[4]!
|
|
512
|
+
const names: string[] = []
|
|
513
|
+
if (imp[1]) names.push(imp[1].trim())
|
|
514
|
+
if (imp[2])
|
|
515
|
+
for (const part of imp[2].split(',')) {
|
|
516
|
+
const local = part
|
|
517
|
+
.split(/\bas\b/)
|
|
518
|
+
.pop()!
|
|
519
|
+
.trim()
|
|
520
|
+
if (local) names.push(local)
|
|
521
|
+
}
|
|
511
522
|
for (const name of names) {
|
|
512
|
-
if (!isComponentName(name)) continue
|
|
513
|
-
const usage = new RegExp(`<${name}\\b`).exec(pageBody)
|
|
514
|
-
if (!usage || insideSuspense(pageBody, usage.index)) continue
|
|
515
|
-
const moduleFile = resolveModule(pageFile, specifier)
|
|
516
|
-
if (!moduleFile) continue
|
|
517
|
-
const clientSource = readSource(moduleFile)
|
|
518
|
-
if (!clientSource || !/^\s*(['"])use client\1/.test(clientSource)) continue
|
|
519
|
-
const fn = collectFunctions(clientSource).find(candidate => candidate.name === name)
|
|
520
|
-
if (!fn) continue
|
|
521
|
-
const site = renderTimeSyncIO(clientSource, fn)
|
|
522
|
-
if (!site) continue
|
|
523
|
-
const clientDisplay = appRelativeDisplayPath(appPath, moduleFile)
|
|
524
|
-
const pageDisplay = appRelativeDisplayPath(appPath, pageFile)
|
|
525
|
-
const clientFrame = frameAt(clientSource, fn.bodyStart + site.index, name)
|
|
526
|
-
const pageFrame = frameAt(source, page.bodyStart + usage.index, page.name)
|
|
523
|
+
if (!isComponentName(name)) continue
|
|
524
|
+
const usage = new RegExp(`<${name}\\b`).exec(pageBody)
|
|
525
|
+
if (!usage || insideSuspense(pageBody, usage.index)) continue
|
|
526
|
+
const moduleFile = resolveModule(pageFile, specifier)
|
|
527
|
+
if (!moduleFile) continue
|
|
528
|
+
const clientSource = readSource(moduleFile)
|
|
529
|
+
if (!clientSource || !/^\s*(['"])use client\1/.test(clientSource)) continue
|
|
530
|
+
const fn = collectFunctions(clientSource).find(candidate => candidate.name === name)
|
|
531
|
+
if (!fn) continue
|
|
532
|
+
const site = renderTimeSyncIO(clientSource, fn)
|
|
533
|
+
if (!site) continue
|
|
534
|
+
const clientDisplay = appRelativeDisplayPath(appPath, moduleFile)
|
|
535
|
+
const pageDisplay = appRelativeDisplayPath(appPath, pageFile)
|
|
536
|
+
const clientFrame = frameAt(clientSource, fn.bodyStart + site.index, name)
|
|
537
|
+
const pageFrame = frameAt(source, page.bodyStart + usage.index, page.name)
|
|
527
538
|
const intro =
|
|
528
539
|
`Error: Route "${route}": Next.js encountered the unstable value \`${site.label}\` in a Client Component.\n\n` +
|
|
529
540
|
'This value would be evaluated during the prerender, instead of recomputed on each visit.\n\n' +
|
|
530
541
|
'Ways to fix this:\n' +
|
|
531
|
-
CLIENT_CURRENT_TIME_WAYS
|
|
542
|
+
CLIENT_CURRENT_TIME_WAYS
|
|
532
543
|
if (debugPrerender) {
|
|
533
544
|
return [
|
|
534
545
|
intro,
|
|
@@ -536,12 +547,14 @@ function detectClientSyncIO(
|
|
|
536
547
|
` at ${pageFrame.name} (webpack:///${pageDisplay}:${pageFrame.line}:${pageFrame.col})`,
|
|
537
548
|
codeFrame(clientSource, clientFrame.line, clientFrame.col),
|
|
538
549
|
debugTrailer(route),
|
|
539
|
-
].join('\n')
|
|
550
|
+
].join('\n')
|
|
540
551
|
}
|
|
541
|
-
return [intro, ' at r (.next/server/chunks/main.js:1:1)', minifiedTrailer(route)].join(
|
|
552
|
+
return [intro, ' at r (.next/server/chunks/main.js:1:1)', minifiedTrailer(route)].join(
|
|
553
|
+
'\n',
|
|
554
|
+
)
|
|
542
555
|
}
|
|
543
556
|
}
|
|
544
|
-
return undefined
|
|
557
|
+
return undefined
|
|
545
558
|
}
|
|
546
559
|
|
|
547
560
|
/**
|
|
@@ -549,12 +562,12 @@ function detectClientSyncIO(
|
|
|
549
562
|
* (brace depth 0 — not inside a microtask/effect/event-handler callback).
|
|
550
563
|
*/
|
|
551
564
|
function renderTimeSyncIO(source: string, fn: FunctionInfo): SyncIOSite | undefined {
|
|
552
|
-
const body = source.slice(fn.bodyStart, fn.bodyEnd)
|
|
553
|
-
const site = findSyncIOSite(body, /['"]node:crypto['"]/.test(source))
|
|
554
|
-
if (site?.family !== 'current-time') return undefined
|
|
555
|
-
const before = body.slice(0, site.index)
|
|
556
|
-
const depth = (before.match(/\{/g)?.length ?? 0) - (before.match(/\}/g)?.length ?? 0)
|
|
557
|
-
return depth > 0 ? undefined : site
|
|
565
|
+
const body = source.slice(fn.bodyStart, fn.bodyEnd)
|
|
566
|
+
const site = findSyncIOSite(body, /['"]node:crypto['"]/.test(source))
|
|
567
|
+
if (site?.family !== 'current-time') return undefined
|
|
568
|
+
const before = body.slice(0, site.index)
|
|
569
|
+
const depth = (before.match(/\{/g)?.length ?? 0) - (before.match(/\}/g)?.length ?? 0)
|
|
570
|
+
return depth > 0 ? undefined : site
|
|
558
571
|
}
|
|
559
572
|
|
|
560
573
|
function syncIOBlock(
|
|
@@ -569,18 +582,18 @@ function syncIOBlock(
|
|
|
569
582
|
'This value can change between renders, so it must be either prerendered or computed later.\n\n' +
|
|
570
583
|
'Ways to fix this:\n' +
|
|
571
584
|
SYNC_IO_WAYS[finding.family],
|
|
572
|
-
]
|
|
585
|
+
]
|
|
573
586
|
if (debugPrerender) {
|
|
574
587
|
for (const frame of finding.frames) {
|
|
575
|
-
parts.push(` at ${frame.name} (webpack:///${displayPath}:${frame.line}:${frame.col})`)
|
|
588
|
+
parts.push(` at ${frame.name} (webpack:///${displayPath}:${frame.line}:${frame.col})`)
|
|
576
589
|
}
|
|
577
|
-
parts.push(codeFrame(source, finding.anchor.line, finding.anchor.col))
|
|
578
|
-
parts.push(debugTrailer(route))
|
|
590
|
+
parts.push(codeFrame(source, finding.anchor.line, finding.anchor.col))
|
|
591
|
+
parts.push(debugTrailer(route))
|
|
579
592
|
} else {
|
|
580
|
-
parts.push(' at r (.next/server/chunks/main.js:1:1)')
|
|
581
|
-
parts.push(minifiedTrailer(route))
|
|
593
|
+
parts.push(' at r (.next/server/chunks/main.js:1:1)')
|
|
594
|
+
parts.push(minifiedTrailer(route))
|
|
582
595
|
}
|
|
583
|
-
return parts.join('\n')
|
|
596
|
+
return parts.join('\n')
|
|
584
597
|
}
|
|
585
598
|
|
|
586
599
|
// --- request APIs read synchronously ----------------------------------------
|
|
@@ -601,38 +614,34 @@ function detectSyncRequestApi(
|
|
|
601
614
|
route: string,
|
|
602
615
|
debugPrerender: boolean,
|
|
603
616
|
): string | undefined {
|
|
604
|
-
const apis = 'cookies|headers|draftMode'
|
|
617
|
+
const apis = 'cookies|headers|draftMode'
|
|
605
618
|
// `(<api>() as any).member(` — the optional parens/cast are how the fixtures
|
|
606
619
|
// (and real apps) silence the type error around the sync read.
|
|
607
620
|
const pattern = new RegExp(
|
|
608
621
|
`\\(?\\s*\\b(${apis})\\s*\\(\\s*\\)(?:\\s+as\\s+[\\w<>[\\]|]+)?\\s*\\)?\\s*\\.\\s*(\\w+)\\s*\\(`,
|
|
609
622
|
'g',
|
|
610
|
-
)
|
|
623
|
+
)
|
|
611
624
|
for (const fn of collectFunctions(source)) {
|
|
612
|
-
const body = source.slice(fn.bodyStart, fn.bodyEnd)
|
|
613
|
-
if (hasUseCachePrologue(body)) continue
|
|
625
|
+
const body = source.slice(fn.bodyStart, fn.bodyEnd)
|
|
626
|
+
if (hasUseCachePrologue(body)) continue
|
|
614
627
|
for (const match of body.matchAll(pattern)) {
|
|
615
|
-
const index = match.index ?? 0
|
|
628
|
+
const index = match.index ?? 0
|
|
616
629
|
// `await cookies()` resolves the promise first — the member call is fine.
|
|
617
|
-
if (/\bawait\s*\(?\s*$/.test(body.slice(0, index))) continue
|
|
618
|
-
if (dynamicAccessBefore(body, index)) continue
|
|
619
|
-
const api = match[1]
|
|
620
|
-
const member = match[2]
|
|
621
|
-
const frame = frameAt(
|
|
622
|
-
source,
|
|
623
|
-
fn.bodyStart + index + match[0].lastIndexOf(member),
|
|
624
|
-
fn.name,
|
|
625
|
-
);
|
|
630
|
+
if (/\bawait\s*\(?\s*$/.test(body.slice(0, index))) continue
|
|
631
|
+
if (dynamicAccessBefore(body, index)) continue
|
|
632
|
+
const api = match[1]!
|
|
633
|
+
const member = match[2]!
|
|
634
|
+
const frame = frameAt(source, fn.bodyStart + index + match[0].lastIndexOf(member), fn.name)
|
|
626
635
|
// Webpack's module-namespace call form — the suites normalize it to
|
|
627
636
|
// `<module-function>()`, so only its shape matters.
|
|
628
|
-
const message = `TypeError: (0 , ${debugPrerender ? '_headers' : 'e'}.${api})(...).${member} is not a function
|
|
637
|
+
const message = `TypeError: (0 , ${debugPrerender ? '_headers' : 'e'}.${api})(...).${member} is not a function`
|
|
629
638
|
if (debugPrerender) {
|
|
630
639
|
return [
|
|
631
640
|
prerenderErrorLine(route),
|
|
632
641
|
message,
|
|
633
642
|
` at ${frame.name} (webpack:///${displayPath}:${frame.line}:${frame.col})`,
|
|
634
643
|
codeFrame(source, frame.line, frame.col) + digestTrailer(),
|
|
635
|
-
].join('\n')
|
|
644
|
+
].join('\n')
|
|
636
645
|
}
|
|
637
646
|
// Minified: the throwing frame lives in a dist chunk (no codeframe), with
|
|
638
647
|
// the owner component as the anonymous frame above it.
|
|
@@ -641,25 +650,24 @@ function detectSyncRequestApi(
|
|
|
641
650
|
message,
|
|
642
651
|
' at r (.next/server/chunks/main.js:1:1)',
|
|
643
652
|
` at ${frame.name} (<anonymous>)${digestTrailer()}`,
|
|
644
|
-
].join('\n')
|
|
653
|
+
].join('\n')
|
|
645
654
|
}
|
|
646
655
|
}
|
|
647
|
-
return undefined
|
|
656
|
+
return undefined
|
|
648
657
|
}
|
|
649
658
|
|
|
650
659
|
// --- request APIs inside `use cache` ---------------------------------------
|
|
651
660
|
|
|
652
|
-
type UseCacheApi = 'cookies' | 'headers' | 'connection' | 'draftMode'
|
|
661
|
+
type UseCacheApi = 'cookies' | 'headers' | 'connection' | 'draftMode'
|
|
653
662
|
|
|
654
663
|
interface UseCacheFinding {
|
|
655
|
-
api: UseCacheApi
|
|
656
|
-
frames: Frame[]
|
|
657
|
-
anchor: Frame
|
|
664
|
+
api: UseCacheApi
|
|
665
|
+
frames: Frame[]
|
|
666
|
+
anchor: Frame
|
|
658
667
|
}
|
|
659
668
|
|
|
660
669
|
function useCacheMessage(route: string, api: UseCacheApi): string {
|
|
661
|
-
const seeMore =
|
|
662
|
-
'See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache';
|
|
670
|
+
const seeMore = 'See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache'
|
|
663
671
|
switch (api) {
|
|
664
672
|
case 'cookies':
|
|
665
673
|
case 'headers':
|
|
@@ -668,55 +676,55 @@ function useCacheMessage(route: string, api: UseCacheApi): string {
|
|
|
668
676
|
'Accessing Dynamic data sources inside a cache scope is not supported. ' +
|
|
669
677
|
`If you need this data inside a cached function use \`${api}()\` outside of the cached function and pass the required dynamic data in as an argument. ` +
|
|
670
678
|
seeMore
|
|
671
|
-
)
|
|
679
|
+
)
|
|
672
680
|
case 'connection':
|
|
673
681
|
return (
|
|
674
682
|
`Error: Route ${route} used \`connection()\` inside "use cache". ` +
|
|
675
683
|
'The `connection()` function is used to indicate the subsequent code must only run when there is an actual request, but caches must be able to be produced before a request, so this function is not allowed in this scope. ' +
|
|
676
684
|
seeMore
|
|
677
|
-
)
|
|
685
|
+
)
|
|
678
686
|
case 'draftMode':
|
|
679
687
|
return (
|
|
680
688
|
`Error: Route ${route} used "draftMode().enable()" inside "use cache". ` +
|
|
681
689
|
'The enabled status of `draftMode()` can be read in caches but you must not enable or disable `draftMode()` inside a cache. ' +
|
|
682
690
|
seeMore
|
|
683
|
-
)
|
|
691
|
+
)
|
|
684
692
|
}
|
|
685
693
|
}
|
|
686
694
|
|
|
687
695
|
/** A request API awaited inside a function body carrying a `use cache` prologue. */
|
|
688
696
|
function detectUseCacheViolation(source: string): UseCacheFinding | undefined {
|
|
689
|
-
const functions = collectFunctions(source)
|
|
690
|
-
const byName = new Map(functions.map(fn => [fn.name, fn]))
|
|
691
|
-
const page = byName.get('Page') ?? functions.find(fn => isComponentName(fn.name))
|
|
692
|
-
if (!page) return undefined
|
|
697
|
+
const functions = collectFunctions(source)
|
|
698
|
+
const byName = new Map(functions.map(fn => [fn.name, fn]))
|
|
699
|
+
const page = byName.get('Page') ?? functions.find(fn => isComponentName(fn.name))
|
|
700
|
+
if (!page) return undefined
|
|
693
701
|
for (const fn of functions) {
|
|
694
|
-
if (fn === page) continue
|
|
695
|
-
const body = source.slice(fn.bodyStart, fn.bodyEnd)
|
|
696
|
-
if (!hasUseCachePrologue(body)) continue
|
|
702
|
+
if (fn === page) continue
|
|
703
|
+
const body = source.slice(fn.bodyStart, fn.bodyEnd)
|
|
704
|
+
if (!hasUseCachePrologue(body)) continue
|
|
697
705
|
// `use cache: private` scopes run per-request and MAY read request data.
|
|
698
|
-
if (/^\s*(['"])use cache\s*:\s*private\1/.test(body)) continue
|
|
699
|
-
let api: UseCacheApi | undefined
|
|
700
|
-
let anchorIndex: number | undefined
|
|
701
|
-
const enable = /draftMode\s*\(\s*\)\s*\)?\s*\.\s*(enable)\s*\(/.exec(body)
|
|
706
|
+
if (/^\s*(['"])use cache\s*:\s*private\1/.test(body)) continue
|
|
707
|
+
let api: UseCacheApi | undefined
|
|
708
|
+
let anchorIndex: number | undefined
|
|
709
|
+
const enable = /draftMode\s*\(\s*\)\s*\)?\s*\.\s*(enable)\s*\(/.exec(body)
|
|
702
710
|
if (enable) {
|
|
703
|
-
api = 'draftMode'
|
|
704
|
-
anchorIndex = enable.index + enable[0].lastIndexOf('enable')
|
|
711
|
+
api = 'draftMode'
|
|
712
|
+
anchorIndex = enable.index + enable[0].lastIndexOf('enable')
|
|
705
713
|
} else {
|
|
706
|
-
const request = /\bawait\s+(cookies|headers|connection)\s*(\()/.exec(body)
|
|
714
|
+
const request = /\bawait\s+(cookies|headers|connection)\s*(\()/.exec(body)
|
|
707
715
|
if (request) {
|
|
708
|
-
api = request[1] as UseCacheApi
|
|
709
|
-
anchorIndex = request.index + request[0].length - 1
|
|
716
|
+
api = request[1] as UseCacheApi
|
|
717
|
+
anchorIndex = request.index + request[0].length - 1
|
|
710
718
|
}
|
|
711
719
|
}
|
|
712
|
-
if (api === undefined || anchorIndex === undefined) continue
|
|
713
|
-
const frames: Frame[] = [frameAt(source, fn.bodyStart + anchorIndex, fn.name)]
|
|
714
|
-
const pageBody = source.slice(page.bodyStart, page.bodyEnd)
|
|
715
|
-
const usage = new RegExp(`<${fn.name}\\b`).exec(pageBody)
|
|
716
|
-
if (usage) frames.push(frameAt(source, page.bodyStart + usage.index, page.name))
|
|
717
|
-
return { api, frames, anchor: frames[0]! }
|
|
720
|
+
if (api === undefined || anchorIndex === undefined) continue
|
|
721
|
+
const frames: Frame[] = [frameAt(source, fn.bodyStart + anchorIndex, fn.name)]
|
|
722
|
+
const pageBody = source.slice(page.bodyStart, page.bodyEnd)
|
|
723
|
+
const usage = new RegExp(`<${fn.name}\\b`).exec(pageBody)
|
|
724
|
+
if (usage) frames.push(frameAt(source, page.bodyStart + usage.index, page.name))
|
|
725
|
+
return { api, frames, anchor: frames[0]! }
|
|
718
726
|
}
|
|
719
|
-
return undefined
|
|
727
|
+
return undefined
|
|
720
728
|
}
|
|
721
729
|
|
|
722
730
|
function useCacheBlock(
|
|
@@ -726,21 +734,21 @@ function useCacheBlock(
|
|
|
726
734
|
finding: UseCacheFinding,
|
|
727
735
|
debugPrerender: boolean,
|
|
728
736
|
): string {
|
|
729
|
-
const parts: string[] = [useCacheMessage(route, finding.api)]
|
|
737
|
+
const parts: string[] = [useCacheMessage(route, finding.api)]
|
|
730
738
|
if (debugPrerender) {
|
|
731
739
|
for (const frame of finding.frames) {
|
|
732
|
-
parts.push(` at ${frame.name} (webpack:///${displayPath}:${frame.line}:${frame.col})`)
|
|
740
|
+
parts.push(` at ${frame.name} (webpack:///${displayPath}:${frame.line}:${frame.col})`)
|
|
733
741
|
}
|
|
734
|
-
parts.push(codeFrame(source, finding.anchor.line, finding.anchor.col))
|
|
735
|
-
parts.push(debugTrailer(route))
|
|
742
|
+
parts.push(codeFrame(source, finding.anchor.line, finding.anchor.col))
|
|
743
|
+
parts.push(debugTrailer(route))
|
|
736
744
|
} else {
|
|
737
745
|
// Next's minified use-cache stack keeps two dist frames for the request
|
|
738
746
|
// APIs but only one for the draftMode().enable() variant.
|
|
739
|
-
const count = finding.api === 'draftMode' ? 1 : 2
|
|
740
|
-
for (let i = 0; i < count; i++) parts.push(' at r (.next/server/chunks/main.js:1:1)')
|
|
741
|
-
parts.push(minifiedTrailer(route))
|
|
747
|
+
const count = finding.api === 'draftMode' ? 1 : 2
|
|
748
|
+
for (let i = 0; i < count; i++) parts.push(' at r (.next/server/chunks/main.js:1:1)')
|
|
749
|
+
parts.push(minifiedTrailer(route))
|
|
742
750
|
}
|
|
743
|
-
return parts.join('\n')
|
|
751
|
+
return parts.join('\n')
|
|
744
752
|
}
|
|
745
753
|
|
|
746
754
|
// --- `use cache: private` nesting misuse ------------------------------------
|
|
@@ -750,7 +758,7 @@ function useCacheBlock(
|
|
|
750
758
|
* error's stack. The suite normalizes the numeric digest to `<error-digest>`.
|
|
751
759
|
*/
|
|
752
760
|
function digestTrailer(): string {
|
|
753
|
-
return " {\n digest: '3491756801'\n}"
|
|
761
|
+
return " {\n digest: '3491756801'\n}"
|
|
754
762
|
}
|
|
755
763
|
|
|
756
764
|
/**
|
|
@@ -759,7 +767,7 @@ function digestTrailer(): string {
|
|
|
759
767
|
* declaration line plus the serialized error digest.
|
|
760
768
|
*/
|
|
761
769
|
const PRIVATE_IN_USE_CACHE_MESSAGE =
|
|
762
|
-
'Error: "use cache: private" must not be used within "use cache". It can only be nested inside of another "use cache: private".'
|
|
770
|
+
'Error: "use cache: private" must not be used within "use cache". It can only be nested inside of another "use cache: private".'
|
|
763
771
|
|
|
764
772
|
function detectPrivateInUseCache(
|
|
765
773
|
source: string,
|
|
@@ -767,26 +775,26 @@ function detectPrivateInUseCache(
|
|
|
767
775
|
route: string,
|
|
768
776
|
debugPrerender: boolean,
|
|
769
777
|
): string | undefined {
|
|
770
|
-
const functions = collectFunctions(source)
|
|
778
|
+
const functions = collectFunctions(source)
|
|
771
779
|
const privateFn = functions.find(fn =>
|
|
772
780
|
hasPrivateCachePrologue(source.slice(fn.bodyStart, fn.bodyEnd)),
|
|
773
|
-
)
|
|
774
|
-
if (!privateFn) return undefined
|
|
781
|
+
)
|
|
782
|
+
if (!privateFn) return undefined
|
|
775
783
|
const parent = functions.find(
|
|
776
784
|
fn =>
|
|
777
785
|
fn !== privateFn &&
|
|
778
786
|
hasSharedCachePrologue(source.slice(fn.bodyStart, fn.bodyEnd)) &&
|
|
779
787
|
new RegExp(`<${privateFn.name}\\b`).test(source.slice(fn.bodyStart, fn.bodyEnd)),
|
|
780
|
-
)
|
|
781
|
-
if (!parent) return undefined
|
|
788
|
+
)
|
|
789
|
+
if (!parent) return undefined
|
|
782
790
|
if (debugPrerender) {
|
|
783
|
-
const frame = frameAt(source, privateFn.declIndex, privateFn.name)
|
|
791
|
+
const frame = frameAt(source, privateFn.declIndex, privateFn.name)
|
|
784
792
|
return [
|
|
785
793
|
PRIVATE_IN_USE_CACHE_MESSAGE,
|
|
786
794
|
` at ${frame.name} (webpack:///${displayPath}:${frame.line}:${frame.col})`,
|
|
787
795
|
codeFrame(source, frame.line, frame.col) + digestTrailer(),
|
|
788
796
|
debugTrailer(route),
|
|
789
|
-
].join('\n')
|
|
797
|
+
].join('\n')
|
|
790
798
|
}
|
|
791
799
|
// Minified: the runtime error is logged twice (Next's known double-log), each
|
|
792
800
|
// with a dist frame + a host-anonymous frame carrying the serialized digest.
|
|
@@ -794,8 +802,8 @@ function detectPrivateInUseCache(
|
|
|
794
802
|
PRIVATE_IN_USE_CACHE_MESSAGE,
|
|
795
803
|
' at r (.next/server/chunks/main.js:1:1)',
|
|
796
804
|
` at ${privateFn.name} (<anonymous>)${digestTrailer()}`,
|
|
797
|
-
].join('\n')
|
|
798
|
-
return [`⨯ ${loggedError}`, loggedError, minifiedTrailer(route)].join('\n')
|
|
805
|
+
].join('\n')
|
|
806
|
+
return [`⨯ ${loggedError}`, loggedError, minifiedTrailer(route)].join('\n')
|
|
799
807
|
}
|
|
800
808
|
|
|
801
809
|
/**
|
|
@@ -808,13 +816,13 @@ function detectPrivateInUnstableCache(
|
|
|
808
816
|
route: string,
|
|
809
817
|
debugPrerender: boolean,
|
|
810
818
|
): string | undefined {
|
|
811
|
-
const decl = /\bconst\s+(\w+)\s*=\s*unstable_cache\s*\(\s*/.exec(source)
|
|
812
|
-
if (!decl) return undefined
|
|
813
|
-
const callbackIndex = decl.index + decl[0].length
|
|
814
|
-
const callbackBody = source.slice(callbackIndex, callbackIndex + 400)
|
|
815
|
-
if (!/(['"])use cache\s*:\s*private\1/.test(callbackBody)) return undefined
|
|
816
|
-
const cacheName = decl[1]
|
|
817
|
-
const message = 'Error: "use cache: private" must not be used within `unstable_cache()`.'
|
|
819
|
+
const decl = /\bconst\s+(\w+)\s*=\s*unstable_cache\s*\(\s*/.exec(source)
|
|
820
|
+
if (!decl) return undefined
|
|
821
|
+
const callbackIndex = decl.index + decl[0].length
|
|
822
|
+
const callbackBody = source.slice(callbackIndex, callbackIndex + 400)
|
|
823
|
+
if (!/(['"])use cache\s*:\s*private\1/.test(callbackBody)) return undefined
|
|
824
|
+
const cacheName = decl[1]!
|
|
825
|
+
const message = 'Error: "use cache: private" must not be used within `unstable_cache()`.'
|
|
818
826
|
if (!debugPrerender) {
|
|
819
827
|
// Minified: three dist frames, no codeframe.
|
|
820
828
|
return [
|
|
@@ -823,27 +831,27 @@ function detectPrivateInUnstableCache(
|
|
|
823
831
|
' at r (.next/server/chunks/main.js:1:1)',
|
|
824
832
|
' at r (.next/server/chunks/main.js:1:1)',
|
|
825
833
|
minifiedTrailer(route),
|
|
826
|
-
].join('\n')
|
|
834
|
+
].join('\n')
|
|
827
835
|
}
|
|
828
836
|
|
|
829
|
-
const frames: Frame[] = [frameAt(source, callbackIndex, '<unknown>')]
|
|
837
|
+
const frames: Frame[] = [frameAt(source, callbackIndex, '<unknown>')]
|
|
830
838
|
// The component that awaits the cached function (owner frame above it).
|
|
831
|
-
const functions = collectFunctions(source)
|
|
839
|
+
const functions = collectFunctions(source)
|
|
832
840
|
for (const fn of functions) {
|
|
833
|
-
const body = source.slice(fn.bodyStart, fn.bodyEnd)
|
|
834
|
-
const call = new RegExp(`\\bawait\\s+${cacheName}\\s*\\(`).exec(body)
|
|
841
|
+
const body = source.slice(fn.bodyStart, fn.bodyEnd)
|
|
842
|
+
const call = new RegExp(`\\bawait\\s+${cacheName}\\s*\\(`).exec(body)
|
|
835
843
|
if (call) {
|
|
836
|
-
frames.push(frameAt(source, fn.bodyStart + call.index, `async ${fn.name}`))
|
|
837
|
-
break
|
|
844
|
+
frames.push(frameAt(source, fn.bodyStart + call.index, `async ${fn.name}`))
|
|
845
|
+
break
|
|
838
846
|
}
|
|
839
847
|
}
|
|
840
|
-
const anchor = frames[0]
|
|
848
|
+
const anchor = frames[0]!
|
|
841
849
|
return [
|
|
842
850
|
message,
|
|
843
851
|
...frames.map(f => ` at ${f.name} (webpack:///${displayPath}:${f.line}:${f.col})`),
|
|
844
852
|
codeFrame(source, anchor.line, anchor.col),
|
|
845
853
|
debugTrailer(route),
|
|
846
|
-
].join('\n')
|
|
854
|
+
].join('\n')
|
|
847
855
|
}
|
|
848
856
|
|
|
849
857
|
// --- nested short-lived `use cache` without explicit outer cacheLife ---------
|
|
@@ -859,34 +867,34 @@ function detectNestedShortCacheLife(
|
|
|
859
867
|
route: string,
|
|
860
868
|
debugPrerender: boolean,
|
|
861
869
|
): string | undefined {
|
|
862
|
-
const functions = collectFunctions(source)
|
|
870
|
+
const functions = collectFunctions(source)
|
|
863
871
|
const inner = functions.find(fn => {
|
|
864
|
-
const body = source.slice(fn.bodyStart, fn.bodyEnd)
|
|
865
|
-
return hasSharedCachePrologue(body) && shortCacheLifeKind(body) !== undefined
|
|
866
|
-
})
|
|
867
|
-
if (!inner) return undefined
|
|
868
|
-
const kind = shortCacheLifeKind(source.slice(inner.bodyStart, inner.bodyEnd))
|
|
872
|
+
const body = source.slice(fn.bodyStart, fn.bodyEnd)
|
|
873
|
+
return hasSharedCachePrologue(body) && shortCacheLifeKind(body) !== undefined
|
|
874
|
+
})
|
|
875
|
+
if (!inner) return undefined
|
|
876
|
+
const kind = shortCacheLifeKind(source.slice(inner.bodyStart, inner.bodyEnd))!
|
|
869
877
|
const outer = functions.find(fn => {
|
|
870
|
-
if (fn === inner) return false
|
|
871
|
-
const body = source.slice(fn.bodyStart, fn.bodyEnd)
|
|
878
|
+
if (fn === inner) return false
|
|
879
|
+
const body = source.slice(fn.bodyStart, fn.bodyEnd)
|
|
872
880
|
return (
|
|
873
881
|
hasSharedCachePrologue(body) &&
|
|
874
882
|
!/\bcacheLife\s*\(/.test(body) &&
|
|
875
883
|
new RegExp(`\\b${inner.name}\\s*\\(`).test(body)
|
|
876
|
-
)
|
|
877
|
-
})
|
|
878
|
-
if (!outer) return undefined
|
|
884
|
+
)
|
|
885
|
+
})
|
|
886
|
+
if (!outer) return undefined
|
|
879
887
|
const page = functions.find(fn =>
|
|
880
888
|
new RegExp(`\\bawait\\s+${outer.name}\\s*\\(`).test(source.slice(fn.bodyStart, fn.bodyEnd)),
|
|
881
|
-
)
|
|
882
|
-
if (!page) return undefined
|
|
889
|
+
)
|
|
890
|
+
if (!page) return undefined
|
|
883
891
|
|
|
884
892
|
const message =
|
|
885
893
|
kind === 'expire'
|
|
886
894
|
? 'Error: A "use cache" with short `expire` (under 5 minutes) is nested inside another "use cache" that has no explicit `cacheLife`, which is not allowed during prerendering. Add `cacheLife()` to the outer "use cache" to choose whether it should be prerendered (with longer `expire`) or remain dynamic (with short `expire`). Read more: https://nextjs.org/docs/messages/nested-use-cache-no-explicit-cachelife'
|
|
887
|
-
: 'Error: A "use cache" with zero `revalidate` is nested inside another "use cache" that has no explicit `cacheLife`, which is not allowed during prerendering. Add `cacheLife()` to the outer "use cache" to choose whether it should be prerendered (with non-zero `revalidate`) or remain dynamic (with zero `revalidate`). Read more: https://nextjs.org/docs/messages/nested-use-cache-no-explicit-cachelife'
|
|
895
|
+
: 'Error: A "use cache" with zero `revalidate` is nested inside another "use cache" that has no explicit `cacheLife`, which is not allowed during prerendering. Add `cacheLife()` to the outer "use cache" to choose whether it should be prerendered (with non-zero `revalidate`) or remain dynamic (with zero `revalidate`). Read more: https://nextjs.org/docs/messages/nested-use-cache-no-explicit-cachelife'
|
|
888
896
|
const causeLine =
|
|
889
|
-
' [cause]: Nested dynamic "use cache": This "use cache" has a dynamic cache life that was propagated to its parent.'
|
|
897
|
+
' [cause]: Nested dynamic "use cache": This "use cache" has a dynamic cache life that was propagated to its parent.'
|
|
890
898
|
|
|
891
899
|
if (!debugPrerender) {
|
|
892
900
|
return [
|
|
@@ -898,16 +906,16 @@ function detectNestedShortCacheLife(
|
|
|
898
906
|
' at r (.next/server/chunks/main.js:1:1)',
|
|
899
907
|
'}',
|
|
900
908
|
minifiedTrailer(route),
|
|
901
|
-
].join('\n')
|
|
909
|
+
].join('\n')
|
|
902
910
|
}
|
|
903
911
|
|
|
904
|
-
const pageBody = source.slice(page.bodyStart, page.bodyEnd)
|
|
905
|
-
const awaitCall = new RegExp(`\\bawait\\s+${outer.name}\\s*\\(`).exec(pageBody)
|
|
906
|
-
const pageFrame = frameAt(source, page.bodyStart + awaitCall.index, `async ${page.name}`)
|
|
907
|
-
const innerFrame = frameAt(source, inner.declIndex, inner.name)
|
|
908
|
-
const outerBody = source.slice(outer.bodyStart, outer.bodyEnd)
|
|
909
|
-
const innerCall = new RegExp(`\\b${inner.name}\\s*\\(`).exec(outerBody)
|
|
910
|
-
const outerFrame = frameAt(source, outer.bodyStart + innerCall.index, outer.name)
|
|
912
|
+
const pageBody = source.slice(page.bodyStart, page.bodyEnd)
|
|
913
|
+
const awaitCall = new RegExp(`\\bawait\\s+${outer.name}\\s*\\(`).exec(pageBody)!
|
|
914
|
+
const pageFrame = frameAt(source, page.bodyStart + awaitCall.index, `async ${page.name}`)
|
|
915
|
+
const innerFrame = frameAt(source, inner.declIndex, inner.name)
|
|
916
|
+
const outerBody = source.slice(outer.bodyStart, outer.bodyEnd)
|
|
917
|
+
const innerCall = new RegExp(`\\b${inner.name}\\s*\\(`).exec(outerBody)!
|
|
918
|
+
const outerFrame = frameAt(source, outer.bodyStart + innerCall.index, outer.name)
|
|
911
919
|
|
|
912
920
|
return [
|
|
913
921
|
message,
|
|
@@ -920,7 +928,7 @@ function detectNestedShortCacheLife(
|
|
|
920
928
|
indentLines(codeFrame(source, innerFrame.line, innerFrame.col), ' '),
|
|
921
929
|
'}',
|
|
922
930
|
debugTrailer(route),
|
|
923
|
-
].join('\n')
|
|
931
|
+
].join('\n')
|
|
924
932
|
}
|
|
925
933
|
|
|
926
934
|
/** Prefix every line of `text` with `pad` (for the nested `[cause]` codeframe). */
|
|
@@ -928,7 +936,7 @@ function indentLines(text: string, pad: string): string {
|
|
|
928
936
|
return text
|
|
929
937
|
.split('\n')
|
|
930
938
|
.map(line => pad + line)
|
|
931
|
-
.join('\n')
|
|
939
|
+
.join('\n')
|
|
932
940
|
}
|
|
933
941
|
|
|
934
942
|
// --- request APIs inside a `use cache` imported from another module ----------
|
|
@@ -946,36 +954,43 @@ function detectImportedUseCacheViolation(
|
|
|
946
954
|
route: string,
|
|
947
955
|
debugPrerender: boolean,
|
|
948
956
|
): string | undefined {
|
|
949
|
-
const functions = collectFunctions(source)
|
|
950
|
-
const page =
|
|
951
|
-
|
|
952
|
-
|
|
957
|
+
const functions = collectFunctions(source)
|
|
958
|
+
const page =
|
|
959
|
+
functions.find(fn => fn.name === 'Page') ?? functions.find(fn => isComponentName(fn.name))
|
|
960
|
+
if (!page) return undefined
|
|
961
|
+
const pageBody = source.slice(page.bodyStart, page.bodyEnd)
|
|
953
962
|
// Imported bindings: `import { A, B } from 'spec'` / `import Default from 'spec'`.
|
|
954
|
-
for (const imp of source.matchAll(
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
963
|
+
for (const imp of source.matchAll(
|
|
964
|
+
/import\s+(?:([\w$]+)\s*,?\s*)?(?:\{([^}]*)\})?\s*from\s*(['"])([^'"]+)\3/g,
|
|
965
|
+
)) {
|
|
966
|
+
const specifier = imp[4]!
|
|
967
|
+
const names: string[] = []
|
|
968
|
+
if (imp[1]) names.push(imp[1].trim())
|
|
969
|
+
if (imp[2])
|
|
970
|
+
for (const part of imp[2].split(',')) {
|
|
971
|
+
const local = part
|
|
972
|
+
.split(/\bas\b/)
|
|
973
|
+
.pop()!
|
|
974
|
+
.trim()
|
|
975
|
+
if (local) names.push(local)
|
|
976
|
+
}
|
|
962
977
|
for (const name of names) {
|
|
963
|
-
if (!isComponentName(name)) continue
|
|
964
|
-
const usage = new RegExp(`<${name}\\b`).exec(pageBody)
|
|
965
|
-
if (!usage) continue
|
|
966
|
-
const finding = resolveImportedUseCacheApi(pageFile, specifier, name)
|
|
967
|
-
if (!finding) continue
|
|
968
|
-
const frame = frameAt(source, page.bodyStart + usage.index, page.name)
|
|
978
|
+
if (!isComponentName(name)) continue
|
|
979
|
+
const usage = new RegExp(`<${name}\\b`).exec(pageBody)
|
|
980
|
+
if (!usage) continue
|
|
981
|
+
const finding = resolveImportedUseCacheApi(pageFile, specifier, name)
|
|
982
|
+
if (!finding) continue
|
|
983
|
+
const frame = frameAt(source, page.bodyStart + usage.index, page.name)
|
|
969
984
|
return useCacheBlock(
|
|
970
985
|
route,
|
|
971
986
|
displayPath,
|
|
972
987
|
source,
|
|
973
988
|
{ api: finding, frames: [frame], anchor: frame },
|
|
974
989
|
debugPrerender,
|
|
975
|
-
)
|
|
990
|
+
)
|
|
976
991
|
}
|
|
977
992
|
}
|
|
978
|
-
return undefined
|
|
993
|
+
return undefined
|
|
979
994
|
}
|
|
980
995
|
|
|
981
996
|
/** Resolve `specifier` from `pageFile` and detect a request API used inside a
|
|
@@ -985,69 +1000,69 @@ function resolveImportedUseCacheApi(
|
|
|
985
1000
|
specifier: string,
|
|
986
1001
|
name: string,
|
|
987
1002
|
): UseCacheApi | undefined {
|
|
988
|
-
const moduleFile = resolveModule(pageFile, specifier)
|
|
989
|
-
if (!moduleFile) return undefined
|
|
990
|
-
const moduleSource = readSource(moduleFile)
|
|
991
|
-
if (!moduleSource) return undefined
|
|
992
|
-
const fn = collectFunctions(moduleSource).find(candidate => candidate.name === name)
|
|
993
|
-
if (!fn) return undefined
|
|
994
|
-
const body = moduleSource.slice(fn.bodyStart, fn.bodyEnd)
|
|
995
|
-
if (!hasUseCachePrologue(body) || hasPrivateCachePrologue(body)) return undefined
|
|
996
|
-
if (/draftMode\s*\(\s*\)\s*\)?\s*\.\s*enable\s*\(/.test(body)) return 'draftMode'
|
|
997
|
-
const request = /\bawait\s+(cookies|headers|connection)\s*\(/.exec(body)
|
|
998
|
-
return request ? (request[1] as UseCacheApi) : undefined
|
|
1003
|
+
const moduleFile = resolveModule(pageFile, specifier)
|
|
1004
|
+
if (!moduleFile) return undefined
|
|
1005
|
+
const moduleSource = readSource(moduleFile)
|
|
1006
|
+
if (!moduleSource) return undefined
|
|
1007
|
+
const fn = collectFunctions(moduleSource).find(candidate => candidate.name === name)
|
|
1008
|
+
if (!fn) return undefined
|
|
1009
|
+
const body = moduleSource.slice(fn.bodyStart, fn.bodyEnd)
|
|
1010
|
+
if (!hasUseCachePrologue(body) || hasPrivateCachePrologue(body)) return undefined
|
|
1011
|
+
if (/draftMode\s*\(\s*\)\s*\)?\s*\.\s*enable\s*\(/.test(body)) return 'draftMode'
|
|
1012
|
+
const request = /\bawait\s+(cookies|headers|connection)\s*\(/.exec(body)
|
|
1013
|
+
return request ? (request[1] as UseCacheApi) : undefined
|
|
999
1014
|
}
|
|
1000
1015
|
|
|
1001
1016
|
/** Minimal module resolution: relative paths + bare packages via node_modules. */
|
|
1002
1017
|
function resolveModule(fromFile: string, specifier: string): string | undefined {
|
|
1003
|
-
const exts = ['tsx', 'ts', 'jsx', 'js', 'mjs', 'cjs']
|
|
1018
|
+
const exts = ['tsx', 'ts', 'jsx', 'js', 'mjs', 'cjs']
|
|
1004
1019
|
const tryFile = (base: string): string | undefined => {
|
|
1005
|
-
if (existsSync(base) && !isDirectory(base)) return base
|
|
1006
|
-
for (const ext of exts) if (existsSync(`${base}.${ext}`)) return `${base}.${ext}
|
|
1020
|
+
if (existsSync(base) && !isDirectory(base)) return base
|
|
1021
|
+
for (const ext of exts) if (existsSync(`${base}.${ext}`)) return `${base}.${ext}`
|
|
1007
1022
|
for (const ext of exts) {
|
|
1008
|
-
const index = path.join(base, `index.${ext}`)
|
|
1009
|
-
if (existsSync(index)) return index
|
|
1023
|
+
const index = path.join(base, `index.${ext}`)
|
|
1024
|
+
if (existsSync(index)) return index
|
|
1010
1025
|
}
|
|
1011
|
-
return undefined
|
|
1012
|
-
}
|
|
1026
|
+
return undefined
|
|
1027
|
+
}
|
|
1013
1028
|
if (specifier.startsWith('.')) {
|
|
1014
|
-
return tryFile(path.resolve(path.dirname(fromFile), specifier))
|
|
1029
|
+
return tryFile(path.resolve(path.dirname(fromFile), specifier))
|
|
1015
1030
|
}
|
|
1016
1031
|
// Bare specifier: walk up looking for node_modules/<pkg>.
|
|
1017
1032
|
const [scope, sub] = specifier.startsWith('@')
|
|
1018
1033
|
? [specifier.split('/').slice(0, 2).join('/'), specifier.split('/').slice(2).join('/')]
|
|
1019
|
-
: [specifier.split('/')[0]!, specifier.split('/').slice(1).join('/')]
|
|
1020
|
-
let dir = path.dirname(fromFile)
|
|
1034
|
+
: [specifier.split('/')[0]!, specifier.split('/').slice(1).join('/')]
|
|
1035
|
+
let dir = path.dirname(fromFile)
|
|
1021
1036
|
for (;;) {
|
|
1022
|
-
const pkgDir = path.join(dir, 'node_modules', scope)
|
|
1037
|
+
const pkgDir = path.join(dir, 'node_modules', scope)
|
|
1023
1038
|
if (existsSync(pkgDir)) {
|
|
1024
|
-
if (sub) return tryFile(path.join(pkgDir, sub))
|
|
1025
|
-
const entry = packageEntry(pkgDir)
|
|
1026
|
-
if (entry) return tryFile(path.join(pkgDir, entry)) ?? tryFile(path.join(pkgDir, 'index'))
|
|
1027
|
-
return tryFile(path.join(pkgDir, 'index'))
|
|
1039
|
+
if (sub) return tryFile(path.join(pkgDir, sub))
|
|
1040
|
+
const entry = packageEntry(pkgDir)
|
|
1041
|
+
if (entry) return tryFile(path.join(pkgDir, entry)) ?? tryFile(path.join(pkgDir, 'index'))
|
|
1042
|
+
return tryFile(path.join(pkgDir, 'index'))
|
|
1028
1043
|
}
|
|
1029
|
-
const parent = path.dirname(dir)
|
|
1030
|
-
if (parent === dir) return undefined
|
|
1031
|
-
dir = parent
|
|
1044
|
+
const parent = path.dirname(dir)
|
|
1045
|
+
if (parent === dir) return undefined
|
|
1046
|
+
dir = parent
|
|
1032
1047
|
}
|
|
1033
1048
|
}
|
|
1034
1049
|
|
|
1035
1050
|
function packageEntry(pkgDir: string): string | undefined {
|
|
1036
|
-
const source = readSource(path.join(pkgDir, 'package.json'))
|
|
1037
|
-
if (!source) return undefined
|
|
1051
|
+
const source = readSource(path.join(pkgDir, 'package.json'))
|
|
1052
|
+
if (!source) return undefined
|
|
1038
1053
|
try {
|
|
1039
|
-
const pkg = JSON.parse(source) as { exports?: unknown; main?: string; module?: string }
|
|
1040
|
-
const dot = (pkg.exports as Record<string, unknown> | undefined)?.['.']
|
|
1041
|
-
if (typeof dot === 'string') return dot
|
|
1054
|
+
const pkg = JSON.parse(source) as { exports?: unknown; main?: string; module?: string }
|
|
1055
|
+
const dot = (pkg.exports as Record<string, unknown> | undefined)?.['.']
|
|
1056
|
+
if (typeof dot === 'string') return dot
|
|
1042
1057
|
if (dot && typeof dot === 'object') {
|
|
1043
|
-
const cond = dot as Record<string, unknown
|
|
1058
|
+
const cond = dot as Record<string, unknown>
|
|
1044
1059
|
for (const key of ['import', 'default', 'require', 'node']) {
|
|
1045
|
-
if (typeof cond[key] === 'string') return cond[key]
|
|
1060
|
+
if (typeof cond[key] === 'string') return cond[key]
|
|
1046
1061
|
}
|
|
1047
1062
|
}
|
|
1048
|
-
return pkg.module ?? pkg.main
|
|
1063
|
+
return pkg.module ?? pkg.main
|
|
1049
1064
|
} catch {
|
|
1050
|
-
return undefined
|
|
1065
|
+
return undefined
|
|
1051
1066
|
}
|
|
1052
1067
|
}
|
|
1053
1068
|
|
|
@@ -1066,28 +1081,29 @@ function detectPageDynamicHole(
|
|
|
1066
1081
|
layouts: string[],
|
|
1067
1082
|
debugPrerender: boolean,
|
|
1068
1083
|
): string | undefined {
|
|
1069
|
-
if (layouts.some(layoutWrapsChildrenInSuspense)) return undefined
|
|
1070
|
-
const functions = collectFunctions(source)
|
|
1071
|
-
const page =
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1084
|
+
if (layouts.some(layoutWrapsChildrenInSuspense)) return undefined
|
|
1085
|
+
const functions = collectFunctions(source)
|
|
1086
|
+
const page =
|
|
1087
|
+
functions.find(fn => fn.name === 'Page') ?? functions.find(fn => isComponentName(fn.name))
|
|
1088
|
+
if (!page) return undefined
|
|
1089
|
+
const body = source.slice(page.bodyStart, page.bodyEnd)
|
|
1090
|
+
if (!hasSharedCachePrologue(body)) return undefined
|
|
1091
|
+
const shortCache = shortCacheLifeKind(body) !== undefined
|
|
1076
1092
|
const fallbackParams =
|
|
1077
1093
|
/\bawait\s+params\b/.test(body) &&
|
|
1078
1094
|
/\[[^\]]+\]/.test(route) &&
|
|
1079
|
-
!/\bgenerateStaticParams\b/.test(source)
|
|
1080
|
-
if (!shortCache && !fallbackParams) return undefined
|
|
1081
|
-
const anchor = frameAt(source, page.declIndex, page.name)
|
|
1082
|
-
const violation: Violation = { frames: [anchor], anchor, wrapperDepth: 0 }
|
|
1083
|
-
return blockingDynamicBlock(route, displayPath, source, violation, debugPrerender)
|
|
1095
|
+
!/\bgenerateStaticParams\b/.test(source)
|
|
1096
|
+
if (!shortCache && !fallbackParams) return undefined
|
|
1097
|
+
const anchor = frameAt(source, page.declIndex, page.name)
|
|
1098
|
+
const violation: Violation = { frames: [anchor], anchor, wrapperDepth: 0 }
|
|
1099
|
+
return blockingDynamicBlock(route, displayPath, source, violation, debugPrerender)
|
|
1084
1100
|
}
|
|
1085
1101
|
|
|
1086
1102
|
/** A layout wraps `{children}` inside <Suspense> (the dynamic hole is covered). */
|
|
1087
1103
|
function layoutWrapsChildrenInSuspense(source: string): boolean {
|
|
1088
|
-
const suspense = source.indexOf('<Suspense')
|
|
1089
|
-
const children = source.indexOf('{children}')
|
|
1090
|
-
return suspense !== -1 && children !== -1 && suspense < children
|
|
1104
|
+
const suspense = source.indexOf('<Suspense')
|
|
1105
|
+
const children = source.indexOf('{children}')
|
|
1106
|
+
return suspense !== -1 && children !== -1 && suspense < children
|
|
1091
1107
|
}
|
|
1092
1108
|
|
|
1093
1109
|
// --- codeframe --------------------------------------------------------------
|
|
@@ -1097,100 +1113,98 @@ function layoutWrapsChildrenInSuspense(source: string): boolean {
|
|
|
1097
1113
|
* `maxWidth`; with a piped stdout `process.stdout.columns` is undefined and the
|
|
1098
1114
|
* native renderer's 100-column default applies, ellipsizing longer rows.
|
|
1099
1115
|
*/
|
|
1100
|
-
const CODE_FRAME_MAX_WIDTH = 100
|
|
1116
|
+
const CODE_FRAME_MAX_WIDTH = 100
|
|
1101
1117
|
|
|
1102
1118
|
function clampCodeFrameRow(row: string): string {
|
|
1103
|
-
return row.length <= CODE_FRAME_MAX_WIDTH
|
|
1104
|
-
? row
|
|
1105
|
-
: `${row.slice(0, CODE_FRAME_MAX_WIDTH - 3)}...`;
|
|
1119
|
+
return row.length <= CODE_FRAME_MAX_WIDTH ? row : `${row.slice(0, CODE_FRAME_MAX_WIDTH - 3)}...`
|
|
1106
1120
|
}
|
|
1107
1121
|
|
|
1108
1122
|
/** Babel-style codeframe: 2 context lines above, 3 below, `>` cursor + caret. */
|
|
1109
1123
|
function codeFrame(source: string, line: number, col: number): string {
|
|
1110
|
-
const lines = source.split('\n')
|
|
1111
|
-
const first = Math.max(1, line - 2)
|
|
1112
|
-
const last = Math.min(lines.length, line + 3)
|
|
1113
|
-
const width = String(last).length
|
|
1114
|
-
const out: string[] = []
|
|
1124
|
+
const lines = source.split('\n')
|
|
1125
|
+
const first = Math.max(1, line - 2)
|
|
1126
|
+
const last = Math.min(lines.length, line + 3)
|
|
1127
|
+
const width = String(last).length
|
|
1128
|
+
const out: string[] = []
|
|
1115
1129
|
for (let n = first; n <= last; n += 1) {
|
|
1116
|
-
const cursor = n === line ? '>' : ' '
|
|
1117
|
-
const text = lines[n - 1] ?? ''
|
|
1130
|
+
const cursor = n === line ? '>' : ' '
|
|
1131
|
+
const text = lines[n - 1] ?? ''
|
|
1118
1132
|
// Empty source lines keep a bare `|` gutter (no trailing space), matching
|
|
1119
1133
|
// Next's babel-style codeframe output that the snapshots encode.
|
|
1120
|
-
out.push(clampCodeFrameRow(`${cursor} ${String(n).padStart(width)} |${text ? ` ${text}` : ''}`))
|
|
1134
|
+
out.push(clampCodeFrameRow(`${cursor} ${String(n).padStart(width)} |${text ? ` ${text}` : ''}`))
|
|
1121
1135
|
if (n === line) {
|
|
1122
|
-
out.push(`${' '.repeat(width + 2)} | ${' '.repeat(col - 1)}^`)
|
|
1136
|
+
out.push(`${' '.repeat(width + 2)} | ${' '.repeat(col - 1)}^`)
|
|
1123
1137
|
}
|
|
1124
1138
|
}
|
|
1125
|
-
return out.join('\n')
|
|
1139
|
+
return out.join('\n')
|
|
1126
1140
|
}
|
|
1127
1141
|
|
|
1128
1142
|
// --- static page analysis ---------------------------------------------------
|
|
1129
1143
|
|
|
1130
1144
|
interface PageAnalysis {
|
|
1131
|
-
dynamicMetadata: boolean
|
|
1132
|
-
dynamicViewport: boolean
|
|
1133
|
-
instantFalse: boolean
|
|
1145
|
+
dynamicMetadata: boolean
|
|
1146
|
+
dynamicViewport: boolean
|
|
1147
|
+
instantFalse: boolean
|
|
1134
1148
|
/** The page renders dynamic content, but wrapped in <Suspense> (partial). */
|
|
1135
|
-
hasSuspendedDynamic: boolean
|
|
1136
|
-
violations: Violation[]
|
|
1149
|
+
hasSuspendedDynamic: boolean
|
|
1150
|
+
violations: Violation[]
|
|
1137
1151
|
}
|
|
1138
1152
|
|
|
1139
1153
|
interface FunctionInfo {
|
|
1140
|
-
name: string
|
|
1154
|
+
name: string
|
|
1141
1155
|
/** 0-based index of the declaration START (the `export`/`async`/`function`/`const`). */
|
|
1142
|
-
declIndex: number
|
|
1156
|
+
declIndex: number
|
|
1143
1157
|
/** 0-based index of the function NAME token in the source. */
|
|
1144
|
-
nameIndex: number
|
|
1158
|
+
nameIndex: number
|
|
1145
1159
|
/** 0-based index just past the body's opening brace. */
|
|
1146
|
-
bodyStart: number
|
|
1160
|
+
bodyStart: number
|
|
1147
1161
|
/** 0-based index of the body's closing brace. */
|
|
1148
|
-
bodyEnd: number
|
|
1162
|
+
bodyEnd: number
|
|
1149
1163
|
}
|
|
1150
1164
|
|
|
1151
|
-
const DYNAMIC_APIS = ['cookies', 'headers', 'connection', 'draftMode'] as const
|
|
1165
|
+
const DYNAMIC_APIS = ['cookies', 'headers', 'connection', 'draftMode'] as const
|
|
1152
1166
|
|
|
1153
1167
|
function analyzePage(source: string): PageAnalysis {
|
|
1154
|
-
const functions = collectFunctions(source)
|
|
1155
|
-
const byName = new Map(functions.map(fn => [fn.name, fn]))
|
|
1156
|
-
const instantFalse = /\bexport\s+const\s+unstable_instant\s*=\s*false\b/.test(source)
|
|
1157
|
-
const dynamicMetadata = generatorIsDynamic(source, byName.get('generateMetadata'))
|
|
1158
|
-
const dynamicViewport = generatorIsDynamic(source, byName.get('generateViewport'))
|
|
1159
|
-
|
|
1160
|
-
const page = byName.get('Page') ?? functions.find(fn => isComponentName(fn.name))
|
|
1161
|
-
const violations: Violation[] = []
|
|
1162
|
-
let hasSuspendedDynamic = false
|
|
1168
|
+
const functions = collectFunctions(source)
|
|
1169
|
+
const byName = new Map(functions.map(fn => [fn.name, fn]))
|
|
1170
|
+
const instantFalse = /\bexport\s+const\s+unstable_instant\s*=\s*false\b/.test(source)
|
|
1171
|
+
const dynamicMetadata = generatorIsDynamic(source, byName.get('generateMetadata'))
|
|
1172
|
+
const dynamicViewport = generatorIsDynamic(source, byName.get('generateViewport'))
|
|
1173
|
+
|
|
1174
|
+
const page = byName.get('Page') ?? functions.find(fn => isComponentName(fn.name))
|
|
1175
|
+
const violations: Violation[] = []
|
|
1176
|
+
let hasSuspendedDynamic = false
|
|
1163
1177
|
if (page) {
|
|
1164
|
-
const body = source.slice(page.bodyStart, page.bodyEnd)
|
|
1178
|
+
const body = source.slice(page.bodyStart, page.bodyEnd)
|
|
1165
1179
|
// Direct dynamic await in the page body itself (outside any JSX child).
|
|
1166
1180
|
for (const fn of functions) {
|
|
1167
|
-
if (!isComponentName(fn.name) || fn === page) continue
|
|
1168
|
-
const usageState = componentDynamicState(source, fn, byName)
|
|
1169
|
-
if (usageState === 'static') continue
|
|
1181
|
+
if (!isComponentName(fn.name) || fn === page) continue
|
|
1182
|
+
const usageState = componentDynamicState(source, fn, byName)
|
|
1183
|
+
if (usageState === 'static') continue
|
|
1170
1184
|
// Every JSX usage of this component inside the page body.
|
|
1171
|
-
const usagePattern = new RegExp(`<${fn.name}\\b[^>]*>`, 'g')
|
|
1185
|
+
const usagePattern = new RegExp(`<${fn.name}\\b[^>]*>`, 'g')
|
|
1172
1186
|
for (const usage of body.matchAll(usagePattern)) {
|
|
1173
|
-
const usageIndex = page.bodyStart + (usage.index ?? 0)
|
|
1187
|
+
const usageIndex = page.bodyStart + (usage.index ?? 0)
|
|
1174
1188
|
if (usageState === 'conditional' && /\bcached(?:=\{true\}|\b(?!=))/.test(usage[0])) {
|
|
1175
|
-
continue
|
|
1189
|
+
continue
|
|
1176
1190
|
}
|
|
1177
1191
|
if (insideSuspense(body, usage.index ?? 0)) {
|
|
1178
|
-
hasSuspendedDynamic = true
|
|
1179
|
-
continue
|
|
1192
|
+
hasSuspendedDynamic = true
|
|
1193
|
+
continue
|
|
1180
1194
|
}
|
|
1181
|
-
violations.push(buildViolation(source, fn, byName, page, usageIndex))
|
|
1195
|
+
violations.push(buildViolation(source, fn, byName, page, usageIndex))
|
|
1182
1196
|
}
|
|
1183
1197
|
}
|
|
1184
1198
|
}
|
|
1185
|
-
return { dynamicMetadata, dynamicViewport, instantFalse, hasSuspendedDynamic, violations }
|
|
1199
|
+
return { dynamicMetadata, dynamicViewport, instantFalse, hasSuspendedDynamic, violations }
|
|
1186
1200
|
}
|
|
1187
1201
|
|
|
1188
1202
|
/** generateMetadata/generateViewport is dynamic: awaits without 'use cache'. */
|
|
1189
1203
|
function generatorIsDynamic(source: string, fn: FunctionInfo | undefined): boolean {
|
|
1190
|
-
if (!fn) return false
|
|
1191
|
-
const body = source.slice(fn.bodyStart, fn.bodyEnd)
|
|
1192
|
-
if (/(['"])use cache(?:\s*:\s*[\w-]+)?\1/.test(body)) return false
|
|
1193
|
-
return /\bawait\b/.test(body)
|
|
1204
|
+
if (!fn) return false
|
|
1205
|
+
const body = source.slice(fn.bodyStart, fn.bodyEnd)
|
|
1206
|
+
if (/(['"])use cache(?:\s*:\s*[\w-]+)?\1/.test(body)) return false
|
|
1207
|
+
return /\bawait\b/.test(body)
|
|
1194
1208
|
}
|
|
1195
1209
|
|
|
1196
1210
|
/**
|
|
@@ -1203,23 +1217,26 @@ function componentDynamicState(
|
|
|
1203
1217
|
fn: FunctionInfo,
|
|
1204
1218
|
byName: Map<string, FunctionInfo>,
|
|
1205
1219
|
): 'static' | 'dynamic' | 'conditional' {
|
|
1206
|
-
const body = source.slice(fn.bodyStart, fn.bodyEnd)
|
|
1220
|
+
const body = source.slice(fn.bodyStart, fn.bodyEnd)
|
|
1207
1221
|
// A `use cache: private` scope is treated as dynamic during prerendering, so
|
|
1208
1222
|
// it must sit under a <Suspense> boundary — an unguarded usage is a blocking
|
|
1209
1223
|
// dynamic hole (use-cache-private-without-suspense).
|
|
1210
|
-
if (hasPrivateCachePrologue(body)) return 'dynamic'
|
|
1224
|
+
if (hasPrivateCachePrologue(body)) return 'dynamic'
|
|
1211
1225
|
// A `use cache` scope resolves at buildtime — its awaits are never blocking
|
|
1212
1226
|
// dynamic data (the use-cache request-API check catches the illegal ones).
|
|
1213
|
-
if (hasUseCachePrologue(body)) return 'static'
|
|
1214
|
-
const conditional = /\bcached\s*\?/.test(body)
|
|
1215
|
-
if (directDynamicAwait(body) !== undefined) return 'dynamic'
|
|
1227
|
+
if (hasUseCachePrologue(body)) return 'static'
|
|
1228
|
+
const conditional = /\bcached\s*\?/.test(body)
|
|
1229
|
+
if (directDynamicAwait(body) !== undefined) return 'dynamic'
|
|
1216
1230
|
for (const call of body.matchAll(/\bawait\s+([a-z][\w$]*)\s*\(/g)) {
|
|
1217
|
-
const helper = byName.get(call[1]!)
|
|
1218
|
-
if (
|
|
1219
|
-
|
|
1231
|
+
const helper = byName.get(call[1]!)
|
|
1232
|
+
if (
|
|
1233
|
+
helper &&
|
|
1234
|
+
directDynamicAwait(source.slice(helper.bodyStart, helper.bodyEnd)) !== undefined
|
|
1235
|
+
) {
|
|
1236
|
+
return conditional ? 'conditional' : 'dynamic'
|
|
1220
1237
|
}
|
|
1221
1238
|
}
|
|
1222
|
-
return 'static'
|
|
1239
|
+
return 'static'
|
|
1223
1240
|
}
|
|
1224
1241
|
|
|
1225
1242
|
/**
|
|
@@ -1228,13 +1245,13 @@ function componentDynamicState(
|
|
|
1228
1245
|
* Returns the offset used for frame positions (see frame column rules below).
|
|
1229
1246
|
*/
|
|
1230
1247
|
function directDynamicAwait(body: string): { index: number; kind: 'api' | 'promise' } | undefined {
|
|
1231
|
-
const api = new RegExp(`\\bawait\\s+(?:${DYNAMIC_APIS.join('|')})\\s*(\\()`).exec(body)
|
|
1232
|
-
const fetchCall = /\bawait\s+fetch\s*(\()/.exec(body)
|
|
1248
|
+
const api = new RegExp(`\\bawait\\s+(?:${DYNAMIC_APIS.join('|')})\\s*(\\()`).exec(body)
|
|
1249
|
+
const fetchCall = /\bawait\s+fetch\s*(\()/.exec(body)
|
|
1233
1250
|
const uncachedFetch =
|
|
1234
1251
|
fetchCall && !body.slice(fetchCall.index, fetchCall.index + 300).includes('force-cache')
|
|
1235
1252
|
? fetchCall
|
|
1236
|
-
: null
|
|
1237
|
-
const promise = /\bawait\s+(new\s+Promise)/.exec(body)
|
|
1253
|
+
: null
|
|
1254
|
+
const promise = /\bawait\s+(new\s+Promise)/.exec(body)
|
|
1238
1255
|
const candidates = [
|
|
1239
1256
|
...(api ? [{ index: api.index + api[0].length - 1, kind: 'api' as const }] : []),
|
|
1240
1257
|
...(uncachedFetch
|
|
@@ -1243,9 +1260,9 @@ function directDynamicAwait(body: string): { index: number; kind: 'api' | 'promi
|
|
|
1243
1260
|
...(promise
|
|
1244
1261
|
? [{ index: promise.index + promise[0].indexOf('new'), kind: 'promise' as const }]
|
|
1245
1262
|
: []),
|
|
1246
|
-
]
|
|
1247
|
-
candidates.sort((a, b) => a.index - b.index)
|
|
1248
|
-
return candidates[0]
|
|
1263
|
+
]
|
|
1264
|
+
candidates.sort((a, b) => a.index - b.index)
|
|
1265
|
+
return candidates[0]
|
|
1249
1266
|
}
|
|
1250
1267
|
|
|
1251
1268
|
/**
|
|
@@ -1260,35 +1277,35 @@ function buildViolation(
|
|
|
1260
1277
|
page: FunctionInfo,
|
|
1261
1278
|
usageIndex: number,
|
|
1262
1279
|
): Violation {
|
|
1263
|
-
const frames: Frame[] = []
|
|
1264
|
-
const body = source.slice(component.bodyStart, component.bodyEnd)
|
|
1265
|
-
const direct = directDynamicAwait(body)
|
|
1280
|
+
const frames: Frame[] = []
|
|
1281
|
+
const body = source.slice(component.bodyStart, component.bodyEnd)
|
|
1282
|
+
const direct = directDynamicAwait(body)
|
|
1266
1283
|
const helperCall = [...body.matchAll(/\bawait\s+([a-z][\w$]*)\s*\(/g)].find(call => {
|
|
1267
|
-
const helper = byName.get(call[1]!)
|
|
1284
|
+
const helper = byName.get(call[1]!)
|
|
1268
1285
|
return (
|
|
1269
1286
|
helper && directDynamicAwait(source.slice(helper.bodyStart, helper.bodyEnd)) !== undefined
|
|
1270
|
-
)
|
|
1271
|
-
})
|
|
1287
|
+
)
|
|
1288
|
+
})
|
|
1272
1289
|
if (helperCall && !direct) {
|
|
1273
|
-
const helper = byName.get(helperCall[1]!)
|
|
1274
|
-
const helperBody = source.slice(helper.bodyStart, helper.bodyEnd)
|
|
1275
|
-
const site = directDynamicAwait(helperBody)
|
|
1276
|
-
frames.push(frameAt(source, helper.bodyStart + site.index, helper.name))
|
|
1290
|
+
const helper = byName.get(helperCall[1]!)!
|
|
1291
|
+
const helperBody = source.slice(helper.bodyStart, helper.bodyEnd)
|
|
1292
|
+
const site = directDynamicAwait(helperBody)!
|
|
1293
|
+
frames.push(frameAt(source, helper.bodyStart + site.index, helper.name))
|
|
1277
1294
|
// Callsite frame: position of the callee NAME within the component body.
|
|
1278
1295
|
const calleeIndex =
|
|
1279
|
-
component.bodyStart + (helperCall.index ?? 0) + helperCall[0].indexOf(helperCall[1]!)
|
|
1280
|
-
frames.push(frameAt(source, calleeIndex, component.name))
|
|
1296
|
+
component.bodyStart + (helperCall.index ?? 0) + helperCall[0].indexOf(helperCall[1]!)
|
|
1297
|
+
frames.push(frameAt(source, calleeIndex, component.name))
|
|
1281
1298
|
} else if (direct?.kind === 'api') {
|
|
1282
1299
|
// A direct request-API await (`(await cookies())...`) anchors the await
|
|
1283
1300
|
// call site — Next's owner stack captures the throwing access, not the
|
|
1284
1301
|
// enclosing declaration.
|
|
1285
|
-
frames.push(frameAt(source, component.bodyStart + direct.index, component.name))
|
|
1302
|
+
frames.push(frameAt(source, component.bodyStart + direct.index, component.name))
|
|
1286
1303
|
} else {
|
|
1287
1304
|
// A direct constructed-promise/fetch await anchors the component's
|
|
1288
1305
|
// function-name declaration (metadata-error-route shape).
|
|
1289
|
-
frames.push(frameAt(source, component.nameIndex, component.name))
|
|
1306
|
+
frames.push(frameAt(source, component.nameIndex, component.name))
|
|
1290
1307
|
}
|
|
1291
|
-
frames.push(frameAt(source, usageIndex, page.name))
|
|
1308
|
+
frames.push(frameAt(source, usageIndex, page.name))
|
|
1292
1309
|
return {
|
|
1293
1310
|
frames,
|
|
1294
1311
|
anchor: frames[0]!,
|
|
@@ -1296,7 +1313,7 @@ function buildViolation(
|
|
|
1296
1313
|
source.slice(page.bodyStart, page.bodyEnd),
|
|
1297
1314
|
usageIndex - page.bodyStart,
|
|
1298
1315
|
),
|
|
1299
|
-
}
|
|
1316
|
+
}
|
|
1300
1317
|
}
|
|
1301
1318
|
|
|
1302
1319
|
/**
|
|
@@ -1304,137 +1321,137 @@ function buildViolation(
|
|
|
1304
1321
|
* chain is printed verbatim in the stack, and a Suspense boundary means the usage never reaches this path.
|
|
1305
1322
|
*/
|
|
1306
1323
|
function componentWrapperDepth(body: string, index: number): number {
|
|
1307
|
-
let depth = 0
|
|
1324
|
+
let depth = 0
|
|
1308
1325
|
for (const tag of body.slice(0, index).matchAll(/<(\/?)([A-Z][\w$.]*)\b([^>]*)>/g)) {
|
|
1309
|
-
if (tag[2] === 'Suspense') continue
|
|
1310
|
-
if (tag[1] === '/') depth -= 1
|
|
1311
|
-
else if (!tag[3]!.trimEnd().endsWith('/')) depth += 1
|
|
1326
|
+
if (tag[2] === 'Suspense') continue
|
|
1327
|
+
if (tag[1] === '/') depth -= 1
|
|
1328
|
+
else if (!tag[3]!.trimEnd().endsWith('/')) depth += 1
|
|
1312
1329
|
}
|
|
1313
|
-
return Math.max(0, depth)
|
|
1330
|
+
return Math.max(0, depth)
|
|
1314
1331
|
}
|
|
1315
1332
|
|
|
1316
1333
|
function frameAt(source: string, index: number, name: string): Frame {
|
|
1317
|
-
const before = source.slice(0, index)
|
|
1318
|
-
const line = before.split('\n').length
|
|
1319
|
-
const col = index - before.lastIndexOf('\n')
|
|
1320
|
-
return { name, line, col }
|
|
1334
|
+
const before = source.slice(0, index)
|
|
1335
|
+
const line = before.split('\n').length
|
|
1336
|
+
const col = index - before.lastIndexOf('\n')
|
|
1337
|
+
return { name, line, col }
|
|
1321
1338
|
}
|
|
1322
1339
|
|
|
1323
1340
|
/** Whether a body offset sits inside an open <Suspense> element. */
|
|
1324
1341
|
function insideSuspense(body: string, index: number): boolean {
|
|
1325
|
-
const before = body.slice(0, index)
|
|
1326
|
-
const opens = (before.match(/<Suspense[\s>]/g) ?? []).length
|
|
1327
|
-
const closes = (before.match(/<\/Suspense>/g) ?? []).length
|
|
1328
|
-
return opens > closes
|
|
1342
|
+
const before = body.slice(0, index)
|
|
1343
|
+
const opens = (before.match(/<Suspense[\s>]/g) ?? []).length
|
|
1344
|
+
const closes = (before.match(/<\/Suspense>/g) ?? []).length
|
|
1345
|
+
return opens > closes
|
|
1329
1346
|
}
|
|
1330
1347
|
|
|
1331
1348
|
function isComponentName(name: string): boolean {
|
|
1332
|
-
return /^[A-Z]/.test(name) && name !== 'Fallback'
|
|
1349
|
+
return /^[A-Z]/.test(name) && name !== 'Fallback'
|
|
1333
1350
|
}
|
|
1334
1351
|
|
|
1335
1352
|
/** All named function declarations + const arrow/function initializers. */
|
|
1336
1353
|
function collectFunctions(source: string): FunctionInfo[] {
|
|
1337
|
-
const out: FunctionInfo[] = []
|
|
1354
|
+
const out: FunctionInfo[] = []
|
|
1338
1355
|
const patterns = [
|
|
1339
1356
|
/(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*(?::[^{]+)?\{/g,
|
|
1340
1357
|
/(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?(?:function\s*)?\([^)]*\)\s*(?::[^={]+)?=>?\s*\{/g,
|
|
1341
|
-
]
|
|
1358
|
+
]
|
|
1342
1359
|
for (const pattern of patterns) {
|
|
1343
1360
|
for (const match of source.matchAll(pattern)) {
|
|
1344
|
-
const name = match[1]
|
|
1345
|
-
const matchIndex = match.index ?? 0
|
|
1346
|
-
const nameIndex = matchIndex + match[0].indexOf(name)
|
|
1347
|
-
const bodyStart = matchIndex + match[0].length
|
|
1361
|
+
const name = match[1]!
|
|
1362
|
+
const matchIndex = match.index ?? 0
|
|
1363
|
+
const nameIndex = matchIndex + match[0].indexOf(name)
|
|
1364
|
+
const bodyStart = matchIndex + match[0].length
|
|
1348
1365
|
out.push({
|
|
1349
1366
|
name,
|
|
1350
1367
|
declIndex: matchIndex,
|
|
1351
1368
|
nameIndex,
|
|
1352
1369
|
bodyStart,
|
|
1353
1370
|
bodyEnd: bodyEndFrom(source, bodyStart),
|
|
1354
|
-
})
|
|
1371
|
+
})
|
|
1355
1372
|
}
|
|
1356
1373
|
}
|
|
1357
|
-
return out
|
|
1374
|
+
return out
|
|
1358
1375
|
}
|
|
1359
1376
|
|
|
1360
1377
|
/** Index of the `}` closing the body whose content starts at `offset`. */
|
|
1361
1378
|
function bodyEndFrom(source: string, offset: number): number {
|
|
1362
|
-
let depth = 1
|
|
1363
|
-
let quote: string | undefined
|
|
1379
|
+
let depth = 1
|
|
1380
|
+
let quote: string | undefined
|
|
1364
1381
|
for (let cursor = offset; cursor < source.length; cursor++) {
|
|
1365
|
-
const current = source[cursor]
|
|
1382
|
+
const current = source[cursor]!
|
|
1366
1383
|
if (quote) {
|
|
1367
|
-
if (current === '\\') cursor
|
|
1368
|
-
else if (current === quote) quote = undefined
|
|
1369
|
-
continue
|
|
1384
|
+
if (current === '\\') cursor++
|
|
1385
|
+
else if (current === quote) quote = undefined
|
|
1386
|
+
continue
|
|
1370
1387
|
}
|
|
1371
1388
|
if (current === '/' && source[cursor + 1] === '/') {
|
|
1372
|
-
const newline = source.indexOf('\n', cursor + 2)
|
|
1373
|
-
if (newline === -1) return source.length
|
|
1374
|
-
cursor = newline
|
|
1375
|
-
continue
|
|
1389
|
+
const newline = source.indexOf('\n', cursor + 2)
|
|
1390
|
+
if (newline === -1) return source.length
|
|
1391
|
+
cursor = newline
|
|
1392
|
+
continue
|
|
1376
1393
|
}
|
|
1377
1394
|
if (current === '/' && source[cursor + 1] === '*') {
|
|
1378
|
-
const end = source.indexOf('*/', cursor + 2)
|
|
1379
|
-
if (end === -1) return source.length
|
|
1380
|
-
cursor = end + 1
|
|
1381
|
-
continue
|
|
1395
|
+
const end = source.indexOf('*/', cursor + 2)
|
|
1396
|
+
if (end === -1) return source.length
|
|
1397
|
+
cursor = end + 1
|
|
1398
|
+
continue
|
|
1382
1399
|
}
|
|
1383
1400
|
if (current === '"' || current === "'" || current === '`') {
|
|
1384
|
-
quote = current
|
|
1385
|
-
continue
|
|
1401
|
+
quote = current
|
|
1402
|
+
continue
|
|
1386
1403
|
}
|
|
1387
|
-
if (current === '{') depth
|
|
1388
|
-
else if (current === '}' && --depth === 0) return cursor
|
|
1404
|
+
if (current === '{') depth++
|
|
1405
|
+
else if (current === '}' && --depth === 0) return cursor
|
|
1389
1406
|
}
|
|
1390
|
-
return source.length
|
|
1407
|
+
return source.length
|
|
1391
1408
|
}
|
|
1392
1409
|
|
|
1393
1410
|
// --- layout chain -----------------------------------------------------------
|
|
1394
1411
|
|
|
1395
1412
|
function layoutChainSources(appPath: string, pageFile: string): string[] {
|
|
1396
|
-
const sources: string[] = []
|
|
1397
|
-
let dir = path.dirname(pageFile)
|
|
1413
|
+
const sources: string[] = []
|
|
1414
|
+
let dir = path.dirname(pageFile)
|
|
1398
1415
|
while (dir.startsWith(appPath)) {
|
|
1399
1416
|
for (const ext of ['tsx', 'ts', 'jsx', 'js']) {
|
|
1400
|
-
const candidate = path.join(dir, `layout.${ext}`)
|
|
1417
|
+
const candidate = path.join(dir, `layout.${ext}`)
|
|
1401
1418
|
if (existsSync(candidate)) {
|
|
1402
|
-
const source = readSource(candidate)
|
|
1403
|
-
if (source) sources.push(source)
|
|
1419
|
+
const source = readSource(candidate)
|
|
1420
|
+
if (source) sources.push(source)
|
|
1404
1421
|
}
|
|
1405
1422
|
}
|
|
1406
|
-
if (dir === appPath) break
|
|
1407
|
-
dir = path.dirname(dir)
|
|
1423
|
+
if (dir === appPath) break
|
|
1424
|
+
dir = path.dirname(dir)
|
|
1408
1425
|
}
|
|
1409
|
-
return sources
|
|
1426
|
+
return sources
|
|
1410
1427
|
}
|
|
1411
1428
|
|
|
1412
1429
|
/** A layout wraps <body> inside <Suspense> (the dynamic-shell opt-in). */
|
|
1413
1430
|
function layoutWrapsBodyInSuspense(source: string): boolean {
|
|
1414
|
-
const suspense = source.indexOf('<Suspense')
|
|
1415
|
-
const body = source.indexOf('<body')
|
|
1416
|
-
return suspense !== -1 && body !== -1 && suspense < body
|
|
1431
|
+
const suspense = source.indexOf('<Suspense')
|
|
1432
|
+
const body = source.indexOf('<body')
|
|
1433
|
+
return suspense !== -1 && body !== -1 && suspense < body
|
|
1417
1434
|
}
|
|
1418
1435
|
|
|
1419
1436
|
function readSource(file: string): string | undefined {
|
|
1420
1437
|
try {
|
|
1421
|
-
return readFileSync(file, 'utf8')
|
|
1438
|
+
return readFileSync(file, 'utf8')
|
|
1422
1439
|
} catch {
|
|
1423
|
-
return undefined
|
|
1440
|
+
return undefined
|
|
1424
1441
|
}
|
|
1425
1442
|
}
|
|
1426
1443
|
|
|
1427
1444
|
function isDirectory(file: string): boolean {
|
|
1428
1445
|
try {
|
|
1429
|
-
return statSync(file).isDirectory()
|
|
1446
|
+
return statSync(file).isDirectory()
|
|
1430
1447
|
} catch {
|
|
1431
|
-
return false
|
|
1448
|
+
return false
|
|
1432
1449
|
}
|
|
1433
1450
|
}
|
|
1434
1451
|
|
|
1435
1452
|
function appRelativeDisplayPath(appPath: string, file: string): string {
|
|
1436
|
-
const relative = path.relative(appPath, file).split(path.sep).join('/')
|
|
1437
|
-
return `app/${relative}
|
|
1453
|
+
const relative = path.relative(appPath, file).split(path.sep).join('/')
|
|
1454
|
+
return `app/${relative}`
|
|
1438
1455
|
}
|
|
1439
1456
|
|
|
1440
1457
|
// --- runtime `use cache` error funnel ---------------------------------------
|
|
@@ -1456,14 +1473,14 @@ const CACHE_FLIGHT_FRAMES = [
|
|
|
1456
1473
|
'processFullBinaryRow',
|
|
1457
1474
|
'processBinaryChunk',
|
|
1458
1475
|
'progress',
|
|
1459
|
-
]
|
|
1476
|
+
]
|
|
1460
1477
|
|
|
1461
1478
|
/** React's production redaction text for an error that crossed the RSC boundary. */
|
|
1462
1479
|
export const RSC_REDACTED_RUNTIME_MESSAGE =
|
|
1463
1480
|
'An error occurred in the Server Components render. The specific message is ' +
|
|
1464
1481
|
'omitted in production builds to avoid leaking sensitive details. A digest ' +
|
|
1465
1482
|
'property is included on this error instance which may provide additional ' +
|
|
1466
|
-
'details about the nature of the error.'
|
|
1483
|
+
'details about the nature of the error.'
|
|
1467
1484
|
|
|
1468
1485
|
/**
|
|
1469
1486
|
* The user frames of a runtime `use cache` throw: the leading run of app-code frames (the throw site up
|
|
@@ -1471,22 +1488,22 @@ export const RSC_REDACTED_RUNTIME_MESSAGE =
|
|
|
1471
1488
|
* Runtime/bundler frames end the run - they are replaced by the flight-client tail.
|
|
1472
1489
|
*/
|
|
1473
1490
|
export function cacheRuntimeErrorFrames(stack: string): string[] {
|
|
1474
|
-
const names: string[] = []
|
|
1491
|
+
const names: string[] = []
|
|
1475
1492
|
for (const line of stack.split('\n')) {
|
|
1476
|
-
const match = /^\s+at (.+?) \((.+)\)$/.exec(line)
|
|
1477
|
-
if (!match) continue
|
|
1478
|
-
const [, name, location] = match as unknown as [string, string, string]
|
|
1493
|
+
const match = /^\s+at (.+?) \((.+)\)$/.exec(line)
|
|
1494
|
+
if (!match) continue
|
|
1495
|
+
const [, name, location] = match as unknown as [string, string, string]
|
|
1479
1496
|
if (
|
|
1480
1497
|
location.startsWith('node:') ||
|
|
1481
1498
|
location.includes('/external/') ||
|
|
1482
1499
|
location.includes('/packages/pnext/src/') ||
|
|
1483
1500
|
name === '<anonymous>'
|
|
1484
1501
|
) {
|
|
1485
|
-
break
|
|
1502
|
+
break
|
|
1486
1503
|
}
|
|
1487
|
-
if (names[names.length - 1] !== name) names.push(name)
|
|
1504
|
+
if (names[names.length - 1] !== name) names.push(name)
|
|
1488
1505
|
}
|
|
1489
|
-
return names
|
|
1506
|
+
return names
|
|
1490
1507
|
}
|
|
1491
1508
|
|
|
1492
1509
|
/**
|
|
@@ -1500,9 +1517,9 @@ export function formatCacheRuntimeErrorStack(
|
|
|
1500
1517
|
frameNames: string[],
|
|
1501
1518
|
debugPrerender: boolean,
|
|
1502
1519
|
): string {
|
|
1503
|
-
const frames = debugPrerender ? [...frameNames, ...CACHE_FLIGHT_FRAMES] : ['r', 'r']
|
|
1520
|
+
const frames = debugPrerender ? [...frameNames, ...CACHE_FLIGHT_FRAMES] : ['r', 'r']
|
|
1504
1521
|
return [
|
|
1505
1522
|
`Error: ${message}`,
|
|
1506
1523
|
...frames.map(name => ` at ${name} (.next/server/chunks/main.js:1:1)`),
|
|
1507
|
-
].join('\n')
|
|
1524
|
+
].join('\n')
|
|
1508
1525
|
}
|