Update the frontend dynamic feature loader according to changes in the backend remote asset server.

Signed-off-by: David Festal <dfestal@redhat.com>
This commit is contained in:
David Festal
2025-04-08 21:49:41 +02:00
parent f7886f62fd
commit 4335b04da0
15 changed files with 639 additions and 418 deletions
@@ -3,16 +3,22 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { CreateAppFeatureLoader } from '@backstage/frontend-defaults';
import { init } from '@module-federation/enhanced/runtime';
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,
): CreateAppFeatureLoader;
): FrontendFeatureLoader;
// @public (undocumented)
export type DynamicFrontendFeaturesLoaderOptions = {
moduleFederation: Omit<Parameters<typeof init>[0], 'name' | 'remotes'>;
moduleFederation: {
shared?: UserOptions['shared'];
shareStrategy?: ShareStrategy;
plugins?: Array<FederationRuntimePlugin>;
};
};
```
@@ -26,6 +26,7 @@ import { Module } from '@module-federation/sdk';
import { createFrontendPlugin } from '@backstage/frontend-plugin-api';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { InternalFrontendFeatureLoader } from '../../frontend-plugin-api/src/wiring/createFrontendFeatureLoader';
import { resetFederationGlobalInfo } from '@module-federation/runtime/core';
const baseUrl = 'http://localhost:7007';
@@ -37,6 +38,7 @@ describe('dynamicFrontendFeaturesLoader', () => {
error: jest.spyOn(console, 'error').mockImplementation(() => {}),
warn: jest.spyOn(console, 'warn').mockImplementation(() => {}),
info: jest.spyOn(console, 'info').mockImplementation(() => {}),
debug: jest.spyOn(console, 'debug').mockImplementation(() => {}),
},
federation: {
get: jest.fn((_: { name: string; id: string }): Module => ({})),
@@ -53,10 +55,6 @@ describe('dynamicFrontendFeaturesLoader', () => {
plugins: [
{
name: 'load-entry-mock',
errorLoadRemote: args => {
// eslint-disable-next-line no-console
console.error(args);
},
loadEntry: async args => {
return {
get: (id: string) => async () => {
@@ -103,14 +101,17 @@ describe('dynamicFrontendFeaturesLoader', () => {
mocks.console.error.mockReset();
mocks.console.warn.mockReset();
mocks.console.info.mockReset();
mocks.console.debug.mockReset();
mocks.federation.get.mockReset();
mocks.federation.onLoad.mockReset();
resetFederationGlobalInfo();
});
it('should return immediately if dynamic plugins are not enabled in config', async () => {
let manifestsEndpointCalled = false;
server.use(
rest.get(
`${baseUrl}/api/core.dynamicplugins.frontendRemotes/manifests`,
`${baseUrl}/.backstage/dynamic-plugins/remotes`,
(_, res, ctx) => {
manifestsEndpointCalled = true;
return res(ctx.json({}));
@@ -153,16 +154,23 @@ describe('dynamicFrontendFeaturesLoader', () => {
it('should load a dynamic frontend plugin with the default exposed remote module', async () => {
server.use(
rest.get(
`${baseUrl}/api/core.dynamicplugins.frontendRemotes/manifests`,
`${baseUrl}/.backstage/dynamic-features/remotes`,
(_, res, ctx) =>
res(
ctx.json({
'test-plugin': `${baseUrl}/api/core.dynamicplugins.frontendRemotes/remotes/test-plugin/mf-manifest.json`,
}),
ctx.json([
{
packageName: 'plugin-test-dynamic',
exposedModules: ['.'],
remoteInfo: {
name: 'test_plugin',
entry: `${baseUrl}/.backstage/dynamic-features/remotes/plugin-test-dynamic/mf-manifest.json`,
},
},
]),
),
),
rest.get(
`${baseUrl}/api/core.dynamicplugins.frontendRemotes/remotes/test-plugin/mf-manifest.json`,
`${baseUrl}/.backstage/dynamic-features/remotes/plugin-test-dynamic/mf-manifest.json`,
(_, res, ctx) =>
res(
ctx.json({
@@ -223,13 +231,16 @@ describe('dynamicFrontendFeaturesLoader', () => {
]);
const infoCalls = mocks.console.info.mock.calls.flatMap(e => e[0]);
expect(infoCalls).toEqual([
"Loading dynamic plugin 'test-plugin' from 'http://localhost:7007/api/core.dynamicplugins.frontendRemotes/remotes/test-plugin/mf-manifest.json'",
"Dynamic plugin remote module 'test-plugin' loaded from http://localhost:7007/api/core.dynamicplugins.frontendRemotes/remotes/test-plugin/mf-manifest.json",
"Remote module 'test_plugin' of dynamic plugin 'plugin-test-dynamic' loaded from http://localhost:7007/.backstage/dynamic-features/remotes/plugin-test-dynamic/mf-manifest.json",
]);
const debugCalls = mocks.console.debug.mock.calls.flatMap(e => e[0]);
expect(debugCalls).toEqual([
"Loading dynamic plugin 'plugin-test-dynamic' from 'http://localhost:7007/.backstage/dynamic-features/remotes/plugin-test-dynamic/mf-manifest.json'",
]);
expect(mocks.federation.get.mock.calls.flatMap(e => e[0])).toEqual([
{
id: '.',
name: 'test-plugin',
name: 'test_plugin',
},
]);
});
@@ -237,17 +248,31 @@ describe('dynamicFrontendFeaturesLoader', () => {
it('should load several dynamic frontend plugins', async () => {
server.use(
rest.get(
`${baseUrl}/api/core.dynamicplugins.frontendRemotes/manifests`,
`${baseUrl}/.backstage/dynamic-features/remotes`,
(_, res, ctx) =>
res(
ctx.json({
'plugin-1': `${baseUrl}/api/core.dynamicplugins.frontendRemotes/remotes/plugin-1/mf-manifest.json`,
'plugin-2': `${baseUrl}/api/core.dynamicplugins.frontendRemotes/remotes/plugin-2/mf-manifest.json`,
}),
ctx.json([
{
packageName: 'plugin-1',
exposedModules: ['.'],
remoteInfo: {
name: 'plugin_1',
entry: `${baseUrl}/.backstage/dynamic-features/remotes/plugin-1/mf-manifest.json`,
},
},
{
packageName: 'plugin-2',
exposedModules: ['.'],
remoteInfo: {
name: 'plugin_2',
entry: `${baseUrl}/.backstage/dynamic-features/remotes/plugin-2/mf-manifest.json`,
},
},
]),
),
),
rest.get(
`${baseUrl}/api/core.dynamicplugins.frontendRemotes/remotes/plugin-1/mf-manifest.json`,
`${baseUrl}/.backstage/dynamic-features/remotes/plugin-1/mf-manifest.json`,
(_, res, ctx) =>
res(
ctx.json({
@@ -265,7 +290,7 @@ describe('dynamicFrontendFeaturesLoader', () => {
),
),
rest.get(
`${baseUrl}/api/core.dynamicplugins.frontendRemotes/remotes/plugin-2/mf-manifest.json`,
`${baseUrl}/.backstage/dynamic-features/remotes/plugin-2/mf-manifest.json`,
(_, res, ctx) =>
res(
ctx.json({
@@ -305,7 +330,6 @@ describe('dynamicFrontendFeaturesLoader', () => {
config: mockApis.config({
data: {
app: {
packageName: 'app-2',
experimental: {
packages: {
include: [],
@@ -338,19 +362,22 @@ describe('dynamicFrontendFeaturesLoader', () => {
]);
const infoCalls = mocks.console.info.mock.calls.flatMap(e => e[0]);
expect(infoCalls).toEqual([
"Loading dynamic plugin 'plugin-1' from 'http://localhost:7007/api/core.dynamicplugins.frontendRemotes/remotes/plugin-1/mf-manifest.json'",
"Loading dynamic plugin 'plugin-2' from 'http://localhost:7007/api/core.dynamicplugins.frontendRemotes/remotes/plugin-2/mf-manifest.json'",
"Dynamic plugin remote module 'plugin-1' loaded from http://localhost:7007/api/core.dynamicplugins.frontendRemotes/remotes/plugin-1/mf-manifest.json",
"Dynamic plugin remote module 'plugin-2' loaded from http://localhost:7007/api/core.dynamicplugins.frontendRemotes/remotes/plugin-2/mf-manifest.json",
"Remote module 'plugin_1' of dynamic plugin 'plugin-1' loaded from http://localhost:7007/.backstage/dynamic-features/remotes/plugin-1/mf-manifest.json",
"Remote module 'plugin_2' of dynamic plugin 'plugin-2' loaded from http://localhost:7007/.backstage/dynamic-features/remotes/plugin-2/mf-manifest.json",
]);
const debugCalls = mocks.console.debug.mock.calls.flatMap(e => e[0]);
expect(debugCalls).toEqual([
"Loading dynamic plugin 'plugin-1' from 'http://localhost:7007/.backstage/dynamic-features/remotes/plugin-1/mf-manifest.json'",
"Loading dynamic plugin 'plugin-2' from 'http://localhost:7007/.backstage/dynamic-features/remotes/plugin-2/mf-manifest.json'",
]);
expect(mocks.federation.get.mock.calls.flatMap(e => e[0])).toEqual([
{
id: '.',
name: 'plugin-1',
name: 'plugin_1',
},
{
id: '.',
name: 'plugin-2',
name: 'plugin_2',
},
]);
});
@@ -358,16 +385,23 @@ describe('dynamicFrontendFeaturesLoader', () => {
it('should load a dynamic frontend plugin with several exposed remote modules', async () => {
server.use(
rest.get(
`${baseUrl}/api/core.dynamicplugins.frontendRemotes/manifests`,
`${baseUrl}/.backstage/dynamic-features/remotes`,
(_, res, ctx) =>
res(
ctx.json({
'test-plugin': `${baseUrl}/api/core.dynamicplugins.frontendRemotes/remotes/test-plugin/mf-manifest.json`,
}),
ctx.json([
{
packageName: 'plugin-test-dynamic',
exposedModules: ['.', 'alpha'],
remoteInfo: {
name: 'test_plugin',
entry: `${baseUrl}/.backstage/dynamic-features/remotes/plugin-test-dynamic/mf-manifest.json`,
},
},
]),
),
),
rest.get(
`${baseUrl}/api/core.dynamicplugins.frontendRemotes/remotes/test-plugin/mf-manifest.json`,
`${baseUrl}/.backstage/dynamic-features/remotes/plugin-test-dynamic/mf-manifest.json`,
(_, res, ctx) =>
res(
ctx.json({
@@ -418,7 +452,6 @@ describe('dynamicFrontendFeaturesLoader', () => {
include: [],
},
},
packageName: 'app-3',
},
backend: {
baseUrl,
@@ -446,18 +479,104 @@ describe('dynamicFrontendFeaturesLoader', () => {
]);
const infoCalls = mocks.console.info.mock.calls.flatMap(e => e[0]);
expect(infoCalls).toEqual([
"Loading dynamic plugin 'test-plugin' from 'http://localhost:7007/api/core.dynamicplugins.frontendRemotes/remotes/test-plugin/mf-manifest.json'",
"Dynamic plugin remote module 'test-plugin' loaded from http://localhost:7007/api/core.dynamicplugins.frontendRemotes/remotes/test-plugin/mf-manifest.json",
"Dynamic plugin remote module 'test-plugin/alpha' loaded from http://localhost:7007/api/core.dynamicplugins.frontendRemotes/remotes/test-plugin/mf-manifest.json",
"Remote module 'test_plugin' of dynamic plugin 'plugin-test-dynamic' loaded from http://localhost:7007/.backstage/dynamic-features/remotes/plugin-test-dynamic/mf-manifest.json",
"Remote module 'test_plugin/alpha' of dynamic plugin 'plugin-test-dynamic' loaded from http://localhost:7007/.backstage/dynamic-features/remotes/plugin-test-dynamic/mf-manifest.json",
]);
const debugCalls = mocks.console.debug.mock.calls.flatMap(e => e[0]);
expect(debugCalls).toEqual([
"Loading dynamic plugin 'plugin-test-dynamic' from 'http://localhost:7007/.backstage/dynamic-features/remotes/plugin-test-dynamic/mf-manifest.json'",
]);
expect(mocks.federation.get.mock.calls.flatMap(e => e[0])).toEqual([
{
id: '.',
name: 'test-plugin',
name: 'test_plugin',
},
{
id: './alpha',
name: 'test-plugin',
name: 'test_plugin',
},
]);
});
it('should load a dynamic frontend plugin from Javascript remote entry', async () => {
mocks.federation.get.mockRestore();
mocks.federation.onLoad.mockRestore();
server.use(
rest.get(
`${baseUrl}/.backstage/dynamic-features/remotes`,
(_, res, ctx) =>
res(
ctx.json([
{
packageName: 'plugin-test-dynamic',
exposedModules: ['.'],
remoteInfo: {
name: 'test_plugin',
entry: `${baseUrl}/.backstage/dynamic-features/remotes/plugin-test-dynamic/remoteEntry.js`,
type: 'jsonp',
},
},
]),
),
),
rest.get(
`${baseUrl}/.backstage/dynamic-features/remotes/plugin-test-dynamic/remoteEntry.js`,
(_, res, ctx) => res(ctx.text('coucou :-)')),
),
);
mocks.federation.get.mockReturnValueOnce({
default: createFrontendPlugin({
id: 'test-plugin',
extensions: [],
}),
});
const features = await (
dynamicFrontendFeaturesLoader({
...getCommonOptions(),
}) as InternalFrontendFeatureLoader
).loader({
config: mockApis.config({
data: {
app: {
experimental: {
packages: {
include: [],
},
},
},
backend: {
baseUrl,
},
dynamicPlugins: {},
},
}),
});
const errorCalls = mocks.console.error.mock.calls.flatMap(e => e[0]);
expect(errorCalls).toEqual([]);
const warnCalls = mocks.console.warn.mock.calls.flatMap(e => e[0]);
expect(warnCalls).toEqual([]);
expect(features).toMatchObject([
{
$$type: '@backstage/FrontendPlugin',
id: 'test-plugin',
version: 'v1',
},
]);
const infoCalls = mocks.console.info.mock.calls.flatMap(e => e[0]);
expect(infoCalls).toEqual([
"Remote module 'test_plugin' of dynamic plugin 'plugin-test-dynamic' loaded from http://localhost:7007/.backstage/dynamic-features/remotes/plugin-test-dynamic/remoteEntry.js",
]);
const debugCalls = mocks.console.debug.mock.calls.flatMap(e => e[0]);
expect(debugCalls).toEqual([
"Loading dynamic plugin 'plugin-test-dynamic' from 'http://localhost:7007/.backstage/dynamic-features/remotes/plugin-test-dynamic/remoteEntry.js'",
]);
expect(mocks.federation.get.mock.calls.flatMap(e => e[0])).toEqual([
{
id: '.',
name: 'test_plugin',
},
]);
});
@@ -465,7 +584,7 @@ describe('dynamicFrontendFeaturesLoader', () => {
it('should warn and recover from a 404 error fetching module feredation configuration', async () => {
server.use(
rest.get(
`${baseUrl}/api/core.dynamicplugins.frontendRemotes/manifests`,
`${baseUrl}/.backstage/dynamic-features/remotes`,
(_, res, ctx) => res(ctx.status(404, 'NOT FOUND')),
),
);
@@ -511,59 +630,10 @@ describe('dynamicFrontendFeaturesLoader', () => {
expect(mocks.federation.get.mock.calls.flatMap(e => e[0])).toEqual([]);
});
it('should warn and recover from unexpected Json while fetching module feredation configuration', async () => {
server.use(
rest.get(
`${baseUrl}/api/core.dynamicplugins.frontendRemotes/manifests`,
(_, res, ctx) => res(ctx.json('A Json String')),
),
);
mocks.federation.get.mockReturnValue({
default: createFrontendPlugin({
id: 'test-plugin',
extensions: [],
}),
});
const features = await (
dynamicFrontendFeaturesLoader({
...getCommonOptions(),
}) as InternalFrontendFeatureLoader
).loader({
config: mockApis.config({
data: {
app: {
experimental: {
packages: {
include: [],
},
},
},
backend: {
baseUrl,
},
dynamicPlugins: {},
},
}),
});
const errorCalls = mocks.console.error.mock.calls.flatMap(e => e[0]);
expect(errorCalls).toEqual([
`Failed fetching module federation configuration of dynamic frontend plugins: Error: Invalid Json content: should be a Json object`,
]);
const warnCalls = mocks.console.warn.mock.calls.flatMap(e => e[0]);
expect(warnCalls).toEqual([]);
expect(features).toMatchObject([]);
const infoCalls = mocks.console.info.mock.calls.flatMap(e => e[0]);
expect(infoCalls).toEqual([]);
expect(mocks.federation.get.mock.calls.flatMap(e => e[0])).toEqual([]);
});
it('should warn and recover from empty response while fetching module feredation configuration', async () => {
server.use(
rest.get(
`${baseUrl}/api/core.dynamicplugins.frontendRemotes/manifests`,
`${baseUrl}/.backstage/dynamic-features/remotes`,
(_, res, ctx) => res(ctx.status(200)),
),
);
@@ -599,7 +669,7 @@ describe('dynamicFrontendFeaturesLoader', () => {
const errorCalls = mocks.console.error.mock.calls.flatMap(e => e[0]);
expect(errorCalls).toEqual([
`Failed fetching module federation configuration of dynamic frontend plugins: FetchError: invalid json response body at http://localhost:7007/api/core.dynamicplugins.frontendRemotes/manifests reason: Unexpected end of JSON input`,
`Failed fetching module federation configuration of dynamic frontend plugins: FetchError: invalid json response body at http://localhost:7007/.backstage/dynamic-features/remotes reason: Unexpected end of JSON input`,
]);
const warnCalls = mocks.console.warn.mock.calls.flatMap(e => e[0]);
expect(warnCalls).toEqual([]);
@@ -609,206 +679,34 @@ describe('dynamicFrontendFeaturesLoader', () => {
expect(mocks.federation.get.mock.calls.flatMap(e => e[0])).toEqual([]);
});
it('should warn on 404 error fetching module feredation manifest, but still load other remotes', async () => {
server.use(
rest.get(
`${baseUrl}/api/core.dynamicplugins.frontendRemotes/manifests`,
(_, res, ctx) =>
res(
ctx.json({
'plugin-1': `${baseUrl}/api/core.dynamicplugins.frontendRemotes/remotes/plugin-1/mf-manifest.json`,
'plugin-2': `${baseUrl}/api/core.dynamicplugins.frontendRemotes/remotes/plugin-2/mf-manifest.json`,
}),
),
),
rest.get(
`${baseUrl}/api/core.dynamicplugins.frontendRemotes/remotes/plugin-1/mf-manifest.json`,
(_, res, ctx) => res(ctx.json({}), ctx.status(404, 'NOT FOUND')),
),
rest.get(
`${baseUrl}/api/core.dynamicplugins.frontendRemotes/remotes/plugin-2/mf-manifest.json`,
(_, res, ctx) =>
res(
ctx.json({
name: 'plugin_2',
...manifestDummyData,
exposes: [
{
id: 'plugin_2:.',
name: '.',
path: '.',
...manifestExposedRemoteDummyData,
},
],
}),
),
),
);
mocks.federation.get.mockReturnValueOnce({
default: createFrontendPlugin({
id: 'plugin-2',
extensions: [],
}),
});
const features = await (
dynamicFrontendFeaturesLoader({
...getCommonOptions(),
}) as InternalFrontendFeatureLoader
).loader({
config: mockApis.config({
data: {
app: {
packageName: 'app-4',
experimental: {
packages: {
include: [],
},
},
},
backend: {
baseUrl,
},
dynamicPlugins: {},
},
}),
});
const errorCalls = mocks.console.error.mock.calls.flatMap(e => e[0]);
expect(errorCalls).toEqual([
"Failed fetching module federation manifest from 'http://localhost:7007/api/core.dynamicplugins.frontendRemotes/remotes/plugin-1/mf-manifest.json': Error: 404 - NOT FOUND",
]);
const warnCalls = mocks.console.warn.mock.calls.flatMap(e => e[0]);
expect(warnCalls).toEqual([]);
expect(features).toMatchObject([
{
$$type: '@backstage/FrontendPlugin',
id: 'plugin-2',
version: 'v1',
},
]);
const infoCalls = mocks.console.info.mock.calls.flatMap(e => e[0]);
expect(infoCalls).toEqual([
"Loading dynamic plugin 'plugin-1' from 'http://localhost:7007/api/core.dynamicplugins.frontendRemotes/remotes/plugin-1/mf-manifest.json'",
"Loading dynamic plugin 'plugin-2' from 'http://localhost:7007/api/core.dynamicplugins.frontendRemotes/remotes/plugin-2/mf-manifest.json'",
"Dynamic plugin remote module 'plugin-2' loaded from http://localhost:7007/api/core.dynamicplugins.frontendRemotes/remotes/plugin-2/mf-manifest.json",
]);
expect(mocks.federation.get.mock.calls.flatMap(e => e[0])).toEqual([
{
id: '.',
name: 'plugin-2',
},
]);
});
it('should warn on unexpected Json content while fetching module feredation manifest, but still load other remotes', async () => {
server.use(
rest.get(
`${baseUrl}/api/core.dynamicplugins.frontendRemotes/manifests`,
(_, res, ctx) =>
res(
ctx.json({
'plugin-1': `${baseUrl}/api/core.dynamicplugins.frontendRemotes/remotes/plugin-1/mf-manifest.json`,
'plugin-2': `${baseUrl}/api/core.dynamicplugins.frontendRemotes/remotes/plugin-2/mf-manifest.json`,
}),
),
),
rest.get(
`${baseUrl}/api/core.dynamicplugins.frontendRemotes/remotes/plugin-1/mf-manifest.json`,
(_, res, ctx) => res(ctx.json('A Json String')),
),
rest.get(
`${baseUrl}/api/core.dynamicplugins.frontendRemotes/remotes/plugin-2/mf-manifest.json`,
(_, res, ctx) =>
res(
ctx.json({
name: 'plugin-2',
...manifestDummyData,
exposes: [
{
id: 'plugin-2:.',
name: '.',
path: '.',
...manifestExposedRemoteDummyData,
},
],
}),
),
),
);
mocks.federation.get.mockReturnValueOnce({
default: createFrontendPlugin({
id: 'plugin-2',
extensions: [],
}),
});
const features = await (
dynamicFrontendFeaturesLoader({
...getCommonOptions(),
}) as InternalFrontendFeatureLoader
).loader({
config: mockApis.config({
data: {
app: {
packageName: 'app-5',
experimental: {
packages: {
include: [],
},
},
},
backend: {
baseUrl,
},
dynamicPlugins: {},
},
}),
});
const errorCalls = mocks.console.error.mock.calls.flatMap(e => e[0]);
expect(errorCalls).toEqual([
"Failed fetching module federation manifest from 'http://localhost:7007/api/core.dynamicplugins.frontendRemotes/remotes/plugin-1/mf-manifest.json': Error: Invalid Json content: should be a Json object",
]);
const warnCalls = mocks.console.warn.mock.calls.flatMap(e => e[0]);
expect(warnCalls).toEqual([]);
expect(features).toMatchObject([
{
$$type: '@backstage/FrontendPlugin',
id: 'plugin-2',
version: 'v1',
},
]);
const infoCalls = mocks.console.info.mock.calls.flatMap(e => e[0]);
expect(infoCalls).toEqual([
"Loading dynamic plugin 'plugin-1' from 'http://localhost:7007/api/core.dynamicplugins.frontendRemotes/remotes/plugin-1/mf-manifest.json'",
"Loading dynamic plugin 'plugin-2' from 'http://localhost:7007/api/core.dynamicplugins.frontendRemotes/remotes/plugin-2/mf-manifest.json'",
"Dynamic plugin remote module 'plugin-2' loaded from http://localhost:7007/api/core.dynamicplugins.frontendRemotes/remotes/plugin-2/mf-manifest.json",
]);
expect(mocks.federation.get.mock.calls.flatMap(e => e[0])).toEqual([
{
id: '.',
name: 'plugin-2',
},
]);
});
it('should warn on empty module, but still load other remotes', async () => {
server.use(
rest.get(
`${baseUrl}/api/core.dynamicplugins.frontendRemotes/manifests`,
`${baseUrl}/.backstage/dynamic-features/remotes`,
(_, res, ctx) =>
res(
ctx.json({
'plugin-1': `${baseUrl}/api/core.dynamicplugins.frontendRemotes/remotes/plugin-1/mf-manifest.json`,
'plugin-2': `${baseUrl}/api/core.dynamicplugins.frontendRemotes/remotes/plugin-2/mf-manifest.json`,
}),
ctx.json([
{
packageName: 'plugin-1',
exposedModules: ['.'],
remoteInfo: {
name: 'plugin_1',
entry: `${baseUrl}/.backstage/dynamic-features/remotes/plugin-1/mf-manifest.json`,
},
},
{
packageName: 'plugin-2',
exposedModules: ['.'],
remoteInfo: {
name: 'plugin_2',
entry: `${baseUrl}/.backstage/dynamic-features/remotes/plugin-2/mf-manifest.json`,
},
},
]),
),
),
rest.get(
`${baseUrl}/api/core.dynamicplugins.frontendRemotes/remotes/plugin-1/mf-manifest.json`,
`${baseUrl}/.backstage/dynamic-features/remotes/plugin-1/mf-manifest.json`,
(_, res, ctx) =>
res(
ctx.json({
@@ -826,7 +724,7 @@ describe('dynamicFrontendFeaturesLoader', () => {
),
),
rest.get(
`${baseUrl}/api/core.dynamicplugins.frontendRemotes/remotes/plugin-2/mf-manifest.json`,
`${baseUrl}/.backstage/dynamic-features/remotes/plugin-2/mf-manifest.json`,
(_, res, ctx) =>
res(
ctx.json({
@@ -861,7 +759,6 @@ describe('dynamicFrontendFeaturesLoader', () => {
config: mockApis.config({
data: {
app: {
packageName: 'app-6',
experimental: {
packages: {
include: [],
@@ -880,7 +777,7 @@ describe('dynamicFrontendFeaturesLoader', () => {
expect(errorCalls).toEqual([]);
const warnCalls = mocks.console.warn.mock.calls.flatMap(e => e[0]);
expect(warnCalls).toEqual([
"Skipping empty dynamic plugin remote module 'plugin-1'.",
"Skipping empty dynamic plugin remote module 'plugin_1'.",
]);
expect(features).toMatchObject([
{
@@ -891,36 +788,53 @@ describe('dynamicFrontendFeaturesLoader', () => {
]);
const infoCalls = mocks.console.info.mock.calls.flatMap(e => e[0]);
expect(infoCalls).toEqual([
"Loading dynamic plugin 'plugin-1' from 'http://localhost:7007/api/core.dynamicplugins.frontendRemotes/remotes/plugin-1/mf-manifest.json'",
"Loading dynamic plugin 'plugin-2' from 'http://localhost:7007/api/core.dynamicplugins.frontendRemotes/remotes/plugin-2/mf-manifest.json'",
"Dynamic plugin remote module 'plugin-2' loaded from http://localhost:7007/api/core.dynamicplugins.frontendRemotes/remotes/plugin-2/mf-manifest.json",
"Remote module 'plugin_2' of dynamic plugin 'plugin-2' loaded from http://localhost:7007/.backstage/dynamic-features/remotes/plugin-2/mf-manifest.json",
]);
const debugCalls = mocks.console.debug.mock.calls.flatMap(e => e[0]);
expect(debugCalls).toEqual([
"Loading dynamic plugin 'plugin-1' from 'http://localhost:7007/.backstage/dynamic-features/remotes/plugin-1/mf-manifest.json'",
"Loading dynamic plugin 'plugin-2' from 'http://localhost:7007/.backstage/dynamic-features/remotes/plugin-2/mf-manifest.json'",
]);
expect(mocks.federation.get.mock.calls.flatMap(e => e[0])).toEqual([
{
id: '.',
name: 'plugin-1',
name: 'plugin_1',
},
{
id: '.',
name: 'plugin-2',
name: 'plugin_2',
},
]);
});
it('should warn on module without default export, but still load other remotes', async () => {
it('should skip module without default export, but still load other remotes', async () => {
server.use(
rest.get(
`${baseUrl}/api/core.dynamicplugins.frontendRemotes/manifests`,
`${baseUrl}/.backstage/dynamic-features/remotes`,
(_, res, ctx) =>
res(
ctx.json({
'plugin-1': `${baseUrl}/api/core.dynamicplugins.frontendRemotes/remotes/plugin-1/mf-manifest.json`,
'plugin-2': `${baseUrl}/api/core.dynamicplugins.frontendRemotes/remotes/plugin-2/mf-manifest.json`,
}),
ctx.json([
{
packageName: 'plugin-1',
exposedModules: ['.'],
remoteInfo: {
name: 'plugin_1',
entry: `${baseUrl}/.backstage/dynamic-features/remotes/plugin-1/mf-manifest.json`,
},
},
{
packageName: 'plugin-2',
exposedModules: ['.'],
remoteInfo: {
name: 'plugin_2',
entry: `${baseUrl}/.backstage/dynamic-features/remotes/plugin-2/mf-manifest.json`,
},
},
]),
),
),
rest.get(
`${baseUrl}/api/core.dynamicplugins.frontendRemotes/remotes/plugin-1/mf-manifest.json`,
`${baseUrl}/.backstage/dynamic-features/remotes/plugin-1/mf-manifest.json`,
(_, res, ctx) =>
res(
ctx.json({
@@ -938,7 +852,7 @@ describe('dynamicFrontendFeaturesLoader', () => {
),
),
rest.get(
`${baseUrl}/api/core.dynamicplugins.frontendRemotes/remotes/plugin-2/mf-manifest.json`,
`${baseUrl}/.backstage/dynamic-features/remotes/plugin-2/mf-manifest.json`,
(_, res, ctx) =>
res(
ctx.json({
@@ -975,7 +889,6 @@ describe('dynamicFrontendFeaturesLoader', () => {
config: mockApis.config({
data: {
app: {
packageName: 'app-7',
experimental: {
packages: {
include: [],
@@ -993,9 +906,7 @@ describe('dynamicFrontendFeaturesLoader', () => {
const errorCalls = mocks.console.error.mock.calls.flatMap(e => e[0]);
expect(errorCalls).toEqual([]);
const warnCalls = mocks.console.warn.mock.calls.flatMap(e => e[0]);
expect(warnCalls).toEqual([
"Skipping dynamic plugin remote module 'plugin-1' since it doesn't export a new 'FrontendFeature' as default export.",
]);
expect(warnCalls).toEqual([]);
expect(features).toMatchObject([
{
$$type: '@backstage/FrontendPlugin',
@@ -1005,19 +916,242 @@ describe('dynamicFrontendFeaturesLoader', () => {
]);
const infoCalls = mocks.console.info.mock.calls.flatMap(e => e[0]);
expect(infoCalls).toEqual([
"Loading dynamic plugin 'plugin-1' from 'http://localhost:7007/api/core.dynamicplugins.frontendRemotes/remotes/plugin-1/mf-manifest.json'",
"Loading dynamic plugin 'plugin-2' from 'http://localhost:7007/api/core.dynamicplugins.frontendRemotes/remotes/plugin-2/mf-manifest.json'",
"Dynamic plugin remote module 'plugin-1' loaded from http://localhost:7007/api/core.dynamicplugins.frontendRemotes/remotes/plugin-1/mf-manifest.json",
"Dynamic plugin remote module 'plugin-2' loaded from http://localhost:7007/api/core.dynamicplugins.frontendRemotes/remotes/plugin-2/mf-manifest.json",
"Remote module 'plugin_1' of dynamic plugin 'plugin-1' loaded from http://localhost:7007/.backstage/dynamic-features/remotes/plugin-1/mf-manifest.json",
"Remote module 'plugin_2' of dynamic plugin 'plugin-2' loaded from http://localhost:7007/.backstage/dynamic-features/remotes/plugin-2/mf-manifest.json",
]);
const debugCalls = mocks.console.debug.mock.calls.flatMap(e => e[0]);
expect(debugCalls).toEqual([
"Loading dynamic plugin 'plugin-1' from 'http://localhost:7007/.backstage/dynamic-features/remotes/plugin-1/mf-manifest.json'",
"Loading dynamic plugin 'plugin-2' from 'http://localhost:7007/.backstage/dynamic-features/remotes/plugin-2/mf-manifest.json'",
"Skipping dynamic plugin remote module '[object Object]' since it doesn't export a new 'FrontendFeature' as default export.",
]);
expect(mocks.federation.get.mock.calls.flatMap(e => e[0])).toEqual([
{
id: '.',
name: 'plugin-1',
name: 'plugin_1',
},
{
id: '.',
name: 'plugin-2',
name: 'plugin_2',
},
]);
});
it('should warn on 404 error fetching module feredation manifest, but still load other remotes', async () => {
server.use(
rest.get(
`${baseUrl}/.backstage/dynamic-features/remotes`,
(_, res, ctx) =>
res(
ctx.json([
{
packageName: 'plugin-1',
exposedModules: ['.'],
remoteInfo: {
name: 'plugin_1',
entry: `${baseUrl}/.backstage/dynamic-features/remotes/plugin-1/mf-manifest.json`,
},
},
{
packageName: 'plugin-2',
exposedModules: ['.'],
remoteInfo: {
name: 'plugin_2',
entry: `${baseUrl}/.backstage/dynamic-features/remotes/plugin-2/mf-manifest.json`,
},
},
]),
),
),
rest.get(
`${baseUrl}/.backstage/dynamic-features/remotes/plugin-1/mf-manifest.json`,
(_, res, ctx) => res(ctx.json({}), ctx.status(404, 'NOT FOUND')),
),
rest.get(
`${baseUrl}/.backstage/dynamic-features/remotes/plugin-2/mf-manifest.json`,
(_, res, ctx) =>
res(
ctx.json({
name: 'plugin_2',
...manifestDummyData,
exposes: [
{
id: 'plugin_2:.',
name: '.',
path: '.',
...manifestExposedRemoteDummyData,
},
],
}),
),
),
);
mocks.federation.get.mockReturnValueOnce({
default: createFrontendPlugin({
id: 'plugin-2',
extensions: [],
}),
});
const features = await (
dynamicFrontendFeaturesLoader({
...getCommonOptions(),
}) as InternalFrontendFeatureLoader
).loader({
config: mockApis.config({
data: {
app: {
experimental: {
packages: {
include: [],
},
},
},
backend: {
baseUrl,
},
dynamicPlugins: {},
},
}),
});
const errorCalls = mocks.console.error.mock.calls.flatMap(e => e[0]);
expect(errorCalls).toEqual([
"Failed loading remote module 'plugin_1' of dynamic plugin 'plugin-1': Error: [ Federation Runtime ]: [ Federation Runtime ]: [ Federation Runtime ]: http://localhost:7007/.backstage/dynamic-features/remotes/plugin-1/mf-manifest.json is not a federation manifest",
]);
const warnCalls = mocks.console.warn.mock.calls.flatMap(e => e[0]);
expect(warnCalls).toEqual([]);
expect(features).toMatchObject([
{
$$type: '@backstage/FrontendPlugin',
id: 'plugin-2',
version: 'v1',
},
]);
const infoCalls = mocks.console.info.mock.calls.flatMap(e => e[0]);
expect(infoCalls).toEqual([
"Remote module 'plugin_2' of dynamic plugin 'plugin-2' loaded from http://localhost:7007/.backstage/dynamic-features/remotes/plugin-2/mf-manifest.json",
]);
const debugCalls = mocks.console.debug.mock.calls.flatMap(e => e[0]);
expect(debugCalls).toEqual([
"Loading dynamic plugin 'plugin-1' from 'http://localhost:7007/.backstage/dynamic-features/remotes/plugin-1/mf-manifest.json'",
"Loading dynamic plugin 'plugin-2' from 'http://localhost:7007/.backstage/dynamic-features/remotes/plugin-2/mf-manifest.json'",
]);
expect(mocks.federation.get.mock.calls.flatMap(e => e[0])).toEqual([
{
id: '.',
name: 'plugin_2',
},
]);
});
it('should warn on unexpected Json content while fetching module feredation manifest, but still load other remotes', async () => {
server.use(
rest.get(
`${baseUrl}/.backstage/dynamic-features/remotes`,
(_, res, ctx) =>
res(
ctx.json([
{
packageName: 'plugin-1',
exposedModules: ['.'],
remoteInfo: {
name: 'plugin_1',
entry: `${baseUrl}/.backstage/dynamic-features/remotes/plugin-1/mf-manifest.json`,
},
},
{
packageName: 'plugin-2',
exposedModules: ['.'],
remoteInfo: {
name: 'plugin_2',
entry: `${baseUrl}/.backstage/dynamic-features/remotes/plugin-2/mf-manifest.json`,
},
},
]),
),
),
rest.get(
`${baseUrl}/.backstage/dynamic-features/remotes/plugin-1/mf-manifest.json`,
(_, res, ctx) => res(ctx.json('A Json String')),
),
rest.get(
`${baseUrl}/.backstage/dynamic-features/remotes/plugin-2/mf-manifest.json`,
(_, res, ctx) =>
res(
ctx.json({
name: 'plugin-2',
...manifestDummyData,
exposes: [
{
id: 'plugin-2:.',
name: '.',
path: '.',
...manifestExposedRemoteDummyData,
},
],
}),
),
),
);
mocks.federation.get.mockReturnValueOnce({
default: createFrontendPlugin({
id: 'plugin-2',
extensions: [],
}),
});
const features = await (
dynamicFrontendFeaturesLoader({
...getCommonOptions(),
}) as InternalFrontendFeatureLoader
).loader({
config: mockApis.config({
data: {
app: {
experimental: {
packages: {
include: [],
},
},
},
backend: {
baseUrl,
},
dynamicPlugins: {},
},
}),
});
const errorCalls = mocks.console.error.mock.calls.flatMap(e => e[0]);
expect(errorCalls).toEqual([
"Failed loading remote module 'plugin_1' of dynamic plugin 'plugin-1': Error: [ Federation Runtime ]: [ Federation Runtime ]: [ Federation Runtime ]: http://localhost:7007/.backstage/dynamic-features/remotes/plugin-1/mf-manifest.json is not a federation manifest",
]);
const warnCalls = mocks.console.warn.mock.calls.flatMap(e => e[0]);
expect(warnCalls).toEqual([]);
expect(features).toMatchObject([
{
$$type: '@backstage/FrontendPlugin',
id: 'plugin-2',
version: 'v1',
},
]);
const infoCalls = mocks.console.info.mock.calls.flatMap(e => e[0]);
expect(infoCalls).toEqual([
"Remote module 'plugin_2' of dynamic plugin 'plugin-2' loaded from http://localhost:7007/.backstage/dynamic-features/remotes/plugin-2/mf-manifest.json",
]);
const debugCalls = mocks.console.debug.mock.calls.flatMap(e => e[0]);
expect(debugCalls).toEqual([
"Loading dynamic plugin 'plugin-1' from 'http://localhost:7007/.backstage/dynamic-features/remotes/plugin-1/mf-manifest.json'",
"Loading dynamic plugin 'plugin-2' from 'http://localhost:7007/.backstage/dynamic-features/remotes/plugin-2/mf-manifest.json'",
]);
expect(mocks.federation.get.mock.calls.flatMap(e => e[0])).toEqual([
{
id: '.',
name: 'plugin_2',
},
]);
});
@@ -14,15 +14,19 @@
* limitations under the License.
*/
import { init, loadRemote } from '@module-federation/enhanced/runtime';
import { Manifest, Module } from '@module-federation/sdk';
import { DefaultApiClient } from './schema/openapi';
import { FrontendHostDiscovery } from '@backstage/core-app-api';
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';
/**
*
@@ -32,7 +36,11 @@ export type DynamicFrontendFeaturesLoaderOptions = {
/**
* Additional module federation arguments for the Module Federation runtime initialization.
*/
moduleFederation: Omit<Parameters<typeof init>[0], 'name' | 'remotes'>;
moduleFederation: {
shared?: UserOptions['shared'];
shareStrategy?: ShareStrategy;
plugins?: Array<FederationRuntimePlugin>;
};
};
/**
@@ -60,14 +68,16 @@ export function dynamicFrontendFeaturesLoader(
);
}
const backendBaseUrl = config.getString('backend.baseUrl');
const appPackageName =
config.getOptionalString('app.packageName') ?? 'app';
let frontendPluginManifests: {
[key: string]: string;
};
let frontendPluginRemotes: Array<Remote>;
try {
const apiClient = new DefaultApiClient({
discoveryApi: FrontendHostDiscovery.fromConfig(config),
discoveryApi: {
getBaseUrl: async rootPath => `${backendBaseUrl}/${rootPath}`,
},
fetchApi: {
fetch(input) {
return global.fetch(input);
@@ -75,14 +85,11 @@ export function dynamicFrontendFeaturesLoader(
},
});
const response = await apiClient.getManifests({});
const response = await apiClient.getRemotes({});
if (!response.ok) {
throw new Error(`${response.status} - ${response.statusText}`);
}
frontendPluginManifests = await response.json();
if (typeof frontendPluginManifests !== 'object') {
throw new Error(`Invalid Json content: should be a Json object`);
}
frontendPluginRemotes = await response.json();
} catch (err) {
error(
`Failed fetching module federation configuration of dynamic frontend plugins`,
@@ -98,12 +105,10 @@ export function dynamicFrontendFeaturesLoader(
.replaceAll('@', '')
.replaceAll('/', '__')
.replaceAll('-', '_'),
remotes: Object.entries(frontendPluginManifests).map(
([name, manifestLocation]) => ({
name: name,
entry: manifestLocation,
}),
),
remotes: frontendPluginRemotes.map(remote => ({
alias: remote.packageName,
...remote.remoteInfo,
})),
});
} catch (err) {
error(`Failed initializing module federation`, err);
@@ -112,73 +117,52 @@ export function dynamicFrontendFeaturesLoader(
const features = (
await Promise.all(
Object.entries(frontendPluginManifests).map(
async ([name, manifestLocation]) => {
// eslint-disable-next-line no-console
console.info(
`Loading dynamic plugin '${name}' from '${manifestLocation}'`,
);
let manifest: Manifest;
try {
const response = await fetch(manifestLocation);
if (!response.ok) {
throw new Error(
`${response.status} - ${response.statusText}`,
);
}
manifest = await response.json();
if (typeof manifest !== 'object') {
throw new Error(
`Invalid Json content: should be a Json object`,
);
}
} catch (err) {
error(
`Failed fetching module federation manifest from '${manifestLocation}'`,
err,
);
return undefined;
}
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(
manifest.exposes.map(async expose => {
const remote =
expose.name === '.' ? name : `${name}/${expose.name}`;
let module: Module;
try {
module = await loadRemote<Module>(remote);
} catch (err) {
error(
`Failed loading dynamic plugin remote module '${remote}'`,
err,
);
return undefined;
}
if (!module) {
// eslint-disable-next-line no-console
console.warn(
`Skipping empty dynamic plugin remote module '${remote}'.`,
);
return undefined;
}
// eslint-disable-next-line no-console
console.info(
`Dynamic plugin remote module '${remote}' loaded from ${manifestLocation}`,
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,
);
const defaultEntry = module.default;
if (!isFrontendPluginOrModule(defaultEntry)) {
// eslint-disable-next-line no-console
console.warn(
`Skipping dynamic plugin remote module '${remote}' since it doesn't export a new 'FrontendFeature' as default export.`,
);
return undefined;
}
return defaultEntry;
}),
);
return moduleFeatures;
},
),
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()
@@ -189,7 +173,7 @@ export function dynamicFrontendFeaturesLoader(
});
}
function isFrontendPluginOrModule(obj: unknown): obj is FrontendFeature {
function isLoadable(obj: unknown): obj is FrontendFeature {
if (obj !== null && typeof obj === 'object' && '$$type' in obj) {
return (
obj.$$type === '@backstage/FrontendPlugin' ||
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* 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.
@@ -22,6 +22,7 @@ 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.
@@ -40,14 +41,12 @@ export type TypedResponse<T> = Omit<Response, 'json'> & {
export interface RequestOptions {
token?: string;
}
/**
* @public
*/
export type GetManifests = {};
export type GetRemotes = {};
/**
* no description
* @public
*/
export class DefaultApiClient {
@@ -63,16 +62,16 @@ export class DefaultApiClient {
}
/**
* Get the Module Federation manifest files of dynamic frontend plugins.
* Get the Module Federation remote definitions.
*/
public async getManifests(
public async getRemotes(
// @ts-ignore
request: GetManifests,
request: GetRemotes,
options?: RequestOptions,
): Promise<TypedResponse<{ [key: string]: string }>> {
): Promise<TypedResponse<Array<Remote>>> {
const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);
const uriTemplate = `/manifests`;
const uriTemplate = `/remotes`;
const uri = parser.parse(uriTemplate).expand({});
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* 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.
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export * from './DefaultApi.client';
export * from './Api.client';
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* 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.
@@ -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';
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* 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.
@@ -18,3 +18,5 @@ 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';
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* 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.
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export const pluginId = 'core.dynamicplugins.frontendRemotes';
export const pluginId = '.backstage/dynamic-features';
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* 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.