🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@fluojs/config

Package Overview
Dependencies
Maintainers
1
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@fluojs/config - npm Package Compare versions

Comparing version
1.0.2
to
1.0.3
+1
-1
dist/load.d.ts.map

@@ -1,1 +0,1 @@

{"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../src/load.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,EAEjB,cAAc,EAKf,MAAM,YAAY,CAAC;AAwZpB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,iBAAiB,GAAG,cAAc,CAgC/E;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,CAEvE"}
{"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../src/load.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,EAEjB,cAAc,EAKf,MAAM,YAAY,CAAC;AAisBpB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,iBAAiB,GAAG,cAAc,CAgC/E;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,CAEvE"}

@@ -1,10 +0,69 @@

import { createHash } from 'node:crypto';
import { existsSync, readFileSync, watch } from 'node:fs';
import { basename, dirname, join } from 'node:path';
import { FluoError } from '@fluojs/core';
import { parse as dotenvParse } from 'dotenv';
import { expand as dotenvExpand } from 'dotenv-expand';
import { cloneConfigDictionary } from './clone.js';
import { snapshotConfigLoadOptions } from './options.js';
const reloadFailureReasons = new WeakMap();
const nodeBuiltinRuntimeRequirement = 'Node.js 20.16.0 or newer is required when @fluojs/config loads env files or starts watch mode.';
let requireNodeBuiltin;
function resolveRequireNodeBuiltin() {
if (requireNodeBuiltin) {
return requireNodeBuiltin;
}
const nodeModule = globalThis.process?.getBuiltinModule?.('node:module');
if (!nodeModule?.createRequire) {
throw new FluoError('Node.js configuration loading is unavailable in this runtime.', {
code: 'CONFIG_RUNTIME_UNAVAILABLE',
cause: new Error(`${nodeBuiltinRuntimeRequirement} The host runtime did not expose a synchronous node:module loader. Use in-memory config options or run env-file loading on Node.js.`)
});
}
requireNodeBuiltin = nodeModule.createRequire(import.meta.url);
return requireNodeBuiltin;
}
function requireNodeBuiltinFallback(id) {
try {
return resolveRequireNodeBuiltin()(id);
} catch (error) {
if (error instanceof FluoError) {
throw error;
}
throw new FluoError('Node.js configuration loading is unavailable in this runtime.', {
code: 'CONFIG_RUNTIME_UNAVAILABLE',
cause: new Error(`${nodeBuiltinRuntimeRequirement} The host runtime could not load ${id}. Use in-memory config options or run env-file loading on Node.js.`, {
cause: error
})
});
}
}
function resolveNodeBuiltin(id) {
const getBuiltinModule = globalThis.process?.getBuiltinModule;
if (!getBuiltinModule) {
return requireNodeBuiltinFallback(id);
}
const module = getBuiltinModule(id);
if (!module) {
return requireNodeBuiltinFallback(id);
}
return module;
}
function nodeCrypto() {
return resolveNodeBuiltin('node:crypto');
}
function nodeFs() {
return resolveNodeBuiltin('node:fs');
}
function nodePath() {
return resolveNodeBuiltin('node:path');
}
function resolveCurrentWorkingDirectory() {
const cwd = globalThis.process?.cwd;
if (!cwd) {
throw new FluoError('Node.js configuration loading is unavailable in this runtime.', {
code: 'CONFIG_RUNTIME_UNAVAILABLE',
cause: new Error('The host runtime did not expose process.cwd(). Pass envFilePath or avoid env-file loading outside Node.js.')
});
}
return cwd();
}
function isNodeFsError(error) {
return typeof error === 'object' && error !== null && 'code' in error;
}
function markReloadFailure(error, reason) {

@@ -21,2 +80,130 @@ if (typeof error === 'object' && error !== null) {

}
function unquoteEnvValue(value) {
if (value.length < 2) {
return value;
}
const quote = value[0];
if (quote !== '"' && quote !== "'" && quote !== '`' || value[value.length - 1] !== quote) {
return value;
}
const unquoted = value.slice(1, -1);
return quote === '"' ? unquoted.replace(/\\n/g, '\n').replace(/\\r/g, '\r') : unquoted;
}
function stripInlineEnvComment(value) {
const commentIndex = value.search(/\s#/);
return commentIndex === -1 ? value : value.slice(0, commentIndex);
}
function findClosingEnvQuote(value, quote) {
for (let index = 1; index < value.length; index += 1) {
if (value[index] === quote && value[index - 1] !== '\\') {
return index;
}
}
return -1;
}
function collectQuotedEnvValue(lines, startIndex, valueStart, quote) {
let value = valueStart;
let currentIndex = startIndex;
while (currentIndex + 1 < lines.length && findClosingEnvQuote(value, quote) === -1) {
currentIndex += 1;
value += `\n${lines[currentIndex]}`;
}
const closingQuoteIndex = findClosingEnvQuote(value, quote);
return {
nextIndex: currentIndex,
value: closingQuoteIndex === -1 ? value : value.slice(0, closingQuoteIndex + 1)
};
}
function parseDotenvContent(content) {
const parsed = {};
const lines = content.split(/\r?\n/);
for (let index = 0; index < lines.length; index += 1) {
const rawLine = lines[index];
const line = rawLine.trim();
if (line.length === 0 || line.startsWith('#')) {
continue;
}
const entry = line.startsWith('export ') ? line.slice(7).trimStart() : line;
const separatorIndex = entry.search(/[:=]/);
if (separatorIndex <= 0) {
continue;
}
const key = entry.slice(0, separatorIndex).trim();
const rawValue = entry.slice(separatorIndex + 1).trim();
if (!/^[\w.-]+$/.test(key)) {
continue;
}
const quote = rawValue[0];
const quotedValue = quote === '"' || quote === "'" || quote === '`';
const collected = quotedValue ? collectQuotedEnvValue(lines, index, rawValue, quote) : {
nextIndex: index,
value: stripInlineEnvComment(rawValue).trim()
};
index = collected.nextIndex;
parsed[key] = unquoteEnvValue(collected.value);
}
return parsed;
}
function expandEnvVariables(parsed, safeProcessEnv) {
const expanded = {};
const source = {
...parsed,
...safeProcessEnv
};
const parseBracedExpansion = expression => {
if (expression in source) {
return {
variableName: expression
};
}
const colonDefaultIndex = expression.indexOf(':-');
if (colonDefaultIndex !== -1) {
return {
defaultOperator: ':-',
defaultValue: expression.slice(colonDefaultIndex + 2),
variableName: expression.slice(0, colonDefaultIndex)
};
}
const defaultIndex = expression.indexOf('-');
if (defaultIndex !== -1) {
return {
defaultOperator: '-',
defaultValue: expression.slice(defaultIndex + 1),
variableName: expression.slice(0, defaultIndex)
};
}
return {
variableName: expression
};
};
const expandValue = (value, visiting) => value.replace(/(^|[^\\])\$(?:\{([^}]*)\}|([\w.-]+))/g, (match, prefix, bracedExpression, bareVariableName) => {
const expansion = bracedExpression === undefined ? {
variableName: bareVariableName ?? ''
} : parseBracedExpansion(bracedExpression);
const {
defaultOperator,
defaultValue,
variableName
} = expansion;
if (!variableName) {
return match;
}
const hasSourceValue = variableName in source;
const sourceValue = hasSourceValue ? source[variableName] ?? '' : undefined;
const shouldUseDefaultValue = defaultValue !== undefined && (!hasSourceValue || defaultOperator === ':-' && sourceValue === '');
if (shouldUseDefaultValue) {
return `${prefix}${expandValue(defaultValue, visiting)}`;
}
if (!hasSourceValue || visiting.has(variableName)) {
return prefix;
}
const replacement = variableName in safeProcessEnv ? safeProcessEnv[variableName] : variableName in expanded ? expanded[variableName] : expandValue(sourceValue ?? '', new Set([...visiting, variableName]));
return `${prefix}${replacement}`;
}).replace(/\\\$/g, '$');
for (const [key, value] of Object.entries(parsed)) {
expanded[key] = expandValue(value, new Set([key]));
source[key] = expanded[key];
}
return expanded;
}
function parseEnvContent(content, safeProcessEnv, customParser) {

@@ -26,10 +213,3 @@ if (customParser) {

}
const parsed = dotenvParse(content);
const result = dotenvExpand({
parsed,
processEnv: {
...safeProcessEnv
}
});
return result.parsed ?? {};
return expandEnvVariables(parseDotenvContent(content), safeProcessEnv);
}

@@ -49,4 +229,7 @@ function sanitizeProcessEnv(processEnv) {

rejectLegacyValidateOption(options);
const cwd = options.cwd ?? process.cwd();
const envFile = options.envFilePath ?? options.envFile ?? join(cwd, '.env');
const hasExplicitEnvFile = options.envFilePath !== undefined || options.envFile !== undefined;
const hasExplicitInMemorySource = options.defaults !== undefined || options.processEnv !== undefined || options.runtimeOverrides !== undefined;
const shouldUseDefaultEnvFile = !hasExplicitEnvFile && (options.cwd !== undefined || options.watch === true || !hasExplicitInMemorySource);
const cwd = shouldUseDefaultEnvFile && options.envFilePath === undefined && options.envFile === undefined ? options.cwd ?? resolveCurrentWorkingDirectory() : options.cwd;
const envFile = options.envFilePath ?? options.envFile ?? (cwd ? nodePath().join(cwd, '.env') : undefined);
const defaults = options.defaults ?? {};

@@ -66,6 +249,9 @@ const processEnv = options.processEnv ?? {};

function readEnvFileValues(options) {
if (options.envFile === undefined) {
return {};
}
try {
return parseEnvContent(readFileSync(options.envFile, 'utf8'), options.safeProcessEnv, options.parse);
return parseEnvContent(nodeFs().readFileSync(options.envFile, 'utf8'), options.safeProcessEnv, options.parse);
} catch (error) {
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT') {
if (isNodeFsError(error) && error.code === 'ENOENT') {
return {};

@@ -149,3 +335,3 @@ }

function isInvalidConfigError(error) {
return error instanceof FluoError && error.code === 'INVALID_CONFIG';
return typeof error === 'object' && error !== null && 'code' in error && error.code === 'INVALID_CONFIG';
}

@@ -191,5 +377,5 @@ function readConfigSchemaResult(result) {

try {
return createHash('sha256').update(readFileSync(envFile)).digest('hex');
return nodeCrypto().createHash('sha256').update(nodeFs().readFileSync(envFile)).digest('hex');
} catch (error) {
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT') {
if (isNodeFsError(error) && error.code === 'ENOENT') {
return undefined;

@@ -250,8 +436,14 @@ }

}
const watchTarget = dirname(normalized.envFile);
const watchedEnvFileName = basename(normalized.envFile);
if (!existsSync(watchTarget)) {
if (normalized.envFile === undefined) {
return undefined;
}
return watch(watchTarget, {
const path = nodePath();
const fs = nodeFs();
const envFile = normalized.envFile;
const watchTarget = path.dirname(envFile);
const watchedEnvFileName = path.basename(envFile);
if (!fs.existsSync(watchTarget)) {
return undefined;
}
return fs.watch(watchTarget, {
persistent: false

@@ -263,3 +455,3 @@ }, (_eventType, filename) => {

try {
const nextEnvFileHash = hashEnvFileContent(normalized.envFile);
const nextEnvFileHash = hashEnvFileContent(envFile);
if (nextEnvFileHash === state.watchedEnvFileHash) {

@@ -311,3 +503,3 @@ return;

reloading: false,
watchedEnvFileHash: hashEnvFileContent(normalized.envFile),
watchedEnvFileHash: normalized.envFile === undefined ? undefined : hashEnvFileContent(normalized.envFile),
watcher: undefined

@@ -314,0 +506,0 @@ };

@@ -11,3 +11,3 @@ {

],
"version": "1.0.2",
"version": "1.0.3",
"private": false,

@@ -21,3 +21,3 @@ "license": "MIT",

"engines": {
"node": ">=20.0.0"
"node": ">=20.16.0"
},

@@ -41,9 +41,7 @@ "publishConfig": {

"@standard-schema/spec": "^1.1.0",
"dotenv": "^16.0.0",
"dotenv-expand": "^11.0.0",
"@fluojs/core": "^1.0.2"
"@fluojs/core": "^1.0.3"
},
"devDependencies": {
"vitest": "^3.2.4",
"@fluojs/di": "^1.0.2"
"@fluojs/di": "^1.1.0"
},

@@ -50,0 +48,0 @@ "scripts": {

@@ -23,2 +23,4 @@ # @fluojs/config

패키지는 Node.js 20.16.0 이상을 지원합니다. Env-file loading, 기본 `.env` loading, watch mode는 Node filesystem, path, crypto builtin을 host runtime boundary를 통해 lazy하게 해석하며 `process.getBuiltinModule(...)`을 요구합니다. 이 API가 존재하지만 filesystem/path/crypto direct lookup이 불가능할 때는 published ESM과 호환되는 `node:module` fallback을 사용합니다. `ConfigService`와 `loadConfig({ defaults, processEnv, runtimeOverrides })`의 in-memory 사용은 env-file access를 요구하지 않지만, 배포 계약은 package-level Node.js 20.16.0 engine을 따릅니다.
## 사용 시점

@@ -83,4 +85,6 @@

`envFilePath`는 `envFile`보다 우선하며, `parse`를 사용하면 flat key/value 파일을 위한 custom parser로 dotenv parsing을 대체할 수 있습니다. 누락된 env file은 load 시 빈 입력처럼 처리됩니다. watch mode에서는 parent directory도 관찰하므로 나중에 파일을 생성해도 reload를 트리거할 수 있습니다.
`envFilePath`는 `envFile`보다 우선하며, `parse`를 사용하면 flat key/value 파일을 위한 custom parser로 dotenv parsing을 대체할 수 있습니다. 빈 load/module option은 `loadConfig({})`와 `ConfigModule.forRoot()`에 대해 기본 `<cwd>/.env` 동작을 보존합니다. 누락된 env file은 load 시 빈 입력처럼 처리됩니다. watch mode에서는 parent directory도 관찰하므로 나중에 파일을 생성해도 reload를 트리거할 수 있습니다.
Root `@fluojs/config` 패키지를 import하는 것만으로는 Node filesystem, path, crypto builtin을 해석하지 않습니다. `ConfigService`, option type, 또는 명시적 in-memory 입력을 쓰는 `loadConfig(...)` consumer는 root import를 안전하게 사용할 수 있고, Node builtin은 env-file load 또는 watch mode가 실제로 실행될 때 lazy하게 해석됩니다. `loadConfig({ defaults, processEnv, runtimeOverrides })`는 `process.cwd()`, 기본 `.env` path, Node filesystem/path/crypto builtin을 해석하지 않습니다. Published package engine이 Node.js 20.16.0 이상이므로 Node.js 20.0.0부터 20.15.x까지와 Node.js 밖의 runtime은 env-file, 기본 `.env`, watch execution path의 지원 package contract에 포함되지 않습니다.
### 객체 단위 딥 머지

@@ -87,0 +91,0 @@

@@ -23,2 +23,4 @@ # @fluojs/config

The package supports Node.js 20.16.0 or newer. Env-file loading, default `.env` loading, and watch mode resolve Node filesystem, path, and crypto builtins lazily through the host runtime boundary, requiring `process.getBuiltinModule(...)`; when that API is present but direct filesystem/path/crypto lookup is unavailable, a published-ESM-compatible `node:module` fallback is used. In-memory use of `ConfigService` and `loadConfig({ defaults, processEnv, runtimeOverrides })` does not require env-file access, but it is still distributed under the package-level Node.js 20.16.0 engine contract.
## When to Use

@@ -89,4 +91,6 @@

`envFilePath` overrides `envFile`, and `parse` lets callers replace dotenv parsing with a custom parser for flat key/value files. Missing env files are treated as empty input during load; watch mode also observes the parent directory so creating the file later can trigger a reload.
`envFilePath` overrides `envFile`, and `parse` lets callers replace dotenv parsing with a custom parser for flat key/value files. Empty load/module options preserve the default `<cwd>/.env` behavior for `loadConfig({})` and `ConfigModule.forRoot()`. Missing env files are treated as empty input during load; watch mode also observes the parent directory so creating the file later can trigger a reload.
Importing the root `@fluojs/config` package is safe for in-memory consumers that only need `ConfigService`, option types, or `loadConfig(...)` with explicit in-memory inputs: Node filesystem, path, and crypto builtins are resolved lazily only when env-file loading or watch mode actually runs. `loadConfig({ defaults, processEnv, runtimeOverrides })` does not resolve `process.cwd()`, a default `.env` path, or Node filesystem/path/crypto builtins. Because the published package engine is Node.js 20.16.0 or newer, Node.js 20.0.0 through 20.15.x and non-Node runtimes are outside the supported package contract for env-file, default `.env`, and watch execution paths.
### Deep Merging

@@ -93,0 +97,0 @@ Plain objects are deep-merged by key. Arrays and primitive values from higher-precedence sources completely replace lower-precedence ones.