feat(ivy): ngcc - compile only specified package.json format properties (#29092)

You can now specify a list of properties in the package.json that
should be considered (in order) to find the path to the format to compile.

The build marker system has been updated to store the markers in
the package.json rather than an additional external file.
Also instead of tracking the underlying bundle format that was compiled,
it now tracks the package.json property.

BREAKING CHANGE:

The `proertiesToConsider` option replaces the previous `formats` option,
which specified the final bundle format, rather than the property in the
package.json.
If you were using this option to compile only specific bundle formats,
you must now modify your usage to pass in the properties in the package.json
that map to the format that you wish to compile.

In the CLI, the `--formats` is no longer available. Instead use the
`--properties` option.

FW-1120

PR Close #29092
This commit is contained in:
Pete Bacon Darwin 2019-03-20 13:47:58 +00:00 committed by Matias Niemelä
parent 4bb0259bc0
commit cd449021c1
11 changed files with 355 additions and 270 deletions

View File

@ -17,7 +17,7 @@
"build": "yarn ~~build", "build": "yarn ~~build",
"prebuild-local": "yarn setup-local", "prebuild-local": "yarn setup-local",
"build-local": "yarn ~~build", "build-local": "yarn ~~build",
"prebuild-with-ivy": "yarn setup-local && yarn ivy-ngcc --formats fesm2015 fesm5", "prebuild-with-ivy": "yarn setup-local && yarn ivy-ngcc",
"build-with-ivy": "node scripts/build-with-ivy", "build-with-ivy": "node scripts/build-with-ivy",
"extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js cafa558cf", "extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js cafa558cf",
"lint": "yarn check-env && yarn docs-lint && ng lint && yarn example-lint && yarn tools-lint", "lint": "yarn check-env && yarn docs-lint && ng lint && yarn example-lint && yarn tools-lint",

View File

@ -12,80 +12,36 @@ const EXAMPLES_BASE_PATH = path.resolve(__dirname, '../../content/examples');
const BOILERPLATE_PATHS = { const BOILERPLATE_PATHS = {
cli: [ cli: [
'src/environments/environment.prod.ts', 'src/environments/environment.prod.ts', 'src/environments/environment.ts',
'src/environments/environment.ts', 'src/assets/.gitkeep', 'src/browserslist', 'src/favicon.ico', 'src/karma.conf.js',
'src/assets/.gitkeep', 'src/polyfills.ts', 'src/test.ts', 'src/tsconfig.app.json', 'src/tsconfig.spec.json',
'src/browserslist', 'src/tslint.json', 'e2e/src/app.po.ts', 'e2e/protractor.conf.js', 'e2e/tsconfig.e2e.json',
'src/favicon.ico', '.editorconfig', 'angular.json', 'package.json', 'tsconfig.json', 'tslint.json'
'src/karma.conf.js',
'src/polyfills.ts',
'src/test.ts',
'src/tsconfig.app.json',
'src/tsconfig.spec.json',
'src/tslint.json',
'e2e/src/app.po.ts',
'e2e/protractor.conf.js',
'e2e/tsconfig.e2e.json',
'.editorconfig',
'angular.json',
'package.json',
'tsconfig.json',
'tslint.json'
], ],
systemjs: [ systemjs: [
'src/systemjs-angular-loader.js', 'src/systemjs-angular-loader.js', 'src/systemjs.config.js', 'src/tsconfig.json',
'src/systemjs.config.js', 'bs-config.json', 'bs-config.e2e.json', 'package.json', 'tslint.json'
'src/tsconfig.json',
'bs-config.json',
'bs-config.e2e.json',
'package.json',
'tslint.json'
], ],
common: [ common: ['src/styles.css']
'src/styles.css'
]
}; };
// All paths in this tool are relative to the current boilerplate folder, i.e boilerplate/i18n // All paths in this tool are relative to the current boilerplate folder, i.e boilerplate/i18n
// This maps the CLI files that exists in a parent folder // This maps the CLI files that exists in a parent folder
const cliRelativePath = BOILERPLATE_PATHS.cli.map(file => `../cli/${file}`); const cliRelativePath = BOILERPLATE_PATHS.cli.map(file => `../cli/${file}`);
BOILERPLATE_PATHS.elements = [ BOILERPLATE_PATHS.elements = [...cliRelativePath, 'tsconfig.json'];
...cliRelativePath,
'tsconfig.json'
];
BOILERPLATE_PATHS.i18n = [ BOILERPLATE_PATHS.i18n = [...cliRelativePath, 'angular.json', 'package.json'];
...cliRelativePath,
'angular.json',
'package.json'
];
BOILERPLATE_PATHS['service-worker'] = [ BOILERPLATE_PATHS['service-worker'] = [...cliRelativePath, 'angular.json', 'package.json'];
...cliRelativePath,
'angular.json',
'package.json'
];
BOILERPLATE_PATHS.testing = [ BOILERPLATE_PATHS.testing = [...cliRelativePath, 'angular.json'];
...cliRelativePath,
'angular.json'
];
BOILERPLATE_PATHS.universal = [ BOILERPLATE_PATHS.universal = [...cliRelativePath, 'angular.json', 'package.json'];
...cliRelativePath,
'angular.json',
'package.json'
];
BOILERPLATE_PATHS.ivy = { BOILERPLATE_PATHS.ivy = {
systemjs: [ systemjs: ['rollup-config.js', 'tsconfig-aot.json'],
'rollup-config.js', cli: ['src/tsconfig.app.json']
'tsconfig-aot.json'
],
cli: [
'src/tsconfig.app.json'
]
}; };
BOILERPLATE_PATHS.schematics = [ BOILERPLATE_PATHS.schematics = [
@ -101,10 +57,12 @@ class ExampleBoilerPlate {
*/ */
add(ivy = false) { add(ivy = false) {
// Get all the examples folders, indicated by those that contain a `example-config.json` file // Get all the examples folders, indicated by those that contain a `example-config.json` file
const exampleFolders = this.getFoldersContaining(EXAMPLES_BASE_PATH, EXAMPLE_CONFIG_FILENAME, 'node_modules'); const exampleFolders =
this.getFoldersContaining(EXAMPLES_BASE_PATH, EXAMPLE_CONFIG_FILENAME, 'node_modules');
if (!fs.existsSync(SHARED_NODE_MODULES_PATH)) { if (!fs.existsSync(SHARED_NODE_MODULES_PATH)) {
throw new Error(`The shared node_modules folder for the examples (${SHARED_NODE_MODULES_PATH}) is missing.\n` + throw new Error(
`The shared node_modules folder for the examples (${SHARED_NODE_MODULES_PATH}) is missing.\n` +
`Perhaps you need to run "yarn example-use-npm" or "yarn example-use-local" to install the dependencies?`); `Perhaps you need to run "yarn example-use-npm" or "yarn example-use-local" to install the dependencies?`);
} }
@ -114,7 +72,7 @@ class ExampleBoilerPlate {
// the module typings if we specified an "es2015" format. This means that // the module typings if we specified an "es2015" format. This means that
// we also need to build with "fesm2015" in order to get updated typings // we also need to build with "fesm2015" in order to get updated typings
// which are needed for compilation. // which are needed for compilation.
shelljs.exec(`yarn --cwd ${SHARED_PATH} ivy-ngcc --formats fesm2015 fesm5`); shelljs.exec(`yarn --cwd ${SHARED_PATH} ivy-ngcc`);
} }
exampleFolders.forEach(exampleFolder => { exampleFolders.forEach(exampleFolder => {
@ -128,16 +86,20 @@ class ExampleBoilerPlate {
const boilerPlateBasePath = path.resolve(BOILERPLATE_BASE_PATH, boilerPlateType); const boilerPlateBasePath = path.resolve(BOILERPLATE_BASE_PATH, boilerPlateType);
// Copy the boilerplate specific files // Copy the boilerplate specific files
BOILERPLATE_PATHS[boilerPlateType].forEach(filePath => this.copyFile(boilerPlateBasePath, exampleFolder, filePath)); BOILERPLATE_PATHS[boilerPlateType].forEach(
filePath => this.copyFile(boilerPlateBasePath, exampleFolder, filePath));
// Copy the boilerplate common files // Copy the boilerplate common files
BOILERPLATE_PATHS.common.forEach(filePath => this.copyFile(BOILERPLATE_COMMON_BASE_PATH, exampleFolder, filePath)); BOILERPLATE_PATHS.common.forEach(
filePath => this.copyFile(BOILERPLATE_COMMON_BASE_PATH, exampleFolder, filePath));
// Copy Ivy specific files // Copy Ivy specific files
if (ivy) { if (ivy) {
const ivyBoilerPlateType = boilerPlateType === 'systemjs' ? 'systemjs' : 'cli'; const ivyBoilerPlateType = boilerPlateType === 'systemjs' ? 'systemjs' : 'cli';
const ivyBoilerPlateBasePath = path.resolve(BOILERPLATE_BASE_PATH, 'ivy', ivyBoilerPlateType); const ivyBoilerPlateBasePath =
BOILERPLATE_PATHS.ivy[ivyBoilerPlateType].forEach(filePath => this.copyFile(ivyBoilerPlateBasePath, exampleFolder, filePath)); path.resolve(BOILERPLATE_BASE_PATH, 'ivy', ivyBoilerPlateType);
BOILERPLATE_PATHS.ivy[ivyBoilerPlateType].forEach(
filePath => this.copyFile(ivyBoilerPlateBasePath, exampleFolder, filePath));
} }
}); });
} }
@ -145,13 +107,10 @@ class ExampleBoilerPlate {
/** /**
* Remove all the boilerplate files from all the examples * Remove all the boilerplate files from all the examples
*/ */
remove() { remove() { shelljs.exec('git clean -xdfq', {cwd: EXAMPLES_BASE_PATH}); }
shelljs.exec('git clean -xdfq', { cwd: EXAMPLES_BASE_PATH });
}
main() { main() {
yargs yargs.usage('$0 <cmd> [args]')
.usage('$0 <cmd> [args]')
.command('add', 'add the boilerplate to each example', (yrgs) => this.add(yrgs.argv.ivy)) .command('add', 'add the boilerplate to each example', (yrgs) => this.add(yrgs.argv.ivy))
.command('remove', 'remove the boilerplate from each example', () => this.remove()) .command('remove', 'remove the boilerplate from each example', () => this.remove())
.demandCommand(1, 'Please supply a command from the list above') .demandCommand(1, 'Please supply a command from the list above')
@ -161,7 +120,7 @@ class ExampleBoilerPlate {
getFoldersContaining(basePath, filename, ignore) { getFoldersContaining(basePath, filename, ignore) {
const pattern = path.resolve(basePath, '**', filename); const pattern = path.resolve(basePath, '**', filename);
const ignorePattern = path.resolve(basePath, '**', ignore, '**'); const ignorePattern = path.resolve(basePath, '**', ignore, '**');
return glob.sync(pattern, { ignore: [ignorePattern] }).map(file => path.dirname(file)); return glob.sync(pattern, {ignore: [ignorePattern]}).map(file => path.dirname(file));
} }
copyFile(sourceFolder, destinationFolder, filePath) { copyFile(sourceFolder, destinationFolder, filePath) {
@ -171,13 +130,11 @@ class ExampleBoilerPlate {
filePath = this.normalizePath(filePath); filePath = this.normalizePath(filePath);
const destinationPath = path.resolve(destinationFolder, filePath); const destinationPath = path.resolve(destinationFolder, filePath);
fs.copySync(sourcePath, destinationPath, { overwrite: true }); fs.copySync(sourcePath, destinationPath, {overwrite: true});
fs.chmodSync(destinationPath, 444); fs.chmodSync(destinationPath, 444);
} }
loadJsonFile(filePath) { loadJsonFile(filePath) { return fs.readJsonSync(filePath, {throws: false}) || {}; }
return fs.readJsonSync(filePath, { throws: false }) || {};
}
normalizePath(filePath) { normalizePath(filePath) {
// transform for example ../cli/src/tsconfig.app.json to src/tsconfig.app.json // transform for example ../cli/src/tsconfig.app.json to src/tsconfig.app.json

View File

@ -10,11 +10,25 @@ ivy-ngcc --help
ivy-ngcc ivy-ngcc
# Did it add the appropriate build markers? # Did it add the appropriate build markers?
# - fesm2015
ls node_modules/@angular/common | grep __modified_by_ngcc_for_fesm2015
if [[ $? != 0 ]]; then exit 1; fi
# - esm2015 # - esm2015
ls node_modules/@angular/common | grep __modified_by_ngcc_for_esm2015 grep '"__modified_by_ngcc__":[^}]*"esm2015":"' node_modules/@angular/common/package.json
if [[ $? != 0 ]]; then exit 1; fi
# - fesm2015
grep '"__modified_by_ngcc__":[^}]*"fesm2015":"' node_modules/@angular/common/package.json
if [[ $? != 0 ]]; then exit 1; fi
grep '"__modified_by_ngcc__":[^}]*"es2015":"' node_modules/@angular/common/package.json
if [[ $? != 0 ]]; then exit 1; fi
# - esm5
grep '"__modified_by_ngcc__":[^}]*"esm5":"' node_modules/@angular/common/package.json
if [[ $? != 0 ]]; then exit 1; fi
# - fesm5
grep '"__modified_by_ngcc__":[^}]*"module":"' node_modules/@angular/common/package.json
if [[ $? != 0 ]]; then exit 1; fi
grep '"__modified_by_ngcc__":[^}]*"fesm5":"' node_modules/@angular/common/package.json
if [[ $? != 0 ]]; then exit 1; fi if [[ $? != 0 ]]; then exit 1; fi
# Did it replace the PRE_R3 markers correctly? # Did it replace the PRE_R3 markers correctly?
@ -48,6 +62,9 @@ ivy-ngcc
# Can it be safely run again (as a noop)? # Can it be safely run again (as a noop)?
ivy-ngcc ivy-ngcc
# Does running it with --formats fail?
ivy-ngcc --formats fesm2015 && exit 1
# Now try compiling the app using the ngcc compiled libraries # Now try compiling the app using the ngcc compiled libraries
ngc -p tsconfig-app.json ngc -p tsconfig-app.json

View File

@ -10,7 +10,7 @@ import * as path from 'canonical-path';
import * as yargs from 'yargs'; import * as yargs from 'yargs';
import {mainNgcc} from './src/main'; import {mainNgcc} from './src/main';
import {EntryPointFormat} from './src/packages/entry_point'; import {EntryPointJsonProperty} from './src/packages/entry_point';
// CLI entry point // CLI entry point
if (require.main === module) { if (require.main === module) {
@ -22,11 +22,15 @@ if (require.main === module) {
describe: 'A path to the root folder to compile.', describe: 'A path to the root folder to compile.',
default: './node_modules' default: './node_modules'
}) })
.option('f', { .option('f', {alias: 'formats', hidden: true, array: true})
alias: 'formats', .option('p', {
alias: 'properties',
array: true, array: true,
describe: 'An array of formats to compile.', describe:
default: ['fesm2015', 'esm2015', 'fesm5', 'esm5'] 'An array of names of properties in package.json (e.g. `module` or `es2015`)\n' +
'These properties should hold a path to a bundle-format to compile.\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.'
}) })
.option('t', { .option('t', {
alias: 'target', alias: 'target',
@ -36,11 +40,16 @@ if (require.main === module) {
.help() .help()
.parse(args); .parse(args);
if (options['f'] && options['f'].length) {
console.error(
'The formats option (-f/--formats) has been removed. Consider the properties option (-p/--properties) instead.');
process.exit(1);
}
const baseSourcePath: string = path.resolve(options['s']); const baseSourcePath: string = path.resolve(options['s']);
const formats: EntryPointFormat[] = options['f']; const propertiesToConsider: EntryPointJsonProperty[] = options['p'];
const baseTargetPath: string = options['t']; const baseTargetPath: string = options['t'];
try { try {
mainNgcc({baseSourcePath, formats, baseTargetPath}); mainNgcc({baseSourcePath, propertiesToConsider, baseTargetPath});
process.exitCode = 0; process.exitCode = 0;
} catch (e) { } catch (e) {
console.error(e.stack || e.message); console.error(e.stack || e.message);

View File

@ -6,10 +6,10 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {checkMarkerFile, writeMarkerFile} from './packages/build_marker'; import {checkMarker, writeMarker} from './packages/build_marker';
import {DependencyHost} from './packages/dependency_host'; import {DependencyHost} from './packages/dependency_host';
import {DependencyResolver} from './packages/dependency_resolver'; import {DependencyResolver} from './packages/dependency_resolver';
import {EntryPointFormat} from './packages/entry_point'; import {EntryPointFormat, EntryPointJsonProperty, getEntryPointFormat} from './packages/entry_point';
import {makeEntryPointBundle} from './packages/entry_point_bundle'; import {makeEntryPointBundle} from './packages/entry_point_bundle';
import {EntryPointFinder} from './packages/entry_point_finder'; import {EntryPointFinder} from './packages/entry_point_finder';
import {Transformer} from './packages/transformer'; import {Transformer} from './packages/transformer';
@ -20,8 +20,6 @@ import {Transformer} from './packages/transformer';
export interface NgccOptions { export interface NgccOptions {
/** The path to the node_modules folder that contains the packages to compile. */ /** The path to the node_modules folder that contains the packages to compile. */
baseSourcePath: string; baseSourcePath: string;
/** A list of JavaScript bundle formats that should be compiled. */
formats: EntryPointFormat[];
/** The path to the node_modules folder where modified files should be written. */ /** The path to the node_modules folder where modified files should be written. */
baseTargetPath?: string; baseTargetPath?: string;
/** /**
@ -29,8 +27,15 @@ export interface NgccOptions {
* All its dependencies will need to be compiled too. * All its dependencies will need to be compiled too.
*/ */
targetEntryPointPath?: string; targetEntryPointPath?: string;
/**
* Which entry-point properties in the package.json to consider when compiling.
* Each of properties contain a path to particular bundle format for a given entry-point.
*/
propertiesToConsider?: EntryPointJsonProperty[];
} }
const SUPPORTED_FORMATS: EntryPointFormat[] = ['esm5', 'esm2015', 'fesm5', 'fesm2015'];
/** /**
* This is the main entry-point into ngcc (aNGular Compatibility Compiler). * This is the main entry-point into ngcc (aNGular Compatibility Compiler).
* *
@ -39,8 +44,8 @@ export interface NgccOptions {
* *
* @param options The options telling ngcc what to compile and how. * @param options The options telling ngcc what to compile and how.
*/ */
export function mainNgcc({baseSourcePath, formats, baseTargetPath = baseSourcePath, export function mainNgcc({baseSourcePath, baseTargetPath = baseSourcePath, targetEntryPointPath,
targetEntryPointPath}: NgccOptions): void { propertiesToConsider}: NgccOptions): void {
const transformer = new Transformer(baseSourcePath, baseTargetPath); const transformer = new Transformer(baseSourcePath, baseTargetPath);
const host = new DependencyHost(); const host = new DependencyHost();
const resolver = new DependencyResolver(host); const resolver = new DependencyResolver(host);
@ -52,28 +57,51 @@ export function mainNgcc({baseSourcePath, formats, baseTargetPath = baseSourcePa
// Are we compiling the Angular core? // Are we compiling the Angular core?
const isCore = entryPoint.name === '@angular/core'; const isCore = entryPoint.name === '@angular/core';
// We transform the d.ts typings files while transforming one of the formats. let dtsTransformFormat: EntryPointFormat|undefined;
// This variable decides with which of the available formats to do this transform.
// It is marginally faster to process via the flat file if available.
const dtsTransformFormat: EntryPointFormat = entryPoint.fesm2015 ? 'fesm2015' : 'esm2015';
formats.forEach(format => { const propertiesToCompile =
if (checkMarkerFile(entryPoint, format)) { propertiesToConsider || Object.keys(entryPoint.packageJson) as EntryPointJsonProperty[];
console.warn(`Skipping ${entryPoint.name} : ${format} (already built).`); const compiledFormats = new Set<EntryPointFormat>();
return;
}
for (let i = 0; i < propertiesToCompile.length; i++) {
const property = propertiesToCompile[i];
const format = getEntryPointFormat(entryPoint.packageJson, property);
// No format then this property is not supposed to be compiled.
if (!format || SUPPORTED_FORMATS.indexOf(format) === -1) continue;
// We don't want to compile a format more than once.
// This could happen if there are multiple properties that map to the same format...
// E.g. `fesm5` and `module` both can point to the flat ESM5 format.
if (!compiledFormats.has(format)) {
compiledFormats.add(format);
// Use the first format found for typings transformation.
dtsTransformFormat = dtsTransformFormat || format;
if (checkMarker(entryPoint, property)) {
const bundle = const bundle =
makeEntryPointBundle(entryPoint, isCore, format, format === dtsTransformFormat); makeEntryPointBundle(entryPoint, isCore, format, format === dtsTransformFormat);
if (bundle === null) { if (bundle) {
transformer.transform(entryPoint, isCore, bundle);
} else {
console.warn( console.warn(
`Skipping ${entryPoint.name} : ${format} (no entry point file for this format).`); `Skipping ${entryPoint.name} : ${format} (no entry point file for this format).`);
}
} else { } else {
transformer.transform(entryPoint, isCore, bundle); console.warn(`Skipping ${entryPoint.name} : ${property} (already compiled).`);
}
}
// Write the built-with-ngcc marker.
writeMarker(entryPoint, property);
} }
// Write the built-with-ngcc marker if (!dtsTransformFormat) {
writeMarkerFile(entryPoint, format); throw new Error(
}); `Failed to compile any formats for entry-point at (${entryPoint.path}). Tried ${propertiesToCompile}.`);
}
}); });
} }
export {NGCC_VERSION} from './packages/build_marker';

View File

@ -7,35 +7,42 @@
*/ */
import {resolve} from 'canonical-path'; import {resolve} from 'canonical-path';
import {existsSync, readFileSync, writeFileSync} from 'fs'; import {writeFileSync} from 'fs';
import {EntryPoint, EntryPointFormat} from './entry_point';
import {EntryPoint, EntryPointJsonProperty} from './entry_point';
export const NGCC_VERSION = '0.0.0-PLACEHOLDER'; export const NGCC_VERSION = '0.0.0-PLACEHOLDER';
function getMarkerPath(entryPointPath: string, format: EntryPointFormat) {
return resolve(entryPointPath, `__modified_by_ngcc_for_${format}__`);
}
/** /**
* Check whether there is a build marker for the given entry point and format. * Check whether there is a build marker for the given entry-point and format property.
* @param entryPoint the entry point to check for a marker. * @param entryPoint the entry-point to check for a marker.
* @param format the format for which we are checking for a marker. * @param format the property in the package.json of the format for which we are checking for a
* marker.
* @returns true if the entry-point and format have already been built with this ngcc version.
* @throws Error if the entry-point and format have already been built with a different ngcc
* version.
*/ */
export function checkMarkerFile(entryPoint: EntryPoint, format: EntryPointFormat): boolean { export function checkMarker(entryPoint: EntryPoint, format: EntryPointJsonProperty): boolean {
const markerPath = getMarkerPath(entryPoint.path, format); const pkg = entryPoint.packageJson;
const markerExists = existsSync(markerPath); const compiledVersion = pkg.__modified_by_ngcc__ && pkg.__modified_by_ngcc__[format];
if (markerExists) { if (compiledVersion && compiledVersion !== NGCC_VERSION) {
const previousVersion = readFileSync(markerPath, 'utf8');
if (previousVersion !== NGCC_VERSION) {
throw new Error( throw new Error(
'The ngcc compiler has changed since the last ngcc build.\n' + 'The ngcc compiler has changed since the last ngcc build.\n' +
'Please completely remove `node_modules` and try again.'); 'Please completely remove `node_modules` and try again.');
} }
} return !!compiledVersion;
return markerExists;
} }
export function writeMarkerFile(entryPoint: EntryPoint, format: EntryPointFormat) { /**
const markerPath = getMarkerPath(entryPoint.path, format); * Write a build marker for the given entry-point and format property, to indicate that it has
writeFileSync(markerPath, NGCC_VERSION, 'utf8'); * been compiled by this version of ngcc.
*
* @param entryPoint the entry-point to write a marker.
* @param format the property in the package.json of the format for which we are writing the marker.
*/
export function writeMarker(entryPoint: EntryPoint, format: EntryPointJsonProperty) {
const pkg = entryPoint.packageJson;
if (!pkg.__modified_by_ngcc__) pkg.__modified_by_ngcc__ = {};
pkg.__modified_by_ngcc__[format] = NGCC_VERSION;
writeFileSync(resolve(entryPoint.path, 'package.json'), JSON.stringify(pkg), 'utf8');
} }

View File

@ -8,6 +8,7 @@
import * as path from 'canonical-path'; import * as path from 'canonical-path';
import * as fs from 'fs'; import * as fs from 'fs';
import {isDefined} from '../utils';
/** /**
@ -33,6 +34,8 @@ export type EntryPointFormat = keyof(EntryPointPaths);
export interface EntryPoint extends EntryPointPaths { export interface EntryPoint extends EntryPointPaths {
/** The name of the package (e.g. `@angular/core`). */ /** The name of the package (e.g. `@angular/core`). */
name: string; name: string;
/** The parsed package.json file for this entry-point. */
packageJson: EntryPointPackageJson;
/** The path to the package that contains this entry-point. */ /** The path to the package that contains this entry-point. */
package: string; package: string;
/** The path to this entry point. */ /** The path to this entry point. */
@ -41,11 +44,7 @@ export interface EntryPoint extends EntryPointPaths {
typings: string; typings: string;
} }
/** interface PackageJsonFormatProperties {
* The properties that may be loaded from the `package.json` file.
*/
interface EntryPointPackageJson {
name: string;
fesm2015?: string; fesm2015?: string;
fesm5?: string; fesm5?: string;
es2015?: string; // if exists then it is actually FESM2015 es2015?: string; // if exists then it is actually FESM2015
@ -57,6 +56,103 @@ interface EntryPointPackageJson {
typings?: string; // TypeScript .d.ts files typings?: string; // TypeScript .d.ts files
} }
/**
* The properties that may be loaded from the `package.json` file.
*/
export interface EntryPointPackageJson extends PackageJsonFormatProperties {
name: string;
__modified_by_ngcc__?: {[key: string]: string};
}
export type EntryPointJsonProperty = keyof(PackageJsonFormatProperties);
/**
* Try to create an entry-point from the given paths and properties.
*
* @param packagePath the absolute path to the containing npm package
* @param entryPointPath the absolute path to the potential entry-point.
* @returns An entry-point if it is valid, `null` otherwise.
*/
export function getEntryPointInfo(packagePath: string, entryPointPath: string): EntryPoint|null {
const packageJsonPath = path.resolve(entryPointPath, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
return null;
}
const entryPointPackageJson = loadEntryPointPackage(packageJsonPath);
if (!entryPointPackageJson) {
return null;
}
// We must have a typings property
const typings = entryPointPackageJson.typings || entryPointPackageJson.types;
if (!typings) {
return null;
}
// Also there must exist a `metadata.json` file next to the typings entry-point.
const metadataPath =
path.resolve(entryPointPath, typings.replace(/\.d\.ts$/, '') + '.metadata.json');
if (!fs.existsSync(metadataPath)) {
return null;
}
const formats = Object.keys(entryPointPackageJson)
.map((property: EntryPointJsonProperty) => {
const format = getEntryPointFormat(entryPointPackageJson, property);
return format ? {property, format} : undefined;
})
.filter(isDefined);
const entryPointInfo: EntryPoint = {
name: entryPointPackageJson.name,
packageJson: entryPointPackageJson,
package: packagePath,
path: entryPointPath,
typings: path.resolve(entryPointPath, typings)
};
// Add the formats to the entry-point info object.
formats.forEach(
item => entryPointInfo[item.format] =
path.resolve(entryPointPath, entryPointPackageJson[item.property] !));
return entryPointInfo;
}
/**
* Convert a package.json property into an entry-point format.
*
* The actual format is dependent not only on the property itself but also
* on what other properties exist in the package.json.
*
* @param entryPointProperties The package.json that contains the properties.
* @param property The property to convert to a format.
* @returns An entry-point format or `undefined` if none match the given property.
*/
export function getEntryPointFormat(
entryPointProperties: EntryPointPackageJson, property: string): EntryPointFormat|undefined {
switch (property) {
case 'fesm2015':
return 'fesm2015';
case 'fesm5':
return 'fesm5';
case 'es2015':
return !entryPointProperties.fesm2015 ? 'fesm2015' : 'esm2015';
case 'esm2015':
return 'esm2015';
case 'esm5':
return 'esm5';
case 'main':
return 'umd';
case 'module':
return !entryPointProperties.fesm5 ? 'fesm5' : 'esm5';
default:
return undefined;
}
}
/** /**
* Parses the JSON from a package.json file. * Parses the JSON from a package.json file.
* @param packageJsonPath the absolute path to the package.json file. * @param packageJsonPath the absolute path to the package.json file.
@ -71,72 +167,3 @@ function loadEntryPointPackage(packageJsonPath: string): EntryPointPackageJson|n
return null; return null;
} }
} }
/**
* Try to get an entry point from the given path.
* @param packagePath the absolute path to the containing npm package
* @param entryPointPath the absolute path to the potential entry point.
* @returns Info about the entry point if it is valid, `null` otherwise.
*/
export function getEntryPointInfo(packagePath: string, entryPointPath: string): EntryPoint|null {
const packageJsonPath = path.resolve(entryPointPath, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
return null;
}
const entryPointPackageJson = loadEntryPointPackage(packageJsonPath);
if (!entryPointPackageJson) {
return null;
}
// If there is `esm2015` then `es2015` will be FESM2015, otherwise ESM2015.
// If there is `esm5` then `module` will be FESM5, otherwise it will be ESM5.
const {
name,
module: modulePath,
types,
typings = types, // synonymous
es2015,
fesm2015 = es2015, // synonymous
fesm5 = modulePath, // synonymous
esm2015,
esm5,
main
} = entryPointPackageJson;
// Minimum requirement is that we have typings and one of esm2015 or fesm2015 formats.
if (!typings || !(fesm2015 || esm2015)) {
return null;
}
// Also there must exist a `metadata.json` file next to the typings entry-point.
const metadataPath =
path.resolve(entryPointPath, typings.replace(/\.d\.ts$/, '') + '.metadata.json');
if (!fs.existsSync(metadataPath)) {
return null;
}
const entryPointInfo: EntryPoint = {
name,
package: packagePath,
path: entryPointPath,
typings: path.resolve(entryPointPath, typings),
};
if (esm2015) {
entryPointInfo.esm2015 = path.resolve(entryPointPath, esm2015);
}
if (fesm2015) {
entryPointInfo.fesm2015 = path.resolve(entryPointPath, fesm2015);
}
if (fesm5) {
entryPointInfo.fesm5 = path.resolve(entryPointPath, fesm5);
}
if (esm5) {
entryPointInfo.esm5 = path.resolve(entryPointPath, esm5);
}
if (main) {
entryPointInfo.umd = path.resolve(entryPointPath, main);
}
return entryPointInfo;
}

View File

@ -19,38 +19,74 @@ describe('ngcc main()', () => {
afterEach(restoreRealFileSystem); afterEach(restoreRealFileSystem);
it('should run ngcc without errors for fesm2015', () => { it('should run ngcc without errors for fesm2015', () => {
expect(() => mainNgcc({baseSourcePath: '/node_modules', formats: ['fesm2015']})).not.toThrow(); expect(() => mainNgcc({baseSourcePath: '/node_modules', propertiesToConsider: ['fesm2015']}))
.not.toThrow();
}); });
it('should run ngcc without errors for fesm5', () => { it('should run ngcc without errors for fesm5', () => {
expect(() => mainNgcc({baseSourcePath: '/node_modules', formats: ['fesm5']})).not.toThrow(); expect(() => mainNgcc({baseSourcePath: '/node_modules', propertiesToConsider: ['fesm5']}))
.not.toThrow();
}); });
it('should run ngcc without errors for esm2015', () => { it('should run ngcc without errors for esm2015', () => {
expect(() => mainNgcc({baseSourcePath: '/node_modules', formats: ['esm2015']})).not.toThrow(); expect(() => mainNgcc({baseSourcePath: '/node_modules', propertiesToConsider: ['esm2015']}))
.not.toThrow();
}); });
it('should run ngcc without errors for esm5', () => { it('should run ngcc without errors for esm5', () => {
expect(() => mainNgcc({baseSourcePath: '/node_modules', formats: ['esm5']})).not.toThrow(); expect(() => mainNgcc({baseSourcePath: '/node_modules', propertiesToConsider: ['esm5']}))
.not.toThrow();
}); });
it('should only compile the given package entry-point (and its dependencies)', () => { it('should only compile the given package entry-point (and its dependencies)', () => {
mainNgcc({ mainNgcc({baseSourcePath: '/node_modules', targetEntryPointPath: '@angular/common/http'});
baseSourcePath: '/node_modules',
formats: ['esm2015'],
targetEntryPointPath: '@angular/common'
});
expect(loadPackage('@angular/common').__modified_by_ngcc__).toEqual({ expect(loadPackage('@angular/common/http').__modified_by_ngcc__).toEqual({
esm2015: '0.0.0-PLACEHOLDER', module: '0.0.0-PLACEHOLDER',
es2015: '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'
});
expect(loadPackage('@angular/common').__modified_by_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'
}); });
expect(loadPackage('@angular/core').__modified_by_ngcc__).toEqual({ expect(loadPackage('@angular/core').__modified_by_ngcc__).toEqual({
esm2015: '0.0.0-PLACEHOLDER', module: '0.0.0-PLACEHOLDER',
es2015: '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'
}); });
expect(loadPackage('@angular/common/testing').__modified_by_ngcc__).toBeUndefined(); expect(loadPackage('@angular/common/testing').__modified_by_ngcc__).toBeUndefined();
expect(loadPackage('@angular/common/http').__modified_by_ngcc__).toBeUndefined(); });
it('should only build the format properties specified for each entry-point', () => {
mainNgcc({baseSourcePath: '/node_modules', propertiesToConsider: ['main', 'esm5', 'module']});
expect(loadPackage('@angular/core').__modified_by_ngcc__).toEqual({
esm5: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('@angular/common').__modified_by_ngcc__).toEqual({
esm5: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('@angular/common/testing').__modified_by_ngcc__).toEqual({
esm5: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('@angular/common/http').__modified_by_ngcc__).toEqual({
esm5: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
});
}); });
}); });

View File

@ -6,10 +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 {readFileSync, writeFileSync} from 'fs';
import * as mockFs from 'mock-fs'; import * as mockFs from 'mock-fs';
import {checkMarkerFile, writeMarkerFile} from '../../src/packages/build_marker'; import {checkMarker, writeMarker} from '../../src/packages/build_marker';
import {EntryPoint} from '../../src/packages/entry_point'; import {EntryPoint} from '../../src/packages/entry_point';
function createMockFileSystem() { function createMockFileSystem() {
@ -94,58 +94,55 @@ function restoreRealFileSystem() {
} }
function createEntryPoint(path: string): EntryPoint { function createEntryPoint(path: string): EntryPoint {
return {name: 'some-package', path, package: '', typings: ''}; return {
name: 'some-package',
path,
package: path,
typings: '',
packageJson: JSON.parse(readFileSync(path + '/package.json', 'utf8'))
};
} }
describe('Marker files', () => { describe('Marker files', () => {
beforeEach(createMockFileSystem); beforeEach(createMockFileSystem);
afterEach(restoreRealFileSystem); afterEach(restoreRealFileSystem);
describe('writeMarkerFile', () => { describe('writeMarker', () => {
it('should write a file containing the version placeholder', () => { it('should write a property in the package.json containing the version placeholder', () => {
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__')) let pkg = JSON.parse(readFileSync('/node_modules/@angular/common/package.json', 'utf8'));
.toBe(false); expect(pkg.__modified_by_ngcc__).toBeUndefined();
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__')).toBe(false); expect(pkg.__modified_by_ngcc__).toBeUndefined();
writeMarkerFile(createEntryPoint('/node_modules/@angular/common'), 'fesm2015'); writeMarker(createEntryPoint('/node_modules/@angular/common'), 'fesm2015');
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__')) pkg = JSON.parse(readFileSync('/node_modules/@angular/common/package.json', 'utf8'));
.toBe(true); expect(pkg.__modified_by_ngcc__.fesm2015).toEqual('0.0.0-PLACEHOLDER');
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__')).toBe(false); expect(pkg.__modified_by_ngcc__.esm5).toBeUndefined();
expect(
readFileSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', 'utf8'))
.toEqual('0.0.0-PLACEHOLDER');
writeMarkerFile(createEntryPoint('/node_modules/@angular/common'), 'esm5'); writeMarker(createEntryPoint('/node_modules/@angular/common'), 'esm5');
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__')) pkg = JSON.parse(readFileSync('/node_modules/@angular/common/package.json', 'utf8'));
.toBe(true); expect(pkg.__modified_by_ngcc__.fesm2015).toEqual('0.0.0-PLACEHOLDER');
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__')).toBe(true); expect(pkg.__modified_by_ngcc__.esm5).toEqual('0.0.0-PLACEHOLDER');
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', () => { describe('checkMarker', () => {
it('should return false if the marker file does not exist', () => { it('should return false if the marker property does not exist', () => {
expect(checkMarkerFile(createEntryPoint('/node_modules/@angular/common'), 'fesm2015')) expect(checkMarker(createEntryPoint('/node_modules/@angular/common'), 'fesm2015'))
.toBe(false); .toBe(false);
}); });
it('should return true if the marker file exists and contains the correct version', () => { it('should return true if the marker property exists and contains the correct version', () => {
writeFileSync( const pkg = JSON.parse(readFileSync('/node_modules/@angular/common/package.json', 'utf8'));
'/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', '0.0.0-PLACEHOLDER', pkg.__modified_by_ngcc__ = {fesm2015: '0.0.0-PLACEHOLDER'};
'utf8'); writeFileSync('/node_modules/@angular/common/package.json', JSON.stringify(pkg), 'utf8');
expect(checkMarkerFile(createEntryPoint('/node_modules/@angular/common'), 'fesm2015')) expect(checkMarker(createEntryPoint('/node_modules/@angular/common'), 'fesm2015')).toBe(true);
.toBe(true);
}); });
it('should throw if the marker file exists but contains the wrong version', () => { it('should throw if the marker property exists but contains the wrong version', () => {
writeFileSync( const pkg = JSON.parse(readFileSync('/node_modules/@angular/common/package.json', 'utf8'));
'/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', 'WRONG_VERSION', pkg.__modified_by_ngcc__ = {fesm2015: 'WRONG_VERSION'};
'utf8'); writeFileSync('/node_modules/@angular/common/package.json', JSON.stringify(pkg), 'utf8');
expect(() => checkMarkerFile(createEntryPoint('/node_modules/@angular/common'), 'fesm2015')) expect(() => checkMarker(createEntryPoint('/node_modules/@angular/common'), 'fesm2015'))
.toThrowError( .toThrowError(
'The ngcc compiler has changed since the last ngcc build.\n' + 'The ngcc compiler has changed since the last ngcc build.\n' +
'Please completely remove `node_modules` and try again.'); 'Please completely remove `node_modules` and try again.');

View File

@ -6,10 +6,10 @@
* 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 * as mockFs from 'mock-fs'; import * as mockFs from 'mock-fs';
import {getEntryPointInfo} from '../../src/packages/entry_point'; import {getEntryPointInfo} from '../../src/packages/entry_point';
describe('getEntryPointInfo()', () => { describe('getEntryPointInfo()', () => {
beforeEach(createMockFileSystem); beforeEach(createMockFileSystem);
afterEach(restoreRealFileSystem); afterEach(restoreRealFileSystem);
@ -27,6 +27,7 @@ describe('getEntryPointInfo()', () => {
fesm5: `/some_package/valid_entry_point/fesm2015/valid_entry_point.js`, fesm5: `/some_package/valid_entry_point/fesm2015/valid_entry_point.js`,
esm5: `/some_package/valid_entry_point/esm2015/valid_entry_point.js`, esm5: `/some_package/valid_entry_point/esm2015/valid_entry_point.js`,
umd: `/some_package/valid_entry_point/bundles/valid_entry_point.umd.js`, umd: `/some_package/valid_entry_point/bundles/valid_entry_point.umd.js`,
packageJson: loadPackageJson('/some_package/valid_entry_point'),
}); });
}); });
@ -63,6 +64,7 @@ describe('getEntryPointInfo()', () => {
fesm5: `/some_package/types_rather_than_typings/fesm2015/types_rather_than_typings.js`, fesm5: `/some_package/types_rather_than_typings/fesm2015/types_rather_than_typings.js`,
esm5: `/some_package/types_rather_than_typings/esm2015/types_rather_than_typings.js`, esm5: `/some_package/types_rather_than_typings/esm2015/types_rather_than_typings.js`,
umd: `/some_package/types_rather_than_typings/bundles/types_rather_than_typings.umd.js`, umd: `/some_package/types_rather_than_typings/bundles/types_rather_than_typings.umd.js`,
packageJson: loadPackageJson('/some_package/types_rather_than_typings'),
}); });
}); });
@ -76,6 +78,7 @@ describe('getEntryPointInfo()', () => {
fesm2015: `/some_package/material_style/esm2015/material_style.js`, fesm2015: `/some_package/material_style/esm2015/material_style.js`,
fesm5: `/some_package/material_style/esm5/material_style.es5.js`, fesm5: `/some_package/material_style/esm5/material_style.es5.js`,
umd: `/some_package/material_style/bundles/material_style.umd.js`, umd: `/some_package/material_style/bundles/material_style.umd.js`,
packageJson: loadPackageJson('/some_package/material_style'),
}); });
}); });
@ -154,3 +157,7 @@ function createPackageJson(
} }
return JSON.stringify(packageJson); return JSON.stringify(packageJson);
} }
export function loadPackageJson(packagePath: string) {
return JSON.parse(readFileSync(packagePath + '/package.json', 'utf8'));
}