diff --git a/packages/backend-dynamic-feature-service/package.json b/packages/backend-dynamic-feature-service/package.json index 90964cc004..1e62f8d296 100644 --- a/packages/backend-dynamic-feature-service/package.json +++ b/packages/backend-dynamic-feature-service/package.json @@ -38,6 +38,9 @@ "scripts": { "build": "backstage-cli package build", "clean": "backstage-cli package clean", + "diff": "backstage-repo-tools package schema openapi diff", + "fuzz": "backstage-repo-tools package schema openapi fuzz --exclude-checks response_schema_conformance", + "generate": "backstage-repo-tools package schema openapi generate --server --client-package packages/frontend-dynamic-feature-loader", "lint": "backstage-cli package lint", "prepack": "backstage-cli package prepack", "postpack": "backstage-cli package postpack", @@ -46,6 +49,7 @@ }, "dependencies": { "@backstage/backend-defaults": "workspace:^", + "@backstage/backend-openapi-utils": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/cli-common": "workspace:^", "@backstage/cli-node": "workspace:^", @@ -64,9 +68,11 @@ "@backstage/plugin-search-common": "workspace:^", "@backstage/types": "workspace:^", "@manypkg/get-packages": "^1.1.3", + "@module-federation/sdk": "^0.9.0", "@types/express": "^4.17.6", "chokidar": "^3.5.3", "express": "^4.17.1", + "express-promise-router": "^4.1.0", "fs-extra": "^11.2.0", "lodash": "^4.17.21", "winston": "^3.2.1" @@ -76,7 +82,9 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-app-backend": "workspace:^", + "@backstage/repo-tools": "workspace:^", "triple-beam": "^1.4.1", "wait-for-expect": "^3.0.2" - } + }, + "configSchema": "config.d.ts" } diff --git a/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic/dist/index.cjs.js b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic/dist/index.cjs.js index 77c2e32bef..5ce6f4e48f 100644 --- a/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic/dist/index.cjs.js +++ b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic/dist/index.cjs.js @@ -31,27 +31,8 @@ const testPlugin = backendPluginApi.createBackendPlugin({ metadata, }) { logger.info("This secret value should be hidden by the dynamic-plugin-aware logger: AVerySecretValue"); - const externalBaseUrl = await discovery.getExternalBaseUrl(metadata.getId()); - const router = express.Router(); - const frontendPluginsIndexPath = privateDep.frontendPluginsIndexPath; - const frontendPluginManifests = Object.fromEntries(dynamicPlugins.frontendPlugins().map(fp => { - const pluginScannedPackage = dynamicPlugins.getScannedPackage(fp); - const pkgDistLocation = path.resolve( - url.fileURLToPath(pluginScannedPackage.location), - 'dist', - ); - router.use(`/${frontendPluginsIndexPath}/${fp.name}`, express.static(pkgDistLocation)) - return [fp.name, `${externalBaseUrl}/${frontendPluginsIndexPath}/${fp.name}/mf-manifest.json`] - })); - router.get(`/${frontendPluginsIndexPath}`, (req, res) => { - res.status(200).json(frontendPluginManifests); - }); - http.use(router); - http.addAuthPolicy({ - path: `/`, - allow: 'unauthenticated', - }); - + const messageFromPrivateDep = privateDep.message; + logger.info(messageFromPrivateDep); } }); } diff --git a/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic/node_modules/private-dep-with-frontend-plugin-index-path/index.js b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic/node_modules/private-dep-with-frontend-plugin-index-path/index.js index e0fb6a55de..279a8baa6c 100644 --- a/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic/node_modules/private-dep-with-frontend-plugin-index-path/index.js +++ b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic/node_modules/private-dep-with-frontend-plugin-index-path/index.js @@ -2,6 +2,6 @@ Object.defineProperty(exports, '__esModule', { value: true }); -const frontendPluginsIndexPath = 'frontend-plugins'; +const message = 'This message comes from a plugin private dependency'; -exports.frontendPluginsIndexPath = frontendPluginsIndexPath; +exports.message = message; diff --git a/packages/backend-dynamic-feature-service/src/features/features.test.ts b/packages/backend-dynamic-feature-service/src/features/features.test.ts index fb6aa5e48a..d1ef658d47 100644 --- a/packages/backend-dynamic-feature-service/src/features/features.test.ts +++ b/packages/backend-dynamic-feature-service/src/features/features.test.ts @@ -389,15 +389,15 @@ Require stack: }); const list = await fetch( - `http://localhost:${server.port()}/api/test/frontend-plugins`, + `http://localhost:${server.port()}/api/core.dynamicplugins.frontendRemotes/manifests`, ); expect(list.ok).toBe(true); expect(await list.json()).toEqual({ - 'plugin-test-dynamic': `http://localhost:${server.port()}/api/test/frontend-plugins/plugin-test-dynamic/mf-manifest.json`, + 'plugin-test-dynamic': `http://localhost:${server.port()}/api/core.dynamicplugins.frontendRemotes/remotes/plugin-test-dynamic/mf-manifest.json`, }); const manifest = await fetch( - `http://localhost:${server.port()}/api/test/frontend-plugins/plugin-test-dynamic/mf-manifest.json`, + `http://localhost:${server.port()}/api/core.dynamicplugins.frontendRemotes/remotes/plugin-test-dynamic/mf-manifest.json`, ); expect(manifest.ok).toBe(true); expect(await manifest.json()).toMatchObject({ diff --git a/packages/backend-dynamic-feature-service/src/features/features.ts b/packages/backend-dynamic-feature-service/src/features/features.ts index 94711816d7..e300af36c4 100644 --- a/packages/backend-dynamic-feature-service/src/features/features.ts +++ b/packages/backend-dynamic-feature-service/src/features/features.ts @@ -32,6 +32,7 @@ import { dynamicPluginsRootLoggerServiceFactory, dynamicPluginsSchemasServiceFactory, } from '../schemas'; +import frontendRemotesServerPlugin from '../server'; /** * @public @@ -64,6 +65,7 @@ const dynamicPluginsFeatureLoaderWithOptions = ( yield* [ dynamicPluginsRootLoggerServiceFactory(rootLoggerOptions), dynamicPluginsFrontendSchemas, + frontendRemotesServerPlugin, dynamicPluginsFeatureDiscoveryLoader, ]; } diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi.yaml b/packages/backend-dynamic-feature-service/src/schema/openapi.yaml new file mode 100644 index 0000000000..c546a57ea4 --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/schema/openapi.yaml @@ -0,0 +1,90 @@ +openapi: 3.0.3 +info: + title: core.dynamicplugins.frontendRemotes + version: '1' + description: The Backstage backend plugin that serves the frontend plugins module federation manifests and assets + license: + name: Apache-2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + contact: {} +servers: + - url: / +components: + examples: {} + headers: {} + parameters: {} + requestBodies: {} + responses: + ErrorResponse: + description: An error response from the backend. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + schemas: + Error: + type: object + properties: + error: + type: object + properties: + name: + type: string + message: + type: string + stack: + type: string + code: + type: string + required: + - name + - message + request: + type: object + properties: + method: + type: string + url: + type: string + required: + - method + - url + response: + type: object + properties: + statusCode: + type: number + required: + - statusCode + required: + - error + - response + additionalProperties: {} + securitySchemes: + JWT: + type: http + scheme: bearer + bearerFormat: JWT +paths: + /manifests: + get: + operationId: GetManifests + description: Get the Module Federation manifest files of dynamic frontend plugins. + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + properties: {} + additionalProperties: + type: string + '400': + $ref: '#/components/responses/ErrorResponse' + default: + $ref: '#/components/responses/ErrorResponse' + security: + - {} + - JWT: [] + parameters: [] diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/apis/DefaultApi.server.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/apis/DefaultApi.server.ts new file mode 100644 index 0000000000..c2ca54a21d --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/apis/DefaultApi.server.ts @@ -0,0 +1,34 @@ +/* + * 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 type GetManifests = { + response: { [key: string]: string } | Error | Error; +}; + +/** + * no description + */ + +export type EndpointMap = { + '#get|/manifests': GetManifests; +}; diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/apis/index.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/apis/index.ts new file mode 100644 index 0000000000..79855a6fc8 --- /dev/null +++ b/packages/backend-dynamic-feature-service/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.server'; diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/index.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/index.ts new file mode 100644 index 0000000000..69c39313c6 --- /dev/null +++ b/packages/backend-dynamic-feature-service/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 './router'; diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorError.model.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorError.model.ts new file mode 100644 index 0000000000..fe5811628d --- /dev/null +++ b/packages/backend-dynamic-feature-service/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/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorRequest.model.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorRequest.model.ts new file mode 100644 index 0000000000..d44dcb66d9 --- /dev/null +++ b/packages/backend-dynamic-feature-service/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/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorResponse.model.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorResponse.model.ts new file mode 100644 index 0000000000..91c120483d --- /dev/null +++ b/packages/backend-dynamic-feature-service/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/backend-dynamic-feature-service/src/schema/openapi/generated/models/ModelError.model.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ModelError.model.ts new file mode 100644 index 0000000000..5526d703e6 --- /dev/null +++ b/packages/backend-dynamic-feature-service/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/backend-dynamic-feature-service/src/schema/openapi/generated/models/index.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/index.ts new file mode 100644 index 0000000000..4d19a3b2e9 --- /dev/null +++ b/packages/backend-dynamic-feature-service/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/backend-dynamic-feature-service/src/schema/openapi/generated/router.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/router.ts new file mode 100644 index 0000000000..af61500fea --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/router.ts @@ -0,0 +1,161 @@ +/* + * 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 { createValidatedOpenApiRouterFromGeneratedEndpointMap } from '@backstage/backend-openapi-utils'; +import { EndpointMap } from './'; + +export const spec = { + openapi: '3.0.3', + info: { + title: 'core.dynamicplugins.frontendRemotes', + version: '1', + description: + 'The Backstage backend plugin that serves the frontend plugins module federation manifests and assets', + license: { + name: 'Apache-2.0', + url: 'http://www.apache.org/licenses/LICENSE-2.0.html', + }, + contact: {}, + }, + servers: [ + { + url: '/', + }, + ], + components: { + examples: {}, + headers: {}, + parameters: {}, + requestBodies: {}, + responses: { + ErrorResponse: { + description: 'An error response from the backend.', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error', + }, + }, + }, + }, + }, + schemas: { + Error: { + type: 'object', + properties: { + error: { + type: 'object', + properties: { + name: { + type: 'string', + }, + message: { + type: 'string', + }, + stack: { + type: 'string', + }, + code: { + type: 'string', + }, + }, + required: ['name', 'message'], + }, + request: { + type: 'object', + properties: { + method: { + type: 'string', + }, + url: { + type: 'string', + }, + }, + required: ['method', 'url'], + }, + response: { + type: 'object', + properties: { + statusCode: { + type: 'number', + }, + }, + required: ['statusCode'], + }, + }, + required: ['error', 'response'], + additionalProperties: {}, + }, + }, + securitySchemes: { + JWT: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, + }, + }, + paths: { + '/manifests': { + get: { + operationId: 'GetManifests', + description: + 'Get the Module Federation manifest files of dynamic frontend plugins.', + responses: { + '200': { + description: '', + content: { + 'application/json': { + schema: { + type: 'object', + properties: {}, + additionalProperties: { + type: 'string', + }, + }, + }, + }, + }, + '400': { + $ref: '#/components/responses/ErrorResponse', + }, + default: { + $ref: '#/components/responses/ErrorResponse', + }, + }, + security: [ + {}, + { + JWT: [], + }, + ], + parameters: [], + }, + }, + }, +} as const; +export const createOpenApiRouter = async ( + options?: Parameters< + typeof createValidatedOpenApiRouterFromGeneratedEndpointMap + >['1'], +) => + createValidatedOpenApiRouterFromGeneratedEndpointMap( + spec, + options, + ); diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/index.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/index.ts new file mode 100644 index 0000000000..db98243cbf --- /dev/null +++ b/packages/backend-dynamic-feature-service/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/backend-dynamic-feature-service/src/server/index.ts b/packages/backend-dynamic-feature-service/src/server/index.ts new file mode 100644 index 0000000000..ee39fcb84c --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/server/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { frontendRemotesServerPlugin as default } from './plugin'; diff --git a/packages/backend-dynamic-feature-service/src/server/plugin.ts b/packages/backend-dynamic-feature-service/src/server/plugin.ts new file mode 100644 index 0000000000..1e021444ea --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/server/plugin.ts @@ -0,0 +1,57 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { createRouter } from './router'; +import { dynamicPluginsServiceRef } from '@backstage/backend-dynamic-feature-service'; + +/** + * frontendRemotesServerPlugin backend plugin + * + * @internal + */ +export const frontendRemotesServerPlugin = createBackendPlugin({ + pluginId: 'core.dynamicplugins.frontendRemotes', + register(env) { + env.registerInit({ + deps: { + logger: coreServices.logger, + auth: coreServices.auth, + httpAuth: coreServices.httpAuth, + httpRouter: coreServices.httpRouter, + discovery: coreServices.discovery, + metadata: coreServices.pluginMetadata, + dynamicPlugins: dynamicPluginsServiceRef, + }, + async init({ logger, httpRouter, discovery, metadata, dynamicPlugins }) { + httpRouter.use( + await createRouter({ + logger, + discovery, + metadata, + dynamicPlugins, + }), + ); + httpRouter.addAuthPolicy({ + path: `/`, + allow: 'unauthenticated', + }); + }, + }); + }, +}); diff --git a/packages/backend-dynamic-feature-service/src/server/router.ts b/packages/backend-dynamic-feature-service/src/server/router.ts new file mode 100644 index 0000000000..5b9e8a0297 --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/server/router.ts @@ -0,0 +1,100 @@ +/* + * 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 { + DiscoveryService, + LoggerService, + PluginMetadataService, +} from '@backstage/backend-plugin-api'; +import express from 'express'; +import { createOpenApiRouter } from '../schema/openapi'; +import { DynamicPluginProvider } from '@backstage/backend-dynamic-feature-service'; +import { ManifestFileName } from '@module-federation/sdk'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as url from 'url'; + +export async function createRouter({ + logger, + discovery, + metadata, + dynamicPlugins, +}: { + logger: LoggerService; + discovery: DiscoveryService; + metadata: PluginMetadataService; + dynamicPlugins: DynamicPluginProvider; +}): Promise { + const typedRouter = await createOpenApiRouter(); + + const externalBaseUrl = await discovery.getExternalBaseUrl(metadata.getId()); + + const frontendPluginManifests: { + [key: string]: string; + } = {}; + + for (const plugin of dynamicPlugins.frontendPlugins()) { + const pluginScannedPackage = dynamicPlugins.getScannedPackage(plugin); + const pkgDistLocation = path.resolve( + url.fileURLToPath(pluginScannedPackage.location), + 'dist', + ); + + const pkgManifestLocation = path.resolve(pkgDistLocation, ManifestFileName); + if (!fs.existsSync(pkgManifestLocation)) { + logger.warn( + `Could not find '${pkgManifestLocation}' for frontend plugin ${plugin.name}@${plugin.version}`, + ); + continue; + } + + let moduleName: string | undefined; + try { + const pkgManifest = JSON.parse( + fs.readFileSync(pkgManifestLocation).toString(), + ); + moduleName = pkgManifest.name; + if (!moduleName) { + logger.error( + `Dynamic frontend plugin module name not found in manifest for plugin ${plugin.name}@${plugin.version}`, + ); + continue; + } + } catch (error) { + logger.error( + `Dynamic frontend plugin manifest could not be loaded for plugin ${plugin.name}@${plugin.version}`, + ); + continue; + } + + const remoteAssetsPrefix = `/remotes/${plugin.name}`; + typedRouter.use(remoteAssetsPrefix, express.static(pkgDistLocation)); + + logger.info( + `Exposed dynamic frontend plugin '${plugin.name}' from '${pluginScannedPackage.location}' `, + ); + + frontendPluginManifests[ + plugin.name + ] = `${externalBaseUrl}/remotes/${plugin.name}/${ManifestFileName}`; + } + + typedRouter.get('/manifests', (_, res) => { + res.status(200).json(frontendPluginManifests); + }); + + return typedRouter; +} diff --git a/yarn.lock b/yarn.lock index 457e63dad3..c30b98094c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3335,16 +3335,7 @@ __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.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": +"@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": version: 7.27.0 resolution: "@babel/runtime@npm:7.27.0" dependencies: @@ -3665,6 +3656,7 @@ __metadata: dependencies: "@backstage/backend-app-api": "workspace:^" "@backstage/backend-defaults": "workspace:^" + "@backstage/backend-openapi-utils": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" @@ -3684,11 +3676,14 @@ __metadata: "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/plugin-search-backend-node": "workspace:^" "@backstage/plugin-search-common": "workspace:^" + "@backstage/repo-tools": "workspace:^" "@backstage/types": "workspace:^" "@manypkg/get-packages": "npm:^1.1.3" + "@module-federation/sdk": "npm:^0.9.0" "@types/express": "npm:^4.17.6" chokidar: "npm:^3.5.3" express: "npm:^4.17.1" + express-promise-router: "npm:^4.1.0" fs-extra: "npm:^11.2.0" lodash: "npm:^4.17.21" triple-beam: "npm:^1.4.1" @@ -20558,16 +20553,7 @@ __metadata: languageName: node linkType: hard -"@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": +"@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": version: 22.13.10 resolution: "@types/node@npm:22.13.10" dependencies: @@ -30842,25 +30828,7 @@ __metadata: languageName: node linkType: hard -"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": +"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": version: 1.3.0 resolution: "get-intrinsic@npm:1.3.0" dependencies: @@ -31924,7 +31892,7 @@ __metadata: languageName: node linkType: hard -"html-entities@npm:^2.1.0, html-entities@npm:^2.4.0, html-entities@npm:^2.5.2": +"html-entities@npm:^2.1.0, html-entities@npm:^2.5.2": version: 2.5.2 resolution: "html-entities@npm:2.5.2" checksum: 10/4ec12ebdf2d5ba8192c68e1aef3c1e4a4f36b29246a0a88464fe278a54517d0196d3489af46a3145c7ecacb4fc5fd50497be19eb713b810acab3f0efcf36fdc2 @@ -32125,7 +32093,7 @@ __metadata: languageName: node linkType: hard -"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": +"http-proxy-middleware@npm:^2.0.0, 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: @@ -36389,7 +36357,7 @@ __metadata: languageName: node linkType: hard -"math-intrinsics@npm:^1.0.0, math-intrinsics@npm:^1.1.0": +"math-intrinsics@npm:^1.1.0": version: 1.1.0 resolution: "math-intrinsics@npm:1.1.0" checksum: 10/11df2eda46d092a6035479632e1ec865b8134bdfc4bd9e571a656f4191525404f13a283a515938c3a8de934dbfd9c09674d9da9fa831e6eb7e22b50b197d2edd @@ -47769,7 +47737,7 @@ __metadata: languageName: node linkType: hard -"webpack-dev-server@npm:5.2.0": +"webpack-dev-server@npm:5.2.0, webpack-dev-server@npm:^5.0.0": version: 5.2.0 resolution: "webpack-dev-server@npm:5.2.0" dependencies: @@ -47813,51 +47781,6 @@ __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"