clean.ts 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. import { lstat, readdir, rm } from 'node:fs/promises'
  2. import { dirname, join, relative, resolve, sep } from 'node:path'
  3. import { fileURLToPath } from 'node:url'
  4. const knownOrphanEntries = new Set(['node_modules', 'lib', '.typecheck'])
  5. function isMissing(error: unknown): boolean {
  6. return error instanceof Error && 'code' in error && error.code === 'ENOENT'
  7. }
  8. async function exists(path: string): Promise<boolean> {
  9. try {
  10. await lstat(path)
  11. return true
  12. } catch (error) {
  13. if (isMissing(error)) return false
  14. throw error
  15. }
  16. }
  17. async function childDirectories(path: string): Promise<string[]> {
  18. try {
  19. const entries = await readdir(path, { withFileTypes: true })
  20. return entries.filter(entry => entry.isDirectory()).map(entry => join(path, entry.name))
  21. } catch (error) {
  22. if (isMissing(error)) return []
  23. throw error
  24. }
  25. }
  26. function repositoryPath(root: string, path: string): string {
  27. return relative(root, path).split(sep).join('/')
  28. }
  29. class RepositoryCleaner {
  30. constructor(private readonly root: string) {}
  31. /**
  32. * Remove generated build state and package directories containing only known residue.
  33. * @returns Repository-relative paths that were removed.
  34. */
  35. async clean(): Promise<string[]> {
  36. const targets = await this.plan()
  37. for (const target of targets) await rm(target, { recursive: true, force: true })
  38. return targets.map(target => repositoryPath(this.root, target))
  39. }
  40. private async plan(): Promise<string[]> {
  41. const targets = new Set<string>()
  42. const unsafeOrphans: string[] = []
  43. await this.addIfPresent(targets, join(this.root, '.typecheck'))
  44. for (const entry of await readdir(this.root, { withFileTypes: true })) {
  45. if (entry.isFile() && entry.name.endsWith('.tsbuildinfo')) targets.add(join(this.root, entry.name))
  46. }
  47. for (const vendorDirectory of await childDirectories(join(this.root, 'vendor'))) {
  48. await this.addIfPresent(targets, join(vendorDirectory, 'lib'))
  49. }
  50. await this.addIfPresent(targets, join(this.root, 'apps', 'cli', 'lib'))
  51. for (const groupDirectory of await childDirectories(join(this.root, 'packages'))) {
  52. for (const packageDirectory of await childDirectories(groupDirectory)) {
  53. if (await exists(join(packageDirectory, 'package.json'))) {
  54. await this.addIfPresent(targets, join(packageDirectory, 'lib'))
  55. continue
  56. }
  57. const entries = await readdir(packageDirectory)
  58. const unknown = entries.filter(entry => !knownOrphanEntries.has(entry) && !entry.endsWith('.tsbuildinfo'))
  59. if (unknown.length > 0) {
  60. unsafeOrphans.push(...unknown.map(entry => repositoryPath(this.root, join(packageDirectory, entry))))
  61. } else {
  62. targets.add(packageDirectory)
  63. }
  64. }
  65. }
  66. if (unsafeOrphans.length > 0) {
  67. throw new Error([
  68. 'clean: refusing to remove package directories without package.json; unknown entries remain:',
  69. ...unsafeOrphans.sort().map(path => ` ${path}`),
  70. ].join('\n'))
  71. }
  72. return [...targets].sort()
  73. }
  74. private async addIfPresent(targets: Set<string>, path: string): Promise<void> {
  75. if (await exists(path)) targets.add(path)
  76. }
  77. }
  78. const scriptPath = fileURLToPath(import.meta.url)
  79. if (process.argv[1] !== undefined && resolve(process.argv[1]) === scriptPath) {
  80. try {
  81. const removed = await new RepositoryCleaner(resolve(dirname(scriptPath), '..')).clean()
  82. if (removed.length === 0) {
  83. console.log('clean: already clean')
  84. } else {
  85. console.log(`clean: removed ${removed.length} paths`)
  86. }
  87. } catch (error) {
  88. console.error(error instanceof Error ? error.message : error)
  89. process.exitCode = 1
  90. }
  91. }