@torpor/build 0.4.14 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +20 -2
  2. package/dist/Jsonify-Dg6gChd3.d.mts +10 -0
  3. package/dist/Jsonify-Dg6gChd3.d.mts.map +1 -0
  4. package/dist/ParseRouteParams-DBmqlmLh.d.mts +42 -0
  5. package/dist/ParseRouteParams-DBmqlmLh.d.mts.map +1 -0
  6. package/dist/{RouteType-B6BMOyYw.mjs → Router-C4ADUfhv.mjs} +30 -42
  7. package/dist/Router-C4ADUfhv.mjs.map +1 -0
  8. package/dist/{Server-CF5wDJp6.d.mts → Server-C_IKav_e.d.mts} +44 -16
  9. package/dist/Server-C_IKav_e.d.mts.map +1 -0
  10. package/dist/{ServerEvent-CxPwA4lH.mjs → ServerEvent-Dljn5i7A.mjs} +37 -11
  11. package/dist/ServerEvent-Dljn5i7A.mjs.map +1 -0
  12. package/dist/Site-CMa153FA.d.mts +565 -0
  13. package/dist/Site-CMa153FA.d.mts.map +1 -0
  14. package/dist/StandardSchema-D28L8_zZ.d.mts +100 -0
  15. package/dist/StandardSchema-D28L8_zZ.d.mts.map +1 -0
  16. package/dist/TypedResponse-6lznqN4U.d.mts +22 -0
  17. package/dist/TypedResponse-6lznqN4U.d.mts.map +1 -0
  18. package/dist/{_page-BQs4WvCO.mjs → _page-4cQKs_Dn.mjs} +3 -5
  19. package/dist/_page-4cQKs_Dn.mjs.map +1 -0
  20. package/dist/bin/index.d.mts +1 -1
  21. package/dist/bin/index.js +15 -11
  22. package/dist/bin/index.js.map +1 -1
  23. package/dist/flattenHeaders-C_YYLdOq.mjs +114 -0
  24. package/dist/flattenHeaders-C_YYLdOq.mjs.map +1 -0
  25. package/dist/form.d.mts +35 -0
  26. package/dist/form.d.mts.map +1 -0
  27. package/dist/form.mjs +48 -0
  28. package/dist/form.mjs.map +1 -0
  29. package/dist/index.d.mts +72 -156
  30. package/dist/index.d.mts.map +1 -1
  31. package/dist/index.mjs +158 -26
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/nav.d.mts +58 -5
  34. package/dist/nav.d.mts.map +1 -1
  35. package/dist/nav.mjs +116 -38
  36. package/dist/nav.mjs.map +1 -1
  37. package/dist/openapi.d.mts +144 -0
  38. package/dist/openapi.d.mts.map +1 -0
  39. package/dist/openapi.mjs +2 -0
  40. package/dist/pathTrie-D4Ax2Mu9.mjs +160 -0
  41. package/dist/pathTrie-D4Ax2Mu9.mjs.map +1 -0
  42. package/dist/plugin-D8Www11B.mjs +202 -0
  43. package/dist/plugin-D8Www11B.mjs.map +1 -0
  44. package/dist/{response-C5TtAsh1.mjs → response-Bg4w2S1q.mjs} +16 -39
  45. package/dist/response-Bg4w2S1q.mjs.map +1 -0
  46. package/dist/response.d.mts +309 -289
  47. package/dist/response.d.mts.map +1 -1
  48. package/dist/response.mjs +3 -4
  49. package/dist/run.d.mts +11 -4
  50. package/dist/run.d.mts.map +1 -1
  51. package/dist/run.mjs +2 -8
  52. package/dist/runOpenApi-BUi1RH1l.mjs +1381 -0
  53. package/dist/runOpenApi-BUi1RH1l.mjs.map +1 -0
  54. package/dist/schema.d.mts +32 -0
  55. package/dist/schema.d.mts.map +1 -0
  56. package/dist/schema.mjs +2 -0
  57. package/dist/server.d.mts +10 -8
  58. package/dist/server.d.mts.map +1 -1
  59. package/dist/server.mjs +132 -4
  60. package/dist/server.mjs.map +1 -0
  61. package/dist/state.d.mts.map +1 -1
  62. package/dist/state.mjs +2 -3
  63. package/dist/test.d.mts +8 -10
  64. package/dist/test.d.mts.map +1 -1
  65. package/dist/test.mjs +283 -79
  66. package/dist/test.mjs.map +1 -1
  67. package/dist/{seeOther-B4Yhu9iq.mjs → unprocessable-DAZbrDeR.mjs} +34 -13
  68. package/dist/unprocessable-DAZbrDeR.mjs.map +1 -0
  69. package/dist/validate-Ok4krlkX.mjs +40 -0
  70. package/dist/validate-Ok4krlkX.mjs.map +1 -0
  71. package/package.json +28 -17
  72. package/src/bin/index.ts +13 -2
  73. package/src/dev.ts +10 -0
  74. package/src/form/formDataToRecord.ts +23 -0
  75. package/src/form/readForm.ts +71 -0
  76. package/src/form.ts +4 -0
  77. package/src/index.ts +53 -1
  78. package/src/nav/api.test-d.ts +79 -0
  79. package/src/nav/api.ts +104 -0
  80. package/src/nav/formSubmit.ts +18 -4
  81. package/src/nav/navigate.ts +40 -15
  82. package/src/nav/route.ts +38 -0
  83. package/src/nav.ts +3 -1
  84. package/src/openapi/docsHtml.ts +25 -0
  85. package/src/openapi/document.ts +154 -0
  86. package/src/openapi/plugin.ts +78 -0
  87. package/src/openapi/types.ts +82 -0
  88. package/src/openapi.ts +14 -0
  89. package/src/response/TypedResponse.ts +17 -0
  90. package/src/response/badRequest.ts +14 -2
  91. package/src/response/created.ts +13 -2
  92. package/src/response/found.ts +2 -2
  93. package/src/response/movedPermanently.ts +2 -2
  94. package/src/response/notModified.ts +2 -2
  95. package/src/response/ok.ts +13 -4
  96. package/src/response/response.ts +4 -4
  97. package/src/response/unprocessable.ts +14 -2
  98. package/src/run/depCache.ts +175 -0
  99. package/src/run/devPlugin.ts +128 -0
  100. package/src/run/prepareTemplate.ts +6 -3
  101. package/src/run/run.ts +63 -39
  102. package/src/run/runBuild.ts +63 -12
  103. package/src/run/runDev.ts +146 -58
  104. package/src/run/runOpenApi.ts +52 -0
  105. package/src/run/runPreview.ts +17 -26
  106. package/src/run/staleTorpCopies.ts +100 -0
  107. package/src/run.ts +2 -1
  108. package/src/schema.ts +7 -0
  109. package/src/server/CookieHelper.ts +17 -7
  110. package/src/server/Server.ts +54 -30
  111. package/src/server/ServerEvent.ts +24 -1
  112. package/src/server/connect/connectMiddleware.ts +44 -40
  113. package/src/server/connect/flattenHeaders.ts +1 -23
  114. package/src/server/connect/requestToNodeMessage.ts +6 -3
  115. package/src/server/types/MiddlewareFunction.ts +18 -4
  116. package/src/site/Router.ts +52 -41
  117. package/src/site/Site.ts +184 -10
  118. package/src/site/checkLayoutSlots.ts +114 -0
  119. package/src/site/checkRoutes.ts +435 -0
  120. package/src/site/clientEntry.ts +22 -9
  121. package/src/site/layoutSlots.ts +53 -0
  122. package/src/site/manifest.ts +125 -6
  123. package/src/site/serverEntry.ts +341 -132
  124. package/src/state/$page.ts +2 -1
  125. package/src/state/$serverPage.ts +22 -0
  126. package/src/test/runTest.ts +306 -118
  127. package/src/types/Adapter.ts +12 -0
  128. package/src/types/Jsonify.ts +20 -0
  129. package/src/types/PageData.test-d.ts +136 -0
  130. package/src/types/PageData.ts +35 -0
  131. package/src/types/PageEndPoint.ts +25 -9
  132. package/src/types/PageForm.test-d.ts +78 -0
  133. package/src/types/PageForm.ts +29 -0
  134. package/src/types/PageLoadEvent.ts +16 -4
  135. package/src/types/PageLoadReturn.ts +13 -0
  136. package/src/types/PageProps.ts +14 -0
  137. package/src/types/PageServerAction.ts +8 -2
  138. package/src/types/PageServerEndPoint.test-d.ts +105 -0
  139. package/src/types/PageServerEndPoint.ts +99 -5
  140. package/src/types/PageServerLoad.ts +11 -3
  141. package/src/types/ParseRouteParams.test-d.ts +84 -0
  142. package/src/types/ParseRouteParams.ts +64 -0
  143. package/src/types/Route.ts +20 -1
  144. package/src/types/RouteHandler.ts +8 -1
  145. package/src/types/ServerEndPoint.test-d.ts +77 -0
  146. package/src/types/ServerEndPoint.ts +148 -16
  147. package/src/types/ServerHook.ts +10 -3
  148. package/src/types/ServerLoadEvent.ts +48 -3
  149. package/src/types/ServerRequest.ts +10 -2
  150. package/src/types/SitePlugin.ts +24 -0
  151. package/src/types/StandardSchema.ts +109 -0
  152. package/src/utils/pathToRegex.ts +4 -2
  153. package/src/utils/pathTrie.ts +182 -0
  154. package/src/utils/searchParamsToRecord.ts +18 -0
  155. package/src/utils/torporPackages.ts +150 -0
  156. package/src/utils/tsconfigAliases.ts +90 -0
  157. package/src/validation/ValidationError.ts +18 -0
  158. package/src/validation/endpoint.ts +83 -0
  159. package/src/validation/validate.ts +26 -0
  160. package/dist/RouteType-B6BMOyYw.mjs.map +0 -1
  161. package/dist/Server-CF5wDJp6.d.mts.map +0 -1
  162. package/dist/ServerEvent-CxPwA4lH.mjs.map +0 -1
  163. package/dist/Site-DgC6WWn1.d.mts +0 -78
  164. package/dist/Site-DgC6WWn1.d.mts.map +0 -1
  165. package/dist/_page-BQs4WvCO.mjs.map +0 -1
  166. package/dist/connectMiddleware-D7nehjj3.mjs +0 -252
  167. package/dist/connectMiddleware-D7nehjj3.mjs.map +0 -1
  168. package/dist/pathToRegex-TUAMc3zS.mjs +0 -11
  169. package/dist/pathToRegex-TUAMc3zS.mjs.map +0 -1
  170. package/dist/response-C5TtAsh1.mjs.map +0 -1
  171. package/dist/run-BPFtSYA3.mjs +0 -332
  172. package/dist/run-BPFtSYA3.mjs.map +0 -1
  173. package/dist/seeOther-B4Yhu9iq.mjs.map +0 -1
  174. package/src/server/Routerx.ts +0 -72
  175. package/src/server/connect/bufferToArrayBuffer.ts +0 -8
  176. package/src/server/connect/readableToBuffer.ts +0 -16
@@ -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(body?: object | string): Response {
17
- return response(422, body ?? "Unprocessable");
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 bodyStart = regexIndexOf(result, /<div\s+id=("app"|'app'|app)\s+/);
19
- bodyStart = result.indexOf(">", bodyStart) + 1;
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 (bodyStart === -1 || bodyEnd === -1) {
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 { transformWithEsbuild } from "vite";
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
- export default async function run(
10
- folder: string,
11
- mode: "dev" | "build" | "preview",
12
- ): Promise<void> {
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
- // If it's TS, we need to convert it to JS
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
- const site = (await import(configFile)).default as Site;
36
- if (!site || !site.root || !site.routes) {
37
- throw new Error("Invalid site in config file");
38
- }
39
- if (deleteConfigFile) {
40
- fs.rmSync(configFile);
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
- switch (mode) {
44
- case "dev": {
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
- case "preview": {
53
- await runPreview(site);
54
- break;
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
- default: {
57
- console.log("No mode passed, running in dev");
58
- await runDev(site);
59
- break;
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
  }
@@ -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
- let siteHtml = path.resolve(site.root, "src/site.html");
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
- clientConfig.plugins = [manifest(site), torpor(), ...site.plugins];
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
- serverConfig.plugins = [manifest(site, true), torpor(), ...site.plugins];
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
- siteHtml = path.join(clientFolder, "site.html");
70
- await fs.rename(path.join(clientFolder, "src", "site.html"), siteHtml);
71
- await fs.rm(path.join(clientFolder, "src"), { recursive: true });
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
+ }