dotenv-diff 1.6.4 → 2.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/README.md +45 -0
- package/dist/src/cli/program.js +7 -1
- package/dist/src/cli/run.js +20 -0
- package/dist/src/commands/compare.d.ts +3 -26
- package/dist/src/commands/compare.js +116 -47
- package/dist/src/commands/scanUsage.d.ts +43 -0
- package/dist/src/commands/scanUsage.js +154 -0
- package/dist/src/config/options.d.ts +1 -24
- package/dist/src/config/options.js +30 -7
- package/dist/src/config/types.d.ts +63 -0
- package/dist/src/config/types.js +8 -0
- package/dist/src/services/codeBaseScanner.d.ts +32 -0
- package/dist/src/services/codeBaseScanner.js +236 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Easily compare your .env, .env.example, and other environment files (like .env.local, .env.production) to detect missing, extra, empty, or mismatched variables — and ensure they’re properly ignored by Git.
|
|
4
4
|
|
|
5
|
+
Or scan your codebase to find out which environment variables are actually used in your code, and which ones are not.
|
|
6
|
+
|
|
7
|
+
Optimized for JavaScript/TypeScript projects and frontend frameworks including Node.js, Next.js, Vite, SvelteKit, Nuxt, Vue, and Deno. Can also be used with other project types for basic .env file comparison.
|
|
8
|
+
|
|
5
9
|
[](https://www.npmjs.com/package/dotenv-diff)
|
|
6
10
|
[](https://www.npmjs.com/package/dotenv-diff)
|
|
7
11
|
|
|
@@ -31,6 +35,38 @@ dotenv-diff will automatically compare all matching .env* files in your project
|
|
|
31
35
|
- `.env.production`
|
|
32
36
|
- Any other .env.* file
|
|
33
37
|
|
|
38
|
+
## Scan your codebase for environment variable usage
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
dotenv-diff --scan-usage
|
|
42
|
+
```
|
|
43
|
+
This scans your entire codebase to detect which environment variables are actually used in the code — and compare them against your `.env` file(s).
|
|
44
|
+
|
|
45
|
+
## Show unused variables
|
|
46
|
+
|
|
47
|
+
Use `--show-unused` together with `--scan-usage` to list variables that are defined in `.env` but never used in your codebase:
|
|
48
|
+
```bash
|
|
49
|
+
dotenv-diff --scan-usage --show-unused
|
|
50
|
+
```
|
|
51
|
+
This will show you which variables are defined in your `.env` file but not used in your codebase. This helps you clean up unnecessary environment variables.
|
|
52
|
+
|
|
53
|
+
## Show scan statistics
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
dotenv-diff --show-stats
|
|
57
|
+
```
|
|
58
|
+
This will display statistics about the scan, such as the number of files scanned, variables found, and any unused variables. It provides a quick overview of your environment variable usage.
|
|
59
|
+
|
|
60
|
+
## include or exclude specific files for scanning
|
|
61
|
+
|
|
62
|
+
You can specify which files to include or exclude from the scan using the `--include-files` and `--exclude-files` options:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
dotenv-diff --scan-usage --include-files '**/*.js,**/*.ts' --exclude-files '**/*.spec.ts'
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
This allows you to focus the scan on specific file types or directories, making it more efficient and tailored to your project structure.
|
|
69
|
+
|
|
34
70
|
## Optional: Check values too
|
|
35
71
|
|
|
36
72
|
```bash
|
|
@@ -43,6 +79,15 @@ When using the `--check-values` option, the tool will also compare the actual va
|
|
|
43
79
|
|
|
44
80
|
`dotenv-diff` warns when a `.env*` file contains the same key multiple times. The last occurrence wins. Suppress these warnings with `--allow-duplicates`.
|
|
45
81
|
|
|
82
|
+
## Only show specific categories
|
|
83
|
+
|
|
84
|
+
Use the `--only` flag to restrict the comparison to specific categories. For example:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
dotenv-diff --only missing,extra
|
|
88
|
+
```
|
|
89
|
+
This will only show missing and extra keys, ignoring empty, mismatched, duplicate keys and so on.
|
|
90
|
+
|
|
46
91
|
## Ignore specific keys
|
|
47
92
|
|
|
48
93
|
Exclude certain keys from the comparison using `--ignore` for exact names or `--ignore-regex` for patterns:
|
package/dist/src/cli/program.js
CHANGED
|
@@ -11,5 +11,11 @@ export function createProgram() {
|
|
|
11
11
|
.option('--allow-duplicates', 'Do not warn about duplicate keys in .env* files')
|
|
12
12
|
.option('--ignore <keys>', 'Comma-separated list of keys to ignore')
|
|
13
13
|
.option('--ignore-regex <pattern>', 'Regex pattern to ignore matching keys')
|
|
14
|
-
.option('--json', 'Output results in JSON format')
|
|
14
|
+
.option('--json', 'Output results in JSON format')
|
|
15
|
+
.option('--only <list>', 'Comma-separated categories to only run (missing,extra,empty,mismatch,duplicate,gitignore)')
|
|
16
|
+
.option('--scan-usage', 'Scan codebase for environment variable usage')
|
|
17
|
+
.option('--include-files <patterns>', '[requires --scan-usage] Comma-separated file patterns to include in scan (default: **/*.{js,ts,jsx,tsx,vue,svelte})')
|
|
18
|
+
.option('--exclude-files <patterns>', '[requires --scan-usage] Comma-separated file patterns to exclude from scan')
|
|
19
|
+
.option('--show-unused', '[requires --scan-usage] Show variables defined in .env but not used in code')
|
|
20
|
+
.option('--show-stats', 'Show statistics (in scan-usage mode: scan stats, in compare mode: env compare stats)');
|
|
15
21
|
}
|
package/dist/src/cli/run.js
CHANGED
|
@@ -6,10 +6,26 @@ import { discoverEnvFiles } from '../services/envDiscovery.js';
|
|
|
6
6
|
import { pairWithExample } from '../services/envPairing.js';
|
|
7
7
|
import { ensureFilesOrPrompt } from '../commands/init.js';
|
|
8
8
|
import { compareMany } from '../commands/compare.js';
|
|
9
|
+
import { scanUsage } from '../commands/scanUsage.js';
|
|
9
10
|
export async function run(program) {
|
|
10
11
|
program.parse(process.argv);
|
|
11
12
|
const raw = program.opts();
|
|
12
13
|
const opts = normalizeOptions(raw);
|
|
14
|
+
if (opts.scanUsage) {
|
|
15
|
+
const envPath = opts.envFlag || (fs.existsSync('.env') ? '.env' : undefined);
|
|
16
|
+
const { exitWithError } = await scanUsage({
|
|
17
|
+
cwd: opts.cwd,
|
|
18
|
+
include: opts.includeFiles,
|
|
19
|
+
exclude: opts.excludeFiles,
|
|
20
|
+
ignore: opts.ignore,
|
|
21
|
+
ignoreRegex: opts.ignoreRegex,
|
|
22
|
+
envPath,
|
|
23
|
+
json: opts.json,
|
|
24
|
+
showUnused: opts.showUnused,
|
|
25
|
+
showStats: opts.showStats,
|
|
26
|
+
});
|
|
27
|
+
process.exit(exitWithError ? 1 : 0);
|
|
28
|
+
}
|
|
13
29
|
// Special-case: both flags → direct comparison of exactly those two files
|
|
14
30
|
if (opts.envFlag && opts.exampleFlag) {
|
|
15
31
|
const envExists = fs.existsSync(opts.envFlag);
|
|
@@ -37,6 +53,8 @@ export async function run(program) {
|
|
|
37
53
|
json: opts.json,
|
|
38
54
|
ignore: opts.ignore,
|
|
39
55
|
ignoreRegex: opts.ignoreRegex,
|
|
56
|
+
only: opts.only,
|
|
57
|
+
showStats: opts.showStats,
|
|
40
58
|
collect: (e) => report.push(e),
|
|
41
59
|
});
|
|
42
60
|
if (opts.json) {
|
|
@@ -75,6 +93,8 @@ export async function run(program) {
|
|
|
75
93
|
json: opts.json,
|
|
76
94
|
ignore: opts.ignore,
|
|
77
95
|
ignoreRegex: opts.ignoreRegex,
|
|
96
|
+
only: opts.only,
|
|
97
|
+
showStats: opts.showStats,
|
|
78
98
|
collect: (e) => report.push(e),
|
|
79
99
|
});
|
|
80
100
|
if (opts.json) {
|
|
@@ -1,29 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
env: string;
|
|
3
|
-
example: string;
|
|
4
|
-
skipped?: {
|
|
5
|
-
reason: string;
|
|
6
|
-
};
|
|
7
|
-
duplicates?: {
|
|
8
|
-
env?: Array<{
|
|
9
|
-
key: string;
|
|
10
|
-
count: number;
|
|
11
|
-
}>;
|
|
12
|
-
example?: Array<{
|
|
13
|
-
key: string;
|
|
14
|
-
count: number;
|
|
15
|
-
}>;
|
|
16
|
-
};
|
|
17
|
-
missing?: string[];
|
|
18
|
-
extra?: string[];
|
|
19
|
-
empty?: string[];
|
|
20
|
-
valueMismatches?: Array<{
|
|
21
|
-
key: string;
|
|
22
|
-
expected: string;
|
|
23
|
-
actual: string;
|
|
24
|
-
}>;
|
|
25
|
-
ok?: boolean;
|
|
26
|
-
};
|
|
1
|
+
import type { Category, CompareJsonEntry } from '../config/types.js';
|
|
27
2
|
export declare function compareMany(pairs: Array<{
|
|
28
3
|
envName: string;
|
|
29
4
|
envPath: string;
|
|
@@ -36,6 +11,8 @@ export declare function compareMany(pairs: Array<{
|
|
|
36
11
|
ignore: string[];
|
|
37
12
|
ignoreRegex: RegExp[];
|
|
38
13
|
collect?: (entry: CompareJsonEntry) => void;
|
|
14
|
+
only?: Category[];
|
|
15
|
+
showStats?: boolean;
|
|
39
16
|
}): Promise<{
|
|
40
17
|
exitWithError: boolean;
|
|
41
18
|
}>;
|
|
@@ -8,6 +8,18 @@ import { findDuplicateKeys } from '../services/duplicates.js';
|
|
|
8
8
|
import { filterIgnoredKeys } from '../core/filterIgnoredKeys.js';
|
|
9
9
|
export async function compareMany(pairs, opts) {
|
|
10
10
|
let exitWithError = false;
|
|
11
|
+
const onlySet = opts.only?.length
|
|
12
|
+
? new Set(opts.only)
|
|
13
|
+
: undefined;
|
|
14
|
+
const run = (cat) => !onlySet || onlySet.has(cat);
|
|
15
|
+
const totals = {
|
|
16
|
+
missing: 0,
|
|
17
|
+
extra: 0,
|
|
18
|
+
empty: 0,
|
|
19
|
+
mismatch: 0,
|
|
20
|
+
duplicate: 0,
|
|
21
|
+
gitignore: 0,
|
|
22
|
+
};
|
|
11
23
|
for (const { envName, envPath, examplePath } of pairs) {
|
|
12
24
|
const exampleName = path.basename(examplePath);
|
|
13
25
|
const entry = { env: envName, example: exampleName };
|
|
@@ -25,36 +37,45 @@ export async function compareMany(pairs, opts) {
|
|
|
25
37
|
console.log(chalk.bold(`🔍 Comparing ${envName} ↔ ${exampleName}...`));
|
|
26
38
|
}
|
|
27
39
|
// Git ignore hint (only when not JSON)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
let gitignoreUnsafe = false;
|
|
41
|
+
if (run('gitignore')) {
|
|
42
|
+
warnIfEnvNotIgnored({
|
|
43
|
+
cwd: opts.cwd,
|
|
44
|
+
envFile: envName,
|
|
45
|
+
log: (msg) => {
|
|
46
|
+
gitignoreUnsafe = true;
|
|
47
|
+
if (!opts.json)
|
|
48
|
+
console.log(msg.replace(/^/gm, ' '));
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// still call to keep previous hints? No—masked by --only.
|
|
54
|
+
}
|
|
55
|
+
// Duplicate detection (skip entirely if --only excludes it)
|
|
56
|
+
let dupsEnv = [];
|
|
57
|
+
let dupsEx = [];
|
|
58
|
+
if (!opts.allowDuplicates && run('duplicate')) {
|
|
59
|
+
dupsEnv = findDuplicateKeys(envPath).filter(({ key }) => !opts.ignore.includes(key) &&
|
|
39
60
|
!opts.ignoreRegex.some((rx) => rx.test(key)));
|
|
40
|
-
|
|
61
|
+
dupsEx = findDuplicateKeys(examplePath).filter(({ key }) => !opts.ignore.includes(key) &&
|
|
41
62
|
!opts.ignoreRegex.some((rx) => rx.test(key)));
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
63
|
+
}
|
|
64
|
+
if (dupsEnv.length || dupsEx.length) {
|
|
65
|
+
entry.duplicates = {};
|
|
66
|
+
}
|
|
67
|
+
if (dupsEnv.length) {
|
|
68
|
+
entry.duplicates.env = dupsEnv;
|
|
69
|
+
if (!opts.json) {
|
|
70
|
+
console.log(chalk.yellow(` ⚠️ Duplicate keys in ${envName} (last occurrence wins):`));
|
|
71
|
+
dupsEnv.forEach(({ key, count }) => console.log(chalk.yellow(` - ${key} (${count} occurrences)`)));
|
|
51
72
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
73
|
+
}
|
|
74
|
+
if (dupsEx.length) {
|
|
75
|
+
entry.duplicates.example = dupsEx;
|
|
76
|
+
if (!opts.json) {
|
|
77
|
+
console.log(chalk.yellow(` ⚠️ Duplicate keys in ${exampleName} (last occurrence wins):`));
|
|
78
|
+
dupsEx.forEach(({ key, count }) => console.log(chalk.yellow(` - ${key} (${count} occurrences)`)));
|
|
58
79
|
}
|
|
59
80
|
}
|
|
60
81
|
// Diff + empty
|
|
@@ -68,10 +89,19 @@ export async function compareMany(pairs, opts) {
|
|
|
68
89
|
const emptyKeys = Object.entries(current)
|
|
69
90
|
.filter(([, v]) => (v ?? '').trim() === '')
|
|
70
91
|
.map(([k]) => k);
|
|
71
|
-
const
|
|
72
|
-
diff.
|
|
73
|
-
|
|
74
|
-
|
|
92
|
+
const filtered = {
|
|
93
|
+
missing: run('missing') ? diff.missing : [],
|
|
94
|
+
extra: run('extra') ? diff.extra : [],
|
|
95
|
+
empty: run('empty') ? emptyKeys : [],
|
|
96
|
+
mismatches: run('mismatch') && opts.checkValues ? diff.valueMismatches : [],
|
|
97
|
+
duplicatesEnv: run('duplicate') ? dupsEnv : [],
|
|
98
|
+
duplicatesEx: run('duplicate') ? dupsEx : [],
|
|
99
|
+
gitignoreUnsafe: run('gitignore') ? gitignoreUnsafe : false,
|
|
100
|
+
};
|
|
101
|
+
const allOk = filtered.missing.length === 0 &&
|
|
102
|
+
filtered.extra.length === 0 &&
|
|
103
|
+
filtered.empty.length === 0 &&
|
|
104
|
+
filtered.mismatches.length === 0;
|
|
75
105
|
if (allOk) {
|
|
76
106
|
entry.ok = true;
|
|
77
107
|
if (!opts.json) {
|
|
@@ -81,33 +111,72 @@ export async function compareMany(pairs, opts) {
|
|
|
81
111
|
opts.collect?.(entry);
|
|
82
112
|
continue;
|
|
83
113
|
}
|
|
84
|
-
|
|
85
|
-
|
|
114
|
+
// --- Stats block for compare mode when --show-stats is active ---
|
|
115
|
+
if (opts.showStats && !opts.json) {
|
|
116
|
+
const envCount = currentKeys.length;
|
|
117
|
+
const exampleCount = exampleKeys.length;
|
|
118
|
+
const sharedCount = new Set(currentKeys.filter((k) => exampleKeys.includes(k))).size;
|
|
119
|
+
// Duplicate "occurrences beyond the first", summed across both files
|
|
120
|
+
const duplicateCount = [...dupsEnv, ...dupsEx].reduce((acc, { count }) => acc + Math.max(0, count - 1), 0);
|
|
121
|
+
const valueMismatchCount = opts.checkValues
|
|
122
|
+
? filtered.mismatches.length
|
|
123
|
+
: 0;
|
|
124
|
+
console.log(chalk.bold(' 📊 Compare Statistics:'));
|
|
125
|
+
console.log(chalk.gray(` Keys in ${envName}: ${envCount}`));
|
|
126
|
+
console.log(chalk.gray(` Keys in ${exampleName}: ${exampleCount}`));
|
|
127
|
+
console.log(chalk.gray(` Shared keys: ${sharedCount}`));
|
|
128
|
+
console.log(chalk.gray(` Missing (in ${envName}): ${filtered.missing.length}`));
|
|
129
|
+
console.log(chalk.gray(` Extra (not in ${exampleName}): ${filtered.extra.length}`));
|
|
130
|
+
console.log(chalk.gray(` Empty values: ${filtered.empty.length}`));
|
|
131
|
+
console.log(chalk.gray(` Duplicate keys: ${duplicateCount}`));
|
|
132
|
+
console.log(chalk.gray(` Value mismatches: ${valueMismatchCount}`));
|
|
133
|
+
console.log();
|
|
134
|
+
}
|
|
135
|
+
if (filtered.missing.length) {
|
|
136
|
+
entry.missing = filtered.missing;
|
|
86
137
|
exitWithError = true;
|
|
138
|
+
totals.missing += filtered.missing.length;
|
|
87
139
|
}
|
|
88
|
-
if (
|
|
89
|
-
entry.extra =
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
140
|
+
if (filtered.extra.length) {
|
|
141
|
+
entry.extra = filtered.extra;
|
|
142
|
+
exitWithError = true;
|
|
143
|
+
totals.extra += filtered.extra.length;
|
|
144
|
+
}
|
|
145
|
+
if (filtered.empty.length) {
|
|
146
|
+
entry.empty = filtered.empty;
|
|
147
|
+
exitWithError = true;
|
|
148
|
+
totals.empty += filtered.empty.length;
|
|
149
|
+
}
|
|
150
|
+
if (filtered.mismatches.length) {
|
|
151
|
+
entry.valueMismatches = filtered.mismatches;
|
|
152
|
+
totals.mismatch += filtered.mismatches.length;
|
|
153
|
+
exitWithError = true;
|
|
154
|
+
}
|
|
155
|
+
if (filtered.duplicatesEnv.length || filtered.duplicatesEx.length) {
|
|
156
|
+
totals.duplicate +=
|
|
157
|
+
filtered.duplicatesEnv.length + filtered.duplicatesEx.length;
|
|
158
|
+
exitWithError = true;
|
|
159
|
+
}
|
|
160
|
+
if (filtered.gitignoreUnsafe) {
|
|
161
|
+
totals.gitignore += 1;
|
|
162
|
+
exitWithError = true;
|
|
94
163
|
}
|
|
95
164
|
if (!opts.json) {
|
|
96
|
-
if (
|
|
165
|
+
if (filtered.missing.length) {
|
|
97
166
|
console.log(chalk.red(' ❌ Missing keys:'));
|
|
98
|
-
|
|
167
|
+
filtered.missing.forEach((key) => console.log(chalk.red(` - ${key}`)));
|
|
99
168
|
}
|
|
100
|
-
if (
|
|
169
|
+
if (filtered.extra.length) {
|
|
101
170
|
console.log(chalk.yellow(' ⚠️ Extra keys (not in example):'));
|
|
102
|
-
|
|
171
|
+
filtered.extra.forEach((key) => console.log(chalk.yellow(` - ${key}`)));
|
|
103
172
|
}
|
|
104
|
-
if (
|
|
173
|
+
if (filtered.empty.length) {
|
|
105
174
|
console.log(chalk.yellow(' ⚠️ Empty values:'));
|
|
106
|
-
|
|
175
|
+
filtered.empty.forEach((key) => console.log(chalk.yellow(` - ${key}`)));
|
|
107
176
|
}
|
|
108
|
-
if (
|
|
177
|
+
if (filtered.mismatches.length) {
|
|
109
178
|
console.log(chalk.yellow(' ⚠️ Value mismatches:'));
|
|
110
|
-
|
|
179
|
+
filtered.mismatches.forEach(({ key, expected, actual }) => console.log(chalk.yellow(` - ${key}: expected '${expected}', but got '${actual}'`)));
|
|
111
180
|
}
|
|
112
181
|
console.log();
|
|
113
182
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type ScanOptions } from '../services/codeBaseScanner.js';
|
|
2
|
+
export interface ScanUsageOptions extends ScanOptions {
|
|
3
|
+
envPath?: string;
|
|
4
|
+
json: boolean;
|
|
5
|
+
showUnused: boolean;
|
|
6
|
+
showStats: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface ScanJsonEntry {
|
|
9
|
+
stats: {
|
|
10
|
+
filesScanned: number;
|
|
11
|
+
totalUsages: number;
|
|
12
|
+
uniqueVariables: number;
|
|
13
|
+
};
|
|
14
|
+
missing: Array<{
|
|
15
|
+
variable: string;
|
|
16
|
+
usages: Array<{
|
|
17
|
+
file: string;
|
|
18
|
+
line: number;
|
|
19
|
+
pattern: string;
|
|
20
|
+
context: string;
|
|
21
|
+
}>;
|
|
22
|
+
}>;
|
|
23
|
+
unused: string[];
|
|
24
|
+
allUsages?: Array<{
|
|
25
|
+
variable: string;
|
|
26
|
+
file: string;
|
|
27
|
+
line: number;
|
|
28
|
+
pattern: string;
|
|
29
|
+
context: string;
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Scans codebase for environment variable usage and compares with .env file
|
|
34
|
+
* @param {ScanUsageOptions} opts - Scan configuration options
|
|
35
|
+
* @param {string} [opts.envPath] - Path to .env file for comparison
|
|
36
|
+
* @param {boolean} opts.json - Output as JSON instead of console
|
|
37
|
+
* @param {boolean} opts.showUnused - Show unused variables from .env
|
|
38
|
+
* @param {boolean} opts.showStats - Show detailed statistics
|
|
39
|
+
* @returns {Promise<{exitWithError: boolean}>} Returns true if missing variables found
|
|
40
|
+
*/
|
|
41
|
+
export declare function scanUsage(opts: ScanUsageOptions): Promise<{
|
|
42
|
+
exitWithError: boolean;
|
|
43
|
+
}>;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { parseEnvFile } from '../lib/parseEnv.js';
|
|
3
|
+
import { scanCodebase, compareWithEnvFiles, } from '../services/codeBaseScanner.js';
|
|
4
|
+
import { filterIgnoredKeys } from '../core/filterIgnoredKeys.js';
|
|
5
|
+
/**
|
|
6
|
+
* Scans codebase for environment variable usage and compares with .env file
|
|
7
|
+
* @param {ScanUsageOptions} opts - Scan configuration options
|
|
8
|
+
* @param {string} [opts.envPath] - Path to .env file for comparison
|
|
9
|
+
* @param {boolean} opts.json - Output as JSON instead of console
|
|
10
|
+
* @param {boolean} opts.showUnused - Show unused variables from .env
|
|
11
|
+
* @param {boolean} opts.showStats - Show detailed statistics
|
|
12
|
+
* @returns {Promise<{exitWithError: boolean}>} Returns true if missing variables found
|
|
13
|
+
*/
|
|
14
|
+
export async function scanUsage(opts) {
|
|
15
|
+
if (!opts.json) {
|
|
16
|
+
console.log(chalk.bold('🔍 Scanning codebase for environment variable usage...'));
|
|
17
|
+
console.log();
|
|
18
|
+
}
|
|
19
|
+
// Scan the codebase
|
|
20
|
+
let scanResult = await scanCodebase(opts);
|
|
21
|
+
// If we have an env file, compare with it
|
|
22
|
+
let envVariables = {};
|
|
23
|
+
if (opts.envPath) {
|
|
24
|
+
try {
|
|
25
|
+
const envFull = parseEnvFile(opts.envPath);
|
|
26
|
+
const envKeys = filterIgnoredKeys(Object.keys(envFull), opts.ignore, opts.ignoreRegex);
|
|
27
|
+
envVariables = Object.fromEntries(envKeys.map((k) => [k, envFull[k]]));
|
|
28
|
+
scanResult = compareWithEnvFiles(scanResult, envVariables);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (!opts.json) {
|
|
32
|
+
console.log(chalk.yellow(`⚠️ Could not read env file: ${opts.envPath} - ${error}`));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Prepare JSON output
|
|
37
|
+
if (opts.json) {
|
|
38
|
+
const jsonOutput = createJsonOutput(scanResult, opts);
|
|
39
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
40
|
+
return { exitWithError: scanResult.missing.length > 0 };
|
|
41
|
+
}
|
|
42
|
+
// Console output
|
|
43
|
+
return outputToConsole(scanResult, opts);
|
|
44
|
+
}
|
|
45
|
+
function createJsonOutput(scanResult, opts) {
|
|
46
|
+
// Group usages by variable for missing variables
|
|
47
|
+
const missingGrouped = scanResult.missing.map((variable) => ({
|
|
48
|
+
variable,
|
|
49
|
+
usages: scanResult.used
|
|
50
|
+
.filter((u) => u.variable === variable)
|
|
51
|
+
.map((u) => ({
|
|
52
|
+
file: u.file,
|
|
53
|
+
line: u.line,
|
|
54
|
+
pattern: u.pattern,
|
|
55
|
+
context: u.context,
|
|
56
|
+
})),
|
|
57
|
+
}));
|
|
58
|
+
const output = {
|
|
59
|
+
stats: scanResult.stats,
|
|
60
|
+
missing: missingGrouped,
|
|
61
|
+
unused: scanResult.unused,
|
|
62
|
+
};
|
|
63
|
+
// Optionally include all usages
|
|
64
|
+
if (opts.showStats) {
|
|
65
|
+
output.allUsages = scanResult.used.map((u) => ({
|
|
66
|
+
variable: u.variable,
|
|
67
|
+
file: u.file,
|
|
68
|
+
line: u.line,
|
|
69
|
+
pattern: u.pattern,
|
|
70
|
+
context: u.context,
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
return output;
|
|
74
|
+
}
|
|
75
|
+
function outputToConsole(scanResult, opts) {
|
|
76
|
+
let exitWithError = false;
|
|
77
|
+
// Show stats if requested
|
|
78
|
+
if (opts.showStats) {
|
|
79
|
+
console.log(chalk.bold('📊 Scan Statistics:'));
|
|
80
|
+
console.log(chalk.gray(` Files scanned: ${scanResult.stats.filesScanned}`));
|
|
81
|
+
console.log(chalk.gray(` Total usages found: ${scanResult.stats.totalUsages}`));
|
|
82
|
+
console.log(chalk.gray(` Unique variables: ${scanResult.stats.uniqueVariables}`));
|
|
83
|
+
console.log();
|
|
84
|
+
}
|
|
85
|
+
// Always show found variables when not in .env comparison mode
|
|
86
|
+
if (!opts.envPath || scanResult.missing.length === 0) {
|
|
87
|
+
console.log(chalk.green(`✅ Found ${scanResult.stats.uniqueVariables} unique environment variables in use`));
|
|
88
|
+
console.log();
|
|
89
|
+
// List all variables found (if any)
|
|
90
|
+
if (scanResult.stats.uniqueVariables > 0) {
|
|
91
|
+
// Group by variable to get unique list
|
|
92
|
+
const variableUsages = scanResult.used.reduce((acc, usage) => {
|
|
93
|
+
if (!acc[usage.variable]) {
|
|
94
|
+
acc[usage.variable] = [];
|
|
95
|
+
}
|
|
96
|
+
acc[usage.variable].push(usage);
|
|
97
|
+
return acc;
|
|
98
|
+
}, {});
|
|
99
|
+
// Display each unique variable
|
|
100
|
+
for (const [variable, usages] of Object.entries(variableUsages)) {
|
|
101
|
+
console.log(chalk.blue(` ${variable}`));
|
|
102
|
+
// Show usage details if stats are enabled
|
|
103
|
+
if (opts.showStats) {
|
|
104
|
+
const displayUsages = usages.slice(0, 2);
|
|
105
|
+
displayUsages.forEach((usage) => {
|
|
106
|
+
console.log(chalk.gray(` Used in: ${usage.file}:${usage.line} (${usage.pattern})`));
|
|
107
|
+
});
|
|
108
|
+
if (usages.length > 2) {
|
|
109
|
+
console.log(chalk.gray(` ... and ${usages.length - 2} more locations`));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
console.log();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Missing variables (used in code but not in .env)
|
|
117
|
+
if (scanResult.missing.length > 0) {
|
|
118
|
+
exitWithError = true;
|
|
119
|
+
console.log(chalk.red('❌ Missing in .env:'));
|
|
120
|
+
const grouped = scanResult.missing.reduce((acc, variable) => {
|
|
121
|
+
const usages = scanResult.used.filter((u) => u.variable === variable);
|
|
122
|
+
acc[variable] = usages;
|
|
123
|
+
return acc;
|
|
124
|
+
}, {});
|
|
125
|
+
for (const [variable, usages] of Object.entries(grouped)) {
|
|
126
|
+
console.log(chalk.red(` - ${variable}`));
|
|
127
|
+
// Show first few usages
|
|
128
|
+
const maxShow = 3;
|
|
129
|
+
usages.slice(0, maxShow).forEach((usage) => {
|
|
130
|
+
console.log(chalk.gray(` Used in: ${usage.file}:${usage.line} (${usage.pattern})`));
|
|
131
|
+
});
|
|
132
|
+
if (usages.length > maxShow) {
|
|
133
|
+
console.log(chalk.gray(` ... and ${usages.length - maxShow} more locations`));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
console.log();
|
|
137
|
+
}
|
|
138
|
+
// Unused variables (in .env but not used in code)
|
|
139
|
+
if (opts.showUnused && scanResult.unused.length > 0) {
|
|
140
|
+
console.log(chalk.yellow('⚠️ Unused in codebase:'));
|
|
141
|
+
scanResult.unused.forEach((variable) => {
|
|
142
|
+
console.log(chalk.yellow(` - ${variable}`));
|
|
143
|
+
});
|
|
144
|
+
console.log();
|
|
145
|
+
}
|
|
146
|
+
// Success message for .env comparison
|
|
147
|
+
if (opts.envPath && scanResult.missing.length === 0) {
|
|
148
|
+
console.log(chalk.green('✅ All used environment variables are defined in .env'));
|
|
149
|
+
if (opts.showUnused && scanResult.unused.length === 0) {
|
|
150
|
+
console.log(chalk.green('✅ No unused environment variables found'));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return { exitWithError };
|
|
154
|
+
}
|
|
@@ -1,25 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
checkValues: boolean;
|
|
3
|
-
isCiMode: boolean;
|
|
4
|
-
isYesMode: boolean;
|
|
5
|
-
allowDuplicates: boolean;
|
|
6
|
-
json: boolean;
|
|
7
|
-
envFlag: string | null;
|
|
8
|
-
exampleFlag: string | null;
|
|
9
|
-
ignore: string[];
|
|
10
|
-
ignoreRegex: RegExp[];
|
|
11
|
-
cwd: string;
|
|
12
|
-
};
|
|
13
|
-
type RawOptions = {
|
|
14
|
-
checkValues?: boolean;
|
|
15
|
-
ci?: boolean;
|
|
16
|
-
yes?: boolean;
|
|
17
|
-
allowDuplicates?: boolean;
|
|
18
|
-
json?: boolean;
|
|
19
|
-
env?: string;
|
|
20
|
-
example?: string;
|
|
21
|
-
ignore?: string | string[];
|
|
22
|
-
ignoreRegex?: string | string[];
|
|
23
|
-
};
|
|
1
|
+
import { Options, RawOptions } from './types.js';
|
|
24
2
|
export declare function normalizeOptions(raw: RawOptions): Options;
|
|
25
|
-
export {};
|
|
@@ -1,18 +1,35 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { ALLOWED_CATEGORIES } from './types.js';
|
|
4
|
+
function parseList(val) {
|
|
5
|
+
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
|
6
|
+
return arr
|
|
7
|
+
.flatMap((s) => String(s).split(','))
|
|
8
|
+
.map((s) => s.trim())
|
|
9
|
+
.filter(Boolean);
|
|
10
|
+
}
|
|
11
|
+
function parseCategories(val, flagName = '') {
|
|
12
|
+
const raw = parseList(val);
|
|
13
|
+
const bad = raw.filter((c) => !ALLOWED_CATEGORIES.includes(c));
|
|
14
|
+
if (bad.length) {
|
|
15
|
+
console.error(chalk.red(`❌ Error: invalid ${flagName} value(s): ${bad.join(', ')}. Allowed: ${ALLOWED_CATEGORIES.join(', ')}`));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
return raw;
|
|
19
|
+
}
|
|
3
20
|
export function normalizeOptions(raw) {
|
|
4
21
|
const checkValues = raw.checkValues ?? false;
|
|
5
22
|
const isCiMode = Boolean(raw.ci);
|
|
6
23
|
const isYesMode = Boolean(raw.yes);
|
|
7
24
|
const allowDuplicates = Boolean(raw.allowDuplicates);
|
|
8
25
|
const json = Boolean(raw.json);
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
26
|
+
const onlyParsed = parseCategories(raw.only, '--only');
|
|
27
|
+
const only = onlyParsed.length ? onlyParsed : undefined;
|
|
28
|
+
const scanUsage = Boolean(raw.scanUsage);
|
|
29
|
+
const includeFiles = parseList(raw.includeFiles);
|
|
30
|
+
const excludeFiles = parseList(raw.excludeFiles);
|
|
31
|
+
const showUnused = Boolean(raw.showUnused);
|
|
32
|
+
const showStats = Boolean(raw.showStats);
|
|
16
33
|
const ignore = parseList(raw.ignore);
|
|
17
34
|
const ignoreRegex = [];
|
|
18
35
|
for (const pattern of parseList(raw.ignoreRegex)) {
|
|
@@ -41,5 +58,11 @@ export function normalizeOptions(raw) {
|
|
|
41
58
|
ignore,
|
|
42
59
|
ignoreRegex,
|
|
43
60
|
cwd,
|
|
61
|
+
only,
|
|
62
|
+
scanUsage,
|
|
63
|
+
includeFiles,
|
|
64
|
+
excludeFiles,
|
|
65
|
+
showUnused,
|
|
66
|
+
showStats,
|
|
44
67
|
};
|
|
45
68
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export declare const ALLOWED_CATEGORIES: readonly ["missing", "extra", "empty", "mismatch", "duplicate", "gitignore"];
|
|
2
|
+
export type Category = (typeof ALLOWED_CATEGORIES)[number];
|
|
3
|
+
export type Options = {
|
|
4
|
+
checkValues: boolean;
|
|
5
|
+
isCiMode: boolean;
|
|
6
|
+
isYesMode: boolean;
|
|
7
|
+
allowDuplicates: boolean;
|
|
8
|
+
json: boolean;
|
|
9
|
+
envFlag: string | null;
|
|
10
|
+
exampleFlag: string | null;
|
|
11
|
+
ignore: string[];
|
|
12
|
+
ignoreRegex: RegExp[];
|
|
13
|
+
cwd: string;
|
|
14
|
+
only?: Category[];
|
|
15
|
+
scanUsage: boolean;
|
|
16
|
+
includeFiles: string[];
|
|
17
|
+
excludeFiles: string[];
|
|
18
|
+
showUnused: boolean;
|
|
19
|
+
showStats: boolean;
|
|
20
|
+
};
|
|
21
|
+
export type RawOptions = {
|
|
22
|
+
checkValues?: boolean;
|
|
23
|
+
ci?: boolean;
|
|
24
|
+
yes?: boolean;
|
|
25
|
+
allowDuplicates?: boolean;
|
|
26
|
+
json?: boolean;
|
|
27
|
+
env?: string;
|
|
28
|
+
example?: string;
|
|
29
|
+
ignore?: string | string[];
|
|
30
|
+
ignoreRegex?: string | string[];
|
|
31
|
+
only?: string | string[];
|
|
32
|
+
scanUsage?: boolean;
|
|
33
|
+
includeFiles?: string | string[];
|
|
34
|
+
excludeFiles?: string | string[];
|
|
35
|
+
showUnused?: boolean;
|
|
36
|
+
showStats?: boolean;
|
|
37
|
+
};
|
|
38
|
+
export type CompareJsonEntry = {
|
|
39
|
+
env: string;
|
|
40
|
+
example: string;
|
|
41
|
+
skipped?: {
|
|
42
|
+
reason: string;
|
|
43
|
+
};
|
|
44
|
+
duplicates?: {
|
|
45
|
+
env?: Array<{
|
|
46
|
+
key: string;
|
|
47
|
+
count: number;
|
|
48
|
+
}>;
|
|
49
|
+
example?: Array<{
|
|
50
|
+
key: string;
|
|
51
|
+
count: number;
|
|
52
|
+
}>;
|
|
53
|
+
};
|
|
54
|
+
missing?: string[];
|
|
55
|
+
extra?: string[];
|
|
56
|
+
empty?: string[];
|
|
57
|
+
valueMismatches?: Array<{
|
|
58
|
+
key: string;
|
|
59
|
+
expected: string;
|
|
60
|
+
actual: string;
|
|
61
|
+
}>;
|
|
62
|
+
ok?: boolean;
|
|
63
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface EnvUsage {
|
|
2
|
+
variable: string;
|
|
3
|
+
file: string;
|
|
4
|
+
line: number;
|
|
5
|
+
column: number;
|
|
6
|
+
pattern: 'process.env' | 'import.meta.env' | 'sveltekit' | 'deno' | 'next' | 'nuxt';
|
|
7
|
+
context: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ScanOptions {
|
|
10
|
+
cwd: string;
|
|
11
|
+
include: string[];
|
|
12
|
+
exclude: string[];
|
|
13
|
+
ignore: string[];
|
|
14
|
+
ignoreRegex: RegExp[];
|
|
15
|
+
}
|
|
16
|
+
export interface ScanResult {
|
|
17
|
+
used: EnvUsage[];
|
|
18
|
+
missing: string[];
|
|
19
|
+
unused: string[];
|
|
20
|
+
stats: {
|
|
21
|
+
filesScanned: number;
|
|
22
|
+
totalUsages: number;
|
|
23
|
+
uniqueVariables: number;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Scans the codebase for environment variable usage based on the provided options.
|
|
28
|
+
* @param opts - Options for scanning the codebase.
|
|
29
|
+
* @returns A promise that resolves to the scan result containing used, missing, and unused variables.
|
|
30
|
+
*/
|
|
31
|
+
export declare function scanCodebase(opts: ScanOptions): Promise<ScanResult>;
|
|
32
|
+
export declare function compareWithEnvFiles(scanResult: ScanResult, envVariables: Record<string, string | undefined>): ScanResult;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
// Framework-specific patterns for finding environment variable usage
|
|
4
|
+
const ENV_PATTERNS = [
|
|
5
|
+
{
|
|
6
|
+
name: 'process.env',
|
|
7
|
+
regex: /process\.env\.([A-Z_][A-Z0-9_]*)/g,
|
|
8
|
+
frameworks: ['node', 'next', 'general'],
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
name: 'import.meta.env',
|
|
12
|
+
regex: /import\.meta\.env\.([A-Z_][A-Z0-9_]*)/g,
|
|
13
|
+
frameworks: ['vite', 'svelte', 'vue'],
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'sveltekit',
|
|
17
|
+
regex: /\$env\/(?:static|dynamic)\/(?:private|public)\/([A-Z_][A-Z0-9_]*)/g,
|
|
18
|
+
frameworks: ['sveltekit'],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'deno',
|
|
22
|
+
regex: /Deno\.env\.get\(['"`]([A-Z_][A-Z0-9_]*)['"`]\)/g,
|
|
23
|
+
frameworks: ['deno'],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'next',
|
|
27
|
+
regex: /process\.env\.(NEXT_PUBLIC_[A-Z_][A-Z0-9_]*)/g,
|
|
28
|
+
frameworks: ['next'],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'nuxt',
|
|
32
|
+
regex: /(?:\$config|useRuntimeConfig\(\))\.([A-Z_][A-Z0-9_]*)/g,
|
|
33
|
+
frameworks: ['nuxt'],
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
const DEFAULT_INCLUDE_EXTENSIONS = [
|
|
37
|
+
'.js',
|
|
38
|
+
'.ts',
|
|
39
|
+
'.jsx',
|
|
40
|
+
'.tsx',
|
|
41
|
+
'.vue',
|
|
42
|
+
'.svelte',
|
|
43
|
+
'.mjs',
|
|
44
|
+
'.cjs',
|
|
45
|
+
];
|
|
46
|
+
const DEFAULT_EXCLUDE_PATTERNS = [
|
|
47
|
+
'node_modules',
|
|
48
|
+
'dist',
|
|
49
|
+
'build',
|
|
50
|
+
'.next',
|
|
51
|
+
'.nuxt',
|
|
52
|
+
'coverage',
|
|
53
|
+
'.git',
|
|
54
|
+
'.vscode',
|
|
55
|
+
'.idea',
|
|
56
|
+
'.test.',
|
|
57
|
+
'.spec.',
|
|
58
|
+
'__tests__',
|
|
59
|
+
'__mocks__',
|
|
60
|
+
];
|
|
61
|
+
/**
|
|
62
|
+
* Scans the codebase for environment variable usage based on the provided options.
|
|
63
|
+
* @param opts - Options for scanning the codebase.
|
|
64
|
+
* @returns A promise that resolves to the scan result containing used, missing, and unused variables.
|
|
65
|
+
*/
|
|
66
|
+
export async function scanCodebase(opts) {
|
|
67
|
+
const files = await findFiles(opts.cwd, {
|
|
68
|
+
include: opts.include,
|
|
69
|
+
exclude: [...DEFAULT_EXCLUDE_PATTERNS, ...opts.exclude],
|
|
70
|
+
});
|
|
71
|
+
const allUsages = [];
|
|
72
|
+
let filesScanned = 0;
|
|
73
|
+
for (const filePath of files) {
|
|
74
|
+
try {
|
|
75
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
76
|
+
const fileUsages = await scanFile(filePath, content, opts);
|
|
77
|
+
allUsages.push(...fileUsages);
|
|
78
|
+
filesScanned++;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Skip files we can't read (binary, permissions, etc.)
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Filter out ignored variables
|
|
86
|
+
const filteredUsages = allUsages.filter((usage) => !opts.ignore.includes(usage.variable) &&
|
|
87
|
+
!opts.ignoreRegex.some((regex) => regex.test(usage.variable)));
|
|
88
|
+
const uniqueVariables = [...new Set(filteredUsages.map((u) => u.variable))];
|
|
89
|
+
return {
|
|
90
|
+
used: filteredUsages,
|
|
91
|
+
missing: [],
|
|
92
|
+
unused: [],
|
|
93
|
+
stats: {
|
|
94
|
+
filesScanned,
|
|
95
|
+
totalUsages: filteredUsages.length,
|
|
96
|
+
uniqueVariables: uniqueVariables.length,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Recursively finds all files in the given directory matching the include patterns,
|
|
102
|
+
* while excluding files and directories that match the exclude patterns.
|
|
103
|
+
* @param rootDir The root directory to start searching from.
|
|
104
|
+
* @param opts Options for include and exclude patterns.
|
|
105
|
+
* @returns A promise that resolves to an array of file paths.
|
|
106
|
+
*/
|
|
107
|
+
async function findFiles(rootDir, opts) {
|
|
108
|
+
const files = [];
|
|
109
|
+
async function walk(currentDir) {
|
|
110
|
+
let entries;
|
|
111
|
+
try {
|
|
112
|
+
entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Skip directories we can't read (permissions, etc.)
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
120
|
+
const relativePath = path.relative(rootDir, fullPath);
|
|
121
|
+
// Check if should be excluded (directory or file)
|
|
122
|
+
if (shouldExclude(entry.name, relativePath, opts.exclude)) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (entry.isDirectory()) {
|
|
126
|
+
await walk(fullPath);
|
|
127
|
+
}
|
|
128
|
+
else if (entry.isFile() &&
|
|
129
|
+
shouldInclude(entry.name, relativePath, opts.include)) {
|
|
130
|
+
files.push(fullPath);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
await walk(rootDir);
|
|
135
|
+
return files;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Check if a file should be included based on its name, path, and include patterns.
|
|
139
|
+
* @param fileName The name of the file.
|
|
140
|
+
* @param relativePath The relative path of the file.
|
|
141
|
+
* @param patterns The include patterns to match against.
|
|
142
|
+
* @returns True if the file should be included, false otherwise.
|
|
143
|
+
*/
|
|
144
|
+
function shouldInclude(fileName, relativePath, patterns) {
|
|
145
|
+
// If no include patterns specified, use default extensions
|
|
146
|
+
if (!patterns.length) {
|
|
147
|
+
return DEFAULT_INCLUDE_EXTENSIONS.some((ext) => fileName.endsWith(ext));
|
|
148
|
+
}
|
|
149
|
+
return patterns.some((pattern) => {
|
|
150
|
+
if (pattern.startsWith('**')) {
|
|
151
|
+
// Handle **/*.ext patterns
|
|
152
|
+
const ext = pattern.substring(pattern.lastIndexOf('.'));
|
|
153
|
+
return fileName.endsWith(ext);
|
|
154
|
+
}
|
|
155
|
+
else if (pattern.includes('*')) {
|
|
156
|
+
// Simple glob pattern matching
|
|
157
|
+
return matchesGlobPattern(relativePath, pattern);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
// Exact match or extension
|
|
161
|
+
return relativePath.includes(pattern) || fileName.endsWith(pattern);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function shouldExclude(fileName, relativePath, patterns) {
|
|
166
|
+
// Check if filename or any part of the path should be excluded
|
|
167
|
+
return patterns.some((pattern) => {
|
|
168
|
+
// Direct name match (like 'node_modules')
|
|
169
|
+
if (fileName === pattern)
|
|
170
|
+
return true;
|
|
171
|
+
// Path contains pattern
|
|
172
|
+
if (relativePath.includes(pattern))
|
|
173
|
+
return true;
|
|
174
|
+
// Pattern matching for extensions and wildcards
|
|
175
|
+
if (pattern.includes('*')) {
|
|
176
|
+
return matchesGlobPattern(relativePath, pattern);
|
|
177
|
+
}
|
|
178
|
+
// Special case for test files
|
|
179
|
+
if (pattern.includes('.test.') && fileName.includes('.test.'))
|
|
180
|
+
return true;
|
|
181
|
+
if (pattern.includes('.spec.') && fileName.includes('.spec.'))
|
|
182
|
+
return true;
|
|
183
|
+
return false;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
function matchesGlobPattern(filePath, pattern) {
|
|
187
|
+
// Convert simple glob patterns to regex
|
|
188
|
+
// This handles basic cases like *.js, **/*.ts, etc.
|
|
189
|
+
const regexPattern = pattern
|
|
190
|
+
.replace(/\*\*/g, '.*') // ** matches any path
|
|
191
|
+
.replace(/\*/g, '[^/]*') // * matches any filename chars (not path separators)
|
|
192
|
+
.replace(/\./g, '\\.') // Escape dots
|
|
193
|
+
.replace(/\//g, '[/\\\\]'); // Handle both forward and back slashes
|
|
194
|
+
const regex = new RegExp(`^${regexPattern}$`, 'i'); // Case insensitive
|
|
195
|
+
return regex.test(filePath.replace(/\\/g, '/')); // Normalize path separators
|
|
196
|
+
}
|
|
197
|
+
async function scanFile(filePath, content, opts) {
|
|
198
|
+
const usages = [];
|
|
199
|
+
const lines = content.split('\n');
|
|
200
|
+
const relativePath = path.relative(opts.cwd, filePath);
|
|
201
|
+
for (const pattern of ENV_PATTERNS) {
|
|
202
|
+
let match;
|
|
203
|
+
const regex = new RegExp(pattern.regex.source, pattern.regex.flags);
|
|
204
|
+
while ((match = regex.exec(content)) !== null) {
|
|
205
|
+
const variable = match[1];
|
|
206
|
+
const matchIndex = match.index;
|
|
207
|
+
// Find line and column
|
|
208
|
+
const beforeMatch = content.substring(0, matchIndex);
|
|
209
|
+
const lineNumber = beforeMatch.split('\n').length;
|
|
210
|
+
const lastNewlineIndex = beforeMatch.lastIndexOf('\n');
|
|
211
|
+
const column = matchIndex - lastNewlineIndex;
|
|
212
|
+
// Get the context (the actual line)
|
|
213
|
+
const contextLine = lines[lineNumber - 1]?.trim() || '';
|
|
214
|
+
usages.push({
|
|
215
|
+
variable,
|
|
216
|
+
file: relativePath,
|
|
217
|
+
line: lineNumber,
|
|
218
|
+
column,
|
|
219
|
+
pattern: pattern.name,
|
|
220
|
+
context: contextLine,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return usages;
|
|
225
|
+
}
|
|
226
|
+
export function compareWithEnvFiles(scanResult, envVariables) {
|
|
227
|
+
const usedVariables = new Set(scanResult.used.map((u) => u.variable));
|
|
228
|
+
const envKeys = new Set(Object.keys(envVariables));
|
|
229
|
+
const missing = [...usedVariables].filter((v) => !envKeys.has(v));
|
|
230
|
+
const unused = [...envKeys].filter((v) => !usedVariables.has(v));
|
|
231
|
+
return {
|
|
232
|
+
...scanResult,
|
|
233
|
+
missing,
|
|
234
|
+
unused,
|
|
235
|
+
};
|
|
236
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dotenv-diff",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "A CLI tool to find differences between .env and .env.example / .env.* files.",
|
|
5
|
+
"description": "A CLI tool to find differences between .env and .env.example / .env.* files. And optionally scan your codebase to find out which environment variables are actually used in your code.",
|
|
6
6
|
"bin": {
|
|
7
7
|
"dotenv-diff": "dist/bin/dotenv-diff.js"
|
|
8
8
|
},
|