@torpor/build 0.4.14 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/README.md +20 -2
  2. package/dist/Jsonify-Dg6gChd3.d.mts +10 -0
  3. package/dist/Jsonify-Dg6gChd3.d.mts.map +1 -0
  4. package/dist/ParseRouteParams-DBmqlmLh.d.mts +42 -0
  5. package/dist/ParseRouteParams-DBmqlmLh.d.mts.map +1 -0
  6. package/dist/{RouteType-B6BMOyYw.mjs → Router-C4ADUfhv.mjs} +30 -42
  7. package/dist/Router-C4ADUfhv.mjs.map +1 -0
  8. package/dist/Server-BnjLJleZ.mjs +104 -0
  9. package/dist/Server-BnjLJleZ.mjs.map +1 -0
  10. package/dist/Server-Cler2w5O.d.mts +61 -0
  11. package/dist/Server-Cler2w5O.d.mts.map +1 -0
  12. package/dist/ServerEvent-CTx3Q-hk.d.mts +48 -0
  13. package/dist/ServerEvent-CTx3Q-hk.d.mts.map +1 -0
  14. package/dist/{ServerEvent-CxPwA4lH.mjs → ServerEvent-Dljn5i7A.mjs} +37 -11
  15. package/dist/ServerEvent-Dljn5i7A.mjs.map +1 -0
  16. package/dist/Site-CE0_o5Ci.d.mts +566 -0
  17. package/dist/Site-CE0_o5Ci.d.mts.map +1 -0
  18. package/dist/StandardSchema-D28L8_zZ.d.mts +100 -0
  19. package/dist/StandardSchema-D28L8_zZ.d.mts.map +1 -0
  20. package/dist/TypedResponse-6lznqN4U.d.mts +22 -0
  21. package/dist/TypedResponse-6lznqN4U.d.mts.map +1 -0
  22. package/dist/{_page-BQs4WvCO.mjs → _page-4cQKs_Dn.mjs} +3 -5
  23. package/dist/_page-4cQKs_Dn.mjs.map +1 -0
  24. package/dist/bin/index.d.mts +1 -1
  25. package/dist/bin/index.js +15 -11
  26. package/dist/bin/index.js.map +1 -1
  27. package/dist/clientEntry.d.mts +1 -0
  28. package/dist/clientEntry.mjs +59 -0
  29. package/dist/clientEntry.mjs.map +1 -0
  30. package/dist/clientEntryDev.d.mts +1 -0
  31. package/dist/clientEntryDev.mjs +7 -0
  32. package/dist/clientEntryDev.mjs.map +1 -0
  33. package/dist/dev.d.mts +3 -0
  34. package/dist/dev.mjs +3 -0
  35. package/dist/endpoint-EULRGCKA.mjs +108 -0
  36. package/dist/endpoint-EULRGCKA.mjs.map +1 -0
  37. package/dist/flattenHeaders-DM5qsSqO.mjs +13 -0
  38. package/dist/flattenHeaders-DM5qsSqO.mjs.map +1 -0
  39. package/dist/form.d.mts +35 -0
  40. package/dist/form.d.mts.map +1 -0
  41. package/dist/form.mjs +48 -0
  42. package/dist/form.mjs.map +1 -0
  43. package/dist/index.d.mts +72 -156
  44. package/dist/index.d.mts.map +1 -1
  45. package/dist/index.mjs +158 -26
  46. package/dist/index.mjs.map +1 -1
  47. package/dist/load-BntzvmwL.mjs +228 -0
  48. package/dist/load-BntzvmwL.mjs.map +1 -0
  49. package/dist/nav.d.mts +58 -5
  50. package/dist/nav.d.mts.map +1 -1
  51. package/dist/nav.mjs +63 -209
  52. package/dist/nav.mjs.map +1 -1
  53. package/dist/openapi.d.mts +144 -0
  54. package/dist/openapi.d.mts.map +1 -0
  55. package/dist/openapi.mjs +2 -0
  56. package/dist/pathTrie-D4Ax2Mu9.mjs +160 -0
  57. package/dist/pathTrie-D4Ax2Mu9.mjs.map +1 -0
  58. package/dist/plugin-D8Www11B.mjs +202 -0
  59. package/dist/plugin-D8Www11B.mjs.map +1 -0
  60. package/dist/{response-C5TtAsh1.mjs → response-Bg4w2S1q.mjs} +16 -39
  61. package/dist/response-Bg4w2S1q.mjs.map +1 -0
  62. package/dist/response.d.mts +309 -289
  63. package/dist/response.d.mts.map +1 -1
  64. package/dist/response.mjs +3 -4
  65. package/dist/run.d.mts +11 -4
  66. package/dist/run.d.mts.map +1 -1
  67. package/dist/run.mjs +2 -8
  68. package/dist/runOpenApi-DlOgF2Il.mjs +1419 -0
  69. package/dist/runOpenApi-DlOgF2Il.mjs.map +1 -0
  70. package/dist/schema.d.mts +32 -0
  71. package/dist/schema.d.mts.map +1 -0
  72. package/dist/schema.mjs +2 -0
  73. package/dist/server/Server.d.mts +2 -0
  74. package/dist/server/Server.mjs +2 -0
  75. package/dist/server.d.mts +11 -8
  76. package/dist/server.d.mts.map +1 -1
  77. package/dist/server.mjs +133 -4
  78. package/dist/server.mjs.map +1 -0
  79. package/dist/serverEntry-1qY3wZig.mjs +351 -0
  80. package/dist/serverEntry-1qY3wZig.mjs.map +1 -0
  81. package/dist/serverEntry-BAO8m03V.d.mts +6 -0
  82. package/dist/serverEntry-BAO8m03V.d.mts.map +1 -0
  83. package/dist/serverEntry.d.mts +2 -0
  84. package/dist/serverEntry.mjs +2 -0
  85. package/dist/state.d.mts.map +1 -1
  86. package/dist/state.mjs +2 -3
  87. package/dist/test.d.mts +8 -10
  88. package/dist/test.d.mts.map +1 -1
  89. package/dist/test.mjs +181 -79
  90. package/dist/test.mjs.map +1 -1
  91. package/dist/{seeOther-B4Yhu9iq.mjs → unprocessable-DAZbrDeR.mjs} +34 -13
  92. package/dist/unprocessable-DAZbrDeR.mjs.map +1 -0
  93. package/dist/validate-Ok4krlkX.mjs +40 -0
  94. package/dist/validate-Ok4krlkX.mjs.map +1 -0
  95. package/package.json +30 -18
  96. package/dist/RouteType-B6BMOyYw.mjs.map +0 -1
  97. package/dist/Server-CF5wDJp6.d.mts +0 -78
  98. package/dist/Server-CF5wDJp6.d.mts.map +0 -1
  99. package/dist/ServerEvent-CxPwA4lH.mjs.map +0 -1
  100. package/dist/Site-DgC6WWn1.d.mts +0 -78
  101. package/dist/Site-DgC6WWn1.d.mts.map +0 -1
  102. package/dist/_page-BQs4WvCO.mjs.map +0 -1
  103. package/dist/connectMiddleware-D7nehjj3.mjs +0 -252
  104. package/dist/connectMiddleware-D7nehjj3.mjs.map +0 -1
  105. package/dist/pathToRegex-TUAMc3zS.mjs +0 -11
  106. package/dist/pathToRegex-TUAMc3zS.mjs.map +0 -1
  107. package/dist/response-C5TtAsh1.mjs.map +0 -1
  108. package/dist/run-BPFtSYA3.mjs +0 -332
  109. package/dist/run-BPFtSYA3.mjs.map +0 -1
  110. package/dist/seeOther-B4Yhu9iq.mjs.map +0 -1
  111. package/src/bin/index.ts +0 -11
  112. package/src/index.ts +0 -30
  113. package/src/nav/formSubmit.ts +0 -56
  114. package/src/nav/load.ts +0 -9
  115. package/src/nav/loadData.ts +0 -134
  116. package/src/nav/navigate.ts +0 -164
  117. package/src/nav/reload.ts +0 -9
  118. package/src/nav.ts +0 -4
  119. package/src/response/badRequest.ts +0 -19
  120. package/src/response/created.ts +0 -20
  121. package/src/response/forbidden.ts +0 -21
  122. package/src/response/found.ts +0 -30
  123. package/src/response/methodNotAllowed.ts +0 -18
  124. package/src/response/movedPermanently.ts +0 -25
  125. package/src/response/notFound.ts +0 -24
  126. package/src/response/notModified.ts +0 -34
  127. package/src/response/ok.ts +0 -29
  128. package/src/response/permanentRedirect.ts +0 -30
  129. package/src/response/response.ts +0 -22
  130. package/src/response/seeOther.ts +0 -19
  131. package/src/response/serverError.ts +0 -24
  132. package/src/response/temporaryRedirect.ts +0 -31
  133. package/src/response/transfer.ts +0 -14
  134. package/src/response/unauthorized.ts +0 -21
  135. package/src/response/unprocessable.ts +0 -18
  136. package/src/response.ts +0 -37
  137. package/src/run/prepareTemplate.ts +0 -39
  138. package/src/run/run.ts +0 -62
  139. package/src/run/runBuild.ts +0 -77
  140. package/src/run/runDev.ts +0 -89
  141. package/src/run/runPreview.ts +0 -93
  142. package/src/run.ts +0 -6
  143. package/src/server/CookieHelper.ts +0 -36
  144. package/src/server/HeaderHelper.ts +0 -25
  145. package/src/server/Routerx.ts +0 -72
  146. package/src/server/Server.ts +0 -144
  147. package/src/server/ServerEvent.ts +0 -39
  148. package/src/server/connect/bufferToArrayBuffer.ts +0 -8
  149. package/src/server/connect/connectMiddleware.ts +0 -81
  150. package/src/server/connect/flattenHeaders.ts +0 -44
  151. package/src/server/connect/nodeMessageToNodeResponse.ts +0 -89
  152. package/src/server/connect/readableToBuffer.ts +0 -16
  153. package/src/server/connect/requestToNodeMessage.ts +0 -26
  154. package/src/server/contentType.ts +0 -85
  155. package/src/server/types/HttpMethod.ts +0 -12
  156. package/src/server/types/MiddlewareFunction.ts +0 -8
  157. package/src/server/types/ServerFunction.ts +0 -7
  158. package/src/server.ts +0 -7
  159. package/src/site/Router.ts +0 -164
  160. package/src/site/Site.ts +0 -214
  161. package/src/site/clientEntry.ts +0 -108
  162. package/src/site/clientEntryDev.ts +0 -3
  163. package/src/site/defaultAdapter.ts +0 -11
  164. package/src/site/manifest.ts +0 -54
  165. package/src/site/serverEntry.ts +0 -439
  166. package/src/state/$page.ts +0 -24
  167. package/src/state/client.ts +0 -15
  168. package/src/state.ts +0 -3
  169. package/src/test/runTest.ts +0 -350
  170. package/src/test.ts +0 -3
  171. package/src/types/Adapter.ts +0 -8
  172. package/src/types/ClientState.ts +0 -8
  173. package/src/types/InternalState.ts +0 -7
  174. package/src/types/LayoutHandler.ts +0 -5
  175. package/src/types/LayoutPath.ts +0 -10
  176. package/src/types/ManifestRoute.ts +0 -6
  177. package/src/types/PageEndPoint.ts +0 -39
  178. package/src/types/PageLoadEvent.ts +0 -14
  179. package/src/types/PageServerAction.ts +0 -7
  180. package/src/types/PageServerEndPoint.ts +0 -16
  181. package/src/types/PageServerLoad.ts +0 -7
  182. package/src/types/PageState.ts +0 -8
  183. package/src/types/Route.ts +0 -11
  184. package/src/types/RouteHandler.ts +0 -16
  185. package/src/types/RouteLayoutHandler.ts +0 -5
  186. package/src/types/RouteMatchResult.ts +0 -7
  187. package/src/types/RouteType.ts +0 -19
  188. package/src/types/ServerEndPoint.ts +0 -37
  189. package/src/types/ServerHook.ts +0 -11
  190. package/src/types/ServerLoadEvent.ts +0 -37
  191. package/src/types/ServerRequest.ts +0 -7
  192. package/src/utils/pathToRegex.ts +0 -15
  193. package/src/vite-env.d.ts +0 -2
@@ -0,0 +1,1419 @@
1
+ import { i as buildOpenApiDocument, t as OPEN_API_STATE_KEY } from "./plugin-D8Www11B.mjs";
2
+ import { r as serverError } from "./response-Bg4w2S1q.mjs";
3
+ import { t as Server } from "./Server-BnjLJleZ.mjs";
4
+ import { t as flattenHeaders } from "./flattenHeaders-DM5qsSqO.mjs";
5
+ import { createRequire, isBuiltin } from "node:module";
6
+ import fs, { existsSync, promises, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
7
+ import fpath from "node:path";
8
+ import { build, createServer, defineConfig } from "vite";
9
+ import torpor from "@torpor/unplugin/vite";
10
+ import { parse } from "@torpor/view/compile";
11
+ import estorpor from "@torpor/unplugin/esbuild";
12
+ import { configDotenv } from "dotenv";
13
+ import { Readable } from "node:stream";
14
+ import { createHash } from "node:crypto";
15
+ import { writeFile } from "node:fs/promises";
16
+ //#region src/utils/tsconfigAliases.ts
17
+ /**
18
+ * Reads `tsconfig.json` `compilerOptions.paths` (relative to `baseUrl`,
19
+ * defaulting to the tsconfig directory) and converts them to Vite
20
+ * `resolve.alias` entries. Only handles paths declared in the root tsconfig
21
+ * (not inherited via `extends`).
22
+ */
23
+ function tsconfigAliases(root) {
24
+ const file = fpath.join(root, "tsconfig.json");
25
+ let paths;
26
+ let baseUrl;
27
+ try {
28
+ const compilerOptions = JSON.parse(stripJsonc(readFileSync(file, "utf-8"))).compilerOptions ?? {};
29
+ baseUrl = compilerOptions.baseUrl ? fpath.resolve(root, compilerOptions.baseUrl) : root;
30
+ paths = compilerOptions.paths;
31
+ } catch {
32
+ return [];
33
+ }
34
+ if (!paths) return [];
35
+ const aliases = [];
36
+ for (const [pattern, targets] of Object.entries(paths)) {
37
+ const target = targets?.[0];
38
+ if (!target) continue;
39
+ if (pattern.endsWith("*")) {
40
+ const find = pattern.slice(0, -1);
41
+ const targetDir = target.slice(0, -1);
42
+ aliases.push({
43
+ find,
44
+ replacement: `${fpath.resolve(baseUrl, targetDir)}/`
45
+ });
46
+ } else aliases.push({
47
+ find: pattern,
48
+ replacement: fpath.resolve(baseUrl, target)
49
+ });
50
+ }
51
+ return aliases;
52
+ }
53
+ /**
54
+ * Strips JSONC comments and trailing commas so `tsconfig.json` (which permits
55
+ * both) can be parsed with `JSON.parse`. String contents are preserved.
56
+ */
57
+ function stripJsonc(text) {
58
+ let out = "";
59
+ for (let i = 0; i < text.length;) {
60
+ const c = text[i];
61
+ if (c === "\"" || c === "'") {
62
+ const quote = c;
63
+ out += c;
64
+ i++;
65
+ while (i < text.length) {
66
+ if (text[i] === "\\") {
67
+ out += text[i] + (text[i + 1] ?? "");
68
+ i += 2;
69
+ continue;
70
+ }
71
+ const cc = text[i++];
72
+ out += cc;
73
+ if (cc === quote) break;
74
+ }
75
+ continue;
76
+ }
77
+ if (c === "/" && text[i + 1] === "/") {
78
+ i += 2;
79
+ while (i < text.length && text[i] !== "\n") i++;
80
+ continue;
81
+ }
82
+ if (c === "/" && text[i + 1] === "*") {
83
+ i += 2;
84
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
85
+ i += 2;
86
+ continue;
87
+ }
88
+ out += c;
89
+ i++;
90
+ }
91
+ return out.replace(/,(\s*[}\]])/g, "$1");
92
+ }
93
+ //#endregion
94
+ //#region src/site/checkRoutes.ts
95
+ const ENDPOINT_TYPES = [
96
+ "PageServerEndPoint",
97
+ "PageEndPoint",
98
+ "ServerEndPoint",
99
+ "ServerHook"
100
+ ];
101
+ const USAGE_RE = new RegExp(`\\b(satisfies|as|:)\\s*\\b(${ENDPOINT_TYPES.join("|")})\\b`, "g");
102
+ const TYPE_NAME_RE = new RegExp(`^(?:${ENDPOINT_TYPES.join("|")})\\b`);
103
+ /**
104
+ * Checks the route files of a site against their type annotations:
105
+ *
106
+ * - Route path annotations (e.g. `PageServerEndPoint<"/posts/[id]">`) must
107
+ * match the route derived from the file's location, so types can't silently
108
+ * lie when files move
109
+ * - Exports that widen the endpoint type (`satisfies T as T`, `as T`, or a
110
+ * variable annotation) are flagged, because they destroy `PageData` /
111
+ * `PageForm` / param inference
112
+ * - Routes with dynamic params but no route annotation are flagged, because
113
+ * their params are loosely typed
114
+ *
115
+ * Only `.ts` route files are checked; annotations are matched by the standard
116
+ * type names (renamed imports are not detected). Best-effort, source-level —
117
+ * comments mentioning these patterns may produce false positives.
118
+ */
119
+ function checkRoutes(site) {
120
+ const issues = [];
121
+ for (const route of site.routes) issues.push(...checkRoute(site, route));
122
+ return issues;
123
+ }
124
+ /**
125
+ * Checks a single route's file. Returns no issues for routes without a `.ts`
126
+ * file (`.torp` components, inline endpoints, `.js` files).
127
+ */
128
+ function checkRoute(site, route) {
129
+ if (!route.file || !route.file.endsWith(".ts")) return [];
130
+ let source;
131
+ try {
132
+ source = readFileSync(fpath.join(site.root, route.file), "utf8");
133
+ } catch {
134
+ return [];
135
+ }
136
+ return checkRouteSource(source, route.file, expectedAnnotationPath(route), typeNameFor(route));
137
+ }
138
+ /**
139
+ * Checks a single route file's source. Exported for reuse (e.g. re-checking a
140
+ * changed file during dev).
141
+ * @param source The route file's source code
142
+ * @param file The file path, relative to the site root (used in messages)
143
+ * @param expected The route path derived from the file's location
144
+ * @param typeName The endpoint type name conventionally used by this route
145
+ * type (used in suggestions)
146
+ */
147
+ function checkRouteSource(source, file, expected, typeName) {
148
+ const issues = [];
149
+ let sawAnnotation = false;
150
+ USAGE_RE.lastIndex = 0;
151
+ for (let m = USAGE_RE.exec(source); m; m = USAGE_RE.exec(source)) {
152
+ sawAnnotation = true;
153
+ const kind = m[1];
154
+ const usedTypeName = m[2];
155
+ const { routePath, end } = parseTypeArgs(source, m.index + m[0].length);
156
+ const line = lineOf(source, m.index);
157
+ if (routePath !== void 0 && routePath !== expected) issues.push({
158
+ file,
159
+ line,
160
+ severity: "error",
161
+ message: `Route annotation "${routePath}" doesn't match this file's route "${expected}" (derived from its location). Did the file move?`
162
+ });
163
+ if (kind === "as" || kind === ":") issues.push({
164
+ file,
165
+ line,
166
+ severity: "warning",
167
+ message: kind === "as" ? `The \`as ${usedTypeName}\` cast widens the exported type, so PageData/PageForm/param inference falls back to loose types. Use \`satisfies ${usedTypeName}${routePathArg(expected, routePath)}\` without a cast instead.` : `The variable annotation widens the exported type, so PageData/PageForm/param inference falls back to loose types. Use \`satisfies ${usedTypeName}${routePathArg(expected, routePath)}\` on the default export instead.`
168
+ });
169
+ else {
170
+ const cast = parseAsCast(source, end);
171
+ if (cast) {
172
+ issues.push({
173
+ file,
174
+ line,
175
+ severity: "warning",
176
+ message: `The \`as\` cast after \`satisfies\` widens the exported type, so PageData/PageForm/param inference falls back to loose types. Use \`satisfies ${usedTypeName}${routePathArg(expected, routePath ?? cast.routePath)}\` on its own.`
177
+ });
178
+ if (cast.routePath !== void 0 && cast.routePath !== expected) issues.push({
179
+ file,
180
+ line,
181
+ severity: "error",
182
+ message: `Route annotation "${cast.routePath}" doesn't match this file's route "${expected}" (derived from its location). Did the file move?`
183
+ });
184
+ USAGE_RE.lastIndex = cast.end;
185
+ } else if (routePath === void 0 && expected.includes("[")) issues.push({
186
+ file,
187
+ line,
188
+ severity: "warning",
189
+ message: `No route path on the annotation, so params are loosely typed. Use \`satisfies ${usedTypeName}<"${expected}">\`.`
190
+ });
191
+ }
192
+ }
193
+ if (!sawAnnotation && file.endsWith(".ts") && expected.includes("[")) issues.push({
194
+ file,
195
+ line: 1,
196
+ severity: "warning",
197
+ message: `No route annotation, so params are loosely typed. Add \`satisfies ${typeName}<"${expected}">\` to the default export.`
198
+ });
199
+ return issues;
200
+ }
201
+ /**
202
+ * Prints issues to the console and returns the number of errors.
203
+ */
204
+ function reportRouteIssues(issues) {
205
+ for (const issue of issues) console.error(`[torpor] ${issue.severity}: ${issue.file}:${issue.line} ${issue.message}`);
206
+ return issues.filter((i) => i.severity === "error").length;
207
+ }
208
+ const MAKE_API_RE = /\bmakeApi\s*<\s*(["'])([^"']+)\1\s*,\s*typeof\s+([A-Za-z_$][\w$]*)/g;
209
+ const DEFAULT_IMPORT_RE = /\bimport\s+(?:type\s+)?([A-Za-z_$][\w$]*)\s+from\s*(["'])([^"']+)\2/g;
210
+ const SKIP_DIRS = /* @__PURE__ */ new Set([
211
+ "node_modules",
212
+ "dist",
213
+ ".git",
214
+ ".torpor",
215
+ ".vite",
216
+ "coverage"
217
+ ]);
218
+ const SOURCE_FILE_RE = /\.(ts|js|torp)$/;
219
+ /**
220
+ * Creates the shared state used by `checkApiCallSource`, so per-file checks
221
+ * (e.g. re-checking a changed file during dev) don't rebuild it.
222
+ */
223
+ function createApiCallCheck(site) {
224
+ const routesByFile = /* @__PURE__ */ new Map();
225
+ for (const route of site.routes) if (route.file) routesByFile.set(route.file.replaceAll("\\", "/"), route);
226
+ return {
227
+ root: site.root,
228
+ routesByFile,
229
+ aliases: tsconfigAliases(site.root)
230
+ };
231
+ }
232
+ /**
233
+ * Checks `makeApi<Route, typeof endpoint>` calls across the site's source
234
+ * files: the `Route` argument must match the route derived from the imported
235
+ * endpoint file's location. Calls whose endpoint isn't a type-only import of
236
+ * a known route file are skipped.
237
+ */
238
+ function checkApiCalls(site) {
239
+ const check = createApiCallCheck(site);
240
+ const issues = [];
241
+ for (const file of walkSourceFiles(site.root)) {
242
+ let source;
243
+ try {
244
+ source = readFileSync(file, "utf8");
245
+ } catch {
246
+ continue;
247
+ }
248
+ issues.push(...checkApiCallSource(source, file, check));
249
+ }
250
+ return issues;
251
+ }
252
+ /**
253
+ * Checks the `makeApi` calls in a single file's source.
254
+ * @param source The file's source code
255
+ * @param file The file's absolute path (used to resolve relative imports and
256
+ * to derive the path shown in messages)
257
+ * @param check Shared state from `createApiCallCheck`
258
+ */
259
+ function checkApiCallSource(source, file, check) {
260
+ const imports = /* @__PURE__ */ new Map();
261
+ for (let m = DEFAULT_IMPORT_RE.exec(source); m; m = DEFAULT_IMPORT_RE.exec(source)) imports.set(m[1], m[3]);
262
+ const issues = [];
263
+ for (let m = MAKE_API_RE.exec(source); m; m = MAKE_API_RE.exec(source)) {
264
+ const specifier = imports.get(m[3]);
265
+ if (!specifier) continue;
266
+ const resolved = resolveSpecifier(specifier, file, check);
267
+ if (!resolved) continue;
268
+ const relFile = fpath.relative(check.root, resolved).replaceAll("\\", "/");
269
+ const route = check.routesByFile.get(relFile);
270
+ if (!route) continue;
271
+ const expected = expectedAnnotationPath(route);
272
+ if (m[2] !== expected) issues.push({
273
+ file: fpath.relative(check.root, file).replaceAll("\\", "/"),
274
+ line: lineOf(source, m.index),
275
+ severity: "error",
276
+ message: `makeApi route "${m[2]}" doesn't match the endpoint's route "${expected}" (derived from ${relFile}). Did the endpoint move?`
277
+ });
278
+ }
279
+ return issues;
280
+ }
281
+ /**
282
+ * Resolves an import specifier to an existing file: relative specifiers are
283
+ * resolved against the importing file, others against tsconfig path aliases.
284
+ * Bare module specifiers that match no alias return undefined.
285
+ */
286
+ function resolveSpecifier(specifier, fromFile, check) {
287
+ let candidate;
288
+ if (specifier.startsWith("./") || specifier.startsWith("../")) candidate = fpath.resolve(fpath.dirname(fromFile), specifier);
289
+ else for (const alias of check.aliases) if (typeof alias.find === "string") {
290
+ if (specifier.startsWith(alias.find)) {
291
+ candidate = fpath.resolve(alias.replacement, specifier.slice(alias.find.length));
292
+ break;
293
+ }
294
+ } else if (alias.find.test(specifier)) {
295
+ candidate = specifier.replace(alias.find, alias.replacement);
296
+ break;
297
+ }
298
+ if (!candidate) return void 0;
299
+ const probes = [
300
+ candidate,
301
+ `${candidate}.ts`,
302
+ `${candidate}.js`,
303
+ fpath.join(candidate, "index.ts"),
304
+ fpath.join(candidate, "index.js")
305
+ ];
306
+ for (const probe of probes) if (existsSync(probe) && statSync(probe).isFile()) return probe;
307
+ }
308
+ function walkSourceFiles(root) {
309
+ const files = [];
310
+ const visit = (dir) => {
311
+ let entries;
312
+ try {
313
+ entries = readdirSync(dir, { withFileTypes: true });
314
+ } catch {
315
+ return;
316
+ }
317
+ for (const entry of entries) if (entry.isDirectory()) {
318
+ if (!SKIP_DIRS.has(entry.name)) visit(fpath.join(dir, entry.name));
319
+ } else if (entry.isFile() && SOURCE_FILE_RE.test(entry.name) && !entry.name.endsWith(".d.ts")) files.push(fpath.join(dir, entry.name));
320
+ };
321
+ visit(root);
322
+ return files;
323
+ }
324
+ /**
325
+ * The route path a file's annotation should use, derived from its manifest
326
+ * entry: internal suffixes (`/~server`, `/_layout` etc) are stripped so e.g.
327
+ * `posts/[id]/+page.server.ts` expects `"/posts/[id]"`.
328
+ */
329
+ function expectedAnnotationPath(route) {
330
+ let p = route.path;
331
+ switch (route.type) {
332
+ case 1:
333
+ p = p.replace(/\/~server$/, "");
334
+ break;
335
+ case 4:
336
+ p = p.replace(/\/_layout$/, "");
337
+ break;
338
+ case 5:
339
+ p = p.replace(/\/_layout\/~server$/, "");
340
+ break;
341
+ case 6:
342
+ p = p.replace(/\/_hook$/, "");
343
+ break;
344
+ case 7:
345
+ p = p.replace(/\/_hook\/~server$/, "");
346
+ break;
347
+ case 8:
348
+ p = p.replace(/\/_error$/, "");
349
+ break;
350
+ }
351
+ return p === "" ? "/" : p;
352
+ }
353
+ function typeNameFor(route) {
354
+ switch (route.type) {
355
+ case 1:
356
+ case 5: return "PageServerEndPoint";
357
+ case 3: return "ServerEndPoint";
358
+ case 6:
359
+ case 7: return "ServerHook";
360
+ default: return "PageEndPoint";
361
+ }
362
+ }
363
+ /**
364
+ * Parses the type arguments that follow an endpoint type name, returning the
365
+ * first argument when it is a string literal (the route path), plus the index
366
+ * after the closing `>` (or after the type name when there are no arguments).
367
+ */
368
+ function parseTypeArgs(source, start) {
369
+ let i = start;
370
+ while (i < source.length && /\s/.test(source[i])) i++;
371
+ if (source[i] !== "<") return {
372
+ routePath: void 0,
373
+ end: i
374
+ };
375
+ i++;
376
+ let depth = 1;
377
+ let routePath;
378
+ let seenComma = false;
379
+ while (i < source.length) {
380
+ const c = source[i];
381
+ if (c === "\"" || c === "'") {
382
+ const quote = c;
383
+ let j = i + 1;
384
+ while (j < source.length && source[j] !== quote) {
385
+ if (source[j] === "\\") j++;
386
+ j++;
387
+ }
388
+ if (routePath === void 0 && !seenComma) routePath = source.slice(i + 1, j);
389
+ i = j + 1;
390
+ continue;
391
+ }
392
+ if (c === "<") depth++;
393
+ else if (c === ">") {
394
+ depth--;
395
+ if (depth === 0) return {
396
+ routePath,
397
+ end: i + 1
398
+ };
399
+ } else if (c === "," && routePath === void 0) seenComma = true;
400
+ i++;
401
+ }
402
+ return {
403
+ routePath,
404
+ end: i
405
+ };
406
+ }
407
+ /**
408
+ * Parses an `as T`-style cast that follows a `satisfies T` usage, returning
409
+ * its optional route path argument and the index after the cast.
410
+ */
411
+ function parseAsCast(source, start) {
412
+ let i = start;
413
+ while (i < source.length && /\s/.test(source[i])) i++;
414
+ if (!/^as\b/.test(source.slice(i, i + 3))) return void 0;
415
+ i += 2;
416
+ while (i < source.length && /\s/.test(source[i])) i++;
417
+ if (!TYPE_NAME_RE.test(source.slice(i))) return void 0;
418
+ i = i + source.slice(i).match(TYPE_NAME_RE)[0].length;
419
+ return parseTypeArgs(source, i);
420
+ }
421
+ function routePathArg(expected, actual) {
422
+ return `<"${actual ?? expected}">`;
423
+ }
424
+ function lineOf(source, index) {
425
+ let line = 1;
426
+ for (let i = 0; i < index; i++) if (source[i] === "\n") line++;
427
+ return line;
428
+ }
429
+ //#endregion
430
+ //#region src/site/checkLayoutSlots.ts
431
+ /**
432
+ * The default import of a `.torp` component from a `.ts`/`.js` route file,
433
+ * e.g. `import component from "@/views/Layout.torp"`.
434
+ */
435
+ const DEFAULT_TORP_IMPORT_RE = /import\s+[A-Za-z_$][\w$]*\s+from\s*["']([^"']+\.torp)["']/;
436
+ /**
437
+ * Checks that every `_layout` route renders its default `<slot />`.
438
+ *
439
+ * The layout engine composes pages by passing each layout (and finally the
440
+ * page) as the default slot of its parent, so a layout that never renders
441
+ * `<slot />` drops its entire content: the server renders the layout's own
442
+ * markup with an empty content area (no error), and the first client-side
443
+ * navigation that reuses the layout fails on a null slot region.
444
+ *
445
+ * Runs at build time (failing the build) and at dev startup. Best-effort and
446
+ * static: a layout that renders `<slot />` conditionally (inside `@if`)
447
+ * passes here, and is caught at request time by the server render warning
448
+ * (see site/layoutSlots.ts).
449
+ */
450
+ function checkLayoutSlots(site) {
451
+ const issues = [];
452
+ for (const route of site.routes) if (route.type === 4) issues.push(...checkLayoutSlot(site, route));
453
+ return issues;
454
+ }
455
+ /**
456
+ * Checks a single layout route's component (the `.torp` route file itself,
457
+ * or the `.torp` file its `.ts`/`.js` endpoint re-exports as its default
458
+ * import). Routes without a file (inline endpoints) can't hold a component,
459
+ * so they are skipped.
460
+ */
461
+ function checkLayoutSlot(site, route) {
462
+ if (route.type !== 4 || !route.file) return [];
463
+ const check = createApiCallCheck(site);
464
+ let torpFile = fpath.join(site.root, route.file);
465
+ if (!route.file.endsWith(".torp")) {
466
+ const source = readIfExists(torpFile);
467
+ if (source === void 0) return [];
468
+ const match = DEFAULT_TORP_IMPORT_RE.exec(source);
469
+ if (!match) return [];
470
+ const resolved = resolveSpecifier(match[1], torpFile, check);
471
+ if (!resolved) return [];
472
+ torpFile = resolved;
473
+ }
474
+ const source = readIfExists(torpFile);
475
+ if (source === void 0) return [];
476
+ const parsed = parse(source);
477
+ if (!parsed.ok || !parsed.template) return [];
478
+ if (rendersDefaultSlot(parsed.template)) return [];
479
+ return [{
480
+ file: route.file,
481
+ line: 1,
482
+ severity: "error",
483
+ message: "The layout never renders <slot />, so pages under it render without their content. Every _layout component must render <slot />, or pass it down to a child component it renders."
484
+ }];
485
+ }
486
+ function readIfExists(file) {
487
+ try {
488
+ if (!existsSync(file) || !statSync(file).isFile()) return void 0;
489
+ return readFileSync(file, "utf8");
490
+ } catch {
491
+ return;
492
+ }
493
+ }
494
+ /**
495
+ * Walks a parsed template looking for a default `<slot />` anywhere in the
496
+ * render tree — including nested inside a child component element, which is
497
+ * how a layout passes the page slot down (e.g. `<Shell><slot /></Shell>`).
498
+ */
499
+ function rendersDefaultSlot(template) {
500
+ const component = template.components?.[0];
501
+ if (!component) return false;
502
+ return walk(component.markup);
503
+ }
504
+ function walk(node) {
505
+ if ((node.type === "element" || node.type === "special") && node.tagName === "slot" && !node.attributes?.some((a) => a.name === "name")) return true;
506
+ return (node.children ?? []).some(walk);
507
+ }
508
+ //#endregion
509
+ //#region src/site/manifest.ts
510
+ const moduleId = "@torpor/build/manifest";
511
+ const LOAD_EXPORT_RE = /[{,]\s*load\s*[:(]|\bexport\s+(?:async\s+)?(?:function|const|let|var)\s+load\b|\bexport\s*\{[^}]*\bload\b/;
512
+ function hasLoadExport(source) {
513
+ return LOAD_EXPORT_RE.test(source);
514
+ }
515
+ /**
516
+ * A Vite plugin that provides information about the app to both clientEntry and
517
+ * serverEntry.
518
+ */
519
+ function manifest(site, server = false) {
520
+ return {
521
+ name: "torpor-manifest",
522
+ resolveId: {
523
+ order: "pre",
524
+ handler(id) {
525
+ if (id === moduleId) return id;
526
+ }
527
+ },
528
+ load(id, viteOptions) {
529
+ if (id === moduleId) {
530
+ let serverRequest = server && !!viteOptions?.ssr;
531
+ const hasInline = site.routes.some((r) => r.endPoint && !r.file);
532
+ const hasPlugins = site.plugins.length > 0;
533
+ const hasOpenApi = site.pluginState.has(OPEN_API_STATE_KEY);
534
+ const configImport = hasInline || hasPlugins || hasOpenApi ? serverRequest && site.configFile ? `import __site from ${JSON.stringify(site.configFile)};` : "" : "";
535
+ const pluginLoop = serverRequest && hasPlugins ? `for (const __plugin of __site.plugins ?? []) { await __plugin(__site); }` : "";
536
+ const openApiOptions = site.pluginState.get(OPEN_API_STATE_KEY);
537
+ const openApiGlue = serverRequest && openApiOptions && site.configFile ? openApiGlueCode(site, openApiOptions) : "";
538
+ const openApiRoute = serverRequest && openApiOptions ? `{ path: ${JSON.stringify(openApiOptions.path)}, type: 3, endPoint: () => Promise.resolve({ default: { get: __openApiGet } }), subFolder: undefined },` : "";
539
+ return `
540
+ ${!serverRequest ? "const load = { default: { load: true } };" : ""}
541
+ ${configImport}
542
+ ${pluginLoop}
543
+ ${openApiGlue}
544
+ export default {
545
+ routes: [
546
+ ${site.routes.map((r) => {
547
+ let endPoint;
548
+ if (!r.file) if (serverRequest) {
549
+ const key = `${r.path}:${r.type}`;
550
+ endPoint = `() => Promise.resolve({ default: __site.inlineEndPoints[${JSON.stringify(key)}] })`;
551
+ } else endPoint = typeof r.endPoint?.load === "function" ? "() => load" : "undefined";
552
+ else if (serverRequest || !/server\.(ts|js)$/.test(r.file)) {
553
+ const importExpr = `() => import("${fpath.join(site.root, r.file)}")`;
554
+ endPoint = r.file.endsWith(".torp") ? `${importExpr}.then((m) => ({ default: { component: m.default } }))` : importExpr;
555
+ } else endPoint = hasLoadExport(readFileSync(fpath.join(site.root, r.file), "utf8")) ? "() => load" : "undefined";
556
+ const sub = r.subFolder ? `"${r.subFolder}"` : "undefined";
557
+ return `{ path: "${r.path}", type: ${r.type}, endPoint: ${endPoint}, subFolder: ${sub} },`;
558
+ }).join("\n ")}
559
+ ${openApiRoute}
560
+ ],
561
+ };
562
+ `;
563
+ }
564
+ }
565
+ };
566
+ }
567
+ /**
568
+ * Generates the module code for the OpenAPI document endpoint: a map of
569
+ * route path to endpoint loader, and a cached handler that loads every
570
+ * endpoint and builds the document on first use.
571
+ */
572
+ function openApiGlueCode(site, options) {
573
+ return `
574
+ import { buildOpenApiDocument, OPEN_API_STATE_KEY } from "@torpor/build/openapi";
575
+ let __openApiDoc;
576
+ const __openApiRoutes = {
577
+ ${site.routes.filter((r) => r.type === 3).filter((r) => r.path !== options.docs).map((r) => {
578
+ if (!r.file) {
579
+ const key = `${r.path}:${r.type}`;
580
+ return `${JSON.stringify(r.path)}: () => Promise.resolve({ default: __site.inlineEndPoints[${JSON.stringify(key)}] })`;
581
+ }
582
+ const filePath = fpath.join(site.root, r.file);
583
+ return `${JSON.stringify(r.path)}: () => import(${JSON.stringify(filePath)})`;
584
+ }).join(",\n ")}
585
+ };
586
+ async function __openApiGet() {
587
+ if (!__openApiDoc) {
588
+ const __entries = [];
589
+ for (const [__path, __loader] of Object.entries(__openApiRoutes)) {
590
+ __entries.push({ path: __path, endPoint: (await __loader()).default });
591
+ }
592
+ __openApiDoc = buildOpenApiDocument(__entries, __site.pluginState.get(OPEN_API_STATE_KEY));
593
+ }
594
+ return new Response(JSON.stringify(__openApiDoc), {
595
+ headers: { "Content-Type": "application/json" },
596
+ });
597
+ }
598
+ `;
599
+ }
600
+ //#endregion
601
+ //#region src/utils/torporPackages.ts
602
+ /**
603
+ * Finds packages installed in the site that ship `.torp` files, e.g. icon
604
+ * libraries like phosphor-torpor. These packages need the torpor compiler, so
605
+ * they can't go through Vite's default pipelines: dep optimization parses
606
+ * their files as plain JavaScript (and fails on torpor syntax like `@render`
607
+ * or `import type` from an uncompiled source), and SSR externalization would
608
+ * leave raw `.torp` imports that neither Node nor workerd can load.
609
+ *
610
+ * A package is a torpor package when it declares a `torpor` entry in its
611
+ * package.json (e.g. `"torpor": "./lib/index.js"`, mirroring the `svelte`
612
+ * field convention), or when any of its export targets is a `.torp` file.
613
+ *
614
+ * @param siteRoot The root folder of the site
615
+ * @returns The names of the torpor packages found (may be empty)
616
+ */
617
+ function findTorporPackages(siteRoot) {
618
+ const sitePackageJson = fpath.resolve(siteRoot, "package.json");
619
+ if (!existsSync(sitePackageJson)) return [];
620
+ const pkg = JSON.parse(readFileSync(sitePackageJson, "utf8"));
621
+ const packages = /* @__PURE__ */ new Set();
622
+ const visited = /* @__PURE__ */ new Set();
623
+ const rootRequire = createRequire(sitePackageJson);
624
+ for (const name of dependencyNames$1(pkg)) collectTorporPackage(name, rootRequire, packages, visited);
625
+ return [...packages];
626
+ }
627
+ /**
628
+ * Adds `optimizeDeps.exclude` and `ssr.noExternal` entries for packages that
629
+ * ship `.torp` files, so that they are served/bundled through the torpor
630
+ * compiler instead.
631
+ */
632
+ function addTorporPackageConfig(siteRoot, config) {
633
+ const packages = findTorporPackages(siteRoot);
634
+ if (packages.length === 0) return;
635
+ config.optimizeDeps ??= {};
636
+ config.optimizeDeps.exclude = [...config.optimizeDeps.exclude ?? [], ...packages];
637
+ config.ssr ??= {};
638
+ const noExternal = config.ssr.noExternal;
639
+ if (noExternal === void 0) config.ssr.noExternal = packages;
640
+ else if (Array.isArray(noExternal)) noExternal.push(...packages);
641
+ }
642
+ function dependencyNames$1(pkg) {
643
+ return [
644
+ ...Object.keys(pkg.dependencies ?? {}),
645
+ ...Object.keys(pkg.devDependencies ?? {}),
646
+ ...Object.keys(pkg.optionalDependencies ?? {}),
647
+ ...Object.keys(pkg.peerDependencies ?? {})
648
+ ];
649
+ }
650
+ function collectTorporPackage(name, require, packages, visited) {
651
+ if (isBuiltin(name)) return;
652
+ const packageJsonPath = resolvePackageJson(name, require);
653
+ if (!packageJsonPath || visited.has(packageJsonPath)) return;
654
+ visited.add(packageJsonPath);
655
+ let pkg;
656
+ try {
657
+ pkg = JSON.parse(readFileSync(packageJsonPath, "utf8"));
658
+ } catch {
659
+ return;
660
+ }
661
+ if (isTorporPackage(pkg)) packages.add(pkg.name ?? name);
662
+ const depRequire = createRequire(packageJsonPath);
663
+ for (const depName of dependencyNames$1(pkg)) collectTorporPackage(depName, depRequire, packages, visited);
664
+ }
665
+ function isTorporPackage(pkg) {
666
+ if (typeof pkg.torpor === "string") return true;
667
+ return exportsIncludeTorp(pkg.exports);
668
+ }
669
+ function exportsIncludeTorp(exports) {
670
+ if (typeof exports === "string") return exports.endsWith(".torp");
671
+ if (Array.isArray(exports)) return exports.some(exportsIncludeTorp);
672
+ if (exports && typeof exports === "object") return Object.values(exports).some(exportsIncludeTorp);
673
+ return false;
674
+ }
675
+ function resolvePackageJson(name, require) {
676
+ try {
677
+ const packageJson = require.resolve(`${name}/package.json`);
678
+ return fpath.isAbsolute(packageJson) ? packageJson : void 0;
679
+ } catch {
680
+ let entry;
681
+ try {
682
+ entry = require.resolve(name);
683
+ } catch {
684
+ return;
685
+ }
686
+ if (!fpath.isAbsolute(entry)) return void 0;
687
+ let dir = fpath.dirname(entry);
688
+ while (true) {
689
+ const packageJson = fpath.join(dir, "package.json");
690
+ if (existsSync(packageJson)) return packageJson;
691
+ const parent = fpath.dirname(dir);
692
+ if (parent === dir) return void 0;
693
+ dir = parent;
694
+ }
695
+ }
696
+ }
697
+ //#endregion
698
+ //#region src/utils/entryPaths.ts
699
+ /**
700
+ * Whether tb should run the framework from its TypeScript source instead of
701
+ * its compiled dist. This is a framework-development convenience: registry
702
+ * installs always run dist. Source mode is enabled explicitly via the
703
+ * TORPOR_SOURCE_DEV env var, or automatically when both `@torpor/build` and
704
+ * `@torpor/view` are symlinked into the app from outside node_modules (as
705
+ * workspace links and `link:` overrides do). Both packages must be linked,
706
+ * because source mode resolves their `development` export conditions, which
707
+ * point at src files that registry installs don't ship.
708
+ */
709
+ function detectSourceMode(siteRoot) {
710
+ if (process.env.TORPOR_SOURCE_DEV) return true;
711
+ return ["@torpor/build", "@torpor/view"].every((pkg) => {
712
+ try {
713
+ return !realpathSync(fpath.resolve(siteRoot, "node_modules", pkg, "dist/index.mjs")).includes("node_modules");
714
+ } catch {
715
+ return false;
716
+ }
717
+ });
718
+ }
719
+ /**
720
+ * The site entry files for the given mode: raw TypeScript sources when the
721
+ * framework is linked (Vite transforms them, and edits show up without a
722
+ * rebuild), compiled `.mjs` bundles for registry installs.
723
+ */
724
+ function siteEntryPaths(siteRoot, sourceMode) {
725
+ const folder = fpath.resolve(siteRoot, "node_modules/@torpor/build", sourceMode ? "src/site" : "dist");
726
+ const ext = sourceMode ? ".ts" : ".mjs";
727
+ return {
728
+ serverEntry: fpath.join(folder, `serverEntry${ext}`),
729
+ clientEntry: fpath.join(folder, `clientEntry${ext}`),
730
+ clientDevEntry: fpath.join(folder, `clientEntryDev${ext}`)
731
+ };
732
+ }
733
+ //#endregion
734
+ //#region src/run/runBuild.ts
735
+ async function runBuild(site) {
736
+ let errorCount = reportRouteIssues(checkRoutes(site));
737
+ errorCount += reportRouteIssues(checkApiCalls(site));
738
+ errorCount += reportRouteIssues(checkLayoutSlots(site));
739
+ if (errorCount > 0) throw new Error(`Route type check failed with ${errorCount} error${errorCount === 1 ? "" : "s"} (see above)`);
740
+ const distFolder = fpath.resolve(site.root, "dist");
741
+ if (existsSync(distFolder)) await promises.rm(distFolder, { recursive: true });
742
+ const clientFolder = fpath.join(distFolder, "client");
743
+ const serverFolder = fpath.join(distFolder, "server");
744
+ if (site.adapter.prebuild) await site.adapter.prebuild(site);
745
+ const siteHtml = fpath.resolve(site.root, "src/site.html");
746
+ const hasSiteHtml = existsSync(siteHtml);
747
+ const entries = siteEntryPaths(site.root, false);
748
+ const clientScript = entries.clientEntry;
749
+ const serverScript = entries.serverEntry;
750
+ const clientConfig = structuredClone(site.viteConfig ?? {});
751
+ clientConfig.resolve ??= {};
752
+ clientConfig.resolve.tsconfigPaths ??= true;
753
+ clientConfig.resolve.alias = [...asAliasArray$1(clientConfig.resolve.alias), ...tsconfigAliases(site.root)];
754
+ clientConfig.plugins = [
755
+ manifest(site),
756
+ torpor(),
757
+ ...site.vitePlugins
758
+ ];
759
+ clientConfig.build ??= {};
760
+ clientConfig.build.outDir = clientFolder;
761
+ clientConfig.build.rollupOptions ??= {};
762
+ clientConfig.build.rollupOptions.input = [
763
+ ...hasSiteHtml ? [siteHtml] : [],
764
+ clientScript,
765
+ ...site.routes.filter((r) => r.file && !/server\.(ts|js)$/.test(r.file)).map((r) => fpath.resolve(site.root, r.file)),
766
+ ...site.inputs.filter((f) => !/server\.(ts|js)$/.test(f))
767
+ ];
768
+ clientConfig.build.ssrManifest = true;
769
+ await build(defineConfig(clientConfig));
770
+ const serverConfig = structuredClone(site.viteConfig ?? {});
771
+ serverConfig.resolve ??= {};
772
+ serverConfig.resolve.tsconfigPaths ??= true;
773
+ serverConfig.resolve.alias = [...asAliasArray$1(serverConfig.resolve.alias), ...tsconfigAliases(site.root)];
774
+ serverConfig.plugins = [
775
+ manifest(site, true),
776
+ torpor(),
777
+ ...site.vitePlugins
778
+ ];
779
+ serverConfig.build ??= {};
780
+ serverConfig.build.outDir = serverFolder;
781
+ addTorporPackageConfig(site.root, serverConfig);
782
+ serverConfig.build.rollupOptions ??= {};
783
+ serverConfig.build.rollupOptions.input = [
784
+ serverScript,
785
+ ...site.routes.filter((r) => r.file).map((r) => fpath.resolve(site.root, r.file)),
786
+ ...site.inputs
787
+ ];
788
+ serverConfig.build.ssr = serverScript;
789
+ await build(defineConfig(serverConfig));
790
+ if (hasSiteHtml) {
791
+ await promises.rename(fpath.join(clientFolder, "src", "site.html"), fpath.join(clientFolder, "site.html"));
792
+ await promises.rm(fpath.join(clientFolder, "src"), { recursive: true });
793
+ }
794
+ if (site.adapter.postbuild) await site.adapter.postbuild(site);
795
+ }
796
+ function asAliasArray$1(alias) {
797
+ if (!alias) return [];
798
+ if (Array.isArray(alias)) return alias;
799
+ return Object.entries(alias).map(([find, replacement]) => ({
800
+ find,
801
+ replacement
802
+ }));
803
+ }
804
+ //#endregion
805
+ //#region src/run/prepareTemplate.ts
806
+ function prepareTemplate(template, clientScript, clientDevScript) {
807
+ let result = template;
808
+ let headStart = result.indexOf("</head>");
809
+ if (headStart === -1) throw new Error(`Couldn't find <head> end tag`);
810
+ result = result.substring(0, headStart) + "%COMPONENT_HEAD%" + result.substring(headStart);
811
+ let divMatchStart = regexIndexOf(result, /<div\s+id=("app"|'app'|app)[\s>]/);
812
+ if (divMatchStart === -1) throw new Error(`Couldn't find <div id="app"></div>`);
813
+ let bodyStart = result.indexOf(">", divMatchStart) + 1;
814
+ let bodyEnd = result.indexOf("</div>", bodyStart);
815
+ if (bodyEnd === -1) throw new Error(`Couldn't find <div id="app"></div>`);
816
+ result = result.substring(0, bodyStart) + "%COMPONENT_BODY%" + result.substring(bodyEnd);
817
+ if (clientDevScript) result += `<script type="module" src="${clientDevScript}"><\/script>`;
818
+ result += `<script type="module" src="${clientScript}"><\/script>`;
819
+ return result;
820
+ }
821
+ function regexIndexOf(string, regex, position) {
822
+ var indexOf = string.substring(position || 0).search(regex);
823
+ return indexOf >= 0 ? indexOf + (position || 0) : indexOf;
824
+ }
825
+ //#endregion
826
+ //#region src/run/devPlugin.ts
827
+ /**
828
+ * The default Node-runtime dev plugin. Reads `src/site.html`, sets up a torpor
829
+ * `Server` whose catch-all route renders via `vite.ssrLoadModule`, and registers
830
+ * a Connect middleware (after Vite's own middlewares) that bridges Node
831
+ * req/res to the torpor Server's fetch handler.
832
+ *
833
+ * Adapters that need a different runtime (e.g. workerd) provide their own
834
+ * `dev()` plugin instead.
835
+ */
836
+ function devPlugin(site, sourceMode = false) {
837
+ return {
838
+ name: "torpor-dev",
839
+ configureServer(vite) {
840
+ const handlerPromise = createDevHandler(vite, site, sourceMode);
841
+ return () => {
842
+ vite.middlewares.use(async (req, res, next) => {
843
+ try {
844
+ await (await handlerPromise)(req, res);
845
+ } catch (e) {
846
+ next(e);
847
+ }
848
+ });
849
+ };
850
+ }
851
+ };
852
+ }
853
+ async function createDevHandler(vite, site, sourceMode) {
854
+ const entries = siteEntryPaths(site.root, sourceMode);
855
+ const serverScript = entries.serverEntry;
856
+ let template;
857
+ const templateFile = fpath.resolve(site.root, "src/site.html");
858
+ if (existsSync(templateFile)) {
859
+ template = await promises.readFile(templateFile, "utf-8");
860
+ template = await vite.transformIndexHtml("", template);
861
+ template = prepareTemplate(template, entries.clientEntry, entries.clientDevEntry);
862
+ }
863
+ const server = new Server();
864
+ server.add("*", async (ev) => {
865
+ try {
866
+ const { load } = await vite.ssrLoadModule(serverScript);
867
+ return await load(ev, template);
868
+ } catch (e) {
869
+ vite.ssrFixStacktrace(e);
870
+ return serverError(e);
871
+ }
872
+ });
873
+ globalThis.adapter = { env: process.env };
874
+ return async (req, res) => {
875
+ const request = await nodeReqToRequest(req);
876
+ await sendResponse(await server.fetch(request), res);
877
+ };
878
+ }
879
+ async function nodeReqToRequest(req) {
880
+ const url = req.url ?? "/";
881
+ const host = req.headers.host ?? "localhost";
882
+ const protocol = "encrypted" in req.socket && req.socket.encrypted ? "https" : "http";
883
+ const init = {
884
+ method: req.method,
885
+ headers: flattenHeaders(req.headers)
886
+ };
887
+ if (req.method !== "GET" && req.method !== "HEAD") {
888
+ init.body = Readable.toWeb(req);
889
+ init.duplex = "half";
890
+ }
891
+ return new Request(`${protocol}://${host}${url}`, init);
892
+ }
893
+ async function sendResponse(response, res) {
894
+ res.statusCode = response.status;
895
+ response.headers.forEach((value, key) => res.setHeader(key, value));
896
+ if (!response.body) {
897
+ res.end();
898
+ return;
899
+ }
900
+ Readable.fromWeb(response.body).pipe(res);
901
+ }
902
+ //#endregion
903
+ //#region src/run/depCache.ts
904
+ /**
905
+ * Vite's dep-optimization cache (node_modules/.vite) keys off the lockfile
906
+ * and config, but not the content of workspace packages. When a workspace
907
+ * package is rebuilt while the cache holds prebundled copies of it, dev SSR
908
+ * keeps executing the stale modules -- producing bizarre errors (missing
909
+ * runtime identifiers, "$cache must be used in a getter", ...) that only a
910
+ * manual cache delete fixes.
911
+ *
912
+ * clearStaleDepCache runs at `tb --dev` startup: it fingerprints every
913
+ * workspace package the site depends on (path, size and mtime of each file)
914
+ * and, when the fingerprint differs from the previous run, deletes the cache
915
+ * so Vite re-optimizes from the rebuilt packages.
916
+ *
917
+ * Live rebuilds while the server is up are still not hot-invalidated (that
918
+ * needs a watcher feeding Vite's module graph); this closes the "restart and
919
+ * hope you remember the trick" gap.
920
+ */
921
+ const STORE_NAME = ".torpor-dev-cache.json";
922
+ /**
923
+ * Clears site/node_modules/.vite when workspace packages changed since the
924
+ * last dev run. Returns true when the cache was removed.
925
+ */
926
+ function clearStaleDepCache(siteRoot, log) {
927
+ try {
928
+ const workspaceRoot = findWorkspaceRoot(siteRoot);
929
+ if (!workspaceRoot) return false;
930
+ const cacheDir = fpath.join(siteRoot, "node_modules", ".vite");
931
+ const storePath = fpath.join(siteRoot, "node_modules", STORE_NAME);
932
+ const fingerprint = fingerprintWorkspacePackages(siteRoot, workspaceRoot);
933
+ if (fingerprint === void 0) return false;
934
+ const previous = readStore(storePath);
935
+ let cleared = false;
936
+ if (existsSync(cacheDir) && previous !== fingerprint) {
937
+ rmSync(cacheDir, {
938
+ recursive: true,
939
+ force: true
940
+ });
941
+ cleared = true;
942
+ log?.("Workspace packages changed; clearing the Vite dep cache");
943
+ }
944
+ writeFileSync(storePath, JSON.stringify({ fingerprint }));
945
+ return cleared;
946
+ } catch (e) {
947
+ if (process.env.TORPOR_DEBUG_CACHE) throw e;
948
+ return false;
949
+ }
950
+ }
951
+ function findWorkspaceRoot(from) {
952
+ let dir = fpath.resolve(from);
953
+ while (true) {
954
+ if (existsSync(fpath.join(dir, "pnpm-workspace.yaml"))) return dir;
955
+ if (hasWorkspacesField(dir)) return dir;
956
+ const parent = fpath.dirname(dir);
957
+ if (parent === dir) return void 0;
958
+ dir = parent;
959
+ }
960
+ }
961
+ function hasWorkspacesField(dir) {
962
+ const packageJsonPath = fpath.join(dir, "package.json");
963
+ if (!existsSync(packageJsonPath)) return false;
964
+ try {
965
+ const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8"));
966
+ return Array.isArray(pkg.workspaces) && pkg.workspaces.length > 0;
967
+ } catch {
968
+ return false;
969
+ }
970
+ }
971
+ function readStore(storePath) {
972
+ if (!existsSync(storePath)) return void 0;
973
+ try {
974
+ return JSON.parse(readFileSync(storePath, "utf8")).fingerprint;
975
+ } catch {
976
+ return;
977
+ }
978
+ }
979
+ /**
980
+ * Resolves every site dependency that lives inside the workspace to its
981
+ * package folder. Shared with the other dev-startup checks (e.g. the stale
982
+ * .torp copy report), which need the same set of packages.
983
+ */
984
+ function workspaceDependencyDirs(siteRoot, workspaceRoot) {
985
+ const sitePackageJson = fpath.join(siteRoot, "package.json");
986
+ if (!existsSync(sitePackageJson)) return [];
987
+ const pkg = JSON.parse(readFileSync(sitePackageJson, "utf8"));
988
+ const require = createRequire(sitePackageJson);
989
+ const packageDirs = /* @__PURE__ */ new Set();
990
+ for (const name of dependencyNames(pkg)) {
991
+ const dir = resolvePackageDir(name, require);
992
+ if (dir && insideWorkspace(realpathSync(dir), workspaceRoot)) packageDirs.add(realpathSync(dir));
993
+ }
994
+ return [...packageDirs].sort();
995
+ }
996
+ function fingerprintWorkspacePackages(siteRoot, workspaceRoot) {
997
+ if (!existsSync(fpath.join(siteRoot, "package.json"))) return void 0;
998
+ const packageDirs = workspaceDependencyDirs(siteRoot, workspaceRoot);
999
+ if (packageDirs.length === 0) return void 0;
1000
+ const hash = createHash("sha1");
1001
+ for (let dir of packageDirs) {
1002
+ hash.update(`package:${dir}\n`);
1003
+ fingerprintDir(dir, dir, hash);
1004
+ }
1005
+ return hash.digest("hex");
1006
+ }
1007
+ function dependencyNames(pkg) {
1008
+ return [...Object.keys(pkg.dependencies ?? {}), ...Object.keys(pkg.devDependencies ?? {})];
1009
+ }
1010
+ function resolvePackageDir(name, require) {
1011
+ try {
1012
+ const packageJsonPath = require.resolve(`${name}/package.json`);
1013
+ return fpath.dirname(packageJsonPath);
1014
+ } catch {
1015
+ return;
1016
+ }
1017
+ }
1018
+ function insideWorkspace(dir, workspaceRoot) {
1019
+ const relative = fpath.relative(workspaceRoot, dir);
1020
+ return relative === "" || !relative.startsWith("..") && !fpath.isAbsolute(relative);
1021
+ }
1022
+ function fingerprintDir(dir, root, hash) {
1023
+ for (let entry of readdirSync(dir, { withFileTypes: true })) {
1024
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
1025
+ const full = fpath.join(dir, entry.name);
1026
+ if (entry.isDirectory()) {
1027
+ fingerprintDir(full, root, hash);
1028
+ continue;
1029
+ }
1030
+ if (!entry.isFile()) continue;
1031
+ const stats = statSync(full);
1032
+ hash.update(`file:${fpath.relative(root, full).replaceAll("\\", "/")}:${stats.size}:${Math.floor(stats.mtimeMs)}\n`);
1033
+ }
1034
+ }
1035
+ //#endregion
1036
+ //#region src/run/staleTorpCopies.ts
1037
+ /**
1038
+ * Workspace packages that ship copies of their .torp components (e.g.
1039
+ * @torpor/ui copies its src .torp files into dist/ during its build) can go
1040
+ * stale: a site resolves the package to its dist folder, so edits to src
1041
+ * silently keep serving the OLD components until the package is rebuilt.
1042
+ * The symptom is confusing -- e.g. a TypeError about a renamed slot prop
1043
+ * after a component's API changed.
1044
+ *
1045
+ * reportStaleTorpCopies runs at `tb --dev` startup: for every workspace
1046
+ * dependency with both src and dist folders, it compares the .torp files and
1047
+ * reports anything missing from dist, differing from src, or left over in
1048
+ * dist without a src file.
1049
+ */
1050
+ /**
1051
+ * Reports stale dist .torp copies in the site's workspace dependencies.
1052
+ * Never throws -- a broken check must not break dev startup.
1053
+ */
1054
+ function reportStaleTorpCopies(siteRoot, log) {
1055
+ try {
1056
+ const workspaceRoot = findWorkspaceRoot(siteRoot);
1057
+ if (!workspaceRoot) return;
1058
+ for (const dir of workspaceDependencyDirs(siteRoot, workspaceRoot)) {
1059
+ const srcFolder = fpath.join(dir, "src");
1060
+ const distFolder = fpath.join(dir, "dist");
1061
+ if (!existsSync(srcFolder) || !existsSync(distFolder)) continue;
1062
+ const src = collectTorpFiles(srcFolder);
1063
+ const dist = collectTorpFiles(distFolder);
1064
+ if (src.size === 0 && dist.size === 0) continue;
1065
+ const problems = [];
1066
+ for (const [rel, content] of src) {
1067
+ const distContent = dist.get(rel);
1068
+ if (distContent === void 0) problems.push(`${rel} (not copied to dist)`);
1069
+ else if (distContent !== content) problems.push(`${rel} (differs from src)`);
1070
+ }
1071
+ for (const rel of dist.keys()) if (!src.has(rel)) problems.push(`${rel} (left over in dist, no src file)`);
1072
+ if (problems.length === 0) continue;
1073
+ const shown = problems.slice(0, 5).map((p) => ` ${p}`).join("\n");
1074
+ const more = problems.length > 5 ? `\n ... and ${problems.length - 5} more` : "";
1075
+ log?.(`${packageLabel(dir, workspaceRoot)}'s dist .torp copies are stale, so the site is serving OLD components:\n${shown}${more}\nRebuild the package (e.g. \`pnpm build\` in ${fpath.relative(workspaceRoot, dir) || "."}) and restart \`tb --dev\`.`);
1076
+ }
1077
+ } catch (e) {
1078
+ if (process.env.TORPOR_DEBUG_CACHE) throw e;
1079
+ }
1080
+ }
1081
+ function collectTorpFiles(folder) {
1082
+ const files = /* @__PURE__ */ new Map();
1083
+ collect(folder, folder, files);
1084
+ return files;
1085
+ }
1086
+ function collect(folder, root, files) {
1087
+ for (const entry of readdirSync(folder, { withFileTypes: true })) {
1088
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
1089
+ const full = fpath.join(folder, entry.name);
1090
+ if (entry.isDirectory()) {
1091
+ collect(full, root, files);
1092
+ continue;
1093
+ }
1094
+ if (!entry.isFile() || !entry.name.endsWith(".torp")) continue;
1095
+ files.set(fpath.relative(root, full).replaceAll("\\", "/"), readFileSync(full, "utf8"));
1096
+ }
1097
+ }
1098
+ function packageLabel(dir, workspaceRoot) {
1099
+ try {
1100
+ const pkg = JSON.parse(readFileSync(fpath.join(dir, "package.json"), "utf8"));
1101
+ if (pkg.name) return pkg.name;
1102
+ } catch {}
1103
+ return fpath.relative(workspaceRoot, dir) || dir;
1104
+ }
1105
+ //#endregion
1106
+ //#region src/run/runDev.ts
1107
+ async function runDev(site) {
1108
+ const config = structuredClone(site.viteConfig ?? {});
1109
+ config.appType = "custom";
1110
+ config.server ??= {};
1111
+ config.server.host ??= "localhost";
1112
+ config.server.port ??= 7059;
1113
+ config.resolve ??= {};
1114
+ config.resolve.tsconfigPaths ??= true;
1115
+ const sourceMode = detectSourceMode(site.root);
1116
+ if (sourceMode) {
1117
+ config.resolve.conditions ??= [];
1118
+ config.resolve.conditions.push("development");
1119
+ }
1120
+ config.resolve.alias = [...asAliasArray(config.resolve.alias), ...tsconfigAliases(site.root)];
1121
+ const adapterDev = site.adapter.dev?.(site);
1122
+ const devPlugins = normalizePlugins(adapterDev ?? devPlugin(site, sourceMode));
1123
+ config.plugins = [
1124
+ manifest(site, true),
1125
+ torpor({ dev: true }),
1126
+ ...devPlugins,
1127
+ ...site.vitePlugins
1128
+ ];
1129
+ config.optimizeDeps ??= {};
1130
+ config.optimizeDeps.extensions ??= [];
1131
+ config.optimizeDeps.extensions.push(".torp");
1132
+ config.optimizeDeps.rolldownOptions ??= {};
1133
+ config.optimizeDeps.rolldownOptions.plugins ??= [estorpor()];
1134
+ addTorporPackageConfig(site.root, config);
1135
+ clearStaleDepCache(site.root, (message) => console.log(`\n${message}`));
1136
+ reportStaleTorpCopies(site.root, (message) => console.log(`\n${message}`));
1137
+ configDotenv();
1138
+ process.env.PROTOCOL ??= "http:";
1139
+ process.env.HOST ??= "localhost";
1140
+ process.env.PORT ??= "7059";
1141
+ const connectingUrl = `${process.env.PROTOCOL}//${process.env.HOST}:${process.env.PORT}`;
1142
+ console.log(`\nConnecting to ${connectingUrl}`);
1143
+ const vite = await createServer(config);
1144
+ await vite.listen();
1145
+ watchRouteTypes(site, vite);
1146
+ const listeningUrl = vite.resolvedUrls?.local?.[0] ?? connectingUrl;
1147
+ console.log(`Listening on ${listeningUrl}\n`);
1148
+ }
1149
+ /**
1150
+ * Reports route type issues at startup, then re-checks route files and
1151
+ * `makeApi` calls as they change so annotation problems surface during
1152
+ * development without failing the dev server.
1153
+ */
1154
+ function watchRouteTypes(site, vite) {
1155
+ reportRouteIssues(checkRoutes(site));
1156
+ reportRouteIssues(checkApiCalls(site));
1157
+ reportRouteIssues(checkLayoutSlots(site));
1158
+ const routeFiles = new Map(site.routes.filter((r) => r.file?.endsWith(".ts") || r.file?.endsWith(".torp") && r.type === 4).map((r) => [fpath.resolve(site.root, r.file), r]));
1159
+ const apiCheck = createApiCallCheck(site);
1160
+ const reported = /* @__PURE__ */ new Map();
1161
+ const report = (file, issues) => {
1162
+ const key = JSON.stringify(issues);
1163
+ if (reported.get(file) === key) return;
1164
+ reported.set(file, key);
1165
+ reportRouteIssues(issues);
1166
+ };
1167
+ const recheck = (file) => {
1168
+ const route = routeFiles.get(file);
1169
+ if (route) {
1170
+ report(file, route.type === 4 ? checkLayoutSlot(site, route) : checkRoute(site, route));
1171
+ return;
1172
+ }
1173
+ if (!/\.(ts|js|torp)$/.test(file)) return;
1174
+ let source;
1175
+ try {
1176
+ source = readFileSync(file, "utf8");
1177
+ } catch {
1178
+ return;
1179
+ }
1180
+ report(file, checkApiCallSource(source, file, apiCheck));
1181
+ };
1182
+ vite.watcher.on("change", recheck);
1183
+ vite.watcher.on("add", recheck);
1184
+ }
1185
+ function normalizePlugins(plugins) {
1186
+ if (!plugins) return [];
1187
+ return Array.isArray(plugins) ? plugins : [plugins];
1188
+ }
1189
+ function asAliasArray(alias) {
1190
+ if (!alias) return [];
1191
+ if (Array.isArray(alias)) return alias;
1192
+ return Object.entries(alias).map(([find, replacement]) => ({
1193
+ find,
1194
+ replacement
1195
+ }));
1196
+ }
1197
+ //#endregion
1198
+ //#region src/server/contentType.ts
1199
+ function contentType(extension) {
1200
+ return types[extension] || "application/octet-stream";
1201
+ }
1202
+ const types = {
1203
+ ".aac": "audio/aac",
1204
+ ".abw": "application/x-abiword",
1205
+ ".apng": "image/apng",
1206
+ ".arc": "application/x-freearc",
1207
+ ".avif": "image/avif",
1208
+ ".avi": "video/x-msvideo",
1209
+ ".azw": "application/vnd.amazon.ebook",
1210
+ ".bin": "application/octet-stream",
1211
+ ".bmp": "image/bmp",
1212
+ ".bz": "application/x-bzip",
1213
+ ".bz2": "application/x-bzip2",
1214
+ ".cda": "application/x-cdf",
1215
+ ".csh": "application/x-csh",
1216
+ ".css": "text/css",
1217
+ ".csv": "text/csv",
1218
+ ".doc": "application/msword",
1219
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1220
+ ".eot": "application/vnd.ms-fontobject",
1221
+ ".epub": "application/epub+zip",
1222
+ ".gz": "application/gzip",
1223
+ ".gif": "image/gif",
1224
+ ".htm": "text/html",
1225
+ ".html": "text/html",
1226
+ ".ico": "image/vnd.microsoft.icon",
1227
+ ".ics": "text/calendar",
1228
+ ".jar": "application/java-archive",
1229
+ ".jpeg": "image/jpeg",
1230
+ ".jpg": "image/jpeg",
1231
+ ".js": "text/javascript",
1232
+ ".json": "application/json",
1233
+ ".jsonld": "application/ld+json",
1234
+ ".mid": "audio/midi",
1235
+ ".midi": "audio/midi",
1236
+ ".mjs": "text/javascript",
1237
+ ".mp3": "audio/mpeg",
1238
+ ".mp4": "video/mp4",
1239
+ ".mpeg": "video/mpeg",
1240
+ ".mpkg": "application/vnd.apple.installer+xml",
1241
+ ".odp": "application/vnd.oasis.opendocument.presentation",
1242
+ ".ods": "application/vnd.oasis.opendocument.spreadsheet",
1243
+ ".odt": "application/vnd.oasis.opendocument.text",
1244
+ ".oga": "audio/ogg",
1245
+ ".ogv": "video/ogg",
1246
+ ".ogx": "application/ogg",
1247
+ ".opus": "audio/ogg",
1248
+ ".otf": "font/otf",
1249
+ ".png": "image/png",
1250
+ ".pdf": "application/pdf",
1251
+ ".php": "application/x-httpd-php",
1252
+ ".ppt": "application/vnd.ms-powerpoint",
1253
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1254
+ ".rar": "application/vnd.rar",
1255
+ ".rtf": "application/rtf",
1256
+ ".sh": "application/x-sh",
1257
+ ".svg": "image/svg+xml",
1258
+ ".tar": "application/x-tar",
1259
+ ".tif": "image/tiff",
1260
+ ".tiff": "image/tiff",
1261
+ ".ts": "video/mp2t",
1262
+ ".ttf": "font/ttf",
1263
+ ".txt": "text/plain",
1264
+ ".vsd": "application/vnd.visio",
1265
+ ".wav": "audio/wav",
1266
+ ".weba": "audio/webm",
1267
+ ".webm": "video/webm",
1268
+ ".webp": "image/webp",
1269
+ ".woff": "font/woff",
1270
+ ".woff2": "font/woff2",
1271
+ ".xhtml": "application/xhtml+xml",
1272
+ ".xls": "application/vnd.ms-excel",
1273
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1274
+ ".xml": "application/xml",
1275
+ ".xul": "application/vnd.mozilla.xul+xml",
1276
+ ".zip": "application/zip",
1277
+ ".3gp": "video/3gpp",
1278
+ ".3g2": "video/3gpp2",
1279
+ ".7z": "application/x-7z-compressed"
1280
+ };
1281
+ //#endregion
1282
+ //#region src/run/runPreview.ts
1283
+ async function runPreview(site) {
1284
+ await runBuild(site);
1285
+ const distFolder = fpath.resolve(site.root, "dist");
1286
+ const clientFolder = fpath.join(distFolder, "client");
1287
+ const serverFolder = fpath.join(distFolder, "server");
1288
+ let template;
1289
+ const siteHtml = fpath.join(clientFolder, "site.html");
1290
+ if (existsSync(siteHtml)) {
1291
+ let clientScript = (await promises.readdir(fpath.join(clientFolder, "assets"))).find((f) => f.startsWith("clientEntry-"));
1292
+ if (!clientScript) throw new Error("clientEntry.js not found");
1293
+ clientScript = `/assets/${clientScript}`;
1294
+ template = await promises.readFile(siteHtml, "utf-8");
1295
+ template = prepareTemplate(template, clientScript);
1296
+ }
1297
+ const server = new Server();
1298
+ server.add("/assets/*", async (ev) => {
1299
+ try {
1300
+ const url = new URL(ev.request.url);
1301
+ const file = fpath.join(site.root, "dist", "client", url.pathname);
1302
+ if (existsSync(file)) {
1303
+ const body = await promises.readFile(file);
1304
+ return new Response(body, {
1305
+ status: 200,
1306
+ headers: { "Content-Type": contentType(fpath.extname(file)) }
1307
+ });
1308
+ }
1309
+ } catch (e) {
1310
+ return serverError(e);
1311
+ }
1312
+ });
1313
+ const serverScript = fpath.resolve(serverFolder, "serverEntry.js");
1314
+ server.add("*", async (ev) => {
1315
+ try {
1316
+ const { load } = await import(serverScript);
1317
+ return await load(ev, template);
1318
+ } catch (e) {
1319
+ return serverError(e);
1320
+ }
1321
+ });
1322
+ configDotenv();
1323
+ process.env.PROTOCOL ??= "http:";
1324
+ process.env.HOST ??= "localhost";
1325
+ process.env.PORT ??= "7059";
1326
+ site.adapter.serve(server, site);
1327
+ }
1328
+ //#endregion
1329
+ //#region src/run/run.ts
1330
+ /**
1331
+ * Looks for and loads a site.config.js/ts file in the working directory,
1332
+ * running the site's plugins. The Vite server used to load the config is
1333
+ * returned as well, so that (for example) route files can be loaded through
1334
+ * it; callers are responsible for closing it.
1335
+ */
1336
+ async function loadSite(folder) {
1337
+ const jsConfigFile = fpath.join(folder, "site.config.js");
1338
+ const tsConfigFile = fpath.join(folder, "site.config.ts");
1339
+ let configFile = "";
1340
+ if (fs.existsSync(jsConfigFile)) configFile = jsConfigFile;
1341
+ else if (fs.existsSync(tsConfigFile)) configFile = tsConfigFile;
1342
+ else throw new Error("site.config file not found");
1343
+ const vite = await createServer({
1344
+ server: { middlewareMode: true },
1345
+ appType: "custom",
1346
+ resolve: { tsconfigPaths: true },
1347
+ optimizeDeps: { noDiscovery: true }
1348
+ });
1349
+ try {
1350
+ const site = (await vite.ssrLoadModule(configFile)).default;
1351
+ site.configFile = configFile;
1352
+ if (!site || !site.root || !site.routes) throw new Error("Invalid site in config file");
1353
+ for (const plugin of site.plugins) await plugin(site);
1354
+ return {
1355
+ site,
1356
+ vite
1357
+ };
1358
+ } catch (error) {
1359
+ await vite.close();
1360
+ throw error;
1361
+ }
1362
+ }
1363
+ async function run(folder, mode) {
1364
+ const { site, vite } = await loadSite(folder);
1365
+ try {
1366
+ switch (mode) {
1367
+ case "dev":
1368
+ await runDev(site);
1369
+ break;
1370
+ case "build":
1371
+ await runBuild(site);
1372
+ break;
1373
+ case "preview":
1374
+ await runPreview(site);
1375
+ break;
1376
+ default:
1377
+ console.log("No mode passed, running in dev");
1378
+ await runDev(site);
1379
+ break;
1380
+ }
1381
+ } finally {
1382
+ await vite.close();
1383
+ }
1384
+ }
1385
+ //#endregion
1386
+ //#region src/run/runOpenApi.ts
1387
+ /**
1388
+ * Writes an OpenAPI document for the site's +server endpoints to a file.
1389
+ * Route files are loaded through the supplied Vite server, so this must be
1390
+ * called while the server from `loadSite` is still open.
1391
+ */
1392
+ async function runOpenApi(site, vite, outFile = "openapi.json") {
1393
+ const options = site.pluginState.get(OPEN_API_STATE_KEY);
1394
+ if (!options) throw new Error("No OpenAPI options found. Add the openApi() plugin to site.plugins to use \"tb --openapi\"");
1395
+ const entries = [];
1396
+ for (const route of site.routes) {
1397
+ if (route.type !== 3) continue;
1398
+ if (options.docs && route.path === options.docs) continue;
1399
+ if (route.endPoint && !route.file) entries.push({
1400
+ path: route.path,
1401
+ endPoint: route.endPoint
1402
+ });
1403
+ else if (route.file) {
1404
+ const file = fpath.resolve(site.root, route.file);
1405
+ const mod = await vite.ssrLoadModule(file);
1406
+ entries.push({
1407
+ path: route.path,
1408
+ endPoint: mod.default
1409
+ });
1410
+ }
1411
+ }
1412
+ const doc = buildOpenApiDocument(entries, options);
1413
+ await writeFile(fpath.resolve(site.root, outFile), JSON.stringify(doc, null, 2) + "\n");
1414
+ console.log(`OpenAPI document written to ${outFile} (${Object.keys(doc.paths ?? {}).length} paths)`);
1415
+ }
1416
+ //#endregion
1417
+ export { runDev as a, runPreview as i, loadSite as n, runBuild as o, run as r, runOpenApi as t };
1418
+
1419
+ //# sourceMappingURL=runOpenApi-DlOgF2Il.mjs.map