From ce22236c3cba8f8add0224196c2aaf8b7914fe86 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 26 May 2020 18:14:50 +0200 Subject: [PATCH 01/15] packages/core-api: added initial ConfigApi --- .../src/apis/definitions/ConfigApi.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 packages/core-api/src/apis/definitions/ConfigApi.ts diff --git a/packages/core-api/src/apis/definitions/ConfigApi.ts b/packages/core-api/src/apis/definitions/ConfigApi.ts new file mode 100644 index 0000000000..20676df899 --- /dev/null +++ b/packages/core-api/src/apis/definitions/ConfigApi.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createApiRef } from '../ApiRef'; + +export type Config = { + getConfig(key: string): Config; + + getConfigArray(key: string): Config[]; + + getNumber(key: string): number | undefined; + + getBoolean(key: string): boolean | undefined; + + getString(key: string): string | undefined; + + getStringArray(key: string): string[] | undefined; +}; + +// Using interface to make the ConfigApi name show up in docs +export interface ConfigApi extends Config {} + +export const configApiRef = createApiRef({ + id: 'core.config', + description: 'Used to access runtime configuration', +}); From 0c6dbe0545360cce2ef41f0e8c600c2cc16759db Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 26 May 2020 20:46:31 +0200 Subject: [PATCH 02/15] packages/core-api: added initial ConfigReader --- .../ConfigApi/ConfigReader.test.ts | 126 ++++++++++++++ .../implementations/ConfigApi/ConfigReader.ts | 164 ++++++++++++++++++ .../apis/implementations/ConfigApi/index.ts | 17 ++ 3 files changed, 307 insertions(+) create mode 100644 packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts create mode 100644 packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts create mode 100644 packages/core-api/src/apis/implementations/ConfigApi/index.ts diff --git a/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts new file mode 100644 index 0000000000..206527b95d --- /dev/null +++ b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts @@ -0,0 +1,126 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConfigReader } from './ConfigReader'; + +const DATA = { + zero: 0, + one: 1, + true: true, + false: false, + null: null, + string: 'string', + emptyString: '', + strings: ['string1', 'string2'], + badStrings: ['string1', ''], + worseStrings: ['string1', 3] as string[], + worstStrings: ['string1', 'string2', {}] as string[], + nested: { + one: 1, + string: 'string', + strings: ['string1', 'string2'], + }, + nestlings: [{ boolean: true }, { string: 'string' }, { number: 42 }] as {}[], +}; + +describe('ConfigReader', () => { + it('should read empty config with valid keys', () => { + const config = new ConfigReader({}); + expect(config.getString('x')).toBeUndefined(); + expect(config.getString('x_x')).toBeUndefined(); + expect(config.getString('x-X')).toBeUndefined(); + expect(config.getString('x0')).toBeUndefined(); + expect(config.getString('X-x2')).toBeUndefined(); + expect(config.getString('x0_x0')).toBeUndefined(); + expect(config.getString('x_x-x_x')).toBeUndefined(); + }); + + it('should throw on invalid keys', () => { + const config = new ConfigReader({}); + + expect(() => config.getString('.')).toThrow(/^Invalid config key/); + expect(() => config.getString('0')).toThrow(/^Invalid config key/); + expect(() => config.getString('(')).toThrow(/^Invalid config key/); + expect(() => config.getString('z-_')).toThrow(/^Invalid config key/); + expect(() => config.getString('-')).toThrow(/^Invalid config key/); + expect(() => config.getString('.a')).toThrow(/^Invalid config key/); + expect(() => config.getString('0.a')).toThrow(/^Invalid config key/); + expect(() => config.getString('0a')).toThrow(/^Invalid config key/); + expect(() => config.getString('a.0a')).toThrow(/^Invalid config key/); + expect(() => config.getString('a..a')).toThrow(/^Invalid config key/); + expect(() => config.getString('a.')).toThrow(/^Invalid config key/); + expect(() => config.getString('a...')).toThrow(/^Invalid config key/); + expect(() => config.getString('a.a.a.a.')).toThrow(/^Invalid config key/); + expect(() => config.getString('a._')).toThrow(/^Invalid config key/); + expect(() => config.getString('a.-.a')).toThrow(/^Invalid config key/); + }); + + it('should read valid values', () => { + const config = new ConfigReader(DATA); + expect(config.getNumber('zero')).toBe(0); + expect(config.getNumber('one')).toBe(1); + expect(config.getBoolean('true')).toBe(true); + expect(config.getBoolean('false')).toBe(false); + expect(config.getString('string')).toBe('string'); + expect(config.getStringArray('strings')).toEqual(['string1', 'string2']); + expect(config.getConfig('nested').getNumber('one')).toBe(1); + expect(config.getConfig('nested').getString('string')).toBe('string'); + expect(config.getConfig('nested').getStringArray('strings')).toEqual([ + 'string1', + 'string2', + ]); + + const [config1, config2, config3] = config.getConfigArray('nestlings'); + expect(config1.getBoolean('boolean')).toBe(true); + expect(config2.getString('string')).toBe('string'); + expect(config3.getNumber('number')).toBe(42); + }); + + it('should fail to read invalid values', () => { + const config = new ConfigReader(DATA); + + expect(() => config.getNumber('string')).toThrow( + 'Invalid type in config for key string, got string, wanted number', + ); + expect(() => config.getString('one')).toThrow( + 'Invalid type in config for key one, got number, wanted string', + ); + expect(() => config.getNumber('true')).toThrow( + 'Invalid type in config for key true, got boolean, wanted number', + ); + expect(() => config.getStringArray('null')).toThrow( + 'Invalid type in config for key null, got null, wanted string-array', + ); + expect(() => config.getString('emptyString')).toThrow( + 'Invalid type in config for key emptyString, got empty-string, wanted string', + ); + expect(() => config.getStringArray('badStrings')).toThrow( + 'Invalid type in config for key badStrings[1], got empty-string, wanted string', + ); + expect(() => config.getStringArray('worseStrings')).toThrow( + 'Invalid type in config for key worseStrings[1], got number, wanted string', + ); + expect(() => config.getStringArray('worstStrings')).toThrow( + 'Invalid type in config for key worstStrings[2], got object, wanted string', + ); + expect(() => config.getConfig('one')).toThrow( + 'Invalid type in config for key one, got number, wanted object', + ); + expect(() => config.getConfigArray('one')).toThrow( + 'Invalid type in config for key one, got number, wanted object-array', + ); + }); +}); diff --git a/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts new file mode 100644 index 0000000000..ac725ba59d --- /dev/null +++ b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts @@ -0,0 +1,164 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConfigApi, Config } from '../../definitions/ConfigApi'; + +const CONFIG_KEY_PART_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z][a-z0-9]*)*$/i; + +type JsonObject = { [key in string]: JsonValue }; +type JsonArray = JsonValue[]; +type JsonValue = JsonObject | JsonArray | number | string | boolean | null; + +function isObject(value: JsonValue | undefined): value is JsonObject { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function typeOf(value: JsonValue | undefined): string { + if (value === null) { + return 'null'; + } else if (Array.isArray(value)) { + return 'array'; + } + const type = typeof value; + if (type === 'number' && isNaN(value as number)) { + return 'nan'; + } + return type; +} + +function typeErrorMessage(key: string, got: string, wanted: string) { + return `Invalid type in config for key ${key}, got ${got}, wanted ${wanted}`; +} + +function validateString( + key: string, + value: JsonValue | undefined, +): value is string { + if (typeof value === 'string' && value.length > 0) { + return true; + } + if (value === '') { + throw new TypeError(typeErrorMessage(key, 'empty-string', 'string')); + } + if (value !== undefined) { + throw new TypeError(typeErrorMessage(key, typeOf(value), 'string')); + } + return false; +} + +export class ConfigReader implements ConfigApi { + static nullReader = new ConfigReader({}); + + constructor(private readonly data: JsonObject) {} + + getConfig(key: string): Config { + const value = this.readValue(key); + if (isObject(value)) { + return new ConfigReader(value); + } + if (value !== undefined) { + throw new TypeError(typeErrorMessage(key, typeOf(value), 'object')); + } + return ConfigReader.nullReader; + } + + getConfigArray(key: string): Config[] { + const values = this.readValue(key); + if (Array.isArray(values)) { + return values.map((value, index) => { + if (isObject(value)) { + return new ConfigReader(value); + } + throw new TypeError( + typeErrorMessage(`${key}[${index}]`, typeOf(value), 'object'), + ); + }); + } + if (values !== undefined) { + throw new TypeError( + typeErrorMessage(key, typeOf(values), 'object-array'), + ); + } + return []; + } + + getNumber(key: string): number | undefined { + const value = this.readValue(key); + if (typeof value === 'number' && !isNaN(value)) { + return value; + } + if (value !== undefined) { + throw new TypeError(typeErrorMessage(key, typeOf(value), 'number')); + } + return undefined; + } + + getBoolean(key: string): boolean | undefined { + const value = this.readValue(key); + if (typeof value === 'boolean') { + return value; + } + if (value !== undefined) { + throw new TypeError(typeErrorMessage(key, typeOf(value), 'boolean')); + } + return undefined; + } + + getString(key: string): string | undefined { + const value = this.readValue(key); + if (validateString(key, value)) { + return value; + } + return undefined; + } + + getStringArray(key: string): string[] | undefined { + const values = this.readValue(key); + if (Array.isArray(values)) { + for (const [index, value] of values.entries()) { + const iKey = `${key}[${index}]`; + if (!validateString(iKey, value)) { + throw new TypeError(typeErrorMessage(iKey, typeOf(value), 'string')); + } + } + return values as string[]; + } + if (values !== undefined) { + throw new TypeError( + typeErrorMessage(key, typeOf(values), 'string-array'), + ); + } + return undefined; + } + + private readValue(key: string): JsonValue | undefined { + const parts = key.split('.'); + + let value: JsonValue | undefined = this.data; + for (const part of parts) { + if (!CONFIG_KEY_PART_PATTERN.test(part)) { + throw new TypeError(`Invalid config key '${key}'`); + } + if (isObject(value)) { + value = value[part]; + } else { + value = undefined; + } + } + + return value; + } +} diff --git a/packages/core-api/src/apis/implementations/ConfigApi/index.ts b/packages/core-api/src/apis/implementations/ConfigApi/index.ts new file mode 100644 index 0000000000..8839cb948e --- /dev/null +++ b/packages/core-api/src/apis/implementations/ConfigApi/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { ConfigReader } from './ConfigReader'; From d471182708b55fd617008416da6908c724e33687 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 28 May 2020 12:57:33 +0200 Subject: [PATCH 03/15] packages/core-api: add fallback capability to ConfigReader --- .../ConfigApi/ConfigReader.test.ts | 133 +++++++++++------- .../implementations/ConfigApi/ConfigReader.ts | 20 +-- 2 files changed, 97 insertions(+), 56 deletions(-) diff --git a/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts index 206527b95d..a9731c0d2c 100644 --- a/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts +++ b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts @@ -36,6 +36,59 @@ const DATA = { nestlings: [{ boolean: true }, { string: 'string' }, { number: 42 }] as {}[], }; +function expectValidValues(config: ConfigReader) { + expect(config.getNumber('zero')).toBe(0); + expect(config.getNumber('one')).toBe(1); + expect(config.getBoolean('true')).toBe(true); + expect(config.getBoolean('false')).toBe(false); + expect(config.getString('string')).toBe('string'); + expect(config.getStringArray('strings')).toEqual(['string1', 'string2']); + expect(config.getConfig('nested').getNumber('one')).toBe(1); + expect(config.getConfig('nested').getString('string')).toBe('string'); + expect(config.getConfig('nested').getStringArray('strings')).toEqual([ + 'string1', + 'string2', + ]); + + const [config1, config2, config3] = config.getConfigArray('nestlings'); + expect(config1.getBoolean('boolean')).toBe(true); + expect(config2.getString('string')).toBe('string'); + expect(config3.getNumber('number')).toBe(42); +} + +function expectInvalidValues(config: ConfigReader) { + expect(() => config.getNumber('string')).toThrow( + 'Invalid type in config for key string, got string, wanted number', + ); + expect(() => config.getString('one')).toThrow( + 'Invalid type in config for key one, got number, wanted string', + ); + expect(() => config.getNumber('true')).toThrow( + 'Invalid type in config for key true, got boolean, wanted number', + ); + expect(() => config.getStringArray('null')).toThrow( + 'Invalid type in config for key null, got null, wanted string-array', + ); + expect(() => config.getString('emptyString')).toThrow( + 'Invalid type in config for key emptyString, got empty-string, wanted string', + ); + expect(() => config.getStringArray('badStrings')).toThrow( + 'Invalid type in config for key badStrings[1], got empty-string, wanted string', + ); + expect(() => config.getStringArray('worseStrings')).toThrow( + 'Invalid type in config for key worseStrings[1], got number, wanted string', + ); + expect(() => config.getStringArray('worstStrings')).toThrow( + 'Invalid type in config for key worstStrings[2], got object, wanted string', + ); + expect(() => config.getConfig('one')).toThrow( + 'Invalid type in config for key one, got number, wanted object', + ); + expect(() => config.getConfigArray('one')).toThrow( + 'Invalid type in config for key one, got number, wanted object-array', + ); +} + describe('ConfigReader', () => { it('should read empty config with valid keys', () => { const config = new ConfigReader({}); @@ -70,57 +123,41 @@ describe('ConfigReader', () => { it('should read valid values', () => { const config = new ConfigReader(DATA); - expect(config.getNumber('zero')).toBe(0); - expect(config.getNumber('one')).toBe(1); - expect(config.getBoolean('true')).toBe(true); - expect(config.getBoolean('false')).toBe(false); - expect(config.getString('string')).toBe('string'); - expect(config.getStringArray('strings')).toEqual(['string1', 'string2']); - expect(config.getConfig('nested').getNumber('one')).toBe(1); - expect(config.getConfig('nested').getString('string')).toBe('string'); - expect(config.getConfig('nested').getStringArray('strings')).toEqual([ - 'string1', - 'string2', - ]); - - const [config1, config2, config3] = config.getConfigArray('nestlings'); - expect(config1.getBoolean('boolean')).toBe(true); - expect(config2.getString('string')).toBe('string'); - expect(config3.getNumber('number')).toBe(42); + expectValidValues(config); }); it('should fail to read invalid values', () => { const config = new ConfigReader(DATA); - - expect(() => config.getNumber('string')).toThrow( - 'Invalid type in config for key string, got string, wanted number', - ); - expect(() => config.getString('one')).toThrow( - 'Invalid type in config for key one, got number, wanted string', - ); - expect(() => config.getNumber('true')).toThrow( - 'Invalid type in config for key true, got boolean, wanted number', - ); - expect(() => config.getStringArray('null')).toThrow( - 'Invalid type in config for key null, got null, wanted string-array', - ); - expect(() => config.getString('emptyString')).toThrow( - 'Invalid type in config for key emptyString, got empty-string, wanted string', - ); - expect(() => config.getStringArray('badStrings')).toThrow( - 'Invalid type in config for key badStrings[1], got empty-string, wanted string', - ); - expect(() => config.getStringArray('worseStrings')).toThrow( - 'Invalid type in config for key worseStrings[1], got number, wanted string', - ); - expect(() => config.getStringArray('worstStrings')).toThrow( - 'Invalid type in config for key worstStrings[2], got object, wanted string', - ); - expect(() => config.getConfig('one')).toThrow( - 'Invalid type in config for key one, got number, wanted object', - ); - expect(() => config.getConfigArray('one')).toThrow( - 'Invalid type in config for key one, got number, wanted object-array', - ); + expectInvalidValues(config); + }); +}); + +describe('ConfigReader with fallback', () => { + it('should behave as if without fallback', () => { + const config = new ConfigReader({}, new ConfigReader(DATA)); + expect(config.getString('x')).toBeUndefined(); + expect(() => config.getString('.')).toThrow(/^Invalid config key/); + expect(() => config.getString('a.')).toThrow(/^Invalid config key/); + }); + + it('should read values from itself', () => { + const config = new ConfigReader(DATA, new ConfigReader({})); + expectValidValues(config); + expectInvalidValues(config); + }); + + it('should read values from a fallback', () => { + const config = new ConfigReader({}, new ConfigReader(DATA)); + expectValidValues(config); + expectInvalidValues(config); + }); + + it('should read values from multiple levels of fallbacks', () => { + const config = new ConfigReader( + {}, + new ConfigReader({}, new ConfigReader({}, new ConfigReader(DATA))), + ); + expectValidValues(config); + expectInvalidValues(config); }); }); diff --git a/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts index ac725ba59d..7a5cf3b185 100644 --- a/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts +++ b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts @@ -62,17 +62,21 @@ function validateString( export class ConfigReader implements ConfigApi { static nullReader = new ConfigReader({}); - constructor(private readonly data: JsonObject) {} + constructor( + private readonly data: JsonObject, + private readonly fallback?: ConfigApi, + ) {} getConfig(key: string): Config { const value = this.readValue(key); + const fallbackConfig = this.fallback?.getConfig(key); if (isObject(value)) { - return new ConfigReader(value); + return new ConfigReader(value, fallbackConfig); } if (value !== undefined) { throw new TypeError(typeErrorMessage(key, typeOf(value), 'object')); } - return ConfigReader.nullReader; + return fallbackConfig ?? ConfigReader.nullReader; } getConfigArray(key: string): Config[] { @@ -92,7 +96,7 @@ export class ConfigReader implements ConfigApi { typeErrorMessage(key, typeOf(values), 'object-array'), ); } - return []; + return this.fallback?.getConfigArray(key) ?? []; } getNumber(key: string): number | undefined { @@ -103,7 +107,7 @@ export class ConfigReader implements ConfigApi { if (value !== undefined) { throw new TypeError(typeErrorMessage(key, typeOf(value), 'number')); } - return undefined; + return this.fallback?.getNumber(key); } getBoolean(key: string): boolean | undefined { @@ -114,7 +118,7 @@ export class ConfigReader implements ConfigApi { if (value !== undefined) { throw new TypeError(typeErrorMessage(key, typeOf(value), 'boolean')); } - return undefined; + return this.fallback?.getBoolean(key); } getString(key: string): string | undefined { @@ -122,7 +126,7 @@ export class ConfigReader implements ConfigApi { if (validateString(key, value)) { return value; } - return undefined; + return this.fallback?.getString(key); } getStringArray(key: string): string[] | undefined { @@ -141,7 +145,7 @@ export class ConfigReader implements ConfigApi { typeErrorMessage(key, typeOf(values), 'string-array'), ); } - return undefined; + return this.fallback?.getStringArray(key); } private readValue(key: string): JsonValue | undefined { From 810de67f9c7a378d587c303bc271746a6bdab7f0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 28 May 2020 13:16:59 +0200 Subject: [PATCH 04/15] packages/core-api: some more tests for ConfigReader to make sure deep merge is correct --- .../ConfigApi/ConfigReader.test.ts | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts index a9731c0d2c..68c1fd5353 100644 --- a/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts +++ b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts @@ -160,4 +160,67 @@ describe('ConfigReader with fallback', () => { expectValidValues(config); expectInvalidValues(config); }); + + it('should read merged objects', () => { + const a = { + merged: { + x: 'x', + z: 'z1', + arr: ['a', 'b'], + config: { d: 'd' }, + configs: [{ a: 'a' }], + }, + }; + const b = { + merged: { + y: 'y', + z: 'z2', + arr: ['c'], + config: { e: 'e' }, + configs: [{ b: 'b' }], + }, + }; + + const config = new ConfigReader(a, new ConfigReader(b)); + + expect(config.getString('merged.x')).toBe('x'); + expect(config.getString('merged.y')).toBe('y'); + expect(config.getString('merged.z')).toBe('z1'); + expect(config.getConfig('merged').getString('x')).toBe('x'); + expect(config.getConfig('merged').getString('y')).toBe('y'); + expect(config.getConfig('merged').getString('z')).toBe('z1'); + expect(config.getString('merged.config.d')).toBe('d'); + expect(config.getString('merged.config.e')).toBe('e'); + expect(config.getConfig('merged').getString('config.d')).toBe('d'); + expect(config.getConfig('merged').getString('config.e')).toBe('e'); + expect(config.getConfig('merged').getConfig('config').getString('d')).toBe( + 'd', + ); + expect(config.getConfig('merged').getConfig('config').getString('e')).toBe( + 'e', + ); + + // Arrays are not merged + expect(config.getStringArray('merged.arr')).toEqual(['a', 'b']); + expect(config.getConfig('merged').getStringArray('arr')).toEqual([ + 'a', + 'b', + ]); + + // Config arrays aren't merged either + expect(config.getConfigArray('merged.configs').length).toBe(1); + expect(config.getConfigArray('merged.configs')[0].getString('a')).toBe('a'); + expect( + config.getConfigArray('merged.configs')[0].getString('b'), + ).toBeUndefined(); + + // Config arrays aren't merged either + expect(config.getConfig('merged').getConfigArray('configs').length).toBe(1); + expect( + config.getConfig('merged').getConfigArray('configs')[0].getString('a'), + ).toBe('a'); + expect( + config.getConfig('merged').getConfigArray('configs')[0].getString('b'), + ).toBeUndefined(); + }); }); From 1ab6499bbbe1abede1b0d3ab1751557f72a6228c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 28 May 2020 13:53:06 +0200 Subject: [PATCH 05/15] packages/core-api: export ConfigApi + implementation --- packages/core-api/src/apis/definitions/index.ts | 1 + packages/core-api/src/apis/implementations/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/core-api/src/apis/definitions/index.ts b/packages/core-api/src/apis/definitions/index.ts index 2e008965cd..2e9db325dc 100644 --- a/packages/core-api/src/apis/definitions/index.ts +++ b/packages/core-api/src/apis/definitions/index.ts @@ -24,6 +24,7 @@ export * from './auth'; export * from './AlertApi'; export * from './AppThemeApi'; +export * from './ConfigApi'; export * from './ErrorApi'; export * from './FeatureFlagsApi'; export * from './OAuthRequestApi'; diff --git a/packages/core-api/src/apis/implementations/index.ts b/packages/core-api/src/apis/implementations/index.ts index bb77cf5bd3..b5cc250ae4 100644 --- a/packages/core-api/src/apis/implementations/index.ts +++ b/packages/core-api/src/apis/implementations/index.ts @@ -22,5 +22,6 @@ export * from './auth'; export * from './AlertApi'; export * from './AppThemeApi'; +export * from './ConfigApi'; export * from './ErrorApi'; export * from './OAuthRequestApi'; From bdf32a00a031bc6cf67bb73940138871e16f1abe Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 28 May 2020 14:16:43 +0200 Subject: [PATCH 06/15] packages/core-api: add configLoader option to App --- packages/core-api/src/app/App.tsx | 44 ++++++++++++++------ packages/core-api/src/app/types.ts | 27 ++++++++++++ packages/core/src/api-wrappers/createApp.tsx | 27 +++++++++++- 3 files changed, 84 insertions(+), 14 deletions(-) diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index 844c38ecce..c3ea77d87f 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -17,7 +17,7 @@ import React, { ComponentType, FC } from 'react'; import { Route, Switch, Redirect } from 'react-router-dom'; import { AppContextProvider } from './AppContext'; -import { BackstageApp, AppComponents } from './types'; +import { BackstageApp, AppComponents, AppConfigLoader } from './types'; import { BackstagePlugin } from '../plugin'; import { FeatureFlagsRegistryItem } from './FeatureFlags'; import { featureFlagsApiRef } from '../apis/definitions'; @@ -31,8 +31,11 @@ import { AppTheme, AppThemeSelector, appThemeApiRef, + configApiRef, + ConfigReader, } from '../apis'; import { ApiAggregator } from '../apis/ApiAggregator'; +import { useAsync } from 'react-use'; type FullAppOptions = { apis: ApiHolder; @@ -40,6 +43,7 @@ type FullAppOptions = { plugins: BackstagePlugin[]; components: AppComponents; themes: AppTheme[]; + configLoader: AppConfigLoader; }; export class PrivateAppImpl implements BackstageApp { @@ -48,6 +52,7 @@ export class PrivateAppImpl implements BackstageApp { private readonly plugins: BackstagePlugin[]; private readonly components: AppComponents; private readonly themes: AppTheme[]; + private readonly configLoader: AppConfigLoader; constructor(options: FullAppOptions) { this.apis = options.apis; @@ -55,6 +60,7 @@ export class PrivateAppImpl implements BackstageApp { this.plugins = options.plugins; this.components = options.components; this.themes = options.themes; + this.configLoader = options.configLoader; } getApis(): ApiHolder { @@ -141,18 +147,32 @@ export class PrivateAppImpl implements BackstageApp { } getProvider(): ComponentType<{}> { - const appApis = ApiRegistry.from([ - [appThemeApiRef, AppThemeSelector.createWithStorage(this.themes)], - ]); - const apis = new ApiAggregator(this.apis, appApis); + const Provider: FC<{}> = ({ children }) => { + const config = useAsync(this.configLoader); + if (config.loading) { + return null; + } - const Provider: FC<{}> = ({ children }) => ( - - - {children} - - - ); + let errorPage = undefined; + if (config.error) { + const { BootErrorPage } = this.components; + errorPage = ; + } + + const appApis = ApiRegistry.from([ + [appThemeApiRef, AppThemeSelector.createWithStorage(this.themes)], + [configApiRef, new ConfigReader(config.value ?? {})], + ]); + const apis = new ApiAggregator(this.apis, appApis); + + return ( + + + {errorPage ?? children} + + + ); + }; return Provider; } diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts index ea3a812557..e43a625569 100644 --- a/packages/core-api/src/app/types.ts +++ b/packages/core-api/src/app/types.ts @@ -20,10 +20,26 @@ import { BackstagePlugin } from '../plugin'; import { ApiHolder } from '../apis'; import { AppTheme } from '../apis/definitions'; +export type BootErrorPageProps = { + step: 'load-config'; + error: Error; +}; + export type AppComponents = { NotFoundErrorPage: ComponentType<{}>; + BootErrorPage: ComponentType; }; +/** + * TBD + */ +export type AppConfig = any; + +/** + * A function that loads in the App config that will be accessible via the ConfigApi. + */ +export type AppConfigLoader = () => Promise; + export type AppOptions = { /** * A holder of all APIs available in the app. @@ -68,6 +84,17 @@ export type AppOptions = { * ``` */ themes?: AppTheme[]; + + /** + * A function that loads in App configuration that will be accessible via + * the ConfigApi. + * + * Defaults to an empty config. + * + * TODO(Rugvip): Omitting this should instead default to loading in configuration + * that was packaged by the backstage-cli and default docker container boot script. + */ + configLoader?: AppConfigLoader; }; export type BackstageApp = { diff --git a/packages/core/src/api-wrappers/createApp.tsx b/packages/core/src/api-wrappers/createApp.tsx index 03adc41ec6..cc083b0e24 100644 --- a/packages/core/src/api-wrappers/createApp.tsx +++ b/packages/core/src/api-wrappers/createApp.tsx @@ -14,12 +14,14 @@ * limitations under the License. */ -import React from 'react'; +import React, { FC } from 'react'; import privateExports, { AppOptions, ApiRegistry, defaultSystemIcons, + BootErrorPageProps, } from '@backstage/core-api'; +import { BrowserRouter as Router } from 'react-router-dom'; import { ErrorPage } from '../layout/ErrorPage'; import { lightTheme, darkTheme } from '@backstage/theme'; @@ -38,12 +40,25 @@ export function createApp(options?: AppOptions) { const DefaultNotFoundPage = () => ( ); + const DefaultBootErrorPage: FC = ({ step, error }) => { + let message = ''; + if (step === 'load-config') { + message = `The configuration failed to load, someone should have a look at this error: ${error.message}`; + } + // TODO: figure out a nicer way to handle routing on the error page, when it can be done. + return ( + + + + ); + }; const apis = options?.apis ?? ApiRegistry.from([]); const icons = { ...defaultSystemIcons, ...options?.icons }; const plugins = options?.plugins ?? []; const components = { NotFoundErrorPage: DefaultNotFoundPage, + BootErrorPage: DefaultBootErrorPage, ...options?.components, }; const themes = options?.themes ?? [ @@ -60,8 +75,16 @@ export function createApp(options?: AppOptions) { theme: darkTheme, }, ]; + const configLoader = options?.configLoader ?? (async () => ({})); - const app = new PrivateAppImpl({ apis, icons, plugins, components, themes }); + const app = new PrivateAppImpl({ + apis, + icons, + plugins, + components, + themes, + configLoader, + }); app.verify(); From a115bd63311f8134be0d173d8918af22052d1068 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 28 May 2020 14:26:21 +0200 Subject: [PATCH 07/15] packages/app: add mock config loader and use in welcome plugin --- packages/app/src/App.tsx | 12 ++++++++++++ .../src/components/WelcomePage/WelcomePage.test.tsx | 13 +++++++++++-- .../src/components/WelcomePage/WelcomePage.tsx | 5 ++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 56faf4402b..461d8f193b 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -29,6 +29,18 @@ import apis from './apis'; const app = createApp({ apis, plugins: Object.values(plugins), + configLoader: async () => ({ + app: { + title: 'Backstage Example App', + baseUrl: 'http://localhost:3000', + }, + backend: { + baseUrl: 'http://localhost:7000', + }, + organization: { + name: 'Spotify', + }, + }), }); const AppProvider = app.getProvider(); diff --git a/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx b/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx index 9cba76fddc..b639da9d50 100644 --- a/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx +++ b/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx @@ -19,14 +19,23 @@ import { render } from '@testing-library/react'; import WelcomePage from './WelcomePage'; import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; -import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; +import { + ApiProvider, + ApiRegistry, + errorApiRef, + configApiRef, + ConfigReader, +} from '@backstage/core'; describe('WelcomePage', () => { it('should render', () => { // TODO: use common test app with mock implementations of all core APIs const rendered = render( diff --git a/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx b/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx index 1036096ae5..6a1b0e0008 100644 --- a/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx +++ b/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx @@ -34,15 +34,18 @@ import { ContentHeader, SupportButton, WarningPanel, + useApi, + configApiRef, } from '@backstage/core'; const WelcomePage: FC<{}> = () => { + const appTitle = useApi(configApiRef).getString('app.title') ?? 'Backstage'; const profile = { givenName: '' }; return (
From d120e5f8cb9116aa89eb1c479939ed410ce29c29 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 28 May 2020 19:55:02 +0200 Subject: [PATCH 08/15] packages/core-api: add Progress as a configurable app component --- packages/core-api/src/app/App.tsx | 15 ++++++++------- packages/core-api/src/app/types.ts | 1 + packages/core/src/api-wrappers/createApp.tsx | 2 ++ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index c3ea77d87f..49d2291acd 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -149,14 +149,15 @@ export class PrivateAppImpl implements BackstageApp { getProvider(): ComponentType<{}> { const Provider: FC<{}> = ({ children }) => { const config = useAsync(this.configLoader); - if (config.loading) { - return null; - } - let errorPage = undefined; - if (config.error) { + let childNode = children; + + if (config.loading) { + const { Progress } = this.components; + childNode = ; + } else if (config.error) { const { BootErrorPage } = this.components; - errorPage = ; + childNode = ; } const appApis = ApiRegistry.from([ @@ -168,7 +169,7 @@ export class PrivateAppImpl implements BackstageApp { return ( - {errorPage ?? children} + {childNode} ); diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts index e43a625569..defc155a82 100644 --- a/packages/core-api/src/app/types.ts +++ b/packages/core-api/src/app/types.ts @@ -28,6 +28,7 @@ export type BootErrorPageProps = { export type AppComponents = { NotFoundErrorPage: ComponentType<{}>; BootErrorPage: ComponentType; + Progress: ComponentType<{}>; }; /** diff --git a/packages/core/src/api-wrappers/createApp.tsx b/packages/core/src/api-wrappers/createApp.tsx index cc083b0e24..7c605c4e18 100644 --- a/packages/core/src/api-wrappers/createApp.tsx +++ b/packages/core/src/api-wrappers/createApp.tsx @@ -24,6 +24,7 @@ import privateExports, { import { BrowserRouter as Router } from 'react-router-dom'; import { ErrorPage } from '../layout/ErrorPage'; +import Progress from '../components/Progress'; import { lightTheme, darkTheme } from '@backstage/theme'; const { PrivateAppImpl } = privateExports; @@ -59,6 +60,7 @@ export function createApp(options?: AppOptions) { const components = { NotFoundErrorPage: DefaultNotFoundPage, BootErrorPage: DefaultBootErrorPage, + Progress: Progress, ...options?.components, }; const themes = options?.themes ?? [ From 8f56253f2a250ae25cf6a47c8fb3f186af30c6b8 Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Sat, 30 May 2020 11:36:57 +0200 Subject: [PATCH 09/15] remove github login page --- packages/app/src/App.tsx | 10 +- .../core/src/layout/LoginPage/LoginPage.tsx | 178 ------------------ packages/core/src/layout/LoginPage/index.ts | 17 -- packages/core/src/layout/index.ts | 1 - 4 files changed, 2 insertions(+), 204 deletions(-) delete mode 100644 packages/core/src/layout/LoginPage/LoginPage.tsx delete mode 100644 packages/core/src/layout/LoginPage/index.ts diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 56faf4402b..b4d01e067b 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -14,14 +14,9 @@ * limitations under the License. */ -import { - createApp, - AlertDisplay, - OAuthRequestDialog, - LoginPage, -} from '@backstage/core'; +import { createApp, AlertDisplay, OAuthRequestDialog } from '@backstage/core'; import React, { FC } from 'react'; -import { BrowserRouter as Router, Route } from 'react-router-dom'; +import { BrowserRouter as Router } from 'react-router-dom'; import Root from './components/Root'; import * as plugins from './plugins'; import apis from './apis'; @@ -40,7 +35,6 @@ const App: FC<{}> = () => ( - diff --git a/packages/core/src/layout/LoginPage/LoginPage.tsx b/packages/core/src/layout/LoginPage/LoginPage.tsx deleted file mode 100644 index 7bf14dba01..0000000000 --- a/packages/core/src/layout/LoginPage/LoginPage.tsx +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { FC, useState } from 'react'; -import GitHubIcon from '@material-ui/icons/GitHub'; -import { Page } from '../Page'; -import { Header } from '../Header'; -import { Content } from '../Content'; -import { ContentHeader } from '../ContentHeader'; -import { InfoCard } from '../InfoCard/InfoCard'; -import { - Grid, - Typography, - Button, - TextField, - List, - ListItem, - Link, -} from '@material-ui/core'; - -enum AuthType { - GitHub, -} - -export const LoginPage: FC<{}> = () => { - const [githubUsername, setGithubUsername] = useState(String); - const [githubPersonalAuthToken, setGithubPersonalAuthToken] = useState( - String, - ); - const [loginDetails, setLoginDetails] = useState(Object); - - const saveGithubInfo = (info: {}) => { - localStorage.setItem('githubLoginDetails', JSON.stringify(info)); - setLoginDetails(info); - }; - - const deleteGithubInfo = () => { - localStorage.removeItem('githubLoginDetails'); - setLoginDetails(undefined); - }; - - const handleTokenRegistration = (event: any) => { - switch (event.target.name) { - case 'github-username-tf': - setGithubUsername(event.target.value); - break; - case 'github-auth-tf': - setGithubPersonalAuthToken(event.target.value); - break; - default: - break; - } - }; - - const fetchGitHubToken = (username: String, token: String) => { - fetch('https://api.github.com/user', { - headers: new Headers({ - Authorization: `Basic ${btoa(`${username}:${token}`)}`, - 'Content-Type': 'application/x-www-form-urlencoded', - }), - }) - .then(response => { - if (response.status === 200) return response.json(); - throw Error(`${response.status} ${response.statusText}`); - }) - .then(data => { - const info = { - username: username, - token: token, - name: data.name || data.login, - }; - saveGithubInfo(info); - }) - .catch(() => {}); - }; - - const validateUsernameAndToken = (username: String, token: String) => { - if (username === undefined || username === null || username === '') - return false; - - if (token === undefined || token === null || token === '') return false; - - return true; - }; - - const authenticate = (type: AuthType) => { - switch (type) { - case AuthType.GitHub: - { - const username = githubUsername; - const token = githubPersonalAuthToken; - if (validateUsernameAndToken(username, token)) - fetchGitHubToken(username, token); - } - break; - default: - break; - } - }; - - const LoginIndicator = () => { - const ls = localStorage.getItem('githubLoginDetails'); - if (ls !== null) { - const obj = ls || loginDetails ? JSON.parse(ls) : loginDetails; - return ( - - {`Welcome, ${obj.name}!`} -
- Logout -
- ); - } - return ( - - Welcome, guest! - - ); - }; - - return ( - -
- -
- - - - - - - GitHub - - - - - - - - - - - - - - - - -
- ); -}; diff --git a/packages/core/src/layout/LoginPage/index.ts b/packages/core/src/layout/LoginPage/index.ts deleted file mode 100644 index caa94bd6d7..0000000000 --- a/packages/core/src/layout/LoginPage/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { LoginPage } from './LoginPage'; diff --git a/packages/core/src/layout/index.ts b/packages/core/src/layout/index.ts index c9dbae9ca2..e8341e1124 100644 --- a/packages/core/src/layout/index.ts +++ b/packages/core/src/layout/index.ts @@ -21,7 +21,6 @@ export * from './Header'; export * from './HeaderLabel'; export * from './HomepageTimer'; export * from './InfoCard'; -export * from './LoginPage'; export * from './Page'; export * from './Sidebar'; export * from './TabbedCard'; From 43d13afba5f3e430675899b46c4d63683c491531 Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Sat, 30 May 2020 13:57:26 +0200 Subject: [PATCH 10/15] Add catalog-model dep --- plugins/catalog/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index cd8548bdde..0cdc5d2618 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -19,6 +19,7 @@ "dependencies": { "@backstage/core": "^0.1.1-alpha.6", "@backstage/theme": "^0.1.1-alpha.6", + "@backstage/catalog-model": "^0.1.1-alpha.6", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", From a0a891a193d389b6f9c2dd8d89fc91f2b4d83230 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Sat, 30 May 2020 21:37:54 +0900 Subject: [PATCH 11/15] Fix overlap issue with Sidebar and Dismissable banner (#1077) * Increase zIndex of Sidebar to be on top of the page As of now, the Sidebar is being overlapped by Dismissable banner component * Unset z-index of Dismissable Banner It inherits a z-index of 1400 from Material UI's snackbar component --- .../core/src/components/DismissableBanner/DismissableBanner.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/components/DismissableBanner/DismissableBanner.tsx b/packages/core/src/components/DismissableBanner/DismissableBanner.tsx index 7190ee8725..b7a36c8b98 100644 --- a/packages/core/src/components/DismissableBanner/DismissableBanner.tsx +++ b/packages/core/src/components/DismissableBanner/DismissableBanner.tsx @@ -31,6 +31,7 @@ const useStyles = makeStyles((theme: Theme) => ({ marginTop: -theme.spacing(3), display: 'flex', flexFlow: 'row nowrap', + zIndex: 'unset', }, icon: { fontSize: 20, From a8e42631d2cf77a616c0e9645ce5468f32095f4a Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sat, 30 May 2020 14:39:30 +0200 Subject: [PATCH 12/15] build(deps): bump rollup-plugin-dts from 1.4.6 to 1.4.7 (#1021) Bumps [rollup-plugin-dts](https://github.com/Swatinem/rollup-plugin-dts) from 1.4.6 to 1.4.7. - [Release notes](https://github.com/Swatinem/rollup-plugin-dts/releases) - [Changelog](https://github.com/Swatinem/rollup-plugin-dts/blob/master/CHANGELOG.md) - [Commits](https://github.com/Swatinem/rollup-plugin-dts/compare/v1.4.6...v1.4.7) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index b34d5505d2..77528ae6f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18353,9 +18353,9 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: inherits "^2.0.1" rollup-plugin-dts@^1.4.6: - version "1.4.6" - resolved "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-1.4.6.tgz#26e3da11ec647cfffee9658b63fa41d67e7840b9" - integrity sha512-1o5+eI97Ne8zXJrgdasn/xGi0xKuovCQwZRtPI2Lfl/c6qa9jQTFbn60NwOx3gWJ89K265/6kpDuahnBbplyWA== + version "1.4.7" + resolved "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-1.4.7.tgz#6255147ac777314c0725a1efcb42df10fe282243" + integrity sha512-QkunbJ96yUNkW95k/Vd6SdTjCbWSG0rMVUtpHSCwfg078Z7vbDaBnfz/gkSqR5h8WFMxoccBT4aodHm6387Jvg== optionalDependencies: "@babel/code-frame" "^7.8.3" From 53ec396aebce6c4bcc53e10308f9b1e617c5a97c Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2020 09:34:15 +0200 Subject: [PATCH 13/15] build(deps): bump clsx from 1.1.0 to 1.1.1 (#1084) Bumps [clsx](https://github.com/lukeed/clsx) from 1.1.0 to 1.1.1. - [Release notes](https://github.com/lukeed/clsx/releases) - [Commits](https://github.com/lukeed/clsx/compare/v1.1.0...v1.1.1) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 77528ae6f7..54d9ec857f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6871,9 +6871,9 @@ clone@^1.0.2: integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= clsx@^1.0.1, clsx@^1.0.2, clsx@^1.0.4, clsx@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.0.tgz#62937c6adfea771247c34b54d320fb99624f5702" - integrity sha512-3avwM37fSK5oP6M5rQ9CNe99lwxhXDOeSWVPAOYF6OazUTgZCMb0yWlJpmdD74REy1gkEaFiub2ULv4fq9GUhA== + version "1.1.1" + resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" + integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== cmd-shim@^3.0.0, cmd-shim@^3.0.3: version "3.0.3" From cc44775ecdb573a426c988bb6dba7b9d4e63967f Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2020 09:36:52 +0200 Subject: [PATCH 14/15] build(deps-dev): bump cypress from 4.6.0 to 4.7.0 (#1086) Bumps [cypress](https://github.com/cypress-io/cypress) from 4.6.0 to 4.7.0. - [Release notes](https://github.com/cypress-io/cypress/releases) - [Commits](https://github.com/cypress-io/cypress/compare/v4.6.0...v4.7.0) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 54d9ec857f..056b6dd57b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7866,9 +7866,9 @@ cyclist@^1.0.1: integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= cypress@*, cypress@^4.2.0: - version "4.6.0" - resolved "https://registry.npmjs.org/cypress/-/cypress-4.6.0.tgz#ac76786500580df1347a0a50be63e5c59ffbef59" - integrity sha512-vIPXAceRP+Nxvnm/O9ruY9EQaRGmVVybtk9F1sfC9mH3067YbitrdBTynaaLuHFj90p9e0U2ZCV7OkX4x4V/Wg== + version "4.7.0" + resolved "https://registry.npmjs.org/cypress/-/cypress-4.7.0.tgz#3ea29bddaf9a1faeaa5b8d54b60a84ed1cafa83d" + integrity sha512-Vav6wUFhPRlImIND/2lOQlUnAWzgCC/iXyJlJjX9nJOJul5LC1vUpf/m8Oiae870PFPyT0ZLLwPHKTXZNdXmHw== dependencies: "@cypress/listr-verbose-renderer" "0.4.1" "@cypress/request" "2.88.5" From 15a4d9357631c026e926f4034fc90e9311b3e739 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2020 09:45:15 +0200 Subject: [PATCH 15/15] build(deps-dev): bump start-server-and-test from 1.10.11 to 1.11.0 (#1085) Bumps [start-server-and-test](https://github.com/bahmutov/start-server-and-test) from 1.10.11 to 1.11.0. - [Release notes](https://github.com/bahmutov/start-server-and-test/releases) - [Commits](https://github.com/bahmutov/start-server-and-test/compare/v1.10.11...v1.11.0) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 056b6dd57b..b115b0bf9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19328,9 +19328,9 @@ stacktrace-js@^2.0.0: stacktrace-gps "^3.0.4" start-server-and-test@^1.10.11: - version "1.10.11" - resolved "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-1.10.11.tgz#24290ee8a5ed15f4a34e9bb45a5d6ff93c93c83e" - integrity sha512-CZilaj293uQWdD4vgOxTOuzlCWxOyBm6bzmH1r6OGLG/q5zcBmGYevLfOimkg0kSn9jLHwYSXLuoKG/DDQJhww== + version "1.11.0" + resolved "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-1.11.0.tgz#1b1a83d062b0028ee6e296bb4e0231f2d8b2f4af" + integrity sha512-FhkJFYL/lvbd0tKWvbxWNWjtFtq3Zpa09QDjA8EUH88AsgNL4hkAAKYNmbac+fFM8/GIZoJ1Mj4mm3SMI0X1bA== dependencies: bluebird "3.7.2" check-more-types "2.24.0"