Merge branch 'master' into feat/k8s_objects
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': minor
|
||||
---
|
||||
|
||||
The `repo test` command will no longer default to watch mode if the `--since` flag is provided.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-test-utils': patch
|
||||
---
|
||||
|
||||
Disable the built-in `SignInPage` in `createExtensionTester` in order to not mess with existing tests
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
The `scaffolder-module` template has been updated to use a more modern layout and new testing utilities for scaffolder actions.
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
'@backstage/backend-dynamic-feature-service': patch
|
||||
---
|
||||
|
||||
Enhance and simplify the activation of the dynamic plugins feature:
|
||||
|
||||
- The dynamic plugins service (which implements the `DynamicPluginsProvider`) is restored, since it is required for plugins to depend on it in order to get the details of loaded dynamic plugins (possibly with loading errors to be surfaced in some UI).
|
||||
- A new all-in-one feature loader (`dynamicPluginsFeatureLoader`) is provided that allows a 1-liner activation of both the dynamic features and additional services or plugins required to have the dynamic plugins work correctly with dynamic plugins config schemas. Previous service factories or feature loaders are deprecated.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
---
|
||||
|
||||
It is possible to define a custom error element to be shown when sign in fails
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
'@backstage/backend-dynamic-feature-service': patch
|
||||
---
|
||||
|
||||
Enhance the API of the `DynamicPluginProvider` (available as a service) to:
|
||||
|
||||
- expose the new `getScannedPackage()` method that returns the `ScannedPluginPackage` from which a given plugin has been loaded,
|
||||
- add an optional `includeFailed` argument in the plugins list retrieval methods, to include the plugins that could be successfully loaded (`false` by default).
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-dynamic-feature-service': patch
|
||||
---
|
||||
|
||||
Allow passing an async module loader in the `DynamicPluginsFeatureLoaderOptions`.
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend-module-confluence-to-markdown': patch
|
||||
'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch
|
||||
'@backstage/plugin-scaffolder-backend-module-gitlab': patch
|
||||
'@backstage/plugin-scaffolder-backend-module-rails': patch
|
||||
'@backstage/plugin-scaffolder-backend-module-sentry': patch
|
||||
'@backstage/plugin-scaffolder-backend-module-yeoman': patch
|
||||
'@backstage/plugin-search-backend-module-stack-overflow-collator': patch
|
||||
---
|
||||
|
||||
Updated installation instructions in README to not include `/alpha`.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
'@backstage/plugin-search-backend-module-elasticsearch': minor
|
||||
'@backstage/plugin-scaffolder-backend': minor
|
||||
'@backstage/plugin-techdocs-backend': minor
|
||||
'@backstage/plugin-catalog-backend': minor
|
||||
'@backstage/plugin-search-backend': minor
|
||||
'@backstage/plugin-catalog-backend-module-incremental-ingestion': patch
|
||||
'@backstage/plugin-catalog-backend-module-bitbucket-server': patch
|
||||
'@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch
|
||||
'@backstage/plugin-events-backend-module-bitbucket-cloud': patch
|
||||
'@backstage/plugin-catalog-backend-module-msgraph': patch
|
||||
'@backstage/plugin-search-backend-module-techdocs': patch
|
||||
'@backstage/plugin-catalog-backend-module-gerrit': patch
|
||||
'@backstage/plugin-catalog-backend-module-github': patch
|
||||
'@backstage/plugin-catalog-backend-module-gitlab': patch
|
||||
'@backstage/plugin-events-backend-module-aws-sqs': patch
|
||||
'@backstage/plugin-search-backend-module-catalog': patch
|
||||
'@backstage/plugin-search-backend-module-explore': patch
|
||||
'@backstage/plugin-catalog-backend-module-azure': patch
|
||||
'@backstage/plugin-events-backend-module-gerrit': patch
|
||||
'@backstage/plugin-events-backend-module-azure': patch
|
||||
'@backstage/plugin-catalog-backend-module-aws': patch
|
||||
'@backstage/plugin-search-backend-module-pg': patch
|
||||
'@backstage/plugin-user-settings-backend': patch
|
||||
'@backstage/plugin-kubernetes-backend': patch
|
||||
'@backstage/plugin-permission-backend': patch
|
||||
'@backstage/plugin-events-backend': patch
|
||||
'@backstage/plugin-proxy-backend': patch
|
||||
'@backstage/plugin-app-backend': patch
|
||||
---
|
||||
|
||||
The export for the new backend system at the `/alpha` export is now also available via the main entry point, which means that you can remove the `/alpha` suffix from the import.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
The backend plugin template for the `new` command has been updated to provide more guidance and use a more modern structure.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-incremental-ingestion': patch
|
||||
---
|
||||
|
||||
Turn down the logging level on most "all is well" type log messages
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-test-utils': patch
|
||||
---
|
||||
|
||||
The `createMockDirectory` cleanup strategy has been changed, no longer requiring it to be called outside individual tests.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-app': patch
|
||||
---
|
||||
|
||||
Added missing default `SignInPageExtension` which by default uses guest auth, missing `ApiExtensions` for `scmAuth`
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Add configuration parameters for deferred stitcher
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/codemods': patch
|
||||
---
|
||||
|
||||
Updated dependency `@types/jscodeshift` to `^0.12.0`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/create-app': patch
|
||||
---
|
||||
|
||||
Tweak `Dockerfile` to fix deprecated syntax.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Adds the ability to disable catalog processing `catalog.processingInterval: false` in `app-config`
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
'@backstage/plugin-auth-node': patch
|
||||
---
|
||||
|
||||
Fix authentication error handling using redirect flow via `enableExperimentalRedirectFlow` config. If an error is caught during authentication, the user is redirected back to app origin with `error` query parameter containing the error message.
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
|
||||
- run: yarn build-storybook
|
||||
|
||||
- uses: chromaui/action@30b6228aa809059d46219e0f556752e8672a7e26 # v11
|
||||
- uses: chromaui/action@bbbf288765438d5fd2be13e1d80d542a39e74108 # v11
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# projectToken intentionally shared to allow collaborators to run Chromatic on forks
|
||||
|
||||
@@ -21,7 +21,7 @@ import scaffolderPlugin from '@backstage/plugin-scaffolder-backend';
|
||||
const backend = createBackend();
|
||||
|
||||
// Install desired features
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
|
||||
// Features can also be installed using an explicit reference
|
||||
backend.add(scaffolderPlugin);
|
||||
|
||||
@@ -24,10 +24,10 @@ A feature loader can simply return a list of features to be installed:
|
||||
export default createBackendFeatureLoader({
|
||||
loader() {
|
||||
return [
|
||||
import('@backstage/plugin-search-backend/alpha'),
|
||||
import('@backstage/plugin-search-backend-module-catalog/alpha'),
|
||||
import('@backstage/plugin-search-backend-module-explore/alpha'),
|
||||
import('@backstage/plugin-search-backend-module-techdocs/alpha'),
|
||||
import('@backstage/plugin-search-backend'),
|
||||
import('@backstage/plugin-search-backend-module-catalog'),
|
||||
import('@backstage/plugin-search-backend-module-explore'),
|
||||
import('@backstage/plugin-search-backend-module-techdocs'),
|
||||
];
|
||||
},
|
||||
});
|
||||
@@ -64,10 +64,10 @@ export default createBackendFeatureLoader({
|
||||
*loader({ config }) {
|
||||
// Example of a custom config flag to enable search
|
||||
if (config.getOptionalString('customFeatureToggle.search')) {
|
||||
yield import('@backstage/plugin-search-backend/alpha');
|
||||
yield import('@backstage/plugin-search-backend-module-catalog/alpha');
|
||||
yield import('@backstage/plugin-search-backend-module-explore/alpha');
|
||||
yield import('@backstage/plugin-search-backend-module-techdocs/alpha');
|
||||
yield import('@backstage/plugin-search-backend');
|
||||
yield import('@backstage/plugin-search-backend-module-catalog');
|
||||
yield import('@backstage/plugin-search-backend-module-explore');
|
||||
yield import('@backstage/plugin-search-backend-module-techdocs');
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -84,16 +84,16 @@ export default createBackendFeatureLoader({
|
||||
const localMetadata = await readMetadataFromDisk();
|
||||
|
||||
if (localMetadata.enableSearch) {
|
||||
yield import('@backstage/plugin-search-backend/alpha');
|
||||
yield import('@backstage/plugin-search-backend-module-catalog/alpha');
|
||||
yield import('@backstage/plugin-search-backend');
|
||||
yield import('@backstage/plugin-search-backend-module-catalog');
|
||||
|
||||
const remoteMetadata = await fetchMetadata();
|
||||
|
||||
if (remoteMetadata.enableExplore) {
|
||||
yield import('@backstage/plugin-search-backend-module-explore/alpha');
|
||||
yield import('@backstage/plugin-search-backend-module-explore');
|
||||
}
|
||||
if (remoteMetadata.enableTechDocs) {
|
||||
yield import('@backstage/plugin-search-backend-module-techdocs/alpha');
|
||||
yield import('@backstage/plugin-search-backend-module-techdocs');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -26,9 +26,9 @@ import { createBackend } from '@backstage/backend-defaults'; // Omitted in the e
|
||||
|
||||
const backend = createBackend();
|
||||
|
||||
backend.add(import('@backstage/plugin-app-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-app-backend'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend'));
|
||||
backend.add(
|
||||
import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'),
|
||||
);
|
||||
@@ -126,8 +126,8 @@ You can now trim down the `src/index.ts` files to only include the plugins and m
|
||||
```ts
|
||||
const backend = createBackend();
|
||||
|
||||
backend.add(import('@backstage/plugin-app-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-app-backend'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
backend.add(
|
||||
import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'),
|
||||
);
|
||||
@@ -139,7 +139,7 @@ And `backend-b`, don't forget to clean up dependencies in `package.json` as well
|
||||
```ts
|
||||
const backend = createBackend();
|
||||
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend'));
|
||||
backend.start();
|
||||
```
|
||||
|
||||
|
||||
@@ -237,7 +237,7 @@ be used in its new form.
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
const backend = createBackend();
|
||||
/* highlight-add-next-line */
|
||||
backend.add(import('@backstage/plugin-app-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-app-backend'));
|
||||
```
|
||||
|
||||
If you need to override the app package name, which otherwise defaults to `"app"`,
|
||||
@@ -252,7 +252,7 @@ A basic installation of the catalog plugin looks as follows.
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
const backend = createBackend();
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
backend.add(
|
||||
import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'),
|
||||
);
|
||||
@@ -277,9 +277,9 @@ For `AwsS3DiscoveryProcessor`, first migrate to `AwsS3EntityProvider`.
|
||||
To migrate `AwsS3EntityProvider` to the new backend system, add a reference to the `@backstage/plugin-catalog-backend-module-aws` module.
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-aws/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-aws'));
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
@@ -306,9 +306,9 @@ For `AzureDevOpsDiscoveryProcessor`, first migrate to `AzureDevOpsEntityProvider
|
||||
To migrate `AzureDevOpsEntityProvider` to the new backend system, add a reference to the `@backstage/plugin-catalog-backend-module-azure` module.
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-azure/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-azure'));
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
@@ -340,11 +340,9 @@ For `BitbucketDiscoveryProcessor`, migrate to `BitbucketCloudEntityProvider` or
|
||||
To migrate `BitbucketCloudEntityProvider` to the new backend system, add a reference to the `@backstage/plugin-catalog-backend-module-bitbucket-cloud` module.
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
/* highlight-add-start */
|
||||
backend.add(
|
||||
import('@backstage/plugin-catalog-backend-module-bitbucket-cloud/alpha'),
|
||||
);
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-bitbucket-cloud'));
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
@@ -367,10 +365,10 @@ catalog:
|
||||
To migrate `BitbucketServerEntityProvider` to the new backend system, add a reference to `@backstage/plugin-catalog-backend-module-bitbucket-server`.
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
/* highlight-add-start */
|
||||
backend.add(
|
||||
import('@backstage/plugin-catalog-backend-module-bitbucket-server/alpha'),
|
||||
import('@backstage/plugin-catalog-backend-module-bitbucket-server'),
|
||||
);
|
||||
/* highlight-add-end */
|
||||
```
|
||||
@@ -396,7 +394,7 @@ catalog:
|
||||
To migrate `GkeEntityProvider` to the new backend system, add a reference to `@backstage/plugin-catalog-backend-module-gcp`.
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-gcp'));
|
||||
/* highlight-add-end */
|
||||
@@ -409,9 +407,9 @@ Configuration in app-config.yaml remains the same.
|
||||
To migrate `GerritEntityProvider` to the new backend system, add a reference to `@backstage/plugin-catalog-backend-module-gerrit`.
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-gerrit/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-gerrit'));
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
@@ -438,9 +436,9 @@ For `GithubDiscoveryProcessor`, `GithubMultiOrgReaderProcessor` and `GithubOrgRe
|
||||
To migrate `GithubEntityProvider` to the new backend system, add a reference to `@backstage/plugin-catalog-backend-module-github`.
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-github/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-github'));
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
@@ -463,7 +461,7 @@ catalog:
|
||||
To migrate `GithubMultiOrgEntityProvider` or `GithubOrgEntityProvider` to the new backend system, add a reference to `@backstage/plugin-catalog-backend-module-github-org`.
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-github-org'));
|
||||
/* highlight-add-end */
|
||||
@@ -581,9 +579,9 @@ For `MicrosoftGraphOrgReaderProcessor`, first migrate to `MicrosoftGraphOrgEntit
|
||||
To migrate `MicrosoftGraphOrgEntityProvider` to the new backend system, add a reference to `@backstage/plugin-catalog-backend-module-msgraph`.
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-msgraph/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-msgraph'));
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
@@ -670,7 +668,7 @@ const catalogModuleCustomExtensions = createBackendModule({
|
||||
/* highlight-add-end */
|
||||
|
||||
const backend = createBackend();
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
backend.add(
|
||||
import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'),
|
||||
);
|
||||
@@ -699,7 +697,7 @@ A basic installation of the events plugin looks as follows.
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
const backend = createBackend();
|
||||
/* highlight-add-next-line */
|
||||
backend.add(import('@backstage/plugin-events-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-events-backend'));
|
||||
```
|
||||
|
||||
If you have other customizations made to `plugins/events.ts`, such as adding
|
||||
@@ -760,7 +758,7 @@ const otherPluginModuleCustomExtensions = createBackendModule({
|
||||
/* highlight-add-end */
|
||||
|
||||
const backend = createBackend();
|
||||
backend.add(import('@backstage/plugin-events-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-events-backend'));
|
||||
/* highlight-add-next-line */
|
||||
backend.add(eventsModuleCustomExtensions);
|
||||
/* highlight-add-next-line */
|
||||
@@ -780,7 +778,7 @@ A basic installation of the scaffolder plugin looks as follows.
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
const backend = createBackend();
|
||||
/* highlight-add-next-line */
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend'));
|
||||
```
|
||||
|
||||
With the new Backend System version of the Scaffolder plugin, any provider specific actions will need to be installed separately.
|
||||
@@ -788,7 +786,7 @@ For example - GitHub actions are now collected under the `@backstage/plugin-scaf
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
const backend = createBackend();
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend'));
|
||||
|
||||
/* highlight-add-next-line */
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend-module-github'));
|
||||
@@ -839,7 +837,7 @@ const scaffolderModuleCustomExtensions = createBackendModule({
|
||||
/* highlight-add-end */
|
||||
|
||||
const backend = createBackend();
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend'));
|
||||
/* highlight-add-next-line */
|
||||
backend.add(scaffolderModuleCustomExtensions);
|
||||
```
|
||||
@@ -1143,7 +1141,7 @@ const backend = createBackend();
|
||||
// Other plugins...
|
||||
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-search-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-search-backend'));
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
@@ -1167,8 +1165,8 @@ const backend = createBackend();
|
||||
// Other plugins...
|
||||
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-search-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-search-backend-module-pg/alpha'));
|
||||
backend.add(import('@backstage/plugin-search-backend'));
|
||||
backend.add(import('@backstage/plugin-search-backend-module-pg'));
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
@@ -1182,10 +1180,8 @@ const backend = createBackend();
|
||||
// Other plugins...
|
||||
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-search-backend/alpha'));
|
||||
backend.add(
|
||||
import('@backstage/plugin-search-backend-module-elasticsearch/alpha'),
|
||||
);
|
||||
backend.add(import('@backstage/plugin-search-backend'));
|
||||
backend.add(import('@backstage/plugin-search-backend-module-elasticsearch'));
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
@@ -1203,8 +1199,8 @@ const backend = createBackend();
|
||||
// Other plugins...
|
||||
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-search-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-search-backend-module-catalog/alpha'));
|
||||
backend.add(import('@backstage/plugin-search-backend'));
|
||||
backend.add(import('@backstage/plugin-search-backend-module-catalog'));
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
@@ -1218,8 +1214,8 @@ const backend = createBackend();
|
||||
// Other plugins...
|
||||
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-search-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-search-backend-module-techdocs/alpha'));
|
||||
backend.add(import('@backstage/plugin-search-backend'));
|
||||
backend.add(import('@backstage/plugin-search-backend-module-techdocs'));
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
@@ -1233,7 +1229,7 @@ const backend = createBackend();
|
||||
// Other plugins...
|
||||
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-permission-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-permission-backend'));
|
||||
backend.add(
|
||||
import('@backstage/plugin-permission-backend-module-allow-all-policy'),
|
||||
);
|
||||
@@ -1294,7 +1290,7 @@ const backend = createBackend();
|
||||
// Other plugins...
|
||||
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-permission-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-permission-backend'));
|
||||
backend.add(customPermissionBackendModule);
|
||||
/* highlight-add-end */
|
||||
```
|
||||
@@ -1309,7 +1305,7 @@ const backend = createBackend();
|
||||
// Other plugins...
|
||||
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-techdocs-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-techdocs-backend'));
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
@@ -1323,7 +1319,7 @@ const backend = createBackend();
|
||||
// Other plugins...
|
||||
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-kubernetes-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-kubernetes-backend'));
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ COPY --chown=node:node .yarnrc.yml ./
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# This disables node snapshot for Node 20 to work with the Scaffolder
|
||||
ENV NODE_OPTIONS "--no-node-snapshot"
|
||||
ENV NODE_OPTIONS="--no-node-snapshot"
|
||||
|
||||
# Copy repo skeleton first, to avoid unnecessary docker cache invalidation.
|
||||
# The skeleton contains the package.json of each package in the monorepo,
|
||||
@@ -287,7 +287,7 @@ COPY --chown=node:node examples ./examples
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# This disables node snapshot for Node 20 to work with the Scaffolder
|
||||
ENV NODE_OPTIONS "--no-node-snapshot"
|
||||
ENV NODE_OPTIONS="--no-node-snapshot"
|
||||
|
||||
CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.production.yaml"]
|
||||
```
|
||||
@@ -349,7 +349,7 @@ package, which is done as follows:
|
||||
2. Remove the following line from `packages/backend/src/index.ts`:
|
||||
|
||||
```ts
|
||||
backend.add(import('@backstage/plugin-app-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-app-backend'));
|
||||
```
|
||||
|
||||
3. Remove the `@backstage/plugin-app-backend` and the app package dependency
|
||||
|
||||
@@ -120,7 +120,7 @@ const backend = createBackend();
|
||||
// Other plugins...
|
||||
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-kubernetes-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-kubernetes-backend'));
|
||||
/* highlight-add-end */
|
||||
|
||||
backend.start();
|
||||
@@ -228,7 +228,7 @@ export const kubernetesModuleCustomClusterDiscovery = createBackendModule({
|
||||
});
|
||||
|
||||
// Other plugins...
|
||||
backend.add(import('@backstage/plugin-kubernetes-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-kubernetes-backend'));
|
||||
backend.add(kubernetesModuleCustomClusterDiscovery);
|
||||
|
||||
backend.start();
|
||||
|
||||
@@ -24,10 +24,10 @@ const backend = createBackend();
|
||||
// Other plugins...
|
||||
|
||||
// search plugin
|
||||
backend.add(import('@backstage/plugin-search-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-search-backend'));
|
||||
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-search-backend-module-catalog/alpha'));
|
||||
backend.add(import('@backstage/plugin-search-backend-module-catalog'));
|
||||
/* highlight-add-end */
|
||||
|
||||
backend.start();
|
||||
@@ -68,10 +68,10 @@ const backend = createBackend();
|
||||
// Other plugins...
|
||||
|
||||
// search plugin
|
||||
backend.add(import('@backstage/plugin-search-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-search-backend'));
|
||||
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-search-backend-module-techdocs/alpha'));
|
||||
backend.add(import('@backstage/plugin-search-backend-module-techdocs'));
|
||||
/* highlight-add-end */
|
||||
|
||||
backend.start();
|
||||
|
||||
@@ -145,14 +145,14 @@ const backend = createBackend();
|
||||
|
||||
/* highlight-add-start */
|
||||
// search plugin
|
||||
backend.add(import('@backstage/plugin-search-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-search-backend'));
|
||||
|
||||
// search engines
|
||||
backend.add(import('@backstage/plugin-search-backend-module-pg/alpha'));
|
||||
backend.add(import('@backstage/plugin-search-backend-module-pg'));
|
||||
|
||||
// search collators
|
||||
backend.add(import('@backstage/plugin-search-backend-module-catalog/alpha'));
|
||||
backend.add(import('@backstage/plugin-search-backend-module-techdocs/alpha'));
|
||||
backend.add(import('@backstage/plugin-search-backend-module-catalog'));
|
||||
backend.add(import('@backstage/plugin-search-backend-module-techdocs'));
|
||||
/* highlight-add-end */
|
||||
|
||||
backend.start();
|
||||
|
||||
@@ -25,7 +25,7 @@ const backend = createBackend();
|
||||
// Other plugins...
|
||||
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-search-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-search-backend'));
|
||||
/* highlight-add-end */
|
||||
|
||||
backend.start();
|
||||
@@ -65,10 +65,10 @@ const backend = createBackend();
|
||||
// Other plugins...
|
||||
|
||||
// search plugin
|
||||
backend.add(import('@backstage/plugin-search-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-search-backend'));
|
||||
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-search-backend-module-pg/alpha'));
|
||||
backend.add(import('@backstage/plugin-search-backend-module-pg'));
|
||||
/* highlight-add-end */
|
||||
|
||||
backend.start();
|
||||
@@ -124,12 +124,10 @@ const backend = createBackend();
|
||||
// Other plugins...
|
||||
|
||||
// search plugin
|
||||
backend.add(import('@backstage/plugin-search-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-search-backend'));
|
||||
|
||||
/* highlight-add-start */
|
||||
backend.add(
|
||||
import('@backstage/plugin-search-backend-module-elasticsearch/alpha'),
|
||||
);
|
||||
backend.add(import('@backstage/plugin-search-backend-module-elasticsearch'));
|
||||
/* highlight-add-end */
|
||||
|
||||
backend.start();
|
||||
|
||||
@@ -165,7 +165,7 @@ catalog:
|
||||
|
||||
## Processing Interval
|
||||
|
||||
The [processing loop](https://backstage.io/docs/features/software-catalog/life-of-an-entity) is
|
||||
The [processing loop](./life-of-an-entity.md#processing) is
|
||||
responsible for running your registered processors on all entities, on a certain
|
||||
interval. That interval can be configured with the `processingInterval`
|
||||
app-config parameter.
|
||||
@@ -192,20 +192,50 @@ Setting this value too low risks exhausting rate limits on external systems that
|
||||
are queried by processors, such as version control systems housing catalog-info
|
||||
files.
|
||||
|
||||
## Stitching strategy
|
||||
|
||||
[Stitching](./life-of-an-entity.md#stitching) finalizes the entity. It can be run in
|
||||
two modes:
|
||||
|
||||
- `immediate` - performs stitching in-band immediately when needed
|
||||
- `deferred` - performs the stitching asynchronously
|
||||
|
||||
It can be configured with the `stitchingStrategy` app-config parameter.
|
||||
|
||||
```yaml title="app-config.yaml"
|
||||
catalog:
|
||||
stitchingStrategy: immediate
|
||||
```
|
||||
|
||||
For the `deferred` mode you can set up additional parameters to further tune the process,
|
||||
by setting the following parameters:
|
||||
|
||||
- `pollingInterval` - the interval between polling for entities that need stitching
|
||||
- `stitchTimeout` - the maximum time to wait for an entity to be stitched
|
||||
|
||||
These parameters accept a duration object, similar to the `processingInterval` parameter.
|
||||
|
||||
```yaml title="app-config.yaml"
|
||||
catalog:
|
||||
stitchingStrategy: deferred
|
||||
pollingInterval: { seconds: 1 }
|
||||
stitchTimeout: { minutes: 1 };
|
||||
```
|
||||
|
||||
## Subscribing to Catalog Errors
|
||||
|
||||
Catalog errors are published to the [events plugin](https://github.com/backstage/backstage/tree/master/plugins/events-node): `@backstage/plugin-events-node`. You can subscribe to events and respond to errors, for example you may wish to log them.
|
||||
|
||||
The first step is to add the events backend plugin to your Backstage application. Navigate to your Backstage application directory and add the plugin package.
|
||||
|
||||
```ts title="From your Backstage root directory"
|
||||
```bash title="From your Backstage root directory"
|
||||
yarn --cwd packages/backend add @backstage/plugin-events-backend
|
||||
```
|
||||
|
||||
Now you can install the events backend plugin in your backend.
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
backend.add(import('@backstage/plugin-events-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-events-backend'));
|
||||
```
|
||||
|
||||
### Logging Errors
|
||||
@@ -214,7 +244,7 @@ If you want to log catalog errors you can install the `@backstage/plugin-catalog
|
||||
|
||||
Install the catalog logs module.
|
||||
|
||||
```ts title="From your Backstage root directory"
|
||||
```bash title="From your Backstage root directory"
|
||||
yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-logs
|
||||
```
|
||||
|
||||
|
||||
@@ -326,7 +326,7 @@ export const catalogModuleFrobsProvider = createBackendModule({
|
||||
|
||||
const backend = createBackend();
|
||||
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
backend.add(catalogModuleFrobsProvider);
|
||||
|
||||
// Other plugins ...
|
||||
@@ -764,7 +764,7 @@ export const catalogModuleSystemXReaderProcessor = createBackendModule({
|
||||
|
||||
const backend = createBackend();
|
||||
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
backend.add(catalogModuleSystemXReaderProcessor);
|
||||
|
||||
// Other plugins ...
|
||||
@@ -1025,7 +1025,7 @@ export const catalogModuleCustomDataParser = createBackendModule({
|
||||
|
||||
const backend = createBackend();
|
||||
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
backend.add(catalogModuleCustomDataParser);
|
||||
|
||||
// Other plugins ...
|
||||
|
||||
@@ -24,3 +24,21 @@ Doing on-demand user creation _is_ technically possible by writing custom [entit
|
||||
On the technical side, this is unwanted complexity. You need to implement and maintain a custom provider, instead of what usually amounts to a very easily set-up batch ingestion schedule with providers that come out of the box. Also even if you do this, the catalog is an eventually consistent engine. The user that the provider feeds into the system is not guaranteed to appear immediately. Your experience will likely be only partially functional at bootstrapping time which may have unwanted side effects.
|
||||
|
||||
On the user experience side, a Backstage experience without complete organizational data is a serious hindrance to getting the full power out of the tool. Your users won't be able to click on owners and seeing who they are and what teams they belong to. They won't be able to find out what the communications paths are when they need to reach you or your managers when something goes wrong or they have a feature request. They can't get an overview of what teams own and how they relate to each other. It will be a much more barren experience. Organizational data is highly valuable to have centrally available, complete and correct.
|
||||
|
||||
## Can I call the catalog itself from inside a processor / provider?
|
||||
|
||||
Any backend module, including those that provide processors and entity providers to the catalog, are technically able to get hold of a catalog client via the `catalogServiceRef` from `@backstage/plugin-catalog-node`. However, it is almost never the right thing to do - especially from processors - and we strongly discourage from doing so.
|
||||
|
||||
The catalog processing loop is a very high-speed system where your entire catalog cluster collaborates to race through all entities at the highest possible rate. The ideal processor does an absolute minimum of work, and immediately relinquishes control back. Performing asynchronous requests to external systems - including the catalog - from processors, can quickly become overwhelming for that external system and starve their resources if they aren't prepared to deal with very high rates of small requests. It also significantly slows down the procesing loop, when each step needs to wait for responses. This can lead to work "piling up" in the catalog and delays in seeing entities get updated. The [life of an entity](./life-of-an-entity.md) article shows the sequence of events that happen when an entity goes from original ingestion, through processing, and to becoming final entities.
|
||||
|
||||
See also [the related validation topic](#can-i-validate-relations-in-processors).
|
||||
|
||||
## Can I validate relations in processors?
|
||||
|
||||
Processors are responsible for generating relations from the entity body - see the [life of an entity](./life-of-an-entity.md) article for more details. It's tempting to put rules in your processors that mark entities as invalid if they have a relation to some other entity that does not exist. For example, a `Component` entity that declares a `spec.owner` to a team that has been disbanded. We strongly discourage from doing this type of "hard" validation in processors, for two reasons.
|
||||
|
||||
First, performance. As is described [here](#can-i-call-the-catalog-itself-from-inside-a-processor--provider), you should avoid calling out to the catalog for any reason in processors, including for checking whether a target entity exists. Besides the performance issues, it can also lead to data races where hidden dependencies between entities lead to them never properly settling, or flickering back and forth between states for hard-to-debug reasons.
|
||||
|
||||
Second, user experience. The catalog is an eventually consistent system that constantly tries to mirror external realities. Users make changes in catalog-info files, or things update in external systems, and those changes get streamed in and settle over time inside the catalog. But throwing an error in a processor, instantly aborts processing of that entity and stops its ingestion. Now imagine being a large organization where these changes happen maybe hundreds of times per day. Owners of catalog-info files will constantly be surprised by their files "breaking" in ingestion, maybe a very long time after they were initially created - they were valid at their time of creation and haven't been touched since! This is very frustrating and slows down your users because things break silently for reasons out of your control.
|
||||
|
||||
There are cases where it's fine to throw hard validation errors in processors. Notably, when it doesn't pass a schema test at all and readers of the catalog data will break if the data was let through. Setting the owner to be a number instead of a string could be such an example.
|
||||
|
||||
@@ -281,7 +281,7 @@ const backend = createBackend();
|
||||
// Other plugins...
|
||||
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-permission-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-permission-backend'));
|
||||
backend.add(customPermissionBackendModule);
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
@@ -35,16 +35,16 @@ import { createBackend } from '@backstage/backend-defaults';
|
||||
|
||||
const backend = createBackend();
|
||||
|
||||
backend.add(import('@backstage/plugin-app-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-app-backend'));
|
||||
|
||||
// catalog plugin
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
backend.add(
|
||||
import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'),
|
||||
);
|
||||
|
||||
// scaffolder plugin
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend'));
|
||||
/* highlight-add-next-line */
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend-module-github'));
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ const scaffolderModuleCustomExtensions = createBackendModule({
|
||||
/* highlight-add-end */
|
||||
|
||||
const backend = createBackend();
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend'));
|
||||
/* highlight-add-next-line */
|
||||
backend.add(scaffolderModuleCustomExtensions);
|
||||
```
|
||||
|
||||
@@ -801,34 +801,33 @@ The `projectSlug` filter generates a project slug from a repository URL
|
||||
- **Input**: `github.com?repo=backstage&org=backstage`
|
||||
- **Output**: `backstage/backstage`
|
||||
|
||||
## Custom Filters
|
||||
## Custom Filters and Globals
|
||||
|
||||
Whenever it is needed to extend the built-in filters with yours `${{ parameters.name | my-filter1 | my-filter2 | etc }}`, then you can add them
|
||||
using the property `additionalTemplateFilters`.
|
||||
You may wish to extend the filters and globals with your own custom ones. For example `${{ myGlobal | myFilter | myOtherFilter }}` or `${{ myFunctionGlobal(1,2) | myFilter }}`.
|
||||
This can be achieved using the `additionalTemplateFilters` and `additionalTemplateGlobals` properties respectively.
|
||||
|
||||
The `additionalTemplateFilters` property accepts as type a `Record`
|
||||
These properties accept a `Record`
|
||||
|
||||
```ts title="plugins/scaffolder-backend/src/service/Router.ts"
|
||||
```ts title="plugins/scaffolder-backend/src/service/router.ts"
|
||||
additionalTemplateFilters?: Record<string, TemplateFilter>;
|
||||
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
|
||||
```
|
||||
|
||||
where the first parameter is the name of the filter and the second receives a list of `JSON value` arguments. The `templateFilter()` function must return a JsonValue which is either a Json array, object or primitive.
|
||||
where the first parameter is the identifier of the filter or global and the second is a `TemplateFilter` or a `TemplateGlobal` respectively.
|
||||
A `TemplateFilter` is a function which will be called using the previous `JsonValue` objects and may return a `JsonValue` object.
|
||||
A `TemplateGlobal` can either be a function which will be called using the passed `JsonValue` objects and may return a `JsonValue` object or it can be a `JsonValue` object itself.
|
||||
|
||||
```ts title="plugins/scaffolder-node/src/types.ts"
|
||||
export type TemplateFilter = (...args: JsonValue[]) => JsonValue | undefined;
|
||||
|
||||
export type TemplateGlobal =
|
||||
| ((...args: JsonValue[]) => JsonValue | undefined)
|
||||
| JsonValue;
|
||||
```
|
||||
|
||||
From a practical coding point of view, you will translate that into the following snippet code handling 2 filters:
|
||||
**Usage Example**
|
||||
|
||||
```ts"
|
||||
...
|
||||
additionalTemplateFilters: {
|
||||
base64: (...args: JsonValue[]) => btoa(args.join("")),
|
||||
betterFilter: (...args: JsonValue[]) => { return `This is a much better string than "${args}", don't you think?` }
|
||||
}
|
||||
```
|
||||
|
||||
And within your template, you will be able to use the filters using a parameter and the filter passed using the pipe symbol
|
||||
Given you want to have the following filters and globals available in you template:
|
||||
|
||||
```yaml
|
||||
apiVersion: scaffolder.backstage.io/v1beta3
|
||||
@@ -840,22 +839,21 @@ spec:
|
||||
owner: user:guest
|
||||
type: service
|
||||
|
||||
parameters:
|
||||
- title: Test custom filters
|
||||
properties:
|
||||
userName:
|
||||
title: Name of the user
|
||||
type: string
|
||||
|
||||
steps:
|
||||
- id: debug
|
||||
name: debug
|
||||
- id: debug1
|
||||
name: debug1
|
||||
action: debug:log
|
||||
input:
|
||||
message: ${{ parameters.userName | betterFilter | base64 }}
|
||||
message: ${{ myGlobal | myFilter | myOtherFilter }}
|
||||
|
||||
- id: debug2
|
||||
name: debug2
|
||||
action: debug:log
|
||||
input:
|
||||
message: ${{ myFunctionGlobal(1,2) | myFilter }}
|
||||
```
|
||||
|
||||
Next, you will have to register the property `addTemplateFilters` using the `scaffolderTemplatingExtensionPoint` of a new `BackendModule` [created](../../backend-system/architecture/06-modules.md).
|
||||
You will have to create a new [`BackendModule`](../../backend-system/architecture/06-modules.md) using the `scaffolderTemplatingExtensionPoint`.
|
||||
|
||||
Here is a very simplified example of how to do that:
|
||||
|
||||
@@ -876,11 +874,13 @@ const scaffolderModuleCustomFilters = createBackendModule({
|
||||
// ... and other dependencies as needed
|
||||
},
|
||||
async init({ scaffolder /* ..., other dependencies */ }) {
|
||||
scaffolder.addTemplateGlobals({
|
||||
myGlobal: () => 'myGlobal',
|
||||
myFunctionGlobal: (...args: JsonValue[]) => args[0] + args[1],
|
||||
});
|
||||
scaffolder.addTemplateFilters({
|
||||
base64: (...args: JsonValue[]) => btoa(args.join('')),
|
||||
betterFilter: (...args: JsonValue[]) => {
|
||||
return `This is a much better string than "${args}", don't you think?`;
|
||||
},
|
||||
myFilter: () => 'the value is this now',
|
||||
myOtherFilter: (...args: JsonValue[]) => args.join(''),
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -889,7 +889,7 @@ const scaffolderModuleCustomFilters = createBackendModule({
|
||||
/* highlight-add-end */
|
||||
|
||||
const backend = createBackend();
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend'));
|
||||
/* highlight-add-next-line */
|
||||
backend.add(scaffolderModuleCustomFilters);
|
||||
```
|
||||
@@ -908,10 +908,16 @@ export default async function createPlugin({
|
||||
|
||||
additionalTemplateFilters: {
|
||||
<YOUR_FILTERS>
|
||||
}
|
||||
},
|
||||
additionalTemplateGlobals: {
|
||||
<YOUR_GLOBALS>
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Note that additional template global functions are currently not supported in `fetch:template` (see #25445).
|
||||
|
||||
## Template Editor
|
||||
|
||||
Writing template is most of the times an iterative process. You will need to test your template to make sure it has a good user experience and that it works as expected. To help on this process the scaffolder comes with a build in template editor that allows you to test your template in a real environment for querying data and execute the actions on dry-run mode to see the results of those one.
|
||||
|
||||
@@ -209,7 +209,7 @@ const backend = createBackend();
|
||||
// Other plugins...
|
||||
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-techdocs-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-techdocs-backend'));
|
||||
/* highlight-add-end */
|
||||
|
||||
backend.start();
|
||||
|
||||
@@ -799,7 +799,7 @@ const techdocsCustomBuildStrategy = createBackendModule({
|
||||
// Other plugins...
|
||||
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-techdocs-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-techdocs-backend'));
|
||||
backend.add(techdocsCustomBuildStrategy);
|
||||
/* highlight-add-end */
|
||||
|
||||
|
||||
@@ -73,8 +73,8 @@ yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-aws
|
||||
Then update your backend by adding the following line:
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-aws/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-aws'));
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
@@ -109,8 +109,8 @@ yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-azure
|
||||
Then updated your backend by adding the following line:
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-azure/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-azure'));
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
@@ -49,9 +49,9 @@ For large organizations, this plugin can take a long time, so be careful setting
|
||||
Finally, updated your backend by adding the following line:
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-msgraph/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-msgraph'));
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
|
||||
@@ -27,15 +27,11 @@ yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-bitbuck
|
||||
|
||||
```ts
|
||||
// optional if you want HTTP endpoints to receive external events
|
||||
// backend.add(import('@backstage/plugin-events-backend/alpha'));
|
||||
// backend.add(import('@backstage/plugin-events-backend'));
|
||||
// optional if you want to use AWS SQS instead of HTTP endpoints to receive external events
|
||||
// backend.add(import('@backstage/plugin-events-backend-module-aws-sqs/alpha'));
|
||||
backend.add(
|
||||
import('@backstage/plugin-events-backend-module-bitbucket-cloud/alpha'),
|
||||
);
|
||||
backend.add(
|
||||
import('@backstage/plugin-catalog-backend-module-bitbucket-cloud/alpha'),
|
||||
);
|
||||
// backend.add(import('@backstage/plugin-events-backend-module-aws-sqs'));
|
||||
backend.add(import('@backstage/plugin-events-backend-module-bitbucket-cloud'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-bitbucket-cloud'));
|
||||
```
|
||||
|
||||
You need to decide how you want to receive events from external sources like
|
||||
|
||||
@@ -29,10 +29,10 @@ yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-bitbuck
|
||||
And update your backend by adding the following line:
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
/* highlight-add-start */
|
||||
backend.add(
|
||||
import('@backstage/plugin-catalog-backend-module-bitbucket-server/alpha'),
|
||||
import('@backstage/plugin-catalog-backend-module-bitbucket-server'),
|
||||
);
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
@@ -27,9 +27,9 @@ yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-gerrit
|
||||
Then update your backend by adding the following line:
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-gerrit/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-gerrit'));
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
|
||||
@@ -31,9 +31,9 @@ yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-github
|
||||
And then update your backend by adding the following line:
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-github/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-github'));
|
||||
```
|
||||
|
||||
## Events Support
|
||||
|
||||
@@ -60,7 +60,7 @@ catalog:
|
||||
Finally, update your backend by adding the following line:
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-github-org'));
|
||||
```
|
||||
@@ -167,7 +167,7 @@ const backend = createBackend();
|
||||
|
||||
// Other items
|
||||
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
|
||||
backend.add(githubOrgModule);
|
||||
|
||||
|
||||
@@ -30,14 +30,14 @@ Then add the following to your backend initialization:
|
||||
|
||||
```ts title="packages/backend/src/index.ts
|
||||
// optional if you want HTTP endpoints to receive external events
|
||||
// backend.add(import('@backstage/plugin-events-backend/alpha'));
|
||||
// backend.add(import('@backstage/plugin-events-backend'));
|
||||
// optional if you want to use AWS SQS instead of HTTP endpoints to receive external events
|
||||
// backend.add(import('@backstage/plugin-events-backend-module-aws-sqs/alpha'));
|
||||
// backend.add(import('@backstage/plugin-events-backend-module-aws-sqs'));
|
||||
// optional - event router for gitlab. See.: https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-gitlab/README.md
|
||||
// backend.add(eventsModuleGitlabEventRouter);
|
||||
// optional - token validator for the gitlab topic
|
||||
// backend.add(eventsModuleGitlabWebhook);
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-gitlab/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-gitlab'));
|
||||
```
|
||||
|
||||
You need to decide how you want to receive events from external sources like
|
||||
|
||||
@@ -35,9 +35,9 @@ Then add the following to your backend initialization:
|
||||
|
||||
```ts title="packages/backend/src/index.ts
|
||||
// optional if you want HTTP endpoints to receive external events
|
||||
// backend.add(import('@backstage/plugin-events-backend/alpha'));
|
||||
// backend.add(import('@backstage/plugin-events-backend'));
|
||||
// optional if you want to use AWS SQS instead of HTTP endpoints to receive external events
|
||||
// backend.add(import('@backstage/plugin-events-backend-module-aws-sqs/alpha'));
|
||||
// backend.add(import('@backstage/plugin-events-backend-module-aws-sqs'));
|
||||
// optional - event router for gitlab. See.: https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-gitlab/README.md
|
||||
// backend.add(eventsModuleGitlabEventRouter);
|
||||
// optional - token validator for the gitlab topic
|
||||
|
||||
@@ -44,7 +44,7 @@ catalog:
|
||||
Finally, updated your backend by adding the following line:
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-ldap'));
|
||||
/* highlight-add-end */
|
||||
|
||||
@@ -173,7 +173,7 @@ The api for providing custom rules may differ between plugins, but there should
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
// catalog plugin
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
backend.add(
|
||||
import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'),
|
||||
);
|
||||
|
||||
@@ -34,7 +34,7 @@ To help validate the permission framework is setup we'll create a Test Permissio
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
// permission plugin
|
||||
backend.add(import('@backstage/plugin-permission-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-permission-backend'));
|
||||
/* highlight-remove-start */
|
||||
backend.add(
|
||||
import('@backstage/plugin-permission-backend-module-allow-all-policy'),
|
||||
@@ -85,7 +85,7 @@ To help validate the permission framework is setup we'll create a Test Permissio
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
// permission plugin
|
||||
backend.add(import('@backstage/plugin-permission-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-permission-backend'));
|
||||
/* highlight-add-next-line */
|
||||
backend.add(import('./extensions/permissionsPolicyExtension'));
|
||||
```
|
||||
|
||||
@@ -59,7 +59,7 @@ The source code is available here:
|
||||
//...
|
||||
/* highlight-add-start */
|
||||
// Installing the permission plugin
|
||||
backend.add(import('@backstage/plugin-permission-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-permission-backend'));
|
||||
// Installing the allow all permission policy module
|
||||
backend.add(
|
||||
import('@backstage/plugin-permission-backend-module-allow-all-policy'),
|
||||
|
||||
@@ -227,7 +227,7 @@ import { createBackend } from '@backstage/backend-defaults';
|
||||
//...
|
||||
const backend = createBackend();
|
||||
// Installing the search backend plugin
|
||||
backend.add(import('@backstage/plugin-search-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-search-backend'));
|
||||
// Installing the newly created faq snippets collator module
|
||||
backend.add(
|
||||
import(
|
||||
|
||||
@@ -20,7 +20,7 @@ The plugin is already added to a default Backstage project.
|
||||
To add it to a project, add the following line in `packages/backend/src/index.ts`:
|
||||
|
||||
```ts
|
||||
backend.add(import('@backstage/plugin-proxy-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-proxy-backend'));
|
||||
```
|
||||
|
||||
### Old Backend
|
||||
@@ -28,7 +28,7 @@ backend.add(import('@backstage/plugin-proxy-backend/alpha'));
|
||||
In `packages/backend/src/index.ts`:
|
||||
|
||||
```ts
|
||||
backend.add(import('@backstage/plugin-proxy-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-proxy-backend'));
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -26,7 +26,6 @@ import homePlugin, {
|
||||
import {
|
||||
coreExtensionData,
|
||||
createExtension,
|
||||
ApiBlueprint,
|
||||
createFrontendModule,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import {
|
||||
@@ -41,14 +40,7 @@ import { convertLegacyApp } from '@backstage/core-compat-api';
|
||||
import { FlatRoutes } from '@backstage/core-app-api';
|
||||
import { Route } from 'react-router';
|
||||
import { CatalogImportPage } from '@backstage/plugin-catalog-import';
|
||||
import { createApiFactory, configApiRef } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
ScmAuth,
|
||||
ScmIntegrationsApi,
|
||||
scmIntegrationsApiRef,
|
||||
} from '@backstage/integration-react';
|
||||
import kubernetesPlugin from '@backstage/plugin-kubernetes/alpha';
|
||||
import { signInPageModule } from './overrides/SignInPage';
|
||||
import { convertLegacyPlugin } from '@backstage/core-compat-api';
|
||||
import { convertLegacyPageExtension } from '@backstage/core-compat-api';
|
||||
import { convertLegacyEntityContentExtension } from '@backstage/plugin-catalog-react/alpha';
|
||||
@@ -118,28 +110,6 @@ const customHomePageModule = createFrontendModule({
|
||||
],
|
||||
});
|
||||
|
||||
const scmModule = createFrontendModule({
|
||||
pluginId: 'app',
|
||||
extensions: [
|
||||
ApiBlueprint.make({
|
||||
name: 'scm-auth',
|
||||
params: {
|
||||
factory: ScmAuth.createDefaultApiFactory(),
|
||||
},
|
||||
}),
|
||||
ApiBlueprint.make({
|
||||
name: 'scm-integrations',
|
||||
params: {
|
||||
factory: createApiFactory({
|
||||
api: scmIntegrationsApiRef,
|
||||
deps: { configApi: configApiRef },
|
||||
factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const notFoundErrorPageModule = createFrontendModule({
|
||||
pluginId: 'app',
|
||||
extensions: [notFoundErrorPage],
|
||||
@@ -159,8 +129,6 @@ const app = createApp({
|
||||
homePlugin,
|
||||
appVisualizerPlugin,
|
||||
kubernetesPlugin,
|
||||
signInPageModule,
|
||||
scmModule,
|
||||
notFoundErrorPageModule,
|
||||
customHomePageModule,
|
||||
...collectedLegacyPlugins,
|
||||
|
||||
@@ -15,11 +15,8 @@
|
||||
*/
|
||||
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { signInPageModule } from './overrides/SignInPage';
|
||||
import { createPublicSignInApp } from '@backstage/frontend-defaults';
|
||||
|
||||
const app = createPublicSignInApp({
|
||||
features: [signInPageModule],
|
||||
});
|
||||
const app = createPublicSignInApp();
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(app.createRoot());
|
||||
|
||||
@@ -15,8 +15,7 @@ In the `backend` application, it can be enabled by adding the `backend-dynamic-f
|
||||
```ts
|
||||
const backend = createBackend();
|
||||
+
|
||||
+ backend.add(dynamicPluginsFeatureDiscoveryServiceFactory) // overridden version of the FeatureDiscoveryService which provides features loaded by dynamic plugins
|
||||
+ backend.add(dynamicPluginsServiceFactory)
|
||||
+ backend.add(dynamicPluginsFeatureLoader) // provides features loaded by dynamic plugins
|
||||
+
|
||||
```
|
||||
|
||||
|
||||
@@ -76,6 +76,8 @@
|
||||
"devDependencies": {
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/plugin-app-backend": "workspace:^",
|
||||
"triple-beam": "^1.4.1",
|
||||
"wait-for-expect": "^3.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,11 +33,12 @@ import { ServiceRef } from '@backstage/backend-plugin-api';
|
||||
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
|
||||
import { TokenManager } from '@backstage/backend-common';
|
||||
import { UrlReaderService } from '@backstage/backend-plugin-api';
|
||||
import { WinstonLoggerOptions } from '@backstage/backend-defaults/rootLogger';
|
||||
|
||||
// @public (undocumented)
|
||||
export interface BackendDynamicPlugin extends BaseDynamicPlugin {
|
||||
// (undocumented)
|
||||
installer: BackendDynamicPluginInstaller;
|
||||
installer?: BackendDynamicPluginInstaller;
|
||||
// (undocumented)
|
||||
platform: 'node';
|
||||
}
|
||||
@@ -50,11 +51,13 @@ export type BackendDynamicPluginInstaller =
|
||||
// @public (undocumented)
|
||||
export interface BackendPluginProvider {
|
||||
// (undocumented)
|
||||
backendPlugins(): BackendDynamicPlugin[];
|
||||
backendPlugins(options?: { includeFailed?: boolean }): BackendDynamicPlugin[];
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface BaseDynamicPlugin {
|
||||
// (undocumented)
|
||||
failure?: string;
|
||||
// (undocumented)
|
||||
name: string;
|
||||
// (undocumented)
|
||||
@@ -75,15 +78,19 @@ export class DynamicPluginManager implements DynamicPluginProvider {
|
||||
// (undocumented)
|
||||
get availablePackages(): ScannedPluginPackage[];
|
||||
// (undocumented)
|
||||
backendPlugins(): BackendDynamicPlugin[];
|
||||
backendPlugins(options?: { includeFailed?: boolean }): BackendDynamicPlugin[];
|
||||
// (undocumented)
|
||||
static create(
|
||||
options: DynamicPluginManagerOptions,
|
||||
): Promise<DynamicPluginManager>;
|
||||
// (undocumented)
|
||||
frontendPlugins(): FrontendDynamicPlugin[];
|
||||
frontendPlugins(options?: {
|
||||
includeFailed?: boolean;
|
||||
}): FrontendDynamicPlugin[];
|
||||
// (undocumented)
|
||||
plugins(): DynamicPlugin[];
|
||||
getScannedPackage(plugin: DynamicPlugin): ScannedPluginPackage;
|
||||
// (undocumented)
|
||||
plugins(options?: { includeFailed?: boolean }): DynamicPlugin[];
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -103,20 +110,19 @@ export interface DynamicPluginProvider
|
||||
extends FrontendPluginProvider,
|
||||
BackendPluginProvider {
|
||||
// (undocumented)
|
||||
plugins(): DynamicPlugin[];
|
||||
getScannedPackage(plugin: DynamicPlugin): ScannedPluginPackage;
|
||||
// (undocumented)
|
||||
plugins(options?: { includeFailed?: boolean }): DynamicPlugin[];
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface DynamicPluginsFactoryOptions {
|
||||
// (undocumented)
|
||||
moduleLoader?(logger: LoggerService): ModuleLoader;
|
||||
moduleLoader?(logger: LoggerService): ModuleLoader | Promise<ModuleLoader>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export const dynamicPluginsFeatureDiscoveryLoader: ((
|
||||
options?: DynamicPluginsFactoryOptions,
|
||||
) => BackendFeature) &
|
||||
BackendFeature;
|
||||
// @public @deprecated (undocumented)
|
||||
export const dynamicPluginsFeatureDiscoveryLoader: BackendFeature;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export const dynamicPluginsFeatureDiscoveryServiceFactory: ServiceFactory<
|
||||
@@ -125,16 +131,32 @@ export const dynamicPluginsFeatureDiscoveryServiceFactory: ServiceFactory<
|
||||
'singleton'
|
||||
>;
|
||||
|
||||
// @public
|
||||
export const dynamicPluginsFeatureLoader: ((
|
||||
options?: DynamicPluginsFeatureLoaderOptions,
|
||||
) => BackendFeature) &
|
||||
BackendFeature;
|
||||
|
||||
// @public (undocumented)
|
||||
export type DynamicPluginsFeatureLoaderOptions = DynamicPluginsFactoryOptions &
|
||||
DynamicPluginsSchemasOptions &
|
||||
DynamicPluginsRootLoggerFactoryOptions;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export const dynamicPluginsFrontendSchemas: BackendFeature;
|
||||
|
||||
// @public (undocumented)
|
||||
export const dynamicPluginsRootLoggerServiceFactory: ServiceFactory<
|
||||
RootLoggerService,
|
||||
'root',
|
||||
'singleton'
|
||||
export type DynamicPluginsRootLoggerFactoryOptions = Omit<
|
||||
WinstonLoggerOptions,
|
||||
'meta'
|
||||
>;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export const dynamicPluginsRootLoggerServiceFactory: ((
|
||||
options?: DynamicPluginsRootLoggerFactoryOptions,
|
||||
) => ServiceFactory<RootLoggerService, 'root', 'singleton'>) &
|
||||
ServiceFactory<RootLoggerService, 'root', 'singleton'>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface DynamicPluginsSchemasOptions {
|
||||
schemaLocator?: (pluginPackage: ScannedPluginPackage) => string;
|
||||
@@ -148,31 +170,24 @@ export interface DynamicPluginsSchemasService {
|
||||
}>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const dynamicPluginsSchemasServiceFactory: ServiceFactory<
|
||||
DynamicPluginsSchemasService,
|
||||
'root',
|
||||
'singleton'
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const dynamicPluginsSchemasServiceFactoryWithOptions: (
|
||||
// @public @deprecated (undocumented)
|
||||
export const dynamicPluginsSchemasServiceFactory: ((
|
||||
options?: DynamicPluginsSchemasOptions,
|
||||
) => ServiceFactory<DynamicPluginsSchemasService, 'root', 'singleton'>;
|
||||
) => ServiceFactory<DynamicPluginsSchemasService, 'root', 'singleton'>) &
|
||||
ServiceFactory<DynamicPluginsSchemasService, 'root', 'singleton'>;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export const dynamicPluginsServiceFactory: ServiceFactory<
|
||||
DynamicPluginProvider,
|
||||
'root',
|
||||
'singleton'
|
||||
>;
|
||||
export const dynamicPluginsServiceFactory: ((
|
||||
options?: DynamicPluginsFactoryOptions,
|
||||
) => ServiceFactory<DynamicPluginProvider, 'root', 'singleton'>) &
|
||||
ServiceFactory<DynamicPluginProvider, 'root', 'singleton'>;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export const dynamicPluginsServiceFactoryWithOptions: (
|
||||
options?: DynamicPluginsFactoryOptions,
|
||||
) => ServiceFactory<DynamicPluginProvider, 'root', 'singleton'>;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
// @public (undocumented)
|
||||
export const dynamicPluginsServiceRef: ServiceRef<
|
||||
DynamicPluginProvider,
|
||||
'root',
|
||||
@@ -188,7 +203,9 @@ export interface FrontendDynamicPlugin extends BaseDynamicPlugin {
|
||||
// @public (undocumented)
|
||||
export interface FrontendPluginProvider {
|
||||
// (undocumented)
|
||||
frontendPlugins(): FrontendDynamicPlugin[];
|
||||
frontendPlugins(options?: {
|
||||
includeFailed?: boolean;
|
||||
}): FrontendDynamicPlugin[];
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -278,6 +295,7 @@ export interface ScannedPluginPackage {
|
||||
|
||||
// Warnings were encountered during analysis:
|
||||
//
|
||||
// src/features/features.d.ts:7:1 - (ae-undocumented) Missing documentation for "DynamicPluginsFeatureLoaderOptions".
|
||||
// src/loader/types.d.ts:4:1 - (ae-undocumented) Missing documentation for "ModuleLoader".
|
||||
// src/loader/types.d.ts:5:5 - (ae-undocumented) Missing documentation for "bootstrap".
|
||||
// src/loader/types.d.ts:6:5 - (ae-undocumented) Missing documentation for "load".
|
||||
@@ -291,56 +309,60 @@ export interface ScannedPluginPackage {
|
||||
// src/manager/plugin-manager.d.ts:27:5 - (ae-undocumented) Missing documentation for "availablePackages".
|
||||
// src/manager/plugin-manager.d.ts:28:5 - (ae-undocumented) Missing documentation for "addBackendPlugin".
|
||||
// src/manager/plugin-manager.d.ts:31:5 - (ae-undocumented) Missing documentation for "backendPlugins".
|
||||
// src/manager/plugin-manager.d.ts:32:5 - (ae-undocumented) Missing documentation for "frontendPlugins".
|
||||
// src/manager/plugin-manager.d.ts:33:5 - (ae-undocumented) Missing documentation for "plugins".
|
||||
// src/manager/plugin-manager.d.ts:39:22 - (ae-undocumented) Missing documentation for "dynamicPluginsServiceRef".
|
||||
// src/manager/plugin-manager.d.ts:43:1 - (ae-undocumented) Missing documentation for "DynamicPluginsFactoryOptions".
|
||||
// src/manager/plugin-manager.d.ts:44:5 - (ae-undocumented) Missing documentation for "moduleLoader".
|
||||
// src/manager/plugin-manager.d.ts:50:22 - (ae-undocumented) Missing documentation for "dynamicPluginsServiceFactoryWithOptions".
|
||||
// src/manager/plugin-manager.d.ts:55:22 - (ae-undocumented) Missing documentation for "dynamicPluginsServiceFactory".
|
||||
// src/manager/plugin-manager.d.ts:60:22 - (ae-undocumented) Missing documentation for "dynamicPluginsFeatureDiscoveryServiceFactory".
|
||||
// src/manager/types.d.ts:27:1 - (ae-undocumented) Missing documentation for "LegacyPluginEnvironment".
|
||||
// src/manager/types.d.ts:45:1 - (ae-undocumented) Missing documentation for "DynamicPluginProvider".
|
||||
// src/manager/types.d.ts:46:5 - (ae-undocumented) Missing documentation for "plugins".
|
||||
// src/manager/types.d.ts:51:1 - (ae-undocumented) Missing documentation for "BackendPluginProvider".
|
||||
// src/manager/types.d.ts:52:5 - (ae-undocumented) Missing documentation for "backendPlugins".
|
||||
// src/manager/types.d.ts:57:1 - (ae-undocumented) Missing documentation for "FrontendPluginProvider".
|
||||
// src/manager/types.d.ts:58:5 - (ae-undocumented) Missing documentation for "frontendPlugins".
|
||||
// src/manager/types.d.ts:63:1 - (ae-undocumented) Missing documentation for "BaseDynamicPlugin".
|
||||
// src/manager/types.d.ts:64:5 - (ae-undocumented) Missing documentation for "name".
|
||||
// src/manager/types.d.ts:65:5 - (ae-undocumented) Missing documentation for "version".
|
||||
// src/manager/types.d.ts:66:5 - (ae-undocumented) Missing documentation for "role".
|
||||
// src/manager/types.d.ts:67:5 - (ae-undocumented) Missing documentation for "platform".
|
||||
// src/manager/types.d.ts:72:1 - (ae-undocumented) Missing documentation for "DynamicPlugin".
|
||||
// src/manager/types.d.ts:76:1 - (ae-undocumented) Missing documentation for "FrontendDynamicPlugin".
|
||||
// src/manager/types.d.ts:77:5 - (ae-undocumented) Missing documentation for "platform".
|
||||
// src/manager/types.d.ts:82:1 - (ae-undocumented) Missing documentation for "BackendDynamicPlugin".
|
||||
// src/manager/types.d.ts:83:5 - (ae-undocumented) Missing documentation for "platform".
|
||||
// src/manager/types.d.ts:84:5 - (ae-undocumented) Missing documentation for "installer".
|
||||
// src/manager/types.d.ts:89:1 - (ae-undocumented) Missing documentation for "BackendDynamicPluginInstaller".
|
||||
// src/manager/types.d.ts:93:1 - (ae-undocumented) Missing documentation for "NewBackendPluginInstaller".
|
||||
// src/manager/types.d.ts:94:5 - (ae-undocumented) Missing documentation for "kind".
|
||||
// src/manager/types.d.ts:95:5 - (ae-undocumented) Missing documentation for "install".
|
||||
// src/manager/types.d.ts:108:1 - (ae-undocumented) Missing documentation for "LegacyBackendPluginInstaller".
|
||||
// src/manager/types.d.ts:109:5 - (ae-undocumented) Missing documentation for "kind".
|
||||
// src/manager/types.d.ts:110:5 - (ae-undocumented) Missing documentation for "router".
|
||||
// src/manager/types.d.ts:114:5 - (ae-undocumented) Missing documentation for "catalog".
|
||||
// src/manager/types.d.ts:115:5 - (ae-undocumented) Missing documentation for "scaffolder".
|
||||
// src/manager/types.d.ts:116:5 - (ae-undocumented) Missing documentation for "search".
|
||||
// src/manager/types.d.ts:117:5 - (ae-undocumented) Missing documentation for "events".
|
||||
// src/manager/types.d.ts:118:5 - (ae-undocumented) Missing documentation for "permissions".
|
||||
// src/manager/types.d.ts:125:1 - (ae-undocumented) Missing documentation for "isBackendDynamicPluginInstaller".
|
||||
// src/manager/plugin-manager.d.ts:34:5 - (ae-undocumented) Missing documentation for "frontendPlugins".
|
||||
// src/manager/plugin-manager.d.ts:37:5 - (ae-undocumented) Missing documentation for "plugins".
|
||||
// src/manager/plugin-manager.d.ts:40:5 - (ae-undocumented) Missing documentation for "getScannedPackage".
|
||||
// src/manager/plugin-manager.d.ts:45:22 - (ae-undocumented) Missing documentation for "dynamicPluginsServiceRef".
|
||||
// src/manager/plugin-manager.d.ts:49:1 - (ae-undocumented) Missing documentation for "DynamicPluginsFactoryOptions".
|
||||
// src/manager/plugin-manager.d.ts:50:5 - (ae-undocumented) Missing documentation for "moduleLoader".
|
||||
// src/manager/plugin-manager.d.ts:56:22 - (ae-undocumented) Missing documentation for "dynamicPluginsServiceFactoryWithOptions".
|
||||
// src/manager/plugin-manager.d.ts:61:22 - (ae-undocumented) Missing documentation for "dynamicPluginsServiceFactory".
|
||||
// src/manager/plugin-manager.d.ts:66:22 - (ae-undocumented) Missing documentation for "dynamicPluginsFeatureDiscoveryServiceFactory".
|
||||
// src/manager/plugin-manager.d.ts:71:22 - (ae-undocumented) Missing documentation for "dynamicPluginsFeatureDiscoveryLoader".
|
||||
// src/manager/types.d.ts:28:1 - (ae-undocumented) Missing documentation for "LegacyPluginEnvironment".
|
||||
// src/manager/types.d.ts:46:1 - (ae-undocumented) Missing documentation for "DynamicPluginProvider".
|
||||
// src/manager/types.d.ts:47:5 - (ae-undocumented) Missing documentation for "plugins".
|
||||
// src/manager/types.d.ts:50:5 - (ae-undocumented) Missing documentation for "getScannedPackage".
|
||||
// src/manager/types.d.ts:55:1 - (ae-undocumented) Missing documentation for "BackendPluginProvider".
|
||||
// src/manager/types.d.ts:56:5 - (ae-undocumented) Missing documentation for "backendPlugins".
|
||||
// src/manager/types.d.ts:63:1 - (ae-undocumented) Missing documentation for "FrontendPluginProvider".
|
||||
// src/manager/types.d.ts:64:5 - (ae-undocumented) Missing documentation for "frontendPlugins".
|
||||
// src/manager/types.d.ts:71:1 - (ae-undocumented) Missing documentation for "BaseDynamicPlugin".
|
||||
// src/manager/types.d.ts:72:5 - (ae-undocumented) Missing documentation for "name".
|
||||
// src/manager/types.d.ts:73:5 - (ae-undocumented) Missing documentation for "version".
|
||||
// src/manager/types.d.ts:74:5 - (ae-undocumented) Missing documentation for "role".
|
||||
// src/manager/types.d.ts:75:5 - (ae-undocumented) Missing documentation for "platform".
|
||||
// src/manager/types.d.ts:76:5 - (ae-undocumented) Missing documentation for "failure".
|
||||
// src/manager/types.d.ts:81:1 - (ae-undocumented) Missing documentation for "DynamicPlugin".
|
||||
// src/manager/types.d.ts:85:1 - (ae-undocumented) Missing documentation for "FrontendDynamicPlugin".
|
||||
// src/manager/types.d.ts:86:5 - (ae-undocumented) Missing documentation for "platform".
|
||||
// src/manager/types.d.ts:91:1 - (ae-undocumented) Missing documentation for "BackendDynamicPlugin".
|
||||
// src/manager/types.d.ts:92:5 - (ae-undocumented) Missing documentation for "platform".
|
||||
// src/manager/types.d.ts:93:5 - (ae-undocumented) Missing documentation for "installer".
|
||||
// src/manager/types.d.ts:98:1 - (ae-undocumented) Missing documentation for "BackendDynamicPluginInstaller".
|
||||
// src/manager/types.d.ts:102:1 - (ae-undocumented) Missing documentation for "NewBackendPluginInstaller".
|
||||
// src/manager/types.d.ts:103:5 - (ae-undocumented) Missing documentation for "kind".
|
||||
// src/manager/types.d.ts:104:5 - (ae-undocumented) Missing documentation for "install".
|
||||
// src/manager/types.d.ts:117:1 - (ae-undocumented) Missing documentation for "LegacyBackendPluginInstaller".
|
||||
// src/manager/types.d.ts:118:5 - (ae-undocumented) Missing documentation for "kind".
|
||||
// src/manager/types.d.ts:119:5 - (ae-undocumented) Missing documentation for "router".
|
||||
// src/manager/types.d.ts:123:5 - (ae-undocumented) Missing documentation for "catalog".
|
||||
// src/manager/types.d.ts:124:5 - (ae-undocumented) Missing documentation for "scaffolder".
|
||||
// src/manager/types.d.ts:125:5 - (ae-undocumented) Missing documentation for "search".
|
||||
// src/manager/types.d.ts:126:5 - (ae-undocumented) Missing documentation for "events".
|
||||
// src/manager/types.d.ts:127:5 - (ae-undocumented) Missing documentation for "permissions".
|
||||
// src/manager/types.d.ts:134:1 - (ae-undocumented) Missing documentation for "isBackendDynamicPluginInstaller".
|
||||
// src/scanner/types.d.ts:5:1 - (ae-undocumented) Missing documentation for "ScannedPluginPackage".
|
||||
// src/scanner/types.d.ts:6:5 - (ae-undocumented) Missing documentation for "location".
|
||||
// src/scanner/types.d.ts:7:5 - (ae-undocumented) Missing documentation for "manifest".
|
||||
// src/scanner/types.d.ts:12:1 - (ae-undocumented) Missing documentation for "ScannedPluginManifest".
|
||||
// src/schemas/appBackendModule.d.ts:2:22 - (ae-undocumented) Missing documentation for "dynamicPluginsFrontendSchemas".
|
||||
// src/schemas/rootLoggerServiceFactory.d.ts:2:22 - (ae-undocumented) Missing documentation for "dynamicPluginsRootLoggerServiceFactory".
|
||||
// src/schemas/frontend.d.ts:5:22 - (ae-undocumented) Missing documentation for "dynamicPluginsFrontendSchemas".
|
||||
// src/schemas/rootLogger.d.ts:5:1 - (ae-undocumented) Missing documentation for "DynamicPluginsRootLoggerFactoryOptions".
|
||||
// src/schemas/rootLogger.d.ts:10:22 - (ae-undocumented) Missing documentation for "dynamicPluginsRootLoggerServiceFactory".
|
||||
// src/schemas/schemas.d.ts:7:1 - (ae-undocumented) Missing documentation for "DynamicPluginsSchemasService".
|
||||
// src/schemas/schemas.d.ts:8:5 - (ae-undocumented) Missing documentation for "addDynamicPluginsSchemas".
|
||||
// src/schemas/schemas.d.ts:21:1 - (ae-undocumented) Missing documentation for "DynamicPluginsSchemasOptions".
|
||||
// src/schemas/schemas.d.ts:36:22 - (ae-undocumented) Missing documentation for "dynamicPluginsSchemasServiceFactoryWithOptions".
|
||||
// src/schemas/schemas.d.ts:40:22 - (ae-undocumented) Missing documentation for "dynamicPluginsSchemasServiceFactory".
|
||||
// src/schemas/schemas.d.ts:37:22 - (ae-undocumented) Missing documentation for "dynamicPluginsSchemasServiceFactory".
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
!dist
|
||||
!node_modules
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"test-backend": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"secretValue"
|
||||
],
|
||||
"properties": {
|
||||
"secretValue": {
|
||||
"type": "string",
|
||||
"visibility": "secret"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
|
||||
const { dynamicPluginsServiceRef } = require('../../../../../manager');
|
||||
var backendPluginApi = require('@backstage/backend-plugin-api');
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const url = require('url');
|
||||
|
||||
const privateDep = require('private-dep-with-frontend-plugin-index-path');
|
||||
|
||||
const testPlugin = backendPluginApi.createBackendPlugin({
|
||||
pluginId: "test",
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
http: backendPluginApi.coreServices.httpRouter,
|
||||
logger: backendPluginApi.coreServices.rootLogger,
|
||||
discovery: backendPluginApi.coreServices.discovery,
|
||||
dynamicPlugins: dynamicPluginsServiceRef,
|
||||
metadata: backendPluginApi.coreServices.pluginMetadata,
|
||||
},
|
||||
async init({
|
||||
http,
|
||||
logger,
|
||||
discovery,
|
||||
dynamicPlugins,
|
||||
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',
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
exports.default = testPlugin;
|
||||
//# sourceMappingURL=alpha.cjs.js.map
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
|
||||
const frontendPluginsIndexPath = 'frontend-plugins';
|
||||
|
||||
exports.frontendPluginsIndexPath = frontendPluginsIndexPath;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "private-dep-with-frontend-plugin-index-path",
|
||||
"version": "0.0.0",
|
||||
"description": "private dependency of the test backend plugin",
|
||||
"main": "index.js",
|
||||
"dependencies": {}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "plugin-test-backend-dynamic",
|
||||
"version": "0.0.0",
|
||||
"description": "A test dynamic backend module that exposes the dynamic frontend plugins to an endpoint.",
|
||||
"backstage": {
|
||||
"role": "backend-plugin",
|
||||
"pluginId": "test",
|
||||
"pluginPackages": [
|
||||
"plugin-test",
|
||||
"plugin-test-backend"
|
||||
]
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage",
|
||||
"dynamic"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"require": "./dist/index.cjs.js",
|
||||
"default": "./dist/index.cjs.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"main": "./dist/index.cjs.js",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"private-dep-with-frontend-plugin-index-path": "0.0.0"
|
||||
},
|
||||
"bundleDependencies": true
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"test-frontend": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"frontendValue"
|
||||
],
|
||||
"properties": {
|
||||
"frontendValue": {
|
||||
"type": "string",
|
||||
"visibility": "frontend"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"id": "backstage__plugin_test",
|
||||
"name": "backstage__plugin_test",
|
||||
"metaData": {
|
||||
"name": "backstage__plugin_test",
|
||||
"type": "app",
|
||||
"buildInfo": {
|
||||
"buildVersion": "0.0.0",
|
||||
"buildName": "@backstage/plugin-test"
|
||||
},
|
||||
"remoteEntry": {
|
||||
"name": "remoteEntry.js",
|
||||
"path": "",
|
||||
"type": "global"
|
||||
},
|
||||
"types": {
|
||||
"path": "",
|
||||
"name": "",
|
||||
"zip": "",
|
||||
"api": ""
|
||||
},
|
||||
"globalName": "backstage__plugin_test",
|
||||
"pluginVersion": "0.0.0",
|
||||
"publicPath": "auto"
|
||||
},
|
||||
"shared": [],
|
||||
"remotes": [],
|
||||
"exposes": []
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
(function doNothing(){})();
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "plugin-test-dynamic",
|
||||
"version": "0.0.0",
|
||||
"description": "A test dynamic, module-federation-based, Backstage frontend plugin that does nothing",
|
||||
"backstage": {
|
||||
"role": "frontend-dynamic-container",
|
||||
"pluginId": "test",
|
||||
"pluginPackages": [
|
||||
"plugin-test"
|
||||
]
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage",
|
||||
"dynamic"
|
||||
],
|
||||
"main": "remote-entry.js"
|
||||
}
|
||||
Generated
Vendored
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
throw new Error("False @backstage/backend-plugin-api package which should be skipped by the CommonJSModuleLoader");
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/backend-plugin-api",
|
||||
"version": "0.0.0",
|
||||
"description": "dummy backstage package that should be skipped by the ComonJSLoduleLoader",
|
||||
"main": "index.js",
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
/*
|
||||
* 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 {
|
||||
startTestBackend,
|
||||
mockServices,
|
||||
createMockDirectory,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import { dynamicPluginsFeatureLoader } from './features';
|
||||
import { DynamicPlugin, dynamicPluginsServiceRef } from '../manager';
|
||||
import path, { resolve as resolvePath } from 'path';
|
||||
import {
|
||||
BackendFeature,
|
||||
createBackendPlugin,
|
||||
LoggerService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { CommonJSModuleLoader } from '../loader/CommonJSModuleLoader';
|
||||
import * as winston from 'winston';
|
||||
import { MESSAGE } from 'triple-beam';
|
||||
import { overridePackagePathResolution } from '@backstage/backend-plugin-api/testUtils';
|
||||
|
||||
// these can get a bit slow in CI
|
||||
jest.setTimeout(60_000);
|
||||
|
||||
async function jestFreeTypescriptAwareModuleLoader(
|
||||
logger: LoggerService,
|
||||
dontBootstrap: boolean = false,
|
||||
) {
|
||||
const loader = new CommonJSModuleLoader(logger);
|
||||
(loader as any).module = await loader.load('node:module');
|
||||
loader.load(path.resolve(__dirname, '../../../cli/config/nodeTransform.cjs'));
|
||||
if (dontBootstrap) {
|
||||
loader.bootstrap = async () => {};
|
||||
}
|
||||
return loader;
|
||||
}
|
||||
|
||||
class MockedTransport extends winston.transports.Console {
|
||||
readonly logs: string[] = [];
|
||||
|
||||
public log(info: any, callback: () => void) {
|
||||
if (!info[MESSAGE]?.includes('info: Plugin initialization ')) {
|
||||
this.logs.push(info[MESSAGE]);
|
||||
}
|
||||
super.log!(info, callback);
|
||||
}
|
||||
public logv(info: any, callback: () => void) {
|
||||
if (!info[MESSAGE]?.includes('info: Plugin initialization ')) {
|
||||
this.logs.push(info[MESSAGE]);
|
||||
}
|
||||
super.log!(info, callback);
|
||||
}
|
||||
}
|
||||
|
||||
class DynamicPluginLister {
|
||||
readonly loadedPlugins: DynamicPlugin[] = [];
|
||||
feature(): BackendFeature {
|
||||
// eslint-disable-next-line consistent-this
|
||||
const that = this;
|
||||
return createBackendPlugin({
|
||||
pluginId: 'dynamicPluginsLister',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {
|
||||
dynamicPlugins: dynamicPluginsServiceRef,
|
||||
},
|
||||
async init({ dynamicPlugins }) {
|
||||
that.loadedPlugins.push(
|
||||
...dynamicPlugins.plugins({ includeFailed: true }),
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
describe('dynamicPluginsFeatureLoader', () => {
|
||||
const dynamicPluginsRootDirectory = resolvePath(
|
||||
__dirname,
|
||||
'__fixtures__/dynamic-plugins-root',
|
||||
);
|
||||
|
||||
// A dummy `@backstage/backend-plugin-api` package which throws an error is available inside the test fixtures,
|
||||
// in a `node_modules` folder which is a sibling of the `dynamic-plugins-root`.
|
||||
// This test demonstrates how, without the skipping logic implemented in the {@link CommonJSModelLoader},
|
||||
// this dummy package would be loaded by the backend dynamic plugins instead of the one of the backstage root.
|
||||
it('should fail because the model loader is not skipping modules living in unexpected locations.', async () => {
|
||||
const dynamicPLuginsLister = new DynamicPluginLister();
|
||||
const mockedTransport = new MockedTransport();
|
||||
await startTestBackend({
|
||||
features: [
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
dynamicPlugins: {
|
||||
rootDirectory: dynamicPluginsRootDirectory,
|
||||
},
|
||||
},
|
||||
}),
|
||||
dynamicPluginsFeatureLoader({
|
||||
moduleLoader: logger =>
|
||||
jestFreeTypescriptAwareModuleLoader(logger, true),
|
||||
transports: [mockedTransport],
|
||||
format: winston.format.simple(),
|
||||
}),
|
||||
dynamicPLuginsLister.feature(),
|
||||
],
|
||||
});
|
||||
expect(mockedTransport.logs).toContainEqual(
|
||||
expect.stringMatching(
|
||||
"error: an error occurred while loading dynamic backend plugin 'plugin-test-backend-dynamic' from '.*/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic",
|
||||
),
|
||||
);
|
||||
expect(dynamicPLuginsLister.loadedPlugins).toMatchObject([
|
||||
{
|
||||
name: 'plugin-test-backend-dynamic',
|
||||
platform: 'node',
|
||||
role: 'backend-plugin',
|
||||
version: '0.0.0',
|
||||
failure:
|
||||
'Error: False @backstage/backend-plugin-api package which should be skipped by the CommonJSModuleLoader',
|
||||
},
|
||||
expect.anything(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should load and show the 2 dynamic plugins in a list of dynamic plugins returned by a static backend plugin', async () => {
|
||||
const dynamicPLuginsLister = new DynamicPluginLister();
|
||||
await startTestBackend({
|
||||
features: [
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
dynamicPlugins: {
|
||||
rootDirectory: dynamicPluginsRootDirectory,
|
||||
},
|
||||
},
|
||||
}),
|
||||
dynamicPluginsFeatureLoader({
|
||||
moduleLoader: jestFreeTypescriptAwareModuleLoader,
|
||||
}),
|
||||
dynamicPLuginsLister.feature(),
|
||||
],
|
||||
});
|
||||
|
||||
expect(dynamicPLuginsLister.loadedPlugins).toMatchObject([
|
||||
{
|
||||
installer: {
|
||||
kind: 'new',
|
||||
},
|
||||
name: 'plugin-test-backend-dynamic',
|
||||
platform: 'node',
|
||||
role: 'backend-plugin',
|
||||
version: '0.0.0',
|
||||
},
|
||||
{
|
||||
name: 'plugin-test-dynamic',
|
||||
platform: 'web',
|
||||
role: 'frontend-dynamic-container',
|
||||
version: '0.0.0',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should redact the secret config values of dynamic plugin config schemas in logs', async () => {
|
||||
const mockedTransport = new MockedTransport();
|
||||
await startTestBackend({
|
||||
features: [
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
dynamicPlugins: {
|
||||
rootDirectory: dynamicPluginsRootDirectory,
|
||||
},
|
||||
'test-backend': {
|
||||
secretValue: 'AVerySecretValue',
|
||||
},
|
||||
},
|
||||
}),
|
||||
dynamicPluginsFeatureLoader({
|
||||
moduleLoader: jestFreeTypescriptAwareModuleLoader,
|
||||
transports: [mockedTransport],
|
||||
format: winston.format.simple(),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
expect(mockedTransport.logs).toContainEqual(
|
||||
'info: Found 1 new secrets in config that will be redacted {"service":"backstage"}',
|
||||
);
|
||||
|
||||
expect(mockedTransport.logs).toContainEqual(
|
||||
'info: This secret value should be hidden by the dynamic-plugin-aware logger: *** {"service":"backstage"}',
|
||||
);
|
||||
});
|
||||
|
||||
const mockAppDir = createMockDirectory();
|
||||
overridePackagePathResolution({
|
||||
packageName: 'app',
|
||||
path: mockAppDir.path,
|
||||
});
|
||||
|
||||
it('should inject frontend config values of dynamic frontend plugin config schemas to the frontend application', async () => {
|
||||
mockAppDir.setContent({
|
||||
'package.json': '{}',
|
||||
dist: {
|
||||
static: {},
|
||||
'index.html.tmpl': '<head></head>',
|
||||
'.config-schema.json': `
|
||||
{
|
||||
"backstageConfigSchemaVersion": 1,
|
||||
"schemas": []
|
||||
}
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
dynamicPlugins: {
|
||||
rootDirectory: dynamicPluginsRootDirectory,
|
||||
},
|
||||
'test-frontend': {
|
||||
frontendValue: 'AFrontendValue',
|
||||
},
|
||||
},
|
||||
}),
|
||||
dynamicPluginsFeatureLoader({
|
||||
moduleLoader: jestFreeTypescriptAwareModuleLoader,
|
||||
}),
|
||||
import('@backstage/plugin-app-backend'),
|
||||
],
|
||||
});
|
||||
|
||||
await expect(
|
||||
fetch(`http://localhost:${server.port()}`).then(res => res.text()),
|
||||
).resolves.toBe(`<head>
|
||||
<script type="backstage.io/config">
|
||||
[
|
||||
{
|
||||
"context": "app",
|
||||
"deprecatedKeys": [],
|
||||
"data": {
|
||||
"test-frontend": {
|
||||
"frontendValue": "AFrontendValue"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
</script>
|
||||
</head>`);
|
||||
});
|
||||
|
||||
it('should access the module federation assets of the frontend plugin through the backend plugin', async () => {
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
dynamicPlugins: {
|
||||
rootDirectory: dynamicPluginsRootDirectory,
|
||||
},
|
||||
},
|
||||
}),
|
||||
dynamicPluginsFeatureLoader({
|
||||
moduleLoader: jestFreeTypescriptAwareModuleLoader,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const list = await fetch(
|
||||
`http://localhost:${server.port()}/api/test/frontend-plugins`,
|
||||
);
|
||||
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`,
|
||||
});
|
||||
|
||||
const manifest = await fetch(
|
||||
`http://localhost:${server.port()}/api/test/frontend-plugins/plugin-test-dynamic/mf-manifest.json`,
|
||||
);
|
||||
expect(manifest.ok).toBe(true);
|
||||
expect(await manifest.json()).toMatchObject({
|
||||
exposes: [],
|
||||
id: 'backstage__plugin_test',
|
||||
name: 'backstage__plugin_test',
|
||||
metaData: {
|
||||
buildInfo: {
|
||||
buildName: '@backstage/plugin-test',
|
||||
buildVersion: '0.0.0',
|
||||
},
|
||||
globalName: 'backstage__plugin_test',
|
||||
name: 'backstage__plugin_test',
|
||||
pluginVersion: '0.0.0',
|
||||
publicPath: 'auto',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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,
|
||||
createBackendFeatureLoader,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
DynamicPluginsSchemasOptions,
|
||||
dynamicPluginsFrontendSchemas,
|
||||
dynamicPluginsRootLoggerServiceFactory,
|
||||
dynamicPluginsSchemasServiceFactory,
|
||||
} from '../schemas';
|
||||
import {
|
||||
DynamicPluginsFactoryOptions,
|
||||
dynamicPluginsFeatureDiscoveryLoader,
|
||||
dynamicPluginsServiceFactory,
|
||||
} from '../manager';
|
||||
import { DynamicPluginsRootLoggerFactoryOptions } from '../schemas';
|
||||
import { configKey } from '../scanner/plugin-scanner';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type DynamicPluginsFeatureLoaderOptions = DynamicPluginsFactoryOptions &
|
||||
DynamicPluginsSchemasOptions &
|
||||
DynamicPluginsRootLoggerFactoryOptions;
|
||||
|
||||
const dynamicPluginsFeatureLoaderWithOptions = (
|
||||
options?: DynamicPluginsFeatureLoaderOptions,
|
||||
) =>
|
||||
createBackendFeatureLoader({
|
||||
deps: {
|
||||
config: coreServices.rootConfig,
|
||||
},
|
||||
*loader({ config }) {
|
||||
const dynamicPluginsEnabled = config.has(configKey);
|
||||
|
||||
yield* [
|
||||
dynamicPluginsSchemasServiceFactory(options),
|
||||
dynamicPluginsServiceFactory(options),
|
||||
];
|
||||
if (dynamicPluginsEnabled) {
|
||||
yield* [
|
||||
dynamicPluginsRootLoggerServiceFactory(options),
|
||||
dynamicPluginsFrontendSchemas,
|
||||
dynamicPluginsFeatureDiscoveryLoader,
|
||||
];
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* A backend feature loader that fully enable backend dynamic plugins.
|
||||
* More precisely it:
|
||||
* - adds the dynamic plugins root service (typically depended upon by plugins),
|
||||
* - adds additional required features to allow supporting dynamic plugins config schemas
|
||||
* in the frontend application and the backend root logger,
|
||||
* - uses the dynamic plugins service to discover and expose dynamic plugins as features.
|
||||
*
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* Using the `dynamicPluginsFeatureLoader` loader in a backend instance:
|
||||
* ```ts
|
||||
* //...
|
||||
* import { createBackend } from '@backstage/backend-defaults';
|
||||
* import { dynamicPluginsFeatureLoader } from '@backstage/backend-dynamic-feature-service';
|
||||
*
|
||||
* const backend = createBackend();
|
||||
* backend.add(dynamicPluginsFeatureLoader);
|
||||
* //...
|
||||
* backend.start();
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Passing options to the `dynamicPluginsFeatureLoader` loader in a backend instance:
|
||||
* ```ts
|
||||
* //...
|
||||
* import { createBackend } from '@backstage/backend-defaults';
|
||||
* import { dynamicPluginsFeatureLoader } from '@backstage/backend-dynamic-feature-service';
|
||||
* import { myCustomModuleLoader } from './myCustomModuleLoader';
|
||||
* import { myCustomSchemaLocator } from './myCustomSchemaLocator';
|
||||
*
|
||||
* const backend = createBackend();
|
||||
* backend.add(dynamicPluginsFeatureLoader({
|
||||
* moduleLoader: myCustomModuleLoader,
|
||||
* schemaLocator: myCustomSchemaLocator,
|
||||
*
|
||||
* }));
|
||||
* //...
|
||||
* backend.start();
|
||||
* ```
|
||||
*/
|
||||
export const dynamicPluginsFeatureLoader = Object.assign(
|
||||
dynamicPluginsFeatureLoaderWithOptions,
|
||||
dynamicPluginsFeatureLoaderWithOptions(),
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './features';
|
||||
@@ -18,3 +18,4 @@ export * from './loader';
|
||||
export * from './scanner';
|
||||
export * from './manager';
|
||||
export * from './schemas';
|
||||
export * from './features';
|
||||
|
||||
@@ -18,7 +18,11 @@ import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import path from 'path';
|
||||
|
||||
export class CommonJSModuleLoader implements ModuleLoader {
|
||||
constructor(public readonly logger: LoggerService) {}
|
||||
private module: any;
|
||||
|
||||
constructor(public readonly logger: LoggerService) {
|
||||
this.module = require('node:module');
|
||||
}
|
||||
|
||||
async bootstrap(
|
||||
backstageRoot: string,
|
||||
@@ -28,9 +32,8 @@ export class CommonJSModuleLoader implements ModuleLoader {
|
||||
const dynamicNodeModulesPaths = [
|
||||
...dynamicPluginsPaths.map(p => path.resolve(p, 'node_modules')),
|
||||
];
|
||||
const Module = require('module');
|
||||
const oldNodeModulePaths = Module._nodeModulePaths;
|
||||
Module._nodeModulePaths = (from: string): string[] => {
|
||||
const oldNodeModulePaths = this.module._nodeModulePaths;
|
||||
this.module._nodeModulePaths = (from: string): string[] => {
|
||||
const result: string[] = oldNodeModulePaths(from);
|
||||
if (!dynamicPluginsPaths.some(p => from.startsWith(p))) {
|
||||
return result;
|
||||
@@ -49,6 +52,6 @@ export class CommonJSModuleLoader implements ModuleLoader {
|
||||
}
|
||||
|
||||
async load(packagePath: string): Promise<any> {
|
||||
return await require(/* webpackIgnore: true */ packagePath);
|
||||
return await this.module.prototype.require(packagePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ import { PluginScanner } from '../scanner/plugin-scanner';
|
||||
import { findPaths } from '@backstage/cli-common';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
import { rootLifecycleServiceFactory } from '@backstage/backend-defaults/rootLifecycle';
|
||||
import { PackageRole } from '@backstage/cli-node';
|
||||
|
||||
describe('backend-dynamic-feature-service', () => {
|
||||
const mockDir = createMockDirectory();
|
||||
@@ -299,6 +300,29 @@ describe('backend-dynamic-feature-service', () => {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should ignore plugin package with incompatible role',
|
||||
packageManifest: {
|
||||
name: 'backend-dynamic-plugin-test',
|
||||
version: '0.0.0',
|
||||
backstage: {
|
||||
role: 'node-library',
|
||||
},
|
||||
main: 'dist/index.cjs.js',
|
||||
},
|
||||
expectedLogs(location) {
|
||||
return {
|
||||
infos: [
|
||||
{
|
||||
message: `skipping dynamic plugin package 'backend-dynamic-plugin-test' from '${location}': incompatible role 'node-library'`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
checkLoadedPlugins(plugins) {
|
||||
expect(plugins).toMatchObject([]);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should fail when no index file',
|
||||
packageManifest: {
|
||||
@@ -342,7 +366,17 @@ describe('backend-dynamic-feature-service', () => {
|
||||
};
|
||||
},
|
||||
checkLoadedPlugins(plugins) {
|
||||
expect(plugins).toMatchObject([]);
|
||||
expect(plugins).toMatchObject([
|
||||
{
|
||||
name: 'backend-dynamic-plugin-test',
|
||||
version: '0.0.0',
|
||||
role: 'backend-plugin',
|
||||
platform: 'node',
|
||||
failure: expect.stringMatching(
|
||||
`^Error: Cannot find module '[^']*' from .*`,
|
||||
),
|
||||
},
|
||||
]);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -369,7 +403,15 @@ describe('backend-dynamic-feature-service', () => {
|
||||
};
|
||||
},
|
||||
checkLoadedPlugins(plugins) {
|
||||
expect(plugins).toMatchObject([]);
|
||||
expect(plugins).toMatchObject([
|
||||
{
|
||||
name: 'backend-dynamic-plugin-test',
|
||||
version: '0.0.0',
|
||||
role: 'backend-plugin',
|
||||
platform: 'node',
|
||||
failure: `the module should either export a 'BackendFeature' or 'BackendFeatureFactory' as default export, or export a 'const dynamicPluginInstaller: BackendDynamicPluginInstaller' field as dynamic loading entrypoint.`,
|
||||
},
|
||||
]);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -397,7 +439,15 @@ describe('backend-dynamic-feature-service', () => {
|
||||
};
|
||||
},
|
||||
checkLoadedPlugins(plugins) {
|
||||
expect(plugins).toMatchObject([]);
|
||||
expect(plugins).toMatchObject([
|
||||
{
|
||||
name: 'backend-dynamic-plugin-test',
|
||||
version: '0.0.0',
|
||||
role: 'backend-plugin',
|
||||
platform: 'node',
|
||||
failure: `the module should either export a 'BackendFeature' or 'BackendFeatureFactory' as default export, or export a 'const dynamicPluginInstaller: BackendDynamicPluginInstaller' field as dynamic loading entrypoint.`,
|
||||
},
|
||||
]);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -428,7 +478,17 @@ describe('backend-dynamic-feature-service', () => {
|
||||
};
|
||||
},
|
||||
checkLoadedPlugins(plugins) {
|
||||
expect(plugins).toMatchObject([]);
|
||||
expect(plugins).toMatchObject([
|
||||
{
|
||||
name: 'backend-dynamic-plugin-test',
|
||||
version: '0.0.0',
|
||||
role: 'backend-plugin',
|
||||
platform: 'node',
|
||||
failure: expect.stringMatching(
|
||||
`^SyntaxError: Unexpected identifier.*`,
|
||||
),
|
||||
},
|
||||
]);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -495,6 +555,27 @@ describe('backend-dynamic-feature-service', () => {
|
||||
]);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should successfully load a frontend plugin (experimental dynamic container)',
|
||||
packageManifest: {
|
||||
name: 'frontend-dynamic-plugin-test',
|
||||
version: '0.0.0',
|
||||
backstage: {
|
||||
role: 'frontend-dynamic-container' as PackageRole,
|
||||
},
|
||||
main: 'dist/index.esm.js',
|
||||
},
|
||||
checkLoadedPlugins(plugins) {
|
||||
expect(plugins).toMatchObject([
|
||||
{
|
||||
name: 'frontend-dynamic-plugin-test',
|
||||
version: '0.0.0',
|
||||
role: 'frontend-dynamic-container',
|
||||
platform: 'web',
|
||||
},
|
||||
]);
|
||||
},
|
||||
},
|
||||
])('$name', async (tc: TestCase): Promise<void> => {
|
||||
const plugin: ScannedPluginPackage = {
|
||||
location: url.pathToFileURL(mockDir.resolve(randomUUID())),
|
||||
@@ -538,33 +619,54 @@ describe('backend-dynamic-feature-service', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('backendPlugins', () => {
|
||||
describe('plugin getters', () => {
|
||||
const plugins: BaseDynamicPlugin[] = [
|
||||
{
|
||||
name: 'a-frontend-plugin',
|
||||
platform: 'web',
|
||||
role: 'frontend-plugin',
|
||||
version: '0.0.0',
|
||||
},
|
||||
{
|
||||
name: 'a-frontend-module',
|
||||
platform: 'web',
|
||||
role: 'frontend-plugin-module',
|
||||
version: '0.0.0',
|
||||
},
|
||||
{
|
||||
name: 'a-failing-frontend-plugin',
|
||||
platform: 'web',
|
||||
role: 'frontend-plugin',
|
||||
version: '0.0.0',
|
||||
failure: 'Some frontend failure',
|
||||
},
|
||||
{
|
||||
name: 'a-backend-plugin',
|
||||
platform: 'node',
|
||||
role: 'backend-plugin',
|
||||
version: '0.0.0',
|
||||
},
|
||||
{
|
||||
name: 'a-backend-module',
|
||||
platform: 'node',
|
||||
role: 'backend-plugin-module',
|
||||
version: '0.0.0',
|
||||
},
|
||||
{
|
||||
name: 'a-failing-backend-plugin',
|
||||
platform: 'node',
|
||||
role: 'backend-plugin',
|
||||
version: '0.0.0',
|
||||
failure: 'Some backend failure',
|
||||
},
|
||||
];
|
||||
|
||||
it('should return only backend plugins and modules', async () => {
|
||||
const logger = new MockedLogger();
|
||||
const pluginManager = new (DynamicPluginManager as any)(
|
||||
logger,
|
||||
[],
|
||||
) as DynamicPluginManager;
|
||||
const plugins: BaseDynamicPlugin[] = [
|
||||
{
|
||||
name: 'a-frontend-plugin',
|
||||
platform: 'web',
|
||||
role: 'frontend-plugin',
|
||||
version: '0.0.0',
|
||||
},
|
||||
{
|
||||
name: 'a-backend-plugin',
|
||||
platform: 'node',
|
||||
role: 'backend-plugin',
|
||||
version: '0.0.0',
|
||||
},
|
||||
{
|
||||
name: 'a-backend-module',
|
||||
platform: 'node',
|
||||
role: 'backend-plugin-module',
|
||||
version: '0.0.0',
|
||||
},
|
||||
];
|
||||
(pluginManager as any)._plugins = plugins;
|
||||
expect(pluginManager.backendPlugins()).toEqual([
|
||||
{
|
||||
@@ -580,17 +682,109 @@ describe('backend-dynamic-feature-service', () => {
|
||||
version: '0.0.0',
|
||||
},
|
||||
]);
|
||||
expect(pluginManager.backendPlugins({ includeFailed: false })).toEqual([
|
||||
{
|
||||
name: 'a-backend-plugin',
|
||||
platform: 'node',
|
||||
role: 'backend-plugin',
|
||||
version: '0.0.0',
|
||||
},
|
||||
{
|
||||
name: 'a-backend-module',
|
||||
platform: 'node',
|
||||
role: 'backend-plugin-module',
|
||||
version: '0.0.0',
|
||||
},
|
||||
]);
|
||||
expect(pluginManager.backendPlugins({ includeFailed: true })).toEqual([
|
||||
{
|
||||
name: 'a-backend-plugin',
|
||||
platform: 'node',
|
||||
role: 'backend-plugin',
|
||||
version: '0.0.0',
|
||||
},
|
||||
{
|
||||
name: 'a-backend-module',
|
||||
platform: 'node',
|
||||
role: 'backend-plugin-module',
|
||||
version: '0.0.0',
|
||||
},
|
||||
{
|
||||
name: 'a-failing-backend-plugin',
|
||||
platform: 'node',
|
||||
role: 'backend-plugin',
|
||||
version: '0.0.0',
|
||||
failure: 'Some backend failure',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('frontendPlugins', () => {
|
||||
it('should return only frontend plugins', async () => {
|
||||
const logger = new MockedLogger();
|
||||
const pluginManager = new (DynamicPluginManager as any)(
|
||||
logger,
|
||||
[],
|
||||
) as DynamicPluginManager;
|
||||
const plugins: BaseDynamicPlugin[] = [
|
||||
(pluginManager as any)._plugins = plugins;
|
||||
expect(pluginManager.frontendPlugins()).toEqual([
|
||||
{
|
||||
name: 'a-frontend-plugin',
|
||||
platform: 'web',
|
||||
role: 'frontend-plugin',
|
||||
version: '0.0.0',
|
||||
},
|
||||
{
|
||||
name: 'a-frontend-module',
|
||||
platform: 'web',
|
||||
role: 'frontend-plugin-module',
|
||||
version: '0.0.0',
|
||||
},
|
||||
]);
|
||||
expect(pluginManager.frontendPlugins({ includeFailed: false })).toEqual([
|
||||
{
|
||||
name: 'a-frontend-plugin',
|
||||
platform: 'web',
|
||||
role: 'frontend-plugin',
|
||||
version: '0.0.0',
|
||||
},
|
||||
{
|
||||
name: 'a-frontend-module',
|
||||
platform: 'web',
|
||||
role: 'frontend-plugin-module',
|
||||
version: '0.0.0',
|
||||
},
|
||||
]);
|
||||
expect(pluginManager.frontendPlugins({ includeFailed: true })).toEqual([
|
||||
{
|
||||
name: 'a-frontend-plugin',
|
||||
platform: 'web',
|
||||
role: 'frontend-plugin',
|
||||
version: '0.0.0',
|
||||
},
|
||||
{
|
||||
name: 'a-frontend-module',
|
||||
platform: 'web',
|
||||
role: 'frontend-plugin-module',
|
||||
version: '0.0.0',
|
||||
},
|
||||
{
|
||||
name: 'a-failing-frontend-plugin',
|
||||
platform: 'web',
|
||||
role: 'frontend-plugin',
|
||||
version: '0.0.0',
|
||||
failure: 'Some frontend failure',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return all plugins', async () => {
|
||||
const logger = new MockedLogger();
|
||||
const pluginManager = new (DynamicPluginManager as any)(
|
||||
logger,
|
||||
[],
|
||||
) as DynamicPluginManager;
|
||||
(pluginManager as any)._plugins = plugins;
|
||||
expect(pluginManager.plugins()).toEqual([
|
||||
{
|
||||
name: 'a-frontend-plugin',
|
||||
platform: 'web',
|
||||
@@ -615,9 +809,8 @@ describe('backend-dynamic-feature-service', () => {
|
||||
role: 'backend-plugin-module',
|
||||
version: '0.0.0',
|
||||
},
|
||||
];
|
||||
(pluginManager as any)._plugins = plugins;
|
||||
expect(pluginManager.frontendPlugins()).toEqual([
|
||||
]);
|
||||
expect(pluginManager.plugins({ includeFailed: false })).toEqual([
|
||||
{
|
||||
name: 'a-frontend-plugin',
|
||||
platform: 'web',
|
||||
@@ -630,7 +823,93 @@ describe('backend-dynamic-feature-service', () => {
|
||||
role: 'frontend-plugin-module',
|
||||
version: '0.0.0',
|
||||
},
|
||||
{
|
||||
name: 'a-backend-plugin',
|
||||
platform: 'node',
|
||||
role: 'backend-plugin',
|
||||
version: '0.0.0',
|
||||
},
|
||||
{
|
||||
name: 'a-backend-module',
|
||||
platform: 'node',
|
||||
role: 'backend-plugin-module',
|
||||
version: '0.0.0',
|
||||
},
|
||||
]);
|
||||
expect(pluginManager.plugins({ includeFailed: true })).toEqual([
|
||||
{
|
||||
name: 'a-frontend-plugin',
|
||||
platform: 'web',
|
||||
role: 'frontend-plugin',
|
||||
version: '0.0.0',
|
||||
},
|
||||
{
|
||||
name: 'a-frontend-module',
|
||||
platform: 'web',
|
||||
role: 'frontend-plugin-module',
|
||||
version: '0.0.0',
|
||||
},
|
||||
{
|
||||
name: 'a-failing-frontend-plugin',
|
||||
platform: 'web',
|
||||
role: 'frontend-plugin',
|
||||
version: '0.0.0',
|
||||
failure: 'Some frontend failure',
|
||||
},
|
||||
{
|
||||
name: 'a-backend-plugin',
|
||||
platform: 'node',
|
||||
role: 'backend-plugin',
|
||||
version: '0.0.0',
|
||||
},
|
||||
{
|
||||
name: 'a-backend-module',
|
||||
platform: 'node',
|
||||
role: 'backend-plugin-module',
|
||||
version: '0.0.0',
|
||||
},
|
||||
{
|
||||
name: 'a-failing-backend-plugin',
|
||||
platform: 'node',
|
||||
role: 'backend-plugin',
|
||||
version: '0.0.0',
|
||||
failure: 'Some backend failure',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get scanned package', () => {
|
||||
it('should return the scanned package of the plugin', async () => {
|
||||
const logger = new MockedLogger();
|
||||
const packageFolder = mockDir.resolve(randomUUID());
|
||||
const scannedPackage = {
|
||||
manifest: {
|
||||
name: 'backend-dynamic-plugin-test',
|
||||
version: '0.0.0',
|
||||
backstage: {
|
||||
role: 'backend-plugin',
|
||||
},
|
||||
main: 'dist/index.cjs.js',
|
||||
},
|
||||
location: url.pathToFileURL(packageFolder),
|
||||
};
|
||||
const plugin = {
|
||||
name: 'backend-dynamic-plugin-test',
|
||||
version: '0.0.0',
|
||||
role: 'backend-plugin',
|
||||
platform: 'node',
|
||||
installer: {
|
||||
kind: 'new',
|
||||
},
|
||||
} as BackendDynamicPlugin;
|
||||
|
||||
const pluginManager = new (DynamicPluginManager as any)(logger, [
|
||||
scannedPackage,
|
||||
]) as DynamicPluginManager;
|
||||
(pluginManager as any)._plugins = [plugin];
|
||||
|
||||
expect(pluginManager.getScannedPackage(plugin)).toEqual(scannedPackage);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
createServiceFactory,
|
||||
createServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { PackageRoles } from '@backstage/cli-node';
|
||||
import { PackageRole, PackageRoles } from '@backstage/cli-node';
|
||||
import { findPaths } from '@backstage/cli-common';
|
||||
import path from 'path';
|
||||
import * as fs from 'fs';
|
||||
@@ -88,7 +88,7 @@ export class DynamicPluginManager implements DynamicPluginProvider {
|
||||
),
|
||||
);
|
||||
|
||||
moduleLoader.bootstrap(backstageRoot, dynamicPluginsPaths);
|
||||
await moduleLoader.bootstrap(backstageRoot, dynamicPluginsPaths);
|
||||
|
||||
scanner.subscribeToRootDirectoryChange(async () => {
|
||||
manager._availablePackages = (await scanner.scanRoot()).packages;
|
||||
@@ -104,7 +104,7 @@ export class DynamicPluginManager implements DynamicPluginProvider {
|
||||
|
||||
private constructor(
|
||||
private readonly logger: LoggerService,
|
||||
private packages: ScannedPluginPackage[],
|
||||
private readonly packages: ScannedPluginPackage[],
|
||||
private readonly moduleLoader: ModuleLoader,
|
||||
) {
|
||||
this._plugins = [];
|
||||
@@ -123,26 +123,39 @@ export class DynamicPluginManager implements DynamicPluginProvider {
|
||||
const loadedPlugins: DynamicPlugin[] = [];
|
||||
|
||||
for (const scannedPlugin of this.packages) {
|
||||
const platform = PackageRoles.getRoleInfo(
|
||||
scannedPlugin.manifest.backstage.role,
|
||||
).platform;
|
||||
const role = scannedPlugin.manifest.backstage.role;
|
||||
const platform = PackageRoles.getRoleInfo(role).platform;
|
||||
const isPlugin =
|
||||
role.endsWith('-plugin') ||
|
||||
role.endsWith('-plugin-module') ||
|
||||
role === ('frontend-dynamic-container' as PackageRole);
|
||||
|
||||
if (
|
||||
platform === 'node' &&
|
||||
scannedPlugin.manifest.backstage.role.includes('-plugin')
|
||||
) {
|
||||
const plugin = await this.loadBackendPlugin(scannedPlugin);
|
||||
if (plugin !== undefined) {
|
||||
loadedPlugins.push(plugin);
|
||||
}
|
||||
} else {
|
||||
loadedPlugins.push({
|
||||
name: scannedPlugin.manifest.name,
|
||||
version: scannedPlugin.manifest.version,
|
||||
role: scannedPlugin.manifest.backstage.role,
|
||||
platform: 'web',
|
||||
// TODO(davidfestal): add required front-end plugin information here.
|
||||
});
|
||||
if (!isPlugin) {
|
||||
this.logger.info(
|
||||
`skipping dynamic plugin package '${scannedPlugin.manifest.name}' from '${scannedPlugin.location}': incompatible role '${role}'`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (platform) {
|
||||
case 'node':
|
||||
loadedPlugins.push(await this.loadBackendPlugin(scannedPlugin));
|
||||
break;
|
||||
|
||||
case 'web':
|
||||
loadedPlugins.push({
|
||||
name: scannedPlugin.manifest.name,
|
||||
version: scannedPlugin.manifest.version,
|
||||
role: scannedPlugin.manifest.backstage.role,
|
||||
platform: 'web',
|
||||
// TODO(davidfestal): add required front-end plugin information here.
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
this.logger.info(
|
||||
`skipping dynamic plugin package '${scannedPlugin.manifest.name}' from '${scannedPlugin.location}': unrelated platform '${platform}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return loadedPlugins;
|
||||
@@ -150,72 +163,97 @@ export class DynamicPluginManager implements DynamicPluginProvider {
|
||||
|
||||
private async loadBackendPlugin(
|
||||
plugin: ScannedPluginPackage,
|
||||
): Promise<BackendDynamicPlugin | undefined> {
|
||||
): Promise<BackendDynamicPlugin> {
|
||||
const packagePath = url.fileURLToPath(
|
||||
`${plugin.location}/${plugin.manifest.main}`,
|
||||
);
|
||||
const dynamicPlugin: BackendDynamicPlugin = {
|
||||
name: plugin.manifest.name,
|
||||
version: plugin.manifest.version,
|
||||
platform: 'node',
|
||||
role: plugin.manifest.backstage.role,
|
||||
};
|
||||
|
||||
try {
|
||||
const pluginModule = await this.moduleLoader.load(packagePath);
|
||||
|
||||
let dynamicPluginInstaller;
|
||||
if (isBackendFeature(pluginModule.default)) {
|
||||
dynamicPluginInstaller = {
|
||||
dynamicPlugin.installer = {
|
||||
kind: 'new',
|
||||
install: () => pluginModule.default,
|
||||
};
|
||||
} else if (isBackendFeatureFactory(pluginModule.default)) {
|
||||
dynamicPluginInstaller = {
|
||||
dynamicPlugin.installer = {
|
||||
kind: 'new',
|
||||
install: pluginModule.default,
|
||||
};
|
||||
} else {
|
||||
dynamicPluginInstaller = pluginModule.dynamicPluginInstaller;
|
||||
} else if (
|
||||
isBackendDynamicPluginInstaller(pluginModule.dynamicPluginInstaller)
|
||||
) {
|
||||
dynamicPlugin.installer = pluginModule.dynamicPluginInstaller;
|
||||
}
|
||||
if (!isBackendDynamicPluginInstaller(dynamicPluginInstaller)) {
|
||||
this.logger.error(
|
||||
`dynamic backend plugin '${plugin.manifest.name}' could not be loaded from '${plugin.location}': the module should either export a 'BackendFeature' or 'BackendFeatureFactory' as default export, or export a 'const dynamicPluginInstaller: BackendDynamicPluginInstaller' field as dynamic loading entrypoint.`,
|
||||
if (dynamicPlugin.installer) {
|
||||
this.logger.info(
|
||||
`loaded dynamic backend plugin '${plugin.manifest.name}' from '${plugin.location}'`,
|
||||
);
|
||||
} else {
|
||||
dynamicPlugin.failure = `the module should either export a 'BackendFeature' or 'BackendFeatureFactory' as default export, or export a 'const dynamicPluginInstaller: BackendDynamicPluginInstaller' field as dynamic loading entrypoint.`;
|
||||
this.logger.error(
|
||||
`dynamic backend plugin '${plugin.manifest.name}' could not be loaded from '${plugin.location}': ${dynamicPlugin.failure}`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
this.logger.info(
|
||||
`loaded dynamic backend plugin '${plugin.manifest.name}' from '${plugin.location}'`,
|
||||
);
|
||||
return {
|
||||
name: plugin.manifest.name,
|
||||
version: plugin.manifest.version,
|
||||
platform: 'node',
|
||||
role: plugin.manifest.backstage.role,
|
||||
installer: dynamicPluginInstaller,
|
||||
};
|
||||
return dynamicPlugin;
|
||||
} catch (error) {
|
||||
const typedError =
|
||||
typeof error === 'object' && 'message' in error && 'name' in error
|
||||
? error
|
||||
: new Error(error);
|
||||
dynamicPlugin.failure = `${typedError.name}: ${typedError.message}`;
|
||||
this.logger.error(
|
||||
`an error occurred while loading dynamic backend plugin '${plugin.manifest.name}' from '${plugin.location}'`,
|
||||
error,
|
||||
typedError,
|
||||
);
|
||||
return undefined;
|
||||
return dynamicPlugin;
|
||||
}
|
||||
}
|
||||
|
||||
backendPlugins(): BackendDynamicPlugin[] {
|
||||
return this._plugins.filter(
|
||||
backendPlugins(options?: {
|
||||
includeFailed?: boolean;
|
||||
}): BackendDynamicPlugin[] {
|
||||
return this.plugins(options).filter(
|
||||
(p): p is BackendDynamicPlugin => p.platform === 'node',
|
||||
);
|
||||
}
|
||||
|
||||
frontendPlugins(): FrontendDynamicPlugin[] {
|
||||
return this._plugins.filter(
|
||||
frontendPlugins(options?: {
|
||||
includeFailed?: boolean;
|
||||
}): FrontendDynamicPlugin[] {
|
||||
return this.plugins(options).filter(
|
||||
(p): p is FrontendDynamicPlugin => p.platform === 'web',
|
||||
);
|
||||
}
|
||||
|
||||
plugins(): DynamicPlugin[] {
|
||||
return this._plugins;
|
||||
plugins(options?: { includeFailed?: boolean }): DynamicPlugin[] {
|
||||
return this._plugins.filter(p => options?.includeFailed || !p.failure);
|
||||
}
|
||||
|
||||
getScannedPackage(plugin: DynamicPlugin): ScannedPluginPackage {
|
||||
const pkg = this.packages.find(
|
||||
p =>
|
||||
p.manifest.name === plugin.name &&
|
||||
p.manifest.version === plugin.version,
|
||||
);
|
||||
if (pkg === undefined) {
|
||||
throw new Error(
|
||||
`The scanned package of a dynamic plugin should always be available: ${plugin.name}/${plugin.version}`,
|
||||
);
|
||||
}
|
||||
return pkg;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated The `featureDiscoveryService` is deprecated in favor of using {@link dynamicPluginsFeatureDiscoveryLoader} instead.
|
||||
*/
|
||||
export const dynamicPluginsServiceRef = createServiceRef<DynamicPluginProvider>(
|
||||
{
|
||||
@@ -228,12 +266,12 @@ export const dynamicPluginsServiceRef = createServiceRef<DynamicPluginProvider>(
|
||||
* @public
|
||||
*/
|
||||
export interface DynamicPluginsFactoryOptions {
|
||||
moduleLoader?(logger: LoggerService): ModuleLoader;
|
||||
moduleLoader?(logger: LoggerService): ModuleLoader | Promise<ModuleLoader>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Use {@link dynamicPluginsFeatureDiscoveryLoader} instead.
|
||||
* @deprecated Use {@link dynamicPluginsFeatureLoader} instead, which gathers all services and features required for dynamic plugins.
|
||||
*/
|
||||
export const dynamicPluginsServiceFactoryWithOptions = (
|
||||
options?: DynamicPluginsFactoryOptions,
|
||||
@@ -249,17 +287,19 @@ export const dynamicPluginsServiceFactoryWithOptions = (
|
||||
config,
|
||||
logger,
|
||||
preferAlpha: true,
|
||||
moduleLoader: options?.moduleLoader?.(logger),
|
||||
moduleLoader: await options?.moduleLoader?.(logger),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Use {@link dynamicPluginsFeatureDiscoveryLoader} instead.
|
||||
* @deprecated Use {@link dynamicPluginsFeatureLoader} instead, which gathers all services and features required for dynamic plugins.
|
||||
*/
|
||||
export const dynamicPluginsServiceFactory =
|
||||
dynamicPluginsServiceFactoryWithOptions();
|
||||
export const dynamicPluginsServiceFactory = Object.assign(
|
||||
dynamicPluginsServiceFactoryWithOptions,
|
||||
dynamicPluginsServiceFactoryWithOptions(),
|
||||
);
|
||||
|
||||
class DynamicPluginsEnabledFeatureDiscoveryService
|
||||
implements FeatureDiscoveryService
|
||||
@@ -279,7 +319,7 @@ class DynamicPluginsEnabledFeatureDiscoveryService
|
||||
...this.dynamicPlugins
|
||||
.backendPlugins()
|
||||
.flatMap((plugin): BackendFeature[] => {
|
||||
if (plugin.installer.kind === 'new') {
|
||||
if (plugin.installer?.kind === 'new') {
|
||||
const installed = plugin.installer.install();
|
||||
if (Array.isArray(installed)) {
|
||||
return installed;
|
||||
@@ -296,7 +336,7 @@ class DynamicPluginsEnabledFeatureDiscoveryService
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated The `featureDiscoveryService` is deprecated in favor of using {@link dynamicPluginsFeatureDiscoveryLoader} instead.
|
||||
* @deprecated Use {@link dynamicPluginsFeatureLoader} instead, which gathers all services and features required for dynamic plugins.
|
||||
*/
|
||||
export const dynamicPluginsFeatureDiscoveryServiceFactory =
|
||||
createServiceFactory({
|
||||
@@ -310,65 +350,22 @@ export const dynamicPluginsFeatureDiscoveryServiceFactory =
|
||||
},
|
||||
});
|
||||
|
||||
const dynamicPluginsFeatureDiscoveryLoaderWithOptions = (
|
||||
options?: DynamicPluginsFactoryOptions,
|
||||
) =>
|
||||
createBackendFeatureLoader({
|
||||
deps: {
|
||||
config: coreServices.rootConfig,
|
||||
logger: coreServices.rootLogger,
|
||||
},
|
||||
async loader({ config, logger }) {
|
||||
const manager = await DynamicPluginManager.create({
|
||||
config,
|
||||
logger,
|
||||
preferAlpha: true,
|
||||
moduleLoader: options?.moduleLoader?.(logger),
|
||||
});
|
||||
const service = new DynamicPluginsEnabledFeatureDiscoveryService(manager);
|
||||
const { features } = await service.getBackendFeatures();
|
||||
return features;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* A backend feature loader that uses the dynamic plugins system to discover features.
|
||||
*
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* Using the `dynamicPluginsFeatureDiscoveryLoader` loader in a backend instance:
|
||||
* ```ts
|
||||
* //...
|
||||
* import { createBackend } from '@backstage/backend-defaults';
|
||||
* import { dynamicPluginsFeatureDiscoveryLoader } from '@backstage/backend-dynamic-feature-service';
|
||||
*
|
||||
* const backend = createBackend();
|
||||
* backend.add(dynamicPluginsFeatureDiscoveryLoader);
|
||||
* //...
|
||||
* backend.start();
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Passing options to the `dynamicPluginsFeatureDiscoveryLoader` loader in a backend instance:
|
||||
* ```ts
|
||||
* //...
|
||||
* import { createBackend } from '@backstage/backend-defaults';
|
||||
* import { dynamicPluginsFeatureDiscoveryLoader } from '@backstage/backend-dynamic-feature-service';
|
||||
* import { myCustomModuleLoader } from './myCustomModuleLoader';
|
||||
*
|
||||
* const backend = createBackend();
|
||||
* backend.add(dynamicPluginsFeatureDiscoveryLoader({
|
||||
* moduleLoader: myCustomModuleLoader
|
||||
* }));
|
||||
* //...
|
||||
* backend.start();
|
||||
* ```
|
||||
* @deprecated Use {@link dynamicPluginsFeatureLoader} instead, which gathers all services and features required for dynamic plugins.
|
||||
*/
|
||||
export const dynamicPluginsFeatureDiscoveryLoader = Object.assign(
|
||||
dynamicPluginsFeatureDiscoveryLoaderWithOptions,
|
||||
dynamicPluginsFeatureDiscoveryLoaderWithOptions(),
|
||||
);
|
||||
export const dynamicPluginsFeatureDiscoveryLoader = createBackendFeatureLoader({
|
||||
deps: {
|
||||
dynamicPlugins: dynamicPluginsServiceRef,
|
||||
},
|
||||
async loader({ dynamicPlugins }) {
|
||||
const service = new DynamicPluginsEnabledFeatureDiscoveryService(
|
||||
dynamicPlugins,
|
||||
);
|
||||
const { features } = await service.getBackendFeatures();
|
||||
return features;
|
||||
},
|
||||
});
|
||||
|
||||
function isBackendFeature(value: unknown): value is BackendFeature {
|
||||
return (
|
||||
|
||||
@@ -43,6 +43,7 @@ import { TemplateAction } from '@backstage/plugin-scaffolder-node';
|
||||
import { IndexBuilder } from '@backstage/plugin-search-backend-node';
|
||||
import { EventsBackend } from '@backstage/plugin-events-backend';
|
||||
import { PermissionPolicy } from '@backstage/plugin-permission-node';
|
||||
import { ScannedPluginPackage } from '../scanner';
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -78,21 +79,24 @@ export type LegacyPluginEnvironment = {
|
||||
export interface DynamicPluginProvider
|
||||
extends FrontendPluginProvider,
|
||||
BackendPluginProvider {
|
||||
plugins(): DynamicPlugin[];
|
||||
plugins(options?: { includeFailed?: boolean }): DynamicPlugin[];
|
||||
getScannedPackage(plugin: DynamicPlugin): ScannedPluginPackage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface BackendPluginProvider {
|
||||
backendPlugins(): BackendDynamicPlugin[];
|
||||
backendPlugins(options?: { includeFailed?: boolean }): BackendDynamicPlugin[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface FrontendPluginProvider {
|
||||
frontendPlugins(): FrontendDynamicPlugin[];
|
||||
frontendPlugins(options?: {
|
||||
includeFailed?: boolean;
|
||||
}): FrontendDynamicPlugin[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,6 +107,7 @@ export interface BaseDynamicPlugin {
|
||||
version: string;
|
||||
role: PackageRole;
|
||||
platform: PackagePlatform;
|
||||
failure?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,7 +127,7 @@ export interface FrontendDynamicPlugin extends BaseDynamicPlugin {
|
||||
*/
|
||||
export interface BackendDynamicPlugin extends BaseDynamicPlugin {
|
||||
platform: 'node';
|
||||
installer: BackendDynamicPluginInstaller;
|
||||
installer?: BackendDynamicPluginInstaller;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -35,6 +35,8 @@ export interface ScanRootResponse {
|
||||
packages: ScannedPluginPackage[];
|
||||
}
|
||||
|
||||
export const configKey = 'dynamicPlugins';
|
||||
|
||||
export class PluginScanner {
|
||||
private _rootDirectory?: string;
|
||||
private configUnsubscribe?: () => void;
|
||||
@@ -68,27 +70,27 @@ export class PluginScanner {
|
||||
}
|
||||
|
||||
private applyConfig(): void | never {
|
||||
const dynamicPlugins = this.config.getOptional('dynamicPlugins');
|
||||
const dynamicPlugins = this.config.getOptional(configKey);
|
||||
if (!dynamicPlugins) {
|
||||
this.logger.info("'dynamicPlugins' config entry not found.");
|
||||
this.logger.info(`'${configKey}' config entry not found.`);
|
||||
this._rootDirectory = undefined;
|
||||
return;
|
||||
}
|
||||
if (typeof dynamicPlugins !== 'object') {
|
||||
this.logger.warn("'dynamicPlugins' config entry should be an object.");
|
||||
this.logger.warn(`'${configKey}' config entry should be an object.`);
|
||||
this._rootDirectory = undefined;
|
||||
return;
|
||||
}
|
||||
if (!('rootDirectory' in dynamicPlugins)) {
|
||||
this.logger.warn(
|
||||
"'dynamicPlugins' config entry does not contain the 'rootDirectory' field.",
|
||||
`'${configKey}' config entry does not contain the 'rootDirectory' field.`,
|
||||
);
|
||||
this._rootDirectory = undefined;
|
||||
return;
|
||||
}
|
||||
if (typeof dynamicPlugins.rootDirectory !== 'string') {
|
||||
this.logger.warn(
|
||||
"'dynamicPlugins.rootDirectory' config entry should be a string.",
|
||||
`'${configKey}.rootDirectory' config entry should be a string.`,
|
||||
);
|
||||
this._rootDirectory = undefined;
|
||||
return;
|
||||
|
||||
+5
-1
@@ -25,7 +25,10 @@ import {
|
||||
loadCompiledConfigSchema,
|
||||
} from '@backstage/plugin-app-node';
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Use {@link dynamicPluginsFeatureLoader} instead, which gathers all services and features required for dynamic plugins.
|
||||
*/
|
||||
export const dynamicPluginsFrontendSchemas = createBackendModule({
|
||||
pluginId: 'app',
|
||||
moduleId: 'core.dynamicplugins.frontendSchemas',
|
||||
@@ -41,6 +44,7 @@ export const dynamicPluginsFrontendSchemas = createBackendModule({
|
||||
config.getOptionalString('app.packageName') ?? 'app';
|
||||
const appDistDir = resolvePackagePath(appPackageName, 'dist');
|
||||
const compiledConfigSchema = await loadCompiledConfigSchema(appDistDir);
|
||||
// TODO(davidfestal): Add dynamic pliugin config schemas even if the compiled schemas are empty.
|
||||
if (compiledConfigSchema) {
|
||||
configSchemaExtension.setConfigSchema(
|
||||
(await schemas.addDynamicPluginsSchemas(compiledConfigSchema))
|
||||
@@ -16,10 +16,12 @@
|
||||
|
||||
export {
|
||||
dynamicPluginsSchemasServiceFactory,
|
||||
dynamicPluginsSchemasServiceFactoryWithOptions,
|
||||
type DynamicPluginsSchemasService,
|
||||
type DynamicPluginsSchemasOptions,
|
||||
} from './schemas';
|
||||
|
||||
export { dynamicPluginsFrontendSchemas } from './appBackendModule';
|
||||
export { dynamicPluginsRootLoggerServiceFactory } from './rootLoggerServiceFactory';
|
||||
export { dynamicPluginsFrontendSchemas } from './frontend';
|
||||
export {
|
||||
dynamicPluginsRootLoggerServiceFactory,
|
||||
type DynamicPluginsRootLoggerFactoryOptions,
|
||||
} from './rootLogger';
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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 {
|
||||
createServiceFactory,
|
||||
coreServices,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
WinstonLogger,
|
||||
WinstonLoggerOptions,
|
||||
} from '@backstage/backend-defaults/rootLogger';
|
||||
import { createConfigSecretEnumerator } from '@backstage/backend-defaults/rootConfig';
|
||||
import { transports, format } from 'winston';
|
||||
import { loadConfigSchema } from '@backstage/config-loader';
|
||||
import { getPackages } from '@manypkg/get-packages';
|
||||
import { dynamicPluginsSchemasServiceRef } from './schemas';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type DynamicPluginsRootLoggerFactoryOptions = Omit<
|
||||
WinstonLoggerOptions,
|
||||
'meta'
|
||||
>;
|
||||
|
||||
const dynamicPluginsRootLoggerServiceFactoryWithOptions = (
|
||||
options?: DynamicPluginsRootLoggerFactoryOptions,
|
||||
) =>
|
||||
createServiceFactory({
|
||||
service: coreServices.rootLogger,
|
||||
deps: {
|
||||
config: coreServices.rootConfig,
|
||||
schemas: dynamicPluginsSchemasServiceRef,
|
||||
},
|
||||
async factory({ config, schemas }) {
|
||||
const logger = WinstonLogger.create({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
format:
|
||||
process.env.NODE_ENV === 'production'
|
||||
? format.json()
|
||||
: WinstonLogger.colorFormat(),
|
||||
transports: [new transports.Console()],
|
||||
...options,
|
||||
meta: {
|
||||
service: 'backstage',
|
||||
},
|
||||
});
|
||||
|
||||
const configSchema = await loadConfigSchema({
|
||||
dependencies: (
|
||||
await getPackages(process.cwd())
|
||||
).packages.map(p => p.packageJson.name),
|
||||
});
|
||||
|
||||
const secretEnumerator = await createConfigSecretEnumerator({
|
||||
logger,
|
||||
schema: (await schemas.addDynamicPluginsSchemas(configSchema)).schema,
|
||||
});
|
||||
logger.addRedactions(secretEnumerator(config));
|
||||
config.subscribe?.(() => logger.addRedactions(secretEnumerator(config)));
|
||||
|
||||
return logger;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Use {@link dynamicPluginsFeatureLoader} instead, which gathers all services and features required for dynamic plugins.
|
||||
*/
|
||||
export const dynamicPluginsRootLoggerServiceFactory = Object.assign(
|
||||
dynamicPluginsRootLoggerServiceFactoryWithOptions,
|
||||
dynamicPluginsRootLoggerServiceFactoryWithOptions(),
|
||||
);
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* 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 {
|
||||
createServiceFactory,
|
||||
coreServices,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { WinstonLogger } from '@backstage/backend-defaults/rootLogger';
|
||||
import { transports, format } from 'winston';
|
||||
import { createConfigSecretEnumerator } from '@backstage/backend-common';
|
||||
import { loadConfigSchema } from '@backstage/config-loader';
|
||||
import { getPackages } from '@manypkg/get-packages';
|
||||
import { dynamicPluginsSchemasServiceRef } from './schemas';
|
||||
|
||||
/** @public */
|
||||
export const dynamicPluginsRootLoggerServiceFactory = createServiceFactory({
|
||||
service: coreServices.rootLogger,
|
||||
deps: {
|
||||
config: coreServices.rootConfig,
|
||||
schemas: dynamicPluginsSchemasServiceRef,
|
||||
},
|
||||
async factory({ config, schemas }) {
|
||||
const logger = WinstonLogger.create({
|
||||
meta: {
|
||||
service: 'backstage',
|
||||
},
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
format:
|
||||
process.env.NODE_ENV === 'production'
|
||||
? format.json()
|
||||
: WinstonLogger.colorFormat(),
|
||||
transports: [new transports.Console()],
|
||||
});
|
||||
|
||||
const configSchema = await loadConfigSchema({
|
||||
dependencies: (
|
||||
await getPackages(process.cwd())
|
||||
).packages.map(p => p.packageJson.name),
|
||||
});
|
||||
|
||||
const secretEnumerator = await createConfigSecretEnumerator({
|
||||
logger,
|
||||
schema: (await schemas.addDynamicPluginsSchemas(configSchema)).schema,
|
||||
});
|
||||
logger.addRedactions(secretEnumerator(config));
|
||||
config.subscribe?.(() => logger.addRedactions(secretEnumerator(config)));
|
||||
|
||||
return logger;
|
||||
},
|
||||
});
|
||||
@@ -30,6 +30,7 @@ import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { PluginScanner } from '../scanner/plugin-scanner';
|
||||
import { ConfigSchema, loadConfigSchema } from '@backstage/config-loader';
|
||||
import { dynamicPluginsFeatureLoader } from '../features';
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -68,10 +69,7 @@ export interface DynamicPluginsSchemasOptions {
|
||||
schemaLocator?: (pluginPackage: ScannedPluginPackage) => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const dynamicPluginsSchemasServiceFactoryWithOptions = (
|
||||
const dynamicPluginsSchemasServiceFactoryWithOptions = (
|
||||
options?: DynamicPluginsSchemasOptions,
|
||||
) =>
|
||||
createServiceFactory({
|
||||
@@ -143,9 +141,12 @@ export const dynamicPluginsSchemasServiceFactoryWithOptions = (
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Use {@link dynamicPluginsFeatureLoader} instead, which gathers all services and features required for dynamic plugins.
|
||||
*/
|
||||
export const dynamicPluginsSchemasServiceFactory =
|
||||
dynamicPluginsSchemasServiceFactoryWithOptions();
|
||||
export const dynamicPluginsSchemasServiceFactory = Object.assign(
|
||||
dynamicPluginsSchemasServiceFactoryWithOptions,
|
||||
dynamicPluginsSchemasServiceFactoryWithOptions(),
|
||||
);
|
||||
|
||||
/** @internal */
|
||||
async function gatherDynamicPluginsSchemas(
|
||||
|
||||
@@ -349,8 +349,11 @@ class MockDirectoryImpl {
|
||||
*/
|
||||
export interface CreateMockDirectoryOptions {
|
||||
/**
|
||||
* In addition to creating a temporary directory, also mock `os.tmpdir()` to return the
|
||||
* mock directory path until the end of the test suite.
|
||||
* In addition to creating a temporary directory, also mock `os.tmpdir()` to
|
||||
* return the mock directory path until the end of the test suite.
|
||||
*
|
||||
* When this option is provided the `createMockDirectory` call must happen in
|
||||
* a scope where calling `afterAll` from Jest is allowed
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
@@ -362,6 +365,34 @@ export interface CreateMockDirectoryOptions {
|
||||
content?: MockDirectoryContent;
|
||||
}
|
||||
|
||||
const cleanupCallbacks = new Array<() => void>();
|
||||
|
||||
let registered = false;
|
||||
function registerTestHooks() {
|
||||
if (typeof afterAll !== 'function') {
|
||||
return;
|
||||
}
|
||||
if (registered) {
|
||||
return;
|
||||
}
|
||||
registered = true;
|
||||
|
||||
afterAll(async () => {
|
||||
for (const callback of cleanupCallbacks) {
|
||||
try {
|
||||
callback();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to clean up mock directory after tests, ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
cleanupCallbacks.length = 0;
|
||||
});
|
||||
}
|
||||
|
||||
registerTestHooks();
|
||||
|
||||
/**
|
||||
* Creates a new temporary mock directory that will be removed after the tests have completed.
|
||||
*
|
||||
@@ -410,17 +441,14 @@ export function createMockDirectory(
|
||||
process.on('beforeExit', mocker.remove);
|
||||
}
|
||||
|
||||
try {
|
||||
if (needsCleanup) {
|
||||
cleanupCallbacks.push(() => mocker.remove());
|
||||
}
|
||||
|
||||
if (origTmpdir) {
|
||||
afterAll(() => {
|
||||
if (origTmpdir) {
|
||||
os.tmpdir = origTmpdir;
|
||||
}
|
||||
if (needsCleanup) {
|
||||
mocker.remove();
|
||||
}
|
||||
os.tmpdir = origTmpdir;
|
||||
});
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
if (options?.content) {
|
||||
|
||||
@@ -23,37 +23,37 @@ const backend = createBackend();
|
||||
// access root-scoped services by adding `deps`.
|
||||
const searchLoader = createBackendFeatureLoader({
|
||||
*loader() {
|
||||
yield import('@backstage/plugin-search-backend/alpha');
|
||||
yield import('@backstage/plugin-search-backend-module-catalog/alpha');
|
||||
yield import('@backstage/plugin-search-backend-module-explore/alpha');
|
||||
yield import('@backstage/plugin-search-backend-module-techdocs/alpha');
|
||||
yield import('@backstage/plugin-search-backend');
|
||||
yield import('@backstage/plugin-search-backend-module-catalog');
|
||||
yield import('@backstage/plugin-search-backend-module-explore');
|
||||
yield import('@backstage/plugin-search-backend-module-techdocs');
|
||||
},
|
||||
});
|
||||
|
||||
backend.add(import('@backstage/plugin-auth-backend'));
|
||||
backend.add(import('./authModuleGithubProvider'));
|
||||
backend.add(import('@backstage/plugin-auth-backend-module-guest-provider'));
|
||||
backend.add(import('@backstage/plugin-app-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-app-backend'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-unprocessed'));
|
||||
backend.add(
|
||||
import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'),
|
||||
);
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-events-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
backend.add(import('@backstage/plugin-events-backend'));
|
||||
backend.add(import('@backstage/plugin-devtools-backend'));
|
||||
backend.add(import('@backstage/plugin-kubernetes-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-kubernetes-backend'));
|
||||
backend.add(
|
||||
import('@backstage/plugin-permission-backend-module-allow-all-policy'),
|
||||
);
|
||||
backend.add(import('@backstage/plugin-permission-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-proxy-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-permission-backend'));
|
||||
backend.add(import('@backstage/plugin-proxy-backend'));
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend'));
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend-module-github'));
|
||||
backend.add(
|
||||
import('@backstage/plugin-catalog-backend-module-backstage-openapi'),
|
||||
);
|
||||
backend.add(searchLoader);
|
||||
backend.add(import('@backstage/plugin-techdocs-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-techdocs-backend'));
|
||||
backend.add(import('@backstage/plugin-signals-backend'));
|
||||
backend.add(import('@backstage/plugin-notifications-backend'));
|
||||
|
||||
|
||||
@@ -163,11 +163,18 @@
|
||||
"@backstage/backend-common": "^0.25.0",
|
||||
"@backstage/backend-plugin-api": "workspace:^",
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/catalog-client": "workspace:^",
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/core-app-api": "workspace:^",
|
||||
"@backstage/core-components": "workspace:^",
|
||||
"@backstage/core-plugin-api": "workspace:^",
|
||||
"@backstage/dev-utils": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/plugin-auth-backend": "workspace:^",
|
||||
"@backstage/plugin-auth-backend-module-guest-provider": "workspace:^",
|
||||
"@backstage/plugin-catalog-node": "workspace:^",
|
||||
"@backstage/plugin-scaffolder-node": "workspace:^",
|
||||
"@backstage/plugin-scaffolder-node-test-utils": "workspace:^",
|
||||
"@backstage/test-utils": "workspace:^",
|
||||
"@backstage/theme": "workspace:^",
|
||||
"@rspack/core": "^1.0.10",
|
||||
|
||||
@@ -172,7 +172,11 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
}
|
||||
|
||||
// Run in watch mode unless in CI, coverage mode, or running all tests
|
||||
if (!process.env.CI && !hasFlags('--coverage', '--watch', '--watchAll')) {
|
||||
if (
|
||||
!opts.since &&
|
||||
!process.env.CI &&
|
||||
!hasFlags('--coverage', '--watch', '--watchAll')
|
||||
) {
|
||||
const isGitRepo = () =>
|
||||
runCheck('git', 'rev-parse', '--is-inside-work-tree');
|
||||
const isMercurialRepo = () => runCheck('hg', '--cwd', '.', 'root');
|
||||
@@ -217,6 +221,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
return packageGraph;
|
||||
}
|
||||
|
||||
let selectedProjects: string[] | undefined = undefined;
|
||||
if (opts.since && !hasFlags('--selectProjects')) {
|
||||
const graph = await getPackageGraph();
|
||||
const changedPackages = await graph.listChangedPackages({
|
||||
@@ -224,19 +229,19 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
analyzeLockfile: true,
|
||||
});
|
||||
|
||||
const packageNames = Array.from(
|
||||
selectedProjects = Array.from(
|
||||
graph.collectPackageNames(
|
||||
changedPackages.map(pkg => pkg.name),
|
||||
pkg => pkg.allLocalDependents.keys(),
|
||||
),
|
||||
);
|
||||
|
||||
if (packageNames.length === 0) {
|
||||
if (selectedProjects.length === 0) {
|
||||
console.log(`No packages changed since ${opts.since}`);
|
||||
return;
|
||||
}
|
||||
|
||||
args.push('--selectProjects', ...packageNames);
|
||||
args.push('--selectProjects', ...selectedProjects);
|
||||
}
|
||||
|
||||
// This is the only thing that is not implemented by jest.run(), so we do it here instead
|
||||
@@ -346,7 +351,9 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
projectHashes.set(packageName, sha);
|
||||
|
||||
if (cache?.includes(sha)) {
|
||||
console.log(`Skipped ${packageName} due to cache hit`);
|
||||
if (!selectedProjects || selectedProjects.includes(packageName)) {
|
||||
console.log(`Skipped ${packageName} due to cache hit`);
|
||||
}
|
||||
outputSuccessCache.push(sha);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -92,12 +92,16 @@ describe('backendPlugin factory', () => {
|
||||
'templating .eslintrc.js.hbs',
|
||||
'templating README.md.hbs',
|
||||
'templating index.ts.hbs',
|
||||
'templating index.ts.hbs',
|
||||
'templating package.json.hbs',
|
||||
'templating plugin.ts.hbs',
|
||||
'templating plugin.test.ts.hbs',
|
||||
'copying index.ts',
|
||||
'copying setupTests.ts',
|
||||
'copying router.test.ts',
|
||||
'copying router.ts',
|
||||
'templating plugin.ts.hbs',
|
||||
'copying router.test.ts',
|
||||
'copying createTodoListService.ts',
|
||||
'copying types.ts',
|
||||
'Installing:',
|
||||
`moving plugins${sep}test-backend`,
|
||||
'backend adding dependency',
|
||||
|
||||
@@ -93,10 +93,8 @@ describe('scaffolderModule factory', () => {
|
||||
'templating README.md.hbs',
|
||||
'templating package.json.hbs',
|
||||
'templating index.ts.hbs',
|
||||
'copying index.ts',
|
||||
'copying example.test.ts',
|
||||
'copying example.ts',
|
||||
'copying index.ts',
|
||||
'copying module.ts',
|
||||
'Installing:',
|
||||
`moving plugins${sep}scaffolder-backend-module-test`,
|
||||
|
||||
@@ -34,16 +34,20 @@ leaving any imports in place.
|
||||
*/
|
||||
import { version as backendPluginApi } from '../../../../packages/backend-plugin-api/package.json';
|
||||
import { version as backendTestUtils } from '../../../../packages/backend-test-utils/package.json';
|
||||
import { version as catalogClient } from '../../../../packages/catalog-client/package.json';
|
||||
import { version as cli } from '../../../../packages/cli/package.json';
|
||||
import { version as config } from '../../../../packages/config/package.json';
|
||||
import { version as coreAppApi } from '../../../../packages/core-app-api/package.json';
|
||||
import { version as coreComponents } from '../../../../packages/core-components/package.json';
|
||||
import { version as corePluginApi } from '../../../../packages/core-plugin-api/package.json';
|
||||
import { version as devUtils } from '../../../../packages/dev-utils/package.json';
|
||||
import { version as errors } from '../../../../packages/errors/package.json';
|
||||
import { version as testUtils } from '../../../../packages/test-utils/package.json';
|
||||
import { version as scaffolderNode } from '../../../../plugins/scaffolder-node/package.json';
|
||||
import { version as scaffolderNodeTestUtils } from '../../../../plugins/scaffolder-node-test-utils/package.json';
|
||||
import { version as authBackend } from '../../../../plugins/auth-backend/package.json';
|
||||
import { version as authBackendModuleGuestProvider } from '../../../../plugins/auth-backend-module-guest-provider/package.json';
|
||||
import { version as catalogNode } from '../../../../plugins/catalog-node/package.json';
|
||||
import { version as theme } from '../../../../packages/theme/package.json';
|
||||
import { version as backendDefaults } from '../../../../packages/backend-defaults/package.json';
|
||||
|
||||
@@ -51,18 +55,22 @@ export const packageVersions: Record<string, string> = {
|
||||
'@backstage/backend-defaults': backendDefaults,
|
||||
'@backstage/backend-plugin-api': backendPluginApi,
|
||||
'@backstage/backend-test-utils': backendTestUtils,
|
||||
'@backstage/catalog-client': catalogClient,
|
||||
'@backstage/cli': cli,
|
||||
'@backstage/config': config,
|
||||
'@backstage/core-app-api': coreAppApi,
|
||||
'@backstage/core-components': coreComponents,
|
||||
'@backstage/core-plugin-api': corePluginApi,
|
||||
'@backstage/dev-utils': devUtils,
|
||||
'@backstage/errors': errors,
|
||||
'@backstage/test-utils': testUtils,
|
||||
'@backstage/theme': theme,
|
||||
'@backstage/plugin-scaffolder-node': scaffolderNode,
|
||||
'@backstage/plugin-scaffolder-node-test-utils': scaffolderNodeTestUtils,
|
||||
'@backstage/plugin-auth-backend': authBackend,
|
||||
'@backstage/plugin-auth-backend-module-guest-provider':
|
||||
authBackendModuleGuestProvider,
|
||||
'@backstage/plugin-catalog-node': catalogNode,
|
||||
};
|
||||
|
||||
export function findVersion() {
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
# {{id}}
|
||||
|
||||
Welcome to the {{id}} backend plugin!
|
||||
This plugin backend was templated using the Backstage CLI. You should replace this text with a description of your plugin backend.
|
||||
|
||||
_This plugin was created through the Backstage CLI_
|
||||
## Installation
|
||||
|
||||
## Getting started
|
||||
This plugin is installed via the `{{name}}` package. To install it to your backend package, run the following command:
|
||||
|
||||
Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn
|
||||
start` in the root directory, and then navigating to [/{{id}}/health](http://localhost:7007/api/{{id}}/health).
|
||||
```bash
|
||||
# From your root directory
|
||||
yarn --cwd packages/backend add {{name}}
|
||||
```
|
||||
|
||||
You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
|
||||
This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
|
||||
It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory.
|
||||
Then add the plugin to your backend in `packages/backend/src/index.ts`:
|
||||
|
||||
```ts
|
||||
const backend = createBackend();
|
||||
// ...
|
||||
backend.add(import('{{name}}'));
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
This plugin backend can be started in a standalone mode from directly in this
|
||||
package with `yarn start`. It is a limited setup that is most convenient when
|
||||
developing the plugin backend itself.
|
||||
|
||||
If you want to run the entire project, including the frontend, run `yarn dev` from the root directory.
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { createBackend } from '@backstage/backend-defaults';
|
||||
|
||||
const backend = createBackend();
|
||||
|
||||
backend.add(import('@backstage/plugin-auth-backend'));
|
||||
backend.add(import('@backstage/plugin-auth-backend-module-guest-provider'));
|
||||
backend.add(import('../src'));
|
||||
|
||||
backend.start();
|
||||
@@ -0,0 +1,60 @@
|
||||
import { createBackend } from '@backstage/backend-defaults';
|
||||
import { mockServices } from '@backstage/backend-test-utils';
|
||||
import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils';
|
||||
|
||||
// TEMPLATE NOTE:
|
||||
// This is the development setup for your plugin that wires up a
|
||||
// minimal backend that can use both real and mocked plugins and services.
|
||||
//
|
||||
// Start up the backend by running `yarn start` in the package directory.
|
||||
// Once it's up and running, try out the following requests:
|
||||
//
|
||||
// Create a new todo item, standalone or for the sample component:
|
||||
//
|
||||
// curl http://localhost:7007/api/{{id}}/todos -H 'Content-Type: application/json' -d '{"title": "My Todo"}'
|
||||
// curl http://localhost:7007/api/{{id}}/todos -H 'Content-Type: application/json' -d '{"title": "My Todo", "entityRef": "component:default/sample"}'
|
||||
//
|
||||
// List TODOs:
|
||||
//
|
||||
// curl http://localhost:7007/api/{{id}}/todos
|
||||
//
|
||||
// Explicitly make an unauthenticated request, or with service auth:
|
||||
//
|
||||
// curl http://localhost:7007/api/{{id}}/todos -H 'Authorization: Bearer mock-none-token'
|
||||
// curl http://localhost:7007/api/{{id}}/todos -H 'Authorization: Bearer mock-service-token'
|
||||
|
||||
const backend = createBackend();
|
||||
|
||||
// TEMPLATE NOTE:
|
||||
// Mocking the auth and httpAuth service allows you to call your plugin API without
|
||||
// having to authenticate.
|
||||
//
|
||||
// If you want to use real auth, you can install the following instead:
|
||||
// backend.add(import('@backstage/plugin-auth-backend'));
|
||||
// backend.add(import('@backstage/plugin-auth-backend-module-guest-provider'));
|
||||
backend.add(mockServices.auth.factory());
|
||||
backend.add(mockServices.httpAuth.factory());
|
||||
|
||||
// TEMPLATE NOTE:
|
||||
// Rather than using a real catalog you can use a mock with a fixed set of entities.
|
||||
backend.add(
|
||||
catalogServiceMock.factory({
|
||||
entities: [
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'sample',
|
||||
title: 'Sample Component',
|
||||
},
|
||||
spec: {
|
||||
type: 'service',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
backend.add(import('../src'));
|
||||
|
||||
backend.start();
|
||||
@@ -30,19 +30,19 @@
|
||||
"dependencies": {
|
||||
"@backstage/backend-defaults": "{{versionQuery '@backstage/backend-defaults'}}",
|
||||
"@backstage/backend-plugin-api": "{{versionQuery '@backstage/backend-plugin-api'}}",
|
||||
"@backstage/catalog-client": "{{versionQuery '@backstage/catalog-client'}}",
|
||||
"@backstage/errors": "{{versionQuery '@backstage/errors'}}",
|
||||
"@backstage/plugin-catalog-node": "{{versionQuery '@backstage/plugin-catalog-node'}}",
|
||||
"express": "{{versionQuery 'express' '4.17.1'}}",
|
||||
"express-promise-router": "{{versionQuery 'express-promise-router' '4.1.0'}}",
|
||||
"node-fetch": "{{versionQuery 'node-fetch' '2.6.7'}}"
|
||||
"zod": "{{versionQuery 'zod' '3.22.4'}}"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-test-utils": "{{versionQuery '@backstage/backend-test-utils'}}",
|
||||
"@backstage/cli": "{{versionQuery '@backstage/cli'}}",
|
||||
"@backstage/plugin-auth-backend": "{{versionQuery '@backstage/plugin-auth-backend'}}",
|
||||
"@backstage/plugin-auth-backend-module-guest-provider": "{{versionQuery '@backstage/plugin-auth-backend-module-guest-provider'}}",
|
||||
"@types/express": "{{versionQuery '@types/express' '4.17.6'}}",
|
||||
"@types/supertest": "{{versionQuery '@types/supertest' '2.0.12'}}",
|
||||
"supertest": "{{versionQuery 'supertest' '6.2.4'}}",
|
||||
"msw": "{{versionQuery 'msw' '2.3.1'}}"
|
||||
"supertest": "{{versionQuery 'supertest' '6.2.4'}}"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export * from './service/router';
|
||||
export { {{pluginVar}} as default } from './plugin';
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
mockCredentials,
|
||||
startTestBackend,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import { {{pluginVar}} } from './plugin';
|
||||
import request from 'supertest';
|
||||
import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils';
|
||||
|
||||
// TEMPLATE NOTE:
|
||||
// Plugin tests are integration tests for your plugin, ensuring that all pieces
|
||||
// work together end-to-end. You can still mock injected backend services
|
||||
// however, just like anyone who installs your plugin might replace the
|
||||
// services with their own implementations.
|
||||
describe('plugin', () => {
|
||||
it('should create and read TODO items', async () => {
|
||||
const { server } = await startTestBackend({
|
||||
features: [{{pluginVar}}],
|
||||
});
|
||||
|
||||
await request(server).get('/api/{{id}}/todos').expect(200, {
|
||||
items: [],
|
||||
});
|
||||
|
||||
const createRes = await request(server)
|
||||
.post('/api/{{id}}/todos')
|
||||
.send({ title: 'My Todo' });
|
||||
|
||||
expect(createRes.status).toBe(201);
|
||||
expect(createRes.body).toEqual({
|
||||
id: expect.any(String),
|
||||
title: 'My Todo',
|
||||
createdBy: mockCredentials.user().principal.userEntityRef,
|
||||
createdAt: expect.any(String),
|
||||
});
|
||||
|
||||
const createdTodoItem = createRes.body;
|
||||
|
||||
await request(server)
|
||||
.get('/api/{{id}}/todos')
|
||||
.expect(200, {
|
||||
items: [createdTodoItem],
|
||||
});
|
||||
|
||||
await request(server)
|
||||
.get(`/api/{{id}}/todos/${createdTodoItem.id}`)
|
||||
.expect(200, createdTodoItem);
|
||||
});
|
||||
|
||||
it('should create TODO item with catalog information', async () => {
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
{{pluginVar}},
|
||||
catalogServiceMock.factory({
|
||||
entities: [
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'my-component',
|
||||
namespace: 'default',
|
||||
title: 'My Component',
|
||||
},
|
||||
spec: {
|
||||
type: 'service',
|
||||
owner: 'me',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const createRes = await request(server)
|
||||
.post('/api/{{id}}/todos')
|
||||
.send({ title: 'My Todo', entityRef: 'component:default/my-component' });
|
||||
|
||||
expect(createRes.status).toBe(201);
|
||||
expect(createRes.body).toEqual({
|
||||
id: expect.any(String),
|
||||
title: '[My Component] My Todo',
|
||||
createdBy: mockCredentials.user().principal.userEntityRef,
|
||||
createdAt: expect.any(String),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,9 @@ import {
|
||||
coreServices,
|
||||
createBackendPlugin,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { createRouter } from './service/router';
|
||||
import { createRouter } from './router';
|
||||
import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha';
|
||||
import { createTodoListService } from './services/TodoListService';
|
||||
|
||||
/**
|
||||
* {{pluginVar}} backend plugin
|
||||
@@ -14,25 +16,25 @@ export const {{pluginVar}} = createBackendPlugin({
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
httpRouter: coreServices.httpRouter,
|
||||
logger: coreServices.logger,
|
||||
config: coreServices.rootConfig,
|
||||
auth: coreServices.auth,
|
||||
httpAuth: coreServices.httpAuth,
|
||||
httpRouter: coreServices.httpRouter,
|
||||
catalog: catalogServiceRef,
|
||||
},
|
||||
async init({
|
||||
httpRouter,
|
||||
logger,
|
||||
config,
|
||||
}) {
|
||||
async init({ logger, auth, httpAuth, httpRouter, catalog }) {
|
||||
const todoListService = await createTodoListService({
|
||||
logger,
|
||||
auth,
|
||||
catalog,
|
||||
});
|
||||
|
||||
httpRouter.use(
|
||||
await createRouter({
|
||||
logger,
|
||||
config,
|
||||
httpAuth,
|
||||
todoListService,
|
||||
}),
|
||||
);
|
||||
httpRouter.addAuthPolicy({
|
||||
path: '/health',
|
||||
allow: 'unauthenticated',
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
mockCredentials,
|
||||
mockErrorHandler,
|
||||
mockServices,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
|
||||
import { createRouter } from './router';
|
||||
import { TodoListService } from './services/TodoListService/types';
|
||||
|
||||
const mockTodoItem = {
|
||||
title: 'Do the thing',
|
||||
id: '123',
|
||||
createdBy: mockCredentials.user().principal.userEntityRef,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// TEMPLATE NOTE:
|
||||
// Testing the router directly allows you to write a unit test that mocks the provided options.
|
||||
describe('createRouter', () => {
|
||||
let app: express.Express;
|
||||
let todoListService: jest.Mocked<TodoListService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
todoListService = {
|
||||
createTodo: jest.fn(),
|
||||
listTodos: jest.fn(),
|
||||
getTodo: jest.fn(),
|
||||
};
|
||||
const router = await createRouter({
|
||||
httpAuth: mockServices.httpAuth(),
|
||||
todoListService,
|
||||
});
|
||||
app = express();
|
||||
app.use(router);
|
||||
app.use(mockErrorHandler());
|
||||
});
|
||||
|
||||
it('should create a TODO', async () => {
|
||||
todoListService.createTodo.mockResolvedValue(mockTodoItem);
|
||||
|
||||
const response = await request(app).post('/todos').send({
|
||||
title: 'Do the thing',
|
||||
});
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body).toEqual(mockTodoItem);
|
||||
});
|
||||
|
||||
it('should not allow unauthenticated requests to create a TODO', async () => {
|
||||
todoListService.createTodo.mockResolvedValue(mockTodoItem);
|
||||
|
||||
// TEMPLATE NOTE:
|
||||
// The HttpAuth mock service considers all requests to be authenticated as a
|
||||
// mock user by default. In order to test other cases we need to explicitly
|
||||
// pass an authorization header with mock credentials.
|
||||
const response = await request(app)
|
||||
.post('/todos')
|
||||
.set('Authorization', mockCredentials.none.header())
|
||||
.send({
|
||||
title: 'Do the thing',
|
||||
});
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user