1
1
import { IncomingMessage , ServerResponse } from 'http'
2
- import Stream from 'stream'
3
- import nodeUrl , { UrlWithParsedQuery } from 'url'
4
- import Server from './next-server'
2
+ import path from 'path'
3
+ import { UrlWithParsedQuery } from 'url'
5
4
import puppeteer from 'puppeteer-core'
6
5
7
6
let browser : puppeteer . Browser | undefined
8
7
9
- /* eslint-disable-next-line */
8
+ // eslint-disable-next-line
10
9
enum ImageType {
11
10
png = 'png' ,
12
11
jpeg = 'jpeg' ,
13
12
}
14
13
15
14
export async function ogImageGenerator (
16
- server : Server ,
17
15
req : IncomingMessage ,
18
16
res : ServerResponse ,
19
- parsedUrl : UrlWithParsedQuery ,
20
- isDev = false
17
+ pathname : string ,
18
+ query : UrlWithParsedQuery [ 'query' ] ,
19
+ nonce : string
21
20
) {
22
- const { url, w, h, t } = parsedUrl . query
23
-
24
- if ( ! url ) {
25
- res . statusCode = 400
26
- res . end ( '"url" parameter is required' )
27
- return { finished : true }
28
- } else if ( Array . isArray ( url ) ) {
29
- res . statusCode = 400
30
- res . end ( '"url" parameter cannot be an array' )
31
- return { finished : true }
32
- }
21
+ const { w, h } = query
33
22
34
23
if ( ! w ) {
35
24
res . statusCode = 400
@@ -51,16 +40,6 @@ export async function ogImageGenerator(
51
40
return { finished : true }
52
41
}
53
42
54
- if ( ! t ) {
55
- res . statusCode = 400
56
- res . end ( '"t" parameter (type) is required' )
57
- return { finished : true }
58
- } else if ( Array . isArray ( t ) ) {
59
- res . statusCode = 400
60
- res . end ( '"t" parameter (type) cannot be an array' )
61
- return { finished : true }
62
- }
63
-
64
43
const width = parseInt ( w , 10 )
65
44
if ( ! width || isNaN ( width ) ) {
66
45
res . statusCode = 400
@@ -75,90 +54,49 @@ export async function ogImageGenerator(
75
54
return { finished : true }
76
55
}
77
56
78
- let type = t as ImageType
79
- if ( ! Object . values ( ImageType ) . includes ( type ) ) {
57
+ const type : ImageType = path . extname ( pathname ) . substr ( 1 ) as ImageType
58
+
59
+ if ( ! Object . keys ( ImageType ) . includes ( type ) ) {
80
60
res . statusCode = 400
81
61
res . end (
82
62
`"t" parameter (type) must be one of: ${ Object . keys ( ImageType ) . join (
83
- '\n '
63
+ ', '
84
64
) } `
85
65
)
86
66
return { finished : true }
87
67
}
88
68
89
- let upstreamStatus : number
90
- let upstreamCache : string | null
91
-
92
- try {
93
- const mockRes : any = new Stream . Writable ( )
94
-
95
- const isStreamFinished = new Promise ( function ( resolve , reject ) {
96
- mockRes . on ( 'finish' , ( ) => resolve ( true ) )
97
- mockRes . on ( 'end' , ( ) => resolve ( true ) )
98
- mockRes . on ( 'error' , ( ) => reject ( ) )
99
- } )
100
-
101
- mockRes . write = ( _chunk : Buffer | string ) => {
102
- // no-op
103
- }
104
- mockRes . _write = ( chunk : Buffer | string ) => {
105
- mockRes . write ( chunk )
106
- }
107
-
108
- const mockHeaders : Record < string , string | string [ ] > = { }
109
-
110
- mockRes . writeHead = ( _status : any , _headers : any ) =>
111
- Object . assign ( mockHeaders , _headers )
112
- mockRes . getHeader = ( name : string ) => mockHeaders [ name . toLowerCase ( ) ]
113
- mockRes . getHeaders = ( ) => mockHeaders
114
- mockRes . getHeaderNames = ( ) => Object . keys ( mockHeaders )
115
- mockRes . setHeader = ( name : string , value : string | string [ ] ) =>
116
- ( mockHeaders [ name . toLowerCase ( ) ] = value )
117
- mockRes . _implicitHeader = ( ) => { }
118
- mockRes . finished = false
119
- mockRes . statusCode = 200
120
-
121
- const mockReq : any = new Stream . Readable ( )
122
-
123
- mockReq . _read = ( ) => {
124
- mockReq . emit ( 'end' )
125
- mockReq . emit ( 'close' )
126
- return Buffer . from ( '' )
127
- }
128
-
129
- mockReq . headers = req . headers
130
- mockReq . method = req . method
131
- mockReq . url = url
132
-
133
- await server . getRequestHandler ( ) ( mockReq , mockRes , nodeUrl . parse ( url , true ) )
134
- await isStreamFinished
135
- upstreamStatus = mockRes . statusCode
136
- upstreamCache = mockRes . getHeader ( 'Cache-Control' )
137
- } catch ( err ) {
138
- res . statusCode = 500
139
- res . end ( '"url" parameter is valid but upstream response is invalid' )
140
- return { finished : true }
141
- }
69
+ const { localAddress, localPort } = req . connection
70
+ const _server = ( req . connection as any ) . _server
71
+ const isHTTPS = _server . secureProtocol
72
+ const imageUrl = `http${
73
+ isHTTPS ? 's' : ''
74
+ } ://${ localAddress } :${ localPort } ${ pathname . replace (
75
+ / \. ( j p e ? g | p n g ) / ,
76
+ ''
77
+ ) } ?_nextImageNonce=${ nonce } `
78
+
79
+ const absoluteUrl = new URL ( imageUrl )
80
+ const { buffer, upstreamStatus, upstreamCache } = await getScreenshot (
81
+ absoluteUrl ,
82
+ width ,
83
+ height ,
84
+ type
85
+ )
142
86
143
- const proto = isDev ? 'http' : 'https'
144
- const host = req . headers . host
145
- console . log ( { url, prefix : `${ proto } ://${ host } ` } )
146
- const absoluteUrl = new URL ( url , `${ proto } ://${ host } ` )
147
- const buffer = await getScreenshot ( isDev , absoluteUrl , width , height , type )
148
87
res . statusCode = upstreamStatus
149
88
res . setHeader ( 'Content-Type' , `image/${ type } ` )
89
+
150
90
// TODO: should we also set ETag header?
91
+ // re-use send-payload util?
151
92
if ( upstreamCache ) {
152
93
res . setHeader ( 'Cache-Control' , upstreamCache )
153
94
}
154
95
res . end ( buffer )
155
96
return { finished : true }
156
97
}
157
98
158
- function getOptions ( isDev : boolean ) {
159
- if ( ! isDev ) {
160
- throw new Error ( 'Production is not implemented yet' )
161
- }
99
+ function getOptions ( ) {
162
100
return {
163
101
args : [ ] ,
164
102
headless : true ,
@@ -173,23 +111,34 @@ function getOptions(isDev: boolean) {
173
111
}
174
112
175
113
async function getScreenshot (
176
- isDev : boolean ,
177
114
url : URL ,
178
115
width : number ,
179
116
height : number ,
180
117
type : ImageType
181
- ) {
118
+ ) : Promise < {
119
+ buffer : Buffer
120
+ upstreamStatus : number
121
+ upstreamCache : string
122
+ } > {
182
123
if ( ! browser ) {
183
- const options = getOptions ( isDev )
124
+ const options = getOptions ( )
184
125
browser = await puppeteer . launch ( options )
185
126
}
186
127
const page = await browser . newPage ( )
187
128
await page . setViewport ( { width, height } )
188
- await page . goto ( url . href )
129
+ const response = await page . goto ( url . href )
130
+ const upstreamStatus = response . status ( )
131
+ const upstreamCache = response . headers ( ) [ 'cache-control' ]
189
132
const file = await page . screenshot ( { type, encoding : 'binary' } )
133
+
190
134
if ( ! file || typeof file === 'string' ) {
191
135
throw new Error ( 'Expected buffer but found ' + typeof file )
192
136
}
193
137
await page . close ( )
194
- return file
138
+
139
+ return {
140
+ buffer : file ,
141
+ upstreamStatus,
142
+ upstreamCache,
143
+ }
195
144
}
0 commit comments