feat(ivy): customize ngcc via configuration files (#30591)

There are scenarios where it is not possible for ngcc to guess the format
or configuration of an entry-point just from the files on disk.

Such scenarios include:

1) Unwanted entry-points: A spurious package.json makes ngcc think
there is an entry-point when there should not be one.

2) Deep-import entry-points: some packages allow deep-imports but do not
provide package.json files to indicate to ngcc that the imported path is
actually an entry-point to be processed.

3) Invalid/missing package.json properties: For example, an entry-point
that does not provide a valid property to a required format.

The configuration is provided by one or more `ngcc.config.js` files:

* If placed at the root of the project, this file can provide configuration
for named packages (and their entry-points) that have been npm installed
into the project.

* If published as part of a package, the file can provide configuration
for entry-points of the package.

The configured of a package at the project level will override any
configuration provided by the package itself.

PR Close #30591
This commit is contained in:
Pete Bacon Darwin 2019-05-21 15:23:24 +01:00 committed by Kara Erickson
parent 4004d15ba5
commit 7c4c676413
10 changed files with 655 additions and 73 deletions

View File

@ -5,7 +5,7 @@
* 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, getFileSystem, resolve} from '../../src/ngtsc/file_system';
import {AbsoluteFsPath, FileSystem, absoluteFrom, dirname, getFileSystem, resolve} from '../../src/ngtsc/file_system';
import {CommonJsDependencyHost} from './dependencies/commonjs_dependency_host';
import {DependencyResolver} from './dependencies/dependency_resolver';
import {EsmDependencyHost} from './dependencies/esm_dependency_host';
@ -14,6 +14,7 @@ import {UmdDependencyHost} from './dependencies/umd_dependency_host';
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 {EntryPointFormat, EntryPointJsonProperty, SUPPORTED_FORMAT_PROPERTIES, getEntryPointFormat} from './packages/entry_point';
import {makeEntryPointBundle} from './packages/entry_point_bundle';
import {EntryPointFinder} from './packages/entry_point_finder';
@ -92,7 +93,8 @@ export function mainNgcc(
umd: umdDependencyHost,
commonjs: commonJsDependencyHost
});
const finder = new EntryPointFinder(fileSystem, logger, resolver);
const config = new NgccConfiguration(fileSystem, dirname(absoluteFrom(basePath)));
const finder = new EntryPointFinder(fileSystem, config, logger, resolver);
const fileWriter = getFileWriter(fileSystem, createNewEntryPointFormats);
const absoluteTargetEntryPointPath =
@ -192,6 +194,10 @@ 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) {

View File

@ -5,7 +5,7 @@
* 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} from '../../../src/ngtsc/file_system';
import {AbsoluteFsPath, FileSystem, dirname} from '../../../src/ngtsc/file_system';
import {EntryPointJsonProperty, EntryPointPackageJson} from './entry_point';
export const NGCC_VERSION = '0.0.0-PLACEHOLDER';
@ -49,5 +49,8 @@ export function markAsProcessed(
format: EntryPointJsonProperty) {
if (!packageJson.__processed_by_ivy_ngcc__) packageJson.__processed_by_ivy_ngcc__ = {};
packageJson.__processed_by_ivy_ngcc__[format] = NGCC_VERSION;
// Just in case this package.json was synthesized due to a custom configuration
// we will ensure that the path to the containing folder exists before we write the file.
fs.ensureDir(dirname(packageJsonPath));
fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
}

View File

@ -0,0 +1,124 @@
/**
* @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 * as vm from 'vm';
import {AbsoluteFsPath, FileSystem, dirname, join, resolve} from '../../../src/ngtsc/file_system';
import {PackageJsonFormatProperties} from './entry_point';
/**
* The format of a project level configuration file.
*/
export interface NgccProjectConfig { packages: {[packagePath: string]: NgccPackageConfig}; }
/**
* The format of a package level configuration file.
*/
export interface NgccPackageConfig {
/**
* The entry-points to configure for this package.
*
* In the config file the keys can be paths relative to the package path;
* but when being read back from the `NgccConfiguration` service, these paths
* will be absolute.
*/
entryPoints: {[entryPointPath: string]: NgccEntryPointConfig;};
}
/**
* Configuration options for an entry-point.
*
* The existence of a configuration for a path tells ngcc that this should be considered for
* processing as an entry-point.
*/
export interface NgccEntryPointConfig {
/** Do not process (or even acknowledge the existence of) this entry-point, if true. */
ignore?: boolean;
/**
* This property, if provided, holds values that will override equivalent properties in an
* entry-point's package.json file.
*/
override?: PackageJsonFormatProperties;
}
const NGCC_CONFIG_FILENAME = 'ngcc.config.js';
export class NgccConfiguration {
// TODO: change string => ModuleSpecifier when we tighten the path types in #30556
private cache = new Map<string, NgccPackageConfig>();
constructor(private fs: FileSystem, baseDir: AbsoluteFsPath) {
const projectConfig = this.loadProjectConfig(baseDir);
for (const packagePath in projectConfig.packages) {
const absPackagePath = resolve(baseDir, 'node_modules', packagePath);
const packageConfig = projectConfig.packages[packagePath];
packageConfig.entryPoints =
this.processEntryPoints(absPackagePath, packageConfig.entryPoints);
this.cache.set(absPackagePath, packageConfig);
}
}
getConfig(packagePath: AbsoluteFsPath): NgccPackageConfig {
if (this.cache.has(packagePath)) {
return this.cache.get(packagePath) !;
}
const packageConfig = this.loadPackageConfig(packagePath);
packageConfig.entryPoints = this.processEntryPoints(packagePath, packageConfig.entryPoints);
this.cache.set(packagePath, packageConfig);
return packageConfig;
}
private loadProjectConfig(baseDir: AbsoluteFsPath): NgccProjectConfig {
const configFilePath = join(baseDir, NGCC_CONFIG_FILENAME);
if (this.fs.exists(configFilePath)) {
try {
return this.evalSrcFile(configFilePath);
} catch (e) {
throw new Error(`Invalid project configuration file at "${configFilePath}": ` + e.message);
}
} else {
return {packages: {}};
}
}
private loadPackageConfig(packagePath: AbsoluteFsPath): NgccPackageConfig {
const configFilePath = join(packagePath, NGCC_CONFIG_FILENAME);
if (this.fs.exists(configFilePath)) {
try {
return this.evalSrcFile(configFilePath);
} catch (e) {
throw new Error(`Invalid package configuration file at "${configFilePath}": ` + e.message);
}
} else {
return {entryPoints: {}};
}
}
private evalSrcFile(srcPath: AbsoluteFsPath): any {
const src = this.fs.readFile(srcPath);
const theExports = {};
const sandbox = {
module: {exports: theExports},
exports: theExports, require,
__dirname: dirname(srcPath),
__filename: srcPath
};
vm.runInNewContext(src, sandbox, {filename: srcPath});
return sandbox.module.exports;
}
private processEntryPoints(
packagePath: AbsoluteFsPath, entryPoints: {[entryPointPath: string]: NgccEntryPointConfig;}):
{[entryPointPath: string]: NgccEntryPointConfig;} {
const processedEntryPoints: {[entryPointPath: string]: NgccEntryPointConfig;} = {};
for (const entryPointPath in entryPoints) {
// Change the keys to be absolute paths
processedEntryPoints[resolve(packagePath, entryPointPath)] = entryPoints[entryPointPath];
}
return processedEntryPoints;
}
}

View File

@ -5,10 +5,13 @@
* 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 {relative} from 'canonical-path';
import {basename} from 'path';
import * as ts from 'typescript';
import {AbsoluteFsPath, FileSystem, join, resolve} from '../../../src/ngtsc/file_system';
import {parseStatementForUmdModule} from '../host/umd_host';
import {Logger} from '../logging/logger';
import {NgccConfiguration, NgccEntryPointConfig} from './configuration';
/**
* The possible values for the format of an entry-point.
@ -34,7 +37,7 @@ export interface EntryPoint {
compiledByAngular: boolean;
}
interface PackageJsonFormatProperties {
export interface PackageJsonFormatProperties {
fesm2015?: string;
fesm5?: string;
es2015?: string; // if exists then it is actually FESM2015
@ -67,18 +70,25 @@ export const SUPPORTED_FORMAT_PROPERTIES: EntryPointJsonProperty[] =
* @returns An entry-point if it is valid, `null` otherwise.
*/
export function getEntryPointInfo(
fs: FileSystem, logger: Logger, packagePath: AbsoluteFsPath,
fs: FileSystem, config: NgccConfiguration, logger: Logger, packagePath: AbsoluteFsPath,
entryPointPath: AbsoluteFsPath): EntryPoint|null {
const packageJsonPath = resolve(entryPointPath, 'package.json');
if (!fs.exists(packageJsonPath)) {
const entryPointConfig = config.getConfig(packagePath).entryPoints[entryPointPath];
if (entryPointConfig === undefined && !fs.exists(packageJsonPath)) {
return null;
}
const entryPointPackageJson = loadEntryPointPackage(fs, logger, packageJsonPath);
if (!entryPointPackageJson) {
if (entryPointConfig !== undefined && entryPointConfig.ignore === true) {
return null;
}
const loadedEntryPointPackageJson =
loadEntryPointPackage(fs, logger, packageJsonPath, entryPointConfig !== undefined);
const entryPointPackageJson = mergeConfigAndPackageJson(
loadedEntryPointPackageJson, entryPointConfig, packagePath, entryPointPath);
if (entryPointPackageJson === null) {
return null;
}
// We must have a typings property
const typings = entryPointPackageJson.typings || entryPointPackageJson.types;
@ -86,16 +96,18 @@ export function getEntryPointInfo(
return null;
}
// Also there must exist a `metadata.json` file next to the typings entry-point.
// An entry-point is assumed to be compiled by Angular if there is either:
// * a `metadata.json` file next to the typings entry-point
// * a custom config for this entry-point
const metadataPath = resolve(entryPointPath, typings.replace(/\.d\.ts$/, '') + '.metadata.json');
const compiledByAngular = entryPointConfig !== undefined || fs.exists(metadataPath);
const entryPointInfo: EntryPoint = {
name: entryPointPackageJson.name,
packageJson: entryPointPackageJson,
package: packagePath,
path: entryPointPath,
typings: resolve(entryPointPath, typings),
compiledByAngular: fs.exists(metadataPath),
typings: resolve(entryPointPath, typings), compiledByAngular,
};
return entryPointInfo;
@ -140,12 +152,15 @@ export function getEntryPointFormat(
* @returns JSON from the package.json file if it is valid, `null` otherwise.
*/
function loadEntryPointPackage(
fs: FileSystem, logger: Logger, packageJsonPath: AbsoluteFsPath): EntryPointPackageJson|null {
fs: FileSystem, logger: Logger, packageJsonPath: AbsoluteFsPath,
hasConfig: boolean): EntryPointPackageJson|null {
try {
return JSON.parse(fs.readFile(packageJsonPath));
} catch (e) {
// We may have run into a package.json with unexpected symbols
logger.warn(`Failed to read entry point info from ${packageJsonPath} with error ${e}.`);
if (!hasConfig) {
// We may have run into a package.json with unexpected symbols
logger.warn(`Failed to read entry point info from ${packageJsonPath} with error ${e}.`);
}
return null;
}
}
@ -156,3 +171,23 @@ function isUmdModule(fs: FileSystem, sourceFilePath: AbsoluteFsPath): boolean {
return sourceFile.statements.length > 0 &&
parseStatementForUmdModule(sourceFile.statements[0]) !== null;
}
function mergeConfigAndPackageJson(
entryPointPackageJson: EntryPointPackageJson | null,
entryPointConfig: NgccEntryPointConfig | undefined, packagePath: AbsoluteFsPath,
entryPointPath: AbsoluteFsPath): EntryPointPackageJson|null {
if (entryPointPackageJson !== null) {
if (entryPointConfig === undefined) {
return entryPointPackageJson;
} else {
return {...entryPointPackageJson, ...entryPointConfig.override};
}
} else {
if (entryPointConfig === undefined) {
return null;
} else {
const name = `${basename(packagePath)}/${relative(packagePath, entryPointPath)}`;
return {name, ...entryPointConfig.override};
}
}
}

View File

@ -9,12 +9,13 @@ import {AbsoluteFsPath, FileSystem, join, resolve} from '../../../src/ngtsc/file
import {DependencyResolver, SortedEntryPointsInfo} from '../dependencies/dependency_resolver';
import {Logger} from '../logging/logger';
import {PathMappings} from '../utils';
import {NgccConfiguration} from './configuration';
import {EntryPoint, getEntryPointInfo} from './entry_point';
export class EntryPointFinder {
constructor(
private fs: FileSystem, private logger: Logger, private resolver: DependencyResolver) {}
private fs: FileSystem, private config: NgccConfiguration, private logger: Logger,
private resolver: DependencyResolver) {}
/**
* 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.
@ -111,7 +112,8 @@ export class EntryPointFinder {
const entryPoints: EntryPoint[] = [];
// Try to get an entry point from the top level package directory
const topLevelEntryPoint = getEntryPointInfo(this.fs, this.logger, packagePath, packagePath);
const topLevelEntryPoint =
getEntryPointInfo(this.fs, this.config, this.logger, packagePath, packagePath);
// If there is no primary entry-point then exit
if (topLevelEntryPoint === null) {
@ -120,8 +122,11 @@ export class EntryPointFinder {
// Otherwise store it and search for secondary entry-points
entryPoints.push(topLevelEntryPoint);
this.walkDirectory(packagePath, subdir => {
const subEntryPoint = getEntryPointInfo(this.fs, this.logger, packagePath, subdir);
this.walkDirectory(packagePath, packagePath, (path, isDirectory) => {
// If the path is a JS file then strip its extension and see if we can match an entry-point.
const possibleEntryPointPath = isDirectory ? path : stripJsExtension(path);
const subEntryPoint =
getEntryPointInfo(this.fs, this.config, this.logger, packagePath, possibleEntryPointPath);
if (subEntryPoint !== null) {
entryPoints.push(subEntryPoint);
}
@ -136,22 +141,29 @@ export class EntryPointFinder {
* @param dir the directory to recursively walk.
* @param fn the function to apply to each directory.
*/
private walkDirectory(dir: AbsoluteFsPath, fn: (dir: AbsoluteFsPath) => void) {
private walkDirectory(
packagePath: AbsoluteFsPath, dir: AbsoluteFsPath,
fn: (path: AbsoluteFsPath, isDirectory: boolean) => void) {
return this.fs
.readdir(dir)
// Not interested in hidden files
.filter(p => !p.startsWith('.'))
.filter(path => !path.startsWith('.'))
// Ignore node_modules
.filter(p => p !== 'node_modules')
// Only interested in directories (and only those that are not symlinks)
.filter(p => {
const stat = this.fs.lstat(resolve(dir, p));
return stat.isDirectory() && !stat.isSymbolicLink();
})
.forEach(subDir => {
const resolvedSubDir = resolve(dir, subDir);
fn(resolvedSubDir);
this.walkDirectory(resolvedSubDir, fn);
.filter(path => path !== 'node_modules')
.map(path => resolve(dir, path))
.forEach(path => {
const stat = this.fs.lstat(path);
if (stat.isSymbolicLink()) {
// We are not interested in symbolic links
return;
}
fn(path, stat.isDirectory());
if (stat.isDirectory()) {
this.walkDirectory(packagePath, path, fn);
}
});
}
}
@ -187,3 +199,7 @@ function removeDeeperPaths(value: AbsoluteFsPath, index: number, array: Absolute
function values<T>(obj: {[key: string]: T}): T[] {
return Object.keys(obj).map(key => obj[key]);
}
function stripJsExtension<T extends string>(filePath: T): T {
return filePath.replace(/\.js$/, '') as T;
}

View File

@ -351,6 +351,92 @@ runInEachFileSystem(() => {
});
});
describe('with configuration files', () => {
it('should process a configured deep-import as an entry-point', () => {
loadTestFiles([
{
name: _('/ngcc.config.js'),
contents: `module.exports = { packages: {
'deep_import': {
entryPoints: {
'./entry_point': { override: { typings: '../entry_point.d.ts', es2015: '../entry_point.js' } }
}
}
}};`,
},
{
name: _('/node_modules/deep_import/package.json'),
contents: '{"name": "deep-import", "es2015": "./index.js", "typings": "./index.d.ts"}',
},
{
name: _('/node_modules/deep_import/entry_point.js'),
contents: `
import {Component} from '@angular/core';
@Component({selector: 'entry-point'})
export class EntryPoint {}
`,
},
{
name: _('/node_modules/deep_import/entry_point.d.ts'),
contents: `
import {Component} from '@angular/core';
@Component({selector: 'entry-point'})
export class EntryPoint {}
`,
},
]);
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'deep_import/entry_point',
propertiesToConsider: ['es2015']
});
// The containing package is not processed
expect(loadPackage('deep_import').__processed_by_ivy_ngcc__).toBeUndefined();
// But the configured entry-point and its dependency (@angular/core) are processed.
expect(loadPackage('deep_import/entry_point').__processed_by_ivy_ngcc__).toEqual({
es2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
es2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
});
it('should not process ignored entry-points', () => {
loadTestFiles([
{
name: _('/ngcc.config.js'),
contents: `module.exports = { packages: {
'@angular/core': {
entryPoints: {
'./testing': {ignore: true}
},
},
'@angular/common': {
entryPoints: {
'.': {ignore: true}
},
}
}};`,
},
]);
mainNgcc({basePath: '/node_modules', propertiesToConsider: ['es2015']});
// We process core but not core/testing.
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
es2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('@angular/core/testing').__processed_by_ivy_ngcc__).toBeUndefined();
// We do not compile common but we do compile its sub-entry-points.
expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__).toBeUndefined();
expect(loadPackage('@angular/common/http').__processed_by_ivy_ngcc__).toEqual({
es2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
});
});
function loadPackage(
packageName: string, basePath: AbsoluteFsPath = _('/node_modules')): EntryPointPackageJson {
return JSON.parse(fs.readFile(fs.resolve(basePath, packageName, 'package.json')));

View File

@ -0,0 +1,168 @@
/**
* @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 {FileSystem, absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {loadTestFiles} from '../../../test/helpers';
import {NgccConfiguration} from '../../src/packages/configuration';
runInEachFileSystem(() => {
let _Abs: typeof absoluteFrom;
let fs: FileSystem;
beforeEach(() => {
_Abs = absoluteFrom;
fs = getFileSystem();
});
describe('NgccConfiguration', () => {
describe('constructor', () => {
it('should error if a project level config file is badly formatted', () => {
loadTestFiles([{name: _Abs('/project-1/ngcc.config.js'), contents: `bad js code`}]);
expect(() => new NgccConfiguration(fs, _Abs('/project-1')))
.toThrowError(
`Invalid project configuration file at "${_Abs('/project-1/ngcc.config.js')}": Unexpected identifier`);
});
});
describe('getConfig()', () => {
it('should return configuration for a package found in a package level file', () => {
loadTestFiles([{
name: _Abs('/project-1/node_modules/package-1/ngcc.config.js'),
contents: `module.exports = {entryPoints: { './entry-point-1': {}}}`
}]);
const readFileSpy = spyOn(fs, 'readFile').and.callThrough();
const configuration = new NgccConfiguration(fs, _Abs('/project-1'));
const config = configuration.getConfig(_Abs('/project-1/node_modules/package-1'));
expect(config).toEqual(
{entryPoints: {[_Abs('/project-1/node_modules/package-1/entry-point-1')]: {}}});
expect(readFileSpy)
.toHaveBeenCalledWith(_Abs('/project-1/node_modules/package-1/ngcc.config.js'));
});
it('should cache configuration for a package found in a package level file', () => {
loadTestFiles([{
name: _Abs('/project-1/node_modules/package-1/ngcc.config.js'),
contents: `
module.exports = {
entryPoints: {
'./entry-point-1': {}
},
};`
}]);
const configuration = new NgccConfiguration(fs, _Abs('/project-1'));
// Populate the cache
configuration.getConfig(_Abs('/project-1/node_modules/package-1'));
const readFileSpy = spyOn(fs, 'readFile').and.callThrough();
const config = configuration.getConfig(_Abs('/project-1/node_modules/package-1'));
expect(config).toEqual(
{entryPoints: {[_Abs('/project-1/node_modules/package-1/entry-point-1')]: {}}});
expect(readFileSpy).not.toHaveBeenCalled();
});
it('should return an empty configuration object if there is no matching config file', () => {
const configuration = new NgccConfiguration(fs, _Abs('/project-1'));
const config = configuration.getConfig(_Abs('/project-1/node_modules/package-1'));
expect(config).toEqual({entryPoints: {}});
});
it('should error if a package level config file is badly formatted', () => {
loadTestFiles([{
name: _Abs('/project-1/node_modules/package-1/ngcc.config.js'),
contents: `bad js code`
}]);
const configuration = new NgccConfiguration(fs, _Abs('/project-1'));
expect(() => configuration.getConfig(_Abs('/project-1/node_modules/package-1')))
.toThrowError(
`Invalid package configuration file at "${_Abs('/project-1/node_modules/package-1/ngcc.config.js')}": Unexpected identifier`);
});
it('should return configuration for a package found in a project level file', () => {
loadTestFiles([{
name: _Abs('/project-1/ngcc.config.js'),
contents: `
module.exports = {
packages: {
'package-1': {
entryPoints: {
'./entry-point-1': {}
},
},
},
};`
}]);
const readFileSpy = spyOn(fs, 'readFile').and.callThrough();
const configuration = new NgccConfiguration(fs, _Abs('/project-1'));
expect(readFileSpy).toHaveBeenCalledWith(_Abs('/project-1/ngcc.config.js'));
const config = configuration.getConfig(_Abs('/project-1/node_modules/package-1'));
expect(config).toEqual(
{entryPoints: {[_Abs('/project-1/node_modules/package-1/entry-point-1')]: {}}});
});
it('should override package level config with project level config per package', () => {
loadTestFiles([
{
name: _Abs('/project-1/ngcc.config.js'),
contents: `
module.exports = {
packages: {
'package-2': {
entryPoints: {
'./project-setting-entry-point': {}
},
},
},
};`,
},
{
name: _Abs('/project-1/node_modules/package-1/ngcc.config.js'),
contents: `
module.exports = {
entryPoints: {
'./package-setting-entry-point': {}
},
};`,
},
{
name: _Abs('/project-1/node_modules/package-2/ngcc.config.js'),
contents: `
module.exports = {
entryPoints: {
'./package-setting-entry-point': {}
},
};`,
}
]);
const readFileSpy = spyOn(fs, 'readFile').and.callThrough();
const configuration = new NgccConfiguration(fs, _Abs('/project-1'));
expect(readFileSpy).toHaveBeenCalledWith(_Abs('/project-1/ngcc.config.js'));
const package1Config = configuration.getConfig(_Abs('/project-1/node_modules/package-1'));
expect(package1Config).toEqual({
entryPoints:
{[_Abs('/project-1/node_modules/package-1/package-setting-entry-point')]: {}}
});
expect(readFileSpy)
.toHaveBeenCalledWith(_Abs('/project-1/node_modules/package-1/ngcc.config.js'));
const package2Config = configuration.getConfig(_Abs('/project-1/node_modules/package-2'));
expect(package2Config).toEqual({
entryPoints:
{[_Abs('/project-1/node_modules/package-2/project-setting-entry-point')]: {}}
});
expect(readFileSpy)
.not.toHaveBeenCalledWith(_Abs('/project-1/node_modules/package-2/ngcc.config.js'));
});
});
});
});

View File

@ -11,6 +11,7 @@ import {loadTestFiles} from '../../../test/helpers';
import {DependencyResolver} from '../../src/dependencies/dependency_resolver';
import {EsmDependencyHost} from '../../src/dependencies/esm_dependency_host';
import {ModuleResolver} from '../../src/dependencies/module_resolver';
import {NgccConfiguration} from '../../src/packages/configuration';
import {EntryPoint} from '../../src/packages/entry_point';
import {EntryPointFinder} from '../../src/packages/entry_point_finder';
import {MockLogger} from '../helpers/mock_logger';
@ -31,7 +32,8 @@ runInEachFileSystem(() => {
spyOn(resolver, 'sortEntryPointsByDependency').and.callFake((entryPoints: EntryPoint[]) => {
return {entryPoints, ignoredEntryPoints: [], ignoredDependencies: []};
});
finder = new EntryPointFinder(fs, new MockLogger(), resolver);
finder =
new EntryPointFinder(fs, new NgccConfiguration(fs, _('/')), new MockLogger(), resolver);
});
it('should find sub-entry-points within a package', () => {

View File

@ -9,6 +9,7 @@
import {AbsoluteFsPath, FileSystem, absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {loadTestFiles} from '../../../test/helpers';
import {NgccConfiguration} from '../../src/packages/configuration';
import {getEntryPointInfo} from '../../src/packages/entry_point';
import {MockLogger} from '../helpers/mock_logger';
@ -19,8 +20,8 @@ runInEachFileSystem(() => {
let fs: FileSystem;
beforeEach(() => {
SOME_PACKAGE = absoluteFrom('/some_package');
_ = absoluteFrom;
SOME_PACKAGE = _('/project/node_modules/some_package');
fs = getFileSystem();
});
@ -28,51 +29,152 @@ runInEachFileSystem(() => {
() => {
loadTestFiles([
{
name: _('/some_package/valid_entry_point/package.json'),
name: _('/project/node_modules/some_package/valid_entry_point/package.json'),
contents: createPackageJson('valid_entry_point')
},
{
name: _('/some_package/valid_entry_point/valid_entry_point.metadata.json'),
name: _(
'/project/node_modules/some_package/valid_entry_point/valid_entry_point.metadata.json'),
contents: 'some meta data'
},
]);
const config = new NgccConfiguration(fs, _('/project'));
const entryPoint = getEntryPointInfo(
fs, new MockLogger(), SOME_PACKAGE, _('/some_package/valid_entry_point'));
fs, config, new MockLogger(), SOME_PACKAGE,
_('/project/node_modules/some_package/valid_entry_point'));
expect(entryPoint).toEqual({
name: 'some-package/valid_entry_point',
name: 'some_package/valid_entry_point',
package: SOME_PACKAGE,
path: _('/some_package/valid_entry_point'),
typings: _(`/some_package/valid_entry_point/valid_entry_point.d.ts`),
packageJson: loadPackageJson(fs, '/some_package/valid_entry_point'),
path: _('/project/node_modules/some_package/valid_entry_point'),
typings:
_(`/project/node_modules/some_package/valid_entry_point/valid_entry_point.d.ts`),
packageJson: loadPackageJson(fs, '/project/node_modules/some_package/valid_entry_point'),
compiledByAngular: true,
});
});
it('should return null if configured to ignore the specified entry-point', () => {
loadTestFiles([
{
name: _('/project/node_modules/some_package/valid_entry_point/package.json'),
contents: createPackageJson('valid_entry_point'),
},
{
name: _(
'/project/node_modules/some_package/valid_entry_point/valid_entry_point.metadata.json'),
contents: 'some meta data',
},
]);
const config = new NgccConfiguration(fs, _('/project'));
spyOn(config, 'getConfig').and.returnValue({
entryPoints:
{[_('/project/node_modules/some_package/valid_entry_point')]: {ignore: true}}
});
const entryPoint = getEntryPointInfo(
fs, config, new MockLogger(), SOME_PACKAGE,
_('/project/node_modules/some_package/valid_entry_point'));
expect(entryPoint).toBe(null);
});
it('should override the properties on package.json if the entry-point is configured', () => {
loadTestFiles([
{
name: _('/project/node_modules/some_package/valid_entry_point/package.json'),
contents: createPackageJson('valid_entry_point'),
},
{
name: _(
'/project/node_modules/some_package/valid_entry_point/valid_entry_point.metadata.json'),
contents: 'some meta data',
},
]);
const config = new NgccConfiguration(fs, _('/project'));
const override = {
typings: './some_other.d.ts',
esm2015: './some_other.js',
};
spyOn(config, 'getConfig').and.returnValue({
entryPoints: {[_('/project/node_modules/some_package/valid_entry_point')]: {override}}
});
const entryPoint = getEntryPointInfo(
fs, config, new MockLogger(), SOME_PACKAGE,
_('/project/node_modules/some_package/valid_entry_point'));
const overriddenPackageJson = {
...loadPackageJson(fs, '/project/node_modules/some_package/valid_entry_point'),
...override};
expect(entryPoint).toEqual({
name: 'some_package/valid_entry_point',
package: SOME_PACKAGE,
path: _('/project/node_modules/some_package/valid_entry_point'),
typings: _('/project/node_modules/some_package/valid_entry_point/some_other.d.ts'),
packageJson: overriddenPackageJson,
compiledByAngular: true,
});
});
it('should return null if there is no package.json at the entry-point path', () => {
loadTestFiles([
{
name: _('/some_package/missing_package_json/missing_package_json.metadata.json'),
name: _(
'/project/node_modules/some_package/missing_package_json/missing_package_json.metadata.json'),
contents: 'some meta data'
},
]);
const config = new NgccConfiguration(fs, _('/project'));
const entryPoint = getEntryPointInfo(
fs, new MockLogger(), SOME_PACKAGE, _('/some_package/missing_package_json'));
fs, config, new MockLogger(), SOME_PACKAGE,
_('/project/node_modules/some_package/missing_package_json'));
expect(entryPoint).toBe(null);
});
it('should return a configured entry-point if there is no package.json at the entry-point path',
() => {
loadTestFiles([
// no package.json!
{
name: _(
'/project/node_modules/some_package/missing_package_json/missing_package_json.metadata.json'),
contents: 'some meta data',
},
]);
const config = new NgccConfiguration(fs, _('/project'));
const override =
JSON.parse(createPackageJson('missing_package_json', {excludes: ['name']}));
spyOn(config, 'getConfig').and.returnValue({
entryPoints:
{[_('/project/node_modules/some_package/missing_package_json')]: {override}}
});
const entryPoint = getEntryPointInfo(
fs, config, new MockLogger(), SOME_PACKAGE,
_('/project/node_modules/some_package/missing_package_json'));
expect(entryPoint).toEqual({
name: 'some_package/missing_package_json',
package: SOME_PACKAGE,
path: _('/project/node_modules/some_package/missing_package_json'),
typings: _(
'/project/node_modules/some_package/missing_package_json/missing_package_json.d.ts'),
packageJson: {name: 'some_package/missing_package_json', ...override},
compiledByAngular: true,
});
});
it('should return null if there is no typings or types field in the package.json', () => {
loadTestFiles([
{
name: _('/some_package/missing_typings/package.json'),
name: _('/project/node_modules/some_package/missing_typings/package.json'),
contents: createPackageJson('missing_typings', {excludes: ['typings']})
},
{
name: _('/some_package/missing_typings/missing_typings.metadata.json'),
name:
_('/project/node_modules/some_package/missing_typings/missing_typings.metadata.json'),
contents: 'some meta data'
},
]);
const entryPoint =
getEntryPointInfo(fs, new MockLogger(), SOME_PACKAGE, _('/some_package/missing_typings'));
const config = new NgccConfiguration(fs, _('/project'));
const entryPoint = getEntryPointInfo(
fs, config, new MockLogger(), SOME_PACKAGE,
_('/project/node_modules/some_package/missing_typings'));
expect(entryPoint).toBe(null);
});
@ -80,42 +182,74 @@ runInEachFileSystem(() => {
() => {
loadTestFiles([
{
name: _('/some_package/missing_metadata/package.json'),
name: _('/project/node_modules/some_package/missing_metadata/package.json'),
contents: createPackageJson('missing_metadata')
},
]);
const config = new NgccConfiguration(fs, _('/project'));
const entryPoint = getEntryPointInfo(
fs, new MockLogger(), SOME_PACKAGE, _('/some_package/missing_metadata'));
fs, config, new MockLogger(), SOME_PACKAGE,
_('/project/node_modules/some_package/missing_metadata'));
expect(entryPoint).toEqual({
name: 'some-package/missing_metadata',
name: 'some_package/missing_metadata',
package: SOME_PACKAGE,
path: _('/some_package/missing_metadata'),
typings: _(`/some_package/missing_metadata/missing_metadata.d.ts`),
packageJson: loadPackageJson(fs, '/some_package/missing_metadata'),
path: _('/project/node_modules/some_package/missing_metadata'),
typings: _(`/project/node_modules/some_package/missing_metadata/missing_metadata.d.ts`),
packageJson: loadPackageJson(fs, '/project/node_modules/some_package/missing_metadata'),
compiledByAngular: false,
});
});
it('should return an object with `compiledByAngular` set to true if there is no metadata.json file but the entry-point has a configuration',
() => {
loadTestFiles([
{
name: _('/project/node_modules/some_package/missing_metadata/package.json'),
contents: createPackageJson('missing_metadata'),
},
// no metadata.json!
]);
const config = new NgccConfiguration(fs, _('/project'));
spyOn(config, 'getConfig').and.returnValue({
entryPoints: {[_('/project/node_modules/some_package/missing_metadata')]: {}}
});
const entryPoint = getEntryPointInfo(
fs, config, new MockLogger(), SOME_PACKAGE,
_('/project/node_modules/some_package/missing_metadata'));
expect(entryPoint).toEqual({
name: 'some_package/missing_metadata',
package: SOME_PACKAGE,
path: _('/project/node_modules/some_package/missing_metadata'),
typings: _('/project/node_modules/some_package/missing_metadata/missing_metadata.d.ts'),
packageJson: loadPackageJson(fs, '/project/node_modules/some_package/missing_metadata'),
compiledByAngular: true,
});
});
it('should work if the typings field is named `types', () => {
loadTestFiles([
{
name: _('/some_package/types_rather_than_typings/package.json'),
name: _('/project/node_modules/some_package/types_rather_than_typings/package.json'),
contents: createPackageJson('types_rather_than_typings', {}, 'types')
},
{
name:
_('/some_package/types_rather_than_typings/types_rather_than_typings.metadata.json'),
name: _(
'/project/node_modules/some_package/types_rather_than_typings/types_rather_than_typings.metadata.json'),
contents: 'some meta data'
},
]);
const config = new NgccConfiguration(fs, _('/project'));
const entryPoint = getEntryPointInfo(
fs, new MockLogger(), SOME_PACKAGE, _('/some_package/types_rather_than_typings'));
fs, config, new MockLogger(), SOME_PACKAGE,
_('/project/node_modules/some_package/types_rather_than_typings'));
expect(entryPoint).toEqual({
name: 'some-package/types_rather_than_typings',
name: 'some_package/types_rather_than_typings',
package: SOME_PACKAGE,
path: _('/some_package/types_rather_than_typings'),
typings: _(`/some_package/types_rather_than_typings/types_rather_than_typings.d.ts`),
packageJson: loadPackageJson(fs, '/some_package/types_rather_than_typings'),
path: _('/project/node_modules/some_package/types_rather_than_typings'),
typings: _(
`/project/node_modules/some_package/types_rather_than_typings/types_rather_than_typings.d.ts`),
packageJson:
loadPackageJson(fs, '/project/node_modules/some_package/types_rather_than_typings'),
compiledByAngular: true,
});
});
@ -123,7 +257,7 @@ runInEachFileSystem(() => {
it('should work with Angular Material style package.json', () => {
loadTestFiles([
{
name: _('/some_package/material_style/package.json'),
name: _('/project/node_modules/some_package/material_style/package.json'),
contents: `{
"name": "some_package/material_style",
"typings": "./material_style.d.ts",
@ -133,18 +267,20 @@ runInEachFileSystem(() => {
}`
},
{
name: _('/some_package/material_style/material_style.metadata.json'),
name: _('/project/node_modules/some_package/material_style/material_style.metadata.json'),
contents: 'some meta data'
},
]);
const entryPoint =
getEntryPointInfo(fs, new MockLogger(), SOME_PACKAGE, _('/some_package/material_style'));
const config = new NgccConfiguration(fs, _('/project'));
const entryPoint = getEntryPointInfo(
fs, config, new MockLogger(), SOME_PACKAGE,
_('/project/node_modules/some_package/material_style'));
expect(entryPoint).toEqual({
name: 'some_package/material_style',
package: SOME_PACKAGE,
path: _('/some_package/material_style'),
typings: _(`/some_package/material_style/material_style.d.ts`),
packageJson: loadPackageJson(fs, '/some_package/material_style'),
path: _('/project/node_modules/some_package/material_style'),
typings: _(`/project/node_modules/some_package/material_style/material_style.d.ts`),
packageJson: loadPackageJson(fs, '/project/node_modules/some_package/material_style'),
compiledByAngular: true,
});
});
@ -155,12 +291,14 @@ runInEachFileSystem(() => {
// for example, @schematics/angular contains a package.json blueprint
// with unexpected symbols
{
name: _('/some_package/unexpected_symbols/package.json'),
name: _('/project/node_modules/some_package/unexpected_symbols/package.json'),
contents: '{"devDependencies": {<% if (!minimal) { %>"@types/jasmine": "~2.8.8" <% } %>}}'
},
]);
const config = new NgccConfiguration(fs, _('/project'));
const entryPoint = getEntryPointInfo(
fs, new MockLogger(), SOME_PACKAGE, _('/some_package/unexpected_symbols'));
fs, config, new MockLogger(), SOME_PACKAGE,
_('/project/node_modules/some_package/unexpected_symbols'));
expect(entryPoint).toBe(null);
});
});
@ -169,7 +307,7 @@ runInEachFileSystem(() => {
packageName: string, {excludes}: {excludes?: string[]} = {},
typingsProp: string = 'typings'): string {
const packageJson: any = {
name: `some-package/${packageName}`,
name: `some_package/${packageName}`,
[typingsProp]: `./${packageName}.d.ts`,
fesm2015: `./fesm2015/${packageName}.js`,
esm2015: `./esm2015/${packageName}.js`,

View File

@ -8,6 +8,7 @@
import {FileSystem, absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {loadTestFiles} from '../../../test/helpers';
import {NgccConfiguration} from '../../src/packages/configuration';
import {EntryPoint, EntryPointFormat, EntryPointJsonProperty, getEntryPointInfo} from '../../src/packages/entry_point';
import {EntryPointBundle, makeEntryPointBundle} from '../../src/packages/entry_point_bundle';
import {FileWriter} from '../../src/writing/file_writer';
@ -86,8 +87,9 @@ runInEachFileSystem(() => {
beforeEach(() => {
fs = getFileSystem();
fileWriter = new NewEntryPointFileWriter(fs);
const config = new NgccConfiguration(fs, _('/'));
entryPoint = getEntryPointInfo(
fs, new MockLogger(), _('/node_modules/test'), _('/node_modules/test')) !;
fs, config, new MockLogger(), _('/node_modules/test'), _('/node_modules/test')) !;
esm5bundle = makeTestBundle(fs, entryPoint, 'module', 'esm5');
esm2015bundle = makeTestBundle(fs, entryPoint, 'es2015', 'esm2015');
});
@ -174,8 +176,9 @@ runInEachFileSystem(() => {
beforeEach(() => {
fs = getFileSystem();
fileWriter = new NewEntryPointFileWriter(fs);
const config = new NgccConfiguration(fs, _('/'));
entryPoint = getEntryPointInfo(
fs, new MockLogger(), _('/node_modules/test'), _('/node_modules/test/a')) !;
fs, config, new MockLogger(), _('/node_modules/test'), _('/node_modules/test/a')) !;
esm5bundle = makeTestBundle(fs, entryPoint, 'module', 'esm5');
esm2015bundle = makeTestBundle(fs, entryPoint, 'es2015', 'esm2015');
});
@ -251,8 +254,9 @@ runInEachFileSystem(() => {
beforeEach(() => {
fs = getFileSystem();
fileWriter = new NewEntryPointFileWriter(fs);
const config = new NgccConfiguration(fs, _('/'));
entryPoint = getEntryPointInfo(
fs, new MockLogger(), _('/node_modules/test'), _('/node_modules/test/b')) !;
fs, config, new MockLogger(), _('/node_modules/test'), _('/node_modules/test/b')) !;
esm5bundle = makeTestBundle(fs, entryPoint, 'module', 'esm5');
esm2015bundle = makeTestBundle(fs, entryPoint, 'es2015', 'esm2015');
});