/** * @license * Copyright Google Inc. All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {AbsoluteFsPath, FileSystem, absoluteFrom, dirname, getFileSystem, resolve} from '../../src/ngtsc/file_system'; import {CommonJsDependencyHost} from './dependencies/commonjs_dependency_host'; import {DependencyResolver, InvalidEntryPoint, SortedEntryPointsInfo} from './dependencies/dependency_resolver'; import {EsmDependencyHost} from './dependencies/esm_dependency_host'; import {ModuleResolver} from './dependencies/module_resolver'; import {UmdDependencyHost} from './dependencies/umd_dependency_host'; import {DirectoryWalkerEntryPointFinder} from './entry_point_finder/directory_walker_entry_point_finder'; import {TargetedEntryPointFinder} from './entry_point_finder/targeted_entry_point_finder'; import {ConsoleLogger, LogLevel} from './logging/console_logger'; import {Logger} from './logging/logger'; import {hasBeenProcessed, markAsProcessed} from './packages/build_marker'; import {NgccConfiguration} from './packages/configuration'; import {EntryPoint, EntryPointJsonProperty, EntryPointPackageJson, SUPPORTED_FORMAT_PROPERTIES, getEntryPointFormat} from './packages/entry_point'; import {makeEntryPointBundle} from './packages/entry_point_bundle'; import {Transformer} from './packages/transformer'; import {PathMappings} from './utils'; import {FileWriter} from './writing/file_writer'; import {InPlaceFileWriter} from './writing/in_place_file_writer'; import {NewEntryPointFileWriter} from './writing/new_entry_point_file_writer'; /** * The options to configure the ngcc compiler. */ export interface NgccOptions { /** The absolute path to the `node_modules` folder that contains the packages to process. */ basePath: string; /** * The path to the primary package to be processed. If not absolute then it must be relative to * `basePath`. * * All its dependencies will need to be processed too. */ targetEntryPointPath?: string; /** * Which entry-point properties in the package.json to consider when processing an entry-point. * Each property should hold a path to the particular bundle format for the entry-point. * Defaults to all the properties in the package.json. */ propertiesToConsider?: string[]; /** * Whether to process all formats specified by (`propertiesToConsider`) or to stop processing * this entry-point at the first matching format. Defaults to `true`. */ compileAllFormats?: boolean; /** * Whether to create new entry-points bundles rather than overwriting the original files. */ createNewEntryPointFormats?: boolean; /** * Provide a logger that will be called with log messages. */ logger?: Logger; /** * Paths mapping configuration (`paths` and `baseUrl`), as found in `ts.CompilerOptions`. * These are used to resolve paths to locally built Angular libraries. */ pathMappings?: PathMappings; /** * Provide a file-system service that will be used by ngcc for all file interactions. */ fileSystem?: FileSystem; } /** * This is the main entry-point into ngcc (aNGular Compatibility Compiler). * * You can call this function to process one or more npm packages, to ensure * that they are compatible with the ivy compiler (ngtsc). * * @param options The options telling ngcc what to compile and how. */ export function mainNgcc( {basePath, targetEntryPointPath, propertiesToConsider = SUPPORTED_FORMAT_PROPERTIES, compileAllFormats = true, createNewEntryPointFormats = false, logger = new ConsoleLogger(LogLevel.info), pathMappings}: NgccOptions): void { const supportedPropertiesToConsider = ensureSupportedProperties(propertiesToConsider); const fileSystem = getFileSystem(); const transformer = new Transformer(fileSystem, logger); const moduleResolver = new ModuleResolver(fileSystem, pathMappings); const esmDependencyHost = new EsmDependencyHost(fileSystem, moduleResolver); const umdDependencyHost = new UmdDependencyHost(fileSystem, moduleResolver); const commonJsDependencyHost = new CommonJsDependencyHost(fileSystem, moduleResolver); const resolver = new DependencyResolver(fileSystem, logger, { esm5: esmDependencyHost, esm2015: esmDependencyHost, umd: umdDependencyHost, commonjs: commonJsDependencyHost }); const absBasePath = absoluteFrom(basePath); const config = new NgccConfiguration(fileSystem, dirname(absBasePath)); const fileWriter = getFileWriter(fileSystem, createNewEntryPointFormats); const entryPoints = getEntryPoints( fileSystem, config, logger, resolver, absBasePath, targetEntryPointPath, pathMappings, supportedPropertiesToConsider, compileAllFormats); for (const entryPoint of entryPoints) { // Are we compiling the Angular core? const isCore = entryPoint.name === '@angular/core'; const entryPointPackageJson = entryPoint.packageJson; const entryPointPackageJsonPath = fileSystem.resolve(entryPoint.path, 'package.json'); const {propertiesToProcess, propertyToPropertiesToMarkAsProcessed} = getPropertiesToProcessAndMarkAsProcessed( entryPointPackageJson, supportedPropertiesToConsider); let hasAnyProcessedFormat = false; let processDts = !hasBeenProcessed(entryPointPackageJson, 'typings'); for (const property of propertiesToProcess) { // If we only need one format processed and we already have one, exit the loop. if (!compileAllFormats && hasAnyProcessedFormat) break; const formatPath = entryPointPackageJson[property]; const format = getEntryPointFormat(fileSystem, entryPoint, property); // All properties listed in `propertiesToProcess` are guaranteed to point to a format-path // (i.e. they exist in `entryPointPackageJson`). Furthermore, they are also guaranteed to be // among `SUPPORTED_FORMAT_PROPERTIES`. // Based on the above, `formatPath` should always be defined and `getEntryPointFormat()` // should always return a format here (and not `undefined`). if (!formatPath || !format) { // This should never happen. throw new Error( `Invariant violated: No format-path or format for ${entryPoint.path} : ${property} ` + `(formatPath: ${formatPath} | format: ${format})`); } // The `formatPath` which the property maps to is already processed - nothing to do. if (hasBeenProcessed(entryPointPackageJson, property)) { hasAnyProcessedFormat = true; logger.debug(`Skipping ${entryPoint.name} : ${property} (already compiled).`); continue; } const bundle = makeEntryPointBundle( fileSystem, entryPoint, formatPath, isCore, property, format, processDts, pathMappings, true); logger.info(`Compiling ${entryPoint.name} : ${property} as ${format}`); const transformedFiles = transformer.transform(bundle); fileWriter.writeBundle(entryPoint, bundle, transformedFiles); hasAnyProcessedFormat = true; const propsToMarkAsProcessed: (EntryPointJsonProperty | 'typings')[] = propertyToPropertiesToMarkAsProcessed.get(property) !; if (processDts) { propsToMarkAsProcessed.push('typings'); processDts = false; } markAsProcessed( fileSystem, entryPointPackageJson, entryPointPackageJsonPath, propsToMarkAsProcessed); } if (!hasAnyProcessedFormat) { throw new Error( `Failed to compile any formats for entry-point at (${entryPoint.path}). Tried ${supportedPropertiesToConsider}.`); } } } function ensureSupportedProperties(properties: string[]): EntryPointJsonProperty[] { // Short-circuit the case where `properties` has fallen back to the default value: // `SUPPORTED_FORMAT_PROPERTIES` if (properties === SUPPORTED_FORMAT_PROPERTIES) return SUPPORTED_FORMAT_PROPERTIES; const supportedProperties: EntryPointJsonProperty[] = []; for (const prop of properties as EntryPointJsonProperty[]) { if (SUPPORTED_FORMAT_PROPERTIES.indexOf(prop) !== -1) { supportedProperties.push(prop); } } if (supportedProperties.length === 0) { throw new Error( `No supported format property to consider among [${properties.join(', ')}]. ` + `Supported properties: ${SUPPORTED_FORMAT_PROPERTIES.join(', ')}`); } return supportedProperties; } function getFileWriter(fs: FileSystem, createNewEntryPointFormats: boolean): FileWriter { return createNewEntryPointFormats ? new NewEntryPointFileWriter(fs) : new InPlaceFileWriter(fs); } function getEntryPoints( fs: FileSystem, config: NgccConfiguration, logger: Logger, resolver: DependencyResolver, basePath: AbsoluteFsPath, targetEntryPointPath: string | undefined, pathMappings: PathMappings | undefined, propertiesToConsider: string[], compileAllFormats: boolean): EntryPoint[] { const {entryPoints, invalidEntryPoints} = (targetEntryPointPath !== undefined) ? getTargetedEntryPoints( fs, config, logger, resolver, basePath, targetEntryPointPath, propertiesToConsider, compileAllFormats, pathMappings) : getAllEntryPoints(fs, config, logger, resolver, basePath, pathMappings); logInvalidEntryPoints(logger, invalidEntryPoints); return entryPoints; } function getTargetedEntryPoints( fs: FileSystem, config: NgccConfiguration, logger: Logger, resolver: DependencyResolver, basePath: AbsoluteFsPath, targetEntryPointPath: string, propertiesToConsider: string[], compileAllFormats: boolean, pathMappings: PathMappings | undefined): SortedEntryPointsInfo { const absoluteTargetEntryPointPath = resolve(basePath, targetEntryPointPath); if (hasProcessedTargetEntryPoint( fs, absoluteTargetEntryPointPath, propertiesToConsider, compileAllFormats)) { logger.debug('The target entry-point has already been processed'); return {entryPoints: [], invalidEntryPoints: [], ignoredDependencies: []}; } const finder = new TargetedEntryPointFinder( fs, config, logger, resolver, basePath, absoluteTargetEntryPointPath, pathMappings); const entryPointInfo = finder.findEntryPoints(); const invalidTarget = entryPointInfo.invalidEntryPoints.find( i => i.entryPoint.path === absoluteTargetEntryPointPath); if (invalidTarget !== undefined) { throw new Error( `The target entry-point "${invalidTarget.entryPoint.name}" has missing dependencies:\n` + invalidTarget.missingDependencies.map(dep => ` - ${dep}\n`)); } if (entryPointInfo.entryPoints.length === 0) { markNonAngularPackageAsProcessed(fs, absoluteTargetEntryPointPath); } return entryPointInfo; } function getAllEntryPoints( fs: FileSystem, config: NgccConfiguration, logger: Logger, resolver: DependencyResolver, basePath: AbsoluteFsPath, pathMappings: PathMappings | undefined): SortedEntryPointsInfo { const finder = new DirectoryWalkerEntryPointFinder(fs, config, logger, resolver, basePath, pathMappings); return finder.findEntryPoints(); } function hasProcessedTargetEntryPoint( fs: FileSystem, targetPath: AbsoluteFsPath, propertiesToConsider: string[], compileAllFormats: boolean) { const packageJsonPath = resolve(targetPath, 'package.json'); // It might be that this target is configured in which case its package.json might not exist. if (!fs.exists(packageJsonPath)) { return false; } const packageJson = JSON.parse(fs.readFile(packageJsonPath)); for (const property of propertiesToConsider) { if (packageJson[property]) { // Here is a property that should be processed if (hasBeenProcessed(packageJson, property as EntryPointJsonProperty)) { if (!compileAllFormats) { // It has been processed and we only need one, so we are done. return true; } } else { // It has not been processed but we need all of them, so we are done. return false; } } } // Either all formats need to be compiled and there were none that were unprocessed, // Or only the one matching format needs to be compiled but there was at least one matching // property before the first processed format that was unprocessed. return true; } /** * If we get here, then the requested entry-point did not contain anything compiled by * the old Angular compiler. Therefore there is nothing for ngcc to do. * So mark all formats in this entry-point as processed so that clients of ngcc can avoid * triggering ngcc for this entry-point in the future. */ function markNonAngularPackageAsProcessed(fs: FileSystem, path: AbsoluteFsPath) { const packageJsonPath = resolve(path, 'package.json'); const packageJson = JSON.parse(fs.readFile(packageJsonPath)); // Note: We are marking all supported properties as processed, even if they don't exist in the // `package.json` file. While this is redundant, it is also harmless. markAsProcessed(fs, packageJson, packageJsonPath, SUPPORTED_FORMAT_PROPERTIES); } function logInvalidEntryPoints(logger: Logger, invalidEntryPoints: InvalidEntryPoint[]): void { invalidEntryPoints.forEach(invalidEntryPoint => { logger.debug( `Invalid entry-point ${invalidEntryPoint.entryPoint.path}.`, `It is missing required dependencies:\n` + invalidEntryPoint.missingDependencies.map(dep => ` - ${dep}`).join('\n')); }); } /** * This function computes and returns the following: * - `propertiesToProcess`: An (ordered) list of properties that exist and need to be processed, * based on the specified `propertiesToConsider`, the properties in `package.json` and their * corresponding format-paths. NOTE: Only one property per format-path needs to be processed. * - `propertyToPropertiesToMarkAsProcessed`: A mapping from each property in `propertiesToProcess` * to the list of other properties in `package.json` that need to be marked as processed as soon * as of the former being processed. */ function getPropertiesToProcessAndMarkAsProcessed( packageJson: EntryPointPackageJson, propertiesToConsider: EntryPointJsonProperty[]): { propertiesToProcess: EntryPointJsonProperty[]; propertyToPropertiesToMarkAsProcessed: Map; } { const formatPathsToConsider = new Set(); const propertiesToProcess: EntryPointJsonProperty[] = []; for (const prop of propertiesToConsider) { // Ignore properties that are not in `package.json`. if (!packageJson.hasOwnProperty(prop)) continue; const formatPath = packageJson[prop] !; // Ignore properties that map to the same format-path as a preceding property. if (formatPathsToConsider.has(formatPath)) continue; // Process this property, because it is the first one to map to this format-path. formatPathsToConsider.add(formatPath); propertiesToProcess.push(prop); } const formatPathToProperties: {[formatPath: string]: EntryPointJsonProperty[]} = {}; for (const prop of SUPPORTED_FORMAT_PROPERTIES) { // Ignore properties that are not in `package.json`. if (!packageJson.hasOwnProperty(prop)) continue; const formatPath = packageJson[prop] !; // Ignore properties that do not map to a format-path that will be considered. if (!formatPathsToConsider.has(formatPath)) continue; // Add this property to the map. const list = formatPathToProperties[formatPath] || (formatPathToProperties[formatPath] = []); list.push(prop); } const propertyToPropertiesToMarkAsProcessed = new Map(); for (const prop of propertiesToConsider) { const formatPath = packageJson[prop] !; const propertiesToMarkAsProcessed = formatPathToProperties[formatPath]; propertyToPropertiesToMarkAsProcessed.set(prop, propertiesToMarkAsProcessed); } return {propertiesToProcess, propertyToPropertiesToMarkAsProcessed}; }