-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
/
node.ts
137 lines (122 loc) · 4 KB
/
node.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import type { RouteData } from '../../@types/astro.js';
import type { SerializedSSRManifest, SSRManifest } from './types.js';
import * as fs from 'node:fs';
import { IncomingMessage } from 'node:http';
import { TLSSocket } from 'node:tls';
import { deserializeManifest } from './common.js';
import { App } from './index.js';
export { apply as applyPolyfills } from '../polyfill.js';
const clientAddressSymbol = Symbol.for('astro.clientAddress');
type CreateNodeRequestOptions = {
emptyBody?: boolean;
};
type BodyProps = Partial<RequestInit>;
function createRequestFromNodeRequest(
req: NodeIncomingMessage,
options?: CreateNodeRequestOptions
): Request {
const protocol =
req.socket instanceof TLSSocket || req.headers['x-forwarded-proto'] === 'https'
? 'https'
: 'http';
const hostname = req.headers.host || req.headers[':authority'];
const url = `${protocol}://${hostname}${req.url}`;
const headers = makeRequestHeaders(req);
const method = req.method || 'GET';
let bodyProps: BodyProps = {};
const bodyAllowed = method !== 'HEAD' && method !== 'GET' && !options?.emptyBody;
if (bodyAllowed) {
bodyProps = makeRequestBody(req);
}
const request = new Request(url, {
method,
headers,
...bodyProps,
});
if (req.socket?.remoteAddress) {
Reflect.set(request, clientAddressSymbol, req.socket.remoteAddress);
}
return request;
}
function makeRequestHeaders(req: NodeIncomingMessage): Headers {
const headers = new Headers();
for (const [name, value] of Object.entries(req.headers)) {
if (value === undefined) {
continue;
}
if (Array.isArray(value)) {
for (const item of value) {
headers.append(name, item);
}
} else {
headers.append(name, value);
}
}
return headers;
}
function makeRequestBody(req: NodeIncomingMessage): BodyProps {
if (req.body !== undefined) {
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)) };
}
// This covers all async iterables including Readable and ReadableStream.
if (
typeof req.body === 'object' &&
req.body !== null &&
typeof (req.body as any)[Symbol.asyncIterator] !== 'undefined'
) {
return asyncIterableToBodyProps(req.body as AsyncIterable<any>);
}
}
// Return default body.
return asyncIterableToBodyProps(req);
}
function asyncIterableToBodyProps(iterable: AsyncIterable<any>): BodyProps {
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.
// @ts-expect-error
duplex: 'half',
} satisfies BodyProps;
}
class NodeIncomingMessage extends IncomingMessage {
/**
* Allow the request body to be explicitly overridden. For example, this
* is used by the Express JSON middleware.
*/
body?: unknown;
}
export class NodeApp extends App {
match(req: NodeIncomingMessage | Request) {
if (!(req instanceof Request)) {
req = createRequestFromNodeRequest(req, {
emptyBody: true,
});
}
return super.match(req);
}
render(req: NodeIncomingMessage | Request, routeData?: RouteData, locals?: object) {
if (!(req instanceof Request)) {
req = createRequestFromNodeRequest(req);
}
return super.render(req, routeData, locals);
}
}
export async function loadManifest(rootFolder: URL): Promise<SSRManifest> {
const manifestFile = new URL('./manifest.json', rootFolder);
const rawManifest = await fs.promises.readFile(manifestFile, 'utf-8');
const serializedManifest: SerializedSSRManifest = JSON.parse(rawManifest);
return deserializeManifest(serializedManifest);
}
export async function loadApp(rootFolder: URL): Promise<NodeApp> {
const manifest = await loadManifest(rootFolder);
return new NodeApp(manifest);
}