From ee1735adba0fcc06122de90665494e1a3d95f286 Mon Sep 17 00:00:00 2001 From: Tomas Zijdemans Date: Sat, 26 Sep 2026 13:14:46 +0200 Subject: [PATCH 1/2] BREAKING(dotenv): remove deprecated loaders --- _tools/check_mod_exports.ts | 1 - dotenv/deno.json | 1 - dotenv/load.ts | 32 ---- dotenv/load_test.ts | 54 ------ dotenv/mod.ts | 255 +---------------------------- dotenv/mod_test.ts | 249 ---------------------------- dotenv/testdata/.env | 1 - dotenv/testdata/.env.multiple | 2 - dotenv/testdata/.env.single.expand | 1 - dotenv/testdata/app_defaults.ts | 5 - dotenv/testdata/app_load.ts | 4 - dotenv/testdata/app_load_child.ts | 5 - dotenv/testdata/app_load_parent.ts | 4 - 13 files changed, 6 insertions(+), 608 deletions(-) delete mode 100644 dotenv/load.ts delete mode 100644 dotenv/load_test.ts delete mode 100644 dotenv/mod_test.ts delete mode 100644 dotenv/testdata/.env delete mode 100644 dotenv/testdata/.env.multiple delete mode 100644 dotenv/testdata/.env.single.expand delete mode 100644 dotenv/testdata/app_defaults.ts delete mode 100644 dotenv/testdata/app_load.ts delete mode 100644 dotenv/testdata/app_load_child.ts delete mode 100644 dotenv/testdata/app_load_parent.ts diff --git a/_tools/check_mod_exports.ts b/_tools/check_mod_exports.ts index e33f5f8ff963..80382a76c279 100644 --- a/_tools/check_mod_exports.ts +++ b/_tools/check_mod_exports.ts @@ -42,7 +42,6 @@ for (const modFilePath of MOD_FILE_PATHS) { maxDepth: 1, skip: [ /unstable/, - /dotenv(\/|\\)load\.ts$/, /front_matter(\/|\\)yaml\.ts$/, /front_matter(\/|\\)json\.ts$/, /front_matter(\/|\\)toml\.ts$/, diff --git a/dotenv/deno.json b/dotenv/deno.json index cb918eee727b..e3550f6e4fb3 100644 --- a/dotenv/deno.json +++ b/dotenv/deno.json @@ -3,7 +3,6 @@ "version": "0.225.8", "exports": { ".": "./mod.ts", - "./load": "./load.ts", "./parse": "./parse.ts", "./stringify": "./stringify.ts" } diff --git a/dotenv/load.ts b/dotenv/load.ts deleted file mode 100644 index da8596d2d47a..000000000000 --- a/dotenv/load.ts +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2018-2026 the Deno authors. MIT license. - -/** - * Loads environment variables from a `.env` file into the process environment - * as a side effect of importing this module. - * - * ```ts ignore - * import "@std/dotenv/load"; - * - * Deno.env.get("GREETING"); // "hello world" - * ``` - * - * @deprecated This will be removed in 0.227.0. Use the - * {@link https://docs.deno.com/runtime/reference/env_variables/ | --env-file} - * flag instead. See the - * {@link https://jsr.io/@std/dotenv | module documentation} for migration - * notes. - * - * @module - */ - -import { loadSync } from "./mod.ts"; - -if (!(Deno.readTextFileSync instanceof Function)) { - // Avoid errors that occur in deno deploy: https://github.com/denoland/std/issues/1957 - // deno-lint-ignore no-console - console.warn( - `Deno.readTextFileSync is not a function: No .env data was read.`, - ); -} else { - loadSync({ export: true }); -} diff --git a/dotenv/load_test.ts b/dotenv/load_test.ts deleted file mode 100644 index e0e3c36e5f74..000000000000 --- a/dotenv/load_test.ts +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2018-2026 the Deno authors. MIT license. -import { assertEquals } from "@std/assert"; -import * as path from "@std/path"; - -const moduleDir = path.dirname(path.fromFileUrl(import.meta.url)); -const testdataDir = path.resolve(moduleDir, "testdata"); - -Deno.test({ - name: "load()", - async fn() { - const command = new Deno.Command(Deno.execPath(), { - args: [ - "run", - "--allow-read", - "--allow-env", - "--no-lock", - path.join(testdataDir, "./app_load.ts"), - ], - clearEnv: true, - cwd: testdataDir, - }); - const { stdout } = await command.output(); - - const decoder = new TextDecoder(); - assertEquals( - decoder.decode(stdout).trim(), - "hello world", - ); - }, -}); - -Deno.test({ - name: "load() works as expected when the multiple files are imported", - async fn() { - const command = new Deno.Command(Deno.execPath(), { - args: [ - "run", - "--no-lock", - "--allow-read", - "--allow-env", - path.join(testdataDir, "./app_load_parent.ts"), - ], - clearEnv: true, - cwd: testdataDir, - }); - const { stdout } = await command.output(); - - const decoder = new TextDecoder(); - assertEquals( - decoder.decode(stdout).trim(), - "hello world", - ); - }, -}); diff --git a/dotenv/mod.ts b/dotenv/mod.ts index 30bd527d3487..9301462a0c51 100644 --- a/dotenv/mod.ts +++ b/dotenv/mod.ts @@ -15,8 +15,8 @@ * * ## Migrating from `load()` * - * {@linkcode load}, {@linkcode loadSync} and the `@std/dotenv/load` side-effect - * module are deprecated in favor of the runtime's + * `load()`, `loadSync()` and the `@std/dotenv/load` side-effect module have been + * removed. Use the runtime's * {@link https://docs.deno.com/runtime/reference/env_variables/ | --env-file} * flag, which Node.js and Bun also support: * @@ -25,8 +25,8 @@ * deno run --env-file=.env --env-file=.env.local app.ts * ``` * - * | Deprecated API | Replacement | - * | -------------- | ----------- | + * | Removed API | Replacement | + * | ----------- | ----------- | * | `import "@std/dotenv/load"` | `deno run --env-file app.ts` | * | `load({ export: true })` / `loadSync({ export: true })` | `--env-file` | * | `load({ envPath: "./.env_prod" })` | `--env-file=.env_prod` | @@ -34,7 +34,7 @@ * * Differences to be aware of: * - * - `load()` silently ignores a missing file. `--env-file` warns but continues, + * - `load()` silently ignored a missing file. `--env-file` warns but continues, * while the `parse()` replacement above throws. To treat the file as * optional: * @@ -49,7 +49,7 @@ * } * ``` * - * - `$VAR` inside a double-quoted value stays literal with `load()` but + * - `$VAR` inside a double-quoted value stayed literal with `load()` but * expands with `--env-file`. Use single quotes for values containing a * literal `$`. * - `${KEY:-default}` and nested defaults are not supported by `--env-file`. @@ -61,248 +61,5 @@ * @module */ -import { parse } from "./parse.ts"; - export * from "./stringify.ts"; export * from "./parse.ts"; - -/** - * Options for {@linkcode load} and {@linkcode loadSync}. - * - * @deprecated This will be removed in 0.227.0. Use the - * {@link https://docs.deno.com/runtime/reference/env_variables/ | --env-file} - * flag or {@linkcode parse} instead. See the - * {@link https://jsr.io/@std/dotenv | module documentation} for migration - * notes. - */ -export interface LoadOptions { - /** - * Optional path to `.env` file. To prevent the default value from being - * used, set to `null`. - * - * @default {"./.env"} - */ - envPath?: string | URL | null; - - /** - * Set to `true` to export all `.env` variables to the current processes - * environment. Variables are then accessible via `Deno.env.get()`. - * - * @default {false} - */ - export?: boolean; -} - -/** - * Works identically to {@linkcode load}, but synchronously. - * - * @example Usage - * ```ts ignore - * import { loadSync } from "@std/dotenv"; - * - * const conf = loadSync(); - * ``` - * - * @param options Options for loading the environment variables. - * @returns The parsed environment variables. - * - * @deprecated This will be removed in 0.227.0. Use the - * {@link https://docs.deno.com/runtime/reference/env_variables/ | --env-file} - * flag or {@linkcode parse} instead. See the - * {@link https://jsr.io/@std/dotenv | module documentation} for migration - * notes. - */ -export function loadSync( - options: LoadOptions = {}, -): Record { - const { - envPath = ".env", - export: _export = false, - } = options; - const conf = envPath ? parseFileSync(envPath) : {}; - - if (_export) { - for (const [key, value] of Object.entries(conf)) { - if (Deno.env.get(key) !== undefined) continue; - Deno.env.set(key, value); - } - } - - return conf; -} - -/** - * Load environment variables from a `.env` file. Loaded variables are accessible - * in a configuration object returned by the `load()` function, as well as optionally - * exporting them to the process environment using the `export` option. - * - * Inspired by the node modules {@linkcode https://github.com/motdotla/dotenv | dotenv} - * and {@linkcode https://github.com/motdotla/dotenv-expand | dotenv-expand}. - * - * Note: The key needs to match the pattern /^[a-zA-Z_][a-zA-Z0-9_]*$/. - * - * ## Basic usage - * ```sh - * # .env - * GREETING=hello world - * ``` - * - * Then import the environment variables using the `load` function. - * - * @example Basic usage - * ```ts ignore - * // app.ts - * import { load } from "@std/dotenv"; - * - * console.log(await load({ export: true })); // { GREETING: "hello world" } - * console.log(Deno.env.get("GREETING")); // hello world - * ``` - * - * Run this with `deno run --allow-read --allow-env app.ts`. - * - * .env files support blank lines, comments, multi-line values and more. - * See Parsing Rules below for more detail. - * - * ## Auto loading - * Import the `load.ts` module to auto-import from the `.env` file and into - * the process environment. - * - * @example Auto-loading - * ```ts ignore - * // app.ts - * import "@std/dotenv/load"; - * - * console.log(Deno.env.get("GREETING")); // hello world - * ``` - * - * Run this with `deno run --allow-read --allow-env app.ts`. - * - * ## Files - * Dotenv supports a number of different files, all of which are optional. - * File names and paths are configurable. - * - * |File|Purpose| - * |----|-------| - * |.env|primary file for storing key-value environment entries - * - * ## Configuration - * - * Loading environment files comes with a number of options passed into - * the `load()` function, all of which are optional. - * - * |Option|Default|Description - * |------|-------|----------- - * |envPath|./.env|Path and filename of the `.env` file. Use null to prevent the .env file from being loaded. - * |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 - * - * @example Using with options - * ```ts ignore - * import { load } from "@std/dotenv"; - * - * const conf = await load({ - * envPath: "./.env_prod", // Uses .env_prod instead of .env - * export: true, // Exports all variables to the environment - * }); - * ``` - * - * ## Permissions - * - * At a minimum, loading the `.env` related files requires the `--allow-read` permission. Additionally, if - * you access the process environment, either through exporting your configuration or expanding variables - * in your `.env` file, you will need the `--allow-env` permission. E.g. - * - * ```sh - * deno run --allow-read=.env --allow-env=ENV1,ENV2 app.ts - * ``` - * - * ## Parsing Rules - * - * The parsing engine currently supports the following rules: - * - * - Variables that already exist in the environment are not overridden with - * `export: true` - * - `BASIC=basic` becomes `{ BASIC: "basic" }` - * - empty lines are skipped - * - lines beginning with `#` are treated as comments - * - empty values become empty strings (`EMPTY=` becomes `{ EMPTY: "" }`) - * - single and double quoted values are escaped (`SINGLE_QUOTE='quoted'` becomes - * `{ SINGLE_QUOTE: "quoted" }`) - * - new lines are expanded in double quoted values (`MULTILINE="new\nline"` - * becomes - * - * ``` - * { MULTILINE: "new\nline" } - * ``` - * - * - inner quotes are maintained (think JSON) (`JSON={"foo": "bar"}` becomes - * `{ JSON: "{\"foo\": \"bar\"}" }`) - * - whitespace is removed from both ends of unquoted values (see more on - * {@linkcode https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim | trim}) - * (`FOO= some value` becomes `{ FOO: "some value" }`) - * - whitespace is preserved on both ends of quoted values (`FOO=" some value "` - * becomes `{ FOO: " some value " }`) - * - dollar sign with an environment key in or without curly braces in unquoted - * values will expand the environment key (`KEY=$KEY` or `KEY=${KEY}` becomes - * `{ KEY: "" }`) - * - escaped dollar sign with an environment key in unquoted values will escape the - * environment key rather than expand (`KEY=\$KEY` becomes `{ KEY: "\\$KEY" }`) - * - colon and a minus sign with a default value(which can also be another expand - * value) in expanding construction in unquoted values will first attempt to - * expand the environment key. If it’s not found, then it will return the default - * value (`KEY=${KEY:-default}` If KEY exists it becomes - * `{ KEY: "" }` If not, then it becomes - * `{ KEY: "default" }`. Also there is possible to do this case - * `KEY=${NO_SUCH_KEY:-${EXISTING_KEY:-default}}` which becomes - * `{ KEY: "" }`) - * - * @param options The options - * @returns The parsed environment variables - * - * @deprecated This will be removed in 0.227.0. Use the - * {@link https://docs.deno.com/runtime/reference/env_variables/ | --env-file} - * flag or {@linkcode parse} instead. See the - * {@link https://jsr.io/@std/dotenv | module documentation} for migration - * notes. - */ -export async function load( - options: LoadOptions = {}, -): Promise> { - const { - envPath = ".env", - export: _export = false, - } = options; - const conf = envPath ? await parseFile(envPath) : {}; - - if (_export) { - for (const [key, value] of Object.entries(conf)) { - if (Deno.env.get(key) !== undefined) continue; - Deno.env.set(key, value); - } - } - - return conf; -} - -function parseFileSync( - filepath: string | URL, -): Record { - try { - return parse(Deno.readTextFileSync(filepath)); - } catch (e) { - if (e instanceof Deno.errors.NotFound) return {}; - throw e; - } -} - -async function parseFile( - filepath: string | URL, -): Promise> { - try { - return parse(await Deno.readTextFile(filepath)); - } catch (e) { - if (e instanceof Deno.errors.NotFound) return {}; - throw e; - } -} diff --git a/dotenv/mod_test.ts b/dotenv/mod_test.ts deleted file mode 100644 index bc385006e9ea..000000000000 --- a/dotenv/mod_test.ts +++ /dev/null @@ -1,249 +0,0 @@ -// Copyright 2018-2026 the Deno authors. MIT license. - -import { - assert, - assertEquals, - assertStrictEquals, - assertThrows, -} from "@std/assert"; -import { load, type LoadOptions, loadSync } from "./mod.ts"; -import * as path from "@std/path"; - -const moduleDir = path.dirname(path.fromFileUrl(import.meta.url)); -const testdataDir = path.resolve(moduleDir, "testdata"); - -const testOptions = Object.freeze({ - envPath: path.join(testdataDir, ".env"), -}); - -Deno.test("load() handles non-existent .env files", async () => { - // .env doesn't exist in the current directory - assertEquals({}, await load()); - assertEquals({}, loadSync()); - - const loadOptions = { - envPath: "some.nonexistent.env", - }; - assertEquals({}, await load(loadOptions)); - assertEquals({}, loadSync(loadOptions)); -}); - -Deno.test("load() handles URL as path for .env files", async () => { - const conf = loadSync({ - envPath: new URL( - path.toFileUrl(path.join(testdataDir, ".env")), - import.meta.url, - ), - }); - assertEquals(conf.GREETING, "hello world", "loaded from .env"); - - const asyncConf = await load({ - envPath: new URL( - path.toFileUrl(path.join(testdataDir, ".env")), - import.meta.url, - ), - }); - assertEquals(asyncConf.GREETING, "hello world", "loaded from .env"); -}); - -Deno.test("load() handles comprised .env and .env.defaults", async () => { - const conf = loadSync(testOptions); - assertEquals(conf.GREETING, "hello world", "loaded from .env"); - - const asyncConf = await load(testOptions); - assertEquals(asyncConf.GREETING, "hello world", "loaded from .env"); -}); - -Deno.test("load() handles exported entries accessibility in Deno.env", async () => { - assert(Deno.env.get("GREETING") === undefined, "GREETING is not set"); - assert(Deno.env.get("DEFAULT1") === undefined, "DEFAULT1 is not set"); - - loadSync({ ...testOptions, export: true }); - validateExport(); - - await load({ ...testOptions, export: true }); - validateExport(); -}); - -function validateExport(): void { - try { - assertEquals( - Deno.env.get("GREETING"), - "hello world", - "exported from .env -> Deno.env", - ); - } finally { - Deno.env.delete("GREETING"); - } -} - -Deno.test("load() process does not overridde env vars by .env values", async () => { - Deno.env.set("GREETING", "Do not override!"); - assert(Deno.env.get("DEFAULT1") === undefined, "DEFAULT1 is not set"); - - validateNotOverridden(loadSync({ ...testOptions, export: true })); - validateNotOverridden(await load({ ...testOptions, export: true })); -}); - -function validateNotOverridden(conf: Record): void { - try { - assertEquals(conf.GREETING, "hello world", "value from .env"); - assertEquals( - Deno.env.get("GREETING"), - "Do not override!", - "not exported from .env -> Deno.env", - ); - } finally { - Deno.env.delete("DEFAULT1"); - } -} - -Deno.test("load() loads .env successfully from default file names/paths", async () => { - const command = new Deno.Command(Deno.execPath(), { - args: [ - "run", - "--no-lock", - "--allow-read", - "--allow-env", - path.join(testdataDir, "./app_defaults.ts"), - ], - cwd: testdataDir, - }); - const { stdout } = await command.output(); - - const decoder = new TextDecoder(); - const conf = JSON.parse(decoder.decode(stdout).trim()); - - assertEquals(conf.GREETING, "hello world", "fetches .env by default"); -}); - -Deno.test("load() expands empty values from process env expand as empty value", async () => { - try { - Deno.env.set("EMPTY", ""); - - // .env.single.expand contains one key which expands to the "EMPTY" process env var - const loadOptions = { - envPath: path.join(testdataDir, "./.env.single.expand"), - }; - - const conf = loadSync(loadOptions); - assertEquals( - conf.EXPECT_EMPTY, - "", - "empty value expanded from process env", - ); - - const asyncConf = await load(loadOptions); - assertEquals( - asyncConf.EXPECT_EMPTY, - "", - "empty value expanded from process env", - ); - } finally { - Deno.env.delete("EMPTY"); - } -}); - -Deno.test( - "loadSync() checks that --allow-env is not required if no process env vars are expanded upon", - { - permissions: { - read: true, - }, - }, - () => { - // note lack of --allow-env permission - const conf = loadSync(testOptions); - assertEquals(conf.GREETING, "hello world"); - }, -); - -Deno.test( - "loadSync() checks that --allow-env is required when process env vars are expanded upon", - { - permissions: { - read: true, - }, - }, - () => { - // ./app_permission_test.ts loads a .env with one key which expands a process env var - // note lack of --allow-env permission - const loadOptions = { - envPath: path.join(testdataDir, "./.env.single.expand"), - }; - assertThrows( - () => loadSync(loadOptions), - // deno-lint-ignore no-explicit-any - (Deno as any).errors.NotCapable ?? Deno.errors.PermissionDenied, - `Requires env access to "EMPTY", run again with the --allow-env flag`, - ); - }, -); - -Deno.test( - "loadSync() checks that --allow-env restricted access works when process env vars are expanded upon", - { - permissions: { - read: true, - env: ["EMPTY"], - }, - }, - () => { - try { - Deno.env.set("EMPTY", ""); - - const loadOptions = { - envPath: path.join(testdataDir, "./.env.single.expand"), - }; - const conf = loadSync(loadOptions); - assertEquals( - conf.EXPECT_EMPTY, - "", - "empty value expanded from process env", - ); - } finally { - Deno.env.delete("EMPTY"); - } - }, -); - -// TODO(cknight): test permissions - -Deno.test( - "loadSync() prevents file system reads of default path parameter values by using explicit null", - { - permissions: { - env: ["GREETING", "DO_NOT_OVERRIDE"], - read: [path.join(testdataDir, "./.env.multiple")], - }, - }, - async (t) => { - const optsNoPaths = { - envPath: null, - } satisfies LoadOptions; - - const optsEnvPath = { - envPath: path.join(testdataDir, "./.env.multiple"), - } satisfies LoadOptions; - - const optsOnlyEnvPath = { - ...optsEnvPath, - } satisfies LoadOptions; - - const assertEnv = (env: Record): void => { - assertStrictEquals(Object.keys(env).length, 2); - assertStrictEquals(env["GREETING"], "hello world"); - assertStrictEquals(env["DO_NOT_OVERRIDE"], "overridden"); - }; - - await t.step("load", async () => { - assertStrictEquals(Object.keys(await load(optsNoPaths)).length, 0); - assertEnv(await load(optsOnlyEnvPath)); - }); - - await t.step("loadSync", () => { - assertStrictEquals(Object.keys(loadSync(optsNoPaths)).length, 0); - assertEnv(loadSync(optsOnlyEnvPath)); - }); - }, -); diff --git a/dotenv/testdata/.env b/dotenv/testdata/.env deleted file mode 100644 index 1466fff39f08..000000000000 --- a/dotenv/testdata/.env +++ /dev/null @@ -1 +0,0 @@ -GREETING=hello world \ No newline at end of file diff --git a/dotenv/testdata/.env.multiple b/dotenv/testdata/.env.multiple deleted file mode 100644 index 4b6242a920af..000000000000 --- a/dotenv/testdata/.env.multiple +++ /dev/null @@ -1,2 +0,0 @@ -GREETING=hello world -DO_NOT_OVERRIDE=overridden \ No newline at end of file diff --git a/dotenv/testdata/.env.single.expand b/dotenv/testdata/.env.single.expand deleted file mode 100644 index 3bee96b62cf2..000000000000 --- a/dotenv/testdata/.env.single.expand +++ /dev/null @@ -1 +0,0 @@ -EXPECT_EMPTY=${EMPTY} \ No newline at end of file diff --git a/dotenv/testdata/app_defaults.ts b/dotenv/testdata/app_defaults.ts deleted file mode 100644 index fb7c88c4e1fb..000000000000 --- a/dotenv/testdata/app_defaults.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { load } from "../mod.ts"; -const conf = await load(); - -// deno-lint-ignore no-console -console.log(JSON.stringify(conf, null, 2)); diff --git a/dotenv/testdata/app_load.ts b/dotenv/testdata/app_load.ts deleted file mode 100644 index d04581133e04..000000000000 --- a/dotenv/testdata/app_load.ts +++ /dev/null @@ -1,4 +0,0 @@ -import "../load.ts"; - -// deno-lint-ignore no-console -console.log(Deno.env.get("GREETING")); diff --git a/dotenv/testdata/app_load_child.ts b/dotenv/testdata/app_load_child.ts deleted file mode 100644 index c9e9316dd376..000000000000 --- a/dotenv/testdata/app_load_child.ts +++ /dev/null @@ -1,5 +0,0 @@ -// test file for https://github.com/denoland/std/issues/1957 -// This file is imported from ./app_load_parent.ts - -// deno-lint-ignore no-console -console.log(Deno.env.get("GREETING")); diff --git a/dotenv/testdata/app_load_parent.ts b/dotenv/testdata/app_load_parent.ts deleted file mode 100644 index 1ac269ee1315..000000000000 --- a/dotenv/testdata/app_load_parent.ts +++ /dev/null @@ -1,4 +0,0 @@ -// test file for https://github.com/denoland/std/issues/1957 - -import "../load.ts"; -import "./app_load_child.ts"; From b6f83e43580d3aee7b6d0791960d603af5fc22ff Mon Sep 17 00:00:00 2001 From: Tomas Zijdemans Date: Sat, 26 Sep 2026 13:28:26 +0200 Subject: [PATCH 2/2] test(dotenv): cover parse() process env expansion These cases previously ran through loadSync() in mod_test.ts. --- dotenv/parse_test.ts | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/dotenv/parse_test.ts b/dotenv/parse_test.ts index e825b9728284..9720013fff6b 100644 --- a/dotenv/parse_test.ts +++ b/dotenv/parse_test.ts @@ -1,6 +1,6 @@ // Copyright 2018-2026 the Deno authors. MIT license. -import { assertEquals } from "@std/assert"; +import { assertEquals, assertThrows } from "@std/assert"; import { parse } from "./parse.ts"; import * as path from "@std/path"; import { assertSpyCall, spy } from "@std/testing/mock"; @@ -277,6 +277,40 @@ Deno.test("parse() expands variables", () => { ); }); +Deno.test( + "parse() expands an empty process env var as an empty value", + { permissions: { env: ["EMPTY"] } }, + () => { + try { + Deno.env.set("EMPTY", ""); + assertEquals(parse("EXPECT_EMPTY=${EMPTY}"), { EXPECT_EMPTY: "" }); + } finally { + Deno.env.delete("EMPTY"); + } + }, +); + +Deno.test( + "parse() does not require env access when nothing is expanded from the process env", + { permissions: "none" }, + () => { + assertEquals(parse("GREETING=hello world"), { GREETING: "hello world" }); + }, +); + +Deno.test( + "parse() requires env access when expanding from the process env", + { permissions: "none" }, + () => { + assertThrows( + () => parse("EXPECT_EMPTY=${EMPTY}"), + // deno-lint-ignore no-explicit-any + (Deno as any).errors.NotCapable ?? Deno.errors.PermissionDenied, + `Requires env access to "EMPTY", run again with the --allow-env flag`, + ); + }, +); + Deno.test("parse() result is not affected by extended Object.prototype", () => { // deno-lint-ignore no-explicit-any (Object.prototype as any).foo = 1;