@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,295 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Metadata,
|
|
3
|
+
MetadataOpenGraph,
|
|
4
|
+
PageProps,
|
|
5
|
+
RouteManifestEntry,
|
|
6
|
+
Viewport,
|
|
7
|
+
} from '../types';
|
|
8
|
+
import { staticMetadataForPathFromFiles, type StaticMetadataFile } from '../routing/metadata';
|
|
9
|
+
import { coerceString } from '../render/metadata';
|
|
10
|
+
import { readSourceText } from '../resolve/source-text';
|
|
11
|
+
import { SPAN_TYPE, withChildSpan } from './otel/tracer';
|
|
12
|
+
import { settledPromise } from '../render/renderer';
|
|
13
|
+
import {
|
|
14
|
+
createVaryingParams,
|
|
15
|
+
createVaryingSearchParams,
|
|
16
|
+
getMetadataVaryParamsAccumulator,
|
|
17
|
+
varyTrackingInfo,
|
|
18
|
+
type VaryTrackingInfo,
|
|
19
|
+
} from './segment/vary-params';
|
|
20
|
+
|
|
21
|
+
type NextGenerateMetadata = (
|
|
22
|
+
props: Pick<PageProps, 'params' | 'searchParams'>,
|
|
23
|
+
parent: Promise<Metadata>,
|
|
24
|
+
) => Metadata | Promise<Metadata>;
|
|
25
|
+
|
|
26
|
+
type NextGenerateViewport = (
|
|
27
|
+
props: Pick<PageProps, 'params' | 'searchParams'>,
|
|
28
|
+
) => Viewport | Promise<Viewport>;
|
|
29
|
+
|
|
30
|
+
export async function readNextModuleMetadata(
|
|
31
|
+
module: Record<string, unknown>,
|
|
32
|
+
props: PageProps,
|
|
33
|
+
parent: Metadata,
|
|
34
|
+
modulePath?: string,
|
|
35
|
+
): Promise<Metadata | undefined> {
|
|
36
|
+
if (typeof module.generateMetadata === 'function') {
|
|
37
|
+
const generate = module.generateMetadata as NextGenerateMetadata;
|
|
38
|
+
// `generateMetadata <page>` OTel span (ResolveMetadata.generateMetadata),
|
|
39
|
+
// named by the route-relative module path (e.g. `/app/[param]/layout`).
|
|
40
|
+
// Inert when otel-api is unavailable or no root span is open.
|
|
41
|
+
const headProps = headVaryTrackedProps(props);
|
|
42
|
+
const run = () =>
|
|
43
|
+
generate(
|
|
44
|
+
{ params: headProps.params, searchParams: headProps.searchParams },
|
|
45
|
+
Promise.resolve(resolvedParentMetadata(parent)),
|
|
46
|
+
);
|
|
47
|
+
const name = metadataSpanName(modulePath);
|
|
48
|
+
return await (name
|
|
49
|
+
? withChildSpan(
|
|
50
|
+
name,
|
|
51
|
+
SPAN_TYPE.generateMetadata,
|
|
52
|
+
{ 'next.page': name.slice('generateMetadata '.length) },
|
|
53
|
+
run,
|
|
54
|
+
)
|
|
55
|
+
: run());
|
|
56
|
+
}
|
|
57
|
+
return module.metadata as Metadata | undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Route-relative module path for a `generateMetadata` span name: an absolute `<root>/app/[param]/
|
|
61
|
+
// layout.tsx` becomes `generateMetadata /app/[param]/layout`. Next names the span with the app-dir-
|
|
62
|
+
// relative path prefixed by the `/app` segment. Returns undefined when the path cannot be derived, in
|
|
63
|
+
// which case the span is skipped rather than failing.
|
|
64
|
+
function metadataSpanName(modulePath: string | undefined): string | undefined {
|
|
65
|
+
if (!modulePath) return undefined;
|
|
66
|
+
const normalized = modulePath.replace(/\\/g, '/').replace(/\.[cm]?[jt]sx?$/, '');
|
|
67
|
+
// Keep everything from the LAST `/app/` segment onward (handles src/app too).
|
|
68
|
+
const idx = normalized.lastIndexOf('/app/');
|
|
69
|
+
const rel = idx >= 0 ? normalized.slice(idx) : `/${normalized.replace(/^\/+/, '')}`;
|
|
70
|
+
return `generateMetadata ${rel}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function readNextModuleViewport(
|
|
74
|
+
module: Record<string, unknown>,
|
|
75
|
+
props: PageProps,
|
|
76
|
+
): Promise<Viewport | undefined> {
|
|
77
|
+
if (typeof module.generateViewport === 'function') {
|
|
78
|
+
const headProps = headVaryTrackedProps(props);
|
|
79
|
+
return (module.generateViewport as NextGenerateViewport)({
|
|
80
|
+
params: headProps.params,
|
|
81
|
+
searchParams: headProps.searchParams,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return module.viewport as Viewport | undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* `generateMetadata` / `generateViewport` param access accrues to the HEAD segment's vary set, never the
|
|
89
|
+
* page's - a page that shares one payload across params but whose metadata reads `params.id` must keep
|
|
90
|
+
* the head keyed per id while the body stays shared.
|
|
91
|
+
*
|
|
92
|
+
* The incoming props already carry the PAGE's tracking view; re-wrap the raw object behind it against
|
|
93
|
+
* the head accumulator so reading here never widens the page's set. Untracked or hanging
|
|
94
|
+
* (fallback-shell) props pass through untouched - a hanging promise must keep its identity so the
|
|
95
|
+
* postpone machinery still sees it.
|
|
96
|
+
*/
|
|
97
|
+
function headVaryTrackedProps(props: PageProps): Pick<PageProps, 'params' | 'searchParams'> {
|
|
98
|
+
const head = getMetadataVaryParamsAccumulator();
|
|
99
|
+
if (!head) return { params: props.params, searchParams: props.searchParams };
|
|
100
|
+
return {
|
|
101
|
+
params: retrackSettled(props.params, (raw, info) =>
|
|
102
|
+
createVaryingParams(head, raw, info.optionalCatchAllParam),
|
|
103
|
+
),
|
|
104
|
+
searchParams: retrackSettled(props.searchParams, raw => createVaryingSearchParams(head, raw)),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Re-wrap a SETTLED params/searchParams promise around a fresh tracking view
|
|
110
|
+
* built from the ORIGINAL object behind it. Anything else (a hanging promise, an
|
|
111
|
+
* untracked value) is returned as-is.
|
|
112
|
+
*/
|
|
113
|
+
function retrackSettled<T extends Record<string, unknown>>(
|
|
114
|
+
promise: Promise<T>,
|
|
115
|
+
wrap: (raw: T, info: VaryTrackingInfo) => T,
|
|
116
|
+
): Promise<T> {
|
|
117
|
+
const settled = promise as Promise<T> & { status?: string; value?: T };
|
|
118
|
+
if (settled.status !== 'fulfilled' || settled.value === undefined) return promise;
|
|
119
|
+
const info = varyTrackingInfo(settled.value);
|
|
120
|
+
if (!info) return promise;
|
|
121
|
+
return settledPromise(wrap(info.raw as T, info));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function hasRuntimeMetadata(module: Record<string, unknown>) {
|
|
125
|
+
return typeof module.generateMetadata === 'function';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function shouldRenderMetadataInBody({
|
|
129
|
+
config,
|
|
130
|
+
request,
|
|
131
|
+
}: {
|
|
132
|
+
config: { htmlLimitedBots?: RegExp };
|
|
133
|
+
request?: Request;
|
|
134
|
+
}) {
|
|
135
|
+
const userAgent = request?.headers.get('user-agent') ?? '';
|
|
136
|
+
const limitedBot =
|
|
137
|
+
config.htmlLimitedBots ??
|
|
138
|
+
/Twitterbot|Slackbot|Bingbot|Discordbot|LinkedInBot|Google-PageRenderer|Chrome-Lighthouse|Lighthouse/i;
|
|
139
|
+
return !limitedBot.test(userAgent);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function resolvedParentMetadata(parent: Metadata): Metadata {
|
|
143
|
+
return {
|
|
144
|
+
...parent,
|
|
145
|
+
metadataBase: parent.metadataBase === undefined ? undefined : coerceString(parent.metadataBase),
|
|
146
|
+
keywords: normalizeKeywordList(parent.keywords),
|
|
147
|
+
openGraph: resolvedParentOpenGraph(parent.openGraph),
|
|
148
|
+
twitter: parent.twitter ?? {},
|
|
149
|
+
robots: parent.robots ?? {},
|
|
150
|
+
alternates: parent.alternates ?? {},
|
|
151
|
+
icons: parent.icons ?? {},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function resolvedParentOpenGraph(openGraph: MetadataOpenGraph | undefined): MetadataOpenGraph {
|
|
156
|
+
if (!openGraph) return {};
|
|
157
|
+
return {
|
|
158
|
+
...openGraph,
|
|
159
|
+
title:
|
|
160
|
+
openGraph.title === undefined ||
|
|
161
|
+
(typeof openGraph.title === 'object' && openGraph.title !== null)
|
|
162
|
+
? openGraph.title
|
|
163
|
+
: { absolute: coerceString(openGraph.title) },
|
|
164
|
+
images:
|
|
165
|
+
openGraph.images === undefined
|
|
166
|
+
? []
|
|
167
|
+
: Array.isArray(openGraph.images)
|
|
168
|
+
? openGraph.images
|
|
169
|
+
: [openGraph.images],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function normalizeKeywordList(value: Metadata['keywords']): (string | number)[] | undefined {
|
|
174
|
+
if (value === undefined) return undefined;
|
|
175
|
+
return Array.isArray(value) ? value : [value];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function warnNextMetadataBuildIssues(
|
|
179
|
+
appPath: string,
|
|
180
|
+
routes: RouteManifestEntry[],
|
|
181
|
+
staticMetadataFiles: StaticMetadataFile[],
|
|
182
|
+
) {
|
|
183
|
+
const warned = new Set<string>();
|
|
184
|
+
// A route's own file plus its layout chain: on a 23-route app with one shell
|
|
185
|
+
// that is the same handful of layouts 23 times over. Each file's four verdicts
|
|
186
|
+
// are derived once and reused, and the reads come from the build's shared
|
|
187
|
+
// source cache (the route-fact walk has already read every one of them).
|
|
188
|
+
const facts = new Map<string, MetadataSourceFacts>();
|
|
189
|
+
const factsFor = async (file: string) => {
|
|
190
|
+
let cached = facts.get(file);
|
|
191
|
+
if (!cached) {
|
|
192
|
+
cached = metadataSourceFacts(await readSourceTextOrEmpty(file));
|
|
193
|
+
facts.set(file, cached);
|
|
194
|
+
}
|
|
195
|
+
return cached;
|
|
196
|
+
};
|
|
197
|
+
for (const route of routes) {
|
|
198
|
+
if (route.kind !== 'page') continue;
|
|
199
|
+
const routeFacts = await Promise.all(routeSourceFiles(appPath, route).map(factsFor));
|
|
200
|
+
const some = (pick: (fact: MetadataSourceFacts) => boolean) => routeFacts.some(pick);
|
|
201
|
+
if (some(fact => fact.themeColor)) {
|
|
202
|
+
warnOnce(
|
|
203
|
+
warned,
|
|
204
|
+
`themeColor:${route.route}`,
|
|
205
|
+
`Unsupported metadata themeColor is configured in metadata export in ${route.route || '/'}. Please move it to viewport\nRead more: https://nextjs.org/docs/app/api-reference/functions/generate-viewport`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
if (some(fact => fact.appleTouchFullscreen)) {
|
|
209
|
+
warnOnce(
|
|
210
|
+
warned,
|
|
211
|
+
'apple-touch-fullscreen',
|
|
212
|
+
'Unsupported metadata other.apple-touch-fullscreen is configured in metadata export. Use appleWebApp instead.',
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
if (some(fact => fact.appleTouchIconPrecomposed)) {
|
|
216
|
+
warnOnce(
|
|
217
|
+
warned,
|
|
218
|
+
'apple-touch-icon-precomposed',
|
|
219
|
+
'Unsupported metadata other.apple-touch-icon-precomposed is configured in metadata export. Use icons.apple instead.',
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
if (
|
|
223
|
+
!some(fact => fact.metadataBase) &&
|
|
224
|
+
routeUsesRelativeSocialImage(route, routeFacts, staticMetadataFiles)
|
|
225
|
+
) {
|
|
226
|
+
warnOnce(
|
|
227
|
+
warned,
|
|
228
|
+
'metadataBase',
|
|
229
|
+
'metadataBase property in metadata export is not set for resolving social open graph or twitter images, using "http://localhost:3000". See https://nextjs.org/docs/app/api-reference/functions/generate-metadata#metadatabase',
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The app-dir files whose metadata exports apply to this route. */
|
|
236
|
+
function routeSourceFiles(appPath: string, route: RouteManifestEntry) {
|
|
237
|
+
const root = `${appPath}/`;
|
|
238
|
+
return route.sourceFiles.filter(file => file === appPath || file.startsWith(root));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function readSourceTextOrEmpty(file: string) {
|
|
242
|
+
try {
|
|
243
|
+
return await readSourceText(file);
|
|
244
|
+
} catch {
|
|
245
|
+
return '';
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Every metadata verdict this pass needs from one source file. The substring
|
|
251
|
+
* gates run first: a regex over the whole source costs ~40x a `includes`, and
|
|
252
|
+
* nearly every file has none of these keys.
|
|
253
|
+
*/
|
|
254
|
+
interface MetadataSourceFacts {
|
|
255
|
+
themeColor: boolean;
|
|
256
|
+
appleTouchFullscreen: boolean;
|
|
257
|
+
appleTouchIconPrecomposed: boolean;
|
|
258
|
+
metadataBase: boolean;
|
|
259
|
+
relativeSocialImage: boolean;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function metadataSourceFacts(source: string): MetadataSourceFacts {
|
|
263
|
+
return {
|
|
264
|
+
themeColor: source.includes('themeColor') && /\bthemeColor\s*:/.test(source),
|
|
265
|
+
appleTouchFullscreen:
|
|
266
|
+
source.includes('apple-touch-fullscreen') && /['"]apple-touch-fullscreen['"]/.test(source),
|
|
267
|
+
appleTouchIconPrecomposed:
|
|
268
|
+
source.includes('apple-touch-icon-precomposed') &&
|
|
269
|
+
/['"]apple-touch-icon-precomposed['"]/.test(source),
|
|
270
|
+
metadataBase: source.includes('metadataBase') && /\bmetadataBase\s*:/.test(source),
|
|
271
|
+
relativeSocialImage: hasRelativeSocialImage(source),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function routeUsesRelativeSocialImage(
|
|
276
|
+
route: RouteManifestEntry,
|
|
277
|
+
facts: MetadataSourceFacts[],
|
|
278
|
+
staticMetadataFiles: StaticMetadataFile[],
|
|
279
|
+
) {
|
|
280
|
+
if (facts.some(fact => fact.relativeSocialImage)) return true;
|
|
281
|
+
if (route.params.length > 0) return false;
|
|
282
|
+
const staticMetadata = staticMetadataForPathFromFiles(staticMetadataFiles, route.route);
|
|
283
|
+
return Boolean(staticMetadata.openGraphImage || staticMetadata.twitterImage);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function hasRelativeSocialImage(source: string) {
|
|
287
|
+
if (!source.includes('images')) return false;
|
|
288
|
+
return /(?:openGraph|twitter)\s*:\s*\{[\s\S]*?\bimages\s*:\s*['"`](?:\/|\.\.?\/)/m.test(source);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function warnOnce(warned: Set<string>, key: string, message: string) {
|
|
292
|
+
if (warned.has(key)) return;
|
|
293
|
+
warned.add(key);
|
|
294
|
+
console.warn(message);
|
|
295
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// Middleware manifest emission (COMPAT - may import core freely).
|
|
2
|
+
//
|
|
3
|
+
// Writes the two Next-shaped manifests a few suites read directly from the build output:
|
|
4
|
+
// - `<out>/server/middleware-manifest.json`: the edge-middleware entry with its compiled matchers,
|
|
5
|
+
// entrypoint and files list.
|
|
6
|
+
// - `<out>/server/functions-config-manifest.json`: the node-runtime variant, when the middleware opts
|
|
7
|
+
// into `runtime: 'nodejs'`.
|
|
8
|
+
//
|
|
9
|
+
// Only the fields the tests read are populated. A pure-core app writes neither.
|
|
10
|
+
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
13
|
+
import { findProxyFile, type ProxyConfig, type ProxyModule } from '../../proxy';
|
|
14
|
+
import { pathToFileHref, type ResolvedConfig } from '../../config';
|
|
15
|
+
import { toPosixPath } from '../../utils/fs';
|
|
16
|
+
import type { RouteManifestEntry } from '../../types';
|
|
17
|
+
import { readMiddlewareWasmBindingsForOutPath } from '../bundler/wasm';
|
|
18
|
+
import { preferredRegionForRoute } from '../next/preferred-region';
|
|
19
|
+
|
|
20
|
+
interface ManifestMatcher {
|
|
21
|
+
regexp: string;
|
|
22
|
+
originalSource: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface MiddlewareEntry {
|
|
26
|
+
name: string;
|
|
27
|
+
page: string;
|
|
28
|
+
matchers: ManifestMatcher[];
|
|
29
|
+
wasm: { name: string; filePath: string }[];
|
|
30
|
+
assets: never[];
|
|
31
|
+
files: string[];
|
|
32
|
+
entrypoint: string;
|
|
33
|
+
env: Record<string, string>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface MiddlewareManifest {
|
|
37
|
+
version: number;
|
|
38
|
+
sortedMiddleware: string[];
|
|
39
|
+
middleware: Record<string, MiddlewareEntry>;
|
|
40
|
+
functions: Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface FunctionsConfigManifest {
|
|
44
|
+
version: number;
|
|
45
|
+
functions: Record<string, { runtime: string; matchers: ManifestMatcher[] }>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Emit the middleware manifests for the resolved proxy file, if any. `entryFile`
|
|
50
|
+
* is the compiled proxy module path (relative to `outPath`, posix) produced by
|
|
51
|
+
* the build; it is recorded as the manifest `entrypoint` under `server/`.
|
|
52
|
+
*/
|
|
53
|
+
export async function writeMiddlewareManifests(
|
|
54
|
+
config: ResolvedConfig,
|
|
55
|
+
entryFile: string | undefined,
|
|
56
|
+
routes: RouteManifestEntry[] = [],
|
|
57
|
+
): Promise<void> {
|
|
58
|
+
const source = findProxyFile(config);
|
|
59
|
+
// Edge-runtime app segments are manifest `functions` entries alongside the
|
|
60
|
+
// middleware, keyed by their app path (route groups included) — an app with
|
|
61
|
+
// an edge page and no middleware still writes the manifest for them.
|
|
62
|
+
const functions = edgeFunctionEntries(config, routes);
|
|
63
|
+
if (!source || !entryFile) {
|
|
64
|
+
if (Object.keys(functions).length === 0) return;
|
|
65
|
+
const serverDir = path.join(config.outPath, 'server');
|
|
66
|
+
await mkdir(serverDir, { recursive: true });
|
|
67
|
+
await writeFile(
|
|
68
|
+
path.join(serverDir, 'middleware-manifest.json'),
|
|
69
|
+
`${JSON.stringify({ ...emptyMiddlewareManifest(), functions }, null, 2)}\n`,
|
|
70
|
+
);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const moduleFile = path.resolve(config.outPath, entryFile);
|
|
75
|
+
const module = (await import(pathToFileHref(moduleFile))) as ProxyModule;
|
|
76
|
+
const matchers = manifestMatchers(module.config);
|
|
77
|
+
const runtime = proxyRuntime(module.config);
|
|
78
|
+
const entrypoint = toServerEntrypoint(entryFile);
|
|
79
|
+
|
|
80
|
+
const serverDir = path.join(config.outPath, 'server');
|
|
81
|
+
await mkdir(serverDir, { recursive: true });
|
|
82
|
+
|
|
83
|
+
if (runtime === 'nodejs') {
|
|
84
|
+
const functionsConfig: FunctionsConfigManifest = {
|
|
85
|
+
version: 1,
|
|
86
|
+
functions: { '/_middleware': { runtime: 'nodejs', matchers } },
|
|
87
|
+
};
|
|
88
|
+
await writeFile(
|
|
89
|
+
path.join(serverDir, 'functions-config-manifest.json'),
|
|
90
|
+
`${JSON.stringify(functionsConfig, null, 2)}\n`,
|
|
91
|
+
);
|
|
92
|
+
// A node-runtime middleware still lists an empty edge middleware manifest so
|
|
93
|
+
// consumers that always read it find a valid (empty) shape.
|
|
94
|
+
await writeFile(
|
|
95
|
+
path.join(serverDir, 'middleware-manifest.json'),
|
|
96
|
+
`${JSON.stringify({ ...emptyMiddlewareManifest(), functions }, null, 2)}\n`,
|
|
97
|
+
);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const entry: MiddlewareEntry = {
|
|
102
|
+
name: 'middleware',
|
|
103
|
+
page: '/',
|
|
104
|
+
matchers,
|
|
105
|
+
wasm: readMiddlewareWasmBindingsForOutPath(config.outPath),
|
|
106
|
+
assets: [],
|
|
107
|
+
files: [entrypoint],
|
|
108
|
+
entrypoint,
|
|
109
|
+
env: {},
|
|
110
|
+
};
|
|
111
|
+
const manifest: MiddlewareManifest = {
|
|
112
|
+
version: 3,
|
|
113
|
+
sortedMiddleware: ['/'],
|
|
114
|
+
middleware: { '/': entry },
|
|
115
|
+
functions,
|
|
116
|
+
};
|
|
117
|
+
await writeFile(
|
|
118
|
+
path.join(serverDir, 'middleware-manifest.json'),
|
|
119
|
+
`${JSON.stringify(manifest, null, 2)}\n`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function emptyMiddlewareManifest(): MiddlewareManifest {
|
|
124
|
+
return { version: 3, sortedMiddleware: [], middleware: {}, functions: {} };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Manifest `functions` entries for app segments that declared
|
|
129
|
+
* `export const runtime = 'edge'`. Next keys them by the segment's app path
|
|
130
|
+
* (route groups kept, e.g. `/(group)/group/page`) and records the URL the
|
|
131
|
+
* segment answers as its matcher.
|
|
132
|
+
*/
|
|
133
|
+
function edgeFunctionEntries(
|
|
134
|
+
config: ResolvedConfig,
|
|
135
|
+
routes: RouteManifestEntry[],
|
|
136
|
+
): Record<string, unknown> {
|
|
137
|
+
const entries: Record<string, unknown> = {};
|
|
138
|
+
if (!config.appPath) return entries;
|
|
139
|
+
for (const route of routes) {
|
|
140
|
+
const runtime = route.segmentConfig?.runtime;
|
|
141
|
+
if (runtime !== 'edge' && runtime !== 'experimental-edge') continue;
|
|
142
|
+
const page = appManifestKey(config.appPath, route.file);
|
|
143
|
+
if (!page) continue;
|
|
144
|
+
const regions = preferredRegionForRoute(route);
|
|
145
|
+
entries[page] = {
|
|
146
|
+
files: [],
|
|
147
|
+
name: `app${page}`,
|
|
148
|
+
page,
|
|
149
|
+
matchers: [{ regexp: `^${routeMatcherRegexp(route.route)}$`, originalSource: route.route }],
|
|
150
|
+
wasm: [],
|
|
151
|
+
assets: [],
|
|
152
|
+
env: {},
|
|
153
|
+
...(regions ? { regions } : {}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return entries;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// `<app>/(group)/group/page.tsx` → `/(group)/group/page`.
|
|
160
|
+
function appManifestKey(appPath: string, file: string): string | undefined {
|
|
161
|
+
const relative = toPosixPath(path.relative(appPath, file));
|
|
162
|
+
if (!relative || relative.startsWith('..')) return undefined;
|
|
163
|
+
return `/${relative.replace(/\.[^./]+$/, '')}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// The URL regexp Next records for a segment: literal path with each dynamic
|
|
167
|
+
// segment widened to a single path part.
|
|
168
|
+
function routeMatcherRegexp(route: string): string {
|
|
169
|
+
return route.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/:([^/\\]+)/g, '[^/]+');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Compile config.matcher entries into { regexp, originalSource } pairs. With no
|
|
173
|
+
// matcher, the middleware runs on every path: Next records the `/:path*` source
|
|
174
|
+
// and its `^/.*$` regexp.
|
|
175
|
+
function manifestMatchers(config: ProxyConfig | undefined): ManifestMatcher[] {
|
|
176
|
+
const matcher = config?.matcher;
|
|
177
|
+
if (!matcher) return [{ regexp: '^/.*$', originalSource: '/:path*' }];
|
|
178
|
+
const list = Array.isArray(matcher) ? matcher : [matcher];
|
|
179
|
+
return list.map(item => {
|
|
180
|
+
const originalSource = typeof item === 'string' ? item : item.source;
|
|
181
|
+
return { regexp: `^${matcherRegexp(originalSource)}$`, originalSource };
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Mirror src/proxy.ts matcherPattern for the manifest regexp string (kept in
|
|
186
|
+
// sync with the runtime matcher). Raw regex sources pass through unchanged.
|
|
187
|
+
function matcherRegexp(matcher: string): string {
|
|
188
|
+
if (matcher.includes('(')) return matcher;
|
|
189
|
+
if (matcher === '/') return '/';
|
|
190
|
+
let source = '';
|
|
191
|
+
for (const segment of matcher.split('/').filter(Boolean)) {
|
|
192
|
+
if (/^:[^/]+\*$/.test(segment)) source += '(?:/.*)?';
|
|
193
|
+
else if (/^:[^/]+$/.test(segment)) source += '/[^/]+';
|
|
194
|
+
else source += `/${segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`;
|
|
195
|
+
}
|
|
196
|
+
return source || '/';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function proxyRuntime(config: ProxyConfig | undefined): 'edge' | 'nodejs' {
|
|
200
|
+
const runtime = (config as { runtime?: unknown } | undefined)?.runtime;
|
|
201
|
+
return runtime === 'nodejs' ? 'nodejs' : 'edge';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Record the compiled entry under the `server/` prefix Next uses, forcing a
|
|
205
|
+
// `.js` extension (the manifest tests assert `^server/.+\.(js|mjs|cjs)$`).
|
|
206
|
+
function toServerEntrypoint(entryFile: string): string {
|
|
207
|
+
const posix = toPosixPath(entryFile).replace(/^\.?\//, '');
|
|
208
|
+
const withinServer = posix.startsWith('server/') ? posix : `server/${path.posix.basename(posix)}`;
|
|
209
|
+
return withinServer.replace(/\.[^./]+$/, '.js');
|
|
210
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// Server-action RETURN-value handling (COMPAT).
|
|
2
|
+
//
|
|
3
|
+
// Two Next behaviors the e2e suite asserts about what an action sends back:
|
|
4
|
+
//
|
|
5
|
+
// 1. temporary-references: a client passes a plain object to an action and the action returns that SAME
|
|
6
|
+
// object; the client must receive the IDENTICAL object it sent (referential ===), not a structural
|
|
7
|
+
// copy. Next threads a "temporary reference" through the flight wire. This mirrors it with a
|
|
8
|
+
// lightweight tag protocol: the client stamps each top-level object arg with a temp-ref id and
|
|
9
|
+
// remembers id -> object; the server keeps that id associated with the decoded arg object (a
|
|
10
|
+
// WeakMap), and when it serializes the return value it emits a bare marker for any object the client
|
|
11
|
+
// originally sent. The client swaps the marker back for its original object.
|
|
12
|
+
//
|
|
13
|
+
// 2. actions-streaming: an action may return a ReadableStream; the response must stream it to the
|
|
14
|
+
// client unbuffered. JSON cannot carry a stream, so the server responds with the raw stream body
|
|
15
|
+
// under a marker header/content type and the client returns a live ReadableStream from the call.
|
|
16
|
+
//
|
|
17
|
+
// These helpers are pure and node-safe. The client counterparts live inline in actions/action-client.ts.
|
|
18
|
+
|
|
19
|
+
/** Marker key naming a temporary-reference id in the args / return wire. */
|
|
20
|
+
export const TEMP_REF_MARKER = '$$pnext_temp_ref';
|
|
21
|
+
export const ACTION_ELEMENT_MARKER = '$$pnext_element';
|
|
22
|
+
/**
|
|
23
|
+
* Marker carrying the SERVER-RENDERED HTML of a React element tree an action returned (host elements /
|
|
24
|
+
* async server components - NOT a client-reference element, which uses ACTION_ELEMENT_MARKER). The
|
|
25
|
+
* client revives it into a renderable node for useActionState.
|
|
26
|
+
*/
|
|
27
|
+
export const ACTION_ELEMENT_HTML_MARKER = '$$pnext_element_html';
|
|
28
|
+
|
|
29
|
+
const clientReferenceSymbol = Symbol.for('pnext.clientReference');
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Response content-type/marker for a streamed action return. The client detects
|
|
33
|
+
* this and returns the response body as a ReadableStream from the call instead
|
|
34
|
+
* of parsing JSON.
|
|
35
|
+
*/
|
|
36
|
+
export const ACTION_STREAM_CONTENT_TYPE = 'x-component/pnext-stream';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Per-request association of a decoded arg object -> the temp-ref id the client
|
|
40
|
+
* stamped on it. A WeakMap keyed by the live object preserves identity across
|
|
41
|
+
* the action call (the action returns the same reference it received).
|
|
42
|
+
*/
|
|
43
|
+
export interface TempRefScope {
|
|
44
|
+
/** Record that a decoded arg object carries the client's temp-ref id. */
|
|
45
|
+
track(object: object, id: string): void;
|
|
46
|
+
/** The temp-ref id the client sent for this object, if any. */
|
|
47
|
+
idFor(value: unknown): string | undefined;
|
|
48
|
+
/** True once any temp-ref arg has been seen (skip serialization walk otherwise). */
|
|
49
|
+
readonly active: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createTempRefScope(): TempRefScope {
|
|
53
|
+
const ids = new WeakMap<object, string>();
|
|
54
|
+
let active = false;
|
|
55
|
+
return {
|
|
56
|
+
track(object, id) {
|
|
57
|
+
ids.set(object, id);
|
|
58
|
+
active = true;
|
|
59
|
+
},
|
|
60
|
+
idFor(value) {
|
|
61
|
+
if (value === null || typeof value !== 'object') return undefined;
|
|
62
|
+
return ids.get(value);
|
|
63
|
+
},
|
|
64
|
+
get active() {
|
|
65
|
+
return active;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Strip and record temp-ref markers from a decoded argument tree in place.
|
|
72
|
+
* A `{ [TEMP_REF_MARKER]: id, ...rest }` object is unwrapped to `rest` and the
|
|
73
|
+
* resulting object is tracked under `id`, so the action sees a clean object and
|
|
74
|
+
* a later return of that same object serializes back to the marker.
|
|
75
|
+
*/
|
|
76
|
+
export function extractTempRefs(value: unknown, scope: TempRefScope, seen = new Set<unknown>()): unknown {
|
|
77
|
+
if (value === null || typeof value !== 'object') return value;
|
|
78
|
+
if (seen.has(value)) return value;
|
|
79
|
+
seen.add(value);
|
|
80
|
+
if (Array.isArray(value)) {
|
|
81
|
+
for (let i = 0; i < value.length; i++) value[i] = extractTempRefs(value[i], scope, seen);
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
const record = value as Record<string, unknown>;
|
|
85
|
+
const id = record[TEMP_REF_MARKER];
|
|
86
|
+
if (typeof id === 'string') {
|
|
87
|
+
delete record[TEMP_REF_MARKER];
|
|
88
|
+
scope.track(record, id);
|
|
89
|
+
}
|
|
90
|
+
for (const key of Object.keys(record)) {
|
|
91
|
+
record[key] = extractTempRefs(record[key], scope, seen);
|
|
92
|
+
}
|
|
93
|
+
return record;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Serialize temporary references and elements in an action return.
|
|
98
|
+
* - A client-reference element carries only its client-reference id, props and key.
|
|
99
|
+
* - A server element tree is server-rendered to island wire HTML via the injected `renderElement`;
|
|
100
|
+
* JSON cannot carry vnode $$typeof Symbols, so without this the tree dumps as raw vnode internals and
|
|
101
|
+
* useActionState never gets a node. Async because rendering resolves async server components; when no
|
|
102
|
+
* renderer is injected, element trees fall through unchanged.
|
|
103
|
+
*/
|
|
104
|
+
export async function serializeReturnWithTempRefs(
|
|
105
|
+
value: unknown,
|
|
106
|
+
scope: TempRefScope,
|
|
107
|
+
renderElement?: (element: unknown) => Promise<string>,
|
|
108
|
+
): Promise<unknown> {
|
|
109
|
+
const walk = async (input: unknown, seen: Set<unknown>): Promise<unknown> => {
|
|
110
|
+
if (input === null || typeof input !== 'object') return input;
|
|
111
|
+
const marker = scope.active ? scope.idFor(input) : undefined;
|
|
112
|
+
if (marker !== undefined) return { [TEMP_REF_MARKER]: marker };
|
|
113
|
+
if (seen.has(input)) return input;
|
|
114
|
+
seen.add(input);
|
|
115
|
+
const element = clientReferenceElement(input);
|
|
116
|
+
if (element) {
|
|
117
|
+
return {
|
|
118
|
+
[ACTION_ELEMENT_MARKER]: {
|
|
119
|
+
id: element.id,
|
|
120
|
+
props: await walk(element.props, seen),
|
|
121
|
+
...(element.key !== undefined ? { key: element.key } : {}),
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (renderElement && isServerElement(input)) {
|
|
126
|
+
return { [ACTION_ELEMENT_HTML_MARKER]: await renderElement(input) };
|
|
127
|
+
}
|
|
128
|
+
if (Array.isArray(input)) return Promise.all(input.map(item => walk(item, seen)));
|
|
129
|
+
const proto = Object.getPrototypeOf(input) as object | null;
|
|
130
|
+
if (proto !== Object.prototype && proto !== null) return input;
|
|
131
|
+
const out: Record<string, unknown> = {};
|
|
132
|
+
for (const [key, item] of Object.entries(input as Record<string, unknown>)) {
|
|
133
|
+
out[key] = await walk(item, seen);
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
};
|
|
137
|
+
return walk(value, new Set());
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* True for a React element / preact vnode that is NOT a client-reference element
|
|
142
|
+
* (those are handled by clientReferenceElement first). Detects the preact vnode
|
|
143
|
+
* brand (own `constructor === undefined`) or the react.element `$$typeof`, so a
|
|
144
|
+
* plain data object that merely has `type`/`props` keys is never mistaken for an
|
|
145
|
+
* element (it stays JSON-serialized).
|
|
146
|
+
*/
|
|
147
|
+
function isServerElement(value: object): boolean {
|
|
148
|
+
const vnode = value as { type?: unknown; props?: unknown; $$typeof?: unknown };
|
|
149
|
+
if (!('type' in vnode) || !('props' in vnode)) return false;
|
|
150
|
+
if (vnode.$$typeof === Symbol.for('react.element')) return true;
|
|
151
|
+
return (
|
|
152
|
+
Object.prototype.hasOwnProperty.call(value, 'constructor') &&
|
|
153
|
+
(value as { constructor?: unknown }).constructor === undefined
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function clientReferenceElement(
|
|
158
|
+
value: object,
|
|
159
|
+
): { id: string; props: unknown; key?: string | number } | undefined {
|
|
160
|
+
const vnode = value as { type?: unknown; props?: unknown; key?: unknown };
|
|
161
|
+
if (typeof vnode.type !== 'function') return undefined;
|
|
162
|
+
const reference = (vnode.type as unknown as Record<symbol, unknown>)[clientReferenceSymbol] as
|
|
163
|
+
| { id?: unknown }
|
|
164
|
+
| undefined;
|
|
165
|
+
if (typeof reference?.id !== 'string') return undefined;
|
|
166
|
+
const key = typeof vnode.key === 'string' || typeof vnode.key === 'number' ? vnode.key : undefined;
|
|
167
|
+
return { id: reference.id, props: vnode.props ?? {}, ...(key !== undefined ? { key } : {}) };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** True for a value the streaming action path must send as a raw stream body. */
|
|
171
|
+
export function isReadableStreamReturn(value: unknown): value is ReadableStream {
|
|
172
|
+
return typeof ReadableStream !== 'undefined' && value instanceof ReadableStream;
|
|
173
|
+
}
|