clean.spec.ts 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. import { existsSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
  2. import { tmpdir } from 'node:os'
  3. import { dirname, join } from 'node:path'
  4. import { afterEach, describe, expect, it } from 'vitest'
  5. import { RepositoryCleaner } from './clean.ts'
  6. const roots: string[] = []
  7. function fixture(): string {
  8. const root = mkdtempSync(join(tmpdir(), 'dsh-clean-'))
  9. roots.push(root)
  10. return root
  11. }
  12. function write(path: string, content = ''): void {
  13. mkdirSync(dirname(path), { recursive: true })
  14. writeFileSync(path, content)
  15. }
  16. function addProject(root: string, path: string): void {
  17. write(join(root, 'tsconfig.json'), JSON.stringify({ files: [], references: [{ path }] }))
  18. write(join(root, path, 'tsconfig.json'), JSON.stringify({
  19. compilerOptions: { composite: true, outDir: 'lib/types' },
  20. include: ['src'],
  21. }))
  22. write(join(root, path, 'src/index.ts'), 'export {}\n')
  23. }
  24. afterEach(() => {
  25. for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
  26. })
  27. describe('RepositoryCleaner', () => {
  28. it('derives live build outputs from project references and removes safe stale package residue', async () => {
  29. const root = fixture()
  30. addProject(root, 'products/shell')
  31. write(join(root, 'products/shell/lib/types/index.js'))
  32. write(join(root, 'products/shell/lib/index.js'))
  33. write(join(root, '.typecheck/legacy.tsbuildinfo'))
  34. write(join(root, 'root.tsbuildinfo'))
  35. write(join(root, 'packages/removed/ghost/node_modules/.bin/tool'))
  36. await new RepositoryCleaner(root).clean()
  37. expect(existsSync(join(root, 'products/shell/lib'))).toBe(false)
  38. expect(existsSync(join(root, 'products/shell/src/index.ts'))).toBe(true)
  39. expect(existsSync(join(root, '.typecheck'))).toBe(false)
  40. expect(existsSync(join(root, 'root.tsbuildinfo'))).toBe(false)
  41. expect(existsSync(join(root, 'packages/removed/ghost'))).toBe(false)
  42. })
  43. it('does not delete any target when a manifest-less package contains an unknown file', async () => {
  44. const root = fixture()
  45. addProject(root, 'products/shell')
  46. write(join(root, 'products/shell/lib/types/index.js'))
  47. write(join(root, 'packages/removed/ghost/notes.txt'))
  48. await expect(new RepositoryCleaner(root).clean()).rejects.toThrow('packages/removed/ghost/notes.txt')
  49. expect(existsSync(join(root, 'products/shell/lib'))).toBe(true)
  50. })
  51. it('refuses project outputs reached through a symlink outside the repository', async () => {
  52. const root = fixture()
  53. const externalProject = fixture()
  54. write(join(root, 'tsconfig.json'), JSON.stringify({ files: [], references: [{ path: './linked' }] }))
  55. write(join(externalProject, 'tsconfig.json'), JSON.stringify({
  56. compilerOptions: { composite: true, outDir: 'lib/types' },
  57. include: ['src'],
  58. }))
  59. write(join(externalProject, 'src/index.ts'), 'export {}\n')
  60. write(join(externalProject, 'lib/types/index.js'))
  61. symlinkSync(externalProject, join(root, 'linked'), process.platform === 'win32' ? 'junction' : 'dir')
  62. await expect(new RepositoryCleaner(root).clean()).rejects.toThrow('outside repository')
  63. expect(existsSync(join(externalProject, 'lib/types/index.js'))).toBe(true)
  64. })
  65. })