From 64e5628897e1e04d866a92dd2b7f352b1e8da792 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Wed, 20 Mar 2019 13:47:59 +0000 Subject: [PATCH] feat(ivy): ngcc - support creating a new copy of the entry-point format (#29092) This commit adds a `NewEntryPointFileWriter` that will be used in webpack integration. Instead of overwriting files in-place, this `FileWriter` will make a copy of the TS program files and write the transformed files there. It also updates the package.json with new properties that can be used to access the new entry-point format. FW-1121 PR Close #29092 --- packages/compiler-cli/ngcc/src/main.ts | 19 +- .../ngcc/src/packages/entry_point_bundle.ts | 8 +- .../writing/new_entry_point_file_writer.ts | 72 +++++ .../compiler-cli/ngcc/test/helpers/utils.ts | 10 +- .../ngcc/test/integration/ngcc_spec.ts | 40 ++- .../test/rendering/esm2015_renderer_spec.ts | 2 +- .../ngcc/test/rendering/esm5_renderer_spec.ts | 2 +- .../ngcc/test/rendering/renderer_spec.ts | 2 +- .../new_entry_point_file_writer_spec.ts | 282 ++++++++++++++++++ 9 files changed, 420 insertions(+), 17 deletions(-) create mode 100644 packages/compiler-cli/ngcc/src/writing/new_entry_point_file_writer.ts create mode 100644 packages/compiler-cli/ngcc/test/writing/new_entry_point_file_writer_spec.ts diff --git a/packages/compiler-cli/ngcc/src/main.ts b/packages/compiler-cli/ngcc/src/main.ts index 4fb88c1e18..b359b9af5b 100644 --- a/packages/compiler-cli/ngcc/src/main.ts +++ b/packages/compiler-cli/ngcc/src/main.ts @@ -20,6 +20,7 @@ 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'; +import {NewEntryPointFileWriter} from './writing/new_entry_point_file_writer'; /** @@ -45,6 +46,10 @@ export interface NgccOptions { * this entry-point at the first matching format. Defaults to `true`. */ compileAllFormats?: boolean; + /** + * Whether to create new entry-points bundles rather than overwriting the original files. + */ + createNewEntryPointFormats?: boolean; } const SUPPORTED_FORMATS: EntryPointFormat[] = ['esm5', 'esm2015']; @@ -57,14 +62,14 @@ const SUPPORTED_FORMATS: EntryPointFormat[] = ['esm5', 'esm2015']; * * @param options The options telling ngcc what to compile and how. */ -export function mainNgcc({basePath, targetEntryPointPath, - propertiesToConsider = SUPPORTED_FORMAT_PROPERTIES, - compileAllFormats = true}: NgccOptions): void { +export function mainNgcc( + {basePath, targetEntryPointPath, propertiesToConsider = SUPPORTED_FORMAT_PROPERTIES, + compileAllFormats = true, createNewEntryPointFormats = false}: NgccOptions): void { const transformer = new Transformer(basePath, basePath); const host = new DependencyHost(); const resolver = new DependencyResolver(host); const finder = new EntryPointFinder(resolver); - const fileWriter = getFileWriter(); + const fileWriter = getFileWriter(createNewEntryPointFormats); const absoluteTargetEntryPointPath = targetEntryPointPath ? AbsoluteFsPath.from(resolve(basePath, targetEntryPointPath)) : @@ -115,7 +120,7 @@ export function mainNgcc({basePath, targetEntryPointPath, // the property as processed even if its underlying format has been built already. if (!compiledFormats.has(formatPath) && (compileAllFormats || compiledFormats.size === 0)) { const bundle = makeEntryPointBundle( - entryPoint.path, formatPath, entryPoint.typings, isCore, format, + entryPoint.path, formatPath, entryPoint.typings, isCore, property, format, compiledFormats.size === 0); if (bundle) { console.warn(`Compiling ${entryPoint.name} : ${property} as ${format}`); @@ -144,6 +149,6 @@ export function mainNgcc({basePath, targetEntryPointPath, }); } -function getFileWriter(): FileWriter { - return new InPlaceFileWriter(); +function getFileWriter(createNewEntryPointFormats: boolean): FileWriter { + return createNewEntryPointFormats ? new NewEntryPointFileWriter() : new InPlaceFileWriter(); } diff --git a/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts b/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts index a911b39909..0130d52677 100644 --- a/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts +++ b/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts @@ -10,7 +10,7 @@ import * as ts from 'typescript'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {BundleProgram, makeBundleProgram} from './bundle_program'; -import {EntryPointFormat} from './entry_point'; +import {EntryPointFormat, EntryPointJsonProperty} from './entry_point'; @@ -19,6 +19,7 @@ import {EntryPointFormat} from './entry_point'; * format of a package entry-point. */ export interface EntryPointBundle { + formatProperty: EntryPointJsonProperty; format: EntryPointFormat; isCore: boolean; isFlatCore: boolean; @@ -38,7 +39,8 @@ export interface EntryPointBundle { */ export function makeEntryPointBundle( entryPointPath: string, formatPath: string, typingsPath: string, isCore: boolean, - format: EntryPointFormat, transformDts: boolean): EntryPointBundle|null { + formatProperty: EntryPointJsonProperty, format: EntryPointFormat, + transformDts: boolean): EntryPointBundle|null { // Create the TS program and necessary helpers. const options: ts.CompilerOptions = { allowJs: true, @@ -57,5 +59,5 @@ export function makeEntryPointBundle( null; const isFlatCore = isCore && src.r3SymbolsFile === null; - return {format, rootDirs, isCore, isFlatCore, src, dts}; + return {format, formatProperty, rootDirs, isCore, isFlatCore, src, dts}; } diff --git a/packages/compiler-cli/ngcc/src/writing/new_entry_point_file_writer.ts b/packages/compiler-cli/ngcc/src/writing/new_entry_point_file_writer.ts new file mode 100644 index 0000000000..b79e4ed174 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/writing/new_entry_point_file_writer.ts @@ -0,0 +1,72 @@ + +/** + * @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, join, relative} from 'canonical-path'; +import {writeFileSync} from 'fs'; +import {cp, mkdir} from 'shelljs'; + +import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {isDtsPath} from '../../../src/ngtsc/util/src/typescript'; +import {EntryPoint, EntryPointJsonProperty} from '../packages/entry_point'; +import {EntryPointBundle} from '../packages/entry_point_bundle'; +import {FileInfo} from '../rendering/renderer'; + +import {InPlaceFileWriter} from './in_place_file_writer'; + +const NGCC_DIRECTORY = '__ivy_ngcc__'; + +/** + * This FileWriter creates a copy of the original entry-point, then writes the transformed + * files onto the files in this copy, and finally updates the package.json with a new + * entry-point format property that points to this new entry-point. + * + * If there are transformed typings files in this bundle, they are updated in-place (see the + * `InPlaceFileWriter`). + */ +export class NewEntryPointFileWriter extends InPlaceFileWriter { + writeBundle(entryPoint: EntryPoint, bundle: EntryPointBundle, transformedFiles: FileInfo[]) { + // The new folder is at the root of the overall package + const relativeEntryPointPath = relative(entryPoint.package, entryPoint.path); + const relativeNewDir = join(NGCC_DIRECTORY, relativeEntryPointPath); + const newDir = AbsoluteFsPath.fromUnchecked(join(entryPoint.package, relativeNewDir)); + this.copyBundle(bundle, entryPoint.path, newDir); + transformedFiles.forEach(file => this.writeFile(file, entryPoint.path, newDir)); + this.updatePackageJson(entryPoint, bundle.formatProperty, newDir); + } + + protected copyBundle( + bundle: EntryPointBundle, entryPointPath: AbsoluteFsPath, newDir: AbsoluteFsPath) { + bundle.src.program.getSourceFiles().forEach(sourceFile => { + const relativePath = relative(entryPointPath, sourceFile.fileName); + const newFilePath = join(newDir, relativePath); + mkdir('-p', dirname(newFilePath)); + cp(sourceFile.fileName, newFilePath); + }); + } + + protected writeFile(file: FileInfo, entryPointPath: AbsoluteFsPath, newDir: AbsoluteFsPath): + void { + if (isDtsPath(file.path)) { + super.writeFileAndBackup(file); + } else { + const relativePath = relative(entryPointPath, file.path); + const newFilePath = join(newDir, relativePath); + mkdir('-p', dirname(newFilePath)); + writeFileSync(newFilePath, file.contents, 'utf8'); + } + } + + protected updatePackageJson( + entryPoint: EntryPoint, formatProperty: EntryPointJsonProperty, newDir: AbsoluteFsPath) { + const bundlePath = entryPoint.packageJson[formatProperty] !; + const newBundlePath = relative(entryPoint.path, join(newDir, bundlePath)); + (entryPoint.packageJson as any)[formatProperty + '_ivy_ngcc'] = newBundlePath; + writeFileSync(join(entryPoint.path, 'package.json'), JSON.stringify(entryPoint.packageJson)); + } +} diff --git a/packages/compiler-cli/ngcc/test/helpers/utils.ts b/packages/compiler-cli/ngcc/test/helpers/utils.ts index 4e06fe32dd..6969c51ea5 100644 --- a/packages/compiler-cli/ngcc/test/helpers/utils.ts +++ b/packages/compiler-cli/ngcc/test/helpers/utils.ts @@ -10,7 +10,7 @@ import * as ts from 'typescript'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {makeProgram} from '../../../src/ngtsc/testing/in_memory_typescript'; import {BundleProgram} from '../../src/packages/bundle_program'; -import {EntryPointFormat} from '../../src/packages/entry_point'; +import {EntryPointFormat, EntryPointJsonProperty} from '../../src/packages/entry_point'; import {EntryPointBundle} from '../../src/packages/entry_point_bundle'; export {getDeclaration} from '../../../src/ngtsc/testing/in_memory_typescript'; @@ -22,13 +22,17 @@ export {getDeclaration} from '../../../src/ngtsc/testing/in_memory_typescript'; * @param dtsFiles The typings files to include the bundle. */ export function makeTestEntryPointBundle( - format: EntryPointFormat, isCore: boolean, + formatProperty: EntryPointJsonProperty, format: EntryPointFormat, isCore: boolean, files: {name: string, contents: string, isRoot?: boolean}[], dtsFiles?: {name: string, contents: string, isRoot?: boolean}[]): EntryPointBundle { const src = makeTestBundleProgram(files); const dts = dtsFiles ? makeTestBundleProgram(dtsFiles) : null; const isFlatCore = isCore && src.r3SymbolsFile === null; - return {format, rootDirs: [AbsoluteFsPath.fromUnchecked('/')], src, dts, isCore, isFlatCore}; + return { + formatProperty, + format, + rootDirs: [AbsoluteFsPath.fromUnchecked('/')], src, dts, isCore, isFlatCore + }; } /** diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts index adc91151fc..3c2593d7d7 100644 --- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts +++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts @@ -141,6 +141,44 @@ describe('ngcc main()', () => { }); }); }); + + describe('with createNewEntryPointFormats', () => { + it('should create new files rather than overwriting the originals', () => { + const ANGULAR_CORE_IMPORT_REGEX = /import \* as ɵngcc\d+ from '@angular\/core';/; + mainNgcc({ + basePath: '/node_modules', + createNewEntryPointFormats: true, + propertiesToConsider: ['esm5'] + }); + + // Updates the package.json + expect(loadPackage('@angular/common').esm5).toEqual('./esm5/common.js'); + expect((loadPackage('@angular/common') as any).esm5_ivy_ngcc) + .toEqual('__ivy_ngcc__/esm5/common.js'); + + // Doesn't touch original files + expect(readFileSync(`/node_modules/@angular/common/esm5/src/common_module.js`, 'utf8')) + .not.toMatch(ANGULAR_CORE_IMPORT_REGEX); + // Or create a backup of the original + expect(existsSync(`/node_modules/@angular/common/esm5/src/common_module.js.__ivy_ngcc_bak`)) + .toBe(false); + + // Creates new files + expect(readFileSync( + `/node_modules/@angular/common/__ivy_ngcc__/esm5/src/common_module.js`, 'utf8')) + .toMatch(ANGULAR_CORE_IMPORT_REGEX); + + // Copies over files (unchanged) that did not need compiling + expect(existsSync(`/node_modules/@angular/common/__ivy_ngcc__/esm5/src/version.js`)); + expect(readFileSync(`/node_modules/@angular/common/__ivy_ngcc__/esm5/src/version.js`, 'utf8')) + .toEqual(readFileSync(`/node_modules/@angular/common/esm5/src/version.js`, 'utf8')); + + // Overwrites .d.ts files (as usual) + expect(readFileSync(`/node_modules/@angular/common/common.d.ts`, 'utf8')) + .toMatch(ANGULAR_CORE_IMPORT_REGEX); + expect(existsSync(`/node_modules/@angular/common/common.d.ts.__ivy_ngcc_bak`)).toBe(true); + }); + }); }); @@ -232,4 +270,4 @@ function mockResolve(request: string): string|null { function loadPackage(packageName: string): EntryPointPackageJson { return JSON.parse(readFileSync(`/node_modules/${packageName}/package.json`, 'utf8')); -} \ No newline at end of file +} diff --git a/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts index 52c35f6af0..9176141ae1 100644 --- a/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts @@ -18,7 +18,7 @@ import {makeTestEntryPointBundle} from '../helpers/utils'; function setup(file: {name: string, contents: string}) { const dir = dirname(file.name); - const bundle = makeTestEntryPointBundle('esm2015', false, [file]) !; + const bundle = makeTestEntryPointBundle('es2015', 'esm2015', false, [file]) !; const typeChecker = bundle.src.program.getTypeChecker(); const host = new Esm2015ReflectionHost(false, typeChecker); const referencesRegistry = new NgccReferencesRegistry(host); diff --git a/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts index b87ef4e3f9..efb1641690 100644 --- a/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts @@ -18,7 +18,7 @@ import {makeTestEntryPointBundle, getDeclaration} from '../helpers/utils'; function setup(file: {name: string, contents: string}) { const dir = dirname(file.name); - const bundle = makeTestEntryPointBundle('esm5', false, [file]); + const bundle = makeTestEntryPointBundle('module', 'esm5', false, [file]); const typeChecker = bundle.src.program.getTypeChecker(); const host = new Esm5ReflectionHost(false, typeChecker); const referencesRegistry = new NgccReferencesRegistry(host); diff --git a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts index dd68f21e6f..775068bd4c 100644 --- a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts @@ -50,7 +50,7 @@ function createTestRenderer( packageName: string, files: {name: string, contents: string}[], dtsFiles?: {name: string, contents: string}[]) { const isCore = packageName === '@angular/core'; - const bundle = makeTestEntryPointBundle('esm2015', isCore, files, dtsFiles); + const bundle = makeTestEntryPointBundle('es2015', 'esm2015', isCore, files, dtsFiles); const typeChecker = bundle.src.program.getTypeChecker(); const host = new Esm2015ReflectionHost(isCore, typeChecker, bundle.dts); const referencesRegistry = new NgccReferencesRegistry(host); diff --git a/packages/compiler-cli/ngcc/test/writing/new_entry_point_file_writer_spec.ts b/packages/compiler-cli/ngcc/test/writing/new_entry_point_file_writer_spec.ts new file mode 100644 index 0000000000..956fba49f0 --- /dev/null +++ b/packages/compiler-cli/ngcc/test/writing/new_entry_point_file_writer_spec.ts @@ -0,0 +1,282 @@ +/** + * @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 {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {EntryPoint, EntryPointFormat, EntryPointJsonProperty, getEntryPointInfo} from '../../src/packages/entry_point'; +import {EntryPointBundle, makeEntryPointBundle} from '../../src/packages/entry_point_bundle'; +import {FileWriter} from '../../src/writing/file_writer'; +import {NewEntryPointFileWriter} from '../../src/writing/new_entry_point_file_writer'; +import {loadPackageJson} from '../packages/entry_point_spec'; + +const _ = AbsoluteFsPath.from; + +function createMockFileSystem() { + mockFs({ + '/node_modules/test': { + 'package.json': + '{"module": "./esm5.js", "es2015": "./es2015/index.js", "typings": "./index.d.ts"}', + 'index.d.ts': 'export declare class FooTop {}', + 'index.metadata.json': '...', + 'esm5.js': 'export function FooTop() {}', + 'es2015': { + 'index.js': 'import {FooTop} from "./foo";', + 'foo.js': 'export class FooTop {}', + }, + 'a': { + 'package.json': + '{"module": "./esm5.js", "es2015": "./es2015/index.js", "typings": "./index.d.ts"}', + 'index.d.ts': 'export declare class FooA {}', + 'index.metadata.json': '...', + 'esm5.js': 'export function FooA() {}', + 'es2015': { + 'index.js': 'import {FooA} from "./foo";', + 'foo.js': 'export class FooA {}', + }, + }, + 'b': { + // This entry-point points to files outside its folder + 'package.json': + '{"module": "../lib/esm5.js", "es2015": "../lib/es2015/index.js", "typings": "../typings/index.d.ts"}', + }, + 'lib': { + 'esm5.js': 'export function FooB() {}', + 'es2015': { + 'index.js': 'import {FooB} from "./foo";', + 'foo.js': 'export class FooB {}', + }, + }, + 'typings': { + 'index.d.ts': 'export declare class FooB {}', + 'index.metadata.json': '...', + } + }, + }); +} + +function restoreRealFileSystem() { + mockFs.restore(); +} + +describe('NewEntryPointFileWriter', () => { + beforeEach(createMockFileSystem); + afterEach(restoreRealFileSystem); + + let fileWriter: FileWriter; + let entryPoint: EntryPoint; + let esm5bundle: EntryPointBundle; + let esm2015bundle: EntryPointBundle; + + describe('writeBundle() [primary entry-point]', () => { + beforeEach(() => { + fileWriter = new NewEntryPointFileWriter(); + entryPoint = getEntryPointInfo(_('/node_modules/test'), _('/node_modules/test')) !; + esm5bundle = makeTestBundle(entryPoint, 'module', 'esm5'); + esm2015bundle = makeTestBundle(entryPoint, 'es2015', 'esm2015'); + }); + + it('should write the modified file to a new folder', () => { + fileWriter.writeBundle(entryPoint, esm5bundle, [ + {path: '/node_modules/test/esm5.js', contents: 'export function FooTop() {} // MODIFIED'}, + ]); + expect(readFileSync('/node_modules/test/__ivy_ngcc__/esm5.js', 'utf8')) + .toEqual('export function FooTop() {} // MODIFIED'); + expect(readFileSync('/node_modules/test/esm5.js', 'utf8')) + .toEqual('export function FooTop() {}'); + }); + + it('should also copy unmodified files in the program', () => { + fileWriter.writeBundle(entryPoint, esm2015bundle, [ + {path: '/node_modules/test/es2015/foo.js', contents: 'export class FooTop {} // MODIFIED'}, + ]); + expect(readFileSync('/node_modules/test/__ivy_ngcc__/es2015/foo.js', 'utf8')) + .toEqual('export class FooTop {} // MODIFIED'); + expect(readFileSync('/node_modules/test/es2015/foo.js', 'utf8')) + .toEqual('export class FooTop {}'); + expect(readFileSync('/node_modules/test/__ivy_ngcc__/es2015/index.js', 'utf8')) + .toEqual('import {FooTop} from "./foo";'); + expect(readFileSync('/node_modules/test/es2015/index.js', 'utf8')) + .toEqual('import {FooTop} from "./foo";'); + }); + + it('should update the package.json properties', () => { + fileWriter.writeBundle(entryPoint, esm5bundle, [ + {path: '/node_modules/test/esm5.js', contents: 'export function FooTop() {} // MODIFIED'}, + ]); + expect(loadPackageJson('/node_modules/test')).toEqual(jasmine.objectContaining({ + module_ivy_ngcc: '__ivy_ngcc__/esm5.js', + })); + + fileWriter.writeBundle(entryPoint, esm2015bundle, [ + {path: '/node_modules/test/es2015/foo.js', contents: 'export class FooTop {} // MODIFIED'}, + ]); + expect(loadPackageJson('/node_modules/test')).toEqual(jasmine.objectContaining({ + module_ivy_ngcc: '__ivy_ngcc__/esm5.js', + es2015_ivy_ngcc: '__ivy_ngcc__/es2015/index.js', + })); + }); + + it('should overwrite and backup typings files', () => { + fileWriter.writeBundle(entryPoint, esm2015bundle, [ + { + path: '/node_modules/test/index.d.ts', + contents: 'export declare class FooTop {} // MODIFIED' + }, + ]); + expect(readFileSync('/node_modules/test/index.d.ts', 'utf8')) + .toEqual('export declare class FooTop {} // MODIFIED'); + expect(readFileSync('/node_modules/test/index.d.ts.__ivy_ngcc_bak', 'utf8')) + .toEqual('export declare class FooTop {}'); + expect(existsSync('/node_modules/test/__ivy_ngcc__/index.d.ts')).toBe(false); + }); + }); + + describe('writeBundle() [secondary entry-point]', () => { + beforeEach(() => { + fileWriter = new NewEntryPointFileWriter(); + entryPoint = getEntryPointInfo(_('/node_modules/test'), _('/node_modules/test/a')) !; + esm5bundle = makeTestBundle(entryPoint, 'module', 'esm5'); + esm2015bundle = makeTestBundle(entryPoint, 'es2015', 'esm2015'); + }); + + it('should write the modified file to a new folder', () => { + fileWriter.writeBundle(entryPoint, esm5bundle, [ + {path: '/node_modules/test/a/esm5.js', contents: 'export function FooA() {} // MODIFIED'}, + ]); + expect(readFileSync('/node_modules/test/__ivy_ngcc__/a/esm5.js', 'utf8')) + .toEqual('export function FooA() {} // MODIFIED'); + expect(readFileSync('/node_modules/test/a/esm5.js', 'utf8')) + .toEqual('export function FooA() {}'); + }); + + it('should also copy unmodified files in the program', () => { + fileWriter.writeBundle(entryPoint, esm2015bundle, [ + {path: '/node_modules/test/a/es2015/foo.js', contents: 'export class FooA {} // MODIFIED'}, + ]); + expect(readFileSync('/node_modules/test/__ivy_ngcc__/a/es2015/foo.js', 'utf8')) + .toEqual('export class FooA {} // MODIFIED'); + expect(readFileSync('/node_modules/test/a/es2015/foo.js', 'utf8')) + .toEqual('export class FooA {}'); + expect(readFileSync('/node_modules/test/__ivy_ngcc__/a/es2015/index.js', 'utf8')) + .toEqual('import {FooA} from "./foo";'); + expect(readFileSync('/node_modules/test/a/es2015/index.js', 'utf8')) + .toEqual('import {FooA} from "./foo";'); + }); + + it('should update the package.json properties', () => { + fileWriter.writeBundle(entryPoint, esm5bundle, [ + {path: '/node_modules/test/a/esm5.js', contents: 'export function FooA() {} // MODIFIED'}, + ]); + expect(loadPackageJson('/node_modules/test/a')).toEqual(jasmine.objectContaining({ + module_ivy_ngcc: '../__ivy_ngcc__/a/esm5.js', + })); + + fileWriter.writeBundle(entryPoint, esm2015bundle, [ + {path: '/node_modules/test/a/es2015/foo.js', contents: 'export class FooA {} // MODIFIED'}, + ]); + expect(loadPackageJson('/node_modules/test/a')).toEqual(jasmine.objectContaining({ + module_ivy_ngcc: '../__ivy_ngcc__/a/esm5.js', + es2015_ivy_ngcc: '../__ivy_ngcc__/a/es2015/index.js', + })); + }); + + it('should overwrite and backup typings files', () => { + fileWriter.writeBundle(entryPoint, esm2015bundle, [ + { + path: '/node_modules/test/a/index.d.ts', + contents: 'export declare class FooA {} // MODIFIED' + }, + ]); + expect(readFileSync('/node_modules/test/a/index.d.ts', 'utf8')) + .toEqual('export declare class FooA {} // MODIFIED'); + expect(readFileSync('/node_modules/test/a/index.d.ts.__ivy_ngcc_bak', 'utf8')) + .toEqual('export declare class FooA {}'); + expect(existsSync('/node_modules/test/__ivy_ngcc__/a/index.d.ts')).toBe(false); + }); + }); + + describe('writeBundle() [entry-point (with files placed outside entry-point folder)]', () => { + beforeEach(() => { + fileWriter = new NewEntryPointFileWriter(); + entryPoint = getEntryPointInfo(_('/node_modules/test'), _('/node_modules/test/b')) !; + esm5bundle = makeTestBundle(entryPoint, 'module', 'esm5'); + esm2015bundle = makeTestBundle(entryPoint, 'es2015', 'esm2015'); + }); + + it('should write the modified file to a new folder', () => { + fileWriter.writeBundle(entryPoint, esm5bundle, [ + {path: '/node_modules/test/lib/esm5.js', contents: 'export function FooB() {} // MODIFIED'}, + ]); + expect(readFileSync('/node_modules/test/__ivy_ngcc__/lib/esm5.js', 'utf8')) + .toEqual('export function FooB() {} // MODIFIED'); + expect(readFileSync('/node_modules/test/lib/esm5.js', 'utf8')) + .toEqual('export function FooB() {}'); + }); + + it('should also copy unmodified files in the program', () => { + fileWriter.writeBundle(entryPoint, esm2015bundle, [ + { + path: '/node_modules/test/lib/es2015/foo.js', + contents: 'export class FooB {} // MODIFIED' + }, + ]); + expect(readFileSync('/node_modules/test/__ivy_ngcc__/lib/es2015/foo.js', 'utf8')) + .toEqual('export class FooB {} // MODIFIED'); + expect(readFileSync('/node_modules/test/lib/es2015/foo.js', 'utf8')) + .toEqual('export class FooB {}'); + expect(readFileSync('/node_modules/test/__ivy_ngcc__/lib/es2015/index.js', 'utf8')) + .toEqual('import {FooB} from "./foo";'); + expect(readFileSync('/node_modules/test/lib/es2015/index.js', 'utf8')) + .toEqual('import {FooB} from "./foo";'); + }); + + it('should update the package.json properties', () => { + fileWriter.writeBundle(entryPoint, esm5bundle, [ + {path: '/node_modules/test/lib/esm5.js', contents: 'export function FooB() {} // MODIFIED'}, + ]); + expect(loadPackageJson('/node_modules/test/b')).toEqual(jasmine.objectContaining({ + module_ivy_ngcc: '../__ivy_ngcc__/lib/esm5.js', + })); + + fileWriter.writeBundle(entryPoint, esm2015bundle, [ + { + path: '/node_modules/test/lib/es2015/foo.js', + contents: 'export class FooB {} // MODIFIED' + }, + ]); + expect(loadPackageJson('/node_modules/test/b')).toEqual(jasmine.objectContaining({ + module_ivy_ngcc: '../__ivy_ngcc__/lib/esm5.js', + es2015_ivy_ngcc: '../__ivy_ngcc__/lib/es2015/index.js', + })); + }); + + it('should overwrite and backup typings files', () => { + fileWriter.writeBundle(entryPoint, esm2015bundle, [ + { + path: '/node_modules/test/typings/index.d.ts', + contents: 'export declare class FooB {} // MODIFIED' + }, + ]); + expect(readFileSync('/node_modules/test/typings/index.d.ts', 'utf8')) + .toEqual('export declare class FooB {} // MODIFIED'); + expect(readFileSync('/node_modules/test/typings/index.d.ts.__ivy_ngcc_bak', 'utf8')) + .toEqual('export declare class FooB {}'); + expect(existsSync('/node_modules/test/__ivy_ngcc__/typings/index.d.ts')).toBe(false); + }); + }); +}); + +function makeTestBundle( + entryPoint: EntryPoint, formatProperty: EntryPointJsonProperty, + format: EntryPointFormat): EntryPointBundle { + return makeEntryPointBundle( + entryPoint.path, entryPoint.packageJson[formatProperty] !, entryPoint.typings, false, + formatProperty, format, true) !; +}