@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
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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;
|
package/src/site/Router.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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.
|
|
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
|
-
#
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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.
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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) => {
|