@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
@@ -4,6 +4,12 @@ import HeaderHelper from "./HeaderHelper";
4
4
  export default class ServerEvent {
5
5
  request: Request;
6
6
  response: Response | undefined | void;
7
+ /**
8
+ * Set when the route handler (or a middleware enter hook) threw an
9
+ * error. Exit hooks can return a Response to handle it; if none does,
10
+ * the error is rethrown to the caller.
11
+ */
12
+ error: unknown;
7
13
  // TODO: Should we cast types??
8
14
  params?: Record<string, string>;
9
15
 
@@ -12,9 +18,19 @@ export default class ServerEvent {
12
18
  cookies: CookieHelper;
13
19
  headers: HeaderHelper;
14
20
 
15
- constructor(request: Request, params?: Record<string, string>) {
21
+ // Parsed once per request; Server.fetch passes the URL it already needed
22
+ // for matching so it isn't parsed twice
23
+ #url: URL | undefined;
24
+
25
+ /** The request URL, parsed lazily. */
26
+ get url(): URL {
27
+ return (this.#url ??= new URL(this.request.url));
28
+ }
29
+
30
+ constructor(request: Request, params?: Record<string, string>, url?: URL) {
16
31
  this.request = request;
17
32
  this.params = params;
33
+ this.#url = url;
18
34
  this.cookies = new CookieHelper(request);
19
35
  this.headers = new HeaderHelper(request);
20
36
 
@@ -36,4 +52,11 @@ export default class ServerEvent {
36
52
  this.response.headers.append(header[0], header[1]);
37
53
  }
38
54
  }
55
+
56
+ /**
57
+ * Reads the request body as JSON.
58
+ */
59
+ json(): Promise<unknown> {
60
+ return this.request.json();
61
+ }
39
62
  }
@@ -1,62 +1,66 @@
1
1
  import { IncomingMessage, ServerResponse } from "node:http";
2
+ import { Readable } from "node:stream";
2
3
  import type MiddlewareFunction from "../types/MiddlewareFunction";
3
- import bufferToArrayBuffer from "./bufferToArrayBuffer";
4
4
  import flattenHeaders from "./flattenHeaders";
5
5
  import nodeMessageToNodeResponse from "./nodeMessageToNodeResponse";
6
- import readableToBuffer from "./readableToBuffer";
7
6
  import requestToNodeMessage from "./requestToNodeMessage";
8
7
 
9
8
  // From https://github.com/vikejs/vike-node/blob/main/packages/vike-node/src/runtime/adapters/connectToWeb.ts
10
9
 
11
10
  /**
12
- * Converts a Connect-style middleware to a web-compatible request handler.
11
+ * Converts a Connect-style middleware to a torpor middleware.
12
+ *
13
+ * If the Connect handler writes a response, it is returned from the `enter`
14
+ * hook and the middleware chain is short-circuited. If the handler calls
15
+ * `next()`, the rest of the chain runs after this middleware completes.
13
16
  *
14
17
  * @param handler - The Connect-style middleware function to be converted.
15
- * @returns A function that handles web requests and returns a Response or undefined.
18
+ * @returns A middleware whose `enter` hook handles the web request.
16
19
  */
17
20
  export default function connectMiddleware(
18
21
  handler: ConnectMiddleware | ConnectMiddlewareBoolean,
19
22
  ): MiddlewareFunction {
20
- return async (ev, next) => {
21
- const req = requestToNodeMessage(ev.request);
22
- const { res, onReadable } = nodeMessageToNodeResponse(req);
23
+ return {
24
+ enter: (ev) =>
25
+ new Promise<Response | void>((resolve, reject) => {
26
+ const req = requestToNodeMessage(ev.request);
27
+ const { res, onReadable } = nodeMessageToNodeResponse(req);
23
28
 
24
- return new Promise<void>(async (resolve, reject) => {
25
- onReadable(async ({ readable, headers, status }) => {
26
- const responseBody = statusCodesWithoutBody.includes(status)
27
- ? null
28
- : //(Readable.toWeb(readable) as ReadableStream);
29
- // HACK: Chunked bodies are not being
30
- // read correctly somewhere...
31
- bufferToArrayBuffer(await readableToBuffer(readable));
32
- ev.response = new Response(responseBody, {
33
- status,
34
- headers: flattenHeaders(headers),
29
+ onReadable(({ readable, headers, status }) => {
30
+ const responseBody: ReadableStream | null = statusCodesWithoutBody.includes(status)
31
+ ? null
32
+ : (Readable.toWeb(readable) as unknown as ReadableStream);
33
+ resolve(
34
+ new Response(responseBody, {
35
+ status,
36
+ headers: flattenHeaders(headers),
37
+ }),
38
+ );
35
39
  });
36
- resolve();
37
- });
38
40
 
39
- const cnext = async (error?: unknown) => {
40
- if (error) {
41
- // eslint-disable-next-line no-base-to-string
42
- reject(error instanceof Error ? error : new Error(String(error)));
43
- } else {
44
- // TODO: Can we nest the next functions less? This is a bit weird
45
- await next();
46
- resolve();
47
- }
48
- };
41
+ const cnext = (error?: unknown) => {
42
+ if (error) {
43
+ // eslint-disable-next-line no-base-to-string
44
+ reject(error instanceof Error ? error : new Error(String(error)));
45
+ } else {
46
+ // The Connect handler is passing the request on
47
+ resolve();
48
+ }
49
+ };
49
50
 
50
- try {
51
- const handled = await handler(req, res, cnext);
52
- if (handled === false) {
53
- res.destroy();
54
- resolve();
55
- }
56
- } catch (e) {
57
- await cnext(e);
58
- }
59
- });
51
+ Promise.resolve(handler(req, res, cnext))
52
+ .then((handled) => {
53
+ if (handled === false) {
54
+ res.destroy();
55
+ // Returning false signals that the request was handled,
56
+ // so return an empty response to stop the chain
57
+ resolve(new Response(null, { status: 200 }));
58
+ }
59
+ })
60
+ .catch((e: unknown) => {
61
+ cnext(e);
62
+ });
63
+ }),
60
64
  };
61
65
  }
62
66
 
@@ -11,7 +11,7 @@ export default function flattenHeaders(
11
11
  } else if (Array.isArray(value)) {
12
12
  for (const v of value) {
13
13
  if (v !== undefined && v !== null) {
14
- value.forEach((v) => flatHeaders.append(key, v));
14
+ flatHeaders.append(key, String(v));
15
15
  }
16
16
  }
17
17
  } else {
@@ -19,26 +19,4 @@ export default function flattenHeaders(
19
19
  }
20
20
  }
21
21
  return flatHeaders;
22
-
23
- /*
24
- const flatHeaders: [string, string][] = [];
25
-
26
- for (const [key, value] of Object.entries(headers)) {
27
- if (value === undefined || value === null) {
28
- continue;
29
- }
30
-
31
- if (Array.isArray(value)) {
32
- for (const v of value) {
33
- if (v != null) {
34
- flatHeaders.push([key, String(v)]);
35
- }
36
- }
37
- } else {
38
- flatHeaders.push([key, String(value)]);
39
- }
40
- }
41
-
42
- return flatHeaders;
43
- */
44
22
  }
@@ -8,15 +8,18 @@ const DUMMY_BASE_URL = "http://localhost";
8
8
  /**
9
9
  * Creates an IncomingMessage object from a web Request.
10
10
  *
11
+ * The request body is intentionally not forwarded: this is only used to wrap
12
+ * Connect-style middlewares (e.g. Vite's dev middleware), which don't read
13
+ * the body. The actual route handler later reads `ev.request.body` directly,
14
+ * and forwarding the body here would consume it and break the handler.
15
+ *
11
16
  * @param {Request} request - The web Request object.
12
17
  * @returns {IncomingMessage} An IncomingMessage-like object compatible with Node.js HTTP module.
13
18
  */
14
19
  export default function requestToNodeMessage(request: Request): IncomingMessage {
15
20
  const parsedUrl = new URL(request.url, DUMMY_BASE_URL);
16
21
  const pathAndQuery = (parsedUrl.pathname || "") + (parsedUrl.search || "");
17
- // HACK: Using Readable.fromWeb locks request.body so that the request cannot be used again
18
- // As we're only using this for Vite middlewares, it's safe to discard the body for now?
19
- const body = /*request.body ? Readable.fromWeb(request.body as any) :*/ Readable.from([]);
22
+ const body = Readable.from([]);
20
23
 
21
24
  return Object.assign(body, {
22
25
  url: pathAndQuery,
@@ -1,8 +1,22 @@
1
1
  import ServerEvent from "../ServerEvent";
2
2
 
3
- type MiddlewareFunction = (
4
- ev: ServerEvent,
5
- next: () => void | Promise<void>,
6
- ) => void | Promise<void>;
3
+ type MiddlewareFunction = {
4
+ /**
5
+ * Called before the route handler. Middleware are run in the order they
6
+ * were added. Return a Response to short-circuit the request; the
7
+ * remaining middleware and the route handler are then skipped.
8
+ */
9
+ enter?: (ev: ServerEvent) => Response | void | Promise<Response | void>;
10
+ /**
11
+ * Called after the route handler (even if it threw), in reverse order.
12
+ * Return a Response to replace the response. Middleware that were skipped
13
+ * by a short-circuit don't get their exit hook run.
14
+ *
15
+ * If the route handler (or a later enter hook) threw, the error is
16
+ * available as `ev.error`; returning a Response handles the error and
17
+ * stops it from propagating.
18
+ */
19
+ exit?: (ev: ServerEvent) => Response | void | Promise<Response | void>;
20
+ };
7
21
 
8
22
  export default MiddlewareFunction;
@@ -1,13 +1,17 @@
1
1
  import type LayoutHandler from "../types/LayoutHandler";
2
2
  import type ManifestRoute from "../types/ManifestRoute";
3
3
  import type RouteHandler from "../types/RouteHandler";
4
- import pathToRegex from "../utils/pathToRegex";
4
+ import PathTrie from "../utils/pathTrie";
5
5
 
6
6
  /**
7
7
  * A router that handles file routes with layouts, hooks, etc.
8
8
  */
9
9
  export default class Router {
10
10
  routes: Route[] = [];
11
+ // Match index for the routes above. The routes list is kept for
12
+ // introspection (layouts/hooks lookup, client nav, tests); the trie is
13
+ // what `match` walks.
14
+ #trie = new PathTrie<Route>();
11
15
 
12
16
  constructor() {}
13
17
 
@@ -18,29 +22,21 @@ export default class Router {
18
22
 
19
23
  addPages(routes: ManifestRoute[]): this {
20
24
  for (let r of routes) {
21
- this.routes.push(
22
- new Route(r.path, {
23
- path: r.path,
24
- type: r.type,
25
- endPoint: r.endPoint,
26
- subFolder: r.subFolder,
27
- }),
28
- );
25
+ this.addPage(r.path, r.type, r.endPoint, r.subFolder);
29
26
  }
30
- this.#sortRoutes();
31
27
  return this;
32
28
  }
33
29
 
34
30
  // TODO: Allow calling with the endpoint itself, so that you can setup an app with no scaffold
35
31
  addPage(path: string, type: number, endPoint: () => Promise<any>, subFolder?: string): this {
36
- this.routes.push(
37
- new Route(path, {
38
- path,
39
- type,
40
- endPoint,
41
- subFolder,
42
- }),
43
- );
32
+ let route = new Route(path, {
33
+ path,
34
+ type,
35
+ endPoint,
36
+ subFolder,
37
+ });
38
+ this.routes.push(route);
39
+ this.#trie.insert(path, route);
44
40
  this.#sortRoutes();
45
41
  return this;
46
42
  }
@@ -56,27 +52,26 @@ export default class Router {
56
52
  */
57
53
 
58
54
  match(path: string, query: URLSearchParams): RouteMatch | undefined {
59
- for (let route of this.routes) {
60
- let match = path.match(route.regex);
61
- if (match) {
62
- // Lazy load server endpoints and layouts
63
- if (!route.handler.loaded) {
64
- this.#loadHandler(route.handler, route.path);
65
- }
66
-
67
- return {
68
- handler: route.handler,
69
- params: match.groups,
70
- query,
71
- };
55
+ let found = this.#trie.match(path);
56
+ if (found) {
57
+ let route = found.value;
58
+ // Lazy load server endpoints and layouts
59
+ if (!route.handler.loaded) {
60
+ this.#loadHandler(route.handler, route.path);
72
61
  }
62
+
63
+ return {
64
+ handler: route.handler,
65
+ params: found.params,
66
+ query,
67
+ };
73
68
  }
74
69
  }
75
70
 
76
71
  #loadHandler(handler: RouteHandler, path: string) {
77
72
  handler.layouts = this.#findLayouts(path, handler);
78
73
  handler.serverEndPoint = this.#findServer(path);
79
- handler.serverHook = this.#findServerHook(handler);
74
+ handler.serverHooks = this.#findServerHooks(handler);
80
75
  }
81
76
 
82
77
  #findLayouts(path: string, handler: RouteHandler): LayoutHandler[] | undefined {
@@ -113,14 +108,32 @@ export default class Router {
113
108
  return serverRoute && serverRoute.handler.endPoint;
114
109
  }
115
110
 
116
- #findServerHook(handler: RouteHandler): (() => Promise<any>) | undefined {
117
- // TODO: Should this be a collection, like layouts?
118
- let serverHookPath = "/_hook/~server";
119
- if (handler.subFolder !== undefined) {
120
- serverHookPath = handler.subFolder + serverHookPath;
111
+ #findServerHooks(handler: RouteHandler): (() => Promise<any>)[] | undefined {
112
+ // Collect the hooks from the root down to the route path, like
113
+ // layouts. Only hooks with the same subFolder are included, so that
114
+ // e.g. an `api` set of routes can have its own hooks
115
+ const hooks: (() => Promise<any>)[] = [];
116
+ let parts = handler.path
117
+ // Strip the trailing `~server` (so we don't look for
118
+ // `~server/_hook/~server` which will never exist)
119
+ .replace(/\/~server$/, "")
120
+ // The path will always start with / so splitting e.g.
121
+ // `/first/second` will result in ['', 'first', 'second'] and
122
+ // checks for `/_hook/~server`, `/first/_hook/~server` and
123
+ // `/second/_hook/~server`
124
+ .split("/");
125
+ let basePath = "";
126
+ for (let i = 0; i < parts.length; i++) {
127
+ if (i > 0) {
128
+ basePath += "/" + parts[i];
129
+ }
130
+ const hookPath = basePath + "/_hook/~server";
131
+ const hookRoute = this.routes.find((r) => r.path === hookPath);
132
+ if (hookRoute && hookRoute.handler.subFolder === handler.subFolder) {
133
+ hooks.push(hookRoute.handler.endPoint);
134
+ }
121
135
  }
122
- const serverHookRoute = this.routes.find((r) => r.path === serverHookPath);
123
- return serverHookRoute && serverHookRoute.handler.endPoint;
136
+ return hooks.length ? hooks : undefined;
124
137
  }
125
138
 
126
139
  // HACK: We're doing this here as well as in Site, so that you can use the
@@ -147,12 +160,10 @@ export default class Router {
147
160
 
148
161
  class Route {
149
162
  path: string;
150
- regex: RegExp;
151
163
  handler: RouteHandler;
152
164
 
153
165
  constructor(path: string, handler: RouteHandler) {
154
166
  this.path = path;
155
- this.regex = pathToRegex(path);
156
167
  this.handler = handler;
157
168
  }
158
169
  }
package/src/site/Site.ts CHANGED
@@ -1,9 +1,13 @@
1
1
  import { promises as fs } from "node:fs";
2
2
  import fpath from "node:path";
3
3
  import { type Plugin, type UserConfig } from "vite";
4
- import tsconfigPaths from "vite-tsconfig-paths";
5
4
  import type Adapter from "../types/Adapter";
5
+ import type SitePlugin from "../types/SitePlugin";
6
+ import type { InlineEndPoint } from "../types/Route";
7
+ import type PageServerEndPoint from "../types/PageServerEndPoint";
6
8
  import type Route from "../types/Route";
9
+ import type ServerEndPoint from "../types/ServerEndPoint";
10
+ import type ServerHook from "../types/ServerHook";
7
11
  import {
8
12
  ERROR_ROUTE,
9
13
  HOOK_ROUTE,
@@ -29,11 +33,17 @@ import defaultAdapter from "./defaultAdapter";
29
33
  export default class Site {
30
34
  root: string;
31
35
  routes: Route[] = [];
32
- // Is default plugins a bad idea?
33
- // HACK: Set loose because otherwise it uses a list of allowed extensions
34
- // TODO: Should probably just do this ourselves and let the user pass in
35
- // tsconfigPaths themselves if they want to do something funky
36
- plugins: Plugin[] = [tsconfigPaths({ loose: true })];
36
+ /**
37
+ * Torpor site plugins, run when the site config is loaded (for dev and
38
+ * build) and when the server starts up. Use these to register extra
39
+ * routes (e.g. `openApi()` from `@torpor/build/openapi`) or add other
40
+ * framework-level functionality
41
+ */
42
+ plugins: SitePlugin[] = [];
43
+ /**
44
+ * Vite plugins to add to the client and server builds
45
+ */
46
+ vitePlugins: Plugin[] = [];
37
47
  // Is default adapter a bad idea?
38
48
  adapter: Adapter = defaultAdapter;
39
49
  /**
@@ -45,10 +55,36 @@ export default class Site {
45
55
  /**
46
56
  * Vite config options to merge with the standard Torpor build options.
47
57
  * `plugins` and `build.rollupOptions.input` will be overridden, so you
48
- * should set `Site.plugins` and `Site.inputs` instead. Other options may be
58
+ * should set `Site.vitePlugins` and `Site.inputs` instead. Other options may be
49
59
  * overridden or incompatible, so experimentation may be required
50
60
  */
51
61
  viteConfig?: UserConfig;
62
+ /**
63
+ * The path to the site.config file. Set automatically by the CLI; used by
64
+ * the manifest plugin to import inline endpoints for server builds.
65
+ */
66
+ configFile?: string;
67
+ /**
68
+ * A state map for plugins to store their configuration and state on the
69
+ * site, so that framework internals (e.g. the manifest plugin) and other
70
+ * plugins can read them back:
71
+ *
72
+ * ```ts
73
+ * const myKey = Symbol.for("my-plugin");
74
+ * site.pluginState.set(myKey, options);
75
+ * ```
76
+ *
77
+ * Prefer `Symbol.for("...")` keys (or plain strings) over `Symbol()`,
78
+ * because the user's config file and framework internals can be loaded as
79
+ * separate module instances -- registry symbols are shared across
80
+ * instances, `Symbol()` ones are not.
81
+ */
82
+ pluginState: Map<PropertyKey, unknown> = new Map();
83
+ /**
84
+ * Inline endpoints keyed by `"path:type"`. Populated by `addRoute` when
85
+ * the user passes an inline endpoint object instead of a file path.
86
+ */
87
+ inlineEndPoints: Record<string, InlineEndPoint> = {};
52
88
 
53
89
  constructor() {
54
90
  this.root = process.cwd();
@@ -189,9 +225,147 @@ export default class Site {
189
225
  }
190
226
  }
191
227
 
192
- // TODO: You could make your site in a single file, like e.g. Hono...
193
- //addPage(path: string, endPoint: PageEndPoint, serverEndPoint?: PageServerEndPoint) {}
194
- //addEndPoint(path: string, endPoint: ServerEndPoint) {}
228
+ /**
229
+ * Adds a route in code, without needing the standard file naming
230
+ * conventions (+page.ts, +page.server.ts, etc). This is useful for small
231
+ * sites with one or two pages.
232
+ *
233
+ * Any combination of options may be provided; each maps to the
234
+ * corresponding file-route type:
235
+ *
236
+ * - `page` — a `+page` (`.torp` component or `.ts`/`.js` `PageEndPoint`)
237
+ * - `pageServer` — a `+page.server` (`PageServerEndPoint`, file or inline)
238
+ * - `server` — a `+server` HTTP endpoint (`ServerEndPoint`, file or inline)
239
+ * - `layout` — a `_layout` (`.torp` component or `.ts`/`.js` `PageEndPoint`)
240
+ * - `layoutServer` — a `_layout.server` (`PageServerEndPoint`, file or inline)
241
+ * - `hookServer` — a `_hook.server` (`ServerHook`, file or inline)
242
+ * - `error` — an `_error` page (`.torp` component or `.ts`/`.js` `PageEndPoint`)
243
+ *
244
+ * Options that need a component (`page`, `layout`, `error`) must be file
245
+ * paths so the component is included in the client bundle. Server-only
246
+ * options (`pageServer`, `server`, `layoutServer`, `hookServer`) accept
247
+ * either a file path or an inline object; inline server code is kept out
248
+ * of the client bundle automatically.
249
+ *
250
+ * @example
251
+ * // A page with an inline server action
252
+ * site.addRoute("/", {
253
+ * page: "./src/Counter.torp",
254
+ * pageServer: { actions: { set: async ({ request }) => ok() } },
255
+ * });
256
+ * @example
257
+ * // A JSON API endpoint
258
+ * site.addRoute("/api/time", {
259
+ * server: { get: async () => ok({ time: Date.now() }) },
260
+ * });
261
+ * @example
262
+ * // A root layout, server hook, and error page
263
+ * site.addRoute("/", {
264
+ * layout: "./src/Layout.torp",
265
+ * hookServer: { enter: async (event) => {} },
266
+ * error: "./src/ErrorPage.torp",
267
+ * });
268
+ *
269
+ * @param path The route path, e.g. `/` or `/about`
270
+ * @param options File paths and/or inline endpoints for the route
271
+ * @param subFolder An optional subFolder for grouping routes
272
+ */
273
+ addRoute(
274
+ path: string,
275
+ options: {
276
+ page?: string;
277
+ pageServer?: string | PageServerEndPoint;
278
+ server?: string | ServerEndPoint;
279
+ layout?: string;
280
+ layoutServer?: string | PageServerEndPoint;
281
+ hookServer?: string | ServerHook;
282
+ error?: string;
283
+ },
284
+ subFolder?: string,
285
+ ): void {
286
+ let normalizedSubFolder = subFolder;
287
+ if (normalizedSubFolder !== undefined && !normalizedSubFolder.startsWith("/")) {
288
+ normalizedSubFolder = "/" + normalizedSubFolder;
289
+ }
290
+
291
+ // The base path with any trailing slash stripped, so that we can
292
+ // append "/_layout", "/~server" etc. For the root "/" this is "".
293
+ let base = path.replace(/\/$/, "");
294
+
295
+ if (options.page !== undefined) {
296
+ this.#pushRoute(path, PAGE_ROUTE, options.page, normalizedSubFolder);
297
+ }
298
+ if (options.pageServer !== undefined) {
299
+ this.#pushRoute(
300
+ `${base}/~server`,
301
+ PAGE_SERVER_ROUTE,
302
+ options.pageServer,
303
+ normalizedSubFolder,
304
+ );
305
+ }
306
+ if (options.server !== undefined) {
307
+ this.#pushRoute(path, SERVER_ROUTE, options.server, normalizedSubFolder);
308
+ }
309
+ if (options.layout !== undefined) {
310
+ this.#pushRoute(`${base}/_layout`, LAYOUT_ROUTE, options.layout, normalizedSubFolder);
311
+ }
312
+ if (options.layoutServer !== undefined) {
313
+ this.#pushRoute(
314
+ `${base}/_layout/~server`,
315
+ LAYOUT_SERVER_ROUTE,
316
+ options.layoutServer,
317
+ normalizedSubFolder,
318
+ );
319
+ }
320
+ if (options.hookServer !== undefined) {
321
+ this.#pushRoute(
322
+ `${base}/_hook/~server`,
323
+ HOOK_SERVER_ROUTE,
324
+ options.hookServer,
325
+ normalizedSubFolder,
326
+ );
327
+ }
328
+ if (options.error !== undefined) {
329
+ this.#pushRoute(`${base}/_error`, ERROR_ROUTE, options.error, normalizedSubFolder);
330
+ }
331
+
332
+ this.#sortRoutes();
333
+ }
334
+
335
+ /**
336
+ * Pushes a single route entry, handling both file-path and inline-endpoint
337
+ * targets. File paths are resolved relative to the site root; inline
338
+ * endpoints are also stored in `inlineEndPoints` keyed by `"path:type"`
339
+ * so the manifest plugin can resolve them at build time.
340
+ *
341
+ * If a route with the same path and type was already added, it is
342
+ * replaced, so that running a plugin twice (config load and server
343
+ * startup) doesn't add duplicate routes.
344
+ */
345
+ #pushRoute(
346
+ routePath: string,
347
+ type: RouteType,
348
+ target: string | InlineEndPoint,
349
+ subFolder?: string,
350
+ ): void {
351
+ const index = this.routes.findIndex((r) => r.path === routePath && r.type === type);
352
+ if (typeof target === "string") {
353
+ let file = fpath.relative(this.root, fpath.resolve(this.root, target));
354
+ if (index >= 0) {
355
+ this.routes[index] = { path: routePath, file, type, subFolder };
356
+ } else {
357
+ this.routes.push({ path: routePath, file, type, subFolder });
358
+ }
359
+ } else {
360
+ let key = `${routePath}:${type}`;
361
+ this.inlineEndPoints[key] = target;
362
+ if (index >= 0) {
363
+ this.routes[index] = { path: routePath, endPoint: target, type, subFolder };
364
+ } else {
365
+ this.routes.push({ path: routePath, endPoint: target, type, subFolder });
366
+ }
367
+ }
368
+ }
195
369
 
196
370
  #sortRoutes() {
197
371
  this.routes = this.routes.sort((a, b) => {