dotenv-diff 2.1.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 +12 -3
- package/dist/src/cli/program.js +2 -1
- package/dist/src/cli/run.js +1 -0
- package/dist/src/commands/scanUsage.d.ts +1 -0
- package/dist/src/commands/scanUsage.js +30 -6
- 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
|
@@ -80,13 +80,22 @@ This will display statistics about the scan, such as the number of files scanned
|
|
|
80
80
|
|
|
81
81
|
## include or exclude specific files for scanning
|
|
82
82
|
|
|
83
|
-
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:
|
|
84
84
|
|
|
85
85
|
```bash
|
|
86
|
-
dotenv-diff --scan-usage --include
|
|
86
|
+
dotenv-diff --scan-usage --include '**/*.js,**/*.ts' --exclude '**/*.spec.ts'
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
-
|
|
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.
|
|
90
99
|
|
|
91
100
|
## Optional: Check values too
|
|
92
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
|
@@ -4,6 +4,7 @@ import path from 'path';
|
|
|
4
4
|
import { parseEnvFile } from '../lib/parseEnv.js';
|
|
5
5
|
import { scanCodebase, compareWithEnvFiles, } from '../services/codeBaseScanner.js';
|
|
6
6
|
import { filterIgnoredKeys } from '../core/filterIgnoredKeys.js';
|
|
7
|
+
const resolveFromCwd = (cwd, p) => path.isAbsolute(p) ? p : path.resolve(cwd, p);
|
|
7
8
|
/**
|
|
8
9
|
* Scans codebase for environment variable usage and compares with .env file
|
|
9
10
|
* @param {ScanUsageOptions} opts - Scan configuration options
|
|
@@ -22,6 +23,23 @@ export async function scanUsage(opts) {
|
|
|
22
23
|
}
|
|
23
24
|
// Scan the codebase
|
|
24
25
|
let scanResult = await scanCodebase(opts);
|
|
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
|
+
}
|
|
25
43
|
// Determine which file to compare against
|
|
26
44
|
const compareFile = determineComparisonFile(opts);
|
|
27
45
|
let envVariables = {};
|
|
@@ -38,7 +56,7 @@ export async function scanUsage(opts) {
|
|
|
38
56
|
const errorMessage = `⚠️ Could not read ${compareFile.name}: ${compareFile.path} - ${error}`;
|
|
39
57
|
if (opts.isCiMode) {
|
|
40
58
|
// In CI mode, exit with error if file doesn't exist
|
|
41
|
-
console.
|
|
59
|
+
console.log(chalk.red(`❌ ${errorMessage}`));
|
|
42
60
|
return { exitWithError: true };
|
|
43
61
|
}
|
|
44
62
|
if (!opts.json) {
|
|
@@ -60,13 +78,19 @@ export async function scanUsage(opts) {
|
|
|
60
78
|
*/
|
|
61
79
|
function determineComparisonFile(opts) {
|
|
62
80
|
// Priority: explicit flags first, then auto-discovery
|
|
63
|
-
if (opts.examplePath
|
|
64
|
-
|
|
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
|
+
}
|
|
65
86
|
}
|
|
66
|
-
if (opts.envPath
|
|
67
|
-
|
|
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
|
+
}
|
|
68
92
|
}
|
|
69
|
-
// Auto-discovery: look for common env files
|
|
93
|
+
// Auto-discovery: look for common env files relative to cwd
|
|
70
94
|
const candidates = ['.env', '.env.example', '.env.local', '.env.production'];
|
|
71
95
|
for (const candidate of candidates) {
|
|
72
96
|
const fullPath = path.resolve(opts.cwd, candidate);
|
|
@@ -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.1.
|
|
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": {
|