Skip to content

Commit b9f229e

Browse files
Merge branch 'main' into renovate/netlify-blobs-10.x
2 parents d419de6 + 25fab29 commit b9f229e

File tree

2 files changed

+174
-1
lines changed

2 files changed

+174
-1
lines changed

src/lib/functions/runtimes/js/index.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,30 @@ export const invokeFunction = async ({
8484
timeoutMs: timeout * SECONDS_TO_MILLISECONDS,
8585
}
8686

87-
const worker = new Worker(workerURL, { workerData })
87+
const worker = new Worker(workerURL, {
88+
env: {
89+
...process.env,
90+
// AWS Lambda disables these Node.js experimental features, even in Node.js versions where they are enabled by
91+
// default: https://docs.aws.amazon.com/lambda/latest/dg/lambda-nodejs.html#w292aac41c19.
92+
// They also allow users to re-enable (i.e. not disable) these by co-opting the positive flag (which in reality
93+
// may or may not exist depending on the exact node.js version). We replicate all this behavior here.
94+
NODE_OPTIONS: [
95+
...(process.env.NODE_OPTIONS?.split(' ') ?? []),
96+
...[
97+
...(process.env.NODE_OPTIONS?.includes('--experimental-require-module')
98+
? []
99+
: ['--no-experimental-require-module']),
100+
...(process.env.NODE_OPTIONS?.includes('--experimental-detect-module')
101+
? []
102+
: ['--no-experimental-detect-module']),
103+
]
104+
// Unfortunately Node.js throws if `NODE_OPTIONS` contains any unsupported flags and these flags have been
105+
// added and removed in various specific versions in each major line. Luckily Node.js has an API just for this!
106+
.filter((flag) => process.allowedNodeEnvironmentFlags.has(flag)),
107+
].join(' '),
108+
},
109+
workerData,
110+
})
88111
return await new Promise((resolve, reject) => {
89112
worker.on('message', (result: WorkerMessage): void => {
90113
// TODO(serhalp): Improve `WorkerMessage` type. It sure would be nice to keep it simple as it

tests/integration/commands/functions-serve/functions-serve.test.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import js from 'dedent'
33
import execa from 'execa'
44
import getPort from 'get-port'
55
import fetch from 'node-fetch'
6+
import semver from 'semver'
67
import { describe, test } from 'vitest'
78
import waitPort from 'wait-port'
89

@@ -203,6 +204,155 @@ describe.concurrent('functions:serve command', () => {
203204
})
204205
})
205206

207+
test('should thread env vars from user env to function execution environment', async (t) => {
208+
const port = await getPort()
209+
await withSiteBuilder(t, async (builder) => {
210+
await builder
211+
.withContentFile({
212+
path: 'netlify/functions/get-env.js',
213+
content: `
214+
export default async () => Response.json(process.env)
215+
export const config = { path: "/get-env" }
216+
`,
217+
})
218+
.build()
219+
220+
await withFunctionsServer({ builder, args: ['--port', port.toString()], port, env: { foo: 'bar' } }, async () => {
221+
const response = await fetch(`http://localhost:${port.toString()}/get-env`)
222+
t.expect(await response.json()).toMatchObject(t.expect.objectContaining({ foo: 'bar' }))
223+
})
224+
})
225+
})
226+
227+
test('should thread `NODE_OPTIONS` if set in user env to function execution environment', async (t) => {
228+
const port = await getPort()
229+
await withSiteBuilder(t, async (builder) => {
230+
await builder
231+
.withContentFile({
232+
path: 'netlify/functions/get-env.js',
233+
content: `
234+
export default async () => new Response(process.env.NODE_OPTIONS)
235+
export const config = { path: "/get-env" }
236+
`,
237+
})
238+
.build()
239+
240+
await withFunctionsServer(
241+
{
242+
builder,
243+
args: ['--port', port.toString()],
244+
port,
245+
env: { NODE_OPTIONS: '--abort-on-uncaught-exception --trace-exit' },
246+
},
247+
async () => {
248+
const response = await fetch(`http://localhost:${port.toString()}/get-env`)
249+
t.expect(await response.text()).toContain('--abort-on-uncaught-exception --trace-exit')
250+
},
251+
)
252+
})
253+
})
254+
255+
// Testing just 22.12.0+ for simplicity. The real range is quite complex.
256+
test.runIf(semver.gte(process.versions.node, '22.12.0'))(
257+
'should add AWS Lambda compat `NODE_OPTIONS` to function execution environment',
258+
async (t) => {
259+
const port = await getPort()
260+
await withSiteBuilder(t, async (builder) => {
261+
await builder
262+
.withContentFile({
263+
path: 'netlify/functions/get-env.js',
264+
content: `
265+
export default async () => new Response(process.env.NODE_OPTIONS)
266+
export const config = { path: "/get-env" }
267+
`,
268+
})
269+
.build()
270+
271+
await withFunctionsServer(
272+
{
273+
builder,
274+
args: ['--port', port.toString()],
275+
port,
276+
env: { NODE_OPTIONS: '--abort-on-uncaught-exception --trace-exit' },
277+
},
278+
async () => {
279+
const response = await fetch(`http://localhost:${port.toString()}/get-env`)
280+
const body = await response.text()
281+
t.expect(body).toContain('--no-experimental-require-module')
282+
t.expect(body).toContain('--no-experimental-detect-module')
283+
t.expect(body).toContain('--abort-on-uncaught-exception --trace-exit')
284+
},
285+
)
286+
})
287+
},
288+
)
289+
290+
test.runIf(
291+
process.allowedNodeEnvironmentFlags.has('--no-experimental-require-module') ||
292+
process.allowedNodeEnvironmentFlags.has('--experimental-require-module'),
293+
)('should allow user to re-enable experimental require module feature', async (t) => {
294+
const port = await getPort()
295+
await withSiteBuilder(t, async (builder) => {
296+
await builder
297+
.withContentFile({
298+
path: 'netlify/functions/get-env.js',
299+
content: `
300+
export default async () => new Response(process.env.NODE_OPTIONS)
301+
export const config = { path: "/get-env" }
302+
`,
303+
})
304+
.build()
305+
306+
await withFunctionsServer(
307+
{
308+
builder,
309+
args: ['--port', port.toString()],
310+
port,
311+
env: { NODE_OPTIONS: '--experimental-require-module' },
312+
},
313+
async () => {
314+
const response = await fetch(`http://localhost:${port.toString()}/get-env`)
315+
const body = await response.text()
316+
t.expect(body).toContain('--experimental-require-module')
317+
t.expect(body).not.toContain('--no-experimental-require-module')
318+
},
319+
)
320+
})
321+
})
322+
323+
test.runIf(
324+
process.allowedNodeEnvironmentFlags.has('--no-experimental-detect-module') ||
325+
process.allowedNodeEnvironmentFlags.has('--experimental-detect-module'),
326+
)('should allow user to re-enable experimental detect module feature', async (t) => {
327+
const port = await getPort()
328+
await withSiteBuilder(t, async (builder) => {
329+
await builder
330+
.withContentFile({
331+
path: 'netlify/functions/get-env.js',
332+
content: `
333+
export default async () => new Response(process.env.NODE_OPTIONS)
334+
export const config = { path: "/get-env" }
335+
`,
336+
})
337+
.build()
338+
339+
await withFunctionsServer(
340+
{
341+
builder,
342+
args: ['--port', port.toString()],
343+
port,
344+
env: { NODE_OPTIONS: '--experimental-detect-module' },
345+
},
346+
async () => {
347+
const response = await fetch(`http://localhost:${port.toString()}/get-env`)
348+
const body = await response.text()
349+
t.expect(body).toContain('--experimental-detect-module')
350+
t.expect(body).not.toContain('--no-experimental-detect-module')
351+
},
352+
)
353+
})
354+
})
355+
206356
test('should inject AI Gateway when linked site and online', async (t) => {
207357
await withSiteBuilder(t, async (builder) => {
208358
const { siteInfo, aiGatewayToken, routes } = createAIGatewayTestData()

0 commit comments

Comments
 (0)