@warlock.js/web 5.0.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 (182) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +125 -0
  4. package/esm/build/contribution.d.mts +55 -0
  5. package/esm/build/contribution.mjs +132 -0
  6. package/esm/build/contribution.mjs.map +1 -0
  7. package/esm/build/discover-pages.mjs +294 -0
  8. package/esm/build/discover-pages.mjs.map +1 -0
  9. package/esm/build/generate-client-registry.mjs +123 -0
  10. package/esm/build/generate-client-registry.mjs.map +1 -0
  11. package/esm/build/generate-pages-barrel.mjs +330 -0
  12. package/esm/build/generate-pages-barrel.mjs.map +1 -0
  13. package/esm/build/read-route-exports.mjs +212 -0
  14. package/esm/build/read-route-exports.mjs.map +1 -0
  15. package/esm/client/build-hydrated-tree.mjs +113 -0
  16. package/esm/client/build-hydrated-tree.mjs.map +1 -0
  17. package/esm/client/hydrate-page.mjs +73 -0
  18. package/esm/client/hydrate-page.mjs.map +1 -0
  19. package/esm/client/navigation/current-route.d.mts +91 -0
  20. package/esm/client/navigation/current-route.mjs +66 -0
  21. package/esm/client/navigation/current-route.mjs.map +1 -0
  22. package/esm/client/navigation/fetch-page-data.mjs +97 -0
  23. package/esm/client/navigation/fetch-page-data.mjs.map +1 -0
  24. package/esm/client/navigation/navigation-root.mjs +196 -0
  25. package/esm/client/navigation/navigation-root.mjs.map +1 -0
  26. package/esm/client/navigation/prefetch.mjs +110 -0
  27. package/esm/client/navigation/prefetch.mjs.map +1 -0
  28. package/esm/client/navigation/refresh.d.mts +40 -0
  29. package/esm/client/navigation/refresh.mjs +146 -0
  30. package/esm/client/navigation/refresh.mjs.map +1 -0
  31. package/esm/client/navigation/verbs.d.mts +55 -0
  32. package/esm/client/navigation/verbs.mjs +66 -0
  33. package/esm/client/navigation/verbs.mjs.map +1 -0
  34. package/esm/client/runtime/index.d.mts +4 -0
  35. package/esm/client/runtime/index.mjs +4 -0
  36. package/esm/client/runtime/manifest.d.mts +8 -0
  37. package/esm/client/runtime/manifest.mjs +77 -0
  38. package/esm/client/runtime/manifest.mjs.map +1 -0
  39. package/esm/client/runtime/matcher.d.mts +7 -0
  40. package/esm/client/runtime/matcher.mjs +169 -0
  41. package/esm/client/runtime/matcher.mjs.map +1 -0
  42. package/esm/client/runtime/types.d.mts +22 -0
  43. package/esm/components/default-app.mjs +32 -0
  44. package/esm/components/default-app.mjs.map +1 -0
  45. package/esm/components/document-context.d.mts +1 -0
  46. package/esm/components/document-context.mjs +24 -0
  47. package/esm/components/document-context.mjs.map +1 -0
  48. package/esm/components/head.d.mts +14 -0
  49. package/esm/components/head.mjs +63 -0
  50. package/esm/components/head.mjs.map +1 -0
  51. package/esm/components/link.d.mts +157 -0
  52. package/esm/components/link.mjs +194 -0
  53. package/esm/components/link.mjs.map +1 -0
  54. package/esm/components/scripts.d.mts +14 -0
  55. package/esm/components/scripts.mjs +21 -0
  56. package/esm/components/scripts.mjs.map +1 -0
  57. package/esm/connector/index.d.mts +6 -0
  58. package/esm/connector/index.mjs +7 -0
  59. package/esm/context.d.mts +107 -0
  60. package/esm/hydration/index.d.mts +1 -0
  61. package/esm/hydration/index.mjs +33 -0
  62. package/esm/hydration/index.mjs.map +1 -0
  63. package/esm/hydration-payload.d.mts +1 -0
  64. package/esm/hydration-payload.mjs +72 -0
  65. package/esm/hydration-payload.mjs.map +1 -0
  66. package/esm/index.d.mts +35 -0
  67. package/esm/index.mjs +12 -0
  68. package/esm/loaders.d.mts +47 -0
  69. package/esm/metadata.d.mts +54 -0
  70. package/esm/props.d.mts +42 -0
  71. package/esm/route.d.mts +30 -0
  72. package/esm/routing/compose-route-path.mjs +33 -0
  73. package/esm/routing/compose-route-path.mjs.map +1 -0
  74. package/esm/routing/data-request.mjs +52 -0
  75. package/esm/routing/data-request.mjs.map +1 -0
  76. package/esm/routing/layout-policy.mjs +78 -0
  77. package/esm/routing/layout-policy.mjs.map +1 -0
  78. package/esm/routing/navigator.mjs +21 -0
  79. package/esm/routing/navigator.mjs.map +1 -0
  80. package/esm/routing/query-string.d.mts +250 -0
  81. package/esm/routing/query-string.mjs +340 -0
  82. package/esm/routing/query-string.mjs.map +1 -0
  83. package/esm/routing/route-identity.mjs +68 -0
  84. package/esm/routing/route-identity.mjs.map +1 -0
  85. package/esm/routing/route-table.d.mts +34 -0
  86. package/esm/routing/route-table.mjs +182 -0
  87. package/esm/routing/route-table.mjs.map +1 -0
  88. package/esm/routing/router-events.d.mts +159 -0
  89. package/esm/routing/router-events.mjs +83 -0
  90. package/esm/routing/router-events.mjs.map +1 -0
  91. package/esm/server/buffered-response.mjs +115 -0
  92. package/esm/server/buffered-response.mjs.map +1 -0
  93. package/esm/server/build-hydration-payload.mjs +27 -0
  94. package/esm/server/build-hydration-payload.mjs.map +1 -0
  95. package/esm/server/client-asset-url-prefix.d.mts +22 -0
  96. package/esm/server/client-asset-url-prefix.mjs +23 -0
  97. package/esm/server/client-asset-url-prefix.mjs.map +1 -0
  98. package/esm/server/create-page-module-loader.mjs +49 -0
  99. package/esm/server/create-page-module-loader.mjs.map +1 -0
  100. package/esm/server/create-page-route-handler.mjs +107 -0
  101. package/esm/server/create-page-route-handler.mjs.map +1 -0
  102. package/esm/server/dev-server.mjs +167 -0
  103. package/esm/server/dev-server.mjs.map +1 -0
  104. package/esm/server/execute-page-request.d.mts +1 -0
  105. package/esm/server/execute-page-request.mjs +166 -0
  106. package/esm/server/execute-page-request.mjs.map +1 -0
  107. package/esm/server/execute-page-request.types.d.mts +1 -0
  108. package/esm/server/hydration-client-url.d.mts +37 -0
  109. package/esm/server/hydration-client-url.mjs +101 -0
  110. package/esm/server/hydration-client-url.mjs.map +1 -0
  111. package/esm/server/index.mjs +13 -0
  112. package/esm/server/install-page-routes-from-manifest.mjs +133 -0
  113. package/esm/server/install-page-routes-from-manifest.mjs.map +1 -0
  114. package/esm/server/install-page-routes.d.mts +1 -0
  115. package/esm/server/install-page-routes.mjs +6 -0
  116. package/esm/server/install-production-page-routes.mjs +55 -0
  117. package/esm/server/install-production-page-routes.mjs.map +1 -0
  118. package/esm/server/match-page-route.mjs +48 -0
  119. package/esm/server/match-page-route.mjs.map +1 -0
  120. package/esm/server/page-context.d.mts +1 -0
  121. package/esm/server/page-context.mjs +33 -0
  122. package/esm/server/page-context.mjs.map +1 -0
  123. package/esm/server/page-manifest.d.mts +102 -0
  124. package/esm/server/page-manifest.mjs +31 -0
  125. package/esm/server/page-manifest.mjs.map +1 -0
  126. package/esm/server/render-page.mjs +214 -0
  127. package/esm/server/render-page.mjs.map +1 -0
  128. package/esm/server/resolve-page-metadata.mjs +33 -0
  129. package/esm/server/resolve-page-metadata.mjs.map +1 -0
  130. package/esm/server/resolve-validation-data.mjs +27 -0
  131. package/esm/server/resolve-validation-data.mjs.map +1 -0
  132. package/esm/server/settle-page-response.d.mts +1 -0
  133. package/esm/server/settle-page-response.mjs +90 -0
  134. package/esm/server/settle-page-response.mjs.map +1 -0
  135. package/esm/server/stylesheet-urls.mjs +65 -0
  136. package/esm/server/stylesheet-urls.mjs.map +1 -0
  137. package/esm/server/web-connector-factory.d.mts +25 -0
  138. package/esm/server/web-connector-factory.mjs +121 -0
  139. package/esm/server/web-connector-factory.mjs.map +1 -0
  140. package/esm/server/web-connector.d.mts +34 -0
  141. package/esm/server/web-connector.mjs +516 -0
  142. package/esm/server/web-connector.mjs.map +1 -0
  143. package/esm/shared.d.mts +22 -0
  144. package/esm/shared.mjs +223 -0
  145. package/esm/shared.mjs.map +1 -0
  146. package/esm/validation.d.mts +31 -0
  147. package/esm/vite/app-convention-aliases.mjs +43 -0
  148. package/esm/vite/app-convention-aliases.mjs.map +1 -0
  149. package/esm/vite/build-client.d.mts +38 -0
  150. package/esm/vite/build-client.mjs +72 -0
  151. package/esm/vite/build-client.mjs.map +1 -0
  152. package/esm/vite/gate-a-resolve.d.mts +57 -0
  153. package/esm/vite/gate-a-resolve.mjs +834 -0
  154. package/esm/vite/gate-a-resolve.mjs.map +1 -0
  155. package/esm/vite/gate-b-secrets.d.mts +57 -0
  156. package/esm/vite/gate-b-secrets.mjs +299 -0
  157. package/esm/vite/gate-b-secrets.mjs.map +1 -0
  158. package/esm/vite/gate-c-verify.d.mts +118 -0
  159. package/esm/vite/gate-c-verify.mjs +263 -0
  160. package/esm/vite/gate-c-verify.mjs.map +1 -0
  161. package/esm/vite/hydration-entries.d.mts +17 -0
  162. package/esm/vite/hydration-entries.mjs +45 -0
  163. package/esm/vite/hydration-entries.mjs.map +1 -0
  164. package/esm/vite/index.d.mts +126 -0
  165. package/esm/vite/index.mjs +146 -0
  166. package/esm/vite/index.mjs.map +1 -0
  167. package/esm/vite/page-registry-plugin.d.mts +43 -0
  168. package/esm/vite/page-registry-plugin.mjs +138 -0
  169. package/esm/vite/page-registry-plugin.mjs.map +1 -0
  170. package/esm/vite/projection.d.mts +31 -0
  171. package/esm/vite/projection.mjs +384 -0
  172. package/esm/vite/projection.mjs.map +1 -0
  173. package/llms-full.txt +1041 -0
  174. package/llms.txt +21 -0
  175. package/package.json +77 -0
  176. package/skills/add-web-to-an-app/SKILL.md +103 -0
  177. package/skills/create-a-page/SKILL.md +141 -0
  178. package/skills/load-page-data/SKILL.md +183 -0
  179. package/skills/navigate-on-the-client/SKILL.md +182 -0
  180. package/skills/serve-styles/SKILL.md +145 -0
  181. package/skills/use-layouts/SKILL.md +121 -0
  182. package/skills/write-the-root/SKILL.md +131 -0
@@ -0,0 +1,330 @@
1
+ import { NestedLayoutsNotSupportedError } from "../routing/layout-policy.mjs";
2
+ import { DuplicatePageRouteNameError, discoverPages, discoverWebRoots, isFile, layoutChainFor, toPosix, walkFiles } from "./discover-pages.mjs";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { parse } from "@babel/parser";
6
+
7
+ //#region ../web/src/build/generate-pages-barrel.ts
8
+ /**
9
+ * The server half of the build→runtime handoff: the generated
10
+ * `<productionDir>/pages.ts` barrel that carries the page graph into the
11
+ * server bundle.
12
+ *
13
+ * The graph itself comes from `discover-pages.ts`, the one scan every provider
14
+ * shares — this module is the BARREL provider and nothing more. It takes the
15
+ * recipe, runs the Vite-switch tripwire over the sources the recipe came from,
16
+ * and writes the file.
17
+ *
18
+ * "Static" means the FILESYSTEM ONLY — discovery globs `*.page.tsx`,
19
+ * `layout.tsx` and `root.tsx` and never imports a single application module,
20
+ * mirroring how the builder already globs (`production-builder.ts:184-242`).
21
+ * The imports happen at BOOT, when the compiled barrel runs, which is what
22
+ * makes a page that throws on import a loud pre-listen failure in production.
23
+ *
24
+ * This is the HEAVY half of the contribution: `contribution.ts` reaches it by
25
+ * dynamic import inside its `generate` hook, so nothing here lands in a
26
+ * connector's static graph.
27
+ */
28
+ /**
29
+ * Declared BEFORE the patch that reads it. `const` is hoisted but sits in the
30
+ * temporal dead zone, and the patch below is an object literal evaluated at
31
+ * module load — reading this from further down the file would throw.
32
+ */
33
+ const STYLE_EXTENSIONS = [
34
+ ".css",
35
+ ".scss",
36
+ ".sass",
37
+ ".less",
38
+ ".styl"
39
+ ];
40
+ /**
41
+ * The esbuild patch web contributes (spike-settled, contract §3a).
42
+ *
43
+ * NO `process.env.NODE_ENV` define: warlock assigns to `process.env.NODE_ENV`
44
+ * at runtime (`core/src/utils/environment.ts:14`) and a define would turn that
45
+ * assignment into an assignment to a literal.
46
+ */
47
+ const WEB_ESBUILD_PATCH = {
48
+ jsx: "automatic",
49
+ jsxImportSource: "react",
50
+ define: {
51
+ "import.meta.env.DEV": "false",
52
+ "import.meta.env.PROD": "true",
53
+ "import.meta.env.SSR": "true",
54
+ "import.meta.env.MODE": "\"production\""
55
+ },
56
+ /**
57
+ * Stylesheets compile to NOTHING in the server bundle, rather than failing it.
58
+ *
59
+ * `root.tsx` importing `./app.css` is how Tailwind — and every other CSS
60
+ * pipeline — is normally wired, so rejecting it meant the framework could not
61
+ * build a styled application at all. But the server has no use for the bytes:
62
+ * it renders HTML, and all it ever needs of a stylesheet is a URL to put in a
63
+ * `<link>`. The CSS itself belongs to the CLIENT bundle, which Vite builds
64
+ * from the same `root.tsx` and which handles CSS natively.
65
+ *
66
+ * So the import stays in the user's source, the server drops it, and Vite
67
+ * emits the real asset. Nothing is silently lost: a stylesheet that does not
68
+ * exist still fails the client build, loudly, where it matters.
69
+ */
70
+ loader: Object.fromEntries(STYLE_EXTENSIONS.map((extension) => [extension, "empty"]))
71
+ };
72
+ /** Line appended to section 4 of the generated production entry, after `./routes`. */
73
+ 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";
75
+ const ASSET_EXTENSIONS = [
76
+ ".svg",
77
+ ".png",
78
+ ".jpg",
79
+ ".jpeg",
80
+ ".gif",
81
+ ".webp",
82
+ ".avif",
83
+ ".ico",
84
+ ".woff",
85
+ ".woff2",
86
+ ".ttf",
87
+ ".eot",
88
+ ".mp4",
89
+ ".webm"
90
+ ];
91
+ /** Raised by the Vite-switch tripwire. */
92
+ var WebPageGraphUnsupportedImportError = class extends Error {
93
+ sourceFile;
94
+ specifier;
95
+ constructor(sourceFile, specifier, reason) {
96
+ super(`Cannot build the page graph: "${sourceFile}" ${reason} (${specifier}). Pages are compiled into the server bundle, which supports plain code imports only — static assets, \`?raw\`/\`?url\` queries and \`import.meta.url\` are not supported there. (Stylesheets ARE supported: the server bundle drops them and the client bundle emits them.) To fix: ${REMEDY}.`);
97
+ this.sourceFile = sourceFile;
98
+ this.specifier = specifier;
99
+ this.name = "WebPageGraphUnsupportedImportError";
100
+ }
101
+ };
102
+ const FROM_SPECIFIER = /\bfrom\s*["']([^"']+)["']/g;
103
+ const BARE_IMPORT = /\bimport\s*["']([^"']+)["']/g;
104
+ const DYNAMIC_IMPORT = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
105
+ /**
106
+ * Blank every comment, preserving byte offsets.
107
+ *
108
+ * The scan below is regex over source text, so without this a specifier written
109
+ * inside a comment is indistinguishable from a real import. That is not
110
+ * hypothetical: documenting this very restriction — writing `import "./app.css"`
111
+ * in a doc comment to explain why it is unsupported — failed the build, and the
112
+ * error then insisted the file imported a stylesheet that it did not import.
113
+ * Removing the real import did not help, because the comment was the match.
114
+ *
115
+ * Comments are replaced with spaces rather than deleted so every subsequent
116
+ * offset, and therefore every line number in anything reported from here,
117
+ * stays exactly where it was.
118
+ *
119
+ * A parse failure is deliberately NOT fatal: this function's job is to find
120
+ * hazards, not to validate syntax. esbuild reports a genuine syntax error far
121
+ * better than we would, and failing here would replace its clear message with a
122
+ * worse one. On a parse failure the raw source is scanned — the pre-existing
123
+ * behaviour, false positives and all.
124
+ */
125
+ function withoutComments(source) {
126
+ try {
127
+ const ast = parse(source, {
128
+ sourceType: "module",
129
+ errorRecovery: true,
130
+ plugins: [
131
+ "typescript",
132
+ "jsx",
133
+ "decorators-legacy"
134
+ ]
135
+ });
136
+ let stripped = source;
137
+ for (const comment of ast.comments ?? []) {
138
+ const { start, end } = comment;
139
+ stripped = stripped.slice(0, start) + " ".repeat(end - start) + stripped.slice(end);
140
+ }
141
+ return stripped;
142
+ } catch {
143
+ return source;
144
+ }
145
+ }
146
+ /** Expects source whose comments have already been blanked by {@link withoutComments}. */
147
+ function collectSpecifiers(source) {
148
+ const specifiers = [];
149
+ for (const pattern of [
150
+ FROM_SPECIFIER,
151
+ BARE_IMPORT,
152
+ DYNAMIC_IMPORT
153
+ ]) {
154
+ pattern.lastIndex = 0;
155
+ let match = pattern.exec(source);
156
+ while (match !== null) {
157
+ specifiers.push(match[1]);
158
+ match = pattern.exec(source);
159
+ }
160
+ }
161
+ return specifiers;
162
+ }
163
+ function hazardFor(specifier) {
164
+ const [base, query] = specifier.split("?", 2);
165
+ const lowered = base.toLowerCase();
166
+ if (ASSET_EXTENSIONS.some((extension) => lowered.endsWith(extension))) return "imports a static asset";
167
+ if (query !== void 0 && /(^|&)(raw|url)(&|=|$)/.test(query)) return "uses a bundler-specific import query";
168
+ }
169
+ /**
170
+ * The Vite-switch tripwire. Fails the build
171
+ * the moment the page graph needs a capability the esbuild server build does
172
+ * not have, naming the file, the specifier and the remedy.
173
+ *
174
+ * LIMITATIONS, stated on purpose — BOTH directions, because stating only one
175
+ * is what made the other one expensive to find:
176
+ *
177
+ * FALSE NEGATIVES. This is a FILE-LEVEL scan of the `.ts`/`.tsx` sources under
178
+ * the discovered web roots — it is not transitive resolution. A page that
179
+ * imports a workspace package which itself imports CSS is not caught here; that
180
+ * failure surfaces at esbuild time instead.
181
+ *
182
+ * FALSE POSITIVES. The scan is regex over source text, so anything that LOOKS
183
+ * like a specifier counts. Comments are blanked first ({@link withoutComments})
184
+ * because a documented example used to fail the build while insisting on an
185
+ * import that was not there. String and template literals are NOT blanked —
186
+ * they cannot be, since real specifiers are string literals — so a specifier
187
+ * inside a template literal (a code sample in a docs page, say) still matches.
188
+ * Rare, and it fails loudly rather than silently, but it is not impossible.
189
+ */
190
+ function assertNoViteOnlyImports(webRoots, appRoot) {
191
+ for (const webRoot of webRoots) {
192
+ const sources = walkFiles(webRoot, (fileName) => fileName.endsWith(".ts") || fileName.endsWith(".tsx"));
193
+ for (const sourceFile of sources) {
194
+ const source = withoutComments(fs.readFileSync(sourceFile, "utf-8"));
195
+ const relative = toPosix(path.relative(appRoot, sourceFile));
196
+ for (const specifier of collectSpecifiers(source)) {
197
+ const reason = hazardFor(specifier);
198
+ if (reason !== void 0) throw new WebPageGraphUnsupportedImportError(relative, specifier, reason);
199
+ }
200
+ if (source.includes("import.meta.url")) throw new WebPageGraphUnsupportedImportError(relative, "import.meta.url", "uses `import.meta.url`, whose meaning changes under the bundled server build");
201
+ }
202
+ }
203
+ }
204
+ /** `<productionDir>` -> `<file>` as an extensionless, POSIX, relative specifier. */
205
+ function importSpecifierFor(productionDir, file) {
206
+ const relative = toPosix(path.relative(productionDir, file)).replace(/\.tsx?$/, "");
207
+ return relative.startsWith(".") ? relative : `./${relative}`;
208
+ }
209
+ function quote(value) {
210
+ return JSON.stringify(value);
211
+ }
212
+ /** Header of every generated barrel — the banner plus the one runtime import. */
213
+ const BARREL_HEADER = [
214
+ "// AUTO-GENERATED by @warlock.js/web — do not edit.",
215
+ "// The page graph, compiled into the server bundle.",
216
+ "// Route paths are deliberately absent: they are read off each module at boot.",
217
+ "import { providePageManifest } from \"@warlock.js/web/connector\";"
218
+ ];
219
+ /**
220
+ * The barrel written when discovery found no pages at all.
221
+ *
222
+ * No app entry: `root.tsx` is the root every PAGE renders inside, so with zero
223
+ * pages there is nothing for it to wrap and requiring it would fail builds that
224
+ * are legitimately page-free.
225
+ */
226
+ const EMPTY_BARREL_CONTENTS = [
227
+ ...BARREL_HEADER,
228
+ "",
229
+ "// Zero pages were discovered. The empty table is still provided, because that",
230
+ "// is what tells the boot-time reader \"this bundle WAS built with web, it just",
231
+ "// has no pages\" — as opposed to a bundle built without web at all, where no",
232
+ "// barrel runs and the manifest stays absent.",
233
+ "providePageManifest({ pages: [] });",
234
+ ""
235
+ ].join("\n");
236
+ async function writeBarrel(productionDir, contents) {
237
+ const barrelFile = path.join(productionDir, "pages.ts");
238
+ await fs.promises.mkdir(productionDir, { recursive: true });
239
+ await fs.promises.writeFile(barrelFile, contents, "utf-8");
240
+ return barrelFile;
241
+ }
242
+ /**
243
+ * Discovers the page graph, runs the tripwire, and writes
244
+ * `<productionDir>/pages.ts`.
245
+ *
246
+ * Zero pages is NOT an error, and it is NOT silence either: the barrel is
247
+ * written anyway carrying an empty table, so "configured with web, no pages"
248
+ * reaches the runtime as a fact instead of looking identical to "never built
249
+ * with web". The run also says so out loud (no silent caps). What the caller
250
+ * skips on zero pages is the CLIENT bundle — no pages means nothing to
251
+ * hydrate.
252
+ */
253
+ async function generatePagesBarrel(options) {
254
+ const { appRoot, productionDir, clientDir } = options;
255
+ const srcRoot = path.join(appRoot, options.srcDir ?? "src");
256
+ const webRoots = discoverWebRoots(srcRoot);
257
+ const discovered = discoverPages({
258
+ appRoot,
259
+ srcDir: options.srcDir
260
+ });
261
+ if (discovered.length === 0) {
262
+ console.log("web configured, 0 pages");
263
+ return {
264
+ pageCount: 0,
265
+ barrelFile: await writeBarrel(productionDir, EMPTY_BARREL_CONTENTS),
266
+ contents: EMPTY_BARREL_CONTENTS
267
+ };
268
+ }
269
+ assertNoViteOnlyImports(webRoots, appRoot);
270
+ 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.`);
272
+ const layoutIdentifiers = /* @__PURE__ */ new Map();
273
+ const pages = [];
274
+ for (const [index, page] of discovered.entries()) {
275
+ const layouts = page.layouts.map((layoutFile) => {
276
+ let identifier = layoutIdentifiers.get(layoutFile);
277
+ if (identifier === void 0) {
278
+ identifier = `l${layoutIdentifiers.size}`;
279
+ layoutIdentifiers.set(layoutFile, identifier);
280
+ }
281
+ return {
282
+ identifier,
283
+ sourceFile: layoutFile
284
+ };
285
+ });
286
+ pages.push({
287
+ identifier: `p${index}`,
288
+ sourceFile: page.pageFile,
289
+ layouts
290
+ });
291
+ }
292
+ const importLines = [
293
+ `import * as app from ${quote(importSpecifierFor(productionDir, appFile))};`,
294
+ ...[...layoutIdentifiers.entries()].map(([layoutFile, identifier]) => `import * as ${identifier} from ${quote(importSpecifierFor(productionDir, layoutFile))};`),
295
+ ...pages.map((page) => `import * as ${page.identifier} from ${quote(importSpecifierFor(productionDir, page.sourceFile))};`)
296
+ ];
297
+ const relativeToApp = (file) => quote(toPosix(path.relative(appRoot, file)));
298
+ const pageEntries = pages.map((page) => {
299
+ const layouts = page.layouts.map((layout) => `{ module: ${layout.identifier}, sourceFile: ${relativeToApp(layout.sourceFile)} }`).join(", ");
300
+ return [
301
+ " {",
302
+ ` module: ${page.identifier},`,
303
+ ` sourceFile: ${relativeToApp(page.sourceFile)},`,
304
+ ` layouts: [${layouts}],`,
305
+ " },"
306
+ ].join("\n");
307
+ });
308
+ const contents = [
309
+ ...BARREL_HEADER,
310
+ ...importLines,
311
+ "",
312
+ "providePageManifest({",
313
+ ` clientDir: ${quote(clientDir)},`,
314
+ ` app: { module: app, sourceFile: ${relativeToApp(appFile)} },`,
315
+ " pages: [",
316
+ ...pageEntries.map((entry) => entry.split("\n").map((line) => ` ${line}`).join("\n")),
317
+ " ],",
318
+ "});",
319
+ ""
320
+ ].join("\n");
321
+ return {
322
+ pageCount: pages.length,
323
+ barrelFile: await writeBarrel(productionDir, contents),
324
+ contents
325
+ };
326
+ }
327
+
328
+ //#endregion
329
+ export { WEB_ENTRY_IMPORT, WEB_ESBUILD_PATCH, generatePagesBarrel };
330
+ //# sourceMappingURL=generate-pages-barrel.mjs.map
@@ -0,0 +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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"}
@@ -0,0 +1,212 @@
1
+ import fs from "node:fs";
2
+ import { parse } from "@babel/parser";
3
+
4
+ //#region ../web/src/build/read-route-exports.ts
5
+ /**
6
+ * Reads a page's `route` export and a layout's `prefix` export STATICALLY —
7
+ * by parsing the source, never by loading the module.
8
+ *
9
+ * The build has to know a page's declared route before anything is built, and
10
+ * the only other way to learn it is to run the page: import the module, let its
11
+ * top-level code execute, and read the binding. That is a different program
12
+ * from the one being built, with the application's own side effects in it. So
13
+ * this module parses instead, and the price of parsing is that the declaration
14
+ * has to be readable without evaluation — a literal. What cannot be read is
15
+ * REFUSED rather than guessed: a wrong route path that builds is worse than a
16
+ * build that stops and says which file to change.
17
+ *
18
+ * `route` and `prefix` are names the page contract reserves, so this reads them
19
+ * out of whichever file it is given and refuses a computed one wherever it
20
+ * appears — a page that exports `prefix`, or a layout that exports `route`, is
21
+ * using a name the framework already owns.
22
+ *
23
+ * Single responsibility, deliberately: this returns values or a typed
24
+ * rejection and decides nothing. What a rejection costs, and when a page is
25
+ * routed at all, belongs to the caller.
26
+ */
27
+ const EXAMPLES = {
28
+ route: "export const route = \"/list\"; (or export const route = { path: \"/list\", name: \"shop.list\" };)",
29
+ prefix: "export const prefix = \"/shop\";"
30
+ };
31
+ /**
32
+ * The one thing an app developer is told when a declaration cannot be read.
33
+ *
34
+ * It names the file, says what was found, says why a literal is required, and
35
+ * shows one — because the reader of this message is someone who wrote perfectly
36
+ * valid TypeScript and needs to know why the build will not take it.
37
+ */
38
+ var NonLiteralRouteExportError = class extends Error {
39
+ rejection;
40
+ constructor(rejection) {
41
+ const { sourceFile, exportName, detail } = rejection;
42
+ super(`Cannot read the \`${exportName}\` export of "${sourceFile}": ${detail}. The build reads route declarations without running your application code, so this value has to be written out as a literal. For example: ${EXAMPLES[exportName]}`);
43
+ this.rejection = rejection;
44
+ this.name = "NonLiteralRouteExportError";
45
+ }
46
+ };
47
+ /**
48
+ * `as const`, `satisfies`, a non-null assertion and parentheses all wrap a value
49
+ * without changing it, so reading through them costs nothing and refusing them
50
+ * would reject declarations that are literal in every sense that matters here.
51
+ */
52
+ function unwrap(node) {
53
+ switch (node.type) {
54
+ case "TSAsExpression":
55
+ case "TSSatisfiesExpression":
56
+ case "TSNonNullExpression":
57
+ case "TypeCastExpression":
58
+ case "ParenthesizedExpression": return unwrap(node.expression);
59
+ default: return node;
60
+ }
61
+ }
62
+ /** The string a node denotes, or `undefined` when that needs evaluation to know. */
63
+ function stringLiteralOf(node) {
64
+ const value = unwrap(node);
65
+ if (value.type === "StringLiteral") return value.value;
66
+ if (value.type === "TemplateLiteral" && value.expressions.length === 0) return value.quasis[0]?.value.cooked ?? value.quasis[0]?.value.raw;
67
+ }
68
+ /** A sentence fragment naming what was found, for the developer-facing message. */
69
+ function describe(node) {
70
+ const value = unwrap(node);
71
+ switch (value.type) {
72
+ case "CallExpression":
73
+ case "OptionalCallExpression":
74
+ case "NewExpression": return "its value is a function call";
75
+ case "Identifier": return `its value is the variable \`${value.name}\``;
76
+ case "MemberExpression":
77
+ case "OptionalMemberExpression": return "its value is read off another object";
78
+ case "TemplateLiteral": return "its value is a template literal with an expression in it";
79
+ case "ConditionalExpression": return "its value depends on a condition";
80
+ case "BinaryExpression":
81
+ case "LogicalExpression": return "its value is built by an expression";
82
+ default: return "its value is computed rather than written out";
83
+ }
84
+ }
85
+ /**
86
+ * The object form. Unknown keys are IGNORED rather than refused, matching the
87
+ * server, which reads `path` and `name` and lets a page carry whatever else it
88
+ * wants alongside them. A spread is not an unknown key: it can contribute
89
+ * `path` itself, so an object that spreads is an object whose route this cannot
90
+ * claim to have read.
91
+ */
92
+ function readRouteObject(node) {
93
+ let routePath;
94
+ let routeName;
95
+ for (const property of node.properties) {
96
+ if (property.type === "SpreadElement") return {
97
+ ok: false,
98
+ detail: "the object spreads another value into itself"
99
+ };
100
+ if (property.computed) return {
101
+ ok: false,
102
+ detail: "one of the object's keys is computed"
103
+ };
104
+ const { key } = property;
105
+ const keyName = key.type === "Identifier" ? key.name : key.type === "StringLiteral" ? key.value : void 0;
106
+ if (keyName !== "path" && keyName !== "name") continue;
107
+ if (property.type !== "ObjectProperty") return {
108
+ ok: false,
109
+ detail: `\`${keyName}\` is declared as a method`
110
+ };
111
+ const value = stringLiteralOf(property.value);
112
+ if (value === void 0) return {
113
+ ok: false,
114
+ detail: `its \`${keyName}\` is not written as a string literal`
115
+ };
116
+ if (keyName === "path") routePath = value;
117
+ else routeName = value;
118
+ }
119
+ if (routePath === void 0) return {
120
+ ok: false,
121
+ detail: "the object does not declare a `path`"
122
+ };
123
+ return {
124
+ ok: true,
125
+ route: routeName === void 0 ? { path: routePath } : {
126
+ path: routePath,
127
+ name: routeName
128
+ }
129
+ };
130
+ }
131
+ /**
132
+ * Parses the source, or THROWS when it cannot be parsed at all.
133
+ *
134
+ * A syntax error is not a rejection, deliberately: nothing about the route
135
+ * declaration is known yet, so telling the developer to write a literal would
136
+ * answer a question they did not ask.
137
+ */
138
+ function parseSource(sourceFile, source) {
139
+ try {
140
+ return parse(source, {
141
+ sourceType: "module",
142
+ plugins: ["typescript", "jsx"],
143
+ errorRecovery: false
144
+ });
145
+ } catch (error) {
146
+ throw new Error(`Cannot read the route declarations of "${sourceFile}": the file could not be parsed (${error.message}). Fix the syntax error and the build will continue.`);
147
+ }
148
+ }
149
+ /**
150
+ * Returns the literal `route` and `prefix` this file declares.
151
+ *
152
+ * Absent is not a rejection: a file that declares neither is read successfully
153
+ * with both fields unset, and what THAT means — a page with no public URL, a
154
+ * layout that adds no prefix — is the caller's call to make.
155
+ *
156
+ * `source` is an override for callers that already hold the text; by default
157
+ * the file is read from disk.
158
+ */
159
+ function readRouteExports(sourceFile, source) {
160
+ const ast = parseSource(sourceFile, source ?? fs.readFileSync(sourceFile, "utf-8"));
161
+ const reject = (exportName, detail) => ({
162
+ ok: false,
163
+ rejection: {
164
+ sourceFile,
165
+ exportName,
166
+ detail
167
+ }
168
+ });
169
+ let route;
170
+ let prefix;
171
+ for (const statement of ast.program.body) {
172
+ if (statement.type !== "ExportNamedDeclaration" || statement.exportKind === "type") continue;
173
+ for (const specifier of statement.specifiers) {
174
+ if (specifier.type !== "ExportSpecifier" || specifier.exportKind === "type") continue;
175
+ const exported = specifier.exported.type === "Identifier" ? specifier.exported.name : specifier.exported.value;
176
+ if (exported === "route" || exported === "prefix") return reject(exported, "it is exported through an export list rather than declared with `export const`");
177
+ }
178
+ const { declaration } = statement;
179
+ if (declaration?.type !== "VariableDeclaration") continue;
180
+ for (const declarator of declaration.declarations) {
181
+ if (declarator.id.type !== "Identifier") continue;
182
+ const declared = declarator.id.name;
183
+ if (declared !== "route" && declared !== "prefix") continue;
184
+ if (declarator.init === null || declarator.init === void 0) return reject(declared, "it is declared without a value");
185
+ if (declared === "prefix") {
186
+ const value = stringLiteralOf(declarator.init);
187
+ if (value === void 0) return reject("prefix", describe(declarator.init));
188
+ prefix = value;
189
+ continue;
190
+ }
191
+ const value = stringLiteralOf(declarator.init);
192
+ if (value !== void 0) {
193
+ route = { path: value };
194
+ continue;
195
+ }
196
+ const object = unwrap(declarator.init);
197
+ if (object.type !== "ObjectExpression") return reject("route", describe(declarator.init));
198
+ const read = readRouteObject(object);
199
+ if (!read.ok) return reject("route", read.detail);
200
+ route = read.route;
201
+ }
202
+ }
203
+ return {
204
+ ok: true,
205
+ ...route === void 0 ? {} : { route },
206
+ ...prefix === void 0 ? {} : { prefix }
207
+ };
208
+ }
209
+
210
+ //#endregion
211
+ export { NonLiteralRouteExportError, readRouteExports };
212
+ //# sourceMappingURL=read-route-exports.mjs.map
@@ -0,0 +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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"}