Skip to content

Commit 55e13ab

Browse files
committed
feat: switch function bundling to esbuild
1 parent c43f518 commit 55e13ab

12 files changed

+441
-257
lines changed

src/lib/config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ const NEXT_SRC_DIRS = getNextSrcDirs()
2323
const TEMPLATES_DIR = join(__dirname, 'templates')
2424

2525
// This is the Netlify Function template that wraps all SSR pages
26-
const FUNCTION_TEMPLATE_PATH = join(TEMPLATES_DIR, 'netlifyFunction.js')
26+
const FUNCTION_TEMPLATE_PATH = join(TEMPLATES_DIR, 'netlifyFunction.ts')
2727

2828
// This is the Netlify Builder template that wraps ISR pages
29-
const BUILDER_TEMPLATE_PATH = join(TEMPLATES_DIR, 'netlifyOnDemandBuilder.js')
29+
const BUILDER_TEMPLATE_PATH = join(TEMPLATES_DIR, 'netlifyOnDemandBuilder.ts')
3030

3131
// This is the file where custom redirects can be configured
3232
const CUSTOM_REDIRECTS_PATH = join('.', '_redirects')

src/lib/functions/index.js

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
// TEMPLATE: This file will be copied to the Netlify functions directory
2+
3+
// Render function for the Next.js page
4+
const { Buffer } = require('buffer')
5+
const http = require('http')
6+
const queryString = require('querystring')
7+
const Stream = require('stream')
8+
9+
// Mock a HTTP IncomingMessage object from the Netlify Function event parameters
10+
// Based on API Gateway Lambda Compat
11+
// Source: https://github.com/serverless-nextjs/serverless-next.js/blob/master/packages/compat-layers/apigw-lambda-compat/lib/compatLayer.js
12+
13+
const createRequestObject = ({ event, context }) => {
14+
const {
15+
requestContext = {},
16+
path = '',
17+
multiValueQueryStringParameters,
18+
pathParameters,
19+
httpMethod,
20+
multiValueHeaders = {},
21+
body,
22+
isBase64Encoded,
23+
} = event
24+
25+
const newStream = new Stream.Readable()
26+
const req = Object.assign(newStream, http.IncomingMessage.prototype)
27+
req.url = (requestContext.path || path || '').replace(new RegExp(`^/${requestContext.stage}`), '') || '/'
28+
29+
let qs = ''
30+
31+
if (multiValueQueryStringParameters) {
32+
qs += queryString.stringify(multiValueQueryStringParameters)
33+
}
34+
35+
if (pathParameters) {
36+
const pathParametersQs = queryString.stringify(pathParameters)
37+
38+
qs += qs.length === 0 ? pathParametersQs : `&${pathParametersQs}`
39+
}
40+
41+
const hasQueryString = qs.length !== 0
42+
43+
if (hasQueryString) {
44+
req.url += `?${qs}`
45+
}
46+
47+
req.method = httpMethod
48+
req.rawHeaders = []
49+
req.headers = {}
50+
51+
// Expose Netlify Function event and callback on request object.
52+
// This makes it possible to access the clientContext, for example.
53+
// See: https://github.com/netlify/next-on-netlify/issues/20
54+
// It also allows users to change the behavior of waiting for empty event
55+
// loop.
56+
// See: https://github.com/netlify/next-on-netlify/issues/66#issuecomment-719988804
57+
req.netlifyFunctionParams = { event, context }
58+
59+
for (const key of Object.keys(multiValueHeaders)) {
60+
for (const value of multiValueHeaders[key]) {
61+
req.rawHeaders.push(key)
62+
req.rawHeaders.push(value)
63+
}
64+
req.headers[key.toLowerCase()] = multiValueHeaders[key].toString()
65+
}
66+
67+
req.getHeader = (name) => req.headers[name.toLowerCase()]
68+
req.getHeaders = () => req.headers
69+
70+
req.connection = {}
71+
72+
if (body) {
73+
req.push(body, isBase64Encoded ? 'base64' : undefined)
74+
}
75+
76+
req.push(null)
77+
78+
return req
79+
}
80+
81+
// Mock a HTTP ServerResponse object that returns a Netlify Function-compatible
82+
// response via the onResEnd callback when res.end() is called.
83+
// Based on API Gateway Lambda Compat
84+
// Source: https://github.com/serverless-nextjs/serverless-next.js/blob/master/packages/compat-layers/apigw-lambda-compat/lib/compatLayer.js
85+
86+
const createResponseObject = ({ onResEnd }) => {
87+
const response = {
88+
isBase64Encoded: true,
89+
multiValueHeaders: {},
90+
}
91+
92+
const res = new Stream()
93+
Object.defineProperty(res, 'statusCode', {
94+
get() {
95+
return response.statusCode
96+
},
97+
set(statusCode) {
98+
response.statusCode = statusCode
99+
},
100+
})
101+
res.headers = {}
102+
res.writeHead = (status, headers) => {
103+
response.statusCode = status
104+
if (headers) res.headers = Object.assign(res.headers, headers)
105+
106+
// Return res object to allow for chaining
107+
// Fixes: https://github.com/netlify/next-on-netlify/pull/74
108+
return res
109+
}
110+
res.write = (chunk) => {
111+
if (!response.body) {
112+
response.body = Buffer.from('')
113+
}
114+
115+
response.body = Buffer.concat([
116+
Buffer.isBuffer(response.body) ? response.body : Buffer.from(response.body),
117+
Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk),
118+
])
119+
}
120+
res.setHeader = (name, value) => {
121+
res.headers[name.toLowerCase()] = value
122+
}
123+
res.removeHeader = (name) => {
124+
delete res.headers[name.toLowerCase()]
125+
}
126+
res.getHeader = (name) => res.headers[name.toLowerCase()]
127+
res.getHeaders = () => res.headers
128+
res.hasHeader = (name) => Boolean(res.getHeader(name))
129+
res.end = (text) => {
130+
if (text) res.write(text)
131+
if (!res.statusCode) {
132+
res.statusCode = 200
133+
}
134+
135+
if (response.body) {
136+
response.body = Buffer.from(response.body).toString('base64')
137+
}
138+
response.multiValueHeaders = res.headers
139+
res.writeHead(response.statusCode)
140+
141+
// Convert all multiValueHeaders into arrays
142+
for (const key of Object.keys(response.multiValueHeaders)) {
143+
if (!Array.isArray(response.multiValueHeaders[key])) {
144+
response.multiValueHeaders[key] = [response.multiValueHeaders[key]]
145+
}
146+
}
147+
148+
res.finished = true
149+
res.writableEnded = true
150+
// Call onResEnd handler with the response object
151+
onResEnd(response)
152+
}
153+
154+
return res
155+
}
156+
157+
// Render the Next.js page
158+
const renderNextPage = ({ event, context }, nextPage) => {
159+
// The Next.js page is rendered inside a promise that is resolved when the
160+
// Next.js page ends the response via `res.end()`
161+
const promise = new Promise((resolve) => {
162+
// Create a Next.js-compatible request and response object
163+
// These mock the ClientRequest and ServerResponse classes from node http
164+
// See: https://nodejs.org/api/http.html
165+
const req = createRequestObject({ event, context })
166+
const res = createResponseObject({
167+
onResEnd: (response) => resolve(response),
168+
})
169+
170+
// Check if page is a Next.js page or an API route
171+
const isNextPage = nextPage.render instanceof Function
172+
const isApiRoute = !isNextPage
173+
174+
// Perform the render: render() for Next.js page or default() for API route
175+
if (isNextPage) return nextPage.render(req, res)
176+
if (isApiRoute) return nextPage.default(req, res)
177+
})
178+
179+
// Return the promise
180+
return promise
181+
}
182+
183+
const getHandlerFunction = (nextPage) => async (event, context) => {
184+
// x-forwarded-host is undefined on Netlify for proxied apps that need it
185+
// fixes https://github.com/netlify/next-on-netlify/issues/46
186+
if (!event.multiValueHeaders.hasOwnProperty('x-forwarded-host')) {
187+
// eslint-disable-next-line no-param-reassign
188+
event.multiValueHeaders['x-forwarded-host'] = [event.headers.host]
189+
}
190+
191+
// Get the request URL
192+
const { path } = event
193+
console.log('[request]', path)
194+
195+
// Render the Next.js page
196+
const response = await renderNextPage({ event, context }, nextPage)
197+
198+
// Convert header values to string. Netlify does not support integers as
199+
// header values. See: https://github.com/netlify/cli/issues/451
200+
Object.keys(response.multiValueHeaders).forEach((key) => {
201+
response.multiValueHeaders[key] = response.multiValueHeaders[key].map((value) => String(value))
202+
})
203+
204+
response.multiValueHeaders['Cache-Control'] = ['no-cache']
205+
206+
return response
207+
}
208+
209+
exports.getHandlerFunction = getHandlerFunction

src/lib/helpers/setupNetlifyFunctionForPage.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ const setupNetlifyFunctionForPage = async ({ filePath, functionsPath, isApiPage,
2020
}
2121

2222
// Copy function templates
23-
const functionTemplateCopyPath = join(functionDirectory, `${functionName}.js`)
23+
const functionTemplateCopyPath = join(functionDirectory, `${functionName}.ts`)
2424
const srcTemplatePath = isISR ? BUILDER_TEMPLATE_PATH : FUNCTION_TEMPLATE_PATH
2525
copySync(srcTemplatePath, functionTemplateCopyPath, {
2626
overwrite: false,
2727
errorOnExist: true,
2828
})
2929

3030
// Copy function helpers
31-
const functionHelpers = ['functionBase.js', 'renderNextPage.js', 'createRequestObject.js', 'createResponseObject.js']
31+
const functionHelpers = ['getHandlerFunction.js']
3232
functionHelpers.forEach((helper) => {
3333
copySync(join(TEMPLATES_DIR, helper), join(functionDirectory, helper), {
3434
overwrite: false,

src/lib/templates/createRequestObject.js

Lines changed: 0 additions & 85 deletions
This file was deleted.

0 commit comments

Comments
 (0)