@torpor/build 0.4.10 → 0.4.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{_page-DBSsnHFG.mjs → _page-BQs4WvCO.mjs} +2 -2
- package/dist/_page-BQs4WvCO.mjs.map +1 -0
- package/dist/nav.mjs +2 -2
- package/dist/nav.mjs.map +1 -1
- package/dist/state.mjs +1 -1
- package/dist/test.mjs +1 -1
- package/package.json +4 -4
- package/src/bin/index.ts +11 -0
- package/src/index.ts +30 -0
- package/src/nav/formSubmit.ts +56 -0
- package/src/nav/load.ts +9 -0
- package/src/nav/loadData.ts +134 -0
- package/src/nav/navigate.ts +164 -0
- package/src/nav/reload.ts +9 -0
- package/src/nav.ts +4 -0
- package/src/response/badRequest.ts +19 -0
- package/src/response/created.ts +20 -0
- package/src/response/forbidden.ts +21 -0
- package/src/response/found.ts +30 -0
- package/src/response/methodNotAllowed.ts +18 -0
- package/src/response/movedPermanently.ts +25 -0
- package/src/response/notFound.ts +24 -0
- package/src/response/notModified.ts +34 -0
- package/src/response/ok.ts +29 -0
- package/src/response/permanentRedirect.ts +30 -0
- package/src/response/response.ts +22 -0
- package/src/response/seeOther.ts +19 -0
- package/src/response/serverError.ts +24 -0
- package/src/response/temporaryRedirect.ts +31 -0
- package/src/response/transfer.ts +14 -0
- package/src/response/unauthorized.ts +21 -0
- package/src/response/unprocessable.ts +18 -0
- package/src/response.ts +37 -0
- package/src/run/prepareTemplate.ts +39 -0
- package/src/run/run.ts +62 -0
- package/src/run/runBuild.ts +77 -0
- package/src/run/runDev.ts +89 -0
- package/src/run/runPreview.ts +93 -0
- package/src/run.ts +6 -0
- package/src/server/CookieHelper.ts +36 -0
- package/src/server/HeaderHelper.ts +25 -0
- package/src/server/Routerx.ts +72 -0
- package/src/server/Server.ts +144 -0
- package/src/server/ServerEvent.ts +39 -0
- package/src/server/connect/bufferToArrayBuffer.ts +8 -0
- package/src/server/connect/connectMiddleware.ts +81 -0
- package/src/server/connect/flattenHeaders.ts +44 -0
- package/src/server/connect/nodeMessageToNodeResponse.ts +89 -0
- package/src/server/connect/readableToBuffer.ts +16 -0
- package/src/server/connect/requestToNodeMessage.ts +26 -0
- package/src/server/contentType.ts +85 -0
- package/src/server/types/HttpMethod.ts +12 -0
- package/src/server/types/MiddlewareFunction.ts +8 -0
- package/src/server/types/ServerFunction.ts +7 -0
- package/src/server.ts +7 -0
- package/src/site/Router.ts +164 -0
- package/src/site/Site.ts +214 -0
- package/src/site/clientEntry.ts +108 -0
- package/src/site/clientEntryDev.ts +3 -0
- package/src/site/defaultAdapter.ts +11 -0
- package/src/site/manifest.ts +54 -0
- package/src/site/serverEntry.ts +417 -0
- package/src/state/$page.ts +24 -0
- package/src/state/client.ts +15 -0
- package/src/state.ts +3 -0
- package/src/test/runTest.ts +350 -0
- package/src/test.ts +3 -0
- package/src/types/Adapter.ts +8 -0
- package/src/types/ClientState.ts +8 -0
- package/src/types/InternalState.ts +7 -0
- package/src/types/LayoutHandler.ts +5 -0
- package/src/types/LayoutPath.ts +10 -0
- package/src/types/ManifestRoute.ts +6 -0
- package/src/types/PageEndPoint.ts +39 -0
- package/src/types/PageLoadEvent.ts +14 -0
- package/src/types/PageServerAction.ts +7 -0
- package/src/types/PageServerEndPoint.ts +16 -0
- package/src/types/PageServerLoad.ts +7 -0
- package/src/types/PageState.ts +8 -0
- package/src/types/Route.ts +11 -0
- package/src/types/RouteHandler.ts +16 -0
- package/src/types/RouteLayoutHandler.ts +5 -0
- package/src/types/RouteMatchResult.ts +7 -0
- package/src/types/RouteType.ts +19 -0
- package/src/types/ServerEndPoint.ts +37 -0
- package/src/types/ServerHook.ts +11 -0
- package/src/types/ServerLoadEvent.ts +37 -0
- package/src/types/ServerRequest.ts +7 -0
- package/src/utils/pathToRegex.ts +15 -0
- package/src/vite-env.d.ts +2 -0
- package/dist/_page-DBSsnHFG.mjs.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { $watch } from "@torpor/view";
|
|
2
2
|
|
|
3
3
|
//#region src/state/$page.ts
|
|
4
|
-
const PAGE_SYMBOL = Symbol.for("
|
|
4
|
+
const PAGE_SYMBOL = Symbol.for("torp.Page");
|
|
5
5
|
const $page = globalThis[PAGE_SYMBOL] ??= $watch({
|
|
6
6
|
status: 404,
|
|
7
7
|
url: new URL("http://localhost"),
|
|
@@ -12,4 +12,4 @@ var $page_default = $page;
|
|
|
12
12
|
|
|
13
13
|
//#endregion
|
|
14
14
|
export { $page_default as t };
|
|
15
|
-
//# sourceMappingURL=_page-
|
|
15
|
+
//# sourceMappingURL=_page-BQs4WvCO.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_page-BQs4WvCO.mjs","names":["PAGE_SYMBOL: unique symbol","$page: PageState"],"sources":["../src/state/$page.ts"],"sourcesContent":["import { $watch } from \"@torpor/view\";\nimport type PageState from \"../types/PageState\";\n\nconst PAGE_SYMBOL: unique symbol = Symbol.for(\"torp.Page\");\n\nconst $page: PageState =\n\t// @ts-ignore\n\t(globalThis[PAGE_SYMBOL] ??=\n\t\t// TODO: Should use the SSR $watch if entering via serverEntry\n\t\t$watch(\n\t\t\t{\n\t\t\t\tstatus: 404,\n\t\t\t\turl: new URL(\"http://localhost\"),\n\t\t\t\tform: undefined,\n\t\t\t\terror: {\n\t\t\t\t\tmessage: \"\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tshallow: true,\n\t\t\t},\n\t\t));\n\nexport default $page;\n"],"mappings":";;;AAGA,MAAMA,cAA6B,OAAO,IAAI,YAAY;AAE1D,MAAMC,QAEJ,WAAW,iBAEX,OACC;CACC,QAAQ;CACR,KAAK,IAAI,IAAI,mBAAmB;CAChC,MAAM;CACN,OAAO,EACN,SAAS,IACT;CACD,EACD,EACC,SAAS,MACT,CACD;AAEH,oBAAe"}
|
package/dist/nav.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { t as $page_default } from "./_page-
|
|
1
|
+
import { t as $page_default } from "./_page-BQs4WvCO.mjs";
|
|
2
2
|
import { clearLayoutSlot, fillLayoutSlot, hydrate, mount } from "@torpor/view";
|
|
3
3
|
|
|
4
4
|
//#region src/state/client.ts
|
|
5
|
-
const CLIENT_SYMBOL = Symbol.for("
|
|
5
|
+
const CLIENT_SYMBOL = Symbol.for("torp.Client");
|
|
6
6
|
const client = globalThis[CLIENT_SYMBOL] ??= {
|
|
7
7
|
router: null,
|
|
8
8
|
layoutStack: [],
|
package/dist/nav.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nav.mjs","names":["CLIENT_SYMBOL: unique symbol","client: ClientState","client","layoutEndPoint: PageEndPoint | undefined","layoutServerEndPoint: PageServerEndPoint | undefined","client","clientEndPoint: PageEndPoint | undefined","serverEndPoint: PageServerEndPoint | undefined","newLayoutStack: LayoutPath[]","form: Record<string, string> | undefined","$props: Record<string, any>","$page","slots: Record<string, SlotRender> | undefined","slotFunctions: SlotRender[]","parent","layoutEndPoint: PageEndPoint | undefined","$context"],"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(\"t_client_state\");\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 (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 } 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\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// Set the parent to add the new content to (from the old\n\t\t\t\t\t// content), clear the range under this point, and set the\n\t\t\t\t\t// component to the slot function within this layout\n\t\t\t\t\tparent = layoutStack[i].slotRegion.startNode.parentNode as HTMLElement;\n\t\t\t\t\tclearLayoutSlot(layoutStack[i].slotRegion);\n\t\t\t\t\tcomponent = slotFunctions[i + 1];\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\tif (withHydration) {\n\t\thydrate(parent, component, $props, slots);\n\t} else {\n\t\ttry {\n\t\t\tmount(parent, component, $props, slots);\n\t\t} catch (error) {\n\t\t\t// TODO: Show a proper Error component\n\t\t\tparent.innerHTML = '<span style=\"color: red\">Script syntax error</span><p>' + error + \"</p>\";\n\t\t\tconsole.log(error);\n\t\t}\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":"nav.mjs","names":["CLIENT_SYMBOL: unique symbol","client: ClientState","client","layoutEndPoint: PageEndPoint | undefined","layoutServerEndPoint: PageServerEndPoint | undefined","client","clientEndPoint: PageEndPoint | undefined","serverEndPoint: PageServerEndPoint | undefined","newLayoutStack: LayoutPath[]","form: Record<string, string> | undefined","$props: Record<string, any>","$page","slots: Record<string, SlotRender> | undefined","slotFunctions: SlotRender[]","parent","layoutEndPoint: PageEndPoint | undefined","$context"],"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 (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 } 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\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// Set the parent to add the new content to (from the old\n\t\t\t\t\t// content), clear the range under this point, and set the\n\t\t\t\t\t// component to the slot function within this layout\n\t\t\t\t\tparent = layoutStack[i].slotRegion.startNode.parentNode as HTMLElement;\n\t\t\t\t\tclearLayoutSlot(layoutStack[i].slotRegion);\n\t\t\t\t\tcomponent = slotFunctions[i + 1];\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\tif (withHydration) {\n\t\thydrate(parent, component, $props, slots);\n\t} else {\n\t\ttry {\n\t\t\tmount(parent, component, $props, slots);\n\t\t} catch (error) {\n\t\t\t// TODO: Show a proper Error component\n\t\t\tparent.innerHTML = '<span style=\"color: red\">Script syntax error</span><p>' + error + \"</p>\";\n\t\t\tconsole.log(error);\n\t\t}\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/state.mjs
CHANGED
package/dist/test.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "./pathToRegex-TUAMc3zS.mjs";
|
|
2
2
|
import { a as LAYOUT_SERVER_ROUTE, c as SERVER_ROUTE, i as LAYOUT_ROUTE, l as Router, n as HOOK_ROUTE, o as PAGE_ROUTE, r as HOOK_SERVER_ROUTE, s as PAGE_SERVER_ROUTE } from "./RouteType-B6BMOyYw.mjs";
|
|
3
|
-
import { t as $page_default } from "./_page-
|
|
3
|
+
import { t as $page_default } from "./_page-BQs4WvCO.mjs";
|
|
4
4
|
import { n as ok, r as notFound, t as seeOther } from "./seeOther-B4Yhu9iq.mjs";
|
|
5
5
|
import { t as ServerEvent } from "./ServerEvent-CxPwA4lH.mjs";
|
|
6
6
|
import path from "node:path";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@torpor/build",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.12",
|
|
4
4
|
"description": "A framework for building sites and apps with Torpor",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"files": [
|
|
40
40
|
"dist",
|
|
41
|
-
"
|
|
41
|
+
"src"
|
|
42
42
|
],
|
|
43
43
|
"publishConfig": {
|
|
44
44
|
"access": "public"
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"dotenv": "^17.2.3",
|
|
55
55
|
"vite": "^7.2.2",
|
|
56
56
|
"vite-tsconfig-paths": "^5.1.4",
|
|
57
|
-
"@torpor/
|
|
58
|
-
"@torpor/
|
|
57
|
+
"@torpor/unplugin": "^0.2.1",
|
|
58
|
+
"@torpor/view": "^0.4.18"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@trivago/prettier-plugin-sort-imports": "^6.0.0",
|
package/src/bin/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#! /usr/bin/env node
|
|
2
|
+
import run from "../run/run";
|
|
3
|
+
|
|
4
|
+
const workingDir = process.cwd();
|
|
5
|
+
if (process.argv.includes("--dev")) {
|
|
6
|
+
await run(workingDir, "dev");
|
|
7
|
+
} else if (process.argv.includes("--build")) {
|
|
8
|
+
await run(workingDir, "build");
|
|
9
|
+
} else if (process.argv.includes("--preview")) {
|
|
10
|
+
await run(workingDir, "preview");
|
|
11
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import Router from "./site/Router";
|
|
2
|
+
import Site from "./site/Site";
|
|
3
|
+
import type Adapter from "./types/Adapter";
|
|
4
|
+
import type PageEndPoint from "./types/PageEndPoint";
|
|
5
|
+
import type PageLoadEvent from "./types/PageLoadEvent";
|
|
6
|
+
import type PageServerAction from "./types/PageServerAction";
|
|
7
|
+
import type PageServerEndPoint from "./types/PageServerEndPoint";
|
|
8
|
+
import type PageServerLoad from "./types/PageServerLoad";
|
|
9
|
+
import type ServerEndPoint from "./types/ServerEndPoint";
|
|
10
|
+
import type ServerHook from "./types/ServerHook";
|
|
11
|
+
import type ServerLoadEvent from "./types/ServerLoadEvent";
|
|
12
|
+
import type ServerRequest from "./types/ServerRequest";
|
|
13
|
+
|
|
14
|
+
// NOTE: Don't export any general functionality from here -- it may result in
|
|
15
|
+
// Vite errors like `Could not resolve '../pkg' in lightningcss` etc
|
|
16
|
+
|
|
17
|
+
export { Site, Router };
|
|
18
|
+
|
|
19
|
+
export type {
|
|
20
|
+
Adapter,
|
|
21
|
+
PageEndPoint,
|
|
22
|
+
PageServerEndPoint,
|
|
23
|
+
PageServerLoad,
|
|
24
|
+
PageServerAction,
|
|
25
|
+
ServerEndPoint,
|
|
26
|
+
ServerRequest,
|
|
27
|
+
ServerHook,
|
|
28
|
+
PageLoadEvent,
|
|
29
|
+
ServerLoadEvent,
|
|
30
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import $page from "../state/$page";
|
|
2
|
+
import load from "./load";
|
|
3
|
+
import reload from "./reload";
|
|
4
|
+
|
|
5
|
+
export default async function formSubmit(e: SubmitEvent): Promise<void> {
|
|
6
|
+
// We'll take it from here
|
|
7
|
+
e.preventDefault();
|
|
8
|
+
|
|
9
|
+
// Reset $page on submit and link navigation
|
|
10
|
+
// TODO: Should we put data in $page.data??
|
|
11
|
+
$page.form = undefined;
|
|
12
|
+
|
|
13
|
+
// Perform a POST fetch to the form's action location
|
|
14
|
+
const form = e.target as HTMLFormElement;
|
|
15
|
+
const button = e.submitter as HTMLButtonElement;
|
|
16
|
+
// HACK: submit buttons have their formAction set to the current page if not
|
|
17
|
+
// set, so we can't just check whether it's empty and fallback to form action
|
|
18
|
+
//const action = button?.formAction ?? form.action;
|
|
19
|
+
let action = form.action || document.URL;
|
|
20
|
+
if (button && button.formAction && button.formAction !== document.URL) {
|
|
21
|
+
action = button.formAction;
|
|
22
|
+
}
|
|
23
|
+
const options = {
|
|
24
|
+
body: new FormData(form),
|
|
25
|
+
method: "POST",
|
|
26
|
+
mode: "cors",
|
|
27
|
+
credentials: "same-origin",
|
|
28
|
+
// Add a special header which tells the server that we want the raw
|
|
29
|
+
// error response (if applicable) rather than the reloaded page that you
|
|
30
|
+
// would get if submitting a page without javascript
|
|
31
|
+
headers: {
|
|
32
|
+
"x-torpor-form-submit": "",
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
const response = await fetch(
|
|
36
|
+
action,
|
|
37
|
+
// @ts-ignore -- don't know what it's complaining about here
|
|
38
|
+
options,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (response.status === 200 && response.headers.has("x-torpor-form-redirect")) {
|
|
42
|
+
// TODO: Should do the correct type of redirect e.g. seeOther etc
|
|
43
|
+
const redirect = response.headers.get("location")!;
|
|
44
|
+
await load(redirect);
|
|
45
|
+
window.location.href = redirect;
|
|
46
|
+
} else if (
|
|
47
|
+
(response.status >= 200 && response.status <= 299) ||
|
|
48
|
+
(response.status >= 400 && response.status <= 499)
|
|
49
|
+
) {
|
|
50
|
+
// Put the response into `$page.form`, and it will be put into `$props` in reload
|
|
51
|
+
if (response.headers.get("content-type") === "application/json") {
|
|
52
|
+
$page.form = await response.json();
|
|
53
|
+
}
|
|
54
|
+
await reload();
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/nav/load.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import client from "../state/client";
|
|
2
|
+
import type LayoutPath from "../types/LayoutPath";
|
|
3
|
+
import type PageEndPoint from "../types/PageEndPoint";
|
|
4
|
+
import type PageServerEndPoint from "../types/PageServerEndPoint";
|
|
5
|
+
import type RouteHandler from "../types/RouteHandler";
|
|
6
|
+
|
|
7
|
+
export default async function loadData(
|
|
8
|
+
handler: RouteHandler,
|
|
9
|
+
params: Record<string, string>,
|
|
10
|
+
path: string,
|
|
11
|
+
query: URLSearchParams,
|
|
12
|
+
newLayoutStack: LayoutPath[],
|
|
13
|
+
clientEndPoint: PageEndPoint | undefined,
|
|
14
|
+
serverEndPoint: PageServerEndPoint | undefined,
|
|
15
|
+
prefetch = false,
|
|
16
|
+
): Promise<Record<string, string> | void> {
|
|
17
|
+
let data = {};
|
|
18
|
+
if (handler.layouts) {
|
|
19
|
+
let layoutStack = client.layoutStack;
|
|
20
|
+
for (let [i, layout] of handler.layouts.entries()) {
|
|
21
|
+
let layoutPath = layout.path;
|
|
22
|
+
for (let key in params) {
|
|
23
|
+
layoutPath = layoutPath.replace(`[${key}]`, params[key]);
|
|
24
|
+
}
|
|
25
|
+
if (layoutStack.at(i)?.path === layoutPath) {
|
|
26
|
+
// We've already loaded this layout, we can just re-use its data and UI
|
|
27
|
+
if (!prefetch) {
|
|
28
|
+
layoutStack[i].reuse = true;
|
|
29
|
+
}
|
|
30
|
+
newLayoutStack[i] = layoutStack[i];
|
|
31
|
+
Object.assign(data, layoutStack[i].data);
|
|
32
|
+
} else {
|
|
33
|
+
const stackLayout = { path: layoutPath, data: {}, reuse: false, slotRegion: null };
|
|
34
|
+
const layoutEndPoint: PageEndPoint | undefined = (await layout.endPoint())?.default;
|
|
35
|
+
const layoutServerEndPoint: PageServerEndPoint | undefined =
|
|
36
|
+
layout.serverEndPoint && (await layout.serverEndPoint())?.default;
|
|
37
|
+
const layoutResponse = await loadClientAndServerData(
|
|
38
|
+
stackLayout.data,
|
|
39
|
+
document.location.origin + layoutPath,
|
|
40
|
+
query,
|
|
41
|
+
params,
|
|
42
|
+
layoutEndPoint,
|
|
43
|
+
layoutServerEndPoint,
|
|
44
|
+
);
|
|
45
|
+
if (layoutResponse?.ok === false) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
Object.assign(data, stackLayout.data);
|
|
49
|
+
newLayoutStack.push(stackLayout);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
let endPointResponse = await loadClientAndServerData(
|
|
54
|
+
data,
|
|
55
|
+
document.location.origin + path,
|
|
56
|
+
query,
|
|
57
|
+
params,
|
|
58
|
+
clientEndPoint,
|
|
59
|
+
serverEndPoint,
|
|
60
|
+
);
|
|
61
|
+
if (endPointResponse?.ok === false) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
return data;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function loadClientAndServerData(
|
|
68
|
+
data: Record<string, any>,
|
|
69
|
+
location: string,
|
|
70
|
+
query: URLSearchParams,
|
|
71
|
+
params: Record<string, string>,
|
|
72
|
+
clientEndPoint?: PageEndPoint,
|
|
73
|
+
serverEndPoint?: PageServerEndPoint,
|
|
74
|
+
): Promise<Response | undefined | void> {
|
|
75
|
+
let prefetchedData = client.prefetchedData;
|
|
76
|
+
|
|
77
|
+
if (clientEndPoint?.load) {
|
|
78
|
+
const clientKey = location + (query.size > 0 ? `?${query}` : "");
|
|
79
|
+
if (prefetchedData[clientKey]) {
|
|
80
|
+
Object.assign(data, prefetchedData[clientKey]);
|
|
81
|
+
} else {
|
|
82
|
+
const clientUrl = new URL(document.location.href);
|
|
83
|
+
for (let [name, value] of query) {
|
|
84
|
+
clientUrl.searchParams.append(name, value);
|
|
85
|
+
}
|
|
86
|
+
const clientParams = buildClientParams(clientUrl, params, data);
|
|
87
|
+
const clientResponse = await clientEndPoint.load(clientParams);
|
|
88
|
+
if (clientResponse) {
|
|
89
|
+
if (clientResponse.ok) {
|
|
90
|
+
if (clientResponse.headers.get("Content-Type")?.includes("application/json")) {
|
|
91
|
+
const clientData = await clientResponse.json();
|
|
92
|
+
Object.assign(data, clientData);
|
|
93
|
+
prefetchedData[clientKey] = clientData;
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
return clientResponse;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (serverEndPoint?.load) {
|
|
103
|
+
const serverLocation = location.replace(/\/$/, "") + "/~server";
|
|
104
|
+
const serverKey = serverLocation + (query.size > 0 ? `?${query}` : "");
|
|
105
|
+
if (prefetchedData[serverKey]) {
|
|
106
|
+
Object.assign(data, prefetchedData[serverKey]);
|
|
107
|
+
} else {
|
|
108
|
+
const serverUrl = new URL(serverLocation);
|
|
109
|
+
for (let [name, value] of query) {
|
|
110
|
+
serverUrl.searchParams.append(name, value);
|
|
111
|
+
}
|
|
112
|
+
const serverResponse = await fetch(serverUrl);
|
|
113
|
+
if (serverResponse) {
|
|
114
|
+
if (serverResponse.ok) {
|
|
115
|
+
if (serverResponse.headers.get("Content-Type")?.includes("application/json")) {
|
|
116
|
+
const serverData = await serverResponse.json();
|
|
117
|
+
Object.assign(data, serverData);
|
|
118
|
+
prefetchedData[serverKey] = serverData;
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
return serverResponse;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function buildClientParams(url: URL, params: Record<string, string>, data: Record<string, any>) {
|
|
129
|
+
return {
|
|
130
|
+
url,
|
|
131
|
+
params,
|
|
132
|
+
data,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { clearLayoutSlot, fillLayoutSlot, hydrate } from "@torpor/view";
|
|
2
|
+
import { type Component, type SlotRender } from "@torpor/view";
|
|
3
|
+
import { mount } from "@torpor/view";
|
|
4
|
+
import $page from "../state/$page";
|
|
5
|
+
import client from "../state/client";
|
|
6
|
+
import type LayoutPath from "../types/LayoutPath";
|
|
7
|
+
import type PageEndPoint from "../types/PageEndPoint";
|
|
8
|
+
import type PageServerEndPoint from "../types/PageServerEndPoint";
|
|
9
|
+
import formSubmit from "./formSubmit";
|
|
10
|
+
import loadData from "./loadData";
|
|
11
|
+
|
|
12
|
+
// @ts-ignore
|
|
13
|
+
export default async function navigate(url: URL, withHydration = false): Promise<boolean> {
|
|
14
|
+
let parent = document.getElementById("app");
|
|
15
|
+
if (!parent) {
|
|
16
|
+
// TODO: 500
|
|
17
|
+
console.log("500");
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const path = url.pathname;
|
|
22
|
+
const query = url.searchParams;
|
|
23
|
+
|
|
24
|
+
//console.log(`navigating to '${path}'${query.size ? ` with ${query}` : ""}`);
|
|
25
|
+
|
|
26
|
+
const route = client.router.match(path, query);
|
|
27
|
+
if (!route) {
|
|
28
|
+
// TODO: 404
|
|
29
|
+
console.log("404");
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Update $page before building the components
|
|
34
|
+
$page.url = url;
|
|
35
|
+
if (path.endsWith("/_error")) {
|
|
36
|
+
$page.status = parseInt(query.get("status") ?? "404");
|
|
37
|
+
$page.error = { message: query.get("message") ?? "" };
|
|
38
|
+
// Make it look a bit classier by removing the query
|
|
39
|
+
window.history.replaceState({}, "", url.toString().split("?")[0]);
|
|
40
|
+
} else {
|
|
41
|
+
$page.status = 200;
|
|
42
|
+
}
|
|
43
|
+
const handler = route.handler;
|
|
44
|
+
const params = route.params || {};
|
|
45
|
+
|
|
46
|
+
// There must be a client endpoint with a component
|
|
47
|
+
const clientEndPoint: PageEndPoint | undefined = (await handler.endPoint()).default;
|
|
48
|
+
if (!clientEndPoint?.component) {
|
|
49
|
+
// TODO: 404
|
|
50
|
+
console.log("404");
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// There may be a server endpoint
|
|
55
|
+
const serverEndPoint: PageServerEndPoint | undefined =
|
|
56
|
+
handler.serverEndPoint && (await handler.serverEndPoint())?.default;
|
|
57
|
+
|
|
58
|
+
let newLayoutStack: LayoutPath[] = [];
|
|
59
|
+
|
|
60
|
+
// Pass the data into $props
|
|
61
|
+
// TODO: Don't load if this is the first time -- it should have been passed
|
|
62
|
+
// to us, somehow...
|
|
63
|
+
const data = await loadData(
|
|
64
|
+
handler,
|
|
65
|
+
params,
|
|
66
|
+
path,
|
|
67
|
+
query,
|
|
68
|
+
newLayoutStack,
|
|
69
|
+
clientEndPoint,
|
|
70
|
+
serverEndPoint,
|
|
71
|
+
);
|
|
72
|
+
// We may have form data in a hidden input -- not sure if this is the best
|
|
73
|
+
// way to do it
|
|
74
|
+
let formInput = document.getElementById("t-form-data") as HTMLInputElement;
|
|
75
|
+
let form: Record<string, string> | undefined;
|
|
76
|
+
if (formInput) {
|
|
77
|
+
form = JSON.parse(formInput.value);
|
|
78
|
+
$page.form = form;
|
|
79
|
+
formInput.remove();
|
|
80
|
+
}
|
|
81
|
+
let $props: Record<string, any> = { data };
|
|
82
|
+
|
|
83
|
+
// We may have form data in $page.form, either from the hidden input
|
|
84
|
+
// (above), or added via onformsubmit (below)
|
|
85
|
+
$props.form = $page.form;
|
|
86
|
+
|
|
87
|
+
// Add some special context for submitting Forms client-side
|
|
88
|
+
const $context = {
|
|
89
|
+
TorporBuildContext: { onformsubmit: formSubmit },
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
client.layoutStack.push({ path: route.handler.path, data: {}, reuse: false, slotRegion: null });
|
|
93
|
+
client.layoutStack = newLayoutStack;
|
|
94
|
+
let layoutStack = client.layoutStack;
|
|
95
|
+
|
|
96
|
+
// If there are layouts, work our way upwards, pushing each component into
|
|
97
|
+
// the default slot of its parent
|
|
98
|
+
// TODO: There's probably a nicer way to do this with reducers or something
|
|
99
|
+
let component = clientEndPoint.component as Component;
|
|
100
|
+
let slots: Record<string, SlotRender> | undefined = undefined;
|
|
101
|
+
if (handler.layouts) {
|
|
102
|
+
let slotFunctions: SlotRender[] = [];
|
|
103
|
+
// The last slot function will render the client component
|
|
104
|
+
slotFunctions[handler.layouts.length] = function clientComponent(parent, anchor) {
|
|
105
|
+
let i = layoutStack.length - 1;
|
|
106
|
+
layoutStack[i].slotRegion = fillLayoutSlot(
|
|
107
|
+
clientEndPoint.component!,
|
|
108
|
+
slotFunctions[i + 1],
|
|
109
|
+
parent,
|
|
110
|
+
anchor,
|
|
111
|
+
$props,
|
|
112
|
+
$context,
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Each earlier slot function will render a layout
|
|
117
|
+
for (let i = handler.layouts.length - 1; i >= 0; i--) {
|
|
118
|
+
const layoutEndPoint: PageEndPoint | undefined = (await handler.layouts[i].endPoint())
|
|
119
|
+
?.default;
|
|
120
|
+
if (layoutEndPoint?.component) {
|
|
121
|
+
if (layoutStack[i].reuse) {
|
|
122
|
+
// Set the parent to add the new content to (from the old
|
|
123
|
+
// content), clear the range under this point, and set the
|
|
124
|
+
// component to the slot function within this layout
|
|
125
|
+
parent = layoutStack[i].slotRegion.startNode.parentNode as HTMLElement;
|
|
126
|
+
clearLayoutSlot(layoutStack[i].slotRegion);
|
|
127
|
+
component = slotFunctions[i + 1];
|
|
128
|
+
break;
|
|
129
|
+
} else if (i === 0) {
|
|
130
|
+
component = layoutEndPoint.component as Component;
|
|
131
|
+
slots = { _: slotFunctions[i + 1] };
|
|
132
|
+
} else {
|
|
133
|
+
slotFunctions[i] = function layoutComponent(parent, anchor, _, $context) {
|
|
134
|
+
layoutStack[i - 1].slotRegion = fillLayoutSlot(
|
|
135
|
+
layoutEndPoint.component!,
|
|
136
|
+
slotFunctions[i + 1],
|
|
137
|
+
parent,
|
|
138
|
+
anchor,
|
|
139
|
+
$props,
|
|
140
|
+
$context,
|
|
141
|
+
);
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (withHydration) {
|
|
149
|
+
hydrate(parent, component, $props, slots);
|
|
150
|
+
} else {
|
|
151
|
+
try {
|
|
152
|
+
mount(parent, component, $props, slots);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
// TODO: Show a proper Error component
|
|
155
|
+
parent.innerHTML = '<span style="color: red">Script syntax error</span><p>' + error + "</p>";
|
|
156
|
+
console.log(error);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Reset prefetched data on each navigation
|
|
161
|
+
client.prefetchedData = {};
|
|
162
|
+
|
|
163
|
+
return true;
|
|
164
|
+
}
|
package/src/nav.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import response from "./response";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 400 Bad Request
|
|
5
|
+
*
|
|
6
|
+
* The HTTP 400 Bad Request client error response status code indicates that the
|
|
7
|
+
* server would not process the request due to something the server considered
|
|
8
|
+
* to be a client error. The reason for a 400 response is typically due to
|
|
9
|
+
* malformed request syntax, invalid request message framing, or deceptive
|
|
10
|
+
* request routing.
|
|
11
|
+
*
|
|
12
|
+
* Clients that receive a 400 response should expect that repeating the request
|
|
13
|
+
* without modification will fail with the same error.
|
|
14
|
+
*
|
|
15
|
+
* See https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/400
|
|
16
|
+
*/
|
|
17
|
+
export default function badRequest(body?: object | string): Response {
|
|
18
|
+
return response(400, body ?? "Bad request");
|
|
19
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import response from "./response";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 201 Created
|
|
5
|
+
*
|
|
6
|
+
* The HTTP 201 Created successful response status code indicates that the HTTP
|
|
7
|
+
* request has led to the creation of a resource. This status code is commonly
|
|
8
|
+
* sent as the result of a POST request.
|
|
9
|
+
*
|
|
10
|
+
* The new resource, or a description and link to the new resource, is created
|
|
11
|
+
* before the response is returned. The newly-created items can be returned in
|
|
12
|
+
* the body of the response message, but must be locatable by the URL of the
|
|
13
|
+
* initiating request or by the URL in the value of the Location header provided
|
|
14
|
+
* with the response.
|
|
15
|
+
*
|
|
16
|
+
* See https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/201
|
|
17
|
+
*/
|
|
18
|
+
export default function created(body?: object | string): Response {
|
|
19
|
+
return response(201, body);
|
|
20
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import response from "./response";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 403 Forbidden
|
|
5
|
+
*
|
|
6
|
+
* The HTTP 403 Forbidden client error response status code indicates that the
|
|
7
|
+
* server understood the request but refused to process it. This status is
|
|
8
|
+
* similar to 401, except that for 403 Forbidden responses, authenticating or
|
|
9
|
+
* re-authenticating makes no difference. The request failure is tied to
|
|
10
|
+
* application logic, such as insufficient permissions to a resource or action.
|
|
11
|
+
*
|
|
12
|
+
* Clients that receive a 403 response should expect that repeating the request
|
|
13
|
+
* without modification will fail with the same error. Server owners may decide
|
|
14
|
+
* to send a 404 response instead of a 403 if acknowledging the existence of a
|
|
15
|
+
* resource to clients with insufficient privileges is not desired.
|
|
16
|
+
*
|
|
17
|
+
* See https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/403
|
|
18
|
+
*/
|
|
19
|
+
export default function forbidden(body?: object | string): Response {
|
|
20
|
+
return response(403, body ?? "Forbidden");
|
|
21
|
+
}
|