diff --git a/packages/compiler-cli/integrationtest/BUILD.bazel b/packages/compiler-cli/integrationtest/BUILD.bazel index 450a81d041..5774b13445 100644 --- a/packages/compiler-cli/integrationtest/BUILD.bazel +++ b/packages/compiler-cli/integrationtest/BUILD.bazel @@ -31,6 +31,7 @@ nodejs_test( "@nodejs//:node", "@npm//domino", "@npm//chokidar", + "@npm//fs-extra", "@npm//source-map-support", "@npm//shelljs", "@npm//typescript", diff --git a/packages/compiler-cli/ngcc/src/main.ts b/packages/compiler-cli/ngcc/src/main.ts index 38f7b9c512..4abaf878f7 100644 --- a/packages/compiler-cli/ngcc/src/main.ts +++ b/packages/compiler-cli/ngcc/src/main.ts @@ -38,12 +38,12 @@ import {EntryPoint, EntryPointJsonProperty, EntryPointPackageJson, SUPPORTED_FOR import {makeEntryPointBundle} from './packages/entry_point_bundle'; import {Transformer} from './packages/transformer'; import {PathMappings} from './utils'; +import {cleanOutdatedPackages} from './writing/cleaning/package_cleaner'; import {FileWriter} from './writing/file_writer'; import {InPlaceFileWriter} from './writing/in_place_file_writer'; import {NewEntryPointFileWriter} from './writing/new_entry_point_file_writer'; import {DirectPackageJsonUpdater, PackageJsonUpdater} from './writing/package_json_updater'; - /** * The options to configure the ngcc compiler for synchronous execution. */ @@ -188,10 +188,19 @@ export function mainNgcc( const absBasePath = absoluteFrom(basePath); const config = new NgccConfiguration(fileSystem, dirname(absBasePath)); - const {entryPoints, graph} = getEntryPoints( + let entryPointInfo = getEntryPoints( fileSystem, pkgJsonUpdater, logger, dependencyResolver, config, absBasePath, absoluteTargetEntryPointPath, pathMappings); + const cleaned = cleanOutdatedPackages(fileSystem, entryPointInfo.entryPoints); + if (cleaned) { + // If we had to clean up one or more packages then we must read in the entry-points again. + entryPointInfo = getEntryPoints( + fileSystem, pkgJsonUpdater, logger, dependencyResolver, config, absBasePath, + absoluteTargetEntryPointPath, pathMappings); + } + const {entryPoints, graph} = entryPointInfo; + const unprocessableEntryPointPaths: string[] = []; // The tasks are partially ordered by virtue of the entry-points being partially ordered too. const tasks: PartiallyOrderedTasks = [] as any; diff --git a/packages/compiler-cli/ngcc/src/writing/cleaning/cleaning_strategies.ts b/packages/compiler-cli/ngcc/src/writing/cleaning/cleaning_strategies.ts new file mode 100644 index 0000000000..3bf1861955 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/writing/cleaning/cleaning_strategies.ts @@ -0,0 +1,63 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {AbsoluteFsPath, FileSystem, PathSegment, absoluteFrom} from '../../../../src/ngtsc/file_system'; +import {cleanPackageJson} from '../../packages/build_marker'; +import {NGCC_BACKUP_EXTENSION} from '../in_place_file_writer'; +import {NGCC_DIRECTORY} from '../new_entry_point_file_writer'; +import {isLocalDirectory} from './utils'; + +/** +* Implement this interface to extend the cleaning strategies of the `PackageCleaner`. +*/ +export interface CleaningStrategy { + canClean(path: AbsoluteFsPath, basename: PathSegment): boolean; + clean(path: AbsoluteFsPath, basename: PathSegment): void; +} + +/** + * A CleaningStrategy that reverts changes to package.json files by removing the build marker and + * other properties. + */ +export class PackageJsonCleaner implements CleaningStrategy { + constructor(private fs: FileSystem) {} + canClean(_path: AbsoluteFsPath, basename: PathSegment): boolean { + return basename === 'package.json'; + } + clean(path: AbsoluteFsPath, _basename: PathSegment): void { + const packageJson = JSON.parse(this.fs.readFile(path)); + if (cleanPackageJson(packageJson)) { + this.fs.writeFile(path, `${JSON.stringify(packageJson, null, 2)}\n`); + } + } +} + +/** + * A CleaningStrategy that removes the extra directory containing generated entry-point formats. + */ +export class NgccDirectoryCleaner implements CleaningStrategy { + constructor(private fs: FileSystem) {} + canClean(path: AbsoluteFsPath, basename: PathSegment): boolean { + return basename === NGCC_DIRECTORY && isLocalDirectory(this.fs, path); + } + clean(path: AbsoluteFsPath, _basename: PathSegment): void { this.fs.removeDeep(path); } +} + +/** + * A CleaningStrategy that reverts files that were overwritten and removes the backup files that + * ngcc created. + */ +export class BackupFileCleaner implements CleaningStrategy { + constructor(private fs: FileSystem) {} + canClean(path: AbsoluteFsPath, basename: PathSegment): boolean { + return this.fs.extname(basename) === NGCC_BACKUP_EXTENSION && + this.fs.exists(absoluteFrom(path.replace(NGCC_BACKUP_EXTENSION, ''))); + } + clean(path: AbsoluteFsPath, _basename: PathSegment): void { + this.fs.moveFile(path, absoluteFrom(path.replace(NGCC_BACKUP_EXTENSION, ''))); + } +} diff --git a/packages/compiler-cli/ngcc/src/writing/cleaning/package_cleaner.ts b/packages/compiler-cli/ngcc/src/writing/cleaning/package_cleaner.ts new file mode 100644 index 0000000000..68917d41ff --- /dev/null +++ b/packages/compiler-cli/ngcc/src/writing/cleaning/package_cleaner.ts @@ -0,0 +1,80 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {AbsoluteFsPath, FileSystem} from '../../../../src/ngtsc/file_system'; +import {needsCleaning} from '../../packages/build_marker'; +import {EntryPoint} from '../../packages/entry_point'; + +import {BackupFileCleaner, CleaningStrategy, NgccDirectoryCleaner, PackageJsonCleaner} from './cleaning_strategies'; +import {isLocalDirectory} from './utils'; + +/** + * A class that can clean ngcc artifacts from a directory. + */ +export class PackageCleaner { + constructor(private fs: FileSystem, private cleaners: CleaningStrategy[]) {} + + /** + * Recurse through the file-system cleaning files and directories as determined by the configured + * cleaning-strategies. + * + * @param directory the current directory to clean + */ + clean(directory: AbsoluteFsPath) { + const basenames = this.fs.readdir(directory); + for (const basename of basenames) { + if (basename === 'node_modules') { + continue; + } + + const path = this.fs.resolve(directory, basename); + for (const cleaner of this.cleaners) { + if (cleaner.canClean(path, basename)) { + cleaner.clean(path, basename); + break; + } + } + // Recurse into subdirectories (note that a cleaner may have removed this path) + if (isLocalDirectory(this.fs, path)) { + this.clean(path); + } + } + } +} + + +/** + * Iterate through the given `entryPoints` identifying the package for each that has at least one + * outdated processed format, then cleaning those packages. + * + * Note that we have to clean entire packages because there is no clear file-system boundary + * between entry-points within a package. So if one entry-point is outdated we have to clean + * everything within that package. + * + * @param fileSystem the current file-system + * @param entryPoints the entry-points that have been collected for this run of ngcc + * @returns true if packages needed to be cleaned. + */ +export function cleanOutdatedPackages(fileSystem: FileSystem, entryPoints: EntryPoint[]): boolean { + const packagesToClean = new Set(); + for (const entryPoint of entryPoints) { + if (needsCleaning(entryPoint.packageJson)) { + packagesToClean.add(entryPoint.package); + } + } + + const cleaner = new PackageCleaner(fileSystem, [ + new PackageJsonCleaner(fileSystem), + new NgccDirectoryCleaner(fileSystem), + new BackupFileCleaner(fileSystem), + ]); + for (const packagePath of packagesToClean) { + cleaner.clean(packagePath); + } + + return packagesToClean.size > 0; +} diff --git a/packages/compiler-cli/ngcc/src/writing/cleaning/utils.ts b/packages/compiler-cli/ngcc/src/writing/cleaning/utils.ts new file mode 100644 index 0000000000..2afece08c6 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/writing/cleaning/utils.ts @@ -0,0 +1,23 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {AbsoluteFsPath, FileSystem} from '../../../../src/ngtsc/file_system'; + +/** + * Returns true if the given `path` is a directory (not a symlink) and actually exists. + * + * @param fs the current filesystem + * @param path the path to check + */ +export function isLocalDirectory(fs: FileSystem, path: AbsoluteFsPath): boolean { + if (fs.exists(path)) { + const stat = fs.lstat(path); + return stat.isDirectory(); + } else { + return false; + } +} diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts index f3fefc1e14..c2ca59d889 100644 --- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts +++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts @@ -150,18 +150,18 @@ runInEachFileSystem(() => { it(`should be able to process spread operator inside objects for ${target} format`, () => { compileIntoApf( 'test-package', { - '/index.ts': ` + '/index.ts': ` import {Directive, Input, NgModule} from '@angular/core'; - + const a = { '[class.a]': 'true' }; const b = { '[class.b]': 'true' }; - + @Directive({ selector: '[foo]', host: {...a, ...b, '[class.c]': 'false'} }) export class FooDirective {} - + @NgModule({ declarations: [FooDirective], }) @@ -590,7 +590,6 @@ runInEachFileSystem(() => { }); }); - function markPropertiesAsProcessed(packagePath: string, properties: EntryPointJsonProperty[]) { const basePath = _('/node_modules'); const targetPackageJsonPath = join(basePath, packagePath, 'package.json'); @@ -599,6 +598,49 @@ runInEachFileSystem(() => { pkgJsonUpdater, targetPackage, targetPackageJsonPath, ['typings', ...properties]); } + it('should clean up outdated artifacts', () => { + compileIntoFlatEs5Package('test-package', { + 'index.ts': ` + import {Directive} from '@angular/core'; + + @Directive({selector: '[foo]'}) + export class FooDirective { + } + `, + }); + mainNgcc({ + basePath: '/node_modules', + propertiesToConsider: ['main'], + logger: new MockLogger(), + }); + + // Now hack the files to look like it was processed by an outdated version of ngcc + const packageJson = loadPackage('test-package', _('/node_modules')); + packageJson.__processed_by_ivy_ngcc__ !.typings = '8.0.0'; + packageJson.main_ivy_ngcc = '__ivy_ngcc__/main.js'; + fs.writeFile(_('/node_modules/test-package/package.json'), JSON.stringify(packageJson)); + fs.writeFile(_('/node_modules/test-package/x.js'), 'processed content'); + fs.writeFile(_('/node_modules/test-package/x.js.__ivy_ngcc_bak'), 'original content'); + fs.ensureDir(_('/node_modules/test-package/__ivy_ngcc__/foo')); + + // Now run ngcc again to see that it cleans out the outdated artifacts + mainNgcc({ + basePath: '/node_modules', + propertiesToConsider: ['main'], + logger: new MockLogger(), + }); + const newPackageJson = loadPackage('test-package', _('/node_modules')); + expect(newPackageJson.__processed_by_ivy_ngcc__).toEqual({ + main: '0.0.0-PLACEHOLDER', + typings: '0.0.0-PLACEHOLDER', + }); + expect(newPackageJson.main_ivy_ngcc).toBeUndefined(); + expect(fs.exists(_('/node_modules/test-package/x.js'))).toBe(true); + expect(fs.exists(_('/node_modules/test-package/x.js.__ivy_ngcc_bak'))).toBe(false); + expect(fs.readFile(_('/node_modules/test-package/x.js'))).toEqual('original content'); + expect(fs.exists(_('/node_modules/test-package/__ivy_ngcc__'))).toBe(false); + }); + describe('with propertiesToConsider', () => { it('should complain if none of the properties in the `propertiesToConsider` list is supported', diff --git a/packages/compiler-cli/ngcc/test/writing/cleaning/cleaning_strategies_spec.ts b/packages/compiler-cli/ngcc/test/writing/cleaning/cleaning_strategies_spec.ts new file mode 100644 index 0000000000..e8b2b5ec71 --- /dev/null +++ b/packages/compiler-cli/ngcc/test/writing/cleaning/cleaning_strategies_spec.ts @@ -0,0 +1,236 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {AbsoluteFsPath, FileSystem, PathSegment, absoluteFrom, getFileSystem} from '../../../../src/ngtsc/file_system'; +import {runInEachFileSystem} from '../../../../src/ngtsc/file_system/testing'; +import {EntryPointPackageJson} from '../../../src/packages/entry_point'; +import {BackupFileCleaner, NgccDirectoryCleaner, PackageJsonCleaner} from '../../../src/writing/cleaning/cleaning_strategies'; + +runInEachFileSystem(() => { + describe('cleaning strategies', () => { + let fs: FileSystem; + let _abs: typeof absoluteFrom; + + beforeEach(() => { + fs = getFileSystem(); + _abs = absoluteFrom; + }); + + describe('PackageJsonCleaner', () => { + + let packageJsonPath: AbsoluteFsPath; + beforeEach(() => { packageJsonPath = _abs('/node_modules/pkg/package.json'); }); + + describe('canClean()', () => { + it('should return true if the basename is package.json', () => { + const strategy = new PackageJsonCleaner(fs); + expect(strategy.canClean(packageJsonPath, fs.basename(packageJsonPath))).toBe(true); + }); + + it('should return false if the basename is not package.json', () => { + const filePath = _abs('/node_modules/pkg/index.js'); + const fileName = fs.basename(filePath); + const strategy = new PackageJsonCleaner(fs); + expect(strategy.canClean(filePath, fileName)).toBe(false); + }); + }); + + describe('clean()', () => { + it('should not touch the file if there is no build marker', () => { + const strategy = new PackageJsonCleaner(fs); + const packageJson: EntryPointPackageJson = {name: 'test-package'}; + fs.ensureDir(fs.dirname(packageJsonPath)); + fs.writeFile(packageJsonPath, JSON.stringify(packageJson)); + strategy.clean(packageJsonPath, fs.basename(packageJsonPath)); + const newPackageJson: EntryPointPackageJson = JSON.parse(fs.readFile(packageJsonPath)); + expect(newPackageJson).toEqual({name: 'test-package'}); + }); + + it('should remove the processed marker', () => { + const strategy = new PackageJsonCleaner(fs); + const packageJson: EntryPointPackageJson = { + name: 'test-package', + __processed_by_ivy_ngcc__: {'fesm2015': '8.0.0'} + }; + fs.ensureDir(fs.dirname(packageJsonPath)); + fs.writeFile(packageJsonPath, JSON.stringify(packageJson)); + strategy.clean(packageJsonPath, fs.basename(packageJsonPath)); + const newPackageJson: EntryPointPackageJson = JSON.parse(fs.readFile(packageJsonPath)); + expect(newPackageJson).toEqual({name: 'test-package'}); + }); + + it('should remove the new entry points', () => { + const strategy = new PackageJsonCleaner(fs); + const packageJson: EntryPointPackageJson = { + name: 'test-package', + __processed_by_ivy_ngcc__: {'fesm2015': '8.0.0'} + }; + fs.ensureDir(fs.dirname(packageJsonPath)); + fs.writeFile(packageJsonPath, JSON.stringify(packageJson)); + strategy.clean(packageJsonPath, fs.basename(packageJsonPath)); + const newPackageJson: EntryPointPackageJson = JSON.parse(fs.readFile(packageJsonPath)); + expect(newPackageJson).toEqual({name: 'test-package'}); + }); + + it('should remove the prepublish script if there was a processed marker', () => { + const strategy = new PackageJsonCleaner(fs); + const packageJson: EntryPointPackageJson = { + name: 'test-package', + __processed_by_ivy_ngcc__: {'fesm2015': '8.0.0'}, + scripts: {prepublishOnly: 'added by ngcc', test: 'do testing'}, + }; + fs.ensureDir(fs.dirname(packageJsonPath)); + fs.writeFile(packageJsonPath, JSON.stringify(packageJson)); + strategy.clean(packageJsonPath, fs.basename(packageJsonPath)); + const newPackageJson: EntryPointPackageJson = JSON.parse(fs.readFile(packageJsonPath)); + expect(newPackageJson).toEqual({ + name: 'test-package', + scripts: {test: 'do testing'}, + }); + }); + + it('should revert and remove the backup for the prepublish script if there was a processed marker', + () => { + const strategy = new PackageJsonCleaner(fs); + const packageJson: EntryPointPackageJson = { + name: 'test-package', + __processed_by_ivy_ngcc__: {'fesm2015': '8.0.0'}, + scripts: { + prepublishOnly: 'added by ngcc', + prepublishOnly__ivy_ngcc_bak: 'original', + test: 'do testing' + }, + }; + fs.ensureDir(fs.dirname(packageJsonPath)); + fs.writeFile(packageJsonPath, JSON.stringify(packageJson)); + strategy.clean(packageJsonPath, fs.basename(packageJsonPath)); + const newPackageJson: EntryPointPackageJson = JSON.parse(fs.readFile(packageJsonPath)); + expect(newPackageJson).toEqual({ + name: 'test-package', + scripts: {prepublishOnly: 'original', test: 'do testing'}, + }); + }); + + it('should not touch the scripts if there was not processed marker', () => { + const strategy = new PackageJsonCleaner(fs); + const packageJson: EntryPointPackageJson = { + name: 'test-package', + scripts: { + prepublishOnly: 'added by ngcc', + prepublishOnly__ivy_ngcc_bak: 'original', + test: 'do testing' + }, + }; + fs.ensureDir(fs.dirname(packageJsonPath)); + fs.writeFile(packageJsonPath, JSON.stringify(packageJson)); + strategy.clean(packageJsonPath, fs.basename(packageJsonPath)); + const newPackageJson: EntryPointPackageJson = JSON.parse(fs.readFile(packageJsonPath)); + expect(newPackageJson).toEqual({ + name: 'test-package', + scripts: { + prepublishOnly: 'added by ngcc', + prepublishOnly__ivy_ngcc_bak: 'original', + test: 'do testing' + } + }); + }); + }); + }); + + describe('BackupFileCleaner', () => { + let filePath: AbsoluteFsPath; + let backupFilePath: AbsoluteFsPath; + beforeEach(() => { + filePath = _abs('/node_modules/pkg/index.js'); + backupFilePath = _abs('/node_modules/pkg/index.js.__ivy_ngcc_bak'); + }); + + describe('canClean()', () => { + + it('should return true if the file name ends in .__ivy_ngcc_bak and the processed file exists', + () => { + const strategy = new BackupFileCleaner(fs); + fs.ensureDir(fs.dirname(filePath)); + fs.writeFile(filePath, 'processed file'); + fs.writeFile(backupFilePath, 'original file'); + expect(strategy.canClean(backupFilePath, fs.basename(backupFilePath))).toBe(true); + }); + + it('should return false if the file does not end in .__ivy_ngcc_bak', () => { + const strategy = new BackupFileCleaner(fs); + fs.ensureDir(fs.dirname(filePath)); + fs.writeFile(filePath, 'processed file'); + fs.writeFile(backupFilePath, 'original file'); + expect(strategy.canClean(filePath, fs.basename(filePath))).toBe(false); + }); + + it('should return false if the file ends in .__ivy_ngcc_bak but the processed file does not exist', + () => { + const strategy = new BackupFileCleaner(fs); + fs.ensureDir(fs.dirname(filePath)); + fs.writeFile(backupFilePath, 'original file'); + expect(strategy.canClean(backupFilePath, fs.basename(backupFilePath))).toBe(false); + }); + }); + + describe('clean()', () => { + it('should move the backup file back to its original file path', () => { + const strategy = new BackupFileCleaner(fs); + fs.ensureDir(fs.dirname(filePath)); + fs.writeFile(filePath, 'processed file'); + fs.writeFile(backupFilePath, 'original file'); + strategy.clean(backupFilePath, fs.basename(backupFilePath)); + expect(fs.exists(backupFilePath)).toBe(false); + expect(fs.readFile(filePath)).toEqual('original file'); + }); + }); + }); + + describe('NgccDirectoryCleaner', () => { + let ivyDirectory: AbsoluteFsPath; + beforeEach(() => { ivyDirectory = _abs('/node_modules/pkg/__ivy_ngcc__'); }); + + describe('canClean()', () => { + it('should return true if the path is a directory and is called __ivy_ngcc__', () => { + const strategy = new NgccDirectoryCleaner(fs); + fs.ensureDir(ivyDirectory); + expect(strategy.canClean(ivyDirectory, fs.basename(ivyDirectory))).toBe(true); + }); + + it('should return false if the path is a directory and not called __ivy_ngcc__', () => { + const strategy = new NgccDirectoryCleaner(fs); + const filePath = _abs('/node_modules/pkg/other'); + fs.ensureDir(ivyDirectory); + expect(strategy.canClean(filePath, fs.basename(filePath))).toBe(false); + }); + + it('should return false if the path is called __ivy_ngcc__ but does not exist', () => { + const strategy = new NgccDirectoryCleaner(fs); + expect(strategy.canClean(ivyDirectory, fs.basename(ivyDirectory))).toBe(false); + }); + + it('should return false if the path is called __ivy_ngcc__ but is not a directory', () => { + const strategy = new NgccDirectoryCleaner(fs); + fs.ensureDir(fs.dirname(ivyDirectory)); + fs.writeFile(ivyDirectory, 'some contents'); + expect(strategy.canClean(ivyDirectory, fs.basename(ivyDirectory))).toBe(false); + }); + }); + + describe('clean()', () => { + it('should remove the __ivy_ngcc__ directory', () => { + const strategy = new NgccDirectoryCleaner(fs); + fs.ensureDir(ivyDirectory); + fs.ensureDir(fs.resolve(ivyDirectory, 'subfolder')); + fs.writeFile(fs.resolve(ivyDirectory, 'subfolder', 'file.txt'), 'file contents'); + strategy.clean(ivyDirectory, fs.basename(ivyDirectory)); + expect(fs.exists(ivyDirectory)).toBe(false); + }); + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/compiler-cli/ngcc/test/writing/cleaning/package_cleaner_spec.ts b/packages/compiler-cli/ngcc/test/writing/cleaning/package_cleaner_spec.ts new file mode 100644 index 0000000000..762d024461 --- /dev/null +++ b/packages/compiler-cli/ngcc/test/writing/cleaning/package_cleaner_spec.ts @@ -0,0 +1,86 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {AbsoluteFsPath, FileSystem, PathSegment, absoluteFrom, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; + +import {runInEachFileSystem} from '../../../../src/ngtsc/file_system/testing'; +import {CleaningStrategy} from '../../../src/writing/cleaning/cleaning_strategies'; +import {PackageCleaner} from '../../../src/writing/cleaning/package_cleaner'; + +runInEachFileSystem(() => { + describe('PackageCleaner', () => { + let fs: FileSystem; + let _: typeof absoluteFrom; + beforeEach(() => { + fs = getFileSystem(); + _ = absoluteFrom; + }); + + describe('clean()', () => { + it('should call `canClean()` on each cleaner for each directory and file below the given one', + () => { + const log: string[] = []; + fs.ensureDir(_('/a/b/c')); + fs.writeFile(_('/a/b/d.txt'), 'd contents'); + fs.writeFile(_('/a/b/c/e.txt'), 'e contents'); + const a = new MockCleaningStrategy(log, 'a', false); + const b = new MockCleaningStrategy(log, 'b', false); + const c = new MockCleaningStrategy(log, 'c', false); + const cleaner = new PackageCleaner(fs, [a, b, c]); + cleaner.clean(_('/a/b')); + expect(log).toEqual([ + `a:canClean('${_('/a/b/c')}', 'c')`, + `b:canClean('${_('/a/b/c')}', 'c')`, + `c:canClean('${_('/a/b/c')}', 'c')`, + `a:canClean('${_('/a/b/c/e.txt')}', 'e.txt')`, + `b:canClean('${_('/a/b/c/e.txt')}', 'e.txt')`, + `c:canClean('${_('/a/b/c/e.txt')}', 'e.txt')`, + `a:canClean('${_('/a/b/d.txt')}', 'd.txt')`, + `b:canClean('${_('/a/b/d.txt')}', 'd.txt')`, + `c:canClean('${_('/a/b/d.txt')}', 'd.txt')`, + ]); + }); + + it('should call `clean()` for the first cleaner that returns true for `canClean()`', () => { + const log: string[] = []; + fs.ensureDir(_('/a/b/c')); + fs.writeFile(_('/a/b/d.txt'), 'd contents'); + fs.writeFile(_('/a/b/c/e.txt'), 'e contents'); + const a = new MockCleaningStrategy(log, 'a', false); + const b = new MockCleaningStrategy(log, 'b', true); + const c = new MockCleaningStrategy(log, 'c', false); + const cleaner = new PackageCleaner(fs, [a, b, c]); + cleaner.clean(_('/a/b')); + expect(log).toEqual([ + `a:canClean('${_('/a/b/c')}', 'c')`, + `b:canClean('${_('/a/b/c')}', 'c')`, + `b:clean('${_('/a/b/c')}', 'c')`, + `a:canClean('${_('/a/b/c/e.txt')}', 'e.txt')`, + `b:canClean('${_('/a/b/c/e.txt')}', 'e.txt')`, + `b:clean('${_('/a/b/c/e.txt')}', 'e.txt')`, + `a:canClean('${_('/a/b/d.txt')}', 'd.txt')`, + `b:canClean('${_('/a/b/d.txt')}', 'd.txt')`, + `b:clean('${_('/a/b/d.txt')}', 'd.txt')`, + ]); + }); + }); + }); +}); + + +class MockCleaningStrategy implements CleaningStrategy { + constructor(private log: string[], private label: string, private _canClean: boolean) {} + + canClean(path: AbsoluteFsPath, basename: PathSegment) { + this.log.push(`${this.label}:canClean('${path}', '${basename}')`); + return this._canClean; + } + + clean(path: AbsoluteFsPath, basename: PathSegment): void { + this.log.push(`${this.label}:clean('${path}', '${basename}')`); + } +} \ No newline at end of file diff --git a/packages/compiler-cli/package.json b/packages/compiler-cli/package.json index 92544a7f79..c6624b3e5b 100644 --- a/packages/compiler-cli/package.json +++ b/packages/compiler-cli/package.json @@ -17,6 +17,7 @@ "chokidar": "^3.0.0", "convert-source-map": "^1.5.1", "dependency-graph": "^0.7.2", + "fs-extra": "4.0.2", "magic-string": "^0.25.0", "semver": "^6.3.0", "source-map": "^0.6.1",