@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,602 @@
|
|
|
1
|
+
import { isNotFoundError, isRedirectError, type PNextRedirectError } from '../../api/navigation';
|
|
2
|
+
import { runWithRequest, setPhase } from '../../request/context';
|
|
3
|
+
import { toNextRequest } from '../../api/server';
|
|
4
|
+
import { revalidationEventCount } from '../cache/revalidate';
|
|
5
|
+
import { serializeBoundaryError } from '../lifecycle/error-serialize';
|
|
6
|
+
import { lookupAction } from './registry';
|
|
7
|
+
import { instanceActionRouteId, isInstanceActionId, lookupActionInstance } from './instances';
|
|
8
|
+
import { inlineActionModuleKey, isInlineActionId } from './rewrite';
|
|
9
|
+
import {
|
|
10
|
+
ACTION_STREAM_CONTENT_TYPE,
|
|
11
|
+
createTempRefScope,
|
|
12
|
+
extractTempRefs,
|
|
13
|
+
isReadableStreamReturn,
|
|
14
|
+
serializeReturnWithTempRefs,
|
|
15
|
+
} from '../misc/action-return';
|
|
16
|
+
import {
|
|
17
|
+
ACTION_ARGS_FIELD,
|
|
18
|
+
ACTION_ID_FIELD,
|
|
19
|
+
FORM_STATE_FIELD,
|
|
20
|
+
ACTION_ID_HEADER,
|
|
21
|
+
ACTION_NOT_FOUND_HEADER,
|
|
22
|
+
NEXT_ACTION_NOT_FOUND_HEADER,
|
|
23
|
+
ACTION_REDIRECT_HEADER,
|
|
24
|
+
ACTION_REFRESH_HEADER,
|
|
25
|
+
NEXT_ACTION_REVALIDATED_HEADER,
|
|
26
|
+
ARG_ACTION_MARKER,
|
|
27
|
+
ARG_FILE_MARKER,
|
|
28
|
+
ARG_FILE_PREFIX,
|
|
29
|
+
ARG_FORMDATA_MARKER,
|
|
30
|
+
ARG_FORMDATA_PREFIX,
|
|
31
|
+
REDIRECT_TYPE_HEADER,
|
|
32
|
+
ACTION_ERROR_DIGEST_HEADER,
|
|
33
|
+
MASKED_ACTION_ERROR,
|
|
34
|
+
MAX_ACTION_ARGS,
|
|
35
|
+
tooManyArgsMessage,
|
|
36
|
+
type ActionSuccessBody,
|
|
37
|
+
} from './protocol';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Framework-agnostic runtime handler for a server-action RPC. It reads the action id header, decodes
|
|
41
|
+
* the arguments (JSON args as text/plain, or multipart/urlencoded FormData for form submissions),
|
|
42
|
+
* resolves the action via the module registry or the live-instance registry, and invokes it inside a
|
|
43
|
+
* request scope so headers()/cookies() work and cookie mutations propagate to the response.
|
|
44
|
+
* redirect()/notFound() thrown by the action are translated to the right HTTP responses; other throws
|
|
45
|
+
* become masked (prod) or detailed (dev) text/plain errors; successes become { data } JSON.
|
|
46
|
+
*
|
|
47
|
+
* This module never imports build/dev machinery - `importModule` and `rerenderRoute` are the only
|
|
48
|
+
* bridges to real module loading/rendering, which keeps the endpoint portable across runtimes.
|
|
49
|
+
*/
|
|
50
|
+
export interface RunActionOptions {
|
|
51
|
+
/** Load an action module by its registered modulePath. Injected by the caller. */
|
|
52
|
+
importModule: (modulePath: string) => Promise<Record<string, unknown>>;
|
|
53
|
+
/**
|
|
54
|
+
* Re-render a route server-side to re-register live inline-action instances
|
|
55
|
+
* (see instances.ts) after a process restart. Optional; without it a cold
|
|
56
|
+
* instance id fails with unknown-action.
|
|
57
|
+
*/
|
|
58
|
+
rerenderRoute?: (routeId: string, request: Request) => Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Import an app module by its workspace-relative source key so its
|
|
61
|
+
* compile-injected inline-action registrations run (cold 'if:' id lookups).
|
|
62
|
+
*/
|
|
63
|
+
importSourceModule?: (moduleKey: string) => Promise<void>;
|
|
64
|
+
/** Dev mode: error bodies carry real messages instead of the masked text. */
|
|
65
|
+
dev?: boolean;
|
|
66
|
+
/** Body size limit in bytes (Next default 1mb). */
|
|
67
|
+
maxBodyBytes?: number;
|
|
68
|
+
/**
|
|
69
|
+
* Report a thrown action error to the app's onRequestError instrumentation.
|
|
70
|
+
* Injected by the caller (compat glue) so this module stays portable; called
|
|
71
|
+
* once for a real (non control-flow) throw before the masked 500 is returned.
|
|
72
|
+
*/
|
|
73
|
+
onError?: (error: unknown, request: Request) => void | Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Server-render a React element tree an action RETURNED to island wire HTML
|
|
76
|
+
* (the JSON return serializer cannot carry vnode Symbols). Injected by the
|
|
77
|
+
* caller (register-actions) so this module never imports the renderer; when
|
|
78
|
+
* absent, element returns fall through the JSON serializer unchanged.
|
|
79
|
+
*/
|
|
80
|
+
renderActionElement?: (element: unknown) => Promise<string>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Server-side fallback for client-action stubs. A `'use server'` function imported into a client
|
|
87
|
+
* component compiles, in the CLIENT bundle, to a stub that dispatches over `window.__PNEXT_ACTIONS__`.
|
|
88
|
+
* That same bundle server-renders the client component, so when such an action is *called* on the
|
|
89
|
+
* server the stub has no window and would throw. Expose the endpoint's own id-to-module resolution on
|
|
90
|
+
* a global the stub can reach, so it instead runs the real compiled action, inheriting the request
|
|
91
|
+
* scope already active around it.
|
|
92
|
+
*/
|
|
93
|
+
function installServerActionDispatch(
|
|
94
|
+
importModule: (modulePath: string) => Promise<Record<string, unknown>>,
|
|
95
|
+
): void {
|
|
96
|
+
(
|
|
97
|
+
globalThis as {
|
|
98
|
+
__PNEXT_SERVER_ACTIONS__?: { call(id: string, args: unknown[]): Promise<unknown> };
|
|
99
|
+
}
|
|
100
|
+
).__PNEXT_SERVER_ACTIONS__ = {
|
|
101
|
+
async call(dispatchId: string, dispatchArgs: unknown[]): Promise<unknown> {
|
|
102
|
+
const target = lookupAction(dispatchId);
|
|
103
|
+
if (!target) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Failed to find Server Action "${dispatchId}". This request might be from an older or newer deployment.`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
const module = await importModule(target.modulePath);
|
|
109
|
+
const fn = module[target.exportName];
|
|
110
|
+
if (typeof fn !== 'function') {
|
|
111
|
+
throw new Error(`Failed to find Server Action "${dispatchId}".`);
|
|
112
|
+
}
|
|
113
|
+
return (fn as (...a: unknown[]) => unknown)(...dispatchArgs);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function runAction(request: Request, options: RunActionOptions): Promise<Response> {
|
|
119
|
+
const id = request.headers.get(ACTION_ID_HEADER);
|
|
120
|
+
if (!id) {
|
|
121
|
+
return errorResponse(`Missing ${ACTION_ID_HEADER} header`, 400);
|
|
122
|
+
}
|
|
123
|
+
// Arm the server-side stub dispatcher for this request's module loader before
|
|
124
|
+
// resolving/running the action (a client-imported action may re-render the
|
|
125
|
+
// route, whose SSR captures client stubs that dispatch back through here).
|
|
126
|
+
installServerActionDispatch(options.importModule);
|
|
127
|
+
|
|
128
|
+
// A resolution failure (a compiled action module that fails to import, a route re-render that
|
|
129
|
+
// throws) must surface as the recognizable 404 not-found wire - never an unhandled 500 the client
|
|
130
|
+
// reads as "unexpected response". A forwarded action dispatched from another page hits the same
|
|
131
|
+
// global resolution and needs the same graceful degradation.
|
|
132
|
+
let action: ((...args: unknown[]) => unknown) | undefined;
|
|
133
|
+
try {
|
|
134
|
+
action = await resolveAction(id, request, options);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error(error);
|
|
137
|
+
action = undefined;
|
|
138
|
+
}
|
|
139
|
+
if (!action) {
|
|
140
|
+
// Wire + log shape asserted by the actions-unrecognized suite: the log is
|
|
141
|
+
// the two-line "Failed to find" text; the RPC body is the exact message
|
|
142
|
+
// React's flight client throws for a missing action (unmasked even in
|
|
143
|
+
// prod — version skew is actionable for users).
|
|
144
|
+
console.error(
|
|
145
|
+
`Failed to find Server Action "${id}". This request might be from an older or newer deployment.\n` +
|
|
146
|
+
'Read more: https://nextjs.org/docs/messages/failed-to-find-server-action',
|
|
147
|
+
);
|
|
148
|
+
return new Response(`Server Action "${id}" was not found on the server.`, {
|
|
149
|
+
status: 404,
|
|
150
|
+
headers: {
|
|
151
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
152
|
+
[NEXT_ACTION_NOT_FOUND_HEADER]: '1',
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const limit = options.maxBodyBytes ?? DEFAULT_BODY_LIMIT_BYTES;
|
|
158
|
+
let args: unknown[];
|
|
159
|
+
try {
|
|
160
|
+
args = await decodeArgs(request, limit);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (error instanceof BodyLimitError) {
|
|
163
|
+
console.error(`Error: ${error.message}`);
|
|
164
|
+
console.error(
|
|
165
|
+
'To configure the body size limit for Server Actions, see: https://nextjs.org/docs/app/api-reference/next-config-js/serverActions#bodysizelimit',
|
|
166
|
+
);
|
|
167
|
+
return errorResponse(options.dev ? error.message : MASKED_ACTION_ERROR, 500);
|
|
168
|
+
}
|
|
169
|
+
return errorResponse(`Invalid action payload: ${messageOf(error)}`, 400);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
args = await reviveActionRefs(args, id => resolveAction(id, request, options));
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (error instanceof UnknownActionReferenceError) {
|
|
176
|
+
return errorResponse(error.message, 404);
|
|
177
|
+
}
|
|
178
|
+
return errorResponse(`Invalid action payload: ${messageOf(error)}`, 400);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (args.length > MAX_ACTION_ARGS) {
|
|
182
|
+
const message = tooManyArgsMessage(args.length);
|
|
183
|
+
console.error(`Error: ${message}`);
|
|
184
|
+
return errorResponse(options.dev ? message : MASKED_ACTION_ERROR, 500);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Temporary-reference scope: strip the client's temp-ref tags off the decoded
|
|
188
|
+
// args (so the action sees clean objects) and remember object -> id, so a
|
|
189
|
+
// returned arg object serializes back to the identical client reference.
|
|
190
|
+
const tempRefs = createTempRefScope();
|
|
191
|
+
args = args.map(arg => extractTempRefs(arg, tempRefs));
|
|
192
|
+
|
|
193
|
+
// Run inside a request scope with a response-headers channel: headers() and
|
|
194
|
+
// cookies() read the RPC request (which carries the browser's live cookies),
|
|
195
|
+
// and cookies().set()/delete() append set-cookie onto the channel.
|
|
196
|
+
const responseHeaders = new Headers();
|
|
197
|
+
const revalidationsBefore = revalidationEventCount();
|
|
198
|
+
const nextRequest = toNextRequest(request);
|
|
199
|
+
try {
|
|
200
|
+
// The action body runs under the 'action' phase so cookies().set() is legal
|
|
201
|
+
// here (and illegal once the phase flips to 'render'/'after' — phase-changes
|
|
202
|
+
// suite). Restored to 'render' once the action settles so a follow-up
|
|
203
|
+
// redirect-target render is treated as a render.
|
|
204
|
+
setPhase('action');
|
|
205
|
+
const data = await runWithRequest(
|
|
206
|
+
nextRequest,
|
|
207
|
+
() => Promise.resolve(action(...args)),
|
|
208
|
+
{},
|
|
209
|
+
{ responseHeaders },
|
|
210
|
+
).finally(() => setPhase('render'));
|
|
211
|
+
const refresh =
|
|
212
|
+
revalidationEventCount() !== revalidationsBefore || hasSetCookie(responseHeaders);
|
|
213
|
+
// A ReadableStream return streams the raw bytes to the client unbuffered
|
|
214
|
+
// (actions-streaming): JSON can't carry a stream, so the body IS the stream
|
|
215
|
+
// under a marker content-type the client detects.
|
|
216
|
+
if (isReadableStreamReturn(data)) {
|
|
217
|
+
const headers = new Headers({ 'content-type': ACTION_STREAM_CONTENT_TYPE });
|
|
218
|
+
setRefreshHeaders(headers, refresh);
|
|
219
|
+
return withResponseHeaders(new Response(data, { status: 200, headers }), responseHeaders);
|
|
220
|
+
}
|
|
221
|
+
const serialized = await serializeReturnWithTempRefs(
|
|
222
|
+
data,
|
|
223
|
+
tempRefs,
|
|
224
|
+
options.renderActionElement,
|
|
225
|
+
);
|
|
226
|
+
const body: ActionSuccessBody = {
|
|
227
|
+
data: encodeBinaryReturn(serialized),
|
|
228
|
+
};
|
|
229
|
+
const headers = new Headers({ 'content-type': 'application/json; charset=utf-8' });
|
|
230
|
+
setRefreshHeaders(headers, refresh);
|
|
231
|
+
return withResponseHeaders(
|
|
232
|
+
new Response(JSON.stringify(body), { status: 200, headers }),
|
|
233
|
+
responseHeaders,
|
|
234
|
+
);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (isRedirectError(error)) {
|
|
237
|
+
return withResponseHeaders(
|
|
238
|
+
redirectResponse(
|
|
239
|
+
error,
|
|
240
|
+
revalidationEventCount() !== revalidationsBefore || hasSetCookie(responseHeaders),
|
|
241
|
+
),
|
|
242
|
+
responseHeaders,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
if (isNotFoundError(error)) {
|
|
246
|
+
return withResponseHeaders(
|
|
247
|
+
new Response('Not found', {
|
|
248
|
+
status: 404,
|
|
249
|
+
headers: {
|
|
250
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
251
|
+
[ACTION_NOT_FOUND_HEADER]: '1',
|
|
252
|
+
},
|
|
253
|
+
}),
|
|
254
|
+
responseHeaders,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
const message = messageOf(error);
|
|
258
|
+
// Compute the boundary digest with the same deterministic scheme the render
|
|
259
|
+
// path uses (error-serialize.ts) so the client-visible digest and the server
|
|
260
|
+
// log agree. Next's prod server log prints the ORIGINAL error trace followed
|
|
261
|
+
// by `digest: '<digest>'` so operators can correlate the redacted client
|
|
262
|
+
// digest with the full server-side stack.
|
|
263
|
+
let digest: string | undefined;
|
|
264
|
+
if (error instanceof Error) {
|
|
265
|
+
digest = serializeBoundaryError({ error, dev: Boolean(options.dev) }).digest;
|
|
266
|
+
const trace = error.stack ?? error.message;
|
|
267
|
+
console.error(typeof digest === 'string' ? `${trace}\n digest: '${digest}'` : trace);
|
|
268
|
+
} else {
|
|
269
|
+
console.error(error);
|
|
270
|
+
}
|
|
271
|
+
await options.onError?.(error, request);
|
|
272
|
+
const masked = errorResponse(options.dev ? message : MASKED_ACTION_ERROR, 500);
|
|
273
|
+
if (typeof digest === 'string') masked.headers.set(ACTION_ERROR_DIGEST_HEADER, digest);
|
|
274
|
+
return withResponseHeaders(masked, responseHeaders);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function resolveAction(
|
|
279
|
+
id: string,
|
|
280
|
+
request: Request,
|
|
281
|
+
options: RunActionOptions,
|
|
282
|
+
): Promise<((...args: unknown[]) => unknown) | undefined> {
|
|
283
|
+
if (isInlineActionId(id)) {
|
|
284
|
+
let instance = lookupActionInstance(id);
|
|
285
|
+
if (!instance && options.importSourceModule) {
|
|
286
|
+
const moduleKey = inlineActionModuleKey(id);
|
|
287
|
+
if (moduleKey) {
|
|
288
|
+
try {
|
|
289
|
+
await options.importSourceModule(moduleKey);
|
|
290
|
+
} catch {
|
|
291
|
+
// fall through to the unknown-action error
|
|
292
|
+
}
|
|
293
|
+
instance = lookupActionInstance(id);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return instance;
|
|
297
|
+
}
|
|
298
|
+
if (isInstanceActionId(id)) {
|
|
299
|
+
let instance = lookupActionInstance(id);
|
|
300
|
+
if (!instance && options.rerenderRoute) {
|
|
301
|
+
const routeId = instanceActionRouteId(id);
|
|
302
|
+
if (routeId) {
|
|
303
|
+
try {
|
|
304
|
+
await options.rerenderRoute(routeId, request);
|
|
305
|
+
} catch {
|
|
306
|
+
// fall through to the unknown-action error below
|
|
307
|
+
}
|
|
308
|
+
instance = lookupActionInstance(id);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return instance;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const target = lookupAction(id);
|
|
315
|
+
if (!target) return undefined;
|
|
316
|
+
const module = await options.importModule(target.modulePath);
|
|
317
|
+
const action = module[target.exportName];
|
|
318
|
+
return typeof action === 'function' ? (action as (...args: unknown[]) => unknown) : undefined;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
class BodyLimitError extends Error {}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Drain the request body ONCE into bytes and enforce the size limit. All decoding then happens off
|
|
325
|
+
* this buffer: the text path decodes the bytes directly (a single whole-buffer UTF-8 decode, so
|
|
326
|
+
* multi-byte characters can never split across chunk boundaries), and the multipart path parses a
|
|
327
|
+
* reconstructed Request whose stale content-length header is dropped - the upstream request may have
|
|
328
|
+
* been rewrapped by forwarding/middleware bridges, and a mismatched declared length must not truncate
|
|
329
|
+
* the buffered body.
|
|
330
|
+
*/
|
|
331
|
+
async function readBodyBytesWithLimit(request: Request, limit: number): Promise<ArrayBuffer> {
|
|
332
|
+
const bytes = await request.arrayBuffer();
|
|
333
|
+
if (bytes.byteLength > limit) {
|
|
334
|
+
throw new BodyLimitError(`Body exceeded ${formatLimit(limit)} limit`);
|
|
335
|
+
}
|
|
336
|
+
return bytes;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function bufferedFormRequest(request: Request, bytes: ArrayBuffer): Request {
|
|
340
|
+
const headers = new Headers(request.headers);
|
|
341
|
+
headers.delete('content-length');
|
|
342
|
+
return new Request(request.url, {
|
|
343
|
+
method: request.method,
|
|
344
|
+
headers,
|
|
345
|
+
body: bytes.byteLength === 0 ? undefined : bytes,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function formatLimit(limit: number) {
|
|
350
|
+
if (limit % (1024 * 1024) === 0) return `${limit / (1024 * 1024)}mb`;
|
|
351
|
+
if (limit % 1024 === 0) return `${limit / 1024}kb`;
|
|
352
|
+
return `${limit} bytes`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Decode the request body into an argument array.
|
|
357
|
+
* - multipart/form-data or urlencoded without ACTION_ARGS_FIELD -> a single
|
|
358
|
+
* FormData argument (form submissions; the action id field is stripped).
|
|
359
|
+
* - multipart with ACTION_ARGS_FIELD -> the JSON arg list with FormData/File
|
|
360
|
+
* slots re-materialized from the other parts (bound args ahead of a form).
|
|
361
|
+
* - anything else (the client runtime sends text/plain) -> JSON args array.
|
|
362
|
+
* - empty body -> no args.
|
|
363
|
+
*/
|
|
364
|
+
async function decodeArgs(request: Request, limit: number): Promise<unknown[]> {
|
|
365
|
+
const bytes = await readBodyBytesWithLimit(request, limit);
|
|
366
|
+
const contentType = request.headers.get('content-type') ?? '';
|
|
367
|
+
|
|
368
|
+
if (contentType.includes('multipart/form-data') || contentType.includes('form-urlencoded')) {
|
|
369
|
+
const form = await bufferedFormRequest(request, bytes).formData();
|
|
370
|
+
const argsField = form.get(ACTION_ARGS_FIELD);
|
|
371
|
+
form.delete(ACTION_ID_FIELD);
|
|
372
|
+
if (typeof argsField === 'string') {
|
|
373
|
+
form.delete(ACTION_ARGS_FIELD);
|
|
374
|
+
return decodeStructuredArgs(argsField, form);
|
|
375
|
+
}
|
|
376
|
+
// Progressive useActionState submission: the form carries its current
|
|
377
|
+
// state; the action signature is (state, formData).
|
|
378
|
+
const stateField = form.get(FORM_STATE_FIELD);
|
|
379
|
+
if (typeof stateField === 'string') {
|
|
380
|
+
form.delete(FORM_STATE_FIELD);
|
|
381
|
+
let state: unknown = null;
|
|
382
|
+
try {
|
|
383
|
+
state = JSON.parse(stateField);
|
|
384
|
+
} catch {
|
|
385
|
+
state = stateField;
|
|
386
|
+
}
|
|
387
|
+
return [state, form];
|
|
388
|
+
}
|
|
389
|
+
return [form];
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Decode the buffered bytes in one shot (never per-chunk): a streaming or
|
|
393
|
+
// rewrapped body can otherwise split a multi-byte UTF-8 character across
|
|
394
|
+
// chunk boundaries (decode-req-body suite, 100k あ ≈ 300KB).
|
|
395
|
+
const text = new TextDecoder('utf-8').decode(bytes);
|
|
396
|
+
if (text.trim() === '') return [];
|
|
397
|
+
|
|
398
|
+
const parsed = JSON.parse(text) as unknown;
|
|
399
|
+
return Array.isArray(parsed) ? (parsed as unknown[]) : [parsed];
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Revive server-action references passed as arguments: the client encodes a
|
|
404
|
+
* function arg that is itself an action as its wire id; the server swaps the
|
|
405
|
+
* marker back for the live server-side function, preserving identity (an
|
|
406
|
+
* action passed through the wire compares === to the module/instance action).
|
|
407
|
+
*/
|
|
408
|
+
async function reviveActionRefs(
|
|
409
|
+
args: unknown[],
|
|
410
|
+
resolve: (id: string) => Promise<((...a: unknown[]) => unknown) | undefined>,
|
|
411
|
+
): Promise<unknown[]> {
|
|
412
|
+
const reviveValue = async (value: unknown): Promise<unknown> => {
|
|
413
|
+
if (value === null || typeof value !== 'object') return value;
|
|
414
|
+
if (Array.isArray(value)) return Promise.all(value.map(reviveValue));
|
|
415
|
+
const record = value as Record<string, unknown>;
|
|
416
|
+
if (typeof record[ARG_ACTION_MARKER] === 'string') {
|
|
417
|
+
const fn = await resolve(record[ARG_ACTION_MARKER]);
|
|
418
|
+
if (!fn) {
|
|
419
|
+
const id = String(record[ARG_ACTION_MARKER]);
|
|
420
|
+
console.error(
|
|
421
|
+
`Failed to find Server Action "${id}". This request might be from an older or newer deployment.\n` +
|
|
422
|
+
'Read more: https://nextjs.org/docs/messages/failed-to-find-server-action',
|
|
423
|
+
);
|
|
424
|
+
throw new UnknownActionReferenceError(id);
|
|
425
|
+
}
|
|
426
|
+
return fn;
|
|
427
|
+
}
|
|
428
|
+
if (record.$$pnext_undefined === true) return undefined;
|
|
429
|
+
const binary = reviveBinaryMarker(record);
|
|
430
|
+
if (binary !== undefined) return binary;
|
|
431
|
+
if (typeof record.$$pnext_date === 'string') return new Date(record.$$pnext_date);
|
|
432
|
+
if (Array.isArray(record.$$pnext_map)) {
|
|
433
|
+
const entries = await Promise.all(
|
|
434
|
+
(record.$$pnext_map as [unknown, unknown][]).map(
|
|
435
|
+
async ([k, v]) => [await reviveValue(k), await reviveValue(v)] as [unknown, unknown],
|
|
436
|
+
),
|
|
437
|
+
);
|
|
438
|
+
return new Map(entries);
|
|
439
|
+
}
|
|
440
|
+
if (Array.isArray(record.$$pnext_set)) {
|
|
441
|
+
return new Set(await Promise.all((record.$$pnext_set as unknown[]).map(reviveValue)));
|
|
442
|
+
}
|
|
443
|
+
if (value instanceof FormData || value instanceof Blob) return value;
|
|
444
|
+
if (
|
|
445
|
+
Object.getPrototypeOf(value) !== Object.prototype &&
|
|
446
|
+
Object.getPrototypeOf(value) !== null
|
|
447
|
+
) {
|
|
448
|
+
return value;
|
|
449
|
+
}
|
|
450
|
+
const out: Record<string, unknown> = {};
|
|
451
|
+
for (const [key, item] of Object.entries(record)) out[key] = await reviveValue(item);
|
|
452
|
+
return out;
|
|
453
|
+
};
|
|
454
|
+
return Promise.all(args.map(reviveValue));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const TYPED_ARRAY_MARKER = '$$pnext_typed_array';
|
|
458
|
+
const ARRAY_BUFFER_MARKER = '$$pnext_array_buffer';
|
|
459
|
+
|
|
460
|
+
const TYPED_ARRAY_CTORS: Record<string, new (buffer: ArrayBuffer) => ArrayBufferView> = {
|
|
461
|
+
Int8Array,
|
|
462
|
+
Uint8Array,
|
|
463
|
+
Uint8ClampedArray,
|
|
464
|
+
Int16Array,
|
|
465
|
+
Uint16Array,
|
|
466
|
+
Int32Array,
|
|
467
|
+
Uint32Array,
|
|
468
|
+
Float32Array,
|
|
469
|
+
Float64Array,
|
|
470
|
+
BigInt64Array,
|
|
471
|
+
BigUint64Array,
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
function isTypedArrayView(value: unknown): value is ArrayBufferView {
|
|
475
|
+
return ArrayBuffer.isView(value) && !(value instanceof DataView);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** Deep-copy a return value, marker-encoding typed arrays / ArrayBuffers. */
|
|
479
|
+
function encodeBinaryReturn(value: unknown, seen = new Set<unknown>()): unknown {
|
|
480
|
+
if (value === null || typeof value !== 'object') return value;
|
|
481
|
+
if (value instanceof ArrayBuffer) {
|
|
482
|
+
return { [ARRAY_BUFFER_MARKER]: Buffer.from(new Uint8Array(value)).toString('base64') };
|
|
483
|
+
}
|
|
484
|
+
if (isTypedArrayView(value)) {
|
|
485
|
+
const bytes = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
486
|
+
return {
|
|
487
|
+
[TYPED_ARRAY_MARKER]: {
|
|
488
|
+
type: value.constructor.name,
|
|
489
|
+
data: Buffer.from(bytes).toString('base64'),
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
if (seen.has(value)) return value;
|
|
494
|
+
seen.add(value);
|
|
495
|
+
if (Array.isArray(value)) return value.map(item => encodeBinaryReturn(item, seen));
|
|
496
|
+
const proto = Object.getPrototypeOf(value) as object | null;
|
|
497
|
+
if (proto !== Object.prototype && proto !== null) return value;
|
|
498
|
+
const out: Record<string, unknown> = {};
|
|
499
|
+
for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
|
|
500
|
+
out[key] = encodeBinaryReturn(item, seen);
|
|
501
|
+
}
|
|
502
|
+
return out;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** Revive a typed-array / ArrayBuffer wire marker back into a live view, else undefined. */
|
|
506
|
+
function reviveBinaryMarker(record: Record<string, unknown>): unknown {
|
|
507
|
+
const typed = record[TYPED_ARRAY_MARKER];
|
|
508
|
+
if (typed && typeof typed === 'object') {
|
|
509
|
+
const { type, data } = typed as { type?: string; data?: string };
|
|
510
|
+
const Ctor = type ? TYPED_ARRAY_CTORS[type] : undefined;
|
|
511
|
+
if (Ctor && typeof data === 'string') {
|
|
512
|
+
const bytes = Buffer.from(data, 'base64');
|
|
513
|
+
return new Ctor(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const buffer = record[ARRAY_BUFFER_MARKER];
|
|
517
|
+
if (typeof buffer === 'string') {
|
|
518
|
+
const bytes = Buffer.from(buffer, 'base64');
|
|
519
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
520
|
+
}
|
|
521
|
+
return undefined;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
class UnknownActionReferenceError extends Error {
|
|
525
|
+
constructor(public actionId: string) {
|
|
526
|
+
super(`Server Action "${actionId}" was not found on the server.`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function decodeStructuredArgs(argsJson: string, form: FormData): unknown[] {
|
|
531
|
+
const parsed = JSON.parse(argsJson) as unknown;
|
|
532
|
+
const list = Array.isArray(parsed) ? (parsed as unknown[]) : [parsed];
|
|
533
|
+
return list.map(item => reviveArg(item, form));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function reviveArg(value: unknown, form: FormData): unknown {
|
|
537
|
+
if (value === null || typeof value !== 'object') return value;
|
|
538
|
+
if (Array.isArray(value)) return value.map(item => reviveArg(item, form));
|
|
539
|
+
|
|
540
|
+
const record = value as Record<string, unknown>;
|
|
541
|
+
if (typeof record[ARG_FORMDATA_MARKER] === 'number') {
|
|
542
|
+
const slot = record[ARG_FORMDATA_MARKER];
|
|
543
|
+
const prefix = `${ARG_FORMDATA_PREFIX}${slot}:`;
|
|
544
|
+
const data = new FormData();
|
|
545
|
+
for (const [key, item] of form.entries()) {
|
|
546
|
+
if (key.startsWith(prefix)) data.append(key.slice(prefix.length), item as string | Blob);
|
|
547
|
+
}
|
|
548
|
+
return data;
|
|
549
|
+
}
|
|
550
|
+
if (typeof record[ARG_FILE_MARKER] === 'number') {
|
|
551
|
+
const slot = record[ARG_FILE_MARKER];
|
|
552
|
+
return form.get(`${ARG_FILE_PREFIX}${slot}`);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const revived: Record<string, unknown> = {};
|
|
556
|
+
for (const [key, item] of Object.entries(record)) revived[key] = reviveArg(item, form);
|
|
557
|
+
return revived;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function redirectResponse(error: PNextRedirectError, refresh: boolean): Response {
|
|
561
|
+
const redirectType = (error as PNextRedirectError & { redirectType?: string }).redirectType;
|
|
562
|
+
// 303: fetch() does not follow 307/308 the way a browser follows a nav, and
|
|
563
|
+
// a no-JS form submit needs the browser to follow with a GET. The redirect
|
|
564
|
+
// marker header lets the client runtime treat this as a framework redirect
|
|
565
|
+
// (soft-navigable) rather than an opaque HTTP redirect.
|
|
566
|
+
const headers = new Headers({
|
|
567
|
+
location: error.location,
|
|
568
|
+
[ACTION_REDIRECT_HEADER]: error.location,
|
|
569
|
+
});
|
|
570
|
+
if (redirectType === 'replace') headers.set(REDIRECT_TYPE_HEADER, 'replace');
|
|
571
|
+
setRefreshHeaders(headers, refresh);
|
|
572
|
+
return new Response(null, { status: 303, headers });
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function setRefreshHeaders(headers: Headers, refresh: boolean): void {
|
|
576
|
+
if (!refresh) return;
|
|
577
|
+
headers.set(ACTION_REFRESH_HEADER, '1');
|
|
578
|
+
headers.set(NEXT_ACTION_REVALIDATED_HEADER, '1');
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function errorResponse(message: string, status: number): Response {
|
|
582
|
+
return new Response(message, {
|
|
583
|
+
status,
|
|
584
|
+
headers: { 'content-type': 'text/plain; charset=utf-8' },
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/** Merge scope-collected headers (set-cookie appends) onto the response. */
|
|
589
|
+
function withResponseHeaders(response: Response, extra: Headers): Response {
|
|
590
|
+
const setCookies = (extra as Headers & { getSetCookie?: () => string[] }).getSetCookie?.() ?? [];
|
|
591
|
+
if (setCookies.length === 0) return response;
|
|
592
|
+
for (const cookie of setCookies) response.headers.append('set-cookie', cookie);
|
|
593
|
+
return response;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function hasSetCookie(headers: Headers): boolean {
|
|
597
|
+
return headers.has('set-cookie');
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function messageOf(error: unknown): string {
|
|
601
|
+
return error instanceof Error ? error.message : String(error);
|
|
602
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ACTION_REDIRECT_HEADER, REDIRECT_TYPE_HEADER } from './protocol';
|
|
2
|
+
|
|
3
|
+
export const ACTION_FLIGHT_CONTENT_TYPE = 'text/x-component';
|
|
4
|
+
|
|
5
|
+
export interface ActionFlightPayload {
|
|
6
|
+
a?: unknown;
|
|
7
|
+
f: { kind: 'pnext-html'; html: string; url: string } | '';
|
|
8
|
+
q: string;
|
|
9
|
+
i: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CreateActionFlightResponseOptions {
|
|
13
|
+
pageResponse: Response;
|
|
14
|
+
target: URL;
|
|
15
|
+
actionHeaders: Headers;
|
|
16
|
+
redirect?: string;
|
|
17
|
+
redirectType?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function createActionFlightResponse({
|
|
21
|
+
pageResponse,
|
|
22
|
+
target,
|
|
23
|
+
actionHeaders,
|
|
24
|
+
redirect,
|
|
25
|
+
redirectType,
|
|
26
|
+
}: CreateActionFlightResponseOptions): Promise<Response> {
|
|
27
|
+
const html = await pageResponse.text();
|
|
28
|
+
const payload: ActionFlightPayload = {
|
|
29
|
+
f: { kind: 'pnext-html', html, url: target.pathname + target.search },
|
|
30
|
+
q: target.search,
|
|
31
|
+
i: false,
|
|
32
|
+
};
|
|
33
|
+
const headers = new Headers(pageResponse.headers);
|
|
34
|
+
headers.set('content-type', ACTION_FLIGHT_CONTENT_TYPE);
|
|
35
|
+
copyActionHeaders(actionHeaders, headers);
|
|
36
|
+
if (redirect) headers.set(ACTION_REDIRECT_HEADER, redirect);
|
|
37
|
+
if (redirectType) headers.set(REDIRECT_TYPE_HEADER, redirectType);
|
|
38
|
+
// A framework redirect returns HTTP 303 (matching Next's action-redirect wire
|
|
39
|
+
// the e2e suite asserts) with the destination page HTML inline in the flight
|
|
40
|
+
// payload; the client swaps the document without a follow-up GET. A plain
|
|
41
|
+
// flight envelope (revalidation, no redirect) stays 200.
|
|
42
|
+
return new Response(JSON.stringify(payload), { status: redirect ? 303 : 200, headers });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function copyActionHeaders(from: Headers, to: Headers): void {
|
|
46
|
+
for (const name of ['x-pnext-action-refresh', 'x-action-revalidated']) {
|
|
47
|
+
const value = from.get(name);
|
|
48
|
+
if (value) to.set(name, value);
|
|
49
|
+
}
|
|
50
|
+
const setCookies = (from as Headers & { getSetCookie?: () => string[] }).getSetCookie?.() ?? [];
|
|
51
|
+
for (const cookie of setCookies) to.append('set-cookie', cookie);
|
|
52
|
+
}
|