From 4335b04da0394e2eb126146392ed527c301580a1 Mon Sep 17 00:00:00 2001 From: David Festal Date: Tue, 8 Apr 2025 21:49:41 +0200 Subject: [PATCH] Update the frontend dynamic feature loader according to changes in the backend remote asset server. Signed-off-by: David Festal --- .../report.api.md | 14 +- .../src/loader.test.tsx | 750 +++++++++++------- .../src/loader.ts | 156 ++-- .../{DefaultApi.client.ts => Api.client.ts} | 17 +- .../schema/openapi/generated/apis/index.ts | 4 +- .../src/schema/openapi/generated/index.ts | 2 +- .../generated/models/ErrorError.model.ts | 2 +- .../generated/models/ErrorRequest.model.ts | 2 +- .../generated/models/ErrorResponse.model.ts | 2 +- .../generated/models/ModelError.model.ts | 2 +- .../openapi/generated/models/Remote.model.ts | 36 + .../generated/models/RemoteInfo.model.ts | 60 ++ .../schema/openapi/generated/models/index.ts | 4 +- .../src/schema/openapi/generated/pluginId.ts | 4 +- .../src/schema/openapi/index.ts | 2 +- 15 files changed, 639 insertions(+), 418 deletions(-) rename packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/{DefaultApi.client.ts => Api.client.ts} (87%) create mode 100644 packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/Remote.model.ts create mode 100644 packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/RemoteInfo.model.ts diff --git a/packages/frontend-dynamic-feature-loader/report.api.md b/packages/frontend-dynamic-feature-loader/report.api.md index 532dddfc43..3188551bad 100644 --- a/packages/frontend-dynamic-feature-loader/report.api.md +++ b/packages/frontend-dynamic-feature-loader/report.api.md @@ -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[0], 'name' | 'remotes'>; + moduleFederation: { + shared?: UserOptions['shared']; + shareStrategy?: ShareStrategy; + plugins?: Array; + }; }; ``` diff --git a/packages/frontend-dynamic-feature-loader/src/loader.test.tsx b/packages/frontend-dynamic-feature-loader/src/loader.test.tsx index c958688e20..87ddc7770f 100644 --- a/packages/frontend-dynamic-feature-loader/src/loader.test.tsx +++ b/packages/frontend-dynamic-feature-loader/src/loader.test.tsx @@ -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', }, ]); }); diff --git a/packages/frontend-dynamic-feature-loader/src/loader.ts b/packages/frontend-dynamic-feature-loader/src/loader.ts index c15dcb9f3c..9e2585d1fa 100644 --- a/packages/frontend-dynamic-feature-loader/src/loader.ts +++ b/packages/frontend-dynamic-feature-loader/src/loader.ts @@ -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[0], 'name' | 'remotes'>; + moduleFederation: { + shared?: UserOptions['shared']; + shareStrategy?: ShareStrategy; + plugins?: Array; + }; }; /** @@ -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; 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(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(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' || diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/DefaultApi.client.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/Api.client.ts similarity index 87% rename from packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/DefaultApi.client.ts rename to packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/Api.client.ts index 1ce952d36d..8623dc3c33 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/DefaultApi.client.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/Api.client.ts @@ -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 = Omit & { 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> { + ): Promise>> { const baseUrl = await this.discoveryApi.getBaseUrl(pluginId); - const uriTemplate = `/manifests`; + const uriTemplate = `/remotes`; const uri = parser.parse(uriTemplate).expand({}); diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/index.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/index.ts index 51dcca33fe..fc7c83b736 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/index.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/index.ts @@ -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'; diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/index.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/index.ts index bb399e97a0..dc3055033d 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/index.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/index.ts @@ -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. diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorError.model.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorError.model.ts index fe5811628d..e0265e95d7 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorError.model.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorError.model.ts @@ -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. diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorRequest.model.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorRequest.model.ts index d44dcb66d9..3eb5e15740 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorRequest.model.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorRequest.model.ts @@ -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. diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorResponse.model.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorResponse.model.ts index 91c120483d..edbcc32df7 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorResponse.model.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorResponse.model.ts @@ -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. diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ModelError.model.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ModelError.model.ts index 5526d703e6..958fde7d0b 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ModelError.model.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ModelError.model.ts @@ -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. diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/Remote.model.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/Remote.model.ts new file mode 100644 index 0000000000..7440110ad8 --- /dev/null +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/Remote.model.ts @@ -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; +} diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/RemoteInfo.model.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/RemoteInfo.model.ts new file mode 100644 index 0000000000..72edba90d4 --- /dev/null +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/RemoteInfo.model.ts @@ -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'; diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/index.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/index.ts index 4d19a3b2e9..0c54a6585b 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/index.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/index.ts @@ -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'; diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/pluginId.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/pluginId.ts index 21cdf38c03..eaea330f99 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/pluginId.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/pluginId.ts @@ -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'; diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/index.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/index.ts index db98243cbf..196aad553a 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/index.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/index.ts @@ -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.