env-example-doctor 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,163 @@
1
+ // src/analyze.ts
2
+ import { access, readFile as readFile2 } from "fs/promises";
3
+ import path2 from "path";
4
+
5
+ // src/env-file.ts
6
+ function parseEnvExample(content) {
7
+ const keys = /* @__PURE__ */ new Set();
8
+ for (const line of content.split(/\r?\n/)) {
9
+ const trimmed = line.trim();
10
+ if (!trimmed || trimmed.startsWith("#")) continue;
11
+ const eq = trimmed.indexOf("=");
12
+ const key = (eq === -1 ? trimmed : trimmed.slice(0, eq)).trim();
13
+ if (/^[A-Z_][A-Z0-9_]*$/.test(key)) keys.add(key);
14
+ }
15
+ return keys;
16
+ }
17
+ function suggestEnvExampleLines(keys) {
18
+ return keys.map((k) => `${k}=`);
19
+ }
20
+ function formatEnvExampleAppend(keys) {
21
+ if (keys.length === 0) return "";
22
+ const body = keys.map((k) => `${k}=
23
+ `).join("");
24
+ return `
25
+ # suggested by env-doctor
26
+ ${body}`;
27
+ }
28
+
29
+ // src/scan.ts
30
+ var ENV_DOT_RX = /process\.env\.([A-Z_][A-Z0-9_]*)/g;
31
+ var META_DOT_RX = /import\.meta\.env\.([A-Z_][A-Z0-9_]*)/g;
32
+ var ENV_BRACKET_RX = /process\.env\[['"]([A-Z_][A-Z0-9_]*)['"]\]/g;
33
+ var META_BRACKET_RX = /import\.meta\.env\[['"]([A-Z_][A-Z0-9_]*)['"]\]/g;
34
+ var ENV_DESTRUCT_RX = /\{\s*([^}]+)\s*\}\s*=\s*process\.env\b/g;
35
+ var KEY_RX = /^[A-Z_][A-Z0-9_]*$/;
36
+ function collectMatches(text, rx, found) {
37
+ rx.lastIndex = 0;
38
+ let m;
39
+ while ((m = rx.exec(text)) !== null) {
40
+ if (m[1]) found.add(m[1]);
41
+ }
42
+ }
43
+ function collectDestructured(text, found) {
44
+ ENV_DESTRUCT_RX.lastIndex = 0;
45
+ let m;
46
+ while ((m = ENV_DESTRUCT_RX.exec(text)) !== null) {
47
+ const inner = m[1];
48
+ if (!inner) continue;
49
+ for (const part of inner.split(",")) {
50
+ const trimmed = part.trim();
51
+ if (!trimmed) continue;
52
+ const head = trimmed.split(/\s*[=:]/)[0]?.trim() ?? "";
53
+ const name = head.replace(/^\.\.\./, "").trim();
54
+ if (KEY_RX.test(name)) found.add(name);
55
+ }
56
+ }
57
+ }
58
+ function scanSourceForEnv(text) {
59
+ const found = /* @__PURE__ */ new Set();
60
+ for (const rx of [ENV_DOT_RX, META_DOT_RX, ENV_BRACKET_RX, META_BRACKET_RX]) {
61
+ collectMatches(text, rx, found);
62
+ }
63
+ collectDestructured(text, found);
64
+ return found;
65
+ }
66
+
67
+ // src/walk.ts
68
+ import { readdir, readFile, stat } from "fs/promises";
69
+ import path from "path";
70
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "coverage", ".turbo"]);
71
+ var SOURCE_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".vue", ".svelte"]);
72
+ function pathIgnored(rel, patterns) {
73
+ const norm = rel.replace(/\\/g, "/");
74
+ return patterns.some((pat) => {
75
+ if (pat.includes("*")) {
76
+ const re = new RegExp(
77
+ "^" + pat.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\?/g, ".") + "$"
78
+ );
79
+ return re.test(norm);
80
+ }
81
+ return norm.includes(pat.replace(/\\/g, "/"));
82
+ });
83
+ }
84
+ async function listSourceFiles(root, ignore = []) {
85
+ const out = [];
86
+ async function walk(dir) {
87
+ const entries = await readdir(dir, { withFileTypes: true });
88
+ for (const ent of entries) {
89
+ const full = path.join(dir, ent.name);
90
+ const rel = path.relative(root, full);
91
+ if (pathIgnored(rel, ignore)) continue;
92
+ if (ent.isDirectory()) {
93
+ if (SKIP_DIRS.has(ent.name)) continue;
94
+ await walk(full);
95
+ } else if (ent.isFile()) {
96
+ const ext = path.extname(ent.name);
97
+ if (SOURCE_EXT.has(ext)) out.push(full);
98
+ }
99
+ }
100
+ }
101
+ const st = await stat(root);
102
+ if (!st.isDirectory()) return out;
103
+ await walk(root);
104
+ return out;
105
+ }
106
+ async function readSources(paths) {
107
+ const map = /* @__PURE__ */ new Map();
108
+ await Promise.all(
109
+ paths.map(async (p) => {
110
+ const text = await readFile(p, "utf8");
111
+ map.set(p, text);
112
+ })
113
+ );
114
+ return map;
115
+ }
116
+
117
+ // src/analyze.ts
118
+ async function analyzeEnv(options = {}) {
119
+ const root = path2.resolve(options.root ?? process.cwd());
120
+ const envPath = path2.resolve(options.envExamplePath ?? path2.join(root, ".env.example"));
121
+ const ignore = options.ignore ?? [];
122
+ let documented = /* @__PURE__ */ new Set();
123
+ let envExampleFound = false;
124
+ try {
125
+ await access(envPath);
126
+ envExampleFound = true;
127
+ const example = await readFile2(envPath, "utf8");
128
+ documented = parseEnvExample(example);
129
+ } catch {
130
+ documented = /* @__PURE__ */ new Set();
131
+ }
132
+ const files = await listSourceFiles(root, ignore);
133
+ const sources = await readSources(files);
134
+ const used = /* @__PURE__ */ new Set();
135
+ for (const text of sources.values()) {
136
+ for (const k of scanSourceForEnv(text)) used.add(k);
137
+ }
138
+ const missingInExample = [...used].filter((k) => !documented.has(k)).sort();
139
+ const unusedInCode = [...documented].filter((k) => !used.has(k)).sort();
140
+ return {
141
+ usedInCode: [...used].sort(),
142
+ documented: [...documented].sort(),
143
+ missingInExample,
144
+ unusedInCode,
145
+ ok: missingInExample.length === 0 && unusedInCode.length === 0,
146
+ envExamplePath: envPath,
147
+ envExampleFound
148
+ };
149
+ }
150
+ function shouldFail(report, failOn) {
151
+ if (failOn === "missing") return report.missingInExample.length > 0;
152
+ if (failOn === "unused") return report.unusedInCode.length > 0;
153
+ return !report.ok;
154
+ }
155
+ export {
156
+ analyzeEnv,
157
+ formatEnvExampleAppend,
158
+ parseEnvExample,
159
+ scanSourceForEnv,
160
+ shouldFail,
161
+ suggestEnvExampleLines
162
+ };
163
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/analyze.ts","../src/env-file.ts","../src/scan.ts","../src/walk.ts"],"sourcesContent":["import { access, readFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport { parseEnvExample } from './env-file.js';\nimport { scanSourceForEnv } from './scan.js';\nimport { listSourceFiles, readSources } from './walk.js';\n\nexport type FailOn = 'missing' | 'unused' | 'any';\n\nexport interface EnvDoctorOptions {\n root?: string;\n envExamplePath?: string;\n ignore?: string[];\n}\n\nexport interface EnvDoctorReport {\n usedInCode: string[];\n documented: string[];\n missingInExample: string[];\n unusedInCode: string[];\n ok: boolean;\n /** Resolved path to the env example file that was read (or attempted). */\n envExamplePath: string;\n /** False when the env example file does not exist (all keys treated as undocumented). */\n envExampleFound: boolean;\n}\n\n/**\n * Compare env usage in source against `.env.example`.\n */\nexport async function analyzeEnv(options: EnvDoctorOptions = {}): Promise<EnvDoctorReport> {\n const root = path.resolve(options.root ?? process.cwd());\n const envPath = path.resolve(options.envExamplePath ?? path.join(root, '.env.example'));\n const ignore = options.ignore ?? [];\n\n let documented = new Set<string>();\n let envExampleFound = false;\n try {\n await access(envPath);\n envExampleFound = true;\n const example = await readFile(envPath, 'utf8');\n documented = parseEnvExample(example);\n } catch {\n documented = new Set();\n }\n\n const files = await listSourceFiles(root, ignore);\n const sources = await readSources(files);\n const used = new Set<string>();\n for (const text of sources.values()) {\n for (const k of scanSourceForEnv(text)) used.add(k);\n }\n\n const missingInExample = [...used].filter((k) => !documented.has(k)).sort();\n const unusedInCode = [...documented].filter((k) => !used.has(k)).sort();\n\n return {\n usedInCode: [...used].sort(),\n documented: [...documented].sort(),\n missingInExample,\n unusedInCode,\n ok: missingInExample.length === 0 && unusedInCode.length === 0,\n envExamplePath: envPath,\n envExampleFound,\n };\n}\n\nexport function shouldFail(report: EnvDoctorReport, failOn: FailOn): boolean {\n if (failOn === 'missing') return report.missingInExample.length > 0;\n if (failOn === 'unused') return report.unusedInCode.length > 0;\n return !report.ok;\n}\n","/**\n * Parse `.env.example` style files (KEY=value, comments and blanks ignored).\n */\nexport function parseEnvExample(content: string): Set<string> {\n const keys = new Set<string>();\n for (const line of content.split(/\\r?\\n/)) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith('#')) continue;\n const eq = trimmed.indexOf('=');\n const key = (eq === -1 ? trimmed : trimmed.slice(0, eq)).trim();\n if (/^[A-Z_][A-Z0-9_]*$/.test(key)) keys.add(key);\n }\n return keys;\n}\n\n/** Lines to append when keys are missing from `.env.example`. */\nexport function suggestEnvExampleLines(keys: string[]): string[] {\n return keys.map((k) => `${k}=`);\n}\n\nexport function formatEnvExampleAppend(keys: string[]): string {\n if (keys.length === 0) return '';\n const body = keys.map((k) => `${k}=\\n`).join('');\n return `\\n# suggested by env-doctor\\n${body}`;\n}\n","const ENV_DOT_RX = /process\\.env\\.([A-Z_][A-Z0-9_]*)/g;\nconst META_DOT_RX = /import\\.meta\\.env\\.([A-Z_][A-Z0-9_]*)/g;\nconst ENV_BRACKET_RX = /process\\.env\\[['\"]([A-Z_][A-Z0-9_]*)['\"]\\]/g;\nconst META_BRACKET_RX = /import\\.meta\\.env\\[['\"]([A-Z_][A-Z0-9_]*)['\"]\\]/g;\nconst ENV_DESTRUCT_RX = /\\{\\s*([^}]+)\\s*\\}\\s*=\\s*process\\.env\\b/g;\n\nconst KEY_RX = /^[A-Z_][A-Z0-9_]*$/;\n\nfunction collectMatches(text: string, rx: RegExp, found: Set<string>): void {\n rx.lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = rx.exec(text)) !== null) {\n if (m[1]) found.add(m[1]);\n }\n}\n\nfunction collectDestructured(text: string, found: Set<string>): void {\n ENV_DESTRUCT_RX.lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = ENV_DESTRUCT_RX.exec(text)) !== null) {\n const inner = m[1];\n if (!inner) continue;\n for (const part of inner.split(',')) {\n const trimmed = part.trim();\n if (!trimmed) continue;\n const head = trimmed.split(/\\s*[=:]/)[0]?.trim() ?? '';\n const name = head.replace(/^\\.\\.\\./, '').trim();\n if (KEY_RX.test(name)) found.add(name);\n }\n }\n}\n\n/**\n * Extract environment variable names referenced in source text.\n * Supports dot access, bracket access, and simple `const { A, B } = process.env`.\n * Does not resolve dynamic keys (`process.env[name]`).\n */\nexport function scanSourceForEnv(text: string): Set<string> {\n const found = new Set<string>();\n for (const rx of [ENV_DOT_RX, META_DOT_RX, ENV_BRACKET_RX, META_BRACKET_RX]) {\n collectMatches(text, rx, found);\n }\n collectDestructured(text, found);\n return found;\n}\n","import { readdir, readFile, stat } from 'node:fs/promises';\nimport path from 'node:path';\n\nconst SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'coverage', '.turbo']);\n\nconst SOURCE_EXT = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.vue', '.svelte']);\n\nfunction pathIgnored(rel: string, patterns: string[]): boolean {\n const norm = rel.replace(/\\\\/g, '/');\n return patterns.some((pat) => {\n if (pat.includes('*')) {\n const re = new RegExp(\n '^' + pat.replace(/\\*\\*/g, '.*').replace(/\\*/g, '[^/]*').replace(/\\?/g, '.') + '$',\n );\n return re.test(norm);\n }\n return norm.includes(pat.replace(/\\\\/g, '/'));\n });\n}\n\n/**\n * Recursively list source files under `root`, honouring ignore globs (simple `*` / `**`).\n */\nexport async function listSourceFiles(root: string, ignore: string[] = []): Promise<string[]> {\n const out: string[] = [];\n\n async function walk(dir: string): Promise<void> {\n const entries = await readdir(dir, { withFileTypes: true });\n for (const ent of entries) {\n const full = path.join(dir, ent.name);\n const rel = path.relative(root, full);\n if (pathIgnored(rel, ignore)) continue;\n if (ent.isDirectory()) {\n if (SKIP_DIRS.has(ent.name)) continue;\n await walk(full);\n } else if (ent.isFile()) {\n const ext = path.extname(ent.name);\n if (SOURCE_EXT.has(ext)) out.push(full);\n }\n }\n }\n\n const st = await stat(root);\n if (!st.isDirectory()) return out;\n await walk(root);\n return out;\n}\n\nexport async function readSources(paths: string[]): Promise<Map<string, string>> {\n const map = new Map<string, string>();\n await Promise.all(\n paths.map(async (p) => {\n const text = await readFile(p, 'utf8');\n map.set(p, text);\n }),\n );\n return map;\n}\n"],"mappings":"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","names":["readFile","path","path","readFile"]}
package/package.json CHANGED
@@ -1,6 +1,77 @@
1
1
  {
2
2
  "name": "env-example-doctor",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "author": "Muhammad Ali <maliuppal@users.noreply.github.com> (https://github.com/maliuppal)",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/maliuppal/env-doctor.git"
8
+ },
9
+ "bugs": {
10
+ "url": "https://github.com/maliuppal/env-doctor/issues"
11
+ },
12
+ "homepage": "https://github.com/maliuppal/env-doctor#readme",
13
+ "description": "Keep .env.example in sync with code: missing/unused env vars, Vite import.meta.env, CI fail-on, --suggest—zero dependencies",
14
+ "license": "MIT",
15
+ "sideEffects": false,
16
+ "type": "module",
17
+ "bin": {
18
+ "env-doctor": "./dist/cli.cjs"
19
+ },
20
+ "exports": {
21
+ ".": {
22
+ "import": {
23
+ "types": "./dist/index.d.ts",
24
+ "default": "./dist/index.js"
25
+ },
26
+ "require": {
27
+ "types": "./dist/index.d.cts",
28
+ "default": "./dist/index.cjs"
29
+ }
30
+ }
31
+ },
32
+ "main": "./dist/index.cjs",
33
+ "module": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "files": [
36
+ "dist",
37
+ "README.md",
38
+ "LICENSE"
39
+ ],
40
+ "engines": {
41
+ "node": ">=18"
42
+ },
43
+ "scripts": {
44
+ "build": "tsup",
45
+ "test": "vitest run --coverage",
46
+ "typecheck": "tsc --noEmit",
47
+ "lint": "eslint src test"
48
+ },
49
+ "keywords": [
50
+ "env-doctor",
51
+ "env",
52
+ "dotenv",
53
+ "environment-variables",
54
+ "env-example",
55
+ "env-sync",
56
+ "env-sample",
57
+ "vite",
58
+ "import-meta-env",
59
+ "process-env",
60
+ "twelve-factor",
61
+ "github-actions",
62
+ "ci",
63
+ "pre-commit",
64
+ "cli",
65
+ "typescript",
66
+ "node",
67
+ "onboarding",
68
+ "devops"
69
+ ],
70
+ "devDependencies": {
71
+ "@types/node": "^22.10.2",
72
+ "@vitest/coverage-v8": "^2.1.8",
73
+ "tsup": "^8.3.5",
74
+ "typescript": "~5.7.2",
75
+ "vitest": "^2.1.8"
76
+ }
77
+ }