@backstage/config-loader 0.9.3 → 0.9.6
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 +29 -0
- package/dist/index.cjs.js +17 -7
- package/dist/index.cjs.js.map +1 -1
- package/package.json +17 -13
- package/dist/index.esm.js +0 -737
- package/dist/index.esm.js.map +0 -1
package/dist/index.esm.js
DELETED
|
@@ -1,737 +0,0 @@
|
|
|
1
|
-
import { assertError, ForwardedError } from '@backstage/errors';
|
|
2
|
-
import yaml from 'yaml';
|
|
3
|
-
import { extname, resolve, dirname, sep, relative, isAbsolute, basename } from 'path';
|
|
4
|
-
import Ajv from 'ajv';
|
|
5
|
-
import mergeAllOf from 'json-schema-merge-allof';
|
|
6
|
-
import traverse from 'json-schema-traverse';
|
|
7
|
-
import { ConfigReader } from '@backstage/config';
|
|
8
|
-
import fs from 'fs-extra';
|
|
9
|
-
import { getProgramFromFiles, generateSchema } from 'typescript-json-schema';
|
|
10
|
-
import chokidar from 'chokidar';
|
|
11
|
-
import fetch from 'node-fetch';
|
|
12
|
-
|
|
13
|
-
const ENV_PREFIX = "APP_CONFIG_";
|
|
14
|
-
const CONFIG_KEY_PART_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z][a-z0-9]*)*$/i;
|
|
15
|
-
function readEnvConfig(env) {
|
|
16
|
-
var _a;
|
|
17
|
-
let data = void 0;
|
|
18
|
-
for (const [name, value] of Object.entries(env)) {
|
|
19
|
-
if (!value) {
|
|
20
|
-
continue;
|
|
21
|
-
}
|
|
22
|
-
if (name.startsWith(ENV_PREFIX)) {
|
|
23
|
-
const key = name.replace(ENV_PREFIX, "");
|
|
24
|
-
const keyParts = key.split("_");
|
|
25
|
-
let obj = data = data != null ? data : {};
|
|
26
|
-
for (const [index, part] of keyParts.entries()) {
|
|
27
|
-
if (!CONFIG_KEY_PART_PATTERN.test(part)) {
|
|
28
|
-
throw new TypeError(`Invalid env config key '${key}'`);
|
|
29
|
-
}
|
|
30
|
-
if (index < keyParts.length - 1) {
|
|
31
|
-
obj = obj[part] = (_a = obj[part]) != null ? _a : {};
|
|
32
|
-
if (typeof obj !== "object" || Array.isArray(obj)) {
|
|
33
|
-
const subKey = keyParts.slice(0, index + 1).join("_");
|
|
34
|
-
throw new TypeError(`Could not nest config for key '${key}' under existing value '${subKey}'`);
|
|
35
|
-
}
|
|
36
|
-
} else {
|
|
37
|
-
if (part in obj) {
|
|
38
|
-
throw new TypeError(`Refusing to override existing config at key '${key}'`);
|
|
39
|
-
}
|
|
40
|
-
try {
|
|
41
|
-
const [, parsedValue] = safeJsonParse(value);
|
|
42
|
-
if (parsedValue === null) {
|
|
43
|
-
throw new Error("value may not be null");
|
|
44
|
-
}
|
|
45
|
-
obj[part] = parsedValue;
|
|
46
|
-
} catch (error) {
|
|
47
|
-
throw new TypeError(`Failed to parse JSON-serialized config value for key '${key}', ${error}`);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return data ? [{ data, context: "env" }] : [];
|
|
54
|
-
}
|
|
55
|
-
function safeJsonParse(str) {
|
|
56
|
-
try {
|
|
57
|
-
return [null, JSON.parse(str)];
|
|
58
|
-
} catch (err) {
|
|
59
|
-
assertError(err);
|
|
60
|
-
return [err, str];
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function isObject(obj) {
|
|
65
|
-
if (typeof obj !== "object") {
|
|
66
|
-
return false;
|
|
67
|
-
} else if (Array.isArray(obj)) {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
return obj !== null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function applyConfigTransforms(initialDir, input, transforms) {
|
|
74
|
-
async function transform(inputObj, path, baseDir) {
|
|
75
|
-
var _a;
|
|
76
|
-
let obj = inputObj;
|
|
77
|
-
let dir = baseDir;
|
|
78
|
-
for (const tf of transforms) {
|
|
79
|
-
try {
|
|
80
|
-
const result = await tf(inputObj, baseDir);
|
|
81
|
-
if (result.applied) {
|
|
82
|
-
if (result.value === void 0) {
|
|
83
|
-
return void 0;
|
|
84
|
-
}
|
|
85
|
-
obj = result.value;
|
|
86
|
-
dir = (_a = result.newBaseDir) != null ? _a : dir;
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
} catch (error) {
|
|
90
|
-
assertError(error);
|
|
91
|
-
throw new Error(`error at ${path}, ${error.message}`);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
if (typeof obj !== "object") {
|
|
95
|
-
return obj;
|
|
96
|
-
} else if (obj === null) {
|
|
97
|
-
return void 0;
|
|
98
|
-
} else if (Array.isArray(obj)) {
|
|
99
|
-
const arr = new Array();
|
|
100
|
-
for (const [index, value] of obj.entries()) {
|
|
101
|
-
const out2 = await transform(value, `${path}[${index}]`, dir);
|
|
102
|
-
if (out2 !== void 0) {
|
|
103
|
-
arr.push(out2);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return arr;
|
|
107
|
-
}
|
|
108
|
-
const out = {};
|
|
109
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
110
|
-
if (value !== void 0) {
|
|
111
|
-
const result = await transform(value, `${path}.${key}`, dir);
|
|
112
|
-
if (result !== void 0) {
|
|
113
|
-
out[key] = result;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return out;
|
|
118
|
-
}
|
|
119
|
-
const finalData = await transform(input, "", initialDir);
|
|
120
|
-
if (!isObject(finalData)) {
|
|
121
|
-
throw new TypeError("expected object at config root");
|
|
122
|
-
}
|
|
123
|
-
return finalData;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const includeFileParser = {
|
|
127
|
-
".json": async (content) => JSON.parse(content),
|
|
128
|
-
".yaml": async (content) => yaml.parse(content),
|
|
129
|
-
".yml": async (content) => yaml.parse(content)
|
|
130
|
-
};
|
|
131
|
-
function createIncludeTransform(env, readFile, substitute) {
|
|
132
|
-
return async (input, baseDir) => {
|
|
133
|
-
if (!isObject(input)) {
|
|
134
|
-
return { applied: false };
|
|
135
|
-
}
|
|
136
|
-
const [includeKey] = Object.keys(input).filter((key) => key.startsWith("$"));
|
|
137
|
-
if (includeKey) {
|
|
138
|
-
if (Object.keys(input).length !== 1) {
|
|
139
|
-
throw new Error(`include key ${includeKey} should not have adjacent keys`);
|
|
140
|
-
}
|
|
141
|
-
} else {
|
|
142
|
-
return { applied: false };
|
|
143
|
-
}
|
|
144
|
-
const rawIncludedValue = input[includeKey];
|
|
145
|
-
if (typeof rawIncludedValue !== "string") {
|
|
146
|
-
throw new Error(`${includeKey} include value is not a string`);
|
|
147
|
-
}
|
|
148
|
-
const substituteResults = await substitute(rawIncludedValue, baseDir);
|
|
149
|
-
const includeValue = substituteResults.applied ? substituteResults.value : rawIncludedValue;
|
|
150
|
-
if (includeValue === void 0 || typeof includeValue !== "string") {
|
|
151
|
-
throw new Error(`${includeKey} substitution value was undefined`);
|
|
152
|
-
}
|
|
153
|
-
switch (includeKey) {
|
|
154
|
-
case "$file":
|
|
155
|
-
try {
|
|
156
|
-
const value = await readFile(resolve(baseDir, includeValue));
|
|
157
|
-
return { applied: true, value };
|
|
158
|
-
} catch (error) {
|
|
159
|
-
throw new Error(`failed to read file ${includeValue}, ${error}`);
|
|
160
|
-
}
|
|
161
|
-
case "$env":
|
|
162
|
-
try {
|
|
163
|
-
return { applied: true, value: await env(includeValue) };
|
|
164
|
-
} catch (error) {
|
|
165
|
-
throw new Error(`failed to read env ${includeValue}, ${error}`);
|
|
166
|
-
}
|
|
167
|
-
case "$include": {
|
|
168
|
-
const [filePath, dataPath] = includeValue.split(/#(.*)/);
|
|
169
|
-
const ext = extname(filePath);
|
|
170
|
-
const parser = includeFileParser[ext];
|
|
171
|
-
if (!parser) {
|
|
172
|
-
throw new Error(`no configuration parser available for included file ${filePath}`);
|
|
173
|
-
}
|
|
174
|
-
const path = resolve(baseDir, filePath);
|
|
175
|
-
const content = await readFile(path);
|
|
176
|
-
const newBaseDir = dirname(path);
|
|
177
|
-
const parts = dataPath ? dataPath.split(".") : [];
|
|
178
|
-
let value;
|
|
179
|
-
try {
|
|
180
|
-
value = await parser(content);
|
|
181
|
-
} catch (error) {
|
|
182
|
-
throw new Error(`failed to parse included file ${filePath}, ${error}`);
|
|
183
|
-
}
|
|
184
|
-
for (const [index, part] of parts.entries()) {
|
|
185
|
-
if (!isObject(value)) {
|
|
186
|
-
const errPath = parts.slice(0, index).join(".");
|
|
187
|
-
throw new Error(`value at '${errPath}' in included file ${filePath} is not an object`);
|
|
188
|
-
}
|
|
189
|
-
value = value[part];
|
|
190
|
-
}
|
|
191
|
-
return {
|
|
192
|
-
applied: true,
|
|
193
|
-
value,
|
|
194
|
-
newBaseDir: newBaseDir !== baseDir ? newBaseDir : void 0
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
default:
|
|
198
|
-
throw new Error(`unknown include ${includeKey}`);
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function createSubstitutionTransform(env) {
|
|
204
|
-
return async (input) => {
|
|
205
|
-
if (typeof input !== "string") {
|
|
206
|
-
return { applied: false };
|
|
207
|
-
}
|
|
208
|
-
const parts = input.split(/(\$?\$\{[^{}]*\})/);
|
|
209
|
-
for (let i = 1; i < parts.length; i += 2) {
|
|
210
|
-
const part = parts[i];
|
|
211
|
-
if (part.startsWith("$$")) {
|
|
212
|
-
parts[i] = part.slice(1);
|
|
213
|
-
} else {
|
|
214
|
-
parts[i] = await env(part.slice(2, -1).trim());
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
if (parts.some((part) => part === void 0)) {
|
|
218
|
-
return { applied: true, value: void 0 };
|
|
219
|
-
}
|
|
220
|
-
return { applied: true, value: parts.join("") };
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const CONFIG_VISIBILITIES = ["frontend", "backend", "secret"];
|
|
225
|
-
const DEFAULT_CONFIG_VISIBILITY = "backend";
|
|
226
|
-
|
|
227
|
-
function compileConfigSchemas(schemas) {
|
|
228
|
-
const visibilityByDataPath = /* @__PURE__ */ new Map();
|
|
229
|
-
const deprecationByDataPath = /* @__PURE__ */ new Map();
|
|
230
|
-
const ajv = new Ajv({
|
|
231
|
-
allErrors: true,
|
|
232
|
-
allowUnionTypes: true,
|
|
233
|
-
schemas: {
|
|
234
|
-
"https://backstage.io/schema/config-v1": true
|
|
235
|
-
}
|
|
236
|
-
}).addKeyword({
|
|
237
|
-
keyword: "visibility",
|
|
238
|
-
metaSchema: {
|
|
239
|
-
type: "string",
|
|
240
|
-
enum: CONFIG_VISIBILITIES
|
|
241
|
-
},
|
|
242
|
-
compile(visibility) {
|
|
243
|
-
return (_data, context) => {
|
|
244
|
-
if ((context == null ? void 0 : context.dataPath) === void 0) {
|
|
245
|
-
return false;
|
|
246
|
-
}
|
|
247
|
-
if (visibility && visibility !== "backend") {
|
|
248
|
-
const normalizedPath = context.dataPath.replace(/\['?(.*?)'?\]/g, (_, segment) => `/${segment}`);
|
|
249
|
-
visibilityByDataPath.set(normalizedPath, visibility);
|
|
250
|
-
}
|
|
251
|
-
return true;
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
}).removeKeyword("deprecated").addKeyword({
|
|
255
|
-
keyword: "deprecated",
|
|
256
|
-
metaSchema: { type: "string" },
|
|
257
|
-
compile(deprecationDescription) {
|
|
258
|
-
return (_data, context) => {
|
|
259
|
-
if ((context == null ? void 0 : context.dataPath) === void 0) {
|
|
260
|
-
return false;
|
|
261
|
-
}
|
|
262
|
-
const normalizedPath = context.dataPath.replace(/\['?(.*?)'?\]/g, (_, segment) => `/${segment}`);
|
|
263
|
-
deprecationByDataPath.set(normalizedPath, deprecationDescription);
|
|
264
|
-
return true;
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
for (const schema of schemas) {
|
|
269
|
-
try {
|
|
270
|
-
ajv.compile(schema.value);
|
|
271
|
-
} catch (error) {
|
|
272
|
-
throw new Error(`Schema at ${schema.path} is invalid, ${error}`);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
const merged = mergeConfigSchemas(schemas.map((_) => _.value));
|
|
276
|
-
const validate = ajv.compile(merged);
|
|
277
|
-
const visibilityBySchemaPath = /* @__PURE__ */ new Map();
|
|
278
|
-
traverse(merged, (schema, path) => {
|
|
279
|
-
if (schema.visibility && schema.visibility !== "backend") {
|
|
280
|
-
visibilityBySchemaPath.set(path, schema.visibility);
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
return (configs) => {
|
|
284
|
-
var _a;
|
|
285
|
-
const config = ConfigReader.fromConfigs(configs).get();
|
|
286
|
-
visibilityByDataPath.clear();
|
|
287
|
-
const valid = validate(config);
|
|
288
|
-
if (!valid) {
|
|
289
|
-
return {
|
|
290
|
-
errors: (_a = validate.errors) != null ? _a : [],
|
|
291
|
-
visibilityByDataPath: new Map(visibilityByDataPath),
|
|
292
|
-
visibilityBySchemaPath,
|
|
293
|
-
deprecationByDataPath
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
return {
|
|
297
|
-
visibilityByDataPath: new Map(visibilityByDataPath),
|
|
298
|
-
visibilityBySchemaPath,
|
|
299
|
-
deprecationByDataPath
|
|
300
|
-
};
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
function mergeConfigSchemas(schemas) {
|
|
304
|
-
const merged = mergeAllOf({ allOf: schemas }, {
|
|
305
|
-
ignoreAdditionalProperties: true,
|
|
306
|
-
resolvers: {
|
|
307
|
-
visibility(values, path) {
|
|
308
|
-
const hasFrontend = values.some((_) => _ === "frontend");
|
|
309
|
-
const hasSecret = values.some((_) => _ === "secret");
|
|
310
|
-
if (hasFrontend && hasSecret) {
|
|
311
|
-
throw new Error(`Config schema visibility is both 'frontend' and 'secret' for ${path.join("/")}`);
|
|
312
|
-
} else if (hasFrontend) {
|
|
313
|
-
return "frontend";
|
|
314
|
-
} else if (hasSecret) {
|
|
315
|
-
return "secret";
|
|
316
|
-
}
|
|
317
|
-
return "backend";
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
});
|
|
321
|
-
return merged;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const req = typeof __non_webpack_require__ === "undefined" ? require : __non_webpack_require__;
|
|
325
|
-
async function collectConfigSchemas(packageNames, packagePaths) {
|
|
326
|
-
const schemas = new Array();
|
|
327
|
-
const tsSchemaPaths = new Array();
|
|
328
|
-
const visitedPackageVersions = /* @__PURE__ */ new Map();
|
|
329
|
-
const currentDir = await fs.realpath(process.cwd());
|
|
330
|
-
async function processItem(item) {
|
|
331
|
-
var _a, _b, _c, _d;
|
|
332
|
-
let pkgPath = item.packagePath;
|
|
333
|
-
if (pkgPath) {
|
|
334
|
-
const pkgExists = await fs.pathExists(pkgPath);
|
|
335
|
-
if (!pkgExists) {
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
} else if (item.name) {
|
|
339
|
-
const { name, parentPath } = item;
|
|
340
|
-
try {
|
|
341
|
-
pkgPath = req.resolve(`${name}/package.json`, parentPath && {
|
|
342
|
-
paths: [parentPath]
|
|
343
|
-
});
|
|
344
|
-
} catch {
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
if (!pkgPath) {
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
const pkg = await fs.readJson(pkgPath);
|
|
351
|
-
let versions = visitedPackageVersions.get(pkg.name);
|
|
352
|
-
if (versions == null ? void 0 : versions.has(pkg.version)) {
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
if (!versions) {
|
|
356
|
-
versions = /* @__PURE__ */ new Set();
|
|
357
|
-
visitedPackageVersions.set(pkg.name, versions);
|
|
358
|
-
}
|
|
359
|
-
versions.add(pkg.version);
|
|
360
|
-
const depNames = [
|
|
361
|
-
...Object.keys((_a = pkg.dependencies) != null ? _a : {}),
|
|
362
|
-
...Object.keys((_b = pkg.devDependencies) != null ? _b : {}),
|
|
363
|
-
...Object.keys((_c = pkg.optionalDependencies) != null ? _c : {}),
|
|
364
|
-
...Object.keys((_d = pkg.peerDependencies) != null ? _d : {})
|
|
365
|
-
];
|
|
366
|
-
const hasSchema = "configSchema" in pkg;
|
|
367
|
-
const hasBackstageDep = depNames.some((_) => _.startsWith("@backstage/"));
|
|
368
|
-
if (!hasSchema && !hasBackstageDep) {
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
if (hasSchema) {
|
|
372
|
-
if (typeof pkg.configSchema === "string") {
|
|
373
|
-
const isJson = pkg.configSchema.endsWith(".json");
|
|
374
|
-
const isDts = pkg.configSchema.endsWith(".d.ts");
|
|
375
|
-
if (!isJson && !isDts) {
|
|
376
|
-
throw new Error(`Config schema files must be .json or .d.ts, got ${pkg.configSchema}`);
|
|
377
|
-
}
|
|
378
|
-
if (isDts) {
|
|
379
|
-
tsSchemaPaths.push(relative(currentDir, resolve(dirname(pkgPath), pkg.configSchema)));
|
|
380
|
-
} else {
|
|
381
|
-
const path = resolve(dirname(pkgPath), pkg.configSchema);
|
|
382
|
-
const value = await fs.readJson(path);
|
|
383
|
-
schemas.push({
|
|
384
|
-
value,
|
|
385
|
-
path: relative(currentDir, path)
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
} else {
|
|
389
|
-
schemas.push({
|
|
390
|
-
value: pkg.configSchema,
|
|
391
|
-
path: relative(currentDir, pkgPath)
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
await Promise.all(depNames.map((depName) => processItem({ name: depName, parentPath: pkgPath })));
|
|
396
|
-
}
|
|
397
|
-
await Promise.all([
|
|
398
|
-
...packageNames.map((name) => processItem({ name, parentPath: currentDir })),
|
|
399
|
-
...packagePaths.map((path) => processItem({ name: path, packagePath: path }))
|
|
400
|
-
]);
|
|
401
|
-
const tsSchemas = compileTsSchemas(tsSchemaPaths);
|
|
402
|
-
return schemas.concat(tsSchemas);
|
|
403
|
-
}
|
|
404
|
-
function compileTsSchemas(paths) {
|
|
405
|
-
if (paths.length === 0) {
|
|
406
|
-
return [];
|
|
407
|
-
}
|
|
408
|
-
const program = getProgramFromFiles(paths, {
|
|
409
|
-
incremental: false,
|
|
410
|
-
isolatedModules: true,
|
|
411
|
-
lib: ["ES5"],
|
|
412
|
-
noEmit: true,
|
|
413
|
-
noResolve: true,
|
|
414
|
-
skipLibCheck: true,
|
|
415
|
-
skipDefaultLibCheck: true,
|
|
416
|
-
strict: true,
|
|
417
|
-
typeRoots: [],
|
|
418
|
-
types: []
|
|
419
|
-
});
|
|
420
|
-
const tsSchemas = paths.map((path) => {
|
|
421
|
-
let value;
|
|
422
|
-
try {
|
|
423
|
-
value = generateSchema(program, "Config", {
|
|
424
|
-
required: true,
|
|
425
|
-
validationKeywords: ["visibility", "deprecated"]
|
|
426
|
-
}, [path.split(sep).join("/")]);
|
|
427
|
-
} catch (error) {
|
|
428
|
-
assertError(error);
|
|
429
|
-
if (error.message !== "type Config not found") {
|
|
430
|
-
throw error;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
if (!value) {
|
|
434
|
-
throw new Error(`Invalid schema in ${path}, missing Config export`);
|
|
435
|
-
}
|
|
436
|
-
return { path, value };
|
|
437
|
-
});
|
|
438
|
-
return tsSchemas;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function filterByVisibility(data, includeVisibilities, visibilityByDataPath, deprecationByDataPath, transformFunc, withFilteredKeys, withDeprecatedKeys) {
|
|
442
|
-
var _a;
|
|
443
|
-
const filteredKeys = new Array();
|
|
444
|
-
const deprecatedKeys = new Array();
|
|
445
|
-
function transform(jsonVal, visibilityPath, filterPath) {
|
|
446
|
-
var _a2;
|
|
447
|
-
const visibility = (_a2 = visibilityByDataPath.get(visibilityPath)) != null ? _a2 : DEFAULT_CONFIG_VISIBILITY;
|
|
448
|
-
const isVisible = includeVisibilities.includes(visibility);
|
|
449
|
-
const deprecation = deprecationByDataPath.get(visibilityPath);
|
|
450
|
-
if (deprecation) {
|
|
451
|
-
deprecatedKeys.push({ key: filterPath, description: deprecation });
|
|
452
|
-
}
|
|
453
|
-
if (typeof jsonVal !== "object") {
|
|
454
|
-
if (isVisible) {
|
|
455
|
-
if (transformFunc) {
|
|
456
|
-
return transformFunc(jsonVal, { visibility });
|
|
457
|
-
}
|
|
458
|
-
return jsonVal;
|
|
459
|
-
}
|
|
460
|
-
if (withFilteredKeys) {
|
|
461
|
-
filteredKeys.push(filterPath);
|
|
462
|
-
}
|
|
463
|
-
return void 0;
|
|
464
|
-
} else if (jsonVal === null) {
|
|
465
|
-
return void 0;
|
|
466
|
-
} else if (Array.isArray(jsonVal)) {
|
|
467
|
-
const arr = new Array();
|
|
468
|
-
for (const [index, value] of jsonVal.entries()) {
|
|
469
|
-
let path = visibilityPath;
|
|
470
|
-
const hasVisibilityInIndex = visibilityByDataPath.get(`${visibilityPath}/${index}`);
|
|
471
|
-
if (hasVisibilityInIndex || typeof value === "object") {
|
|
472
|
-
path = `${visibilityPath}/${index}`;
|
|
473
|
-
}
|
|
474
|
-
const out = transform(value, path, `${filterPath}[${index}]`);
|
|
475
|
-
if (out !== void 0) {
|
|
476
|
-
arr.push(out);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
if (arr.length > 0 || isVisible) {
|
|
480
|
-
return arr;
|
|
481
|
-
}
|
|
482
|
-
return void 0;
|
|
483
|
-
}
|
|
484
|
-
const outObj = {};
|
|
485
|
-
let hasOutput = false;
|
|
486
|
-
for (const [key, value] of Object.entries(jsonVal)) {
|
|
487
|
-
if (value === void 0) {
|
|
488
|
-
continue;
|
|
489
|
-
}
|
|
490
|
-
const out = transform(value, `${visibilityPath}/${key}`, filterPath ? `${filterPath}.${key}` : key);
|
|
491
|
-
if (out !== void 0) {
|
|
492
|
-
outObj[key] = out;
|
|
493
|
-
hasOutput = true;
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
if (hasOutput || isVisible) {
|
|
497
|
-
return outObj;
|
|
498
|
-
}
|
|
499
|
-
return void 0;
|
|
500
|
-
}
|
|
501
|
-
return {
|
|
502
|
-
filteredKeys: withFilteredKeys ? filteredKeys : void 0,
|
|
503
|
-
deprecatedKeys: withDeprecatedKeys ? deprecatedKeys : void 0,
|
|
504
|
-
data: (_a = transform(data, "", "")) != null ? _a : {}
|
|
505
|
-
};
|
|
506
|
-
}
|
|
507
|
-
function filterErrorsByVisibility(errors, includeVisibilities, visibilityByDataPath, visibilityBySchemaPath) {
|
|
508
|
-
if (!errors) {
|
|
509
|
-
return [];
|
|
510
|
-
}
|
|
511
|
-
if (!includeVisibilities) {
|
|
512
|
-
return errors;
|
|
513
|
-
}
|
|
514
|
-
const visibleSchemaPaths = Array.from(visibilityBySchemaPath).filter(([, v]) => includeVisibilities.includes(v)).map(([k]) => k);
|
|
515
|
-
return errors.filter((error) => {
|
|
516
|
-
var _a;
|
|
517
|
-
if (error.keyword === "type" && ["object", "array"].includes(error.params.type)) {
|
|
518
|
-
return true;
|
|
519
|
-
}
|
|
520
|
-
if (error.keyword === "required") {
|
|
521
|
-
const trimmedPath = error.schemaPath.slice(1, -"/required".length);
|
|
522
|
-
const fullPath = `${trimmedPath}/properties/${error.params.missingProperty}`;
|
|
523
|
-
if (visibleSchemaPaths.some((visiblePath) => visiblePath.startsWith(fullPath))) {
|
|
524
|
-
return true;
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
const vis = (_a = visibilityByDataPath.get(error.dataPath)) != null ? _a : DEFAULT_CONFIG_VISIBILITY;
|
|
528
|
-
return vis && includeVisibilities.includes(vis);
|
|
529
|
-
});
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
function errorsToError(errors) {
|
|
533
|
-
const messages = errors.map(({ dataPath, message, params }) => {
|
|
534
|
-
const paramStr = Object.entries(params).map(([name, value]) => `${name}=${value}`).join(" ");
|
|
535
|
-
return `Config ${message || ""} { ${paramStr} } at ${dataPath}`;
|
|
536
|
-
});
|
|
537
|
-
const error = new Error(`Config validation failed, ${messages.join("; ")}`);
|
|
538
|
-
error.messages = messages;
|
|
539
|
-
return error;
|
|
540
|
-
}
|
|
541
|
-
async function loadConfigSchema(options) {
|
|
542
|
-
var _a;
|
|
543
|
-
let schemas;
|
|
544
|
-
if ("dependencies" in options) {
|
|
545
|
-
schemas = await collectConfigSchemas(options.dependencies, (_a = options.packagePaths) != null ? _a : []);
|
|
546
|
-
} else {
|
|
547
|
-
const { serialized } = options;
|
|
548
|
-
if ((serialized == null ? void 0 : serialized.backstageConfigSchemaVersion) !== 1) {
|
|
549
|
-
throw new Error("Serialized configuration schema is invalid or has an invalid version number");
|
|
550
|
-
}
|
|
551
|
-
schemas = serialized.schemas;
|
|
552
|
-
}
|
|
553
|
-
const validate = compileConfigSchemas(schemas);
|
|
554
|
-
return {
|
|
555
|
-
process(configs, { visibility, valueTransform, withFilteredKeys, withDeprecatedKeys } = {}) {
|
|
556
|
-
const result = validate(configs);
|
|
557
|
-
const visibleErrors = filterErrorsByVisibility(result.errors, visibility, result.visibilityByDataPath, result.visibilityBySchemaPath);
|
|
558
|
-
if (visibleErrors.length > 0) {
|
|
559
|
-
throw errorsToError(visibleErrors);
|
|
560
|
-
}
|
|
561
|
-
let processedConfigs = configs;
|
|
562
|
-
if (visibility) {
|
|
563
|
-
processedConfigs = processedConfigs.map(({ data, context }) => ({
|
|
564
|
-
context,
|
|
565
|
-
...filterByVisibility(data, visibility, result.visibilityByDataPath, result.deprecationByDataPath, valueTransform, withFilteredKeys, withDeprecatedKeys)
|
|
566
|
-
}));
|
|
567
|
-
} else if (valueTransform) {
|
|
568
|
-
processedConfigs = processedConfigs.map(({ data, context }) => ({
|
|
569
|
-
context,
|
|
570
|
-
...filterByVisibility(data, Array.from(CONFIG_VISIBILITIES), result.visibilityByDataPath, result.deprecationByDataPath, valueTransform, withFilteredKeys, withDeprecatedKeys)
|
|
571
|
-
}));
|
|
572
|
-
}
|
|
573
|
-
return processedConfigs;
|
|
574
|
-
},
|
|
575
|
-
serialize() {
|
|
576
|
-
return {
|
|
577
|
-
schemas,
|
|
578
|
-
backstageConfigSchemaVersion: 1
|
|
579
|
-
};
|
|
580
|
-
}
|
|
581
|
-
};
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
function isValidUrl(url) {
|
|
585
|
-
try {
|
|
586
|
-
new URL(url);
|
|
587
|
-
return true;
|
|
588
|
-
} catch {
|
|
589
|
-
return false;
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
async function loadConfig(options) {
|
|
594
|
-
const { configRoot, experimentalEnvFunc: envFunc, watch, remote } = options;
|
|
595
|
-
const configPaths = options.configTargets.slice().filter((e) => e.hasOwnProperty("path")).map((configTarget) => configTarget.path);
|
|
596
|
-
const configUrls = options.configTargets.slice().filter((e) => e.hasOwnProperty("url")).map((configTarget) => configTarget.url);
|
|
597
|
-
if (remote === void 0) {
|
|
598
|
-
if (configUrls.length > 0) {
|
|
599
|
-
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.`);
|
|
600
|
-
}
|
|
601
|
-
} else if (remote.reloadIntervalSeconds <= 0) {
|
|
602
|
-
throw new Error(`Remote config must be contain a non zero reloadIntervalSeconds: <seconds> value`);
|
|
603
|
-
}
|
|
604
|
-
if (configPaths.length === 0 && configUrls.length === 0) {
|
|
605
|
-
configPaths.push(resolve(configRoot, "app-config.yaml"));
|
|
606
|
-
const localConfig = resolve(configRoot, "app-config.local.yaml");
|
|
607
|
-
if (await fs.pathExists(localConfig)) {
|
|
608
|
-
configPaths.push(localConfig);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
const env = envFunc != null ? envFunc : async (name) => process.env[name];
|
|
612
|
-
const loadConfigFiles = async () => {
|
|
613
|
-
const configs = [];
|
|
614
|
-
for (const configPath of configPaths) {
|
|
615
|
-
if (!isAbsolute(configPath)) {
|
|
616
|
-
throw new Error(`Config load path is not absolute: '${configPath}'`);
|
|
617
|
-
}
|
|
618
|
-
const dir = dirname(configPath);
|
|
619
|
-
const readFile = (path) => fs.readFile(resolve(dir, path), "utf8");
|
|
620
|
-
const input = yaml.parse(await readFile(configPath));
|
|
621
|
-
const substitutionTransform = createSubstitutionTransform(env);
|
|
622
|
-
const data = await applyConfigTransforms(dir, input, [
|
|
623
|
-
createIncludeTransform(env, readFile, substitutionTransform),
|
|
624
|
-
substitutionTransform
|
|
625
|
-
]);
|
|
626
|
-
configs.push({ data, context: basename(configPath) });
|
|
627
|
-
}
|
|
628
|
-
return configs;
|
|
629
|
-
};
|
|
630
|
-
const loadRemoteConfigFiles = async () => {
|
|
631
|
-
const configs = [];
|
|
632
|
-
const readConfigFromUrl = async (url) => {
|
|
633
|
-
const response = await fetch(url);
|
|
634
|
-
if (!response.ok) {
|
|
635
|
-
throw new Error(`Could not read config file at ${url}`);
|
|
636
|
-
}
|
|
637
|
-
return await response.text();
|
|
638
|
-
};
|
|
639
|
-
for (let i = 0; i < configUrls.length; i++) {
|
|
640
|
-
const configUrl = configUrls[i];
|
|
641
|
-
if (!isValidUrl(configUrl)) {
|
|
642
|
-
throw new Error(`Config load path is not valid: '${configUrl}'`);
|
|
643
|
-
}
|
|
644
|
-
const remoteConfigContent = await readConfigFromUrl(configUrl);
|
|
645
|
-
if (!remoteConfigContent) {
|
|
646
|
-
throw new Error(`Config is not valid`);
|
|
647
|
-
}
|
|
648
|
-
const configYaml = yaml.parse(remoteConfigContent);
|
|
649
|
-
const substitutionTransform = createSubstitutionTransform(env);
|
|
650
|
-
const data = await applyConfigTransforms(configRoot, configYaml, [
|
|
651
|
-
substitutionTransform
|
|
652
|
-
]);
|
|
653
|
-
configs.push({ data, context: configUrl });
|
|
654
|
-
}
|
|
655
|
-
return configs;
|
|
656
|
-
};
|
|
657
|
-
let fileConfigs;
|
|
658
|
-
try {
|
|
659
|
-
fileConfigs = await loadConfigFiles();
|
|
660
|
-
} catch (error) {
|
|
661
|
-
throw new ForwardedError("Failed to read static configuration file", error);
|
|
662
|
-
}
|
|
663
|
-
let remoteConfigs = [];
|
|
664
|
-
if (remote) {
|
|
665
|
-
try {
|
|
666
|
-
remoteConfigs = await loadRemoteConfigFiles();
|
|
667
|
-
} catch (error) {
|
|
668
|
-
throw new ForwardedError(`Failed to read remote configuration file`, error);
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
const envConfigs = await readEnvConfig(process.env);
|
|
672
|
-
const watchConfigFile = (watchProp) => {
|
|
673
|
-
const watcher = chokidar.watch(configPaths, {
|
|
674
|
-
usePolling: process.env.NODE_ENV === "test"
|
|
675
|
-
});
|
|
676
|
-
let currentSerializedConfig = JSON.stringify(fileConfigs);
|
|
677
|
-
watcher.on("change", async () => {
|
|
678
|
-
try {
|
|
679
|
-
const newConfigs = await loadConfigFiles();
|
|
680
|
-
const newSerializedConfig = JSON.stringify(newConfigs);
|
|
681
|
-
if (currentSerializedConfig === newSerializedConfig) {
|
|
682
|
-
return;
|
|
683
|
-
}
|
|
684
|
-
currentSerializedConfig = newSerializedConfig;
|
|
685
|
-
watchProp.onChange([...remoteConfigs, ...newConfigs, ...envConfigs]);
|
|
686
|
-
} catch (error) {
|
|
687
|
-
console.error(`Failed to reload configuration files, ${error}`);
|
|
688
|
-
}
|
|
689
|
-
});
|
|
690
|
-
if (watchProp.stopSignal) {
|
|
691
|
-
watchProp.stopSignal.then(() => {
|
|
692
|
-
watcher.close();
|
|
693
|
-
});
|
|
694
|
-
}
|
|
695
|
-
};
|
|
696
|
-
const watchRemoteConfig = (watchProp, remoteProp) => {
|
|
697
|
-
const hasConfigChanged = async (oldRemoteConfigs, newRemoteConfigs) => {
|
|
698
|
-
return JSON.stringify(oldRemoteConfigs) !== JSON.stringify(newRemoteConfigs);
|
|
699
|
-
};
|
|
700
|
-
let handle;
|
|
701
|
-
try {
|
|
702
|
-
handle = setInterval(async () => {
|
|
703
|
-
console.info(`Checking for config update`);
|
|
704
|
-
const newRemoteConfigs = await loadRemoteConfigFiles();
|
|
705
|
-
if (await hasConfigChanged(remoteConfigs, newRemoteConfigs)) {
|
|
706
|
-
remoteConfigs = newRemoteConfigs;
|
|
707
|
-
console.info(`Remote config change, reloading config ...`);
|
|
708
|
-
watchProp.onChange([...remoteConfigs, ...fileConfigs, ...envConfigs]);
|
|
709
|
-
console.info(`Remote config reloaded`);
|
|
710
|
-
}
|
|
711
|
-
}, remoteProp.reloadIntervalSeconds * 1e3);
|
|
712
|
-
} catch (error) {
|
|
713
|
-
console.error(`Failed to reload configuration files, ${error}`);
|
|
714
|
-
}
|
|
715
|
-
if (watchProp.stopSignal) {
|
|
716
|
-
watchProp.stopSignal.then(() => {
|
|
717
|
-
if (handle !== void 0) {
|
|
718
|
-
console.info(`Stopping remote config watch`);
|
|
719
|
-
clearInterval(handle);
|
|
720
|
-
handle = void 0;
|
|
721
|
-
}
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
};
|
|
725
|
-
if (watch) {
|
|
726
|
-
watchConfigFile(watch);
|
|
727
|
-
}
|
|
728
|
-
if (watch && remote) {
|
|
729
|
-
watchRemoteConfig(watch, remote);
|
|
730
|
-
}
|
|
731
|
-
return {
|
|
732
|
-
appConfigs: remote ? [...remoteConfigs, ...fileConfigs, ...envConfigs] : [...fileConfigs, ...envConfigs]
|
|
733
|
-
};
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
export { loadConfig, loadConfigSchema, mergeConfigSchemas, readEnvConfig };
|
|
737
|
-
//# sourceMappingURL=index.esm.js.map
|