@torpor/build 0.4.10 → 0.4.12
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/dist/{_page-DBSsnHFG.mjs → _page-BQs4WvCO.mjs} +2 -2
- package/dist/_page-BQs4WvCO.mjs.map +1 -0
- package/dist/nav.mjs +2 -2
- package/dist/nav.mjs.map +1 -1
- package/dist/state.mjs +1 -1
- package/dist/test.mjs +1 -1
- package/package.json +4 -4
- package/src/bin/index.ts +11 -0
- package/src/index.ts +30 -0
- package/src/nav/formSubmit.ts +56 -0
- package/src/nav/load.ts +9 -0
- package/src/nav/loadData.ts +134 -0
- package/src/nav/navigate.ts +164 -0
- package/src/nav/reload.ts +9 -0
- package/src/nav.ts +4 -0
- package/src/response/badRequest.ts +19 -0
- package/src/response/created.ts +20 -0
- package/src/response/forbidden.ts +21 -0
- package/src/response/found.ts +30 -0
- package/src/response/methodNotAllowed.ts +18 -0
- package/src/response/movedPermanently.ts +25 -0
- package/src/response/notFound.ts +24 -0
- package/src/response/notModified.ts +34 -0
- package/src/response/ok.ts +29 -0
- package/src/response/permanentRedirect.ts +30 -0
- package/src/response/response.ts +22 -0
- package/src/response/seeOther.ts +19 -0
- package/src/response/serverError.ts +24 -0
- package/src/response/temporaryRedirect.ts +31 -0
- package/src/response/transfer.ts +14 -0
- package/src/response/unauthorized.ts +21 -0
- package/src/response/unprocessable.ts +18 -0
- package/src/response.ts +37 -0
- package/src/run/prepareTemplate.ts +39 -0
- package/src/run/run.ts +62 -0
- package/src/run/runBuild.ts +77 -0
- package/src/run/runDev.ts +89 -0
- package/src/run/runPreview.ts +93 -0
- package/src/run.ts +6 -0
- package/src/server/CookieHelper.ts +36 -0
- package/src/server/HeaderHelper.ts +25 -0
- package/src/server/Routerx.ts +72 -0
- package/src/server/Server.ts +144 -0
- package/src/server/ServerEvent.ts +39 -0
- package/src/server/connect/bufferToArrayBuffer.ts +8 -0
- package/src/server/connect/connectMiddleware.ts +81 -0
- package/src/server/connect/flattenHeaders.ts +44 -0
- package/src/server/connect/nodeMessageToNodeResponse.ts +89 -0
- package/src/server/connect/readableToBuffer.ts +16 -0
- package/src/server/connect/requestToNodeMessage.ts +26 -0
- package/src/server/contentType.ts +85 -0
- package/src/server/types/HttpMethod.ts +12 -0
- package/src/server/types/MiddlewareFunction.ts +8 -0
- package/src/server/types/ServerFunction.ts +7 -0
- package/src/server.ts +7 -0
- package/src/site/Router.ts +164 -0
- package/src/site/Site.ts +214 -0
- package/src/site/clientEntry.ts +108 -0
- package/src/site/clientEntryDev.ts +3 -0
- package/src/site/defaultAdapter.ts +11 -0
- package/src/site/manifest.ts +54 -0
- package/src/site/serverEntry.ts +417 -0
- package/src/state/$page.ts +24 -0
- package/src/state/client.ts +15 -0
- package/src/state.ts +3 -0
- package/src/test/runTest.ts +350 -0
- package/src/test.ts +3 -0
- package/src/types/Adapter.ts +8 -0
- package/src/types/ClientState.ts +8 -0
- package/src/types/InternalState.ts +7 -0
- package/src/types/LayoutHandler.ts +5 -0
- package/src/types/LayoutPath.ts +10 -0
- package/src/types/ManifestRoute.ts +6 -0
- package/src/types/PageEndPoint.ts +39 -0
- package/src/types/PageLoadEvent.ts +14 -0
- package/src/types/PageServerAction.ts +7 -0
- package/src/types/PageServerEndPoint.ts +16 -0
- package/src/types/PageServerLoad.ts +7 -0
- package/src/types/PageState.ts +8 -0
- package/src/types/Route.ts +11 -0
- package/src/types/RouteHandler.ts +16 -0
- package/src/types/RouteLayoutHandler.ts +5 -0
- package/src/types/RouteMatchResult.ts +7 -0
- package/src/types/RouteType.ts +19 -0
- package/src/types/ServerEndPoint.ts +37 -0
- package/src/types/ServerHook.ts +11 -0
- package/src/types/ServerLoadEvent.ts +37 -0
- package/src/types/ServerRequest.ts +7 -0
- package/src/utils/pathToRegex.ts +15 -0
- package/src/vite-env.d.ts +2 -0
- package/dist/_page-DBSsnHFG.mjs.map +0 -1
|
@@ -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): 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
|
+
};
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import CookieHelper from "./server/CookieHelper";
|
|
2
|
+
import HeaderHelper from "./server/HeaderHelper";
|
|
3
|
+
import Server from "./server/Server";
|
|
4
|
+
import ServerEvent from "./server/ServerEvent";
|
|
5
|
+
import connectMiddleware from "./server/connect/connectMiddleware";
|
|
6
|
+
|
|
7
|
+
export { Server, ServerEvent, CookieHelper, HeaderHelper, connectMiddleware };
|
|
@@ -0,0 +1,164 @@
|
|
|
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 "../utils/pathToRegex";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A router that handles file routes with layouts, hooks, etc.
|
|
8
|
+
*/
|
|
9
|
+
export default class Router {
|
|
10
|
+
routes: Route[] = [];
|
|
11
|
+
|
|
12
|
+
constructor() {}
|
|
13
|
+
|
|
14
|
+
//add(path: string, file: string) {
|
|
15
|
+
// // TODO: Call the specific addX method based on the file path
|
|
16
|
+
// this.addPage(route, file);
|
|
17
|
+
//}
|
|
18
|
+
|
|
19
|
+
addPages(routes: ManifestRoute[]): this {
|
|
20
|
+
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
|
+
);
|
|
29
|
+
}
|
|
30
|
+
this.#sortRoutes();
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// TODO: Allow calling with the endpoint itself, so that you can setup an app with no scaffold
|
|
35
|
+
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
|
+
);
|
|
44
|
+
this.#sortRoutes();
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/*
|
|
49
|
+
loadPage(ev: ServerEvent, file: string): Response {
|
|
50
|
+
let page = this.pages.get(file);
|
|
51
|
+
if (!page) {
|
|
52
|
+
page = {};
|
|
53
|
+
this.pages.set(file, page);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
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
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#loadHandler(handler: RouteHandler, path: string) {
|
|
77
|
+
handler.layouts = this.#findLayouts(path, handler);
|
|
78
|
+
handler.serverEndPoint = this.#findServer(path);
|
|
79
|
+
handler.serverHook = this.#findServerHook(handler);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#findLayouts(path: string, handler: RouteHandler): LayoutHandler[] | undefined {
|
|
83
|
+
let layouts: LayoutHandler[] = [];
|
|
84
|
+
let parts = path
|
|
85
|
+
// Strip the trailing`~/server` (so we don't look for `~/server/_layout`
|
|
86
|
+
// which will never exist)
|
|
87
|
+
.replace(/\/~server$/, "")
|
|
88
|
+
// The path will always start with / so splitting e.g. `/first/second`
|
|
89
|
+
// will result in ['', 'first', 'second'] and checks for `/_layout`,
|
|
90
|
+
// `/first/_layout` and `/second/_layout`
|
|
91
|
+
.split("/");
|
|
92
|
+
let basePath = "";
|
|
93
|
+
for (let i = 0; i < parts.length; i++) {
|
|
94
|
+
if (i > 0) {
|
|
95
|
+
basePath += "/" + parts[i];
|
|
96
|
+
}
|
|
97
|
+
const layoutPath = basePath + "/_layout";
|
|
98
|
+
const layoutRoute = this.routes.find((r) => r.path === layoutPath);
|
|
99
|
+
if (layoutRoute && layoutRoute.handler.subFolder === handler.subFolder) {
|
|
100
|
+
layouts.push({
|
|
101
|
+
path: layoutPath,
|
|
102
|
+
endPoint: layoutRoute.handler.endPoint,
|
|
103
|
+
serverEndPoint: this.#findServer(layoutPath),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return layouts.length ? layouts : undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#findServer(path: string): (() => Promise<any>) | undefined {
|
|
111
|
+
const serverPath = path.replace(/\/$/, "") + "/~server";
|
|
112
|
+
const serverRoute = this.routes.find((r) => r.path === serverPath);
|
|
113
|
+
return serverRoute && serverRoute.handler.endPoint;
|
|
114
|
+
}
|
|
115
|
+
|
|
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;
|
|
121
|
+
}
|
|
122
|
+
const serverHookRoute = this.routes.find((r) => r.path === serverHookPath);
|
|
123
|
+
return serverHookRoute && serverHookRoute.handler.endPoint;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// HACK: We're doing this here as well as in Site, so that you can use the
|
|
127
|
+
// Router externally without Site
|
|
128
|
+
#sortRoutes() {
|
|
129
|
+
this.routes = this.routes.sort((a, b) => {
|
|
130
|
+
// Sort [param]s after paths
|
|
131
|
+
// There might be a quicker/easier way to do this
|
|
132
|
+
for (let i = 0; i < Math.min(a.path.length, b.path.length); i++) {
|
|
133
|
+
if (a.path[i] === "[" && b.path[i] !== "[") {
|
|
134
|
+
return 1;
|
|
135
|
+
} else if (b.path[i] === "[" && a.path[i] !== "[") {
|
|
136
|
+
return -1;
|
|
137
|
+
} else if (a.path[i] === b.path[i]) {
|
|
138
|
+
// Keep going...
|
|
139
|
+
} else {
|
|
140
|
+
return a.path[i].localeCompare(b.path[i]);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return a.path.length - b.path.length;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
class Route {
|
|
149
|
+
path: string;
|
|
150
|
+
regex: RegExp;
|
|
151
|
+
handler: RouteHandler;
|
|
152
|
+
|
|
153
|
+
constructor(path: string, handler: RouteHandler) {
|
|
154
|
+
this.path = path;
|
|
155
|
+
this.regex = pathToRegex(path);
|
|
156
|
+
this.handler = handler;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
interface RouteMatch {
|
|
161
|
+
handler: RouteHandler;
|
|
162
|
+
params?: Record<string, string>;
|
|
163
|
+
query: URLSearchParams;
|
|
164
|
+
}
|
package/src/site/Site.ts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import fpath from "node:path";
|
|
3
|
+
import { type Plugin, type UserConfig } from "vite";
|
|
4
|
+
import tsconfigPaths from "vite-tsconfig-paths";
|
|
5
|
+
import type Adapter from "../types/Adapter";
|
|
6
|
+
import type Route from "../types/Route";
|
|
7
|
+
import {
|
|
8
|
+
ERROR_ROUTE,
|
|
9
|
+
HOOK_ROUTE,
|
|
10
|
+
HOOK_SERVER_ROUTE,
|
|
11
|
+
LAYOUT_ROUTE,
|
|
12
|
+
LAYOUT_SERVER_ROUTE,
|
|
13
|
+
PAGE_ROUTE,
|
|
14
|
+
PAGE_SERVER_ROUTE,
|
|
15
|
+
RouteType,
|
|
16
|
+
SERVER_ROUTE,
|
|
17
|
+
} from "../types/RouteType";
|
|
18
|
+
import defaultAdapter from "./defaultAdapter";
|
|
19
|
+
|
|
20
|
+
// 1. The user sets this up
|
|
21
|
+
// 2. It is passed into serverEntry and clientEntry through a Vite plugin
|
|
22
|
+
// (defined in manifest.ts)
|
|
23
|
+
// 3. In those files, it is used to build a Router
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The Site class contains the information necessary to setup a @torpor/build
|
|
27
|
+
* site, including routes, plugins, and options.
|
|
28
|
+
*/
|
|
29
|
+
export default class Site {
|
|
30
|
+
root: string;
|
|
31
|
+
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 })];
|
|
37
|
+
// Is default adapter a bad idea?
|
|
38
|
+
adapter: Adapter = defaultAdapter;
|
|
39
|
+
/**
|
|
40
|
+
* Extra inputs, as absolute file paths, that will be compiled with the
|
|
41
|
+
* output. What you do with them after that is up to you. File paths ending
|
|
42
|
+
* with server.ts/js will only be compiled with the server output
|
|
43
|
+
*/
|
|
44
|
+
inputs: string[] = [];
|
|
45
|
+
/**
|
|
46
|
+
* Vite config options to merge with the standard Torpor build options.
|
|
47
|
+
* `plugins` and `build.rollupOptions.input` will be overridden, so you
|
|
48
|
+
* should set `Site.plugins` and `Site.inputs` instead. Other options may be
|
|
49
|
+
* overridden or incompatible, so experimentation may be required
|
|
50
|
+
*/
|
|
51
|
+
viteConfig?: UserConfig;
|
|
52
|
+
|
|
53
|
+
constructor() {
|
|
54
|
+
this.root = process.cwd();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Adds a folder of routes, based on the names of the files in the folder
|
|
59
|
+
* @param folder The folder containing the routes
|
|
60
|
+
* @param subFolder If a subFolder is set, all routes in the folder will be
|
|
61
|
+
* placed under the subFolder, and only hooks, layouts etc will be used if
|
|
62
|
+
* they have the same subFolder. This can be used to create e.g. an `api`
|
|
63
|
+
* set of routes with their own hook for authorization from various clients
|
|
64
|
+
*/
|
|
65
|
+
async addRouteFolder(folder: string, subFolder?: string): Promise<void> {
|
|
66
|
+
const routeFolder = fpath.join(this.root, folder);
|
|
67
|
+
const routeFiles = await fs.readdir(routeFolder, { recursive: true });
|
|
68
|
+
if (subFolder !== undefined && !subFolder.startsWith("/")) {
|
|
69
|
+
subFolder = "/" + subFolder;
|
|
70
|
+
}
|
|
71
|
+
for (let file of routeFiles) {
|
|
72
|
+
// The file must start with `+` or `_` and end with `.js` or `.ts`
|
|
73
|
+
if (/^(\+|_).+(\.js|\.ts)$/.test(fpath.basename(file))) {
|
|
74
|
+
let routePath = this.#routePath(file);
|
|
75
|
+
if (subFolder !== undefined) {
|
|
76
|
+
routePath = subFolder + routePath;
|
|
77
|
+
}
|
|
78
|
+
let type = this.#routeType(file);
|
|
79
|
+
file = fpath.relative(this.root, fpath.resolve(routeFolder, file));
|
|
80
|
+
this.routes.push({
|
|
81
|
+
path: routePath,
|
|
82
|
+
file,
|
|
83
|
+
type,
|
|
84
|
+
subFolder: subFolder,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
this.#sortRoutes();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Adds a file route, which can have a standard name (e.g. +page.ts) or any
|
|
93
|
+
* name if you pass the type
|
|
94
|
+
* @param path The route path
|
|
95
|
+
* @param file The file
|
|
96
|
+
* @param type The file type (if the file name is non-standard)
|
|
97
|
+
*/
|
|
98
|
+
addRouteFile(
|
|
99
|
+
path: string,
|
|
100
|
+
file: string,
|
|
101
|
+
type?:
|
|
102
|
+
| "+page"
|
|
103
|
+
| "+page.server"
|
|
104
|
+
| "+server"
|
|
105
|
+
| "_layout"
|
|
106
|
+
| "_layout.server"
|
|
107
|
+
| "_hook"
|
|
108
|
+
| "_hook.server"
|
|
109
|
+
| "_error",
|
|
110
|
+
subFolder?: string,
|
|
111
|
+
): void {
|
|
112
|
+
let routePath = this.#routePath(fpath.join(path, type ?? fpath.basename(file)));
|
|
113
|
+
let routeType = type ? this.#routeTypeFromString(type) : this.#routeType(file);
|
|
114
|
+
if (subFolder !== undefined && !subFolder.startsWith("/")) {
|
|
115
|
+
subFolder = "/" + subFolder;
|
|
116
|
+
}
|
|
117
|
+
this.routes.push({
|
|
118
|
+
path: routePath,
|
|
119
|
+
file,
|
|
120
|
+
type: routeType,
|
|
121
|
+
subFolder: subFolder,
|
|
122
|
+
});
|
|
123
|
+
this.#sortRoutes();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#routePath(file: string): string {
|
|
127
|
+
let routePath = file
|
|
128
|
+
.replace(/^\//, "")
|
|
129
|
+
.replace(/(\.ts|\.js)$/, "")
|
|
130
|
+
.replace(/_hook.server$/, "_hook/~server")
|
|
131
|
+
.replace(/_layout.server$/, "_layout/~server")
|
|
132
|
+
.replace(/\+page.server$/, "~server")
|
|
133
|
+
.replace(/\+server$/, "")
|
|
134
|
+
.replace(/\+page$/, "")
|
|
135
|
+
.replace(/\/$/, "");
|
|
136
|
+
return routePath.length > 0 ? `/${routePath}` : "/";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#routeType(file: string): RouteType {
|
|
140
|
+
let routePath = file.replace(/^\//, "").replace(/(\.ts|\.js)$/, "");
|
|
141
|
+
if (routePath.endsWith("+page")) {
|
|
142
|
+
return PAGE_ROUTE;
|
|
143
|
+
} else if (routePath.endsWith("+page.server")) {
|
|
144
|
+
return PAGE_SERVER_ROUTE;
|
|
145
|
+
} else if (routePath.endsWith("+server")) {
|
|
146
|
+
return SERVER_ROUTE;
|
|
147
|
+
} else if (routePath.endsWith("_layout")) {
|
|
148
|
+
return LAYOUT_ROUTE;
|
|
149
|
+
} else if (routePath.endsWith("_layout.server")) {
|
|
150
|
+
return LAYOUT_SERVER_ROUTE;
|
|
151
|
+
} else if (routePath.endsWith("_hook")) {
|
|
152
|
+
return HOOK_ROUTE;
|
|
153
|
+
} else if (routePath.endsWith("_hook.server")) {
|
|
154
|
+
return HOOK_SERVER_ROUTE;
|
|
155
|
+
} else if (routePath.endsWith("_error")) {
|
|
156
|
+
return ERROR_ROUTE;
|
|
157
|
+
}
|
|
158
|
+
return -1;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#routeTypeFromString(
|
|
162
|
+
type:
|
|
163
|
+
| "+page"
|
|
164
|
+
| "+page.server"
|
|
165
|
+
| "+server"
|
|
166
|
+
| "_layout"
|
|
167
|
+
| "_layout.server"
|
|
168
|
+
| "_hook"
|
|
169
|
+
| "_hook.server"
|
|
170
|
+
| "_error",
|
|
171
|
+
) {
|
|
172
|
+
switch (type) {
|
|
173
|
+
case "+page":
|
|
174
|
+
return PAGE_ROUTE;
|
|
175
|
+
case "+page.server":
|
|
176
|
+
return PAGE_SERVER_ROUTE;
|
|
177
|
+
case "+server":
|
|
178
|
+
return SERVER_ROUTE;
|
|
179
|
+
case "_layout":
|
|
180
|
+
return LAYOUT_ROUTE;
|
|
181
|
+
case "_layout.server":
|
|
182
|
+
return LAYOUT_SERVER_ROUTE;
|
|
183
|
+
case "_hook":
|
|
184
|
+
return HOOK_ROUTE;
|
|
185
|
+
case "_hook.server":
|
|
186
|
+
return HOOK_SERVER_ROUTE;
|
|
187
|
+
case "_error":
|
|
188
|
+
return ERROR_ROUTE;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
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) {}
|
|
195
|
+
|
|
196
|
+
#sortRoutes() {
|
|
197
|
+
this.routes = this.routes.sort((a, b) => {
|
|
198
|
+
// Sort [param]s after paths
|
|
199
|
+
// There might be a quicker/easier way to do this
|
|
200
|
+
for (let i = 0; i < Math.min(a.path.length, b.path.length); i++) {
|
|
201
|
+
if (a.path[i] === "[" && b.path[i] !== "[") {
|
|
202
|
+
return 1;
|
|
203
|
+
} else if (b.path[i] === "[" && a.path[i] !== "[") {
|
|
204
|
+
return -1;
|
|
205
|
+
} else if (a.path[i] === b.path[i]) {
|
|
206
|
+
// Keep going...
|
|
207
|
+
} else {
|
|
208
|
+
return a.path[i].localeCompare(b.path[i]);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return a.path.length - b.path.length;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import manifest from "@torpor/build/manifest";
|
|
2
|
+
import loadData from "../nav/loadData.ts";
|
|
3
|
+
import navigate from "../nav/navigate.ts";
|
|
4
|
+
import $page from "../state/$page.ts";
|
|
5
|
+
import client from "../state/client";
|
|
6
|
+
import type LayoutPath from "../types/LayoutPath";
|
|
7
|
+
import type PageEndPoint from "../types/PageEndPoint";
|
|
8
|
+
import type PageServerEndPoint from "../types/PageServerEndPoint";
|
|
9
|
+
import Router from "./Router.ts";
|
|
10
|
+
|
|
11
|
+
// Build the router from the Site object created by the user
|
|
12
|
+
const router = new Router();
|
|
13
|
+
router.addPages(manifest.routes);
|
|
14
|
+
|
|
15
|
+
// Put it in client state so that it can be accessed from navigate. We can't
|
|
16
|
+
// just import it to navigate because that gets built by tsdown, without Vite
|
|
17
|
+
// and the manifest
|
|
18
|
+
client.router = router;
|
|
19
|
+
|
|
20
|
+
// Intercept clicks on links
|
|
21
|
+
window.addEventListener("click", async (e) => {
|
|
22
|
+
if (e.target && (e.target as HTMLElement).tagName === "A") {
|
|
23
|
+
const link = e.target as HTMLAnchorElement;
|
|
24
|
+
|
|
25
|
+
if (ignoreAnchorMouseEvent(link, e)) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
e.preventDefault();
|
|
30
|
+
|
|
31
|
+
const href = link.href;
|
|
32
|
+
const url = new URL(href);
|
|
33
|
+
|
|
34
|
+
if (await navigate(url)) {
|
|
35
|
+
window.history.pushState(null, "", url);
|
|
36
|
+
} else {
|
|
37
|
+
window.location.href = href;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Intercept mouseover/focus/touchstart on links for prefetching
|
|
43
|
+
// TODO: settings e.g. <a href="" data-tb-prefetch="eager"/"visible"/"hover"/"touch">
|
|
44
|
+
window.addEventListener("mouseover", maybePrefetch);
|
|
45
|
+
window.addEventListener("touchstart", maybePrefetch);
|
|
46
|
+
|
|
47
|
+
async function maybePrefetch(e: MouseEvent | TouchEvent) {
|
|
48
|
+
if (e.target && (e.target as HTMLElement).tagName === "A") {
|
|
49
|
+
const link = e.target as HTMLAnchorElement;
|
|
50
|
+
|
|
51
|
+
if (ignoreAnchorMouseEvent(link, e)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const href = link.href;
|
|
56
|
+
const url = new URL(href);
|
|
57
|
+
|
|
58
|
+
const path = url.pathname;
|
|
59
|
+
const query = url.searchParams;
|
|
60
|
+
const route = router.match(path, query);
|
|
61
|
+
if (route) {
|
|
62
|
+
const handler = route.handler;
|
|
63
|
+
const params = route.params || {};
|
|
64
|
+
|
|
65
|
+
// There should be a client endpoint
|
|
66
|
+
const clientEndPoint: PageEndPoint | undefined = (await handler.endPoint()).default;
|
|
67
|
+
|
|
68
|
+
// There may be a server endpoint
|
|
69
|
+
const serverEndPoint: PageServerEndPoint | undefined =
|
|
70
|
+
handler.serverEndPoint && (await handler.serverEndPoint())?.default;
|
|
71
|
+
|
|
72
|
+
// Just dummy this up
|
|
73
|
+
let newStack: LayoutPath[] = [];
|
|
74
|
+
|
|
75
|
+
await loadData(handler, params, path, query, newStack, clientEndPoint, serverEndPoint, true);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function ignoreAnchorMouseEvent(link: HTMLAnchorElement, e: MouseEvent | TouchEvent) {
|
|
81
|
+
// Ignore the event if a modifier key is pressed, or target is not
|
|
82
|
+
// "_self", or if rel="external"
|
|
83
|
+
return (
|
|
84
|
+
e.altKey ||
|
|
85
|
+
e.ctrlKey ||
|
|
86
|
+
e.metaKey ||
|
|
87
|
+
e.shiftKey ||
|
|
88
|
+
(link.target && link.target !== "_self") ||
|
|
89
|
+
link.rel === "external"
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Listen for changes to the URL that occur when the user navigates using the
|
|
94
|
+
// back or forward buttons
|
|
95
|
+
window.addEventListener("popstate", async () => {
|
|
96
|
+
await navigateToLocation(document.location);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Do the initial navigation and hydration
|
|
100
|
+
await navigateToLocation(document.location, true);
|
|
101
|
+
|
|
102
|
+
async function navigateToLocation(location: Location, firstTime = false) {
|
|
103
|
+
// Reset $page on submit and link navigation
|
|
104
|
+
// TODO: Should we put data in $page.data??
|
|
105
|
+
$page.form = undefined;
|
|
106
|
+
|
|
107
|
+
return await navigate(new URL(location.toString()), firstTime);
|
|
108
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type Adapter from "../types/Adapter";
|
|
2
|
+
|
|
3
|
+
// TODO: This should be an adapter-auto package that conditionally loads other packages...
|
|
4
|
+
|
|
5
|
+
const defaultAdapter: Adapter = {
|
|
6
|
+
serve: () => {
|
|
7
|
+
console.log("An adapter needs to be set in site.config");
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default defaultAdapter;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Plugin } from "vite";
|
|
4
|
+
import Site from "./Site";
|
|
5
|
+
|
|
6
|
+
const moduleId = "@torpor/build/manifest";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A Vite plugin that provides information about the app to both clientEntry and
|
|
10
|
+
* serverEntry.
|
|
11
|
+
*/
|
|
12
|
+
export default function manifest(site: Site, server = false): Plugin {
|
|
13
|
+
return {
|
|
14
|
+
name: "torpor-manifest",
|
|
15
|
+
resolveId: {
|
|
16
|
+
order: "pre",
|
|
17
|
+
handler(id) {
|
|
18
|
+
if (id === moduleId) {
|
|
19
|
+
return id;
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
// @ts-ignore
|
|
24
|
+
load(id, viteOptions) {
|
|
25
|
+
if (id === moduleId) {
|
|
26
|
+
let serverRequest = server && !!viteOptions?.ssr;
|
|
27
|
+
return `
|
|
28
|
+
${!serverRequest ? "const load = { default: { load: true } };" : ""}
|
|
29
|
+
export default {
|
|
30
|
+
routes: [
|
|
31
|
+
${site.routes
|
|
32
|
+
.map((r) => {
|
|
33
|
+
let endPoint: string;
|
|
34
|
+
if (serverRequest || !/server\.(ts|js)$/.test(r.file)) {
|
|
35
|
+
endPoint = `() => import(/* @vite-ignore */ "${path.join(site.root, r.file)}")`;
|
|
36
|
+
} else {
|
|
37
|
+
// On the client, for a server route, we need to check
|
|
38
|
+
// whether there's a load function and set a dummy endPoint
|
|
39
|
+
// if so. The correct endpoint will be hit in clientEntry
|
|
40
|
+
// HACK: Should do this better than checking for `load:` in source...
|
|
41
|
+
const haveLoad = readFileSync(path.join(site.root, r.file)).includes("load:");
|
|
42
|
+
endPoint = haveLoad ? "() => load" : "undefined";
|
|
43
|
+
}
|
|
44
|
+
const sub = r.subFolder ? `"${r.subFolder}"` : "undefined";
|
|
45
|
+
return `{ path: "${r.path}", type: ${r.type}, endPoint: ${endPoint}, subFolder: ${sub} }`;
|
|
46
|
+
})
|
|
47
|
+
.join(",\n ")}
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|