From c98a4d6ddde28986e6e2241dbba8886957c0b126 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Wed, 6 May 2020 16:54:44 +0200 Subject: [PATCH] feat(ngcc): support for new APF where `module` points to esm2015 output (#36944) As of version 10, libraries following the APF will no longer contain ESM5 output. Hence, tests in ngcc need to be updated as they currently rely on the release output of `@angular/core`. Additionally, we'd need to support in ngcc that the `module` property of entry-points no longer necessarily refers to `esm5` output, but instead can also target `esm2015`. We currently achieve this by checking the path the `module` property points to. We can do this because as per APF, the folder name is known for the esm2015 output. Long-term for more coverage, we want to sniff the format by looking for known ES2015 constructs in the file `module` refers to. PR Close #36944 --- integration/ngcc/test.sh | 22 +- package.json | 1 + .../ngcc/src/command_line_options.ts | 4 +- .../ngcc/src/packages/entry_point.ts | 9 + packages/compiler-cli/ngcc/test/BUILD.bazel | 23 ++ .../ngcc/test/integration/ngcc_spec.ts | 225 +++++++++++------- .../ngcc/test/integration/util.ts | 25 +- .../ngcc/test/packages/entry_point_spec.ts | 6 + yarn.lock | 28 ++- 9 files changed, 231 insertions(+), 112 deletions(-) diff --git a/integration/ngcc/test.sh b/integration/ngcc/test.sh index 0d6936323f..4d77b10ec1 100755 --- a/integration/ngcc/test.sh +++ b/integration/ngcc/test.sh @@ -64,42 +64,32 @@ assertSucceeded "Expected 'ngcc' to log 'Compiling'." cat node_modules/@angular/common/package.json | awk 'ORS=" "' | grep '"__processed_by_ivy_ngcc__":[^}]*"fesm2015": "' assertSucceeded "Expected 'ngcc' to add build marker for 'fesm2015' in '@angular/common'." + # `es2015` is an alias of `fesm2015`. cat node_modules/@angular/common/package.json | awk 'ORS=" "' | grep '"__processed_by_ivy_ngcc__":[^}]*"es2015": "' assertSucceeded "Expected 'ngcc' to add build marker for 'es2015' in '@angular/common'." - # - esm5 - cat node_modules/@angular/common/package.json | awk 'ORS=" "' | grep '"__processed_by_ivy_ngcc__":[^}]*"esm5": "' - assertSucceeded "Expected 'ngcc' to add build marker for 'esm5' in '@angular/common'." - - # - fesm5 + # `module` is an alias of `fesm2015` cat node_modules/@angular/common/package.json | awk 'ORS=" "' | grep '"__processed_by_ivy_ngcc__":[^}]*"module": "' assertSucceeded "Expected 'ngcc' to add build marker for 'module' in '@angular/common'." - cat node_modules/@angular/common/package.json | awk 'ORS=" "' | grep '"__processed_by_ivy_ngcc__":[^}]*"fesm5": "' - assertSucceeded "Expected 'ngcc' to add build marker for 'fesm5' in '@angular/common'." - - # Did it replace the PRE_R3 markers correctly? grep "= SWITCH_COMPILE_COMPONENT__POST_R3__" node_modules/@angular/core/fesm2015/core.js assertSucceeded "Expected 'ngcc' to replace 'SWITCH_COMPILE_COMPONENT__PRE_R3__' in '@angular/core' (fesm2015)." - grep "= SWITCH_COMPILE_COMPONENT__POST_R3__" node_modules/@angular/core/fesm5/core.js - assertSucceeded "Expected 'ngcc' to replace 'SWITCH_COMPILE_COMPONENT__PRE_R3__' in '@angular/core' (fesm5)." + grep "= SWITCH_COMPILE_COMPONENT__POST_R3__" node_modules/@angular/core/bundles/core.umd.js + assertSucceeded "Expected 'ngcc' to replace 'SWITCH_COMPILE_COMPONENT__PRE_R3__' in '@angular/core' (main)." # Did it compile @angular/core/ApplicationModule correctly? grep "ApplicationModule.ɵmod = ɵɵdefineNgModule" node_modules/@angular/core/fesm2015/core.js assertSucceeded "Expected 'ngcc' to correctly compile 'ApplicationModule' in '@angular/core' (fesm2015)." - grep "ApplicationModule.ɵmod = ɵɵdefineNgModule" node_modules/@angular/core/fesm5/core.js - assertSucceeded "Expected 'ngcc' to correctly compile 'ApplicationModule' in '@angular/core' (fesm5)." + grep "ApplicationModule.ɵmod = ɵɵdefineNgModule" node_modules/@angular/core/bundles/core.umd.js + assertSucceeded "Expected 'ngcc' to correctly compile 'ApplicationModule' in '@angular/core' (main)." grep "ApplicationModule.ɵmod = ɵngcc0.ɵɵdefineNgModule" node_modules/@angular/core/esm2015/src/application_module.js assertSucceeded "Expected 'ngcc' to correctly compile 'ApplicationModule' in '@angular/core' (esm2015)." - grep "ApplicationModule.ɵmod = ɵngcc0.ɵɵdefineNgModule" node_modules/@angular/core/esm5/src/application_module.js - assertSucceeded "Expected 'ngcc' to correctly compile 'ApplicationModule' in '@angular/core' (esm5)." - # Did it place the `setClassMetadata` call correctly? cat node_modules/@angular/core/fesm2015/core.js | awk 'ORS=" "' | grep "ApplicationRef.ctorParameters.*setClassMetadata(ApplicationRef" assertSucceeded "Expected 'ngcc' to place 'setClassMetadata' after static properties like 'ctorParameters' in '@angular/core' (fesm2015)." diff --git a/package.json b/package.json index 4b5b6f48c3..dd83d259bc 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@angular-devkit/build-optimizer": "0.901.0", "@angular-devkit/core": "9.1.0", "@angular-devkit/schematics": "9.1.0", + "@babel/cli": "^7.8.4", "@babel/core": "^7.8.6", "@babel/generator": "^7.8.6", "@babel/template": "^7.8.6", diff --git a/packages/compiler-cli/ngcc/src/command_line_options.ts b/packages/compiler-cli/ngcc/src/command_line_options.ts index 65ca7fac61..081d859c9d 100644 --- a/packages/compiler-cli/ngcc/src/command_line_options.ts +++ b/packages/compiler-cli/ngcc/src/command_line_options.ts @@ -27,7 +27,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions { alias: 'properties', array: true, describe: - 'An array of names of properties in package.json to compile (e.g. `module` or `es2015`)\n' + + 'An array of names of properties in package.json to compile (e.g. `module` or `main`)\n' + 'Each of these properties should hold the path to a bundle-format.\n' + 'If provided, only the specified properties are considered for processing.\n' + 'If not provided, all the supported format properties (e.g. fesm2015, fesm5, es2015, esm2015, esm5, main, module) in the package.json are considered.' @@ -136,4 +136,4 @@ export function parseCommandLineOptions(args: string[]): NgccOptions { errorOnFailedEntryPoint, tsConfigPath }; -} \ No newline at end of file +} diff --git a/packages/compiler-cli/ngcc/src/packages/entry_point.ts b/packages/compiler-cli/ngcc/src/packages/entry_point.ts index 6ff336a2e0..271f94bd3a 100644 --- a/packages/compiler-cli/ngcc/src/packages/entry_point.ts +++ b/packages/compiler-cli/ngcc/src/packages/entry_point.ts @@ -207,6 +207,15 @@ export function getEntryPointFormat( } return sniffModuleFormat(fs, join(entryPoint.path, mainFile)); case 'module': + const moduleFilePath = entryPoint.packageJson['module']; + // As of version 10, the `module` property in `package.json` should point to + // the ESM2015 format output as per Angular Package format specification. This + // means that the `module` property captures multiple formats, as old libraries + // built with the old APF can still be processed. We detect the format by checking + // the paths that should be used as per APF specification. + if (typeof moduleFilePath === 'string' && moduleFilePath.includes('esm2015')) { + return `esm2015`; + } return 'esm5'; default: return undefined; diff --git a/packages/compiler-cli/ngcc/test/BUILD.bazel b/packages/compiler-cli/ngcc/test/BUILD.bazel index 320d64646a..9e603dcfcf 100644 --- a/packages/compiler-cli/ngcc/test/BUILD.bazel +++ b/packages/compiler-cli/ngcc/test/BUILD.bazel @@ -1,3 +1,4 @@ +load("@npm//@babel/cli:index.bzl", "babel") load("//tools:defaults.bzl", "jasmine_node_test", "ts_library") package(default_visibility = ["//visibility:public"]) @@ -62,11 +63,33 @@ ts_library( ], ) +# As of version 10, the release packages do not contain esm2015 output anymore. The ngcc +# integration tests intend to test ES5 features though, so we downlevel the flat esm2015 +# file to ES5 using Babel. We can then link that into the mock file system as if the Angular +# core package is still built with previous APF versions where esm5 output was shipped. This +# allows us to ensure that ngcc properly processes libraries with esm5 output. **Note**: We are +# using Babel instead of `tsc` as TypeScript does not allow us to downlevel the file without +# setting the module resolution to either `amd` or `system`. We want to preserve ES modules. +babel( + name = "fesm5_angular_core", + outs = ["fesm5_angular_core.js"], + args = [ + "$(execpath //packages/core:npm_package)/fesm2015/core.js", + "--presets @babel/preset-env", + "--out-file $(execpath fesm5_angular_core.js)", + ], + data = [ + "//packages/core:npm_package", + "@npm//@babel/preset-env", + ], +) + jasmine_node_test( name = "integration", timeout = "long", bootstrap = ["//tools/testing:node_no_angular_es5"], data = [ + ":fesm5_angular_core", "//packages/common:npm_package", "//packages/core:npm_package", "@npm//rxjs", diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts index 7503eeb6e6..b98f64f853 100644 --- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts +++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts @@ -7,6 +7,7 @@ */ /// +import {readFileSync} from 'fs'; import * as os from 'os'; import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem, join} from '../../../src/ngtsc/file_system'; @@ -21,7 +22,7 @@ import {Transformer} from '../../src/packages/transformer'; import {DirectPackageJsonUpdater, PackageJsonUpdater} from '../../src/writing/package_json_updater'; import {MockLogger} from '../helpers/mock_logger'; -import {compileIntoApf, compileIntoFlatEs5Package} from './util'; +import {compileIntoApf, compileIntoFlatEs2015Package, compileIntoFlatEs5Package} from './util'; const testFiles = loadStandardTestFiles({fakeCore: false, rxjs: true}); @@ -41,18 +42,44 @@ runInEachFileSystem(() => { spyOn(os, 'cpus').and.returnValue([{model: 'Mock CPU'} as any]); }); + /** + * Sets up the esm5 format in the Angular core package. By default, package output + * no longer contains esm5 output, so we process the fesm2015 file into ES5 and + * link it as if its the ESM5 output. + */ + function setupAngularCoreEsm5() { + const pkgPath = _('/node_modules/@angular/core'); + const pkgJsonPath = fs.join(pkgPath, 'package.json'); + const pkgJson = JSON.parse(fs.readFile(pkgJsonPath)); + + fs.ensureDir(fs.join(pkgPath, 'fesm5')); + fs.writeFile( + fs.join(pkgPath, 'fesm5/core.js'), + readFileSync(require.resolve('../fesm5_angular_core.js'), 'utf8')); + + pkgJson.esm5 = './fesm5/core.js'; + pkgJson.fesm5 = './fesm5/core.js'; + + fs.writeFile(pkgJsonPath, JSON.stringify(pkgJson, null, 2)); + } + it('should run ngcc without errors for esm2015', () => { expect(() => mainNgcc({basePath: '/node_modules', propertiesToConsider: ['esm2015']})) .not.toThrow(); }); it('should run ngcc without errors for esm5', () => { + setupAngularCoreEsm5(); + expect(() => mainNgcc({ basePath: '/node_modules', propertiesToConsider: ['esm5'], + targetEntryPointPath: '@angular/core', logger: new MockLogger(), })) .not.toThrow(); + + expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toBeDefined(); }); it('should run ngcc without errors when "main" property is not present', () => { @@ -114,6 +141,7 @@ runInEachFileSystem(() => { }); it('should generate correct metadata for decorated getter/setter properties', () => { + setupAngularCoreEsm5(); compileIntoFlatEs5Package('test-package', { '/index.ts': ` import {Directive, Input, NgModule} from '@angular/core'; @@ -134,7 +162,7 @@ runInEachFileSystem(() => { mainNgcc({ basePath: '/node_modules', targetEntryPointPath: 'test-package', - propertiesToConsider: ['module'], + propertiesToConsider: ['esm5'], }); const jsContents = fs.readFile(_(`/node_modules/test-package/index.js`)).replace(/\s+/g, ' '); @@ -150,6 +178,7 @@ runInEachFileSystem(() => { it(`should be able to process spread operator inside objects for ${ target} format (imported helpers)`, () => { + setupAngularCoreEsm5(); compileIntoApf( 'test-package', { '/index.ts': ` @@ -190,6 +219,7 @@ runInEachFileSystem(() => { it(`should be able to process emitted spread operator inside objects for ${ target} format (emitted helpers)`, () => { + setupAngularCoreEsm5(); compileIntoApf( 'test-package', { '/index.ts': ` @@ -225,6 +255,7 @@ runInEachFileSystem(() => { }); it('should not add `const` in ES5 generated code', () => { + setupAngularCoreEsm5(); compileIntoFlatEs5Package('test-package', { '/index.ts': ` import {Directive, Input, NgModule} from '@angular/core'; @@ -246,7 +277,7 @@ runInEachFileSystem(() => { mainNgcc({ basePath: '/node_modules', targetEntryPointPath: 'test-package', - propertiesToConsider: ['module'], + propertiesToConsider: ['esm5'], }); const jsContents = fs.readFile(_(`/node_modules/test-package/index.js`)); @@ -280,7 +311,7 @@ runInEachFileSystem(() => { ` }); - compileIntoFlatEs5Package('test-package', { + compileIntoFlatEs2015Package('test-package', { '/index.ts': ` import {Directive, Input, NgModule} from '@angular/core'; import * as lib from 'lib'; @@ -308,7 +339,7 @@ runInEachFileSystem(() => { mainNgcc({ basePath: '/node_modules', targetEntryPointPath: 'test-package', - propertiesToConsider: ['module'], + propertiesToConsider: ['esm2015'], }); const jsContents = fs.readFile(_(`/node_modules/test-package/index.js`)); @@ -358,7 +389,7 @@ runInEachFileSystem(() => { }); it('should add ɵfac but not duplicate ɵprov properties on injectables', () => { - compileIntoFlatEs5Package('test-package', { + compileIntoFlatEs2015Package('test-package', { '/index.ts': ` import {Injectable, ɵɵdefineInjectable} from '@angular/core'; export const TestClassToken = 'TestClassToken'; @@ -374,7 +405,7 @@ runInEachFileSystem(() => { mainNgcc({ basePath: '/node_modules', targetEntryPointPath: 'test-package', - propertiesToConsider: ['module'], + propertiesToConsider: ['esm2015'], }); const after = fs.readFile(_(`/node_modules/test-package/index.js`)); @@ -389,7 +420,7 @@ runInEachFileSystem(() => { // This is necessary to ensure XPipeDef.fac is defined when delegated from injectable def it('should always generate factory def (fac) before injectable def (prov)', () => { - compileIntoFlatEs5Package('test-package', { + compileIntoFlatEs2015Package('test-package', { '/index.ts': ` import {Injectable, Pipe, PipeTransform} from '@angular/core'; @@ -406,7 +437,7 @@ runInEachFileSystem(() => { mainNgcc({ basePath: '/node_modules', targetEntryPointPath: 'test-package', - propertiesToConsider: ['module'], + propertiesToConsider: ['esm2015'], }); const jsContents = fs.readFile(_(`/node_modules/test-package/index.js`)); @@ -508,6 +539,7 @@ runInEachFileSystem(() => { }); it('should use `$localize` calls rather than tagged templates in ES5 generated code', () => { + setupAngularCoreEsm5(); compileIntoFlatEs5Package('test-package', { '/index.ts': ` import {Component, Input, NgModule} from '@angular/core'; @@ -529,7 +561,7 @@ runInEachFileSystem(() => { mainNgcc({ basePath: '/node_modules', targetEntryPointPath: 'test-package', - propertiesToConsider: ['module'], + propertiesToConsider: ['esm5'], }); const jsContents = fs.readFile(_(`/node_modules/test-package/index.js`)); @@ -611,9 +643,7 @@ runInEachFileSystem(() => { main: '0.0.0-PLACEHOLDER', module: '0.0.0-PLACEHOLDER', es2015: '0.0.0-PLACEHOLDER', - esm5: '0.0.0-PLACEHOLDER', esm2015: '0.0.0-PLACEHOLDER', - fesm5: '0.0.0-PLACEHOLDER', fesm2015: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER', }; @@ -710,7 +740,7 @@ runInEachFileSystem(() => { mainNgcc({ basePath: '/node_modules', targetEntryPointPath: '@angular/common/http/testing', - propertiesToConsider: ['fesm2015', 'esm5', 'esm2015'], + propertiesToConsider: ['fesm2015', 'main', 'esm2015'], logger, }); expect(logger.logs.debug).not.toContain([ @@ -727,7 +757,7 @@ runInEachFileSystem(() => { mainNgcc({ basePath: '/node_modules', targetEntryPointPath: '@angular/common/http/testing', - propertiesToConsider: ['esm5', 'esm2015'], + propertiesToConsider: ['main', 'esm2015'], compileAllFormats: false, logger, }); @@ -784,7 +814,7 @@ runInEachFileSystem(() => { } it('should clean up outdated artifacts', () => { - compileIntoFlatEs5Package('test-package', { + compileIntoFlatEs2015Package('test-package', { 'index.ts': ` import {Directive} from '@angular/core'; @@ -795,7 +825,7 @@ runInEachFileSystem(() => { }); mainNgcc({ basePath: '/node_modules', - propertiesToConsider: ['module'], + propertiesToConsider: ['esm2015'], logger: new MockLogger(), }); @@ -811,12 +841,12 @@ runInEachFileSystem(() => { // Now run ngcc again to see that it cleans out the outdated artifacts mainNgcc({ basePath: '/node_modules', - propertiesToConsider: ['module'], + propertiesToConsider: ['esm2015'], logger: new MockLogger(), }); const newPackageJson = loadPackage('test-package', _('/node_modules')); expect(newPackageJson.__processed_by_ivy_ngcc__).toEqual({ - module: '0.0.0-PLACEHOLDER', + esm2015: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER', }); expect(newPackageJson.module_ivy_ngcc).toBeUndefined(); @@ -843,38 +873,41 @@ runInEachFileSystem(() => { () => { mainNgcc({ basePath: '/node_modules', - propertiesToConsider: ['main', 'esm5', 'module', 'fesm5'], + propertiesToConsider: ['main', 'module'], logger: new MockLogger(), - }); // The ES2015 formats are not compiled as they are not in `propertiesToConsider`. expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({ - esm5: '0.0.0-PLACEHOLDER', main: '0.0.0-PLACEHOLDER', + // `module` and `es2015` are aliases of `fesm2015`. module: '0.0.0-PLACEHOLDER', - fesm5: '0.0.0-PLACEHOLDER', + es2015: '0.0.0-PLACEHOLDER', + fesm2015: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER', }); expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__).toEqual({ - esm5: '0.0.0-PLACEHOLDER', main: '0.0.0-PLACEHOLDER', + // `module` and `es2015` are aliases of `fesm2015`. module: '0.0.0-PLACEHOLDER', - fesm5: '0.0.0-PLACEHOLDER', + es2015: '0.0.0-PLACEHOLDER', + fesm2015: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER', }); expect(loadPackage('@angular/common/testing').__processed_by_ivy_ngcc__).toEqual({ - esm5: '0.0.0-PLACEHOLDER', main: '0.0.0-PLACEHOLDER', + // `module` and `es2015` are aliases for `fesm2015`. module: '0.0.0-PLACEHOLDER', - fesm5: '0.0.0-PLACEHOLDER', + es2015: '0.0.0-PLACEHOLDER', + fesm2015: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER', }); expect(loadPackage('@angular/common/http').__processed_by_ivy_ngcc__).toEqual({ - esm5: '0.0.0-PLACEHOLDER', main: '0.0.0-PLACEHOLDER', + // `module` and `es2015` are aliases for `fesm2015`. module: '0.0.0-PLACEHOLDER', - fesm5: '0.0.0-PLACEHOLDER', + es2015: '0.0.0-PLACEHOLDER', + fesm2015: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER', }); }); @@ -893,6 +926,8 @@ runInEachFileSystem(() => { expect(logs).not.toContain(['Skipping @angular/common : es2015 (already compiled).']); expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__).toEqual({ + // `module` and `es2015` are aliases of `fesm2015`. + module: '0.0.0-PLACEHOLDER', es2015: '0.0.0-PLACEHOLDER', fesm2015: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER', @@ -913,30 +948,34 @@ runInEachFileSystem(() => { it('should only compile the first matching format', () => { mainNgcc({ basePath: '/node_modules', - propertiesToConsider: ['module', 'fesm5', 'esm5'], + propertiesToConsider: ['module', 'fesm2015', 'main'], compileAllFormats: false, logger: new MockLogger(), }); - // * In the Angular packages fesm5 and module have the same underlying format, - // so both are marked as compiled. - // * The `esm5` is not compiled because we stopped after the `fesm5` format. + // * In the Angular packages fesm2015, module and `es2015` have the same + // underlying format, so both are marked as compiled. + // * The `main` is not compiled because we stopped after the `fesm2015` format. expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({ - fesm5: '0.0.0-PLACEHOLDER', + fesm2015: '0.0.0-PLACEHOLDER', + es2015: '0.0.0-PLACEHOLDER', module: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER', }); expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__).toEqual({ - fesm5: '0.0.0-PLACEHOLDER', + fesm2015: '0.0.0-PLACEHOLDER', + es2015: '0.0.0-PLACEHOLDER', module: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER', }); expect(loadPackage('@angular/common/testing').__processed_by_ivy_ngcc__).toEqual({ - fesm5: '0.0.0-PLACEHOLDER', + fesm2015: '0.0.0-PLACEHOLDER', + es2015: '0.0.0-PLACEHOLDER', module: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER', }); expect(loadPackage('@angular/common/http').__processed_by_ivy_ngcc__).toEqual({ - fesm5: '0.0.0-PLACEHOLDER', + fesm2015: '0.0.0-PLACEHOLDER', + es2015: '0.0.0-PLACEHOLDER', module: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER', }); @@ -946,27 +985,25 @@ runInEachFileSystem(() => { () => { mainNgcc({ basePath: '/node_modules', - propertiesToConsider: ['module'], + propertiesToConsider: ['main'], compileAllFormats: false, logger: new MockLogger(), }); expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({ - fesm5: '0.0.0-PLACEHOLDER', - module: '0.0.0-PLACEHOLDER', + main: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER', }); // If ngcc tries to write out the typings files again, this will throw an exception. mainNgcc({ basePath: '/node_modules', - propertiesToConsider: ['esm5'], + propertiesToConsider: ['esm2015'], compileAllFormats: false, logger: new MockLogger(), }); expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({ - esm5: '0.0.0-PLACEHOLDER', - fesm5: '0.0.0-PLACEHOLDER', - module: '0.0.0-PLACEHOLDER', + main: '0.0.0-PLACEHOLDER', + esm2015: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER', }); }); @@ -978,34 +1015,33 @@ runInEachFileSystem(() => { mainNgcc({ basePath: '/node_modules', createNewEntryPointFormats: true, - propertiesToConsider: ['esm5'], + propertiesToConsider: ['esm2015'], logger: new MockLogger(), - }); // 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'); + expect(loadPackage('@angular/common').esm2015).toEqual('./esm2015/common.js'); + expect((loadPackage('@angular/common') as any).esm2015_ivy_ngcc) + .toEqual('__ivy_ngcc__/esm2015/common.js'); // Doesn't touch original files - expect(fs.readFile(_(`/node_modules/@angular/common/esm5/src/common_module.js`))) + expect(fs.readFile(_(`/node_modules/@angular/common/esm2015/src/common_module.js`))) .not.toMatch(ANGULAR_CORE_IMPORT_REGEX); // Or create a backup of the original - expect( - fs.exists(_(`/node_modules/@angular/common/esm5/src/common_module.js.__ivy_ngcc_bak`))) + expect(fs.exists( + _(`/node_modules/@angular/common/esm2015/src/common_module.js.__ivy_ngcc_bak`))) .toBe(false); // Creates new files - expect( - fs.readFile(_(`/node_modules/@angular/common/__ivy_ngcc__/esm5/src/common_module.js`))) + expect(fs.readFile( + _(`/node_modules/@angular/common/__ivy_ngcc__/esm2015/src/common_module.js`))) .toMatch(ANGULAR_CORE_IMPORT_REGEX); // Copies over files (unchanged) that did not need compiling - expect(fs.exists(_(`/node_modules/@angular/common/__ivy_ngcc__/esm5/src/version.js`))) + expect(fs.exists(_(`/node_modules/@angular/common/__ivy_ngcc__/esm2015/src/version.js`))) .toBeTrue(); - expect(fs.readFile(_(`/node_modules/@angular/common/__ivy_ngcc__/esm5/src/version.js`))) - .toEqual(fs.readFile(_(`/node_modules/@angular/common/esm5/src/version.js`))); + expect(fs.readFile(_(`/node_modules/@angular/common/__ivy_ngcc__/esm2015/src/version.js`))) + .toEqual(fs.readFile(_(`/node_modules/@angular/common/esm2015/src/version.js`))); // Overwrites .d.ts files (as usual) expect(fs.readFile(_(`/node_modules/@angular/common/common.d.ts`))) @@ -1017,69 +1053,71 @@ runInEachFileSystem(() => { mainNgcc({ basePath: '/node_modules/@angular/core', createNewEntryPointFormats: true, - propertiesToConsider: ['fesm2015', 'fesm5'], + propertiesToConsider: ['fesm2015', 'main'], }); const pkg: any = loadPackage('@angular/core'); - // `es2015` is an alias of `fesm2015`. + // `es2015` and `module` are aliases of `fesm2015`. expect(pkg.fesm2015).toEqual('./fesm2015/core.js'); expect(pkg.es2015).toEqual('./fesm2015/core.js'); + expect(pkg.module).toEqual('./fesm2015/core.js'); expect(pkg.fesm2015_ivy_ngcc).toEqual('__ivy_ngcc__/fesm2015/core.js'); expect(pkg.es2015_ivy_ngcc).toEqual('__ivy_ngcc__/fesm2015/core.js'); + expect(pkg.module_ivy_ngcc).toEqual('__ivy_ngcc__/fesm2015/core.js'); - // `module` is an alias of `fesm5`. - expect(pkg.fesm5).toEqual('./fesm5/core.js'); - expect(pkg.module).toEqual('./fesm5/core.js'); - expect(pkg.fesm5_ivy_ngcc).toEqual('__ivy_ngcc__/fesm5/core.js'); - expect(pkg.module_ivy_ngcc).toEqual('__ivy_ngcc__/fesm5/core.js'); + expect(pkg.main).toEqual('./bundles/core.umd.js'); + expect(pkg.main_ivy_ngcc).toEqual('__ivy_ngcc__/bundles/core.umd.js'); }); it('should update `package.json` deterministically (regardless of entry-point processing order)', () => { // Ensure formats are not marked as processed in `package.json` at the beginning. let pkg = loadPackage('@angular/core'); - expectNotToHaveProp(pkg, 'esm5_ivy_ngcc'); + expectNotToHaveProp(pkg, 'main_ivy_ngcc'); + expectNotToHaveProp(pkg, 'esm2015_ivy_ngcc'); expectNotToHaveProp(pkg, 'fesm2015_ivy_ngcc'); - expectNotToHaveProp(pkg, 'fesm5_ivy_ngcc'); + expectNotToHaveProp(pkg, 'module_ivy_ngcc'); expectNotToHaveProp(pkg, '__processed_by_ivy_ngcc__'); // Process `fesm2015` and update `package.json`. pkg = processFormatAndUpdatePackageJson('fesm2015'); - expectNotToHaveProp(pkg, 'esm5_ivy_ngcc'); + expectNotToHaveProp(pkg, 'main_ivy_ngcc'); + expectNotToHaveProp(pkg, 'esm2015_ivy_ngcc'); expectToHaveProp(pkg, 'fesm2015_ivy_ngcc'); - expectNotToHaveProp(pkg, 'fesm5_ivy_ngcc'); + expectToHaveProp(pkg, 'module_ivy_ngcc'); expectToHaveProp(pkg.__processed_by_ivy_ngcc__!, 'fesm2015'); - // Process `fesm5` and update `package.json`. - pkg = processFormatAndUpdatePackageJson('fesm5'); - expectNotToHaveProp(pkg, 'esm5_ivy_ngcc'); + // Process `esm2015` and update `package.json`. + pkg = processFormatAndUpdatePackageJson('esm2015'); + expectNotToHaveProp(pkg, 'main_ivy_ngcc'); + expectToHaveProp(pkg, 'esm2015_ivy_ngcc'); expectToHaveProp(pkg, 'fesm2015_ivy_ngcc'); - expectToHaveProp(pkg, 'fesm5_ivy_ngcc'); - expectToHaveProp(pkg.__processed_by_ivy_ngcc__!, 'fesm5'); + expectToHaveProp(pkg, 'module_ivy_ngcc'); + expectToHaveProp(pkg.__processed_by_ivy_ngcc__!, 'esm2015'); - // Process `esm5` and update `package.json`. - pkg = processFormatAndUpdatePackageJson('esm5'); - expectToHaveProp(pkg, 'esm5_ivy_ngcc'); + // Process `main` and update `package.json`. + pkg = processFormatAndUpdatePackageJson('main'); + expectToHaveProp(pkg, 'main_ivy_ngcc'); + expectToHaveProp(pkg, 'esm2015_ivy_ngcc'); expectToHaveProp(pkg, 'fesm2015_ivy_ngcc'); - expectToHaveProp(pkg, 'fesm5_ivy_ngcc'); - expectToHaveProp(pkg.__processed_by_ivy_ngcc__!, 'esm5'); + expectToHaveProp(pkg, 'module_ivy_ngcc'); + expectToHaveProp(pkg.__processed_by_ivy_ngcc__!, 'main'); // Ensure the properties are in deterministic order (regardless of processing order). const pkgKeys = stringifyKeys(pkg); - expect(pkgKeys).toContain('|esm5_ivy_ngcc|esm5|'); + expect(pkgKeys).toContain('|main_ivy_ngcc|main|'); expect(pkgKeys).toContain('|fesm2015_ivy_ngcc|fesm2015|'); - expect(pkgKeys).toContain('|fesm5_ivy_ngcc|fesm5|'); + expect(pkgKeys).toContain('|esm2015_ivy_ngcc|esm2015|'); // NOTE: // Along with the first format that is processed, the typings are processed as well. // Also, once a property has been processed, alias properties as also marked as // processed. Aliases properties are properties that point to the same entry-point file. // For example: - // - `fesm2015` <=> `es2015` - // - `fesm5` <=> `module` + // - `fesm2015` <=> `module <=> es2015` expect(stringifyKeys(pkg.__processed_by_ivy_ngcc__!)) - .toBe('|es2015|esm5|fesm2015|fesm5|module|typings|'); + .toBe('|es2015|esm2015|fesm2015|main|module|typings|'); // Helpers function expectNotToHaveProp(obj: object, prop: string) { @@ -1118,11 +1156,11 @@ runInEachFileSystem(() => { fs.writeFile(_('/yarn.lock'), 'DUMMY YARN LOCK FILE'); // Populate the manifest file mainNgcc( - {basePath: '/node_modules', propertiesToConsider: ['esm5'], logger: new MockLogger()}); + {basePath: '/node_modules', propertiesToConsider: ['main'], logger: new MockLogger()}); // Check that common/testing ES5 was processed let commonTesting = JSON.parse(fs.readFile(_('/node_modules/@angular/common/testing/package.json'))); - expect(hasBeenProcessed(commonTesting, 'esm5')).toBe(true); + expect(hasBeenProcessed(commonTesting, 'main')).toBe(true); expect(hasBeenProcessed(commonTesting, 'esm2015')).toBe(false); // Modify the manifest to test that is has no effect let manifest: EntryPointManifestFile = @@ -1141,7 +1179,7 @@ runInEachFileSystem(() => { // Check that common/testing ES2015 is now processed, despite the manifest not listing it commonTesting = JSON.parse(fs.readFile(_('/node_modules/@angular/common/testing/package.json'))); - expect(hasBeenProcessed(commonTesting, 'esm5')).toBe(true); + expect(hasBeenProcessed(commonTesting, 'main')).toBe(true); expect(hasBeenProcessed(commonTesting, 'esm2015')).toBe(true); // Check that the newly computed manifest has written to disk, containing the path that we // had removed earlier. @@ -1383,6 +1421,8 @@ runInEachFileSystem(() => { logger }); expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({ + // `module` and `es2015` are aliases for `fesm2015`. + module: '0.0.0-PLACEHOLDER', es2015: '0.0.0-PLACEHOLDER', fesm2015: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER', @@ -1523,6 +1563,7 @@ runInEachFileSystem(() => { typings: '0.0.0-PLACEHOLDER', }); expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({ + module: '0.0.0-PLACEHOLDER', es2015: '0.0.0-PLACEHOLDER', fesm2015: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER', @@ -1550,6 +1591,7 @@ runInEachFileSystem(() => { mainNgcc({basePath: '/node_modules', propertiesToConsider: ['es2015']}); // We process core but not core/testing. expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({ + module: '0.0.0-PLACEHOLDER', es2015: '0.0.0-PLACEHOLDER', fesm2015: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER', @@ -1558,6 +1600,7 @@ runInEachFileSystem(() => { // We do not compile common but we do compile its sub-entry-points. expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__).toBeUndefined(); expect(loadPackage('@angular/common/http').__processed_by_ivy_ngcc__).toEqual({ + module: '0.0.0-PLACEHOLDER', es2015: '0.0.0-PLACEHOLDER', fesm2015: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER', @@ -1629,7 +1672,7 @@ runInEachFileSystem(() => { describe('undecorated child class migration', () => { it('should generate a directive definition with CopyDefinitionFeature for an undecorated child directive', () => { - compileIntoFlatEs5Package('test-package', { + compileIntoFlatEs2015Package('test-package', { '/index.ts': ` import {Directive, NgModule} from '@angular/core'; @@ -1651,7 +1694,7 @@ runInEachFileSystem(() => { mainNgcc({ basePath: '/node_modules', targetEntryPointPath: 'test-package', - propertiesToConsider: ['module'], + propertiesToConsider: ['esm2015'], }); @@ -1670,7 +1713,7 @@ runInEachFileSystem(() => { it('should generate a component definition with CopyDefinitionFeature for an undecorated child component', () => { - compileIntoFlatEs5Package('test-package', { + compileIntoFlatEs2015Package('test-package', { '/index.ts': ` import {Component, NgModule} from '@angular/core'; @@ -1692,7 +1735,7 @@ runInEachFileSystem(() => { mainNgcc({ basePath: '/node_modules', targetEntryPointPath: 'test-package', - propertiesToConsider: ['module'], + propertiesToConsider: ['esm2015'], }); @@ -1710,7 +1753,7 @@ runInEachFileSystem(() => { it('should generate directive definitions with CopyDefinitionFeature for undecorated child directives in a long inheritance chain', () => { - compileIntoFlatEs5Package('test-package', { + compileIntoFlatEs2015Package('test-package', { '/index.ts': ` import {Directive, NgModule} from '@angular/core'; @@ -1735,7 +1778,7 @@ runInEachFileSystem(() => { mainNgcc({ basePath: '/node_modules', targetEntryPointPath: 'test-package', - propertiesToConsider: ['module'], + propertiesToConsider: ['esm2015'], }); const dtsContents = fs.readFile(_(`/node_modules/test-package/index.d.ts`)); diff --git a/packages/compiler-cli/ngcc/test/integration/util.ts b/packages/compiler-cli/ngcc/test/integration/util.ts index 735b33d13b..c48d223604 100644 --- a/packages/compiler-cli/ngcc/test/integration/util.ts +++ b/packages/compiler-cli/ngcc/test/integration/util.ts @@ -33,7 +33,28 @@ export function compileIntoFlatEs5Package(pkgName: string, sources: PackageSourc compileIntoFlatPackage(pkgName, sources, { target: ts.ScriptTarget.ES5, module: ts.ModuleKind.ESNext, - formatProperty: 'module', + formatProperty: 'esm5', + }); +} + +/** + * Instead of writing packaged code by hand, and manually describing the layout of the package, + * this function transpiles the TypeScript sources into a flat file structure using the ES2015 + * format. In this package layout, all compiled sources are at the root of the package, with + * `.d.ts` files next to the `.js` files. Each `.js` also has a corresponding `.metadata.js` + * file alongside with it. + * + * All generated code is written into the `node_modules` in the top-level filesystem, ready for use + * in testing ngcc. + * + * @param pkgName The name of the package to compile. + * @param sources The TypeScript sources to compile. + */ +export function compileIntoFlatEs2015Package(pkgName: string, sources: PackageSources): void { + compileIntoFlatPackage(pkgName, sources, { + target: ts.ScriptTarget.ES2015, + module: ts.ModuleKind.ESNext, + formatProperty: 'esm2015', }); } @@ -184,7 +205,7 @@ export function compileIntoApf( version: '0.0.1', esm5: './esm5/index.js', esm2015: './esm2015/index.js', - module: './esm5/index.js', + module: './esm2015/index.js', typings: './index.d.ts', }; diff --git a/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts b/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts index aef13dbe65..4a0dc47aab 100644 --- a/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts @@ -429,6 +429,12 @@ runInEachFileSystem(() => { expect(getEntryPointFormat(fs, entryPoint, 'module')).toBe('esm5'); }); + it('should return `esm2015` format for `module` property if it points to esm2015 output', + () => { + entryPoint.packageJson['module'] = '../fesm2015/valid-entry-point.js'; + expect(getEntryPointFormat(fs, entryPoint, 'module')).toBe('esm2015'); + }); + (['browser', 'main'] as EntryPointJsonProperty[]).forEach(browserOrMain => { it('should return `esm5` for `' + browserOrMain + '` if the file contains import or export statements', diff --git a/yarn.lock b/yarn.lock index 08864a0d27..329f19beba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -153,6 +153,22 @@ universal-analytics "0.4.20" uuid "7.0.2" +"@babel/cli@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.8.4.tgz#505fb053721a98777b2b175323ea4f090b7d3c1c" + integrity sha512-XXLgAm6LBbaNxaGhMAznXXaxtCWfuv6PIDJ9Alsy9JYTOh+j2jJz+L/162kkfU1j/pTSxK1xGmlwI4pdIMkoag== + dependencies: + commander "^4.0.1" + convert-source-map "^1.1.0" + fs-readdir-recursive "^1.1.0" + glob "^7.0.0" + lodash "^4.17.13" + make-dir "^2.1.0" + slash "^2.0.0" + source-map "^0.5.0" + optionalDependencies: + chokidar "^2.1.8" + "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" @@ -3984,7 +4000,7 @@ conventional-commits-parser@^3.0.3, conventional-commits-parser@^3.0.8: through2 "^3.0.0" trim-off-newlines "^1.0.0" -convert-source-map@^1.5.1, convert-source-map@^1.7.0: +convert-source-map@^1.1.0, convert-source-map@^1.5.1, convert-source-map@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== @@ -6039,6 +6055,11 @@ fs-promise@0.3.1: dependencies: any-promise "~0.1.0" +fs-readdir-recursive@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27" + integrity sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA== + fs-write-stream-atomic@^1.0.8: version "1.0.10" resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" @@ -12626,6 +12647,11 @@ slash@^1.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"