@torpor/build 0.4.4 → 0.4.6

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 (99) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +2 -2
  3. package/dist/{RouteType-VOBjKJ8I.js → RouteType-B6BMOyYw.mjs} +13 -12
  4. package/dist/RouteType-B6BMOyYw.mjs.map +1 -0
  5. package/dist/{Server-DL9neaq0.d.ts → Server-CF5wDJp6.d.mts} +1 -1
  6. package/dist/Server-CF5wDJp6.d.mts.map +1 -0
  7. package/dist/{ServerEvent-Dnk2FIKQ.js → ServerEvent-CxPwA4lH.mjs} +1 -1
  8. package/dist/ServerEvent-CxPwA4lH.mjs.map +1 -0
  9. package/dist/{Site-B8TxSzPk.d.ts → Site-DgC6WWn1.d.mts} +20 -9
  10. package/dist/Site-DgC6WWn1.d.mts.map +1 -0
  11. package/dist/{_page-ef72MjgG.js → _page-DBSsnHFG.mjs} +1 -1
  12. package/dist/_page-DBSsnHFG.mjs.map +1 -0
  13. package/dist/bin/index.js +7 -7
  14. package/dist/bin/index.js.map +1 -1
  15. package/dist/{connectMiddleware-PBcWWRr-.js → connectMiddleware-D7nehjj3.mjs} +3 -3
  16. package/dist/connectMiddleware-D7nehjj3.mjs.map +1 -0
  17. package/dist/{index.d.ts → index.d.mts} +11 -6
  18. package/dist/index.d.mts.map +1 -0
  19. package/dist/{index.js → index.mjs} +21 -12
  20. package/dist/index.mjs.map +1 -0
  21. package/dist/{nav.d.ts → nav.d.mts} +1 -1
  22. package/dist/nav.d.mts.map +1 -0
  23. package/dist/{nav.js → nav.mjs} +17 -12
  24. package/dist/nav.mjs.map +1 -0
  25. package/dist/{pathToRegex-BTPkBIYJ.js → pathToRegex-TUAMc3zS.mjs} +1 -1
  26. package/dist/pathToRegex-TUAMc3zS.mjs.map +1 -0
  27. package/dist/{response-KnPcsvtU.js → response-C5TtAsh1.mjs} +2 -2
  28. package/dist/response-C5TtAsh1.mjs.map +1 -0
  29. package/dist/{response.d.ts → response.d.mts} +1 -1
  30. package/dist/response.d.mts.map +1 -0
  31. package/dist/{response.js → response.mjs} +2 -2
  32. package/dist/{run-CaFHeN6K.js → run-DiZgkMnG.mjs} +37 -37
  33. package/dist/run-DiZgkMnG.mjs.map +1 -0
  34. package/dist/{run.d.ts → run.d.mts} +3 -3
  35. package/dist/run.d.mts.map +1 -0
  36. package/dist/run.mjs +8 -0
  37. package/dist/{seeOther-C-36lhS5.js → seeOther-B4Yhu9iq.mjs} +1 -1
  38. package/dist/seeOther-B4Yhu9iq.mjs.map +1 -0
  39. package/dist/{server.d.ts → server.d.mts} +2 -2
  40. package/dist/server.d.mts.map +1 -0
  41. package/dist/{server.js → server.mjs} +3 -3
  42. package/dist/{state.d.ts → state.d.mts} +1 -1
  43. package/dist/state.d.mts.map +1 -0
  44. package/dist/state.mjs +3 -0
  45. package/dist/{test.d.ts → test.d.mts} +3 -3
  46. package/dist/test.d.mts.map +1 -0
  47. package/dist/{test.js → test.mjs} +25 -14
  48. package/dist/test.mjs.map +1 -0
  49. package/package.json +26 -24
  50. package/scripts/postbuild.ts +12 -0
  51. package/src/nav/formSubmit.ts +2 -1
  52. package/src/nav/loadData.ts +16 -5
  53. package/src/nav/navigate.ts +1 -0
  54. package/src/site/Router.ts +12 -13
  55. package/src/site/Site.ts +29 -4
  56. package/src/site/clientEntry.ts +1 -1
  57. package/src/site/manifest.ts +5 -2
  58. package/src/test/runTest.ts +15 -6
  59. package/src/types/ManifestRoute.ts +1 -0
  60. package/src/types/Route.ts +11 -0
  61. package/src/types/RouteHandler.ts +4 -0
  62. package/test/api/_hook.server.ts +3 -0
  63. package/test/api/posts/+page.server.ts +3 -0
  64. package/test/api/posts/[id]/+page.server.ts +3 -0
  65. package/test/router.test.ts +78 -0
  66. package/test/routes/+page.server.ts +3 -0
  67. package/test/routes/+page.ts +3 -0
  68. package/test/routes/_error.ts +3 -0
  69. package/test/routes/_hook.server.ts +3 -0
  70. package/test/routes/_layout.server.ts +3 -0
  71. package/test/routes/_layout.ts +3 -0
  72. package/test/routes/posts/+page.ts +3 -0
  73. package/test/routes/posts/[id]/+page.ts +3 -0
  74. package/test/site.test.ts +40 -0
  75. package/tsdown.config.ts +3 -10
  76. package/vitest.config.ts +2 -2
  77. package/dist/RouteType-VOBjKJ8I.js.map +0 -1
  78. package/dist/Server-DL9neaq0.d.ts.map +0 -1
  79. package/dist/ServerEvent-Dnk2FIKQ.js.map +0 -1
  80. package/dist/Site-B8TxSzPk.d.ts.map +0 -1
  81. package/dist/_page-ef72MjgG.js.map +0 -1
  82. package/dist/connectMiddleware-PBcWWRr-.js.map +0 -1
  83. package/dist/index.d.ts.map +0 -1
  84. package/dist/index.js.map +0 -1
  85. package/dist/nav.d.ts.map +0 -1
  86. package/dist/nav.js.map +0 -1
  87. package/dist/pathToRegex-BTPkBIYJ.js.map +0 -1
  88. package/dist/response-KnPcsvtU.js.map +0 -1
  89. package/dist/response.d.ts.map +0 -1
  90. package/dist/run-CaFHeN6K.js.map +0 -1
  91. package/dist/run.d.ts.map +0 -1
  92. package/dist/run.js +0 -8
  93. package/dist/seeOther-C-36lhS5.js.map +0 -1
  94. package/dist/server.d.ts.map +0 -1
  95. package/dist/state.d.ts.map +0 -1
  96. package/dist/state.js +0 -3
  97. package/dist/test.d.ts.map +0 -1
  98. package/dist/test.js.map +0 -1
  99. /package/dist/bin/{index.d.ts → index.d.mts} +0 -0
package/package.json CHANGED
@@ -1,36 +1,36 @@
1
1
  {
2
2
  "name": "@torpor/build",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "A framework for building sites and apps with Torpor",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
8
- "types": "./dist/index.d.ts",
9
- "import": "./dist/index.js"
8
+ "types": "./dist/index.d.mts",
9
+ "import": "./dist/index.mjs"
10
10
  },
11
11
  "./nav": {
12
- "types": "./dist/nav.d.ts",
13
- "import": "./dist/nav.js"
12
+ "types": "./dist/nav.d.mts",
13
+ "import": "./dist/nav.mjs"
14
14
  },
15
15
  "./response": {
16
- "types": "./dist/response.d.ts",
17
- "import": "./dist/response.js"
16
+ "types": "./dist/response.d.mts",
17
+ "import": "./dist/response.mjs"
18
18
  },
19
19
  "./run": {
20
- "types": "./dist/run.d.ts",
21
- "import": "./dist/run.js"
20
+ "types": "./dist/run.d.mts",
21
+ "import": "./dist/run.mjs"
22
22
  },
23
23
  "./server": {
24
- "types": "./dist/server.d.ts",
25
- "import": "./dist/server.js"
24
+ "types": "./dist/server.d.mts",
25
+ "import": "./dist/server.mjs"
26
26
  },
27
27
  "./state": {
28
- "types": "./dist/state.d.ts",
29
- "import": "./dist/state.js"
28
+ "types": "./dist/state.d.mts",
29
+ "import": "./dist/state.mjs"
30
30
  },
31
31
  "./test": {
32
- "types": "./dist/test.d.ts",
33
- "import": "./dist/test.js"
32
+ "types": "./dist/test.d.mts",
33
+ "import": "./dist/test.mjs"
34
34
  },
35
35
  "./manifest": {
36
36
  "types": "./src/types/manifest.d.ts"
@@ -40,7 +40,7 @@
40
40
  "access": "public"
41
41
  },
42
42
  "bin": {
43
- "tb": "./dist/bin/index.js"
43
+ "tb": "./dist/bin/index.mjs"
44
44
  },
45
45
  "keywords": [],
46
46
  "author": "",
@@ -48,23 +48,25 @@
48
48
  "dependencies": {
49
49
  "cookie": "^1.0.2",
50
50
  "dotenv": "^17.2.3",
51
- "vite": "^7.1.12",
51
+ "vite": "^7.2.2",
52
52
  "vite-tsconfig-paths": "^5.1.4",
53
53
  "@torpor/unplugin": "^0.2.1",
54
- "@torpor/view": "^0.4.7"
54
+ "@torpor/view": "^0.4.14"
55
55
  },
56
56
  "devDependencies": {
57
- "@trivago/prettier-plugin-sort-imports": "^5.2.2",
58
- "@typescript/native-preview": "7.0.0-dev.20251103.1",
59
- "oxlint-tsgolint": "^0.4.0",
60
- "tsdown": "^0.15.12",
57
+ "@trivago/prettier-plugin-sort-imports": "^6.0.0",
58
+ "@typescript/native-preview": "7.0.0-dev.20251112.1",
59
+ "esno": "^4.8.0",
60
+ "oxlint-tsgolint": "^0.6.0",
61
+ "tsdown": "^0.16.4",
61
62
  "typescript": "^5.9.3",
62
- "vitest": "^4.0.6"
63
+ "vitest": "^4.0.8"
63
64
  },
64
65
  "scripts": {
66
+ "check": "tsgo --noEmit && pnpm dlx oxlint --type-aware",
65
67
  "build": "tsgo --noEmit && tsdown",
68
+ "build:fix": "esno scripts/postbuild.ts",
66
69
  "build:watch": "tsdown --watch",
67
- "check": "tsgo --noEmit && pnpm dlx oxlint --type-aware",
68
70
  "test": "vitest"
69
71
  }
70
72
  }
@@ -0,0 +1,12 @@
1
+ import { renameSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ // HACK: tsdown only creates *.mjs files now (post v0.16), so we need to rename
6
+ // the bin/index.mjs file to bin/index.js for it to be run by node
7
+
8
+ const scriptsFolder = dirname(fileURLToPath(import.meta.url));
9
+ const distFolder = resolve(scriptsFolder, "../dist");
10
+
11
+ renameSync(join(distFolder, "bin", "index.mjs"), join(distFolder, "bin", "index.js"));
12
+ renameSync(join(distFolder, "bin", "index.mjs.map"), join(distFolder, "bin", "index.js.map"));
@@ -12,6 +12,7 @@ export default async function formSubmit(e: SubmitEvent): Promise<void> {
12
12
 
13
13
  // Perform a POST fetch to the form's action location
14
14
  const form = e.target as HTMLFormElement;
15
+ const action = (e.submitter as HTMLButtonElement)?.formAction ?? form.action;
15
16
  const options = {
16
17
  body: new FormData(form),
17
18
  method: "POST",
@@ -25,7 +26,7 @@ export default async function formSubmit(e: SubmitEvent): Promise<void> {
25
26
  },
26
27
  };
27
28
  const response = await fetch(
28
- form.action,
29
+ action,
29
30
  // @ts-ignore -- don't know what it's complaining about here
30
31
  options,
31
32
  );
@@ -8,6 +8,7 @@ export default async function loadData(
8
8
  handler: RouteHandler,
9
9
  params: Record<string, string>,
10
10
  path: string,
11
+ query: URLSearchParams,
11
12
  newLayoutStack: LayoutPath[],
12
13
  clientEndPoint: PageEndPoint | undefined,
13
14
  serverEndPoint: PageServerEndPoint | undefined,
@@ -36,6 +37,7 @@ export default async function loadData(
36
37
  const layoutResponse = await loadClientAndServerData(
37
38
  stackLayout.data,
38
39
  document.location.origin + layoutPath,
40
+ query,
39
41
  params,
40
42
  layoutEndPoint,
41
43
  layoutServerEndPoint,
@@ -51,6 +53,7 @@ export default async function loadData(
51
53
  let endPointResponse = await loadClientAndServerData(
52
54
  data,
53
55
  document.location.origin + path,
56
+ query,
54
57
  params,
55
58
  clientEndPoint,
56
59
  serverEndPoint,
@@ -64,6 +67,7 @@ export default async function loadData(
64
67
  async function loadClientAndServerData(
65
68
  data: Record<string, any>,
66
69
  location: string,
70
+ query: URLSearchParams,
67
71
  params: Record<string, string>,
68
72
  clientEndPoint?: PageEndPoint,
69
73
  serverEndPoint?: PageServerEndPoint,
@@ -75,6 +79,9 @@ async function loadClientAndServerData(
75
79
  Object.assign(data, prefetchedData[location]);
76
80
  } else {
77
81
  const clientUrl = new URL(document.location.href);
82
+ for (let [name, value] of query) {
83
+ clientUrl.searchParams.append(name, value);
84
+ }
78
85
  const clientParams = buildClientParams(clientUrl, params, data);
79
86
  const clientResponse = await clientEndPoint.load(clientParams);
80
87
  if (clientResponse) {
@@ -90,19 +97,23 @@ async function loadClientAndServerData(
90
97
  }
91
98
  }
92
99
  }
93
- if (serverEndPoint?.load) {
94
- const serverUrl = location.replace(/\/$/, "") + "/~server";
95
100
 
96
- if (prefetchedData[serverUrl]) {
97
- Object.assign(data, prefetchedData[serverUrl]);
101
+ if (serverEndPoint?.load) {
102
+ const serverLocation = location.replace(/\/$/, "") + "/~server";
103
+ if (prefetchedData[serverLocation]) {
104
+ Object.assign(data, prefetchedData[serverLocation]);
98
105
  } else {
106
+ const serverUrl = new URL(serverLocation);
107
+ for (let [name, value] of query) {
108
+ serverUrl.searchParams.append(name, value);
109
+ }
99
110
  const serverResponse = await fetch(serverUrl);
100
111
  if (serverResponse) {
101
112
  if (serverResponse.ok) {
102
113
  if (serverResponse.headers.get("Content-Type")?.includes("application/json")) {
103
114
  const serverData = await serverResponse.json();
104
115
  Object.assign(data, serverData);
105
- prefetchedData[serverUrl] = serverData;
116
+ prefetchedData[serverLocation] = serverData;
106
117
  }
107
118
  } else {
108
119
  return serverResponse;
@@ -64,6 +64,7 @@ export default async function navigate(url: URL, withHydration = false): Promise
64
64
  handler,
65
65
  params,
66
66
  path,
67
+ query,
67
68
  newLayoutStack,
68
69
  clientEndPoint,
69
70
  serverEndPoint,
@@ -23,6 +23,7 @@ export default class Router {
23
23
  path: r.path,
24
24
  type: r.type,
25
25
  endPoint: r.endPoint,
26
+ subFolder: r.subFolder,
26
27
  }),
27
28
  );
28
29
  }
@@ -31,12 +32,13 @@ export default class Router {
31
32
  }
32
33
 
33
34
  // TODO: Allow calling with the endpoint itself, so that you can setup an app with no scaffold
34
- addPage(path: string, type: number, endPoint: () => Promise<any>): this {
35
+ addPage(path: string, type: number, endPoint: () => Promise<any>, subFolder?: string): this {
35
36
  this.routes.push(
36
37
  new Route(path, {
37
38
  path,
38
39
  type,
39
40
  endPoint,
41
+ subFolder,
40
42
  }),
41
43
  );
42
44
  this.#sortRoutes();
@@ -53,7 +55,7 @@ export default class Router {
53
55
  }
54
56
  */
55
57
 
56
- match(path: string, query: URLSearchParams): Match | undefined {
58
+ match(path: string, query: URLSearchParams): RouteMatch | undefined {
57
59
  for (let route of this.routes) {
58
60
  let match = path.match(route.regex);
59
61
  if (match) {
@@ -69,17 +71,15 @@ export default class Router {
69
71
  };
70
72
  }
71
73
  }
72
-
73
- console.log(`not found: '${path}'`);
74
74
  }
75
75
 
76
76
  #loadHandler(handler: RouteHandler, path: string) {
77
- handler.layouts = this.#findLayouts(path);
77
+ handler.layouts = this.#findLayouts(path, handler);
78
78
  handler.serverEndPoint = this.#findServer(path);
79
- handler.serverHook = this.#findServerHook(path);
79
+ handler.serverHook = this.#findServerHook(handler);
80
80
  }
81
81
 
82
- #findLayouts(path: string): LayoutHandler[] | undefined {
82
+ #findLayouts(path: string, handler: RouteHandler): LayoutHandler[] | undefined {
83
83
  let layouts: LayoutHandler[] = [];
84
84
  let parts = path
85
85
  // Strip the trailing`~/server` (so we don't look for `~/server/_layout`
@@ -96,7 +96,7 @@ export default class Router {
96
96
  }
97
97
  const layoutPath = basePath + "/_layout";
98
98
  const layoutRoute = this.routes.find((r) => r.path === layoutPath);
99
- if (layoutRoute) {
99
+ if (layoutRoute && layoutRoute.handler.subFolder === handler.subFolder) {
100
100
  layouts.push({
101
101
  path: layoutPath,
102
102
  endPoint: layoutRoute.handler.endPoint,
@@ -113,12 +113,11 @@ export default class Router {
113
113
  return serverRoute && serverRoute.handler.endPoint;
114
114
  }
115
115
 
116
- #findServerHook(path: string): (() => Promise<any>) | undefined {
116
+ #findServerHook(handler: RouteHandler): (() => Promise<any>) | undefined {
117
117
  // TODO: Should this be a collection, like layouts?
118
118
  let serverHookPath = "/_hook/~server";
119
- // HACK: Generalise this
120
- if (path.startsWith("/api/")) {
121
- serverHookPath = "/api" + serverHookPath;
119
+ if (handler.subFolder !== undefined) {
120
+ serverHookPath = handler.subFolder + serverHookPath;
122
121
  }
123
122
  const serverHookRoute = this.routes.find((r) => r.path === serverHookPath);
124
123
  return serverHookRoute && serverHookRoute.handler.endPoint;
@@ -158,7 +157,7 @@ class Route {
158
157
  }
159
158
  }
160
159
 
161
- interface Match {
160
+ interface RouteMatch {
162
161
  handler: RouteHandler;
163
162
  params?: Record<string, string>;
164
163
  query: URLSearchParams;
package/src/site/Site.ts CHANGED
@@ -3,6 +3,7 @@ import fpath from "node:path";
3
3
  import { type Plugin, type UserConfig } from "vite";
4
4
  import tsconfigPaths from "vite-tsconfig-paths";
5
5
  import type Adapter from "../types/Adapter";
6
+ import type Route from "../types/Route";
6
7
  import {
7
8
  ERROR_ROUTE,
8
9
  HOOK_ROUTE,
@@ -27,7 +28,7 @@ import defaultAdapter from "./defaultAdapter";
27
28
  */
28
29
  export default class Site {
29
30
  root: string;
30
- routes: { path: string; file: string; type: RouteType }[] = [];
31
+ routes: Route[] = [];
31
32
  // Is default plugins a bad idea?
32
33
  // HACK: Set loose because otherwise it uses a list of allowed extensions
33
34
  // TODO: Should probably just do this ourselves and let the user pass in
@@ -56,17 +57,32 @@ export default class Site {
56
57
  /**
57
58
  * Adds a folder of routes, based on the names of the files in the folder
58
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
59
64
  */
60
- async addRouteFolder(folder: string): Promise<void> {
65
+ async addRouteFolder(folder: string, subFolder?: string): Promise<void> {
61
66
  const routeFolder = fpath.join(this.root, folder);
62
67
  const routeFiles = await fs.readdir(routeFolder, { recursive: true });
68
+ if (subFolder !== undefined && !subFolder.startsWith("/")) {
69
+ subFolder = "/" + subFolder;
70
+ }
63
71
  for (let file of routeFiles) {
64
72
  // The file must start with `+` or `_` and end with `.js` or `.ts`
65
73
  if (/^(\+|_).+(\.js|\.ts)$/.test(fpath.basename(file))) {
66
74
  let routePath = this.#routePath(file);
75
+ if (subFolder !== undefined) {
76
+ routePath = subFolder + routePath;
77
+ }
67
78
  let type = this.#routeType(file);
68
79
  file = fpath.relative(this.root, fpath.resolve(routeFolder, file));
69
- this.routes.push({ path: routePath, file, type });
80
+ this.routes.push({
81
+ path: routePath,
82
+ file,
83
+ type,
84
+ subFolder: subFolder,
85
+ });
70
86
  }
71
87
  }
72
88
  this.#sortRoutes();
@@ -91,10 +107,19 @@ export default class Site {
91
107
  | "_hook"
92
108
  | "_hook.server"
93
109
  | "_error",
110
+ subFolder?: string,
94
111
  ): void {
95
112
  let routePath = this.#routePath(fpath.join(path, type ?? fpath.basename(file)));
96
113
  let routeType = type ? this.#routeTypeFromString(type) : this.#routeType(file);
97
- this.routes.push({ path: routePath, file, type: routeType });
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
+ });
98
123
  this.#sortRoutes();
99
124
  }
100
125
 
@@ -72,7 +72,7 @@ async function maybePrefetch(e: MouseEvent | TouchEvent) {
72
72
  // Just dummy this up
73
73
  let newStack: LayoutPath[] = [];
74
74
 
75
- await loadData(handler, params, path, newStack, clientEndPoint, serverEndPoint, true);
75
+ await loadData(handler, params, path, query, newStack, clientEndPoint, serverEndPoint, true);
76
76
  }
77
77
  }
78
78
  }
@@ -30,16 +30,19 @@ export default {
30
30
  routes: [
31
31
  ${site.routes
32
32
  .map((r) => {
33
+ let endPoint: string;
33
34
  if (serverRequest || !/server\.(ts|js)$/.test(r.file)) {
34
- return `{ path: "${r.path}", type: ${r.type}, endPoint: ${serverRequest || !/server\.(ts|js)$/.test(r.file) ? `() => import(/* @vite-ignore */ "${path.join(site.root, r.file)}")` : "undefined"} }`;
35
+ endPoint = `() => import(/* @vite-ignore */ "${path.join(site.root, r.file)}")`;
35
36
  } else {
36
37
  // On the client, for a server route, we need to check
37
38
  // whether there's a load function and set a dummy endPoint
38
39
  // if so. The correct endpoint will be hit in clientEntry
39
40
  // HACK: Should do this better than checking for `load:` in source...
40
41
  const haveLoad = readFileSync(path.join(site.root, r.file)).includes("load:");
41
- return `{ path: "${r.path}", type: ${r.type}, endPoint: ${haveLoad ? "() => load" : "undefined"} }`;
42
+ endPoint = haveLoad ? "() => load" : "undefined";
42
43
  }
44
+ const sub = r.subFolder ? `"${r.subFolder}"` : "undefined";
45
+ return `{ path: "${r.path}", type: ${r.type}, endPoint: ${endPoint}, subFolder: ${sub} }`;
43
46
  })
44
47
  .join(",\n ")}
45
48
  ],
@@ -48,6 +48,7 @@ export default async function runTest(
48
48
  path: r.path,
49
49
  type: r.type,
50
50
  endPoint: () => import(/* @vite-ignore */ path.join(site.root, r.file)),
51
+ subFolder: r.subFolder,
51
52
  })),
52
53
  );
53
54
 
@@ -201,17 +202,21 @@ async function loadView(
201
202
  }
202
203
  let $props: Record<string, any> = { data };
203
204
 
205
+ let styles = "";
206
+
204
207
  // If there are layouts, work our way upwards, pushing each component into
205
208
  // the default slot of its parent
206
209
  // TODO: Also handle layout server data
207
210
  // TODO: There's probably a nicer way to do this with reducers or something
208
- // TODO: Re-use parent layouts
209
211
  let component = clientEndPoint.component as ServerComponent;
210
212
  let slots: Record<string, ServerSlotRender> | undefined = undefined;
211
213
  if (handler.layouts) {
212
214
  let slotFunctions: ServerSlotRender[] = [];
213
- slotFunctions[handler.layouts.length] = (_, context) =>
214
- (clientEndPoint.component as ServerComponent)($props, context);
215
+ slotFunctions[handler.layouts.length] = (_, $context) => {
216
+ let { body, head } = (clientEndPoint.component as ServerComponent)($props, $context);
217
+ styles += head;
218
+ return body;
219
+ };
215
220
  for (let i = handler.layouts.length - 1; i >= 0; i--) {
216
221
  const layoutEndPoint: PageEndPoint | undefined = (await handler.layouts[i].endPoint())
217
222
  ?.default;
@@ -220,10 +225,13 @@ async function loadView(
220
225
  component = layoutEndPoint.component as ServerComponent;
221
226
  slots = { _: slotFunctions[i + 1] };
222
227
  } else {
223
- slotFunctions[i] = (_, context) =>
224
- (layoutEndPoint.component as ServerComponent)($props, context, {
228
+ slotFunctions[i] = (_, $context) => {
229
+ let { body, head } = (layoutEndPoint.component as ServerComponent)($props, $context, {
225
230
  _: slotFunctions[i + 1],
226
231
  });
232
+ styles += head;
233
+ return body;
234
+ };
227
235
  }
228
236
  }
229
237
  }
@@ -232,7 +240,8 @@ async function loadView(
232
240
  let html;
233
241
  try {
234
242
  let { body, head } = component($props, undefined, slots);
235
- html = template.replace("%COMPONENT_BODY%", body).replace("%COMPONENT_HEAD", head);
243
+ styles += head;
244
+ html = template.replace("%COMPONENT_BODY%", body).replace("%COMPONENT_HEAD", styles);
236
245
  } catch (error) {
237
246
  // TODO: Show a proper Error component
238
247
  html = '<span style="color: red">Script syntax error</span><p>' + error + "</p>";
@@ -2,4 +2,5 @@ export default interface ManifestRoute {
2
2
  path: string;
3
3
  type: number;
4
4
  endPoint: () => Promise<any>;
5
+ subFolder: string | undefined;
5
6
  }
@@ -0,0 +1,11 @@
1
+ import { type RouteType } from "./RouteType";
2
+
3
+ /**
4
+ * A route that is added to the Site.
5
+ */
6
+ export default interface Route {
7
+ path: string;
8
+ file: string;
9
+ type: RouteType;
10
+ subFolder?: string;
11
+ }
@@ -1,9 +1,13 @@
1
1
  import type RouteLayoutHandler from "./RouteLayoutHandler";
2
2
 
3
+ /**
4
+ * A handler for a route in the Router.
5
+ */
3
6
  export default interface RouteHandler {
4
7
  path: string;
5
8
  type: number;
6
9
  endPoint: () => Promise<any>;
10
+ subFolder?: string;
7
11
 
8
12
  loaded?: boolean;
9
13
  layouts?: RouteLayoutHandler[];
@@ -0,0 +1,3 @@
1
+ import type { ServerHook } from "@torpor/build";
2
+
3
+ export default {} satisfies ServerHook as ServerHook;
@@ -0,0 +1,3 @@
1
+ import { type PageServerEndPoint } from "@torpor/build";
2
+
3
+ export default {} satisfies PageServerEndPoint as PageServerEndPoint;
@@ -0,0 +1,3 @@
1
+ import { type PageServerEndPoint } from "@torpor/build";
2
+
3
+ export default {} satisfies PageServerEndPoint as PageServerEndPoint;
@@ -0,0 +1,78 @@
1
+ import { assert, expect, test } from "vitest";
2
+ import Router from "../src/site/Router";
3
+ import { HOOK_SERVER_ROUTE, LAYOUT_ROUTE, PAGE_ROUTE } from "../src/types/RouteType";
4
+
5
+ test("router method sorting", () => {
6
+ let r = new Router();
7
+ r.addPage("/posts/[id]", PAGE_ROUTE, async () => {});
8
+ r.addPage("/posts/drafts/[id]", PAGE_ROUTE, async () => {});
9
+ expect(r.routes.map((r) => r.path).join(",")).toBe("/posts/drafts/[id],/posts/[id]");
10
+ });
11
+
12
+ test("router matching", () => {
13
+ let r = new Router();
14
+ r.addPage("/posts", PAGE_ROUTE, async () => {});
15
+ const match = r.match("/posts", new URLSearchParams());
16
+ assert(match, "no match");
17
+ expect(match.params).toBeUndefined();
18
+ assert(match.handler, "no handler");
19
+ });
20
+
21
+ test("router matching with param", () => {
22
+ let r = new Router();
23
+ r.addPage("/posts/[id]", PAGE_ROUTE, async () => {});
24
+ r.addPage("/posts/drafts/[id]", PAGE_ROUTE, async () => {});
25
+ const match = r.match("/posts/5", new URLSearchParams());
26
+ assert(match, "no match");
27
+ assert(match.params, "no params");
28
+ expect(match.params["id"]).toBe("5");
29
+ });
30
+
31
+ test("router matching not found", () => {
32
+ let r = new Router();
33
+ r.addPage("/posts/[id]", PAGE_ROUTE, async () => {});
34
+ r.addPage("/posts/drafts/[id]", PAGE_ROUTE, async () => {});
35
+ const match = r.match("/postss/5", new URLSearchParams());
36
+ expect(match).toBeUndefined();
37
+ });
38
+
39
+ test("router matching layouts", () => {
40
+ let r = new Router();
41
+ r.addPage("/_layout", LAYOUT_ROUTE, async () => {});
42
+ r.addPage("/posts/_layout", LAYOUT_ROUTE, async () => {});
43
+ r.addPage("/posts/[id]", PAGE_ROUTE, async () => {});
44
+ r.addPage("/posts/drafts/[id]", PAGE_ROUTE, async () => {});
45
+ const match = r.match("/posts/5", new URLSearchParams());
46
+ assert(match, "no match");
47
+ assert(match.handler.layouts, "no layouts");
48
+ expect(match.handler.layouts.length).toBe(2);
49
+ });
50
+
51
+ test("router matching hook", () => {
52
+ let r = new Router();
53
+ r.addPage("/_hook/~server", HOOK_SERVER_ROUTE, async () => {});
54
+ r.addPage("/posts/[id]", PAGE_ROUTE, async () => {});
55
+ const match = r.match("/posts/5", new URLSearchParams());
56
+ assert(match, "no match");
57
+ assert(match.handler.serverHook, "no hook");
58
+ });
59
+
60
+ test("router matching base folder", async () => {
61
+ let r = new Router();
62
+ r.addPage("/_hook/~server", HOOK_SERVER_ROUTE, async () => {});
63
+ r.addPage("/_layout", LAYOUT_ROUTE, async () => {});
64
+ r.addPage("/posts/_layout", LAYOUT_ROUTE, async () => {});
65
+ r.addPage("/posts/[id]", PAGE_ROUTE, async () => {});
66
+ r.addPage("/api/posts/[id]", PAGE_ROUTE, async () => {}, "/api");
67
+ r.addPage("/api/_hook/~server", HOOK_SERVER_ROUTE, async () => "api hook", "/api");
68
+ const match = r.match("/api/posts/5", new URLSearchParams());
69
+ assert(match, "no match");
70
+ assert(match.params, "no params");
71
+ expect(match.params["id"]).toBe("5");
72
+
73
+ expect(match.handler.path).toBe("/api/posts/[id]");
74
+ assert(match.handler.serverHook, "no hook");
75
+ expect(await match.handler.serverHook()).toBe("api hook");
76
+
77
+ expect(match.handler.layouts).toBeUndefined();
78
+ });
@@ -0,0 +1,3 @@
1
+ import { type PageServerEndPoint } from "@torpor/build";
2
+
3
+ export default {} satisfies PageServerEndPoint as PageServerEndPoint;
@@ -0,0 +1,3 @@
1
+ import { type PageEndPoint } from "@torpor/build";
2
+
3
+ export default {} satisfies PageEndPoint as PageEndPoint;
@@ -0,0 +1,3 @@
1
+ import { type PageEndPoint } from "@torpor/build";
2
+
3
+ export default {} satisfies PageEndPoint as PageEndPoint;
@@ -0,0 +1,3 @@
1
+ import type { ServerHook } from "@torpor/build";
2
+
3
+ export default {} satisfies ServerHook as ServerHook;
@@ -0,0 +1,3 @@
1
+ import type { PageServerEndPoint } from "@torpor/build";
2
+
3
+ export default {} satisfies PageServerEndPoint as PageServerEndPoint;
@@ -0,0 +1,3 @@
1
+ import { type PageEndPoint } from "@torpor/build";
2
+
3
+ export default {} satisfies PageEndPoint as PageEndPoint;
@@ -0,0 +1,3 @@
1
+ import { type PageEndPoint } from "@torpor/build";
2
+
3
+ export default {} satisfies PageEndPoint as PageEndPoint;
@@ -0,0 +1,3 @@
1
+ import { type PageEndPoint } from "@torpor/build";
2
+
3
+ export default {} satisfies PageEndPoint as PageEndPoint;
@@ -0,0 +1,40 @@
1
+ import { expect, test } from "vitest";
2
+ import Site from "../src/site/Site";
3
+
4
+ test("site routes", async () => {
5
+ let site = new Site();
6
+ await site.addRouteFolder("./test/routes");
7
+ expect(site.routes.map((r) => r.path).join("\n")).toBe(
8
+ `
9
+ /
10
+ /_error
11
+ /_hook/~server
12
+ /_layout
13
+ /_layout/~server
14
+ /~server
15
+ /posts
16
+ /posts/[id]
17
+ `.trim(),
18
+ );
19
+ });
20
+
21
+ test("site routes with base folder", async () => {
22
+ let site = new Site();
23
+ await site.addRouteFolder("./test/routes");
24
+ await site.addRouteFolder("./test/api", "api");
25
+ expect(site.routes.map((r) => r.path).join("\n")).toBe(
26
+ `
27
+ /
28
+ /_error
29
+ /_hook/~server
30
+ /_layout
31
+ /_layout/~server
32
+ /~server
33
+ /api/_hook/~server
34
+ /api/posts/~server
35
+ /api/posts/[id]/~server
36
+ /posts
37
+ /posts/[id]
38
+ `.trim(),
39
+ );
40
+ });