@warlock.js/web 5.1.0 → 5.2.0
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/CHANGELOG.md +174 -0
- package/README.md +126 -12
- package/esm/build/contribution.d.mts +11 -14
- package/esm/build/contribution.mjs +46 -58
- package/esm/build/contribution.mjs.map +1 -1
- package/esm/build/discover-pages.mjs +105 -45
- package/esm/build/discover-pages.mjs.map +1 -1
- package/esm/build/generate-client-registry.mjs +47 -13
- package/esm/build/generate-client-registry.mjs.map +1 -1
- package/esm/build/generate-pages-barrel.mjs +55 -13
- package/esm/build/generate-pages-barrel.mjs.map +1 -1
- package/esm/build/page-default-export.mjs +48 -0
- package/esm/build/page-default-export.mjs.map +1 -0
- package/esm/build/page-routes-manifest.mjs +21 -0
- package/esm/build/page-routes-manifest.mjs.map +1 -0
- package/esm/build/public-files.mjs +45 -0
- package/esm/build/public-files.mjs.map +1 -0
- package/esm/build/read-route-exports.mjs +1 -1
- package/esm/build/read-route-exports.mjs.map +1 -1
- package/esm/client/build-hydrated-tree.mjs +40 -9
- package/esm/client/build-hydrated-tree.mjs.map +1 -1
- package/esm/client/hydrate-page.mjs +1 -1
- package/esm/client/hydrate-page.mjs.map +1 -1
- package/esm/client/navigation/current-route.d.mts +1 -1
- package/esm/client/navigation/current-route.mjs +1 -1
- package/esm/client/navigation/current-route.mjs.map +1 -1
- package/esm/client/navigation/fetch-page-data.mjs +1 -1
- package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
- package/esm/client/navigation/navigation-root.mjs +1 -1
- package/esm/client/navigation/navigation-root.mjs.map +1 -1
- package/esm/client/navigation/prefetch.mjs +1 -1
- package/esm/client/navigation/prefetch.mjs.map +1 -1
- package/esm/client/navigation/refresh.d.mts +1 -1
- package/esm/client/navigation/refresh.mjs +1 -1
- package/esm/client/navigation/refresh.mjs.map +1 -1
- package/esm/client/navigation/scroll-to-fragment.mjs +1 -1
- package/esm/client/navigation/scroll-to-fragment.mjs.map +1 -1
- package/esm/client/navigation/verbs.d.mts +1 -1
- package/esm/client/navigation/verbs.mjs +1 -1
- package/esm/client/navigation/verbs.mjs.map +1 -1
- package/esm/client/runtime/index.d.mts +2 -1
- package/esm/client/runtime/index.mjs +2 -1
- package/esm/client/runtime/manifest.d.mts +1 -1
- package/esm/client/runtime/manifest.mjs +3 -2
- package/esm/client/runtime/manifest.mjs.map +1 -1
- package/esm/client/runtime/matcher.d.mts +13 -1
- package/esm/client/runtime/matcher.mjs +13 -1
- package/esm/client/runtime/matcher.mjs.map +1 -1
- package/esm/client/runtime/types.d.mts +15 -2
- package/esm/components/default-app.mjs +1 -1
- package/esm/components/default-app.mjs.map +1 -1
- package/esm/components/document-context.d.mts +40 -2
- package/esm/components/document-context.mjs +1 -1
- package/esm/components/document-context.mjs.map +1 -1
- package/esm/components/head.d.mts +1 -1
- package/esm/components/head.mjs +1 -1
- package/esm/components/head.mjs.map +1 -1
- package/esm/components/link.d.mts +1 -1
- package/esm/components/link.mjs +1 -1
- package/esm/components/link.mjs.map +1 -1
- package/esm/components/scripts.d.mts +1 -1
- package/esm/components/scripts.mjs +4 -2
- package/esm/components/scripts.mjs.map +1 -1
- package/esm/context.d.mts +6 -102
- package/esm/hydration/index.mjs +1 -1
- package/esm/hydration/index.mjs.map +1 -1
- package/esm/hydration-payload.d.mts +1 -1
- package/esm/hydration-payload.mjs +37 -8
- package/esm/hydration-payload.mjs.map +1 -1
- package/esm/index.d.mts +4 -3
- package/esm/loaders.d.mts +7 -36
- package/esm/metadata.d.mts +1 -1
- package/esm/metadata.mjs +1 -1
- package/esm/metadata.mjs.map +1 -1
- package/esm/props.d.mts +16 -8
- package/esm/route.d.mts +2 -17
- package/esm/routing/compose-route-path.d.mts +1 -1
- package/esm/routing/compose-route-path.mjs +1 -1
- package/esm/routing/compose-route-path.mjs.map +1 -1
- package/esm/routing/data-request.mjs +1 -1
- package/esm/routing/data-request.mjs.map +1 -1
- package/esm/routing/filesystem-route.mjs +45 -0
- package/esm/routing/filesystem-route.mjs.map +1 -0
- package/esm/routing/layout-policy.mjs +1 -1
- package/esm/routing/layout-policy.mjs.map +1 -1
- package/esm/routing/navigator.mjs +1 -1
- package/esm/routing/navigator.mjs.map +1 -1
- package/esm/routing/query-string.d.mts +1 -1
- package/esm/routing/query-string.mjs +1 -1
- package/esm/routing/query-string.mjs.map +1 -1
- package/esm/routing/route-identity.mjs +1 -1
- package/esm/routing/route-identity.mjs.map +1 -1
- package/esm/routing/route-table.d.mts +1 -1
- package/esm/routing/route-table.mjs +1 -1
- package/esm/routing/route-table.mjs.map +1 -1
- package/esm/routing/router-events.d.mts +1 -1
- package/esm/routing/router-events.mjs +1 -1
- package/esm/routing/router-events.mjs.map +1 -1
- package/esm/routing/url-fragment.mjs +1 -1
- package/esm/routing/url-fragment.mjs.map +1 -1
- package/esm/runtime/register-modules.d.mts +19 -0
- package/esm/runtime/register-modules.mjs +22 -0
- package/esm/runtime/register-modules.mjs.map +1 -0
- package/esm/server/build-hydration-payload.mjs +2 -1
- package/esm/server/build-hydration-payload.mjs.map +1 -1
- package/esm/server/client-asset-url-prefix.d.mts +1 -1
- package/esm/server/client-asset-url-prefix.mjs +1 -1
- package/esm/server/client-asset-url-prefix.mjs.map +1 -1
- package/esm/server/create-page-module-loader.d.mts +1 -1
- package/esm/server/create-page-module-loader.mjs +2 -1
- package/esm/server/create-page-module-loader.mjs.map +1 -1
- package/esm/server/create-page-route-handler.d.mts +31 -6
- package/esm/server/create-page-route-handler.mjs +128 -43
- package/esm/server/create-page-route-handler.mjs.map +1 -1
- package/esm/server/dev-server.mjs +2 -13
- package/esm/server/dev-server.mjs.map +1 -1
- package/esm/server/error-page.d.mts +14 -0
- package/esm/server/error-page.mjs +39 -0
- package/esm/server/error-page.mjs.map +1 -0
- package/esm/server/execute-page-request.d.mts +7 -4
- package/esm/server/execute-page-request.mjs +79 -80
- package/esm/server/execute-page-request.mjs.map +1 -1
- package/esm/server/execute-page-request.types.d.mts +33 -92
- package/esm/server/hydration-client-url.d.mts +1 -1
- package/esm/server/hydration-client-url.mjs +2 -2
- package/esm/server/hydration-client-url.mjs.map +1 -1
- package/esm/server/index.d.mts +5 -5
- package/esm/server/index.mjs +3 -4
- package/esm/server/install-page-routes-from-manifest.d.mts +17 -7
- package/esm/server/install-page-routes-from-manifest.mjs +71 -10
- package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
- package/esm/server/install-page-routes.d.mts +32 -15
- package/esm/server/install-page-routes.mjs +91 -39
- package/esm/server/install-page-routes.mjs.map +1 -1
- package/esm/server/install-production-page-routes.mjs +3 -36
- package/esm/server/install-production-page-routes.mjs.map +1 -1
- package/esm/server/match-page-route.mjs +2 -2
- package/esm/server/match-page-route.mjs.map +1 -1
- package/esm/server/not-found-page.d.mts +1 -1
- package/esm/server/not-found-page.mjs +2 -2
- package/esm/server/not-found-page.mjs.map +1 -1
- package/esm/server/page-context.d.mts +3 -9
- package/esm/server/page-context.mjs +3 -16
- package/esm/server/page-context.mjs.map +1 -1
- package/esm/server/page-file-change.mjs +77 -0
- package/esm/server/page-file-change.mjs.map +1 -0
- package/esm/server/page-manifest.d.mts +25 -10
- package/esm/server/page-manifest.mjs +1 -1
- package/esm/server/page-manifest.mjs.map +1 -1
- package/esm/server/page-render-bundle.mjs +38 -0
- package/esm/server/page-render-bundle.mjs.map +1 -0
- package/esm/server/page-route-reload.mjs +54 -0
- package/esm/server/page-route-reload.mjs.map +1 -0
- package/esm/server/register-production-public-files.mjs +40 -0
- package/esm/server/register-production-public-files.mjs.map +1 -0
- package/esm/server/render-page.d.mts +11 -14
- package/esm/server/render-page.mjs +154 -28
- package/esm/server/render-page.mjs.map +1 -1
- package/esm/server/resolve-page-metadata.mjs +2 -2
- package/esm/server/resolve-page-metadata.mjs.map +1 -1
- package/esm/server/resolve-validation-data.mjs +1 -1
- package/esm/server/resolve-validation-data.mjs.map +1 -1
- package/esm/server/settle-page-response.d.mts +58 -1
- package/esm/server/settle-page-response.mjs +111 -46
- package/esm/server/settle-page-response.mjs.map +1 -1
- package/esm/server/stylesheet-urls.d.mts +41 -30
- package/esm/server/stylesheet-urls.mjs +137 -52
- package/esm/server/stylesheet-urls.mjs.map +1 -1
- package/esm/server/unregistered-pages.mjs +42 -0
- package/esm/server/unregistered-pages.mjs.map +1 -0
- package/esm/server/web-connector-factory.d.mts +2 -3
- package/esm/server/web-connector-factory.mjs +7 -6
- package/esm/server/web-connector-factory.mjs.map +1 -1
- package/esm/server/web-connector.d.mts +1 -1
- package/esm/server/web-connector.mjs +138 -18
- package/esm/server/web-connector.mjs.map +1 -1
- package/esm/shared.d.mts +18 -3
- package/esm/shared.mjs +3 -7
- package/esm/shared.mjs.map +1 -1
- package/esm/validation.d.mts +1 -1
- package/esm/vite/app-convention-aliases.mjs +1 -1
- package/esm/vite/app-convention-aliases.mjs.map +1 -1
- package/esm/vite/build-client.d.mts +1 -1
- package/esm/vite/build-client.mjs +2 -2
- package/esm/vite/build-client.mjs.map +1 -1
- package/esm/vite/gate-a-resolve.d.mts +1 -1
- package/esm/vite/gate-a-resolve.mjs +67 -13
- package/esm/vite/gate-a-resolve.mjs.map +1 -1
- package/esm/vite/gate-b-secrets.d.mts +1 -1
- package/esm/vite/gate-b-secrets.mjs +35 -17
- package/esm/vite/gate-b-secrets.mjs.map +1 -1
- package/esm/vite/gate-c-verify.d.mts +1 -1
- package/esm/vite/gate-c-verify.mjs +6 -6
- package/esm/vite/gate-c-verify.mjs.map +1 -1
- package/esm/vite/hydration-entries.d.mts +1 -1
- package/esm/vite/hydration-entries.mjs +2 -2
- package/esm/vite/hydration-entries.mjs.map +1 -1
- package/esm/vite/index.d.mts +9 -6
- package/esm/vite/index.mjs +124 -11
- package/esm/vite/index.mjs.map +1 -1
- package/esm/vite/page-registry-plugin.d.mts +22 -3
- package/esm/vite/page-registry-plugin.mjs +48 -14
- package/esm/vite/page-registry-plugin.mjs.map +1 -1
- package/esm/vite/projection.d.mts +2 -2
- package/esm/vite/projection.mjs +38 -15
- package/esm/vite/projection.mjs.map +1 -1
- package/llms-full.txt +220 -56
- package/llms.txt +5 -3
- package/package.json +3 -3
- package/skills/add-web-to-an-app/SKILL.md +3 -3
- package/skills/create-a-page/SKILL.md +117 -10
- package/skills/load-page-data/SKILL.md +92 -12
- package/skills/navigate-on-the-client/SKILL.md +1 -1
- package/skills/serve-styles/SKILL.md +13 -12
- package/skills/use-layouts/SKILL.md +14 -8
- package/skills/write-the-root/SKILL.md +3 -1
- package/esm/server/buffered-response.d.mts +0 -58
- package/esm/server/buffered-response.mjs +0 -115
- package/esm/server/buffered-response.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"page-manifest.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"page-manifest.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/server/page-manifest.ts"],"sourcesContent":["/**\n * The build→runtime handoff table for production pages.\n *\n * The generated `<productionDir>/pages.ts` barrel — written by\n * `web/src/build/generate-pages-barrel.ts` — statically imports every page,\n * layout and the app root, then calls {@link providePageManifest} exactly\n * once. The connector reads it back at boot with {@link consumePageManifest}.\n *\n * DEPENDENCY-FREE ON PURPOSE: this module is re-exported from web's root\n * barrel (`web/src/index.ts`) so the generated barrel — which lives in the\n * consuming app's production dir — can reach it by package specifier. Adding\n * an import here would add that import to every app that loads\n * `@warlock.js/web`.\n *\n * Entries carry MODULES + FILESYSTEM FACTS ONLY — never route paths. A page's\n * route is read off its module (`route` export) at boot and, when present,\n * composed with its layout `prefix`; when absent, the path is instead derived\n * from the page's own `sourceFile` and its layouts' prefixes, exactly as\n * `install-page-routes.ts:377-382` does in dev and `discover-pages.ts:925-935`\n * does at build — so explicit-wins semantics live in one place for all three.\n */\n\nexport type PageManifestLayoutEntry = {\n /** The imported layout module namespace object (default export = component, optional `prefix`). */\n module: Record<string, unknown>;\n /** App-root-relative POSIX source path, e.g. \"src/web/layout.tsx\". */\n sourceFile: string;\n};\n\nexport type PageManifestPageEntry = {\n /** The imported page module namespace object (default export = component, optional `route`). */\n module: Record<string, unknown>;\n /**\n * App-root-relative POSIX source path, e.g. \"src/web/home.page.tsx\"\n * — feeds FS-derived route/name fallback. The anchor is the APP ROOT, not the\n * repo root; the two differ the moment the app does not sit at the repo root.\n * Consumers compare these ids VERBATIM — the barrel that wrote them is the\n * authority on their form, and re-resolving or normalising one on the way in\n * turns a lookup miss into a silent mis-match.\n */\n sourceFile: string;\n /** Layout chain, outermost first. */\n layouts: readonly PageManifestLayoutEntry[];\n};\n\n/** The optional application error boundary, kept outside the routable page table. */\nexport type PageManifestErrorPageEntry = {\n module: Record<string, unknown>;\n sourceFile: string;\n};\n\nexport type PageManifest = {\n /**\n * Where the client bundle was written, as an APP-ROOT-RELATIVE POSIX path\n * (`\"dist/client\"`). Resolved against the process cwd at boot, the same\n * anchor `sourceFile` uses.\n *\n * BAKED AT BUILD TIME because the runtime has no other way to learn it. The\n * directory is `<build.outdir>/client`, and `build.outdir` is configurable —\n * but it lives in `warlock.config.ts`, a BUILD-TIME file that is never\n * shipped and cannot be loaded by the plain `node dist/app.js` process\n * `warlock start` spawns. Reading it at runtime through\n * `resolveBuildConfig()` is what used to throw\n * `WarlockConfig not loaded` during connector boot, taking the whole server\n * down before it could listen.\n *\n * OPTIONAL only when the build has neither pages nor public files. A\n * zero-page build skips the hydration bundle, but still names this directory\n * when it copied app-owned `public/` files below it. Present whenever\n * `pages` or `publicFiles` is non-empty.\n */\n clientDir?: string;\n /**\n * App-public file paths copied below `<clientDir>/public`, relative and\n * POSIX. Production registers these exact URLs; it never mounts the client\n * root and therefore never exposes `.vite/manifest.json`.\n */\n publicFiles?: readonly string[];\n /**\n * The application root component every page renders inside.\n *\n * OPTIONAL because it is only meaningful when pages exist: a build that\n * discovered zero pages provides `{ pages: [] }` and has no root to wrap\n * anything in. The build enforces the other direction — pages without an\n * app root fail generation — so `app` absent means `pages` is empty. Callers\n * must still branch on `pages.length` before reading `app`: the invariant is\n * the generator's, and a consumer that assumes it without checking turns a\n * malformed manifest into a crash deep in rendering instead of a named error.\n */\n app?: { module: Record<string, unknown>; sourceFile: string };\n /** An application-owned error boundary. It has no route name or path. */\n errorPage?: PageManifestErrorPageEntry;\n /**\n * The page table. EMPTY IS A REAL STATE, not a missing one: an empty array\n * means \"this bundle was built with web, and it has no pages\", which is what\n * separates it from {@link consumePageManifest} returning `undefined`.\n */\n pages: readonly PageManifestPageEntry[];\n};\n\nlet providedManifest: PageManifest | undefined;\n\n/**\n * Called exactly once by the generated production/pages.ts barrel.\n *\n * A second call throws: two barrels in one process means either a stale\n * generated file survived into the bundle or the build ran twice, and the\n * second table silently replacing the first is precisely the drift this\n * contract exists to make impossible.\n */\nexport function providePageManifest(manifest: PageManifest): void {\n if (providedManifest !== undefined) {\n throw new Error(\n \"providePageManifest: a page manifest was already provided \" +\n \"(web/src/server/page-manifest.ts). The generated production/pages.ts barrel \" +\n \"must run exactly once per process.\",\n );\n }\n\n providedManifest = manifest;\n}\n\n/**\n * Connector-side read (WebConnector.boot, prod mode).\n *\n * `undefined` = no barrel ran → the app was not built with web. A manifest\n * whose `pages` is empty is the OTHER answer — built with web, no pages — and\n * the two are deliberately distinguishable here rather than collapsed into one\n * falsy result. The registry itself never throws on absence — absence is a\n * fact, not an error; the connector branches on MODE (prod: boot error; dev:\n * normal) so `undefined` never means two things at one call site.\n */\nexport function consumePageManifest(): PageManifest | undefined {\n return providedManifest;\n}\n"],"mappings":";AAoGA,IAAI;;;;;;;;;AAUJ,SAAgB,oBAAoB,UAA8B;CAChE,IAAI,qBAAqB,QACvB,MAAM,IAAI,MACR,0KAGF;CAGF,mBAAmB;AACrB;;;;;;;;;;;AAYA,SAAgB,sBAAgD;CAC9D,OAAO;AACT"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//#region web/src/server/page-render-bundle.ts
|
|
2
|
+
/**
|
|
3
|
+
* The internal, non-serializable marker `renderPageFailure` (render-page.ts)
|
|
4
|
+
* sets on its bundle AND on the document payload it hands to
|
|
5
|
+
* `DocumentContext` — never on anything `finishRender` produces.
|
|
6
|
+
*
|
|
7
|
+
* `renderPageFailure` runs when a module-load or registration throw happens
|
|
8
|
+
* BEFORE any page/layout/app triple exists — there is no trustworthy server
|
|
9
|
+
* composition for the browser to hydrate against, unlike a normal app-owned
|
|
10
|
+
* `error.page.tsx` reached through `finishRender`, which renders inside a
|
|
11
|
+
* real triple and stays hydratable. Marking both the bundle (read by
|
|
12
|
+
* `create-page-route-handler.ts`'s catch path to skip the hydration module)
|
|
13
|
+
* and the payload (read by `<Scripts/>` to skip `__WARLOCK_DATA__`) lets both
|
|
14
|
+
* call sites agree from the SAME flag instead of two independent guesses.
|
|
15
|
+
*
|
|
16
|
+
* A `Symbol`, non-enumerable, and never part of any exported type: nothing
|
|
17
|
+
* that serializes or spreads these objects (`JSON.stringify`, `{ ...x }`) can
|
|
18
|
+
* observe or carry it forward — only `isNonHydrating` reads it, and only
|
|
19
|
+
* `markNonHydrating` writes it.
|
|
20
|
+
*/
|
|
21
|
+
const NON_HYDRATING = Symbol("warlock.web/non-hydrating");
|
|
22
|
+
/** Set only by `renderPageFailure` — see the module doc above. */
|
|
23
|
+
function markNonHydrating(target) {
|
|
24
|
+
Object.defineProperty(target, NON_HYDRATING, {
|
|
25
|
+
value: true,
|
|
26
|
+
enumerable: false,
|
|
27
|
+
configurable: true
|
|
28
|
+
});
|
|
29
|
+
return target;
|
|
30
|
+
}
|
|
31
|
+
/** Read by `create-page-route-handler.ts` and `<Scripts/>` — see the module doc above. */
|
|
32
|
+
function isNonHydrating(target) {
|
|
33
|
+
return target !== void 0 && target[NON_HYDRATING] === true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
export { isNonHydrating, markNonHydrating };
|
|
38
|
+
//# sourceMappingURL=page-render-bundle.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-render-bundle.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/server/page-render-bundle.ts"],"sourcesContent":["/**\n * The internal, non-serializable marker `renderPageFailure` (render-page.ts)\n * sets on its bundle AND on the document payload it hands to\n * `DocumentContext` — never on anything `finishRender` produces.\n *\n * `renderPageFailure` runs when a module-load or registration throw happens\n * BEFORE any page/layout/app triple exists — there is no trustworthy server\n * composition for the browser to hydrate against, unlike a normal app-owned\n * `error.page.tsx` reached through `finishRender`, which renders inside a\n * real triple and stays hydratable. Marking both the bundle (read by\n * `create-page-route-handler.ts`'s catch path to skip the hydration module)\n * and the payload (read by `<Scripts/>` to skip `__WARLOCK_DATA__`) lets both\n * call sites agree from the SAME flag instead of two independent guesses.\n *\n * A `Symbol`, non-enumerable, and never part of any exported type: nothing\n * that serializes or spreads these objects (`JSON.stringify`, `{ ...x }`) can\n * observe or carry it forward — only `isNonHydrating` reads it, and only\n * `markNonHydrating` writes it.\n */\nconst NON_HYDRATING = Symbol(\"warlock.web/non-hydrating\");\n\ntype MarkedNonHydrating = {\n readonly [NON_HYDRATING]?: true;\n};\n\n/** Set only by `renderPageFailure` — see the module doc above. */\nexport function markNonHydrating<T extends object>(target: T): T {\n Object.defineProperty(target, NON_HYDRATING, {\n value: true,\n enumerable: false,\n configurable: true,\n });\n\n return target;\n}\n\n/** Read by `create-page-route-handler.ts` and `<Scripts/>` — see the module doc above. */\nexport function isNonHydrating(target: object | undefined): boolean {\n return target !== undefined && (target as MarkedNonHydrating)[NON_HYDRATING] === true;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAmBA,MAAM,gBAAgB,OAAO,2BAA2B;;AAOxD,SAAgB,iBAAmC,QAAc;CAC/D,OAAO,eAAe,QAAQ,eAAe;EAC3C,OAAO;EACP,YAAY;EACZ,cAAc;CAChB,CAAC;CAED,OAAO;AACT;;AAGA,SAAgB,eAAe,QAAqC;CAClE,OAAO,WAAW,UAAc,OAA8B,mBAAmB;AACnF"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { isNotFoundPageFile } from "./not-found-page.mjs";
|
|
2
|
+
import { resolvePageRouteIdentity } from "./install-page-routes.mjs";
|
|
3
|
+
import { isErrorPageFilePath } from "./page-file-change.mjs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
//#region web/src/server/page-route-reload.ts
|
|
7
|
+
function pathKey(file) {
|
|
8
|
+
const normalized = path.resolve(file).replace(/\\/g, "/");
|
|
9
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Every real page file currently represented in the route table. This includes
|
|
13
|
+
* a custom `404.page.tsx`, which is intentionally absent from the public page
|
|
14
|
+
* table because it has no linkable URL of its own.
|
|
15
|
+
*/
|
|
16
|
+
function registeredPageFiles(routes, appSrcRoot) {
|
|
17
|
+
const sourceRoot = path.dirname(appSrcRoot);
|
|
18
|
+
return [...new Set(routes.filter((route) => route.isPage && route.sourceFile && !route.sourceFile.startsWith("\0")).map((route) => path.resolve(sourceRoot, route.sourceFile)))];
|
|
19
|
+
}
|
|
20
|
+
/** Exact route-source owners replaced by a live page-table transaction. */
|
|
21
|
+
function pageRouteSourceFiles(routes) {
|
|
22
|
+
return [...new Set(routes.filter((route) => route.isPage && route.sourceFile).map((route) => route.sourceFile))];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Inspect only in-place page edits. Vite's SSR graph is invalidated explicitly
|
|
26
|
+
* before evaluation because core's file watcher and Vite's watcher have no
|
|
27
|
+
* ordering contract. Component-only edits compare equal and stay in Fast
|
|
28
|
+
* Refresh; route/name changes request an atomic table replacement.
|
|
29
|
+
*/
|
|
30
|
+
async function pageRoutesNeedReplacement(changes, options) {
|
|
31
|
+
let replace = changes.added.length > 0 || changes.removed.length > 0;
|
|
32
|
+
const installedByFile = new Map(options.installedPages.map((page) => [pathKey(page.file), page]));
|
|
33
|
+
for (const file of changes.inspectionNeeded) {
|
|
34
|
+
if (isErrorPageFilePath(file)) return true;
|
|
35
|
+
options.vite.environments.ssr.moduleGraph.onFileChange(file);
|
|
36
|
+
const pageModule = await options.vite.ssrLoadModule(file);
|
|
37
|
+
if (isNotFoundPageFile(file)) {
|
|
38
|
+
if (pageModule.route !== void 0) replace = true;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const installed = installedByFile.get(pathKey(file));
|
|
42
|
+
if (installed === void 0) {
|
|
43
|
+
replace = true;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const next = resolvePageRouteIdentity(pageModule.route, file, options.appSrcRoot);
|
|
47
|
+
if (next.declaredPath !== installed.declaredPath || next.name !== installed.name) replace = true;
|
|
48
|
+
}
|
|
49
|
+
return replace;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
//#endregion
|
|
53
|
+
export { pageRouteSourceFiles, pageRoutesNeedReplacement, registeredPageFiles };
|
|
54
|
+
//# sourceMappingURL=page-route-reload.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-route-reload.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/server/page-route-reload.ts"],"sourcesContent":["import path from \"node:path\";\nimport type { Router } from \"@warlock.js/core\";\nimport type { ViteDevServer } from \"vite\";\nimport type { InstalledPageRoute, PageModuleShape } from \"./install-page-routes\";\nimport { resolvePageRouteIdentity } from \"./install-page-routes\";\nimport { isNotFoundPageFile } from \"./not-found-page\";\nimport type { PageFileChanges } from \"./page-file-change\";\nimport { isErrorPageFilePath } from \"./page-file-change\";\n\nfunction pathKey(file: string): string {\n const normalized = path.resolve(file).replace(/\\\\/g, \"/\");\n\n return process.platform === \"win32\" ? normalized.toLowerCase() : normalized;\n}\n\n/**\n * Every real page file currently represented in the route table. This includes\n * a custom `404.page.tsx`, which is intentionally absent from the public page\n * table because it has no linkable URL of its own.\n */\nexport function registeredPageFiles(\n routes: ReturnType<Router[\"list\"]>,\n appSrcRoot: string,\n): string[] {\n const sourceRoot = path.dirname(appSrcRoot);\n\n return [\n ...new Set(\n routes\n .filter((route) => route.isPage && route.sourceFile && !route.sourceFile.startsWith(\"\\0\"))\n .map((route) => path.resolve(sourceRoot, route.sourceFile)),\n ),\n ];\n}\n\n/** Exact route-source owners replaced by a live page-table transaction. */\nexport function pageRouteSourceFiles(routes: ReturnType<Router[\"list\"]>): string[] {\n return [\n ...new Set(\n routes\n .filter((route) => route.isPage && route.sourceFile)\n .map((route) => route.sourceFile),\n ),\n ];\n}\n\n/**\n * Inspect only in-place page edits. Vite's SSR graph is invalidated explicitly\n * before evaluation because core's file watcher and Vite's watcher have no\n * ordering contract. Component-only edits compare equal and stay in Fast\n * Refresh; route/name changes request an atomic table replacement.\n */\nexport async function pageRoutesNeedReplacement(\n changes: PageFileChanges,\n options: {\n vite: ViteDevServer;\n appSrcRoot: string;\n installedPages: readonly InstalledPageRoute[];\n },\n): Promise<boolean> {\n let replace = changes.added.length > 0 || changes.removed.length > 0;\n\n const installedByFile = new Map(\n options.installedPages.map((page) => [pathKey(page.file), page] as const),\n );\n\n for (const file of changes.inspectionNeeded) {\n // An error boundary has no installed router route to compare against. Its\n // namespace is projected into every client composition, so any edit must\n // replace the generated table; do not load it here just to inspect it.\n if (isErrorPageFilePath(file)) return true;\n\n options.vite.environments.ssr.moduleGraph.onFileChange(file);\n const pageModule = (await options.vite.ssrLoadModule(file)) as PageModuleShape;\n\n // A valid 404 page has no route identity. Its component body remains Vite\n // HMR territory; adding an illegal route export must still enter the\n // transaction so the installer can reject it without touching live routes.\n if (isNotFoundPageFile(file)) {\n if (pageModule.route !== undefined) replace = true;\n continue;\n }\n\n const installed = installedByFile.get(pathKey(file));\n if (installed === undefined) {\n replace = true;\n continue;\n }\n\n const next = resolvePageRouteIdentity(pageModule.route, file, options.appSrcRoot);\n if (next.declaredPath !== installed.declaredPath || next.name !== installed.name) {\n replace = true;\n }\n }\n\n return replace;\n}\n"],"mappings":";;;;;;AASA,SAAS,QAAQ,MAAsB;CACrC,MAAM,aAAa,KAAK,QAAQ,IAAI,EAAE,QAAQ,OAAO,GAAG;CAExD,OAAO,QAAQ,aAAa,UAAU,WAAW,YAAY,IAAI;AACnE;;;;;;AAOA,SAAgB,oBACd,QACA,YACU;CACV,MAAM,aAAa,KAAK,QAAQ,UAAU;CAE1C,OAAO,CACL,GAAG,IAAI,IACL,OACG,QAAQ,UAAU,MAAM,UAAU,MAAM,cAAc,CAAC,MAAM,WAAW,WAAW,IAAI,CAAC,EACxF,KAAK,UAAU,KAAK,QAAQ,YAAY,MAAM,UAAU,CAAC,CAC9D,CACF;AACF;;AAGA,SAAgB,qBAAqB,QAA8C;CACjF,OAAO,CACL,GAAG,IAAI,IACL,OACG,QAAQ,UAAU,MAAM,UAAU,MAAM,UAAU,EAClD,KAAK,UAAU,MAAM,UAAU,CACpC,CACF;AACF;;;;;;;AAQA,eAAsB,0BACpB,SACA,SAKkB;CAClB,IAAI,UAAU,QAAQ,MAAM,SAAS,KAAK,QAAQ,QAAQ,SAAS;CAEnE,MAAM,kBAAkB,IAAI,IAC1B,QAAQ,eAAe,KAAK,SAAS,CAAC,QAAQ,KAAK,IAAI,GAAG,IAAI,CAAU,CAC1E;CAEA,KAAK,MAAM,QAAQ,QAAQ,kBAAkB;EAI3C,IAAI,oBAAoB,IAAI,GAAG,OAAO;EAEtC,QAAQ,KAAK,aAAa,IAAI,YAAY,aAAa,IAAI;EAC3D,MAAM,aAAc,MAAM,QAAQ,KAAK,cAAc,IAAI;EAKzD,IAAI,mBAAmB,IAAI,GAAG;GAC5B,IAAI,WAAW,UAAU,QAAW,UAAU;GAC9C;EACF;EAEA,MAAM,YAAY,gBAAgB,IAAI,QAAQ,IAAI,CAAC;EACnD,IAAI,cAAc,QAAW;GAC3B,UAAU;GACV;EACF;EAEA,MAAM,OAAO,yBAAyB,WAAW,OAAO,MAAM,QAAQ,UAAU;EAChF,IAAI,KAAK,iBAAiB,UAAU,gBAAgB,KAAK,SAAS,UAAU,MAC1E,UAAU;CAEd;CAEA,OAAO;AACT"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
//#region web/src/server/register-production-public-files.ts
|
|
5
|
+
var InvalidProductionPublicFileError = class extends Error {
|
|
6
|
+
constructor(publicFile) {
|
|
7
|
+
super(`Cannot register production public file ${JSON.stringify(publicFile)}: the build manifest must contain a non-empty relative POSIX path with no traversal segments.`);
|
|
8
|
+
this.name = "InvalidProductionPublicFileError";
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
var MissingProductionPublicFileError = class extends Error {
|
|
12
|
+
constructor(publicFile, absoluteFile) {
|
|
13
|
+
super(`Cannot register production public file ${JSON.stringify(publicFile)}: the successful build manifest recorded it, but ${JSON.stringify(absoluteFile)} is missing or not a file. Run \`warlock build\` again to replace the incomplete artifact.`);
|
|
14
|
+
this.name = "MissingProductionPublicFileError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
function assertRelativePublicFile(publicFile) {
|
|
18
|
+
const segments = publicFile.split("/");
|
|
19
|
+
if (publicFile.length === 0 || publicFile.startsWith("/") || publicFile.includes("\\") || segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) throw new InvalidProductionPublicFileError(publicFile);
|
|
20
|
+
return segments;
|
|
21
|
+
}
|
|
22
|
+
/** Register the exact app-public files recorded by the successful build. */
|
|
23
|
+
function registerProductionPublicFiles(router, clientDir, publicFiles) {
|
|
24
|
+
const publicRoot = path.join(clientDir, "public");
|
|
25
|
+
for (const publicFile of publicFiles) {
|
|
26
|
+
const absoluteFile = path.join(publicRoot, ...assertRelativePublicFile(publicFile));
|
|
27
|
+
let stat;
|
|
28
|
+
try {
|
|
29
|
+
stat = fs.statSync(absoluteFile);
|
|
30
|
+
} catch {
|
|
31
|
+
throw new MissingProductionPublicFileError(publicFile, absoluteFile);
|
|
32
|
+
}
|
|
33
|
+
if (!stat.isFile()) throw new MissingProductionPublicFileError(publicFile, absoluteFile);
|
|
34
|
+
router.file(`/${publicFile}`, absoluteFile);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
export { registerProductionPublicFiles };
|
|
40
|
+
//# sourceMappingURL=register-production-public-files.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register-production-public-files.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/server/register-production-public-files.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type { Router } from \"@warlock.js/core\";\n\nexport class InvalidProductionPublicFileError extends Error {\n public constructor(publicFile: string) {\n super(\n `Cannot register production public file ${JSON.stringify(publicFile)}: ` +\n \"the build manifest must contain a non-empty relative POSIX path with no \" +\n \"traversal segments.\",\n );\n this.name = \"InvalidProductionPublicFileError\";\n }\n}\n\nexport class MissingProductionPublicFileError extends Error {\n public constructor(publicFile: string, absoluteFile: string) {\n super(\n `Cannot register production public file ${JSON.stringify(publicFile)}: ` +\n \"the successful build manifest recorded it, but \" +\n `${JSON.stringify(absoluteFile)} is missing or not a file. ` +\n \"Run `warlock build` again to replace the incomplete artifact.\",\n );\n this.name = \"MissingProductionPublicFileError\";\n }\n}\n\nfunction assertRelativePublicFile(publicFile: string): string[] {\n const segments = publicFile.split(\"/\");\n\n if (\n publicFile.length === 0 ||\n publicFile.startsWith(\"/\") ||\n publicFile.includes(\"\\\\\") ||\n segments.some((segment) => segment.length === 0 || segment === \".\" || segment === \"..\")\n ) {\n throw new InvalidProductionPublicFileError(publicFile);\n }\n\n return segments;\n}\n\n/** Register the exact app-public files recorded by the successful build. */\nexport function registerProductionPublicFiles(\n router: Router,\n clientDir: string,\n publicFiles: readonly string[],\n): void {\n const publicRoot = path.join(clientDir, \"public\");\n\n for (const publicFile of publicFiles) {\n const absoluteFile = path.join(publicRoot, ...assertRelativePublicFile(publicFile));\n\n let stat: fs.Stats;\n\n try {\n stat = fs.statSync(absoluteFile);\n } catch {\n throw new MissingProductionPublicFileError(publicFile, absoluteFile);\n }\n\n if (!stat.isFile()) {\n throw new MissingProductionPublicFileError(publicFile, absoluteFile);\n }\n\n router.file(`/${publicFile}`, absoluteFile);\n }\n}\n"],"mappings":";;;;AAIA,IAAa,mCAAb,cAAsD,MAAM;CAC1D,AAAO,YAAY,YAAoB;EACrC,MACE,0CAA0C,KAAK,UAAU,UAAU,EAAE,8FAGvE;EACA,KAAK,OAAO;CACd;AACF;AAEA,IAAa,mCAAb,cAAsD,MAAM;CAC1D,AAAO,YAAY,YAAoB,cAAsB;EAC3D,MACE,0CAA0C,KAAK,UAAU,UAAU,EAAE,mDAEhE,KAAK,UAAU,YAAY,EAAE,2FAEpC;EACA,KAAK,OAAO;CACd;AACF;AAEA,SAAS,yBAAyB,YAA8B;CAC9D,MAAM,WAAW,WAAW,MAAM,GAAG;CAErC,IACE,WAAW,WAAW,KACtB,WAAW,WAAW,GAAG,KACzB,WAAW,SAAS,IAAI,KACxB,SAAS,MAAM,YAAY,QAAQ,WAAW,KAAK,YAAY,OAAO,YAAY,IAAI,GAEtF,MAAM,IAAI,iCAAiC,UAAU;CAGvD,OAAO;AACT;;AAGA,SAAgB,8BACd,QACA,WACA,aACM;CACN,MAAM,aAAa,KAAK,KAAK,WAAW,QAAQ;CAEhD,KAAK,MAAM,cAAc,aAAa;EACpC,MAAM,eAAe,KAAK,KAAK,YAAY,GAAG,yBAAyB,UAAU,CAAC;EAElF,IAAI;EAEJ,IAAI;GACF,OAAO,GAAG,SAAS,YAAY;EACjC,QAAQ;GACN,MAAM,IAAI,iCAAiC,YAAY,YAAY;EACrE;EAEA,IAAI,CAAC,KAAK,OAAO,GACf,MAAM,IAAI,iCAAiC,YAAY,YAAY;EAGrE,OAAO,KAAK,IAAI,cAAc,YAAY;CAC5C;AACF"}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { PAYLOAD_SCRIPT_ID, escapePayload } from "../components/document-context.mjs";
|
|
2
|
-
import { BufferedCookie } from "./
|
|
2
|
+
import { BufferedCookie } from "./settle-page-response.mjs";
|
|
3
3
|
import { ExecutePageRequestOptions, PageDataBundle, PageRouteEntry } from "./execute-page-request.types.mjs";
|
|
4
|
-
|
|
4
|
+
import { ErrorPageModuleLoader } from "./error-page.mjs";
|
|
5
|
+
import { Request, Response } from "@warlock.js/core";
|
|
6
|
+
|
|
7
|
+
//#region web/src/server/render-page.d.ts
|
|
5
8
|
/**
|
|
6
9
|
* Pipeline stages 9–10: RENDER the page tree from the
|
|
7
10
|
* data bundle stages 1–8 produced, then return finalized { html, status,
|
|
8
|
-
* headers
|
|
11
|
+
* headers }. Stage 10 happens at the CALL SITE in two halves —
|
|
9
12
|
* 10a the caller applies status + headers (the single live-response write,
|
|
10
13
|
* after render, before anything flushes), 10b it flushes
|
|
11
14
|
* the document. Nothing in this module writes the live response. It never
|
|
@@ -39,7 +42,8 @@ type RenderPageOptions = {
|
|
|
39
42
|
*/
|
|
40
43
|
as?: unknown; /** Per-call overrides of the connected registry (tests, mostly). */
|
|
41
44
|
routes?: readonly PageRouteEntry[];
|
|
42
|
-
createHttp?: ExecutePageRequestOptions["createHttp"];
|
|
45
|
+
createHttp?: ExecutePageRequestOptions["createHttp"]; /** Loaded only after the ordinary boundary chain has been exhausted. */
|
|
46
|
+
loadErrorPage?: ErrorPageModuleLoader;
|
|
43
47
|
};
|
|
44
48
|
/**
|
|
45
49
|
* `renderPageRequest` takes the URL itself, so `params`/`query` (the
|
|
@@ -50,14 +54,7 @@ type RenderPageRequestOptions = Omit<RenderPageOptions, "params" | "query">;
|
|
|
50
54
|
type RenderedPage = {
|
|
51
55
|
/** The full document ("" when the pipeline short-circuited before render). */html: string;
|
|
52
56
|
status: number; /** Committed response headers, lowercased key → value. */
|
|
53
|
-
headers: Record<string, string>;
|
|
54
|
-
/**
|
|
55
|
-
* Committed cookies in commit order, attribute-faithful: each entry carries
|
|
56
|
-
* the loader's raw value (pre-serialization) AND its options
|
|
57
|
-
* (`httpOnly`/`secure`/`sameSite`/`path`/`expires`/…). Never flattened to a
|
|
58
|
-
* name→value map — a map cannot express the attributes, and a Set-Cookie
|
|
59
|
-
* built without them is a security defect, not a convenience.
|
|
60
|
-
*/
|
|
57
|
+
headers: Record<string, string>; /** Committed response cookies, in commit order — stage 7's `bundle.commit.cookies`. */
|
|
61
58
|
cookies: BufferedCookie[];
|
|
62
59
|
/**
|
|
63
60
|
* The PAGE loader's data — `data.product.name` reads as the dx story
|
|
@@ -72,7 +69,7 @@ type RenderedPage = {
|
|
|
72
69
|
*/
|
|
73
70
|
bundle: PageDataBundle | undefined;
|
|
74
71
|
};
|
|
75
|
-
declare function renderPage(routeName: string, options?: RenderPageOptions): Promise<RenderedPage>;
|
|
72
|
+
declare function renderPage(routeName: string, options?: RenderPageOptions): Promise<RenderedPage | Response>;
|
|
76
73
|
/**
|
|
77
74
|
* The URL-based sibling of `renderPage` — the production render surface: a
|
|
78
75
|
* real HTTP server has a URL, not a route name. The url goes STRAIGHT to
|
|
@@ -83,7 +80,7 @@ declare function renderPage(routeName: string, options?: RenderPageOptions): Pro
|
|
|
83
80
|
* URL matching no route is a legitimate 404, and a server must ANSWER it —
|
|
84
81
|
* `{ html: "", status: 404 }` with an undefined `bundle` (see RenderedPage).
|
|
85
82
|
*/
|
|
86
|
-
declare function renderPageRequest(url: string, options?: RenderPageRequestOptions): Promise<RenderedPage>;
|
|
83
|
+
declare function renderPageRequest(url: string, options?: RenderPageRequestOptions): Promise<RenderedPage | Response>;
|
|
87
84
|
//#endregion
|
|
88
85
|
export { PageRoutesRegistry, RenderPageOptions, RenderPageRequestOptions, RenderedPage, connectPageRoutes, renderPage, renderPageRequest };
|
|
89
86
|
//# sourceMappingURL=render-page.d.mts.map
|
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import { DocumentContext, PAYLOAD_SCRIPT_ID, escapePayload } from "../components/document-context.mjs";
|
|
2
|
+
import { markNonHydrating } from "./page-render-bundle.mjs";
|
|
3
|
+
import { registerModules } from "../runtime/register-modules.mjs";
|
|
2
4
|
import DefaultApp from "../components/default-app.mjs";
|
|
3
5
|
import { buildHydrationPayload } from "./build-hydration-payload.mjs";
|
|
6
|
+
import { ERROR_PAGE_METADATA } from "./resolve-page-metadata.mjs";
|
|
7
|
+
import { hydrationErrorPageProps, resolveErrorPageMetadata } from "./error-page.mjs";
|
|
4
8
|
import { buildErrorRecord, designateBoundary } from "./settle-page-response.mjs";
|
|
5
9
|
import { executePageRequest } from "./execute-page-request.mjs";
|
|
6
10
|
import { createElement } from "react";
|
|
11
|
+
import { Response } from "@warlock.js/core";
|
|
7
12
|
|
|
8
|
-
//#region
|
|
13
|
+
//#region web/src/server/render-page.ts
|
|
14
|
+
/** Reads the stage 7 commit into the lowercased header map `RenderedPage` carries. */
|
|
15
|
+
function committedHeaders(bundle) {
|
|
16
|
+
const headers = {};
|
|
17
|
+
for (const header of bundle.commit?.headers ?? []) headers[header.key.toLowerCase()] = header.value;
|
|
18
|
+
return headers;
|
|
19
|
+
}
|
|
20
|
+
/** Reads the stage 7 commit into the cookie list `RenderedPage` carries. */
|
|
21
|
+
function committedCookies(bundle) {
|
|
22
|
+
return bundle.commit?.cookies ?? [];
|
|
23
|
+
}
|
|
9
24
|
let pageRoutesRegistry;
|
|
10
25
|
/**
|
|
11
26
|
* Boot-time wiring so `renderPage(name, options)` can resolve a route NAME
|
|
@@ -46,6 +61,11 @@ function buildUrl(entry, params, query) {
|
|
|
46
61
|
function FrameworkRootBoundary() {
|
|
47
62
|
return createElement("main", { role: "alert" }, "Something went wrong.");
|
|
48
63
|
}
|
|
64
|
+
function errorPageElement(module, props) {
|
|
65
|
+
const ErrorPage = module.default;
|
|
66
|
+
if (!ErrorPage) throw new Error("The application error.page.tsx module has no default export.");
|
|
67
|
+
return createElement(ErrorPage, props);
|
|
68
|
+
}
|
|
49
69
|
const DATA_KEYS = {
|
|
50
70
|
app: "appData",
|
|
51
71
|
layout: "layoutData",
|
|
@@ -63,7 +83,7 @@ const DATA_KEYS = {
|
|
|
63
83
|
* with no DOM.
|
|
64
84
|
*/
|
|
65
85
|
function buildPageElement(triple, bundle) {
|
|
66
|
-
return wrapRootward(triple, bundle, "page", buildLeaf(triple.page, bundle
|
|
86
|
+
return wrapRootward(triple, bundle, "page", buildLeaf(triple.page, bundle));
|
|
67
87
|
}
|
|
68
88
|
/**
|
|
69
89
|
* The error path renders the DESIGNATED boundary in place of the level it
|
|
@@ -81,12 +101,13 @@ function buildBoundaryElement(triple, bundle, record) {
|
|
|
81
101
|
const wrapped = wrapRootward(triple, bundle, boundary.boundaryLevel, element);
|
|
82
102
|
return boundary.boundaryLevel === "app" ? createElement(DefaultApp, { children: wrapped }) : wrapped;
|
|
83
103
|
}
|
|
84
|
-
function buildLeaf(module, bundle
|
|
104
|
+
function buildLeaf(module, bundle) {
|
|
85
105
|
const Component = module.default;
|
|
86
106
|
if (!Component) return null;
|
|
87
107
|
return createElement(Component, {
|
|
88
|
-
data: bundle
|
|
89
|
-
shared: bundle.shared
|
|
108
|
+
data: bundle.pageData,
|
|
109
|
+
shared: bundle.shared,
|
|
110
|
+
params: bundle.route.params
|
|
90
111
|
});
|
|
91
112
|
}
|
|
92
113
|
function wrapRootward(triple, bundle, from, leaf) {
|
|
@@ -131,20 +152,12 @@ function capturingCreateHttp(registry, as) {
|
|
|
131
152
|
createHttp(match) {
|
|
132
153
|
state.match = match;
|
|
133
154
|
state.captured = registry.createHttp(match);
|
|
134
|
-
if (as
|
|
155
|
+
if (as != null) state.captured.request.user = as;
|
|
135
156
|
return state.captured;
|
|
136
157
|
}
|
|
137
158
|
};
|
|
138
159
|
}
|
|
139
|
-
/**
|
|
140
|
-
* Structural read of the two core `Request` fields the slots need. Neither
|
|
141
|
-
* `nonce` nor `locale` is declared on `PipelineRequest`/`WebRequest` (the
|
|
142
|
-
* loader-facing facade only declares `validated`/`input`/`user`,
|
|
143
|
-
* context.ts:30-68) — a narrow typed intersection at the one call site that
|
|
144
|
-
* needs it, the same pattern `execute-page-request.ts` uses for its own
|
|
145
|
-
* `(response as PipelineResponse & { statusCode?: number })` read
|
|
146
|
-
* (execute-page-request.ts:517).
|
|
147
|
-
*/
|
|
160
|
+
/** Reads document slots directly from core's Request. */
|
|
148
161
|
function documentSlotsFrom(captured) {
|
|
149
162
|
const request = captured?.request;
|
|
150
163
|
return {
|
|
@@ -152,13 +165,12 @@ function documentSlotsFrom(captured) {
|
|
|
152
165
|
lang: request?.locale
|
|
153
166
|
};
|
|
154
167
|
}
|
|
155
|
-
async function finishRender(triple, bundle, documentSlots) {
|
|
156
|
-
const headers =
|
|
157
|
-
|
|
158
|
-
const cookies = bundle.commit?.cookies ?? [];
|
|
168
|
+
async function finishRender(triple, bundle, documentSlots, response, loadErrorPage) {
|
|
169
|
+
const headers = committedHeaders(bundle);
|
|
170
|
+
const cookies = committedCookies(bundle);
|
|
159
171
|
if (bundle.shortCircuit) return {
|
|
160
172
|
html: "",
|
|
161
|
-
status: bundle.shortCircuit.stage === "validation" ? bundle.shortCircuit.status : bundle.shortCircuit.
|
|
173
|
+
status: bundle.shortCircuit.stage === "validation" ? bundle.shortCircuit.status : bundle.shortCircuit.statusCode ?? 200,
|
|
162
174
|
headers,
|
|
163
175
|
cookies,
|
|
164
176
|
data: bundle.pageData,
|
|
@@ -166,7 +178,7 @@ async function finishRender(triple, bundle, documentSlots) {
|
|
|
166
178
|
};
|
|
167
179
|
if (headers["cache-control"] === void 0) headers["cache-control"] = "private";
|
|
168
180
|
const { renderToString } = await import("react-dom/server");
|
|
169
|
-
|
|
181
|
+
let documentValue = {
|
|
170
182
|
metadata: bundle.metadata,
|
|
171
183
|
payload: buildHydrationPayload(bundle),
|
|
172
184
|
nonce: documentSlots.nonce,
|
|
@@ -177,20 +189,58 @@ async function finishRender(triple, bundle, documentSlots) {
|
|
|
177
189
|
children: element
|
|
178
190
|
}));
|
|
179
191
|
let currentError = bundle.error;
|
|
180
|
-
let renderTimeThrow = false;
|
|
181
192
|
let body;
|
|
193
|
+
const renderFrameworkRoot = () => renderWithContext(createElement(DefaultApp, { children: createElement(FrameworkRootBoundary, {}) }));
|
|
194
|
+
const renderFrameworkAfterErrorPageFailure = () => {
|
|
195
|
+
bundle.errorPage = void 0;
|
|
196
|
+
bundle.metadata = ERROR_PAGE_METADATA;
|
|
197
|
+
documentValue = {
|
|
198
|
+
...documentValue,
|
|
199
|
+
metadata: bundle.metadata,
|
|
200
|
+
payload: buildHydrationPayload(bundle)
|
|
201
|
+
};
|
|
202
|
+
return renderFrameworkRoot();
|
|
203
|
+
};
|
|
204
|
+
const renderErrorPage = async (thrown, serializableError = thrown) => {
|
|
205
|
+
if (!loadErrorPage) return void 0;
|
|
206
|
+
const props = {
|
|
207
|
+
error: thrown,
|
|
208
|
+
status: 500
|
|
209
|
+
};
|
|
210
|
+
const module = await loadErrorPage();
|
|
211
|
+
registerModules([module]);
|
|
212
|
+
bundle.errorPage = hydrationErrorPageProps(props, serializableError);
|
|
213
|
+
bundle.metadata = resolveErrorPageMetadata(module, props);
|
|
214
|
+
documentValue = {
|
|
215
|
+
...documentValue,
|
|
216
|
+
metadata: bundle.metadata,
|
|
217
|
+
payload: buildHydrationPayload(bundle)
|
|
218
|
+
};
|
|
219
|
+
return renderWithContext(wrapRootward(triple, bundle, "page", errorPageElement(module, props)));
|
|
220
|
+
};
|
|
182
221
|
for (;;) try {
|
|
222
|
+
if (currentError?.boundary.boundaryLevel === "app" && !triple.app.ErrorBoundary) {
|
|
223
|
+
try {
|
|
224
|
+
body = await renderErrorPage(currentError.originalError ?? currentError.error, currentError.error) ?? renderFrameworkRoot();
|
|
225
|
+
} catch {
|
|
226
|
+
body = renderFrameworkAfterErrorPageFailure();
|
|
227
|
+
}
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
183
230
|
body = renderWithContext(currentError ? buildBoundaryElement(triple, bundle, currentError) : buildPageElement(triple, bundle));
|
|
184
231
|
break;
|
|
185
232
|
} catch (thrown) {
|
|
186
|
-
renderTimeThrow = true;
|
|
187
233
|
if (currentError?.boundary.boundaryLevel === "app") {
|
|
188
|
-
|
|
234
|
+
try {
|
|
235
|
+
body = await renderErrorPage(thrown) ?? renderFrameworkRoot();
|
|
236
|
+
} catch {
|
|
237
|
+
body = renderFrameworkAfterErrorPageFailure();
|
|
238
|
+
}
|
|
189
239
|
break;
|
|
190
240
|
}
|
|
191
241
|
currentError = buildErrorRecord(thrown, designateBoundary(currentError?.boundary.boundaryLevel === "layout" ? "app" : currentError ? "layout" : "page", triple));
|
|
192
242
|
}
|
|
193
|
-
const status =
|
|
243
|
+
const status = currentError ? 500 : bundle.commit?.statusCode ?? 200;
|
|
194
244
|
return {
|
|
195
245
|
html: emitDocument(body),
|
|
196
246
|
status,
|
|
@@ -200,6 +250,82 @@ async function finishRender(triple, bundle, documentSlots) {
|
|
|
200
250
|
bundle
|
|
201
251
|
};
|
|
202
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* Render failures that happen before the page pipeline has a triple (notably a
|
|
255
|
+
* module-load or registration throw). This deliberately owns one terminal
|
|
256
|
+
* attempt: an error-page failure falls straight to FrameworkRootBoundary.
|
|
257
|
+
*
|
|
258
|
+
* There is no triple yet, so there is no trustworthy server composition for
|
|
259
|
+
* the browser to hydrate against — every response this function produces is
|
|
260
|
+
* marked `markNonHydrating` (page-render-bundle.ts), on both the bundle and
|
|
261
|
+
* the document payload, whether or not it managed to render the app's own
|
|
262
|
+
* `error.page.tsx`. A normal app error page reached through `finishRender`
|
|
263
|
+
* renders inside a real triple and stays hydratable; this path never does.
|
|
264
|
+
*/
|
|
265
|
+
async function renderPageFailure(options) {
|
|
266
|
+
const { request, response, name, path, thrown, loadErrorPage } = options;
|
|
267
|
+
const bundle = markNonHydrating({ route: {
|
|
268
|
+
name,
|
|
269
|
+
path,
|
|
270
|
+
params: {},
|
|
271
|
+
query: {}
|
|
272
|
+
} });
|
|
273
|
+
const headers = { "cache-control": "private" };
|
|
274
|
+
const { renderToString } = await import("react-dom/server");
|
|
275
|
+
const slots = documentSlotsFrom({
|
|
276
|
+
request,
|
|
277
|
+
response
|
|
278
|
+
});
|
|
279
|
+
const frameworkPayload = markNonHydrating(buildHydrationPayload(bundle));
|
|
280
|
+
let value = {
|
|
281
|
+
metadata: void 0,
|
|
282
|
+
payload: frameworkPayload,
|
|
283
|
+
nonce: slots.nonce,
|
|
284
|
+
lang: slots.lang
|
|
285
|
+
};
|
|
286
|
+
const renderWithContext = (element) => renderToString(createElement(DocumentContext.Provider, {
|
|
287
|
+
value,
|
|
288
|
+
children: element
|
|
289
|
+
}));
|
|
290
|
+
let body;
|
|
291
|
+
try {
|
|
292
|
+
if (!loadErrorPage) throw new Error("No application error page is configured.");
|
|
293
|
+
const props = {
|
|
294
|
+
error: thrown,
|
|
295
|
+
status: 500
|
|
296
|
+
};
|
|
297
|
+
const module = await loadErrorPage();
|
|
298
|
+
registerModules([module]);
|
|
299
|
+
const errorPage = hydrationErrorPageProps(props);
|
|
300
|
+
bundle.errorPage = errorPage;
|
|
301
|
+
value = {
|
|
302
|
+
...value,
|
|
303
|
+
metadata: resolveErrorPageMetadata(module, props),
|
|
304
|
+
payload: markNonHydrating({
|
|
305
|
+
...frameworkPayload,
|
|
306
|
+
errorPage
|
|
307
|
+
})
|
|
308
|
+
};
|
|
309
|
+
body = renderWithContext(createElement(DefaultApp, { children: errorPageElement(module, props) }));
|
|
310
|
+
} catch {
|
|
311
|
+
bundle.errorPage = void 0;
|
|
312
|
+
bundle.metadata = ERROR_PAGE_METADATA;
|
|
313
|
+
value = {
|
|
314
|
+
...value,
|
|
315
|
+
metadata: bundle.metadata,
|
|
316
|
+
payload: markNonHydrating(buildHydrationPayload(bundle))
|
|
317
|
+
};
|
|
318
|
+
body = renderWithContext(createElement(DefaultApp, { children: createElement(FrameworkRootBoundary, {}) }));
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
html: emitDocument(body),
|
|
322
|
+
status: 500,
|
|
323
|
+
headers,
|
|
324
|
+
cookies: [],
|
|
325
|
+
data: void 0,
|
|
326
|
+
bundle
|
|
327
|
+
};
|
|
328
|
+
}
|
|
203
329
|
async function renderPage(routeName, options = {}) {
|
|
204
330
|
const registry = requireRegistry(options);
|
|
205
331
|
const entry = registry.routes.find((candidate) => candidate.name === routeName);
|
|
@@ -213,7 +339,7 @@ async function renderPage(routeName, options = {}) {
|
|
|
213
339
|
url,
|
|
214
340
|
routes: registry.routes,
|
|
215
341
|
createHttp,
|
|
216
|
-
finish: (bundle) => finishRender(entry.triple, bundle, documentSlotsFrom(state.captured))
|
|
342
|
+
finish: (bundle) => finishRender(entry.triple, bundle, documentSlotsFrom(state.captured), state.captured.response, options.loadErrorPage)
|
|
217
343
|
});
|
|
218
344
|
if (!rendered) throw new Error(`renderPage("${routeName}"): the built URL "${url}" did not match stage 1 (web/src/server/render-page.ts). The name resolved but the matcher disagreed — that is a manifest bug, not a caller bug.`);
|
|
219
345
|
return rendered;
|
|
@@ -235,7 +361,7 @@ async function renderPageRequest(url, options = {}) {
|
|
|
235
361
|
url,
|
|
236
362
|
routes: registry.routes,
|
|
237
363
|
createHttp,
|
|
238
|
-
finish: (bundle) => finishRender(state.match.entry.triple, bundle, documentSlotsFrom(state.captured))
|
|
364
|
+
finish: (bundle) => finishRender(state.match.entry.triple, bundle, documentSlotsFrom(state.captured), state.captured.response, options.loadErrorPage)
|
|
239
365
|
});
|
|
240
366
|
if (!rendered) return {
|
|
241
367
|
html: "",
|
|
@@ -249,5 +375,5 @@ async function renderPageRequest(url, options = {}) {
|
|
|
249
375
|
}
|
|
250
376
|
|
|
251
377
|
//#endregion
|
|
252
|
-
export { connectPageRoutes, renderPage, renderPageRequest };
|
|
378
|
+
export { connectPageRoutes, renderPage, renderPageFailure, renderPageRequest };
|
|
253
379
|
//# sourceMappingURL=render-page.mjs.map
|