From 3595c974f68910a3a81322ff9c9b3adf9ebd2fc5 Mon Sep 17 00:00:00 2001 From: Jack Palmer Date: Tue, 7 Apr 2026 09:34:54 +0100 Subject: [PATCH 1/4] feat(backend): add extensionPointFactoryMiddleware to createBackend Allow the backend to reimplement extension point outputs at creation time via a new extensionPointFactoryMiddleware option on createBackend(). Each middleware entry declaratively targets a specific extension point by reference and the framework handles matching and pass-through automatically. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Jack Palmer --- ...ension-point-middleware-backend-app-api.md | 5 + ...nsion-point-middleware-backend-defaults.md | 5 + packages/backend-app-api/report.api.md | 17 ++ .../src/wiring/BackendInitializer.test.ts | 203 ++++++++++++++++++ .../src/wiring/BackendInitializer.ts | 36 ++-- .../src/wiring/BackstageBackend.ts | 16 +- .../src/wiring/createSpecializedBackend.ts | 5 +- packages/backend-app-api/src/wiring/index.ts | 2 + packages/backend-app-api/src/wiring/types.ts | 29 +++ packages/backend-defaults/report.api.md | 10 +- .../backend-defaults/src/CreateBackend.ts | 15 +- packages/backend-defaults/src/index.ts | 2 + 12 files changed, 323 insertions(+), 22 deletions(-) create mode 100644 .changeset/extension-point-middleware-backend-app-api.md create mode 100644 .changeset/extension-point-middleware-backend-defaults.md diff --git a/.changeset/extension-point-middleware-backend-app-api.md b/.changeset/extension-point-middleware-backend-app-api.md new file mode 100644 index 0000000000..2d0ea0dfbc --- /dev/null +++ b/.changeset/extension-point-middleware-backend-app-api.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': minor +--- + +Added `ExtensionPointFactoryMiddleware` type and `createExtensionPointFactoryMiddleware` helper to reimplement extension point outputs at backend creation time. diff --git a/.changeset/extension-point-middleware-backend-defaults.md b/.changeset/extension-point-middleware-backend-defaults.md new file mode 100644 index 0000000000..481ba41bed --- /dev/null +++ b/.changeset/extension-point-middleware-backend-defaults.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Added `extensionPointFactoryMiddleware` option to `createBackend()` to reimplement extension point outputs at backend initialization time. Also re-exports `ExtensionPointFactoryMiddleware` type and `createExtensionPointFactoryMiddleware` helper from `@backstage/backend-app-api`. diff --git a/packages/backend-app-api/report.api.md b/packages/backend-app-api/report.api.md index 62b46b93e6..e0557b8fd0 100644 --- a/packages/backend-app-api/report.api.md +++ b/packages/backend-app-api/report.api.md @@ -5,6 +5,7 @@ ```ts import { BackendFeature } from '@backstage/backend-plugin-api'; import { CustomErrorBase } from '@backstage/errors'; +import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { ServiceFactory } from '@backstage/backend-plugin-api'; // @public (undocumented) @@ -42,6 +43,12 @@ export interface BackendStartupResult { resultAt: Date; } +// @public +export function createExtensionPointFactoryMiddleware( + extensionPoint: ExtensionPoint, + middleware: (original: T) => T, +): ExtensionPointFactoryMiddleware; + // @public (undocumented) export function createSpecializedBackend( options: CreateSpecializedBackendOptions, @@ -51,6 +58,16 @@ export function createSpecializedBackend( export interface CreateSpecializedBackendOptions { // (undocumented) defaultServiceFactories: ServiceFactory[]; + // (undocumented) + extensionPointFactoryMiddleware?: ExtensionPointFactoryMiddleware[]; +} + +// @public +export interface ExtensionPointFactoryMiddleware { + // (undocumented) + extensionPoint: ExtensionPoint; + // (undocumented) + middleware: (original: T) => T; } // @public diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts index f782295e71..c1801f0398 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts @@ -28,6 +28,7 @@ import { import { BackendInitializer } from './BackendInitializer'; import { mockServices } from '@backstage/backend-test-utils'; import { BackendStartupError } from './BackendStartupError'; +import { createExtensionPointFactoryMiddleware } from './types'; const baseFactories = [ mockServices.rootLifecycle.factory(), @@ -2111,4 +2112,206 @@ describe('BackendInitializer', () => { ); }); }); + + describe('extensionPointFactoryMiddleware', () => { + it('should apply middleware to matching extension points', async () => { + expect.assertions(1); + + const extensionPoint = createExtensionPoint<{ values: string[] }>({ + id: 'test.ext', + }); + + const init = new BackendInitializer(baseFactories, [ + createExtensionPointFactoryMiddleware(extensionPoint, original => ({ + ...original, + values: [...original.values, 'from-middleware'], + })), + ]); + + init.add(testPlugin); + init.add( + createBackendModule({ + pluginId: 'test', + moduleId: 'provider', + register(reg) { + reg.registerExtensionPoint(extensionPoint, { + values: ['original'], + }); + reg.registerInit({ deps: {}, async init() {} }); + }, + }), + ); + init.add( + createBackendModule({ + pluginId: 'test', + moduleId: 'consumer', + register(reg) { + reg.registerInit({ + deps: { ext: extensionPoint }, + async init({ ext }) { + expect(ext.values).toEqual(['original', 'from-middleware']); + }, + }); + }, + }), + ); + + await init.start(); + }); + + it('should not affect non-matching extension points', async () => { + expect.assertions(1); + + const extensionPointA = createExtensionPoint<{ values: string[] }>({ + id: 'test.a', + }); + const extensionPointB = createExtensionPoint<{ values: string[] }>({ + id: 'test.b', + }); + + const init = new BackendInitializer(baseFactories, [ + createExtensionPointFactoryMiddleware(extensionPointA, original => ({ + ...original, + values: [...original.values, 'wrapped'], + })), + ]); + + init.add(testPlugin); + init.add( + createBackendModule({ + pluginId: 'test', + moduleId: 'provider', + register(reg) { + reg.registerExtensionPoint(extensionPointB, { + values: ['untouched'], + }); + reg.registerInit({ deps: {}, async init() {} }); + }, + }), + ); + init.add( + createBackendModule({ + pluginId: 'test', + moduleId: 'consumer', + register(reg) { + reg.registerInit({ + deps: { ext: extensionPointB }, + async init({ ext }) { + expect(ext.values).toEqual(['untouched']); + }, + }); + }, + }), + ); + + await init.start(); + }); + + it('should chain multiple middlewares for the same extension point', async () => { + expect.assertions(1); + + const extensionPoint = createExtensionPoint<{ values: string[] }>({ + id: 'test.ext', + }); + + const init = new BackendInitializer(baseFactories, [ + createExtensionPointFactoryMiddleware(extensionPoint, original => ({ + ...original, + values: [...original.values, 'first'], + })), + createExtensionPointFactoryMiddleware(extensionPoint, original => ({ + ...original, + values: [...original.values, 'second'], + })), + ]); + + init.add(testPlugin); + init.add( + createBackendModule({ + pluginId: 'test', + moduleId: 'provider', + register(reg) { + reg.registerExtensionPoint(extensionPoint, { values: ['base'] }); + reg.registerInit({ deps: {}, async init() {} }); + }, + }), + ); + init.add( + createBackendModule({ + pluginId: 'test', + moduleId: 'consumer', + register(reg) { + reg.registerInit({ + deps: { ext: extensionPoint }, + async init({ ext }) { + expect(ext.values).toEqual(['base', 'first', 'second']); + }, + }); + }, + }), + ); + + await init.start(); + }); + + it('should not fail when middleware targets an unregistered extension point', async () => { + const unregisteredExtensionPoint = createExtensionPoint<{ + values: string[]; + }>({ + id: 'test.unregistered', + }); + + const init = new BackendInitializer(baseFactories, [ + createExtensionPointFactoryMiddleware( + unregisteredExtensionPoint, + original => ({ + ...original, + values: [...original.values, 'never-applied'], + }), + ), + ]); + + init.add(testPlugin); + const { result } = await init.start(); + expect(result.outcome).toBe('success'); + }); + + it('should pass through when no middleware is provided', async () => { + expect.assertions(1); + + const extensionPoint = createExtensionPoint<{ values: string[] }>({ + id: 'test.ext', + }); + + const init = new BackendInitializer(baseFactories); + + init.add(testPlugin); + init.add( + createBackendModule({ + pluginId: 'test', + moduleId: 'provider', + register(reg) { + reg.registerExtensionPoint(extensionPoint, { values: ['orig'] }); + reg.registerInit({ deps: {}, async init() {} }); + }, + }), + ); + init.add( + createBackendModule({ + pluginId: 'test', + moduleId: 'consumer', + register(reg) { + reg.registerInit({ + deps: { ext: extensionPoint }, + async init({ ext }) { + expect(ext.values).toEqual(['orig']); + }, + }); + }, + }), + ); + + await init.start(); + }); + }); }); diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index 8ebab723da..211fb7dccc 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -25,7 +25,10 @@ import { createServiceFactory, ExtensionPointFactoryContext, } from '@backstage/backend-plugin-api'; -import { ServiceOrExtensionPoint } from './types'; +import { + ExtensionPointFactoryMiddleware, + ServiceOrExtensionPoint, +} from './types'; // Direct internal import to avoid duplication // eslint-disable-next-line @backstage/no-relative-monorepo-imports import type { @@ -166,11 +169,17 @@ export class BackendInitializer { #serviceRegistry: ServiceRegistry; #registeredFeatures = new Array>(); #registeredFeatureLoaders = new Array(); + #extensionPointFactoryMiddleware: ExtensionPointFactoryMiddleware[]; #unhandledRejectionHandler?: (reason: Error) => void; #uncaughtExceptionHandler?: (error: Error) => void; - constructor(defaultApiFactories: ServiceFactory[]) { + constructor( + defaultApiFactories: ServiceFactory[], + extensionPointFactoryMiddleware?: ExtensionPointFactoryMiddleware[], + ) { this.#serviceRegistry = ServiceRegistry.create([...defaultApiFactories]); + this.#extensionPointFactoryMiddleware = + extensionPointFactoryMiddleware ?? []; } async #getInitDeps( @@ -195,18 +204,17 @@ export class BackendInitializer { `Rejected dependency on extension point ${ref.id} from outside of a module`, ); } - result.set( - name, - ep.factory({ - reportModuleStartupFailure: ({ error }) => { - resultCollector.amendPluginModuleResult( - pluginId, - moduleId, - error, - ); - }, - }), - ); + let epImpl = ep.factory({ + reportModuleStartupFailure: ({ error }) => { + resultCollector.amendPluginModuleResult(pluginId, moduleId, error); + }, + }); + for (const mw of this.#extensionPointFactoryMiddleware) { + if (mw.extensionPoint.id === ref.id) { + epImpl = (mw.middleware as (original: unknown) => unknown)(epImpl); + } + } + result.set(name, epImpl); } else { const impl = await this.#serviceRegistry.get( ref as ServiceRef, diff --git a/packages/backend-app-api/src/wiring/BackstageBackend.ts b/packages/backend-app-api/src/wiring/BackstageBackend.ts index 79ee55780f..a38d1991cd 100644 --- a/packages/backend-app-api/src/wiring/BackstageBackend.ts +++ b/packages/backend-app-api/src/wiring/BackstageBackend.ts @@ -17,13 +17,23 @@ import { BackendFeature, ServiceFactory } from '@backstage/backend-plugin-api'; import { BackendInitializer } from './BackendInitializer'; import { unwrapFeature } from './helpers'; -import { Backend, BackendStartupResult } from './types'; +import { + Backend, + BackendStartupResult, + ExtensionPointFactoryMiddleware, +} from './types'; export class BackstageBackend implements Backend { #initializer: BackendInitializer; - constructor(defaultServiceFactories: ServiceFactory[]) { - this.#initializer = new BackendInitializer(defaultServiceFactories); + constructor( + defaultServiceFactories: ServiceFactory[], + extensionPointFactoryMiddleware?: ExtensionPointFactoryMiddleware[], + ) { + this.#initializer = new BackendInitializer( + defaultServiceFactories, + extensionPointFactoryMiddleware, + ); } add(feature: BackendFeature | Promise<{ default: BackendFeature }>): void { diff --git a/packages/backend-app-api/src/wiring/createSpecializedBackend.ts b/packages/backend-app-api/src/wiring/createSpecializedBackend.ts index fd449be541..d1a88a21e4 100644 --- a/packages/backend-app-api/src/wiring/createSpecializedBackend.ts +++ b/packages/backend-app-api/src/wiring/createSpecializedBackend.ts @@ -43,5 +43,8 @@ export function createSpecializedBackend( ); } - return new BackstageBackend(options.defaultServiceFactories); + return new BackstageBackend( + options.defaultServiceFactories, + options.extensionPointFactoryMiddleware, + ); } diff --git a/packages/backend-app-api/src/wiring/index.ts b/packages/backend-app-api/src/wiring/index.ts index cc084e5f34..2ddd70e878 100644 --- a/packages/backend-app-api/src/wiring/index.ts +++ b/packages/backend-app-api/src/wiring/index.ts @@ -17,9 +17,11 @@ export type { Backend, CreateSpecializedBackendOptions, + ExtensionPointFactoryMiddleware, BackendStartupResult, PluginStartupResult, ModuleStartupResult, } from './types'; +export { createExtensionPointFactoryMiddleware } from './types'; export { createSpecializedBackend } from './createSpecializedBackend'; export { BackendStartupError } from './BackendStartupError'; diff --git a/packages/backend-app-api/src/wiring/types.ts b/packages/backend-app-api/src/wiring/types.ts index 5782021656..766d737dea 100644 --- a/packages/backend-app-api/src/wiring/types.ts +++ b/packages/backend-app-api/src/wiring/types.ts @@ -21,6 +21,34 @@ import { ServiceFactory, } from '@backstage/backend-plugin-api'; +/** + * A middleware entry that reimplements a specific extension point's output. + * The framework matches by extension point ID and passes through all + * non-matching extension points automatically. + * + * @public + */ +export interface ExtensionPointFactoryMiddleware { + extensionPoint: ExtensionPoint; + middleware: (original: T) => T; +} + +/** + * Creates a typed middleware entry that reimplements a specific extension point. + * Use this helper to preserve type inference for the middleware callback. + * + * @public + */ +export function createExtensionPointFactoryMiddleware( + extensionPoint: ExtensionPoint, + middleware: (original: T) => T, +): ExtensionPointFactoryMiddleware { + return { + extensionPoint: extensionPoint as ExtensionPoint, + middleware: middleware as (original: unknown) => unknown, + }; +} + /** * @public */ @@ -35,6 +63,7 @@ export interface Backend { */ export interface CreateSpecializedBackendOptions { defaultServiceFactories: ServiceFactory[]; + extensionPointFactoryMiddleware?: ExtensionPointFactoryMiddleware[]; } /** diff --git a/packages/backend-defaults/report.api.md b/packages/backend-defaults/report.api.md index def38beda7..ffc67e5473 100644 --- a/packages/backend-defaults/report.api.md +++ b/packages/backend-defaults/report.api.md @@ -5,10 +5,18 @@ ```ts import { Backend } from '@backstage/backend-app-api'; import { BackendFeature } from '@backstage/backend-plugin-api'; +import { createExtensionPointFactoryMiddleware } from '@backstage/backend-app-api'; +import { ExtensionPointFactoryMiddleware } from '@backstage/backend-app-api'; // @public (undocumented) -export function createBackend(): Backend; +export function createBackend(options?: { + extensionPointFactoryMiddleware?: ExtensionPointFactoryMiddleware[]; +}): Backend; + +export { createExtensionPointFactoryMiddleware }; // @public export const discoveryFeatureLoader: BackendFeature; + +export { ExtensionPointFactoryMiddleware }; ``` diff --git a/packages/backend-defaults/src/CreateBackend.ts b/packages/backend-defaults/src/CreateBackend.ts index 2b4ee5befa..e6976f6500 100644 --- a/packages/backend-defaults/src/CreateBackend.ts +++ b/packages/backend-defaults/src/CreateBackend.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { Backend, createSpecializedBackend } from '@backstage/backend-app-api'; +import { + Backend, + createSpecializedBackend, + ExtensionPointFactoryMiddleware, +} from '@backstage/backend-app-api'; import { auditorServiceFactory } from '@backstage/backend-defaults/auditor'; import { authServiceFactory } from '@backstage/backend-defaults/auth'; import { cacheServiceFactory } from '@backstage/backend-defaults/cache'; @@ -76,6 +80,11 @@ export const defaultServiceFactories = [ /** * @public */ -export function createBackend(): Backend { - return createSpecializedBackend({ defaultServiceFactories }); +export function createBackend(options?: { + extensionPointFactoryMiddleware?: ExtensionPointFactoryMiddleware[]; +}): Backend { + return createSpecializedBackend({ + defaultServiceFactories, + extensionPointFactoryMiddleware: options?.extensionPointFactoryMiddleware, + }); } diff --git a/packages/backend-defaults/src/index.ts b/packages/backend-defaults/src/index.ts index 5f26df8921..eb3579c88a 100644 --- a/packages/backend-defaults/src/index.ts +++ b/packages/backend-defaults/src/index.ts @@ -21,4 +21,6 @@ */ export { createBackend } from './CreateBackend'; +export type { ExtensionPointFactoryMiddleware } from '@backstage/backend-app-api'; +export { createExtensionPointFactoryMiddleware } from '@backstage/backend-app-api'; export { discoveryFeatureLoader } from './discoveryFeatureLoader'; From e782afcb3b9fa51ce0d245f30287841499b524d7 Mon Sep 17 00:00:00 2001 From: Jack Palmer Date: Thu, 9 Apr 2026 15:59:44 +0100 Subject: [PATCH 2/4] Address PR review comments - Make ExtensionPointFactoryMiddleware an opaque type via new @internal/backend package using OpaqueType from @internal/opaque - Use options object for createExtensionPointFactoryMiddleware - Make middleware function async (returns Promise) - Remove extensionPointFactoryMiddleware from createBackend, keep only on createSpecializedBackend - Export defaultServiceFactories from @backstage/backend-defaults Signed-off-by: Jack Palmer --- ...nsion-point-middleware-backend-defaults.md | 2 +- packages/backend-app-api/report.api.md | 14 +++-- .../src/wiring/BackendInitializer.test.ts | 52 ++++++++++++------- .../src/wiring/BackendInitializer.ts | 6 ++- packages/backend-app-api/src/wiring/types.ts | 22 ++++---- packages/backend-defaults/report.api.md | 12 ++--- .../backend-defaults/src/CreateBackend.ts | 19 +++---- packages/backend-defaults/src/index.ts | 4 +- packages/backend-internal/.eslintrc.js | 5 ++ packages/backend-internal/README.md | 3 ++ packages/backend-internal/catalog-info.yaml | 9 ++++ packages/backend-internal/package.json | 28 ++++++++++ packages/backend-internal/src/index.ts | 17 ++++++ .../OpaqueExtensionPointFactoryMiddleware.ts | 29 +++++++++++ packages/backend-internal/src/wiring/index.ts | 17 ++++++ yarn.lock | 8 +++ 16 files changed, 181 insertions(+), 66 deletions(-) create mode 100644 packages/backend-internal/.eslintrc.js create mode 100644 packages/backend-internal/README.md create mode 100644 packages/backend-internal/catalog-info.yaml create mode 100644 packages/backend-internal/package.json create mode 100644 packages/backend-internal/src/index.ts create mode 100644 packages/backend-internal/src/wiring/OpaqueExtensionPointFactoryMiddleware.ts create mode 100644 packages/backend-internal/src/wiring/index.ts diff --git a/.changeset/extension-point-middleware-backend-defaults.md b/.changeset/extension-point-middleware-backend-defaults.md index 481ba41bed..73add7bc7f 100644 --- a/.changeset/extension-point-middleware-backend-defaults.md +++ b/.changeset/extension-point-middleware-backend-defaults.md @@ -2,4 +2,4 @@ '@backstage/backend-defaults': patch --- -Added `extensionPointFactoryMiddleware` option to `createBackend()` to reimplement extension point outputs at backend initialization time. Also re-exports `ExtensionPointFactoryMiddleware` type and `createExtensionPointFactoryMiddleware` helper from `@backstage/backend-app-api`. +Exported `defaultServiceFactories` to allow use with `createSpecializedBackend` for advanced configuration like `extensionPointFactoryMiddleware`. diff --git a/packages/backend-app-api/report.api.md b/packages/backend-app-api/report.api.md index e0557b8fd0..63e8bb1d27 100644 --- a/packages/backend-app-api/report.api.md +++ b/packages/backend-app-api/report.api.md @@ -44,10 +44,10 @@ export interface BackendStartupResult { } // @public -export function createExtensionPointFactoryMiddleware( - extensionPoint: ExtensionPoint, - middleware: (original: T) => T, -): ExtensionPointFactoryMiddleware; +export function createExtensionPointFactoryMiddleware(options: { + extensionPoint: ExtensionPoint; + middleware: (original: T) => Promise; +}): ExtensionPointFactoryMiddleware; // @public (undocumented) export function createSpecializedBackend( @@ -63,11 +63,9 @@ export interface CreateSpecializedBackendOptions { } // @public -export interface ExtensionPointFactoryMiddleware { +export interface ExtensionPointFactoryMiddleware<_T = unknown> { // (undocumented) - extensionPoint: ExtensionPoint; - // (undocumented) - middleware: (original: T) => T; + $$type: '@backstage/ExtensionPointFactoryMiddleware'; } // @public diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts index c1801f0398..fb4da6ca05 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts @@ -2122,10 +2122,13 @@ describe('BackendInitializer', () => { }); const init = new BackendInitializer(baseFactories, [ - createExtensionPointFactoryMiddleware(extensionPoint, original => ({ - ...original, - values: [...original.values, 'from-middleware'], - })), + createExtensionPointFactoryMiddleware({ + extensionPoint, + middleware: async original => ({ + ...original, + values: [...original.values, 'from-middleware'], + }), + }), ]); init.add(testPlugin); @@ -2170,10 +2173,13 @@ describe('BackendInitializer', () => { }); const init = new BackendInitializer(baseFactories, [ - createExtensionPointFactoryMiddleware(extensionPointA, original => ({ - ...original, - values: [...original.values, 'wrapped'], - })), + createExtensionPointFactoryMiddleware({ + extensionPoint: extensionPointA, + middleware: async original => ({ + ...original, + values: [...original.values, 'wrapped'], + }), + }), ]); init.add(testPlugin); @@ -2215,14 +2221,20 @@ describe('BackendInitializer', () => { }); const init = new BackendInitializer(baseFactories, [ - createExtensionPointFactoryMiddleware(extensionPoint, original => ({ - ...original, - values: [...original.values, 'first'], - })), - createExtensionPointFactoryMiddleware(extensionPoint, original => ({ - ...original, - values: [...original.values, 'second'], - })), + createExtensionPointFactoryMiddleware({ + extensionPoint, + middleware: async original => ({ + ...original, + values: [...original.values, 'first'], + }), + }), + createExtensionPointFactoryMiddleware({ + extensionPoint, + middleware: async original => ({ + ...original, + values: [...original.values, 'second'], + }), + }), ]); init.add(testPlugin); @@ -2262,13 +2274,13 @@ describe('BackendInitializer', () => { }); const init = new BackendInitializer(baseFactories, [ - createExtensionPointFactoryMiddleware( - unregisteredExtensionPoint, - original => ({ + createExtensionPointFactoryMiddleware({ + extensionPoint: unregisteredExtensionPoint, + middleware: async original => ({ ...original, values: [...original.values, 'never-applied'], }), - ), + }), ]); init.add(testPlugin); diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index 211fb7dccc..9ff04652e6 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -29,6 +29,7 @@ import { ExtensionPointFactoryMiddleware, ServiceOrExtensionPoint, } from './types'; +import { OpaqueExtensionPointFactoryMiddleware } from '@internal/backend'; // Direct internal import to avoid duplication // eslint-disable-next-line @backstage/no-relative-monorepo-imports import type { @@ -210,8 +211,9 @@ export class BackendInitializer { }, }); for (const mw of this.#extensionPointFactoryMiddleware) { - if (mw.extensionPoint.id === ref.id) { - epImpl = (mw.middleware as (original: unknown) => unknown)(epImpl); + const internal = OpaqueExtensionPointFactoryMiddleware.toInternal(mw); + if (internal.extensionPointId === ref.id) { + epImpl = await internal.middleware(epImpl); } } result.set(name, epImpl); diff --git a/packages/backend-app-api/src/wiring/types.ts b/packages/backend-app-api/src/wiring/types.ts index 766d737dea..53cd8cfb5f 100644 --- a/packages/backend-app-api/src/wiring/types.ts +++ b/packages/backend-app-api/src/wiring/types.ts @@ -20,6 +20,7 @@ import { ServiceRef, ServiceFactory, } from '@backstage/backend-plugin-api'; +import { OpaqueExtensionPointFactoryMiddleware } from '@internal/backend'; /** * A middleware entry that reimplements a specific extension point's output. @@ -28,9 +29,8 @@ import { * * @public */ -export interface ExtensionPointFactoryMiddleware { - extensionPoint: ExtensionPoint; - middleware: (original: T) => T; +export interface ExtensionPointFactoryMiddleware<_T = unknown> { + $$type: '@backstage/ExtensionPointFactoryMiddleware'; } /** @@ -39,14 +39,14 @@ export interface ExtensionPointFactoryMiddleware { * * @public */ -export function createExtensionPointFactoryMiddleware( - extensionPoint: ExtensionPoint, - middleware: (original: T) => T, -): ExtensionPointFactoryMiddleware { - return { - extensionPoint: extensionPoint as ExtensionPoint, - middleware: middleware as (original: unknown) => unknown, - }; +export function createExtensionPointFactoryMiddleware(options: { + extensionPoint: ExtensionPoint; + middleware: (original: T) => Promise; +}): ExtensionPointFactoryMiddleware { + return OpaqueExtensionPointFactoryMiddleware.createInstance('v1', { + extensionPointId: options.extensionPoint.id, + middleware: options.middleware as (original: unknown) => Promise, + }); } /** diff --git a/packages/backend-defaults/report.api.md b/packages/backend-defaults/report.api.md index ffc67e5473..a6702e59f9 100644 --- a/packages/backend-defaults/report.api.md +++ b/packages/backend-defaults/report.api.md @@ -5,18 +5,14 @@ ```ts import { Backend } from '@backstage/backend-app-api'; import { BackendFeature } from '@backstage/backend-plugin-api'; -import { createExtensionPointFactoryMiddleware } from '@backstage/backend-app-api'; -import { ExtensionPointFactoryMiddleware } from '@backstage/backend-app-api'; +import { ServiceFactory } from '@backstage/backend-plugin-api'; // @public (undocumented) -export function createBackend(options?: { - extensionPointFactoryMiddleware?: ExtensionPointFactoryMiddleware[]; -}): Backend; +export function createBackend(): Backend; -export { createExtensionPointFactoryMiddleware }; +// @public (undocumented) +export const defaultServiceFactories: ServiceFactory[]; // @public export const discoveryFeatureLoader: BackendFeature; - -export { ExtensionPointFactoryMiddleware }; ``` diff --git a/packages/backend-defaults/src/CreateBackend.ts b/packages/backend-defaults/src/CreateBackend.ts index e6976f6500..d069c17b99 100644 --- a/packages/backend-defaults/src/CreateBackend.ts +++ b/packages/backend-defaults/src/CreateBackend.ts @@ -14,11 +14,8 @@ * limitations under the License. */ -import { - Backend, - createSpecializedBackend, - ExtensionPointFactoryMiddleware, -} from '@backstage/backend-app-api'; +import { Backend, createSpecializedBackend } from '@backstage/backend-app-api'; +import { ServiceFactory } from '@backstage/backend-plugin-api'; import { auditorServiceFactory } from '@backstage/backend-defaults/auditor'; import { authServiceFactory } from '@backstage/backend-defaults/auth'; import { cacheServiceFactory } from '@backstage/backend-defaults/cache'; @@ -46,7 +43,8 @@ import { } from '@backstage/backend-defaults/alpha'; import { instanceMetadataServiceFactory } from './alpha/entrypoints/instanceMetadata/instanceMetadataServiceFactory'; -export const defaultServiceFactories = [ +/** @public */ +export const defaultServiceFactories: ServiceFactory[] = [ auditorServiceFactory, authServiceFactory, cacheServiceFactory, @@ -80,11 +78,6 @@ export const defaultServiceFactories = [ /** * @public */ -export function createBackend(options?: { - extensionPointFactoryMiddleware?: ExtensionPointFactoryMiddleware[]; -}): Backend { - return createSpecializedBackend({ - defaultServiceFactories, - extensionPointFactoryMiddleware: options?.extensionPointFactoryMiddleware, - }); +export function createBackend(): Backend { + return createSpecializedBackend({ defaultServiceFactories }); } diff --git a/packages/backend-defaults/src/index.ts b/packages/backend-defaults/src/index.ts index eb3579c88a..ccf75ed9f8 100644 --- a/packages/backend-defaults/src/index.ts +++ b/packages/backend-defaults/src/index.ts @@ -20,7 +20,5 @@ * @packageDocumentation */ -export { createBackend } from './CreateBackend'; -export type { ExtensionPointFactoryMiddleware } from '@backstage/backend-app-api'; -export { createExtensionPointFactoryMiddleware } from '@backstage/backend-app-api'; +export { createBackend, defaultServiceFactories } from './CreateBackend'; export { discoveryFeatureLoader } from './discoveryFeatureLoader'; diff --git a/packages/backend-internal/.eslintrc.js b/packages/backend-internal/.eslintrc.js new file mode 100644 index 0000000000..e487f765b2 --- /dev/null +++ b/packages/backend-internal/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { + rules: { + '@backstage/no-top-level-material-ui-4-imports': 'error', + }, +}); diff --git a/packages/backend-internal/README.md b/packages/backend-internal/README.md new file mode 100644 index 0000000000..8994693a1b --- /dev/null +++ b/packages/backend-internal/README.md @@ -0,0 +1,3 @@ +# @internal/backend + +This is an internal package used by the other backend packages. It does not get published to NPM, but instead inlined into consuming packages due to the `backstage.inline` flag in `package.json`. diff --git a/packages/backend-internal/catalog-info.yaml b/packages/backend-internal/catalog-info.yaml new file mode 100644 index 0000000000..37bc00382a --- /dev/null +++ b/packages/backend-internal/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: internal-backend + title: '@internal/backend' +spec: + lifecycle: experimental + type: backstage-node-library + owner: framework-maintainers diff --git a/packages/backend-internal/package.json b/packages/backend-internal/package.json new file mode 100644 index 0000000000..b043d1d42f --- /dev/null +++ b/packages/backend-internal/package.json @@ -0,0 +1,28 @@ +{ + "name": "@internal/backend", + "version": "0.0.1", + "backstage": { + "role": "node-library", + "inline": true + }, + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/backend-internal" + }, + "license": "Apache-2.0", + "sideEffects": false, + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist" + ], + "scripts": { + "lint": "backstage-cli package lint", + "test": "backstage-cli package test" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + } +} diff --git a/packages/backend-internal/src/index.ts b/packages/backend-internal/src/index.ts new file mode 100644 index 0000000000..add807a2c4 --- /dev/null +++ b/packages/backend-internal/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2026 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { OpaqueExtensionPointFactoryMiddleware } from './wiring'; diff --git a/packages/backend-internal/src/wiring/OpaqueExtensionPointFactoryMiddleware.ts b/packages/backend-internal/src/wiring/OpaqueExtensionPointFactoryMiddleware.ts new file mode 100644 index 0000000000..d94226ed93 --- /dev/null +++ b/packages/backend-internal/src/wiring/OpaqueExtensionPointFactoryMiddleware.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2026 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { OpaqueType } from '@internal/opaque'; + +export const OpaqueExtensionPointFactoryMiddleware = OpaqueType.create<{ + public: { $$type: '@backstage/ExtensionPointFactoryMiddleware' }; + versions: { + readonly version: 'v1'; + readonly extensionPointId: string; + readonly middleware: (original: unknown) => Promise; + }; +}>({ + type: '@backstage/ExtensionPointFactoryMiddleware', + versions: ['v1'], +}); diff --git a/packages/backend-internal/src/wiring/index.ts b/packages/backend-internal/src/wiring/index.ts new file mode 100644 index 0000000000..72c745bc9b --- /dev/null +++ b/packages/backend-internal/src/wiring/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2026 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { OpaqueExtensionPointFactoryMiddleware } from './OpaqueExtensionPointFactoryMiddleware'; diff --git a/yarn.lock b/yarn.lock index 13b51ed309..38fbd212ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9977,6 +9977,14 @@ __metadata: languageName: node linkType: hard +"@internal/backend@workspace:packages/backend-internal": + version: 0.0.0-use.local + resolution: "@internal/backend@workspace:packages/backend-internal" + dependencies: + "@backstage/cli": "workspace:^" + languageName: unknown + linkType: soft + "@internal/cli@workspace:packages/cli-internal": version: 0.0.0-use.local resolution: "@internal/cli@workspace:packages/cli-internal" From 28a362f2f769e73e3ab749a5c195a2eff9a58407 Mon Sep 17 00:00:00 2001 From: Jack Palmer Date: Fri, 10 Apr 2026 08:50:41 +0100 Subject: [PATCH 3/4] Remove unused type parameter from ExtensionPointFactoryMiddleware The generic is no longer needed since the interface is opaque. Type inference is handled by createExtensionPointFactoryMiddleware instead. Signed-off-by: Jack Palmer --- packages/backend-app-api/report.api.md | 4 ++-- packages/backend-app-api/src/wiring/types.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/backend-app-api/report.api.md b/packages/backend-app-api/report.api.md index 63e8bb1d27..1fcfc5926e 100644 --- a/packages/backend-app-api/report.api.md +++ b/packages/backend-app-api/report.api.md @@ -47,7 +47,7 @@ export interface BackendStartupResult { export function createExtensionPointFactoryMiddleware(options: { extensionPoint: ExtensionPoint; middleware: (original: T) => Promise; -}): ExtensionPointFactoryMiddleware; +}): ExtensionPointFactoryMiddleware; // @public (undocumented) export function createSpecializedBackend( @@ -63,7 +63,7 @@ export interface CreateSpecializedBackendOptions { } // @public -export interface ExtensionPointFactoryMiddleware<_T = unknown> { +export interface ExtensionPointFactoryMiddleware { // (undocumented) $$type: '@backstage/ExtensionPointFactoryMiddleware'; } diff --git a/packages/backend-app-api/src/wiring/types.ts b/packages/backend-app-api/src/wiring/types.ts index 53cd8cfb5f..afd0cecbfb 100644 --- a/packages/backend-app-api/src/wiring/types.ts +++ b/packages/backend-app-api/src/wiring/types.ts @@ -29,7 +29,7 @@ import { OpaqueExtensionPointFactoryMiddleware } from '@internal/backend'; * * @public */ -export interface ExtensionPointFactoryMiddleware<_T = unknown> { +export interface ExtensionPointFactoryMiddleware { $$type: '@backstage/ExtensionPointFactoryMiddleware'; } @@ -42,7 +42,7 @@ export interface ExtensionPointFactoryMiddleware<_T = unknown> { export function createExtensionPointFactoryMiddleware(options: { extensionPoint: ExtensionPoint; middleware: (original: T) => Promise; -}): ExtensionPointFactoryMiddleware { +}): ExtensionPointFactoryMiddleware { return OpaqueExtensionPointFactoryMiddleware.createInstance('v1', { extensionPointId: options.extensionPoint.id, middleware: options.middleware as (original: unknown) => Promise, From 90b62c24ef4ae3a76c1332714058b66fbe709e64 Mon Sep 17 00:00:00 2001 From: Jack Palmer Date: Mon, 13 Apr 2026 09:57:34 +0100 Subject: [PATCH 4/4] fix: backend-internal eslint config Signed-off-by: Jack Palmer --- packages/backend-internal/.eslintrc.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/backend-internal/.eslintrc.js b/packages/backend-internal/.eslintrc.js index e487f765b2..e2a53a6ad2 100644 --- a/packages/backend-internal/.eslintrc.js +++ b/packages/backend-internal/.eslintrc.js @@ -1,5 +1 @@ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { - rules: { - '@backstage/no-top-level-material-ui-4-imports': 'error', - }, -}); +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);