Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/cloudflare/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ declare global {
NEXT_PRIVATE_DEBUG_CACHE?: string;
__OPENNEXT_KV_BINDING_NAME: string;
OPEN_NEXT_ORIGIN: string;
NODE_ENV?: string;
__OPENNEXT_PROCESSED_ENV?: string;
}
}
}
Expand Down
7 changes: 5 additions & 2 deletions packages/cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,14 @@
"tsup": "catalog:",
"typescript": "catalog:",
"typescript-eslint": "catalog:",
"vitest": "catalog:"
"vitest": "catalog:",
"mock-fs": "catalog:",
"@types/mock-fs": "catalog:"
},
"dependencies": {
"@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@678",
"ts-morph": "catalog:"
"ts-morph": "catalog:",
"@dotenvx/dotenvx": "catalog:"
},
"peerDependencies": {
"wrangler": "catalog:"
Expand Down
1 change: 1 addition & 0 deletions packages/cloudflare/src/cli/build/bundle-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export async function bundleServer(config: Config, openNextOptions: BuildOptions
copyPrerenderedRoutes(config);

patches.copyPackageCliFiles(packageDistDir, config, openNextOptions);
patches.copyEnvFiles(openNextOptions);

const nextConfigStr =
fs
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import fs from "node:fs";
import path from "node:path";

import { BuildOptions } from "@opennextjs/aws/build/helper.js";

import { extractProjectEnvVars } from "../../utils";

/**
* Copies the values extracted from the project's env files to the output directory for use in the worker.
*/
export function copyEnvFiles(options: BuildOptions) {
["production", "development", "test"].forEach((mode) =>
fs.appendFileSync(
path.join(options.outputDir, `.env.mjs`),
`export const ${mode} = ${JSON.stringify(extractProjectEnvVars(mode, options))};\n`
)
);
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./copy-env-files";
export * from "./copy-package-cli-files";
export * from "./patch-cache";
export * from "./patch-require";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { appendFileSync, writeFileSync } from "node:fs";

import { BuildOptions } from "@opennextjs/aws/build/helper.js";
import mockFs from "mock-fs";
import { afterEach, beforeEach, describe, expect, it } from "vitest";

import { extractProjectEnvVars } from "./extract-project-env-vars";

const options = { monorepoRoot: "", appPath: "" } as BuildOptions;

describe("extractProjectEnvVars", () => {
beforeEach(() => {
mockFs({
".env": "ENV_VAR=value",
".env.local": "ENV_LOCAL_VAR=value",
".env.development": "ENV_DEV_VAR=value",
".env.development.local": "ENV_DEV_LOCAL_VAR=value",
".env.production": "ENV_PROD_VAR=value",
".env.production.local": "ENV_PROD_LOCAL_VAR=value",
});
});

afterEach(() => mockFs.restore());

it("should extract production env vars", () => {
const result = extractProjectEnvVars("production", options);
expect(result).toEqual({
ENV_LOCAL_VAR: "value",
ENV_PROD_LOCAL_VAR: "value",
ENV_PROD_VAR: "value",
ENV_VAR: "value",
});
});

it("should extract development env vars", () => {
writeFileSync(".dev.vars", 'NEXTJS_ENV = "development"');

const result = extractProjectEnvVars("development", options);
expect(result).toEqual({
ENV_LOCAL_VAR: "value",
ENV_DEV_LOCAL_VAR: "value",
ENV_DEV_VAR: "value",
ENV_VAR: "value",
});
});

it("should override env vars with those in a local file", () => {
writeFileSync(".env.production.local", "ENV_PROD_VAR=overridden");

const result = extractProjectEnvVars("production", options);
expect(result).toEqual({
ENV_LOCAL_VAR: "value",
ENV_PROD_VAR: "overridden",
ENV_VAR: "value",
});
});

it("should support referencing variables", () => {
appendFileSync(".env.production.local", "\nENV_PROD_LOCAL_VAR_REF=$ENV_PROD_LOCAL_VAR");

const result = extractProjectEnvVars("production", options);
expect(result).toEqual({
ENV_LOCAL_VAR: "value",
ENV_PROD_LOCAL_VAR: "value",
ENV_PROD_LOCAL_VAR_REF: "value",
ENV_PROD_VAR: "value",
ENV_VAR: "value",
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as fs from "node:fs";
import * as path from "node:path";

import { parse } from "@dotenvx/dotenvx";
import type { BuildOptions } from "@opennextjs/aws/build/helper.js";

function readEnvFiles(fileNames: string[], { monorepoRoot, appPath }: BuildOptions) {
return fileNames
.flatMap((fileName) => [
...(monorepoRoot !== appPath ? [path.join(monorepoRoot, fileName)] : []),
path.join(appPath, fileName),
])
.filter((filePath) => fs.existsSync(filePath) && fs.statSync(filePath).isFile())
.map((filePath) => parse(fs.readFileSync(filePath).toString()));
}

/**
* Extracts the environment variables defined in various .env files for a project.
*
* The `NEXTJS_ENV` environment variable in `.dev.vars` determines the mode.
*
* Merged variables respect the following priority order.
* 1. `.env.{mode}.local`
* 2. `.env.local`
* 3. `.env.{mode}`
* 4. `.env`
*
* In a monorepo, the env files in an app's directory will take precedence over
* the env files at the root of the monorepo.
*/
export function extractProjectEnvVars(mode: string, options: BuildOptions) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't that mean that we will always use the .local files while we only want to use them for wrangler dev but not in prod?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. How come you wouldn't want them in production? Next.js would use those files. I suppose you're talking about if someone builds it locally and then deploys, but in that case, you could make the argument that we shouldnt include any .env files at all because technically they're all local to the machine. I'm not quite sure what else you could do here - open to suggestions though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@james-elicx I did a quick test and you are right, .local files are used if present, regardless if it is a prod or dev build. I was wrong to think that they would only be used in dev.

const envVars = readEnvFiles([".env", `.env.${mode}`, ".env.local", `.env.${mode}.local`], options);

return envVars.reduce<Record<string, string>>((acc, overrides) => ({ ...acc, ...overrides }), {});
}
1 change: 1 addition & 0 deletions packages/cloudflare/src/cli/build/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./copy-prerendered-routes";
export * from "./extract-project-env-vars";
export * from "./normalize-path";
export * from "./ts-parse-file";
19 changes: 18 additions & 1 deletion packages/cloudflare/src/cli/templates/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@ const cloudflareContextALS = new AsyncLocalStorage<CloudflareContext>();
}
);

async function applyProjectEnvVars(mode = "production") {
if (process.env.__OPENNEXT_PROCESSED_ENV === "1") return;

// @ts-expect-error: resolved by wrangler build
const nextEnvVars = await import("./.env.mjs");

if (nextEnvVars[mode]) {
for (const key in nextEnvVars[mode]) {
process.env[key] = nextEnvVars[mode][key];
}
}

process.env.__OPENNEXT_PROCESSED_ENV = "1";
}

export default {
async fetch(request, env, ctx) {
return cloudflareContextALS.run({ env, ctx, cf: request.cf }, async () => {
Expand All @@ -34,6 +49,8 @@ export default {
},
});

await applyProjectEnvVars(env.NEXTJS_ENV ?? globalThis.process.env.NODE_ENV);

// The Middleware handler can return either a `Response` or a `Request`:
// - `Response`s should be returned early
// - `Request`s are handled by the Next server
Expand All @@ -46,4 +63,4 @@ export default {
return serverHandler(reqOrResp, env, ctx);
});
},
} as ExportedHandler<{ ASSETS: Fetcher }>;
} as ExportedHandler<{ ASSETS: Fetcher; NEXTJS_ENV?: string }>;
Loading
Loading