diff --git a/playground/nitro.config.ts b/playground/nitro.config.ts index 2a9dd131e6..a763adbd67 100644 --- a/playground/nitro.config.ts +++ b/playground/nitro.config.ts @@ -1 +1 @@ -export default defineNitroConfig({}); +export default defineNitroConfig({}); \ No newline at end of file diff --git a/src/runtime/config.ts b/src/runtime/config.ts index 7bc504e018..a03ce12130 100644 --- a/src/runtime/config.ts +++ b/src/runtime/config.ts @@ -1,19 +1,20 @@ -import destr from "destr"; -import { snakeCase } from "scule"; import { klona } from "klona"; import { H3Event } from "h3"; +import { type EnvOptions, applyEnv } from "./utils.env"; import { appConfig as _inlineAppConfig } from "#internal/nitro/virtual/app-config"; import type { NitroRuntimeConfig } from "nitropack"; // Static runtime config inlined by nitro build const _inlineRuntimeConfig = process.env.RUNTIME_CONFIG as any; -const ENV_PREFIX = "NITRO_"; -const ENV_PREFIX_ALT = - _inlineRuntimeConfig.nitro.envPrefix ?? process.env.NITRO_ENV_PREFIX ?? "_"; +const envOptions: EnvOptions = { + prefix: "NITRO_", + altPrefix: + _inlineRuntimeConfig.nitro.envPrefix ?? process.env.NITRO_ENV_PREFIX ?? "_", +}; // Runtime config const _sharedRuntimeConfig = _deepFreeze( - _applyEnv(klona(_inlineRuntimeConfig)) + applyEnv(klona(_inlineRuntimeConfig), envOptions) ); export function useRuntimeConfig< T extends NitroRuntimeConfig = NitroRuntimeConfig, @@ -28,7 +29,7 @@ export function useRuntimeConfig< } // Prepare runtime config for event context const runtimeConfig = klona(_inlineRuntimeConfig) as T; - _applyEnv(runtimeConfig); + applyEnv(runtimeConfig, envOptions); event.context.nitro.runtimeConfig = runtimeConfig; return runtimeConfig; } @@ -52,33 +53,6 @@ export function useAppConfig(event?: H3Event) { // --- Utils --- -function _getEnv(key: string) { - const envKey = snakeCase(key).toUpperCase(); - return destr( - process.env[ENV_PREFIX + envKey] ?? process.env[ENV_PREFIX_ALT + envKey] - ); -} - -function _isObject(input: unknown) { - return typeof input === "object" && !Array.isArray(input); -} - -function _applyEnv(obj: object, parentKey = "") { - for (const key in obj) { - const subKey = parentKey ? `${parentKey}_${key}` : key; - const envValue = _getEnv(subKey); - if (_isObject(obj[key])) { - if (_isObject(envValue)) { - obj[key] = { ...obj[key], ...(envValue as any) }; - } - _applyEnv(obj[key], subKey); - } else { - obj[key] = envValue ?? obj[key]; - } - } - return obj; -} - function _deepFreeze(object: Record) { const propNames = Object.getOwnPropertyNames(object); for (const name of propNames) { diff --git a/src/runtime/utils.env.ts b/src/runtime/utils.env.ts new file mode 100644 index 0000000000..62578fa2c1 --- /dev/null +++ b/src/runtime/utils.env.ts @@ -0,0 +1,45 @@ +import destr from "destr"; +import { snakeCase } from "scule"; + +export type EnvOptions = { + prefix?: string; + altPrefix?: string; +}; + +export function getEnv(key: string, opts: EnvOptions) { + const envKey = snakeCase(key).toUpperCase(); + return destr( + process.env[opts.prefix + envKey] ?? process.env[opts.altPrefix + envKey] + ); +} + +function _isObject(input: unknown) { + return typeof input === "object" && !Array.isArray(input); +} + +export function applyEnv(obj: object, opts: EnvOptions, parentKey = "") { + for (const key in obj) { + const subKey = parentKey ? `${parentKey}_${key}` : key; + const envValue = getEnv(subKey, opts); + if (_isObject(obj[key])) { + // Same as before + if (_isObject(envValue)) { + obj[key] = { ...obj[key], ...(envValue as object) }; + applyEnv(obj[key], opts, subKey); + } + // If envValue is undefined + // Then proceed to nested properties + else if (envValue === undefined) { + applyEnv(obj[key], opts, subKey); + } + // If envValue is a primitive other than undefined + // Then set objValue and ignore the nested properties + else { + obj[key] = envValue ?? obj[key]; + } + } else { + obj[key] = envValue ?? obj[key]; + } + } + return obj; +} diff --git a/test/unit/utils.env.test.ts b/test/unit/utils.env.test.ts new file mode 100644 index 0000000000..fae5305749 --- /dev/null +++ b/test/unit/utils.env.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect } from "vitest"; +import { applyEnv } from "../../src/runtime/utils.env"; + +describe("env utils", () => { + describe("applyEnv", () => { + const tests = [ + { + config: { a: 1, b: 2 }, + env: { NITRO_A: "123" }, + expected: { a: 123, b: 2 }, + }, + { + config: { feature: { options: { optionA: true, optionB: true } } }, + env: { NITRO_FEATURE: false }, + expected: { feature: false }, + }, + { + config: { feature: { options: { optionA: true, optionB: true } } }, + env: { NITRO_FEATURE_OPTIONS: false }, + expected: { feature: { options: false } }, + }, + ]; + for (const test of tests) { + it(`Config: ${JSON.stringify(test.config)} Env: { ${Object.entries( + test.env + ) + .map(([key, value]) => `${key}=${JSON.stringify(value)}`) + .join(" ")} }`, () => { + for (const key in test.env) { + process.env[key] = test.env[key]; + } + expect(applyEnv(test.config, { prefix: "NITRO_" })).toEqual( + test.expected + ); + for (const key in test.env) { + delete process.env[key]; + } + }); + } + }); +});