@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,679 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { basename, join, relative, sep } from 'node:path';
|
|
3
|
+
import { h, type ComponentChildren, type ComponentType } from 'preact';
|
|
4
|
+
import type { RouteParamsSnapshot } from './island-context';
|
|
5
|
+
import { Suspense } from '../api/suspense';
|
|
6
|
+
import type { ClientReference } from '../client/reference';
|
|
7
|
+
import {
|
|
8
|
+
consumedSegments,
|
|
9
|
+
isGroupSegment,
|
|
10
|
+
isSlotSegment,
|
|
11
|
+
matchSegments,
|
|
12
|
+
matchSlotInterceptor,
|
|
13
|
+
pathnameSegments,
|
|
14
|
+
slotConventionFile,
|
|
15
|
+
slotDirectoriesIn,
|
|
16
|
+
type SegmentMatch,
|
|
17
|
+
} from '../routing/slots';
|
|
18
|
+
import { toPosixPath } from '../utils/fs';
|
|
19
|
+
import type {
|
|
20
|
+
PageProps,
|
|
21
|
+
RouteManifestEntry,
|
|
22
|
+
RouteParamValue,
|
|
23
|
+
ServerComponent,
|
|
24
|
+
} from '../types';
|
|
25
|
+
|
|
26
|
+
interface SlotRenderOptions {
|
|
27
|
+
config: { appPath: string };
|
|
28
|
+
url: URL;
|
|
29
|
+
nav?: {
|
|
30
|
+
soft?: boolean;
|
|
31
|
+
childrenPath?: string;
|
|
32
|
+
targetPath?: string;
|
|
33
|
+
state?: { childrenSearch?: string; slots?: Record<string, string> };
|
|
34
|
+
};
|
|
35
|
+
route: Pick<RouteManifestEntry, 'clientReferences' | 'slotDirs'>;
|
|
36
|
+
params?: Record<string, RouteParamValue>;
|
|
37
|
+
clientComponents?: Map<string, ComponentType<Record<string, unknown>>>;
|
|
38
|
+
slotStateOut?: Record<string, string>;
|
|
39
|
+
moduleLoader?: (file: string) => Promise<Record<string, unknown>> | Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface ServerVNodeProps extends Record<string, unknown> {
|
|
43
|
+
children?: ComponentChildren;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface PageModule {
|
|
47
|
+
default?: ServerComponent<PageProps>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface LayoutModule {
|
|
51
|
+
default?: ServerComponent<{ children: ComponentChildren }>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface LoadingModule {
|
|
55
|
+
default?: ServerComponent<Record<string, never>>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface SlotRenderDeps<Options extends SlotRenderOptions> {
|
|
59
|
+
importModule: (file: string, options: Options) => Promise<Record<string, unknown>>;
|
|
60
|
+
markServerReference: <T>(component: T) => T;
|
|
61
|
+
clientReferenceStub: (reference: ClientReference) => ComponentType<Record<string, unknown>>;
|
|
62
|
+
createPageProps: (options: Options) => PageProps;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface SlotContext<Options extends SlotRenderOptions = SlotRenderOptions> {
|
|
66
|
+
options: Options;
|
|
67
|
+
deps: SlotRenderDeps<Options>;
|
|
68
|
+
appPath: string;
|
|
69
|
+
urlPath: string;
|
|
70
|
+
urlSegments: string[];
|
|
71
|
+
/** The request's search string (`?a=b` or ''), `_rsc` union query stripped. */
|
|
72
|
+
urlSearch: string;
|
|
73
|
+
childrenPath: string;
|
|
74
|
+
childrenSegments: string[];
|
|
75
|
+
/** Search string the children tree renders with (host renders inherit it). */
|
|
76
|
+
childrenSearch: string;
|
|
77
|
+
soft: boolean;
|
|
78
|
+
statePaths: Record<string, string>;
|
|
79
|
+
stateOut: Record<string, string>;
|
|
80
|
+
matches: ResolvedSlotMatch[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface ResolvedSlotMatch {
|
|
84
|
+
slotDir: string;
|
|
85
|
+
pageFile?: string;
|
|
86
|
+
layoutFiles: string[];
|
|
87
|
+
params: Record<string, RouteParamValue>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function createSlotContext<Options extends SlotRenderOptions>(
|
|
91
|
+
options: Options,
|
|
92
|
+
deps: SlotRenderDeps<Options>,
|
|
93
|
+
): SlotContext<Options> {
|
|
94
|
+
const urlPath = trimPathname(options.nav?.targetPath ?? options.url.pathname);
|
|
95
|
+
const childrenPath = trimPathname(options.nav?.childrenPath ?? options.url.pathname);
|
|
96
|
+
const urlSearch = stripRscSearch(options.url.search);
|
|
97
|
+
// A host render (children anchored away from the URL) keeps the children
|
|
98
|
+
// tree's own recorded query; a direct render's children query IS the URL's.
|
|
99
|
+
const childrenSearch =
|
|
100
|
+
options.nav?.soft && childrenPath !== trimPathname(options.url.pathname)
|
|
101
|
+
? (options.nav?.state?.childrenSearch ?? '')
|
|
102
|
+
: urlSearch;
|
|
103
|
+
return {
|
|
104
|
+
options,
|
|
105
|
+
deps,
|
|
106
|
+
appPath: options.config.appPath,
|
|
107
|
+
urlPath,
|
|
108
|
+
urlSegments: pathnameSegments(urlPath),
|
|
109
|
+
urlSearch,
|
|
110
|
+
childrenPath,
|
|
111
|
+
childrenSegments: pathnameSegments(childrenPath),
|
|
112
|
+
childrenSearch,
|
|
113
|
+
soft: Boolean(options.nav?.soft),
|
|
114
|
+
statePaths: options.nav?.state?.slots ?? {},
|
|
115
|
+
stateOut: options.slotStateOut ?? {},
|
|
116
|
+
matches: [],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function trimPathname(pathname: string) {
|
|
121
|
+
return pathname.length > 1 ? pathname.replace(/\/+$/, '') || '/' : pathname;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Split a recorded slot source (`pathname` or `pathname?query`) into parts. */
|
|
125
|
+
function sourceParts(source: string): { path: string; search: string } {
|
|
126
|
+
const queryIndex = source.indexOf('?');
|
|
127
|
+
if (queryIndex === -1) return { path: trimPathname(source), search: '' };
|
|
128
|
+
return {
|
|
129
|
+
path: trimPathname(source.slice(0, queryIndex)),
|
|
130
|
+
search: source.slice(queryIndex),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The router's `_rsc` union query is a CDN cache key, never an app param. */
|
|
135
|
+
function stripRscSearch(search: string): string {
|
|
136
|
+
if (!search.includes('_rsc')) return search;
|
|
137
|
+
const params = new URLSearchParams(search);
|
|
138
|
+
params.delete('_rsc');
|
|
139
|
+
const stripped = params.toString();
|
|
140
|
+
return stripped ? `?${stripped}` : '';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Render every `@slot` in `dir` into layout props. */
|
|
144
|
+
export async function renderDirSlots<Options extends SlotRenderOptions>(
|
|
145
|
+
context: SlotContext<Options>,
|
|
146
|
+
dir: string,
|
|
147
|
+
): Promise<Record<string, ComponentChildren>> {
|
|
148
|
+
const slotDirs = slotDirectoriesIn(dir);
|
|
149
|
+
if (slotDirs.length === 0) return {};
|
|
150
|
+
const consumed = consumedSegments(context.appPath, dir);
|
|
151
|
+
// A `[...catchAll]` layout greedily consumes every remaining URL segment (a catch-all is always the last
|
|
152
|
+
// route segment), so any `@slot` it hosts matches at the index - the empty remaining path. Treat Infinity
|
|
153
|
+
// as "all segments consumed" rather than skipping slot rendering, which would drop the slot entirely on a
|
|
154
|
+
// catch-all route.
|
|
155
|
+
const effectiveConsumed = Number.isFinite(consumed)
|
|
156
|
+
? consumed
|
|
157
|
+
: context.childrenSegments.length;
|
|
158
|
+
const props: Record<string, ComponentChildren> = {};
|
|
159
|
+
for (const slotDir of slotDirs) {
|
|
160
|
+
const content = await resolveSlotContent(context, slotDir, effectiveConsumed);
|
|
161
|
+
// An unmatched slot with no `default.*` stays a bare null: wrapping it in
|
|
162
|
+
// the boundary marker would make it a truthy VNode, and an unmatched
|
|
163
|
+
// `@children` slot must stay falsy so childrenFromSlots can fall back to
|
|
164
|
+
// the segment-matched children tree.
|
|
165
|
+
props[basename(slotDir).slice(1)] = content == null ? null : wrapSlotBoundary(content);
|
|
166
|
+
}
|
|
167
|
+
return props;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function resolveSlotContent<Options extends SlotRenderOptions>(
|
|
171
|
+
context: SlotContext<Options>,
|
|
172
|
+
slotDir: string,
|
|
173
|
+
consumed: number,
|
|
174
|
+
): Promise<ComponentChildren> {
|
|
175
|
+
const key = toPosixPath(relative(context.appPath, slotDir));
|
|
176
|
+
|
|
177
|
+
// Interceptor targets are absolute paths, so the navigated URL applies to
|
|
178
|
+
// them regardless of the chain's consumed prefix (that mismatch is exactly
|
|
179
|
+
// what interception markers express). This only happens on a host render
|
|
180
|
+
// (children anchored at the origin path while the URL shows the target);
|
|
181
|
+
// when children == URL the server matched the real route and the slot must
|
|
182
|
+
// not intercept it. Interceptors go before direct matching: an interception
|
|
183
|
+
// route beats a catch-all inside the same slot.
|
|
184
|
+
if (context.soft && context.urlPath !== context.childrenPath) {
|
|
185
|
+
const intercepted = matchSlotInterceptor(context.appPath, slotDir, context.urlPath);
|
|
186
|
+
if (intercepted) {
|
|
187
|
+
context.stateOut[key] = context.urlPath + context.urlSearch;
|
|
188
|
+
return renderSlotMatch(context, slotDir, intercepted, context.urlSearch);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Each candidate source carries the search string its render uses: a slot
|
|
193
|
+
// re-rendered from a RECORDED source keeps the query it was fetched with
|
|
194
|
+
// (Next's per-segment refetch URL), not the current request's.
|
|
195
|
+
const sources: { path: string; search: string }[] = [];
|
|
196
|
+
const pushSource = (path: string, search: string) => {
|
|
197
|
+
if (!sources.some(source => source.path === path)) sources.push({ path, search });
|
|
198
|
+
};
|
|
199
|
+
const rawStateSource = context.statePaths[key];
|
|
200
|
+
const stateSource = rawStateSource ? sourceParts(rawStateSource) : undefined;
|
|
201
|
+
const preserveForInterception =
|
|
202
|
+
context.soft &&
|
|
203
|
+
context.urlPath !== context.childrenPath &&
|
|
204
|
+
context.options.route.slotDirs?.some(dir =>
|
|
205
|
+
Boolean(matchSlotInterceptor(context.appPath, dir, context.urlPath)),
|
|
206
|
+
);
|
|
207
|
+
if (
|
|
208
|
+
preserveForInterception &&
|
|
209
|
+
stateSource &&
|
|
210
|
+
segmentsSharePrefix(pathnameSegments(stateSource.path), context.childrenSegments, consumed)
|
|
211
|
+
) {
|
|
212
|
+
pushSource(stateSource.path, stateSource.search);
|
|
213
|
+
}
|
|
214
|
+
if (segmentsSharePrefix(context.urlSegments, context.childrenSegments, consumed)) {
|
|
215
|
+
pushSource(context.urlPath, context.urlSearch);
|
|
216
|
+
}
|
|
217
|
+
if (
|
|
218
|
+
context.soft &&
|
|
219
|
+
stateSource &&
|
|
220
|
+
segmentsSharePrefix(pathnameSegments(stateSource.path), context.childrenSegments, consumed)
|
|
221
|
+
) {
|
|
222
|
+
pushSource(stateSource.path, stateSource.search);
|
|
223
|
+
}
|
|
224
|
+
// Host renders (children anchored away from the URL) fall back to the
|
|
225
|
+
// children path itself, so sibling slots keep matching the visible page even
|
|
226
|
+
// without an explicit state entry.
|
|
227
|
+
if (context.childrenPath !== context.urlPath) {
|
|
228
|
+
pushSource(context.childrenPath, context.childrenSearch);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
for (const source of sources) {
|
|
232
|
+
const intercepted =
|
|
233
|
+
context.soft && source.path !== context.urlPath
|
|
234
|
+
? matchSlotInterceptor(context.appPath, slotDir, source.path)
|
|
235
|
+
: null;
|
|
236
|
+
const match =
|
|
237
|
+
intercepted ?? matchSegments(slotDir, pathnameSegments(source.path).slice(consumed));
|
|
238
|
+
if (!match) continue;
|
|
239
|
+
context.stateOut[key] = source.path + source.search;
|
|
240
|
+
return renderSlotMatch(context, slotDir, match, source.search);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const defaultFile = slotConventionFile(slotDir, 'default');
|
|
244
|
+
if (!defaultFile) return null;
|
|
245
|
+
return renderSlotComponent(context, defaultFile, {});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Render a matched slot page inside its slot-internal layout chain. */
|
|
249
|
+
async function renderSlotMatch<Options extends SlotRenderOptions>(
|
|
250
|
+
context: SlotContext<Options>,
|
|
251
|
+
slotDir: string,
|
|
252
|
+
match: SegmentMatch,
|
|
253
|
+
sourceSearch?: string,
|
|
254
|
+
): Promise<ComponentChildren> {
|
|
255
|
+
// A pageless match renders null children under the matched layout chain.
|
|
256
|
+
let tree: ComponentChildren = match.file
|
|
257
|
+
? await renderSlotComponent(context, match.file, match.params, sourceSearch)
|
|
258
|
+
: null;
|
|
259
|
+
|
|
260
|
+
// Wrap innermost-out: loading.* at the page's own dir suspends the page;
|
|
261
|
+
// each dir's layout receives its own nested slots as props.
|
|
262
|
+
const chain: string[] = [slotDir];
|
|
263
|
+
for (const name of match.dirs) chain.push(join(chain[chain.length - 1]!, name));
|
|
264
|
+
context.matches.push({
|
|
265
|
+
slotDir,
|
|
266
|
+
...(match.file ? { pageFile: match.file } : {}),
|
|
267
|
+
layoutFiles: chain.flatMap(dir => slotConventionFile(dir, 'layout') ?? []),
|
|
268
|
+
params: match.params,
|
|
269
|
+
});
|
|
270
|
+
for (const dir of [...chain].reverse()) {
|
|
271
|
+
const loadingFile = slotConventionFile(dir, 'loading');
|
|
272
|
+
if (loadingFile) {
|
|
273
|
+
const module = (await context.deps.importModule(
|
|
274
|
+
loadingFile,
|
|
275
|
+
context.options,
|
|
276
|
+
)) as LoadingModule;
|
|
277
|
+
const Loading = context.deps.markServerReference(module.default);
|
|
278
|
+
if (Loading) {
|
|
279
|
+
tree = h(
|
|
280
|
+
Suspense,
|
|
281
|
+
{ fallback: h(Loading as ComponentType<Record<string, never>>, {}) },
|
|
282
|
+
tree,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const layoutFile = slotConventionFile(dir, 'layout');
|
|
287
|
+
if (!layoutFile) continue;
|
|
288
|
+
const module = (await context.deps.importModule(layoutFile, context.options)) as LayoutModule;
|
|
289
|
+
const Layout = context.deps.markServerReference(module.default);
|
|
290
|
+
if (!Layout) continue;
|
|
291
|
+
const nestedSlots = await renderDirSlots(context, dir);
|
|
292
|
+
tree = h(Layout as ComponentType<ServerVNodeProps>, {
|
|
293
|
+
...slotPageProps(context, match.params, sourceSearch),
|
|
294
|
+
...nestedSlots,
|
|
295
|
+
children: tree,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
// Any `'use client'` island rendered anywhere inside this slot subtree must
|
|
299
|
+
// see the slot's own params (its dynamic / catch-all captures) from useParams,
|
|
300
|
+
// not just the global route params. Wrap the subtree in a params scope so the
|
|
301
|
+
// renderer stamps each island it resolves within it (see resolveServerTree's
|
|
302
|
+
// paramsScopeSymbol handling + render/island-context.ts RouteParamsContext).
|
|
303
|
+
return wrapSlotParamsScope(
|
|
304
|
+
{ ...(context.options.params ?? {}), ...match.params },
|
|
305
|
+
tree,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
// Per-slot params scope (CORE seam for compat useParams inside a slot island).
|
|
311
|
+
//
|
|
312
|
+
// The scope is a marker VNode the server-tree resolver recognizes (via
|
|
313
|
+
// paramsScopeSymbol) and re-establishes as an async scope around resolving its
|
|
314
|
+
// children, so every island created deep inside the slot subtree captures the
|
|
315
|
+
// slot's resolved params. Mirrors the layout-segment scope above.
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
export const paramsScopeSymbol = Symbol.for('pnext.paramsScope');
|
|
319
|
+
|
|
320
|
+
const PARAMS_SCOPE_PROP = '__pnextParams';
|
|
321
|
+
|
|
322
|
+
const paramsSnapshotStorage = new AsyncLocalStorage<RouteParamsSnapshot>();
|
|
323
|
+
|
|
324
|
+
type ParamsScopeMarker = ((props: ServerVNodeProps) => ComponentChildren) & {
|
|
325
|
+
[paramsScopeSymbol]?: true;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const ParamsScopeMarker: ParamsScopeMarker = props => props.children;
|
|
329
|
+
ParamsScopeMarker[paramsScopeSymbol] = true;
|
|
330
|
+
|
|
331
|
+
// Parallel-route slot marker (task-boundary scoping). A named `@slot` subtree is wrapped in this marker so
|
|
332
|
+
// the server-tree resolver can arm the build prerender's task boundary ONLY while resolving slot content.
|
|
333
|
+
// That keeps the task-boundary abort - a slot that resolves in a task becomes a hole, matching Next - scoped
|
|
334
|
+
// to parallel-route slots, the shape it was designed for, and leaves every other <Suspense> baking its task
|
|
335
|
+
// work as before.
|
|
336
|
+
|
|
337
|
+
export const slotBoundarySymbol = Symbol.for('pnext.slotBoundary');
|
|
338
|
+
|
|
339
|
+
type SlotBoundaryMarker = ((props: ServerVNodeProps) => ComponentChildren) & {
|
|
340
|
+
[slotBoundarySymbol]?: true;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const SlotBoundaryMarker: SlotBoundaryMarker = props => props.children;
|
|
344
|
+
SlotBoundaryMarker[slotBoundarySymbol] = true;
|
|
345
|
+
|
|
346
|
+
/** Wrap a named parallel-route slot subtree in the task-boundary marker. */
|
|
347
|
+
export function wrapSlotBoundary(content: ComponentChildren): ComponentChildren {
|
|
348
|
+
return h(SlotBoundaryMarker as ComponentType<ServerVNodeProps>, null, content);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Read the params for the slot scope active in the current async resolution. */
|
|
352
|
+
export function currentParamsSnapshot(): RouteParamsSnapshot | undefined {
|
|
353
|
+
return paramsSnapshotStorage.getStore();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Run `fn` with the given slot params installed as the active params scope. */
|
|
357
|
+
export function runInParamsScope<T>(params: RouteParamsSnapshot, fn: () => T): T {
|
|
358
|
+
return paramsSnapshotStorage.run(params, fn);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Read the params a params-scope marker VNode carries. */
|
|
362
|
+
export function paramsScopeParams(props: Record<string, unknown>): RouteParamsSnapshot {
|
|
363
|
+
return (props[PARAMS_SCOPE_PROP] as RouteParamsSnapshot | undefined) ?? {};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function wrapSlotParamsScope(
|
|
367
|
+
params: Record<string, RouteParamValue>,
|
|
368
|
+
tree: ComponentChildren,
|
|
369
|
+
): ComponentChildren {
|
|
370
|
+
return h(
|
|
371
|
+
ParamsScopeMarker as ComponentType<ServerVNodeProps>,
|
|
372
|
+
{ [PARAMS_SCOPE_PROP]: params },
|
|
373
|
+
tree,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function renderSlotComponent<Options extends SlotRenderOptions>(
|
|
378
|
+
context: SlotContext<Options>,
|
|
379
|
+
file: string,
|
|
380
|
+
params: Record<string, RouteParamValue>,
|
|
381
|
+
sourceSearch?: string,
|
|
382
|
+
): Promise<ComponentChildren> {
|
|
383
|
+
const props = slotPageProps(context, params, sourceSearch);
|
|
384
|
+
|
|
385
|
+
// A 'use client' page inside a slot renders as an island: the reference
|
|
386
|
+
// stub becomes a hydrated boundary in resolveServerTree. Its promise props
|
|
387
|
+
// (params/searchParams) travel as plain markers the island runtime revives.
|
|
388
|
+
const reference = context.options.route.clientReferences.find(
|
|
389
|
+
item => item.file === file && item.exportName === 'default',
|
|
390
|
+
);
|
|
391
|
+
if (reference) {
|
|
392
|
+
const Component =
|
|
393
|
+
context.options.clientComponents?.get(reference.id) ??
|
|
394
|
+
context.deps.clientReferenceStub(reference);
|
|
395
|
+
return h(Component, {
|
|
396
|
+
params: promiseMarker(await props.params),
|
|
397
|
+
searchParams: promiseMarker(await props.searchParams),
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const module = (await context.deps.importModule(file, context.options)) as PageModule;
|
|
402
|
+
const Component = context.deps.markServerReference(module.default);
|
|
403
|
+
if (!Component) return null;
|
|
404
|
+
return h(Component as ComponentType<PageProps>, props);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const PROMISE_MARKER_KEY = '__pnextPromise';
|
|
408
|
+
|
|
409
|
+
export function promiseMarker(value: unknown) {
|
|
410
|
+
return { [PROMISE_MARKER_KEY]: value ?? null };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** The discriminator that makes a placeholder marker rewritable at serve time. */
|
|
414
|
+
const PROMISE_PLACEHOLDER_KEY = '__pnextParams';
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* The wire placeholder for a request-API promise prop that a partial prerender must NOT await (see ppr.ts
|
|
418
|
+
* `isHangingPromise`). The baked bytes carry no resolved param, and the discriminator names which request
|
|
419
|
+
* value re-stamps it per serving request - `data-pnext-props` is the third param carrier next to
|
|
420
|
+
* `window.__PNEXT_ROUTE__` and `data-pnext-params`, and all three must agree.
|
|
421
|
+
*/
|
|
422
|
+
export function promisePlaceholderMarker(kind: 'params' | 'searchParams') {
|
|
423
|
+
return { [PROMISE_MARKER_KEY]: null, [PROMISE_PLACEHOLDER_KEY]: kind };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** The serialized placeholder the serve-time re-stamp searches for. */
|
|
427
|
+
export function promisePlaceholderJson(kind: 'params' | 'searchParams'): string {
|
|
428
|
+
return `{"${PROMISE_MARKER_KEY}":null,"${PROMISE_PLACEHOLDER_KEY}":"${kind}"}`;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** Revive promise markers into thenables for the SSR pass of an island. */
|
|
432
|
+
export function revivePromiseProps(props: Record<string, unknown>) {
|
|
433
|
+
const out: Record<string, unknown> = {};
|
|
434
|
+
for (const [key, value] of Object.entries(props)) {
|
|
435
|
+
if (value && typeof value === 'object' && PROMISE_MARKER_KEY in value) {
|
|
436
|
+
out[key] = Promise.resolve((value as Record<string, unknown>)[PROMISE_MARKER_KEY]);
|
|
437
|
+
} else {
|
|
438
|
+
out[key] = value;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return out;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function slotPageProps<Options extends SlotRenderOptions>(
|
|
445
|
+
context: SlotContext<Options>,
|
|
446
|
+
params: Record<string, RouteParamValue>,
|
|
447
|
+
sourceSearch?: string,
|
|
448
|
+
): PageProps {
|
|
449
|
+
const base = context.deps.createPageProps(context.options);
|
|
450
|
+
// What createPageProps already resolves searchParams from: the children
|
|
451
|
+
// tree's recorded query on a host render, the request's otherwise (see
|
|
452
|
+
// childrenSourceSearchParams in renderer.ts).
|
|
453
|
+
const baseSearch =
|
|
454
|
+
context.soft && context.childrenPath !== trimPathname(context.options.url.pathname)
|
|
455
|
+
? context.childrenSearch
|
|
456
|
+
: context.urlSearch;
|
|
457
|
+
return {
|
|
458
|
+
...base,
|
|
459
|
+
params: Promise.resolve({ ...(context.options.params ?? {}), ...params }),
|
|
460
|
+
// A slot rendered from a source whose query differs (a recorded refetch
|
|
461
|
+
// URL, or the target URL's query on a host render) sees ITS source's
|
|
462
|
+
// searchParams — mirrors Next's per-segment refetch semantics.
|
|
463
|
+
...(sourceSearch !== undefined && sourceSearch !== baseSearch
|
|
464
|
+
? { searchParams: fulfilledPromise(searchParamsObject(sourceSearch)) }
|
|
465
|
+
: {}),
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Repeated keys collect into arrays (Next's searchParams page-prop shape).
|
|
470
|
+
function searchParamsObject(search: string): Record<string, string | string[]> {
|
|
471
|
+
const result: Record<string, string | string[]> = {};
|
|
472
|
+
new URLSearchParams(search).forEach((value, key) => {
|
|
473
|
+
if (key === '_rsc') return;
|
|
474
|
+
const current = result[key];
|
|
475
|
+
if (current === undefined) result[key] = value;
|
|
476
|
+
else if (Array.isArray(current)) current.push(value);
|
|
477
|
+
else result[key] = [current, value];
|
|
478
|
+
});
|
|
479
|
+
return result;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Pre-settled promise (status/value readable synchronously) so preact's use()
|
|
483
|
+
// does not suspend during streaming SSR — mirrors renderer.ts settledPromise
|
|
484
|
+
// (not imported: renderer.ts imports from this module).
|
|
485
|
+
function fulfilledPromise<T>(value: T): Promise<T> {
|
|
486
|
+
const promise = Promise.resolve(value) as Promise<T> & { status?: string; value?: T };
|
|
487
|
+
promise.status = 'fulfilled';
|
|
488
|
+
promise.value = value;
|
|
489
|
+
return promise;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function segmentsSharePrefix(a: string[], b: string[], length: number) {
|
|
493
|
+
for (let index = 0; index < length; index += 1) {
|
|
494
|
+
if (a[index] !== b[index]) return false;
|
|
495
|
+
}
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ---------------------------------------------------------------------------
|
|
500
|
+
// Layout-segment context (CORE seam for compat useSelectedLayoutSegment(s)).
|
|
501
|
+
//
|
|
502
|
+
// Each layout in the chain is rendered inside a scope carrying the ordered list
|
|
503
|
+
// of URL segments BELOW that layout's own directory. `useSelectedLayoutSegment(s)`
|
|
504
|
+
// (compat/next/navigation.ts) reads the active scope. Route groups are kept as
|
|
505
|
+
// literal `(group)` entries; a catch-all is a single joined string; a page (leaf)
|
|
506
|
+
// component renders with an empty list. The scope is request-render-scoped so
|
|
507
|
+
// concurrent renders never leak segments across each other.
|
|
508
|
+
// ---------------------------------------------------------------------------
|
|
509
|
+
|
|
510
|
+
export interface LayoutSegmentScope {
|
|
511
|
+
segments: string[];
|
|
512
|
+
appPath?: string;
|
|
513
|
+
layoutDir?: string;
|
|
514
|
+
slots?: Record<string, string>;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const layoutSegmentStorage = new AsyncLocalStorage<LayoutSegmentScope>();
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Run `fn` inside a layout-segment scope. The synchronous portion of a layout's
|
|
521
|
+
* body (where `useSelectedLayoutSegment(s)` is called, per hook rules) sees
|
|
522
|
+
* `segments`; nested layouts install their own deeper scope which correctly
|
|
523
|
+
* shadows this one across awaits (async_hooks propagation, like runWithRequest).
|
|
524
|
+
*/
|
|
525
|
+
export function runInLayoutSegmentScope<T>(
|
|
526
|
+
segments: string[],
|
|
527
|
+
fn: () => T,
|
|
528
|
+
scope: Omit<LayoutSegmentScope, 'segments'> = {},
|
|
529
|
+
): T {
|
|
530
|
+
return layoutSegmentStorage.run({ segments, ...scope }, fn);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Snapshot the active layout scope for stamping onto a client island (see
|
|
535
|
+
* render/island-context.ts). `depth` is how many segments the owning layout
|
|
536
|
+
* consumes; `segments` is its resolved "below me" list; `slots` maps each of
|
|
537
|
+
* the layout's OWN parallel-route slot keys to that slot's resolved segments.
|
|
538
|
+
* Returns undefined outside any layout scope (e.g. a leaf page island).
|
|
539
|
+
*/
|
|
540
|
+
export function currentLayoutSegmentSnapshot():
|
|
541
|
+
| { depth: number; segments: string[]; slots: Record<string, string[]> }
|
|
542
|
+
| undefined {
|
|
543
|
+
const scope = layoutSegmentStorage.getStore();
|
|
544
|
+
if (!scope) return undefined;
|
|
545
|
+
const slots: Record<string, string[]> = {};
|
|
546
|
+
let depth = 0;
|
|
547
|
+
if (scope.appPath && scope.layoutDir) {
|
|
548
|
+
depth = layoutSegmentDepth(scope.appPath, scope.layoutDir);
|
|
549
|
+
const owner = toPosixPath(relative(scope.appPath, scope.layoutDir));
|
|
550
|
+
const prefix = owner ? `${owner}/@` : '@';
|
|
551
|
+
for (const [dir, path] of Object.entries(scope.slots ?? {})) {
|
|
552
|
+
if (!dir.startsWith(prefix)) continue;
|
|
553
|
+
const key = dir.slice(prefix.length);
|
|
554
|
+
if (key.includes('/')) continue;
|
|
555
|
+
slots[key] = pathnameSegments(path);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return { depth, segments: scope.segments, slots };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Whether a layout-segment scope is currently active. A server component in a
|
|
563
|
+
* layout runs inside one (currentLayoutSegments reads it); an island rendered to
|
|
564
|
+
* string does NOT (its scope closed during the resolve phase), so the compat
|
|
565
|
+
* hook falls back to the island context instead. Distinguishes the two callers.
|
|
566
|
+
*/
|
|
567
|
+
export function hasLayoutSegmentScope(): boolean {
|
|
568
|
+
return layoutSegmentStorage.getStore() !== undefined;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** The active layout's "segments below me", or `[]` outside any layout scope. */
|
|
572
|
+
export function currentLayoutSegments(slotKey?: string): string[] {
|
|
573
|
+
const scope = layoutSegmentStorage.getStore();
|
|
574
|
+
if (!scope) return [];
|
|
575
|
+
if (!slotKey) return scope.segments;
|
|
576
|
+
if (!scope.appPath || !scope.layoutDir) return [];
|
|
577
|
+
const owner = toPosixPath(relative(scope.appPath, scope.layoutDir));
|
|
578
|
+
const slotPath = scope.slots?.[owner ? `${owner}/@${slotKey}` : `@${slotKey}`];
|
|
579
|
+
return slotPath ? pathnameSegments(slotPath) : [];
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* The ordered URL segments from the app root down to the leaf, using the real
|
|
584
|
+
* on-disk directory chain (so route groups appear as literal `(group)` entries),
|
|
585
|
+
* with `[param]` dirs replaced by their decoded value, `[...catchall]` /
|
|
586
|
+
* `[[...catchall]]` dirs replaced by the joined catch-all string, and slots
|
|
587
|
+
* (`@name`) skipped. Empty optional catch-all contributes nothing.
|
|
588
|
+
*/
|
|
589
|
+
export function fullLayoutSegments(
|
|
590
|
+
appPath: string,
|
|
591
|
+
leafDir: string,
|
|
592
|
+
params: Record<string, RouteParamValue>,
|
|
593
|
+
): string[] {
|
|
594
|
+
const relativeDir = relative(appPath, leafDir);
|
|
595
|
+
if (relativeDir.startsWith('..')) return [];
|
|
596
|
+
const segments: string[] = [];
|
|
597
|
+
for (const name of relativeDir.split(sep).filter(Boolean)) {
|
|
598
|
+
if (isSlotSegment(name)) continue;
|
|
599
|
+
if (isGroupSegment(name)) {
|
|
600
|
+
segments.push(name);
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
const optional = /^\[\[\.\.\.([^\]]+)\]\]$/.exec(name);
|
|
604
|
+
const catchAll = optional ?? /^\[\.\.\.([^\]]+)\]$/.exec(name);
|
|
605
|
+
if (catchAll?.[1]) {
|
|
606
|
+
const value = params[catchAll[1]];
|
|
607
|
+
const joined = Array.isArray(value) ? value.join('/') : (value ?? '');
|
|
608
|
+
// An empty optional catch-all contributes no segment at all.
|
|
609
|
+
if (joined) segments.push(joined);
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
const dynamic = /^\[([^\].]+)\]$/.exec(name);
|
|
613
|
+
if (dynamic?.[1]) {
|
|
614
|
+
const value = params[dynamic[1]];
|
|
615
|
+
segments.push(Array.isArray(value) ? value.join('/') : String(value ?? ''));
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
segments.push(name);
|
|
619
|
+
}
|
|
620
|
+
return segments;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* The params a layout at `layoutDir` receives: only those introduced by a
|
|
625
|
+
* dynamic segment AT or ABOVE the layout's own directory (Next scopes params
|
|
626
|
+
* per layout level). A catch-all param stays a JSON array; an EMPTY optional
|
|
627
|
+
* catch-all contributes no key at all (so the layout renders no params object).
|
|
628
|
+
* Params below the layout are excluded.
|
|
629
|
+
*/
|
|
630
|
+
export function layoutParamsForDir(
|
|
631
|
+
appPath: string,
|
|
632
|
+
layoutDir: string,
|
|
633
|
+
params: Record<string, RouteParamValue>,
|
|
634
|
+
): Record<string, RouteParamValue> {
|
|
635
|
+
const relativeDir = relative(appPath, layoutDir);
|
|
636
|
+
if (relativeDir.startsWith('..')) return {};
|
|
637
|
+
const scoped: Record<string, RouteParamValue> = {};
|
|
638
|
+
for (const name of relativeDir.split(sep).filter(Boolean)) {
|
|
639
|
+
if (isSlotSegment(name) || isGroupSegment(name)) continue;
|
|
640
|
+
const optional = /^\[\[\.\.\.([^\]]+)\]\]$/.exec(name);
|
|
641
|
+
if (optional?.[1]) {
|
|
642
|
+
const value = params[optional[1]];
|
|
643
|
+
// Empty optional catch-all: omit entirely (no params object for the layout).
|
|
644
|
+
if (value !== undefined && (Array.isArray(value) ? value.length > 0 : value != null)) {
|
|
645
|
+
scoped[optional[1]] = value;
|
|
646
|
+
}
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
const catchAll = /^\[\.\.\.([^\]]+)\]$/.exec(name);
|
|
650
|
+
if (catchAll?.[1]) {
|
|
651
|
+
const value = params[catchAll[1]];
|
|
652
|
+
if (value !== undefined) scoped[catchAll[1]] = value;
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
const dynamic = /^\[([^\].]+)\]$/.exec(name);
|
|
656
|
+
if (dynamic?.[1]) {
|
|
657
|
+
const value = params[dynamic[1]];
|
|
658
|
+
if (value !== undefined) scoped[dynamic[1]] = value;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return scoped;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Count how many entries of `fullLayoutSegments` a layout directory consumes
|
|
666
|
+
* (i.e. the segments AT or ABOVE the layout). The "segments below me" a layout
|
|
667
|
+
* receives is `full.slice(depthOfDir)`. Groups count as one entry (they are
|
|
668
|
+
* present in the full list); slots count as zero.
|
|
669
|
+
*/
|
|
670
|
+
export function layoutSegmentDepth(appPath: string, layoutDir: string): number {
|
|
671
|
+
const relativeDir = relative(appPath, layoutDir);
|
|
672
|
+
if (relativeDir.startsWith('..') || relativeDir === '') return 0;
|
|
673
|
+
let depth = 0;
|
|
674
|
+
for (const name of relativeDir.split(sep).filter(Boolean)) {
|
|
675
|
+
if (isSlotSegment(name)) continue;
|
|
676
|
+
depth += 1;
|
|
677
|
+
}
|
|
678
|
+
return depth;
|
|
679
|
+
}
|