1
+ import fs from 'node:fs' ;
2
+ import { App } from './index.js' ;
3
+ import { deserializeManifest } from './common.js' ;
4
+ import { createOutgoingHttpHeaders } from './createOutgoingHttpHeaders.js' ;
5
+ import type { IncomingMessage , ServerResponse } from 'node:http' ;
1
6
import type { RouteData } from '../../@types/astro.js' ;
2
7
import type { RenderOptions } from './index.js' ;
3
8
import type { SerializedSSRManifest , SSRManifest } from './types.js' ;
4
9
5
- import * as fs from 'node:fs' ;
6
- import { IncomingMessage } from 'node:http' ;
7
- import { TLSSocket } from 'node:tls' ;
8
- import { deserializeManifest } from './common.js' ;
9
- import { App } from './index.js' ;
10
10
export { apply as applyPolyfills } from '../polyfill.js' ;
11
11
12
12
const clientAddressSymbol = Symbol . for ( 'astro.clientAddress' ) ;
13
13
14
- type CreateNodeRequestOptions = {
15
- emptyBody ?: boolean ;
16
- } ;
17
-
18
- type BodyProps = Partial < RequestInit > ;
14
+ /**
15
+ * Allow the request body to be explicitly overridden. For example, this
16
+ * is used by the Express JSON middleware.
17
+ */
18
+ interface NodeRequest extends IncomingMessage {
19
+ body ?: unknown ;
20
+ }
19
21
20
- function createRequestFromNodeRequest (
21
- req : NodeIncomingMessage ,
22
- options ?: CreateNodeRequestOptions
23
- ) : Request {
24
- const protocol =
25
- req . socket instanceof TLSSocket || req . headers [ 'x-forwarded-proto' ] === 'https'
26
- ? 'https'
27
- : 'http' ;
28
- const hostname = req . headers . host || req . headers [ ':authority' ] ;
29
- const url = `${ protocol } ://${ hostname } ${ req . url } ` ;
30
- const headers = makeRequestHeaders ( req ) ;
31
- const method = req . method || 'GET' ;
32
- let bodyProps : BodyProps = { } ;
33
- const bodyAllowed = method !== 'HEAD' && method !== 'GET' && ! options ?. emptyBody ;
34
- if ( bodyAllowed ) {
35
- bodyProps = makeRequestBody ( req ) ;
22
+ export class NodeApp extends App {
23
+ match ( req : NodeRequest | Request ) {
24
+ if ( ! ( req instanceof Request ) ) {
25
+ req = NodeApp . createRequest ( req , {
26
+ skipBody : true ,
27
+ } ) ;
28
+ }
29
+ return super . match ( req ) ;
30
+ }
31
+ render ( request : NodeRequest | Request , options ?: RenderOptions ) : Promise < Response > ;
32
+ /**
33
+ * @deprecated Instead of passing `RouteData` and locals individually, pass an object with `routeData` and `locals` properties.
34
+ * See https://github.com/withastro/astro/pull/9199 for more information.
35
+ */
36
+ render (
37
+ request : NodeRequest | Request ,
38
+ routeData ?: RouteData ,
39
+ locals ?: object
40
+ ) : Promise < Response > ;
41
+ render (
42
+ req : NodeRequest | Request ,
43
+ routeDataOrOptions ?: RouteData | RenderOptions ,
44
+ maybeLocals ?: object
45
+ ) {
46
+ if ( ! ( req instanceof Request ) ) {
47
+ req = NodeApp . createRequest ( req ) ;
48
+ }
49
+ // @ts -expect-error The call would have succeeded against the implementation, but implementation signatures of overloads are not externally visible.
50
+ return super . render ( req , routeDataOrOptions , maybeLocals ) ;
36
51
}
37
- const request = new Request ( url , {
38
- method,
39
- headers,
40
- ...bodyProps ,
41
- } ) ;
42
- if ( req . socket ?. remoteAddress ) {
43
- Reflect . set ( request , clientAddressSymbol , req . socket . remoteAddress ) ;
52
+
53
+ /**
54
+ * Converts a NodeJS IncomingMessage into a web standard Request.
55
+ * ```js
56
+ * import { NodeApp } from 'astro/app/node';
57
+ * import { createServer } from 'node:http';
58
+ *
59
+ * const server = createServer(async (req, res) => {
60
+ * const request = NodeApp.createRequest(req);
61
+ * const response = await app.render(request);
62
+ * await NodeApp.writeResponse(response, res);
63
+ * })
64
+ * ```
65
+ */
66
+ static createRequest (
67
+ req : NodeRequest ,
68
+ { skipBody = false } = { }
69
+ ) : Request {
70
+ const protocol = req . headers [ 'x-forwarded-proto' ] ??
71
+ ( 'encrypted' in req . socket && req . socket . encrypted ? 'https' : 'http' ) ;
72
+ const hostname = req . headers . host || req . headers [ ':authority' ] ;
73
+ const url = `${ protocol } ://${ hostname } ${ req . url } ` ;
74
+ const options : RequestInit = {
75
+ method : req . method || 'GET' ,
76
+ headers : makeRequestHeaders ( req ) ,
77
+ }
78
+ const bodyAllowed = options . method !== 'HEAD' && options . method !== 'GET' && skipBody === false ;
79
+ if ( bodyAllowed ) {
80
+ Object . assign ( options , makeRequestBody ( req ) ) ;
81
+ }
82
+ const request = new Request ( url , options ) ;
83
+ if ( req . socket ?. remoteAddress ) {
84
+ Reflect . set ( request , clientAddressSymbol , req . socket . remoteAddress ) ;
85
+ }
86
+ return request ;
44
87
}
45
- return request ;
88
+
89
+ /**
90
+ * Streams a web-standard Response into a NodeJS Server Response.
91
+ * ```js
92
+ * import { NodeApp } from 'astro/app/node';
93
+ * import { createServer } from 'node:http';
94
+ *
95
+ * const server = createServer(async (req, res) => {
96
+ * const request = NodeApp.createRequest(req);
97
+ * const response = await app.render(request);
98
+ * await NodeApp.writeResponse(response, res);
99
+ * })
100
+ * ```
101
+ * @param source WhatWG Response
102
+ * @param destination NodeJS ServerResponse
103
+ */
104
+ static async writeResponse ( source : Response , destination : ServerResponse ) {
105
+ const { status, headers, body } = source ;
106
+ destination . writeHead ( status , createOutgoingHttpHeaders ( headers ) ) ;
107
+ if ( body ) {
108
+ try {
109
+ const reader = body . getReader ( ) ;
110
+ destination . on ( 'close' , ( ) => {
111
+ // Cancelling the reader may reject not just because of
112
+ // an error in the ReadableStream's cancel callback, but
113
+ // also because of an error anywhere in the stream.
114
+ reader . cancel ( ) . catch ( err => {
115
+ console . error ( `There was an uncaught error in the middle of the stream while rendering ${ destination . req . url } .` , err ) ;
116
+ } ) ;
117
+ } ) ;
118
+ let result = await reader . read ( ) ;
119
+ while ( ! result . done ) {
120
+ destination . write ( result . value ) ;
121
+ result = await reader . read ( ) ;
122
+ }
123
+ // the error will be logged by the "on end" callback above
124
+ } catch {
125
+ destination . write ( 'Internal server error' ) ;
126
+ }
127
+ }
128
+ destination . end ( ) ;
129
+ } ;
46
130
}
47
131
48
- function makeRequestHeaders ( req : NodeIncomingMessage ) : Headers {
132
+ function makeRequestHeaders ( req : NodeRequest ) : Headers {
49
133
const headers = new Headers ( ) ;
50
134
for ( const [ name , value ] of Object . entries ( req . headers ) ) {
51
135
if ( value === undefined ) {
@@ -62,7 +146,7 @@ function makeRequestHeaders(req: NodeIncomingMessage): Headers {
62
146
return headers ;
63
147
}
64
148
65
- function makeRequestBody ( req : NodeIncomingMessage ) : BodyProps {
149
+ function makeRequestBody ( req : NodeRequest ) : RequestInit {
66
150
if ( req . body !== undefined ) {
67
151
if ( typeof req . body === 'string' && req . body . length > 0 ) {
68
152
return { body : Buffer . from ( req . body ) } ;
@@ -86,7 +170,7 @@ function makeRequestBody(req: NodeIncomingMessage): BodyProps {
86
170
return asyncIterableToBodyProps ( req ) ;
87
171
}
88
172
89
- function asyncIterableToBodyProps ( iterable : AsyncIterable < any > ) : BodyProps {
173
+ function asyncIterableToBodyProps ( iterable : AsyncIterable < any > ) : RequestInit {
90
174
return {
91
175
// Node uses undici for the Request implementation. Undici accepts
92
176
// a non-standard async iterable for the body.
@@ -95,49 +179,8 @@ function asyncIterableToBodyProps(iterable: AsyncIterable<any>): BodyProps {
95
179
// The duplex property is required when using a ReadableStream or async
96
180
// iterable for the body. The type definitions do not include the duplex
97
181
// property because they are not up-to-date.
98
- // @ts -expect-error
99
182
duplex : 'half' ,
100
- } satisfies BodyProps ;
101
- }
102
-
103
- class NodeIncomingMessage extends IncomingMessage {
104
- /**
105
- * Allow the request body to be explicitly overridden. For example, this
106
- * is used by the Express JSON middleware.
107
- */
108
- body ?: unknown ;
109
- }
110
-
111
- export class NodeApp extends App {
112
- match ( req : NodeIncomingMessage | Request ) {
113
- if ( ! ( req instanceof Request ) ) {
114
- req = createRequestFromNodeRequest ( req , {
115
- emptyBody : true ,
116
- } ) ;
117
- }
118
- return super . match ( req ) ;
119
- }
120
- render ( request : NodeIncomingMessage | Request , options ?: RenderOptions ) : Promise < Response > ;
121
- /**
122
- * @deprecated Instead of passing `RouteData` and locals individually, pass an object with `routeData` and `locals` properties.
123
- * See https://github.com/withastro/astro/pull/9199 for more information.
124
- */
125
- render (
126
- request : NodeIncomingMessage | Request ,
127
- routeData ?: RouteData ,
128
- locals ?: object
129
- ) : Promise < Response > ;
130
- render (
131
- req : NodeIncomingMessage | Request ,
132
- routeDataOrOptions ?: RouteData | RenderOptions ,
133
- maybeLocals ?: object
134
- ) {
135
- if ( ! ( req instanceof Request ) ) {
136
- req = createRequestFromNodeRequest ( req ) ;
137
- }
138
- // @ts -expect-error The call would have succeeded against the implementation, but implementation signatures of overloads are not externally visible.
139
- return super . render ( req , routeDataOrOptions , maybeLocals ) ;
140
- }
183
+ } ;
141
184
}
142
185
143
186
export async function loadManifest ( rootFolder : URL ) : Promise < SSRManifest > {
0 commit comments