Merge branch 'master' of github.com:backstage/backstage into readersprocessorstasks

This commit is contained in:
Brian Fletcher
2022-01-10 14:40:04 +00:00
849 changed files with 15137 additions and 6032 deletions
+44
View File
@@ -1,5 +1,49 @@
# example-app
## 0.2.59
### Patch Changes
- Updated dependencies
- @backstage/cli@0.10.5
- @backstage/plugin-search@0.5.4
- @backstage/plugin-techdocs@0.12.13
- @backstage/core-plugin-api@0.4.1
- @backstage/plugin-catalog-react@0.6.10
- @backstage/plugin-kubernetes@0.5.3
- @backstage/core-app-api@0.3.1
- @backstage/core-components@0.8.3
- @backstage/plugin-apache-airflow@0.1.2
- @backstage/plugin-azure-devops@0.1.9
- @backstage/plugin-badges@0.2.18
- @backstage/plugin-catalog@0.7.7
- @backstage/plugin-catalog-graph@0.2.5
- @backstage/plugin-catalog-import@0.7.8
- @backstage/plugin-circleci@0.2.33
- @backstage/plugin-cloudbuild@0.2.31
- @backstage/plugin-code-coverage@0.1.21
- @backstage/plugin-cost-insights@0.11.16
- @backstage/plugin-explore@0.3.24
- @backstage/plugin-gcp-projects@0.3.12
- @backstage/plugin-github-actions@0.4.30
- @backstage/plugin-graphiql@0.2.26
- @backstage/plugin-home@0.4.9
- @backstage/plugin-jenkins@0.5.16
- @backstage/plugin-kafka@0.2.24
- @backstage/plugin-lighthouse@0.2.33
- @backstage/plugin-newrelic@0.3.12
- @backstage/plugin-newrelic-dashboard@0.1.2
- @backstage/plugin-org@0.3.33
- @backstage/plugin-pagerduty@0.3.21
- @backstage/plugin-rollbar@0.3.22
- @backstage/plugin-scaffolder@0.11.17
- @backstage/plugin-sentry@0.3.32
- @backstage/plugin-shortcuts@0.1.18
- @backstage/plugin-tech-insights@0.1.4
- @backstage/plugin-tech-radar@0.5.1
- @backstage/plugin-user-settings@0.3.15
- @backstage/plugin-api-docs@0.6.21
## 0.2.58
### Patch Changes
+43 -42
View File
@@ -1,60 +1,61 @@
{
"name": "example-app",
"version": "0.2.58",
"version": "0.2.59",
"private": true,
"bundled": true,
"dependencies": {
"@backstage/app-defaults": "^0.1.3",
"@backstage/catalog-model": "^0.9.8",
"@backstage/cli": "^0.10.3",
"@backstage/core-app-api": "^0.3.0",
"@backstage/core-components": "^0.8.2",
"@backstage/core-plugin-api": "^0.4.0",
"@backstage/cli": "^0.10.5",
"@backstage/core-app-api": "^0.3.1",
"@backstage/core-components": "^0.8.3",
"@backstage/core-plugin-api": "^0.4.1",
"@backstage/integration-react": "^0.1.16",
"@backstage/plugin-api-docs": "^0.6.19",
"@backstage/plugin-azure-devops": "^0.1.8",
"@backstage/plugin-apache-airflow": "^0.1.1",
"@backstage/plugin-badges": "^0.2.17",
"@backstage/plugin-catalog": "^0.7.5",
"@backstage/plugin-catalog-graph": "^0.2.4",
"@backstage/plugin-catalog-import": "^0.7.6",
"@backstage/plugin-catalog-react": "^0.6.8",
"@backstage/plugin-circleci": "^0.2.32",
"@backstage/plugin-cloudbuild": "^0.2.30",
"@backstage/plugin-code-coverage": "^0.1.20",
"@backstage/plugin-cost-insights": "^0.11.14",
"@backstage/plugin-explore": "^0.3.23",
"@backstage/plugin-gcp-projects": "^0.3.11",
"@backstage/plugin-github-actions": "^0.4.28",
"@backstage/plugin-graphiql": "^0.2.25",
"@backstage/plugin-home": "^0.4.8",
"@backstage/plugin-jenkins": "^0.5.15",
"@backstage/plugin-kafka": "^0.2.23",
"@backstage/plugin-kubernetes": "^0.5.2",
"@backstage/plugin-lighthouse": "^0.2.32",
"@backstage/plugin-newrelic": "^0.3.11",
"@backstage/plugin-newrelic-dashboard": "^0.1.1",
"@backstage/plugin-org": "^0.3.32",
"@backstage/plugin-pagerduty": "0.3.20",
"@backstage/plugin-rollbar": "^0.3.21",
"@backstage/plugin-scaffolder": "^0.11.15",
"@backstage/plugin-search": "^0.5.2",
"@backstage/plugin-sentry": "^0.3.31",
"@backstage/plugin-shortcuts": "^0.1.16",
"@backstage/plugin-tech-radar": "^0.5.0",
"@backstage/plugin-techdocs": "^0.12.11",
"@backstage/plugin-airbrake": "^0.0.0",
"@backstage/plugin-api-docs": "^0.6.21",
"@backstage/plugin-azure-devops": "^0.1.9",
"@backstage/plugin-apache-airflow": "^0.1.2",
"@backstage/plugin-badges": "^0.2.18",
"@backstage/plugin-catalog": "^0.7.7",
"@backstage/plugin-catalog-graph": "^0.2.5",
"@backstage/plugin-catalog-import": "^0.7.8",
"@backstage/plugin-catalog-react": "^0.6.10",
"@backstage/plugin-circleci": "^0.2.33",
"@backstage/plugin-cloudbuild": "^0.2.31",
"@backstage/plugin-code-coverage": "^0.1.21",
"@backstage/plugin-cost-insights": "^0.11.16",
"@backstage/plugin-explore": "^0.3.24",
"@backstage/plugin-gcp-projects": "^0.3.12",
"@backstage/plugin-github-actions": "^0.4.30",
"@backstage/plugin-graphiql": "^0.2.26",
"@backstage/plugin-home": "^0.4.9",
"@backstage/plugin-jenkins": "^0.5.16",
"@backstage/plugin-kafka": "^0.2.24",
"@backstage/plugin-kubernetes": "^0.5.3",
"@backstage/plugin-lighthouse": "^0.2.33",
"@backstage/plugin-newrelic": "^0.3.12",
"@backstage/plugin-newrelic-dashboard": "^0.1.2",
"@backstage/plugin-org": "^0.3.33",
"@backstage/plugin-pagerduty": "0.3.21",
"@backstage/plugin-rollbar": "^0.3.22",
"@backstage/plugin-scaffolder": "^0.11.17",
"@backstage/plugin-search": "^0.5.4",
"@backstage/plugin-sentry": "^0.3.32",
"@backstage/plugin-shortcuts": "^0.1.18",
"@backstage/plugin-tech-radar": "^0.5.1",
"@backstage/plugin-techdocs": "^0.12.13",
"@backstage/plugin-todo": "^0.1.17",
"@backstage/plugin-user-settings": "^0.3.14",
"@backstage/plugin-user-settings": "^0.3.15",
"@backstage/search-common": "^0.2.0",
"@backstage/plugin-tech-insights": "^0.1.2",
"@backstage/plugin-tech-insights": "^0.1.4",
"@backstage/theme": "^0.2.14",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
"@octokit/rest": "^18.5.3",
"@roadiehq/backstage-plugin-github-insights": "^1.2.2",
"@roadiehq/backstage-plugin-github-pull-requests": "^1.0.13",
"@roadiehq/backstage-plugin-travis-ci": "^1.0.11",
"@roadiehq/backstage-plugin-github-insights": "^1.4.2",
"@roadiehq/backstage-plugin-github-pull-requests": "^1.3.2",
"@roadiehq/backstage-plugin-travis-ci": "^1.3.2",
"history": "^5.0.0",
"prop-types": "^15.7.2",
"react": "^16.13.1",
+1 -6
View File
@@ -42,11 +42,6 @@
href="<%= publicPath %>/safari-pinned-tab.svg"
color="#5bbad5"
/>
<style>
#root {
min-height: 100%;
}
</style>
<title><%= config.getString('app.title') %></title>
<% if (config.has('app.googleAnalyticsTrackingId')) { %>
@@ -103,7 +98,7 @@
<% } %>
</head>
<body style="margin: 0">
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
@@ -14,7 +14,14 @@
* limitations under the License.
*/
import { Content, Header, Lifecycle, Page } from '@backstage/core-components';
import {
CatalogIcon,
Content,
DocsIcon,
Header,
Lifecycle,
Page,
} from '@backstage/core-components';
import { CatalogResultListItem } from '@backstage/plugin-catalog';
import {
DefaultResultListItem,
@@ -39,6 +46,7 @@ const useStyles = makeStyles((theme: Theme) => ({
},
filters: {
padding: theme.spacing(2),
marginTop: theme.spacing(2),
},
}));
@@ -55,12 +63,23 @@ const SearchPage = () => {
</Paper>
</Grid>
<Grid item xs={3}>
<SearchType.Accordion
name="Result Type"
defaultValue="software-catalog"
types={[
{
value: 'software-catalog',
name: 'Software Catalog',
icon: <CatalogIcon />,
},
{
value: 'techdocs',
name: 'Documentation',
icon: <DocsIcon />,
},
]}
/>
<Paper className={classes.filters}>
<SearchType
values={['techdocs', 'software-catalog']}
name="type"
defaultValue="software-catalog"
/>
<SearchFilter.Select
className={classes.filter}
name="kind"
+23
View File
@@ -1,5 +1,28 @@
# @backstage/backend-common
## 0.10.2
### Patch Changes
- 21ae56168e: Updated the Git class with the following:
- Added `depth` and `noCheckout` options to Git clone, using these you can create a bare clone that includes just the git history
- New `log` function which you can use to view the commit history of a git repo
- eacc582473: Reverted the default CSP configuration to include `'unsafe-eval'` again, which was mistakenly removed in the previous version.
## 0.10.1
### Patch Changes
- 94cdf5d1bd: In-memory cache clients instantiated from the same cache manager now share the same memory space.
- 916b2f1f3e: Use the default CSP policy provided by `helmet` directly rather than a copy.
- 7d4b4e937c: Uptake changes to the GitHub Credentials Provider interface.
- 995e4c7d9d: Added support for non-"amazonaws.com" hosts (for example when testing with LocalStack) in AwsS3UrlReader.
- Updated dependencies
- @backstage/integration@0.7.0
- @backstage/config-loader@0.9.1
## 0.10.0
### Minor Changes
+31 -37
View File
@@ -34,9 +34,7 @@ import { Server } from 'http';
import * as winston from 'winston';
import { Writable } from 'stream';
// Warning: (ae-missing-release-tag) "AwsS3UrlReader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public
export class AwsS3UrlReader implements UrlReader {
constructor(
integration: AwsS3Integration,
@@ -59,7 +57,7 @@ export class AwsS3UrlReader implements UrlReader {
toString(): string;
}
// @public (undocumented)
// @public
export class AzureUrlReader implements UrlReader {
constructor(
integration: AzureIntegration,
@@ -114,12 +112,12 @@ export interface CacheClient {
): Promise<void>;
}
// @public (undocumented)
// @public
export type CacheClientOptions = {
defaultTtl?: number;
};
// @public (undocumented)
// @public
export type CacheClientSetOptions = {
ttl?: number;
};
@@ -133,18 +131,17 @@ export class CacheManager {
): CacheManager;
}
// @public (undocumented)
// @public
export type CacheManagerOptions = {
logger?: Logger_2;
onError?: (err: Error) => void;
};
// @public (undocumented)
// @public
export const coloredFormat: winston.Logform.Format;
// @public (undocumented)
// @public
export interface ContainerRunner {
// (undocumented)
runContainer(opts: RunContainerOptions): Promise<void>;
}
@@ -157,7 +154,7 @@ export function createDatabaseClient(
overrides?: Partial<Knex.Config>,
): Knex<any, unknown[]>;
// @public (undocumented)
// @public
export function createRootLogger(
options?: winston.LoggerOptions,
env?: NodeJS.ProcessEnv,
@@ -166,14 +163,14 @@ export function createRootLogger(
// @public
export function createServiceBuilder(_module: NodeModule): ServiceBuilder;
// @public (undocumented)
// @public
export function createStatusCheckRouter(options: {
logger: Logger_2;
path?: string;
statusCheck?: StatusCheck;
}): Promise<express.Router>;
// @public (undocumented)
// @public
export class DatabaseManager {
forPlugin(pluginId: string): PluginDatabaseManager;
static fromConfig(
@@ -187,7 +184,7 @@ export type DatabaseManagerOptions = {
migrations?: PluginDatabaseManager['migrations'];
};
// @public (undocumented)
// @public
export class DockerContainerRunner implements ContainerRunner {
constructor(options: { dockerClient: Docker });
// (undocumented)
@@ -205,7 +202,7 @@ export function errorHandler(
options?: ErrorHandlerOptions,
): ErrorRequestHandler;
// @public (undocumented)
// @public
export type ErrorHandlerOptions = {
showStackTraces?: boolean;
logger?: Logger_2;
@@ -218,13 +215,13 @@ export type FromReadableArrayOptions = Array<{
path: string;
}>;
// @public (undocumented)
// @public
export function getRootLogger(): winston.Logger;
// @public
export function getVoidLogger(): winston.Logger;
// @public (undocumented)
// @public
export class Git {
// (undocumented)
add(options: { dir: string; filepath: string }): Promise<void>;
@@ -234,8 +231,13 @@ export class Git {
remote: string;
url: string;
}): Promise<void>;
// (undocumented)
clone(options: { url: string; dir: string; ref?: string }): Promise<void>;
clone(options: {
url: string;
dir: string;
ref?: string;
depth?: number;
noCheckout?: boolean;
}): Promise<void>;
// (undocumented)
commit(options: {
dir: string;
@@ -249,12 +251,10 @@ export class Git {
email: string;
};
}): Promise<string>;
// (undocumented)
currentBranch(options: {
dir: string;
fullName?: boolean;
}): Promise<string | undefined>;
// (undocumented)
fetch(options: { dir: string; remote?: string }): Promise<void>;
// (undocumented)
static fromAuth: (options: {
@@ -264,7 +264,7 @@ export class Git {
}) => Git;
// (undocumented)
init(options: { dir: string; defaultBranch?: string }): Promise<void>;
// (undocumented)
log(options: { dir: string; ref?: string }): Promise<ReadCommitResult[]>;
merge(options: {
dir: string;
theirs: string;
@@ -280,9 +280,7 @@ export class Git {
}): Promise<MergeResult>;
// (undocumented)
push(options: { dir: string; remote: string }): Promise<PushResult>;
// (undocumented)
readCommit(options: { dir: string; sha: string }): Promise<ReadCommitResult>;
// (undocumented)
resolveRef(options: { dir: string; ref: string }): Promise<string>;
}
@@ -309,7 +307,7 @@ export class GithubUrlReader implements UrlReader {
toString(): string;
}
// @public (undocumented)
// @public
export class GitlabUrlReader implements UrlReader {
constructor(
integration: GitLabIntegration,
@@ -397,7 +395,7 @@ export type ReadTreeResponseDirOptions = {
targetDir?: string;
};
// @public (undocumented)
// @public
export interface ReadTreeResponseFactory {
// (undocumented)
fromReadableArray(
@@ -447,7 +445,7 @@ export type ReadUrlResponse = {
// @public
export function requestLoggingHandler(logger?: Logger_2): RequestHandler;
// @public (undocumented)
// @public
export type RequestLoggingHandlerFactory = (
logger?: Logger_2,
) => RequestHandler;
@@ -458,7 +456,7 @@ export function resolvePackagePath(name: string, ...paths: string[]): string;
// @public
export function resolveSafeChildPath(base: string, path: string): string;
// @public (undocumented)
// @public
export type RunContainerOptions = {
imageName: string;
command?: string | string[];
@@ -507,7 +505,7 @@ export class ServerTokenManager implements TokenManager {
static noop(): TokenManager;
}
// @public (undocumented)
// @public
export type ServiceBuilder = {
loadConfig(config: Config): ServiceBuilder;
setPort(port: number): ServiceBuilder;
@@ -533,7 +531,7 @@ export type ServiceBuilder = {
start(): Promise<Server>;
};
// @public (undocumented)
// @public
export function setRootLogger(newLogger: winston.Logger): void;
// @public @deprecated
@@ -553,7 +551,7 @@ export class SingleHostDiscovery implements PluginEndpointDiscovery {
getExternalBaseUrl(pluginId: string): Promise<string>;
}
// @public (undocumented)
// @public
export type StatusCheck = () => Promise<any>;
// @public
@@ -561,7 +559,7 @@ export function statusCheckHandler(
options?: StatusCheckHandlerOptions,
): Promise<RequestHandler>;
// @public (undocumented)
// @public
export interface StatusCheckHandlerOptions {
statusCheck?: StatusCheck;
}
@@ -596,7 +594,7 @@ export class UrlReaders {
static default(options: UrlReadersOptions): UrlReader;
}
// @public (undocumented)
// @public
export type UrlReadersOptions = {
config: Config;
logger: Logger_2;
@@ -611,8 +609,4 @@ export function useHotCleanup(
// @public
export function useHotMemoize<T>(_module: NodeModule, valueFactory: () => T): T;
// Warnings were encountered during analysis:
//
// src/database/types.d.ts:23:12 - (tsdoc-undefined-tag) The TSDoc tag "@default" is not defined in this configuration
```
+7 -7
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
"version": "0.10.0",
"version": "0.10.2",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -31,12 +31,12 @@
"dependencies": {
"@backstage/cli-common": "^0.1.6",
"@backstage/config": "^0.1.11",
"@backstage/config-loader": "^0.9.0",
"@backstage/config-loader": "^0.9.1",
"@backstage/errors": "^0.1.5",
"@backstage/integration": "^0.6.10",
"@backstage/integration": "^0.7.0",
"@backstage/types": "^0.1.1",
"@google-cloud/storage": "^5.8.0",
"@lerna/project": "^4.0.0",
"@manypkg/get-packages": "^1.1.3",
"@octokit/rest": "^18.5.3",
"@types/cors": "^2.8.6",
"@types/dockerode": "^3.3.0",
@@ -58,7 +58,7 @@
"keyv-memcache": "^1.2.5",
"knex": "^0.95.1",
"lodash": "^4.17.21",
"logform": "^2.1.1",
"logform": "^2.3.2",
"minimatch": "^3.0.4",
"minimist": "^1.2.5",
"morgan": "^1.10.0",
@@ -81,8 +81,8 @@
}
},
"devDependencies": {
"@backstage/cli": "^0.10.3",
"@backstage/test-utils": "^0.2.0",
"@backstage/cli": "^0.10.5",
"@backstage/test-utils": "^0.2.1",
"@types/archiver": "^5.1.0",
"@types/compression": "^1.7.0",
"@types/concat-stream": "^1.6.0",
+5 -1
View File
@@ -22,7 +22,11 @@ type CacheClientArgs = {
client: Keyv;
};
/** @public */
/**
* Options passed to {@link CacheClient.set}.
*
* @public
*/
export type CacheClientSetOptions = {
/**
* Optional TTL in milliseconds. Defaults to the TTL provided when the client
+16
View File
@@ -147,6 +147,22 @@ describe('CacheManager', () => {
});
});
it('shares memory across multiple instances of the memory client', () => {
const manager = CacheManager.fromConfig(defaultConfig());
const plugin = 'test-plugin';
// Instantiate two in-memory clients.
manager.forPlugin(plugin).getClient({ defaultTtl: 10 });
manager.forPlugin(plugin).getClient({ defaultTtl: 10 });
const cache = Keyv as unknown as jest.Mock;
const mockCall2 = cache.mock.calls.splice(-1)[0][0];
const mockCall1 = cache.mock.calls.splice(-1)[0][0];
// Note: .toBe() checks referential identity of object instances.
expect(mockCall1.store).toBe(mockCall2.store);
});
it('returns a memcache client when configured', () => {
const expectedHost = '127.0.0.1:11211';
const manager = CacheManager.fromConfig(
+12 -3
View File
@@ -42,14 +42,21 @@ export class CacheManager {
none: this.getNoneClient,
};
/**
* Shared memory store for the in-memory cache client. Sharing the same Map
* instance ensures get/set/delete operations hit the same store, regardless
* of where/when a client is instantiated.
*/
private readonly memoryStore = new Map();
private readonly logger: Logger;
private readonly store: keyof CacheManager['storeFactories'];
private readonly connection: string;
private readonly errorHandler: CacheManagerOptions['onError'];
/**
* Creates a new CacheManager instance by reading from the `backend` config
* section, specifically the `.cache` key.
* Creates a new {@link CacheManager} instance by reading from the `backend`
* config section, specifically the `.cache` key.
*
* @param config - The loaded application configuration.
*/
@@ -86,7 +93,8 @@ export class CacheManager {
/**
* Generates a PluginCacheManager for consumption by plugins.
*
* @param pluginId - The plugin that the cache manager should be created for. Plugin names should be unique.
* @param pluginId - The plugin that the cache manager should be created for.
* Plugin names should be unique.
*/
forPlugin(pluginId: string): PluginCacheManager {
return {
@@ -133,6 +141,7 @@ export class CacheManager {
return new Keyv({
namespace: pluginId,
ttl: defaultTtl,
store: this.memoryStore,
});
}
+17 -7
View File
@@ -17,7 +17,11 @@
import { Logger } from 'winston';
import { CacheClient } from './CacheClient';
/** @public */
/**
* Options given when constructing a {@link CacheClient}.
*
* @public
*/
export type CacheClientOptions = {
/**
* An optional default TTL (in milliseconds) to be set when getting a client
@@ -27,7 +31,11 @@ export type CacheClientOptions = {
defaultTtl?: number;
};
/** @public */
/**
* Options given when constructing a {@link CacheManager}.
*
* @public
*/
export type CacheManagerOptions = {
/**
* An optional logger for use by the PluginCacheManager.
@@ -42,17 +50,19 @@ export type CacheManagerOptions = {
};
/**
* The PluginCacheManager manages access to cache stores that Plugins get.
* Manages access to cache stores that plugins get.
*
* @public
*/
export type PluginCacheManager = {
/**
* getClient provides backend plugins cache connections for itself.
* Provides backend plugins cache connections for themselves.
*
* The purpose of this method is to allow plugins to get isolated data
* stores so that plugins are discouraged from cache-level integration
* and/or cache key collisions.
* @remarks
*
* The purpose of this method is to allow plugins to get isolated data stores
* so that plugins are discouraged from cache-level integration and/or cache
* key collisions.
*/
getClient: (options?: CacheClientOptions) => CacheClient;
};
+3 -4
View File
@@ -27,6 +27,7 @@ import {
} from '@backstage/config-loader';
import { AppConfig, Config, ConfigReader } from '@backstage/config';
import { JsonValue } from '@backstage/types';
import { getPackages } from '@manypkg/get-packages';
import { isValidUrl } from './urls';
@@ -194,11 +195,9 @@ export async function loadBackendConfig(options: {
// TODO(hhogg): This is fetching _all_ of the packages of the monorepo
// in order to find the secrets for redactions, however we only care about
// the backend ones, we need to find a way to exclude the frontend packages.
const { Project } = require('@lerna/project');
const project = new Project(paths.targetDir);
const packages = await project.getPackages();
const { packages } = await getPackages(paths.targetDir);
const schema = await loadConfigSchema({
dependencies: packages.map((p: any) => p.name),
dependencies: packages.map(p => p.packageJson.name),
});
const config = new ObservableConfigProxy(options.logger);
@@ -38,7 +38,7 @@ function pluginPath(pluginId: string): string {
}
/**
* Configuration options object.
* Creation options for {@link DatabaseManager}.
*
* @public
*/
@@ -46,15 +46,20 @@ export type DatabaseManagerOptions = {
migrations?: PluginDatabaseManager['migrations'];
};
/** @public */
/**
* Manages database connections for Backstage backend plugins.
*
* The database manager allows the user to set connection and client settings on
* a per pluginId basis by defining a database config block under
* `plugin.<pluginId>` in addition to top level defaults. Optionally, a user may
* set `prefix` which is used to prefix generated database names if config is
* not provided.
*
* @public
*/
export class DatabaseManager {
/**
* Creates a DatabaseManager from `backend.database` config.
*
* The database manager allows the user to set connection and client settings on a per pluginId
* basis by defining a database config block under `plugin.<pluginId>` in addition to top level
* defaults. Optionally, a user may set `prefix` which is used to prefix generated database
* names if config is not provided.
* Creates a {@link DatabaseManager} from `backend.database` config.
*
* @param config - The loaded application configuration.
* @param options - An optional configuration object.
@@ -108,7 +113,7 @@ export class DatabaseManager {
* which is the pluginId prefixed with 'backstage_plugin_'. If `pluginDivisionMode` is
* `schema`, it will fallback to using the default database for the knex instance.
*
* @param pluginId Lookup the database name for given plugin
* @param pluginId - Lookup the database name for given plugin
* @returns String representing the plugin's database name
*/
private getDatabaseName(pluginId: string): string | undefined {
@@ -143,12 +148,13 @@ export class DatabaseManager {
/**
* Provides the client type which should be used for a given plugin.
*
* The client type is determined by plugin specific config if present. Otherwise the base
* client is used as the fallback.
* The client type is determined by plugin specific config if present.
* Otherwise the base client is used as the fallback.
*
* @param pluginId Plugin to get the client type for
* @returns Object with client type returned as `client` and boolean representing whether
* or not the client was overridden as `overridden`
* @param pluginId - Plugin to get the client type for
* @returns Object with client type returned as `client` and boolean
* representing whether or not the client was overridden as
* `overridden`
*/
private getClientType(pluginId: string): {
client: string;
@@ -169,8 +175,8 @@ export class DatabaseManager {
/**
* Provides the knexConfig which should be used for a given plugin.
*
* @param pluginId Plugin to get the knexConfig for
* @returns the merged kexConfig value or undefined if it isn't specified
* @param pluginId - Plugin to get the knexConfig for
* @returns The merged knexConfig value or undefined if it isn't specified
*/
private getAdditionalKnexConfig(pluginId: string): JsonObject | undefined {
const pluginConfig = this.config
@@ -197,13 +203,15 @@ export class DatabaseManager {
}
/**
* Provides a Knex connection plugin config by combining base and plugin config.
* Provides a Knex connection plugin config by combining base and plugin
* config.
*
* This method provides a baseConfig for a plugin database connector. If the client type
* has not been overridden, the global connection config will be included with plugin
* specific config as the base. Values from the plugin connection take precedence over the
* base. Base database name is omitted for all supported databases excluding SQLite unless
* `pluginDivisionMode` is set to `schema`.
* This method provides a baseConfig for a plugin database connector. If the
* client type has not been overridden, the global connection config will be
* included with plugin specific config as the base. Values from the plugin
* connection take precedence over the base. Base database name is omitted for
* all supported databases excluding SQLite unless `pluginDivisionMode` is set
* to `schema`.
*/
private getConnectionConfig(
pluginId: string,
@@ -249,9 +257,10 @@ export class DatabaseManager {
/**
* Provides a Knex database config for a given plugin.
*
* This method provides a Knex configuration object along with the plugin's client type.
* This method provides a Knex configuration object along with the plugin's
* client type.
*
* @param pluginId The plugin that the database config should correspond with
* @param pluginId - The plugin that the database config should correspond with
*/
private getConfigForPlugin(pluginId: string): Knex.Config {
const { client } = this.getClientType(pluginId);
@@ -264,20 +273,21 @@ export class DatabaseManager {
}
/**
* Provides a partial Knex.Config database schema override for a given plugin.
* Provides a partial `Knex.Config` database schema override for a given
* plugin.
*
* @param pluginId Target plugin to get database schema override
* @returns Partial Knex.Config with database schema override
* @param pluginId - Target plugin to get database schema override
* @returns Partial `Knex.Config` with database schema override
*/
private getSchemaOverrides(pluginId: string): Knex.Config | undefined {
return createSchemaOverride(this.getClientType(pluginId).client, pluginId);
}
/**
* Provides a partial Knex.Config database name override for a given plugin.
* Provides a partial `Knex.Config`• database name override for a given plugin.
*
* @param pluginId Target plugin to get database name override
* @returns Partial Knex.Config with database name override
* @param pluginId - Target plugin to get database name override
* @returns Partial `Knex.Config` with database name override
*/
private getDatabaseOverrides(pluginId: string): Knex.Config {
const databaseName = this.getDatabaseName(pluginId);
@@ -289,8 +299,9 @@ export class DatabaseManager {
/**
* Provides a scoped Knex client for a plugin as per application config.
*
* @param pluginId Plugin to get a Knex client for
* @returns Promise which resolves to a scoped Knex database client for a plugin
* @param pluginId - Plugin to get a Knex client for
* @returns Promise which resolves to a scoped Knex database client for a
* plugin
*/
private async getDatabase(pluginId: string): Promise<Knex> {
const pluginConfig = new ConfigReader(
@@ -20,8 +20,8 @@ import { merge } from 'lodash';
* Merges database objects together
*
* @public
* @param config The base config. The input is not modified
* @param overrides Any additional overrides
* @param config - The base config. The input is not modified
* @param overrides - Any additional overrides
*/
export function mergeDatabaseConfig(config: any, ...overrides: any[]) {
return merge({}, config, ...overrides);
@@ -58,7 +58,7 @@ export function createDatabaseClient(
}
/**
* Alias for createDatabaseClient
* Alias for {@link createDatabaseClient}
*
* @public
* @deprecated Use createDatabaseClient instead
@@ -100,7 +100,8 @@ export async function ensureSchemaExists(
}
/**
* Provides a Knex.Config object with the provided database name for a given client.
* Provides a `Knex.Config` object with the provided database name for a given
* client.
*/
export function createNameOverride(
client: string,
@@ -117,7 +118,8 @@ export function createNameOverride(
}
/**
* Provides a Knex.Config object with the provided database schema for a given client. Currently only supported by `pg`.
* Provides a `Knex.Config` object with the provided database schema for a given
* client. Currently only supported by `pg`.
*/
export function createSchemaOverride(
client: string,
@@ -156,7 +158,8 @@ export function parseConnectionString(
}
/**
* Normalizes a connection config or string into an object which can be passed to Knex.
* Normalizes a connection config or string into an object which can be passed
* to Knex.
*/
export function normalizeConnection(
connection: Knex.StaticConnectionConfig | JsonObject | string | undefined,
@@ -21,7 +21,7 @@ import { Knex } from 'knex';
* Default override for knex database drivers which accept ConnectionConfig
* with `connection.database` as the database name field.
*
* @param name database name to get config override for
* @param name - database name to get config override for
*/
export default function defaultNameOverride(
name: string,
@@ -18,7 +18,7 @@ import { Knex } from 'knex';
/**
* Provides a partial knex config with schema name override.
*
* @param name schema name to get config override for
* @param name - schema name to get config override for
*/
export default function defaultSchemaOverride(
name: string,
@@ -26,8 +26,8 @@ import defaultNameOverride from './defaultNameOverride';
/**
* Creates a knex mysql database connection
*
* @param dbConfig The database config
* @param overrides Additional options to merge with the config
* @param dbConfig - The database config
* @param overrides - Additional options to merge with the config
*/
export function createMysqlDatabaseClient(
dbConfig: Config,
@@ -41,8 +41,8 @@ export function createMysqlDatabaseClient(
/**
* Builds a knex mysql database connection
*
* @param dbConfig The database config
* @param overrides Additional options to merge with the config
* @param dbConfig - The database config
* @param overrides - Additional options to merge with the config
*/
export function buildMysqlDatabaseConfig(
dbConfig: Config,
@@ -61,8 +61,8 @@ export function buildMysqlDatabaseConfig(
/**
* Gets the mysql connection config
*
* @param dbConfig The database config
* @param parseConnectionString Flag to explicitly control connection string parsing
* @param dbConfig - The database config
* @param parseConnectionString - Flag to explicitly control connection string parsing
*/
export function getMysqlConnectionConfig(
dbConfig: Config,
@@ -86,7 +86,7 @@ export function getMysqlConnectionConfig(
* Parses a mysql connection string.
*
* e.g. mysql://examplename:somepassword@examplehost:3306/dbname
* @param connectionString The mysql connection string
* @param connectionString - The mysql connection string
*/
export function parseMysqlConnectionString(
connectionString: string,
@@ -140,8 +140,8 @@ export function parseMysqlConnectionString(
/**
* Creates the missing mysql database if it does not exist
*
* @param dbConfig The database config
* @param databases The names of the databases to create
* @param dbConfig - The database config
* @param databases - The names of the databases to create
*/
export async function ensureMysqlDatabaseExists(
dbConfig: Config,
@@ -26,8 +26,8 @@ import defaultSchemaOverride from './defaultSchemaOverride';
/**
* Creates a knex postgres database connection
*
* @param dbConfig The database config
* @param overrides Additional options to merge with the config
* @param dbConfig - The database config
* @param overrides - Additional options to merge with the config
*/
export function createPgDatabaseClient(
dbConfig: Config,
@@ -41,8 +41,8 @@ export function createPgDatabaseClient(
/**
* Builds a knex postgres database connection
*
* @param dbConfig The database config
* @param overrides Additional options to merge with the config
* @param dbConfig - The database config
* @param overrides - Additional options to merge with the config
*/
export function buildPgDatabaseConfig(
dbConfig: Config,
@@ -61,8 +61,8 @@ export function buildPgDatabaseConfig(
/**
* Gets the postgres connection config
*
* @param dbConfig The database config
* @param parseConnectionString Flag to explicitly control connection string parsing
* @param dbConfig - The database config
* @param parseConnectionString - Flag to explicitly control connection string parsing
*/
export function getPgConnectionConfig(
dbConfig: Config,
@@ -85,7 +85,7 @@ export function getPgConnectionConfig(
/**
* Parses a connection string using pg-connection-string
*
* @param connectionString The postgres connection string
* @param connectionString - The postgres connection string
*/
export function parsePgConnectionString(connectionString: string) {
const parse = requirePgConnectionString();
@@ -103,8 +103,8 @@ function requirePgConnectionString() {
/**
* Creates the missing Postgres database if it does not exist
*
* @param dbConfig The database config
* @param databases The name of the databases to create
* @param dbConfig - The database config
* @param databases - The name of the databases to create
*/
export async function ensurePgDatabaseExists(
dbConfig: Config,
@@ -139,8 +139,8 @@ export async function ensurePgDatabaseExists(
/**
* Creates the missing Postgres schema if it does not exist
*
* @param dbConfig The database config
* @param schemas The name of the schemas to create
* @param dbConfig - The database config
* @param schemas - The name of the schemas to create
*/
export async function ensurePgSchemaExists(
dbConfig: Config,
@@ -25,8 +25,8 @@ import { DatabaseConnector } from '../types';
/**
* Creates a knex SQLite3 database connection
*
* @param dbConfig The database config
* @param overrides Additional options to merge with the config
* @param dbConfig - The database config
* @param overrides - Additional options to merge with the config
*/
export function createSqliteDatabaseClient(
dbConfig: Config,
@@ -58,8 +58,8 @@ export function createSqliteDatabaseClient(
/**
* Builds a knex SQLite3 connection config
*
* @param dbConfig The database config
* @param overrides Additional options to merge with the config
* @param dbConfig - The database config
* @param overrides - Additional options to merge with the config
*/
export function buildSqliteDatabaseConfig(
dbConfig: Config,
@@ -38,7 +38,7 @@ export interface PluginDatabaseManager {
/**
* skip database migrations. Useful if connecting to a read-only database.
*
* @default false
* @defaultValue false
*/
skip?: boolean;
};
@@ -31,7 +31,11 @@ const coloredTemplate = (info: TransformableInfo) => {
return `${timestampColor} ${prefixColor} ${level} ${message} ${extraFields}`;
};
/** @public */
/**
* A logging format that adds coloring to console output.
*
* @public
*/
export const coloredFormat = winston.format.combine(
winston.format.timestamp(),
winston.format.colorize({
@@ -23,12 +23,29 @@ import { escapeRegExp } from '../util/escapeRegExp';
let rootLogger: winston.Logger;
let redactionRegExp: RegExp | undefined;
/** @public */
/**
* Gets the current root logger.
*
* @public
*/
export function getRootLogger(): winston.Logger {
return rootLogger;
}
/** @public */
/**
* Sets a completely custom default "root" logger.
*
* @remarks
*
* This is the logger instance that will be the foundation for all other logger
* instances passed to plugins etc, in a given backend.
*
* Only use this if you absolutely need to make a completely custom logger.
* Normally if you want to make light adaptations to the default logger
* behavior, you would instead call {@link createRootLogger}.
*
* @public
*/
export function setRootLogger(newLogger: winston.Logger) {
rootLogger = newLogger;
}
@@ -67,7 +84,17 @@ function redactLogLine(info: winston.Logform.TransformableInfo) {
return info;
}
/** @public */
/**
* Creates a default "root" logger. This also calls {@link setRootLogger} under
* the hood.
*
* @remarks
*
* This is the logger instance that will be the foundation for all other logger
* instances passed to plugins etc, in a given backend.
*
* @public
*/
export function createRootLogger(
options: winston.LoggerOptions = {},
env = process.env,
@@ -17,7 +17,7 @@
import {
AuthenticationError,
ConflictError,
ErrorResponse,
ErrorResponseBody,
InputError,
NotAllowedError,
NotFoundError,
@@ -28,7 +28,11 @@ import { ErrorRequestHandler, NextFunction, Request, Response } from 'express';
import { Logger } from 'winston';
import { getRootLogger } from '../logging';
/** @public */
/**
* Options passed to the {@link errorHandler} middleware.
*
* @public
*/
export type ErrorHandlerOptions = {
/**
* Whether error response bodies should show error stack traces or not.
@@ -89,7 +93,7 @@ export function errorHandler(
return;
}
const body: ErrorResponse = {
const body: ErrorResponseBody = {
error: serializeError(error, { includeStack: showStackTraces }),
request: { method: req.method, url: req.url },
response: { statusCode },
@@ -16,10 +16,19 @@
import { NextFunction, Request, Response, RequestHandler } from 'express';
/** @public */
/**
* A custom status checking function, passed to {@link statusCheckHandler} and
* {@link createStatusCheckRouter}.
*
* @public
*/
export type StatusCheck = () => Promise<any>;
/** @public */
/**
* Options passed to {@link statusCheckHandler}.
*
* @public
*/
export interface StatusCheckHandlerOptions {
/**
* Optional status function which returns a message.
@@ -178,7 +178,7 @@ describe('AwsS3UrlReader', () => {
),
).rejects.toThrow(
Error(
`Could not retrieve file from S3; caused by Error: not a valid AWS S3 URL: https://test-bucket.s3.us-east-2.NOTamazonaws.com/file.yaml`,
`Could not retrieve file from S3; caused by Error: invalid AWS S3 URL, cannot parse region from host in https://test-bucket.s3.us-east-2.NOTamazonaws.com/file.yaml`,
),
);
});
@@ -234,7 +234,7 @@ describe('AwsS3UrlReader', () => {
),
).rejects.toThrow(
Error(
`Could not retrieve file from S3; caused by Error: not a valid AWS S3 URL: https://test-bucket.s3.us-east-2.NOTamazonaws.com/file.yaml`,
`Could not retrieve file from S3; caused by Error: invalid AWS S3 URL, cannot parse region from host in https://test-bucket.s3.us-east-2.NOTamazonaws.com/file.yaml`,
),
);
});
@@ -332,4 +332,47 @@ describe('AwsS3UrlReader', () => {
expect(body.toString().trim()).toBe('site_name: Test');
});
});
describe('readNonAwsHost', () => {
let awsS3UrlReader: AwsS3UrlReader;
beforeAll(() => {
AWSMock.setSDKInstance(aws);
AWSMock.mock(
'S3',
'getObject',
Buffer.from(
require('fs').readFileSync(
path.resolve(
__dirname,
'__fixtures__/awsS3/awsS3-mock-object.yaml',
),
),
),
);
const s3 = new aws.S3();
awsS3UrlReader = new AwsS3UrlReader(
new AwsS3Integration(
readAwsS3IntegrationConfig(
new ConfigReader({
host: 'localhost:4566',
accessKeyId: 'fake-access-key',
secretAccessKey: 'fake-secret-key',
endpoint: 'http://localhost:4566',
s3ForcePathStyle: true,
}),
),
),
{ s3, treeResponseFactory },
);
});
it('returns contents of an object in a bucket', async () => {
const response = await awsS3UrlReader.read(
'http://localhost:4566/test-bucket/awsS3-mock-object.yaml',
);
expect(response.toString().trim()).toBe('site_name: Test');
});
});
});
@@ -27,12 +27,17 @@ import {
UrlReader,
} from './types';
import getRawBody from 'raw-body';
import { AwsS3Integration, ScmIntegrations } from '@backstage/integration';
import {
AwsS3Integration,
ScmIntegrations,
AwsS3IntegrationConfig,
} from '@backstage/integration';
import { ForwardedError, NotModifiedError } from '@backstage/errors';
import { ListObjectsV2Output, ObjectList } from 'aws-sdk/clients/s3';
const parseURL = (
url: string,
config: AwsS3IntegrationConfig,
): { path: string; bucket: string; region: string } => {
let { host, pathname } = new URL(url);
@@ -42,20 +47,45 @@ const parseURL = (
*/
pathname = pathname.substr(1);
let bucket;
let region;
/**
* Checks that the given URL is a valid S3 object url.
* Format of a Valid S3 URL: https://bucket-name.s3.Region.amazonaws.com/keyname
* Path style URLs: https://s3.Region.amazonaws.com/bucket-name/key-name
* Virtual hosted style URLs: https://bucket-name.s3.Region.amazonaws.com/key-name
* See https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html#path-style-access
*/
const validHost = new RegExp(
/^[a-z\d][a-z\d\.-]{1,61}[a-z\d]\.s3\.[a-z\d-]+\.amazonaws.com$/,
);
if (!validHost.test(host)) {
throw new Error(`not a valid AWS S3 URL: ${url}`);
if (config.s3ForcePathStyle) {
if (pathname.indexOf('/') < 0) {
throw new Error(
`invalid path-style AWS S3 URL, ${url} does not contain bucket in the path`,
);
}
[bucket] = pathname.split('/');
pathname = pathname.substr(bucket.length + 1);
} else {
if (host.indexOf('.') < 0) {
throw new Error(
`invalid virtual hosted-style AWS S3 URL, ${url} does not contain bucket prefix in the host`,
);
}
[bucket] = host.split('.');
host = host.substr(bucket.length + 1);
}
const [bucket] = host.split(/\.s3\.[a-z\d-]+\.amazonaws.com/);
host = host.substring(bucket.length);
const [, , region, ,] = host.split('.');
// Only extract region from *.amazonaws.com hosts
if (config.host === 'amazonaws.com') {
// At this point bucket prefix is removed from host for virtual hosted URLs
const match = host.match(/^s3\.([a-z\d-]+)\.amazonaws\.com$/);
if (!match) {
throw new Error(
`invalid AWS S3 URL, cannot parse region from host in ${url}`,
);
}
region = match[1];
} else {
region = '';
}
return {
path: pathname,
@@ -64,15 +94,23 @@ const parseURL = (
};
};
/**
* Implements a {@link UrlReader} for AWS S3 buckets.
*
* @public
*/
export class AwsS3UrlReader implements UrlReader {
static factory: ReaderFactory = ({ config, treeResponseFactory }) => {
const integrations = ScmIntegrations.fromConfig(config);
return integrations.awsS3.list().map(integration => {
const creds = AwsS3UrlReader.buildCredentials(integration);
const s3 = new S3({
apiVersion: '2006-03-01',
credentials: creds,
endpoint: integration.config.endpoint,
s3ForcePathStyle: integration.config.s3ForcePathStyle,
});
const reader = new AwsS3UrlReader(integration, {
s3,
@@ -138,7 +176,7 @@ export class AwsS3UrlReader implements UrlReader {
options?: ReadUrlOptions,
): Promise<ReadUrlResponse> {
try {
const { path, bucket, region } = parseURL(url);
const { path, bucket, region } = parseURL(url, this.integration.config);
aws.config.update({ region: region });
let params;
@@ -178,7 +216,7 @@ export class AwsS3UrlReader implements UrlReader {
options?: ReadTreeOptions,
): Promise<ReadTreeResponse> {
try {
const { path, bucket, region } = parseURL(url);
const { path, bucket, region } = parseURL(url, this.integration.config);
const allObjects: ObjectList = [];
const responses = [];
let continuationToken: string | undefined;
@@ -38,7 +38,11 @@ import {
ReadUrlResponse,
} from './types';
/** @public */
/**
* Implements a {@link UrlReader} for Azure repos.
*
* @public
*/
export class AzureUrlReader implements UrlReader {
static factory: ReaderFactory = ({ config, treeResponseFactory }) => {
const integrations = ScmIntegrations.fromConfig(config);
@@ -41,8 +41,8 @@ import {
} from './types';
/**
* A processor that adds the ability to read files from Bitbucket v1 and v2 APIs, such as
* the one exposed by Bitbucket Cloud itself.
* Implements a {@link UrlReader} for files from Bitbucket v1 and v2 APIs, such
* as the one exposed by Bitbucket Cloud itself.
*
* @public
*/
@@ -27,7 +27,7 @@ import {
import path from 'path';
/**
* A UrlReader that does a plain fetch of the URL.
* A {@link UrlReader} that does a plain fetch of the URL.
*
* @public
*/
@@ -50,7 +50,7 @@ export type GhBlobResponse =
RestEndpointMethodTypes['git']['getBlob']['response']['data'];
/**
* A processor that adds the ability to read files from GitHub v3 APIs, such as
* Implements a {@link UrlReader} for files through the GitHub v3 APIs, such as
* the one exposed by GitHub itself.
*
* @public
@@ -39,7 +39,11 @@ import {
} from './types';
import { trimEnd } from 'lodash';
/** @public */
/**
* Implements a {@link UrlReader} for files on GitLab.
*
* @public
*/
export class GitlabUrlReader implements UrlReader {
static factory: ReaderFactory = ({ config, treeResponseFactory }) => {
const integrations = ScmIntegrations.fromConfig(config);
@@ -48,7 +48,11 @@ const parseURL = (
};
};
/** @public */
/**
* Implements a {@link UrlReader} for files on Google GCS.
*
* @public
*/
export class GoogleGcsUrlReader implements UrlReader {
static factory: ReaderFactory = ({ config, logger }) => {
if (!config.has('integrations.googleGcs')) {
@@ -27,7 +27,11 @@ import { FetchUrlReader } from './FetchUrlReader';
import { GoogleGcsUrlReader } from './GoogleGcsUrlReader';
import { AwsS3UrlReader } from './AwsS3UrlReader';
/** @public */
/**
* Creation options for {@link UrlReaders}.
*
* @public
*/
export type UrlReadersOptions = {
/** Root config object */
config: Config;
@@ -38,13 +42,13 @@ export type UrlReadersOptions = {
};
/**
* UrlReaders provide various utilities related to the UrlReader interface.
* Helps construct {@link UrlReader}s.
*
* @public
*/
export class UrlReaders {
/**
* Creates a UrlReader without any known types.
* Creates a custom {@link UrlReader} wrapper for your own set of factories.
*/
static create(options: UrlReadersOptions): UrlReader {
const { logger, config, factories } = options;
@@ -65,7 +69,8 @@ export class UrlReaders {
}
/**
* Creates a UrlReader that includes all the default factories from this package.
* Creates a {@link UrlReader} wrapper that includes all the default factories
* from this package.
*
* Any additional factories passed will be loaded before the default ones.
*/
+6 -1
View File
@@ -281,7 +281,12 @@ export type FromReadableArrayOptions = Array<{
path: string;
}>;
/** @public */
/**
* A factory for response factories that handle the unpacking and inspection of
* complex responses such as archive data.
*
* @public
*/
export interface ReadTreeResponseFactory {
fromTarArchive(
options: ReadTreeResponseFactoryOptions,
@@ -320,4 +320,21 @@ describe('Git', () => {
});
});
});
describe('log', () => {
it('should call isomorphic-git with the correct arguments', async () => {
const dir = '/some/mock/dir';
const ref = 'as43bd7';
const git = Git.fromAuth({});
await git.log({ dir, ref });
expect(isomorphic.log).toHaveBeenCalledWith({
fs,
dir,
ref,
});
});
});
});
+29 -8
View File
@@ -33,7 +33,11 @@ From : https://isomorphic-git.org/docs/en/onAuth with fix for GitHub
Azure 'notempty' token
*/
/** @public */
/**
* A convenience wrapper around the `isomorphic-git` library.
*
* @public
*/
export class Git {
private constructor(
private readonly config: {
@@ -76,12 +80,15 @@ export class Git {
return git.commit({ fs, dir, message, author, committer });
}
/** https://isomorphic-git.org/docs/en/clone */
async clone(options: {
url: string;
dir: string;
ref?: string;
depth?: number;
noCheckout?: boolean;
}): Promise<void> {
const { url, dir, ref } = options;
const { url, dir, ref, depth, noCheckout } = options;
this.config.logger?.info(`Cloning repo {dir=${dir},url=${url}}`);
return git.clone({
fs,
@@ -90,7 +97,8 @@ export class Git {
dir,
ref,
singleBranch: true,
depth: 1,
depth: depth ?? 1,
noCheckout,
onProgress: this.onProgressHandler(),
headers: {
'user-agent': 'git/@isomorphic-git',
@@ -99,7 +107,7 @@ export class Git {
});
}
// https://isomorphic-git.org/docs/en/currentBranch
/** https://isomorphic-git.org/docs/en/currentBranch */
async currentBranch(options: {
dir: string;
fullName?: boolean;
@@ -110,7 +118,7 @@ export class Git {
>;
}
// https://isomorphic-git.org/docs/en/fetch
/** https://isomorphic-git.org/docs/en/fetch */
async fetch(options: { dir: string; remote?: string }): Promise<void> {
const { dir, remote = 'origin' } = options;
this.config.logger?.info(
@@ -138,7 +146,7 @@ export class Git {
});
}
// https://isomorphic-git.org/docs/en/merge
/** https://isomorphic-git.org/docs/en/merge */
async merge(options: {
dir: string;
theirs: string;
@@ -180,7 +188,7 @@ export class Git {
});
}
// https://isomorphic-git.org/docs/en/readCommit
/** https://isomorphic-git.org/docs/en/readCommit */
async readCommit(options: {
dir: string;
sha: string;
@@ -189,12 +197,25 @@ export class Git {
return git.readCommit({ fs, dir, oid: sha });
}
// https://isomorphic-git.org/docs/en/resolveRef
/** https://isomorphic-git.org/docs/en/resolveRef */
async resolveRef(options: { dir: string; ref: string }): Promise<string> {
const { dir, ref } = options;
return git.resolveRef({ fs, dir, ref });
}
/** https://isomorphic-git.org/docs/en/log */
async log(options: {
dir: string;
ref?: string;
}): Promise<ReadCommitResult[]> {
const { dir, ref } = options;
return git.log({
fs,
dir,
ref: ref ?? 'HEAD',
});
}
private onAuth = () => ({
username: this.config.username,
password: this.config.password,
@@ -19,9 +19,26 @@ import Router from 'express-promise-router';
import express from 'express';
import { errorHandler, statusCheckHandler, StatusCheck } from '../middleware';
/** @public */
/**
* Creates a default status checking router, that you can add to your express
* app.
*
* @remarks
*
* This adds a `/healthcheck` route (or any other path, if given as an
* argument), which your infra can call to see if the service is ready to serve
* requests.
*
* @public
*/
export async function createStatusCheckRouter(options: {
logger: Logger;
/**
* The path (including a leading slash) that the health check should be
* mounted on.
*
* @defaultValue '/healthcheck'
*/
path?: string;
/**
* If not implemented, the default express middleware always returns 200.
@@ -19,6 +19,7 @@ import compression from 'compression';
import cors from 'cors';
import express, { Router, ErrorRequestHandler } from 'express';
import helmet from 'helmet';
import { ContentSecurityPolicyOptions } from 'helmet/dist/middlewares/content-security-policy';
import * as http from 'http';
import stoppable from 'stoppable';
import { Logger } from 'winston';
@@ -43,19 +44,6 @@ import { createHttpServer, createHttpsServer } from './hostFactory';
export const DEFAULT_PORT = 7007;
// '' is express default, which listens to all interfaces
const DEFAULT_HOST = '';
// taken from the helmet source code - don't seem to be exported
const DEFAULT_CSP = {
'default-src': ["'self'"],
'base-uri': ["'self'"],
'block-all-mixed-content': [],
'font-src': ["'self'", 'https:', 'data:'],
'frame-ancestors': ["'self'"],
'img-src': ["'self'", 'data:'],
'object-src': ["'none'"],
'script-src': ["'self'", "'unsafe-eval'"],
'script-src-attr': ["'none'"],
'style-src': ["'self'", 'https:', "'unsafe-inline'"],
};
export class ServiceBuilderImpl implements ServiceBuilder {
private port: number | undefined;
@@ -236,8 +224,13 @@ export class ServiceBuilderImpl implements ServiceBuilder {
export function applyCspDirectives(
directives: Record<string, string[] | false> | undefined,
): CspOptions | undefined {
const result: CspOptions = { ...DEFAULT_CSP };
): ContentSecurityPolicyOptions['directives'] {
const result: ContentSecurityPolicyOptions['directives'] =
helmet.contentSecurityPolicy.getDefaultDirectives();
// TODO(Rugvip): We currently use non-precompiled AJV for validation in the frontend, which uses eval.
// It should be replaced by any other solution that doesn't require unsafe-eval.
result['script-src'] = ["'self'", "'unsafe-eval'"];
if (directives) {
for (const [key, value] of Object.entries(directives)) {
@@ -42,8 +42,6 @@ export type CertificateAttributes = {
/**
* A map from CSP directive names to their values.
*
* Added here since helmet doesn't export this type publicly.
*/
export type CspOptions = Record<string, string[]>;
@@ -57,7 +55,7 @@ type CustomOrigin = (
/**
* Reads some base options out of a config object.
*
* @param config The root of a backend config object
* @param config - The root of a backend config object
* @returns A base options object
*
* @example
@@ -100,7 +98,7 @@ export function readBaseOptions(config: Config): BaseOptions {
/**
* Attempts to read a CORS options object from the root of a config object.
*
* @param config The root of a backend config object
* @param config - The root of a backend config object
* @returns A CORS options object, or undefined if not specified
*
* @example
@@ -134,7 +132,7 @@ export function readCorsOptions(config: Config): CorsOptions | undefined {
/**
* Attempts to read a CSP options object from the root of a config object.
*
* @param config The root of a backend config object
* @param config - The root of a backend config object
* @returns A CSP options object, or undefined if not specified. Values can be
* false as well, which means to remove the default behavior for that
* key.
@@ -170,7 +168,7 @@ export function readCspOptions(
/**
* Attempts to read a https settings object from the root of a config object.
*
* @param config The root of a backend config object
* @param config - The root of a backend config object
* @returns A https settings object, or undefined if not specified
*
* @example
@@ -29,8 +29,8 @@ const IP_HOSTNAME_REGEX = /:|^\d+\.\d+\.\d+\.\d+$/;
/**
* Creates a Http server instance based on an Express application.
*
* @param app The Express application object
* @param logger Optional Winston logger object
* @param app - The Express application object
* @param logger - Optional Winston logger object
* @returns A Http server instance
*
*/
@@ -46,9 +46,9 @@ export function createHttpServer(
/**
* Creates a Https server instance based on an Express application.
*
* @param app The Express application object
* @param httpsSettings HttpsSettings for self-signed certificate generation
* @param logger Optional Winston logger object
* @param app - The Express application object
* @param httpsSettings - HttpsSettings for self-signed certificate generation
* @param logger - Optional Winston logger object
* @returns A Https server instance
*
*/
+10 -2
View File
@@ -20,7 +20,11 @@ import { Router, RequestHandler, ErrorRequestHandler } from 'express';
import { Server } from 'http';
import { Logger } from 'winston';
/** @public */
/**
* A helper for building backend service instances.
*
* @public
*/
export type ServiceBuilder = {
/**
* Sets the service parameters based on configuration.
@@ -119,5 +123,9 @@ export type ServiceBuilder = {
start(): Promise<Server>;
};
/** @public */
/**
* A factory for request loggers.
*
* @public
*/
export type RequestLoggingHandlerFactory = (logger?: Logger) => RequestHandler;
@@ -16,7 +16,11 @@
import { Writable } from 'stream';
/** @public */
/**
* Options passed to the {@link ContainerRunner.runContainer} method.
*
* @public
*/
export type RunContainerOptions = {
imageName: string;
command?: string | string[];
@@ -28,7 +32,14 @@ export type RunContainerOptions = {
pullImage?: boolean;
};
/** @public */
/**
* Handles the running of containers, on behalf of others.
*
* @public
*/
export interface ContainerRunner {
/**
* Runs a container image to completion.
*/
runContainer(opts: RunContainerOptions): Promise<void>;
}
@@ -24,7 +24,11 @@ export type UserOptions = {
User?: string;
};
/** @public */
/**
* A {@link ContainerRunner} for Docker containers.
*
* @public
*/
export class DockerContainerRunner implements ContainerRunner {
private readonly dockerClient: Docker;
+13
View File
@@ -1,5 +1,18 @@
# @backstage/backend-tasks
## 0.1.2
### Patch Changes
- e188b37024: Updated README to clarify the following:
- Dashes cannot be used in the `id`, changed to an underscore in the example
- The `timeout` is required, this was also added to the example
- Added a note about tasks not running as expected for local development using persistent database
- Updated dependencies
- @backstage/backend-common@0.10.2
## 0.1.1
### Patch Changes
+6 -1
View File
@@ -21,14 +21,19 @@ import { Duration } from 'luxon';
const scheduler = TaskScheduler.fromConfig(rootConfig).forPlugin('my-plugin');
await scheduler.scheduleTask({
id: 'refresh-things',
id: 'refresh_things',
frequency: Duration.fromObject({ minutes: 10 }),
timeout: Duration.fromObject({ minutes: 15 }),
fn: async () => {
await entityProvider.run();
},
});
```
## Local Development
When working with the `@backstage/backend-tasks` library you may run into your task not running immediately at startup as expected if you are using a persistent database. This is by design - the library respects the previous state and does not run the task sooner than the specified frequency. If you want to get around this, there is a table called `backstage_backend_tasks__tasks` in the applicable plugin's database which will contain a record with the next run date and time. You can delete this record to get things back to what you expect.
## Documentation
- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md)
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-tasks",
"description": "Common distributed task management library for Backstage backends",
"version": "0.1.1",
"version": "0.1.2",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -29,7 +29,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.10.0",
"@backstage/backend-common": "^0.10.2",
"@backstage/config": "^0.1.11",
"@backstage/errors": "^0.1.4",
"@backstage/types": "^0.1.1",
@@ -44,7 +44,7 @@
},
"devDependencies": {
"@backstage/backend-test-utils": "^0.1.11",
"@backstage/cli": "^0.10.3",
"@backstage/cli": "^0.10.5",
"jest": "^26.0.1",
"wait-for-expect": "^3.0.2"
},
+9
View File
@@ -1,5 +1,14 @@
# @backstage/backend-test-utils
## 0.1.12
### Patch Changes
- 130b7aadf2: Lazy-load `testcontainers` module in order to avoid side-effects.
- Updated dependencies
- @backstage/backend-common@0.10.1
- @backstage/cli@0.10.4
## 0.1.11
### Patch Changes
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-test-utils",
"description": "Test helpers library for Backstage backends",
"version": "0.1.11",
"version": "0.1.12",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -30,18 +30,18 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.10.0",
"@backstage/cli": "^0.10.3",
"@backstage/backend-common": "^0.10.1",
"@backstage/cli": "^0.10.4",
"@backstage/config": "^0.1.9",
"knex": "^0.95.1",
"mysql2": "^2.2.5",
"pg": "^8.3.0",
"sqlite3": "^5.0.1",
"testcontainers": "^7.23.0",
"testcontainers": "^8.1.2",
"uuid": "^8.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.10.3",
"@backstage/cli": "^0.10.4",
"jest": "^26.0.1"
},
"files": [
@@ -15,7 +15,6 @@
*/
import createConnection, { Knex } from 'knex';
import { GenericContainer } from 'testcontainers';
import { v4 as uuid } from 'uuid';
async function waitForMysqlReady(
@@ -50,6 +49,9 @@ export async function startMysqlContainer(image: string) {
const user = 'root';
const password = uuid();
// Lazy-load to avoid side-effect of importing testcontainers
const { GenericContainer } = await import('testcontainers');
const container = await new GenericContainer(image)
.withExposedPorts(3306)
.withEnv('MYSQL_ROOT_PASSWORD', password)
@@ -15,7 +15,6 @@
*/
import createConnection, { Knex } from 'knex';
import { GenericContainer } from 'testcontainers';
import { v4 as uuid } from 'uuid';
async function waitForPostgresReady(
@@ -50,6 +49,9 @@ export async function startPostgresContainer(image: string) {
const user = 'postgres';
const password = uuid();
// Lazy-load to avoid side-effect of importing testcontainers
const { GenericContainer } = await import('testcontainers');
const container = await new GenericContainer(image)
.withExposedPorts(5432)
.withEnv('POSTGRES_PASSWORD', password)
+20
View File
@@ -1,5 +1,25 @@
# example-backend
## 0.2.59
### Patch Changes
- Updated dependencies
- @backstage/plugin-rollbar-backend@0.1.18
- @backstage/plugin-auth-backend@0.6.0
- @backstage/backend-common@0.10.1
- @backstage/plugin-app-backend@0.3.21
- @backstage/plugin-catalog-backend@0.19.4
- @backstage/plugin-scaffolder-backend@0.15.19
- @backstage/integration@0.7.0
- @backstage/plugin-techdocs-backend@0.12.2
- @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.4
- @backstage/plugin-permission-backend@0.2.3
- @backstage/plugin-permission-node@0.2.3
- @backstage/plugin-code-coverage-backend@0.1.18
- @backstage/plugin-scaffolder-backend-module-rails@0.2.2
- @backstage/plugin-todo-backend@0.1.17
## 0.2.58
### Patch Changes
+17 -16
View File
@@ -1,6 +1,6 @@
{
"name": "example-backend",
"version": "0.2.58",
"version": "0.2.59",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -24,37 +24,38 @@
"migrate:create": "knex migrate:make -x ts"
},
"dependencies": {
"@backstage/backend-common": "^0.10.0",
"@backstage/backend-common": "^0.10.1",
"@backstage/backend-tasks": "^0.1.0",
"@backstage/catalog-client": "^0.5.3",
"@backstage/catalog-model": "^0.9.8",
"@backstage/config": "^0.1.10",
"@backstage/integration": "^0.6.10",
"@backstage/plugin-app-backend": "^0.3.20",
"@backstage/plugin-auth-backend": "^0.5.2",
"@backstage/integration": "^0.7.0",
"@backstage/plugin-app-backend": "^0.3.21",
"@backstage/plugin-auth-backend": "^0.6.0",
"@backstage/plugin-azure-devops-backend": "^0.2.6",
"@backstage/plugin-badges-backend": "^0.1.14",
"@backstage/plugin-catalog-backend": "^0.19.3",
"@backstage/plugin-code-coverage-backend": "^0.1.17",
"@backstage/plugin-catalog-backend": "^0.19.4",
"@backstage/plugin-code-coverage-backend": "^0.1.18",
"@backstage/plugin-graphql-backend": "^0.1.10",
"@backstage/plugin-jenkins-backend": "^0.1.10",
"@backstage/plugin-kubernetes-backend": "^0.4.1",
"@backstage/plugin-kafka-backend": "^0.2.13",
"@backstage/plugin-permission-backend": "^0.2.2",
"@backstage/plugin-permission-backend": "^0.2.3",
"@backstage/plugin-permission-common": "^0.3.0",
"@backstage/plugin-permission-node": "^0.2.2",
"@backstage/plugin-permission-node": "^0.2.3",
"@backstage/plugin-proxy-backend": "^0.2.15",
"@backstage/plugin-rollbar-backend": "^0.1.17",
"@backstage/plugin-scaffolder-backend": "^0.15.18",
"@backstage/plugin-scaffolder-backend-module-rails": "^0.2.1",
"@backstage/plugin-rollbar-backend": "^0.1.18",
"@backstage/plugin-scaffolder-backend": "^0.15.19",
"@backstage/plugin-scaffolder-backend-module-rails": "^0.2.2",
"@backstage/plugin-search-backend": "^0.3.0",
"@backstage/plugin-search-backend-node": "^0.4.2",
"@backstage/plugin-search-backend-module-elasticsearch": "^0.0.7",
"@backstage/plugin-search-backend-module-pg": "^0.2.3",
"@backstage/plugin-techdocs-backend": "^0.12.1",
"@backstage/plugin-techdocs-backend": "^0.12.2",
"@backstage/plugin-tech-insights-backend": "^0.1.4",
"@backstage/plugin-tech-insights-node": "^0.1.2",
"@backstage/plugin-tech-insights-backend-module-jsonfc": "^0.1.3",
"@backstage/plugin-todo-backend": "^0.1.16",
"@backstage/plugin-tech-insights-backend-module-jsonfc": "^0.1.4",
"@backstage/plugin-todo-backend": "^0.1.17",
"@gitbeaker/node": "^34.6.0",
"@octokit/rest": "^18.5.3",
"azure-devops-node-api": "^11.0.1",
@@ -71,7 +72,7 @@
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.10.3",
"@backstage/cli": "^0.10.4",
"@types/dockerode": "^3.3.0",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5"
+6 -3
View File
@@ -35,6 +35,7 @@ import {
useHotMemoize,
ServerTokenManager,
} from '@backstage/backend-common';
import { TaskScheduler } from '@backstage/backend-tasks';
import { Config } from '@backstage/config';
import healthcheck from './plugins/healthcheck';
import { metricsInit, metricsHandler } from './metrics';
@@ -68,16 +69,17 @@ function makeCreateEnv(config: Config) {
discovery,
tokenManager,
});
root.info(`Created UrlReader ${reader}`);
const databaseManager = DatabaseManager.fromConfig(config);
const cacheManager = CacheManager.fromConfig(config);
const taskScheduler = TaskScheduler.fromConfig(config);
root.info(`Created UrlReader ${reader}`);
return (plugin: string): PluginEnvironment => {
const logger = root.child({ type: 'plugin', plugin });
const database = databaseManager.forPlugin(plugin);
const cache = cacheManager.forPlugin(plugin);
const scheduler = taskScheduler.forPlugin(plugin);
return {
logger,
cache,
@@ -87,6 +89,7 @@ function makeCreateEnv(config: Config) {
discovery,
tokenManager,
permissions,
scheduler,
};
};
}
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* Copyright 2020 Spotify AB
* Copyright 2020 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.
+3 -3
View File
@@ -41,11 +41,11 @@ export default async function createPlugin({
discovery,
factRetrievers: [
createFactRetrieverRegistration(
'* * * * *', // Example cron, every minute
'1 1 1 * *', // Example cron, At 01:01 on day-of-month 1.
entityOwnershipFactRetriever,
),
createFactRetrieverRegistration('* * * * *', entityMetadataFactRetriever),
createFactRetrieverRegistration('* * * * *', techdocsFactRetriever),
createFactRetrieverRegistration('1 1 1 * *', entityMetadataFactRetriever),
createFactRetrieverRegistration('1 1 1 * *', techdocsFactRetriever),
],
factCheckerFactory: new JsonRulesEngineFactCheckerFactory({
checks: [
+2
View File
@@ -24,6 +24,7 @@ import {
UrlReader,
} from '@backstage/backend-common';
import { ServerPermissionClient } from '@backstage/plugin-permission-node';
import { PluginTaskScheduler } from '@backstage/backend-tasks';
export type PluginEnvironment = {
logger: Logger;
@@ -34,4 +35,5 @@ export type PluginEnvironment = {
discovery: PluginEndpointDiscovery;
tokenManager: TokenManager;
permissions: ServerPermissionClient;
scheduler: PluginTaskScheduler;
};
+19
View File
@@ -1,5 +1,24 @@
# @backstage/cli
## 0.10.5
### Patch Changes
- 37123b4851: Bump `css-loader` from `5.2.6` to `6.5.1`
- 9534391ae4: Add --pattern option to override matching glob patterns for backstage versioning
- 4ce51ab0f1: Internal refactor of the `react-use` imports to use `react-use/lib/*` instead.
## 0.10.4
### Patch Changes
- 1d26017090: Fix issue with plugin:serve for Plugins not using Lerna monorepo.
- 9c3aaf3512: Bump `@typescript-eslint/eslint-plugin` to `4.33.0`
- 808828e4fa: remove inline CSS from serve_index.html
- 6e4080d31b: Add option to build command for minifying the generated code
- Updated dependencies
- @backstage/config-loader@0.9.1
## 0.10.3
### Patch Changes
+4
View File
@@ -80,6 +80,10 @@ module.exports = {
name: '@material-ui/icons',
message: "Please import '@material-ui/icons/<Icon>' instead.",
},
{
name: '@material-ui/icons/', // because this is possible too ._.
message: "Please import '@material-ui/icons/<Icon>' instead.",
},
...require('module').builtinModules,
],
// Avoid cross-package imports
+22 -20
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
"version": "0.10.3",
"version": "0.10.5",
"private": false,
"publishConfig": {
"access": "public"
@@ -30,12 +30,11 @@
"dependencies": {
"@backstage/cli-common": "^0.1.6",
"@backstage/config": "^0.1.11",
"@backstage/config-loader": "^0.9.0",
"@backstage/config-loader": "^0.9.1",
"@backstage/errors": "^0.1.5",
"@backstage/types": "^0.1.1",
"@hot-loader/react-dom": "^16.13.0",
"@lerna/package-graph": "^4.0.0",
"@lerna/project": "^4.0.0",
"@manypkg/get-packages": "^1.1.3",
"@octokit/request": "^5.4.12",
"@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-json": "^4.1.0",
@@ -51,26 +50,27 @@
"@svgr/rollup": "5.5.x",
"@svgr/webpack": "5.5.x",
"@types/webpack-env": "^1.15.2",
"@typescript-eslint/eslint-plugin": "^v4.30.0",
"@typescript-eslint/parser": "^v4.28.3",
"@typescript-eslint/eslint-plugin": "^5.9.0",
"@typescript-eslint/parser": "^5.9.0",
"@yarnpkg/lockfile": "^1.1.0",
"bfj": "^7.0.2",
"buffer": "^6.0.3",
"chalk": "^4.0.0",
"chokidar": "^3.3.1",
"commander": "^6.1.0",
"css-loader": "^5.2.6",
"css-loader": "^6.5.1",
"diff": "^5.0.0",
"esbuild": "^0.14.1",
"eslint": "^7.30.0",
"esbuild": "^0.14.10",
"esbuild-loader": "^2.18.0",
"eslint": "^8.6.0",
"eslint-config-prettier": "^8.3.0",
"eslint-formatter-friendly": "^7.0.0",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-jest": "^24.1.0",
"eslint-plugin-jsx-a11y": "^6.2.1",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jest": "^25.3.4",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-monorepo": "^0.3.2",
"eslint-plugin-react": "^7.12.4",
"eslint-plugin-react-hooks": "^4.0.0",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"express": "^4.17.1",
"fork-ts-checker-webpack-plugin": "^4.0.5",
"fs-extra": "9.1.0",
@@ -83,6 +83,7 @@
"json-schema": "^0.4.0",
"jest-transform-yaml": "^0.1.1",
"lodash": "^4.17.21",
"minimatch": "3.0.4",
"mini-css-extract-plugin": "^2.4.2",
"node-libs-browser": "^2.2.1",
"ora": "^5.3.0",
@@ -114,19 +115,20 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/backend-common": "^0.10.0",
"@backstage/backend-common": "^0.10.2",
"@backstage/config": "^0.1.11",
"@backstage/core-components": "^0.8.2",
"@backstage/core-plugin-api": "^0.4.0",
"@backstage/core-app-api": "^0.3.0",
"@backstage/dev-utils": "^0.2.15",
"@backstage/test-utils": "^0.2.0",
"@backstage/core-components": "^0.8.3",
"@backstage/core-plugin-api": "^0.4.1",
"@backstage/core-app-api": "^0.3.1",
"@backstage/dev-utils": "^0.2.16",
"@backstage/test-utils": "^0.2.1",
"@backstage/theme": "^0.2.14",
"@types/diff": "^5.0.0",
"@types/express": "^4.17.6",
"@types/fs-extra": "^9.0.1",
"@types/http-proxy": "^1.17.4",
"@types/inquirer": "^8.1.3",
"@types/minimatch": "^3.0.5",
"@types/mock-fs": "^4.13.0",
"@types/node": "^14.14.32",
"@types/recursive-readdir": "^2.2.0",
+1 -1
View File
@@ -33,5 +33,5 @@ export default async (cmd: Command) => {
outputs = new Set([Output.types, Output.esm, Output.cjs]);
}
await buildPackage({ outputs });
await buildPackage({ outputs, minify: cmd.minify });
};
+5
View File
@@ -138,6 +138,7 @@ export function registerCommands(program: CommanderStatic) {
.command('build')
.description('Build a package for publishing')
.option('--outputs <formats>', 'List of formats to output [types,cjs,esm]')
.option('--minify', 'Minify the generated code')
.action(lazy(() => import('./build').then(m => m.default)));
program
@@ -213,6 +214,10 @@ export function registerCommands(program: CommanderStatic) {
program
.command('versions:bump')
.option(
'--pattern <glob>',
'Override glob for matching packages to upgrade',
)
.description('Bump Backstage packages to the latest versions')
.action(lazy(() => import('./versions/bump').then(m => m.default)));
+150 -14
View File
@@ -16,9 +16,9 @@
import fs from 'fs-extra';
import mockFs from 'mock-fs';
import { Command } from 'commander';
import { resolve as resolvePath } from 'path';
import { paths } from '../../lib/paths';
import { mapDependencies } from '../../lib/versioning';
import * as runObj from '../../lib/run';
import bump, { bumpBackstageJsonVersion } from './bump';
import { withLogCollector } from '@backstage/test-utils';
@@ -36,6 +36,8 @@ const REGISTRY_VERSIONS: { [name: string]: string } = {
'@backstage/core': '1.0.6',
'@backstage/core-api': '1.0.7',
'@backstage/theme': '2.0.0',
'@backstage-extra/custom': '1.1.0',
'@backstage-extra/custom-two': '2.0.0',
};
const HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
@@ -82,13 +84,12 @@ describe('bump', () => {
});
it('should bump backstage dependencies', async () => {
// Make sure all modules involved in package discovery are in the module cache before we mock fs
await mapDependencies(paths.targetDir);
mockFs({
'/yarn.lock': lockfileMock,
'/lerna.json': JSON.stringify({
packages: ['packages/*'],
'/package.json': JSON.stringify({
workspaces: {
packages: ['packages/*'],
},
}),
'/packages/a/package.json': JSON.stringify({
name: 'a',
@@ -105,7 +106,6 @@ describe('bump', () => {
}),
});
paths.targetDir = '/';
jest
.spyOn(paths, 'resolveTargetRoot')
.mockImplementation((...path) => resolvePath('/', ...path));
@@ -123,9 +123,10 @@ describe('bump', () => {
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
const { log: logs } = await withLogCollector(['log'], async () => {
await bump();
await bump({ pattern: null } as unknown as Command);
});
expect(logs.filter(Boolean)).toEqual([
'Using default pattern glob @backstage/*',
'Checking for updates of @backstage/theme',
'Checking for updates of @backstage/core',
'Checking for updates of @backstage/core-api',
@@ -178,13 +179,148 @@ describe('bump', () => {
});
});
it('should bump backstage dependencies and dependencies matching pattern glob', async () => {
const customLockfileMock = `${lockfileMock}
"@backstage-extra/custom@^1.1.0":
version "1.1.0"
"@backstage-extra/custom@^1.0.1":
version "1.0.1"
"@backstage-extra/custom-two@^1.0.0":
version "1.0.0"
`;
const customLockfileMockResult = `${HEADER}
"@backstage-extra/custom-two@^1.0.0":
version "1.0.0"
"@backstage-extra/custom@^1.1.0":
version "1.1.0"
"@backstage/core@^1.0.5":
version "1.0.6"
dependencies:
"@backstage/core-api" "^1.0.6"
"@backstage/theme@^1.0.0":
version "1.0.0"
`;
mockFs({
'/yarn.lock': customLockfileMock,
'/package.json': JSON.stringify({
workspaces: {
packages: ['packages/*'],
},
}),
'/packages/a/package.json': JSON.stringify({
name: 'a',
dependencies: {
'@backstage/core': '^1.0.5',
'@backstage-extra/custom': '^1.0.1',
'@backstage-extra/custom-two': '^1.0.0',
},
}),
'/packages/b/package.json': JSON.stringify({
name: 'b',
dependencies: {
'@backstage/core': '^1.0.3',
'@backstage/theme': '^1.0.0',
'@backstage-extra/custom': '^1.1.0',
'@backstage-extra/custom-two': '^1.0.0',
},
}),
});
jest
.spyOn(paths, 'resolveTargetRoot')
.mockImplementation((...path) => resolvePath('/', ...path));
jest.spyOn(runObj, 'runPlain').mockImplementation(async (...[, , , name]) =>
JSON.stringify({
type: 'inspect',
data: {
name: name,
'dist-tags': {
latest: REGISTRY_VERSIONS[name],
},
},
}),
);
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
const { log: logs } = await withLogCollector(['log'], async () => {
await bump({ pattern: '@{backstage,backstage-extra}/*' } as any);
});
expect(logs.filter(Boolean)).toEqual([
'Using custom pattern glob @{backstage,backstage-extra}/*',
'Checking for updates of @backstage/theme',
'Checking for updates of @backstage-extra/custom-two',
'Checking for updates of @backstage-extra/custom',
'Checking for updates of @backstage/core',
'Checking for updates of @backstage/core-api',
'Some packages are outdated, updating',
'unlocking @backstage-extra/custom@^1.0.1 ~> 1.1.0',
'unlocking @backstage/core@^1.0.3 ~> 1.0.6',
'unlocking @backstage/core-api@^1.0.6 ~> 1.0.7',
'unlocking @backstage/core-api@^1.0.3 ~> 1.0.7',
'bumping @backstage-extra/custom-two in a to ^2.0.0',
'bumping @backstage/theme in b to ^2.0.0',
'bumping @backstage-extra/custom-two in b to ^2.0.0',
'Running yarn install to install new versions',
'⚠️ The following packages may have breaking changes:',
' @backstage-extra/custom-two : 1.0.0 ~> 2.0.0',
' @backstage/theme : 1.0.0 ~> 2.0.0',
' https://github.com/backstage/backstage/blob/master/packages/theme/CHANGELOG.md',
'Version bump complete!',
]);
expect(runObj.runPlain).toHaveBeenCalledTimes(6);
expect(runObj.runPlain).toHaveBeenCalledWith(
'yarn',
'info',
'--json',
'@backstage/core',
);
expect(runObj.runPlain).toHaveBeenCalledWith(
'yarn',
'info',
'--json',
'@backstage/theme',
);
expect(runObj.run).toHaveBeenCalledTimes(1);
expect(runObj.run).toHaveBeenCalledWith('yarn', ['install']);
const lockfileContents = await fs.readFile('/yarn.lock', 'utf8');
expect(lockfileContents).toEqual(customLockfileMockResult);
const packageA = await fs.readJson('/packages/a/package.json');
expect(packageA).toEqual({
name: 'a',
dependencies: {
'@backstage-extra/custom': '^1.0.1',
'@backstage-extra/custom-two': '^2.0.0',
'@backstage/core': '^1.0.5', // not bumped since new version is within range
},
});
const packageB = await fs.readJson('/packages/b/package.json');
expect(packageB).toEqual({
name: 'b',
dependencies: {
'@backstage-extra/custom': '^1.1.0',
'@backstage-extra/custom-two': '^2.0.0',
'@backstage/core': '^1.0.3', // not bumped
'@backstage/theme': '^2.0.0', // bumped since newer
},
});
});
it('should ignore not found packages', async () => {
// Make sure all modules involved in package discovery are in the module cache before we mock fs
await mapDependencies(paths.targetDir);
mockFs({
'/yarn.lock': lockfileMockResult,
'/lerna.json': JSON.stringify({
packages: ['packages/*'],
'/package.json': JSON.stringify({
workspaces: {
packages: ['packages/*'],
},
}),
'/packages/a/package.json': JSON.stringify({
name: 'a',
@@ -201,7 +337,6 @@ describe('bump', () => {
}),
});
paths.targetDir = '/';
jest
.spyOn(paths, 'resolveTargetRoot')
.mockImplementation((...path) => resolvePath('/', ...path));
@@ -209,9 +344,10 @@ describe('bump', () => {
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
const { log: logs } = await withLogCollector(['log'], async () => {
await bump();
await bump({ pattern: null } as unknown as Command);
});
expect(logs.filter(Boolean)).toEqual([
'Using default pattern glob @backstage/*',
'Checking for updates of @backstage/theme',
'Checking for updates of @backstage/core',
'Package info not found, ignoring package @backstage/theme',
+19 -5
View File
@@ -17,6 +17,8 @@
import fs from 'fs-extra';
import chalk from 'chalk';
import semver from 'semver';
import minimatch from 'minimatch';
import { Command } from 'commander';
import { isError } from '@backstage/errors';
import { resolve as resolvePath } from 'path';
import { run } from '../../lib/run';
@@ -26,7 +28,7 @@ import {
fetchPackageInfo,
Lockfile,
} from '../../lib/versioning';
import { includedFilter, forbiddenDuplicatesFilter } from './lint';
import { forbiddenDuplicatesFilter } from './lint';
import { BACKSTAGE_JSON } from '@backstage/cli-common';
const DEP_TYPES = [
@@ -36,6 +38,8 @@ const DEP_TYPES = [
'optionalDependencies',
];
const DEFAULT_PATTERN_GLOB = '@backstage/*';
type PkgVersionInfo = {
range: string;
target: string;
@@ -43,14 +47,22 @@ type PkgVersionInfo = {
location: string;
};
export default async () => {
export default async (cmd: Command) => {
const lockfilePath = paths.resolveTargetRoot('yarn.lock');
const lockfile = await Lockfile.load(lockfilePath);
let pattern = cmd.pattern;
if (!pattern) {
console.log(`Using default pattern glob ${DEFAULT_PATTERN_GLOB}`);
pattern = DEFAULT_PATTERN_GLOB;
} else {
console.log(`Using custom pattern glob ${pattern}`);
}
const findTargetVersion = createVersionFinder();
// First we discover all Backstage dependencies within our own repo
const dependencyMap = await mapDependencies(paths.targetDir);
const dependencyMap = await mapDependencies(paths.targetDir, pattern);
// Next check with the package registry to see which dependency ranges we need to bump
const versionBumps = new Map<string, PkgVersionInfo[]>();
@@ -88,10 +100,12 @@ export default async () => {
}
});
const filter = (name: string) => minimatch(name, pattern);
// Check for updates of transitive backstage dependencies
await workerThreads(16, lockfile.keys(), async name => {
// Only check @backstage packages and friends, we don't want this to do a full update of all deps
if (!includedFilter(name)) {
if (!filter(name)) {
return;
}
@@ -237,7 +251,7 @@ export default async () => {
// Finally we make sure the new lockfile doesn't have any duplicates
const dedupLockfile = await Lockfile.load(lockfilePath);
const result = dedupLockfile.analyze({
filter: includedFilter,
filter,
});
if (result.newVersions.length > 0) {
@@ -26,12 +26,18 @@ describe('LinkedPackageResolvePlugin', () => {
path.resolve(root, 'repo/node_modules'),
[
{
name: 'a',
location: path.resolve(root, 'external-a'),
dir: path.resolve(root, 'external-a'),
packageJson: {
name: 'a',
version: '1.0.0',
},
},
{
name: '@s/b',
location: path.resolve(root, 'external-b'),
dir: path.resolve(root, 'external-b'),
packageJson: {
name: '@s/b',
version: '1.0.0',
},
},
],
);
@@ -17,7 +17,7 @@
import { resolve as resolvePath } from 'path';
import { WebpackPluginInstance } from 'webpack';
import { isChildPath } from '@backstage/cli-common';
import { LernaPackage } from './types';
import { Package } from '@manypkg/get-packages';
// Enables proper resolution of packages when linking in external packages.
// Without this the packages would depend on dependencies in the node_modules
@@ -25,7 +25,7 @@ import { LernaPackage } from './types';
export class LinkedPackageResolvePlugin implements WebpackPluginInstance {
constructor(
private readonly targetModules: string,
private readonly packages: LernaPackage[],
private readonly packages: Package[],
) {}
apply(resolver: any) {
@@ -41,7 +41,7 @@ export class LinkedPackageResolvePlugin implements WebpackPluginInstance {
callback: () => void,
) => {
const pkg = this.packages.find(
pkge => data.path && isChildPath(pkge.location, data.path),
pkge => data.path && isChildPath(pkge.dir, data.path),
);
if (!pkg) {
callback();
@@ -51,14 +51,14 @@ export class LinkedPackageResolvePlugin implements WebpackPluginInstance {
// pkg here is an external package. We rewrite the context of any imports to resolve
// from the location of the package within the node_modules of the target root rather
// than the real location of the external package.
const modulesLocation = resolvePath(this.targetModules, pkg.name);
const modulesLocation = resolvePath(
this.targetModules,
pkg.packageJson.name,
);
const newContext = data.context?.issuer
? {
...data.context,
issuer: data.context.issuer.replace(
pkg.location,
modulesLocation,
),
issuer: data.context.issuer.replace(pkg.dir, modulesLocation),
}
: data.context;
@@ -70,7 +70,7 @@ export class LinkedPackageResolvePlugin implements WebpackPluginInstance {
{
...data,
context: newContext,
path: data.path && data.path.replace(pkg.location, modulesLocation),
path: data.path && data.path.replace(pkg.dir, modulesLocation),
},
`resolve ${data.request} in ${modulesLocation}`,
context,
+9 -19
View File
@@ -24,12 +24,13 @@ import { RunScriptWebpackPlugin } from 'run-script-webpack-plugin';
import webpack, { ProvidePlugin } from 'webpack';
import nodeExternals from 'webpack-node-externals';
import { isChildPath } from '@backstage/cli-common';
import { getPackages } from '@manypkg/get-packages';
import { optimization } from './optimization';
import { Config } from '@backstage/config';
import { BundlingPaths } from './paths';
import { transforms } from './transforms';
import { LinkedPackageResolvePlugin } from './LinkedPackageResolvePlugin';
import { BundlingOptions, BackendBundlingOptions, LernaPackage } from './types';
import { BundlingOptions, BackendBundlingOptions } from './types';
import { version } from '../../lib/version';
import { paths as cliPaths } from '../../lib/paths';
import { runPlain } from '../run';
@@ -74,25 +75,17 @@ async function readBuildInfo() {
};
}
async function loadLernaPackages(): Promise<LernaPackage[]> {
const { Project } = require('@lerna/project');
const project = new Project(cliPaths.targetDir);
return project.getPackages();
}
export async function createConfig(
paths: BundlingPaths,
options: BundlingOptions,
): Promise<webpack.Configuration> {
const { checksEnabled, isDev, frontendConfig } = options;
const packages = await loadLernaPackages();
const { plugins, loaders } = transforms(options);
// Any package that is part of the monorepo but outside the monorepo root dir need
// separate resolution logic.
const externalPkgs = packages.filter(
p => !isChildPath(paths.root, p.location),
);
const { packages } = await getPackages(cliPaths.targetDir);
const externalPkgs = packages.filter(p => !isChildPath(paths.root, p.dir));
const baseUrl = frontendConfig.getString('app.baseUrl');
const validBaseUrl = new URL(baseUrl);
@@ -253,14 +246,11 @@ export async function createBackendConfig(
const { checksEnabled, isDev } = options;
// Find all local monorepo packages and their node_modules, and mark them as external.
const packages = await await loadLernaPackages();
const localPackageNames = packages.map((p: any) => p.name);
const moduleDirs = packages.map((p: any) =>
resolvePath(p.location, 'node_modules'),
);
const externalPkgs = packages.filter(
p => !isChildPath(paths.root, p.location),
); // See frontend config
const { packages } = await getPackages(cliPaths.targetDir);
const localPackageNames = packages.map(p => p.packageJson.name);
const moduleDirs = packages.map(p => resolvePath(p.dir, 'node_modules'));
// See frontend config
const externalPkgs = packages.filter(p => !isChildPath(paths.root, p.dir));
const { loaders } = transforms(options);
+8 -13
View File
@@ -14,10 +14,10 @@
* limitations under the License.
*/
import { WebpackOptionsNormalized, WebpackPluginInstance } from 'webpack';
import TerserPlugin from 'terser-webpack-plugin';
import { WebpackOptionsNormalized } from 'webpack';
import { BundlingOptions } from './types';
import { isParallelDefault } from '../parallel';
const { ESBuildMinifyPlugin } = require('esbuild-loader');
export const optimization = (
options: BundlingOptions,
@@ -26,16 +26,11 @@ export const optimization = (
return {
minimize: !isDev,
// Only configure when parallel is explicitly overridden from the default
...(!isParallelDefault(options.parallel)
? {
minimizer: [
new TerserPlugin({
parallel: options.parallel,
}) as unknown as WebpackPluginInstance,
],
}
: {}),
minimizer: [
new ESBuildMinifyPlugin({
target: 'es2019',
}),
],
runtimeChunk: 'single',
splitChunks: {
automaticNameDelimiter: '-',
-5
View File
@@ -55,8 +55,3 @@ export type BackendServeOptions = BundlingPathsOptions & {
inspectEnabled: boolean;
inspectBrkEnabled: boolean;
};
export type LernaPackage = {
name: string;
location: string;
};
+23 -37
View File
@@ -22,6 +22,8 @@ import {
import { ConfigReader } from '@backstage/config';
import { paths } from './paths';
import { isValidUrl } from './urls';
import { getPackages } from '@manypkg/get-packages';
import { PackageGraph } from './monorepo';
type Options = {
args: string[];
@@ -40,13 +42,28 @@ export async function loadCliConfig(options: Options) {
});
// Consider all packages in the monorepo when loading in config
const { Project } = require('@lerna/project');
const project = new Project(paths.targetDir);
const packages = await project.getPackages();
const { packages } = await getPackages(paths.targetDir);
const localPackageNames = options.fromPackage
? findPackages(packages, options.fromPackage)
: packages.map((p: any) => p.name);
let localPackageNames;
if (options.fromPackage) {
if (packages.length) {
const graph = PackageGraph.fromPackages(packages);
localPackageNames = Array.from(
graph.collectPackageNames([options.fromPackage], node => {
// Workaround for Backstage main repo only, since the CLI has some artificial devDependencies
if (node.name === '@backstage/cli') {
return undefined;
}
return node.localDependencies.keys();
}),
);
} else {
// No packages: it means that it's not a monorepo (e.g. standalone plugin)
localPackageNames = [options.fromPackage];
}
} else {
localPackageNames = packages.map(p => p.packageJson.name);
}
const schema = await loadConfigSchema({
dependencies: localPackageNames,
@@ -92,34 +109,3 @@ export async function loadCliConfig(options: Options) {
throw error;
}
}
function findPackages(packages: any[], fromPackage: string): string[] {
const { PackageGraph } = require('@lerna/package-graph');
const graph = new PackageGraph(packages);
const targets = new Set<string>();
const searchNames = [fromPackage];
while (searchNames.length) {
const name = searchNames.pop()!;
if (targets.has(name)) {
continue;
}
const node = graph.get(name);
if (!node) {
throw new Error(`Package '${name}' not found`);
}
targets.add(name);
// Workaround for Backstage main repo only, since the CLI has some artificial devDependencies
if (name !== '@backstage/cli') {
searchNames.push(...node.localDependencies.keys());
}
}
return Array.from(targets);
}
@@ -0,0 +1,127 @@
/*
* Copyright 2020 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 { getPackages } from '@manypkg/get-packages';
import { paths } from '../paths';
import { PackageGraph } from './PackageGraph';
const testPackages = [
{
dir: '/packages/a',
packageJson: {
name: 'a',
version: '1.0.0',
dependencies: {
b: '1.0.0',
},
devDependencies: {
c: '1.0.0',
},
},
},
{
dir: '/packages/b',
packageJson: {
name: 'b',
version: '1.0.0',
devDependencies: {
c: '1.0.0',
},
},
},
{
dir: '/packages/c',
packageJson: {
name: 'c',
version: '1.0.0',
},
},
];
describe('PackageGraph', () => {
it('is able to construct a graph from this repo', async () => {
const { packages } = await getPackages(paths.ownDir);
const graph = PackageGraph.fromPackages(packages);
expect(graph.has('@backstage/cli')).toBe(true);
});
it('creates a graph', () => {
const graph = PackageGraph.fromPackages(testPackages);
const a = graph.get('a');
const b = graph.get('b');
const c = graph.get('c');
expect(a).toMatchObject({
name: 'a',
dir: '/packages/a',
allLocalDependencies: new Map([
['b', b],
['c', c],
]),
publishedLocalDependencies: new Map([['b', b]]),
localDependencies: new Map([['b', b]]),
localDevDependencies: new Map([['c', c]]),
localOptionalDependencies: new Map(),
});
expect(b).toMatchObject({
name: 'b',
dir: '/packages/b',
allLocalDependencies: new Map([['c', c]]),
publishedLocalDependencies: new Map(),
localDependencies: new Map(),
localDevDependencies: new Map([['c', c]]),
localOptionalDependencies: new Map(),
});
expect(c).toMatchObject({
name: 'c',
dir: '/packages/c',
allLocalDependencies: new Map(),
publishedLocalDependencies: new Map(),
localDependencies: new Map(),
localDevDependencies: new Map(),
localOptionalDependencies: new Map(),
});
});
it('collects package names', () => {
const graph = PackageGraph.fromPackages(testPackages);
expect(graph.collectPackageNames(['a'], () => undefined)).toEqual(
new Set(['a']),
);
expect(
graph.collectPackageNames(['a'], pkg => pkg.localDependencies.keys()),
).toEqual(new Set(['a', 'b']));
expect(
graph.collectPackageNames(['a'], pkg => pkg.localDevDependencies.keys()),
).toEqual(new Set(['a', 'c']));
expect(
graph.collectPackageNames(['b', 'a'], pkg =>
pkg.localDevDependencies.keys(),
),
).toEqual(new Set(['b', 'a', 'c']));
// Should not get stuck in cycles
expect(graph.collectPackageNames(['a'], () => ['a', 'b', 'c'])).toEqual(
new Set(['b', 'a', 'c']),
);
// Throws on unknown packages
expect(() => graph.collectPackageNames(['a'], () => ['unknown'])).toThrow(
`Package 'unknown' not found`,
);
});
});
@@ -0,0 +1,145 @@
/*
* Copyright 2020 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 { Package } from '@manypkg/get-packages';
type PackageJSON = Package['packageJson'];
export interface ExtendedPackageJSON extends PackageJSON {
scripts?: {
[key: string]: string;
};
// The `bundled` field is a field known within Backstage, it means
// that the package bundles all of its dependencies in its build output.
bundled?: boolean;
}
export type PackageGraphNode = {
/** The name of the package */
name: string;
/** The directory of the package */
dir: string;
/** The package data of the package itself */
packageJson: ExtendedPackageJSON;
/** All direct local dependencies of the package */
allLocalDependencies: Map<string, PackageGraphNode>;
/** All direct local dependencies that will be present in the published package */
publishedLocalDependencies: Map<string, PackageGraphNode>;
/** Local dependencies */
localDependencies: Map<string, PackageGraphNode>;
/** Local devDependencies */
localDevDependencies: Map<string, PackageGraphNode>;
/** Local optionalDependencies */
localOptionalDependencies: Map<string, PackageGraphNode>;
};
export class PackageGraph extends Map<string, PackageGraphNode> {
static fromPackages(packages: Package[]): PackageGraph {
const graph = new PackageGraph();
// Add all local packages to the graph
for (const pkg of packages) {
const name = pkg.packageJson.name;
const existingPkg = graph.get(name);
if (existingPkg) {
throw new Error(
`Duplicate package name '${name}' at ${pkg.dir} and ${existingPkg.dir}`,
);
}
graph.set(name, {
name,
dir: pkg.dir,
packageJson: pkg.packageJson as ExtendedPackageJSON,
allLocalDependencies: new Map(),
publishedLocalDependencies: new Map(),
localDependencies: new Map(),
localDevDependencies: new Map(),
localOptionalDependencies: new Map(),
});
}
// Populate the local dependency structure
for (const node of graph.values()) {
for (const depName of Object.keys(node.packageJson.dependencies || {})) {
const depPkg = graph.get(depName);
if (depPkg) {
node.allLocalDependencies.set(depName, depPkg);
node.publishedLocalDependencies.set(depName, depPkg);
node.localDependencies.set(depName, depPkg);
}
}
for (const depName of Object.keys(
node.packageJson.devDependencies || {},
)) {
const depPkg = graph.get(depName);
if (depPkg) {
node.allLocalDependencies.set(depName, depPkg);
node.localDevDependencies.set(depName, depPkg);
}
}
for (const depName of Object.keys(
node.packageJson.optionalDependencies || {},
)) {
const depPkg = graph.get(depName);
if (depPkg) {
node.allLocalDependencies.set(depName, depPkg);
node.publishedLocalDependencies.set(depName, depPkg);
node.localOptionalDependencies.set(depName, depPkg);
}
}
}
return graph;
}
/**
* Traverses the package graph and collects a set of package names.
*
* The traversal starts at the provided list names, and continues
* throughout all the names returned by the `collectFn`, which is
* called once for each seen package.
*/
collectPackageNames(
startingPackageNames: string[],
collectFn: (pkg: PackageGraphNode) => Iterable<string> | undefined,
): Set<string> {
const targets = new Set<string>();
const searchNames = startingPackageNames.slice();
while (searchNames.length) {
const name = searchNames.pop()!;
if (targets.has(name)) {
continue;
}
const node = this.get(name);
if (!node) {
throw new Error(`Package '${name}' not found`);
}
targets.add(name);
const collected = collectFn(node);
if (collected) {
searchNames.push(...collected);
}
}
return targets;
}
}
+18
View File
@@ -0,0 +1,18 @@
/*
* Copyright 2022 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 { PackageGraph } from './PackageGraph';
export type { PackageGraphNode } from './PackageGraph';
+21 -55
View File
@@ -29,6 +29,8 @@ import {
dependencies as cliDependencies,
devDependencies as cliDevDependencies,
} from '../../../package.json';
import { getPackages } from '@manypkg/get-packages';
import { PackageGraph, PackageGraphNode } from '../monorepo';
// These packages aren't safe to pack in parallel since the CLI depends on them
const UNSAFE_PACKAGES = [
@@ -36,14 +38,6 @@ const UNSAFE_PACKAGES = [
...Object.keys(cliDevDependencies),
];
type LernaPackage = {
name: string;
private: boolean;
location: string;
scripts: Record<string, string>;
get(key: string): any;
};
type FileEntry =
| string
| {
@@ -102,7 +96,17 @@ export async function createDistWorkspace(
options.targetDir ??
(await fs.mkdtemp(resolvePath(tmpdir(), 'dist-workspace')));
const targets = await findTargetPackages(packageNames);
const { packages } = await getPackages(paths.targetDir);
const packageGraph = PackageGraph.fromPackages(packages);
const targetNames = packageGraph.collectPackageNames(packageNames, node => {
// Don't include dependencies of packages that are marked as bundled
if (node.packageJson.bundled) {
return undefined;
}
return node.publishedLocalDependencies.keys();
});
const targets = Array.from(targetNames).map(name => packageGraph.get(name)!);
if (options.buildDependencies) {
const exclude = options.buildExcludes ?? [];
@@ -133,7 +137,7 @@ export async function createDistWorkspace(
if (options.skeleton) {
const skeletonFiles = targets.map(target => {
const dir = relativePath(paths.targetRoot, target.location);
const dir = relativePath(paths.targetRoot, target.dir);
return joinPath(dir, 'package.json');
});
@@ -154,21 +158,21 @@ export async function createDistWorkspace(
async function moveToDistWorkspace(
workspaceDir: string,
localPackages: LernaPackage[],
localPackages: PackageGraphNode[],
): Promise<void> {
async function pack(target: LernaPackage, archive: string) {
async function pack(target: PackageGraphNode, archive: string) {
console.log(`Repacking ${target.name} into dist workspace`);
const archivePath = resolvePath(workspaceDir, archive);
await run('yarn', ['pack', '--filename', archivePath], {
cwd: target.location,
cwd: target.dir,
});
// TODO(Rugvip): yarn pack doesn't call postpack, once the bug is fixed this can be removed
if (target.scripts.postpack) {
await run('yarn', ['postpack'], { cwd: target.location });
if (target.packageJson?.scripts?.postpack) {
await run('yarn', ['postpack'], { cwd: target.dir });
}
const outputDir = relativePath(paths.targetRoot, target.location);
const outputDir = relativePath(paths.targetRoot, target.dir);
const absoluteOutputPath = resolvePath(workspaceDir, outputDir);
await fs.ensureDir(absoluteOutputPath);
@@ -181,7 +185,7 @@ async function moveToDistWorkspace(
// We remove the dependencies from package.json of packages that are marked
// as bundled, so that yarn doesn't try to install them.
if (target.get('bundled')) {
if (target.packageJson.bundled) {
const pkgJson = await fs.readJson(
resolvePath(absoluteOutputPath, 'package.json'),
);
@@ -220,41 +224,3 @@ async function moveToDistWorkspace(
),
);
}
async function findTargetPackages(pkgNames: string[]): Promise<LernaPackage[]> {
const { Project } = require('@lerna/project');
const { PackageGraph } = require('@lerna/package-graph');
const project = new Project(paths.targetDir);
const packages = await project.getPackages();
const graph = new PackageGraph(packages);
const targets = new Map<string, any>();
const searchNames = pkgNames.slice();
while (searchNames.length) {
const name = searchNames.pop()!;
if (targets.has(name)) {
continue;
}
const node = graph.get(name);
if (!node) {
throw new Error(`Package '${name}' not found`);
}
// Don't include dependencies of packages that are marked as bundled
if (!node.pkg.get('bundled')) {
const pkgDeps = Object.keys(node.pkg.dependencies ?? {});
const localDeps: string[] = Array.from(node.localDependencies.keys());
const filteredDeps = localDeps.filter(dep => pkgDeps.includes(dep));
searchNames.push(...filteredDeps);
}
targets.set(name, node.pkg);
}
return Array.from(targets.values());
}
@@ -17,7 +17,6 @@
import mockFs from 'mock-fs';
import path from 'path';
import * as runObj from '../run';
import { paths } from '../paths';
import { fetchPackageInfo, mapDependencies } from './packages';
import { NotFoundError } from '../errors';
@@ -58,22 +57,19 @@ describe('mapDependencies', () => {
});
it('should read dependencies', async () => {
// Make sure all modules involved in package discovery are in the module cache before we mock fs
const { Project } = require('@lerna/project');
const project = new Project(paths.targetDir);
await project.getPackages();
mockFs({
'lerna.json': JSON.stringify({
packages: ['pkgs/*'],
'/root/package.json': JSON.stringify({
workspaces: {
packages: ['pkgs/*'],
},
}),
'pkgs/a/package.json': JSON.stringify({
'/root/pkgs/a/package.json': JSON.stringify({
name: 'a',
dependencies: {
'@backstage/core': '1 || 2',
},
}),
'pkgs/b/package.json': JSON.stringify({
'/root/pkgs/b/package.json': JSON.stringify({
name: 'b',
dependencies: {
'@backstage/core': '3',
@@ -82,7 +78,7 @@ describe('mapDependencies', () => {
}),
});
const dependencyMap = await mapDependencies(paths.targetDir);
const dependencyMap = await mapDependencies('/root', '@backstage/*');
expect(Array.from(dependencyMap)).toEqual([
[
'@backstage/core',
@@ -90,12 +86,12 @@ describe('mapDependencies', () => {
{
name: 'a',
range: '1 || 2',
location: path.resolve('pkgs', 'a'),
location: path.resolve('/root/pkgs/a'),
},
{
name: 'b',
range: '3',
location: path.resolve('pkgs', 'b'),
location: path.resolve('/root/pkgs/b'),
},
],
],
@@ -105,7 +101,7 @@ describe('mapDependencies', () => {
{
name: 'b',
range: '^0',
location: path.resolve('pkgs', 'b'),
location: path.resolve('/root/pkgs/b'),
},
],
],
+9 -11
View File
@@ -13,18 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import minimatch from 'minimatch';
import { getPackages } from '@manypkg/get-packages';
import { runPlain } from '../../lib/run';
import { NotFoundError } from '../errors';
const PREFIX = '@backstage';
const DEP_TYPES = [
'dependencies',
'devDependencies',
'peerDependencies',
'optionalDependencies',
];
] as const;
// Package data as returned by `yarn info`
export type YarnInfoInspectData = {
@@ -66,25 +65,24 @@ export async function fetchPackageInfo(
/** Map all dependencies in the repo as dependency => dependents */
export async function mapDependencies(
targetDir: string,
pattern: string,
): Promise<Map<string, PkgVersionInfo[]>> {
const { Project } = require('@lerna/project');
const project = new Project(targetDir);
const packages = await project.getPackages();
const { packages } = await getPackages(targetDir);
const dependencyMap = new Map<string, PkgVersionInfo[]>();
for (const pkg of packages) {
const deps = DEP_TYPES.flatMap(
t => Object.entries(pkg.get(t) ?? {}) as [string, string][],
t => Object.entries(pkg.packageJson[t] ?? {}) as [string, string][],
);
for (const [name, range] of deps) {
if (name.startsWith(PREFIX)) {
if (minimatch(name, pattern)) {
dependencyMap.set(
name,
(dependencyMap.get(name) ?? []).concat({
range,
name: pkg.name,
location: pkg.location,
name: pkg.packageJson.name,
location: pkg.dir,
}),
);
}
@@ -5,7 +5,7 @@
* 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
* 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,
@@ -2,7 +2,7 @@ import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import { Table, TableColumn, Progress } from '@backstage/core-components';
import Alert from '@material-ui/lab/Alert';
import { useAsync } from 'react-use';
import useAsync from 'react-use/lib/useAsync';
const useStyles = makeStyles({
avatar: {
+1 -1
View File
@@ -10,7 +10,7 @@
/>
<title>Backstage</title>
</head>
<body style="margin: 0">
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
+9
View File
@@ -1,5 +1,14 @@
# @backstage/codemods
## 0.1.28
### Patch Changes
- Updated dependencies
- @backstage/core-plugin-api@0.4.1
- @backstage/core-app-api@0.3.1
- @backstage/core-components@0.8.3
## 0.1.27
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/codemods",
"description": "A collection of codemods for Backstage projects",
"version": "0.1.27",
"version": "0.1.28",
"private": false,
"publishConfig": {
"access": "public",
+6
View File
@@ -1,5 +1,11 @@
# @backstage/config-loader
## 0.9.1
### Patch Changes
- 84663d59a3: Bump `typescript-json-schema` from `^0.51.0` to `^0.52.0`.
## 0.9.0
### Minor Changes
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/config-loader",
"description": "Config loading functionality used by Backstage backend, and CLI",
"version": "0.9.0",
"version": "0.9.1",
"private": false,
"publishConfig": {
"access": "public",
@@ -42,7 +42,7 @@
"json-schema-merge-allof": "^0.8.1",
"json-schema-traverse": "^1.0.0",
"node-fetch": "^2.6.1",
"typescript-json-schema": "^0.51.0",
"typescript-json-schema": "^0.52.0",
"yaml": "^1.9.2",
"yup": "^0.32.9"
},
+55 -8
View File
@@ -265,6 +265,19 @@ describe('ConfigReader', () => {
withLogCollector(() => config.getOptionalConfigArray('b')),
).toMatchObject({ warn: [] });
});
it('should coerce number strings to numbers', () => {
const config = ConfigReader.fromConfigs([
{
data: {
port: '123',
},
context: '1',
},
]);
expect(config.getNumber('port')).toEqual(123);
});
});
describe('ConfigReader with fallback', () => {
@@ -661,16 +674,50 @@ describe('ConfigReader.get()', () => {
});
});
it('coerces number strings to numbers', () => {
const config = ConfigReader.fromConfigs([
{
data: {
port: '123',
},
context: '1',
it('should return deep clones of the backing data', () => {
const data1 = {
foo: {
bar: [],
baz: {},
},
};
const data2 = {
x: {
y: {
z: {},
},
},
};
const reader = ConfigReader.fromConfigs([
{ data: data1, context: '1' },
{ data: data2, context: '2' },
]);
expect(config.getNumber('port')).toEqual(123);
reader.get<any>().foo.bar.push(1);
reader.get<any>('foo').bar.push(1);
reader.get<any>('foo.bar').push(1);
reader.get<any>().foo.baz.x = 1;
reader.get<any>('foo').baz.x = 1;
reader.get<any>('foo.baz').x = 1;
reader.get<any>().x.y.z.w = 1;
reader.get<any>('x').y.z.w = 1;
reader.get<any>('x.y').z.w = 1;
reader.get<any>('x.y.z').w = 1;
const readerSingle = ConfigReader.fromConfigs([
{ data: data1, context: '1' },
]);
readerSingle.get<any>().foo.bar.push(1);
readerSingle.get<any>('foo').bar.push(1);
readerSingle.get<any>('foo.bar').push(1);
readerSingle.get<any>().foo.baz.x = 1;
readerSingle.get<any>('foo').baz.x = 1;
readerSingle.get<any>('foo.baz').x = 1;
expect(data1.foo.bar).toEqual([]);
expect(data1.foo.baz).toEqual({});
expect(data2.x.y.z).toEqual({});
});
});
+3 -6
View File
@@ -126,7 +126,7 @@ export class ConfigReader implements Config {
/** {@inheritdoc Config.getOptional} */
getOptional<T = JsonValue>(key?: string): T | undefined {
const value = this.readValue(key);
const value = cloneDeep(this.readValue(key));
const fallbackValue = this.fallback?.getOptional<T>(key);
if (value === undefined) {
@@ -153,11 +153,8 @@ export class ConfigReader implements Config {
// Avoid merging arrays and primitive values, since that's how merging works for other
// methods for reading config.
return mergeWith(
{},
{ value: cloneDeep(fallbackValue) },
{ value },
(into, from) => (!isObject(from) || !isObject(into) ? from : undefined),
return mergeWith({}, { value: fallbackValue }, { value }, (into, from) =>
!isObject(from) || !isObject(into) ? from : undefined,
).value as T;
}
+9
View File
@@ -1,5 +1,14 @@
# @backstage/core-app-api
## 0.3.1
### Patch Changes
- 4ce51ab0f1: Internal refactor of the `react-use` imports to use `react-use/lib/*` instead.
- Updated dependencies
- @backstage/core-plugin-api@0.4.1
- @backstage/core-components@0.8.3
## 0.3.0
### Minor Changes
+15 -26
View File
@@ -50,7 +50,6 @@ import { oktaAuthApiRef } from '@backstage/core-plugin-api';
import { oneloginAuthApiRef } from '@backstage/core-plugin-api';
import { OpenIdConnectApi } from '@backstage/core-plugin-api';
import { PendingOAuthRequest } from '@backstage/core-plugin-api';
import { PluginOutput } from '@backstage/core-plugin-api';
import { ProfileInfo } from '@backstage/core-plugin-api';
import { ProfileInfoApi } from '@backstage/core-plugin-api';
import { PropsWithChildren } from 'react';
@@ -129,21 +128,6 @@ export type ApiProviderProps = {
children: ReactNode;
};
// @public @deprecated
export class ApiRegistry implements ApiHolder {
constructor(apis: Map<string, unknown>);
// Warning: (ae-forgotten-export) The symbol "ApiRegistryBuilder" needs to be exported by the entry point index.d.ts
//
// (undocumented)
static builder(): ApiRegistryBuilder;
// Warning: (ae-forgotten-export) The symbol "ApiImpl" needs to be exported by the entry point index.d.ts
static from(apis: ApiImpl[]): ApiRegistry;
// (undocumented)
get<T>(api: ApiRef<T>): T | undefined;
static with<T>(api: ApiRef<T>, impl: T): ApiRegistry;
with<T>(api: ApiRef<T>, impl: T): ApiRegistry;
}
// @public
export class ApiResolver implements ApiHolder {
constructor(factories: ApiFactoryHolder);
@@ -208,14 +192,19 @@ export type AppOptions = {
icons: AppIcons & {
[key in string]: IconComponent;
};
plugins?: (Omit<BackstagePlugin<any, any>, 'output'> & {
output(): (
| PluginOutput
| {
type: string;
}
)[];
})[];
plugins?: Array<
BackstagePlugin<any, any> & {
output?(): Array<
| {
type: 'feature-flag';
name: string;
}
| {
type: string;
}
>;
}
>;
components: AppComponents;
themes: (Partial<AppTheme> & Omit<AppTheme, 'theme'>)[];
configLoader?: AppConfigLoader;
@@ -410,7 +399,7 @@ export class GithubAuth implements OAuthApi, SessionApi {
signOut(): Promise<void>;
}
// @public
// @public @deprecated
export type GithubSession = {
providerInfo: {
accessToken: string;
@@ -562,7 +551,7 @@ export class SamlAuth
signOut(): Promise<void>;
}
// @public
// @public @deprecated
export type SamlSession = {
userId: string;
profile: ProfileInfo;
+3 -8
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-app-api",
"description": "Core app API used by Backstage apps",
"version": "0.3.0",
"version": "0.3.1",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,15 +29,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/app-defaults": "^0.1.3",
"@backstage/core-components": "^0.8.2",
"@backstage/config": "^0.1.11",
"@backstage/core-plugin-api": "^0.4.0",
"@backstage/theme": "^0.2.14",
"@backstage/core-plugin-api": "^0.4.1",
"@backstage/types": "^0.1.1",
"@backstage/version-bridge": "^0.1.1",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@types/prop-types": "^15.7.3",
"prop-types": "^15.7.2",
"react-router-dom": "6.0.0-beta.0",
@@ -50,7 +45,7 @@
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.10.3",
"@backstage/cli": "^0.10.5",
"@backstage/test-utils": "^0.2.0",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
@@ -1,11 +1,5 @@
import {
ErrorApi,
ErrorApiError,
ErrorApiErrorContext,
} from '@backstage/core-plugin-api';
/*
* Copyright 2020 Spotify AB
* Copyright 2020 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.
@@ -20,6 +14,12 @@ import {
* limitations under the License.
*/
import {
ErrorApi,
ErrorApiError,
ErrorApiErrorContext,
} from '@backstage/core-plugin-api';
/**
* Utility class that helps with error forwarding.
*
@@ -17,10 +17,13 @@
import { ProfileInfo, BackstageIdentity } from '@backstage/core-plugin-api';
import { z } from 'zod';
// TODO(Rugvip): Make GithubSession internal
/**
* Session information for GitHub auth.
*
* @public
* @deprecated This type is internal and will be removed
*/
export type GithubSession = {
providerInfo: {
@@ -29,6 +32,7 @@ export type GithubSession = {
expiresAt?: Date;
};
profile: ProfileInfo;
// TODO(Rugvip): This should be made optional once the type is no longer public
backstageIdentity: BackstageIdentity;
};
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { default as SamlAuth } from './SamlAuth';
export type { SamlSession } from './types';
export type { ExportedSamlSession as SamlSession } from './types';
@@ -21,15 +21,22 @@ import { z } from 'zod';
* Session information for SAML auth.
*
* @public
* @deprecated This type is internal and will be removed
*/
export type SamlSession = {
export type ExportedSamlSession = {
userId: string;
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
};
/** @internal */
export type SamlSession = {
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
};
/** @internal */
export const samlSessionSchema: z.ZodSchema<SamlSession> = z.object({
userId: z.string(),
profile: z.object({
email: z.string().optional(),
displayName: z.string().optional(),
@@ -18,6 +18,7 @@ import { ApiRef, ApiHolder } from '@backstage/core-plugin-api';
type ApiImpl<T = unknown> = readonly [ApiRef<T>, T];
/** @internal */
class ApiRegistryBuilder {
private apis: [string, unknown][] = [];
@@ -35,8 +36,7 @@ class ApiRegistryBuilder {
/**
* A registry for utility APIs.
*
* @public
* @deprecated Will be removed, use {@link @backstage/test-utils#TestApiProvider} or {@link @backstage/test-utils#TestApiRegistry} instead.
* @internal
*/
export class ApiRegistry implements ApiHolder {
static builder() {
@@ -16,7 +16,6 @@
export { ApiProvider } from './ApiProvider';
export type { ApiProviderProps } from './ApiProvider';
export { ApiRegistry } from './ApiRegistry';
export { ApiResolver } from './ApiResolver';
export { ApiFactoryRegistry } from './ApiFactoryRegistry';
export type { ApiFactoryScope } from './ApiFactoryRegistry';
@@ -316,7 +316,7 @@ describe('Integration Test', () => {
plugins: [
createPlugin({
id: 'test',
register: p => p.featureFlags.register('name'),
featureFlags: [{ name: 'name' }],
}),
],
components,
@@ -384,8 +384,24 @@ describe('Integration Test', () => {
name: 'foo',
},
],
register: p => p.featureFlags.register('name'),
}),
// We still support consuming the old feature flag API for a little while longer
{
getId() {
return 'old-test';
},
getApis() {
return [];
},
output() {
return [
{
type: 'feature-flag',
name: 'old-feature-flag',
},
];
},
} as any,
],
components,
configLoader: async () => [],
@@ -412,12 +428,12 @@ describe('Integration Test', () => {
);
expect(storageFlags.registerFlag).toHaveBeenCalledWith({
name: 'name',
name: 'foo',
pluginId: 'test',
});
expect(storageFlags.registerFlag).toHaveBeenCalledWith({
name: 'foo',
pluginId: 'test',
name: 'old-feature-flag',
pluginId: 'old-test',
});
});
+37 -21
View File
@@ -24,10 +24,9 @@ import React, {
useState,
} from 'react';
import { Route, Routes } from 'react-router-dom';
import { useAsync } from 'react-use';
import useAsync from 'react-use/lib/useAsync';
import {
ApiProvider,
ApiRegistry,
AppThemeSelector,
ConfigReader,
LocalStorageFeatureFlags,
@@ -50,7 +49,6 @@ import {
SubRouteRef,
ExternalRouteRef,
} from '@backstage/core-plugin-api';
import { UserIdentity } from '@backstage/core-components';
import { ApiFactoryRegistry, ApiResolver } from '../apis/system';
import {
childDiscoverer,
@@ -80,6 +78,13 @@ import {
} from './types';
import { AppThemeProvider } from './AppThemeProvider';
import { defaultConfigLoader } from './defaultConfigLoader';
import { ApiRegistry } from '../apis/system/ApiRegistry';
type CompatiblePlugin =
| BackstagePlugin<any, any>
| (Omit<BackstagePlugin<any, any>, 'getFeatureFlags'> & {
output(): Array<{ type: 'feature-flag'; name: string }>;
});
export function generateBoundRoutes(bindRoutes: AppOptions['bindRoutes']) {
const result = new Map<ExternalRouteRef, RouteRef | SubRouteRef>();
@@ -149,7 +154,7 @@ function useConfigLoader(
if (noConfigNode) {
return {
node: (
<ApiProvider apis={ApiRegistry.from([[appThemeApiRef, appThemeApi]])}>
<ApiProvider apis={ApiRegistry.with(appThemeApiRef, appThemeApi)}>
<ThemeProvider>{noConfigNode}</ThemeProvider>
</ApiProvider>
),
@@ -183,7 +188,7 @@ export class AppManager implements BackstageApp {
private readonly apis: Iterable<AnyApiFactory>;
private readonly icons: NonNullable<AppOptions['icons']>;
private readonly plugins: Set<BackstagePlugin<any, any>>;
private readonly plugins: Set<CompatiblePlugin>;
private readonly components: AppComponents;
private readonly themes: AppTheme[];
private readonly configLoader?: AppConfigLoader;
@@ -196,9 +201,7 @@ export class AppManager implements BackstageApp {
constructor(options: AppOptions) {
this.apis = options.apis ?? [];
this.icons = options.icons;
this.plugins = new Set(
(options.plugins as BackstagePlugin<any, any>[]) ?? [],
);
this.plugins = new Set((options.plugins as CompatiblePlugin[]) ?? []);
this.components = options.components;
this.themes = options.themes as AppTheme[];
this.configLoader = options.configLoader ?? defaultConfigLoader;
@@ -208,7 +211,7 @@ export class AppManager implements BackstageApp {
}
getPlugins(): BackstagePlugin<any, any>[] {
return Array.from(this.plugins);
return Array.from(this.plugins) as BackstagePlugin<any, any>[];
}
getSystemIcon(key: string): IconComponent | undefined {
@@ -282,16 +285,11 @@ export class AppManager implements BackstageApp {
}
} else {
for (const output of plugin.output()) {
switch (output.type) {
case 'feature-flag': {
featureFlagsApi.registerFlag({
name: output.name,
pluginId: plugin.getId(),
});
break;
}
default:
break;
if (output.type === 'feature-flag') {
featureFlagsApi.registerFlag({
name: output.name,
pluginId: plugin.getId(),
});
}
}
}
@@ -361,7 +359,25 @@ export class AppManager implements BackstageApp {
// If the app hasn't configured a sign-in page, we just continue as guest.
if (!SignInPageComponent) {
this.appIdentityProxy.setTarget(UserIdentity.createGuest());
this.appIdentityProxy.setTarget({
getUserId: () => 'guest',
getIdToken: async () => undefined,
getProfile: () => ({
email: 'guest@example.com',
displayName: 'Guest',
}),
getProfileInfo: async () => ({
email: 'guest@example.com',
displayName: 'Guest',
}),
getBackstageIdentity: async () => ({
type: 'user',
userEntityRef: 'user:default/guest',
ownershipEntityRefs: ['user:default/guest'],
}),
getCredentials: async () => ({}),
signOut: async () => {},
});
return (
<RouterComponent>
@@ -468,7 +484,7 @@ export class AppManager implements BackstageApp {
return this.apiHolder;
}
private verifyPlugins(plugins: Iterable<BackstagePlugin>) {
private verifyPlugins(plugins: Iterable<CompatiblePlugin>) {
const pluginIds = new Set<string>();
for (const plugin of plugins) {
@@ -16,7 +16,7 @@
import React, { useMemo, useEffect, useState, PropsWithChildren } from 'react';
import { useApi, appThemeApiRef, AppTheme } from '@backstage/core-plugin-api';
import { useObservable } from 'react-use';
import useObservable from 'react-use/lib/useObservable';
// This tries to find the most accurate match, but also falls back to less
// accurate results in order to avoid errors.
+7 -4
View File
@@ -24,7 +24,6 @@ import {
RouteRef,
SubRouteRef,
ExternalRouteRef,
PluginOutput,
IdentityApi,
} from '@backstage/core-plugin-api';
import { AppConfig } from '@backstage/config';
@@ -232,9 +231,13 @@ export type AppOptions = {
/**
* A list of all plugins to include in the app.
*/
plugins?: (Omit<BackstagePlugin<any, any>, 'output'> & {
output(): (PluginOutput | { type: string })[];
})[];
plugins?: Array<
BackstagePlugin<any, any> & {
output?(): Array<
{ type: 'feature-flag'; name: string } | { type: string }
>; // support for old plugins
}
>;
/**
* Supply components to the app to override the default ones.
+8
View File
@@ -1,5 +1,13 @@
# @backstage/core-components
## 0.8.3
### Patch Changes
- 4ce51ab0f1: Internal refactor of the `react-use` imports to use `react-use/lib/*` instead.
- Updated dependencies
- @backstage/core-plugin-api@0.4.1
## 0.8.2
### Patch Changes
-15
View File
@@ -151,7 +151,6 @@ export function CodeSnippet(props: CodeSnippetProps): JSX.Element;
export interface CodeSnippetProps {
customStyle?: any;
highlightedNumbers?: number[];
// Warning: (tsdoc-reference-missing-identifier) Syntax error in declaration reference: expecting a member identifier
language: string;
showCopyCodeButton?: boolean;
showLineNumbers?: boolean;
@@ -527,8 +526,6 @@ export type InfoCardVariants = 'flex' | 'fullHeight' | 'gridItem';
// @public
export function IntroCard(props: IntroCardProps): JSX.Element;
// Warning: (tsdoc-malformed-html-name) Invalid HTML element: Expecting an HTML name
// Warning: (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag
// Warning: (ae-forgotten-export) The symbol "ItemCardProps" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "ItemCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -2415,18 +2412,6 @@ export function useSupportConfig(): SupportConfig;
// @public (undocumented)
export function WarningIcon(props: IconComponentProps): JSX.Element;
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// Warning: (tsdoc-param-tag-with-invalid-optional-name) The @param should not include a JSDoc-style optional name; it must not be enclosed in '[ ]' brackets.
// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}'
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// Warning: (tsdoc-param-tag-with-invalid-optional-name) The @param should not include a JSDoc-style optional name; it must not be enclosed in '[ ]' brackets.
// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}'
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// Warning: (tsdoc-param-tag-with-invalid-optional-name) The @param should not include a JSDoc-style optional name; it must not be enclosed in '[ ]' brackets.
// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}'
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// Warning: (tsdoc-param-tag-with-invalid-optional-name) The @param should not include a JSDoc-style optional name; it must not be enclosed in '[ ]' brackets.
// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}'
// Warning: (ae-forgotten-export) The symbol "WarningProps" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "WarningPanel" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//

Some files were not shown because too many files have changed in this diff Show More