152 lines
4.9 KiB
Plaintext
152 lines
4.9 KiB
Plaintext
|
import fs from "node:fs";
|
||
|
import { App } from "./index.js";
|
||
|
import { deserializeManifest } from "./common.js";
|
||
|
import { createOutgoingHttpHeaders } from "./createOutgoingHttpHeaders.js";
|
||
|
import { apply } from "../polyfill.js";
|
||
|
const clientAddressSymbol = Symbol.for("astro.clientAddress");
|
||
|
class NodeApp extends App {
|
||
|
match(req) {
|
||
|
if (!(req instanceof Request)) {
|
||
|
req = NodeApp.createRequest(req, {
|
||
|
skipBody: true
|
||
|
});
|
||
|
}
|
||
|
return super.match(req);
|
||
|
}
|
||
|
render(req, routeDataOrOptions, maybeLocals) {
|
||
|
if (!(req instanceof Request)) {
|
||
|
req = NodeApp.createRequest(req);
|
||
|
}
|
||
|
return super.render(req, routeDataOrOptions, maybeLocals);
|
||
|
}
|
||
|
/**
|
||
|
* Converts a NodeJS IncomingMessage into a web standard Request.
|
||
|
* ```js
|
||
|
* import { NodeApp } from 'astro/app/node';
|
||
|
* import { createServer } from 'node:http';
|
||
|
*
|
||
|
* const server = createServer(async (req, res) => {
|
||
|
* const request = NodeApp.createRequest(req);
|
||
|
* const response = await app.render(request);
|
||
|
* await NodeApp.writeResponse(response, res);
|
||
|
* })
|
||
|
* ```
|
||
|
*/
|
||
|
static createRequest(req, { skipBody = false } = {}) {
|
||
|
const protocol = req.headers["x-forwarded-proto"] ?? ("encrypted" in req.socket && req.socket.encrypted ? "https" : "http");
|
||
|
const hostname = req.headers.host || req.headers[":authority"];
|
||
|
const url = `${protocol}://${hostname}${req.url}`;
|
||
|
const options = {
|
||
|
method: req.method || "GET",
|
||
|
headers: makeRequestHeaders(req)
|
||
|
};
|
||
|
const bodyAllowed = options.method !== "HEAD" && options.method !== "GET" && skipBody === false;
|
||
|
if (bodyAllowed) {
|
||
|
Object.assign(options, makeRequestBody(req));
|
||
|
}
|
||
|
const request = new Request(url, options);
|
||
|
if (req.socket?.remoteAddress) {
|
||
|
Reflect.set(request, clientAddressSymbol, req.socket.remoteAddress);
|
||
|
}
|
||
|
return request;
|
||
|
}
|
||
|
/**
|
||
|
* Streams a web-standard Response into a NodeJS Server Response.
|
||
|
* ```js
|
||
|
* import { NodeApp } from 'astro/app/node';
|
||
|
* import { createServer } from 'node:http';
|
||
|
*
|
||
|
* const server = createServer(async (req, res) => {
|
||
|
* const request = NodeApp.createRequest(req);
|
||
|
* const response = await app.render(request);
|
||
|
* await NodeApp.writeResponse(response, res);
|
||
|
* })
|
||
|
* ```
|
||
|
* @param source WhatWG Response
|
||
|
* @param destination NodeJS ServerResponse
|
||
|
*/
|
||
|
static async writeResponse(source, destination) {
|
||
|
const { status, headers, body } = source;
|
||
|
destination.writeHead(status, createOutgoingHttpHeaders(headers));
|
||
|
if (!body)
|
||
|
return destination.end();
|
||
|
try {
|
||
|
const reader = body.getReader();
|
||
|
destination.on("close", () => {
|
||
|
reader.cancel().catch((err) => {
|
||
|
console.error(
|
||
|
`There was an uncaught error in the middle of the stream while rendering ${destination.req.url}.`,
|
||
|
err
|
||
|
);
|
||
|
});
|
||
|
});
|
||
|
let result = await reader.read();
|
||
|
while (!result.done) {
|
||
|
destination.write(result.value);
|
||
|
result = await reader.read();
|
||
|
}
|
||
|
destination.end();
|
||
|
} catch {
|
||
|
destination.end("Internal server error");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
function makeRequestHeaders(req) {
|
||
|
const headers = new Headers();
|
||
|
for (const [name, value] of Object.entries(req.headers)) {
|
||
|
if (value === void 0) {
|
||
|
continue;
|
||
|
}
|
||
|
if (Array.isArray(value)) {
|
||
|
for (const item of value) {
|
||
|
headers.append(name, item);
|
||
|
}
|
||
|
} else {
|
||
|
headers.append(name, value);
|
||
|
}
|
||
|
}
|
||
|
return headers;
|
||
|
}
|
||
|
function makeRequestBody(req) {
|
||
|
if (req.body !== void 0) {
|
||
|
if (typeof req.body === "string" && req.body.length > 0) {
|
||
|
return { body: Buffer.from(req.body) };
|
||
|
}
|
||
|
if (typeof req.body === "object" && req.body !== null && Object.keys(req.body).length > 0) {
|
||
|
return { body: Buffer.from(JSON.stringify(req.body)) };
|
||
|
}
|
||
|
if (typeof req.body === "object" && req.body !== null && typeof req.body[Symbol.asyncIterator] !== "undefined") {
|
||
|
return asyncIterableToBodyProps(req.body);
|
||
|
}
|
||
|
}
|
||
|
return asyncIterableToBodyProps(req);
|
||
|
}
|
||
|
function asyncIterableToBodyProps(iterable) {
|
||
|
return {
|
||
|
// Node uses undici for the Request implementation. Undici accepts
|
||
|
// a non-standard async iterable for the body.
|
||
|
// @ts-expect-error
|
||
|
body: iterable,
|
||
|
// The duplex property is required when using a ReadableStream or async
|
||
|
// iterable for the body. The type definitions do not include the duplex
|
||
|
// property because they are not up-to-date.
|
||
|
duplex: "half"
|
||
|
};
|
||
|
}
|
||
|
async function loadManifest(rootFolder) {
|
||
|
const manifestFile = new URL("./manifest.json", rootFolder);
|
||
|
const rawManifest = await fs.promises.readFile(manifestFile, "utf-8");
|
||
|
const serializedManifest = JSON.parse(rawManifest);
|
||
|
return deserializeManifest(serializedManifest);
|
||
|
}
|
||
|
async function loadApp(rootFolder) {
|
||
|
const manifest = await loadManifest(rootFolder);
|
||
|
return new NodeApp(manifest);
|
||
|
}
|
||
|
export {
|
||
|
NodeApp,
|
||
|
apply as applyPolyfills,
|
||
|
loadApp,
|
||
|
loadManifest
|
||
|
};
|