fix(scripts): remove dangling pnpm links in clean
pnpm install keeps hoisted links to deleted workspace packages, and the Desktop development build fails reading their package.json. Fixes #6823
This commit is contained in:
1 parent
051ac9154b
commit
81ee28eebb
2 files changed
+53
-3
No files matched your search
@@ -82,6 +82,28 @@ describe('RepositoryCleaner', () => {
|
||||
expect(existsSync(join(root, 'native/system/tsconfig.tsbuildinfo'))).toBe(false)
|
||||
})
|
||||
|
||||
it('removes pnpm links to deleted packages and keeps live links', async () => {
|
||||
const root = fixture()
|
||||
const type = process.platform === 'win32' ? 'junction' : 'dir'
|
||||
addProject(root, 'packages/core/live')
|
||||
write(join(root, 'packages/core/live/package.json'), '{}')
|
||||
const hoisted = join(root, 'node_modules/.pnpm/node_modules')
|
||||
mkdirSync(join(hoisted, '@deepseek-ai'), { recursive: true })
|
||||
symlinkSync(join(root, 'packages/core/live'), join(hoisted, '@deepseek-ai/dsh-live'), type)
|
||||
symlinkSync(join(root, 'packages/hooks/gone'), join(hoisted, '@deepseek-ai/dsh-gone'), type)
|
||||
symlinkSync(join(root, 'packages/hooks/plain'), join(hoisted, 'plain'), type)
|
||||
symlinkSync(join(root, 'packages/hooks/top'), join(root, 'node_modules/top'), type)
|
||||
|
||||
const removed = await new RepositoryCleaner(root).clean()
|
||||
|
||||
expect(removed).toEqual([
|
||||
'node_modules/.pnpm/node_modules/@deepseek-ai/dsh-gone',
|
||||
'node_modules/.pnpm/node_modules/plain',
|
||||
'node_modules/top',
|
||||
])
|
||||
expect(existsSync(join(hoisted, '@deepseek-ai/dsh-live/package.json'))).toBe(true)
|
||||
})
|
||||
|
||||
it('refuses project outputs reached through a symlink outside the repository', async () => {
|
||||
const root = fixture()
|
||||
const externalProject = fixture()
|
||||
|
||||
+31
-3
@@ -1,4 +1,4 @@
|
||||
import { lstat, readdir, realpath, rm } from 'node:fs/promises'
|
||||
import { lstat, readdir, realpath, rm, stat } from 'node:fs/promises'
|
||||
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import ts from 'typescript'
|
||||
@@ -10,9 +10,9 @@ function isMissing(error: unknown): boolean {
|
||||
return error instanceof Error && 'code' in error && error.code === 'ENOENT'
|
||||
}
|
||||
|
||||
async function exists(path: string): Promise<boolean> {
|
||||
async function exists(path: string, probe: typeof lstat = lstat): Promise<boolean> {
|
||||
try {
|
||||
await lstat(path)
|
||||
await probe(path)
|
||||
return true
|
||||
} catch (error) {
|
||||
if (isMissing(error)) return false
|
||||
@@ -30,6 +30,27 @@ async function childDirectories(path: string): Promise<string[]> {
|
||||
}
|
||||
}
|
||||
|
||||
/** Lists symbolic links whose targets are missing in a node_modules directory and its `@scope` directories. */
|
||||
async function danglingLinks(modulesDirectory: string): Promise<string[]> {
|
||||
const links: string[] = []
|
||||
const scan = async (directory: string, scopes: boolean): Promise<void> => {
|
||||
let entries
|
||||
try {
|
||||
entries = await readdir(directory, { withFileTypes: true })
|
||||
} catch (error) {
|
||||
if (isMissing(error)) return
|
||||
throw error
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const path = join(directory, entry.name)
|
||||
if (scopes && entry.isDirectory() && entry.name.startsWith('@')) await scan(path, false)
|
||||
else if (entry.isSymbolicLink() && !await exists(path, stat)) links.push(path)
|
||||
}
|
||||
}
|
||||
await scan(modulesDirectory, true)
|
||||
return links
|
||||
}
|
||||
|
||||
function repositoryPath(root: string, path: string): string {
|
||||
return relative(root, path).split(sep).join('/')
|
||||
}
|
||||
@@ -88,6 +109,13 @@ export class RepositoryCleaner {
|
||||
await this.addIfPresent(targets, outputDirectory, canonicalRoot)
|
||||
}
|
||||
|
||||
// pnpm install keeps links whose targets are gone, such as hoisted links to deleted workspace packages.
|
||||
for (const modulesDirectory of ['node_modules', 'node_modules/.pnpm/node_modules']) {
|
||||
for (const link of await danglingLinks(join(this.root, modulesDirectory))) {
|
||||
await this.addIfPresent(targets, link, canonicalRoot)
|
||||
}
|
||||
}
|
||||
|
||||
for (const groupDirectory of await childDirectories(join(this.root, 'packages'))) {
|
||||
for (const packageDirectory of await childDirectories(groupDirectory)) {
|
||||
// A package.json marks a live package; its output was discovered from the
|
||||
|
||||
Reference in new issue
Block a user