@torpor/build 0.4.10 → 0.4.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/{_page-DBSsnHFG.mjs → _page-BQs4WvCO.mjs} +2 -2
  2. package/dist/_page-BQs4WvCO.mjs.map +1 -0
  3. package/dist/nav.mjs +2 -2
  4. package/dist/nav.mjs.map +1 -1
  5. package/dist/state.mjs +1 -1
  6. package/dist/test.mjs +1 -1
  7. package/package.json +4 -4
  8. package/src/bin/index.ts +11 -0
  9. package/src/index.ts +30 -0
  10. package/src/nav/formSubmit.ts +56 -0
  11. package/src/nav/load.ts +9 -0
  12. package/src/nav/loadData.ts +134 -0
  13. package/src/nav/navigate.ts +164 -0
  14. package/src/nav/reload.ts +9 -0
  15. package/src/nav.ts +4 -0
  16. package/src/response/badRequest.ts +19 -0
  17. package/src/response/created.ts +20 -0
  18. package/src/response/forbidden.ts +21 -0
  19. package/src/response/found.ts +30 -0
  20. package/src/response/methodNotAllowed.ts +18 -0
  21. package/src/response/movedPermanently.ts +25 -0
  22. package/src/response/notFound.ts +24 -0
  23. package/src/response/notModified.ts +34 -0
  24. package/src/response/ok.ts +29 -0
  25. package/src/response/permanentRedirect.ts +30 -0
  26. package/src/response/response.ts +22 -0
  27. package/src/response/seeOther.ts +19 -0
  28. package/src/response/serverError.ts +24 -0
  29. package/src/response/temporaryRedirect.ts +31 -0
  30. package/src/response/transfer.ts +14 -0
  31. package/src/response/unauthorized.ts +21 -0
  32. package/src/response/unprocessable.ts +18 -0
  33. package/src/response.ts +37 -0
  34. package/src/run/prepareTemplate.ts +39 -0
  35. package/src/run/run.ts +62 -0
  36. package/src/run/runBuild.ts +77 -0
  37. package/src/run/runDev.ts +89 -0
  38. package/src/run/runPreview.ts +93 -0
  39. package/src/run.ts +6 -0
  40. package/src/server/CookieHelper.ts +36 -0
  41. package/src/server/HeaderHelper.ts +25 -0
  42. package/src/server/Routerx.ts +72 -0
  43. package/src/server/Server.ts +144 -0
  44. package/src/server/ServerEvent.ts +39 -0
  45. package/src/server/connect/bufferToArrayBuffer.ts +8 -0
  46. package/src/server/connect/connectMiddleware.ts +81 -0
  47. package/src/server/connect/flattenHeaders.ts +44 -0
  48. package/src/server/connect/nodeMessageToNodeResponse.ts +89 -0
  49. package/src/server/connect/readableToBuffer.ts +16 -0
  50. package/src/server/connect/requestToNodeMessage.ts +26 -0
  51. package/src/server/contentType.ts +85 -0
  52. package/src/server/types/HttpMethod.ts +12 -0
  53. package/src/server/types/MiddlewareFunction.ts +8 -0
  54. package/src/server/types/ServerFunction.ts +7 -0
  55. package/src/server.ts +7 -0
  56. package/src/site/Router.ts +164 -0
  57. package/src/site/Site.ts +214 -0
  58. package/src/site/clientEntry.ts +108 -0
  59. package/src/site/clientEntryDev.ts +3 -0
  60. package/src/site/defaultAdapter.ts +11 -0
  61. package/src/site/manifest.ts +54 -0
  62. package/src/site/serverEntry.ts +417 -0
  63. package/src/state/$page.ts +24 -0
  64. package/src/state/client.ts +15 -0
  65. package/src/state.ts +3 -0
  66. package/src/test/runTest.ts +350 -0
  67. package/src/test.ts +3 -0
  68. package/src/types/Adapter.ts +8 -0
  69. package/src/types/ClientState.ts +8 -0
  70. package/src/types/InternalState.ts +7 -0
  71. package/src/types/LayoutHandler.ts +5 -0
  72. package/src/types/LayoutPath.ts +10 -0
  73. package/src/types/ManifestRoute.ts +6 -0
  74. package/src/types/PageEndPoint.ts +39 -0
  75. package/src/types/PageLoadEvent.ts +14 -0
  76. package/src/types/PageServerAction.ts +7 -0
  77. package/src/types/PageServerEndPoint.ts +16 -0
  78. package/src/types/PageServerLoad.ts +7 -0
  79. package/src/types/PageState.ts +8 -0
  80. package/src/types/Route.ts +11 -0
  81. package/src/types/RouteHandler.ts +16 -0
  82. package/src/types/RouteLayoutHandler.ts +5 -0
  83. package/src/types/RouteMatchResult.ts +7 -0
  84. package/src/types/RouteType.ts +19 -0
  85. package/src/types/ServerEndPoint.ts +37 -0
  86. package/src/types/ServerHook.ts +11 -0
  87. package/src/types/ServerLoadEvent.ts +37 -0
  88. package/src/types/ServerRequest.ts +7 -0
  89. package/src/utils/pathToRegex.ts +15 -0
  90. package/src/vite-env.d.ts +2 -0
  91. package/dist/_page-DBSsnHFG.mjs.map +0 -1
@@ -0,0 +1,93 @@
1
+ import { configDotenv } from "dotenv";
2
+ import { existsSync, promises as fs } from "node:fs";
3
+ import path from "node:path";
4
+ import { serverError } from "../response.ts";
5
+ import Server from "../server/Server.ts";
6
+ import contentType from "../server/contentType.ts";
7
+ import Site from "../site/Site";
8
+ import prepareTemplate from "./prepareTemplate.ts";
9
+ import runBuild from "./runBuild";
10
+
11
+ export default async function runPreview(site: Site): Promise<void> {
12
+ // Build the site
13
+ await runBuild(site);
14
+
15
+ const distFolder = path.resolve(site.root, "dist");
16
+ const clientFolder = path.join(distFolder, "client");
17
+ const serverFolder = path.join(distFolder, "server");
18
+
19
+ // Find the client script file in /assets
20
+ let clientScript = (await fs.readdir(path.join(clientFolder, "assets"))).find((f) =>
21
+ f.startsWith("clientEntry-"),
22
+ );
23
+ if (!clientScript) {
24
+ throw new Error("clientEntry.js not found");
25
+ }
26
+ clientScript = `/assets/${clientScript}`;
27
+
28
+ // Read site.html
29
+ // It's called site.html because @torpor/build builds the site (html, routes
30
+ // etc) while the user builds the app (components etc)
31
+ let siteHtml = path.join(clientFolder, "site.html");
32
+ let template = await fs.readFile(siteHtml, "utf-8");
33
+
34
+ // Prepare site.html so that we can just splice components into it
35
+ template = prepareTemplate(template, clientScript);
36
+
37
+ // Configure the server
38
+ const server = new Server();
39
+
40
+ // Serve dist/client/assets statically
41
+ server.add("/assets/*", async (ev) => {
42
+ try {
43
+ const url = new URL(ev.request.url);
44
+ const file = path.join(site.root, "dist", "client", url.pathname);
45
+ if (existsSync(file)) {
46
+ // TODO: Stream the data?
47
+ const body = bufferToArrayBuffer(await fs.readFile(file));
48
+ return new Response(body, {
49
+ status: 200,
50
+ headers: {
51
+ "Content-Type": contentType(path.extname(file)),
52
+ },
53
+ });
54
+ }
55
+ } catch (e: any) {
56
+ return serverError(e);
57
+ }
58
+ });
59
+
60
+ // Every request (GET, POST, etc) goes through loadEndPoint
61
+ const serverScript = path.resolve(serverFolder, "serverEntry.js");
62
+ server.add("*", async (ev) => {
63
+ try {
64
+ // Load the server entry
65
+ const { load } = await import(serverScript);
66
+
67
+ // Render the app HTML (or fetch server data etc) via serverEntry's
68
+ // exported `load` function
69
+ return await load(ev, template);
70
+ } catch (e: any) {
71
+ return serverError(e);
72
+ }
73
+ });
74
+
75
+ // Load environment variables from a `.env` file, with defaults if not set
76
+ configDotenv();
77
+
78
+ process.env.PROTOCOL ??= "http:";
79
+ process.env.HOST ??= "localhost";
80
+ process.env.PORT ??= "7059";
81
+
82
+ // Serve the site
83
+ site.adapter.serve(server, site);
84
+ }
85
+
86
+ // From https://stackoverflow.com/a/79345620
87
+ // No idea why it is needed
88
+ function bufferToArrayBuffer(buffer: Buffer): ArrayBuffer {
89
+ return buffer.buffer.slice(
90
+ buffer.byteOffset,
91
+ buffer.byteOffset + buffer.byteLength,
92
+ ) as ArrayBuffer;
93
+ }
package/src/run.ts ADDED
@@ -0,0 +1,6 @@
1
+ import run from "./run/run";
2
+ import runBuild from "./run/runBuild";
3
+ import runDev from "./run/runDev";
4
+ import runPreview from "./run/runPreview";
5
+
6
+ export { run, runDev, runBuild, runPreview };
@@ -0,0 +1,36 @@
1
+ import { SerializeOptions, parse, serialize } from "cookie";
2
+
3
+ export default class CookieHelper {
4
+ request: Request;
5
+ cookies: Map<string, string> = new Map();
6
+
7
+ constructor(request: Request) {
8
+ this.request = request;
9
+ }
10
+
11
+ get(name: string): string | undefined {
12
+ // If it's been set, return the value from the map
13
+ if (this.cookies.has(name)) {
14
+ return parse(this.cookies.get(name)!)[name];
15
+ }
16
+ // Otherwise, return the value from the `cookie` header
17
+ const header = this.request.headers.get("cookie");
18
+ return parse(header ?? "")[name];
19
+ }
20
+
21
+ set(name: string, value: string, options?: SerializeOptions): void {
22
+ // Set a cookie header with some default values for security
23
+ options ??= {};
24
+ options.httpOnly ??= true;
25
+ options.secure ??= true;
26
+ options.sameSite ??= "lax";
27
+ this.cookies.set(name, serialize(name, value, options));
28
+ }
29
+
30
+ delete(name: string, options?: SerializeOptions): void {
31
+ // Set a cookie header with an immediate expiry date
32
+ options ??= {};
33
+ options.expires = new Date(0);
34
+ this.cookies.set(name, serialize(name, "", options));
35
+ }
36
+ }
@@ -0,0 +1,25 @@
1
+ export default class HeaderHelper {
2
+ request: Request;
3
+ headers: Map<string, string> = new Map();
4
+
5
+ constructor(request: Request) {
6
+ this.request = request;
7
+ }
8
+
9
+ get(name: string): string | undefined {
10
+ // If it's been set, return the updated value
11
+ if (this.headers.has(name)) {
12
+ return this.headers.get(name);
13
+ }
14
+ // Otherwise, return the value from the header
15
+ return this.request.headers.get(name) ?? undefined;
16
+ }
17
+
18
+ set(name: string, value: string): void {
19
+ this.headers.set(name, value);
20
+ }
21
+
22
+ delete(name: string): void {
23
+ this.headers.delete(name);
24
+ }
25
+ }
@@ -0,0 +1,72 @@
1
+ import pathToRegex from "../utils/pathToRegex";
2
+ import type HttpMethod from "./types/HttpMethod";
3
+ import type MiddlewareFunction from "./types/MiddlewareFunction";
4
+ import type ServerFunction from "./types/ServerFunction";
5
+
6
+ // This is not actually used anywhere, for now at least
7
+
8
+ export default class Router {
9
+ methods: Map<HttpMethod, RouteHandler[]> = new Map<HttpMethod, RouteHandler[]>();
10
+ middleware: MiddlewareHandler[] = [];
11
+
12
+ constructor() {
13
+ // Create all method handlers up front so we don't have to worry about
14
+ // checking if they exist. Using a non-standard method will just error
15
+ this.methods.set("GET", []);
16
+ this.methods.set("HEAD", []);
17
+ this.methods.set("PATCH", []);
18
+ this.methods.set("POST", []);
19
+ this.methods.set("PUT", []);
20
+ this.methods.set("DELETE", []);
21
+ this.methods.set("OPTIONS", []);
22
+ this.methods.set("CONNECT", []);
23
+ this.methods.set("TRACE", []);
24
+ }
25
+
26
+ add(method: HttpMethod, path: string, fn: ServerFunction): Router {
27
+ this.methods.get(method)!.push(new RouteHandler(path, fn));
28
+ return this;
29
+ }
30
+
31
+ match(method: HttpMethod, path: string): Match | undefined {
32
+ for (let handler of this.methods.get(method)!) {
33
+ let match = path.match(handler.regex);
34
+ if (match) {
35
+ console.log("found", handler);
36
+ return {
37
+ fn: handler.fn,
38
+ params: match.groups,
39
+ };
40
+ }
41
+ }
42
+ }
43
+ }
44
+
45
+ class RouteHandler {
46
+ path: string;
47
+ regex: RegExp;
48
+ fn: ServerFunction;
49
+
50
+ constructor(path: string, fn: ServerFunction) {
51
+ this.path = path;
52
+ this.regex = pathToRegex(path);
53
+ this.fn = fn;
54
+ }
55
+ }
56
+
57
+ class MiddlewareHandler {
58
+ path: string;
59
+ regex: RegExp;
60
+ fn: MiddlewareFunction;
61
+
62
+ constructor(path: string, fn: MiddlewareFunction) {
63
+ this.path = path;
64
+ this.regex = pathToRegex(path);
65
+ this.fn = fn;
66
+ }
67
+ }
68
+
69
+ interface Match {
70
+ fn: ServerFunction;
71
+ params?: Record<string, string>;
72
+ }
@@ -0,0 +1,144 @@
1
+ import pathToRegex from "../utils/pathToRegex";
2
+ import ServerEvent from "./ServerEvent";
3
+ import type MiddlewareFunction from "./types/MiddlewareFunction";
4
+ import type ServerFunction from "./types/ServerFunction";
5
+
6
+ const noop = () => {};
7
+
8
+ /**
9
+ * A Fetch API Request/Response server.
10
+ */
11
+ export default class Server {
12
+ // TODO: We don't use any of the routes, just add("*") in runDev/runBuild...
13
+ routes: RouteHandler[] = [];
14
+ // TODO: We only use the Vite middleware here
15
+ middleware: MiddlewareFunction[] = [];
16
+
17
+ constructor() {
18
+ this.routes = [];
19
+ this.middleware = [];
20
+ }
21
+
22
+ /**
23
+ * The standard Fetch API method that should work in any runtime (including
24
+ * Node with polyfills).
25
+ * @returns
26
+ */
27
+ fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
28
+ const request = new Request(input, init);
29
+ const url = new URL(request.url);
30
+ const match = this.match(url.pathname);
31
+ let ev = new ServerEvent(request, match?.params);
32
+
33
+ if (this.middleware.length) {
34
+ // TODO: Get middleware that applies to this route only?
35
+ await this.middleware[0](ev, buildNext(this.middleware, 0));
36
+ function buildNext(middleware: MiddlewareFunction[], i: number): () => void | Promise<void> {
37
+ if (ev.response) {
38
+ return noop;
39
+ }
40
+
41
+ if (i < middleware.length - 1) {
42
+ let next = middleware[i + 1];
43
+ return async () => next(ev, buildNext(middleware, i + 1));
44
+ } else {
45
+ return match
46
+ ? async () => {
47
+ ev.response = await match.fn(ev);
48
+ }
49
+ : noop;
50
+ }
51
+ }
52
+ } else if (match) {
53
+ ev.response = await match.fn(ev);
54
+ } else {
55
+ return new Response(null, { status: 404 });
56
+ }
57
+
58
+ // TODO: Should I be returning 200 if there's no response? Because it's not an error??
59
+ ev.response ??= new Response(null, { status: 200 });
60
+ ev.addHeaders();
61
+
62
+ return ev.response;
63
+ };
64
+
65
+ /**
66
+ * Adds a server function at a route path.
67
+ * @param path The route path pattern.
68
+ * @param fn The function to call when the pattern is matched.
69
+ * @returns
70
+ */
71
+ add(path: string, fn: ServerFunction): Server {
72
+ this.routes.push(new RouteHandler(path, fn));
73
+ this.#sortRoutes();
74
+ return this;
75
+ }
76
+
77
+ match(path: string): Match | undefined {
78
+ for (let handler of this.routes) {
79
+ let match = path.match(handler.regex);
80
+ if (match) {
81
+ return {
82
+ fn: handler.fn,
83
+ params: match.groups,
84
+ };
85
+ }
86
+ }
87
+ console.log(`Server path not found: ${path}`);
88
+ }
89
+
90
+ // TODO:
91
+ //use(path: string, ...fn: MiddlewareFunction[])
92
+ use(...fn: MiddlewareFunction[]): Server {
93
+ this.middleware = this.middleware.concat(fn);
94
+ return this;
95
+ }
96
+
97
+ #sortRoutes() {
98
+ this.routes = this.routes.sort((a, b) => {
99
+ // Sort [param]s after paths
100
+ // There might be a quicker/easier way to do this
101
+ for (let i = 0; i < Math.min(a.path.length, b.path.length); i++) {
102
+ if (a.path[i] === "[" && b.path[i] !== "[") {
103
+ return 1;
104
+ } else if (b.path[i] === "[" && a.path[i] !== "[") {
105
+ return -1;
106
+ } else if (a.path[i] === b.path[i]) {
107
+ // Keep going...
108
+ } else {
109
+ return a.path[i].localeCompare(b.path[i]);
110
+ }
111
+ }
112
+ return a.path.length - b.path.length;
113
+ });
114
+ }
115
+ }
116
+
117
+ class RouteHandler {
118
+ path: string;
119
+ regex: RegExp;
120
+ fn: ServerFunction;
121
+
122
+ constructor(path: string, fn: ServerFunction) {
123
+ this.path = path;
124
+ this.regex = pathToRegex(path);
125
+ this.fn = fn;
126
+ }
127
+ }
128
+
129
+ //class MiddlewareHandler {
130
+ // path: string;
131
+ // regex: RegExp;
132
+ // fn: MiddlewareFunction;
133
+ //
134
+ // constructor(path: string, fn: MiddlewareFunction) {
135
+ // this.path = path;
136
+ // this.regex = pathToRegex(path);
137
+ // this.fn = fn;
138
+ // }
139
+ //}
140
+
141
+ interface Match {
142
+ fn: ServerFunction;
143
+ params?: Record<string, string>;
144
+ }
@@ -0,0 +1,39 @@
1
+ import CookieHelper from "./CookieHelper";
2
+ import HeaderHelper from "./HeaderHelper";
3
+
4
+ export default class ServerEvent {
5
+ request: Request;
6
+ response: Response | undefined | void;
7
+ // TODO: Should we cast types??
8
+ params?: Record<string, string>;
9
+
10
+ adapter: any;
11
+
12
+ cookies: CookieHelper;
13
+ headers: HeaderHelper;
14
+
15
+ constructor(request: Request, params?: Record<string, string>) {
16
+ this.request = request;
17
+ this.params = params;
18
+ this.cookies = new CookieHelper(request);
19
+ this.headers = new HeaderHelper(request);
20
+
21
+ // Adapters may have added the `adapter` property to globalThis
22
+ // @ts-ignore
23
+ this.adapter = globalThis.adapter;
24
+ }
25
+
26
+ addHeaders(): void {
27
+ if (!this.response) {
28
+ throw new Error("Response not created yet");
29
+ }
30
+
31
+ for (let cookie of this.cookies.cookies.values()) {
32
+ this.response.headers.append("set-cookie", cookie);
33
+ }
34
+
35
+ for (let header of this.headers.headers.entries()) {
36
+ this.response.headers.append(header[0], header[1]);
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,8 @@
1
+ // From https://stackoverflow.com/a/79345620
2
+ // No idea why it is needed
3
+ export default function bufferToArrayBuffer(buffer: Buffer): ArrayBuffer {
4
+ return buffer.buffer.slice(
5
+ buffer.byteOffset,
6
+ buffer.byteOffset + buffer.byteLength,
7
+ ) as ArrayBuffer;
8
+ }
@@ -0,0 +1,81 @@
1
+ import { IncomingMessage, ServerResponse } from "node:http";
2
+ import type MiddlewareFunction from "../types/MiddlewareFunction";
3
+ import bufferToArrayBuffer from "./bufferToArrayBuffer";
4
+ import flattenHeaders from "./flattenHeaders";
5
+ import nodeMessageToNodeResponse from "./nodeMessageToNodeResponse";
6
+ import readableToBuffer from "./readableToBuffer";
7
+ import requestToNodeMessage from "./requestToNodeMessage";
8
+
9
+ // From https://github.com/vikejs/vike-node/blob/main/packages/vike-node/src/runtime/adapters/connectToWeb.ts
10
+
11
+ /**
12
+ * Converts a Connect-style middleware to a web-compatible request handler.
13
+ *
14
+ * @param handler - The Connect-style middleware function to be converted.
15
+ * @returns A function that handles web requests and returns a Response or undefined.
16
+ */
17
+ export default function connectMiddleware(
18
+ handler: ConnectMiddleware | ConnectMiddlewareBoolean,
19
+ ): MiddlewareFunction {
20
+ return async (ev, next) => {
21
+ const req = requestToNodeMessage(ev.request);
22
+ const { res, onReadable } = nodeMessageToNodeResponse(req);
23
+
24
+ return new Promise<void>(async (resolve, reject) => {
25
+ onReadable(async ({ readable, headers, status }) => {
26
+ const responseBody = statusCodesWithoutBody.includes(status)
27
+ ? null
28
+ : //(Readable.toWeb(readable) as ReadableStream);
29
+ // HACK: Chunked bodies are not being
30
+ // read correctly somewhere...
31
+ bufferToArrayBuffer(await readableToBuffer(readable));
32
+ ev.response = new Response(responseBody, {
33
+ status,
34
+ headers: flattenHeaders(headers),
35
+ });
36
+ resolve();
37
+ });
38
+
39
+ const cnext = async (error?: unknown) => {
40
+ if (error) {
41
+ // eslint-disable-next-line no-base-to-string
42
+ reject(error instanceof Error ? error : new Error(String(error)));
43
+ } else {
44
+ // TODO: Can we nest the next functions less? This is a bit weird
45
+ await next();
46
+ resolve();
47
+ }
48
+ };
49
+
50
+ try {
51
+ const handled = await handler(req, res, cnext);
52
+ if (handled === false) {
53
+ res.destroy();
54
+ resolve();
55
+ }
56
+ } catch (e) {
57
+ await cnext(e);
58
+ }
59
+ });
60
+ };
61
+ }
62
+
63
+ const statusCodesWithoutBody = [
64
+ 100, // Continue
65
+ 101, // Switching Protocols
66
+ 102, // Processing (WebDAV)
67
+ 103, // Early Hints
68
+ 204, // No Content
69
+ 205, // Reset Content
70
+ 304, // Not Modified
71
+ ];
72
+
73
+ type NextFunction = (err?: unknown) => void;
74
+ type ConnectMiddleware<
75
+ PlatformRequest extends IncomingMessage = IncomingMessage,
76
+ PlatformResponse extends ServerResponse = ServerResponse,
77
+ > = (req: PlatformRequest, res: PlatformResponse, next: NextFunction) => void | Promise<void>;
78
+ type ConnectMiddlewareBoolean<
79
+ PlatformRequest extends IncomingMessage = IncomingMessage,
80
+ PlatformResponse extends ServerResponse = ServerResponse,
81
+ > = (req: PlatformRequest, res: PlatformResponse, next: NextFunction) => boolean | Promise<boolean>;
@@ -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,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
+ interface ServerResponseResponse {
85
+ res: ServerResponse;
86
+ onReadable: (
87
+ cb: (result: { readable: Readable; headers: OutgoingHttpHeaders; status: number }) => void,
88
+ ) => void;
89
+ }
@@ -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
+ }