@torpor/build 1.0.0 → 1.0.1
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/{flattenHeaders-C_YYLdOq.mjs → Server-BnjLJleZ.mjs} +2 -12
- package/dist/Server-BnjLJleZ.mjs.map +1 -0
- package/dist/{Server-C_IKav_e.d.mts → Server-Cler2w5O.d.mts} +3 -48
- package/dist/Server-Cler2w5O.d.mts.map +1 -0
- package/dist/ServerEvent-CTx3Q-hk.d.mts +48 -0
- package/dist/ServerEvent-CTx3Q-hk.d.mts.map +1 -0
- package/dist/{Site-CMa153FA.d.mts → Site-CE0_o5Ci.d.mts} +3 -2
- package/dist/Site-CE0_o5Ci.d.mts.map +1 -0
- package/dist/bin/index.js +1 -1
- package/dist/clientEntry.d.mts +1 -0
- package/dist/clientEntry.mjs +59 -0
- package/dist/clientEntry.mjs.map +1 -0
- package/dist/clientEntryDev.d.mts +1 -0
- package/dist/clientEntryDev.mjs +7 -0
- package/dist/clientEntryDev.mjs.map +1 -0
- package/dist/dev.d.mts +3 -0
- package/dist/dev.mjs +3 -0
- package/dist/endpoint-EULRGCKA.mjs +108 -0
- package/dist/endpoint-EULRGCKA.mjs.map +1 -0
- package/dist/flattenHeaders-DM5qsSqO.mjs +13 -0
- package/dist/flattenHeaders-DM5qsSqO.mjs.map +1 -0
- package/dist/index.d.mts +1 -1
- package/dist/load-BntzvmwL.mjs +228 -0
- package/dist/load-BntzvmwL.mjs.map +1 -0
- package/dist/nav.mjs +1 -225
- package/dist/nav.mjs.map +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/run.d.mts +1 -1
- package/dist/run.d.mts.map +1 -1
- package/dist/run.mjs +1 -1
- package/dist/{runOpenApi-BUi1RH1l.mjs → runOpenApi-DlOgF2Il.mjs} +54 -16
- package/dist/runOpenApi-DlOgF2Il.mjs.map +1 -0
- package/dist/server/Server.d.mts +2 -0
- package/dist/server/Server.mjs +2 -0
- package/dist/server.d.mts +2 -1
- package/dist/server.d.mts.map +1 -1
- package/dist/server.mjs +2 -1
- package/dist/server.mjs.map +1 -1
- package/dist/serverEntry-1qY3wZig.mjs +351 -0
- package/dist/serverEntry-1qY3wZig.mjs.map +1 -0
- package/dist/serverEntry-BAO8m03V.d.mts +6 -0
- package/dist/serverEntry-BAO8m03V.d.mts.map +1 -0
- package/dist/serverEntry.d.mts +2 -0
- package/dist/serverEntry.mjs +2 -0
- package/dist/test.d.mts +2 -2
- package/dist/test.mjs +2 -104
- package/dist/test.mjs.map +1 -1
- package/package.json +9 -8
- package/dist/Server-C_IKav_e.d.mts.map +0 -1
- package/dist/Site-CMa153FA.d.mts.map +0 -1
- package/dist/flattenHeaders-C_YYLdOq.mjs.map +0 -1
- package/dist/runOpenApi-BUi1RH1l.mjs.map +0 -1
- package/src/bin/index.ts +0 -22
- package/src/dev.ts +0 -10
- package/src/form/formDataToRecord.ts +0 -23
- package/src/form/readForm.ts +0 -71
- package/src/form.ts +0 -4
- package/src/index.ts +0 -82
- package/src/nav/api.test-d.ts +0 -79
- package/src/nav/api.ts +0 -104
- package/src/nav/formSubmit.ts +0 -70
- package/src/nav/load.ts +0 -9
- package/src/nav/loadData.ts +0 -134
- package/src/nav/navigate.ts +0 -189
- package/src/nav/reload.ts +0 -9
- package/src/nav/route.ts +0 -38
- package/src/nav.ts +0 -6
- package/src/openapi/docsHtml.ts +0 -25
- package/src/openapi/document.ts +0 -154
- package/src/openapi/plugin.ts +0 -78
- package/src/openapi/types.ts +0 -82
- package/src/openapi.ts +0 -14
- package/src/response/TypedResponse.ts +0 -17
- package/src/response/badRequest.ts +0 -31
- package/src/response/created.ts +0 -31
- package/src/response/forbidden.ts +0 -21
- package/src/response/found.ts +0 -30
- package/src/response/methodNotAllowed.ts +0 -18
- package/src/response/movedPermanently.ts +0 -25
- package/src/response/notFound.ts +0 -24
- package/src/response/notModified.ts +0 -34
- package/src/response/ok.ts +0 -38
- package/src/response/permanentRedirect.ts +0 -30
- package/src/response/response.ts +0 -22
- package/src/response/seeOther.ts +0 -19
- package/src/response/serverError.ts +0 -24
- package/src/response/temporaryRedirect.ts +0 -31
- package/src/response/transfer.ts +0 -14
- package/src/response/unauthorized.ts +0 -21
- package/src/response/unprocessable.ts +0 -30
- package/src/response.ts +0 -37
- package/src/run/depCache.ts +0 -175
- package/src/run/devPlugin.ts +0 -128
- package/src/run/prepareTemplate.ts +0 -42
- package/src/run/run.ts +0 -86
- package/src/run/runBuild.ts +0 -128
- package/src/run/runDev.ts +0 -177
- package/src/run/runOpenApi.ts +0 -52
- package/src/run/runPreview.ts +0 -84
- package/src/run/staleTorpCopies.ts +0 -100
- package/src/run.ts +0 -7
- package/src/schema.ts +0 -7
- package/src/server/CookieHelper.ts +0 -46
- package/src/server/HeaderHelper.ts +0 -25
- package/src/server/Server.ts +0 -168
- package/src/server/ServerEvent.ts +0 -62
- package/src/server/connect/connectMiddleware.ts +0 -85
- package/src/server/connect/flattenHeaders.ts +0 -22
- package/src/server/connect/nodeMessageToNodeResponse.ts +0 -89
- package/src/server/connect/requestToNodeMessage.ts +0 -29
- package/src/server/contentType.ts +0 -85
- package/src/server/types/HttpMethod.ts +0 -12
- package/src/server/types/MiddlewareFunction.ts +0 -22
- package/src/server/types/ServerFunction.ts +0 -7
- package/src/server.ts +0 -7
- package/src/site/Router.ts +0 -175
- package/src/site/Site.ts +0 -388
- package/src/site/checkLayoutSlots.ts +0 -114
- package/src/site/checkRoutes.ts +0 -435
- package/src/site/clientEntry.ts +0 -121
- package/src/site/clientEntryDev.ts +0 -3
- package/src/site/defaultAdapter.ts +0 -11
- package/src/site/layoutSlots.ts +0 -53
- package/src/site/manifest.ts +0 -173
- package/src/site/serverEntry.ts +0 -648
- package/src/state/$page.ts +0 -25
- package/src/state/$serverPage.ts +0 -22
- package/src/state/client.ts +0 -15
- package/src/state.ts +0 -3
- package/src/test/runTest.ts +0 -538
- package/src/test.ts +0 -3
- package/src/types/Adapter.ts +0 -20
- package/src/types/ClientState.ts +0 -8
- package/src/types/InternalState.ts +0 -7
- package/src/types/Jsonify.ts +0 -20
- package/src/types/LayoutHandler.ts +0 -5
- package/src/types/LayoutPath.ts +0 -10
- package/src/types/ManifestRoute.ts +0 -6
- package/src/types/PageData.test-d.ts +0 -136
- package/src/types/PageData.ts +0 -35
- package/src/types/PageEndPoint.ts +0 -55
- package/src/types/PageForm.test-d.ts +0 -78
- package/src/types/PageForm.ts +0 -29
- package/src/types/PageLoadEvent.ts +0 -26
- package/src/types/PageLoadReturn.ts +0 -13
- package/src/types/PageProps.ts +0 -14
- package/src/types/PageServerAction.ts +0 -13
- package/src/types/PageServerEndPoint.test-d.ts +0 -105
- package/src/types/PageServerEndPoint.ts +0 -110
- package/src/types/PageServerLoad.ts +0 -15
- package/src/types/PageState.ts +0 -8
- package/src/types/ParseRouteParams.test-d.ts +0 -84
- package/src/types/ParseRouteParams.ts +0 -64
- package/src/types/Route.ts +0 -30
- package/src/types/RouteHandler.ts +0 -23
- package/src/types/RouteLayoutHandler.ts +0 -5
- package/src/types/RouteMatchResult.ts +0 -7
- package/src/types/RouteType.ts +0 -19
- package/src/types/ServerEndPoint.test-d.ts +0 -77
- package/src/types/ServerEndPoint.ts +0 -169
- package/src/types/ServerHook.ts +0 -18
- package/src/types/ServerLoadEvent.ts +0 -82
- package/src/types/ServerRequest.ts +0 -15
- package/src/types/SitePlugin.ts +0 -24
- package/src/types/StandardSchema.ts +0 -109
- package/src/utils/pathToRegex.ts +0 -17
- package/src/utils/pathTrie.ts +0 -182
- package/src/utils/searchParamsToRecord.ts +0 -18
- package/src/utils/torporPackages.ts +0 -150
- package/src/utils/tsconfigAliases.ts +0 -90
- package/src/validation/ValidationError.ts +0 -18
- package/src/validation/endpoint.ts +0 -83
- package/src/validation/validate.ts +0 -26
- package/src/vite-env.d.ts +0 -2
package/src/nav/navigate.ts
DELETED
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
import { clearLayoutSlot, fillLayoutSlot, hydrate } from "@torpor/view";
|
|
2
|
-
import { type Component, type SlotRender } from "@torpor/view";
|
|
3
|
-
import { mount, unmount } 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
|
-
let reused = false;
|
|
102
|
-
// The index of the innermost reused layout. Its slotRegion is the region
|
|
103
|
-
// that must be cleared and refilled (it contains the next layout's or the
|
|
104
|
-
// page's content). This is usually the last entry of the stack, but not
|
|
105
|
-
// when an outer layout is reused while an inner one is new — e.g.
|
|
106
|
-
// navigating between sections that share the root layout.
|
|
107
|
-
let reusedIndex = -1;
|
|
108
|
-
if (handler.layouts) {
|
|
109
|
-
let slotFunctions: SlotRender[] = [];
|
|
110
|
-
// The last slot function will render the client component
|
|
111
|
-
slotFunctions[handler.layouts.length] = function clientComponent(parent, anchor) {
|
|
112
|
-
let i = layoutStack.length - 1;
|
|
113
|
-
layoutStack[i].slotRegion = fillLayoutSlot(
|
|
114
|
-
clientEndPoint.component!,
|
|
115
|
-
slotFunctions[i + 1],
|
|
116
|
-
parent,
|
|
117
|
-
anchor,
|
|
118
|
-
$props,
|
|
119
|
-
$context,
|
|
120
|
-
);
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
// Each earlier slot function will render a layout
|
|
124
|
-
for (let i = handler.layouts.length - 1; i >= 0; i--) {
|
|
125
|
-
const layoutEndPoint: PageEndPoint | undefined = (await handler.layouts[i].endPoint())
|
|
126
|
-
?.default;
|
|
127
|
-
if (layoutEndPoint?.component) {
|
|
128
|
-
if (layoutStack[i].reuse) {
|
|
129
|
-
// Reuse this layout — clear and refill its slot (done in
|
|
130
|
-
// the try block below so a failure doesn't leave the slot
|
|
131
|
-
// half-cleared)
|
|
132
|
-
component = slotFunctions[i + 1] as Component;
|
|
133
|
-
reusedIndex = i;
|
|
134
|
-
reused = true;
|
|
135
|
-
break;
|
|
136
|
-
} else if (i === 0) {
|
|
137
|
-
component = layoutEndPoint.component as Component;
|
|
138
|
-
slots = { _: slotFunctions[i + 1] };
|
|
139
|
-
} else {
|
|
140
|
-
slotFunctions[i] = function layoutComponent(parent, anchor, _, $context) {
|
|
141
|
-
layoutStack[i - 1].slotRegion = fillLayoutSlot(
|
|
142
|
-
layoutEndPoint.component!,
|
|
143
|
-
slotFunctions[i + 1],
|
|
144
|
-
parent,
|
|
145
|
-
anchor,
|
|
146
|
-
$props,
|
|
147
|
-
$context,
|
|
148
|
-
);
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
try {
|
|
156
|
-
if (reused) {
|
|
157
|
-
// The layout is being reused — clear the old slot content, then
|
|
158
|
-
// call the slot function directly to fill it with the new page.
|
|
159
|
-
// We must not go through `mount` here: the slot's container still
|
|
160
|
-
// holds the layout's own children (e.g. a header), which `mount`
|
|
161
|
-
// refuses to mount into. Both the clear and the fill are inside
|
|
162
|
-
// the try so that a failure doesn't leave the slot half-cleared.
|
|
163
|
-
const slotRegion = layoutStack[reusedIndex].slotRegion;
|
|
164
|
-
parent = slotRegion.startNode!.parentNode as HTMLElement;
|
|
165
|
-
clearLayoutSlot(slotRegion);
|
|
166
|
-
component(parent, null);
|
|
167
|
-
} else if (withHydration) {
|
|
168
|
-
hydrate(parent, component, $props, slots);
|
|
169
|
-
} else {
|
|
170
|
-
// The layout chain changed (or there was no previous layout to
|
|
171
|
-
// reuse): tear down the previous UI entirely — disposing its
|
|
172
|
-
// region tree and clearing `#app` — so `mount` starts fresh.
|
|
173
|
-
// Without this, `mount` throws because `#app` still holds the
|
|
174
|
-
// previous render's children, and it would reuse a stale root
|
|
175
|
-
// region.
|
|
176
|
-
unmount(parent);
|
|
177
|
-
mount(parent, component, $props, slots);
|
|
178
|
-
}
|
|
179
|
-
} catch (error) {
|
|
180
|
-
// TODO: Show a proper Error component
|
|
181
|
-
parent.innerHTML = '<span style="color: red">Script syntax error</span><p>' + error + "</p>";
|
|
182
|
-
console.log(error);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Reset prefetched data on each navigation
|
|
186
|
-
client.prefetchedData = {};
|
|
187
|
-
|
|
188
|
-
return true;
|
|
189
|
-
}
|
package/src/nav/reload.ts
DELETED
package/src/nav/route.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import type { ExactRouteParams, ParseRouteParams } from "../types/ParseRouteParams";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Builds a route path in a type-safe manner, filling in dynamic segments, e.g.
|
|
5
|
-
* `route("/posts/[id]", { id: 5 })` returns `/posts/5`. The params object is
|
|
6
|
-
* checked for exact keys: missing or unknown params error at compile time.
|
|
7
|
-
*
|
|
8
|
-
* NOTE: The rest args conditional must stay INLINE — routing it through a type
|
|
9
|
-
* alias defeats `Params` inference, and excess keys stop being checked
|
|
10
|
-
* @param path The route path
|
|
11
|
-
* @param params The route params, required if the path has dynamic segments
|
|
12
|
-
* @returns The path with the params filled in, URI-encoded
|
|
13
|
-
*/
|
|
14
|
-
export default function route<
|
|
15
|
-
Route extends string,
|
|
16
|
-
Params extends ParseRouteParams<Route> = ParseRouteParams<Route>,
|
|
17
|
-
>(
|
|
18
|
-
path: Route,
|
|
19
|
-
...args: string extends Route
|
|
20
|
-
? [params?: Record<string, string>]
|
|
21
|
-
: keyof ParseRouteParams<Route> extends never
|
|
22
|
-
? []
|
|
23
|
-
: [params: Params & ExactRouteParams<Params, ParseRouteParams<Route>>]
|
|
24
|
-
): string {
|
|
25
|
-
const params = ((args as unknown[])[0] ?? {}) as Record<string, string>;
|
|
26
|
-
return path.replace(/\[(\.\.\.)?([^\]]+)\]/g, (_, splat: string | undefined, name: string) => {
|
|
27
|
-
const value = params[name];
|
|
28
|
-
if (value === undefined) {
|
|
29
|
-
throw new Error(`Missing param '${name}' for route '${path}'`);
|
|
30
|
-
}
|
|
31
|
-
return splat
|
|
32
|
-
? value
|
|
33
|
-
.split("/")
|
|
34
|
-
.map((segment) => encodeURIComponent(segment))
|
|
35
|
-
.join("/")
|
|
36
|
-
: encodeURIComponent(String(value));
|
|
37
|
-
});
|
|
38
|
-
}
|
package/src/nav.ts
DELETED
package/src/openapi/docsHtml.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A standalone HTML page loading Swagger UI from a CDN, pointed at the
|
|
3
|
-
* OpenAPI document endpoint.
|
|
4
|
-
*/
|
|
5
|
-
export default function openApiDocsHtml(docPath: string): string {
|
|
6
|
-
return `<!doctype html>
|
|
7
|
-
<html lang="en">
|
|
8
|
-
<head>
|
|
9
|
-
<meta charset="utf-8" />
|
|
10
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
11
|
-
<title>API Docs</title>
|
|
12
|
-
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" />
|
|
13
|
-
</head>
|
|
14
|
-
<body>
|
|
15
|
-
<div id="swagger-ui"></div>
|
|
16
|
-
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js" crossorigin></script>
|
|
17
|
-
<script>
|
|
18
|
-
window.addEventListener("DOMContentLoaded", () => {
|
|
19
|
-
SwaggerUIBundle({ url: ${JSON.stringify(docPath)}, dom_id: "#swagger-ui" });
|
|
20
|
-
});
|
|
21
|
-
</script>
|
|
22
|
-
</body>
|
|
23
|
-
</html>
|
|
24
|
-
`;
|
|
25
|
-
}
|
package/src/openapi/document.ts
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import type { StandardSchemaV1 } from "../types/StandardSchema";
|
|
2
|
-
import type { OpenApiPluginOptions, OpenApiRouteEntry, JsonSchema } from "./types";
|
|
3
|
-
|
|
4
|
-
// The endpoint handler names, in the order they appear in docs
|
|
5
|
-
const METHODS = ["get", "post", "patch", "put", "del", "options", "head"] as const;
|
|
6
|
-
type Method = (typeof METHODS)[number];
|
|
7
|
-
|
|
8
|
-
// Handlers whose schema validates the query string; all others validate the
|
|
9
|
-
// json request body
|
|
10
|
-
const QUERY_METHODS: readonly string[] = ["get", "head"];
|
|
11
|
-
const BODY_METHODS: readonly string[] = ["post", "patch", "put", "del", "options"];
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Builds an OpenAPI 3.1 document from a list of +server endpoints.
|
|
15
|
-
*
|
|
16
|
-
* Each endpoint's route path (`/api/posts/[id]`) is converted to OpenAPI
|
|
17
|
-
* syntax (`/api/posts/{id}`); its `params` schema becomes path parameters,
|
|
18
|
-
* its `get`/`head` schema becomes query parameters, and its `post`/`patch`/
|
|
19
|
-
* `put`/`del`/`options` schema becomes a json request body. Responses are
|
|
20
|
-
* stubbed: a `200` for every handler, plus a `422` when the handler declares
|
|
21
|
-
* an input schema (validation errors are returned automatically).
|
|
22
|
-
*
|
|
23
|
-
* Throws if any endpoint declares a schema but the options have no
|
|
24
|
-
* `toJsonSchema` converter.
|
|
25
|
-
*/
|
|
26
|
-
export default function buildOpenApiDocument(
|
|
27
|
-
entries: OpenApiRouteEntry[],
|
|
28
|
-
options: OpenApiPluginOptions,
|
|
29
|
-
): Record<string, unknown> {
|
|
30
|
-
if (!options.toJsonSchema) {
|
|
31
|
-
const withSchema = entries.find(hasSchemas);
|
|
32
|
-
if (withSchema) {
|
|
33
|
-
throw new Error(
|
|
34
|
-
`The endpoint at ${withSchema.path} declares schema(s), but the openApi() plugin ` +
|
|
35
|
-
`was not given a "toJsonSchema" converter. Pass one, e.g. ` +
|
|
36
|
-
`openApi({ toJsonSchema: (s) => z.toJSONSchema(s) })`,
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const paths: Record<string, Record<string, unknown>> = {};
|
|
42
|
-
for (const entry of entries) {
|
|
43
|
-
const pathItem = buildPathItem(entry, options);
|
|
44
|
-
if (pathItem) {
|
|
45
|
-
paths[openApiPath(entry.path)] = pathItem;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
openapi: "3.1.0",
|
|
51
|
-
info: {
|
|
52
|
-
title: options.title ?? "API",
|
|
53
|
-
version: options.version ?? "1.0.0",
|
|
54
|
-
},
|
|
55
|
-
paths,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function hasSchemas(entry: OpenApiRouteEntry): boolean {
|
|
60
|
-
const schema = entry.endPoint?.schema;
|
|
61
|
-
if (!schema) return false;
|
|
62
|
-
return METHODS.some((method) => schema[method]) || !!schema.params;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function buildPathItem(
|
|
66
|
-
entry: OpenApiRouteEntry,
|
|
67
|
-
options: OpenApiPluginOptions,
|
|
68
|
-
): Record<string, unknown> | undefined {
|
|
69
|
-
const endPoint = entry.endPoint;
|
|
70
|
-
const pathItem: Record<string, unknown> = {};
|
|
71
|
-
let count = 0;
|
|
72
|
-
|
|
73
|
-
for (const method of METHODS) {
|
|
74
|
-
if (typeof endPoint[method] !== "function") {
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
count++;
|
|
78
|
-
|
|
79
|
-
const schema = endPoint.schema?.[method];
|
|
80
|
-
const paramsSchema = endPoint.schema?.params;
|
|
81
|
-
|
|
82
|
-
const parameters: Record<string, unknown>[] = [];
|
|
83
|
-
if (paramsSchema) {
|
|
84
|
-
parameters.push(...flattenObject(convert(paramsSchema, options), "path"));
|
|
85
|
-
}
|
|
86
|
-
if (schema && QUERY_METHODS.includes(method)) {
|
|
87
|
-
parameters.push(...flattenObject(convert(schema, options), "query"));
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const responses: Record<string, unknown> = { "200": { description: "OK" } };
|
|
91
|
-
if (schema) {
|
|
92
|
-
responses["422"] = { description: "Validation failed" };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
pathItem[method === "del" ? "delete" : method] = {
|
|
96
|
-
operationId: operationId(method, entry.path),
|
|
97
|
-
...(parameters.length ? { parameters } : {}),
|
|
98
|
-
...(schema && BODY_METHODS.includes(method)
|
|
99
|
-
? {
|
|
100
|
-
requestBody: {
|
|
101
|
-
required: true,
|
|
102
|
-
content: { "application/json": { schema: convert(schema, options) } },
|
|
103
|
-
},
|
|
104
|
-
}
|
|
105
|
-
: {}),
|
|
106
|
-
responses,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return count > 0 ? pathItem : undefined;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function convert(schema: StandardSchemaV1, options: OpenApiPluginOptions): JsonSchema {
|
|
114
|
-
return options.toJsonSchema!(schema);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Flattens a converted object schema into OpenAPI parameters. Path
|
|
119
|
-
* parameters are always required; query parameters take their `required`
|
|
120
|
-
* flag from the schema's `required` array.
|
|
121
|
-
*/
|
|
122
|
-
function flattenObject(
|
|
123
|
-
jsonSchema: JsonSchema,
|
|
124
|
-
location: "path" | "query",
|
|
125
|
-
): Record<string, unknown>[] {
|
|
126
|
-
const properties = jsonSchema.properties;
|
|
127
|
-
if (!properties || typeof properties !== "object") {
|
|
128
|
-
return [];
|
|
129
|
-
}
|
|
130
|
-
const required = Array.isArray(jsonSchema.required) ? jsonSchema.required : [];
|
|
131
|
-
return Object.entries(properties as Record<string, JsonSchema>).map(([name, propSchema]) => ({
|
|
132
|
-
name,
|
|
133
|
-
in: location,
|
|
134
|
-
required: location === "path" ? true : required.includes(name),
|
|
135
|
-
schema: propSchema,
|
|
136
|
-
}));
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Converts a route path to OpenAPI syntax: `/api/posts/[id]` becomes
|
|
141
|
-
* `/api/posts/{id}`.
|
|
142
|
-
*/
|
|
143
|
-
function openApiPath(routePath: string): string {
|
|
144
|
-
return routePath.replace(/\[\.\.\.([^\]]+)\]/g, "{$1}").replace(/\[([^\]]+)\]/g, "{$1}");
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function operationId(method: Method, routePath: string): string {
|
|
148
|
-
const segments = openApiPath(routePath)
|
|
149
|
-
.split(/[^a-zA-Z0-9]+/)
|
|
150
|
-
.filter((s) => s.length > 0)
|
|
151
|
-
.map((s) => s.charAt(0).toUpperCase() + s.slice(1));
|
|
152
|
-
const verb = method === "del" ? "delete" : method;
|
|
153
|
-
return verb + (segments.join("") || "Root");
|
|
154
|
-
}
|
package/src/openapi/plugin.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import type SitePlugin from "../types/SitePlugin";
|
|
2
|
-
import openApiDocsHtml from "./docsHtml";
|
|
3
|
-
import type { OpenApiPluginOptions, ResolvedOpenApiOptions } from "./types";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* The `site.pluginState` key the openApi plugin stores its resolved options
|
|
7
|
-
* under. A registry symbol (`Symbol.for`), so that the user's config, the
|
|
8
|
-
* manifest plugin and the generated runtime code all resolve to the same
|
|
9
|
-
* key even when they are loaded as separate module instances.
|
|
10
|
-
*/
|
|
11
|
-
export const OPEN_API_STATE_KEY: symbol = Symbol.for("@torpor/build/openapi");
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Serves an OpenAPI 3.1 document for the site's `+server` endpoints, plus an
|
|
15
|
-
* optional interactive docs page.
|
|
16
|
-
*
|
|
17
|
-
* ```ts
|
|
18
|
-
* import { Site } from "@torpor/build";
|
|
19
|
-
* import { openApi } from "@torpor/build/openapi";
|
|
20
|
-
* import { z } from "zod";
|
|
21
|
-
*
|
|
22
|
-
* const site = new Site();
|
|
23
|
-
* site.addRouteFolder("src/routes");
|
|
24
|
-
* site.plugins = [
|
|
25
|
-
* openApi({
|
|
26
|
-
* title: "My API",
|
|
27
|
-
* toJsonSchema: (s) => z.toJSONSchema(s as z.ZodType, { io: "input" }),
|
|
28
|
-
* }),
|
|
29
|
-
* ];
|
|
30
|
-
* export default site;
|
|
31
|
-
* ```
|
|
32
|
-
*
|
|
33
|
-
* The document is served at `/openapi.json` (configurable with `path`) and
|
|
34
|
-
* the docs page at `/docs` (set `docs: false` to disable). Endpoints are
|
|
35
|
-
* picked up from every `+server.ts` route: its `params` schema becomes path
|
|
36
|
-
* parameters, `get`/`head` schemas become query parameters, and
|
|
37
|
-
* `post`/`patch`/`put`/`del`/`options` schemas become request bodies.
|
|
38
|
-
*
|
|
39
|
-
* The resolved options are stored in `site.pluginState` under
|
|
40
|
-
* `OPEN_API_STATE_KEY`, which the manifest plugin reads at build time to
|
|
41
|
-
* generate the document endpoint.
|
|
42
|
-
*/
|
|
43
|
-
export function openApi(options: OpenApiPluginOptions = {}): SitePlugin {
|
|
44
|
-
return (site) => {
|
|
45
|
-
const docPath = withLeadingSlash(options.path ?? "/openapi.json");
|
|
46
|
-
if (site.routes.some((route) => route.path === docPath)) {
|
|
47
|
-
throw new Error(
|
|
48
|
-
`There is already a route registered at ${docPath}, so the OpenAPI document can't be ` +
|
|
49
|
-
`served there. Pass a different "path" to the openApi() plugin`,
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const resolved: ResolvedOpenApiOptions = {
|
|
54
|
-
path: docPath,
|
|
55
|
-
docs: options.docs === false ? undefined : withLeadingSlash(options.docs ?? "/docs"),
|
|
56
|
-
title: options.title ?? "API",
|
|
57
|
-
version: options.version ?? "1.0.0",
|
|
58
|
-
toJsonSchema: options.toJsonSchema,
|
|
59
|
-
};
|
|
60
|
-
site.pluginState.set(OPEN_API_STATE_KEY, resolved);
|
|
61
|
-
|
|
62
|
-
if (resolved.docs) {
|
|
63
|
-
const html = openApiDocsHtml(docPath);
|
|
64
|
-
site.addRoute(resolved.docs, {
|
|
65
|
-
server: {
|
|
66
|
-
get: () =>
|
|
67
|
-
new Response(html, {
|
|
68
|
-
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
69
|
-
}),
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function withLeadingSlash(pathName: string): string {
|
|
77
|
-
return pathName.startsWith("/") ? pathName : "/" + pathName;
|
|
78
|
-
}
|
package/src/openapi/types.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import type { StandardSchemaV1 } from "../types/StandardSchema";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* A JSON Schema object, as produced by a schema library's converter (e.g.
|
|
5
|
-
* `z.toJSONSchema`, valibot's `toJsonSchema`, arktype's `toJsonSchema`).
|
|
6
|
-
*/
|
|
7
|
-
export type JsonSchema = Record<string, unknown>;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Converts a Standard Schema (zod, valibot, arktype, ...) into a JSON Schema
|
|
11
|
-
* object, for inclusion in an OpenAPI document. Supplied to the openApi
|
|
12
|
-
* plugin, since each schema library has its own conversion function:
|
|
13
|
-
*
|
|
14
|
-
* ```ts
|
|
15
|
-
* // zod
|
|
16
|
-
* openApi({ toJsonSchema: (s) => z.toJSONSchema(s as z.ZodType, { io: "input" }) })
|
|
17
|
-
* // arktype
|
|
18
|
-
* openApi({ toJsonSchema: (s) => (s as Type).toJsonSchema() })
|
|
19
|
-
* ```
|
|
20
|
-
*/
|
|
21
|
-
export type ToJsonSchema = (schema: StandardSchemaV1) => JsonSchema;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Options for the openApi plugin.
|
|
25
|
-
*/
|
|
26
|
-
export type OpenApiPluginOptions = {
|
|
27
|
-
/**
|
|
28
|
-
* The path to serve the OpenAPI document at. Defaults to `/openapi.json`.
|
|
29
|
-
*/
|
|
30
|
-
path?: string;
|
|
31
|
-
/**
|
|
32
|
-
* The path to serve interactive API docs at, or `false` to disable the
|
|
33
|
-
* docs page. Defaults to `/docs`.
|
|
34
|
-
*/
|
|
35
|
-
docs?: string | false;
|
|
36
|
-
/**
|
|
37
|
-
* The API title for the document's `info` section. Defaults to `"API"`.
|
|
38
|
-
*/
|
|
39
|
-
title?: string;
|
|
40
|
-
/**
|
|
41
|
-
* The API version for the document's `info` section. Defaults to
|
|
42
|
-
* `"1.0.0"`.
|
|
43
|
-
*/
|
|
44
|
-
version?: string;
|
|
45
|
-
/**
|
|
46
|
-
* Converts an endpoint's schemas into JSON Schema. Required for endpoints
|
|
47
|
-
* that declare schemas; endpoints without schemas are documented with
|
|
48
|
-
* their paths and methods only.
|
|
49
|
-
*/
|
|
50
|
-
toJsonSchema?: ToJsonSchema;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* OpenApi options with defaults applied. Created by the openApi plugin and
|
|
55
|
-
* consumed by the document builder.
|
|
56
|
-
*/
|
|
57
|
-
export type ResolvedOpenApiOptions = {
|
|
58
|
-
path: string;
|
|
59
|
-
/** Undefined when the docs page is disabled */
|
|
60
|
-
docs: string | undefined;
|
|
61
|
-
title: string;
|
|
62
|
-
version: string;
|
|
63
|
-
toJsonSchema: ToJsonSchema | undefined;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* A ServerEndPoint shape, loosely typed so that endpoint modules loaded from
|
|
68
|
-
* anywhere can be passed to the document builder.
|
|
69
|
-
*/
|
|
70
|
-
export type OpenApiEndPoint = {
|
|
71
|
-
schema?: Record<string, StandardSchemaV1 | undefined> | undefined;
|
|
72
|
-
[method: string]: unknown;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* A single API endpoint to include in an OpenAPI document: its route path
|
|
77
|
-
* (with `[param]` segments) and its default-exported endpoint object.
|
|
78
|
-
*/
|
|
79
|
-
export type OpenApiRouteEntry = {
|
|
80
|
-
path: string;
|
|
81
|
-
endPoint: OpenApiEndPoint;
|
|
82
|
-
};
|
package/src/openapi.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import buildOpenApiDocument from "./openapi/document";
|
|
2
|
-
import openApiDocsHtml from "./openapi/docsHtml";
|
|
3
|
-
import { OPEN_API_STATE_KEY, openApi } from "./openapi/plugin";
|
|
4
|
-
|
|
5
|
-
export { buildOpenApiDocument, openApi, openApiDocsHtml, OPEN_API_STATE_KEY };
|
|
6
|
-
|
|
7
|
-
export type {
|
|
8
|
-
JsonSchema,
|
|
9
|
-
OpenApiEndPoint,
|
|
10
|
-
OpenApiPluginOptions,
|
|
11
|
-
OpenApiRouteEntry,
|
|
12
|
-
ResolvedOpenApiOptions,
|
|
13
|
-
ToJsonSchema,
|
|
14
|
-
} from "./openapi/types";
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A Response whose JSON body type is known, as returned by e.g. `ok({ ... })`.
|
|
3
|
-
* Purely a compile-time marker: `__body` is never set at runtime.
|
|
4
|
-
*/
|
|
5
|
-
export default interface TypedResponse<T> extends Response {
|
|
6
|
-
/**
|
|
7
|
-
* Phantom property carrying the body type. Required so that plain
|
|
8
|
-
* `Response` values do not match `TypedResponse<infer T>`.
|
|
9
|
-
*/
|
|
10
|
-
readonly __body: T;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* A Response that does not carry typed JSON data (e.g. redirects, errors,
|
|
15
|
-
* plain-text bodies). Loads may return these alongside typed data responses.
|
|
16
|
-
*/
|
|
17
|
-
export type UntypedResponse = Response & { readonly __body?: never };
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import response from "./response";
|
|
2
|
-
import type { Jsonify } from "../types/Jsonify";
|
|
3
|
-
import type TypedResponse from "./TypedResponse";
|
|
4
|
-
|
|
5
|
-
type BadRequestResponse<T extends object | string | undefined> = T extends object
|
|
6
|
-
? TypedResponse<Jsonify<T>>
|
|
7
|
-
: Response;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* 400 Bad Request
|
|
11
|
-
*
|
|
12
|
-
* The HTTP 400 Bad Request client error response status code indicates that the
|
|
13
|
-
* server would not process the request due to something the server considered
|
|
14
|
-
* to be a client error. The reason for a 400 response is typically due to
|
|
15
|
-
* malformed request syntax, invalid request message framing, or deceptive
|
|
16
|
-
* request routing.
|
|
17
|
-
*
|
|
18
|
-
* Clients that receive a 400 response should expect that repeating the request
|
|
19
|
-
* without modification will fail with the same error.
|
|
20
|
-
*
|
|
21
|
-
* An object body is typed: it becomes the page's `$props.form` (with its JSON
|
|
22
|
-
* form) after a form submit, so validation errors can be surfaced with their
|
|
23
|
-
* field types via `PageForm`.
|
|
24
|
-
*
|
|
25
|
-
* See https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/400
|
|
26
|
-
*/
|
|
27
|
-
export default function badRequest<T extends object | string | undefined>(
|
|
28
|
-
body?: T,
|
|
29
|
-
): BadRequestResponse<T> {
|
|
30
|
-
return response(400, body ?? "Bad request") as BadRequestResponse<T>;
|
|
31
|
-
}
|
package/src/response/created.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import type { Jsonify } from "../types/Jsonify";
|
|
2
|
-
import type TypedResponse from "./TypedResponse";
|
|
3
|
-
import response from "./response";
|
|
4
|
-
|
|
5
|
-
type CreatedResponse<T extends object | string | undefined> = T extends object
|
|
6
|
-
? TypedResponse<Jsonify<T>>
|
|
7
|
-
: Response;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* 201 Created
|
|
11
|
-
*
|
|
12
|
-
* The HTTP 201 Created successful response status code indicates that the HTTP
|
|
13
|
-
* request has led to the creation of a resource. This status code is commonly
|
|
14
|
-
* sent as the result of a POST request.
|
|
15
|
-
*
|
|
16
|
-
* The new resource, or a description and link to the new resource, is created
|
|
17
|
-
* before the response is returned. The newly-created items can be returned in
|
|
18
|
-
* the body of the response message, but must be locatable by the URL of the
|
|
19
|
-
* initiating request or by the URL in the value of the Location header provided
|
|
20
|
-
* with the response.
|
|
21
|
-
*
|
|
22
|
-
* An object body is typed: the client sees its JSON form through
|
|
23
|
-
* `makeApi`-created callers.
|
|
24
|
-
*
|
|
25
|
-
* See https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/201
|
|
26
|
-
*/
|
|
27
|
-
export default function created<T extends object | string | undefined>(
|
|
28
|
-
body?: T,
|
|
29
|
-
): CreatedResponse<T> {
|
|
30
|
-
return response(201, body) as CreatedResponse<T>;
|
|
31
|
-
}
|