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"