@itsthw/envguard
Advanced tools
+94
-80
@@ -28,26 +28,58 @@ #!/usr/bin/env node | ||
| var import_commander = require("commander"); | ||
| var import_dotenv = __toESM(require("dotenv"), 1); | ||
| var import_fs = __toESM(require("fs"), 1); | ||
| var import_node_fs2 = __toESM(require("fs"), 1); | ||
| var import_node_path2 = __toESM(require("path"), 1); | ||
| // src/validate.ts | ||
| function parse(value, type) { | ||
| if (type === "string") return value; | ||
| if (type === "number") { | ||
| const n = Number(value); | ||
| return Number.isFinite(n) ? n : null; | ||
| // src/scan.ts | ||
| var import_node_fs = __toESM(require("fs"), 1); | ||
| var import_node_path = __toESM(require("path"), 1); | ||
| function scanProjectForEnvKeys(rootDir) { | ||
| const exts = /* @__PURE__ */ new Set([".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs"]); | ||
| const ignoreDirs = /* @__PURE__ */ new Set([ | ||
| "node_modules", | ||
| ".git", | ||
| "dist", | ||
| "build", | ||
| ".next" | ||
| ]); | ||
| const keys = /* @__PURE__ */ new Set(); | ||
| walk(rootDir); | ||
| return keys; | ||
| function walk(dir) { | ||
| for (const entry of import_node_fs.default.readdirSync(dir, { withFileTypes: true })) { | ||
| const full = import_node_path.default.join(dir, entry.name); | ||
| if (entry.isDirectory()) { | ||
| if (!ignoreDirs.has(entry.name)) walk(full); | ||
| continue; | ||
| } | ||
| if (!exts.has(import_node_path.default.extname(entry.name))) continue; | ||
| const content = safeRead(full); | ||
| if (!content) continue; | ||
| for (const m of content.matchAll(/\bprocess\.env\.([A-Z0-9_]+)\b/g)) { | ||
| keys.add(m[1]); | ||
| } | ||
| for (const m of content.matchAll( | ||
| /\bprocess\.env\[\s*["']([A-Z0-9_]+)["']\s*\]/g | ||
| )) { | ||
| keys.add(m[1]); | ||
| } | ||
| for (const m of content.matchAll( | ||
| /\bimport\.meta\.env\.([A-Z0-9_]+)\b/g | ||
| )) { | ||
| keys.add(m[1]); | ||
| } | ||
| } | ||
| } | ||
| if (type === "boolean") { | ||
| if (value === "true") return true; | ||
| if (value === "false") return false; | ||
| return null; | ||
| function safeRead(file) { | ||
| try { | ||
| return import_node_fs.default.readFileSync(file, "utf8"); | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| if (typeof type === "object" && "enum" in type) { | ||
| return type.enum.includes(value) ? value : null; | ||
| } | ||
| return null; | ||
| } | ||
| function validateEnv(schema) { | ||
| // src/validate.ts | ||
| function validateProcessEnv(schema) { | ||
| const errors = []; | ||
| const values = {}; | ||
| for (const key in schema) { | ||
| for (const [key, type] of Object.entries(schema)) { | ||
| const raw = process.env[key]; | ||
@@ -58,75 +90,57 @@ if (!raw) { | ||
| } | ||
| const parsed = parse(raw, schema[key]); | ||
| if (parsed === null) { | ||
| errors.push(`Invalid env: ${key}=${raw}`); | ||
| continue; | ||
| if (type === "number" && !Number.isFinite(Number(raw))) { | ||
| errors.push(`Invalid number: ${key}="${raw}"`); | ||
| } | ||
| values[key] = parsed; | ||
| } | ||
| if (errors.length) { | ||
| throw new Error(errors.join("\n")); | ||
| } | ||
| return values; | ||
| } | ||
| // src/example.ts | ||
| function placeholder(type) { | ||
| if (type === "number") return "3000"; | ||
| if (type === "boolean") return "true"; | ||
| if (typeof type === "object") return type.enum[0]; | ||
| return "value"; | ||
| } | ||
| function generateExample(schema) { | ||
| return Object.entries(schema).map(([k, t]) => `${k}=${placeholder(t)}`).join("\n") + "\n"; | ||
| } | ||
| // src/infer.ts | ||
| function inferSchema(env) { | ||
| const schema = {}; | ||
| for (const [key, value] of Object.entries(env)) { | ||
| if (!value) continue; | ||
| if (key.startsWith("npm_") || key.startsWith("NODE_") || key.startsWith("PATH") || key.startsWith("HOME")) { | ||
| continue; | ||
| if (type === "boolean" && !(raw === "true" || raw === "false")) { | ||
| errors.push(`Invalid boolean: ${key}="${raw}"`); | ||
| } | ||
| schema[key] = inferType(value); | ||
| } | ||
| return schema; | ||
| return { ok: errors.length === 0, errors }; | ||
| } | ||
| function inferType(value) { | ||
| if (/^-?\d+$/.test(value)) return "number"; | ||
| if (value === "true" || value === "false") return "boolean"; | ||
| return "string"; | ||
| } | ||
| // src/envguard.ts | ||
| import_dotenv.default.config(); | ||
| var program = new import_commander.Command(); | ||
| var SCHEMA_FILE = "envguard.schema.json"; | ||
| var EXAMPLE_FILE = ".env.example"; | ||
| function loadSchema() { | ||
| return JSON.parse(import_fs.default.readFileSync(SCHEMA_FILE, "utf8")); | ||
| } | ||
| program.name("envguard").description("Ultra-light env validation").version("1.0.0"); | ||
| program.command("init").description("Auto-generate envguard.schema.json and .env.example").action(() => { | ||
| const schema = inferSchema(process.env); | ||
| if (Object.keys(schema).length === 0) { | ||
| console.error("\u274C No environment variables found to infer schema."); | ||
| var DEFAULT_SCHEMA = "envguard.schema.json"; | ||
| var DEFAULT_ENV_FILE = ".env"; | ||
| program.name("envguard").description("Lightweight env discovery & validation tool").version("1.0.0"); | ||
| program.command("init").option("--root <dir>", "Project root to scan", ".").option("--env <file>", "Env file to generate", DEFAULT_ENV_FILE).option("--schema <file>", "Schema file to generate", DEFAULT_SCHEMA).option("--force", "Overwrite files if they exist").description("Scan project and generate env files").action((opts) => { | ||
| const root = import_node_path2.default.resolve(process.cwd(), opts.root); | ||
| const envPath = import_node_path2.default.resolve(process.cwd(), opts.env); | ||
| const schemaPath = import_node_path2.default.resolve(process.cwd(), opts.schema); | ||
| const keys = scanProjectForEnvKeys(root); | ||
| if (keys.size === 0) { | ||
| console.error("\u274C No environment variables found in project source."); | ||
| process.exit(1); | ||
| } | ||
| import_fs.default.writeFileSync(SCHEMA_FILE, JSON.stringify(schema, null, 2)); | ||
| import_fs.default.writeFileSync(EXAMPLE_FILE, generateExample(schema)); | ||
| console.log("\u2705 envguard initialized"); | ||
| console.log(` - ${SCHEMA_FILE}`); | ||
| console.log(` - ${EXAMPLE_FILE}`); | ||
| if (!opts.force) { | ||
| if (import_node_fs2.default.existsSync(envPath) || import_node_fs2.default.existsSync(schemaPath)) { | ||
| console.error("\u274C Files already exist. Use --force to overwrite."); | ||
| process.exit(1); | ||
| } | ||
| } | ||
| const schema = {}; | ||
| for (const key of keys) schema[key] = "string"; | ||
| import_node_fs2.default.writeFileSync(schemaPath, JSON.stringify(schema, null, 2)); | ||
| const envContent = [...keys].map((k) => `${k}=`).join("\n") + "\n"; | ||
| import_node_fs2.default.writeFileSync(envPath, envContent); | ||
| console.log("\u2705 EnvGuard initialized"); | ||
| console.log(` - ${opts.env}`); | ||
| console.log(` - ${opts.schema}`); | ||
| console.log(` - ${keys.size} env vars discovered`); | ||
| }); | ||
| program.command("check").description("Validate process.env").action(() => { | ||
| try { | ||
| validateEnv(loadSchema()); | ||
| console.log("\u2705 env OK"); | ||
| } catch (e) { | ||
| console.error("\u274C env invalid"); | ||
| console.error(e.message); | ||
| program.command("check").option("--schema <file>", "Schema file", DEFAULT_SCHEMA).description("Validate process.env against schema").action((opts) => { | ||
| const schemaPath = import_node_path2.default.resolve(process.cwd(), opts.schema); | ||
| if (!import_node_fs2.default.existsSync(schemaPath)) { | ||
| console.error("\u274C Schema file not found. Run `envguard init` first."); | ||
| process.exit(1); | ||
| } | ||
| const schema = JSON.parse(import_node_fs2.default.readFileSync(schemaPath, "utf8")); | ||
| const res = validateProcessEnv(schema); | ||
| if (!res.ok) { | ||
| console.error("\u274C Env validation failed:"); | ||
| for (const e of res.errors) console.error(" -", e); | ||
| process.exit(1); | ||
| } | ||
| console.log("\u2705 Env OK"); | ||
| }); | ||
| program.parse(); | ||
| program.parse(process.argv); |
+69
-41
| "use strict"; | ||
| 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; | ||
@@ -18,2 +20,10 @@ var __export = (target, all) => { | ||
| }; | ||
| 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); | ||
@@ -24,28 +34,60 @@ | ||
| __export(index_exports, { | ||
| generateExample: () => generateExample, | ||
| validateEnv: () => validateEnv | ||
| scanProjectForEnvKeys: () => scanProjectForEnvKeys, | ||
| validateProcessEnv: () => validateProcessEnv | ||
| }); | ||
| module.exports = __toCommonJS(index_exports); | ||
| // src/validate.ts | ||
| function parse(value, type) { | ||
| if (type === "string") return value; | ||
| if (type === "number") { | ||
| const n = Number(value); | ||
| return Number.isFinite(n) ? n : null; | ||
| // src/scan.ts | ||
| var import_node_fs = __toESM(require("fs"), 1); | ||
| var import_node_path = __toESM(require("path"), 1); | ||
| function scanProjectForEnvKeys(rootDir) { | ||
| const exts = /* @__PURE__ */ new Set([".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs"]); | ||
| const ignoreDirs = /* @__PURE__ */ new Set([ | ||
| "node_modules", | ||
| ".git", | ||
| "dist", | ||
| "build", | ||
| ".next" | ||
| ]); | ||
| const keys = /* @__PURE__ */ new Set(); | ||
| walk(rootDir); | ||
| return keys; | ||
| function walk(dir) { | ||
| for (const entry of import_node_fs.default.readdirSync(dir, { withFileTypes: true })) { | ||
| const full = import_node_path.default.join(dir, entry.name); | ||
| if (entry.isDirectory()) { | ||
| if (!ignoreDirs.has(entry.name)) walk(full); | ||
| continue; | ||
| } | ||
| if (!exts.has(import_node_path.default.extname(entry.name))) continue; | ||
| const content = safeRead(full); | ||
| if (!content) continue; | ||
| for (const m of content.matchAll(/\bprocess\.env\.([A-Z0-9_]+)\b/g)) { | ||
| keys.add(m[1]); | ||
| } | ||
| for (const m of content.matchAll( | ||
| /\bprocess\.env\[\s*["']([A-Z0-9_]+)["']\s*\]/g | ||
| )) { | ||
| keys.add(m[1]); | ||
| } | ||
| for (const m of content.matchAll( | ||
| /\bimport\.meta\.env\.([A-Z0-9_]+)\b/g | ||
| )) { | ||
| keys.add(m[1]); | ||
| } | ||
| } | ||
| } | ||
| if (type === "boolean") { | ||
| if (value === "true") return true; | ||
| if (value === "false") return false; | ||
| return null; | ||
| function safeRead(file) { | ||
| try { | ||
| return import_node_fs.default.readFileSync(file, "utf8"); | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| if (typeof type === "object" && "enum" in type) { | ||
| return type.enum.includes(value) ? value : null; | ||
| } | ||
| return null; | ||
| } | ||
| function validateEnv(schema) { | ||
| // src/validate.ts | ||
| function validateProcessEnv(schema) { | ||
| const errors = []; | ||
| const values = {}; | ||
| for (const key in schema) { | ||
| for (const [key, type] of Object.entries(schema)) { | ||
| const raw = process.env[key]; | ||
@@ -56,29 +98,15 @@ if (!raw) { | ||
| } | ||
| const parsed = parse(raw, schema[key]); | ||
| if (parsed === null) { | ||
| errors.push(`Invalid env: ${key}=${raw}`); | ||
| continue; | ||
| if (type === "number" && !Number.isFinite(Number(raw))) { | ||
| errors.push(`Invalid number: ${key}="${raw}"`); | ||
| } | ||
| values[key] = parsed; | ||
| if (type === "boolean" && !(raw === "true" || raw === "false")) { | ||
| errors.push(`Invalid boolean: ${key}="${raw}"`); | ||
| } | ||
| } | ||
| if (errors.length) { | ||
| throw new Error(errors.join("\n")); | ||
| } | ||
| return values; | ||
| return { ok: errors.length === 0, errors }; | ||
| } | ||
| // src/example.ts | ||
| function placeholder(type) { | ||
| if (type === "number") return "3000"; | ||
| if (type === "boolean") return "true"; | ||
| if (typeof type === "object") return type.enum[0]; | ||
| return "value"; | ||
| } | ||
| function generateExample(schema) { | ||
| return Object.entries(schema).map(([k, t]) => `${k}=${placeholder(t)}`).join("\n") + "\n"; | ||
| } | ||
| // Annotate the CommonJS export names for ESM import in node: | ||
| 0 && (module.exports = { | ||
| generateExample, | ||
| validateEnv | ||
| scanProjectForEnvKeys, | ||
| validateProcessEnv | ||
| }); |
+17
-8
@@ -1,10 +0,19 @@ | ||
| type EnvType = "string" | "number" | "boolean" | { | ||
| enum: string[]; | ||
| /** | ||
| * Scan project source code to find env usage. | ||
| * Supports: | ||
| * - process.env.MY_KEY | ||
| * - process.env["MY_KEY"] | ||
| * - import.meta.env.MY_KEY | ||
| */ | ||
| declare function scanProjectForEnvKeys(rootDir: string): Set<string>; | ||
| type Schema = Record<string, "string" | "number" | "boolean">; | ||
| /** | ||
| * Validate process.env against schema. | ||
| */ | ||
| declare function validateProcessEnv(schema: Schema): { | ||
| ok: boolean; | ||
| errors: string[]; | ||
| }; | ||
| type EnvSchema = Record<string, EnvType>; | ||
| declare function validateEnv(schema: EnvSchema): Record<string, any>; | ||
| declare function generateExample(schema: EnvSchema): string; | ||
| export { type EnvSchema, type EnvType, generateExample, validateEnv }; | ||
| export { scanProjectForEnvKeys, validateProcessEnv }; |
+2
-3
| { | ||
| "name": "@itsthw/envguard", | ||
| "version": "2.1.0", | ||
| "version": "2.1.1", | ||
| "type": "module", | ||
@@ -12,4 +12,3 @@ "bin": { | ||
| "dependencies": { | ||
| "commander": "^12.0.0", | ||
| "dotenv": "^16.4.5" | ||
| "commander": "^12.0.0" | ||
| }, | ||
@@ -16,0 +15,0 @@ "scripts": { |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
10231
40.15%1
-50%247
21.67%6
20%1
Infinity%- Removed
- Removed