dotenv-diff 2.0.0 → 2.1.1
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 +33 -3
- package/dist/src/cli/program.js +2 -1
- package/dist/src/cli/run.js +3 -0
- package/dist/src/commands/scanUsage.d.ts +7 -0
- package/dist/src/commands/scanUsage.js +94 -17
- package/dist/src/config/options.js +2 -0
- package/dist/src/config/types.d.ts +2 -0
- package/dist/src/services/codeBaseScanner.d.ts +1 -0
- package/dist/src/services/codeBaseScanner.js +113 -7
- 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:
|
|
@@ -59,13 +80,22 @@ This will display statistics about the scan, such as the number of files scanned
|
|
|
59
80
|
|
|
60
81
|
## include or exclude specific files for scanning
|
|
61
82
|
|
|
62
|
-
You can specify which files to include or exclude from the scan using the `--include
|
|
83
|
+
You can specify which files to include or exclude from the scan using the `--include` and `--exclude` options:
|
|
63
84
|
|
|
64
85
|
```bash
|
|
65
|
-
dotenv-diff --scan-usage --include
|
|
86
|
+
dotenv-diff --scan-usage --include '**/*.js,**/*.ts' --exclude '**/*.spec.ts'
|
|
66
87
|
```
|
|
67
88
|
|
|
68
|
-
|
|
89
|
+
By default, the scanner looks at JavaScript, TypeScript, Vue, and Svelte files.
|
|
90
|
+
The --include and --exclude options let you refine this list to focus on specific file types or directories.
|
|
91
|
+
|
|
92
|
+
### Override with `--files`
|
|
93
|
+
|
|
94
|
+
If you want to completely override the default include/exclude logic (for example, to also include test files), you can use --files:
|
|
95
|
+
```bash
|
|
96
|
+
dotenv-diff --scan-usage --files '**/*.js'
|
|
97
|
+
```
|
|
98
|
+
This will only scan the files matching the given patterns, even if they would normally be excluded.
|
|
69
99
|
|
|
70
100
|
## Optional: Check values too
|
|
71
101
|
|
package/dist/src/cli/program.js
CHANGED
|
@@ -14,7 +14,8 @@ export function createProgram() {
|
|
|
14
14
|
.option('--json', 'Output results in JSON format')
|
|
15
15
|
.option('--only <list>', 'Comma-separated categories to only run (missing,extra,empty,mismatch,duplicate,gitignore)')
|
|
16
16
|
.option('--scan-usage', 'Scan codebase for environment variable usage')
|
|
17
|
-
.option('--include-files <patterns>', '[requires --scan-usage] Comma-separated file patterns to
|
|
17
|
+
.option('--include-files <patterns>', '[requires --scan-usage] Comma-separated file patterns to ADD to default scan patterns (extends default)')
|
|
18
|
+
.option('--files <patterns>', '[requires --scan-usage] Comma-separated file patterns to scan (completely replaces default patterns)')
|
|
18
19
|
.option('--exclude-files <patterns>', '[requires --scan-usage] Comma-separated file patterns to exclude from scan')
|
|
19
20
|
.option('--show-unused', '[requires --scan-usage] Show variables defined in .env but not used in code')
|
|
20
21
|
.option('--show-stats', 'Show statistics (in scan-usage mode: scan stats, in compare mode: env compare stats)');
|
package/dist/src/cli/run.js
CHANGED
|
@@ -19,10 +19,13 @@ 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,
|
|
28
|
+
files: opts.files,
|
|
26
29
|
});
|
|
27
30
|
process.exit(exitWithError ? 1 : 0);
|
|
28
31
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
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;
|
|
9
|
+
files?: string[];
|
|
7
10
|
}
|
|
8
11
|
export interface ScanJsonEntry {
|
|
9
12
|
stats: {
|
|
@@ -28,14 +31,18 @@ export interface ScanJsonEntry {
|
|
|
28
31
|
pattern: string;
|
|
29
32
|
context: string;
|
|
30
33
|
}>;
|
|
34
|
+
comparedAgainst?: string;
|
|
35
|
+
totalEnvVariables?: number;
|
|
31
36
|
}
|
|
32
37
|
/**
|
|
33
38
|
* Scans codebase for environment variable usage and compares with .env file
|
|
34
39
|
* @param {ScanUsageOptions} opts - Scan configuration options
|
|
35
40
|
* @param {string} [opts.envPath] - Path to .env file for comparison
|
|
41
|
+
* @param {string} [opts.examplePath] - Path to .env.example file for comparison
|
|
36
42
|
* @param {boolean} opts.json - Output as JSON instead of console
|
|
37
43
|
* @param {boolean} opts.showUnused - Show unused variables from .env
|
|
38
44
|
* @param {boolean} opts.showStats - Show detailed statistics
|
|
45
|
+
* @param {boolean} [opts.isCiMode] - Run in CI mode (exit with error code)
|
|
39
46
|
* @returns {Promise<{exitWithError: boolean}>} Returns true if missing variables found
|
|
40
47
|
*/
|
|
41
48
|
export declare function scanUsage(opts: ScanUsageOptions): Promise<{
|
|
@@ -1,14 +1,19 @@
|
|
|
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';
|
|
7
|
+
const resolveFromCwd = (cwd, p) => path.isAbsolute(p) ? p : path.resolve(cwd, p);
|
|
5
8
|
/**
|
|
6
9
|
* Scans codebase for environment variable usage and compares with .env file
|
|
7
10
|
* @param {ScanUsageOptions} opts - Scan configuration options
|
|
8
11
|
* @param {string} [opts.envPath] - Path to .env file for comparison
|
|
12
|
+
* @param {string} [opts.examplePath] - Path to .env.example file for comparison
|
|
9
13
|
* @param {boolean} opts.json - Output as JSON instead of console
|
|
10
14
|
* @param {boolean} opts.showUnused - Show unused variables from .env
|
|
11
15
|
* @param {boolean} opts.showStats - Show detailed statistics
|
|
16
|
+
* @param {boolean} [opts.isCiMode] - Run in CI mode (exit with error code)
|
|
12
17
|
* @returns {Promise<{exitWithError: boolean}>} Returns true if missing variables found
|
|
13
18
|
*/
|
|
14
19
|
export async function scanUsage(opts) {
|
|
@@ -18,31 +23,84 @@ export async function scanUsage(opts) {
|
|
|
18
23
|
}
|
|
19
24
|
// Scan the codebase
|
|
20
25
|
let scanResult = await scanCodebase(opts);
|
|
21
|
-
// If
|
|
26
|
+
// If user explicitly passed --example but the file doesn't exist:
|
|
27
|
+
if (opts.examplePath) {
|
|
28
|
+
const exampleAbs = resolveFromCwd(opts.cwd, opts.examplePath);
|
|
29
|
+
const missing = !fs.existsSync(exampleAbs);
|
|
30
|
+
if (missing) {
|
|
31
|
+
const msg = `❌ Missing specified example file: ${opts.examplePath}`;
|
|
32
|
+
if (opts.isCiMode) {
|
|
33
|
+
// IMPORTANT: stdout (console.log), not stderr, to satisfy the test
|
|
34
|
+
console.log(chalk.red(msg));
|
|
35
|
+
return { exitWithError: true };
|
|
36
|
+
}
|
|
37
|
+
else if (!opts.json) {
|
|
38
|
+
console.log(chalk.yellow(msg.replace('❌', '⚠️')));
|
|
39
|
+
}
|
|
40
|
+
// Non-CI: continue without comparison
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Determine which file to compare against
|
|
44
|
+
const compareFile = determineComparisonFile(opts);
|
|
22
45
|
let envVariables = {};
|
|
23
|
-
|
|
46
|
+
let comparedAgainst = '';
|
|
47
|
+
if (compareFile) {
|
|
24
48
|
try {
|
|
25
|
-
const envFull = parseEnvFile(
|
|
49
|
+
const envFull = parseEnvFile(compareFile.path);
|
|
26
50
|
const envKeys = filterIgnoredKeys(Object.keys(envFull), opts.ignore, opts.ignoreRegex);
|
|
27
51
|
envVariables = Object.fromEntries(envKeys.map((k) => [k, envFull[k]]));
|
|
28
52
|
scanResult = compareWithEnvFiles(scanResult, envVariables);
|
|
53
|
+
comparedAgainst = compareFile.name;
|
|
29
54
|
}
|
|
30
55
|
catch (error) {
|
|
56
|
+
const errorMessage = `⚠️ Could not read ${compareFile.name}: ${compareFile.path} - ${error}`;
|
|
57
|
+
if (opts.isCiMode) {
|
|
58
|
+
// In CI mode, exit with error if file doesn't exist
|
|
59
|
+
console.log(chalk.red(`❌ ${errorMessage}`));
|
|
60
|
+
return { exitWithError: true };
|
|
61
|
+
}
|
|
31
62
|
if (!opts.json) {
|
|
32
|
-
console.log(chalk.yellow(
|
|
63
|
+
console.log(chalk.yellow(errorMessage));
|
|
33
64
|
}
|
|
34
65
|
}
|
|
35
66
|
}
|
|
36
67
|
// Prepare JSON output
|
|
37
68
|
if (opts.json) {
|
|
38
|
-
const jsonOutput = createJsonOutput(scanResult, opts);
|
|
69
|
+
const jsonOutput = createJsonOutput(scanResult, opts, comparedAgainst, Object.keys(envVariables).length);
|
|
39
70
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
40
71
|
return { exitWithError: scanResult.missing.length > 0 };
|
|
41
72
|
}
|
|
42
73
|
// Console output
|
|
43
|
-
return outputToConsole(scanResult, opts);
|
|
74
|
+
return outputToConsole(scanResult, opts, comparedAgainst);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Determines which file to use for comparison based on provided options
|
|
78
|
+
*/
|
|
79
|
+
function determineComparisonFile(opts) {
|
|
80
|
+
// Priority: explicit flags first, then auto-discovery
|
|
81
|
+
if (opts.examplePath) {
|
|
82
|
+
const p = resolveFromCwd(opts.cwd, opts.examplePath);
|
|
83
|
+
if (fs.existsSync(p)) {
|
|
84
|
+
return { path: p, name: path.basename(opts.examplePath) };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (opts.envPath) {
|
|
88
|
+
const p = resolveFromCwd(opts.cwd, opts.envPath);
|
|
89
|
+
if (fs.existsSync(p)) {
|
|
90
|
+
return { path: p, name: path.basename(opts.envPath) };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Auto-discovery: look for common env files relative to cwd
|
|
94
|
+
const candidates = ['.env', '.env.example', '.env.local', '.env.production'];
|
|
95
|
+
for (const candidate of candidates) {
|
|
96
|
+
const fullPath = path.resolve(opts.cwd, candidate);
|
|
97
|
+
if (fs.existsSync(fullPath)) {
|
|
98
|
+
return { path: fullPath, name: candidate };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
44
102
|
}
|
|
45
|
-
function createJsonOutput(scanResult, opts) {
|
|
103
|
+
function createJsonOutput(scanResult, opts, comparedAgainst, totalEnvVariables) {
|
|
46
104
|
// Group usages by variable for missing variables
|
|
47
105
|
const missingGrouped = scanResult.missing.map((variable) => ({
|
|
48
106
|
variable,
|
|
@@ -60,6 +118,11 @@ function createJsonOutput(scanResult, opts) {
|
|
|
60
118
|
missing: missingGrouped,
|
|
61
119
|
unused: scanResult.unused,
|
|
62
120
|
};
|
|
121
|
+
// Add comparison info if we compared against a file
|
|
122
|
+
if (comparedAgainst) {
|
|
123
|
+
output.comparedAgainst = comparedAgainst;
|
|
124
|
+
output.totalEnvVariables = totalEnvVariables;
|
|
125
|
+
}
|
|
63
126
|
// Optionally include all usages
|
|
64
127
|
if (opts.showStats) {
|
|
65
128
|
output.allUsages = scanResult.used.map((u) => ({
|
|
@@ -72,8 +135,13 @@ function createJsonOutput(scanResult, opts) {
|
|
|
72
135
|
}
|
|
73
136
|
return output;
|
|
74
137
|
}
|
|
75
|
-
function outputToConsole(scanResult, opts) {
|
|
138
|
+
function outputToConsole(scanResult, opts, comparedAgainst) {
|
|
76
139
|
let exitWithError = false;
|
|
140
|
+
// Show what we're comparing against
|
|
141
|
+
if (comparedAgainst) {
|
|
142
|
+
console.log(chalk.gray(`📋 Comparing codebase usage against: ${comparedAgainst}`));
|
|
143
|
+
console.log();
|
|
144
|
+
}
|
|
77
145
|
// Show stats if requested
|
|
78
146
|
if (opts.showStats) {
|
|
79
147
|
console.log(chalk.bold('📊 Scan Statistics:'));
|
|
@@ -82,8 +150,8 @@ function outputToConsole(scanResult, opts) {
|
|
|
82
150
|
console.log(chalk.gray(` Unique variables: ${scanResult.stats.uniqueVariables}`));
|
|
83
151
|
console.log();
|
|
84
152
|
}
|
|
85
|
-
// Always show found variables when not
|
|
86
|
-
if (!
|
|
153
|
+
// Always show found variables when not comparing or when no missing variables
|
|
154
|
+
if (!comparedAgainst || scanResult.missing.length === 0) {
|
|
87
155
|
console.log(chalk.green(`✅ Found ${scanResult.stats.uniqueVariables} unique environment variables in use`));
|
|
88
156
|
console.log();
|
|
89
157
|
// List all variables found (if any)
|
|
@@ -113,10 +181,11 @@ function outputToConsole(scanResult, opts) {
|
|
|
113
181
|
console.log();
|
|
114
182
|
}
|
|
115
183
|
}
|
|
116
|
-
// Missing variables (used in code but not in
|
|
184
|
+
// Missing variables (used in code but not in env file)
|
|
117
185
|
if (scanResult.missing.length > 0) {
|
|
118
186
|
exitWithError = true;
|
|
119
|
-
|
|
187
|
+
const fileType = comparedAgainst || 'environment file';
|
|
188
|
+
console.log(chalk.red(`❌ Missing in ${fileType}:`));
|
|
120
189
|
const grouped = scanResult.missing.reduce((acc, variable) => {
|
|
121
190
|
const usages = scanResult.used.filter((u) => u.variable === variable);
|
|
122
191
|
acc[variable] = usages;
|
|
@@ -134,21 +203,29 @@ function outputToConsole(scanResult, opts) {
|
|
|
134
203
|
}
|
|
135
204
|
}
|
|
136
205
|
console.log();
|
|
206
|
+
// CI mode specific message
|
|
207
|
+
if (opts.isCiMode) {
|
|
208
|
+
console.log(chalk.red(`💥 Found ${scanResult.missing.length} missing environment variable(s).`));
|
|
209
|
+
console.log(chalk.red(` Add these variables to ${comparedAgainst || 'your environment file'} to fix this error.`));
|
|
210
|
+
console.log();
|
|
211
|
+
}
|
|
137
212
|
}
|
|
138
|
-
// Unused variables (in
|
|
213
|
+
// Unused variables (in env file but not used in code)
|
|
139
214
|
if (opts.showUnused && scanResult.unused.length > 0) {
|
|
140
|
-
|
|
215
|
+
const fileType = comparedAgainst || 'environment file';
|
|
216
|
+
console.log(chalk.yellow(`⚠️ Unused in codebase (defined in ${fileType}):`));
|
|
141
217
|
scanResult.unused.forEach((variable) => {
|
|
142
218
|
console.log(chalk.yellow(` - ${variable}`));
|
|
143
219
|
});
|
|
144
220
|
console.log();
|
|
145
221
|
}
|
|
146
|
-
// Success message for
|
|
147
|
-
if (
|
|
148
|
-
console.log(chalk.green(
|
|
222
|
+
// Success message for env file comparison
|
|
223
|
+
if (comparedAgainst && scanResult.missing.length === 0) {
|
|
224
|
+
console.log(chalk.green(`✅ All used environment variables are defined in ${comparedAgainst}`));
|
|
149
225
|
if (opts.showUnused && scanResult.unused.length === 0) {
|
|
150
226
|
console.log(chalk.green('✅ No unused environment variables found'));
|
|
151
227
|
}
|
|
228
|
+
console.log();
|
|
152
229
|
}
|
|
153
230
|
return { exitWithError };
|
|
154
231
|
}
|
|
@@ -30,6 +30,7 @@ export function normalizeOptions(raw) {
|
|
|
30
30
|
const excludeFiles = parseList(raw.excludeFiles);
|
|
31
31
|
const showUnused = Boolean(raw.showUnused);
|
|
32
32
|
const showStats = Boolean(raw.showStats);
|
|
33
|
+
const files = parseList(raw.files);
|
|
33
34
|
const ignore = parseList(raw.ignore);
|
|
34
35
|
const ignoreRegex = [];
|
|
35
36
|
for (const pattern of parseList(raw.ignoreRegex)) {
|
|
@@ -64,5 +65,6 @@ export function normalizeOptions(raw) {
|
|
|
64
65
|
excludeFiles,
|
|
65
66
|
showUnused,
|
|
66
67
|
showStats,
|
|
68
|
+
files,
|
|
67
69
|
};
|
|
68
70
|
}
|
|
@@ -17,6 +17,7 @@ export type Options = {
|
|
|
17
17
|
excludeFiles: string[];
|
|
18
18
|
showUnused: boolean;
|
|
19
19
|
showStats: boolean;
|
|
20
|
+
files?: string[];
|
|
20
21
|
};
|
|
21
22
|
export type RawOptions = {
|
|
22
23
|
checkValues?: boolean;
|
|
@@ -34,6 +35,7 @@ export type RawOptions = {
|
|
|
34
35
|
excludeFiles?: string | string[];
|
|
35
36
|
showUnused?: boolean;
|
|
36
37
|
showStats?: boolean;
|
|
38
|
+
files?: string | string[];
|
|
37
39
|
};
|
|
38
40
|
export type CompareJsonEntry = {
|
|
39
41
|
env: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import fsSync from 'fs';
|
|
3
4
|
// Framework-specific patterns for finding environment variable usage
|
|
4
5
|
const ENV_PATTERNS = [
|
|
5
6
|
{
|
|
@@ -67,6 +68,7 @@ export async function scanCodebase(opts) {
|
|
|
67
68
|
const files = await findFiles(opts.cwd, {
|
|
68
69
|
include: opts.include,
|
|
69
70
|
exclude: [...DEFAULT_EXCLUDE_PATTERNS, ...opts.exclude],
|
|
71
|
+
files: opts.files, // Pass files option
|
|
70
72
|
});
|
|
71
73
|
const allUsages = [];
|
|
72
74
|
let filesScanned = 0;
|
|
@@ -97,14 +99,117 @@ export async function scanCodebase(opts) {
|
|
|
97
99
|
},
|
|
98
100
|
};
|
|
99
101
|
}
|
|
102
|
+
function expandBraceSets(pattern) {
|
|
103
|
+
// Single-level brace expansion: **/*.{js,ts,svelte} -> [**/*.js, **/*.ts, **/*.svelte]
|
|
104
|
+
const m = pattern.match(/\{([^}]+)\}/);
|
|
105
|
+
if (!m)
|
|
106
|
+
return [pattern];
|
|
107
|
+
const variants = m[1]
|
|
108
|
+
.split(',')
|
|
109
|
+
.map((p) => p.trim())
|
|
110
|
+
.filter(Boolean);
|
|
111
|
+
const prefix = pattern.slice(0, m.index);
|
|
112
|
+
const suffix = pattern.slice(m.index + m[0].length);
|
|
113
|
+
return variants.flatMap((v) => expandBraceSets(`${prefix}${v}${suffix}`));
|
|
114
|
+
}
|
|
100
115
|
/**
|
|
101
116
|
* Recursively finds all files in the given directory matching the include patterns,
|
|
102
117
|
* while excluding files and directories that match the exclude patterns.
|
|
103
118
|
* @param rootDir The root directory to start searching from.
|
|
104
|
-
* @param opts Options for include and
|
|
119
|
+
* @param opts Options for include, exclude patterns and files override.
|
|
105
120
|
* @returns A promise that resolves to an array of file paths.
|
|
106
121
|
*/
|
|
107
122
|
async function findFiles(rootDir, opts) {
|
|
123
|
+
// If --files provided, keep existing replacement behavior
|
|
124
|
+
if (opts.files && opts.files.length > 0) {
|
|
125
|
+
return findFilesByPatterns(rootDir, opts.files);
|
|
126
|
+
}
|
|
127
|
+
const defaultPatterns = getDefaultPatterns();
|
|
128
|
+
const rawInclude = opts.include.length > 0
|
|
129
|
+
? [...defaultPatterns, ...opts.include]
|
|
130
|
+
: defaultPatterns;
|
|
131
|
+
const includePatterns = rawInclude.flatMap(expandBraceSets);
|
|
132
|
+
const files = [];
|
|
133
|
+
const walked = new Set();
|
|
134
|
+
// Compute additional roots for include patterns that point outside cwd or are absolute
|
|
135
|
+
const extraRoots = new Set();
|
|
136
|
+
for (const p of includePatterns) {
|
|
137
|
+
const hasParentEscape = p.includes('..') || path.isAbsolute(p);
|
|
138
|
+
if (!hasParentEscape)
|
|
139
|
+
continue;
|
|
140
|
+
const dir = getPatternBaseDir(rootDir, p);
|
|
141
|
+
if (dir && !dir.startsWith(rootDir)) {
|
|
142
|
+
extraRoots.add(dir);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function walk(startDir) {
|
|
146
|
+
// prevent duplicate subtree walks
|
|
147
|
+
const key = path.resolve(startDir);
|
|
148
|
+
if (walked.has(key))
|
|
149
|
+
return;
|
|
150
|
+
walked.add(key);
|
|
151
|
+
let entries;
|
|
152
|
+
try {
|
|
153
|
+
entries = await fs.readdir(startDir, { withFileTypes: true });
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
for (const entry of entries) {
|
|
159
|
+
const fullPath = path.join(startDir, entry.name);
|
|
160
|
+
const relativeToRoot = path.relative(rootDir, fullPath);
|
|
161
|
+
// Exclude checks should use path relative to *rootDir* (keeps existing semantics)
|
|
162
|
+
if (shouldExclude(entry.name, relativeToRoot, [
|
|
163
|
+
...DEFAULT_EXCLUDE_PATTERNS,
|
|
164
|
+
...opts.exclude,
|
|
165
|
+
])) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (entry.isDirectory()) {
|
|
169
|
+
await walk(fullPath);
|
|
170
|
+
}
|
|
171
|
+
else if (entry.isFile() &&
|
|
172
|
+
shouldInclude(entry.name, relativeToRoot, includePatterns)) {
|
|
173
|
+
files.push(fullPath);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Walk root first (current behavior)
|
|
178
|
+
await walk(rootDir);
|
|
179
|
+
// Walk any extra roots (e.g., ../../packages/…)
|
|
180
|
+
for (const r of extraRoots) {
|
|
181
|
+
await walk(r);
|
|
182
|
+
}
|
|
183
|
+
return files;
|
|
184
|
+
}
|
|
185
|
+
function getPatternBaseDir(rootDir, pattern) {
|
|
186
|
+
// Stop at first glob char — DO NOT include '/' here
|
|
187
|
+
const idx = pattern.search(/[*?\[\]{}]/); // <— removed '/'
|
|
188
|
+
const raw = idx === -1 ? pattern : pattern.slice(0, idx);
|
|
189
|
+
const base = path.isAbsolute(raw) ? raw : path.resolve(rootDir, raw);
|
|
190
|
+
try {
|
|
191
|
+
const st = fsSync.statSync(base);
|
|
192
|
+
if (st.isDirectory())
|
|
193
|
+
return base;
|
|
194
|
+
if (st.isFile())
|
|
195
|
+
return path.dirname(base);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
const dir = path.dirname(base);
|
|
199
|
+
try {
|
|
200
|
+
const st2 = fsSync.statSync(dir);
|
|
201
|
+
if (st2.isDirectory())
|
|
202
|
+
return dir;
|
|
203
|
+
}
|
|
204
|
+
catch { }
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Find files using the --files patterns (complete replacement mode)
|
|
210
|
+
*/
|
|
211
|
+
async function findFilesByPatterns(rootDir, patterns) {
|
|
212
|
+
const expanded = patterns.flatMap(expandBraceSets);
|
|
108
213
|
const files = [];
|
|
109
214
|
async function walk(currentDir) {
|
|
110
215
|
let entries;
|
|
@@ -112,21 +217,16 @@ async function findFiles(rootDir, opts) {
|
|
|
112
217
|
entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
113
218
|
}
|
|
114
219
|
catch {
|
|
115
|
-
// Skip directories we can't read (permissions, etc.)
|
|
116
220
|
return;
|
|
117
221
|
}
|
|
118
222
|
for (const entry of entries) {
|
|
119
223
|
const fullPath = path.join(currentDir, entry.name);
|
|
120
224
|
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
225
|
if (entry.isDirectory()) {
|
|
126
226
|
await walk(fullPath);
|
|
127
227
|
}
|
|
128
228
|
else if (entry.isFile() &&
|
|
129
|
-
shouldInclude(entry.name, relativePath,
|
|
229
|
+
shouldInclude(entry.name, relativePath, expanded)) {
|
|
130
230
|
files.push(fullPath);
|
|
131
231
|
}
|
|
132
232
|
}
|
|
@@ -134,6 +234,12 @@ async function findFiles(rootDir, opts) {
|
|
|
134
234
|
await walk(rootDir);
|
|
135
235
|
return files;
|
|
136
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Generate default patterns from extensions
|
|
239
|
+
*/
|
|
240
|
+
function getDefaultPatterns() {
|
|
241
|
+
return DEFAULT_INCLUDE_EXTENSIONS.map((ext) => `**/*${ext}`);
|
|
242
|
+
}
|
|
137
243
|
/**
|
|
138
244
|
* Check if a file should be included based on its name, path, and include patterns.
|
|
139
245
|
* @param fileName The name of the file.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dotenv-diff",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
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": {
|