@backstage/config-loader 1.9.1 → 1.9.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 +21 -0
- package/dist/index.cjs.js +22 -1650
- package/dist/index.cjs.js.map +1 -1
- package/dist/loader.cjs.js +60 -0
- package/dist/loader.cjs.js.map +1 -0
- package/dist/schema/collect.cjs.js +169 -0
- package/dist/schema/collect.cjs.js.map +1 -0
- package/dist/schema/compile.cjs.js +185 -0
- package/dist/schema/compile.cjs.js.map +1 -0
- package/dist/schema/filtering.cjs.js +112 -0
- package/dist/schema/filtering.cjs.js.map +1 -0
- package/dist/schema/load.cjs.js +101 -0
- package/dist/schema/load.cjs.js.map +1 -0
- package/dist/schema/types.cjs.js +8 -0
- package/dist/schema/types.cjs.js.map +1 -0
- package/dist/schema/utils.cjs.js +8 -0
- package/dist/schema/utils.cjs.js.map +1 -0
- package/dist/sources/ConfigSources.cjs.js +178 -0
- package/dist/sources/ConfigSources.cjs.js.map +1 -0
- package/dist/sources/EnvConfigSource.cjs.js +88 -0
- package/dist/sources/EnvConfigSource.cjs.js.map +1 -0
- package/dist/sources/FileConfigSource.cjs.js +153 -0
- package/dist/sources/FileConfigSource.cjs.js.map +1 -0
- package/dist/sources/MergedConfigSource.cjs.js +72 -0
- package/dist/sources/MergedConfigSource.cjs.js.map +1 -0
- package/dist/sources/MutableConfigSource.cjs.js +75 -0
- package/dist/sources/MutableConfigSource.cjs.js.map +1 -0
- package/dist/sources/ObservableConfigProxy.cjs.js +123 -0
- package/dist/sources/ObservableConfigProxy.cjs.js.map +1 -0
- package/dist/sources/RemoteConfigSource.cjs.js +107 -0
- package/dist/sources/RemoteConfigSource.cjs.js.map +1 -0
- package/dist/sources/StaticConfigSource.cjs.js +95 -0
- package/dist/sources/StaticConfigSource.cjs.js.map +1 -0
- package/dist/sources/transform/apply.cjs.js +79 -0
- package/dist/sources/transform/apply.cjs.js.map +1 -0
- package/dist/sources/transform/include.cjs.js +107 -0
- package/dist/sources/transform/include.cjs.js.map +1 -0
- package/dist/sources/transform/substitution.cjs.js +34 -0
- package/dist/sources/transform/substitution.cjs.js.map +1 -0
- package/dist/sources/transform/utils.cjs.js +13 -0
- package/dist/sources/transform/utils.cjs.js.map +1 -0
- package/dist/sources/utils.cjs.js +38 -0
- package/dist/sources/utils.cjs.js.map +1 -0
- package/package.json +14 -7
package/dist/index.cjs.js
CHANGED
|
@@ -1,1653 +1,25 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
4
|
-
var
|
|
5
|
-
var
|
|
6
|
-
var
|
|
7
|
-
var
|
|
8
|
-
var
|
|
9
|
-
var
|
|
10
|
-
var
|
|
11
|
-
var
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
var parseArgs__default = /*#__PURE__*/_interopDefaultCompat(parseArgs);
|
|
26
|
-
var chokidar__default = /*#__PURE__*/_interopDefaultCompat(chokidar);
|
|
27
|
-
var yaml__default = /*#__PURE__*/_interopDefaultCompat(yaml);
|
|
28
|
-
var isEqual__default = /*#__PURE__*/_interopDefaultCompat(isEqual);
|
|
29
|
-
var fetch__default = /*#__PURE__*/_interopDefaultCompat(fetch);
|
|
30
|
-
|
|
31
|
-
const CONFIG_VISIBILITIES = ["frontend", "backend", "secret"];
|
|
32
|
-
const DEFAULT_CONFIG_VISIBILITY = "backend";
|
|
33
|
-
|
|
34
|
-
function normalizeAjvPath(path) {
|
|
35
|
-
return path.replace(/~1/g, "/").replace(/\['?(.*?)'?\]/g, (_, segment) => `/${segment}`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const inheritedVisibility = Symbol("inherited-visibility");
|
|
39
|
-
function compileConfigSchemas(schemas, options) {
|
|
40
|
-
const visibilityByDataPath = /* @__PURE__ */ new Map();
|
|
41
|
-
const deepVisibilityByDataPath = /* @__PURE__ */ new Map();
|
|
42
|
-
const deprecationByDataPath = /* @__PURE__ */ new Map();
|
|
43
|
-
const ajv = new Ajv__default.default({
|
|
44
|
-
allErrors: true,
|
|
45
|
-
allowUnionTypes: true,
|
|
46
|
-
coerceTypes: true,
|
|
47
|
-
schemas: {
|
|
48
|
-
"https://backstage.io/schema/config-v1": true
|
|
49
|
-
}
|
|
50
|
-
}).addKeyword({
|
|
51
|
-
keyword: "visibility",
|
|
52
|
-
metaSchema: {
|
|
53
|
-
type: "string",
|
|
54
|
-
enum: CONFIG_VISIBILITIES
|
|
55
|
-
},
|
|
56
|
-
compile(visibility) {
|
|
57
|
-
return (_data, context) => {
|
|
58
|
-
if (context?.instancePath === void 0) {
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
if (visibility && visibility !== "backend") {
|
|
62
|
-
const normalizedPath = normalizeAjvPath(context.instancePath);
|
|
63
|
-
visibilityByDataPath.set(normalizedPath, visibility);
|
|
64
|
-
}
|
|
65
|
-
return true;
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
}).addKeyword({
|
|
69
|
-
keyword: "deepVisibility",
|
|
70
|
-
metaSchema: {
|
|
71
|
-
type: "string",
|
|
72
|
-
/**
|
|
73
|
-
* Disallow 'backend' deepVisibility to prevent cases of permission escaping.
|
|
74
|
-
*
|
|
75
|
-
* Something like:
|
|
76
|
-
* - deepVisibility secret -> backend -> frontend.
|
|
77
|
-
* - deepVisibility secret -> backend -> visibility frontend.
|
|
78
|
-
*/
|
|
79
|
-
enum: ["frontend", "secret"]
|
|
80
|
-
},
|
|
81
|
-
compile(visibility) {
|
|
82
|
-
return (_data, context) => {
|
|
83
|
-
if (context?.instancePath === void 0) {
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
if (visibility) {
|
|
87
|
-
const normalizedPath = normalizeAjvPath(context.instancePath);
|
|
88
|
-
deepVisibilityByDataPath.set(normalizedPath, visibility);
|
|
89
|
-
}
|
|
90
|
-
return true;
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
}).removeKeyword("deprecated").addKeyword({
|
|
94
|
-
keyword: "deprecated",
|
|
95
|
-
metaSchema: { type: "string" },
|
|
96
|
-
compile(deprecationDescription) {
|
|
97
|
-
return (_data, context) => {
|
|
98
|
-
if (context?.instancePath === void 0) {
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
const normalizedPath = normalizeAjvPath(context.instancePath);
|
|
102
|
-
deprecationByDataPath.set(normalizedPath, deprecationDescription);
|
|
103
|
-
return true;
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
for (const schema of schemas) {
|
|
108
|
-
try {
|
|
109
|
-
ajv.compile(schema.value);
|
|
110
|
-
} catch (error) {
|
|
111
|
-
throw new Error(`Schema at ${schema.path} is invalid, ${error}`);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
const merged = mergeConfigSchemas(schemas.map((_) => _.value));
|
|
115
|
-
traverse__default.default(
|
|
116
|
-
merged,
|
|
117
|
-
(schema, jsonPtr, _1, _2, _3, parentSchema) => {
|
|
118
|
-
schema[inheritedVisibility] ??= schema?.deepVisibility ?? parentSchema?.[inheritedVisibility];
|
|
119
|
-
if (schema[inheritedVisibility]) {
|
|
120
|
-
const values = [
|
|
121
|
-
schema.visibility,
|
|
122
|
-
schema[inheritedVisibility],
|
|
123
|
-
parentSchema?.[inheritedVisibility]
|
|
124
|
-
];
|
|
125
|
-
const hasFrontend = values.some((e) => e === "frontend");
|
|
126
|
-
const hasSecret = values.some((e) => e === "secret");
|
|
127
|
-
if (hasFrontend && hasSecret) {
|
|
128
|
-
throw new Error(
|
|
129
|
-
`Config schema visibility is both 'frontend' and 'secret' for ${jsonPtr}`
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
if (options?.noUndeclaredProperties) {
|
|
134
|
-
if (schema?.type === "object") {
|
|
135
|
-
schema.additionalProperties ||= false;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
);
|
|
140
|
-
const validate = ajv.compile(merged);
|
|
141
|
-
const visibilityBySchemaPath = /* @__PURE__ */ new Map();
|
|
142
|
-
traverse__default.default(merged, (schema, path) => {
|
|
143
|
-
if (schema.visibility && schema.visibility !== "backend") {
|
|
144
|
-
visibilityBySchemaPath.set(normalizeAjvPath(path), schema.visibility);
|
|
145
|
-
}
|
|
146
|
-
if (schema.deepVisibility) {
|
|
147
|
-
visibilityBySchemaPath.set(normalizeAjvPath(path), schema.deepVisibility);
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
return (configs) => {
|
|
151
|
-
const config$1 = config.ConfigReader.fromConfigs(configs).getOptional();
|
|
152
|
-
visibilityByDataPath.clear();
|
|
153
|
-
deepVisibilityByDataPath.clear();
|
|
154
|
-
const valid = validate(config$1);
|
|
155
|
-
if (!valid) {
|
|
156
|
-
return {
|
|
157
|
-
errors: validate.errors ?? [],
|
|
158
|
-
visibilityByDataPath: new Map(visibilityByDataPath),
|
|
159
|
-
deepVisibilityByDataPath: new Map(deepVisibilityByDataPath),
|
|
160
|
-
visibilityBySchemaPath,
|
|
161
|
-
deprecationByDataPath
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
return {
|
|
165
|
-
visibilityByDataPath: new Map(visibilityByDataPath),
|
|
166
|
-
deepVisibilityByDataPath: new Map(deepVisibilityByDataPath),
|
|
167
|
-
visibilityBySchemaPath,
|
|
168
|
-
deprecationByDataPath
|
|
169
|
-
};
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
function mergeConfigSchemas(schemas) {
|
|
173
|
-
const merged = mergeAllOf__default.default(
|
|
174
|
-
{ allOf: schemas },
|
|
175
|
-
{
|
|
176
|
-
// JSONSchema is typically subtractive, as in it always reduces the set of allowed
|
|
177
|
-
// inputs through constraints. This changes the object property merging to be additive
|
|
178
|
-
// rather than subtractive.
|
|
179
|
-
ignoreAdditionalProperties: true,
|
|
180
|
-
resolvers: {
|
|
181
|
-
// This ensures that the visibilities across different schemas are sound, and
|
|
182
|
-
// selects the most specific visibility for each path.
|
|
183
|
-
visibility(values, path) {
|
|
184
|
-
const hasFrontend = values.some((_) => _ === "frontend");
|
|
185
|
-
const hasSecret = values.some((_) => _ === "secret");
|
|
186
|
-
if (hasFrontend && hasSecret) {
|
|
187
|
-
throw new Error(
|
|
188
|
-
`Config schema visibility is both 'frontend' and 'secret' for ${path.join(
|
|
189
|
-
"/"
|
|
190
|
-
)}`
|
|
191
|
-
);
|
|
192
|
-
} else if (hasFrontend) {
|
|
193
|
-
return "frontend";
|
|
194
|
-
} else if (hasSecret) {
|
|
195
|
-
return "secret";
|
|
196
|
-
}
|
|
197
|
-
return "backend";
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
);
|
|
202
|
-
return merged;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const req = typeof __non_webpack_require__ === "undefined" ? require : __non_webpack_require__;
|
|
206
|
-
async function collectConfigSchemas(packageNames, packagePaths) {
|
|
207
|
-
const schemas = new Array();
|
|
208
|
-
const tsSchemaPaths = new Array();
|
|
209
|
-
const visitedPackageVersions = /* @__PURE__ */ new Map();
|
|
210
|
-
const currentDir = await fs__default.default.realpath(process.cwd());
|
|
211
|
-
async function processItem(item) {
|
|
212
|
-
let pkgPath = item.packagePath;
|
|
213
|
-
if (pkgPath) {
|
|
214
|
-
const pkgExists = await fs__default.default.pathExists(pkgPath);
|
|
215
|
-
if (!pkgExists) {
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
} else if (item.name) {
|
|
219
|
-
const { name, parentPath } = item;
|
|
220
|
-
try {
|
|
221
|
-
pkgPath = req.resolve(
|
|
222
|
-
`${name}/package.json`,
|
|
223
|
-
parentPath && {
|
|
224
|
-
paths: [parentPath]
|
|
225
|
-
}
|
|
226
|
-
);
|
|
227
|
-
} catch {
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
if (!pkgPath) {
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
const pkg = await fs__default.default.readJson(pkgPath);
|
|
234
|
-
let versions = visitedPackageVersions.get(pkg.name);
|
|
235
|
-
if (versions?.has(pkg.version)) {
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
if (!versions) {
|
|
239
|
-
versions = /* @__PURE__ */ new Set();
|
|
240
|
-
visitedPackageVersions.set(pkg.name, versions);
|
|
241
|
-
}
|
|
242
|
-
versions.add(pkg.version);
|
|
243
|
-
const depNames = [
|
|
244
|
-
...Object.keys(pkg.dependencies ?? {}),
|
|
245
|
-
...Object.keys(pkg.devDependencies ?? {}),
|
|
246
|
-
...Object.keys(pkg.optionalDependencies ?? {}),
|
|
247
|
-
...Object.keys(pkg.peerDependencies ?? {})
|
|
248
|
-
];
|
|
249
|
-
const hasSchema = "configSchema" in pkg;
|
|
250
|
-
const hasBackstageDep = depNames.some((_) => _.startsWith("@backstage/"));
|
|
251
|
-
if (!hasSchema && !hasBackstageDep) {
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
if (hasSchema) {
|
|
255
|
-
if (typeof pkg.configSchema === "string") {
|
|
256
|
-
const isJson = pkg.configSchema.endsWith(".json");
|
|
257
|
-
const isDts = pkg.configSchema.endsWith(".d.ts");
|
|
258
|
-
if (!isJson && !isDts) {
|
|
259
|
-
throw new Error(
|
|
260
|
-
`Config schema files must be .json or .d.ts, got ${pkg.configSchema}`
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
if (isDts) {
|
|
264
|
-
tsSchemaPaths.push(
|
|
265
|
-
path.relative(
|
|
266
|
-
currentDir,
|
|
267
|
-
path.resolve(path.dirname(pkgPath), pkg.configSchema)
|
|
268
|
-
)
|
|
269
|
-
);
|
|
270
|
-
} else {
|
|
271
|
-
const path$1 = path.resolve(path.dirname(pkgPath), pkg.configSchema);
|
|
272
|
-
const value = await fs__default.default.readJson(path$1);
|
|
273
|
-
schemas.push({
|
|
274
|
-
value,
|
|
275
|
-
path: path.relative(currentDir, path$1)
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
} else {
|
|
279
|
-
schemas.push({
|
|
280
|
-
value: pkg.configSchema,
|
|
281
|
-
path: path.relative(currentDir, pkgPath)
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
await Promise.all(
|
|
286
|
-
depNames.map(
|
|
287
|
-
(depName) => processItem({ name: depName, parentPath: pkgPath })
|
|
288
|
-
)
|
|
289
|
-
);
|
|
290
|
-
}
|
|
291
|
-
await Promise.all([
|
|
292
|
-
...packageNames.map((name) => processItem({ name, parentPath: currentDir })),
|
|
293
|
-
...packagePaths.map((path) => processItem({ name: path, packagePath: path }))
|
|
294
|
-
]);
|
|
295
|
-
const tsSchemas = await compileTsSchemas(tsSchemaPaths);
|
|
296
|
-
return schemas.concat(tsSchemas);
|
|
297
|
-
}
|
|
298
|
-
async function compileTsSchemas(paths) {
|
|
299
|
-
if (paths.length === 0) {
|
|
300
|
-
return [];
|
|
301
|
-
}
|
|
302
|
-
const { getProgramFromFiles, buildGenerator } = await import('typescript-json-schema');
|
|
303
|
-
const program = getProgramFromFiles(paths, {
|
|
304
|
-
incremental: false,
|
|
305
|
-
isolatedModules: true,
|
|
306
|
-
lib: ["ES5"],
|
|
307
|
-
// Skipping most libs speeds processing up a lot, we just need the primitive types anyway
|
|
308
|
-
noEmit: true,
|
|
309
|
-
noResolve: true,
|
|
310
|
-
skipLibCheck: true,
|
|
311
|
-
// Skipping lib checks speeds things up
|
|
312
|
-
skipDefaultLibCheck: true,
|
|
313
|
-
strict: true,
|
|
314
|
-
typeRoots: [],
|
|
315
|
-
// Do not include any additional types
|
|
316
|
-
types: []
|
|
317
|
-
});
|
|
318
|
-
const tsSchemas = paths.map((path$1) => {
|
|
319
|
-
let value;
|
|
320
|
-
try {
|
|
321
|
-
const generator = buildGenerator(
|
|
322
|
-
program,
|
|
323
|
-
// This enables the use of these tags in TSDoc comments
|
|
324
|
-
{
|
|
325
|
-
required: true,
|
|
326
|
-
validationKeywords: ["visibility", "deepVisibility", "deprecated"]
|
|
327
|
-
},
|
|
328
|
-
[path$1.split(path.sep).join("/")]
|
|
329
|
-
// Unix paths are expected for all OSes here
|
|
330
|
-
);
|
|
331
|
-
value = generator?.getSchemaForSymbol("Config");
|
|
332
|
-
const userSymbols = new Set(generator?.getUserSymbols());
|
|
333
|
-
userSymbols.delete("Config");
|
|
334
|
-
if (userSymbols.size !== 0) {
|
|
335
|
-
const names = Array.from(userSymbols).join("', '");
|
|
336
|
-
throw new Error(
|
|
337
|
-
`Invalid configuration schema in ${path$1}, additional symbol definitions are not allowed, found '${names}'`
|
|
338
|
-
);
|
|
339
|
-
}
|
|
340
|
-
const reffedDefs = Object.keys(generator?.ReffedDefinitions ?? {});
|
|
341
|
-
if (reffedDefs.length !== 0) {
|
|
342
|
-
const lines = reffedDefs.join(`${os.EOL} `);
|
|
343
|
-
throw new Error(
|
|
344
|
-
`Invalid configuration schema in ${path$1}, the following definitions are not supported:${os.EOL}${os.EOL} ${lines}`
|
|
345
|
-
);
|
|
346
|
-
}
|
|
347
|
-
} catch (error) {
|
|
348
|
-
errors.assertError(error);
|
|
349
|
-
if (error.message !== "type Config not found") {
|
|
350
|
-
throw error;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
if (!value) {
|
|
354
|
-
throw new Error(`Invalid schema in ${path$1}, missing Config export`);
|
|
355
|
-
}
|
|
356
|
-
return { path: path$1, value };
|
|
357
|
-
});
|
|
358
|
-
return tsSchemas;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function filterByVisibility(data, includeVisibilities, visibilityByDataPath, deepVisibilityByDataPath, deprecationByDataPath, transformFunc, withFilteredKeys, withDeprecatedKeys) {
|
|
362
|
-
const filteredKeys = new Array();
|
|
363
|
-
const deprecatedKeys = new Array();
|
|
364
|
-
function transform(jsonVal, visibilityPath, filterPath, inheritedVisibility) {
|
|
365
|
-
const visibility = visibilityByDataPath.get(visibilityPath) ?? inheritedVisibility;
|
|
366
|
-
const isVisible = includeVisibilities.includes(visibility);
|
|
367
|
-
const newInheritedVisibility = deepVisibilityByDataPath.get(visibilityPath) ?? inheritedVisibility;
|
|
368
|
-
const deprecation = deprecationByDataPath.get(visibilityPath);
|
|
369
|
-
if (deprecation) {
|
|
370
|
-
deprecatedKeys.push({ key: filterPath, description: deprecation });
|
|
371
|
-
}
|
|
372
|
-
if (typeof jsonVal !== "object") {
|
|
373
|
-
if (isVisible) {
|
|
374
|
-
if (transformFunc) {
|
|
375
|
-
return transformFunc(jsonVal, { visibility, path: filterPath });
|
|
376
|
-
}
|
|
377
|
-
return jsonVal;
|
|
378
|
-
}
|
|
379
|
-
if (withFilteredKeys) {
|
|
380
|
-
filteredKeys.push(filterPath);
|
|
381
|
-
}
|
|
382
|
-
return void 0;
|
|
383
|
-
} else if (jsonVal === null) {
|
|
384
|
-
return void 0;
|
|
385
|
-
} else if (Array.isArray(jsonVal)) {
|
|
386
|
-
const arr = new Array();
|
|
387
|
-
for (const [index, value] of jsonVal.entries()) {
|
|
388
|
-
let path = visibilityPath;
|
|
389
|
-
const hasVisibilityInIndex = visibilityByDataPath.get(
|
|
390
|
-
`${visibilityPath}/${index}`
|
|
391
|
-
);
|
|
392
|
-
if (hasVisibilityInIndex || typeof value === "object") {
|
|
393
|
-
path = `${visibilityPath}/${index}`;
|
|
394
|
-
}
|
|
395
|
-
const out = transform(
|
|
396
|
-
value,
|
|
397
|
-
path,
|
|
398
|
-
`${filterPath}[${index}]`,
|
|
399
|
-
newInheritedVisibility
|
|
400
|
-
);
|
|
401
|
-
if (out !== void 0) {
|
|
402
|
-
arr.push(out);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
if (arr.length > 0 || isVisible) {
|
|
406
|
-
return arr;
|
|
407
|
-
}
|
|
408
|
-
return void 0;
|
|
409
|
-
}
|
|
410
|
-
const outObj = {};
|
|
411
|
-
let hasOutput = false;
|
|
412
|
-
for (const [key, value] of Object.entries(jsonVal)) {
|
|
413
|
-
if (value === void 0) {
|
|
414
|
-
continue;
|
|
415
|
-
}
|
|
416
|
-
const out = transform(
|
|
417
|
-
value,
|
|
418
|
-
`${visibilityPath}/${key}`,
|
|
419
|
-
filterPath ? `${filterPath}.${key}` : key,
|
|
420
|
-
newInheritedVisibility
|
|
421
|
-
);
|
|
422
|
-
if (out !== void 0) {
|
|
423
|
-
outObj[key] = out;
|
|
424
|
-
hasOutput = true;
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
if (hasOutput || isVisible) {
|
|
428
|
-
return outObj;
|
|
429
|
-
}
|
|
430
|
-
return void 0;
|
|
431
|
-
}
|
|
432
|
-
return {
|
|
433
|
-
filteredKeys: withFilteredKeys ? filteredKeys : void 0,
|
|
434
|
-
deprecatedKeys: withDeprecatedKeys ? deprecatedKeys : void 0,
|
|
435
|
-
data: transform(data, "", "", DEFAULT_CONFIG_VISIBILITY) ?? {}
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
function filterErrorsByVisibility(errors, includeVisibilities, visibilityByDataPath, visibilityBySchemaPath) {
|
|
439
|
-
if (!errors) {
|
|
440
|
-
return [];
|
|
441
|
-
}
|
|
442
|
-
if (!includeVisibilities) {
|
|
443
|
-
return errors;
|
|
444
|
-
}
|
|
445
|
-
const visibleSchemaPaths = Array.from(visibilityBySchemaPath).filter(([, v]) => includeVisibilities.includes(v)).map(([k]) => k);
|
|
446
|
-
return errors.filter((error) => {
|
|
447
|
-
if (error.keyword === "type" && ["object", "array"].includes(error.params.type)) {
|
|
448
|
-
return true;
|
|
449
|
-
}
|
|
450
|
-
if (error.keyword === "required") {
|
|
451
|
-
const trimmedPath = normalizeAjvPath(error.schemaPath).slice(
|
|
452
|
-
1,
|
|
453
|
-
-"/required".length
|
|
454
|
-
);
|
|
455
|
-
const fullPath = `${trimmedPath}/properties/${error.params.missingProperty}`;
|
|
456
|
-
if (visibleSchemaPaths.some((visiblePath) => visiblePath.startsWith(fullPath))) {
|
|
457
|
-
return true;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
const vis = visibilityByDataPath.get(normalizeAjvPath(error.instancePath)) ?? DEFAULT_CONFIG_VISIBILITY;
|
|
461
|
-
return vis && includeVisibilities.includes(vis);
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
function errorsToError(errors) {
|
|
466
|
-
const messages = errors.map(({ instancePath, message, params }) => {
|
|
467
|
-
const paramStr = Object.entries(params).map(([name, value]) => `${name}=${value}`).join(" ");
|
|
468
|
-
return `Config ${message || ""} { ${paramStr} } at ${normalizeAjvPath(
|
|
469
|
-
instancePath
|
|
470
|
-
)}`;
|
|
471
|
-
});
|
|
472
|
-
const error = new Error(`Config validation failed, ${messages.join("; ")}`);
|
|
473
|
-
error.messages = messages;
|
|
474
|
-
return error;
|
|
475
|
-
}
|
|
476
|
-
async function loadConfigSchema(options) {
|
|
477
|
-
let schemas;
|
|
478
|
-
if ("dependencies" in options) {
|
|
479
|
-
schemas = await collectConfigSchemas(
|
|
480
|
-
options.dependencies,
|
|
481
|
-
options.packagePaths ?? []
|
|
482
|
-
);
|
|
483
|
-
} else {
|
|
484
|
-
const { serialized } = options;
|
|
485
|
-
if (serialized?.backstageConfigSchemaVersion !== 1) {
|
|
486
|
-
throw new Error(
|
|
487
|
-
"Serialized configuration schema is invalid or has an invalid version number"
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
schemas = serialized.schemas;
|
|
491
|
-
}
|
|
492
|
-
const validate = compileConfigSchemas(schemas, {
|
|
493
|
-
noUndeclaredProperties: options.noUndeclaredProperties
|
|
494
|
-
});
|
|
495
|
-
return {
|
|
496
|
-
process(configs, {
|
|
497
|
-
visibility,
|
|
498
|
-
valueTransform,
|
|
499
|
-
withFilteredKeys,
|
|
500
|
-
withDeprecatedKeys,
|
|
501
|
-
ignoreSchemaErrors
|
|
502
|
-
} = {}) {
|
|
503
|
-
const result = validate(configs);
|
|
504
|
-
if (!ignoreSchemaErrors) {
|
|
505
|
-
const visibleErrors = filterErrorsByVisibility(
|
|
506
|
-
result.errors,
|
|
507
|
-
visibility,
|
|
508
|
-
result.visibilityByDataPath,
|
|
509
|
-
result.visibilityBySchemaPath
|
|
510
|
-
);
|
|
511
|
-
if (visibleErrors.length > 0) {
|
|
512
|
-
throw errorsToError(visibleErrors);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
let processedConfigs = configs;
|
|
516
|
-
if (visibility) {
|
|
517
|
-
processedConfigs = processedConfigs.map(({ data, context }) => ({
|
|
518
|
-
context,
|
|
519
|
-
...filterByVisibility(
|
|
520
|
-
data,
|
|
521
|
-
visibility,
|
|
522
|
-
result.visibilityByDataPath,
|
|
523
|
-
result.deepVisibilityByDataPath,
|
|
524
|
-
result.deprecationByDataPath,
|
|
525
|
-
valueTransform,
|
|
526
|
-
withFilteredKeys,
|
|
527
|
-
withDeprecatedKeys
|
|
528
|
-
)
|
|
529
|
-
}));
|
|
530
|
-
} else if (valueTransform) {
|
|
531
|
-
processedConfigs = processedConfigs.map(({ data, context }) => ({
|
|
532
|
-
context,
|
|
533
|
-
...filterByVisibility(
|
|
534
|
-
data,
|
|
535
|
-
Array.from(CONFIG_VISIBILITIES),
|
|
536
|
-
result.visibilityByDataPath,
|
|
537
|
-
result.deepVisibilityByDataPath,
|
|
538
|
-
result.deprecationByDataPath,
|
|
539
|
-
valueTransform,
|
|
540
|
-
withFilteredKeys,
|
|
541
|
-
withDeprecatedKeys
|
|
542
|
-
)
|
|
543
|
-
}));
|
|
544
|
-
}
|
|
545
|
-
return processedConfigs;
|
|
546
|
-
},
|
|
547
|
-
serialize() {
|
|
548
|
-
return {
|
|
549
|
-
schemas,
|
|
550
|
-
backstageConfigSchemaVersion: 1
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
class EnvConfigSource {
|
|
557
|
-
constructor(env) {
|
|
558
|
-
this.env = env;
|
|
559
|
-
}
|
|
560
|
-
/**
|
|
561
|
-
* Creates a new config source that reads from the environment.
|
|
562
|
-
*
|
|
563
|
-
* @param options - Options for the config source.
|
|
564
|
-
* @returns A new config source that reads from the environment.
|
|
565
|
-
*/
|
|
566
|
-
static create(options) {
|
|
567
|
-
return new EnvConfigSource(options?.env ?? process.env);
|
|
568
|
-
}
|
|
569
|
-
async *readConfigData() {
|
|
570
|
-
const configs = readEnvConfig(this.env);
|
|
571
|
-
yield { configs };
|
|
572
|
-
return;
|
|
573
|
-
}
|
|
574
|
-
toString() {
|
|
575
|
-
const keys = Object.keys(this.env).filter(
|
|
576
|
-
(key) => key.startsWith("APP_CONFIG_")
|
|
577
|
-
);
|
|
578
|
-
return `EnvConfigSource{count=${keys.length}}`;
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
const ENV_PREFIX = "APP_CONFIG_";
|
|
582
|
-
const CONFIG_KEY_PART_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z][a-z0-9]*)*$/i;
|
|
583
|
-
function readEnvConfig(env) {
|
|
584
|
-
let data = void 0;
|
|
585
|
-
for (const [name, value] of Object.entries(env)) {
|
|
586
|
-
if (!value) {
|
|
587
|
-
continue;
|
|
588
|
-
}
|
|
589
|
-
if (name.startsWith(ENV_PREFIX)) {
|
|
590
|
-
const key = name.replace(ENV_PREFIX, "");
|
|
591
|
-
const keyParts = key.split("_");
|
|
592
|
-
let obj = data = data ?? {};
|
|
593
|
-
for (const [index, part] of keyParts.entries()) {
|
|
594
|
-
if (!CONFIG_KEY_PART_PATTERN.test(part)) {
|
|
595
|
-
throw new TypeError(`Invalid env config key '${key}'`);
|
|
596
|
-
}
|
|
597
|
-
if (index < keyParts.length - 1) {
|
|
598
|
-
obj = obj[part] = obj[part] ?? {};
|
|
599
|
-
if (typeof obj !== "object" || Array.isArray(obj)) {
|
|
600
|
-
const subKey = keyParts.slice(0, index + 1).join("_");
|
|
601
|
-
throw new TypeError(
|
|
602
|
-
`Could not nest config for key '${key}' under existing value '${subKey}'`
|
|
603
|
-
);
|
|
604
|
-
}
|
|
605
|
-
} else {
|
|
606
|
-
if (part in obj) {
|
|
607
|
-
throw new TypeError(
|
|
608
|
-
`Refusing to override existing config at key '${key}'`
|
|
609
|
-
);
|
|
610
|
-
}
|
|
611
|
-
try {
|
|
612
|
-
const [, parsedValue] = safeJsonParse(value);
|
|
613
|
-
if (parsedValue === null) {
|
|
614
|
-
throw new Error("value may not be null");
|
|
615
|
-
}
|
|
616
|
-
obj[part] = parsedValue;
|
|
617
|
-
} catch (error) {
|
|
618
|
-
throw new TypeError(
|
|
619
|
-
`Failed to parse JSON-serialized config value for key '${key}', ${error}`
|
|
620
|
-
);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
return data ? [{ data, context: "env" }] : [];
|
|
627
|
-
}
|
|
628
|
-
function safeJsonParse(str) {
|
|
629
|
-
try {
|
|
630
|
-
return [null, JSON.parse(str)];
|
|
631
|
-
} catch (err) {
|
|
632
|
-
errors.assertError(err);
|
|
633
|
-
return [err, str];
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
function isObject(obj) {
|
|
638
|
-
if (typeof obj !== "object") {
|
|
639
|
-
return false;
|
|
640
|
-
} else if (Array.isArray(obj)) {
|
|
641
|
-
return false;
|
|
642
|
-
}
|
|
643
|
-
return obj !== null;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
function createSubstitutionTransform(env) {
|
|
647
|
-
return async (input) => {
|
|
648
|
-
if (typeof input !== "string") {
|
|
649
|
-
return { applied: false };
|
|
650
|
-
}
|
|
651
|
-
const parts = input.split(/(\$?\$\{[^{}]*\})/);
|
|
652
|
-
for (let i = 1; i < parts.length; i += 2) {
|
|
653
|
-
const part = parts[i];
|
|
654
|
-
if (part.startsWith("$$")) {
|
|
655
|
-
parts[i] = part.slice(1);
|
|
656
|
-
} else {
|
|
657
|
-
const indexOfFallbackSeparator = part.indexOf(":-");
|
|
658
|
-
if (indexOfFallbackSeparator > -1) {
|
|
659
|
-
const envVarValue = await env(
|
|
660
|
-
part.slice(2, indexOfFallbackSeparator).trim()
|
|
661
|
-
);
|
|
662
|
-
const fallbackValue = part.slice(indexOfFallbackSeparator + ":-".length, -1).trim();
|
|
663
|
-
parts[i] = envVarValue || fallbackValue || void 0;
|
|
664
|
-
} else {
|
|
665
|
-
parts[i] = await env(part.slice(2, -1).trim());
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
if (parts.some((part) => part === void 0)) {
|
|
670
|
-
return { applied: true, value: void 0 };
|
|
671
|
-
}
|
|
672
|
-
return { applied: true, value: parts.join("") };
|
|
673
|
-
};
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
const includeFileParser = {
|
|
677
|
-
".json": async (content) => JSON.parse(content),
|
|
678
|
-
".yaml": async (content) => yaml__default.default.parse(content),
|
|
679
|
-
".yml": async (content) => yaml__default.default.parse(content)
|
|
680
|
-
};
|
|
681
|
-
function createIncludeTransform(env, readFile, substitute) {
|
|
682
|
-
return async (input, context) => {
|
|
683
|
-
const { dir } = context;
|
|
684
|
-
if (!dir) {
|
|
685
|
-
throw new Error("Include transform requires a base directory");
|
|
686
|
-
}
|
|
687
|
-
if (!isObject(input)) {
|
|
688
|
-
return { applied: false };
|
|
689
|
-
}
|
|
690
|
-
const [includeKey] = Object.keys(input).filter((key) => key.startsWith("$"));
|
|
691
|
-
if (includeKey) {
|
|
692
|
-
if (Object.keys(input).length !== 1) {
|
|
693
|
-
throw new Error(
|
|
694
|
-
`include key ${includeKey} should not have adjacent keys`
|
|
695
|
-
);
|
|
696
|
-
}
|
|
697
|
-
} else {
|
|
698
|
-
return { applied: false };
|
|
699
|
-
}
|
|
700
|
-
const rawIncludedValue = input[includeKey];
|
|
701
|
-
if (typeof rawIncludedValue !== "string") {
|
|
702
|
-
throw new Error(`${includeKey} include value is not a string`);
|
|
703
|
-
}
|
|
704
|
-
const substituteResults = await substitute(rawIncludedValue, { dir });
|
|
705
|
-
const includeValue = substituteResults.applied ? substituteResults.value : rawIncludedValue;
|
|
706
|
-
if (includeValue === void 0 || typeof includeValue !== "string") {
|
|
707
|
-
throw new Error(`${includeKey} substitution value was undefined`);
|
|
708
|
-
}
|
|
709
|
-
switch (includeKey) {
|
|
710
|
-
case "$file":
|
|
711
|
-
try {
|
|
712
|
-
const value = await readFile(path.resolve(dir, includeValue));
|
|
713
|
-
return { applied: true, value: value.trimEnd() };
|
|
714
|
-
} catch (error) {
|
|
715
|
-
throw new Error(`failed to read file ${includeValue}, ${error}`);
|
|
716
|
-
}
|
|
717
|
-
case "$env":
|
|
718
|
-
try {
|
|
719
|
-
return { applied: true, value: await env(includeValue) };
|
|
720
|
-
} catch (error) {
|
|
721
|
-
throw new Error(`failed to read env ${includeValue}, ${error}`);
|
|
722
|
-
}
|
|
723
|
-
case "$include": {
|
|
724
|
-
const [filePath, dataPath] = includeValue.split(/#(.*)/);
|
|
725
|
-
const ext = path.extname(filePath);
|
|
726
|
-
const parser = includeFileParser[ext];
|
|
727
|
-
if (!parser) {
|
|
728
|
-
throw new Error(
|
|
729
|
-
`no configuration parser available for included file ${filePath}`
|
|
730
|
-
);
|
|
731
|
-
}
|
|
732
|
-
const path$1 = path.resolve(dir, filePath);
|
|
733
|
-
const content = await readFile(path$1);
|
|
734
|
-
const newDir = path.dirname(path$1);
|
|
735
|
-
const parts = dataPath ? dataPath.split(".") : [];
|
|
736
|
-
let value;
|
|
737
|
-
try {
|
|
738
|
-
value = await parser(content);
|
|
739
|
-
} catch (error) {
|
|
740
|
-
throw new Error(
|
|
741
|
-
`failed to parse included file ${filePath}, ${error}`
|
|
742
|
-
);
|
|
743
|
-
}
|
|
744
|
-
for (const [index, part] of parts.entries()) {
|
|
745
|
-
if (!isObject(value)) {
|
|
746
|
-
const errPath = parts.slice(0, index).join(".");
|
|
747
|
-
throw new Error(
|
|
748
|
-
`value at '${errPath}' in included file ${filePath} is not an object`
|
|
749
|
-
);
|
|
750
|
-
}
|
|
751
|
-
value = value[part];
|
|
752
|
-
}
|
|
753
|
-
if (typeof value === "string") {
|
|
754
|
-
const substituted = await substitute(value, { dir: newDir });
|
|
755
|
-
if (substituted.applied) {
|
|
756
|
-
value = substituted.value;
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
return {
|
|
760
|
-
applied: true,
|
|
761
|
-
value,
|
|
762
|
-
newDir: newDir !== dir ? newDir : void 0
|
|
763
|
-
};
|
|
764
|
-
}
|
|
765
|
-
default:
|
|
766
|
-
throw new Error(`unknown include ${includeKey}`);
|
|
767
|
-
}
|
|
768
|
-
};
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
async function applyConfigTransforms(input, context, transforms) {
|
|
772
|
-
async function transform(inputObj, path, baseDir) {
|
|
773
|
-
let obj = inputObj;
|
|
774
|
-
let dir = baseDir;
|
|
775
|
-
for (const tf of transforms) {
|
|
776
|
-
try {
|
|
777
|
-
const result = await tf(inputObj, { dir });
|
|
778
|
-
if (result.applied) {
|
|
779
|
-
if (result.value === void 0) {
|
|
780
|
-
return void 0;
|
|
781
|
-
}
|
|
782
|
-
obj = result.value;
|
|
783
|
-
dir = result?.newDir ?? dir;
|
|
784
|
-
break;
|
|
785
|
-
}
|
|
786
|
-
} catch (error) {
|
|
787
|
-
errors.assertError(error);
|
|
788
|
-
throw new Error(`error at ${path}, ${error.message}`);
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
if (typeof obj !== "object") {
|
|
792
|
-
return obj;
|
|
793
|
-
} else if (obj === null) {
|
|
794
|
-
return null;
|
|
795
|
-
} else if (Array.isArray(obj)) {
|
|
796
|
-
const arr = new Array();
|
|
797
|
-
for (const [index, value] of obj.entries()) {
|
|
798
|
-
const out2 = await transform(value, `${path}[${index}]`, dir);
|
|
799
|
-
if (out2 !== void 0) {
|
|
800
|
-
arr.push(out2);
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
return arr;
|
|
804
|
-
}
|
|
805
|
-
const out = {};
|
|
806
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
807
|
-
if (value !== void 0) {
|
|
808
|
-
const result = await transform(value, `${path}.${key}`, dir);
|
|
809
|
-
if (result !== void 0) {
|
|
810
|
-
out[key] = result;
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
return out;
|
|
815
|
-
}
|
|
816
|
-
const finalData = await transform(input, "", context?.dir);
|
|
817
|
-
if (!isObject(finalData)) {
|
|
818
|
-
throw new TypeError("expected object at config root");
|
|
819
|
-
}
|
|
820
|
-
return finalData;
|
|
821
|
-
}
|
|
822
|
-
function createConfigTransformer(options) {
|
|
823
|
-
const {
|
|
824
|
-
substitutionFunc = async (name) => process.env[name]?.trim(),
|
|
825
|
-
readFile
|
|
826
|
-
} = options;
|
|
827
|
-
const substitutionTransform = createSubstitutionTransform(substitutionFunc);
|
|
828
|
-
const transforms = [substitutionTransform];
|
|
829
|
-
if (readFile) {
|
|
830
|
-
const includeTransform = createIncludeTransform(
|
|
831
|
-
substitutionFunc,
|
|
832
|
-
readFile,
|
|
833
|
-
substitutionTransform
|
|
834
|
-
);
|
|
835
|
-
transforms.push(includeTransform);
|
|
836
|
-
}
|
|
837
|
-
return async (input, ctx) => applyConfigTransforms(input, ctx ?? {}, transforms);
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
function simpleDefer() {
|
|
841
|
-
let resolve;
|
|
842
|
-
const promise = new Promise((_resolve) => {
|
|
843
|
-
resolve = _resolve;
|
|
844
|
-
});
|
|
845
|
-
return { promise, resolve };
|
|
846
|
-
}
|
|
847
|
-
async function waitOrAbort(promise, signal) {
|
|
848
|
-
const signals = [signal].flat().filter((x) => !!x);
|
|
849
|
-
return new Promise((resolve, reject) => {
|
|
850
|
-
if (signals.some((s) => s.aborted)) {
|
|
851
|
-
resolve([false]);
|
|
852
|
-
}
|
|
853
|
-
const onAbort = () => {
|
|
854
|
-
resolve([false]);
|
|
855
|
-
};
|
|
856
|
-
promise.then(
|
|
857
|
-
(value) => {
|
|
858
|
-
resolve([true, value]);
|
|
859
|
-
signals.forEach((s) => s.removeEventListener("abort", onAbort));
|
|
860
|
-
},
|
|
861
|
-
(error) => {
|
|
862
|
-
reject(error);
|
|
863
|
-
signals.forEach((s) => s.removeEventListener("abort", onAbort));
|
|
864
|
-
}
|
|
865
|
-
);
|
|
866
|
-
signals.forEach((s) => s.addEventListener("abort", onAbort));
|
|
867
|
-
});
|
|
868
|
-
}
|
|
869
|
-
const parseYamlContent = async ({ contents }) => {
|
|
870
|
-
const parsed = yaml__default.default.parse(contents);
|
|
871
|
-
return { result: parsed === null ? void 0 : parsed };
|
|
872
|
-
};
|
|
873
|
-
|
|
874
|
-
async function readFile(path) {
|
|
875
|
-
try {
|
|
876
|
-
const content = await fs__default.default.readFile(path, "utf8");
|
|
877
|
-
if (content === "") {
|
|
878
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
879
|
-
return await fs__default.default.readFile(path, "utf8");
|
|
880
|
-
}
|
|
881
|
-
return content;
|
|
882
|
-
} catch (error) {
|
|
883
|
-
if (error.code === "ENOENT") {
|
|
884
|
-
return void 0;
|
|
885
|
-
}
|
|
886
|
-
throw error;
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
class FileConfigSource {
|
|
890
|
-
/**
|
|
891
|
-
* Creates a new config source that loads configuration from the given path.
|
|
892
|
-
*
|
|
893
|
-
* @remarks
|
|
894
|
-
*
|
|
895
|
-
* The source will watch the file for changes, as well as any referenced files.
|
|
896
|
-
*
|
|
897
|
-
* @param options - Options for the config source.
|
|
898
|
-
* @returns A new config source that loads from the given path.
|
|
899
|
-
*/
|
|
900
|
-
static create(options) {
|
|
901
|
-
if (!path.isAbsolute(options.path)) {
|
|
902
|
-
throw new Error(`Config load path is not absolute: "${options.path}"`);
|
|
903
|
-
}
|
|
904
|
-
return new FileConfigSource(options);
|
|
905
|
-
}
|
|
906
|
-
#path;
|
|
907
|
-
#substitutionFunc;
|
|
908
|
-
#watch;
|
|
909
|
-
#parser;
|
|
910
|
-
constructor(options) {
|
|
911
|
-
this.#path = options.path;
|
|
912
|
-
this.#substitutionFunc = options.substitutionFunc;
|
|
913
|
-
this.#watch = options.watch ?? true;
|
|
914
|
-
this.#parser = options.parser ?? parseYamlContent;
|
|
915
|
-
}
|
|
916
|
-
// Work is duplicated across each read, in practice that should not
|
|
917
|
-
// have any impact since there won't be multiple consumers. If that
|
|
918
|
-
// changes it might be worth refactoring this to avoid duplicate work.
|
|
919
|
-
async *readConfigData(options) {
|
|
920
|
-
const signal = options?.signal;
|
|
921
|
-
const configFileName = path.basename(this.#path);
|
|
922
|
-
let watchedPaths = null;
|
|
923
|
-
let watcher = null;
|
|
924
|
-
if (this.#watch) {
|
|
925
|
-
watchedPaths = new Array();
|
|
926
|
-
watcher = chokidar__default.default.watch(this.#path, {
|
|
927
|
-
usePolling: process.env.NODE_ENV === "test"
|
|
928
|
-
});
|
|
929
|
-
}
|
|
930
|
-
const dir = path.dirname(this.#path);
|
|
931
|
-
const transformer = createConfigTransformer({
|
|
932
|
-
substitutionFunc: this.#substitutionFunc,
|
|
933
|
-
readFile: async (path$1) => {
|
|
934
|
-
const fullPath = path.resolve(dir, path$1);
|
|
935
|
-
if (watcher && watchedPaths) {
|
|
936
|
-
watcher.add(fullPath);
|
|
937
|
-
watchedPaths.push(fullPath);
|
|
938
|
-
}
|
|
939
|
-
const data = await readFile(fullPath);
|
|
940
|
-
if (data === void 0) {
|
|
941
|
-
throw new errors.NotFoundError(
|
|
942
|
-
`failed to include "${fullPath}", file does not exist`
|
|
943
|
-
);
|
|
944
|
-
}
|
|
945
|
-
return data;
|
|
946
|
-
}
|
|
947
|
-
});
|
|
948
|
-
const readConfigFile = async () => {
|
|
949
|
-
if (watcher && watchedPaths) {
|
|
950
|
-
watcher.unwatch(watchedPaths);
|
|
951
|
-
watchedPaths.length = 0;
|
|
952
|
-
watcher.add(this.#path);
|
|
953
|
-
watchedPaths.push(this.#path);
|
|
954
|
-
}
|
|
955
|
-
const contents = await readFile(this.#path);
|
|
956
|
-
if (contents === void 0) {
|
|
957
|
-
throw new errors.NotFoundError(`Config file "${this.#path}" does not exist`);
|
|
958
|
-
}
|
|
959
|
-
const { result: parsed } = await this.#parser({ contents });
|
|
960
|
-
if (parsed === void 0) {
|
|
961
|
-
return [];
|
|
962
|
-
}
|
|
963
|
-
try {
|
|
964
|
-
const data = await transformer(parsed, { dir });
|
|
965
|
-
return [{ data, context: configFileName, path: this.#path }];
|
|
966
|
-
} catch (error) {
|
|
967
|
-
throw new Error(
|
|
968
|
-
`Failed to read config file at "${this.#path}", ${error.message}`
|
|
969
|
-
);
|
|
970
|
-
}
|
|
971
|
-
};
|
|
972
|
-
const onAbort = () => {
|
|
973
|
-
signal?.removeEventListener("abort", onAbort);
|
|
974
|
-
if (watcher) watcher.close();
|
|
975
|
-
};
|
|
976
|
-
signal?.addEventListener("abort", onAbort);
|
|
977
|
-
yield { configs: await readConfigFile() };
|
|
978
|
-
if (watcher) {
|
|
979
|
-
for (; ; ) {
|
|
980
|
-
const event = await this.#waitForEvent(watcher, signal);
|
|
981
|
-
if (event === "abort") {
|
|
982
|
-
return;
|
|
983
|
-
}
|
|
984
|
-
yield { configs: await readConfigFile() };
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
toString() {
|
|
989
|
-
return `FileConfigSource{path="${this.#path}"}`;
|
|
990
|
-
}
|
|
991
|
-
#waitForEvent(watcher, signal) {
|
|
992
|
-
return new Promise((resolve) => {
|
|
993
|
-
function onChange() {
|
|
994
|
-
resolve("change");
|
|
995
|
-
onDone();
|
|
996
|
-
}
|
|
997
|
-
function onAbort() {
|
|
998
|
-
resolve("abort");
|
|
999
|
-
onDone();
|
|
1000
|
-
}
|
|
1001
|
-
function onDone() {
|
|
1002
|
-
watcher.removeListener("change", onChange);
|
|
1003
|
-
signal?.removeEventListener("abort", onAbort);
|
|
1004
|
-
}
|
|
1005
|
-
watcher.addListener("change", onChange);
|
|
1006
|
-
signal?.addEventListener("abort", onAbort);
|
|
1007
|
-
});
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
const sourcesSymbol = Symbol.for(
|
|
1012
|
-
"@backstage/config-loader#MergedConfigSource.sources"
|
|
1013
|
-
);
|
|
1014
|
-
class MergedConfigSource {
|
|
1015
|
-
constructor(sources) {
|
|
1016
|
-
this.sources = sources;
|
|
1017
|
-
this[sourcesSymbol] = this.sources;
|
|
1018
|
-
}
|
|
1019
|
-
// An optimization to flatten nested merged sources to avid unnecessary microtasks
|
|
1020
|
-
static #flattenSources(sources) {
|
|
1021
|
-
return sources.flatMap((source) => {
|
|
1022
|
-
if (sourcesSymbol in source && Array.isArray(source[sourcesSymbol])) {
|
|
1023
|
-
return this.#flattenSources(
|
|
1024
|
-
source[sourcesSymbol]
|
|
1025
|
-
);
|
|
1026
|
-
}
|
|
1027
|
-
return source;
|
|
1028
|
-
});
|
|
1029
|
-
}
|
|
1030
|
-
static from(sources) {
|
|
1031
|
-
return new MergedConfigSource(this.#flattenSources(sources));
|
|
1032
|
-
}
|
|
1033
|
-
[sourcesSymbol];
|
|
1034
|
-
async *readConfigData(options) {
|
|
1035
|
-
const its = this.sources.map((source) => source.readConfigData(options));
|
|
1036
|
-
const initialResults = await Promise.all(its.map((it) => it.next()));
|
|
1037
|
-
const configs = initialResults.map((result, i) => {
|
|
1038
|
-
if (result.done) {
|
|
1039
|
-
throw new Error(
|
|
1040
|
-
`Config source ${String(this.sources[i])} returned no data`
|
|
1041
|
-
);
|
|
1042
|
-
}
|
|
1043
|
-
return result.value.configs;
|
|
1044
|
-
});
|
|
1045
|
-
yield { configs: configs.flat(1) };
|
|
1046
|
-
const results = its.map((it, i) => nextWithIndex(it, i));
|
|
1047
|
-
while (results.some(Boolean)) {
|
|
1048
|
-
try {
|
|
1049
|
-
const [i, result] = await Promise.race(results.filter(Boolean));
|
|
1050
|
-
if (result.done) {
|
|
1051
|
-
results[i] = void 0;
|
|
1052
|
-
} else {
|
|
1053
|
-
results[i] = nextWithIndex(its[i], i);
|
|
1054
|
-
configs[i] = result.value.configs;
|
|
1055
|
-
yield { configs: configs.flat(1) };
|
|
1056
|
-
}
|
|
1057
|
-
} catch (error) {
|
|
1058
|
-
const source = this.sources[error.index];
|
|
1059
|
-
if (source) {
|
|
1060
|
-
throw new Error(`Config source ${String(source)} failed: ${error}`);
|
|
1061
|
-
}
|
|
1062
|
-
throw error;
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
toString() {
|
|
1067
|
-
return `MergedConfigSource{${this.sources.map(String).join(", ")}}`;
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
function nextWithIndex(iterator, index) {
|
|
1071
|
-
return iterator.next().then(
|
|
1072
|
-
(r) => [index, r],
|
|
1073
|
-
(e) => {
|
|
1074
|
-
throw Object.assign(e, { index });
|
|
1075
|
-
}
|
|
1076
|
-
);
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
const DEFAULT_RELOAD_INTERVAL = { seconds: 60 };
|
|
1080
|
-
class RemoteConfigSource {
|
|
1081
|
-
/**
|
|
1082
|
-
* Creates a new {@link RemoteConfigSource}.
|
|
1083
|
-
*
|
|
1084
|
-
* @param options - Options for the source.
|
|
1085
|
-
* @returns A new remote config source.
|
|
1086
|
-
*/
|
|
1087
|
-
static create(options) {
|
|
1088
|
-
try {
|
|
1089
|
-
new URL(options.url);
|
|
1090
|
-
} catch (error) {
|
|
1091
|
-
throw new Error(
|
|
1092
|
-
`Invalid URL provided to remote config source, '${options.url}', ${error}`
|
|
1093
|
-
);
|
|
1094
|
-
}
|
|
1095
|
-
return new RemoteConfigSource(options);
|
|
1096
|
-
}
|
|
1097
|
-
#url;
|
|
1098
|
-
#reloadIntervalMs;
|
|
1099
|
-
#transformer;
|
|
1100
|
-
#parser;
|
|
1101
|
-
constructor(options) {
|
|
1102
|
-
this.#url = options.url;
|
|
1103
|
-
this.#reloadIntervalMs = types.durationToMilliseconds(
|
|
1104
|
-
options.reloadInterval ?? DEFAULT_RELOAD_INTERVAL
|
|
1105
|
-
);
|
|
1106
|
-
this.#transformer = createConfigTransformer({
|
|
1107
|
-
substitutionFunc: options.substitutionFunc
|
|
1108
|
-
});
|
|
1109
|
-
this.#parser = options.parser ?? parseYamlContent;
|
|
1110
|
-
}
|
|
1111
|
-
async *readConfigData(options) {
|
|
1112
|
-
let data = await this.#load();
|
|
1113
|
-
yield { configs: [{ data, context: this.#url }] };
|
|
1114
|
-
for (; ; ) {
|
|
1115
|
-
await this.#wait(options?.signal);
|
|
1116
|
-
if (options?.signal?.aborted) {
|
|
1117
|
-
return;
|
|
1118
|
-
}
|
|
1119
|
-
try {
|
|
1120
|
-
const newData = await this.#load(options?.signal);
|
|
1121
|
-
if (newData && !isEqual__default.default(data, newData)) {
|
|
1122
|
-
data = newData;
|
|
1123
|
-
yield { configs: [{ data, context: this.#url }] };
|
|
1124
|
-
}
|
|
1125
|
-
} catch (error) {
|
|
1126
|
-
if (error.name !== "AbortError") {
|
|
1127
|
-
console.error(`Failed to read config from ${this.#url}, ${error}`);
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
toString() {
|
|
1133
|
-
return `RemoteConfigSource{path="${this.#url}"}`;
|
|
1134
|
-
}
|
|
1135
|
-
async #load(signal) {
|
|
1136
|
-
const res = await fetch__default.default(this.#url, {
|
|
1137
|
-
signal
|
|
1138
|
-
});
|
|
1139
|
-
if (!res.ok) {
|
|
1140
|
-
throw await errors.ResponseError.fromResponse(res);
|
|
1141
|
-
}
|
|
1142
|
-
const contents = await res.text();
|
|
1143
|
-
const { result: rawData } = await this.#parser({ contents });
|
|
1144
|
-
if (rawData === void 0) {
|
|
1145
|
-
throw new Error("configuration data is null");
|
|
1146
|
-
}
|
|
1147
|
-
const data = await this.#transformer(rawData);
|
|
1148
|
-
if (typeof data !== "object") {
|
|
1149
|
-
throw new Error("configuration data is not an object");
|
|
1150
|
-
} else if (Array.isArray(data)) {
|
|
1151
|
-
throw new Error(
|
|
1152
|
-
"configuration data is an array, expected an object instead"
|
|
1153
|
-
);
|
|
1154
|
-
}
|
|
1155
|
-
return data;
|
|
1156
|
-
}
|
|
1157
|
-
async #wait(signal) {
|
|
1158
|
-
return new Promise((resolve) => {
|
|
1159
|
-
const timeoutId = setTimeout(onDone, this.#reloadIntervalMs);
|
|
1160
|
-
signal?.addEventListener("abort", onDone);
|
|
1161
|
-
function onDone() {
|
|
1162
|
-
clearTimeout(timeoutId);
|
|
1163
|
-
signal?.removeEventListener("abort", onDone);
|
|
1164
|
-
resolve();
|
|
1165
|
-
}
|
|
1166
|
-
});
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
class ObservableConfigProxy {
|
|
1171
|
-
constructor(parent, parentKey, abortController) {
|
|
1172
|
-
this.parent = parent;
|
|
1173
|
-
this.parentKey = parentKey;
|
|
1174
|
-
this.abortController = abortController;
|
|
1175
|
-
if (parent && !parentKey) {
|
|
1176
|
-
throw new Error("parentKey is required if parent is set");
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
config = new config.ConfigReader({});
|
|
1180
|
-
subscribers = [];
|
|
1181
|
-
static create(abortController) {
|
|
1182
|
-
return new ObservableConfigProxy(void 0, void 0, abortController);
|
|
1183
|
-
}
|
|
1184
|
-
setConfig(config) {
|
|
1185
|
-
if (this.parent) {
|
|
1186
|
-
throw new Error("immutable");
|
|
1187
|
-
}
|
|
1188
|
-
const changed = !isEqual__default.default(this.config.get(), config.get());
|
|
1189
|
-
this.config = config;
|
|
1190
|
-
if (changed) {
|
|
1191
|
-
for (const subscriber of this.subscribers) {
|
|
1192
|
-
try {
|
|
1193
|
-
subscriber();
|
|
1194
|
-
} catch (error) {
|
|
1195
|
-
console.error(`Config subscriber threw error, ${error}`);
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
close() {
|
|
1201
|
-
if (!this.abortController) {
|
|
1202
|
-
throw new Error("Only the root config can be closed");
|
|
1203
|
-
}
|
|
1204
|
-
this.abortController.abort();
|
|
1205
|
-
}
|
|
1206
|
-
subscribe(onChange) {
|
|
1207
|
-
if (this.parent) {
|
|
1208
|
-
return this.parent.subscribe(onChange);
|
|
1209
|
-
}
|
|
1210
|
-
this.subscribers.push(onChange);
|
|
1211
|
-
return {
|
|
1212
|
-
unsubscribe: () => {
|
|
1213
|
-
const index = this.subscribers.indexOf(onChange);
|
|
1214
|
-
if (index >= 0) {
|
|
1215
|
-
this.subscribers.splice(index, 1);
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
};
|
|
1219
|
-
}
|
|
1220
|
-
select(required) {
|
|
1221
|
-
if (this.parent && this.parentKey) {
|
|
1222
|
-
if (required) {
|
|
1223
|
-
return this.parent.select(true).getConfig(this.parentKey);
|
|
1224
|
-
}
|
|
1225
|
-
return this.parent.select(false)?.getOptionalConfig(this.parentKey);
|
|
1226
|
-
}
|
|
1227
|
-
return this.config;
|
|
1228
|
-
}
|
|
1229
|
-
has(key) {
|
|
1230
|
-
return this.select(false)?.has(key) ?? false;
|
|
1231
|
-
}
|
|
1232
|
-
keys() {
|
|
1233
|
-
return this.select(false)?.keys() ?? [];
|
|
1234
|
-
}
|
|
1235
|
-
get(key) {
|
|
1236
|
-
return this.select(true).get(key);
|
|
1237
|
-
}
|
|
1238
|
-
getOptional(key) {
|
|
1239
|
-
return this.select(false)?.getOptional(key);
|
|
1240
|
-
}
|
|
1241
|
-
getConfig(key) {
|
|
1242
|
-
return new ObservableConfigProxy(this, key);
|
|
1243
|
-
}
|
|
1244
|
-
getOptionalConfig(key) {
|
|
1245
|
-
if (this.select(false)?.has(key)) {
|
|
1246
|
-
return new ObservableConfigProxy(this, key);
|
|
1247
|
-
}
|
|
1248
|
-
return void 0;
|
|
1249
|
-
}
|
|
1250
|
-
getConfigArray(key) {
|
|
1251
|
-
return this.select(true).getConfigArray(key);
|
|
1252
|
-
}
|
|
1253
|
-
getOptionalConfigArray(key) {
|
|
1254
|
-
return this.select(false)?.getOptionalConfigArray(key);
|
|
1255
|
-
}
|
|
1256
|
-
getNumber(key) {
|
|
1257
|
-
return this.select(true).getNumber(key);
|
|
1258
|
-
}
|
|
1259
|
-
getOptionalNumber(key) {
|
|
1260
|
-
return this.select(false)?.getOptionalNumber(key);
|
|
1261
|
-
}
|
|
1262
|
-
getBoolean(key) {
|
|
1263
|
-
return this.select(true).getBoolean(key);
|
|
1264
|
-
}
|
|
1265
|
-
getOptionalBoolean(key) {
|
|
1266
|
-
return this.select(false)?.getOptionalBoolean(key);
|
|
1267
|
-
}
|
|
1268
|
-
getString(key) {
|
|
1269
|
-
return this.select(true).getString(key);
|
|
1270
|
-
}
|
|
1271
|
-
getOptionalString(key) {
|
|
1272
|
-
return this.select(false)?.getOptionalString(key);
|
|
1273
|
-
}
|
|
1274
|
-
getStringArray(key) {
|
|
1275
|
-
return this.select(true).getStringArray(key);
|
|
1276
|
-
}
|
|
1277
|
-
getOptionalStringArray(key) {
|
|
1278
|
-
return this.select(false)?.getOptionalStringArray(key);
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
class ConfigSources {
|
|
1283
|
-
/**
|
|
1284
|
-
* Parses command line arguments and returns the config targets.
|
|
1285
|
-
*
|
|
1286
|
-
* @param argv - The command line arguments to parse. Defaults to `process.argv`
|
|
1287
|
-
* @returns A list of config targets
|
|
1288
|
-
*/
|
|
1289
|
-
static parseArgs(argv = process.argv) {
|
|
1290
|
-
const args = [parseArgs__default.default(argv).config].flat().filter(Boolean);
|
|
1291
|
-
return args.map((target) => {
|
|
1292
|
-
try {
|
|
1293
|
-
const url = new URL(target);
|
|
1294
|
-
if (!url.host) {
|
|
1295
|
-
return { type: "path", target };
|
|
1296
|
-
}
|
|
1297
|
-
return { type: "url", target };
|
|
1298
|
-
} catch {
|
|
1299
|
-
return { type: "path", target };
|
|
1300
|
-
}
|
|
1301
|
-
});
|
|
1302
|
-
}
|
|
1303
|
-
/**
|
|
1304
|
-
* Creates the default config sources for the provided targets.
|
|
1305
|
-
*
|
|
1306
|
-
* @remarks
|
|
1307
|
-
*
|
|
1308
|
-
* This will create {@link FileConfigSource}s and {@link RemoteConfigSource}s
|
|
1309
|
-
* for the provided targets, and merge them together to a single source.
|
|
1310
|
-
* If no targets are provided it will fall back to `app-config.yaml` and
|
|
1311
|
-
* `app-config.local.yaml`.
|
|
1312
|
-
*
|
|
1313
|
-
* URL targets are only supported if the `remote` option is provided.
|
|
1314
|
-
*
|
|
1315
|
-
* @param options - Options
|
|
1316
|
-
* @returns A config source for the provided targets
|
|
1317
|
-
*/
|
|
1318
|
-
static defaultForTargets(options) {
|
|
1319
|
-
const rootDir = options.rootDir ?? cliCommon.findPaths(process.cwd()).targetRoot;
|
|
1320
|
-
const argSources = options.targets.map((arg) => {
|
|
1321
|
-
if (arg.type === "url") {
|
|
1322
|
-
if (!options.remote) {
|
|
1323
|
-
throw new Error(
|
|
1324
|
-
`Config argument "${arg.target}" looks like a URL but remote configuration is not enabled. Enable it by passing the \`remote\` option`
|
|
1325
|
-
);
|
|
1326
|
-
}
|
|
1327
|
-
return RemoteConfigSource.create({
|
|
1328
|
-
url: arg.target,
|
|
1329
|
-
substitutionFunc: options.substitutionFunc,
|
|
1330
|
-
reloadInterval: options.remote.reloadInterval
|
|
1331
|
-
});
|
|
1332
|
-
}
|
|
1333
|
-
return FileConfigSource.create({
|
|
1334
|
-
watch: options.watch,
|
|
1335
|
-
path: path.resolve(arg.target),
|
|
1336
|
-
substitutionFunc: options.substitutionFunc
|
|
1337
|
-
});
|
|
1338
|
-
});
|
|
1339
|
-
if (argSources.length === 0) {
|
|
1340
|
-
const defaultPath = path.resolve(rootDir, "app-config.yaml");
|
|
1341
|
-
const localPath = path.resolve(rootDir, "app-config.local.yaml");
|
|
1342
|
-
const alwaysIncludeDefaultConfigSource = !options.allowMissingDefaultConfig;
|
|
1343
|
-
if (alwaysIncludeDefaultConfigSource || fs__default.default.pathExistsSync(defaultPath)) {
|
|
1344
|
-
argSources.push(
|
|
1345
|
-
FileConfigSource.create({
|
|
1346
|
-
watch: options.watch,
|
|
1347
|
-
path: defaultPath,
|
|
1348
|
-
substitutionFunc: options.substitutionFunc
|
|
1349
|
-
})
|
|
1350
|
-
);
|
|
1351
|
-
}
|
|
1352
|
-
if (fs__default.default.pathExistsSync(localPath)) {
|
|
1353
|
-
argSources.push(
|
|
1354
|
-
FileConfigSource.create({
|
|
1355
|
-
watch: options.watch,
|
|
1356
|
-
path: localPath,
|
|
1357
|
-
substitutionFunc: options.substitutionFunc
|
|
1358
|
-
})
|
|
1359
|
-
);
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
return this.merge(argSources);
|
|
1363
|
-
}
|
|
1364
|
-
/**
|
|
1365
|
-
* Creates the default config source for Backstage.
|
|
1366
|
-
*
|
|
1367
|
-
* @remarks
|
|
1368
|
-
*
|
|
1369
|
-
* This will read from `app-config.yaml` and `app-config.local.yaml` by
|
|
1370
|
-
* default, as well as environment variables prefixed with `APP_CONFIG_`.
|
|
1371
|
-
* If `--config <path|url>` command line arguments are passed, these will
|
|
1372
|
-
* override the default configuration file paths. URLs are only supported
|
|
1373
|
-
* if the `remote` option is provided.
|
|
1374
|
-
*
|
|
1375
|
-
* @param options - Options
|
|
1376
|
-
* @returns The default Backstage config source
|
|
1377
|
-
*/
|
|
1378
|
-
static default(options) {
|
|
1379
|
-
const argSource = this.defaultForTargets({
|
|
1380
|
-
...options,
|
|
1381
|
-
targets: this.parseArgs(options.argv)
|
|
1382
|
-
});
|
|
1383
|
-
const envSource = EnvConfigSource.create({
|
|
1384
|
-
env: options.env
|
|
1385
|
-
});
|
|
1386
|
-
return this.merge([argSource, envSource]);
|
|
1387
|
-
}
|
|
1388
|
-
/**
|
|
1389
|
-
* Merges multiple config sources into a single source that reads from all
|
|
1390
|
-
* sources and concatenates the result.
|
|
1391
|
-
*
|
|
1392
|
-
* @param sources - The config sources to merge
|
|
1393
|
-
* @returns A single config source that concatenates the data from the given sources
|
|
1394
|
-
*/
|
|
1395
|
-
static merge(sources) {
|
|
1396
|
-
return MergedConfigSource.from(sources);
|
|
1397
|
-
}
|
|
1398
|
-
/**
|
|
1399
|
-
* Creates an observable {@link @backstage/config#Config} implementation from a {@link ConfigSource}.
|
|
1400
|
-
*
|
|
1401
|
-
* @remarks
|
|
1402
|
-
*
|
|
1403
|
-
* If you only want to read the config once you can close the returned config immediately.
|
|
1404
|
-
*
|
|
1405
|
-
* @example
|
|
1406
|
-
*
|
|
1407
|
-
* ```ts
|
|
1408
|
-
* const sources = ConfigSources.default(...)
|
|
1409
|
-
* const config = await ConfigSources.toConfig(source)
|
|
1410
|
-
* config.close()
|
|
1411
|
-
* const example = config.getString(...)
|
|
1412
|
-
* ```
|
|
1413
|
-
*
|
|
1414
|
-
* @param source - The config source to read from
|
|
1415
|
-
* @returns A promise that resolves to a closable config
|
|
1416
|
-
*/
|
|
1417
|
-
static toConfig(source) {
|
|
1418
|
-
return new Promise(async (resolve, reject) => {
|
|
1419
|
-
let config$1 = void 0;
|
|
1420
|
-
try {
|
|
1421
|
-
const abortController = new AbortController();
|
|
1422
|
-
for await (const { configs } of source.readConfigData({
|
|
1423
|
-
signal: abortController.signal
|
|
1424
|
-
})) {
|
|
1425
|
-
if (config$1) {
|
|
1426
|
-
config$1.setConfig(config.ConfigReader.fromConfigs(configs));
|
|
1427
|
-
} else {
|
|
1428
|
-
config$1 = ObservableConfigProxy.create(abortController);
|
|
1429
|
-
config$1.setConfig(config.ConfigReader.fromConfigs(configs));
|
|
1430
|
-
resolve(config$1);
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
} catch (error) {
|
|
1434
|
-
reject(error);
|
|
1435
|
-
}
|
|
1436
|
-
});
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
class MutableConfigSource {
|
|
1441
|
-
/**
|
|
1442
|
-
* Creates a new mutable config source.
|
|
1443
|
-
*
|
|
1444
|
-
* @param options - Options for the config source.
|
|
1445
|
-
* @returns A new mutable config source.
|
|
1446
|
-
*/
|
|
1447
|
-
static create(options) {
|
|
1448
|
-
return new MutableConfigSource(
|
|
1449
|
-
options?.context ?? "mutable-config",
|
|
1450
|
-
options?.data
|
|
1451
|
-
);
|
|
1452
|
-
}
|
|
1453
|
-
#currentData;
|
|
1454
|
-
#deferred;
|
|
1455
|
-
#context;
|
|
1456
|
-
#abortController = new AbortController();
|
|
1457
|
-
constructor(context, initialData) {
|
|
1458
|
-
this.#currentData = initialData;
|
|
1459
|
-
this.#context = context;
|
|
1460
|
-
this.#deferred = simpleDefer();
|
|
1461
|
-
}
|
|
1462
|
-
async *readConfigData(options) {
|
|
1463
|
-
let deferredPromise = this.#deferred.promise;
|
|
1464
|
-
if (this.#currentData !== void 0) {
|
|
1465
|
-
yield { configs: [{ data: this.#currentData, context: this.#context }] };
|
|
1466
|
-
}
|
|
1467
|
-
for (; ; ) {
|
|
1468
|
-
const [ok] = await waitOrAbort(deferredPromise, [
|
|
1469
|
-
options?.signal,
|
|
1470
|
-
this.#abortController.signal
|
|
1471
|
-
]);
|
|
1472
|
-
if (!ok) {
|
|
1473
|
-
return;
|
|
1474
|
-
}
|
|
1475
|
-
deferredPromise = this.#deferred.promise;
|
|
1476
|
-
if (this.#currentData !== void 0) {
|
|
1477
|
-
yield {
|
|
1478
|
-
configs: [{ data: this.#currentData, context: this.#context }]
|
|
1479
|
-
};
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
/**
|
|
1484
|
-
* Set the data of the config source.
|
|
1485
|
-
*
|
|
1486
|
-
* @param data - The new data to set
|
|
1487
|
-
*/
|
|
1488
|
-
setData(data) {
|
|
1489
|
-
if (!this.#abortController.signal.aborted) {
|
|
1490
|
-
this.#currentData = data;
|
|
1491
|
-
const oldDeferred = this.#deferred;
|
|
1492
|
-
this.#deferred = simpleDefer();
|
|
1493
|
-
oldDeferred.resolve();
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
/**
|
|
1497
|
-
* Close the config source, preventing any further updates.
|
|
1498
|
-
*/
|
|
1499
|
-
close() {
|
|
1500
|
-
this.#currentData = void 0;
|
|
1501
|
-
this.#abortController.abort();
|
|
1502
|
-
}
|
|
1503
|
-
toString() {
|
|
1504
|
-
return `MutableConfigSource{}`;
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
|
|
1508
|
-
class StaticObservableConfigSource {
|
|
1509
|
-
constructor(data, context) {
|
|
1510
|
-
this.data = data;
|
|
1511
|
-
this.context = context;
|
|
1512
|
-
}
|
|
1513
|
-
async *readConfigData(options) {
|
|
1514
|
-
const queue = new Array();
|
|
1515
|
-
let deferred = simpleDefer();
|
|
1516
|
-
const sub = this.data.subscribe({
|
|
1517
|
-
next(value) {
|
|
1518
|
-
queue.push(value);
|
|
1519
|
-
deferred.resolve();
|
|
1520
|
-
deferred = simpleDefer();
|
|
1521
|
-
},
|
|
1522
|
-
complete() {
|
|
1523
|
-
deferred.resolve();
|
|
1524
|
-
}
|
|
1525
|
-
});
|
|
1526
|
-
const signal = options?.signal;
|
|
1527
|
-
if (signal) {
|
|
1528
|
-
const onAbort = () => {
|
|
1529
|
-
sub.unsubscribe();
|
|
1530
|
-
queue.length = 0;
|
|
1531
|
-
deferred.resolve();
|
|
1532
|
-
signal.removeEventListener("abort", onAbort);
|
|
1533
|
-
};
|
|
1534
|
-
signal.addEventListener("abort", onAbort);
|
|
1535
|
-
}
|
|
1536
|
-
for (; ; ) {
|
|
1537
|
-
await deferred.promise;
|
|
1538
|
-
if (queue.length === 0) {
|
|
1539
|
-
return;
|
|
1540
|
-
}
|
|
1541
|
-
while (queue.length > 0) {
|
|
1542
|
-
yield { configs: [{ data: queue.shift(), context: this.context }] };
|
|
1543
|
-
}
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1547
|
-
function isObservable(value) {
|
|
1548
|
-
return "subscribe" in value && typeof value.subscribe === "function";
|
|
1549
|
-
}
|
|
1550
|
-
function isAsyncIterable(value) {
|
|
1551
|
-
return Symbol.asyncIterator in value;
|
|
1552
|
-
}
|
|
1553
|
-
class StaticConfigSource {
|
|
1554
|
-
constructor(promise, context) {
|
|
1555
|
-
this.promise = promise;
|
|
1556
|
-
this.context = context;
|
|
1557
|
-
}
|
|
1558
|
-
/**
|
|
1559
|
-
* Creates a new {@link StaticConfigSource}.
|
|
1560
|
-
*
|
|
1561
|
-
* @param options - Options for the config source
|
|
1562
|
-
* @returns A new static config source
|
|
1563
|
-
*/
|
|
1564
|
-
static create(options) {
|
|
1565
|
-
const { data, context = "static-config" } = options;
|
|
1566
|
-
if (!data) {
|
|
1567
|
-
return {
|
|
1568
|
-
async *readConfigData() {
|
|
1569
|
-
yield { configs: [] };
|
|
1570
|
-
return;
|
|
1571
|
-
}
|
|
1572
|
-
};
|
|
1573
|
-
}
|
|
1574
|
-
if (isObservable(data)) {
|
|
1575
|
-
return new StaticObservableConfigSource(data, context);
|
|
1576
|
-
}
|
|
1577
|
-
if (isAsyncIterable(data)) {
|
|
1578
|
-
return {
|
|
1579
|
-
async *readConfigData() {
|
|
1580
|
-
for await (const value of data) {
|
|
1581
|
-
yield { configs: [{ data: value, context }] };
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
};
|
|
1585
|
-
}
|
|
1586
|
-
return new StaticConfigSource(data, context);
|
|
1587
|
-
}
|
|
1588
|
-
async *readConfigData() {
|
|
1589
|
-
yield { configs: [{ data: await this.promise, context: this.context }] };
|
|
1590
|
-
return;
|
|
1591
|
-
}
|
|
1592
|
-
toString() {
|
|
1593
|
-
return `StaticConfigSource{}`;
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
async function loadConfig(options) {
|
|
1598
|
-
const source = ConfigSources.default({
|
|
1599
|
-
substitutionFunc: options.experimentalEnvFunc,
|
|
1600
|
-
remote: options.remote && {
|
|
1601
|
-
reloadInterval: { seconds: options.remote.reloadIntervalSeconds }
|
|
1602
|
-
},
|
|
1603
|
-
watch: Boolean(options.watch),
|
|
1604
|
-
rootDir: options.configRoot,
|
|
1605
|
-
argv: options.configTargets.flatMap((t) => [
|
|
1606
|
-
"--config",
|
|
1607
|
-
"url" in t ? t.url : t.path
|
|
1608
|
-
])
|
|
1609
|
-
});
|
|
1610
|
-
return new Promise((resolve, reject) => {
|
|
1611
|
-
async function loadConfigReaderLoop() {
|
|
1612
|
-
let loaded = false;
|
|
1613
|
-
try {
|
|
1614
|
-
const abortController = new AbortController();
|
|
1615
|
-
options.watch?.stopSignal?.then(() => abortController.abort());
|
|
1616
|
-
for await (const { configs } of source.readConfigData({
|
|
1617
|
-
signal: abortController.signal
|
|
1618
|
-
})) {
|
|
1619
|
-
if (loaded) {
|
|
1620
|
-
options.watch?.onChange(configs);
|
|
1621
|
-
} else {
|
|
1622
|
-
resolve({ appConfigs: configs });
|
|
1623
|
-
loaded = true;
|
|
1624
|
-
if (options.watch) {
|
|
1625
|
-
options.watch.stopSignal?.then(() => abortController.abort());
|
|
1626
|
-
} else {
|
|
1627
|
-
abortController.abort();
|
|
1628
|
-
}
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
|
-
} catch (error) {
|
|
1632
|
-
if (loaded) {
|
|
1633
|
-
console.error(`Failed to reload configuration, ${error}`);
|
|
1634
|
-
} else {
|
|
1635
|
-
reject(error);
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
loadConfigReaderLoop();
|
|
1640
|
-
});
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
exports.ConfigSources = ConfigSources;
|
|
1644
|
-
exports.EnvConfigSource = EnvConfigSource;
|
|
1645
|
-
exports.FileConfigSource = FileConfigSource;
|
|
1646
|
-
exports.MutableConfigSource = MutableConfigSource;
|
|
1647
|
-
exports.RemoteConfigSource = RemoteConfigSource;
|
|
1648
|
-
exports.StaticConfigSource = StaticConfigSource;
|
|
1649
|
-
exports.loadConfig = loadConfig;
|
|
1650
|
-
exports.loadConfigSchema = loadConfigSchema;
|
|
1651
|
-
exports.mergeConfigSchemas = mergeConfigSchemas;
|
|
1652
|
-
exports.readEnvConfig = readEnvConfig;
|
|
3
|
+
var compile = require('./schema/compile.cjs.js');
|
|
4
|
+
var load = require('./schema/load.cjs.js');
|
|
5
|
+
var loader = require('./loader.cjs.js');
|
|
6
|
+
var ConfigSources = require('./sources/ConfigSources.cjs.js');
|
|
7
|
+
var EnvConfigSource = require('./sources/EnvConfigSource.cjs.js');
|
|
8
|
+
var FileConfigSource = require('./sources/FileConfigSource.cjs.js');
|
|
9
|
+
var MutableConfigSource = require('./sources/MutableConfigSource.cjs.js');
|
|
10
|
+
var RemoteConfigSource = require('./sources/RemoteConfigSource.cjs.js');
|
|
11
|
+
var StaticConfigSource = require('./sources/StaticConfigSource.cjs.js');
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
exports.mergeConfigSchemas = compile.mergeConfigSchemas;
|
|
16
|
+
exports.loadConfigSchema = load.loadConfigSchema;
|
|
17
|
+
exports.loadConfig = loader.loadConfig;
|
|
18
|
+
exports.ConfigSources = ConfigSources.ConfigSources;
|
|
19
|
+
exports.EnvConfigSource = EnvConfigSource.EnvConfigSource;
|
|
20
|
+
exports.readEnvConfig = EnvConfigSource.readEnvConfig;
|
|
21
|
+
exports.FileConfigSource = FileConfigSource.FileConfigSource;
|
|
22
|
+
exports.MutableConfigSource = MutableConfigSource.MutableConfigSource;
|
|
23
|
+
exports.RemoteConfigSource = RemoteConfigSource.RemoteConfigSource;
|
|
24
|
+
exports.StaticConfigSource = StaticConfigSource.StaticConfigSource;
|
|
1653
25
|
//# sourceMappingURL=index.cjs.js.map
|