diff --git a/.changeset/loud-walls-collect.md b/.changeset/loud-walls-collect.md new file mode 100644 index 0000000000..78356eef38 --- /dev/null +++ b/.changeset/loud-walls-collect.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Fixed module resolution of external libraries during backend development. Modules used to be resolved relative to the backend entrypoint, but are now resolved relative to each individual module. diff --git a/.changeset/neat-brooms-allow.md b/.changeset/neat-brooms-allow.md new file mode 100644 index 0000000000..a4c69b0945 --- /dev/null +++ b/.changeset/neat-brooms-allow.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': patch +--- + +Each piece of the configuration schema is now validated upfront, in order to produce more informative errors. diff --git a/.changeset/nice-bottles-battle.md b/.changeset/nice-bottles-battle.md new file mode 100644 index 0000000000..a688241caa --- /dev/null +++ b/.changeset/nice-bottles-battle.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-app-backend': patch +--- + +Failures to load the frontend configuration schema now throws an error that includes more context and instructions for how to fix the issue. diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index c53a8329f5..6fbcc65c9a 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -219,7 +219,7 @@ export async function createBackendConfig( } : {}), externals: [ - nodeExternals({ + nodeExternalsWithResolve({ modulesDir: paths.rootNodeModules, additionalModuleDirs: moduleDirs, allowlist: ['webpack/hot/poll?100', ...localPackageNames], @@ -296,3 +296,32 @@ export async function createBackendConfig( ], }; } + +// This makes the module resolution happen from the context of each non-external module, rather +// than the main entrypoint. This fixes a bug where dependencies would be resolved from the backend +// package rather than each individual backend package and plugin. +// +// TODO(Rugvip): Feature suggestion/contribute this to webpack-externals +function nodeExternalsWithResolve( + options: Parameters[0], +) { + let currentContext: string; + const externals = nodeExternals({ + ...options, + importType(request) { + const resolved = require.resolve(request, { + paths: [currentContext], + }); + return `commonjs ${resolved}`; + }, + }); + + return ( + context: string, + request: string, + callback: webpack.ExternalsFunctionCallback, + ) => { + currentContext = context; + return externals(context, request, callback); + }; +} diff --git a/packages/config-loader/src/lib/schema/compile.ts b/packages/config-loader/src/lib/schema/compile.ts index 2607d51f80..e340d775a1 100644 --- a/packages/config-loader/src/lib/schema/compile.ts +++ b/packages/config-loader/src/lib/schema/compile.ts @@ -69,6 +69,14 @@ export function compileConfigSchemas( }, }); + for (const schema of schemas) { + try { + ajv.compile(schema.value); + } catch (error) { + throw new Error(`Schema at ${schema.path} is invalid, ${error}`); + } + } + const merged = mergeAllOf( { allOf: schemas.map(_ => _.value) }, { diff --git a/plugins/app-backend/src/lib/config.ts b/plugins/app-backend/src/lib/config.ts index 99590c1e2d..0607956894 100644 --- a/plugins/app-backend/src/lib/config.ts +++ b/plugins/app-backend/src/lib/config.ts @@ -84,13 +84,21 @@ export async function readConfigs(options: ReadOptions): Promise { const schemaPath = resolvePath(appDistDir, '.config-schema.json'); if (await fs.pathExists(schemaPath)) { const serializedSchema = await fs.readJson(schemaPath); - const schema = await loadConfigSchema({ serialized: serializedSchema }); - const frontendConfigs = await schema.process( - [{ data: config.get() as JsonObject, context: 'app' }], - { visibility: ['frontend'] }, - ); - appConfigs.push(...frontendConfigs); + try { + const schema = await loadConfigSchema({ serialized: serializedSchema }); + + const frontendConfigs = await schema.process( + [{ data: config.get() as JsonObject, context: 'app' }], + { visibility: ['frontend'] }, + ); + appConfigs.push(...frontendConfigs); + } catch (error) { + throw new Error( + 'Invalid schema embedded in the app bundle, to fix this issue you need ' + + `to correct the schema and then rebuild the app bundle. ${error}`, + ); + } } return appConfigs;