dotenv-diff 1.6.5 → 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 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
  [![npm version](https://img.shields.io/npm/v/dotenv-diff.svg)](https://www.npmjs.com/package/dotenv-diff)
6
10
  [![npm downloads](https://img.shields.io/npm/dt/dotenv-diff.svg)](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
@@ -12,5 +12,10 @@ export function createProgram() {
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
14
  .option('--json', 'Output results in JSON format')
15
- .option('--only <list>', 'Comma-separated categories to only run (missing,extra,empty,mismatch,duplicate,gitignore)');
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)');
16
21
  }
@@ -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);
@@ -38,6 +54,7 @@ export async function run(program) {
38
54
  ignore: opts.ignore,
39
55
  ignoreRegex: opts.ignoreRegex,
40
56
  only: opts.only,
57
+ showStats: opts.showStats,
41
58
  collect: (e) => report.push(e),
42
59
  });
43
60
  if (opts.json) {
@@ -77,6 +94,7 @@ export async function run(program) {
77
94
  ignore: opts.ignore,
78
95
  ignoreRegex: opts.ignoreRegex,
79
96
  only: opts.only,
97
+ showStats: opts.showStats,
80
98
  collect: (e) => report.push(e),
81
99
  });
82
100
  if (opts.json) {
@@ -12,6 +12,7 @@ export declare function compareMany(pairs: Array<{
12
12
  ignoreRegex: RegExp[];
13
13
  collect?: (entry: CompareJsonEntry) => void;
14
14
  only?: Category[];
15
+ showStats?: boolean;
15
16
  }): Promise<{
16
17
  exitWithError: boolean;
17
18
  }>;
@@ -60,22 +60,22 @@ export async function compareMany(pairs, opts) {
60
60
  !opts.ignoreRegex.some((rx) => rx.test(key)));
61
61
  dupsEx = findDuplicateKeys(examplePath).filter(({ key }) => !opts.ignore.includes(key) &&
62
62
  !opts.ignoreRegex.some((rx) => rx.test(key)));
63
- if (dupsEnv.length || dupsEx.length) {
64
- entry.duplicates = {};
65
- }
66
- if (dupsEnv.length) {
67
- entry.duplicates.env = dupsEnv;
68
- if (!opts.json) {
69
- console.log(chalk.yellow(` ⚠️ Duplicate keys in ${envName} (last occurrence wins):`));
70
- dupsEnv.forEach(({ key, count }) => console.log(chalk.yellow(` - ${key} (${count} occurrences)`)));
71
- }
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)`)));
72
72
  }
73
- if (dupsEx.length) {
74
- entry.duplicates.example = dupsEx;
75
- if (!opts.json) {
76
- console.log(chalk.yellow(` ⚠️ Duplicate keys in ${exampleName} (last occurrence wins):`));
77
- dupsEx.forEach(({ key, count }) => console.log(chalk.yellow(` - ${key} (${count} occurrences)`)));
78
- }
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)`)));
79
79
  }
80
80
  }
81
81
  // Diff + empty
@@ -111,6 +111,27 @@ export async function compareMany(pairs, opts) {
111
111
  opts.collect?.(entry);
112
112
  continue;
113
113
  }
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
+ }
114
135
  if (filtered.missing.length) {
115
136
  entry.missing = filtered.missing;
116
137
  exitWithError = true;
@@ -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
+ }
@@ -25,6 +25,11 @@ export function normalizeOptions(raw) {
25
25
  const json = Boolean(raw.json);
26
26
  const onlyParsed = parseCategories(raw.only, '--only');
27
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);
28
33
  const ignore = parseList(raw.ignore);
29
34
  const ignoreRegex = [];
30
35
  for (const pattern of parseList(raw.ignoreRegex)) {
@@ -54,5 +59,10 @@ export function normalizeOptions(raw) {
54
59
  ignoreRegex,
55
60
  cwd,
56
61
  only,
62
+ scanUsage,
63
+ includeFiles,
64
+ excludeFiles,
65
+ showUnused,
66
+ showStats,
57
67
  };
58
68
  }
@@ -12,6 +12,11 @@ export type Options = {
12
12
  ignoreRegex: RegExp[];
13
13
  cwd: string;
14
14
  only?: Category[];
15
+ scanUsage: boolean;
16
+ includeFiles: string[];
17
+ excludeFiles: string[];
18
+ showUnused: boolean;
19
+ showStats: boolean;
15
20
  };
16
21
  export type RawOptions = {
17
22
  checkValues?: boolean;
@@ -24,6 +29,11 @@ export type RawOptions = {
24
29
  ignore?: string | string[];
25
30
  ignoreRegex?: string | string[];
26
31
  only?: string | string[];
32
+ scanUsage?: boolean;
33
+ includeFiles?: string | string[];
34
+ excludeFiles?: string | string[];
35
+ showUnused?: boolean;
36
+ showStats?: boolean;
27
37
  };
28
38
  export type CompareJsonEntry = {
29
39
  env: string;
@@ -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": "1.6.5",
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
  },