diff --git a/packages/compiler-cli/ngcc/src/main.ts b/packages/compiler-cli/ngcc/src/main.ts index 90cddf3a6f..4fb88c1e18 100644 --- a/packages/compiler-cli/ngcc/src/main.ts +++ b/packages/compiler-cli/ngcc/src/main.ts @@ -18,6 +18,8 @@ import {EntryPointFormat, EntryPointJsonProperty, SUPPORTED_FORMAT_PROPERTIES, g import {makeEntryPointBundle} from './packages/entry_point_bundle'; import {EntryPointFinder} from './packages/entry_point_finder'; import {Transformer} from './packages/transformer'; +import {FileWriter} from './writing/file_writer'; +import {InPlaceFileWriter} from './writing/in_place_file_writer'; /** @@ -62,6 +64,7 @@ export function mainNgcc({basePath, targetEntryPointPath, const host = new DependencyHost(); const resolver = new DependencyResolver(host); const finder = new EntryPointFinder(resolver); + const fileWriter = getFileWriter(); const absoluteTargetEntryPointPath = targetEntryPointPath ? AbsoluteFsPath.from(resolve(basePath, targetEntryPointPath)) : @@ -116,7 +119,8 @@ export function mainNgcc({basePath, targetEntryPointPath, compiledFormats.size === 0); if (bundle) { console.warn(`Compiling ${entryPoint.name} : ${property} as ${format}`); - transformer.transform(bundle); + const transformedFiles = transformer.transform(bundle); + fileWriter.writeBundle(entryPoint, bundle, transformedFiles); compiledFormats.add(formatPath); } else { console.warn( @@ -139,3 +143,7 @@ export function mainNgcc({basePath, targetEntryPointPath, } }); } + +function getFileWriter(): FileWriter { + return new InPlaceFileWriter(); +} diff --git a/packages/compiler-cli/ngcc/src/packages/transformer.ts b/packages/compiler-cli/ngcc/src/packages/transformer.ts index 2a24ed0759..1cf50fa860 100644 --- a/packages/compiler-cli/ngcc/src/packages/transformer.ts +++ b/packages/compiler-cli/ngcc/src/packages/transformer.ts @@ -5,9 +5,6 @@ * 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 {dirname} from 'canonical-path'; -import {existsSync, writeFileSync} from 'fs'; -import {mkdir, mv} from 'shelljs'; import * as ts from 'typescript'; import {CompiledFile, DecorationAnalyzer} from '../analysis/decoration_analyzer'; @@ -22,7 +19,6 @@ import {Esm5Renderer} from '../rendering/esm5_renderer'; import {EsmRenderer} from '../rendering/esm_renderer'; import {FileInfo, Renderer} from '../rendering/renderer'; -import {EntryPoint} from './entry_point'; import {EntryPointBundle} from './entry_point_bundle'; @@ -54,8 +50,9 @@ export class Transformer { /** * Transform the source (and typings) files of a bundle. * @param bundle the bundle to transform. + * @returns information about the files that were transformed. */ - transform(bundle: EntryPointBundle): void { + transform(bundle: EntryPointBundle): FileInfo[] { const isCore = bundle.isCore; const reflectionHost = this.getHost(isCore, bundle); @@ -69,8 +66,7 @@ export class Transformer { decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses); - // Write out all the transformed files. - renderedFiles.forEach(file => this.writeFile(file)); + return renderedFiles; } getHost(isCore: boolean, bundle: EntryPointBundle): NgccReflectionHost { @@ -122,15 +118,6 @@ export class Transformer { return {decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses}; } - - writeFile(file: FileInfo): void { - mkdir('-p', dirname(file.path)); - const backPath = file.path + '.bak'; - if (existsSync(file.path) && !existsSync(backPath)) { - mv(file.path, backPath); - } - writeFileSync(file.path, file.contents, 'utf8'); - } } diff --git a/packages/compiler-cli/ngcc/src/writing/file_writer.ts b/packages/compiler-cli/ngcc/src/writing/file_writer.ts new file mode 100644 index 0000000000..0d7ee1e8a6 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/writing/file_writer.ts @@ -0,0 +1,19 @@ + +/** + * @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 {EntryPoint} from '../packages/entry_point'; +import {EntryPointBundle} from '../packages/entry_point_bundle'; +import {FileInfo} from '../rendering/renderer'; + + +/** + * Responsible for writing out the transformed files to disk. + */ +export interface FileWriter { + writeBundle(entryPoint: EntryPoint, bundle: EntryPointBundle, transformedFiles: FileInfo[]): void; +} diff --git a/packages/compiler-cli/ngcc/src/writing/in_place_file_writer.ts b/packages/compiler-cli/ngcc/src/writing/in_place_file_writer.ts new file mode 100644 index 0000000000..3a5790c85b --- /dev/null +++ b/packages/compiler-cli/ngcc/src/writing/in_place_file_writer.ts @@ -0,0 +1,40 @@ + +/** + * @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 {dirname} from 'canonical-path'; +import {existsSync, writeFileSync} from 'fs'; +import {mkdir, mv} from 'shelljs'; + +import {EntryPoint} from '../packages/entry_point'; +import {EntryPointBundle} from '../packages/entry_point_bundle'; +import {FileInfo} from '../rendering/renderer'; + +import {FileWriter} from './file_writer'; + +/** + * This FileWriter overwrites the transformed file, in-place, while creating + * a back-up of the original file with an extra `.bak` extension. + */ +export class InPlaceFileWriter implements FileWriter { + writeBundle(_entryPoint: EntryPoint, _bundle: EntryPointBundle, transformedFiles: FileInfo[]) { + transformedFiles.forEach(file => this.writeFileAndBackup(file)); + } + protected writeFileAndBackup(file: FileInfo): void { + mkdir('-p', dirname(file.path)); + const backPath = file.path + '.__ivy_ngcc_bak'; + if (existsSync(backPath)) { + throw new Error( + `Tried to overwrite ${backPath} with an ngcc back up file, which is disallowed.`); + } + if (existsSync(file.path)) { + mv(file.path, backPath); + } + writeFileSync(file.path, file.contents, 'utf8'); + } +} diff --git a/packages/compiler-cli/ngcc/test/BUILD.bazel b/packages/compiler-cli/ngcc/test/BUILD.bazel index 37421184f5..5a97a88dd6 100644 --- a/packages/compiler-cli/ngcc/test/BUILD.bazel +++ b/packages/compiler-cli/ngcc/test/BUILD.bazel @@ -34,6 +34,7 @@ jasmine_node_test( "//tools/testing:node_no_angular", "@npm//canonical-path", "@npm//convert-source-map", + "@npm//shelljs", ], ) diff --git a/packages/compiler-cli/ngcc/test/writing/in_place_file_writer_spec.ts b/packages/compiler-cli/ngcc/test/writing/in_place_file_writer_spec.ts new file mode 100644 index 0000000000..96dcb76d4a --- /dev/null +++ b/packages/compiler-cli/ngcc/test/writing/in_place_file_writer_spec.ts @@ -0,0 +1,87 @@ +/** + * @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 {existsSync, readFileSync} from 'fs'; +import * as mockFs from 'mock-fs'; + +import {EntryPoint} from '../../src/packages/entry_point'; +import {EntryPointBundle} from '../../src/packages/entry_point_bundle'; +import {InPlaceFileWriter} from '../../src/writing/in_place_file_writer'; + +function createMockFileSystem() { + mockFs({ + '/package/path': { + 'top-level.js': 'ORIGINAL TOP LEVEL', + 'folder-1': { + 'file-1.js': 'ORIGINAL FILE 1', + 'file-2.js': 'ORIGINAL FILE 2', + }, + 'folder-2': { + 'file-3.js': 'ORIGINAL FILE 3', + 'file-4.js': 'ORIGINAL FILE 4', + }, + 'already-backed-up.js.__ivy_ngcc_bak': 'BACKED UP', + } + }); +} + +function restoreRealFileSystem() { + mockFs.restore(); +} + +describe('InPlaceFileWriter', () => { + beforeEach(createMockFileSystem); + afterEach(restoreRealFileSystem); + + it('should write all the FileInfo to the disk', () => { + const fileWriter = new InPlaceFileWriter(); + fileWriter.writeBundle({} as EntryPoint, {} as EntryPointBundle, [ + {path: '/package/path/top-level.js', contents: 'MODIFIED TOP LEVEL'}, + {path: '/package/path/folder-1/file-1.js', contents: 'MODIFIED FILE 1'}, + {path: '/package/path/folder-2/file-4.js', contents: 'MODIFIED FILE 4'}, + {path: '/package/path/folder-3/file-5.js', contents: 'NEW FILE 5'}, + ]); + expect(readFileSync('/package/path/top-level.js', 'utf8')).toEqual('MODIFIED TOP LEVEL'); + expect(readFileSync('/package/path/folder-1/file-1.js', 'utf8')).toEqual('MODIFIED FILE 1'); + expect(readFileSync('/package/path/folder-1/file-2.js', 'utf8')).toEqual('ORIGINAL FILE 2'); + expect(readFileSync('/package/path/folder-2/file-3.js', 'utf8')).toEqual('ORIGINAL FILE 3'); + expect(readFileSync('/package/path/folder-2/file-4.js', 'utf8')).toEqual('MODIFIED FILE 4'); + expect(readFileSync('/package/path/folder-3/file-5.js', 'utf8')).toEqual('NEW FILE 5'); + }); + + it('should create backups of all files that previously existed', () => { + const fileWriter = new InPlaceFileWriter(); + fileWriter.writeBundle({} as EntryPoint, {} as EntryPointBundle, [ + {path: '/package/path/top-level.js', contents: 'MODIFIED TOP LEVEL'}, + {path: '/package/path/folder-1/file-1.js', contents: 'MODIFIED FILE 1'}, + {path: '/package/path/folder-2/file-4.js', contents: 'MODIFIED FILE 4'}, + {path: '/package/path/folder-3/file-5.js', contents: 'NEW FILE 5'}, + ]); + expect(readFileSync('/package/path/top-level.js.__ivy_ngcc_bak', 'utf8')) + .toEqual('ORIGINAL TOP LEVEL'); + expect(readFileSync('/package/path/folder-1/file-1.js.__ivy_ngcc_bak', 'utf8')) + .toEqual('ORIGINAL FILE 1'); + expect(existsSync('/package/path/folder-1/file-2.js.__ivy_ngcc_bak')).toBe(false); + expect(existsSync('/package/path/folder-2/file-3.js.__ivy_ngcc_bak')).toBe(false); + expect(readFileSync('/package/path/folder-2/file-4.js.__ivy_ngcc_bak', 'utf8')) + .toEqual('ORIGINAL FILE 4'); + expect(existsSync('/package/path/folder-3/file-5.js.__ivy_ngcc_bak')).toBe(false); + }); + + it('should error if the backup file already exists', () => { + const fileWriter = new InPlaceFileWriter(); + expect( + () => fileWriter.writeBundle( + {} as EntryPoint, {} as EntryPointBundle, + [ + {path: '/package/path/already-backed-up.js', contents: 'MODIFIED BACKED UP'}, + ])) + .toThrowError( + 'Tried to overwrite /package/path/already-backed-up.js.__ivy_ngcc_bak with an ngcc back up file, which is disallowed.'); + }); +});