@torpor/build 0.0.5 → 0.1.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 (106) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -1
  3. package/dist/Server-CB99REgs.d.cts +74 -0
  4. package/dist/Server-CB99REgs.d.ts +74 -0
  5. package/dist/Site-CgoeDt8k.d.cts +29 -0
  6. package/dist/Site-D95nwrAg.d.ts +29 -0
  7. package/dist/adapters/cloudflare/index.cjs +125 -0
  8. package/dist/adapters/cloudflare/index.cjs.map +1 -0
  9. package/dist/adapters/cloudflare/index.d.cts +13 -0
  10. package/dist/adapters/cloudflare/index.d.ts +13 -0
  11. package/dist/adapters/cloudflare/index.js +80 -0
  12. package/dist/adapters/cloudflare/index.js.map +1 -0
  13. package/dist/adapters/node/index.cjs +152 -0
  14. package/dist/adapters/node/index.cjs.map +1 -0
  15. package/dist/adapters/node/index.d.cts +8 -0
  16. package/dist/adapters/node/index.d.ts +8 -0
  17. package/dist/adapters/node/index.js +8 -0
  18. package/dist/adapters/node/index.js.map +1 -0
  19. package/dist/bin/index.cjs +541 -93
  20. package/dist/bin/index.cjs.map +1 -1
  21. package/dist/bin/index.js +511 -22
  22. package/dist/bin/index.js.map +1 -1
  23. package/dist/chunk-5X4T3PZN.js +23 -0
  24. package/dist/chunk-5X4T3PZN.js.map +1 -0
  25. package/dist/chunk-ALHFYD7U.js +113 -0
  26. package/dist/chunk-ALHFYD7U.js.map +1 -0
  27. package/dist/chunk-V4XTDQEV.js +19 -0
  28. package/dist/chunk-V4XTDQEV.js.map +1 -0
  29. package/dist/chunk-VAGHGWM5.js +110 -0
  30. package/dist/chunk-VAGHGWM5.js.map +1 -0
  31. package/dist/index.cjs +200 -97
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +33 -36
  34. package/dist/index.d.ts +33 -36
  35. package/dist/index.js +85 -17
  36. package/dist/index.js.map +1 -1
  37. package/dist/metafile-cjs.json +1 -1
  38. package/dist/metafile-esm.json +1 -1
  39. package/dist/response.js +17 -91
  40. package/dist/response.js.map +1 -1
  41. package/package.json +26 -9
  42. package/src/adapters/cloudflare/_worker.ts +38 -0
  43. package/src/adapters/cloudflare/index.ts +98 -0
  44. package/src/adapters/node/constants.ts +1 -0
  45. package/src/adapters/node/createFetchHandler.ts +29 -0
  46. package/src/adapters/node/createMiddlewareHandler.ts +79 -0
  47. package/src/adapters/node/createNodeServer.ts +8 -0
  48. package/src/adapters/node/flattenHeaders.ts +44 -0
  49. package/src/adapters/node/index.ts +18 -0
  50. package/src/adapters/node/nodeMessageToNodeResponse.ts +89 -0
  51. package/src/adapters/node/nodeMessageToRequest.ts +43 -0
  52. package/src/adapters/node/readableToBuffer.ts +16 -0
  53. package/src/adapters/node/requestToNodeMessage.ts +26 -0
  54. package/src/adapters/node/responseToNodeResponse.ts +44 -0
  55. package/src/bin/index.ts +43 -36
  56. package/src/index.ts +6 -14
  57. package/src/server/CookieHelper.ts +36 -0
  58. package/src/server/HeaderHelper.ts +25 -0
  59. package/src/server/Server.ts +119 -0
  60. package/src/server/ServerEvent.ts +33 -0
  61. package/src/server/ServerFunction.ts +5 -0
  62. package/src/server/contentType.ts +85 -0
  63. package/src/site/Router.ts +136 -0
  64. package/src/site/Routerx.ts +67 -0
  65. package/src/site/Site.ts +114 -0
  66. package/src/site/clientEntry.ts +22 -20
  67. package/src/site/manifest.ts +49 -0
  68. package/src/site/pathToRegex.ts +10 -0
  69. package/src/site/prepareTemplate.ts +20 -0
  70. package/src/site/runBuild.ts +92 -0
  71. package/src/site/runDev.ts +84 -0
  72. package/src/site/runPreview.ts +83 -0
  73. package/src/site/serverEntry.ts +113 -200
  74. package/src/types/Adapter.ts +10 -0
  75. package/src/types/HttpMethod.ts +12 -0
  76. package/src/types/LayoutHandler.ts +7 -0
  77. package/src/types/ManifestRoute.ts +8 -0
  78. package/src/types/MiddlewareFunction.ts +8 -0
  79. package/src/types/PageEndPoint.ts +4 -3
  80. package/src/types/{PageEvent.ts → PageLoadEvent.ts} +3 -2
  81. package/src/types/PageServerAction.ts +5 -2
  82. package/src/types/PageServerEndPoint.ts +1 -0
  83. package/src/types/PageServerLoad.ts +5 -2
  84. package/src/types/RouteHandler.ts +6 -5
  85. package/src/types/RouteHandlerCollection.ts +1 -0
  86. package/src/types/RouteLayoutHandler.ts +3 -2
  87. package/src/types/RouteMatchResult.ts +3 -2
  88. package/src/types/RouteType.ts +7 -0
  89. package/src/types/ServerEndPoint.ts +1 -0
  90. package/src/types/ServerHook.ts +3 -2
  91. package/src/types/{ServerEvent.ts → ServerLoadEvent.ts} +13 -6
  92. package/src/types/ServerRequest.ts +5 -2
  93. package/src/types/manifest.d.ts +7 -0
  94. package/test/app.test.ts +47 -0
  95. package/tsconfig.json +1 -1
  96. package/tsup.config.ts +11 -1
  97. package/vitest.config.ts +3 -0
  98. package/dist/chunk-L7GOYKUG.js +0 -93
  99. package/dist/chunk-L7GOYKUG.js.map +0 -1
  100. package/src/site/FileSystemRouter.ts +0 -25
  101. package/src/site/buildRoutePath.ts +0 -25
  102. package/src/site/defineConfig.ts +0 -75
  103. package/src/site/lazyRoute.ts +0 -72
  104. package/src/site/routeHandlers.ts +0 -126
  105. package/src/types/CookieHelper.ts +0 -8
  106. package/src/types/UserConfig.ts +0 -4
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/response/response.ts","../src/response/badRequest.ts","../src/response/created.ts","../src/response/forbidden.ts","../src/response/methodNotAllowed.ts","../src/response/transfer.ts","../src/response/moved.ts","../src/response/notFound.ts","../src/response/notModified.ts","../src/response/ok.ts","../src/response/redirect.ts","../src/response/seeOther.ts","../src/response/serverError.ts","../src/response/unauthorized.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) {\n\tlet headers: Record<string, string> | undefined = undefined;\n\tif (body) {\n\t\tif (typeof body === \"object\") {\n\t\t\theaders = { \"content-type\": \"application/json\" };\n\t\t\tbody = JSON.stringify(body);\n\t\t} else {\n\t\t\theaders = { \"content-type\": \"text/plain\" };\n\t\t}\n\t}\n\treturn new Response(body, {\n\t\tstatus,\n\t\theaders,\n\t});\n}\n","import response from \"./response\";\n\n/**\n * 400 Bad Request\n *\n * The server cannot or will not process the request due to something that is\n * perceived to be a client error (e.g., malformed request syntax, invalid\n * request message framing, or deceptive request routing).\n *\n * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400\n */\nexport default function badRequest(body?: object | string): Response {\n\treturn response(400, body);\n}\n","import response from \"./response\";\n\n/**\n * 201 Created\n *\n * The request succeeded, and a new resource was created as a result. This is\n * typically the response sent after POST requests, or some PUT requests.\n *\n * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/201\n */\nexport default function created(body?: object | string): Response {\n\treturn response(201, body);\n}\n","import response from \"./response\";\n\n/**\n * 403 Forbidden\n *\n * The client does not have access rights to the content; that is, it is\n * unauthorized, so the server is refusing to give the requested resource.\n * Unlike 401 Unauthorized, the client's identity is known to the server.\n *\n * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403\n */\nexport default function forbidden(body?: object | string): Response {\n\treturn response(403, body);\n}\n","import response from \"./response\";\n\n/**\n * 405 Method Not Allowed\n *\n * The request method is known by the server but is not supported by the target\n * resource. For example, an API may not allow calling DELETE to remove a\n * resource.\n *\n * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405\n */\nexport default function methodNotAllowed(body?: object | string): Response {\n\treturn response(405, body);\n}\n","/**\n * Creates a redirect response with the supplied status code.\n */\nexport default function transfer(\n\tstatus: 300 | 301 | 302 | 303 | 304 | 307 | 308,\n\tlocation: string,\n): Response {\n\treturn new Response(null, {\n\t\tstatus,\n\t\theaders: {\n\t\t\tlocation,\n\t\t},\n\t});\n}\n","import transfer from \"./transfer\";\n\n/**\n * 308 Permanent Redirect\n *\n * This means that the resource is now permanently located at another URI,\n * specified by the Location: HTTP Response header. This has the same semantics\n * as the 301 Moved Permanently HTTP response code, with the exception that the\n * user agent must not change the HTTP method used: if a POST was used in the\n * first request, a POST must be used in the second request.\n *\n * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/308\n */\nexport default function moved(location: string): Response {\n\treturn transfer(308, location);\n}\n","import response from \"./response\";\n\n/**\n * 404 Not Found\n *\n * The server cannot find the requested resource. In the browser, this means the\n * URL is not recognized. In an API, this can also mean that the endpoint is\n * valid but the resource itself does not exist. Servers may also send this\n * response instead of 403 Forbidden to hide the existence of a resource from an\n * unauthorized client. This response code is probably the most well known due\n * to its frequent occurrence on the web.\n *\n * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404\n */\nexport default function notFound(body?: object | string): Response {\n\treturn response(404, body);\n}\n","import transfer from \"./transfer\";\n\n/**\n * 304 Not Modified\n *\n * This is used for caching purposes. It tells the client that the response has\n * not been modified, so the client can continue to use the same cached version\n * of the response.\n *\n * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304\n */\nexport default function notModified(location: string): Response {\n\treturn transfer(304, location);\n}\n","import response from \"./response\";\n\n/**\n * 200 OK\n *\n * The request succeeded. The result meaning of \"success\" depends on the HTTP\n * method:\n * - GET: The resource has been fetched and transmitted in the message body.\n * - HEAD: The representation headers are included in the response without any\n * message body.\n * - PUT or POST: The resource describing the result of the action is\n * transmitted in the message body.\n * - TRACE: The message body contains the request message as received by the\n * server.\n *\n * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/200\n */\nexport default function ok(body?: object | string): Response {\n\treturn response(200, body);\n}\n","import transfer from \"./transfer\";\n\n/**\n * 307 Temporary Redirect\n *\n * The server sends this response to direct the client to get the requested\n * resource at another URI with the same method that was used in the prior\n * request. This has the same semantics as the 302 Found HTTP response code,\n * with the exception that the user agent must not change the HTTP method used:\n * if a POST was used in the first request, a POST must be used in the second\n * request.\n *\n * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/307\n */\nexport default function redirect(location: string): Response {\n\treturn transfer(307, location);\n}\n","import transfer from \"./transfer\";\n\n/**\n * 303 See Other\n *\n * The server sent this response to direct the client to get the requested\n * resource at another URI with a GET request.\n *\n * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/303\n */\nexport default function seeOther(location: string): Response {\n\treturn transfer(303, location);\n}\n","import response from \"./response\";\n\n/**\n * 500 Server Error\n *\n * The server has encountered a situation it does not know how to handle.\n *\n * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500\n */\nexport default function serverError(body?: object | string): Response {\n\treturn response(500, body);\n}\n","import response from \"./response\";\n\n/**\n * 401 Unauthorized\n *\n * Although the HTTP standard specifies \"unauthorized\", semantically this\n * response means \"unauthenticated\". That is, the client must authenticate\n * itself to get the requested response.\n *\n * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401\n */\nexport default function unauthorized(body?: object | string): Response {\n\treturn response(401, body);\n}\n","import response from \"./response\";\n\n/**\n * 422 Unprocessable Content\n *\n * The request was well-formed but was unable to be followed due to semantic errors.\n *\n * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422\n */\nexport default function unprocessable(body?: object | string): Response {\n\treturn response(422, body);\n}\n"],"mappings":";AAOe,SAAR,SAA0B,QAAgB,MAAwB;AACxE,MAAI,UAA8C;AAClD,MAAI,MAAM;AACT,QAAI,OAAO,SAAS,UAAU;AAC7B,gBAAU,EAAE,gBAAgB,mBAAmB;AAC/C,aAAO,KAAK,UAAU,IAAI;AAAA,IAC3B,OAAO;AACN,gBAAU,EAAE,gBAAgB,aAAa;AAAA,IAC1C;AAAA,EACD;AACA,SAAO,IAAI,SAAS,MAAM;AAAA,IACzB;AAAA,IACA;AAAA,EACD,CAAC;AACF;;;ACVe,SAAR,WAA4B,MAAkC;AACpE,SAAO,SAAS,KAAK,IAAI;AAC1B;;;ACHe,SAAR,QAAyB,MAAkC;AACjE,SAAO,SAAS,KAAK,IAAI;AAC1B;;;ACDe,SAAR,UAA2B,MAAkC;AACnE,SAAO,SAAS,KAAK,IAAI;AAC1B;;;ACFe,SAAR,iBAAkC,MAAkC;AAC1E,SAAO,SAAS,KAAK,IAAI;AAC1B;;;ACVe,SAAR,SACN,QACA,UACW;AACX,SAAO,IAAI,SAAS,MAAM;AAAA,IACzB;AAAA,IACA,SAAS;AAAA,MACR;AAAA,IACD;AAAA,EACD,CAAC;AACF;;;ACAe,SAAR,MAAuB,UAA4B;AACzD,SAAO,SAAS,KAAK,QAAQ;AAC9B;;;ACDe,SAAR,SAA0B,MAAkC;AAClE,SAAO,SAAS,KAAK,IAAI;AAC1B;;;ACLe,SAAR,YAA6B,UAA4B;AAC/D,SAAO,SAAS,KAAK,QAAQ;AAC9B;;;ACIe,SAAR,GAAoB,MAAkC;AAC5D,SAAO,SAAS,KAAK,IAAI;AAC1B;;;ACLe,SAAR,SAA0B,UAA4B;AAC5D,SAAO,SAAS,KAAK,QAAQ;AAC9B;;;ACNe,SAAR,SAA0B,UAA4B;AAC5D,SAAO,SAAS,KAAK,QAAQ;AAC9B;;;ACHe,SAAR,YAA6B,MAAkC;AACrE,SAAO,SAAS,KAAK,IAAI;AAC1B;;;ACAe,SAAR,aAA8B,MAAkC;AACtE,SAAO,SAAS,KAAK,IAAI;AAC1B;;;ACJe,SAAR,cAA+B,MAAkC;AACvE,SAAO,SAAS,KAAK,IAAI;AAC1B;","names":[]}
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torpor/build",
3
- "version": "0.0.5",
3
+ "version": "0.1.0",
4
4
  "description": "A framework for building sites and apps with Torpor",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -21,6 +21,19 @@
21
21
  "types": "./dist/state.d.ts",
22
22
  "import": "./dist/state.js",
23
23
  "require": "./dist/state.cjs"
24
+ },
25
+ "./manifest": {
26
+ "types": "./src/types/manifest.d.ts"
27
+ },
28
+ "./adapters/node": {
29
+ "types": "./dist/adapters/node/index.d.ts",
30
+ "import": "./dist/adapters/node/index.js",
31
+ "require": "./dist/adapters/node/index.cjs"
32
+ },
33
+ "./adapters/cloudflare": {
34
+ "types": "./dist/adapters/cloudflare/index.d.ts",
35
+ "import": "./dist/adapters/cloudflare/index.js",
36
+ "require": "./dist/adapters/cloudflare/index.cjs"
24
37
  }
25
38
  },
26
39
  "publishConfig": {
@@ -33,20 +46,24 @@
33
46
  "author": "",
34
47
  "license": "ISC",
35
48
  "dependencies": {
36
- "vinxi": "^0.5.3",
37
- "@torpor/unplugin": "^0.0.5",
38
- "@torpor/view": "^0.0.6"
49
+ "cookie": "^1.0.2",
50
+ "dotenv": "^16.4.7",
51
+ "vite": "^6.2.4",
52
+ "vite-tsconfig-paths": "^5.1.4",
53
+ "@torpor/view": "^0.0.7",
54
+ "@torpor/unplugin": "^0.0.6"
39
55
  },
40
56
  "devDependencies": {
41
57
  "@trivago/prettier-plugin-sort-imports": "^5.2.2",
42
- "lilconfig": "^3.1.3",
43
- "tsup": "^8.3.6",
44
- "typescript": "^5.7.3",
45
- "vite": "^6.1.0"
58
+ "tsup": "^8.4.0",
59
+ "tsx": "^4.19.3",
60
+ "typescript": "^5.8.2",
61
+ "vitest": "^3.1.1"
46
62
  },
47
63
  "scripts": {
48
64
  "build": "tsup",
49
65
  "build:watch": "tsup --watch",
50
- "check": "tsc -p tsconfig.json --noEmit"
66
+ "check": "tsc --noEmit",
67
+ "test": "vitest"
51
68
  }
52
69
  }
@@ -0,0 +1,38 @@
1
+ // @ts-ignore
2
+ import Server from "%SERVER_CLASS%";
3
+ // @ts-ignore
4
+ import { load } from "%SERVER_SCRIPT%";
5
+
6
+ // This file has the above imports replaced and is then compiled to create a Cloudflare Pages Worker
7
+
8
+ const template = `%HTML_TEMPLATE%`;
9
+
10
+ // Send all requests through Server to handle middleware, cookies, headers, etc
11
+ const server = new Server();
12
+ server.add("*", async (ev: any) => {
13
+ try {
14
+ // Render the app HTML (or fetch server data etc) via serverEntry's
15
+ // exported `load` function
16
+ return await load(ev, template);
17
+ } catch (e: any) {
18
+ // TODO: Message?
19
+ console.log("ERROR:", e);
20
+ return new Response(null, {
21
+ status: 500,
22
+ });
23
+ }
24
+ });
25
+
26
+ export default {
27
+ async fetch(request: Request, env: any) {
28
+ const url = new URL(request.url);
29
+
30
+ // Serve dist/client/assets statically
31
+ if (url.pathname.startsWith("/assets/")) {
32
+ return env.ASSETS.fetch(request);
33
+ }
34
+
35
+ // Every request (GET, POST, etc) goes through loadEndPoint
36
+ return await server.fetch(request);
37
+ },
38
+ };
@@ -0,0 +1,98 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { build, defineConfig } from "vite";
5
+ import Site from "../../site/Site";
6
+ import prepareTemplate from "../../site/prepareTemplate";
7
+ import type Adapter from "../../types/Adapter";
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+
11
+ export default {
12
+ postbuild,
13
+ serve: (/* server: Server, site: Site*/) => {
14
+ console.log("TODO");
15
+ },
16
+ } satisfies Adapter;
17
+
18
+ async function postbuild(site: Site) {
19
+ // Build _worker.js as per
20
+ // https://developers.cloudflare.com/pages/functions/advanced-mode/
21
+
22
+ const distFolder = path.resolve(site.root, "dist");
23
+ const clientFolder = path.join(distFolder, "client");
24
+ const serverFolder = path.join(distFolder, "server");
25
+ const tempFolder = path.join(distFolder, "temp");
26
+ const cloudflareFolder = path.join(distFolder, "cloudflare");
27
+
28
+ // Compile _worker.ts
29
+ let workerFile = path.resolve(__dirname, "../../../src/adapters/cloudflare/_worker.ts");
30
+ let workerSource = await fs.readFile(workerFile, "utf-8");
31
+
32
+ // Find the client script file in /assets
33
+ let clientScript = (await fs.readdir(path.join(clientFolder, "assets"))).find((f) =>
34
+ f.startsWith("clientEntry-"),
35
+ );
36
+ if (!clientScript) {
37
+ throw new Error("clientEntry.js not found");
38
+ }
39
+ clientScript = `/assets/${clientScript}`;
40
+
41
+ // Read and prepare site.html so that we can just splice components into it
42
+ let siteHtml = path.join(clientFolder, "site.html");
43
+ let template = await fs.readFile(siteHtml, "utf-8");
44
+ template = prepareTemplate(template, clientScript);
45
+
46
+ const serverClass = path.resolve(__dirname, "../../../src/server/Server.ts");
47
+ const serverScript = path.join(serverFolder, "serverEntry.js");
48
+
49
+ // Splice file names into _worker.ts
50
+ //let workerSource = await fs.readFile(workerFile, "utf-8");
51
+ workerSource = workerSource
52
+ .replace("%SERVER_CLASS%", serverClass)
53
+ .replace("%SERVER_SCRIPT%", serverScript)
54
+ .replace("%HTML_TEMPLATE%", template);
55
+
56
+ workerFile = path.join(tempFolder, "_worker.ts");
57
+ await fs.mkdir(tempFolder);
58
+ await fs.writeFile(workerFile, workerSource);
59
+
60
+ await build(
61
+ defineConfig({
62
+ build: {
63
+ outDir: cloudflareFolder,
64
+ rollupOptions: {
65
+ input: [workerFile],
66
+ output: {
67
+ entryFileNames: `[name].js`,
68
+ chunkFileNames: `[name].js`,
69
+ assetFileNames: `[name].[ext]`,
70
+ },
71
+ preserveEntrySignatures: "strict",
72
+ },
73
+ // HACK: We don't need to minify as the worker isn't downloaded?
74
+ // And it makes debugging easier
75
+ minify: false,
76
+ // HACK: Disable preload so that the _worker.js file doesn't end
77
+ // up with document and window references that are not available
78
+ modulePreload: false,
79
+ },
80
+ }),
81
+ );
82
+ workerFile = path.join(distFolder, "cloudflare", "_worker.js");
83
+
84
+ // Build a wrangler.json file for running with `wrangler dev`
85
+ const wranglerConfig = {
86
+ name: "torpor-miniflare",
87
+ main: "cloudflare/_worker.js",
88
+ compatibility_date: "2025-01-01",
89
+ assets: {
90
+ directory: "./client",
91
+ binding: "ASSETS",
92
+ },
93
+ };
94
+ const wranglerFile = path.join(distFolder, "wrangler.json");
95
+ await fs.writeFile(wranglerFile, JSON.stringify(wranglerConfig, null, 2).replaceAll(" ", "\t"));
96
+
97
+ await fs.rm(tempFolder, { recursive: true });
98
+ }
@@ -0,0 +1 @@
1
+ export const INTERNAL_BODY = Symbol("internal_body");
@@ -0,0 +1,29 @@
1
+ import { type IncomingMessage, ServerResponse } from "node:http";
2
+ import { Http2ServerRequest, Http2ServerResponse } from "node:http2";
3
+ import { INTERNAL_BODY } from "./constants";
4
+ import nodeMessageToRequest from "./nodeMessageToRequest";
5
+ import responseToNodeResponse from "./responseToNodeResponse";
6
+
7
+ // From https://gist.github.com/marvinhagemeister/cc236ec97235ce0305ae9d48a24a607d
8
+ // Referenced from https://marvinh.dev/blog/modern-way-to-write-javascript-servers/
9
+
10
+ const GlobalResponse = Response;
11
+ globalThis.Response = class Response extends GlobalResponse {
12
+ [INTERNAL_BODY]: BodyInit | null | undefined = null;
13
+
14
+ constructor(body?: BodyInit | null, init?: ResponseInit) {
15
+ super(body, init);
16
+ this[INTERNAL_BODY] = body;
17
+ }
18
+ };
19
+
20
+ export function createFetchHandler(handler: (req: Request) => Response | Promise<Response>) {
21
+ return async (
22
+ incoming: IncomingMessage | Http2ServerRequest,
23
+ outgoing: ServerResponse | Http2ServerResponse,
24
+ ) => {
25
+ const req = await nodeMessageToRequest(incoming);
26
+ const res = await handler(req);
27
+ return await responseToNodeResponse(res, outgoing);
28
+ };
29
+ }
@@ -0,0 +1,79 @@
1
+ import { IncomingMessage, ServerResponse } from "node:http";
2
+ import type MiddlewareFunction from "../../types/MiddlewareFunction";
3
+ import flattenHeaders from "./flattenHeaders";
4
+ import nodeMessageToNodeResponse from "./nodeMessageToNodeResponse";
5
+ import readableToBuffer from "./readableToBuffer";
6
+ import requestToNodeMessage from "./requestToNodeMessage";
7
+
8
+ // From https://github.com/vikejs/vike-node/blob/main/packages/vike-node/src/runtime/adapters/connectToWeb.ts
9
+
10
+ /**
11
+ * Converts a Connect-style middleware to a web-compatible request handler.
12
+ *
13
+ * @param handler - The Connect-style middleware function to be converted.
14
+ * @returns A function that handles web requests and returns a Response or undefined.
15
+ */
16
+ export default function createMiddlewareHandler(
17
+ handler: ConnectMiddleware | ConnectMiddlewareBoolean,
18
+ ): MiddlewareFunction {
19
+ return async (ev, next) => {
20
+ const req = requestToNodeMessage(ev.request);
21
+ const { res, onReadable } = nodeMessageToNodeResponse(req);
22
+
23
+ return new Promise<void>(async (resolve, reject) => {
24
+ onReadable(async ({ readable, headers, status }) => {
25
+ const responseBody = statusCodesWithoutBody.includes(status)
26
+ ? null
27
+ : //(Readable.toWeb(readable) as ReadableStream);
28
+ // HACK: Chunked bodies are not being
29
+ // read correctly somewhere...
30
+ await readableToBuffer(readable);
31
+ ev.response = new Response(responseBody, {
32
+ status,
33
+ headers: flattenHeaders(headers),
34
+ });
35
+ resolve();
36
+ });
37
+
38
+ const cnext = async (error?: unknown) => {
39
+ if (error) {
40
+ reject(error instanceof Error ? error : new Error(String(error)));
41
+ } else {
42
+ // TODO: Can we nest the next functions less? This is a bit weird
43
+ await next();
44
+ resolve();
45
+ }
46
+ };
47
+
48
+ try {
49
+ const handled = await handler(req, res, cnext);
50
+ if (handled === false) {
51
+ res.destroy();
52
+ resolve();
53
+ }
54
+ } catch (e) {
55
+ cnext(e);
56
+ }
57
+ });
58
+ };
59
+ }
60
+
61
+ const statusCodesWithoutBody = [
62
+ 100, // Continue
63
+ 101, // Switching Protocols
64
+ 102, // Processing (WebDAV)
65
+ 103, // Early Hints
66
+ 204, // No Content
67
+ 205, // Reset Content
68
+ 304, // Not Modified
69
+ ];
70
+
71
+ type NextFunction = (err?: unknown) => void;
72
+ type ConnectMiddleware<
73
+ PlatformRequest extends IncomingMessage = IncomingMessage,
74
+ PlatformResponse extends ServerResponse = ServerResponse,
75
+ > = (req: PlatformRequest, res: PlatformResponse, next: NextFunction) => void | Promise<void>;
76
+ type ConnectMiddlewareBoolean<
77
+ PlatformRequest extends IncomingMessage = IncomingMessage,
78
+ PlatformResponse extends ServerResponse = ServerResponse,
79
+ > = (req: PlatformRequest, res: PlatformResponse, next: NextFunction) => boolean | Promise<boolean>;
@@ -0,0 +1,8 @@
1
+ import { createServer } from "node:http";
2
+ import { createFetchHandler } from "./createFetchHandler";
3
+
4
+ export default function createNodeServer(handler: (req: Request) => Response | Promise<Response>) {
5
+ // Create a server, polyfill it so that it can handle standard fetch methods,
6
+ // and pass our app fetch method to the handler
7
+ return createServer(createFetchHandler(handler));
8
+ }
@@ -0,0 +1,44 @@
1
+ import { IncomingHttpHeaders } from "node:http";
2
+ import { OutgoingHttpHeaders } from "node:http2";
3
+
4
+ export default function flattenHeaders(
5
+ headers: IncomingHttpHeaders | OutgoingHttpHeaders,
6
+ ): HeadersInit {
7
+ const flatHeaders = new Headers();
8
+ for (const [key, value] of Object.entries(headers)) {
9
+ if (value === undefined || value === null) {
10
+ continue;
11
+ } else if (Array.isArray(value)) {
12
+ for (const v of value) {
13
+ if (v !== undefined && v !== null) {
14
+ value.forEach((v) => flatHeaders.append(key, v));
15
+ }
16
+ }
17
+ } else {
18
+ flatHeaders.set(key, String(value));
19
+ }
20
+ }
21
+ return flatHeaders;
22
+
23
+ /*
24
+ const flatHeaders: [string, string][] = [];
25
+
26
+ for (const [key, value] of Object.entries(headers)) {
27
+ if (value === undefined || value === null) {
28
+ continue;
29
+ }
30
+
31
+ if (Array.isArray(value)) {
32
+ for (const v of value) {
33
+ if (v != null) {
34
+ flatHeaders.push([key, String(v)]);
35
+ }
36
+ }
37
+ } else {
38
+ flatHeaders.push([key, String(value)]);
39
+ }
40
+ }
41
+
42
+ return flatHeaders;
43
+ */
44
+ }
@@ -0,0 +1,18 @@
1
+ import Server from "../../server/Server";
2
+ import type Adapter from "../../types/Adapter";
3
+ import createNodeServer from "./createNodeServer";
4
+
5
+ export default {
6
+ serve: (server: Server) => {
7
+ process.env.PROTOCOL ??= "http:";
8
+ process.env.HOST ??= "localhost";
9
+ process.env.PORT ??= "7059";
10
+
11
+ // Create the Node server and start listening
12
+ console.log(`\nConnecting to ${process.env.HOST}:${process.env.PORT}`);
13
+ const devServer = createNodeServer(server.fetch);
14
+ devServer.listen(parseInt(process.env.PORT), process.env.HOST, () => {
15
+ console.log(`Listening on ${process.env.HOST}:${process.env.PORT}\n`);
16
+ });
17
+ },
18
+ } satisfies Adapter;
@@ -0,0 +1,89 @@
1
+ import {
2
+ type IncomingMessage,
3
+ type OutgoingHttpHeader,
4
+ type OutgoingHttpHeaders,
5
+ ServerResponse,
6
+ } from "node:http";
7
+ import { PassThrough, Readable } from "node:stream";
8
+
9
+ // From https://github.com/vikejs/vike-node/blob/main/packages/vike-node/src/runtime/adapters/createServerResponse.ts
10
+
11
+ /**
12
+ * Creates a custom ServerResponse object that allows for intercepting and
13
+ * streaming the response.
14
+ *
15
+ * @param incomingMessage The incoming HTTP request message.
16
+ * @returns An object containing the custom ServerResponse and a function that
17
+ * takes a callback to invoke when the response is readable, providing an object
18
+ * with the readable stream, headers and status code.
19
+ */
20
+ export default function nodeMessageToNodeResponse(
21
+ incomingMessage: IncomingMessage,
22
+ ): ServerResponseResponse {
23
+ const res = new ServerResponse(incomingMessage);
24
+ const passThrough = new PassThrough();
25
+ let handled = false;
26
+
27
+ const onReadable = (
28
+ cb: (result: { readable: Readable; headers: OutgoingHttpHeaders; status: number }) => void,
29
+ ) => {
30
+ const handleReadable = () => {
31
+ if (handled) return;
32
+ handled = true;
33
+ cb({
34
+ readable: Readable.from(passThrough),
35
+ headers: res.getHeaders(),
36
+ status: res.statusCode,
37
+ });
38
+ };
39
+
40
+ passThrough.once("readable", handleReadable);
41
+ passThrough.once("end", handleReadable);
42
+ };
43
+
44
+ passThrough.once("finish", () => {
45
+ res.emit("finish");
46
+ });
47
+ passThrough.once("close", () => {
48
+ res.destroy();
49
+ res.emit("close");
50
+ });
51
+ passThrough.on("drain", () => {
52
+ res.emit("drain");
53
+ });
54
+
55
+ res.write = passThrough.write.bind(passThrough);
56
+ res.end = passThrough.end.bind(passThrough) as any;
57
+
58
+ res.writeHead = function writeHead(
59
+ statusCode: number,
60
+ statusMessage?: string | OutgoingHttpHeaders | OutgoingHttpHeader[],
61
+ headers?: OutgoingHttpHeaders | OutgoingHttpHeader[],
62
+ ): ServerResponse {
63
+ res.statusCode = statusCode;
64
+ if (typeof statusMessage === "object") {
65
+ headers = statusMessage;
66
+ statusMessage = undefined;
67
+ }
68
+ if (headers) {
69
+ for (const [key, value] of Object.entries(headers)) {
70
+ if (value !== undefined) {
71
+ res.setHeader(key, value);
72
+ }
73
+ }
74
+ }
75
+ return res;
76
+ };
77
+
78
+ return {
79
+ res,
80
+ onReadable,
81
+ };
82
+ }
83
+
84
+ type ServerResponseResponse = {
85
+ res: ServerResponse;
86
+ onReadable: (
87
+ cb: (result: { readable: Readable; headers: OutgoingHttpHeaders; status: number }) => void,
88
+ ) => void;
89
+ };
@@ -0,0 +1,43 @@
1
+ import { type IncomingMessage } from "node:http";
2
+ import { Http2ServerRequest } from "node:http2";
3
+ import { Readable } from "node:stream";
4
+ import flattenHeaders from "./flattenHeaders";
5
+
6
+ // From https://stackoverflow.com/a/78849544
7
+ // and https://gist.github.com/marvinhagemeister/cc236ec97235ce0305ae9d48a24a607d
8
+
9
+ export default async function nodeMessageToRequest(
10
+ req: IncomingMessage | Http2ServerRequest,
11
+ ): Promise<Request> {
12
+ // Build the full url
13
+ let path = req.url;
14
+ if (path === undefined) {
15
+ throw new Error(`Empty request URL`);
16
+ }
17
+ if (!path.startsWith("/")) {
18
+ path = "/" + path;
19
+ }
20
+ const { host } = req.headers;
21
+ if (host == undefined) {
22
+ throw new Error(`Missing "Host" header`);
23
+ }
24
+ const protocol = "encrypted" in req.socket && req.socket.encrypted ? "https" : "http";
25
+ const url = `${protocol}://${host}${path}`;
26
+
27
+ // Flatten the headers
28
+ const headers = flattenHeaders(req.headers);
29
+
30
+ // Convert body to Buffer if applicable
31
+ const body =
32
+ req.method !== "GET" && req.method !== "HEAD"
33
+ ? (Readable.toWeb(req) as ReadableStream) // await readableToBuffer(req)
34
+ : null;
35
+
36
+ return new Request(url, {
37
+ method: req.method,
38
+ headers: headers,
39
+ body: body,
40
+ // @ts-ignore this is needed for using a Readable for some reason
41
+ duplex: "half",
42
+ });
43
+ }
@@ -0,0 +1,16 @@
1
+ import { Readable } from "node:stream";
2
+
3
+ export default function readableToBuffer(req: Readable): Promise<Buffer> {
4
+ return new Promise<Buffer>((resolve, reject) => {
5
+ const chunks: Buffer[] = [];
6
+ req.on("data", (chunk: Buffer) => {
7
+ chunks.push(chunk);
8
+ });
9
+ req.on("end", () => {
10
+ resolve(Buffer.concat(chunks));
11
+ });
12
+ req.on("error", (err) => {
13
+ reject(err);
14
+ });
15
+ });
16
+ }
@@ -0,0 +1,26 @@
1
+ import { type IncomingMessage } from "node:http";
2
+ import { Readable } from "node:stream";
3
+
4
+ // From https://github.com/vikejs/vike-node/blob/main/packages/vike-node/src/runtime/adapters/connectToWeb.ts
5
+
6
+ const DUMMY_BASE_URL = "http://localhost";
7
+
8
+ /**
9
+ * Creates an IncomingMessage object from a web Request.
10
+ *
11
+ * @param {Request} request - The web Request object.
12
+ * @returns {IncomingMessage} An IncomingMessage-like object compatible with Node.js HTTP module.
13
+ */
14
+ export default function requestToNodeMessage(request: Request): IncomingMessage {
15
+ const parsedUrl = new URL(request.url, DUMMY_BASE_URL);
16
+ const pathAndQuery = (parsedUrl.pathname || "") + (parsedUrl.search || "");
17
+ // HACK: Using Readable.fromWeb locks request.body so that the request cannot be used again
18
+ // As we're only using this for Vite middlewares, it's safe to discard the body for now?
19
+ const body = /*request.body ? Readable.fromWeb(request.body as any) :*/ Readable.from([]);
20
+
21
+ return Object.assign(body, {
22
+ url: pathAndQuery,
23
+ method: request.method,
24
+ headers: Object.fromEntries(request.headers),
25
+ }) as IncomingMessage;
26
+ }
@@ -0,0 +1,44 @@
1
+ import { ServerResponse } from "node:http";
2
+ import { Http2ServerResponse } from "node:http2";
3
+ import { isArrayBufferView } from "node:util/types";
4
+ import { INTERNAL_BODY } from "./constants";
5
+
6
+ export default async function responseToNodeResponse(
7
+ res: Response,
8
+ outgoing: ServerResponse | Http2ServerResponse,
9
+ ) {
10
+ outgoing.statusCode = res.status;
11
+ outgoing.statusMessage = res.statusText;
12
+
13
+ res.headers.forEach((value, key) => {
14
+ outgoing.setHeader(key, value);
15
+ });
16
+
17
+ const body = (res as any)[INTERNAL_BODY];
18
+
19
+ if (body === null || body === undefined) {
20
+ outgoing.writeHead(res.status, res.statusText);
21
+ outgoing.end();
22
+ } else if (typeof body === "string" || body instanceof Uint8Array) {
23
+ outgoing.writeHead(res.status, res.statusText);
24
+ outgoing.end(body);
25
+ } else if (body instanceof Blob) {
26
+ outgoing.writeHead(res.status, res.statusText);
27
+ outgoing.end(new Uint8Array(await body.arrayBuffer()));
28
+ } else if (body instanceof ArrayBuffer) {
29
+ outgoing.writeHead(res.status, res.statusText);
30
+ outgoing.end(new Uint8Array(body));
31
+ } else if (isArrayBufferView(body)) {
32
+ outgoing.writeHead(res.status, res.statusText);
33
+ outgoing.end(new Uint8Array(body.buffer));
34
+ } else if (body instanceof FormData) {
35
+ // TODO
36
+ outgoing.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
37
+
38
+ outgoing.writeHead(res.status, res.statusText);
39
+ outgoing.end();
40
+ } else {
41
+ outgoing.writeHead(res.status, res.statusText);
42
+ outgoing.end();
43
+ }
44
+ }