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