also looking at this
fix: prevent generated dependency cache PRs
#1366
16 files
+356
-72
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -7,6 +7,7 @@ import path from 'node:path' | ||
| 7 | 7 | import process from 'node:process' |
| 8 | 8 | import { PackageRegistryError } from '../types' |
| 9 | 9 | import { getUpdateType } from '../utils/helpers' |
| 10 | import { shouldSkipPackageDirectory } from '../utils/package-paths' | |
| 10 | 11 | |
| 11 | 12 | export interface BunOutdatedResult { |
| 12 | 13 | name: string |
| @@ -1342,26 +1343,10 @@ export class RegistryClient { | ||
| 1342 | 1343 | } |
| 1343 | 1344 | |
| 1344 | 1345 | /** |
| 1345 | * Check if a directory should be skipped during scanning | |
| 1346 | * Check if a directory is safe to descend into during package discovery. | |
| 1346 | 1347 | */ |
| 1347 | 1348 | private shouldSkipDirectory(dirName: string): boolean { |
| 1348 | const skipDirs = [ | |
| 1349 | 'node_modules', | |
| 1350 | '.git', | |
| 1351 | '.next', | |
| 1352 | '.nuxt', | |
| 1353 | 'dist', | |
| 1354 | 'build', | |
| 1355 | 'coverage', | |
| 1356 | '.nyc_output', | |
| 1357 | 'tmp', | |
| 1358 | 'temp', | |
| 1359 | '.cache', | |
| 1360 | '.vscode', | |
| 1361 | '.idea', | |
| 1362 | ] | |
| 1363 | ||
| 1364 | return skipDirs.includes(dirName) || dirName.startsWith('.') | |
| 1349 | return shouldSkipPackageDirectory(dirName) | |
| 1365 | 1350 | } |
| 1366 | 1351 | |
| 1367 | 1352 | /** |
| @@ -10,6 +10,7 @@ import { BuddyError } from '../types' | ||
| 10 | 10 | import { isDependencyFile, parseDependencyFile as parseDepFile } from '../utils/dependency-file-parser' |
| 11 | 11 | import { isDockerfile, parseDockerfile as parseDockerfileUtil } from '../utils/dockerfile-parser' |
| 12 | 12 | import { isGitHubActionsFile, parseGitHubActionsFile } from '../utils/github-actions-parser' |
| 13 | import { shouldSkipPackageDirectory } from '../utils/package-paths' | |
| 13 | 14 | import { parsePantryLockFile } from '../utils/pantry-parser' |
| 14 | 15 | import { parseZigManifest } from '../utils/zig-parser' |
| 15 | 16 | |
| @@ -537,7 +538,7 @@ export class PackageScanner { | ||
| 537 | 538 | |
| 538 | 539 | if (stats.isDirectory()) { |
| 539 | 540 | // Skip node_modules and other common ignored directories |
| 540 | if (!this.shouldSkipDirectory(entry)) { | |
| 541 | if (!shouldSkipPackageDirectory(entry)) { | |
| 541 | 542 | const subFiles = await this.findFiles(fileName, fullPath) |
| 542 | 543 | files.push(...subFiles) |
| 543 | 544 | } |
| @@ -597,7 +598,7 @@ export class PackageScanner { | ||
| 597 | 598 | // Normalize path separators to forward slashes for consistency |
| 598 | 599 | files.push(relativePath.split(sep).join('/')) |
| 599 | 600 | } |
| 600 | else if (stats.isDirectory() && !this.shouldSkipDirectory(entry)) { | |
| 601 | else if (stats.isDirectory() && !shouldSkipPackageDirectory(entry)) { | |
| 601 | 602 | // Recursively search subdirectories |
| 602 | 603 | const subFiles = await this.findFilesByPatternInDir(pattern, fullPath) |
| 603 | 604 | files.push(...subFiles) |
| @@ -631,7 +632,7 @@ export class PackageScanner { | ||
| 631 | 632 | // Normalize path separators to forward slashes for consistency |
| 632 | 633 | files.push(relativePath.split(sep).join('/')) |
| 633 | 634 | } |
| 634 | else if (stats.isDirectory() && !this.shouldSkipDirectory(entry)) { | |
| 635 | else if (stats.isDirectory() && !shouldSkipPackageDirectory(entry)) { | |
| 635 | 636 | const subFiles = await this.findFilesByPatternInDir(pattern, fullPath) |
| 636 | 637 | files.push(...subFiles) |
| 637 | 638 | } |
| @@ -670,29 +671,6 @@ export class PackageScanner { | ||
| 670 | 671 | return false |
| 671 | 672 | } |
| 672 | 673 | |
| 673 | /** | |
| 674 | * Check if a directory should be skipped during scanning | |
| 675 | */ | |
| 676 | private shouldSkipDirectory(dirName: string): boolean { | |
| 677 | const skipDirs = [ | |
| 678 | 'node_modules', | |
| 679 | '.git', | |
| 680 | '.next', | |
| 681 | '.nuxt', | |
| 682 | 'dist', | |
| 683 | 'build', | |
| 684 | 'coverage', | |
| 685 | '.nyc_output', | |
| 686 | 'tmp', | |
| 687 | 'temp', | |
| 688 | '.cache', | |
| 689 | '.vscode', | |
| 690 | '.idea', | |
| 691 | ] | |
| 692 | ||
| 693 | return skipDirs.includes(dirName) || dirName.startsWith('.') | |
| 694 | } | |
| 695 | ||
| 696 | 674 | /** |
| 697 | 675 | * Get relative path from project root |
| 698 | 676 | */ |
| @@ -0,0 +1,32 @@ | ||
| 1 | const GENERATED_DEPENDENCY_DIRECTORIES = new Set([ | |
| 2 | 'bower_components', | |
| 3 | 'node_modules', | |
| 4 | 'pantry', | |
| 5 | 'ts-pantry', | |
| 6 | 'vendor', | |
| 7 | ]) | |
| 8 | ||
| 9 | const GENERATED_OUTPUT_DIRECTORIES = new Set([ | |
| 10 | '.cache', | |
| 11 | '.idea', | |
| 12 | '.next', | |
| 13 | '.nuxt', | |
| 14 | '.nyc_output', | |
| 15 | '.vscode', | |
| 16 | 'build', | |
| 17 | 'coverage', | |
| 18 | 'dist', | |
| 19 | 'temp', | |
| 20 | 'tmp', | |
| 21 | ]) | |
| 22 | ||
| 23 | /** | |
| 24 | * Package discovery must not descend into installed dependencies, generated | |
| 25 | * package-manager trees, or build output. Manifests in those directories are | |
| 26 | * implementation details, not dependency declarations owned by the project. | |
| 27 | */ | |
| 28 | export function shouldSkipPackageDirectory(dirName: string): boolean { | |
| 29 | return dirName.startsWith('.') | |
| 30 | || GENERATED_DEPENDENCY_DIRECTORIES.has(dirName) | |
| 31 | || GENERATED_OUTPUT_DIRECTORIES.has(dirName) | |
| 32 | } | |
| @@ -0,0 +1,71 @@ | ||
| 1 | import { afterEach, describe, expect, it } from 'bun:test' | |
| 2 | import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' | |
| 3 | import { tmpdir } from 'node:os' | |
| 4 | import { join } from 'node:path' | |
| 5 | import { RegistryClient } from '../src/registry/registry-client' | |
| 6 | import { PackageScanner } from '../src/scanner/package-scanner' | |
| 7 | import { Logger } from '../src/utils/logger' | |
| 8 | import { shouldSkipPackageDirectory } from '../src/utils/package-paths' | |
| 9 | ||
| 10 | const generatedDirectories = [ | |
| 11 | 'bower_components', | |
| 12 | 'node_modules', | |
| 13 | 'pantry', | |
| 14 | 'ts-pantry', | |
| 15 | 'vendor', | |
| 16 | ] | |
| 17 | ||
| 18 | describe('generated dependency directory filtering', () => { | |
| 19 | const temporaryDirectories: string[] = [] | |
| 20 | const logger = Logger.quiet() | |
| 21 | ||
| 22 | afterEach(async () => { | |
| 23 | await Promise.all(temporaryDirectories.splice(0).map(directory => | |
| 24 | rm(directory, { recursive: true, force: true }), | |
| 25 | )) | |
| 26 | }) | |
| 27 | ||
| 28 | it('classifies package-manager install trees as generated', () => { | |
| 29 | for (const directory of generatedDirectories) | |
| 30 | expect(shouldSkipPackageDirectory(directory)).toBe(true) | |
| 31 | ||
| 32 | expect(shouldSkipPackageDirectory('packages')).toBe(false) | |
| 33 | }) | |
| 34 | ||
| 35 | it('excludes generated manifests from both package discovery paths', async () => { | |
| 36 | const projectPath = await mkdtemp(join(tmpdir(), 'buddy-generated-dependencies-')) | |
| 37 | temporaryDirectories.push(projectPath) | |
| 38 | ||
| 39 | await writePackageJson(projectPath, 'package.json', 'root') | |
| 40 | await writePackageJson(projectPath, 'packages/app/package.json', 'app') | |
| 41 | ||
| 42 | for (const directory of generatedDirectories) | |
| 43 | await writePackageJson(projectPath, `${directory}/cached/package.json`, `generated-${directory}`) | |
| 44 | ||
| 45 | const scanner = new PackageScanner(projectPath, logger) | |
| 46 | const scannedPaths = (await scanner.scanProject()).map(file => file.path) | |
| 47 | ||
| 48 | const registryClient = new RegistryClient(projectPath, logger) | |
| 49 | const registryPaths = await (registryClient as any).findPackageJsonFiles() | |
| 50 | ||
| 51 | expect(scannedPaths.filter(path => path.endsWith('package.json')).sort()).toEqual([ | |
| 52 | 'package.json', | |
| 53 | 'packages/app/package.json', | |
| 54 | ]) | |
| 55 | expect(registryPaths.sort()).toEqual([ | |
| 56 | 'package.json', | |
| 57 | 'packages/app/package.json', | |
| 58 | ]) | |
| 59 | }) | |
| 60 | }) | |
| 61 | ||
| 62 | async function writePackageJson(projectPath: string, relativePath: string, name: string): Promise<void> { | |
| 63 | const filePath = join(projectPath, relativePath) | |
| 64 | await mkdir(join(filePath, '..'), { recursive: true }) | |
| 65 | await writeFile(filePath, JSON.stringify({ | |
| 66 | name, | |
| 67 | dependencies: { | |
| 68 | acorn: '^8.0.0', | |
| 69 | }, | |
| 70 | })) | |
| 71 | } | |