feat(ngcc): automatically clean outdated ngcc artifacts (#35079)

If ngcc gets updated to a new version then the artifacts
left in packages that were processed by the previous
version are possibly invalid.

Previously we just errored if we found packages that
had already been processed by an outdated version.

Now we automatically clean the packages that have
outdated artifacts so that they can be reprocessed
correctly with the current ngcc version.

Fixes #35082

PR Close #35079
This commit is contained in:
Pete Bacon Darwin 2020-01-31 21:07:59 +00:00 committed by Misko Hevery
parent 2e52fcf1eb
commit 2bfddcf29f
9 changed files with 548 additions and 7 deletions

View File

@ -31,6 +31,7 @@ nodejs_test(
"@nodejs//:node",
"@npm//domino",
"@npm//chokidar",
"@npm//fs-extra",
"@npm//source-map-support",
"@npm//shelljs",
"@npm//typescript",

View File

@ -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;

View File

@ -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, '')));
}
}

View File

@ -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<AbsoluteFsPath>();
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;
}

View File

@ -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;
}
}

View File

@ -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',

View File

@ -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);
});
});
});
});
});

View File

@ -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}')`);
}
}

View File

@ -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",