@warlock.js/web 5.5.0 → 5.6.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.
Files changed (138) hide show
  1. package/CHANGELOG.md +27 -5
  2. package/esm/build/contribution.d.mts +2 -2
  3. package/esm/build/contribution.mjs.map +1 -1
  4. package/esm/build/discover-pages.mjs +127 -205
  5. package/esm/build/discover-pages.mjs.map +1 -1
  6. package/esm/build/generate-pages-barrel.mjs +5 -3
  7. package/esm/build/generate-pages-barrel.mjs.map +1 -1
  8. package/esm/build/public-files.mjs +29 -1
  9. package/esm/build/public-files.mjs.map +1 -1
  10. package/esm/build/read-metadata-keys.mjs +225 -0
  11. package/esm/build/read-metadata-keys.mjs.map +1 -0
  12. package/esm/build/read-route-exports.mjs.map +1 -1
  13. package/esm/client/build-hydrated-tree.mjs +1 -1
  14. package/esm/client/build-hydrated-tree.mjs.map +1 -1
  15. package/esm/client/navigation/document-metadata.mjs +108 -0
  16. package/esm/client/navigation/document-metadata.mjs.map +1 -0
  17. package/esm/client/navigation/navigation-root.mjs +23 -103
  18. package/esm/client/navigation/navigation-root.mjs.map +1 -1
  19. package/esm/client/navigation/scroll-to-fragment.mjs.map +1 -1
  20. package/esm/client/navigation/use-query-string.d.mts +35 -0
  21. package/esm/client/navigation/use-query-string.mjs +68 -0
  22. package/esm/client/navigation/use-query-string.mjs.map +1 -0
  23. package/esm/client/runtime/index.d.mts +1 -1
  24. package/esm/client/runtime/index.mjs +1 -1
  25. package/esm/client/runtime/manifest.mjs.map +1 -1
  26. package/esm/client/runtime/matcher.mjs.map +1 -1
  27. package/esm/client/runtime/types.d.mts +1 -1
  28. package/esm/components/link.mjs.map +1 -1
  29. package/esm/core/src/router/normalize-route-path.mjs +33 -0
  30. package/esm/core/src/router/normalize-route-path.mjs.map +1 -0
  31. package/esm/{hydration → entry}/index.mjs +1 -1
  32. package/esm/entry/index.mjs.map +1 -0
  33. package/esm/hydration-payload.mjs.map +1 -1
  34. package/esm/index.d.mts +3 -2
  35. package/esm/index.mjs +3 -2
  36. package/esm/loaders.d.mts +10 -3
  37. package/esm/localization.mjs.map +1 -1
  38. package/esm/metadata.d.mts +4 -4
  39. package/esm/metadata.mjs.map +1 -1
  40. package/esm/node_modules/.pnpm/@mongez_concat-route@1.2.0/node_modules/@mongez/concat-route/esm/index.mjs +33 -0
  41. package/esm/node_modules/.pnpm/@mongez_concat-route@1.2.0/node_modules/@mongez/concat-route/esm/index.mjs.map +1 -0
  42. package/esm/{runtime/register-modules.d.mts → register-modules.d.mts} +1 -1
  43. package/esm/{runtime/register-modules.mjs → register-modules.mjs} +1 -1
  44. package/esm/register-modules.mjs.map +1 -0
  45. package/esm/route.d.mts +35 -2
  46. package/esm/routing/duplicate-route-path.mjs +22 -0
  47. package/esm/routing/duplicate-route-path.mjs.map +1 -0
  48. package/esm/routing/layout-level.mjs +50 -0
  49. package/esm/routing/layout-level.mjs.map +1 -0
  50. package/esm/routing/layout-policy.mjs.map +1 -1
  51. package/esm/routing/query-string.d.mts +23 -1
  52. package/esm/routing/query-string.mjs +23 -1
  53. package/esm/routing/query-string.mjs.map +1 -1
  54. package/esm/routing/route-identity.mjs +27 -2
  55. package/esm/routing/route-identity.mjs.map +1 -1
  56. package/esm/routing/route-table.mjs.map +1 -1
  57. package/esm/routing/router-events.d.mts +5 -4
  58. package/esm/routing/router-events.mjs.map +1 -1
  59. package/esm/server/create-page-route-handler.d.mts +2 -2
  60. package/esm/server/create-page-route-handler.mjs +6 -6
  61. package/esm/server/create-page-route-handler.mjs.map +1 -1
  62. package/esm/server/{dev-server.mjs → dev-error-transport.mjs} +2 -2
  63. package/esm/server/dev-error-transport.mjs.map +1 -0
  64. package/esm/server/execute-page-request.mjs +87 -23
  65. package/esm/server/execute-page-request.mjs.map +1 -1
  66. package/esm/server/execute-page-request.types.d.mts +10 -1
  67. package/esm/server/fold-layout-loaders.mjs +36 -0
  68. package/esm/server/fold-layout-loaders.mjs.map +1 -0
  69. package/esm/server/hydration-client-url.mjs +2 -2
  70. package/esm/server/hydration-client-url.mjs.map +1 -1
  71. package/esm/server/index.d.mts +2 -2
  72. package/esm/server/index.mjs +2 -2
  73. package/esm/server/install-page-routes-from-manifest.d.mts +1 -1
  74. package/esm/server/install-page-routes-from-manifest.mjs +43 -40
  75. package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
  76. package/esm/server/install-page-routes.d.mts +22 -2
  77. package/esm/server/install-page-routes.mjs +108 -42
  78. package/esm/server/install-page-routes.mjs.map +1 -1
  79. package/esm/server/install-production-page-routes.mjs.map +1 -1
  80. package/esm/server/match-page-route.mjs.map +1 -1
  81. package/esm/server/page-file-change.mjs.map +1 -1
  82. package/esm/server/page-route-reload.mjs +4 -3
  83. package/esm/server/page-route-reload.mjs.map +1 -1
  84. package/esm/server/register-production-public-files.mjs +25 -1
  85. package/esm/server/register-production-public-files.mjs.map +1 -1
  86. package/esm/server/render-page.d.mts +11 -48
  87. package/esm/server/render-page.mjs +13 -55
  88. package/esm/server/render-page.mjs.map +1 -1
  89. package/esm/server/resolve-page-metadata.mjs.map +1 -1
  90. package/esm/server/resolve-route-validation-input.mjs +11 -0
  91. package/esm/server/resolve-route-validation-input.mjs.map +1 -0
  92. package/esm/server/route-validation-error.mjs +32 -0
  93. package/esm/server/route-validation-error.mjs.map +1 -0
  94. package/esm/server/settle-page-response.mjs +5 -3
  95. package/esm/server/settle-page-response.mjs.map +1 -1
  96. package/esm/server/unregistered-pages.mjs +2 -1
  97. package/esm/server/unregistered-pages.mjs.map +1 -1
  98. package/esm/server/web-connector-factory.mjs +1 -1
  99. package/esm/server/web-connector-factory.mjs.map +1 -1
  100. package/esm/server/web-connector.mjs +24 -321
  101. package/esm/server/web-connector.mjs.map +1 -1
  102. package/esm/shared/module-key.mjs +15 -0
  103. package/esm/shared/module-key.mjs.map +1 -0
  104. package/esm/shared/to-posix.mjs +13 -0
  105. package/esm/shared/to-posix.mjs.map +1 -0
  106. package/esm/shared.mjs +24 -0
  107. package/esm/shared.mjs.map +1 -1
  108. package/esm/vite/build-client.mjs.map +1 -1
  109. package/esm/vite/dev-server-config.mjs +240 -0
  110. package/esm/vite/dev-server-config.mjs.map +1 -0
  111. package/esm/vite/gate-a-resolve.d.mts +5 -4
  112. package/esm/vite/gate-a-resolve.mjs +31 -70
  113. package/esm/vite/gate-a-resolve.mjs.map +1 -1
  114. package/esm/vite/gate-b-secrets.mjs.map +1 -1
  115. package/esm/vite/gate-c-verify.d.mts +1 -1
  116. package/esm/vite/gate-c-verify.mjs +1 -1
  117. package/esm/vite/gate-c-verify.mjs.map +1 -1
  118. package/esm/vite/hydration-entries.mjs +5 -7
  119. package/esm/vite/hydration-entries.mjs.map +1 -1
  120. package/esm/vite/index.d.mts +1 -1
  121. package/esm/vite/index.mjs +4 -110
  122. package/esm/vite/index.mjs.map +1 -1
  123. package/esm/vite/page-registry-plugin.mjs +9 -26
  124. package/esm/vite/page-registry-plugin.mjs.map +1 -1
  125. package/esm/vite/projection.mjs.map +1 -1
  126. package/esm/vite/react-refresh-preamble.mjs +90 -0
  127. package/esm/vite/react-refresh-preamble.mjs.map +1 -0
  128. package/esm/vite/ssr-client-view.mjs +135 -0
  129. package/esm/vite/ssr-client-view.mjs.map +1 -0
  130. package/llms-full.txt +558 -482
  131. package/llms.txt +2 -2
  132. package/package.json +6 -6
  133. package/skills/create-a-page/SKILL.md +348 -295
  134. package/skills/navigate-on-the-client/SKILL.md +212 -189
  135. package/esm/hydration/index.mjs.map +0 -1
  136. package/esm/runtime/register-modules.mjs.map +0 -1
  137. package/esm/server/dev-server.mjs.map +0 -1
  138. /package/esm/{hydration → entry}/index.d.mts +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"discover-pages.mjs","names":[],"sources":["../../../../../../../web/src/build/discover-pages.ts"],"sourcesContent":["/**\r\n * Static discovery of the application's page graph — the ONE scan every\r\n * provider shares.\r\n *\r\n * This module has a single responsibility: look at the filesystem and produce\r\n * the recipe. It writes nothing, knows nothing about the generated barrel, and\r\n * imports neither Vite nor a single application module. Everything that turns\r\n * the recipe into an artefact (the production barrel, the dev plugin, the\r\n * generated client entry) lives with that artefact and consumes\r\n * {@link discoverPages}.\r\n *\r\n * The reason it is one module rather than one function per consumer: two\r\n * scanners that agree today drift tomorrow, and a page that exists for the\r\n * server but not the client is the silent failure that costs a day to find.\r\n * Sharing the scan makes that disagreement impossible by construction instead\r\n * of catchable by test.\r\n *\r\n * \"Static\" means WITHOUT RUNNING THE APPLICATION — this module globs\r\n * `*.page.tsx`, `layout.tsx` and `root.tsx`, and reads each page's declared\r\n * `route` and each layout's declared `prefix` by PARSING the source\r\n * ({@link readRouteExports}). It still imports no application module.\r\n *\r\n * An explicit `route` export wins. Otherwise the URL is derived from the page's\r\n * path beneath `src/web`: directories contribute segments, `(groups)` do not,\r\n * `index.page.tsx` claims its directory, and `[id]` becomes `:id`. A layout\r\n * `prefix` replaces its own directory segment. The same recipe is emitted to\r\n * every provider so build and boot cannot drift.\r\n *\r\n * Discovery also CLASSIFIES each layout — does its module have a default\r\n * export, does it export `middleware` — because the layout policy\r\n * ({@link \"../routing/layout-policy.ts\"}) owns the rule but may not touch a\r\n * filesystem to learn the facts the rule needs. That classification is another\r\n * parse, never an import: a layout is read exactly the way a page's `route` is.\r\n *\r\n * And it CHECKS the page's `metadata` keys ({@link UnknownMetadataKeyError}) —\r\n * see that error and {@link readMetadataKeys} for why a type alone does not\r\n * close that hole.\r\n */\r\nimport fs from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport { parse } from \"@babel/parser\";\r\nimport { composeRoutePath } from \"../routing/compose-route-path\";\r\n// `../server/not-found-page` is imported for its CONSTANTS and its filename\r\n// predicate only. Its sole runtime import is the default fallback's build-time\r\n// stylesheet data URL; it imports no renderer or application runtime, so this\r\n// edge cannot drag the render pipeline into the build. What a not-found page is,\r\n// and what identity it carries, is decided in one place for discovery and both\r\n// installers.\r\nimport {\r\n isNotFoundPageFile,\r\n NotFoundPageDeclaresRouteError,\r\n NOT_FOUND_ROUTE_NAME,\r\n NOT_FOUND_ROUTE_PATH,\r\n} from \"../server/not-found-page\";\r\n// The metadata key set, as VALUES. `../metadata` is a types-plus-constants\r\n// module with no runtime imports of its own (both its imports are `import\r\n// type`), so this costs the build nothing and buys the one thing a parse cannot\r\n// get from a type: the list of keys a page is allowed to write.\r\nimport { METADATA_KEYS, OPEN_GRAPH_KEYS, TWITTER_KEYS } from \"../metadata\";\r\nimport { NestedLayoutsNotSupportedError, selectPageLayout } from \"../routing/layout-policy\";\r\nimport { deriveFilesystemRoutePath } from \"../routing/filesystem-route\";\r\nimport { canonicalizeRouteExport, resolvePageRouteName } from \"../routing/route-identity\";\r\nimport { assertPageHasDefaultExport } from \"./page-default-export\";\r\nimport type { RouteExportsReadResult } from \"./read-route-exports\";\r\nimport { NonLiteralRouteExportError, readRouteExports } from \"./read-route-exports\";\r\n\r\nexport type DiscoverPagesOptions = {\r\n /** Absolute path to the application root (where `package.json` lives). */\r\n appRoot: string;\r\n /** Source directory name under `appRoot`; defaults to `\"src\"`. */\r\n srcDir?: string;\r\n};\r\n\r\nexport type DiscoveredRoutablePage = {\r\n /** A browsable page. Error pages are deliberately a different discovery kind. */\r\n type: \"page\";\r\n /**\r\n * The page's route name: the `name` its `route` export declares, or its\r\n * filesystem identity as dotted segments, with groups omitted.\r\n *\r\n * Unique across the whole graph — {@link discoverPages} refuses to return a\r\n * result where it is not.\r\n */\r\n routeName: string;\r\n /**\r\n * The page's effective route path. Explicit routes retain their existing\r\n * layout-prefix composition. Derived routes use filesystem segments, with\r\n * each declared layout prefix replacing that layout directory's segment.\r\n * Always `/`-prefixed.\r\n */\r\n routePath: string;\r\n /** Absolute path to the `*.page.tsx` file. */\r\n pageFile: string;\r\n /** The web root this page was found under — the root its layout chain climbs to. */\r\n webRoot: string;\r\n /** Absolute paths of every `layout.tsx` from the web root down to the page's own directory, OUTERMOST FIRST. */\r\n layouts: string[];\r\n /**\r\n * The page's middleware chain: the subset of {@link layouts} whose modules\r\n * export `middleware`, OUTERMOST FIRST — the order they must run in.\r\n *\r\n * Source files rather than the middleware values themselves, because\r\n * discovery never runs application code: this says WHICH layouts contribute a\r\n * guard, and the consumer that already loads those modules reads the values\r\n * off them.\r\n *\r\n * Empty when no layout on the path declares middleware, which is the common\r\n * case; a page with no layouts always has an empty chain.\r\n */\r\n middlewareLayouts: string[];\r\n /** Absolute path to the global `root.tsx` every page renders inside, when it exists. */\r\n appFile?: string;\r\n};\r\n\r\n/** The one application error boundary. It deliberately has no route identity. */\r\nexport type DiscoveredErrorPage = {\r\n type: \"error\";\r\n /** Absolute path to the sole `error.page.tsx` beneath `src/web`. */\r\n pageFile: string;\r\n webRoot: string;\r\n appFile?: string;\r\n};\r\n\r\n/** The complete static web graph: routable leaves plus the optional error boundary. */\r\nexport type DiscoveredPage = DiscoveredRoutablePage | DiscoveredErrorPage;\r\n\r\nexport function isDiscoveredRoutablePage(page: DiscoveredPage): page is DiscoveredRoutablePage {\r\n return page.type === \"page\";\r\n}\r\n\r\nexport function isErrorPageFile(file: string): boolean {\r\n return path.basename(file) === \"error.page.tsx\";\r\n}\r\n\r\nexport class DuplicateErrorPageError extends Error {\r\n public constructor(firstFile: string, secondFile: string) {\r\n super(\r\n `Two error pages were found: \"${firstFile}\" and \"${secondFile}\". An application may own exactly one \\`error.page.tsx\\` anywhere beneath src/web.`,\r\n );\r\n this.name = \"DuplicateErrorPageError\";\r\n }\r\n}\r\n\r\nexport class ErrorPageDeclaresRouteError extends Error {\r\n public constructor(pageFile: string) {\r\n super(\r\n `The error page \"${pageFile}\" exports \\`route\\`. error.page.tsx is an error boundary, not a browsable page; remove the route export.`,\r\n );\r\n this.name = \"ErrorPageDeclaresRouteError\";\r\n }\r\n}\r\n\r\n/** Raised when two pages claim one route name. */\r\nexport class DuplicatePageRouteNameError extends Error {\r\n public constructor(\r\n public readonly routeName: string,\r\n public readonly firstFile: string,\r\n public readonly secondFile: string,\r\n ) {\r\n super(\r\n `Two pages resolve to the same route name \"${routeName}\": \"${firstFile}\" and ` +\r\n `\"${secondFile}\". A route name identifies exactly one page, so the second ` +\r\n \"page would be unreachable. To fix: rename one of the files, or move it so \" +\r\n \"its directory gives it a different route name.\",\r\n );\r\n this.name = \"DuplicatePageRouteNameError\";\r\n }\r\n}\r\n\r\n/** Raised when two different page files resolve to one effective URL. */\r\nexport class DuplicatePageRoutePathError extends Error {\r\n public constructor(\r\n public readonly routePath: string,\r\n public readonly firstFile: string,\r\n public readonly secondFile: string,\r\n ) {\r\n super(\r\n `Two pages resolve to the same route path \"${routePath}\": \"${firstFile}\" and ` +\r\n `\"${secondFile}\". Each URL may identify exactly one page. Rename or move one file, ` +\r\n \"or give one page an explicit route with a different path.\",\r\n );\r\n this.name = \"DuplicatePageRoutePathError\";\r\n }\r\n}\r\n\r\nexport function toPosix(value: string): string {\r\n return value.replace(/\\\\/g, \"/\");\r\n}\r\n\r\nfunction isDirectory(candidate: string): boolean {\r\n try {\r\n return fs.statSync(candidate).isDirectory();\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\nexport function isFile(candidate: string): boolean {\r\n try {\r\n return fs.statSync(candidate).isFile();\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * The page root. It is optional; a project without it has zero pages, which is\r\n * a legal empty state.\r\n */\r\nexport function discoverWebRoots(srcRoot: string): string[] {\r\n const webRoot = path.join(srcRoot, \"web\");\r\n\r\n return isDirectory(webRoot) ? [webRoot] : [];\r\n}\r\n\r\nfunction byName(left: { name: string }, right: { name: string }): number {\r\n return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;\r\n}\r\n\r\nexport type DiscoveredPageFile = {\r\n /** Absolute path to the `*.page.tsx` file. */\r\n pageFile: string;\r\n /** The web root ({@link discoverWebRoots}) this page was found under. */\r\n webRoot: string;\r\n};\r\n\r\n/**\r\n * The subject list: every `*.page.tsx` under the page root, one call for the\r\n * whole graph.\r\n *\r\n * Unlike {@link discoverPages}, this reads no `route` or `prefix` export and\r\n * throws on nothing — it answers only \"which page files exist\", so a provider\r\n * that still resolves a page's route by its own means (dev's\r\n * `ssrLoadModule`-driven installer, chiefly) can share the walk without\r\n * inheriting the static-parsing refusals that answering \"what route is this\"\r\n * requires.\r\n */\r\nexport function discoverPageFiles(srcRoot: string): DiscoveredPageFile[] {\r\n const found: DiscoveredPageFile[] = [];\r\n\r\n for (const webRoot of discoverWebRoots(srcRoot)) {\r\n for (const pageFile of walkFiles(webRoot, (fileName) => fileName.endsWith(\".page.tsx\"))) {\r\n found.push({ pageFile, webRoot });\r\n }\r\n }\r\n\r\n return found;\r\n}\r\n\r\n/** Every file under `dir` (recursive) whose name matches `predicate`. */\r\nexport function walkFiles(dir: string, predicate: (fileName: string) => boolean): string[] {\r\n const found: string[] = [];\r\n\r\n for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort(byName)) {\r\n const full = path.join(dir, entry.name);\r\n\r\n if (entry.isDirectory()) {\r\n found.push(...walkFiles(full, predicate));\r\n } else if (entry.isFile() && predicate(entry.name)) {\r\n found.push(full);\r\n }\r\n }\r\n\r\n return found;\r\n}\r\n\r\n/**\r\n * A page's layout chain: every `layout.tsx` in the directories from its web\r\n * root down to its own directory, OUTERMOST FIRST.\r\n *\r\n * Dev's installer reads only the nearest layout today, because its page module\r\n * triple holds a single layout; the recipe carries the whole chain so the\r\n * runtime side can compose\r\n * nested layouts without a second discovery pass. The nearest layout is always\r\n * the LAST element, so a consumer that still wants dev's one-layout behaviour\r\n * reads `layouts.at(-1)`.\r\n */\r\nexport function layoutChainFor(pageFile: string, webRoot: string): string[] {\r\n const chain: string[] = [];\r\n const relativeDir = path.relative(webRoot, path.dirname(pageFile));\r\n const segments = relativeDir === \"\" ? [] : relativeDir.split(path.sep);\r\n\r\n let current = webRoot;\r\n\r\n for (let index = 0; index <= segments.length; index++) {\r\n if (index > 0) {\r\n current = path.join(current, segments[index - 1]);\r\n }\r\n\r\n const candidate = path.join(current, \"layout.tsx\");\r\n\r\n if (isFile(candidate)) {\r\n chain.push(candidate);\r\n }\r\n }\r\n\r\n return chain;\r\n}\r\n\r\nfunction compareStrings(left: string, right: string): number {\r\n return left < right ? -1 : left > right ? 1 : 0;\r\n}\r\n\r\n/**\r\n * The total order every consumer sees, and the reason discovery returns an\r\n * array rather than a set: the page's SOURCE FILE PATH, lexicographic, POSIX.\r\n *\r\n * Serialization order only; matching precedence is a property of the route\r\n * grammar, not of this array.\r\n *\r\n * The file path rather than the route path, now that the route is the declared\r\n * one: a page's route can be rewritten by editing one line, which would reorder\r\n * an artefact that has not otherwise changed, while the file it lives in is the\r\n * stable identity the artefact is built from. It also carries no suggestion of\r\n * precedence — nobody reads \"sorted by file name\" as \"most specific first\",\r\n * which is the misreading a route-path order invites.\r\n *\r\n * Lexicographic because it is byte-comparable output across every provider,\r\n * which keeps diffs stable and keeps filesystem enumeration order out of the\r\n * artefact: two machines that list a directory differently must still produce\r\n * byte-identical output, or a build is only reproducible by luck.\r\n */\r\nfunction comparePages(left: DiscoveredPage, right: DiscoveredPage): number {\r\n return compareStrings(toPosix(left.pageFile), toPosix(right.pageFile));\r\n}\r\n\r\nfunction assertUniqueRouteNames(pages: readonly DiscoveredRoutablePage[], appRoot: string): void {\r\n const fileByRouteName = new Map<string, string>();\r\n\r\n for (const page of pages) {\r\n const existing = fileByRouteName.get(page.routeName);\r\n const relative = toPosix(path.relative(appRoot, page.pageFile));\r\n\r\n if (existing !== undefined) {\r\n throw new DuplicatePageRouteNameError(page.routeName, existing, relative);\r\n }\r\n\r\n fileByRouteName.set(page.routeName, relative);\r\n }\r\n}\r\n\r\n/**\r\n * A page's declared `route.path` is a choice its author made deliberately, so\r\n * two pages that both declare the same path are the author's call, not an\r\n * accident — {@link assertUniqueRouteNames} is what keeps each of them\r\n * reachable by name. What this refuses is a COLLISION NOBODY CHOSE: at least\r\n * one side's path is filesystem-derived, which is exactly the case a rename\r\n * or a new file can produce without anyone noticing two pages now answer the\r\n * same URL. The reserved not-found path (`\"*\"`) is never a candidate here\r\n * either way — it identifies no browsable URL, and {@link assertUniqueRouteNames}\r\n * already refuses a second `404.page.tsx` by its own reserved name.\r\n */\r\nfunction assertUniqueRoutePaths(\r\n pages: readonly DiscoveredRoutablePage[],\r\n appRoot: string,\r\n explicitFiles: ReadonlySet<string>,\r\n): void {\r\n const fileByRoutePath = new Map<string, string>();\r\n\r\n for (const page of pages) {\r\n if (page.routePath === NOT_FOUND_ROUTE_PATH) continue;\r\n\r\n const existing = fileByRoutePath.get(page.routePath);\r\n const relative = toPosix(path.relative(appRoot, page.pageFile));\r\n\r\n if (existing !== undefined && !(explicitFiles.has(existing) && explicitFiles.has(relative))) {\r\n throw new DuplicatePageRoutePathError(page.routePath, existing, relative);\r\n }\r\n\r\n if (existing === undefined) fileByRoutePath.set(page.routePath, relative);\r\n }\r\n}\r\n\r\n/** One key a page's `metadata` declares that nothing reads, and where it is written. */\r\nexport type UnknownMetadataKey = {\r\n /** The object it was declared in: `metadata`, `metadata.openGraph`, `metadata.twitter`. */\r\n container: string;\r\n /** The key exactly as the page wrote it. */\r\n key: string;\r\n /** 1-based line in the page file, so the message points at the character that is wrong. */\r\n line: number;\r\n /** The known key it is within two edits of, when there is one. Usually the whole answer. */\r\n suggestion?: string;\r\n};\r\n\r\n/**\r\n * Raised when a page's `metadata` export declares a key nothing reads.\r\n *\r\n * THE POINT OF THIS ERROR IS THE UNANNOTATED CASE. A page that writes\r\n * `export const metadata: PageMetadata = { tittle: \"x\" }` is already refused by\r\n * TypeScript, and if that were the whole story this class would not need to\r\n * exist. But the annotation is optional, nobody writes it, and\r\n * `export const metadata = { tittle: \"x\" }` is a perfectly well-typed program:\r\n * the compiler infers `{ tittle: string }`, has nothing to check it against, and\r\n * says nothing. The page is then served with no `<title>` — not a wrong title, a\r\n * missing one — and no error is raised anywhere, at build or at runtime, ever.\r\n *\r\n * So the check lives HERE instead, at the gate every page already passes\r\n * through: a page that silently omits requested metadata is worse than a build\r\n * that stops and says which line to fix.\r\n *\r\n * The alternative considered and rejected was a `defineMetadata({...})` wrapper,\r\n * which would infer the type for free. It also puts framework ceremony in every\r\n * page, and a page is meant to be two lines of framework surface (canon\r\n * `6ea0662f`). The gate gets the same safety without spending that.\r\n */\r\nexport class UnknownMetadataKeyError extends Error {\r\n public constructor(\r\n public readonly pageFile: string,\r\n public readonly unknownKeys: readonly UnknownMetadataKey[],\r\n ) {\r\n const findings = unknownKeys\r\n .map(({ container, key, line, suggestion }) => {\r\n const where = `line ${line}: \\`${container}.${key}\\` — no such key.`;\r\n\r\n return suggestion === undefined ? where : `${where} Did you mean \\`${suggestion}\\`?`;\r\n })\r\n .join(\"\\n \");\r\n\r\n super(\r\n `The \\`metadata\\` export of \"${pageFile}\" declares a key nothing reads:\\n ${findings}\\n` +\r\n \"Nothing writes an unknown key to `<head>`, so the tag it was meant to produce would \" +\r\n \"simply be absent from every response, with no error at build time or at runtime. The \" +\r\n \"build refuses it here instead.\\n\" +\r\n ` Known keys: ${METADATA_KEYS.join(\", \")}.\\n` +\r\n ` Inside \\`openGraph\\`: ${OPEN_GRAPH_KEYS.join(\", \")}.\\n` +\r\n ` Inside \\`twitter\\`: ${TWITTER_KEYS.join(\", \")}.\\n` +\r\n \"Annotating the export — `export const metadata: PageMetadata = { … }` — gets you the \" +\r\n \"same list as autocomplete in the editor, before the build runs.\",\r\n );\r\n this.name = \"UnknownMetadataKeyError\";\r\n }\r\n}\r\n\r\n/**\r\n * The AST types, derived from `parse`'s own return type rather than imported\r\n * from `@babel/types`, for the reason `read-route-exports.ts` gives: the parser\r\n * resolves its own copy of that package and nodes from one copy are not\r\n * assignable to the identical types from the other.\r\n */\r\ntype PageStatement = ReturnType<typeof parse>[\"program\"][\"body\"][number];\r\ntype PageExpression = Extract<PageStatement, { type: \"ExpressionStatement\" }>[\"expression\"];\r\ntype PageObjectExpression = Extract<PageExpression, { type: \"ObjectExpression\" }>;\r\ntype PageObjectProperty = Extract<\r\n PageObjectExpression[\"properties\"][number],\r\n { type: \"ObjectProperty\" }\r\n>;\r\ntype PageValueNode = PageObjectProperty[\"value\"];\r\n\r\n/** `as const`, `satisfies`, `!` and parentheses wrap a value without changing it. */\r\nfunction unwrapValue(node: PageValueNode): PageValueNode {\r\n switch (node.type) {\r\n case \"TSAsExpression\":\r\n case \"TSSatisfiesExpression\":\r\n case \"TSNonNullExpression\":\r\n case \"TypeCastExpression\":\r\n case \"ParenthesizedExpression\":\r\n return unwrapValue(node.expression);\r\n default:\r\n return node;\r\n }\r\n}\r\n\r\n/** Levenshtein distance — small strings, so the plain two-row table is the whole cost. */\r\nfunction editDistance(left: string, right: string): number {\r\n let previous = Array.from({ length: right.length + 1 }, (_, index) => index);\r\n\r\n for (let row = 1; row <= left.length; row++) {\r\n const current = [row];\r\n\r\n for (let column = 1; column <= right.length; column++) {\r\n const substitution = previous[column - 1] + (left[row - 1] === right[column - 1] ? 0 : 1);\r\n current[column] = Math.min(substitution, previous[column] + 1, current[column - 1] + 1);\r\n }\r\n\r\n previous = current;\r\n }\r\n\r\n return previous[right.length];\r\n}\r\n\r\n/**\r\n * The known key the written one was probably meant to be.\r\n *\r\n * Two edits, because that covers the typos this exists for — `tittle`,\r\n * `descriptoin`, `keywrods` — without reaching so far that `image` gets\r\n * suggested for `alt`. Case is ignored first, so `Title` resolves exactly.\r\n */\r\nfunction suggestKey(written: string, known: readonly string[]): string | undefined {\r\n const lowered = written.toLowerCase();\r\n const sameLetters = known.find((candidate) => candidate.toLowerCase() === lowered);\r\n\r\n if (sameLetters !== undefined) return sameLetters;\r\n\r\n let best: string | undefined;\r\n let bestDistance = Number.POSITIVE_INFINITY;\r\n\r\n for (const candidate of known) {\r\n const distance = editDistance(lowered, candidate.toLowerCase());\r\n\r\n if (distance < bestDistance) {\r\n best = candidate;\r\n bestDistance = distance;\r\n }\r\n }\r\n\r\n return bestDistance <= 2 ? best : undefined;\r\n}\r\n\r\n/** The name an object key denotes, or `undefined` when knowing it needs evaluation. */\r\nfunction propertyKeyName(property: PageObjectProperty): string | undefined {\r\n if (property.computed) return undefined;\r\n\r\n const { key } = property;\r\n\r\n if (key.type === \"Identifier\") return key.name;\r\n if (key.type === \"StringLiteral\") return key.value;\r\n\r\n return undefined;\r\n}\r\n\r\n/** The nested objects that carry a key set of their own. */\r\nconst NESTED_METADATA_KEYS: Record<string, readonly string[]> = {\r\n openGraph: OPEN_GRAPH_KEYS,\r\n twitter: TWITTER_KEYS,\r\n};\r\n\r\n/**\r\n * Every unknown key in one metadata object literal, and in the `openGraph` /\r\n * `twitter` literals inside it.\r\n *\r\n * A SPREAD does not suppress the check, unlike the route reader's rule: a\r\n * spread can only ADD keys, and no value it contributes can make a key written\r\n * out beside it correct. A COMPUTED key is skipped — its name is not knowable\r\n * without running the page, and refusing what cannot be read would fail builds\r\n * that are fine. Both are silence in the narrow places where the parse genuinely\r\n * does not know, and the annotation is the second net there.\r\n */\r\nfunction collectUnknownKeys(\r\n object: PageObjectExpression,\r\n allowed: readonly string[],\r\n container: string,\r\n into: UnknownMetadataKey[],\r\n): void {\r\n for (const property of object.properties) {\r\n if (property.type === \"SpreadElement\") continue;\r\n\r\n const key = propertyKeyName(property as PageObjectProperty);\r\n\r\n if (key === undefined) continue;\r\n\r\n if (!allowed.includes(key)) {\r\n const suggestion = suggestKey(key, allowed);\r\n\r\n into.push({\r\n container,\r\n key,\r\n line: property.loc?.start.line ?? 0,\r\n ...(suggestion === undefined ? {} : { suggestion }),\r\n });\r\n\r\n continue;\r\n }\r\n\r\n const nested = container === \"metadata\" ? NESTED_METADATA_KEYS[key] : undefined;\r\n\r\n if (nested === undefined || property.type !== \"ObjectProperty\") continue;\r\n\r\n const value = unwrapValue(property.value);\r\n\r\n if (value.type === \"ObjectExpression\") {\r\n collectUnknownKeys(value, nested, `${container}.${key}`, into);\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Every object literal a function form RETURNS, without descending into\r\n * functions nested inside it — a callback's return value is not the metadata.\r\n *\r\n * A generic walk rather than a statement-by-statement one because a `return` is\r\n * legal anywhere a statement is: inside an `if`, a `switch`, a `try`. Enumerating\r\n * the statement types that may contain one is a list that is wrong the moment\r\n * the language grows.\r\n */\r\nfunction collectReturnedObjects(node: unknown, into: PageObjectExpression[]): void {\r\n if (node === null || typeof node !== \"object\") return;\r\n\r\n if (Array.isArray(node)) {\r\n for (const item of node) collectReturnedObjects(item, into);\r\n\r\n return;\r\n }\r\n\r\n const candidate = node as { type?: string; argument?: unknown };\r\n\r\n if (\r\n candidate.type === \"FunctionDeclaration\" ||\r\n candidate.type === \"FunctionExpression\" ||\r\n candidate.type === \"ArrowFunctionExpression\" ||\r\n candidate.type === \"ObjectMethod\" ||\r\n candidate.type === \"ClassMethod\"\r\n ) {\r\n return;\r\n }\r\n\r\n if (candidate.type === \"ReturnStatement\") {\r\n if (candidate.argument === null || candidate.argument === undefined) return;\r\n\r\n const returned = unwrapValue(candidate.argument as PageValueNode);\r\n\r\n if (returned.type === \"ObjectExpression\") into.push(returned);\r\n\r\n // Not descending into the returned value: a `return` inside it belongs to a\r\n // function this walk is deliberately not entering.\r\n return;\r\n }\r\n\r\n for (const value of Object.values(node as Record<string, unknown>)) {\r\n collectReturnedObjects(value, into);\r\n }\r\n}\r\n\r\n/** The metadata object literals one `metadata` export declares, if any can be seen at all. */\r\nfunction metadataObjectsOf(init: PageValueNode): PageObjectExpression[] {\r\n const value = unwrapValue(init);\r\n\r\n if (value.type === \"ObjectExpression\") return [value];\r\n\r\n if (value.type === \"ArrowFunctionExpression\" || value.type === \"FunctionExpression\") {\r\n const body = unwrapValue(value.body as PageValueNode);\r\n\r\n // The concise arrow body — `({ data }) => ({ title: data.name })`, which is\r\n // how every function-form metadata export in the reference app is written.\r\n if (body.type === \"ObjectExpression\") return [body];\r\n\r\n const returned: PageObjectExpression[] = [];\r\n\r\n collectReturnedObjects(value.body, returned);\r\n\r\n return returned;\r\n }\r\n\r\n // `export const metadata = buildMetadata()`, or a bare identifier: the keys\r\n // are not in this file. Silent by design — see `collectUnknownKeys`.\r\n return [];\r\n}\r\n\r\n/**\r\n * The unknown keys a page's `metadata` export declares, read by PARSING — the\r\n * same rule the rest of this module lives by, and the reason this check can run\r\n * before anything is built.\r\n *\r\n * Empty for a page with no `metadata` export, for one whose metadata is a value\r\n * this file cannot see into, and for a correct one.\r\n */\r\nexport function readMetadataKeys(pageFile: string, source: string): UnknownMetadataKey[] {\r\n let program: ReturnType<typeof parse>[\"program\"];\r\n\r\n try {\r\n program = parse(source, {\r\n sourceType: \"module\",\r\n plugins: [\"typescript\", \"jsx\"],\r\n errorRecovery: false,\r\n }).program;\r\n } catch (error) {\r\n throw new Error(\r\n `Cannot read the \\`metadata\\` export of \"${pageFile}\": the file could not be parsed ` +\r\n `(${(error as Error).message}). Fix the syntax error and the build will continue.`,\r\n );\r\n }\r\n\r\n const unknownKeys: UnknownMetadataKey[] = [];\r\n\r\n for (const statement of program.body) {\r\n if (statement.type !== \"ExportNamedDeclaration\" || statement.exportKind === \"type\") continue;\r\n\r\n const { declaration } = statement;\r\n\r\n if (declaration?.type !== \"VariableDeclaration\") continue;\r\n\r\n for (const declarator of declaration.declarations) {\r\n if (declarator.id.type !== \"Identifier\" || declarator.id.name !== \"metadata\") continue;\r\n if (declarator.init === null || declarator.init === undefined) continue;\r\n\r\n for (const object of metadataObjectsOf(declarator.init)) {\r\n collectUnknownKeys(object, METADATA_KEYS, \"metadata\", unknownKeys);\r\n }\r\n }\r\n }\r\n\r\n return unknownKeys;\r\n}\r\n\r\n/**\r\n * The declared exports of one file, or a thrown\r\n * {@link NonLiteralRouteExportError} when they cannot be read without running\r\n * the application. Layouts are read once per run and remembered: a layout is\r\n * the nearest one for every page beside it, and parsing it once per page would\r\n * be the same answer bought repeatedly.\r\n *\r\n * `source` is the file's text when the caller already holds it — the page loop\r\n * reads each page once and spends that read on both the route declarations and\r\n * the metadata check, rather than opening the same file twice.\r\n */\r\nfunction readDeclarations(\r\n sourceFile: string,\r\n cache: Map<string, RouteExportsReadResult>,\r\n source?: string,\r\n) {\r\n let result = cache.get(sourceFile);\r\n\r\n if (result === undefined) {\r\n result = readRouteExports(sourceFile, source);\r\n cache.set(sourceFile, result);\r\n }\r\n\r\n if (!result.ok) {\r\n throw new NonLiteralRouteExportError(result.rejection);\r\n }\r\n\r\n return result;\r\n}\r\n\r\n/**\r\n * What a layout DOES, read by parsing it — the facts the layout policy's rule\r\n * needs and, being a pure module, cannot go and find for itself.\r\n */\r\ntype LayoutShape = {\r\n /**\r\n * Whether the module has a default export — the export that puts an element\r\n * in the document, and therefore the one thing that makes a layout count\r\n * against the single-rendering-layout rule.\r\n */\r\n renders: boolean;\r\n /** Whether the module exports `middleware`, or might via a re-export this cannot see through. */\r\n hasMiddleware: boolean;\r\n};\r\n\r\n/**\r\n * Parses one layout and reports its shape. Remembered per run for the same\r\n * reason declarations are: a layout is on the path of every page beneath it.\r\n */\r\nfunction readLayoutShape(layoutFile: string, cache: Map<string, LayoutShape>): LayoutShape {\r\n const cached = cache.get(layoutFile);\r\n\r\n if (cached !== undefined) return cached;\r\n\r\n const source = fs.readFileSync(layoutFile, \"utf-8\");\r\n let program: ReturnType<typeof parse>[\"program\"];\r\n\r\n try {\r\n program = parse(source, {\r\n sourceType: \"module\",\r\n // Every file this reads is a layout, i.e. `.tsx`.\r\n plugins: [\"typescript\", \"jsx\"],\r\n errorRecovery: false,\r\n }).program;\r\n } catch (error) {\r\n throw new Error(\r\n `Cannot read the exports of \"${layoutFile}\": the file could not be parsed ` +\r\n `(${(error as Error).message}). Fix the syntax error and the build will continue.`,\r\n );\r\n }\r\n\r\n const shape: LayoutShape = { renders: false, hasMiddleware: false };\r\n\r\n for (const statement of program.body) {\r\n if (statement.type === \"ExportDefaultDeclaration\") {\r\n shape.renders = true;\r\n continue;\r\n }\r\n\r\n // `export * from \"./guard\"` cannot re-export a default — the language\r\n // excludes it — but it CAN contribute `middleware`, and no parse can see\r\n // through it without resolving and reading another module. Reading it as\r\n // \"no middleware here\" is exactly the silent unguarding this slice exists\r\n // to prevent, so it is read as \"possibly\" and fails loudly downstream.\r\n if (statement.type === \"ExportAllDeclaration\") {\r\n shape.hasMiddleware = true;\r\n continue;\r\n }\r\n\r\n if (statement.type !== \"ExportNamedDeclaration\" || statement.exportKind === \"type\") continue;\r\n\r\n for (const specifier of statement.specifiers) {\r\n if (specifier.type !== \"ExportSpecifier\" || specifier.exportKind === \"type\") continue;\r\n\r\n const exported =\r\n specifier.exported.type === \"Identifier\"\r\n ? specifier.exported.name\r\n : specifier.exported.value;\r\n\r\n if (exported === \"default\") shape.renders = true;\r\n if (exported === \"middleware\") shape.hasMiddleware = true;\r\n }\r\n\r\n const { declaration } = statement;\r\n\r\n if (declaration === null || declaration === undefined) continue;\r\n\r\n if (declaration.type === \"VariableDeclaration\") {\r\n for (const declarator of declaration.declarations) {\r\n if (declarator.id.type === \"Identifier\" && declarator.id.name === \"middleware\") {\r\n shape.hasMiddleware = true;\r\n }\r\n }\r\n\r\n continue;\r\n }\r\n\r\n if (\r\n (declaration.type === \"FunctionDeclaration\" || declaration.type === \"ClassDeclaration\") &&\r\n declaration.id?.name === \"middleware\"\r\n ) {\r\n shape.hasMiddleware = true;\r\n }\r\n }\r\n\r\n cache.set(layoutFile, shape);\r\n\r\n return shape;\r\n}\r\n\r\n/**\r\n * Scans the page root and returns the pages in a defined total order.\r\n *\r\n * Zero pages is a legal result, not an error: a project may be configured\r\n * with web and have nothing to serve yet. What is an error is a `*.page.tsx`\r\n * with a route or metadata export that cannot be read statically; two pages\r\n * claiming one effective path or route name, which this refuses\r\n * to return at all — the alternative is an artefact in which one of them is\r\n * silently unreachable; a `route` or `prefix` that cannot be read without\r\n * running the application, which is refused before any page is reported at\r\n * all; a `metadata` export declaring a key nothing reads, which no compiler\r\n * catches unless the page opted into the type and which otherwise serves a page\r\n * with a silently missing tag; and a page whose layout chain holds more than one RENDERING layout, which\r\n * the production installer would refuse anyway — discovery refuses it first so\r\n * that artefact is never produced.\r\n */\r\nexport function discoverPages(options: DiscoverPagesOptions): DiscoveredPage[] {\r\n const { appRoot } = options;\r\n const srcRoot = path.join(appRoot, options.srcDir ?? \"src\");\r\n const webRoots = discoverWebRoots(srcRoot);\r\n const appFile = path.join(srcRoot, \"web\", \"root.tsx\");\r\n const hasAppFile = isFile(appFile);\r\n const declarations = new Map<string, RouteExportsReadResult>();\r\n const layoutShapes = new Map<string, LayoutShape>();\r\n const relativeToApp = (file: string) => toPosix(path.relative(appRoot, file));\r\n\r\n const pages: DiscoveredPage[] = [];\r\n const explicitRouteFiles = new Set<string>();\r\n let errorPage: DiscoveredErrorPage | undefined;\r\n\r\n for (const webRoot of webRoots) {\r\n for (const pageFile of walkFiles(webRoot, (fileName) => fileName.endsWith(\".page.tsx\"))) {\r\n const pageSource = fs.readFileSync(pageFile, \"utf-8\");\r\n assertPageHasDefaultExport(relativeToApp(pageFile), pageSource);\r\n const { route } = readDeclarations(pageFile, declarations, pageSource);\r\n if (isErrorPageFile(pageFile)) {\r\n if (route !== undefined) throw new ErrorPageDeclaresRouteError(relativeToApp(pageFile));\r\n\r\n if (errorPage !== undefined) {\r\n throw new DuplicateErrorPageError(\r\n relativeToApp(errorPage.pageFile),\r\n relativeToApp(pageFile),\r\n );\r\n }\r\n\r\n errorPage = { type: \"error\", pageFile, webRoot, ...(hasAppFile ? { appFile } : {}) };\r\n continue;\r\n }\r\n const isNotFoundPage = isNotFoundPageFile(pageFile);\r\n\r\n // THE NOT-FOUND PAGE IS THE ONE PAGE WITH NO URL, in both directions.\r\n //\r\n // Every ordinary page may derive its URL from the filesystem.\r\n // `404.page.tsx` is reached by NOT matching, so the opposite remains the\r\n // error: a `route` export here reads as a promise that some path is\r\n // browsable, which the installers never keep.\r\n //\r\n // Discovery still reports it, with the SAME reserved identity both\r\n // installers register it under, so the emitted artefacts agree with the\r\n // server about which entry is the not-found page — and so the client\r\n // registry carries the module the SSR'd document has to hydrate against.\r\n if (isNotFoundPage && route !== undefined) {\r\n throw new NotFoundPageDeclaresRouteError(relativeToApp(pageFile));\r\n }\r\n\r\n // The page contract is not only `route`. `metadata` is the other export\r\n // every page may declare, and it is the one with no compiler behind it\r\n // unless the author opted in to a type annotation — so it is checked\r\n // here, by name, exactly like the route above.\r\n //\r\n // AFTER the reserved-404 route check on purpose: an impossible 404 route\r\n // contract is more fundamental than a malformed `<head>` declaration.\r\n const unknownMetadataKeys = readMetadataKeys(relativeToApp(pageFile), pageSource);\r\n\r\n if (unknownMetadataKeys.length > 0) {\r\n throw new UnknownMetadataKeyError(relativeToApp(pageFile), unknownMetadataKeys);\r\n }\r\n\r\n // The policy decides which layout the page RENDERS INSIDE, from the FULL\r\n // enumerated chain: a layout anywhere on the ancestry path counts, not\r\n // just one in the page's own directory. Discovery supplies the one fact\r\n // the rule needs and the pure policy cannot learn — whether each layout\r\n // renders anything at all.\r\n const layouts = layoutChainFor(pageFile, webRoot);\r\n const shapes = layouts.map((layoutFile) => readLayoutShape(layoutFile, layoutShapes));\r\n const selection = selectPageLayout(\r\n layouts.map((layout, index) => ({ layout, renders: shapes[index].renders })),\r\n );\r\n\r\n if (selection.type === \"rejected\") {\r\n throw new NestedLayoutsNotSupportedError(\r\n relativeToApp(pageFile),\r\n selection.layouts.map(relativeToApp),\r\n );\r\n }\r\n\r\n // Every layout that declares a guard, outermost first. Both installers\r\n // now CONCATENATE the whole chain into the pipeline's single layout slot\r\n // (`../server/install-page-routes.ts`, `../server/install-page-routes-from-manifest.ts`),\r\n // so a guard anywhere on the path runs, in this order — which is why the\r\n // temporary refusal that used to stand here is gone rather than relaxed.\r\n const middlewareLayouts = layouts.filter((_, index) => shapes[index].hasMiddleware);\r\n\r\n // EVERY prefix on the path, outermost first: a `prefix`-only layout is\r\n // still a segment of the URL, and composing only the rendering layout's\r\n // would serve the subtree from a path nobody declared.\r\n const layoutPrefix = layouts.reduce(\r\n (composed, layoutFile) =>\r\n composeRoutePath(composed, readDeclarations(layoutFile, declarations).prefix ?? \"/\"),\r\n \"/\",\r\n );\r\n\r\n const relativePageFile = toPosix(path.relative(webRoot, pageFile));\r\n const layoutPrefixes = Object.fromEntries(\r\n layouts.flatMap((layoutFile) => {\r\n const prefix = readDeclarations(layoutFile, declarations).prefix;\r\n if (prefix === undefined) return [];\r\n\r\n const directory = toPosix(path.relative(webRoot, path.dirname(layoutFile)));\r\n return [[directory, prefix]];\r\n }),\r\n );\r\n // Routed through `canonicalizeRouteExport` — the ONE seam that validates\r\n // a declared `route.path` (`../routing/route-identity.ts`) — rather than\r\n // reading `route.path` directly, so `effectiveRoutePath` can never carry\r\n // a path the grammar has rejected. This does not depend on `routeName`\r\n // (below) also validating: even if that field were reordered, made\r\n // conditional, or removed, an unsupported declared path still cannot\r\n // reach `effectiveRoutePath` unvalidated.\r\n const canonicalRoute =\r\n !isNotFoundPage && route ? canonicalizeRouteExport(route, relativePageFile) : undefined;\r\n\r\n const effectiveRoutePath = isNotFoundPage\r\n ? NOT_FOUND_ROUTE_PATH\r\n : canonicalRoute\r\n ? composeRoutePath(layoutPrefix, canonicalRoute.path)\r\n : deriveFilesystemRoutePath({ pageFile: relativePageFile, layoutPrefixes });\r\n\r\n if (!isNotFoundPage && route !== undefined) {\r\n explicitRouteFiles.add(relativeToApp(pageFile));\r\n }\r\n\r\n pages.push({\r\n type: \"page\",\r\n routeName: isNotFoundPage\r\n ? NOT_FOUND_ROUTE_NAME\r\n : resolvePageRouteName(route, relativePageFile),\r\n // The catch-all, which the client route matcher already understands as\r\n // a terminal `catch-all` token sorted LAST by specificity\r\n // (`../client/runtime/matcher.ts`) — so the browser resolves the\r\n // not-found page for a URL that matched nothing, exactly as the server\r\n // did, and never in preference to a real page.\r\n routePath: effectiveRoutePath,\r\n pageFile,\r\n webRoot,\r\n // THE NOT-FOUND PAGE RENDERS INSIDE THE APPLICATION ROOT AND NOTHING\r\n // ELSE — an EMPTY chain, not the one enumerated above.\r\n //\r\n // Both installers render it with `layoutFile: undefined`, deliberately\r\n // and independently (`../server/install-page-routes.ts`,\r\n // `../server/install-page-routes-from-manifest.ts`), for the same\r\n // reason the 404 page takes no loader: a path whose entire job is to\r\n // handle failure must not depend on chrome that can itself throw,\r\n // redirect, or need data. Reporting layouts here anyway put them in the\r\n // client registry and therefore in HYDRATION, so the server rendered\r\n // `App(Page)` while the browser rebuilt `App(Layout(Page))` — a\r\n // guaranteed mismatch on the one route nobody is watching, invisible to\r\n // any application that happens to have no layouts.\r\n //\r\n // Aligned by REMOVING them from the client, never by giving them to the\r\n // server: layouts on the not-found route would make it the most fragile\r\n // route in the application.\r\n //\r\n // The chain above is still enumerated and still validated, so a nested\r\n // layout on this page's path is refused at build time exactly as it is\r\n // everywhere else — what changes is only what the page renders inside.\r\n layouts: isNotFoundPage ? [] : layouts,\r\n middlewareLayouts: isNotFoundPage ? [] : middlewareLayouts,\r\n ...(hasAppFile ? { appFile } : {}),\r\n });\r\n }\r\n }\r\n\r\n // The error page is captured separately above so a second one can be\r\n // compared against the first before either is trusted — it joins the\r\n // returned graph here, once discovery knows there is exactly one.\r\n if (errorPage !== undefined) pages.push(errorPage);\r\n\r\n pages.sort(comparePages);\r\n\r\n const routablePages = pages.filter(isDiscoveredRoutablePage);\r\n assertUniqueRoutePaths(routablePages, appRoot, explicitRouteFiles);\r\n assertUniqueRouteNames(routablePages, appRoot);\r\n\r\n return pages;\r\n}\r\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 * An explicit `route` export wins. Otherwise the URL is derived from the page's\n * path beneath `src/web`: directories contribute segments, `(groups)` do not,\n * `index.page.tsx` claims its directory, and `[id]` becomes `:id`. A layout\n * `prefix` replaces its own directory segment. The same recipe is emitted to\n * every provider so build and boot cannot drift.\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\";\n// `../server/not-found-page` is imported for its CONSTANTS and its filename\n// predicate only. Its sole runtime import is the default fallback's build-time\n// stylesheet data URL; it imports no renderer or application runtime, so this\n// edge cannot drag the render pipeline into the build. What a not-found page is,\n// and what identity it carries, is decided in one place for discovery and both\n// installers.\nimport {\n isNotFoundPageFile,\n NotFoundPageDeclaresRouteError,\n NOT_FOUND_ROUTE_NAME,\n NOT_FOUND_ROUTE_PATH,\n} from \"../server/not-found-page\";\nimport { NestedLayoutsNotSupportedError, selectPageLayout } from \"../routing/layout-policy\";\nimport { deriveFilesystemRoutePath } from \"../routing/filesystem-route\";\nimport { canonicalizeRouteExport, resolvePageRouteName } from \"../routing/route-identity\";\nimport { assertPageHasDefaultExport } from \"./page-default-export\";\nimport { UnknownMetadataKeyError, readMetadataKeys } from \"./read-metadata-keys\";\nimport type { RouteExportsReadResult } from \"./read-route-exports\";\nimport { NonLiteralRouteExportError, readRouteExports } from \"./read-route-exports\";\nimport { toPosix } from \"../shared/to-posix\";\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 DiscoveredRoutablePage = {\n /** A browsable page. Error pages are deliberately a different discovery kind. */\n type: \"page\";\n /**\n * The page's route name: the `name` its `route` export declares, or its\n * filesystem identity as dotted segments, with groups omitted.\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. Explicit routes retain their existing\n * layout-prefix composition. Derived routes use filesystem segments, with\n * each declared layout prefix replacing that layout directory's segment.\n * Always `/`-prefixed.\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/** The one application error boundary. It deliberately has no route identity. */\nexport type DiscoveredErrorPage = {\n type: \"error\";\n /** Absolute path to the sole `error.page.tsx` beneath `src/web`. */\n pageFile: string;\n webRoot: string;\n appFile?: string;\n};\n\n/** The complete static web graph: routable leaves plus the optional error boundary. */\nexport type DiscoveredPage = DiscoveredRoutablePage | DiscoveredErrorPage;\n\nexport function isDiscoveredRoutablePage(page: DiscoveredPage): page is DiscoveredRoutablePage {\n return page.type === \"page\";\n}\n\nexport function isErrorPageFile(file: string): boolean {\n return path.basename(file) === \"error.page.tsx\";\n}\n\nexport class DuplicateErrorPageError extends Error {\n public constructor(firstFile: string, secondFile: string) {\n super(\n `Two error pages were found: \"${firstFile}\" and \"${secondFile}\". An application may own exactly one \\`error.page.tsx\\` anywhere beneath src/web.`,\n );\n this.name = \"DuplicateErrorPageError\";\n }\n}\n\nexport class ErrorPageDeclaresRouteError extends Error {\n public constructor(pageFile: string) {\n super(\n `The error page \"${pageFile}\" exports \\`route\\`. error.page.tsx is an error boundary, not a browsable page; remove the route export.`,\n );\n this.name = \"ErrorPageDeclaresRouteError\";\n }\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\n/** Raised when two different page files resolve to one effective URL. */\nexport class DuplicatePageRoutePathError extends Error {\n public constructor(\n public readonly routePath: string,\n public readonly firstFile: string,\n public readonly secondFile: string,\n ) {\n super(\n `Two pages resolve to the same route path \"${routePath}\": \"${firstFile}\" and ` +\n `\"${secondFile}\". Each URL may identify exactly one page. Rename or move one file, ` +\n \"or give one page an explicit route with a different path.\",\n );\n this.name = \"DuplicatePageRoutePathError\";\n }\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 page root. It is optional; a project without it has zero pages, which is\n * a legal empty state.\n */\nexport function discoverWebRoots(srcRoot: string): string[] {\n const webRoot = path.join(srcRoot, \"web\");\n\n return isDirectory(webRoot) ? [webRoot] : [];\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\n/**\n * Every directory named exactly `web` beneath `src/app`, at any depth — the\n * shape a page or a named layout takes BEFORE it moves to the one supported\n * page root, {@link discoverWebRoots}. Descent stops at a `web` directory\n * itself: a `web` folder nested inside another is not a shape this codebase\n * uses, so there is nothing further down worth walking.\n */\nfunction discoverIgnoredAppWebDirs(srcRoot: string): string[] {\n const appDir = path.join(srcRoot, \"app\");\n\n if (!isDirectory(appDir)) return [];\n\n const found: string[] = [];\n\n const walk = (dir: string) => {\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n if (!entry.isDirectory()) continue;\n\n const full = path.join(dir, entry.name);\n\n if (entry.name === \"web\") {\n found.push(full);\n } else {\n walk(full);\n }\n }\n };\n\n walk(appDir);\n\n return found;\n}\n\n/** One file {@link discoverIgnoredAppWebFiles} found, and what kind it is. */\nexport type IgnoredAppWebFile = {\n /** Absolute path to the file. */\n file: string;\n /**\n * `\"page\"` for a `*.page.tsx`; `\"layout\"` for a NAMED `*.layout.tsx` or a\n * POSITIONAL `layout.tsx` — both are wired to nothing, but a layout's loss\n * is a dropped guard rather than a dropped page, and the two are worded\n * differently; see {@link warnIgnoredAppWebFiles}.\n */\n kind: \"page\" | \"layout\";\n};\n\n/**\n * Every `*.page.tsx` and every `layout.tsx` (named or positional) beneath\n * `src/app/**\\/web/**`, sorted lexicographically (POSIX) for a stable\n * diagnostic.\n *\n * `src/web` is the only page root (canon `4a6524f3`) — nothing found here is\n * registered, ever. This function exists only to NAME what is being ignored\n * instead of ignoring it in silence; see {@link warnIgnoredAppWebFiles}.\n *\n * A POSITIONAL `layout.tsx` is scanned here deliberately (closes board card\n * `eb82c3c7`): it may declare `middleware`, and a page that loses its layout\n * chain by moving to `src/app/**\\/web/**` loses that guard with it, silently —\n * the page still serves, just unprotected. This function does not read\n * whether a given layout actually declares `middleware`; it reports every\n * `layout.tsx` under the ignored tree, because a layout's whole job — prefix,\n * middleware, loader — is lost the moment it is wired to nothing.\n */\nexport function discoverIgnoredAppWebFiles(srcRoot: string): IgnoredAppWebFile[] {\n const found: IgnoredAppWebFile[] = [];\n\n for (const webDir of discoverIgnoredAppWebDirs(srcRoot)) {\n for (const file of walkFiles(webDir, (fileName) => fileName.endsWith(\".page.tsx\"))) {\n found.push({ file, kind: \"page\" });\n }\n\n for (const file of walkFiles(\n webDir,\n (fileName) => fileName.endsWith(\".layout.tsx\") || fileName === \"layout.tsx\",\n )) {\n found.push({ file, kind: \"layout\" });\n }\n }\n\n return found.sort((left, right) => compareStrings(toPosix(left.file), toPosix(right.file)));\n}\n\n/**\n * Files {@link discoverIgnoredAppWebFiles} has already reported once this\n * process — the \"once per boot\" guard. Keyed by absolute path rather than\n * cleared between calls: {@link discoverPages} runs once per dev reload as\n * well as once per production build, and the point of this set is that the\n * same stray file does not get a fresh warning on every one of those calls\n * within a single boot.\n */\nconst warnedIgnoredAppWebFiles = new Set<string>();\n\n/** Renders one ignored file's path, relative to `appRoot`, POSIX. */\nfunction namedFile(file: string, appRoot: string): string {\n return ` - ${toPosix(path.relative(appRoot, file))}`;\n}\n\n/**\n * The page half of the diagnostic: a missing page is visible the moment\n * someone loads its URL, so this says only that it is absent from the route\n * graph.\n */\nfunction pageIgnoredMessage(files: readonly string[], appRoot: string): string {\n const plural = files.length === 1 ? \"\" : \"s\";\n const named = files.map((file) => namedFile(file, appRoot)).join(\"\\n\");\n\n return (\n `${files.length} page file${plural} under src/app/**/web/** ` +\n `${files.length === 1 ? \"is\" : \"are\"} ignored — the only supported page root is ` +\n `src/web/**, so ${files.length === 1 ? \"it is\" : \"they are\"} absent from the route ` +\n `graph:\\n${named}\\nMove ${files.length === 1 ? \"it\" : \"them\"} under src/web/** to register ` +\n `${files.length === 1 ? \"it\" : \"them\"}.`\n );\n}\n\n/**\n * The layout half of the diagnostic: worded differently from\n * {@link pageIgnoredMessage} on purpose. A missing route guard is not visible\n * at all — the page it would have gated still serves, just unprotected — so\n * this names what is lost (`prefix`, `middleware`, `loader`) rather than\n * merely saying a file was ignored.\n */\nfunction layoutIgnoredMessage(files: readonly string[], appRoot: string): string {\n const plural = files.length === 1 ? \"\" : \"s\";\n const named = files.map((file) => namedFile(file, appRoot)).join(\"\\n\");\n\n return (\n `${files.length} layout file${plural} under src/app/**/web/** ` +\n `${files.length === 1 ? \"is\" : \"are\"} ignored — the only supported page root is ` +\n `src/web/**, so ${files.length === 1 ? \"its\" : \"their\"} \\`prefix\\`, \\`middleware\\` and ` +\n `\\`loader\\` apply to no page at all:\\n${named}\\nA page that relied on ` +\n `${files.length === 1 ? \"it\" : \"them\"} for a guard — auth, a redirect, anything in ` +\n `\\`middleware\\` — is served with that guard silently absent. Move ` +\n `${files.length === 1 ? \"it\" : \"them\"} under src/web/** to apply ` +\n `${files.length === 1 ? \"it\" : \"them\"} again.`\n );\n}\n\n/**\n * The one place this module writes to the console. Reports every file\n * {@link discoverIgnoredAppWebFiles} found and has not already warned about\n * this boot — never crashes the build, because an application mid-migration\n * off `src/app/**\\/web/**` is legitimate, not broken.\n *\n * Pages and layouts are worded separately (see {@link pageIgnoredMessage},\n * {@link layoutIgnoredMessage}) but printed in ONE `console.warn` call, so a\n * boot that has both still prints once.\n */\nfunction warnIgnoredAppWebFiles(files: readonly IgnoredAppWebFile[], appRoot: string): void {\n const unwarned = files.filter((entry) => !warnedIgnoredAppWebFiles.has(entry.file));\n\n if (unwarned.length === 0) return;\n\n for (const entry of unwarned) warnedIgnoredAppWebFiles.add(entry.file);\n\n const pages = unwarned.filter((entry) => entry.kind === \"page\").map((entry) => entry.file);\n const layouts = unwarned.filter((entry) => entry.kind === \"layout\").map((entry) => entry.file);\n\n const sections = [\n ...(pages.length > 0 ? [pageIgnoredMessage(pages, appRoot)] : []),\n ...(layouts.length > 0 ? [layoutIgnoredMessage(layouts, appRoot)] : []),\n ];\n\n console.warn(sections.map((section) => `[warlock:web] ${section}`).join(\"\\n\\n\"));\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 the page root, 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 *\n * ALSO the ignored-`src/app/**\\/web/**` diagnostic's boot-path call site: dev's\n * route installer (`../server/install-page-routes.ts`) calls this function,\n * not {@link discoverPages}, so the warning has to fire from here too or a\n * plain `warlock dev` boot never prints it — only a request for a page's\n * hydration bundle would. `appRoot` is derived as this srcRoot's parent, which\n * holds for every real caller: both this function and {@link discoverPages}\n * are only ever handed `<appRoot>/src`.\n */\nexport function discoverPageFiles(srcRoot: string): DiscoveredPageFile[] {\n const found: DiscoveredPageFile[] = [];\n\n const ignoredAppWebFiles = discoverIgnoredAppWebFiles(srcRoot);\n if (ignoredAppWebFiles.length > 0) {\n warnIgnoredAppWebFiles(ignoredAppWebFiles, path.dirname(srcRoot));\n }\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 DiscoveredRoutablePage[], 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/**\n * A page's declared `route.path` is a choice its author made deliberately, so\n * two pages that both declare the same path are the author's call, not an\n * accident — {@link assertUniqueRouteNames} is what keeps each of them\n * reachable by name. What this refuses is a COLLISION NOBODY CHOSE: at least\n * one side's path is filesystem-derived, which is exactly the case a rename\n * or a new file can produce without anyone noticing two pages now answer the\n * same URL. The reserved not-found path (`\"*\"`) is never a candidate here\n * either way — it identifies no browsable URL, and {@link assertUniqueRouteNames}\n * already refuses a second `404.page.tsx` by its own reserved name.\n */\nfunction assertUniqueRoutePaths(\n pages: readonly DiscoveredRoutablePage[],\n appRoot: string,\n explicitFiles: ReadonlySet<string>,\n): void {\n const fileByRoutePath = new Map<string, string>();\n\n for (const page of pages) {\n if (page.routePath === NOT_FOUND_ROUTE_PATH) continue;\n\n const existing = fileByRoutePath.get(page.routePath);\n const relative = toPosix(path.relative(appRoot, page.pageFile));\n\n if (existing !== undefined && !(explicitFiles.has(existing) && explicitFiles.has(relative))) {\n throw new DuplicatePageRoutePathError(page.routePath, existing, relative);\n }\n\n if (existing === undefined) fileByRoutePath.set(page.routePath, relative);\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 *\n * `source` is the file's text when the caller already holds it — the page loop\n * reads each page once and spends that read on both the route declarations and\n * the metadata check, rather than opening the same file twice.\n */\nfunction readDeclarations(\n sourceFile: string,\n cache: Map<string, RouteExportsReadResult>,\n source?: string,\n) {\n let result = cache.get(sourceFile);\n\n if (result === undefined) {\n result = readRouteExports(sourceFile, source);\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 the page root 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 a route or metadata export that cannot be read statically; two pages\n * claiming one effective path or 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; a `metadata` export declaring a key nothing reads, which no compiler\n * catches unless the page opted into the type and which otherwise serves a page\n * with a silently missing tag; 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 *\n * It also WARNS — never refuses — when it finds a `*.page.tsx` or named\n * `*.layout.tsx` under `src/app/**\\/web/**`, the shape the page root took\n * before it moved to `src/web`. Those files are not registered either way;\n * see {@link discoverIgnoredAppWebFiles}.\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 const explicitRouteFiles = new Set<string>();\n let errorPage: DiscoveredErrorPage | undefined;\n\n const ignoredAppWebFiles = discoverIgnoredAppWebFiles(srcRoot);\n if (ignoredAppWebFiles.length > 0) warnIgnoredAppWebFiles(ignoredAppWebFiles, appRoot);\n\n for (const webRoot of webRoots) {\n for (const pageFile of walkFiles(webRoot, (fileName) => fileName.endsWith(\".page.tsx\"))) {\n const pageSource = fs.readFileSync(pageFile, \"utf-8\");\n assertPageHasDefaultExport(relativeToApp(pageFile), pageSource);\n const { route } = readDeclarations(pageFile, declarations, pageSource);\n if (isErrorPageFile(pageFile)) {\n if (route !== undefined) throw new ErrorPageDeclaresRouteError(relativeToApp(pageFile));\n\n if (errorPage !== undefined) {\n throw new DuplicateErrorPageError(\n relativeToApp(errorPage.pageFile),\n relativeToApp(pageFile),\n );\n }\n\n errorPage = { type: \"error\", pageFile, webRoot, ...(hasAppFile ? { appFile } : {}) };\n continue;\n }\n const isNotFoundPage = isNotFoundPageFile(pageFile);\n\n // THE NOT-FOUND PAGE IS THE ONE PAGE WITH NO URL, in both directions.\n //\n // Every ordinary page may derive its URL from the filesystem.\n // `404.page.tsx` is reached by NOT matching, so the opposite remains the\n // error: a `route` export here reads as a promise that some path is\n // browsable, which the installers never keep.\n //\n // Discovery still reports it, with the SAME reserved identity both\n // installers register it under, so the emitted artefacts agree with the\n // server about which entry is the not-found page — and so the client\n // registry carries the module the SSR'd document has to hydrate against.\n if (isNotFoundPage && route !== undefined) {\n throw new NotFoundPageDeclaresRouteError(relativeToApp(pageFile));\n }\n\n // The page contract is not only `route`. `metadata` is the other export\n // every page may declare, and it is the one with no compiler behind it\n // unless the author opted in to a type annotation — so it is checked\n // here, by name, exactly like the route above.\n //\n // AFTER the reserved-404 route check on purpose: an impossible 404 route\n // contract is more fundamental than a malformed `<head>` declaration.\n const unknownMetadataKeys = readMetadataKeys(relativeToApp(pageFile), pageSource);\n\n if (unknownMetadataKeys.length > 0) {\n throw new UnknownMetadataKeyError(relativeToApp(pageFile), unknownMetadataKeys);\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 const relativePageFile = toPosix(path.relative(webRoot, pageFile));\n const layoutPrefixes = Object.fromEntries(\n layouts.flatMap((layoutFile) => {\n const prefix = readDeclarations(layoutFile, declarations).prefix;\n if (prefix === undefined) return [];\n\n const directory = toPosix(path.relative(webRoot, path.dirname(layoutFile)));\n return [[directory, prefix]];\n }),\n );\n // Routed through `canonicalizeRouteExport` — the ONE seam that validates\n // a declared `route.path` (`../routing/route-identity.ts`) — rather than\n // reading `route.path` directly, so `effectiveRoutePath` can never carry\n // a path the grammar has rejected. This does not depend on `routeName`\n // (below) also validating: even if that field were reordered, made\n // conditional, or removed, an unsupported declared path still cannot\n // reach `effectiveRoutePath` unvalidated.\n const canonicalRoute =\n !isNotFoundPage && route ? canonicalizeRouteExport(route, relativePageFile) : undefined;\n\n const effectiveRoutePath = isNotFoundPage\n ? NOT_FOUND_ROUTE_PATH\n : canonicalRoute\n ? composeRoutePath(layoutPrefix, canonicalRoute.path)\n : deriveFilesystemRoutePath({ pageFile: relativePageFile, layoutPrefixes });\n\n if (!isNotFoundPage && route !== undefined) {\n explicitRouteFiles.add(relativeToApp(pageFile));\n }\n\n pages.push({\n type: \"page\",\n routeName: isNotFoundPage\n ? NOT_FOUND_ROUTE_NAME\n : resolvePageRouteName(route, relativePageFile),\n // The catch-all, which the client route matcher already understands as\n // a terminal `catch-all` token sorted LAST by specificity\n // (`../client/runtime/matcher.ts`) — so the browser resolves the\n // not-found page for a URL that matched nothing, exactly as the server\n // did, and never in preference to a real page.\n routePath: effectiveRoutePath,\n pageFile,\n webRoot,\n // THE NOT-FOUND PAGE RENDERS INSIDE THE APPLICATION ROOT AND NOTHING\n // ELSE — an EMPTY chain, not the one enumerated above.\n //\n // Both installers render it with `layoutFile: undefined`, deliberately\n // and independently (`../server/install-page-routes.ts`,\n // `../server/install-page-routes-from-manifest.ts`), for the same\n // reason the 404 page takes no loader: a path whose entire job is to\n // handle failure must not depend on chrome that can itself throw,\n // redirect, or need data. Reporting layouts here anyway put them in the\n // client registry and therefore in HYDRATION, so the server rendered\n // `App(Page)` while the browser rebuilt `App(Layout(Page))` — a\n // guaranteed mismatch on the one route nobody is watching, invisible to\n // any application that happens to have no layouts.\n //\n // Aligned by REMOVING them from the client, never by giving them to the\n // server: layouts on the not-found route would make it the most fragile\n // route in the application.\n //\n // The chain above is still enumerated and still validated, so a nested\n // layout on this page's path is refused at build time exactly as it is\n // everywhere else — what changes is only what the page renders inside.\n layouts: isNotFoundPage ? [] : layouts,\n middlewareLayouts: isNotFoundPage ? [] : middlewareLayouts,\n ...(hasAppFile ? { appFile } : {}),\n });\n }\n }\n\n // The error page is captured separately above so a second one can be\n // compared against the first before either is trusted — it joins the\n // returned graph here, once discovery knows there is exactly one.\n if (errorPage !== undefined) pages.push(errorPage);\n\n pages.sort(comparePages);\n\n const routablePages = pages.filter(isDiscoveredRoutablePage);\n assertUniqueRoutePaths(routablePages, appRoot, explicitRouteFiles);\n assertUniqueRouteNames(routablePages, appRoot);\n\n return pages;\n}\n"],"mappings":"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"}
@@ -1,7 +1,8 @@
1
1
  import { NestedLayoutsNotSupportedError } from "../routing/layout-policy.mjs";
2
2
  import { NOT_FOUND_ROUTE_NAME, NOT_FOUND_ROUTE_PATH, isNotFoundPageFile } from "../server/not-found-page.mjs";
3
- import { DuplicatePageRouteNameError, discoverPages, discoverWebRoots, isDiscoveredRoutablePage, isFile, layoutChainFor, toPosix, walkFiles } from "./discover-pages.mjs";
4
- import { normalizeRoutePath } from "@warlock.js/core";
3
+ import { toPosix } from "../shared/to-posix.mjs";
4
+ import { discoverPages, discoverWebRoots, isDiscoveredRoutablePage, isFile, layoutChainFor, walkFiles } from "./discover-pages.mjs";
5
+ import { normalizeRoutePath } from "../core/src/router/normalize-route-path.mjs";
5
6
  import fs from "node:fs";
6
7
  import path from "node:path";
7
8
  import { parse } from "@babel/parser";
@@ -295,6 +296,7 @@ async function generatePagesBarrel(options) {
295
296
  if (routablePages.length > 0 && !isFile(appFile)) throw new Error(`Cannot generate the page barrel: ${discovered.length} page(s) were discovered but the application root component "${toPosix(path.relative(appRoot, appFile))}" does not exist. Every page renders inside it.`);
296
297
  const layoutIdentifiers = /* @__PURE__ */ new Map();
297
298
  const pages = [];
299
+ const notFoundPage = routablePages.find((page) => isNotFoundPageFile(page.pageFile));
298
300
  const pageRoutes = {
299
301
  version: 1,
300
302
  routes: [...routablePages.filter((page) => !isNotFoundPageFile(page.pageFile)).map((page) => ({
@@ -306,7 +308,7 @@ async function generatePagesBarrel(options) {
306
308
  method: "GET",
307
309
  path: normalizeRoutePath("*"),
308
310
  name: NOT_FOUND_ROUTE_NAME,
309
- source: routablePages.find((page) => isNotFoundPageFile(page.pageFile)) === void 0 ? "\0warlock:framework-default-404" : toPosix(path.relative(appRoot, routablePages.find((page) => isNotFoundPageFile(page.pageFile)).pageFile))
311
+ source: notFoundPage === void 0 ? "\0warlock:framework-default-404" : toPosix(path.relative(appRoot, notFoundPage.pageFile))
310
312
  }]
311
313
  };
312
314
  for (const [index, page] of routablePages.entries()) {
@@ -1 +1 @@
1
- {"version":3,"file":"generate-pages-barrel.mjs","names":[],"sources":["../../../../../../../web/src/build/generate-pages-barrel.ts"],"sourcesContent":["/**\r\n * The server half of the build→runtime handoff: the generated\r\n * `<productionDir>/pages.ts` barrel that carries the page graph into the\r\n * server bundle.\r\n *\r\n * The graph itself comes from `discover-pages.ts`, the one scan every provider\r\n * shares — this module is the BARREL provider and nothing more. It takes the\r\n * recipe, runs the Vite-switch tripwire over the sources the recipe came from,\r\n * and writes the file.\r\n *\r\n * \"Static\" means the FILESYSTEM ONLY — discovery globs `*.page.tsx`,\r\n * `layout.tsx` and `root.tsx` and never imports a single application module,\r\n * mirroring how the builder already globs (`production-builder.ts:184-242`).\r\n * The imports happen at BOOT, when the compiled barrel runs, which is what\r\n * makes a page that throws on import a loud pre-listen failure in production.\r\n *\r\n * This is the HEAVY half of the contribution: `contribution.ts` reaches it by\r\n * dynamic import inside its `generate` hook, so nothing here lands in a\r\n * connector's static graph.\r\n */\r\nimport fs from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport { parse } from \"@babel/parser\";\r\nimport { normalizeRoutePath, type ConnectorEsbuildPatch } from \"@warlock.js/core\";\r\nimport {\r\n discoverPages,\r\n discoverWebRoots,\r\n isDiscoveredRoutablePage,\r\n isFile,\r\n toPosix,\r\n walkFiles,\r\n} from \"./discover-pages\";\r\nimport {\r\n isNotFoundPageFile,\r\n NOT_FOUND_ROUTE_NAME,\r\n NOT_FOUND_ROUTE_PATH,\r\n} from \"../server/not-found-page\";\r\n\r\n// Re-exported, not redefined: discovery's helpers were this module's before the\r\n// split, and the callers that already reach for them here should keep getting\r\n// the one implementation rather than a copy that can drift from it. The nested\r\n// layout error re-exports from the layout policy module, the one place that\r\n// owns layout selection and its error contract.\r\nexport {\r\n discoverPages,\r\n discoverWebRoots,\r\n DuplicatePageRouteNameError,\r\n layoutChainFor,\r\n} from \"./discover-pages\";\r\nexport { NestedLayoutsNotSupportedError } from \"../routing/layout-policy\";\r\nexport type { DiscoveredPage, DiscoverPagesOptions } from \"./discover-pages\";\r\n\r\n/**\r\n * Declared BEFORE the patch that reads it. `const` is hoisted but sits in the\r\n * temporal dead zone, and the patch below is an object literal evaluated at\r\n * module load — reading this from further down the file would throw.\r\n */\r\nconst STYLE_EXTENSIONS = [\".css\", \".scss\", \".sass\", \".less\", \".styl\"];\r\n\r\n/**\r\n * The esbuild patch web contributes (spike-settled, contract §3a).\r\n *\r\n * NO `process.env.NODE_ENV` define: warlock assigns to `process.env.NODE_ENV`\r\n * at runtime (`core/src/utils/environment.ts:14`) and a define would turn that\r\n * assignment into an assignment to a literal.\r\n */\r\nexport const WEB_ESBUILD_PATCH: ConnectorEsbuildPatch = {\r\n jsx: \"automatic\",\r\n jsxImportSource: \"react\",\r\n define: {\r\n \"import.meta.env.DEV\": \"false\",\r\n \"import.meta.env.PROD\": \"true\",\r\n \"import.meta.env.SSR\": \"true\",\r\n \"import.meta.env.MODE\": '\"production\"',\r\n },\r\n /**\r\n * Stylesheets compile to NOTHING in the server bundle, rather than failing it.\r\n *\r\n * `root.tsx` importing `./app.css` is how Tailwind — and every other CSS\r\n * pipeline — is normally wired, so rejecting it meant the framework could not\r\n * build a styled application at all. But the server has no use for the bytes:\r\n * it renders HTML, and all it ever needs of a stylesheet is a URL to put in a\r\n * `<link>`. The CSS itself belongs to the CLIENT bundle, which Vite builds\r\n * from the same `root.tsx` and which handles CSS natively.\r\n *\r\n * So the import stays in the user's source, the server drops it, and Vite\r\n * emits the real asset. Nothing is silently lost: a stylesheet that does not\r\n * exist still fails the client build, loudly, where it matters.\r\n */\r\n loader: Object.fromEntries(\r\n STYLE_EXTENSIONS.map((extension) => [extension, \"empty\" as const]),\r\n ) as ConnectorEsbuildPatch[\"loader\"],\r\n};\r\n\r\n/** Line appended to section 4 of the generated production entry, after `./routes`. */\r\nexport const WEB_ENTRY_IMPORT = 'await import(\"./pages\");';\r\n\r\nconst REMEDY =\n \"move the static file into the application's public/ directory and reference \" +\n \"it by its root URL instead (for example, public/logo.svg becomes /logo.svg). \" +\n \"The production server build does not compile static-asset imports, ?raw/?url \" +\n \"queries, or import.meta.url in the page graph in 5.2\";\n\r\nconst ASSET_EXTENSIONS = [\r\n \".svg\",\r\n \".png\",\r\n \".jpg\",\r\n \".jpeg\",\r\n \".gif\",\r\n \".webp\",\r\n \".avif\",\r\n \".ico\",\r\n \".woff\",\r\n \".woff2\",\r\n \".ttf\",\r\n \".eot\",\r\n \".mp4\",\r\n \".webm\",\r\n];\r\n\r\nexport type GeneratePagesBarrelOptions = {\r\n /** Absolute path to the application root (where `package.json` lives). */\r\n appRoot: string;\r\n /** Absolute path to `.warlock/production` — where the barrel is written. */\r\n productionDir: string;\r\n /** Source directory name under `appRoot`; defaults to `\"src\"`. */\r\n srcDir?: string;\r\n /**\r\n * App-root-relative POSIX path of the client bundle directory\r\n * (`\"dist/client\"`), baked into the manifest for the runtime to read back.\r\n * See `PageManifest.clientDir` for why it cannot be derived at boot.\r\n *\r\n * The CALLER resolves it, from the same `context.options.outdir` the client\r\n * build itself writes to, so the two cannot name different directories.\r\n */\n clientDir: string;\n /** App-root-public file paths copied into the production client directory. */\n publicFiles?: readonly string[];\n};\r\n\r\nexport type GeneratePagesBarrelResult = {\r\n /** How many `*.page.tsx` files discovery found under the page root. */\r\n pageCount: number;\r\n /** Absolute path of the written barrel. Always written when this runs at all. */\r\n barrelFile: string;\r\n /** The written barrel source. */\r\n contents: string;\r\n /** The build-time route surface persisted beside the production output. */\r\n pageRoutes: PageRoutesManifest;\r\n};\r\n\r\nexport type PageRouteManifestEntry = {\r\n method: \"GET\";\r\n path: string;\r\n name: string;\r\n source: string;\r\n};\r\n\r\nexport type PageRoutesManifest = {\r\n version: 1;\r\n routes: PageRouteManifestEntry[];\r\n};\r\n\r\n/** Raised by the Vite-switch tripwire. */\r\nexport class WebPageGraphUnsupportedImportError extends Error {\r\n public constructor(\r\n public readonly sourceFile: string,\r\n public readonly specifier: string,\r\n reason: string,\r\n ) {\r\n super(\r\n `Cannot build the page graph: \"${sourceFile}\" ${reason} (${specifier}). ` +\r\n \"Pages are compiled into the server bundle, which supports plain code imports only — \" +\r\n \"static assets, `?raw`/`?url` queries and `import.meta.url` are not supported there. \" +\r\n \"(Stylesheets ARE supported: the server bundle drops them and the client bundle emits them.) \" +\r\n `To fix: ${REMEDY}.`,\r\n );\r\n this.name = \"WebPageGraphUnsupportedImportError\";\r\n }\r\n}\r\n\r\nconst FROM_SPECIFIER = /\\bfrom\\s*[\"']([^\"']+)[\"']/g;\r\nconst BARE_IMPORT = /\\bimport\\s*[\"']([^\"']+)[\"']/g;\r\nconst DYNAMIC_IMPORT = /\\bimport\\s*\\(\\s*[\"']([^\"']+)[\"']\\s*\\)/g;\r\n\r\n/**\r\n * Blank every comment, preserving byte offsets.\r\n *\r\n * The scan below is regex over source text, so without this a specifier written\r\n * inside a comment is indistinguishable from a real import. That is not\r\n * hypothetical: documenting this very restriction — writing `import \"./app.css\"`\r\n * in a doc comment to explain why it is unsupported — failed the build, and the\r\n * error then insisted the file imported a stylesheet that it did not import.\r\n * Removing the real import did not help, because the comment was the match.\r\n *\r\n * Comments are replaced with spaces rather than deleted so every subsequent\r\n * offset, and therefore every line number in anything reported from here,\r\n * stays exactly where it was.\r\n *\r\n * A parse failure is deliberately NOT fatal: this function's job is to find\r\n * hazards, not to validate syntax. esbuild reports a genuine syntax error far\r\n * better than we would, and failing here would replace its clear message with a\r\n * worse one. On a parse failure the raw source is scanned — the pre-existing\r\n * behaviour, false positives and all.\r\n */\r\nfunction withoutComments(source: string): string {\r\n try {\r\n const ast = parse(source, {\r\n sourceType: \"module\",\r\n errorRecovery: true,\r\n plugins: [\"typescript\", \"jsx\", \"decorators-legacy\"],\r\n });\r\n\r\n let stripped = source;\r\n\r\n for (const comment of ast.comments ?? []) {\r\n const { start, end } = comment as { start: number; end: number };\r\n\r\n stripped =\r\n stripped.slice(0, start) + \" \".repeat(end - start) + stripped.slice(end);\r\n }\r\n\r\n return stripped;\r\n } catch {\r\n return source;\r\n }\r\n}\r\n\r\n/** Expects source whose comments have already been blanked by {@link withoutComments}. */\r\nfunction collectSpecifiers(source: string): string[] {\r\n const specifiers: string[] = [];\r\n\r\n for (const pattern of [FROM_SPECIFIER, BARE_IMPORT, DYNAMIC_IMPORT]) {\r\n pattern.lastIndex = 0;\r\n\r\n let match = pattern.exec(source);\r\n\r\n while (match !== null) {\r\n specifiers.push(match[1]);\r\n match = pattern.exec(source);\r\n }\r\n }\r\n\r\n return specifiers;\r\n}\r\n\r\nfunction hazardFor(specifier: string): string | undefined {\r\n const [base, query] = specifier.split(\"?\", 2);\r\n const lowered = base.toLowerCase();\r\n\r\n /*\r\n STYLESHEETS ARE NOT A HAZARD — see `loader` in WEB_ESBUILD_PATCH above.\r\n\r\n They used to be rejected here, which meant `import \"./app.css\"` in\r\n `root.tsx` — the ordinary way to wire Tailwind or any other CSS pipeline —\r\n failed the build outright. The server bundle now compiles them to nothing,\r\n because the server only ever needs a stylesheet's URL, and the client bundle\r\n that Vite builds from the same sources emits the real asset.\r\n\r\n STYLE_EXTENSIONS is still the single list both halves read from, so the set\r\n esbuild stubs and the set this check tolerates cannot drift apart.\r\n */\r\n\r\n if (ASSET_EXTENSIONS.some((extension) => lowered.endsWith(extension))) {\r\n return \"imports a static asset\";\r\n }\r\n\r\n if (query !== undefined && /(^|&)(raw|url)(&|=|$)/.test(query)) {\r\n return \"uses a bundler-specific import query\";\r\n }\r\n\r\n return undefined;\r\n}\r\n\r\n/**\r\n * The Vite-switch tripwire. Fails the build\r\n * the moment the page graph needs a capability the esbuild server build does\r\n * not have, naming the file, the specifier and the remedy.\r\n *\r\n * LIMITATIONS, stated on purpose — BOTH directions, because stating only one\r\n * is what made the other one expensive to find:\r\n *\r\n * FALSE NEGATIVES. This is a FILE-LEVEL scan of the `.ts`/`.tsx` sources under\r\n * the discovered web roots — it is not transitive resolution. A page that\r\n * imports a workspace package which itself imports CSS is not caught here; that\r\n * failure surfaces at esbuild time instead.\r\n *\r\n * FALSE POSITIVES. The scan is regex over source text, so anything that LOOKS\r\n * like a specifier counts. Comments are blanked first ({@link withoutComments})\r\n * because a documented example used to fail the build while insisting on an\r\n * import that was not there. String and template literals are NOT blanked —\r\n * they cannot be, since real specifiers are string literals — so a specifier\r\n * inside a template literal (a code sample in a docs page, say) still matches.\r\n * Rare, and it fails loudly rather than silently, but it is not impossible.\r\n */\r\nexport function assertNoViteOnlyImports(webRoots: readonly string[], appRoot: string): void {\r\n for (const webRoot of webRoots) {\r\n const sources = walkFiles(\r\n webRoot,\r\n (fileName) => fileName.endsWith(\".ts\") || fileName.endsWith(\".tsx\"),\r\n );\r\n\r\n for (const sourceFile of sources) {\r\n // Comments stripped ONCE, and reused by both checks below: `import.meta.url`\r\n // was as blind to comments as the specifier scan was, and mentioning it in\r\n // prose — as this very file does — would have failed the build.\r\n const source = withoutComments(fs.readFileSync(sourceFile, \"utf-8\"));\r\n const relative = toPosix(path.relative(appRoot, sourceFile));\r\n\r\n for (const specifier of collectSpecifiers(source)) {\r\n const reason = hazardFor(specifier);\r\n\r\n if (reason !== undefined) {\r\n throw new WebPageGraphUnsupportedImportError(relative, specifier, reason);\r\n }\r\n }\r\n\r\n if (source.includes(\"import.meta.url\")) {\r\n throw new WebPageGraphUnsupportedImportError(\r\n relative,\r\n \"import.meta.url\",\r\n \"uses `import.meta.url`, whose meaning changes under the bundled server build\",\r\n );\r\n }\r\n }\r\n }\r\n}\r\n\r\n/** `<productionDir>` -> `<file>` as an extensionless, POSIX, relative specifier. */\r\nfunction importSpecifierFor(productionDir: string, file: string): string {\r\n const relative = toPosix(path.relative(productionDir, file)).replace(/\\.tsx?$/, \"\");\r\n\r\n return relative.startsWith(\".\") ? relative : `./${relative}`;\r\n}\r\n\r\nfunction quote(value: string): string {\r\n return JSON.stringify(value);\r\n}\r\n\r\ntype BarrelPage = {\r\n identifier: string;\r\n sourceFile: string;\r\n layouts: { identifier: string; sourceFile: string }[];\r\n};\r\n\r\n/** Header of every generated barrel — the banner plus the one runtime import. */\r\nconst BARREL_HEADER = [\r\n \"// AUTO-GENERATED by @warlock.js/web — do not edit.\",\r\n \"// The page graph, compiled into the server bundle.\",\r\n \"// Route paths are deliberately absent: they are read off each module at boot.\",\r\n // The CONNECTOR subpath, never the root barrel: this file is compiled into\r\n // the server bundle, and `\"@warlock.js/web\"` would drag React in with it.\r\n 'import { providePageManifest } from \"@warlock.js/web/connector\";',\r\n];\r\n\r\n/**\r\n * The barrel written when discovery found no pages at all.\r\n *\r\n * No app entry: `root.tsx` is the root every PAGE renders inside, so with zero\r\n * pages there is nothing for it to wrap and requiring it would fail builds that\r\n * are legitimately page-free.\r\n */\r\nconst EMPTY_BARREL_CONTENTS = [\n ...BARREL_HEADER,\r\n \"\",\r\n \"// Zero pages were discovered. The empty table is still provided, because that\",\r\n \"// is what tells the boot-time reader \\\"this bundle WAS built with web, it just\",\r\n '// has no pages\" — as opposed to a bundle built without web at all, where no',\r\n \"// barrel runs and the manifest stays absent.\",\r\n \"providePageManifest({ pages: [] });\",\r\n \"\",\r\n].join(\"\\n\");\n\nfunction emptyPageBarrelContents(clientDir: string, publicFiles: readonly string[]): string {\n if (publicFiles.length === 0) return EMPTY_BARREL_CONTENTS;\n\n return [\n ...BARREL_HEADER,\n \"\",\n \"// Zero pages were discovered, but app-owned public files still belong to\",\n \"// the production browser surface and are served from the copied client tree.\",\n \"providePageManifest({\",\n ` clientDir: ${quote(clientDir)},`,\n ` publicFiles: ${JSON.stringify([...publicFiles])},`,\n \" pages: [],\",\n \"});\",\n \"\",\n ].join(\"\\n\");\n}\n\r\nasync function writeBarrel(productionDir: string, contents: string): Promise<string> {\r\n const barrelFile = path.join(productionDir, \"pages.ts\");\r\n\r\n await fs.promises.mkdir(productionDir, { recursive: true });\r\n await fs.promises.writeFile(barrelFile, contents, \"utf-8\");\r\n\r\n return barrelFile;\r\n}\r\n\r\n/**\r\n * Discovers the page graph, runs the tripwire, and writes\r\n * `<productionDir>/pages.ts`.\r\n *\r\n * Zero pages is NOT an error, and it is NOT silence either: the barrel is\r\n * written anyway carrying an empty table, so \"configured with web, no pages\"\r\n * reaches the runtime as a fact instead of looking identical to \"never built\r\n * with web\". The run also says so out loud (no silent caps). What the caller\r\n * skips on zero pages is the CLIENT bundle — no pages means nothing to\r\n * hydrate.\r\n */\r\nexport async function generatePagesBarrel(\r\n options: GeneratePagesBarrelOptions,\r\n): Promise<GeneratePagesBarrelResult> {\r\n const { appRoot, productionDir, clientDir, publicFiles = [] } = options;\n const srcRoot = path.join(appRoot, options.srcDir ?? \"src\");\r\n const webRoots = discoverWebRoots(srcRoot);\r\n const discovered = discoverPages({ appRoot, srcDir: options.srcDir });\r\n const routablePages = discovered.filter(isDiscoveredRoutablePage);\r\n const errorPage = discovered.find((page) => page.type === \"error\");\r\n\r\n if (discovered.length === 0) {\n console.log(\"web configured, 0 pages\");\n const contents = emptyPageBarrelContents(clientDir, publicFiles);\n\n return {\n pageCount: 0,\n barrelFile: await writeBarrel(productionDir, contents),\n contents,\n pageRoutes: { version: 1, routes: [] },\r\n };\r\n }\r\n\r\n assertNoViteOnlyImports(webRoots, appRoot);\r\n\r\n const appFile = path.join(srcRoot, \"web\", \"root.tsx\");\r\n\r\n if (routablePages.length > 0 && !isFile(appFile)) {\r\n throw new Error(\r\n `Cannot generate the page barrel: ${discovered.length} page(s) were discovered but the application root ` +\r\n `component \"${toPosix(path.relative(appRoot, appFile))}\" does not exist. Every page renders inside it.`,\r\n );\r\n }\r\n\r\n const layoutIdentifiers = new Map<string, string>();\r\n const pages: BarrelPage[] = [];\r\n // Persisted paths run through the ROUTER's own `normalizeRoutePath`, the\r\n // single definition of a route path's canonical form. Discovery derives these\r\n // from the filesystem and never registers anything, so without this the\r\n // manifest records a spelling the router never serves — the catch-all is\r\n // literally `\"*\"` here and `\"/*\"` once registered — and `warlock routes:diff`\r\n // reports drift on an untouched checkout right after a successful build.\r\n const pageRoutes: PageRoutesManifest = {\r\n version: 1,\r\n routes: [\r\n ...routablePages.filter((page) => !isNotFoundPageFile(page.pageFile)).map((page) => ({\r\n method: \"GET\" as const,\r\n path: normalizeRoutePath(page.routePath),\r\n name: page.routeName,\r\n source: toPosix(path.relative(appRoot, page.pageFile)),\r\n })),\r\n {\r\n method: \"GET\" as const,\r\n path: normalizeRoutePath(NOT_FOUND_ROUTE_PATH),\r\n name: NOT_FOUND_ROUTE_NAME,\r\n source: routablePages.find((page) => isNotFoundPageFile(page.pageFile)) === undefined\r\n ? \"\\u0000warlock:framework-default-404\"\r\n : toPosix(path.relative(appRoot, routablePages.find((page) => isNotFoundPageFile(page.pageFile))!.pageFile)),\r\n },\r\n ],\r\n };\r\n\r\n for (const [index, page] of routablePages.entries()) {\r\n const layouts = page.layouts.map((layoutFile) => {\r\n let identifier = layoutIdentifiers.get(layoutFile);\r\n\r\n if (identifier === undefined) {\r\n identifier = `l${layoutIdentifiers.size}`;\r\n layoutIdentifiers.set(layoutFile, identifier);\r\n }\r\n\r\n return { identifier, sourceFile: layoutFile };\r\n });\r\n\r\n pages.push({ identifier: `p${index}`, sourceFile: page.pageFile, layouts });\r\n }\r\n\r\n const importLines = [\r\n ...(routablePages.length === 0\r\n ? []\r\n : [`import * as app from ${quote(importSpecifierFor(productionDir, appFile))};`]),\r\n ...(errorPage === undefined\r\n ? []\r\n : [`import * as errorPage from ${quote(importSpecifierFor(productionDir, errorPage.pageFile))};`]),\r\n ...[...layoutIdentifiers.entries()].map(\r\n ([layoutFile, identifier]) =>\r\n `import * as ${identifier} from ${quote(importSpecifierFor(productionDir, layoutFile))};`,\r\n ),\r\n ...pages.map(\r\n (page) =>\r\n `import * as ${page.identifier} from ${quote(importSpecifierFor(productionDir, page.sourceFile))};`,\r\n ),\r\n ];\r\n\r\n const relativeToApp = (file: string) => quote(toPosix(path.relative(appRoot, file)));\r\n\r\n const pageEntries = pages.map((page) => {\r\n const layouts = page.layouts\r\n .map((layout) => `{ module: ${layout.identifier}, sourceFile: ${relativeToApp(layout.sourceFile)} }`)\r\n .join(\", \");\r\n\r\n return [\r\n \" {\",\r\n ` module: ${page.identifier},`,\r\n ` sourceFile: ${relativeToApp(page.sourceFile)},`,\r\n ` layouts: [${layouts}],`,\r\n \" },\",\r\n ].join(\"\\n\");\r\n });\r\n\r\n const contents = [\r\n ...BARREL_HEADER,\r\n ...importLines,\n \"\",\n \"providePageManifest({\",\n ...(routablePages.length === 0 && publicFiles.length === 0\n ? []\n : [` clientDir: ${quote(clientDir)},`]),\n ...(publicFiles.length === 0\n ? []\n : [` publicFiles: ${JSON.stringify([...publicFiles])},`]),\n ...(routablePages.length === 0 ? [] : [` app: { module: app, sourceFile: ${relativeToApp(appFile)} },`]),\r\n ...(errorPage === undefined\r\n ? []\r\n : [` errorPage: { module: errorPage, sourceFile: ${relativeToApp(errorPage.pageFile)} },`]),\r\n \" pages: [\",\r\n ...pageEntries.map((entry) =>\r\n entry\r\n .split(\"\\n\")\r\n .map((line) => ` ${line}`)\r\n .join(\"\\n\"),\r\n ),\r\n \" ],\",\r\n \"});\",\r\n \"\",\r\n ].join(\"\\n\");\r\n\r\n return {\r\n pageCount: pages.length,\r\n barrelFile: await writeBarrel(productionDir, contents),\r\n contents,\r\n pageRoutes,\r\n };\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"generate-pages-barrel.mjs","names":[],"sources":["../../../../../../../web/src/build/generate-pages-barrel.ts"],"sourcesContent":["/**\r\n * The server half of the build→runtime handoff: the generated\r\n * `<productionDir>/pages.ts` barrel that carries the page graph into the\r\n * server bundle.\r\n *\r\n * The graph itself comes from `discover-pages.ts`, the one scan every provider\r\n * shares — this module is the BARREL provider and nothing more. It takes the\r\n * recipe, runs the Vite-switch tripwire over the sources the recipe came from,\r\n * and writes the file.\r\n *\r\n * \"Static\" means the FILESYSTEM ONLY — discovery globs `*.page.tsx`,\r\n * `layout.tsx` and `root.tsx` and never imports a single application module,\r\n * mirroring how the builder already globs (`production-builder.ts:184-242`).\r\n * The imports happen at BOOT, when the compiled barrel runs, which is what\r\n * makes a page that throws on import a loud pre-listen failure in production.\r\n *\r\n * This is the HEAVY half of the contribution: `contribution.ts` reaches it by\r\n * dynamic import inside its `generate` hook, so nothing here lands in a\r\n * connector's static graph.\r\n */\r\nimport fs from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport { parse } from \"@babel/parser\";\r\nimport type { ConnectorEsbuildPatch } from \"@warlock.js/core\";\r\n// Deep import, not the barrel: `@warlock.js/core`'s root re-exports database,\r\n// http, socket, mail and connectors alongside the router. Measured with\r\n// esbuild (bundle a single entry point, read the metafile) — importing the\r\n// barrel for this one function pulled in 215 core/src files (955 KB, and the\r\n// package's own database/http/socket/mail/connectors/react graph, 4544\r\n// modules total once their dependencies are counted) into this build-tool\r\n// module's graph; importing this file directly pulls in 2 (itself and\r\n// `@mongez/concat-route`). Same pattern as `web-connector-factory.ts`'s\r\n// `../../../core/src/connectors/types` deep import, for the same reason.\r\nimport { normalizeRoutePath } from \"../../../core/src/router/normalize-route-path\";\r\nimport {\r\n discoverPages,\r\n discoverWebRoots,\r\n isDiscoveredRoutablePage,\r\n isFile,\r\n walkFiles,\r\n} from \"./discover-pages\";\r\nimport {\r\n isNotFoundPageFile,\r\n NOT_FOUND_ROUTE_NAME,\r\n NOT_FOUND_ROUTE_PATH,\r\n} from \"../server/not-found-page\";\r\nimport { toPosix } from \"../shared/to-posix\";\r\n\r\n// Re-exported, not redefined: `layoutChainFor` was this module's before the\r\n// split, and the callers that already reach for it here should keep getting\r\n// the one implementation rather than a copy that can drift from it. The nested\r\n// layout error re-exports from the layout policy module, the one place that\r\n// owns layout selection and its error contract.\r\nexport { layoutChainFor } from \"./discover-pages\";\r\nexport { NestedLayoutsNotSupportedError } from \"../routing/layout-policy\";\r\n\r\n/**\r\n * Declared BEFORE the patch that reads it. `const` is hoisted but sits in the\r\n * temporal dead zone, and the patch below is an object literal evaluated at\r\n * module load — reading this from further down the file would throw.\r\n */\r\nconst STYLE_EXTENSIONS = [\".css\", \".scss\", \".sass\", \".less\", \".styl\"];\r\n\r\n/**\r\n * The esbuild patch web contributes (spike-settled, contract §3a).\r\n *\r\n * NO `process.env.NODE_ENV` define: warlock assigns to `process.env.NODE_ENV`\r\n * at runtime (`core/src/utils/environment.ts:14`) and a define would turn that\r\n * assignment into an assignment to a literal.\r\n */\r\nexport const WEB_ESBUILD_PATCH: ConnectorEsbuildPatch = {\r\n jsx: \"automatic\",\r\n jsxImportSource: \"react\",\r\n define: {\r\n \"import.meta.env.DEV\": \"false\",\r\n \"import.meta.env.PROD\": \"true\",\r\n \"import.meta.env.SSR\": \"true\",\r\n \"import.meta.env.MODE\": '\"production\"',\r\n },\r\n /**\r\n * Stylesheets compile to NOTHING in the server bundle, rather than failing it.\r\n *\r\n * `root.tsx` importing `./app.css` is how Tailwind — and every other CSS\r\n * pipeline — is normally wired, so rejecting it meant the framework could not\r\n * build a styled application at all. But the server has no use for the bytes:\r\n * it renders HTML, and all it ever needs of a stylesheet is a URL to put in a\r\n * `<link>`. The CSS itself belongs to the CLIENT bundle, which Vite builds\r\n * from the same `root.tsx` and which handles CSS natively.\r\n *\r\n * So the import stays in the user's source, the server drops it, and Vite\r\n * emits the real asset. Nothing is silently lost: a stylesheet that does not\r\n * exist still fails the client build, loudly, where it matters.\r\n */\r\n loader: Object.fromEntries(\r\n STYLE_EXTENSIONS.map((extension) => [extension, \"empty\" as const]),\r\n ) as ConnectorEsbuildPatch[\"loader\"],\r\n};\r\n\r\n/** Line appended to section 4 of the generated production entry, after `./routes`. */\r\nexport const WEB_ENTRY_IMPORT = 'await import(\"./pages\");';\r\n\r\nconst REMEDY =\r\n \"move the static file into the application's public/ directory and reference \" +\r\n \"it by its root URL instead (for example, public/logo.svg becomes /logo.svg). \" +\r\n \"The production server build does not compile static-asset imports, ?raw/?url \" +\r\n \"queries, or import.meta.url in the page graph in 5.2\";\r\n\r\nconst ASSET_EXTENSIONS = [\r\n \".svg\",\r\n \".png\",\r\n \".jpg\",\r\n \".jpeg\",\r\n \".gif\",\r\n \".webp\",\r\n \".avif\",\r\n \".ico\",\r\n \".woff\",\r\n \".woff2\",\r\n \".ttf\",\r\n \".eot\",\r\n \".mp4\",\r\n \".webm\",\r\n];\r\n\r\nexport type GeneratePagesBarrelOptions = {\r\n /** Absolute path to the application root (where `package.json` lives). */\r\n appRoot: string;\r\n /** Absolute path to `.warlock/production` — where the barrel is written. */\r\n productionDir: string;\r\n /** Source directory name under `appRoot`; defaults to `\"src\"`. */\r\n srcDir?: string;\r\n /**\r\n * App-root-relative POSIX path of the client bundle directory\r\n * (`\"dist/client\"`), baked into the manifest for the runtime to read back.\r\n * See `PageManifest.clientDir` for why it cannot be derived at boot.\r\n *\r\n * The CALLER resolves it, from the same `context.options.outdir` the client\r\n * build itself writes to, so the two cannot name different directories.\r\n */\r\n clientDir: string;\r\n /** App-root-public file paths copied into the production client directory. */\r\n publicFiles?: readonly string[];\r\n};\r\n\r\nexport type GeneratePagesBarrelResult = {\r\n /** How many `*.page.tsx` files discovery found under the page root. */\r\n pageCount: number;\r\n /** Absolute path of the written barrel. Always written when this runs at all. */\r\n barrelFile: string;\r\n /** The written barrel source. */\r\n contents: string;\r\n /** The build-time route surface persisted beside the production output. */\r\n pageRoutes: PageRoutesManifest;\r\n};\r\n\r\nexport type PageRouteManifestEntry = {\r\n method: \"GET\";\r\n path: string;\r\n name: string;\r\n source: string;\r\n};\r\n\r\nexport type PageRoutesManifest = {\r\n version: 1;\r\n routes: PageRouteManifestEntry[];\r\n};\r\n\r\n/** Raised by the Vite-switch tripwire. */\r\nexport class WebPageGraphUnsupportedImportError extends Error {\r\n public constructor(\r\n public readonly sourceFile: string,\r\n public readonly specifier: string,\r\n reason: string,\r\n ) {\r\n super(\r\n `Cannot build the page graph: \"${sourceFile}\" ${reason} (${specifier}). ` +\r\n \"Pages are compiled into the server bundle, which supports plain code imports only — \" +\r\n \"static assets, `?raw`/`?url` queries and `import.meta.url` are not supported there. \" +\r\n \"(Stylesheets ARE supported: the server bundle drops them and the client bundle emits them.) \" +\r\n `To fix: ${REMEDY}.`,\r\n );\r\n this.name = \"WebPageGraphUnsupportedImportError\";\r\n }\r\n}\r\n\r\nconst FROM_SPECIFIER = /\\bfrom\\s*[\"']([^\"']+)[\"']/g;\r\nconst BARE_IMPORT = /\\bimport\\s*[\"']([^\"']+)[\"']/g;\r\nconst DYNAMIC_IMPORT = /\\bimport\\s*\\(\\s*[\"']([^\"']+)[\"']\\s*\\)/g;\r\n\r\n/**\r\n * Blank every comment, preserving byte offsets.\r\n *\r\n * The scan below is regex over source text, so without this a specifier written\r\n * inside a comment is indistinguishable from a real import. That is not\r\n * hypothetical: documenting this very restriction — writing `import \"./app.css\"`\r\n * in a doc comment to explain why it is unsupported — failed the build, and the\r\n * error then insisted the file imported a stylesheet that it did not import.\r\n * Removing the real import did not help, because the comment was the match.\r\n *\r\n * Comments are replaced with spaces rather than deleted so every subsequent\r\n * offset, and therefore every line number in anything reported from here,\r\n * stays exactly where it was.\r\n *\r\n * A parse failure is deliberately NOT fatal: this function's job is to find\r\n * hazards, not to validate syntax. esbuild reports a genuine syntax error far\r\n * better than we would, and failing here would replace its clear message with a\r\n * worse one. On a parse failure the raw source is scanned — the pre-existing\r\n * behaviour, false positives and all.\r\n */\r\nfunction withoutComments(source: string): string {\r\n try {\r\n const ast = parse(source, {\r\n sourceType: \"module\",\r\n errorRecovery: true,\r\n plugins: [\"typescript\", \"jsx\", \"decorators-legacy\"],\r\n });\r\n\r\n let stripped = source;\r\n\r\n for (const comment of ast.comments ?? []) {\r\n const { start, end } = comment as { start: number; end: number };\r\n\r\n stripped = stripped.slice(0, start) + \" \".repeat(end - start) + stripped.slice(end);\r\n }\r\n\r\n return stripped;\r\n } catch {\r\n return source;\r\n }\r\n}\r\n\r\n/** Expects source whose comments have already been blanked by {@link withoutComments}. */\r\nfunction collectSpecifiers(source: string): string[] {\r\n const specifiers: string[] = [];\r\n\r\n for (const pattern of [FROM_SPECIFIER, BARE_IMPORT, DYNAMIC_IMPORT]) {\r\n pattern.lastIndex = 0;\r\n\r\n let match = pattern.exec(source);\r\n\r\n while (match !== null) {\r\n specifiers.push(match[1]);\r\n match = pattern.exec(source);\r\n }\r\n }\r\n\r\n return specifiers;\r\n}\r\n\r\nfunction hazardFor(specifier: string): string | undefined {\r\n const [base, query] = specifier.split(\"?\", 2);\r\n const lowered = base.toLowerCase();\r\n\r\n /*\r\n STYLESHEETS ARE NOT A HAZARD — see `loader` in WEB_ESBUILD_PATCH above.\r\n\r\n They used to be rejected here, which meant `import \"./app.css\"` in\r\n `root.tsx` — the ordinary way to wire Tailwind or any other CSS pipeline —\r\n failed the build outright. The server bundle now compiles them to nothing,\r\n because the server only ever needs a stylesheet's URL, and the client bundle\r\n that Vite builds from the same sources emits the real asset.\r\n\r\n STYLE_EXTENSIONS is still the single list both halves read from, so the set\r\n esbuild stubs and the set this check tolerates cannot drift apart.\r\n */\r\n\r\n if (ASSET_EXTENSIONS.some((extension) => lowered.endsWith(extension))) {\r\n return \"imports a static asset\";\r\n }\r\n\r\n if (query !== undefined && /(^|&)(raw|url)(&|=|$)/.test(query)) {\r\n return \"uses a bundler-specific import query\";\r\n }\r\n\r\n return undefined;\r\n}\r\n\r\n/**\r\n * The Vite-switch tripwire. Fails the build\r\n * the moment the page graph needs a capability the esbuild server build does\r\n * not have, naming the file, the specifier and the remedy.\r\n *\r\n * LIMITATIONS, stated on purpose — BOTH directions, because stating only one\r\n * is what made the other one expensive to find:\r\n *\r\n * FALSE NEGATIVES. This is a FILE-LEVEL scan of the `.ts`/`.tsx` sources under\r\n * the discovered web roots — it is not transitive resolution. A page that\r\n * imports a workspace package which itself imports CSS is not caught here; that\r\n * failure surfaces at esbuild time instead.\r\n *\r\n * FALSE POSITIVES. The scan is regex over source text, so anything that LOOKS\r\n * like a specifier counts. Comments are blanked first ({@link withoutComments})\r\n * because a documented example used to fail the build while insisting on an\r\n * import that was not there. String and template literals are NOT blanked —\r\n * they cannot be, since real specifiers are string literals — so a specifier\r\n * inside a template literal (a code sample in a docs page, say) still matches.\r\n * Rare, and it fails loudly rather than silently, but it is not impossible.\r\n */\r\nexport function assertNoViteOnlyImports(webRoots: readonly string[], appRoot: string): void {\r\n for (const webRoot of webRoots) {\r\n const sources = walkFiles(\r\n webRoot,\r\n (fileName) => fileName.endsWith(\".ts\") || fileName.endsWith(\".tsx\"),\r\n );\r\n\r\n for (const sourceFile of sources) {\r\n // Comments stripped ONCE, and reused by both checks below: `import.meta.url`\r\n // was as blind to comments as the specifier scan was, and mentioning it in\r\n // prose — as this very file does — would have failed the build.\r\n const source = withoutComments(fs.readFileSync(sourceFile, \"utf-8\"));\r\n const relative = toPosix(path.relative(appRoot, sourceFile));\r\n\r\n for (const specifier of collectSpecifiers(source)) {\r\n const reason = hazardFor(specifier);\r\n\r\n if (reason !== undefined) {\r\n throw new WebPageGraphUnsupportedImportError(relative, specifier, reason);\r\n }\r\n }\r\n\r\n if (source.includes(\"import.meta.url\")) {\r\n throw new WebPageGraphUnsupportedImportError(\r\n relative,\r\n \"import.meta.url\",\r\n \"uses `import.meta.url`, whose meaning changes under the bundled server build\",\r\n );\r\n }\r\n }\r\n }\r\n}\r\n\r\n/** `<productionDir>` -> `<file>` as an extensionless, POSIX, relative specifier. */\r\nfunction importSpecifierFor(productionDir: string, file: string): string {\r\n const relative = toPosix(path.relative(productionDir, file)).replace(/\\.tsx?$/, \"\");\r\n\r\n return relative.startsWith(\".\") ? relative : `./${relative}`;\r\n}\r\n\r\nfunction quote(value: string): string {\r\n return JSON.stringify(value);\r\n}\r\n\r\ntype BarrelPage = {\r\n identifier: string;\r\n sourceFile: string;\r\n layouts: { identifier: string; sourceFile: string }[];\r\n};\r\n\r\n/** Header of every generated barrel — the banner plus the one runtime import. */\r\nconst BARREL_HEADER = [\r\n \"// AUTO-GENERATED by @warlock.js/web — do not edit.\",\r\n \"// The page graph, compiled into the server bundle.\",\r\n \"// Route paths are deliberately absent: they are read off each module at boot.\",\r\n // The CONNECTOR subpath, never the root barrel: this file is compiled into\r\n // the server bundle, and `\"@warlock.js/web\"` would drag React in with it.\r\n 'import { providePageManifest } from \"@warlock.js/web/connector\";',\r\n];\r\n\r\n/**\r\n * The barrel written when discovery found no pages at all.\r\n *\r\n * No app entry: `root.tsx` is the root every PAGE renders inside, so with zero\r\n * pages there is nothing for it to wrap and requiring it would fail builds that\r\n * are legitimately page-free.\r\n */\r\nconst EMPTY_BARREL_CONTENTS = [\r\n ...BARREL_HEADER,\r\n \"\",\r\n \"// Zero pages were discovered. The empty table is still provided, because that\",\r\n '// is what tells the boot-time reader \"this bundle WAS built with web, it just',\r\n '// has no pages\" — as opposed to a bundle built without web at all, where no',\r\n \"// barrel runs and the manifest stays absent.\",\r\n \"providePageManifest({ pages: [] });\",\r\n \"\",\r\n].join(\"\\n\");\r\n\r\nfunction emptyPageBarrelContents(clientDir: string, publicFiles: readonly string[]): string {\r\n if (publicFiles.length === 0) return EMPTY_BARREL_CONTENTS;\r\n\r\n return [\r\n ...BARREL_HEADER,\r\n \"\",\r\n \"// Zero pages were discovered, but app-owned public files still belong to\",\r\n \"// the production browser surface and are served from the copied client tree.\",\r\n \"providePageManifest({\",\r\n ` clientDir: ${quote(clientDir)},`,\r\n ` publicFiles: ${JSON.stringify([...publicFiles])},`,\r\n \" pages: [],\",\r\n \"});\",\r\n \"\",\r\n ].join(\"\\n\");\r\n}\r\n\r\nasync function writeBarrel(productionDir: string, contents: string): Promise<string> {\r\n const barrelFile = path.join(productionDir, \"pages.ts\");\r\n\r\n await fs.promises.mkdir(productionDir, { recursive: true });\r\n await fs.promises.writeFile(barrelFile, contents, \"utf-8\");\r\n\r\n return barrelFile;\r\n}\r\n\r\n/**\r\n * Discovers the page graph, runs the tripwire, and writes\r\n * `<productionDir>/pages.ts`.\r\n *\r\n * Zero pages is NOT an error, and it is NOT silence either: the barrel is\r\n * written anyway carrying an empty table, so \"configured with web, no pages\"\r\n * reaches the runtime as a fact instead of looking identical to \"never built\r\n * with web\". The run also says so out loud (no silent caps). What the caller\r\n * skips on zero pages is the CLIENT bundle — no pages means nothing to\r\n * hydrate.\r\n */\r\nexport async function generatePagesBarrel(\r\n options: GeneratePagesBarrelOptions,\r\n): Promise<GeneratePagesBarrelResult> {\r\n const { appRoot, productionDir, clientDir, publicFiles = [] } = options;\r\n const srcRoot = path.join(appRoot, options.srcDir ?? \"src\");\r\n const webRoots = discoverWebRoots(srcRoot);\r\n const discovered = discoverPages({ appRoot, srcDir: options.srcDir });\r\n const routablePages = discovered.filter(isDiscoveredRoutablePage);\r\n const errorPage = discovered.find((page) => page.type === \"error\");\r\n\r\n if (discovered.length === 0) {\r\n console.log(\"web configured, 0 pages\");\r\n const contents = emptyPageBarrelContents(clientDir, publicFiles);\r\n\r\n return {\r\n pageCount: 0,\r\n barrelFile: await writeBarrel(productionDir, contents),\r\n contents,\r\n pageRoutes: { version: 1, routes: [] },\r\n };\r\n }\r\n\r\n assertNoViteOnlyImports(webRoots, appRoot);\r\n\r\n const appFile = path.join(srcRoot, \"web\", \"root.tsx\");\r\n\r\n if (routablePages.length > 0 && !isFile(appFile)) {\r\n throw new Error(\r\n `Cannot generate the page barrel: ${discovered.length} page(s) were discovered but the application root ` +\r\n `component \"${toPosix(path.relative(appRoot, appFile))}\" does not exist. Every page renders inside it.`,\r\n );\r\n }\r\n\r\n const layoutIdentifiers = new Map<string, string>();\r\n const pages: BarrelPage[] = [];\r\n // Persisted paths run through the ROUTER's own `normalizeRoutePath`, the\r\n // single definition of a route path's canonical form. Discovery derives these\r\n // from the filesystem and never registers anything, so without this the\r\n // manifest records a spelling the router never serves — the catch-all is\r\n // literally `\"*\"` here and `\"/*\"` once registered — and `warlock routes:diff`\r\n // reports drift on an untouched checkout right after a successful build.\r\n const notFoundPage = routablePages.find((page) => isNotFoundPageFile(page.pageFile));\r\n const pageRoutes: PageRoutesManifest = {\r\n version: 1,\r\n routes: [\r\n ...routablePages\r\n .filter((page) => !isNotFoundPageFile(page.pageFile))\r\n .map((page) => ({\r\n method: \"GET\" as const,\r\n path: normalizeRoutePath(page.routePath),\r\n name: page.routeName,\r\n source: toPosix(path.relative(appRoot, page.pageFile)),\r\n })),\r\n {\r\n method: \"GET\" as const,\r\n path: normalizeRoutePath(NOT_FOUND_ROUTE_PATH),\r\n name: NOT_FOUND_ROUTE_NAME,\r\n source:\r\n notFoundPage === undefined\r\n ? \"\\u0000warlock:framework-default-404\"\r\n : toPosix(path.relative(appRoot, notFoundPage.pageFile)),\r\n },\r\n ],\r\n };\r\n\r\n for (const [index, page] of routablePages.entries()) {\r\n const layouts = page.layouts.map((layoutFile) => {\r\n let identifier = layoutIdentifiers.get(layoutFile);\r\n\r\n if (identifier === undefined) {\r\n identifier = `l${layoutIdentifiers.size}`;\r\n layoutIdentifiers.set(layoutFile, identifier);\r\n }\r\n\r\n return { identifier, sourceFile: layoutFile };\r\n });\r\n\r\n pages.push({ identifier: `p${index}`, sourceFile: page.pageFile, layouts });\r\n }\r\n\r\n const importLines = [\r\n ...(routablePages.length === 0\r\n ? []\r\n : [`import * as app from ${quote(importSpecifierFor(productionDir, appFile))};`]),\r\n ...(errorPage === undefined\r\n ? []\r\n : [\r\n `import * as errorPage from ${quote(importSpecifierFor(productionDir, errorPage.pageFile))};`,\r\n ]),\r\n ...[...layoutIdentifiers.entries()].map(\r\n ([layoutFile, identifier]) =>\r\n `import * as ${identifier} from ${quote(importSpecifierFor(productionDir, layoutFile))};`,\r\n ),\r\n ...pages.map(\r\n (page) =>\r\n `import * as ${page.identifier} from ${quote(importSpecifierFor(productionDir, page.sourceFile))};`,\r\n ),\r\n ];\r\n\r\n const relativeToApp = (file: string) => quote(toPosix(path.relative(appRoot, file)));\r\n\r\n const pageEntries = pages.map((page) => {\r\n const layouts = page.layouts\r\n .map(\r\n (layout) =>\r\n `{ module: ${layout.identifier}, sourceFile: ${relativeToApp(layout.sourceFile)} }`,\r\n )\r\n .join(\", \");\r\n\r\n return [\r\n \" {\",\r\n ` module: ${page.identifier},`,\r\n ` sourceFile: ${relativeToApp(page.sourceFile)},`,\r\n ` layouts: [${layouts}],`,\r\n \" },\",\r\n ].join(\"\\n\");\r\n });\r\n\r\n const contents = [\r\n ...BARREL_HEADER,\r\n ...importLines,\r\n \"\",\r\n \"providePageManifest({\",\r\n ...(routablePages.length === 0 && publicFiles.length === 0\r\n ? []\r\n : [` clientDir: ${quote(clientDir)},`]),\r\n ...(publicFiles.length === 0 ? [] : [` publicFiles: ${JSON.stringify([...publicFiles])},`]),\r\n ...(routablePages.length === 0\r\n ? []\r\n : [` app: { module: app, sourceFile: ${relativeToApp(appFile)} },`]),\r\n ...(errorPage === undefined\r\n ? []\r\n : [` errorPage: { module: errorPage, sourceFile: ${relativeToApp(errorPage.pageFile)} },`]),\r\n \" pages: [\",\r\n ...pageEntries.map((entry) =>\r\n entry\r\n .split(\"\\n\")\r\n .map((line) => ` ${line}`)\r\n .join(\"\\n\"),\r\n ),\r\n \" ],\",\r\n \"});\",\r\n \"\",\r\n ].join(\"\\n\");\r\n\r\n return {\r\n pageCount: pages.length,\r\n barrelFile: await writeBarrel(productionDir, contents),\r\n contents,\r\n pageRoutes,\r\n };\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DA,MAAM,mBAAmB;CAAC;CAAQ;CAAS;CAAS;CAAS;AAAO;;;;;;;;AASpE,MAAa,oBAA2C;CACtD,KAAK;CACL,iBAAiB;CACjB,QAAQ;EACN,uBAAuB;EACvB,wBAAwB;EACxB,uBAAuB;EACvB,wBAAwB;CAC1B;;;;;;;;;;;;;;;CAeA,QAAQ,OAAO,YACb,iBAAiB,KAAK,cAAc,CAAC,WAAW,OAAgB,CAAC,CACnE;AACF;;AAGA,MAAa,mBAAmB;AAEhC,MAAM,SACJ;AAKF,MAAM,mBAAmB;CACvB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;AA8CA,IAAa,qCAAb,cAAwD,MAAM;CAE1C;CACA;CAFlB,AAAO,YACL,AAAgB,YAChB,AAAgB,WAChB,QACA;EACA,MACE,iCAAiC,WAAW,IAAI,OAAO,IAAI,UAAU,uRAIxD,OAAO,EACtB;EAVgB;EACA;EAUhB,KAAK,OAAO;CACd;AACF;AAEA,MAAM,iBAAiB;AACvB,MAAM,cAAc;AACpB,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;AAsBvB,SAAS,gBAAgB,QAAwB;CAC/C,IAAI;EACF,MAAM,MAAM,MAAM,QAAQ;GACxB,YAAY;GACZ,eAAe;GACf,SAAS;IAAC;IAAc;IAAO;GAAmB;EACpD,CAAC;EAED,IAAI,WAAW;EAEf,KAAK,MAAM,WAAW,IAAI,YAAY,CAAC,GAAG;GACxC,MAAM,EAAE,OAAO,QAAQ;GAEvB,WAAW,SAAS,MAAM,GAAG,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,IAAI,SAAS,MAAM,GAAG;EACpF;EAEA,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;;AAGA,SAAS,kBAAkB,QAA0B;CACnD,MAAM,aAAuB,CAAC;CAE9B,KAAK,MAAM,WAAW;EAAC;EAAgB;EAAa;CAAc,GAAG;EACnE,QAAQ,YAAY;EAEpB,IAAI,QAAQ,QAAQ,KAAK,MAAM;EAE/B,OAAO,UAAU,MAAM;GACrB,WAAW,KAAK,MAAM,EAAE;GACxB,QAAQ,QAAQ,KAAK,MAAM;EAC7B;CACF;CAEA,OAAO;AACT;AAEA,SAAS,UAAU,WAAuC;CACxD,MAAM,CAAC,MAAM,SAAS,UAAU,MAAM,KAAK,CAAC;CAC5C,MAAM,UAAU,KAAK,YAAY;CAejC,IAAI,iBAAiB,MAAM,cAAc,QAAQ,SAAS,SAAS,CAAC,GAClE,OAAO;CAGT,IAAI,UAAU,UAAa,wBAAwB,KAAK,KAAK,GAC3D,OAAO;AAIX;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,wBAAwB,UAA6B,SAAuB;CAC1F,KAAK,MAAM,WAAW,UAAU;EAC9B,MAAM,UAAU,UACd,UACC,aAAa,SAAS,SAAS,KAAK,KAAK,SAAS,SAAS,MAAM,CACpE;EAEA,KAAK,MAAM,cAAc,SAAS;GAIhC,MAAM,SAAS,gBAAgB,GAAG,aAAa,YAAY,OAAO,CAAC;GACnE,MAAM,WAAW,QAAQ,KAAK,SAAS,SAAS,UAAU,CAAC;GAE3D,KAAK,MAAM,aAAa,kBAAkB,MAAM,GAAG;IACjD,MAAM,SAAS,UAAU,SAAS;IAElC,IAAI,WAAW,QACb,MAAM,IAAI,mCAAmC,UAAU,WAAW,MAAM;GAE5E;GAEA,IAAI,OAAO,SAAS,iBAAiB,GACnC,MAAM,IAAI,mCACR,UACA,mBACA,8EACF;EAEJ;CACF;AACF;;AAGA,SAAS,mBAAmB,eAAuB,MAAsB;CACvE,MAAM,WAAW,QAAQ,KAAK,SAAS,eAAe,IAAI,CAAC,CAAC,CAAC,QAAQ,WAAW,EAAE;CAElF,OAAO,SAAS,WAAW,GAAG,IAAI,WAAW,KAAK;AACpD;AAEA,SAAS,MAAM,OAAuB;CACpC,OAAO,KAAK,UAAU,KAAK;AAC7B;;AASA,MAAM,gBAAgB;CACpB;CACA;CACA;CAGA;AACF;;;;;;;;AASA,MAAM,wBAAwB;CAC5B,GAAG;CACH;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,IAAI;AAEX,SAAS,wBAAwB,WAAmB,aAAwC;CAC1F,IAAI,YAAY,WAAW,GAAG,OAAO;CAErC,OAAO;EACL,GAAG;EACH;EACA;EACA;EACA;EACA,gBAAgB,MAAM,SAAS,EAAE;EACjC,kBAAkB,KAAK,UAAU,CAAC,GAAG,WAAW,CAAC,EAAE;EACnD;EACA;EACA;CACF,CAAC,CAAC,KAAK,IAAI;AACb;AAEA,eAAe,YAAY,eAAuB,UAAmC;CACnF,MAAM,aAAa,KAAK,KAAK,eAAe,UAAU;CAEtD,MAAM,GAAG,SAAS,MAAM,eAAe,EAAE,WAAW,KAAK,CAAC;CAC1D,MAAM,GAAG,SAAS,UAAU,YAAY,UAAU,OAAO;CAEzD,OAAO;AACT;;;;;;;;;;;;AAaA,eAAsB,oBACpB,SACoC;CACpC,MAAM,EAAE,SAAS,eAAe,WAAW,cAAc,CAAC,MAAM;CAChE,MAAM,UAAU,KAAK,KAAK,SAAS,QAAQ,UAAU,KAAK;CAC1D,MAAM,WAAW,iBAAiB,OAAO;CACzC,MAAM,aAAa,cAAc;EAAE;EAAS,QAAQ,QAAQ;CAAO,CAAC;CACpE,MAAM,gBAAgB,WAAW,OAAO,wBAAwB;CAChE,MAAM,YAAY,WAAW,MAAM,SAAS,KAAK,SAAS,OAAO;CAEjE,IAAI,WAAW,WAAW,GAAG;EAC3B,QAAQ,IAAI,yBAAyB;EACrC,MAAM,WAAW,wBAAwB,WAAW,WAAW;EAE/D,OAAO;GACL,WAAW;GACX,YAAY,MAAM,YAAY,eAAe,QAAQ;GACrD;GACA,YAAY;IAAE,SAAS;IAAG,QAAQ,CAAC;GAAE;EACvC;CACF;CAEA,wBAAwB,UAAU,OAAO;CAEzC,MAAM,UAAU,KAAK,KAAK,SAAS,OAAO,UAAU;CAEpD,IAAI,cAAc,SAAS,KAAK,CAAC,OAAO,OAAO,GAC7C,MAAM,IAAI,MACR,oCAAoC,WAAW,OAAO,+DACtC,QAAQ,KAAK,SAAS,SAAS,OAAO,CAAC,EAAE,gDAC3D;CAGF,MAAM,oCAAoB,IAAI,IAAoB;CAClD,MAAM,QAAsB,CAAC;CAO7B,MAAM,eAAe,cAAc,MAAM,SAAS,mBAAmB,KAAK,QAAQ,CAAC;CACnF,MAAM,aAAiC;EACrC,SAAS;EACT,QAAQ,CACN,GAAG,cACA,QAAQ,SAAS,CAAC,mBAAmB,KAAK,QAAQ,CAAC,CAAC,CACpD,KAAK,UAAU;GACd,QAAQ;GACR,MAAM,mBAAmB,KAAK,SAAS;GACvC,MAAM,KAAK;GACX,QAAQ,QAAQ,KAAK,SAAS,SAAS,KAAK,QAAQ,CAAC;EACvD,EAAE,GACJ;GACE,QAAQ;GACR,MAAM,sBAAuC;GAC7C,MAAM;GACN,QACE,iBAAiB,SACb,oCACA,QAAQ,KAAK,SAAS,SAAS,aAAa,QAAQ,CAAC;EAC7D,CACF;CACF;CAEA,KAAK,MAAM,CAAC,OAAO,SAAS,cAAc,QAAQ,GAAG;EACnD,MAAM,UAAU,KAAK,QAAQ,KAAK,eAAe;GAC/C,IAAI,aAAa,kBAAkB,IAAI,UAAU;GAEjD,IAAI,eAAe,QAAW;IAC5B,aAAa,IAAI,kBAAkB;IACnC,kBAAkB,IAAI,YAAY,UAAU;GAC9C;GAEA,OAAO;IAAE;IAAY,YAAY;GAAW;EAC9C,CAAC;EAED,MAAM,KAAK;GAAE,YAAY,IAAI;GAAS,YAAY,KAAK;GAAU;EAAQ,CAAC;CAC5E;CAEA,MAAM,cAAc;EAClB,GAAI,cAAc,WAAW,IACzB,CAAC,IACD,CAAC,wBAAwB,MAAM,mBAAmB,eAAe,OAAO,CAAC,EAAE,EAAE;EACjF,GAAI,cAAc,SACd,CAAC,IACD,CACE,8BAA8B,MAAM,mBAAmB,eAAe,UAAU,QAAQ,CAAC,EAAE,EAC7F;EACJ,GAAG,CAAC,GAAG,kBAAkB,QAAQ,CAAC,CAAC,CAAC,KACjC,CAAC,YAAY,gBACZ,eAAe,WAAW,QAAQ,MAAM,mBAAmB,eAAe,UAAU,CAAC,EAAE,EAC3F;EACA,GAAG,MAAM,KACN,SACC,eAAe,KAAK,WAAW,QAAQ,MAAM,mBAAmB,eAAe,KAAK,UAAU,CAAC,EAAE,EACrG;CACF;CAEA,MAAM,iBAAiB,SAAiB,MAAM,QAAQ,KAAK,SAAS,SAAS,IAAI,CAAC,CAAC;CAEnF,MAAM,cAAc,MAAM,KAAK,SAAS;EACtC,MAAM,UAAU,KAAK,QAClB,KACE,WACC,aAAa,OAAO,WAAW,gBAAgB,cAAc,OAAO,UAAU,EAAE,GACpF,CAAC,CACA,KAAK,IAAI;EAEZ,OAAO;GACL;GACA,eAAe,KAAK,WAAW;GAC/B,mBAAmB,cAAc,KAAK,UAAU,EAAE;GAClD,iBAAiB,QAAQ;GACzB;EACF,CAAC,CAAC,KAAK,IAAI;CACb,CAAC;CAED,MAAM,WAAW;EACf,GAAG;EACH,GAAG;EACH;EACA;EACA,GAAI,cAAc,WAAW,KAAK,YAAY,WAAW,IACrD,CAAC,IACD,CAAC,gBAAgB,MAAM,SAAS,EAAE,EAAE;EACxC,GAAI,YAAY,WAAW,IAAI,CAAC,IAAI,CAAC,kBAAkB,KAAK,UAAU,CAAC,GAAG,WAAW,CAAC,EAAE,EAAE;EAC1F,GAAI,cAAc,WAAW,IACzB,CAAC,IACD,CAAC,qCAAqC,cAAc,OAAO,EAAE,IAAI;EACrE,GAAI,cAAc,SACd,CAAC,IACD,CAAC,iDAAiD,cAAc,UAAU,QAAQ,EAAE,IAAI;EAC5F;EACA,GAAG,YAAY,KAAK,UAClB,MACG,MAAM,IAAI,CAAC,CACX,KAAK,SAAS,KAAK,MAAM,CAAC,CAC1B,KAAK,IAAI,CACd;EACA;EACA;EACA;CACF,CAAC,CAAC,KAAK,IAAI;CAEX,OAAO;EACL,WAAW,MAAM;EACjB,YAAY,MAAM,YAAY,eAAe,QAAQ;EACrD;EACA;CACF;AACF"}
@@ -29,6 +29,34 @@ async function collectPublicFiles(publicRoot) {
29
29
  await visit(publicRoot);
30
30
  return files;
31
31
  }
32
+ /**
33
+ * The synchronous twin of {@link collectPublicFiles}, for callers that walk
34
+ * `public/` at a point that cannot await — production boot registers routes
35
+ * synchronously, so its staleness check needs this instead of the build-time
36
+ * async walk.
37
+ */
38
+ function collectPublicFilesSync(publicRoot) {
39
+ let root;
40
+ try {
41
+ root = fs.statSync(publicRoot);
42
+ } catch (error) {
43
+ if (error.code === "ENOENT") return [];
44
+ throw error;
45
+ }
46
+ if (!root.isDirectory()) return [];
47
+ const files = [];
48
+ function visit(directory) {
49
+ const entries = fs.readdirSync(directory, { withFileTypes: true });
50
+ entries.sort((left, right) => left.name.localeCompare(right.name));
51
+ for (const entry of entries) {
52
+ const absolute = path.join(directory, entry.name);
53
+ if (entry.isDirectory()) visit(absolute);
54
+ else if (entry.isFile()) files.push(path.relative(publicRoot, absolute).split(path.sep).join("/"));
55
+ }
56
+ }
57
+ visit(publicRoot);
58
+ return files;
59
+ }
32
60
  /** Copy exactly the files recorded by {@link collectPublicFiles}. */
33
61
  async function copyPublicFiles(publicRoot, outputRoot, files) {
34
62
  for (const file of files) {
@@ -41,5 +69,5 @@ async function copyPublicFiles(publicRoot, outputRoot, files) {
41
69
  }
42
70
 
43
71
  //#endregion
44
- export { collectPublicFiles, copyPublicFiles };
72
+ export { collectPublicFiles, collectPublicFilesSync, copyPublicFiles };
45
73
  //# sourceMappingURL=public-files.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"public-files.mjs","names":[],"sources":["../../../../../../../web/src/build/public-files.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\n/**\n * Enumerate the application-owned files Vite serves from `<appRoot>/public`\n * in development. Paths are POSIX and relative so the same list can be baked\n * into the production page manifest without carrying a machine-local root.\n */\nexport async function collectPublicFiles(publicRoot: string): Promise<string[]> {\n let root: fs.Stats;\n\n try {\n root = await fs.promises.stat(publicRoot);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return [];\n throw error;\n }\n\n if (!root.isDirectory()) return [];\n\n const files: string[] = [];\n\n async function visit(directory: string): Promise<void> {\n const entries = await fs.promises.readdir(directory, { withFileTypes: true });\n entries.sort((left, right) => left.name.localeCompare(right.name));\n\n for (const entry of entries) {\n const absolute = path.join(directory, entry.name);\n\n if (entry.isDirectory()) {\n await visit(absolute);\n } else if (entry.isFile()) {\n files.push(path.relative(publicRoot, absolute).split(path.sep).join(\"/\"));\n }\n }\n }\n\n await visit(publicRoot);\n return files;\n}\n\n/** Copy exactly the files recorded by {@link collectPublicFiles}. */\nexport async function copyPublicFiles(\n publicRoot: string,\n outputRoot: string,\n files: readonly string[],\n): Promise<void> {\n for (const file of files) {\n const segments = file.split(\"/\");\n const source = path.join(publicRoot, ...segments);\n const target = path.join(outputRoot, ...segments);\n\n await fs.promises.mkdir(path.dirname(target), { recursive: true });\n await fs.promises.copyFile(source, target);\n }\n}\n"],"mappings":";;;;;;;;;AAQA,eAAsB,mBAAmB,YAAuC;CAC9E,IAAI;CAEJ,IAAI;EACF,OAAO,MAAM,GAAG,SAAS,KAAK,UAAU;CAC1C,SAAS,OAAO;EACd,IAAK,MAAgC,SAAS,UAAU,OAAO,CAAC;EAChE,MAAM;CACR;CAEA,IAAI,CAAC,KAAK,YAAY,GAAG,OAAO,CAAC;CAEjC,MAAM,QAAkB,CAAC;CAEzB,eAAe,MAAM,WAAkC;EACrD,MAAM,UAAU,MAAM,GAAG,SAAS,QAAQ,WAAW,EAAE,eAAe,KAAK,CAAC;EAC5E,QAAQ,MAAM,MAAM,UAAU,KAAK,KAAK,cAAc,MAAM,IAAI,CAAC;EAEjE,KAAK,MAAM,SAAS,SAAS;GAC3B,MAAM,WAAW,KAAK,KAAK,WAAW,MAAM,IAAI;GAEhD,IAAI,MAAM,YAAY,GACpB,MAAM,MAAM,QAAQ;QACf,IAAI,MAAM,OAAO,GACtB,MAAM,KAAK,KAAK,SAAS,YAAY,QAAQ,CAAC,CAAC,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC;EAE5E;CACF;CAEA,MAAM,MAAM,UAAU;CACtB,OAAO;AACT;;AAGA,eAAsB,gBACpB,YACA,YACA,OACe;CACf,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,WAAW,KAAK,MAAM,GAAG;EAC/B,MAAM,SAAS,KAAK,KAAK,YAAY,GAAG,QAAQ;EAChD,MAAM,SAAS,KAAK,KAAK,YAAY,GAAG,QAAQ;EAEhD,MAAM,GAAG,SAAS,MAAM,KAAK,QAAQ,MAAM,GAAG,EAAE,WAAW,KAAK,CAAC;EACjE,MAAM,GAAG,SAAS,SAAS,QAAQ,MAAM;CAC3C;AACF"}
1
+ {"version":3,"file":"public-files.mjs","names":[],"sources":["../../../../../../../web/src/build/public-files.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\n/**\n * Enumerate the application-owned files Vite serves from `<appRoot>/public`\n * in development. Paths are POSIX and relative so the same list can be baked\n * into the production page manifest without carrying a machine-local root.\n */\nexport async function collectPublicFiles(publicRoot: string): Promise<string[]> {\n let root: fs.Stats;\n\n try {\n root = await fs.promises.stat(publicRoot);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return [];\n throw error;\n }\n\n if (!root.isDirectory()) return [];\n\n const files: string[] = [];\n\n async function visit(directory: string): Promise<void> {\n const entries = await fs.promises.readdir(directory, { withFileTypes: true });\n entries.sort((left, right) => left.name.localeCompare(right.name));\n\n for (const entry of entries) {\n const absolute = path.join(directory, entry.name);\n\n if (entry.isDirectory()) {\n await visit(absolute);\n } else if (entry.isFile()) {\n files.push(path.relative(publicRoot, absolute).split(path.sep).join(\"/\"));\n }\n }\n }\n\n await visit(publicRoot);\n return files;\n}\n\n/**\n * The synchronous twin of {@link collectPublicFiles}, for callers that walk\n * `public/` at a point that cannot await — production boot registers routes\n * synchronously, so its staleness check needs this instead of the build-time\n * async walk.\n */\nexport function collectPublicFilesSync(publicRoot: string): string[] {\n let root: fs.Stats;\n\n try {\n root = fs.statSync(publicRoot);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return [];\n throw error;\n }\n\n if (!root.isDirectory()) return [];\n\n const files: string[] = [];\n\n function visit(directory: string): void {\n const entries = fs.readdirSync(directory, { withFileTypes: true });\n entries.sort((left, right) => left.name.localeCompare(right.name));\n\n for (const entry of entries) {\n const absolute = path.join(directory, entry.name);\n\n if (entry.isDirectory()) {\n visit(absolute);\n } else if (entry.isFile()) {\n files.push(path.relative(publicRoot, absolute).split(path.sep).join(\"/\"));\n }\n }\n }\n\n visit(publicRoot);\n return files;\n}\n\n/** Copy exactly the files recorded by {@link collectPublicFiles}. */\nexport async function copyPublicFiles(\n publicRoot: string,\n outputRoot: string,\n files: readonly string[],\n): Promise<void> {\n for (const file of files) {\n const segments = file.split(\"/\");\n const source = path.join(publicRoot, ...segments);\n const target = path.join(outputRoot, ...segments);\n\n await fs.promises.mkdir(path.dirname(target), { recursive: true });\n await fs.promises.copyFile(source, target);\n }\n}\n"],"mappings":";;;;;;;;;AAQA,eAAsB,mBAAmB,YAAuC;CAC9E,IAAI;CAEJ,IAAI;EACF,OAAO,MAAM,GAAG,SAAS,KAAK,UAAU;CAC1C,SAAS,OAAO;EACd,IAAK,MAAgC,SAAS,UAAU,OAAO,CAAC;EAChE,MAAM;CACR;CAEA,IAAI,CAAC,KAAK,YAAY,GAAG,OAAO,CAAC;CAEjC,MAAM,QAAkB,CAAC;CAEzB,eAAe,MAAM,WAAkC;EACrD,MAAM,UAAU,MAAM,GAAG,SAAS,QAAQ,WAAW,EAAE,eAAe,KAAK,CAAC;EAC5E,QAAQ,MAAM,MAAM,UAAU,KAAK,KAAK,cAAc,MAAM,IAAI,CAAC;EAEjE,KAAK,MAAM,SAAS,SAAS;GAC3B,MAAM,WAAW,KAAK,KAAK,WAAW,MAAM,IAAI;GAEhD,IAAI,MAAM,YAAY,GACpB,MAAM,MAAM,QAAQ;QACf,IAAI,MAAM,OAAO,GACtB,MAAM,KAAK,KAAK,SAAS,YAAY,QAAQ,CAAC,CAAC,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC;EAE5E;CACF;CAEA,MAAM,MAAM,UAAU;CACtB,OAAO;AACT;;;;;;;AAQA,SAAgB,uBAAuB,YAA8B;CACnE,IAAI;CAEJ,IAAI;EACF,OAAO,GAAG,SAAS,UAAU;CAC/B,SAAS,OAAO;EACd,IAAK,MAAgC,SAAS,UAAU,OAAO,CAAC;EAChE,MAAM;CACR;CAEA,IAAI,CAAC,KAAK,YAAY,GAAG,OAAO,CAAC;CAEjC,MAAM,QAAkB,CAAC;CAEzB,SAAS,MAAM,WAAyB;EACtC,MAAM,UAAU,GAAG,YAAY,WAAW,EAAE,eAAe,KAAK,CAAC;EACjE,QAAQ,MAAM,MAAM,UAAU,KAAK,KAAK,cAAc,MAAM,IAAI,CAAC;EAEjE,KAAK,MAAM,SAAS,SAAS;GAC3B,MAAM,WAAW,KAAK,KAAK,WAAW,MAAM,IAAI;GAEhD,IAAI,MAAM,YAAY,GACpB,MAAM,QAAQ;QACT,IAAI,MAAM,OAAO,GACtB,MAAM,KAAK,KAAK,SAAS,YAAY,QAAQ,CAAC,CAAC,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC;EAE5E;CACF;CAEA,MAAM,UAAU;CAChB,OAAO;AACT;;AAGA,eAAsB,gBACpB,YACA,YACA,OACe;CACf,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,WAAW,KAAK,MAAM,GAAG;EAC/B,MAAM,SAAS,KAAK,KAAK,YAAY,GAAG,QAAQ;EAChD,MAAM,SAAS,KAAK,KAAK,YAAY,GAAG,QAAQ;EAEhD,MAAM,GAAG,SAAS,MAAM,KAAK,QAAQ,MAAM,GAAG,EAAE,WAAW,KAAK,CAAC;EACjE,MAAM,GAAG,SAAS,SAAS,QAAQ,MAAM;CAC3C;AACF"}