schema-env
Version:
Type-safe environment variable validation for Node.js using Zod schemas or custom adapters. Load .env files, expand variables, fetch async secrets, and validate process.env at startup.
311 lines (310 loc) • 10.5 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
createEnv: () => createEnv,
createEnvAsync: () => createEnvAsync
});
module.exports = __toCommonJS(index_exports);
var import_node_fs = __toESM(require("fs"), 1);
var import_dotenv = __toESM(require("dotenv"), 1);
var import_dotenv_expand = require("dotenv-expand");
var import_zod = require("zod");
var ZodValidatorAdapter = class {
constructor(schema) {
this.schema = schema;
}
validate(data) {
const result = this.schema.safeParse(data);
if (result.success) {
return { success: true, data: result.data };
} else {
return {
success: false,
error: {
// Map Zod errors to standardized format
issues: result.error.errors.map((zodError) => ({
path: zodError.path,
message: zodError.message
}))
}
};
}
}
};
function _loadDotEnvFiles(dotEnvPath, nodeEnv) {
if (dotEnvPath === false) {
return {};
}
let mergedDotEnvParsed = {};
const loadEnvFile = (filePath) => {
try {
const fileContent = import_node_fs.default.readFileSync(filePath, { encoding: "utf8" });
const parsed = import_dotenv.default.parse(fileContent);
return parsed;
} catch (e) {
const err = e;
if (err.code !== "ENOENT") {
throw new Error(
`\u274C Failed to load environment file from ${filePath}: ${err.message}`
);
}
return {};
}
};
let pathsToLoad = [];
if (dotEnvPath === void 0) {
pathsToLoad = ["./.env"];
} else if (typeof dotEnvPath === "string") {
pathsToLoad = [dotEnvPath];
} else if (Array.isArray(dotEnvPath)) {
pathsToLoad = dotEnvPath.filter((path) => {
if (typeof path !== "string") {
console.warn(
`\u26A0\uFE0F [schema-env] Warning: Invalid path ignored in dotEnvPath array: ${String(
path
)}`
);
return false;
}
return true;
});
}
for (const path of pathsToLoad) {
const parsed = loadEnvFile(path);
mergedDotEnvParsed = { ...mergedDotEnvParsed, ...parsed };
}
if (nodeEnv) {
const envSpecificPath = `./.env.${nodeEnv}`;
const envSpecificParsed = loadEnvFile(envSpecificPath);
mergedDotEnvParsed = { ...mergedDotEnvParsed, ...envSpecificParsed };
}
return mergedDotEnvParsed;
}
function _expandDotEnvValues(mergedDotEnvParsed, expandVariables, expandDotenv) {
if (!expandVariables || !mergedDotEnvParsed || Object.keys(mergedDotEnvParsed).length === 0) {
return mergedDotEnvParsed || {};
}
const configToExpand = {
parsed: { ...mergedDotEnvParsed }
};
try {
const expansionResult = expandDotenv(configToExpand);
return expansionResult?.parsed || mergedDotEnvParsed || {};
} catch (e) {
console.error(
`\u274C Error during variable expansion: ${e instanceof Error ? e.message : String(e)}`
);
return mergedDotEnvParsed || {};
}
}
function _mergeProcessEnv(sourceInput) {
const sourceWithProcessEnv = { ...sourceInput };
for (const key in process.env) {
if (Object.prototype.hasOwnProperty.call(process.env, key)) {
const value = process.env[key];
if (value !== void 0) {
sourceWithProcessEnv[key] = value;
}
}
}
return sourceWithProcessEnv;
}
function _formatValidationError(error) {
let issues;
if (error instanceof import_zod.ZodError) {
issues = error.errors.map((err) => ({
path: err.path,
message: err.message
}));
} else if (error && Array.isArray(error.issues)) {
issues = error.issues;
} else {
return "\u274C Unknown validation error occurred.";
}
const formattedErrors = issues.map(
(err) => ` - ${err.path.join(".") || "UNKNOWN_PATH"}: ${err.message}`
);
return `\u274C Invalid environment variables:
${formattedErrors.join("\n")}`;
}
function _validateEnvironment(adapter, sourceForValidation) {
return adapter.validate(sourceForValidation);
}
async function _fetchSecrets(secretsSources) {
if (!secretsSources || secretsSources.length === 0) {
return {};
}
const results = await Promise.allSettled(
secretsSources.map((sourceFn, index) => {
try {
const maybePromise = sourceFn();
if (maybePromise && typeof maybePromise.then === "function") {
return maybePromise;
} else {
return Promise.reject(
new Error(
`Sync return value from secrets source function at index ${index}. Function must return a Promise.`
)
);
}
} catch (syncError) {
return Promise.reject(
new Error(
`Sync error in secrets source function at index ${index}: ${syncError instanceof Error ? syncError.message : String(syncError)}`
)
);
}
})
);
let mergedSecrets = {};
let successfulFetches = 0;
results.forEach((result, index) => {
if (result.status === "fulfilled") {
successfulFetches++;
if (result.value && typeof result.value === "object") {
mergedSecrets = { ...mergedSecrets, ...result.value };
} else if (result.value !== void 0 && result.value !== null) {
console.warn(
`\u26A0\uFE0F [schema-env] Warning: Secrets source function at index ${index} resolved with non-object value: ${typeof result.value}. Expected Record<string, string | undefined>.`
);
}
} else {
console.warn(
`\u26A0\uFE0F [schema-env] Warning: Secrets source function at index ${index} failed: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`
);
}
});
if (successfulFetches === 0 && secretsSources.length > 0) {
console.warn(
`\u26A0\uFE0F [schema-env] Warning: All ${secretsSources.length} provided secretsSources functions failed to resolve successfully.`
);
return {};
}
return mergedSecrets;
}
function _getValidatorAdapter(options) {
const { schema, validator } = options;
if (schema && validator) {
throw new Error("Cannot provide both 'schema' and 'validator' options.");
}
if (validator) {
return validator;
}
if (schema) {
if (!(schema instanceof import_zod.ZodObject)) {
throw new Error(
"Invalid 'schema' provided. Expected a ZodObject when 'validator' is not used."
);
}
return new ZodValidatorAdapter(
schema
);
}
throw new Error("Must provide either a 'schema' or a 'validator' option.");
}
function createEnv(options) {
const {
dotEnvPath,
expandVariables = false,
_internalDotenvExpand = import_dotenv_expand.expand
// _internalDotenvConfig removed
} = options;
const adapter = _getValidatorAdapter(options);
const mergedDotEnvParsed = _loadDotEnvFiles(
dotEnvPath,
process.env.NODE_ENV
// Use actual process.env value here for deciding which env-specific file to load
// _internalDotenvConfig removed from call
);
const finalDotEnvValues = _expandDotEnvValues(
mergedDotEnvParsed,
expandVariables,
_internalDotenvExpand
);
const sourceForValidation = _mergeProcessEnv(finalDotEnvValues);
const validationResult = _validateEnvironment(adapter, sourceForValidation);
if (!validationResult.success) {
const errorMessage = _formatValidationError(validationResult.error);
console.error(errorMessage);
throw new Error("Environment validation failed. Check console output.");
}
return validationResult.data;
}
async function createEnvAsync(options) {
const {
dotEnvPath,
expandVariables = false,
secretsSources,
_internalDotenvExpand = import_dotenv_expand.expand
// _internalDotenvConfig removed
} = options;
const adapter = _getValidatorAdapter(options);
const mergedDotEnvParsed = _loadDotEnvFiles(
dotEnvPath,
process.env.NODE_ENV
// Use actual process.env value here for deciding which env-specific file to load
// _internalDotenvConfig removed from call
);
const expandedDotEnvValues = _expandDotEnvValues(
mergedDotEnvParsed,
expandVariables,
_internalDotenvExpand
);
try {
const secretsValues = await _fetchSecrets(secretsSources);
const sourceBeforeProcessEnv = {
...expandedDotEnvValues,
...secretsValues
};
const sourceForValidation = _mergeProcessEnv(sourceBeforeProcessEnv);
const validationResult = _validateEnvironment(adapter, sourceForValidation);
if (!validationResult.success) {
const errorMessage = _formatValidationError(validationResult.error);
console.error(errorMessage);
throw new Error("Environment validation failed. Check console output.");
}
return validationResult.data;
} catch (error) {
if (error instanceof Error) {
return Promise.reject(error);
} else {
return Promise.reject(
new Error(`An unexpected error occurred: ${String(error)}`)
);
}
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
createEnv,
createEnvAsync
});
//# sourceMappingURL=index.cjs.map