diff --git a/packages/cli/src/commands/config/schema.ts b/packages/cli/src/commands/config/schema.ts index 1d6adf72d6..6badec6b6d 100644 --- a/packages/cli/src/commands/config/schema.ts +++ b/packages/cli/src/commands/config/schema.ts @@ -17,6 +17,7 @@ import { Command } from 'commander'; import { stringify as stringifyYaml } from 'yaml'; import { loadCliConfig } from '../../lib/config'; +import { mergeConfigSchemas } from '@backstage/config-loader'; export default async (cmd: Command) => { const { schema } = await loadCliConfig({ @@ -25,7 +26,7 @@ export default async (cmd: Command) => { mockEnv: true, }); - const data = schema.serialize(); + const { schema: data } = mergeConfigSchemas(schema.serialize().schemas); if (cmd.format === 'json') { process.stdout.write(`${JSON.stringify(data, null, 2)}\n`); diff --git a/packages/config-loader/src/index.ts b/packages/config-loader/src/index.ts index 9ad54c5f18..d9e5ae1350 100644 --- a/packages/config-loader/src/index.ts +++ b/packages/config-loader/src/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -export { readEnvConfig, loadConfigSchema } from './lib'; +export { readEnvConfig, loadConfigSchema, mergeConfigSchemas } from './lib'; export type { ConfigSchema, ConfigVisibility } from './lib'; export { loadConfig } from './loader'; export type { LoadConfigOptions } from './loader'; diff --git a/packages/config-loader/src/lib/schema/compile.ts b/packages/config-loader/src/lib/schema/compile.ts index e340d775a1..0cbd075893 100644 --- a/packages/config-loader/src/lib/schema/compile.ts +++ b/packages/config-loader/src/lib/schema/compile.ts @@ -35,6 +35,44 @@ import { export function compileConfigSchemas( schemas: ConfigSchemaPackageEntry[], ): ValidationFunc { + const { schema: merged, parser: ajv, visibilityByPath } = mergeConfigSchemas( + schemas, + ); + + const validate = ajv.compile(merged); + + return configs => { + const config = ConfigReader.fromConfigs(configs).get(); + + visibilityByPath.clear(); + + const valid = validate(config); + if (!valid) { + const errors = validate.errors ?? []; + return { + errors: errors.map(({ dataPath, message, params }) => { + const paramStr = Object.entries(params) + .map(([name, value]) => `${name}=${value}`) + .join(' '); + return `Config ${message || ''} { ${paramStr} } at ${dataPath}`; + }), + visibilityByPath: new Map(), + }; + } + + return { + visibilityByPath: new Map(visibilityByPath), + }; + }; +} + +/** + * Given a list of configuration schemas from packages, merge them + * into a single json schema. + */ +export function mergeConfigSchemas( + schemas: ConfigSchemaPackageEntry[], +): { schema: JSONSchema; parser: Ajv } { // The ajv instance below is stateful and doesn't really allow for additional // output during validation. We work around this by having this extra piece // of state that we reset before each validation. @@ -108,29 +146,9 @@ export function compileConfigSchemas( }, ); - const validate = ajv.compile(merged); - - return configs => { - const config = ConfigReader.fromConfigs(configs).get(); - - visibilityByPath.clear(); - - const valid = validate(config); - if (!valid) { - const errors = validate.errors ?? []; - return { - errors: errors.map(({ dataPath, message, params }) => { - const paramStr = Object.entries(params) - .map(([name, value]) => `${name}=${value}`) - .join(' '); - return `Config ${message || ''} { ${paramStr} } at ${dataPath}`; - }), - visibilityByPath: new Map(), - }; - } - - return { - visibilityByPath: new Map(visibilityByPath), - }; + return { + schema: merged, + parser: ajv, + visibilityByPath, }; } diff --git a/packages/config-loader/src/lib/schema/index.ts b/packages/config-loader/src/lib/schema/index.ts index 8cefb93b3c..00bb5c7d10 100644 --- a/packages/config-loader/src/lib/schema/index.ts +++ b/packages/config-loader/src/lib/schema/index.ts @@ -14,5 +14,6 @@ * limitations under the License. */ +export { mergeConfigSchemas } from './compile'; export { loadConfigSchema } from './load'; export type { ConfigSchema, ConfigVisibility } from './types';