From f4af5d1dced13c29bf6fdb07fd024979453920d7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 8 Nov 2020 15:22:28 +0100 Subject: [PATCH] config-loader: consider all deps when discovering schema and parallelize processing --- .../config-loader/src/lib/schema/collect.ts | 109 +++++++++++------- packages/config-loader/src/lib/schema/load.ts | 2 +- 2 files changed, 66 insertions(+), 45 deletions(-) diff --git a/packages/config-loader/src/lib/schema/collect.ts b/packages/config-loader/src/lib/schema/collect.ts index 25bdbe7f55..6e3755926c 100644 --- a/packages/config-loader/src/lib/schema/collect.ts +++ b/packages/config-loader/src/lib/schema/collect.ts @@ -18,6 +18,11 @@ import fs from 'fs-extra'; import { resolve as resolvePath, dirname } from 'path'; import { ConfigSchemaPackageEntry } from './types'; +type Item = { + name: string; + parentPath?: string; +}; + const req = typeof __non_webpack_require__ === 'undefined' ? require @@ -27,55 +32,71 @@ const req = * This collects all known config schemas across all dependencies of the app. */ export async function collectConfigSchemas( - name: string, - opts: unknown = {}, - visited: Set = new Set(), - schemas: ConfigSchemaPackageEntry[] = [], + packageNames: string[], ): Promise { - const pkgPath = req.resolve(`${name}/package.json`, opts); - if (visited.has(pkgPath)) { - return schemas; - } - visited.add(pkgPath); - const pkg = await fs.readJson(pkgPath); - const depNames = [ - ...Object.keys(pkg.dependencies ?? {}), - ...Object.keys(pkg.peerDependencies ?? {}), - ]; + const visitedPackages = new Set(); + const schemas = Array(); - // TODO(Rugvip): Trying this out to avoid having to traverse the full dependency graph, - // since that's pretty slow. We probably need a better way to determine when - // we've left the Backstage ecosystem, but this will do for now. - const hasSchema = 'configSchema' in pkg; - const hasBackstageDep = depNames.some(_ => _.startsWith('@backstage/')); - if (!hasSchema && !hasBackstageDep) { - return schemas; - } - if (hasSchema) { - if (typeof pkg.configSchema === 'string') { - if (!pkg.configSchema.endsWith('.json')) { - throw new Error( - `Config schema files must be .json, got ${pkg.configSchema}`, - ); - } - const value = await fs.readJson( - resolvePath(dirname(pkgPath), pkg.configSchema), - ); - schemas.push({ - value, - path: pkgPath, - }); - } else { - schemas.push({ - value: pkg.configSchema, - path: pkgPath, - }); + async function process({ name, parentPath }: Item) { + // Ensures that we only process each package once. We don't bother with + // loading in schemas from duplicates of different versions, as that's not + // supported by Backstage right now anyway. We may want to change that in + // the future though, if it for example becomes possible to load in two + // different versions of e.g. @backstage/core at once. + if (visitedPackages.has(name)) { + return; } + visitedPackages.add(name); + + const pkgPath = req.resolve( + `${name}/package.json`, + parentPath && { + paths: [parentPath], + }, + ); + + const pkg = await fs.readJson(pkgPath); + const depNames = [ + ...Object.keys(pkg.dependencies ?? {}), + ...Object.keys(pkg.peerDependencies ?? {}), + ]; + + // TODO(Rugvip): Trying this out to avoid having to traverse the full dependency graph, + // since that's pretty slow. We probably need a better way to determine when + // we've left the Backstage ecosystem, but this will do for now. + const hasSchema = 'configSchema' in pkg; + const hasBackstageDep = depNames.some(_ => _.startsWith('@backstage/')); + if (!hasSchema && !hasBackstageDep) { + return; + } + if (hasSchema) { + if (typeof pkg.configSchema === 'string') { + if (!pkg.configSchema.endsWith('.json')) { + throw new Error( + `Config schema files must be .json, got ${pkg.configSchema}`, + ); + } + const value = await fs.readJson( + resolvePath(dirname(pkgPath), pkg.configSchema), + ); + schemas.push({ + value, + path: pkgPath, + }); + } else { + schemas.push({ + value: pkg.configSchema, + path: pkgPath, + }); + } + } + + await Promise.all( + depNames.map(name => process({ name, parentPath: pkgPath })), + ); } - for (const depName of depNames) { - await collectConfigSchemas(depName, { paths: [pkgPath] }, visited, schemas); - } + await Promise.all(packageNames.map(name => process({ name }))); return schemas; } diff --git a/packages/config-loader/src/lib/schema/load.ts b/packages/config-loader/src/lib/schema/load.ts index 46e1358ba9..6352da65af 100644 --- a/packages/config-loader/src/lib/schema/load.ts +++ b/packages/config-loader/src/lib/schema/load.ts @@ -30,7 +30,7 @@ type Options = { export async function loadConfigSchema( options: Options, ): Promise { - const schemas = await collectConfigSchemas(options.dependencies[0]); + const schemas = await collectConfigSchemas(options.dependencies); const validate = compileConfigSchemas(schemas);