Skip to content

compileEnvFiles produces duplicate exports on Windows when called twice #1274

Description

@chdimosthenis

Summary

compileEnvFiles in dist/cli/build/open-next/compile-env-files.js uses fs.appendFileSync to write .open-next/cloudflare/next-env.mjs. The function is not idempotent — if it is ever invoked twice against the same outputDir, the file ends up with duplicate export const production = {…}, export const development = {…}, export const test = {…} lines.

Wrangler then rejects the bundle at deploy time:

X [ERROR] Multiple exports with the same name "production"
    .open-next/cloudflare/next-env.mjs:4:13:
      4 │ export const production = {…}
                       ~~~~~~~~~~
  The symbol "production" was originally declared here:
    .open-next/cloudflare/next-env.mjs:1:13:
      1 │ export const production = {…}
                       ~~~~~~~~~~

Source

dist/cli/build/open-next/compile-env-files.js (v1.15.1, line 10):

["production", "development", "test"].forEach((mode) =>
  fs.appendFileSync(
    path.join(envDir, `next-env.mjs`),
    `export const ${mode} = ${JSON.stringify(extractProjectEnvVars(mode, buildOpts))};\n`
  )
);

Reproduction

On Windows 11 / Node 22 / @opennextjs/cloudflare@1.15.1, the duplicated-file state reproduced in this project across several npm run cf:build runs — the file consistently ended up with 6 lines (each export written twice) rather than 3. I could not isolate a deterministic call path that invokes compileEnvFiles twice in a single build, but the symptom appears reliably and is consistent with a second invocation against a non-empty file (since appendFileSync always appends and never truncates).

Suggested fix

Make the writer idempotent. Either:

  1. Truncate first (smallest diff):
    const targetPath = path.join(envDir, "next-env.mjs");
    fs.writeFileSync(targetPath, "");
    ["production", "development", "test"].forEach((mode) =>
      fs.appendFileSync(targetPath, `export const ${mode} = …\n`)
    );
  2. Single write: build the whole content in memory and writeFileSync once.

Either makes the function safe to call multiple times without producing duplicate exports.

Workaround

I'm currently pinning a truncate-then-append patch via patch-package:

- ["production", "development", "test"].forEach((mode) => fs.appendFileSync(path.join(envDir, `next-env.mjs`), …));
+ const targetPath = path.join(envDir, "next-env.mjs");
+ fs.writeFileSync(targetPath, "");
+ ["production", "development", "test"].forEach((mode) => fs.appendFileSync(targetPath, …));

Happy to open a PR if helpful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions