@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,21 @@
|
|
|
1
|
+
// next/image static-import extension registration (COMPAT - may import core).
|
|
2
|
+
//
|
|
3
|
+
// Registers the Next static-image module producer into core's `staticAssetModule` seam so
|
|
4
|
+
// `import img from './pic.png'` in the server module pipeline evaluates to the full
|
|
5
|
+
// `{src,width,height,blurDataURL,blurWidth,blurHeight}` descriptor next/image consumes, with the asset
|
|
6
|
+
// emitted under `/_next/static/media/<name>.<hash>.<ext>`.
|
|
7
|
+
//
|
|
8
|
+
// Safe in a pure-core app: the seam only overrides core's generic asset module for recognized image
|
|
9
|
+
// extensions; everything else falls through.
|
|
10
|
+
|
|
11
|
+
import { setAssetExtensions } from '../../extensions';
|
|
12
|
+
import { staticImageModule } from '../next/image/static-metadata';
|
|
13
|
+
|
|
14
|
+
export function registerStaticImageExtensions(): void {
|
|
15
|
+
setAssetExtensions({
|
|
16
|
+
staticAssetPublicPrefixes: () => ['/__pnext/static/media/', '/_next/static/media/'],
|
|
17
|
+
staticAssetRelativePath: ({ base, hash, ext }) =>
|
|
18
|
+
['_next', 'static', 'media', `${base}.${hash}${ext}`].join('/'),
|
|
19
|
+
staticAssetModule: staticImageModule,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Typed-routes codegen registration (COMPAT - may import core freely). Wires a build step into
|
|
2
|
+
// buildExtensions.steps that emits Next-shaped route type definitions under
|
|
3
|
+
// `<root>/.next/types/{routes.d.ts,link.d.ts,validator.ts}`. Runs during a build under compat.next;
|
|
4
|
+
// pure-core apps register nothing.
|
|
5
|
+
|
|
6
|
+
import type { ResolvedConfig } from '../../config';
|
|
7
|
+
import {
|
|
8
|
+
registerBuildParallelPhases,
|
|
9
|
+
registerBuildSteps,
|
|
10
|
+
type BuildStepContext,
|
|
11
|
+
} from '../../extensions';
|
|
12
|
+
import type { RouteManifestEntry } from '../../types';
|
|
13
|
+
import { nextCompatEnabled } from '../index';
|
|
14
|
+
import { writeTsconfigDefaults } from '../tsconfig-defaults';
|
|
15
|
+
import { typecheckDisabled, validateTypesOffThread } from '../typecheck';
|
|
16
|
+
import { generateTypedRoutes } from '../typed-routes';
|
|
17
|
+
|
|
18
|
+
export function registerTypedRoutesExtensions(_config: ResolvedConfig): void {
|
|
19
|
+
registerBuildSteps(runTypedRoutesBuildStep);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function runTypedRoutesBuildStep(ctx: BuildStepContext): Promise<void> {
|
|
23
|
+
if (!nextCompatEnabled(ctx.config)) return;
|
|
24
|
+
await writeTsconfigDefaults(ctx.config);
|
|
25
|
+
await generateTypedRoutes(ctx.config, ctx.routes as RouteManifestEntry[]);
|
|
26
|
+
// The typecheck reads only files that already exist on disk, so it runs on a worker thread
|
|
27
|
+
// alongside client bundling and prerendering rather than ahead of them; the CLI awaits it before
|
|
28
|
+
// the summary. A build that disables it reports no line at all.
|
|
29
|
+
if (typecheckDisabled()) return;
|
|
30
|
+
const startedAt = performance.now();
|
|
31
|
+
registerBuildParallelPhases({
|
|
32
|
+
name: 'Typecheck',
|
|
33
|
+
run: validateTypesOffThread(ctx.config).then(() => performance.now() - startedAt),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// `use cache` response-header registration (COMPAT - may import core freely).
|
|
2
|
+
//
|
|
3
|
+
// Wires the SWR cache-control headers Next emits for a `use cache` route into core via a
|
|
4
|
+
// responseFinalizer: `cache-control: s-maxage=<revalidate>, stale-while-revalidate=<expire-revalidate>`
|
|
5
|
+
// and `x-nextjs-stale-time: <stale>`.
|
|
6
|
+
//
|
|
7
|
+
// Mechanism mirrors the font runtime: as each `use cache` entry resolves during a render it stashes its
|
|
8
|
+
// effective cacheLife on the request work unit. This finalizer, running before the first flush inside the
|
|
9
|
+
// same work unit, reads the aggregate and sets the headers. A user-supplied Cache-Control survives - the
|
|
10
|
+
// header is only set when absent, matching the "merge, do not overwrite" finalizer discipline.
|
|
11
|
+
//
|
|
12
|
+
// LIMITATION: statically-served prerendered pages (a cache HIT from prebuilt bytes) do not re-run a
|
|
13
|
+
// render, so no cacheLife is stashed for them - the header is emitted only on dynamic renders and
|
|
14
|
+
// lazy-static MISS responses. Emitting it for pure static HITs needs the route's cacheLife persisted in
|
|
15
|
+
// the build manifest and read by the static-serve path in src/cli/start.ts.
|
|
16
|
+
|
|
17
|
+
import type { ResolvedConfig } from '../../config';
|
|
18
|
+
import { nextCompatEnabled } from '../../compat';
|
|
19
|
+
import {
|
|
20
|
+
onExtensionHostReset,
|
|
21
|
+
registerBuildCompleteHooks,
|
|
22
|
+
registerBuildSteps,
|
|
23
|
+
registerResponseFinalizers,
|
|
24
|
+
type ResponseFinalizer,
|
|
25
|
+
type ResponseFinalizerContext,
|
|
26
|
+
} from '../../extensions';
|
|
27
|
+
import {
|
|
28
|
+
setUseCacheFillTimeout,
|
|
29
|
+
setUseCacheRootParamNames,
|
|
30
|
+
takeCacheLifeStash,
|
|
31
|
+
} from '../cache/use-cache';
|
|
32
|
+
import { scanRootParams } from '../ppr/root-params-scan';
|
|
33
|
+
import {
|
|
34
|
+
beginPrerenderErrorCollection,
|
|
35
|
+
endPrerenderErrorCollection,
|
|
36
|
+
} from '../cache/build-prerender-errors';
|
|
37
|
+
import { getNextConfig } from '../next/config-loader';
|
|
38
|
+
|
|
39
|
+
// Route kinds a `use cache` page/handler can be served as.
|
|
40
|
+
const CACHEABLE_ROUTE_KINDS = new Set<ResponseFinalizerContext['routeKind']>([
|
|
41
|
+
'html',
|
|
42
|
+
'data',
|
|
43
|
+
'route-handler',
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
const useCacheHeaderFinalizer: ResponseFinalizer = ctx => {
|
|
47
|
+
const life = takeCacheLifeStash();
|
|
48
|
+
if (!life) return;
|
|
49
|
+
if (!CACHEABLE_ROUTE_KINDS.has(ctx.routeKind)) return;
|
|
50
|
+
|
|
51
|
+
const { revalidateSeconds, expireSeconds, staleSeconds } = life;
|
|
52
|
+
|
|
53
|
+
// SWR cache-control: s-maxage = revalidate, stale-while-revalidate =
|
|
54
|
+
// expire − revalidate. Emitted only when a revalidate window is known and no
|
|
55
|
+
// explicit Cache-Control was already set by the app/route.
|
|
56
|
+
if (
|
|
57
|
+
revalidateSeconds !== undefined &&
|
|
58
|
+
expireSeconds !== undefined &&
|
|
59
|
+
!ctx.headers.has('cache-control')
|
|
60
|
+
) {
|
|
61
|
+
const swr = Math.max(0, expireSeconds - revalidateSeconds);
|
|
62
|
+
ctx.headers.set('cache-control', `s-maxage=${revalidateSeconds}, stale-while-revalidate=${swr}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// x-nextjs-stale-time drives the client router's cache staleness window.
|
|
66
|
+
// Set unconditionally: the segment finalizer runs earlier and stamps a
|
|
67
|
+
// mode-derived default, but the live render's own cacheLife is authoritative
|
|
68
|
+
// (the header is internal — never user-supplied — so overwriting is safe).
|
|
69
|
+
if (staleSeconds !== undefined) {
|
|
70
|
+
ctx.headers.set('x-nextjs-stale-time', String(staleSeconds));
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
let registered = false;
|
|
75
|
+
// Host-scoped guard: a fresh host must be registered into again.
|
|
76
|
+
onExtensionHostReset(() => (registered = false));
|
|
77
|
+
|
|
78
|
+
export function registerUseCacheExtensions(config: ResolvedConfig): void {
|
|
79
|
+
if (registered) return;
|
|
80
|
+
if (!nextCompatEnabled(config)) return;
|
|
81
|
+
registered = true;
|
|
82
|
+
registerResponseFinalizers(useCacheHeaderFinalizer);
|
|
83
|
+
|
|
84
|
+
// Seed the root-param NAME set for cache-key participation (Stage B-3): a
|
|
85
|
+
// `use cache` entry's conservative first-call key may only discriminate by
|
|
86
|
+
// root params, so the runtime needs the scanned names at build AND serve.
|
|
87
|
+
setUseCacheRootParamNames(scanRootParams(config.appPath));
|
|
88
|
+
|
|
89
|
+
// Arm `use cache` build-time validation for this build's prerender pass.
|
|
90
|
+
// Registered as a build STEP so it runs before any page is prerendered; the
|
|
91
|
+
// registry is process-global, so re-check enablement per build (a later
|
|
92
|
+
// pure-core buildProject in the same process must stay unarmed).
|
|
93
|
+
registerBuildSteps(ctx => {
|
|
94
|
+
if (nextCompatEnabled(ctx.config)) {
|
|
95
|
+
const config = getNextConfig() as {
|
|
96
|
+
experimental?: { useCacheTimeout?: number };
|
|
97
|
+
staticPageGenerationTimeout?: number;
|
|
98
|
+
};
|
|
99
|
+
const configured = config.experimental?.useCacheTimeout;
|
|
100
|
+
const staticTimeout = config.staticPageGenerationTimeout;
|
|
101
|
+
// Next clamps the build-time `use cache` fill timeout to just under the
|
|
102
|
+
// page-generation timeout (1s buffer before the build worker kills the
|
|
103
|
+
// page), so a slow fill fails via the clamp rather than the worker.
|
|
104
|
+
const baseMs = typeof configured === 'number' ? configured * 1000 : 50_000;
|
|
105
|
+
const clampMs =
|
|
106
|
+
typeof staticTimeout === 'number' && staticTimeout > 1
|
|
107
|
+
? (staticTimeout - 1) * 1000
|
|
108
|
+
: undefined;
|
|
109
|
+
const fillMs = clampMs !== undefined ? Math.min(baseMs, clampMs) : baseMs;
|
|
110
|
+
setUseCacheFillTimeout(fillMs);
|
|
111
|
+
beginPrerenderErrorCollection();
|
|
112
|
+
}
|
|
113
|
+
return Promise.resolve();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// After prerendering, surface any recorded `use cache` violations. Each is
|
|
117
|
+
// printed with Next's exact error text plus the "Error occurred prerendering
|
|
118
|
+
// page" line the e2e suites grep for, then the build is aborted (exit 1).
|
|
119
|
+
registerBuildCompleteHooks(ctx => {
|
|
120
|
+
const errors = endPrerenderErrorCollection();
|
|
121
|
+
if (!nextCompatEnabled(ctx.config) || errors.length === 0) return Promise.resolve();
|
|
122
|
+
// Messages were already printed at detection time (see recordPrerenderError);
|
|
123
|
+
// the hook only forces the non-zero exit for builds that otherwise completed
|
|
124
|
+
// (a swallowed hanging timeout or a caught searchParams access).
|
|
125
|
+
throw new Error(
|
|
126
|
+
`pnext build: ${errors.length} "use cache" validation error${
|
|
127
|
+
errors.length === 1 ? '' : 's'
|
|
128
|
+
} (see above)`,
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Build-time validation registration (COMPAT - may import core freely).
|
|
2
|
+
//
|
|
3
|
+
// Registers a single build step that runs the Next-compatible validation pass over the scanned route
|
|
4
|
+
// table. A validation failure throws PnextBuildValidationError, aborting the build with Next's exact
|
|
5
|
+
// error string on stderr, which the Next e2e suites substring-match.
|
|
6
|
+
//
|
|
7
|
+
// The step is ordered FIRST among build steps (registered before action discovery) so validation fails
|
|
8
|
+
// fast, before any bundling or prerender work. Registration is idempotent and no-op for pure-core apps.
|
|
9
|
+
|
|
10
|
+
import type { ResolvedConfig } from '../../config';
|
|
11
|
+
import { nextCompatEnabled } from '../../compat';
|
|
12
|
+
import {
|
|
13
|
+
onExtensionHostReset,
|
|
14
|
+
registerBuildSteps,
|
|
15
|
+
setBuildCompatExtensions,
|
|
16
|
+
type BuildStep,
|
|
17
|
+
} from '../../extensions';
|
|
18
|
+
import type { RouteManifestEntry } from '../../types';
|
|
19
|
+
import { writeBuildFlags } from '../cache/build-flags';
|
|
20
|
+
import { validateBuild } from '../validation';
|
|
21
|
+
import {
|
|
22
|
+
diagnoseCacheComponentsPrerender,
|
|
23
|
+
diagnosticLeadsWithErrorLine,
|
|
24
|
+
prerenderFailureFooter,
|
|
25
|
+
} from '../validation/prerender-diagnostics';
|
|
26
|
+
|
|
27
|
+
let registered = false;
|
|
28
|
+
// Host-scoped guard: a fresh host must be registered into again.
|
|
29
|
+
onExtensionHostReset(() => (registered = false));
|
|
30
|
+
|
|
31
|
+
export function registerValidationExtensions(config: ResolvedConfig): void {
|
|
32
|
+
if (registered) return;
|
|
33
|
+
if (!nextCompatEnabled(config)) return;
|
|
34
|
+
registered = true;
|
|
35
|
+
|
|
36
|
+
const step: BuildStep = async ctx => {
|
|
37
|
+
// Runtime compat gate: the extension registry is process-global, so once any compat.next build
|
|
38
|
+
// registers this step it stays registered for every later buildProject() in the same process -
|
|
39
|
+
// including pure-core fixtures that legitimately have no root layout. Re-check enablement per build so
|
|
40
|
+
// validation only runs for the app that actually enabled compat.next.
|
|
41
|
+
if (!nextCompatEnabled(ctx.config)) return;
|
|
42
|
+
await ctx.log.step('validate build', () =>
|
|
43
|
+
Promise.resolve(validateBuild(ctx.config, ctx.routes as RouteManifestEntry[])),
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
// Gate step: validation diagnostics must beat any error the invalid app would
|
|
47
|
+
// otherwise raise first from bundling or the not-found prerender.
|
|
48
|
+
step.gate = true;
|
|
49
|
+
registerBuildSteps(step);
|
|
50
|
+
setBuildCompatExtensions({
|
|
51
|
+
diagnoseCacheComponentsPrerender,
|
|
52
|
+
diagnosticLeadsWithErrorLine,
|
|
53
|
+
prerenderFailureFooter,
|
|
54
|
+
recordBuildFlags: writeBuildFlags,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// In-page loading-boundary detection (COMPAT).
|
|
2
|
+
//
|
|
3
|
+
// A route serves a truncated loading shell on a default (partial) prefetch when its render is GUARANTEED
|
|
4
|
+
// to stop at a boundary. The `loading` convention file is one such marker; an in-page `<Suspense>` around
|
|
5
|
+
// the params-dependent subtree is the other - Next's vary-params suites prefetch exactly that shape and
|
|
6
|
+
// expect the shared fallback shell rather than a segment miss.
|
|
7
|
+
//
|
|
8
|
+
// Detection is a static source scan (no bundling, no evaluation), deliberately narrow: the identifier
|
|
9
|
+
// must be imported from `react` AND used as JSX in the scanned source. Missing a boundary only leaves the
|
|
10
|
+
// route on today's segment-miss path, so false negatives are the safe direction.
|
|
11
|
+
|
|
12
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* True when the route's PAGE file wraps content in a React `<Suspense>`.
|
|
17
|
+
*
|
|
18
|
+
* The manifest's page file is not always the author's source: the hybrid
|
|
19
|
+
* pages+app materializer shims every app convention as `export * from
|
|
20
|
+
* '<relative source>'`, so a text scan of the shim sees no JSX at all. Follow
|
|
21
|
+
* relative re-exports (one shim hop is all the materializer emits; the depth
|
|
22
|
+
* bound keeps a cyclic re-export from looping) before scanning.
|
|
23
|
+
*/
|
|
24
|
+
export function pageHasInPageSuspense(file: string): boolean {
|
|
25
|
+
return scanSourceOf(file, 2).some(sourceHasInPageSuspense);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** `file`'s source plus the sources of its relative re-export targets. */
|
|
29
|
+
function scanSourceOf(file: string, depth: number, seen = new Set<string>()): string[] {
|
|
30
|
+
const resolved = resolveSourceFile(file);
|
|
31
|
+
if (resolved === null || seen.has(resolved)) return [];
|
|
32
|
+
seen.add(resolved);
|
|
33
|
+
let source: string;
|
|
34
|
+
try {
|
|
35
|
+
source = readFileSync(resolved, 'utf8');
|
|
36
|
+
} catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const sources = [source];
|
|
40
|
+
if (depth <= 0) return sources;
|
|
41
|
+
const pattern = /(?:export|import)\s+(?:\*(?:\s+as\s+[\w$]+)?|\{[^}]*\})\s+from\s*['"](\.[^'"]+)['"]/g;
|
|
42
|
+
let match: RegExpExecArray | null;
|
|
43
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
44
|
+
const specifier = match[1];
|
|
45
|
+
if (!specifier) continue;
|
|
46
|
+
sources.push(...scanSourceOf(path.resolve(path.dirname(resolved), specifier), depth - 1, seen));
|
|
47
|
+
}
|
|
48
|
+
return sources;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** `file` itself, or the first existing extension/index candidate for it. */
|
|
52
|
+
function resolveSourceFile(file: string): string | null {
|
|
53
|
+
if (existsSync(file) && statSync(file).isFile()) return file;
|
|
54
|
+
for (const extension of SOURCE_EXTENSIONS) {
|
|
55
|
+
for (const candidate of [`${file}${extension}`, path.join(file, `index${extension}`)]) {
|
|
56
|
+
if (existsSync(candidate)) return candidate;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const SOURCE_EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs'];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* True when `source` imports `Suspense` from React and uses it as a JSX element. Comments are stripped
|
|
66
|
+
* first (fixtures routinely describe the shape they do NOT have in prose above the component). Both the
|
|
67
|
+
* named import and the namespace/default form are recognized.
|
|
68
|
+
*/
|
|
69
|
+
export function sourceHasInPageSuspense(source: string): boolean {
|
|
70
|
+
const stripped = stripComments(source);
|
|
71
|
+
const clauses = reactImportClauses(stripped);
|
|
72
|
+
if (clauses.length === 0) return false;
|
|
73
|
+
const named = clauses.some(clause => /\bSuspense\b/.test(clause));
|
|
74
|
+
if (named && /<\s*Suspense[\s/>]/.test(stripped)) return true;
|
|
75
|
+
// `<React.Suspense>` / `<Foo.Suspense>` needs a default or namespace binding
|
|
76
|
+
// of the react module under that name.
|
|
77
|
+
return namespaceBindings(clauses).some(name =>
|
|
78
|
+
new RegExp(`<\\s*${escapeRegExp(name)}\\.Suspense[\\s/>]`).test(stripped),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The import clauses of every `... from 'react'` statement in `source`. */
|
|
83
|
+
function reactImportClauses(source: string): string[] {
|
|
84
|
+
const clauses: string[] = [];
|
|
85
|
+
const pattern = /import\s+([^'"]*?)\s+from\s*['"]react['"]/g;
|
|
86
|
+
let match: RegExpExecArray | null;
|
|
87
|
+
while ((match = pattern.exec(source)) !== null) clauses.push(match[1] ?? '');
|
|
88
|
+
// `const React = require('react')` — the CJS form app code still uses.
|
|
89
|
+
const cjs = /(?:const|let|var)\s+([\w$]+)\s*=\s*require\(\s*['"]react['"]\s*\)/g;
|
|
90
|
+
while ((match = cjs.exec(source)) !== null) clauses.push(`* as ${match[1] ?? ''}`);
|
|
91
|
+
return clauses;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Names bound to the whole react module by an import clause (default/namespace). */
|
|
95
|
+
function namespaceBindings(clauses: readonly string[]): string[] {
|
|
96
|
+
const names: string[] = [];
|
|
97
|
+
for (const clause of clauses) {
|
|
98
|
+
const namespace = /\*\s+as\s+([\w$]+)/.exec(clause);
|
|
99
|
+
if (namespace?.[1]) names.push(namespace[1]);
|
|
100
|
+
// A default binding precedes any `{ ... }` named list.
|
|
101
|
+
const fallback = /^\s*([\w$]+)\s*(?:,|$)/.exec(clause.split('{')[0] ?? '');
|
|
102
|
+
if (fallback?.[1] && fallback[1] !== '*') names.push(fallback[1]);
|
|
103
|
+
}
|
|
104
|
+
return names;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function stripComments(source: string): string {
|
|
108
|
+
return source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function escapeRegExp(value: string): string {
|
|
112
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
113
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// Page-slot framing for the layout/page segment split (COMPAT - browser-safe).
|
|
2
|
+
//
|
|
3
|
+
// pnext answers a segment prefetch with ONE document that carries the layout chain AND the page. Next
|
|
4
|
+
// answers with per-segment frames, each keyed on the params THAT segment read - which is what lets a
|
|
5
|
+
// navigation re-fetch a layout (vary `[category, item]`) while reusing the cached page (vary `[category]`).
|
|
6
|
+
//
|
|
7
|
+
// pnext keeps the one-document wire form and derives the two frames from it: the page's markup always
|
|
8
|
+
// lives inside the renderer's page slot, emitted as either a `<div id="pnext-page">` wrapper or the
|
|
9
|
+
// neutralized comment pair a client-island bootstrap dissolves it into. So the LAYOUT frame is the
|
|
10
|
+
// document with the slot's CHILDREN removed, the PAGE frame is the document as rendered, and composing
|
|
11
|
+
// them splices the page frame's slot children into the layout frame's slot.
|
|
12
|
+
//
|
|
13
|
+
// Deliberately string-level, not DOM-level: the server has no DOM and the client runs this before it
|
|
14
|
+
// parses anything. The div scan is BALANCED - a greedy `</div>` match swallows ancestor closing tags and
|
|
15
|
+
// leaves the layout unclosed.
|
|
16
|
+
|
|
17
|
+
/** The byte range of a document's page slot and of the page markup inside it. */
|
|
18
|
+
export interface PageSlotRange {
|
|
19
|
+
/** Index of the opening `<div …>` / `<!--pnext-page:…-->` marker. */
|
|
20
|
+
start: number;
|
|
21
|
+
/** Index just past the opening marker (where the page's markup begins). */
|
|
22
|
+
childrenStart: number;
|
|
23
|
+
/** Index of the closing `</div>` / `<!--/pnext-page-->` marker. */
|
|
24
|
+
childrenEnd: number;
|
|
25
|
+
/** Index just past the closing marker. */
|
|
26
|
+
end: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const SLOT_DIV_OPEN = /<div\b[^>]*\bid="pnext-page"[^>]*>/;
|
|
30
|
+
const SLOT_COMMENT_OPEN = /<!--pnext-page:[^>]*-->/;
|
|
31
|
+
const SLOT_COMMENT_CLOSE = '<!--/pnext-page-->';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Locate the page slot in a rendered document, or null when it has none (a
|
|
35
|
+
* global-error document, a page that ships no client runtime and therefore had
|
|
36
|
+
* its slot markers dropped, a non-pnext body).
|
|
37
|
+
*/
|
|
38
|
+
export function pageSlotRange(html: string): PageSlotRange | null {
|
|
39
|
+
const comment = SLOT_COMMENT_OPEN.exec(html);
|
|
40
|
+
if (comment) {
|
|
41
|
+
const childrenStart = comment.index + comment[0].length;
|
|
42
|
+
const childrenEnd = html.indexOf(SLOT_COMMENT_CLOSE, childrenStart);
|
|
43
|
+
if (childrenEnd === -1) return null;
|
|
44
|
+
return {
|
|
45
|
+
start: comment.index,
|
|
46
|
+
childrenStart,
|
|
47
|
+
childrenEnd,
|
|
48
|
+
end: childrenEnd + SLOT_COMMENT_CLOSE.length,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const open = SLOT_DIV_OPEN.exec(html);
|
|
52
|
+
if (!open) return null;
|
|
53
|
+
const childrenStart = open.index + open[0].length;
|
|
54
|
+
const tag = /<div\b|<\/div>/g;
|
|
55
|
+
tag.lastIndex = childrenStart;
|
|
56
|
+
let depth = 1;
|
|
57
|
+
for (let match = tag.exec(html); match; match = tag.exec(html)) {
|
|
58
|
+
depth += match[0] === '</div>' ? -1 : 1;
|
|
59
|
+
if (depth !== 0) continue;
|
|
60
|
+
return {
|
|
61
|
+
start: open.index,
|
|
62
|
+
childrenStart,
|
|
63
|
+
childrenEnd: match.index,
|
|
64
|
+
end: match.index + match[0].length,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The page's own markup (the slot's children), or null without a slot. */
|
|
71
|
+
export function pageSlotContent(html: string): string | null {
|
|
72
|
+
const range = pageSlotRange(html);
|
|
73
|
+
return range ? html.slice(range.childrenStart, range.childrenEnd) : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The LAYOUT frame of a rendered document: the same document with the page's markup removed and the slot
|
|
78
|
+
* left empty. Returned unchanged when the document has no page slot - an empty layout frame would be
|
|
79
|
+
* worse than a whole one.
|
|
80
|
+
*/
|
|
81
|
+
export function stripPageSlotContent(html: string): string {
|
|
82
|
+
const range = pageSlotRange(html);
|
|
83
|
+
if (!range) return html;
|
|
84
|
+
return html.slice(0, range.childrenStart) + html.slice(range.childrenEnd);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// A streamed Suspense continuation: the hidden chunk divs the renderer appends
|
|
88
|
+
// near the end of the document (renderer.ts STREAM_CHUNK_MARKER), plus the
|
|
89
|
+
// inline-suspense variant. A page's dynamic content resolves INTO one of these,
|
|
90
|
+
// not into the slot, so a page frame that stopped at the slot would carry only
|
|
91
|
+
// the page's `loading` fallback.
|
|
92
|
+
const STREAM_CHUNK_MARKERS = ['<div hidden data-pnext-stream', '<div hidden><preact-island'];
|
|
93
|
+
|
|
94
|
+
/** Index of the first streamed continuation chunk at/after `from`, or -1. */
|
|
95
|
+
function streamedContinuationStart(html: string, from: number): number {
|
|
96
|
+
const cuts = STREAM_CHUNK_MARKERS.map(marker => html.indexOf(marker, from)).filter(
|
|
97
|
+
index => index !== -1,
|
|
98
|
+
);
|
|
99
|
+
return cuts.length > 0 ? Math.min(...cuts) : -1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The PAGE frame of a rendered document: the page slot - its markers plus the page's own markup -
|
|
104
|
+
* followed by the document's streamed continuation chunks, with the whole layout chain and the document
|
|
105
|
+
* shell cut away. Null when the document has no page slot.
|
|
106
|
+
*
|
|
107
|
+
* The inverse of `stripPageSlotContent`, and what the `/_page` segment request serves: the response must
|
|
108
|
+
* not carry the layout's markup (a navigation that already renders that layout rejects it), while
|
|
109
|
+
* carrying everything needed to resolve the page's own boundaries. The frame is self-identifying: its
|
|
110
|
+
* page slot starts at offset 0, which is what `composeSegmentFrames` keys on.
|
|
111
|
+
*/
|
|
112
|
+
export function pageSlotFrame(html: string): string | null {
|
|
113
|
+
const range = pageSlotRange(html);
|
|
114
|
+
if (!range) return null;
|
|
115
|
+
const slot = html.slice(range.start, range.end);
|
|
116
|
+
const chunks = streamedContinuationStart(html, range.end);
|
|
117
|
+
if (chunks === -1) return slot;
|
|
118
|
+
const bodyEnd = html.lastIndexOf('</body>');
|
|
119
|
+
return slot + html.slice(chunks, bodyEnd > chunks ? bodyEnd : html.length);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* A LAYOUT frame with the streamed continuation chunks of the render it came from cut away.
|
|
124
|
+
*
|
|
125
|
+
* Every layout frame is derived from a document that also rendered a PAGE, and that page's dynamic content
|
|
126
|
+
* resolves into the chunks near the end of the document, not into the slot. Splicing a FRESH page frame
|
|
127
|
+
* into a frame that still carries the old page's chunks would graft the stale content over the new page's
|
|
128
|
+
* boundaries. Scanned on the frame, whose slot is already empty, so the first marker found is always a
|
|
129
|
+
* continuation chunk.
|
|
130
|
+
*/
|
|
131
|
+
export function stripStreamedContinuation(html: string): string {
|
|
132
|
+
// Each chunk is removed SURGICALLY (its balanced div plus the promotion
|
|
133
|
+
// script that follows it), never "from the first marker to </body>": a layout
|
|
134
|
+
// whose own content streams emits chunks BEFORE the page slot, and cutting to
|
|
135
|
+
// the end of the body would take the slot with them, leaving a frame nothing
|
|
136
|
+
// can be composed into.
|
|
137
|
+
let result = html;
|
|
138
|
+
let from = 0;
|
|
139
|
+
for (;;) {
|
|
140
|
+
const start = streamedContinuationStart(result, from);
|
|
141
|
+
if (start === -1) return result;
|
|
142
|
+
const end = chunkEnd(result, start);
|
|
143
|
+
if (end === -1) return result;
|
|
144
|
+
// A chunk that CONTAINS the page slot is the frame's own slot carrier (the
|
|
145
|
+
// page's markup streams in below the layout), not stale continuation —
|
|
146
|
+
// dropping it would leave nothing to compose a fresh page into.
|
|
147
|
+
const slot = pageSlotRange(result);
|
|
148
|
+
if (slot && slot.start > start && slot.end <= end) {
|
|
149
|
+
from = end;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
result = result.slice(0, start) + result.slice(end);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Index just past a continuation chunk (its balanced div + promotion script). */
|
|
157
|
+
function chunkEnd(html: string, start: number): number {
|
|
158
|
+
const tag = /<div\b|<\/div>/g;
|
|
159
|
+
tag.lastIndex = start + '<div'.length;
|
|
160
|
+
let depth = 1;
|
|
161
|
+
let divEnd = -1;
|
|
162
|
+
for (let match = tag.exec(html); match; match = tag.exec(html)) {
|
|
163
|
+
depth += match[0] === '</div>' ? -1 : 1;
|
|
164
|
+
if (depth > 0) continue;
|
|
165
|
+
divEnd = match.index + match[0].length;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
if (divEnd === -1) return -1;
|
|
169
|
+
const rest = html.slice(divEnd);
|
|
170
|
+
const script = /^\s*<script\b[^>]*>[\s\S]*?<\/script>/.exec(rest);
|
|
171
|
+
return script ? divEnd + script[0].length : divEnd;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Compose a cached LAYOUT frame with a PAGE frame: the layout document with the page frame's slot children
|
|
176
|
+
* spliced into its (empty) slot.
|
|
177
|
+
*
|
|
178
|
+
* When `pageHtml` is a `/_page` FRAME - its slot starts at offset 0 - rather than a whole document,
|
|
179
|
+
* whatever follows the slot is the page's streamed continuation and is appended before the layout's
|
|
180
|
+
* `</body>`, so the composed document resolves the page's boundaries exactly as the whole-document
|
|
181
|
+
* response would have. Null when either side has no page slot: the frames cannot be proven to line up,
|
|
182
|
+
* and painting a half-composed document is worse than fetching.
|
|
183
|
+
*/
|
|
184
|
+
export function composeSegmentFrames(layoutHtml: string, pageHtml: string): string | null {
|
|
185
|
+
const range = pageSlotRange(layoutHtml);
|
|
186
|
+
if (!range) return null;
|
|
187
|
+
const pageRange = pageSlotRange(pageHtml);
|
|
188
|
+
if (!pageRange) return null;
|
|
189
|
+
const page = pageHtml.slice(pageRange.childrenStart, pageRange.childrenEnd);
|
|
190
|
+
const composed =
|
|
191
|
+
layoutHtml.slice(0, range.childrenStart) + page + layoutHtml.slice(range.childrenEnd);
|
|
192
|
+
// A whole document's tail is its own layout/scripts, already present in the
|
|
193
|
+
// layout frame; only a frame's tail is continuation markup to carry over.
|
|
194
|
+
const tail = pageRange.start === 0 ? pageHtml.slice(pageRange.end) : '';
|
|
195
|
+
if (!tail) return composed;
|
|
196
|
+
const insertAt = composed.lastIndexOf('</body>');
|
|
197
|
+
return insertAt === -1
|
|
198
|
+
? composed + tail
|
|
199
|
+
: composed.slice(0, insertAt) + tail + composed.slice(insertAt);
|
|
200
|
+
}
|