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 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-files` and `--exclude-files` options:
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-files '**/*.js,**/*.ts' --exclude-files '**/*.spec.ts'
86
+ dotenv-diff --scan-usage --include '**/*.js,**/*.ts' --exclude '**/*.spec.ts'
87
87
  ```
88
88
 
89
- This allows you to focus the scan on specific file types or directories, making it more efficient and tailored to your project structure.
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
 
@@ -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 include in scan (default: **/*.{js,ts,jsx,tsx,vue,svelte})')
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)');
@@ -25,6 +25,7 @@ export async function run(program) {
25
25
  showUnused: opts.showUnused,
26
26
  showStats: opts.showStats,
27
27
  isCiMode: opts.isCiMode,
28
+ files: opts.files,
28
29
  });
29
30
  process.exit(exitWithError ? 1 : 0);
30
31
  }
@@ -6,6 +6,7 @@ export interface ScanUsageOptions extends ScanOptions {
6
6
  showUnused: boolean;
7
7
  showStats: boolean;
8
8
  isCiMode?: boolean;
9
+ files?: string[];
9
10
  }
10
11
  export interface ScanJsonEntry {
11
12
  stats: {
@@ -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.error(chalk.red(`❌ ${errorMessage}`));
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 && fs.existsSync(opts.examplePath)) {
64
- return { path: opts.examplePath, name: path.basename(opts.examplePath) };
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 && fs.existsSync(opts.envPath)) {
67
- return { path: opts.envPath, name: path.basename(opts.envPath) };
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;
@@ -12,6 +12,7 @@ export interface ScanOptions {
12
12
  exclude: string[];
13
13
  ignore: string[];
14
14
  ignoreRegex: RegExp[];
15
+ files?: string[];
15
16
  }
16
17
  export interface ScanResult {
17
18
  used: EnvUsage[];
@@ -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 exclude patterns.
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, opts.include)) {
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.0",
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": {