feat(ngcc): support fallback to a default configuration (#33008)

It is now possible to include a set of default ngcc configurations
that ship with ngcc out of the box. This allows ngcc to handle a
set of common packages, which are unlikely to be fixed, without
requiring the application developer to write their own configuration
for them.

Any packages that are configured at the package or project level
will override these default configurations. This allows a reasonable
level of control at the package and user level.

PR Close #33008
This commit is contained in:
Pete Bacon Darwin 2019-10-04 11:54:33 +01:00 committed by Miško Hevery
parent 35a95a8a7e
commit 916762440c
2 changed files with 239 additions and 101 deletions

View File

@ -44,13 +44,50 @@ export interface NgccEntryPointConfig {
override?: PackageJsonFormatPropertiesMap; override?: PackageJsonFormatPropertiesMap;
} }
/**
* The default configuration for ngcc.
*
* This is the ultimate fallback configuration that ngcc will use if there is no configuration
* for a package at the package level or project level.
*
* This configuration is for packages that are "dead" - i.e. no longer maintained and so are
* unlikely to be fixed to work with ngcc, nor provide a package level config of their own.
*
* The fallback process for looking up configuration is:
*
* Project -> Package -> Default
*
* If a package provides its own configuration then that would override this default one.
*
* Also application developers can always provide configuration at their project level which
* will override everything else.
*
* Note that the fallback is package based not entry-point based.
* For example, if a there is configuration for a package at the project level this will replace all
* entry-point configurations that may have been provided in the package level or default level
* configurations, even if the project level configuration does not provide for a given entry-point.
*/
export const DEFAULT_NGCC_CONFIG: NgccProjectConfig = {
packages: {
// Add default package configuration here. For example:
// '@angular/fire@^5.2.0': {
// entryPoints: {
// './database-deprecated': {
// ignore: true,
// },
// },
// },
}
};
const NGCC_CONFIG_FILENAME = 'ngcc.config.js'; const NGCC_CONFIG_FILENAME = 'ngcc.config.js';
export class NgccConfiguration { export class NgccConfiguration {
// TODO: change string => ModuleSpecifier when we tighten the path types in #30556 private defaultConfig: NgccProjectConfig;
private cache = new Map<string, NgccPackageConfig>(); private cache = new Map<string, NgccPackageConfig>();
constructor(private fs: FileSystem, baseDir: AbsoluteFsPath) { constructor(private fs: FileSystem, baseDir: AbsoluteFsPath) {
this.defaultConfig = this.processDefaultConfig(baseDir);
const projectConfig = this.loadProjectConfig(baseDir); const projectConfig = this.loadProjectConfig(baseDir);
for (const packagePath in projectConfig.packages) { for (const packagePath in projectConfig.packages) {
const absPackagePath = resolve(baseDir, 'node_modules', packagePath); const absPackagePath = resolve(baseDir, 'node_modules', packagePath);
@ -66,12 +103,26 @@ export class NgccConfiguration {
return this.cache.get(packagePath) !; return this.cache.get(packagePath) !;
} }
const packageConfig = this.loadPackageConfig(packagePath); const packageConfig = this.loadPackageConfig(packagePath) ||
packageConfig.entryPoints = this.processEntryPoints(packagePath, packageConfig.entryPoints); this.defaultConfig.packages[packagePath] || {entryPoints: {}};
this.cache.set(packagePath, packageConfig); this.cache.set(packagePath, packageConfig);
return packageConfig; return packageConfig;
} }
private processDefaultConfig(baseDir: AbsoluteFsPath): NgccProjectConfig {
const defaultConfig: NgccProjectConfig = {packages: {}};
for (const packagePath in DEFAULT_NGCC_CONFIG.packages) {
const absPackagePath = resolve(baseDir, 'node_modules', packagePath);
const packageConfig = DEFAULT_NGCC_CONFIG.packages[packagePath];
if (packageConfig) {
packageConfig.entryPoints =
this.processEntryPoints(absPackagePath, packageConfig.entryPoints);
defaultConfig.packages[absPackagePath] = packageConfig;
}
}
return defaultConfig;
}
private loadProjectConfig(baseDir: AbsoluteFsPath): NgccProjectConfig { private loadProjectConfig(baseDir: AbsoluteFsPath): NgccProjectConfig {
const configFilePath = join(baseDir, NGCC_CONFIG_FILENAME); const configFilePath = join(baseDir, NGCC_CONFIG_FILENAME);
if (this.fs.exists(configFilePath)) { if (this.fs.exists(configFilePath)) {
@ -85,16 +136,18 @@ export class NgccConfiguration {
} }
} }
private loadPackageConfig(packagePath: AbsoluteFsPath): NgccPackageConfig { private loadPackageConfig(packagePath: AbsoluteFsPath): NgccPackageConfig|null {
const configFilePath = join(packagePath, NGCC_CONFIG_FILENAME); const configFilePath = join(packagePath, NGCC_CONFIG_FILENAME);
if (this.fs.exists(configFilePath)) { if (this.fs.exists(configFilePath)) {
try { try {
return this.evalSrcFile(configFilePath); const packageConfig = this.evalSrcFile(configFilePath);
packageConfig.entryPoints = this.processEntryPoints(packagePath, packageConfig.entryPoints);
return packageConfig;
} catch (e) { } catch (e) {
throw new Error(`Invalid package configuration file at "${configFilePath}": ` + e.message); throw new Error(`Invalid package configuration file at "${configFilePath}": ` + e.message);
} }
} else { } else {
return {entryPoints: {}}; return null;
} }
} }

View File

@ -8,7 +8,7 @@
import {FileSystem, absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system'; import {FileSystem, absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {loadTestFiles} from '../../../test/helpers'; import {loadTestFiles} from '../../../test/helpers';
import {NgccConfiguration} from '../../src/packages/configuration'; import {DEFAULT_NGCC_CONFIG, NgccConfiguration} from '../../src/packages/configuration';
runInEachFileSystem(() => { runInEachFileSystem(() => {
@ -31,65 +31,69 @@ runInEachFileSystem(() => {
}); });
describe('getConfig()', () => { describe('getConfig()', () => {
it('should return configuration for a package found in a package level file', () => { describe('at the package level', () => {
loadTestFiles([{ it('should return configuration for a package found in a package level file', () => {
name: _Abs('/project-1/node_modules/package-1/ngcc.config.js'), loadTestFiles([{
contents: `module.exports = {entryPoints: { './entry-point-1': {}}}` 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 readFileSpy = spyOn(fs, 'readFile').and.callThrough();
const config = configuration.getConfig(_Abs('/project-1/node_modules/package-1')); const configuration = new NgccConfiguration(fs, _Abs('/project-1'));
const config = configuration.getConfig(_Abs('/project-1/node_modules/package-1'));
expect(config).toEqual( expect(config).toEqual(
{entryPoints: {[_Abs('/project-1/node_modules/package-1/entry-point-1')]: {}}}); {entryPoints: {[_Abs('/project-1/node_modules/package-1/entry-point-1')]: {}}});
expect(readFileSpy) expect(readFileSpy)
.toHaveBeenCalledWith(_Abs('/project-1/node_modules/package-1/ngcc.config.js')); .toHaveBeenCalledWith(_Abs('/project-1/node_modules/package-1/ngcc.config.js'));
}); });
it('should cache configuration for a package found in a package level file', () => { it('should used cached configuration for a package if available', () => {
loadTestFiles([{ loadTestFiles([{
name: _Abs('/project-1/node_modules/package-1/ngcc.config.js'), name: _Abs('/project-1/node_modules/package-1/ngcc.config.js'),
contents: ` contents: `
module.exports = { module.exports = {
entryPoints: { entryPoints: {
'./entry-point-1': {} './entry-point-1': {}
}, },
};` };`
}]); }]);
const configuration = new NgccConfiguration(fs, _Abs('/project-1')); const configuration = new NgccConfiguration(fs, _Abs('/project-1'));
// Populate the cache // Populate the cache
configuration.getConfig(_Abs('/project-1/node_modules/package-1')); configuration.getConfig(_Abs('/project-1/node_modules/package-1'));
const readFileSpy = spyOn(fs, 'readFile').and.callThrough(); const readFileSpy = spyOn(fs, 'readFile').and.callThrough();
const config = configuration.getConfig(_Abs('/project-1/node_modules/package-1')); const config = configuration.getConfig(_Abs('/project-1/node_modules/package-1'));
expect(config).toEqual( expect(config).toEqual(
{entryPoints: {[_Abs('/project-1/node_modules/package-1/entry-point-1')]: {}}}); {entryPoints: {[_Abs('/project-1/node_modules/package-1/entry-point-1')]: {}}});
expect(readFileSpy).not.toHaveBeenCalled(); expect(readFileSpy).not.toHaveBeenCalled();
});
it('should return an empty configuration object if there is no matching configuration for the package',
() => {
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 an empty configuration object if there is no matching config file', () => { describe('at the project level', () => {
const configuration = new NgccConfiguration(fs, _Abs('/project-1')); it('should return configuration for a package found in a project level file', () => {
const config = configuration.getConfig(_Abs('/project-1/node_modules/package-1')); loadTestFiles([{
expect(config).toEqual({entryPoints: {}}); name: _Abs('/project-1/ngcc.config.js'),
}); contents: `
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 = { module.exports = {
packages: { packages: {
'package-1': { 'package-1': {
@ -99,21 +103,21 @@ runInEachFileSystem(() => {
}, },
}, },
};` };`
}]); }]);
const readFileSpy = spyOn(fs, 'readFile').and.callThrough(); const readFileSpy = spyOn(fs, 'readFile').and.callThrough();
const configuration = new NgccConfiguration(fs, _Abs('/project-1')); const configuration = new NgccConfiguration(fs, _Abs('/project-1'));
expect(readFileSpy).toHaveBeenCalledWith(_Abs('/project-1/ngcc.config.js')); expect(readFileSpy).toHaveBeenCalledWith(_Abs('/project-1/ngcc.config.js'));
const config = configuration.getConfig(_Abs('/project-1/node_modules/package-1')); const config = configuration.getConfig(_Abs('/project-1/node_modules/package-1'));
expect(config).toEqual( expect(config).toEqual(
{entryPoints: {[_Abs('/project-1/node_modules/package-1/entry-point-1')]: {}}}); {entryPoints: {[_Abs('/project-1/node_modules/package-1/entry-point-1')]: {}}});
}); });
it('should override package level config with project level config per package', () => { it('should override package level config with project level config per package', () => {
loadTestFiles([ loadTestFiles([
{ {
name: _Abs('/project-1/ngcc.config.js'), name: _Abs('/project-1/ngcc.config.js'),
contents: ` contents: `
module.exports = { module.exports = {
packages: { packages: {
'package-2': { 'package-2': {
@ -123,45 +127,126 @@ runInEachFileSystem(() => {
}, },
}, },
};`, };`,
}, },
{ {
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'));
// Note that for `package-2` only the project level entry-point is left.
// This is because overriding happens for packages as a whole and there is no attempt to
// merge entry-points.
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'));
});
});
describe('at the default level', () => {
const originalDefaultConfig = DEFAULT_NGCC_CONFIG.packages['package-1'];
beforeEach(() => {
DEFAULT_NGCC_CONFIG.packages['package-1'] = {
entryPoints: {'./default-level-entry-point': {}},
};
});
afterEach(() => { DEFAULT_NGCC_CONFIG.packages['package-1'] = originalDefaultConfig; });
it('should return configuration for a package found in the default config', () => {
const readFileSpy = spyOn(fs, 'readFile').and.callThrough();
const configuration = new NgccConfiguration(fs, _Abs('/project-1'));
expect(readFileSpy).not.toHaveBeenCalled();
const config = configuration.getConfig(_Abs('/project-1/node_modules/package-1'));
expect(config).toEqual({
entryPoints:
{[_Abs('/project-1/node_modules/package-1/default-level-entry-point')]: {}}
});
});
it('should override default level config with package level config, if provided', () => {
loadTestFiles([{
name: _Abs('/project-1/node_modules/package-1/ngcc.config.js'), name: _Abs('/project-1/node_modules/package-1/ngcc.config.js'),
contents: ` contents: `
module.exports = { module.exports = {
entryPoints: { entryPoints: {'./package-level-entry-point': {}},
'./package-setting-entry-point': {} };`,
}, }]);
};`, const configuration = new NgccConfiguration(fs, _Abs('/project-1'));
}, const config = configuration.getConfig(_Abs('/project-1/node_modules/package-1'));
{ // Note that only the package-level-entry-point is left.
name: _Abs('/project-1/node_modules/package-2/ngcc.config.js'), // This is because overriding happens for packages as a whole and there is no attempt to
contents: ` // merge entry-points.
module.exports = { expect(config).toEqual({
entryPoints: { entryPoints:
'./package-setting-entry-point': {} {[_Abs('/project-1/node_modules/package-1/package-level-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')); it('should override default level config with project level config, if provided', () => {
expect(package2Config).toEqual({ loadTestFiles([
entryPoints: {
{[_Abs('/project-1/node_modules/package-2/project-setting-entry-point')]: {}} name: _Abs('/project-1/node_modules/package-1/ngcc.config.js'),
contents: `
module.exports = {
entryPoints: {'./package-level-entry-point': {}},
};`,
},
{
name: _Abs('/project-1/ngcc.config.js'),
contents: `
module.exports = {
packages: {
'package-1': {
entryPoints: {
'./project-level-entry-point': {}
},
},
},
};`,
},
]);
const configuration = new NgccConfiguration(fs, _Abs('/project-1'));
const config = configuration.getConfig(_Abs('/project-1/node_modules/package-1'));
// Note that only the project-level-entry-point is left.
// This is because overriding happens for packages as a whole and there is no attempt to
// merge entry-points.
expect(config).toEqual({
entryPoints:
{[_Abs('/project-1/node_modules/package-1/project-level-entry-point')]: {}}
});
}); });
expect(readFileSpy)
.not.toHaveBeenCalledWith(_Abs('/project-1/node_modules/package-2/ngcc.config.js'));
}); });
}); });
}); });