@warlock.js/web 5.1.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/README.md +126 -12
  3. package/esm/build/contribution.d.mts +11 -14
  4. package/esm/build/contribution.mjs +46 -58
  5. package/esm/build/contribution.mjs.map +1 -1
  6. package/esm/build/discover-pages.mjs +105 -45
  7. package/esm/build/discover-pages.mjs.map +1 -1
  8. package/esm/build/generate-client-registry.mjs +47 -13
  9. package/esm/build/generate-client-registry.mjs.map +1 -1
  10. package/esm/build/generate-pages-barrel.mjs +55 -13
  11. package/esm/build/generate-pages-barrel.mjs.map +1 -1
  12. package/esm/build/page-default-export.mjs +48 -0
  13. package/esm/build/page-default-export.mjs.map +1 -0
  14. package/esm/build/page-routes-manifest.mjs +21 -0
  15. package/esm/build/page-routes-manifest.mjs.map +1 -0
  16. package/esm/build/public-files.mjs +45 -0
  17. package/esm/build/public-files.mjs.map +1 -0
  18. package/esm/build/read-route-exports.mjs +1 -1
  19. package/esm/build/read-route-exports.mjs.map +1 -1
  20. package/esm/client/build-hydrated-tree.mjs +40 -9
  21. package/esm/client/build-hydrated-tree.mjs.map +1 -1
  22. package/esm/client/hydrate-page.mjs +1 -1
  23. package/esm/client/hydrate-page.mjs.map +1 -1
  24. package/esm/client/navigation/current-route.d.mts +1 -1
  25. package/esm/client/navigation/current-route.mjs +1 -1
  26. package/esm/client/navigation/current-route.mjs.map +1 -1
  27. package/esm/client/navigation/fetch-page-data.mjs +1 -1
  28. package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
  29. package/esm/client/navigation/navigation-root.mjs +1 -1
  30. package/esm/client/navigation/navigation-root.mjs.map +1 -1
  31. package/esm/client/navigation/prefetch.mjs +1 -1
  32. package/esm/client/navigation/prefetch.mjs.map +1 -1
  33. package/esm/client/navigation/refresh.d.mts +1 -1
  34. package/esm/client/navigation/refresh.mjs +1 -1
  35. package/esm/client/navigation/refresh.mjs.map +1 -1
  36. package/esm/client/navigation/scroll-to-fragment.mjs +1 -1
  37. package/esm/client/navigation/scroll-to-fragment.mjs.map +1 -1
  38. package/esm/client/navigation/verbs.d.mts +1 -1
  39. package/esm/client/navigation/verbs.mjs +1 -1
  40. package/esm/client/navigation/verbs.mjs.map +1 -1
  41. package/esm/client/runtime/index.d.mts +2 -1
  42. package/esm/client/runtime/index.mjs +2 -1
  43. package/esm/client/runtime/manifest.d.mts +1 -1
  44. package/esm/client/runtime/manifest.mjs +3 -2
  45. package/esm/client/runtime/manifest.mjs.map +1 -1
  46. package/esm/client/runtime/matcher.d.mts +13 -1
  47. package/esm/client/runtime/matcher.mjs +13 -1
  48. package/esm/client/runtime/matcher.mjs.map +1 -1
  49. package/esm/client/runtime/types.d.mts +15 -2
  50. package/esm/components/default-app.mjs +1 -1
  51. package/esm/components/default-app.mjs.map +1 -1
  52. package/esm/components/document-context.d.mts +40 -2
  53. package/esm/components/document-context.mjs +1 -1
  54. package/esm/components/document-context.mjs.map +1 -1
  55. package/esm/components/head.d.mts +1 -1
  56. package/esm/components/head.mjs +1 -1
  57. package/esm/components/head.mjs.map +1 -1
  58. package/esm/components/link.d.mts +1 -1
  59. package/esm/components/link.mjs +1 -1
  60. package/esm/components/link.mjs.map +1 -1
  61. package/esm/components/scripts.d.mts +1 -1
  62. package/esm/components/scripts.mjs +4 -2
  63. package/esm/components/scripts.mjs.map +1 -1
  64. package/esm/context.d.mts +6 -102
  65. package/esm/hydration/index.mjs +1 -1
  66. package/esm/hydration/index.mjs.map +1 -1
  67. package/esm/hydration-payload.d.mts +1 -1
  68. package/esm/hydration-payload.mjs +37 -8
  69. package/esm/hydration-payload.mjs.map +1 -1
  70. package/esm/index.d.mts +4 -3
  71. package/esm/loaders.d.mts +7 -36
  72. package/esm/metadata.d.mts +1 -1
  73. package/esm/metadata.mjs +1 -1
  74. package/esm/metadata.mjs.map +1 -1
  75. package/esm/props.d.mts +16 -8
  76. package/esm/route.d.mts +2 -17
  77. package/esm/routing/compose-route-path.d.mts +1 -1
  78. package/esm/routing/compose-route-path.mjs +1 -1
  79. package/esm/routing/compose-route-path.mjs.map +1 -1
  80. package/esm/routing/data-request.mjs +1 -1
  81. package/esm/routing/data-request.mjs.map +1 -1
  82. package/esm/routing/filesystem-route.mjs +45 -0
  83. package/esm/routing/filesystem-route.mjs.map +1 -0
  84. package/esm/routing/layout-policy.mjs +1 -1
  85. package/esm/routing/layout-policy.mjs.map +1 -1
  86. package/esm/routing/navigator.mjs +1 -1
  87. package/esm/routing/navigator.mjs.map +1 -1
  88. package/esm/routing/query-string.d.mts +1 -1
  89. package/esm/routing/query-string.mjs +1 -1
  90. package/esm/routing/query-string.mjs.map +1 -1
  91. package/esm/routing/route-identity.mjs +1 -1
  92. package/esm/routing/route-identity.mjs.map +1 -1
  93. package/esm/routing/route-table.d.mts +1 -1
  94. package/esm/routing/route-table.mjs +1 -1
  95. package/esm/routing/route-table.mjs.map +1 -1
  96. package/esm/routing/router-events.d.mts +1 -1
  97. package/esm/routing/router-events.mjs +1 -1
  98. package/esm/routing/router-events.mjs.map +1 -1
  99. package/esm/routing/url-fragment.mjs +1 -1
  100. package/esm/routing/url-fragment.mjs.map +1 -1
  101. package/esm/runtime/register-modules.d.mts +19 -0
  102. package/esm/runtime/register-modules.mjs +22 -0
  103. package/esm/runtime/register-modules.mjs.map +1 -0
  104. package/esm/server/build-hydration-payload.mjs +2 -1
  105. package/esm/server/build-hydration-payload.mjs.map +1 -1
  106. package/esm/server/client-asset-url-prefix.d.mts +1 -1
  107. package/esm/server/client-asset-url-prefix.mjs +1 -1
  108. package/esm/server/client-asset-url-prefix.mjs.map +1 -1
  109. package/esm/server/create-page-module-loader.d.mts +1 -1
  110. package/esm/server/create-page-module-loader.mjs +2 -1
  111. package/esm/server/create-page-module-loader.mjs.map +1 -1
  112. package/esm/server/create-page-route-handler.d.mts +31 -6
  113. package/esm/server/create-page-route-handler.mjs +128 -43
  114. package/esm/server/create-page-route-handler.mjs.map +1 -1
  115. package/esm/server/dev-server.mjs +2 -13
  116. package/esm/server/dev-server.mjs.map +1 -1
  117. package/esm/server/error-page.d.mts +14 -0
  118. package/esm/server/error-page.mjs +39 -0
  119. package/esm/server/error-page.mjs.map +1 -0
  120. package/esm/server/execute-page-request.d.mts +7 -4
  121. package/esm/server/execute-page-request.mjs +79 -80
  122. package/esm/server/execute-page-request.mjs.map +1 -1
  123. package/esm/server/execute-page-request.types.d.mts +33 -92
  124. package/esm/server/hydration-client-url.d.mts +1 -1
  125. package/esm/server/hydration-client-url.mjs +2 -2
  126. package/esm/server/hydration-client-url.mjs.map +1 -1
  127. package/esm/server/index.d.mts +5 -5
  128. package/esm/server/index.mjs +3 -4
  129. package/esm/server/install-page-routes-from-manifest.d.mts +17 -7
  130. package/esm/server/install-page-routes-from-manifest.mjs +71 -10
  131. package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
  132. package/esm/server/install-page-routes.d.mts +32 -15
  133. package/esm/server/install-page-routes.mjs +91 -39
  134. package/esm/server/install-page-routes.mjs.map +1 -1
  135. package/esm/server/install-production-page-routes.mjs +3 -36
  136. package/esm/server/install-production-page-routes.mjs.map +1 -1
  137. package/esm/server/match-page-route.mjs +2 -2
  138. package/esm/server/match-page-route.mjs.map +1 -1
  139. package/esm/server/not-found-page.d.mts +1 -1
  140. package/esm/server/not-found-page.mjs +2 -2
  141. package/esm/server/not-found-page.mjs.map +1 -1
  142. package/esm/server/page-context.d.mts +3 -9
  143. package/esm/server/page-context.mjs +3 -16
  144. package/esm/server/page-context.mjs.map +1 -1
  145. package/esm/server/page-file-change.mjs +77 -0
  146. package/esm/server/page-file-change.mjs.map +1 -0
  147. package/esm/server/page-manifest.d.mts +25 -10
  148. package/esm/server/page-manifest.mjs +1 -1
  149. package/esm/server/page-manifest.mjs.map +1 -1
  150. package/esm/server/page-render-bundle.mjs +38 -0
  151. package/esm/server/page-render-bundle.mjs.map +1 -0
  152. package/esm/server/page-route-reload.mjs +54 -0
  153. package/esm/server/page-route-reload.mjs.map +1 -0
  154. package/esm/server/register-production-public-files.mjs +40 -0
  155. package/esm/server/register-production-public-files.mjs.map +1 -0
  156. package/esm/server/render-page.d.mts +11 -14
  157. package/esm/server/render-page.mjs +154 -28
  158. package/esm/server/render-page.mjs.map +1 -1
  159. package/esm/server/resolve-page-metadata.mjs +2 -2
  160. package/esm/server/resolve-page-metadata.mjs.map +1 -1
  161. package/esm/server/resolve-validation-data.mjs +1 -1
  162. package/esm/server/resolve-validation-data.mjs.map +1 -1
  163. package/esm/server/settle-page-response.d.mts +58 -1
  164. package/esm/server/settle-page-response.mjs +111 -46
  165. package/esm/server/settle-page-response.mjs.map +1 -1
  166. package/esm/server/stylesheet-urls.d.mts +41 -30
  167. package/esm/server/stylesheet-urls.mjs +137 -52
  168. package/esm/server/stylesheet-urls.mjs.map +1 -1
  169. package/esm/server/unregistered-pages.mjs +42 -0
  170. package/esm/server/unregistered-pages.mjs.map +1 -0
  171. package/esm/server/web-connector-factory.d.mts +2 -3
  172. package/esm/server/web-connector-factory.mjs +7 -6
  173. package/esm/server/web-connector-factory.mjs.map +1 -1
  174. package/esm/server/web-connector.d.mts +1 -1
  175. package/esm/server/web-connector.mjs +138 -18
  176. package/esm/server/web-connector.mjs.map +1 -1
  177. package/esm/shared.d.mts +18 -3
  178. package/esm/shared.mjs +3 -7
  179. package/esm/shared.mjs.map +1 -1
  180. package/esm/validation.d.mts +1 -1
  181. package/esm/vite/app-convention-aliases.mjs +1 -1
  182. package/esm/vite/app-convention-aliases.mjs.map +1 -1
  183. package/esm/vite/build-client.d.mts +1 -1
  184. package/esm/vite/build-client.mjs +2 -2
  185. package/esm/vite/build-client.mjs.map +1 -1
  186. package/esm/vite/gate-a-resolve.d.mts +1 -1
  187. package/esm/vite/gate-a-resolve.mjs +67 -13
  188. package/esm/vite/gate-a-resolve.mjs.map +1 -1
  189. package/esm/vite/gate-b-secrets.d.mts +1 -1
  190. package/esm/vite/gate-b-secrets.mjs +35 -17
  191. package/esm/vite/gate-b-secrets.mjs.map +1 -1
  192. package/esm/vite/gate-c-verify.d.mts +1 -1
  193. package/esm/vite/gate-c-verify.mjs +6 -6
  194. package/esm/vite/gate-c-verify.mjs.map +1 -1
  195. package/esm/vite/hydration-entries.d.mts +1 -1
  196. package/esm/vite/hydration-entries.mjs +2 -2
  197. package/esm/vite/hydration-entries.mjs.map +1 -1
  198. package/esm/vite/index.d.mts +9 -6
  199. package/esm/vite/index.mjs +124 -11
  200. package/esm/vite/index.mjs.map +1 -1
  201. package/esm/vite/page-registry-plugin.d.mts +22 -3
  202. package/esm/vite/page-registry-plugin.mjs +48 -14
  203. package/esm/vite/page-registry-plugin.mjs.map +1 -1
  204. package/esm/vite/projection.d.mts +2 -2
  205. package/esm/vite/projection.mjs +38 -15
  206. package/esm/vite/projection.mjs.map +1 -1
  207. package/llms-full.txt +220 -56
  208. package/llms.txt +5 -3
  209. package/package.json +3 -3
  210. package/skills/add-web-to-an-app/SKILL.md +3 -3
  211. package/skills/create-a-page/SKILL.md +117 -10
  212. package/skills/load-page-data/SKILL.md +92 -12
  213. package/skills/navigate-on-the-client/SKILL.md +1 -1
  214. package/skills/serve-styles/SKILL.md +13 -12
  215. package/skills/use-layouts/SKILL.md +14 -8
  216. package/skills/write-the-root/SKILL.md +3 -1
  217. package/esm/server/buffered-response.d.mts +0 -58
  218. package/esm/server/buffered-response.mjs +0 -115
  219. package/esm/server/buffered-response.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"generate-client-registry.mjs","names":[],"sources":["../../../../../../../web/src/build/generate-client-registry.ts"],"sourcesContent":["/**\n * The CLIENT half of the build→runtime handoff: the source of a generated\n * module that carries the page graph into the BROWSER.\n *\n * `generate-pages-barrel.ts` is the server half. The two are deliberately not\n * one function, because they want opposite things from the same recipe: the\n * server barrel emits STATIC `import * as` so every page lands in the one\n * server bundle and its route can be read off the module at boot, while the\n * browser must never download page B to render page A — so every module\n * reference here is a dynamic `import()`, and `name`/`path` are baked in as\n * DATA so the client runtime can match a route before loading anything.\n *\n * This function is PURE. It reads no filesystem, writes no file, and does not\n * call {@link discoverPages} — the caller passes the already-discovered graph\n * in. That is what keeps the \"one discovery run, one graph\" invariant: names\n * and paths agree with the server's because there is only ever one scan, not\n * because two scanners were written to match.\n *\n * Module RESOLUTION is the one thing that legitimately differs between the dev\n * provider and the production provider, so it is injected as\n * {@link GenerateClientRegistryOptions.toImportSpecifier} rather than computed\n * here. No provider-specific data — no Vite ids, no `/@fs/` prefixes, no\n * hashed chunk urls, no HMR metadata — reaches the emitted entry, and every\n * other property stays testable with a trivial mapper.\n */\nimport type { DiscoveredPage } from \"./discover-pages\";\n\nexport type GenerateClientRegistryOptions = {\n /**\n * The discovered page graph, IN THE ORDER DISCOVERY GAVE IT.\n *\n * `discoverPages` already sorts by POSIX source-file path\n * (`discover-pages.ts:368`, comparator at `discover-pages.ts:248-250`), so\n * this emitter preserves that order rather than imposing a second one: two\n * orderings of one graph is exactly the drift the shared scan exists to\n * prevent. Serialization stability only — nothing emitted here implies\n * matching precedence, which is a property of the route grammar.\n */\n pages: readonly DiscoveredPage[];\n /** Maps an absolute source file path to the specifier the emitted module will import. */\n toImportSpecifier: (absoluteFilePath: string) => string;\n};\n\n/** The name of the array the generated module exports. */\nexport const CLIENT_REGISTRY_EXPORT_NAME = \"pages\";\n\n/** The specifier the generated module imports its entry type from. */\nconst CLIENT_RUNTIME_SPECIFIER = \"@warlock.js/web/client/runtime\";\n\n/**\n * Raised when two pages claim one route name.\n *\n * `discoverPages` promises uniqueness already (`discover-pages.ts:370`). This\n * asserts it anyway: the alternative is trusting an upstream promise and\n * emitting a registry in which one page is silently unreachable, which costs\n * far more to diagnose than the check costs to run.\n */\nexport class DuplicateClientPageNameError extends Error {\n public constructor(\n public readonly routeName: string,\n public readonly firstPageFile: string,\n public readonly secondPageFile: string,\n ) {\n super(\n `Two pages claim the client route name \"${routeName}\": \"${firstPageFile}\" and ` +\n `\"${secondPageFile}\". A route name identifies exactly one page, so the second ` +\n \"page would be unreachable in the browser. To fix: rename one page's `route` \" +\n \"export, or move the file so its directory gives it a different route name.\",\n );\n this.name = \"DuplicateClientPageNameError\";\n }\n}\n\n/**\n * Every literal the generator emits goes through here, without exception.\n * Windows separators, quotes and backslashes are all in scope, and a\n * hand-quoted literal that gets one of them wrong fails at bundle time with a\n * parse error no user can act on.\n */\nfunction quote(value: string): string {\n return JSON.stringify(value);\n}\n\nconst HEADER = [\n \"// AUTO-GENERATED by @warlock.js/web — do not edit.\",\n \"// The page graph, compiled into the CLIENT bundle.\",\n \"//\",\n \"// `name` and `path` are baked in as data so the client runtime can match a\",\n \"// route before loading anything; every module reference is a dynamic\",\n \"// `import()` so a page's code is downloaded only when that page is rendered.\",\n `import type { ClientPageEntry } from ${quote(CLIENT_RUNTIME_SPECIFIER)};`,\n];\n\nfunction assertUniqueNames(pages: readonly DiscoveredPage[]): void {\n const pageFileByName = new Map<string, string>();\n\n for (const page of pages) {\n const existing = pageFileByName.get(page.routeName);\n\n if (existing !== undefined) {\n throw new DuplicateClientPageNameError(page.routeName, existing, page.pageFile);\n }\n\n pageFileByName.set(page.routeName, page.pageFile);\n }\n}\n\n/**\n * One entry's `load`.\n *\n * It memoises NOTHING and holds no one-shot state: the runtime calls it again\n * on re-navigation to the same route, and a loader that works once and returns\n * undefined the second time is a silent blank page. Dynamic `import()` is\n * already module-cached by the bundler, so the correct implementation is to\n * let the cache do the caching.\n */\nfunction loadSource(page: DiscoveredPage, toImportSpecifier: (file: string) => string): string[] {\n const bindings = [\"Page\"];\n const specifiers = [toImportSpecifier(page.pageFile)];\n\n // Outermost first, exactly as discovery enumerated the chain — the order IS\n // the nesting, so reordering it would silently reparent the page.\n //\n // Emitted VERBATIM, including the empty chain discovery reports for the\n // not-found page (`./discover-pages.ts`), which is what keeps hydration\n // rendering the same `App(Page)` the installers SSR'd. The not-found case is\n // NOT re-detected here: a second rule about which page gets layouts is a\n // second convention waiting to disagree with the server's, which is the same\n // reason `routePath` below is not re-composed either.\n const layoutBindings = page.layouts.map((layoutFile, index) => {\n bindings.push(`layout${index}`);\n specifiers.push(toImportSpecifier(layoutFile));\n\n return `layout${index}`;\n });\n\n if (page.appFile !== undefined) {\n bindings.push(\"App\");\n specifiers.push(toImportSpecifier(page.appFile));\n }\n\n // `App` is OMITTED, never emitted as `App: undefined`: the runtime validator\n // reads an own `App` key as a promise that a module namespace is behind it.\n const app = page.appFile === undefined ? \"\" : \", App\";\n\n return [\n \" load: async () => {\",\n ` const [${bindings.join(\", \")}] = await Promise.all([`,\n ...specifiers.map((specifier) => ` import(${quote(specifier)}),`),\n \" ]);\",\n \"\",\n ` return { Page, layouts: [${layoutBindings.join(\", \")}]${app} };`,\n \" },\",\n ];\n}\n\nfunction entrySource(\n page: DiscoveredPage,\n toImportSpecifier: (file: string) => string,\n): string[] {\n return [\n \" {\",\n ' type: \"page\",',\n // Verbatim from discovery, both of them. `routePath` in particular is NOT\n // re-derived or re-normalised here: discovery already composed the layout\n // prefix with the declared route, and a second composition is a second\n // convention waiting to disagree with the server's.\n ` name: ${quote(page.routeName)},`,\n ` path: ${quote(page.routePath)},`,\n ...loadSource(page, toImportSpecifier),\n \" },\",\n ];\n}\n\n/**\n * Returns the SOURCE of the client page registry module. Writes nothing.\n *\n * Zero pages is a legal result, not an error, and it is emitted as an explicit\n * empty registry: \"built with web, no pages\" must reach the runtime as a fact\n * rather than as a missing module.\n */\nexport function generateClientRegistry(options: GenerateClientRegistryOptions): string {\n const { pages, toImportSpecifier } = options;\n\n assertUniqueNames(pages);\n\n const declaration = `export const ${CLIENT_REGISTRY_EXPORT_NAME}: readonly ClientPageEntry[] =`;\n\n if (pages.length === 0) {\n return [...HEADER, \"\", `${declaration} [];`, \"\"].join(\"\\n\");\n }\n\n return [\n ...HEADER,\n \"\",\n `${declaration} [`,\n ...pages.flatMap((page) => entrySource(page, toImportSpecifier)),\n \"];\",\n \"\",\n ].join(\"\\n\");\n}\n"],"mappings":";;AA4CA,MAAa,8BAA8B;;AAG3C,MAAM,2BAA2B;;;;;;;;;AAUjC,IAAa,+BAAb,cAAkD,MAAM;CAEpC;CACA;CACA;CAHlB,AAAO,YACL,AAAgB,WAChB,AAAgB,eAChB,AAAgB,gBAChB;EACA,MACE,0CAA0C,UAAU,MAAM,cAAc,SAClE,eAAe,oNAGvB;EATgB;EACA;EACA;EAQhB,KAAK,OAAO;CACd;AACF;;;;;;;AAQA,SAAS,MAAM,OAAuB;CACpC,OAAO,KAAK,UAAU,KAAK;AAC7B;AAEA,MAAM,SAAS;CACb;CACA;CACA;CACA;CACA;CACA;CACA,wCAAwC,MAAM,wBAAwB,EAAE;AAC1E;AAEA,SAAS,kBAAkB,OAAwC;CACjE,MAAM,iCAAiB,IAAI,IAAoB;CAE/C,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,WAAW,eAAe,IAAI,KAAK,SAAS;EAElD,IAAI,aAAa,QACf,MAAM,IAAI,6BAA6B,KAAK,WAAW,UAAU,KAAK,QAAQ;EAGhF,eAAe,IAAI,KAAK,WAAW,KAAK,QAAQ;CAClD;AACF;;;;;;;;;;AAWA,SAAS,WAAW,MAAsB,mBAAuD;CAC/F,MAAM,WAAW,CAAC,MAAM;CACxB,MAAM,aAAa,CAAC,kBAAkB,KAAK,QAAQ,CAAC;CAWpD,MAAM,iBAAiB,KAAK,QAAQ,KAAK,YAAY,UAAU;EAC7D,SAAS,KAAK,SAAS,OAAO;EAC9B,WAAW,KAAK,kBAAkB,UAAU,CAAC;EAE7C,OAAO,SAAS;CAClB,CAAC;CAED,IAAI,KAAK,YAAY,QAAW;EAC9B,SAAS,KAAK,KAAK;EACnB,WAAW,KAAK,kBAAkB,KAAK,OAAO,CAAC;CACjD;CAIA,MAAM,MAAM,KAAK,YAAY,SAAY,KAAK;CAE9C,OAAO;EACL;EACA,gBAAgB,SAAS,KAAK,IAAI,EAAE;EACpC,GAAG,WAAW,KAAK,cAAc,kBAAkB,MAAM,SAAS,EAAE,GAAG;EACvE;EACA;EACA,kCAAkC,eAAe,KAAK,IAAI,EAAE,GAAG,IAAI;EACnE;CACF;AACF;AAEA,SAAS,YACP,MACA,mBACU;CACV,OAAO;EACL;EACA;EAKA,aAAa,MAAM,KAAK,SAAS,EAAE;EACnC,aAAa,MAAM,KAAK,SAAS,EAAE;EACnC,GAAG,WAAW,MAAM,iBAAiB;EACrC;CACF;AACF;;;;;;;;AASA,SAAgB,uBAAuB,SAAgD;CACrF,MAAM,EAAE,OAAO,sBAAsB;CAErC,kBAAkB,KAAK;CAEvB,MAAM,cAAc,gBAAgB,4BAA4B;CAEhE,IAAI,MAAM,WAAW,GACnB,OAAO;EAAC,GAAG;EAAQ;EAAI,GAAG,YAAY;EAAO;CAAE,CAAC,CAAC,KAAK,IAAI;CAG5D,OAAO;EACL,GAAG;EACH;EACA,GAAG,YAAY;EACf,GAAG,MAAM,SAAS,SAAS,YAAY,MAAM,iBAAiB,CAAC;EAC/D;EACA;CACF,CAAC,CAAC,KAAK,IAAI;AACb"}
1
+ {"version":3,"file":"generate-client-registry.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/build/generate-client-registry.ts"],"sourcesContent":["/**\r\n * The CLIENT half of the build→runtime handoff: the source of a generated\r\n * module that carries the page graph into the BROWSER.\r\n *\r\n * `generate-pages-barrel.ts` is the server half. The two are deliberately not\r\n * one function, because they want opposite things from the same recipe: the\r\n * server barrel emits STATIC `import * as` so every page lands in the one\r\n * server bundle and its route can be read off the module at boot, while the\r\n * browser must never download page B to render page A — so every module\r\n * reference here is a dynamic `import()`, and `name`/`path` are baked in as\r\n * DATA so the client runtime can match a route before loading anything.\r\n *\r\n * This function is PURE. It reads no filesystem, writes no file, and does not\r\n * call {@link discoverPages} — the caller passes the already-discovered graph\r\n * in. That is what keeps the \"one discovery run, one graph\" invariant: names\r\n * and paths agree with the server's because there is only ever one scan, not\r\n * because two scanners were written to match.\r\n *\r\n * Module RESOLUTION is the one thing that legitimately differs between the dev\r\n * provider and the production provider, so it is injected as\r\n * {@link GenerateClientRegistryOptions.toImportSpecifier} rather than computed\r\n * here. No provider-specific data — no Vite ids, no `/@fs/` prefixes, no\r\n * hashed chunk urls, no HMR metadata — reaches the emitted entry, and every\r\n * other property stays testable with a trivial mapper.\r\n */\r\nimport {\r\n isDiscoveredRoutablePage,\r\n type DiscoveredPage,\r\n type DiscoveredRoutablePage,\r\n} from \"./discover-pages\";\r\n\r\nexport type GenerateClientRegistryOptions = {\r\n /**\r\n * The discovered page graph, IN THE ORDER DISCOVERY GAVE IT.\r\n *\r\n * `discoverPages` already sorts by POSIX source-file path\r\n * (`discover-pages.ts:368`, comparator at `discover-pages.ts:248-250`), so\r\n * this emitter preserves that order rather than imposing a second one: two\r\n * orderings of one graph is exactly the drift the shared scan exists to\r\n * prevent. Serialization stability only — nothing emitted here implies\r\n * matching precedence, which is a property of the route grammar.\r\n */\r\n pages: readonly DiscoveredPage[];\r\n /** Maps an absolute source file path to the specifier the emitted module will import. */\r\n toImportSpecifier: (absoluteFilePath: string) => string;\r\n};\r\n\r\n/** The name of the array the generated module exports. */\r\nexport const CLIENT_REGISTRY_EXPORT_NAME = \"pages\";\r\n\r\n/** The specifier the generated module imports its entry type from. */\r\nconst CLIENT_RUNTIME_SPECIFIER = \"@warlock.js/web/client/runtime\";\r\n\r\n/**\r\n * Raised when two pages claim one route name.\r\n *\r\n * `discoverPages` promises uniqueness already (`discover-pages.ts:370`). This\r\n * asserts it anyway: the alternative is trusting an upstream promise and\r\n * emitting a registry in which one page is silently unreachable, which costs\r\n * far more to diagnose than the check costs to run.\r\n */\r\nexport class DuplicateClientPageNameError extends Error {\r\n public constructor(\r\n public readonly routeName: string,\r\n public readonly firstPageFile: string,\r\n public readonly secondPageFile: string,\r\n ) {\r\n super(\r\n `Two pages claim the client route name \"${routeName}\": \"${firstPageFile}\" and ` +\r\n `\"${secondPageFile}\". A route name identifies exactly one page, so the second ` +\r\n \"page would be unreachable in the browser. To fix: rename one page's `route` \" +\r\n \"export, or move the file so its directory gives it a different route name.\",\r\n );\r\n this.name = \"DuplicateClientPageNameError\";\r\n }\r\n}\r\n\r\n/**\r\n * Every literal the generator emits goes through here, without exception.\r\n * Windows separators, quotes and backslashes are all in scope, and a\r\n * hand-quoted literal that gets one of them wrong fails at bundle time with a\r\n * parse error no user can act on.\r\n */\r\nfunction quote(value: string): string {\r\n return JSON.stringify(value);\r\n}\r\n\r\nconst HEADER = [\r\n \"// AUTO-GENERATED by @warlock.js/web — do not edit.\",\r\n \"// The page graph, compiled into the CLIENT bundle.\",\r\n \"//\",\r\n \"// `name` and `path` are baked in as data so the client runtime can match a\",\r\n \"// route before loading anything; every module reference is a dynamic\",\r\n \"// `import()` so a page's code is downloaded only when that page is rendered.\",\r\n `import type { ClientPageEntry } from ${quote(CLIENT_RUNTIME_SPECIFIER)};`,\r\n];\r\n\r\nfunction assertUniqueNames(pages: readonly DiscoveredRoutablePage[]): void {\r\n const pageFileByName = new Map<string, string>();\r\n\r\n for (const page of pages) {\r\n const existing = pageFileByName.get(page.routeName);\r\n\r\n if (existing !== undefined) {\r\n throw new DuplicateClientPageNameError(page.routeName, existing, page.pageFile);\r\n }\r\n\r\n pageFileByName.set(page.routeName, page.pageFile);\r\n }\r\n}\r\n\r\n/**\r\n * One entry's `load`.\r\n *\r\n * It memoises NOTHING and holds no one-shot state: the runtime calls it again\r\n * on re-navigation to the same route, and a loader that works once and returns\r\n * undefined the second time is a silent blank page. Dynamic `import()` is\r\n * already module-cached by the bundler, so the correct implementation is to\r\n * let the cache do the caching.\r\n */\r\nfunction loadSource(\r\n page: DiscoveredRoutablePage,\r\n errorPageFile: string | undefined,\r\n toImportSpecifier: (file: string) => string,\r\n): string[] {\r\n // These bindings deliberately retain the REAL module namespace objects\r\n // returned by import(). The client lifecycle registers exports on those\r\n // namespaces before the tree builder extracts their default components.\r\n const bindings = [\"PageModule\"];\r\n const specifiers = [toImportSpecifier(page.pageFile)];\r\n\r\n // Outermost first, exactly as discovery enumerated the chain — the order IS\r\n // the nesting, so reordering it would silently reparent the page.\r\n //\r\n // Emitted VERBATIM, including the empty chain discovery reports for the\r\n // not-found page (`./discover-pages.ts`), which is what keeps hydration\r\n // rendering the same `App(Page)` the installers SSR'd. The not-found case is\r\n // NOT re-detected here: a second rule about which page gets layouts is a\r\n // second convention waiting to disagree with the server's, which is the same\r\n // reason `routePath` below is not re-composed either.\r\n const layoutBindings = page.layouts.map((layoutFile, index) => {\r\n bindings.push(`layoutModule${index}`);\r\n specifiers.push(toImportSpecifier(layoutFile));\r\n\r\n return `layoutModule${index}`;\r\n });\r\n\r\n if (page.appFile !== undefined) {\r\n bindings.push(\"AppModule\");\r\n specifiers.push(toImportSpecifier(page.appFile));\r\n }\r\n\r\n if (errorPageFile !== undefined) {\r\n bindings.push(\"ErrorPageModule\");\r\n specifiers.push(toImportSpecifier(errorPageFile));\r\n }\r\n\r\n // `App` is OMITTED, never emitted as `App: undefined`: the runtime validator\r\n // reads an own `App` key as a promise that a module namespace is behind it.\r\n const app = page.appFile === undefined ? \"\" : \", App: AppModule\";\r\n const errorPage = errorPageFile === undefined ? \"\" : \", ErrorPage: ErrorPageModule\";\r\n\r\n return [\r\n \" load: async () => {\",\r\n ` const [${bindings.join(\", \")}] = await Promise.all([`,\r\n ...specifiers.map((specifier) => ` import(${quote(specifier)}),`),\r\n \" ]);\",\r\n \"\",\r\n ` return { Page: PageModule, layouts: [${layoutBindings.join(\", \")}]${app}${errorPage} };`,\r\n \" },\",\r\n ];\r\n}\r\n\r\nfunction entrySource(\r\n page: DiscoveredRoutablePage,\r\n errorPageFile: string | undefined,\r\n toImportSpecifier: (file: string) => string,\r\n): string[] {\r\n return [\r\n \" {\",\r\n ' type: \"page\",',\r\n // Verbatim from discovery, both of them. `routePath` in particular is NOT\r\n // re-derived or re-normalised here: discovery already composed the layout\r\n // prefix with the declared route, and a second composition is a second\r\n // convention waiting to disagree with the server's.\r\n ` name: ${quote(page.routeName)},`,\r\n ` path: ${quote(page.routePath)},`,\r\n ...loadSource(page, errorPageFile, toImportSpecifier),\r\n \" },\",\r\n ];\r\n}\r\n\r\n/**\r\n * Returns the SOURCE of the client page registry module. Writes nothing.\r\n *\r\n * Zero pages is a legal result, not an error, and it is emitted as an explicit\r\n * empty registry: \"built with web, no pages\" must reach the runtime as a fact\r\n * rather than as a missing module.\r\n */\r\nexport function generateClientRegistry(options: GenerateClientRegistryOptions): string {\r\n const { pages, toImportSpecifier } = options;\r\n const routablePages = pages.filter(isDiscoveredRoutablePage);\r\n const errorPageFile = pages.find((page) => page.type === \"error\")?.pageFile;\r\n\r\n assertUniqueNames(routablePages);\r\n\r\n const declaration = `export const ${CLIENT_REGISTRY_EXPORT_NAME}: readonly ClientPageEntry[] =`;\r\n\r\n if (routablePages.length === 0) {\r\n return [...HEADER, \"\", `${declaration} [];`, \"\"].join(\"\\n\");\r\n }\r\n\r\n return [\r\n ...HEADER,\r\n \"\",\r\n `${declaration} [`,\r\n ...routablePages.flatMap((page) => entrySource(page, errorPageFile, toImportSpecifier)),\r\n \"];\",\r\n \"\",\r\n ].join(\"\\n\");\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,MAAa,8BAA8B;;AAG3C,MAAM,2BAA2B;;;;;;;;;AAUjC,IAAa,+BAAb,cAAkD,MAAM;CAEpC;CACA;CACA;CAHlB,AAAO,YACL,AAAgB,WAChB,AAAgB,eAChB,AAAgB,gBAChB;EACA,MACE,0CAA0C,UAAU,MAAM,cAAc,SAClE,eAAe,oNAGvB;EATgB;EACA;EACA;EAQhB,KAAK,OAAO;CACd;AACF;;;;;;;AAQA,SAAS,MAAM,OAAuB;CACpC,OAAO,KAAK,UAAU,KAAK;AAC7B;AAEA,MAAM,SAAS;CACb;CACA;CACA;CACA;CACA;CACA;CACA,wCAAwC,MAAM,wBAAwB,EAAE;AAC1E;AAEA,SAAS,kBAAkB,OAAgD;CACzE,MAAM,iCAAiB,IAAI,IAAoB;CAE/C,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,WAAW,eAAe,IAAI,KAAK,SAAS;EAElD,IAAI,aAAa,QACf,MAAM,IAAI,6BAA6B,KAAK,WAAW,UAAU,KAAK,QAAQ;EAGhF,eAAe,IAAI,KAAK,WAAW,KAAK,QAAQ;CAClD;AACF;;;;;;;;;;AAWA,SAAS,WACP,MACA,eACA,mBACU;CAIV,MAAM,WAAW,CAAC,YAAY;CAC9B,MAAM,aAAa,CAAC,kBAAkB,KAAK,QAAQ,CAAC;CAWpD,MAAM,iBAAiB,KAAK,QAAQ,KAAK,YAAY,UAAU;EAC7D,SAAS,KAAK,eAAe,OAAO;EACpC,WAAW,KAAK,kBAAkB,UAAU,CAAC;EAE7C,OAAO,eAAe;CACxB,CAAC;CAED,IAAI,KAAK,YAAY,QAAW;EAC9B,SAAS,KAAK,WAAW;EACzB,WAAW,KAAK,kBAAkB,KAAK,OAAO,CAAC;CACjD;CAEA,IAAI,kBAAkB,QAAW;EAC/B,SAAS,KAAK,iBAAiB;EAC/B,WAAW,KAAK,kBAAkB,aAAa,CAAC;CAClD;CAIA,MAAM,MAAM,KAAK,YAAY,SAAY,KAAK;CAC9C,MAAM,YAAY,kBAAkB,SAAY,KAAK;CAErD,OAAO;EACL;EACA,gBAAgB,SAAS,KAAK,IAAI,EAAE;EACpC,GAAG,WAAW,KAAK,cAAc,kBAAkB,MAAM,SAAS,EAAE,GAAG;EACvE;EACA;EACA,8CAA8C,eAAe,KAAK,IAAI,EAAE,GAAG,MAAM,UAAU;EAC3F;CACF;AACF;AAEA,SAAS,YACP,MACA,eACA,mBACU;CACV,OAAO;EACL;EACA;EAKA,aAAa,MAAM,KAAK,SAAS,EAAE;EACnC,aAAa,MAAM,KAAK,SAAS,EAAE;EACnC,GAAG,WAAW,MAAM,eAAe,iBAAiB;EACpD;CACF;AACF;;;;;;;;AASA,SAAgB,uBAAuB,SAAgD;CACrF,MAAM,EAAE,OAAO,sBAAsB;CACrC,MAAM,gBAAgB,MAAM,OAAO,wBAAwB;CAC3D,MAAM,gBAAgB,MAAM,MAAM,SAAS,KAAK,SAAS,OAAO,GAAG;CAEnE,kBAAkB,aAAa;CAE/B,MAAM,cAAc,gBAAgB,4BAA4B;CAEhE,IAAI,cAAc,WAAW,GAC3B,OAAO;EAAC,GAAG;EAAQ;EAAI,GAAG,YAAY;EAAO;CAAE,EAAE,KAAK,IAAI;CAG5D,OAAO;EACL,GAAG;EACH;EACA,GAAG,YAAY;EACf,GAAG,cAAc,SAAS,SAAS,YAAY,MAAM,eAAe,iBAAiB,CAAC;EACtF;EACA;CACF,EAAE,KAAK,IAAI;AACb"}
@@ -1,10 +1,12 @@
1
1
  import { NestedLayoutsNotSupportedError } from "../routing/layout-policy.mjs";
2
- import { DuplicatePageRouteNameError, discoverPages, discoverWebRoots, isFile, layoutChainFor, toPosix, walkFiles } from "./discover-pages.mjs";
3
- import path from "node:path";
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";
4
5
  import fs from "node:fs";
6
+ import path from "node:path";
5
7
  import { parse } from "@babel/parser";
6
8
 
7
- //#region ../web/src/build/generate-pages-barrel.ts
9
+ //#region web/src/build/generate-pages-barrel.ts
8
10
  /**
9
11
  * The server half of the build→runtime handoff: the generated
10
12
  * `<productionDir>/pages.ts` barrel that carries the page graph into the
@@ -71,7 +73,7 @@ const WEB_ESBUILD_PATCH = {
71
73
  };
72
74
  /** Line appended to section 4 of the generated production entry, after `./routes`. */
73
75
  const WEB_ENTRY_IMPORT = "await import(\"./pages\");";
74
- const REMEDY = "remove the import from this file (move it into a client-only module, or reference the asset by URL instead), or wait for the Vite-based server build, which is the only build that can compile imports like this one";
76
+ const REMEDY = "move the static file into the application's public/ directory and reference it by its root URL instead (for example, public/logo.svg becomes /logo.svg). The production server build does not compile static-asset imports, ?raw/?url queries, or import.meta.url in the page graph in 5.2";
75
77
  const ASSET_EXTENSIONS = [
76
78
  ".svg",
77
79
  ".png",
@@ -233,6 +235,21 @@ const EMPTY_BARREL_CONTENTS = [
233
235
  "providePageManifest({ pages: [] });",
234
236
  ""
235
237
  ].join("\n");
238
+ function emptyPageBarrelContents(clientDir, publicFiles) {
239
+ if (publicFiles.length === 0) return EMPTY_BARREL_CONTENTS;
240
+ return [
241
+ ...BARREL_HEADER,
242
+ "",
243
+ "// Zero pages were discovered, but app-owned public files still belong to",
244
+ "// the production browser surface and are served from the copied client tree.",
245
+ "providePageManifest({",
246
+ ` clientDir: ${quote(clientDir)},`,
247
+ ` publicFiles: ${JSON.stringify([...publicFiles])},`,
248
+ " pages: [],",
249
+ "});",
250
+ ""
251
+ ].join("\n");
252
+ }
236
253
  async function writeBarrel(productionDir, contents) {
237
254
  const barrelFile = path.join(productionDir, "pages.ts");
238
255
  await fs.promises.mkdir(productionDir, { recursive: true });
@@ -251,27 +268,48 @@ async function writeBarrel(productionDir, contents) {
251
268
  * hydrate.
252
269
  */
253
270
  async function generatePagesBarrel(options) {
254
- const { appRoot, productionDir, clientDir } = options;
271
+ const { appRoot, productionDir, clientDir, publicFiles = [] } = options;
255
272
  const srcRoot = path.join(appRoot, options.srcDir ?? "src");
256
273
  const webRoots = discoverWebRoots(srcRoot);
257
274
  const discovered = discoverPages({
258
275
  appRoot,
259
276
  srcDir: options.srcDir
260
277
  });
278
+ const routablePages = discovered.filter(isDiscoveredRoutablePage);
279
+ const errorPage = discovered.find((page) => page.type === "error");
261
280
  if (discovered.length === 0) {
262
281
  console.log("web configured, 0 pages");
282
+ const contents = emptyPageBarrelContents(clientDir, publicFiles);
263
283
  return {
264
284
  pageCount: 0,
265
- barrelFile: await writeBarrel(productionDir, EMPTY_BARREL_CONTENTS),
266
- contents: EMPTY_BARREL_CONTENTS
285
+ barrelFile: await writeBarrel(productionDir, contents),
286
+ contents,
287
+ pageRoutes: {
288
+ version: 1,
289
+ routes: []
290
+ }
267
291
  };
268
292
  }
269
293
  assertNoViteOnlyImports(webRoots, appRoot);
270
294
  const appFile = path.join(srcRoot, "web", "root.tsx");
271
- if (!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.`);
295
+ 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.`);
272
296
  const layoutIdentifiers = /* @__PURE__ */ new Map();
273
297
  const pages = [];
274
- for (const [index, page] of discovered.entries()) {
298
+ const pageRoutes = {
299
+ version: 1,
300
+ routes: [...routablePages.filter((page) => !isNotFoundPageFile(page.pageFile)).map((page) => ({
301
+ method: "GET",
302
+ path: normalizeRoutePath(page.routePath),
303
+ name: page.routeName,
304
+ source: toPosix(path.relative(appRoot, page.pageFile))
305
+ })), {
306
+ method: "GET",
307
+ path: normalizeRoutePath("*"),
308
+ 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))
310
+ }]
311
+ };
312
+ for (const [index, page] of routablePages.entries()) {
275
313
  const layouts = page.layouts.map((layoutFile) => {
276
314
  let identifier = layoutIdentifiers.get(layoutFile);
277
315
  if (identifier === void 0) {
@@ -290,7 +328,8 @@ async function generatePagesBarrel(options) {
290
328
  });
291
329
  }
292
330
  const importLines = [
293
- `import * as app from ${quote(importSpecifierFor(productionDir, appFile))};`,
331
+ ...routablePages.length === 0 ? [] : [`import * as app from ${quote(importSpecifierFor(productionDir, appFile))};`],
332
+ ...errorPage === void 0 ? [] : [`import * as errorPage from ${quote(importSpecifierFor(productionDir, errorPage.pageFile))};`],
294
333
  ...[...layoutIdentifiers.entries()].map(([layoutFile, identifier]) => `import * as ${identifier} from ${quote(importSpecifierFor(productionDir, layoutFile))};`),
295
334
  ...pages.map((page) => `import * as ${page.identifier} from ${quote(importSpecifierFor(productionDir, page.sourceFile))};`)
296
335
  ];
@@ -310,8 +349,10 @@ async function generatePagesBarrel(options) {
310
349
  ...importLines,
311
350
  "",
312
351
  "providePageManifest({",
313
- ` clientDir: ${quote(clientDir)},`,
314
- ` app: { module: app, sourceFile: ${relativeToApp(appFile)} },`,
352
+ ...routablePages.length === 0 && publicFiles.length === 0 ? [] : [` clientDir: ${quote(clientDir)},`],
353
+ ...publicFiles.length === 0 ? [] : [` publicFiles: ${JSON.stringify([...publicFiles])},`],
354
+ ...routablePages.length === 0 ? [] : [` app: { module: app, sourceFile: ${relativeToApp(appFile)} },`],
355
+ ...errorPage === void 0 ? [] : [` errorPage: { module: errorPage, sourceFile: ${relativeToApp(errorPage.pageFile)} },`],
315
356
  " pages: [",
316
357
  ...pageEntries.map((entry) => entry.split("\n").map((line) => ` ${line}`).join("\n")),
317
358
  " ],",
@@ -321,7 +362,8 @@ async function generatePagesBarrel(options) {
321
362
  return {
322
363
  pageCount: pages.length,
323
364
  barrelFile: await writeBarrel(productionDir, contents),
324
- contents
365
+ contents,
366
+ pageRoutes
325
367
  };
326
368
  }
327
369
 
@@ -1 +1 @@
1
- {"version":3,"file":"generate-pages-barrel.mjs","names":[],"sources":["../../../../../../../web/src/build/generate-pages-barrel.ts"],"sourcesContent":["/**\n * The server half of the build→runtime handoff: the generated\n * `<productionDir>/pages.ts` barrel that carries the page graph into the\n * server bundle.\n *\n * The graph itself comes from `discover-pages.ts`, the one scan every provider\n * shares — this module is the BARREL provider and nothing more. It takes the\n * recipe, runs the Vite-switch tripwire over the sources the recipe came from,\n * and writes the file.\n *\n * \"Static\" means the FILESYSTEM ONLY — discovery globs `*.page.tsx`,\n * `layout.tsx` and `root.tsx` and never imports a single application module,\n * mirroring how the builder already globs (`production-builder.ts:184-242`).\n * The imports happen at BOOT, when the compiled barrel runs, which is what\n * makes a page that throws on import a loud pre-listen failure in production.\n *\n * This is the HEAVY half of the contribution: `contribution.ts` reaches it by\n * dynamic import inside its `generate` hook, so nothing here lands in a\n * connector's static graph.\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { parse } from \"@babel/parser\";\nimport type { ConnectorEsbuildPatch } from \"@warlock.js/core\";\nimport { discoverPages, discoverWebRoots, isFile, toPosix, walkFiles } from \"./discover-pages\";\n\n// Re-exported, not redefined: discovery's helpers were this module's before the\n// split, and the callers that already reach for them here should keep getting\n// the one implementation rather than a copy that can drift from it. The nested\n// layout error re-exports from the layout policy module, the one place that\n// owns layout selection and its error contract.\nexport {\n discoverPages,\n discoverWebRoots,\n DuplicatePageRouteNameError,\n layoutChainFor,\n} from \"./discover-pages\";\nexport { NestedLayoutsNotSupportedError } from \"../routing/layout-policy\";\nexport type { DiscoveredPage, DiscoverPagesOptions } from \"./discover-pages\";\n\n/**\n * Declared BEFORE the patch that reads it. `const` is hoisted but sits in the\n * temporal dead zone, and the patch below is an object literal evaluated at\n * module load — reading this from further down the file would throw.\n */\nconst STYLE_EXTENSIONS = [\".css\", \".scss\", \".sass\", \".less\", \".styl\"];\n\n/**\n * The esbuild patch web contributes (spike-settled, contract §3a).\n *\n * NO `process.env.NODE_ENV` define: warlock assigns to `process.env.NODE_ENV`\n * at runtime (`core/src/utils/environment.ts:14`) and a define would turn that\n * assignment into an assignment to a literal.\n */\nexport const WEB_ESBUILD_PATCH: ConnectorEsbuildPatch = {\n jsx: \"automatic\",\n jsxImportSource: \"react\",\n define: {\n \"import.meta.env.DEV\": \"false\",\n \"import.meta.env.PROD\": \"true\",\n \"import.meta.env.SSR\": \"true\",\n \"import.meta.env.MODE\": '\"production\"',\n },\n /**\n * Stylesheets compile to NOTHING in the server bundle, rather than failing it.\n *\n * `root.tsx` importing `./app.css` is how Tailwind — and every other CSS\n * pipeline — is normally wired, so rejecting it meant the framework could not\n * build a styled application at all. But the server has no use for the bytes:\n * it renders HTML, and all it ever needs of a stylesheet is a URL to put in a\n * `<link>`. The CSS itself belongs to the CLIENT bundle, which Vite builds\n * from the same `root.tsx` and which handles CSS natively.\n *\n * So the import stays in the user's source, the server drops it, and Vite\n * emits the real asset. Nothing is silently lost: a stylesheet that does not\n * exist still fails the client build, loudly, where it matters.\n */\n loader: Object.fromEntries(\n STYLE_EXTENSIONS.map((extension) => [extension, \"empty\" as const]),\n ) as ConnectorEsbuildPatch[\"loader\"],\n};\n\n/** Line appended to section 4 of the generated production entry, after `./routes`. */\nexport const WEB_ENTRY_IMPORT = 'await import(\"./pages\");';\n\nconst REMEDY =\n \"remove the import from this file (move it into a client-only module, or \" +\n \"reference the asset by URL instead), or wait for the Vite-based server \" +\n \"build, which is the only build that can compile imports like this one\";\n\nconst ASSET_EXTENSIONS = [\n \".svg\",\n \".png\",\n \".jpg\",\n \".jpeg\",\n \".gif\",\n \".webp\",\n \".avif\",\n \".ico\",\n \".woff\",\n \".woff2\",\n \".ttf\",\n \".eot\",\n \".mp4\",\n \".webm\",\n];\n\nexport type GeneratePagesBarrelOptions = {\n /** Absolute path to the application root (where `package.json` lives). */\n appRoot: string;\n /** Absolute path to `.warlock/production` — where the barrel is written. */\n productionDir: string;\n /** Source directory name under `appRoot`; defaults to `\"src\"`. */\n srcDir?: string;\n /**\n * App-root-relative POSIX path of the client bundle directory\n * (`\"dist/client\"`), baked into the manifest for the runtime to read back.\n * See `PageManifest.clientDir` for why it cannot be derived at boot.\n *\n * The CALLER resolves it, from the same `context.options.outdir` the client\n * build itself writes to, so the two cannot name different directories.\n */\n clientDir: string;\n};\n\nexport type GeneratePagesBarrelResult = {\n /** How many `*.page.tsx` files discovery found across both web roots. */\n pageCount: number;\n /** Absolute path of the written barrel. Always written when this runs at all. */\n barrelFile: string;\n /** The written barrel source. */\n contents: string;\n};\n\n/** Raised by the Vite-switch tripwire. */\nexport class WebPageGraphUnsupportedImportError extends Error {\n public constructor(\n public readonly sourceFile: string,\n public readonly specifier: string,\n reason: string,\n ) {\n super(\n `Cannot build the page graph: \"${sourceFile}\" ${reason} (${specifier}). ` +\n \"Pages are compiled into the server bundle, which supports plain code imports only — \" +\n \"static assets, `?raw`/`?url` queries and `import.meta.url` are not supported there. \" +\n \"(Stylesheets ARE supported: the server bundle drops them and the client bundle emits them.) \" +\n `To fix: ${REMEDY}.`,\n );\n this.name = \"WebPageGraphUnsupportedImportError\";\n }\n}\n\nconst FROM_SPECIFIER = /\\bfrom\\s*[\"']([^\"']+)[\"']/g;\nconst BARE_IMPORT = /\\bimport\\s*[\"']([^\"']+)[\"']/g;\nconst DYNAMIC_IMPORT = /\\bimport\\s*\\(\\s*[\"']([^\"']+)[\"']\\s*\\)/g;\n\n/**\n * Blank every comment, preserving byte offsets.\n *\n * The scan below is regex over source text, so without this a specifier written\n * inside a comment is indistinguishable from a real import. That is not\n * hypothetical: documenting this very restriction — writing `import \"./app.css\"`\n * in a doc comment to explain why it is unsupported — failed the build, and the\n * error then insisted the file imported a stylesheet that it did not import.\n * Removing the real import did not help, because the comment was the match.\n *\n * Comments are replaced with spaces rather than deleted so every subsequent\n * offset, and therefore every line number in anything reported from here,\n * stays exactly where it was.\n *\n * A parse failure is deliberately NOT fatal: this function's job is to find\n * hazards, not to validate syntax. esbuild reports a genuine syntax error far\n * better than we would, and failing here would replace its clear message with a\n * worse one. On a parse failure the raw source is scanned — the pre-existing\n * behaviour, false positives and all.\n */\nfunction withoutComments(source: string): string {\n try {\n const ast = parse(source, {\n sourceType: \"module\",\n errorRecovery: true,\n plugins: [\"typescript\", \"jsx\", \"decorators-legacy\"],\n });\n\n let stripped = source;\n\n for (const comment of ast.comments ?? []) {\n const { start, end } = comment as { start: number; end: number };\n\n stripped =\n stripped.slice(0, start) + \" \".repeat(end - start) + stripped.slice(end);\n }\n\n return stripped;\n } catch {\n return source;\n }\n}\n\n/** Expects source whose comments have already been blanked by {@link withoutComments}. */\nfunction collectSpecifiers(source: string): string[] {\n const specifiers: string[] = [];\n\n for (const pattern of [FROM_SPECIFIER, BARE_IMPORT, DYNAMIC_IMPORT]) {\n pattern.lastIndex = 0;\n\n let match = pattern.exec(source);\n\n while (match !== null) {\n specifiers.push(match[1]);\n match = pattern.exec(source);\n }\n }\n\n return specifiers;\n}\n\nfunction hazardFor(specifier: string): string | undefined {\n const [base, query] = specifier.split(\"?\", 2);\n const lowered = base.toLowerCase();\n\n /*\n STYLESHEETS ARE NOT A HAZARD — see `loader` in WEB_ESBUILD_PATCH above.\n\n They used to be rejected here, which meant `import \"./app.css\"` in\n `root.tsx` — the ordinary way to wire Tailwind or any other CSS pipeline —\n failed the build outright. The server bundle now compiles them to nothing,\n because the server only ever needs a stylesheet's URL, and the client bundle\n that Vite builds from the same sources emits the real asset.\n\n STYLE_EXTENSIONS is still the single list both halves read from, so the set\n esbuild stubs and the set this check tolerates cannot drift apart.\n */\n\n if (ASSET_EXTENSIONS.some((extension) => lowered.endsWith(extension))) {\n return \"imports a static asset\";\n }\n\n if (query !== undefined && /(^|&)(raw|url)(&|=|$)/.test(query)) {\n return \"uses a bundler-specific import query\";\n }\n\n return undefined;\n}\n\n/**\n * The Vite-switch tripwire. Fails the build\n * the moment the page graph needs a capability the esbuild server build does\n * not have, naming the file, the specifier and the remedy.\n *\n * LIMITATIONS, stated on purpose — BOTH directions, because stating only one\n * is what made the other one expensive to find:\n *\n * FALSE NEGATIVES. This is a FILE-LEVEL scan of the `.ts`/`.tsx` sources under\n * the discovered web roots — it is not transitive resolution. A page that\n * imports a workspace package which itself imports CSS is not caught here; that\n * failure surfaces at esbuild time instead.\n *\n * FALSE POSITIVES. The scan is regex over source text, so anything that LOOKS\n * like a specifier counts. Comments are blanked first ({@link withoutComments})\n * because a documented example used to fail the build while insisting on an\n * import that was not there. String and template literals are NOT blanked —\n * they cannot be, since real specifiers are string literals — so a specifier\n * inside a template literal (a code sample in a docs page, say) still matches.\n * Rare, and it fails loudly rather than silently, but it is not impossible.\n */\nexport function assertNoViteOnlyImports(webRoots: readonly string[], appRoot: string): void {\n for (const webRoot of webRoots) {\n const sources = walkFiles(\n webRoot,\n (fileName) => fileName.endsWith(\".ts\") || fileName.endsWith(\".tsx\"),\n );\n\n for (const sourceFile of sources) {\n // Comments stripped ONCE, and reused by both checks below: `import.meta.url`\n // was as blind to comments as the specifier scan was, and mentioning it in\n // prose — as this very file does — would have failed the build.\n const source = withoutComments(fs.readFileSync(sourceFile, \"utf-8\"));\n const relative = toPosix(path.relative(appRoot, sourceFile));\n\n for (const specifier of collectSpecifiers(source)) {\n const reason = hazardFor(specifier);\n\n if (reason !== undefined) {\n throw new WebPageGraphUnsupportedImportError(relative, specifier, reason);\n }\n }\n\n if (source.includes(\"import.meta.url\")) {\n throw new WebPageGraphUnsupportedImportError(\n relative,\n \"import.meta.url\",\n \"uses `import.meta.url`, whose meaning changes under the bundled server build\",\n );\n }\n }\n }\n}\n\n/** `<productionDir>` -> `<file>` as an extensionless, POSIX, relative specifier. */\nfunction importSpecifierFor(productionDir: string, file: string): string {\n const relative = toPosix(path.relative(productionDir, file)).replace(/\\.tsx?$/, \"\");\n\n return relative.startsWith(\".\") ? relative : `./${relative}`;\n}\n\nfunction quote(value: string): string {\n return JSON.stringify(value);\n}\n\ninterface BarrelPage {\n identifier: string;\n sourceFile: string;\n layouts: { identifier: string; sourceFile: string }[];\n}\n\n/** Header of every generated barrel — the banner plus the one runtime import. */\nconst BARREL_HEADER = [\n \"// AUTO-GENERATED by @warlock.js/web — do not edit.\",\n \"// The page graph, compiled into the server bundle.\",\n \"// Route paths are deliberately absent: they are read off each module at boot.\",\n // The CONNECTOR subpath, never the root barrel: this file is compiled into\n // the server bundle, and `\"@warlock.js/web\"` would drag React in with it.\n 'import { providePageManifest } from \"@warlock.js/web/connector\";',\n];\n\n/**\n * The barrel written when discovery found no pages at all.\n *\n * No app entry: `root.tsx` is the root every PAGE renders inside, so with zero\n * pages there is nothing for it to wrap and requiring it would fail builds that\n * are legitimately page-free.\n */\nconst EMPTY_BARREL_CONTENTS = [\n ...BARREL_HEADER,\n \"\",\n \"// Zero pages were discovered. The empty table is still provided, because that\",\n \"// is what tells the boot-time reader \\\"this bundle WAS built with web, it just\",\n '// has no pages\" — as opposed to a bundle built without web at all, where no',\n \"// barrel runs and the manifest stays absent.\",\n \"providePageManifest({ pages: [] });\",\n \"\",\n].join(\"\\n\");\n\nasync function writeBarrel(productionDir: string, contents: string): Promise<string> {\n const barrelFile = path.join(productionDir, \"pages.ts\");\n\n await fs.promises.mkdir(productionDir, { recursive: true });\n await fs.promises.writeFile(barrelFile, contents, \"utf-8\");\n\n return barrelFile;\n}\n\n/**\n * Discovers the page graph, runs the tripwire, and writes\n * `<productionDir>/pages.ts`.\n *\n * Zero pages is NOT an error, and it is NOT silence either: the barrel is\n * written anyway carrying an empty table, so \"configured with web, no pages\"\n * reaches the runtime as a fact instead of looking identical to \"never built\n * with web\". The run also says so out loud (no silent caps). What the caller\n * skips on zero pages is the CLIENT bundle — no pages means nothing to\n * hydrate.\n */\nexport async function generatePagesBarrel(\n options: GeneratePagesBarrelOptions,\n): Promise<GeneratePagesBarrelResult> {\n const { appRoot, productionDir, clientDir } = options;\n const srcRoot = path.join(appRoot, options.srcDir ?? \"src\");\n const webRoots = discoverWebRoots(srcRoot);\n const discovered = discoverPages({ appRoot, srcDir: options.srcDir });\n\n if (discovered.length === 0) {\n console.log(\"web configured, 0 pages\");\n\n return {\n pageCount: 0,\n barrelFile: await writeBarrel(productionDir, EMPTY_BARREL_CONTENTS),\n contents: EMPTY_BARREL_CONTENTS,\n };\n }\n\n assertNoViteOnlyImports(webRoots, appRoot);\n\n const appFile = path.join(srcRoot, \"web\", \"root.tsx\");\n\n if (!isFile(appFile)) {\n throw new Error(\n `Cannot generate the page barrel: ${discovered.length} page(s) were discovered but the application root ` +\n `component \"${toPosix(path.relative(appRoot, appFile))}\" does not exist. Every page renders inside it.`,\n );\n }\n\n const layoutIdentifiers = new Map<string, string>();\n const pages: BarrelPage[] = [];\n\n for (const [index, page] of discovered.entries()) {\n const layouts = page.layouts.map((layoutFile) => {\n let identifier = layoutIdentifiers.get(layoutFile);\n\n if (identifier === undefined) {\n identifier = `l${layoutIdentifiers.size}`;\n layoutIdentifiers.set(layoutFile, identifier);\n }\n\n return { identifier, sourceFile: layoutFile };\n });\n\n pages.push({ identifier: `p${index}`, sourceFile: page.pageFile, layouts });\n }\n\n const importLines = [\n `import * as app from ${quote(importSpecifierFor(productionDir, appFile))};`,\n ...[...layoutIdentifiers.entries()].map(\n ([layoutFile, identifier]) =>\n `import * as ${identifier} from ${quote(importSpecifierFor(productionDir, layoutFile))};`,\n ),\n ...pages.map(\n (page) =>\n `import * as ${page.identifier} from ${quote(importSpecifierFor(productionDir, page.sourceFile))};`,\n ),\n ];\n\n const relativeToApp = (file: string) => quote(toPosix(path.relative(appRoot, file)));\n\n const pageEntries = pages.map((page) => {\n const layouts = page.layouts\n .map((layout) => `{ module: ${layout.identifier}, sourceFile: ${relativeToApp(layout.sourceFile)} }`)\n .join(\", \");\n\n return [\n \" {\",\n ` module: ${page.identifier},`,\n ` sourceFile: ${relativeToApp(page.sourceFile)},`,\n ` layouts: [${layouts}],`,\n \" },\",\n ].join(\"\\n\");\n });\n\n const contents = [\n ...BARREL_HEADER,\n ...importLines,\n \"\",\n \"providePageManifest({\",\n ` clientDir: ${quote(clientDir)},`,\n ` app: { module: app, sourceFile: ${relativeToApp(appFile)} },`,\n \" pages: [\",\n ...pageEntries.map((entry) =>\n entry\n .split(\"\\n\")\n .map((line) => ` ${line}`)\n .join(\"\\n\"),\n ),\n \" ],\",\n \"});\",\n \"\",\n ].join(\"\\n\");\n\n return {\n pageCount: pages.length,\n barrelFile: await writeBarrel(productionDir, contents),\n contents,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"generate-pages-barrel.mjs","names":[],"sources":["C:/Work/Warlock.js/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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"}
@@ -0,0 +1,48 @@
1
+ import { parse } from "@babel/parser";
2
+
3
+ //#region web/src/build/page-default-export.ts
4
+ /** Raised when a `*.page.tsx` module cannot render a page component. */
5
+ var MissingPageDefaultExportError = class extends Error {
6
+ pageFile;
7
+ constructor(pageFile) {
8
+ super(`The page "${pageFile}" has no runtime default export. Every \`*.page.tsx\` file must default-export the React component it renders. For example: \`export default function Page() { return <main />; }\``);
9
+ this.pageFile = pageFile;
10
+ this.name = "MissingPageDefaultExportError";
11
+ }
12
+ };
13
+ /**
14
+ * Refuses a page that only has named or type exports.
15
+ *
16
+ * This is static on purpose: discovery runs in both the dev registry and the
17
+ * production barrel generator, before either consumer imports application
18
+ * code. Re-exporting a runtime binding as `default` is valid ES module syntax
19
+ * and therefore satisfies the same contract as `export default`.
20
+ */
21
+ function assertPageHasDefaultExport(pageFile, source) {
22
+ let program;
23
+ try {
24
+ program = parse(source, {
25
+ sourceType: "module",
26
+ plugins: ["typescript", "jsx"],
27
+ errorRecovery: false
28
+ }).program;
29
+ } catch (error) {
30
+ throw new Error(`Cannot inspect the default export of "${pageFile}": the file could not be parsed (${error.message}). Fix the syntax error and discovery will continue.`);
31
+ }
32
+ for (const statement of program.body) {
33
+ if (statement.type === "ExportDefaultDeclaration") {
34
+ if (statement.declaration.type !== "TSInterfaceDeclaration") return;
35
+ continue;
36
+ }
37
+ if (statement.type !== "ExportNamedDeclaration" || statement.exportKind === "type") continue;
38
+ for (const specifier of statement.specifiers) {
39
+ if (!("exported" in specifier) || specifier.type === "ExportSpecifier" && specifier.exportKind === "type") continue;
40
+ if ((specifier.exported.type === "Identifier" ? specifier.exported.name : specifier.exported.value) === "default") return;
41
+ }
42
+ }
43
+ throw new MissingPageDefaultExportError(pageFile);
44
+ }
45
+
46
+ //#endregion
47
+ export { assertPageHasDefaultExport };
48
+ //# sourceMappingURL=page-default-export.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-default-export.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/build/page-default-export.ts"],"sourcesContent":["import { parse } from \"@babel/parser\";\n\n/** Raised when a `*.page.tsx` module cannot render a page component. */\nexport class MissingPageDefaultExportError extends Error {\n public constructor(public readonly pageFile: string) {\n super(\n `The page \"${pageFile}\" has no runtime default export. Every \\`*.page.tsx\\` file must ` +\n \"default-export the React component it renders. For example: \" +\n \"`export default function Page() { return <main />; }`\",\n );\n this.name = \"MissingPageDefaultExportError\";\n }\n}\n\n/**\n * Refuses a page that only has named or type exports.\n *\n * This is static on purpose: discovery runs in both the dev registry and the\n * production barrel generator, before either consumer imports application\n * code. Re-exporting a runtime binding as `default` is valid ES module syntax\n * and therefore satisfies the same contract as `export default`.\n */\nexport function assertPageHasDefaultExport(pageFile: string, source: string): void {\n let program: ReturnType<typeof parse>[\"program\"];\n\n try {\n program = parse(source, {\n sourceType: \"module\",\n plugins: [\"typescript\", \"jsx\"],\n errorRecovery: false,\n }).program;\n } catch (error) {\n throw new Error(\n `Cannot inspect the default export of \"${pageFile}\": the file could not be parsed ` +\n `(${(error as Error).message}). Fix the syntax error and discovery will continue.`,\n );\n }\n\n for (const statement of program.body) {\n if (statement.type === \"ExportDefaultDeclaration\") {\n // `export default interface Page {}` is erased by TypeScript and leaves\n // no runtime component for either SSR or hydration to render.\n if ((statement.declaration as { type: string }).type !== \"TSInterfaceDeclaration\") return;\n continue;\n }\n\n if (statement.type !== \"ExportNamedDeclaration\" || statement.exportKind === \"type\") continue;\n\n for (const specifier of statement.specifiers) {\n if (\n !(\"exported\" in specifier) ||\n (specifier.type === \"ExportSpecifier\" && specifier.exportKind === \"type\")\n ) {\n continue;\n }\n\n const exported =\n specifier.exported.type === \"Identifier\"\n ? specifier.exported.name\n : specifier.exported.value;\n\n if (exported === \"default\") return;\n }\n }\n\n throw new MissingPageDefaultExportError(pageFile);\n}\n"],"mappings":";;;;AAGA,IAAa,gCAAb,cAAmD,MAAM;CACpB;CAAnC,AAAO,YAAY,AAAgB,UAAkB;EACnD,MACE,aAAa,SAAS,oLAGxB;EALiC;EAMjC,KAAK,OAAO;CACd;AACF;;;;;;;;;AAUA,SAAgB,2BAA2B,UAAkB,QAAsB;CACjF,IAAI;CAEJ,IAAI;EACF,UAAU,MAAM,QAAQ;GACtB,YAAY;GACZ,SAAS,CAAC,cAAc,KAAK;GAC7B,eAAe;EACjB,CAAC,EAAE;CACL,SAAS,OAAO;EACd,MAAM,IAAI,MACR,yCAAyC,SAAS,mCAC3C,MAAgB,QAAQ,qDACjC;CACF;CAEA,KAAK,MAAM,aAAa,QAAQ,MAAM;EACpC,IAAI,UAAU,SAAS,4BAA4B;GAGjD,IAAK,UAAU,YAAiC,SAAS,0BAA0B;GACnF;EACF;EAEA,IAAI,UAAU,SAAS,4BAA4B,UAAU,eAAe,QAAQ;EAEpF,KAAK,MAAM,aAAa,UAAU,YAAY;GAC5C,IACE,EAAE,cAAc,cACf,UAAU,SAAS,qBAAqB,UAAU,eAAe,QAElE;GAQF,KAJE,UAAU,SAAS,SAAS,eACxB,UAAU,SAAS,OACnB,UAAU,SAAS,WAER,WAAW;EAC9B;CACF;CAEA,MAAM,IAAI,8BAA8B,QAAQ;AAClD"}
@@ -0,0 +1,21 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ //#region web/src/build/page-routes-manifest.ts
5
+ const PAGE_ROUTES_MANIFEST_FILE = "page-routes.manifest.json";
6
+ /** Atomically replace the snapshot only after the rest of the build succeeds. */
7
+ async function writePageRoutesManifest(outdir, manifest) {
8
+ const target = path.join(outdir, PAGE_ROUTES_MANIFEST_FILE);
9
+ const temporary = path.join(outdir, `.${PAGE_ROUTES_MANIFEST_FILE}.${process.pid}.${Date.now()}.tmp`);
10
+ await fs.promises.mkdir(outdir, { recursive: true });
11
+ try {
12
+ await fs.promises.writeFile(temporary, `${JSON.stringify(manifest, null, 2)}\n`, "utf-8");
13
+ await fs.promises.rename(temporary, target);
14
+ } finally {
15
+ await fs.promises.unlink(temporary).catch(() => {});
16
+ }
17
+ }
18
+
19
+ //#endregion
20
+ export { writePageRoutesManifest };
21
+ //# sourceMappingURL=page-routes-manifest.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-routes-manifest.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/build/page-routes-manifest.ts"],"sourcesContent":["import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport type { PageRoutesManifest } from \"./generate-pages-barrel\";\r\n\r\nexport const PAGE_ROUTES_MANIFEST_FILE = \"page-routes.manifest.json\";\r\n\r\n/** Atomically replace the snapshot only after the rest of the build succeeds. */\r\nexport async function writePageRoutesManifest(\r\n outdir: string,\r\n manifest: PageRoutesManifest,\r\n): Promise<void> {\r\n const target = path.join(outdir, PAGE_ROUTES_MANIFEST_FILE);\r\n const temporary = path.join(\r\n outdir,\r\n `.${PAGE_ROUTES_MANIFEST_FILE}.${process.pid}.${Date.now()}.tmp`,\r\n );\r\n\r\n await fs.promises.mkdir(outdir, { recursive: true });\r\n\r\n try {\r\n await fs.promises.writeFile(temporary, `${JSON.stringify(manifest, null, 2)}\\n`, \"utf-8\");\r\n await fs.promises.rename(temporary, target);\r\n } finally {\r\n await fs.promises.unlink(temporary).catch(() => {});\r\n }\r\n}\r\n"],"mappings":";;;;AAIA,MAAa,4BAA4B;;AAGzC,eAAsB,wBACpB,QACA,UACe;CACf,MAAM,SAAS,KAAK,KAAK,QAAQ,yBAAyB;CAC1D,MAAM,YAAY,KAAK,KACrB,QACA,IAAI,0BAA0B,GAAG,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,KAC7D;CAEA,MAAM,GAAG,SAAS,MAAM,QAAQ,EAAE,WAAW,KAAK,CAAC;CAEnD,IAAI;EACF,MAAM,GAAG,SAAS,UAAU,WAAW,GAAG,KAAK,UAAU,UAAU,MAAM,CAAC,EAAE,KAAK,OAAO;EACxF,MAAM,GAAG,SAAS,OAAO,WAAW,MAAM;CAC5C,UAAU;EACR,MAAM,GAAG,SAAS,OAAO,SAAS,EAAE,YAAY,CAAC,CAAC;CACpD;AACF"}
@@ -0,0 +1,45 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ //#region web/src/build/public-files.ts
5
+ /**
6
+ * Enumerate the application-owned files Vite serves from `<appRoot>/public`
7
+ * in development. Paths are POSIX and relative so the same list can be baked
8
+ * into the production page manifest without carrying a machine-local root.
9
+ */
10
+ async function collectPublicFiles(publicRoot) {
11
+ let root;
12
+ try {
13
+ root = await fs.promises.stat(publicRoot);
14
+ } catch (error) {
15
+ if (error.code === "ENOENT") return [];
16
+ throw error;
17
+ }
18
+ if (!root.isDirectory()) return [];
19
+ const files = [];
20
+ async function visit(directory) {
21
+ const entries = await fs.promises.readdir(directory, { withFileTypes: true });
22
+ entries.sort((left, right) => left.name.localeCompare(right.name));
23
+ for (const entry of entries) {
24
+ const absolute = path.join(directory, entry.name);
25
+ if (entry.isDirectory()) await visit(absolute);
26
+ else if (entry.isFile()) files.push(path.relative(publicRoot, absolute).split(path.sep).join("/"));
27
+ }
28
+ }
29
+ await visit(publicRoot);
30
+ return files;
31
+ }
32
+ /** Copy exactly the files recorded by {@link collectPublicFiles}. */
33
+ async function copyPublicFiles(publicRoot, outputRoot, files) {
34
+ for (const file of files) {
35
+ const segments = file.split("/");
36
+ const source = path.join(publicRoot, ...segments);
37
+ const target = path.join(outputRoot, ...segments);
38
+ await fs.promises.mkdir(path.dirname(target), { recursive: true });
39
+ await fs.promises.copyFile(source, target);
40
+ }
41
+ }
42
+
43
+ //#endregion
44
+ export { collectPublicFiles, copyPublicFiles };
45
+ //# sourceMappingURL=public-files.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public-files.mjs","names":[],"sources":["C:/Work/Warlock.js/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,EAAE,MAAM,KAAK,GAAG,EAAE,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,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import { parse } from "@babel/parser";
3
3
 
4
- //#region ../web/src/build/read-route-exports.ts
4
+ //#region web/src/build/read-route-exports.ts
5
5
  /**
6
6
  * Reads a page's `route` export and a layout's `prefix` export STATICALLY —
7
7
  * by parsing the source, never by loading the module.
@@ -1 +1 @@
1
- {"version":3,"file":"read-route-exports.mjs","names":[],"sources":["../../../../../../../web/src/build/read-route-exports.ts"],"sourcesContent":["/**\n * Reads a page's `route` export and a layout's `prefix` export STATICALLY —\n * by parsing the source, never by loading the module.\n *\n * The build has to know a page's declared route before anything is built, and\n * the only other way to learn it is to run the page: import the module, let its\n * top-level code execute, and read the binding. That is a different program\n * from the one being built, with the application's own side effects in it. So\n * this module parses instead, and the price of parsing is that the declaration\n * has to be readable without evaluation — a literal. What cannot be read is\n * REFUSED rather than guessed: a wrong route path that builds is worse than a\n * build that stops and says which file to change.\n *\n * `route` and `prefix` are names the page contract reserves, so this reads them\n * out of whichever file it is given and refuses a computed one wherever it\n * appears — a page that exports `prefix`, or a layout that exports `route`, is\n * using a name the framework already owns.\n *\n * Single responsibility, deliberately: this returns values or a typed\n * rejection and decides nothing. What a rejection costs, and when a page is\n * routed at all, belongs to the caller.\n */\nimport fs from \"node:fs\";\nimport { parse } from \"@babel/parser\";\n\n/**\n * The AST types are derived from `parse`'s own return type rather than imported\n * from `@babel/types`: the parser resolves its own copy of that package, and a\n * node from one copy is not assignable to the identically-shaped type from the\n * other. Reading the types off the function that produced the nodes cannot\n * disagree with it.\n */\ntype Statement = ReturnType<typeof parse>[\"program\"][\"body\"][number];\ntype Expression = Extract<Statement, { type: \"ExpressionStatement\" }>[\"expression\"];\ntype ObjectExpression = Extract<Expression, { type: \"ObjectExpression\" }>;\ntype ObjectProperty = Extract<ObjectExpression[\"properties\"][number], { type: \"ObjectProperty\" }>;\n\n/**\n * Anything that can appear where a value is expected — an expression, or one of\n * the destructuring patterns that are legal in an object literal's value slot\n * and are never a literal string.\n */\ntype ValueNode = ObjectProperty[\"value\"];\n\n/**\n * A declared route, normalised. The bare-string form (`route = \"/list\"`) and\n * the object form (`route = { path: \"/list\" }`) reach the caller identically,\n * because the server resolves them identically — `name` is absent exactly when\n * the source omitted it, which is the caller's signal to derive one.\n */\nexport type DeclaredRoute = { path: string; name?: string };\n\n/** Which export could not be read, from which file, and what was found instead. */\nexport type RouteExportsRejection = {\n sourceFile: string;\n exportName: \"route\" | \"prefix\";\n /** A sentence fragment naming the form that was found, e.g. \"its value is a function call\". */\n detail: string;\n};\n\nexport type RouteExportsReadResult =\n | { ok: true; route?: DeclaredRoute; prefix?: string }\n | { ok: false; rejection: RouteExportsRejection };\n\nconst EXAMPLES: Record<\"route\" | \"prefix\", string> = {\n route: 'export const route = \"/list\"; (or export const route = { path: \"/list\", name: \"shop.list\" };)',\n prefix: 'export const prefix = \"/shop\";',\n};\n\n/**\n * The one thing an app developer is told when a declaration cannot be read.\n *\n * It names the file, says what was found, says why a literal is required, and\n * shows one — because the reader of this message is someone who wrote perfectly\n * valid TypeScript and needs to know why the build will not take it.\n */\nexport class NonLiteralRouteExportError extends Error {\n public constructor(public readonly rejection: RouteExportsRejection) {\n const { sourceFile, exportName, detail } = rejection;\n\n super(\n `Cannot read the \\`${exportName}\\` export of \"${sourceFile}\": ${detail}. The build reads ` +\n \"route declarations without running your application code, so this value has to be \" +\n `written out as a literal. For example: ${EXAMPLES[exportName]}`,\n );\n\n this.name = \"NonLiteralRouteExportError\";\n }\n}\n\n/**\n * `as const`, `satisfies`, a non-null assertion and parentheses all wrap a value\n * without changing it, so reading through them costs nothing and refusing them\n * would reject declarations that are literal in every sense that matters here.\n */\nfunction unwrap(node: ValueNode): ValueNode {\n switch (node.type) {\n case \"TSAsExpression\":\n case \"TSSatisfiesExpression\":\n case \"TSNonNullExpression\":\n case \"TypeCastExpression\":\n case \"ParenthesizedExpression\":\n return unwrap(node.expression);\n default:\n return node;\n }\n}\n\n/** The string a node denotes, or `undefined` when that needs evaluation to know. */\nfunction stringLiteralOf(node: ValueNode): string | undefined {\n const value = unwrap(node);\n\n if (value.type === \"StringLiteral\") return value.value;\n\n // A template with no substitutions is a string spelled with backticks.\n if (value.type === \"TemplateLiteral\" && value.expressions.length === 0) {\n return value.quasis[0]?.value.cooked ?? value.quasis[0]?.value.raw;\n }\n\n return undefined;\n}\n\n/** A sentence fragment naming what was found, for the developer-facing message. */\nfunction describe(node: ValueNode): string {\n const value = unwrap(node);\n\n switch (value.type) {\n case \"CallExpression\":\n case \"OptionalCallExpression\":\n case \"NewExpression\":\n return \"its value is a function call\";\n case \"Identifier\":\n return `its value is the variable \\`${value.name}\\``;\n case \"MemberExpression\":\n case \"OptionalMemberExpression\":\n return \"its value is read off another object\";\n case \"TemplateLiteral\":\n return \"its value is a template literal with an expression in it\";\n case \"ConditionalExpression\":\n return \"its value depends on a condition\";\n case \"BinaryExpression\":\n case \"LogicalExpression\":\n return \"its value is built by an expression\";\n default:\n return \"its value is computed rather than written out\";\n }\n}\n\ntype ObjectRead = { ok: true; route: DeclaredRoute } | { ok: false; detail: string };\n\n/**\n * The object form. Unknown keys are IGNORED rather than refused, matching the\n * server, which reads `path` and `name` and lets a page carry whatever else it\n * wants alongside them. A spread is not an unknown key: it can contribute\n * `path` itself, so an object that spreads is an object whose route this cannot\n * claim to have read.\n */\nfunction readRouteObject(node: ObjectExpression): ObjectRead {\n let routePath: string | undefined;\n let routeName: string | undefined;\n\n for (const property of node.properties) {\n if (property.type === \"SpreadElement\") {\n return { ok: false, detail: \"the object spreads another value into itself\" };\n }\n\n if (property.computed) {\n return { ok: false, detail: \"one of the object's keys is computed\" };\n }\n\n const { key } = property;\n const keyName =\n key.type === \"Identifier\" ? key.name : key.type === \"StringLiteral\" ? key.value : undefined;\n\n if (keyName !== \"path\" && keyName !== \"name\") continue;\n\n if (property.type !== \"ObjectProperty\") {\n return { ok: false, detail: `\\`${keyName}\\` is declared as a method` };\n }\n\n const value = stringLiteralOf(property.value);\n\n if (value === undefined) {\n return { ok: false, detail: `its \\`${keyName}\\` is not written as a string literal` };\n }\n\n if (keyName === \"path\") routePath = value;\n else routeName = value;\n }\n\n if (routePath === undefined) {\n return { ok: false, detail: \"the object does not declare a `path`\" };\n }\n\n return {\n ok: true,\n route: routeName === undefined ? { path: routePath } : { path: routePath, name: routeName },\n };\n}\n\n/**\n * Parses the source, or THROWS when it cannot be parsed at all.\n *\n * A syntax error is not a rejection, deliberately: nothing about the route\n * declaration is known yet, so telling the developer to write a literal would\n * answer a question they did not ask.\n */\nfunction parseSource(sourceFile: string, source: string) {\n try {\n return parse(source, {\n sourceType: \"module\",\n // Every file this reads is a page or a layout, i.e. `.tsx`.\n plugins: [\"typescript\", \"jsx\"],\n errorRecovery: false,\n });\n } catch (error) {\n throw new Error(\n `Cannot read the route declarations of \"${sourceFile}\": the file could not be parsed ` +\n `(${(error as Error).message}). Fix the syntax error and the build will continue.`,\n );\n }\n}\n\n/**\n * Returns the literal `route` and `prefix` this file declares.\n *\n * Absent is not a rejection: a file that declares neither is read successfully\n * with both fields unset, and what THAT means — a page with no public URL, a\n * layout that adds no prefix — is the caller's call to make.\n *\n * `source` is an override for callers that already hold the text; by default\n * the file is read from disk.\n */\nexport function readRouteExports(sourceFile: string, source?: string): RouteExportsReadResult {\n const text = source ?? fs.readFileSync(sourceFile, \"utf-8\");\n const ast = parseSource(sourceFile, text);\n\n const reject = (exportName: \"route\" | \"prefix\", detail: string): RouteExportsReadResult => ({\n ok: false,\n rejection: { sourceFile, exportName, detail },\n });\n\n let route: DeclaredRoute | undefined;\n let prefix: string | undefined;\n\n for (const statement of ast.program.body) {\n if (statement.type !== \"ExportNamedDeclaration\" || statement.exportKind === \"type\") continue;\n\n // `export { route }` hides the value behind a binding this cannot follow\n // without resolving scope — and following it across modules is exactly the\n // evaluation this reader exists to avoid.\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 === \"route\" || exported === \"prefix\") {\n return reject(\n exported,\n \"it is exported through an export list rather than declared with `export const`\",\n );\n }\n }\n\n const { declaration } = statement;\n\n if (declaration?.type !== \"VariableDeclaration\") continue;\n\n for (const declarator of declaration.declarations) {\n if (declarator.id.type !== \"Identifier\") continue;\n\n const declared = declarator.id.name;\n\n if (declared !== \"route\" && declared !== \"prefix\") continue;\n\n if (declarator.init === null || declarator.init === undefined) {\n return reject(declared, \"it is declared without a value\");\n }\n\n if (declared === \"prefix\") {\n const value = stringLiteralOf(declarator.init);\n\n if (value === undefined) return reject(\"prefix\", describe(declarator.init));\n\n prefix = value;\n continue;\n }\n\n const value = stringLiteralOf(declarator.init);\n\n if (value !== undefined) {\n route = { path: value };\n continue;\n }\n\n const object = unwrap(declarator.init);\n\n if (object.type !== \"ObjectExpression\") return reject(\"route\", describe(declarator.init));\n\n const read = readRouteObject(object);\n\n if (!read.ok) return reject(\"route\", read.detail);\n\n route = read.route;\n }\n }\n\n return {\n ok: true,\n ...(route === undefined ? {} : { route }),\n ...(prefix === undefined ? {} : { prefix }),\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"read-route-exports.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/build/read-route-exports.ts"],"sourcesContent":["/**\r\n * Reads a page's `route` export and a layout's `prefix` export STATICALLY —\r\n * by parsing the source, never by loading the module.\r\n *\r\n * The build has to know a page's declared route before anything is built, and\r\n * the only other way to learn it is to run the page: import the module, let its\r\n * top-level code execute, and read the binding. That is a different program\r\n * from the one being built, with the application's own side effects in it. So\r\n * this module parses instead, and the price of parsing is that the declaration\r\n * has to be readable without evaluation — a literal. What cannot be read is\r\n * REFUSED rather than guessed: a wrong route path that builds is worse than a\r\n * build that stops and says which file to change.\r\n *\r\n * `route` and `prefix` are names the page contract reserves, so this reads them\r\n * out of whichever file it is given and refuses a computed one wherever it\r\n * appears — a page that exports `prefix`, or a layout that exports `route`, is\r\n * using a name the framework already owns.\r\n *\r\n * Single responsibility, deliberately: this returns values or a typed\r\n * rejection and decides nothing. What a rejection costs, and when a page is\r\n * routed at all, belongs to the caller.\r\n */\r\nimport fs from \"node:fs\";\r\nimport { parse } from \"@babel/parser\";\r\n\r\n/**\r\n * The AST types are derived from `parse`'s own return type rather than imported\r\n * from `@babel/types`: the parser resolves its own copy of that package, and a\r\n * node from one copy is not assignable to the identically-shaped type from the\r\n * other. Reading the types off the function that produced the nodes cannot\r\n * disagree with it.\r\n */\r\ntype Statement = ReturnType<typeof parse>[\"program\"][\"body\"][number];\r\ntype Expression = Extract<Statement, { type: \"ExpressionStatement\" }>[\"expression\"];\r\ntype ObjectExpression = Extract<Expression, { type: \"ObjectExpression\" }>;\r\ntype ObjectProperty = Extract<ObjectExpression[\"properties\"][number], { type: \"ObjectProperty\" }>;\r\n\r\n/**\r\n * Anything that can appear where a value is expected — an expression, or one of\r\n * the destructuring patterns that are legal in an object literal's value slot\r\n * and are never a literal string.\r\n */\r\ntype ValueNode = ObjectProperty[\"value\"];\r\n\r\n/**\r\n * A declared route, normalised. The bare-string form (`route = \"/list\"`) and\r\n * the object form (`route = { path: \"/list\" }`) reach the caller identically,\r\n * because the server resolves them identically — `name` is absent exactly when\r\n * the source omitted it, which is the caller's signal to derive one.\r\n */\r\nexport type DeclaredRoute = { path: string; name?: string };\r\n\r\n/** Which export could not be read, from which file, and what was found instead. */\r\nexport type RouteExportsRejection = {\r\n sourceFile: string;\r\n exportName: \"route\" | \"prefix\";\r\n /** A sentence fragment naming the form that was found, e.g. \"its value is a function call\". */\r\n detail: string;\r\n};\r\n\r\nexport type RouteExportsReadResult =\r\n | { ok: true; route?: DeclaredRoute; prefix?: string }\r\n | { ok: false; rejection: RouteExportsRejection };\r\n\r\nconst EXAMPLES: Record<\"route\" | \"prefix\", string> = {\r\n route: 'export const route = \"/list\"; (or export const route = { path: \"/list\", name: \"shop.list\" };)',\r\n prefix: 'export const prefix = \"/shop\";',\r\n};\r\n\r\n/**\r\n * The one thing an app developer is told when a declaration cannot be read.\r\n *\r\n * It names the file, says what was found, says why a literal is required, and\r\n * shows one — because the reader of this message is someone who wrote perfectly\r\n * valid TypeScript and needs to know why the build will not take it.\r\n */\r\nexport class NonLiteralRouteExportError extends Error {\r\n public constructor(public readonly rejection: RouteExportsRejection) {\r\n const { sourceFile, exportName, detail } = rejection;\r\n\r\n super(\r\n `Cannot read the \\`${exportName}\\` export of \"${sourceFile}\": ${detail}. The build reads ` +\r\n \"route declarations without running your application code, so this value has to be \" +\r\n `written out as a literal. For example: ${EXAMPLES[exportName]}`,\r\n );\r\n\r\n this.name = \"NonLiteralRouteExportError\";\r\n }\r\n}\r\n\r\n/**\r\n * `as const`, `satisfies`, a non-null assertion and parentheses all wrap a value\r\n * without changing it, so reading through them costs nothing and refusing them\r\n * would reject declarations that are literal in every sense that matters here.\r\n */\r\nfunction unwrap(node: ValueNode): ValueNode {\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 unwrap(node.expression);\r\n default:\r\n return node;\r\n }\r\n}\r\n\r\n/** The string a node denotes, or `undefined` when that needs evaluation to know. */\r\nfunction stringLiteralOf(node: ValueNode): string | undefined {\r\n const value = unwrap(node);\r\n\r\n if (value.type === \"StringLiteral\") return value.value;\r\n\r\n // A template with no substitutions is a string spelled with backticks.\r\n if (value.type === \"TemplateLiteral\" && value.expressions.length === 0) {\r\n return value.quasis[0]?.value.cooked ?? value.quasis[0]?.value.raw;\r\n }\r\n\r\n return undefined;\r\n}\r\n\r\n/** A sentence fragment naming what was found, for the developer-facing message. */\r\nfunction describe(node: ValueNode): string {\r\n const value = unwrap(node);\r\n\r\n switch (value.type) {\r\n case \"CallExpression\":\r\n case \"OptionalCallExpression\":\r\n case \"NewExpression\":\r\n return \"its value is a function call\";\r\n case \"Identifier\":\r\n return `its value is the variable \\`${value.name}\\``;\r\n case \"MemberExpression\":\r\n case \"OptionalMemberExpression\":\r\n return \"its value is read off another object\";\r\n case \"TemplateLiteral\":\r\n return \"its value is a template literal with an expression in it\";\r\n case \"ConditionalExpression\":\r\n return \"its value depends on a condition\";\r\n case \"BinaryExpression\":\r\n case \"LogicalExpression\":\r\n return \"its value is built by an expression\";\r\n default:\r\n return \"its value is computed rather than written out\";\r\n }\r\n}\r\n\r\ntype ObjectRead = { ok: true; route: DeclaredRoute } | { ok: false; detail: string };\r\n\r\n/**\r\n * The object form. Unknown keys are IGNORED rather than refused, matching the\r\n * server, which reads `path` and `name` and lets a page carry whatever else it\r\n * wants alongside them. A spread is not an unknown key: it can contribute\r\n * `path` itself, so an object that spreads is an object whose route this cannot\r\n * claim to have read.\r\n */\r\nfunction readRouteObject(node: ObjectExpression): ObjectRead {\r\n let routePath: string | undefined;\r\n let routeName: string | undefined;\r\n\r\n for (const property of node.properties) {\r\n if (property.type === \"SpreadElement\") {\r\n return { ok: false, detail: \"the object spreads another value into itself\" };\r\n }\r\n\r\n if (property.computed) {\r\n return { ok: false, detail: \"one of the object's keys is computed\" };\r\n }\r\n\r\n const { key } = property;\r\n const keyName =\r\n key.type === \"Identifier\" ? key.name : key.type === \"StringLiteral\" ? key.value : undefined;\r\n\r\n if (keyName !== \"path\" && keyName !== \"name\") continue;\r\n\r\n if (property.type !== \"ObjectProperty\") {\r\n return { ok: false, detail: `\\`${keyName}\\` is declared as a method` };\r\n }\r\n\r\n const value = stringLiteralOf(property.value);\r\n\r\n if (value === undefined) {\r\n return { ok: false, detail: `its \\`${keyName}\\` is not written as a string literal` };\r\n }\r\n\r\n if (keyName === \"path\") routePath = value;\r\n else routeName = value;\r\n }\r\n\r\n if (routePath === undefined) {\r\n return { ok: false, detail: \"the object does not declare a `path`\" };\r\n }\r\n\r\n return {\r\n ok: true,\r\n route: routeName === undefined ? { path: routePath } : { path: routePath, name: routeName },\r\n };\r\n}\r\n\r\n/**\r\n * Parses the source, or THROWS when it cannot be parsed at all.\r\n *\r\n * A syntax error is not a rejection, deliberately: nothing about the route\r\n * declaration is known yet, so telling the developer to write a literal would\r\n * answer a question they did not ask.\r\n */\r\nfunction parseSource(sourceFile: string, source: string) {\r\n try {\r\n return parse(source, {\r\n sourceType: \"module\",\r\n // Every file this reads is a page or a layout, i.e. `.tsx`.\r\n plugins: [\"typescript\", \"jsx\"],\r\n errorRecovery: false,\r\n });\r\n } catch (error) {\r\n throw new Error(\r\n `Cannot read the route declarations of \"${sourceFile}\": 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\r\n/**\r\n * Returns the literal `route` and `prefix` this file declares.\r\n *\r\n * Absent is not a rejection: a file that declares neither is read successfully\r\n * with both fields unset, and what THAT means — a page with no public URL, a\r\n * layout that adds no prefix — is the caller's call to make.\r\n *\r\n * `source` is an override for callers that already hold the text; by default\r\n * the file is read from disk.\r\n */\r\nexport function readRouteExports(sourceFile: string, source?: string): RouteExportsReadResult {\r\n const text = source ?? fs.readFileSync(sourceFile, \"utf-8\");\r\n const ast = parseSource(sourceFile, text);\r\n\r\n const reject = (exportName: \"route\" | \"prefix\", detail: string): RouteExportsReadResult => ({\r\n ok: false,\r\n rejection: { sourceFile, exportName, detail },\r\n });\r\n\r\n let route: DeclaredRoute | undefined;\r\n let prefix: string | undefined;\r\n\r\n for (const statement of ast.program.body) {\r\n if (statement.type !== \"ExportNamedDeclaration\" || statement.exportKind === \"type\") continue;\r\n\r\n // `export { route }` hides the value behind a binding this cannot follow\r\n // without resolving scope — and following it across modules is exactly the\r\n // evaluation this reader exists to avoid.\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 === \"route\" || exported === \"prefix\") {\r\n return reject(\r\n exported,\r\n \"it is exported through an export list rather than declared with `export const`\",\r\n );\r\n }\r\n }\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\") continue;\r\n\r\n const declared = declarator.id.name;\r\n\r\n if (declared !== \"route\" && declared !== \"prefix\") continue;\r\n\r\n if (declarator.init === null || declarator.init === undefined) {\r\n return reject(declared, \"it is declared without a value\");\r\n }\r\n\r\n if (declared === \"prefix\") {\r\n const value = stringLiteralOf(declarator.init);\r\n\r\n if (value === undefined) return reject(\"prefix\", describe(declarator.init));\r\n\r\n prefix = value;\r\n continue;\r\n }\r\n\r\n const value = stringLiteralOf(declarator.init);\r\n\r\n if (value !== undefined) {\r\n route = { path: value };\r\n continue;\r\n }\r\n\r\n const object = unwrap(declarator.init);\r\n\r\n if (object.type !== \"ObjectExpression\") return reject(\"route\", describe(declarator.init));\r\n\r\n const read = readRouteObject(object);\r\n\r\n if (!read.ok) return reject(\"route\", read.detail);\r\n\r\n route = read.route;\r\n }\r\n }\r\n\r\n return {\r\n ok: true,\r\n ...(route === undefined ? {} : { route }),\r\n ...(prefix === undefined ? {} : { prefix }),\r\n };\r\n}\r\n"],"mappings":"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"}