@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.
- package/README.md +20 -2
- package/dist/Jsonify-Dg6gChd3.d.mts +10 -0
- package/dist/Jsonify-Dg6gChd3.d.mts.map +1 -0
- package/dist/ParseRouteParams-DBmqlmLh.d.mts +42 -0
- package/dist/ParseRouteParams-DBmqlmLh.d.mts.map +1 -0
- package/dist/{RouteType-B6BMOyYw.mjs → Router-C4ADUfhv.mjs} +30 -42
- package/dist/Router-C4ADUfhv.mjs.map +1 -0
- package/dist/{Server-CF5wDJp6.d.mts → Server-C_IKav_e.d.mts} +44 -16
- package/dist/Server-C_IKav_e.d.mts.map +1 -0
- package/dist/{ServerEvent-CxPwA4lH.mjs → ServerEvent-Dljn5i7A.mjs} +37 -11
- package/dist/ServerEvent-Dljn5i7A.mjs.map +1 -0
- package/dist/Site-CMa153FA.d.mts +565 -0
- package/dist/Site-CMa153FA.d.mts.map +1 -0
- package/dist/StandardSchema-D28L8_zZ.d.mts +100 -0
- package/dist/StandardSchema-D28L8_zZ.d.mts.map +1 -0
- package/dist/TypedResponse-6lznqN4U.d.mts +22 -0
- package/dist/TypedResponse-6lznqN4U.d.mts.map +1 -0
- package/dist/{_page-BQs4WvCO.mjs → _page-4cQKs_Dn.mjs} +3 -5
- package/dist/_page-4cQKs_Dn.mjs.map +1 -0
- package/dist/bin/index.d.mts +1 -1
- package/dist/bin/index.js +15 -11
- package/dist/bin/index.js.map +1 -1
- package/dist/flattenHeaders-C_YYLdOq.mjs +114 -0
- package/dist/flattenHeaders-C_YYLdOq.mjs.map +1 -0
- package/dist/form.d.mts +35 -0
- package/dist/form.d.mts.map +1 -0
- package/dist/form.mjs +48 -0
- package/dist/form.mjs.map +1 -0
- package/dist/index.d.mts +72 -156
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +158 -26
- package/dist/index.mjs.map +1 -1
- package/dist/nav.d.mts +58 -5
- package/dist/nav.d.mts.map +1 -1
- package/dist/nav.mjs +116 -38
- package/dist/nav.mjs.map +1 -1
- package/dist/openapi.d.mts +144 -0
- package/dist/openapi.d.mts.map +1 -0
- package/dist/openapi.mjs +2 -0
- package/dist/pathTrie-D4Ax2Mu9.mjs +160 -0
- package/dist/pathTrie-D4Ax2Mu9.mjs.map +1 -0
- package/dist/plugin-D8Www11B.mjs +202 -0
- package/dist/plugin-D8Www11B.mjs.map +1 -0
- package/dist/{response-C5TtAsh1.mjs → response-Bg4w2S1q.mjs} +16 -39
- package/dist/response-Bg4w2S1q.mjs.map +1 -0
- package/dist/response.d.mts +309 -289
- package/dist/response.d.mts.map +1 -1
- package/dist/response.mjs +3 -4
- package/dist/run.d.mts +11 -4
- package/dist/run.d.mts.map +1 -1
- package/dist/run.mjs +2 -8
- package/dist/runOpenApi-BUi1RH1l.mjs +1381 -0
- package/dist/runOpenApi-BUi1RH1l.mjs.map +1 -0
- package/dist/schema.d.mts +32 -0
- package/dist/schema.d.mts.map +1 -0
- package/dist/schema.mjs +2 -0
- package/dist/server.d.mts +10 -8
- package/dist/server.d.mts.map +1 -1
- package/dist/server.mjs +132 -4
- package/dist/server.mjs.map +1 -0
- package/dist/state.d.mts.map +1 -1
- package/dist/state.mjs +2 -3
- package/dist/test.d.mts +8 -10
- package/dist/test.d.mts.map +1 -1
- package/dist/test.mjs +283 -79
- package/dist/test.mjs.map +1 -1
- package/dist/{seeOther-B4Yhu9iq.mjs → unprocessable-DAZbrDeR.mjs} +34 -13
- package/dist/unprocessable-DAZbrDeR.mjs.map +1 -0
- package/dist/validate-Ok4krlkX.mjs +40 -0
- package/dist/validate-Ok4krlkX.mjs.map +1 -0
- package/package.json +28 -17
- package/src/bin/index.ts +13 -2
- package/src/dev.ts +10 -0
- package/src/form/formDataToRecord.ts +23 -0
- package/src/form/readForm.ts +71 -0
- package/src/form.ts +4 -0
- package/src/index.ts +53 -1
- package/src/nav/api.test-d.ts +79 -0
- package/src/nav/api.ts +104 -0
- package/src/nav/formSubmit.ts +18 -4
- package/src/nav/navigate.ts +40 -15
- package/src/nav/route.ts +38 -0
- package/src/nav.ts +3 -1
- package/src/openapi/docsHtml.ts +25 -0
- package/src/openapi/document.ts +154 -0
- package/src/openapi/plugin.ts +78 -0
- package/src/openapi/types.ts +82 -0
- package/src/openapi.ts +14 -0
- package/src/response/TypedResponse.ts +17 -0
- package/src/response/badRequest.ts +14 -2
- package/src/response/created.ts +13 -2
- package/src/response/found.ts +2 -2
- package/src/response/movedPermanently.ts +2 -2
- package/src/response/notModified.ts +2 -2
- package/src/response/ok.ts +13 -4
- package/src/response/response.ts +4 -4
- package/src/response/unprocessable.ts +14 -2
- package/src/run/depCache.ts +175 -0
- package/src/run/devPlugin.ts +128 -0
- package/src/run/prepareTemplate.ts +6 -3
- package/src/run/run.ts +63 -39
- package/src/run/runBuild.ts +63 -12
- package/src/run/runDev.ts +146 -58
- package/src/run/runOpenApi.ts +52 -0
- package/src/run/runPreview.ts +17 -26
- package/src/run/staleTorpCopies.ts +100 -0
- package/src/run.ts +2 -1
- package/src/schema.ts +7 -0
- package/src/server/CookieHelper.ts +17 -7
- package/src/server/Server.ts +54 -30
- package/src/server/ServerEvent.ts +24 -1
- package/src/server/connect/connectMiddleware.ts +44 -40
- package/src/server/connect/flattenHeaders.ts +1 -23
- package/src/server/connect/requestToNodeMessage.ts +6 -3
- package/src/server/types/MiddlewareFunction.ts +18 -4
- package/src/site/Router.ts +52 -41
- package/src/site/Site.ts +184 -10
- package/src/site/checkLayoutSlots.ts +114 -0
- package/src/site/checkRoutes.ts +435 -0
- package/src/site/clientEntry.ts +22 -9
- package/src/site/layoutSlots.ts +53 -0
- package/src/site/manifest.ts +125 -6
- package/src/site/serverEntry.ts +341 -132
- package/src/state/$page.ts +2 -1
- package/src/state/$serverPage.ts +22 -0
- package/src/test/runTest.ts +306 -118
- package/src/types/Adapter.ts +12 -0
- package/src/types/Jsonify.ts +20 -0
- package/src/types/PageData.test-d.ts +136 -0
- package/src/types/PageData.ts +35 -0
- package/src/types/PageEndPoint.ts +25 -9
- package/src/types/PageForm.test-d.ts +78 -0
- package/src/types/PageForm.ts +29 -0
- package/src/types/PageLoadEvent.ts +16 -4
- package/src/types/PageLoadReturn.ts +13 -0
- package/src/types/PageProps.ts +14 -0
- package/src/types/PageServerAction.ts +8 -2
- package/src/types/PageServerEndPoint.test-d.ts +105 -0
- package/src/types/PageServerEndPoint.ts +99 -5
- package/src/types/PageServerLoad.ts +11 -3
- package/src/types/ParseRouteParams.test-d.ts +84 -0
- package/src/types/ParseRouteParams.ts +64 -0
- package/src/types/Route.ts +20 -1
- package/src/types/RouteHandler.ts +8 -1
- package/src/types/ServerEndPoint.test-d.ts +77 -0
- package/src/types/ServerEndPoint.ts +148 -16
- package/src/types/ServerHook.ts +10 -3
- package/src/types/ServerLoadEvent.ts +48 -3
- package/src/types/ServerRequest.ts +10 -2
- package/src/types/SitePlugin.ts +24 -0
- package/src/types/StandardSchema.ts +109 -0
- package/src/utils/pathToRegex.ts +4 -2
- package/src/utils/pathTrie.ts +182 -0
- package/src/utils/searchParamsToRecord.ts +18 -0
- package/src/utils/torporPackages.ts +150 -0
- package/src/utils/tsconfigAliases.ts +90 -0
- package/src/validation/ValidationError.ts +18 -0
- package/src/validation/endpoint.ts +83 -0
- package/src/validation/validate.ts +26 -0
- package/dist/RouteType-B6BMOyYw.mjs.map +0 -1
- package/dist/Server-CF5wDJp6.d.mts.map +0 -1
- package/dist/ServerEvent-CxPwA4lH.mjs.map +0 -1
- package/dist/Site-DgC6WWn1.d.mts +0 -78
- package/dist/Site-DgC6WWn1.d.mts.map +0 -1
- package/dist/_page-BQs4WvCO.mjs.map +0 -1
- package/dist/connectMiddleware-D7nehjj3.mjs +0 -252
- package/dist/connectMiddleware-D7nehjj3.mjs.map +0 -1
- package/dist/pathToRegex-TUAMc3zS.mjs +0 -11
- package/dist/pathToRegex-TUAMc3zS.mjs.map +0 -1
- package/dist/response-C5TtAsh1.mjs.map +0 -1
- package/dist/run-BPFtSYA3.mjs +0 -332
- package/dist/run-BPFtSYA3.mjs.map +0 -1
- package/dist/seeOther-B4Yhu9iq.mjs.map +0 -1
- package/src/server/Routerx.ts +0 -72
- package/src/server/connect/bufferToArrayBuffer.ts +0 -8
- 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
|