@backstage/config-loader 0.6.9 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/dist/index.cjs.js +160 -28
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +44 -18
- package/dist/index.esm.js +158 -28
- package/dist/index.esm.js.map +1 -1
- package/package.json +13 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# @backstage/config-loader
|
|
2
2
|
|
|
3
|
+
## 0.7.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 0611f3b3e2: Reading app config from a remote server
|
|
8
|
+
- 26c5659c97: Bump msw to the same version as the rest
|
|
9
|
+
|
|
10
|
+
## 0.7.1
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- 10615525f3: Switch to use the json and observable types from `@backstage/types`
|
|
15
|
+
- ea21f7f567: bump `typescript-json-schema` from 0.50.1 to 0.51.0
|
|
16
|
+
- Updated dependencies
|
|
17
|
+
- @backstage/config@0.1.11
|
|
18
|
+
- @backstage/cli-common@0.1.5
|
|
19
|
+
- @backstage/errors@0.1.4
|
|
20
|
+
|
|
21
|
+
## 0.7.0
|
|
22
|
+
|
|
23
|
+
### Minor Changes
|
|
24
|
+
|
|
25
|
+
- 7e97d0b8c1: Removed the `EnvFunc` public export. Its only usage was to be passed in to `LoadConfigOptions.experimentalEnvFunc`. If you were using this type, add a definition in your own project instead with the signature `(name: string) => Promise<string | undefined>`.
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- 223e8de6b4: Configuration schema errors are now filtered using the provided visibility option. This means that schema errors due to missing backend configuration will no longer break frontend builds.
|
|
30
|
+
- 7e97d0b8c1: Add public tags and documentation
|
|
31
|
+
- 36e67d2f24: Internal updates to apply more strict checks to throw errors.
|
|
32
|
+
- Updated dependencies
|
|
33
|
+
- @backstage/errors@0.1.3
|
|
34
|
+
|
|
35
|
+
## 0.6.10
|
|
36
|
+
|
|
37
|
+
### Patch Changes
|
|
38
|
+
|
|
39
|
+
- 957e4b3351: Updated dependencies
|
|
40
|
+
- Updated dependencies
|
|
41
|
+
- @backstage/cli-common@0.1.4
|
|
42
|
+
|
|
3
43
|
## 0.6.9
|
|
4
44
|
|
|
5
45
|
### Patch Changes
|
package/dist/index.cjs.js
CHANGED
|
@@ -2,22 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
+
var errors = require('@backstage/errors');
|
|
5
6
|
var yaml = require('yaml');
|
|
6
7
|
var path = require('path');
|
|
7
8
|
var Ajv = require('ajv');
|
|
8
9
|
var mergeAllOf = require('json-schema-merge-allof');
|
|
10
|
+
var traverse = require('json-schema-traverse');
|
|
9
11
|
var config = require('@backstage/config');
|
|
10
12
|
var fs = require('fs-extra');
|
|
11
13
|
var typescriptJsonSchema = require('typescript-json-schema');
|
|
12
14
|
var chokidar = require('chokidar');
|
|
15
|
+
var fetch = require('node-fetch');
|
|
13
16
|
|
|
14
17
|
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
15
18
|
|
|
16
19
|
var yaml__default = /*#__PURE__*/_interopDefaultLegacy(yaml);
|
|
17
20
|
var Ajv__default = /*#__PURE__*/_interopDefaultLegacy(Ajv);
|
|
18
21
|
var mergeAllOf__default = /*#__PURE__*/_interopDefaultLegacy(mergeAllOf);
|
|
22
|
+
var traverse__default = /*#__PURE__*/_interopDefaultLegacy(traverse);
|
|
19
23
|
var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
|
|
20
24
|
var chokidar__default = /*#__PURE__*/_interopDefaultLegacy(chokidar);
|
|
25
|
+
var fetch__default = /*#__PURE__*/_interopDefaultLegacy(fetch);
|
|
21
26
|
|
|
22
27
|
const ENV_PREFIX = "APP_CONFIG_";
|
|
23
28
|
const CONFIG_KEY_PART_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z][a-z0-9]*)*$/i;
|
|
@@ -65,6 +70,7 @@ function safeJsonParse(str) {
|
|
|
65
70
|
try {
|
|
66
71
|
return [null, JSON.parse(str)];
|
|
67
72
|
} catch (err) {
|
|
73
|
+
errors.assertError(err);
|
|
68
74
|
return [err, str];
|
|
69
75
|
}
|
|
70
76
|
}
|
|
@@ -95,6 +101,7 @@ async function applyConfigTransforms(initialDir, input, transforms) {
|
|
|
95
101
|
break;
|
|
96
102
|
}
|
|
97
103
|
} catch (error) {
|
|
104
|
+
errors.assertError(error);
|
|
98
105
|
throw new Error(`error at ${path}, ${error.message}`);
|
|
99
106
|
}
|
|
100
107
|
}
|
|
@@ -232,7 +239,7 @@ const CONFIG_VISIBILITIES = ["frontend", "backend", "secret"];
|
|
|
232
239
|
const DEFAULT_CONFIG_VISIBILITY = "backend";
|
|
233
240
|
|
|
234
241
|
function compileConfigSchemas(schemas) {
|
|
235
|
-
const
|
|
242
|
+
const visibilityByDataPath = new Map();
|
|
236
243
|
const ajv = new Ajv__default['default']({
|
|
237
244
|
allErrors: true,
|
|
238
245
|
allowUnionTypes: true,
|
|
@@ -252,7 +259,7 @@ function compileConfigSchemas(schemas) {
|
|
|
252
259
|
}
|
|
253
260
|
if (visibility && visibility !== "backend") {
|
|
254
261
|
const normalizedPath = context.dataPath.replace(/\['?(.*?)'?\]/g, (_, segment) => `/${segment}`);
|
|
255
|
-
|
|
262
|
+
visibilityByDataPath.set(normalizedPath, visibility);
|
|
256
263
|
}
|
|
257
264
|
return true;
|
|
258
265
|
};
|
|
@@ -267,23 +274,27 @@ function compileConfigSchemas(schemas) {
|
|
|
267
274
|
}
|
|
268
275
|
const merged = mergeConfigSchemas(schemas.map((_) => _.value));
|
|
269
276
|
const validate = ajv.compile(merged);
|
|
277
|
+
const visibilityBySchemaPath = new Map();
|
|
278
|
+
traverse__default['default'](merged, (schema, path) => {
|
|
279
|
+
if (schema.visibility && schema.visibility !== "backend") {
|
|
280
|
+
visibilityBySchemaPath.set(path, schema.visibility);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
270
283
|
return (configs) => {
|
|
271
284
|
var _a;
|
|
272
285
|
const config$1 = config.ConfigReader.fromConfigs(configs).get();
|
|
273
|
-
|
|
286
|
+
visibilityByDataPath.clear();
|
|
274
287
|
const valid = validate(config$1);
|
|
275
288
|
if (!valid) {
|
|
276
|
-
const errors = (_a = validate.errors) != null ? _a : [];
|
|
277
289
|
return {
|
|
278
|
-
errors:
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}),
|
|
282
|
-
visibilityByPath: new Map()
|
|
290
|
+
errors: (_a = validate.errors) != null ? _a : [],
|
|
291
|
+
visibilityByDataPath: new Map(visibilityByDataPath),
|
|
292
|
+
visibilityBySchemaPath
|
|
283
293
|
};
|
|
284
294
|
}
|
|
285
295
|
return {
|
|
286
|
-
|
|
296
|
+
visibilityByDataPath: new Map(visibilityByDataPath),
|
|
297
|
+
visibilityBySchemaPath
|
|
287
298
|
};
|
|
288
299
|
};
|
|
289
300
|
}
|
|
@@ -412,6 +423,7 @@ function compileTsSchemas(paths) {
|
|
|
412
423
|
validationKeywords: ["visibility"]
|
|
413
424
|
}, [path$1.split(path.sep).join("/")]);
|
|
414
425
|
} catch (error) {
|
|
426
|
+
errors.assertError(error);
|
|
415
427
|
if (error.message !== "type Config not found") {
|
|
416
428
|
throw error;
|
|
417
429
|
}
|
|
@@ -424,12 +436,12 @@ function compileTsSchemas(paths) {
|
|
|
424
436
|
return tsSchemas;
|
|
425
437
|
}
|
|
426
438
|
|
|
427
|
-
function filterByVisibility(data, includeVisibilities,
|
|
439
|
+
function filterByVisibility(data, includeVisibilities, visibilityByDataPath, transformFunc, withFilteredKeys) {
|
|
428
440
|
var _a;
|
|
429
441
|
const filteredKeys = new Array();
|
|
430
442
|
function transform(jsonVal, visibilityPath, filterPath) {
|
|
431
443
|
var _a2;
|
|
432
|
-
const visibility = (_a2 =
|
|
444
|
+
const visibility = (_a2 = visibilityByDataPath.get(visibilityPath)) != null ? _a2 : DEFAULT_CONFIG_VISIBILITY;
|
|
433
445
|
const isVisible = includeVisibilities.includes(visibility);
|
|
434
446
|
if (typeof jsonVal !== "object") {
|
|
435
447
|
if (isVisible) {
|
|
@@ -479,7 +491,40 @@ function filterByVisibility(data, includeVisibilities, visibilityByPath, transfo
|
|
|
479
491
|
data: (_a = transform(data, "", "")) != null ? _a : {}
|
|
480
492
|
};
|
|
481
493
|
}
|
|
494
|
+
function filterErrorsByVisibility(errors, includeVisibilities, visibilityByDataPath, visibilityBySchemaPath) {
|
|
495
|
+
if (!errors) {
|
|
496
|
+
return [];
|
|
497
|
+
}
|
|
498
|
+
if (!includeVisibilities) {
|
|
499
|
+
return errors;
|
|
500
|
+
}
|
|
501
|
+
const visibleSchemaPaths = Array.from(visibilityBySchemaPath).filter(([, v]) => includeVisibilities.includes(v)).map(([k]) => k);
|
|
502
|
+
return errors.filter((error) => {
|
|
503
|
+
var _a;
|
|
504
|
+
if (error.keyword === "type" && ["object", "array"].includes(error.params.type)) {
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
if (error.keyword === "required") {
|
|
508
|
+
const trimmedPath = error.schemaPath.slice(1, -"/required".length);
|
|
509
|
+
const fullPath = `${trimmedPath}/properties/${error.params.missingProperty}`;
|
|
510
|
+
if (visibleSchemaPaths.some((visiblePath) => visiblePath.startsWith(fullPath))) {
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const vis = (_a = visibilityByDataPath.get(error.dataPath)) != null ? _a : DEFAULT_CONFIG_VISIBILITY;
|
|
515
|
+
return vis && includeVisibilities.includes(vis);
|
|
516
|
+
});
|
|
517
|
+
}
|
|
482
518
|
|
|
519
|
+
function errorsToError(errors) {
|
|
520
|
+
const messages = errors.map(({dataPath, message, params}) => {
|
|
521
|
+
const paramStr = Object.entries(params).map(([name, value]) => `${name}=${value}`).join(" ");
|
|
522
|
+
return `Config ${message || ""} { ${paramStr} } at ${dataPath}`;
|
|
523
|
+
});
|
|
524
|
+
const error = new Error(`Config validation failed, ${messages.join("; ")}`);
|
|
525
|
+
error.messages = messages;
|
|
526
|
+
return error;
|
|
527
|
+
}
|
|
483
528
|
async function loadConfigSchema(options) {
|
|
484
529
|
var _a;
|
|
485
530
|
let schemas;
|
|
@@ -496,21 +541,20 @@ async function loadConfigSchema(options) {
|
|
|
496
541
|
return {
|
|
497
542
|
process(configs, {visibility, valueTransform, withFilteredKeys} = {}) {
|
|
498
543
|
const result = validate(configs);
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
throw error;
|
|
544
|
+
const visibleErrors = filterErrorsByVisibility(result.errors, visibility, result.visibilityByDataPath, result.visibilityBySchemaPath);
|
|
545
|
+
if (visibleErrors.length > 0) {
|
|
546
|
+
throw errorsToError(visibleErrors);
|
|
503
547
|
}
|
|
504
548
|
let processedConfigs = configs;
|
|
505
549
|
if (visibility) {
|
|
506
550
|
processedConfigs = processedConfigs.map(({data, context}) => ({
|
|
507
551
|
context,
|
|
508
|
-
...filterByVisibility(data, visibility, result.
|
|
552
|
+
...filterByVisibility(data, visibility, result.visibilityByDataPath, valueTransform, withFilteredKeys)
|
|
509
553
|
}));
|
|
510
554
|
} else if (valueTransform) {
|
|
511
555
|
processedConfigs = processedConfigs.map(({data, context}) => ({
|
|
512
556
|
context,
|
|
513
|
-
...filterByVisibility(data, Array.from(CONFIG_VISIBILITIES), result.
|
|
557
|
+
...filterByVisibility(data, Array.from(CONFIG_VISIBILITIES), result.visibilityByDataPath, valueTransform, withFilteredKeys)
|
|
514
558
|
}));
|
|
515
559
|
}
|
|
516
560
|
return processedConfigs;
|
|
@@ -524,10 +568,28 @@ async function loadConfigSchema(options) {
|
|
|
524
568
|
};
|
|
525
569
|
}
|
|
526
570
|
|
|
571
|
+
function isValidUrl(url) {
|
|
572
|
+
try {
|
|
573
|
+
new URL(url);
|
|
574
|
+
return true;
|
|
575
|
+
} catch {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
527
580
|
async function loadConfig(options) {
|
|
528
|
-
const {configRoot, experimentalEnvFunc: envFunc, watch} = options;
|
|
529
|
-
const configPaths = options.
|
|
530
|
-
|
|
581
|
+
const {configRoot, experimentalEnvFunc: envFunc, watch, remote} = options;
|
|
582
|
+
const configPaths = options.configTargets.slice().filter((e) => e.hasOwnProperty("path")).map((configTarget) => configTarget.path);
|
|
583
|
+
options.configPaths.forEach((cp) => {
|
|
584
|
+
if (!configPaths.includes(cp)) {
|
|
585
|
+
configPaths.push(cp);
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
const configUrls = options.configTargets.slice().filter((e) => e.hasOwnProperty("url")).map((configTarget) => configTarget.url);
|
|
589
|
+
if (remote === void 0 && configUrls.length > 0) {
|
|
590
|
+
throw new Error(`Remote config detected but this feature is turned off`);
|
|
591
|
+
}
|
|
592
|
+
if (configPaths.length === 0 && configUrls.length === 0) {
|
|
531
593
|
configPaths.push(path.resolve(configRoot, "app-config.yaml"));
|
|
532
594
|
const localConfig = path.resolve(configRoot, "app-config.local.yaml");
|
|
533
595
|
if (await fs__default['default'].pathExists(localConfig)) {
|
|
@@ -553,18 +615,53 @@ async function loadConfig(options) {
|
|
|
553
615
|
}
|
|
554
616
|
return configs;
|
|
555
617
|
};
|
|
618
|
+
const loadRemoteConfigFiles = async () => {
|
|
619
|
+
const configs = [];
|
|
620
|
+
const readConfigFromUrl = async (url) => {
|
|
621
|
+
const response = await fetch__default['default'](url);
|
|
622
|
+
if (!response.ok) {
|
|
623
|
+
throw new Error(`Could not read config file at ${url}`);
|
|
624
|
+
}
|
|
625
|
+
return await response.text();
|
|
626
|
+
};
|
|
627
|
+
for (let i = 0; i < configUrls.length; i++) {
|
|
628
|
+
const configUrl = configUrls[i];
|
|
629
|
+
if (!isValidUrl(configUrl)) {
|
|
630
|
+
throw new Error(`Config load path is not valid: '${configUrl}'`);
|
|
631
|
+
}
|
|
632
|
+
const remoteConfigContent = await readConfigFromUrl(configUrl);
|
|
633
|
+
if (!remoteConfigContent) {
|
|
634
|
+
throw new Error(`Config is not valid`);
|
|
635
|
+
}
|
|
636
|
+
const configYaml = yaml__default['default'].parse(remoteConfigContent);
|
|
637
|
+
const substitutionTransform = createSubstitutionTransform(env);
|
|
638
|
+
const data = await applyConfigTransforms(configRoot, configYaml, [
|
|
639
|
+
substitutionTransform
|
|
640
|
+
]);
|
|
641
|
+
configs.push({data, context: configUrl});
|
|
642
|
+
}
|
|
643
|
+
return configs;
|
|
644
|
+
};
|
|
556
645
|
let fileConfigs;
|
|
557
646
|
try {
|
|
558
647
|
fileConfigs = await loadConfigFiles();
|
|
559
648
|
} catch (error) {
|
|
560
|
-
throw new
|
|
649
|
+
throw new errors.ForwardedError("Failed to read static configuration file", error);
|
|
650
|
+
}
|
|
651
|
+
let remoteConfigs = [];
|
|
652
|
+
if (remote) {
|
|
653
|
+
try {
|
|
654
|
+
remoteConfigs = await loadRemoteConfigFiles();
|
|
655
|
+
} catch (error) {
|
|
656
|
+
throw new errors.ForwardedError(`Failed to read remote configuration file`, error);
|
|
657
|
+
}
|
|
561
658
|
}
|
|
562
659
|
const envConfigs = await readEnvConfig(process.env);
|
|
563
|
-
|
|
564
|
-
let currentSerializedConfig = JSON.stringify(fileConfigs);
|
|
660
|
+
const watchConfigFile = (watchProp) => {
|
|
565
661
|
const watcher = chokidar__default['default'].watch(configPaths, {
|
|
566
662
|
usePolling: process.env.NODE_ENV === "test"
|
|
567
663
|
});
|
|
664
|
+
let currentSerializedConfig = JSON.stringify(fileConfigs);
|
|
568
665
|
watcher.on("change", async () => {
|
|
569
666
|
try {
|
|
570
667
|
const newConfigs = await loadConfigFiles();
|
|
@@ -573,18 +670,53 @@ async function loadConfig(options) {
|
|
|
573
670
|
return;
|
|
574
671
|
}
|
|
575
672
|
currentSerializedConfig = newSerializedConfig;
|
|
576
|
-
|
|
673
|
+
watchProp.onChange([...remoteConfigs, ...newConfigs, ...envConfigs]);
|
|
577
674
|
} catch (error) {
|
|
578
675
|
console.error(`Failed to reload configuration files, ${error}`);
|
|
579
676
|
}
|
|
580
677
|
});
|
|
581
|
-
if (
|
|
582
|
-
|
|
678
|
+
if (watchProp.stopSignal) {
|
|
679
|
+
watchProp.stopSignal.then(() => {
|
|
583
680
|
watcher.close();
|
|
584
681
|
});
|
|
585
682
|
}
|
|
683
|
+
};
|
|
684
|
+
const watchRemoteConfig = (watchProp, remoteProp) => {
|
|
685
|
+
const hasConfigChanged = async (oldRemoteConfigs, newRemoteConfigs) => {
|
|
686
|
+
return JSON.stringify(oldRemoteConfigs) !== JSON.stringify(newRemoteConfigs);
|
|
687
|
+
};
|
|
688
|
+
let handle;
|
|
689
|
+
try {
|
|
690
|
+
handle = setInterval(async () => {
|
|
691
|
+
console.info(`Checking for config update`);
|
|
692
|
+
const newRemoteConfigs = await loadRemoteConfigFiles();
|
|
693
|
+
if (await hasConfigChanged(remoteConfigs, newRemoteConfigs)) {
|
|
694
|
+
remoteConfigs = newRemoteConfigs;
|
|
695
|
+
console.info(`Remote config change, reloading config ...`);
|
|
696
|
+
watchProp.onChange([...remoteConfigs, ...fileConfigs, ...envConfigs]);
|
|
697
|
+
console.info(`Remote config reloaded`);
|
|
698
|
+
}
|
|
699
|
+
}, remoteProp.reloadIntervalSeconds * 1e3);
|
|
700
|
+
} catch (error) {
|
|
701
|
+
console.error(`Failed to reload configuration files, ${error}`);
|
|
702
|
+
}
|
|
703
|
+
if (watchProp.stopSignal) {
|
|
704
|
+
watchProp.stopSignal.then(() => {
|
|
705
|
+
if (handle !== void 0) {
|
|
706
|
+
console.info(`Stopping remote config watch`);
|
|
707
|
+
clearInterval(handle);
|
|
708
|
+
handle = void 0;
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
if (watch) {
|
|
714
|
+
watchConfigFile(watch);
|
|
715
|
+
}
|
|
716
|
+
if (watch && remote) {
|
|
717
|
+
watchRemoteConfig(watch, remote);
|
|
586
718
|
}
|
|
587
|
-
return [...fileConfigs, ...envConfigs];
|
|
719
|
+
return remote ? [...remoteConfigs, ...fileConfigs, ...envConfigs] : [...fileConfigs, ...envConfigs];
|
|
588
720
|
}
|
|
589
721
|
|
|
590
722
|
exports.loadConfig = loadConfig;
|