dotenv-diff 2.0.0 → 2.1.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 +21 -0
- package/dist/src/cli/run.js +2 -0
- package/dist/src/commands/scanUsage.d.ts +6 -0
- package/dist/src/commands/scanUsage.js +70 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,6 +42,27 @@ dotenv-diff --scan-usage
|
|
|
42
42
|
```
|
|
43
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
44
|
|
|
45
|
+
## CI/CD integration with `--ci` option
|
|
46
|
+
You can scan and compare against specific environment file, eg. `.env.example`
|
|
47
|
+
This is useful for CI/CD pipelines to ensure that the environment variables used in your code match those defined in your `.env.example` file.
|
|
48
|
+
|
|
49
|
+
Use the `--ci` flag for automated environments. This enables strict mode where the tool exits with code 1 on any issues, making it perfect for CI/CD pipelines.
|
|
50
|
+
|
|
51
|
+
And the `--example` option allows you to specify which `.env.example` file to compare against.
|
|
52
|
+
|
|
53
|
+
### Use it in Github Actions:
|
|
54
|
+
|
|
55
|
+
```yaml
|
|
56
|
+
- name: Check environment variables
|
|
57
|
+
run: dotenv-diff --scan-usage --example .env.example --ci
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
You can also change the comparison file by using the `--example` flag to point to a different `.env.example` file.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
dotenv-diff --scan-usage --example .env.example.staging --ci
|
|
64
|
+
```
|
|
65
|
+
|
|
45
66
|
## Show unused variables
|
|
46
67
|
|
|
47
68
|
Use `--show-unused` together with `--scan-usage` to list variables that are defined in `.env` but never used in your codebase:
|
package/dist/src/cli/run.js
CHANGED
|
@@ -19,10 +19,12 @@ export async function run(program) {
|
|
|
19
19
|
exclude: opts.excludeFiles,
|
|
20
20
|
ignore: opts.ignore,
|
|
21
21
|
ignoreRegex: opts.ignoreRegex,
|
|
22
|
+
examplePath: opts.exampleFlag || undefined,
|
|
22
23
|
envPath,
|
|
23
24
|
json: opts.json,
|
|
24
25
|
showUnused: opts.showUnused,
|
|
25
26
|
showStats: opts.showStats,
|
|
27
|
+
isCiMode: opts.isCiMode,
|
|
26
28
|
});
|
|
27
29
|
process.exit(exitWithError ? 1 : 0);
|
|
28
30
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { type ScanOptions } from '../services/codeBaseScanner.js';
|
|
2
2
|
export interface ScanUsageOptions extends ScanOptions {
|
|
3
3
|
envPath?: string;
|
|
4
|
+
examplePath?: string;
|
|
4
5
|
json: boolean;
|
|
5
6
|
showUnused: boolean;
|
|
6
7
|
showStats: boolean;
|
|
8
|
+
isCiMode?: boolean;
|
|
7
9
|
}
|
|
8
10
|
export interface ScanJsonEntry {
|
|
9
11
|
stats: {
|
|
@@ -28,14 +30,18 @@ export interface ScanJsonEntry {
|
|
|
28
30
|
pattern: string;
|
|
29
31
|
context: string;
|
|
30
32
|
}>;
|
|
33
|
+
comparedAgainst?: string;
|
|
34
|
+
totalEnvVariables?: number;
|
|
31
35
|
}
|
|
32
36
|
/**
|
|
33
37
|
* Scans codebase for environment variable usage and compares with .env file
|
|
34
38
|
* @param {ScanUsageOptions} opts - Scan configuration options
|
|
35
39
|
* @param {string} [opts.envPath] - Path to .env file for comparison
|
|
40
|
+
* @param {string} [opts.examplePath] - Path to .env.example file for comparison
|
|
36
41
|
* @param {boolean} opts.json - Output as JSON instead of console
|
|
37
42
|
* @param {boolean} opts.showUnused - Show unused variables from .env
|
|
38
43
|
* @param {boolean} opts.showStats - Show detailed statistics
|
|
44
|
+
* @param {boolean} [opts.isCiMode] - Run in CI mode (exit with error code)
|
|
39
45
|
* @returns {Promise<{exitWithError: boolean}>} Returns true if missing variables found
|
|
40
46
|
*/
|
|
41
47
|
export declare function scanUsage(opts: ScanUsageOptions): Promise<{
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
2
4
|
import { parseEnvFile } from '../lib/parseEnv.js';
|
|
3
5
|
import { scanCodebase, compareWithEnvFiles, } from '../services/codeBaseScanner.js';
|
|
4
6
|
import { filterIgnoredKeys } from '../core/filterIgnoredKeys.js';
|
|
@@ -6,9 +8,11 @@ import { filterIgnoredKeys } from '../core/filterIgnoredKeys.js';
|
|
|
6
8
|
* Scans codebase for environment variable usage and compares with .env file
|
|
7
9
|
* @param {ScanUsageOptions} opts - Scan configuration options
|
|
8
10
|
* @param {string} [opts.envPath] - Path to .env file for comparison
|
|
11
|
+
* @param {string} [opts.examplePath] - Path to .env.example file for comparison
|
|
9
12
|
* @param {boolean} opts.json - Output as JSON instead of console
|
|
10
13
|
* @param {boolean} opts.showUnused - Show unused variables from .env
|
|
11
14
|
* @param {boolean} opts.showStats - Show detailed statistics
|
|
15
|
+
* @param {boolean} [opts.isCiMode] - Run in CI mode (exit with error code)
|
|
12
16
|
* @returns {Promise<{exitWithError: boolean}>} Returns true if missing variables found
|
|
13
17
|
*/
|
|
14
18
|
export async function scanUsage(opts) {
|
|
@@ -18,31 +22,61 @@ export async function scanUsage(opts) {
|
|
|
18
22
|
}
|
|
19
23
|
// Scan the codebase
|
|
20
24
|
let scanResult = await scanCodebase(opts);
|
|
21
|
-
//
|
|
25
|
+
// Determine which file to compare against
|
|
26
|
+
const compareFile = determineComparisonFile(opts);
|
|
22
27
|
let envVariables = {};
|
|
23
|
-
|
|
28
|
+
let comparedAgainst = '';
|
|
29
|
+
if (compareFile) {
|
|
24
30
|
try {
|
|
25
|
-
const envFull = parseEnvFile(
|
|
31
|
+
const envFull = parseEnvFile(compareFile.path);
|
|
26
32
|
const envKeys = filterIgnoredKeys(Object.keys(envFull), opts.ignore, opts.ignoreRegex);
|
|
27
33
|
envVariables = Object.fromEntries(envKeys.map((k) => [k, envFull[k]]));
|
|
28
34
|
scanResult = compareWithEnvFiles(scanResult, envVariables);
|
|
35
|
+
comparedAgainst = compareFile.name;
|
|
29
36
|
}
|
|
30
37
|
catch (error) {
|
|
38
|
+
const errorMessage = `⚠️ Could not read ${compareFile.name}: ${compareFile.path} - ${error}`;
|
|
39
|
+
if (opts.isCiMode) {
|
|
40
|
+
// In CI mode, exit with error if file doesn't exist
|
|
41
|
+
console.error(chalk.red(`❌ ${errorMessage}`));
|
|
42
|
+
return { exitWithError: true };
|
|
43
|
+
}
|
|
31
44
|
if (!opts.json) {
|
|
32
|
-
console.log(chalk.yellow(
|
|
45
|
+
console.log(chalk.yellow(errorMessage));
|
|
33
46
|
}
|
|
34
47
|
}
|
|
35
48
|
}
|
|
36
49
|
// Prepare JSON output
|
|
37
50
|
if (opts.json) {
|
|
38
|
-
const jsonOutput = createJsonOutput(scanResult, opts);
|
|
51
|
+
const jsonOutput = createJsonOutput(scanResult, opts, comparedAgainst, Object.keys(envVariables).length);
|
|
39
52
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
40
53
|
return { exitWithError: scanResult.missing.length > 0 };
|
|
41
54
|
}
|
|
42
55
|
// Console output
|
|
43
|
-
return outputToConsole(scanResult, opts);
|
|
56
|
+
return outputToConsole(scanResult, opts, comparedAgainst);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Determines which file to use for comparison based on provided options
|
|
60
|
+
*/
|
|
61
|
+
function determineComparisonFile(opts) {
|
|
62
|
+
// Priority: explicit flags first, then auto-discovery
|
|
63
|
+
if (opts.examplePath && fs.existsSync(opts.examplePath)) {
|
|
64
|
+
return { path: opts.examplePath, name: path.basename(opts.examplePath) };
|
|
65
|
+
}
|
|
66
|
+
if (opts.envPath && fs.existsSync(opts.envPath)) {
|
|
67
|
+
return { path: opts.envPath, name: path.basename(opts.envPath) };
|
|
68
|
+
}
|
|
69
|
+
// Auto-discovery: look for common env files
|
|
70
|
+
const candidates = ['.env', '.env.example', '.env.local', '.env.production'];
|
|
71
|
+
for (const candidate of candidates) {
|
|
72
|
+
const fullPath = path.resolve(opts.cwd, candidate);
|
|
73
|
+
if (fs.existsSync(fullPath)) {
|
|
74
|
+
return { path: fullPath, name: candidate };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
44
78
|
}
|
|
45
|
-
function createJsonOutput(scanResult, opts) {
|
|
79
|
+
function createJsonOutput(scanResult, opts, comparedAgainst, totalEnvVariables) {
|
|
46
80
|
// Group usages by variable for missing variables
|
|
47
81
|
const missingGrouped = scanResult.missing.map((variable) => ({
|
|
48
82
|
variable,
|
|
@@ -60,6 +94,11 @@ function createJsonOutput(scanResult, opts) {
|
|
|
60
94
|
missing: missingGrouped,
|
|
61
95
|
unused: scanResult.unused,
|
|
62
96
|
};
|
|
97
|
+
// Add comparison info if we compared against a file
|
|
98
|
+
if (comparedAgainst) {
|
|
99
|
+
output.comparedAgainst = comparedAgainst;
|
|
100
|
+
output.totalEnvVariables = totalEnvVariables;
|
|
101
|
+
}
|
|
63
102
|
// Optionally include all usages
|
|
64
103
|
if (opts.showStats) {
|
|
65
104
|
output.allUsages = scanResult.used.map((u) => ({
|
|
@@ -72,8 +111,13 @@ function createJsonOutput(scanResult, opts) {
|
|
|
72
111
|
}
|
|
73
112
|
return output;
|
|
74
113
|
}
|
|
75
|
-
function outputToConsole(scanResult, opts) {
|
|
114
|
+
function outputToConsole(scanResult, opts, comparedAgainst) {
|
|
76
115
|
let exitWithError = false;
|
|
116
|
+
// Show what we're comparing against
|
|
117
|
+
if (comparedAgainst) {
|
|
118
|
+
console.log(chalk.gray(`📋 Comparing codebase usage against: ${comparedAgainst}`));
|
|
119
|
+
console.log();
|
|
120
|
+
}
|
|
77
121
|
// Show stats if requested
|
|
78
122
|
if (opts.showStats) {
|
|
79
123
|
console.log(chalk.bold('📊 Scan Statistics:'));
|
|
@@ -82,8 +126,8 @@ function outputToConsole(scanResult, opts) {
|
|
|
82
126
|
console.log(chalk.gray(` Unique variables: ${scanResult.stats.uniqueVariables}`));
|
|
83
127
|
console.log();
|
|
84
128
|
}
|
|
85
|
-
// Always show found variables when not
|
|
86
|
-
if (!
|
|
129
|
+
// Always show found variables when not comparing or when no missing variables
|
|
130
|
+
if (!comparedAgainst || scanResult.missing.length === 0) {
|
|
87
131
|
console.log(chalk.green(`✅ Found ${scanResult.stats.uniqueVariables} unique environment variables in use`));
|
|
88
132
|
console.log();
|
|
89
133
|
// List all variables found (if any)
|
|
@@ -113,10 +157,11 @@ function outputToConsole(scanResult, opts) {
|
|
|
113
157
|
console.log();
|
|
114
158
|
}
|
|
115
159
|
}
|
|
116
|
-
// Missing variables (used in code but not in
|
|
160
|
+
// Missing variables (used in code but not in env file)
|
|
117
161
|
if (scanResult.missing.length > 0) {
|
|
118
162
|
exitWithError = true;
|
|
119
|
-
|
|
163
|
+
const fileType = comparedAgainst || 'environment file';
|
|
164
|
+
console.log(chalk.red(`❌ Missing in ${fileType}:`));
|
|
120
165
|
const grouped = scanResult.missing.reduce((acc, variable) => {
|
|
121
166
|
const usages = scanResult.used.filter((u) => u.variable === variable);
|
|
122
167
|
acc[variable] = usages;
|
|
@@ -134,21 +179,29 @@ function outputToConsole(scanResult, opts) {
|
|
|
134
179
|
}
|
|
135
180
|
}
|
|
136
181
|
console.log();
|
|
182
|
+
// CI mode specific message
|
|
183
|
+
if (opts.isCiMode) {
|
|
184
|
+
console.log(chalk.red(`💥 Found ${scanResult.missing.length} missing environment variable(s).`));
|
|
185
|
+
console.log(chalk.red(` Add these variables to ${comparedAgainst || 'your environment file'} to fix this error.`));
|
|
186
|
+
console.log();
|
|
187
|
+
}
|
|
137
188
|
}
|
|
138
|
-
// Unused variables (in
|
|
189
|
+
// Unused variables (in env file but not used in code)
|
|
139
190
|
if (opts.showUnused && scanResult.unused.length > 0) {
|
|
140
|
-
|
|
191
|
+
const fileType = comparedAgainst || 'environment file';
|
|
192
|
+
console.log(chalk.yellow(`⚠️ Unused in codebase (defined in ${fileType}):`));
|
|
141
193
|
scanResult.unused.forEach((variable) => {
|
|
142
194
|
console.log(chalk.yellow(` - ${variable}`));
|
|
143
195
|
});
|
|
144
196
|
console.log();
|
|
145
197
|
}
|
|
146
|
-
// Success message for
|
|
147
|
-
if (
|
|
148
|
-
console.log(chalk.green(
|
|
198
|
+
// Success message for env file comparison
|
|
199
|
+
if (comparedAgainst && scanResult.missing.length === 0) {
|
|
200
|
+
console.log(chalk.green(`✅ All used environment variables are defined in ${comparedAgainst}`));
|
|
149
201
|
if (opts.showUnused && scanResult.unused.length === 0) {
|
|
150
202
|
console.log(chalk.green('✅ No unused environment variables found'));
|
|
151
203
|
}
|
|
204
|
+
console.log();
|
|
152
205
|
}
|
|
153
206
|
return { exitWithError };
|
|
154
207
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dotenv-diff",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"type": "module",
|
|
5
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": {
|