config-loader: allow schemas to be collected by package path as well

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2021-09-25 15:59:32 +02:00
parent 2325563877
commit ee7a1a4b64
5 changed files with 105 additions and 46 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/config-loader': patch
---
Add option to collect configuration schemas from explicit package paths in addition to by package name.
+1
View File
@@ -53,6 +53,7 @@ export function loadConfigSchema(
export type LoadConfigSchemaOptions =
| {
dependencies: string[];
packagePaths?: string[];
}
| {
serialized: JsonObject;
@@ -49,7 +49,7 @@ describe('collectConfigSchemas', () => {
}),
});
await expect(collectConfigSchemas([])).resolves.toEqual([]);
await expect(collectConfigSchemas([], [])).resolves.toEqual([]);
});
it('should find schema in a local package', async () => {
@@ -64,7 +64,7 @@ describe('collectConfigSchemas', () => {
},
});
await expect(collectConfigSchemas(['a'])).resolves.toEqual([
await expect(collectConfigSchemas(['a'], [])).resolves.toEqual([
{
path: path.join('node_modules', 'a', 'package.json'),
value: mockSchema,
@@ -72,8 +72,34 @@ describe('collectConfigSchemas', () => {
]);
});
it('should find schema in transitive dependencies', async () => {
it('should find schema at explicit package path', async () => {
mockFs({
root: {
'package.json': JSON.stringify({
name: 'root',
configSchema: mockSchema,
}),
},
});
await expect(
collectConfigSchemas([], [path.join('root', 'package.json')]),
).resolves.toEqual([
{
path: path.join('root', 'package.json'),
value: mockSchema,
},
]);
});
it('should find schema in transitive dependencies and explicit path', async () => {
mockFs({
root: {
'package.json': JSON.stringify({
name: 'root',
configSchema: { ...mockSchema, title: 'root' },
}),
},
node_modules: {
a: {
'package.json': JSON.stringify({
@@ -124,20 +150,28 @@ describe('collectConfigSchemas', () => {
},
});
await expect(collectConfigSchemas(['a'])).resolves.toEqual([
{
path: path.join('node_modules', 'b', 'package.json'),
value: { ...mockSchema, title: 'b' },
},
{
path: path.join('node_modules', 'c1', 'package.json'),
value: { ...mockSchema, title: 'c1' },
},
{
path: path.join('node_modules', 'd1', 'package.json'),
value: { ...mockSchema, title: 'd1' },
},
]);
await expect(
collectConfigSchemas(['a'], [path.join('root', 'package.json')]),
).resolves.toEqual(
expect.arrayContaining([
{
path: path.join('node_modules', 'b', 'package.json'),
value: { ...mockSchema, title: 'b' },
},
{
path: path.join('node_modules', 'c1', 'package.json'),
value: { ...mockSchema, title: 'c1' },
},
{
path: path.join('node_modules', 'd1', 'package.json'),
value: { ...mockSchema, title: 'd1' },
},
{
path: path.join('root', 'package.json'),
value: { ...mockSchema, title: 'root' },
},
]),
);
});
it('should schema of different types', async () => {
@@ -171,7 +205,7 @@ describe('collectConfigSchemas', () => {
[typescriptModuleDir]: (mockFs as any).load(typescriptModuleDir),
});
await expect(collectConfigSchemas(['a', 'b', 'c'])).resolves.toEqual([
await expect(collectConfigSchemas(['a', 'b', 'c'], [])).resolves.toEqual([
{
path: path.join('node_modules', 'a', 'package.json'),
value: { ...mockSchema, title: 'inline' },
@@ -210,7 +244,7 @@ describe('collectConfigSchemas', () => {
},
});
await expect(collectConfigSchemas(['a'])).rejects.toThrow(
await expect(collectConfigSchemas(['a'], [])).rejects.toThrow(
'Config schema files must be .json or .d.ts, got schema.yaml',
);
});
@@ -230,7 +264,7 @@ describe('collectConfigSchemas', () => {
[typescriptModuleDir]: (mockFs as any).load(typescriptModuleDir),
});
await expect(collectConfigSchemas(['a'])).rejects.toThrow(
await expect(collectConfigSchemas(['a'], [])).rejects.toThrow(
`Invalid schema in ${path.join(
'node_modules',
'a',
@@ -26,8 +26,9 @@ import { getProgramFromFiles, generateSchema } from 'typescript-json-schema';
import { JsonObject } from '@backstage/config';
type Item = {
name: string;
name?: string;
parentPath?: string;
packagePath?: string;
};
const req =
@@ -40,34 +41,47 @@ const req =
*/
export async function collectConfigSchemas(
packageNames: string[],
packagePaths: string[],
): Promise<ConfigSchemaPackageEntry[]> {
const visitedPackages = new Set<string>();
const schemas = Array<ConfigSchemaPackageEntry>();
const tsSchemaPaths = Array<string>();
const currentDir = await fs.realpath(process.cwd());
async function processItem({ 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);
async function processItem(item: Item) {
let pkgPath = item.packagePath;
let pkgPath: string;
try {
pkgPath = req.resolve(
`${name}/package.json`,
parentPath && {
paths: [parentPath],
},
);
} catch {
// We can somewhat safely ignore packages that don't export package.json,
// as they are likely not part of the Backstage ecosystem anyway.
if (pkgPath) {
const pkgExists = await fs.pathExists(pkgPath);
if (!pkgExists) {
return;
}
} else if (item.name) {
const { 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);
try {
pkgPath = req.resolve(
`${name}/package.json`,
parentPath && {
paths: [parentPath],
},
);
} catch {
// We can somewhat safely ignore packages that don't export package.json,
// as they are likely not part of the Backstage ecosystem anyway.
}
}
if (!pkgPath) {
return;
}
@@ -126,9 +140,10 @@ export async function collectConfigSchemas(
);
}
await Promise.all(
packageNames.map(name => processItem({ name, parentPath: currentDir })),
);
await Promise.all([
...packageNames.map(name => processItem({ name, parentPath: currentDir })),
...packagePaths.map(path => processItem({ name: path, packagePath: path })),
]);
const tsSchemas = compileTsSchemas(tsSchemaPaths);
@@ -28,6 +28,7 @@ import {
export type LoadConfigSchemaOptions =
| {
dependencies: string[];
packagePaths?: string[];
}
| {
serialized: JsonObject;
@@ -44,7 +45,10 @@ export async function loadConfigSchema(
let schemas: ConfigSchemaPackageEntry[];
if ('dependencies' in options) {
schemas = await collectConfigSchemas(options.dependencies);
schemas = await collectConfigSchemas(
options.dependencies,
options.packagePaths ?? [],
);
} else {
const { serialized } = options;
if (serialized?.backstageConfigSchemaVersion !== 1) {