Merge pull request #28076 from davidfestal/new-frontend-dynamic-features

Dynamic frontend features based on module federation and the new frontend system
This commit is contained in:
Patrik Oldsberg
2025-04-15 12:38:01 +02:00
committed by GitHub
61 changed files with 3977 additions and 265 deletions
+10
View File
@@ -0,0 +1,10 @@
---
'@backstage/backend-dynamic-feature-service': patch
'@backstage/frontend-dynamic-feature-loader': minor
'@backstage/frontend-defaults': patch
---
The new package `frontend-dynamic-features-loader` provides a frontend feature loader that dynamically
loads frontend features based on the new frontend system and exposed as module federation remotes.
This new frontend feature loader works hand-in-hand with a new server of frontend plugin module federation
remotes, which is added as part of backend dynamic feature service in package `@backstage/backend-dynamic-feature-service`.
@@ -38,6 +38,9 @@
"scripts": {
"build": "backstage-cli package build",
"clean": "backstage-cli package clean",
"diff": "backstage-repo-tools package schema openapi diff",
"fuzz": "backstage-repo-tools package schema openapi fuzz --exclude-checks response_schema_conformance",
"generate": "backstage-repo-tools package schema openapi generate --server --client-package packages/frontend-dynamic-feature-loader",
"lint": "backstage-cli package lint",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack",
@@ -46,6 +49,7 @@
},
"dependencies": {
"@backstage/backend-defaults": "workspace:^",
"@backstage/backend-openapi-utils": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/cli-common": "workspace:^",
"@backstage/cli-node": "workspace:^",
@@ -64,9 +68,11 @@
"@backstage/plugin-search-common": "workspace:^",
"@backstage/types": "workspace:^",
"@manypkg/get-packages": "^1.1.3",
"@module-federation/sdk": "^0.9.0",
"@types/express": "^4.17.6",
"chokidar": "^3.5.3",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"fs-extra": "^11.2.0",
"lodash": "^4.17.21",
"winston": "^3.2.1"
@@ -76,7 +82,9 @@
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/plugin-app-backend": "workspace:^",
"@backstage/repo-tools": "workspace:^",
"triple-beam": "^1.4.1",
"wait-for-expect": "^3.0.2"
}
},
"configSchema": "config.d.ts"
}
@@ -15,6 +15,7 @@ import { EventsService } from '@backstage/plugin-events-node';
import { HttpPostIngressOptions } from '@backstage/plugin-events-node';
import { IdentityApi } from '@backstage/plugin-auth-node';
import { IndexBuilder } from '@backstage/plugin-search-backend-node';
import { JsonObject } from '@backstage/types';
import { Logger } from 'winston';
import { LoggerService } from '@backstage/backend-plugin-api';
import { PackagePlatform } from '@backstage/cli-node';
@@ -31,6 +32,9 @@ import { TemplateAction } from '@backstage/plugin-scaffolder-node';
import { UrlReaderService } from '@backstage/backend-plugin-api';
import { WinstonLoggerOptions } from '@backstage/backend-defaults/rootLogger';
// @public (undocumented)
export type AdditionalRemoteInfo = Omit<RemoteInfo, 'name' | 'entry'>;
// @public (undocumented)
export interface BackendDynamicPlugin extends BaseDynamicPlugin {
// (undocumented)
@@ -158,9 +162,22 @@ export type DynamicPluginsFeatureLoaderOptions = DynamicPluginsFactoryOptions &
logger?: (config?: Config) => DynamicPluginsRootLoggerFactoryOptions;
};
// @public (undocumented)
export interface DynamicPluginsFrontendRemotesService {
// (undocumented)
setResolverProvider(provider: FrontendRemoteResolverProvider): void;
}
// @public @deprecated (undocumented)
export const dynamicPluginsFrontendSchemas: BackendFeature;
// @public
export const dynamicPluginsFrontendServiceRef: ServiceRef<
DynamicPluginsFrontendRemotesService,
'root',
'singleton'
>;
// @public (undocumented)
export type DynamicPluginsRootLoggerFactoryOptions = Omit<
WinstonLoggerOptions,
@@ -224,6 +241,31 @@ export interface FrontendPluginProvider {
}): FrontendDynamicPlugin[];
}
// @public (undocumented)
export type FrontendRemoteResolver = {
assetsPathFromPackage?: string;
manifestFileName?: string;
getRemoteEntryType?: (
manifestContent: JsonObject,
) => 'manifest' | 'javascript';
getAdditionaRemoteInfo?: (
manifestContent: JsonObject,
) => AdditionalRemoteInfo;
overrideExposedModules?: (
exposedModules: string[],
manifestContent: JsonObject,
) => string[];
customizeManifest?: (content: JsonObject) => JsonObject;
};
// @public (undocumented)
export type FrontendRemoteResolverProvider = {
for(
pluginName: string,
pluginPackagePath: string,
): Partial<FrontendRemoteResolver> | undefined;
};
// @public (undocumented)
export function isBackendDynamicPluginInstaller(
obj: any,
@@ -291,6 +333,39 @@ export interface NewBackendPluginInstaller {
kind: 'new';
}
// @public
export interface RemoteInfo {
entry: string;
// (undocumented)
entryGlobalName?: string;
name: string;
// (undocumented)
shareScope?: string;
// (undocumented)
type?: RemoteInfoTypeEnum;
}
// @public (undocumented)
export type RemoteInfoTypeEnum =
| 'var'
| 'module'
| 'assign'
| 'assign-properties'
| 'this'
| 'window'
| 'self'
| 'global'
| 'commonjs'
| 'commonjs2'
| 'commonjs-module'
| 'commonjs-static'
| 'amd'
| 'amd-require'
| 'umd'
| 'umd2'
| 'jsonp'
| 'system';
// @public (undocumented)
export type ScannedPluginManifest = BackstagePackageJson &
Required<Pick<BackstagePackageJson, 'main'>> &
@@ -31,27 +31,8 @@ const testPlugin = backendPluginApi.createBackendPlugin({
metadata,
}) {
logger.info("This secret value should be hidden by the dynamic-plugin-aware logger: AVerySecretValue");
const externalBaseUrl = await discovery.getExternalBaseUrl(metadata.getId());
const router = express.Router();
const frontendPluginsIndexPath = privateDep.frontendPluginsIndexPath;
const frontendPluginManifests = Object.fromEntries(dynamicPlugins.frontendPlugins().map(fp => {
const pluginScannedPackage = dynamicPlugins.getScannedPackage(fp);
const pkgDistLocation = path.resolve(
url.fileURLToPath(pluginScannedPackage.location),
'dist',
);
router.use(`/${frontendPluginsIndexPath}/${fp.name}`, express.static(pkgDistLocation))
return [fp.name, `${externalBaseUrl}/${frontendPluginsIndexPath}/${fp.name}/mf-manifest.json`]
}));
router.get(`/${frontendPluginsIndexPath}`, (req, res) => {
res.status(200).json(frontendPluginManifests);
});
http.use(router);
http.addAuthPolicy({
path: `/`,
allow: 'unauthenticated',
});
const messageFromPrivateDep = privateDep.message;
logger.info(messageFromPrivateDep);
}
});
}
@@ -2,6 +2,6 @@
Object.defineProperty(exports, '__esModule', { value: true });
const frontendPluginsIndexPath = 'frontend-plugins';
const message = 'This message comes from a plugin private dependency';
exports.frontendPluginsIndexPath = frontendPluginsIndexPath;
exports.message = message;
@@ -0,0 +1,25 @@
{
"schemas": [
{
"packageName": "plugin-test-dynamic",
"path": "./test-dynamic",
"value": {
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"test-frontend": {
"type": "object",
"required": ["frontendValue"],
"properties": {
"frontendValue": {
"type": "string",
"visibility": "frontend"
}
}
}
}
}
}
],
"backstageConfigSchemaVersion": 1
}
@@ -0,0 +1,36 @@
{
"id": "backstage__plugin_test",
"name": "backstage__plugin_test",
"metaData": {
"name": "backstage__plugin_test",
"type": "app",
"buildInfo": {
"buildVersion": "0.0.0",
"buildName": "@backstage/plugin-test"
},
"remoteEntry": {
"name": "remoteEntry.js",
"path": "",
"type": "global"
},
"types": {
"path": "",
"name": "",
"zip": "",
"api": ""
},
"globalName": "backstage__plugin_test",
"pluginVersion": "0.0.0",
"publicPath": "auto"
},
"shared": [],
"remotes": [],
"exposes": [
{
"name": "."
},
{
"name": "alpha"
}
]
}
@@ -0,0 +1,17 @@
/*
* Copyright 2023 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.
*/
(function doNothing() {})();
@@ -0,0 +1,27 @@
{
"schemas": [
{
"packageName": "plugin-test-dynamic",
"path": "./test-dynamic",
"value": {
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"test-frontend": {
"type": "object",
"required": [
"frontendValue"
],
"properties": {
"frontendValue": {
"type": "string",
"visibility": "frontend"
}
}
}
}
}
}
],
"backstageConfigSchemaVersion": 1
}
@@ -1,18 +0,0 @@
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"test-frontend": {
"type": "object",
"required": [
"frontendValue"
],
"properties": {
"frontendValue": {
"type": "string",
"visibility": "frontend"
}
}
}
}
}
@@ -0,0 +1,33 @@
{
"id": "backstage__plugin_test",
"name": "backstage__plugin_test",
"metaData": {
"name": "backstage__plugin_test",
"type": "app",
"buildInfo": {
"buildVersion": "0.0.0",
"buildName": "@backstage/plugin-test"
},
"remoteEntry": {
"name": "remoteEntry-not-found.js",
"path": "",
"type": "global"
},
"types": {
"path": "",
"name": "",
"zip": "",
"api": ""
},
"globalName": "backstage__plugin_test",
"pluginVersion": "0.0.0",
"publicPath": "auto"
},
"shared": [],
"remotes": [],
"exposes": [{
"name": "."
},{
"name": "alpha"
}]
}
@@ -0,0 +1,32 @@
{
"id": "backstage__plugin_test",
"metaData": {
"name": "backstage__plugin_test",
"type": "app",
"buildInfo": {
"buildVersion": "0.0.0",
"buildName": "@backstage/plugin-test"
},
"remoteEntry": {
"name": "remoteEntry.js",
"path": "",
"type": "global"
},
"types": {
"path": "",
"name": "",
"zip": "",
"api": ""
},
"globalName": "backstage__plugin_test",
"pluginVersion": "0.0.0",
"publicPath": "auto"
},
"shared": [],
"remotes": [],
"exposes": [{
"name": "."
},{
"name": "alpha"
}]
}
@@ -25,5 +25,9 @@
},
"shared": [],
"remotes": [],
"exposes": []
"exposes": [{
"name": "."
},{
"name": "alpha"
}]
}
@@ -35,6 +35,10 @@ import * as url from 'url';
import { MESSAGE } from 'triple-beam';
import { overridePackagePathResolution } from '@backstage/backend-plugin-api/testUtils';
import { ScannedPluginPackage } from '../scanner';
import {
dynamicPluginsFrontendServiceRef,
FrontendRemoteResolverProvider,
} from '../server/frontendRemotesServer';
// these can get a bit slow in CI
jest.setTimeout(60_000);
@@ -117,6 +121,9 @@ describe('dynamicPluginsFeatureLoader', () => {
dynamicPlugins: {
rootDirectory: dynamicPluginsRootDirectory,
},
backend: {
baseUrl: `http://localhost:0`,
},
},
}),
dynamicPluginsFeatureLoader({
@@ -161,6 +168,9 @@ describe('dynamicPluginsFeatureLoader', () => {
dynamicPlugins: {
rootDirectory: dynamicPluginsRootDirectory,
},
backend: {
baseUrl: `http://localhost:0`,
},
},
}),
dynamicPluginsFeatureLoader({
@@ -211,6 +221,9 @@ Require stack:
dynamicPlugins: {
rootDirectory: dynamicPluginsRootDirectory,
},
backend: {
baseUrl: `http://localhost:0`,
},
},
}),
dynamicPluginsFeatureLoader({
@@ -250,6 +263,9 @@ Require stack:
rootDirectory: dynamicPluginsRootDirectory,
},
customLogLabel: 'a very nice label',
backend: {
baseUrl: `http://localhost:0`,
},
},
}),
dynamicPluginsFeatureLoader({
@@ -289,6 +305,9 @@ Require stack:
'test-backend': {
secretValue: 'AVerySecretValue',
},
backend: {
baseUrl: `http://localhost:0`,
},
},
}),
dynamicPluginsFeatureLoader({
@@ -342,6 +361,9 @@ Require stack:
'test-frontend': {
frontendValue: 'AFrontendValue',
},
backend: {
baseUrl: `http://localhost:0`,
},
},
}),
dynamicPluginsFeatureLoader({
@@ -371,52 +393,6 @@ Require stack:
</head>`);
});
it('should access the module federation assets of the frontend plugin through the backend plugin', async () => {
const { server } = await startTestBackend({
features: [
mockServices.rootConfig.factory({
data: {
dynamicPlugins: {
rootDirectory: dynamicPluginsRootDirectory,
},
},
}),
dynamicPluginsFeatureLoader({
moduleLoader: logger =>
jestFreeTypescriptAwareModuleLoader({ logger }),
}),
],
});
const list = await fetch(
`http://localhost:${server.port()}/api/test/frontend-plugins`,
);
expect(list.ok).toBe(true);
expect(await list.json()).toEqual({
'plugin-test-dynamic': `http://localhost:${server.port()}/api/test/frontend-plugins/plugin-test-dynamic/mf-manifest.json`,
});
const manifest = await fetch(
`http://localhost:${server.port()}/api/test/frontend-plugins/plugin-test-dynamic/mf-manifest.json`,
);
expect(manifest.ok).toBe(true);
expect(await manifest.json()).toMatchObject({
exposes: [],
id: 'backstage__plugin_test',
name: 'backstage__plugin_test',
metaData: {
buildInfo: {
buildName: '@backstage/plugin-test',
buildVersion: '0.0.0',
},
globalName: 'backstage__plugin_test',
name: 'backstage__plugin_test',
pluginVersion: '0.0.0',
publicPath: 'auto',
},
});
});
it('should load a backend plugin from the alpha package first', async () => {
const dynamicPLuginsLister = new DynamicPluginLister();
const mockedTransport = new MockedTransport();
@@ -431,6 +407,9 @@ Require stack:
dynamicPlugins: {
rootDirectory: dynamicPluginsRootForAlpha,
},
backend: {
baseUrl: `http://localhost:0`,
},
},
}),
dynamicPluginsFeatureLoader({
@@ -485,4 +464,385 @@ Require stack:
},
});
});
describe('module federation support', () => {
const createRemoteProviderPlugin = (
provider: FrontendRemoteResolverProvider,
) =>
createBackendPlugin({
pluginId: 'test-remote-provider',
register(reg) {
reg.registerInit({
deps: {
frontendRemotes: dynamicPluginsFrontendServiceRef,
},
async init({ frontendRemotes }) {
frontendRemotes.setResolverProvider(provider);
},
});
},
});
it('should access the module federation assets of the frontend plugin through the backend plugin', async () => {
const { server } = await startTestBackend({
features: [
mockServices.rootConfig.factory({
data: {
dynamicPlugins: {
rootDirectory: dynamicPluginsRootDirectory,
},
backend: {
baseUrl: `http://localhost:0`,
},
},
}),
dynamicPluginsFeatureLoader({
moduleLoader: logger =>
jestFreeTypescriptAwareModuleLoader({ logger }),
}),
],
});
const list = await fetch(
`http://localhost:${server.port()}/.backstage/dynamic-features/remotes`,
);
expect(list.ok).toBe(true);
expect(await list.json()).toEqual([
{
packageName: 'plugin-test-dynamic',
exposedModules: ['.', 'alpha'],
remoteInfo: {
name: 'backstage__plugin_test',
entry:
'http://localhost:0/.backstage/dynamic-features/remotes/plugin-test-dynamic/mf-manifest.json',
},
},
]);
const manifest = await fetch(
`http://localhost:${server.port()}/.backstage/dynamic-features/remotes/plugin-test-dynamic/mf-manifest.json`,
);
expect(manifest.ok).toBe(true);
expect(await manifest.json()).toMatchObject({
exposes: [{ name: '.' }, { name: 'alpha' }],
id: 'backstage__plugin_test',
name: 'backstage__plugin_test',
metaData: {
buildInfo: {
buildName: '@backstage/plugin-test',
buildVersion: '0.0.0',
},
globalName: 'backstage__plugin_test',
name: 'backstage__plugin_test',
pluginVersion: '0.0.0',
publicPath: 'auto',
},
});
});
it('should allow overriding the module federation assets folder, return the Javascript remote entry, the exposed modules and additional remote intry info fields', async () => {
const mockedTransport = new MockedTransport();
const { server } = await startTestBackend({
features: [
mockServices.rootConfig.factory({
data: {
dynamicPlugins: {
rootDirectory: dynamicPluginsRootDirectory,
},
backend: {
baseUrl: `http://localhost:0`,
},
},
}),
createRemoteProviderPlugin({
for: () => ({
assetsPathFromPackage: 'dist-alternate',
getRemoteEntryType: () => 'javascript',
getAdditionaRemoteInfo: manifest => ({
type: (manifest as any).metaData.remoteEntry.type,
}),
overrideExposedModules: exposedModules =>
exposedModules.filter(name => name !== 'alpha'),
}),
}),
dynamicPluginsFeatureLoader({
moduleLoader: logger =>
jestFreeTypescriptAwareModuleLoader({ logger }),
logger: () => ({
transports: [mockedTransport],
format: winston.format.simple(),
}),
}),
],
});
const list = await fetch(
`http://localhost:${server.port()}/.backstage/dynamic-features/remotes`,
);
expect(list.ok).toBe(true);
expect(await list.json()).toEqual([
{
exposedModules: ['.'],
packageName: 'plugin-test-dynamic',
remoteInfo: {
entry:
'http://localhost:0/.backstage/dynamic-features/remotes/plugin-test-dynamic/remoteEntry.js',
name: 'backstage__plugin_test',
type: 'global',
},
},
]);
expect(mockedTransport.logs).toContainEqual(
expect.stringMatching(
`Exposed dynamic frontend plugin 'plugin-test-dynamic' from '.*/dist-alternate' `,
),
);
});
it('should allow customizing the module federation manifest when returning it as the remote entry', async () => {
const mockedTransport = new MockedTransport();
const { server } = await startTestBackend({
features: [
mockServices.rootConfig.factory({
data: {
dynamicPlugins: {
rootDirectory: dynamicPluginsRootDirectory,
},
backend: {
baseUrl: `http://localhost:0`,
},
},
}),
createRemoteProviderPlugin({
for: () => ({
customizeManifest(manifest) {
(manifest as any).metaData.publicPath =
'https://some-cdn-url-where-module-federation-assets-are-mirrored';
return manifest;
},
}),
}),
dynamicPluginsFeatureLoader({
moduleLoader: logger =>
jestFreeTypescriptAwareModuleLoader({ logger }),
logger: () => ({
transports: [mockedTransport],
format: winston.format.simple(),
}),
}),
],
});
const list = await fetch(
`http://localhost:${server.port()}/.backstage/dynamic-features/remotes`,
);
expect(list.ok).toBe(true);
expect(await list.json()).toEqual([
{
packageName: 'plugin-test-dynamic',
exposedModules: ['.', 'alpha'],
remoteInfo: {
name: 'backstage__plugin_test',
entry:
'http://localhost:0/.backstage/dynamic-features/remotes/plugin-test-dynamic/mf-manifest.json',
},
},
]);
const manifest = await fetch(
`http://localhost:${server.port()}/.backstage/dynamic-features/remotes/plugin-test-dynamic/mf-manifest.json`,
);
expect(manifest.ok).toBe(true);
expect(await manifest.json()).toMatchObject({
exposes: [{ name: '.' }, { name: 'alpha' }],
id: 'backstage__plugin_test',
name: 'backstage__plugin_test',
metaData: {
buildInfo: {
buildName: '@backstage/plugin-test',
buildVersion: '0.0.0',
},
globalName: 'backstage__plugin_test',
name: 'backstage__plugin_test',
pluginVersion: '0.0.0',
publicPath:
'https://some-cdn-url-where-module-federation-assets-are-mirrored',
},
});
expect(mockedTransport.logs).toContainEqual(
expect.stringMatching(
`Exposed dynamic frontend plugin 'plugin-test-dynamic' from '.*/dist' `,
),
);
});
it('should log an error and skip the plugin if the module federation manifest does not contain a valid name', async () => {
const mockedTransport = new MockedTransport();
const { server } = await startTestBackend({
features: [
mockServices.rootConfig.factory({
data: {
dynamicPlugins: {
rootDirectory: dynamicPluginsRootDirectory,
},
backend: {
baseUrl: `http://localhost:0`,
},
},
}),
createRemoteProviderPlugin({
for: () => ({
manifestFileName: 'mf-manifest-without-name.json',
}),
}),
dynamicPluginsFeatureLoader({
moduleLoader: logger =>
jestFreeTypescriptAwareModuleLoader({ logger }),
logger: () => ({
transports: [mockedTransport],
format: winston.format.simple(),
}),
}),
],
});
const list = await fetch(
`http://localhost:${server.port()}/.backstage/dynamic-features/remotes`,
);
expect(list.ok).toBe(true);
expect(await list.json()).toEqual([]);
expect(mockedTransport.logs).toContainEqual(
expect.stringMatching(
"error: Error in manifest '.*' for plugin .*@.*: module name not found",
),
);
});
it('should log an error and skip the plugin if a resolver provider triggers an error', async () => {
const mockedTransport = new MockedTransport();
const { server } = await startTestBackend({
features: [
mockServices.rootConfig.factory({
data: {
dynamicPlugins: {
rootDirectory: dynamicPluginsRootDirectory,
},
backend: {
baseUrl: `http://localhost:0`,
},
},
}),
createRemoteProviderPlugin({
for() {
throw new Error('Resolver provider error');
},
}),
dynamicPluginsFeatureLoader({
moduleLoader: logger =>
jestFreeTypescriptAwareModuleLoader({ logger }),
logger: () => ({
transports: [mockedTransport],
format: winston.format.simple(),
}),
}),
],
});
const list = await fetch(
`http://localhost:${server.port()}/.backstage/dynamic-features/remotes`,
);
expect(list.ok).toBe(true);
expect(await list.json()).toEqual([]);
expect(mockedTransport.logs).toContainEqual(
expect.stringMatching(
"error: Unexpected error when exposing dynamic frontend plugin 'plugin-test-dynamic@0.0.0' Resolver provider error",
),
);
});
it('should log an error and skip the plugin if the module federation manifest is not found', async () => {
const mockedTransport = new MockedTransport();
const { server } = await startTestBackend({
features: [
mockServices.rootConfig.factory({
data: {
dynamicPlugins: {
rootDirectory: dynamicPluginsRootDirectory,
},
backend: {
baseUrl: `http://localhost:0`,
},
},
}),
createRemoteProviderPlugin({
for: () => ({
manifestFileName: 'mf-manifest-not-found.json',
}),
}),
dynamicPluginsFeatureLoader({
moduleLoader: logger =>
jestFreeTypescriptAwareModuleLoader({ logger }),
logger: () => ({
transports: [mockedTransport],
format: winston.format.simple(),
}),
}),
],
});
const list = await fetch(
`http://localhost:${server.port()}/.backstage/dynamic-features/remotes`,
);
expect(list.ok).toBe(true);
expect(await list.json()).toEqual([]);
expect(mockedTransport.logs).toContainEqual(
expect.stringMatching(
"error: Could not find manifest '.*/dist/mf-manifest-not-found.json' for frontend plugin plugin-test-dynamic@0.0.0",
),
);
});
it('should log an error and skip the plugin if the module federation remote entry asset is not found', async () => {
const mockedTransport = new MockedTransport();
const { server } = await startTestBackend({
features: [
mockServices.rootConfig.factory({
data: {
dynamicPlugins: {
rootDirectory: dynamicPluginsRootDirectory,
},
backend: {
baseUrl: `http://localhost:0`,
},
},
}),
createRemoteProviderPlugin({
for: () => ({
manifestFileName: 'mf-manifest-with-wrong-remote-entry.json',
getRemoteEntryType: () => 'javascript',
}),
}),
dynamicPluginsFeatureLoader({
moduleLoader: logger =>
jestFreeTypescriptAwareModuleLoader({ logger }),
logger: () => ({
transports: [mockedTransport],
format: winston.format.simple(),
}),
}),
],
});
const list = await fetch(
`http://localhost:${server.port()}/.backstage/dynamic-features/remotes`,
);
expect(list.ok).toBe(true);
expect(await list.json()).toEqual([]);
expect(mockedTransport.logs).toContainEqual(
expect.stringMatching(
"error: Could not find remote entry asset '.*/dist/remoteEntry-not-found.js' for frontend plugin plugin-test-dynamic@0.0.0",
),
);
});
});
});
@@ -32,6 +32,7 @@ import {
dynamicPluginsRootLoggerServiceFactory,
dynamicPluginsSchemasServiceFactory,
} from '../schemas';
import { frontendRemotesServerService } from '../server/frontendRemotesServer';
/**
* @public
@@ -64,6 +65,7 @@ const dynamicPluginsFeatureLoaderWithOptions = (
yield* [
dynamicPluginsRootLoggerServiceFactory(rootLoggerOptions),
dynamicPluginsFrontendSchemas,
frontendRemotesServerService,
dynamicPluginsFeatureDiscoveryLoader,
];
}
@@ -19,3 +19,8 @@ export * from './scanner';
export * from './manager';
export * from './schemas';
export * from './features';
export * from './server';
export {
type RemoteInfo,
type RemoteInfoTypeEnum,
} from './schema/openapi/generated/models';
@@ -0,0 +1,159 @@
openapi: 3.0.3
info:
title: .backstage/dynamic-features
version: '1'
description: The Backstage backend plugin that serves the frontend plugins module federation manifests and assets
license:
name: Apache-2.0
url: http://www.apache.org/licenses/LICENSE-2.0.html
contact: {}
servers:
- url: /
components:
examples: {}
headers: {}
parameters: {}
requestBodies: {}
responses:
RemotesResponse:
description: List of Module Federation Remotes exposed by the backend
content:
application/json:
schema:
$ref: '#/components/schemas/Remotes'
ErrorResponse:
description: An error response from the backend.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
schemas:
Remotes:
type: array
items:
$ref: '#/components/schemas/Remote'
Remote:
description: Definition of a frontend plugin Module Federation remote served by the backend
type: object
properties:
packageName:
description: Name of the package exposed through this Module Federation remote
type: string
remoteInfo:
$ref: '#/components/schemas/RemoteInfo'
exposedModules:
description: Names of modules exposed by this module federation remote
type: array
items:
type: string
required:
- packageName
- remoteInfo
- exposedModules
RemoteInfo:
description: Definition of a frontend plugin Module Federation remote served by the backend
externalDocs:
url: https://module-federation.io/guide/basic/runtime.html#init
type: object
properties:
name:
description: Name of the module federation remote
type: string
entry:
description: Remote entry, either the remote manifest file, or the remote entry Javascript file.
type: string
entryGlobalName:
type: string
shareScope:
type: string
type:
type: string
enum:
[
'var',
'module',
'assign',
'assign-properties',
'this',
'window',
'self',
'global',
'commonjs',
'commonjs2',
'commonjs-module',
'commonjs-static',
'amd',
'amd-require',
'umd',
'umd2',
'jsonp',
'system',
]
required:
- 'name'
- 'entry'
Error:
type: object
properties:
error:
type: object
properties:
name:
type: string
message:
type: string
stack:
type: string
code:
type: string
required:
- name
- message
request:
type: object
properties:
method:
type: string
url:
type: string
required:
- method
- url
response:
type: object
properties:
statusCode:
type: number
required:
- statusCode
required:
- error
- response
additionalProperties: {}
securitySchemes:
JWT:
type: http
scheme: bearer
bearerFormat: JWT
paths:
/remotes:
get:
operationId: GetRemotes
description: Get the Module Federation remote definitions.
responses:
'200':
$ref: '#/components/responses/RemotesResponse'
'400':
$ref: '#/components/responses/ErrorResponse'
default:
$ref: '#/components/responses/ErrorResponse'
security:
- {}
- JWT: []
parameters: []
@@ -0,0 +1,33 @@
/*
* 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.
*/
//
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { Remote } from '../models/Remote.model';
/**
* @public
*/
export type GetRemotes = {
response: Array<Remote> | Error | Error;
};
export type EndpointMap = {
'#get|/remotes': GetRemotes;
};
@@ -0,0 +1,23 @@
/*
* 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.
*/
//
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
export * from './Api.server';
@@ -0,0 +1,18 @@
/*
* 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.
*/
export * from './apis';
export * from './router';
@@ -0,0 +1,29 @@
/*
* 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.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
/**
* @public
*/
export interface ErrorError {
name: string;
message: string;
stack?: string;
code?: string;
}
@@ -0,0 +1,27 @@
/*
* 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.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
/**
* @public
*/
export interface ErrorRequest {
method: string;
url: string;
}
@@ -0,0 +1,26 @@
/*
* 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.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
/**
* @public
*/
export interface ErrorResponse {
statusCode: number;
}
@@ -0,0 +1,33 @@
/*
* 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.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { ErrorError } from '../models/ErrorError.model';
import { ErrorRequest } from '../models/ErrorRequest.model';
import { ErrorResponse } from '../models/ErrorResponse.model';
/**
* @public
*/
export interface ModelError {
[key: string]: any;
error: ErrorError;
request?: ErrorRequest;
response: ErrorResponse;
}
@@ -0,0 +1,36 @@
/*
* 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.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { RemoteInfo } from '../models/RemoteInfo.model';
/**
* Definition of a frontend plugin Module Federation remote served by the backend
* @public
*/
export interface Remote {
/**
* Name of the package exposed through this Module Federation remote
*/
packageName: string;
remoteInfo: RemoteInfo;
/**
* Names of modules exposed by this module federation remote
*/
exposedModules: Array<string>;
}
@@ -0,0 +1,60 @@
/*
* 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.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
/**
* Definition of a frontend plugin Module Federation remote served by the backend
* @public
*/
export interface RemoteInfo {
/**
* Name of the module federation remote
*/
name: string;
/**
* Remote entry, either the remote manifest file, or the remote entry Javascript file.
*/
entry: string;
entryGlobalName?: string;
shareScope?: string;
type?: RemoteInfoTypeEnum;
}
/**
* @public
*/
export type RemoteInfoTypeEnum =
| 'var'
| 'module'
| 'assign'
| 'assign-properties'
| 'this'
| 'window'
| 'self'
| 'global'
| 'commonjs'
| 'commonjs2'
| 'commonjs-module'
| 'commonjs-static'
| 'amd'
| 'amd-require'
| 'umd'
| 'umd2'
| 'jsonp'
| 'system';
@@ -0,0 +1,22 @@
/*
* 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.
*/
export * from '../models/ErrorError.model';
export * from '../models/ErrorRequest.model';
export * from '../models/ErrorResponse.model';
export * from '../models/ModelError.model';
export * from '../models/Remote.model';
export * from '../models/RemoteInfo.model';
@@ -0,0 +1,238 @@
/*
* 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.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { createValidatedOpenApiRouterFromGeneratedEndpointMap } from '@backstage/backend-openapi-utils';
import { EndpointMap } from './';
export const spec = {
openapi: '3.0.3',
info: {
title: '.backstage/dynamic-features',
version: '1',
description:
'The Backstage backend plugin that serves the frontend plugins module federation manifests and assets',
license: {
name: 'Apache-2.0',
url: 'http://www.apache.org/licenses/LICENSE-2.0.html',
},
contact: {},
},
servers: [
{
url: '/',
},
],
components: {
examples: {},
headers: {},
parameters: {},
requestBodies: {},
responses: {
RemotesResponse: {
description: 'List of Module Federation Remotes exposed by the backend',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Remotes',
},
},
},
},
ErrorResponse: {
description: 'An error response from the backend.',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error',
},
},
},
},
},
schemas: {
Remotes: {
type: 'array',
items: {
$ref: '#/components/schemas/Remote',
},
},
Remote: {
description:
'Definition of a frontend plugin Module Federation remote served by the backend',
type: 'object',
properties: {
packageName: {
description:
'Name of the package exposed through this Module Federation remote',
type: 'string',
},
remoteInfo: {
$ref: '#/components/schemas/RemoteInfo',
},
exposedModules: {
description:
'Names of modules exposed by this module federation remote',
type: 'array',
items: {
type: 'string',
},
},
},
required: ['packageName', 'remoteInfo', 'exposedModules'],
},
RemoteInfo: {
description:
'Definition of a frontend plugin Module Federation remote served by the backend',
externalDocs: {
url: 'https://module-federation.io/guide/basic/runtime.html#init',
},
type: 'object',
properties: {
name: {
description: 'Name of the module federation remote',
type: 'string',
},
entry: {
description:
'Remote entry, either the remote manifest file, or the remote entry Javascript file.',
type: 'string',
},
entryGlobalName: {
type: 'string',
},
shareScope: {
type: 'string',
},
type: {
type: 'string',
enum: [
'var',
'module',
'assign',
'assign-properties',
'this',
'window',
'self',
'global',
'commonjs',
'commonjs2',
'commonjs-module',
'commonjs-static',
'amd',
'amd-require',
'umd',
'umd2',
'jsonp',
'system',
],
},
},
required: ['name', 'entry'],
},
Error: {
type: 'object',
properties: {
error: {
type: 'object',
properties: {
name: {
type: 'string',
},
message: {
type: 'string',
},
stack: {
type: 'string',
},
code: {
type: 'string',
},
},
required: ['name', 'message'],
},
request: {
type: 'object',
properties: {
method: {
type: 'string',
},
url: {
type: 'string',
},
},
required: ['method', 'url'],
},
response: {
type: 'object',
properties: {
statusCode: {
type: 'number',
},
},
required: ['statusCode'],
},
},
required: ['error', 'response'],
additionalProperties: {},
},
},
securitySchemes: {
JWT: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
},
paths: {
'/remotes': {
get: {
operationId: 'GetRemotes',
description: 'Get the Module Federation remote definitions.',
responses: {
'200': {
$ref: '#/components/responses/RemotesResponse',
},
'400': {
$ref: '#/components/responses/ErrorResponse',
},
default: {
$ref: '#/components/responses/ErrorResponse',
},
},
security: [
{},
{
JWT: [],
},
],
parameters: [],
},
},
},
} as const;
export const createOpenApiRouter = async (
options?: Parameters<
typeof createValidatedOpenApiRouterFromGeneratedEndpointMap
>['1'],
) =>
createValidatedOpenApiRouterFromGeneratedEndpointMap<EndpointMap>(
spec,
options,
);
@@ -0,0 +1,17 @@
/*
* 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.
*/
export * from './generated';
@@ -29,7 +29,11 @@ import { isEmpty } from 'lodash';
import { LoggerService } from '@backstage/backend-plugin-api';
import { JsonObject } from '@backstage/types';
import { PluginScanner } from '../scanner/plugin-scanner';
import { ConfigSchema, loadConfigSchema } from '@backstage/config-loader';
import {
ConfigSchema,
loadConfigSchema,
mergeConfigSchemas,
} from '@backstage/config-loader';
import { dynamicPluginsFeatureLoader } from '../features';
/**
@@ -153,7 +157,7 @@ async function gatherDynamicPluginsSchemas(
packages: ScannedPluginPackage[],
logger: LoggerService,
schemaLocator: (pluginPackage: ScannedPluginPackage) => string = () =>
path.join('dist', 'configSchema.json'),
path.join('dist', '.config-schema.json'),
): Promise<{ [context: string]: JsonObject }> {
const allSchemas: { [context: string]: JsonObject } = {};
@@ -169,7 +173,7 @@ async function gatherDynamicPluginsSchemas(
continue;
}
const serialized = await fs.readJson(schemaLocation);
let serialized = await fs.readJson(schemaLocation);
if (!serialized) {
continue;
}
@@ -178,6 +182,12 @@ async function gatherDynamicPluginsSchemas(
continue;
}
if (serialized?.backstageConfigSchemaVersion === 1) {
serialized = mergeConfigSchemas(
(serialized?.schemas as JsonObject[]).map(_ => _.value as any),
);
}
if (!serialized?.$schema || serialized?.type !== 'object') {
logger.error(
`Serialized configuration schema is invalid for plugin ${pluginPackage.manifest.name}`,
@@ -0,0 +1,166 @@
/*
* Copyright 2024 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 {
coreServices,
createServiceFactory,
createServiceRef,
} from '@backstage/backend-plugin-api';
import { createRouter } from './router';
import { dynamicPluginsServiceRef } from '@backstage/backend-dynamic-feature-service';
import { spec } from '../schema/openapi';
import { ManifestFileName } from '@module-federation/sdk';
import { RemoteInfo } from '../schema/openapi/generated/models';
import { JsonObject } from '@backstage/types';
/**
*
* @public
* */
export type AdditionalRemoteInfo = Omit<RemoteInfo, 'name' | 'entry'>;
/**
*
* @public
* */
export type FrontendRemoteResolver = {
/**
* Relative path to the module federation assets folder from thr root folder of the plugin package.
* Default value is `dist`.
*/
assetsPathFromPackage?: string;
/**
* File name of the module federation manifest inside the module federation assets folder.
* Default value is `mf-manifest.json`.
*/
manifestFileName?: string;
/**
* Type of the remote entry returned in the RemoteInfo for this remote.
* Default value is `manifest`.
*/
getRemoteEntryType?: (
manifestContent: JsonObject,
) => 'manifest' | 'javascript';
/**
* Additional module federation fields, which might be required if the remote entry type is 'javascript'.
*/
getAdditionaRemoteInfo?: (
manifestContent: JsonObject,
) => AdditionalRemoteInfo;
/**
* Overrides the list of exposed modules. By default the exposed modules are read from the manifest file.
*/
overrideExposedModules?: (
exposedModules: string[],
manifestContent: JsonObject,
) => string[];
/**
* Customizes the manifest before returning it as the remote entry.
*/
customizeManifest?: (content: JsonObject) => JsonObject;
};
/**
*
* @public
* */
export type FrontendRemoteResolverProvider = {
for(
pluginName: string,
pluginPackagePath: string,
): Partial<FrontendRemoteResolver> | undefined;
};
/**
*
* @public
* */
export interface DynamicPluginsFrontendRemotesService {
setResolverProvider(provider: FrontendRemoteResolverProvider): void;
}
/**
* A service that serves the frontend module federation remotes,
* and allows a plugin to customize the way remotes are served,
* by setting a ResolverProvider.
*
* @public
*/
export const dynamicPluginsFrontendServiceRef =
createServiceRef<DynamicPluginsFrontendRemotesService>({
id: 'core.dynamicplugins.frontendRemotes',
scope: 'root',
});
export type FrontendRemoteResolvers = {
default: FrontendRemoteResolver &
Required<
Pick<
FrontendRemoteResolver,
'assetsPathFromPackage' | 'manifestFileName' | 'getRemoteEntryType'
>
>;
provider?: FrontendRemoteResolverProvider;
};
export const frontendRemotesServerService = createServiceFactory({
service: dynamicPluginsFrontendServiceRef,
deps: {
logger: coreServices.rootLogger,
rootHttpRouter: coreServices.rootHttpRouter,
config: coreServices.rootConfig,
dynamicPlugins: dynamicPluginsServiceRef,
lifecycle: coreServices.rootLifecycle,
},
async factory({ logger, rootHttpRouter, config, dynamicPlugins, lifecycle }) {
const resolvers: FrontendRemoteResolvers = {
default: {
assetsPathFromPackage: 'dist',
manifestFileName: ManifestFileName,
getRemoteEntryType: () => 'manifest',
},
provider: undefined,
};
lifecycle.addStartupHook(async () => {
rootHttpRouter.use(
`/${spec.info.title}`,
await createRouter({
logger,
config,
dynamicPlugins,
resolvers,
}),
);
});
return {
setResolverProvider(resolver) {
logger.info('Setting resolver provider');
if (resolvers.provider) {
throw new Error(
'Attempted to install a frontend remote resolver provider twice',
);
}
resolvers.provider = resolver;
},
};
},
});
@@ -0,0 +1,23 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { dynamicPluginsFrontendServiceRef } from './frontendRemotesServer';
export type {
DynamicPluginsFrontendRemotesService,
FrontendRemoteResolver,
FrontendRemoteResolverProvider,
AdditionalRemoteInfo,
} from './frontendRemotesServer';
@@ -0,0 +1,195 @@
/*
* Copyright 2024 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 {
LoggerService,
RootConfigService,
} from '@backstage/backend-plugin-api';
import express from 'express';
import { createOpenApiRouter, spec } from '../schema/openapi';
import { DynamicPluginProvider } from '@backstage/backend-dynamic-feature-service';
import * as fs from 'fs';
import * as path from 'path';
import * as url from 'url';
import { FrontendRemoteResolvers } from './frontendRemotesServer';
import { Remote } from '../schema/openapi/generated/models';
import { JsonObject } from '@backstage/types';
export async function createRouter({
logger,
config,
dynamicPlugins,
resolvers,
}: {
logger: LoggerService;
config: RootConfigService;
dynamicPlugins: DynamicPluginProvider;
resolvers: FrontendRemoteResolvers;
}): Promise<express.Router> {
const externalBaseUrl = `${config.getString('backend.baseUrl')}/${
spec.info.title
}`;
const typedRouter = await createOpenApiRouter();
const frontendPluginRemotes: Remote[] = [];
const { default: defaultResolver, provider: resolverProvider } = resolvers;
for (const plugin of dynamicPlugins.frontendPlugins()) {
try {
const pluginScannedPackage = dynamicPlugins.getScannedPackage(plugin);
const pluginScannedPackagePath = path.resolve(
url.fileURLToPath(pluginScannedPackage.location),
);
const providedResolver = resolverProvider?.for(
plugin.name,
pluginScannedPackagePath,
);
const assetsPath = path.resolve(
pluginScannedPackagePath,
providedResolver?.assetsPathFromPackage ??
defaultResolver.assetsPathFromPackage,
);
const manifestFileName =
providedResolver?.manifestFileName ?? defaultResolver.manifestFileName;
const manifestLocation = path.resolve(assetsPath, manifestFileName);
if (!fs.existsSync(manifestLocation)) {
logger.error(
`Could not find manifest '${manifestLocation}' for frontend plugin ${plugin.name}@${plugin.version}`,
);
continue;
}
let manifest: JsonObject;
try {
manifest = JSON.parse(fs.readFileSync(manifestLocation).toString());
} catch (error) {
logger.error(
`Dynamic frontend plugin manifest '${manifestLocation}' could not be parsed for plugin ${plugin.name}@${plugin.version}`,
);
continue;
}
if (!manifest.name || typeof manifest.name !== 'string') {
logger.error(
`Error in manifest '${manifestLocation}' for plugin ${plugin.name}@${plugin.version}: module name not found`,
);
continue;
}
if (
!manifest.metaData ||
typeof manifest.metaData !== 'object' ||
!('remoteEntry' in manifest.metaData) ||
!manifest.metaData.remoteEntry ||
typeof manifest.metaData.remoteEntry !== 'object' ||
!('name' in manifest.metaData.remoteEntry) ||
typeof manifest.metaData.remoteEntry.name !== 'string'
) {
logger.error(
`Could not find remote entry asset in the manifest '${manifestLocation}' for plugin ${plugin.name}@${plugin.version}`,
);
continue;
}
if (
!manifest.exposes ||
!Array.isArray(manifest.exposes) ||
!manifest.exposes.every<{ name: string }>(
(i): i is { name: string } =>
i !== null && typeof i === 'object' && 'name' in i,
)
) {
logger.error(
`Could not find the exposes field in the manifest '${manifestLocation}' for plugin ${plugin.name}@${plugin.version}`,
);
continue;
}
const getAdditionalRemoteInfo =
providedResolver?.getAdditionaRemoteInfo ??
defaultResolver.getAdditionaRemoteInfo;
const getRemoteEntryType =
providedResolver?.getRemoteEntryType ??
defaultResolver.getRemoteEntryType;
const remoteEntryType = getRemoteEntryType(manifest);
let remoteEntryAsset = manifestFileName;
if (remoteEntryType === 'javascript') {
remoteEntryAsset = manifest.metaData.remoteEntry.name;
}
const remoteEntryAssetLocation = path.resolve(
assetsPath,
remoteEntryAsset,
);
if (!fs.existsSync(remoteEntryAssetLocation)) {
logger.error(
`Could not find remote entry asset '${remoteEntryAssetLocation}' for frontend plugin ${plugin.name}@${plugin.version}`,
);
continue;
}
const remoteAssetsPrefix = `/remotes/${plugin.name}`;
const remoteEntryPath = `${remoteAssetsPrefix}/${remoteEntryAsset}`;
const overrideExposedModules =
providedResolver?.overrideExposedModules ??
defaultResolver.overrideExposedModules;
const exposedModules = manifest.exposes.map(e => e.name);
frontendPluginRemotes.push({
packageName: plugin.name,
remoteInfo: {
name: manifest.name,
entry: `${externalBaseUrl}${remoteEntryPath}`,
...getAdditionalRemoteInfo?.(manifest),
},
exposedModules:
overrideExposedModules?.(exposedModules, manifest) ?? exposedModules,
});
const customizeManifest =
providedResolver?.customizeManifest ??
defaultResolver.customizeManifest;
if (remoteEntryType === 'manifest' && customizeManifest) {
const customizedContent = customizeManifest(manifest);
typedRouter.use(`${remoteEntryPath}`, (_, res) => {
res.json(customizedContent);
});
}
typedRouter.use(remoteAssetsPrefix, express.static(assetsPath));
logger.info(
`Exposed dynamic frontend plugin '${plugin.name}' from '${assetsPath}' `,
);
} catch (error) {
logger.error(
`Unexpected error when exposing dynamic frontend plugin '${plugin.name}@${plugin.version}'`,
error,
);
continue;
}
}
logger.info(`/remotes => ${JSON.stringify(frontendPluginRemotes)}`);
typedRouter.get('/remotes', (_, res) => {
res.status(200).json(frontendPluginRemotes);
});
return typedRouter;
}
@@ -0,0 +1,5 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, {
rules: {
'@backstage/no-top-level-material-ui-4-imports': 'error',
},
});
@@ -0,0 +1,32 @@
# @backstage/frontend-dynamic-feature-loader
Backstage frontend feature loader to load new frontend system plugins exposed as module federation remotes.
The frontend feature loader provided in this package works hand-in-hand with the server of frontend plugin module federation remotes server which is part of backend dynamic feature service in package `@backstage/backend-dynamic-feature-service`.
**NOTE: The [new frontend system](https://backstage.io/docs/frontend-system/) that this package is relaying upon is in alpha, and we do not yet recommend using it for production deployments**
## Usage
- To enable this loader, you should:
- Enable the backend dynamic features in your backend application, as explained in the [`backend-dynamic-feature-service` README.md file](../backend-dynamic-feature-service/README.md#how-it-works)
- Add the frontend feature loader to the list of features when creating the frontend application:
```typescript
const app = createApp({
features: [...someOtherFeatures, dynamicFrontendFeaturesLoader()],
});
```
## How to add a frontend plugin for dynamic loading
Adding a frontend plugin (with new frontend system support, possibly in alpha support), is straightforward and consists in:
- building the frontend plugin with the `frontend-dynamic-container` role, which enables the module federation support, and packages the plugin as a module remote
- copying the frontend package folder, with the `dist` folder generated during the build, to the dynamic plugins root folder of the Backstage installation (defined by the `dynamicPlugins.rootDirectory` configuration value, which is usually set as `dynamic-plugins-root`).
So from a frontend plugin package folder, you would use the following command:
```bash
yarn build --role frontend-dynamic-container && cp -R $(pwd) <target backstage>/dynamic-plugins-root/
```
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-frontend-dynamic-feature-loader
title: '@backstage/frontend-dynamic-feature-loader'
description: Backstage frontend feature loader to load new frontend system plugins exposed as module federation remotes.
spec:
lifecycle: experimental
type: backstage-web-library
owner: maintainers
+22
View File
@@ -0,0 +1,22 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface Config {
/**
* @visibility frontend
*/
dynamicPlugins?: {};
}
@@ -0,0 +1,2 @@
# Knip report
@@ -0,0 +1,66 @@
{
"name": "@backstage/frontend-dynamic-feature-loader",
"version": "0.0.0",
"backstage": {
"role": "web-library"
},
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "packages/frontend-dynamic-feature-loader"
},
"license": "Apache-2.0",
"sideEffects": false,
"main": "src/index.ts",
"types": "src/index.ts",
"files": [
"config.d.ts",
"dist"
],
"scripts": {
"build": "backstage-cli package build",
"clean": "backstage-cli package clean",
"lint": "backstage-cli package lint",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack",
"start": "backstage-cli package start",
"test": "backstage-cli package test"
},
"dependencies": {
"@backstage/config": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"@module-federation/enhanced": "^0.9.0",
"@module-federation/runtime": "^0.9.0",
"@module-federation/sdk": "^0.9.0",
"cross-fetch": "^4.0.0",
"uri-template": "^2.0.0"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^16.0.0",
"@types/react": "^18.0.0",
"msw": "^1.0.0",
"react": "^18.0.2",
"react-dom": "^18.0.2",
"react-router-dom": "^6.3.0"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0",
"react-router-dom": "^6.3.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
},
"configSchema": "config.d.ts"
}
@@ -0,0 +1,24 @@
## API Report File for "@backstage/frontend-dynamic-feature-loader"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';
import { FrontendFeatureLoader } from '@backstage/frontend-plugin-api';
import { ShareStrategy } from '@module-federation/runtime/types';
import { UserOptions } from '@module-federation/runtime/types';
// @public
export function dynamicFrontendFeaturesLoader(
options?: DynamicFrontendFeaturesLoaderOptions,
): FrontendFeatureLoader;
// @public (undocumented)
export type DynamicFrontendFeaturesLoaderOptions = {
moduleFederation: {
shared?: UserOptions['shared'];
shareStrategy?: ShareStrategy;
plugins?: Array<FederationRuntimePlugin>;
};
};
```
@@ -0,0 +1,23 @@
/*
* Copyright 2024 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.
*/
/**
* Core API used by Backstage frontend apps.
*
* @packageDocumentation
*/
export * from './loader';
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,184 @@
/*
* Copyright 2024 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 {
FederationRuntimePlugin,
init,
loadRemote,
} from '@module-federation/enhanced/runtime';
import { Module } from '@module-federation/sdk';
import { DefaultApiClient, Remote } from './schema/openapi';
import {
FrontendFeature,
FrontendFeatureLoader,
createFrontendFeatureLoader,
} from '@backstage/frontend-plugin-api';
import { ShareStrategy, UserOptions } from '@module-federation/runtime/types';
/**
*
* @public
*/
export type DynamicFrontendFeaturesLoaderOptions = {
/**
* Additional module federation arguments for the Module Federation runtime initialization.
*/
moduleFederation: {
shared?: UserOptions['shared'];
shareStrategy?: ShareStrategy;
plugins?: Array<FederationRuntimePlugin>;
};
};
/**
* A function providing a loader of frontend features exposed as module federation remotes
* from the backend dynamic features service.
*
* @public
*/
export function dynamicFrontendFeaturesLoader(
options?: DynamicFrontendFeaturesLoaderOptions,
): FrontendFeatureLoader {
return createFrontendFeatureLoader({
async loader({ config }) {
const dynamicPLuginsConfig = config.getOptionalConfig('dynamicPlugins');
if (!dynamicPLuginsConfig) {
return [];
}
function error(message: string, err: unknown) {
// eslint-disable-next-line no-console
console.error(
`${message}: ${
err instanceof Error ? err.toString() : JSON.stringify(err)
}`,
);
}
const backendBaseUrl = config.getString('backend.baseUrl');
const appPackageName =
config.getOptionalString('app.packageName') ?? 'app';
let frontendPluginRemotes: Array<Remote>;
try {
const apiClient = new DefaultApiClient({
discoveryApi: {
getBaseUrl: async rootPath => `${backendBaseUrl}/${rootPath}`,
},
fetchApi: {
fetch(input) {
return global.fetch(input);
},
},
});
const response = await apiClient.getRemotes({});
if (!response.ok) {
throw new Error(`${response.status} - ${response.statusText}`);
}
frontendPluginRemotes = await response.json();
} catch (err) {
error(
`Failed fetching module federation configuration of dynamic frontend plugins`,
err,
);
return [];
}
try {
init({
...options?.moduleFederation,
name: appPackageName
.replaceAll('@', '')
.replaceAll('/', '__')
.replaceAll('-', '_'),
remotes: frontendPluginRemotes.map(remote => ({
alias: remote.packageName,
...remote.remoteInfo,
})),
});
} catch (err) {
error(`Failed initializing module federation`, err);
return [];
}
const features = (
await Promise.all(
frontendPluginRemotes.map(async remote => {
// eslint-disable-next-line no-console
console.debug(
`Loading dynamic plugin '${remote.packageName}' from '${remote.remoteInfo.entry}'`,
);
const moduleFeatures = await Promise.all(
remote.exposedModules.map(async exposedModuleName => {
const remoteModuleName =
exposedModuleName === '.'
? remote.remoteInfo.name
: `${remote.remoteInfo.name}/${exposedModuleName}`;
let module: Module;
try {
module = await loadRemote<Module>(remoteModuleName);
} catch (err) {
error(
`Failed loading remote module '${remoteModuleName}' of dynamic plugin '${remote.packageName}'`,
err,
);
return undefined;
}
if (!module) {
// eslint-disable-next-line no-console
console.warn(
`Skipping empty dynamic plugin remote module '${remoteModuleName}'.`,
);
return undefined;
}
// eslint-disable-next-line no-console
console.info(
`Remote module '${remoteModuleName}' of dynamic plugin '${remote.packageName}' loaded from ${remote.remoteInfo.entry}`,
);
const defaultEntry = module.default;
if (!isLoadable(defaultEntry)) {
// eslint-disable-next-line no-console
console.debug(
`Skipping dynamic plugin remote module '${remote}' since it doesn't export a new 'FrontendFeature' as default export.`,
);
return undefined;
}
return defaultEntry;
}),
);
return moduleFeatures;
}),
)
)
.flat()
.filter((feature): feature is FrontendFeature => feature !== undefined);
return [...features];
},
});
}
function isLoadable(obj: unknown): obj is FrontendFeature {
if (obj !== null && typeof obj === 'object' && '$$type' in obj) {
return (
obj.$$type === '@backstage/FrontendPlugin' ||
obj.$$type === '@backstage/FrontendModule'
);
}
return false;
}
@@ -0,0 +1,86 @@
/*
* 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.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { DiscoveryApi } from '../types/discovery';
import { FetchApi } from '../types/fetch';
import crossFetch from 'cross-fetch';
import { pluginId } from '../pluginId';
import * as parser from 'uri-template';
import { Remote } from '../models/Remote.model';
/**
* Wraps the Response type to convey a type on the json call.
*
* @public
*/
export type TypedResponse<T> = Omit<Response, 'json'> & {
json: () => Promise<T>;
};
/**
* Options you can pass into a request for additional information.
*
* @public
*/
export interface RequestOptions {
token?: string;
}
/**
* @public
*/
export type GetRemotes = {};
/**
* @public
*/
export class DefaultApiClient {
private readonly discoveryApi: DiscoveryApi;
private readonly fetchApi: FetchApi;
constructor(options: {
discoveryApi: { getBaseUrl(pluginId: string): Promise<string> };
fetchApi?: { fetch: typeof fetch };
}) {
this.discoveryApi = options.discoveryApi;
this.fetchApi = options.fetchApi || { fetch: crossFetch };
}
/**
* Get the Module Federation remote definitions.
*/
public async getRemotes(
// @ts-ignore
request: GetRemotes,
options?: RequestOptions,
): Promise<TypedResponse<Array<Remote>>> {
const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);
const uriTemplate = `/remotes`;
const uri = parser.parse(uriTemplate).expand({});
return await this.fetchApi.fetch(`${baseUrl}${uri}`, {
headers: {
'Content-Type': 'application/json',
...(options?.token && { Authorization: `Bearer ${options?.token}` }),
},
method: 'GET',
});
}
}
@@ -0,0 +1,17 @@
/*
* 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.
*/
export * from './Api.client';
@@ -0,0 +1,18 @@
/*
* 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.
*/
export * from './apis';
export * from './models';
@@ -0,0 +1,29 @@
/*
* 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.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
/**
* @public
*/
export interface ErrorError {
name: string;
message: string;
stack?: string;
code?: string;
}
@@ -0,0 +1,27 @@
/*
* 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.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
/**
* @public
*/
export interface ErrorRequest {
method: string;
url: string;
}
@@ -0,0 +1,26 @@
/*
* 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.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
/**
* @public
*/
export interface ErrorResponse {
statusCode: number;
}
@@ -0,0 +1,33 @@
/*
* 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.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { ErrorError } from '../models/ErrorError.model';
import { ErrorRequest } from '../models/ErrorRequest.model';
import { ErrorResponse } from '../models/ErrorResponse.model';
/**
* @public
*/
export interface ModelError {
[key: string]: any;
error: ErrorError;
request?: ErrorRequest;
response: ErrorResponse;
}
@@ -0,0 +1,36 @@
/*
* 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.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { RemoteInfo } from '../models/RemoteInfo.model';
/**
* Definition of a frontend plugin Module Federation remote served by the backend
* @public
*/
export interface Remote {
/**
* Name of the package exposed through this Module Federation remote
*/
packageName: string;
remoteInfo: RemoteInfo;
/**
* Names of modules exposed by this module federation remote
*/
exposedModules: Array<string>;
}
@@ -0,0 +1,60 @@
/*
* 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.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
/**
* Definition of a frontend plugin Module Federation remote served by the backend
* @public
*/
export interface RemoteInfo {
/**
* Name of the module federation remote
*/
name: string;
/**
* Remote entry, either the remote manifest file, or the remote entry Javascript file.
*/
entry: string;
entryGlobalName?: string;
shareScope?: string;
type?: RemoteInfoTypeEnum;
}
/**
* @public
*/
export type RemoteInfoTypeEnum =
| 'var'
| 'module'
| 'assign'
| 'assign-properties'
| 'this'
| 'window'
| 'self'
| 'global'
| 'commonjs'
| 'commonjs2'
| 'commonjs-module'
| 'commonjs-static'
| 'amd'
| 'amd-require'
| 'umd'
| 'umd2'
| 'jsonp'
| 'system';
@@ -0,0 +1,22 @@
/*
* 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.
*/
export * from '../models/ErrorError.model';
export * from '../models/ErrorRequest.model';
export * from '../models/ErrorResponse.model';
export * from '../models/ModelError.model';
export * from '../models/Remote.model';
export * from '../models/RemoteInfo.model';
@@ -0,0 +1,17 @@
/*
* 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.
*/
export const pluginId = '.backstage/dynamic-features';
@@ -0,0 +1,22 @@
/*
* Copyright 2023 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.
*/
/**
* This is a copy of the DiscoveryApi, to avoid importing core-plugin-api.
*/
export type DiscoveryApi = {
getBaseUrl(pluginId: string): Promise<string>;
};
@@ -0,0 +1,22 @@
/*
* Copyright 2023 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.
*/
/**
* This is a copy of FetchApi, to avoid importing core-plugin-api.
*/
export type FetchApi = {
fetch: typeof fetch;
};
@@ -0,0 +1,17 @@
/*
* 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.
*/
export * from './generated';
@@ -0,0 +1,17 @@
/*
* Copyright 2024 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 '@testing-library/jest-dom';
+1
View File
@@ -77,6 +77,7 @@ const roleRules = [
'@backstage/core-compat-api',
'@backstage/dev-utils',
'@backstage/frontend-defaults',
'@backstage/frontend-dynamic-feature-loader',
'@backstage/frontend-app-api',
'@backstage/frontend-test-utils',
'@backstage/test-utils',
+197 -173
View File
@@ -3656,6 +3656,7 @@ __metadata:
dependencies:
"@backstage/backend-app-api": "workspace:^"
"@backstage/backend-defaults": "workspace:^"
"@backstage/backend-openapi-utils": "workspace:^"
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-test-utils": "workspace:^"
"@backstage/cli": "workspace:^"
@@ -3675,11 +3676,14 @@ __metadata:
"@backstage/plugin-scaffolder-node": "workspace:^"
"@backstage/plugin-search-backend-node": "workspace:^"
"@backstage/plugin-search-common": "workspace:^"
"@backstage/repo-tools": "workspace:^"
"@backstage/types": "workspace:^"
"@manypkg/get-packages": "npm:^1.1.3"
"@module-federation/sdk": "npm:^0.9.0"
"@types/express": "npm:^4.17.6"
chokidar: "npm:^3.5.3"
express: "npm:^4.17.1"
express-promise-router: "npm:^4.1.0"
fs-extra: "npm:^11.2.0"
lodash: "npm:^4.17.21"
triple-beam: "npm:^1.4.1"
@@ -4097,7 +4101,7 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/core-app-api@npm:^1.16.0":
"@backstage/core-app-api@npm:^1.15.5, @backstage/core-app-api@npm:^1.16.0":
version: 1.16.0
resolution: "@backstage/core-app-api@npm:1.16.0"
dependencies:
@@ -4169,13 +4173,12 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/core-compat-api@npm:^0.4.0":
version: 0.4.0
resolution: "@backstage/core-compat-api@npm:0.4.0"
"@backstage/core-compat-api@npm:^0.3.6":
version: 0.3.6
resolution: "@backstage/core-compat-api@npm:0.3.6"
dependencies:
"@backstage/core-plugin-api": "npm:^1.10.5"
"@backstage/frontend-plugin-api": "npm:^0.10.0"
"@backstage/plugin-catalog-react": "npm:^1.16.0"
"@backstage/core-plugin-api": "npm:^1.10.4"
"@backstage/frontend-plugin-api": "npm:^0.9.5"
"@backstage/version-bridge": "npm:^1.0.11"
lodash: "npm:^4.17.21"
peerDependencies:
@@ -4186,7 +4189,7 @@ __metadata:
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10/6b29f14dadc5dd5c055d063cb12ff901e2ff4458eb606489fc917e029afc3bb81b5292cf701227cedf35c3df25dc85ad1c60ca20ef3b8f75aa41d40a7f5580ee
checksum: 10/befd4cb7108672ff0c3f4433e39bf5557e43a755fdf658de7b4a161977efb69b9b8a129930b141b02ea7bca5f71d70353d415d7d89f9a440afd6b4fa28c9a8cf
languageName: node
linkType: hard
@@ -4330,12 +4333,12 @@ __metadata:
languageName: node
linkType: hard
"@backstage/core-components@npm:^0.17.0":
version: 0.17.0
resolution: "@backstage/core-components@npm:0.17.0"
"@backstage/core-components@npm:^0.16.4":
version: 0.16.4
resolution: "@backstage/core-components@npm:0.16.4"
dependencies:
"@backstage/config": "npm:^1.3.2"
"@backstage/core-plugin-api": "npm:^1.10.5"
"@backstage/core-plugin-api": "npm:^1.10.4"
"@backstage/errors": "npm:^1.2.7"
"@backstage/theme": "npm:^0.6.4"
"@backstage/version-bridge": "npm:^1.0.11"
@@ -4380,7 +4383,7 @@ __metadata:
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10/b48f7a3a6df469f26eb033f2f59da36dcf56bb52ba7298b7dba05d9132c90f8ab5b58dd247a8f9c1b652b91138358fbae482654465550c74780a32fe04bf6e7e
checksum: 10/472f4a17edc740cec15041612068320114b0128d6917d6968db8b435c3f4c2f002be939978b29efb12ce32c3d660b28e9de051ac1104c14f4eae2b6129c5ab49
languageName: node
linkType: hard
@@ -4462,7 +4465,7 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/core-plugin-api@npm:^1.10.0, @backstage/core-plugin-api@npm:^1.10.5, @backstage/core-plugin-api@npm:^1.8.2":
"@backstage/core-plugin-api@npm:^1.10.0, @backstage/core-plugin-api@npm:^1.10.4, @backstage/core-plugin-api@npm:^1.10.5, @backstage/core-plugin-api@npm:^1.8.2":
version: 1.10.5
resolution: "@backstage/core-plugin-api@npm:1.10.5"
dependencies:
@@ -4616,16 +4619,16 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/frontend-app-api@npm:^0.11.0":
version: 0.11.0
resolution: "@backstage/frontend-app-api@npm:0.11.0"
"@backstage/frontend-app-api@npm:^0.10.5":
version: 0.10.5
resolution: "@backstage/frontend-app-api@npm:0.10.5"
dependencies:
"@backstage/config": "npm:^1.3.2"
"@backstage/core-app-api": "npm:^1.16.0"
"@backstage/core-plugin-api": "npm:^1.10.5"
"@backstage/core-app-api": "npm:^1.15.5"
"@backstage/core-plugin-api": "npm:^1.10.4"
"@backstage/errors": "npm:^1.2.7"
"@backstage/frontend-defaults": "npm:^0.2.0"
"@backstage/frontend-plugin-api": "npm:^0.10.0"
"@backstage/frontend-defaults": "npm:^0.1.6"
"@backstage/frontend-plugin-api": "npm:^0.9.5"
"@backstage/types": "npm:^1.2.1"
"@backstage/version-bridge": "npm:^1.0.11"
lodash: "npm:^4.17.21"
@@ -4638,7 +4641,7 @@ __metadata:
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10/5522ddd487766ea44288d5626f269e42ad7d84902a5a257fd3962d2b2409743cf1b5de135e15915ba568bf2b12ec25c814ece077c87e1436c67e63998b1b9652
checksum: 10/55837eeacbc3bac8b9e684c63d8784f56d5adc76d20e415f1e1a30c7fb2014b8407c7e30bdd9823a5f2c9c63ce793271b5f0997c5057359a4c5e73b7ba346afb
languageName: node
linkType: hard
@@ -4676,15 +4679,15 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/frontend-defaults@npm:^0.2.0":
version: 0.2.0
resolution: "@backstage/frontend-defaults@npm:0.2.0"
"@backstage/frontend-defaults@npm:^0.1.6":
version: 0.1.6
resolution: "@backstage/frontend-defaults@npm:0.1.6"
dependencies:
"@backstage/config": "npm:^1.3.2"
"@backstage/errors": "npm:^1.2.7"
"@backstage/frontend-app-api": "npm:^0.11.0"
"@backstage/frontend-plugin-api": "npm:^0.10.0"
"@backstage/plugin-app": "npm:^0.1.7"
"@backstage/frontend-app-api": "npm:^0.10.5"
"@backstage/frontend-plugin-api": "npm:^0.9.5"
"@backstage/plugin-app": "npm:^0.1.6"
"@react-hookz/web": "npm:^24.0.0"
peerDependencies:
"@types/react": ^17.0.0 || ^18.0.0
@@ -4694,7 +4697,7 @@ __metadata:
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10/65decef0c603ededc36791db771f82c35f6b8c283a65d6b81ad098175290ce8d125d22127968820d29dcbc69c4241b7e703013c5091a1c433bfaf86eaad482ea
checksum: 10/2e387815586c436c5c24a116f0294dddcf1a5bc0a84b8dd73bdb9fc797fd5aca2aeaa77b4da3908f48e4b18a356751d9241473bfbe4951edf56138e0a62994e1
languageName: node
linkType: hard
@@ -4728,12 +4731,43 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/frontend-plugin-api@npm:^0.10.0":
version: 0.10.0
resolution: "@backstage/frontend-plugin-api@npm:0.10.0"
"@backstage/frontend-dynamic-feature-loader@workspace:packages/frontend-dynamic-feature-loader":
version: 0.0.0-use.local
resolution: "@backstage/frontend-dynamic-feature-loader@workspace:packages/frontend-dynamic-feature-loader"
dependencies:
"@backstage/core-components": "npm:^0.17.0"
"@backstage/core-plugin-api": "npm:^1.10.5"
"@backstage/cli": "workspace:^"
"@backstage/config": "workspace:^"
"@backstage/frontend-plugin-api": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@module-federation/enhanced": "npm:^0.9.0"
"@module-federation/runtime": "npm:^0.9.0"
"@module-federation/sdk": "npm:^0.9.0"
"@testing-library/jest-dom": "npm:^6.0.0"
"@testing-library/react": "npm:^16.0.0"
"@types/react": "npm:^18.0.0"
cross-fetch: "npm:^4.0.0"
msw: "npm:^1.0.0"
react: "npm:^18.0.2"
react-dom: "npm:^18.0.2"
react-router-dom: "npm:^6.3.0"
uri-template: "npm:^2.0.0"
peerDependencies:
"@types/react": ^17.0.0 || ^18.0.0
react: ^17.0.0 || ^18.0.0
react-dom: ^17.0.0 || ^18.0.0
react-router-dom: ^6.3.0
peerDependenciesMeta:
"@types/react":
optional: true
languageName: unknown
linkType: soft
"@backstage/frontend-plugin-api@npm:^0.9.5":
version: 0.9.5
resolution: "@backstage/frontend-plugin-api@npm:0.9.5"
dependencies:
"@backstage/core-components": "npm:^0.16.4"
"@backstage/core-plugin-api": "npm:^1.10.4"
"@backstage/types": "npm:^1.2.1"
"@backstage/version-bridge": "npm:^1.0.11"
"@material-ui/core": "npm:^4.12.4"
@@ -4748,7 +4782,7 @@ __metadata:
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10/9f52031e65d38087da1ad8d3117fdb2c54dbde20326f32c7cfebd755af6d28d881c9e57b5f9c17f4920fb40eea90b3b5b48b4cfc95f7dd2a3359866544ad5411
checksum: 10/91a55d7d12545aa8041b64c2443e566775c9c6cca62e6b8bd5d3ac5eb4c8f2d28a8c7bb602c1389537be4ce61d0bf93e2ddeddd4e2fa3ae691817a3bb55fabab
languageName: node
linkType: hard
@@ -4786,15 +4820,15 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/frontend-test-utils@npm:^0.3.0":
version: 0.3.0
resolution: "@backstage/frontend-test-utils@npm:0.3.0"
"@backstage/frontend-test-utils@npm:^0.2.6":
version: 0.2.6
resolution: "@backstage/frontend-test-utils@npm:0.2.6"
dependencies:
"@backstage/config": "npm:^1.3.2"
"@backstage/frontend-app-api": "npm:^0.11.0"
"@backstage/frontend-plugin-api": "npm:^0.10.0"
"@backstage/plugin-app": "npm:^0.1.7"
"@backstage/test-utils": "npm:^1.7.6"
"@backstage/frontend-app-api": "npm:^0.10.5"
"@backstage/frontend-plugin-api": "npm:^0.9.5"
"@backstage/plugin-app": "npm:^0.1.6"
"@backstage/test-utils": "npm:^1.7.5"
"@backstage/types": "npm:^1.2.1"
"@backstage/version-bridge": "npm:^1.0.11"
zod: "npm:^3.22.4"
@@ -4807,7 +4841,7 @@ __metadata:
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10/d141510fd32dc585677e1e1d7e0962b7656fd3ba7c214ef6341da39771e6fc41f5a1ca593ccfe660017fab15c90e7f7166c501618cb4c21d076ea98f101039dc
checksum: 10/0fd0c43c85f6a28c23840ac44064d9294340e892bb6e316d66afc683b05eaadebc9415a32a3f98fb81d4e8090b9496255e10ed519bb74757e089ab1d70794174
languageName: node
linkType: hard
@@ -4860,7 +4894,7 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/integration-react@npm:^1.1.24, @backstage/integration-react@npm:^1.2.5":
"@backstage/integration-react@npm:^1.1.24, @backstage/integration-react@npm:^1.2.4":
version: 1.2.5
resolution: "@backstage/integration-react@npm:1.2.5"
dependencies:
@@ -5091,17 +5125,16 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/plugin-app@npm:^0.1.7":
version: 0.1.7
resolution: "@backstage/plugin-app@npm:0.1.7"
"@backstage/plugin-app@npm:^0.1.6":
version: 0.1.6
resolution: "@backstage/plugin-app@npm:0.1.6"
dependencies:
"@backstage/core-components": "npm:^0.17.0"
"@backstage/core-plugin-api": "npm:^1.10.5"
"@backstage/frontend-plugin-api": "npm:^0.10.0"
"@backstage/integration-react": "npm:^1.2.5"
"@backstage/plugin-permission-react": "npm:^0.4.32"
"@backstage/core-components": "npm:^0.16.4"
"@backstage/core-plugin-api": "npm:^1.10.4"
"@backstage/frontend-plugin-api": "npm:^0.9.5"
"@backstage/integration-react": "npm:^1.2.4"
"@backstage/plugin-permission-react": "npm:^0.4.31"
"@backstage/theme": "npm:^0.6.4"
"@backstage/types": "npm:^1.2.1"
"@material-ui/core": "npm:^4.9.13"
"@material-ui/icons": "npm:^4.9.1"
"@material-ui/lab": "npm:^4.0.0-alpha.61"
@@ -5114,7 +5147,7 @@ __metadata:
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10/29819de2c81dbeb787e485d43b8ccb475d42fb4e071ce2eab6e0a814e35f5d6f14deb1266082326dd3b07005136a34b780a14af3aed7efae6f0fff4ae28bc9cb
checksum: 10/6ba4de2ba60b95366f71b1b301c234a3d7ff34356db006696e8cae15b8c1ba9e3f31e17acf205cc74f2faa068b55ecdc535b1c087ae3a97c90915841192efee6
languageName: node
linkType: hard
@@ -6276,22 +6309,22 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/plugin-catalog-react@npm:^1.14.0, @backstage/plugin-catalog-react@npm:^1.16.0, @backstage/plugin-catalog-react@npm:^1.9.3":
version: 1.16.0
resolution: "@backstage/plugin-catalog-react@npm:1.16.0"
"@backstage/plugin-catalog-react@npm:^1.14.0, @backstage/plugin-catalog-react@npm:^1.9.3":
version: 1.15.2
resolution: "@backstage/plugin-catalog-react@npm:1.15.2"
dependencies:
"@backstage/catalog-client": "npm:^1.9.1"
"@backstage/catalog-model": "npm:^1.7.3"
"@backstage/core-compat-api": "npm:^0.4.0"
"@backstage/core-components": "npm:^0.17.0"
"@backstage/core-plugin-api": "npm:^1.10.5"
"@backstage/core-compat-api": "npm:^0.3.6"
"@backstage/core-components": "npm:^0.16.4"
"@backstage/core-plugin-api": "npm:^1.10.4"
"@backstage/errors": "npm:^1.2.7"
"@backstage/frontend-plugin-api": "npm:^0.10.0"
"@backstage/frontend-test-utils": "npm:^0.3.0"
"@backstage/integration-react": "npm:^1.2.5"
"@backstage/frontend-plugin-api": "npm:^0.9.5"
"@backstage/frontend-test-utils": "npm:^0.2.6"
"@backstage/integration-react": "npm:^1.2.4"
"@backstage/plugin-catalog-common": "npm:^1.1.3"
"@backstage/plugin-permission-common": "npm:^0.8.4"
"@backstage/plugin-permission-react": "npm:^0.4.32"
"@backstage/plugin-permission-react": "npm:^0.4.31"
"@backstage/types": "npm:^1.2.1"
"@backstage/version-bridge": "npm:^1.0.11"
"@material-ui/core": "npm:^4.12.2"
@@ -6313,7 +6346,7 @@ __metadata:
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10/0149b99ede5d563cd81dfa1d657cf332af2cbb3e6ff2e88244c4b3c59e2109264c62e3a6903ee471596a3c709294b81fe2da83513d79690704fb37f6cf67d305
checksum: 10/379f934cc871a101714675bbe6aa51a4b0a831731dfac807004b74c804b107f2def033b53bb56c10c7bda1b16d07a014c00b374fccad7677e3f9b29be51fc354
languageName: node
linkType: hard
@@ -7381,7 +7414,7 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/plugin-permission-react@npm:^0.4.32":
"@backstage/plugin-permission-react@npm:^0.4.31, @backstage/plugin-permission-react@npm:^0.4.32":
version: 0.4.32
resolution: "@backstage/plugin-permission-react@npm:0.4.32"
dependencies:
@@ -8804,7 +8837,7 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/test-utils@npm:^1.7.6":
"@backstage/test-utils@npm:^1.7.5":
version: 1.7.6
resolution: "@backstage/test-utils@npm:1.7.6"
dependencies:
@@ -12032,39 +12065,39 @@ __metadata:
languageName: node
linkType: hard
"@module-federation/bridge-react-webpack-plugin@npm:0.9.0":
version: 0.9.0
resolution: "@module-federation/bridge-react-webpack-plugin@npm:0.9.0"
"@module-federation/bridge-react-webpack-plugin@npm:0.9.1":
version: 0.9.1
resolution: "@module-federation/bridge-react-webpack-plugin@npm:0.9.1"
dependencies:
"@module-federation/sdk": "npm:0.9.0"
"@module-federation/sdk": "npm:0.9.1"
"@types/semver": "npm:7.5.8"
semver: "npm:7.6.3"
checksum: 10/234b022407b1d84790aecc32f2b825ad22c7ebfa541914367270f307b0962194511460d6ace0b7c76bcf9aaf6dc4d09c3021f4cb73ec6568412d31023bf9c93f
checksum: 10/4ff197741b1bdccf8f9e2236781e5ce3ef434e4207c5462b4b95b044c4c57a3ab3dd4dce48490d9fde5bd06fb855b9918841c7d04306726ee224d3e288074091
languageName: node
linkType: hard
"@module-federation/data-prefetch@npm:0.9.0":
version: 0.9.0
resolution: "@module-federation/data-prefetch@npm:0.9.0"
"@module-federation/data-prefetch@npm:0.9.1":
version: 0.9.1
resolution: "@module-federation/data-prefetch@npm:0.9.1"
dependencies:
"@module-federation/runtime": "npm:0.9.0"
"@module-federation/sdk": "npm:0.9.0"
"@module-federation/runtime": "npm:0.9.1"
"@module-federation/sdk": "npm:0.9.1"
fs-extra: "npm:9.1.0"
peerDependencies:
react: ">=16.9.0"
react-dom: ">=16.9.0"
checksum: 10/d8cfef2e3427326048e17a686e3d2df8df7f6913170ddd03fff93af29bd03e68ab26f8b2587bec99357e2fb3fbed457f81e3ba6e8af98deacd744850869c1516
checksum: 10/a3c0e8d77f4d06e3851d041175ef3f55d47ff4069a526b9ac773c8aba71a520a0490653c87cdcf3761e96f9eb9ea16d45026f3aafdf0851c34ff4db6d71ec113
languageName: node
linkType: hard
"@module-federation/dts-plugin@npm:0.9.0":
version: 0.9.0
resolution: "@module-federation/dts-plugin@npm:0.9.0"
"@module-federation/dts-plugin@npm:0.9.1":
version: 0.9.1
resolution: "@module-federation/dts-plugin@npm:0.9.1"
dependencies:
"@module-federation/error-codes": "npm:0.9.0"
"@module-federation/managers": "npm:0.9.0"
"@module-federation/sdk": "npm:0.9.0"
"@module-federation/third-party-dts-extractor": "npm:0.9.0"
"@module-federation/error-codes": "npm:0.9.1"
"@module-federation/managers": "npm:0.9.1"
"@module-federation/sdk": "npm:0.9.1"
"@module-federation/third-party-dts-extractor": "npm:0.9.1"
adm-zip: "npm:^0.5.10"
ansi-colors: "npm:^4.1.3"
axios: "npm:^1.7.4"
@@ -12083,24 +12116,24 @@ __metadata:
peerDependenciesMeta:
vue-tsc:
optional: true
checksum: 10/539d277dd7c769ec9edde394fe3e9dddb38533c6fde0ac0fef0346d1229795a1b2199c4cc73e457b0ac2480c3548814e01d9fa47e12327fe97e3f6486300002d
checksum: 10/e9fd11b150456f2621636587d181f6456fe5cd60a0720843fe1310e350c3ff8ebef02ea87d1bf40dc04a4b4cd65cfb2e9007ca1c20e4e5664142b7c670c4a57d
languageName: node
linkType: hard
"@module-federation/enhanced@npm:^0.9.0":
version: 0.9.0
resolution: "@module-federation/enhanced@npm:0.9.0"
version: 0.9.1
resolution: "@module-federation/enhanced@npm:0.9.1"
dependencies:
"@module-federation/bridge-react-webpack-plugin": "npm:0.9.0"
"@module-federation/data-prefetch": "npm:0.9.0"
"@module-federation/dts-plugin": "npm:0.9.0"
"@module-federation/error-codes": "npm:0.9.0"
"@module-federation/inject-external-runtime-core-plugin": "npm:0.9.0"
"@module-federation/managers": "npm:0.9.0"
"@module-federation/manifest": "npm:0.9.0"
"@module-federation/rspack": "npm:0.9.0"
"@module-federation/runtime-tools": "npm:0.9.0"
"@module-federation/sdk": "npm:0.9.0"
"@module-federation/bridge-react-webpack-plugin": "npm:0.9.1"
"@module-federation/data-prefetch": "npm:0.9.1"
"@module-federation/dts-plugin": "npm:0.9.1"
"@module-federation/error-codes": "npm:0.9.1"
"@module-federation/inject-external-runtime-core-plugin": "npm:0.9.1"
"@module-federation/managers": "npm:0.9.1"
"@module-federation/manifest": "npm:0.9.1"
"@module-federation/rspack": "npm:0.9.1"
"@module-federation/runtime-tools": "npm:0.9.1"
"@module-federation/sdk": "npm:0.9.1"
btoa: "npm:^1.2.1"
upath: "npm:2.0.1"
peerDependencies:
@@ -12114,7 +12147,7 @@ __metadata:
optional: true
webpack:
optional: true
checksum: 10/9cca13bf8118ed3e11a54adb1006dc5a55437c0140d1edff3a8969c0ed90c07a268f42360e47f90070a32cdfedc340129b3b4e50143ca846c3b8e4ee5880523e
checksum: 10/a7955711f37ba02a18f3570289dfd8fbed30511b82fcf593e56808559cfd49d8852028aa67950db5e45617d0917536d067d85847892e62c20b68ba6ab40e17c4
languageName: node
linkType: hard
@@ -12125,57 +12158,57 @@ __metadata:
languageName: node
linkType: hard
"@module-federation/error-codes@npm:0.9.0":
version: 0.9.0
resolution: "@module-federation/error-codes@npm:0.9.0"
checksum: 10/9ebe1bbf3ed61b1d9ec3643d6743884ab5b44290fe5f1cb080ef3f514f1bf9c4cd39309fa3733697030f2468577c6f5159ec0a12d1cb69c7842aaee11159a647
"@module-federation/error-codes@npm:0.9.1":
version: 0.9.1
resolution: "@module-federation/error-codes@npm:0.9.1"
checksum: 10/545aecc606a506ee47f061835e0eaa41b8d1b02f6bf71b36ec9ae85a1b0370af1f7b7cf92a8f52c3c4b35da858653244316de5ab06bea5dac5b92995467631cc
languageName: node
linkType: hard
"@module-federation/inject-external-runtime-core-plugin@npm:0.9.0":
version: 0.9.0
resolution: "@module-federation/inject-external-runtime-core-plugin@npm:0.9.0"
"@module-federation/inject-external-runtime-core-plugin@npm:0.9.1":
version: 0.9.1
resolution: "@module-federation/inject-external-runtime-core-plugin@npm:0.9.1"
peerDependencies:
"@module-federation/runtime-tools": 0.9.0
checksum: 10/7821cc6bc2cd865ec4b0496c1c53e009611e1bf7571ca6be7e852e79de418df3952ce02b02f7a6773d833638bccbc2b3b853ebd22869a6abdaec717dc5996a8a
"@module-federation/runtime-tools": 0.9.1
checksum: 10/931eef6292c278450fc8cdb017073fa0b721796461eee12a254fc60a88a2f17e91395b12371f2c8b3b25b4056fc2dd2b76d1022e679be7353947c3d797e75ea5
languageName: node
linkType: hard
"@module-federation/managers@npm:0.9.0":
version: 0.9.0
resolution: "@module-federation/managers@npm:0.9.0"
"@module-federation/managers@npm:0.9.1":
version: 0.9.1
resolution: "@module-federation/managers@npm:0.9.1"
dependencies:
"@module-federation/sdk": "npm:0.9.0"
"@module-federation/sdk": "npm:0.9.1"
find-pkg: "npm:2.0.0"
fs-extra: "npm:9.1.0"
checksum: 10/64f8430d6beff7b76e0fe1eaead080bd42a8565bcd3c80b7dcbfba69578c8407ee725785aee847c11e3c3a28c5defb30a39780cb319dca3140c8296c5af0fcfe
checksum: 10/32c1666244ba98644ab6eccdc844415d1aece1c105f6ba2ff17a3836866e7cdb2f61e556cb5a2b506b898ec709951f318d397f4d153efeff377067237968462f
languageName: node
linkType: hard
"@module-federation/manifest@npm:0.9.0":
version: 0.9.0
resolution: "@module-federation/manifest@npm:0.9.0"
"@module-federation/manifest@npm:0.9.1":
version: 0.9.1
resolution: "@module-federation/manifest@npm:0.9.1"
dependencies:
"@module-federation/dts-plugin": "npm:0.9.0"
"@module-federation/managers": "npm:0.9.0"
"@module-federation/sdk": "npm:0.9.0"
"@module-federation/dts-plugin": "npm:0.9.1"
"@module-federation/managers": "npm:0.9.1"
"@module-federation/sdk": "npm:0.9.1"
chalk: "npm:3.0.0"
find-pkg: "npm:2.0.0"
checksum: 10/4902b945845fc3a0d1cffb33227fadc235ed02f62e436bb90dfb4c45b32f508e3692ccc4799733d8439314b8588ef14e597ef11927eea74b6ecd3878924e6ad6
checksum: 10/539b86bd5388296fb35a34c7b732b92788600ab6625d53f11588ad67c3a603ac3c1974541d9cf04db2d24635be1610414c29388e849d96bbb812cad3b1c79425
languageName: node
linkType: hard
"@module-federation/rspack@npm:0.9.0":
version: 0.9.0
resolution: "@module-federation/rspack@npm:0.9.0"
"@module-federation/rspack@npm:0.9.1":
version: 0.9.1
resolution: "@module-federation/rspack@npm:0.9.1"
dependencies:
"@module-federation/bridge-react-webpack-plugin": "npm:0.9.0"
"@module-federation/dts-plugin": "npm:0.9.0"
"@module-federation/inject-external-runtime-core-plugin": "npm:0.9.0"
"@module-federation/managers": "npm:0.9.0"
"@module-federation/manifest": "npm:0.9.0"
"@module-federation/runtime-tools": "npm:0.9.0"
"@module-federation/sdk": "npm:0.9.0"
"@module-federation/bridge-react-webpack-plugin": "npm:0.9.1"
"@module-federation/dts-plugin": "npm:0.9.1"
"@module-federation/inject-external-runtime-core-plugin": "npm:0.9.1"
"@module-federation/managers": "npm:0.9.1"
"@module-federation/manifest": "npm:0.9.1"
"@module-federation/runtime-tools": "npm:0.9.1"
"@module-federation/sdk": "npm:0.9.1"
peerDependencies:
"@rspack/core": ">=0.7"
typescript: ^4.9.0 || ^5.0.0
@@ -12185,7 +12218,7 @@ __metadata:
optional: true
vue-tsc:
optional: true
checksum: 10/3c67889f1f4114ea2f0180f3ba5f2e9cbbc687590d6ec07a20638655b4041a1874bafbe685a93fcb8132c9ad67ace7fe6cde474c70562f8c4fb9af9d9de5aa62
checksum: 10/e6deb7236ccc66d7f0475a09916ec714c9855847bfd3d3ec6cdbee991016a222a22f6ae3fd14d71351d55e22e8949140f37ce9ce79dece5416731c6698726096
languageName: node
linkType: hard
@@ -12199,13 +12232,13 @@ __metadata:
languageName: node
linkType: hard
"@module-federation/runtime-core@npm:0.6.21":
version: 0.6.21
resolution: "@module-federation/runtime-core@npm:0.6.21"
"@module-federation/runtime-core@npm:0.9.1":
version: 0.9.1
resolution: "@module-federation/runtime-core@npm:0.9.1"
dependencies:
"@module-federation/error-codes": "npm:0.9.0"
"@module-federation/sdk": "npm:0.9.0"
checksum: 10/6cff7180b7fd36f37188bc8605861f709ffdb89a1d4600c6d60c6fa4fb44672927f855fbae973e7d0b73f4d1df3408e96af6f616890225d5b4463e48afc2d1f1
"@module-federation/error-codes": "npm:0.9.1"
"@module-federation/sdk": "npm:0.9.1"
checksum: 10/6f9edbe23013395d7896fc2a24cb4055bc78df5a335f090e079df951835c1cf91c567228f19879eee3fddb0b34128abd0b50feaca1cf3fb2828c7b9bacc22169
languageName: node
linkType: hard
@@ -12219,13 +12252,13 @@ __metadata:
languageName: node
linkType: hard
"@module-federation/runtime-tools@npm:0.9.0":
version: 0.9.0
resolution: "@module-federation/runtime-tools@npm:0.9.0"
"@module-federation/runtime-tools@npm:0.9.1":
version: 0.9.1
resolution: "@module-federation/runtime-tools@npm:0.9.1"
dependencies:
"@module-federation/runtime": "npm:0.9.0"
"@module-federation/webpack-bundler-runtime": "npm:0.9.0"
checksum: 10/c92c4614745ded2bbc43edcadb6d661fc392efdc8b0dc72e1f8e34763d3dd326f4b986d4db7ae8e911d6422e5f2cc212d052bd57067e9c4d52a7a572cbdf914c
"@module-federation/runtime": "npm:0.9.1"
"@module-federation/webpack-bundler-runtime": "npm:0.9.1"
checksum: 10/9436e814a4ab72839b8aed1aa097f32cf7d4d49728dd863c9ce6dc4fb149fe49da6dc9cdeb97814f0023cc91489c9aca06cbfdf9fb4e43d20498ab6ee78c95dd
languageName: node
linkType: hard
@@ -12240,14 +12273,14 @@ __metadata:
languageName: node
linkType: hard
"@module-federation/runtime@npm:0.9.0":
version: 0.9.0
resolution: "@module-federation/runtime@npm:0.9.0"
"@module-federation/runtime@npm:0.9.1, @module-federation/runtime@npm:^0.9.0":
version: 0.9.1
resolution: "@module-federation/runtime@npm:0.9.1"
dependencies:
"@module-federation/error-codes": "npm:0.9.0"
"@module-federation/runtime-core": "npm:0.6.21"
"@module-federation/sdk": "npm:0.9.0"
checksum: 10/4bed92de413925fa66ffac9f176a72a83b47266830b9a7fe6bbc2e3eea226110455a43d737b1476b314c2a7b38564debe7e1dd6d066a0c6724f8c3be07b2eeef
"@module-federation/error-codes": "npm:0.9.1"
"@module-federation/runtime-core": "npm:0.9.1"
"@module-federation/sdk": "npm:0.9.1"
checksum: 10/71eb1c3e81b307ebfe06c43ce70bfa56b217e9dfbed27f0b4235d3d0d05cc0fe2eb1dc57fffb3260ed9e0239257ef117ae13924c642b5ff97d9c65bdf48206fe
languageName: node
linkType: hard
@@ -12258,23 +12291,21 @@ __metadata:
languageName: node
linkType: hard
"@module-federation/sdk@npm:0.9.0":
version: 0.9.0
resolution: "@module-federation/sdk@npm:0.9.0"
dependencies:
isomorphic-rslog: "npm:0.0.7"
checksum: 10/031db27afc3ee552dd052f12c8b3d6d967b6590768ebd9b5052c37047453261b9d3bb68e325bdf018f9db88e0a7276bdfeaf82b5cef332f4dd1d5e4a91add0f4
"@module-federation/sdk@npm:0.9.1, @module-federation/sdk@npm:^0.9.0":
version: 0.9.1
resolution: "@module-federation/sdk@npm:0.9.1"
checksum: 10/ea0320feff328a05405e65503d1df28e46a9ad17ef99b77f1428db5c89efbadd2a76ec82d99a54ac1d21286cee6d9ddbba881a9c46748dfe8abdb11e9afef7da
languageName: node
linkType: hard
"@module-federation/third-party-dts-extractor@npm:0.9.0":
version: 0.9.0
resolution: "@module-federation/third-party-dts-extractor@npm:0.9.0"
"@module-federation/third-party-dts-extractor@npm:0.9.1":
version: 0.9.1
resolution: "@module-federation/third-party-dts-extractor@npm:0.9.1"
dependencies:
find-pkg: "npm:2.0.0"
fs-extra: "npm:9.1.0"
resolve: "npm:1.22.8"
checksum: 10/accece159cded982499f239b8e7cc7410249ee91b28584652aebb7cc13d07b540939fa9af7ac2609ca20c4b7d2bd113d7120ad535722a3178c404bf19b8ed048
checksum: 10/68c70c79573cd927212d95879aa7b35490519d0ec9f58dd264c9d61e22fea8d452e45a52a94155128d8378e5cdcaecb229707b5b0f4e4c0d53ae96e2fbac366a
languageName: node
linkType: hard
@@ -12288,13 +12319,13 @@ __metadata:
languageName: node
linkType: hard
"@module-federation/webpack-bundler-runtime@npm:0.9.0":
version: 0.9.0
resolution: "@module-federation/webpack-bundler-runtime@npm:0.9.0"
"@module-federation/webpack-bundler-runtime@npm:0.9.1":
version: 0.9.1
resolution: "@module-federation/webpack-bundler-runtime@npm:0.9.1"
dependencies:
"@module-federation/runtime": "npm:0.9.0"
"@module-federation/sdk": "npm:0.9.0"
checksum: 10/8199fa4b5b088e717ac6cd43bd4d2038099d3660568cf79b9e376a395906c40dfe02095f1e1c2c083e3195326a7648dc05ea495701c91eee1fac0e64246cf783
"@module-federation/runtime": "npm:0.9.1"
"@module-federation/sdk": "npm:0.9.1"
checksum: 10/430cac0a770b3c46bc195088eb4c1892e1e29a69238dbe72423d64b2b67050afeca2b5026b1a30659b4fe8d9faa038ff97cceba7e2ddf6193b93763f270d9df6
languageName: node
linkType: hard
@@ -33563,13 +33594,6 @@ __metadata:
languageName: node
linkType: hard
"isomorphic-rslog@npm:0.0.7":
version: 0.0.7
resolution: "isomorphic-rslog@npm:0.0.7"
checksum: 10/cb6f03a64bdbb3972c22429a4fcdff25505ceca2bf09ca2cebc1c8086b2580f67d4dd04daba79d7dd642af099deea1eb884667ddf183bd3192b67faaf86438cc
languageName: node
linkType: hard
"isomorphic-textencoder@npm:^1.0.1":
version: 1.0.1
resolution: "isomorphic-textencoder@npm:1.0.1"