@backstage/config-loader 0.7.1 → 0.9.0

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 CHANGED
@@ -1,5 +1,51 @@
1
1
  # @backstage/config-loader
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f6722d2458: Removed deprecated option `env` from `LoadConfigOptions` and associated tests
8
+ - 67d6cb3c7e: Removed deprecated option `configPaths` as it has been superseded by `configTargets`
9
+
10
+ ### Patch Changes
11
+
12
+ - 1e7070443d: In case remote.reloadIntervalSeconds is passed, it must be a valid positive value
13
+
14
+ ## 0.8.1
15
+
16
+ ### Patch Changes
17
+
18
+ - b055a6addc: Align on usage of `cross-fetch` vs `node-fetch` in frontend vs backend packages, and remove some unnecessary imports of either one of them
19
+ - 4bea7b81d3: Uses key visibility as fallback in non-object arrays
20
+
21
+ ## 0.8.0
22
+
23
+ ### Minor Changes
24
+
25
+ - 1e99c73c75: Update `loadConfig` to return `LoadConfigResult` instead of an array of `AppConfig`.
26
+
27
+ This function is primarily used internally by other config loaders like `loadBackendConfig` which means no changes are required for most users.
28
+
29
+ If you use `loadConfig` directly you will need to update your usage from:
30
+
31
+ ```diff
32
+ - const appConfigs = await loadConfig(options)
33
+ + const { appConfigs } = await loadConfig(options)
34
+ ```
35
+
36
+ ### Patch Changes
37
+
38
+ - 8809b6c0dd: Update the json-schema dependency version.
39
+ - Updated dependencies
40
+ - @backstage/cli-common@0.1.6
41
+
42
+ ## 0.7.2
43
+
44
+ ### Patch Changes
45
+
46
+ - 0611f3b3e2: Reading app config from a remote server
47
+ - 26c5659c97: Bump msw to the same version as the rest
48
+
3
49
  ## 0.7.1
4
50
 
5
51
  ### Patch Changes
package/dist/index.cjs.js CHANGED
@@ -12,6 +12,7 @@ var config = require('@backstage/config');
12
12
  var fs = require('fs-extra');
13
13
  var typescriptJsonSchema = require('typescript-json-schema');
14
14
  var chokidar = require('chokidar');
15
+ var fetch = require('node-fetch');
15
16
 
16
17
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
17
18
 
@@ -21,6 +22,7 @@ var mergeAllOf__default = /*#__PURE__*/_interopDefaultLegacy(mergeAllOf);
21
22
  var traverse__default = /*#__PURE__*/_interopDefaultLegacy(traverse);
22
23
  var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
23
24
  var chokidar__default = /*#__PURE__*/_interopDefaultLegacy(chokidar);
25
+ var fetch__default = /*#__PURE__*/_interopDefaultLegacy(fetch);
24
26
 
25
27
  const ENV_PREFIX = "APP_CONFIG_";
26
28
  const CONFIG_KEY_PART_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z][a-z0-9]*)*$/i;
@@ -62,7 +64,7 @@ function readEnvConfig(env) {
62
64
  }
63
65
  }
64
66
  }
65
- return data ? [{data, context: "env"}] : [];
67
+ return data ? [{ data, context: "env" }] : [];
66
68
  }
67
69
  function safeJsonParse(str) {
68
70
  try {
@@ -137,13 +139,13 @@ async function applyConfigTransforms(initialDir, input, transforms) {
137
139
 
138
140
  const includeFileParser = {
139
141
  ".json": async (content) => JSON.parse(content),
140
- ".yaml": async (content) => yaml__default['default'].parse(content),
141
- ".yml": async (content) => yaml__default['default'].parse(content)
142
+ ".yaml": async (content) => yaml__default["default"].parse(content),
143
+ ".yml": async (content) => yaml__default["default"].parse(content)
142
144
  };
143
145
  function createIncludeTransform(env, readFile, substitute) {
144
146
  return async (input, baseDir) => {
145
147
  if (!isObject(input)) {
146
- return {applied: false};
148
+ return { applied: false };
147
149
  }
148
150
  const [includeKey] = Object.keys(input).filter((key) => key.startsWith("$"));
149
151
  if (includeKey) {
@@ -151,7 +153,7 @@ function createIncludeTransform(env, readFile, substitute) {
151
153
  throw new Error(`include key ${includeKey} should not have adjacent keys`);
152
154
  }
153
155
  } else {
154
- return {applied: false};
156
+ return { applied: false };
155
157
  }
156
158
  const rawIncludedValue = input[includeKey];
157
159
  if (typeof rawIncludedValue !== "string") {
@@ -166,13 +168,13 @@ function createIncludeTransform(env, readFile, substitute) {
166
168
  case "$file":
167
169
  try {
168
170
  const value = await readFile(path.resolve(baseDir, includeValue));
169
- return {applied: true, value};
171
+ return { applied: true, value };
170
172
  } catch (error) {
171
173
  throw new Error(`failed to read file ${includeValue}, ${error}`);
172
174
  }
173
175
  case "$env":
174
176
  try {
175
- return {applied: true, value: await env(includeValue)};
177
+ return { applied: true, value: await env(includeValue) };
176
178
  } catch (error) {
177
179
  throw new Error(`failed to read env ${includeValue}, ${error}`);
178
180
  }
@@ -215,7 +217,7 @@ function createIncludeTransform(env, readFile, substitute) {
215
217
  function createSubstitutionTransform(env) {
216
218
  return async (input) => {
217
219
  if (typeof input !== "string") {
218
- return {applied: false};
220
+ return { applied: false };
219
221
  }
220
222
  const parts = input.split(/(\$?\$\{[^{}]*\})/);
221
223
  for (let i = 1; i < parts.length; i += 2) {
@@ -227,9 +229,9 @@ function createSubstitutionTransform(env) {
227
229
  }
228
230
  }
229
231
  if (parts.some((part) => part === void 0)) {
230
- return {applied: true, value: void 0};
232
+ return { applied: true, value: void 0 };
231
233
  }
232
- return {applied: true, value: parts.join("")};
234
+ return { applied: true, value: parts.join("") };
233
235
  };
234
236
  }
235
237
 
@@ -237,8 +239,8 @@ const CONFIG_VISIBILITIES = ["frontend", "backend", "secret"];
237
239
  const DEFAULT_CONFIG_VISIBILITY = "backend";
238
240
 
239
241
  function compileConfigSchemas(schemas) {
240
- const visibilityByDataPath = new Map();
241
- const ajv = new Ajv__default['default']({
242
+ const visibilityByDataPath = /* @__PURE__ */ new Map();
243
+ const ajv = new Ajv__default["default"]({
242
244
  allErrors: true,
243
245
  allowUnionTypes: true,
244
246
  schemas: {
@@ -272,8 +274,8 @@ function compileConfigSchemas(schemas) {
272
274
  }
273
275
  const merged = mergeConfigSchemas(schemas.map((_) => _.value));
274
276
  const validate = ajv.compile(merged);
275
- const visibilityBySchemaPath = new Map();
276
- traverse__default['default'](merged, (schema, path) => {
277
+ const visibilityBySchemaPath = /* @__PURE__ */ new Map();
278
+ traverse__default["default"](merged, (schema, path) => {
277
279
  if (schema.visibility && schema.visibility !== "backend") {
278
280
  visibilityBySchemaPath.set(path, schema.visibility);
279
281
  }
@@ -297,7 +299,7 @@ function compileConfigSchemas(schemas) {
297
299
  };
298
300
  }
299
301
  function mergeConfigSchemas(schemas) {
300
- const merged = mergeAllOf__default['default']({allOf: schemas}, {
302
+ const merged = mergeAllOf__default["default"]({ allOf: schemas }, {
301
303
  ignoreAdditionalProperties: true,
302
304
  resolvers: {
303
305
  visibility(values, path) {
@@ -321,18 +323,18 @@ const req = typeof __non_webpack_require__ === "undefined" ? require : __non_web
321
323
  async function collectConfigSchemas(packageNames, packagePaths) {
322
324
  const schemas = new Array();
323
325
  const tsSchemaPaths = new Array();
324
- const visitedPackageVersions = new Map();
325
- const currentDir = await fs__default['default'].realpath(process.cwd());
326
+ const visitedPackageVersions = /* @__PURE__ */ new Map();
327
+ const currentDir = await fs__default["default"].realpath(process.cwd());
326
328
  async function processItem(item) {
327
329
  var _a, _b, _c, _d;
328
330
  let pkgPath = item.packagePath;
329
331
  if (pkgPath) {
330
- const pkgExists = await fs__default['default'].pathExists(pkgPath);
332
+ const pkgExists = await fs__default["default"].pathExists(pkgPath);
331
333
  if (!pkgExists) {
332
334
  return;
333
335
  }
334
336
  } else if (item.name) {
335
- const {name, parentPath} = item;
337
+ const { name, parentPath } = item;
336
338
  try {
337
339
  pkgPath = req.resolve(`${name}/package.json`, parentPath && {
338
340
  paths: [parentPath]
@@ -343,13 +345,13 @@ async function collectConfigSchemas(packageNames, packagePaths) {
343
345
  if (!pkgPath) {
344
346
  return;
345
347
  }
346
- const pkg = await fs__default['default'].readJson(pkgPath);
348
+ const pkg = await fs__default["default"].readJson(pkgPath);
347
349
  let versions = visitedPackageVersions.get(pkg.name);
348
350
  if (versions == null ? void 0 : versions.has(pkg.version)) {
349
351
  return;
350
352
  }
351
353
  if (!versions) {
352
- versions = new Set();
354
+ versions = /* @__PURE__ */ new Set();
353
355
  visitedPackageVersions.set(pkg.name, versions);
354
356
  }
355
357
  versions.add(pkg.version);
@@ -375,7 +377,7 @@ async function collectConfigSchemas(packageNames, packagePaths) {
375
377
  tsSchemaPaths.push(path.relative(currentDir, path.resolve(path.dirname(pkgPath), pkg.configSchema)));
376
378
  } else {
377
379
  const path$1 = path.resolve(path.dirname(pkgPath), pkg.configSchema);
378
- const value = await fs__default['default'].readJson(path$1);
380
+ const value = await fs__default["default"].readJson(path$1);
379
381
  schemas.push({
380
382
  value,
381
383
  path: path.relative(currentDir, path$1)
@@ -388,11 +390,11 @@ async function collectConfigSchemas(packageNames, packagePaths) {
388
390
  });
389
391
  }
390
392
  }
391
- await Promise.all(depNames.map((depName) => processItem({name: depName, parentPath: pkgPath})));
393
+ await Promise.all(depNames.map((depName) => processItem({ name: depName, parentPath: pkgPath })));
392
394
  }
393
395
  await Promise.all([
394
- ...packageNames.map((name) => processItem({name, parentPath: currentDir})),
395
- ...packagePaths.map((path) => processItem({name: path, packagePath: path}))
396
+ ...packageNames.map((name) => processItem({ name, parentPath: currentDir })),
397
+ ...packagePaths.map((path) => processItem({ name: path, packagePath: path }))
396
398
  ]);
397
399
  const tsSchemas = compileTsSchemas(tsSchemaPaths);
398
400
  return schemas.concat(tsSchemas);
@@ -429,7 +431,7 @@ function compileTsSchemas(paths) {
429
431
  if (!value) {
430
432
  throw new Error(`Invalid schema in ${path$1}, missing Config export`);
431
433
  }
432
- return {path: path$1, value};
434
+ return { path: path$1, value };
433
435
  });
434
436
  return tsSchemas;
435
437
  }
@@ -444,7 +446,7 @@ function filterByVisibility(data, includeVisibilities, visibilityByDataPath, tra
444
446
  if (typeof jsonVal !== "object") {
445
447
  if (isVisible) {
446
448
  if (transformFunc) {
447
- return transformFunc(jsonVal, {visibility});
449
+ return transformFunc(jsonVal, { visibility });
448
450
  }
449
451
  return jsonVal;
450
452
  }
@@ -457,7 +459,12 @@ function filterByVisibility(data, includeVisibilities, visibilityByDataPath, tra
457
459
  } else if (Array.isArray(jsonVal)) {
458
460
  const arr = new Array();
459
461
  for (const [index, value] of jsonVal.entries()) {
460
- const out = transform(value, `${visibilityPath}/${index}`, `${filterPath}[${index}]`);
462
+ let path = visibilityPath;
463
+ const hasVisibilityInIndex = visibilityByDataPath.get(`${visibilityPath}/${index}`);
464
+ if (hasVisibilityInIndex || typeof value === "object") {
465
+ path = `${visibilityPath}/${index}`;
466
+ }
467
+ const out = transform(value, path, `${filterPath}[${index}]`);
461
468
  if (out !== void 0) {
462
469
  arr.push(out);
463
470
  }
@@ -515,7 +522,7 @@ function filterErrorsByVisibility(errors, includeVisibilities, visibilityByDataP
515
522
  }
516
523
 
517
524
  function errorsToError(errors) {
518
- const messages = errors.map(({dataPath, message, params}) => {
525
+ const messages = errors.map(({ dataPath, message, params }) => {
519
526
  const paramStr = Object.entries(params).map(([name, value]) => `${name}=${value}`).join(" ");
520
527
  return `Config ${message || ""} { ${paramStr} } at ${dataPath}`;
521
528
  });
@@ -529,7 +536,7 @@ async function loadConfigSchema(options) {
529
536
  if ("dependencies" in options) {
530
537
  schemas = await collectConfigSchemas(options.dependencies, (_a = options.packagePaths) != null ? _a : []);
531
538
  } else {
532
- const {serialized} = options;
539
+ const { serialized } = options;
533
540
  if ((serialized == null ? void 0 : serialized.backstageConfigSchemaVersion) !== 1) {
534
541
  throw new Error("Serialized configuration schema is invalid or has an invalid version number");
535
542
  }
@@ -537,7 +544,7 @@ async function loadConfigSchema(options) {
537
544
  }
538
545
  const validate = compileConfigSchemas(schemas);
539
546
  return {
540
- process(configs, {visibility, valueTransform, withFilteredKeys} = {}) {
547
+ process(configs, { visibility, valueTransform, withFilteredKeys } = {}) {
541
548
  const result = validate(configs);
542
549
  const visibleErrors = filterErrorsByVisibility(result.errors, visibility, result.visibilityByDataPath, result.visibilityBySchemaPath);
543
550
  if (visibleErrors.length > 0) {
@@ -545,12 +552,12 @@ async function loadConfigSchema(options) {
545
552
  }
546
553
  let processedConfigs = configs;
547
554
  if (visibility) {
548
- processedConfigs = processedConfigs.map(({data, context}) => ({
555
+ processedConfigs = processedConfigs.map(({ data, context }) => ({
549
556
  context,
550
557
  ...filterByVisibility(data, visibility, result.visibilityByDataPath, valueTransform, withFilteredKeys)
551
558
  }));
552
559
  } else if (valueTransform) {
553
- processedConfigs = processedConfigs.map(({data, context}) => ({
560
+ processedConfigs = processedConfigs.map(({ data, context }) => ({
554
561
  context,
555
562
  ...filterByVisibility(data, Array.from(CONFIG_VISIBILITIES), result.visibilityByDataPath, valueTransform, withFilteredKeys)
556
563
  }));
@@ -566,13 +573,30 @@ async function loadConfigSchema(options) {
566
573
  };
567
574
  }
568
575
 
576
+ function isValidUrl(url) {
577
+ try {
578
+ new URL(url);
579
+ return true;
580
+ } catch {
581
+ return false;
582
+ }
583
+ }
584
+
569
585
  async function loadConfig(options) {
570
- const {configRoot, experimentalEnvFunc: envFunc, watch} = options;
571
- const configPaths = options.configPaths.slice();
572
- if (configPaths.length === 0) {
586
+ const { configRoot, experimentalEnvFunc: envFunc, watch, remote } = options;
587
+ const configPaths = options.configTargets.slice().filter((e) => e.hasOwnProperty("path")).map((configTarget) => configTarget.path);
588
+ const configUrls = options.configTargets.slice().filter((e) => e.hasOwnProperty("url")).map((configTarget) => configTarget.url);
589
+ if (remote === void 0) {
590
+ if (configUrls.length > 0) {
591
+ throw new Error(`Please make sure you are passing the remote option when loading remote configurations. See https://backstage.io/docs/conf/writing#configuration-files for detailed info.`);
592
+ }
593
+ } else if (remote.reloadIntervalSeconds <= 0) {
594
+ throw new Error(`Remote config must be contain a non zero reloadIntervalSeconds: <seconds> value`);
595
+ }
596
+ if (configPaths.length === 0 && configUrls.length === 0) {
573
597
  configPaths.push(path.resolve(configRoot, "app-config.yaml"));
574
598
  const localConfig = path.resolve(configRoot, "app-config.local.yaml");
575
- if (await fs__default['default'].pathExists(localConfig)) {
599
+ if (await fs__default["default"].pathExists(localConfig)) {
576
600
  configPaths.push(localConfig);
577
601
  }
578
602
  }
@@ -584,14 +608,41 @@ async function loadConfig(options) {
584
608
  throw new Error(`Config load path is not absolute: '${configPath}'`);
585
609
  }
586
610
  const dir = path.dirname(configPath);
587
- const readFile = (path$1) => fs__default['default'].readFile(path.resolve(dir, path$1), "utf8");
588
- const input = yaml__default['default'].parse(await readFile(configPath));
611
+ const readFile = (path$1) => fs__default["default"].readFile(path.resolve(dir, path$1), "utf8");
612
+ const input = yaml__default["default"].parse(await readFile(configPath));
589
613
  const substitutionTransform = createSubstitutionTransform(env);
590
614
  const data = await applyConfigTransforms(dir, input, [
591
615
  createIncludeTransform(env, readFile, substitutionTransform),
592
616
  substitutionTransform
593
617
  ]);
594
- configs.push({data, context: path.basename(configPath)});
618
+ configs.push({ data, context: path.basename(configPath) });
619
+ }
620
+ return configs;
621
+ };
622
+ const loadRemoteConfigFiles = async () => {
623
+ const configs = [];
624
+ const readConfigFromUrl = async (url) => {
625
+ const response = await fetch__default["default"](url);
626
+ if (!response.ok) {
627
+ throw new Error(`Could not read config file at ${url}`);
628
+ }
629
+ return await response.text();
630
+ };
631
+ for (let i = 0; i < configUrls.length; i++) {
632
+ const configUrl = configUrls[i];
633
+ if (!isValidUrl(configUrl)) {
634
+ throw new Error(`Config load path is not valid: '${configUrl}'`);
635
+ }
636
+ const remoteConfigContent = await readConfigFromUrl(configUrl);
637
+ if (!remoteConfigContent) {
638
+ throw new Error(`Config is not valid`);
639
+ }
640
+ const configYaml = yaml__default["default"].parse(remoteConfigContent);
641
+ const substitutionTransform = createSubstitutionTransform(env);
642
+ const data = await applyConfigTransforms(configRoot, configYaml, [
643
+ substitutionTransform
644
+ ]);
645
+ configs.push({ data, context: configUrl });
595
646
  }
596
647
  return configs;
597
648
  };
@@ -601,12 +652,20 @@ async function loadConfig(options) {
601
652
  } catch (error) {
602
653
  throw new errors.ForwardedError("Failed to read static configuration file", error);
603
654
  }
655
+ let remoteConfigs = [];
656
+ if (remote) {
657
+ try {
658
+ remoteConfigs = await loadRemoteConfigFiles();
659
+ } catch (error) {
660
+ throw new errors.ForwardedError(`Failed to read remote configuration file`, error);
661
+ }
662
+ }
604
663
  const envConfigs = await readEnvConfig(process.env);
605
- if (watch) {
606
- let currentSerializedConfig = JSON.stringify(fileConfigs);
607
- const watcher = chokidar__default['default'].watch(configPaths, {
664
+ const watchConfigFile = (watchProp) => {
665
+ const watcher = chokidar__default["default"].watch(configPaths, {
608
666
  usePolling: process.env.NODE_ENV === "test"
609
667
  });
668
+ let currentSerializedConfig = JSON.stringify(fileConfigs);
610
669
  watcher.on("change", async () => {
611
670
  try {
612
671
  const newConfigs = await loadConfigFiles();
@@ -615,18 +674,55 @@ async function loadConfig(options) {
615
674
  return;
616
675
  }
617
676
  currentSerializedConfig = newSerializedConfig;
618
- watch.onChange([...newConfigs, ...envConfigs]);
677
+ watchProp.onChange([...remoteConfigs, ...newConfigs, ...envConfigs]);
619
678
  } catch (error) {
620
679
  console.error(`Failed to reload configuration files, ${error}`);
621
680
  }
622
681
  });
623
- if (watch.stopSignal) {
624
- watch.stopSignal.then(() => {
682
+ if (watchProp.stopSignal) {
683
+ watchProp.stopSignal.then(() => {
625
684
  watcher.close();
626
685
  });
627
686
  }
687
+ };
688
+ const watchRemoteConfig = (watchProp, remoteProp) => {
689
+ const hasConfigChanged = async (oldRemoteConfigs, newRemoteConfigs) => {
690
+ return JSON.stringify(oldRemoteConfigs) !== JSON.stringify(newRemoteConfigs);
691
+ };
692
+ let handle;
693
+ try {
694
+ handle = setInterval(async () => {
695
+ console.info(`Checking for config update`);
696
+ const newRemoteConfigs = await loadRemoteConfigFiles();
697
+ if (await hasConfigChanged(remoteConfigs, newRemoteConfigs)) {
698
+ remoteConfigs = newRemoteConfigs;
699
+ console.info(`Remote config change, reloading config ...`);
700
+ watchProp.onChange([...remoteConfigs, ...fileConfigs, ...envConfigs]);
701
+ console.info(`Remote config reloaded`);
702
+ }
703
+ }, remoteProp.reloadIntervalSeconds * 1e3);
704
+ } catch (error) {
705
+ console.error(`Failed to reload configuration files, ${error}`);
706
+ }
707
+ if (watchProp.stopSignal) {
708
+ watchProp.stopSignal.then(() => {
709
+ if (handle !== void 0) {
710
+ console.info(`Stopping remote config watch`);
711
+ clearInterval(handle);
712
+ handle = void 0;
713
+ }
714
+ });
715
+ }
716
+ };
717
+ if (watch) {
718
+ watchConfigFile(watch);
719
+ }
720
+ if (watch && remote) {
721
+ watchRemoteConfig(watch, remote);
628
722
  }
629
- return [...fileConfigs, ...envConfigs];
723
+ return {
724
+ appConfigs: remote ? [...remoteConfigs, ...fileConfigs, ...envConfigs] : [...fileConfigs, ...envConfigs]
725
+ };
630
726
  }
631
727
 
632
728
  exports.loadConfig = loadConfig;