@wular/pnext 0.0.1
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 +153 -0
- package/bin/pnext +67 -0
- package/config/lint/base.js +48 -0
- package/config/ts/base.json +26 -0
- package/config/ts/react.json +13 -0
- package/package.json +70 -0
- package/reference/compat.md +63 -0
- package/reference/config.md +120 -0
- package/reference/css.md +58 -0
- package/reference/dev.md +69 -0
- package/reference/env.md +40 -0
- package/reference/metadata.md +86 -0
- package/reference/navigation.md +149 -0
- package/reference/overview.md +35 -0
- package/reference/performance.md +97 -0
- package/reference/rendering.md +127 -0
- package/reference/routing.md +167 -0
- package/reference/typegen.md +64 -0
- package/src/api/cache.ts +80 -0
- package/src/api/client-cache.ts +9 -0
- package/src/api/client-navigation.ts +279 -0
- package/src/api/dynamic.tsx +102 -0
- package/src/api/link.tsx +119 -0
- package/src/api/navigation.ts +198 -0
- package/src/api/router/events.ts +53 -0
- package/src/api/router/history.ts +70 -0
- package/src/api/router/hub.ts +194 -0
- package/src/api/router/policies.ts +107 -0
- package/src/api/router/runtime.ts +5238 -0
- package/src/api/router/types.ts +299 -0
- package/src/api/router.ts +167 -0
- package/src/api/server.ts +323 -0
- package/src/api/suspense.ts +16 -0
- package/src/cache/context.ts +61 -0
- package/src/cli/adapters/vercel-warm.ts +375 -0
- package/src/cli/adapters/vercel.ts +1310 -0
- package/src/cli/analyze-print.ts +181 -0
- package/src/cli/analyze.ts +328 -0
- package/src/cli/boot-trace.ts +29 -0
- package/src/cli/build.ts +3114 -0
- package/src/cli/dev.ts +276 -0
- package/src/cli/index.ts +196 -0
- package/src/cli/named-bin.ts +119 -0
- package/src/cli/serve-ui.ts +160 -0
- package/src/cli/start.ts +1425 -0
- package/src/client/build.ts +2136 -0
- package/src/client/chunk-fold.ts +526 -0
- package/src/client/entry.ts +1525 -0
- package/src/client/paths.ts +22 -0
- package/src/client/prebuilt.ts +621 -0
- package/src/client/profile.ts +75 -0
- package/src/client/react-compiler.ts +94 -0
- package/src/client/reference-stub.ts +145 -0
- package/src/client/reference.ts +47 -0
- package/src/compat/actions/action-client.ts +531 -0
- package/src/compat/actions/action-dispatch.ts +676 -0
- package/src/compat/actions/action-router.ts +40 -0
- package/src/compat/actions/action-shared.ts +95 -0
- package/src/compat/actions/client-plugin.ts +135 -0
- package/src/compat/actions/client-stub.ts +65 -0
- package/src/compat/actions/config.ts +164 -0
- package/src/compat/actions/detect.ts +208 -0
- package/src/compat/actions/discovery.ts +244 -0
- package/src/compat/actions/early-submit.ts +40 -0
- package/src/compat/actions/endpoint.ts +602 -0
- package/src/compat/actions/flight.ts +52 -0
- package/src/compat/actions/form-state.ts +73 -0
- package/src/compat/actions/hoist.ts +485 -0
- package/src/compat/actions/ids.ts +39 -0
- package/src/compat/actions/index.ts +41 -0
- package/src/compat/actions/instances.ts +144 -0
- package/src/compat/actions/origin.ts +109 -0
- package/src/compat/actions/protocol.ts +125 -0
- package/src/compat/actions/registry.ts +74 -0
- package/src/compat/actions/rewrite.ts +282 -0
- package/src/compat/actions/serve.ts +414 -0
- package/src/compat/actions/server-tag.ts +21 -0
- package/src/compat/actions/unrecognized-error.ts +30 -0
- package/src/compat/adapter/build-complete.ts +257 -0
- package/src/compat/bundler/bun-externals.ts +53 -0
- package/src/compat/bundler/cjs-exports.ts +542 -0
- package/src/compat/bundler/config.ts +363 -0
- package/src/compat/bundler/externals.ts +34 -0
- package/src/compat/bundler/import-meta-url.ts +60 -0
- package/src/compat/bundler/modularize-imports.ts +119 -0
- package/src/compat/bundler/new-url-asset.ts +87 -0
- package/src/compat/bundler/optimize-package-imports.ts +273 -0
- package/src/compat/bundler/polyfill.ts +88 -0
- package/src/compat/bundler/react-compiler.ts +61 -0
- package/src/compat/bundler/react-profiler.tsx +25 -0
- package/src/compat/bundler/relay-transform.ts +116 -0
- package/src/compat/bundler/require-context.ts +281 -0
- package/src/compat/bundler/resolve-extensions.ts +75 -0
- package/src/compat/bundler/source-cache.ts +61 -0
- package/src/compat/bundler/static-imports.ts +25 -0
- package/src/compat/bundler/symlink-imports.ts +119 -0
- package/src/compat/bundler/tsconfig-paths.ts +50 -0
- package/src/compat/bundler/wasm.ts +153 -0
- package/src/compat/bundler/webpack-loaders.ts +685 -0
- package/src/compat/bundler/worker.ts +278 -0
- package/src/compat/cache/build-flags.ts +81 -0
- package/src/compat/cache/build-prerender-errors.ts +163 -0
- package/src/compat/cache/custom-handler.ts +159 -0
- package/src/compat/cache/fetch-patch.ts +745 -0
- package/src/compat/cache/handler.ts +100 -0
- package/src/compat/cache/modern-handler.ts +275 -0
- package/src/compat/cache/resume-data-cache.ts +143 -0
- package/src/compat/cache/revalidate.ts +759 -0
- package/src/compat/cache/runtime-error.ts +124 -0
- package/src/compat/cache/use-cache-transform.ts +961 -0
- package/src/compat/cache/use-cache.ts +1695 -0
- package/src/compat/cache-control.ts +269 -0
- package/src/compat/client/base-path.ts +64 -0
- package/src/compat/client/css-order.ts +36 -0
- package/src/compat/client/errors/control-flow.ts +92 -0
- package/src/compat/client/errors/error-boundary.ts +222 -0
- package/src/compat/client/errors/global-error.ts +238 -0
- package/src/compat/client/errors/install.ts +217 -0
- package/src/compat/client/errors/lazy.ts +53 -0
- package/src/compat/client/errors/primitive-throw.ts +126 -0
- package/src/compat/client/errors/soft-refresh.ts +14 -0
- package/src/compat/client/link-status.ts +86 -0
- package/src/compat/client/nav-compat-runtime.ts +57 -0
- package/src/compat/client/nav-compat.ts +42 -0
- package/src/compat/client/navigation-scroll.ts +154 -0
- package/src/compat/client/optimistic-routing.ts +206 -0
- package/src/compat/client/prefetch-cache.ts +111 -0
- package/src/compat/client/route-announcer.ts +72 -0
- package/src/compat/client/segment-cache-policy.ts +159 -0
- package/src/compat/client/segment-cache.ts +1077 -0
- package/src/compat/client/segment-prefetch.ts +375 -0
- package/src/compat/client/trailing-slash.ts +24 -0
- package/src/compat/css/chunking.ts +254 -0
- package/src/compat/css/inline-css.ts +73 -0
- package/src/compat/css/lightningcss.ts +90 -0
- package/src/compat/css/modules.ts +373 -0
- package/src/compat/css/nonce.ts +30 -0
- package/src/compat/css/sass-plugin.ts +65 -0
- package/src/compat/css/sass.ts +392 -0
- package/src/compat/css/styled-jsx-runtime.ts +80 -0
- package/src/compat/css/styled-jsx.ts +49 -0
- package/src/compat/edge-runtime.ts +71 -0
- package/src/compat/export/client.ts +112 -0
- package/src/compat/export/index.ts +272 -0
- package/src/compat/export/standalone.ts +207 -0
- package/src/compat/image-optimizer/cache.ts +119 -0
- package/src/compat/image-optimizer/detect.ts +143 -0
- package/src/compat/image-optimizer/index.ts +601 -0
- package/src/compat/image-optimizer/source.ts +243 -0
- package/src/compat/index.ts +458 -0
- package/src/compat/lifecycle/after-scope.ts +86 -0
- package/src/compat/lifecycle/after.ts +173 -0
- package/src/compat/lifecycle/error-funnel.ts +306 -0
- package/src/compat/lifecycle/error-serialize.ts +87 -0
- package/src/compat/lifecycle/error-ui.ts +167 -0
- package/src/compat/lifecycle/instrumentation-client.ts +138 -0
- package/src/compat/lifecycle/instrumentation.ts +277 -0
- package/src/compat/lifecycle/node-console.ts +19 -0
- package/src/compat/lifecycle/testmode.ts +263 -0
- package/src/compat/mdx/compile.ts +219 -0
- package/src/compat/mdx/next-mdx-stub.ts +46 -0
- package/src/compat/mdx/plugin.ts +37 -0
- package/src/compat/metadata-route-artifacts.ts +458 -0
- package/src/compat/metadata.ts +295 -0
- package/src/compat/middleware/manifest.ts +210 -0
- package/src/compat/misc/action-return.ts +173 -0
- package/src/compat/next/cache.ts +211 -0
- package/src/compat/next/canonical-url.ts +35 -0
- package/src/compat/next/client-cache.ts +57 -0
- package/src/compat/next/client-navigation.ts +313 -0
- package/src/compat/next/client-only.ts +3 -0
- package/src/compat/next/client-script.tsx +215 -0
- package/src/compat/next/client-server.ts +39 -0
- package/src/compat/next/config-loader.ts +569 -0
- package/src/compat/next/config.ts +29 -0
- package/src/compat/next/constants.cjs +6 -0
- package/src/compat/next/constants.ts +6 -0
- package/src/compat/next/custom-server.ts +236 -0
- package/src/compat/next/dist/client/components/app-router-headers.ts +32 -0
- package/src/compat/next/dist/server/app-render/work-unit-async-storage.external.cjs +38 -0
- package/src/compat/next/dist/server/web/spec-extension/revalidate.ts +1 -0
- package/src/compat/next/dist/server/web/spec-extension/unstable-cache.ts +1 -0
- package/src/compat/next/dist/server/web/spec-extension/unstable-no-store.ts +1 -0
- package/src/compat/next/dynamic.tsx +46 -0
- package/src/compat/next/error.tsx +148 -0
- package/src/compat/next/font/cache.ts +171 -0
- package/src/compat/next/font/google.ts +2 -0
- package/src/compat/next/font/index.ts +8 -0
- package/src/compat/next/font/local.ts +5 -0
- package/src/compat/next/font/runtime-client.ts +71 -0
- package/src/compat/next/font/runtime.ts +974 -0
- package/src/compat/next/font/shared.ts +281 -0
- package/src/compat/next/form.tsx +156 -0
- package/src/compat/next/head.tsx +10 -0
- package/src/compat/next/headers.ts +247 -0
- package/src/compat/next/image/config.ts +196 -0
- package/src/compat/next/image/optimizer.ts +96 -0
- package/src/compat/next/image/patterns.ts +103 -0
- package/src/compat/next/image/shared.ts +141 -0
- package/src/compat/next/image/static-metadata.ts +283 -0
- package/src/compat/next/image/validate.ts +269 -0
- package/src/compat/next/image-client.tsx +215 -0
- package/src/compat/next/image-props.ts +575 -0
- package/src/compat/next/image-usage.ts +102 -0
- package/src/compat/next/image.tsx +56 -0
- package/src/compat/next/index.ts +1 -0
- package/src/compat/next/legacy-image.tsx +97 -0
- package/src/compat/next/link-usage.ts +29 -0
- package/src/compat/next/link-validation-transform.ts +200 -0
- package/src/compat/next/link.tsx +466 -0
- package/src/compat/next/navigation.cjs +21 -0
- package/src/compat/next/navigation.ts +188 -0
- package/src/compat/next/offline.ts +51 -0
- package/src/compat/next/og.ts +324 -0
- package/src/compat/next/optimistic-route-state.ts +188 -0
- package/src/compat/next/preferred-region.ts +39 -0
- package/src/compat/next/redirects.ts +131 -0
- package/src/compat/next/resource-hints.ts +136 -0
- package/src/compat/next/rewrites.ts +350 -0
- package/src/compat/next/root-params.ts +142 -0
- package/src/compat/next/router.cjs +49 -0
- package/src/compat/next/router.ts +143 -0
- package/src/compat/next/script.tsx +355 -0
- package/src/compat/next/server-only.ts +3 -0
- package/src/compat/next/server.ts +28 -0
- package/src/compat/next/svgr.ts +58 -0
- package/src/compat/next/telemetry.ts +77 -0
- package/src/compat/next/user-agent.ts +100 -0
- package/src/compat/next/web-vitals.ts +56 -0
- package/src/compat/otel/api.ts +95 -0
- package/src/compat/otel/client-trace-metadata.ts +71 -0
- package/src/compat/otel/fetch-span.ts +77 -0
- package/src/compat/otel/tracer.ts +944 -0
- package/src/compat/pages/client-plugin.ts +108 -0
- package/src/compat/pages/index.ts +527 -0
- package/src/compat/pages/router-state.ts +94 -0
- package/src/compat/ppr/io.ts +38 -0
- package/src/compat/ppr/missing-root-params.ts +105 -0
- package/src/compat/ppr/root-params-scan.ts +164 -0
- package/src/compat/ppr/root-params-transform.ts +75 -0
- package/src/compat/ppr/root-params.ts +129 -0
- package/src/compat/ppr/segment-config-incompat.ts +34 -0
- package/src/compat/protocol.ts +202 -0
- package/src/compat/react/client.ts +59 -0
- package/src/compat/react/compiler-runtime.ts +60 -0
- package/src/compat/react/dom-client.ts +115 -0
- package/src/compat/react/dom-react-server.ts +20 -0
- package/src/compat/react/dom-server.ts +40 -0
- package/src/compat/react/dom.ts +154 -0
- package/src/compat/react/preact.ts +522 -0
- package/src/compat/react/react-server.ts +84 -0
- package/src/compat/react/router-shim.ts +26 -0
- package/src/compat/react/server-component-use.ts +48 -0
- package/src/compat/react/server-inserted-html.ts +87 -0
- package/src/compat/react/server.ts +156 -0
- package/src/compat/react/view-transition.ts +60 -0
- package/src/compat/register/actions.ts +875 -0
- package/src/compat/register/boot.ts +141 -0
- package/src/compat/register/build-tier.ts +11 -0
- package/src/compat/register/build.ts +182 -0
- package/src/compat/register/bundler.ts +587 -0
- package/src/compat/register/cache.ts +85 -0
- package/src/compat/register/client-errors.ts +18 -0
- package/src/compat/register/config.ts +17 -0
- package/src/compat/register/css-extras.ts +120 -0
- package/src/compat/register/edge-runtime.ts +6 -0
- package/src/compat/register/errors.ts +46 -0
- package/src/compat/register/export.ts +23 -0
- package/src/compat/register/font.ts +36 -0
- package/src/compat/register/hooks.ts +34 -0
- package/src/compat/register/image.ts +133 -0
- package/src/compat/register/index.ts +111 -0
- package/src/compat/register/instrumentation-client.ts +35 -0
- package/src/compat/register/lifecycle.ts +86 -0
- package/src/compat/register/mdx.ts +48 -0
- package/src/compat/register/middleware.ts +36 -0
- package/src/compat/register/misc.ts +44 -0
- package/src/compat/register/otel.ts +288 -0
- package/src/compat/register/pages-api.ts +473 -0
- package/src/compat/register/ppr.ts +56 -0
- package/src/compat/register/protocol.ts +57 -0
- package/src/compat/register/proxy.ts +127 -0
- package/src/compat/register/render.ts +268 -0
- package/src/compat/register/routing.ts +410 -0
- package/src/compat/register/segment.ts +1903 -0
- package/src/compat/register/static-image.ts +21 -0
- package/src/compat/register/typed-routes.ts +35 -0
- package/src/compat/register/usecache.ts +131 -0
- package/src/compat/register/validation.ts +56 -0
- package/src/compat/segment/loading-boundary.ts +113 -0
- package/src/compat/segment/page-slot.ts +200 -0
- package/src/compat/segment/tree.ts +481 -0
- package/src/compat/segment/vary-key.ts +102 -0
- package/src/compat/segment/vary-params.ts +551 -0
- package/src/compat/static-params.ts +33 -0
- package/src/compat/tsconfig-defaults.ts +301 -0
- package/src/compat/typecheck/index.ts +1481 -0
- package/src/compat/typecheck/worker.ts +26 -0
- package/src/compat/typed-routes/index.ts +92 -0
- package/src/compat/typed-routes/manifest.ts +356 -0
- package/src/compat/typed-routes/typegen.ts +566 -0
- package/src/compat/validation/errors.ts +159 -0
- package/src/compat/validation/index.ts +1770 -0
- package/src/compat/validation/prerender-diagnostics.ts +1508 -0
- package/src/compat-bootstrap.ts +67 -0
- package/src/config.ts +218 -0
- package/src/css/build.ts +697 -0
- package/src/css/index.ts +2 -0
- package/src/css/postcss.ts +236 -0
- package/src/css/worker.ts +34 -0
- package/src/dev/client-actions.ts +35 -0
- package/src/dev/client-chunk-store.ts +92 -0
- package/src/dev/client-key-cache.ts +178 -0
- package/src/dev/global-css-cache.ts +212 -0
- package/src/dev/imports.ts +2430 -0
- package/src/dev/module-cache.ts +721 -0
- package/src/dev/module-generations.ts +38 -0
- package/src/dev/module-transform.ts +188 -0
- package/src/dev/node-module-bundle-cache.ts +63 -0
- package/src/dev/restart-cache.ts +10 -0
- package/src/dev/route-bundle-key-cache.ts +154 -0
- package/src/dev/route-facts-cache.ts +223 -0
- package/src/dev/server.ts +1710 -0
- package/src/dynamic/source.ts +307 -0
- package/src/dynamic/tree-shake.ts +262 -0
- package/src/env.ts +92 -0
- package/src/extensions.ts +1898 -0
- package/src/index.ts +34 -0
- package/src/internal.ts +43 -0
- package/src/islands/boundary-error.ts +8 -0
- package/src/islands/static-children.ts +37 -0
- package/src/islands/static-slots.ts +106 -0
- package/src/ppr-postpone.ts +24 -0
- package/src/ppr.ts +784 -0
- package/src/proxy.ts +752 -0
- package/src/render/hooks.ts +384 -0
- package/src/render/index.ts +1 -0
- package/src/render/island-context.ts +47 -0
- package/src/render/metadata.ts +857 -0
- package/src/render/renderer.ts +7391 -0
- package/src/render/resource-hints.ts +44 -0
- package/src/render/slots.tsx +679 -0
- package/src/request/context.ts +396 -0
- package/src/resolve/engine.ts +219 -0
- package/src/resolve/imports.ts +1104 -0
- package/src/resolve/scan-facts.ts +474 -0
- package/src/resolve/source-text.ts +86 -0
- package/src/routing/forwarded.ts +41 -0
- package/src/routing/handler.ts +271 -0
- package/src/routing/href.ts +203 -0
- package/src/routing/metadata.ts +1018 -0
- package/src/routing/request-runtime.ts +43 -0
- package/src/routing/routes.ts +2560 -0
- package/src/routing/slots.ts +432 -0
- package/src/runtime/server.ts +3453 -0
- package/src/runtime/vendor.ts +1160 -0
- package/src/style-modules.d.ts +9 -0
- package/src/typegen.ts +151 -0
- package/src/types.ts +725 -0
- package/src/utils/ansi.ts +9 -0
- package/src/utils/content-type.ts +31 -0
- package/src/utils/decode.ts +7 -0
- package/src/utils/dev-profile.ts +31 -0
- package/src/utils/error-log.ts +29 -0
- package/src/utils/fs-cache.ts +31 -0
- package/src/utils/fs.ts +119 -0
- package/src/utils/html.ts +46 -0
- package/src/utils/serialize.ts +378 -0
- package/src/utils/source.ts +35 -0
- package/src/utils/verbose.ts +39 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,875 @@
|
|
|
1
|
+
// Action extension registration (COMPAT - may import core freely).
|
|
2
|
+
//
|
|
3
|
+
// Wires next/actions compat behavior into the core extension registries so core never statically imports
|
|
4
|
+
// compat/actions/* or compat/next/rewrites:
|
|
5
|
+
// - buildExtensions.steps <- action discovery/bundling + server-reference manifest
|
|
6
|
+
// - requestExtensions.interceptors <- action dispatch, then next.config rewrites
|
|
7
|
+
// - renderExtensions.serializeServerActionProp <- action id policy
|
|
8
|
+
// - bundlerExtensions.clientEsbuildPlugins <- action client-stub plugin
|
|
9
|
+
//
|
|
10
|
+
// The request interceptors read the live per-server routing state core publishes via
|
|
11
|
+
// routing/request-runtime.ts and call core render/import helpers directly (compat to core is allowed),
|
|
12
|
+
// so the thin `{ config }` interceptor context stays unchanged.
|
|
13
|
+
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { existsSync } from 'node:fs';
|
|
16
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import type { ResolvedConfig } from '../../config';
|
|
19
|
+
import { pathToFileHref } from '../../config';
|
|
20
|
+
import { nextCompatEnabled } from '../../compat';
|
|
21
|
+
import {
|
|
22
|
+
registerBuildSteps,
|
|
23
|
+
registerInitHooks,
|
|
24
|
+
registerOutsideBasePathInterceptors,
|
|
25
|
+
registerRequestInterceptors,
|
|
26
|
+
registerServerSourceTransforms,
|
|
27
|
+
setBundlerExtensions,
|
|
28
|
+
setRenderExtensions,
|
|
29
|
+
withSniff,
|
|
30
|
+
type ActionRef,
|
|
31
|
+
type BuildStep,
|
|
32
|
+
type BuildStepContext,
|
|
33
|
+
type RequestInterceptor,
|
|
34
|
+
type ServerActionPropContext,
|
|
35
|
+
} from '../../extensions';
|
|
36
|
+
import { setActionModuleExtensions } from '../../render/hooks';
|
|
37
|
+
import { getRequestRuntime, type RequestRuntime } from '../../routing/request-runtime';
|
|
38
|
+
import { setClientActionBundler } from '../../dev/client-actions';
|
|
39
|
+
import { toPosixPath } from '../../utils/fs';
|
|
40
|
+
import type { ActionManifestEntry, BuildManifest, RouteManifestEntry } from '../../types';
|
|
41
|
+
import { devServerModuleHref } from '../../dev/imports';
|
|
42
|
+
import { serverBundleTargetForRuntime } from '../../runtime/server';
|
|
43
|
+
import {
|
|
44
|
+
renderActionReturnElement,
|
|
45
|
+
renderGlobalNotFoundResponse,
|
|
46
|
+
renderPage,
|
|
47
|
+
renderPageResponse,
|
|
48
|
+
} from '../../render';
|
|
49
|
+
import { routeParamsFromPath } from '../../routing/handler';
|
|
50
|
+
import { selectRouteForRequest } from '../../routing/routes';
|
|
51
|
+
import { discoverActions, registerDiscoveredActions } from '../actions/discovery';
|
|
52
|
+
import { clearActions, lookupActionSource, registerActionModule } from '../actions/registry';
|
|
53
|
+
import {
|
|
54
|
+
actionClientStubPlugin,
|
|
55
|
+
actionStubSourceFor,
|
|
56
|
+
canonical,
|
|
57
|
+
canonicalActionId,
|
|
58
|
+
clearClientActionModules,
|
|
59
|
+
clientActionModuleFiles,
|
|
60
|
+
clientActionModulesRoot,
|
|
61
|
+
isClientActionModule,
|
|
62
|
+
setClientActionModules,
|
|
63
|
+
} from '../actions/client-plugin';
|
|
64
|
+
import {
|
|
65
|
+
isActionRequest,
|
|
66
|
+
isProgressiveActionRequest,
|
|
67
|
+
serveAction,
|
|
68
|
+
type ServeActionOptions,
|
|
69
|
+
} from '../actions/serve';
|
|
70
|
+
import {
|
|
71
|
+
NEXT_ACTION_NOT_FOUND_HEADER,
|
|
72
|
+
PROP_ACTION_MARKER,
|
|
73
|
+
} from '../actions/protocol';
|
|
74
|
+
import { loadServerActionsConfig, validateServerActionsConfigSync } from '../actions/config';
|
|
75
|
+
import { renderWithFormStateOverride } from '../actions/form-state';
|
|
76
|
+
import { loadCompatRewrites, type CompatRewrite, resolveCompatRewrite } from '../next/rewrites';
|
|
77
|
+
import { loadCompatRedirects, type CompatRedirect, resolveCompatRedirect } from '../next/redirects';
|
|
78
|
+
import { recordRewrite } from '../protocol';
|
|
79
|
+
import { activateTestProxy } from '../lifecycle/testmode';
|
|
80
|
+
import { canonicalUrlHref, recordCanonicalUrl } from '../next/canonical-url';
|
|
81
|
+
import {
|
|
82
|
+
assignedInlineActionId,
|
|
83
|
+
createInstanceScope,
|
|
84
|
+
instanceActionRouteId,
|
|
85
|
+
isInstanceActionId,
|
|
86
|
+
isMarkedInlineAction,
|
|
87
|
+
overriddenActionId,
|
|
88
|
+
registerActionInstance,
|
|
89
|
+
type InstanceScope,
|
|
90
|
+
} from '../actions/instances';
|
|
91
|
+
import { serverActionId, tagServerAction } from '../actions/server-tag';
|
|
92
|
+
import { opensWithUseServerDirective } from '../actions/detect';
|
|
93
|
+
import { hoistNonCapturingInlineActions } from '../actions/hoist';
|
|
94
|
+
import {
|
|
95
|
+
inlineActionModuleKey,
|
|
96
|
+
isInlineActionId,
|
|
97
|
+
rewriteAnonymousInlineActionMarks,
|
|
98
|
+
rewriteInlineActionTags,
|
|
99
|
+
} from '../actions/rewrite';
|
|
100
|
+
import { reportActionErrorToUser } from '../lifecycle/error-funnel';
|
|
101
|
+
|
|
102
|
+
export function registerActionExtensions(_config: ResolvedConfig): void {
|
|
103
|
+
// Discovery reads config + source text only, so it runs under the route-facts
|
|
104
|
+
// scan; the client bundle is the one thing that waits on its stub set.
|
|
105
|
+
runActionBuildStep.early = true;
|
|
106
|
+
registerBuildSteps(runActionBuildStep);
|
|
107
|
+
// Validate serverActions.bodySizeLimit at build + start startup (Next fails
|
|
108
|
+
// the CLI with the exact size-limit error before serving), rather than only
|
|
109
|
+
// lazily on the first action request.
|
|
110
|
+
registerInitHooks(config => validateServerActionsConfigSync(config.root));
|
|
111
|
+
// Ordering matches the old servers: action dispatch first (on the requested
|
|
112
|
+
// pathname), then next.config redirects (short-circuit before routing), then
|
|
113
|
+
// rewrites swap the request for downstream matching / static-lookup /
|
|
114
|
+
// staleness.
|
|
115
|
+
registerRequestInterceptors(actionDispatchInterceptor, redirectInterceptor, rewriteInterceptor);
|
|
116
|
+
// The same two rules also serve requests from outside the basePath, where only
|
|
117
|
+
// their `basePath: false` entries apply (both interceptors partition their
|
|
118
|
+
// rules on ctx.outsideBasePath).
|
|
119
|
+
registerOutsideBasePathInterceptors(redirectInterceptor, rewriteInterceptor);
|
|
120
|
+
// Tag MODULE-SCOPE inline 'use server' functions with React-compatible `$$id` metadata at module-eval
|
|
121
|
+
// time. Without this, top-level inline actions that module-scope code introspects - Next's version-skew
|
|
122
|
+
// fixtures read and override `fn.$$id` - throw at import. Runs on the server graph only; module-level
|
|
123
|
+
// 'use server' files are skipped by the transform. Anonymous inline actions get no id, only a mark: the
|
|
124
|
+
// compiled function loses its directive, so this is what tells serialization it is an action and not a
|
|
125
|
+
// client handler. Hoisting runs FIRST: it lifts capture-free anonymous inline actions to module-scope
|
|
126
|
+
// declarations, which the tag pass then gives stable `if:` ids, taking the whole-route recovery render
|
|
127
|
+
// off the action's critical path for pages served from prebuilt HTML.
|
|
128
|
+
const inlineActionSniff = ['use server'];
|
|
129
|
+
registerServerSourceTransforms(
|
|
130
|
+
withSniff(inlineActionSniff, source => hoistNonCapturingInlineActions(source)),
|
|
131
|
+
withSniff(inlineActionSniff, (source, file, root) =>
|
|
132
|
+
rewriteInlineActionTags(source, file, root),
|
|
133
|
+
),
|
|
134
|
+
withSniff(inlineActionSniff, source => rewriteAnonymousInlineActionMarks(source)),
|
|
135
|
+
);
|
|
136
|
+
setRenderExtensions({ serializeServerActionProp });
|
|
137
|
+
// Tag imported action-module function exports with their stable wire id so a
|
|
138
|
+
// <form action={serverAction}> imported straight into a page (same module
|
|
139
|
+
// singleton) serializes to that module id instead of a per-render inline id.
|
|
140
|
+
setActionModuleExtensions({ tagActionModuleExports, tagActionModulesForRender });
|
|
141
|
+
// Prod/plain client builds swap 'use server' imports for the RPC stub via
|
|
142
|
+
// the standalone esbuild plugin (client/build.ts appends these). No-op for
|
|
143
|
+
// pure-core / non-next apps.
|
|
144
|
+
setBundlerExtensions({ clientEsbuildPlugins: () => [actionClientStubPlugin()] });
|
|
145
|
+
// The dev route-client bundle resolves modules into its own esbuild namespace
|
|
146
|
+
// that a standalone plugin can't intercept, so dev/imports reads this holder
|
|
147
|
+
// instead. ensureArmed runs the discovery dev startup/reload used to run
|
|
148
|
+
// inline, lazily per dev import version.
|
|
149
|
+
setClientActionBundler({
|
|
150
|
+
ensureArmed: async () => {
|
|
151
|
+
const runtime = getRequestRuntime();
|
|
152
|
+
if (runtime?.dev) await ensureActionsRegistered(runtime);
|
|
153
|
+
},
|
|
154
|
+
isClientActionModule,
|
|
155
|
+
stubSource: file => actionStubSourceFor(file, clientActionModulesRoot() ?? undefined),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Build step: action discovery/bundling + server-reference manifest.
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
interface ActionStepState {
|
|
164
|
+
actions: ActionManifestEntry[];
|
|
165
|
+
actionSources?: string[];
|
|
166
|
+
deferred?: Promise<void>;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const runActionBuildStep: BuildStep = async (ctx: BuildStepContext): Promise<void> => {
|
|
170
|
+
const config = ctx.config;
|
|
171
|
+
const state = ctx.manifest as ActionStepState;
|
|
172
|
+
// Discovery is what arms the client stub set and names the action source
|
|
173
|
+
// files, and it is the only part of this step the client stage waits on.
|
|
174
|
+
const discovery = await (ctx.log
|
|
175
|
+
? ctx.log.step('action discovery', () => discoverActions(config))
|
|
176
|
+
: discoverActions(config));
|
|
177
|
+
state.actionSources = actionSourceKeys(config, discovery);
|
|
178
|
+
// Compiling those modules to server bundles produces manifest entries nothing
|
|
179
|
+
// upstream of the build manifest reads, so it is handed back to run under the
|
|
180
|
+
// client stage instead of ahead of it.
|
|
181
|
+
state.deferred = (async () => {
|
|
182
|
+
const actions = await compileActionModules(config, discovery, ctx.log);
|
|
183
|
+
// The step's `manifest` accumulator carries the actions array the core build
|
|
184
|
+
// folds into the build manifest (manifest.actions) verbatim.
|
|
185
|
+
state.actions = actions;
|
|
186
|
+
if (actions.length > 0) await writeServerReferenceManifest(config, actions);
|
|
187
|
+
})();
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Discover 'use server' modules, compile each to a server bundle (compat
|
|
192
|
+
* aliases + directive handling applied, like the proxy module), and produce the
|
|
193
|
+
* action manifest start serves from. Also arms the client-bundle stub set so
|
|
194
|
+
* client imports of these modules become the RPC stub during buildClientEntries.
|
|
195
|
+
*
|
|
196
|
+
* The wire id is computed from the SOURCE file path (config.root-relative), so
|
|
197
|
+
* the client stub and the server registry agree; modulePath is the COMPILED
|
|
198
|
+
* output the endpoint imports at request time.
|
|
199
|
+
*/
|
|
200
|
+
function actionSourceKeys(
|
|
201
|
+
config: ResolvedConfig,
|
|
202
|
+
discovery: Awaited<ReturnType<typeof discoverActions>>,
|
|
203
|
+
): string[] {
|
|
204
|
+
clearActions();
|
|
205
|
+
if (discovery.modules.length === 0) {
|
|
206
|
+
setClientActionModules(config.root, []);
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
// Client bundling replaces these source imports with the stub.
|
|
210
|
+
setClientActionModules(config.root, discovery.actionFiles);
|
|
211
|
+
// Registered from the discovery facts alone (ids come from source paths), so
|
|
212
|
+
// build-time lookupAction parity holds before the compile below finishes.
|
|
213
|
+
registerDiscoveredActions(config, discovery);
|
|
214
|
+
return discovery.modules.map(module => actionSourceKey(config, module.file));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** The registry id's source key: canonical, root-relative, POSIX. */
|
|
218
|
+
function actionSourceKey(config: ResolvedConfig, file: string) {
|
|
219
|
+
// Must reproduce the registry id at startup: registerManifestActions calls
|
|
220
|
+
// actionId(sourceKey) with no root, so this has to equal
|
|
221
|
+
// workspaceRelative(canonical(file), canonical(root)).
|
|
222
|
+
return toPosixPath(path.relative(canonical(config.root), canonical(file)));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function compileActionModules(
|
|
226
|
+
config: ResolvedConfig,
|
|
227
|
+
discovery: Awaited<ReturnType<typeof discoverActions>>,
|
|
228
|
+
log?: BuildStepContext['log'],
|
|
229
|
+
): Promise<ActionManifestEntry[]> {
|
|
230
|
+
if (discovery.modules.length === 0) return [];
|
|
231
|
+
|
|
232
|
+
const entries: ActionManifestEntry[] = [];
|
|
233
|
+
const compile = async () => {
|
|
234
|
+
for (const module of discovery.modules) {
|
|
235
|
+
// 'build', matching render/index.tsx's prod moduleHref: the endpoint and the
|
|
236
|
+
// page render must import the SAME compiled file so a module imported in
|
|
237
|
+
// both the render and action layers is one instance (shared module state).
|
|
238
|
+
const href = await devServerModuleHref(config, module.file, 'build');
|
|
239
|
+
const compiled = fileURLToPath(href);
|
|
240
|
+
const modulePath = toPosixPath(path.relative(config.outPath, compiled));
|
|
241
|
+
const sourceKey = actionSourceKey(config, module.file);
|
|
242
|
+
for (const exportName of module.exports) {
|
|
243
|
+
entries.push({ id: module.ids[exportName]!, sourceKey, modulePath, exportName });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
if (log) await log.step(`action modules (${discovery.modules.length})`, compile);
|
|
248
|
+
else await compile();
|
|
249
|
+
return entries;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Next-compatible server-reference manifest (.next/server/): tools and Next's
|
|
253
|
+
// own e2e suite introspect it for the registered action ids and their source
|
|
254
|
+
// modules. pnext's real registry lives in .pnext/manifest.json; this mirror
|
|
255
|
+
// exists purely for ecosystem compatibility.
|
|
256
|
+
async function writeServerReferenceManifest(
|
|
257
|
+
config: ResolvedConfig,
|
|
258
|
+
actions: ActionManifestEntry[],
|
|
259
|
+
) {
|
|
260
|
+
const node: Record<
|
|
261
|
+
string,
|
|
262
|
+
{
|
|
263
|
+
workers: Record<string, unknown>;
|
|
264
|
+
layer: Record<string, unknown>;
|
|
265
|
+
filename: string;
|
|
266
|
+
exportedName: string;
|
|
267
|
+
}
|
|
268
|
+
> = {};
|
|
269
|
+
for (const action of actions) {
|
|
270
|
+
node[action.id] = {
|
|
271
|
+
workers: {},
|
|
272
|
+
layer: {},
|
|
273
|
+
filename: action.sourceKey,
|
|
274
|
+
exportedName: action.exportName,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
const manifest = { node, edge: {}, encryptionKey: 'pnext' };
|
|
278
|
+
const outDir = path.join(config.root, '.next', 'server');
|
|
279
|
+
await mkdir(outDir, { recursive: true });
|
|
280
|
+
await writeFile(
|
|
281
|
+
path.join(outDir, 'server-reference-manifest.json'),
|
|
282
|
+
JSON.stringify(manifest, null, 2),
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Request interceptor: server-action dispatch. Actions POST to the page's own URL (id in the
|
|
287
|
+
// next-action header for JS clients, or a hidden form field for no-JS progressive submissions).
|
|
288
|
+
// Route-handler POSTs pass through even with an id header - actions only dispatch on page URLs, or on
|
|
289
|
+
// unmatched paths, which still get the action error semantics.
|
|
290
|
+
|
|
291
|
+
const actionDispatchInterceptor: RequestInterceptor = async (request, ctx) => {
|
|
292
|
+
const config = ctx.config;
|
|
293
|
+
if (!nextCompatEnabled(config)) return undefined;
|
|
294
|
+
const runtime = getRequestRuntime();
|
|
295
|
+
if (!runtime) return undefined;
|
|
296
|
+
const url = new URL(request.url);
|
|
297
|
+
const actionTarget = selectRouteForRequest(runtime.routes, url.pathname, undefined);
|
|
298
|
+
// Route-handler POSTs pass through even with an id header — actions only
|
|
299
|
+
// dispatch on page URLs (or unmatched paths, which still get the action
|
|
300
|
+
// error semantics the unrecognized-action suite asserts).
|
|
301
|
+
if (actionTarget && actionTarget.route.kind !== 'page') return undefined;
|
|
302
|
+
|
|
303
|
+
// A form-encoded POST to an existing page with no recognizable action id is still an MPA action attempt
|
|
304
|
+
// in Next, which routes every page form POST through the action handler: with no action to run it
|
|
305
|
+
// answers 404 action-not-found rather than rendering the page. Only applies to real page targets - a
|
|
306
|
+
// POST to a nonexistent route stays a clean 404.
|
|
307
|
+
const isFormPost = actionTarget?.route.kind === 'page' && isFormPostRequest(request);
|
|
308
|
+
if (
|
|
309
|
+
!(isActionRequest(request) || (await isProgressiveActionRequest(request)) || isFormPost)
|
|
310
|
+
) {
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
await ensureActionsRegistered(runtime);
|
|
315
|
+
|
|
316
|
+
const actionId = request.headers.get('next-action');
|
|
317
|
+
if (
|
|
318
|
+
actionId &&
|
|
319
|
+
canonicalUrlHref() !== undefined &&
|
|
320
|
+
isForwardedActionForRoute(config, actionTarget, actionId)
|
|
321
|
+
) {
|
|
322
|
+
return new Response(`Server Action "${actionId}" was not found on the server.`, {
|
|
323
|
+
status: 404,
|
|
324
|
+
headers: {
|
|
325
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
326
|
+
[NEXT_ACTION_NOT_FOUND_HEADER]: '1',
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const maxBodyBytes = runtime.maxBodyBytes ?? (await resolveMaxBodyBytes(config));
|
|
332
|
+
const options = buildServeActionOptions(runtime, request, actionTarget, maxBodyBytes);
|
|
333
|
+
return serveAction(request, options);
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// Strip the configured basePath from a redirect-target URL so it lines up with
|
|
337
|
+
// the app-relative route table. Same matching rule as start.ts stripBasePath;
|
|
338
|
+
// a target outside the basePath (a `basePath:false` rewrite destination) is
|
|
339
|
+
// returned unchanged so it simply fails to match a page (external redirect).
|
|
340
|
+
function stripActionBasePath(target: URL, basePath: string): URL {
|
|
341
|
+
if (!basePath) return target;
|
|
342
|
+
const pathname = target.pathname;
|
|
343
|
+
if (pathname !== basePath && !pathname.startsWith(`${basePath}/`)) return target;
|
|
344
|
+
const stripped = new URL(target);
|
|
345
|
+
stripped.pathname = pathname.slice(basePath.length) || '/';
|
|
346
|
+
return stripped;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Rebuild the render request against the basePath-stripped URL, preserving the
|
|
350
|
+
// action's applied cookie/header state. Returns the original request untouched
|
|
351
|
+
// when no stripping happened.
|
|
352
|
+
function strippedRequest(request: Request, target: URL, stripped: URL): Request {
|
|
353
|
+
if (stripped.href === target.href) return request;
|
|
354
|
+
return new Request(stripped.href, { headers: request.headers });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function isFormPostRequest(request: Request): boolean {
|
|
358
|
+
if (request.method.toUpperCase() !== 'POST') return false;
|
|
359
|
+
if (request.headers.has('next-action')) return false;
|
|
360
|
+
const contentType = request.headers.get('content-type') ?? '';
|
|
361
|
+
// Only multipart counts as an id-less MPA action attempt: React's progressive action forms always post
|
|
362
|
+
// multipart/form-data, so a urlencoded POST without an action id is a plain form POST to the page - a
|
|
363
|
+
// browser re-POSTing after a route handler's 307/308 redirect, say - and must render the page, not 404.
|
|
364
|
+
// urlencoded POSTs that DO carry the hidden action id field are still dispatched.
|
|
365
|
+
return contentType.includes('multipart/form-data');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// serverActions.bodySizeLimit from next.config, resolved once per project.
|
|
369
|
+
// Invalid values throw (Next's message) — surfaced on the first action request
|
|
370
|
+
// as a 500, matching the size-limit-invalid suite's expectation.
|
|
371
|
+
const bodyLimitCache = new Map<string, Promise<number | undefined>>();
|
|
372
|
+
|
|
373
|
+
function resolveMaxBodyBytes(config: ResolvedConfig): Promise<number | undefined> {
|
|
374
|
+
const key = config.root;
|
|
375
|
+
let cached = bodyLimitCache.get(key);
|
|
376
|
+
if (!cached) {
|
|
377
|
+
cached = loadServerActionsConfig(config.root).then(c => c.bodySizeLimitBytes);
|
|
378
|
+
bodyLimitCache.set(key, cached);
|
|
379
|
+
}
|
|
380
|
+
return cached;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Instance-recovery render dedup: the first dispatch against a prebuilt page
|
|
384
|
+
// re-renders the route to re-materialize its `i:` action closures
|
|
385
|
+
// (instances.ts); concurrent dispatches share that in-flight render instead of
|
|
386
|
+
// each starting a full one. Keyed route+pathname (dynamic routes recover per
|
|
387
|
+
// path), cleared on settle so evicted instances recover again.
|
|
388
|
+
const instanceRecoveries = new Map<string, Promise<void>>();
|
|
389
|
+
|
|
390
|
+
function dedupeInstanceRecovery(
|
|
391
|
+
routeId: string,
|
|
392
|
+
pathname: string,
|
|
393
|
+
render: () => Promise<void>,
|
|
394
|
+
): Promise<void> {
|
|
395
|
+
const key = `${routeId}${pathname}`;
|
|
396
|
+
const inFlight = instanceRecoveries.get(key);
|
|
397
|
+
if (inFlight) return inFlight;
|
|
398
|
+
const started = render().finally(() => instanceRecoveries.delete(key));
|
|
399
|
+
instanceRecoveries.set(key, started);
|
|
400
|
+
return started;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Reconstruct the ServeActionOptions the old start/dev handlers built inline,
|
|
405
|
+
* from the published request runtime + core render/import helpers. Dev vs prod
|
|
406
|
+
* only changes how modules resolve to hrefs.
|
|
407
|
+
*/
|
|
408
|
+
function buildServeActionOptions(
|
|
409
|
+
runtime: RequestRuntime,
|
|
410
|
+
request: Request,
|
|
411
|
+
actionTarget: ReturnType<typeof selectRouteForRequest>,
|
|
412
|
+
maxBodyBytes: number | undefined,
|
|
413
|
+
): ServeActionOptions {
|
|
414
|
+
const config = runtime.config;
|
|
415
|
+
const dev = runtime.dev;
|
|
416
|
+
const version = runtime.devImportVersion ?? 'build';
|
|
417
|
+
const url = new URL(request.url);
|
|
418
|
+
const actionConditionTarget =
|
|
419
|
+
actionTarget?.route.kind === 'page'
|
|
420
|
+
? serverBundleTargetForRuntime(actionTarget.route.segmentConfig?.runtime)
|
|
421
|
+
: 'server';
|
|
422
|
+
|
|
423
|
+
const importModule = dev
|
|
424
|
+
? async (modulePath: string) =>
|
|
425
|
+
(await import(
|
|
426
|
+
await devServerModuleHref(config, modulePath, version, {
|
|
427
|
+
conditionTarget: actionConditionTarget,
|
|
428
|
+
})
|
|
429
|
+
)) as Record<string, unknown>
|
|
430
|
+
: async (modulePath: string) =>
|
|
431
|
+
(await import(pathToFileHref(path.resolve(config.outPath, modulePath)))) as Record<
|
|
432
|
+
string,
|
|
433
|
+
unknown
|
|
434
|
+
>;
|
|
435
|
+
|
|
436
|
+
const options: ServeActionOptions = {
|
|
437
|
+
...(dev ? { dev: true } : {}),
|
|
438
|
+
importModule,
|
|
439
|
+
renderNotFound: async notFoundRequest =>
|
|
440
|
+
renderGlobalNotFoundResponse({
|
|
441
|
+
config,
|
|
442
|
+
url: new URL(notFoundRequest.url),
|
|
443
|
+
// GET conversion: the page renderer refuses non-GET requests.
|
|
444
|
+
request: new Request(notFoundRequest.url, { headers: notFoundRequest.headers }),
|
|
445
|
+
...(dev ? { dev: true, devImportVersion: version } : {}),
|
|
446
|
+
}),
|
|
447
|
+
renderPageForFormState: async (formRequest, state, actionId) => {
|
|
448
|
+
if (actionTarget?.route.kind !== 'page') return null;
|
|
449
|
+
const formUrl = new URL(formRequest.url);
|
|
450
|
+
return renderWithFormStateOverride(state, actionId, () =>
|
|
451
|
+
renderPageResponse({
|
|
452
|
+
config,
|
|
453
|
+
route: actionTarget.route,
|
|
454
|
+
params: actionTarget.params,
|
|
455
|
+
url: formUrl,
|
|
456
|
+
request: new Request(formRequest.url, { headers: formRequest.headers }),
|
|
457
|
+
...(dev ? { dev: true, devImportVersion: version } : {}),
|
|
458
|
+
}),
|
|
459
|
+
);
|
|
460
|
+
},
|
|
461
|
+
renderRedirectTarget: async (redirectRequest, target) => {
|
|
462
|
+
// The redirect target arrives in basePath space (redirect() prefixes it);
|
|
463
|
+
// strip the prefix so it matches the app-relative route table and the
|
|
464
|
+
// rendered page's usePathname excludes the basePath, exactly like a
|
|
465
|
+
// freshly-routed GET (which start.ts strips before matching).
|
|
466
|
+
const stripped = stripActionBasePath(target, config.basePath);
|
|
467
|
+
const targetMatch = selectRouteForRequest(runtime.routes, stripped.pathname, undefined);
|
|
468
|
+
if (targetMatch?.route.kind !== 'page') return null;
|
|
469
|
+
return renderPageResponse({
|
|
470
|
+
config,
|
|
471
|
+
route: targetMatch.route,
|
|
472
|
+
params: targetMatch.params,
|
|
473
|
+
url: stripped,
|
|
474
|
+
request: strippedRequest(redirectRequest, target, stripped),
|
|
475
|
+
...(dev ? { dev: true, devImportVersion: version } : {}),
|
|
476
|
+
});
|
|
477
|
+
},
|
|
478
|
+
renderActionFlight: async (flightRequest, target) => {
|
|
479
|
+
const stripped = stripActionBasePath(target, config.basePath);
|
|
480
|
+
const targetMatch = selectRouteForRequest(runtime.routes, stripped.pathname, undefined);
|
|
481
|
+
if (targetMatch?.route.kind !== 'page') return null;
|
|
482
|
+
return renderPageResponse({
|
|
483
|
+
config,
|
|
484
|
+
route: targetMatch.route,
|
|
485
|
+
params: targetMatch.params,
|
|
486
|
+
url: stripped,
|
|
487
|
+
request: strippedRequest(flightRequest, target, stripped),
|
|
488
|
+
...(dev ? { dev: true, devImportVersion: version } : {}),
|
|
489
|
+
});
|
|
490
|
+
},
|
|
491
|
+
importSourceModule: async moduleKey => {
|
|
492
|
+
const absolute = path.resolve(config.root, moduleKey);
|
|
493
|
+
await import(
|
|
494
|
+
await devServerModuleHref(config, absolute, version, {
|
|
495
|
+
conditionTarget: actionConditionTarget,
|
|
496
|
+
}),
|
|
497
|
+
);
|
|
498
|
+
},
|
|
499
|
+
rerenderRoute: async routeId => {
|
|
500
|
+
const route = runtime.routes.find(entry => entry.id === routeId && entry.kind === 'page');
|
|
501
|
+
if (!route) throw new Error(`Unknown route for action re-render: ${routeId}`);
|
|
502
|
+
await dedupeInstanceRecovery(routeId, url.pathname, () =>
|
|
503
|
+
renderPage({
|
|
504
|
+
config,
|
|
505
|
+
route,
|
|
506
|
+
params: routeParamsFromPath(route, url.pathname),
|
|
507
|
+
url,
|
|
508
|
+
request: new Request(request.url, { headers: request.headers }),
|
|
509
|
+
...(dev ? { dev: true, devImportVersion: version } : {}),
|
|
510
|
+
}).then(() => undefined),
|
|
511
|
+
);
|
|
512
|
+
},
|
|
513
|
+
isForwardedAction: id => isForwardedActionForRoute(config, actionTarget, id),
|
|
514
|
+
onError: reportActionErrorToUser,
|
|
515
|
+
...(maxBodyBytes !== undefined ? { maxBodyBytes } : {}),
|
|
516
|
+
// Element-returning actions server-render the returned tree through the same
|
|
517
|
+
// machinery a page render uses (async server components, host elements),
|
|
518
|
+
// producing the island wire HTML the action client revives. Scoped to page
|
|
519
|
+
// targets — the only routes that own such actions.
|
|
520
|
+
...(actionTarget?.route.kind === 'page'
|
|
521
|
+
? {
|
|
522
|
+
renderActionElement: (element: unknown) =>
|
|
523
|
+
renderActionReturnElement(element, {
|
|
524
|
+
config,
|
|
525
|
+
route: actionTarget.route,
|
|
526
|
+
params: actionTarget.params,
|
|
527
|
+
url,
|
|
528
|
+
request: new Request(request.url, { headers: request.headers }),
|
|
529
|
+
...(dev ? { dev: true, devImportVersion: version } : {}),
|
|
530
|
+
}),
|
|
531
|
+
}
|
|
532
|
+
: {}),
|
|
533
|
+
};
|
|
534
|
+
return options;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Next "forwarded action" discrimination: the POSTed route's static source closure does not contain the
|
|
539
|
+
* action's source module, meaning the client dispatched the action after navigating to a different page.
|
|
540
|
+
* For live-instance actions the owning route is encoded in the id; for module and inline actions the
|
|
541
|
+
* registered source file is checked against the route's sourceFiles closure, realpath-canonicalized since
|
|
542
|
+
* the closure may hold /private-var vs /var aliases of the same file on macOS.
|
|
543
|
+
*/
|
|
544
|
+
function isForwardedActionForRoute(
|
|
545
|
+
config: ResolvedConfig,
|
|
546
|
+
actionTarget: ReturnType<typeof selectRouteForRequest>,
|
|
547
|
+
id: string,
|
|
548
|
+
): boolean {
|
|
549
|
+
const route = actionTarget?.route;
|
|
550
|
+
if (route?.kind !== 'page') return false;
|
|
551
|
+
if (isInstanceActionId(id)) {
|
|
552
|
+
const owner = instanceActionRouteId(id);
|
|
553
|
+
return Boolean(owner && owner !== route.id);
|
|
554
|
+
}
|
|
555
|
+
const source = isInlineActionId(id) ? inlineActionModuleKey(id) : lookupActionSource(id);
|
|
556
|
+
if (!source) return false;
|
|
557
|
+
const sources = route.sourceFiles;
|
|
558
|
+
if (!sources?.length) return false;
|
|
559
|
+
const target = canonical(path.resolve(config.root, source));
|
|
560
|
+
return !sources.some(file => canonical(file) === target);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// ---------------------------------------------------------------------------
|
|
564
|
+
// Action registry arming.
|
|
565
|
+
//
|
|
566
|
+
// prod: rebuild the registry from the built manifest once (id parity with the
|
|
567
|
+
// client stub via the stored source key).
|
|
568
|
+
// dev: re-run discovery whenever the dev import version changes (reload), so
|
|
569
|
+
// the endpoint + client-stub set stay current without core importing us.
|
|
570
|
+
// ---------------------------------------------------------------------------
|
|
571
|
+
|
|
572
|
+
// Memoized on the in-flight PROMISE, not on a "started" flag: a page can fire several actions at once,
|
|
573
|
+
// and a flag set before the await lets the second POST through against a registry that is still empty, so
|
|
574
|
+
// it 404s as an unknown action. Every caller awaits the same registration. A failed one is dropped so the
|
|
575
|
+
// next request retries instead of inheriting the rejection forever. `UNREGISTERED` keys the initial (and
|
|
576
|
+
// post-failure) state so the memo itself is never optional.
|
|
577
|
+
const UNREGISTERED = Symbol('pnext.actions.unregistered');
|
|
578
|
+
const noRegistration = { key: UNREGISTERED, version: UNREGISTERED, done: Promise.resolve() };
|
|
579
|
+
let manifestRegistration: { key: string | symbol; done: Promise<void> } = noRegistration;
|
|
580
|
+
let devRegistration: { version: string | symbol | undefined; done: Promise<void> } =
|
|
581
|
+
noRegistration;
|
|
582
|
+
|
|
583
|
+
async function ensureActionsRegistered(runtime: RequestRuntime): Promise<void> {
|
|
584
|
+
if (runtime.dev) {
|
|
585
|
+
if (devRegistration.version !== runtime.devImportVersion) {
|
|
586
|
+
const pending = { version: runtime.devImportVersion, done: Promise.resolve() };
|
|
587
|
+
pending.done = refreshDevActions(runtime.config).catch((error: unknown) => {
|
|
588
|
+
if (devRegistration === pending) devRegistration = noRegistration;
|
|
589
|
+
throw error;
|
|
590
|
+
});
|
|
591
|
+
devRegistration = pending;
|
|
592
|
+
}
|
|
593
|
+
await devRegistration.done;
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
const key = runtime.config.outPath;
|
|
597
|
+
if (manifestRegistration.key !== key) {
|
|
598
|
+
const pending = { key, done: Promise.resolve() };
|
|
599
|
+
pending.done = registerManifestActions(runtime.config).catch((error: unknown) => {
|
|
600
|
+
if (manifestRegistration === pending) manifestRegistration = noRegistration;
|
|
601
|
+
throw error;
|
|
602
|
+
});
|
|
603
|
+
manifestRegistration = pending;
|
|
604
|
+
}
|
|
605
|
+
await manifestRegistration.done;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Rebuild the action registry from the built manifest so the endpoint can
|
|
609
|
+
// resolve an incoming id to its compiled module + export. Also restore the
|
|
610
|
+
// client-action module set (normally armed by the build): render-time form/prop
|
|
611
|
+
// tagging reads it to give module-level actions their stable wire ids.
|
|
612
|
+
async function registerManifestActions(config: ResolvedConfig): Promise<void> {
|
|
613
|
+
const manifestPath = path.join(config.outPath, 'manifest.json');
|
|
614
|
+
let manifest: BuildManifest;
|
|
615
|
+
try {
|
|
616
|
+
manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as BuildManifest;
|
|
617
|
+
} catch {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const files = new Set<string>();
|
|
621
|
+
for (const action of manifest.actions ?? []) {
|
|
622
|
+
registerActionModule(action.sourceKey, [action.exportName], {
|
|
623
|
+
modulePath: action.modulePath,
|
|
624
|
+
});
|
|
625
|
+
files.add(path.resolve(config.root, action.sourceKey));
|
|
626
|
+
}
|
|
627
|
+
if (files.size > 0) setClientActionModules(config.root, files);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Rescan the app for 'use server' modules and refresh both the endpoint
|
|
631
|
+
// registry (id -> module) and the client-bundle stub set. In dev the action's
|
|
632
|
+
// modulePath stays the source file: devServerModuleHref compiles it on demand
|
|
633
|
+
// when the endpoint imports it. Gated on compat.next inside discoverActions.
|
|
634
|
+
async function refreshDevActions(config: ResolvedConfig): Promise<void> {
|
|
635
|
+
clearActions();
|
|
636
|
+
clearClientActionModules();
|
|
637
|
+
const discovery = await discoverActions(config);
|
|
638
|
+
if (discovery.modules.length === 0) return;
|
|
639
|
+
registerDiscoveredActions(config, discovery);
|
|
640
|
+
setClientActionModules(config.root, discovery.actionFiles);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// ---------------------------------------------------------------------------
|
|
644
|
+
// Request interceptor: next.config rewrites.
|
|
645
|
+
//
|
|
646
|
+
// Rewrites map the request pathname to its destination before static lookup and
|
|
647
|
+
// routing, so prebuilt output + revalidatePath marks work against the
|
|
648
|
+
// destination. The render keeps the requested URL canonical (usePathname); core
|
|
649
|
+
// preserves the original URL for rendering (see start/dev canonicalUrl).
|
|
650
|
+
// ---------------------------------------------------------------------------
|
|
651
|
+
|
|
652
|
+
const rewriteCache = new Map<string, Promise<CompatRewrite[]>>();
|
|
653
|
+
|
|
654
|
+
function loadRewritesFor(config: ResolvedConfig): Promise<CompatRewrite[]> {
|
|
655
|
+
const key = config.root;
|
|
656
|
+
let cached = rewriteCache.get(key);
|
|
657
|
+
if (!cached) {
|
|
658
|
+
cached = loadCompatRewrites(config.root);
|
|
659
|
+
rewriteCache.set(key, cached);
|
|
660
|
+
}
|
|
661
|
+
return cached;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const redirectCache = new Map<string, Promise<CompatRedirect[]>>();
|
|
665
|
+
|
|
666
|
+
function loadRedirectsFor(config: ResolvedConfig): Promise<CompatRedirect[]> {
|
|
667
|
+
const key = config.root;
|
|
668
|
+
let cached = redirectCache.get(key);
|
|
669
|
+
if (!cached) {
|
|
670
|
+
cached = loadCompatRedirects(config.root);
|
|
671
|
+
redirectCache.set(key, cached);
|
|
672
|
+
}
|
|
673
|
+
return cached;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const redirectInterceptor: RequestInterceptor = async (request, ctx) => {
|
|
677
|
+
const config = ctx.config;
|
|
678
|
+
if (!nextCompatEnabled(config)) return undefined;
|
|
679
|
+
// `basePath: false` entries match the raw path core would otherwise 404;
|
|
680
|
+
// every other entry matches the in-app (basePath-stripped) path. The two
|
|
681
|
+
// passes are disjoint.
|
|
682
|
+
const redirects = (await loadRedirectsFor(config)).filter(
|
|
683
|
+
redirect => redirect.outsideBasePath === Boolean(ctx.outsideBasePath),
|
|
684
|
+
);
|
|
685
|
+
if (redirects.length === 0) return undefined;
|
|
686
|
+
const url = new URL(request.url);
|
|
687
|
+
const result = resolveCompatRedirect(redirects, url.pathname, {
|
|
688
|
+
host: request.headers.get('host') ?? '',
|
|
689
|
+
headers: request.headers,
|
|
690
|
+
cookies: parseCookieHeader(request.headers.get('cookie')),
|
|
691
|
+
query: url.searchParams,
|
|
692
|
+
});
|
|
693
|
+
if (!result) return undefined;
|
|
694
|
+
const location = new URL(result.location, url.href);
|
|
695
|
+
// Next passes the incoming query through to the redirect destination (the
|
|
696
|
+
// `_rsc` union query among it — rsc-query-routing relies on the followed
|
|
697
|
+
// request keeping it). Destination-specified params win.
|
|
698
|
+
for (const [key, value] of url.searchParams) {
|
|
699
|
+
if (!location.searchParams.has(key)) location.searchParams.append(key, value);
|
|
700
|
+
}
|
|
701
|
+
return new Response(null, { status: result.status, headers: { location: location.href } });
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
const rewriteInterceptor: RequestInterceptor = async (request, ctx) => {
|
|
705
|
+
const config = ctx.config;
|
|
706
|
+
if (!nextCompatEnabled(config)) return undefined;
|
|
707
|
+
// See redirectInterceptor: `basePath: false` rules own the outside-basePath
|
|
708
|
+
// pass, the rest own the in-app one.
|
|
709
|
+
const rewrites = (await loadRewritesFor(config)).filter(
|
|
710
|
+
rewrite => rewrite.outsideBasePath === Boolean(ctx.outsideBasePath),
|
|
711
|
+
);
|
|
712
|
+
if (rewrites.length === 0) return undefined;
|
|
713
|
+
const url = new URL(request.url);
|
|
714
|
+
const context = {
|
|
715
|
+
host: request.headers.get('host') ?? '',
|
|
716
|
+
headers: request.headers,
|
|
717
|
+
cookies: parseCookieHeader(request.headers.get('cookie')),
|
|
718
|
+
query: url.searchParams,
|
|
719
|
+
};
|
|
720
|
+
// beforeFiles/afterFiles (and the array form) apply ahead of routing; a
|
|
721
|
+
// `fallback` rewrite only gets a turn once nothing else can serve the path.
|
|
722
|
+
let result = resolveCompatRewrite(
|
|
723
|
+
rewrites.filter(rewrite => !rewrite.fallback),
|
|
724
|
+
url.pathname,
|
|
725
|
+
context,
|
|
726
|
+
);
|
|
727
|
+
if (!result && requestWouldMiss(config, url.pathname)) {
|
|
728
|
+
result = resolveCompatRewrite(
|
|
729
|
+
rewrites.filter(rewrite => rewrite.fallback),
|
|
730
|
+
url.pathname,
|
|
731
|
+
context,
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
if (!result) return undefined;
|
|
735
|
+
if (result.destination) {
|
|
736
|
+
activateTestProxy(request.headers);
|
|
737
|
+
// The incoming `host` (and its forwarded twins) name THIS server; carrying
|
|
738
|
+
// them onto an outbound request makes the TLS handshake disagree with the
|
|
739
|
+
// destination's certificate. Let fetch derive them from the destination URL.
|
|
740
|
+
const headers = new Headers(request.headers);
|
|
741
|
+
for (const header of ['host', 'x-forwarded-host', 'x-forwarded-port', 'x-forwarded-proto']) {
|
|
742
|
+
headers.delete(header);
|
|
743
|
+
}
|
|
744
|
+
const rewrittenRequest = new Request(result.destination, {
|
|
745
|
+
method: request.method,
|
|
746
|
+
headers,
|
|
747
|
+
...(request.body ? { body: request.body, duplex: 'half' } : {}),
|
|
748
|
+
});
|
|
749
|
+
return fetch(rewrittenRequest);
|
|
750
|
+
}
|
|
751
|
+
const rewritten = new URL(url.href);
|
|
752
|
+
rewritten.pathname = result.pathname;
|
|
753
|
+
rewritten.search = result.search.toString();
|
|
754
|
+
recordCanonicalUrl(url);
|
|
755
|
+
recordRewrite(url, rewritten);
|
|
756
|
+
return { request: new Request(rewritten, request) };
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Whether a request for `pathname` would 404 - the gate Next's `fallback` rewrites sit behind. A path is
|
|
761
|
+
* servable when the route table matches it or a built/public file backs it; `/_next/*` assets are always
|
|
762
|
+
* served by core. Divergence: dev has no built output, so only the route table is consulted.
|
|
763
|
+
*/
|
|
764
|
+
function requestWouldMiss(config: ResolvedConfig, pathname: string): boolean {
|
|
765
|
+
if (pathname.startsWith('/_next/')) return false;
|
|
766
|
+
const runtime = getRequestRuntime();
|
|
767
|
+
if (runtime && selectRouteForRequest(runtime.routes, pathname)) return false;
|
|
768
|
+
return !servableFile(config, pathname);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function servableFile(config: ResolvedConfig, pathname: string): boolean {
|
|
772
|
+
const publicRoot = path.join(config.outPath, 'public');
|
|
773
|
+
const trimmed = pathname.replace(/^\/+|\/+$/g, '');
|
|
774
|
+
const file = trimmed ? path.join(publicRoot, trimmed) : publicRoot;
|
|
775
|
+
// Never let a traversal-shaped path claim to be servable (it is not).
|
|
776
|
+
const relative = path.relative(publicRoot, file);
|
|
777
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) return false;
|
|
778
|
+
return (
|
|
779
|
+
existsSync(path.join(file, 'index.html')) ||
|
|
780
|
+
(trimmed !== '' && (existsSync(file) || existsSync(`${file}.html`)))
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function parseCookieHeader(header: string | null): Record<string, string> {
|
|
785
|
+
if (!header) return {};
|
|
786
|
+
const cookies: Record<string, string> = {};
|
|
787
|
+
for (const part of header.split(';')) {
|
|
788
|
+
const eq = part.indexOf('=');
|
|
789
|
+
if (eq === -1) continue;
|
|
790
|
+
cookies[part.slice(0, eq).trim()] = part.slice(eq + 1).trim();
|
|
791
|
+
}
|
|
792
|
+
return cookies;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// ---------------------------------------------------------------------------
|
|
796
|
+
// Render extension: serialize a server-action function crossing the
|
|
797
|
+
// server->client boundary into the { [PROP_ACTION_MARKER]: id } marker.
|
|
798
|
+
//
|
|
799
|
+
// Id policy (Next-compatible), highest priority first:
|
|
800
|
+
// 1. caller-overridden $$id (version-skew simulation)
|
|
801
|
+
// 2. compile-tagged module-scope inline action ($$pnextAssignedId, 'if:')
|
|
802
|
+
// 3. tagged module-level 'use server' export (stable module id)
|
|
803
|
+
// 4. inline / .bind() / HOC closures -> a live per-render instance ('i:')
|
|
804
|
+
//
|
|
805
|
+
// Per-render instance scopes are keyed on the opaque renderKey (core's
|
|
806
|
+
// RenderOptions) so occurrence indices stay consistent within one render.
|
|
807
|
+
// ---------------------------------------------------------------------------
|
|
808
|
+
|
|
809
|
+
const instanceScopes = new WeakMap<object, InstanceScope>();
|
|
810
|
+
|
|
811
|
+
function serializeServerActionProp(
|
|
812
|
+
value: unknown,
|
|
813
|
+
context: ServerActionPropContext,
|
|
814
|
+
): ActionRef | undefined {
|
|
815
|
+
if (typeof value !== 'function') return undefined;
|
|
816
|
+
if (!nextCompatEnabled(context.config)) return undefined;
|
|
817
|
+
|
|
818
|
+
const overridden = overriddenActionId(value);
|
|
819
|
+
if (overridden) return { [PROP_ACTION_MARKER]: overridden };
|
|
820
|
+
|
|
821
|
+
const assigned = assignedInlineActionId(value);
|
|
822
|
+
if (assigned) return { [PROP_ACTION_MARKER]: assigned };
|
|
823
|
+
|
|
824
|
+
const moduleId = serverActionId(value);
|
|
825
|
+
if (moduleId) return { [PROP_ACTION_MARKER]: moduleId };
|
|
826
|
+
|
|
827
|
+
if (context.identifiedOnly && !isMarkedInlineAction(value) && !opensWithUseServerDirective(value)) {
|
|
828
|
+
return undefined;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const scope = instanceScopeFor(context.renderKey);
|
|
832
|
+
const id = registerActionInstance(scope, value);
|
|
833
|
+
return { [PROP_ACTION_MARKER]: id };
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/** Tag each function export of a discovered action module with its wire id. */
|
|
837
|
+
function tagActionModuleExports(
|
|
838
|
+
config: ResolvedConfig,
|
|
839
|
+
file: string,
|
|
840
|
+
module: Record<string, unknown>,
|
|
841
|
+
): void {
|
|
842
|
+
if (!nextCompatEnabled(config) || !isClientActionModule(file)) return;
|
|
843
|
+
const root = clientActionModulesRoot() ?? config.root;
|
|
844
|
+
for (const [exportName, value] of Object.entries(module)) {
|
|
845
|
+
if (typeof value === 'function') {
|
|
846
|
+
tagServerAction(value, canonicalActionId(file, exportName, root));
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/** Import + tag every discovered action module ahead of a page render. */
|
|
852
|
+
async function tagActionModulesForRender(
|
|
853
|
+
config: ResolvedConfig,
|
|
854
|
+
importModule: (file: string) => Promise<Record<string, unknown>>,
|
|
855
|
+
): Promise<void> {
|
|
856
|
+
if (!nextCompatEnabled(config)) return;
|
|
857
|
+
for (const file of clientActionModuleFiles()) {
|
|
858
|
+
try {
|
|
859
|
+
tagActionModuleExports(config, file, await importModule(file));
|
|
860
|
+
} catch {
|
|
861
|
+
// Best-effort: an action module that fails to import here still POSTs via
|
|
862
|
+
// the client stub when JS runs; the form just renders without the hidden id.
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function instanceScopeFor(renderKey: object): InstanceScope {
|
|
868
|
+
let scope = instanceScopes.get(renderKey);
|
|
869
|
+
if (!scope) {
|
|
870
|
+
const routeId = (renderKey as { route?: RouteManifestEntry }).route?.id ?? 'unknown';
|
|
871
|
+
scope = createInstanceScope(routeId);
|
|
872
|
+
instanceScopes.set(renderKey, scope);
|
|
873
|
+
}
|
|
874
|
+
return scope;
|
|
875
|
+
}
|