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

@ -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`,