@torpor/build 0.0.6 → 0.1.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 (106) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1 -1
  3. package/dist/Server-CB99REgs.d.cts +74 -0
  4. package/dist/Server-CB99REgs.d.ts +74 -0
  5. package/dist/Site-CgoeDt8k.d.cts +29 -0
  6. package/dist/Site-D95nwrAg.d.ts +29 -0
  7. package/dist/adapters/cloudflare/index.cjs +125 -0
  8. package/dist/adapters/cloudflare/index.cjs.map +1 -0
  9. package/dist/adapters/cloudflare/index.d.cts +13 -0
  10. package/dist/adapters/cloudflare/index.d.ts +13 -0
  11. package/dist/adapters/cloudflare/index.js +80 -0
  12. package/dist/adapters/cloudflare/index.js.map +1 -0
  13. package/dist/adapters/node/index.cjs +152 -0
  14. package/dist/adapters/node/index.cjs.map +1 -0
  15. package/dist/adapters/node/index.d.cts +8 -0
  16. package/dist/adapters/node/index.d.ts +8 -0
  17. package/dist/adapters/node/index.js +8 -0
  18. package/dist/adapters/node/index.js.map +1 -0
  19. package/dist/bin/index.cjs +541 -93
  20. package/dist/bin/index.cjs.map +1 -1
  21. package/dist/bin/index.js +511 -22
  22. package/dist/bin/index.js.map +1 -1
  23. package/dist/chunk-5X4T3PZN.js +23 -0
  24. package/dist/chunk-5X4T3PZN.js.map +1 -0
  25. package/dist/chunk-ALHFYD7U.js +113 -0
  26. package/dist/chunk-ALHFYD7U.js.map +1 -0
  27. package/dist/chunk-V4XTDQEV.js +19 -0
  28. package/dist/chunk-V4XTDQEV.js.map +1 -0
  29. package/dist/chunk-VAGHGWM5.js +110 -0
  30. package/dist/chunk-VAGHGWM5.js.map +1 -0
  31. package/dist/index.cjs +200 -97
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +20 -28
  34. package/dist/index.d.ts +20 -28
  35. package/dist/index.js +85 -17
  36. package/dist/index.js.map +1 -1
  37. package/dist/metafile-cjs.json +1 -1
  38. package/dist/metafile-esm.json +1 -1
  39. package/dist/response.js +17 -91
  40. package/dist/response.js.map +1 -1
  41. package/package.json +24 -7
  42. package/src/adapters/cloudflare/_worker.ts +38 -0
  43. package/src/adapters/cloudflare/index.ts +98 -0
  44. package/src/adapters/node/constants.ts +1 -0
  45. package/src/adapters/node/createFetchHandler.ts +29 -0
  46. package/src/adapters/node/createMiddlewareHandler.ts +79 -0
  47. package/src/adapters/node/createNodeServer.ts +8 -0
  48. package/src/adapters/node/flattenHeaders.ts +44 -0
  49. package/src/adapters/node/index.ts +18 -0
  50. package/src/adapters/node/nodeMessageToNodeResponse.ts +89 -0
  51. package/src/adapters/node/nodeMessageToRequest.ts +43 -0
  52. package/src/adapters/node/readableToBuffer.ts +16 -0
  53. package/src/adapters/node/requestToNodeMessage.ts +26 -0
  54. package/src/adapters/node/responseToNodeResponse.ts +44 -0
  55. package/src/bin/index.ts +43 -36
  56. package/src/index.ts +6 -14
  57. package/src/server/CookieHelper.ts +36 -0
  58. package/src/server/HeaderHelper.ts +25 -0
  59. package/src/server/Server.ts +119 -0
  60. package/src/server/ServerEvent.ts +33 -0
  61. package/src/server/ServerFunction.ts +5 -0
  62. package/src/server/contentType.ts +85 -0
  63. package/src/site/Router.ts +136 -0
  64. package/src/site/Routerx.ts +67 -0
  65. package/src/site/Site.ts +114 -0
  66. package/src/site/clientEntry.ts +22 -20
  67. package/src/site/manifest.ts +49 -0
  68. package/src/site/pathToRegex.ts +10 -0
  69. package/src/site/prepareTemplate.ts +20 -0
  70. package/src/site/runBuild.ts +92 -0
  71. package/src/site/runDev.ts +84 -0
  72. package/src/site/runPreview.ts +83 -0
  73. package/src/site/serverEntry.ts +113 -200
  74. package/src/types/Adapter.ts +10 -0
  75. package/src/types/HttpMethod.ts +12 -0
  76. package/src/types/LayoutHandler.ts +7 -0
  77. package/src/types/ManifestRoute.ts +8 -0
  78. package/src/types/MiddlewareFunction.ts +8 -0
  79. package/src/types/PageEndPoint.ts +4 -3
  80. package/src/types/{PageEvent.ts → PageLoadEvent.ts} +3 -2
  81. package/src/types/PageServerAction.ts +5 -2
  82. package/src/types/PageServerEndPoint.ts +1 -0
  83. package/src/types/PageServerLoad.ts +5 -2
  84. package/src/types/RouteHandler.ts +6 -5
  85. package/src/types/RouteHandlerCollection.ts +1 -0
  86. package/src/types/RouteLayoutHandler.ts +3 -2
  87. package/src/types/RouteMatchResult.ts +3 -2
  88. package/src/types/RouteType.ts +7 -0
  89. package/src/types/ServerEndPoint.ts +1 -0
  90. package/src/types/ServerHook.ts +3 -2
  91. package/src/types/{ServerEvent.ts → ServerLoadEvent.ts} +13 -6
  92. package/src/types/ServerRequest.ts +5 -2
  93. package/src/types/manifest.d.ts +7 -0
  94. package/test/app.test.ts +47 -0
  95. package/tsconfig.json +1 -1
  96. package/tsup.config.ts +11 -1
  97. package/vitest.config.ts +3 -0
  98. package/dist/chunk-IE5EKBNX.js +0 -93
  99. package/dist/chunk-IE5EKBNX.js.map +0 -1
  100. package/src/site/FileSystemRouter.ts +0 -25
  101. package/src/site/buildRoutePath.ts +0 -25
  102. package/src/site/defineConfig.ts +0 -75
  103. package/src/site/lazyRoute.ts +0 -72
  104. package/src/site/routeHandlers.ts +0 -126
  105. package/src/types/CookieHelper.ts +0 -8
  106. package/src/types/UserConfig.ts +0 -4
@@ -0,0 +1,25 @@
1
+ export default class HeaderHelper {
2
+ request: Request;
3
+ headers: Map<string, string> = new Map();
4
+
5
+ constructor(request: Request) {
6
+ this.request = request;
7
+ }
8
+
9
+ get(name: string): string | undefined {
10
+ // If it's been set, return the updated value
11
+ if (this.headers.has(name)) {
12
+ return this.headers.get(name);
13
+ }
14
+ // Otherwise, return the value from the header
15
+ return this.request.headers.get(name) ?? undefined;
16
+ }
17
+
18
+ set(name: string, value: string) {
19
+ this.headers.set(name, value);
20
+ }
21
+
22
+ delete(name: string) {
23
+ this.headers.delete(name);
24
+ }
25
+ }
@@ -0,0 +1,119 @@
1
+ import pathToRegex from "../site/pathToRegex";
2
+ import type MiddlewareFunction from "../types/MiddlewareFunction";
3
+ import ServerEvent from "./ServerEvent";
4
+ import type ServerFunction from "./ServerFunction";
5
+
6
+ const noop = () => {};
7
+
8
+ /**
9
+ * A Fetch API Request/Response server.
10
+ */
11
+ export default class Server {
12
+ // TODO: We don't use any of the routes, just add("*") in runDev/runBuild...
13
+ routes: RouteHandler[] = [];
14
+ // TODO: We only use the Vite middleware here
15
+ middleware: MiddlewareFunction[] = [];
16
+
17
+ constructor() {
18
+ this.routes = [];
19
+ this.middleware = [];
20
+ }
21
+
22
+ /**
23
+ * The standard Fetch API method that should work in any runtime (including
24
+ * Node with polyfills).
25
+ * @returns
26
+ */
27
+ fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
28
+ const request = new Request(input, init);
29
+ const url = new URL(request.url);
30
+ const match = this.match(url.pathname);
31
+ let ev = new ServerEvent(request, match?.params);
32
+
33
+ if (this.middleware.length) {
34
+ // TODO: Get middleware that applies to this route only?
35
+ await this.middleware[0](ev, buildNext(this.middleware, 0));
36
+ function buildNext(middleware: MiddlewareFunction[], i: number): () => void | Promise<void> {
37
+ if (ev.response) {
38
+ return noop;
39
+ }
40
+
41
+ if (i < middleware.length - 1) {
42
+ let next = middleware[i + 1];
43
+ return async () => next(ev, buildNext(middleware, i + 1));
44
+ } else {
45
+ return match
46
+ ? async () => {
47
+ ev.response = await match.fn(ev);
48
+ }
49
+ : noop;
50
+ }
51
+ }
52
+ } else if (match) {
53
+ ev.response = await match.fn(ev);
54
+ } else {
55
+ return new Response(null, { status: 404 });
56
+ }
57
+
58
+ // TODO: Should I be returning 200 if there's no response? Because it's not an error??
59
+ ev.response ??= new Response(null, { status: 200 });
60
+ ev.addHeaders();
61
+
62
+ return ev.response;
63
+ };
64
+
65
+ /**
66
+ * Adds a server function at a route path.
67
+ * @param path The route path pattern.
68
+ * @param fn The function to call when the pattern is matched.
69
+ * @returns
70
+ */
71
+ add(path: string, fn: ServerFunction): Server {
72
+ this.routes.push(new RouteHandler(path, fn));
73
+ return this;
74
+ }
75
+
76
+ match(path: string) {
77
+ for (let handler of this.routes) {
78
+ let match = path.match(handler.regex);
79
+ if (match) {
80
+ return {
81
+ fn: handler.fn,
82
+ params: match.groups,
83
+ };
84
+ }
85
+ }
86
+ console.log(`Server path not found: ${path}`);
87
+ }
88
+
89
+ // TODO:
90
+ //use(path: string, ...fn: MiddlewareFunction[])
91
+ use(...fn: MiddlewareFunction[]): Server {
92
+ this.middleware = this.middleware.concat(fn);
93
+ return this;
94
+ }
95
+ }
96
+
97
+ class RouteHandler {
98
+ path: string;
99
+ regex: RegExp;
100
+ fn: ServerFunction;
101
+
102
+ constructor(path: string, fn: ServerFunction) {
103
+ this.path = path;
104
+ this.regex = pathToRegex(path);
105
+ this.fn = fn;
106
+ }
107
+ }
108
+
109
+ //class MiddlewareHandler {
110
+ // path: string;
111
+ // regex: RegExp;
112
+ // fn: MiddlewareFunction;
113
+ //
114
+ // constructor(path: string, fn: MiddlewareFunction) {
115
+ // this.path = path;
116
+ // this.regex = pathToRegex(path);
117
+ // this.fn = fn;
118
+ // }
119
+ //}
@@ -0,0 +1,33 @@
1
+ import CookieHelper from "./CookieHelper";
2
+ import HeaderHelper from "./HeaderHelper";
3
+
4
+ export default class ServerEvent {
5
+ request: Request;
6
+ response?: Response;
7
+ // TODO: Should we cast types??
8
+ params?: { [key: string]: string };
9
+
10
+ cookies: CookieHelper;
11
+ headers: HeaderHelper;
12
+
13
+ constructor(request: Request, params?: { [key: string]: string }) {
14
+ this.request = request;
15
+ this.params = params;
16
+ this.cookies = new CookieHelper(request);
17
+ this.headers = new HeaderHelper(request);
18
+ }
19
+
20
+ addHeaders() {
21
+ if (!this.response) {
22
+ throw new Error("Response not created yet");
23
+ }
24
+
25
+ for (let cookie of this.cookies.cookies.values()) {
26
+ this.response.headers.append("set-cookie", cookie);
27
+ }
28
+
29
+ for (let header of this.headers.headers.entries()) {
30
+ this.response.headers.append(header[0], header[1]);
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,5 @@
1
+ import ServerEvent from "./ServerEvent";
2
+
3
+ type ServerFunction = (e: ServerEvent) => Response | Promise<Response | undefined> | undefined;
4
+
5
+ export default ServerFunction;
@@ -0,0 +1,85 @@
1
+ // From https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types/Common_types
2
+
3
+ export default function contentType(extension: string) {
4
+ return types[extension] || "application/octet-stream";
5
+ }
6
+
7
+ const types: Record<string, string> = {
8
+ ".aac": "audio/aac",
9
+ ".abw": "application/x-abiword",
10
+ ".apng": "image/apng",
11
+ ".arc": "application/x-freearc",
12
+ ".avif": "image/avif",
13
+ ".avi": "video/x-msvideo",
14
+ ".azw": "application/vnd.amazon.ebook",
15
+ ".bin": "application/octet-stream",
16
+ ".bmp": "image/bmp",
17
+ ".bz": "application/x-bzip",
18
+ ".bz2": "application/x-bzip2",
19
+ ".cda": "application/x-cdf",
20
+ ".csh": "application/x-csh",
21
+ ".css": "text/css",
22
+ ".csv": "text/csv",
23
+ ".doc": "application/msword",
24
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
25
+ ".eot": "application/vnd.ms-fontobject",
26
+ ".epub": "application/epub+zip",
27
+ ".gz": "application/gzip",
28
+ ".gif": "image/gif",
29
+ ".htm": "text/html",
30
+ ".html": "text/html",
31
+ ".ico": "image/vnd.microsoft.icon",
32
+ ".ics": "text/calendar",
33
+ ".jar": "application/java-archive",
34
+ ".jpeg": "image/jpeg",
35
+ ".jpg": "image/jpeg",
36
+ ".js": "text/javascript",
37
+ ".json": "application/json",
38
+ ".jsonld": "application/ld+json",
39
+ ".mid": "audio/midi",
40
+ ".midi": "audio/midi",
41
+ ".mjs": "text/javascript",
42
+ ".mp3": "audio/mpeg",
43
+ ".mp4": "video/mp4",
44
+ ".mpeg": "video/mpeg",
45
+ ".mpkg": "application/vnd.apple.installer+xml",
46
+ ".odp": "application/vnd.oasis.opendocument.presentation",
47
+ ".ods": "application/vnd.oasis.opendocument.spreadsheet",
48
+ ".odt": "application/vnd.oasis.opendocument.text",
49
+ ".oga": "audio/ogg",
50
+ ".ogv": "video/ogg",
51
+ ".ogx": "application/ogg",
52
+ ".opus": "audio/ogg",
53
+ ".otf": "font/otf",
54
+ ".png": "image/png",
55
+ ".pdf": "application/pdf",
56
+ ".php": "application/x-httpd-php",
57
+ ".ppt": "application/vnd.ms-powerpoint",
58
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
59
+ ".rar": "application/vnd.rar",
60
+ ".rtf": "application/rtf",
61
+ ".sh": "application/x-sh",
62
+ ".svg": "image/svg+xml",
63
+ ".tar": "application/x-tar",
64
+ ".tif": "image/tiff",
65
+ ".tiff": "image/tiff",
66
+ ".ts": "video/mp2t",
67
+ ".ttf": "font/ttf",
68
+ ".txt": "text/plain",
69
+ ".vsd": "application/vnd.visio",
70
+ ".wav": "audio/wav",
71
+ ".weba": "audio/webm",
72
+ ".webm": "video/webm",
73
+ ".webp": "image/webp",
74
+ ".woff": "font/woff",
75
+ ".woff2": "font/woff2",
76
+ ".xhtml": "application/xhtml+xml",
77
+ ".xls": "application/vnd.ms-excel",
78
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
79
+ ".xml": "application/xml",
80
+ ".xul": "application/vnd.mozilla.xul+xml",
81
+ ".zip": "application/zip",
82
+ ".3gp": "video/3gpp",
83
+ ".3g2": "video/3gpp2",
84
+ ".7z": "application/x-7z-compressed",
85
+ };
@@ -0,0 +1,136 @@
1
+ import type LayoutHandler from "../types/LayoutHandler";
2
+ import type ManifestRoute from "../types/ManifestRoute";
3
+ import type RouteHandler from "../types/RouteHandler";
4
+ import pathToRegex from "./pathToRegex";
5
+
6
+ export default class Router {
7
+ routes: Route[] = [];
8
+ //pages = new Map<string, PageEndPoint>();
9
+
10
+ constructor() {}
11
+
12
+ //add(path: string, file: string) {
13
+ // // TODO: Call the specific addX method based on the file path
14
+ // this.addPage(route, file);
15
+ //}
16
+
17
+ addPages(routes: ManifestRoute[]) {
18
+ for (let r of routes) {
19
+ this.routes.push(
20
+ new Route(r.path, {
21
+ path: r.path,
22
+ type: r.type,
23
+ endPoint: r.endPoint,
24
+ }),
25
+ );
26
+ }
27
+ return this;
28
+ }
29
+
30
+ // TODO: Allow calling with the endpoint itself, so that you can setup an app with no scaffold
31
+ addPage(path: string, type: number, endPoint: () => Promise<any>) {
32
+ //this.server.get(route, (ev) => this.loadPage(ev, file));
33
+ this.routes.push(
34
+ new Route(path, {
35
+ path,
36
+ type,
37
+ endPoint,
38
+ }),
39
+ );
40
+ return this;
41
+ }
42
+
43
+ /*
44
+ loadPage(ev: ServerEvent, file: string): Response {
45
+ let page = this.pages.get(file);
46
+ if (!page) {
47
+ page = {};
48
+ this.pages.set(file, page);
49
+ }
50
+ }
51
+ */
52
+
53
+ match(path: string, query: URLSearchParams) {
54
+ for (let route of this.routes) {
55
+ let match = path.match(route.regex);
56
+ if (match) {
57
+ // Lazy load server endpoints and layouts
58
+ if (!route.handler.loaded) {
59
+ this.loadHandler(route.handler, route.path);
60
+ }
61
+
62
+ return {
63
+ handler: route.handler,
64
+ params: match.groups,
65
+ query,
66
+ };
67
+ }
68
+ }
69
+
70
+ console.log(`not found: '${path}'`);
71
+ }
72
+
73
+ loadHandler(handler: RouteHandler, path: string) {
74
+ ////handler.endPoint = import(handler.path);
75
+ handler.layouts = this.findLayouts(path);
76
+ handler.serverEndPoint = this.findServer(path);
77
+ handler.serverHook = this.findServerHook(path);
78
+ }
79
+
80
+ findLayouts(path: string): LayoutHandler[] | undefined {
81
+ let layouts: LayoutHandler[] = [];
82
+ let parts = path
83
+ // Strip the trailing`~/server` (so we don't look for `~/server/_layout`
84
+ // which will never exist)
85
+ .replace(/\/~server$/, "")
86
+ // The path will always start with / so splitting e.g. `/first/second`
87
+ // will result in ['', 'first', 'second'] and checks for `/_layout`,
88
+ // `/first/_layout` and `/second/_layout`
89
+ .split("/");
90
+ let basePath = "";
91
+ for (let i = 0; i < parts.length; i++) {
92
+ if (i > 0) {
93
+ basePath += "/" + parts[i];
94
+ }
95
+ const layoutPath = basePath + "/_layout";
96
+ const layoutRoute = this.routes.find((r) => r.path === layoutPath);
97
+ if (layoutRoute) {
98
+ layouts.push({
99
+ path: layoutPath,
100
+ endPoint: layoutRoute.handler.endPoint,
101
+ serverEndPoint: this.findServer(layoutPath),
102
+ });
103
+ }
104
+ }
105
+ return layouts.length ? layouts : undefined;
106
+ }
107
+
108
+ findServer(path: string): (() => Promise<any>) | undefined {
109
+ const serverPath = path.replace(/\/$/, "") + "/~server";
110
+ const serverRoute = this.routes.find((r) => r.path === serverPath);
111
+ return serverRoute && serverRoute.handler.endPoint;
112
+ }
113
+
114
+ findServerHook(path: string): (() => Promise<any>) | undefined {
115
+ // TODO: Should this be a collection, like layouts?
116
+ let serverHookPath = "/_hook/~server";
117
+ // HACK: Generalise this
118
+ if (path.startsWith("/api/")) {
119
+ serverHookPath = "/api" + serverHookPath;
120
+ }
121
+ const serverHookRoute = this.routes.find((r) => r.path === serverHookPath);
122
+ return serverHookRoute && serverHookRoute.handler.endPoint;
123
+ }
124
+ }
125
+
126
+ class Route {
127
+ path: string;
128
+ regex: RegExp;
129
+ handler: RouteHandler;
130
+
131
+ constructor(path: string, handler: RouteHandler) {
132
+ this.path = path;
133
+ this.regex = pathToRegex(path);
134
+ this.handler = handler;
135
+ }
136
+ }
@@ -0,0 +1,67 @@
1
+ import ServerFunction from "../server/ServerFunction";
2
+ import type HttpMethod from "../types/HttpMethod";
3
+ import type MiddlewareFunction from "../types/MiddlewareFunction";
4
+ import pathToRegex from "./pathToRegex";
5
+
6
+ // This is not actually used anywhere, for now at least
7
+
8
+ export default class Router {
9
+ methods = new Map<HttpMethod, RouteHandler[]>();
10
+ middleware: MiddlewareHandler[] = [];
11
+
12
+ constructor() {
13
+ // Create all method handlers up front so we don't have to worry about
14
+ // checking if they exist. Using a non-standard method will just error
15
+ this.methods.set("GET", []);
16
+ this.methods.set("HEAD", []);
17
+ this.methods.set("PATCH", []);
18
+ this.methods.set("POST", []);
19
+ this.methods.set("PUT", []);
20
+ this.methods.set("DELETE", []);
21
+ this.methods.set("OPTIONS", []);
22
+ this.methods.set("CONNECT", []);
23
+ this.methods.set("TRACE", []);
24
+ }
25
+
26
+ add(method: HttpMethod, path: string, fn: ServerFunction): Router {
27
+ this.methods.get(method)!.push(new RouteHandler(path, fn));
28
+ return this;
29
+ }
30
+
31
+ match(method: HttpMethod, path: string) {
32
+ for (let handler of this.methods.get(method)!) {
33
+ let match = path.match(handler.regex);
34
+ if (match) {
35
+ console.log("found", handler);
36
+ return {
37
+ fn: handler.fn,
38
+ params: match.groups,
39
+ };
40
+ }
41
+ }
42
+ }
43
+ }
44
+
45
+ class RouteHandler {
46
+ path: string;
47
+ regex: RegExp;
48
+ fn: ServerFunction;
49
+
50
+ constructor(path: string, fn: ServerFunction) {
51
+ this.path = path;
52
+ this.regex = pathToRegex(path);
53
+ this.fn = fn;
54
+ }
55
+ }
56
+
57
+ class MiddlewareHandler {
58
+ path: string;
59
+ regex: RegExp;
60
+ fn: MiddlewareFunction;
61
+
62
+ constructor(path: string, fn: MiddlewareFunction) {
63
+ this.path = path;
64
+ this.regex = pathToRegex(path);
65
+ this.fn = fn;
66
+ }
67
+ }
@@ -0,0 +1,114 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { Plugin } from "vite";
4
+ import tsconfigPaths from "vite-tsconfig-paths";
5
+ import node from "../adapters/node";
6
+ import Adapter from "../types/Adapter";
7
+ import {
8
+ HOOK_ROUTE,
9
+ HOOK_SERVER_ROUTE,
10
+ LAYOUT_ROUTE,
11
+ LAYOUT_SERVER_ROUTE,
12
+ PAGE_ROUTE,
13
+ PAGE_SERVER_ROUTE,
14
+ SERVER_ROUTE,
15
+ } from "../types/RouteType";
16
+
17
+ // 1. The user sets this up
18
+ // 2. It is passed into serverEntry and clientEntry through a Vite plugin
19
+ // (defined in manifest.ts)
20
+ // 3. In those files, it is used to build a Router
21
+
22
+ /**
23
+ * The Site class contains the information necessary to setup a @torpor/build
24
+ * site, including routes, plugins, and options.
25
+ */
26
+ export default class Site {
27
+ root: string;
28
+ routes: { path: string; file: string; type: number }[] = [];
29
+ // Is default plugins a bad idea?
30
+ // HACK: Set loose because otherwise it uses a list of allowed extensions
31
+ // TODO: Should probably just do this ourselves and let the user pass in
32
+ // tsconfigPaths themselves if they want to do something funky
33
+ plugins: Plugin[] = [tsconfigPaths({ loose: true })];
34
+ // Is default adapter a bad idea?
35
+ adapter: Adapter = node;
36
+
37
+ constructor() {
38
+ this.root = process.cwd();
39
+ }
40
+
41
+ async addRouteFolder(folder: string) {
42
+ const routeFolder = path.join(this.root, folder);
43
+ const routeFiles = await fs.readdir(routeFolder, { recursive: true });
44
+
45
+ for (let file of routeFiles) {
46
+ // The file must start with `+` or `_` and end with `.js` or `.ts`
47
+ if (/^(\+|_).+(\.js|\.ts)$/.test(path.basename(file))) {
48
+ let routePath = file
49
+ .replace(/^\//, "")
50
+ .replace(/(\.ts|\.js)$/, "")
51
+ .replace(/_hook.server$/, "_hook/~server")
52
+ .replace(/_layout.server$/, "_layout/~server")
53
+ .replace(/\+page.server$/, "~server")
54
+ .replace(/\+server$/, "")
55
+ .replace(/\+page$/, "")
56
+ .replace(/\/$/, "");
57
+ routePath = routePath.length > 0 ? `/${routePath}` : "/";
58
+ let type = this.#routeType(file);
59
+ file = path.relative(this.root, path.resolve(routeFolder, file));
60
+ this.routes.push({ path: routePath, file, type });
61
+ }
62
+ }
63
+ this.#sortRoutes();
64
+ }
65
+
66
+ addRouteFile(path: string, file: string) {
67
+ let type = this.#routeType(file);
68
+ this.routes.push({ path, file, type });
69
+ this.#sortRoutes();
70
+ }
71
+
72
+ #routeType(file: string): number {
73
+ let routePath = file.replace(/^\//, "").replace(/(\.ts|\.js)$/, "");
74
+ if (routePath.endsWith("+page")) {
75
+ return PAGE_ROUTE;
76
+ } else if (routePath.endsWith("+page.server")) {
77
+ return PAGE_SERVER_ROUTE;
78
+ } else if (routePath.endsWith("+server")) {
79
+ return SERVER_ROUTE;
80
+ } else if (routePath.endsWith("_layout")) {
81
+ return LAYOUT_ROUTE;
82
+ } else if (routePath.endsWith("_layout.server")) {
83
+ return LAYOUT_SERVER_ROUTE;
84
+ } else if (routePath.endsWith("_hook")) {
85
+ return HOOK_ROUTE;
86
+ } else if (routePath.endsWith("_hook.server")) {
87
+ return HOOK_SERVER_ROUTE;
88
+ }
89
+ return -1;
90
+ }
91
+
92
+ // TODO: You could make your site in a single file, like e.g. Hono...
93
+ //addPage(path: string, endPoint: PageEndPoint, serverEndPoint?: PageServerEndPoint) {}
94
+ //addEndPoint(path: string, endPoint: ServerEndPoint) {}
95
+
96
+ #sortRoutes() {
97
+ this.routes = this.routes.sort((a, b) => {
98
+ // Sort [param]s after paths
99
+ // There might be a quicker/easier way to do this
100
+ for (let i = 0; i < Math.min(a.path.length, b.path.length); i++) {
101
+ if (a.path[i] === "[" && b.path[i] !== "[") {
102
+ return 1;
103
+ } else if (b.path[i] === "[" && a.path[i] !== "[") {
104
+ return -1;
105
+ } else if (a.path[i] === b.path[i]) {
106
+ // Keep going...
107
+ } else {
108
+ return a.path[i].localeCompare(b.path[i]);
109
+ }
110
+ }
111
+ return a.path.length - b.path.length;
112
+ });
113
+ }
114
+ }
@@ -1,10 +1,14 @@
1
+ import manifest from "@torpor/build/manifest";
1
2
  import { hydrate, mount } from "@torpor/view";
2
3
  import { type Component, type SlotRender } from "@torpor/view";
3
- import "vinxi/client";
4
- import $page from "../state/$page";
5
- import type PageEndPoint from "../types/PageEndPoint";
6
- import type PageServerEndPoint from "../types/PageServerEndPoint";
7
- import routeHandlers from "./routeHandlers";
4
+ import $page from "../state/$page.ts";
5
+ import type PageEndPoint from "../types/PageEndPoint.ts";
6
+ import type PageServerEndPoint from "../types/PageServerEndPoint.ts";
7
+ import Router from "./Router.ts";
8
+
9
+ // Build the router from the Site object created by the user
10
+ const router = new Router();
11
+ router.addPages(manifest.routes);
8
12
 
9
13
  // Intercept clicks on links
10
14
  window.addEventListener("click", async (e) => {
@@ -38,12 +42,8 @@ async function navigateToLocation(location: Location, firstTime = false) {
38
42
  return await navigate(location.pathname, new URLSearchParams(location.search), firstTime);
39
43
  }
40
44
 
41
- async function navigate(
42
- path: string,
43
- urlParams: URLSearchParams,
44
- firstTime = false,
45
- ): Promise<boolean> {
46
- console.log("navigating client to", path, urlParams.size ? `with ${urlParams}` : "");
45
+ async function navigate(path: string, query: URLSearchParams, firstTime = false): Promise<boolean> {
46
+ console.log(`navigating to '${path}'${query.size ? ` with ${query}` : ""}`);
47
47
 
48
48
  const parent = document.getElementById("app");
49
49
  if (!parent) {
@@ -52,7 +52,7 @@ async function navigate(
52
52
  return false;
53
53
  }
54
54
 
55
- const route = routeHandlers.match(path, urlParams);
55
+ const route = router.match(path, query);
56
56
  if (!route) {
57
57
  // TODO: 404
58
58
  console.log("404");
@@ -64,10 +64,10 @@ async function navigate(
64
64
  //$page.url = new URL(document.location.href);
65
65
 
66
66
  const handler = route.handler;
67
- const params = route.routeParams || {};
67
+ const params = route.params || {};
68
68
 
69
69
  // There must be a client endpoint with a component
70
- const clientEndPoint: PageEndPoint | undefined = (await handler.endPoint).default;
70
+ const clientEndPoint: PageEndPoint | undefined = (await handler.endPoint()).default;
71
71
  if (!clientEndPoint?.component) {
72
72
  // TODO: 404
73
73
  console.log("404");
@@ -75,7 +75,8 @@ async function navigate(
75
75
  }
76
76
 
77
77
  // There may be a server endpoint
78
- const serverEndPoint: PageServerEndPoint | undefined = (await handler.serverEndPoint)?.default;
78
+ const serverEndPoint: PageServerEndPoint | undefined =
79
+ handler.serverEndPoint && (await handler.serverEndPoint())?.default;
79
80
 
80
81
  // Pass the data into $props
81
82
  // TODO: Don't load if this is the first time -- it should have been passed to us, somehow...
@@ -86,9 +87,9 @@ async function navigate(
86
87
  for (let key in params) {
87
88
  layoutPath = layoutPath.replace(`[${key}]`, params[key]);
88
89
  }
89
- const layoutEndPoint: PageEndPoint | undefined = (await layout.endPoint)?.default;
90
- const layoutServerEndPoint: PageServerEndPoint | undefined = (await layout.serverEndPoint)
91
- ?.default;
90
+ const layoutEndPoint: PageEndPoint | undefined = (await layout.endPoint())?.default;
91
+ const layoutServerEndPoint: PageServerEndPoint | undefined =
92
+ layout.serverEndPoint && (await layout.serverEndPoint())?.default;
92
93
  const layoutData = await loadClientAndServerData(
93
94
  data,
94
95
  document.location.origin + layoutPath,
@@ -120,7 +121,8 @@ async function navigate(
120
121
  slotFunctions[handler.layouts.length] = (parent, anchor, _, context) =>
121
122
  clientEndPoint.component!(parent, anchor, $props, context);
122
123
  for (let i = handler.layouts.length - 1; i >= 0; i--) {
123
- const layoutEndPoint: PageEndPoint | undefined = (await handler.layouts[i].endPoint)?.default;
124
+ const layoutEndPoint: PageEndPoint | undefined = (await handler.layouts[i].endPoint())
125
+ ?.default;
124
126
  if (layoutEndPoint?.component) {
125
127
  if (i === 0) {
126
128
  component = layoutEndPoint.component as Component;
@@ -174,7 +176,7 @@ async function loadClientAndServerData(
174
176
  const serverResponse = await fetch(serverUrl);
175
177
  if (
176
178
  serverResponse?.ok &&
177
- serverResponse.headers.get("content-type")?.includes("application/json")
179
+ serverResponse.headers.get("Content-Type")?.includes("application/json")
178
180
  ) {
179
181
  const serverData = await serverResponse.json();
180
182
  Object.assign(newData, serverData);