@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.
Files changed (176) hide show
  1. package/README.md +20 -2
  2. package/dist/Jsonify-Dg6gChd3.d.mts +10 -0
  3. package/dist/Jsonify-Dg6gChd3.d.mts.map +1 -0
  4. package/dist/ParseRouteParams-DBmqlmLh.d.mts +42 -0
  5. package/dist/ParseRouteParams-DBmqlmLh.d.mts.map +1 -0
  6. package/dist/{RouteType-B6BMOyYw.mjs → Router-C4ADUfhv.mjs} +30 -42
  7. package/dist/Router-C4ADUfhv.mjs.map +1 -0
  8. package/dist/{Server-CF5wDJp6.d.mts → Server-C_IKav_e.d.mts} +44 -16
  9. package/dist/Server-C_IKav_e.d.mts.map +1 -0
  10. package/dist/{ServerEvent-CxPwA4lH.mjs → ServerEvent-Dljn5i7A.mjs} +37 -11
  11. package/dist/ServerEvent-Dljn5i7A.mjs.map +1 -0
  12. package/dist/Site-CMa153FA.d.mts +565 -0
  13. package/dist/Site-CMa153FA.d.mts.map +1 -0
  14. package/dist/StandardSchema-D28L8_zZ.d.mts +100 -0
  15. package/dist/StandardSchema-D28L8_zZ.d.mts.map +1 -0
  16. package/dist/TypedResponse-6lznqN4U.d.mts +22 -0
  17. package/dist/TypedResponse-6lznqN4U.d.mts.map +1 -0
  18. package/dist/{_page-BQs4WvCO.mjs → _page-4cQKs_Dn.mjs} +3 -5
  19. package/dist/_page-4cQKs_Dn.mjs.map +1 -0
  20. package/dist/bin/index.d.mts +1 -1
  21. package/dist/bin/index.js +15 -11
  22. package/dist/bin/index.js.map +1 -1
  23. package/dist/flattenHeaders-C_YYLdOq.mjs +114 -0
  24. package/dist/flattenHeaders-C_YYLdOq.mjs.map +1 -0
  25. package/dist/form.d.mts +35 -0
  26. package/dist/form.d.mts.map +1 -0
  27. package/dist/form.mjs +48 -0
  28. package/dist/form.mjs.map +1 -0
  29. package/dist/index.d.mts +72 -156
  30. package/dist/index.d.mts.map +1 -1
  31. package/dist/index.mjs +158 -26
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/nav.d.mts +58 -5
  34. package/dist/nav.d.mts.map +1 -1
  35. package/dist/nav.mjs +116 -38
  36. package/dist/nav.mjs.map +1 -1
  37. package/dist/openapi.d.mts +144 -0
  38. package/dist/openapi.d.mts.map +1 -0
  39. package/dist/openapi.mjs +2 -0
  40. package/dist/pathTrie-D4Ax2Mu9.mjs +160 -0
  41. package/dist/pathTrie-D4Ax2Mu9.mjs.map +1 -0
  42. package/dist/plugin-D8Www11B.mjs +202 -0
  43. package/dist/plugin-D8Www11B.mjs.map +1 -0
  44. package/dist/{response-C5TtAsh1.mjs → response-Bg4w2S1q.mjs} +16 -39
  45. package/dist/response-Bg4w2S1q.mjs.map +1 -0
  46. package/dist/response.d.mts +309 -289
  47. package/dist/response.d.mts.map +1 -1
  48. package/dist/response.mjs +3 -4
  49. package/dist/run.d.mts +11 -4
  50. package/dist/run.d.mts.map +1 -1
  51. package/dist/run.mjs +2 -8
  52. package/dist/runOpenApi-BUi1RH1l.mjs +1381 -0
  53. package/dist/runOpenApi-BUi1RH1l.mjs.map +1 -0
  54. package/dist/schema.d.mts +32 -0
  55. package/dist/schema.d.mts.map +1 -0
  56. package/dist/schema.mjs +2 -0
  57. package/dist/server.d.mts +10 -8
  58. package/dist/server.d.mts.map +1 -1
  59. package/dist/server.mjs +132 -4
  60. package/dist/server.mjs.map +1 -0
  61. package/dist/state.d.mts.map +1 -1
  62. package/dist/state.mjs +2 -3
  63. package/dist/test.d.mts +8 -10
  64. package/dist/test.d.mts.map +1 -1
  65. package/dist/test.mjs +283 -79
  66. package/dist/test.mjs.map +1 -1
  67. package/dist/{seeOther-B4Yhu9iq.mjs → unprocessable-DAZbrDeR.mjs} +34 -13
  68. package/dist/unprocessable-DAZbrDeR.mjs.map +1 -0
  69. package/dist/validate-Ok4krlkX.mjs +40 -0
  70. package/dist/validate-Ok4krlkX.mjs.map +1 -0
  71. package/package.json +28 -17
  72. package/src/bin/index.ts +13 -2
  73. package/src/dev.ts +10 -0
  74. package/src/form/formDataToRecord.ts +23 -0
  75. package/src/form/readForm.ts +71 -0
  76. package/src/form.ts +4 -0
  77. package/src/index.ts +53 -1
  78. package/src/nav/api.test-d.ts +79 -0
  79. package/src/nav/api.ts +104 -0
  80. package/src/nav/formSubmit.ts +18 -4
  81. package/src/nav/navigate.ts +40 -15
  82. package/src/nav/route.ts +38 -0
  83. package/src/nav.ts +3 -1
  84. package/src/openapi/docsHtml.ts +25 -0
  85. package/src/openapi/document.ts +154 -0
  86. package/src/openapi/plugin.ts +78 -0
  87. package/src/openapi/types.ts +82 -0
  88. package/src/openapi.ts +14 -0
  89. package/src/response/TypedResponse.ts +17 -0
  90. package/src/response/badRequest.ts +14 -2
  91. package/src/response/created.ts +13 -2
  92. package/src/response/found.ts +2 -2
  93. package/src/response/movedPermanently.ts +2 -2
  94. package/src/response/notModified.ts +2 -2
  95. package/src/response/ok.ts +13 -4
  96. package/src/response/response.ts +4 -4
  97. package/src/response/unprocessable.ts +14 -2
  98. package/src/run/depCache.ts +175 -0
  99. package/src/run/devPlugin.ts +128 -0
  100. package/src/run/prepareTemplate.ts +6 -3
  101. package/src/run/run.ts +63 -39
  102. package/src/run/runBuild.ts +63 -12
  103. package/src/run/runDev.ts +146 -58
  104. package/src/run/runOpenApi.ts +52 -0
  105. package/src/run/runPreview.ts +17 -26
  106. package/src/run/staleTorpCopies.ts +100 -0
  107. package/src/run.ts +2 -1
  108. package/src/schema.ts +7 -0
  109. package/src/server/CookieHelper.ts +17 -7
  110. package/src/server/Server.ts +54 -30
  111. package/src/server/ServerEvent.ts +24 -1
  112. package/src/server/connect/connectMiddleware.ts +44 -40
  113. package/src/server/connect/flattenHeaders.ts +1 -23
  114. package/src/server/connect/requestToNodeMessage.ts +6 -3
  115. package/src/server/types/MiddlewareFunction.ts +18 -4
  116. package/src/site/Router.ts +52 -41
  117. package/src/site/Site.ts +184 -10
  118. package/src/site/checkLayoutSlots.ts +114 -0
  119. package/src/site/checkRoutes.ts +435 -0
  120. package/src/site/clientEntry.ts +22 -9
  121. package/src/site/layoutSlots.ts +53 -0
  122. package/src/site/manifest.ts +125 -6
  123. package/src/site/serverEntry.ts +341 -132
  124. package/src/state/$page.ts +2 -1
  125. package/src/state/$serverPage.ts +22 -0
  126. package/src/test/runTest.ts +306 -118
  127. package/src/types/Adapter.ts +12 -0
  128. package/src/types/Jsonify.ts +20 -0
  129. package/src/types/PageData.test-d.ts +136 -0
  130. package/src/types/PageData.ts +35 -0
  131. package/src/types/PageEndPoint.ts +25 -9
  132. package/src/types/PageForm.test-d.ts +78 -0
  133. package/src/types/PageForm.ts +29 -0
  134. package/src/types/PageLoadEvent.ts +16 -4
  135. package/src/types/PageLoadReturn.ts +13 -0
  136. package/src/types/PageProps.ts +14 -0
  137. package/src/types/PageServerAction.ts +8 -2
  138. package/src/types/PageServerEndPoint.test-d.ts +105 -0
  139. package/src/types/PageServerEndPoint.ts +99 -5
  140. package/src/types/PageServerLoad.ts +11 -3
  141. package/src/types/ParseRouteParams.test-d.ts +84 -0
  142. package/src/types/ParseRouteParams.ts +64 -0
  143. package/src/types/Route.ts +20 -1
  144. package/src/types/RouteHandler.ts +8 -1
  145. package/src/types/ServerEndPoint.test-d.ts +77 -0
  146. package/src/types/ServerEndPoint.ts +148 -16
  147. package/src/types/ServerHook.ts +10 -3
  148. package/src/types/ServerLoadEvent.ts +48 -3
  149. package/src/types/ServerRequest.ts +10 -2
  150. package/src/types/SitePlugin.ts +24 -0
  151. package/src/types/StandardSchema.ts +109 -0
  152. package/src/utils/pathToRegex.ts +4 -2
  153. package/src/utils/pathTrie.ts +182 -0
  154. package/src/utils/searchParamsToRecord.ts +18 -0
  155. package/src/utils/torporPackages.ts +150 -0
  156. package/src/utils/tsconfigAliases.ts +90 -0
  157. package/src/validation/ValidationError.ts +18 -0
  158. package/src/validation/endpoint.ts +83 -0
  159. package/src/validation/validate.ts +26 -0
  160. package/dist/RouteType-B6BMOyYw.mjs.map +0 -1
  161. package/dist/Server-CF5wDJp6.d.mts.map +0 -1
  162. package/dist/ServerEvent-CxPwA4lH.mjs.map +0 -1
  163. package/dist/Site-DgC6WWn1.d.mts +0 -78
  164. package/dist/Site-DgC6WWn1.d.mts.map +0 -1
  165. package/dist/_page-BQs4WvCO.mjs.map +0 -1
  166. package/dist/connectMiddleware-D7nehjj3.mjs +0 -252
  167. package/dist/connectMiddleware-D7nehjj3.mjs.map +0 -1
  168. package/dist/pathToRegex-TUAMc3zS.mjs +0 -11
  169. package/dist/pathToRegex-TUAMc3zS.mjs.map +0 -1
  170. package/dist/response-C5TtAsh1.mjs.map +0 -1
  171. package/dist/run-BPFtSYA3.mjs +0 -332
  172. package/dist/run-BPFtSYA3.mjs.map +0 -1
  173. package/dist/seeOther-B4Yhu9iq.mjs.map +0 -1
  174. package/src/server/Routerx.ts +0 -72
  175. package/src/server/connect/bufferToArrayBuffer.ts +0 -8
  176. 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.4.14",
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": "^1.0.2",
54
- "dotenv": "^17.2.3",
55
- "vite": "^7.2.2",
56
- "vite-tsconfig-paths": "^5.1.4",
57
- "@torpor/unplugin": "^0.2.1",
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
- "@trivago/prettier-plugin-sort-imports": "^6.0.0",
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
- "oxlint-tsgolint": "^0.6.0",
65
- "tsdown": "^0.16.4",
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 && tsdown",
72
- "build:fix": "esno scripts/postbuild.ts",
73
- "build:watch": "tsdown --watch",
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("--dev")) {
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
@@ -0,0 +1,4 @@
1
+ import readForm from "./form/readForm";
2
+
3
+ export { readForm };
4
+ export type { FormValues } from "./form/readForm";
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
+ }
@@ -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
- "x-torpor-form-submit": "",
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 (response.status === 200 && response.headers.has("x-torpor-form-redirect")) {
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("location")!;
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("content-type") === "application/json") {
65
+ if (response.headers.get("Content-Type") === "application/json") {
52
66
  $page.form = await response.json();
53
67
  }
54
68
  await reload();