Skip to content

Commit 39f5ae1

Browse files
piehmrstork
andauthoredApr 30, 2024
feat: support dotenv files (#429)
* test: add e2e tests for checking access to env vars defined in dot env files * fix: support dotenv files in generated handler function --------- Co-authored-by: Mateusz Bocian <mrstork@users.noreply.github.com>
1 parent 9620588 commit 39f5ae1

File tree

20 files changed

+115
-27
lines changed

20 files changed

+115
-27
lines changed
 

‎src/build/functions/server.ts

+28-20
Original file line numberDiff line numberDiff line change
@@ -20,30 +20,38 @@ const tracer = wrapTracer(trace.getTracer('Next runtime'))
2020
const copyHandlerDependencies = async (ctx: PluginContext) => {
2121
await tracer.withActiveSpan('copyHandlerDependencies', async (span) => {
2222
const promises: Promise<void>[] = []
23-
const { included_files: includedFiles = [] } = ctx.netlifyConfig.functions?.['*'] || {}
2423
// if the user specified some files to include in the lambda
2524
// we need to copy them to the functions-internal folder
25+
const { included_files: includedFiles = [] } = ctx.netlifyConfig.functions?.['*'] || {}
26+
27+
// we also force including the .env files to ensure those are available in the lambda
28+
includedFiles.push(
29+
posixJoin(ctx.relativeAppDir, '.env'),
30+
posixJoin(ctx.relativeAppDir, '.env.production'),
31+
posixJoin(ctx.relativeAppDir, '.env.local'),
32+
posixJoin(ctx.relativeAppDir, '.env.production.local'),
33+
)
34+
2635
span.setAttribute('next.includedFiles', includedFiles.join(','))
27-
if (includedFiles.length !== 0) {
28-
const resolvedFiles = await Promise.all(
29-
includedFiles.map((globPattern) => glob(globPattern, { cwd: process.cwd() })),
36+
37+
const resolvedFiles = await Promise.all(
38+
includedFiles.map((globPattern) => glob(globPattern, { cwd: process.cwd() })),
39+
)
40+
for (const filePath of resolvedFiles.flat()) {
41+
promises.push(
42+
cp(
43+
join(process.cwd(), filePath),
44+
// the serverHandlerDir is aware of the dist dir.
45+
// The distDir must not be the package path therefore we need to rely on the
46+
// serverHandlerDir instead of the serverHandlerRootDir
47+
// therefore we need to remove the package path from the filePath
48+
join(ctx.serverHandlerDir, relative(ctx.relativeAppDir, filePath)),
49+
{
50+
recursive: true,
51+
force: true,
52+
},
53+
),
3054
)
31-
for (const filePath of resolvedFiles.flat()) {
32-
promises.push(
33-
cp(
34-
join(process.cwd(), filePath),
35-
// the serverHandlerDir is aware of the dist dir.
36-
// The distDir must not be the package path therefore we need to rely on the
37-
// serverHandlerDir instead of the serverHandlerRootDir
38-
// therefore we need to remove the package path from the filePath
39-
join(ctx.serverHandlerDir, relative(ctx.relativeAppDir, filePath)),
40-
{
41-
recursive: true,
42-
force: true,
43-
},
44-
),
45-
)
46-
}
4755
}
4856

4957
const fileList = await glob('dist/**/*', { cwd: ctx.pluginDir })

‎tests/e2e/nx-integrated.test.ts

+13
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,16 @@ test('Renders the Home page correctly with distDir', async ({ page, nxIntegrated
2929

3030
await expectImageWasLoaded(page.locator('img'))
3131
})
32+
33+
test('environment variables from .env files should be available for functions', async ({
34+
nxIntegratedDistDir,
35+
}) => {
36+
const response = await fetch(`${nxIntegratedDistDir.url}/api/env`)
37+
const data = await response.json()
38+
expect(data).toEqual({
39+
'.env': 'defined in .env',
40+
'.env.local': 'defined in .env.local',
41+
'.env.production': 'defined in .env.production',
42+
'.env.production.local': 'defined in .env.production.local',
43+
})
44+
})

‎tests/e2e/page-router.test.ts

+13
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,19 @@ test.describe('Simple Page Router (no basePath, no i18n)', () => {
379379
expect(headers['netlify-cdn-cache-control']).toBe('max-age=31536000')
380380
expect(headers['cache-control']).toBe('public,max-age=0,must-revalidate')
381381
})
382+
383+
test('environment variables from .env files should be available for functions', async ({
384+
pageRouter,
385+
}) => {
386+
const response = await fetch(`${pageRouter.url}/api/env`)
387+
const data = await response.json()
388+
expect(data).toEqual({
389+
'.env': 'defined in .env',
390+
'.env.local': 'defined in .env.local',
391+
'.env.production': 'defined in .env.production',
392+
'.env.production.local': 'defined in .env.production.local',
393+
})
394+
})
382395
})
383396

384397
test.describe('Page Router with basePath and i18n', () => {

‎tests/e2e/turborepo.test.ts

+13
Original file line numberDiff line numberDiff line change
@@ -206,4 +206,17 @@ test.describe('[NPM] Package manager', () => {
206206
const date3 = await page.textContent('[data-testid="date-now"]')
207207
expect(date3).not.toBe(date2)
208208
})
209+
210+
test('environment variables from .env files should be available for functions', async ({
211+
turborepoNPM,
212+
}) => {
213+
const response = await fetch(`${turborepoNPM.url}/api/env`)
214+
const data = await response.json()
215+
expect(data).toEqual({
216+
'.env': 'defined in .env',
217+
'.env.local': 'defined in .env.local',
218+
'.env.production': 'defined in .env.production',
219+
'.env.production.local': 'defined in .env.production.local',
220+
})
221+
})
209222
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
FROM_DOT_ENV="defined in .env"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
FROM_DOT_ENV_DOT_LOCAL="defined in .env.local"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
FROM_DOT_ENV_DOT_PRODUCTION="defined in .env.production"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
FROM_DOT_ENV_DOT_PRODUCTION_DOT_LOCAL="defined in .env.production.local"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { NextResponse } from 'next/server'
2+
3+
export async function GET() {
4+
return NextResponse.json({
5+
'.env': process.env.FROM_DOT_ENV ?? 'undefined',
6+
'.env.local': process.env.FROM_DOT_ENV_DOT_LOCAL ?? 'undefined',
7+
'.env.production': process.env.FROM_DOT_ENV_DOT_PRODUCTION ?? 'undefined',
8+
'.env.production.local': process.env.FROM_DOT_ENV_DOT_PRODUCTION_DOT_LOCAL ?? 'undefined',
9+
})
10+
}
11+
12+
export const dynamic = 'force-dynamic'

‎tests/fixtures/page-router/.env

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
FROM_DOT_ENV="defined in .env"

‎tests/fixtures/page-router/.env.local

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
FROM_DOT_ENV_DOT_LOCAL="defined in .env.local"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
FROM_DOT_ENV_DOT_PRODUCTION="defined in .env.production"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
FROM_DOT_ENV_DOT_PRODUCTION_DOT_LOCAL="defined in .env.production.local"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* @param {import('next').NextApiRequest} _req
3+
* @param {import('next').NextApiResponse} res
4+
*/
5+
export default async function handler(_req, res) {
6+
res.status(200).json({
7+
'.env': process.env.FROM_DOT_ENV ?? 'undefined',
8+
'.env.local': process.env.FROM_DOT_ENV_DOT_LOCAL ?? 'undefined',
9+
'.env.production': process.env.FROM_DOT_ENV_DOT_PRODUCTION ?? 'undefined',
10+
'.env.production.local': process.env.FROM_DOT_ENV_DOT_PRODUCTION_DOT_LOCAL ?? 'undefined',
11+
})
12+
}

‎tests/fixtures/turborepo-npm/.gitignore

-7
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,6 @@ node_modules
55
.pnp
66
.pnp.js
77

8-
# Local env files
9-
.env
10-
.env.local
11-
.env.development.local
12-
.env.test.local
13-
.env.production.local
14-
158
# Testing
169
coverage
1710

Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
FROM_DOT_ENV="defined in .env"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
FROM_DOT_ENV_DOT_LOCAL="defined in .env.local"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
FROM_DOT_ENV_DOT_PRODUCTION="defined in .env.production"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
FROM_DOT_ENV_DOT_PRODUCTION_DOT_LOCAL="defined in .env.production.local"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* @param {import('next').NextApiRequest} _req
3+
* @param {import('next').NextApiResponse} res
4+
*/
5+
export default async function handler(_req, res) {
6+
res.status(200).json({
7+
'.env': process.env.FROM_DOT_ENV ?? 'undefined',
8+
'.env.local': process.env.FROM_DOT_ENV_DOT_LOCAL ?? 'undefined',
9+
'.env.production': process.env.FROM_DOT_ENV_DOT_PRODUCTION ?? 'undefined',
10+
'.env.production.local': process.env.FROM_DOT_ENV_DOT_PRODUCTION_DOT_LOCAL ?? 'undefined',
11+
})
12+
}

0 commit comments

Comments
 (0)
Please sign in to comment.