Skip to content

Commit d2fc4fe

Browse files
james-elicxvicb
authored andcommitted
inline project env files in worker (#181)
* extract env vars from file system * combine variables from a global with the request-scoped env * inline build-time env vars in the worker script * for some reason the tests failed in the pipeline but not locally * switch between modes at runtime and apply on process.env * add test for referencing variables * use a .env.mjs file for the vars * Update packages/cloudflare/src/cli/build/patches/investigated/copy-package-cli-files.ts * move the merging to extractProjectEnvVars * rename secrets to nextEnvVars * add missing mode when retrieving value * add link to nextjs var load order * rename to compile * change function to read a single file * move the readEnvFile call inside the flatMap * remove process.env.node_env usage * add e2e test for env vars * move locations
1 parent 4889a15 commit d2fc4fe

File tree

14 files changed

+294
-3
lines changed

14 files changed

+294
-3
lines changed

examples/api/.dev.vars

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
NEXTJS_ENV=development

examples/api/.env.development

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
TEST_ENV_VAR=TEST_VALUE

examples/api/app/api/env/route.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export async function GET() {
2+
return new Response(JSON.stringify(process.env));
3+
}

examples/api/e2e/base.spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,8 @@ test("the hello-world api POST route works as intended", async ({ page }) => {
3030
expect(res.headers()["content-type"]).toContain("text/plain");
3131
await expect(res.text()).resolves.toEqual("Hello post-World! body=some body");
3232
});
33+
34+
test("sets environment variables from the Next.js env file", async ({ page }) => {
35+
const res = await page.request.get("/api/env");
36+
await expect(res.json()).resolves.toEqual(expect.objectContaining({ TEST_ENV_VAR: "TEST_VALUE" }));
37+
});

packages/cloudflare/env.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ declare global {
77
NEXT_PRIVATE_DEBUG_CACHE?: string;
88
__OPENNEXT_KV_BINDING_NAME: string;
99
OPEN_NEXT_ORIGIN: string;
10+
NODE_ENV?: string;
11+
__OPENNEXT_PROCESSED_ENV?: string;
1012
}
1113
}
1214
}

packages/cloudflare/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,14 @@
6060
"tsup": "catalog:",
6161
"typescript": "catalog:",
6262
"typescript-eslint": "catalog:",
63-
"vitest": "catalog:"
63+
"vitest": "catalog:",
64+
"mock-fs": "catalog:",
65+
"@types/mock-fs": "catalog:"
6466
},
6567
"dependencies": {
6668
"@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@678",
67-
"ts-morph": "catalog:"
69+
"ts-morph": "catalog:",
70+
"@dotenvx/dotenvx": "catalog:"
6871
},
6972
"peerDependencies": {
7073
"wrangler": "catalog:"

packages/cloudflare/src/cli/build/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type { OpenNextConfig } from "@opennextjs/aws/types/open-next.js";
1515
import type { ProjectOptions } from "../config";
1616
import { containsDotNextDir, getConfig } from "../config";
1717
import { bundleServer } from "./bundle-server";
18+
import { compileEnvFiles } from "./open-next/compile-env-files";
1819
import { createServerBundle } from "./open-next/createServerBundle";
1920

2021
/**
@@ -71,6 +72,9 @@ export async function build(projectOpts: ProjectOptions): Promise<void> {
7172
// Compile cache.ts
7273
compileCache(options);
7374

75+
// Compile .env files
76+
compileEnvFiles(options);
77+
7478
// Compile middleware
7579
await createMiddleware(options, { forceOnlyBuildOnce: true });
7680

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import fs from "node:fs";
2+
import path from "node:path";
3+
4+
import { BuildOptions } from "@opennextjs/aws/build/helper.js";
5+
6+
import { extractProjectEnvVars } from "../utils";
7+
8+
/**
9+
* Compiles the values extracted from the project's env files to the output directory for use in the worker.
10+
*/
11+
export function compileEnvFiles(options: BuildOptions) {
12+
["production", "development", "test"].forEach((mode) =>
13+
fs.appendFileSync(
14+
path.join(options.outputDir, `.env.mjs`),
15+
`export const ${mode} = ${JSON.stringify(extractProjectEnvVars(mode, options))};\n`
16+
)
17+
);
18+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { appendFileSync, writeFileSync } from "node:fs";
2+
3+
import { BuildOptions } from "@opennextjs/aws/build/helper.js";
4+
import mockFs from "mock-fs";
5+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
6+
7+
import { extractProjectEnvVars } from "./extract-project-env-vars";
8+
9+
const options = { monorepoRoot: "", appPath: "" } as BuildOptions;
10+
11+
describe("extractProjectEnvVars", () => {
12+
beforeEach(() => {
13+
mockFs({
14+
".env": "ENV_VAR=value",
15+
".env.local": "ENV_LOCAL_VAR=value",
16+
".env.development": "ENV_DEV_VAR=value",
17+
".env.development.local": "ENV_DEV_LOCAL_VAR=value",
18+
".env.production": "ENV_PROD_VAR=value",
19+
".env.production.local": "ENV_PROD_LOCAL_VAR=value",
20+
});
21+
});
22+
23+
afterEach(() => mockFs.restore());
24+
25+
it("should extract production env vars", () => {
26+
const result = extractProjectEnvVars("production", options);
27+
expect(result).toEqual({
28+
ENV_LOCAL_VAR: "value",
29+
ENV_PROD_LOCAL_VAR: "value",
30+
ENV_PROD_VAR: "value",
31+
ENV_VAR: "value",
32+
});
33+
});
34+
35+
it("should extract development env vars", () => {
36+
writeFileSync(".dev.vars", 'NEXTJS_ENV = "development"');
37+
38+
const result = extractProjectEnvVars("development", options);
39+
expect(result).toEqual({
40+
ENV_LOCAL_VAR: "value",
41+
ENV_DEV_LOCAL_VAR: "value",
42+
ENV_DEV_VAR: "value",
43+
ENV_VAR: "value",
44+
});
45+
});
46+
47+
it("should override env vars with those in a local file", () => {
48+
writeFileSync(".env.production.local", "ENV_PROD_VAR=overridden");
49+
50+
const result = extractProjectEnvVars("production", options);
51+
expect(result).toEqual({
52+
ENV_LOCAL_VAR: "value",
53+
ENV_PROD_VAR: "overridden",
54+
ENV_VAR: "value",
55+
});
56+
});
57+
58+
it("should support referencing variables", () => {
59+
appendFileSync(".env.production.local", "\nENV_PROD_LOCAL_VAR_REF=$ENV_PROD_LOCAL_VAR");
60+
61+
const result = extractProjectEnvVars("production", options);
62+
expect(result).toEqual({
63+
ENV_LOCAL_VAR: "value",
64+
ENV_PROD_LOCAL_VAR: "value",
65+
ENV_PROD_LOCAL_VAR_REF: "value",
66+
ENV_PROD_VAR: "value",
67+
ENV_VAR: "value",
68+
});
69+
});
70+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import * as fs from "node:fs";
2+
import * as path from "node:path";
3+
4+
import { parse } from "@dotenvx/dotenvx";
5+
import type { BuildOptions } from "@opennextjs/aws/build/helper.js";
6+
7+
function readEnvFile(filePath: string) {
8+
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
9+
return parse(fs.readFileSync(filePath).toString());
10+
}
11+
}
12+
13+
/**
14+
* Extracts the environment variables defined in various .env files for a project.
15+
*
16+
* The `NEXTJS_ENV` environment variable in `.dev.vars` determines the mode.
17+
*
18+
* Merged variables respect the following priority order.
19+
* 1. `.env.{mode}.local`
20+
* 2. `.env.local`
21+
* 3. `.env.{mode}`
22+
* 4. `.env`
23+
*
24+
* https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables#environment-variable-load-order
25+
*
26+
* In a monorepo, the env files in an app's directory will take precedence over
27+
* the env files at the root of the monorepo.
28+
*/
29+
export function extractProjectEnvVars(mode: string, { monorepoRoot, appPath }: BuildOptions) {
30+
return [".env", `.env.${mode}`, ".env.local", `.env.${mode}.local`]
31+
.flatMap((fileName) => [
32+
...(monorepoRoot !== appPath ? [readEnvFile(path.join(monorepoRoot, fileName))] : []),
33+
readEnvFile(path.join(appPath, fileName)),
34+
])
35+
.reduce<Record<string, string>>((acc, overrides) => ({ ...acc, ...overrides }), {});
36+
}

0 commit comments

Comments
 (0)