diff --git a/packages/frontend-dynamic-feature-loader/.eslintrc.js b/packages/frontend-dynamic-feature-loader/.eslintrc.js new file mode 100644 index 0000000000..e487f765b2 --- /dev/null +++ b/packages/frontend-dynamic-feature-loader/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { + rules: { + '@backstage/no-top-level-material-ui-4-imports': 'error', + }, +}); diff --git a/packages/frontend-dynamic-feature-loader/README.md b/packages/frontend-dynamic-feature-loader/README.md new file mode 100644 index 0000000000..cd5ad44fa9 --- /dev/null +++ b/packages/frontend-dynamic-feature-loader/README.md @@ -0,0 +1,32 @@ +# @backstage/frontend-dynamic-feature-loader + +Backstage frontend feature loader to load new frontend system plugins exposed as module federation remotes. +The frontend feature loader provided in this package works hand-in-hand with the server of frontend plugin module federation remotes server which is part of backend dynamic feature service in package `@backstage/backend-dynamic-feature-service`. + +**NOTE: The [new frontend system](https://backstage.io/docs/frontend-system/) that this package is relaying upon is in alpha, and we do not yet recommend using it for production deployments** + +## Usage + +- To enable this loader, you should: + + - Enable the backend dynamic features in your backend application, as explained in `packages/backend-dynamic-feature-service/README.md#how-it-works` + - Add the frontend feature loader to the list of features when creating the frontend application: + + ```typescript + const app = createApp({ + features: [...someOtherFeatures, dynamicFrontendFeaturesLoader()], + }); + ``` + +## How to add a frontend plugin for dynamic loading + +Adding a frontend plugin (with new frontend system support, possibly in alpha support), is straightforward and consists in: + +- building the frontend plugin with the `frontend-dynamic-container` role, which enables the module federation support, and packages the plugin as a module remote +- copying the frontend package folder, with the `dist` folder generated during the build, to the dynamic plugins root folder of the Backstage installation (defined by the `dynamicPlugins.rootDirectory` configuration value, which is usually set as `dynamic-plugins-root`). + +So from a frontend plugin package folder, you would use the following command: + +```bash +yarn build --role frontend-dynamic-container && cp -R $(pwd) /dynamic-plugins-root/ +``` diff --git a/packages/frontend-dynamic-feature-loader/catalog-info.yaml b/packages/frontend-dynamic-feature-loader/catalog-info.yaml new file mode 100644 index 0000000000..3f9ef427ed --- /dev/null +++ b/packages/frontend-dynamic-feature-loader/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-frontend-dynamic-feature-loader + title: '@backstage/frontend-dynamic-feature-loader' + description: Backstage frontend feature loader to load new frontend system plugins exposed as module federation remotes. +spec: + lifecycle: experimental + type: backstage-web-library + owner: maintainers diff --git a/packages/frontend-dynamic-feature-loader/config.d.ts b/packages/frontend-dynamic-feature-loader/config.d.ts new file mode 100644 index 0000000000..c541782683 --- /dev/null +++ b/packages/frontend-dynamic-feature-loader/config.d.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Config { + /** + * @visibility frontend + */ + dynamicPlugins?: {}; +} diff --git a/packages/frontend-dynamic-feature-loader/knip-report.md b/packages/frontend-dynamic-feature-loader/knip-report.md new file mode 100644 index 0000000000..2661c35327 --- /dev/null +++ b/packages/frontend-dynamic-feature-loader/knip-report.md @@ -0,0 +1,2 @@ +# Knip report + diff --git a/packages/frontend-dynamic-feature-loader/package.json b/packages/frontend-dynamic-feature-loader/package.json new file mode 100644 index 0000000000..9d8fbbdfd2 --- /dev/null +++ b/packages/frontend-dynamic-feature-loader/package.json @@ -0,0 +1,66 @@ +{ + "name": "@backstage/frontend-dynamic-feature-loader", + "version": "0.0.1-next.0", + "backstage": { + "role": "web-library" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/frontend-dynamic-feature-loader" + }, + "license": "Apache-2.0", + "sideEffects": false, + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "config.d.ts", + "dist" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "start": "backstage-cli package start", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/config": "workspace:^", + "@backstage/core-app-api": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", + "@module-federation/enhanced": "^0.9.0", + "@module-federation/sdk": "^0.9.0", + "cross-fetch": "^4.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@module-federation/runtime": "^0.9.0", + "@testing-library/jest-dom": "^6.0.0", + "@testing-library/react": "^16.0.0", + "@types/react": "^18.0.0", + "msw": "^1.0.0", + "react": "^18.0.2", + "react-dom": "^18.0.2", + "react-router-dom": "^6.3.0" + }, + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0", + "react": "^16.13.1 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0", + "react-router-dom": "6.0.0-beta.0 || ^6.3.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + }, + "configSchema": "config.d.ts" +} diff --git a/packages/frontend-dynamic-feature-loader/report.api.md b/packages/frontend-dynamic-feature-loader/report.api.md new file mode 100644 index 0000000000..532dddfc43 --- /dev/null +++ b/packages/frontend-dynamic-feature-loader/report.api.md @@ -0,0 +1,18 @@ +## API Report File for "@backstage/frontend-dynamic-feature-loader" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CreateAppFeatureLoader } from '@backstage/frontend-defaults'; +import { init } from '@module-federation/enhanced/runtime'; + +// @public +export function dynamicFrontendFeaturesLoader( + options?: DynamicFrontendFeaturesLoaderOptions, +): CreateAppFeatureLoader; + +// @public (undocumented) +export type DynamicFrontendFeaturesLoaderOptions = { + moduleFederation: Omit[0], 'name' | 'remotes'>; +}; +``` diff --git a/packages/frontend-dynamic-feature-loader/src/index.ts b/packages/frontend-dynamic-feature-loader/src/index.ts new file mode 100644 index 0000000000..2b3c4616f5 --- /dev/null +++ b/packages/frontend-dynamic-feature-loader/src/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Core API used by Backstage frontend apps. + * + * @packageDocumentation + */ + +export * from './loader'; diff --git a/packages/frontend-dynamic-feature-loader/src/loader.test.tsx b/packages/frontend-dynamic-feature-loader/src/loader.test.tsx new file mode 100644 index 0000000000..c958688e20 --- /dev/null +++ b/packages/frontend-dynamic-feature-loader/src/loader.test.tsx @@ -0,0 +1,1024 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { mockApis, registerMswTestHooks } from '@backstage/test-utils'; +import { + DynamicFrontendFeaturesLoaderOptions, + dynamicFrontendFeaturesLoader, +} from './loader'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { RemoteEntryExports } from '@module-federation/runtime/types'; +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'; + +const baseUrl = 'http://localhost:7007'; + +describe('dynamicFrontendFeaturesLoader', () => { + const server = setupServer(); + registerMswTestHooks(server); + const mocks = { + console: { + error: jest.spyOn(console, 'error').mockImplementation(() => {}), + warn: jest.spyOn(console, 'warn').mockImplementation(() => {}), + info: jest.spyOn(console, 'info').mockImplementation(() => {}), + }, + federation: { + get: jest.fn((_: { name: string; id: string }): Module => ({})), + onLoad: jest.fn(() => {}), + }, + }; + + const getCommonOptions = (): DynamicFrontendFeaturesLoaderOptions => ({ + moduleFederation: { + // We add this module federation plugin to mock the + // effective retrieval of the remote content, since it + // normally requires a host application built with module federation support, + // and won't work by default in Jest tests. + plugins: [ + { + name: 'load-entry-mock', + errorLoadRemote: args => { + // eslint-disable-next-line no-console + console.error(args); + }, + loadEntry: async args => { + return { + get: (id: string) => async () => { + return await mocks.federation.get({ + name: args.remoteInfo.name, + id, + }); + }, + init: async () => {}, + } as RemoteEntryExports; + }, + onLoad: mocks.federation.onLoad, + }, + ], + }, + }); + + const manifestDummyData = { + metaData: { + buildInfo: {}, + remoteEntry: { + name: 'remoteEntry.js', + }, + types: {}, + publicPath: 'auto', + }, + shared: [], + }; + + const manifestExposedRemoteDummyData = { + assets: { + js: { + sync: [], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }; + + afterEach(() => { + mocks.console.error.mockReset(); + mocks.console.warn.mockReset(); + mocks.console.info.mockReset(); + mocks.federation.get.mockReset(); + }); + + 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`, + (_, res, ctx) => { + manifestsEndpointCalled = true; + return res(ctx.json({})); + }, + ), + ); + + const features = await ( + dynamicFrontendFeaturesLoader({ + ...getCommonOptions(), + }) as InternalFrontendFeatureLoader + ).loader({ + config: mockApis.config({ + data: { + app: { + experimental: { + packages: { + include: [], + }, + }, + }, + backend: { + baseUrl, + }, + }, + }), + }); + + 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([]); + 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([]); + expect(manifestsEndpointCalled).toBe(false); + }); + + it('should load a dynamic frontend plugin with the default exposed remote module', async () => { + server.use( + rest.get( + `${baseUrl}/api/core.dynamicplugins.frontendRemotes/manifests`, + (_, res, ctx) => + res( + ctx.json({ + 'test-plugin': `${baseUrl}/api/core.dynamicplugins.frontendRemotes/remotes/test-plugin/mf-manifest.json`, + }), + ), + ), + rest.get( + `${baseUrl}/api/core.dynamicplugins.frontendRemotes/remotes/test-plugin/mf-manifest.json`, + (_, res, ctx) => + res( + ctx.json({ + name: 'test_plugin', + ...manifestDummyData, + exposes: [ + { + id: 'test_plugin:.', + name: '.', + path: '.', + ...manifestExposedRemoteDummyData, + }, + ], + }), + ), + ), + ); + + 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([]); + 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([ + "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", + ]); + expect(mocks.federation.get.mock.calls.flatMap(e => e[0])).toEqual([ + { + id: '.', + name: 'test-plugin', + }, + ]); + }); + + it('should load several dynamic frontend plugins', 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({ + name: 'plugin_1', + ...manifestDummyData, + exposes: [ + { + id: 'plugin_1:.', + name: '.', + path: '.', + ...manifestExposedRemoteDummyData, + }, + ], + }), + ), + ), + 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-1', + extensions: [], + }), + }); + 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-2', + 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: 'plugin-1', + version: 'v1', + }, + { + $$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-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", + ]); + expect(mocks.federation.get.mock.calls.flatMap(e => e[0])).toEqual([ + { + id: '.', + name: 'plugin-1', + }, + { + id: '.', + name: 'plugin-2', + }, + ]); + }); + + it('should load a dynamic frontend plugin with several exposed remote modules', async () => { + server.use( + rest.get( + `${baseUrl}/api/core.dynamicplugins.frontendRemotes/manifests`, + (_, res, ctx) => + res( + ctx.json({ + 'test-plugin': `${baseUrl}/api/core.dynamicplugins.frontendRemotes/remotes/test-plugin/mf-manifest.json`, + }), + ), + ), + rest.get( + `${baseUrl}/api/core.dynamicplugins.frontendRemotes/remotes/test-plugin/mf-manifest.json`, + (_, res, ctx) => + res( + ctx.json({ + name: 'test_plugin', + ...manifestDummyData, + exposes: [ + { + id: 'test_plugin:.', + name: '.', + path: '.', + ...manifestExposedRemoteDummyData, + }, + { + id: 'test_plugin:alpha', + name: 'alpha', + path: './alpha', + ...manifestExposedRemoteDummyData, + }, + ], + }), + ), + ), + ); + + mocks.federation.get.mockReturnValueOnce({ + default: createFrontendPlugin({ + id: 'test-plugin', + extensions: [], + }), + }); + mocks.federation.get.mockReturnValueOnce({ + default: createFrontendPlugin({ + id: 'test-plugin-alpha', + extensions: [], + }), + }); + + const features = await ( + dynamicFrontendFeaturesLoader({ + ...getCommonOptions(), + }) as InternalFrontendFeatureLoader + ).loader({ + config: mockApis.config({ + data: { + app: { + experimental: { + packages: { + include: [], + }, + }, + packageName: 'app-3', + }, + 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', + }, + { + $$type: '@backstage/FrontendPlugin', + id: 'test-plugin-alpha', + version: 'v1', + }, + ]); + 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", + ]); + expect(mocks.federation.get.mock.calls.flatMap(e => e[0])).toEqual([ + { + id: '.', + name: 'test-plugin', + }, + { + id: './alpha', + name: 'test-plugin', + }, + ]); + }); + + it('should warn and recover from a 404 error fetching module feredation configuration', async () => { + server.use( + rest.get( + `${baseUrl}/api/core.dynamicplugins.frontendRemotes/manifests`, + (_, res, ctx) => res(ctx.status(404, 'NOT FOUND')), + ), + ); + + 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: 404 - NOT FOUND`, + ]); + 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 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`, + (_, res, ctx) => res(ctx.status(200)), + ), + ); + + 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: FetchError: invalid json response body at http://localhost:7007/api/core.dynamicplugins.frontendRemotes/manifests reason: Unexpected end of JSON input`, + ]); + 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 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`, + (_, 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({ + name: 'plugin_1', + ...manifestDummyData, + exposes: [ + { + id: 'plugin_1:.', + name: '.', + path: '.', + ...manifestExposedRemoteDummyData, + }, + ], + }), + ), + ), + 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(undefined); + 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-6', + 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([ + "Skipping empty dynamic plugin remote module 'plugin-1'.", + ]); + 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-1', + }, + { + id: '.', + name: 'plugin-2', + }, + ]); + }); + + it('should warn on module without default export, 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({ + name: 'plugin_1', + ...manifestDummyData, + exposes: [ + { + id: 'plugin_1:.', + name: '.', + path: '.', + ...manifestExposedRemoteDummyData, + }, + ], + }), + ), + ), + 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({ + anExport: 'anExportValue', + }); + 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-7', + 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([ + "Skipping dynamic plugin remote module 'plugin-1' since it doesn't export a new 'FrontendFeature' as default export.", + ]); + 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-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", + ]); + expect(mocks.federation.get.mock.calls.flatMap(e => e[0])).toEqual([ + { + id: '.', + name: 'plugin-1', + }, + { + id: '.', + name: 'plugin-2', + }, + ]); + }); +}); diff --git a/packages/frontend-dynamic-feature-loader/src/loader.ts b/packages/frontend-dynamic-feature-loader/src/loader.ts new file mode 100644 index 0000000000..c15dcb9f3c --- /dev/null +++ b/packages/frontend-dynamic-feature-loader/src/loader.ts @@ -0,0 +1,200 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { 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 { + FrontendFeature, + FrontendFeatureLoader, + createFrontendFeatureLoader, +} from '@backstage/frontend-plugin-api'; + +/** + * + * @public + */ +export type DynamicFrontendFeaturesLoaderOptions = { + /** + * Additional module federation arguments for the Module Federation runtime initialization. + */ + moduleFederation: Omit[0], 'name' | 'remotes'>; +}; + +/** + * A function providing a loader of frontend features exposed as module federation remotes + * from the backend dynamic features service. + * + * @public + */ +export function dynamicFrontendFeaturesLoader( + options?: DynamicFrontendFeaturesLoaderOptions, +): FrontendFeatureLoader { + return createFrontendFeatureLoader({ + async loader({ config }) { + const dynamicPLuginsConfig = config.getOptionalConfig('dynamicPlugins'); + if (!dynamicPLuginsConfig) { + return []; + } + + function error(message: string, err: unknown) { + // eslint-disable-next-line no-console + console.error( + `${message}: ${ + err instanceof Error ? err.toString() : JSON.stringify(err) + }`, + ); + } + + const appPackageName = + config.getOptionalString('app.packageName') ?? 'app'; + let frontendPluginManifests: { + [key: string]: string; + }; + try { + const apiClient = new DefaultApiClient({ + discoveryApi: FrontendHostDiscovery.fromConfig(config), + fetchApi: { + fetch(input) { + return global.fetch(input); + }, + }, + }); + + const response = await apiClient.getManifests({}); + 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`); + } + } catch (err) { + error( + `Failed fetching module federation configuration of dynamic frontend plugins`, + err, + ); + return []; + } + + try { + init({ + ...options?.moduleFederation, + name: appPackageName + .replaceAll('@', '') + .replaceAll('/', '__') + .replaceAll('-', '_'), + remotes: Object.entries(frontendPluginManifests).map( + ([name, manifestLocation]) => ({ + name: name, + entry: manifestLocation, + }), + ), + }); + } catch (err) { + error(`Failed initializing module federation`, err); + return []; + } + + 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; + } + + 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 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; + }, + ), + ) + ) + .flat() + .filter((feature): feature is FrontendFeature => feature !== undefined); + + return [...features]; + }, + }); +} + +function isFrontendPluginOrModule(obj: unknown): obj is FrontendFeature { + if (obj !== null && typeof obj === 'object' && '$$type' in obj) { + return ( + obj.$$type === '@backstage/FrontendPlugin' || + obj.$$type === '@backstage/FrontendModule' + ); + } + return false; +} 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/DefaultApi.client.ts new file mode 100644 index 0000000000..1ce952d36d --- /dev/null +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/DefaultApi.client.ts @@ -0,0 +1,87 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// ****************************************************************** +// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * +// ****************************************************************** +import { DiscoveryApi } from '../types/discovery'; +import { FetchApi } from '../types/fetch'; +import crossFetch from 'cross-fetch'; +import { pluginId } from '../pluginId'; +import * as parser from 'uri-template'; + +/** + * Wraps the Response type to convey a type on the json call. + * + * @public + */ +export type TypedResponse = Omit & { + json: () => Promise; +}; + +/** + * Options you can pass into a request for additional information. + * + * @public + */ +export interface RequestOptions { + token?: string; +} + +/** + * @public + */ +export type GetManifests = {}; + +/** + * no description + * @public + */ +export class DefaultApiClient { + private readonly discoveryApi: DiscoveryApi; + private readonly fetchApi: FetchApi; + + constructor(options: { + discoveryApi: { getBaseUrl(pluginId: string): Promise }; + fetchApi?: { fetch: typeof fetch }; + }) { + this.discoveryApi = options.discoveryApi; + this.fetchApi = options.fetchApi || { fetch: crossFetch }; + } + + /** + * Get the Module Federation manifest files of dynamic frontend plugins. + */ + public async getManifests( + // @ts-ignore + request: GetManifests, + options?: RequestOptions, + ): Promise> { + const baseUrl = await this.discoveryApi.getBaseUrl(pluginId); + + const uriTemplate = `/manifests`; + + const uri = parser.parse(uriTemplate).expand({}); + + return await this.fetchApi.fetch(`${baseUrl}${uri}`, { + headers: { + 'Content-Type': 'application/json', + ...(options?.token && { Authorization: `Bearer ${options?.token}` }), + }, + method: 'GET', + }); + } +} 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 new file mode 100644 index 0000000000..51dcca33fe --- /dev/null +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './DefaultApi.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 new file mode 100644 index 0000000000..bb399e97a0 --- /dev/null +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './apis'; +export * from './models'; 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 new file mode 100644 index 0000000000..fe5811628d --- /dev/null +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorError.model.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// ****************************************************************** +// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * +// ****************************************************************** + +/** + * @public + */ +export interface ErrorError { + name: string; + message: string; + stack?: string; + code?: string; +} 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 new file mode 100644 index 0000000000..d44dcb66d9 --- /dev/null +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorRequest.model.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// ****************************************************************** +// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * +// ****************************************************************** + +/** + * @public + */ +export interface ErrorRequest { + method: string; + url: string; +} 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 new file mode 100644 index 0000000000..91c120483d --- /dev/null +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorResponse.model.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// ****************************************************************** +// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * +// ****************************************************************** + +/** + * @public + */ +export interface ErrorResponse { + statusCode: number; +} 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 new file mode 100644 index 0000000000..5526d703e6 --- /dev/null +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ModelError.model.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// ****************************************************************** +// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * +// ****************************************************************** +import { ErrorError } from '../models/ErrorError.model'; +import { ErrorRequest } from '../models/ErrorRequest.model'; +import { ErrorResponse } from '../models/ErrorResponse.model'; + +/** + * @public + */ +export interface ModelError { + [key: string]: any; + + error: ErrorError; + request?: ErrorRequest; + response: ErrorResponse; +} 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 new file mode 100644 index 0000000000..4d19a3b2e9 --- /dev/null +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from '../models/ErrorError.model'; +export * from '../models/ErrorRequest.model'; +export * from '../models/ErrorResponse.model'; +export * from '../models/ModelError.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 new file mode 100644 index 0000000000..21cdf38c03 --- /dev/null +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/pluginId.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const pluginId = 'core.dynamicplugins.frontendRemotes'; diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/types/discovery.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/types/discovery.ts new file mode 100644 index 0000000000..a7f87d3780 --- /dev/null +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/types/discovery.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This is a copy of the DiscoveryApi, to avoid importing core-plugin-api. + */ +export type DiscoveryApi = { + getBaseUrl(pluginId: string): Promise; +}; diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/types/fetch.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/types/fetch.ts new file mode 100644 index 0000000000..3de56c028e --- /dev/null +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/types/fetch.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This is a copy of FetchApi, to avoid importing core-plugin-api. + */ +export type FetchApi = { + fetch: typeof fetch; +}; diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/index.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/index.ts new file mode 100644 index 0000000000..db98243cbf --- /dev/null +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './generated'; diff --git a/packages/frontend-dynamic-feature-loader/src/setupTests.ts b/packages/frontend-dynamic-feature-loader/src/setupTests.ts new file mode 100644 index 0000000000..91af6695ac --- /dev/null +++ b/packages/frontend-dynamic-feature-loader/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '@testing-library/jest-dom'; diff --git a/scripts/verify-local-dependencies.js b/scripts/verify-local-dependencies.js index 045abcff57..a09571b7fb 100755 --- a/scripts/verify-local-dependencies.js +++ b/scripts/verify-local-dependencies.js @@ -77,6 +77,7 @@ const roleRules = [ '@backstage/core-compat-api', '@backstage/dev-utils', '@backstage/frontend-defaults', + '@backstage/frontend-dynamic-feature-loader', '@backstage/frontend-app-api', '@backstage/frontend-test-utils', '@backstage/test-utils', diff --git a/yarn.lock b/yarn.lock index 97a44d708d..457e63dad3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3335,7 +3335,16 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.26.10, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": + version: 7.26.7 + resolution: "@babel/runtime@npm:7.26.7" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: 10/c7a661a6836b332d9d2e047cba77ba1862c1e4f78cec7146db45808182ef7636d8a7170be9797e5d8fd513180bffb9fa16f6ca1c69341891efec56113cf22bfc + languageName: node + linkType: hard + +"@babel/runtime@npm:^7.26.10": version: 7.27.0 resolution: "@babel/runtime@npm:7.27.0" dependencies: @@ -4096,7 +4105,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/core-app-api@npm:^1.16.0": +"@backstage/core-app-api@npm:^1.15.5, @backstage/core-app-api@npm:^1.16.0": version: 1.16.0 resolution: "@backstage/core-app-api@npm:1.16.0" dependencies: @@ -4168,13 +4177,12 @@ __metadata: languageName: unknown linkType: soft -"@backstage/core-compat-api@npm:^0.4.0": - version: 0.4.0 - resolution: "@backstage/core-compat-api@npm:0.4.0" +"@backstage/core-compat-api@npm:^0.3.6": + version: 0.3.6 + resolution: "@backstage/core-compat-api@npm:0.3.6" dependencies: - "@backstage/core-plugin-api": "npm:^1.10.5" - "@backstage/frontend-plugin-api": "npm:^0.10.0" - "@backstage/plugin-catalog-react": "npm:^1.16.0" + "@backstage/core-plugin-api": "npm:^1.10.4" + "@backstage/frontend-plugin-api": "npm:^0.9.5" "@backstage/version-bridge": "npm:^1.0.11" lodash: "npm:^4.17.21" peerDependencies: @@ -4185,7 +4193,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 10/6b29f14dadc5dd5c055d063cb12ff901e2ff4458eb606489fc917e029afc3bb81b5292cf701227cedf35c3df25dc85ad1c60ca20ef3b8f75aa41d40a7f5580ee + checksum: 10/befd4cb7108672ff0c3f4433e39bf5557e43a755fdf658de7b4a161977efb69b9b8a129930b141b02ea7bca5f71d70353d415d7d89f9a440afd6b4fa28c9a8cf languageName: node linkType: hard @@ -4329,12 +4337,12 @@ __metadata: languageName: node linkType: hard -"@backstage/core-components@npm:^0.17.0": - version: 0.17.0 - resolution: "@backstage/core-components@npm:0.17.0" +"@backstage/core-components@npm:^0.16.4": + version: 0.16.4 + resolution: "@backstage/core-components@npm:0.16.4" dependencies: "@backstage/config": "npm:^1.3.2" - "@backstage/core-plugin-api": "npm:^1.10.5" + "@backstage/core-plugin-api": "npm:^1.10.4" "@backstage/errors": "npm:^1.2.7" "@backstage/theme": "npm:^0.6.4" "@backstage/version-bridge": "npm:^1.0.11" @@ -4379,7 +4387,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 10/b48f7a3a6df469f26eb033f2f59da36dcf56bb52ba7298b7dba05d9132c90f8ab5b58dd247a8f9c1b652b91138358fbae482654465550c74780a32fe04bf6e7e + checksum: 10/472f4a17edc740cec15041612068320114b0128d6917d6968db8b435c3f4c2f002be939978b29efb12ce32c3d660b28e9de051ac1104c14f4eae2b6129c5ab49 languageName: node linkType: hard @@ -4461,7 +4469,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/core-plugin-api@npm:^1.10.0, @backstage/core-plugin-api@npm:^1.10.5, @backstage/core-plugin-api@npm:^1.8.2": +"@backstage/core-plugin-api@npm:^1.10.0, @backstage/core-plugin-api@npm:^1.10.4, @backstage/core-plugin-api@npm:^1.10.5, @backstage/core-plugin-api@npm:^1.8.2": version: 1.10.5 resolution: "@backstage/core-plugin-api@npm:1.10.5" dependencies: @@ -4615,16 +4623,16 @@ __metadata: languageName: unknown linkType: soft -"@backstage/frontend-app-api@npm:^0.11.0": - version: 0.11.0 - resolution: "@backstage/frontend-app-api@npm:0.11.0" +"@backstage/frontend-app-api@npm:^0.10.5": + version: 0.10.5 + resolution: "@backstage/frontend-app-api@npm:0.10.5" dependencies: "@backstage/config": "npm:^1.3.2" - "@backstage/core-app-api": "npm:^1.16.0" - "@backstage/core-plugin-api": "npm:^1.10.5" + "@backstage/core-app-api": "npm:^1.15.5" + "@backstage/core-plugin-api": "npm:^1.10.4" "@backstage/errors": "npm:^1.2.7" - "@backstage/frontend-defaults": "npm:^0.2.0" - "@backstage/frontend-plugin-api": "npm:^0.10.0" + "@backstage/frontend-defaults": "npm:^0.1.6" + "@backstage/frontend-plugin-api": "npm:^0.9.5" "@backstage/types": "npm:^1.2.1" "@backstage/version-bridge": "npm:^1.0.11" lodash: "npm:^4.17.21" @@ -4637,7 +4645,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 10/5522ddd487766ea44288d5626f269e42ad7d84902a5a257fd3962d2b2409743cf1b5de135e15915ba568bf2b12ec25c814ece077c87e1436c67e63998b1b9652 + checksum: 10/55837eeacbc3bac8b9e684c63d8784f56d5adc76d20e415f1e1a30c7fb2014b8407c7e30bdd9823a5f2c9c63ce793271b5f0997c5057359a4c5e73b7ba346afb languageName: node linkType: hard @@ -4675,15 +4683,15 @@ __metadata: languageName: unknown linkType: soft -"@backstage/frontend-defaults@npm:^0.2.0": - version: 0.2.0 - resolution: "@backstage/frontend-defaults@npm:0.2.0" +"@backstage/frontend-defaults@npm:^0.1.6": + version: 0.1.6 + resolution: "@backstage/frontend-defaults@npm:0.1.6" dependencies: "@backstage/config": "npm:^1.3.2" "@backstage/errors": "npm:^1.2.7" - "@backstage/frontend-app-api": "npm:^0.11.0" - "@backstage/frontend-plugin-api": "npm:^0.10.0" - "@backstage/plugin-app": "npm:^0.1.7" + "@backstage/frontend-app-api": "npm:^0.10.5" + "@backstage/frontend-plugin-api": "npm:^0.9.5" + "@backstage/plugin-app": "npm:^0.1.6" "@react-hookz/web": "npm:^24.0.0" peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 @@ -4693,7 +4701,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 10/65decef0c603ededc36791db771f82c35f6b8c283a65d6b81ad098175290ce8d125d22127968820d29dcbc69c4241b7e703013c5091a1c433bfaf86eaad482ea + checksum: 10/2e387815586c436c5c24a116f0294dddcf1a5bc0a84b8dd73bdb9fc797fd5aca2aeaa77b4da3908f48e4b18a356751d9241473bfbe4951edf56138e0a62994e1 languageName: node linkType: hard @@ -4727,12 +4735,43 @@ __metadata: languageName: unknown linkType: soft -"@backstage/frontend-plugin-api@npm:^0.10.0": - version: 0.10.0 - resolution: "@backstage/frontend-plugin-api@npm:0.10.0" +"@backstage/frontend-dynamic-feature-loader@workspace:packages/frontend-dynamic-feature-loader": + version: 0.0.0-use.local + resolution: "@backstage/frontend-dynamic-feature-loader@workspace:packages/frontend-dynamic-feature-loader" dependencies: - "@backstage/core-components": "npm:^0.17.0" - "@backstage/core-plugin-api": "npm:^1.10.5" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/frontend-plugin-api": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@module-federation/enhanced": "npm:^0.9.0" + "@module-federation/runtime": "npm:^0.9.0" + "@module-federation/sdk": "npm:^0.9.0" + "@testing-library/jest-dom": "npm:^6.0.0" + "@testing-library/react": "npm:^16.0.0" + "@types/react": "npm:^18.0.0" + cross-fetch: "npm:^4.0.0" + msw: "npm:^1.0.0" + react: "npm:^18.0.2" + react-dom: "npm:^18.0.2" + react-router-dom: "npm:^6.3.0" + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + peerDependenciesMeta: + "@types/react": + optional: true + languageName: unknown + linkType: soft + +"@backstage/frontend-plugin-api@npm:^0.9.5": + version: 0.9.5 + resolution: "@backstage/frontend-plugin-api@npm:0.9.5" + dependencies: + "@backstage/core-components": "npm:^0.16.4" + "@backstage/core-plugin-api": "npm:^1.10.4" "@backstage/types": "npm:^1.2.1" "@backstage/version-bridge": "npm:^1.0.11" "@material-ui/core": "npm:^4.12.4" @@ -4747,7 +4786,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 10/9f52031e65d38087da1ad8d3117fdb2c54dbde20326f32c7cfebd755af6d28d881c9e57b5f9c17f4920fb40eea90b3b5b48b4cfc95f7dd2a3359866544ad5411 + checksum: 10/91a55d7d12545aa8041b64c2443e566775c9c6cca62e6b8bd5d3ac5eb4c8f2d28a8c7bb602c1389537be4ce61d0bf93e2ddeddd4e2fa3ae691817a3bb55fabab languageName: node linkType: hard @@ -4785,15 +4824,15 @@ __metadata: languageName: unknown linkType: soft -"@backstage/frontend-test-utils@npm:^0.3.0": - version: 0.3.0 - resolution: "@backstage/frontend-test-utils@npm:0.3.0" +"@backstage/frontend-test-utils@npm:^0.2.6": + version: 0.2.6 + resolution: "@backstage/frontend-test-utils@npm:0.2.6" dependencies: "@backstage/config": "npm:^1.3.2" - "@backstage/frontend-app-api": "npm:^0.11.0" - "@backstage/frontend-plugin-api": "npm:^0.10.0" - "@backstage/plugin-app": "npm:^0.1.7" - "@backstage/test-utils": "npm:^1.7.6" + "@backstage/frontend-app-api": "npm:^0.10.5" + "@backstage/frontend-plugin-api": "npm:^0.9.5" + "@backstage/plugin-app": "npm:^0.1.6" + "@backstage/test-utils": "npm:^1.7.5" "@backstage/types": "npm:^1.2.1" "@backstage/version-bridge": "npm:^1.0.11" zod: "npm:^3.22.4" @@ -4806,7 +4845,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 10/d141510fd32dc585677e1e1d7e0962b7656fd3ba7c214ef6341da39771e6fc41f5a1ca593ccfe660017fab15c90e7f7166c501618cb4c21d076ea98f101039dc + checksum: 10/0fd0c43c85f6a28c23840ac44064d9294340e892bb6e316d66afc683b05eaadebc9415a32a3f98fb81d4e8090b9496255e10ed519bb74757e089ab1d70794174 languageName: node linkType: hard @@ -4859,7 +4898,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/integration-react@npm:^1.1.24, @backstage/integration-react@npm:^1.2.5": +"@backstage/integration-react@npm:^1.1.24, @backstage/integration-react@npm:^1.2.4": version: 1.2.5 resolution: "@backstage/integration-react@npm:1.2.5" dependencies: @@ -5090,17 +5129,16 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-app@npm:^0.1.7": - version: 0.1.7 - resolution: "@backstage/plugin-app@npm:0.1.7" +"@backstage/plugin-app@npm:^0.1.6": + version: 0.1.6 + resolution: "@backstage/plugin-app@npm:0.1.6" dependencies: - "@backstage/core-components": "npm:^0.17.0" - "@backstage/core-plugin-api": "npm:^1.10.5" - "@backstage/frontend-plugin-api": "npm:^0.10.0" - "@backstage/integration-react": "npm:^1.2.5" - "@backstage/plugin-permission-react": "npm:^0.4.32" + "@backstage/core-components": "npm:^0.16.4" + "@backstage/core-plugin-api": "npm:^1.10.4" + "@backstage/frontend-plugin-api": "npm:^0.9.5" + "@backstage/integration-react": "npm:^1.2.4" + "@backstage/plugin-permission-react": "npm:^0.4.31" "@backstage/theme": "npm:^0.6.4" - "@backstage/types": "npm:^1.2.1" "@material-ui/core": "npm:^4.9.13" "@material-ui/icons": "npm:^4.9.1" "@material-ui/lab": "npm:^4.0.0-alpha.61" @@ -5113,7 +5151,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 10/29819de2c81dbeb787e485d43b8ccb475d42fb4e071ce2eab6e0a814e35f5d6f14deb1266082326dd3b07005136a34b780a14af3aed7efae6f0fff4ae28bc9cb + checksum: 10/6ba4de2ba60b95366f71b1b301c234a3d7ff34356db006696e8cae15b8c1ba9e3f31e17acf205cc74f2faa068b55ecdc535b1c087ae3a97c90915841192efee6 languageName: node linkType: hard @@ -6275,22 +6313,22 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-catalog-react@npm:^1.14.0, @backstage/plugin-catalog-react@npm:^1.16.0, @backstage/plugin-catalog-react@npm:^1.9.3": - version: 1.16.0 - resolution: "@backstage/plugin-catalog-react@npm:1.16.0" +"@backstage/plugin-catalog-react@npm:^1.14.0, @backstage/plugin-catalog-react@npm:^1.9.3": + version: 1.15.2 + resolution: "@backstage/plugin-catalog-react@npm:1.15.2" dependencies: "@backstage/catalog-client": "npm:^1.9.1" "@backstage/catalog-model": "npm:^1.7.3" - "@backstage/core-compat-api": "npm:^0.4.0" - "@backstage/core-components": "npm:^0.17.0" - "@backstage/core-plugin-api": "npm:^1.10.5" + "@backstage/core-compat-api": "npm:^0.3.6" + "@backstage/core-components": "npm:^0.16.4" + "@backstage/core-plugin-api": "npm:^1.10.4" "@backstage/errors": "npm:^1.2.7" - "@backstage/frontend-plugin-api": "npm:^0.10.0" - "@backstage/frontend-test-utils": "npm:^0.3.0" - "@backstage/integration-react": "npm:^1.2.5" + "@backstage/frontend-plugin-api": "npm:^0.9.5" + "@backstage/frontend-test-utils": "npm:^0.2.6" + "@backstage/integration-react": "npm:^1.2.4" "@backstage/plugin-catalog-common": "npm:^1.1.3" "@backstage/plugin-permission-common": "npm:^0.8.4" - "@backstage/plugin-permission-react": "npm:^0.4.32" + "@backstage/plugin-permission-react": "npm:^0.4.31" "@backstage/types": "npm:^1.2.1" "@backstage/version-bridge": "npm:^1.0.11" "@material-ui/core": "npm:^4.12.2" @@ -6312,7 +6350,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 10/0149b99ede5d563cd81dfa1d657cf332af2cbb3e6ff2e88244c4b3c59e2109264c62e3a6903ee471596a3c709294b81fe2da83513d79690704fb37f6cf67d305 + checksum: 10/379f934cc871a101714675bbe6aa51a4b0a831731dfac807004b74c804b107f2def033b53bb56c10c7bda1b16d07a014c00b374fccad7677e3f9b29be51fc354 languageName: node linkType: hard @@ -7361,7 +7399,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-permission-react@npm:^0.4.32": +"@backstage/plugin-permission-react@npm:^0.4.31, @backstage/plugin-permission-react@npm:^0.4.32": version: 0.4.32 resolution: "@backstage/plugin-permission-react@npm:0.4.32" dependencies: @@ -8784,7 +8822,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/test-utils@npm:^1.7.6": +"@backstage/test-utils@npm:^1.7.5": version: 1.7.6 resolution: "@backstage/test-utils@npm:1.7.6" dependencies: @@ -12112,6 +12150,13 @@ __metadata: languageName: node linkType: hard +"@module-federation/error-codes@npm:0.9.1": + version: 0.9.1 + resolution: "@module-federation/error-codes@npm:0.9.1" + checksum: 10/545aecc606a506ee47f061835e0eaa41b8d1b02f6bf71b36ec9ae85a1b0370af1f7b7cf92a8f52c3c4b35da858653244316de5ab06bea5dac5b92995467631cc + languageName: node + linkType: hard + "@module-federation/inject-external-runtime-core-plugin@npm:0.9.0": version: 0.9.0 resolution: "@module-federation/inject-external-runtime-core-plugin@npm:0.9.0" @@ -12189,6 +12234,16 @@ __metadata: languageName: node linkType: hard +"@module-federation/runtime-core@npm:0.9.1": + version: 0.9.1 + resolution: "@module-federation/runtime-core@npm:0.9.1" + dependencies: + "@module-federation/error-codes": "npm:0.9.1" + "@module-federation/sdk": "npm:0.9.1" + checksum: 10/6f9edbe23013395d7896fc2a24cb4055bc78df5a335f090e079df951835c1cf91c567228f19879eee3fddb0b34128abd0b50feaca1cf3fb2828c7b9bacc22169 + languageName: node + linkType: hard + "@module-federation/runtime-tools@npm:0.11.1": version: 0.11.1 resolution: "@module-federation/runtime-tools@npm:0.11.1" @@ -12231,6 +12286,17 @@ __metadata: languageName: node linkType: hard +"@module-federation/runtime@npm:^0.9.0": + version: 0.9.1 + resolution: "@module-federation/runtime@npm:0.9.1" + dependencies: + "@module-federation/error-codes": "npm:0.9.1" + "@module-federation/runtime-core": "npm:0.9.1" + "@module-federation/sdk": "npm:0.9.1" + checksum: 10/71eb1c3e81b307ebfe06c43ce70bfa56b217e9dfbed27f0b4235d3d0d05cc0fe2eb1dc57fffb3260ed9e0239257ef117ae13924c642b5ff97d9c65bdf48206fe + languageName: node + linkType: hard + "@module-federation/sdk@npm:0.11.1": version: 0.11.1 resolution: "@module-federation/sdk@npm:0.11.1" @@ -12247,6 +12313,13 @@ __metadata: languageName: node linkType: hard +"@module-federation/sdk@npm:0.9.1, @module-federation/sdk@npm:^0.9.0": + version: 0.9.1 + resolution: "@module-federation/sdk@npm:0.9.1" + checksum: 10/ea0320feff328a05405e65503d1df28e46a9ad17ef99b77f1428db5c89efbadd2a76ec82d99a54ac1d21286cee6d9ddbba881a9c46748dfe8abdb11e9afef7da + languageName: node + linkType: hard + "@module-federation/third-party-dts-extractor@npm:0.9.0": version: 0.9.0 resolution: "@module-federation/third-party-dts-extractor@npm:0.9.0" @@ -20485,7 +20558,16 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=12, @types/node@npm:>=12.0.0, @types/node@npm:>=13.7.0, @types/node@npm:>=18.0.0, @types/node@npm:^22.0.0": +"@types/node@npm:*, @types/node@npm:>=13.7.0, @types/node@npm:^22.0.0": + version: 22.13.9 + resolution: "@types/node@npm:22.13.9" + dependencies: + undici-types: "npm:~6.20.0" + checksum: 10/23560df3ee99c907179c688754486b969a72144f2e2bdefe974d320dddc5ca8f93365842966ecbd5c5bba34e919fc1a5a6627712beb8e7f71d71347dcf414a35 + languageName: node + linkType: hard + +"@types/node@npm:>=12, @types/node@npm:>=12.0.0, @types/node@npm:>=18.0.0": version: 22.13.10 resolution: "@types/node@npm:22.13.10" dependencies: @@ -30760,7 +30842,25 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6": +"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6": + version: 1.2.6 + resolution: "get-intrinsic@npm:1.2.6" + dependencies: + call-bind-apply-helpers: "npm:^1.0.1" + dunder-proto: "npm:^1.0.0" + es-define-property: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + es-object-atoms: "npm:^1.0.0" + function-bind: "npm:^1.1.2" + gopd: "npm:^1.2.0" + has-symbols: "npm:^1.1.0" + hasown: "npm:^2.0.2" + math-intrinsics: "npm:^1.0.0" + checksum: 10/a1ffae6d7893a6fa0f4d1472adbc85095edd6b3b0943ead97c3738539cecb19d422ff4d48009eed8c3c27ad678c2b1e38a83b1a1e96b691d13ed8ecefca1068d + languageName: node + linkType: hard + +"get-intrinsic@npm:^1.2.1": version: 1.3.0 resolution: "get-intrinsic@npm:1.3.0" dependencies: @@ -31824,7 +31924,7 @@ __metadata: languageName: node linkType: hard -"html-entities@npm:^2.1.0, html-entities@npm:^2.5.2": +"html-entities@npm:^2.1.0, html-entities@npm:^2.4.0, html-entities@npm:^2.5.2": version: 2.5.2 resolution: "html-entities@npm:2.5.2" checksum: 10/4ec12ebdf2d5ba8192c68e1aef3c1e4a4f36b29246a0a88464fe278a54517d0196d3489af46a3145c7ecacb4fc5fd50497be19eb713b810acab3f0efcf36fdc2 @@ -32025,7 +32125,7 @@ __metadata: languageName: node linkType: hard -"http-proxy-middleware@npm:^2.0.0, http-proxy-middleware@npm:^2.0.6, http-proxy-middleware@npm:^2.0.7": +"http-proxy-middleware@npm:^2.0.0, http-proxy-middleware@npm:^2.0.3, http-proxy-middleware@npm:^2.0.6, http-proxy-middleware@npm:^2.0.7": version: 2.0.7 resolution: "http-proxy-middleware@npm:2.0.7" dependencies: @@ -36289,7 +36389,7 @@ __metadata: languageName: node linkType: hard -"math-intrinsics@npm:^1.1.0": +"math-intrinsics@npm:^1.0.0, math-intrinsics@npm:^1.1.0": version: 1.1.0 resolution: "math-intrinsics@npm:1.1.0" checksum: 10/11df2eda46d092a6035479632e1ec865b8134bdfc4bd9e571a656f4191525404f13a283a515938c3a8de934dbfd9c09674d9da9fa831e6eb7e22b50b197d2edd @@ -47669,7 +47769,7 @@ __metadata: languageName: node linkType: hard -"webpack-dev-server@npm:5.2.0, webpack-dev-server@npm:^5.0.0": +"webpack-dev-server@npm:5.2.0": version: 5.2.0 resolution: "webpack-dev-server@npm:5.2.0" dependencies: @@ -47713,6 +47813,51 @@ __metadata: languageName: node linkType: hard +"webpack-dev-server@npm:^5.0.0": + version: 5.1.0 + resolution: "webpack-dev-server@npm:5.1.0" + dependencies: + "@types/bonjour": "npm:^3.5.13" + "@types/connect-history-api-fallback": "npm:^1.5.4" + "@types/express": "npm:^4.17.21" + "@types/serve-index": "npm:^1.9.4" + "@types/serve-static": "npm:^1.15.5" + "@types/sockjs": "npm:^0.3.36" + "@types/ws": "npm:^8.5.10" + ansi-html-community: "npm:^0.0.8" + bonjour-service: "npm:^1.2.1" + chokidar: "npm:^3.6.0" + colorette: "npm:^2.0.10" + compression: "npm:^1.7.4" + connect-history-api-fallback: "npm:^2.0.0" + express: "npm:^4.19.2" + graceful-fs: "npm:^4.2.6" + html-entities: "npm:^2.4.0" + http-proxy-middleware: "npm:^2.0.3" + ipaddr.js: "npm:^2.1.0" + launch-editor: "npm:^2.6.1" + open: "npm:^10.0.3" + p-retry: "npm:^6.2.0" + schema-utils: "npm:^4.2.0" + selfsigned: "npm:^2.4.1" + serve-index: "npm:^1.9.1" + sockjs: "npm:^0.3.24" + spdy: "npm:^4.0.2" + webpack-dev-middleware: "npm:^7.4.2" + ws: "npm:^8.18.0" + peerDependencies: + webpack: ^5.0.0 + peerDependenciesMeta: + webpack: + optional: true + webpack-cli: + optional: true + bin: + webpack-dev-server: bin/webpack-dev-server.js + checksum: 10/f23255681cc5e2c2709b23ca7b2185aeed83b1c9912657d4512eda8685625a46d7a103a92446494a55fe2afdfab936f9bd4f037d20b52f7fdfff303e7e7199c7 + languageName: node + linkType: hard + "webpack-hot-middleware@npm:^2.25.1": version: 2.26.1 resolution: "webpack-hot-middleware@npm:2.26.1"