feat(ivy): implement ngcc build marker (#25557)

`ngcc` adds marker files to each folder that has been
compiled, containing the version of the ngcc used.

When compiling, it will ignore folders that contain these
marker files, as long as the version matches.

PR Close #25557
This commit is contained in:
Pete Bacon Darwin 2018-08-17 22:00:00 +01:00 committed by Misko Hevery
parent 2a672a97ab
commit b0cb134815
4 changed files with 123 additions and 18 deletions

View File

@ -5,7 +5,14 @@ set -e -x
PATH=$PATH:$(npm bin) PATH=$PATH:$(npm bin)
ivy-ngcc fesm2015,esm2015 ivy-ngcc fesm2015,esm2015
# Did it add the appropriate build markers?
ls node_modules/@angular/common | grep __modified_by_ngcc_for_fesm2015
if [[ $? != 0 ]]; then exit 1; fi
ls node_modules/@angular/common | grep __modified_by_ngcc_for_esm2015
if [[ $? != 0 ]]; then exit 1; fi
ngc -p tsconfig-app.json ngc -p tsconfig-app.json
# Look for correct output # Did it compile the main.ts correctly?
grep "directives: \[\S*\.NgIf\]" dist/src/main.js > /dev/null grep "directives: \[\S*\.NgIf\]" dist/src/main.js
if [[ $? != 0 ]]; then exit 1; fi

View File

@ -25,9 +25,7 @@ import {Esm2015Renderer} from '../rendering/esm2015_renderer';
import {Esm5Renderer} from '../rendering/esm5_renderer'; import {Esm5Renderer} from '../rendering/esm5_renderer';
import {FileInfo, Renderer} from '../rendering/renderer'; import {FileInfo, Renderer} from '../rendering/renderer';
import {getEntryPoints} from './utils'; import {checkMarkerFile, findAllPackageJsonFiles, getEntryPoints, writeMarkerFile} from './utils';
/** /**
* A Package is stored in a directory on disk and that directory can contain one or more package * A Package is stored in a directory on disk and that directory can contain one or more package
@ -52,7 +50,11 @@ export class PackageTransformer {
transform(packagePath: string, format: string, targetPath: string = 'node_modules'): void { transform(packagePath: string, format: string, targetPath: string = 'node_modules'): void {
const sourceNodeModules = this.findNodeModulesPath(packagePath); const sourceNodeModules = this.findNodeModulesPath(packagePath);
const targetNodeModules = resolve(sourceNodeModules, '..', targetPath); const targetNodeModules = resolve(sourceNodeModules, '..', targetPath);
const entryPoints = getEntryPoints(packagePath, format); const packageJsonPaths =
findAllPackageJsonFiles(packagePath)
// Ignore paths that have been built already
.filter(packageJsonPath => !checkMarkerFile(packageJsonPath, format));
const entryPoints = getEntryPoints(packageJsonPaths, format);
entryPoints.forEach(entryPoint => { entryPoints.forEach(entryPoint => {
const outputFiles: FileInfo[] = []; const outputFiles: FileInfo[] = [];
@ -102,6 +104,9 @@ export class PackageTransformer {
// Write out all the transformed files. // Write out all the transformed files.
outputFiles.forEach(file => this.writeFile(file)); outputFiles.forEach(file => this.writeFile(file));
}); });
// Write the built-with-ngcc markers
packageJsonPaths.forEach(packageJsonPath => { writeMarkerFile(packageJsonPath, format); });
} }
getHost(format: string, program: ts.Program, dtsMapper: DtsMapper): NgccReflectionHost { getHost(format: string, program: ts.Program, dtsMapper: DtsMapper): NgccReflectionHost {

View File

@ -6,12 +6,14 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {readFileSync} from 'fs'; import {existsSync, readFileSync, writeFileSync} from 'fs';
import {dirname, relative, resolve} from 'path'; import {dirname, resolve} from 'path';
import {find} from 'shelljs'; import {find} from 'shelljs';
import {isDefined} from '../utils'; import {isDefined} from '../utils';
export const NGCC_VERSION = '0.0.0-PLACEHOLDER';
/** /**
* Represents an entry point to a package or sub-package. * Represents an entry point to a package or sub-package.
* *
@ -29,7 +31,7 @@ export class EntryPoint {
* @param relativeEntryPath The relative path to the entry point file. * @param relativeEntryPath The relative path to the entry point file.
* @param relativeDtsEntryPath The relative path to the `.d.ts` entry point file. * @param relativeDtsEntryPath The relative path to the `.d.ts` entry point file.
*/ */
constructor(packageRoot: string, relativeEntryPath: string, relativeDtsEntryPath: string) { constructor(public packageRoot: string, relativeEntryPath: string, relativeDtsEntryPath: string) {
this.entryFileName = resolve(packageRoot, relativeEntryPath); this.entryFileName = resolve(packageRoot, relativeEntryPath);
this.entryRoot = dirname(this.entryFileName); this.entryRoot = dirname(this.entryFileName);
const dtsEntryFileName = resolve(packageRoot, relativeDtsEntryPath); const dtsEntryFileName = resolve(packageRoot, relativeDtsEntryPath);
@ -63,15 +65,14 @@ export function findAllPackageJsonFiles(rootDirectory: string): string[] {
} }
/** /**
* Identify the entry points of a package. * Identify the entry points of a collection of package.json files.
* *
* @param packageDirectory The absolute path to the root directory that contains the package. * @param packageJsonPaths A collection of absolute paths to the package.json files.
* @param format The format of the entry points to look for within the package. * @param format The format of the entry points to look for within the package.
* *
* @returns A collection of `EntryPoint`s that correspond to entry points for the package. * @returns A collection of `EntryPoint`s that correspond to entry points for the package.
*/ */
export function getEntryPoints(packageDirectory: string, format: string): EntryPoint[] { export function getEntryPoints(packageJsonPaths: string[], format: string): EntryPoint[] {
const packageJsonPaths = findAllPackageJsonFiles(packageDirectory);
const entryPoints = const entryPoints =
packageJsonPaths packageJsonPaths
.map(packageJsonPath => { .map(packageJsonPath => {
@ -86,3 +87,26 @@ export function getEntryPoints(packageDirectory: string, format: string): EntryP
.filter(isDefined); .filter(isDefined);
return entryPoints; return entryPoints;
} }
function getMarkerPath(packageJsonPath: string, format: string) {
return resolve(dirname(packageJsonPath), `__modified_by_ngcc_for_${format}__`);
}
export function checkMarkerFile(packageJsonPath: string, format: string) {
const markerPath = getMarkerPath(packageJsonPath, format);
const markerExists = existsSync(markerPath);
if (markerExists) {
const previousVersion = readFileSync(markerPath, 'utf8');
if (previousVersion !== NGCC_VERSION) {
throw new Error(
'The ngcc compiler has changed since the last ngcc build.\n' +
'Please completely remove `node_modules` and try again.');
}
}
return markerExists;
}
export function writeMarkerFile(packageJsonPath: string, format: string) {
const markerPath = getMarkerPath(packageJsonPath, format);
writeFileSync(markerPath, NGCC_VERSION, 'utf8');
}

View File

@ -6,8 +6,10 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {existsSync, readFileSync, writeFileSync} from 'fs';
import * as mockFs from 'mock-fs'; import * as mockFs from 'mock-fs';
import {EntryPoint, findAllPackageJsonFiles, getEntryPoints} from '../../src/transform/utils';
import {EntryPoint, checkMarkerFile, findAllPackageJsonFiles, getEntryPoints, writeMarkerFile} from '../../src/transform/utils';
function createMockFileSystem() { function createMockFileSystem() {
mockFs({ mockFs({
@ -95,6 +97,11 @@ describe('EntryPoint', () => {
const entryPoint = new EntryPoint('/foo/bar', '../baz/qux/../quux.js', '/typings/foo/bar.d.ts'); const entryPoint = new EntryPoint('/foo/bar', '../baz/qux/../quux.js', '/typings/foo/bar.d.ts');
expect(entryPoint.entryFileName).toBe('/foo/baz/quux.js'); expect(entryPoint.entryFileName).toBe('/foo/baz/quux.js');
}); });
it('should expose the package root for the entry point file', () => {
const entryPoint = new EntryPoint('/foo/bar', '../baz/qux/../quux.js', '/typings/foo/bar.d.ts');
expect(entryPoint.packageRoot).toBe('/foo/bar');
});
}); });
describe('findAllPackageJsonFiles()', () => { describe('findAllPackageJsonFiles()', () => {
@ -138,7 +145,14 @@ describe('getEntryPoints()', () => {
afterEach(restoreRealFileSystem); afterEach(restoreRealFileSystem);
it('should return the entry points for the specified format from each `package.json`', () => { it('should return the entry points for the specified format from each `package.json`', () => {
const entryPoints = getEntryPoints('/node_modules/@angular/common', 'fesm2015'); const entryPoints = getEntryPoints(
[
'/node_modules/@angular/common/package.json',
'/node_modules/@angular/common/http/package.json',
'/node_modules/@angular/common/http/testing/package.json',
'/node_modules/@angular/common/testing/package.json'
],
'fesm2015');
entryPoints.forEach(ep => expect(ep).toEqual(jasmine.any(EntryPoint))); entryPoints.forEach(ep => expect(ep).toEqual(jasmine.any(EntryPoint)));
const sortedPaths = entryPoints.map(x => x.entryFileName).sort(); const sortedPaths = entryPoints.map(x => x.entryFileName).sort();
@ -151,17 +165,18 @@ describe('getEntryPoints()', () => {
}); });
it('should return an empty array if there are no matching `package.json` files', () => { it('should return an empty array if there are no matching `package.json` files', () => {
const entryPoints = getEntryPoints('/node_modules/@angular/other', 'fesm2015'); const entryPoints = getEntryPoints([], 'fesm2015');
expect(entryPoints).toEqual([]); expect(entryPoints).toEqual([]);
}); });
it('should return an empty array if there are no matching formats', () => { it('should return an empty array if there are no matching formats', () => {
const entryPoints = getEntryPoints('/node_modules/@angular/common', 'fesm3000'); const entryPoints = getEntryPoints(['/node_modules/@angular/common/package.json'], 'fesm3000');
expect(entryPoints).toEqual([]); expect(entryPoints).toEqual([]);
}); });
it('should return an entry point even if the typings are not specified', () => { it('should return an entry point even if the typings are not specified', () => {
const entryPoints = getEntryPoints('/node_modules/@angular/no-typings', 'fesm2015'); const entryPoints =
getEntryPoints(['/node_modules/@angular/no-typings/package.json'], 'fesm2015');
expect(entryPoints.length).toEqual(1); expect(entryPoints.length).toEqual(1);
expect(entryPoints[0].entryFileName) expect(entryPoints[0].entryFileName)
.toEqual('/node_modules/@angular/no-typings/fesm2015/index.js'); .toEqual('/node_modules/@angular/no-typings/fesm2015/index.js');
@ -169,3 +184,57 @@ describe('getEntryPoints()', () => {
expect(entryPoints[0].dtsEntryRoot).toEqual(entryPoints[0].entryRoot); expect(entryPoints[0].dtsEntryRoot).toEqual(entryPoints[0].entryRoot);
}); });
}); });
describe('Marker files', () => {
beforeEach(createMockFileSystem);
afterEach(restoreRealFileSystem);
describe('writeMarkerFile', () => {
it('should write a file containing the version placeholder', () => {
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__'))
.toBe(false);
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__')).toBe(false);
writeMarkerFile('/node_modules/@angular/common/package.json', 'fesm2015');
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__'))
.toBe(true);
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__')).toBe(false);
expect(
readFileSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', 'utf8'))
.toEqual('0.0.0-PLACEHOLDER');
writeMarkerFile('/node_modules/@angular/common/package.json', 'esm5');
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__'))
.toBe(true);
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__')).toBe(true);
expect(
readFileSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', 'utf8'))
.toEqual('0.0.0-PLACEHOLDER');
expect(readFileSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__', 'utf8'))
.toEqual('0.0.0-PLACEHOLDER');
});
});
describe('checkMarkerFile', () => {
it('should return false if the marker file does not exist', () => {
expect(checkMarkerFile('/node_modules/@angular/common/package.json', 'fesm2015')).toBe(false);
});
it('should return true if the marker file exists and contains the correct version', () => {
writeFileSync(
'/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', '0.0.0-PLACEHOLDER',
'utf8');
expect(checkMarkerFile('/node_modules/@angular/common/package.json', 'fesm2015')).toBe(true);
});
it('should throw if the marker file exists but contains the wrong version', () => {
writeFileSync(
'/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', 'WRONG_VERSION',
'utf8');
expect(() => checkMarkerFile('/node_modules/@angular/common/package.json', 'fesm2015'))
.toThrowError(
'The ngcc compiler has changed since the last ngcc build.\n' +
'Please completely remove `node_modules` and try again.');
});
});
});