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:
parent
23152c37c8
commit
5ced8fbbd5
|
@ -12,6 +12,7 @@ import {EntryPointJsonProperty, EntryPointPackageJson} from './src/packages/entr
|
||||||
export {ConsoleLogger, LogLevel} from './src/logging/console_logger';
|
export {ConsoleLogger, LogLevel} from './src/logging/console_logger';
|
||||||
export {Logger} from './src/logging/logger';
|
export {Logger} from './src/logging/logger';
|
||||||
export {NgccOptions, mainNgcc as process} from './src/main';
|
export {NgccOptions, mainNgcc as process} from './src/main';
|
||||||
|
export {PathMappings} from './src/utils';
|
||||||
|
|
||||||
export function hasBeenProcessed(packageJson: object, format: string) {
|
export function hasBeenProcessed(packageJson: object, format: string) {
|
||||||
// We are wrapping this function to hide the internal types.
|
// We are wrapping this function to hide the internal types.
|
||||||
|
|
|
@ -21,12 +21,12 @@ import {makeEntryPointBundle} from './packages/entry_point_bundle';
|
||||||
import {EntryPointFinder} from './packages/entry_point_finder';
|
import {EntryPointFinder} from './packages/entry_point_finder';
|
||||||
import {ModuleResolver} from './packages/module_resolver';
|
import {ModuleResolver} from './packages/module_resolver';
|
||||||
import {Transformer} from './packages/transformer';
|
import {Transformer} from './packages/transformer';
|
||||||
|
import {PathMappings} from './utils';
|
||||||
import {FileWriter} from './writing/file_writer';
|
import {FileWriter} from './writing/file_writer';
|
||||||
import {InPlaceFileWriter} from './writing/in_place_file_writer';
|
import {InPlaceFileWriter} from './writing/in_place_file_writer';
|
||||||
import {NewEntryPointFileWriter} from './writing/new_entry_point_file_writer';
|
import {NewEntryPointFileWriter} from './writing/new_entry_point_file_writer';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The options to configure the ngcc compiler.
|
* The options to configure the ngcc compiler.
|
||||||
*/
|
*/
|
||||||
|
@ -58,6 +58,11 @@ export interface NgccOptions {
|
||||||
* Provide a logger that will be called with log messages.
|
* Provide a logger that will be called with log messages.
|
||||||
*/
|
*/
|
||||||
logger?: Logger;
|
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'];
|
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.
|
* @param options The options telling ngcc what to compile and how.
|
||||||
*/
|
*/
|
||||||
export function mainNgcc({basePath, targetEntryPointPath,
|
export function mainNgcc(
|
||||||
propertiesToConsider = SUPPORTED_FORMAT_PROPERTIES,
|
{basePath, targetEntryPointPath, propertiesToConsider = SUPPORTED_FORMAT_PROPERTIES,
|
||||||
compileAllFormats = true, createNewEntryPointFormats = false,
|
compileAllFormats = true, createNewEntryPointFormats = false,
|
||||||
logger = new ConsoleLogger(LogLevel.info)}: NgccOptions): void {
|
logger = new ConsoleLogger(LogLevel.info), pathMappings}: NgccOptions): void {
|
||||||
const transformer = new Transformer(logger);
|
const transformer = new Transformer(logger);
|
||||||
const moduleResolver = new ModuleResolver();
|
const moduleResolver = new ModuleResolver(pathMappings);
|
||||||
const host = new DependencyHost(moduleResolver);
|
const host = new DependencyHost(moduleResolver);
|
||||||
const resolver = new DependencyResolver(logger, host);
|
const resolver = new DependencyResolver(logger, host);
|
||||||
const finder = new EntryPointFinder(logger, resolver);
|
const finder = new EntryPointFinder(logger, resolver);
|
||||||
|
@ -92,8 +97,8 @@ export function mainNgcc({basePath, targetEntryPointPath,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {entryPoints} =
|
const {entryPoints} = finder.findEntryPoints(
|
||||||
finder.findEntryPoints(AbsoluteFsPath.from(basePath), absoluteTargetEntryPointPath);
|
AbsoluteFsPath.from(basePath), absoluteTargetEntryPointPath, pathMappings);
|
||||||
|
|
||||||
if (absoluteTargetEntryPointPath && entryPoints.length === 0) {
|
if (absoluteTargetEntryPointPath && entryPoints.length === 0) {
|
||||||
markNonAngularPackageAsProcessed(absoluteTargetEntryPointPath, propertiesToConsider);
|
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.
|
// the property as processed even if its underlying format has been built already.
|
||||||
if (!compiledFormats.has(formatPath) && (compileAllFormats || isFirstFormat)) {
|
if (!compiledFormats.has(formatPath) && (compileAllFormats || isFirstFormat)) {
|
||||||
const bundle = makeEntryPointBundle(
|
const bundle = makeEntryPointBundle(
|
||||||
entryPoint.path, formatPath, entryPoint.typings, isCore, property, format, processDts);
|
entryPoint.path, formatPath, entryPoint.typings, isCore, property, format, processDts,
|
||||||
|
pathMappings);
|
||||||
if (bundle) {
|
if (bundle) {
|
||||||
logger.info(`Compiling ${entryPoint.name} : ${property} as ${format}`);
|
logger.info(`Compiling ${entryPoint.name} : ${property} as ${format}`);
|
||||||
const transformedFiles = transformer.transform(bundle);
|
const transformedFiles = transformer.transform(bundle);
|
||||||
|
|
|
@ -9,11 +9,11 @@ import {resolve} from 'canonical-path';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
|
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
|
||||||
|
import {PathMappings} from '../utils';
|
||||||
|
|
||||||
import {BundleProgram, makeBundleProgram} from './bundle_program';
|
import {BundleProgram, makeBundleProgram} from './bundle_program';
|
||||||
import {EntryPointFormat, EntryPointJsonProperty} from './entry_point';
|
import {EntryPointFormat, EntryPointJsonProperty} from './entry_point';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A bundle of files and paths (and TS programs) that correspond to a particular
|
* A bundle of files and paths (and TS programs) that correspond to a particular
|
||||||
* format of a package entry-point.
|
* format of a package entry-point.
|
||||||
|
@ -39,13 +39,13 @@ export interface EntryPointBundle {
|
||||||
*/
|
*/
|
||||||
export function makeEntryPointBundle(
|
export function makeEntryPointBundle(
|
||||||
entryPointPath: string, formatPath: string, typingsPath: string, isCore: boolean,
|
entryPointPath: string, formatPath: string, typingsPath: string, isCore: boolean,
|
||||||
formatProperty: EntryPointJsonProperty, format: EntryPointFormat,
|
formatProperty: EntryPointJsonProperty, format: EntryPointFormat, transformDts: boolean,
|
||||||
transformDts: boolean): EntryPointBundle|null {
|
pathMappings?: PathMappings): EntryPointBundle|null {
|
||||||
// Create the TS program and necessary helpers.
|
// Create the TS program and necessary helpers.
|
||||||
const options: ts.CompilerOptions = {
|
const options: ts.CompilerOptions = {
|
||||||
allowJs: true,
|
allowJs: true,
|
||||||
maxNodeModuleJsDepth: Infinity,
|
maxNodeModuleJsDepth: Infinity,
|
||||||
rootDir: entryPointPath,
|
rootDir: entryPointPath, ...pathMappings
|
||||||
};
|
};
|
||||||
const host = ts.createCompilerHost(options);
|
const host = ts.createCompilerHost(options);
|
||||||
const rootDirs = [AbsoluteFsPath.from(entryPointPath)];
|
const rootDirs = [AbsoluteFsPath.from(entryPointPath)];
|
||||||
|
|
|
@ -7,9 +7,11 @@
|
||||||
*/
|
*/
|
||||||
import * as path from 'canonical-path';
|
import * as path from 'canonical-path';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
import {join, resolve} from 'path';
|
||||||
|
|
||||||
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
|
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
|
||||||
import {Logger} from '../logging/logger';
|
import {Logger} from '../logging/logger';
|
||||||
|
import {PathMappings} from '../utils';
|
||||||
|
|
||||||
import {DependencyResolver, SortedEntryPointsInfo} from './dependency_resolver';
|
import {DependencyResolver, SortedEntryPointsInfo} from './dependency_resolver';
|
||||||
import {EntryPoint, getEntryPointInfo} from './entry_point';
|
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.
|
* 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.
|
* @param sourceDirectory An absolute path to the directory to search for entry points.
|
||||||
*/
|
*/
|
||||||
findEntryPoints(sourceDirectory: AbsoluteFsPath, targetEntryPointPath?: AbsoluteFsPath):
|
findEntryPoints(
|
||||||
SortedEntryPointsInfo {
|
sourceDirectory: AbsoluteFsPath, targetEntryPointPath?: AbsoluteFsPath,
|
||||||
const unsortedEntryPoints = this.walkDirectoryForEntryPoints(sourceDirectory);
|
pathMappings?: PathMappings): SortedEntryPointsInfo {
|
||||||
|
const basePaths = this.getBasePaths(sourceDirectory, pathMappings);
|
||||||
|
const unsortedEntryPoints = basePaths.reduce<EntryPoint[]>(
|
||||||
|
(entryPoints, basePath) => entryPoints.concat(this.walkDirectoryForEntryPoints(basePath)),
|
||||||
|
[]);
|
||||||
const targetEntryPoint = targetEntryPointPath ?
|
const targetEntryPoint = targetEntryPointPath ?
|
||||||
unsortedEntryPoints.find(entryPoint => entryPoint.path === targetEntryPointPath) :
|
unsortedEntryPoints.find(entryPoint => entryPoint.path === targetEntryPointPath) :
|
||||||
undefined;
|
undefined;
|
||||||
return this.resolver.sortEntryPointsByDependency(unsortedEntryPoints, targetEntryPoint);
|
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.
|
* 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/...`.
|
* 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]);
|
||||||
|
}
|
||||||
|
|
|
@ -309,6 +309,24 @@ describe('ngcc main()', () => {
|
||||||
expect(logger.logs.info).toContain(['Compiling @angular/common/http : esm2015 as esm2015']);
|
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"}',
|
'package.json': '{"name": "test-package", "es2015": "./index.js", "typings": "./index.d.ts"}',
|
||||||
// no metadata.json file so not compiled by Angular.
|
// no metadata.json file so not compiled by Angular.
|
||||||
'index.js':
|
'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':
|
'index.d.ts':
|
||||||
'import {AppModule} from "@angular/common"; export declare class MyApp extends AppModule;',
|
'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;
|
[pathSegment: string]: string|Directory;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadPackage(packageName: string): EntryPointPackageJson {
|
function loadPackage(packageName: string, basePath = '/node_modules'): EntryPointPackageJson {
|
||||||
return JSON.parse(readFileSync(`/node_modules/${packageName}/package.json`, 'utf8'));
|
return JSON.parse(readFileSync(`${basePath}/${packageName}/package.json`, 'utf8'));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue