feat(ivy): ngcc - support additional paths to process (#29643)

By passing a `pathMappings` configuration (a subset of the
`ts.CompilerOptions` interface), we can instuct ngcc to process
additional paths outside the `node_modules` folder.

PR Close #29643
This commit is contained in:
Pete Bacon Darwin 2019-04-28 20:47:57 +01:00 committed by Andrew Kushnir
parent 23152c37c8
commit 5ced8fbbd5
5 changed files with 129 additions and 21 deletions

View File

@ -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.

View File

@ -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);

View File

@ -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)];

View File

@ -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<EntryPoint[]>(
(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<T>(obj: {[key: string]: T}): T[] {
return Object.keys(obj).map(key => obj[key]);
}

View File

@ -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: '<h2>Hello</h2>'}] }
];`,
'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'));
}