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:
@@ -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' ||
|
||||
|
||||
+8
-9
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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.
|
||||
|
||||
+36
@@ -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>;
|
||||
}
|
||||
+60
@@ -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';
|
||||
+3
-1
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user