@torpor/build 0.4.14 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -2
- package/dist/Jsonify-Dg6gChd3.d.mts +10 -0
- package/dist/Jsonify-Dg6gChd3.d.mts.map +1 -0
- package/dist/ParseRouteParams-DBmqlmLh.d.mts +42 -0
- package/dist/ParseRouteParams-DBmqlmLh.d.mts.map +1 -0
- package/dist/{RouteType-B6BMOyYw.mjs → Router-C4ADUfhv.mjs} +30 -42
- package/dist/Router-C4ADUfhv.mjs.map +1 -0
- package/dist/{Server-CF5wDJp6.d.mts → Server-C_IKav_e.d.mts} +44 -16
- package/dist/Server-C_IKav_e.d.mts.map +1 -0
- package/dist/{ServerEvent-CxPwA4lH.mjs → ServerEvent-Dljn5i7A.mjs} +37 -11
- package/dist/ServerEvent-Dljn5i7A.mjs.map +1 -0
- package/dist/Site-CMa153FA.d.mts +565 -0
- package/dist/Site-CMa153FA.d.mts.map +1 -0
- package/dist/StandardSchema-D28L8_zZ.d.mts +100 -0
- package/dist/StandardSchema-D28L8_zZ.d.mts.map +1 -0
- package/dist/TypedResponse-6lznqN4U.d.mts +22 -0
- package/dist/TypedResponse-6lznqN4U.d.mts.map +1 -0
- package/dist/{_page-BQs4WvCO.mjs → _page-4cQKs_Dn.mjs} +3 -5
- package/dist/_page-4cQKs_Dn.mjs.map +1 -0
- package/dist/bin/index.d.mts +1 -1
- package/dist/bin/index.js +15 -11
- package/dist/bin/index.js.map +1 -1
- package/dist/flattenHeaders-C_YYLdOq.mjs +114 -0
- package/dist/flattenHeaders-C_YYLdOq.mjs.map +1 -0
- package/dist/form.d.mts +35 -0
- package/dist/form.d.mts.map +1 -0
- package/dist/form.mjs +48 -0
- package/dist/form.mjs.map +1 -0
- package/dist/index.d.mts +72 -156
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +158 -26
- package/dist/index.mjs.map +1 -1
- package/dist/nav.d.mts +58 -5
- package/dist/nav.d.mts.map +1 -1
- package/dist/nav.mjs +116 -38
- package/dist/nav.mjs.map +1 -1
- package/dist/openapi.d.mts +144 -0
- package/dist/openapi.d.mts.map +1 -0
- package/dist/openapi.mjs +2 -0
- package/dist/pathTrie-D4Ax2Mu9.mjs +160 -0
- package/dist/pathTrie-D4Ax2Mu9.mjs.map +1 -0
- package/dist/plugin-D8Www11B.mjs +202 -0
- package/dist/plugin-D8Www11B.mjs.map +1 -0
- package/dist/{response-C5TtAsh1.mjs → response-Bg4w2S1q.mjs} +16 -39
- package/dist/response-Bg4w2S1q.mjs.map +1 -0
- package/dist/response.d.mts +309 -289
- package/dist/response.d.mts.map +1 -1
- package/dist/response.mjs +3 -4
- package/dist/run.d.mts +11 -4
- package/dist/run.d.mts.map +1 -1
- package/dist/run.mjs +2 -8
- package/dist/runOpenApi-BUi1RH1l.mjs +1381 -0
- package/dist/runOpenApi-BUi1RH1l.mjs.map +1 -0
- package/dist/schema.d.mts +32 -0
- package/dist/schema.d.mts.map +1 -0
- package/dist/schema.mjs +2 -0
- package/dist/server.d.mts +10 -8
- package/dist/server.d.mts.map +1 -1
- package/dist/server.mjs +132 -4
- package/dist/server.mjs.map +1 -0
- package/dist/state.d.mts.map +1 -1
- package/dist/state.mjs +2 -3
- package/dist/test.d.mts +8 -10
- package/dist/test.d.mts.map +1 -1
- package/dist/test.mjs +283 -79
- package/dist/test.mjs.map +1 -1
- package/dist/{seeOther-B4Yhu9iq.mjs → unprocessable-DAZbrDeR.mjs} +34 -13
- package/dist/unprocessable-DAZbrDeR.mjs.map +1 -0
- package/dist/validate-Ok4krlkX.mjs +40 -0
- package/dist/validate-Ok4krlkX.mjs.map +1 -0
- package/package.json +28 -17
- package/src/bin/index.ts +13 -2
- package/src/dev.ts +10 -0
- package/src/form/formDataToRecord.ts +23 -0
- package/src/form/readForm.ts +71 -0
- package/src/form.ts +4 -0
- package/src/index.ts +53 -1
- package/src/nav/api.test-d.ts +79 -0
- package/src/nav/api.ts +104 -0
- package/src/nav/formSubmit.ts +18 -4
- package/src/nav/navigate.ts +40 -15
- package/src/nav/route.ts +38 -0
- package/src/nav.ts +3 -1
- package/src/openapi/docsHtml.ts +25 -0
- package/src/openapi/document.ts +154 -0
- package/src/openapi/plugin.ts +78 -0
- package/src/openapi/types.ts +82 -0
- package/src/openapi.ts +14 -0
- package/src/response/TypedResponse.ts +17 -0
- package/src/response/badRequest.ts +14 -2
- package/src/response/created.ts +13 -2
- package/src/response/found.ts +2 -2
- package/src/response/movedPermanently.ts +2 -2
- package/src/response/notModified.ts +2 -2
- package/src/response/ok.ts +13 -4
- package/src/response/response.ts +4 -4
- package/src/response/unprocessable.ts +14 -2
- package/src/run/depCache.ts +175 -0
- package/src/run/devPlugin.ts +128 -0
- package/src/run/prepareTemplate.ts +6 -3
- package/src/run/run.ts +63 -39
- package/src/run/runBuild.ts +63 -12
- package/src/run/runDev.ts +146 -58
- package/src/run/runOpenApi.ts +52 -0
- package/src/run/runPreview.ts +17 -26
- package/src/run/staleTorpCopies.ts +100 -0
- package/src/run.ts +2 -1
- package/src/schema.ts +7 -0
- package/src/server/CookieHelper.ts +17 -7
- package/src/server/Server.ts +54 -30
- package/src/server/ServerEvent.ts +24 -1
- package/src/server/connect/connectMiddleware.ts +44 -40
- package/src/server/connect/flattenHeaders.ts +1 -23
- package/src/server/connect/requestToNodeMessage.ts +6 -3
- package/src/server/types/MiddlewareFunction.ts +18 -4
- package/src/site/Router.ts +52 -41
- package/src/site/Site.ts +184 -10
- package/src/site/checkLayoutSlots.ts +114 -0
- package/src/site/checkRoutes.ts +435 -0
- package/src/site/clientEntry.ts +22 -9
- package/src/site/layoutSlots.ts +53 -0
- package/src/site/manifest.ts +125 -6
- package/src/site/serverEntry.ts +341 -132
- package/src/state/$page.ts +2 -1
- package/src/state/$serverPage.ts +22 -0
- package/src/test/runTest.ts +306 -118
- package/src/types/Adapter.ts +12 -0
- package/src/types/Jsonify.ts +20 -0
- package/src/types/PageData.test-d.ts +136 -0
- package/src/types/PageData.ts +35 -0
- package/src/types/PageEndPoint.ts +25 -9
- package/src/types/PageForm.test-d.ts +78 -0
- package/src/types/PageForm.ts +29 -0
- package/src/types/PageLoadEvent.ts +16 -4
- package/src/types/PageLoadReturn.ts +13 -0
- package/src/types/PageProps.ts +14 -0
- package/src/types/PageServerAction.ts +8 -2
- package/src/types/PageServerEndPoint.test-d.ts +105 -0
- package/src/types/PageServerEndPoint.ts +99 -5
- package/src/types/PageServerLoad.ts +11 -3
- package/src/types/ParseRouteParams.test-d.ts +84 -0
- package/src/types/ParseRouteParams.ts +64 -0
- package/src/types/Route.ts +20 -1
- package/src/types/RouteHandler.ts +8 -1
- package/src/types/ServerEndPoint.test-d.ts +77 -0
- package/src/types/ServerEndPoint.ts +148 -16
- package/src/types/ServerHook.ts +10 -3
- package/src/types/ServerLoadEvent.ts +48 -3
- package/src/types/ServerRequest.ts +10 -2
- package/src/types/SitePlugin.ts +24 -0
- package/src/types/StandardSchema.ts +109 -0
- package/src/utils/pathToRegex.ts +4 -2
- package/src/utils/pathTrie.ts +182 -0
- package/src/utils/searchParamsToRecord.ts +18 -0
- package/src/utils/torporPackages.ts +150 -0
- package/src/utils/tsconfigAliases.ts +90 -0
- package/src/validation/ValidationError.ts +18 -0
- package/src/validation/endpoint.ts +83 -0
- package/src/validation/validate.ts +26 -0
- package/dist/RouteType-B6BMOyYw.mjs.map +0 -1
- package/dist/Server-CF5wDJp6.d.mts.map +0 -1
- package/dist/ServerEvent-CxPwA4lH.mjs.map +0 -1
- package/dist/Site-DgC6WWn1.d.mts +0 -78
- package/dist/Site-DgC6WWn1.d.mts.map +0 -1
- package/dist/_page-BQs4WvCO.mjs.map +0 -1
- package/dist/connectMiddleware-D7nehjj3.mjs +0 -252
- package/dist/connectMiddleware-D7nehjj3.mjs.map +0 -1
- package/dist/pathToRegex-TUAMc3zS.mjs +0 -11
- package/dist/pathToRegex-TUAMc3zS.mjs.map +0 -1
- package/dist/response-C5TtAsh1.mjs.map +0 -1
- package/dist/run-BPFtSYA3.mjs +0 -332
- package/dist/run-BPFtSYA3.mjs.map +0 -1
- package/dist/seeOther-B4Yhu9iq.mjs.map +0 -1
- package/src/server/Routerx.ts +0 -72
- package/src/server/connect/bufferToArrayBuffer.ts +0 -8
- package/src/server/connect/readableToBuffer.ts +0 -16
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unprocessable-DAZbrDeR.mjs","names":[],"sources":["../src/response/response.ts","../src/response/transfer.ts","../src/response/notFound.ts","../src/response/ok.ts","../src/response/seeOther.ts","../src/response/unprocessable.ts"],"sourcesContent":["/**\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","/**\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 response from \"./response\";\n\n/**\n * 404 Not Found\n *\n * The HTTP 404 Not Found client error response status code indicates that the\n * server cannot find the requested resource. Links that lead to a 404 page are\n * often called broken or dead links and can be subject to link rot.\n *\n * A 404 status code only indicates that the resource is missing without\n * indicating if this is temporary or permanent. If a resource is permanently\n * removed, servers should send the 410 Gone status instead.\n *\n * 404 errors on a website can lead to a poor user experience for your visitors,\n * so the number of broken links (internal and external) should be minimized to\n * prevent frustration for readers. Common causes of 404 responses are mistyped\n * URLs or pages that are moved or deleted without redirection. For more\n * information, see the Redirections in HTTP guide.\n *\n * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/404\n */\nexport default function notFound(body?: object | string): Response {\n\treturn response(404, body ?? \"Not found\");\n}\n","import type { Jsonify } from \"../types/Jsonify\";\nimport type TypedResponse from \"./TypedResponse\";\nimport response from \"./response\";\n\ntype OkResponse<T extends object | string | undefined> = T extends object\n\t? TypedResponse<Jsonify<T>>\n\t: Response;\n\n/**\n * 200 OK\n *\n * The HTTP 200 OK successful response status code indicates that a request has\n * succeeded. A 200 OK response is cacheable by default.\n *\n * A 200 OK response has a different meaning and format depending on the HTTP\n * request method. Here's how they vary for different methods:\n *\n * - GET: A resource was retrieved by the server and included in the response\n * body.\n * - POST: An action succeeded; the response has a message body describing the\n * result.\n * - HEAD: Identical to GET, except there is no message body.\n * - TRACE: The response has a message body containing the request as received\n * by the server.\n *\n * Although possible, successful PUT or DELETE requests often do not result in\n * a 200 OK response. It is more common to see 201 Created if the resource is\n * uploaded or created for the first time, or 204 No Content upon successful\n * deletion of a resource.\n *\n * An object body is typed: the client sees its JSON form through\n * `makeApi`-created callers.\n *\n * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/200\n */\nexport default function ok<T extends object | string | undefined>(body?: T): OkResponse<T> {\n\treturn response(200, body) as OkResponse<T>;\n}\n","import transfer from \"./transfer\";\n\n/**\n * 303 See Other\n *\n * The HTTP 303 See Other redirection response status code indicates that the\n * browser should redirect to the URL in the Location header instead of\n * rendering the requested resource.\n *\n * This response code is often sent back as a result of PUT or POST methods so\n * the client may retrieve a confirmation, or view a representation of a\n * real-world object (see HTTP range-14). The method to retrieve the redirected\n * resource is always GET.\n *\n * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/303\n */\nexport default function seeOther(location: string): Response {\n\treturn transfer(303, location);\n}\n","import response from \"./response\";\nimport type { Jsonify } from \"../types/Jsonify\";\nimport type TypedResponse from \"./TypedResponse\";\n\ntype UnprocessableResponse<T extends object | string | undefined> = T extends object\n\t? TypedResponse<Jsonify<T>>\n\t: Response;\n\n/**\n * 422 Unprocessable Content\n *\n * The HTTP 422 Unprocessable Content client error response status code\n * indicates that the server understood the content type of the request content,\n * and the syntax of the request content was correct, but it was unable to\n * process the contained instructions.\n *\n * Clients that receive a 422 response should expect that repeating the request\n * without modification will fail with the same error.\n *\n * An object body is typed: it becomes the page's `$props.form` (with its JSON\n * form) after a form submit, so validation errors can be surfaced with their\n * field types via `PageForm`.\n *\n * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/422\n */\nexport default function unprocessable<T extends object | string | undefined>(\n\tbody?: T,\n): UnprocessableResponse<T> {\n\treturn response(422, body ?? \"Unprocessable\") as UnprocessableResponse<T>;\n}\n"],"mappings":";;;;;;;;AAOA,SAAwB,SAAS,QAAgB,MAAkC;CAClF,IAAI,UAA8C,KAAA;CAClD,IAAI,MACH,IAAI,OAAO,SAAS,UAAU;EAC7B,UAAU,EAAE,gBAAgB,mBAAmB;EAC/C,OAAO,KAAK,UAAU,IAAI;CAC3B,OACC,UAAU,EAAE,gBAAgB,aAAa;CAG3C,OAAO,IAAI,SAAS,MAAM;EACzB;EACA;CACD,CAAC;AACF;;;;;;AClBA,SAAwB,SACvB,QACA,UACW;CACX,OAAO,IAAI,SAAS,MAAM;EACzB;EACA,SAAS,EACR,SACD;CACD,CAAC;AACF;;;;;;;;;;;;;;;;;;;;;;ACQA,SAAwB,SAAS,MAAkC;CAClE,OAAO,SAAS,KAAK,QAAQ,WAAW;AACzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACYA,SAAwB,GAA0C,MAAyB;CAC1F,OAAO,SAAS,KAAK,IAAI;AAC1B;;;;;;;;;;;;;;;;;ACrBA,SAAwB,SAAS,UAA4B;CAC5D,OAAO,SAAS,KAAK,QAAQ;AAC9B;;;;;;;;;;;;;;;;;;;;ACOA,SAAwB,cACvB,MAC2B;CAC3B,OAAO,SAAS,KAAK,QAAQ,eAAe;AAC7C"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/validation/ValidationError.ts
|
|
2
|
+
/**
|
|
3
|
+
* The error thrown when a value fails standard schema validation. The
|
|
4
|
+
* framework turns it into a 422 response for endpoint request bodies.
|
|
5
|
+
*/
|
|
6
|
+
var ValidationError = class extends Error {
|
|
7
|
+
/**
|
|
8
|
+
* The issues reported by the schema.
|
|
9
|
+
*/
|
|
10
|
+
issues;
|
|
11
|
+
constructor(issues) {
|
|
12
|
+
super("Validation failed");
|
|
13
|
+
this.name = "ValidationError";
|
|
14
|
+
this.issues = issues;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/validation/validate.ts
|
|
19
|
+
/**
|
|
20
|
+
* Validates a value against a standard schema (zod, valibot, arktype, etc),
|
|
21
|
+
* returning the parsed value, or throwing a `ValidationError` with the
|
|
22
|
+
* schema's issues.
|
|
23
|
+
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* const post = validate(postSchema, { title: "Hello" });
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @param schema A schema implementing the Standard Schema interface
|
|
29
|
+
* @param value The value to validate
|
|
30
|
+
* @returns The parsed value
|
|
31
|
+
*/
|
|
32
|
+
async function validate(schema, value) {
|
|
33
|
+
const result = await schema["~standard"].validate(value);
|
|
34
|
+
if (result.issues) throw new ValidationError(result.issues);
|
|
35
|
+
return result.value;
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { ValidationError as n, validate as t };
|
|
39
|
+
|
|
40
|
+
//# sourceMappingURL=validate-Ok4krlkX.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-Ok4krlkX.mjs","names":[],"sources":["../src/validation/ValidationError.ts","../src/validation/validate.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"../types/StandardSchema\";\n\n/**\n * The error thrown when a value fails standard schema validation. The\n * framework turns it into a 422 response for endpoint request bodies.\n */\nexport default class ValidationError extends Error {\n\t/**\n\t * The issues reported by the schema.\n\t */\n\tissues: ReadonlyArray<StandardSchemaV1.Issue>;\n\n\tconstructor(issues: ReadonlyArray<StandardSchemaV1.Issue>) {\n\t\tsuper(\"Validation failed\");\n\t\tthis.name = \"ValidationError\";\n\t\tthis.issues = issues;\n\t}\n}\n","import type { StandardSchemaV1 } from \"../types/StandardSchema\";\nimport ValidationError from \"./ValidationError\";\n\n/**\n * Validates a value against a standard schema (zod, valibot, arktype, etc),\n * returning the parsed value, or throwing a `ValidationError` with the\n * schema's issues.\n *\n * ```ts\n * const post = validate(postSchema, { title: \"Hello\" });\n * ```\n *\n * @param schema A schema implementing the Standard Schema interface\n * @param value The value to validate\n * @returns The parsed value\n */\nexport default async function validate<Schema extends StandardSchemaV1>(\n\tschema: Schema,\n\tvalue: unknown,\n): Promise<StandardSchemaV1.InferOutput<Schema>> {\n\tconst result = await schema[\"~standard\"].validate(value);\n\tif (result.issues) {\n\t\tthrow new ValidationError(result.issues);\n\t}\n\treturn result.value as StandardSchemaV1.InferOutput<Schema>;\n}\n"],"mappings":";;;;;AAMA,IAAqB,kBAArB,cAA6C,MAAM;;;;CAIlD;CAEA,YAAY,QAA+C;EAC1D,MAAM,mBAAmB;EACzB,KAAK,OAAO;EACZ,KAAK,SAAS;CACf;AACD;;;;;;;;;;;;;;;;ACDA,eAA8B,SAC7B,QACA,OACgD;CAChD,MAAM,SAAS,MAAM,OAAO,YAAY,CAAC,SAAS,KAAK;CACvD,IAAI,OAAO,QACV,MAAM,IAAI,gBAAgB,OAAO,MAAM;CAExC,OAAO,OAAO;AACf"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@torpor/build",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "A framework for building sites and apps with Torpor",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -8,14 +8,30 @@
|
|
|
8
8
|
"types": "./dist/index.d.mts",
|
|
9
9
|
"import": "./dist/index.mjs"
|
|
10
10
|
},
|
|
11
|
+
"./dev": {
|
|
12
|
+
"types": "./src/dev.ts",
|
|
13
|
+
"import": "./src/dev.ts"
|
|
14
|
+
},
|
|
15
|
+
"./form": {
|
|
16
|
+
"types": "./dist/form.d.mts",
|
|
17
|
+
"import": "./dist/form.mjs"
|
|
18
|
+
},
|
|
11
19
|
"./nav": {
|
|
12
20
|
"types": "./dist/nav.d.mts",
|
|
13
21
|
"import": "./dist/nav.mjs"
|
|
14
22
|
},
|
|
23
|
+
"./openapi": {
|
|
24
|
+
"types": "./dist/openapi.d.mts",
|
|
25
|
+
"import": "./dist/openapi.mjs"
|
|
26
|
+
},
|
|
15
27
|
"./response": {
|
|
16
28
|
"types": "./dist/response.d.mts",
|
|
17
29
|
"import": "./dist/response.mjs"
|
|
18
30
|
},
|
|
31
|
+
"./schema": {
|
|
32
|
+
"types": "./dist/schema.d.mts",
|
|
33
|
+
"import": "./dist/schema.mjs"
|
|
34
|
+
},
|
|
19
35
|
"./run": {
|
|
20
36
|
"types": "./dist/run.d.mts",
|
|
21
37
|
"import": "./dist/run.mjs"
|
|
@@ -50,27 +66,22 @@
|
|
|
50
66
|
"author": "",
|
|
51
67
|
"license": "ISC",
|
|
52
68
|
"dependencies": {
|
|
53
|
-
"cookie": "^
|
|
54
|
-
"dotenv": "^17.2
|
|
55
|
-
"vite": "
|
|
56
|
-
"
|
|
57
|
-
"@torpor/
|
|
58
|
-
"@torpor/view": "^0.4.18"
|
|
69
|
+
"cookie": "^2.0.1",
|
|
70
|
+
"dotenv": "^17.4.2",
|
|
71
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.7",
|
|
72
|
+
"@torpor/unplugin": "^1.0.0",
|
|
73
|
+
"@torpor/view": "^1.0.0"
|
|
59
74
|
},
|
|
60
75
|
"devDependencies": {
|
|
61
|
-
"@
|
|
62
|
-
"@typescript/native-preview": "7.0.0-dev.20251112.1",
|
|
76
|
+
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
63
77
|
"esno": "^4.8.0",
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"typescript": "^5.9.3",
|
|
67
|
-
"vitest": "^4.0.8"
|
|
78
|
+
"typescript": "^7.0.2",
|
|
79
|
+
"vite-plus": "0.2.7"
|
|
68
80
|
},
|
|
69
81
|
"scripts": {
|
|
70
82
|
"check": "tsgo --noEmit && pnpm dlx oxlint --type-aware",
|
|
71
|
-
"build": "tsgo --noEmit &&
|
|
72
|
-
"build:
|
|
73
|
-
"
|
|
74
|
-
"test": "vitest"
|
|
83
|
+
"build": "tsgo --noEmit && vp pack",
|
|
84
|
+
"build:watch": "vp pack --watch",
|
|
85
|
+
"test": "vp test"
|
|
75
86
|
}
|
|
76
87
|
}
|
package/src/bin/index.ts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
#! /usr/bin/env node
|
|
2
|
-
import run from "../run/run";
|
|
2
|
+
import run, { loadSite } from "../run/run";
|
|
3
|
+
import runOpenApi from "../run/runOpenApi";
|
|
3
4
|
|
|
4
5
|
const workingDir = process.cwd();
|
|
5
|
-
if (process.argv.includes("--
|
|
6
|
+
if (process.argv.includes("--openapi")) {
|
|
7
|
+
const flagIndex = process.argv.indexOf("--openapi");
|
|
8
|
+
const next = process.argv[flagIndex + 1];
|
|
9
|
+
const outFile = next && !next.startsWith("-") ? next : "openapi.json";
|
|
10
|
+
const { site, vite } = await loadSite(workingDir);
|
|
11
|
+
try {
|
|
12
|
+
await runOpenApi(site, vite, outFile);
|
|
13
|
+
} finally {
|
|
14
|
+
await vite.close();
|
|
15
|
+
}
|
|
16
|
+
} else if (process.argv.includes("--dev")) {
|
|
6
17
|
await run(workingDir, "dev");
|
|
7
18
|
} else if (process.argv.includes("--build")) {
|
|
8
19
|
await run(workingDir, "build");
|
package/src/dev.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev-only entry point re-exporting the server-side pieces that adapters need
|
|
3
|
+
* to assemble a dev runtime (e.g. the Cloudflare adapter's workerd dev worker).
|
|
4
|
+
*
|
|
5
|
+
* Pointed at by the `@torpor/build/dev` export (source, so Vite transforms and
|
|
6
|
+
* HMR apply). Not intended for production use — production workers are bundled
|
|
7
|
+
* from the built output instead.
|
|
8
|
+
*/
|
|
9
|
+
export { default as Server } from "./server/Server.ts";
|
|
10
|
+
export { load } from "./site/serverEntry.ts";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { FormDataRecord } from "../types/ServerLoadEvent";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Converts the submitted fields of a form request into a plain record: one
|
|
5
|
+
* value per field, or an array when a field was submitted with multiple
|
|
6
|
+
* values (e.g. a multi select). File inputs pass through as `File` values;
|
|
7
|
+
* coercing them is left to the schema or the action.
|
|
8
|
+
*
|
|
9
|
+
* @param request The request (or FormData directly)
|
|
10
|
+
* @returns The field values
|
|
11
|
+
*/
|
|
12
|
+
export default async function formDataToRecord(
|
|
13
|
+
request: Request | FormData,
|
|
14
|
+
): Promise<FormDataRecord> {
|
|
15
|
+
const formData = request instanceof FormData ? request : await request.formData();
|
|
16
|
+
const result: FormDataRecord = {};
|
|
17
|
+
for (const key of formData.keys()) {
|
|
18
|
+
if (key in result) continue;
|
|
19
|
+
const values = formData.getAll(key);
|
|
20
|
+
result[key] = values.length > 1 ? values : values[0];
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The values read from a form submission, coerced by the spec's default
|
|
3
|
+
* value types: string defaults produce strings, number defaults produce
|
|
4
|
+
* numbers, boolean defaults produce booleans, and array defaults produce
|
|
5
|
+
* arrays of the element type (an empty array default produces strings).
|
|
6
|
+
*/
|
|
7
|
+
export type FormValues<Spec extends Record<string, unknown>> = {
|
|
8
|
+
[K in keyof Spec]: FormValue<Spec[K]>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type FormValue<T> = T extends boolean
|
|
12
|
+
? boolean
|
|
13
|
+
: T extends number
|
|
14
|
+
? number
|
|
15
|
+
: T extends readonly unknown[]
|
|
16
|
+
? [T[number]] extends [never]
|
|
17
|
+
? string[]
|
|
18
|
+
: T[number][]
|
|
19
|
+
: string;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Reads and coerces the submitted fields of a form action, e.g.
|
|
23
|
+
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* const data = await readForm(request, {
|
|
26
|
+
* title: "",
|
|
27
|
+
* done: false,
|
|
28
|
+
* priority: 0,
|
|
29
|
+
* tags: [],
|
|
30
|
+
* });
|
|
31
|
+
* // data: { title: string; done: boolean; priority: number; tags: string[] }
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* The spec object declares the expected fields; each key's default value
|
|
35
|
+
* determines the type and the fallback when the field is missing or can't be
|
|
36
|
+
* coerced. Booleans follow checkbox semantics (unchecked fields are absent,
|
|
37
|
+
* so they come back as `false`).
|
|
38
|
+
*
|
|
39
|
+
* @param request The action event's request, or its FormData directly
|
|
40
|
+
* @param spec An object mapping field names to default values
|
|
41
|
+
* @returns The coerced field values
|
|
42
|
+
*/
|
|
43
|
+
export default async function readForm<Spec extends Record<string, unknown>>(
|
|
44
|
+
request: Request | FormData,
|
|
45
|
+
spec: Spec,
|
|
46
|
+
): Promise<FormValues<Spec>> {
|
|
47
|
+
const formData = request instanceof FormData ? request : await request.formData();
|
|
48
|
+
const result: Record<string, unknown> = {};
|
|
49
|
+
for (const key of Object.keys(spec)) {
|
|
50
|
+
const defaultValue = spec[key];
|
|
51
|
+
// oxlint-disable-next-line typescript/no-base-to-string
|
|
52
|
+
const values = formData.getAll(key).map((v) => v.toString());
|
|
53
|
+
result[key] = Array.isArray(defaultValue)
|
|
54
|
+
? values.map((value) => coerceValue(value, defaultValue[0]))
|
|
55
|
+
: coerceValue(values[0], defaultValue);
|
|
56
|
+
}
|
|
57
|
+
return result as FormValues<Spec>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function coerceValue(value: string | undefined, defaultValue: unknown): unknown {
|
|
61
|
+
if (typeof defaultValue === "number") {
|
|
62
|
+
const n = value === undefined || value === "" ? NaN : Number(value);
|
|
63
|
+
return Number.isNaN(n) ? defaultValue : n;
|
|
64
|
+
}
|
|
65
|
+
if (typeof defaultValue === "boolean") {
|
|
66
|
+
// Unchecked checkboxes are not submitted at all
|
|
67
|
+
if (value === undefined) return false;
|
|
68
|
+
return !(value === "" || value === "false" || value === "off");
|
|
69
|
+
}
|
|
70
|
+
return value ?? defaultValue ?? "";
|
|
71
|
+
}
|
package/src/form.ts
ADDED
package/src/index.ts
CHANGED
|
@@ -1,15 +1,43 @@
|
|
|
1
1
|
import Router from "./site/Router";
|
|
2
2
|
import Site from "./site/Site";
|
|
3
3
|
import type Adapter from "./types/Adapter";
|
|
4
|
+
import type { Jsonify } from "./types/Jsonify";
|
|
5
|
+
import type { MergePageData, PageData } from "./types/PageData";
|
|
6
|
+
import type { PageForm } from "./types/PageForm";
|
|
4
7
|
import type PageEndPoint from "./types/PageEndPoint";
|
|
5
8
|
import type PageLoadEvent from "./types/PageLoadEvent";
|
|
9
|
+
import type { PageLoadReturn } from "./types/PageLoadReturn";
|
|
10
|
+
import type PageProps from "./types/PageProps";
|
|
11
|
+
import type {
|
|
12
|
+
ParseRouteParams,
|
|
13
|
+
RouteArgs,
|
|
14
|
+
RouteArgsOf,
|
|
15
|
+
RouteParamsOf,
|
|
16
|
+
} from "./types/ParseRouteParams";
|
|
6
17
|
import type PageServerAction from "./types/PageServerAction";
|
|
7
18
|
import type PageServerEndPoint from "./types/PageServerEndPoint";
|
|
19
|
+
import type {
|
|
20
|
+
ActionBody,
|
|
21
|
+
LoadParams,
|
|
22
|
+
LoadQuery,
|
|
23
|
+
PageServerActionSchemas,
|
|
24
|
+
} from "./types/PageServerEndPoint";
|
|
8
25
|
import type PageServerLoad from "./types/PageServerLoad";
|
|
26
|
+
import type TypedResponse from "./response/TypedResponse";
|
|
27
|
+
import type { UntypedResponse } from "./response/TypedResponse";
|
|
9
28
|
import type ServerEndPoint from "./types/ServerEndPoint";
|
|
29
|
+
import type {
|
|
30
|
+
ParamsOf,
|
|
31
|
+
SchemaBody,
|
|
32
|
+
SchemaQuery,
|
|
33
|
+
ServerEndPointSchemas,
|
|
34
|
+
} from "./types/ServerEndPoint";
|
|
10
35
|
import type ServerHook from "./types/ServerHook";
|
|
11
36
|
import type ServerLoadEvent from "./types/ServerLoadEvent";
|
|
37
|
+
import type { FormDataRecord, QueryRecord } from "./types/ServerLoadEvent";
|
|
12
38
|
import type ServerRequest from "./types/ServerRequest";
|
|
39
|
+
import type { StandardSchemaV1 } from "./types/StandardSchema";
|
|
40
|
+
import type SitePlugin from "./types/SitePlugin";
|
|
13
41
|
|
|
14
42
|
// NOTE: Don't export any general functionality from here -- it may result in
|
|
15
43
|
// Vite errors like `Could not resolve '../pkg' in lightningcss` etc
|
|
@@ -20,11 +48,35 @@ export type {
|
|
|
20
48
|
Adapter,
|
|
21
49
|
PageEndPoint,
|
|
22
50
|
PageServerEndPoint,
|
|
51
|
+
PageServerActionSchemas,
|
|
52
|
+
ActionBody,
|
|
53
|
+
LoadQuery,
|
|
54
|
+
LoadParams,
|
|
23
55
|
PageServerLoad,
|
|
24
56
|
PageServerAction,
|
|
25
57
|
ServerEndPoint,
|
|
58
|
+
ServerEndPointSchemas,
|
|
59
|
+
SchemaBody,
|
|
60
|
+
SchemaQuery,
|
|
61
|
+
ParamsOf,
|
|
26
62
|
ServerRequest,
|
|
27
63
|
ServerHook,
|
|
28
|
-
PageLoadEvent,
|
|
29
64
|
ServerLoadEvent,
|
|
65
|
+
SitePlugin,
|
|
66
|
+
FormDataRecord,
|
|
67
|
+
QueryRecord,
|
|
68
|
+
StandardSchemaV1,
|
|
69
|
+
PageLoadEvent,
|
|
70
|
+
ParseRouteParams,
|
|
71
|
+
RouteArgs,
|
|
72
|
+
RouteArgsOf,
|
|
73
|
+
RouteParamsOf,
|
|
74
|
+
Jsonify,
|
|
75
|
+
TypedResponse,
|
|
76
|
+
UntypedResponse,
|
|
77
|
+
PageData,
|
|
78
|
+
MergePageData,
|
|
79
|
+
PageForm,
|
|
80
|
+
PageLoadReturn,
|
|
81
|
+
PageProps,
|
|
30
82
|
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ApiMethods } from "./api";
|
|
2
|
+
import makeApi from "./api";
|
|
3
|
+
import ok from "../response/ok";
|
|
4
|
+
import type TypedResponse from "../response/TypedResponse";
|
|
5
|
+
import type ServerLoadEvent from "../types/ServerLoadEvent";
|
|
6
|
+
|
|
7
|
+
// A sample endpoint shape, as inferred from a `+server` default export
|
|
8
|
+
interface TimeEndPoint {
|
|
9
|
+
get: (event: ServerLoadEvent<"/api/time">) => Promise<TypedResponse<{ time: number }>>;
|
|
10
|
+
post: (event: ServerLoadEvent<"/api/time">) => Promise<Response>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// An endpoint whose handler annotates an expected request body
|
|
14
|
+
interface CreatePostEndPoint {
|
|
15
|
+
post: (
|
|
16
|
+
event: ServerLoadEvent<"/api/posts", { title: string }>,
|
|
17
|
+
) => Promise<TypedResponse<{ id: number }>>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type Equals<A, B> =
|
|
21
|
+
(<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
|
|
22
|
+
type Expect<T extends true> = T;
|
|
23
|
+
|
|
24
|
+
// The get caller resolves to the typed JSON body (or a raw Response)
|
|
25
|
+
export type R01 = Expect<
|
|
26
|
+
Equals<
|
|
27
|
+
ApiMethods<TimeEndPoint>["get"],
|
|
28
|
+
(body?: BodyInit | object, init?: RequestInit) => Promise<{ time: number } | Response>
|
|
29
|
+
>
|
|
30
|
+
>;
|
|
31
|
+
|
|
32
|
+
// Handlers that return a plain Response fall back to unknown
|
|
33
|
+
export type R02 = Expect<
|
|
34
|
+
Equals<
|
|
35
|
+
ApiMethods<TimeEndPoint>["post"],
|
|
36
|
+
(body?: BodyInit | object, init?: RequestInit) => Promise<unknown>
|
|
37
|
+
>
|
|
38
|
+
>;
|
|
39
|
+
|
|
40
|
+
// Only defined handlers are callable
|
|
41
|
+
export type R03 = Expect<Equals<keyof ApiMethods<TimeEndPoint>, "get" | "post">>;
|
|
42
|
+
|
|
43
|
+
// An annotated event body types the client call's body param
|
|
44
|
+
export type R04 = Expect<
|
|
45
|
+
Equals<
|
|
46
|
+
ApiMethods<CreatePostEndPoint>["post"],
|
|
47
|
+
(body?: { title: string }, init?: RequestInit) => Promise<{ id: number } | Response>
|
|
48
|
+
>
|
|
49
|
+
>;
|
|
50
|
+
|
|
51
|
+
// Usage: params are enforced by the route path
|
|
52
|
+
export const timeApi: ApiMethods<TimeEndPoint> = makeApi<"/api/time", TimeEndPoint>("/api/time");
|
|
53
|
+
export const postApi: ApiMethods<TimeEndPoint> = makeApi<"/api/posts/[id]", TimeEndPoint>(
|
|
54
|
+
"/api/posts/[id]",
|
|
55
|
+
{
|
|
56
|
+
id: "5",
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
// @ts-expect-error missing params
|
|
60
|
+
void makeApi<"/api/posts/[id]", TimeEndPoint>("/api/posts/[id]");
|
|
61
|
+
// @ts-expect-error params are not accepted for static paths
|
|
62
|
+
void makeApi<"/api/time", TimeEndPoint>("/api/time", {});
|
|
63
|
+
// @ts-expect-error 'nope' is not a param of /api/posts/[id]
|
|
64
|
+
void makeApi<"/api/posts/[id]", TimeEndPoint>("/api/posts/[id]", { nope: "1" });
|
|
65
|
+
|
|
66
|
+
// Usage: an annotated body is checked at the call site
|
|
67
|
+
export const createApi: ApiMethods<CreatePostEndPoint> = makeApi<"/api/posts", CreatePostEndPoint>(
|
|
68
|
+
"/api/posts",
|
|
69
|
+
);
|
|
70
|
+
void createApi.post({ title: "Hello" });
|
|
71
|
+
// @ts-expect-error 'name' is not part of the endpoint's expected body
|
|
72
|
+
void createApi.post({ name: "Hello" });
|
|
73
|
+
// @ts-expect-error 'title' is required
|
|
74
|
+
void createApi.post({});
|
|
75
|
+
|
|
76
|
+
// ok() typing: object bodies are typed, strings are not
|
|
77
|
+
export const typed: TypedResponse<{ time: number }> = ok({ time: 5 });
|
|
78
|
+
export const untypedResponse: Response = ok("plain");
|
|
79
|
+
export const noBodyResponse: Response = ok();
|
package/src/nav/api.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { ExactRouteParams, ParseRouteParams } from "../types/ParseRouteParams";
|
|
2
|
+
import type TypedResponse from "../response/TypedResponse";
|
|
3
|
+
import route from "./route";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The value a client call resolves to: the endpoint's typed JSON body when it
|
|
7
|
+
* returns a typed response, or the raw Response when the body isn't JSON.
|
|
8
|
+
* Untyped endpoints resolve to `unknown`.
|
|
9
|
+
*/
|
|
10
|
+
type ApiResult<Handler> = Handler extends (event: any) => any
|
|
11
|
+
? Awaited<ReturnType<Handler>> extends TypedResponse<infer Body>
|
|
12
|
+
? Body | Response
|
|
13
|
+
: unknown
|
|
14
|
+
: never;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The body a client call accepts: the endpoint event's annotated JSON body
|
|
18
|
+
* type (from `ServerLoadEvent<Route, Body>`), or a loose value when the
|
|
19
|
+
* handler doesn't annotate one.
|
|
20
|
+
*/
|
|
21
|
+
type ApiRequestBody<Handler> = Handler extends (event: infer Event) => any
|
|
22
|
+
? Event extends { json: () => Promise<infer Body> }
|
|
23
|
+
? unknown extends Body
|
|
24
|
+
? BodyInit | object
|
|
25
|
+
: Body
|
|
26
|
+
: BodyInit | object
|
|
27
|
+
: BodyInit | object;
|
|
28
|
+
|
|
29
|
+
/** The callable methods of an endpoint, one per handler it defines. */
|
|
30
|
+
export type ApiMethods<Endpoint extends object> = {
|
|
31
|
+
[Method in string & keyof Endpoint as NonNullable<Endpoint[Method]> extends (event: any) => any
|
|
32
|
+
? Method
|
|
33
|
+
: never]: (
|
|
34
|
+
body?: ApiRequestBody<NonNullable<Endpoint[Method]>>,
|
|
35
|
+
init?: RequestInit,
|
|
36
|
+
) => Promise<ApiResult<NonNullable<Endpoint[Method]>>>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const METHODS = ["get", "post", "patch", "put", "del", "options", "head"] as const;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Creates typed client callers for a `+server` endpoint, e.g.
|
|
43
|
+
*
|
|
44
|
+
* ```ts
|
|
45
|
+
* import type endpoint from "@/routes/api/posts/[id]/+server";
|
|
46
|
+
* const post = makeApi<"/api/posts/[id]", typeof endpoint>("/api/posts/[id]");
|
|
47
|
+
* const data = await post.get(); // typed from the endpoint's responses
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* Method names match the endpoint's handlers (`del` for DELETE). Params are
|
|
51
|
+
* filled in and URI-encoded from the route path, and JSON responses are
|
|
52
|
+
* parsed; non-JSON responses resolve to the raw Response. Failed requests
|
|
53
|
+
* (non-2xx) throw.
|
|
54
|
+
*
|
|
55
|
+
* @param path The endpoint's route path
|
|
56
|
+
* @param params The route params, required if the path has dynamic segments
|
|
57
|
+
* @returns An object with one caller per handler defined by the endpoint
|
|
58
|
+
*/
|
|
59
|
+
export default function makeApi<
|
|
60
|
+
Route extends string,
|
|
61
|
+
Endpoint extends object,
|
|
62
|
+
Params extends ParseRouteParams<Route> = ParseRouteParams<Route>,
|
|
63
|
+
>(
|
|
64
|
+
path: Route,
|
|
65
|
+
// NOTE: The rest args conditional must stay INLINE — routing it through a
|
|
66
|
+
// type alias defeats `Params` inference, and excess keys stop being checked
|
|
67
|
+
...args: string extends Route
|
|
68
|
+
? [params?: Record<string, string>]
|
|
69
|
+
: keyof ParseRouteParams<Route> extends never
|
|
70
|
+
? []
|
|
71
|
+
: [params: Params & ExactRouteParams<Params, ParseRouteParams<Route>>]
|
|
72
|
+
): ApiMethods<Endpoint> {
|
|
73
|
+
const url = route(path as string, ...(args as [params?: Record<string, string>]));
|
|
74
|
+
const api: Record<string, (body?: unknown, init?: RequestInit) => Promise<unknown>> = {};
|
|
75
|
+
for (const method of METHODS) {
|
|
76
|
+
api[method] = (body, init) => request(method, url, body, init);
|
|
77
|
+
}
|
|
78
|
+
return api as ApiMethods<Endpoint>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function request(
|
|
82
|
+
method: string,
|
|
83
|
+
url: string,
|
|
84
|
+
body: unknown,
|
|
85
|
+
init?: RequestInit,
|
|
86
|
+
): Promise<unknown> {
|
|
87
|
+
// `del` is the endpoint handler name for DELETE requests
|
|
88
|
+
const httpMethod = method === "del" ? "delete" : method;
|
|
89
|
+
const options: RequestInit = { method: httpMethod.toUpperCase(), ...init };
|
|
90
|
+
if (body !== undefined && options.body === undefined && method !== "get" && method !== "head") {
|
|
91
|
+
if (typeof body === "object") {
|
|
92
|
+
options.headers ??= { "Content-Type": "application/json" };
|
|
93
|
+
options.body = JSON.stringify(body);
|
|
94
|
+
} else {
|
|
95
|
+
options.body = body as BodyInit;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const response = await fetch(url, options);
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
throw new Error(`API ${method.toUpperCase()} ${url} failed: ${response.status}`);
|
|
101
|
+
}
|
|
102
|
+
const contentType = response.headers.get("Content-Type") ?? "";
|
|
103
|
+
return contentType.includes("application/json") ? await response.json() : response;
|
|
104
|
+
}
|
package/src/nav/formSubmit.ts
CHANGED
|
@@ -29,7 +29,7 @@ export default async function formSubmit(e: SubmitEvent): Promise<void> {
|
|
|
29
29
|
// error response (if applicable) rather than the reloaded page that you
|
|
30
30
|
// would get if submitting a page without javascript
|
|
31
31
|
headers: {
|
|
32
|
-
"
|
|
32
|
+
"X-Torpor-Form-Submit": "",
|
|
33
33
|
},
|
|
34
34
|
};
|
|
35
35
|
const response = await fetch(
|
|
@@ -38,9 +38,23 @@ export default async function formSubmit(e: SubmitEvent): Promise<void> {
|
|
|
38
38
|
options,
|
|
39
39
|
);
|
|
40
40
|
|
|
41
|
-
if (
|
|
41
|
+
if (
|
|
42
|
+
response.status === 200 &&
|
|
43
|
+
response.headers.has("Content-Disposition") &&
|
|
44
|
+
/attachment;\s*filename="(.+?)"/.test(response.headers.get("Content-Disposition")!)
|
|
45
|
+
) {
|
|
46
|
+
// TODO: Is this enough to check to see if we have a file that should download?
|
|
47
|
+
// Download the file by creating a dummy <a> and clicking it
|
|
48
|
+
const downloadFileName =
|
|
49
|
+
response.headers.get("Content-Disposition")?.match(/attachment;\s*filename="(.+?)"/)![1] ??
|
|
50
|
+
"file";
|
|
51
|
+
const downloadLink = document.createElement("a");
|
|
52
|
+
downloadLink.href = window.URL.createObjectURL(await response.blob());
|
|
53
|
+
downloadLink.download = downloadFileName;
|
|
54
|
+
downloadLink.click();
|
|
55
|
+
} else if (response.status === 200 && response.headers.has("X-Torpor-Form-Redirect")) {
|
|
42
56
|
// TODO: Should do the correct type of redirect e.g. seeOther etc
|
|
43
|
-
const redirect = response.headers.get("
|
|
57
|
+
const redirect = response.headers.get("Location")!;
|
|
44
58
|
await load(redirect);
|
|
45
59
|
window.location.href = redirect;
|
|
46
60
|
} else if (
|
|
@@ -48,7 +62,7 @@ export default async function formSubmit(e: SubmitEvent): Promise<void> {
|
|
|
48
62
|
(response.status >= 400 && response.status <= 499)
|
|
49
63
|
) {
|
|
50
64
|
// Put the response into `$page.form`, and it will be put into `$props` in reload
|
|
51
|
-
if (response.headers.get("
|
|
65
|
+
if (response.headers.get("Content-Type") === "application/json") {
|
|
52
66
|
$page.form = await response.json();
|
|
53
67
|
}
|
|
54
68
|
await reload();
|