ReviewOS

also looking at this

stacks/buddy

fix: prevent generated dependency cache PRs

#1366
Merged chris wants to merge codex/prevent-generated-pantry-prs into main
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.

src/registry/registry-client.tsmodified+3-18
Changes to src/registry/registry-client.ts
@@ -7,6 +7,7 @@ import path from 'node:path'
77import process from 'node:process'
88import { PackageRegistryError } from '../types'
99import { getUpdateType } from '../utils/helpers'
10import { shouldSkipPackageDirectory } from '../utils/package-paths'
1011
1112export interface BunOutdatedResult {
1213 name: string
@@ -1342,26 +1343,10 @@ export class RegistryClient {
13421343 }
13431344
13441345 /**
1345 * Check if a directory should be skipped during scanning
1346 * Check if a directory is safe to descend into during package discovery.
13461347 */
13471348 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)
13651350 }
13661351
13671352 /**
src/scanner/package-scanner.tsmodified+4-26
Changes to src/scanner/package-scanner.ts
@@ -10,6 +10,7 @@ import { BuddyError } from '../types'
1010import { isDependencyFile, parseDependencyFile as parseDepFile } from '../utils/dependency-file-parser'
1111import { isDockerfile, parseDockerfile as parseDockerfileUtil } from '../utils/dockerfile-parser'
1212import { isGitHubActionsFile, parseGitHubActionsFile } from '../utils/github-actions-parser'
13import { shouldSkipPackageDirectory } from '../utils/package-paths'
1314import { parsePantryLockFile } from '../utils/pantry-parser'
1415import { parseZigManifest } from '../utils/zig-parser'
1516
@@ -537,7 +538,7 @@ export class PackageScanner {
537538
538539 if (stats.isDirectory()) {
539540 // Skip node_modules and other common ignored directories
540 if (!this.shouldSkipDirectory(entry)) {
541 if (!shouldSkipPackageDirectory(entry)) {
541542 const subFiles = await this.findFiles(fileName, fullPath)
542543 files.push(...subFiles)
543544 }
@@ -597,7 +598,7 @@ export class PackageScanner {
597598 // Normalize path separators to forward slashes for consistency
598599 files.push(relativePath.split(sep).join('/'))
599600 }
600 else if (stats.isDirectory() && !this.shouldSkipDirectory(entry)) {
601 else if (stats.isDirectory() && !shouldSkipPackageDirectory(entry)) {
601602 // Recursively search subdirectories
602603 const subFiles = await this.findFilesByPatternInDir(pattern, fullPath)
603604 files.push(...subFiles)
@@ -631,7 +632,7 @@ export class PackageScanner {
631632 // Normalize path separators to forward slashes for consistency
632633 files.push(relativePath.split(sep).join('/'))
633634 }
634 else if (stats.isDirectory() && !this.shouldSkipDirectory(entry)) {
635 else if (stats.isDirectory() && !shouldSkipPackageDirectory(entry)) {
635636 const subFiles = await this.findFilesByPatternInDir(pattern, fullPath)
636637 files.push(...subFiles)
637638 }
@@ -670,29 +671,6 @@ export class PackageScanner {
670671 return false
671672 }
672673
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
696674 /**
697675 * Get relative path from project root
698676 */
src/utils/package-paths.tsadded+32-0
Changes to src/utils/package-paths.ts
@@ -0,0 +1,32 @@
1const GENERATED_DEPENDENCY_DIRECTORIES = new Set([
2 'bower_components',
3 'node_modules',
4 'pantry',
5 'ts-pantry',
6 'vendor',
7])
8
9const 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 */
28export function shouldSkipPackageDirectory(dirName: string): boolean {
29 return dirName.startsWith('.')
30 || GENERATED_DEPENDENCY_DIRECTORIES.has(dirName)
31 || GENERATED_OUTPUT_DIRECTORIES.has(dirName)
32}
test/generated-dependency-directories.test.tsadded+71-0
Changes to test/generated-dependency-directories.test.ts
@@ -0,0 +1,71 @@
1import { afterEach, describe, expect, it } from 'bun:test'
2import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
3import { tmpdir } from 'node:os'
4import { join } from 'node:path'
5import { RegistryClient } from '../src/registry/registry-client'
6import { PackageScanner } from '../src/scanner/package-scanner'
7import { Logger } from '../src/utils/logger'
8import { shouldSkipPackageDirectory } from '../src/utils/package-paths'
9
10const generatedDirectories = [
11 'bower_components',
12 'node_modules',
13 'pantry',
14 'ts-pantry',
15 'vendor',
16]
17
18describe('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
62async 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}