'use strict'; var Ajv = require('ajv'); var mergeAllOf = require('json-schema-merge-allof'); var traverse = require('json-schema-traverse'); var config = require('@backstage/config'); var fs = require('fs-extra'); var os = require('os'); var path = require('path'); var errors = require('@backstage/errors'); var parseArgs = require('minimist'); var chokidar = require('chokidar'); var yaml = require('yaml'); var types = require('@backstage/types'); var isEqual = require('lodash/isEqual'); var fetch = require('node-fetch'); var cliCommon = require('@backstage/cli-common'); function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; } var Ajv__default = /*#__PURE__*/_interopDefaultCompat(Ajv); var mergeAllOf__default = /*#__PURE__*/_interopDefaultCompat(mergeAllOf); var traverse__default = /*#__PURE__*/_interopDefaultCompat(traverse); var fs__default = /*#__PURE__*/_interopDefaultCompat(fs); var parseArgs__default = /*#__PURE__*/_interopDefaultCompat(parseArgs); var chokidar__default = /*#__PURE__*/_interopDefaultCompat(chokidar); var yaml__default = /*#__PURE__*/_interopDefaultCompat(yaml); var isEqual__default = /*#__PURE__*/_interopDefaultCompat(isEqual); var fetch__default = /*#__PURE__*/_interopDefaultCompat(fetch); const CONFIG_VISIBILITIES = ["frontend", "backend", "secret"]; const DEFAULT_CONFIG_VISIBILITY = "backend"; function normalizeAjvPath(path) { return path.replace(/~1/g, "/").replace(/\['?(.*?)'?\]/g, (_, segment) => `/${segment}`); } const inheritedVisibility = Symbol("inherited-visibility"); function compileConfigSchemas(schemas, options) { const visibilityByDataPath = /* @__PURE__ */ new Map(); const deepVisibilityByDataPath = /* @__PURE__ */ new Map(); const deprecationByDataPath = /* @__PURE__ */ new Map(); const ajv = new Ajv__default.default({ allErrors: true, allowUnionTypes: true, coerceTypes: true, schemas: { "https://backstage.io/schema/config-v1": true } }).addKeyword({ keyword: "visibility", metaSchema: { type: "string", enum: CONFIG_VISIBILITIES }, compile(visibility) { return (_data, context) => { if ((context == null ? void 0 : context.instancePath) === void 0) { return false; } if (visibility && visibility !== "backend") { const normalizedPath = normalizeAjvPath(context.instancePath); visibilityByDataPath.set(normalizedPath, visibility); } return true; }; } }).addKeyword({ keyword: "deepVisibility", metaSchema: { type: "string", /** * Disallow 'backend' deepVisibility to prevent cases of permission escaping. * * Something like: * - deepVisibility secret -> backend -> frontend. * - deepVisibility secret -> backend -> visibility frontend. */ enum: ["frontend", "secret"] }, compile(visibility) { return (_data, context) => { if ((context == null ? void 0 : context.instancePath) === void 0) { return false; } if (visibility) { const normalizedPath = normalizeAjvPath(context.instancePath); deepVisibilityByDataPath.set(normalizedPath, visibility); } return true; }; } }).removeKeyword("deprecated").addKeyword({ keyword: "deprecated", metaSchema: { type: "string" }, compile(deprecationDescription) { return (_data, context) => { if ((context == null ? void 0 : context.instancePath) === void 0) { return false; } const normalizedPath = normalizeAjvPath(context.instancePath); deprecationByDataPath.set(normalizedPath, deprecationDescription); return true; }; } }); for (const schema of schemas) { try { ajv.compile(schema.value); } catch (error) { throw new Error(`Schema at ${schema.path} is invalid, ${error}`); } } const merged = mergeConfigSchemas(schemas.map((_) => _.value)); traverse__default.default( merged, (schema, jsonPtr, _1, _2, _3, parentSchema) => { var _a, _b; (_b = schema[inheritedVisibility]) != null ? _b : schema[inheritedVisibility] = (_a = schema == null ? void 0 : schema.deepVisibility) != null ? _a : parentSchema == null ? void 0 : parentSchema[inheritedVisibility]; if (schema[inheritedVisibility]) { const values = [ schema.visibility, schema[inheritedVisibility], parentSchema == null ? void 0 : parentSchema[inheritedVisibility] ]; const hasFrontend = values.some((e) => e === "frontend"); const hasSecret = values.some((e) => e === "secret"); if (hasFrontend && hasSecret) { throw new Error( `Config schema visibility is both 'frontend' and 'secret' for ${jsonPtr}` ); } } if (options == null ? void 0 : options.noUndeclaredProperties) { if ((schema == null ? void 0 : schema.type) === "object") { schema.additionalProperties || (schema.additionalProperties = false); } } } ); const validate = ajv.compile(merged); const visibilityBySchemaPath = /* @__PURE__ */ new Map(); traverse__default.default(merged, (schema, path) => { if (schema.visibility && schema.visibility !== "backend") { visibilityBySchemaPath.set(normalizeAjvPath(path), schema.visibility); } if (schema.deepVisibility) { visibilityBySchemaPath.set(normalizeAjvPath(path), schema.deepVisibility); } }); return (configs) => { var _a; const config$1 = config.ConfigReader.fromConfigs(configs).getOptional(); visibilityByDataPath.clear(); deepVisibilityByDataPath.clear(); const valid = validate(config$1); if (!valid) { return { errors: (_a = validate.errors) != null ? _a : [], visibilityByDataPath: new Map(visibilityByDataPath), deepVisibilityByDataPath: new Map(deepVisibilityByDataPath), visibilityBySchemaPath, deprecationByDataPath }; } return { visibilityByDataPath: new Map(visibilityByDataPath), deepVisibilityByDataPath: new Map(deepVisibilityByDataPath), visibilityBySchemaPath, deprecationByDataPath }; }; } function mergeConfigSchemas(schemas) { const merged = mergeAllOf__default.default( { allOf: schemas }, { // JSONSchema is typically subtractive, as in it always reduces the set of allowed // inputs through constraints. This changes the object property merging to be additive // rather than subtractive. ignoreAdditionalProperties: true, resolvers: { // This ensures that the visibilities across different schemas are sound, and // selects the most specific visibility for each path. visibility(values, path) { const hasFrontend = values.some((_) => _ === "frontend"); const hasSecret = values.some((_) => _ === "secret"); if (hasFrontend && hasSecret) { throw new Error( `Config schema visibility is both 'frontend' and 'secret' for ${path.join( "/" )}` ); } else if (hasFrontend) { return "frontend"; } else if (hasSecret) { return "secret"; } return "backend"; } } } ); return merged; } const req = typeof __non_webpack_require__ === "undefined" ? require : __non_webpack_require__; async function collectConfigSchemas(packageNames, packagePaths) { const schemas = new Array(); const tsSchemaPaths = new Array(); const visitedPackageVersions = /* @__PURE__ */ new Map(); const currentDir = await fs__default.default.realpath(process.cwd()); async function processItem(item) { var _a, _b, _c, _d; let pkgPath = item.packagePath; if (pkgPath) { const pkgExists = await fs__default.default.pathExists(pkgPath); if (!pkgExists) { return; } } else if (item.name) { const { name, parentPath } = item; try { pkgPath = req.resolve( `${name}/package.json`, parentPath && { paths: [parentPath] } ); } catch { } } if (!pkgPath) { return; } const pkg = await fs__default.default.readJson(pkgPath); let versions = visitedPackageVersions.get(pkg.name); if (versions == null ? void 0 : versions.has(pkg.version)) { return; } if (!versions) { versions = /* @__PURE__ */ new Set(); visitedPackageVersions.set(pkg.name, versions); } versions.add(pkg.version); const depNames = [ ...Object.keys((_a = pkg.dependencies) != null ? _a : {}), ...Object.keys((_b = pkg.devDependencies) != null ? _b : {}), ...Object.keys((_c = pkg.optionalDependencies) != null ? _c : {}), ...Object.keys((_d = pkg.peerDependencies) != null ? _d : {}) ]; const hasSchema = "configSchema" in pkg; const hasBackstageDep = depNames.some((_) => _.startsWith("@backstage/")); if (!hasSchema && !hasBackstageDep) { return; } if (hasSchema) { if (typeof pkg.configSchema === "string") { const isJson = pkg.configSchema.endsWith(".json"); const isDts = pkg.configSchema.endsWith(".d.ts"); if (!isJson && !isDts) { throw new Error( `Config schema files must be .json or .d.ts, got ${pkg.configSchema}` ); } if (isDts) { tsSchemaPaths.push( path.relative( currentDir, path.resolve(path.dirname(pkgPath), pkg.configSchema) ) ); } else { const path$1 = path.resolve(path.dirname(pkgPath), pkg.configSchema); const value = await fs__default.default.readJson(path$1); schemas.push({ value, path: path.relative(currentDir, path$1) }); } } else { schemas.push({ value: pkg.configSchema, path: path.relative(currentDir, pkgPath) }); } } await Promise.all( depNames.map( (depName) => processItem({ name: depName, parentPath: pkgPath }) ) ); } await Promise.all([ ...packageNames.map((name) => processItem({ name, parentPath: currentDir })), ...packagePaths.map((path) => processItem({ name: path, packagePath: path })) ]); const tsSchemas = await compileTsSchemas(tsSchemaPaths); return schemas.concat(tsSchemas); } async function compileTsSchemas(paths) { if (paths.length === 0) { return []; } const { getProgramFromFiles, buildGenerator } = await import('typescript-json-schema'); const program = getProgramFromFiles(paths, { incremental: false, isolatedModules: true, lib: ["ES5"], // Skipping most libs speeds processing up a lot, we just need the primitive types anyway noEmit: true, noResolve: true, skipLibCheck: true, // Skipping lib checks speeds things up skipDefaultLibCheck: true, strict: true, typeRoots: [], // Do not include any additional types types: [] }); const tsSchemas = paths.map((path$1) => { var _a; let value; try { const generator = buildGenerator( program, // This enables the use of these tags in TSDoc comments { required: true, validationKeywords: ["visibility", "deepVisibility", "deprecated"] }, [path$1.split(path.sep).join("/")] // Unix paths are expected for all OSes here ); value = generator == null ? void 0 : generator.getSchemaForSymbol("Config"); const userSymbols = new Set(generator == null ? void 0 : generator.getUserSymbols()); userSymbols.delete("Config"); if (userSymbols.size !== 0) { const names = Array.from(userSymbols).join("', '"); throw new Error( `Invalid configuration schema in ${path$1}, additional symbol definitions are not allowed, found '${names}'` ); } const reffedDefs = Object.keys((_a = generator == null ? void 0 : generator.ReffedDefinitions) != null ? _a : {}); if (reffedDefs.length !== 0) { const lines = reffedDefs.join(`${os.EOL} `); throw new Error( `Invalid configuration schema in ${path$1}, the following definitions are not supported:${os.EOL}${os.EOL} ${lines}` ); } } catch (error) { errors.assertError(error); if (error.message !== "type Config not found") { throw error; } } if (!value) { throw new Error(`Invalid schema in ${path$1}, missing Config export`); } return { path: path$1, value }; }); return tsSchemas; } function filterByVisibility(data, includeVisibilities, visibilityByDataPath, deepVisibilityByDataPath, deprecationByDataPath, transformFunc, withFilteredKeys, withDeprecatedKeys) { var _a; const filteredKeys = new Array(); const deprecatedKeys = new Array(); function transform(jsonVal, visibilityPath, filterPath, inheritedVisibility) { var _a2, _b; const visibility = (_a2 = visibilityByDataPath.get(visibilityPath)) != null ? _a2 : inheritedVisibility; const isVisible = includeVisibilities.includes(visibility); const newInheritedVisibility = (_b = deepVisibilityByDataPath.get(visibilityPath)) != null ? _b : inheritedVisibility; const deprecation = deprecationByDataPath.get(visibilityPath); if (deprecation) { deprecatedKeys.push({ key: filterPath, description: deprecation }); } if (typeof jsonVal !== "object") { if (isVisible) { if (transformFunc) { return transformFunc(jsonVal, { visibility, path: filterPath }); } return jsonVal; } if (withFilteredKeys) { filteredKeys.push(filterPath); } return void 0; } else if (jsonVal === null) { return void 0; } else if (Array.isArray(jsonVal)) { const arr = new Array(); for (const [index, value] of jsonVal.entries()) { let path = visibilityPath; const hasVisibilityInIndex = visibilityByDataPath.get( `${visibilityPath}/${index}` ); if (hasVisibilityInIndex || typeof value === "object") { path = `${visibilityPath}/${index}`; } const out = transform( value, path, `${filterPath}[${index}]`, newInheritedVisibility ); if (out !== void 0) { arr.push(out); } } if (arr.length > 0 || isVisible) { return arr; } return void 0; } const outObj = {}; let hasOutput = false; for (const [key, value] of Object.entries(jsonVal)) { if (value === void 0) { continue; } const out = transform( value, `${visibilityPath}/${key}`, filterPath ? `${filterPath}.${key}` : key, newInheritedVisibility ); if (out !== void 0) { outObj[key] = out; hasOutput = true; } } if (hasOutput || isVisible) { return outObj; } return void 0; } return { filteredKeys: withFilteredKeys ? filteredKeys : void 0, deprecatedKeys: withDeprecatedKeys ? deprecatedKeys : void 0, data: (_a = transform(data, "", "", DEFAULT_CONFIG_VISIBILITY)) != null ? _a : {} }; } function filterErrorsByVisibility(errors, includeVisibilities, visibilityByDataPath, visibilityBySchemaPath) { if (!errors) { return []; } if (!includeVisibilities) { return errors; } const visibleSchemaPaths = Array.from(visibilityBySchemaPath).filter(([, v]) => includeVisibilities.includes(v)).map(([k]) => k); return errors.filter((error) => { var _a; if (error.keyword === "type" && ["object", "array"].includes(error.params.type)) { return true; } if (error.keyword === "required") { const trimmedPath = normalizeAjvPath(error.schemaPath).slice( 1, -"/required".length ); const fullPath = `${trimmedPath}/properties/${error.params.missingProperty}`; if (visibleSchemaPaths.some((visiblePath) => visiblePath.startsWith(fullPath))) { return true; } } const vis = (_a = visibilityByDataPath.get(normalizeAjvPath(error.instancePath))) != null ? _a : DEFAULT_CONFIG_VISIBILITY; return vis && includeVisibilities.includes(vis); }); } function errorsToError(errors) { const messages = errors.map(({ instancePath, message, params }) => { const paramStr = Object.entries(params).map(([name, value]) => `${name}=${value}`).join(" "); return `Config ${message || ""} { ${paramStr} } at ${normalizeAjvPath( instancePath )}`; }); const error = new Error(`Config validation failed, ${messages.join("; ")}`); error.messages = messages; return error; } async function loadConfigSchema(options) { var _a; let schemas; if ("dependencies" in options) { schemas = await collectConfigSchemas( options.dependencies, (_a = options.packagePaths) != null ? _a : [] ); } else { const { serialized } = options; if ((serialized == null ? void 0 : serialized.backstageConfigSchemaVersion) !== 1) { throw new Error( "Serialized configuration schema is invalid or has an invalid version number" ); } schemas = serialized.schemas; } const validate = compileConfigSchemas(schemas, { noUndeclaredProperties: options.noUndeclaredProperties }); return { process(configs, { visibility, valueTransform, withFilteredKeys, withDeprecatedKeys, ignoreSchemaErrors } = {}) { const result = validate(configs); if (!ignoreSchemaErrors) { const visibleErrors = filterErrorsByVisibility( result.errors, visibility, result.visibilityByDataPath, result.visibilityBySchemaPath ); if (visibleErrors.length > 0) { throw errorsToError(visibleErrors); } } let processedConfigs = configs; if (visibility) { processedConfigs = processedConfigs.map(({ data, context }) => ({ context, ...filterByVisibility( data, visibility, result.visibilityByDataPath, result.deepVisibilityByDataPath, result.deprecationByDataPath, valueTransform, withFilteredKeys, withDeprecatedKeys ) })); } else if (valueTransform) { processedConfigs = processedConfigs.map(({ data, context }) => ({ context, ...filterByVisibility( data, Array.from(CONFIG_VISIBILITIES), result.visibilityByDataPath, result.deepVisibilityByDataPath, result.deprecationByDataPath, valueTransform, withFilteredKeys, withDeprecatedKeys ) })); } return processedConfigs; }, serialize() { return { schemas, backstageConfigSchemaVersion: 1 }; } }; } class EnvConfigSource { constructor(env) { this.env = env; } /** * Creates a new config source that reads from the environment. * * @param options - Options for the config source. * @returns A new config source that reads from the environment. */ static create(options) { var _a; return new EnvConfigSource((_a = options == null ? void 0 : options.env) != null ? _a : process.env); } async *readConfigData() { const configs = readEnvConfig(this.env); yield { configs }; return; } toString() { const keys = Object.keys(this.env).filter( (key) => key.startsWith("APP_CONFIG_") ); return `EnvConfigSource{count=${keys.length}}`; } } const ENV_PREFIX = "APP_CONFIG_"; const CONFIG_KEY_PART_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z][a-z0-9]*)*$/i; function readEnvConfig(env) { var _a; let data = void 0; for (const [name, value] of Object.entries(env)) { if (!value) { continue; } if (name.startsWith(ENV_PREFIX)) { const key = name.replace(ENV_PREFIX, ""); const keyParts = key.split("_"); let obj = data = data != null ? data : {}; for (const [index, part] of keyParts.entries()) { if (!CONFIG_KEY_PART_PATTERN.test(part)) { throw new TypeError(`Invalid env config key '${key}'`); } if (index < keyParts.length - 1) { obj = obj[part] = (_a = obj[part]) != null ? _a : {}; if (typeof obj !== "object" || Array.isArray(obj)) { const subKey = keyParts.slice(0, index + 1).join("_"); throw new TypeError( `Could not nest config for key '${key}' under existing value '${subKey}'` ); } } else { if (part in obj) { throw new TypeError( `Refusing to override existing config at key '${key}'` ); } try { const [, parsedValue] = safeJsonParse(value); if (parsedValue === null) { throw new Error("value may not be null"); } obj[part] = parsedValue; } catch (error) { throw new TypeError( `Failed to parse JSON-serialized config value for key '${key}', ${error}` ); } } } } } return data ? [{ data, context: "env" }] : []; } function safeJsonParse(str) { try { return [null, JSON.parse(str)]; } catch (err) { errors.assertError(err); return [err, str]; } } function isObject(obj) { if (typeof obj !== "object") { return false; } else if (Array.isArray(obj)) { return false; } return obj !== null; } function createSubstitutionTransform(env) { return async (input) => { if (typeof input !== "string") { return { applied: false }; } const parts = input.split(/(\$?\$\{[^{}]*\})/); for (let i = 1; i < parts.length; i += 2) { const part = parts[i]; if (part.startsWith("$$")) { parts[i] = part.slice(1); } else { const indexOfFallbackSeparator = part.indexOf(":-"); if (indexOfFallbackSeparator > -1) { const envVarValue = await env( part.slice(2, indexOfFallbackSeparator).trim() ); const fallbackValue = part.slice(indexOfFallbackSeparator + ":-".length, -1).trim(); parts[i] = envVarValue || fallbackValue || void 0; } else { parts[i] = await env(part.slice(2, -1).trim()); } } } if (parts.some((part) => part === void 0)) { return { applied: true, value: void 0 }; } return { applied: true, value: parts.join("") }; }; } const includeFileParser = { ".json": async (content) => JSON.parse(content), ".yaml": async (content) => yaml__default.default.parse(content), ".yml": async (content) => yaml__default.default.parse(content) }; function createIncludeTransform(env, readFile, substitute) { return async (input, context) => { const { dir } = context; if (!dir) { throw new Error("Include transform requires a base directory"); } if (!isObject(input)) { return { applied: false }; } const [includeKey] = Object.keys(input).filter((key) => key.startsWith("$")); if (includeKey) { if (Object.keys(input).length !== 1) { throw new Error( `include key ${includeKey} should not have adjacent keys` ); } } else { return { applied: false }; } const rawIncludedValue = input[includeKey]; if (typeof rawIncludedValue !== "string") { throw new Error(`${includeKey} include value is not a string`); } const substituteResults = await substitute(rawIncludedValue, { dir }); const includeValue = substituteResults.applied ? substituteResults.value : rawIncludedValue; if (includeValue === void 0 || typeof includeValue !== "string") { throw new Error(`${includeKey} substitution value was undefined`); } switch (includeKey) { case "$file": try { const value = await readFile(path.resolve(dir, includeValue)); return { applied: true, value: value.trimEnd() }; } catch (error) { throw new Error(`failed to read file ${includeValue}, ${error}`); } case "$env": try { return { applied: true, value: await env(includeValue) }; } catch (error) { throw new Error(`failed to read env ${includeValue}, ${error}`); } case "$include": { const [filePath, dataPath] = includeValue.split(/#(.*)/); const ext = path.extname(filePath); const parser = includeFileParser[ext]; if (!parser) { throw new Error( `no configuration parser available for included file ${filePath}` ); } const path$1 = path.resolve(dir, filePath); const content = await readFile(path$1); const newDir = path.dirname(path$1); const parts = dataPath ? dataPath.split(".") : []; let value; try { value = await parser(content); } catch (error) { throw new Error( `failed to parse included file ${filePath}, ${error}` ); } for (const [index, part] of parts.entries()) { if (!isObject(value)) { const errPath = parts.slice(0, index).join("."); throw new Error( `value at '${errPath}' in included file ${filePath} is not an object` ); } value = value[part]; } if (typeof value === "string") { const substituted = await substitute(value, { dir: newDir }); if (substituted.applied) { value = substituted.value; } } return { applied: true, value, newDir: newDir !== dir ? newDir : void 0 }; } default: throw new Error(`unknown include ${includeKey}`); } }; } async function applyConfigTransforms(input, context, transforms) { async function transform(inputObj, path, baseDir) { var _a; let obj = inputObj; let dir = baseDir; for (const tf of transforms) { try { const result = await tf(inputObj, { dir }); if (result.applied) { if (result.value === void 0) { return void 0; } obj = result.value; dir = (_a = result == null ? void 0 : result.newDir) != null ? _a : dir; break; } } catch (error) { errors.assertError(error); throw new Error(`error at ${path}, ${error.message}`); } } if (typeof obj !== "object") { return obj; } else if (obj === null) { return null; } else if (Array.isArray(obj)) { const arr = new Array(); for (const [index, value] of obj.entries()) { const out2 = await transform(value, `${path}[${index}]`, dir); if (out2 !== void 0) { arr.push(out2); } } return arr; } const out = {}; for (const [key, value] of Object.entries(obj)) { if (value !== void 0) { const result = await transform(value, `${path}.${key}`, dir); if (result !== void 0) { out[key] = result; } } } return out; } const finalData = await transform(input, "", context == null ? void 0 : context.dir); if (!isObject(finalData)) { throw new TypeError("expected object at config root"); } return finalData; } function createConfigTransformer(options) { const { substitutionFunc = async (name) => { var _a; return (_a = process.env[name]) == null ? void 0 : _a.trim(); }, readFile } = options; const substitutionTransform = createSubstitutionTransform(substitutionFunc); const transforms = [substitutionTransform]; if (readFile) { const includeTransform = createIncludeTransform( substitutionFunc, readFile, substitutionTransform ); transforms.push(includeTransform); } return async (input, ctx) => applyConfigTransforms(input, ctx != null ? ctx : {}, transforms); } var __accessCheck$3 = (obj, member, msg) => { if (!member.has(obj)) throw TypeError("Cannot " + msg); }; var __privateGet$2 = (obj, member, getter) => { __accessCheck$3(obj, member, "read from private field"); return getter ? getter.call(obj) : member.get(obj); }; var __privateAdd$3 = (obj, member, value) => { if (member.has(obj)) throw TypeError("Cannot add the same private member more than once"); member instanceof WeakSet ? member.add(obj) : member.set(obj, value); }; var __privateSet$2 = (obj, member, value, setter) => { __accessCheck$3(obj, member, "write to private field"); setter ? setter.call(obj, value) : member.set(obj, value); return value; }; var __privateMethod$2 = (obj, member, method) => { __accessCheck$3(obj, member, "access private method"); return method; }; var _path, _substitutionFunc, _watch, _waitForEvent, waitForEvent_fn; async function readFile(path) { try { const content = await fs__default.default.readFile(path, "utf8"); if (content === "") { await new Promise((resolve) => setTimeout(resolve, 10)); return await fs__default.default.readFile(path, "utf8"); } return content; } catch (error) { if (error.code === "ENOENT") { return void 0; } throw error; } } const _FileConfigSource = class _FileConfigSource { constructor(options) { __privateAdd$3(this, _waitForEvent); __privateAdd$3(this, _path, void 0); __privateAdd$3(this, _substitutionFunc, void 0); __privateAdd$3(this, _watch, void 0); var _a; __privateSet$2(this, _path, options.path); __privateSet$2(this, _substitutionFunc, options.substitutionFunc); __privateSet$2(this, _watch, (_a = options.watch) != null ? _a : true); } /** * Creates a new config source that loads configuration from the given path. * * @remarks * * The source will watch the file for changes, as well as any referenced files. * * @param options - Options for the config source. * @returns A new config source that loads from the given path. */ static create(options) { if (!path.isAbsolute(options.path)) { throw new Error(`Config load path is not absolute: "${options.path}"`); } return new _FileConfigSource(options); } // Work is duplicated across each read, in practice that should not // have any impact since there won't be multiple consumers. If that // changes it might be worth refactoring this to avoid duplicate work. async *readConfigData(options) { const signal = options == null ? void 0 : options.signal; const configFileName = path.basename(__privateGet$2(this, _path)); let watchedPaths = null; let watcher = null; if (__privateGet$2(this, _watch)) { watchedPaths = new Array(); watcher = chokidar__default.default.watch(__privateGet$2(this, _path), { usePolling: process.env.NODE_ENV === "test" }); } const dir = path.dirname(__privateGet$2(this, _path)); const transformer = createConfigTransformer({ substitutionFunc: __privateGet$2(this, _substitutionFunc), readFile: async (path$1) => { const fullPath = path.resolve(dir, path$1); if (watcher && watchedPaths) { watcher.add(fullPath); watchedPaths.push(fullPath); } const data = await readFile(fullPath); if (data === void 0) { throw new errors.NotFoundError( `failed to include "${fullPath}", file does not exist` ); } return data; } }); const readConfigFile = async () => { if (watcher && watchedPaths) { watcher.unwatch(watchedPaths); watchedPaths.length = 0; watcher.add(__privateGet$2(this, _path)); watchedPaths.push(__privateGet$2(this, _path)); } const content = await readFile(__privateGet$2(this, _path)); if (content === void 0) { throw new errors.NotFoundError(`Config file "${__privateGet$2(this, _path)}" does not exist`); } const parsed = yaml__default.default.parse(content); if (parsed === null) { return []; } try { const data = await transformer(parsed, { dir }); return [{ data, context: configFileName, path: __privateGet$2(this, _path) }]; } catch (error) { throw new Error( `Failed to read config file at "${__privateGet$2(this, _path)}", ${error.message}` ); } }; const onAbort = () => { signal == null ? void 0 : signal.removeEventListener("abort", onAbort); if (watcher) watcher.close(); }; signal == null ? void 0 : signal.addEventListener("abort", onAbort); yield { configs: await readConfigFile() }; if (watcher) { for (; ; ) { const event = await __privateMethod$2(this, _waitForEvent, waitForEvent_fn).call(this, watcher, signal); if (event === "abort") { return; } yield { configs: await readConfigFile() }; } } } toString() { return `FileConfigSource{path="${__privateGet$2(this, _path)}"}`; } }; _path = new WeakMap(); _substitutionFunc = new WeakMap(); _watch = new WeakMap(); _waitForEvent = new WeakSet(); waitForEvent_fn = function(watcher, signal) { return new Promise((resolve) => { function onChange() { resolve("change"); onDone(); } function onAbort() { resolve("abort"); onDone(); } function onDone() { watcher.removeListener("change", onChange); signal == null ? void 0 : signal.removeEventListener("abort", onAbort); } watcher.addListener("change", onChange); signal == null ? void 0 : signal.addEventListener("abort", onAbort); }); }; let FileConfigSource = _FileConfigSource; var __defProp$1 = Object.defineProperty; var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField$1 = (obj, key, value) => { __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; var __accessCheck$2 = (obj, member, msg) => { if (!member.has(obj)) throw TypeError("Cannot " + msg); }; var __privateAdd$2 = (obj, member, value) => { if (member.has(obj)) throw TypeError("Cannot add the same private member more than once"); member instanceof WeakSet ? member.add(obj) : member.set(obj, value); }; var __privateMethod$1 = (obj, member, method) => { __accessCheck$2(obj, member, "access private method"); return method; }; var _flattenSources, flattenSources_fn, _a; const sourcesSymbol = Symbol.for( "@backstage/config-loader#MergedConfigSource.sources" ); const _MergedConfigSource = class _MergedConfigSource { constructor(sources) { this.sources = sources; __publicField$1(this, _a); this[sourcesSymbol] = this.sources; } static from(sources) { return new _MergedConfigSource(__privateMethod$1(this, _flattenSources, flattenSources_fn).call(this, sources)); } async *readConfigData(options) { const its = this.sources.map((source) => source.readConfigData(options)); const initialResults = await Promise.all(its.map((it) => it.next())); const configs = initialResults.map((result, i) => { if (result.done) { throw new Error( `Config source ${String(this.sources[i])} returned no data` ); } return result.value.configs; }); yield { configs: configs.flat(1) }; const results = its.map((it, i) => nextWithIndex(it, i)); while (results.some(Boolean)) { try { const [i, result] = await Promise.race(results.filter(Boolean)); if (result.done) { results[i] = void 0; } else { results[i] = nextWithIndex(its[i], i); configs[i] = result.value.configs; yield { configs: configs.flat(1) }; } } catch (error) { const source = this.sources[error.index]; if (source) { throw new Error(`Config source ${String(source)} failed: ${error}`); } throw error; } } } toString() { return `MergedConfigSource{${this.sources.map(String).join(", ")}}`; } }; _a = sourcesSymbol; _flattenSources = new WeakSet(); flattenSources_fn = function(sources) { return sources.flatMap((source) => { if (sourcesSymbol in source && Array.isArray(source[sourcesSymbol])) { return __privateMethod$1(this, _flattenSources, flattenSources_fn).call(this, source[sourcesSymbol]); } return source; }); }; // An optimization to flatten nested merged sources to avid unnecessary microtasks __privateAdd$2(_MergedConfigSource, _flattenSources); let MergedConfigSource = _MergedConfigSource; function nextWithIndex(iterator, index) { return iterator.next().then( (r) => [index, r], (e) => { throw Object.assign(e, { index }); } ); } var __accessCheck$1 = (obj, member, msg) => { if (!member.has(obj)) throw TypeError("Cannot " + msg); }; var __privateGet$1 = (obj, member, getter) => { __accessCheck$1(obj, member, "read from private field"); return getter ? getter.call(obj) : member.get(obj); }; var __privateAdd$1 = (obj, member, value) => { if (member.has(obj)) throw TypeError("Cannot add the same private member more than once"); member instanceof WeakSet ? member.add(obj) : member.set(obj, value); }; var __privateSet$1 = (obj, member, value, setter) => { __accessCheck$1(obj, member, "write to private field"); setter ? setter.call(obj, value) : member.set(obj, value); return value; }; var __privateMethod = (obj, member, method) => { __accessCheck$1(obj, member, "access private method"); return method; }; var _url, _reloadIntervalMs, _transformer, _load, load_fn, _wait, wait_fn; const DEFAULT_RELOAD_INTERVAL = { seconds: 60 }; const _RemoteConfigSource = class _RemoteConfigSource { constructor(options) { __privateAdd$1(this, _load); __privateAdd$1(this, _wait); __privateAdd$1(this, _url, void 0); __privateAdd$1(this, _reloadIntervalMs, void 0); __privateAdd$1(this, _transformer, void 0); var _a; __privateSet$1(this, _url, options.url); __privateSet$1(this, _reloadIntervalMs, types.durationToMilliseconds( (_a = options.reloadInterval) != null ? _a : DEFAULT_RELOAD_INTERVAL )); __privateSet$1(this, _transformer, createConfigTransformer({ substitutionFunc: options.substitutionFunc })); } /** * Creates a new {@link RemoteConfigSource}. * * @param options - Options for the source. * @returns A new remote config source. */ static create(options) { try { new URL(options.url); } catch (error) { throw new Error( `Invalid URL provided to remote config source, '${options.url}', ${error}` ); } return new _RemoteConfigSource(options); } async *readConfigData(options) { var _a; let data = await __privateMethod(this, _load, load_fn).call(this); yield { configs: [{ data, context: __privateGet$1(this, _url) }] }; for (; ; ) { await __privateMethod(this, _wait, wait_fn).call(this, options == null ? void 0 : options.signal); if ((_a = options == null ? void 0 : options.signal) == null ? void 0 : _a.aborted) { return; } try { const newData = await __privateMethod(this, _load, load_fn).call(this, options == null ? void 0 : options.signal); if (newData && !isEqual__default.default(data, newData)) { data = newData; yield { configs: [{ data, context: __privateGet$1(this, _url) }] }; } } catch (error) { if (error.name !== "AbortError") { console.error(`Failed to read config from ${__privateGet$1(this, _url)}, ${error}`); } } } } toString() { return `RemoteConfigSource{path="${__privateGet$1(this, _url)}"}`; } }; _url = new WeakMap(); _reloadIntervalMs = new WeakMap(); _transformer = new WeakMap(); _load = new WeakSet(); load_fn = async function(signal) { const res = await fetch__default.default(__privateGet$1(this, _url), { signal }); if (!res.ok) { throw await errors.ResponseError.fromResponse(res); } const content = await res.text(); const data = await __privateGet$1(this, _transformer).call(this, yaml__default.default.parse(content)); if (data === null) { throw new Error("configuration data is null"); } else if (typeof data !== "object") { throw new Error("configuration data is not an object"); } else if (Array.isArray(data)) { throw new Error( "configuration data is an array, expected an object instead" ); } return data; }; _wait = new WeakSet(); wait_fn = async function(signal) { return new Promise((resolve) => { const timeoutId = setTimeout(onDone, __privateGet$1(this, _reloadIntervalMs)); signal == null ? void 0 : signal.addEventListener("abort", onDone); function onDone() { clearTimeout(timeoutId); signal == null ? void 0 : signal.removeEventListener("abort", onDone); resolve(); } }); }; let RemoteConfigSource = _RemoteConfigSource; var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => { __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; class ObservableConfigProxy { constructor(parent, parentKey, abortController) { this.parent = parent; this.parentKey = parentKey; this.abortController = abortController; __publicField(this, "config", new config.ConfigReader({})); __publicField(this, "subscribers", []); if (parent && !parentKey) { throw new Error("parentKey is required if parent is set"); } } static create(abortController) { return new ObservableConfigProxy(void 0, void 0, abortController); } setConfig(config) { if (this.parent) { throw new Error("immutable"); } const changed = !isEqual__default.default(this.config.get(), config.get()); this.config = config; if (changed) { for (const subscriber of this.subscribers) { try { subscriber(); } catch (error) { console.error(`Config subscriber threw error, ${error}`); } } } } close() { if (!this.abortController) { throw new Error("Only the root config can be closed"); } this.abortController.abort(); } subscribe(onChange) { if (this.parent) { return this.parent.subscribe(onChange); } this.subscribers.push(onChange); return { unsubscribe: () => { const index = this.subscribers.indexOf(onChange); if (index >= 0) { this.subscribers.splice(index, 1); } } }; } select(required) { var _a; if (this.parent && this.parentKey) { if (required) { return this.parent.select(true).getConfig(this.parentKey); } return (_a = this.parent.select(false)) == null ? void 0 : _a.getOptionalConfig(this.parentKey); } return this.config; } has(key) { var _a, _b; return (_b = (_a = this.select(false)) == null ? void 0 : _a.has(key)) != null ? _b : false; } keys() { var _a, _b; return (_b = (_a = this.select(false)) == null ? void 0 : _a.keys()) != null ? _b : []; } get(key) { return this.select(true).get(key); } getOptional(key) { var _a; return (_a = this.select(false)) == null ? void 0 : _a.getOptional(key); } getConfig(key) { return new ObservableConfigProxy(this, key); } getOptionalConfig(key) { var _a; if ((_a = this.select(false)) == null ? void 0 : _a.has(key)) { return new ObservableConfigProxy(this, key); } return void 0; } getConfigArray(key) { return this.select(true).getConfigArray(key); } getOptionalConfigArray(key) { var _a; return (_a = this.select(false)) == null ? void 0 : _a.getOptionalConfigArray(key); } getNumber(key) { return this.select(true).getNumber(key); } getOptionalNumber(key) { var _a; return (_a = this.select(false)) == null ? void 0 : _a.getOptionalNumber(key); } getBoolean(key) { return this.select(true).getBoolean(key); } getOptionalBoolean(key) { var _a; return (_a = this.select(false)) == null ? void 0 : _a.getOptionalBoolean(key); } getString(key) { return this.select(true).getString(key); } getOptionalString(key) { var _a; return (_a = this.select(false)) == null ? void 0 : _a.getOptionalString(key); } getStringArray(key) { return this.select(true).getStringArray(key); } getOptionalStringArray(key) { var _a; return (_a = this.select(false)) == null ? void 0 : _a.getOptionalStringArray(key); } } class ConfigSources { /** * Parses command line arguments and returns the config targets. * * @param argv - The command line arguments to parse. Defaults to `process.argv` * @returns A list of config targets */ static parseArgs(argv = process.argv) { const args = [parseArgs__default.default(argv).config].flat().filter(Boolean); return args.map((target) => { try { const url = new URL(target); if (!url.host) { return { type: "path", target }; } return { type: "url", target }; } catch { return { type: "path", target }; } }); } /** * Creates the default config sources for the provided targets. * * @remarks * * This will create {@link FileConfigSource}s and {@link RemoteConfigSource}s * for the provided targets, and merge them together to a single source. * If no targets are provided it will fall back to `app-config.yaml` and * `app-config.local.yaml`. * * URL targets are only supported if the `remote` option is provided. * * @param options - Options * @returns A config source for the provided targets */ static defaultForTargets(options) { var _a; const rootDir = (_a = options.rootDir) != null ? _a : cliCommon.findPaths(process.cwd()).targetRoot; const argSources = options.targets.map((arg) => { if (arg.type === "url") { if (!options.remote) { throw new Error( `Config argument "${arg.target}" looks like a URL but remote configuration is not enabled. Enable it by passing the \`remote\` option` ); } return RemoteConfigSource.create({ url: arg.target, substitutionFunc: options.substitutionFunc, reloadInterval: options.remote.reloadInterval }); } return FileConfigSource.create({ watch: options.watch, path: path.resolve(arg.target), substitutionFunc: options.substitutionFunc }); }); if (argSources.length === 0) { const defaultPath = path.resolve(rootDir, "app-config.yaml"); const localPath = path.resolve(rootDir, "app-config.local.yaml"); argSources.push( FileConfigSource.create({ watch: options.watch, path: defaultPath, substitutionFunc: options.substitutionFunc }) ); if (fs__default.default.pathExistsSync(localPath)) { argSources.push( FileConfigSource.create({ watch: options.watch, path: localPath, substitutionFunc: options.substitutionFunc }) ); } } return this.merge(argSources); } /** * Creates the default config source for Backstage. * * @remarks * * This will read from `app-config.yaml` and `app-config.local.yaml` by * default, as well as environment variables prefixed with `APP_CONFIG_`. * If `--config ` command line arguments are passed, these will * override the default configuration file paths. URLs are only supported * if the `remote` option is provided. * * @param options - Options * @returns The default Backstage config source */ static default(options) { const argSource = this.defaultForTargets({ ...options, targets: this.parseArgs(options.argv) }); const envSource = EnvConfigSource.create({ env: options.env }); return this.merge([argSource, envSource]); } /** * Merges multiple config sources into a single source that reads from all * sources and concatenates the result. * * @param sources - The config sources to merge * @returns A single config source that concatenates the data from the given sources */ static merge(sources) { return MergedConfigSource.from(sources); } /** * Creates an observable {@link @backstage/config#Config} implementation from a {@link ConfigSource}. * * @remarks * * If you only want to read the config once you can close the returned config immediately. * * @example * * ```ts * const sources = ConfigSources.default(...) * const config = await ConfigSources.toConfig(source) * config.close() * const example = config.getString(...) * ``` * * @param source - The config source to read from * @returns A promise that resolves to a closable config */ static toConfig(source) { return new Promise(async (resolve, reject) => { let config$1 = void 0; try { const abortController = new AbortController(); for await (const { configs } of source.readConfigData({ signal: abortController.signal })) { if (config$1) { config$1.setConfig(config.ConfigReader.fromConfigs(configs)); } else { config$1 = ObservableConfigProxy.create(abortController); config$1.setConfig(config.ConfigReader.fromConfigs(configs)); resolve(config$1); } } } catch (error) { reject(error); } }); } } function simpleDefer() { let resolve; const promise = new Promise((_resolve) => { resolve = _resolve; }); return { promise, resolve }; } async function waitOrAbort(promise, signal) { const signals = [signal].flat().filter((x) => !!x); return new Promise((resolve, reject) => { if (signals.some((s) => s.aborted)) { resolve([false]); } const onAbort = () => { resolve([false]); }; promise.then( (value) => { resolve([true, value]); signals.forEach((s) => s.removeEventListener("abort", onAbort)); }, (error) => { reject(error); signals.forEach((s) => s.removeEventListener("abort", onAbort)); } ); signals.forEach((s) => s.addEventListener("abort", onAbort)); }); } var __accessCheck = (obj, member, msg) => { if (!member.has(obj)) throw TypeError("Cannot " + msg); }; var __privateGet = (obj, member, getter) => { __accessCheck(obj, member, "read from private field"); return getter ? getter.call(obj) : member.get(obj); }; var __privateAdd = (obj, member, value) => { if (member.has(obj)) throw TypeError("Cannot add the same private member more than once"); member instanceof WeakSet ? member.add(obj) : member.set(obj, value); }; var __privateSet = (obj, member, value, setter) => { __accessCheck(obj, member, "write to private field"); setter ? setter.call(obj, value) : member.set(obj, value); return value; }; var _currentData, _deferred, _context, _abortController; const _MutableConfigSource = class _MutableConfigSource { constructor(context, initialData) { __privateAdd(this, _currentData, void 0); __privateAdd(this, _deferred, void 0); __privateAdd(this, _context, void 0); __privateAdd(this, _abortController, new AbortController()); __privateSet(this, _currentData, initialData); __privateSet(this, _context, context); __privateSet(this, _deferred, simpleDefer()); } /** * Creates a new mutable config source. * * @param options - Options for the config source. * @returns A new mutable config source. */ static create(options) { var _a; return new _MutableConfigSource( (_a = options == null ? void 0 : options.context) != null ? _a : "mutable-config", options == null ? void 0 : options.data ); } async *readConfigData(options) { let deferredPromise = __privateGet(this, _deferred).promise; if (__privateGet(this, _currentData) !== void 0) { yield { configs: [{ data: __privateGet(this, _currentData), context: __privateGet(this, _context) }] }; } for (; ; ) { const [ok] = await waitOrAbort(deferredPromise, [ options == null ? void 0 : options.signal, __privateGet(this, _abortController).signal ]); if (!ok) { return; } deferredPromise = __privateGet(this, _deferred).promise; if (__privateGet(this, _currentData) !== void 0) { yield { configs: [{ data: __privateGet(this, _currentData), context: __privateGet(this, _context) }] }; } } } /** * Set the data of the config source. * * @param data - The new data to set */ setData(data) { if (!__privateGet(this, _abortController).signal.aborted) { __privateSet(this, _currentData, data); const oldDeferred = __privateGet(this, _deferred); __privateSet(this, _deferred, simpleDefer()); oldDeferred.resolve(); } } /** * Close the config source, preventing any further updates. */ close() { __privateSet(this, _currentData, void 0); __privateGet(this, _abortController).abort(); } toString() { return `MutableConfigSource{}`; } }; _currentData = new WeakMap(); _deferred = new WeakMap(); _context = new WeakMap(); _abortController = new WeakMap(); let MutableConfigSource = _MutableConfigSource; class StaticObservableConfigSource { constructor(data, context) { this.data = data; this.context = context; } async *readConfigData(options) { const queue = new Array(); let deferred = simpleDefer(); const sub = this.data.subscribe({ next(value) { queue.push(value); deferred.resolve(); deferred = simpleDefer(); }, complete() { deferred.resolve(); } }); const signal = options == null ? void 0 : options.signal; if (signal) { const onAbort = () => { sub.unsubscribe(); queue.length = 0; deferred.resolve(); signal.removeEventListener("abort", onAbort); }; signal.addEventListener("abort", onAbort); } for (; ; ) { await deferred.promise; if (queue.length === 0) { return; } while (queue.length > 0) { yield { configs: [{ data: queue.shift(), context: this.context }] }; } } } } function isObservable(value) { return "subscribe" in value && typeof value.subscribe === "function"; } function isAsyncIterable(value) { return Symbol.asyncIterator in value; } class StaticConfigSource { constructor(promise, context) { this.promise = promise; this.context = context; } /** * Creates a new {@link StaticConfigSource}. * * @param options - Options for the config source * @returns A new static config source */ static create(options) { const { data, context = "static-config" } = options; if (!data) { return { async *readConfigData() { yield { configs: [] }; return; } }; } if (isObservable(data)) { return new StaticObservableConfigSource(data, context); } if (isAsyncIterable(data)) { return { async *readConfigData() { for await (const value of data) { yield { configs: [{ data: value, context }] }; } } }; } return new StaticConfigSource(data, context); } async *readConfigData() { yield { configs: [{ data: await this.promise, context: this.context }] }; return; } toString() { return `StaticConfigSource{}`; } } async function loadConfig(options) { const source = ConfigSources.default({ substitutionFunc: options.experimentalEnvFunc, remote: options.remote && { reloadInterval: { seconds: options.remote.reloadIntervalSeconds } }, watch: Boolean(options.watch), rootDir: options.configRoot, argv: options.configTargets.flatMap((t) => [ "--config", "url" in t ? t.url : t.path ]) }); return new Promise((resolve, reject) => { async function loadConfigReaderLoop() { var _a, _b, _c, _d; let loaded = false; try { const abortController = new AbortController(); (_b = (_a = options.watch) == null ? void 0 : _a.stopSignal) == null ? void 0 : _b.then(() => abortController.abort()); for await (const { configs } of source.readConfigData({ signal: abortController.signal })) { if (loaded) { (_c = options.watch) == null ? void 0 : _c.onChange(configs); } else { resolve({ appConfigs: configs }); loaded = true; if (options.watch) { (_d = options.watch.stopSignal) == null ? void 0 : _d.then(() => abortController.abort()); } else { abortController.abort(); } } } } catch (error) { if (loaded) { console.error(`Failed to reload configuration, ${error}`); } else { reject(error); } } } loadConfigReaderLoop(); }); } exports.ConfigSources = ConfigSources; exports.EnvConfigSource = EnvConfigSource; exports.FileConfigSource = FileConfigSource; exports.MutableConfigSource = MutableConfigSource; exports.RemoteConfigSource = RemoteConfigSource; exports.StaticConfigSource = StaticConfigSource; exports.loadConfig = loadConfig; exports.loadConfigSchema = loadConfigSchema; exports.mergeConfigSchemas = mergeConfigSchemas; exports.readEnvConfig = readEnvConfig; //# sourceMappingURL=index.cjs.js.map