@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,1525 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import type { ClientReference } from './reference';
|
|
3
|
+
import { ISLAND_STATIC_CHILDREN_ATTRIBUTE } from '../islands/static-children';
|
|
4
|
+
import {
|
|
5
|
+
ISLAND_BOUNDARY_ERROR_DIGEST_ATTRIBUTE,
|
|
6
|
+
ISLAND_BOUNDARY_ERROR_ELEMENT,
|
|
7
|
+
ISLAND_BOUNDARY_ERROR_MESSAGE_ATTRIBUTE,
|
|
8
|
+
} from '../islands/boundary-error';
|
|
9
|
+
|
|
10
|
+
interface ClientEntryOptions {
|
|
11
|
+
pageFile?: string;
|
|
12
|
+
clientReferences: ClientReference[];
|
|
13
|
+
nextCompat?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The app reaches a server action anywhere (build's action manifest, or a route whose scan
|
|
16
|
+
* recorded the `actions`/`form` client-entry reason). The whole action client runtime - RPC wire,
|
|
17
|
+
* form interception, prop revival, error overlay - is emitted only then.
|
|
18
|
+
*/
|
|
19
|
+
actions?: boolean;
|
|
20
|
+
/** Route's nearest error.* file: rendered as the action-error overlay (compat). */
|
|
21
|
+
errorFile?: string;
|
|
22
|
+
/** Route's nearest not-found.* file: rendered for client-side notFound(). */
|
|
23
|
+
notFoundFile?: string;
|
|
24
|
+
/**
|
|
25
|
+
* A `'use client'` module in the route graph throws notFound()/forbidden()/
|
|
26
|
+
* unauthorized(). The boundary then has something to render (its built-in
|
|
27
|
+
* 404) even with no not-found.* file, so it must ship for this route.
|
|
28
|
+
*/
|
|
29
|
+
controlFlow?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* App-root global-error.* file (compat): passed to installClientErrors as the
|
|
32
|
+
* globalErrorComponent so a user global-error mounts for client throws that
|
|
33
|
+
* escape every route error boundary. Threaded by client/build.ts; consumed by
|
|
34
|
+
* routerImportSource + mountLifecycleSource (mount-section coder owns wiring).
|
|
35
|
+
*/
|
|
36
|
+
globalErrorFile?: string;
|
|
37
|
+
/**
|
|
38
|
+
* The route's root layout is itself a `'use client'` component. Only then does the whole document
|
|
39
|
+
* body need re-rendering through that layout at hydration (the client-shell path, which adopts each
|
|
40
|
+
* parallel-route slot's SSR DOM as a named prop). When the root layout is a server component its
|
|
41
|
+
* client references are ordinary nested/slot islands that mount into their own containers, and
|
|
42
|
+
* building a shell would wrongly remount the body under one of those islands.
|
|
43
|
+
*/
|
|
44
|
+
clientRootLayout?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Some file in this route's closure can reach a soft navigation (a Link, a navigation hook, compat
|
|
47
|
+
* parity). False means no click on this page can ever start one, so the entry ships no router.
|
|
48
|
+
*/
|
|
49
|
+
router?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Layout files outermost-first; the shell nests in this order because
|
|
52
|
+
* `clientReferences` is path-hash sorted (see build.ts shellLayoutOrder).
|
|
53
|
+
*/
|
|
54
|
+
shellLayoutOrder?: string[];
|
|
55
|
+
/**
|
|
56
|
+
* Dev split: the served URL a deferred dynamic reference loads from instead
|
|
57
|
+
* of bundling its target into this entry (see buildClientDynamicChunk).
|
|
58
|
+
*/
|
|
59
|
+
deferredDynamicHref?: (reference: ClientReference) => string | undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The soft-navigation runtime is bundled into every entry from this source
|
|
63
|
+
// file; esbuild resolves the absolute path, so entries share the module (and
|
|
64
|
+
// its prefetch/navigation state) through the common chunk graph.
|
|
65
|
+
function routerModulePath() {
|
|
66
|
+
return path.join(import.meta.dirname, '../api/router.ts');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Server-action client runtime lives in the compat layer (only ever imported
|
|
70
|
+
// into an entry when Next compat is on — see routerImportSource). entry.ts is
|
|
71
|
+
// core but emits these as generated import strings, gated on nextCompat, so
|
|
72
|
+
// core carries no static import of compat.
|
|
73
|
+
function actionRouterModulePath() {
|
|
74
|
+
return path.join(import.meta.dirname, '../compat/actions/action-router.ts');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function actionClientModulePath() {
|
|
78
|
+
return path.join(import.meta.dirname, '../compat/actions/action-client.ts');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function navCompatModulePath() {
|
|
82
|
+
return path.join(import.meta.dirname, '../compat/client/nav-compat.ts');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function islandContextModulePath() {
|
|
86
|
+
return path.join(import.meta.dirname, '../render/island-context.ts');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Wire-marker revival shared with the server encoder (utils/serialize.ts):
|
|
90
|
+
// island props carrying a CYCLE travel as `$$pnext_ref` back-references.
|
|
91
|
+
function staticSlotsModulePath() {
|
|
92
|
+
return path.join(import.meta.dirname, '../islands/static-slots.ts');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function serializeModulePath() {
|
|
96
|
+
return path.join(import.meta.dirname, '../utils/serialize.ts');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Re-provide the per-island layout-segment snapshot (stamped by the renderer as
|
|
100
|
+
// `data-pnext-layout-segments`) around a hydrated island so compat's
|
|
101
|
+
// useSelectedLayoutSegment(s) recomputes live from the layout depth. Emitted
|
|
102
|
+
// only under next compat (the hooks are compat-only); a no-op wrapper otherwise.
|
|
103
|
+
function layoutSegmentHelperSource(nextCompat?: boolean) {
|
|
104
|
+
if (!nextCompat) return '';
|
|
105
|
+
return `
|
|
106
|
+
import { LayoutSegmentContext as __PNextLayoutSegmentContext, RouteParamsContext as __PNextRouteParamsContext } from ${JSON.stringify(islandContextModulePath())};
|
|
107
|
+
function pnextIslandReadJson(root, attr) {
|
|
108
|
+
const raw = root && root.getAttribute ? root.getAttribute(attr) : null;
|
|
109
|
+
if (!raw) return null;
|
|
110
|
+
try { return JSON.parse(raw); } catch (e) { return null; }
|
|
111
|
+
}
|
|
112
|
+
function pnextIslandVNode(h, Component, props, children, root) {
|
|
113
|
+
// An explicit undefined children argument clobbers props.children in
|
|
114
|
+
// preact's createElement; plain children travel inside the parsed props.
|
|
115
|
+
let vnode = children !== undefined ? h(Component, props, children) : h(Component, props);
|
|
116
|
+
const segments = pnextIslandReadJson(root, 'data-pnext-layout-segments');
|
|
117
|
+
if (segments) vnode = h(__PNextLayoutSegmentContext.Provider, { value: segments }, vnode);
|
|
118
|
+
const params = pnextIslandReadJson(root, 'data-pnext-params');
|
|
119
|
+
if (params) vnode = h(__PNextRouteParamsContext.Provider, { value: params }, vnode);
|
|
120
|
+
return vnode;
|
|
121
|
+
}
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function islandVNodeExpr(
|
|
126
|
+
componentExpr: string,
|
|
127
|
+
propsExpr: string,
|
|
128
|
+
childrenExpr: string,
|
|
129
|
+
rootExpr: string,
|
|
130
|
+
nextCompat?: boolean,
|
|
131
|
+
) {
|
|
132
|
+
return nextCompat
|
|
133
|
+
? `pnextIslandVNode(h, ${componentExpr}, ${propsExpr}, ${childrenExpr}, ${rootExpr})`
|
|
134
|
+
: `h(${componentExpr}, ${propsExpr}, ${childrenExpr})`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Two mount passes can reach the same island root concurrently - mountRoute and the streamed-Suspense
|
|
138
|
+
// re-scan both walk the document, and their "already live" guard runs BEFORE mountIsland awaits the
|
|
139
|
+
// component module. Without a synchronous claim both passes mount: the first tree is orphaned over the
|
|
140
|
+
// same DOM, still subscribed to the router's location store, and re-renders its stale state over the
|
|
141
|
+
// live tree on the next URL change.
|
|
142
|
+
function mountOnceSource() {
|
|
143
|
+
return `
|
|
144
|
+
async function mountIslandOnce(root, mountTree) {
|
|
145
|
+
const pending = root.__pnextMounting;
|
|
146
|
+
if (pending) {
|
|
147
|
+
await pending;
|
|
148
|
+
// The other pass mounted this root; only a freshly stashed incoming
|
|
149
|
+
// placeholder (a soft-nav graft) still needs this pass to re-render.
|
|
150
|
+
if (root.__pnextLive && !root.__pnextIncoming) return;
|
|
151
|
+
}
|
|
152
|
+
const mounting = mountTree();
|
|
153
|
+
root.__pnextMounting = mounting;
|
|
154
|
+
try {
|
|
155
|
+
await mounting;
|
|
156
|
+
} finally {
|
|
157
|
+
if (root.__pnextMounting === mounting) root.__pnextMounting = undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Error seams, imported from their leaf modules rather than through a barrel:
|
|
164
|
+
// esbuild assigns chunks per set-of-reaching-entries on the MODULE graph, so a
|
|
165
|
+
// barrel re-export would drag the boundary into first paint even for an entry
|
|
166
|
+
// that never names it.
|
|
167
|
+
function clientErrorsInstallPath() {
|
|
168
|
+
return path.join(import.meta.dirname, '../compat/client/errors/lazy.ts');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function clientErrorBoundaryPath() {
|
|
172
|
+
return path.join(import.meta.dirname, '../compat/client/errors/error-boundary.ts');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function softRefreshModulePath() {
|
|
176
|
+
return path.join(import.meta.dirname, '../compat/client/errors/soft-refresh.ts');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// The in-tree ClientErrorBoundary is emitted only for a route that has an
|
|
180
|
+
// error.*/not-found.* to render at its own position in the tree. Without one it
|
|
181
|
+
// would only re-throw: the deferred window last resort (errors/install) already
|
|
182
|
+
// drives control-flow errors and mounts global-error for anything uncaught, so
|
|
183
|
+
// an app with no boundary conventions ships none of the boundary cluster.
|
|
184
|
+
function boundaryImportSource(nextCompat?: boolean, boundary?: boolean, builtinNotFound?: boolean) {
|
|
185
|
+
if (!nextCompat || !boundary) return '';
|
|
186
|
+
return `import { ClientErrorBoundary } from ${JSON.stringify(clientErrorBoundaryPath())};
|
|
187
|
+
import { softRefreshRoute } from ${JSON.stringify(softRefreshModulePath())};${
|
|
188
|
+
builtinNotFound ? `\n${builtInNotFoundSource()}` : ''
|
|
189
|
+
}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function errorUiModulePath() {
|
|
193
|
+
return path.join(import.meta.dirname, '../compat/lifecycle/error-ui.ts');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Next's default 404 body, for a client notFound() on a route with no not-found.* of its own.
|
|
197
|
+
// Emitted only when some route can reach it: error-ui.ts is first-paint bytes an app with a
|
|
198
|
+
// not-found file never uses.
|
|
199
|
+
function builtInNotFoundSource() {
|
|
200
|
+
return `import { httpAccessFallbackUi as __pnextHttpAccessFallbackUi } from ${JSON.stringify(errorUiModulePath())};
|
|
201
|
+
function __PNextBuiltInNotFound() {
|
|
202
|
+
return __pnextHttpAccessFallbackUi(404, 'This page could not be found.');
|
|
203
|
+
}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The compat facts every entry shape emits from. Kept as one record so the
|
|
208
|
+
* shapes stay in step: each of them decides the same imports and the same
|
|
209
|
+
* install calls, differing only in what they mount.
|
|
210
|
+
*/
|
|
211
|
+
type EntryCompatFacts = Pick<
|
|
212
|
+
ClientEntryOptions,
|
|
213
|
+
| 'nextCompat'
|
|
214
|
+
| 'actions'
|
|
215
|
+
| 'errorFile'
|
|
216
|
+
| 'globalErrorFile'
|
|
217
|
+
| 'notFoundFile'
|
|
218
|
+
| 'controlFlow'
|
|
219
|
+
| 'router'
|
|
220
|
+
>;
|
|
221
|
+
|
|
222
|
+
/** Whether this route has something for the in-tree boundary to render. */
|
|
223
|
+
export function hasErrorBoundary(
|
|
224
|
+
facts: Pick<ClientEntryOptions, 'errorFile' | 'notFoundFile' | 'controlFlow'>,
|
|
225
|
+
) {
|
|
226
|
+
return Boolean(facts.errorFile ?? facts.notFoundFile) || facts.controlFlow === true;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function routerImportSource({
|
|
230
|
+
nextCompat,
|
|
231
|
+
actions,
|
|
232
|
+
errorFile,
|
|
233
|
+
globalErrorFile,
|
|
234
|
+
notFoundFile,
|
|
235
|
+
router,
|
|
236
|
+
}: EntryCompatFacts) {
|
|
237
|
+
// No file in this route's closure can start a soft navigation (no Link, no navigation hook), so
|
|
238
|
+
// the entry imports no router and the first-paint hub leaves the page entirely. The auto-mount
|
|
239
|
+
// guard is the one thing still owed, and it is one window read (autoMountGuard).
|
|
240
|
+
const base = router
|
|
241
|
+
? `import { installRouter, routerImportActive } from ${JSON.stringify(routerModulePath())};`
|
|
242
|
+
: '';
|
|
243
|
+
if (!nextCompat) return base;
|
|
244
|
+
const imports = [
|
|
245
|
+
`${base}
|
|
246
|
+
import { installNavCompat } from ${JSON.stringify(navCompatModulePath())};
|
|
247
|
+
import { installClientErrors } from ${JSON.stringify(clientErrorsInstallPath())};`,
|
|
248
|
+
];
|
|
249
|
+
if (actions) {
|
|
250
|
+
// Compat apps may use server actions whose client stub navigates redirects
|
|
251
|
+
// through window.__PNEXT_ROUTER__; install that global from the entry.
|
|
252
|
+
imports.push(
|
|
253
|
+
`import { installActionRouter } from ${JSON.stringify(actionRouterModulePath())};
|
|
254
|
+
import { isActionError } from ${JSON.stringify(actionClientModulePath())};`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
if (errorFile) {
|
|
258
|
+
// Route error.js: rendered as an overlay when an action error goes uncaught
|
|
259
|
+
// (React's error-boundary semantics for action failures).
|
|
260
|
+
if (actions) {
|
|
261
|
+
imports.push(
|
|
262
|
+
`import { installActionErrorOverlay } from ${JSON.stringify(actionClientModulePath())};`,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
imports.push(`import __PNextRouteErrorComponent from ${JSON.stringify(errorFile)};`);
|
|
266
|
+
}
|
|
267
|
+
if (globalErrorFile) {
|
|
268
|
+
imports.push(`import __PNextGlobalErrorComponent from ${JSON.stringify(globalErrorFile)};`);
|
|
269
|
+
}
|
|
270
|
+
if (notFoundFile) {
|
|
271
|
+
imports.push(`import __PNextRouteNotFoundComponent from ${JSON.stringify(notFoundFile)};`);
|
|
272
|
+
}
|
|
273
|
+
return imports.join('\n');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Island props may carry serialized server-action references; the action runtime revives them into
|
|
277
|
+
// RPC functions. Lazy lookup: the runtime installs in the entry lifecycle before any island mounts.
|
|
278
|
+
// The `$$pnextError` marker is written by the renderer's action-prop serializer, which returns props
|
|
279
|
+
// untouched when next compat is off - so core and react entries import the ref+binary reviver and the
|
|
280
|
+
// error branch never ships.
|
|
281
|
+
function propsParserSource(nextCompat?: boolean) {
|
|
282
|
+
const revive = nextCompat ? 'reviveSerializedErrorRefs' : 'reviveSerializedRefs';
|
|
283
|
+
return `
|
|
284
|
+
import { ${revive} as __pnextReviveSerializedRefs } from ${JSON.stringify(serializeModulePath())};
|
|
285
|
+
import { hasIslandStaticSlots as __pnextHasIslandSlots, reviveIslandStaticSlots as __pnextReviveIslandSlots } from ${JSON.stringify(staticSlotsModulePath())};
|
|
286
|
+
// Element-valued props: the wire carries a \`$$pnext_slot\` id per element and the server rendered it
|
|
287
|
+
// inside a matching \`pnext-static-slot\` host, adopted here exactly like element children. Islands
|
|
288
|
+
// with no element props skip the walk on a substring test of the raw attribute.
|
|
289
|
+
function islandProps(raw, root, toChildren) {
|
|
290
|
+
const props = parseIslandProps(raw);
|
|
291
|
+
if (!__pnextHasIslandSlots(raw)) return props;
|
|
292
|
+
return __pnextReviveIslandSlots(props, root, toChildren);
|
|
293
|
+
}
|
|
294
|
+
function parseIslandProps(raw) {
|
|
295
|
+
const props = JSON.parse(raw || '{}');${
|
|
296
|
+
nextCompat
|
|
297
|
+
? `
|
|
298
|
+
const actions = typeof window !== 'undefined' ? window.__PNEXT_ACTIONS__ : undefined;`
|
|
299
|
+
: ''
|
|
300
|
+
}
|
|
301
|
+
// Cyclic props: the encoder wrote the self-reference as a \`$$pnext_ref\` path
|
|
302
|
+
// marker. Patch the original object back in AFTER action revival (which
|
|
303
|
+
// rebuilds plain objects, so resolving first would restore identity onto
|
|
304
|
+
// objects the revival then replaces).
|
|
305
|
+
const revived = __pnextReviveSerializedRefs(${nextCompat ? 'actions ? actions.reviveProps(props) : props' : 'props'});
|
|
306
|
+
// Promise props (page params/searchParams of client slot pages) travel as
|
|
307
|
+
// markers; revive them into PRE-FULFILLED promises (React use() protocol:
|
|
308
|
+
// status/value readable synchronously). A bare Promise.resolve would make
|
|
309
|
+
// use() suspend during hydration, and a suspended hydration re-render
|
|
310
|
+
// appends fragment siblings instead of reusing the server DOM.
|
|
311
|
+
for (const key of Object.keys(revived)) {
|
|
312
|
+
const value = revived[key];
|
|
313
|
+
if (value && typeof value === 'object' && '__pnextPromise' in value) {
|
|
314
|
+
revived[key] = pnextFulfilled(value.__pnextPromise);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return revived;
|
|
318
|
+
}
|
|
319
|
+
function pnextFulfilled(value) {
|
|
320
|
+
const promise = Promise.resolve(value);
|
|
321
|
+
promise.status = 'fulfilled';
|
|
322
|
+
promise.value = value;
|
|
323
|
+
return promise;
|
|
324
|
+
}
|
|
325
|
+
`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Entries auto-mount only when loaded as the document's own module script.
|
|
329
|
+
// When the router imports an entry (asset warmup or a soft navigation) the
|
|
330
|
+
// router itself decides when to call mountRoute against the swapped DOM.
|
|
331
|
+
function mountLifecycleSource(nextCompat?: boolean) {
|
|
332
|
+
return `
|
|
333
|
+
${
|
|
334
|
+
nextCompat
|
|
335
|
+
? `// __NEXT_HYDRATED is the harness's interaction gate (test/lib/next-webdriver.ts
|
|
336
|
+
// waits on it and clicks immediately after), so it must mean "the visible tree is interactive", not
|
|
337
|
+
// "the island mounts were kicked off". preact's hydrate() attaches listeners during its synchronous
|
|
338
|
+
// commit, so the mount promises settling IS commit-level readiness. The gate waits for every promise
|
|
339
|
+
// that owns visible DOM and flips in a microtask so nothing extra is interposed under CPU load.
|
|
340
|
+
// pnextGateDeadline is a deadlock guard only: a wedged dynamic import must degrade to a late gate,
|
|
341
|
+
// never to a gate that never opens.
|
|
342
|
+
const pnextGateDeadline = 10000;
|
|
343
|
+
function pnextSettled(value) {
|
|
344
|
+
if (value === undefined) return undefined;
|
|
345
|
+
return Promise.resolve(value).then(pnextIgnore, pnextIgnore);
|
|
346
|
+
}
|
|
347
|
+
function pnextIgnore() {}
|
|
348
|
+
function markRouteHydrated() {
|
|
349
|
+
window.__NEXT_HYDRATED = true;
|
|
350
|
+
window.__NEXT_HYDRATED_AT ??= performance.now();
|
|
351
|
+
if (typeof window.__NEXT_HYDRATED_CB === 'function') window.__NEXT_HYDRATED_CB();
|
|
352
|
+
window.dispatchEvent(new Event('pnext:hydrated'));
|
|
353
|
+
}
|
|
354
|
+
// A settled mount promise means the DOM is attached, NOT that useEffect ran - preact defers effects
|
|
355
|
+
// to after-paint, so a layout publishing its handle from an effect is still unset when
|
|
356
|
+
// __NEXT_HYDRATED flips. Drive preact's flush through options.requestAnimationFrame so "effects ran"
|
|
357
|
+
// is an observable event the gate can await (plain rAF/macrotask ordering is not reliable in headless
|
|
358
|
+
// Chromium).
|
|
359
|
+
let pnextPendingEffectFlushes = 0;
|
|
360
|
+
let pnextEffectFlushWaiters = [];
|
|
361
|
+
const pnextEffectFlushRafTimeout = 100;
|
|
362
|
+
function pnextDrainEffectFlush(flush) {
|
|
363
|
+
pnextPendingEffectFlushes--;
|
|
364
|
+
try {
|
|
365
|
+
flush();
|
|
366
|
+
} finally {
|
|
367
|
+
if (pnextPendingEffectFlushes <= 0) {
|
|
368
|
+
pnextPendingEffectFlushes = 0;
|
|
369
|
+
const waiters = pnextEffectFlushWaiters;
|
|
370
|
+
pnextEffectFlushWaiters = [];
|
|
371
|
+
for (const waiter of waiters) waiter();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
__pnextPreactOptions.requestAnimationFrame = flush => {
|
|
376
|
+
pnextPendingEffectFlushes++;
|
|
377
|
+
let ran = false;
|
|
378
|
+
const run = () => {
|
|
379
|
+
if (ran) return;
|
|
380
|
+
ran = true;
|
|
381
|
+
pnextDrainEffectFlush(flush);
|
|
382
|
+
};
|
|
383
|
+
// preact's own afterNextFrame semantics: the frame, or a 100ms fallback for a
|
|
384
|
+
// document whose rAF never fires (a backgrounded page).
|
|
385
|
+
const timer = setTimeout(run, pnextEffectFlushRafTimeout);
|
|
386
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
387
|
+
requestAnimationFrame(() => {
|
|
388
|
+
clearTimeout(timer);
|
|
389
|
+
run();
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
function pnextAfterEffectFlush(run) {
|
|
394
|
+
// Effects are queued during the mount's synchronous commit, so by the time
|
|
395
|
+
// the mount promise settles every pending flush is already registered above.
|
|
396
|
+
// Nothing pending means there is nothing left to wait for.
|
|
397
|
+
if (pnextPendingEffectFlushes === 0) {
|
|
398
|
+
run();
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
pnextEffectFlushWaiters.push(run);
|
|
402
|
+
}
|
|
403
|
+
function pnextGate(mark, islandMounts, visibleMount) {
|
|
404
|
+
const waits = [pnextSettled(islandMounts), pnextSettled(visibleMount)].filter(Boolean);
|
|
405
|
+
if (waits.length === 0) {
|
|
406
|
+
mark();
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
let done = false;
|
|
410
|
+
const once = () => {
|
|
411
|
+
if (done) return;
|
|
412
|
+
done = true;
|
|
413
|
+
mark();
|
|
414
|
+
};
|
|
415
|
+
const timer = setTimeout(once, pnextGateDeadline);
|
|
416
|
+
void Promise.all(waits).then(() => {
|
|
417
|
+
pnextAfterEffectFlush(() => {
|
|
418
|
+
clearTimeout(timer);
|
|
419
|
+
once();
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
`
|
|
424
|
+
: ''
|
|
425
|
+
}
|
|
426
|
+
function keepsMountedRoot(root, keep) {
|
|
427
|
+
if (!keep) return false;
|
|
428
|
+
for (const kept of keep) {
|
|
429
|
+
if (root === kept) return true;
|
|
430
|
+
// The preserved client page's container is a marker-range proxy, not a
|
|
431
|
+
// Node — contains() would throw on it, and only identity can match it.
|
|
432
|
+
if (kept instanceof Node && root.contains && root.contains(kept)) return true;
|
|
433
|
+
}
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
`;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// The per-route half of the lifecycle, closing over the route's own mount
|
|
440
|
+
// state. mountRoute runs the steps the route actually has: an island scan, the
|
|
441
|
+
// client-shell hydration, the client page.
|
|
442
|
+
function routeLifecycleSource({ nextCompat, islands, page, shell }: ClientRuntimeFacts) {
|
|
443
|
+
const gate = nextCompat ? ' pnextGate(markRouteHydrated, pnextIslandMounts, pnextVisibleMount);\n' : '';
|
|
444
|
+
const mountBody = [
|
|
445
|
+
islands
|
|
446
|
+
? ` if (islands.length > 0) {
|
|
447
|
+
pnextIslandScan = scanIslands();
|
|
448
|
+
pnextIslandMounts = pnextIslandScan.done.then(() => pnextIslandScan.mounted);
|
|
449
|
+
}`
|
|
450
|
+
: '',
|
|
451
|
+
shell ? ' if (shell) pnextVisibleMount = hydrateClientShell();' : '',
|
|
452
|
+
page
|
|
453
|
+
? ` if (Page) {
|
|
454
|
+
if (islands.length > 0) pnextVisibleMount = hydratePageAfterIslands();
|
|
455
|
+
else hydratePage();
|
|
456
|
+
}`
|
|
457
|
+
: '',
|
|
458
|
+
]
|
|
459
|
+
.filter(Boolean)
|
|
460
|
+
.join('\n');
|
|
461
|
+
return `
|
|
462
|
+
function mountRoute() {
|
|
463
|
+
${mountBody}
|
|
464
|
+
window.__PNEXT_ACTIVE_ENTRY__ = { unmount: unmountRoute };
|
|
465
|
+
${gate} return pnextIslandMounts;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// A postponed boundary can flush AFTER this entry module has evaluated: the shell's async module
|
|
469
|
+
// scripts race the server's tail, and a server under load loses that race - the initial mountRoute()
|
|
470
|
+
// then sees a document holding only the placeholder, mounts nothing, and the tree stays inert
|
|
471
|
+
// FOREVER, because promotion has no way back into the entry. The promotion script calls this hook
|
|
472
|
+
// once the real DOM is parsed in. Every step is guarded, so re-running them hydrates only what the
|
|
473
|
+
// first pass could not see.
|
|
474
|
+
function remountRoute() {
|
|
475
|
+
${mountBody}
|
|
476
|
+
${gate} return pnextIslandMounts;
|
|
477
|
+
}
|
|
478
|
+
window.__PNEXT_MOUNT_ISLANDS__ = remountRoute;
|
|
479
|
+
|
|
480
|
+
function unmountRoute(keep) {
|
|
481
|
+
// Roots in \`keep\` are grafted into the next document by the router (shared
|
|
482
|
+
// layout preservation) — leave them and any mounted ancestor intact; an
|
|
483
|
+
// ancestor unmount would recursively tear down the preserved island.
|
|
484
|
+
for (const root of pnextMountedRoots) {
|
|
485
|
+
if (keepsMountedRoot(root, keep)) continue;
|
|
486
|
+
render(null, root);
|
|
487
|
+
root.__pnextLive = undefined;
|
|
488
|
+
}
|
|
489
|
+
// A preserved client page keeps its registration: its tree is still mounted
|
|
490
|
+
// and grafted into the new body, so hydratePage must see it as live instead
|
|
491
|
+
// of re-hydrating over it. Every other root is dropped — island roots kept
|
|
492
|
+
// for the bfcache are detached and owned by the cache from here on.
|
|
493
|
+
const page = window.__PNEXT_CLIENT_PAGE_ROOT__;
|
|
494
|
+
let keepsPage = false;
|
|
495
|
+
if (page && keep) for (const kept of keep) if (kept === page) keepsPage = true;
|
|
496
|
+
pnextMountedRoots.clear();
|
|
497
|
+
if (keepsPage) pnextMountedRoots.add(page);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return { mountRoute, remountRoute, unmountRoute };
|
|
501
|
+
`;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* A route that renders no client page and no islands still needs an entry - it has a Link or another
|
|
506
|
+
* router seam - but nothing in it ever renders, so it ships neither preact nor the mount/hydrate
|
|
507
|
+
* machinery: just the router hub and the no-op module shape the router imports across a soft nav.
|
|
508
|
+
*/
|
|
509
|
+
function routerOnlyEntrySource(facts: EntryCompatFacts) {
|
|
510
|
+
// Nothing hydrates here, so the route is interactive the moment mountRoute
|
|
511
|
+
// runs — mark it right away (the island shapes gate on their mount promises
|
|
512
|
+
// first) or the harness waits out its full __NEXT_HYDRATED fallback.
|
|
513
|
+
const mark = facts.nextCompat
|
|
514
|
+
? `
|
|
515
|
+
function markRouteHydrated() {
|
|
516
|
+
window.__NEXT_HYDRATED = true;
|
|
517
|
+
window.__NEXT_HYDRATED_AT ??= performance.now();
|
|
518
|
+
if (typeof window.__NEXT_HYDRATED_CB === 'function') window.__NEXT_HYDRATED_CB();
|
|
519
|
+
window.dispatchEvent(new Event('pnext:hydrated'));
|
|
520
|
+
}
|
|
521
|
+
`
|
|
522
|
+
: '';
|
|
523
|
+
return `
|
|
524
|
+
${routerImportSource(facts)}
|
|
525
|
+
${mark}
|
|
526
|
+
export function mountRoute() {
|
|
527
|
+
window.__PNEXT_ACTIVE_ENTRY__ = { unmount: unmountRoute };
|
|
528
|
+
${facts.nextCompat ? ' markRouteHydrated();\n' : ''}}
|
|
529
|
+
|
|
530
|
+
export function unmountRoute() {}
|
|
531
|
+
|
|
532
|
+
${installRouterCall(facts.router)}${installCallsSource(facts)}
|
|
533
|
+
if (${autoMountGuard(facts.router)}) mountRoute();
|
|
534
|
+
if (window.__PNEXT_REGISTER_ENTRY__) window.__PNEXT_REGISTER_ENTRY__(import.meta.url, { mountRoute, unmountRoute });
|
|
535
|
+
`;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** `installRouter()`, or nothing when the route can never soft-navigate. */
|
|
539
|
+
function installRouterCall(router?: boolean) {
|
|
540
|
+
return router ? 'installRouter();\n' : '';
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Entries auto-mount when the browser loads them as the document's own module script, never when the
|
|
545
|
+
* router imports one. Router-free entries read the counter directly rather than importing
|
|
546
|
+
* `routerImportActive` - that one import would pull the whole first-paint hub back onto the page.
|
|
547
|
+
*/
|
|
548
|
+
function autoMountGuard(router?: boolean) {
|
|
549
|
+
return router ? '!routerImportActive()' : '!((window.__PNEXT_ROUTER_IMPORTS__ ?? 0) > 0)';
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// The compat install calls that follow installRouter() in every entry shape.
|
|
553
|
+
function installCallsSource({
|
|
554
|
+
nextCompat,
|
|
555
|
+
actions,
|
|
556
|
+
errorFile,
|
|
557
|
+
globalErrorFile,
|
|
558
|
+
notFoundFile,
|
|
559
|
+
}: EntryCompatFacts) {
|
|
560
|
+
if (!nextCompat) return '';
|
|
561
|
+
const overlay = actions && errorFile ? 'installActionErrorOverlay(__PNextRouteErrorComponent);\n' : '';
|
|
562
|
+
return `${actions ? 'installActionRouter();\n' : ''}installNavCompat();
|
|
563
|
+
${overlay}installClientErrors({ errorComponent: ${
|
|
564
|
+
errorFile ? '__PNextRouteErrorComponent' : 'undefined'
|
|
565
|
+
}, globalErrorComponent: ${
|
|
566
|
+
globalErrorFile ? '__PNextGlobalErrorComponent' : 'undefined'
|
|
567
|
+
}, notFoundComponent: ${
|
|
568
|
+
notFoundFile ? '__PNextRouteNotFoundComponent' : 'undefined'
|
|
569
|
+
}, isActionError: ${actions ? 'isActionError' : 'undefined'} });`;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* App-wide facts the shared route runtime is emitted from: each piece of glue
|
|
574
|
+
* ships only when some route in the app can actually use it.
|
|
575
|
+
*/
|
|
576
|
+
export interface ClientRuntimeFacts {
|
|
577
|
+
nextCompat?: boolean;
|
|
578
|
+
/** Any route mounts islands (island scan, revival, static children). */
|
|
579
|
+
islands?: boolean;
|
|
580
|
+
/** Any route is a whole-page `'use client'` component. */
|
|
581
|
+
page?: boolean;
|
|
582
|
+
/** Any route hydrates a client root layout as the document shell. */
|
|
583
|
+
shell?: boolean;
|
|
584
|
+
/** Any route has a `dynamic(..., { ssr: false })` island (DeferredIsland). */
|
|
585
|
+
deferredIslands?: boolean;
|
|
586
|
+
/** Any island loads on visibility (IntersectionObserver tier). */
|
|
587
|
+
visibleIslands?: boolean;
|
|
588
|
+
/** Any route has an `error.*`/`not-found.*` the in-tree boundary renders. */
|
|
589
|
+
errorBoundary?: boolean;
|
|
590
|
+
/**
|
|
591
|
+
* Some boundary route has NO `not-found.*` of its own, so a client-side
|
|
592
|
+
* notFound() there falls back to the built-in 404 UI (error-ui.ts). False
|
|
593
|
+
* keeps that module out of the graph entirely.
|
|
594
|
+
*/
|
|
595
|
+
builtinNotFound?: boolean;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/** Module specifier the generated route stubs import their runtime from. */
|
|
599
|
+
export const CLIENT_RUNTIME_MODULE = 'pnext-client-runtime';
|
|
600
|
+
|
|
601
|
+
/** Dev split: single-instance vendor modules the entry shares with on-demand chunks. */
|
|
602
|
+
export const DYN_SHARED_GLOBAL = '__PNEXT_DYN_SHARED__';
|
|
603
|
+
export function dynSharedSpecifiers(nextCompat?: boolean) {
|
|
604
|
+
return nextCompat
|
|
605
|
+
? [
|
|
606
|
+
'preact',
|
|
607
|
+
'preact/hooks',
|
|
608
|
+
'preact/compat',
|
|
609
|
+
'preact/compat/client',
|
|
610
|
+
'preact/jsx-runtime',
|
|
611
|
+
'preact/jsx-dev-runtime',
|
|
612
|
+
]
|
|
613
|
+
: ['preact', 'preact/hooks', 'preact/jsx-runtime'];
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// A separately-built dynamic chunk must render with THIS entry's preact (hooks
|
|
617
|
+
// dispatch through the renderer's own `options`), so the entry publishes its
|
|
618
|
+
// vendor namespaces for the chunk build's shared-vendor shims to read. Emitted
|
|
619
|
+
// whenever the dev split is armed: dynamic() calls inside 'use client' modules
|
|
620
|
+
// are rewritten mid-build (pipeline), after this source is generated.
|
|
621
|
+
function dynSharedTableSource(
|
|
622
|
+
nextCompat?: boolean,
|
|
623
|
+
deferredDynamicHref?: (reference: ClientReference) => string | undefined,
|
|
624
|
+
) {
|
|
625
|
+
if (!deferredDynamicHref) return '';
|
|
626
|
+
const specifiers = dynSharedSpecifiers(nextCompat);
|
|
627
|
+
const imports = specifiers.map(
|
|
628
|
+
(specifier, index) => `import * as __pnextDynShared${index} from ${JSON.stringify(specifier)};`,
|
|
629
|
+
);
|
|
630
|
+
const entries = specifiers.map(
|
|
631
|
+
(specifier, index) => `${JSON.stringify(specifier)}: __pnextDynShared${index}`,
|
|
632
|
+
);
|
|
633
|
+
return `${imports.join('\n')}\nwindow.${DYN_SHARED_GLOBAL} = { ${entries.join(', ')} };\n`;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
export function clientRuntimeFacts(
|
|
637
|
+
routes: { route: RouteFacts; shell?: boolean; boundary?: boolean; notFound?: boolean }[],
|
|
638
|
+
nextCompat?: boolean,
|
|
639
|
+
): ClientRuntimeFacts {
|
|
640
|
+
const references = routes.flatMap(({ route }) => route.clientReferences);
|
|
641
|
+
return {
|
|
642
|
+
nextCompat,
|
|
643
|
+
islands: references.length > 0,
|
|
644
|
+
page: routes.some(({ route }) => route.client),
|
|
645
|
+
shell: routes.some(entry => entry.shell),
|
|
646
|
+
deferredIslands: references.some(reference => reference.dynamic?.ssr === false),
|
|
647
|
+
visibleIslands: references.some(reference => reference.dynamic?.load === 'visible'),
|
|
648
|
+
errorBoundary: routes.some(entry => entry.boundary),
|
|
649
|
+
builtinNotFound: routes.some(entry => entry.boundary && !entry.notFound),
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
interface RouteFacts {
|
|
654
|
+
client?: boolean;
|
|
655
|
+
clientReferences: ClientReference[];
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* The mount/hydrate machinery every route entry runs - emitted ONCE per app and imported by each
|
|
660
|
+
* route's stub, so the identical glue becomes one shared chunk plus a per-route manifest.
|
|
661
|
+
* Route-specific values (islands, the client page, shell layouts, error components) arrive as config.
|
|
662
|
+
*/
|
|
663
|
+
export function clientRuntimeSource(facts: ClientRuntimeFacts) {
|
|
664
|
+
const { nextCompat, deferredIslands, errorBoundary, builtinNotFound, page, shell } = facts;
|
|
665
|
+
// The shell re-renders the whole document through the client root layout, so
|
|
666
|
+
// it revives island DOM exactly like an island mount does.
|
|
667
|
+
const revival = Boolean(facts.islands || shell);
|
|
668
|
+
return `
|
|
669
|
+
import { h, hydrate, render } from 'preact';
|
|
670
|
+
${nextCompat ? "import { options as __pnextPreactOptions } from 'preact';" : ''}
|
|
671
|
+
${deferredIslands ? "import { useEffect, useState } from 'preact/hooks';" : ''}
|
|
672
|
+
${nextCompat ? "import { Suspense } from 'preact/compat';" : ''}
|
|
673
|
+
${boundaryImportSource(nextCompat, errorBoundary, builtinNotFound)}
|
|
674
|
+
${revival ? layoutSegmentHelperSource(nextCompat) : ''}
|
|
675
|
+
${revival ? propsParserSource(nextCompat) : ''}
|
|
676
|
+
${revival ? islandHelpersSource(facts) : ''}
|
|
677
|
+
${islandCssHelperSource()}
|
|
678
|
+
${mountLifecycleSource(nextCompat)}
|
|
679
|
+
|
|
680
|
+
export function bootstrapRoute(config) {
|
|
681
|
+
const islands = config.islands ?? [];
|
|
682
|
+
const Page = config.Page;
|
|
683
|
+
const shell = config.shell;
|
|
684
|
+
const pnextMountedRoots = new Set();
|
|
685
|
+
let pnextIslandMounts;
|
|
686
|
+
let pnextIslandScan;
|
|
687
|
+
let pnextVisibleMount;
|
|
688
|
+
${clientBoundaryHelperSource(nextCompat, errorBoundary, builtinNotFound)}
|
|
689
|
+
${mountSource()}
|
|
690
|
+
${revival ? hydrateIslandsSource(facts) : ''}
|
|
691
|
+
${shell ? hydrateClientShellSource(nextCompat) : ''}
|
|
692
|
+
${page ? hydratePageSource(nextCompat) : ''}
|
|
693
|
+
${routeLifecycleSource(facts)}
|
|
694
|
+
}
|
|
695
|
+
`;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
export function clientEntrySource(options: ClientEntryOptions) {
|
|
699
|
+
const { pageFile, clientReferences, nextCompat, errorFile, notFoundFile, clientRootLayout, shellLayoutOrder } =
|
|
700
|
+
options;
|
|
701
|
+
const facts: EntryCompatFacts = {
|
|
702
|
+
nextCompat,
|
|
703
|
+
actions: options.actions,
|
|
704
|
+
errorFile,
|
|
705
|
+
globalErrorFile: options.globalErrorFile,
|
|
706
|
+
notFoundFile,
|
|
707
|
+
controlFlow: options.controlFlow,
|
|
708
|
+
// The router-only entry shape exists BECAUSE this route needs the router.
|
|
709
|
+
router: options.router || (!pageFile && clientReferences.length === 0),
|
|
710
|
+
};
|
|
711
|
+
if (!pageFile && clientReferences.length === 0) {
|
|
712
|
+
return routerOnlyEntrySource(facts);
|
|
713
|
+
}
|
|
714
|
+
if (!pageFile && clientReferences.length > 0 && clientReferences.every(isVisibleDynamicReference)) {
|
|
715
|
+
return visibleDynamicIslandEntrySource(clientReferences, facts, options.deferredDynamicHref);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const imports = clientReferences
|
|
719
|
+
.map((reference, index) => {
|
|
720
|
+
if (reference.dynamic) return null;
|
|
721
|
+
// A side-effect-only client module (bare `import './client-only'`) is
|
|
722
|
+
// bundled and run for its top-level code but never mounted as an island —
|
|
723
|
+
// webpack-style eager initialization of all client references.
|
|
724
|
+
if (reference.sideEffect) return `import ${JSON.stringify(reference.file)};`;
|
|
725
|
+
return reference.exportName === 'default'
|
|
726
|
+
? `import Island${index} from ${JSON.stringify(reference.file)};`
|
|
727
|
+
: `import { ${reference.exportName} as Island${index} } from ${JSON.stringify(reference.file)};`;
|
|
728
|
+
})
|
|
729
|
+
.filter((item): item is string => Boolean(item))
|
|
730
|
+
.join('\n');
|
|
731
|
+
const islandManifest = clientReferences
|
|
732
|
+
.map((reference, index) =>
|
|
733
|
+
reference.sideEffect
|
|
734
|
+
? null
|
|
735
|
+
: reference.dynamic
|
|
736
|
+
? `{ id: ${JSON.stringify(reference.id)}, options: ${JSON.stringify(reference.dynamic)}, ${dynamicLoader(reference, options.deferredDynamicHref?.(reference))} }`
|
|
737
|
+
: `{ id: ${JSON.stringify(reference.id)}, Component: Island${index} }`,
|
|
738
|
+
)
|
|
739
|
+
.filter((item): item is string => item !== null)
|
|
740
|
+
.join(',\n ');
|
|
741
|
+
|
|
742
|
+
// Island bindings are named by position in the FULL reference array (see
|
|
743
|
+
// `imports` above), so the shell must carry those names rather than re-index
|
|
744
|
+
// its own filtered/reordered view.
|
|
745
|
+
const islandNames = new Map(
|
|
746
|
+
clientReferences.map((reference, index) => [reference, `Island${index}`]),
|
|
747
|
+
);
|
|
748
|
+
const shellReferences = shellClientReferences(
|
|
749
|
+
clientReferences,
|
|
750
|
+
errorFile,
|
|
751
|
+
notFoundFile,
|
|
752
|
+
shellLayoutOrder,
|
|
753
|
+
);
|
|
754
|
+
const shellNames =
|
|
755
|
+
!pageFile && clientRootLayout && shellReferences.some(reference => !reference.dynamic)
|
|
756
|
+
? shellReferences
|
|
757
|
+
.map(reference => (reference.dynamic ? undefined : islandNames.get(reference)))
|
|
758
|
+
.filter((name): name is string => Boolean(name))
|
|
759
|
+
: undefined;
|
|
760
|
+
const needsCss = clientReferences.some(reference => referenceCssHref(reference));
|
|
761
|
+
|
|
762
|
+
return `
|
|
763
|
+
import { bootstrapRoute${needsCss ? ', loadIslandCss' : ''} } from ${JSON.stringify(CLIENT_RUNTIME_MODULE)};
|
|
764
|
+
${pageFile ? `import Page from ${JSON.stringify(pageFile)};` : ''}
|
|
765
|
+
${imports}
|
|
766
|
+
${dynSharedTableSource(nextCompat, options.deferredDynamicHref)}
|
|
767
|
+
${routerImportSource(facts)}
|
|
768
|
+
|
|
769
|
+
const { mountRoute, remountRoute, unmountRoute } = bootstrapRoute({
|
|
770
|
+
islands: [
|
|
771
|
+
${islandManifest}
|
|
772
|
+
],${pageFile ? '\n Page,' : ''}${shellNames ? `\n shell: [${shellNames.join(', ')}],` : ''}${
|
|
773
|
+
nextCompat
|
|
774
|
+
? `\n errorComponent: ${errorFile ? '__PNextRouteErrorComponent' : 'undefined'},\n notFoundComponent: ${
|
|
775
|
+
notFoundFile ? '__PNextRouteNotFoundComponent' : 'undefined'
|
|
776
|
+
},`
|
|
777
|
+
: ''
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
export { mountRoute, remountRoute, unmountRoute };
|
|
781
|
+
|
|
782
|
+
${installRouterCall(facts.router)}${installCallsSource(facts)}
|
|
783
|
+
if (${autoMountGuard(facts.router)}) void mountRoute();
|
|
784
|
+
if (window.__PNEXT_REGISTER_ENTRY__) window.__PNEXT_REGISTER_ENTRY__(import.meta.url, { mountRoute, unmountRoute });
|
|
785
|
+
`;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function shellClientReferences(
|
|
789
|
+
clientReferences: ClientReference[],
|
|
790
|
+
errorFile?: string,
|
|
791
|
+
notFoundFile?: string,
|
|
792
|
+
shellLayoutOrder?: string[],
|
|
793
|
+
) {
|
|
794
|
+
const shell = clientReferences.filter(
|
|
795
|
+
reference =>
|
|
796
|
+
!reference.sideEffect &&
|
|
797
|
+
reference.file !== errorFile &&
|
|
798
|
+
reference.file !== notFoundFile,
|
|
799
|
+
);
|
|
800
|
+
// The shell nests outermost-first, but `clientReferences` is path-hash
|
|
801
|
+
// sorted — layouts must lead in hierarchy order or a non-layout reference
|
|
802
|
+
// can end up outermost and silently drop the layout subtree. Non-layout
|
|
803
|
+
// references keep their relative order behind the chain.
|
|
804
|
+
if (!shellLayoutOrder?.length) return shell;
|
|
805
|
+
const rank = new Map(shellLayoutOrder.map((file, index) => [file, index]));
|
|
806
|
+
return shell
|
|
807
|
+
.map((reference, index) => ({ reference, index }))
|
|
808
|
+
.sort((a, b) => {
|
|
809
|
+
const aRank = rank.get(a.reference.file);
|
|
810
|
+
const bRank = rank.get(b.reference.file);
|
|
811
|
+
if (aRank !== undefined && bRank !== undefined) return aRank - bRank;
|
|
812
|
+
if (aRank !== undefined) return -1;
|
|
813
|
+
if (bRank !== undefined) return 1;
|
|
814
|
+
return a.index - b.index;
|
|
815
|
+
})
|
|
816
|
+
.map(entry => entry.reference);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function isVisibleDynamicReference(reference: ClientReference) {
|
|
820
|
+
return reference.dynamic?.load === 'visible';
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function visibleDynamicIslandEntrySource(
|
|
824
|
+
clientReferences: ClientReference[],
|
|
825
|
+
facts: EntryCompatFacts,
|
|
826
|
+
deferredDynamicHref?: (reference: ClientReference) => string | undefined,
|
|
827
|
+
) {
|
|
828
|
+
const { nextCompat, errorFile, notFoundFile } = facts;
|
|
829
|
+
const boundary = hasErrorBoundary(facts);
|
|
830
|
+
const islandManifest = clientReferences
|
|
831
|
+
.map(reference => `{ id: ${JSON.stringify(reference.id)}, ${visibleDynamicLoader(reference, deferredDynamicHref?.(reference))} }`)
|
|
832
|
+
.join(',\n ');
|
|
833
|
+
|
|
834
|
+
const compatBoundary = !nextCompat
|
|
835
|
+
? ''
|
|
836
|
+
: boundary
|
|
837
|
+
? `function pnextClientBoundary(h, vnode) {
|
|
838
|
+
return h(ClientErrorBoundary, { errorComponent: ${
|
|
839
|
+
errorFile ? '__PNextRouteErrorComponent' : 'undefined'
|
|
840
|
+
}, notFoundComponent: ${
|
|
841
|
+
notFoundFile ? '__PNextRouteNotFoundComponent' : '__PNextBuiltInNotFound'
|
|
842
|
+
}, onReset: softRefreshRoute }, vnode);
|
|
843
|
+
}
|
|
844
|
+
`
|
|
845
|
+
: // No error.*/not-found.* to render here: the deferred window last resort
|
|
846
|
+
// owns every client throw, so the boundary collapses to the vnode.
|
|
847
|
+
`function pnextClientBoundary(h, vnode) {
|
|
848
|
+
return vnode;
|
|
849
|
+
}
|
|
850
|
+
`;
|
|
851
|
+
|
|
852
|
+
return `
|
|
853
|
+
${nextCompat ? "import { options as __pnextPreactOptions } from 'preact';" : ''}
|
|
854
|
+
${dynSharedTableSource(nextCompat, deferredDynamicHref)}
|
|
855
|
+
${islandCssHelperSource()}
|
|
856
|
+
${routerImportSource(facts)}
|
|
857
|
+
${boundaryImportSource(nextCompat, boundary, boundary && !notFoundFile)}
|
|
858
|
+
${layoutSegmentHelperSource(nextCompat)}
|
|
859
|
+
${mountLifecycleSource(nextCompat)}
|
|
860
|
+
const islands = [
|
|
861
|
+
${islandManifest}
|
|
862
|
+
];
|
|
863
|
+
|
|
864
|
+
const pnextMountedRoots = new Set();
|
|
865
|
+
let pnextIslandMounts;
|
|
866
|
+
let pnextRender;
|
|
867
|
+
|
|
868
|
+
${propsParserSource(nextCompat)}
|
|
869
|
+
${compatBoundary}
|
|
870
|
+
export function mountRoute() {
|
|
871
|
+
for (const island of islands) {
|
|
872
|
+
for (const root of document.querySelectorAll(\`[data-pnext-client="\${island.id}"]\`)) {
|
|
873
|
+
if (isNestedIslandRoot(root)) continue;
|
|
874
|
+
// A preserved root (router grafted its live DOM across the soft nav)
|
|
875
|
+
// still shows the previous route's children — remount it immediately
|
|
876
|
+
// instead of waiting for an intersection.
|
|
877
|
+
if (root.__pnextIncoming) {
|
|
878
|
+
void mountIsland(root, island);
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
observeVisibleIsland(root, island);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
window.__PNEXT_ACTIVE_ENTRY__ = { unmount: unmountRoute };
|
|
885
|
+
${nextCompat ? ' pnextGate(markRouteHydrated, pnextIslandMounts, undefined);\n' : ''} return pnextIslandMounts;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
export const remountRoute = mountRoute;
|
|
889
|
+
window.__PNEXT_MOUNT_ISLANDS__ = remountRoute;
|
|
890
|
+
|
|
891
|
+
export function unmountRoute(keep) {
|
|
892
|
+
// Roots in \`keep\` are grafted into the next document by the router (shared
|
|
893
|
+
// layout preservation) — leave them and any mounted ancestor intact; an
|
|
894
|
+
// ancestor unmount would recursively tear down the preserved island.
|
|
895
|
+
if (pnextRender) for (const root of pnextMountedRoots) {
|
|
896
|
+
if (keepsMountedRoot(root, keep)) continue;
|
|
897
|
+
pnextRender(null, root);
|
|
898
|
+
root.__pnextLive = undefined;
|
|
899
|
+
}
|
|
900
|
+
pnextMountedRoots.clear();
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
${installRouterCall(facts.router)}${installCallsSource(facts)}
|
|
904
|
+
if (${autoMountGuard(facts.router)}) void mountRoute();
|
|
905
|
+
if (window.__PNEXT_REGISTER_ENTRY__) window.__PNEXT_REGISTER_ENTRY__(import.meta.url, { mountRoute, unmountRoute });
|
|
906
|
+
|
|
907
|
+
function observeVisibleIsland(root, island) {
|
|
908
|
+
if (typeof IntersectionObserver === 'undefined') {
|
|
909
|
+
void mountIsland(root, island);
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const target = visibleTarget(root);
|
|
914
|
+
const observer = new IntersectionObserver(entries => {
|
|
915
|
+
if (!entries.some(entry => entry.isIntersecting)) return;
|
|
916
|
+
observer.disconnect();
|
|
917
|
+
void mountIsland(root, island);
|
|
918
|
+
}, island.options);
|
|
919
|
+
|
|
920
|
+
observer.observe(target);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function visibleTarget(root) {
|
|
924
|
+
const rect = root.getBoundingClientRect();
|
|
925
|
+
if (rect.width || rect.height) return root;
|
|
926
|
+
return root.parentElement ?? root;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
${mountOnceSource()}
|
|
930
|
+
${
|
|
931
|
+
nextCompat
|
|
932
|
+
? `let islandBoundary;
|
|
933
|
+
|
|
934
|
+
function mountIsland(root, island) {
|
|
935
|
+
return mountIslandOnce(root, () => mountIslandTree(root, island));
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
async function mountIslandTree(root, island) {
|
|
939
|
+
const [{ h, hydrate, render }, { Suspense }, Component] = await Promise.all([
|
|
940
|
+
import('preact'),
|
|
941
|
+
import('preact/compat'),
|
|
942
|
+
island.load(),
|
|
943
|
+
]);
|
|
944
|
+
islandBoundary = Suspense;
|
|
945
|
+
const rawProps = root.getAttribute('data-pnext-props') ?? '{}';
|
|
946
|
+
const source = preservedSource(root, render);
|
|
947
|
+
const vnode = islandVNode(h, Component, await islandProps(rawProps, source, node => domChildren(h, node)), await staticChildren(h, source, island.id));
|
|
948
|
+
const wrapped = h(Suspense, { fallback: null }, pnextClientBoundary(h, vnode));
|
|
949
|
+
if (source !== root) adoptPreserved(render, root, wrapped);
|
|
950
|
+
else mount(hydrate, render, root, wrapped);
|
|
951
|
+
}`
|
|
952
|
+
: `function mountIsland(root, island) {
|
|
953
|
+
return mountIslandOnce(root, () => mountIslandTree(root, island));
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
async function mountIslandTree(root, island) {
|
|
957
|
+
const [{ h, hydrate, render }, Component] = await Promise.all([import('preact'), island.load()]);
|
|
958
|
+
const rawProps = root.getAttribute('data-pnext-props') ?? '{}';
|
|
959
|
+
const source = preservedSource(root, render);
|
|
960
|
+
const vnode = islandVNode(h, Component, await islandProps(rawProps, source, node => domChildren(h, node)), await staticChildren(h, source, island.id));
|
|
961
|
+
if (source !== root) adoptPreserved(render, root, vnode);
|
|
962
|
+
else mount(hydrate, render, root, vnode);
|
|
963
|
+
}`
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// A root preserved across a soft navigation carries the incoming placeholder
|
|
967
|
+
// (fresh SSR children) stashed by the router; render children from it when this
|
|
968
|
+
// preact instance owns the live tree. A foreign entry copy (dev bundles) can't
|
|
969
|
+
// be diffed — restore the SSR children and mount fresh instead.
|
|
970
|
+
function preservedSource(root, render) {
|
|
971
|
+
const incoming = root.__pnextIncoming;
|
|
972
|
+
root.__pnextIncoming = undefined;
|
|
973
|
+
if (!incoming) return root;
|
|
974
|
+
if (root.__pnextLive === render) return incoming;
|
|
975
|
+
root.replaceChildren(...incoming.childNodes);
|
|
976
|
+
root.__pnextLive = undefined;
|
|
977
|
+
return root;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Re-render the preserved root in place so component state survives while the
|
|
981
|
+
// routed content under the island updates.
|
|
982
|
+
function adoptPreserved(render, root, vnode) {
|
|
983
|
+
render(vnode, root);
|
|
984
|
+
pnextRender = render;
|
|
985
|
+
pnextMountedRoots.add(root);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function mount(hydrate, render, root, vnode) {
|
|
989
|
+
// See mountSource(): an SSR-failed placeholder has no server DOM to adopt.
|
|
990
|
+
const ssrFailed = root.getAttribute && root.getAttribute('data-pnext-ssr-failed') != null;
|
|
991
|
+
const apply = root.hasChildNodes() && !ssrFailed ? hydrate : render;
|
|
992
|
+
apply(vnode, root);
|
|
993
|
+
// Stamp the root with this entry's render so the router can preserve it
|
|
994
|
+
// across a soft navigation and the next entry can tell whether its own
|
|
995
|
+
// preact instance owns the live tree.
|
|
996
|
+
root.__pnextLive = render;
|
|
997
|
+
pnextRender = render;
|
|
998
|
+
pnextMountedRoots.add(root);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// Adopted DOM children win (element children); plain children travel inside
|
|
1002
|
+
// the serialized props instead, and an explicit undefined third argument would
|
|
1003
|
+
// clobber props.children in preact's createElement — so only pass it when set.
|
|
1004
|
+
function islandVNode(h, Component, props, adopted) {
|
|
1005
|
+
return adopted !== undefined ? h(Component, props, adopted) : h(Component, props);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
async function staticChildren(h, root, id) {
|
|
1009
|
+
const node = root.querySelector(\`[${ISLAND_STATIC_CHILDREN_ATTRIBUTE}="\${id}"]\`);
|
|
1010
|
+
if (!node) return undefined;
|
|
1011
|
+
// A fallback <template> sidecar (children the component did not render on
|
|
1012
|
+
// the server): materialize any island markers inside its inert content,
|
|
1013
|
+
// adopt, then drop the sidecar so the mount starts from a clean host.
|
|
1014
|
+
const template = node.localName === 'template';
|
|
1015
|
+
const source = template ? node.content : node;
|
|
1016
|
+
if (template && window.__PNEXT_MATERIALIZE_CLIENT_ISLANDS__) {
|
|
1017
|
+
window.__PNEXT_MATERIALIZE_CLIENT_ISLANDS__(source);
|
|
1018
|
+
}
|
|
1019
|
+
const children = await domChildren(h, source);
|
|
1020
|
+
if (template) node.remove();
|
|
1021
|
+
return h('pnext-static-children', {
|
|
1022
|
+
${JSON.stringify(ISLAND_STATIC_CHILDREN_ATTRIBUTE)}: id,
|
|
1023
|
+
style: { display: 'contents' },
|
|
1024
|
+
}, children);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
async function domChildren(h, node) {
|
|
1028
|
+
const children = (await Promise.all(Array.from(node.childNodes, child => domNode(h, child))))
|
|
1029
|
+
.filter(child => child !== null && child !== undefined && child !== false);
|
|
1030
|
+
if (children.length === 0) return undefined;
|
|
1031
|
+
return children.length === 1 ? children[0] : children;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
async function domNode(h, node) {
|
|
1035
|
+
if (node.nodeType === 3) return node.textContent;
|
|
1036
|
+
if (node.nodeType !== 1) return null;
|
|
1037
|
+
|
|
1038
|
+
const element = node;
|
|
1039
|
+
if (element.localName === ${JSON.stringify(ISLAND_BOUNDARY_ERROR_ELEMENT)}) return pnextBoundaryErrorVNode(h, element);
|
|
1040
|
+
if (element.localName === 'pnext-client') {
|
|
1041
|
+
const id = element.getAttribute('data-pnext-client');
|
|
1042
|
+
const island = islands.find(candidate => candidate.id === id);
|
|
1043
|
+
if (!island) return h(element.localName, domProps(element), await domChildren(h, element));
|
|
1044
|
+
const Component = island.Component ?? await island.load();
|
|
1045
|
+
const rawProps = element.getAttribute('data-pnext-props') ?? '{}';
|
|
1046
|
+
const vnode = islandVNode(h, Component, await islandProps(rawProps, element, node => domChildren(h, node)), await staticChildren(h, element, island.id));
|
|
1047
|
+
return ${nextCompat ? 'islandBoundary ? h(islandBoundary, { fallback: null }, pnextClientBoundary(h, vnode)) : pnextClientBoundary(h, vnode)' : 'vnode'};
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
return h(element.localName, domProps(element), await domChildren(h, element));
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function pnextBoundaryErrorVNode(h, element) {
|
|
1054
|
+
const message = element.getAttribute(${JSON.stringify(ISLAND_BOUNDARY_ERROR_MESSAGE_ATTRIBUTE)}) ?? '';
|
|
1055
|
+
const digest = element.getAttribute(${JSON.stringify(ISLAND_BOUNDARY_ERROR_DIGEST_ATTRIBUTE)});
|
|
1056
|
+
return h(function PnextBoundaryError() {
|
|
1057
|
+
const error = new Error(message);
|
|
1058
|
+
if (digest) error.digest = digest;
|
|
1059
|
+
throw error;
|
|
1060
|
+
}, null);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function domProps(element) {
|
|
1064
|
+
const props = {};
|
|
1065
|
+
for (const attribute of element.attributes) props[attribute.name] = attribute.value;
|
|
1066
|
+
return props;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function isNestedIslandRoot(root) {
|
|
1070
|
+
return root.parentElement?.closest('pnext-client') != null;
|
|
1071
|
+
}
|
|
1072
|
+
`;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function visibleDynamicLoader(reference: ClientReference, href?: string) {
|
|
1076
|
+
return `options: ${JSON.stringify(reference.dynamic ?? {})}, ${dynamicLoader(reference, href)}`;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function dynamicLoader(reference: ClientReference, href?: string) {
|
|
1080
|
+
const imported =
|
|
1081
|
+
reference.exportName === 'default'
|
|
1082
|
+
? 'module.default'
|
|
1083
|
+
: `module.${reference.exportName}`;
|
|
1084
|
+
const importExpression = `import(${JSON.stringify(href ?? reference.file)}).then(module => ${imported})`;
|
|
1085
|
+
const cssHref = referenceCssHref(reference);
|
|
1086
|
+
return cssHref
|
|
1087
|
+
? `load: () => Promise.all([${importExpression}, loadIslandCss(${JSON.stringify(cssHref)})]).then(([module]) => module)`
|
|
1088
|
+
: `load: () => ${importExpression}`;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function referenceCssHref(reference: ClientReference) {
|
|
1092
|
+
return reference.cssImports?.length ? `/assets/${reference.id}.css` : undefined;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// The island's stylesheet loads with the island, not with the route: a
|
|
1096
|
+
// non-SSR island renders nothing until it mounts, so its CSS would only
|
|
1097
|
+
// penalize first paint. Resolves on error too — a missing stylesheet should
|
|
1098
|
+
// degrade styling, not block the component forever.
|
|
1099
|
+
function islandCssHelperSource() {
|
|
1100
|
+
return `
|
|
1101
|
+
export function loadIslandCss(href) {
|
|
1102
|
+
if (document.querySelector('link[href="' + href + '"]')) return Promise.resolve();
|
|
1103
|
+
return new Promise(resolve => {
|
|
1104
|
+
const link = document.createElement('link');
|
|
1105
|
+
link.rel = 'stylesheet';
|
|
1106
|
+
link.href = href;
|
|
1107
|
+
link.onload = () => resolve();
|
|
1108
|
+
link.onerror = () => resolve();
|
|
1109
|
+
document.head.append(link);
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
`;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function mountSource() {
|
|
1116
|
+
return `
|
|
1117
|
+
function mount(root, vnode) {
|
|
1118
|
+
// An island whose SSR stringification threw was served as an empty
|
|
1119
|
+
// pnext-client placeholder (data-pnext-ssr-failed) with no server DOM to adopt;
|
|
1120
|
+
// render it fresh rather than hydrating against nothing.
|
|
1121
|
+
const ssrFailed = root.getAttribute && root.getAttribute('data-pnext-ssr-failed') != null;
|
|
1122
|
+
const apply = root.hasChildNodes() && !ssrFailed ? hydrate : render;
|
|
1123
|
+
apply(vnode, root);
|
|
1124
|
+
// Stamp the root with this entry's render so the router can preserve it
|
|
1125
|
+
// across a soft navigation and the next entry can tell whether its own
|
|
1126
|
+
// preact instance owns the live tree (dev bundles carry separate copies).
|
|
1127
|
+
root.__pnextLive = render;
|
|
1128
|
+
pnextMountedRoots.add(root);
|
|
1129
|
+
}
|
|
1130
|
+
`;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function hydratePageSource(nextCompat?: boolean) {
|
|
1134
|
+
return `
|
|
1135
|
+
async function hydratePageAfterIslands() {
|
|
1136
|
+
await pnextIslandMounts;
|
|
1137
|
+
const root = document.getElementById('pnext-page');
|
|
1138
|
+
if (root?.closest('pnext-client')) return;
|
|
1139
|
+
hydratePage();
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function pageProps() {
|
|
1143
|
+
const rawProps = window.__PNEXT_PROPS__ ?? {};
|
|
1144
|
+
// Pre-fulfilled promises (status/value readable synchronously): use() must
|
|
1145
|
+
// not suspend during page hydration or preact appends fragment siblings
|
|
1146
|
+
// instead of reusing the server DOM.
|
|
1147
|
+
const fulfilled = value => {
|
|
1148
|
+
const promise = Promise.resolve(value);
|
|
1149
|
+
promise.status = 'fulfilled';
|
|
1150
|
+
promise.value = value;
|
|
1151
|
+
return promise;
|
|
1152
|
+
};
|
|
1153
|
+
return {
|
|
1154
|
+
...rawProps,
|
|
1155
|
+
params: fulfilled(rawProps.params ?? {}),
|
|
1156
|
+
searchParams: fulfilled(
|
|
1157
|
+
rawProps.searchParams ?? Object.fromEntries(new URLSearchParams(location.search)),
|
|
1158
|
+
),
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function hydratePage() {
|
|
1163
|
+
// SSR-error recovery: when the server errored and served the error-boundary
|
|
1164
|
+
// document, hydrating the real Page against that error HTML corrupts the tree.
|
|
1165
|
+
// The server stamps data-pnext-ssr-error on <html>; skip page hydration and let
|
|
1166
|
+
// the client error runtime (installClientErrors) own the tree instead.
|
|
1167
|
+
if (document.documentElement.hasAttribute('data-pnext-ssr-error')) return;
|
|
1168
|
+
// A refresh of this same route grafts the live page DOM back in (the router's
|
|
1169
|
+
// client-page preservation), so its tree is still mounted — re-hydrating over
|
|
1170
|
+
// it would blow away the state the graft exists to keep.
|
|
1171
|
+
const live = window.__PNEXT_CLIENT_PAGE_ROOT__;
|
|
1172
|
+
if (live && pnextMountedRoots.has(live)) return;
|
|
1173
|
+
const root = pageMountRoot();
|
|
1174
|
+
if (!root) return;
|
|
1175
|
+
// Published for the router: it needs the mount container (and, for a dissolved
|
|
1176
|
+
// page slot, its anchors) to preserve this page across a refresh.
|
|
1177
|
+
window.__PNEXT_CLIENT_PAGE_ROOT__ = root;
|
|
1178
|
+
mount(root, ${wrapInBoundary('h(Page, pageProps())', nextCompat)});
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// Next renders a page's children directly into the layout with no wrapper element, so tests assert
|
|
1182
|
+
// structural selectors against the live DOM. The island bootstrap rebuilt div#pnext-page from the SSR
|
|
1183
|
+
// comment markers; a client page mounted here owns that wrapper exclusively, so dissolve it back into
|
|
1184
|
+
// comment anchors and hydrate against a virtual container whose children are the marker range.
|
|
1185
|
+
// Nested/segment contexts keep the real element: island adoption walks it and segment grafting
|
|
1186
|
+
// queries #pnext-page.
|
|
1187
|
+
function pageMountRoot() {
|
|
1188
|
+
const root = document.getElementById('pnext-page');
|
|
1189
|
+
if (!root || !root.parentNode) return root;
|
|
1190
|
+
if (root.closest('pnext-client') || root.closest('pnext-layout[data-pnext-segment]')) return root;
|
|
1191
|
+
const parent = root.parentNode;
|
|
1192
|
+
let attrs = '';
|
|
1193
|
+
for (const attribute of root.attributes) attrs += ' ' + attribute.name + '="' + attribute.value + '"';
|
|
1194
|
+
// Same encoding the island materializers parse (entity-escaped attribute
|
|
1195
|
+
// text, see renderer encodeIslandMarker) — a bfcache/entry snapshot of this
|
|
1196
|
+
// dissolved marker must re-materialize into a real #pnext-page wrapper.
|
|
1197
|
+
const encoded = attrs.replace(/</g, '<').replace(/>/g, '>');
|
|
1198
|
+
const open = document.createComment('pnext-page:' + encoded);
|
|
1199
|
+
const close = document.createComment('/pnext-page');
|
|
1200
|
+
parent.insertBefore(open, root);
|
|
1201
|
+
while (root.firstChild) parent.insertBefore(root.firstChild, root);
|
|
1202
|
+
parent.replaceChild(close, root);
|
|
1203
|
+
const container = pageMarkerContainer(open, close, parent);
|
|
1204
|
+
// The router preserves this range across a refresh; it needs the anchors.
|
|
1205
|
+
container.__pnextAnchors = [open, close];
|
|
1206
|
+
return container;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// The minimal parent-DOM surface preact's diff touches on a render container
|
|
1210
|
+
// (namespaceURI/firstChild/childNodes/insertBefore), scoped to the nodes
|
|
1211
|
+
// between the two page anchors; child removal goes through each node's own
|
|
1212
|
+
// parentNode. contains/closest serve the entry's own keep/nesting checks.
|
|
1213
|
+
function pageMarkerContainer(open, close, parent) {
|
|
1214
|
+
return {
|
|
1215
|
+
nodeType: 1,
|
|
1216
|
+
namespaceURI: parent.namespaceURI,
|
|
1217
|
+
get firstChild() {
|
|
1218
|
+
const node = open.nextSibling;
|
|
1219
|
+
return node === close ? null : node;
|
|
1220
|
+
},
|
|
1221
|
+
get childNodes() {
|
|
1222
|
+
const nodes = [];
|
|
1223
|
+
for (let node = open.nextSibling; node && node !== close; node = node.nextSibling) nodes.push(node);
|
|
1224
|
+
return nodes;
|
|
1225
|
+
},
|
|
1226
|
+
hasChildNodes() { return open.nextSibling !== close; },
|
|
1227
|
+
insertBefore(node, reference) { return parent.insertBefore(node, reference ?? close); },
|
|
1228
|
+
appendChild(node) { return parent.insertBefore(node, close); },
|
|
1229
|
+
removeChild(node) { return parent.removeChild(node); },
|
|
1230
|
+
contains(node) {
|
|
1231
|
+
for (let current = open.nextSibling; current && current !== close; current = current.nextSibling) {
|
|
1232
|
+
if (current === node || (current.contains && current.contains(node))) return true;
|
|
1233
|
+
}
|
|
1234
|
+
return false;
|
|
1235
|
+
},
|
|
1236
|
+
closest(selector) { return parent.closest ? parent.closest(selector) : null; },
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
`;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// Under next compat every hydrated island and the page mount wraps in the in-tree
|
|
1243
|
+
// ClientErrorBoundary, NESTED INSIDE the implicit Suspense boundary islands must keep (a bare lazy()
|
|
1244
|
+
// without Suspense kills the Preact tree). The boundary renders the route error.js and reset() soft-
|
|
1245
|
+
// refreshes.
|
|
1246
|
+
function wrapInBoundary(vnodeExpression: string, nextCompat?: boolean) {
|
|
1247
|
+
return nextCompat ? `pnextBoundary(${vnodeExpression})` : vnodeExpression;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// Emits the per-route boundary helper referenced by wrapInBoundary. The Suspense wrapper is
|
|
1251
|
+
// unconditional under compat - dropping it kills the tree. The error boundary inside it is emitted
|
|
1252
|
+
// only for an app that has an error.*/not-found.* for it to render: with none it would just re-throw,
|
|
1253
|
+
// and the deferred window last resort already owns that path.
|
|
1254
|
+
function clientBoundaryHelperSource(
|
|
1255
|
+
nextCompat?: boolean,
|
|
1256
|
+
boundary?: boolean,
|
|
1257
|
+
builtinNotFound?: boolean,
|
|
1258
|
+
) {
|
|
1259
|
+
if (!nextCompat) return '';
|
|
1260
|
+
const inner = boundary
|
|
1261
|
+
? `h(ClientErrorBoundary, {
|
|
1262
|
+
errorComponent: config.errorComponent,
|
|
1263
|
+
notFoundComponent: config.notFoundComponent,${
|
|
1264
|
+
builtinNotFound ? '\n builtInNotFound: __PNextBuiltInNotFound,' : ''
|
|
1265
|
+
}
|
|
1266
|
+
onReset: softRefreshRoute,
|
|
1267
|
+
}, vnode)`
|
|
1268
|
+
: 'vnode';
|
|
1269
|
+
return `
|
|
1270
|
+
function pnextBoundary(vnode) {
|
|
1271
|
+
return h(Suspense, { fallback: null }, ${inner});
|
|
1272
|
+
}
|
|
1273
|
+
`;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function hydrateIslandsSource({ nextCompat, visibleIslands, deferredIslands }: ClientRuntimeFacts) {
|
|
1277
|
+
return `
|
|
1278
|
+
const islandById = new Map(islands.map(island => [island.id, island]));
|
|
1279
|
+
|
|
1280
|
+
// The mounted count is known as soon as the scan loop finishes, before any
|
|
1281
|
+
// island module resolves — so the client shell can decide whether it owns the
|
|
1282
|
+
// document without waiting behind island dynamic imports.
|
|
1283
|
+
function scanIslands() {
|
|
1284
|
+
const pending = [];
|
|
1285
|
+
let mounted = 0;${
|
|
1286
|
+
// __PNEXT_ACTIONS__ is installed by the action client, which only a next
|
|
1287
|
+
// compat entry ever imports — the lookup is dead code anywhere else.
|
|
1288
|
+
nextCompat
|
|
1289
|
+
? `
|
|
1290
|
+
for (const island of islands) {
|
|
1291
|
+
if (island.Component) window.__PNEXT_ACTIONS__?.registerClientReference(island.id, island.Component);
|
|
1292
|
+
}`
|
|
1293
|
+
: ''
|
|
1294
|
+
}
|
|
1295
|
+
for (const island of islands) {
|
|
1296
|
+
for (const root of document.querySelectorAll(\`[data-pnext-client="\${island.id}"]\`)) {
|
|
1297
|
+
if (isNestedIslandRoot(root)) continue;
|
|
1298
|
+
// Already mounted (e.g. a re-scan after a streamed Suspense hole materialized new islands):
|
|
1299
|
+
// mount() stamps __pnextLive, so skipping these leaves the shell's live islands untouched. A root
|
|
1300
|
+
// preserved across a soft navigation is ALSO live, but carries the incoming placeholder (fresh SSR
|
|
1301
|
+
// props/children) the router stashed - it must re-render, not be skipped, or it keeps showing the
|
|
1302
|
+
// previous document's props.
|
|
1303
|
+
if (root.__pnextLive && !root.__pnextIncoming) continue;
|
|
1304
|
+
mounted += 1;${
|
|
1305
|
+
visibleIslands
|
|
1306
|
+
? `
|
|
1307
|
+
// A preserved root (router grafted its live DOM across the soft nav) must
|
|
1308
|
+
// re-render immediately — its DOM still shows the previous route's children.
|
|
1309
|
+
if (island.load && island.options?.load === 'visible' && !root.__pnextIncoming) {
|
|
1310
|
+
observeVisibleIsland(root, island);
|
|
1311
|
+
continue;
|
|
1312
|
+
}`
|
|
1313
|
+
: ''
|
|
1314
|
+
}
|
|
1315
|
+
pending.push(mountIsland(root, island));
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
return { mounted, done: Promise.all(pending) };
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
function mountIsland(root, island) {
|
|
1322
|
+
return mountIslandOnce(root, () => mountIslandTree(root, island));
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
async function mountIslandTree(root, island) {
|
|
1326
|
+
const Component = island.Component ?? await island.load();${
|
|
1327
|
+
nextCompat ? `
|
|
1328
|
+
window.__PNEXT_ACTIONS__?.registerClientReference(island.id, Component);` : ''
|
|
1329
|
+
}
|
|
1330
|
+
const rawProps = root.getAttribute('data-pnext-props') ?? '{}';
|
|
1331
|
+
const incoming = root.__pnextIncoming;
|
|
1332
|
+
root.__pnextIncoming = undefined;
|
|
1333
|
+
if (incoming && root.__pnextLive === render) {
|
|
1334
|
+
// Preserved across a soft navigation: re-render in place with the incoming
|
|
1335
|
+
// document's props/children so component state survives while the routed
|
|
1336
|
+
// content under the island updates.
|
|
1337
|
+
const vnode = ${islandVNodeExpr('Component', 'await islandProps(rawProps, incoming, domChildren)', 'await staticChildren(incoming, island.id)', 'root', nextCompat)};
|
|
1338
|
+
render(${wrapInBoundary('vnode', nextCompat)}, root);
|
|
1339
|
+
pnextMountedRoots.add(root);
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
if (incoming) {
|
|
1343
|
+
// Preserved root owned by a foreign entry copy (dev bundles): this preact
|
|
1344
|
+
// instance can't diff its tree — restore the SSR children and mount fresh.
|
|
1345
|
+
root.replaceChildren(...incoming.childNodes);
|
|
1346
|
+
root.__pnextLive = undefined;
|
|
1347
|
+
}
|
|
1348
|
+
const vnode = ${islandVNodeExpr('Component', 'await islandProps(rawProps, root, domChildren)', 'await staticChildren(root, island.id)', 'root', nextCompat)};
|
|
1349
|
+
mount(root, ${wrapInBoundary('vnode', nextCompat)});
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
${
|
|
1353
|
+
visibleIslands
|
|
1354
|
+
? `function observeVisibleIsland(root, island) {
|
|
1355
|
+
if (typeof IntersectionObserver === 'undefined') {
|
|
1356
|
+
void mountIsland(root, island);
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
const observer = new IntersectionObserver(entries => {
|
|
1361
|
+
if (!entries.some(entry => entry.isIntersecting)) return;
|
|
1362
|
+
observer.disconnect();
|
|
1363
|
+
void mountIsland(root, island);
|
|
1364
|
+
}, island.options);
|
|
1365
|
+
|
|
1366
|
+
observer.observe(visibleTarget(root));
|
|
1367
|
+
}
|
|
1368
|
+
`
|
|
1369
|
+
: ''
|
|
1370
|
+
}
|
|
1371
|
+
async function staticChildren(root, id) {
|
|
1372
|
+
const node = root.querySelector(\`[${ISLAND_STATIC_CHILDREN_ATTRIBUTE}="\${id}"]\`);
|
|
1373
|
+
if (!node) return undefined;
|
|
1374
|
+
// A fallback <template> sidecar (children the component did not render on
|
|
1375
|
+
// the server): materialize any island markers inside its inert content,
|
|
1376
|
+
// adopt, then drop the sidecar so the mount starts from a clean host.
|
|
1377
|
+
const template = node.localName === 'template';
|
|
1378
|
+
const source = template ? node.content : node;
|
|
1379
|
+
if (template && window.__PNEXT_MATERIALIZE_CLIENT_ISLANDS__) {
|
|
1380
|
+
window.__PNEXT_MATERIALIZE_CLIENT_ISLANDS__(source);
|
|
1381
|
+
}
|
|
1382
|
+
const children = await domChildren(source);
|
|
1383
|
+
if (Page) {
|
|
1384
|
+
const pageRoot = source.querySelector?.('#pnext-page');
|
|
1385
|
+
if (pageRoot?.parentNode) pageRoot.replaceWith(...pageRoot.childNodes);
|
|
1386
|
+
}
|
|
1387
|
+
if (template) node.remove();
|
|
1388
|
+
return h('pnext-static-children', {
|
|
1389
|
+
${JSON.stringify(ISLAND_STATIC_CHILDREN_ATTRIBUTE)}: id,
|
|
1390
|
+
style: { display: 'contents' },
|
|
1391
|
+
}, children);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
async function domChildren(node) {
|
|
1395
|
+
const children = (await Promise.all(Array.from(node.childNodes, domNode)))
|
|
1396
|
+
.filter(child => child !== null && child !== undefined && child !== false);
|
|
1397
|
+
if (children.length === 0) return undefined;
|
|
1398
|
+
return children.length === 1 ? children[0] : children;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
async function domNode(node) {
|
|
1402
|
+
if (node.nodeType === 3) return node.textContent;
|
|
1403
|
+
if (node.nodeType !== 1) return null;
|
|
1404
|
+
|
|
1405
|
+
const element = node;
|
|
1406
|
+
if (element.localName === ${JSON.stringify(ISLAND_BOUNDARY_ERROR_ELEMENT)}) return pnextBoundaryErrorVNode(element);
|
|
1407
|
+
if (element.localName === 'pnext-client') {
|
|
1408
|
+
const id = element.getAttribute('data-pnext-client');
|
|
1409
|
+
const island = id ? islandById.get(id) : undefined;
|
|
1410
|
+
if (!island) return h(element.localName, domProps(element), await domChildren(element));
|
|
1411
|
+
const rawProps = element.getAttribute('data-pnext-props') ?? '{}';
|
|
1412
|
+
const children = await staticChildren(element, island.id);${
|
|
1413
|
+
deferredIslands
|
|
1414
|
+
? `
|
|
1415
|
+
if (!island.Component && island.options?.ssr === false) {
|
|
1416
|
+
return h(DeferredIsland, { load: island.load, props: parseIslandProps(rawProps), children });
|
|
1417
|
+
}`
|
|
1418
|
+
: ''
|
|
1419
|
+
}
|
|
1420
|
+
const Component = island.Component ?? await island.load();
|
|
1421
|
+
return ${wrapInBoundary(islandVNodeExpr('Component', 'await islandProps(rawProps, element, domChildren)', 'children', 'element', nextCompat), nextCompat)};
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
if (Page && element.id === 'pnext-page') {
|
|
1425
|
+
return ${wrapInBoundary('h(Page, pageProps())', nextCompat)};
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
return h(element.localName, domProps(element), await domChildren(element));
|
|
1429
|
+
}
|
|
1430
|
+
`;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// Helpers with no per-route state: they live once in the shared runtime module,
|
|
1434
|
+
// not inside bootstrapRoute.
|
|
1435
|
+
function islandHelpersSource({ deferredIslands, visibleIslands }: ClientRuntimeFacts) {
|
|
1436
|
+
return `
|
|
1437
|
+
function pnextBoundaryErrorVNode(element) {
|
|
1438
|
+
const message = element.getAttribute(${JSON.stringify(ISLAND_BOUNDARY_ERROR_MESSAGE_ATTRIBUTE)}) ?? '';
|
|
1439
|
+
const digest = element.getAttribute(${JSON.stringify(ISLAND_BOUNDARY_ERROR_DIGEST_ATTRIBUTE)});
|
|
1440
|
+
return h(function PnextBoundaryError() {
|
|
1441
|
+
const error = new Error(message);
|
|
1442
|
+
if (digest) error.digest = digest;
|
|
1443
|
+
throw error;
|
|
1444
|
+
}, null);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
function domProps(element) {
|
|
1448
|
+
const props = {};
|
|
1449
|
+
for (const attribute of element.attributes) props[attribute.name] = attribute.value;
|
|
1450
|
+
return props;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
function isNestedIslandRoot(root) {
|
|
1454
|
+
return root.parentElement?.closest('pnext-client') != null;
|
|
1455
|
+
}
|
|
1456
|
+
${
|
|
1457
|
+
visibleIslands
|
|
1458
|
+
? `
|
|
1459
|
+
function visibleTarget(root) {
|
|
1460
|
+
const rect = root.getBoundingClientRect();
|
|
1461
|
+
if (rect.width || rect.height) return root;
|
|
1462
|
+
return root.parentElement ?? root;
|
|
1463
|
+
}
|
|
1464
|
+
`
|
|
1465
|
+
: ''
|
|
1466
|
+
}${mountOnceSource()}${
|
|
1467
|
+
deferredIslands
|
|
1468
|
+
? `
|
|
1469
|
+
function DeferredIsland({ load, props, children }) {
|
|
1470
|
+
const [Component, setComponent] = useState(null);
|
|
1471
|
+
useEffect(() => {
|
|
1472
|
+
let cancelled = false;
|
|
1473
|
+
void load().then(loaded => {
|
|
1474
|
+
if (!cancelled) setComponent(() => loaded);
|
|
1475
|
+
});
|
|
1476
|
+
return () => {
|
|
1477
|
+
cancelled = true;
|
|
1478
|
+
};
|
|
1479
|
+
}, [load]);
|
|
1480
|
+
return Component ? h(Component, props, children) : null;
|
|
1481
|
+
}
|
|
1482
|
+
`
|
|
1483
|
+
: ''
|
|
1484
|
+
}`;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
function hydrateClientShellSource(nextCompat?: boolean) {
|
|
1488
|
+
// The outermost component (index 0) is the client root layout: hand it the
|
|
1489
|
+
// adopted parallel-route slot props; inner layouts take bare children.
|
|
1490
|
+
const shell = `shell.reduceRight(
|
|
1491
|
+
(children, Component, index) => h(Component, index === 0 ? slotProps : null, children),
|
|
1492
|
+
page,
|
|
1493
|
+
)`;
|
|
1494
|
+
|
|
1495
|
+
return `
|
|
1496
|
+
// A client root layout is re-rendered whole here, but its parallel-route slot
|
|
1497
|
+
// props (nav/auth/…) are server-rendered subtrees that never serialized. Adopt
|
|
1498
|
+
// each slot's preserved SSR DOM (marked with data-pnext-slot by the renderer)
|
|
1499
|
+
// as the named prop on the outermost shell component so the slots survive.
|
|
1500
|
+
async function collectRootSlotProps() {
|
|
1501
|
+
const props = {};
|
|
1502
|
+
for (const el of document.querySelectorAll('[data-pnext-slot]')) {
|
|
1503
|
+
if (el.parentElement?.closest('[data-pnext-slot]')) continue;
|
|
1504
|
+
const name = el.getAttribute('data-pnext-slot');
|
|
1505
|
+
if (!name) continue;
|
|
1506
|
+
props[name] = await domNode(el);
|
|
1507
|
+
}
|
|
1508
|
+
return props;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
async function hydrateClientShell() {
|
|
1512
|
+
if (pnextIslandScan && pnextIslandScan.mounted > 0) return;
|
|
1513
|
+
// The streamed-chunk remount (remountRoute) re-runs every mount step; the
|
|
1514
|
+
// shell owns document.body, so re-rendering a shell that already hydrated
|
|
1515
|
+
// would blow away the live tree. mount() registers its root, and a soft
|
|
1516
|
+
// navigation clears the set, so this only skips a shell that is still live.
|
|
1517
|
+
if (pnextMountedRoots.has(document.body)) return;
|
|
1518
|
+
const pageRoot = document.getElementById('pnext-page');
|
|
1519
|
+
if (!pageRoot) return;
|
|
1520
|
+
const page = await domNode(pageRoot);
|
|
1521
|
+
const slotProps = await collectRootSlotProps();
|
|
1522
|
+
mount(document.body, ${wrapInBoundary(shell, nextCompat)});
|
|
1523
|
+
}
|
|
1524
|
+
`;
|
|
1525
|
+
}
|