Repository: denoland/std (TypeScript)

Issue:
proposal: remove `defaultPath` from `@std/dotenv/load`
[This option](https://jsr.io/@std/dotenv/doc/~/LoadOptions.defaultsPath) uses a `.env` file to define the default values of in a `.env` file, rather than in-code. It also doesn't align with Deno's philosophy of providing control in an explicit manner, instead of magically.

For these reasons, and similarly to #5449, I suggest we remove this option.

Related:
* #4019
* #4578


Discussion:

Patch:
diff --git a/dotenv/mod.ts b/dotenv/mod.ts
index bf232f6ef3eb..0acd8877c45f 100644
--- a/dotenv/mod.ts
+++ b/dotenv/mod.ts
@@ -42,21 +42,6 @@ export interface LoadOptions {
    * @default {false}
    */
   export?: boolean;
-
-  /**
-   * Optional path to `.env.defaults` file which is used to define default
-   * (fallback) values. To prevent the default value from being used,
-   * set to `null`.
-   *
-   * ```sh
-   * # .env.defaults
-   * # Will not be set if GREETING is set in base .env file
-   * GREETING="a secret to everybody"
-   * ```
-   *
-   * @default {"./.env.defaults"}
-   */
-  defaultsPath?: string | null;
 }
 
 /**
@@ -77,20 +62,10 @@ export function loadSync(
 ): Record<string, string> {
   const {
     envPath = ".env",
-    defaultsPath = ".env.defaults",
     export: _export = false,
   } = options;
   const conf = envPath ? parseFileSync(envPath) : {};
 
-  if (defaultsPath) {
-    const confDefaults = parseFileSync(defaultsPath);
-    for (const [key, value] of Object.entries(confDefaults)) {
-      if (!(key in conf)) {
-        conf[key] = value;
-      }
-    }
-  }
-
   if (_export) {
     for (const [key, value] of Object.entries(conf)) {
       if (Deno.env.get(key) !== undefined) continue;
@@ -152,28 +127,6 @@ export function loadSync(
  * |File|Purpose|
  * |----|-------|
  * |.env|primary file for storing key-value environment entries
- * |.env.defaults|specify default values for env variables to be used when there is no entry in the `.env` file
- *
- * ### Defaults
- *
- * This file is used to provide a list of default environment variables
- * which will be used if there is no overriding variable in the `.env`
- * file.
- *
- * ```sh
- * # .env.defaults
- * KEY_1=DEFAULT_VALUE
- * KEY_2=ANOTHER_DEFAULT_VALUE
- * ```
- * ```sh
- * # .env
- * KEY_1=ABCD
- * ```
- * The environment variables set after dotenv loads are:
- * ```sh
- * KEY_1=ABCD
- * KEY_2=ANOTHER_DEFAULT_VALUE
- * ```
  *
  * ## Configuration
  *
@@ -183,8 +136,7 @@ export function loadSync(
  * |Option|Default|Description
  * |------|-------|-----------
  * |envPath|./.env|Path and filename of the `.env` file.  Use null to prevent the .env file from being loaded.
- * |defaultsPath|./.env.defaults|Path and filename of the `.env.defaults` file. Use null to prevent the .env.defaults file from being loaded.
- * |export|false|When true, this will export all environment variables in the `.env` and `.env.default` files to the process environment (e.g. for use by `Deno.env.get()`) but only if they are not already set.  If a variable is already in the process, the `.env` value is ignored.
+ * |export|false|When true, this will export all environment variables in the `.env` file to the process environment (e.g. for use by `Deno.env.get()`) but only if they are not already set.  If a variable is already in the process, the `.env` value is ignored.
  *
  * ### Example configuration
  *
@@ -205,7 +157,7 @@ export function loadSync(
  * in your `.env` file, you will need the `--allow-env` permission.  E.g.
  *
  * ```sh
- * deno run --allow-read=.env,.env.defaults --allow-env=ENV1,ENV2 app.ts
+ * deno run --allow-read=.env --allow-env=ENV1,ENV2 app.ts
  * ```
  *
  * ## Parsing Rules
@@ -256,20 +208,10 @@ export async function load(
 ): Promise<Record<string, string>> {
   const {
     envPath = ".env",
-    defaultsPath = ".env.defaults",
     export: _export = false,
   } = options;
   const conf = envPath ? await parseFile(envPath) : {};
 
-  if (defaultsPath) {
-    const confDefaults = await parseFile(defaultsPath);
-    for (const [key, value] of Object.entries(confDefaults)) {
-      if (!(key in conf)) {
-        conf[key] = value;
-      }
-    }
-  }
-
   if (_export) {
     for (const [key, value] of Object.entries(conf)) {
       if (Deno.env.get(key) !== undefined) continue;


Test patch:
diff --git a/dotenv/mod_test.ts b/dotenv/mod_test.ts
index dd00d78da0d3..f5575cbe86d6 100644
--- a/dotenv/mod_test.ts
+++ b/dotenv/mod_test.ts
@@ -3,7 +3,6 @@
 import {
   assert,
   assertEquals,
-  assertRejects,
   assertStrictEquals,
   assertThrows,
 } from "@std/assert";
@@ -15,42 +14,26 @@ const testdataDir = path.resolve(moduleDir, "testdata");
 
 const testOptions = Object.freeze({
   envPath: path.join(testdataDir, ".env"),
-  defaultsPath: path.join(testdataDir, ".env.defaults"),
 });
 
 Deno.test("load() handles non-existent .env files", async () => {
-  //n.b. neither .env nor .env.default exist in the current directory
+  // .env doesn't exist in the current directory
   assertEquals({}, await load());
   assertEquals({}, loadSync());
 
   const loadOptions = {
     envPath: "some.nonexistent.env",
-    defaultsPath: "some.nonexistent.defaults",
   };
   assertEquals({}, await load(loadOptions));
   assertEquals({}, loadSync(loadOptions));
 });
 
-Deno.test("load() handles build from .env.default only", async () => {
-  const conf = loadSync({
-    defaultsPath: path.join(testdataDir, ".env.defaults"),
-  });
-  assertEquals(conf.DEFAULT1, "Some Default", "loaded from .env.default");
-
-  const asyncConf = await load({
-    defaultsPath: path.join(testdataDir, ".env.defaults"),
-  });
-  assertEquals(asyncConf.DEFAULT1, "Some Default", "loaded from .env.default");
-});
-
 Deno.test("load() handles comprised .env and .env.defaults", async () => {
   const conf = loadSync(testOptions);
   assertEquals(conf.GREETING, "hello world", "loaded from .env");
-  assertEquals(conf.DEFAULT1, "Some Default", "loaded from .env.default");
 
   const asyncConf = await load(testOptions);
   assertEquals(asyncConf.GREETING, "hello world", "loaded from .env");
-  assertEquals(asyncConf.DEFAULT1, "Some Default", "loaded from .env.default");
 });
 
 Deno.test("load() handles exported entries accessibility in Deno.env", async () => {
@@ -71,14 +54,8 @@ function validateExport(): void {
       "hello world",
       "exported from .env -> Deno.env",
     );
-    assertEquals(
-      Deno.env.get("DEFAULT1"),
-      "Some Default",
-      "exported from .env.default -> Deno.env",
-    );
   } finally {
     Deno.env.delete("GREETING");
-    Deno.env.delete("DEFAULT1");
   }
 }
 
@@ -98,17 +75,12 @@ function validateNotOverridden(conf: Record<string, string>): void {
       "Do not override!",
       "not exported from .env -> Deno.env",
     );
-    assertEquals(
-      Deno.env.get("DEFAULT1"),
-      "Some Default",
-      "exported from .env.default -> Deno.env",
-    );
   } finally {
     Deno.env.delete("DEFAULT1");
   }
 }
 
-Deno.test("load() loads .env and .env.defaults successfully from default file names/paths", async () => {
+Deno.test("load() loads .env successfully from default file names/paths", async () => {
   const command = new Deno.Command(Deno.execPath(), {
     args: [
       "run",
@@ -125,7 +97,6 @@ Deno.test("load() loads .env and .env.defaults successfully from default file na
   const conf = JSON.parse(decoder.decode(stdout).trim());
 
   assertEquals(conf.GREETING, "hello world", "fetches .env by default");
-  assertEquals(conf.DEFAULT1, "Some Default", "default value loaded");
 });
 
 Deno.test("load() expands empty values from process env expand as empty value", async () => {
@@ -166,7 +137,6 @@ Deno.test(
     // note lack of --allow-env permission
     const conf = loadSync(testOptions);
     assertEquals(conf.GREETING, "hello world");
-    assertEquals(conf.DEFAULT1, "Some Default");
   },
 );
 
@@ -182,7 +152,6 @@ Deno.test(
     // note lack of --allow-env permission
     const loadOptions = {
       envPath: path.join(testdataDir, "./.env.single.expand"),
-      defaultsPath: null,
     };
     assertThrows(
       () => loadSync(loadOptions),
@@ -206,7 +175,6 @@ Deno.test(
 
       const loadOptions = {
         envPath: path.join(testdataDir, "./.env.single.expand"),
-        defaultsPath: null,
       };
       const conf = loadSync(loadOptions);
       assertEquals(
@@ -232,7 +200,6 @@ Deno.test(
   },
   async (t) => {
     const optsNoPaths = {
-      defaultsPath: null,
       envPath: null,
     } satisfies LoadOptions;
 
@@ -242,7 +209,6 @@ Deno.test(
 
     const optsOnlyEnvPath = {
       ...optsEnvPath,
-      defaultsPath: null,
     } satisfies LoadOptions;
 
     const assertEnv = (env: Record<string, string>): void => {
@@ -254,23 +220,11 @@ Deno.test(
     await t.step("load", async () => {
       assertStrictEquals(Object.keys(await load(optsNoPaths)).length, 0);
       assertEnv(await load(optsOnlyEnvPath));
-
-      await assertRejects(
-        () => load(optsEnvPath),
-        Deno.errors.PermissionDenied,
-        `Requires read access to ".env.defaults"`,
-      );
     });
 
     await t.step("loadSync", () => {
       assertStrictEquals(Object.keys(loadSync(optsNoPaths)).length, 0);
       assertEnv(loadSync(optsOnlyEnvPath));
-
-      assertThrows(
-        () => loadSync(optsEnvPath),
-        Deno.errors.PermissionDenied,
-        `Requires read access to ".env.defaults"`,
-      );
     });
   },
 );
diff --git a/dotenv/testdata/.env.defaults b/dotenv/testdata/.env.defaults
deleted file mode 100644
index 761b076a5233..000000000000
--- a/dotenv/testdata/.env.defaults
+++ /dev/null
@@ -1,1 +0,0 @@
-DEFAULT1=Some Default
diff --git a/dotenv/testdata/.env.example b/dotenv/testdata/.env.example
deleted file mode 100644
index 02c8a7f9bf44..000000000000
--- a/dotenv/testdata/.env.example
+++ /dev/null
@@ -1,1 +0,0 @@
-GREETING=
diff --git a/dotenv/testdata/.env.example.test b/dotenv/testdata/.env.example.test
deleted file mode 100644
index 02c8a7f9bf44..000000000000
--- a/dotenv/testdata/.env.example.test
+++ /dev/null
@@ -1,1 +0,0 @@
-GREETING=
diff --git a/dotenv/testdata/.env.example2.test b/dotenv/testdata/.env.example2.test
deleted file mode 100644
index 9cb839117f8c..000000000000
--- a/dotenv/testdata/.env.example2.test
+++ /dev/null
@@ -1,2 +0,0 @@
-GREETING=
-ANOTHER=
diff --git a/dotenv/testdata/.env.example3.test b/dotenv/testdata/.env.example3.test
deleted file mode 100644
index 2f0f65e0ba57..000000000000
--- a/dotenv/testdata/.env.example3.test
+++ /dev/null
@@ -1,1 +0,0 @@
-DEFAULT1=
diff --git a/dotenv/testdata/.env.required.empty.test b/dotenv/testdata/.env.required.empty.test
deleted file mode 100644
index da08c7715d40..000000000000
--- a/dotenv/testdata/.env.required.empty.test
+++ /dev/null
@@ -1,2 +0,0 @@
-GREETING=hello world
-ANOTHER=