From fdbd40488e5d7f51144c99c5650210fb90350e5a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 13 Feb 2026 13:20:03 +0100 Subject: [PATCH 01/10] module-federation-common: followup to initial implementation Signed-off-by: Patrik Oldsberg --- .changeset/module-federation-cli-updates.md | 5 + .../module-federation-loader-updates.md | 5 + .changeset/module-federation-sdk-bump.md | 7 + .changeset/small-ravens-add.md | 9 +- .../building-apps/07-module-federation.md | 44 +- .../src/modules/build/lib/bundler/config.ts | 19 +- .../lib/bundler/moduleFederation.test.ts | 158 ++++++ .../build/lib/bundler/moduleFederation.ts | 86 ++- .../src/modules/build/lib/bundler/types.ts | 7 +- .../src/loader.ts | 9 +- .../module-federation-common/report.api.md | 84 +-- .../src/defaults.test.ts | 1 + .../module-federation-common/src/defaults.ts | 105 +--- .../module-federation-common/src/index.ts | 13 +- .../loadModuleFederationHostShared.test.ts | 315 +++++++++++ .../src/loadModuleFederationHostShared.ts | 71 +++ .../src/runtime.test.ts | 523 ------------------ .../module-federation-common/src/runtime.ts | 110 ---- .../module-federation-common/src/types.ts | 92 +-- 19 files changed, 788 insertions(+), 875 deletions(-) create mode 100644 .changeset/module-federation-cli-updates.md create mode 100644 .changeset/module-federation-loader-updates.md create mode 100644 .changeset/module-federation-sdk-bump.md create mode 100644 packages/cli/src/modules/build/lib/bundler/moduleFederation.test.ts create mode 100644 packages/module-federation-common/src/loadModuleFederationHostShared.test.ts create mode 100644 packages/module-federation-common/src/loadModuleFederationHostShared.ts delete mode 100644 packages/module-federation-common/src/runtime.test.ts delete mode 100644 packages/module-federation-common/src/runtime.ts diff --git a/.changeset/module-federation-cli-updates.md b/.changeset/module-federation-cli-updates.md new file mode 100644 index 0000000000..da874358d5 --- /dev/null +++ b/.changeset/module-federation-cli-updates.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Removed the `EXPERIMENTAL_MODULE_FEDERATION` environment variable flag, making module federation host support always available during `package start`. The host shared dependencies are now managed through `@backstage/module-federation-common` and injected as a versioned runtime script at build time. diff --git a/.changeset/module-federation-loader-updates.md b/.changeset/module-federation-loader-updates.md new file mode 100644 index 0000000000..21192c0d1d --- /dev/null +++ b/.changeset/module-federation-loader-updates.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-dynamic-feature-loader': patch +--- + +Updated module federation integration to use `@module-federation/enhanced/runtime` `createInstance` API and the new `loadModuleFederationHostShared` from `@backstage/module-federation-common` for loading shared dependencies. Also added support for passing a pre-created `ModuleFederation` instance via the `moduleFederation.instance` option. diff --git a/.changeset/module-federation-sdk-bump.md b/.changeset/module-federation-sdk-bump.md new file mode 100644 index 0000000000..0a092af5fb --- /dev/null +++ b/.changeset/module-federation-sdk-bump.md @@ -0,0 +1,7 @@ +--- +'@backstage/backend-dynamic-feature-service': patch +'@backstage/frontend-dynamic-feature-loader': patch +'@backstage/cli': patch +--- + +Updated `@module-federation/enhanced`, `@module-federation/runtime`, and `@module-federation/sdk` dependencies from `^0.9.0` to `^0.21.6`. diff --git a/.changeset/small-ravens-add.md b/.changeset/small-ravens-add.md index bec57e94fe..6527ec1309 100644 --- a/.changeset/small-ravens-add.md +++ b/.changeset/small-ravens-add.md @@ -1,10 +1,5 @@ --- -'@backstage/backend-dynamic-feature-service': patch -'@backstage/frontend-dynamic-feature-loader': patch -'@backstage/module-federation-common': patch -'@backstage/cli': patch +'@backstage/module-federation-common': minor --- -Enable Module Federation support in the frontend application (Module Federation host) through API only, without using the ModuleFederationPlugin at build time, nor producing specific generated bundled assets. -Module federation remotes still use ModuleFederationPlugin at build time to provide module-federation enabled remote modules, like plugin bundles or dynamic frontend plugins. -Default shared dependencies are provided for both the frontend application (Module Federation host), and Module Federation remotes, maintaining consistency between both sides. +Added new `@backstage/module-federation-common` package that provides shared types, default configurations, and runtime utilities for module federation. It includes `loadModuleFederationHostShared` for loading shared dependencies in parallel at runtime, `defaultHostSharedDependencies` and `defaultRemoteSharedDependencies` for consistent dependency configuration, and types such as `HostSharedDependencies`, `RemoteSharedDependencies`, and `RuntimeSharedDependenciesGlobal`. diff --git a/docs/frontend-system/building-apps/07-module-federation.md b/docs/frontend-system/building-apps/07-module-federation.md index 81d2dde7d6..5e83a86343 100644 --- a/docs/frontend-system/building-apps/07-module-federation.md +++ b/docs/frontend-system/building-apps/07-module-federation.md @@ -74,29 +74,18 @@ import { createInstance, ModuleFederation, } from '@module-federation/enhanced/runtime'; -import { buildRuntimeSharedUserOption } from '@backstage/module-federation-common'; +import { loadModuleFederationHostShared } from '@backstage/module-federation-common'; export async function initializeModuleFederation(): Promise { - // Build the shared dependencies configuration - const { shared, errors } = await buildRuntimeSharedUserOption(); - - // Log any errors loading shared dependencies - if (errors.length > 0) { - for (const err of errors) { - console.error(err.message, err.cause); - } - } - - // Initialize Module Federation Runtime return createInstance({ - name: 'app-next', + name: 'app', remotes: [ { name: 'my_plugin', entry: 'http://localhost:3001/mf-manifest.json', }, ], - shared, + shared: await loadModuleFederationHostShared(), }); } @@ -108,26 +97,31 @@ export async function loadRemote( } ``` +The `loadModuleFederationHostShared` function loads all shared dependencies in parallel and returns them in the format expected by the Module Federation Runtime. By default it will throw if any shared dependency fails to load. You can pass an `onError` callback to handle errors gracefully instead: + +```typescript +const shared = await loadModuleFederationHostShared({ + onError: error => console.error(error.message, error.cause), +}); +``` + ### Integration with Feature Loaders Standard Module Federation runtime API integrates very well with frontend feature loaders, as shown in the example below: -```typescript title="packages/app/src/App.tsx" +```typescript title="packages/app/src/loader.tsx" import { createInstance } from '@module-federation/enhanced/runtime'; -import { buildRuntimeSharedUserOption } from '@backstage/module-federation-common'; +import { loadModuleFederationHostShared } from '@backstage/module-federation-common'; import { createFrontendFeatureLoader } from '@backstage/frontend-plugin-api'; -const moduleFederationInstance = createInstance({ - name: 'app-next', - remotes: [], -}); - -const moduleFederationLoader = createFrontendFeatureLoader({ +export const moduleFederationLoader = createFrontendFeatureLoader({ async loader() { - moduleFederationInstance.registerShared( - (await buildRuntimeSharedUserOption()).shared, - ); + const moduleFederationInstance = createInstance({ + name: 'app', + remotes: [], + shared: await loadModuleFederationHostShared(), + }); moduleFederationInstance.registerRemotes([ { name: 'myFirstRemoteWith2ExposedModules', diff --git a/packages/cli/src/modules/build/lib/bundler/config.ts b/packages/cli/src/modules/build/lib/bundler/config.ts index 278f6a3b69..a4e2b16ab9 100644 --- a/packages/cli/src/modules/build/lib/bundler/config.ts +++ b/packages/cli/src/modules/build/lib/bundler/config.ts @@ -252,24 +252,7 @@ export async function createConfig( exposes, name: options.moduleFederationRemote.name, runtime: false, - shared: Object.fromEntries( - Object.entries(options.moduleFederationRemote.sharedDependencies).map( - ([name, p]) => [ - name, - { - ...(p.version === undefined ? {} : { version: p.version }), - ...(p.requiredVersion === undefined - ? {} - : { requiredVersion: p.requiredVersion }), - ...(p.singleton === undefined - ? {} - : { singleton: p.singleton }), - ...(p.import === undefined ? {} : { import: p.import }), - eager: false, - }, - ], - ), - ), + shared: options.moduleFederationRemote.sharedDependencies, }), ); } diff --git a/packages/cli/src/modules/build/lib/bundler/moduleFederation.test.ts b/packages/cli/src/modules/build/lib/bundler/moduleFederation.test.ts new file mode 100644 index 0000000000..a2c089e539 --- /dev/null +++ b/packages/cli/src/modules/build/lib/bundler/moduleFederation.test.ts @@ -0,0 +1,158 @@ +/* + * Copyright 2025 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 { prepareRuntimeSharedDependenciesScript } from './moduleFederation'; +import { BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL } from '@backstage/module-federation-common'; + +const GLOBAL = BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL; + +describe('prepareRuntimeSharedDependenciesScript', () => { + it('should generate script with a single dependency', () => { + const result = prepareRuntimeSharedDependenciesScript({ + react: { + version: '18.2.0', + requiredVersion: '*', + singleton: true, + eager: false, + }, + }); + + expect(result).toBe(`window['${GLOBAL}'] = { + "items": [ + { + "name": "react", + "version": "18.2.0", + "lib": () => import("react"), + "shareConfig": { + "singleton": true, + "requiredVersion": "*", + "eager": false + } + } + ], + "version": "v1" +};`); + }); + + it('should generate script with multiple dependencies', () => { + const result = prepareRuntimeSharedDependenciesScript({ + react: { + version: '18.2.0', + requiredVersion: '*', + singleton: true, + eager: true, + }, + 'react-dom': { + version: '18.2.0', + requiredVersion: '*', + singleton: true, + eager: true, + }, + lodash: { + version: '4.17.21', + requiredVersion: '*', + singleton: true, + eager: false, + }, + }); + + expect(result).toBe(`window['${GLOBAL}'] = { + "items": [ + { + "name": "react", + "version": "18.2.0", + "lib": () => import("react"), + "shareConfig": { + "singleton": true, + "requiredVersion": "*", + "eager": true + } + }, + { + "name": "react-dom", + "version": "18.2.0", + "lib": () => import("react-dom"), + "shareConfig": { + "singleton": true, + "requiredVersion": "*", + "eager": true + } + }, + { + "name": "lodash", + "version": "4.17.21", + "lib": () => import("lodash"), + "shareConfig": { + "singleton": true, + "requiredVersion": "*", + "eager": false + } + } + ], + "version": "v1" +};`); + }); + + it('should handle custom requiredVersion', () => { + const result = prepareRuntimeSharedDependenciesScript({ + react: { + version: '18.2.0', + requiredVersion: '^18.0.0', + singleton: true, + eager: false, + }, + }); + + expect(result).toContain('"requiredVersion": "^18.0.0"'); + }); + + it('should handle scoped package names', () => { + const result = prepareRuntimeSharedDependenciesScript({ + '@backstage/core-plugin-api': { + version: '1.0.0', + requiredVersion: '*', + singleton: true, + eager: false, + }, + }); + + expect(result).toContain('"name": "@backstage/core-plugin-api"'); + expect(result).toContain( + '"lib": () => import("@backstage/core-plugin-api")', + ); + }); + + it('should handle empty dependencies', () => { + const result = prepareRuntimeSharedDependenciesScript({}); + + expect(result).toBe(`window['${GLOBAL}'] = { + "items": [], + "version": "v1" +};`); + }); + + it('should throw if version is missing', () => { + expect(() => + prepareRuntimeSharedDependenciesScript({ + react: { + requiredVersion: '*', + singleton: true, + eager: false, + }, + }), + ).toThrow("Version is required for shared dependency 'react'"); + }); +}); diff --git a/packages/cli/src/modules/build/lib/bundler/moduleFederation.ts b/packages/cli/src/modules/build/lib/bundler/moduleFederation.ts index becf438c34..8faab7f732 100644 --- a/packages/cli/src/modules/build/lib/bundler/moduleFederation.ts +++ b/packages/cli/src/modules/build/lib/bundler/moduleFederation.ts @@ -22,11 +22,11 @@ import { getEntryPointDefaultFeatureType, } from '../../../../lib/typeDistProject'; import { - SharedDependencies, - Host, - prepareRuntimeSharedDependenciesScript, + BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL, defaultRemoteSharedDependencies, defaultHostSharedDependencies, + HostSharedDependencies, + RuntimeSharedDependenciesGlobal, } from '@backstage/module-federation-common'; import { dirname, join as joinPath, resolve as resolvePath } from 'path'; import fs from 'fs-extra'; @@ -82,6 +82,44 @@ export async function getModuleFederationRemoteOptions( // Module federation host management utilities +/** + * Prepares the runtime shared dependencies script for the module federation host, + * which will be written by the CLI into a Javascript file added as an additional entry point for the frontend bundler. + * This script is used in the browser to build the list of shared dependencies provided to the module federation runtime. + * + * @internal + */ +export function prepareRuntimeSharedDependenciesScript( + hostSharedDependencies: HostSharedDependencies, +) { + const items = Object.entries(hostSharedDependencies).map( + ([name, sharedDep]) => { + if (!sharedDep.version) { + throw new Error(`Version is required for shared dependency '${name}'`); + } + return { + name, + version: sharedDep.version, + lib: name as unknown as () => Promise, // Coverted into import below + shareConfig: { + singleton: sharedDep.singleton, + requiredVersion: sharedDep.requiredVersion, + eager: sharedDep.eager, + }, + }; + }, + ); + + return `window['${BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL}'] = ${JSON.stringify( + { items, version: 'v1' } satisfies RuntimeSharedDependenciesGlobal, + null, + 2, + ).replace( + /"lib": ("[^"]+")/gm, + (_, name) => `"lib": () => import(${name})`, + )};`; +} + const RUNTIME_SHARED_DEPENDENCIES_MODULE_NAME = '__backstage-module-federation-runtime-shared-dependencies__'; @@ -90,7 +128,7 @@ const writeQueue = new PQueue({ concurrency: 1 }); async function writeRuntimeSharedDependenciesModule( targetPath: string, - runtimeSharedDependencies: SharedDependencies, + runtimeSharedDependencies: HostSharedDependencies, ) { const script = prepareRuntimeSharedDependenciesScript( runtimeSharedDependencies, @@ -110,41 +148,31 @@ async function writeRuntimeSharedDependenciesModule( function resolveSharedDependencyVersions( targetPath: string, - hostSharedDependencies: SharedDependencies, -): SharedDependencies { + hostSharedDependencies: HostSharedDependencies, +): HostSharedDependencies { return Object.fromEntries( Object.entries(hostSharedDependencies) .filter(([_, sharedDep]) => sharedDep !== undefined) - .map(([name, sharedDep]) => { - // Use require.resolve to find the package - // For scoped modules, keep the scope and the module name, but remove any sub-folder - const nameParts = name.split('/'); - const moduleName = - nameParts[0].startsWith('@') && nameParts.length > 1 - ? `${nameParts[0]}/${nameParts[1]}` - : nameParts[0]; - let packagePath: string; + .flatMap(([importPath, sharedDep]) => { + // Remove any sub-path exports from the import path + const moduleName = importPath.startsWith('@') + ? importPath.split('/').slice(0, 2).join('/') + : importPath.split('/')[0]; + + let version: string; try { - packagePath = require.resolve(`${moduleName}/package.json`, { + const packagePath = require.resolve(`${moduleName}/package.json`, { paths: [targetPath], }); + version = require(packagePath).version; } catch (e) { - throw new Error( - `Failed to resolve package.json for module federation shared dependency '${name}': ${e}`, - ); - } - const packageJson = require(packagePath); - - if (sharedDep.version && packageJson.version !== sharedDep.version) { - throw new Error( - `Version mismatch for module federation shared dependency '${name}': '${sharedDep.version}' vs '${packageJson.version}' found in '${packagePath}'.`, + console.log( + `Skipping module federation shared dependency '${importPath}' because it could not be resolved.`, ); + return []; } - return [ - name, - { ...sharedDep, version: sharedDep.version ?? packageJson.version }, - ]; + return [[importPath, { ...sharedDep, version }]]; }), ); } diff --git a/packages/cli/src/modules/build/lib/bundler/types.ts b/packages/cli/src/modules/build/lib/bundler/types.ts index 835b92e180..a9a4b0f51f 100644 --- a/packages/cli/src/modules/build/lib/bundler/types.ts +++ b/packages/cli/src/modules/build/lib/bundler/types.ts @@ -17,10 +17,7 @@ import { AppConfig, Config } from '@backstage/config'; import { BundlingPathsOptions } from './paths'; import { ConfigSchema } from '@backstage/config-loader'; -import { - SharedDependencies, - Remote, -} from '@backstage/module-federation-common'; +import { RemoteSharedDependencies } from '@backstage/module-federation-common'; export type ModuleFederationRemoteOptions = { // Unique name for this module federation bundle @@ -31,7 +28,7 @@ export type ModuleFederationRemoteOptions = { */ [k: string]: string; }; - sharedDependencies: SharedDependencies; + sharedDependencies: RemoteSharedDependencies; }; export type BundlingOptions = { diff --git a/packages/frontend-dynamic-feature-loader/src/loader.ts b/packages/frontend-dynamic-feature-loader/src/loader.ts index 616481ae95..9542977203 100644 --- a/packages/frontend-dynamic-feature-loader/src/loader.ts +++ b/packages/frontend-dynamic-feature-loader/src/loader.ts @@ -27,7 +27,7 @@ import { createFrontendFeatureLoader, } from '@backstage/frontend-plugin-api'; import { ShareStrategy, UserOptions } from '@module-federation/runtime/types'; -import { buildRuntimeSharedUserOption } from '@backstage/module-federation-common'; +import { loadModuleFederationHostShared } from '@backstage/module-federation-common'; /** * @@ -105,10 +105,9 @@ export function dynamicFrontendFeaturesLoader( if (options?.moduleFederation?.instance) { instance = options.moduleFederation.instance; } else { - const { shared, errors } = await buildRuntimeSharedUserOption(); - for (const err of errors) { - error(err.message, err.cause); - } + const shared = await loadModuleFederationHostShared({ + onError: err => error(err.message, err.cause), + }); const createOptions: UserOptions = { name: appPackageName diff --git a/packages/module-federation-common/report.api.md b/packages/module-federation-common/report.api.md index ec0fb25394..08c28366df 100644 --- a/packages/module-federation-common/report.api.md +++ b/packages/module-federation-common/report.api.md @@ -3,48 +3,60 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { ForwardedError } from '@backstage/errors'; -import type { UserOptions } from '@module-federation/runtime/types'; +// @public +export const BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL = + '__@backstage/module_federation_shared_dependencies__'; // @public -export function buildRuntimeSharedUserOption(): Promise<{ - shared: UserOptions['shared']; - errors: ForwardedError[]; -}>; +export function defaultHostSharedDependencies(): HostSharedDependencies; // @public -export function defaultHostSharedDependencies(): SharedDependencies; +export function defaultRemoteSharedDependencies(): RemoteSharedDependencies; // @public -export function defaultRemoteSharedDependencies(): SharedDependencies; - -// @public -export type Host = { - eager?: boolean; - requiredVersion: false | string; - version?: string; -}; - -// @public -export function prepareRuntimeSharedDependenciesScript( - hostSharedDependencies: SharedDependencies< - Host & { - version: string; - } - >, -): string; - -// @public -export type Remote = { - import?: false | string; - requiredVersion?: false | string; - version?: false | string; -}; - -// @public -export type SharedDependencies = { +export type HostSharedDependencies = { [name: string]: { - singleton?: boolean; - } & ContextFields; + singleton: boolean; + eager: boolean; + requiredVersion: string; + version?: string; + }; +}; + +// @public +export type LoadedRuntimeSharedDependency = { + version: string; + lib: () => unknown; + shareConfig: HostSharedDependencies[string]; +}; + +// @public +export function loadModuleFederationHostShared(options?: { + onError?: (error: Error) => void; +}): Promise>; + +// @public +export type RemoteSharedDependencies = { + [name: string]: { + singleton: boolean; + eager: boolean; + import?: false | string; + requiredVersion: string; + }; +}; + +// @public +export type RuntimeSharedDependenciesGlobal = { + items: Array<{ + name: string; + version: string; + lib: () => Promise; + shareConfig: { + singleton: boolean; + requiredVersion: string; + eager: boolean; + }; + }>; + version: 'v1'; }; ``` diff --git a/packages/module-federation-common/src/defaults.test.ts b/packages/module-federation-common/src/defaults.test.ts index cc84c82e09..f38379e1c9 100644 --- a/packages/module-federation-common/src/defaults.test.ts +++ b/packages/module-federation-common/src/defaults.test.ts @@ -38,6 +38,7 @@ describe('defaultRemoteSharedDependencies', () => { expect(result.react).toEqual({ requiredVersion: '*', singleton: true, + eager: false, import: false, }); }); diff --git a/packages/module-federation-common/src/defaults.ts b/packages/module-federation-common/src/defaults.ts index 8197f5eb98..27e0e292cf 100644 --- a/packages/module-federation-common/src/defaults.ts +++ b/packages/module-federation-common/src/defaults.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Host, Remote, SharedDependencies } from './types'; +import { RemoteSharedDependencies, HostSharedDependencies } from './types'; /** * The list of default shared dependencies, expected to be the same @@ -22,115 +22,62 @@ import { Host, Remote, SharedDependencies } from './types'; * * @internal */ -const defaultSharedDependencies: SharedDependencies<{ - host: Host; - remote: Remote; -}> = { +const defaultSharedDependencies = { // React react: { - singleton: true, - host: { - eager: true, - requiredVersion: '*', - }, - remote: { - import: false, - requiredVersion: '*', - }, + host: {}, + remote: { import: false }, }, 'react-dom': { - singleton: true, - host: { - eager: true, - requiredVersion: '*', - }, - remote: { - import: false, - requiredVersion: '*', - }, + host: {}, + remote: { import: false }, }, // React Router 'react-router': { - singleton: true, - host: { - eager: true, - requiredVersion: '*', - }, - remote: { - import: false, - requiredVersion: '*', - }, + host: {}, + remote: { import: false }, }, 'react-router-dom': { - singleton: true, - host: { - eager: true, - requiredVersion: '*', - }, - remote: { - import: false, - requiredVersion: '*', - }, + host: {}, + remote: { import: false }, }, // MUI v4 // not setting import: false for MUI packages as this // will break once Backstage moves to BUI '@material-ui/core/styles': { - singleton: true, - host: { - eager: true, - requiredVersion: '*', - }, - remote: { - requiredVersion: '*', - }, + host: {}, + remote: {}, }, '@material-ui/styles': { - singleton: true, - host: { - eager: true, - requiredVersion: '*', - }, - remote: { - requiredVersion: '*', - }, + host: {}, + remote: {}, }, // MUI v5 // not setting import: false for MUI packages as this // will break once Backstage moves to BUI '@mui/material/styles/': { - singleton: true, - host: { - eager: true, - requiredVersion: '*', - }, - remote: { - requiredVersion: '*', - }, + host: {}, + remote: {}, }, '@emotion/react': { - singleton: true, - host: { - eager: true, - requiredVersion: '*', - }, - remote: { - requiredVersion: '*', - }, + host: {}, + remote: {}, }, -}; +} as const; /** * Returns the list of default shared dependencies for the host, with host-only properties. * * @public */ -export function defaultHostSharedDependencies(): SharedDependencies { +export function defaultHostSharedDependencies(): HostSharedDependencies { return Object.fromEntries( Object.entries(defaultSharedDependencies).map(([name, p]) => [ name, { - singleton: p.singleton, + eager: true, + singleton: true, + requiredVersion: '*', ...p.host, }, ]), @@ -142,12 +89,14 @@ export function defaultHostSharedDependencies(): SharedDependencies { * * @public */ -export function defaultRemoteSharedDependencies(): SharedDependencies { +export function defaultRemoteSharedDependencies(): RemoteSharedDependencies { return Object.fromEntries( Object.entries(defaultSharedDependencies).map(([name, p]) => [ name, { - singleton: p.singleton, + eager: false, + singleton: true, + requiredVersion: '*', ...p.remote, }, ]), diff --git a/packages/module-federation-common/src/index.ts b/packages/module-federation-common/src/index.ts index cc79a20c6e..290373ada3 100644 --- a/packages/module-federation-common/src/index.ts +++ b/packages/module-federation-common/src/index.ts @@ -22,12 +22,15 @@ * @packageDocumentation */ -export { - prepareRuntimeSharedDependenciesScript, - buildRuntimeSharedUserOption, -} from './runtime'; +export { loadModuleFederationHostShared } from './loadModuleFederationHostShared'; export { defaultHostSharedDependencies, defaultRemoteSharedDependencies, } from './defaults'; -export type { Host, Remote, SharedDependencies } from './types'; +export { + BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL, + type HostSharedDependencies, + type RemoteSharedDependencies, + type LoadedRuntimeSharedDependency, + type RuntimeSharedDependenciesGlobal, +} from './types'; diff --git a/packages/module-federation-common/src/loadModuleFederationHostShared.test.ts b/packages/module-federation-common/src/loadModuleFederationHostShared.test.ts new file mode 100644 index 0000000000..c62a8b6112 --- /dev/null +++ b/packages/module-federation-common/src/loadModuleFederationHostShared.test.ts @@ -0,0 +1,315 @@ +/* + * Copyright 2025 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 { loadModuleFederationHostShared } from './loadModuleFederationHostShared'; +import { + BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL, + RuntimeSharedDependenciesGlobal, +} from './types'; +import { ForwardedError } from '@backstage/errors'; + +const globalSpy = jest.fn(); +Object.defineProperty(global, BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL, { + get: globalSpy, +}); + +function mockGlobal(items: RuntimeSharedDependenciesGlobal['items']) { + globalSpy.mockReturnValue({ items, version: 'v1' }); +} + +describe('loadModuleFederationHostShared', () => { + afterEach(jest.resetAllMocks); + + it('should preload and return shared dependencies keyed by name', async () => { + const reactMock = { default: { React: 'react' } }; + const reactDomMock = { default: { ReactDom: 'react-dom' } }; + + mockGlobal([ + { + name: 'react', + version: '18.2.0', + lib: async () => reactMock, + shareConfig: { + singleton: true, + requiredVersion: '*', + eager: false, + }, + }, + { + name: 'react-dom', + version: '18.2.0', + lib: async () => reactDomMock, + shareConfig: { + singleton: true, + requiredVersion: '*', + eager: false, + }, + }, + ]); + const shared = await loadModuleFederationHostShared(); + + expect(shared).toEqual({ + react: { + version: '18.2.0', + lib: expect.any(Function), + shareConfig: { + singleton: true, + requiredVersion: '*', + eager: false, + }, + }, + 'react-dom': { + version: '18.2.0', + lib: expect.any(Function), + shareConfig: { + singleton: true, + requiredVersion: '*', + eager: false, + }, + }, + }); + + expect(shared?.react?.lib()).toBe(reactMock); + expect(shared?.['react-dom']?.lib()).toBe(reactDomMock); + }); + + it('should preserve shareConfig properties from each remote', async () => { + mockGlobal([ + { + name: 'react', + version: '18.2.0', + lib: async () => ({ default: {} }), + shareConfig: { + singleton: true, + requiredVersion: '^18.0.0', + eager: true, + }, + }, + { + name: 'lodash', + version: '4.17.21', + lib: async () => ({ default: {} }), + shareConfig: { + singleton: false, + requiredVersion: '^4.17.0', + eager: false, + }, + }, + ]); + const shared = await loadModuleFederationHostShared(); + + expect(shared).toEqual({ + react: { + version: '18.2.0', + lib: expect.any(Function), + shareConfig: { + singleton: true, + requiredVersion: '^18.0.0', + eager: true, + }, + }, + lodash: { + version: '4.17.21', + lib: expect.any(Function), + shareConfig: { + singleton: false, + requiredVersion: '^4.17.0', + eager: false, + }, + }, + }); + }); + + it('should handle empty items', async () => { + mockGlobal([]); + expect(await loadModuleFederationHostShared()).toEqual({}); + }); + + it('should throw on unsupported or missing version', async () => { + globalSpy.mockReturnValue({ items: [], version: 'v2' }); + await expect(loadModuleFederationHostShared()).rejects.toThrow( + 'Unsupported version of the runtime shared dependencies: v2', + ); + + globalSpy.mockReturnValue({ items: [] }); + await expect(loadModuleFederationHostShared()).rejects.toThrow( + 'Unsupported version of the runtime shared dependencies: undefined', + ); + + globalSpy.mockReturnValue(undefined); + await expect(loadModuleFederationHostShared()).rejects.toThrow( + 'Unsupported version of the runtime shared dependencies: undefined', + ); + }); + + it('should report errors via onError and still return successful modules', async () => { + const mockError = new Error('Module import failed'); + const reactMock = { default: { React: 'react' } }; + const lodashMock = { default: { _: 'lodash' } }; + + mockGlobal([ + { + name: 'react', + version: '18.2.0', + lib: async () => reactMock, + shareConfig: { + singleton: true, + requiredVersion: '^18.0.0', + eager: false, + }, + }, + { + name: 'failing-module', + version: '0.0.0', + lib: async () => { + throw mockError; + }, + shareConfig: { + singleton: true, + requiredVersion: '*', + eager: false, + }, + }, + { + name: 'lodash', + version: '4.17.21', + lib: async () => lodashMock, + shareConfig: { + singleton: true, + requiredVersion: '^4.17.0', + eager: false, + }, + }, + ]); + + const errors: Error[] = []; + const shared = await loadModuleFederationHostShared({ + onError: err => errors.push(err), + }); + + expect(errors).toHaveLength(1); + expect(errors[0]).toBeInstanceOf(ForwardedError); + expect(errors[0].message).toContain( + 'Failed to preload module federation shared dependency', + ); + expect(errors[0]).toHaveProperty('cause', mockError); + + expect(shared).toEqual({ + react: { + version: '18.2.0', + lib: expect.any(Function), + shareConfig: { + singleton: true, + requiredVersion: '^18.0.0', + eager: false, + }, + }, + lodash: { + version: '4.17.21', + lib: expect.any(Function), + shareConfig: { + singleton: true, + requiredVersion: '^4.17.0', + eager: false, + }, + }, + }); + + expect(shared?.react?.lib()).toBe(reactMock); + expect(shared?.lodash?.lib()).toBe(lodashMock); + }); + + it('should throw on failure when no onError is provided', async () => { + const mockError = new Error('Module import failed'); + + mockGlobal([ + { + name: 'react', + version: '18.2.0', + lib: async () => ({ default: {} }), + shareConfig: { + singleton: true, + requiredVersion: '*', + eager: false, + }, + }, + { + name: 'failing-module', + version: '0.0.0', + lib: async () => { + throw mockError; + }, + shareConfig: { + singleton: true, + requiredVersion: '*', + eager: false, + }, + }, + ]); + + await expect(loadModuleFederationHostShared()).rejects.toMatchObject({ + message: expect.stringContaining( + 'Failed to preload module federation shared dependency', + ), + cause: mockError, + }); + }); + + it('should report multiple errors via onError', async () => { + const mockError1 = new Error('First module failed'); + const mockError2 = new Error('Second module failed'); + + mockGlobal([ + { + name: 'failing-module-1', + version: '0.0.0', + lib: async () => { + throw mockError1; + }, + shareConfig: { + singleton: true, + requiredVersion: '*', + eager: false, + }, + }, + { + name: 'failing-module-2', + version: '0.0.0', + lib: async () => { + throw mockError2; + }, + shareConfig: { + singleton: true, + requiredVersion: '*', + eager: false, + }, + }, + ]); + + const errors: Error[] = []; + const shared = await loadModuleFederationHostShared({ + onError: err => errors.push(err), + }); + + expect(errors).toHaveLength(2); + expect(errors[0]).toBeInstanceOf(ForwardedError); + expect(errors[0]).toHaveProperty('cause', mockError1); + expect(errors[1]).toBeInstanceOf(ForwardedError); + expect(errors[1]).toHaveProperty('cause', mockError2); + + expect(shared).toEqual({}); + }); +}); diff --git a/packages/module-federation-common/src/loadModuleFederationHostShared.ts b/packages/module-federation-common/src/loadModuleFederationHostShared.ts new file mode 100644 index 0000000000..dd350e583a --- /dev/null +++ b/packages/module-federation-common/src/loadModuleFederationHostShared.ts @@ -0,0 +1,71 @@ +/* + * Copyright 2025 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 { + BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL, + LoadedRuntimeSharedDependency, + RuntimeSharedDependenciesGlobal, +} from './types'; +import { ForwardedError } from '@backstage/errors'; + +/** + * Preloads and builds the list of shared dependencies provided to the module federation runtime. + * + * @public + */ +export async function loadModuleFederationHostShared(options?: { + onError?: (error: Error) => void; +}): Promise> { + const { items = [], version } = + ( + window as { + [BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL]?: RuntimeSharedDependenciesGlobal; + } + )[BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL] ?? {}; + if (version !== 'v1') { + throw new Error( + `Unsupported version of the runtime shared dependencies: ${version}`, + ); + } + + const results = await Promise.allSettled(items.map(item => item.lib())); + + const shared: Record = {}; + for (let i = 0; i < results.length; i++) { + const result = results[i]; + if (result.status === 'fulfilled') { + const mod = result.value; + const item = items[i]; + shared[item.name] = { + version: item.version, + lib: () => mod, + shareConfig: item.shareConfig, + }; + } else { + const error = new ForwardedError( + 'Failed to preload module federation shared dependency', + result.reason, + ); + if (options?.onError) { + options.onError(error); + } else { + throw error; + } + } + } + + return shared; +} diff --git a/packages/module-federation-common/src/runtime.test.ts b/packages/module-federation-common/src/runtime.test.ts deleted file mode 100644 index fb73b866fa..0000000000 --- a/packages/module-federation-common/src/runtime.test.ts +++ /dev/null @@ -1,523 +0,0 @@ -/* - * Copyright 2025 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 { - prepareRuntimeSharedDependenciesScript, - buildRuntimeSharedUserOption, -} from './runtime'; -import { ForwardedError } from '@backstage/errors'; - -describe('prepareRuntimeSharedDependenciesScript', () => { - it('should generate script with minimal required properties', () => { - const sharedDependencies = { - react: { - version: '18.2.0', - requiredVersion: '*', - }, - }; - - const result = prepareRuntimeSharedDependenciesScript(sharedDependencies); - - expect(result) - .toBe(`window['__backstage-module-federation-shared-dependencies__'] = { - "react": { - "version": "18.2.0", - "requiredVersion": "*", - "module": () => import('react') - } -};`); - }); - - it('should handle multiple shared dependencies', () => { - const sharedDependencies = { - react: { - version: '18.2.0', - requiredVersion: '*', - }, - 'react-dom': { - version: '18.2.0', - requiredVersion: '*', - }, - lodash: { - version: '4.17.21', - requiredVersion: '*', - }, - }; - - const result = prepareRuntimeSharedDependenciesScript(sharedDependencies); - - expect(result) - .toBe(`window['__backstage-module-federation-shared-dependencies__'] = { - "react": { - "version": "18.2.0", - "requiredVersion": "*", - "module": () => import('react') - }, - "react-dom": { - "version": "18.2.0", - "requiredVersion": "*", - "module": () => import('react-dom') - }, - "lodash": { - "version": "4.17.21", - "requiredVersion": "*", - "module": () => import('lodash') - } -};`); - }); - - it('should include requiredVersion when provided', () => { - const sharedDependencies = { - react: { - version: '18.2.0', - requiredVersion: '^18.0.0', - }, - }; - - const result = prepareRuntimeSharedDependenciesScript(sharedDependencies); - - expect(result) - .toBe(`window['__backstage-module-federation-shared-dependencies__'] = { - "react": { - "version": "18.2.0", - "requiredVersion": "^18.0.0", - "module": () => import('react') - } -};`); - }); - - it('should include requiredVersion when set to false', () => { - const sharedDependencies = { - react: { - version: '18.2.0', - requiredVersion: false as const, - }, - }; - - const result = prepareRuntimeSharedDependenciesScript(sharedDependencies); - - expect(result) - .toBe(`window['__backstage-module-federation-shared-dependencies__'] = { - "react": { - "version": "18.2.0", - "requiredVersion": false, - "module": () => import('react') - } -};`); - }); - - it('should include singleton when true', () => { - const sharedDependencies = { - react: { - version: '18.2.0', - requiredVersion: '*', - singleton: true, - }, - }; - - const result = prepareRuntimeSharedDependenciesScript(sharedDependencies); - - expect(result) - .toBe(`window['__backstage-module-federation-shared-dependencies__'] = { - "react": { - "version": "18.2.0", - "requiredVersion": "*", - "singleton": true, - "module": () => import('react') - } -};`); - }); - - it('should include singleton when false', () => { - const sharedDependencies = { - lodash: { - version: '4.17.21', - requiredVersion: '*', - singleton: false, - }, - }; - - const result = prepareRuntimeSharedDependenciesScript(sharedDependencies); - - expect(result) - .toBe(`window['__backstage-module-federation-shared-dependencies__'] = { - "lodash": { - "version": "4.17.21", - "requiredVersion": "*", - "singleton": false, - "module": () => import('lodash') - } -};`); - }); - - it('should include eager when true', () => { - const sharedDependencies = { - react: { - version: '18.2.0', - requiredVersion: '*', - eager: true, - }, - }; - - const result = prepareRuntimeSharedDependenciesScript(sharedDependencies); - - expect(result) - .toBe(`window['__backstage-module-federation-shared-dependencies__'] = { - "react": { - "version": "18.2.0", - "requiredVersion": "*", - "eager": true, - "module": () => import('react') - } -};`); - }); - - it('should include eager when false', () => { - const sharedDependencies = { - lodash: { - version: '4.17.21', - requiredVersion: '*', - eager: false, - }, - }; - - const result = prepareRuntimeSharedDependenciesScript(sharedDependencies); - - expect(result) - .toBe(`window['__backstage-module-federation-shared-dependencies__'] = { - "lodash": { - "version": "4.17.21", - "requiredVersion": "*", - "eager": false, - "module": () => import('lodash') - } -};`); - }); - - it('should handle all properties together', () => { - const sharedDependencies = { - react: { - version: '18.2.0', - requiredVersion: '^18.0.0', - singleton: true, - eager: false, - }, - }; - - const result = prepareRuntimeSharedDependenciesScript(sharedDependencies); - - expect(result) - .toBe(`window['__backstage-module-federation-shared-dependencies__'] = { - "react": { - "version": "18.2.0", - "requiredVersion": "^18.0.0", - "singleton": true, - "eager": false, - "module": () => import('react') - } -};`); - }); - - it('should handle empty object', () => { - const sharedDependencies = {}; - - const result = prepareRuntimeSharedDependenciesScript(sharedDependencies); - - expect(result).toBe( - `window['__backstage-module-federation-shared-dependencies__'] = {};`, - ); - }); - - it('should handle scoped package names and special characters', () => { - const sharedDependencies = { - '@backstage/core-plugin-api': { - version: '1.0.0', - requiredVersion: '*', - }, - }; - - const result = prepareRuntimeSharedDependenciesScript(sharedDependencies); - - expect(result) - .toBe(`window['__backstage-module-federation-shared-dependencies__'] = { - "@backstage/core-plugin-api": { - "version": "1.0.0", - "requiredVersion": "*", - "module": () => import('@backstage/core-plugin-api') - } -};`); - }); -}); - -const globalSpy = jest.fn(); -Object.defineProperty( - global, - '__backstage-module-federation-shared-dependencies__', - { - get: globalSpy, - }, -); - -describe('getRuntimeSharedDependencies', () => { - afterEach(jest.resetAllMocks); - - it('should get runtime shared dependencies with minimal required properties', async () => { - const reactMock = { default: { React: 'react' } }; - const reactDomMock = { default: { ReactDom: 'react-dom' } }; - - globalSpy.mockReturnValue({ - react: { - version: '18.2.0', - requiredVersion: '*', - module: async () => reactMock, - }, - 'react-dom': { - version: '18.2.0', - requiredVersion: '*', - module: async () => reactDomMock, - }, - }); - const result = await buildRuntimeSharedUserOption(); - - expect(result.errors).toEqual([]); - expect(result.shared).toEqual({ - react: { - version: '18.2.0', - lib: expect.any(Function), - shareConfig: { - requiredVersion: '*', - }, - }, - 'react-dom': { - version: '18.2.0', - lib: expect.any(Function), - shareConfig: { - requiredVersion: '*', - }, - }, - }); - - // Test that lib functions return the correct modules - expect((result?.shared?.react as { lib: () => any }).lib()).toBe(reactMock); - expect((result?.shared?.['react-dom'] as { lib: () => any }).lib()).toBe( - reactDomMock, - ); - }); - - it('should get runtime shared dependencies with custom version and requiredVersion', async () => { - globalSpy.mockReturnValue({ - react: { - module: async () => ({ default: {} }), - version: '18.2.0', - requiredVersion: '^18.0.0', - }, - lodash: { - module: async () => ({ default: {} }), - version: '4.17.21', - requiredVersion: '^4.17.0', - }, - }); - const result = await buildRuntimeSharedUserOption(); - - expect(result.errors).toEqual([]); - expect(result.shared).toEqual({ - react: { - version: '18.2.0', - lib: expect.any(Function), - shareConfig: { - requiredVersion: '^18.0.0', - }, - }, - lodash: { - version: '4.17.21', - lib: expect.any(Function), - shareConfig: { - requiredVersion: '^4.17.0', - }, - }, - }); - }); - - it('should handle eager', async () => { - globalSpy.mockReturnValue({ - react: { - version: '18.2.0', - requiredVersion: '*', - eager: false, - module: async () => ({ default: {} }), - }, - lodash: { - version: '4.17.21', - requiredVersion: '*', - eager: true, - module: async () => ({ default: {} }), - }, - }); - const result = await buildRuntimeSharedUserOption(); - - expect(result.errors).toEqual([]); - expect(result.shared).toEqual({ - react: { - version: '18.2.0', - lib: expect.any(Function), - shareConfig: { - requiredVersion: '*', - eager: false, - }, - }, - lodash: { - version: '4.17.21', - lib: expect.any(Function), - shareConfig: { - requiredVersion: '*', - eager: true, - }, - }, - }); - }); - - it('should handle singleton', async () => { - globalSpy.mockReturnValue({ - react: { - module: async () => ({ default: {} }), - version: '18.2.0', - requiredVersion: '*', - singleton: true, - }, - lodash: { - module: async () => ({ default: {} }), - version: '4.17.21', - requiredVersion: '*', - singleton: false, - }, - }); - const result = await buildRuntimeSharedUserOption(); - - expect(result.errors).toEqual([]); - expect(result.shared).toEqual({ - react: { - version: '18.2.0', - lib: expect.any(Function), - shareConfig: { - requiredVersion: '*', - singleton: true, - }, - }, - lodash: { - version: '4.17.21', - lib: expect.any(Function), - shareConfig: { - requiredVersion: '*', - singleton: false, - }, - }, - }); - }); - - it('should handle an empty object', async () => { - globalSpy.mockReturnValue({}); - const result = await buildRuntimeSharedUserOption(); - - expect(result.errors).toEqual([]); - expect(result.shared).toEqual({}); - }); - - it('should handle module import failures and collect errors', async () => { - const mockError = new Error('Module import failed'); - const reactMock = { default: { React: 'react' } }; - const lodashMock = { default: { _: 'lodash' } }; - - globalSpy.mockReturnValue({ - react: { - module: async () => reactMock, - version: '18.2.0', - requiredVersion: '^18.0.0', - }, - 'failing-module': { - module: async () => { - throw mockError; - }, - }, - lodash: { - module: async () => lodashMock, - version: '4.17.21', - requiredVersion: '^4.17.0', - }, - }); - - const result = await buildRuntimeSharedUserOption(); - - expect(result.errors).toHaveLength(1); - expect(result.errors[0]).toBeInstanceOf(ForwardedError); - expect(result.errors[0].message).toContain( - 'Failed to dynamically import "failing-module" and add it to module federation shared dependencies:', - ); - expect(result.errors[0].cause).toBe(mockError); - - // Should still include successful modules - expect(result.shared).toEqual({ - react: { - version: '18.2.0', - lib: expect.any(Function), - shareConfig: { - requiredVersion: '^18.0.0', - }, - }, - lodash: { - version: '4.17.21', - lib: expect.any(Function), - shareConfig: { - requiredVersion: '^4.17.0', - }, - }, - }); - - // Test that lib functions work correctly - expect((result?.shared?.react as { lib: () => any }).lib()).toBe(reactMock); - expect((result?.shared?.lodash as { lib: () => any }).lib()).toBe( - lodashMock, - ); - }); - - it('should handle multiple module import failures', async () => { - const mockError1 = new Error('First module failed'); - const mockError2 = new Error('Second module failed'); - globalSpy.mockReturnValue({ - 'failing-module-1': { - module: async () => { - throw mockError1; - }, - }, - 'failing-module-2': { - module: async () => { - throw mockError2; - }, - }, - }); - - const result = await buildRuntimeSharedUserOption(); - - expect(result.errors).toHaveLength(2); - expect(result.errors[0]).toBeInstanceOf(ForwardedError); - expect(result.errors[0].message).toContain('failing-module-1'); - expect(result.errors[0].cause).toBe(mockError1); - expect(result.errors[1]).toBeInstanceOf(ForwardedError); - expect(result.errors[1].message).toContain('failing-module-2'); - expect(result.errors[1].cause).toBe(mockError2); - - expect(result.shared).toEqual({}); - }); -}); diff --git a/packages/module-federation-common/src/runtime.ts b/packages/module-federation-common/src/runtime.ts deleted file mode 100644 index dada9dcf46..0000000000 --- a/packages/module-federation-common/src/runtime.ts +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2025 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 { Host, Runtime, SharedDependencies } from './types'; -import type { UserOptions } from '@module-federation/runtime/types'; -import { ForwardedError } from '@backstage/errors'; - -const BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL = - '__backstage-module-federation-shared-dependencies__'; - -/** - * Prepares the runtime shared dependencies script for the module federation host, - * which will be written by the CLI into a Javascript file added as an additional entry point for the frontend bundler. - * This script is used in the browser to build the list of shared dependencies provided to the module federation runtime. - * - * @see {@link buildRuntimeSharedUserOption} - * - * @param hostSharedDependencies - The shared dependencies for the module federation host. - * @returns The runtime shared dependencies script. - * - * @public - */ -export function prepareRuntimeSharedDependenciesScript( - hostSharedDependencies: SharedDependencies, -) { - type StringModule = Omit & { module: string }; - const runtimeSharedDependencies: SharedDependencies = - Object.fromEntries( - Object.entries(hostSharedDependencies).map(([name, sharedDep]) => [ - name, - { - version: sharedDep.version, - requiredVersion: sharedDep.requiredVersion, - ...(sharedDep.singleton !== undefined - ? { singleton: sharedDep.singleton } - : {}), - ...(sharedDep.eager !== undefined ? { eager: sharedDep.eager } : {}), - module: `() => import('${name}')`, - }, - ]), - ); - - return `window['${BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL}'] = ${JSON.stringify( - runtimeSharedDependencies, - null, - 2, - ).replace( - /(^\s+"module":\s*)"([^"]+)"$/gm, - (_, start, unquoted) => `${start}${unquoted}`, - )};`; -} - -/** - * Builds the list of shared dependencies provided to the module federation runtime. - * It uses the runtime shared dependencies script prepared by {@link prepareRuntimeSharedDependenciesScript}. - * - * @public - */ -export async function buildRuntimeSharedUserOption(): Promise<{ - shared: UserOptions['shared']; - errors: ForwardedError[]; -}> { - const runtimeSharedDependencies = - ( - window as { - [BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL]?: SharedDependencies< - Host & Runtime - >; - } - )[BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL] ?? {}; - - const result: UserOptions['shared'] = {}; - const errors: ForwardedError[] = []; - for (const [name, sharedDep] of Object.entries(runtimeSharedDependencies)) { - try { - const module = await sharedDep.module(); - result[name] = { - version: sharedDep.version, - lib: () => module, - shareConfig: { - singleton: sharedDep.singleton, - requiredVersion: sharedDep.requiredVersion, - eager: sharedDep.eager, - }, - }; - } catch (e) { - errors.push( - new ForwardedError( - `Failed to dynamically import "${name}" and add it to module federation shared dependencies:`, - e, - ), - ); - } - } - - return { shared: result, errors }; -} diff --git a/packages/module-federation-common/src/types.ts b/packages/module-federation-common/src/types.ts index e8070990df..ff528d0844 100644 --- a/packages/module-federation-common/src/types.ts +++ b/packages/module-federation-common/src/types.ts @@ -15,55 +15,79 @@ */ /** - * Generic type for shared dependencies configuration in module federation. - * - * The ContextFields type parameter is used to provide additional fields - * to the shared dependencies configuration according to the context. + * The name of the global object used to pass shared dependencies from the CLI to the module federation runtime. * * @public */ -export type SharedDependencies = { +export const BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL = + '__@backstage/module_federation_shared_dependencies__'; + +/** + * Shared dependencies configuration in module federation remote modules. + * + * @public + */ +export type RemoteSharedDependencies = { [name: string]: { - /** Whether this dependency should be a singleton */ - singleton?: boolean; - } & ContextFields; + /** Whether to share this dependency as a singleton */ + singleton: boolean; + /** Whether to load this dependency eagerly */ + eager: boolean; + /** Provided module for fallback. Set to false to not provide a fallback, or a custom import path. */ + import?: false | string; + /** Required version range. */ + requiredVersion: string; + }; }; /** - * Specific fields for shared dependencies configured in module federation remote modules. + * Shared dependencies configuration in module federation host modules. * * @public */ -export type Remote = { - /** Provided module for fallback. Set to false to not provide a fallback, or a custom import path. */ - import?: false | string; - /** Required version range. Optional for remotes - can be auto-filled from package.json at build time. */ - requiredVersion?: false | string; - /** Version of the shared dependency. Will be resolved at build time by default but can be overridden, but not completely removed. */ - version?: false | string; +export type HostSharedDependencies = { + [name: string]: { + /** Whether to share this dependency as a singleton */ + singleton: boolean; + /** Whether to load this dependency eagerly */ + eager: boolean; + /** Required version range. */ + requiredVersion: string; + /** Version of the shared dependency. Will be resolved at build time by default but can be overridden, but not completely removed. */ + version?: string; + }; }; /** - * Specific fields for shared dependencies configured in the module federation host. + * A single shared runtime dependency from the CLI that has been loaded and ready to be used in the module federation runtime. * * @public */ -export type Host = { - /** Whether to load this dependency eagerly */ - eager?: boolean; - /** Required version range. Required for host. */ - requiredVersion: false | string; - /** Version of the shared dependency. Will be resolved at build time by default but can be overridden, but not completely removed. */ - version?: string; -}; - -/** - * Specific fields for shared dependencies configured when bootstrapping the module federation host at runtime. - * - * @internal - */ -export type Runtime = { - // version is always expected for runtime host dependencies +export type LoadedRuntimeSharedDependency = { version: string; - module: () => Promise; + lib: () => unknown; + shareConfig: HostSharedDependencies[string]; +}; + +/** + * The global object used to pass shared dependencies from the CLI to the module federation runtime. + * + * @remarks + * + * Stored in {@link BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL}. + * + * @public + */ +export type RuntimeSharedDependenciesGlobal = { + items: Array<{ + name: string; + version: string; + lib: () => Promise; + shareConfig: { + singleton: boolean; + requiredVersion: string; + eager: boolean; + }; + }>; + version: 'v1'; }; From 4137a43a1b3e97616d78900370f3587324d1ef1d Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Fri, 13 Feb 2026 13:11:29 +0000 Subject: [PATCH 02/10] Rename and remove CSS design tokens Signed-off-by: Charles de Dreuille --- .changeset/five-ends-design.md | 5 ++++ .changeset/icy-houses-cough.md | 5 ++++ .changeset/plain-deer-sneeze.md | 21 +++++++++++++++ .storybook/preview.tsx | 4 +-- .storybook/storybook.css | 12 ++++----- .storybook/themes/spotify.css | 18 +++++-------- docs-ui/src/app/tokens/page.mdx | 26 ++++++++++++------- .../components/ColorFamily/ColorFamily.tsx | 2 +- .../src/components/Snippet/styles.module.css | 4 +-- docs-ui/src/css/theme-backstage.css | 16 ++++-------- docs-ui/src/css/theme-spotify.css | 16 ++++-------- docs-ui/src/snippets/code-snippets.ts | 4 +-- docs/conf/user-interface/index.md | 21 +++++++-------- docs/releases/v1.48.0-next.2-changelog.md | 2 +- packages/ui/CHANGELOG.md | 2 +- .../src/components/Avatar/Avatar.module.css | 2 +- .../components/Checkbox/Checkbox.module.css | 9 ++----- .../src/components/Dialog/Dialog.module.css | 6 ++--- .../src/components/Header/Header.module.css | 6 ++--- .../ui/src/components/Menu/Menu.module.css | 6 ++--- .../PasswordField/PasswordField.module.css | 12 +-------- .../src/components/Popover/Popover.module.css | 18 ++++++------- .../RadioGroup/RadioGroup.module.css | 14 ++-------- .../SearchField/SearchField.module.css | 12 +-------- .../src/components/Select/Select.module.css | 15 ++--------- .../ui/src/components/Table/Table.module.css | 4 +-- .../components/TextField/TextField.module.css | 17 +----------- .../ToggleButtonGroup.module.css | 6 ++--- .../src/components/Tooltip/Tooltip.module.css | 4 +-- packages/ui/src/css/colors.stories.tsx | 2 +- packages/ui/src/css/core.css | 2 +- packages/ui/src/css/tokens.css | 18 ++++++------- packages/ui/src/css/utilities/sm.css | 3 +-- packages/ui/src/css/utilities/xs.css | 3 +-- .../AppVisualizerPage/DetailedVisualizer.tsx | 2 +- .../AppVisualizerPage/TextVisualizer.tsx | 2 +- .../BuiThemerPage/BuiThemePreview.test.tsx | 2 +- .../BuiThemerPage/BuiThemePreview.tsx | 2 +- .../components/BuiThemerPage/ThemeContent.tsx | 2 +- .../convertMuiToBuiTheme.test.ts | 4 +-- .../BuiThemerPage/convertMuiToBuiTheme.ts | 4 +-- 41 files changed, 144 insertions(+), 191 deletions(-) create mode 100644 .changeset/five-ends-design.md create mode 100644 .changeset/icy-houses-cough.md create mode 100644 .changeset/plain-deer-sneeze.md diff --git a/.changeset/five-ends-design.md b/.changeset/five-ends-design.md new file mode 100644 index 0000000000..47363ed07d --- /dev/null +++ b/.changeset/five-ends-design.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-mui-to-bui': patch +--- + +Updated CSS token references to use renamed `--bui-bg-app` and `--bui-border-2` tokens. diff --git a/.changeset/icy-houses-cough.md b/.changeset/icy-houses-cough.md new file mode 100644 index 0000000000..bd686403bf --- /dev/null +++ b/.changeset/icy-houses-cough.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-app-visualizer': patch +--- + +Updated CSS token references to use renamed `--bui-border-2` token. diff --git a/.changeset/plain-deer-sneeze.md b/.changeset/plain-deer-sneeze.md new file mode 100644 index 0000000000..bb544740ee --- /dev/null +++ b/.changeset/plain-deer-sneeze.md @@ -0,0 +1,21 @@ +--- +'@backstage/ui': minor +--- + +**BREAKING:** Renamed and removed CSS tokens. + +- Renamed `--bui-bg-neutral-0` to `--bui-bg-app`. +- Renamed `--bui-border` to `--bui-border-2`. +- Removed `--bui-border-hover`, `--bui-border-pressed`, and `--bui-border-disabled`. + +**Migration:** + +```diff +- var(--bui-bg-neutral-0) ++ var(--bui-bg-app) + +- var(--bui-border) ++ var(--bui-border-2) +``` + +Remove any references to `--bui-border-hover`, `--bui-border-pressed`, and `--bui-border-disabled` as these tokens no longer exist. diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index dff52c85d6..7be5089893 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -132,11 +132,11 @@ export default definePreview({ }; }, [selectedTheme, selectedThemeName]); - document.body.style.backgroundColor = 'var(--bui-bg-neutral-0)'; + document.body.style.backgroundColor = 'var(--bui-bg-app)'; const docsStoryElements = document.getElementsByClassName('docs-story'); Array.from(docsStoryElements).forEach(element => { (element as HTMLElement).style.backgroundColor = - 'var(--bui-bg-neutral-0)'; + 'var(--bui-bg-app)'; }); return ( diff --git a/.storybook/storybook.css b/.storybook/storybook.css index 617af88ba2..e0f0b68678 100644 --- a/.storybook/storybook.css +++ b/.storybook/storybook.css @@ -5,10 +5,10 @@ --sb-panel-left: 0; --sb-panel-right: 0; --sb-sidebar-border: none; - --sb-sidebar-border-right: 1px solid var(--bui-border); + --sb-sidebar-border-right: 1px solid var(--bui-border-2); --sb-sidebar-bg: #000; --sb-options-border: none; - --sb-options-border-left: 1px solid var(--bui-border); + --sb-options-border-left: 1px solid var(--bui-border-2); --sb-content-padding-inline: 250px; } @@ -31,8 +31,8 @@ } [data-theme-name='spotify'][data-theme-mode='light'] { - --sb-sidebar-border: 1px solid var(--bui-border); - --sb-sidebar-border-right: 1px solid var(--bui-border); - --sb-options-border: 1px solid var(--bui-border); - --sb-options-border-left: 1px solid var(--bui-border); + --sb-sidebar-border: 1px solid var(--bui-border-2); + --sb-sidebar-border-right: 1px solid var(--bui-border-2); + --sb-options-border: 1px solid var(--bui-border-2); + --sb-options-border-left: 1px solid var(--bui-border-2); } diff --git a/.storybook/themes/spotify.css b/.storybook/themes/spotify.css index c52062fe96..b72683f53e 100644 --- a/.storybook/themes/spotify.css +++ b/.storybook/themes/spotify.css @@ -174,7 +174,7 @@ } .bui-TableHeader .bui-TableRow { - border-bottom: 1px solid var(--bui-border); + border-bottom: 1px solid var(--bui-border-2); } .bui-TableHead { @@ -226,10 +226,7 @@ --bui-fg-solid: var(--bui-black); --bui-fg-solid-disabled: #62ab7c; - --bui-border: #d9d9d9; - --bui-border-hover: rgba(0, 0, 0, 0.3); - --bui-border-pressed: rgba(0, 0, 0, 0.5); - --bui-border-disabled: rgba(0, 0, 0, 0.1); + --bui-border-2: #d9d9d9; --bui-border-danger: #f87a7a; --bui-border-warning: #e36d05; --bui-border-success: #53db83; @@ -237,16 +234,16 @@ --bui-ring: rgba(0, 0, 0, 0.2); .bui-HeaderToolbarWrapper { - border: 1px solid var(--bui-border); + border: 1px solid var(--bui-border-2); } .bui-HeaderTabsWrapper { - border: 1px solid var(--bui-border); + border: 1px solid var(--bui-border-2); } } [data-theme-mode='dark'][data-theme-name='spotify'] { - --bui-bg-neutral-0: var(--bui-black); + --bui-bg-app: var(--bui-black); --bui-bg-solid: #1ed760; --bui-bg-solid-hover: #3be477; @@ -266,10 +263,7 @@ --bui-fg-warning: #e36d05; --bui-fg-success: #1db954; - --bui-border: #373737; - --bui-border-hover: rgba(255, 255, 255, 0.4); - --bui-border-pressed: rgba(255, 255, 255, 0.5); - --bui-border-disabled: rgba(255, 255, 255, 0.2); + --bui-border-2: #373737; --bui-border-danger: #f87a7a; --bui-border-warning: #e36d05; --bui-border-success: #53db83; diff --git a/docs-ui/src/app/tokens/page.mdx b/docs-ui/src/app/tokens/page.mdx index 46d67ded95..6d9b5e65ca 100644 --- a/docs-ui/src/app/tokens/page.mdx +++ b/docs-ui/src/app/tokens/page.mdx @@ -137,7 +137,7 @@ the value, you add an object with the value and the breakpoint prefix. ## Neutral background colors These colors form a layered neutral scale for your application backgrounds. -`--bui-bg-neutral-0` is the base background color of your app. Each subsequent level +`--bui-bg-app` is the base background color of your app. Each subsequent level (1 through 4) represents an elevated layer on top of the previous one, with hover, pressed, and disabled variants for interactive states. @@ -151,7 +151,7 @@ pressed, and disabled variants for interactive states. - --bui-bg-neutral-0 + --bui-bg-app The base background color of your Backstage instance. @@ -462,13 +462,19 @@ low contrast to help as a separator with the different background colors. - --bui-border + --bui-border-1 + + Subtle border for low-contrast separators. + + + + --bui-border-2 It should be used on top of `--bui-bg-neutral-1`. - --bui-border-hover + --bui-border-2-hover Used when the component is interactive and hovered. @@ -476,7 +482,7 @@ low contrast to help as a separator with the different background colors. - --bui-border-pressed + --bui-border-2-pressed Used when the component is interactive and focused. @@ -484,31 +490,31 @@ low contrast to help as a separator with the different background colors. - --bui-border-disabled + --bui-border-2-disabled Used when the component is disabled. - --bui-border-danger + --bui-border-2-danger It should be used on top of `--bui-bg-danger`. - --bui-border-warning + --bui-border-2-warning It should be used on top of `--bui-bg-warning`. - --bui-border-success + --bui-border-2-success It should be used on top of `--bui-bg-success`. - --bui-border-info + --bui-border-2-info It should be used on top of `--bui-bg-info`. diff --git a/docs-ui/src/components/ColorFamily/ColorFamily.tsx b/docs-ui/src/components/ColorFamily/ColorFamily.tsx index 0e168f0bbe..07b8779503 100644 --- a/docs-ui/src/components/ColorFamily/ColorFamily.tsx +++ b/docs-ui/src/components/ColorFamily/ColorFamily.tsx @@ -55,7 +55,7 @@ export const ColorFamily = () => {
Neutral 0 diff --git a/docs-ui/src/components/Snippet/styles.module.css b/docs-ui/src/components/Snippet/styles.module.css index 01ed111b91..c6270670a7 100644 --- a/docs-ui/src/components/Snippet/styles.module.css +++ b/docs-ui/src/components/Snippet/styles.module.css @@ -7,7 +7,7 @@ .preview { border-radius: 8px; box-shadow: inset 0 0 0 1px var(--border); - background-color: var(--bui-bg-neutral-0); + background-color: var(--bui-bg-app); padding: 1px; position: relative; } @@ -90,7 +90,7 @@ .sideBySidePreview { border-radius: 8px; box-shadow: inset 0 0 0 1px var(--border); - background-color: var(--bui-bg-neutral-0); + background-color: var(--bui-bg-app); padding: 1px; min-width: 0; display: flex; diff --git a/docs-ui/src/css/theme-backstage.css b/docs-ui/src/css/theme-backstage.css index 96861993ee..7316fe0549 100644 --- a/docs-ui/src/css/theme-backstage.css +++ b/docs-ui/src/css/theme-backstage.css @@ -45,7 +45,7 @@ --bui-bg-solid-hover: #163a66; --bui-bg-solid-pressed: #0f2b4e; --bui-bg-solid-disabled: #163a66; - --bui-bg-neutral-0: #f8f8f8; + --bui-bg-app: #f8f8f8; --bui-bg-neutral-1: #fff; --bui-bg-neutral-1-hover: oklch(0% 0 0/0.12); --bui-bg-neutral-1-pressed: oklch(0% 0 0/0.16); @@ -79,10 +79,7 @@ --bui-fg-warning: #ef7a32; --bui-fg-success: #1ed760; --bui-fg-info: #0d74ce; - --bui-border: #0000001a; - --bui-border-hover: #0003; - --bui-border-pressed: #0006; - --bui-border-disabled: #0000001a; + --bui-border-2: #0000001a; --bui-border-info: #7ea9d6; --bui-border-danger: #f87a7a; --bui-border-warning: #e36d05; @@ -98,7 +95,7 @@ --bui-bg-solid-hover: #83b9fd; --bui-bg-solid-pressed: #83b9fd; --bui-bg-solid-disabled: #1b3d68; - --bui-bg-neutral-0: #333; + --bui-bg-app: #333; --bui-bg-neutral-1: oklch(100% 0 0/0.1); --bui-bg-neutral-1-hover: oklch(100% 0 0/0.14); --bui-bg-neutral-1-pressed: oklch(100% 0 0/0.2); @@ -132,10 +129,7 @@ --bui-fg-warning: #ffa057; --bui-fg-success: #1ed760; --bui-fg-info: #70b8ff; - --bui-border: #ffffff1f; - --bui-border-hover: #fff6; - --bui-border-pressed: #ffffff80; - --bui-border-disabled: #fff3; + --bui-border-2: #ffffff1f; --bui-border-info: #7ea9d6; --bui-border-danger: #f87a7a; --bui-border-warning: #e36d05; @@ -243,7 +237,7 @@ } } body { - background-color: var(--bui-bg-neutral-0); + background-color: var(--bui-bg-app); color: var(--bui-fg-primary); font-family: var(--bui-font-regular); font-weight: var(--bui-font-weight-regular); diff --git a/docs-ui/src/css/theme-spotify.css b/docs-ui/src/css/theme-spotify.css index 73a067d5b0..64afbb6d75 100644 --- a/docs-ui/src/css/theme-spotify.css +++ b/docs-ui/src/css/theme-spotify.css @@ -156,7 +156,7 @@ display: block; } & .bui-TableHeader .bui-TableRow { - border-bottom: 1px solid var(--bui-border); + border-bottom: 1px solid var(--bui-border-2); } & .bui-TableHead { font-size: var(--bui-font-size-2); @@ -198,21 +198,18 @@ --bui-fg-secondary: #757575; --bui-fg-solid: var(--bui-black); --bui-fg-solid-disabled: #62ab7c; - --bui-border: #d9d9d9; - --bui-border-hover: #0000004d; - --bui-border-pressed: #00000080; - --bui-border-disabled: #0000001a; + --bui-border-2: #d9d9d9; --bui-border-danger: #f87a7a; --bui-border-warning: #e36d05; --bui-border-success: #53db83; --bui-ring: #0003; & .bui-HeaderToolbarWrapper, & .bui-HeaderTabsWrapper { - border: 1px solid var(--bui-border); + border: 1px solid var(--bui-border-2); } } [data-theme-mode='dark'][data-theme-name='spotify'] { - --bui-bg-neutral-0: var(--bui-black); + --bui-bg-app: var(--bui-black); --bui-bg-solid: #1ed760; --bui-bg-solid-hover: #3be477; --bui-bg-solid-pressed: #1abc54; @@ -228,10 +225,7 @@ --bui-fg-danger: #e22b2b; --bui-fg-warning: #e36d05; --bui-fg-success: #1db954; - --bui-border: #373737; - --bui-border-hover: #fff6; - --bui-border-pressed: #ffffff80; - --bui-border-disabled: #fff3; + --bui-border-2: #373737; --bui-border-danger: #f87a7a; --bui-border-warning: #e36d05; --bui-border-success: #53db83; diff --git a/docs-ui/src/snippets/code-snippets.ts b/docs-ui/src/snippets/code-snippets.ts index 92a9cfe2db..30d036461f 100644 --- a/docs-ui/src/snippets/code-snippets.ts +++ b/docs-ui/src/snippets/code-snippets.ts @@ -4,7 +4,7 @@ export const customTheme = `:root { --bui-font-regular: system-ui; --bui-font-weight-regular: 400; --bui-font-weight-bold: 600; - --bui-bg-neutral-0: #f8f8f8; + --bui-bg-app: #f8f8f8; --bui-bg-neutral-1: #fff; /* ... other CSS variables */ @@ -19,7 +19,7 @@ export const customTheme = `:root { --bui-font-regular: system-ui; --bui-font-weight-regular: 400; --bui-font-weight-bold: 600; - --bui-bg-neutral-0: #f8f8f8; + --bui-bg-app: #f8f8f8; --bui-bg-neutral-1: #fff; /* ... other CSS variables */ diff --git a/docs/conf/user-interface/index.md b/docs/conf/user-interface/index.md index abe74dddef..a70026a759 100644 --- a/docs/conf/user-interface/index.md +++ b/docs/conf/user-interface/index.md @@ -103,13 +103,13 @@ Backstage UI is using light by default under `:root` but you can target it more [data-theme-mode='light'] { /* Light theme specific styles */ -  --bui-bg-neutral-0: #f8f8f8; +  --bui-bg-app: #f8f8f8; --bui-fg-primary: #000; } [data-theme-mode='dark'] { /* Dark theme specific styles */ -  --bui-bg-neutral-0: #333333; +  --bui-bg-app: #333333; --bui-fg-primary: #fff; } ``` @@ -124,8 +124,8 @@ And if you’d like to go even further, you can target specific component class | Token Name | Description | | -------------------- | --------------------------------------------------------------------------------------------- | -| `--bui-bg-neutral-0` | This is used to define the background color of your app. It will only be used once. | -| `--bui-bg-neutral-1` | We ar using this color to sit on top of `--bui-bg-neutral-0` mostly for `Card`, `Dialog`, ... | +| `--bui-bg-app` | This is used to define the background color of your app. It will only be used once. | +| `--bui-bg-neutral-1` | We ar using this color to sit on top of `--bui-bg-app` mostly for `Card`, `Dialog`, ... | | `--bui-bg-neutral-2` | This is for content inside elevated components. This colour is less common. | | `--bui-bg-solid` | This is used for main actions like primary buttons. | | `--bui-fg-solid` | This is for texts or icons on top of a solid backgrounds. | @@ -135,7 +135,8 @@ And if you’d like to go even further, you can target specific component class | `--bui-fg-warning` | Used for warning states and cautionary information. | | `--bui-fg-success` | Used for success states and positive feedback. | | `--bui-fg-info` | Used for informational content and neutral status. | -| `--bui-border` | Main borders around surfaces like `Card`, `Dialog`, ... | +| `--bui-border-1` | Subtle borders for low-contrast separators. | +| `--bui-border-2` | Main borders around surfaces like `Card`, `Dialog`, ... | | `--bui-font-regular` | The main font of your app. |
@@ -150,11 +151,11 @@ And if you’d like to go even further, you can target specific component class #### Neutral background colors -These colors form a layered neutral scale for your application backgrounds. `--bui-bg-neutral-0` is the base background color. Each subsequent level (1 through 4) represents an elevated layer, with hover, pressed, and disabled variants for interactive states. +These colors form a layered neutral scale for your application backgrounds. `--bui-bg-app` is the base background color. Each subsequent level (1 through 4) represents an elevated layer, with hover, pressed, and disabled variants for interactive states. | Token Name | Description | | ----------------------------- | ------------------------------------------------------------ | -| `--bui-bg-neutral-0` | The base background color of your Backstage instance. | +| `--bui-bg-app` | The base background color of your Backstage instance. | | `--bui-bg-neutral-1` | First elevated layer. Use for cards, dialogs, and panels. | | `--bui-bg-neutral-1-hover` | Hover state for elements on neutral-1. | | `--bui-bg-neutral-1-pressed` | Pressed state for elements on neutral-1. | @@ -215,10 +216,8 @@ These border colors are mostly meant to be used as borders on top of any compone | Token Name | Description | | ----------------------- | --------------------------------------------------- | -| `--bui-border` | It should be used on top of `--bui-bg-neutral-1`. | -| `--bui-border-hover` | Used when the component is interactive and hovered. | -| `--bui-border-pressed` | Used when the component is interactive and hovered. | -| `--bui-border-disabled` | Used when the component is disabled. | +| `--bui-border-1` | Subtle border for low-contrast separators. | +| `--bui-border-2` | It should be used on top of `--bui-bg-neutral-1`. | | `--bui-border-danger` | It should be used on top of `--bui-bg-danger`. | | `--bui-border-warning` | It should be used on top of `--bui-bg-warning`. | | `--bui-border-success` | It should be used on top of `--bui-bg-success`. | diff --git a/docs/releases/v1.48.0-next.2-changelog.md b/docs/releases/v1.48.0-next.2-changelog.md index a9f60b13d0..5d041b559c 100644 --- a/docs/releases/v1.48.0-next.2-changelog.md +++ b/docs/releases/v1.48.0-next.2-changelog.md @@ -90,7 +90,7 @@ Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.48.0-next.2](h ```diff - background: var(--bui-bg-surface-0); - + background: var(--bui-bg-neutral-0); + + background: var(--bui-bg-app); ``` Replace on-surface tokens shifted by +1: diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index a22c5ccc3a..7b42996707 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -14,7 +14,7 @@ ```diff - background: var(--bui-bg-surface-0); - + background: var(--bui-bg-neutral-0); + + background: var(--bui-bg-app); ``` Replace on-surface tokens shifted by +1: diff --git a/packages/ui/src/components/Avatar/Avatar.module.css b/packages/ui/src/components/Avatar/Avatar.module.css index ffaad2d18f..56c68c7100 100644 --- a/packages/ui/src/components/Avatar/Avatar.module.css +++ b/packages/ui/src/components/Avatar/Avatar.module.css @@ -74,7 +74,7 @@ width: 100%; font-size: var(--bui-font-size-3); font-weight: var(--bui-font-weight-regular); - box-shadow: inset 0 0 0 1px var(--bui-border); + box-shadow: inset 0 0 0 1px var(--bui-border-2); border-radius: var(--bui-radius-full); } } diff --git a/packages/ui/src/components/Checkbox/Checkbox.module.css b/packages/ui/src/components/Checkbox/Checkbox.module.css index 0b6613f927..deadc96358 100644 --- a/packages/ui/src/components/Checkbox/Checkbox.module.css +++ b/packages/ui/src/components/Checkbox/Checkbox.module.css @@ -42,7 +42,7 @@ justify-content: center; width: 1rem; height: 1rem; - box-shadow: inset 0 0 0 1px var(--bui-border); + box-shadow: inset 0 0 0 1px var(--bui-border-2); border-radius: 2px; transition: background-color 0.2s ease-in-out; background-color: var(--bui-bg-neutral-1); @@ -64,15 +64,10 @@ .bui-Checkbox[data-indeterminate] & { background-color: var(--bui-bg-neutral-1); - box-shadow: inset 0 0 0 1px var(--bui-border); + box-shadow: inset 0 0 0 1px var(--bui-border-2); color: var(--bui-fg-primary); } - .bui-Checkbox[data-hovered]:not([data-selected]):not([data-indeterminate]) - & { - box-shadow: inset 0 0 0 1px var(--bui-border-hover); - } - @media (prefers-reduced-motion: reduce) { & { transition: none; diff --git a/packages/ui/src/components/Dialog/Dialog.module.css b/packages/ui/src/components/Dialog/Dialog.module.css index 201f25ed3a..cd0351cc4a 100644 --- a/packages/ui/src/components/Dialog/Dialog.module.css +++ b/packages/ui/src/components/Dialog/Dialog.module.css @@ -45,7 +45,7 @@ .bui-Dialog { background: var(--bui-bg-neutral-1); border-radius: 0.5rem; - border: 1px solid var(--bui-border); + border: 1px solid var(--bui-border-2); color: var(--bui-fg-primary); position: relative; width: min(var(--bui-dialog-min-width, 400px), calc(100vw - 3rem)); @@ -73,7 +73,7 @@ align-items: center; padding-inline: var(--bui-space-3); padding-block: var(--bui-space-2); - border-bottom: 1px solid var(--bui-border); + border-bottom: 1px solid var(--bui-border-2); } .bui-DialogHeaderTitle { @@ -89,7 +89,7 @@ gap: var(--bui-space-2); padding-inline: var(--bui-space-3); padding-block: var(--bui-space-3); - border-top: 1px solid var(--bui-border); + border-top: 1px solid var(--bui-border-2); } .bui-DialogBody { diff --git a/packages/ui/src/components/Header/Header.module.css b/packages/ui/src/components/Header/Header.module.css index 4cd94d30ea..ec00d85f86 100644 --- a/packages/ui/src/components/Header/Header.module.css +++ b/packages/ui/src/components/Header/Header.module.css @@ -29,7 +29,7 @@ left: 0px; right: 0px; height: 16px; - background-color: var(--bui-bg-neutral-0); + background-color: var(--bui-bg-app); z-index: 0; } } @@ -43,7 +43,7 @@ justify-content: space-between; background-color: var(--bui-bg-neutral-1); padding-inline: var(--bui-space-5); - border-bottom: 1px solid var(--bui-border); + border-bottom: 1px solid var(--bui-border-2); color: var(--bui-fg-primary); height: 52px; } @@ -89,7 +89,7 @@ .bui-HeaderTabsWrapper { padding-inline: var(--bui-space-3); - border-bottom: 1px solid var(--bui-border); + border-bottom: 1px solid var(--bui-border-2); background-color: var(--bui-bg-neutral-1); } } diff --git a/packages/ui/src/components/Menu/Menu.module.css b/packages/ui/src/components/Menu/Menu.module.css index 759bfb9043..d21806b506 100644 --- a/packages/ui/src/components/Menu/Menu.module.css +++ b/packages/ui/src/components/Menu/Menu.module.css @@ -21,7 +21,7 @@ display: flex; flex-direction: column; box-shadow: var(--bui-shadow); - border: 1px solid var(--bui-border); + border: 1px solid var(--bui-border-2); border-radius: var(--bui-radius-2); background: var(--bui-bg-neutral-1); color: var(--bui-fg-primary); @@ -183,7 +183,7 @@ .bui-MenuSeparator { height: 1px; - background: var(--bui-border); + background: var(--bui-border-2); margin-inline: var(--bui-space-1_5); margin-block: var(--bui-space-1); } @@ -194,7 +194,7 @@ flex-shrink: 0; display: flex; align-items: center; - border-bottom: 1px solid var(--bui-border); + border-bottom: 1px solid var(--bui-border-2); background-color: var(--bui-bg-neutral-1); height: 2rem; diff --git a/packages/ui/src/components/PasswordField/PasswordField.module.css b/packages/ui/src/components/PasswordField/PasswordField.module.css index d89060f248..8ede1212a0 100644 --- a/packages/ui/src/components/PasswordField/PasswordField.module.css +++ b/packages/ui/src/components/PasswordField/PasswordField.module.css @@ -37,7 +37,7 @@ display: flex; align-items: center; border-radius: var(--bui-radius-2); - border: 1px solid var(--bui-border); + border: 1px solid var(--bui-border-2); background-color: var(--bui-bg-neutral-1); transition: border-color 0.2s ease-in-out, outline-color 0.2s ease-in-out; @@ -49,15 +49,6 @@ height: 2.5rem; } - &:focus-within { - border-color: var(--bui-border-pressed); - outline-width: 0px; - } - - &:hover { - border-color: var(--bui-border-hover); - } - &:has([data-invalid]) { border-color: var(--bui-fg-danger); } @@ -65,7 +56,6 @@ &:has([data-disabled]) { opacity: 0.5; cursor: not-allowed; - border: 1px solid var(--bui-border-disabled); } } diff --git a/packages/ui/src/components/Popover/Popover.module.css b/packages/ui/src/components/Popover/Popover.module.css index 8fb53b9ab0..5413532bc7 100644 --- a/packages/ui/src/components/Popover/Popover.module.css +++ b/packages/ui/src/components/Popover/Popover.module.css @@ -20,8 +20,8 @@ .bui-Popover { box-shadow: var(--bui-shadow); border-radius: var(--bui-radius-3); - background: var(--bui-bg-neutral-1); - border: 1px solid var(--bui-border); + background: var(--bui-bg-popover); + border: 1px solid var(--bui-border-1); forced-color-adjust: none; outline: none; max-height: inherit; @@ -90,11 +90,11 @@ elements in order to guarantee that the stroke is always overlaying a consistent color. */ path:nth-child(1) { - fill: var(--bui-bg-neutral-1); + fill: var(--bui-bg-popover); } path:nth-child(2) { - fill: var(--bui-fg-secondary); + fill: var(--bui-border-1); } /* The arrow svg overlaps the popover by 2px, so we @@ -122,16 +122,16 @@ } } - [data-theme='dark'] .bui-Popover { - background: var(--bui-bg-neutral-2); - border: 1px solid var(--bui-border); + /* [data-theme='dark'] .bui-Popover { + background: var(--bui-bg-popover); + border: 1px solid var(--bui-border-1); } [data-theme='dark'] .bui-PopoverArrow svg path:nth-child(1) { - fill: var(--bui-bg-neutral-2); + fill: var(--bui-bg-popover); } [data-theme='dark'] .bui-PopoverArrow svg path:nth-child(2) { fill: var(--bui-fg-secondary); - } + } */ } diff --git a/packages/ui/src/components/RadioGroup/RadioGroup.module.css b/packages/ui/src/components/RadioGroup/RadioGroup.module.css index 3b816709ef..1a2cd221ca 100644 --- a/packages/ui/src/components/RadioGroup/RadioGroup.module.css +++ b/packages/ui/src/components/RadioGroup/RadioGroup.module.css @@ -50,7 +50,7 @@ width: 1rem; height: 1rem; box-sizing: border-box; - border: 0.125rem solid var(--bui-border); + border: 0.125rem solid var(--bui-border-2); background: var(--bui-bg-neutral-1); border-radius: var(--bui-radius-full); transition: all 200ms; @@ -59,7 +59,7 @@ } &[data-pressed]:before { - border-color: var(--bui-border); + border-color: var(--bui-border-2); } &[data-selected] { @@ -83,13 +83,8 @@ color: var(--bui-fg-disabled); &:before { - border-color: var(--bui-border-disabled); background: var(--bui-bg-disabled); } - - &[data-selected]:before { - border-color: var(--bui-border-disabled); - } } &[data-invalid]:before { @@ -105,13 +100,8 @@ color: var(--bui-fg-disabled); &:before { - border-color: var(--bui-border-disabled); background: var(--bui-bg-disabled); } - - &[data-selected]:before { - border-color: var(--bui-border-disabled); - } } } } diff --git a/packages/ui/src/components/SearchField/SearchField.module.css b/packages/ui/src/components/SearchField/SearchField.module.css index 44c1691ab9..da854ee7b1 100644 --- a/packages/ui/src/components/SearchField/SearchField.module.css +++ b/packages/ui/src/components/SearchField/SearchField.module.css @@ -93,7 +93,7 @@ display: flex; align-items: center; border-radius: var(--bui-radius-2); - border: 1px solid var(--bui-border); + border: 1px solid var(--bui-border-2); background-color: var(--bui-bg-neutral-1); transition: border-color 0.2s ease-in-out, outline-color 0.2s ease-in-out; @@ -105,15 +105,6 @@ height: 2.5rem; } - &:focus-within { - border-color: var(--bui-border-pressed); - outline-width: 0px; - } - - &:hover { - border-color: var(--bui-border-hover); - } - &[data-invalid] { border-color: var(--bui-fg-danger); } @@ -121,7 +112,6 @@ &[data-disabled] { opacity: 0.5; cursor: not-allowed; - border: 1px solid var(--bui-border-disabled); } } diff --git a/packages/ui/src/components/Select/Select.module.css b/packages/ui/src/components/Select/Select.module.css index 2c3945b4c2..19118acf3c 100644 --- a/packages/ui/src/components/Select/Select.module.css +++ b/packages/ui/src/components/Select/Select.module.css @@ -35,7 +35,7 @@ .bui-SelectTrigger { box-sizing: border-box; border-radius: var(--bui-radius-3); - border: 1px solid var(--bui-border); + border: 1px solid var(--bui-border-2); background-color: var(--bui-bg-neutral-1); display: flex; justify-content: space-between; @@ -72,16 +72,6 @@ color: var(--bui-fg-secondary); } - &:hover { - transition: border-color 0.2s ease-in-out; - border-color: var(--bui-border-hover); - } - - &:focus-visible { - border-color: var(--bui-border-pressed); - outline: 0; - } - .bui-Select[data-invalid] & { border-color: var(--bui-fg-danger); @@ -93,7 +83,6 @@ &[disabled] { cursor: not-allowed; - border-color: var(--bui-border-disabled); color: var(--bui-fg-disabled); } } @@ -194,7 +183,7 @@ display: flex; align-items: center; padding-inline: var(--bui-space-3) 0; - border-bottom: 1px solid var(--bui-border); + border-bottom: 1px solid var(--bui-border-2); } .bui-SelectSearch { diff --git a/packages/ui/src/components/Table/Table.module.css b/packages/ui/src/components/Table/Table.module.css index be35898fb0..0faa256e06 100644 --- a/packages/ui/src/components/Table/Table.module.css +++ b/packages/ui/src/components/Table/Table.module.css @@ -30,7 +30,7 @@ } .bui-TableHeader { - border-bottom: 1px solid var(--bui-border); + border-bottom: 1px solid var(--bui-border-2); transition: color 0.2s ease-in-out; } @@ -81,7 +81,7 @@ } .bui-TableRow { - border-bottom: 1px solid var(--bui-border); + border-bottom: 1px solid var(--bui-border-2); transition: color 0.2s ease-in-out; &:hover { diff --git a/packages/ui/src/components/TextField/TextField.module.css b/packages/ui/src/components/TextField/TextField.module.css index 5d582d775c..a40c1dc4a7 100644 --- a/packages/ui/src/components/TextField/TextField.module.css +++ b/packages/ui/src/components/TextField/TextField.module.css @@ -75,7 +75,7 @@ align-items: center; padding: 0 var(--bui-space-3); border-radius: var(--bui-radius-2); - border: 1px solid var(--bui-border); + border: 1px solid var(--bui-border-2); background-color: var(--bui-bg-neutral-1); font-size: var(--bui-font-size-3); font-family: var(--bui-font-regular); @@ -95,20 +95,6 @@ color: var(--bui-fg-secondary); } - &[data-focused] { - outline-color: var(--bui-border-pressed); - outline-width: 0px; - } - - &[data-hovered] { - border-color: var(--bui-border-hover); - } - - &[data-focused] { - border-color: var(--bui-border-pressed); - outline-width: 0px; - } - &[data-invalid] { border-color: var(--bui-fg-danger); } @@ -116,7 +102,6 @@ &[data-disabled] { opacity: 0.5; cursor: not-allowed; - border: 1px solid var(--bui-border-disabled); } } } diff --git a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.module.css b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.module.css index 90bedd1756..fef9b33a9a 100644 --- a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.module.css +++ b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.module.css @@ -23,7 +23,7 @@ flex-wrap: nowrap; border-radius: var(--bui-radius-2); overflow: hidden; - box-shadow: inset 0 0 0 1px var(--bui-border); + box-shadow: inset 0 0 0 1px var(--bui-border-2); width: fit-content; } @@ -65,7 +65,7 @@ .bui-ToggleButtonGroup:not([data-orientation='vertical']) :global(.bui-ToggleButton) + :global(.bui-ToggleButton) { - border-left: 1px solid var(--bui-border); + border-left: 1px solid var(--bui-border-2); } /* Vertical dividers */ @@ -77,7 +77,7 @@ .bui-ToggleButtonGroup[data-orientation='vertical'] :global(.bui-ToggleButton) + :global(.bui-ToggleButton) { - border-top: 1px solid var(--bui-border); + border-top: 1px solid var(--bui-border-2); } /* Vertical radius rules */ diff --git a/packages/ui/src/components/Tooltip/Tooltip.module.css b/packages/ui/src/components/Tooltip/Tooltip.module.css index 3841fbbcec..9c9bc503b5 100644 --- a/packages/ui/src/components/Tooltip/Tooltip.module.css +++ b/packages/ui/src/components/Tooltip/Tooltip.module.css @@ -21,7 +21,7 @@ box-shadow: var(--bui-shadow); border-radius: 4px; background: var(--bui-bg-neutral-1); - border: 1px solid var(--bui-border); + border: 1px solid var(--bui-border-2); forced-color-adjust: none; outline: none; padding: var(--bui-space-2) var(--bui-space-3); @@ -109,7 +109,7 @@ [data-theme='dark'] .bui-Tooltip { background: var(--bui-bg-neutral-2); box-shadow: none; - border: 1px solid var(--bui-border); + border: 1px solid var(--bui-border-2); } [data-theme='dark'] .bui-TooltipArrow svg path:nth-child(1) { diff --git a/packages/ui/src/css/colors.stories.tsx b/packages/ui/src/css/colors.stories.tsx index c4768da3ae..faa85aceda 100644 --- a/packages/ui/src/css/colors.stories.tsx +++ b/packages/ui/src/css/colors.stories.tsx @@ -24,7 +24,7 @@ const meta = preview.meta({ export const Default = meta.story({ render: () => ( -
+
Neutral 1 diff --git a/packages/ui/src/css/core.css b/packages/ui/src/css/core.css index f744f253bf..1047215ee8 100644 --- a/packages/ui/src/css/core.css +++ b/packages/ui/src/css/core.css @@ -27,7 +27,7 @@ } body { - background-color: var(--bui-bg-neutral-0); + background-color: var(--bui-bg-app); /* Text defaults */ color: var(--bui-fg-primary); diff --git a/packages/ui/src/css/tokens.css b/packages/ui/src/css/tokens.css index 4f5cb08bfb..7028260eb0 100644 --- a/packages/ui/src/css/tokens.css +++ b/packages/ui/src/css/tokens.css @@ -77,7 +77,8 @@ --bui-bg-solid-disabled: #163a66; /* Neutral background colors */ - --bui-bg-neutral-0: #f8f8f8; + --bui-bg-app: #f8f8f8; + --bui-bg-popover: #ffffff; --bui-bg-neutral-1: #fff; --bui-bg-neutral-1-hover: oklch(0% 0 0 / 12%); @@ -123,10 +124,8 @@ --bui-fg-info: #0d74ce; /* Border colors */ - --bui-border: rgba(0, 0, 0, 0.1); - --bui-border-hover: rgba(0, 0, 0, 0.2); - --bui-border-pressed: rgba(0, 0, 0, 0.4); - --bui-border-disabled: rgba(0, 0, 0, 0.1); + --bui-border-1: #d5d5d5; + --bui-border-2: #bababa; --bui-border-info: #7ea9d6; --bui-border-danger: #f87a7a; --bui-border-warning: #e36d05; @@ -153,7 +152,8 @@ --bui-bg-solid-disabled: #1b3d68; /* Neutral background colors */ - --bui-bg-neutral-0: #333333; + --bui-bg-app: #333333; + --bui-bg-popover: #1a1a1a; --bui-bg-neutral-1: oklch(100% 0 0 / 10%); --bui-bg-neutral-1-hover: oklch(100% 0 0 / 14%); @@ -199,10 +199,8 @@ --bui-fg-info: #70b8ff; /* Border colors */ - --bui-border: rgba(255, 255, 255, 0.12); - --bui-border-hover: rgba(255, 255, 255, 0.4); - --bui-border-pressed: rgba(255, 255, 255, 0.5); - --bui-border-disabled: rgba(255, 255, 255, 0.2); + --bui-border-1: #434343; + --bui-border-2: #585858; --bui-border-info: #7ea9d6; --bui-border-danger: #f87a7a; --bui-border-warning: #e36d05; diff --git a/packages/ui/src/css/utilities/sm.css b/packages/ui/src/css/utilities/sm.css index a9c8ea8ab5..a3b330026e 100644 --- a/packages/ui/src/css/utilities/sm.css +++ b/packages/ui/src/css/utilities/sm.css @@ -21,7 +21,7 @@ } .bui-sm-border-base { - border-color: var(--bui-border); + border-color: var(--bui-border-2); border-width: 1px; border-style: solid; } @@ -38,7 +38,6 @@ } .bui-sm-border-selected { - border-color: var(--bui-border-pressed); border-width: 1px; border-style: solid; } diff --git a/packages/ui/src/css/utilities/xs.css b/packages/ui/src/css/utilities/xs.css index 2907c7d468..dc9d00574d 100644 --- a/packages/ui/src/css/utilities/xs.css +++ b/packages/ui/src/css/utilities/xs.css @@ -20,7 +20,7 @@ } .bui-border-base { - border-color: var(--bui-border); + border-color: var(--bui-border-2); border-width: 1px; border-style: solid; } @@ -37,7 +37,6 @@ } .bui-border-selected { - border-color: var(--bui-border-pressed); border-width: 1px; border-style: solid; } diff --git a/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx b/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx index 37cf9787ae..90ab44b626 100644 --- a/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx +++ b/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx @@ -363,7 +363,7 @@ export function DetailedVisualizer({ tree }: { tree: AppTree }) { style={{ flex: '0 0 auto', background: 'var(--bui-bg-neutral-1)', - border: '1px solid var(--bui-border)', + border: '1px solid var(--bui-border-2)', borderRadius: 'var(--bui-radius-2)', }} > diff --git a/plugins/app-visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx b/plugins/app-visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx index b8bcd859ac..99fb20ac28 100644 --- a/plugins/app-visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx +++ b/plugins/app-visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx @@ -93,7 +93,7 @@ export function TextVisualizer({ tree }: { tree: AppTree }) { px="4" style={{ background: 'var(--bui-bg-neutral-1)', - borderTop: '1px solid var(--bui-border)', + borderTop: '1px solid var(--bui-border-2)', }} > diff --git a/plugins/mui-to-bui/src/components/BuiThemerPage/BuiThemePreview.test.tsx b/plugins/mui-to-bui/src/components/BuiThemerPage/BuiThemePreview.test.tsx index 355100be86..4b6024885b 100644 --- a/plugins/mui-to-bui/src/components/BuiThemerPage/BuiThemePreview.test.tsx +++ b/plugins/mui-to-bui/src/components/BuiThemerPage/BuiThemePreview.test.tsx @@ -26,7 +26,7 @@ describe('BuiThemePreview', () => { mode="light" styleObject={{ '--bui-bg-neutral-2': '#ffffff', - '--bui-border': '#cccccc', + '--bui-border-2': '#cccccc', '--bui-radius-2': '4px', '--bui-space-3': '12px', '--bui-fg-secondary': '#777777', diff --git a/plugins/mui-to-bui/src/components/BuiThemerPage/BuiThemePreview.tsx b/plugins/mui-to-bui/src/components/BuiThemerPage/BuiThemePreview.tsx index 0a745e1aeb..2f1edbff9f 100644 --- a/plugins/mui-to-bui/src/components/BuiThemerPage/BuiThemePreview.tsx +++ b/plugins/mui-to-bui/src/components/BuiThemerPage/BuiThemePreview.tsx @@ -49,7 +49,7 @@ export function BuiThemePreview({ mode, styleObject }: IsolatedPreviewProps) { backgroundColor: 'var(--bui-bg-neutral-2)', padding: 'var(--bui-space-3)', borderRadius: 'var(--bui-radius-2)', - border: '1px solid var(--bui-border)', + border: '1px solid var(--bui-border-2)', }} > diff --git a/plugins/mui-to-bui/src/components/BuiThemerPage/ThemeContent.tsx b/plugins/mui-to-bui/src/components/BuiThemerPage/ThemeContent.tsx index 19749165d7..6341348862 100644 --- a/plugins/mui-to-bui/src/components/BuiThemerPage/ThemeContent.tsx +++ b/plugins/mui-to-bui/src/components/BuiThemerPage/ThemeContent.tsx @@ -99,7 +99,7 @@ export function ThemeContent({ p="3" style={{ backgroundColor: 'var(--bui-bg-neutral-2)', - border: '1px solid var(--bui-border)', + border: '1px solid var(--bui-border-2)', borderRadius: 'var(--bui-radius-2)', fontFamily: 'monospace', fontSize: '14px', diff --git a/plugins/mui-to-bui/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts b/plugins/mui-to-bui/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts index 31c2fa695c..95dac618cf 100644 --- a/plugins/mui-to-bui/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts +++ b/plugins/mui-to-bui/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts @@ -59,7 +59,7 @@ describe('convertMuiToBuiTheme', () => { // Border radius tokens are only generated when radius is 0 expect(result.css).not.toContain('--bui-radius-3:'); // Background default maps to surface-2 - expect(result.css).toContain('--bui-bg-neutral-0: #f5f5f5;'); + expect(result.css).toContain('--bui-bg-app: #f5f5f5;'); expect(result.css).toContain('--bui-bg-neutral-2: #f5f5f5;'); expect(result.css).toContain('--bui-bg-neutral-1: #ffffff;'); expect(result.css).toContain('--bui-fg-primary: #000000;'); @@ -98,7 +98,7 @@ describe('convertMuiToBuiTheme', () => { expect(result.css).toContain("[data-theme-mode='dark'] {"); // Background default maps to surface-2 in dark mode as well - expect(result.css).toContain('--bui-bg-neutral-0: #121212;'); + expect(result.css).toContain('--bui-bg-app: #121212;'); expect(result.css).toContain('--bui-bg-neutral-2: #121212;'); expect(result.css).toContain('--bui-bg-neutral-1: #1e1e1e;'); expect(result.css).toContain('--bui-fg-primary: #ffffff;'); diff --git a/plugins/mui-to-bui/src/components/BuiThemerPage/convertMuiToBuiTheme.ts b/plugins/mui-to-bui/src/components/BuiThemerPage/convertMuiToBuiTheme.ts index 326fd08ef2..7f32621dd2 100644 --- a/plugins/mui-to-bui/src/components/BuiThemerPage/convertMuiToBuiTheme.ts +++ b/plugins/mui-to-bui/src/components/BuiThemerPage/convertMuiToBuiTheme.ts @@ -110,8 +110,8 @@ function generateBuiVariables(theme: Mui5Theme): Record { }); // Generate neutral background colors + styleObject['--bui-bg-app'] = palette.background.default; Object.entries({ - 'neutral-0': palette.background.default, 'neutral-1': palette.background.paper, 'neutral-2': palette.background.default, 'neutral-3': palette.background.default, @@ -138,7 +138,7 @@ function generateBuiVariables(theme: Mui5Theme): Record { }); // Base border color if available - styleObject['--bui-border'] = palette.border || palette.divider; + styleObject['--bui-border-2'] = palette.border || palette.divider; styleObject['--bui-border-danger'] = palette.error.main; styleObject['--bui-border-warning'] = palette.warning.main; styleObject['--bui-border-success'] = palette.success.main; From 0bf7ff679ab3b209d1476545203f2135799c79cb Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Fri, 13 Feb 2026 13:28:09 +0000 Subject: [PATCH 03/10] Cleanups Signed-off-by: Charles de Dreuille --- .storybook/preview.tsx | 3 +- docs-ui/src/app/tokens/page.mdx | 22 --------- docs/conf/user-interface/index.md | 48 +++++++++---------- .../src/components/Popover/Popover.module.css | 13 ----- packages/ui/src/css/utilities/sm.css | 5 -- packages/ui/src/css/utilities/xs.css | 5 -- 6 files changed, 25 insertions(+), 71 deletions(-) diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 7be5089893..6a0177cafe 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -135,8 +135,7 @@ export default definePreview({ document.body.style.backgroundColor = 'var(--bui-bg-app)'; const docsStoryElements = document.getElementsByClassName('docs-story'); Array.from(docsStoryElements).forEach(element => { - (element as HTMLElement).style.backgroundColor = - 'var(--bui-bg-app)'; + (element as HTMLElement).style.backgroundColor = 'var(--bui-bg-app)'; }); return ( diff --git a/docs-ui/src/app/tokens/page.mdx b/docs-ui/src/app/tokens/page.mdx index 6d9b5e65ca..bc6e17a27c 100644 --- a/docs-ui/src/app/tokens/page.mdx +++ b/docs-ui/src/app/tokens/page.mdx @@ -472,28 +472,6 @@ low contrast to help as a separator with the different background colors. It should be used on top of `--bui-bg-neutral-1`. - - - --bui-border-2-hover - - - Used when the component is interactive and hovered. - - - - - --bui-border-2-pressed - - - Used when the component is interactive and focused. - - - - - --bui-border-2-disabled - - Used when the component is disabled. - --bui-border-2-danger diff --git a/docs/conf/user-interface/index.md b/docs/conf/user-interface/index.md index a70026a759..19f5251021 100644 --- a/docs/conf/user-interface/index.md +++ b/docs/conf/user-interface/index.md @@ -122,22 +122,22 @@ We recommend starting with a core set of CSS variables to quickly achieve a bran And if you’d like to go even further, you can target specific component class names for advanced customization. -| Token Name | Description | -| -------------------- | --------------------------------------------------------------------------------------------- | -| `--bui-bg-app` | This is used to define the background color of your app. It will only be used once. | -| `--bui-bg-neutral-1` | We ar using this color to sit on top of `--bui-bg-app` mostly for `Card`, `Dialog`, ... | -| `--bui-bg-neutral-2` | This is for content inside elevated components. This colour is less common. | -| `--bui-bg-solid` | This is used for main actions like primary buttons. | -| `--bui-fg-solid` | This is for texts or icons on top of a solid backgrounds. | -| `--bui-fg-primary` | Your primary text or icon colours. | -| `--bui-fg-secondary` | Your secondary text or icon colours. | -| `--bui-fg-danger` | Used for error states and destructive actions. | -| `--bui-fg-warning` | Used for warning states and cautionary information. | -| `--bui-fg-success` | Used for success states and positive feedback. | -| `--bui-fg-info` | Used for informational content and neutral status. | -| `--bui-border-1` | Subtle borders for low-contrast separators. | -| `--bui-border-2` | Main borders around surfaces like `Card`, `Dialog`, ... | -| `--bui-font-regular` | The main font of your app. | +| Token Name | Description | +| -------------------- | ---------------------------------------------------------------------------------------- | +| `--bui-bg-app` | This is used to define the background color of your app. It will only be used once. | +| `--bui-bg-neutral-1` | We are using this color to sit on top of `--bui-bg-app` mostly for `Card`, `Dialog`, ... | +| `--bui-bg-neutral-2` | This is for content inside elevated components. This colour is less common. | +| `--bui-bg-solid` | This is used for main actions like primary buttons. | +| `--bui-fg-solid` | This is for texts or icons on top of a solid backgrounds. | +| `--bui-fg-primary` | Your primary text or icon colours. | +| `--bui-fg-secondary` | Your secondary text or icon colours. | +| `--bui-fg-danger` | Used for error states and destructive actions. | +| `--bui-fg-warning` | Used for warning states and cautionary information. | +| `--bui-fg-success` | Used for success states and positive feedback. | +| `--bui-fg-info` | Used for informational content and neutral status. | +| `--bui-border-1` | Subtle borders for low-contrast separators. | +| `--bui-border-2` | Main borders around surfaces like `Card`, `Dialog`, ... | +| `--bui-font-regular` | The main font of your app. |
All available CSS variables @@ -155,7 +155,7 @@ These colors form a layered neutral scale for your application backgrounds. `--b | Token Name | Description | | ----------------------------- | ------------------------------------------------------------ | -| `--bui-bg-app` | The base background color of your Backstage instance. | +| `--bui-bg-app` | The base background color of your Backstage instance. | | `--bui-bg-neutral-1` | First elevated layer. Use for cards, dialogs, and panels. | | `--bui-bg-neutral-1-hover` | Hover state for elements on neutral-1. | | `--bui-bg-neutral-1-pressed` | Pressed state for elements on neutral-1. | @@ -214,13 +214,13 @@ Foreground colours are meant to work in pair with a background colours. Typicall These border colors are mostly meant to be used as borders on top of any components with low contrast to help as a separator with the different background colors. -| Token Name | Description | -| ----------------------- | --------------------------------------------------- | -| `--bui-border-1` | Subtle border for low-contrast separators. | -| `--bui-border-2` | It should be used on top of `--bui-bg-neutral-1`. | -| `--bui-border-danger` | It should be used on top of `--bui-bg-danger`. | -| `--bui-border-warning` | It should be used on top of `--bui-bg-warning`. | -| `--bui-border-success` | It should be used on top of `--bui-bg-success`. | +| Token Name | Description | +| ---------------------- | ------------------------------------------------- | +| `--bui-border-1` | Subtle border for low-contrast separators. | +| `--bui-border-2` | It should be used on top of `--bui-bg-neutral-1`. | +| `--bui-border-danger` | It should be used on top of `--bui-bg-danger`. | +| `--bui-border-warning` | It should be used on top of `--bui-bg-warning`. | +| `--bui-border-success` | It should be used on top of `--bui-bg-success`. | #### Special colors diff --git a/packages/ui/src/components/Popover/Popover.module.css b/packages/ui/src/components/Popover/Popover.module.css index 5413532bc7..69d2acffcf 100644 --- a/packages/ui/src/components/Popover/Popover.module.css +++ b/packages/ui/src/components/Popover/Popover.module.css @@ -121,17 +121,4 @@ transform: rotate(-90deg); } } - - /* [data-theme='dark'] .bui-Popover { - background: var(--bui-bg-popover); - border: 1px solid var(--bui-border-1); - } - - [data-theme='dark'] .bui-PopoverArrow svg path:nth-child(1) { - fill: var(--bui-bg-popover); - } - - [data-theme='dark'] .bui-PopoverArrow svg path:nth-child(2) { - fill: var(--bui-fg-secondary); - } */ } diff --git a/packages/ui/src/css/utilities/sm.css b/packages/ui/src/css/utilities/sm.css index a3b330026e..4ceb7600ed 100644 --- a/packages/ui/src/css/utilities/sm.css +++ b/packages/ui/src/css/utilities/sm.css @@ -37,11 +37,6 @@ border-width: 0; } - .bui-sm-border-selected { - border-width: 1px; - border-style: solid; - } - .bui-sm-border-warning { border-color: var(--bui-border-warning); border-width: 1px; diff --git a/packages/ui/src/css/utilities/xs.css b/packages/ui/src/css/utilities/xs.css index dc9d00574d..12d62f1dd8 100644 --- a/packages/ui/src/css/utilities/xs.css +++ b/packages/ui/src/css/utilities/xs.css @@ -36,11 +36,6 @@ border-width: 0; } - .bui-border-selected { - border-width: 1px; - border-style: solid; - } - .bui-border-warning { border-color: var(--bui-border-warning); border-width: 1px; From 01cd764c4c288162d17fb972534d9500264cc405 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Fri, 13 Feb 2026 13:48:37 +0000 Subject: [PATCH 04/10] Cleanup borders and surfaces Signed-off-by: Charles de Dreuille --- .../src/components/Dialog/Dialog.module.css | 8 +++---- .../src/components/Header/Header.module.css | 4 ++-- .../ui/src/components/Menu/Menu.module.css | 8 +++---- .../src/components/Tooltip/Tooltip.module.css | 22 ++++--------------- 4 files changed, 14 insertions(+), 28 deletions(-) diff --git a/packages/ui/src/components/Dialog/Dialog.module.css b/packages/ui/src/components/Dialog/Dialog.module.css index cd0351cc4a..f5968820a2 100644 --- a/packages/ui/src/components/Dialog/Dialog.module.css +++ b/packages/ui/src/components/Dialog/Dialog.module.css @@ -43,9 +43,9 @@ } .bui-Dialog { - background: var(--bui-bg-neutral-1); + background: var(--bui-bg-popover); border-radius: 0.5rem; - border: 1px solid var(--bui-border-2); + border: 1px solid var(--bui-border-1); color: var(--bui-fg-primary); position: relative; width: min(var(--bui-dialog-min-width, 400px), calc(100vw - 3rem)); @@ -73,7 +73,7 @@ align-items: center; padding-inline: var(--bui-space-3); padding-block: var(--bui-space-2); - border-bottom: 1px solid var(--bui-border-2); + border-bottom: 1px solid var(--bui-border-1); } .bui-DialogHeaderTitle { @@ -89,7 +89,7 @@ gap: var(--bui-space-2); padding-inline: var(--bui-space-3); padding-block: var(--bui-space-3); - border-top: 1px solid var(--bui-border-2); + border-top: 1px solid var(--bui-border-1); } .bui-DialogBody { diff --git a/packages/ui/src/components/Header/Header.module.css b/packages/ui/src/components/Header/Header.module.css index ec00d85f86..f945b70bf0 100644 --- a/packages/ui/src/components/Header/Header.module.css +++ b/packages/ui/src/components/Header/Header.module.css @@ -43,7 +43,7 @@ justify-content: space-between; background-color: var(--bui-bg-neutral-1); padding-inline: var(--bui-space-5); - border-bottom: 1px solid var(--bui-border-2); + border-bottom: 1px solid var(--bui-border-1); color: var(--bui-fg-primary); height: 52px; } @@ -89,7 +89,7 @@ .bui-HeaderTabsWrapper { padding-inline: var(--bui-space-3); - border-bottom: 1px solid var(--bui-border-2); + border-bottom: 1px solid var(--bui-border-1); background-color: var(--bui-bg-neutral-1); } } diff --git a/packages/ui/src/components/Menu/Menu.module.css b/packages/ui/src/components/Menu/Menu.module.css index d21806b506..4967d26551 100644 --- a/packages/ui/src/components/Menu/Menu.module.css +++ b/packages/ui/src/components/Menu/Menu.module.css @@ -21,9 +21,9 @@ display: flex; flex-direction: column; box-shadow: var(--bui-shadow); - border: 1px solid var(--bui-border-2); + border: 1px solid var(--bui-border-1); border-radius: var(--bui-radius-2); - background: var(--bui-bg-neutral-1); + background: var(--bui-bg-popover); color: var(--bui-fg-primary); outline: none; transition: transform 200ms, opacity 200ms; @@ -183,7 +183,7 @@ .bui-MenuSeparator { height: 1px; - background: var(--bui-border-2); + background: var(--bui-border-1); margin-inline: var(--bui-space-1_5); margin-block: var(--bui-space-1); } @@ -194,7 +194,7 @@ flex-shrink: 0; display: flex; align-items: center; - border-bottom: 1px solid var(--bui-border-2); + border-bottom: 1px solid var(--bui-border-1); background-color: var(--bui-bg-neutral-1); height: 2rem; diff --git a/packages/ui/src/components/Tooltip/Tooltip.module.css b/packages/ui/src/components/Tooltip/Tooltip.module.css index 9c9bc503b5..22a3283790 100644 --- a/packages/ui/src/components/Tooltip/Tooltip.module.css +++ b/packages/ui/src/components/Tooltip/Tooltip.module.css @@ -20,8 +20,8 @@ .bui-Tooltip { box-shadow: var(--bui-shadow); border-radius: 4px; - background: var(--bui-bg-neutral-1); - border: 1px solid var(--bui-border-2); + background: var(--bui-bg-popover); + border: 1px solid var(--bui-border-1); forced-color-adjust: none; outline: none; padding: var(--bui-space-2) var(--bui-space-3); @@ -74,11 +74,11 @@ elements in order to guarantee that the stroke is always overlaying a consistent color. */ path:nth-child(1) { - fill: var(--bui-bg-neutral-1); + fill: var(--bui-bg-popover); } path:nth-child(2) { - fill: var(--bui-fg-secondary); + fill: var(--bui-border-1); } /* The arrow svg overlaps the tooltip by 2px, so we @@ -105,18 +105,4 @@ transform: rotate(-90deg); } } - - [data-theme='dark'] .bui-Tooltip { - background: var(--bui-bg-neutral-2); - box-shadow: none; - border: 1px solid var(--bui-border-2); - } - - [data-theme='dark'] .bui-TooltipArrow svg path:nth-child(1) { - fill: var(--bui-bg-neutral-2); - } - - [data-theme='dark'] .bui-TooltipArrow svg path:nth-child(2) { - fill: var(--bui-fg-secondary); - } } From e8670d00f1b7f30a0e5cf00665395e88debcefd4 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Fri, 13 Feb 2026 14:01:13 +0000 Subject: [PATCH 05/10] Cleanup docs Signed-off-by: Charles de Dreuille --- .changeset/plain-deer-sneeze.md | 4 +++- docs-ui/src/app/tokens/page.mdx | 8 ++++---- docs/conf/user-interface/index.md | 2 ++ 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.changeset/plain-deer-sneeze.md b/.changeset/plain-deer-sneeze.md index bb544740ee..f492746f0a 100644 --- a/.changeset/plain-deer-sneeze.md +++ b/.changeset/plain-deer-sneeze.md @@ -2,10 +2,12 @@ '@backstage/ui': minor --- -**BREAKING:** Renamed and removed CSS tokens. +**BREAKING:** Renamed, added, and removed CSS tokens. - Renamed `--bui-bg-neutral-0` to `--bui-bg-app`. - Renamed `--bui-border` to `--bui-border-2`. +- Added `--bui-border-1` for subtle, low-contrast borders. +- Added `--bui-bg-popover` for the background color of popovers, tooltips, menus, and dialogs. - Removed `--bui-border-hover`, `--bui-border-pressed`, and `--bui-border-disabled`. **Migration:** diff --git a/docs-ui/src/app/tokens/page.mdx b/docs-ui/src/app/tokens/page.mdx index bc6e17a27c..f359c10525 100644 --- a/docs-ui/src/app/tokens/page.mdx +++ b/docs-ui/src/app/tokens/page.mdx @@ -474,25 +474,25 @@ low contrast to help as a separator with the different background colors. - --bui-border-2-danger + --bui-border-danger It should be used on top of `--bui-bg-danger`. - --bui-border-2-warning + --bui-border-warning It should be used on top of `--bui-bg-warning`. - --bui-border-2-success + --bui-border-success It should be used on top of `--bui-bg-success`. - --bui-border-2-info + --bui-border-info It should be used on top of `--bui-bg-info`. diff --git a/docs/conf/user-interface/index.md b/docs/conf/user-interface/index.md index 19f5251021..cb53ebe81c 100644 --- a/docs/conf/user-interface/index.md +++ b/docs/conf/user-interface/index.md @@ -156,6 +156,7 @@ These colors form a layered neutral scale for your application backgrounds. `--b | Token Name | Description | | ----------------------------- | ------------------------------------------------------------ | | `--bui-bg-app` | The base background color of your Backstage instance. | +| `--bui-bg-popover` | The background color used for popovers, tooltips, and menus. | | `--bui-bg-neutral-1` | First elevated layer. Use for cards, dialogs, and panels. | | `--bui-bg-neutral-1-hover` | Hover state for elements on neutral-1. | | `--bui-bg-neutral-1-pressed` | Pressed state for elements on neutral-1. | @@ -221,6 +222,7 @@ These border colors are mostly meant to be used as borders on top of any compone | `--bui-border-danger` | It should be used on top of `--bui-bg-danger`. | | `--bui-border-warning` | It should be used on top of `--bui-bg-warning`. | | `--bui-border-success` | It should be used on top of `--bui-bg-success`. | +| `--bui-border-info` | It should be used on top of `--bui-bg-info`. | #### Special colors From 286cc4c7b260a29d16af75996b453ea6752f7552 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 13 Feb 2026 15:48:35 +0100 Subject: [PATCH 06/10] frontend-dynamic-feature-loader: update tests for new shared dependencies format Update the loader tests to use the new RuntimeSharedDependenciesGlobal format with versioned items array instead of the old flat object with module properties. Signed-off-by: Patrik Oldsberg --- .../src/loader.test.tsx | 120 ++++++++---------- 1 file changed, 56 insertions(+), 64 deletions(-) diff --git a/packages/frontend-dynamic-feature-loader/src/loader.test.tsx b/packages/frontend-dynamic-feature-loader/src/loader.test.tsx index b24aea0fdd..cb066e8b21 100644 --- a/packages/frontend-dynamic-feature-loader/src/loader.test.tsx +++ b/packages/frontend-dynamic-feature-loader/src/loader.test.tsx @@ -45,14 +45,13 @@ function mockDefaultConfig(): Config { }); } +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL } from '../../module-federation-common/src/types'; + const globalSpy = jest.fn(); -Object.defineProperty( - global, - '__backstage-module-federation-shared-dependencies__', - { - get: globalSpy, - }, -); +Object.defineProperty(global, BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL, { + get: globalSpy, +}); describe('dynamicFrontendFeaturesLoader', () => { const server = setupServer(); @@ -128,67 +127,62 @@ describe('dynamicFrontendFeaturesLoader', () => { resetFederationGlobalInfo(); }); - const mockedDefaultSharedDependencies = { - react: { + const mockedDefaultSharedItems = [ + { + name: 'react', version: '18.3.1', - requiredVersion: '*', - singleton: true, - eager: true, - module: () => ({ default: {} }), + lib: async () => ({ default: {} }), + shareConfig: { singleton: true, requiredVersion: '*', eager: true }, }, - 'react-dom': { + { + name: 'react-dom', version: '18.3.1', - requiredVersion: '*', - singleton: true, - eager: true, - module: () => ({ default: {} }), + lib: async () => ({ default: {} }), + shareConfig: { singleton: true, requiredVersion: '*', eager: true }, }, - 'react-router': { + { + name: 'react-router', version: '6.28.2', - requiredVersion: '*', - singleton: true, - eager: true, - module: () => ({ default: {} }), + lib: async () => ({ default: {} }), + shareConfig: { singleton: true, requiredVersion: '*', eager: true }, }, - 'react-router-dom': { + { + name: 'react-router-dom', version: '6.28.2', - requiredVersion: '*', - singleton: true, - eager: true, - module: () => ({ default: {} }), + lib: async () => ({ default: {} }), + shareConfig: { singleton: true, requiredVersion: '*', eager: true }, }, - '@material-ui/core/styles': { + { + name: '@material-ui/core/styles', version: '4.12.4', - requiredVersion: '*', - singleton: true, - eager: true, - module: () => ({ default: {} }), + lib: async () => ({ default: {} }), + shareConfig: { singleton: true, requiredVersion: '*', eager: true }, }, - '@material-ui/styles': { + { + name: '@material-ui/styles', version: '4.11.5', - requiredVersion: '*', - singleton: true, - eager: true, - module: () => ({ default: {} }), + lib: async () => ({ default: {} }), + shareConfig: { singleton: true, requiredVersion: '*', eager: true }, }, - '@mui/material/styles/': { + { + name: '@mui/material/styles/', version: '5.16.14', - requiredVersion: '*', - singleton: true, - eager: true, - module: () => ({ default: {} }), + lib: async () => ({ default: {} }), + shareConfig: { singleton: true, requiredVersion: '*', eager: true }, }, - '@emotion/react': { + { + name: '@emotion/react', version: '11.13.5', - requiredVersion: '*', - singleton: true, - eager: true, - module: () => ({ default: {} }), + lib: async () => ({ default: {} }), + shareConfig: { singleton: true, requiredVersion: '*', eager: true }, }, - }; + ]; beforeEach(() => { - globalSpy.mockReturnValue(mockedDefaultSharedDependencies); + globalSpy.mockReturnValue({ + items: mockedDefaultSharedItems, + version: 'v1', + }); }); it('should return immediately if dynamic plugins are not enabled in config', async () => { @@ -1152,22 +1146,20 @@ describe('dynamicFrontendFeaturesLoader', () => { expect(spyInit).toHaveBeenCalledTimes(1); expect(spyInit.mock.calls[0][0].options).toMatchObject({ shared: Object.fromEntries( - Object.entries(mockedDefaultSharedDependencies).map( - ([name, shared]) => [ - name, - [ - { - version: shared.version, - shareConfig: { - singleton: shared.singleton, - requiredVersion: shared.requiredVersion, - eager: shared.eager, - }, - strategy: 'version-first', + mockedDefaultSharedItems.map(item => [ + item.name, + [ + { + version: item.version, + shareConfig: { + singleton: item.shareConfig.singleton, + requiredVersion: item.shareConfig.requiredVersion, + eager: item.shareConfig.eager, }, - ], + strategy: 'version-first', + }, ], - ), + ]), ), }); }); From 91babfb48b5357b3848228f552aa1e2c972522d4 Mon Sep 17 00:00:00 2001 From: Iury Lenon Alves Date: Fri, 13 Feb 2026 14:56:23 +0000 Subject: [PATCH 07/10] docs(catalog): add mention of unprocessed entities plugin and module Signed-off-by: Iury Lenon Alves --- docs/features/software-catalog/index.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/features/software-catalog/index.md b/docs/features/software-catalog/index.md index 53afb5205e..88d2d94e05 100644 --- a/docs/features/software-catalog/index.md +++ b/docs/features/software-catalog/index.md @@ -136,6 +136,12 @@ Your Backstage developer portal can be customized by incorporating [existing open source plugins](https://github.com/backstage/backstage/tree/master/plugins), or by [building your own](../../plugins/index.md). +## Unprocessed Entities + +Sometimes entities fail to process correctly. The **Unprocessed Entities** feature helps Backstage admins find and diagnose these entities to understand the state of the Catalog. + +To use this feature, check out the documentation for the [catalog-unprocessed-entities plugin](https://github.com/backstage/backstage/tree/master/plugins/catalog-unprocessed-entities) and its [backend module](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend-module-unprocessed). + ## Links - [[Blog post] Backstage Service Catalog released in alpha](https://backstage.io/blog/2020/06/22/backstage-service-catalog-alpha) From 05b7306370e96795a1e9417b18ed743d2c210846 Mon Sep 17 00:00:00 2001 From: Iury Lenon Date: Fri, 13 Feb 2026 15:09:27 +0000 Subject: [PATCH 08/10] Update docs/features/software-catalog/index.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Iury Lenon --- docs/features/software-catalog/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-catalog/index.md b/docs/features/software-catalog/index.md index 88d2d94e05..a221ed9157 100644 --- a/docs/features/software-catalog/index.md +++ b/docs/features/software-catalog/index.md @@ -138,7 +138,7 @@ or by [building your own](../../plugins/index.md). ## Unprocessed Entities -Sometimes entities fail to process correctly. The **Unprocessed Entities** feature helps Backstage admins find and diagnose these entities to understand the state of the Catalog. +Sometimes entities fail to process correctly. The **Unprocessed Entities** feature helps Backstage admins find and diagnose these entities to understand the state of the catalog. To use this feature, check out the documentation for the [catalog-unprocessed-entities plugin](https://github.com/backstage/backstage/tree/master/plugins/catalog-unprocessed-entities) and its [backend module](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend-module-unprocessed). From cdb4719e08a00ad80e4c6246be7f4881661e3ea8 Mon Sep 17 00:00:00 2001 From: Linus Andersson Date: Fri, 13 Feb 2026 19:08:52 +0100 Subject: [PATCH 09/10] docs: use postgres:17-bookworm in tutorial Signed-off-by: Linus Andersson --- docs/getting-started/config/database.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/getting-started/config/database.md b/docs/getting-started/config/database.md index cb482d5dc0..32d9444c1c 100644 --- a/docs/getting-started/config/database.md +++ b/docs/getting-started/config/database.md @@ -211,13 +211,13 @@ You can run Postgres in a Docker container, this is great for local development First we need to pull down the container image, we'll use Postgres 17, check out the [Postgres Version Policy](../../overview/versioning-policy.md#postgresql-releases) to learn which versions are supported. ```shell -docker pull postgres:17.0-trixie +docker pull postgres:17-bookworm ``` Then we just need to start up the container. ```shell -docker run -d --name postgres --restart=always -p 5432:5432 -e POSTGRES_PASSWORD= postgres:17.0-trixie +docker run -d --name postgres --restart=always -p 5432:5432 -e POSTGRES_PASSWORD= postgres:17-bookworm ``` This will run Postgres in the background for you, but remember to start it up again when you reboot your system. @@ -227,11 +227,9 @@ This will run Postgres in the background for you, but remember to start it up ag Another way to run Postgres is to use Docker Compose, here's what that would look like: ```yaml title="docker-compose.local.yaml" -version: '4' - services: postgres: - image: postgres:17.0-trixie + image: postgres:17-bookworm environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: From 8f6bf6b4e796d42238604981919d8c4b3e876a31 Mon Sep 17 00:00:00 2001 From: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:20:32 -0500 Subject: [PATCH 10/10] docs: adding more golden path docs (#32493) * docs: adding more golden path docs Signed-off-by: aramissennyeydd * cleanup Signed-off-by: aramissennyeydd * add instructions for checking todo list items Signed-off-by: aramissennyeydd * update ids so the number isn't baked into URL Signed-off-by: aramissennyeydd * add headings for all docs Signed-off-by: aramissennyeydd * add frontend plugin docs structure too Signed-off-by: aramissennyeydd * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> * fix bad link Signed-off-by: aramissennyeydd * address feedback Signed-off-by: aramissennyeydd * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> --------- Signed-off-by: aramissennyeydd Signed-off-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../adoption/001-getting-started.md | 72 +++++++++++++++++++ .../adoption/002-leadership-buy-in.md | 30 ++++++++ .../adoption/003-setting-up-a-poc.md | 13 ++++ .../004-first-stakeholder-feedback.md | 17 +++++ .../adoption/005-customizing-your-instance.md | 15 ++++ .../adoption/006-preparing-for-ga.md | 19 +++++ .../adoption/007-plugin-ownership.md | 17 +++++ docs/golden-path/adoption/008-full-catalog.md | 15 ++++ .../adoption/{meta.md => __meta__.md} | 6 +- .../deployment/{meta.md => __meta__.md} | 0 docs/golden-path/deployment/index.md | 0 docs/golden-path/plugins/__meta__.md | 15 ++++ .../plugins/backend/001-first-steps.md | 2 +- .../plugins/backend/002-poking-around.md | 41 ++++++++++- .../plugins/backend/003-persistence.md | 19 +++++ .../plugins/backend/004-source-tracked.md | 19 +++++ .../plugins/backend/005-testing.md | 29 ++++++++ docs/golden-path/plugins/backend/__meta__.md | 39 ++-------- docs/golden-path/plugins/backend/todo.http | 12 ---- .../plugins/frontend/001-first-steps.md | 15 ++++ .../plugins/frontend/002-poking-around.md | 17 +++++ .../plugins/frontend/003-dynamic-config.md | 29 ++++++++ .../plugins/frontend/004-http-client.md | 17 +++++ .../plugins/frontend/005-testing.md | 15 ++++ docs/golden-path/plugins/frontend/__meta__.md | 34 +++++++++ .../plugins/integrations/001-catalog.md | 23 ++++++ .../plugins/integrations/002-search.md | 19 +++++ .../plugins/integrations/003-permissions.md | 23 ++++++ .../plugins/integrations/004-notifications.md | 23 ++++++ 29 files changed, 541 insertions(+), 54 deletions(-) create mode 100644 docs/golden-path/adoption/001-getting-started.md create mode 100644 docs/golden-path/adoption/002-leadership-buy-in.md create mode 100644 docs/golden-path/adoption/003-setting-up-a-poc.md create mode 100644 docs/golden-path/adoption/004-first-stakeholder-feedback.md create mode 100644 docs/golden-path/adoption/005-customizing-your-instance.md create mode 100644 docs/golden-path/adoption/006-preparing-for-ga.md create mode 100644 docs/golden-path/adoption/007-plugin-ownership.md create mode 100644 docs/golden-path/adoption/008-full-catalog.md rename docs/golden-path/adoption/{meta.md => __meta__.md} (98%) rename docs/golden-path/deployment/{meta.md => __meta__.md} (100%) create mode 100644 docs/golden-path/deployment/index.md create mode 100644 docs/golden-path/plugins/__meta__.md create mode 100644 docs/golden-path/plugins/backend/003-persistence.md create mode 100644 docs/golden-path/plugins/backend/004-source-tracked.md create mode 100644 docs/golden-path/plugins/backend/005-testing.md delete mode 100644 docs/golden-path/plugins/backend/todo.http create mode 100644 docs/golden-path/plugins/frontend/001-first-steps.md create mode 100644 docs/golden-path/plugins/frontend/002-poking-around.md create mode 100644 docs/golden-path/plugins/frontend/003-dynamic-config.md create mode 100644 docs/golden-path/plugins/frontend/004-http-client.md create mode 100644 docs/golden-path/plugins/frontend/005-testing.md create mode 100644 docs/golden-path/plugins/frontend/__meta__.md create mode 100644 docs/golden-path/plugins/integrations/001-catalog.md create mode 100644 docs/golden-path/plugins/integrations/002-search.md create mode 100644 docs/golden-path/plugins/integrations/003-permissions.md create mode 100644 docs/golden-path/plugins/integrations/004-notifications.md diff --git a/docs/golden-path/adoption/001-getting-started.md b/docs/golden-path/adoption/001-getting-started.md new file mode 100644 index 0000000000..574380f263 --- /dev/null +++ b/docs/golden-path/adoption/001-getting-started.md @@ -0,0 +1,72 @@ +--- +id: getting-started +sidebar_label: 001 - Getting started +title: Getting started with Backstage +--- + +The adoption journey is a bit different than the other Golden Paths. The goal of this guide is to prepare you for a successful implementation of Backstage in your organization. A technical understanding of Backstage is not needed for this Golden Path, just a desire to help the technical team that will be owning your Backstage instance. + +:::info + +I'd highly recommend poking around https://demo.backstage.io/ before continuing with this guide. It's a test instance of Backstage that provides a good foundation for what to expect from the tool as a user. + +::: + +## What is Backstage? + +At a high level, Backstage is a framework for building developer portals. When implemented successfully, it can reduce toil for your developers by centralizing information like docs and ownership, reducing cognitive overhead due to tool fragmentation and simplify setting up new codebases or integrating with existing ones. + +A few examples, + +> My company tracks everything with spreadsheets. We have a list of all Github repos and who owns them, but it's becoming more and more of an issue to keep up to date. Teams aren't proactively updating it when new projects are created and it quickly falls out of date with reorgs and team charter changes. + +Backstage can help! We provide a core plugin called Software Catalog that automates this process. Teams are asked to maintain a file in their repo with this ownership information and it gets automatically ingested into Backstage where you can view all projects in a single location. + +> My developers have been complaining recently about having to use a growing number of different websites and tools in their day to day. It's getting hard to keep track of all of the tools and for those that we don't use frequently, we lose X minutes trying to remember how to access them. + +Tool fragmentation is a real issue and Backstage can also help here! You can create plugins tailored for your company that talk to these external services. These plugins can be standalone or integrated with the Software Catalog for better context. Imagine all of your [CI/CD workflows visible directly](https://backstage.io/plugins/) on the page for your team's projects. + +It's important to note that Backstage shouldn't be fully replacing these tools, we don't want to reinvent the wheel. The goal is to have all of the really important information in one place. The tool should still be where teams go to do more advanced or in depth work. + +> We have been struggling recently with getting teams to use a standard template for new services. There's no standard set of libraries these services are using or standard infra management. It's increasingly difficult as a platform team to manage everything. + +Backstage can help here too! The Scaffolder provides a templating framework that you can plug a Golden Path implementation to. Similar to Github template repos, this can provide a standard base for teams to create based off of. + +> Our platform teams have been getting more and more support requests to help debug onboarding steps. We've documented these areas really well and there are plenty of examples in Git, but teams keep running into the same issues. It's always either a bad copy paste or they forget to update a template variable. We've started looking into a custom templating solution for this. + +Backstage can help! With the Scaffolder, you can create a template that lets users fill in data through a form and uses that data to create a customized template output. This output is usually in the form of PRs to your various source control systems. Imagine you have a repo for traffic configuration, another for infrastructure management and a third for k8s manifests - with the Scaffolder, you can hide all of this complexity. You may still need to get reviews on the output PRs, but no more copy paste issues! + +## What does adopting Backstage look like? + +a.k.a "what am I signing myself up for?" + +Successfully adopting Backstage usually looks something like this, + +1. Setting up a PoC. +2. Getting leadership buy-in. +3. Identify a group of key stakeholders for the project and iterate with them aggressively. +4. Launch to the larger organization. +5. Drive Catalog adoption to 100%. +6. Your Backstage implementation starts to receive plugins from developers outside of your team. + +A truly successful Backstage implementation bridges delivering value to customers (developers), demonstrating returns to leadership, and fostering an inner source model. It's a long process but has huge dividends for those that achieve it! + +## Getting started + +Now that you know what to expect, let's walk through how to get started. + +:::note + +If you're non-technical, it is highly recommended to find a technical partner for help setting up a proof-of-concept for feedback. + +::: + +### Software Catalog + +Let's go to https://demo.backstage.io/ together. When you first navigate to the page, you will be brought to the Software Catalog page. This is a view of all projects currently registered with the (Demo) Backstage instance. There are a series of filters that you can play around with. If you're _really_ interested, we recommend reading through [the software catalog system model](../../features/software-catalog/system-model.md). + +Let's click into a Component, say "artist-lookup". This will bring you to a specialized view for that Component. Across the top, you can see tabs for "CI/CD", "API", "Dependencies", "Docs" and "TODOs". For your company, you can change this as you see fit. The important takeaway is that all of these tabs are automatically filtered for this Component which makes it easy to see how this could start to replace many navigation to other tools. + +### Scaffolder + +Let's go to https://demo.backstage.io/create now. This is the Scaffolder, a place to store reusable templates. Click the "Choose" button in the "Demo template". This will bring you to a form with some information to input. You don't need to fill this out. The main takeaway here is that this form is generated from YAML and doesn't require a frontend team to implement a custom form for each template you want to create. diff --git a/docs/golden-path/adoption/002-leadership-buy-in.md b/docs/golden-path/adoption/002-leadership-buy-in.md new file mode 100644 index 0000000000..502970d3dc --- /dev/null +++ b/docs/golden-path/adoption/002-leadership-buy-in.md @@ -0,0 +1,30 @@ +--- +id: leadership-buy-in +sidebar_label: 002 - Leadership buy-in +title: Getting leadership buy-in +--- + +## Summary + +In this section, we'll be going over what leadership needs to hear to buy in to your pitch for a developer portal. We expect that you have a good idea of the problem that you want Backstage to solve at your company. If not, we recommend you start small. Look for something that is consistently frustrating developers you work with (this can include you). User interviews are a great way to better understand what needs to improve. It may be IT blocking the creation of new Github repos or databases. It might be 5 hours per week of manual toil that your whole organization has to do. It might be a slow time to production for new services or slow provisioning of test environments. Every company will be different. There is no one size fits all answer we can give you - and if we could, it wouldn't be well-tailored for _your_ leadership team. + +## Milestones + +Every Backstage adoption journey has well-known milestones. + +1. You set up a PoC. +2. You get some users. +3. A group of users _really_ gets the value in the portal and jumps on it. They may even create their own plugins - great! +4. You start to plateau with catalog adoption or daily active users. +5. Leadership starts to get nosy about continued value. +6. You hit a crossroads. Your team either starts to think about building something themselves or going for another off the shelf option or they sit down and do the work to get out of the plateau. +7. If your organization made it this far, you likely now have blocking checks for catalog entries and Backstage is a weekly if not daily portal for your developers - congrats! + +Step 4 and 5 are painful moments. Successful Backstage adoptions can lose steam quickly. That's the nature of these things, the excitement will eventually run out and people will go back to their day jobs. Another YAML file or cataloging tool is just overhead and extra toil, regardless of the problem you're solving. Getting leadership on the same page about the value of Backstage is the first step to a very successful adoption story. + +### Recommendations + +1. Bring something real to your leadership team. This can either be a true proof of concept or [the demo site](https://demo.backstage.io). +2. Define metrics around what you're looking to drive up/down. That may be time to onboarding a new engineer, time to production for a new service, time to mitigate incidents, etc. As we say above, this is the meaty problem that is unique to your company that solving will really move the needle. +3. Lower the barrier to adoption. Many people see yet another YAML file as overhead. If you have an existing cataloging solution, use that to simplify the onboarding process. If you don't, this might be a good opportunity to do that work. +4. Knowledge silos. Every team has preferences on how to do things. Centralizing that data into a single interface while letting teams continue to do things how they want to is a powerful goal and something that Backstage can make happen. diff --git a/docs/golden-path/adoption/003-setting-up-a-poc.md b/docs/golden-path/adoption/003-setting-up-a-poc.md new file mode 100644 index 0000000000..155098c357 --- /dev/null +++ b/docs/golden-path/adoption/003-setting-up-a-poc.md @@ -0,0 +1,13 @@ +--- +id: setting-up-a-poc +sidebar_label: 003 - Setting up a PoC +title: Setting up a PoC +--- + +If you're non-technical, this section should be completed by your technical partner. + +Follow [our golden path for creating an app](../create-app/index.md). Once you have that set up, we recommend adding a few `catalog-info.yaml` files to a few repos/projects you own and setting up [the GitHub catalog provider](../../integrations/github/discovery.md). + +At this stage, you likely want to just get the instance running on your local machine. We'll go over preparing your instance for production at the end of chapter 2. + +You may be tempted to update the theme or add that one plugin your organization _needs_, but hold strong. We'll get there in chapter 3. diff --git a/docs/golden-path/adoption/004-first-stakeholder-feedback.md b/docs/golden-path/adoption/004-first-stakeholder-feedback.md new file mode 100644 index 0000000000..6207a49d10 --- /dev/null +++ b/docs/golden-path/adoption/004-first-stakeholder-feedback.md @@ -0,0 +1,17 @@ +--- +id: first-stakeholder-feedback +sidebar_label: 004 - Stakeholder Feedback +title: First round of stakeholder feedback +--- + +Now that you have a PoC running, let's walk through how to get good feedback. You likely aren't the first person to hear about Backstage or maybe not even the first person to set up a PoC. There may be common pitfalls unique to your company that are worth knowing about - political, organizational or otherwise. + +## Who to look for? + +This depends pretty significantly on your organizational structure. If you have a dedicated platform organization or platform team, start with them. They will either be the technical owners of this application from the go, or will eventually take over ownership. Be kind to them. If you aren't from that organization, we recommend finding your technical partner from somewhere in that organization. + +## What to listen for + +1. IT slowness. Does your organization run on tickets? Are there specific tasks that feel like they should be automated but aren't? +2. User toil. Your developers are super aware of what's slowing them down, they'll tell you the annoying manual parts of their job that they're hoping you can fix. +3. Data sprawl. What services are your users struggling to remember? What vendors are critical but most users only touch once a month? diff --git a/docs/golden-path/adoption/005-customizing-your-instance.md b/docs/golden-path/adoption/005-customizing-your-instance.md new file mode 100644 index 0000000000..9cc9a98d7a --- /dev/null +++ b/docs/golden-path/adoption/005-customizing-your-instance.md @@ -0,0 +1,15 @@ +--- +id: customize-your-instance +sidebar_label: 005 - Customizing your instance +title: Customizing your instance +--- + +You now have the knowledge of what your users want and the go from leadership to continue investing in Backstage. Your job now is to customize your instance for your users to really get the value from. Let's dive in! + +## Open Source or Build it yourself? + +There's a huge community of plugins available for easy installation at https://backstage.io/plugins. We would recommend that you start here for any needs you may be trying to solve. Building a plugin yourself requires significant effort and can be hard to justify early on in your adoption story. If there is a clear gap in the existing offerings for your company, you should build something yourself - otherwise, save yourself the maintenance overhead. + +## Customizing the theme + +Many organizations are tempted to spend a long time making sure the portal resembles their other offerings. This is important but shouldn't be a months long ordeal. Get it looking close enough and iterate. diff --git a/docs/golden-path/adoption/006-preparing-for-ga.md b/docs/golden-path/adoption/006-preparing-for-ga.md new file mode 100644 index 0000000000..0b62f0fa25 --- /dev/null +++ b/docs/golden-path/adoption/006-preparing-for-ga.md @@ -0,0 +1,19 @@ +--- +id: preparing-for-ga +sidebar_label: 006 - Preparing for GA +title: Preparing for GA +--- + +We hope at this point that the developers you're working with have read the [golden path on deploying Backstage](../deployment/index.md). Your Backstage instance should be ready for the scale that comes with a full company launch. + +## Launch Announcements + + + +## What to expect in the coming months + + + +## How to keep iterating + + diff --git a/docs/golden-path/adoption/007-plugin-ownership.md b/docs/golden-path/adoption/007-plugin-ownership.md new file mode 100644 index 0000000000..17455f19e7 --- /dev/null +++ b/docs/golden-path/adoption/007-plugin-ownership.md @@ -0,0 +1,17 @@ +--- +id: plugin-ownership +sidebar_label: 007 - Plugin Ownership +title: Plugin Ownership +--- + +You're now well on your way to a healthy Backstage instance! It's been launched to the whole company and you're loving the feedback developers are giving you. Some developers have even started broaching writing their own plugins. + +## Inner source + +Accepting internal contributions from other teams is a good sign that you are on the road to a developer portal tailored for your developers. This is a well paved path with many upsides, but a few downsides as well. As your Backstage instance grows in size and age, those same developers may be difficult to find. Your team may start to experience more and more struggle updating Backstage. + + + +## Registering Plugins in Your Catalog + + diff --git a/docs/golden-path/adoption/008-full-catalog.md b/docs/golden-path/adoption/008-full-catalog.md new file mode 100644 index 0000000000..1725bf6dd2 --- /dev/null +++ b/docs/golden-path/adoption/008-full-catalog.md @@ -0,0 +1,15 @@ +--- +id: full-catalog +sidebar_label: 008 - A Full Catalog +title: Ensuring your catalog stays complete +--- + +Along your Backstage journey (and any workflow migration journey), you will hit a point where incremental adoption is no longer easy. The new developers are no longer flowing into your tool like they once did. More and more projects are not being listed in your catalog. Something has to change. + +## Enforcing Catalog Files in CI + + + +## Leadership Initiatives + + diff --git a/docs/golden-path/adoption/meta.md b/docs/golden-path/adoption/__meta__.md similarity index 98% rename from docs/golden-path/adoption/meta.md rename to docs/golden-path/adoption/__meta__.md index 1c4756d8ee..05fd50a2f6 100644 --- a/docs/golden-path/adoption/meta.md +++ b/docs/golden-path/adoption/__meta__.md @@ -19,6 +19,8 @@ Users should already have read through the summary section of the docs, "What is We recommend you poke around the demo site to get a feel for what Backstage can provide. If you're technical, or working with someone technical, you can run through the steps in `golden-path/create-app` and `golden-path/deploying-backstage` to get something running for just your company. +## Getting leadership buy-in + ## First round of stakeholder feedback If you think Backstage is a good fit for your company, it's likely there are others that do or will think the same. You may have already identified them. For this initial round of feedback, share recommendations for what that group should look like, is there any required number of technical or non-technical members, do you need leadership involved at this point, etc. @@ -29,9 +31,7 @@ For non-technical users, it's recommended to find a technical partner to help st At this point, we're assuming you already have an instance created through `golden-path/create-app` and `golden-path/deploying-backstage`. You should now start customizing it to your company's needs. We recommend you start small, write some catalog-info YAML files and start to build a personalized catalog. -## Second round of stakeholder feedback - -## Getting leadership buy-in +## Preparing for GA ## Plugin ownership and inner source mentality diff --git a/docs/golden-path/deployment/meta.md b/docs/golden-path/deployment/__meta__.md similarity index 100% rename from docs/golden-path/deployment/meta.md rename to docs/golden-path/deployment/__meta__.md diff --git a/docs/golden-path/deployment/index.md b/docs/golden-path/deployment/index.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/golden-path/plugins/__meta__.md b/docs/golden-path/plugins/__meta__.md new file mode 100644 index 0000000000..ab19c569d9 --- /dev/null +++ b/docs/golden-path/plugins/__meta__.md @@ -0,0 +1,15 @@ + + +## Why build plugins? + +This section should clearly explain why you should build a new plugin. The Backstage framework is deeply empowered by plugins and plugins are core to the project's success. Users should walk away from reading this section with a conviction that plugins are the right path for new functionality. + +## Sustainable plugin development + +Plugins are not developed in a vacuum. Users should reach for them to solve specific business problems facing their developers, for example, you may be tasked to create + +- a new vendor integration like PagerDuty, +- a new plugin backend that talks to an internal service, +- etc. + +This section should contain learnings from successful Backstage deployments about how to engage with stakeholders, how/when to iterate on your plugin, and setting yourself up for future success. diff --git a/docs/golden-path/plugins/backend/001-first-steps.md b/docs/golden-path/plugins/backend/001-first-steps.md index 1850ba52c1..8a0defe183 100644 --- a/docs/golden-path/plugins/backend/001-first-steps.md +++ b/docs/golden-path/plugins/backend/001-first-steps.md @@ -1,5 +1,5 @@ --- -id: 001-first-steps +id: first-steps sidebar_label: 001 - Scaffolding the plugin title: How to scaffold a new plugin? --- diff --git a/docs/golden-path/plugins/backend/002-poking-around.md b/docs/golden-path/plugins/backend/002-poking-around.md index 35de3c4d47..596de01b4b 100644 --- a/docs/golden-path/plugins/backend/002-poking-around.md +++ b/docs/golden-path/plugins/backend/002-poking-around.md @@ -1,5 +1,5 @@ --- -id: 002-poking-around +id: poking-around sidebar_label: 002 - Poking around title: 002 - Poking around --- @@ -16,7 +16,7 @@ To make this plugin production ready, we'll need to adjust a few things, ## Testing locally -Before we jump in to making this plugin ready to ship, let's walk through how to run it locally. If you open your backend plugin's manifest (`plugins/todo-backend/package.json`), and look at the `scripts` section, you'll notice a few important commands. The ones relevant to use right now are +Before we jump in to making this plugin ready to ship, let's walk through how to run it locally. If you open your backend plugin's manifest (`plugins/todo-backend/package.json`), and look at the `scripts` section, you'll notice a few important commands. The ones relevant to us right now are 1. `yarn start` - Starts a local development server using the content in `dev/index.ts` as the backend. 2. `yarn test` - Runs all of the tests for your backend plugin. @@ -27,8 +27,43 @@ If you run `yarn start`, you should see a custom backend for just your plugin st 2025-06-08T16:14:53.229Z rootHttpRouter info Listening on :7007 ``` -This indicates that your HTTP server is up and running and we can start sending test HTTP requests. Grab your favorite HTTP client and let's get testing! If you aren't sure what to use, I'd recommend the `humao.rest-client` VSCode extension which can easily be run in VSCode itself with very little extra set up. +This indicates that your HTTP server is up and running and we can start sending test HTTP requests. Grab your favorite HTTP client and let's get testing! +To start, let's make sure we're starting from a clean slate. You can list all existing TODOs by running the command below. You should get back an empty list: + +```sh +curl http://localhost:7007/api/todo/todos ``` +To create a new TODO, you can send a POST request to the same endpoint. However, you will get a 401 error right now. + +```sh +curl -X POST http://localhost:7007/api/todo/todos \ +-H 'Content-Type: application/json; charset=utf-8' \ +--data-binary @- << EOF +{ + "title": "My Todo" +} +EOF ``` + +The 401 error is because we track the ID of the user that created each TODO. If you send a request to create a TODO but don't provide an `Authorization` header, you will see this failure. For plugins that have a frontend as well, this credential management should happen automatically. Let's try this: + +```sh +curl -v -X POST http://localhost:7007/api/todo/todos \ +-H 'Content-Type: application/json; charset=utf-8' \ +-H "Authorization: Bearer $(curl -s http://localhost:7007/api/auth/guest/refresh | jq -r '.backstageIdentity.token')" \ +--data-binary @- << EOF +{ + "title": "My Todo" +} +EOF +``` + +We can then list all TODOs to see our new TODO! + +```sh +curl http://localhost:7007/api/todo/todos +``` + +You'll notice that `createdBy` is `user:development/guest` which is the token we used to create the TODO. That's the `-H "Authorization: Bearer $(curl -s http://localhost:7007/api/auth/guest/refresh | jq -r '.backstageIdentity.token')"` part of the request. diff --git a/docs/golden-path/plugins/backend/003-persistence.md b/docs/golden-path/plugins/backend/003-persistence.md new file mode 100644 index 0000000000..ba00b2f417 --- /dev/null +++ b/docs/golden-path/plugins/backend/003-persistence.md @@ -0,0 +1,19 @@ +--- +id: persistence +sidebar_label: 003 - Persisting your TODOs +title: 003 - Persisting your TODOs +--- + +## Saving Plugin State Indefinitely + +You may have noticed that your list of TODOs disappears after you restart your Backstage backend. The general flow to restart your backend without having to rerun `yarn start` is to press ENTER on the terminal running `yarn start`. This will force the Backstage backend to restart completely, wiping out any in memory data and starting everything from scratch -- everything except your database. + +### Quick intro to SQLite + +SQLite is the default database for local development. It runs in memory (and can also run from a file on disk). It supports quick iteration cycles and can be easily deleted if anything goes wrong. + +## Adding the `databaseService` to your plugin + + + +## Testing your changes diff --git a/docs/golden-path/plugins/backend/004-source-tracked.md b/docs/golden-path/plugins/backend/004-source-tracked.md new file mode 100644 index 0000000000..074cc85cf1 --- /dev/null +++ b/docs/golden-path/plugins/backend/004-source-tracked.md @@ -0,0 +1,19 @@ +--- +id: source-tracked +sidebar_label: 004 - Integrating with SCMs +title: 004 - Git-tracked TODOs +--- + +Problem: You have TODOs in your source code that you want to ingest with your plugin. + +## Authenticating + + + +## Querying + + + +## Fetching + + diff --git a/docs/golden-path/plugins/backend/005-testing.md b/docs/golden-path/plugins/backend/005-testing.md new file mode 100644 index 0000000000..9643e8a487 --- /dev/null +++ b/docs/golden-path/plugins/backend/005-testing.md @@ -0,0 +1,29 @@ +--- +id: testing +sidebar_label: 005 - Unit testing your plugin +title: 005 - Testing +--- + +## Testing is important + +We've done a lot of manual testing up to this point of functionality. Let's start putting those assumptions into code that we can run on every change to ensure things are working correctly. + +## Router-level testing + + + +## Plugin-level testing + + + +## OpenAPI testing + + + +### Integration with Jest tests + + + +### Fuzzing + + diff --git a/docs/golden-path/plugins/backend/__meta__.md b/docs/golden-path/plugins/backend/__meta__.md index e261a8f970..d78e6a7573 100644 --- a/docs/golden-path/plugins/backend/__meta__.md +++ b/docs/golden-path/plugins/backend/__meta__.md @@ -1,36 +1,7 @@ -# Glossary - -- Page: A single `md` file. -- Guide: A number of pages grouped under the same folder. - -# Overall Writing Guidelines - -The goal of these docs is to provide a comprehensive set of guides that developers + admins can use to quickly get up to speed with plugin development, and then refer to as they're developing their own plugins. - -A user that finishes all of these guides will feel comfortable implementing plugins on their own. If additional assistance is required, they should be referred to other sources of information such as Discord, GitHub, source code, or documentation for further support. The user will also understand why/when to build their own plugins, inner-sourcing their developer portal and contributing internal plugins back to the open-source project. - -At the same time, not all users will finish the docs or they may come back to them as required. Individual guides should have strong "abstracts" (what will I learn by reading this guide), table of contents, and "next steps" (what do I need to do next) to guide users to read the most important pieces for their work. - -When writing guide pages, keep it light! These should be instructional docs, and at the same time conversational and a joy to read. Guides should build on each other, when reading through a progression, the reader should feel more comfortable and confident with concepts as they pop up across progression levels. Guides should be standalone, when finishing one level (for example 101), you should be able to immediately jump into the next (201) without additional research or background. Referencing previous progression levels is ok. - # Sections -## Why build plugins? - -This section should answer definitely why you should build a new plugin. The Backstage framework is deeply empowered by plugins and plugins are core to the project's success. Users should walk away from reading this section with a conviction that plugins are the right path for new functionality. - -## Sustainable plugin development - -Plugins are not developed in a vacuum. Users should reach for them to solve specific business problems facing their developers, for example, you may be tasked to create - -- a new vendor integration like PagerDuty, -- a new plugin backend that talks to an internal service, -- etc. - -This section should contain learnings from successful Backstage deployments about how to engage with stakeholders, how/when to iterate on your plugin, and setting yourself up for future success. - ## Creating your first plugin This section should be extremely deliberate in showing readers every step of the way to create a plugin using Backstage's best practices. A reader that finishes this section should feel extremely comfortable creating new plugins and how to install and use plugins regardless of their experience with JS/TS and Backstage. @@ -78,12 +49,6 @@ app.get('/list', async (req, res) => { }); ``` -### Testing - -Let's write a unit test using `supertest` to make sure that everything is working as expected. - -After verifying everything, introduce the problem of persistence - the todos aren't saved across reloads. - ### Persistence Saving values to the database. Writing a migrations file. Plumbing through the database service. @@ -91,3 +56,7 @@ Saving values to the database. Writing a migrations file. Plumbing through the d ## SCM Integrations Our users love the new plugin, and now they want it to automatically fetch todos from their source code. + +### Testing + +Let's write a unit test using `supertest` to make sure that everything is working as expected. diff --git a/docs/golden-path/plugins/backend/todo.http b/docs/golden-path/plugins/backend/todo.http deleted file mode 100644 index 932dcb76e4..0000000000 --- a/docs/golden-path/plugins/backend/todo.http +++ /dev/null @@ -1,12 +0,0 @@ -POST http://localhost:7007/api/todo/todos -Content-Type: application/json - -{ - "title": "My First TODO" -} - -### - -GET http://localhost:7007/api/todo/todos - -### \ No newline at end of file diff --git a/docs/golden-path/plugins/frontend/001-first-steps.md b/docs/golden-path/plugins/frontend/001-first-steps.md new file mode 100644 index 0000000000..d6c21ed053 --- /dev/null +++ b/docs/golden-path/plugins/frontend/001-first-steps.md @@ -0,0 +1,15 @@ +--- +id: first-steps +sidebar_label: 001 - Scaffolding the plugin +title: How to scaffold a new plugin? +--- + +Running `yarn new` -> `frontend-plugin`. + +## What did we create? + + + +## Common issues + + diff --git a/docs/golden-path/plugins/frontend/002-poking-around.md b/docs/golden-path/plugins/frontend/002-poking-around.md new file mode 100644 index 0000000000..e85ea235f3 --- /dev/null +++ b/docs/golden-path/plugins/frontend/002-poking-around.md @@ -0,0 +1,17 @@ +--- +id: poking-around +sidebar_label: 002 - Poking around +title: 002 - Poking around +--- + +Our frontend TODO plugin is a bit more simplistic than the backend one. We need to implement a new UI to replace the example components we have. + +Let's use this React component to start. Copy this to `plugins/todo/src/components/TodoList.tsx`. + +```tsx +// todo +``` + +### Data Mocking + +You already have a backend with dynamic data. Let's start a little smaller. Using hard coded data can be a great way to iterate quickly. diff --git a/docs/golden-path/plugins/frontend/003-dynamic-config.md b/docs/golden-path/plugins/frontend/003-dynamic-config.md new file mode 100644 index 0000000000..8eecd72c2f --- /dev/null +++ b/docs/golden-path/plugins/frontend/003-dynamic-config.md @@ -0,0 +1,29 @@ +--- +id: dynamic-config +sidebar_label: 003 - Dynamic Config +title: 003 - Dynamic Config +--- + +Your plugin should have been generated by default for the New Frontend System which is config-first. That means you can easily control your frontend components through your `app-config.yaml`. + +Let's try this quickly by disabling our entire TODO page, + +```yaml +# TODO +``` + +We can also do really cool things like provide React props directly through config. Let's try moving our hard coded list of TODOs to config instead, + +```tsx +// todo +``` + +and the config, + +```yaml +# TODO +``` + +### Why does this work? + + diff --git a/docs/golden-path/plugins/frontend/004-http-client.md b/docs/golden-path/plugins/frontend/004-http-client.md new file mode 100644 index 0000000000..b6190f0f5f --- /dev/null +++ b/docs/golden-path/plugins/frontend/004-http-client.md @@ -0,0 +1,17 @@ +--- +id: http-client +sidebar_label: 004 - HTTP Client +title: 004 - HTTP Client +--- + +Now, let's really make our page dynamic. We'll start by writing an HTTP client by hand. + +```tsx +class TodoClient { + // TODO +} +``` + +## OpenAPI Generated Clients + +You can also skip a step and ensure your frontend and backend stay in sync by generating the client from an OpenAPI schema. diff --git a/docs/golden-path/plugins/frontend/005-testing.md b/docs/golden-path/plugins/frontend/005-testing.md new file mode 100644 index 0000000000..fe385d7169 --- /dev/null +++ b/docs/golden-path/plugins/frontend/005-testing.md @@ -0,0 +1,15 @@ +--- +id: testing +sidebar_label: 005 - Testing +title: 005 - Testing +--- + +Everyone's favorite part! Let's make sure our components continue to work even when we're not able to validate the changes. + +## Unit Tests + +Use Jest + RTL + MSW v2. + +## Integration Tests + +Use Playwright. diff --git a/docs/golden-path/plugins/frontend/__meta__.md b/docs/golden-path/plugins/frontend/__meta__.md new file mode 100644 index 0000000000..088d72ce90 --- /dev/null +++ b/docs/golden-path/plugins/frontend/__meta__.md @@ -0,0 +1,34 @@ + + +# Sections + +## Creating your first plugin + +### Scaffolding a new plugin + +Talk through how to run the `backstage-cli create` command as well as what the output it creates is. This should touch on why we install this into `packages/app`. + +### Debugging + +How to handle common errors. + +## First steps with the new plugin + +### Creating a todo plugin + +We're going to be creating the frontend for a todo list plugin. We want the user to be able to create todos for themselves and show the user their current list of todos. + +To start, we'll use a list of mocked data. + +### Controlling your component dynamically + +Update the mocked data to be controlled by config. + +### HTTP API + +We want our todo plugin to reach the backend that we implemented in [the backend plugin Golden Path](../backend/001-first-steps.md). Let's write a client to do this (or use OpenAPI to generate a client for us). + +### Testing + +Unit tests - Let's write a unit test using React Testing Library to make sure that everything is working as expected. +Integration tests - Let's write an integration test using Playwright to _really_ make sure everything is working. diff --git a/docs/golden-path/plugins/integrations/001-catalog.md b/docs/golden-path/plugins/integrations/001-catalog.md new file mode 100644 index 0000000000..f5ed04116e --- /dev/null +++ b/docs/golden-path/plugins/integrations/001-catalog.md @@ -0,0 +1,23 @@ +--- +id: catalog +sidebar_label: 001 - Catalog +title: Integrating with Catalog +--- + +## Software Catalog + +### What is the Software Catalog? + + + +### Integration Points + + + +## Adding a new `backstage.io/todo` annotation + + + +## Custom TODO Entity Kind + + diff --git a/docs/golden-path/plugins/integrations/002-search.md b/docs/golden-path/plugins/integrations/002-search.md new file mode 100644 index 0000000000..b0ef762f27 --- /dev/null +++ b/docs/golden-path/plugins/integrations/002-search.md @@ -0,0 +1,19 @@ +--- +id: search +sidebar_label: 002 - Search +title: Integrating with Search +--- + +## Search + +### What is Backstage Search? + + + +### Common integration points + + + +## Creating a custom TODO collator + + diff --git a/docs/golden-path/plugins/integrations/003-permissions.md b/docs/golden-path/plugins/integrations/003-permissions.md new file mode 100644 index 0000000000..167d40d47f --- /dev/null +++ b/docs/golden-path/plugins/integrations/003-permissions.md @@ -0,0 +1,23 @@ +--- +id: permissions +sidebar_label: 003 - Permissions +title: Integrating with the Permission framework +--- + +## Permissions + +### What is the Permissions framework? + + + +### Common integration points + + + +## Creating private TODOs + + + +## Restricting who can create TODOs + + diff --git a/docs/golden-path/plugins/integrations/004-notifications.md b/docs/golden-path/plugins/integrations/004-notifications.md new file mode 100644 index 0000000000..23c44b4721 --- /dev/null +++ b/docs/golden-path/plugins/integrations/004-notifications.md @@ -0,0 +1,23 @@ +--- +id: notifications +sidebar_label: 004 - Notifications +title: Integrating with Notifications +--- + +## Notifications + +### What are Backstage Notifications? + + + +### Common integration points + + + +## TODO with an alarm + + + +## Create TODOs for other people and notify them + +