config-loader: consider all deps when discovering schema and parallelize processing

This commit is contained in:
Patrik Oldsberg
2020-11-08 15:22:28 +01:00
parent 4ccba1aa96
commit f4af5d1dce
2 changed files with 66 additions and 45 deletions
@@ -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<string> = new Set(),
schemas: ConfigSchemaPackageEntry[] = [],
packageNames: string[],
): Promise<ConfigSchemaPackageEntry[]> {
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<string>();
const schemas = Array<ConfigSchemaPackageEntry>();
// 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;
}
@@ -30,7 +30,7 @@ type Options = {
export async function loadConfigSchema(
options: Options,
): Promise<ConfigSchema> {
const schemas = await collectConfigSchemas(options.dependencies[0]);
const schemas = await collectConfigSchemas(options.dependencies);
const validate = compileConfigSchemas(schemas);