Merge pull request #7326 from backstage/rugvip/root

cli,config-loader: allow config schema to be defined in root package.json
This commit is contained in:
Patrik Oldsberg
2021-09-27 22:50:50 +02:00
committed by GitHub
8 changed files with 116 additions and 49 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Configuration schema is now also collected from the root `package.json` if it exists.
+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.
+4 -3
View File
@@ -13,9 +13,10 @@ which during validation is stitched together into a single schema.
## Schema Collection and Definition
Schemas are collected from all packages and dependencies in each repo that are a
part of the Backstage ecosystem, including transitive dependencies. The current
definition of "part of the ecosystem" is that a package has at least one
dependency in the `@backstage` namespace, but this is subject to change.
part of the Backstage ecosystem, including the root package and transitive
dependencies. The current definition of "part of the ecosystem" is that a
package has at least one dependency in the `@backstage` namespace or a
`"configSchema"` field in `package.json`, but this is subject to change.
Each package is searched for a schema at a single point of entry, a top-level
`"configSchema"` field in `package.json`. The field can either contain an
+2
View File
@@ -39,6 +39,8 @@ export async function loadCliConfig(options: Options) {
const schema = await loadConfigSchema({
dependencies: localPackageNames,
// Include the package.json in the project root if it exists
packagePaths: [paths.resolveTargetRoot('package.json')],
});
const appConfigs = await loadConfig({
+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) {