@backstage/config-loader 0.8.1 → 0.9.3-next.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,53 @@
1
1
  # @backstage/config-loader
2
2
 
3
+ ## 0.9.3-next.0
4
+
5
+ ### Patch Changes
6
+
7
+ - f685e1398f: Loading of app configurations now reference the `@deprecated` construct from
8
+ JSDoc to determine if a property in-use has been deprecated. Users are notified
9
+ of deprecated keys in the format:
10
+
11
+ ```txt
12
+ The configuration key 'catalog.processors.githubOrg' of app-config.yaml is deprecated and may be removed soon. Configure a GitHub integration instead.
13
+ ```
14
+
15
+ When the `withDeprecatedKeys` option is set to `true` in the `process` method
16
+ of `loadConfigSchema`, the user will be notified that deprecated keys have been
17
+ identified in their app configuration.
18
+
19
+ The `backend-common` and `plugin-app-backend` packages have been updated to set
20
+ `withDeprecatedKeys` to true so that users are notified of deprecated settings
21
+ by default.
22
+
23
+ - Updated dependencies
24
+ - @backstage/config@0.1.13-next.0
25
+
26
+ ## 0.9.2
27
+
28
+ ### Patch Changes
29
+
30
+ - Updated dependencies
31
+ - @backstage/config@0.1.12
32
+ - @backstage/errors@0.2.0
33
+
34
+ ## 0.9.1
35
+
36
+ ### Patch Changes
37
+
38
+ - 84663d59a3: Bump `typescript-json-schema` from `^0.51.0` to `^0.52.0`.
39
+
40
+ ## 0.9.0
41
+
42
+ ### Minor Changes
43
+
44
+ - f6722d2458: Removed deprecated option `env` from `LoadConfigOptions` and associated tests
45
+ - 67d6cb3c7e: Removed deprecated option `configPaths` as it has been superseded by `configTargets`
46
+
47
+ ### Patch Changes
48
+
49
+ - 1e7070443d: In case remote.reloadIntervalSeconds is passed, it must be a valid positive value
50
+
3
51
  ## 0.8.1
4
52
 
5
53
  ### Patch Changes
package/dist/index.cjs.js CHANGED
@@ -64,7 +64,7 @@ function readEnvConfig(env) {
64
64
  }
65
65
  }
66
66
  }
67
- return data ? [{data, context: "env"}] : [];
67
+ return data ? [{ data, context: "env" }] : [];
68
68
  }
69
69
  function safeJsonParse(str) {
70
70
  try {
@@ -139,13 +139,13 @@ async function applyConfigTransforms(initialDir, input, transforms) {
139
139
 
140
140
  const includeFileParser = {
141
141
  ".json": async (content) => JSON.parse(content),
142
- ".yaml": async (content) => yaml__default['default'].parse(content),
143
- ".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)
144
144
  };
145
145
  function createIncludeTransform(env, readFile, substitute) {
146
146
  return async (input, baseDir) => {
147
147
  if (!isObject(input)) {
148
- return {applied: false};
148
+ return { applied: false };
149
149
  }
150
150
  const [includeKey] = Object.keys(input).filter((key) => key.startsWith("$"));
151
151
  if (includeKey) {
@@ -153,7 +153,7 @@ function createIncludeTransform(env, readFile, substitute) {
153
153
  throw new Error(`include key ${includeKey} should not have adjacent keys`);
154
154
  }
155
155
  } else {
156
- return {applied: false};
156
+ return { applied: false };
157
157
  }
158
158
  const rawIncludedValue = input[includeKey];
159
159
  if (typeof rawIncludedValue !== "string") {
@@ -168,13 +168,13 @@ function createIncludeTransform(env, readFile, substitute) {
168
168
  case "$file":
169
169
  try {
170
170
  const value = await readFile(path.resolve(baseDir, includeValue));
171
- return {applied: true, value};
171
+ return { applied: true, value };
172
172
  } catch (error) {
173
173
  throw new Error(`failed to read file ${includeValue}, ${error}`);
174
174
  }
175
175
  case "$env":
176
176
  try {
177
- return {applied: true, value: await env(includeValue)};
177
+ return { applied: true, value: await env(includeValue) };
178
178
  } catch (error) {
179
179
  throw new Error(`failed to read env ${includeValue}, ${error}`);
180
180
  }
@@ -217,7 +217,7 @@ function createIncludeTransform(env, readFile, substitute) {
217
217
  function createSubstitutionTransform(env) {
218
218
  return async (input) => {
219
219
  if (typeof input !== "string") {
220
- return {applied: false};
220
+ return { applied: false };
221
221
  }
222
222
  const parts = input.split(/(\$?\$\{[^{}]*\})/);
223
223
  for (let i = 1; i < parts.length; i += 2) {
@@ -229,9 +229,9 @@ function createSubstitutionTransform(env) {
229
229
  }
230
230
  }
231
231
  if (parts.some((part) => part === void 0)) {
232
- return {applied: true, value: void 0};
232
+ return { applied: true, value: void 0 };
233
233
  }
234
- return {applied: true, value: parts.join("")};
234
+ return { applied: true, value: parts.join("") };
235
235
  };
236
236
  }
237
237
 
@@ -239,8 +239,9 @@ const CONFIG_VISIBILITIES = ["frontend", "backend", "secret"];
239
239
  const DEFAULT_CONFIG_VISIBILITY = "backend";
240
240
 
241
241
  function compileConfigSchemas(schemas) {
242
- const visibilityByDataPath = new Map();
243
- const ajv = new Ajv__default['default']({
242
+ const visibilityByDataPath = /* @__PURE__ */ new Map();
243
+ const deprecationByDataPath = /* @__PURE__ */ new Map();
244
+ const ajv = new Ajv__default["default"]({
244
245
  allErrors: true,
245
246
  allowUnionTypes: true,
246
247
  schemas: {
@@ -264,6 +265,19 @@ function compileConfigSchemas(schemas) {
264
265
  return true;
265
266
  };
266
267
  }
268
+ }).removeKeyword("deprecated").addKeyword({
269
+ keyword: "deprecated",
270
+ metaSchema: { type: "string" },
271
+ compile(deprecationDescription) {
272
+ return (_data, context) => {
273
+ if ((context == null ? void 0 : context.dataPath) === void 0) {
274
+ return false;
275
+ }
276
+ const normalizedPath = context.dataPath.replace(/\['?(.*?)'?\]/g, (_, segment) => `/${segment}`);
277
+ deprecationByDataPath.set(normalizedPath, deprecationDescription);
278
+ return true;
279
+ };
280
+ }
267
281
  });
268
282
  for (const schema of schemas) {
269
283
  try {
@@ -274,8 +288,8 @@ function compileConfigSchemas(schemas) {
274
288
  }
275
289
  const merged = mergeConfigSchemas(schemas.map((_) => _.value));
276
290
  const validate = ajv.compile(merged);
277
- const visibilityBySchemaPath = new Map();
278
- traverse__default['default'](merged, (schema, path) => {
291
+ const visibilityBySchemaPath = /* @__PURE__ */ new Map();
292
+ traverse__default["default"](merged, (schema, path) => {
279
293
  if (schema.visibility && schema.visibility !== "backend") {
280
294
  visibilityBySchemaPath.set(path, schema.visibility);
281
295
  }
@@ -289,17 +303,19 @@ function compileConfigSchemas(schemas) {
289
303
  return {
290
304
  errors: (_a = validate.errors) != null ? _a : [],
291
305
  visibilityByDataPath: new Map(visibilityByDataPath),
292
- visibilityBySchemaPath
306
+ visibilityBySchemaPath,
307
+ deprecationByDataPath
293
308
  };
294
309
  }
295
310
  return {
296
311
  visibilityByDataPath: new Map(visibilityByDataPath),
297
- visibilityBySchemaPath
312
+ visibilityBySchemaPath,
313
+ deprecationByDataPath
298
314
  };
299
315
  };
300
316
  }
301
317
  function mergeConfigSchemas(schemas) {
302
- const merged = mergeAllOf__default['default']({allOf: schemas}, {
318
+ const merged = mergeAllOf__default["default"]({ allOf: schemas }, {
303
319
  ignoreAdditionalProperties: true,
304
320
  resolvers: {
305
321
  visibility(values, path) {
@@ -323,18 +339,18 @@ const req = typeof __non_webpack_require__ === "undefined" ? require : __non_web
323
339
  async function collectConfigSchemas(packageNames, packagePaths) {
324
340
  const schemas = new Array();
325
341
  const tsSchemaPaths = new Array();
326
- const visitedPackageVersions = new Map();
327
- const currentDir = await fs__default['default'].realpath(process.cwd());
342
+ const visitedPackageVersions = /* @__PURE__ */ new Map();
343
+ const currentDir = await fs__default["default"].realpath(process.cwd());
328
344
  async function processItem(item) {
329
345
  var _a, _b, _c, _d;
330
346
  let pkgPath = item.packagePath;
331
347
  if (pkgPath) {
332
- const pkgExists = await fs__default['default'].pathExists(pkgPath);
348
+ const pkgExists = await fs__default["default"].pathExists(pkgPath);
333
349
  if (!pkgExists) {
334
350
  return;
335
351
  }
336
352
  } else if (item.name) {
337
- const {name, parentPath} = item;
353
+ const { name, parentPath } = item;
338
354
  try {
339
355
  pkgPath = req.resolve(`${name}/package.json`, parentPath && {
340
356
  paths: [parentPath]
@@ -345,13 +361,13 @@ async function collectConfigSchemas(packageNames, packagePaths) {
345
361
  if (!pkgPath) {
346
362
  return;
347
363
  }
348
- const pkg = await fs__default['default'].readJson(pkgPath);
364
+ const pkg = await fs__default["default"].readJson(pkgPath);
349
365
  let versions = visitedPackageVersions.get(pkg.name);
350
366
  if (versions == null ? void 0 : versions.has(pkg.version)) {
351
367
  return;
352
368
  }
353
369
  if (!versions) {
354
- versions = new Set();
370
+ versions = /* @__PURE__ */ new Set();
355
371
  visitedPackageVersions.set(pkg.name, versions);
356
372
  }
357
373
  versions.add(pkg.version);
@@ -377,7 +393,7 @@ async function collectConfigSchemas(packageNames, packagePaths) {
377
393
  tsSchemaPaths.push(path.relative(currentDir, path.resolve(path.dirname(pkgPath), pkg.configSchema)));
378
394
  } else {
379
395
  const path$1 = path.resolve(path.dirname(pkgPath), pkg.configSchema);
380
- const value = await fs__default['default'].readJson(path$1);
396
+ const value = await fs__default["default"].readJson(path$1);
381
397
  schemas.push({
382
398
  value,
383
399
  path: path.relative(currentDir, path$1)
@@ -390,11 +406,11 @@ async function collectConfigSchemas(packageNames, packagePaths) {
390
406
  });
391
407
  }
392
408
  }
393
- await Promise.all(depNames.map((depName) => processItem({name: depName, parentPath: pkgPath})));
409
+ await Promise.all(depNames.map((depName) => processItem({ name: depName, parentPath: pkgPath })));
394
410
  }
395
411
  await Promise.all([
396
- ...packageNames.map((name) => processItem({name, parentPath: currentDir})),
397
- ...packagePaths.map((path) => processItem({name: path, packagePath: path}))
412
+ ...packageNames.map((name) => processItem({ name, parentPath: currentDir })),
413
+ ...packagePaths.map((path) => processItem({ name: path, packagePath: path }))
398
414
  ]);
399
415
  const tsSchemas = compileTsSchemas(tsSchemaPaths);
400
416
  return schemas.concat(tsSchemas);
@@ -420,7 +436,7 @@ function compileTsSchemas(paths) {
420
436
  try {
421
437
  value = typescriptJsonSchema.generateSchema(program, "Config", {
422
438
  required: true,
423
- validationKeywords: ["visibility"]
439
+ validationKeywords: ["visibility", "deprecated"]
424
440
  }, [path$1.split(path.sep).join("/")]);
425
441
  } catch (error) {
426
442
  errors.assertError(error);
@@ -431,22 +447,27 @@ function compileTsSchemas(paths) {
431
447
  if (!value) {
432
448
  throw new Error(`Invalid schema in ${path$1}, missing Config export`);
433
449
  }
434
- return {path: path$1, value};
450
+ return { path: path$1, value };
435
451
  });
436
452
  return tsSchemas;
437
453
  }
438
454
 
439
- function filterByVisibility(data, includeVisibilities, visibilityByDataPath, transformFunc, withFilteredKeys) {
455
+ function filterByVisibility(data, includeVisibilities, visibilityByDataPath, deprecationByDataPath, transformFunc, withFilteredKeys, withDeprecatedKeys) {
440
456
  var _a;
441
457
  const filteredKeys = new Array();
458
+ const deprecatedKeys = new Array();
442
459
  function transform(jsonVal, visibilityPath, filterPath) {
443
460
  var _a2;
444
461
  const visibility = (_a2 = visibilityByDataPath.get(visibilityPath)) != null ? _a2 : DEFAULT_CONFIG_VISIBILITY;
445
462
  const isVisible = includeVisibilities.includes(visibility);
463
+ const deprecation = deprecationByDataPath.get(visibilityPath);
464
+ if (deprecation) {
465
+ deprecatedKeys.push({ key: filterPath, description: deprecation });
466
+ }
446
467
  if (typeof jsonVal !== "object") {
447
468
  if (isVisible) {
448
469
  if (transformFunc) {
449
- return transformFunc(jsonVal, {visibility});
470
+ return transformFunc(jsonVal, { visibility });
450
471
  }
451
472
  return jsonVal;
452
473
  }
@@ -493,6 +514,7 @@ function filterByVisibility(data, includeVisibilities, visibilityByDataPath, tra
493
514
  }
494
515
  return {
495
516
  filteredKeys: withFilteredKeys ? filteredKeys : void 0,
517
+ deprecatedKeys: withDeprecatedKeys ? deprecatedKeys : void 0,
496
518
  data: (_a = transform(data, "", "")) != null ? _a : {}
497
519
  };
498
520
  }
@@ -522,7 +544,7 @@ function filterErrorsByVisibility(errors, includeVisibilities, visibilityByDataP
522
544
  }
523
545
 
524
546
  function errorsToError(errors) {
525
- const messages = errors.map(({dataPath, message, params}) => {
547
+ const messages = errors.map(({ dataPath, message, params }) => {
526
548
  const paramStr = Object.entries(params).map(([name, value]) => `${name}=${value}`).join(" ");
527
549
  return `Config ${message || ""} { ${paramStr} } at ${dataPath}`;
528
550
  });
@@ -536,7 +558,7 @@ async function loadConfigSchema(options) {
536
558
  if ("dependencies" in options) {
537
559
  schemas = await collectConfigSchemas(options.dependencies, (_a = options.packagePaths) != null ? _a : []);
538
560
  } else {
539
- const {serialized} = options;
561
+ const { serialized } = options;
540
562
  if ((serialized == null ? void 0 : serialized.backstageConfigSchemaVersion) !== 1) {
541
563
  throw new Error("Serialized configuration schema is invalid or has an invalid version number");
542
564
  }
@@ -544,7 +566,7 @@ async function loadConfigSchema(options) {
544
566
  }
545
567
  const validate = compileConfigSchemas(schemas);
546
568
  return {
547
- process(configs, {visibility, valueTransform, withFilteredKeys} = {}) {
569
+ process(configs, { visibility, valueTransform, withFilteredKeys, withDeprecatedKeys } = {}) {
548
570
  const result = validate(configs);
549
571
  const visibleErrors = filterErrorsByVisibility(result.errors, visibility, result.visibilityByDataPath, result.visibilityBySchemaPath);
550
572
  if (visibleErrors.length > 0) {
@@ -552,14 +574,14 @@ async function loadConfigSchema(options) {
552
574
  }
553
575
  let processedConfigs = configs;
554
576
  if (visibility) {
555
- processedConfigs = processedConfigs.map(({data, context}) => ({
577
+ processedConfigs = processedConfigs.map(({ data, context }) => ({
556
578
  context,
557
- ...filterByVisibility(data, visibility, result.visibilityByDataPath, valueTransform, withFilteredKeys)
579
+ ...filterByVisibility(data, visibility, result.visibilityByDataPath, result.deprecationByDataPath, valueTransform, withFilteredKeys, withDeprecatedKeys)
558
580
  }));
559
581
  } else if (valueTransform) {
560
- processedConfigs = processedConfigs.map(({data, context}) => ({
582
+ processedConfigs = processedConfigs.map(({ data, context }) => ({
561
583
  context,
562
- ...filterByVisibility(data, Array.from(CONFIG_VISIBILITIES), result.visibilityByDataPath, valueTransform, withFilteredKeys)
584
+ ...filterByVisibility(data, Array.from(CONFIG_VISIBILITIES), result.visibilityByDataPath, result.deprecationByDataPath, valueTransform, withFilteredKeys, withDeprecatedKeys)
563
585
  }));
564
586
  }
565
587
  return processedConfigs;
@@ -583,21 +605,20 @@ function isValidUrl(url) {
583
605
  }
584
606
 
585
607
  async function loadConfig(options) {
586
- const {configRoot, experimentalEnvFunc: envFunc, watch, remote} = options;
608
+ const { configRoot, experimentalEnvFunc: envFunc, watch, remote } = options;
587
609
  const configPaths = options.configTargets.slice().filter((e) => e.hasOwnProperty("path")).map((configTarget) => configTarget.path);
588
- options.configPaths.forEach((cp) => {
589
- if (!configPaths.includes(cp)) {
590
- configPaths.push(cp);
591
- }
592
- });
593
610
  const configUrls = options.configTargets.slice().filter((e) => e.hasOwnProperty("url")).map((configTarget) => configTarget.url);
594
- if (remote === void 0 && configUrls.length > 0) {
595
- throw new Error(`Remote config detected but this feature is turned off`);
611
+ if (remote === void 0) {
612
+ if (configUrls.length > 0) {
613
+ 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.`);
614
+ }
615
+ } else if (remote.reloadIntervalSeconds <= 0) {
616
+ throw new Error(`Remote config must be contain a non zero reloadIntervalSeconds: <seconds> value`);
596
617
  }
597
618
  if (configPaths.length === 0 && configUrls.length === 0) {
598
619
  configPaths.push(path.resolve(configRoot, "app-config.yaml"));
599
620
  const localConfig = path.resolve(configRoot, "app-config.local.yaml");
600
- if (await fs__default['default'].pathExists(localConfig)) {
621
+ if (await fs__default["default"].pathExists(localConfig)) {
601
622
  configPaths.push(localConfig);
602
623
  }
603
624
  }
@@ -609,21 +630,21 @@ async function loadConfig(options) {
609
630
  throw new Error(`Config load path is not absolute: '${configPath}'`);
610
631
  }
611
632
  const dir = path.dirname(configPath);
612
- const readFile = (path$1) => fs__default['default'].readFile(path.resolve(dir, path$1), "utf8");
613
- const input = yaml__default['default'].parse(await readFile(configPath));
633
+ const readFile = (path$1) => fs__default["default"].readFile(path.resolve(dir, path$1), "utf8");
634
+ const input = yaml__default["default"].parse(await readFile(configPath));
614
635
  const substitutionTransform = createSubstitutionTransform(env);
615
636
  const data = await applyConfigTransforms(dir, input, [
616
637
  createIncludeTransform(env, readFile, substitutionTransform),
617
638
  substitutionTransform
618
639
  ]);
619
- configs.push({data, context: path.basename(configPath)});
640
+ configs.push({ data, context: path.basename(configPath) });
620
641
  }
621
642
  return configs;
622
643
  };
623
644
  const loadRemoteConfigFiles = async () => {
624
645
  const configs = [];
625
646
  const readConfigFromUrl = async (url) => {
626
- const response = await fetch__default['default'](url);
647
+ const response = await fetch__default["default"](url);
627
648
  if (!response.ok) {
628
649
  throw new Error(`Could not read config file at ${url}`);
629
650
  }
@@ -638,12 +659,12 @@ async function loadConfig(options) {
638
659
  if (!remoteConfigContent) {
639
660
  throw new Error(`Config is not valid`);
640
661
  }
641
- const configYaml = yaml__default['default'].parse(remoteConfigContent);
662
+ const configYaml = yaml__default["default"].parse(remoteConfigContent);
642
663
  const substitutionTransform = createSubstitutionTransform(env);
643
664
  const data = await applyConfigTransforms(configRoot, configYaml, [
644
665
  substitutionTransform
645
666
  ]);
646
- configs.push({data, context: configUrl});
667
+ configs.push({ data, context: configUrl });
647
668
  }
648
669
  return configs;
649
670
  };
@@ -663,7 +684,7 @@ async function loadConfig(options) {
663
684
  }
664
685
  const envConfigs = await readEnvConfig(process.env);
665
686
  const watchConfigFile = (watchProp) => {
666
- const watcher = chokidar__default['default'].watch(configPaths, {
687
+ const watcher = chokidar__default["default"].watch(configPaths, {
667
688
  usePolling: process.env.NODE_ENV === "test"
668
689
  });
669
690
  let currentSerializedConfig = JSON.stringify(fileConfigs);