diff --git a/aio/package.json b/aio/package.json index 2a6acf570e..7867e3b51f 100644 --- a/aio/package.json +++ b/aio/package.json @@ -17,7 +17,7 @@ "build": "yarn ~~build", "prebuild-local": "yarn setup-local", "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", "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", diff --git a/aio/tools/examples/example-boilerplate.js b/aio/tools/examples/example-boilerplate.js index 4304264fe4..197ac603e1 100644 --- a/aio/tools/examples/example-boilerplate.js +++ b/aio/tools/examples/example-boilerplate.js @@ -12,80 +12,36 @@ const EXAMPLES_BASE_PATH = path.resolve(__dirname, '../../content/examples'); const BOILERPLATE_PATHS = { cli: [ - 'src/environments/environment.prod.ts', - 'src/environments/environment.ts', - 'src/assets/.gitkeep', - 'src/browserslist', - 'src/favicon.ico', - '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' + 'src/environments/environment.prod.ts', 'src/environments/environment.ts', + 'src/assets/.gitkeep', 'src/browserslist', 'src/favicon.ico', '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: [ - 'src/systemjs-angular-loader.js', - 'src/systemjs.config.js', - 'src/tsconfig.json', - 'bs-config.json', - 'bs-config.e2e.json', - 'package.json', - 'tslint.json' + 'src/systemjs-angular-loader.js', 'src/systemjs.config.js', 'src/tsconfig.json', + 'bs-config.json', 'bs-config.e2e.json', 'package.json', 'tslint.json' ], - common: [ - 'src/styles.css' - ] + common: ['src/styles.css'] }; // 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 const cliRelativePath = BOILERPLATE_PATHS.cli.map(file => `../cli/${file}`); -BOILERPLATE_PATHS.elements = [ - ...cliRelativePath, - 'tsconfig.json' -]; +BOILERPLATE_PATHS.elements = [...cliRelativePath, 'tsconfig.json']; -BOILERPLATE_PATHS.i18n = [ - ...cliRelativePath, - 'angular.json', - 'package.json' -]; +BOILERPLATE_PATHS.i18n = [...cliRelativePath, 'angular.json', 'package.json']; -BOILERPLATE_PATHS['service-worker'] = [ - ...cliRelativePath, - 'angular.json', - 'package.json' -]; +BOILERPLATE_PATHS['service-worker'] = [...cliRelativePath, 'angular.json', 'package.json']; -BOILERPLATE_PATHS.testing = [ - ...cliRelativePath, - 'angular.json' -]; +BOILERPLATE_PATHS.testing = [...cliRelativePath, 'angular.json']; -BOILERPLATE_PATHS.universal = [ - ...cliRelativePath, - 'angular.json', - 'package.json' -]; +BOILERPLATE_PATHS.universal = [...cliRelativePath, 'angular.json', 'package.json']; BOILERPLATE_PATHS.ivy = { - systemjs: [ - 'rollup-config.js', - 'tsconfig-aot.json' - ], - cli: [ - 'src/tsconfig.app.json' - ] + systemjs: ['rollup-config.js', 'tsconfig-aot.json'], + cli: ['src/tsconfig.app.json'] }; BOILERPLATE_PATHS.schematics = [ @@ -101,11 +57,13 @@ class ExampleBoilerPlate { */ add(ivy = false) { // 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)) { - 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?`); + 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?`); } if (ivy) { @@ -114,7 +72,7 @@ class ExampleBoilerPlate { // 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 // 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 => { @@ -128,16 +86,20 @@ class ExampleBoilerPlate { const boilerPlateBasePath = path.resolve(BOILERPLATE_BASE_PATH, boilerPlateType); // 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 - 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 if (ivy) { const ivyBoilerPlateType = boilerPlateType === 'systemjs' ? 'systemjs' : 'cli'; - const ivyBoilerPlateBasePath = path.resolve(BOILERPLATE_BASE_PATH, 'ivy', ivyBoilerPlateType); - BOILERPLATE_PATHS.ivy[ivyBoilerPlateType].forEach(filePath => this.copyFile(ivyBoilerPlateBasePath, exampleFolder, filePath)); + const ivyBoilerPlateBasePath = + path.resolve(BOILERPLATE_BASE_PATH, 'ivy', ivyBoilerPlateType); + BOILERPLATE_PATHS.ivy[ivyBoilerPlateType].forEach( + filePath => this.copyFile(ivyBoilerPlateBasePath, exampleFolder, filePath)); } }); } @@ -145,23 +107,20 @@ class ExampleBoilerPlate { /** * Remove all the boilerplate files from all the examples */ - remove() { - shelljs.exec('git clean -xdfq', { cwd: EXAMPLES_BASE_PATH }); - } + remove() { shelljs.exec('git clean -xdfq', {cwd: EXAMPLES_BASE_PATH}); } main() { - yargs - .usage('$0 [args]') - .command('add', 'add the boilerplate to each example', (yrgs) => this.add(yrgs.argv.ivy)) - .command('remove', 'remove the boilerplate from each example', () => this.remove()) - .demandCommand(1, 'Please supply a command from the list above') - .argv; + yargs.usage('$0 [args]') + .command('add', 'add the boilerplate to each example', (yrgs) => this.add(yrgs.argv.ivy)) + .command('remove', 'remove the boilerplate from each example', () => this.remove()) + .demandCommand(1, 'Please supply a command from the list above') + .argv; } getFoldersContaining(basePath, filename, ignore) { const pattern = path.resolve(basePath, '**', filename); 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) { @@ -171,13 +130,11 @@ class ExampleBoilerPlate { filePath = this.normalizePath(filePath); const destinationPath = path.resolve(destinationFolder, filePath); - fs.copySync(sourcePath, destinationPath, { overwrite: true }); + fs.copySync(sourcePath, destinationPath, {overwrite: true}); fs.chmodSync(destinationPath, 444); } - loadJsonFile(filePath) { - return fs.readJsonSync(filePath, { throws: false }) || {}; - } + loadJsonFile(filePath) { return fs.readJsonSync(filePath, {throws: false}) || {}; } normalizePath(filePath) { // transform for example ../cli/src/tsconfig.app.json to src/tsconfig.app.json diff --git a/integration/ngcc/test.sh b/integration/ngcc/test.sh index f2f09d7865..ec2d418c02 100755 --- a/integration/ngcc/test.sh +++ b/integration/ngcc/test.sh @@ -10,11 +10,25 @@ ivy-ngcc --help ivy-ngcc # 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 - 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 # Did it replace the PRE_R3 markers correctly? @@ -48,6 +62,9 @@ ivy-ngcc # Can it be safely run again (as a noop)? ivy-ngcc +# Does running it with --formats fail? +ivy-ngcc --formats fesm2015 && exit 1 + # Now try compiling the app using the ngcc compiled libraries ngc -p tsconfig-app.json diff --git a/packages/compiler-cli/ngcc/main-ngcc.ts b/packages/compiler-cli/ngcc/main-ngcc.ts index 1d71d0ffee..b00af6ef8e 100644 --- a/packages/compiler-cli/ngcc/main-ngcc.ts +++ b/packages/compiler-cli/ngcc/main-ngcc.ts @@ -10,7 +10,7 @@ import * as path from 'canonical-path'; import * as yargs from 'yargs'; import {mainNgcc} from './src/main'; -import {EntryPointFormat} from './src/packages/entry_point'; +import {EntryPointJsonProperty} from './src/packages/entry_point'; // CLI entry point if (require.main === module) { @@ -22,11 +22,15 @@ if (require.main === module) { describe: 'A path to the root folder to compile.', default: './node_modules' }) - .option('f', { - alias: 'formats', + .option('f', {alias: 'formats', hidden: true, array: true}) + .option('p', { + alias: 'properties', array: true, - describe: 'An array of formats to compile.', - default: ['fesm2015', 'esm2015', 'fesm5', 'esm5'] + describe: + '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', { alias: 'target', @@ -36,11 +40,16 @@ if (require.main === module) { .help() .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 formats: EntryPointFormat[] = options['f']; + const propertiesToConsider: EntryPointJsonProperty[] = options['p']; const baseTargetPath: string = options['t']; try { - mainNgcc({baseSourcePath, formats, baseTargetPath}); + mainNgcc({baseSourcePath, propertiesToConsider, baseTargetPath}); process.exitCode = 0; } catch (e) { console.error(e.stack || e.message); diff --git a/packages/compiler-cli/ngcc/src/main.ts b/packages/compiler-cli/ngcc/src/main.ts index 4c38d55471..e2e68e2691 100644 --- a/packages/compiler-cli/ngcc/src/main.ts +++ b/packages/compiler-cli/ngcc/src/main.ts @@ -6,10 +6,10 @@ * 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 {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 {EntryPointFinder} from './packages/entry_point_finder'; import {Transformer} from './packages/transformer'; @@ -20,8 +20,6 @@ import {Transformer} from './packages/transformer'; export interface NgccOptions { /** The path to the node_modules folder that contains the packages to compile. */ 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. */ baseTargetPath?: string; /** @@ -29,8 +27,15 @@ export interface NgccOptions { * All its dependencies will need to be compiled too. */ 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). * @@ -39,8 +44,8 @@ export interface NgccOptions { * * @param options The options telling ngcc what to compile and how. */ -export function mainNgcc({baseSourcePath, formats, baseTargetPath = baseSourcePath, - targetEntryPointPath}: NgccOptions): void { +export function mainNgcc({baseSourcePath, baseTargetPath = baseSourcePath, targetEntryPointPath, + propertiesToConsider}: NgccOptions): void { const transformer = new Transformer(baseSourcePath, baseTargetPath); const host = new DependencyHost(); const resolver = new DependencyResolver(host); @@ -52,28 +57,51 @@ export function mainNgcc({baseSourcePath, formats, baseTargetPath = baseSourcePa // Are we compiling the Angular core? const isCore = entryPoint.name === '@angular/core'; - // We transform the d.ts typings files while transforming one of the formats. - // 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'; + let dtsTransformFormat: EntryPointFormat|undefined; - formats.forEach(format => { - if (checkMarkerFile(entryPoint, format)) { - console.warn(`Skipping ${entryPoint.name} : ${format} (already built).`); - return; + const propertiesToCompile = + propertiesToConsider || Object.keys(entryPoint.packageJson) as EntryPointJsonProperty[]; + const compiledFormats = new Set(); + + 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 = + makeEntryPointBundle(entryPoint, isCore, format, format === dtsTransformFormat); + if (bundle) { + transformer.transform(entryPoint, isCore, bundle); + } else { + console.warn( + `Skipping ${entryPoint.name} : ${format} (no entry point file for this format).`); + } + } else { + console.warn(`Skipping ${entryPoint.name} : ${property} (already compiled).`); + } } + // Write the built-with-ngcc marker. + writeMarker(entryPoint, property); + } - const bundle = - makeEntryPointBundle(entryPoint, isCore, format, format === dtsTransformFormat); - if (bundle === null) { - console.warn( - `Skipping ${entryPoint.name} : ${format} (no entry point file for this format).`); - } else { - transformer.transform(entryPoint, isCore, bundle); - } - - // Write the built-with-ngcc marker - writeMarkerFile(entryPoint, format); - }); + if (!dtsTransformFormat) { + throw new Error( + `Failed to compile any formats for entry-point at (${entryPoint.path}). Tried ${propertiesToCompile}.`); + } }); } + +export {NGCC_VERSION} from './packages/build_marker'; \ No newline at end of file diff --git a/packages/compiler-cli/ngcc/src/packages/build_marker.ts b/packages/compiler-cli/ngcc/src/packages/build_marker.ts index 986b296a26..e85e7474aa 100644 --- a/packages/compiler-cli/ngcc/src/packages/build_marker.ts +++ b/packages/compiler-cli/ngcc/src/packages/build_marker.ts @@ -7,35 +7,42 @@ */ import {resolve} from 'canonical-path'; -import {existsSync, readFileSync, writeFileSync} from 'fs'; -import {EntryPoint, EntryPointFormat} from './entry_point'; +import {writeFileSync} from 'fs'; + +import {EntryPoint, EntryPointJsonProperty} from './entry_point'; 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 property. + * @param entryPoint the entry-point to check 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 checkMarker(entryPoint: EntryPoint, format: EntryPointJsonProperty): boolean { + const pkg = entryPoint.packageJson; + const compiledVersion = pkg.__modified_by_ngcc__ && pkg.__modified_by_ngcc__[format]; + if (compiledVersion && compiledVersion !== 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 !!compiledVersion; } /** - * Check whether there is a build marker for the given entry point and format. - * @param entryPoint the entry point to check for a marker. - * @param format the format for which we are checking for a marker. + * Write a build marker for the given entry-point and format property, to indicate that it has + * 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 checkMarkerFile(entryPoint: EntryPoint, format: EntryPointFormat): boolean { - const markerPath = getMarkerPath(entryPoint.path, 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(entryPoint: EntryPoint, format: EntryPointFormat) { - const markerPath = getMarkerPath(entryPoint.path, format); - writeFileSync(markerPath, NGCC_VERSION, 'utf8'); +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'); } diff --git a/packages/compiler-cli/ngcc/src/packages/entry_point.ts b/packages/compiler-cli/ngcc/src/packages/entry_point.ts index d2fe854c02..12d26bf6f8 100644 --- a/packages/compiler-cli/ngcc/src/packages/entry_point.ts +++ b/packages/compiler-cli/ngcc/src/packages/entry_point.ts @@ -8,6 +8,7 @@ import * as path from 'canonical-path'; import * as fs from 'fs'; +import {isDefined} from '../utils'; /** @@ -33,6 +34,8 @@ export type EntryPointFormat = keyof(EntryPointPaths); export interface EntryPoint extends EntryPointPaths { /** The name of the package (e.g. `@angular/core`). */ name: string; + /** The parsed package.json file for this entry-point. */ + packageJson: EntryPointPackageJson; /** The path to the package that contains this entry-point. */ package: string; /** The path to this entry point. */ @@ -41,11 +44,7 @@ export interface EntryPoint extends EntryPointPaths { typings: string; } -/** - * The properties that may be loaded from the `package.json` file. - */ -interface EntryPointPackageJson { - name: string; +interface PackageJsonFormatProperties { fesm2015?: string; fesm5?: string; es2015?: string; // if exists then it is actually FESM2015 @@ -57,6 +56,103 @@ interface EntryPointPackageJson { 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. * @param packageJsonPath the absolute path to the package.json file. @@ -71,72 +167,3 @@ function loadEntryPointPackage(packageJsonPath: string): EntryPointPackageJson|n 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; -} diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts index 790de7ec69..11c062ae81 100644 --- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts +++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts @@ -19,38 +19,74 @@ describe('ngcc main()', () => { afterEach(restoreRealFileSystem); 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', () => { - 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', () => { - 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', () => { - 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)', () => { - mainNgcc({ - baseSourcePath: '/node_modules', - formats: ['esm2015'], - targetEntryPointPath: '@angular/common' - }); + mainNgcc({baseSourcePath: '/node_modules', targetEntryPointPath: '@angular/common/http'}); - expect(loadPackage('@angular/common').__modified_by_ngcc__).toEqual({ - esm2015: '0.0.0-PLACEHOLDER', + expect(loadPackage('@angular/common/http').__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/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({ - esm2015: '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' }); 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', + }); }); }); diff --git a/packages/compiler-cli/ngcc/test/packages/build_marker_spec.ts b/packages/compiler-cli/ngcc/test/packages/build_marker_spec.ts index 811bb8cfc6..623ad40b2c 100644 --- a/packages/compiler-cli/ngcc/test/packages/build_marker_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/build_marker_spec.ts @@ -6,10 +6,10 @@ * 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 {checkMarkerFile, writeMarkerFile} from '../../src/packages/build_marker'; +import {checkMarker, writeMarker} from '../../src/packages/build_marker'; import {EntryPoint} from '../../src/packages/entry_point'; function createMockFileSystem() { @@ -94,58 +94,55 @@ function restoreRealFileSystem() { } 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', () => { 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); + describe('writeMarker', () => { + it('should write a property in the package.json containing the version placeholder', () => { + let pkg = JSON.parse(readFileSync('/node_modules/@angular/common/package.json', 'utf8')); + expect(pkg.__modified_by_ngcc__).toBeUndefined(); + expect(pkg.__modified_by_ngcc__).toBeUndefined(); - writeMarkerFile(createEntryPoint('/node_modules/@angular/common'), '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'); + writeMarker(createEntryPoint('/node_modules/@angular/common'), 'fesm2015'); + pkg = JSON.parse(readFileSync('/node_modules/@angular/common/package.json', 'utf8')); + expect(pkg.__modified_by_ngcc__.fesm2015).toEqual('0.0.0-PLACEHOLDER'); + expect(pkg.__modified_by_ngcc__.esm5).toBeUndefined(); - writeMarkerFile(createEntryPoint('/node_modules/@angular/common'), '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'); + writeMarker(createEntryPoint('/node_modules/@angular/common'), 'esm5'); + pkg = JSON.parse(readFileSync('/node_modules/@angular/common/package.json', 'utf8')); + expect(pkg.__modified_by_ngcc__.fesm2015).toEqual('0.0.0-PLACEHOLDER'); + expect(pkg.__modified_by_ngcc__.esm5).toEqual('0.0.0-PLACEHOLDER'); }); }); - describe('checkMarkerFile', () => { - it('should return false if the marker file does not exist', () => { - expect(checkMarkerFile(createEntryPoint('/node_modules/@angular/common'), 'fesm2015')) + describe('checkMarker', () => { + it('should return false if the marker property does not exist', () => { + expect(checkMarker(createEntryPoint('/node_modules/@angular/common'), '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(createEntryPoint('/node_modules/@angular/common'), 'fesm2015')) - .toBe(true); + it('should return true if the marker property exists and contains the correct version', () => { + const pkg = JSON.parse(readFileSync('/node_modules/@angular/common/package.json', 'utf8')); + pkg.__modified_by_ngcc__ = {fesm2015: '0.0.0-PLACEHOLDER'}; + writeFileSync('/node_modules/@angular/common/package.json', JSON.stringify(pkg), 'utf8'); + expect(checkMarker(createEntryPoint('/node_modules/@angular/common'), '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(createEntryPoint('/node_modules/@angular/common'), 'fesm2015')) + it('should throw if the marker property exists but contains the wrong version', () => { + const pkg = JSON.parse(readFileSync('/node_modules/@angular/common/package.json', 'utf8')); + pkg.__modified_by_ngcc__ = {fesm2015: 'WRONG_VERSION'}; + writeFileSync('/node_modules/@angular/common/package.json', JSON.stringify(pkg), 'utf8'); + expect(() => checkMarker(createEntryPoint('/node_modules/@angular/common'), 'fesm2015')) .toThrowError( 'The ngcc compiler has changed since the last ngcc build.\n' + 'Please completely remove `node_modules` and try again.'); diff --git a/packages/compiler-cli/ngcc/test/packages/entry_point_finder_spec.ts b/packages/compiler-cli/ngcc/test/packages/entry_point_finder_spec.ts index 6ceeae840a..dbd2971015 100644 --- a/packages/compiler-cli/ngcc/test/packages/entry_point_finder_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/entry_point_finder_spec.ts @@ -25,7 +25,7 @@ describe('findEntryPoints()', () => { beforeEach(createMockFileSystem); afterEach(restoreRealFileSystem); - it('should find sub-entry-points within a package', () => { + it('should find sub-entry-points within a package', () => { const {entryPoints} = finder.findEntryPoints('/sub_entry_points'); const entryPointPaths = entryPoints.map(x => [x.package, x.path]); expect(entryPointPaths).toEqual([ 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 d84733aaf5..dffa1e8738 100644 --- a/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts @@ -6,10 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ +import {readFileSync} from 'fs'; import * as mockFs from 'mock-fs'; import {getEntryPointInfo} from '../../src/packages/entry_point'; - describe('getEntryPointInfo()', () => { beforeEach(createMockFileSystem); afterEach(restoreRealFileSystem); @@ -27,6 +27,7 @@ describe('getEntryPointInfo()', () => { fesm5: `/some_package/valid_entry_point/fesm2015/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`, + 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`, 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`, + packageJson: loadPackageJson('/some_package/types_rather_than_typings'), }); }); @@ -76,6 +78,7 @@ describe('getEntryPointInfo()', () => { fesm2015: `/some_package/material_style/esm2015/material_style.js`, fesm5: `/some_package/material_style/esm5/material_style.es5.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); } + +export function loadPackageJson(packagePath: string) { + return JSON.parse(readFileSync(packagePath + '/package.json', 'utf8')); +}