@torpor/build 0.0.6 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +1 -1
- package/dist/Server-CB99REgs.d.cts +74 -0
- package/dist/Server-CB99REgs.d.ts +74 -0
- package/dist/Site-CgoeDt8k.d.cts +29 -0
- package/dist/Site-D95nwrAg.d.ts +29 -0
- package/dist/adapters/cloudflare/index.cjs +125 -0
- package/dist/adapters/cloudflare/index.cjs.map +1 -0
- package/dist/adapters/cloudflare/index.d.cts +13 -0
- package/dist/adapters/cloudflare/index.d.ts +13 -0
- package/dist/adapters/cloudflare/index.js +80 -0
- package/dist/adapters/cloudflare/index.js.map +1 -0
- package/dist/adapters/node/index.cjs +152 -0
- package/dist/adapters/node/index.cjs.map +1 -0
- package/dist/adapters/node/index.d.cts +8 -0
- package/dist/adapters/node/index.d.ts +8 -0
- package/dist/adapters/node/index.js +8 -0
- package/dist/adapters/node/index.js.map +1 -0
- package/dist/bin/index.cjs +541 -93
- package/dist/bin/index.cjs.map +1 -1
- package/dist/bin/index.js +511 -22
- package/dist/bin/index.js.map +1 -1
- package/dist/chunk-5X4T3PZN.js +23 -0
- package/dist/chunk-5X4T3PZN.js.map +1 -0
- package/dist/chunk-ALHFYD7U.js +113 -0
- package/dist/chunk-ALHFYD7U.js.map +1 -0
- package/dist/chunk-V4XTDQEV.js +19 -0
- package/dist/chunk-V4XTDQEV.js.map +1 -0
- package/dist/chunk-VAGHGWM5.js +110 -0
- package/dist/chunk-VAGHGWM5.js.map +1 -0
- package/dist/index.cjs +200 -97
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -28
- package/dist/index.d.ts +20 -28
- package/dist/index.js +85 -17
- package/dist/index.js.map +1 -1
- package/dist/metafile-cjs.json +1 -1
- package/dist/metafile-esm.json +1 -1
- package/dist/response.js +17 -91
- package/dist/response.js.map +1 -1
- package/package.json +26 -9
- package/src/adapters/cloudflare/_worker.ts +38 -0
- package/src/adapters/cloudflare/index.ts +98 -0
- package/src/adapters/node/constants.ts +1 -0
- package/src/adapters/node/createFetchHandler.ts +29 -0
- package/src/adapters/node/createMiddlewareHandler.ts +79 -0
- package/src/adapters/node/createNodeServer.ts +8 -0
- package/src/adapters/node/flattenHeaders.ts +44 -0
- package/src/adapters/node/index.ts +18 -0
- package/src/adapters/node/nodeMessageToNodeResponse.ts +89 -0
- package/src/adapters/node/nodeMessageToRequest.ts +43 -0
- package/src/adapters/node/readableToBuffer.ts +16 -0
- package/src/adapters/node/requestToNodeMessage.ts +26 -0
- package/src/adapters/node/responseToNodeResponse.ts +44 -0
- package/src/bin/index.ts +43 -36
- package/src/index.ts +6 -14
- package/src/server/CookieHelper.ts +36 -0
- package/src/server/HeaderHelper.ts +25 -0
- package/src/server/Server.ts +119 -0
- package/src/server/ServerEvent.ts +33 -0
- package/src/server/ServerFunction.ts +5 -0
- package/src/server/contentType.ts +85 -0
- package/src/site/Router.ts +136 -0
- package/src/site/Routerx.ts +67 -0
- package/src/site/Site.ts +114 -0
- package/src/site/clientEntry.ts +22 -20
- package/src/site/manifest.ts +49 -0
- package/src/site/pathToRegex.ts +10 -0
- package/src/site/prepareTemplate.ts +20 -0
- package/src/site/runBuild.ts +92 -0
- package/src/site/runDev.ts +84 -0
- package/src/site/runPreview.ts +83 -0
- package/src/site/serverEntry.ts +113 -200
- package/src/types/Adapter.ts +10 -0
- package/src/types/HttpMethod.ts +12 -0
- package/src/types/LayoutHandler.ts +7 -0
- package/src/types/ManifestRoute.ts +8 -0
- package/src/types/MiddlewareFunction.ts +8 -0
- package/src/types/PageEndPoint.ts +4 -3
- package/src/types/{PageEvent.ts → PageLoadEvent.ts} +3 -2
- package/src/types/PageServerAction.ts +5 -2
- package/src/types/PageServerEndPoint.ts +1 -0
- package/src/types/PageServerLoad.ts +5 -2
- package/src/types/RouteHandler.ts +6 -5
- package/src/types/RouteHandlerCollection.ts +1 -0
- package/src/types/RouteLayoutHandler.ts +3 -2
- package/src/types/RouteMatchResult.ts +3 -2
- package/src/types/RouteType.ts +7 -0
- package/src/types/ServerEndPoint.ts +1 -0
- package/src/types/ServerHook.ts +3 -2
- package/src/types/{ServerEvent.ts → ServerLoadEvent.ts} +13 -6
- package/src/types/ServerRequest.ts +5 -2
- package/src/types/manifest.d.ts +7 -0
- package/test/app.test.ts +47 -0
- package/tsconfig.json +1 -1
- package/tsup.config.ts +11 -1
- package/vitest.config.ts +3 -0
- package/dist/chunk-IE5EKBNX.js +0 -93
- package/dist/chunk-IE5EKBNX.js.map +0 -1
- package/src/site/FileSystemRouter.ts +0 -25
- package/src/site/buildRoutePath.ts +0 -25
- package/src/site/defineConfig.ts +0 -75
- package/src/site/lazyRoute.ts +0 -72
- package/src/site/routeHandlers.ts +0 -126
- package/src/types/CookieHelper.ts +0 -8
- package/src/types/UserConfig.ts +0 -4
|
@@ -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
|
+
}
|
package/src/bin/index.ts
CHANGED
|
@@ -1,42 +1,49 @@
|
|
|
1
1
|
#! /usr/bin/env node
|
|
2
|
-
//import { existsSync, promises as fs } from "node:fs";
|
|
3
2
|
import fs from "node:fs";
|
|
4
|
-
import path from "path";
|
|
5
|
-
import
|
|
6
|
-
import
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { transformWithEsbuild } from "vite";
|
|
5
|
+
import Site from "../site/Site";
|
|
6
|
+
import runBuild from "../site/runBuild";
|
|
7
|
+
import runDev from "../site/runDev";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
// TODO: Make sure it's valid, try/catch, etc
|
|
10
|
-
// TODO: There has GOT to be an easier way
|
|
11
|
-
let options: UserConfig | undefined;
|
|
12
|
-
const workingDir = process.cwd();
|
|
13
|
-
const jsConfigFile = path.join(workingDir, "torpor.config.js");
|
|
14
|
-
const tsConfigFile = path.join(workingDir, "torpor.config.ts");
|
|
15
|
-
let source: string | undefined;
|
|
16
|
-
if (fs.existsSync(jsConfigFile)) {
|
|
17
|
-
//const configFile = path.relative(".", tsConfigFile);
|
|
18
|
-
//options = await import(configFile);
|
|
19
|
-
source = fs.readFileSync(jsConfigFile, "utf8");
|
|
20
|
-
source = source
|
|
21
|
-
.replace(
|
|
22
|
-
/import\s+\{\s+defineConfig\s+\}\s+from\s+["']@torpor\/build["']/,
|
|
23
|
-
"const defineConfig = (x) => x",
|
|
24
|
-
)
|
|
25
|
-
.replace("export default", "");
|
|
26
|
-
} else if (fs.existsSync(tsConfigFile)) {
|
|
27
|
-
console.log("TS config file is not supported");
|
|
28
|
-
}
|
|
9
|
+
run();
|
|
29
10
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
11
|
+
async function run() {
|
|
12
|
+
// Look for and load a site.config.js/ts file in the working directory
|
|
13
|
+
const workingDir = process.cwd();
|
|
14
|
+
let configFile = "";
|
|
15
|
+
let deleteConfigFile = false;
|
|
16
|
+
const jsConfigFile = path.join(workingDir, "site.config.js");
|
|
17
|
+
const tsConfigFile = path.join(workingDir, "site.config.ts");
|
|
18
|
+
if (fs.existsSync(jsConfigFile)) {
|
|
19
|
+
// If it's JS, we can just import it straight up
|
|
20
|
+
configFile = jsConfigFile;
|
|
21
|
+
} else if (fs.existsSync(tsConfigFile)) {
|
|
22
|
+
// If it's TS, we need to convert it to JS
|
|
23
|
+
// TODO: Put the generated file somewhere better (dist?)
|
|
24
|
+
configFile = path.join(workingDir, "site.config.temp.js");
|
|
25
|
+
let source = (
|
|
26
|
+
await transformWithEsbuild(fs.readFileSync(tsConfigFile, "utf-8"), tsConfigFile, {
|
|
27
|
+
loader: "ts",
|
|
28
|
+
})
|
|
29
|
+
).code;
|
|
30
|
+
fs.writeFileSync(configFile, source);
|
|
31
|
+
deleteConfigFile = true;
|
|
32
|
+
} else {
|
|
33
|
+
throw new Error("site.config file not found");
|
|
34
|
+
}
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
const site = (await import(configFile)).default as Site;
|
|
37
|
+
if (!site || !site.root || !site.routes) {
|
|
38
|
+
throw new Error("Invalid site in config file");
|
|
39
|
+
}
|
|
40
|
+
if (deleteConfigFile) {
|
|
41
|
+
fs.rmSync(configFile);
|
|
42
|
+
}
|
|
35
43
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
//}
|
|
44
|
+
if (process.argv.includes("--dev")) {
|
|
45
|
+
await runDev(site);
|
|
46
|
+
} else if (process.argv.includes("--build")) {
|
|
47
|
+
await runBuild(site);
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,24 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
import defineConfig from "./site/defineConfig";
|
|
3
|
-
import type CookieHelper from "./types/CookieHelper";
|
|
1
|
+
import Site from "./site/Site";
|
|
4
2
|
import type PageEndPoint from "./types/PageEndPoint";
|
|
5
|
-
import type
|
|
3
|
+
import type PageLoadEvent from "./types/PageLoadEvent";
|
|
6
4
|
import type PageServerAction from "./types/PageServerAction";
|
|
7
5
|
import type PageServerEndPoint from "./types/PageServerEndPoint";
|
|
8
6
|
import type PageServerLoad from "./types/PageServerLoad";
|
|
9
7
|
import type ServerEndPoint from "./types/ServerEndPoint";
|
|
10
|
-
import type ServerEvent from "./types/ServerEvent";
|
|
11
8
|
import type ServerHook from "./types/ServerHook";
|
|
9
|
+
import type ServerLoadEvent from "./types/ServerLoadEvent";
|
|
12
10
|
import type ServerRequest from "./types/ServerRequest";
|
|
13
|
-
import type UserConfig from "./types/UserConfig";
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
// cause problems when they are re-imported when routes are lazily loaded
|
|
17
|
-
|
|
18
|
-
export { defineConfig, buildRoutePath };
|
|
12
|
+
export { Site };
|
|
19
13
|
|
|
20
14
|
export type {
|
|
21
|
-
UserConfig,
|
|
22
15
|
PageEndPoint,
|
|
23
16
|
PageServerEndPoint,
|
|
24
17
|
PageServerLoad,
|
|
@@ -26,7 +19,6 @@ export type {
|
|
|
26
19
|
ServerEndPoint,
|
|
27
20
|
ServerRequest,
|
|
28
21
|
ServerHook,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
CookieHelper,
|
|
22
|
+
PageLoadEvent,
|
|
23
|
+
ServerLoadEvent,
|
|
32
24
|
};
|
|
@@ -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 updated value
|
|
13
|
+
if (this.cookies.has(name)) {
|
|
14
|
+
return this.cookies.get(name);
|
|
15
|
+
}
|
|
16
|
+
// Otherwise, return the cookie 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) {
|
|
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) {
|
|
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
|
+
}
|