diff --git a/packages/compiler-cli/ngcc/index.ts b/packages/compiler-cli/ngcc/index.ts index dc63ad76b8..8b131672ca 100644 --- a/packages/compiler-cli/ngcc/index.ts +++ b/packages/compiler-cli/ngcc/index.ts @@ -12,6 +12,7 @@ import {EntryPointJsonProperty, EntryPointPackageJson} from './src/packages/entr export {ConsoleLogger, LogLevel} from './src/logging/console_logger'; export {Logger} from './src/logging/logger'; export {NgccOptions, mainNgcc as process} from './src/main'; +export {PathMappings} from './src/utils'; export function hasBeenProcessed(packageJson: object, format: string) { // We are wrapping this function to hide the internal types. diff --git a/packages/compiler-cli/ngcc/src/main.ts b/packages/compiler-cli/ngcc/src/main.ts index acee60c7c6..c99a7bf56d 100644 --- a/packages/compiler-cli/ngcc/src/main.ts +++ b/packages/compiler-cli/ngcc/src/main.ts @@ -21,12 +21,12 @@ import {makeEntryPointBundle} from './packages/entry_point_bundle'; import {EntryPointFinder} from './packages/entry_point_finder'; import {ModuleResolver} from './packages/module_resolver'; 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. */ @@ -58,6 +58,11 @@ export interface NgccOptions { * 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; } const SUPPORTED_FORMATS: EntryPointFormat[] = ['esm5', 'esm2015']; @@ -70,12 +75,12 @@ const SUPPORTED_FORMATS: EntryPointFormat[] = ['esm5', 'esm2015']; * * @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)}: NgccOptions): void { +export function mainNgcc( + {basePath, targetEntryPointPath, propertiesToConsider = SUPPORTED_FORMAT_PROPERTIES, + compileAllFormats = true, createNewEntryPointFormats = false, + logger = new ConsoleLogger(LogLevel.info), pathMappings}: NgccOptions): void { const transformer = new Transformer(logger); - const moduleResolver = new ModuleResolver(); + const moduleResolver = new ModuleResolver(pathMappings); const host = new DependencyHost(moduleResolver); const resolver = new DependencyResolver(logger, host); const finder = new EntryPointFinder(logger, resolver); @@ -92,8 +97,8 @@ export function mainNgcc({basePath, targetEntryPointPath, return; } - const {entryPoints} = - finder.findEntryPoints(AbsoluteFsPath.from(basePath), absoluteTargetEntryPointPath); + const {entryPoints} = finder.findEntryPoints( + AbsoluteFsPath.from(basePath), absoluteTargetEntryPointPath, pathMappings); if (absoluteTargetEntryPointPath && entryPoints.length === 0) { markNonAngularPackageAsProcessed(absoluteTargetEntryPointPath, propertiesToConsider); @@ -132,7 +137,8 @@ export function mainNgcc({basePath, targetEntryPointPath, // the property as processed even if its underlying format has been built already. if (!compiledFormats.has(formatPath) && (compileAllFormats || isFirstFormat)) { const bundle = makeEntryPointBundle( - entryPoint.path, formatPath, entryPoint.typings, isCore, property, format, processDts); + entryPoint.path, formatPath, entryPoint.typings, isCore, property, format, processDts, + pathMappings); if (bundle) { logger.info(`Compiling ${entryPoint.name} : ${property} as ${format}`); const transformedFiles = transformer.transform(bundle); diff --git a/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts b/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts index 0130d52677..778577e305 100644 --- a/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts +++ b/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts @@ -9,11 +9,11 @@ import {resolve} from 'canonical-path'; import * as ts from 'typescript'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {PathMappings} from '../utils'; + import {BundleProgram, makeBundleProgram} from './bundle_program'; import {EntryPointFormat, EntryPointJsonProperty} from './entry_point'; - - /** * A bundle of files and paths (and TS programs) that correspond to a particular * format of a package entry-point. @@ -39,13 +39,13 @@ export interface EntryPointBundle { */ export function makeEntryPointBundle( entryPointPath: string, formatPath: string, typingsPath: string, isCore: boolean, - formatProperty: EntryPointJsonProperty, format: EntryPointFormat, - transformDts: boolean): EntryPointBundle|null { + formatProperty: EntryPointJsonProperty, format: EntryPointFormat, transformDts: boolean, + pathMappings?: PathMappings): EntryPointBundle|null { // Create the TS program and necessary helpers. const options: ts.CompilerOptions = { allowJs: true, maxNodeModuleJsDepth: Infinity, - rootDir: entryPointPath, + rootDir: entryPointPath, ...pathMappings }; const host = ts.createCompilerHost(options); const rootDirs = [AbsoluteFsPath.from(entryPointPath)]; diff --git a/packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts b/packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts index bbb2f51778..2daa002159 100644 --- a/packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts +++ b/packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts @@ -7,9 +7,11 @@ */ import * as path from 'canonical-path'; import * as fs from 'fs'; +import {join, resolve} from 'path'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {Logger} from '../logging/logger'; +import {PathMappings} from '../utils'; import {DependencyResolver, SortedEntryPointsInfo} from './dependency_resolver'; import {EntryPoint, getEntryPointInfo} from './entry_point'; @@ -21,15 +23,51 @@ export class EntryPointFinder { * Search the given directory, and sub-directories, for Angular package entry points. * @param sourceDirectory An absolute path to the directory to search for entry points. */ - findEntryPoints(sourceDirectory: AbsoluteFsPath, targetEntryPointPath?: AbsoluteFsPath): - SortedEntryPointsInfo { - const unsortedEntryPoints = this.walkDirectoryForEntryPoints(sourceDirectory); + findEntryPoints( + sourceDirectory: AbsoluteFsPath, targetEntryPointPath?: AbsoluteFsPath, + pathMappings?: PathMappings): SortedEntryPointsInfo { + const basePaths = this.getBasePaths(sourceDirectory, pathMappings); + const unsortedEntryPoints = basePaths.reduce( + (entryPoints, basePath) => entryPoints.concat(this.walkDirectoryForEntryPoints(basePath)), + []); const targetEntryPoint = targetEntryPointPath ? unsortedEntryPoints.find(entryPoint => entryPoint.path === targetEntryPointPath) : undefined; return this.resolver.sortEntryPointsByDependency(unsortedEntryPoints, targetEntryPoint); } + /** + * Extract all the base-paths that we need to search for entry-points. + * + * This always contains the standard base-path (`sourceDirectory`). + * But it also parses the `paths` mappings object to guess additional base-paths. + * + * For example: + * + * ``` + * getBasePaths('/node_modules', {baseUrl: '/dist', paths: {'*': ['lib/*', 'lib/generated/*']}}) + * > ['/node_modules', '/dist/lib'] + * ``` + * + * Notice that `'/dist'` is not included as there is no `'*'` path, + * and `'/dist/lib/generated'` is not included as it is covered by `'/dist/lib'`. + * + * @param sourceDirectory The standard base-path (e.g. node_modules). + * @param pathMappings Path mapping configuration, from which to extract additional base-paths. + */ + private getBasePaths(sourceDirectory: AbsoluteFsPath, pathMappings?: PathMappings): + AbsoluteFsPath[] { + const basePaths = [sourceDirectory]; + if (pathMappings) { + const baseUrl = AbsoluteFsPath.from(resolve(pathMappings.baseUrl)); + values(pathMappings.paths).forEach(paths => paths.forEach(path => { + basePaths.push(AbsoluteFsPath.fromUnchecked(join(baseUrl, extractPathPrefix(path)))); + })); + } + basePaths.sort(); // Get the paths in order with the shorter ones first. + return basePaths.filter(removeDeeperPaths); + } + /** * Look for entry points that need to be compiled, starting at the source directory. * The function will recurse into directories that start with `@...`, e.g. `@angular/...`. @@ -117,3 +155,35 @@ export class EntryPointFinder { }); } } + +/** + * Extract everything in the `path` up to the first `*`. + * @param path The path to parse. + * @returns The extracted prefix. + */ +function extractPathPrefix(path: string) { + return path.split('*', 1)[0]; +} + +/** + * A filter function that removes paths that are already covered by higher paths. + * + * @param value The current path. + * @param index The index of the current path. + * @param array The array of paths (sorted alphabetically). + * @returns true if this path is not already covered by a previous path. + */ +function removeDeeperPaths(value: AbsoluteFsPath, index: number, array: AbsoluteFsPath[]) { + for (let i = 0; i < index; i++) { + if (value.startsWith(array[i])) return false; + } + return true; +} + +/** + * Extract all the values (not keys) from an object. + * @param obj The object to process. + */ +function values(obj: {[key: string]: T}): T[] { + return Object.keys(obj).map(key => obj[key]); +} diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts index 6500dc9fc9..1ad2c9f6aa 100644 --- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts +++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts @@ -309,6 +309,24 @@ describe('ngcc main()', () => { expect(logger.logs.info).toContain(['Compiling @angular/common/http : esm2015 as esm2015']); }); }); + + describe('with pathMappings', () => { + it('should find and compile packages accessible via the pathMappings', () => { + mainNgcc({ + basePath: '/node_modules', + propertiesToConsider: ['es2015'], + pathMappings: {paths: {'*': ['dist/*']}, baseUrl: '/'}, + }); + expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({ + es2015: '0.0.0-PLACEHOLDER', + typings: '0.0.0-PLACEHOLDER', + }); + expect(loadPackage('local-package', '/dist').__processed_by_ivy_ngcc__).toEqual({ + es2015: '0.0.0-PLACEHOLDER', + typings: '0.0.0-PLACEHOLDER', + }); + }); + }); }); @@ -321,10 +339,23 @@ function createMockFileSystem() { 'package.json': '{"name": "test-package", "es2015": "./index.js", "typings": "./index.d.ts"}', // no metadata.json file so not compiled by Angular. 'index.js': - 'import {AppModule} from "@angular/common"; export class MyApp extends AppModule;', + 'import {AppModule} from "@angular/common"; export class MyApp extends AppModule {};', 'index.d.ts': 'import {AppModule} from "@angular/common"; export declare class MyApp extends AppModule;', - } + }, + '/dist/local-package': { + 'package.json': + '{"name": "local-package", "es2015": "./index.js", "typings": "./index.d.ts"}', + 'index.metadata.json': 'DUMMY DATA', + 'index.js': ` + import {Component} from '@angular/core'; + export class AppComponent {}; + AppComponent.decorators = [ + { type: Component, args: [{selector: 'app', template: '

Hello

'}] } + ];`, + 'index.d.ts': ` + export declare class AppComponent {};`, + }, }); } @@ -367,6 +398,6 @@ interface Directory { [pathSegment: string]: string|Directory; } -function loadPackage(packageName: string): EntryPointPackageJson { - return JSON.parse(readFileSync(`/node_modules/${packageName}/package.json`, 'utf8')); +function loadPackage(packageName: string, basePath = '/node_modules'): EntryPointPackageJson { + return JSON.parse(readFileSync(`${basePath}/${packageName}/package.json`, 'utf8')); }