From 21450f9a6792fc19b33fe71b0be8c3244423a8fe Mon Sep 17 00:00:00 2001 From: brunozoric Date: Thu, 16 Jul 2026 13:01:30 +0200 Subject: [PATCH 1/4] feat(env): add createProcessEnv and createBrowserEnv factory functions Env implementations can now be used outside DI via standalone factory functions, matching the pattern already established by other tools (createPathTool, createFileTool, etc.). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/browser/features/BrowserEnv/BrowserEnv.ts | 8 ++++++++ src/browser/features/BrowserEnv/README.md | 10 ++++++++++ src/browser/features/BrowserEnv/index.ts | 1 + src/browser/index.ts | 6 +++++- src/node/features/ProcessEnv/ProcessEnv.ts | 4 ++++ src/node/features/ProcessEnv/README.md | 9 +++++++++ src/node/features/ProcessEnv/index.ts | 1 + src/node/index.ts | 2 +- 8 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/browser/features/BrowserEnv/BrowserEnv.ts b/src/browser/features/BrowserEnv/BrowserEnv.ts index e9ebf34..670baca 100644 --- a/src/browser/features/BrowserEnv/BrowserEnv.ts +++ b/src/browser/features/BrowserEnv/BrowserEnv.ts @@ -1,6 +1,10 @@ import type { Env } from "~/common/features/Env/abstractions/Env.js"; import { toBoolean } from "~/common/utils/boolean/index.js"; +export interface CreateBrowserEnvParams { + variables: Record; +} + export class BrowserEnv implements Env.Interface { private readonly variables: Record; @@ -70,3 +74,7 @@ export class BrowserEnv implements Env.Interface { return toBoolean(raw); } } + +export function createBrowserEnv(params: CreateBrowserEnvParams): Env.Interface { + return new BrowserEnv(params.variables); +} diff --git a/src/browser/features/BrowserEnv/README.md b/src/browser/features/BrowserEnv/README.md index bab000e..7ae8398 100644 --- a/src/browser/features/BrowserEnv/README.md +++ b/src/browser/features/BrowserEnv/README.md @@ -17,3 +17,13 @@ BrowserEnvFeature.register(container, { }); const env = container.resolve(Env); ``` + +### Without DI + +```ts +import { createBrowserEnv } from "@webiny/stdlib/browser"; + +const env = createBrowserEnv({ + variables: { API_URL: "https://api.example.com" } +}); +``` diff --git a/src/browser/features/BrowserEnv/index.ts b/src/browser/features/BrowserEnv/index.ts index c24ea11..d405ca4 100644 --- a/src/browser/features/BrowserEnv/index.ts +++ b/src/browser/features/BrowserEnv/index.ts @@ -1 +1,2 @@ export { BrowserEnvFeature } from "./feature.js"; +export { createBrowserEnv, type CreateBrowserEnvParams } from "./BrowserEnv.js"; diff --git a/src/browser/index.ts b/src/browser/index.ts index 18d1f19..bbfd67d 100644 --- a/src/browser/index.ts +++ b/src/browser/index.ts @@ -1,4 +1,8 @@ -export { BrowserEnvFeature } from "./features/BrowserEnv/index.js"; +export { + BrowserEnvFeature, + createBrowserEnv, + type CreateBrowserEnvParams +} from "./features/BrowserEnv/index.js"; export { LocalStorageCacheFeature, createLocalStorageCache, diff --git a/src/node/features/ProcessEnv/ProcessEnv.ts b/src/node/features/ProcessEnv/ProcessEnv.ts index 1fe0265..007afd8 100644 --- a/src/node/features/ProcessEnv/ProcessEnv.ts +++ b/src/node/features/ProcessEnv/ProcessEnv.ts @@ -69,3 +69,7 @@ export const ProcessEnv = EnvAbstraction.createImplementation({ implementation: ProcessEnvImpl, dependencies: [] }); + +export function createProcessEnv(): EnvAbstraction.Interface { + return new ProcessEnvImpl(); +} diff --git a/src/node/features/ProcessEnv/README.md b/src/node/features/ProcessEnv/README.md index 14cc91d..471856b 100644 --- a/src/node/features/ProcessEnv/README.md +++ b/src/node/features/ProcessEnv/README.md @@ -17,3 +17,12 @@ const env = container.resolve(Env); const port = env.getNumber("PORT", 3000); ``` + +### Without DI + +```ts +import { createProcessEnv } from "@webiny/stdlib/node"; + +const env = createProcessEnv(); +const port = env.getNumber("PORT", 3000); +``` diff --git a/src/node/features/ProcessEnv/index.ts b/src/node/features/ProcessEnv/index.ts index 6e879fc..166258a 100644 --- a/src/node/features/ProcessEnv/index.ts +++ b/src/node/features/ProcessEnv/index.ts @@ -1 +1,2 @@ export { ProcessEnvFeature } from "./feature.js"; +export { createProcessEnv } from "./ProcessEnv.js"; diff --git a/src/node/index.ts b/src/node/index.ts index c6d0706..12727c7 100644 --- a/src/node/index.ts +++ b/src/node/index.ts @@ -51,7 +51,7 @@ export { type CreatePackageJsonFileToolParams, PackageJsonFile } from "./features/PackageJsonFileTool/index.js"; -export { ProcessEnvFeature } from "./features/ProcessEnv/index.js"; +export { ProcessEnvFeature, createProcessEnv } from "./features/ProcessEnv/index.js"; export { HashFolderTool, HashFolderToolFeature, From 515073aa88bcbbe6faf94016969806cb5a99c304 Mon Sep 17 00:00:00 2001 From: brunozoric Date: Thu, 16 Jul 2026 13:02:36 +0200 Subject: [PATCH 2/4] chore: add changeset for env factory functions Co-Authored-By: Claude Opus 4.6 (1M context) --- .changeset/legal-windows-yawn.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/legal-windows-yawn.md diff --git a/.changeset/legal-windows-yawn.md b/.changeset/legal-windows-yawn.md new file mode 100644 index 0000000..2d8820b --- /dev/null +++ b/.changeset/legal-windows-yawn.md @@ -0,0 +1,5 @@ +--- +"@webiny/stdlib": patch +--- + +Add `createProcessEnv()` and `createBrowserEnv()` factory functions for using Env implementations outside of DI. From 8c6926a79fe99192da21b7dbb45104f8896dc987 Mon Sep 17 00:00:00 2001 From: brunozoric Date: Thu, 16 Jul 2026 13:16:23 +0200 Subject: [PATCH 3/4] feat(env): accept optional variables record in ProcessEnv ProcessEnv now takes an optional variables record instead of reading process.env directly. Defaults to process.env when not provided. Enables pre-processing env vars before injection. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/node/features/ProcessEnv/ProcessEnv.ts | 31 +++++++++++++--------- src/node/features/ProcessEnv/README.md | 17 ++++++++++-- src/node/features/ProcessEnv/feature.ts | 13 ++++++--- src/node/features/ProcessEnv/index.ts | 4 +-- src/node/index.ts | 7 ++++- 5 files changed, 50 insertions(+), 22 deletions(-) diff --git a/src/node/features/ProcessEnv/ProcessEnv.ts b/src/node/features/ProcessEnv/ProcessEnv.ts index 007afd8..b0800b3 100644 --- a/src/node/features/ProcessEnv/ProcessEnv.ts +++ b/src/node/features/ProcessEnv/ProcessEnv.ts @@ -1,11 +1,21 @@ import { Env as EnvAbstraction } from "~/common/features/Env/abstractions/Env.js"; import { toBoolean } from "~/common/utils/boolean/index.js"; +export interface CreateProcessEnvParams { + variables?: Record; +} + class ProcessEnvImpl implements EnvAbstraction.Interface { + private readonly variables: Record; + + public constructor(variables: Record) { + this.variables = variables; + } + getString(key: string): string | undefined; getString(key: string, defaultValue: string): string; getString(key: string, defaultValue?: string): string | undefined { - const value = process.env[key]; + const value = this.variables[key]; if (value === undefined) { return defaultValue; } @@ -13,7 +23,7 @@ class ProcessEnvImpl implements EnvAbstraction.Interface { } getStringOrThrow(key: string): string { - const value = process.env[key]; + const value = this.variables[key]; if (value === undefined) { throw new Error(`Environment variable "${key}" is not set.`); } @@ -23,7 +33,7 @@ class ProcessEnvImpl implements EnvAbstraction.Interface { getNumber(key: string): number | undefined; getNumber(key: string, defaultValue: number): number; getNumber(key: string, defaultValue?: number): number | undefined { - const raw = process.env[key]; + const raw = this.variables[key]; if (raw === undefined || raw === "") { return defaultValue; } @@ -35,7 +45,7 @@ class ProcessEnvImpl implements EnvAbstraction.Interface { } getNumberOrThrow(key: string): number { - const raw = process.env[key]; + const raw = this.variables[key]; if (raw === undefined || raw === "") { throw new Error(`Environment variable "${key}" is not set.`); } @@ -49,7 +59,7 @@ class ProcessEnvImpl implements EnvAbstraction.Interface { getBoolean(key: string): boolean | undefined; getBoolean(key: string, defaultValue: boolean): boolean; getBoolean(key: string, defaultValue?: boolean): boolean | undefined { - const raw = process.env[key]; + const raw = this.variables[key]; if (raw === undefined) { return defaultValue; } @@ -57,7 +67,7 @@ class ProcessEnvImpl implements EnvAbstraction.Interface { } getBooleanOrThrow(key: string): boolean { - const raw = process.env[key]; + const raw = this.variables[key]; if (raw === undefined) { throw new Error(`Environment variable "${key}" is not set.`); } @@ -65,11 +75,6 @@ class ProcessEnvImpl implements EnvAbstraction.Interface { } } -export const ProcessEnv = EnvAbstraction.createImplementation({ - implementation: ProcessEnvImpl, - dependencies: [] -}); - -export function createProcessEnv(): EnvAbstraction.Interface { - return new ProcessEnvImpl(); +export function createProcessEnv(params?: CreateProcessEnvParams): EnvAbstraction.Interface { + return new ProcessEnvImpl(params?.variables ?? process.env); } diff --git a/src/node/features/ProcessEnv/README.md b/src/node/features/ProcessEnv/README.md index 471856b..aaf6b83 100644 --- a/src/node/features/ProcessEnv/README.md +++ b/src/node/features/ProcessEnv/README.md @@ -1,6 +1,6 @@ # ProcessEnv -Node.js implementation of the `Env` abstraction, backed by `process.env`. Provides typed access to environment variables with `getString`, `getNumber`, and `getBoolean` families. +Node.js implementation of the `Env` abstraction, backed by `process.env` by default. Accepts an optional `variables` record to override the source. Provides typed access to environment variables with `getString`, `getNumber`, and `getBoolean` families. See [Env README](../../common/features/Env/README.md) for the full interface and usage. @@ -18,11 +18,24 @@ const env = container.resolve(Env); const port = env.getNumber("PORT", 3000); ``` +### With custom variables (DI) + +```ts +ProcessEnvFeature.register(container, { + variables: { ...process.env, MY_VAR: "overridden" } +}); +``` + ### Without DI ```ts import { createProcessEnv } from "@webiny/stdlib/node"; +// Defaults to process.env const env = createProcessEnv(); -const port = env.getNumber("PORT", 3000); + +// With custom variables +const env = createProcessEnv({ + variables: { ...process.env, MY_VAR: "overridden" } +}); ``` diff --git a/src/node/features/ProcessEnv/feature.ts b/src/node/features/ProcessEnv/feature.ts index 7ec47fd..02a89f4 100644 --- a/src/node/features/ProcessEnv/feature.ts +++ b/src/node/features/ProcessEnv/feature.ts @@ -1,9 +1,14 @@ import { createFeature } from "~/common/index.js"; -import { ProcessEnv } from "./ProcessEnv.js"; +import { Env } from "~/common/features/Env/abstractions/Env.js"; +import { createProcessEnv } from "./ProcessEnv.js"; -export const ProcessEnvFeature = createFeature({ +export interface ProcessEnvFeatureParams { + variables?: Record; +} + +export const ProcessEnvFeature = createFeature({ name: "Node/ProcessEnvFeature", - register(container) { - container.register(ProcessEnv).inSingletonScope(); + register(container, params) { + container.registerInstance(Env, createProcessEnv(params || undefined)); } }); diff --git a/src/node/features/ProcessEnv/index.ts b/src/node/features/ProcessEnv/index.ts index 166258a..6a5d635 100644 --- a/src/node/features/ProcessEnv/index.ts +++ b/src/node/features/ProcessEnv/index.ts @@ -1,2 +1,2 @@ -export { ProcessEnvFeature } from "./feature.js"; -export { createProcessEnv } from "./ProcessEnv.js"; +export { ProcessEnvFeature, type ProcessEnvFeatureParams } from "./feature.js"; +export { createProcessEnv, type CreateProcessEnvParams } from "./ProcessEnv.js"; diff --git a/src/node/index.ts b/src/node/index.ts index 12727c7..3db78c1 100644 --- a/src/node/index.ts +++ b/src/node/index.ts @@ -51,7 +51,12 @@ export { type CreatePackageJsonFileToolParams, PackageJsonFile } from "./features/PackageJsonFileTool/index.js"; -export { ProcessEnvFeature, createProcessEnv } from "./features/ProcessEnv/index.js"; +export { + ProcessEnvFeature, + type ProcessEnvFeatureParams, + createProcessEnv, + type CreateProcessEnvParams +} from "./features/ProcessEnv/index.js"; export { HashFolderTool, HashFolderToolFeature, From 360540da84e195d817f00bcc45f4f4b48f6e1f1e Mon Sep 17 00:00:00 2001 From: brunozoric Date: Thu, 16 Jul 2026 13:20:44 +0200 Subject: [PATCH 4/4] fix(env): make BrowserEnvFeature variables required BrowserEnv has no meaning without variables. Feature now requires the variables record; test updated to pass empty record instead of undefined for the no-variables scenario. Co-Authored-By: Claude Opus 4.6 (1M context) --- __tests__/browser/Env.test.ts | 4 ++-- src/browser/features/BrowserEnv/feature.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/__tests__/browser/Env.test.ts b/__tests__/browser/Env.test.ts index 6214e30..e4df597 100644 --- a/__tests__/browser/Env.test.ts +++ b/__tests__/browser/Env.test.ts @@ -6,13 +6,13 @@ import { ConsoleLoggerFeature } from "../../src/common/features/Logger/feature.j import { Env } from "../../src/common/features/Env/index.js"; import { BrowserEnvFeature } from "../../src/browser/features/BrowserEnv/index.js"; -function makeContainer(vars?: Record): Container { +function makeContainer(vars: Record = {}): Container { const container = new Container(); container.registerInstance(ConsoleLoggerConfig, { getConfig: () => ({ logLevel: "error" as const }) }); ConsoleLoggerFeature.register(container); - BrowserEnvFeature.register(container, vars ? { variables: vars } : undefined); + BrowserEnvFeature.register(container, { variables: vars }); return container; } diff --git a/src/browser/features/BrowserEnv/feature.ts b/src/browser/features/BrowserEnv/feature.ts index f728d82..7ecc91d 100644 --- a/src/browser/features/BrowserEnv/feature.ts +++ b/src/browser/features/BrowserEnv/feature.ts @@ -1,14 +1,14 @@ import { createFeature } from "~/common/index.js"; import { Env } from "~/common/features/Env/abstractions/Env.js"; -import { BrowserEnv } from "./BrowserEnv.js"; +import { createBrowserEnv } from "./BrowserEnv.js"; export interface BrowserEnvFeatureParams { variables: Record; } -export const BrowserEnvFeature = createFeature({ +export const BrowserEnvFeature = createFeature({ name: "Browser/BrowserEnvFeature", register(container, params) { - container.registerInstance(Env, new BrowserEnv(params?.variables || {})); + container.registerInstance(Env, createBrowserEnv(params)); } });