From 22875a6c9d86d801cffaaa8d6af42aed76ea3463 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Sat, 25 Jul 2026 20:06:01 +0800 Subject: [PATCH] fix(build): clean stale workspace residue --- package.json | 2 +- packages/examples/cli-demo/tsconfig.json | 3 +- scripts/clean.ts | 107 +++++++++++++++++++++++ 3 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 scripts/clean.ts diff --git a/package.json b/package.json index a925ea3ec9..f5017bea2f 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "scripts": { "build": "tsc -b && tsdown", "build:web": "pnpm --filter @deepseek-ai/dsh-frontend run build", - "clean:build": "rm -rf .typecheck packages/*/*/lib vendor/*/lib *.tsbuildinfo", + "clean": "tsx scripts/clean.ts", "typecheck": "tsc -b", "lint": "eslint .", "lint:fix": "eslint . --fix", diff --git a/packages/examples/cli-demo/tsconfig.json b/packages/examples/cli-demo/tsconfig.json index df5758b7b8..095f7ce6a3 100644 --- a/packages/examples/cli-demo/tsconfig.json +++ b/packages/examples/cli-demo/tsconfig.json @@ -3,8 +3,7 @@ "compilerOptions": { "composite": true, "rootDir": "src", - "outDir": "lib/types", - "tsBuildInfoFile": "../../../.typecheck/cli-demo.tsbuildinfo" + "outDir": "lib/types" }, "include": ["src/**/*.ts"], "references": [ diff --git a/scripts/clean.ts b/scripts/clean.ts new file mode 100644 index 0000000000..3f0f85836f --- /dev/null +++ b/scripts/clean.ts @@ -0,0 +1,107 @@ +import { lstat, readdir, rm } from 'node:fs/promises' +import { dirname, join, relative, resolve, sep } from 'node:path' +import { fileURLToPath } from 'node:url' + +const knownOrphanEntries = new Set(['node_modules', 'lib', '.typecheck']) + +function isMissing(error: unknown): boolean { + return error instanceof Error && 'code' in error && error.code === 'ENOENT' +} + +async function exists(path: string): Promise { + try { + await lstat(path) + return true + } catch (error) { + if (isMissing(error)) return false + throw error + } +} + +async function childDirectories(path: string): Promise { + try { + const entries = await readdir(path, { withFileTypes: true }) + return entries.filter(entry => entry.isDirectory()).map(entry => join(path, entry.name)) + } catch (error) { + if (isMissing(error)) return [] + throw error + } +} + +function repositoryPath(root: string, path: string): string { + return relative(root, path).split(sep).join('/') +} + +class RepositoryCleaner { + constructor(private readonly root: string) {} + + /** + * Remove generated build state and package directories containing only known residue. + * @returns Repository-relative paths that were removed. + */ + async clean(): Promise { + const targets = await this.plan() + for (const target of targets) await rm(target, { recursive: true, force: true }) + return targets.map(target => repositoryPath(this.root, target)) + } + + private async plan(): Promise { + const targets = new Set() + const unsafeOrphans: string[] = [] + + await this.addIfPresent(targets, join(this.root, '.typecheck')) + for (const entry of await readdir(this.root, { withFileTypes: true })) { + if (entry.isFile() && entry.name.endsWith('.tsbuildinfo')) targets.add(join(this.root, entry.name)) + } + + for (const vendorDirectory of await childDirectories(join(this.root, 'vendor'))) { + await this.addIfPresent(targets, join(vendorDirectory, 'lib')) + } + await this.addIfPresent(targets, join(this.root, 'apps', 'cli', 'lib')) + + for (const groupDirectory of await childDirectories(join(this.root, 'packages'))) { + for (const packageDirectory of await childDirectories(groupDirectory)) { + if (await exists(join(packageDirectory, 'package.json'))) { + await this.addIfPresent(targets, join(packageDirectory, 'lib')) + continue + } + + const entries = await readdir(packageDirectory) + const unknown = entries.filter(entry => !knownOrphanEntries.has(entry) && !entry.endsWith('.tsbuildinfo')) + if (unknown.length > 0) { + unsafeOrphans.push(...unknown.map(entry => repositoryPath(this.root, join(packageDirectory, entry)))) + } else { + targets.add(packageDirectory) + } + } + } + + if (unsafeOrphans.length > 0) { + throw new Error([ + 'clean: refusing to remove package directories without package.json; unknown entries remain:', + ...unsafeOrphans.sort().map(path => ` ${path}`), + ].join('\n')) + } + + return [...targets].sort() + } + + private async addIfPresent(targets: Set, path: string): Promise { + if (await exists(path)) targets.add(path) + } +} + +const scriptPath = fileURLToPath(import.meta.url) +if (process.argv[1] !== undefined && resolve(process.argv[1]) === scriptPath) { + try { + const removed = await new RepositoryCleaner(resolve(dirname(scriptPath), '..')).clean() + if (removed.length === 0) { + console.log('clean: already clean') + } else { + console.log(`clean: removed ${removed.length} paths`) + } + } catch (error) { + console.error(error instanceof Error ? error.message : error) + process.exitCode = 1 + } +}