@torpor/build 1.0.2 → 1.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 (55) hide show
  1. package/dist/Router-COdQuVUi.d.mts +59 -0
  2. package/dist/Router-COdQuVUi.d.mts.map +1 -0
  3. package/dist/{Server-BnjLJleZ.mjs → Server-Dr6yqVa5.mjs} +2 -2
  4. package/dist/{Server-BnjLJleZ.mjs.map → Server-Dr6yqVa5.mjs.map} +1 -1
  5. package/dist/{ServerEvent-Dljn5i7A.mjs → ServerEvent-gmOvC_Yf.mjs} +3 -2
  6. package/dist/{ServerEvent-Dljn5i7A.mjs.map → ServerEvent-gmOvC_Yf.mjs.map} +1 -1
  7. package/dist/ServerHook-BlLArueW.d.mts +91 -0
  8. package/dist/ServerHook-BlLArueW.d.mts.map +1 -0
  9. package/dist/{Site-CE0_o5Ci.d.mts → Site-CTADVh7d.d.mts} +12 -91
  10. package/dist/Site-CTADVh7d.d.mts.map +1 -0
  11. package/dist/bin/index.js +1 -1
  12. package/dist/clientEntry.mjs +3 -2
  13. package/dist/clientEntry.mjs.map +1 -1
  14. package/dist/dev.d.mts +1 -1
  15. package/dist/dev.mjs +2 -2
  16. package/dist/index.d.mts +3 -57
  17. package/dist/index.d.mts.map +1 -1
  18. package/dist/index.mjs +153 -3
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/{load-BntzvmwL.mjs → load-C9RpS_4j.mjs} +32 -26
  21. package/dist/{load-BntzvmwL.mjs.map → load-C9RpS_4j.mjs.map} +1 -1
  22. package/dist/nav.mjs +7 -5
  23. package/dist/nav.mjs.map +1 -1
  24. package/dist/openapi.d.mts +1 -1
  25. package/dist/pathTrie-D4Ax2Mu9.mjs.map +1 -1
  26. package/dist/{response-Bg4w2S1q.mjs → response-CuNPcxsY.mjs} +2 -2
  27. package/dist/{response-Bg4w2S1q.mjs.map → response-CuNPcxsY.mjs.map} +1 -1
  28. package/dist/response.mjs +2 -2
  29. package/dist/run.d.mts +1 -1
  30. package/dist/run.mjs +1 -1
  31. package/dist/{runOpenApi-C8eI2Fh0.mjs → runOpenApi-yIzYasNN.mjs} +15 -15
  32. package/dist/{runOpenApi-C8eI2Fh0.mjs.map → runOpenApi-yIzYasNN.mjs.map} +1 -1
  33. package/dist/server/Server.mjs +1 -1
  34. package/dist/server.mjs +2 -2
  35. package/dist/serverEntry-DVv9YI3I.d.mts +13 -0
  36. package/dist/serverEntry-DVv9YI3I.d.mts.map +1 -0
  37. package/dist/serverEntry-LyB5pl0E.mjs +11 -0
  38. package/dist/serverEntry-LyB5pl0E.mjs.map +1 -0
  39. package/dist/serverEntry.d.mts +1 -1
  40. package/dist/serverEntry.mjs +1 -1
  41. package/dist/{serverEntry-1qY3wZig.mjs → serverHandlers-DeL1qZJa.mjs} +199 -71
  42. package/dist/serverHandlers-DeL1qZJa.mjs.map +1 -0
  43. package/dist/test.d.mts +1 -1
  44. package/dist/test.d.mts.map +1 -1
  45. package/dist/test.mjs +7 -242
  46. package/dist/test.mjs.map +1 -1
  47. package/dist/{unprocessable-DAZbrDeR.mjs → unprocessable-D6RcYLh6.mjs} +7 -5
  48. package/dist/{unprocessable-DAZbrDeR.mjs.map → unprocessable-D6RcYLh6.mjs.map} +1 -1
  49. package/package.json +5 -5
  50. package/dist/Site-CE0_o5Ci.d.mts.map +0 -1
  51. package/dist/endpoint-EULRGCKA.mjs +0 -108
  52. package/dist/endpoint-EULRGCKA.mjs.map +0 -1
  53. package/dist/serverEntry-1qY3wZig.mjs.map +0 -1
  54. package/dist/serverEntry-BAO8m03V.d.mts +0 -6
  55. package/dist/serverEntry-BAO8m03V.d.mts.map +0 -1
package/dist/index.mjs CHANGED
@@ -2,9 +2,151 @@ import { t as Router } from "./Router-C4ADUfhv.mjs";
2
2
  import { promises } from "node:fs";
3
3
  import fpath from "node:path";
4
4
  //#region src/site/defaultAdapter.ts
5
- const defaultAdapter = { serve: () => {
6
- console.log("An adapter needs to be set in site.config");
7
- } };
5
+ /**
6
+ * The known adapter packages, in preference order, with the platform env
7
+ * markers that select them when the site is built in a deployment
8
+ * environment. Only packages that are actually installed (somewhere in the
9
+ * site's node_modules) can be resolved.
10
+ */
11
+ const ADAPTER_PACKAGES = [{
12
+ pkg: "@torpor/adapter-node",
13
+ platforms: []
14
+ }, {
15
+ pkg: "@torpor/adapter-cloudflare",
16
+ platforms: ["CF_PAGES"]
17
+ }];
18
+ /**
19
+ * Loads an adapter package, returning undefined when it isn't installed.
20
+ * The import deliberately uses a variable specifier so that bundlers never
21
+ * try to resolve adapter packages from @torpor/build.
22
+ */
23
+ async function loadAdapterPackage(pkg) {
24
+ try {
25
+ return (await import(
26
+ /* @vite-ignore */
27
+ pkg
28
+ )).default;
29
+ } catch {
30
+ return;
31
+ }
32
+ }
33
+ /**
34
+ * Finds an adapter to use when the site doesn't set one explicitly:
35
+ *
36
+ * 1. A platform build environment (e.g. `CF_PAGES` for Cloudflare Pages)
37
+ * selects its adapter, when the adapter package is installed.
38
+ * 2. Otherwise an installed adapter is used, preferring `adapter-node`
39
+ * (a node server runs anywhere).
40
+ * 3. Otherwise nothing is found, and the caller decides what to do (warn,
41
+ * or serve on the current runtime).
42
+ */
43
+ async function resolveAdapter(options = {}) {
44
+ const env = options.env ?? process.env;
45
+ const load = options.load ?? loadAdapterPackage;
46
+ const loaded = /* @__PURE__ */ new Map();
47
+ const loadOnce = async (pkg) => {
48
+ if (!loaded.has(pkg)) loaded.set(pkg, await load(pkg));
49
+ return loaded.get(pkg);
50
+ };
51
+ for (const candidate of ADAPTER_PACKAGES) for (const marker of candidate.platforms) {
52
+ if (env[marker] === void 0) continue;
53
+ const adapter = await loadOnce(candidate.pkg);
54
+ if (adapter) return {
55
+ adapter,
56
+ pkg: candidate.pkg,
57
+ reason: `${marker} detected`
58
+ };
59
+ }
60
+ for (const candidate of ADAPTER_PACKAGES) {
61
+ const adapter = await loadOnce(candidate.pkg);
62
+ if (adapter) return {
63
+ adapter,
64
+ pkg: candidate.pkg,
65
+ reason: "installed"
66
+ };
67
+ }
68
+ return {
69
+ adapter: void 0,
70
+ pkg: void 0,
71
+ reason: "none found"
72
+ };
73
+ }
74
+ /**
75
+ * The current runtime, when preview can serve a fetch handler without an
76
+ * adapter package. (Node's request conversion lives in adapter-node, so
77
+ * node is deliberately not handled here.)
78
+ */
79
+ function currentRuntime() {
80
+ const global = globalThis;
81
+ if (global.Bun) return "bun";
82
+ if (global.Deno) return "deno";
83
+ }
84
+ /**
85
+ * Serves the built output on the current runtime, for when no adapter
86
+ * package could be found. Returns false when the runtime isn't supported.
87
+ */
88
+ function serveOnCurrentRuntime(server) {
89
+ const runtime = currentRuntime();
90
+ if (!runtime) return false;
91
+ const global = globalThis;
92
+ const port = parseInt(process.env.PORT ?? "7059");
93
+ const hostname = process.env.HOST ?? "localhost";
94
+ console.log(`[torpor] Serving on the ${runtime} runtime at ${hostname}:${port}`);
95
+ if (runtime === "bun") global.Bun.serve({
96
+ port,
97
+ hostname,
98
+ fetch: (req) => server.fetch(req)
99
+ });
100
+ else global.Deno.serve({
101
+ port,
102
+ hostname
103
+ }, (req) => server.fetch(req));
104
+ return true;
105
+ }
106
+ let cache;
107
+ let loggedPkg;
108
+ function getCachedResolution() {
109
+ return cache ??= resolveAdapter();
110
+ }
111
+ function logAdapter(pkg, reason) {
112
+ if (loggedPkg === pkg) return;
113
+ loggedPkg = pkg;
114
+ console.log(`[torpor] Using the ${pkg.replace("@torpor/adapter-", "")} adapter (${reason})`);
115
+ }
116
+ /**
117
+ * The adapter used when the site doesn't set one explicitly. Rather than
118
+ * being a real adapter, it resolves one at the time it's needed (see
119
+ * `resolveAdapter`), and as a last resort serves the built output on the
120
+ * current runtime -- Bun and Deno can serve a fetch handler natively, while
121
+ * node needs `@torpor/adapter-node`.
122
+ *
123
+ * Dev mode is not affected: with no `dev` hook, the framework's default
124
+ * Node-runtime dev plugin is used.
125
+ */
126
+ const defaultAdapter = {
127
+ async serve(server, site) {
128
+ const { adapter, pkg, reason } = await getCachedResolution();
129
+ if (adapter && pkg) {
130
+ logAdapter(pkg, reason);
131
+ await adapter.serve(server, site);
132
+ return;
133
+ }
134
+ if (serveOnCurrentRuntime(server)) return;
135
+ console.log("[torpor] No adapter found -- install one (e.g. `pnpm add @torpor/adapter-node`) or set `site.adapter` in site.config.ts");
136
+ },
137
+ async prebuild(site) {
138
+ const { adapter, pkg, reason } = await getCachedResolution();
139
+ if (!adapter || !pkg) return;
140
+ logAdapter(pkg, reason);
141
+ await adapter.prebuild?.(site);
142
+ },
143
+ async postbuild(site) {
144
+ const { adapter, pkg, reason } = await getCachedResolution();
145
+ if (!adapter || !pkg) return;
146
+ logAdapter(pkg, reason);
147
+ await adapter.postbuild?.(site);
148
+ }
149
+ };
8
150
  //#endregion
9
151
  //#region src/site/Site.ts
10
152
  /**
@@ -25,6 +167,14 @@ var Site = class {
25
167
  * Vite plugins to add to the client and server builds
26
168
  */
27
169
  vitePlugins = [];
170
+ /**
171
+ * The site's adapter, e.g. from `@torpor/adapter-node` or
172
+ * `@torpor/adapter-cloudflare`. When not set, one is resolved when it's
173
+ * needed: a deployment environment (e.g. `CF_PAGES`) picks the platform's
174
+ * adapter when installed, an installed adapter is used otherwise, and
175
+ * preview falls back to serving on the current runtime. See
176
+ * defaultAdapter.ts
177
+ */
28
178
  adapter = defaultAdapter;
29
179
  /**
30
180
  * Extra inputs, as absolute file paths, that will be compiled with the
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["fs","#routePath","#routeType","#sortRoutes","#routeTypeFromString","#pushRoute"],"sources":["../src/site/defaultAdapter.ts","../src/site/Site.ts"],"sourcesContent":["import type Adapter from \"../types/Adapter\";\n\n// TODO: This should be an adapter-auto package that conditionally loads other packages...\n\nconst defaultAdapter: Adapter = {\n\tserve: () => {\n\t\tconsole.log(\"An adapter needs to be set in site.config\");\n\t},\n};\n\nexport default defaultAdapter;\n","import { promises as fs } from \"node:fs\";\nimport fpath from \"node:path\";\nimport { type Plugin, type UserConfig } from \"vite\";\nimport type Adapter from \"../types/Adapter\";\nimport type SitePlugin from \"../types/SitePlugin\";\nimport type { InlineEndPoint } from \"../types/Route\";\nimport type PageServerEndPoint from \"../types/PageServerEndPoint\";\nimport type Route from \"../types/Route\";\nimport type ServerEndPoint from \"../types/ServerEndPoint\";\nimport type ServerHook from \"../types/ServerHook\";\nimport {\n\tERROR_ROUTE,\n\tHOOK_ROUTE,\n\tHOOK_SERVER_ROUTE,\n\tLAYOUT_ROUTE,\n\tLAYOUT_SERVER_ROUTE,\n\tPAGE_ROUTE,\n\tPAGE_SERVER_ROUTE,\n\tRouteType,\n\tSERVER_ROUTE,\n} from \"../types/RouteType\";\nimport defaultAdapter from \"./defaultAdapter\";\n\n// 1. The user sets this up\n// 2. It is passed into serverEntry and clientEntry through a Vite plugin\n// (defined in manifest.ts)\n// 3. In those files, it is used to build a Router\n\n/**\n * The Site class contains the information necessary to setup a @torpor/build\n * site, including routes, plugins, and options.\n */\nexport default class Site {\n\troot: string;\n\troutes: Route[] = [];\n\t/**\n\t * Torpor site plugins, run when the site config is loaded (for dev and\n\t * build) and when the server starts up. Use these to register extra\n\t * routes (e.g. `openApi()` from `@torpor/build/openapi`) or add other\n\t * framework-level functionality\n\t */\n\tplugins: SitePlugin[] = [];\n\t/**\n\t * Vite plugins to add to the client and server builds\n\t */\n\tvitePlugins: Plugin[] = [];\n\t// Is default adapter a bad idea?\n\tadapter: Adapter = defaultAdapter;\n\t/**\n\t * Extra inputs, as absolute file paths, that will be compiled with the\n\t * output. What you do with them after that is up to you. File paths ending\n\t * with server.ts/js will only be compiled with the server output\n\t */\n\tinputs: string[] = [];\n\t/**\n\t * Vite config options to merge with the standard Torpor build options.\n\t * `plugins` and `build.rollupOptions.input` will be overridden, so you\n\t * should set `Site.vitePlugins` and `Site.inputs` instead. Other options may be\n\t * overridden or incompatible, so experimentation may be required\n\t */\n\tviteConfig?: UserConfig;\n\t/**\n\t * The path to the site.config file. Set automatically by the CLI; used by\n\t * the manifest plugin to import inline endpoints for server builds.\n\t */\n\tconfigFile?: string;\n\t/**\n\t * A state map for plugins to store their configuration and state on the\n\t * site, so that framework internals (e.g. the manifest plugin) and other\n\t * plugins can read them back:\n\t *\n\t * ```ts\n\t * const myKey = Symbol.for(\"my-plugin\");\n\t * site.pluginState.set(myKey, options);\n\t * ```\n\t *\n\t * Prefer `Symbol.for(\"...\")` keys (or plain strings) over `Symbol()`,\n\t * because the user's config file and framework internals can be loaded as\n\t * separate module instances -- registry symbols are shared across\n\t * instances, `Symbol()` ones are not.\n\t */\n\tpluginState: Map<PropertyKey, unknown> = new Map();\n\t/**\n\t * Inline endpoints keyed by `\"path:type\"`. Populated by `addRoute` when\n\t * the user passes an inline endpoint object instead of a file path.\n\t */\n\tinlineEndPoints: Record<string, InlineEndPoint> = {};\n\n\tconstructor() {\n\t\tthis.root = process.cwd();\n\t}\n\n\t/**\n\t * Adds a folder of routes, based on the names of the files in the folder\n\t * @param folder The folder containing the routes\n\t * @param subFolder If a subFolder is set, all routes in the folder will be\n\t * placed under the subFolder, and only hooks, layouts etc will be used if\n\t * they have the same subFolder. This can be used to create e.g. an `api`\n\t * set of routes with their own hook for authorization from various clients\n\t */\n\tasync addRouteFolder(folder: string, subFolder?: string): Promise<void> {\n\t\tconst routeFolder = fpath.join(this.root, folder);\n\t\tconst routeFiles = await fs.readdir(routeFolder, { recursive: true });\n\t\tif (subFolder !== undefined && !subFolder.startsWith(\"/\")) {\n\t\t\tsubFolder = \"/\" + subFolder;\n\t\t}\n\t\tfor (let file of routeFiles) {\n\t\t\t// The file must start with `+` or `_` and end with `.js` or `.ts`\n\t\t\tif (/^(\\+|_).+(\\.js|\\.ts)$/.test(fpath.basename(file))) {\n\t\t\t\tlet routePath = this.#routePath(file);\n\t\t\t\tif (subFolder !== undefined) {\n\t\t\t\t\troutePath = subFolder + routePath;\n\t\t\t\t}\n\t\t\t\tlet type = this.#routeType(file);\n\t\t\t\tfile = fpath.relative(this.root, fpath.resolve(routeFolder, file));\n\t\t\t\tthis.routes.push({\n\t\t\t\t\tpath: routePath,\n\t\t\t\t\tfile,\n\t\t\t\t\ttype,\n\t\t\t\t\tsubFolder: subFolder,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tthis.#sortRoutes();\n\t}\n\n\t/**\n\t * Adds a file route, which can have a standard name (e.g. +page.ts) or any\n\t * name if you pass the type\n\t * @param path The route path\n\t * @param file The file\n\t * @param type The file type (if the file name is non-standard)\n\t */\n\taddRouteFile(\n\t\tpath: string,\n\t\tfile: string,\n\t\ttype?:\n\t\t\t| \"+page\"\n\t\t\t| \"+page.server\"\n\t\t\t| \"+server\"\n\t\t\t| \"_layout\"\n\t\t\t| \"_layout.server\"\n\t\t\t| \"_hook\"\n\t\t\t| \"_hook.server\"\n\t\t\t| \"_error\",\n\t\tsubFolder?: string,\n\t): void {\n\t\tlet routePath = this.#routePath(fpath.join(path, type ?? fpath.basename(file)));\n\t\tlet routeType = type ? this.#routeTypeFromString(type) : this.#routeType(file);\n\t\tif (subFolder !== undefined && !subFolder.startsWith(\"/\")) {\n\t\t\tsubFolder = \"/\" + subFolder;\n\t\t}\n\t\tthis.routes.push({\n\t\t\tpath: routePath,\n\t\t\tfile,\n\t\t\ttype: routeType,\n\t\t\tsubFolder: subFolder,\n\t\t});\n\t\tthis.#sortRoutes();\n\t}\n\n\t#routePath(file: string): string {\n\t\tlet routePath = file\n\t\t\t.replace(/^\\//, \"\")\n\t\t\t.replace(/(\\.ts|\\.js)$/, \"\")\n\t\t\t.replace(/_hook.server$/, \"_hook/~server\")\n\t\t\t.replace(/_layout.server$/, \"_layout/~server\")\n\t\t\t.replace(/\\+page.server$/, \"~server\")\n\t\t\t.replace(/\\+server$/, \"\")\n\t\t\t.replace(/\\+page$/, \"\")\n\t\t\t.replace(/\\/$/, \"\");\n\t\treturn routePath.length > 0 ? `/${routePath}` : \"/\";\n\t}\n\n\t#routeType(file: string): RouteType {\n\t\tlet routePath = file.replace(/^\\//, \"\").replace(/(\\.ts|\\.js)$/, \"\");\n\t\tif (routePath.endsWith(\"+page\")) {\n\t\t\treturn PAGE_ROUTE;\n\t\t} else if (routePath.endsWith(\"+page.server\")) {\n\t\t\treturn PAGE_SERVER_ROUTE;\n\t\t} else if (routePath.endsWith(\"+server\")) {\n\t\t\treturn SERVER_ROUTE;\n\t\t} else if (routePath.endsWith(\"_layout\")) {\n\t\t\treturn LAYOUT_ROUTE;\n\t\t} else if (routePath.endsWith(\"_layout.server\")) {\n\t\t\treturn LAYOUT_SERVER_ROUTE;\n\t\t} else if (routePath.endsWith(\"_hook\")) {\n\t\t\treturn HOOK_ROUTE;\n\t\t} else if (routePath.endsWith(\"_hook.server\")) {\n\t\t\treturn HOOK_SERVER_ROUTE;\n\t\t} else if (routePath.endsWith(\"_error\")) {\n\t\t\treturn ERROR_ROUTE;\n\t\t}\n\t\treturn -1;\n\t}\n\n\t#routeTypeFromString(\n\t\ttype:\n\t\t\t| \"+page\"\n\t\t\t| \"+page.server\"\n\t\t\t| \"+server\"\n\t\t\t| \"_layout\"\n\t\t\t| \"_layout.server\"\n\t\t\t| \"_hook\"\n\t\t\t| \"_hook.server\"\n\t\t\t| \"_error\",\n\t) {\n\t\tswitch (type) {\n\t\t\tcase \"+page\":\n\t\t\t\treturn PAGE_ROUTE;\n\t\t\tcase \"+page.server\":\n\t\t\t\treturn PAGE_SERVER_ROUTE;\n\t\t\tcase \"+server\":\n\t\t\t\treturn SERVER_ROUTE;\n\t\t\tcase \"_layout\":\n\t\t\t\treturn LAYOUT_ROUTE;\n\t\t\tcase \"_layout.server\":\n\t\t\t\treturn LAYOUT_SERVER_ROUTE;\n\t\t\tcase \"_hook\":\n\t\t\t\treturn HOOK_ROUTE;\n\t\t\tcase \"_hook.server\":\n\t\t\t\treturn HOOK_SERVER_ROUTE;\n\t\t\tcase \"_error\":\n\t\t\t\treturn ERROR_ROUTE;\n\t\t}\n\t}\n\n\t/**\n\t * Adds a route in code, without needing the standard file naming\n\t * conventions (+page.ts, +page.server.ts, etc). This is useful for small\n\t * sites with one or two pages.\n\t *\n\t * Any combination of options may be provided; each maps to the\n\t * corresponding file-route type:\n\t *\n\t * - `page` — a `+page` (`.torp` component or `.ts`/`.js` `PageEndPoint`)\n\t * - `pageServer` — a `+page.server` (`PageServerEndPoint`, file or inline)\n\t * - `server` — a `+server` HTTP endpoint (`ServerEndPoint`, file or inline)\n\t * - `layout` — a `_layout` (`.torp` component or `.ts`/`.js` `PageEndPoint`)\n\t * - `layoutServer` — a `_layout.server` (`PageServerEndPoint`, file or inline)\n\t * - `hookServer` — a `_hook.server` (`ServerHook`, file or inline)\n\t * - `error` — an `_error` page (`.torp` component or `.ts`/`.js` `PageEndPoint`)\n\t *\n\t * Options that need a component (`page`, `layout`, `error`) must be file\n\t * paths so the component is included in the client bundle. Server-only\n\t * options (`pageServer`, `server`, `layoutServer`, `hookServer`) accept\n\t * either a file path or an inline object; inline server code is kept out\n\t * of the client bundle automatically.\n\t *\n\t * @example\n\t * // A page with an inline server action\n\t * site.addRoute(\"/\", {\n\t * page: \"./src/Counter.torp\",\n\t * pageServer: { actions: { set: async ({ request }) => ok() } },\n\t * });\n\t * @example\n\t * // A JSON API endpoint\n\t * site.addRoute(\"/api/time\", {\n\t * server: { get: async () => ok({ time: Date.now() }) },\n\t * });\n\t * @example\n\t * // A root layout, server hook, and error page\n\t * site.addRoute(\"/\", {\n\t * layout: \"./src/Layout.torp\",\n\t * hookServer: { enter: async (event) => {} },\n\t * error: \"./src/ErrorPage.torp\",\n\t * });\n\t *\n\t * @param path The route path, e.g. `/` or `/about`\n\t * @param options File paths and/or inline endpoints for the route\n\t * @param subFolder An optional subFolder for grouping routes\n\t */\n\taddRoute(\n\t\tpath: string,\n\t\toptions: {\n\t\t\tpage?: string;\n\t\t\tpageServer?: string | PageServerEndPoint;\n\t\t\tserver?: string | ServerEndPoint;\n\t\t\tlayout?: string;\n\t\t\tlayoutServer?: string | PageServerEndPoint;\n\t\t\thookServer?: string | ServerHook;\n\t\t\terror?: string;\n\t\t},\n\t\tsubFolder?: string,\n\t): void {\n\t\tlet normalizedSubFolder = subFolder;\n\t\tif (normalizedSubFolder !== undefined && !normalizedSubFolder.startsWith(\"/\")) {\n\t\t\tnormalizedSubFolder = \"/\" + normalizedSubFolder;\n\t\t}\n\n\t\t// The base path with any trailing slash stripped, so that we can\n\t\t// append \"/_layout\", \"/~server\" etc. For the root \"/\" this is \"\".\n\t\tlet base = path.replace(/\\/$/, \"\");\n\n\t\tif (options.page !== undefined) {\n\t\t\tthis.#pushRoute(path, PAGE_ROUTE, options.page, normalizedSubFolder);\n\t\t}\n\t\tif (options.pageServer !== undefined) {\n\t\t\tthis.#pushRoute(\n\t\t\t\t`${base}/~server`,\n\t\t\t\tPAGE_SERVER_ROUTE,\n\t\t\t\toptions.pageServer,\n\t\t\t\tnormalizedSubFolder,\n\t\t\t);\n\t\t}\n\t\tif (options.server !== undefined) {\n\t\t\tthis.#pushRoute(path, SERVER_ROUTE, options.server, normalizedSubFolder);\n\t\t}\n\t\tif (options.layout !== undefined) {\n\t\t\tthis.#pushRoute(`${base}/_layout`, LAYOUT_ROUTE, options.layout, normalizedSubFolder);\n\t\t}\n\t\tif (options.layoutServer !== undefined) {\n\t\t\tthis.#pushRoute(\n\t\t\t\t`${base}/_layout/~server`,\n\t\t\t\tLAYOUT_SERVER_ROUTE,\n\t\t\t\toptions.layoutServer,\n\t\t\t\tnormalizedSubFolder,\n\t\t\t);\n\t\t}\n\t\tif (options.hookServer !== undefined) {\n\t\t\tthis.#pushRoute(\n\t\t\t\t`${base}/_hook/~server`,\n\t\t\t\tHOOK_SERVER_ROUTE,\n\t\t\t\toptions.hookServer,\n\t\t\t\tnormalizedSubFolder,\n\t\t\t);\n\t\t}\n\t\tif (options.error !== undefined) {\n\t\t\tthis.#pushRoute(`${base}/_error`, ERROR_ROUTE, options.error, normalizedSubFolder);\n\t\t}\n\n\t\tthis.#sortRoutes();\n\t}\n\n\t/**\n\t * Pushes a single route entry, handling both file-path and inline-endpoint\n\t * targets. File paths are resolved relative to the site root; inline\n\t * endpoints are also stored in `inlineEndPoints` keyed by `\"path:type\"`\n\t * so the manifest plugin can resolve them at build time.\n\t *\n\t * If a route with the same path and type was already added, it is\n\t * replaced, so that running a plugin twice (config load and server\n\t * startup) doesn't add duplicate routes.\n\t */\n\t#pushRoute(\n\t\troutePath: string,\n\t\ttype: RouteType,\n\t\ttarget: string | InlineEndPoint,\n\t\tsubFolder?: string,\n\t): void {\n\t\tconst index = this.routes.findIndex((r) => r.path === routePath && r.type === type);\n\t\tif (typeof target === \"string\") {\n\t\t\tlet file = fpath.relative(this.root, fpath.resolve(this.root, target));\n\t\t\tif (index >= 0) {\n\t\t\t\tthis.routes[index] = { path: routePath, file, type, subFolder };\n\t\t\t} else {\n\t\t\t\tthis.routes.push({ path: routePath, file, type, subFolder });\n\t\t\t}\n\t\t} else {\n\t\t\tlet key = `${routePath}:${type}`;\n\t\t\tthis.inlineEndPoints[key] = target;\n\t\t\tif (index >= 0) {\n\t\t\t\tthis.routes[index] = { path: routePath, endPoint: target, type, subFolder };\n\t\t\t} else {\n\t\t\t\tthis.routes.push({ path: routePath, endPoint: target, type, subFolder });\n\t\t\t}\n\t\t}\n\t}\n\n\t#sortRoutes() {\n\t\tthis.routes = this.routes.sort((a, b) => {\n\t\t\t// Sort [param]s after paths\n\t\t\t// There might be a quicker/easier way to do this\n\t\t\tfor (let i = 0; i < Math.min(a.path.length, b.path.length); i++) {\n\t\t\t\tif (a.path[i] === \"[\" && b.path[i] !== \"[\") {\n\t\t\t\t\treturn 1;\n\t\t\t\t} else if (b.path[i] === \"[\" && a.path[i] !== \"[\") {\n\t\t\t\t\treturn -1;\n\t\t\t\t} else if (a.path[i] === b.path[i]) {\n\t\t\t\t\t// Keep going...\n\t\t\t\t} else {\n\t\t\t\t\treturn a.path[i].localeCompare(b.path[i]);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn a.path.length - b.path.length;\n\t\t});\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["fs","#routePath","#routeType","#sortRoutes","#routeTypeFromString","#pushRoute"],"sources":["../src/site/defaultAdapter.ts","../src/site/Site.ts"],"sourcesContent":["import type Server from \"../server/Server\";\nimport type Adapter from \"../types/Adapter\";\n\n/**\n * The known adapter packages, in preference order, with the platform env\n * markers that select them when the site is built in a deployment\n * environment. Only packages that are actually installed (somewhere in the\n * site's node_modules) can be resolved.\n */\nconst ADAPTER_PACKAGES = [\n\t{\n\t\t// The preferred fallback: a node server runs anywhere\n\t\tpkg: \"@torpor/adapter-node\",\n\t\tplatforms: [] as string[],\n\t},\n\t{\n\t\tpkg: \"@torpor/adapter-cloudflare\",\n\t\t// Set when building in a Cloudflare Pages environment. More markers\n\t\t// can be added as adapters are (e.g. `VERCEL` for an adapter-vercel)\n\t\tplatforms: [\"CF_PAGES\"],\n\t},\n];\n\n/**\n * Loads an adapter package, returning undefined when it isn't installed.\n * The import deliberately uses a variable specifier so that bundlers never\n * try to resolve adapter packages from @torpor/build.\n */\nasync function loadAdapterPackage(pkg: string): Promise<Adapter | undefined> {\n\ttry {\n\t\treturn (await import(/* @vite-ignore */ pkg)).default;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport type AdapterResolution = {\n\t/** The resolved adapter, or undefined when none could be found */\n\tadapter: Adapter | undefined;\n\t/** The package the adapter was resolved from */\n\tpkg: string | undefined;\n\t/** Why the adapter was chosen, for logging */\n\treason: string;\n};\n\nexport type ResolveAdapterOptions = {\n\t/** The environment to check for platform markers (defaults to process.env) */\n\tenv?: Record<string, string | undefined>;\n\t/** Loads an adapter package by name (defaults to a dynamic import) */\n\tload?: (pkg: string) => Promise<Adapter | undefined>;\n};\n\n/**\n * Finds an adapter to use when the site doesn't set one explicitly:\n *\n * 1. A platform build environment (e.g. `CF_PAGES` for Cloudflare Pages)\n * selects its adapter, when the adapter package is installed.\n * 2. Otherwise an installed adapter is used, preferring `adapter-node`\n * (a node server runs anywhere).\n * 3. Otherwise nothing is found, and the caller decides what to do (warn,\n * or serve on the current runtime).\n */\nexport async function resolveAdapter(\n\toptions: ResolveAdapterOptions = {},\n): Promise<AdapterResolution> {\n\tconst env = options.env ?? process.env;\n\tconst load = options.load ?? loadAdapterPackage;\n\n\t// Import each package at most once\n\tconst loaded = new Map<string, Adapter | undefined>();\n\tconst loadOnce = async (pkg: string) => {\n\t\tif (!loaded.has(pkg)) loaded.set(pkg, await load(pkg));\n\t\treturn loaded.get(pkg);\n\t};\n\n\t// 1. A deployment environment selects its adapter\n\tfor (const candidate of ADAPTER_PACKAGES) {\n\t\tfor (const marker of candidate.platforms) {\n\t\t\tif (env[marker] === undefined) continue;\n\t\t\tconst adapter = await loadOnce(candidate.pkg);\n\t\t\tif (adapter) {\n\t\t\t\treturn { adapter, pkg: candidate.pkg, reason: `${marker} detected` };\n\t\t\t}\n\t\t}\n\t}\n\n\t// 2. An installed adapter (the list is in preference order)\n\tfor (const candidate of ADAPTER_PACKAGES) {\n\t\tconst adapter = await loadOnce(candidate.pkg);\n\t\tif (adapter) {\n\t\t\treturn { adapter, pkg: candidate.pkg, reason: \"installed\" };\n\t\t}\n\t}\n\n\treturn { adapter: undefined, pkg: undefined, reason: \"none found\" };\n}\n\n/**\n * The current runtime, when preview can serve a fetch handler without an\n * adapter package. (Node's request conversion lives in adapter-node, so\n * node is deliberately not handled here.)\n */\nfunction currentRuntime(): \"bun\" | \"deno\" | undefined {\n\tconst global = globalThis as any;\n\tif (global.Bun) return \"bun\";\n\tif (global.Deno) return \"deno\";\n\treturn undefined;\n}\n\n/**\n * Serves the built output on the current runtime, for when no adapter\n * package could be found. Returns false when the runtime isn't supported.\n */\nfunction serveOnCurrentRuntime(server: Server): boolean {\n\tconst runtime = currentRuntime();\n\tif (!runtime) return false;\n\n\tconst global = globalThis as any;\n\tconst port = parseInt(process.env.PORT ?? \"7059\");\n\tconst hostname = process.env.HOST ?? \"localhost\";\n\tconsole.log(`[torpor] Serving on the ${runtime} runtime at ${hostname}:${port}`);\n\n\tif (runtime === \"bun\") {\n\t\tglobal.Bun.serve({ port, hostname, fetch: (req: Request) => server.fetch(req) });\n\t} else {\n\t\tglobal.Deno.serve({ port, hostname }, (req: Request) => server.fetch(req));\n\t}\n\treturn true;\n}\n\n// The resolution is cached per process, and the \"Using ...\" message is only\n// logged once\nlet cache: Promise<AdapterResolution> | undefined;\nlet loggedPkg: string | undefined;\n\nfunction getCachedResolution(): Promise<AdapterResolution> {\n\treturn (cache ??= resolveAdapter());\n}\n\nfunction logAdapter(pkg: string, reason: string): void {\n\tif (loggedPkg === pkg) return;\n\tloggedPkg = pkg;\n\tconsole.log(`[torpor] Using the ${pkg.replace(\"@torpor/adapter-\", \"\")} adapter (${reason})`);\n}\n\n/**\n * The adapter used when the site doesn't set one explicitly. Rather than\n * being a real adapter, it resolves one at the time it's needed (see\n * `resolveAdapter`), and as a last resort serves the built output on the\n * current runtime -- Bun and Deno can serve a fetch handler natively, while\n * node needs `@torpor/adapter-node`.\n *\n * Dev mode is not affected: with no `dev` hook, the framework's default\n * Node-runtime dev plugin is used.\n */\nconst defaultAdapter: Adapter = {\n\tasync serve(server, site) {\n\t\tconst { adapter, pkg, reason } = await getCachedResolution();\n\t\tif (adapter && pkg) {\n\t\t\tlogAdapter(pkg, reason);\n\t\t\tawait adapter.serve(server, site);\n\t\t\treturn;\n\t\t}\n\t\tif (serveOnCurrentRuntime(server)) return;\n\t\tconsole.log(\n\t\t\t\"[torpor] No adapter found -- install one (e.g. `pnpm add @torpor/adapter-node`) or set `site.adapter` in site.config.ts\",\n\t\t);\n\t},\n\tasync prebuild(site) {\n\t\tconst { adapter, pkg, reason } = await getCachedResolution();\n\t\tif (!adapter || !pkg) return;\n\t\tlogAdapter(pkg, reason);\n\t\tawait adapter.prebuild?.(site);\n\t},\n\tasync postbuild(site) {\n\t\tconst { adapter, pkg, reason } = await getCachedResolution();\n\t\tif (!adapter || !pkg) return;\n\t\tlogAdapter(pkg, reason);\n\t\tawait adapter.postbuild?.(site);\n\t},\n};\n\nexport default defaultAdapter;\n","import { promises as fs } from \"node:fs\";\nimport fpath from \"node:path\";\nimport { type Plugin, type UserConfig } from \"vite\";\nimport type Adapter from \"../types/Adapter\";\nimport type SitePlugin from \"../types/SitePlugin\";\nimport type { InlineEndPoint } from \"../types/Route\";\nimport type PageServerEndPoint from \"../types/PageServerEndPoint\";\nimport type Route from \"../types/Route\";\nimport type ServerEndPoint from \"../types/ServerEndPoint\";\nimport type ServerHook from \"../types/ServerHook\";\nimport {\n\tERROR_ROUTE,\n\tHOOK_ROUTE,\n\tHOOK_SERVER_ROUTE,\n\tLAYOUT_ROUTE,\n\tLAYOUT_SERVER_ROUTE,\n\tPAGE_ROUTE,\n\tPAGE_SERVER_ROUTE,\n\tRouteType,\n\tSERVER_ROUTE,\n} from \"../types/RouteType\";\nimport defaultAdapter from \"./defaultAdapter\";\n\n// 1. The user sets this up\n// 2. It is passed into serverEntry and clientEntry through a Vite plugin\n// (defined in manifest.ts)\n// 3. In those files, it is used to build a Router\n\n/**\n * The Site class contains the information necessary to setup a @torpor/build\n * site, including routes, plugins, and options.\n */\nexport default class Site {\n\troot: string;\n\troutes: Route[] = [];\n\t/**\n\t * Torpor site plugins, run when the site config is loaded (for dev and\n\t * build) and when the server starts up. Use these to register extra\n\t * routes (e.g. `openApi()` from `@torpor/build/openapi`) or add other\n\t * framework-level functionality\n\t */\n\tplugins: SitePlugin[] = [];\n\t/**\n\t * Vite plugins to add to the client and server builds\n\t */\n\tvitePlugins: Plugin[] = [];\n\t/**\n\t * The site's adapter, e.g. from `@torpor/adapter-node` or\n\t * `@torpor/adapter-cloudflare`. When not set, one is resolved when it's\n\t * needed: a deployment environment (e.g. `CF_PAGES`) picks the platform's\n\t * adapter when installed, an installed adapter is used otherwise, and\n\t * preview falls back to serving on the current runtime. See\n\t * defaultAdapter.ts\n\t */\n\tadapter: Adapter = defaultAdapter;\n\t/**\n\t * Extra inputs, as absolute file paths, that will be compiled with the\n\t * output. What you do with them after that is up to you. File paths ending\n\t * with server.ts/js will only be compiled with the server output\n\t */\n\tinputs: string[] = [];\n\t/**\n\t * Vite config options to merge with the standard Torpor build options.\n\t * `plugins` and `build.rollupOptions.input` will be overridden, so you\n\t * should set `Site.vitePlugins` and `Site.inputs` instead. Other options may be\n\t * overridden or incompatible, so experimentation may be required\n\t */\n\tviteConfig?: UserConfig;\n\t/**\n\t * The path to the site.config file. Set automatically by the CLI; used by\n\t * the manifest plugin to import inline endpoints for server builds.\n\t */\n\tconfigFile?: string;\n\t/**\n\t * A state map for plugins to store their configuration and state on the\n\t * site, so that framework internals (e.g. the manifest plugin) and other\n\t * plugins can read them back:\n\t *\n\t * ```ts\n\t * const myKey = Symbol.for(\"my-plugin\");\n\t * site.pluginState.set(myKey, options);\n\t * ```\n\t *\n\t * Prefer `Symbol.for(\"...\")` keys (or plain strings) over `Symbol()`,\n\t * because the user's config file and framework internals can be loaded as\n\t * separate module instances -- registry symbols are shared across\n\t * instances, `Symbol()` ones are not.\n\t */\n\tpluginState: Map<PropertyKey, unknown> = new Map();\n\t/**\n\t * Inline endpoints keyed by `\"path:type\"`. Populated by `addRoute` when\n\t * the user passes an inline endpoint object instead of a file path.\n\t */\n\tinlineEndPoints: Record<string, InlineEndPoint> = {};\n\n\tconstructor() {\n\t\tthis.root = process.cwd();\n\t}\n\n\t/**\n\t * Adds a folder of routes, based on the names of the files in the folder\n\t * @param folder The folder containing the routes\n\t * @param subFolder If a subFolder is set, all routes in the folder will be\n\t * placed under the subFolder, and only hooks, layouts etc will be used if\n\t * they have the same subFolder. This can be used to create e.g. an `api`\n\t * set of routes with their own hook for authorization from various clients\n\t */\n\tasync addRouteFolder(folder: string, subFolder?: string): Promise<void> {\n\t\tconst routeFolder = fpath.join(this.root, folder);\n\t\tconst routeFiles = await fs.readdir(routeFolder, { recursive: true });\n\t\tif (subFolder !== undefined && !subFolder.startsWith(\"/\")) {\n\t\t\tsubFolder = \"/\" + subFolder;\n\t\t}\n\t\tfor (let file of routeFiles) {\n\t\t\t// The file must start with `+` or `_` and end with `.js` or `.ts`\n\t\t\tif (/^(\\+|_).+(\\.js|\\.ts)$/.test(fpath.basename(file))) {\n\t\t\t\tlet routePath = this.#routePath(file);\n\t\t\t\tif (subFolder !== undefined) {\n\t\t\t\t\troutePath = subFolder + routePath;\n\t\t\t\t}\n\t\t\t\tlet type = this.#routeType(file);\n\t\t\t\tfile = fpath.relative(this.root, fpath.resolve(routeFolder, file));\n\t\t\t\tthis.routes.push({\n\t\t\t\t\tpath: routePath,\n\t\t\t\t\tfile,\n\t\t\t\t\ttype,\n\t\t\t\t\tsubFolder: subFolder,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tthis.#sortRoutes();\n\t}\n\n\t/**\n\t * Adds a file route, which can have a standard name (e.g. +page.ts) or any\n\t * name if you pass the type\n\t * @param path The route path\n\t * @param file The file\n\t * @param type The file type (if the file name is non-standard)\n\t */\n\taddRouteFile(\n\t\tpath: string,\n\t\tfile: string,\n\t\ttype?:\n\t\t\t| \"+page\"\n\t\t\t| \"+page.server\"\n\t\t\t| \"+server\"\n\t\t\t| \"_layout\"\n\t\t\t| \"_layout.server\"\n\t\t\t| \"_hook\"\n\t\t\t| \"_hook.server\"\n\t\t\t| \"_error\",\n\t\tsubFolder?: string,\n\t): void {\n\t\tlet routePath = this.#routePath(fpath.join(path, type ?? fpath.basename(file)));\n\t\tlet routeType = type ? this.#routeTypeFromString(type) : this.#routeType(file);\n\t\tif (subFolder !== undefined && !subFolder.startsWith(\"/\")) {\n\t\t\tsubFolder = \"/\" + subFolder;\n\t\t}\n\t\tthis.routes.push({\n\t\t\tpath: routePath,\n\t\t\tfile,\n\t\t\ttype: routeType,\n\t\t\tsubFolder: subFolder,\n\t\t});\n\t\tthis.#sortRoutes();\n\t}\n\n\t#routePath(file: string): string {\n\t\tlet routePath = file\n\t\t\t.replace(/^\\//, \"\")\n\t\t\t.replace(/(\\.ts|\\.js)$/, \"\")\n\t\t\t.replace(/_hook.server$/, \"_hook/~server\")\n\t\t\t.replace(/_layout.server$/, \"_layout/~server\")\n\t\t\t.replace(/\\+page.server$/, \"~server\")\n\t\t\t.replace(/\\+server$/, \"\")\n\t\t\t.replace(/\\+page$/, \"\")\n\t\t\t.replace(/\\/$/, \"\");\n\t\treturn routePath.length > 0 ? `/${routePath}` : \"/\";\n\t}\n\n\t#routeType(file: string): RouteType {\n\t\tlet routePath = file.replace(/^\\//, \"\").replace(/(\\.ts|\\.js)$/, \"\");\n\t\tif (routePath.endsWith(\"+page\")) {\n\t\t\treturn PAGE_ROUTE;\n\t\t} else if (routePath.endsWith(\"+page.server\")) {\n\t\t\treturn PAGE_SERVER_ROUTE;\n\t\t} else if (routePath.endsWith(\"+server\")) {\n\t\t\treturn SERVER_ROUTE;\n\t\t} else if (routePath.endsWith(\"_layout\")) {\n\t\t\treturn LAYOUT_ROUTE;\n\t\t} else if (routePath.endsWith(\"_layout.server\")) {\n\t\t\treturn LAYOUT_SERVER_ROUTE;\n\t\t} else if (routePath.endsWith(\"_hook\")) {\n\t\t\treturn HOOK_ROUTE;\n\t\t} else if (routePath.endsWith(\"_hook.server\")) {\n\t\t\treturn HOOK_SERVER_ROUTE;\n\t\t} else if (routePath.endsWith(\"_error\")) {\n\t\t\treturn ERROR_ROUTE;\n\t\t}\n\t\treturn -1;\n\t}\n\n\t#routeTypeFromString(\n\t\ttype:\n\t\t\t| \"+page\"\n\t\t\t| \"+page.server\"\n\t\t\t| \"+server\"\n\t\t\t| \"_layout\"\n\t\t\t| \"_layout.server\"\n\t\t\t| \"_hook\"\n\t\t\t| \"_hook.server\"\n\t\t\t| \"_error\",\n\t) {\n\t\tswitch (type) {\n\t\t\tcase \"+page\":\n\t\t\t\treturn PAGE_ROUTE;\n\t\t\tcase \"+page.server\":\n\t\t\t\treturn PAGE_SERVER_ROUTE;\n\t\t\tcase \"+server\":\n\t\t\t\treturn SERVER_ROUTE;\n\t\t\tcase \"_layout\":\n\t\t\t\treturn LAYOUT_ROUTE;\n\t\t\tcase \"_layout.server\":\n\t\t\t\treturn LAYOUT_SERVER_ROUTE;\n\t\t\tcase \"_hook\":\n\t\t\t\treturn HOOK_ROUTE;\n\t\t\tcase \"_hook.server\":\n\t\t\t\treturn HOOK_SERVER_ROUTE;\n\t\t\tcase \"_error\":\n\t\t\t\treturn ERROR_ROUTE;\n\t\t}\n\t}\n\n\t/**\n\t * Adds a route in code, without needing the standard file naming\n\t * conventions (+page.ts, +page.server.ts, etc). This is useful for small\n\t * sites with one or two pages.\n\t *\n\t * Any combination of options may be provided; each maps to the\n\t * corresponding file-route type:\n\t *\n\t * - `page` — a `+page` (`.torp` component or `.ts`/`.js` `PageEndPoint`)\n\t * - `pageServer` — a `+page.server` (`PageServerEndPoint`, file or inline)\n\t * - `server` — a `+server` HTTP endpoint (`ServerEndPoint`, file or inline)\n\t * - `layout` — a `_layout` (`.torp` component or `.ts`/`.js` `PageEndPoint`)\n\t * - `layoutServer` — a `_layout.server` (`PageServerEndPoint`, file or inline)\n\t * - `hookServer` — a `_hook.server` (`ServerHook`, file or inline)\n\t * - `error` — an `_error` page (`.torp` component or `.ts`/`.js` `PageEndPoint`)\n\t *\n\t * Options that need a component (`page`, `layout`, `error`) must be file\n\t * paths so the component is included in the client bundle. Server-only\n\t * options (`pageServer`, `server`, `layoutServer`, `hookServer`) accept\n\t * either a file path or an inline object; inline server code is kept out\n\t * of the client bundle automatically.\n\t *\n\t * @example\n\t * // A page with an inline server action\n\t * site.addRoute(\"/\", {\n\t * page: \"./src/Counter.torp\",\n\t * pageServer: { actions: { set: async ({ request }) => ok() } },\n\t * });\n\t * @example\n\t * // A JSON API endpoint\n\t * site.addRoute(\"/api/time\", {\n\t * server: { get: async () => ok({ time: Date.now() }) },\n\t * });\n\t * @example\n\t * // A root layout, server hook, and error page\n\t * site.addRoute(\"/\", {\n\t * layout: \"./src/Layout.torp\",\n\t * hookServer: { enter: async (event) => {} },\n\t * error: \"./src/ErrorPage.torp\",\n\t * });\n\t *\n\t * @param path The route path, e.g. `/` or `/about`\n\t * @param options File paths and/or inline endpoints for the route\n\t * @param subFolder An optional subFolder for grouping routes\n\t */\n\taddRoute(\n\t\tpath: string,\n\t\toptions: {\n\t\t\tpage?: string;\n\t\t\tpageServer?: string | PageServerEndPoint;\n\t\t\tserver?: string | ServerEndPoint;\n\t\t\tlayout?: string;\n\t\t\tlayoutServer?: string | PageServerEndPoint;\n\t\t\thookServer?: string | ServerHook;\n\t\t\terror?: string;\n\t\t},\n\t\tsubFolder?: string,\n\t): void {\n\t\tlet normalizedSubFolder = subFolder;\n\t\tif (normalizedSubFolder !== undefined && !normalizedSubFolder.startsWith(\"/\")) {\n\t\t\tnormalizedSubFolder = \"/\" + normalizedSubFolder;\n\t\t}\n\n\t\t// The base path with any trailing slash stripped, so that we can\n\t\t// append \"/_layout\", \"/~server\" etc. For the root \"/\" this is \"\".\n\t\tlet base = path.replace(/\\/$/, \"\");\n\n\t\tif (options.page !== undefined) {\n\t\t\tthis.#pushRoute(path, PAGE_ROUTE, options.page, normalizedSubFolder);\n\t\t}\n\t\tif (options.pageServer !== undefined) {\n\t\t\tthis.#pushRoute(\n\t\t\t\t`${base}/~server`,\n\t\t\t\tPAGE_SERVER_ROUTE,\n\t\t\t\toptions.pageServer,\n\t\t\t\tnormalizedSubFolder,\n\t\t\t);\n\t\t}\n\t\tif (options.server !== undefined) {\n\t\t\tthis.#pushRoute(path, SERVER_ROUTE, options.server, normalizedSubFolder);\n\t\t}\n\t\tif (options.layout !== undefined) {\n\t\t\tthis.#pushRoute(`${base}/_layout`, LAYOUT_ROUTE, options.layout, normalizedSubFolder);\n\t\t}\n\t\tif (options.layoutServer !== undefined) {\n\t\t\tthis.#pushRoute(\n\t\t\t\t`${base}/_layout/~server`,\n\t\t\t\tLAYOUT_SERVER_ROUTE,\n\t\t\t\toptions.layoutServer,\n\t\t\t\tnormalizedSubFolder,\n\t\t\t);\n\t\t}\n\t\tif (options.hookServer !== undefined) {\n\t\t\tthis.#pushRoute(\n\t\t\t\t`${base}/_hook/~server`,\n\t\t\t\tHOOK_SERVER_ROUTE,\n\t\t\t\toptions.hookServer,\n\t\t\t\tnormalizedSubFolder,\n\t\t\t);\n\t\t}\n\t\tif (options.error !== undefined) {\n\t\t\tthis.#pushRoute(`${base}/_error`, ERROR_ROUTE, options.error, normalizedSubFolder);\n\t\t}\n\n\t\tthis.#sortRoutes();\n\t}\n\n\t/**\n\t * Pushes a single route entry, handling both file-path and inline-endpoint\n\t * targets. File paths are resolved relative to the site root; inline\n\t * endpoints are also stored in `inlineEndPoints` keyed by `\"path:type\"`\n\t * so the manifest plugin can resolve them at build time.\n\t *\n\t * If a route with the same path and type was already added, it is\n\t * replaced, so that running a plugin twice (config load and server\n\t * startup) doesn't add duplicate routes.\n\t */\n\t#pushRoute(\n\t\troutePath: string,\n\t\ttype: RouteType,\n\t\ttarget: string | InlineEndPoint,\n\t\tsubFolder?: string,\n\t): void {\n\t\tconst index = this.routes.findIndex((r) => r.path === routePath && r.type === type);\n\t\tif (typeof target === \"string\") {\n\t\t\tlet file = fpath.relative(this.root, fpath.resolve(this.root, target));\n\t\t\tif (index >= 0) {\n\t\t\t\tthis.routes[index] = { path: routePath, file, type, subFolder };\n\t\t\t} else {\n\t\t\t\tthis.routes.push({ path: routePath, file, type, subFolder });\n\t\t\t}\n\t\t} else {\n\t\t\tlet key = `${routePath}:${type}`;\n\t\t\tthis.inlineEndPoints[key] = target;\n\t\t\tif (index >= 0) {\n\t\t\t\tthis.routes[index] = { path: routePath, endPoint: target, type, subFolder };\n\t\t\t} else {\n\t\t\t\tthis.routes.push({ path: routePath, endPoint: target, type, subFolder });\n\t\t\t}\n\t\t}\n\t}\n\n\t#sortRoutes() {\n\t\tthis.routes = this.routes.sort((a, b) => {\n\t\t\t// Sort [param]s after paths\n\t\t\t// There might be a quicker/easier way to do this\n\t\t\tfor (let i = 0; i < Math.min(a.path.length, b.path.length); i++) {\n\t\t\t\tif (a.path[i] === \"[\" && b.path[i] !== \"[\") {\n\t\t\t\t\treturn 1;\n\t\t\t\t} else if (b.path[i] === \"[\" && a.path[i] !== \"[\") {\n\t\t\t\t\treturn -1;\n\t\t\t\t} else if (a.path[i] === b.path[i]) {\n\t\t\t\t\t// Keep going...\n\t\t\t\t} else {\n\t\t\t\t\treturn a.path[i].localeCompare(b.path[i]);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn a.path.length - b.path.length;\n\t\t});\n\t}\n}\n"],"mappings":"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"}
@@ -88,13 +88,15 @@ async function loadClientAndServerData(data, location, query, params, clientEndP
88
88
  for (let [name, value] of query) clientUrl.searchParams.append(name, value);
89
89
  const clientParams = buildClientParams(clientUrl, params, data);
90
90
  const clientResponse = await clientEndPoint.load(clientParams);
91
- if (clientResponse) if (clientResponse.ok) {
92
- if (clientResponse.headers.get("Content-Type")?.includes("application/json")) {
93
- const clientData = await clientResponse.json();
94
- Object.assign(data, clientData);
95
- prefetchedData[clientKey] = clientData;
96
- }
97
- } else return clientResponse;
91
+ if (clientResponse) {
92
+ if (clientResponse.ok) {
93
+ if (clientResponse.headers.get("Content-Type")?.includes("application/json")) {
94
+ const clientData = await clientResponse.json();
95
+ Object.assign(data, clientData);
96
+ prefetchedData[clientKey] = clientData;
97
+ }
98
+ } else return clientResponse;
99
+ }
98
100
  }
99
101
  }
100
102
  if (serverEndPoint?.load) {
@@ -105,13 +107,15 @@ async function loadClientAndServerData(data, location, query, params, clientEndP
105
107
  const serverUrl = new URL(serverLocation);
106
108
  for (let [name, value] of query) serverUrl.searchParams.append(name, value);
107
109
  const serverResponse = await fetch(serverUrl);
108
- if (serverResponse) if (serverResponse.ok) {
109
- if (serverResponse.headers.get("Content-Type")?.includes("application/json")) {
110
- const serverData = await serverResponse.json();
111
- Object.assign(data, serverData);
112
- prefetchedData[serverKey] = serverData;
113
- }
114
- } else return serverResponse;
110
+ if (serverResponse) {
111
+ if (serverResponse.ok) {
112
+ if (serverResponse.headers.get("Content-Type")?.includes("application/json")) {
113
+ const serverData = await serverResponse.json();
114
+ Object.assign(data, serverData);
115
+ prefetchedData[serverKey] = serverData;
116
+ }
117
+ } else return serverResponse;
118
+ }
115
119
  }
116
120
  }
117
121
  }
@@ -183,17 +187,19 @@ async function navigate(url, withHydration = false) {
183
187
  };
184
188
  for (let i = handler.layouts.length - 1; i >= 0; i--) {
185
189
  const layoutEndPoint = (await handler.layouts[i].endPoint())?.default;
186
- if (layoutEndPoint?.component) if (layoutStack[i].reuse) {
187
- component = slotFunctions[i + 1];
188
- reusedIndex = i;
189
- reused = true;
190
- break;
191
- } else if (i === 0) {
192
- component = layoutEndPoint.component;
193
- slots = { _: slotFunctions[i + 1] };
194
- } else slotFunctions[i] = function layoutComponent(parent, anchor, _, $context) {
195
- layoutStack[i - 1].slotRegion = fillLayoutSlot(layoutEndPoint.component, slotFunctions[i + 1], parent, anchor, $props, $context);
196
- };
190
+ if (layoutEndPoint?.component) {
191
+ if (layoutStack[i].reuse) {
192
+ component = slotFunctions[i + 1];
193
+ reusedIndex = i;
194
+ reused = true;
195
+ break;
196
+ } else if (i === 0) {
197
+ component = layoutEndPoint.component;
198
+ slots = { _: slotFunctions[i + 1] };
199
+ } else slotFunctions[i] = function layoutComponent(parent, anchor, _, $context) {
200
+ layoutStack[i - 1].slotRegion = fillLayoutSlot(layoutEndPoint.component, slotFunctions[i + 1], parent, anchor, $props, $context);
201
+ };
202
+ }
197
203
  }
198
204
  }
199
205
  try {
@@ -225,4 +231,4 @@ async function load(path) {
225
231
  //#endregion
226
232
  export { client as a, reload as i, navigate as n, loadData as r, load as t };
227
233
 
228
- //# sourceMappingURL=load-BntzvmwL.mjs.map
234
+ //# sourceMappingURL=load-C9RpS_4j.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"load-BntzvmwL.mjs","names":[],"sources":["../src/state/client.ts","../src/nav/reload.ts","../src/nav/formSubmit.ts","../src/nav/loadData.ts","../src/nav/navigate.ts","../src/nav/load.ts"],"sourcesContent":["import type ClientState from \"../types/ClientState\";\n\nconst CLIENT_SYMBOL: unique symbol = Symbol.for(\"torp.Client\");\n\nconst client: ClientState =\n\t// @ts-ignore\n\t(globalThis[CLIENT_SYMBOL] ??= {\n\t\t// @ts-ignore This will definitely, 100% get set\n\t\trouter: null,\n\t\tlayoutStack: [],\n\t\t// TODO: Probably expire after 30 seconds or something?\n\t\tprefetchedData: {},\n\t});\n\nexport default client;\n","import navigate from \"./navigate\";\n\n/**\n * Reloads data for the currently loaded page and rerenders it.\n */\nexport default async function reload(): Promise<boolean> {\n\tconst url = new URL(document.location.href);\n\treturn await navigate(url);\n}\n","import $page from \"../state/$page\";\nimport load from \"./load\";\nimport reload from \"./reload\";\n\nexport default async function formSubmit(e: SubmitEvent): Promise<void> {\n\t// We'll take it from here\n\te.preventDefault();\n\n\t// Reset $page on submit and link navigation\n\t// TODO: Should we put data in $page.data??\n\t$page.form = undefined;\n\n\t// Perform a POST fetch to the form's action location\n\tconst form = e.target as HTMLFormElement;\n\tconst button = e.submitter as HTMLButtonElement;\n\t// HACK: submit buttons have their formAction set to the current page if not\n\t// set, so we can't just check whether it's empty and fallback to form action\n\t//const action = button?.formAction ?? form.action;\n\tlet action = form.action || document.URL;\n\tif (button && button.formAction && button.formAction !== document.URL) {\n\t\taction = button.formAction;\n\t}\n\tconst options = {\n\t\tbody: new FormData(form),\n\t\tmethod: \"POST\",\n\t\tmode: \"cors\",\n\t\tcredentials: \"same-origin\",\n\t\t// Add a special header which tells the server that we want the raw\n\t\t// error response (if applicable) rather than the reloaded page that you\n\t\t// would get if submitting a page without javascript\n\t\theaders: {\n\t\t\t\"X-Torpor-Form-Submit\": \"\",\n\t\t},\n\t};\n\tconst response = await fetch(\n\t\taction,\n\t\t// @ts-ignore -- don't know what it's complaining about here\n\t\toptions,\n\t);\n\n\tif (\n\t\tresponse.status === 200 &&\n\t\tresponse.headers.has(\"Content-Disposition\") &&\n\t\t/attachment;\\s*filename=\"(.+?)\"/.test(response.headers.get(\"Content-Disposition\")!)\n\t) {\n\t\t// TODO: Is this enough to check to see if we have a file that should download?\n\t\t// Download the file by creating a dummy <a> and clicking it\n\t\tconst downloadFileName =\n\t\t\tresponse.headers.get(\"Content-Disposition\")?.match(/attachment;\\s*filename=\"(.+?)\"/)![1] ??\n\t\t\t\"file\";\n\t\tconst downloadLink = document.createElement(\"a\");\n\t\tdownloadLink.href = window.URL.createObjectURL(await response.blob());\n\t\tdownloadLink.download = downloadFileName;\n\t\tdownloadLink.click();\n\t} else if (response.status === 200 && response.headers.has(\"X-Torpor-Form-Redirect\")) {\n\t\t// TODO: Should do the correct type of redirect e.g. seeOther etc\n\t\tconst redirect = response.headers.get(\"Location\")!;\n\t\tawait load(redirect);\n\t\twindow.location.href = redirect;\n\t} else if (\n\t\t(response.status >= 200 && response.status <= 299) ||\n\t\t(response.status >= 400 && response.status <= 499)\n\t) {\n\t\t// Put the response into `$page.form`, and it will be put into `$props` in reload\n\t\tif (response.headers.get(\"Content-Type\") === \"application/json\") {\n\t\t\t$page.form = await response.json();\n\t\t}\n\t\tawait reload();\n\t}\n}\n","import client from \"../state/client\";\nimport type LayoutPath from \"../types/LayoutPath\";\nimport type PageEndPoint from \"../types/PageEndPoint\";\nimport type PageServerEndPoint from \"../types/PageServerEndPoint\";\nimport type RouteHandler from \"../types/RouteHandler\";\n\nexport default async function loadData(\n\thandler: RouteHandler,\n\tparams: Record<string, string>,\n\tpath: string,\n\tquery: URLSearchParams,\n\tnewLayoutStack: LayoutPath[],\n\tclientEndPoint: PageEndPoint | undefined,\n\tserverEndPoint: PageServerEndPoint | undefined,\n\tprefetch = false,\n): Promise<Record<string, string> | void> {\n\tlet data = {};\n\tif (handler.layouts) {\n\t\tlet layoutStack = client.layoutStack;\n\t\tfor (let [i, layout] of handler.layouts.entries()) {\n\t\t\tlet layoutPath = layout.path;\n\t\t\tfor (let key in params) {\n\t\t\t\tlayoutPath = layoutPath.replace(`[${key}]`, params[key]);\n\t\t\t}\n\t\t\tif (layoutStack.at(i)?.path === layoutPath) {\n\t\t\t\t// We've already loaded this layout, we can just re-use its data and UI\n\t\t\t\tif (!prefetch) {\n\t\t\t\t\tlayoutStack[i].reuse = true;\n\t\t\t\t}\n\t\t\t\tnewLayoutStack[i] = layoutStack[i];\n\t\t\t\tObject.assign(data, layoutStack[i].data);\n\t\t\t} else {\n\t\t\t\tconst stackLayout = { path: layoutPath, data: {}, reuse: false, slotRegion: null };\n\t\t\t\tconst layoutEndPoint: PageEndPoint | undefined = (await layout.endPoint())?.default;\n\t\t\t\tconst layoutServerEndPoint: PageServerEndPoint | undefined =\n\t\t\t\t\tlayout.serverEndPoint && (await layout.serverEndPoint())?.default;\n\t\t\t\tconst layoutResponse = await loadClientAndServerData(\n\t\t\t\t\tstackLayout.data,\n\t\t\t\t\tdocument.location.origin + layoutPath,\n\t\t\t\t\tquery,\n\t\t\t\t\tparams,\n\t\t\t\t\tlayoutEndPoint,\n\t\t\t\t\tlayoutServerEndPoint,\n\t\t\t\t);\n\t\t\t\tif (layoutResponse?.ok === false) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tObject.assign(data, stackLayout.data);\n\t\t\t\tnewLayoutStack.push(stackLayout);\n\t\t\t}\n\t\t}\n\t}\n\tlet endPointResponse = await loadClientAndServerData(\n\t\tdata,\n\t\tdocument.location.origin + path,\n\t\tquery,\n\t\tparams,\n\t\tclientEndPoint,\n\t\tserverEndPoint,\n\t);\n\tif (endPointResponse?.ok === false) {\n\t\treturn;\n\t}\n\treturn data;\n}\n\nasync function loadClientAndServerData(\n\tdata: Record<string, any>,\n\tlocation: string,\n\tquery: URLSearchParams,\n\tparams: Record<string, string>,\n\tclientEndPoint?: PageEndPoint,\n\tserverEndPoint?: PageServerEndPoint,\n): Promise<Response | undefined | void> {\n\tlet prefetchedData = client.prefetchedData;\n\n\tif (clientEndPoint?.load) {\n\t\tconst clientKey = location + (query.size > 0 ? `?${query}` : \"\");\n\t\tif (prefetchedData[clientKey]) {\n\t\t\tObject.assign(data, prefetchedData[clientKey]);\n\t\t} else {\n\t\t\tconst clientUrl = new URL(document.location.href);\n\t\t\tfor (let [name, value] of query) {\n\t\t\t\tclientUrl.searchParams.append(name, value);\n\t\t\t}\n\t\t\tconst clientParams = buildClientParams(clientUrl, params, data);\n\t\t\tconst clientResponse = await clientEndPoint.load(clientParams);\n\t\t\tif (clientResponse) {\n\t\t\t\tif (clientResponse.ok) {\n\t\t\t\t\tif (clientResponse.headers.get(\"Content-Type\")?.includes(\"application/json\")) {\n\t\t\t\t\t\tconst clientData = await clientResponse.json();\n\t\t\t\t\t\tObject.assign(data, clientData);\n\t\t\t\t\t\tprefetchedData[clientKey] = clientData;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\treturn clientResponse;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif (serverEndPoint?.load) {\n\t\tconst serverLocation = location.replace(/\\/$/, \"\") + \"/~server\";\n\t\tconst serverKey = serverLocation + (query.size > 0 ? `?${query}` : \"\");\n\t\tif (prefetchedData[serverKey]) {\n\t\t\tObject.assign(data, prefetchedData[serverKey]);\n\t\t} else {\n\t\t\tconst serverUrl = new URL(serverLocation);\n\t\t\tfor (let [name, value] of query) {\n\t\t\t\tserverUrl.searchParams.append(name, value);\n\t\t\t}\n\t\t\tconst serverResponse = await fetch(serverUrl);\n\t\t\tif (serverResponse) {\n\t\t\t\tif (serverResponse.ok) {\n\t\t\t\t\tif (serverResponse.headers.get(\"Content-Type\")?.includes(\"application/json\")) {\n\t\t\t\t\t\tconst serverData = await serverResponse.json();\n\t\t\t\t\t\tObject.assign(data, serverData);\n\t\t\t\t\t\tprefetchedData[serverKey] = serverData;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\treturn serverResponse;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction buildClientParams(url: URL, params: Record<string, string>, data: Record<string, any>) {\n\treturn {\n\t\turl,\n\t\tparams,\n\t\tdata,\n\t};\n}\n","import { clearLayoutSlot, fillLayoutSlot, hydrate } from \"@torpor/view\";\nimport { type Component, type SlotRender } from \"@torpor/view\";\nimport { mount, unmount } from \"@torpor/view\";\nimport $page from \"../state/$page\";\nimport client from \"../state/client\";\nimport type LayoutPath from \"../types/LayoutPath\";\nimport type PageEndPoint from \"../types/PageEndPoint\";\nimport type PageServerEndPoint from \"../types/PageServerEndPoint\";\nimport formSubmit from \"./formSubmit\";\nimport loadData from \"./loadData\";\n\n// @ts-ignore\nexport default async function navigate(url: URL, withHydration = false): Promise<boolean> {\n\tlet parent = document.getElementById(\"app\");\n\tif (!parent) {\n\t\t// TODO: 500\n\t\tconsole.log(\"500\");\n\t\treturn false;\n\t}\n\n\tconst path = url.pathname;\n\tconst query = url.searchParams;\n\n\t//console.log(`navigating to '${path}'${query.size ? ` with ${query}` : \"\"}`);\n\n\tconst route = client.router.match(path, query);\n\tif (!route) {\n\t\t// TODO: 404\n\t\tconsole.log(\"404\");\n\t\treturn false;\n\t}\n\n\t// Update $page before building the components\n\t$page.url = url;\n\tif (path.endsWith(\"/_error\")) {\n\t\t$page.status = parseInt(query.get(\"status\") ?? \"404\");\n\t\t$page.error = { message: query.get(\"message\") ?? \"\" };\n\t\t// Make it look a bit classier by removing the query\n\t\twindow.history.replaceState({}, \"\", url.toString().split(\"?\")[0]);\n\t} else {\n\t\t$page.status = 200;\n\t}\n\tconst handler = route.handler;\n\tconst params = route.params || {};\n\n\t// There must be a client endpoint with a component\n\tconst clientEndPoint: PageEndPoint | undefined = (await handler.endPoint()).default;\n\tif (!clientEndPoint?.component) {\n\t\t// TODO: 404\n\t\tconsole.log(\"404\");\n\t\treturn false;\n\t}\n\n\t// There may be a server endpoint\n\tconst serverEndPoint: PageServerEndPoint | undefined =\n\t\thandler.serverEndPoint && (await handler.serverEndPoint())?.default;\n\n\tlet newLayoutStack: LayoutPath[] = [];\n\n\t// Pass the data into $props\n\t// TODO: Don't load if this is the first time -- it should have been passed\n\t// to us, somehow...\n\tconst data = await loadData(\n\t\thandler,\n\t\tparams,\n\t\tpath,\n\t\tquery,\n\t\tnewLayoutStack,\n\t\tclientEndPoint,\n\t\tserverEndPoint,\n\t);\n\t// We may have form data in a hidden input -- not sure if this is the best\n\t// way to do it\n\tlet formInput = document.getElementById(\"t-form-data\") as HTMLInputElement;\n\tlet form: Record<string, string> | undefined;\n\tif (formInput) {\n\t\tform = JSON.parse(formInput.value);\n\t\t$page.form = form;\n\t\tformInput.remove();\n\t}\n\tlet $props: Record<string, any> = { data };\n\n\t// We may have form data in $page.form, either from the hidden input\n\t// (above), or added via onformsubmit (below)\n\t$props.form = $page.form;\n\n\t// Add some special context for submitting Forms client-side\n\tconst $context = {\n\t\tTorporBuildContext: { onformsubmit: formSubmit },\n\t};\n\n\tclient.layoutStack.push({ path: route.handler.path, data: {}, reuse: false, slotRegion: null });\n\tclient.layoutStack = newLayoutStack;\n\tlet layoutStack = client.layoutStack;\n\n\t// If there are layouts, work our way upwards, pushing each component into\n\t// the default slot of its parent\n\t// TODO: There's probably a nicer way to do this with reducers or something\n\tlet component = clientEndPoint.component as Component;\n\tlet slots: Record<string, SlotRender> | undefined = undefined;\n\tlet reused = false;\n\t// The index of the innermost reused layout. Its slotRegion is the region\n\t// that must be cleared and refilled (it contains the next layout's or the\n\t// page's content). This is usually the last entry of the stack, but not\n\t// when an outer layout is reused while an inner one is new — e.g.\n\t// navigating between sections that share the root layout.\n\tlet reusedIndex = -1;\n\tif (handler.layouts) {\n\t\tlet slotFunctions: SlotRender[] = [];\n\t\t// The last slot function will render the client component\n\t\tslotFunctions[handler.layouts.length] = function clientComponent(parent, anchor) {\n\t\t\tlet i = layoutStack.length - 1;\n\t\t\tlayoutStack[i].slotRegion = fillLayoutSlot(\n\t\t\t\tclientEndPoint.component!,\n\t\t\t\tslotFunctions[i + 1],\n\t\t\t\tparent,\n\t\t\t\tanchor,\n\t\t\t\t$props,\n\t\t\t\t$context,\n\t\t\t);\n\t\t};\n\n\t\t// Each earlier slot function will render a layout\n\t\tfor (let i = handler.layouts.length - 1; i >= 0; i--) {\n\t\t\tconst layoutEndPoint: PageEndPoint | undefined = (await handler.layouts[i].endPoint())\n\t\t\t\t?.default;\n\t\t\tif (layoutEndPoint?.component) {\n\t\t\t\tif (layoutStack[i].reuse) {\n\t\t\t\t\t// Reuse this layout — clear and refill its slot (done in\n\t\t\t\t\t// the try block below so a failure doesn't leave the slot\n\t\t\t\t\t// half-cleared)\n\t\t\t\t\tcomponent = slotFunctions[i + 1] as Component;\n\t\t\t\t\treusedIndex = i;\n\t\t\t\t\treused = true;\n\t\t\t\t\tbreak;\n\t\t\t\t} else if (i === 0) {\n\t\t\t\t\tcomponent = layoutEndPoint.component as Component;\n\t\t\t\t\tslots = { _: slotFunctions[i + 1] };\n\t\t\t\t} else {\n\t\t\t\t\tslotFunctions[i] = function layoutComponent(parent, anchor, _, $context) {\n\t\t\t\t\t\tlayoutStack[i - 1].slotRegion = fillLayoutSlot(\n\t\t\t\t\t\t\tlayoutEndPoint.component!,\n\t\t\t\t\t\t\tslotFunctions[i + 1],\n\t\t\t\t\t\t\tparent,\n\t\t\t\t\t\t\tanchor,\n\t\t\t\t\t\t\t$props,\n\t\t\t\t\t\t\t$context,\n\t\t\t\t\t\t);\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\ttry {\n\t\tif (reused) {\n\t\t\t// The layout is being reused — clear the old slot content, then\n\t\t\t// call the slot function directly to fill it with the new page.\n\t\t\t// We must not go through `mount` here: the slot's container still\n\t\t\t// holds the layout's own children (e.g. a header), which `mount`\n\t\t\t// refuses to mount into. Both the clear and the fill are inside\n\t\t\t// the try so that a failure doesn't leave the slot half-cleared.\n\t\t\tconst slotRegion = layoutStack[reusedIndex].slotRegion;\n\t\t\tparent = slotRegion.startNode!.parentNode as HTMLElement;\n\t\t\tclearLayoutSlot(slotRegion);\n\t\t\tcomponent(parent, null);\n\t\t} else if (withHydration) {\n\t\t\thydrate(parent, component, $props, slots);\n\t\t} else {\n\t\t\t// The layout chain changed (or there was no previous layout to\n\t\t\t// reuse): tear down the previous UI entirely — disposing its\n\t\t\t// region tree and clearing `#app` — so `mount` starts fresh.\n\t\t\t// Without this, `mount` throws because `#app` still holds the\n\t\t\t// previous render's children, and it would reuse a stale root\n\t\t\t// region.\n\t\t\tunmount(parent);\n\t\t\tmount(parent, component, $props, slots);\n\t\t}\n\t} catch (error) {\n\t\t// TODO: Show a proper Error component\n\t\tparent.innerHTML = '<span style=\"color: red\">Script syntax error</span><p>' + error + \"</p>\";\n\t\tconsole.log(error);\n\t}\n\n\t// Reset prefetched data on each navigation\n\tclient.prefetchedData = {};\n\n\treturn true;\n}\n","import navigate from \"./navigate\";\n\n/**\n * Loads and renders the page at the supplied path.\n */\nexport default async function load(path: string): Promise<boolean> {\n\tconst url = new URL(path, document.location.href);\n\treturn await navigate(url);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"load-C9RpS_4j.mjs","names":[],"sources":["../src/state/client.ts","../src/nav/reload.ts","../src/nav/formSubmit.ts","../src/nav/loadData.ts","../src/nav/navigate.ts","../src/nav/load.ts"],"sourcesContent":["import type ClientState from \"../types/ClientState\";\n\nconst CLIENT_SYMBOL: unique symbol = Symbol.for(\"torp.Client\");\n\nconst client: ClientState =\n\t// @ts-ignore\n\t(globalThis[CLIENT_SYMBOL] ??= {\n\t\t// @ts-ignore This will definitely, 100% get set\n\t\trouter: null,\n\t\tlayoutStack: [],\n\t\t// TODO: Probably expire after 30 seconds or something?\n\t\tprefetchedData: {},\n\t});\n\nexport default client;\n","import navigate from \"./navigate\";\n\n/**\n * Reloads data for the currently loaded page and rerenders it.\n */\nexport default async function reload(): Promise<boolean> {\n\tconst url = new URL(document.location.href);\n\treturn await navigate(url);\n}\n","import $page from \"../state/$page\";\nimport load from \"./load\";\nimport reload from \"./reload\";\n\nexport default async function formSubmit(e: SubmitEvent): Promise<void> {\n\t// We'll take it from here\n\te.preventDefault();\n\n\t// Reset $page on submit and link navigation\n\t// TODO: Should we put data in $page.data??\n\t$page.form = undefined;\n\n\t// Perform a POST fetch to the form's action location\n\tconst form = e.target as HTMLFormElement;\n\tconst button = e.submitter as HTMLButtonElement;\n\t// HACK: submit buttons have their formAction set to the current page if not\n\t// set, so we can't just check whether it's empty and fallback to form action\n\t//const action = button?.formAction ?? form.action;\n\tlet action = form.action || document.URL;\n\tif (button && button.formAction && button.formAction !== document.URL) {\n\t\taction = button.formAction;\n\t}\n\tconst options = {\n\t\tbody: new FormData(form),\n\t\tmethod: \"POST\",\n\t\tmode: \"cors\",\n\t\tcredentials: \"same-origin\",\n\t\t// Add a special header which tells the server that we want the raw\n\t\t// error response (if applicable) rather than the reloaded page that you\n\t\t// would get if submitting a page without javascript\n\t\theaders: {\n\t\t\t\"X-Torpor-Form-Submit\": \"\",\n\t\t},\n\t};\n\tconst response = await fetch(\n\t\taction,\n\t\t// @ts-ignore -- don't know what it's complaining about here\n\t\toptions,\n\t);\n\n\tif (\n\t\tresponse.status === 200 &&\n\t\tresponse.headers.has(\"Content-Disposition\") &&\n\t\t/attachment;\\s*filename=\"(.+?)\"/.test(response.headers.get(\"Content-Disposition\")!)\n\t) {\n\t\t// TODO: Is this enough to check to see if we have a file that should download?\n\t\t// Download the file by creating a dummy <a> and clicking it\n\t\tconst downloadFileName =\n\t\t\tresponse.headers.get(\"Content-Disposition\")?.match(/attachment;\\s*filename=\"(.+?)\"/)![1] ??\n\t\t\t\"file\";\n\t\tconst downloadLink = document.createElement(\"a\");\n\t\tdownloadLink.href = window.URL.createObjectURL(await response.blob());\n\t\tdownloadLink.download = downloadFileName;\n\t\tdownloadLink.click();\n\t} else if (response.status === 200 && response.headers.has(\"X-Torpor-Form-Redirect\")) {\n\t\t// TODO: Should do the correct type of redirect e.g. seeOther etc\n\t\tconst redirect = response.headers.get(\"Location\")!;\n\t\tawait load(redirect);\n\t\twindow.location.href = redirect;\n\t} else if (\n\t\t(response.status >= 200 && response.status <= 299) ||\n\t\t(response.status >= 400 && response.status <= 499)\n\t) {\n\t\t// Put the response into `$page.form`, and it will be put into `$props` in reload\n\t\tif (response.headers.get(\"Content-Type\") === \"application/json\") {\n\t\t\t$page.form = await response.json();\n\t\t}\n\t\tawait reload();\n\t}\n}\n","import client from \"../state/client\";\nimport type LayoutPath from \"../types/LayoutPath\";\nimport type PageEndPoint from \"../types/PageEndPoint\";\nimport type PageServerEndPoint from \"../types/PageServerEndPoint\";\nimport type RouteHandler from \"../types/RouteHandler\";\n\nexport default async function loadData(\n\thandler: RouteHandler,\n\tparams: Record<string, string>,\n\tpath: string,\n\tquery: URLSearchParams,\n\tnewLayoutStack: LayoutPath[],\n\tclientEndPoint: PageEndPoint | undefined,\n\tserverEndPoint: PageServerEndPoint | undefined,\n\tprefetch = false,\n): Promise<Record<string, string> | void> {\n\tlet data = {};\n\tif (handler.layouts) {\n\t\tlet layoutStack = client.layoutStack;\n\t\tfor (let [i, layout] of handler.layouts.entries()) {\n\t\t\tlet layoutPath = layout.path;\n\t\t\tfor (let key in params) {\n\t\t\t\tlayoutPath = layoutPath.replace(`[${key}]`, params[key]);\n\t\t\t}\n\t\t\tif (layoutStack.at(i)?.path === layoutPath) {\n\t\t\t\t// We've already loaded this layout, we can just re-use its data and UI\n\t\t\t\tif (!prefetch) {\n\t\t\t\t\tlayoutStack[i].reuse = true;\n\t\t\t\t}\n\t\t\t\tnewLayoutStack[i] = layoutStack[i];\n\t\t\t\tObject.assign(data, layoutStack[i].data);\n\t\t\t} else {\n\t\t\t\tconst stackLayout = { path: layoutPath, data: {}, reuse: false, slotRegion: null };\n\t\t\t\tconst layoutEndPoint: PageEndPoint | undefined = (await layout.endPoint())?.default;\n\t\t\t\tconst layoutServerEndPoint: PageServerEndPoint | undefined =\n\t\t\t\t\tlayout.serverEndPoint && (await layout.serverEndPoint())?.default;\n\t\t\t\tconst layoutResponse = await loadClientAndServerData(\n\t\t\t\t\tstackLayout.data,\n\t\t\t\t\tdocument.location.origin + layoutPath,\n\t\t\t\t\tquery,\n\t\t\t\t\tparams,\n\t\t\t\t\tlayoutEndPoint,\n\t\t\t\t\tlayoutServerEndPoint,\n\t\t\t\t);\n\t\t\t\tif (layoutResponse?.ok === false) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tObject.assign(data, stackLayout.data);\n\t\t\t\tnewLayoutStack.push(stackLayout);\n\t\t\t}\n\t\t}\n\t}\n\tlet endPointResponse = await loadClientAndServerData(\n\t\tdata,\n\t\tdocument.location.origin + path,\n\t\tquery,\n\t\tparams,\n\t\tclientEndPoint,\n\t\tserverEndPoint,\n\t);\n\tif (endPointResponse?.ok === false) {\n\t\treturn;\n\t}\n\treturn data;\n}\n\nasync function loadClientAndServerData(\n\tdata: Record<string, any>,\n\tlocation: string,\n\tquery: URLSearchParams,\n\tparams: Record<string, string>,\n\tclientEndPoint?: PageEndPoint,\n\tserverEndPoint?: PageServerEndPoint,\n): Promise<Response | undefined | void> {\n\tlet prefetchedData = client.prefetchedData;\n\n\tif (clientEndPoint?.load) {\n\t\tconst clientKey = location + (query.size > 0 ? `?${query}` : \"\");\n\t\tif (prefetchedData[clientKey]) {\n\t\t\tObject.assign(data, prefetchedData[clientKey]);\n\t\t} else {\n\t\t\tconst clientUrl = new URL(document.location.href);\n\t\t\tfor (let [name, value] of query) {\n\t\t\t\tclientUrl.searchParams.append(name, value);\n\t\t\t}\n\t\t\tconst clientParams = buildClientParams(clientUrl, params, data);\n\t\t\tconst clientResponse = await clientEndPoint.load(clientParams);\n\t\t\tif (clientResponse) {\n\t\t\t\tif (clientResponse.ok) {\n\t\t\t\t\tif (clientResponse.headers.get(\"Content-Type\")?.includes(\"application/json\")) {\n\t\t\t\t\t\tconst clientData = await clientResponse.json();\n\t\t\t\t\t\tObject.assign(data, clientData);\n\t\t\t\t\t\tprefetchedData[clientKey] = clientData;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\treturn clientResponse;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif (serverEndPoint?.load) {\n\t\tconst serverLocation = location.replace(/\\/$/, \"\") + \"/~server\";\n\t\tconst serverKey = serverLocation + (query.size > 0 ? `?${query}` : \"\");\n\t\tif (prefetchedData[serverKey]) {\n\t\t\tObject.assign(data, prefetchedData[serverKey]);\n\t\t} else {\n\t\t\tconst serverUrl = new URL(serverLocation);\n\t\t\tfor (let [name, value] of query) {\n\t\t\t\tserverUrl.searchParams.append(name, value);\n\t\t\t}\n\t\t\tconst serverResponse = await fetch(serverUrl);\n\t\t\tif (serverResponse) {\n\t\t\t\tif (serverResponse.ok) {\n\t\t\t\t\tif (serverResponse.headers.get(\"Content-Type\")?.includes(\"application/json\")) {\n\t\t\t\t\t\tconst serverData = await serverResponse.json();\n\t\t\t\t\t\tObject.assign(data, serverData);\n\t\t\t\t\t\tprefetchedData[serverKey] = serverData;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\treturn serverResponse;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction buildClientParams(url: URL, params: Record<string, string>, data: Record<string, any>) {\n\treturn {\n\t\turl,\n\t\tparams,\n\t\tdata,\n\t};\n}\n","import { clearLayoutSlot, fillLayoutSlot, hydrate } from \"@torpor/view\";\nimport { type Component, type SlotRender } from \"@torpor/view\";\nimport { mount, unmount } from \"@torpor/view\";\nimport $page from \"../state/$page\";\nimport client from \"../state/client\";\nimport type LayoutPath from \"../types/LayoutPath\";\nimport type PageEndPoint from \"../types/PageEndPoint\";\nimport type PageServerEndPoint from \"../types/PageServerEndPoint\";\nimport formSubmit from \"./formSubmit\";\nimport loadData from \"./loadData\";\n\n// @ts-ignore\nexport default async function navigate(url: URL, withHydration = false): Promise<boolean> {\n\tlet parent = document.getElementById(\"app\");\n\tif (!parent) {\n\t\t// TODO: 500\n\t\tconsole.log(\"500\");\n\t\treturn false;\n\t}\n\n\tconst path = url.pathname;\n\tconst query = url.searchParams;\n\n\t//console.log(`navigating to '${path}'${query.size ? ` with ${query}` : \"\"}`);\n\n\tconst route = client.router.match(path, query);\n\tif (!route) {\n\t\t// TODO: 404\n\t\tconsole.log(\"404\");\n\t\treturn false;\n\t}\n\n\t// Update $page before building the components\n\t$page.url = url;\n\tif (path.endsWith(\"/_error\")) {\n\t\t$page.status = parseInt(query.get(\"status\") ?? \"404\");\n\t\t$page.error = { message: query.get(\"message\") ?? \"\" };\n\t\t// Make it look a bit classier by removing the query\n\t\twindow.history.replaceState({}, \"\", url.toString().split(\"?\")[0]);\n\t} else {\n\t\t$page.status = 200;\n\t}\n\tconst handler = route.handler;\n\tconst params = route.params || {};\n\n\t// There must be a client endpoint with a component\n\tconst clientEndPoint: PageEndPoint | undefined = (await handler.endPoint()).default;\n\tif (!clientEndPoint?.component) {\n\t\t// TODO: 404\n\t\tconsole.log(\"404\");\n\t\treturn false;\n\t}\n\n\t// There may be a server endpoint\n\tconst serverEndPoint: PageServerEndPoint | undefined =\n\t\thandler.serverEndPoint && (await handler.serverEndPoint())?.default;\n\n\tlet newLayoutStack: LayoutPath[] = [];\n\n\t// Pass the data into $props\n\t// TODO: Don't load if this is the first time -- it should have been passed\n\t// to us, somehow...\n\tconst data = await loadData(\n\t\thandler,\n\t\tparams,\n\t\tpath,\n\t\tquery,\n\t\tnewLayoutStack,\n\t\tclientEndPoint,\n\t\tserverEndPoint,\n\t);\n\t// We may have form data in a hidden input -- not sure if this is the best\n\t// way to do it\n\tlet formInput = document.getElementById(\"t-form-data\") as HTMLInputElement;\n\tlet form: Record<string, string> | undefined;\n\tif (formInput) {\n\t\tform = JSON.parse(formInput.value);\n\t\t$page.form = form;\n\t\tformInput.remove();\n\t}\n\tlet $props: Record<string, any> = { data };\n\n\t// We may have form data in $page.form, either from the hidden input\n\t// (above), or added via onformsubmit (below)\n\t$props.form = $page.form;\n\n\t// Add some special context for submitting Forms client-side\n\tconst $context = {\n\t\tTorporBuildContext: { onformsubmit: formSubmit },\n\t};\n\n\tclient.layoutStack.push({ path: route.handler.path, data: {}, reuse: false, slotRegion: null });\n\tclient.layoutStack = newLayoutStack;\n\tlet layoutStack = client.layoutStack;\n\n\t// If there are layouts, work our way upwards, pushing each component into\n\t// the default slot of its parent\n\t// TODO: There's probably a nicer way to do this with reducers or something\n\tlet component = clientEndPoint.component as Component;\n\tlet slots: Record<string, SlotRender> | undefined = undefined;\n\tlet reused = false;\n\t// The index of the innermost reused layout. Its slotRegion is the region\n\t// that must be cleared and refilled (it contains the next layout's or the\n\t// page's content). This is usually the last entry of the stack, but not\n\t// when an outer layout is reused while an inner one is new — e.g.\n\t// navigating between sections that share the root layout.\n\tlet reusedIndex = -1;\n\tif (handler.layouts) {\n\t\tlet slotFunctions: SlotRender[] = [];\n\t\t// The last slot function will render the client component\n\t\tslotFunctions[handler.layouts.length] = function clientComponent(parent, anchor) {\n\t\t\tlet i = layoutStack.length - 1;\n\t\t\tlayoutStack[i].slotRegion = fillLayoutSlot(\n\t\t\t\tclientEndPoint.component!,\n\t\t\t\tslotFunctions[i + 1],\n\t\t\t\tparent,\n\t\t\t\tanchor,\n\t\t\t\t$props,\n\t\t\t\t$context,\n\t\t\t);\n\t\t};\n\n\t\t// Each earlier slot function will render a layout\n\t\tfor (let i = handler.layouts.length - 1; i >= 0; i--) {\n\t\t\tconst layoutEndPoint: PageEndPoint | undefined = (await handler.layouts[i].endPoint())\n\t\t\t\t?.default;\n\t\t\tif (layoutEndPoint?.component) {\n\t\t\t\tif (layoutStack[i].reuse) {\n\t\t\t\t\t// Reuse this layout — clear and refill its slot (done in\n\t\t\t\t\t// the try block below so a failure doesn't leave the slot\n\t\t\t\t\t// half-cleared)\n\t\t\t\t\tcomponent = slotFunctions[i + 1] as Component;\n\t\t\t\t\treusedIndex = i;\n\t\t\t\t\treused = true;\n\t\t\t\t\tbreak;\n\t\t\t\t} else if (i === 0) {\n\t\t\t\t\tcomponent = layoutEndPoint.component as Component;\n\t\t\t\t\tslots = { _: slotFunctions[i + 1] };\n\t\t\t\t} else {\n\t\t\t\t\tslotFunctions[i] = function layoutComponent(parent, anchor, _, $context) {\n\t\t\t\t\t\tlayoutStack[i - 1].slotRegion = fillLayoutSlot(\n\t\t\t\t\t\t\tlayoutEndPoint.component!,\n\t\t\t\t\t\t\tslotFunctions[i + 1],\n\t\t\t\t\t\t\tparent,\n\t\t\t\t\t\t\tanchor,\n\t\t\t\t\t\t\t$props,\n\t\t\t\t\t\t\t$context,\n\t\t\t\t\t\t);\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\ttry {\n\t\tif (reused) {\n\t\t\t// The layout is being reused — clear the old slot content, then\n\t\t\t// call the slot function directly to fill it with the new page.\n\t\t\t// We must not go through `mount` here: the slot's container still\n\t\t\t// holds the layout's own children (e.g. a header), which `mount`\n\t\t\t// refuses to mount into. Both the clear and the fill are inside\n\t\t\t// the try so that a failure doesn't leave the slot half-cleared.\n\t\t\tconst slotRegion = layoutStack[reusedIndex].slotRegion;\n\t\t\tparent = slotRegion.startNode!.parentNode as HTMLElement;\n\t\t\tclearLayoutSlot(slotRegion);\n\t\t\tcomponent(parent, null);\n\t\t} else if (withHydration) {\n\t\t\thydrate(parent, component, $props, slots);\n\t\t} else {\n\t\t\t// The layout chain changed (or there was no previous layout to\n\t\t\t// reuse): tear down the previous UI entirely — disposing its\n\t\t\t// region tree and clearing `#app` — so `mount` starts fresh.\n\t\t\t// Without this, `mount` throws because `#app` still holds the\n\t\t\t// previous render's children, and it would reuse a stale root\n\t\t\t// region.\n\t\t\tunmount(parent);\n\t\t\tmount(parent, component, $props, slots);\n\t\t}\n\t} catch (error) {\n\t\t// TODO: Show a proper Error component\n\t\tparent.innerHTML = '<span style=\"color: red\">Script syntax error</span><p>' + error + \"</p>\";\n\t\tconsole.log(error);\n\t}\n\n\t// Reset prefetched data on each navigation\n\tclient.prefetchedData = {};\n\n\treturn true;\n}\n","import navigate from \"./navigate\";\n\n/**\n * Loads and renders the page at the supplied path.\n */\nexport default async function load(path: string): Promise<boolean> {\n\tconst url = new URL(path, document.location.href);\n\treturn await navigate(url);\n}\n"],"mappings":"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"}
package/dist/nav.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { i as reload, t as load } from "./load-BntzvmwL.mjs";
1
+ import { i as reload, t as load } from "./load-C9RpS_4j.mjs";
2
2
  //#region src/nav/route.ts
3
3
  /**
4
4
  * Builds a route path in a type-safe manner, filling in dynamic segments, e.g.
@@ -59,10 +59,12 @@ async function request(method, url, body, init) {
59
59
  method: (method === "del" ? "delete" : method).toUpperCase(),
60
60
  ...init
61
61
  };
62
- if (body !== void 0 && options.body === void 0 && method !== "get" && method !== "head") if (typeof body === "object") {
63
- options.headers ??= { "Content-Type": "application/json" };
64
- options.body = JSON.stringify(body);
65
- } else options.body = body;
62
+ if (body !== void 0 && options.body === void 0 && method !== "get" && method !== "head") {
63
+ if (typeof body === "object") {
64
+ options.headers ??= { "Content-Type": "application/json" };
65
+ options.body = JSON.stringify(body);
66
+ } else options.body = body;
67
+ }
66
68
  const response = await fetch(url, options);
67
69
  if (!response.ok) throw new Error(`API ${method.toUpperCase()} ${url} failed: ${response.status}`);
68
70
  return (response.headers.get("Content-Type") ?? "").includes("application/json") ? await response.json() : response;
package/dist/nav.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"nav.mjs","names":[],"sources":["../src/nav/route.ts","../src/nav/api.ts"],"sourcesContent":["import type { ExactRouteParams, ParseRouteParams } from \"../types/ParseRouteParams\";\n\n/**\n * Builds a route path in a type-safe manner, filling in dynamic segments, e.g.\n * `route(\"/posts/[id]\", { id: 5 })` returns `/posts/5`. The params object is\n * checked for exact keys: missing or unknown params error at compile time.\n *\n * NOTE: The rest args conditional must stay INLINE — routing it through a type\n * alias defeats `Params` inference, and excess keys stop being checked\n * @param path The route path\n * @param params The route params, required if the path has dynamic segments\n * @returns The path with the params filled in, URI-encoded\n */\nexport default function route<\n\tRoute extends string,\n\tParams extends ParseRouteParams<Route> = ParseRouteParams<Route>,\n>(\n\tpath: Route,\n\t...args: string extends Route\n\t\t? [params?: Record<string, string>]\n\t\t: keyof ParseRouteParams<Route> extends never\n\t\t\t? []\n\t\t\t: [params: Params & ExactRouteParams<Params, ParseRouteParams<Route>>]\n): string {\n\tconst params = ((args as unknown[])[0] ?? {}) as Record<string, string>;\n\treturn path.replace(/\\[(\\.\\.\\.)?([^\\]]+)\\]/g, (_, splat: string | undefined, name: string) => {\n\t\tconst value = params[name];\n\t\tif (value === undefined) {\n\t\t\tthrow new Error(`Missing param '${name}' for route '${path}'`);\n\t\t}\n\t\treturn splat\n\t\t\t? value\n\t\t\t\t\t.split(\"/\")\n\t\t\t\t\t.map((segment) => encodeURIComponent(segment))\n\t\t\t\t\t.join(\"/\")\n\t\t\t: encodeURIComponent(String(value));\n\t});\n}\n","import type { ExactRouteParams, ParseRouteParams } from \"../types/ParseRouteParams\";\nimport type TypedResponse from \"../response/TypedResponse\";\nimport route from \"./route\";\n\n/**\n * The value a client call resolves to: the endpoint's typed JSON body when it\n * returns a typed response, or the raw Response when the body isn't JSON.\n * Untyped endpoints resolve to `unknown`.\n */\ntype ApiResult<Handler> = Handler extends (event: any) => any\n\t? Awaited<ReturnType<Handler>> extends TypedResponse<infer Body>\n\t\t? Body | Response\n\t\t: unknown\n\t: never;\n\n/**\n * The body a client call accepts: the endpoint event's annotated JSON body\n * type (from `ServerLoadEvent<Route, Body>`), or a loose value when the\n * handler doesn't annotate one.\n */\ntype ApiRequestBody<Handler> = Handler extends (event: infer Event) => any\n\t? Event extends { json: () => Promise<infer Body> }\n\t\t? unknown extends Body\n\t\t\t? BodyInit | object\n\t\t\t: Body\n\t\t: BodyInit | object\n\t: BodyInit | object;\n\n/** The callable methods of an endpoint, one per handler it defines. */\nexport type ApiMethods<Endpoint extends object> = {\n\t[Method in string & keyof Endpoint as NonNullable<Endpoint[Method]> extends (event: any) => any\n\t\t? Method\n\t\t: never]: (\n\t\tbody?: ApiRequestBody<NonNullable<Endpoint[Method]>>,\n\t\tinit?: RequestInit,\n\t) => Promise<ApiResult<NonNullable<Endpoint[Method]>>>;\n};\n\nconst METHODS = [\"get\", \"post\", \"patch\", \"put\", \"del\", \"options\", \"head\"] as const;\n\n/**\n * Creates typed client callers for a `+server` endpoint, e.g.\n *\n * ```ts\n * import type endpoint from \"@/routes/api/posts/[id]/+server\";\n * const post = makeApi<\"/api/posts/[id]\", typeof endpoint>(\"/api/posts/[id]\");\n * const data = await post.get(); // typed from the endpoint's responses\n * ```\n *\n * Method names match the endpoint's handlers (`del` for DELETE). Params are\n * filled in and URI-encoded from the route path, and JSON responses are\n * parsed; non-JSON responses resolve to the raw Response. Failed requests\n * (non-2xx) throw.\n *\n * @param path The endpoint's route path\n * @param params The route params, required if the path has dynamic segments\n * @returns An object with one caller per handler defined by the endpoint\n */\nexport default function makeApi<\n\tRoute extends string,\n\tEndpoint extends object,\n\tParams extends ParseRouteParams<Route> = ParseRouteParams<Route>,\n>(\n\tpath: Route,\n\t// NOTE: The rest args conditional must stay INLINE — routing it through a\n\t// type alias defeats `Params` inference, and excess keys stop being checked\n\t...args: string extends Route\n\t\t? [params?: Record<string, string>]\n\t\t: keyof ParseRouteParams<Route> extends never\n\t\t\t? []\n\t\t\t: [params: Params & ExactRouteParams<Params, ParseRouteParams<Route>>]\n): ApiMethods<Endpoint> {\n\tconst url = route(path as string, ...(args as [params?: Record<string, string>]));\n\tconst api: Record<string, (body?: unknown, init?: RequestInit) => Promise<unknown>> = {};\n\tfor (const method of METHODS) {\n\t\tapi[method] = (body, init) => request(method, url, body, init);\n\t}\n\treturn api as ApiMethods<Endpoint>;\n}\n\nasync function request(\n\tmethod: string,\n\turl: string,\n\tbody: unknown,\n\tinit?: RequestInit,\n): Promise<unknown> {\n\t// `del` is the endpoint handler name for DELETE requests\n\tconst httpMethod = method === \"del\" ? \"delete\" : method;\n\tconst options: RequestInit = { method: httpMethod.toUpperCase(), ...init };\n\tif (body !== undefined && options.body === undefined && method !== \"get\" && method !== \"head\") {\n\t\tif (typeof body === \"object\") {\n\t\t\toptions.headers ??= { \"Content-Type\": \"application/json\" };\n\t\t\toptions.body = JSON.stringify(body);\n\t\t} else {\n\t\t\toptions.body = body as BodyInit;\n\t\t}\n\t}\n\tconst response = await fetch(url, options);\n\tif (!response.ok) {\n\t\tthrow new Error(`API ${method.toUpperCase()} ${url} failed: ${response.status}`);\n\t}\n\tconst contentType = response.headers.get(\"Content-Type\") ?? \"\";\n\treturn contentType.includes(\"application/json\") ? await response.json() : response;\n}\n"],"mappings":";;;;;;;;;;;;;AAaA,SAAwB,MAIvB,MACA,GAAG,MAKM;CACT,MAAM,SAAW,KAAmB,MAAM,CAAC;CAC3C,OAAO,KAAK,QAAQ,2BAA2B,GAAG,OAA2B,SAAiB;EAC7F,MAAM,QAAQ,OAAO;EACrB,IAAI,UAAU,KAAA,GACb,MAAM,IAAI,MAAM,kBAAkB,KAAK,eAAe,KAAK,EAAE;EAE9D,OAAO,QACJ,MACC,MAAM,GAAG,CAAC,CACV,KAAK,YAAY,mBAAmB,OAAO,CAAC,CAAC,CAC7C,KAAK,GAAG,IACT,mBAAmB,OAAO,KAAK,CAAC;CACpC,CAAC;AACF;;;ACCA,MAAM,UAAU;CAAC;CAAO;CAAQ;CAAS;CAAO;CAAO;CAAW;AAAM;;;;;;;;;;;;;;;;;;;AAoBxE,SAAwB,QAKvB,MAGA,GAAG,MAKoB;CACvB,MAAM,MAAM,MAAM,MAAgB,GAAI,IAA0C;CAChF,MAAM,MAAgF,CAAC;CACvF,KAAK,MAAM,UAAU,SACpB,IAAI,WAAW,MAAM,SAAS,QAAQ,QAAQ,KAAK,MAAM,IAAI;CAE9D,OAAO;AACR;AAEA,eAAe,QACd,QACA,KACA,MACA,MACmB;CAGnB,MAAM,UAAuB;EAAE,SADZ,WAAW,QAAQ,WAAW,OAAA,CACC,YAAY;EAAG,GAAG;CAAK;CACzE,IAAI,SAAS,KAAA,KAAa,QAAQ,SAAS,KAAA,KAAa,WAAW,SAAS,WAAW,QACtF,IAAI,OAAO,SAAS,UAAU;EAC7B,QAAQ,YAAY,EAAE,gBAAgB,mBAAmB;EACzD,QAAQ,OAAO,KAAK,UAAU,IAAI;CACnC,OACC,QAAQ,OAAO;CAGjB,MAAM,WAAW,MAAM,MAAM,KAAK,OAAO;CACzC,IAAI,CAAC,SAAS,IACb,MAAM,IAAI,MAAM,OAAO,OAAO,YAAY,EAAE,GAAG,IAAI,WAAW,SAAS,QAAQ;CAGhF,QADoB,SAAS,QAAQ,IAAI,cAAc,KAAK,GAAA,CACzC,SAAS,kBAAkB,IAAI,MAAM,SAAS,KAAK,IAAI;AAC3E"}
1
+ {"version":3,"file":"nav.mjs","names":[],"sources":["../src/nav/route.ts","../src/nav/api.ts"],"sourcesContent":["import type { ExactRouteParams, ParseRouteParams } from \"../types/ParseRouteParams\";\n\n/**\n * Builds a route path in a type-safe manner, filling in dynamic segments, e.g.\n * `route(\"/posts/[id]\", { id: 5 })` returns `/posts/5`. The params object is\n * checked for exact keys: missing or unknown params error at compile time.\n *\n * NOTE: The rest args conditional must stay INLINE — routing it through a type\n * alias defeats `Params` inference, and excess keys stop being checked\n * @param path The route path\n * @param params The route params, required if the path has dynamic segments\n * @returns The path with the params filled in, URI-encoded\n */\nexport default function route<\n\tRoute extends string,\n\tParams extends ParseRouteParams<Route> = ParseRouteParams<Route>,\n>(\n\tpath: Route,\n\t...args: string extends Route\n\t\t? [params?: Record<string, string>]\n\t\t: keyof ParseRouteParams<Route> extends never\n\t\t\t? []\n\t\t\t: [params: Params & ExactRouteParams<Params, ParseRouteParams<Route>>]\n): string {\n\tconst params = ((args as unknown[])[0] ?? {}) as Record<string, string>;\n\treturn path.replace(/\\[(\\.\\.\\.)?([^\\]]+)\\]/g, (_, splat: string | undefined, name: string) => {\n\t\tconst value = params[name];\n\t\tif (value === undefined) {\n\t\t\tthrow new Error(`Missing param '${name}' for route '${path}'`);\n\t\t}\n\t\treturn splat\n\t\t\t? value\n\t\t\t\t\t.split(\"/\")\n\t\t\t\t\t.map((segment) => encodeURIComponent(segment))\n\t\t\t\t\t.join(\"/\")\n\t\t\t: encodeURIComponent(String(value));\n\t});\n}\n","import type { ExactRouteParams, ParseRouteParams } from \"../types/ParseRouteParams\";\nimport type TypedResponse from \"../response/TypedResponse\";\nimport route from \"./route\";\n\n/**\n * The value a client call resolves to: the endpoint's typed JSON body when it\n * returns a typed response, or the raw Response when the body isn't JSON.\n * Untyped endpoints resolve to `unknown`.\n */\ntype ApiResult<Handler> = Handler extends (event: any) => any\n\t? Awaited<ReturnType<Handler>> extends TypedResponse<infer Body>\n\t\t? Body | Response\n\t\t: unknown\n\t: never;\n\n/**\n * The body a client call accepts: the endpoint event's annotated JSON body\n * type (from `ServerLoadEvent<Route, Body>`), or a loose value when the\n * handler doesn't annotate one.\n */\ntype ApiRequestBody<Handler> = Handler extends (event: infer Event) => any\n\t? Event extends { json: () => Promise<infer Body> }\n\t\t? unknown extends Body\n\t\t\t? BodyInit | object\n\t\t\t: Body\n\t\t: BodyInit | object\n\t: BodyInit | object;\n\n/** The callable methods of an endpoint, one per handler it defines. */\nexport type ApiMethods<Endpoint extends object> = {\n\t[Method in string & keyof Endpoint as NonNullable<Endpoint[Method]> extends (event: any) => any\n\t\t? Method\n\t\t: never]: (\n\t\tbody?: ApiRequestBody<NonNullable<Endpoint[Method]>>,\n\t\tinit?: RequestInit,\n\t) => Promise<ApiResult<NonNullable<Endpoint[Method]>>>;\n};\n\nconst METHODS = [\"get\", \"post\", \"patch\", \"put\", \"del\", \"options\", \"head\"] as const;\n\n/**\n * Creates typed client callers for a `+server` endpoint, e.g.\n *\n * ```ts\n * import type endpoint from \"@/routes/api/posts/[id]/+server\";\n * const post = makeApi<\"/api/posts/[id]\", typeof endpoint>(\"/api/posts/[id]\");\n * const data = await post.get(); // typed from the endpoint's responses\n * ```\n *\n * Method names match the endpoint's handlers (`del` for DELETE). Params are\n * filled in and URI-encoded from the route path, and JSON responses are\n * parsed; non-JSON responses resolve to the raw Response. Failed requests\n * (non-2xx) throw.\n *\n * @param path The endpoint's route path\n * @param params The route params, required if the path has dynamic segments\n * @returns An object with one caller per handler defined by the endpoint\n */\nexport default function makeApi<\n\tRoute extends string,\n\tEndpoint extends object,\n\tParams extends ParseRouteParams<Route> = ParseRouteParams<Route>,\n>(\n\tpath: Route,\n\t// NOTE: The rest args conditional must stay INLINE — routing it through a\n\t// type alias defeats `Params` inference, and excess keys stop being checked\n\t...args: string extends Route\n\t\t? [params?: Record<string, string>]\n\t\t: keyof ParseRouteParams<Route> extends never\n\t\t\t? []\n\t\t\t: [params: Params & ExactRouteParams<Params, ParseRouteParams<Route>>]\n): ApiMethods<Endpoint> {\n\tconst url = route(path as string, ...(args as [params?: Record<string, string>]));\n\tconst api: Record<string, (body?: unknown, init?: RequestInit) => Promise<unknown>> = {};\n\tfor (const method of METHODS) {\n\t\tapi[method] = (body, init) => request(method, url, body, init);\n\t}\n\treturn api as ApiMethods<Endpoint>;\n}\n\nasync function request(\n\tmethod: string,\n\turl: string,\n\tbody: unknown,\n\tinit?: RequestInit,\n): Promise<unknown> {\n\t// `del` is the endpoint handler name for DELETE requests\n\tconst httpMethod = method === \"del\" ? \"delete\" : method;\n\tconst options: RequestInit = { method: httpMethod.toUpperCase(), ...init };\n\tif (body !== undefined && options.body === undefined && method !== \"get\" && method !== \"head\") {\n\t\tif (typeof body === \"object\") {\n\t\t\toptions.headers ??= { \"Content-Type\": \"application/json\" };\n\t\t\toptions.body = JSON.stringify(body);\n\t\t} else {\n\t\t\toptions.body = body as BodyInit;\n\t\t}\n\t}\n\tconst response = await fetch(url, options);\n\tif (!response.ok) {\n\t\tthrow new Error(`API ${method.toUpperCase()} ${url} failed: ${response.status}`);\n\t}\n\tconst contentType = response.headers.get(\"Content-Type\") ?? \"\";\n\treturn contentType.includes(\"application/json\") ? await response.json() : response;\n}\n"],"mappings":";;;;;;;;;;;;;AAaA,SAAwB,MAIvB,MACA,GAAG,MAKM;CACT,MAAM,SAAW,KAAmB,MAAM,CAAC;CAC3C,OAAO,KAAK,QAAQ,2BAA2B,GAAG,OAA2B,SAAiB;EAC7F,MAAM,QAAQ,OAAO;EACrB,IAAI,UAAU,KAAA,GACb,MAAM,IAAI,MAAM,kBAAkB,KAAK,eAAe,KAAK,EAAE;EAE9D,OAAO,QACJ,MACC,MAAM,GAAG,CAAC,CACV,KAAK,YAAY,mBAAmB,OAAO,CAAC,CAAC,CAC7C,KAAK,GAAG,IACT,mBAAmB,OAAO,KAAK,CAAC;CACpC,CAAC;AACF;;;ACCA,MAAM,UAAU;CAAC;CAAO;CAAQ;CAAS;CAAO;CAAO;CAAW;AAAM;;;;;;;;;;;;;;;;;;;AAoBxE,SAAwB,QAKvB,MAGA,GAAG,MAKoB;CACvB,MAAM,MAAM,MAAM,MAAgB,GAAI,IAA0C;CAChF,MAAM,MAAgF,CAAC;CACvF,KAAK,MAAM,UAAU,SACpB,IAAI,WAAW,MAAM,SAAS,QAAQ,QAAQ,KAAK,MAAM,IAAI;CAE9D,OAAO;AACR;AAEA,eAAe,QACd,QACA,KACA,MACA,MACmB;CAGnB,MAAM,UAAuB;EAAE,SADZ,WAAW,QAAQ,WAAW,OAAA,CACC,YAAY;EAAG,GAAG;CAAK;CACzE,IAAI,SAAS,KAAA,KAAa,QAAQ,SAAS,KAAA,KAAa,WAAW,SAAS,WAAW,QAAQ;EAC9F,IAAI,OAAO,SAAS,UAAU;GAC7B,QAAQ,YAAY,EAAE,gBAAgB,mBAAmB;GACzD,QAAQ,OAAO,KAAK,UAAU,IAAI;EACnC,OACC,QAAQ,OAAO;CAEjB;CACA,MAAM,WAAW,MAAM,MAAM,KAAK,OAAO;CACzC,IAAI,CAAC,SAAS,IACb,MAAM,IAAI,MAAM,OAAO,OAAO,YAAY,EAAE,GAAG,IAAI,WAAW,SAAS,QAAQ;CAGhF,QADoB,SAAS,QAAQ,IAAI,cAAc,KAAK,GAAA,CACzC,SAAS,kBAAkB,IAAI,MAAM,SAAS,KAAK,IAAI;AAC3E"}
@@ -1,4 +1,4 @@
1
- import { v as SitePlugin } from "./Site-CE0_o5Ci.mjs";
1
+ import { v as SitePlugin } from "./Site-CTADVh7d.mjs";
2
2
  import { t as StandardSchemaV1 } from "./StandardSchema-D28L8_zZ.mjs";
3
3
  //#region src/openapi/types.d.ts
4
4
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"pathTrie-D4Ax2Mu9.mjs","names":["#root","#matchNode"],"sources":["../src/utils/pathTrie.ts"],"sourcesContent":["/**\n * A simple trie router for path patterns.\n *\n * Pattern segments:\n * - `posts` — static segment\n * - `[id]` — matches one non-empty path segment, captured as `params.id`\n * - `[...path]` — matches the rest of the path (at least one segment),\n * captured as `params.path`\n * - `*` — matches the rest of the path (possibly nothing), not captured\n * - `*.css` — a segment with an embedded `*` matches one path segment\n *\n * `*` and `[...]` must be the last segment of a pattern. Matching priority at\n * each level is static, then param, then glob, then rest. Trailing slashes are\n * ignored, so `/posts` and `/posts/` match the same route.\n */\nexport default class PathTrie<T> {\n\t#root = new TrieNode<T>();\n\n\t/**\n\t * Registers a value at a path pattern. The first registration of an exact\n\t * pattern wins, matching the previous regex-scanning router.\n\t */\n\tinsert(pattern: string, value: T): void {\n\t\tlet node = this.#root;\n\t\tconst segments = splitPath(pattern);\n\t\tfor (let i = 0; i < segments.length; i++) {\n\t\t\tconst segment = segments[i];\n\t\t\tif (segment === \"*\") {\n\t\t\t\tif (i !== segments.length - 1) {\n\t\t\t\t\tthrow new Error(`The '*' wildcard must be the last segment of a path: ${pattern}`);\n\t\t\t\t}\n\t\t\t\tnode.rest ??= { name: undefined, allowEmpty: true, child: new TrieNode() };\n\t\t\t\tnode = node.rest.child;\n\t\t\t} else if (segment.startsWith(\"[...\") && segment.endsWith(\"]\")) {\n\t\t\t\tif (i !== segments.length - 1) {\n\t\t\t\t\tthrow new Error(`The '[...x]' splat must be the last segment of a path: ${pattern}`);\n\t\t\t\t}\n\t\t\t\tnode.rest ??= {\n\t\t\t\t\tname: segment.slice(4, -1),\n\t\t\t\t\tallowEmpty: false,\n\t\t\t\t\tchild: new TrieNode(),\n\t\t\t\t};\n\t\t\t\tnode = node.rest.child;\n\t\t\t} else if (segment.startsWith(\"[\") && segment.endsWith(\"]\")) {\n\t\t\t\tnode.param ??= { name: segment.slice(1, -1), child: new TrieNode() };\n\t\t\t\tnode = node.param.child;\n\t\t\t} else if (segment.includes(\"*\")) {\n\t\t\t\tnode.glob ??= { regex: globToRegex(segment), child: new TrieNode() };\n\t\t\t\tnode = node.glob.child;\n\t\t\t} else {\n\t\t\t\tnode.static ??= new Map();\n\t\t\t\tlet child = node.static.get(segment);\n\t\t\t\tif (!child) {\n\t\t\t\t\tchild = new TrieNode();\n\t\t\t\t\tnode.static.set(segment, child);\n\t\t\t\t}\n\t\t\t\tnode = child;\n\t\t\t}\n\t\t}\n\t\tnode.value ??= value;\n\t}\n\n\t/**\n\t * Matches a request path against the trie. Returns the registered value\n\t * and any captured params, or undefined when nothing matches.\n\t */\n\tmatch(path: string): PathMatch<T> | undefined {\n\t\t// Trailing slashes are optional: `/posts/` matches `/posts`. Whether\n\t\t// there WAS a trailing separator still matters for a trailing bare\n\t\t// `*`, which requires one (e.g. `/assets/*` matches `/assets/` but\n\t\t// not `/assets`)\n\t\tlet trailingSlash = path === \"/\";\n\t\twhile (path.length > 1 && path.endsWith(\"/\")) {\n\t\t\tpath = path.slice(0, -1);\n\t\t\ttrailingSlash = true;\n\t\t}\n\t\tconst segments = path === \"/\" ? [] : splitPath(path);\n\t\treturn this.#matchNode(this.#root, segments, 0, {}, trailingSlash);\n\t}\n\n\t#matchNode(\n\t\tnode: TrieNode<T>,\n\t\tsegments: string[],\n\t\ti: number,\n\t\tparams: Record<string, string>,\n\t\ttrailingSlash: boolean,\n\t): PathMatch<T> | undefined {\n\t\tif (i === segments.length) {\n\t\t\tif (node.value !== undefined) {\n\t\t\t\treturn { value: node.value, params: orUndefined(params) };\n\t\t\t}\n\t\t\t// A trailing bare `*` also matches nothing, when the path ended\n\t\t\t// with its separator (e.g. `/assets/*` matches `/assets/`)\n\t\t\tconst rest = node.rest;\n\t\t\tif (rest?.allowEmpty && trailingSlash && rest.child.value !== undefined) {\n\t\t\t\treturn { value: rest.child.value, params: orUndefined(params) };\n\t\t\t}\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst segment = segments[i];\n\n\t\t// Static segments win over params\n\t\tconst stat = node.static?.get(segment);\n\t\tif (stat) {\n\t\t\tconst match = this.#matchNode(stat, segments, i + 1, params, trailingSlash);\n\t\t\tif (match) return match;\n\t\t}\n\n\t\tif (node.param && segment !== \"\") {\n\t\t\tconst match = this.#matchNode(node.param.child, segments, i + 1, {\n\t\t\t\t...params,\n\t\t\t\t[node.param.name]: segment,\n\t\t\t}, trailingSlash);\n\t\t\tif (match) return match;\n\t\t}\n\n\t\tif (node.glob?.regex.test(segment)) {\n\t\t\tconst glob = node.glob;\n\t\t\tconst match = this.#matchNode(glob.child, segments, i + 1, params, trailingSlash);\n\t\t\tif (match) return match;\n\t\t}\n\n\t\t// The rest matcher consumes all remaining segments\n\t\tconst rest = node.rest;\n\t\tif (rest?.child.value !== undefined) {\n\t\t\t// Only build the captured value (and a new params object) when the\n\t\t\t// pattern actually names it — a bare `*` catch-all shouldn't pay\n\t\t\t// for a slice+join on every request\n\t\t\tif (rest.name !== undefined) {\n\t\t\t\treturn {\n\t\t\t\t\tvalue: rest.child.value,\n\t\t\t\t\tparams: orUndefined({ ...params, [rest.name]: segments.slice(i).join(\"/\") }),\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn { value: rest.child.value, params: orUndefined(params) };\n\t\t}\n\n\t\treturn undefined;\n\t}\n}\n\nexport interface PathMatch<T> {\n\tvalue: T;\n\tparams: Record<string, string> | undefined;\n}\n\nclass TrieNode<T> {\n\tvalue: T | undefined = undefined;\n\tstatic: Map<string, TrieNode<T>> | undefined;\n\tparam: { name: string; child: TrieNode<T> } | undefined;\n\tglob: { regex: RegExp; child: TrieNode<T> } | undefined;\n\trest: { name: string | undefined; allowEmpty: boolean; child: TrieNode<T> } | undefined;\n}\n\n/**\n * Splits a path or pattern into segments, dropping the leading empty segment\n * and any trailing empty (from a trailing slash). `/posts/5` becomes\n * `[\"posts\", \"5\"]` and `/` becomes `[]`.\n */\nfunction splitPath(path: string): string[] {\n\tconst segments = path.split(\"/\");\n\tif (segments[0] === \"\") segments.shift();\n\tif (segments.length && segments[segments.length - 1] === \"\") segments.pop();\n\treturn segments;\n}\n\n/**\n * Compiles a pattern segment with an embedded `*` (e.g. `*.css`) into a\n * regex that matches a single path segment.\n */\nfunction globToRegex(segment: string): RegExp {\n\tconst source = segment\n\t\t.split(\"*\")\n\t\t.map((part) => part.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"))\n\t\t.join(\".*\");\n\treturn new RegExp(`^${source}$`);\n}\n\nfunction orUndefined(params: Record<string, string>): Record<string, string> | undefined {\n\treturn Object.keys(params).length ? params : undefined;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"pathTrie-D4Ax2Mu9.mjs","names":["#root","#matchNode"],"sources":["../src/utils/pathTrie.ts"],"sourcesContent":["/**\n * A simple trie router for path patterns.\n *\n * Pattern segments:\n * - `posts` — static segment\n * - `[id]` — matches one non-empty path segment, captured as `params.id`\n * - `[...path]` — matches the rest of the path (at least one segment),\n * captured as `params.path`\n * - `*` — matches the rest of the path (possibly nothing), not captured\n * - `*.css` — a segment with an embedded `*` matches one path segment\n *\n * `*` and `[...]` must be the last segment of a pattern. Matching priority at\n * each level is static, then param, then glob, then rest. Trailing slashes are\n * ignored, so `/posts` and `/posts/` match the same route.\n */\nexport default class PathTrie<T> {\n\t#root = new TrieNode<T>();\n\n\t/**\n\t * Registers a value at a path pattern. The first registration of an exact\n\t * pattern wins, matching the previous regex-scanning router.\n\t */\n\tinsert(pattern: string, value: T): void {\n\t\tlet node = this.#root;\n\t\tconst segments = splitPath(pattern);\n\t\tfor (let i = 0; i < segments.length; i++) {\n\t\t\tconst segment = segments[i];\n\t\t\tif (segment === \"*\") {\n\t\t\t\tif (i !== segments.length - 1) {\n\t\t\t\t\tthrow new Error(`The '*' wildcard must be the last segment of a path: ${pattern}`);\n\t\t\t\t}\n\t\t\t\tnode.rest ??= { name: undefined, allowEmpty: true, child: new TrieNode() };\n\t\t\t\tnode = node.rest.child;\n\t\t\t} else if (segment.startsWith(\"[...\") && segment.endsWith(\"]\")) {\n\t\t\t\tif (i !== segments.length - 1) {\n\t\t\t\t\tthrow new Error(`The '[...x]' splat must be the last segment of a path: ${pattern}`);\n\t\t\t\t}\n\t\t\t\tnode.rest ??= {\n\t\t\t\t\tname: segment.slice(4, -1),\n\t\t\t\t\tallowEmpty: false,\n\t\t\t\t\tchild: new TrieNode(),\n\t\t\t\t};\n\t\t\t\tnode = node.rest.child;\n\t\t\t} else if (segment.startsWith(\"[\") && segment.endsWith(\"]\")) {\n\t\t\t\tnode.param ??= { name: segment.slice(1, -1), child: new TrieNode() };\n\t\t\t\tnode = node.param.child;\n\t\t\t} else if (segment.includes(\"*\")) {\n\t\t\t\tnode.glob ??= { regex: globToRegex(segment), child: new TrieNode() };\n\t\t\t\tnode = node.glob.child;\n\t\t\t} else {\n\t\t\t\tnode.static ??= new Map();\n\t\t\t\tlet child = node.static.get(segment);\n\t\t\t\tif (!child) {\n\t\t\t\t\tchild = new TrieNode();\n\t\t\t\t\tnode.static.set(segment, child);\n\t\t\t\t}\n\t\t\t\tnode = child;\n\t\t\t}\n\t\t}\n\t\tnode.value ??= value;\n\t}\n\n\t/**\n\t * Matches a request path against the trie. Returns the registered value\n\t * and any captured params, or undefined when nothing matches.\n\t */\n\tmatch(path: string): PathMatch<T> | undefined {\n\t\t// Trailing slashes are optional: `/posts/` matches `/posts`. Whether\n\t\t// there WAS a trailing separator still matters for a trailing bare\n\t\t// `*`, which requires one (e.g. `/assets/*` matches `/assets/` but\n\t\t// not `/assets`)\n\t\tlet trailingSlash = path === \"/\";\n\t\twhile (path.length > 1 && path.endsWith(\"/\")) {\n\t\t\tpath = path.slice(0, -1);\n\t\t\ttrailingSlash = true;\n\t\t}\n\t\tconst segments = path === \"/\" ? [] : splitPath(path);\n\t\treturn this.#matchNode(this.#root, segments, 0, {}, trailingSlash);\n\t}\n\n\t#matchNode(\n\t\tnode: TrieNode<T>,\n\t\tsegments: string[],\n\t\ti: number,\n\t\tparams: Record<string, string>,\n\t\ttrailingSlash: boolean,\n\t): PathMatch<T> | undefined {\n\t\tif (i === segments.length) {\n\t\t\tif (node.value !== undefined) {\n\t\t\t\treturn { value: node.value, params: orUndefined(params) };\n\t\t\t}\n\t\t\t// A trailing bare `*` also matches nothing, when the path ended\n\t\t\t// with its separator (e.g. `/assets/*` matches `/assets/`)\n\t\t\tconst rest = node.rest;\n\t\t\tif (rest?.allowEmpty && trailingSlash && rest.child.value !== undefined) {\n\t\t\t\treturn { value: rest.child.value, params: orUndefined(params) };\n\t\t\t}\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst segment = segments[i];\n\n\t\t// Static segments win over params\n\t\tconst stat = node.static?.get(segment);\n\t\tif (stat) {\n\t\t\tconst match = this.#matchNode(stat, segments, i + 1, params, trailingSlash);\n\t\t\tif (match) return match;\n\t\t}\n\n\t\tif (node.param && segment !== \"\") {\n\t\t\tconst match = this.#matchNode(\n\t\t\t\tnode.param.child,\n\t\t\t\tsegments,\n\t\t\t\ti + 1,\n\t\t\t\t{\n\t\t\t\t\t...params,\n\t\t\t\t\t[node.param.name]: segment,\n\t\t\t\t},\n\t\t\t\ttrailingSlash,\n\t\t\t);\n\t\t\tif (match) return match;\n\t\t}\n\n\t\tif (node.glob?.regex.test(segment)) {\n\t\t\tconst glob = node.glob;\n\t\t\tconst match = this.#matchNode(glob.child, segments, i + 1, params, trailingSlash);\n\t\t\tif (match) return match;\n\t\t}\n\n\t\t// The rest matcher consumes all remaining segments\n\t\tconst rest = node.rest;\n\t\tif (rest?.child.value !== undefined) {\n\t\t\t// Only build the captured value (and a new params object) when the\n\t\t\t// pattern actually names it — a bare `*` catch-all shouldn't pay\n\t\t\t// for a slice+join on every request\n\t\t\tif (rest.name !== undefined) {\n\t\t\t\treturn {\n\t\t\t\t\tvalue: rest.child.value,\n\t\t\t\t\tparams: orUndefined({ ...params, [rest.name]: segments.slice(i).join(\"/\") }),\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn { value: rest.child.value, params: orUndefined(params) };\n\t\t}\n\n\t\treturn undefined;\n\t}\n}\n\nexport interface PathMatch<T> {\n\tvalue: T;\n\tparams: Record<string, string> | undefined;\n}\n\nclass TrieNode<T> {\n\tvalue: T | undefined = undefined;\n\tstatic: Map<string, TrieNode<T>> | undefined;\n\tparam: { name: string; child: TrieNode<T> } | undefined;\n\tglob: { regex: RegExp; child: TrieNode<T> } | undefined;\n\trest: { name: string | undefined; allowEmpty: boolean; child: TrieNode<T> } | undefined;\n}\n\n/**\n * Splits a path or pattern into segments, dropping the leading empty segment\n * and any trailing empty (from a trailing slash). `/posts/5` becomes\n * `[\"posts\", \"5\"]` and `/` becomes `[]`.\n */\nfunction splitPath(path: string): string[] {\n\tconst segments = path.split(\"/\");\n\tif (segments[0] === \"\") segments.shift();\n\tif (segments.length && segments[segments.length - 1] === \"\") segments.pop();\n\treturn segments;\n}\n\n/**\n * Compiles a pattern segment with an embedded `*` (e.g. `*.css`) into a\n * regex that matches a single path segment.\n */\nfunction globToRegex(segment: string): RegExp {\n\tconst source = segment\n\t\t.split(\"*\")\n\t\t.map((part) => part.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"))\n\t\t.join(\".*\");\n\treturn new RegExp(`^${source}$`);\n}\n\nfunction orUndefined(params: Record<string, string>): Record<string, string> | undefined {\n\treturn Object.keys(params).length ? params : undefined;\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { a as transfer, o as response } from "./unprocessable-DAZbrDeR.mjs";
1
+ import { a as transfer, o as response } from "./unprocessable-D6RcYLh6.mjs";
2
2
  //#region src/response/badRequest.ts
3
3
  /**
4
4
  * 400 Bad Request
@@ -281,4 +281,4 @@ function unauthorized(body) {
281
281
  //#endregion
282
282
  export { notModified as a, found as c, badRequest as d, permanentRedirect as i, forbidden as l, temporaryRedirect as n, movedPermanently as o, serverError as r, methodNotAllowed as s, unauthorized as t, created as u };
283
283
 
284
- //# sourceMappingURL=response-Bg4w2S1q.mjs.map
284
+ //# sourceMappingURL=response-CuNPcxsY.mjs.map