From 71931c8d32c9b54714d3b9b696f225e1d09c29f2 Mon Sep 17 00:00:00 2001 From: MarkXian Date: Fri, 24 Jul 2026 17:31:04 +0800 Subject: [PATCH 1/5] fix: guard import.meta env lookup --- packages/effect/src/ConfigProvider.ts | 14 ++++++++++---- packages/effect/test/ConfigProvider.test.ts | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/packages/effect/src/ConfigProvider.ts b/packages/effect/src/ConfigProvider.ts index c76631e7091..697319c1c08 100644 --- a/packages/effect/src/ConfigProvider.ts +++ b/packages/effect/src/ConfigProvider.ts @@ -890,10 +890,8 @@ export function fromEnv(options?: { readonly preserveEmptyStrings?: boolean | undefined }): ConfigProvider { const env: Record = options?.env ?? { - ...(globalThis as { - readonly process?: { readonly env?: Record } - }).process?.env, - ...(import.meta as any)?.env + ...globalThis?.process?.env, + ...getImportMetaEnv() } const preserveEmptyStrings = options?.preserveEmptyStrings === true const trie = buildEnvTrie(env) @@ -901,6 +899,14 @@ export function fromEnv(options?: { return make((path) => Effect.succeed(nodeAtEnv(trie, env, path, preserveEmptyStrings))) } +function getImportMetaEnv(): Record | undefined { + try { + return (import.meta as any)?.env + } catch { + return undefined + } +} + type EnvTrieNode = { children?: Record } diff --git a/packages/effect/test/ConfigProvider.test.ts b/packages/effect/test/ConfigProvider.test.ts index 92c13a0eeab..a98949b656f 100644 --- a/packages/effect/test/ConfigProvider.test.ts +++ b/packages/effect/test/ConfigProvider.test.ts @@ -289,6 +289,22 @@ describe("ConfigProvider", () => { }) describe("fromEnv", () => { + it("uses the default environment when no env is provided", async () => { + const key = "EFFECT_CONFIG_PROVIDER_TEST_DEFAULT_ENV" + const previous = process.env[key] + process.env[key] = "value1" + try { + const provider = ConfigProvider.fromEnv() + await assertSuccess(provider, [key], ConfigProvider.makeValue("value1")) + } finally { + if (previous === undefined) { + delete process.env[key] + } else { + process.env[key] = previous + } + } + }) + it("env without an underscore", async () => { const env = { A: "value1" } const provider = ConfigProvider.fromEnv({ env }) From 18661297cb85fa7c5fa6465d12c3474a5745663f Mon Sep 17 00:00:00 2001 From: MarkXian Date: Fri, 24 Jul 2026 20:57:12 +0800 Subject: [PATCH 2/5] fix: remove import.meta from default env provider --- packages/effect/src/ConfigProvider.ts | 15 +++---------- packages/effect/test/ConfigProvider.test.ts | 25 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/packages/effect/src/ConfigProvider.ts b/packages/effect/src/ConfigProvider.ts index 697319c1c08..962d4878b0d 100644 --- a/packages/effect/src/ConfigProvider.ts +++ b/packages/effect/src/ConfigProvider.ts @@ -850,8 +850,8 @@ function emptyStringAsMissing(value: string | undefined, preserveEmptyStrings: b * purely numeric names, the node is reported as an `Array`; otherwise as a * `Record`. * - * The default environment merges `process.env` and `import.meta.env` (when - * available). Override by passing `{ env: { ... } }`. + * The default environment reads `process.env` when available. For runtimes that + * expose environment variables elsewhere, pass `{ env: { ... } }`. * * Literal empty strings are treated as missing values when loaded as values by * default. Pass `{ preserveEmptyStrings: true }` to keep empty strings as @@ -890,8 +890,7 @@ export function fromEnv(options?: { readonly preserveEmptyStrings?: boolean | undefined }): ConfigProvider { const env: Record = options?.env ?? { - ...globalThis?.process?.env, - ...getImportMetaEnv() + ...globalThis?.process?.env } const preserveEmptyStrings = options?.preserveEmptyStrings === true const trie = buildEnvTrie(env) @@ -899,14 +898,6 @@ export function fromEnv(options?: { return make((path) => Effect.succeed(nodeAtEnv(trie, env, path, preserveEmptyStrings))) } -function getImportMetaEnv(): Record | undefined { - try { - return (import.meta as any)?.env - } catch { - return undefined - } -} - type EnvTrieNode = { children?: Record } diff --git a/packages/effect/test/ConfigProvider.test.ts b/packages/effect/test/ConfigProvider.test.ts index a98949b656f..e83957cea32 100644 --- a/packages/effect/test/ConfigProvider.test.ts +++ b/packages/effect/test/ConfigProvider.test.ts @@ -1,6 +1,7 @@ import { describe, it } from "@effect/vitest" import { deepStrictEqual } from "@effect/vitest/utils" import { ConfigProvider, Effect, FileSystem, Layer, Path, PlatformError, Result } from "effect" +import * as Fs from "node:fs" const notFound = (method: string): PlatformError.PlatformError => PlatformError.systemError({ @@ -305,6 +306,30 @@ describe("ConfigProvider", () => { } }) + it("uses an explicit env over the default environment", async () => { + const key = "EFFECT_CONFIG_PROVIDER_TEST_DEFAULT_ENV" + const previous = process.env[key] + process.env[key] = "default" + try { + const provider = ConfigProvider.fromEnv({ env: { [key]: "explicit" } }) + await assertSuccess(provider, [key], ConfigProvider.makeValue("explicit")) + } finally { + if (previous === undefined) { + delete process.env[key] + } else { + process.env[key] = previous + } + } + }) + + it("does not reference import.meta in the common ConfigProvider module", () => { + const sourcePath = Fs.existsSync("src/ConfigProvider.ts") + ? "src/ConfigProvider.ts" + : "packages/effect/src/ConfigProvider.ts" + const source = Fs.readFileSync(sourcePath, "utf8") + deepStrictEqual(source.includes("import.meta"), false) + }) + it("env without an underscore", async () => { const env = { A: "value1" } const provider = ConfigProvider.fromEnv({ env }) From 731edf16493f6f79c9dd29d9d3a41fbadec9ed63 Mon Sep 17 00:00:00 2001 From: MarkXian Date: Fri, 24 Jul 2026 21:05:16 +0800 Subject: [PATCH 3/5] chore: add config provider changeset --- .changeset/configprovider-import-meta-env.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/configprovider-import-meta-env.md diff --git a/.changeset/configprovider-import-meta-env.md b/.changeset/configprovider-import-meta-env.md new file mode 100644 index 00000000000..bd02b360dbb --- /dev/null +++ b/.changeset/configprovider-import-meta-env.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove the default `import.meta.env` lookup from `ConfigProvider.fromEnv`, fixing module analysis failures in runtimes that do not support `import.meta`, closes #6358. From 8153d3bb94a2eb670ae8b84d6b70b6955e859f5a Mon Sep 17 00:00:00 2001 From: MarkXian Date: Mon, 27 Jul 2026 11:05:24 +0800 Subject: [PATCH 4/5] docs: update ConfigProvider fromEnv default env docs --- packages/effect/CONFIG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/effect/CONFIG.md b/packages/effect/CONFIG.md index 8b932b79019..67540fec520 100644 --- a/packages/effect/CONFIG.md +++ b/packages/effect/CONFIG.md @@ -347,7 +347,7 @@ Effect.runSync(host) // "localhost" **How `_` splitting works**: env var names are split on `_` to build a tree. This means `DATABASE_HOST=localhost` is accessible at both `["DATABASE_HOST"]` (flat) and `["DATABASE", "HOST"]` (nested). Querying `["DATABASE"]` returns a Record node with child key `"HOST"`. -Pass `{ env: { ... } }` for testing. Omit to use `process.env` (merged with `import.meta.env` when available). +Pass `{ env: { ... } }` for testing. Omit to use `process.env` when available. ### `ConfigProvider.fromUnknown` — Plain JS Objects From 56e01fb7cf234f973eeb4229a8e98713d98ce710 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=BC=E5=81=A5=E8=81=AA?= Date: Wed, 5 Aug 2026 21:09:43 +0800 Subject: [PATCH 5/5] test(config): keep import.meta regression portable --- packages/effect/src/ConfigProvider.ts | 4 +++- packages/effect/test/ConfigProvider.test.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/effect/src/ConfigProvider.ts b/packages/effect/src/ConfigProvider.ts index 962d4878b0d..edc9658a40a 100644 --- a/packages/effect/src/ConfigProvider.ts +++ b/packages/effect/src/ConfigProvider.ts @@ -890,7 +890,9 @@ export function fromEnv(options?: { readonly preserveEmptyStrings?: boolean | undefined }): ConfigProvider { const env: Record = options?.env ?? { - ...globalThis?.process?.env + ...(globalThis as { + readonly process?: { readonly env?: Record } + }).process?.env } const preserveEmptyStrings = options?.preserveEmptyStrings === true const trie = buildEnvTrie(env) diff --git a/packages/effect/test/ConfigProvider.test.ts b/packages/effect/test/ConfigProvider.test.ts index e83957cea32..fcde9514639 100644 --- a/packages/effect/test/ConfigProvider.test.ts +++ b/packages/effect/test/ConfigProvider.test.ts @@ -322,12 +322,12 @@ describe("ConfigProvider", () => { } }) - it("does not reference import.meta in the common ConfigProvider module", () => { + it("does not reference import.meta.env in the common ConfigProvider module", () => { const sourcePath = Fs.existsSync("src/ConfigProvider.ts") ? "src/ConfigProvider.ts" : "packages/effect/src/ConfigProvider.ts" const source = Fs.readFileSync(sourcePath, "utf8") - deepStrictEqual(source.includes("import.meta"), false) + deepStrictEqual(source.includes("import.meta.env"), false) }) it("env without an underscore", async () => {