envox
Version:
Fast and flexible environment variable parser with detailed error reporting.
257 lines (248 loc) • 7.83 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
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 __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
fromObject: () => fromObject,
isEnvFile: () => isEnvFile,
parseEnv: () => parseEnv
});
module.exports = __toCommonJS(index_exports);
// src/errors.ts
var EnvoxError = class extends Error {
constructor(line, content, message) {
super(message);
this.line = line;
this.content = content;
this.name = "EnvoxError";
}
};
// src/constants.ts
var REGEX = {
VAR_EXPANSION: /\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*?)(?=[^A-Za-z0-9]|$)/g,
VALID_KEY: /^[A-Za-z_][A-Za-z0-9_]*$/,
NEED_QUOTES: /[\s"'$\\]/,
ENV_VAR_LIKE: /^[A-Za-z_][A-Za-z0-9_]*$/
};
// src/expansions.ts
function expandValue(value, lookup) {
return value.replace(REGEX.VAR_EXPANSION, (_match, braced, simple) => {
const varName = braced || simple;
return lookup(varName) || "";
});
}
// src/helpers.ts
function isEnvFile(content) {
const lines = content.split("\n").filter((line) => line.trim());
if (lines.length === 0) return false;
let validLines = 0;
let totalLines = 0;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith("#")) continue;
totalLines++;
if (isLikeEnvVar(trimmed)) {
validLines++;
}
}
return totalLines > 0 && validLines / totalLines >= 0.5;
}
function isLikeEnvVar(line) {
const cleaned = line.replace(/^\s*export\s+/, "");
const equalsIndex = cleaned.indexOf("=");
if (equalsIndex === -1) return false;
const key = cleaned.substring(0, equalsIndex).trim();
return REGEX.ENV_VAR_LIKE.test(key);
}
function toObject(variables) {
const obj = {};
for (const variable of variables) {
obj[variable.key] = variable.value;
}
return obj;
}
function fromObject(obj, { includeExport = false } = {}) {
const prefix = includeExport ? "export " : "";
return Object.entries(obj).map(([key, value]) => {
const quotedValue = REGEX.NEED_QUOTES.test(value) ? `"${value.replace(/"/g, '\\"')}"` : value;
return `${prefix}${key}=${quotedValue}`;
}).join("\n");
}
// src/schema.ts
function runSchema(schema, data) {
try {
const validation = schema["~standard"].validate(data);
if (validation instanceof Promise) {
throw new TypeError("Schema validation must be synchronous");
}
if ("issues" in validation) {
const errors = validation.issues?.map((issue) => ({
line: 0,
message: issue.message,
content: issue.path ? `Path: ${formatPath(issue.path)}` : "Schema validation failed"
})) || [];
return { success: false, errors };
}
return { success: true, data: validation.value };
} catch (error) {
return {
success: false,
errors: [
{
line: 0,
message: error instanceof Error ? error.message : "Schema validation failed",
content: "Schema validation error"
}
]
};
}
}
function formatPath(path) {
return path.map((segment) => {
if (typeof segment === "object" && "key" in segment) {
return String(segment.key);
}
return String(segment);
}).join(".");
}
// src/utils.ts
function parseLine(line, lineNumber, envMap, options) {
const trimmedLine = line.trim();
if (!trimmedLine) return null;
if (options.allowComments && trimmedLine.startsWith("#")) return null;
let processedLine = trimmedLine;
if (options.allowExport && trimmedLine.startsWith("export ")) {
processedLine = trimmedLine.substring(7);
}
const equalsIndex = processedLine.indexOf("=");
if (equalsIndex === -1) {
if (!options.allowEmpty) {
throw new EnvoxError(lineNumber, line, "Missing equals sign");
}
return null;
}
const key = processedLine.substring(0, equalsIndex).trim();
let value = processedLine.substring(equalsIndex + 1);
if (!isValidKey(key)) {
throw new EnvoxError(
lineNumber,
line,
`Invalid environment variable key: ${key}`
);
}
value = processValue(value, envMap, options);
return { key, value, line: lineNumber };
}
function processValue(value, envMap, options) {
let processedValue = value;
processedValue = handleQuotes(processedValue);
if (options.trimValues) {
processedValue = processedValue.trim();
}
if (options.expandVariables) {
processedValue = expandValue(processedValue, (k) => envMap.get(k));
}
return processedValue;
}
function handleQuotes(value) {
const trimmed = value.trim();
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
return trimmed.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
}
if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
return trimmed.slice(1, -1);
}
return value;
}
function isValidKey(key) {
return REGEX.VALID_KEY.test(key) && !key.includes(" ");
}
// src/index.ts
var defaultOptions = {
allowEmpty: true,
allowComments: true,
allowExport: true,
trimValues: true,
expandVariables: false
};
function parseEnv(source, options = {}) {
const mergedOptions = { ...defaultOptions, ...options };
const variables = [];
const errors = [];
const envMap = /* @__PURE__ */ new Map();
if (typeof source === "string") {
const lines = source.split("\n");
for (const [index, line] of lines.entries()) {
const lineNumber = index + 1;
if (line === void 0) continue;
try {
const result = parseLine(line, lineNumber, envMap, mergedOptions);
if (result) {
variables.push(result);
envMap.set(result.key, result.value);
}
} catch (error) {
if (error instanceof EnvoxError) {
errors.push({
line: error.line,
message: error.message,
content: error.content
});
} else {
errors.push({
line: lineNumber,
message: error instanceof Error ? error.message : "Unknown error",
content: line
});
}
}
}
} else if (typeof source === "object" && source !== null) {
const tempMap = /* @__PURE__ */ new Map();
for (const [key, value] of Object.entries(source)) {
if (isValidKey(key) && value !== void 0) {
tempMap.set(key, value);
}
}
for (const [key, value] of tempMap.entries()) {
const expandedValue = mergedOptions.expandVariables ? expandValue(value, (k) => tempMap.get(k)) : value;
envMap.set(key, expandedValue);
variables.push({ key, value: expandedValue, line: 0 });
}
}
if (errors.length > 0) {
return { ok: false, errors };
}
const obj = toObject(variables);
if (mergedOptions.schema) {
const validationResult = runSchema(mergedOptions.schema, obj);
if (validationResult.success) {
return { ok: true, data: validationResult.data, vars: variables };
} else {
return { ok: false, errors: validationResult.errors };
}
}
return { ok: true, data: obj, vars: variables };
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
fromObject,
isEnvFile,
parseEnv
});