Previously we always walked the whole folder tree looking for entry-points before we tested whether a target package had been processed already. This could take >10secs! This commit does a quick check of the target package before doing the full walk which brings down the execution time for ngcc in this case dramatically. ``` $ time ./node_modules/.bin/ivy-ngcc -t @angular/common/http/testing Compiling @angular/core : fesm2015 as esm2015 Compiling @angular/core : fesm5 as esm5 Compiling @angular/core : esm2015 as esm2015 Compiling @angular/core : esm5 as esm5 Compiling @angular/common/http : fesm2015 as esm2015 Compiling @angular/common/http : fesm5 as esm5 Compiling @angular/common/http : esm2015 as esm2015 Compiling @angular/common/http : esm5 as esm5 Compiling @angular/common/http/testing : fesm2015 as esm2015 Compiling @angular/common/http/testing : fesm5 as esm5 Compiling @angular/common/http/testing : esm2015 as esm2015 Compiling @angular/common/http/testing : esm5 as esm5 real 0m19.766s user 0m28.533s sys 0m2.262s ``` ``` $ time ./node_modules/.bin/ivy-ngcc -t @angular/common/http/testing The target entry-point has already been processed real 0m0.666s user 0m0.605s sys 0m0.113s ``` PR Close #29740
		
			
				
	
	
		
			417 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			417 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| /**
 | |
|  * @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} from '@angular/compiler-cli/src/ngtsc/path';
 | |
| import {existsSync, readFileSync, readdirSync, statSync, writeFileSync} from 'fs';
 | |
| import * as mockFs from 'mock-fs';
 | |
| import {join} from 'path';
 | |
| const Module = require('module');
 | |
| 
 | |
| import {getAngularPackagesFromRunfiles, resolveNpmTreeArtifact} from '../../../test/runfile_helpers';
 | |
| import {mainNgcc} from '../../src/main';
 | |
| import {markAsProcessed} from '../../src/packages/build_marker';
 | |
| import {EntryPointJsonProperty, EntryPointPackageJson, SUPPORTED_FORMAT_PROPERTIES} from '../../src/packages/entry_point';
 | |
| import {MockLogger} from '../helpers/mock_logger';
 | |
| 
 | |
| const _ = AbsoluteFsPath.from;
 | |
| 
 | |
| describe('ngcc main()', () => {
 | |
|   beforeEach(createMockFileSystem);
 | |
|   afterEach(restoreRealFileSystem);
 | |
| 
 | |
|   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', () => {
 | |
|     expect(() => mainNgcc({
 | |
|              basePath: '/node_modules',
 | |
|              propertiesToConsider: ['esm5'],
 | |
|              logger: new MockLogger(),
 | |
|            }))
 | |
|         .not.toThrow();
 | |
|   });
 | |
| 
 | |
|   describe('with targetEntryPointPath', () => {
 | |
|     it('should only compile the given package entry-point (and its dependencies).', () => {
 | |
|       mainNgcc({basePath: '/node_modules', targetEntryPointPath: '@angular/common/http'});
 | |
| 
 | |
|       expect(loadPackage('@angular/common/http').__processed_by_ivy_ngcc__).toEqual({
 | |
|         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',
 | |
|       });
 | |
|       // * `common` is a dependency of `common/http`, so is compiled.
 | |
|       expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__).toEqual({
 | |
|         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',
 | |
|       });
 | |
|       // * `core` is a dependency of `common`, so is compiled.
 | |
|       expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
 | |
|         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',
 | |
|       });
 | |
| 
 | |
|       // * `common/testing` is not a dependency of `common/http` so is not compiled.
 | |
|       expect(loadPackage('@angular/common/testing').__processed_by_ivy_ngcc__).toBeUndefined();
 | |
|     });
 | |
| 
 | |
|     it('should mark a non-Angular package target as processed', () => {
 | |
|       mainNgcc({basePath: '/node_modules', targetEntryPointPath: 'test-package'});
 | |
| 
 | |
|       // `test-package` has no Angular but is marked as processed.
 | |
|       expect(loadPackage('test-package').__processed_by_ivy_ngcc__).toEqual({
 | |
|         es2015: '0.0.0-PLACEHOLDER',
 | |
|       });
 | |
| 
 | |
|       // * `core` is a dependency of `test-package`, but it is not processed, since test-package
 | |
|       // was not processed.
 | |
|       expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toBeUndefined();
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('early skipping of target entry-point', () => {
 | |
|     describe('[compileAllFormats === true]', () => {
 | |
|       it('should skip all processing if all the properties are marked as processed', () => {
 | |
|         const logger = new MockLogger();
 | |
|         markPropertiesAsProcessed('@angular/common/http/testing', SUPPORTED_FORMAT_PROPERTIES);
 | |
|         mainNgcc({
 | |
|           basePath: '/node_modules',
 | |
|           targetEntryPointPath: '@angular/common/http/testing', logger,
 | |
|         });
 | |
|         expect(logger.logs.info).toContain(['The target entry-point has already been processed']);
 | |
|       });
 | |
| 
 | |
|       it('should process the target if any `propertyToConsider` is not marked as processed', () => {
 | |
|         const logger = new MockLogger();
 | |
|         markPropertiesAsProcessed('@angular/common/http/testing', ['esm2015', 'fesm2015']);
 | |
|         mainNgcc({
 | |
|           basePath: '/node_modules',
 | |
|           targetEntryPointPath: '@angular/common/http/testing',
 | |
|           propertiesToConsider: ['fesm2015', 'esm5', 'esm2015'], logger,
 | |
|         });
 | |
|         expect(logger.logs.info).not.toContain([
 | |
|           'The target entry-point has already been processed'
 | |
|         ]);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe('[compileAllFormats === false]', () => {
 | |
|       it('should process the target if the first matching `propertyToConsider` is not marked as processed',
 | |
|          () => {
 | |
|            const logger = new MockLogger();
 | |
|            markPropertiesAsProcessed('@angular/common/http/testing', ['esm2015']);
 | |
|            mainNgcc({
 | |
|              basePath: '/node_modules',
 | |
|              targetEntryPointPath: '@angular/common/http/testing',
 | |
|              propertiesToConsider: ['esm5', 'esm2015'],
 | |
|              compileAllFormats: false, logger,
 | |
|            });
 | |
| 
 | |
|            expect(logger.logs.info).not.toContain([
 | |
|              'The target entry-point has already been processed'
 | |
|            ]);
 | |
|          });
 | |
| 
 | |
|       it('should skip all processing if the first matching `propertyToConsider` is marked as processed',
 | |
|          () => {
 | |
|            const logger = new MockLogger();
 | |
|            markPropertiesAsProcessed('@angular/common/http/testing', ['esm2015']);
 | |
|            mainNgcc({
 | |
|              basePath: '/node_modules',
 | |
|              targetEntryPointPath: '@angular/common/http/testing',
 | |
|              // Simulate a property that does not exist on the package.json and will be ignored.
 | |
|              propertiesToConsider: ['missing', 'esm2015', 'esm5'],
 | |
|              compileAllFormats: false, logger,
 | |
|            });
 | |
| 
 | |
|            expect(logger.logs.info).toContain([
 | |
|              'The target entry-point has already been processed'
 | |
|            ]);
 | |
|          });
 | |
|     });
 | |
|   });
 | |
| 
 | |
| 
 | |
|   function markPropertiesAsProcessed(packagePath: string, properties: EntryPointJsonProperty[]) {
 | |
|     const basePath = '/node_modules';
 | |
|     const targetPackageJsonPath = _(join(basePath, packagePath, 'package.json'));
 | |
|     const targetPackage = loadPackage(packagePath);
 | |
|     markAsProcessed(targetPackage, targetPackageJsonPath, 'typings');
 | |
|     properties.forEach(property => markAsProcessed(targetPackage, targetPackageJsonPath, property));
 | |
|   }
 | |
| 
 | |
| 
 | |
|   describe('with propertiesToConsider', () => {
 | |
|     it('should only compile the entry-point formats given in the `propertiesToConsider` list',
 | |
|        () => {
 | |
|          mainNgcc({
 | |
|            basePath: '/node_modules',
 | |
|            propertiesToConsider: ['main', 'esm5', 'module', 'fesm5'],
 | |
|            logger: new MockLogger(),
 | |
| 
 | |
|          });
 | |
| 
 | |
|          // * the `main` property is UMD, which is not yet supported.
 | |
|          // * none of the ES2015 formats are compiled as they are not on the `propertiesToConsider`
 | |
|          // list.
 | |
|          expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
 | |
|            esm5: '0.0.0-PLACEHOLDER',
 | |
|            module: '0.0.0-PLACEHOLDER',
 | |
|            fesm5: '0.0.0-PLACEHOLDER',
 | |
|            typings: '0.0.0-PLACEHOLDER',
 | |
|          });
 | |
|          expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__).toEqual({
 | |
|            esm5: '0.0.0-PLACEHOLDER',
 | |
|            module: '0.0.0-PLACEHOLDER',
 | |
|            fesm5: '0.0.0-PLACEHOLDER',
 | |
|            typings: '0.0.0-PLACEHOLDER',
 | |
|          });
 | |
|          expect(loadPackage('@angular/common/testing').__processed_by_ivy_ngcc__).toEqual({
 | |
|            esm5: '0.0.0-PLACEHOLDER',
 | |
|            module: '0.0.0-PLACEHOLDER',
 | |
|            fesm5: '0.0.0-PLACEHOLDER',
 | |
|            typings: '0.0.0-PLACEHOLDER',
 | |
|          });
 | |
|          expect(loadPackage('@angular/common/http').__processed_by_ivy_ngcc__).toEqual({
 | |
|            esm5: '0.0.0-PLACEHOLDER',
 | |
|            module: '0.0.0-PLACEHOLDER',
 | |
|            fesm5: '0.0.0-PLACEHOLDER',
 | |
|            typings: '0.0.0-PLACEHOLDER',
 | |
|          });
 | |
|        });
 | |
|   });
 | |
| 
 | |
|   describe('with compileAllFormats set to false', () => {
 | |
|     it('should only compile the first matching format', () => {
 | |
|       mainNgcc({
 | |
|         basePath: '/node_modules',
 | |
|         propertiesToConsider: ['main', 'module', 'fesm5', 'esm5'],
 | |
|         compileAllFormats: false,
 | |
|         logger: new MockLogger(),
 | |
| 
 | |
|       });
 | |
|       // * The `main` is UMD, which is not yet supported, and so is not compiled.
 | |
|       // * 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.
 | |
|       expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
 | |
|         fesm5: '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',
 | |
|         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',
 | |
|         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',
 | |
|         module: '0.0.0-PLACEHOLDER',
 | |
|         typings: '0.0.0-PLACEHOLDER',
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     it('should cope with compiling the same entry-point multiple times with different formats',
 | |
|        () => {
 | |
|          mainNgcc({
 | |
|            basePath: '/node_modules',
 | |
|            propertiesToConsider: ['module'],
 | |
|            compileAllFormats: false,
 | |
|            logger: new MockLogger(),
 | |
| 
 | |
|          });
 | |
|          expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
 | |
|            module: '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'],
 | |
|            compileAllFormats: false,
 | |
|            logger: new MockLogger(),
 | |
|          });
 | |
|          expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
 | |
|            esm5: '0.0.0-PLACEHOLDER',
 | |
|            module: '0.0.0-PLACEHOLDER',
 | |
|            typings: '0.0.0-PLACEHOLDER',
 | |
|          });
 | |
|        });
 | |
|   });
 | |
| 
 | |
|   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'],
 | |
|         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');
 | |
| 
 | |
|       // 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);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('logger', () => {
 | |
|     it('should log info message to the console by default', () => {
 | |
|       const consoleInfoSpy = spyOn(console, 'info');
 | |
|       mainNgcc({basePath: '/node_modules', propertiesToConsider: ['esm2015']});
 | |
|       expect(consoleInfoSpy)
 | |
|           .toHaveBeenCalledWith('Compiling @angular/common/http : esm2015 as esm2015');
 | |
|     });
 | |
| 
 | |
|     it('should use a custom logger if provided', () => {
 | |
|       const logger = new MockLogger();
 | |
|       mainNgcc({
 | |
|         basePath: '/node_modules',
 | |
|         propertiesToConsider: ['esm2015'], logger,
 | |
|       });
 | |
|       expect(logger.logs.info).toContain(['Compiling @angular/common/http : esm2015 as esm2015']);
 | |
|     });
 | |
|   });
 | |
| });
 | |
| 
 | |
| 
 | |
| function createMockFileSystem() {
 | |
|   mockFs({
 | |
|     '/node_modules/@angular': loadAngularPackages(),
 | |
|     '/node_modules/rxjs': loadDirectory(resolveNpmTreeArtifact('rxjs', 'index.js')),
 | |
|     '/node_modules/tslib': loadDirectory(resolveNpmTreeArtifact('tslib', 'tslib.js')),
 | |
|     '/node_modules/test-package': {
 | |
|       'package.json': '{"name": "test-package", "es2015": "./index.js", "typings": "./index.d.ts"}',
 | |
|       'index.js':
 | |
|           'import {AppModule} from "@angular/common"; export class MyApp extends AppModule;',
 | |
|       'index.d.s':
 | |
|           'import {AppModule} from "@angular/common"; export declare class MyApp extends AppModule;',
 | |
|     }
 | |
|   });
 | |
|   spyOn(Module, '_resolveFilename').and.callFake(mockResolve);
 | |
| }
 | |
| 
 | |
| function restoreRealFileSystem() {
 | |
|   mockFs.restore();
 | |
| }
 | |
| 
 | |
| 
 | |
| /** Load the built Angular packages into an in-memory structure. */
 | |
| function loadAngularPackages(): Directory {
 | |
|   const packagesDirectory: Directory = {};
 | |
| 
 | |
|   getAngularPackagesFromRunfiles().forEach(
 | |
|       ({name, pkgPath}) => { packagesDirectory[name] = loadDirectory(pkgPath); });
 | |
| 
 | |
|   return packagesDirectory;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Load real files from the filesystem into an "in-memory" structure,
 | |
|  * which can be used with `mock-fs`.
 | |
|  * @param directoryPath the path to the directory we want to load.
 | |
|  */
 | |
| function loadDirectory(directoryPath: string): Directory {
 | |
|   const directory: Directory = {};
 | |
| 
 | |
|   readdirSync(directoryPath).forEach(item => {
 | |
|     const itemPath = join(directoryPath, item);
 | |
|     if (statSync(itemPath).isDirectory()) {
 | |
|       directory[item] = loadDirectory(itemPath);
 | |
|     } else {
 | |
|       directory[item] = readFileSync(itemPath, 'utf-8');
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   return directory;
 | |
| }
 | |
| 
 | |
| interface Directory {
 | |
|   [pathSegment: string]: string|Directory;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * A mock implementation of the node.js Module._resolveFilename function,
 | |
|  * which we are spying on to support mocking out the file-system in these tests.
 | |
|  *
 | |
|  * @param request the path to a module that needs resolving.
 | |
|  */
 | |
| function mockResolve(request: string): string|null {
 | |
|   if (existsSync(request)) {
 | |
|     const stat = statSync(request);
 | |
|     if (stat.isFile()) {
 | |
|       return request;
 | |
|     } else if (stat.isDirectory()) {
 | |
|       const pIndex = mockResolve(request + '/index');
 | |
|       if (pIndex && existsSync(pIndex)) {
 | |
|         return pIndex;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   for (const ext of ['.js', '.d.ts']) {
 | |
|     if (existsSync(request + ext)) {
 | |
|       return request + ext;
 | |
|     }
 | |
|   }
 | |
|   if (request.indexOf('/node_modules') === 0) {
 | |
|     // We already tried adding node_modules so give up.
 | |
|     return null;
 | |
|   } else {
 | |
|     return mockResolve(join('/node_modules', request));
 | |
|   }
 | |
| }
 | |
| 
 | |
| function loadPackage(packageName: string): EntryPointPackageJson {
 | |
|   return JSON.parse(readFileSync(`/node_modules/${packageName}/package.json`, 'utf8'));
 | |
| }
 |