@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
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import response from "./response";
|
|
2
|
+
import type { Jsonify } from "../types/Jsonify";
|
|
3
|
+
import type TypedResponse from "./TypedResponse";
|
|
4
|
+
|
|
5
|
+
type UnprocessableResponse<T extends object | string | undefined> = T extends object
|
|
6
|
+
? TypedResponse<Jsonify<T>>
|
|
7
|
+
: Response;
|
|
2
8
|
|
|
3
9
|
/**
|
|
4
10
|
* 422 Unprocessable Content
|
|
@@ -11,8 +17,14 @@ import response from "./response";
|
|
|
11
17
|
* Clients that receive a 422 response should expect that repeating the request
|
|
12
18
|
* without modification will fail with the same error.
|
|
13
19
|
*
|
|
20
|
+
* An object body is typed: it becomes the page's `$props.form` (with its JSON
|
|
21
|
+
* form) after a form submit, so validation errors can be surfaced with their
|
|
22
|
+
* field types via `PageForm`.
|
|
23
|
+
*
|
|
14
24
|
* See https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/422
|
|
15
25
|
*/
|
|
16
|
-
export default function unprocessable
|
|
17
|
-
|
|
26
|
+
export default function unprocessable<T extends object | string | undefined>(
|
|
27
|
+
body?: T,
|
|
28
|
+
): UnprocessableResponse<T> {
|
|
29
|
+
return response(422, body ?? "Unprocessable") as UnprocessableResponse<T>;
|
|
18
30
|
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
realpathSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
statSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { createRequire } from "node:module";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import type { PackageJson } from "../utils/torporPackages";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Vite's dep-optimization cache (node_modules/.vite) keys off the lockfile
|
|
17
|
+
* and config, but not the content of workspace packages. When a workspace
|
|
18
|
+
* package is rebuilt while the cache holds prebundled copies of it, dev SSR
|
|
19
|
+
* keeps executing the stale modules -- producing bizarre errors (missing
|
|
20
|
+
* runtime identifiers, "$cache must be used in a getter", ...) that only a
|
|
21
|
+
* manual cache delete fixes.
|
|
22
|
+
*
|
|
23
|
+
* clearStaleDepCache runs at `tb --dev` startup: it fingerprints every
|
|
24
|
+
* workspace package the site depends on (path, size and mtime of each file)
|
|
25
|
+
* and, when the fingerprint differs from the previous run, deletes the cache
|
|
26
|
+
* so Vite re-optimizes from the rebuilt packages.
|
|
27
|
+
*
|
|
28
|
+
* Live rebuilds while the server is up are still not hot-invalidated (that
|
|
29
|
+
* needs a watcher feeding Vite's module graph); this closes the "restart and
|
|
30
|
+
* hope you remember the trick" gap.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const STORE_NAME = ".torpor-dev-cache.json";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Clears site/node_modules/.vite when workspace packages changed since the
|
|
37
|
+
* last dev run. Returns true when the cache was removed.
|
|
38
|
+
*/
|
|
39
|
+
export function clearStaleDepCache(siteRoot: string, log?: (message: string) => void): boolean {
|
|
40
|
+
try {
|
|
41
|
+
const workspaceRoot = findWorkspaceRoot(siteRoot);
|
|
42
|
+
if (!workspaceRoot) return false;
|
|
43
|
+
|
|
44
|
+
const cacheDir = path.join(siteRoot, "node_modules", ".vite");
|
|
45
|
+
const storePath = path.join(siteRoot, "node_modules", STORE_NAME);
|
|
46
|
+
|
|
47
|
+
const fingerprint = fingerprintWorkspacePackages(siteRoot, workspaceRoot);
|
|
48
|
+
if (fingerprint === undefined) return false;
|
|
49
|
+
|
|
50
|
+
const previous = readStore(storePath);
|
|
51
|
+
|
|
52
|
+
let cleared = false;
|
|
53
|
+
if (existsSync(cacheDir) && previous !== fingerprint) {
|
|
54
|
+
rmSync(cacheDir, { recursive: true, force: true });
|
|
55
|
+
cleared = true;
|
|
56
|
+
log?.("Workspace packages changed; clearing the Vite dep cache");
|
|
57
|
+
}
|
|
58
|
+
writeFileSync(storePath, JSON.stringify({ fingerprint }));
|
|
59
|
+
|
|
60
|
+
return cleared;
|
|
61
|
+
} catch (e) {
|
|
62
|
+
if (process.env.TORPOR_DEBUG_CACHE) throw e;
|
|
63
|
+
// Never break dev startup over cache bookkeeping
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function findWorkspaceRoot(from: string): string | undefined {
|
|
69
|
+
let dir = path.resolve(from);
|
|
70
|
+
while (true) {
|
|
71
|
+
// pnpm-style marker, or a root package.json declaring npm workspaces
|
|
72
|
+
if (existsSync(path.join(dir, "pnpm-workspace.yaml"))) {
|
|
73
|
+
return dir;
|
|
74
|
+
}
|
|
75
|
+
if (hasWorkspacesField(dir)) {
|
|
76
|
+
return dir;
|
|
77
|
+
}
|
|
78
|
+
const parent = path.dirname(dir);
|
|
79
|
+
if (parent === dir) return undefined;
|
|
80
|
+
dir = parent;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function hasWorkspacesField(dir: string): boolean {
|
|
85
|
+
const packageJsonPath = path.join(dir, "package.json");
|
|
86
|
+
if (!existsSync(packageJsonPath)) return false;
|
|
87
|
+
try {
|
|
88
|
+
const pkg: PackageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
89
|
+
return Array.isArray(pkg.workspaces) && pkg.workspaces.length > 0;
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function readStore(storePath: string): string | undefined {
|
|
96
|
+
if (!existsSync(storePath)) return undefined;
|
|
97
|
+
try {
|
|
98
|
+
const store = JSON.parse(readFileSync(storePath, "utf8")) as { fingerprint?: string };
|
|
99
|
+
return store.fingerprint;
|
|
100
|
+
} catch {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Resolves every site dependency that lives inside the workspace to its
|
|
107
|
+
* package folder. Shared with the other dev-startup checks (e.g. the stale
|
|
108
|
+
* .torp copy report), which need the same set of packages.
|
|
109
|
+
*/
|
|
110
|
+
export function workspaceDependencyDirs(siteRoot: string, workspaceRoot: string): string[] {
|
|
111
|
+
const sitePackageJson = path.join(siteRoot, "package.json");
|
|
112
|
+
if (!existsSync(sitePackageJson)) return [];
|
|
113
|
+
|
|
114
|
+
const pkg: PackageJson = JSON.parse(readFileSync(sitePackageJson, "utf8"));
|
|
115
|
+
const require = createRequire(sitePackageJson);
|
|
116
|
+
|
|
117
|
+
const packageDirs = new Set<string>();
|
|
118
|
+
for (const name of dependencyNames(pkg)) {
|
|
119
|
+
const dir = resolvePackageDir(name, require);
|
|
120
|
+
if (dir && insideWorkspace(realpathSync(dir), workspaceRoot)) {
|
|
121
|
+
packageDirs.add(realpathSync(dir));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return [...packageDirs].sort();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function fingerprintWorkspacePackages(siteRoot: string, workspaceRoot: string): string | undefined {
|
|
128
|
+
const sitePackageJson = path.join(siteRoot, "package.json");
|
|
129
|
+
if (!existsSync(sitePackageJson)) return undefined;
|
|
130
|
+
|
|
131
|
+
const packageDirs = workspaceDependencyDirs(siteRoot, workspaceRoot);
|
|
132
|
+
if (packageDirs.length === 0) return undefined;
|
|
133
|
+
|
|
134
|
+
const hash = createHash("sha1");
|
|
135
|
+
for (let dir of packageDirs) {
|
|
136
|
+
hash.update(`package:${dir}\n`);
|
|
137
|
+
fingerprintDir(dir, dir, hash);
|
|
138
|
+
}
|
|
139
|
+
return hash.digest("hex");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function dependencyNames(pkg: PackageJson): string[] {
|
|
143
|
+
return [...Object.keys(pkg.dependencies ?? {}), ...Object.keys(pkg.devDependencies ?? {})];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function resolvePackageDir(name: string, require: NodeRequire): string | undefined {
|
|
147
|
+
try {
|
|
148
|
+
const packageJsonPath = require.resolve(`${name}/package.json`);
|
|
149
|
+
return path.dirname(packageJsonPath);
|
|
150
|
+
} catch {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function insideWorkspace(dir: string, workspaceRoot: string): boolean {
|
|
156
|
+
const relative = path.relative(workspaceRoot, dir);
|
|
157
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function fingerprintDir(dir: string, root: string, hash: import("node:crypto").Hash): void {
|
|
161
|
+
for (let entry of readdirSync(dir, { withFileTypes: true })) {
|
|
162
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
163
|
+
const full = path.join(dir, entry.name);
|
|
164
|
+
if (entry.isDirectory()) {
|
|
165
|
+
fingerprintDir(full, root, hash);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!entry.isFile()) continue;
|
|
169
|
+
const stats = statSync(full);
|
|
170
|
+
// NOTE: hash.update takes (data, encoding) -- join into one string
|
|
171
|
+
hash.update(
|
|
172
|
+
`file:${path.relative(root, full).replaceAll("\\", "/")}:${stats.size}:${Math.floor(stats.mtimeMs)}\n`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { type IncomingMessage, type ServerResponse } from "node:http";
|
|
2
|
+
import { existsSync, promises as fs } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Readable } from "node:stream";
|
|
5
|
+
import { type Plugin, type ViteDevServer } from "vite";
|
|
6
|
+
import { serverError } from "../response.ts";
|
|
7
|
+
import Server from "../server/Server.ts";
|
|
8
|
+
import flattenHeaders from "../server/connect/flattenHeaders.ts";
|
|
9
|
+
import type Site from "../site/Site.ts";
|
|
10
|
+
import prepareTemplate from "./prepareTemplate.ts";
|
|
11
|
+
|
|
12
|
+
type ReqHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The default Node-runtime dev plugin. Reads `src/site.html`, sets up a torpor
|
|
16
|
+
* `Server` whose catch-all route renders via `vite.ssrLoadModule`, and registers
|
|
17
|
+
* a Connect middleware (after Vite's own middlewares) that bridges Node
|
|
18
|
+
* req/res to the torpor Server's fetch handler.
|
|
19
|
+
*
|
|
20
|
+
* Adapters that need a different runtime (e.g. workerd) provide their own
|
|
21
|
+
* `dev()` plugin instead.
|
|
22
|
+
*/
|
|
23
|
+
export default function devPlugin(site: Site): Plugin {
|
|
24
|
+
return {
|
|
25
|
+
name: "torpor-dev",
|
|
26
|
+
configureServer(vite) {
|
|
27
|
+
// Kick off async setup immediately; the middleware awaits it so we
|
|
28
|
+
// don't depend on whether Vite awaits configureServer itself.
|
|
29
|
+
const handlerPromise = createDevHandler(vite, site);
|
|
30
|
+
// Return a post-hook so our middleware runs AFTER Vite's internal
|
|
31
|
+
// middlewares (static serving, transforms, HMR), acting as the SSR
|
|
32
|
+
// fallback.
|
|
33
|
+
return () => {
|
|
34
|
+
vite.middlewares.use(async (req, res, next) => {
|
|
35
|
+
try {
|
|
36
|
+
const handler = await handlerPromise;
|
|
37
|
+
await handler(req, res);
|
|
38
|
+
} catch (e) {
|
|
39
|
+
next(e as Error);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function createDevHandler(vite: ViteDevServer, site: Site): Promise<ReqHandler> {
|
|
48
|
+
const siteFolder = path.resolve(site.root, "./node_modules/@torpor/build/src/site/");
|
|
49
|
+
const serverScript = path.join(siteFolder, "serverEntry.ts");
|
|
50
|
+
|
|
51
|
+
// Read site.html if present
|
|
52
|
+
// It's called site.html because @torpor/build builds the site (html, routes
|
|
53
|
+
// etc) while the user builds the app (components etc). An endpoints-only
|
|
54
|
+
// site has no site.html, and no template is needed
|
|
55
|
+
let template: string | undefined;
|
|
56
|
+
const templateFile = path.resolve(site.root, "src/site.html");
|
|
57
|
+
if (existsSync(templateFile)) {
|
|
58
|
+
template = await fs.readFile(templateFile, "utf-8");
|
|
59
|
+
|
|
60
|
+
// Apply Vite HTML transforms. This injects the Vite HMR client, and also
|
|
61
|
+
// applies HTML transforms from Vite plugins. Note that we only support a
|
|
62
|
+
// universal transform, not individual transforms for each route
|
|
63
|
+
template = await vite.transformIndexHtml("", template);
|
|
64
|
+
|
|
65
|
+
// Prepare site.html so that we can just splice components into it
|
|
66
|
+
const clientScript = path.join(siteFolder, "clientEntry.ts");
|
|
67
|
+
const clientDevScript = path.join(siteFolder, "clientEntryDev.ts");
|
|
68
|
+
template = prepareTemplate(template, clientScript, clientDevScript);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const server = new Server();
|
|
72
|
+
|
|
73
|
+
// Every request (GET, POST, etc) goes through loadEndPoint
|
|
74
|
+
server.add("*", async (ev) => {
|
|
75
|
+
try {
|
|
76
|
+
// Load the server entry. ssrLoadModule automatically transforms ESM
|
|
77
|
+
// source code to be usable in Node.js. No bundling is required, and
|
|
78
|
+
// it provides efficient invalidation similar to HMR
|
|
79
|
+
const { load } = await vite.ssrLoadModule(serverScript);
|
|
80
|
+
|
|
81
|
+
// Render the app HTML (or fetch server data etc) via serverEntry's
|
|
82
|
+
// exported `load` function
|
|
83
|
+
return await load(ev, template);
|
|
84
|
+
} catch (e: any) {
|
|
85
|
+
// If an error is caught, let Vite fix the stack trace so it maps
|
|
86
|
+
// back to your actual source code
|
|
87
|
+
vite.ssrFixStacktrace(e);
|
|
88
|
+
return serverError(e);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Put adapter-specific functionality in the `adapter` property of
|
|
93
|
+
// globalThis for now
|
|
94
|
+
// @ts-ignore
|
|
95
|
+
globalThis.adapter = { env: process.env };
|
|
96
|
+
|
|
97
|
+
return async (req, res) => {
|
|
98
|
+
const request = await nodeReqToRequest(req);
|
|
99
|
+
const response = await server.fetch(request);
|
|
100
|
+
await sendResponse(response, res);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function nodeReqToRequest(req: IncomingMessage): Promise<Request> {
|
|
105
|
+
const url = req.url ?? "/";
|
|
106
|
+
const host = req.headers.host ?? "localhost";
|
|
107
|
+
const protocol = "encrypted" in req.socket && req.socket.encrypted ? "https" : "http";
|
|
108
|
+
const init: RequestInit = {
|
|
109
|
+
method: req.method,
|
|
110
|
+
headers: flattenHeaders(req.headers),
|
|
111
|
+
};
|
|
112
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
113
|
+
init.body = Readable.toWeb(req) as ReadableStream;
|
|
114
|
+
// @ts-ignore duplex is required when streaming a body
|
|
115
|
+
init.duplex = "half";
|
|
116
|
+
}
|
|
117
|
+
return new Request(`${protocol}://${host}${url}`, init);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function sendResponse(response: Response, res: ServerResponse): Promise<void> {
|
|
121
|
+
res.statusCode = response.status;
|
|
122
|
+
response.headers.forEach((value, key) => res.setHeader(key, value));
|
|
123
|
+
if (!response.body) {
|
|
124
|
+
res.end();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
Readable.fromWeb(response.body as unknown as import("node:stream/web").ReadableStream).pipe(res);
|
|
128
|
+
}
|
|
@@ -15,10 +15,13 @@ export default function prepareTemplate(
|
|
|
15
15
|
|
|
16
16
|
// Put %COMPONENT_BODY% inside <div id="app"></div>
|
|
17
17
|
// This is where the component's HTML will go
|
|
18
|
-
let
|
|
19
|
-
|
|
18
|
+
let divMatchStart = regexIndexOf(result, /<div\s+id=("app"|'app'|app)[\s>]/);
|
|
19
|
+
if (divMatchStart === -1) {
|
|
20
|
+
throw new Error(`Couldn't find <div id="app"></div>`);
|
|
21
|
+
}
|
|
22
|
+
let bodyStart = result.indexOf(">", divMatchStart) + 1;
|
|
20
23
|
let bodyEnd = result.indexOf("</div>", bodyStart);
|
|
21
|
-
if (
|
|
24
|
+
if (bodyEnd === -1) {
|
|
22
25
|
throw new Error(`Couldn't find <div id="app"></div>`);
|
|
23
26
|
}
|
|
24
27
|
result = result.substring(0, bodyStart) + "%COMPONENT_BODY%" + result.substring(bodyEnd);
|
package/src/run/run.ts
CHANGED
|
@@ -1,62 +1,86 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { createServer as createViteServer, type ViteDevServer } from "vite";
|
|
4
4
|
import Site from "../site/Site";
|
|
5
5
|
import runBuild from "./runBuild";
|
|
6
6
|
import runDev from "./runDev";
|
|
7
7
|
import runPreview from "./runPreview";
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
)
|
|
9
|
+
/**
|
|
10
|
+
* Looks for and loads a site.config.js/ts file in the working directory,
|
|
11
|
+
* running the site's plugins. The Vite server used to load the config is
|
|
12
|
+
* returned as well, so that (for example) route files can be loaded through
|
|
13
|
+
* it; callers are responsible for closing it.
|
|
14
|
+
*/
|
|
15
|
+
export async function loadSite(folder: string): Promise<{ site: Site; vite: ViteDevServer }> {
|
|
13
16
|
// Look for and load a site.config.js/ts file in the working directory
|
|
14
|
-
let configFile = "";
|
|
15
|
-
let deleteConfigFile = false;
|
|
16
17
|
const jsConfigFile = path.join(folder, "site.config.js");
|
|
17
18
|
const tsConfigFile = path.join(folder, "site.config.ts");
|
|
19
|
+
let configFile = "";
|
|
18
20
|
if (fs.existsSync(jsConfigFile)) {
|
|
19
|
-
// If it's JS, we can just import it straight up
|
|
20
21
|
configFile = jsConfigFile;
|
|
21
22
|
} else if (fs.existsSync(tsConfigFile)) {
|
|
22
|
-
|
|
23
|
-
// TODO: Put the generated file somewhere better (dist?)
|
|
24
|
-
configFile = path.join(folder, "site.config.temp.js");
|
|
25
|
-
let source = fs.readFileSync(tsConfigFile, "utf-8");
|
|
26
|
-
let transformed = await transformWithEsbuild(source, tsConfigFile, {
|
|
27
|
-
loader: "ts",
|
|
28
|
-
});
|
|
29
|
-
fs.writeFileSync(configFile, transformed.code);
|
|
30
|
-
deleteConfigFile = true;
|
|
23
|
+
configFile = tsConfigFile;
|
|
31
24
|
} else {
|
|
32
25
|
throw new Error("site.config file not found");
|
|
33
26
|
}
|
|
34
27
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
28
|
+
// Use a temporary Vite server to load the config, so that local TS imports
|
|
29
|
+
// (e.g. a routes.ts file) and path aliases are resolved correctly
|
|
30
|
+
const vite = await createViteServer({
|
|
31
|
+
server: { middlewareMode: true },
|
|
32
|
+
appType: "custom",
|
|
33
|
+
resolve: { tsconfigPaths: true },
|
|
34
|
+
optimizeDeps: { noDiscovery: true },
|
|
35
|
+
});
|
|
36
|
+
try {
|
|
37
|
+
const site = (await vite.ssrLoadModule(configFile)).default as Site;
|
|
38
|
+
site.configFile = configFile;
|
|
42
39
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
await runDev(site);
|
|
46
|
-
break;
|
|
47
|
-
}
|
|
48
|
-
case "build": {
|
|
49
|
-
await runBuild(site);
|
|
50
|
-
break;
|
|
40
|
+
if (!site || !site.root || !site.routes) {
|
|
41
|
+
throw new Error("Invalid site in config file");
|
|
51
42
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
43
|
+
|
|
44
|
+
// Run the site's plugins. The manifest plugin also runs them when the
|
|
45
|
+
// server starts up (dev SSR and production), so that plugin routes are
|
|
46
|
+
// registered in the runtime as well
|
|
47
|
+
for (const plugin of site.plugins) {
|
|
48
|
+
await plugin(site);
|
|
55
49
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
50
|
+
|
|
51
|
+
return { site, vite };
|
|
52
|
+
} catch (error) {
|
|
53
|
+
await vite.close();
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default async function run(
|
|
59
|
+
folder: string,
|
|
60
|
+
mode: "dev" | "build" | "preview",
|
|
61
|
+
): Promise<void> {
|
|
62
|
+
const { site, vite } = await loadSite(folder);
|
|
63
|
+
try {
|
|
64
|
+
switch (mode) {
|
|
65
|
+
case "dev": {
|
|
66
|
+
await runDev(site);
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
case "build": {
|
|
70
|
+
await runBuild(site);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case "preview": {
|
|
74
|
+
await runPreview(site);
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
default: {
|
|
78
|
+
console.log("No mode passed, running in dev");
|
|
79
|
+
await runDev(site);
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
60
82
|
}
|
|
83
|
+
} finally {
|
|
84
|
+
await vite.close();
|
|
61
85
|
}
|
|
62
86
|
}
|
package/src/run/runBuild.ts
CHANGED
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
import torpor from "@torpor/unplugin/vite";
|
|
2
2
|
import { existsSync, promises as fs } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { build, defineConfig } from "vite";
|
|
4
|
+
import { build, defineConfig, type AliasOptions } from "vite";
|
|
5
5
|
import Site from "../site/Site";
|
|
6
|
+
import { checkApiCalls, checkRoutes, reportRouteIssues } from "../site/checkRoutes";
|
|
7
|
+
import { checkLayoutSlots } from "../site/checkLayoutSlots";
|
|
6
8
|
import manifest from "../site/manifest.ts";
|
|
9
|
+
import tsconfigAliases, { type AliasEntry } from "../utils/tsconfigAliases";
|
|
10
|
+
import { addTorporPackageConfig } from "../utils/torporPackages";
|
|
7
11
|
|
|
8
12
|
// TODO: Don't cache index.html in dev?
|
|
9
|
-
// TODO: Multiple hook.server locations
|
|
10
13
|
// TODO: Don't reload layouts during client routing
|
|
11
14
|
// TODO: Call the correct +page and +server routes when in the same folder
|
|
12
15
|
|
|
13
16
|
export default async function runBuild(site: Site): Promise<void> {
|
|
17
|
+
// Check route type annotations and makeApi calls against the routes
|
|
18
|
+
// derived from file locations, and check that layouts render their slot;
|
|
19
|
+
// errors fail the build before anything is written
|
|
20
|
+
let errorCount = reportRouteIssues(checkRoutes(site));
|
|
21
|
+
errorCount += reportRouteIssues(checkApiCalls(site));
|
|
22
|
+
errorCount += reportRouteIssues(checkLayoutSlots(site));
|
|
23
|
+
if (errorCount > 0) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Route type check failed with ${errorCount} error${errorCount === 1 ? "" : "s"} (see above)`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
14
29
|
// Delete the dist folder if it exists
|
|
15
30
|
const distFolder = path.resolve(site.root, "dist");
|
|
16
31
|
if (existsSync(distFolder)) {
|
|
@@ -25,7 +40,10 @@ export default async function runBuild(site: Site): Promise<void> {
|
|
|
25
40
|
}
|
|
26
41
|
|
|
27
42
|
// TODO: From a setting
|
|
28
|
-
|
|
43
|
+
// An endpoints-only site has no site.html, so everything template-related
|
|
44
|
+
// is optional
|
|
45
|
+
const siteHtml = path.resolve(site.root, "src/site.html");
|
|
46
|
+
const hasSiteHtml = existsSync(siteHtml);
|
|
29
47
|
|
|
30
48
|
const siteFolder = path.resolve(site.root, "./node_modules/@torpor/build/src/site/");
|
|
31
49
|
let clientScript = path.join(siteFolder, "clientEntry.ts");
|
|
@@ -34,16 +52,27 @@ export default async function runBuild(site: Site): Promise<void> {
|
|
|
34
52
|
// Build the client assets, including site.html and the route files
|
|
35
53
|
// EXCLUDING anything with `server.js` in the name
|
|
36
54
|
const clientConfig = structuredClone(site.viteConfig ?? {});
|
|
37
|
-
|
|
55
|
+
// Resolve tsconfig path aliases (e.g. `@/*`). Previously provided by the
|
|
56
|
+
// default `vite-tsconfig-paths` plugin on Site; vite-plus handles it inline
|
|
57
|
+
// for the client, but hardcodes tsconfigPaths:false on the SSR
|
|
58
|
+
// externalization path, so mirror tsconfig `compilerOptions.paths` as Vite
|
|
59
|
+
// `resolve.alias` to resolve them during transform for both client and SSR
|
|
60
|
+
clientConfig.resolve ??= {};
|
|
61
|
+
clientConfig.resolve.tsconfigPaths ??= true;
|
|
62
|
+
clientConfig.resolve.alias = [
|
|
63
|
+
...asAliasArray(clientConfig.resolve.alias),
|
|
64
|
+
...tsconfigAliases(site.root),
|
|
65
|
+
];
|
|
66
|
+
clientConfig.plugins = [manifest(site), torpor(), ...site.vitePlugins];
|
|
38
67
|
clientConfig.build ??= {};
|
|
39
68
|
clientConfig.build.outDir = clientFolder;
|
|
40
69
|
clientConfig.build.rollupOptions ??= {};
|
|
41
70
|
clientConfig.build.rollupOptions.input = [
|
|
42
|
-
siteHtml,
|
|
71
|
+
...(hasSiteHtml ? [siteHtml] : []),
|
|
43
72
|
clientScript,
|
|
44
73
|
...site.routes
|
|
45
|
-
.filter((r) => !/server\.(ts|js)$/.test(r.file))
|
|
46
|
-
.map((r) => path.resolve(site.root, r.file)),
|
|
74
|
+
.filter((r) => r.file && !/server\.(ts|js)$/.test(r.file))
|
|
75
|
+
.map((r) => path.resolve(site.root, r.file!)),
|
|
47
76
|
...site.inputs.filter((f) => !/server\.(ts|js)$/.test(f)),
|
|
48
77
|
];
|
|
49
78
|
clientConfig.build.ssrManifest = true;
|
|
@@ -52,13 +81,23 @@ export default async function runBuild(site: Site): Promise<void> {
|
|
|
52
81
|
// Build the server assets, including the server entry script and the route
|
|
53
82
|
// files
|
|
54
83
|
const serverConfig = structuredClone(site.viteConfig ?? {});
|
|
55
|
-
|
|
84
|
+
// Resolve tsconfig path aliases (e.g. `@/*`), as above
|
|
85
|
+
serverConfig.resolve ??= {};
|
|
86
|
+
serverConfig.resolve.tsconfigPaths ??= true;
|
|
87
|
+
serverConfig.resolve.alias = [
|
|
88
|
+
...asAliasArray(serverConfig.resolve.alias),
|
|
89
|
+
...tsconfigAliases(site.root),
|
|
90
|
+
];
|
|
91
|
+
serverConfig.plugins = [manifest(site, true), torpor(), ...site.vitePlugins];
|
|
56
92
|
serverConfig.build ??= {};
|
|
57
93
|
serverConfig.build.outDir = serverFolder;
|
|
94
|
+
// Bundle packages that ship `.torp` files into the server build, as they
|
|
95
|
+
// need the torpor compiler (Node/workerd can't load them externalized)
|
|
96
|
+
addTorporPackageConfig(site.root, serverConfig);
|
|
58
97
|
serverConfig.build.rollupOptions ??= {};
|
|
59
98
|
serverConfig.build.rollupOptions.input = [
|
|
60
99
|
serverScript,
|
|
61
|
-
...site.routes.map((r) => path.resolve(site.root, r.file)),
|
|
100
|
+
...site.routes.filter((r) => r.file).map((r) => path.resolve(site.root, r.file!)),
|
|
62
101
|
...site.inputs,
|
|
63
102
|
];
|
|
64
103
|
serverConfig.build.ssr = serverScript;
|
|
@@ -66,12 +105,24 @@ export default async function runBuild(site: Site): Promise<void> {
|
|
|
66
105
|
|
|
67
106
|
// Move the site.html file into /client
|
|
68
107
|
// HACK: Should do this in Rollup if possible?
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
108
|
+
if (hasSiteHtml) {
|
|
109
|
+
await fs.rename(
|
|
110
|
+
path.join(clientFolder, "src", "site.html"),
|
|
111
|
+
path.join(clientFolder, "site.html"),
|
|
112
|
+
);
|
|
113
|
+
await fs.rm(path.join(clientFolder, "src"), { recursive: true });
|
|
114
|
+
}
|
|
72
115
|
|
|
73
116
|
// HOOK: Postbuild
|
|
74
117
|
if (site.adapter.postbuild) {
|
|
75
118
|
await site.adapter.postbuild(site);
|
|
76
119
|
}
|
|
77
120
|
}
|
|
121
|
+
|
|
122
|
+
function asAliasArray(alias: AliasOptions | undefined): AliasEntry[] {
|
|
123
|
+
// Normalize an existing `resolve.alias` value (array | object | undefined)
|
|
124
|
+
// into an array so we can concatenate our tsconfig-derived aliases.
|
|
125
|
+
if (!alias) return [];
|
|
126
|
+
if (Array.isArray(alias)) return alias;
|
|
127
|
+
return Object.entries(alias).map(([find, replacement]) => ({ find, replacement }));
|
|
128
|
+
}
|