@torpor/build 0.1.23 → 0.1.25

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 (52) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/bin/index.cjs +1 -1
  3. package/dist/bin/index.cjs.map +1 -1
  4. package/dist/bin/index.js +3 -3
  5. package/dist/{chunk-WMHM5RGZ.js → chunk-565UOT3F.js} +5 -2
  6. package/dist/chunk-565UOT3F.js.map +1 -0
  7. package/dist/{chunk-D6LHMH2C.js → chunk-B2UNTYN5.js} +8 -8
  8. package/dist/chunk-B2UNTYN5.js.map +1 -0
  9. package/dist/{chunk-LHRCEWBG.js → chunk-UCA7KIGG.js} +2 -2
  10. package/dist/{chunk-65K3R3CP.js → chunk-UN2WQH2T.js} +4 -2
  11. package/dist/{chunk-65K3R3CP.js.map → chunk-UN2WQH2T.js.map} +1 -1
  12. package/dist/{chunk-2QQN3D3K.js → chunk-WVXAN4IV.js} +2 -2
  13. package/dist/{chunk-2QQN3D3K.js.map → chunk-WVXAN4IV.js.map} +1 -1
  14. package/dist/index.cjs +3 -0
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.js +4 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/metafile-cjs.json +1 -1
  19. package/dist/metafile-esm.json +1 -1
  20. package/dist/response.cjs +7 -7
  21. package/dist/response.cjs.map +1 -1
  22. package/dist/response.js +2 -2
  23. package/dist/run.cjs +1 -1
  24. package/dist/run.cjs.map +1 -1
  25. package/dist/run.js +3 -3
  26. package/dist/state.cjs +4 -1
  27. package/dist/state.cjs.map +1 -1
  28. package/dist/state.d.cts +4 -0
  29. package/dist/state.d.ts +4 -0
  30. package/dist/state.js +1 -1
  31. package/dist/test.cjs +31 -14
  32. package/dist/test.cjs.map +1 -1
  33. package/dist/test.js +29 -15
  34. package/dist/test.js.map +1 -1
  35. package/package.json +4 -4
  36. package/src/response/badRequest.ts +1 -1
  37. package/src/response/forbidden.ts +1 -1
  38. package/src/response/methodNotAllowed.ts +1 -1
  39. package/src/response/notFound.ts +1 -1
  40. package/src/response/serverError.ts +1 -1
  41. package/src/response/unauthorized.ts +1 -1
  42. package/src/response/unprocessable.ts +1 -1
  43. package/src/site/Site.ts +3 -0
  44. package/src/site/clientEntry.ts +34 -17
  45. package/src/site/serverEntry.ts +68 -17
  46. package/src/state/$page.ts +4 -5
  47. package/src/state/Page.ts +8 -0
  48. package/src/test/runTest.ts +26 -12
  49. package/src/types/RouteType.ts +1 -0
  50. package/dist/chunk-D6LHMH2C.js.map +0 -1
  51. package/dist/chunk-WMHM5RGZ.js.map +0 -1
  52. /package/dist/{chunk-LHRCEWBG.js.map → chunk-UCA7KIGG.js.map} +0 -0
package/dist/test.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/test.ts","../src/test/runTest.ts","../src/response/response.ts","../src/response/notFound.ts","../src/response/ok.ts","../src/response/transfer.ts","../src/response/seeOther.ts","../src/server/CookieHelper.ts","../src/server/HeaderHelper.ts","../src/server/ServerEvent.ts","../src/site/pathToRegex.ts","../src/site/Router.ts","../src/state/$page.ts","../src/types/RouteType.ts"],"sourcesContent":["import runTest from \"./test/runTest\";\n\nexport { runTest };\n","import { type ServerComponent, type ServerSlotRender } from \"@torpor/view\";\nimport path from \"node:path\";\nimport notFound from \"../response/notFound.ts\";\nimport ok from \"../response/ok.ts\";\nimport seeOther from \"../response/seeOther.ts\";\nimport ServerEvent from \"../server/ServerEvent.ts\";\nimport Router from \"../site/Router.ts\";\nimport Site from \"../site/Site\";\nimport $page from \"../state/$page.ts\";\nimport type PageEndPoint from \"../types/PageEndPoint.ts\";\nimport type PageServerEndPoint from \"../types/PageServerEndPoint.ts\";\nimport RouteHandler from \"../types/RouteHandler.ts\";\nimport {\n\tHOOK_ROUTE,\n\tHOOK_SERVER_ROUTE,\n\tLAYOUT_ROUTE,\n\tLAYOUT_SERVER_ROUTE,\n\tPAGE_ROUTE,\n\tPAGE_SERVER_ROUTE,\n\tSERVER_ROUTE,\n} from \"../types/RouteType\";\nimport type ServerEndPoint from \"../types/ServerEndPoint.ts\";\nimport type ServerHook from \"../types/ServerHook.ts\";\nimport type ServerLoadEvent from \"../types/ServerLoadEvent.ts\";\n\n/**\n * Runs a site route in test mode\n * @param site The site configuration, with routing configured\n * @param route The route path e.g. `/home`\n * @param ev An optional ServerEvent, which you can use to set test cookies and headers\n * @returns An HTTP response containing the requested data, HTML, or error status\n */\nexport default async function runTest(\n\tsite: Site,\n\troute: string,\n\tev?: ServerEvent,\n): Promise<Response> {\n\tif (!route.startsWith(\"/\")) route += \"/\";\n\tif (!ev) {\n\t\tconst req = new Request(`http://localhost${route}`);\n\t\tev = new ServerEvent(req);\n\t}\n\n\t// Build the router from the Site object created by the user\n\tconst router = new Router();\n\trouter.addPages(\n\t\tsite.routes.map((r) => ({\n\t\t\tpath: r.path,\n\t\t\ttype: r.type,\n\t\t\tendPoint: () => import(/* @vite-ignore */ path.join(site.root, r.file)),\n\t\t})),\n\t);\n\n\treturn await load(router, ev, \"%COMPONENT_HTML%\");\n}\n\n// HACK: This is a copy of serverEntry\n// TODO: Make it shared instead!\nasync function load(router: Router, ev: ServerEvent, template: string): Promise<Response> {\n\t//const url = new URL(`http://${process.env.HOST ?? \"localhost\"}${ev.request.url}`);\n\tconst url = new URL(ev.request.url);\n\tconst path = url.pathname;\n\tconst query = url.searchParams;\n\n\t//console.log(`handling ${ev.request.method} for '${path}'${query.size ? ` with ${query}` : \"\"}`);\n\n\tconst route = router.match(path, query);\n\tif (!route) {\n\t\treturn notFound();\n\t}\n\n\t// Update $page before building the components\n\t// TODO: Find somewhere better to put this\n\t$page.url = url;\n\n\tconst handler = route.handler;\n\tconst params = route.params || {};\n\n\t// TODO: Hit the server hook out here\n\t// We could maybe set data loading up as middleware on a route??????\n\n\tswitch (handler.type) {\n\t\tcase PAGE_ROUTE:\n\t\tcase LAYOUT_ROUTE: {\n\t\t\t// It's a /+page.ts or /_layout.ts endpoint\n\t\t\tif (ev.request.method === \"GET\") {\n\t\t\t\treturn await loadView(ev, url, handler, params, template);\n\t\t\t} else if (ev.request.method === \"POST\") {\n\t\t\t\treturn await runAction(ev, url, handler, params, query);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase PAGE_SERVER_ROUTE:\n\t\tcase LAYOUT_SERVER_ROUTE: {\n\t\t\t// It's a /+page.server.ts or /_layout.server.ts endpoint\n\t\t\tif (ev.request.method === \"GET\") {\n\t\t\t\treturn await loadData(ev, url, handler, \"load\", params);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase SERVER_ROUTE: {\n\t\t\t// It's a /+server.ts endpoint\n\t\t\tconst functionName = ev.request.method.toLowerCase().replace(\"delete\", \"del\");\n\t\t\treturn await loadData(ev, url, handler, functionName, params);\n\t\t}\n\t\tcase HOOK_ROUTE:\n\t\tcase HOOK_SERVER_ROUTE: {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn notFound();\n}\n\nasync function loadData(\n\tev: ServerEvent,\n\turl: URL,\n\thandler: RouteHandler,\n\tfunctionName: string,\n\tparams: Record<string, string>,\n) {\n\tconst serverEndPoint: ServerEndPoint | undefined = (await handler.endPoint()).default;\n\tif (serverEndPoint && serverEndPoint[functionName]) {\n\t\tconst serverParams = buildServerParams(ev, url, params);\n\n\t\tif (handler.serverHook) {\n\t\t\tconst serverHook: ServerHook | undefined = (await handler.serverHook()).default;\n\t\t\tif (serverHook?.handle) {\n\t\t\t\tawait serverHook.handle(serverParams);\n\t\t\t}\n\t\t}\n\n\t\tconst result = await serverEndPoint[functionName](serverParams);\n\n\t\t// If there was no response returned from load (such as errors or a\n\t\t// redirect), send an ok response\n\t\treturn result || ok();\n\t}\n\n\treturn notFound();\n}\n\nasync function loadView(\n\tev: ServerEvent,\n\turl: URL,\n\thandler: RouteHandler,\n\tparams: Record<string, string>,\n\ttemplate: string,\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\treturn notFound();\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\t// Maybe hit the server hook\n\tconst serverParams = buildServerParams(ev, url, params);\n\tif (handler.serverHook) {\n\t\tconst serverHook: ServerHook | undefined = (await handler.serverHook()).default;\n\t\tif (serverHook?.handle) {\n\t\t\tawait serverHook.handle(serverParams);\n\t\t}\n\t}\n\n\t// Pass the data into $props\n\t// TODO: Promise.all\n\t// NOTE: We're loading data from top to bottom, overriding as we go, and I'm not sure if this is the best way to go\n\tlet data = {};\n\tif (handler.layouts) {\n\t\tfor (let layout of handler.layouts) {\n\t\t\tconst layoutEndPoint: PageEndPoint | undefined = (await layout.endPoint())?.default;\n\t\t\tconst layoutServerEndPoint: PageServerEndPoint | undefined =\n\t\t\t\tlayout.serverEndPoint && (await layout.serverEndPoint())?.default;\n\t\t\tconst layoutData = await loadClientAndServerData(\n\t\t\t\turl,\n\t\t\t\tparams,\n\t\t\t\tserverParams,\n\t\t\t\tdata,\n\t\t\t\tlayoutEndPoint,\n\t\t\t\tlayoutServerEndPoint,\n\t\t\t);\n\t\t\tObject.assign(data, layoutData);\n\t\t}\n\t}\n\tlet endPointData = await loadClientAndServerData(\n\t\turl,\n\t\tparams,\n\t\tserverParams,\n\t\tdata,\n\t\tclientEndPoint,\n\t\tserverEndPoint,\n\t);\n\tObject.assign(data, endPointData);\n\tlet $props: Record<string, any> = { data };\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: Also handle layout server data\n\t// TODO: There's probably a nicer way to do this with reducers or something\n\t// TODO: Re-use parent layouts\n\tlet component = clientEndPoint.component as ServerComponent;\n\tlet slots: Record<string, ServerSlotRender> | undefined = undefined;\n\tif (handler.layouts) {\n\t\tlet slotFunctions: ServerSlotRender[] = [];\n\t\tslotFunctions[handler.layouts.length] = (_, context) =>\n\t\t\t(clientEndPoint.component as ServerComponent)($props, context);\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 (i === 0) {\n\t\t\t\t\tcomponent = layoutEndPoint.component as ServerComponent;\n\t\t\t\t\tslots = { _: slotFunctions[i + 1] };\n\t\t\t\t} else {\n\t\t\t\t\tslotFunctions[i] = (_, context) =>\n\t\t\t\t\t\t(layoutEndPoint.component as ServerComponent)($props, context, {\n\t\t\t\t\t\t\t_: slotFunctions[i + 1],\n\t\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tlet html;\n\ttry {\n\t\thtml = component($props, undefined, slots);\n\t\thtml = template.replace(\"%COMPONENT_HTML%\", html);\n\t} catch (error) {\n\t\t// TODO: Show a proper Error component\n\t\thtml = '<span style=\"color: red\">Script syntax error</span><p>' + error + \"</p>\";\n\t\tconsole.log(error);\n\t}\n\n\treturn new Response(html, {\n\t\tstatus: 200,\n\t\theaders: {\n\t\t\t\"Content-Type\": \"text/html\",\n\t\t},\n\t});\n}\n\nasync function runAction(\n\tev: ServerEvent,\n\turl: URL,\n\thandler: RouteHandler,\n\tparams: Record<string, string>,\n\tquery: URLSearchParams,\n) {\n\tconst actionName = (Array.from(query.keys())[0] || \"default\").replace(/^\\//, \"\");\n\tconst serverEndPoint: PageServerEndPoint | undefined =\n\t\thandler.serverEndPoint && (await handler.serverEndPoint()).default;\n\tif (serverEndPoint?.actions) {\n\t\tconst action = serverEndPoint.actions[actionName];\n\t\tif (action) {\n\t\t\t// TODO: form.errors etc\n\t\t\tconst serverParams = buildServerParams(ev, url, params);\n\n\t\t\tif (handler.serverHook) {\n\t\t\t\tconst serverHook: ServerHook | undefined = (await handler.serverHook()).default;\n\t\t\t\tif (serverHook?.handle) {\n\t\t\t\t\tawait serverHook.handle(serverParams);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst result = await action(serverParams); // <--\n\n\t\t\t// If there was no response returned from the action (such as errors\n\t\t\t// or a redirect), reload the page by redirecting\n\t\t\treturn result || seeOther(url.pathname);\n\t\t}\n\t}\n\n\treturn notFound();\n}\n\nasync function loadClientAndServerData(\n\turl: URL,\n\tparams: Record<string, string>,\n\tserverParams: ServerLoadEvent,\n\tdata: Record<string, any>,\n\tclientEndPoint?: PageEndPoint,\n\tserverEndPoint?: PageServerEndPoint,\n) {\n\tlet newData = {};\n\tif (clientEndPoint?.load) {\n\t\tconst clientParams = buildClientParams(url, data, params);\n\t\tconst clientData = await clientEndPoint.load(clientParams);\n\t\tif (clientData) {\n\t\t\tObject.assign(newData, clientData);\n\t\t}\n\t}\n\tif (serverEndPoint?.load) {\n\t\tconst serverData = await serverEndPoint.load(serverParams);\n\t\tif (serverData?.ok) {\n\t\t\tObject.assign(newData, await serverData.json());\n\t\t}\n\t}\n\treturn newData;\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\nfunction buildServerParams(\n\tev: ServerEvent,\n\turl: URL,\n\tparams: Record<string, string>,\n): ServerLoadEvent {\n\treturn {\n\t\turl,\n\t\tparams,\n\t\tappData: {},\n\t\trequest: ev.request,\n\t\tcookies: ev.cookies,\n\t\theaders: ev.headers,\n\t\tadapter: ev.adapter,\n\t};\n}\n","/**\n * Creates a response with the supplied status code and optional body.\n *\n * If the body is an object, it will be converted to JSON and the content-type\n * header set to \"application/json\". Otherwise, if the body is a string, the\n * content-type header will be set to \"text/plain\".\n */\nexport default function response(status: number, body?: object | string): Response {\n\tlet headers: Record<string, string> | undefined = undefined;\n\tif (body) {\n\t\tif (typeof body === \"object\") {\n\t\t\theaders = { \"content-type\": \"application/json\" };\n\t\t\tbody = JSON.stringify(body);\n\t\t} else {\n\t\t\theaders = { \"content-type\": \"text/plain\" };\n\t\t}\n\t}\n\treturn new Response(body, {\n\t\tstatus,\n\t\theaders,\n\t});\n}\n","import response from \"./response\";\n\n/**\n * 404 Not Found\n *\n * The server cannot find the requested resource. In the browser, this means the\n * URL is not recognized. In an API, this can also mean that the endpoint is\n * valid but the resource itself does not exist. Servers may also send this\n * response instead of 403 Forbidden to hide the existence of a resource from an\n * unauthorized client. This response code is probably the most well known due\n * to its frequent occurrence on the web.\n *\n * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404\n */\nexport default function notFound(body?: object | string): Response {\n\treturn response(404, body);\n}\n","import response from \"./response\";\n\n/**\n * 200 OK\n *\n * The request succeeded. The result meaning of \"success\" depends on the HTTP\n * method:\n * - GET: The resource has been fetched and transmitted in the message body.\n * - HEAD: The representation headers are included in the response without any\n * message body.\n * - PUT or POST: The resource describing the result of the action is\n * transmitted in the message body.\n * - TRACE: The message body contains the request message as received by the\n * server.\n *\n * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/200\n */\nexport default function ok(body?: object | string): Response {\n\treturn response(200, body);\n}\n","/**\n * Creates a redirect response with the supplied status code.\n */\nexport default function transfer(\n\tstatus: 300 | 301 | 302 | 303 | 304 | 307 | 308,\n\tlocation: string,\n): Response {\n\treturn new Response(null, {\n\t\tstatus,\n\t\theaders: {\n\t\t\tlocation,\n\t\t},\n\t});\n}\n","import transfer from \"./transfer\";\n\n/**\n * 303 See Other\n *\n * The server sent this response to direct the client to get the requested\n * resource at another URI with a GET request.\n *\n * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/303\n */\nexport default function seeOther(location: string): Response {\n\treturn transfer(303, location);\n}\n","import { SerializeOptions, parse, serialize } from \"cookie\";\n\nexport default class CookieHelper {\n\trequest: Request;\n\tcookies: Map<string, string> = new Map();\n\n\tconstructor(request: Request) {\n\t\tthis.request = request;\n\t}\n\n\tget(name: string): string | undefined {\n\t\t// If it's been set, return the value from the map\n\t\tif (this.cookies.has(name)) {\n\t\t\treturn parse(this.cookies.get(name)!)[name];\n\t\t}\n\t\t// Otherwise, return the value from the `cookie` header\n\t\tconst header = this.request.headers.get(\"cookie\");\n\t\treturn parse(header ?? \"\")[name];\n\t}\n\n\tset(name: string, value: string, options?: SerializeOptions): void {\n\t\t// Set a cookie header with some default values for security\n\t\toptions ??= {};\n\t\toptions.httpOnly ??= true;\n\t\toptions.secure ??= true;\n\t\toptions.sameSite ??= \"lax\";\n\t\tthis.cookies.set(name, serialize(name, value, options));\n\t}\n\n\tdelete(name: string, options?: SerializeOptions): void {\n\t\t// Set a cookie header with an immediate expiry date\n\t\toptions ??= {};\n\t\toptions.expires = new Date(0);\n\t\tthis.cookies.set(name, serialize(name, \"\", options));\n\t}\n}\n","export default class HeaderHelper {\n\trequest: Request;\n\theaders: Map<string, string> = new Map();\n\n\tconstructor(request: Request) {\n\t\tthis.request = request;\n\t}\n\n\tget(name: string): string | undefined {\n\t\t// If it's been set, return the updated value\n\t\tif (this.headers.has(name)) {\n\t\t\treturn this.headers.get(name);\n\t\t}\n\t\t// Otherwise, return the value from the header\n\t\treturn this.request.headers.get(name) ?? undefined;\n\t}\n\n\tset(name: string, value: string): void {\n\t\tthis.headers.set(name, value);\n\t}\n\n\tdelete(name: string): void {\n\t\tthis.headers.delete(name);\n\t}\n}\n","import CookieHelper from \"./CookieHelper\";\nimport HeaderHelper from \"./HeaderHelper\";\n\nexport default class ServerEvent {\n\trequest: Request;\n\tresponse?: Response;\n\t// TODO: Should we cast types??\n\tparams?: Record<string, string>;\n\n\tadapter: any;\n\n\tcookies: CookieHelper;\n\theaders: HeaderHelper;\n\n\tconstructor(request: Request, params?: Record<string, string>) {\n\t\tthis.request = request;\n\t\tthis.params = params;\n\t\tthis.cookies = new CookieHelper(request);\n\t\tthis.headers = new HeaderHelper(request);\n\n\t\t// Adapters may have added the `adapter` property to globalThis\n\t\t// @ts-ignore\n\t\tthis.adapter = globalThis.adapter;\n\t}\n\n\taddHeaders(): void {\n\t\tif (!this.response) {\n\t\t\tthrow new Error(\"Response not created yet\");\n\t\t}\n\n\t\tfor (let cookie of this.cookies.cookies.values()) {\n\t\t\tthis.response.headers.append(\"set-cookie\", cookie);\n\t\t}\n\n\t\tfor (let header of this.headers.headers.entries()) {\n\t\t\tthis.response.headers.append(header[0], header[1]);\n\t\t}\n\t}\n}\n","export default function pathToRegex(path: string): RegExp {\n\tconst pattern =\n\t\tpath\n\t\t\t.split(\"/\")\n\t\t\t// Replace `[slug]` with a grouped match so we can pull out params\n\t\t\t.map((p) => {\n\t\t\t\treturn p.replace(/\\[([^\\/]+?)\\]/, \"(?<$1>[^\\\\/]+?)\");\n\t\t\t})\n\t\t\t.join(\"\\\\/\")\n\t\t\t// Replace globs with regex syntax\n\t\t\t.replaceAll(\"*\", \".*\") +\n\t\t// Ignore trailing slashes -- `/path` should be treated the same as `/path/`\n\t\t\"/*\";\n\treturn new RegExp(`^${pattern}/*$`);\n}\n","import type LayoutHandler from \"../types/LayoutHandler\";\nimport type ManifestRoute from \"../types/ManifestRoute\";\nimport type RouteHandler from \"../types/RouteHandler\";\nimport pathToRegex from \"./pathToRegex\";\n\n/**\n * A router that handles file routes with layouts, hooks, etc.\n */\nexport default class Router {\n\troutes: Route[] = [];\n\n\tconstructor() {}\n\n\t//add(path: string, file: string) {\n\t//\t// TODO: Call the specific addX method based on the file path\n\t//\tthis.addPage(route, file);\n\t//}\n\n\taddPages(routes: ManifestRoute[]): this {\n\t\tfor (let r of routes) {\n\t\t\tthis.routes.push(\n\t\t\t\tnew Route(r.path, {\n\t\t\t\t\tpath: r.path,\n\t\t\t\t\ttype: r.type,\n\t\t\t\t\tendPoint: r.endPoint,\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\tthis.#sortRoutes();\n\t\treturn this;\n\t}\n\n\t// TODO: Allow calling with the endpoint itself, so that you can setup an app with no scaffold\n\taddPage(path: string, type: number, endPoint: () => Promise<any>): this {\n\t\tthis.routes.push(\n\t\t\tnew Route(path, {\n\t\t\t\tpath,\n\t\t\t\ttype,\n\t\t\t\tendPoint,\n\t\t\t}),\n\t\t);\n\t\tthis.#sortRoutes();\n\t\treturn this;\n\t}\n\n\t/*\n\tloadPage(ev: ServerEvent, file: string): Response {\n\t\tlet page = this.pages.get(file);\n\t\tif (!page) {\n\t\t\tpage = {};\n\t\t\tthis.pages.set(file, page);\n\t\t}\n\t}\n\t*/\n\n\tmatch(path: string, query: URLSearchParams): Match | undefined {\n\t\tfor (let route of this.routes) {\n\t\t\tlet match = path.match(route.regex);\n\t\t\tif (match) {\n\t\t\t\t// Lazy load server endpoints and layouts\n\t\t\t\tif (!route.handler.loaded) {\n\t\t\t\t\tthis.#loadHandler(route.handler, route.path);\n\t\t\t\t}\n\n\t\t\t\treturn {\n\t\t\t\t\thandler: route.handler,\n\t\t\t\t\tparams: match.groups,\n\t\t\t\t\tquery,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tconsole.log(`not found: '${path}'`);\n\t}\n\n\t#loadHandler(handler: RouteHandler, path: string) {\n\t\thandler.layouts = this.#findLayouts(path);\n\t\thandler.serverEndPoint = this.#findServer(path);\n\t\thandler.serverHook = this.#findServerHook(path);\n\t}\n\n\t#findLayouts(path: string): LayoutHandler[] | undefined {\n\t\tlet layouts: LayoutHandler[] = [];\n\t\tlet parts = path\n\t\t\t// Strip the trailing`~/server` (so we don't look for `~/server/_layout`\n\t\t\t// which will never exist)\n\t\t\t.replace(/\\/~server$/, \"\")\n\t\t\t// The path will always start with / so splitting e.g. `/first/second`\n\t\t\t// will result in ['', 'first', 'second'] and checks for `/_layout`,\n\t\t\t// `/first/_layout` and `/second/_layout`\n\t\t\t.split(\"/\");\n\t\tlet basePath = \"\";\n\t\tfor (let i = 0; i < parts.length; i++) {\n\t\t\tif (i > 0) {\n\t\t\t\tbasePath += \"/\" + parts[i];\n\t\t\t}\n\t\t\tconst layoutPath = basePath + \"/_layout\";\n\t\t\tconst layoutRoute = this.routes.find((r) => r.path === layoutPath);\n\t\t\tif (layoutRoute) {\n\t\t\t\tlayouts.push({\n\t\t\t\t\tpath: layoutPath,\n\t\t\t\t\tendPoint: layoutRoute.handler.endPoint,\n\t\t\t\t\tserverEndPoint: this.#findServer(layoutPath),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\treturn layouts.length ? layouts : undefined;\n\t}\n\n\t#findServer(path: string): (() => Promise<any>) | undefined {\n\t\tconst serverPath = path.replace(/\\/$/, \"\") + \"/~server\";\n\t\tconst serverRoute = this.routes.find((r) => r.path === serverPath);\n\t\treturn serverRoute && serverRoute.handler.endPoint;\n\t}\n\n\t#findServerHook(path: string): (() => Promise<any>) | undefined {\n\t\t// TODO: Should this be a collection, like layouts?\n\t\tlet serverHookPath = \"/_hook/~server\";\n\t\t// HACK: Generalise this\n\t\tif (path.startsWith(\"/api/\")) {\n\t\t\tserverHookPath = \"/api\" + serverHookPath;\n\t\t}\n\t\tconst serverHookRoute = this.routes.find((r) => r.path === serverHookPath);\n\t\treturn serverHookRoute && serverHookRoute.handler.endPoint;\n\t}\n\n\t// HACK: We're doing this here as well as in Site, so that you can use the\n\t// Router externally without Site\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\nclass Route {\n\tpath: string;\n\tregex: RegExp;\n\thandler: RouteHandler;\n\n\tconstructor(path: string, handler: RouteHandler) {\n\t\tthis.path = path;\n\t\tthis.regex = pathToRegex(path);\n\t\tthis.handler = handler;\n\t}\n}\n\ntype Match = {\n\thandler: RouteHandler;\n\tparams?: Record<string, string>;\n\tquery: URLSearchParams;\n};\n","import { $watch } from \"@torpor/view\";\n\ntype Page = {\n\tstatus: number;\n\turl: URL;\n};\n\nconst $page: Page = $watch(\n\t{\n\t\tstatus: 404,\n\t\turl: new URL(\"http://localhost\"),\n\t},\n\t{ shallow: true },\n);\n\nexport default $page;\n","export const PAGE_ROUTE = 0;\nexport const PAGE_SERVER_ROUTE = 1;\nexport const SERVER_ROUTE = 3;\nexport const LAYOUT_ROUTE = 4;\nexport const LAYOUT_SERVER_ROUTE = 5;\nexport const HOOK_ROUTE = 6;\nexport const HOOK_SERVER_ROUTE = 7;\n"],"mappings":"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","names":["path","path","path"]}
1
+ {"version":3,"sources":["../src/test.ts","../src/test/runTest.ts","../src/response/response.ts","../src/response/notFound.ts","../src/response/ok.ts","../src/response/transfer.ts","../src/response/seeOther.ts","../src/server/CookieHelper.ts","../src/server/HeaderHelper.ts","../src/server/ServerEvent.ts","../src/site/pathToRegex.ts","../src/site/Router.ts","../src/state/$page.ts","../src/types/RouteType.ts"],"sourcesContent":["import runTest from \"./test/runTest\";\n\nexport { runTest };\n","import { type ServerComponent, type ServerSlotRender } from \"@torpor/view\";\nimport path from \"node:path\";\nimport notFound from \"../response/notFound.ts\";\nimport ok from \"../response/ok.ts\";\nimport seeOther from \"../response/seeOther.ts\";\nimport ServerEvent from \"../server/ServerEvent.ts\";\nimport Router from \"../site/Router.ts\";\nimport Site from \"../site/Site\";\nimport $page from \"../state/$page.ts\";\nimport type PageEndPoint from \"../types/PageEndPoint.ts\";\nimport type PageServerEndPoint from \"../types/PageServerEndPoint.ts\";\nimport RouteHandler from \"../types/RouteHandler.ts\";\nimport {\n\tHOOK_ROUTE,\n\tHOOK_SERVER_ROUTE,\n\tLAYOUT_ROUTE,\n\tLAYOUT_SERVER_ROUTE,\n\tPAGE_ROUTE,\n\tPAGE_SERVER_ROUTE,\n\tSERVER_ROUTE,\n} from \"../types/RouteType\";\nimport type ServerEndPoint from \"../types/ServerEndPoint.ts\";\nimport type ServerHook from \"../types/ServerHook.ts\";\nimport type ServerLoadEvent from \"../types/ServerLoadEvent.ts\";\n\n/**\n * Runs a site route in test mode\n * @param site The site configuration, with routing configured\n * @param route The route path e.g. `/home`\n * @param ev An optional ServerEvent, which you can use to set test cookies and headers\n * @returns An HTTP response containing the requested data, HTML, or error status\n */\nexport default async function runTest(\n\tsite: Site,\n\troute: string,\n\tev?: ServerEvent,\n): Promise<Response> {\n\tif (!route.startsWith(\"/\")) route += \"/\";\n\tif (!ev) {\n\t\tconst req = new Request(`http://localhost${route}`);\n\t\tev = new ServerEvent(req);\n\t}\n\n\t// Build the router from the Site object created by the user\n\tconst router = new Router();\n\trouter.addPages(\n\t\tsite.routes.map((r) => ({\n\t\t\tpath: r.path,\n\t\t\ttype: r.type,\n\t\t\tendPoint: () => import(/* @vite-ignore */ path.join(site.root, r.file)),\n\t\t})),\n\t);\n\n\treturn await load(router, ev, \"%COMPONENT_HTML%\");\n}\n\n// HACK: This is a copy of serverEntry\n// TODO: Make it shared instead!\nasync function load(router: Router, ev: ServerEvent, template: string): Promise<Response> {\n\t//const url = new URL(`http://${process.env.HOST ?? \"localhost\"}${ev.request.url}`);\n\tconst url = new URL(ev.request.url);\n\tconst path = url.pathname;\n\tconst query = url.searchParams;\n\n\t//console.log(`handling ${ev.request.method} for '${path}'${query.size ? ` with ${query}` : \"\"}`);\n\n\tconst route = router.match(path, query);\n\tif (!route) {\n\t\treturn notFound();\n\t}\n\n\t// Update $page before building the components\n\t// TODO: Find somewhere better to put this\n\t$page.url = url;\n\n\tconst handler = route.handler;\n\tconst params = route.params || {};\n\n\t// TODO: Hit the server hook out here\n\t// We could maybe set data loading up as middleware on a route??????\n\n\tswitch (handler.type) {\n\t\tcase PAGE_ROUTE:\n\t\tcase LAYOUT_ROUTE: {\n\t\t\t// It's a /+page.ts or /_layout.ts endpoint\n\t\t\tif (ev.request.method === \"GET\") {\n\t\t\t\treturn await loadView(ev, url, handler, params, template);\n\t\t\t} else if (ev.request.method === \"POST\") {\n\t\t\t\treturn await runAction(ev, url, handler, params, query);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase PAGE_SERVER_ROUTE:\n\t\tcase LAYOUT_SERVER_ROUTE: {\n\t\t\t// It's a /+page.server.ts or /_layout.server.ts endpoint\n\t\t\tif (ev.request.method === \"GET\") {\n\t\t\t\treturn await loadData(ev, url, handler, \"load\", params);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase SERVER_ROUTE: {\n\t\t\t// It's a /+server.ts endpoint\n\t\t\tconst functionName = ev.request.method.toLowerCase().replace(\"delete\", \"del\");\n\t\t\treturn await loadData(ev, url, handler, functionName, params);\n\t\t}\n\t\tcase HOOK_ROUTE:\n\t\tcase HOOK_SERVER_ROUTE: {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn notFound();\n}\n\nasync function loadData(\n\tev: ServerEvent,\n\turl: URL,\n\thandler: RouteHandler,\n\tfunctionName: string,\n\tparams: Record<string, string>,\n) {\n\tconst serverEndPoint: ServerEndPoint | undefined = (await handler.endPoint()).default;\n\tif (serverEndPoint && serverEndPoint[functionName]) {\n\t\tconst serverParams = buildServerParams(ev, url, params);\n\n\t\tif (handler.serverHook) {\n\t\t\tconst serverHook: ServerHook | undefined = (await handler.serverHook()).default;\n\t\t\tif (serverHook?.handle) {\n\t\t\t\tawait serverHook.handle(serverParams);\n\t\t\t}\n\t\t}\n\n\t\tconst result = await serverEndPoint[functionName](serverParams);\n\n\t\t// If there was no response returned from load (such as errors or a\n\t\t// redirect), send an ok response\n\t\treturn result || ok();\n\t}\n\n\treturn notFound();\n}\n\nasync function loadView(\n\tev: ServerEvent,\n\turl: URL,\n\thandler: RouteHandler,\n\tparams: Record<string, string>,\n\ttemplate: string,\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\treturn notFound();\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\t// Maybe hit the server hook\n\tconst serverParams = buildServerParams(ev, url, params);\n\tif (handler.serverHook) {\n\t\tconst serverHook: ServerHook | undefined = (await handler.serverHook()).default;\n\t\tif (serverHook?.handle) {\n\t\t\tawait serverHook.handle(serverParams);\n\t\t}\n\t}\n\n\t// Pass the data into $props\n\t// TODO: Promise.all\n\t// NOTE: We're loading data from top to bottom, overriding as we go, and I'm not sure if this is the best way to go\n\tlet data = {};\n\tif (handler.layouts) {\n\t\tfor (let layout of handler.layouts) {\n\t\t\tconst layoutEndPoint: PageEndPoint | undefined = (await layout.endPoint())?.default;\n\t\t\tconst layoutServerEndPoint: PageServerEndPoint | undefined =\n\t\t\t\tlayout.serverEndPoint && (await layout.serverEndPoint())?.default;\n\t\t\tconst layoutResponse = await loadClientAndServerData(\n\t\t\t\turl,\n\t\t\t\tparams,\n\t\t\t\tserverParams,\n\t\t\t\tdata,\n\t\t\t\tlayoutEndPoint,\n\t\t\t\tlayoutServerEndPoint,\n\t\t\t);\n\t\t\tif (layoutResponse?.ok === false) {\n\t\t\t\treturn layoutResponse;\n\t\t\t}\n\t\t}\n\t}\n\tlet endPointResponse = await loadClientAndServerData(\n\t\turl,\n\t\tparams,\n\t\tserverParams,\n\t\tdata,\n\t\tclientEndPoint,\n\t\tserverEndPoint,\n\t);\n\tif (endPointResponse?.ok === false) {\n\t\treturn endPointResponse;\n\t}\n\tlet $props: Record<string, any> = { data };\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: Also handle layout server data\n\t// TODO: There's probably a nicer way to do this with reducers or something\n\t// TODO: Re-use parent layouts\n\tlet component = clientEndPoint.component as ServerComponent;\n\tlet slots: Record<string, ServerSlotRender> | undefined = undefined;\n\tif (handler.layouts) {\n\t\tlet slotFunctions: ServerSlotRender[] = [];\n\t\tslotFunctions[handler.layouts.length] = (_, context) =>\n\t\t\t(clientEndPoint.component as ServerComponent)($props, context);\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 (i === 0) {\n\t\t\t\t\tcomponent = layoutEndPoint.component as ServerComponent;\n\t\t\t\t\tslots = { _: slotFunctions[i + 1] };\n\t\t\t\t} else {\n\t\t\t\t\tslotFunctions[i] = (_, context) =>\n\t\t\t\t\t\t(layoutEndPoint.component as ServerComponent)($props, context, {\n\t\t\t\t\t\t\t_: slotFunctions[i + 1],\n\t\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tlet html;\n\ttry {\n\t\thtml = component($props, undefined, slots);\n\t\thtml = template.replace(\"%COMPONENT_HTML%\", html);\n\t} catch (error) {\n\t\t// TODO: Show a proper Error component\n\t\thtml = '<span style=\"color: red\">Script syntax error</span><p>' + error + \"</p>\";\n\t\tconsole.log(error);\n\t}\n\n\treturn new Response(html, {\n\t\tstatus: 200,\n\t\theaders: {\n\t\t\t\"Content-Type\": \"text/html\",\n\t\t},\n\t});\n}\n\nasync function runAction(\n\tev: ServerEvent,\n\turl: URL,\n\thandler: RouteHandler,\n\tparams: Record<string, string>,\n\tquery: URLSearchParams,\n) {\n\tconst actionName = (Array.from(query.keys())[0] || \"default\").replace(/^\\//, \"\");\n\tconst serverEndPoint: PageServerEndPoint | undefined =\n\t\thandler.serverEndPoint && (await handler.serverEndPoint()).default;\n\tif (serverEndPoint?.actions) {\n\t\tconst action = serverEndPoint.actions[actionName];\n\t\tif (action) {\n\t\t\t// TODO: form.errors etc\n\t\t\tconst serverParams = buildServerParams(ev, url, params);\n\n\t\t\tif (handler.serverHook) {\n\t\t\t\tconst serverHook: ServerHook | undefined = (await handler.serverHook()).default;\n\t\t\t\tif (serverHook?.handle) {\n\t\t\t\t\tawait serverHook.handle(serverParams);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst result = await action(serverParams); // <--\n\n\t\t\t// If there was no response returned from the action (such as errors\n\t\t\t// or a redirect), reload the page by redirecting\n\t\t\treturn result || seeOther(url.pathname);\n\t\t}\n\t}\n\n\treturn notFound();\n}\n\nasync function loadClientAndServerData(\n\turl: URL,\n\tparams: Record<string, string>,\n\tserverParams: ServerLoadEvent,\n\tdata: Record<string, any>,\n\tclientEndPoint?: PageEndPoint,\n\tserverEndPoint?: PageServerEndPoint,\n) {\n\tif (clientEndPoint?.load) {\n\t\tconst clientParams = buildClientParams(url, data, params);\n\t\tconst clientResponse = await clientEndPoint.load(clientParams);\n\t\tif (clientResponse) {\n\t\t\tif (clientResponse.ok) {\n\t\t\t\tif (clientResponse.headers.get(\"Content-Type\")?.includes(\"application/json\")) {\n\t\t\t\t\tObject.assign(data, await clientResponse.json());\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\treturn clientResponse;\n\t\t\t}\n\t\t}\n\t}\n\tif (serverEndPoint?.load) {\n\t\tconst serverResponse = await serverEndPoint.load(serverParams);\n\t\tif (serverResponse) {\n\t\t\tif (serverResponse.ok) {\n\t\t\t\tif (serverResponse.headers.get(\"Content-Type\")?.includes(\"application/json\")) {\n\t\t\t\t\tObject.assign(data, await serverResponse.json());\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\treturn serverResponse;\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\nfunction buildServerParams(\n\tev: ServerEvent,\n\turl: URL,\n\tparams: Record<string, string>,\n): ServerLoadEvent {\n\treturn {\n\t\turl,\n\t\tparams,\n\t\tappData: {},\n\t\trequest: ev.request,\n\t\tcookies: ev.cookies,\n\t\theaders: ev.headers,\n\t\tadapter: ev.adapter,\n\t};\n}\n","/**\n * Creates a response with the supplied status code and optional body.\n *\n * If the body is an object, it will be converted to JSON and the content-type\n * header set to \"application/json\". Otherwise, if the body is a string, the\n * content-type header will be set to \"text/plain\".\n */\nexport default function response(status: number, body?: object | string): Response {\n\tlet headers: Record<string, string> | undefined = undefined;\n\tif (body) {\n\t\tif (typeof body === \"object\") {\n\t\t\theaders = { \"content-type\": \"application/json\" };\n\t\t\tbody = JSON.stringify(body);\n\t\t} else {\n\t\t\theaders = { \"content-type\": \"text/plain\" };\n\t\t}\n\t}\n\treturn new Response(body, {\n\t\tstatus,\n\t\theaders,\n\t});\n}\n","import response from \"./response\";\n\n/**\n * 404 Not Found\n *\n * The server cannot find the requested resource. In the browser, this means the\n * URL is not recognized. In an API, this can also mean that the endpoint is\n * valid but the resource itself does not exist. Servers may also send this\n * response instead of 403 Forbidden to hide the existence of a resource from an\n * unauthorized client. This response code is probably the most well known due\n * to its frequent occurrence on the web.\n *\n * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404\n */\nexport default function notFound(body?: object | string): Response {\n\treturn response(404, body ?? \"Not found\");\n}\n","import response from \"./response\";\n\n/**\n * 200 OK\n *\n * The request succeeded. The result meaning of \"success\" depends on the HTTP\n * method:\n * - GET: The resource has been fetched and transmitted in the message body.\n * - HEAD: The representation headers are included in the response without any\n * message body.\n * - PUT or POST: The resource describing the result of the action is\n * transmitted in the message body.\n * - TRACE: The message body contains the request message as received by the\n * server.\n *\n * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/200\n */\nexport default function ok(body?: object | string): Response {\n\treturn response(200, body);\n}\n","/**\n * Creates a redirect response with the supplied status code.\n */\nexport default function transfer(\n\tstatus: 300 | 301 | 302 | 303 | 304 | 307 | 308,\n\tlocation: string,\n): Response {\n\treturn new Response(null, {\n\t\tstatus,\n\t\theaders: {\n\t\t\tlocation,\n\t\t},\n\t});\n}\n","import transfer from \"./transfer\";\n\n/**\n * 303 See Other\n *\n * The server sent this response to direct the client to get the requested\n * resource at another URI with a GET request.\n *\n * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/303\n */\nexport default function seeOther(location: string): Response {\n\treturn transfer(303, location);\n}\n","import { SerializeOptions, parse, serialize } from \"cookie\";\n\nexport default class CookieHelper {\n\trequest: Request;\n\tcookies: Map<string, string> = new Map();\n\n\tconstructor(request: Request) {\n\t\tthis.request = request;\n\t}\n\n\tget(name: string): string | undefined {\n\t\t// If it's been set, return the value from the map\n\t\tif (this.cookies.has(name)) {\n\t\t\treturn parse(this.cookies.get(name)!)[name];\n\t\t}\n\t\t// Otherwise, return the value from the `cookie` header\n\t\tconst header = this.request.headers.get(\"cookie\");\n\t\treturn parse(header ?? \"\")[name];\n\t}\n\n\tset(name: string, value: string, options?: SerializeOptions): void {\n\t\t// Set a cookie header with some default values for security\n\t\toptions ??= {};\n\t\toptions.httpOnly ??= true;\n\t\toptions.secure ??= true;\n\t\toptions.sameSite ??= \"lax\";\n\t\tthis.cookies.set(name, serialize(name, value, options));\n\t}\n\n\tdelete(name: string, options?: SerializeOptions): void {\n\t\t// Set a cookie header with an immediate expiry date\n\t\toptions ??= {};\n\t\toptions.expires = new Date(0);\n\t\tthis.cookies.set(name, serialize(name, \"\", options));\n\t}\n}\n","export default class HeaderHelper {\n\trequest: Request;\n\theaders: Map<string, string> = new Map();\n\n\tconstructor(request: Request) {\n\t\tthis.request = request;\n\t}\n\n\tget(name: string): string | undefined {\n\t\t// If it's been set, return the updated value\n\t\tif (this.headers.has(name)) {\n\t\t\treturn this.headers.get(name);\n\t\t}\n\t\t// Otherwise, return the value from the header\n\t\treturn this.request.headers.get(name) ?? undefined;\n\t}\n\n\tset(name: string, value: string): void {\n\t\tthis.headers.set(name, value);\n\t}\n\n\tdelete(name: string): void {\n\t\tthis.headers.delete(name);\n\t}\n}\n","import CookieHelper from \"./CookieHelper\";\nimport HeaderHelper from \"./HeaderHelper\";\n\nexport default class ServerEvent {\n\trequest: Request;\n\tresponse?: Response;\n\t// TODO: Should we cast types??\n\tparams?: Record<string, string>;\n\n\tadapter: any;\n\n\tcookies: CookieHelper;\n\theaders: HeaderHelper;\n\n\tconstructor(request: Request, params?: Record<string, string>) {\n\t\tthis.request = request;\n\t\tthis.params = params;\n\t\tthis.cookies = new CookieHelper(request);\n\t\tthis.headers = new HeaderHelper(request);\n\n\t\t// Adapters may have added the `adapter` property to globalThis\n\t\t// @ts-ignore\n\t\tthis.adapter = globalThis.adapter;\n\t}\n\n\taddHeaders(): void {\n\t\tif (!this.response) {\n\t\t\tthrow new Error(\"Response not created yet\");\n\t\t}\n\n\t\tfor (let cookie of this.cookies.cookies.values()) {\n\t\t\tthis.response.headers.append(\"set-cookie\", cookie);\n\t\t}\n\n\t\tfor (let header of this.headers.headers.entries()) {\n\t\t\tthis.response.headers.append(header[0], header[1]);\n\t\t}\n\t}\n}\n","export default function pathToRegex(path: string): RegExp {\n\tconst pattern =\n\t\tpath\n\t\t\t.split(\"/\")\n\t\t\t// Replace `[slug]` with a grouped match so we can pull out params\n\t\t\t.map((p) => {\n\t\t\t\treturn p.replace(/\\[([^\\/]+?)\\]/, \"(?<$1>[^\\\\/]+?)\");\n\t\t\t})\n\t\t\t.join(\"\\\\/\")\n\t\t\t// Replace globs with regex syntax\n\t\t\t.replaceAll(\"*\", \".*\") +\n\t\t// Ignore trailing slashes -- `/path` should be treated the same as `/path/`\n\t\t\"/*\";\n\treturn new RegExp(`^${pattern}/*$`);\n}\n","import type LayoutHandler from \"../types/LayoutHandler\";\nimport type ManifestRoute from \"../types/ManifestRoute\";\nimport type RouteHandler from \"../types/RouteHandler\";\nimport pathToRegex from \"./pathToRegex\";\n\n/**\n * A router that handles file routes with layouts, hooks, etc.\n */\nexport default class Router {\n\troutes: Route[] = [];\n\n\tconstructor() {}\n\n\t//add(path: string, file: string) {\n\t//\t// TODO: Call the specific addX method based on the file path\n\t//\tthis.addPage(route, file);\n\t//}\n\n\taddPages(routes: ManifestRoute[]): this {\n\t\tfor (let r of routes) {\n\t\t\tthis.routes.push(\n\t\t\t\tnew Route(r.path, {\n\t\t\t\t\tpath: r.path,\n\t\t\t\t\ttype: r.type,\n\t\t\t\t\tendPoint: r.endPoint,\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\tthis.#sortRoutes();\n\t\treturn this;\n\t}\n\n\t// TODO: Allow calling with the endpoint itself, so that you can setup an app with no scaffold\n\taddPage(path: string, type: number, endPoint: () => Promise<any>): this {\n\t\tthis.routes.push(\n\t\t\tnew Route(path, {\n\t\t\t\tpath,\n\t\t\t\ttype,\n\t\t\t\tendPoint,\n\t\t\t}),\n\t\t);\n\t\tthis.#sortRoutes();\n\t\treturn this;\n\t}\n\n\t/*\n\tloadPage(ev: ServerEvent, file: string): Response {\n\t\tlet page = this.pages.get(file);\n\t\tif (!page) {\n\t\t\tpage = {};\n\t\t\tthis.pages.set(file, page);\n\t\t}\n\t}\n\t*/\n\n\tmatch(path: string, query: URLSearchParams): Match | undefined {\n\t\tfor (let route of this.routes) {\n\t\t\tlet match = path.match(route.regex);\n\t\t\tif (match) {\n\t\t\t\t// Lazy load server endpoints and layouts\n\t\t\t\tif (!route.handler.loaded) {\n\t\t\t\t\tthis.#loadHandler(route.handler, route.path);\n\t\t\t\t}\n\n\t\t\t\treturn {\n\t\t\t\t\thandler: route.handler,\n\t\t\t\t\tparams: match.groups,\n\t\t\t\t\tquery,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tconsole.log(`not found: '${path}'`);\n\t}\n\n\t#loadHandler(handler: RouteHandler, path: string) {\n\t\thandler.layouts = this.#findLayouts(path);\n\t\thandler.serverEndPoint = this.#findServer(path);\n\t\thandler.serverHook = this.#findServerHook(path);\n\t}\n\n\t#findLayouts(path: string): LayoutHandler[] | undefined {\n\t\tlet layouts: LayoutHandler[] = [];\n\t\tlet parts = path\n\t\t\t// Strip the trailing`~/server` (so we don't look for `~/server/_layout`\n\t\t\t// which will never exist)\n\t\t\t.replace(/\\/~server$/, \"\")\n\t\t\t// The path will always start with / so splitting e.g. `/first/second`\n\t\t\t// will result in ['', 'first', 'second'] and checks for `/_layout`,\n\t\t\t// `/first/_layout` and `/second/_layout`\n\t\t\t.split(\"/\");\n\t\tlet basePath = \"\";\n\t\tfor (let i = 0; i < parts.length; i++) {\n\t\t\tif (i > 0) {\n\t\t\t\tbasePath += \"/\" + parts[i];\n\t\t\t}\n\t\t\tconst layoutPath = basePath + \"/_layout\";\n\t\t\tconst layoutRoute = this.routes.find((r) => r.path === layoutPath);\n\t\t\tif (layoutRoute) {\n\t\t\t\tlayouts.push({\n\t\t\t\t\tpath: layoutPath,\n\t\t\t\t\tendPoint: layoutRoute.handler.endPoint,\n\t\t\t\t\tserverEndPoint: this.#findServer(layoutPath),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\treturn layouts.length ? layouts : undefined;\n\t}\n\n\t#findServer(path: string): (() => Promise<any>) | undefined {\n\t\tconst serverPath = path.replace(/\\/$/, \"\") + \"/~server\";\n\t\tconst serverRoute = this.routes.find((r) => r.path === serverPath);\n\t\treturn serverRoute && serverRoute.handler.endPoint;\n\t}\n\n\t#findServerHook(path: string): (() => Promise<any>) | undefined {\n\t\t// TODO: Should this be a collection, like layouts?\n\t\tlet serverHookPath = \"/_hook/~server\";\n\t\t// HACK: Generalise this\n\t\tif (path.startsWith(\"/api/\")) {\n\t\t\tserverHookPath = \"/api\" + serverHookPath;\n\t\t}\n\t\tconst serverHookRoute = this.routes.find((r) => r.path === serverHookPath);\n\t\treturn serverHookRoute && serverHookRoute.handler.endPoint;\n\t}\n\n\t// HACK: We're doing this here as well as in Site, so that you can use the\n\t// Router externally without Site\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\nclass Route {\n\tpath: string;\n\tregex: RegExp;\n\thandler: RouteHandler;\n\n\tconstructor(path: string, handler: RouteHandler) {\n\t\tthis.path = path;\n\t\tthis.regex = pathToRegex(path);\n\t\tthis.handler = handler;\n\t}\n}\n\ntype Match = {\n\thandler: RouteHandler;\n\tparams?: Record<string, string>;\n\tquery: URLSearchParams;\n};\n","import { $watch } from \"@torpor/view\";\nimport Page from \"./Page\";\n\nconst $page: Page = $watch(\n\t{\n\t\tstatus: 404,\n\t\turl: new URL(\"http://localhost\"),\n\t\terror: {\n\t\t\tmessage: \"\",\n\t\t},\n\t},\n\t{ shallow: true },\n);\n\nexport default $page;\n","export const PAGE_ROUTE = 0;\nexport const PAGE_SERVER_ROUTE = 1;\nexport const SERVER_ROUTE = 3;\nexport const LAYOUT_ROUTE = 4;\nexport const LAYOUT_SERVER_ROUTE = 5;\nexport const HOOK_ROUTE = 6;\nexport const HOOK_SERVER_ROUTE = 7;\nexport const ERROR_ROUTE = 8;\n"],"mappings":"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","names":["path","path","path"]}
package/dist/test.js CHANGED
@@ -7,15 +7,15 @@ import {
7
7
  PAGE_SERVER_ROUTE,
8
8
  Router,
9
9
  SERVER_ROUTE
10
- } from "./chunk-65K3R3CP.js";
10
+ } from "./chunk-UN2WQH2T.js";
11
11
  import {
12
12
  page_default
13
- } from "./chunk-WMHM5RGZ.js";
13
+ } from "./chunk-565UOT3F.js";
14
14
  import {
15
15
  notFound,
16
16
  ok,
17
17
  seeOther
18
- } from "./chunk-2QQN3D3K.js";
18
+ } from "./chunk-WVXAN4IV.js";
19
19
  import {
20
20
  ServerEvent
21
21
  } from "./chunk-KKFND4HK.js";
@@ -114,7 +114,7 @@ async function loadView(ev, url, handler, params, template) {
114
114
  for (let layout of handler.layouts) {
115
115
  const layoutEndPoint = (await layout.endPoint())?.default;
116
116
  const layoutServerEndPoint = layout.serverEndPoint && (await layout.serverEndPoint())?.default;
117
- const layoutData = await loadClientAndServerData(
117
+ const layoutResponse = await loadClientAndServerData(
118
118
  url,
119
119
  params,
120
120
  serverParams,
@@ -122,10 +122,12 @@ async function loadView(ev, url, handler, params, template) {
122
122
  layoutEndPoint,
123
123
  layoutServerEndPoint
124
124
  );
125
- Object.assign(data, layoutData);
125
+ if (layoutResponse?.ok === false) {
126
+ return layoutResponse;
127
+ }
126
128
  }
127
129
  }
128
- let endPointData = await loadClientAndServerData(
130
+ let endPointResponse = await loadClientAndServerData(
129
131
  url,
130
132
  params,
131
133
  serverParams,
@@ -133,7 +135,9 @@ async function loadView(ev, url, handler, params, template) {
133
135
  clientEndPoint,
134
136
  serverEndPoint
135
137
  );
136
- Object.assign(data, endPointData);
138
+ if (endPointResponse?.ok === false) {
139
+ return endPointResponse;
140
+ }
137
141
  let $props = { data };
138
142
  let component = clientEndPoint.component;
139
143
  let slots = void 0;
@@ -189,21 +193,31 @@ async function runAction(ev, url, handler, params, query) {
189
193
  return notFound();
190
194
  }
191
195
  async function loadClientAndServerData(url, params, serverParams, data, clientEndPoint, serverEndPoint) {
192
- let newData = {};
193
196
  if (clientEndPoint?.load) {
194
197
  const clientParams = buildClientParams(url, data, params);
195
- const clientData = await clientEndPoint.load(clientParams);
196
- if (clientData) {
197
- Object.assign(newData, clientData);
198
+ const clientResponse = await clientEndPoint.load(clientParams);
199
+ if (clientResponse) {
200
+ if (clientResponse.ok) {
201
+ if (clientResponse.headers.get("Content-Type")?.includes("application/json")) {
202
+ Object.assign(data, await clientResponse.json());
203
+ }
204
+ } else {
205
+ return clientResponse;
206
+ }
198
207
  }
199
208
  }
200
209
  if (serverEndPoint?.load) {
201
- const serverData = await serverEndPoint.load(serverParams);
202
- if (serverData?.ok) {
203
- Object.assign(newData, await serverData.json());
210
+ const serverResponse = await serverEndPoint.load(serverParams);
211
+ if (serverResponse) {
212
+ if (serverResponse.ok) {
213
+ if (serverResponse.headers.get("Content-Type")?.includes("application/json")) {
214
+ Object.assign(data, await serverResponse.json());
215
+ }
216
+ } else {
217
+ return serverResponse;
218
+ }
204
219
  }
205
220
  }
206
- return newData;
207
221
  }
208
222
  function buildClientParams(url, params, data) {
209
223
  return {
package/dist/test.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/test/runTest.ts"],"sourcesContent":["import { type ServerComponent, type ServerSlotRender } from \"@torpor/view\";\nimport path from \"node:path\";\nimport notFound from \"../response/notFound.ts\";\nimport ok from \"../response/ok.ts\";\nimport seeOther from \"../response/seeOther.ts\";\nimport ServerEvent from \"../server/ServerEvent.ts\";\nimport Router from \"../site/Router.ts\";\nimport Site from \"../site/Site\";\nimport $page from \"../state/$page.ts\";\nimport type PageEndPoint from \"../types/PageEndPoint.ts\";\nimport type PageServerEndPoint from \"../types/PageServerEndPoint.ts\";\nimport RouteHandler from \"../types/RouteHandler.ts\";\nimport {\n\tHOOK_ROUTE,\n\tHOOK_SERVER_ROUTE,\n\tLAYOUT_ROUTE,\n\tLAYOUT_SERVER_ROUTE,\n\tPAGE_ROUTE,\n\tPAGE_SERVER_ROUTE,\n\tSERVER_ROUTE,\n} from \"../types/RouteType\";\nimport type ServerEndPoint from \"../types/ServerEndPoint.ts\";\nimport type ServerHook from \"../types/ServerHook.ts\";\nimport type ServerLoadEvent from \"../types/ServerLoadEvent.ts\";\n\n/**\n * Runs a site route in test mode\n * @param site The site configuration, with routing configured\n * @param route The route path e.g. `/home`\n * @param ev An optional ServerEvent, which you can use to set test cookies and headers\n * @returns An HTTP response containing the requested data, HTML, or error status\n */\nexport default async function runTest(\n\tsite: Site,\n\troute: string,\n\tev?: ServerEvent,\n): Promise<Response> {\n\tif (!route.startsWith(\"/\")) route += \"/\";\n\tif (!ev) {\n\t\tconst req = new Request(`http://localhost${route}`);\n\t\tev = new ServerEvent(req);\n\t}\n\n\t// Build the router from the Site object created by the user\n\tconst router = new Router();\n\trouter.addPages(\n\t\tsite.routes.map((r) => ({\n\t\t\tpath: r.path,\n\t\t\ttype: r.type,\n\t\t\tendPoint: () => import(/* @vite-ignore */ path.join(site.root, r.file)),\n\t\t})),\n\t);\n\n\treturn await load(router, ev, \"%COMPONENT_HTML%\");\n}\n\n// HACK: This is a copy of serverEntry\n// TODO: Make it shared instead!\nasync function load(router: Router, ev: ServerEvent, template: string): Promise<Response> {\n\t//const url = new URL(`http://${process.env.HOST ?? \"localhost\"}${ev.request.url}`);\n\tconst url = new URL(ev.request.url);\n\tconst path = url.pathname;\n\tconst query = url.searchParams;\n\n\t//console.log(`handling ${ev.request.method} for '${path}'${query.size ? ` with ${query}` : \"\"}`);\n\n\tconst route = router.match(path, query);\n\tif (!route) {\n\t\treturn notFound();\n\t}\n\n\t// Update $page before building the components\n\t// TODO: Find somewhere better to put this\n\t$page.url = url;\n\n\tconst handler = route.handler;\n\tconst params = route.params || {};\n\n\t// TODO: Hit the server hook out here\n\t// We could maybe set data loading up as middleware on a route??????\n\n\tswitch (handler.type) {\n\t\tcase PAGE_ROUTE:\n\t\tcase LAYOUT_ROUTE: {\n\t\t\t// It's a /+page.ts or /_layout.ts endpoint\n\t\t\tif (ev.request.method === \"GET\") {\n\t\t\t\treturn await loadView(ev, url, handler, params, template);\n\t\t\t} else if (ev.request.method === \"POST\") {\n\t\t\t\treturn await runAction(ev, url, handler, params, query);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase PAGE_SERVER_ROUTE:\n\t\tcase LAYOUT_SERVER_ROUTE: {\n\t\t\t// It's a /+page.server.ts or /_layout.server.ts endpoint\n\t\t\tif (ev.request.method === \"GET\") {\n\t\t\t\treturn await loadData(ev, url, handler, \"load\", params);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase SERVER_ROUTE: {\n\t\t\t// It's a /+server.ts endpoint\n\t\t\tconst functionName = ev.request.method.toLowerCase().replace(\"delete\", \"del\");\n\t\t\treturn await loadData(ev, url, handler, functionName, params);\n\t\t}\n\t\tcase HOOK_ROUTE:\n\t\tcase HOOK_SERVER_ROUTE: {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn notFound();\n}\n\nasync function loadData(\n\tev: ServerEvent,\n\turl: URL,\n\thandler: RouteHandler,\n\tfunctionName: string,\n\tparams: Record<string, string>,\n) {\n\tconst serverEndPoint: ServerEndPoint | undefined = (await handler.endPoint()).default;\n\tif (serverEndPoint && serverEndPoint[functionName]) {\n\t\tconst serverParams = buildServerParams(ev, url, params);\n\n\t\tif (handler.serverHook) {\n\t\t\tconst serverHook: ServerHook | undefined = (await handler.serverHook()).default;\n\t\t\tif (serverHook?.handle) {\n\t\t\t\tawait serverHook.handle(serverParams);\n\t\t\t}\n\t\t}\n\n\t\tconst result = await serverEndPoint[functionName](serverParams);\n\n\t\t// If there was no response returned from load (such as errors or a\n\t\t// redirect), send an ok response\n\t\treturn result || ok();\n\t}\n\n\treturn notFound();\n}\n\nasync function loadView(\n\tev: ServerEvent,\n\turl: URL,\n\thandler: RouteHandler,\n\tparams: Record<string, string>,\n\ttemplate: string,\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\treturn notFound();\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\t// Maybe hit the server hook\n\tconst serverParams = buildServerParams(ev, url, params);\n\tif (handler.serverHook) {\n\t\tconst serverHook: ServerHook | undefined = (await handler.serverHook()).default;\n\t\tif (serverHook?.handle) {\n\t\t\tawait serverHook.handle(serverParams);\n\t\t}\n\t}\n\n\t// Pass the data into $props\n\t// TODO: Promise.all\n\t// NOTE: We're loading data from top to bottom, overriding as we go, and I'm not sure if this is the best way to go\n\tlet data = {};\n\tif (handler.layouts) {\n\t\tfor (let layout of handler.layouts) {\n\t\t\tconst layoutEndPoint: PageEndPoint | undefined = (await layout.endPoint())?.default;\n\t\t\tconst layoutServerEndPoint: PageServerEndPoint | undefined =\n\t\t\t\tlayout.serverEndPoint && (await layout.serverEndPoint())?.default;\n\t\t\tconst layoutData = await loadClientAndServerData(\n\t\t\t\turl,\n\t\t\t\tparams,\n\t\t\t\tserverParams,\n\t\t\t\tdata,\n\t\t\t\tlayoutEndPoint,\n\t\t\t\tlayoutServerEndPoint,\n\t\t\t);\n\t\t\tObject.assign(data, layoutData);\n\t\t}\n\t}\n\tlet endPointData = await loadClientAndServerData(\n\t\turl,\n\t\tparams,\n\t\tserverParams,\n\t\tdata,\n\t\tclientEndPoint,\n\t\tserverEndPoint,\n\t);\n\tObject.assign(data, endPointData);\n\tlet $props: Record<string, any> = { data };\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: Also handle layout server data\n\t// TODO: There's probably a nicer way to do this with reducers or something\n\t// TODO: Re-use parent layouts\n\tlet component = clientEndPoint.component as ServerComponent;\n\tlet slots: Record<string, ServerSlotRender> | undefined = undefined;\n\tif (handler.layouts) {\n\t\tlet slotFunctions: ServerSlotRender[] = [];\n\t\tslotFunctions[handler.layouts.length] = (_, context) =>\n\t\t\t(clientEndPoint.component as ServerComponent)($props, context);\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 (i === 0) {\n\t\t\t\t\tcomponent = layoutEndPoint.component as ServerComponent;\n\t\t\t\t\tslots = { _: slotFunctions[i + 1] };\n\t\t\t\t} else {\n\t\t\t\t\tslotFunctions[i] = (_, context) =>\n\t\t\t\t\t\t(layoutEndPoint.component as ServerComponent)($props, context, {\n\t\t\t\t\t\t\t_: slotFunctions[i + 1],\n\t\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tlet html;\n\ttry {\n\t\thtml = component($props, undefined, slots);\n\t\thtml = template.replace(\"%COMPONENT_HTML%\", html);\n\t} catch (error) {\n\t\t// TODO: Show a proper Error component\n\t\thtml = '<span style=\"color: red\">Script syntax error</span><p>' + error + \"</p>\";\n\t\tconsole.log(error);\n\t}\n\n\treturn new Response(html, {\n\t\tstatus: 200,\n\t\theaders: {\n\t\t\t\"Content-Type\": \"text/html\",\n\t\t},\n\t});\n}\n\nasync function runAction(\n\tev: ServerEvent,\n\turl: URL,\n\thandler: RouteHandler,\n\tparams: Record<string, string>,\n\tquery: URLSearchParams,\n) {\n\tconst actionName = (Array.from(query.keys())[0] || \"default\").replace(/^\\//, \"\");\n\tconst serverEndPoint: PageServerEndPoint | undefined =\n\t\thandler.serverEndPoint && (await handler.serverEndPoint()).default;\n\tif (serverEndPoint?.actions) {\n\t\tconst action = serverEndPoint.actions[actionName];\n\t\tif (action) {\n\t\t\t// TODO: form.errors etc\n\t\t\tconst serverParams = buildServerParams(ev, url, params);\n\n\t\t\tif (handler.serverHook) {\n\t\t\t\tconst serverHook: ServerHook | undefined = (await handler.serverHook()).default;\n\t\t\t\tif (serverHook?.handle) {\n\t\t\t\t\tawait serverHook.handle(serverParams);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst result = await action(serverParams); // <--\n\n\t\t\t// If there was no response returned from the action (such as errors\n\t\t\t// or a redirect), reload the page by redirecting\n\t\t\treturn result || seeOther(url.pathname);\n\t\t}\n\t}\n\n\treturn notFound();\n}\n\nasync function loadClientAndServerData(\n\turl: URL,\n\tparams: Record<string, string>,\n\tserverParams: ServerLoadEvent,\n\tdata: Record<string, any>,\n\tclientEndPoint?: PageEndPoint,\n\tserverEndPoint?: PageServerEndPoint,\n) {\n\tlet newData = {};\n\tif (clientEndPoint?.load) {\n\t\tconst clientParams = buildClientParams(url, data, params);\n\t\tconst clientData = await clientEndPoint.load(clientParams);\n\t\tif (clientData) {\n\t\t\tObject.assign(newData, clientData);\n\t\t}\n\t}\n\tif (serverEndPoint?.load) {\n\t\tconst serverData = await serverEndPoint.load(serverParams);\n\t\tif (serverData?.ok) {\n\t\t\tObject.assign(newData, await serverData.json());\n\t\t}\n\t}\n\treturn newData;\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\nfunction buildServerParams(\n\tev: ServerEvent,\n\turl: URL,\n\tparams: Record<string, string>,\n): ServerLoadEvent {\n\treturn {\n\t\turl,\n\t\tparams,\n\t\tappData: {},\n\t\trequest: ev.request,\n\t\tcookies: ev.cookies,\n\t\theaders: ev.headers,\n\t\tadapter: ev.adapter,\n\t};\n}\n"],"mappings":"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","names":["path"]}
1
+ {"version":3,"sources":["../src/test/runTest.ts"],"sourcesContent":["import { type ServerComponent, type ServerSlotRender } from \"@torpor/view\";\nimport path from \"node:path\";\nimport notFound from \"../response/notFound.ts\";\nimport ok from \"../response/ok.ts\";\nimport seeOther from \"../response/seeOther.ts\";\nimport ServerEvent from \"../server/ServerEvent.ts\";\nimport Router from \"../site/Router.ts\";\nimport Site from \"../site/Site\";\nimport $page from \"../state/$page.ts\";\nimport type PageEndPoint from \"../types/PageEndPoint.ts\";\nimport type PageServerEndPoint from \"../types/PageServerEndPoint.ts\";\nimport RouteHandler from \"../types/RouteHandler.ts\";\nimport {\n\tHOOK_ROUTE,\n\tHOOK_SERVER_ROUTE,\n\tLAYOUT_ROUTE,\n\tLAYOUT_SERVER_ROUTE,\n\tPAGE_ROUTE,\n\tPAGE_SERVER_ROUTE,\n\tSERVER_ROUTE,\n} from \"../types/RouteType\";\nimport type ServerEndPoint from \"../types/ServerEndPoint.ts\";\nimport type ServerHook from \"../types/ServerHook.ts\";\nimport type ServerLoadEvent from \"../types/ServerLoadEvent.ts\";\n\n/**\n * Runs a site route in test mode\n * @param site The site configuration, with routing configured\n * @param route The route path e.g. `/home`\n * @param ev An optional ServerEvent, which you can use to set test cookies and headers\n * @returns An HTTP response containing the requested data, HTML, or error status\n */\nexport default async function runTest(\n\tsite: Site,\n\troute: string,\n\tev?: ServerEvent,\n): Promise<Response> {\n\tif (!route.startsWith(\"/\")) route += \"/\";\n\tif (!ev) {\n\t\tconst req = new Request(`http://localhost${route}`);\n\t\tev = new ServerEvent(req);\n\t}\n\n\t// Build the router from the Site object created by the user\n\tconst router = new Router();\n\trouter.addPages(\n\t\tsite.routes.map((r) => ({\n\t\t\tpath: r.path,\n\t\t\ttype: r.type,\n\t\t\tendPoint: () => import(/* @vite-ignore */ path.join(site.root, r.file)),\n\t\t})),\n\t);\n\n\treturn await load(router, ev, \"%COMPONENT_HTML%\");\n}\n\n// HACK: This is a copy of serverEntry\n// TODO: Make it shared instead!\nasync function load(router: Router, ev: ServerEvent, template: string): Promise<Response> {\n\t//const url = new URL(`http://${process.env.HOST ?? \"localhost\"}${ev.request.url}`);\n\tconst url = new URL(ev.request.url);\n\tconst path = url.pathname;\n\tconst query = url.searchParams;\n\n\t//console.log(`handling ${ev.request.method} for '${path}'${query.size ? ` with ${query}` : \"\"}`);\n\n\tconst route = router.match(path, query);\n\tif (!route) {\n\t\treturn notFound();\n\t}\n\n\t// Update $page before building the components\n\t// TODO: Find somewhere better to put this\n\t$page.url = url;\n\n\tconst handler = route.handler;\n\tconst params = route.params || {};\n\n\t// TODO: Hit the server hook out here\n\t// We could maybe set data loading up as middleware on a route??????\n\n\tswitch (handler.type) {\n\t\tcase PAGE_ROUTE:\n\t\tcase LAYOUT_ROUTE: {\n\t\t\t// It's a /+page.ts or /_layout.ts endpoint\n\t\t\tif (ev.request.method === \"GET\") {\n\t\t\t\treturn await loadView(ev, url, handler, params, template);\n\t\t\t} else if (ev.request.method === \"POST\") {\n\t\t\t\treturn await runAction(ev, url, handler, params, query);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase PAGE_SERVER_ROUTE:\n\t\tcase LAYOUT_SERVER_ROUTE: {\n\t\t\t// It's a /+page.server.ts or /_layout.server.ts endpoint\n\t\t\tif (ev.request.method === \"GET\") {\n\t\t\t\treturn await loadData(ev, url, handler, \"load\", params);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase SERVER_ROUTE: {\n\t\t\t// It's a /+server.ts endpoint\n\t\t\tconst functionName = ev.request.method.toLowerCase().replace(\"delete\", \"del\");\n\t\t\treturn await loadData(ev, url, handler, functionName, params);\n\t\t}\n\t\tcase HOOK_ROUTE:\n\t\tcase HOOK_SERVER_ROUTE: {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn notFound();\n}\n\nasync function loadData(\n\tev: ServerEvent,\n\turl: URL,\n\thandler: RouteHandler,\n\tfunctionName: string,\n\tparams: Record<string, string>,\n) {\n\tconst serverEndPoint: ServerEndPoint | undefined = (await handler.endPoint()).default;\n\tif (serverEndPoint && serverEndPoint[functionName]) {\n\t\tconst serverParams = buildServerParams(ev, url, params);\n\n\t\tif (handler.serverHook) {\n\t\t\tconst serverHook: ServerHook | undefined = (await handler.serverHook()).default;\n\t\t\tif (serverHook?.handle) {\n\t\t\t\tawait serverHook.handle(serverParams);\n\t\t\t}\n\t\t}\n\n\t\tconst result = await serverEndPoint[functionName](serverParams);\n\n\t\t// If there was no response returned from load (such as errors or a\n\t\t// redirect), send an ok response\n\t\treturn result || ok();\n\t}\n\n\treturn notFound();\n}\n\nasync function loadView(\n\tev: ServerEvent,\n\turl: URL,\n\thandler: RouteHandler,\n\tparams: Record<string, string>,\n\ttemplate: string,\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\treturn notFound();\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\t// Maybe hit the server hook\n\tconst serverParams = buildServerParams(ev, url, params);\n\tif (handler.serverHook) {\n\t\tconst serverHook: ServerHook | undefined = (await handler.serverHook()).default;\n\t\tif (serverHook?.handle) {\n\t\t\tawait serverHook.handle(serverParams);\n\t\t}\n\t}\n\n\t// Pass the data into $props\n\t// TODO: Promise.all\n\t// NOTE: We're loading data from top to bottom, overriding as we go, and I'm not sure if this is the best way to go\n\tlet data = {};\n\tif (handler.layouts) {\n\t\tfor (let layout of handler.layouts) {\n\t\t\tconst layoutEndPoint: PageEndPoint | undefined = (await layout.endPoint())?.default;\n\t\t\tconst layoutServerEndPoint: PageServerEndPoint | undefined =\n\t\t\t\tlayout.serverEndPoint && (await layout.serverEndPoint())?.default;\n\t\t\tconst layoutResponse = await loadClientAndServerData(\n\t\t\t\turl,\n\t\t\t\tparams,\n\t\t\t\tserverParams,\n\t\t\t\tdata,\n\t\t\t\tlayoutEndPoint,\n\t\t\t\tlayoutServerEndPoint,\n\t\t\t);\n\t\t\tif (layoutResponse?.ok === false) {\n\t\t\t\treturn layoutResponse;\n\t\t\t}\n\t\t}\n\t}\n\tlet endPointResponse = await loadClientAndServerData(\n\t\turl,\n\t\tparams,\n\t\tserverParams,\n\t\tdata,\n\t\tclientEndPoint,\n\t\tserverEndPoint,\n\t);\n\tif (endPointResponse?.ok === false) {\n\t\treturn endPointResponse;\n\t}\n\tlet $props: Record<string, any> = { data };\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: Also handle layout server data\n\t// TODO: There's probably a nicer way to do this with reducers or something\n\t// TODO: Re-use parent layouts\n\tlet component = clientEndPoint.component as ServerComponent;\n\tlet slots: Record<string, ServerSlotRender> | undefined = undefined;\n\tif (handler.layouts) {\n\t\tlet slotFunctions: ServerSlotRender[] = [];\n\t\tslotFunctions[handler.layouts.length] = (_, context) =>\n\t\t\t(clientEndPoint.component as ServerComponent)($props, context);\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 (i === 0) {\n\t\t\t\t\tcomponent = layoutEndPoint.component as ServerComponent;\n\t\t\t\t\tslots = { _: slotFunctions[i + 1] };\n\t\t\t\t} else {\n\t\t\t\t\tslotFunctions[i] = (_, context) =>\n\t\t\t\t\t\t(layoutEndPoint.component as ServerComponent)($props, context, {\n\t\t\t\t\t\t\t_: slotFunctions[i + 1],\n\t\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tlet html;\n\ttry {\n\t\thtml = component($props, undefined, slots);\n\t\thtml = template.replace(\"%COMPONENT_HTML%\", html);\n\t} catch (error) {\n\t\t// TODO: Show a proper Error component\n\t\thtml = '<span style=\"color: red\">Script syntax error</span><p>' + error + \"</p>\";\n\t\tconsole.log(error);\n\t}\n\n\treturn new Response(html, {\n\t\tstatus: 200,\n\t\theaders: {\n\t\t\t\"Content-Type\": \"text/html\",\n\t\t},\n\t});\n}\n\nasync function runAction(\n\tev: ServerEvent,\n\turl: URL,\n\thandler: RouteHandler,\n\tparams: Record<string, string>,\n\tquery: URLSearchParams,\n) {\n\tconst actionName = (Array.from(query.keys())[0] || \"default\").replace(/^\\//, \"\");\n\tconst serverEndPoint: PageServerEndPoint | undefined =\n\t\thandler.serverEndPoint && (await handler.serverEndPoint()).default;\n\tif (serverEndPoint?.actions) {\n\t\tconst action = serverEndPoint.actions[actionName];\n\t\tif (action) {\n\t\t\t// TODO: form.errors etc\n\t\t\tconst serverParams = buildServerParams(ev, url, params);\n\n\t\t\tif (handler.serverHook) {\n\t\t\t\tconst serverHook: ServerHook | undefined = (await handler.serverHook()).default;\n\t\t\t\tif (serverHook?.handle) {\n\t\t\t\t\tawait serverHook.handle(serverParams);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst result = await action(serverParams); // <--\n\n\t\t\t// If there was no response returned from the action (such as errors\n\t\t\t// or a redirect), reload the page by redirecting\n\t\t\treturn result || seeOther(url.pathname);\n\t\t}\n\t}\n\n\treturn notFound();\n}\n\nasync function loadClientAndServerData(\n\turl: URL,\n\tparams: Record<string, string>,\n\tserverParams: ServerLoadEvent,\n\tdata: Record<string, any>,\n\tclientEndPoint?: PageEndPoint,\n\tserverEndPoint?: PageServerEndPoint,\n) {\n\tif (clientEndPoint?.load) {\n\t\tconst clientParams = buildClientParams(url, data, params);\n\t\tconst clientResponse = await clientEndPoint.load(clientParams);\n\t\tif (clientResponse) {\n\t\t\tif (clientResponse.ok) {\n\t\t\t\tif (clientResponse.headers.get(\"Content-Type\")?.includes(\"application/json\")) {\n\t\t\t\t\tObject.assign(data, await clientResponse.json());\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\treturn clientResponse;\n\t\t\t}\n\t\t}\n\t}\n\tif (serverEndPoint?.load) {\n\t\tconst serverResponse = await serverEndPoint.load(serverParams);\n\t\tif (serverResponse) {\n\t\t\tif (serverResponse.ok) {\n\t\t\t\tif (serverResponse.headers.get(\"Content-Type\")?.includes(\"application/json\")) {\n\t\t\t\t\tObject.assign(data, await serverResponse.json());\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\treturn serverResponse;\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\nfunction buildServerParams(\n\tev: ServerEvent,\n\turl: URL,\n\tparams: Record<string, string>,\n): ServerLoadEvent {\n\treturn {\n\t\turl,\n\t\tparams,\n\t\tappData: {},\n\t\trequest: ev.request,\n\t\tcookies: ev.cookies,\n\t\theaders: ev.headers,\n\t\tadapter: ev.adapter,\n\t};\n}\n"],"mappings":"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","names":["path"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torpor/build",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "A framework for building sites and apps with Torpor",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -55,15 +55,15 @@
55
55
  "dotenv": "^16.5.0",
56
56
  "vite": "^6.3.5",
57
57
  "vite-tsconfig-paths": "^5.1.4",
58
- "@torpor/unplugin": "^0.1.11",
59
- "@torpor/view": "^0.1.0"
58
+ "@torpor/view": "^0.1.1",
59
+ "@torpor/unplugin": "^0.1.12"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@trivago/prettier-plugin-sort-imports": "^5.2.2",
63
63
  "tsup": "^8.5.0",
64
64
  "tsx": "^4.19.4",
65
65
  "typescript": "^5.8.3",
66
- "vitest": "^3.1.4"
66
+ "vitest": "^3.2.1"
67
67
  },
68
68
  "scripts": {
69
69
  "build": "tsc --noEmit && tsup",
@@ -10,5 +10,5 @@ import response from "./response";
10
10
  * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400
11
11
  */
12
12
  export default function badRequest(body?: object | string): Response {
13
- return response(400, body);
13
+ return response(400, body ?? "Bad request");
14
14
  }
@@ -10,5 +10,5 @@ import response from "./response";
10
10
  * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403
11
11
  */
12
12
  export default function forbidden(body?: object | string): Response {
13
- return response(403, body);
13
+ return response(403, body ?? "Forbidden");
14
14
  }
@@ -10,5 +10,5 @@ import response from "./response";
10
10
  * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405
11
11
  */
12
12
  export default function methodNotAllowed(body?: object | string): Response {
13
- return response(405, body);
13
+ return response(405, body ?? "Method not allowed");
14
14
  }
@@ -13,5 +13,5 @@ import response from "./response";
13
13
  * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404
14
14
  */
15
15
  export default function notFound(body?: object | string): Response {
16
- return response(404, body);
16
+ return response(404, body ?? "Not found");
17
17
  }
@@ -8,5 +8,5 @@ import response from "./response";
8
8
  * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500
9
9
  */
10
10
  export default function serverError(body?: object | string): Response {
11
- return response(500, body);
11
+ return response(500, body ?? "Server error");
12
12
  }
@@ -10,5 +10,5 @@ import response from "./response";
10
10
  * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401
11
11
  */
12
12
  export default function unauthorized(body?: object | string): Response {
13
- return response(401, body);
13
+ return response(401, body ?? "Unauthorized");
14
14
  }
@@ -8,5 +8,5 @@ import response from "./response";
8
8
  * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422
9
9
  */
10
10
  export default function unprocessable(body?: object | string): Response {
11
- return response(422, body);
11
+ return response(422, body ?? "Unprocessable");
12
12
  }
package/src/site/Site.ts CHANGED
@@ -4,6 +4,7 @@ import { type Plugin, type UserConfig } from "vite";
4
4
  import tsconfigPaths from "vite-tsconfig-paths";
5
5
  import type Adapter from "../types/Adapter";
6
6
  import {
7
+ ERROR_ROUTE,
7
8
  HOOK_ROUTE,
8
9
  HOOK_SERVER_ROUTE,
9
10
  LAYOUT_ROUTE,
@@ -98,6 +99,8 @@ export default class Site {
98
99
  return HOOK_ROUTE;
99
100
  } else if (routePath.endsWith("_hook.server")) {
100
101
  return HOOK_SERVER_ROUTE;
102
+ } else if (routePath.endsWith("_error")) {
103
+ return ERROR_ROUTE;
101
104
  }
102
105
  return -1;
103
106
  }
@@ -60,8 +60,15 @@ async function navigate(url: URL, firstTime = false): Promise<boolean> {
60
60
  }
61
61
 
62
62
  // Update $page before building the components
63
- // TODO: Find somewhere better to put this
64
63
  $page.url = url;
64
+ if (path.endsWith("/_error")) {
65
+ $page.status = parseInt(query.get("status") ?? "404");
66
+ $page.error.message = query.get("message") ?? "";
67
+ // Make it look a bit classier by removing the query
68
+ window.history.replaceState({}, "", url.toString().split("?")[0]);
69
+ } else {
70
+ $page.status = 200;
71
+ }
65
72
 
66
73
  const handler = route.handler;
67
74
  const params = route.params || {};
@@ -90,24 +97,28 @@ async function navigate(url: URL, firstTime = false): Promise<boolean> {
90
97
  const layoutEndPoint: PageEndPoint | undefined = (await layout.endPoint())?.default;
91
98
  const layoutServerEndPoint: PageServerEndPoint | undefined =
92
99
  layout.serverEndPoint && (await layout.serverEndPoint())?.default;
93
- const layoutData = await loadClientAndServerData(
100
+ const layoutResponse = await loadClientAndServerData(
94
101
  data,
95
102
  document.location.origin + layoutPath,
96
103
  params,
97
104
  layoutEndPoint,
98
105
  layoutServerEndPoint,
99
106
  );
100
- Object.assign(data, layoutData);
107
+ if (layoutResponse?.ok === false) {
108
+ return false;
109
+ }
101
110
  }
102
111
  }
103
- let endPointData = await loadClientAndServerData(
112
+ let endPointResponse = await loadClientAndServerData(
104
113
  data,
105
114
  document.location.origin + path,
106
115
  params,
107
116
  clientEndPoint,
108
117
  serverEndPoint,
109
118
  );
110
- Object.assign(data, endPointData);
119
+ if (endPointResponse?.ok === false) {
120
+ return false;
121
+ }
111
122
  let $props: Record<string, any> = { data };
112
123
 
113
124
  // If there are layouts, work our way upwards, pushing each component into
@@ -163,28 +174,34 @@ async function loadClientAndServerData(
163
174
  params: Record<string, string>,
164
175
  clientEndPoint?: PageEndPoint,
165
176
  serverEndPoint?: PageServerEndPoint,
166
- ) {
167
- let newData = {};
177
+ ): Promise<Response | void> {
168
178
  if (clientEndPoint?.load) {
169
179
  const clientUrl = new URL(document.location.href);
170
180
  const clientParams = buildClientParams(clientUrl, params, data);
171
- const clientData = await clientEndPoint.load(clientParams);
172
- if (clientData) {
173
- Object.assign(newData, clientData);
181
+ const clientResponse = await clientEndPoint.load(clientParams);
182
+ if (clientResponse) {
183
+ if (clientResponse.ok) {
184
+ if (clientResponse.headers.get("Content-Type")?.includes("application/json")) {
185
+ Object.assign(data, await clientResponse.json());
186
+ }
187
+ } else {
188
+ return clientResponse;
189
+ }
174
190
  }
175
191
  }
176
192
  if (serverEndPoint?.load) {
177
193
  const serverUrl = location.replace(/\/$/, "") + "/~server";
178
194
  const serverResponse = await fetch(serverUrl);
179
- if (
180
- serverResponse?.ok &&
181
- serverResponse.headers.get("Content-Type")?.includes("application/json")
182
- ) {
183
- const serverData = await serverResponse.json();
184
- Object.assign(newData, serverData);
195
+ if (serverResponse) {
196
+ if (serverResponse.ok) {
197
+ if (serverResponse.headers.get("Content-Type")?.includes("application/json")) {
198
+ Object.assign(data, await serverResponse.json());
199
+ }
200
+ } else {
201
+ return serverResponse;
202
+ }
185
203
  }
186
204
  }
187
- return newData;
188
205
  }
189
206
 
190
207
  function buildClientParams(url: URL, params: Record<string, string>, data: Record<string, any>) {