packages: move ConfigReader and config types to separate config package
This commit is contained in:
@@ -29,6 +29,7 @@
|
||||
"backstage-cli": "bin/backstage-cli"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/config": "^0.1.1-alpha.6",
|
||||
"@hot-loader/react-dom": "^16.13.0",
|
||||
"@lerna/package-graph": "^3.18.5",
|
||||
"@lerna/project": "^3.18.0",
|
||||
|
||||
@@ -14,5 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type { AppConfig } from './types';
|
||||
export { loadConfig } from './loaders';
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AppConfig } from './types';
|
||||
import fs from 'fs-extra';
|
||||
import yaml from 'yaml';
|
||||
import { AppConfig } from '@backstage/config';
|
||||
import { paths } from '../paths';
|
||||
|
||||
type LoadConfigOptions = {
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AppConfig } from '@backstage/config';
|
||||
import { BundlingPathsOptions } from './paths';
|
||||
import { AppConfig } from '../app-config';
|
||||
|
||||
export type BundlingOptions = {
|
||||
checksEnabled: boolean;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
rules: {
|
||||
'jest/expect-expect': 0,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
# @backstage/config
|
||||
|
||||
This package provides a config API used by Backstage core, backend, and CLI.
|
||||
|
||||
## Installation
|
||||
|
||||
Do not install this package directly, it is an internal package used by [@backstage/core](https://www.npmjs.com/package/@backstage/core), [@backstage/cli](https://www.npmjs.com/package/@backstage/cli), and [@backstage/backend-common](https://www.npmjs.com/package/@backstage/backend-common). Depend on either of those instead.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Backstage Readme](https://github.com/spotify/backstage/blob/master/README.md)
|
||||
- [Backstage Documentation](https://github.com/spotify/backstage/blob/master/docs/README.md)
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@backstage/config",
|
||||
"description": "Config API used by Backstage core, backend, and CLI",
|
||||
"version": "0.1.1-alpha.6",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spotify/backstage",
|
||||
"directory": "packages/config"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"main": "dist/index.esm.js",
|
||||
"main:src": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "backstage-cli plugin:build",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.6",
|
||||
"@types/jest": "^25.2.2",
|
||||
"@types/node": "^12.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*.{js,d.ts}"
|
||||
]
|
||||
}
|
||||
@@ -14,4 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type AppConfig = any;
|
||||
export type {
|
||||
AppConfig,
|
||||
Config,
|
||||
JsonArray,
|
||||
JsonObject,
|
||||
JsonValue,
|
||||
} from './types';
|
||||
export { ConfigReader } from './reader';
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ConfigReader } from './ConfigReader';
|
||||
import { ConfigReader } from './reader';
|
||||
|
||||
const DATA = {
|
||||
zero: 0,
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* 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 { AppConfig, Config, JsonValue, JsonObject } from './types';
|
||||
|
||||
const CONFIG_KEY_PART_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z][a-z0-9]*)*$/i;
|
||||
|
||||
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 Config {
|
||||
private static readonly nullReader = new ConfigReader({});
|
||||
|
||||
static fromConfigs(configs: AppConfig[]): ConfigReader {
|
||||
if (configs.length === 0) {
|
||||
return new ConfigReader({});
|
||||
}
|
||||
|
||||
// Merge together all configs info a single config with recursive fallback
|
||||
// readers, giving the first config object in the array the highest priority.
|
||||
return configs.reduceRight<ConfigReader>((previousReader, nextConfig) => {
|
||||
return new ConfigReader(nextConfig, previousReader);
|
||||
}, undefined!);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly data: JsonObject,
|
||||
private readonly fallback?: Config,
|
||||
) {}
|
||||
|
||||
getConfig(key: string): Config {
|
||||
const value = this.readValue(key);
|
||||
const fallbackConfig = this.fallback?.getConfig(key);
|
||||
if (isObject(value)) {
|
||||
return new ConfigReader(value, fallbackConfig);
|
||||
}
|
||||
if (value !== undefined) {
|
||||
throw new TypeError(typeErrorMessage(key, typeOf(value), 'object'));
|
||||
}
|
||||
return fallbackConfig ?? 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 this.fallback?.getConfigArray(key) ?? [];
|
||||
}
|
||||
|
||||
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 this.fallback?.getNumber(key);
|
||||
}
|
||||
|
||||
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 this.fallback?.getBoolean(key);
|
||||
}
|
||||
|
||||
getString(key: string): string | undefined {
|
||||
const value = this.readValue(key);
|
||||
if (validateString(key, value)) {
|
||||
return value;
|
||||
}
|
||||
return this.fallback?.getString(key);
|
||||
}
|
||||
|
||||
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 this.fallback?.getStringArray(key);
|
||||
}
|
||||
|
||||
protected 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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 type JsonObject = { [key in string]: JsonValue };
|
||||
export type JsonArray = JsonValue[];
|
||||
export type JsonValue =
|
||||
| JsonObject
|
||||
| JsonArray
|
||||
| number
|
||||
| string
|
||||
| boolean
|
||||
| null;
|
||||
|
||||
export type AppConfig = JsonObject;
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -28,6 +28,7 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/config": "^0.1.1-alpha.6",
|
||||
"@backstage/theme": "^0.1.1-alpha.6",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
|
||||
@@ -14,168 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ConfigApi, Config } from '../../definitions/ConfigApi';
|
||||
import { AppConfig } from '../../../app';
|
||||
import { ConfigReader as BaseConfigReader } from '@backstage/config';
|
||||
|
||||
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({});
|
||||
|
||||
static fromConfigs(configs: AppConfig[]): ConfigReader {
|
||||
if (configs.length === 0) {
|
||||
return new ConfigReader({});
|
||||
}
|
||||
|
||||
// Merge together all configs info a single config with recursive fallback
|
||||
// readers, giving the first config object in the array the highest priority.
|
||||
return configs.reduceRight((previousReader, nextConfig) => {
|
||||
return new ConfigReader(nextConfig, previousReader);
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
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, fallbackConfig);
|
||||
}
|
||||
if (value !== undefined) {
|
||||
throw new TypeError(typeErrorMessage(key, typeOf(value), 'object'));
|
||||
}
|
||||
return fallbackConfig ?? 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 this.fallback?.getConfigArray(key) ?? [];
|
||||
}
|
||||
|
||||
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 this.fallback?.getNumber(key);
|
||||
}
|
||||
|
||||
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 this.fallback?.getBoolean(key);
|
||||
}
|
||||
|
||||
getString(key: string): string | undefined {
|
||||
const value = this.readValue(key);
|
||||
if (validateString(key, value)) {
|
||||
return value;
|
||||
}
|
||||
return this.fallback?.getString(key);
|
||||
}
|
||||
|
||||
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 this.fallback?.getStringArray(key);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
export class ConfigReader extends BaseConfigReader {}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { IconComponent, SystemIconKey, SystemIcons } from '../icons';
|
||||
import { BackstagePlugin } from '../plugin';
|
||||
import { ApiHolder } from '../apis';
|
||||
import { AppTheme } from '../apis/definitions';
|
||||
import { AppConfig } from '@backstage/config';
|
||||
|
||||
export type BootErrorPageProps = {
|
||||
step: 'load-config';
|
||||
@@ -31,11 +32,6 @@ export type AppComponents = {
|
||||
Progress: ComponentType<{}>;
|
||||
};
|
||||
|
||||
/**
|
||||
* TBD
|
||||
*/
|
||||
export type AppConfig = any;
|
||||
|
||||
/**
|
||||
* A function that loads in the App config that will be accessible via the ConfigApi.
|
||||
*
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/config": "^0.1.1-alpha.6",
|
||||
"@backstage/core-api": "0.1.1-alpha.6",
|
||||
"@backstage/theme": "^0.1.1-alpha.6",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
|
||||
@@ -21,13 +21,13 @@ import privateExports, {
|
||||
defaultSystemIcons,
|
||||
BootErrorPageProps,
|
||||
AppConfigLoader,
|
||||
AppConfig,
|
||||
} from '@backstage/core-api';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
|
||||
import { ErrorPage } from '../layout/ErrorPage';
|
||||
import Progress from '../components/Progress';
|
||||
import { lightTheme, darkTheme } from '@backstage/theme';
|
||||
import { AppConfig } from '@backstage/config';
|
||||
|
||||
const { PrivateAppImpl } = privateExports;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user