diff --git a/.changeset/chilly-lands-dance.md b/.changeset/chilly-lands-dance.md new file mode 100644 index 0000000..46f8456 --- /dev/null +++ b/.changeset/chilly-lands-dance.md @@ -0,0 +1,5 @@ +--- +'dotenv-diff': patch +--- + +added more patterns to DEFAULT_EXCLUDE_PATTERNS diff --git a/docs/configuration_and_flags.md b/docs/configuration_and_flags.md index 78006b8..7979358 100644 --- a/docs/configuration_and_flags.md +++ b/docs/configuration_and_flags.md @@ -363,8 +363,7 @@ Usage in the configuration file: ### `--exclude-files ` Specify a comma-separated list of file patterns to exclude from scanning. -These patterns are added to the default exclude patterns (like `node_modules`, `dist`, etc.). -Useful when you want to skip specific files or directories that shouldn't be scanned. +These patterns are added on top of the built-in default exclude patterns. This is useful when you want to skip additional files or directories that should not be scanned in your project. Example usage: @@ -380,6 +379,50 @@ Usage in the configuration file: } ``` +dotenv-diff already excludes the following paths by default: + +```bash +[ + 'node_modules', + '.sveltekit', + '.svelte-kit', + '_actions', + 'dist', + 'build', + '.next', + '.nuxt', + 'coverage', + '.git', + '.vscode', + '.idea', + '.test.', + '.spec.', + '__tests__', + '__mocks__', + 'test', + 'tests', + 'fixtures', + 'fixture', + 'examples', + 'example', + 'samples', + 'sandbox', + '.turbo', + '.cache', + '.output', + '.vercel', + '.yarn', + '.pnpm-store', + '.parcel-cache', + '.rollup.cache', + '.DS_Store' +] +``` + +Your custom --exclude-files patterns are appended to that list. + +If you later want to scan files from one of the default excluded paths, use `--include-files` or `--files` to explicitly include them. + ## Display Options ### `--show-unused` diff --git a/packages/cli/src/core/scan/patterns.ts b/packages/cli/src/core/scan/patterns.ts index 362fe53..0fb8fe7 100644 --- a/packages/cli/src/core/scan/patterns.ts +++ b/packages/cli/src/core/scan/patterns.ts @@ -163,8 +163,31 @@ export const DEFAULT_EXCLUDE_PATTERNS = [ '.git', '.vscode', '.idea', + + // Tests '.test.', '.spec.', '__tests__', '__mocks__', + + // Common noisy paths + 'test', + 'tests', + 'fixtures', + 'fixture', + 'examples', + 'example', + 'samples', + 'sandbox', + + // Generated / vendored / caches + '.turbo', + '.cache', + '.output', + '.vercel', + '.yarn', + '.pnpm-store', + '.parcel-cache', + '.rollup.cache', + '.DS_Store', ]; diff --git a/packages/cli/src/services/fileWalker.ts b/packages/cli/src/services/fileWalker.ts index c300990..9b09bae 100644 --- a/packages/cli/src/services/fileWalker.ts +++ b/packages/cli/src/services/fileWalker.ts @@ -40,6 +40,15 @@ export async function findFiles( ? [...defaultPatterns, ...opts.include] : defaultPatterns; const includePatterns = rawInclude.flatMap(expandBraceSets); + const explicitIncludePatterns = (opts.include ?? []).flatMap(expandBraceSets); + const explicitIncludeBases = explicitIncludePatterns + .map((pattern) => { + const normalized = pattern.replace(/\\/g, '/'); + const idx = normalized.search(/[*?\[\]{}]/); + const base = idx === -1 ? normalized : normalized.slice(0, idx); + return base.replace(/\/$/, ''); + }) + .filter(Boolean); const files: string[] = []; const walked = new Set(); @@ -76,9 +85,23 @@ export async function findFiles( for (const entry of entries) { const fullPath = path.join(startDir, entry.name); const relativeToRoot = path.relative(rootDir, fullPath); + const normalizedRelative = relativeToRoot.replace(/\\/g, '/'); + const explicitlyIncluded = + explicitIncludePatterns.length > 0 && + shouldInclude(entry.name, relativeToRoot, explicitIncludePatterns); + const includedDirectoryRoot = + entry.isDirectory() && + explicitIncludeBases.some( + (base) => + normalizedRelative === base || + normalizedRelative.startsWith(`${base}/`) || + base.startsWith(`${normalizedRelative}/`), + ); // Exclude checks should use path relative to *rootDir* (keeps existing semantics) if ( + !explicitlyIncluded && + !includedDirectoryRoot && shouldExclude(entry.name, relativeToRoot, [ ...DEFAULT_EXCLUDE_PATTERNS, ...(opts.exclude ?? []), diff --git a/packages/cli/test/e2e/frameworks/cli.nextJs.e2e.test.ts b/packages/cli/test/e2e/frameworks/cli.nextJs.e2e.test.ts index 4a26d79..ca7fb88 100644 --- a/packages/cli/test/e2e/frameworks/cli.nextJs.e2e.test.ts +++ b/packages/cli/test/e2e/frameworks/cli.nextJs.e2e.test.ts @@ -57,7 +57,7 @@ describe('Next.js environment variable usage rules', () => { }`, ); - fs.writeFileSync(path.join(cwd, '.env'), `SECRET_SERVER_KEY=ok`); + fs.writeFileSync(path.join(cwd, '.env'), 'SECRET_SERVER_KEY=ok'); const res = runCli(cwd, ['--scan-usage']); @@ -79,7 +79,7 @@ describe('Next.js environment variable usage rules', () => { console.log(process.env.NEXT_PUBLIC_IMAGE_BASE);`, ); - fs.writeFileSync(path.join(cwd, '.env'), `NEXT_PUBLIC_IMAGE_BASE=1`); + fs.writeFileSync(path.join(cwd, '.env'), 'NEXT_PUBLIC_IMAGE_BASE=1'); const res = runCli(cwd, ['--scan-usage']); @@ -98,7 +98,7 @@ console.log(process.env.NEXT_PUBLIC_IMAGE_BASE);`, console.log(process.env.SECRET_TOKEN);`, ); - fs.writeFileSync(path.join(cwd, '.env'), `SECRET_TOKEN=1`); + fs.writeFileSync(path.join(cwd, '.env'), 'SECRET_TOKEN=1'); const res = runCli(cwd, ['--scan-usage']); @@ -111,9 +111,9 @@ console.log(process.env.SECRET_TOKEN);`, const cwd = tmpDir(); makeNextProject(cwd); - fs.mkdirSync(path.join(cwd, 'app/api/test'), { recursive: true }); + fs.mkdirSync(path.join(cwd, 'app/api/code'), { recursive: true }); fs.writeFileSync( - path.join(cwd, 'app/api/test/route.ts'), + path.join(cwd, 'app/api/code/route.ts'), `export async function GET() { process.env.NEXT_PUBLIC_SECRET_PASSWORD; }`, @@ -121,7 +121,7 @@ console.log(process.env.SECRET_TOKEN);`, fs.writeFileSync( path.join(cwd, '.env'), - `NEXT_PUBLIC_SECRET_PASSWORD=secret123`, + 'NEXT_PUBLIC_SECRET_PASSWORD=secret123', ); const res = runCli(cwd, ['--scan-usage', '--json']); @@ -141,7 +141,7 @@ console.log(process.env.SECRET_TOKEN);`, expect(warning.reason).toContain( 'Potential sensitive environment variable exposed to the browser', ); - expect(warning.file).toContain('app/api/test/route.ts'); + expect(warning.file).toContain('app/api/code/route.ts'); expect(warning.line).toBeGreaterThan(0); }); @@ -228,7 +228,7 @@ API_ENDPOINT=api`, }`, ); - fs.writeFileSync(path.join(cwd, '.env'), `DATABASE_URL=postgresql://...`); + fs.writeFileSync(path.join(cwd, '.env'), 'DATABASE_URL=postgresql://...'); const res = runCli(cwd, ['--scan-usage']); @@ -249,7 +249,7 @@ API_ENDPOINT=api`, }`, ); - fs.writeFileSync(path.join(cwd, '.env'), `DATABASE_URL=db`); + fs.writeFileSync(path.join(cwd, '.env'), 'DATABASE_URL=db'); const res = runCli(cwd, ['--scan-usage']); @@ -262,13 +262,13 @@ API_ENDPOINT=api`, fs.mkdirSync(path.join(cwd, 'pages/api'), { recursive: true }); fs.writeFileSync( - path.join(cwd, 'pages/api/test.ts'), + path.join(cwd, 'pages/api/code.ts'), `export default function handler() { console.log(process.env.SECRET_KEY); }`, ); - fs.writeFileSync(path.join(cwd, '.env'), `SECRET_KEY=ok`); + fs.writeFileSync(path.join(cwd, '.env'), 'SECRET_KEY=ok'); const res = runCli(cwd, ['--scan-usage']); @@ -288,7 +288,7 @@ API_ENDPOINT=api`, }`, ); - fs.writeFileSync(path.join(cwd, '.env'), `SECRET_KEY=ok`); + fs.writeFileSync(path.join(cwd, '.env'), 'SECRET_KEY=ok'); const res = runCli(cwd, ['--scan-usage']); diff --git a/packages/cli/test/e2e/frameworks/cli.sveltekit.e2e.test.ts b/packages/cli/test/e2e/frameworks/cli.sveltekit.e2e.test.ts index 5256197..56f509e 100644 --- a/packages/cli/test/e2e/frameworks/cli.sveltekit.e2e.test.ts +++ b/packages/cli/test/e2e/frameworks/cli.sveltekit.e2e.test.ts @@ -47,10 +47,10 @@ describe('SvelteKit environment variable usage rules', () => { fs.writeFileSync( path.join(cwd, 'src/routes/+page.ts'), - `console.log(import.meta.env.PUBLIC_URL);`, + 'console.log(import.meta.env.PUBLIC_URL);', ); - fs.writeFileSync(path.join(cwd, '.env'), `PUBLIC_URL=123`); + fs.writeFileSync(path.join(cwd, '.env'), 'PUBLIC_URL=123'); const res = runCli(cwd, ['--scan-usage']); @@ -65,10 +65,10 @@ describe('SvelteKit environment variable usage rules', () => { fs.writeFileSync( path.join(cwd, 'src/routes/+page.ts'), - `console.log(import.meta.env.VITE_PUBLIC_URL);`, + 'console.log(import.meta.env.VITE_PUBLIC_URL);', ); - fs.writeFileSync(path.join(cwd, '.env'), `VITE_PUBLIC_URL=123`); + fs.writeFileSync(path.join(cwd, '.env'), 'VITE_PUBLIC_URL=123'); const res = runCli(cwd, ['--scan-usage']); @@ -82,10 +82,10 @@ describe('SvelteKit environment variable usage rules', () => { fs.writeFileSync( path.join(cwd, 'src/index.ts'), - `console.log(process.env.VITE_SECRET);`, + 'console.log(process.env.VITE_SECRET);', ); - fs.writeFileSync(path.join(cwd, '.env'), `VITE_SECRET=123`); + fs.writeFileSync(path.join(cwd, '.env'), 'VITE_SECRET=123'); const res = runCli(cwd, ['--scan-usage']); @@ -99,7 +99,7 @@ describe('SvelteKit environment variable usage rules', () => { fs.writeFileSync( path.join(cwd, 'src/app.ts'), - `import { VITE_PUBLIC } from '$env/static/public';`, + 'import { VITE_PUBLIC } from \'$env/static/public\';', ); fs.writeFileSync(path.join(cwd, '.env'), 'VITE_PUBLIC=123'); @@ -135,7 +135,7 @@ describe('SvelteKit environment variable usage rules', () => { fs.writeFileSync( path.join(cwd, 'src/routes/+page.svelte'), - `import { SECRET_KEY } from '$env/static/private';`, + 'import { SECRET_KEY } from \'$env/static/private\';', ); fs.writeFileSync(path.join(cwd, '.env'), 'SECRET_KEY=123'); @@ -174,8 +174,8 @@ describe('SvelteKit environment variable usage rules', () => { makeSvelteKitProject(cwd); fs.writeFileSync( - path.join(cwd, 'src/test.ts'), - `import { PUBLIC_TOKEN } from '$env/static/private';`, + path.join(cwd, 'src/svelteFile.ts'), + 'import { PUBLIC_TOKEN } from \'$env/static/private\';', ); fs.writeFileSync(path.join(cwd, '.env'), 'PUBLIC_TOKEN=123'); @@ -197,7 +197,7 @@ const url2 = import.meta.env.PUBLIC_URL; const url3 = import.meta.env.PUBLIC_URL;`, ); - fs.writeFileSync(path.join(cwd, '.env'), `PUBLIC_URL=123`); + fs.writeFileSync(path.join(cwd, '.env'), 'PUBLIC_URL=123'); const res = runCli(cwd, ['--scan-usage']); @@ -212,10 +212,10 @@ const url3 = import.meta.env.PUBLIC_URL;`, fs.writeFileSync( path.join(cwd, 'src/routes/+page.ts'), - `console.log(import.meta.env.PUBLIC_URL);`, + 'console.log(import.meta.env.PUBLIC_URL);', ); - fs.writeFileSync(path.join(cwd, '.env'), `PUBLIC_URL=123`); + fs.writeFileSync(path.join(cwd, '.env'), 'PUBLIC_URL=123'); const res = runCli(cwd, ['--scan-usage', '--strict']); @@ -230,10 +230,10 @@ const url3 = import.meta.env.PUBLIC_URL;`, fs.writeFileSync( path.join(cwd, 'src/routes/+page.server.ts'), - `console.log(import.meta.env.SECRET_KEY);`, + 'console.log(import.meta.env.SECRET_KEY);', ); - fs.writeFileSync(path.join(cwd, '.env'), `SECRET_KEY=123`); + fs.writeFileSync(path.join(cwd, '.env'), 'SECRET_KEY=123'); const res = runCli(cwd, ['--scan-usage']); @@ -247,10 +247,10 @@ const url3 = import.meta.env.PUBLIC_URL;`, fs.writeFileSync( path.join(cwd, 'src/routes/+server.ts'), - `console.log(import.meta.env.API_KEY);`, + 'console.log(import.meta.env.API_KEY);', ); - fs.writeFileSync(path.join(cwd, '.env'), `API_KEY=123`); + fs.writeFileSync(path.join(cwd, '.env'), 'API_KEY=123'); const res = runCli(cwd, ['--scan-usage']); @@ -302,7 +302,7 @@ const url3 = import.meta.env.PUBLIC_URL;`, fs.writeFileSync( path.join(cwd, 'src/routes/+page.svelte'), - `import SECRET_KEY from '$env/static/private';`, + 'import SECRET_KEY from \'$env/static/private\';', ); fs.writeFileSync(path.join(cwd, '.env'), 'SECRET_KEY=123'); @@ -516,9 +516,9 @@ const url3 = import.meta.env.PUBLIC_URL;`, fs.writeFileSync( path.join(cwd, 'src/+server.ts'), - `console.log(process.env.SECRET_KEY);`, + 'console.log(process.env.SECRET_KEY);', ); - fs.writeFileSync(path.join(cwd, '.env'), `SECRET_KEY=123`); + fs.writeFileSync(path.join(cwd, '.env'), 'SECRET_KEY=123'); const res = runCli(cwd, ['--scan-usage']); @@ -534,9 +534,9 @@ const url3 = import.meta.env.PUBLIC_URL;`, fs.writeFileSync( path.join(cwd, 'src/hooks.server.ts'), - `console.log(process.env.SECRET_KEY);`, + 'console.log(process.env.SECRET_KEY);', ); - fs.writeFileSync(path.join(cwd, '.env'), `SECRET_KEY=123`); + fs.writeFileSync(path.join(cwd, '.env'), 'SECRET_KEY=123'); const res = runCli(cwd, ['--scan-usage']); diff --git a/packages/cli/test/unit/services/filewalker.test.ts b/packages/cli/test/unit/services/filewalker.test.ts index 19b2d6e..0024236 100644 --- a/packages/cli/test/unit/services/filewalker.test.ts +++ b/packages/cli/test/unit/services/filewalker.test.ts @@ -335,6 +335,34 @@ describe('filewalker', () => { }); }); + describe('fileWalker - default exclude patterns', () => { + it('excludes files inside .vercel by default', () => { + const result = shouldExclude('config.js', '.vercel/output/config.js', [ + '.vercel', + ]); + + expect(result).toBe(true); + }); + + it('excludes files inside examples by default', () => { + const result = shouldExclude('demo.ts', 'examples/demo/demo.ts', [ + 'examples', + ]); + + expect(result).toBe(true); + }); + + it('does not exclude a normal source file when no exclude pattern matches', () => { + const result = shouldExclude('config.ts', 'src/config.ts', [ + '.vercel', + 'examples', + 'fixtures', + ]); + + expect(result).toBe(false); + }); + }); + describe('findFiles', () => { it('finds files with default patterns', async () => { const srcDir = path.join(tmpDir, 'src'); @@ -459,5 +487,47 @@ describe('filewalker', () => { expect(result).not.toContain(nodeModulesFile); expect(result).toContain(srcFile); }); + + it('allows include patterns to scan inside node_modules and .vercel', async () => { + const nodeModulesDir = path.join(tmpDir, 'node_modules', 'lib'); + const vercelDir = path.join(tmpDir, '.vercel', 'output'); + + fs.mkdirSync(nodeModulesDir, { recursive: true }); + fs.mkdirSync(vercelDir, { recursive: true }); + + const nmFile = path.join(nodeModulesDir, 'package.js'); + const vercelFile = path.join(vercelDir, 'config.js'); + + fs.writeFileSync(nmFile, ''); + fs.writeFileSync(vercelFile, ''); + + const result = await findFiles(tmpDir, { + include: ['node_modules/**/*.js', '.vercel/**/*.js'], + }); + + expect(result).toContain(nmFile); + expect(result).toContain(vercelFile); + }); + + it('allows --files patterns to scan inside node_modules and .vercel', async () => { + const nodeModulesDir = path.join(tmpDir, 'node_modules', 'lib'); + const vercelDir = path.join(tmpDir, '.vercel', 'output'); + + fs.mkdirSync(nodeModulesDir, { recursive: true }); + fs.mkdirSync(vercelDir, { recursive: true }); + + const nmFile = path.join(nodeModulesDir, 'package.js'); + const vercelFile = path.join(vercelDir, 'config.js'); + + fs.writeFileSync(nmFile, ''); + fs.writeFileSync(vercelFile, ''); + + const result = await findFiles(tmpDir, { + files: ['node_modules/**/*.js', '.vercel/**/*.js'], + }); + + expect(result).toContain(nmFile); + expect(result).toContain(vercelFile); + }); }); });