From ee7a1a4b64820f1793e6c0710cb2adf844ace0f2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 25 Sep 2021 15:59:32 +0200 Subject: [PATCH] config-loader: allow schemas to be collected by package path as well Signed-off-by: Patrik Oldsberg --- .changeset/late-kiwis-double.md | 5 ++ packages/config-loader/api-report.md | 1 + .../src/lib/schema/collect.test.ts | 74 ++++++++++++++----- .../config-loader/src/lib/schema/collect.ts | 65 +++++++++------- packages/config-loader/src/lib/schema/load.ts | 6 +- 5 files changed, 105 insertions(+), 46 deletions(-) create mode 100644 .changeset/late-kiwis-double.md diff --git a/.changeset/late-kiwis-double.md b/.changeset/late-kiwis-double.md new file mode 100644 index 0000000000..b1ec53546e --- /dev/null +++ b/.changeset/late-kiwis-double.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': patch +--- + +Add option to collect configuration schemas from explicit package paths in addition to by package name. diff --git a/packages/config-loader/api-report.md b/packages/config-loader/api-report.md index bb3ae50d23..355839c5af 100644 --- a/packages/config-loader/api-report.md +++ b/packages/config-loader/api-report.md @@ -53,6 +53,7 @@ export function loadConfigSchema( export type LoadConfigSchemaOptions = | { dependencies: string[]; + packagePaths?: string[]; } | { serialized: JsonObject; diff --git a/packages/config-loader/src/lib/schema/collect.test.ts b/packages/config-loader/src/lib/schema/collect.test.ts index ef9d38faa9..3600991717 100644 --- a/packages/config-loader/src/lib/schema/collect.test.ts +++ b/packages/config-loader/src/lib/schema/collect.test.ts @@ -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', diff --git a/packages/config-loader/src/lib/schema/collect.ts b/packages/config-loader/src/lib/schema/collect.ts index b363e8e5f3..81a28e9d7b 100644 --- a/packages/config-loader/src/lib/schema/collect.ts +++ b/packages/config-loader/src/lib/schema/collect.ts @@ -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 { const visitedPackages = new Set(); const schemas = Array(); const tsSchemaPaths = Array(); 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); diff --git a/packages/config-loader/src/lib/schema/load.ts b/packages/config-loader/src/lib/schema/load.ts index 4620961310..2d4af3454c 100644 --- a/packages/config-loader/src/lib/schema/load.ts +++ b/packages/config-loader/src/lib/schema/load.ts @@ -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) {