feat(ngcc): support version ranges in project/default configurations (#33008)

By appending a version range to the package name, it is now possible to
target configuration to specific versions of a package.

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 916762440c
commit 90007e97ca
7 changed files with 308 additions and 128 deletions

View File

@ -61,6 +61,7 @@
"@types/minimist": "^1.2.0",
"@types/node": "^10.9.4",
"@types/selenium-webdriver": "3.0.7",
"@types/semver": "^6.0.2",
"@types/shelljs": "^0.7.8",
"@types/systemjs": "0.19.32",
"@types/yargs": "^11.1.1",
@ -159,7 +160,7 @@
"mutation-observer": "^1.0.3",
"rewire": "2.5.2",
"sauce-connect": "https://saucelabs.com/downloads/sc-4.5.1-linux.tar.gz",
"semver": "5.4.1",
"semver": "^6.3.0",
"tslint-eslint-rules": "4.1.1",
"tslint-no-toplevel-property-access": "0.0.2",
"tsutils": "2.27.2",

View File

@ -27,10 +27,12 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/util",
"@npm//@types/convert-source-map",
"@npm//@types/node",
"@npm//@types/semver",
"@npm//@types/yargs",
"@npm//canonical-path",
"@npm//dependency-graph",
"@npm//magic-string",
"@npm//semver",
"@npm//source-map",
"@npm//typescript",
],

View File

@ -5,6 +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 {satisfies} from 'semver';
import * as vm from 'vm';
import {AbsoluteFsPath, FileSystem, dirname, join, resolve} from '../../../src/ngtsc/file_system';
import {PackageJsonFormatPropertiesMap} from './entry_point';
@ -12,7 +13,7 @@ import {PackageJsonFormatPropertiesMap} from './entry_point';
/**
* The format of a project level configuration file.
*/
export interface NgccProjectConfig { packages: {[packagePath: string]: NgccPackageConfig}; }
export interface NgccProjectConfig<T = NgccPackageConfig> { packages: {[packagePath: string]: T}; }
/**
* The format of a package level configuration file.
@ -80,47 +81,96 @@ export const DEFAULT_NGCC_CONFIG: NgccProjectConfig = {
}
};
interface VersionedPackageConfig extends NgccPackageConfig {
versionRange: string;
}
const NGCC_CONFIG_FILENAME = 'ngcc.config.js';
/**
* Ngcc has a hierarchical configuration system that lets us "fix up" packages that do not
* work with ngcc out of the box.
*
* There are three levels at which configuration can be declared:
*
* * Default level - ngcc comes with built-in configuration for well known cases.
* * Package level - a library author publishes a configuration with their package to fix known
* issues.
* * Project level - the application developer provides a configuration that fixes issues specific
* to the libraries used in their application.
*
* Ngcc will match configuration based on the package name but also on its version. This allows
* configuration to provide different fixes to different version ranges of a package.
*
* * Package level configuration is specific to the package version where the configuration is
* found.
* * Default and project level configuration should provide version ranges to ensure that the
* configuration is only applied to the appropriate versions of a package.
*
* When getting a configuration for a package (via `getConfig()`) the caller should provide the
* version of the package in question, if available. If it is not provided then the first available
* configuration for a package is returned.
*/
export class NgccConfiguration {
private defaultConfig: NgccProjectConfig;
private cache = new Map<string, NgccPackageConfig>();
private defaultConfig: NgccProjectConfig<VersionedPackageConfig[]>;
private projectConfig: NgccProjectConfig<VersionedPackageConfig[]>;
private cache = new Map<string, VersionedPackageConfig>();
constructor(private fs: FileSystem, baseDir: AbsoluteFsPath) {
this.defaultConfig = this.processDefaultConfig(baseDir);
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);
}
this.defaultConfig = this.processProjectConfig(baseDir, DEFAULT_NGCC_CONFIG);
this.projectConfig = this.processProjectConfig(baseDir, this.loadProjectConfig(baseDir));
}
getConfig(packagePath: AbsoluteFsPath): NgccPackageConfig {
if (this.cache.has(packagePath)) {
return this.cache.get(packagePath) !;
/**
* Get a configuration for the given `version` of a package at `packagePath`.
*
* @param packagePath The path to the package whose config we want.
* @param version The version of the package whose config we want, or `null` if the package's
* package.json did not exist or was invalid.
*/
getConfig(packagePath: AbsoluteFsPath, version: string|null): VersionedPackageConfig {
const cacheKey = packagePath + (version !== null ? `@${version}` : '');
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey) !;
}
const packageConfig = this.loadPackageConfig(packagePath) ||
this.defaultConfig.packages[packagePath] || {entryPoints: {}};
this.cache.set(packagePath, packageConfig);
return packageConfig;
const projectLevelConfig =
findSatisfactoryVersion(this.projectConfig.packages[packagePath], version);
if (projectLevelConfig !== null) {
this.cache.set(cacheKey, projectLevelConfig);
return projectLevelConfig;
}
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];
const packageLevelConfig = this.loadPackageConfig(packagePath, version);
if (packageLevelConfig !== null) {
this.cache.set(cacheKey, packageLevelConfig);
return packageLevelConfig;
}
const defaultLevelConfig =
findSatisfactoryVersion(this.defaultConfig.packages[packagePath], version);
if (defaultLevelConfig !== null) {
this.cache.set(cacheKey, defaultLevelConfig);
return defaultLevelConfig;
}
return {versionRange: '*', entryPoints: {}};
}
private processProjectConfig(baseDir: AbsoluteFsPath, projectConfig: NgccProjectConfig):
NgccProjectConfig<VersionedPackageConfig[]> {
const processedConfig: NgccProjectConfig<VersionedPackageConfig[]> = {packages: {}};
for (const packagePathAndVersion in projectConfig.packages) {
const packageConfig = projectConfig.packages[packagePathAndVersion];
if (packageConfig) {
packageConfig.entryPoints =
this.processEntryPoints(absPackagePath, packageConfig.entryPoints);
defaultConfig.packages[absPackagePath] = packageConfig;
const [packagePath, versionRange = '*'] = this.splitPathAndVersion(packagePathAndVersion);
const absPackagePath = resolve(baseDir, 'node_modules', packagePath);
const entryPoints = this.processEntryPoints(absPackagePath, packageConfig);
processedConfig.packages[absPackagePath] = processedConfig.packages[absPackagePath] || [];
processedConfig.packages[absPackagePath].push({versionRange, entryPoints});
}
}
return defaultConfig;
return processedConfig;
}
private loadProjectConfig(baseDir: AbsoluteFsPath): NgccProjectConfig {
@ -136,13 +186,15 @@ export class NgccConfiguration {
}
}
private loadPackageConfig(packagePath: AbsoluteFsPath): NgccPackageConfig|null {
private loadPackageConfig(packagePath: AbsoluteFsPath, version: string|null):
VersionedPackageConfig|null {
const configFilePath = join(packagePath, NGCC_CONFIG_FILENAME);
if (this.fs.exists(configFilePath)) {
try {
const packageConfig = this.evalSrcFile(configFilePath);
packageConfig.entryPoints = this.processEntryPoints(packagePath, packageConfig.entryPoints);
return packageConfig;
return {
versionRange: version || '*',
entryPoints: this.processEntryPoints(packagePath, this.evalSrcFile(configFilePath)),
};
} catch (e) {
throw new Error(`Invalid package configuration file at "${configFilePath}": ` + e.message);
}
@ -164,14 +216,41 @@ export class NgccConfiguration {
return sandbox.module.exports;
}
private processEntryPoints(
packagePath: AbsoluteFsPath, entryPoints: {[entryPointPath: string]: NgccEntryPointConfig;}):
private processEntryPoints(packagePath: AbsoluteFsPath, packageConfig: NgccPackageConfig):
{[entryPointPath: string]: NgccEntryPointConfig;} {
const processedEntryPoints: {[entryPointPath: string]: NgccEntryPointConfig;} = {};
for (const entryPointPath in entryPoints) {
for (const entryPointPath in packageConfig.entryPoints) {
// Change the keys to be absolute paths
processedEntryPoints[resolve(packagePath, entryPointPath)] = entryPoints[entryPointPath];
processedEntryPoints[resolve(packagePath, entryPointPath)] =
packageConfig.entryPoints[entryPointPath];
}
return processedEntryPoints;
}
private splitPathAndVersion(packagePathAndVersion: string): [string, string|undefined] {
const versionIndex = packagePathAndVersion.lastIndexOf('@');
// Note that > 0 is because we don't want to match @ at the start of the line
// which is what you would have with a namespaced package, e.g. `@angular/common`.
return versionIndex > 0 ?
[
packagePathAndVersion.substring(0, versionIndex),
packagePathAndVersion.substring(versionIndex + 1)
] :
[packagePathAndVersion, undefined];
}
}
function findSatisfactoryVersion(
configs: VersionedPackageConfig[] | undefined, version: string | null): VersionedPackageConfig|
null {
if (configs === undefined) {
return null;
}
if (version === null) {
// The package has no version (!) - perhaps the entry-point was from a deep import, which made
// it impossible to find the package.json.
// So just return the first config that matches the package name.
return configs[0];
}
return configs.find(config => satisfies(version, config.versionRange)) || null;
}

View File

@ -82,7 +82,9 @@ export function getEntryPointInfo(
fs: FileSystem, config: NgccConfiguration, logger: Logger, packagePath: AbsoluteFsPath,
entryPointPath: AbsoluteFsPath): EntryPoint|null {
const packageJsonPath = resolve(entryPointPath, 'package.json');
const entryPointConfig = config.getConfig(packagePath).entryPoints[entryPointPath];
const packageVersion = getPackageVersion(fs, packageJsonPath);
const entryPointConfig =
config.getConfig(packagePath, packageVersion).entryPoints[entryPointPath];
if (entryPointConfig === undefined && !fs.exists(packageJsonPath)) {
return null;
}
@ -224,3 +226,20 @@ function guessTypingsFromPackageJson(
}
return null;
}
/**
* Find the version of the package at `packageJsonPath`.
*
* @returns the version string or `null` if the package.json does not exist or is invalid.
*/
function getPackageVersion(fs: FileSystem, packageJsonPath: AbsoluteFsPath): string|null {
try {
if (fs.exists(packageJsonPath)) {
const packageJson = JSON.parse(fs.readFile(packageJsonPath));
return packageJson['version'] || null;
}
} catch {
// Do nothing
}
return null;
}

View File

@ -32,49 +32,47 @@ runInEachFileSystem(() => {
describe('getConfig()', () => {
describe('at the package level', () => {
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': {}}}`
}]);
it('should return configuration for a package found in a package level file, with a matching version',
() => {
loadTestFiles(packageWithConfigFiles('package-1', 'entry-point-1', '1.0.0'));
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'));
const config =
configuration.getConfig(_Abs('/project-1/node_modules/package-1'), '1.0.0');
expect(config).toEqual(
{entryPoints: {[_Abs('/project-1/node_modules/package-1/entry-point-1')]: {}}});
expect(config).toEqual({
versionRange: '1.0.0',
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 used cached configuration for a package if available', () => {
loadTestFiles([{
name: _Abs('/project-1/node_modules/package-1/ngcc.config.js'),
contents: `
module.exports = {
entryPoints: {
'./entry-point-1': {}
},
};`
}]);
loadTestFiles(packageWithConfigFiles('package-1', 'entry-point-1', '1.0.0'));
const configuration = new NgccConfiguration(fs, _Abs('/project-1'));
// Populate the cache
configuration.getConfig(_Abs('/project-1/node_modules/package-1'));
configuration.getConfig(_Abs('/project-1/node_modules/package-1'), '1.0.0');
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'), '1.0.0');
expect(config).toEqual(
{entryPoints: {[_Abs('/project-1/node_modules/package-1/entry-point-1')]: {}}});
expect(config).toEqual({
versionRange: '1.0.0',
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 configuration for the package',
() => {
loadTestFiles(packageWithConfigFiles('package-2', 'entry-point-1', '1.0.0'));
const configuration = new NgccConfiguration(fs, _Abs('/project-1'));
const config = configuration.getConfig(_Abs('/project-1/node_modules/package-1'));
expect(config).toEqual({entryPoints: {}});
const config =
configuration.getConfig(_Abs('/project-1/node_modules/package-1'), '1.0.0');
expect(config).toEqual({versionRange: '*', entryPoints: {}});
});
it('should error if a package level config file is badly formatted', () => {
@ -83,7 +81,7 @@ runInEachFileSystem(() => {
contents: `bad js code`
}]);
const configuration = new NgccConfiguration(fs, _Abs('/project-1'));
expect(() => configuration.getConfig(_Abs('/project-1/node_modules/package-1')))
expect(() => configuration.getConfig(_Abs('/project-1/node_modules/package-1'), '1.0.0'))
.toThrowError(
`Invalid package configuration file at "${_Abs('/project-1/node_modules/package-1/ngcc.config.js')}": Unexpected identifier`);
});
@ -108,14 +106,86 @@ runInEachFileSystem(() => {
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')]: {}}});
const config =
configuration.getConfig(_Abs('/project-1/node_modules/package-1'), '1.0.0');
expect(config).toEqual({
versionRange: '*',
entryPoints: {[_Abs('/project-1/node_modules/package-1/entry-point-1')]: {}}
});
});
it('should return configuration for the correct version of a package found in a project level file',
() => {
loadTestFiles([{
name: _Abs('/project-1/ngcc.config.js'),
contents: `
module.exports = {
packages: {
'package-1@1.0.0': {
entryPoints: {
'./entry-point-1': {}
},
},
'package-1@2.*': {
entryPoints: {
'./entry-point-2': {}
},
},
'package-1@^3.2.0': {
entryPoints: {
'./entry-point-3': {}
},
},
},
};`
}]);
const configuration = new NgccConfiguration(fs, _Abs('/project-1'));
expect(configuration.getConfig(_Abs('/project-1/node_modules/package-1'), '1.0.0'))
.toEqual({
versionRange: '1.0.0',
entryPoints: {[_Abs('/project-1/node_modules/package-1/entry-point-1')]: {}}
});
expect(configuration.getConfig(_Abs('/project-1/node_modules/package-1'), '2.5.0'))
.toEqual({
versionRange: '2.*',
entryPoints: {[_Abs('/project-1/node_modules/package-1/entry-point-2')]: {}}
});
expect(configuration.getConfig(_Abs('/project-1/node_modules/package-1'), '3.2.5'))
.toEqual({
versionRange: '^3.2.0',
entryPoints: {[_Abs('/project-1/node_modules/package-1/entry-point-3')]: {}}
});
expect(configuration.getConfig(_Abs('/project-1/node_modules/package-1'), '4.0.0'))
.toEqual({versionRange: '*', entryPoints: {}});
});
it('should not get confused by the @ in namespaced packages', () => {
loadTestFiles([{
name: _Abs('/project-1/ngcc.config.js'),
contents: `
module.exports = {
packages: {
'@angular/common': {
entryPoints: {
'.': {}
},
},
},
};`
}]);
const configuration = new NgccConfiguration(fs, _Abs('/project-1'));
expect(configuration.getConfig(_Abs('/project-1/node_modules/@angular/common'), '1.0.0'))
.toEqual({
versionRange: '*',
entryPoints: {[_Abs('/project-1/node_modules/@angular/common')]: {}}
});
});
it('should override package level config with project level config per package', () => {
loadTestFiles([
{
loadTestFiles([{
name: _Abs('/project-1/ngcc.config.js'),
contents: `
module.exports = {
@ -127,32 +197,20 @@ 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': {}
},
};`,
}
]);
}]);
loadTestFiles(
packageWithConfigFiles('package-1', 'package-setting-entry-point', '1.0.0'));
loadTestFiles(
packageWithConfigFiles('package-2', 'package-setting-entry-point', '1.0.0'));
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'));
const package1Config =
configuration.getConfig(_Abs('/project-1/node_modules/package-1'), '1.0.0');
expect(package1Config).toEqual({
versionRange: '1.0.0',
entryPoints:
{[_Abs('/project-1/node_modules/package-1/package-setting-entry-point')]: {}}
});
@ -162,8 +220,10 @@ runInEachFileSystem(() => {
// 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'));
const package2Config =
configuration.getConfig(_Abs('/project-1/node_modules/package-2'), '1.0.0');
expect(package2Config).toEqual({
versionRange: '*',
entryPoints:
{[_Abs('/project-1/node_modules/package-2/project-setting-entry-point')]: {}}
});
@ -182,38 +242,36 @@ runInEachFileSystem(() => {
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'));
const config =
configuration.getConfig(_Abs('/project-1/node_modules/package-1'), '1.0.0');
expect(config).toEqual({
versionRange: '*',
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'),
contents: `
module.exports = {
entryPoints: {'./package-level-entry-point': {}},
};`,
}]);
loadTestFiles(packageWithConfigFiles('package-1', 'package-level-entry-point', '1.0.0'));
const configuration = new NgccConfiguration(fs, _Abs('/project-1'));
const config = configuration.getConfig(_Abs('/project-1/node_modules/package-1'));
const config =
configuration.getConfig(_Abs('/project-1/node_modules/package-1'), '1.0.0');
// Note that only the package-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({
versionRange: '1.0.0',
entryPoints:
{[_Abs('/project-1/node_modules/package-1/package-level-entry-point')]: {}}
});
});
it('should override default level config with project level config, if provided', () => {
loadTestFiles(packageWithConfigFiles('package-1', 'package-level-entry-point', '1.0.0'));
loadTestFiles([
{
name: _Abs('/project-1/node_modules/package-1/ngcc.config.js'),
@ -238,11 +296,13 @@ runInEachFileSystem(() => {
]);
const configuration = new NgccConfiguration(fs, _Abs('/project-1'));
const config = configuration.getConfig(_Abs('/project-1/node_modules/package-1'));
const config =
configuration.getConfig(_Abs('/project-1/node_modules/package-1'), '1.0.0');
// 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({
versionRange: '*',
entryPoints:
{[_Abs('/project-1/node_modules/package-1/project-level-entry-point')]: {}}
});
@ -250,4 +310,17 @@ runInEachFileSystem(() => {
});
});
});
function packageWithConfigFiles(packageName: string, entryPointName: string, version: string) {
return [
{
name: _Abs(`/project-1/node_modules/${packageName}/ngcc.config.js`),
contents: `module.exports = {entryPoints: { './${entryPointName}': {}}}`
},
{
name: _Abs(`/project-1/node_modules/${packageName}/package.json`),
contents: `{ "version": "${version}" }`
}
];
}
});

View File

@ -17,6 +17,7 @@
"convert-source-map": "^1.5.1",
"dependency-graph": "^0.7.2",
"magic-string": "^0.25.0",
"semver": "^6.3.0",
"source-map": "^0.6.1",
"tslib": "^1.9.0",
"yargs": "13.1.0"

View File

@ -877,6 +877,11 @@
resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-3.0.14.tgz#0b20a2370e6b1b8322c9c3dfcaa409e6c7c0c0a9"
integrity sha512-4GbNCDs98uHCT/OMv40qQC/OpoPbYn9XdXeTiFwHBBFO6eJhYEPUu2zDKirXSbHlvDV8oZ9l8EQ+HrEx/YS9DQ==
"@types/semver@^6.0.2":
version "6.0.2"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-6.0.2.tgz#5e8b09f0e4af53034b1d0fb9977a277847836205"
integrity sha512-G1Ggy7/9Nsa1Jt2yiBR2riEuyK2DFNnqow6R7cromXPMNynackRY1vqFTLz/gwnef1LHokbXThcPhqMRjUbkpQ==
"@types/shelljs@^0.7.8":
version "0.7.9"
resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.7.9.tgz#3abecb72d9cad9cd4b0e7cb86ed10a97d93ba602"
@ -10057,11 +10062,6 @@ semver@5.1.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-5.1.0.tgz#85f2cf8550465c4df000cf7d86f6b054106ab9e5"
integrity sha1-hfLPhVBGXE3wAM99hvawVBBqueU=
semver@5.4.1:
version "5.4.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
integrity sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==
semver@5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
@ -10072,6 +10072,11 @@ semver@6.0.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.0.0.tgz#05e359ee571e5ad7ed641a6eec1e547ba52dea65"
integrity sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ==
semver@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
semver@~5.0.1:
version "5.0.3"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.0.3.tgz#77466de589cd5d3c95f138aa78bc569a3cb5d27a"