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 {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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)];
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue