@warlock.js/web 5.0.0 → 5.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.
Files changed (38) hide show
  1. package/esm/build/contribution.mjs +1 -1
  2. package/esm/build/discover-pages.mjs +21 -2
  3. package/esm/build/discover-pages.mjs.map +1 -1
  4. package/esm/build/generate-pages-barrel.mjs +1 -1
  5. package/esm/components/document-context.d.mts +7 -1
  6. package/esm/connector/index.mjs +1 -1
  7. package/esm/metadata.d.mts +1 -1
  8. package/esm/routing/compose-route-path.d.mts +29 -0
  9. package/esm/server/buffered-response.d.mts +58 -0
  10. package/esm/server/create-page-module-loader.d.mts +36 -0
  11. package/esm/server/create-page-route-handler.d.mts +31 -0
  12. package/esm/server/execute-page-request.d.mts +7 -1
  13. package/esm/server/execute-page-request.mjs +1 -1
  14. package/esm/server/execute-page-request.types.d.mts +174 -1
  15. package/esm/server/hydration-client-url.mjs +1 -1
  16. package/esm/server/index.d.mts +13 -0
  17. package/esm/server/index.mjs +6 -6
  18. package/esm/server/install-page-routes-from-manifest.d.mts +44 -0
  19. package/esm/server/install-page-routes-from-manifest.mjs +1 -1
  20. package/esm/server/install-page-routes.d.mts +59 -1
  21. package/esm/server/install-page-routes.mjs +149 -3
  22. package/esm/server/install-page-routes.mjs.map +1 -0
  23. package/esm/server/page-context.d.mts +14 -1
  24. package/esm/server/page-context.mjs +11 -1
  25. package/esm/server/page-context.mjs.map +1 -1
  26. package/esm/server/render-page.d.mts +89 -0
  27. package/esm/server/render-page.mjs +40 -1
  28. package/esm/server/render-page.mjs.map +1 -1
  29. package/esm/server/stylesheet-urls.d.mts +52 -0
  30. package/esm/server/stylesheet-urls.mjs +64 -2
  31. package/esm/server/stylesheet-urls.mjs.map +1 -1
  32. package/esm/server/web-connector.d.mts +1 -1
  33. package/esm/server/web-connector.mjs +2 -2
  34. package/esm/shared.d.mts +25 -1
  35. package/esm/vite/build-client.mjs +1 -1
  36. package/esm/vite/gate-a-resolve.mjs +2 -2
  37. package/esm/vite/hydration-entries.mjs +1 -1
  38. package/package.json +10 -4
@@ -1,5 +1,5 @@
1
- import { readFileSync } from "node:fs";
2
1
  import path from "node:path";
2
+ import { readFileSync } from "node:fs";
3
3
 
4
4
  //#region ../web/src/build/contribution.ts
5
5
  /**
@@ -2,8 +2,8 @@ import { composeRoutePath } from "../routing/compose-route-path.mjs";
2
2
  import { NestedLayoutsNotSupportedError, selectPageLayout } from "../routing/layout-policy.mjs";
3
3
  import { deriveFallbackRouteName } from "../routing/route-identity.mjs";
4
4
  import { NonLiteralRouteExportError, readRouteExports } from "./read-route-exports.mjs";
5
- import fs from "node:fs";
6
5
  import path from "node:path";
6
+ import fs from "node:fs";
7
7
  import { parse } from "@babel/parser";
8
8
 
9
9
  //#region ../web/src/build/discover-pages.ts
@@ -93,6 +93,25 @@ function discoverWebRoots(srcRoot) {
93
93
  function byName(left, right) {
94
94
  return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
95
95
  }
96
+ /**
97
+ * The subject list: every `*.page.tsx` under BOTH web roots, one call for the
98
+ * whole graph.
99
+ *
100
+ * Unlike {@link discoverPages}, this reads no `route` or `prefix` export and
101
+ * throws on nothing — it answers only "which page files exist", so a provider
102
+ * that still resolves a page's route by its own means (dev's
103
+ * `ssrLoadModule`-driven installer, chiefly) can share the walk without
104
+ * inheriting the static-parsing refusals that answering "what route is this"
105
+ * requires.
106
+ */
107
+ function discoverPageFiles(srcRoot) {
108
+ const found = [];
109
+ for (const webRoot of discoverWebRoots(srcRoot)) for (const pageFile of walkFiles(webRoot, (fileName) => fileName.endsWith(".page.tsx"))) found.push({
110
+ pageFile,
111
+ webRoot
112
+ });
113
+ return found;
114
+ }
96
115
  /** Every file under `dir` (recursive) whose name matches `predicate`. */
97
116
  function walkFiles(dir, predicate) {
98
117
  const found = [];
@@ -290,5 +309,5 @@ function discoverPages(options) {
290
309
  }
291
310
 
292
311
  //#endregion
293
- export { DuplicatePageRouteNameError, discoverPages, discoverWebRoots, isFile, layoutChainFor, toPosix, walkFiles };
312
+ export { DuplicatePageRouteNameError, discoverPageFiles, discoverPages, discoverWebRoots, isFile, layoutChainFor, toPosix, walkFiles };
294
313
  //# sourceMappingURL=discover-pages.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"discover-pages.mjs","names":[],"sources":["../../../../../../../web/src/build/discover-pages.ts"],"sourcesContent":["/**\n * Static discovery of the application's page graph — the ONE scan every\n * provider shares.\n *\n * This module has a single responsibility: look at the filesystem and produce\n * the recipe. It writes nothing, knows nothing about the generated barrel, and\n * imports neither Vite nor a single application module. Everything that turns\n * the recipe into an artefact (the production barrel, the dev plugin, the\n * generated client entry) lives with that artefact and consumes\n * {@link discoverPages}.\n *\n * The reason it is one module rather than one function per consumer: two\n * scanners that agree today drift tomorrow, and a page that exists for the\n * server but not the client is the silent failure that costs a day to find.\n * Sharing the scan makes that disagreement impossible by construction instead\n * of catchable by test.\n *\n * \"Static\" means WITHOUT RUNNING THE APPLICATION — this module globs\n * `*.page.tsx`, `layout.tsx` and `root.tsx`, and reads each page's declared\n * `route` and each layout's declared `prefix` by PARSING the source\n * ({@link readRouteExports}). It still imports no application module.\n *\n * The route a page is served under is the one the page DECLARES, not the one\n * its directory suggests, so that is the route discovery reports. The\n * composition — EVERY layout `prefix` on the page's path, outermost first, plus\n * the page's own `route` path — and the fallback used when a route omits its\n * name are mirrored from the server's installer (`installPageRoutes`),\n * deliberately and in one direction: build and boot agree because one of them\n * copies the other, not because two conventions were written to match.\n *\n * Discovery also CLASSIFIES each layout — does its module have a default\n * export, does it export `middleware` — because the layout policy\n * ({@link \"../routing/layout-policy.ts\"}) owns the rule but may not touch a\n * filesystem to learn the facts the rule needs. That classification is another\n * parse, never an import: a layout is read exactly the way a page's `route` is.\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { parse } from \"@babel/parser\";\nimport { composeRoutePath } from \"../routing/compose-route-path\";\nimport { NestedLayoutsNotSupportedError, selectPageLayout } from \"../routing/layout-policy\";\nimport { deriveFallbackRouteName } from \"../routing/route-identity\";\nimport type { RouteExportsReadResult } from \"./read-route-exports\";\nimport { NonLiteralRouteExportError, readRouteExports } from \"./read-route-exports\";\n\nexport type DiscoverPagesOptions = {\n /** Absolute path to the application root (where `package.json` lives). */\n appRoot: string;\n /** Source directory name under `appRoot`; defaults to `\"src\"`. */\n srcDir?: string;\n};\n\nexport type DiscoveredPage = {\n /**\n * The page's route name: the `name` its `route` export declares, or the\n * server's own fallback derivation — `<module>.<declared path with dots>` for\n * a page in a module's web tree, the dotted path alone for one in the global\n * tree, and `index` when neither has anything to say.\n *\n * Unique across the whole graph — {@link discoverPages} refuses to return a\n * result where it is not.\n */\n routeName: string;\n /**\n * The page's EFFECTIVE route path: the declared `prefix` of EVERY layout on\n * its path — outermost first, wherever in the ancestry each lives — composed\n * in order with the page's own declared `route` path, which is the path the\n * server registers it under. A layout that declares no `prefix` contributes\n * nothing. Always `/`-prefixed.\n *\n * Every layout, not just the rendering one: a `prefix`-only layout is a real\n * segment of the URL its subtree lives under, and skipping it would serve the\n * subtree from a path nobody wrote down.\n */\n routePath: string;\n /** Absolute path to the `*.page.tsx` file. */\n pageFile: string;\n /** The web root this page was found under — the root its layout chain climbs to. */\n webRoot: string;\n /** Absolute paths of every `layout.tsx` from the web root down to the page's own directory, OUTERMOST FIRST. */\n layouts: string[];\n /**\n * The page's middleware chain: the subset of {@link layouts} whose modules\n * export `middleware`, OUTERMOST FIRST — the order they must run in.\n *\n * Source files rather than the middleware values themselves, because\n * discovery never runs application code: this says WHICH layouts contribute a\n * guard, and the consumer that already loads those modules reads the values\n * off them.\n *\n * Empty when no layout on the path declares middleware, which is the common\n * case; a page with no layouts always has an empty chain.\n */\n middlewareLayouts: string[];\n /** Absolute path to the global `root.tsx` every page renders inside, when it exists. */\n appFile?: string;\n};\n\n/** Raised when two pages claim one route name. */\nexport class DuplicatePageRouteNameError extends Error {\n public constructor(\n public readonly routeName: string,\n public readonly firstFile: string,\n public readonly secondFile: string,\n ) {\n super(\n `Two pages resolve to the same route name \"${routeName}\": \"${firstFile}\" and ` +\n `\"${secondFile}\". A route name identifies exactly one page, so the second ` +\n \"page would be unreachable. To fix: rename one of the files, or move it so \" +\n \"its directory gives it a different route name.\",\n );\n this.name = \"DuplicatePageRouteNameError\";\n }\n}\n\nexport function toPosix(value: string): string {\n return value.replace(/\\\\/g, \"/\");\n}\n\nfunction isDirectory(candidate: string): boolean {\n try {\n return fs.statSync(candidate).isDirectory();\n } catch {\n return false;\n }\n}\n\nexport function isFile(candidate: string): boolean {\n try {\n return fs.statSync(candidate).isFile();\n } catch {\n return false;\n }\n}\n\n/**\n * The two page roots: the global `src/web/**` tree and each\n * module's `src/app/<module>/web/**` tree. Both are optional; a project with\n * neither has zero pages, which is a legal empty state.\n */\nexport function discoverWebRoots(srcRoot: string): string[] {\n const roots: string[] = [];\n const globalRoot = path.join(srcRoot, \"web\");\n\n if (isDirectory(globalRoot)) {\n roots.push(globalRoot);\n }\n\n const appDir = path.join(srcRoot, \"app\");\n\n if (isDirectory(appDir)) {\n for (const entry of fs.readdirSync(appDir, { withFileTypes: true }).sort(byName)) {\n if (!entry.isDirectory()) continue;\n\n const moduleWebRoot = path.join(appDir, entry.name, \"web\");\n\n if (isDirectory(moduleWebRoot)) {\n roots.push(moduleWebRoot);\n }\n }\n }\n\n return roots;\n}\n\nfunction byName(left: { name: string }, right: { name: string }): number {\n return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;\n}\n\nexport type DiscoveredPageFile = {\n /** Absolute path to the `*.page.tsx` file. */\n pageFile: string;\n /** The web root ({@link discoverWebRoots}) this page was found under. */\n webRoot: string;\n};\n\n/**\n * The subject list: every `*.page.tsx` under BOTH web roots, one call for the\n * whole graph.\n *\n * Unlike {@link discoverPages}, this reads no `route` or `prefix` export and\n * throws on nothing — it answers only \"which page files exist\", so a provider\n * that still resolves a page's route by its own means (dev's\n * `ssrLoadModule`-driven installer, chiefly) can share the walk without\n * inheriting the static-parsing refusals that answering \"what route is this\"\n * requires.\n */\nexport function discoverPageFiles(srcRoot: string): DiscoveredPageFile[] {\n const found: DiscoveredPageFile[] = [];\n\n for (const webRoot of discoverWebRoots(srcRoot)) {\n for (const pageFile of walkFiles(webRoot, (fileName) => fileName.endsWith(\".page.tsx\"))) {\n found.push({ pageFile, webRoot });\n }\n }\n\n return found;\n}\n\n/** Every file under `dir` (recursive) whose name matches `predicate`. */\nexport function walkFiles(dir: string, predicate: (fileName: string) => boolean): string[] {\n const found: string[] = [];\n\n for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort(byName)) {\n const full = path.join(dir, entry.name);\n\n if (entry.isDirectory()) {\n found.push(...walkFiles(full, predicate));\n } else if (entry.isFile() && predicate(entry.name)) {\n found.push(full);\n }\n }\n\n return found;\n}\n\n/**\n * A page's layout chain: every `layout.tsx` in the directories from its web\n * root down to its own directory, OUTERMOST FIRST.\n *\n * Dev's installer reads only the nearest layout today, because its page module\n * triple holds a single layout; the recipe carries the whole chain so the\n * runtime side can compose\n * nested layouts without a second discovery pass. The nearest layout is always\n * the LAST element, so a consumer that still wants dev's one-layout behaviour\n * reads `layouts.at(-1)`.\n */\nexport function layoutChainFor(pageFile: string, webRoot: string): string[] {\n const chain: string[] = [];\n const relativeDir = path.relative(webRoot, path.dirname(pageFile));\n const segments = relativeDir === \"\" ? [] : relativeDir.split(path.sep);\n\n let current = webRoot;\n\n for (let index = 0; index <= segments.length; index++) {\n if (index > 0) {\n current = path.join(current, segments[index - 1]);\n }\n\n const candidate = path.join(current, \"layout.tsx\");\n\n if (isFile(candidate)) {\n chain.push(candidate);\n }\n }\n\n return chain;\n}\n\nfunction compareStrings(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n\n/**\n * The total order every consumer sees, and the reason discovery returns an\n * array rather than a set: the page's SOURCE FILE PATH, lexicographic, POSIX.\n *\n * Serialization order only; matching precedence is a property of the route\n * grammar, not of this array.\n *\n * The file path rather than the route path, now that the route is the declared\n * one: a page's route can be rewritten by editing one line, which would reorder\n * an artefact that has not otherwise changed, while the file it lives in is the\n * stable identity the artefact is built from. It also carries no suggestion of\n * precedence — nobody reads \"sorted by file name\" as \"most specific first\",\n * which is the misreading a route-path order invites.\n *\n * Lexicographic because it is byte-comparable output across every provider,\n * which keeps diffs stable and keeps filesystem enumeration order out of the\n * artefact: two machines that list a directory differently must still produce\n * byte-identical output, or a build is only reproducible by luck.\n */\nfunction comparePages(left: DiscoveredPage, right: DiscoveredPage): number {\n return compareStrings(toPosix(left.pageFile), toPosix(right.pageFile));\n}\n\nfunction assertUniqueRouteNames(pages: readonly DiscoveredPage[], appRoot: string): void {\n const fileByRouteName = new Map<string, string>();\n\n for (const page of pages) {\n const existing = fileByRouteName.get(page.routeName);\n const relative = toPosix(path.relative(appRoot, page.pageFile));\n\n if (existing !== undefined) {\n throw new DuplicatePageRouteNameError(page.routeName, existing, relative);\n }\n\n fileByRouteName.set(page.routeName, relative);\n }\n}\n\n/** Raised when a `*.page.tsx` declares no `route` export. */\nexport class MissingRouteExportError extends Error {\n public constructor(public readonly pageFile: string) {\n super(\n `\"${pageFile}\" is a page file but declares no \\`route\\` export. A page with no route is a ` +\n \"page the dev server would still serve and production would 404 on, so the build refuses \" +\n `it instead. For example: export const route = \"/list\";`,\n );\n this.name = \"MissingRouteExportError\";\n }\n}\n\n/**\n * The declared exports of one file, or a thrown\n * {@link NonLiteralRouteExportError} when they cannot be read without running\n * the application. Layouts are read once per run and remembered: a layout is\n * the nearest one for every page beside it, and parsing it once per page would\n * be the same answer bought repeatedly.\n */\nfunction readDeclarations(sourceFile: string, cache: Map<string, RouteExportsReadResult>) {\n let result = cache.get(sourceFile);\n\n if (result === undefined) {\n result = readRouteExports(sourceFile);\n cache.set(sourceFile, result);\n }\n\n if (!result.ok) {\n throw new NonLiteralRouteExportError(result.rejection);\n }\n\n return result;\n}\n\n/**\n * What a layout DOES, read by parsing it — the facts the layout policy's rule\n * needs and, being a pure module, cannot go and find for itself.\n */\ntype LayoutShape = {\n /**\n * Whether the module has a default export — the export that puts an element\n * in the document, and therefore the one thing that makes a layout count\n * against the single-rendering-layout rule.\n */\n renders: boolean;\n /** Whether the module exports `middleware`, or might via a re-export this cannot see through. */\n hasMiddleware: boolean;\n};\n\n/**\n * Parses one layout and reports its shape. Remembered per run for the same\n * reason declarations are: a layout is on the path of every page beneath it.\n */\nfunction readLayoutShape(layoutFile: string, cache: Map<string, LayoutShape>): LayoutShape {\n const cached = cache.get(layoutFile);\n\n if (cached !== undefined) return cached;\n\n const source = fs.readFileSync(layoutFile, \"utf-8\");\n let program: ReturnType<typeof parse>[\"program\"];\n\n try {\n program = parse(source, {\n sourceType: \"module\",\n // Every file this reads is a layout, i.e. `.tsx`.\n plugins: [\"typescript\", \"jsx\"],\n errorRecovery: false,\n }).program;\n } catch (error) {\n throw new Error(\n `Cannot read the exports of \"${layoutFile}\": the file could not be parsed ` +\n `(${(error as Error).message}). Fix the syntax error and the build will continue.`,\n );\n }\n\n const shape: LayoutShape = { renders: false, hasMiddleware: false };\n\n for (const statement of program.body) {\n if (statement.type === \"ExportDefaultDeclaration\") {\n shape.renders = true;\n continue;\n }\n\n // `export * from \"./guard\"` cannot re-export a default — the language\n // excludes it — but it CAN contribute `middleware`, and no parse can see\n // through it without resolving and reading another module. Reading it as\n // \"no middleware here\" is exactly the silent unguarding this slice exists\n // to prevent, so it is read as \"possibly\" and fails loudly downstream.\n if (statement.type === \"ExportAllDeclaration\") {\n shape.hasMiddleware = true;\n continue;\n }\n\n if (statement.type !== \"ExportNamedDeclaration\" || statement.exportKind === \"type\") continue;\n\n for (const specifier of statement.specifiers) {\n if (specifier.type !== \"ExportSpecifier\" || specifier.exportKind === \"type\") continue;\n\n const exported =\n specifier.exported.type === \"Identifier\"\n ? specifier.exported.name\n : specifier.exported.value;\n\n if (exported === \"default\") shape.renders = true;\n if (exported === \"middleware\") shape.hasMiddleware = true;\n }\n\n const { declaration } = statement;\n\n if (declaration === null || declaration === undefined) continue;\n\n if (declaration.type === \"VariableDeclaration\") {\n for (const declarator of declaration.declarations) {\n if (declarator.id.type === \"Identifier\" && declarator.id.name === \"middleware\") {\n shape.hasMiddleware = true;\n }\n }\n\n continue;\n }\n\n if (\n (declaration.type === \"FunctionDeclaration\" || declaration.type === \"ClassDeclaration\") &&\n declaration.id?.name === \"middleware\"\n ) {\n shape.hasMiddleware = true;\n }\n }\n\n cache.set(layoutFile, shape);\n\n return shape;\n}\n\n/**\n * Scans both web roots and returns the pages in a defined total order.\n *\n * Zero pages is a legal result, not an error: a project may be configured\n * with web and have nothing to serve yet. What is an error is a `*.page.tsx`\n * with no `route` export, which is refused rather than silently omitted — an\n * artefact that leaves a page out is a page the dev server still serves and\n * production 404s on; two pages claiming one route name, which this refuses\n * to return at all — the alternative is an artefact in which one of them is\n * silently unreachable; a `route` or `prefix` that cannot be read without\n * running the application, which is refused before any page is reported at\n * all; and a page whose layout chain holds more than one RENDERING layout, which\n * the production installer would refuse anyway — discovery refuses it first so\n * that artefact is never produced.\n */\nexport function discoverPages(options: DiscoverPagesOptions): DiscoveredPage[] {\n const { appRoot } = options;\n const srcRoot = path.join(appRoot, options.srcDir ?? \"src\");\n const webRoots = discoverWebRoots(srcRoot);\n const appFile = path.join(srcRoot, \"web\", \"root.tsx\");\n const hasAppFile = isFile(appFile);\n const declarations = new Map<string, RouteExportsReadResult>();\n const layoutShapes = new Map<string, LayoutShape>();\n const relativeToApp = (file: string) => toPosix(path.relative(appRoot, file));\n\n const pages: DiscoveredPage[] = [];\n\n for (const webRoot of webRoots) {\n for (const pageFile of walkFiles(webRoot, (fileName) => fileName.endsWith(\".page.tsx\"))) {\n const { route } = readDeclarations(pageFile, declarations);\n\n if (route === undefined) {\n throw new MissingRouteExportError(relativeToApp(pageFile));\n }\n\n // The policy decides which layout the page RENDERS INSIDE, from the FULL\n // enumerated chain: a layout anywhere on the ancestry path counts, not\n // just one in the page's own directory. Discovery supplies the one fact\n // the rule needs and the pure policy cannot learn — whether each layout\n // renders anything at all.\n const layouts = layoutChainFor(pageFile, webRoot);\n const shapes = layouts.map((layoutFile) => readLayoutShape(layoutFile, layoutShapes));\n const selection = selectPageLayout(\n layouts.map((layout, index) => ({ layout, renders: shapes[index].renders })),\n );\n\n if (selection.type === \"rejected\") {\n throw new NestedLayoutsNotSupportedError(\n relativeToApp(pageFile),\n selection.layouts.map(relativeToApp),\n );\n }\n\n // Every layout that declares a guard, outermost first. Both installers\n // now CONCATENATE the whole chain into the pipeline's single layout slot\n // (`../server/install-page-routes.ts`, `../server/install-page-routes-from-manifest.ts`),\n // so a guard anywhere on the path runs, in this order — which is why the\n // temporary refusal that used to stand here is gone rather than relaxed.\n const middlewareLayouts = layouts.filter((_, index) => shapes[index].hasMiddleware);\n\n // EVERY prefix on the path, outermost first: a `prefix`-only layout is\n // still a segment of the URL, and composing only the rendering layout's\n // would serve the subtree from a path nobody declared.\n const layoutPrefix = layouts.reduce(\n (composed, layoutFile) =>\n composeRoutePath(composed, readDeclarations(layoutFile, declarations).prefix ?? \"/\"),\n \"/\",\n );\n\n pages.push({\n routeName:\n route.name ??\n deriveFallbackRouteName({\n routePath: route.path,\n sourceFile: relativeToApp(pageFile),\n }),\n routePath: composeRoutePath(layoutPrefix, route.path),\n pageFile,\n webRoot,\n layouts,\n middlewareLayouts,\n ...(hasAppFile ? { appFile } : {}),\n });\n }\n }\n\n pages.sort(comparePages);\n\n assertUniqueRouteNames(pages, appRoot);\n\n return pages;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"discover-pages.mjs","names":[],"sources":["../../../../../../../web/src/build/discover-pages.ts"],"sourcesContent":["/**\n * Static discovery of the application's page graph — the ONE scan every\n * provider shares.\n *\n * This module has a single responsibility: look at the filesystem and produce\n * the recipe. It writes nothing, knows nothing about the generated barrel, and\n * imports neither Vite nor a single application module. Everything that turns\n * the recipe into an artefact (the production barrel, the dev plugin, the\n * generated client entry) lives with that artefact and consumes\n * {@link discoverPages}.\n *\n * The reason it is one module rather than one function per consumer: two\n * scanners that agree today drift tomorrow, and a page that exists for the\n * server but not the client is the silent failure that costs a day to find.\n * Sharing the scan makes that disagreement impossible by construction instead\n * of catchable by test.\n *\n * \"Static\" means WITHOUT RUNNING THE APPLICATION — this module globs\n * `*.page.tsx`, `layout.tsx` and `root.tsx`, and reads each page's declared\n * `route` and each layout's declared `prefix` by PARSING the source\n * ({@link readRouteExports}). It still imports no application module.\n *\n * The route a page is served under is the one the page DECLARES, not the one\n * its directory suggests, so that is the route discovery reports. The\n * composition — EVERY layout `prefix` on the page's path, outermost first, plus\n * the page's own `route` path — and the fallback used when a route omits its\n * name are mirrored from the server's installer (`installPageRoutes`),\n * deliberately and in one direction: build and boot agree because one of them\n * copies the other, not because two conventions were written to match.\n *\n * Discovery also CLASSIFIES each layout — does its module have a default\n * export, does it export `middleware` — because the layout policy\n * ({@link \"../routing/layout-policy.ts\"}) owns the rule but may not touch a\n * filesystem to learn the facts the rule needs. That classification is another\n * parse, never an import: a layout is read exactly the way a page's `route` is.\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { parse } from \"@babel/parser\";\nimport { composeRoutePath } from \"../routing/compose-route-path\";\nimport { NestedLayoutsNotSupportedError, selectPageLayout } from \"../routing/layout-policy\";\nimport { deriveFallbackRouteName } from \"../routing/route-identity\";\nimport type { RouteExportsReadResult } from \"./read-route-exports\";\nimport { NonLiteralRouteExportError, readRouteExports } from \"./read-route-exports\";\n\nexport type DiscoverPagesOptions = {\n /** Absolute path to the application root (where `package.json` lives). */\n appRoot: string;\n /** Source directory name under `appRoot`; defaults to `\"src\"`. */\n srcDir?: string;\n};\n\nexport type DiscoveredPage = {\n /**\n * The page's route name: the `name` its `route` export declares, or the\n * server's own fallback derivation — `<module>.<declared path with dots>` for\n * a page in a module's web tree, the dotted path alone for one in the global\n * tree, and `index` when neither has anything to say.\n *\n * Unique across the whole graph — {@link discoverPages} refuses to return a\n * result where it is not.\n */\n routeName: string;\n /**\n * The page's EFFECTIVE route path: the declared `prefix` of EVERY layout on\n * its path — outermost first, wherever in the ancestry each lives — composed\n * in order with the page's own declared `route` path, which is the path the\n * server registers it under. A layout that declares no `prefix` contributes\n * nothing. Always `/`-prefixed.\n *\n * Every layout, not just the rendering one: a `prefix`-only layout is a real\n * segment of the URL its subtree lives under, and skipping it would serve the\n * subtree from a path nobody wrote down.\n */\n routePath: string;\n /** Absolute path to the `*.page.tsx` file. */\n pageFile: string;\n /** The web root this page was found under — the root its layout chain climbs to. */\n webRoot: string;\n /** Absolute paths of every `layout.tsx` from the web root down to the page's own directory, OUTERMOST FIRST. */\n layouts: string[];\n /**\n * The page's middleware chain: the subset of {@link layouts} whose modules\n * export `middleware`, OUTERMOST FIRST — the order they must run in.\n *\n * Source files rather than the middleware values themselves, because\n * discovery never runs application code: this says WHICH layouts contribute a\n * guard, and the consumer that already loads those modules reads the values\n * off them.\n *\n * Empty when no layout on the path declares middleware, which is the common\n * case; a page with no layouts always has an empty chain.\n */\n middlewareLayouts: string[];\n /** Absolute path to the global `root.tsx` every page renders inside, when it exists. */\n appFile?: string;\n};\n\n/** Raised when two pages claim one route name. */\nexport class DuplicatePageRouteNameError extends Error {\n public constructor(\n public readonly routeName: string,\n public readonly firstFile: string,\n public readonly secondFile: string,\n ) {\n super(\n `Two pages resolve to the same route name \"${routeName}\": \"${firstFile}\" and ` +\n `\"${secondFile}\". A route name identifies exactly one page, so the second ` +\n \"page would be unreachable. To fix: rename one of the files, or move it so \" +\n \"its directory gives it a different route name.\",\n );\n this.name = \"DuplicatePageRouteNameError\";\n }\n}\n\nexport function toPosix(value: string): string {\n return value.replace(/\\\\/g, \"/\");\n}\n\nfunction isDirectory(candidate: string): boolean {\n try {\n return fs.statSync(candidate).isDirectory();\n } catch {\n return false;\n }\n}\n\nexport function isFile(candidate: string): boolean {\n try {\n return fs.statSync(candidate).isFile();\n } catch {\n return false;\n }\n}\n\n/**\n * The two page roots: the global `src/web/**` tree and each\n * module's `src/app/<module>/web/**` tree. Both are optional; a project with\n * neither has zero pages, which is a legal empty state.\n */\nexport function discoverWebRoots(srcRoot: string): string[] {\n const roots: string[] = [];\n const globalRoot = path.join(srcRoot, \"web\");\n\n if (isDirectory(globalRoot)) {\n roots.push(globalRoot);\n }\n\n const appDir = path.join(srcRoot, \"app\");\n\n if (isDirectory(appDir)) {\n for (const entry of fs.readdirSync(appDir, { withFileTypes: true }).sort(byName)) {\n if (!entry.isDirectory()) continue;\n\n const moduleWebRoot = path.join(appDir, entry.name, \"web\");\n\n if (isDirectory(moduleWebRoot)) {\n roots.push(moduleWebRoot);\n }\n }\n }\n\n return roots;\n}\n\nfunction byName(left: { name: string }, right: { name: string }): number {\n return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;\n}\n\nexport type DiscoveredPageFile = {\n /** Absolute path to the `*.page.tsx` file. */\n pageFile: string;\n /** The web root ({@link discoverWebRoots}) this page was found under. */\n webRoot: string;\n};\n\n/**\n * The subject list: every `*.page.tsx` under BOTH web roots, one call for the\n * whole graph.\n *\n * Unlike {@link discoverPages}, this reads no `route` or `prefix` export and\n * throws on nothing — it answers only \"which page files exist\", so a provider\n * that still resolves a page's route by its own means (dev's\n * `ssrLoadModule`-driven installer, chiefly) can share the walk without\n * inheriting the static-parsing refusals that answering \"what route is this\"\n * requires.\n */\nexport function discoverPageFiles(srcRoot: string): DiscoveredPageFile[] {\n const found: DiscoveredPageFile[] = [];\n\n for (const webRoot of discoverWebRoots(srcRoot)) {\n for (const pageFile of walkFiles(webRoot, (fileName) => fileName.endsWith(\".page.tsx\"))) {\n found.push({ pageFile, webRoot });\n }\n }\n\n return found;\n}\n\n/** Every file under `dir` (recursive) whose name matches `predicate`. */\nexport function walkFiles(dir: string, predicate: (fileName: string) => boolean): string[] {\n const found: string[] = [];\n\n for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort(byName)) {\n const full = path.join(dir, entry.name);\n\n if (entry.isDirectory()) {\n found.push(...walkFiles(full, predicate));\n } else if (entry.isFile() && predicate(entry.name)) {\n found.push(full);\n }\n }\n\n return found;\n}\n\n/**\n * A page's layout chain: every `layout.tsx` in the directories from its web\n * root down to its own directory, OUTERMOST FIRST.\n *\n * Dev's installer reads only the nearest layout today, because its page module\n * triple holds a single layout; the recipe carries the whole chain so the\n * runtime side can compose\n * nested layouts without a second discovery pass. The nearest layout is always\n * the LAST element, so a consumer that still wants dev's one-layout behaviour\n * reads `layouts.at(-1)`.\n */\nexport function layoutChainFor(pageFile: string, webRoot: string): string[] {\n const chain: string[] = [];\n const relativeDir = path.relative(webRoot, path.dirname(pageFile));\n const segments = relativeDir === \"\" ? [] : relativeDir.split(path.sep);\n\n let current = webRoot;\n\n for (let index = 0; index <= segments.length; index++) {\n if (index > 0) {\n current = path.join(current, segments[index - 1]);\n }\n\n const candidate = path.join(current, \"layout.tsx\");\n\n if (isFile(candidate)) {\n chain.push(candidate);\n }\n }\n\n return chain;\n}\n\nfunction compareStrings(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n\n/**\n * The total order every consumer sees, and the reason discovery returns an\n * array rather than a set: the page's SOURCE FILE PATH, lexicographic, POSIX.\n *\n * Serialization order only; matching precedence is a property of the route\n * grammar, not of this array.\n *\n * The file path rather than the route path, now that the route is the declared\n * one: a page's route can be rewritten by editing one line, which would reorder\n * an artefact that has not otherwise changed, while the file it lives in is the\n * stable identity the artefact is built from. It also carries no suggestion of\n * precedence — nobody reads \"sorted by file name\" as \"most specific first\",\n * which is the misreading a route-path order invites.\n *\n * Lexicographic because it is byte-comparable output across every provider,\n * which keeps diffs stable and keeps filesystem enumeration order out of the\n * artefact: two machines that list a directory differently must still produce\n * byte-identical output, or a build is only reproducible by luck.\n */\nfunction comparePages(left: DiscoveredPage, right: DiscoveredPage): number {\n return compareStrings(toPosix(left.pageFile), toPosix(right.pageFile));\n}\n\nfunction assertUniqueRouteNames(pages: readonly DiscoveredPage[], appRoot: string): void {\n const fileByRouteName = new Map<string, string>();\n\n for (const page of pages) {\n const existing = fileByRouteName.get(page.routeName);\n const relative = toPosix(path.relative(appRoot, page.pageFile));\n\n if (existing !== undefined) {\n throw new DuplicatePageRouteNameError(page.routeName, existing, relative);\n }\n\n fileByRouteName.set(page.routeName, relative);\n }\n}\n\n/** Raised when a `*.page.tsx` declares no `route` export. */\nexport class MissingRouteExportError extends Error {\n public constructor(public readonly pageFile: string) {\n super(\n `\"${pageFile}\" is a page file but declares no \\`route\\` export. A page with no route is a ` +\n \"page the dev server would still serve and production would 404 on, so the build refuses \" +\n `it instead. For example: export const route = \"/list\";`,\n );\n this.name = \"MissingRouteExportError\";\n }\n}\n\n/**\n * The declared exports of one file, or a thrown\n * {@link NonLiteralRouteExportError} when they cannot be read without running\n * the application. Layouts are read once per run and remembered: a layout is\n * the nearest one for every page beside it, and parsing it once per page would\n * be the same answer bought repeatedly.\n */\nfunction readDeclarations(sourceFile: string, cache: Map<string, RouteExportsReadResult>) {\n let result = cache.get(sourceFile);\n\n if (result === undefined) {\n result = readRouteExports(sourceFile);\n cache.set(sourceFile, result);\n }\n\n if (!result.ok) {\n throw new NonLiteralRouteExportError(result.rejection);\n }\n\n return result;\n}\n\n/**\n * What a layout DOES, read by parsing it — the facts the layout policy's rule\n * needs and, being a pure module, cannot go and find for itself.\n */\ntype LayoutShape = {\n /**\n * Whether the module has a default export — the export that puts an element\n * in the document, and therefore the one thing that makes a layout count\n * against the single-rendering-layout rule.\n */\n renders: boolean;\n /** Whether the module exports `middleware`, or might via a re-export this cannot see through. */\n hasMiddleware: boolean;\n};\n\n/**\n * Parses one layout and reports its shape. Remembered per run for the same\n * reason declarations are: a layout is on the path of every page beneath it.\n */\nfunction readLayoutShape(layoutFile: string, cache: Map<string, LayoutShape>): LayoutShape {\n const cached = cache.get(layoutFile);\n\n if (cached !== undefined) return cached;\n\n const source = fs.readFileSync(layoutFile, \"utf-8\");\n let program: ReturnType<typeof parse>[\"program\"];\n\n try {\n program = parse(source, {\n sourceType: \"module\",\n // Every file this reads is a layout, i.e. `.tsx`.\n plugins: [\"typescript\", \"jsx\"],\n errorRecovery: false,\n }).program;\n } catch (error) {\n throw new Error(\n `Cannot read the exports of \"${layoutFile}\": the file could not be parsed ` +\n `(${(error as Error).message}). Fix the syntax error and the build will continue.`,\n );\n }\n\n const shape: LayoutShape = { renders: false, hasMiddleware: false };\n\n for (const statement of program.body) {\n if (statement.type === \"ExportDefaultDeclaration\") {\n shape.renders = true;\n continue;\n }\n\n // `export * from \"./guard\"` cannot re-export a default — the language\n // excludes it — but it CAN contribute `middleware`, and no parse can see\n // through it without resolving and reading another module. Reading it as\n // \"no middleware here\" is exactly the silent unguarding this slice exists\n // to prevent, so it is read as \"possibly\" and fails loudly downstream.\n if (statement.type === \"ExportAllDeclaration\") {\n shape.hasMiddleware = true;\n continue;\n }\n\n if (statement.type !== \"ExportNamedDeclaration\" || statement.exportKind === \"type\") continue;\n\n for (const specifier of statement.specifiers) {\n if (specifier.type !== \"ExportSpecifier\" || specifier.exportKind === \"type\") continue;\n\n const exported =\n specifier.exported.type === \"Identifier\"\n ? specifier.exported.name\n : specifier.exported.value;\n\n if (exported === \"default\") shape.renders = true;\n if (exported === \"middleware\") shape.hasMiddleware = true;\n }\n\n const { declaration } = statement;\n\n if (declaration === null || declaration === undefined) continue;\n\n if (declaration.type === \"VariableDeclaration\") {\n for (const declarator of declaration.declarations) {\n if (declarator.id.type === \"Identifier\" && declarator.id.name === \"middleware\") {\n shape.hasMiddleware = true;\n }\n }\n\n continue;\n }\n\n if (\n (declaration.type === \"FunctionDeclaration\" || declaration.type === \"ClassDeclaration\") &&\n declaration.id?.name === \"middleware\"\n ) {\n shape.hasMiddleware = true;\n }\n }\n\n cache.set(layoutFile, shape);\n\n return shape;\n}\n\n/**\n * Scans both web roots and returns the pages in a defined total order.\n *\n * Zero pages is a legal result, not an error: a project may be configured\n * with web and have nothing to serve yet. What is an error is a `*.page.tsx`\n * with no `route` export, which is refused rather than silently omitted — an\n * artefact that leaves a page out is a page the dev server still serves and\n * production 404s on; two pages claiming one route name, which this refuses\n * to return at all — the alternative is an artefact in which one of them is\n * silently unreachable; a `route` or `prefix` that cannot be read without\n * running the application, which is refused before any page is reported at\n * all; and a page whose layout chain holds more than one RENDERING layout, which\n * the production installer would refuse anyway — discovery refuses it first so\n * that artefact is never produced.\n */\nexport function discoverPages(options: DiscoverPagesOptions): DiscoveredPage[] {\n const { appRoot } = options;\n const srcRoot = path.join(appRoot, options.srcDir ?? \"src\");\n const webRoots = discoverWebRoots(srcRoot);\n const appFile = path.join(srcRoot, \"web\", \"root.tsx\");\n const hasAppFile = isFile(appFile);\n const declarations = new Map<string, RouteExportsReadResult>();\n const layoutShapes = new Map<string, LayoutShape>();\n const relativeToApp = (file: string) => toPosix(path.relative(appRoot, file));\n\n const pages: DiscoveredPage[] = [];\n\n for (const webRoot of webRoots) {\n for (const pageFile of walkFiles(webRoot, (fileName) => fileName.endsWith(\".page.tsx\"))) {\n const { route } = readDeclarations(pageFile, declarations);\n\n if (route === undefined) {\n throw new MissingRouteExportError(relativeToApp(pageFile));\n }\n\n // The policy decides which layout the page RENDERS INSIDE, from the FULL\n // enumerated chain: a layout anywhere on the ancestry path counts, not\n // just one in the page's own directory. Discovery supplies the one fact\n // the rule needs and the pure policy cannot learn — whether each layout\n // renders anything at all.\n const layouts = layoutChainFor(pageFile, webRoot);\n const shapes = layouts.map((layoutFile) => readLayoutShape(layoutFile, layoutShapes));\n const selection = selectPageLayout(\n layouts.map((layout, index) => ({ layout, renders: shapes[index].renders })),\n );\n\n if (selection.type === \"rejected\") {\n throw new NestedLayoutsNotSupportedError(\n relativeToApp(pageFile),\n selection.layouts.map(relativeToApp),\n );\n }\n\n // Every layout that declares a guard, outermost first. Both installers\n // now CONCATENATE the whole chain into the pipeline's single layout slot\n // (`../server/install-page-routes.ts`, `../server/install-page-routes-from-manifest.ts`),\n // so a guard anywhere on the path runs, in this order — which is why the\n // temporary refusal that used to stand here is gone rather than relaxed.\n const middlewareLayouts = layouts.filter((_, index) => shapes[index].hasMiddleware);\n\n // EVERY prefix on the path, outermost first: a `prefix`-only layout is\n // still a segment of the URL, and composing only the rendering layout's\n // would serve the subtree from a path nobody declared.\n const layoutPrefix = layouts.reduce(\n (composed, layoutFile) =>\n composeRoutePath(composed, readDeclarations(layoutFile, declarations).prefix ?? \"/\"),\n \"/\",\n );\n\n pages.push({\n routeName:\n route.name ??\n deriveFallbackRouteName({\n routePath: route.path,\n sourceFile: relativeToApp(pageFile),\n }),\n routePath: composeRoutePath(layoutPrefix, route.path),\n pageFile,\n webRoot,\n layouts,\n middlewareLayouts,\n ...(hasAppFile ? { appFile } : {}),\n });\n }\n }\n\n pages.sort(comparePages);\n\n assertUniqueRouteNames(pages, appRoot);\n\n return pages;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmGA,IAAa,8BAAb,cAAiD,MAAM;CAEnC;CACA;CACA;CAHlB,AAAO,YACL,AAAgB,WAChB,AAAgB,WAChB,AAAgB,YAChB;EACA,MACE,6CAA6C,UAAU,MAAM,UAAU,SACjE,WAAW,oLAGnB;EATgB;EACA;EACA;EAQhB,KAAK,OAAO;CACd;AACF;AAEA,SAAgB,QAAQ,OAAuB;CAC7C,OAAO,MAAM,QAAQ,OAAO,GAAG;AACjC;AAEA,SAAS,YAAY,WAA4B;CAC/C,IAAI;EACF,OAAO,GAAG,SAAS,SAAS,CAAC,CAAC,YAAY;CAC5C,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAgB,OAAO,WAA4B;CACjD,IAAI;EACF,OAAO,GAAG,SAAS,SAAS,CAAC,CAAC,OAAO;CACvC,QAAQ;EACN,OAAO;CACT;AACF;;;;;;AAOA,SAAgB,iBAAiB,SAA2B;CAC1D,MAAM,QAAkB,CAAC;CACzB,MAAM,aAAa,KAAK,KAAK,SAAS,KAAK;CAE3C,IAAI,YAAY,UAAU,GACxB,MAAM,KAAK,UAAU;CAGvB,MAAM,SAAS,KAAK,KAAK,SAAS,KAAK;CAEvC,IAAI,YAAY,MAAM,GACpB,KAAK,MAAM,SAAS,GAAG,YAAY,QAAQ,EAAE,eAAe,KAAK,CAAC,CAAC,CAAC,KAAK,MAAM,GAAG;EAChF,IAAI,CAAC,MAAM,YAAY,GAAG;EAE1B,MAAM,gBAAgB,KAAK,KAAK,QAAQ,MAAM,MAAM,KAAK;EAEzD,IAAI,YAAY,aAAa,GAC3B,MAAM,KAAK,aAAa;CAE5B;CAGF,OAAO;AACT;AAEA,SAAS,OAAO,MAAwB,OAAiC;CACvE,OAAO,KAAK,OAAO,MAAM,OAAO,KAAK,KAAK,OAAO,MAAM,OAAO,IAAI;AACpE;;;;;;;;;;;;AAoBA,SAAgB,kBAAkB,SAAuC;CACvE,MAAM,QAA8B,CAAC;CAErC,KAAK,MAAM,WAAW,iBAAiB,OAAO,GAC5C,KAAK,MAAM,YAAY,UAAU,UAAU,aAAa,SAAS,SAAS,WAAW,CAAC,GACpF,MAAM,KAAK;EAAE;EAAU;CAAQ,CAAC;CAIpC,OAAO;AACT;;AAGA,SAAgB,UAAU,KAAa,WAAoD;CACzF,MAAM,QAAkB,CAAC;CAEzB,KAAK,MAAM,SAAS,GAAG,YAAY,KAAK,EAAE,eAAe,KAAK,CAAC,CAAC,CAAC,KAAK,MAAM,GAAG;EAC7E,MAAM,OAAO,KAAK,KAAK,KAAK,MAAM,IAAI;EAEtC,IAAI,MAAM,YAAY,GACpB,MAAM,KAAK,GAAG,UAAU,MAAM,SAAS,CAAC;OACnC,IAAI,MAAM,OAAO,KAAK,UAAU,MAAM,IAAI,GAC/C,MAAM,KAAK,IAAI;CAEnB;CAEA,OAAO;AACT;;;;;;;;;;;;AAaA,SAAgB,eAAe,UAAkB,SAA2B;CAC1E,MAAM,QAAkB,CAAC;CACzB,MAAM,cAAc,KAAK,SAAS,SAAS,KAAK,QAAQ,QAAQ,CAAC;CACjE,MAAM,WAAW,gBAAgB,KAAK,CAAC,IAAI,YAAY,MAAM,KAAK,GAAG;CAErE,IAAI,UAAU;CAEd,KAAK,IAAI,QAAQ,GAAG,SAAS,SAAS,QAAQ,SAAS;EACrD,IAAI,QAAQ,GACV,UAAU,KAAK,KAAK,SAAS,SAAS,QAAQ,EAAE;EAGlD,MAAM,YAAY,KAAK,KAAK,SAAS,YAAY;EAEjD,IAAI,OAAO,SAAS,GAClB,MAAM,KAAK,SAAS;CAExB;CAEA,OAAO;AACT;AAEA,SAAS,eAAe,MAAc,OAAuB;CAC3D,OAAO,OAAO,QAAQ,KAAK,OAAO,QAAQ,IAAI;AAChD;;;;;;;;;;;;;;;;;;;;AAqBA,SAAS,aAAa,MAAsB,OAA+B;CACzE,OAAO,eAAe,QAAQ,KAAK,QAAQ,GAAG,QAAQ,MAAM,QAAQ,CAAC;AACvE;AAEA,SAAS,uBAAuB,OAAkC,SAAuB;CACvF,MAAM,kCAAkB,IAAI,IAAoB;CAEhD,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,WAAW,gBAAgB,IAAI,KAAK,SAAS;EACnD,MAAM,WAAW,QAAQ,KAAK,SAAS,SAAS,KAAK,QAAQ,CAAC;EAE9D,IAAI,aAAa,QACf,MAAM,IAAI,4BAA4B,KAAK,WAAW,UAAU,QAAQ;EAG1E,gBAAgB,IAAI,KAAK,WAAW,QAAQ;CAC9C;AACF;;AAGA,IAAa,0BAAb,cAA6C,MAAM;CACd;CAAnC,AAAO,YAAY,AAAgB,UAAkB;EACnD,MACE,IAAI,SAAS,4NAGf;EALiC;EAMjC,KAAK,OAAO;CACd;AACF;;;;;;;;AASA,SAAS,iBAAiB,YAAoB,OAA4C;CACxF,IAAI,SAAS,MAAM,IAAI,UAAU;CAEjC,IAAI,WAAW,QAAW;EACxB,SAAS,iBAAiB,UAAU;EACpC,MAAM,IAAI,YAAY,MAAM;CAC9B;CAEA,IAAI,CAAC,OAAO,IACV,MAAM,IAAI,2BAA2B,OAAO,SAAS;CAGvD,OAAO;AACT;;;;;AAqBA,SAAS,gBAAgB,YAAoB,OAA8C;CACzF,MAAM,SAAS,MAAM,IAAI,UAAU;CAEnC,IAAI,WAAW,QAAW,OAAO;CAEjC,MAAM,SAAS,GAAG,aAAa,YAAY,OAAO;CAClD,IAAI;CAEJ,IAAI;EACF,UAAU,MAAM,QAAQ;GACtB,YAAY;GAEZ,SAAS,CAAC,cAAc,KAAK;GAC7B,eAAe;EACjB,CAAC,CAAC,CAAC;CACL,SAAS,OAAO;EACd,MAAM,IAAI,MACR,+BAA+B,WAAW,mCACnC,MAAgB,QAAQ,qDACjC;CACF;CAEA,MAAM,QAAqB;EAAE,SAAS;EAAO,eAAe;CAAM;CAElE,KAAK,MAAM,aAAa,QAAQ,MAAM;EACpC,IAAI,UAAU,SAAS,4BAA4B;GACjD,MAAM,UAAU;GAChB;EACF;EAOA,IAAI,UAAU,SAAS,wBAAwB;GAC7C,MAAM,gBAAgB;GACtB;EACF;EAEA,IAAI,UAAU,SAAS,4BAA4B,UAAU,eAAe,QAAQ;EAEpF,KAAK,MAAM,aAAa,UAAU,YAAY;GAC5C,IAAI,UAAU,SAAS,qBAAqB,UAAU,eAAe,QAAQ;GAE7E,MAAM,WACJ,UAAU,SAAS,SAAS,eACxB,UAAU,SAAS,OACnB,UAAU,SAAS;GAEzB,IAAI,aAAa,WAAW,MAAM,UAAU;GAC5C,IAAI,aAAa,cAAc,MAAM,gBAAgB;EACvD;EAEA,MAAM,EAAE,gBAAgB;EAExB,IAAI,gBAAgB,QAAQ,gBAAgB,QAAW;EAEvD,IAAI,YAAY,SAAS,uBAAuB;GAC9C,KAAK,MAAM,cAAc,YAAY,cACnC,IAAI,WAAW,GAAG,SAAS,gBAAgB,WAAW,GAAG,SAAS,cAChE,MAAM,gBAAgB;GAI1B;EACF;EAEA,KACG,YAAY,SAAS,yBAAyB,YAAY,SAAS,uBACpE,YAAY,IAAI,SAAS,cAEzB,MAAM,gBAAgB;CAE1B;CAEA,MAAM,IAAI,YAAY,KAAK;CAE3B,OAAO;AACT;;;;;;;;;;;;;;;;AAiBA,SAAgB,cAAc,SAAiD;CAC7E,MAAM,EAAE,YAAY;CACpB,MAAM,UAAU,KAAK,KAAK,SAAS,QAAQ,UAAU,KAAK;CAC1D,MAAM,WAAW,iBAAiB,OAAO;CACzC,MAAM,UAAU,KAAK,KAAK,SAAS,OAAO,UAAU;CACpD,MAAM,aAAa,OAAO,OAAO;CACjC,MAAM,+BAAe,IAAI,IAAoC;CAC7D,MAAM,+BAAe,IAAI,IAAyB;CAClD,MAAM,iBAAiB,SAAiB,QAAQ,KAAK,SAAS,SAAS,IAAI,CAAC;CAE5E,MAAM,QAA0B,CAAC;CAEjC,KAAK,MAAM,WAAW,UACpB,KAAK,MAAM,YAAY,UAAU,UAAU,aAAa,SAAS,SAAS,WAAW,CAAC,GAAG;EACvF,MAAM,EAAE,UAAU,iBAAiB,UAAU,YAAY;EAEzD,IAAI,UAAU,QACZ,MAAM,IAAI,wBAAwB,cAAc,QAAQ,CAAC;EAQ3D,MAAM,UAAU,eAAe,UAAU,OAAO;EAChD,MAAM,SAAS,QAAQ,KAAK,eAAe,gBAAgB,YAAY,YAAY,CAAC;EACpF,MAAM,YAAY,iBAChB,QAAQ,KAAK,QAAQ,WAAW;GAAE;GAAQ,SAAS,OAAO,MAAM,CAAC;EAAQ,EAAE,CAC7E;EAEA,IAAI,UAAU,SAAS,YACrB,MAAM,IAAI,+BACR,cAAc,QAAQ,GACtB,UAAU,QAAQ,IAAI,aAAa,CACrC;EAQF,MAAM,oBAAoB,QAAQ,QAAQ,GAAG,UAAU,OAAO,MAAM,CAAC,aAAa;EAKlF,MAAM,eAAe,QAAQ,QAC1B,UAAU,eACT,iBAAiB,UAAU,iBAAiB,YAAY,YAAY,CAAC,CAAC,UAAU,GAAG,GACrF,GACF;EAEA,MAAM,KAAK;GACT,WACE,MAAM,QACN,wBAAwB;IACtB,WAAW,MAAM;IACjB,YAAY,cAAc,QAAQ;GACpC,CAAC;GACH,WAAW,iBAAiB,cAAc,MAAM,IAAI;GACpD;GACA;GACA;GACA;GACA,GAAI,aAAa,EAAE,QAAQ,IAAI,CAAC;EAClC,CAAC;CACH;CAGF,MAAM,KAAK,YAAY;CAEvB,uBAAuB,OAAO,OAAO;CAErC,OAAO;AACT"}
@@ -1,7 +1,7 @@
1
1
  import { NestedLayoutsNotSupportedError } from "../routing/layout-policy.mjs";
2
2
  import { DuplicatePageRouteNameError, discoverPages, discoverWebRoots, isFile, layoutChainFor, toPosix, walkFiles } from "./discover-pages.mjs";
3
- import fs from "node:fs";
4
3
  import path from "node:path";
4
+ import fs from "node:fs";
5
5
  import { parse } from "@babel/parser";
6
6
 
7
7
  //#region ../web/src/build/generate-pages-barrel.ts
@@ -1 +1,7 @@
1
- export { };
1
+ //#region ../web/src/components/document-context.d.ts
2
+ declare const PAYLOAD_SCRIPT_ID = "__WARLOCK_DATA__";
3
+ /** Escape JSON text for raw insertion into an application/json script. */
4
+ declare function escapePayload(json: string): string;
5
+ //#endregion
6
+ export { PAYLOAD_SCRIPT_ID, escapePayload };
7
+ //# sourceMappingURL=document-context.d.mts.map
@@ -1,6 +1,6 @@
1
+ import { CLIENT_ASSET_URL_PREFIX } from "../server/client-asset-url-prefix.mjs";
1
2
  import { WebPackageRootResolutionError, createWebBuildContribution } from "../build/contribution.mjs";
2
3
  import { consumePageManifest, providePageManifest } from "../server/page-manifest.mjs";
3
- import { CLIENT_ASSET_URL_PREFIX } from "../server/client-asset-url-prefix.mjs";
4
4
  import { WebClientAssetPrefixViolationError, WebClientManifestEntryMissingError, WebClientManifestMalformedError, WebClientManifestMissingError, resolveHydrationClientUrl } from "../server/hydration-client-url.mjs";
5
5
  import { webConnector } from "../server/web-connector-factory.mjs";
6
6
 
@@ -50,5 +50,5 @@ type PageMetadata<TLoader extends LoaderFunction | undefined = undefined> = Meta
50
50
  shared: Readonly<SharedContext>;
51
51
  }) => MetadataOutput);
52
52
  //#endregion
53
- export { PageMetadata };
53
+ export { MetadataOutput, PageMetadata };
54
54
  //# sourceMappingURL=metadata.d.mts.map
@@ -0,0 +1,29 @@
1
+ //#region ../web/src/routing/compose-route-path.d.ts
2
+ /**
3
+ * Route-path composition — the single, pure rule for turning a layout's
4
+ * declared `prefix` and a page's declared `route.path` into the page's
5
+ * effective, registered path. Previously three hand-written copies: the dev
6
+ * installer (`web/src/server/install-page-routes.ts`), the production
7
+ * manifest installer (`web/src/server/install-page-routes-from-manifest.ts`)
8
+ * and build discovery (`web/src/build/discover-pages.ts`) each carried this
9
+ * exact rule so build and boot could not quietly disagree about it; all
10
+ * three now delegate here instead.
11
+ *
12
+ * DIRECTORY CONTRACT — applies to everything in `web/src/routing/`: nothing
13
+ * here may import `node:fs`, `node:path`, `vite`, or `fastify`. This module
14
+ * receives canonical values and trusts nothing about them beyond the input
15
+ * contract asserted below — it asserts rather than trusts, but it never
16
+ * repairs.
17
+ */
18
+ /**
19
+ * A page's effective path: its nearest layout's `prefix` followed by its own
20
+ * declared `route.path`. A root prefix ("/") and a root path ("/") each
21
+ * contribute nothing, so neither adds a slash of its own; every other case
22
+ * joins on the slash `routePath` already starts with, which is what keeps a
23
+ * double slash out of the result, and an empty concatenation is the site
24
+ * root.
25
+ */
26
+ declare function composeRoutePath(layoutPrefix: string, routePath: string): string;
27
+ //#endregion
28
+ export { composeRoutePath };
29
+ //# sourceMappingURL=compose-route-path.d.mts.map
@@ -0,0 +1,58 @@
1
+ import { LoaderShortCircuit, WebResponse } from "../context.mjs";
2
+
3
+ //#region ../web/src/server/buffered-response.d.ts
4
+ /**
5
+ * The per-loader response facade for pipeline stage 6.
6
+ *
7
+ * Loaders run in PARALLEL and never see the real `Response`: core's
8
+ * `Response.header`/`cookie` write straight through to the fastify reply
9
+ * (core/src/http/response.ts:919-923, :955-967), so three concurrent loaders
10
+ * writing directly would interleave nondeterministically and a discarded
11
+ * layer's writes could never be taken back. Instead each loader gets one of
12
+ * these facades; every write lands in a buffer, and the pipeline's
13
+ * settle/commit stage applies the surviving buffers to
14
+ * the real response root→leaf, per cookie name / header key.
15
+ */
16
+ type BufferedHeader = {
17
+ key: string;
18
+ value: string;
19
+ };
20
+ type BufferedCookie = {
21
+ name: string;
22
+ value: unknown;
23
+ options?: Record<string, unknown>;
24
+ };
25
+ type ResponseBuffer = {
26
+ headers: BufferedHeader[];
27
+ cookies: BufferedCookie[];
28
+ statusCode?: number;
29
+ };
30
+ /**
31
+ * Runtime brand behind `LoaderShortCircuit` (web/src/context.ts:13-17
32
+ * declares the compile-time half). A loader RETURNS this from
33
+ * `response.redirect()` / `response.notFound()`; the settle stage detects it
34
+ * by the symbol, never by shape.
35
+ */
36
+ declare const LOADER_SHORT_CIRCUIT: unique symbol;
37
+ type LoaderShortCircuitSignal = LoaderShortCircuit & {
38
+ kind: "redirect" | "notFound";
39
+ statusCode: number;
40
+ url?: string;
41
+ body?: unknown;
42
+ };
43
+ declare function isLoaderShortCircuit(value: unknown): value is LoaderShortCircuitSignal;
44
+ /**
45
+ * The loader-facing surface is `WebResponse` (web/src/context.ts:73-88) plus
46
+ * `cookie()`, which the commit contract needs so buffers can be applied
47
+ * per cookie name.
48
+ */
49
+ type BufferedWebResponse = WebResponse & {
50
+ cookie(name: string, value: unknown, options?: Record<string, unknown>): BufferedWebResponse;
51
+ };
52
+ declare function createBufferedResponse(): {
53
+ response: BufferedWebResponse;
54
+ buffer: ResponseBuffer;
55
+ };
56
+ //#endregion
57
+ export { BufferedCookie, BufferedHeader, BufferedWebResponse, LOADER_SHORT_CIRCUIT, LoaderShortCircuitSignal, ResponseBuffer, createBufferedResponse, isLoaderShortCircuit };
58
+ //# sourceMappingURL=buffered-response.d.mts.map
@@ -0,0 +1,36 @@
1
+ import { PageManifest } from "./page-manifest.mjs";
2
+ import { PageModuleLoader } from "./create-page-route-handler.mjs";
3
+
4
+ //#region ../web/src/server/create-page-module-loader.d.ts
5
+ /**
6
+ * Asked for a module this build does not contain.
7
+ *
8
+ * Hard failure, deliberately. Every alternative — returning `undefined`, an
9
+ * empty namespace, or falling back to a dynamic import — turns a build that
10
+ * shipped the wrong module table into a page that renders blank, or into a
11
+ * production process reaching for source files that are not deployed. The id
12
+ * is in the message because the whole diagnosis is "which id, and why is it
13
+ * not in the table"; the count is there because zero entries means the build
14
+ * discovered no pages at all, which is a different fault from a mismatch.
15
+ */
16
+ declare class PageModuleNotInManifestError extends Error {
17
+ constructor(moduleId: string, knownIdCount: number);
18
+ }
19
+ /**
20
+ * Build the loader for ONE manifest.
21
+ *
22
+ * Ids are compared by exact string equality against the `sourceFile` each
23
+ * entry carries — no lowercasing, no separator swapping, no extension
24
+ * stripping. Both sides of that comparison come from the same generator
25
+ * (`generatePagesBarrel` writes every `sourceFile` through one
26
+ * app-root-relative POSIX derivation), so any spelling difference is a real
27
+ * disagreement about which file is meant, and a normalizer would only hide it.
28
+ *
29
+ * A layout shared by several pages appears once per chain it belongs to; the
30
+ * generator emits one identifier per layout file, so every occurrence of an id
31
+ * carries the same namespace object and re-registering it is a no-op.
32
+ */
33
+ declare function createPageModuleLoader(manifest: PageManifest): PageModuleLoader;
34
+ //#endregion
35
+ export { PageModuleNotInManifestError, createPageModuleLoader };
36
+ //# sourceMappingURL=create-page-module-loader.d.mts.map
@@ -0,0 +1,31 @@
1
+ import { BufferedCookie } from "./buffered-response.mjs";
2
+ import { HttpContext, Response } from "@warlock.js/core";
3
+
4
+ //#region ../web/src/server/create-page-route-handler.d.ts
5
+ /**
6
+ * How the handler obtains a page/layout/app module, by the same id
7
+ * (`appFile`/`layoutFile`/`pageFile`) the caller registered it under. In dev
8
+ * this is `moduleId => vite.ssrLoadModule(moduleId)`; the connector already
9
+ * owns the dev/prod split, so the handler never learns which one it got.
10
+ */
11
+ type PageModuleLoader = (moduleId: string) => Promise<unknown>;
12
+ type PageRouteHandlerOptions = {
13
+ /** The composed, registered route path — `composeRoutePath`'s output. */path: string; /** The resolved route name; shared namespace with API routes. */
14
+ name: string; /** The single global app-root file, e.g. `<appSrcRoot>/web/root.tsx`. */
15
+ appFile: string; /** The page module's id. */
16
+ pageFile: string; /** The page's own-directory `layout.tsx`, when it has one. */
17
+ layoutFile?: string | undefined;
18
+ loadModule: PageModuleLoader; /** Browser module appended after the server-rendered document. */
19
+ hydrationClientModuleUrl?: string;
20
+ /**
21
+ * Stylesheet URLs for this page, emitted into `<head>` so the FIRST paint is
22
+ * styled. Absent or empty means the application has no CSS — it never means
23
+ * a stylesheet failed to resolve, which is the build's job to report.
24
+ */
25
+ stylesheetUrls?: readonly string[]; /** Same helper `dev-server.ts` exports — passed in, never imported. */
26
+ applyBufferedCookie: (response: Response, cookie: BufferedCookie) => void;
27
+ };
28
+ type PageRouteHandler = (context: HttpContext) => Promise<void>;
29
+ //#endregion
30
+ export { PageModuleLoader, PageRouteHandler, PageRouteHandlerOptions };
31
+ //# sourceMappingURL=create-page-route-handler.d.mts.map
@@ -1 +1,7 @@
1
- export { };
1
+ import { ExecutePageRequestOptions, PageBoundaryDesignation, PageContextRunner, PageDataBundle, PageErrorRecord, PageLevelName, PageResponseCommit, PageRouteEntry, PageRouteMatch, PageShortCircuit, PageTripleModule, PipelineLoader, PipelineMiddleware, PipelineRequest, PipelineResponse, PipelineStore } from "./execute-page-request.types.mjs";
2
+ import { connectPageContext, connectPageSharedScope } from "./page-context.mjs";
3
+ //#region ../web/src/server/execute-page-request.d.ts
4
+ declare function executePageRequest<TResult = PageDataBundle>(options: ExecutePageRequestOptions<TResult>): Promise<TResult | undefined>;
5
+ //#endregion
6
+ export { executePageRequest };
7
+ //# sourceMappingURL=execute-page-request.d.mts.map
@@ -1,6 +1,6 @@
1
1
  import { enterSharedScope, sealShared, shared } from "../shared.mjs";
2
2
  import { createBufferedResponse, isLoaderShortCircuit } from "./buffered-response.mjs";
3
- import { connectPageContext, enterAdditionalSharedScope, requireRunner } from "./page-context.mjs";
3
+ import { connectPageContext, connectPageSharedScope, enterAdditionalSharedScope, requireRunner } from "./page-context.mjs";
4
4
  import { matchRoute } from "./match-page-route.mjs";
5
5
  import { resolvePageMetadata } from "./resolve-page-metadata.mjs";
6
6
  import { resolveValidationData } from "./resolve-validation-data.mjs";
@@ -1 +1,174 @@
1
- export { };
1
+ import { MetadataOutput, PageMetadata } from "../metadata.mjs";
2
+ import { SharedStore } from "../shared.mjs";
3
+ import { SharedContext } from "../index.mjs";
4
+ import { WebRequest } from "../context.mjs";
5
+ import { BufferedCookie, BufferedHeader, BufferedWebResponse } from "./buffered-response.mjs";
6
+ import { BaseValidator } from "@warlock.js/seal";
7
+
8
+ //#region ../web/src/server/execute-page-request.types.d.ts
9
+ /** At runtime this IS core's `RequestContextStore` (request-context.ts:10-13). */
10
+ type PipelineStore = SharedStore & {
11
+ request: unknown;
12
+ response: unknown;
13
+ };
14
+ /**
15
+ * Core's `requestContext` satisfies this as-is — `run`/`getStore` are the
16
+ * inherited `Context` delegates and `buildStore` is `RequestContext.buildStore`,
17
+ * the same function core's http path feeds through `contextManager.buildStores`.
18
+ */
19
+ type PageContextRunner = {
20
+ run<T>(store: PipelineStore, callback: () => Promise<T>): Promise<T>;
21
+ getStore(): PipelineStore | undefined;
22
+ buildStore?(payload?: Record<string, any>): PipelineStore;
23
+ };
24
+ type PageLevelName = "app" | "layout" | "page";
25
+ /**
26
+ * The request members the pipeline touches, declared explicitly rather than
27
+ * imported from core: `WebRequest` is the loader-facing minimal facade and does
28
+ * not carry the validation sources.
29
+ *
30
+ * `query`/`params` are core's own parses (request.ts:1010,1026) and the ONLY
31
+ * ones the pipeline reads. Stage 4 used to take them from a match object built
32
+ * by re-parsing the URL — `resolve-validation-data.ts` records what that cost.
33
+ */
34
+ type PipelineRequest = WebRequest & {
35
+ body?: Record<string, unknown>;
36
+ headers?: Record<string, unknown>;
37
+ query?: Record<string, unknown>;
38
+ params?: Record<string, unknown>;
39
+ setValidatedData?(data: Record<string, unknown>): void;
40
+ };
41
+ /**
42
+ * Standalone rather than based on `WebResponse`: that facade's
43
+ * `redirect`/`notFound` return the branded short-circuit signal, which core's
44
+ * real `Response` never carries — basing this on it would reject the very
45
+ * instances the seam exists to admit.
46
+ */
47
+ type PipelineResponse = {
48
+ header(key: string, value: string): PipelineResponse;
49
+ cookie(name: string, value: unknown, options?: Record<string, unknown>): PipelineResponse;
50
+ setStatusCode(statusCode: number): PipelineResponse;
51
+ parse(value: unknown): Promise<unknown>;
52
+ };
53
+ /** Pass-through is `undefined` — core's exact rule (`Request.executeMiddleware`). */
54
+ type PipelineMiddleware = (ctx: {
55
+ request: PipelineRequest;
56
+ response: PipelineResponse;
57
+ }) => unknown | Promise<unknown>;
58
+ type PipelineLoader = (ctx: {
59
+ request: PipelineRequest;
60
+ response: BufferedWebResponse;
61
+ shared: SharedContext;
62
+ }) => unknown | Promise<unknown>;
63
+ /** The server half of a page/layout/App module. */
64
+ type PageTripleModule = {
65
+ route?: string | {
66
+ readonly path: string;
67
+ readonly name?: string;
68
+ };
69
+ middleware?: readonly PipelineMiddleware[];
70
+ validation?: {
71
+ schema?: BaseValidator;
72
+ validating?: readonly string[];
73
+ };
74
+ loader?: PipelineLoader;
75
+ metadata?: PageMetadata<PipelineLoader>; /** Runs-twice half — carried through untouched; the render slice consumes them. */
76
+ default?: unknown;
77
+ ErrorBoundary?: unknown;
78
+ };
79
+ type PageRouteEntry = {
80
+ path: string;
81
+ name: string;
82
+ triple: {
83
+ app: PageTripleModule;
84
+ layout: PageTripleModule;
85
+ page: PageTripleModule;
86
+ };
87
+ };
88
+ type PageRouteMatch = {
89
+ entry: PageRouteEntry;
90
+ params: Record<string, string>;
91
+ query: Record<string, string>;
92
+ };
93
+ type ExecutePageRequestOptions<TResult = PageDataBundle> = {
94
+ /** Path + optional query string, e.g. `/products/42?tab=specs`. */url: string;
95
+ routes: readonly PageRouteEntry[];
96
+ /**
97
+ * Construct the Request/Response pair for this match, mirroring core's
98
+ * `handleRoute` body (router.ts:924-932).
99
+ */
100
+ createHttp(match: PageRouteMatch): {
101
+ request: PipelineRequest;
102
+ response: PipelineResponse;
103
+ };
104
+ /**
105
+ * The render seam — continues stages 9-10 inside the exact ALS store and
106
+ * shared scope that middleware and loaders used.
107
+ */
108
+ finish?(bundle: PageDataBundle): TResult | Promise<TResult>;
109
+ };
110
+ type PageBoundaryDesignation = {
111
+ throwingLevel: PageLevelName; /** Nearest boundary at or rootward of the throw; `app` is the terminal fallback. */
112
+ boundaryLevel: PageLevelName;
113
+ };
114
+ type PageResponseCommit = {
115
+ /** Final per-key state, root→leaf: a leafward write wins its key. */headers: BufferedHeader[];
116
+ cookies: BufferedCookie[];
117
+ statusCode?: number;
118
+ committedLevels: PageLevelName[];
119
+ };
120
+ type PageShortCircuit = {
121
+ stage: "middleware";
122
+ level: PageLevelName;
123
+ value: unknown; /** Captured at stage 3, so `finishRender` stays a pure function of (triple, bundle). */
124
+ statusCode?: number;
125
+ } | {
126
+ stage: "validation";
127
+ status: number;
128
+ errors: unknown;
129
+ } | {
130
+ stage: "loaders";
131
+ level: PageLevelName;
132
+ kind: "redirect" | "notFound";
133
+ statusCode: number;
134
+ url?: string;
135
+ body?: unknown;
136
+ };
137
+ /**
138
+ * What a throw becomes once it enters the bundle. `scrubbed` says whether
139
+ * `error` is the raw thrown value or a production surrogate standing in for it.
140
+ */
141
+ type PageErrorRecord = {
142
+ error: unknown;
143
+ boundary: PageBoundaryDesignation;
144
+ digest: string;
145
+ scrubbed: boolean;
146
+ };
147
+ type PageDataBundle = {
148
+ route: {
149
+ name: string;
150
+ path: string;
151
+ params: Record<string, string>;
152
+ /**
153
+ * ⚠ Re-parsed from the URL by `match-page-route.ts`, NOT core's parse, so it
154
+ * is flat and last-wins: `?tags=a&tags=b&filter[status]=active` arrives as
155
+ * `{ tags: "b", "filter[status]": "active" }`.
156
+ *
157
+ * Nothing in `web` reads it — stage 4 was the last consumer. It survives
158
+ * only as part of this public type and goes away with the second matcher.
159
+ * **Read `request.query`.**
160
+ */
161
+ query: Record<string, string>;
162
+ };
163
+ appData?: unknown;
164
+ layoutData?: unknown;
165
+ pageData?: unknown; /** `sealShared()`'s RETURN — the sealed target, never a proxy re-read. */
166
+ shared?: Readonly<SharedContext>;
167
+ metadata?: MetadataOutput;
168
+ commit?: PageResponseCommit;
169
+ shortCircuit?: PageShortCircuit;
170
+ error?: PageErrorRecord;
171
+ };
172
+ //#endregion
173
+ export { ExecutePageRequestOptions, PageBoundaryDesignation, PageContextRunner, PageDataBundle, PageErrorRecord, PageLevelName, PageResponseCommit, PageRouteEntry, PageRouteMatch, PageShortCircuit, PageTripleModule, PipelineLoader, PipelineMiddleware, PipelineRequest, PipelineResponse, PipelineStore };
174
+ //# sourceMappingURL=execute-page-request.types.d.mts.map
@@ -1,7 +1,7 @@
1
1
  import { CLIENT_ASSET_URL_PREFIX } from "./client-asset-url-prefix.mjs";
2
2
  import { HYDRATION_CLIENT_ENTRY_NAME } from "../vite/hydration-entries.mjs";
3
- import { readFileSync } from "node:fs";
4
3
  import path from "node:path";
4
+ import { readFileSync } from "node:fs";
5
5
 
6
6
  //#region ../web/src/server/hydration-client-url.ts
7
7
  /**
@@ -0,0 +1,13 @@
1
+ import { composeRoutePath } from "../routing/compose-route-path.mjs";
2
+ import { connectSharedStore } from "../shared.mjs";
3
+ import { PAYLOAD_SCRIPT_ID, escapePayload } from "../components/document-context.mjs";
4
+ import { BufferedCookie, BufferedHeader, BufferedWebResponse, LOADER_SHORT_CIRCUIT, LoaderShortCircuitSignal, ResponseBuffer, createBufferedResponse, isLoaderShortCircuit } from "./buffered-response.mjs";
5
+ import { ExecutePageRequestOptions, PageBoundaryDesignation, PageContextRunner, PageDataBundle, PageLevelName, PageResponseCommit, PageRouteEntry, PageRouteMatch, PageShortCircuit, PageTripleModule, PipelineLoader, PipelineMiddleware, PipelineStore } from "./execute-page-request.types.mjs";
6
+ import { connectPageContext, connectPageSharedScope } from "./page-context.mjs";
7
+ import { executePageRequest } from "./execute-page-request.mjs";
8
+ import { InstallPageRoutesOptions, InstalledPageRoute, LayoutModuleShape, PageModuleShape, PageRouteExport, installPageRoutes } from "./install-page-routes.mjs";
9
+ import { PageRoutesRegistry, RenderPageOptions, RenderPageRequestOptions, RenderedPage, connectPageRoutes, renderPage, renderPageRequest } from "./render-page.mjs";
10
+ import { PageModuleNotInManifestError, createPageModuleLoader } from "./create-page-module-loader.mjs";
11
+ import { InstallPageRoutesFromManifestOptions, InstalledManifestPageRoute, PageRouteHandlerFactory, installPageRoutesFromManifest } from "./install-page-routes-from-manifest.mjs";
12
+ import { VITE_DIRECT_CSS_QUERY, devStylesheetUrls, productionStylesheetUrls } from "./stylesheet-urls.mjs";
13
+ export { type BufferedCookie, type BufferedHeader, type BufferedWebResponse, type ExecutePageRequestOptions, type InstallPageRoutesFromManifestOptions, type InstallPageRoutesOptions, type InstalledManifestPageRoute, type InstalledPageRoute, LOADER_SHORT_CIRCUIT, type LayoutModuleShape, type LoaderShortCircuitSignal, PAYLOAD_SCRIPT_ID, type PageBoundaryDesignation, type PageContextRunner, type PageDataBundle, type PageLevelName, PageModuleNotInManifestError, type PageModuleShape, type PageResponseCommit, type PageRouteEntry, type PageRouteExport, type PageRouteHandlerFactory, type PageRouteMatch, type PageRoutesRegistry, type PageShortCircuit, type PageTripleModule, type PipelineLoader, type PipelineMiddleware, type PipelineStore, type RenderPageOptions, type RenderPageRequestOptions, type RenderedPage, type ResponseBuffer, VITE_DIRECT_CSS_QUERY, composeRoutePath, connectPageContext, connectPageRoutes, connectPageSharedScope, connectSharedStore, createBufferedResponse, createPageModuleLoader, devStylesheetUrls, escapePayload, executePageRequest, installPageRoutes, installPageRoutesFromManifest, isLoaderShortCircuit, productionStylesheetUrls, renderPage, renderPageRequest };
@@ -1,13 +1,13 @@
1
1
  import { connectSharedStore } from "../shared.mjs";
2
2
  import { PAYLOAD_SCRIPT_ID, escapePayload } from "../components/document-context.mjs";
3
- import { composeRoutePath } from "../routing/compose-route-path.mjs";
4
3
  import { LOADER_SHORT_CIRCUIT, createBufferedResponse, isLoaderShortCircuit } from "./buffered-response.mjs";
5
- import { connectPageContext } from "./page-context.mjs";
4
+ import { connectPageContext, connectPageSharedScope } from "./page-context.mjs";
6
5
  import { executePageRequest } from "./execute-page-request.mjs";
7
- import { renderPageRequest } from "./render-page.mjs";
6
+ import { connectPageRoutes, renderPage, renderPageRequest } from "./render-page.mjs";
8
7
  import { PageModuleNotInManifestError, createPageModuleLoader } from "./create-page-module-loader.mjs";
8
+ import { composeRoutePath } from "../routing/compose-route-path.mjs";
9
9
  import { installPageRoutesFromManifest } from "./install-page-routes-from-manifest.mjs";
10
- import "./install-page-routes.mjs";
11
- import { productionStylesheetUrls } from "./stylesheet-urls.mjs";
10
+ import { installPageRoutes } from "./install-page-routes.mjs";
11
+ import { VITE_DIRECT_CSS_QUERY, devStylesheetUrls, productionStylesheetUrls } from "./stylesheet-urls.mjs";
12
12
 
13
- export { connectPageContext, connectSharedStore, installPageRoutesFromManifest, productionStylesheetUrls };
13
+ export { LOADER_SHORT_CIRCUIT, PAYLOAD_SCRIPT_ID, PageModuleNotInManifestError, VITE_DIRECT_CSS_QUERY, composeRoutePath, connectPageContext, connectPageRoutes, connectPageSharedScope, connectSharedStore, createBufferedResponse, createPageModuleLoader, devStylesheetUrls, escapePayload, executePageRequest, installPageRoutes, installPageRoutesFromManifest, isLoaderShortCircuit, productionStylesheetUrls, renderPage, renderPageRequest };