Merge branch 'master' of https://github.com/backstage/backstage into bazaar-columns
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Remove the "View Api" icon in the AboutCard, as the information is misleading for some users and is
|
||||
duplicated in the tabs above.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-cost-insights': patch
|
||||
---
|
||||
|
||||
Fixed generation of sample data in the example Cost Insights client
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
'@backstage/catalog-model': patch
|
||||
'@backstage/cli': patch
|
||||
'@backstage/config-loader': patch
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Update the json-schema dependency version.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
'@backstage/plugin-user-settings': patch
|
||||
---
|
||||
|
||||
Add Props Icon for Sidebar Item SidebarSearchField and Settings
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
---
|
||||
|
||||
Changing the `Header` styles to use more theme variables. With this the title `font-size` will not change on resizing the window.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/create-app': patch
|
||||
---
|
||||
|
||||
Refactor and add regression tests for create-app tasks
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
Paths can be specified in backend.reading.allow to further restrict allowed targets
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/core-app-api': patch
|
||||
'@backstage/test-utils': patch
|
||||
---
|
||||
|
||||
Migrated to using new `ErrorApiError` and `ErrorApiErrorContext` names.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
---
|
||||
|
||||
Update OAuthAdapter to create identity.token from identity.idToken if it does not exist, and prevent overwrites to identity.toke. Update login page commonProvider to prefer .token over .idToken
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Skip empty file names when scaffolding with nunjucks
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-app-api': patch
|
||||
---
|
||||
|
||||
Deprecated the `BackstagePluginWithAnyOutput` type.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-app-api': patch
|
||||
---
|
||||
|
||||
Start exporting and marking several types as public to address errors in the API report.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Added a scaffolder backend module template for the `create` command.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-plugin-api': minor
|
||||
---
|
||||
|
||||
Remove exports of unused types(`RouteOptions` and `RoutePath`).
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
Do not redact empty or one-character strings. These imply that it's just a test or local dev, and unnecessarily ruin the log output.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs': patch
|
||||
---
|
||||
|
||||
Fixed entity triplet case handling for certain locales.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/techdocs-common': patch
|
||||
---
|
||||
|
||||
Added OpenStack Swift case migration support.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/techdocs-common': patch
|
||||
'@backstage/plugin-techdocs-backend': patch
|
||||
---
|
||||
|
||||
Allow amazon web services s3 buckets to pass an server side encryption configuration so they can publish to encrypted buckets
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs': patch
|
||||
---
|
||||
|
||||
Use a better checkbox rendering in a task list.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
bump `@rollup/plugin-commonjs` from 17.1.0 to 21.0.1
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Introduces new `backstage-cli create` command to replace `create-plugin` and make space for creating a wider array of things. The create command also adds a new template for creating isomorphic common plugin packages.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
adds getDefaultProcessor method to CatalogBuilder
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-plugin-api': patch
|
||||
---
|
||||
|
||||
Deprecated the `Error` and `ErrorContext` types, replacing them with identical `ErrorApiError` and `ErrorApiErrorContext` types.
|
||||
+4
-1
@@ -64,4 +64,7 @@
|
||||
| [SoundCloud](https://www.soundcloud.com) | [Julio Zynger](https://github.com/julioz) | Developer portal as a [humane registry](https://martinfowler.com/bliki/HumaneRegistry.html) for the organization: catalog of people, services, documentation, feature toggles, escalation policies, etc. |
|
||||
| [Volvofinans Bank](https://www.volvofinans.se) | [Johan Hammar](https://github.com/johanhammar) | Developer portal enabling engineers to manage and explore software and documentation. |
|
||||
| [Palo Alto Networks](https://www.paloaltonetworks.com) | [Jeremy Guarini](https://github.com/jeremyguarini), [Brian Lomeland](https://github.com/bbbmmmlll), [Palo Alto Networks](https://github.com/PaloAltoNetworks) | Developer portal, service catalog, documentation and tooling |
|
||||
| [Signal Iduna Group](https://www.signal-iduna.de/) | [Jonas Thomsen](https://github.com/JoThomsen) | Developer Portal, documentation, monitoring, service catalog for our insurance ecosystem
|
||||
| [Signal Iduna Group](https://www.signal-iduna.de/) | [Jonas Thomsen](https://github.com/JoThomsen) | Developer Portal, documentation, monitoring, service catalog for our insurance ecosystem |
|
||||
| [Tradeshift](https://www.tradeshift.com/) | [Soren Mathiasen](https://github.com/sorenmat) | Developer Portal: documentation, monitoring, service templates, service catalog for our micro services |
|
||||
| [Unity](https://unity.com) | [Ted Cordery](https://github.com/TeddyBallGame) | A centralized service catalog with documentation for our service engineers. |
|
||||
| [PicPay](https://www.picpay.com) | [Luis Baroni](https://github.com/lcsbaroni), [Renata Poluceno](https://github.com/renatapoluceno), [PicPay](https://github.com/picpay) | Developer portal for building services throught templates, service catalog with ownership of services, documentation and metrics providing autonomy and visibility for all. |
|
||||
|
||||
+1
-1
@@ -117,7 +117,7 @@ through the proxy.
|
||||
Learn more about [the different components](overview/what-is-backstage.md) that
|
||||
make up Backstage.
|
||||
|
||||
### Why can't I dynamically install plugins without modifications the app?
|
||||
### Why can't I dynamically install plugins without modifications to the app?
|
||||
|
||||
This decision is part of the core architecture and development flow of
|
||||
Backstage. Plugins have a lot of freedom in what they provide and how they are
|
||||
|
||||
@@ -199,7 +199,7 @@ export a class that `implements` the target API, for example:
|
||||
|
||||
```ts
|
||||
export class IgnoringErrorApi implements ErrorApi {
|
||||
post(error: Error, context?: ErrorContext) {
|
||||
post(error: ErrorApiError, context?: ErrorApiErrorContext) {
|
||||
// ignore error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +144,8 @@ spec:
|
||||
|
||||
Note that to be able to read from targets that are outside of the normal
|
||||
integration points such as `github.com`, you'll need to explicitly allow it by
|
||||
adding an entry in the `backend.reading.allow` list. For example:
|
||||
adding an entry in the `backend.reading.allow` list. Paths can be specified to
|
||||
further restrict targets For example:
|
||||
|
||||
```yml
|
||||
backend:
|
||||
@@ -153,6 +154,8 @@ backend:
|
||||
allow:
|
||||
- host: example.com
|
||||
- host: '*.examples.org'
|
||||
- host: example.net
|
||||
paths: ['/api/']
|
||||
```
|
||||
|
||||
## Common to All Kinds: The Envelope
|
||||
|
||||
@@ -106,6 +106,12 @@ techdocs:
|
||||
# This allows providers like LocalStack, Minio and Wasabi (and possibly others) to be used to host tech docs.
|
||||
s3ForcePathStyle: false
|
||||
|
||||
# (Optional) AWS Server Side Encryption
|
||||
# Defaults to undefined.
|
||||
# If not set, encrypted buckets will fail to publish.
|
||||
# https://docs.aws.amazon.com/AmazonS3/latest/userguide/specifying-s3-encryption.html
|
||||
sse: 'aws:kms' # or AES256
|
||||
|
||||
# Required when techdocs.publisher.type is set to 'azureBlobStorage'. Skip otherwise.
|
||||
|
||||
azureBlobStorage:
|
||||
|
||||
@@ -41,6 +41,7 @@ lint Lint a package
|
||||
test Run tests, forwarding args to Jest, defaulting to watch mode
|
||||
clean Delete cache directories
|
||||
|
||||
create Open up an interactive guide to creating new things in your app
|
||||
create-plugin Creates a new plugin in the current repository
|
||||
remove-plugin Removes plugin in the current repository
|
||||
|
||||
@@ -277,6 +278,44 @@ Options:
|
||||
-h, --help display help for command
|
||||
```
|
||||
|
||||
## create
|
||||
|
||||
Scope: `root`
|
||||
|
||||
The `create` command opens up an interactive guide for you to create new things
|
||||
in your app. If you do not pass in any options it is completely interactive, but
|
||||
it is possible to pre-select what you want to create using the `--select` flag,
|
||||
and provide options using `--options`, for example:
|
||||
|
||||
```bash
|
||||
backstage-cli create --select plugin --option id=foo
|
||||
```
|
||||
|
||||
This command is typically added as script in the root `package.json` to be
|
||||
executed with `yarn backstage-create`, using options that are appropriate for
|
||||
the organization that owns the app repo. For example you may have it set up like
|
||||
this:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"backstage-create": "backstage-cli create --scope internal --no-private --npm-registry https://acme.org/npm"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```text
|
||||
Usage: backstage-cli create [options]
|
||||
|
||||
Options:
|
||||
--select <name> Select the thing you want to be creating upfront
|
||||
--option <name>=<value> Pre-fill options for the creation process (default: [])
|
||||
--scope <scope> The scope to use for new packages
|
||||
--npm-registry <URL> The package registry to use for new packages
|
||||
--no-private Do not mark new packages as private
|
||||
-h, --help display help for command
|
||||
```
|
||||
|
||||
## create-plugin
|
||||
|
||||
Scope: `root`
|
||||
|
||||
+2
-1
@@ -23,7 +23,8 @@
|
||||
"lint:all": "lerna run lint --",
|
||||
"lint:type-deps": "node scripts/check-type-dependencies.js",
|
||||
"docker-build": "yarn tsc && yarn workspace example-backend build --build-dependencies && yarn workspace example-backend build-image",
|
||||
"create-plugin": "backstage-cli create-plugin --scope backstage --no-private",
|
||||
"backstage-create": "backstage-cli create --scope backstage --no-private",
|
||||
"create-plugin": "yarn backstage-create --select plugin",
|
||||
"remove-plugin": "backstage-cli remove-plugin",
|
||||
"release": "changeset version && yarn diff --yes && yarn prettier --write '{packages,plugins}/*/{package.json,CHANGELOG.md}' && yarn install",
|
||||
"prettier:check": "prettier --check .",
|
||||
|
||||
Vendored
+8
@@ -130,6 +130,14 @@ export interface Config {
|
||||
* The host may also contain a port, for example `example.com:8080`.
|
||||
*/
|
||||
host: string;
|
||||
|
||||
/**
|
||||
* An optional list of paths. In case they are present only targets matching
|
||||
* any of them will are allowed. You can use trailing slashes to make sure only
|
||||
* subdirectories are allowed, for example `/mydir/` will allow targets with
|
||||
* paths like `/mydir/a` but will block paths like `/mydir2`.
|
||||
*/
|
||||
paths?: string[];
|
||||
}>;
|
||||
};
|
||||
|
||||
|
||||
@@ -48,6 +48,19 @@ describe('rootLogger', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('redacts but ignores empty and one-character secrets', () => {
|
||||
const logger = createRootLogger();
|
||||
jest.spyOn(logger, 'write');
|
||||
setRootLoggerRedactionList(['SECRET-1', 'SECRET_2', 'Q', '']);
|
||||
logger.info('Logging SECRET-1 and SECRET_2 and Q');
|
||||
|
||||
expect(logger.write).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Logging [REDACTED] and [REDACTED] and Q',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe('createRootLogger', () => {
|
||||
it('creates a new logger', () => {
|
||||
const oldLogger = getRootLogger();
|
||||
|
||||
@@ -21,7 +21,7 @@ import { coloredFormat } from './formats';
|
||||
import { escapeRegExp } from '../util/escapeRegExp';
|
||||
|
||||
let rootLogger: winston.Logger;
|
||||
let redactionRegExp: RegExp;
|
||||
let redactionRegExp: RegExp | undefined;
|
||||
|
||||
/** @public */
|
||||
export function getRootLogger(): winston.Logger {
|
||||
@@ -34,11 +34,18 @@ export function setRootLogger(newLogger: winston.Logger) {
|
||||
}
|
||||
|
||||
export function setRootLoggerRedactionList(redactionList: string[]) {
|
||||
if (redactionList.length) {
|
||||
// Exclude secrets that are empty or just one character in length. These
|
||||
// typically mean that you are running local dev or tests, or using the
|
||||
// --lax flag which sets things to just 'x'. So exclude those.
|
||||
const filtered = redactionList.filter(r => r.length > 1);
|
||||
|
||||
if (filtered.length) {
|
||||
redactionRegExp = new RegExp(
|
||||
`(${redactionList.map(escapeRegExp).join('|')})`,
|
||||
`(${filtered.map(escapeRegExp).join('|')})`,
|
||||
'g',
|
||||
);
|
||||
} else {
|
||||
redactionRegExp = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,10 @@ describe('FetchUrlReader', () => {
|
||||
{ host: 'example.com:700' },
|
||||
{ host: '*.examples.org' },
|
||||
{ host: '*.examples.org:700' },
|
||||
{
|
||||
host: 'foobar.org',
|
||||
paths: ['/dir1/'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -106,6 +110,9 @@ describe('FetchUrlReader', () => {
|
||||
expect(predicate(new URL('https://examples.org:700/test'))).toBe(false);
|
||||
expect(predicate(new URL('https://a.examples.org:700/test'))).toBe(true);
|
||||
expect(predicate(new URL('https://a.b.examples.org:700/test'))).toBe(true);
|
||||
expect(predicate(new URL('https://foobar.org/dir1/subpath'))).toBe(true);
|
||||
expect(predicate(new URL('https://foobar.org/dir12'))).toBe(false);
|
||||
expect(predicate(new URL('https://foobar.org/'))).toBe(false);
|
||||
});
|
||||
|
||||
describe('read', () => {
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
SearchResponse,
|
||||
UrlReader,
|
||||
} from './types';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* A UrlReader that does a plain fetch of the URL.
|
||||
@@ -39,18 +40,30 @@ export class FetchUrlReader implements UrlReader {
|
||||
* `host`:
|
||||
* Either full hostnames to match, or subdomain wildcard matchers with a leading `*`.
|
||||
* For example `example.com` and `*.example.com` are valid values, `prod.*.example.com` is not.
|
||||
*
|
||||
* `paths`:
|
||||
* An optional list of paths which are allowed. If the list is omitted all paths are allowed.
|
||||
*/
|
||||
static factory: ReaderFactory = ({ config }) => {
|
||||
const predicates =
|
||||
config
|
||||
.getOptionalConfigArray('backend.reading.allow')
|
||||
?.map(allowConfig => {
|
||||
const paths = allowConfig.getOptionalStringArray('paths');
|
||||
const checkPath = paths
|
||||
? (url: URL) => {
|
||||
const targetPath = path.posix.normalize(url.pathname);
|
||||
return paths.some(allowedPath =>
|
||||
targetPath.startsWith(allowedPath),
|
||||
);
|
||||
}
|
||||
: (_url: URL) => true;
|
||||
const host = allowConfig.getString('host');
|
||||
if (host.startsWith('*.')) {
|
||||
const suffix = host.slice(1);
|
||||
return (url: URL) => url.host.endsWith(suffix);
|
||||
return (url: URL) => url.host.endsWith(suffix) && checkPath(url);
|
||||
}
|
||||
return (url: URL) => url.host === host;
|
||||
return (url: URL) => url.host === host && checkPath(url);
|
||||
}) ?? [];
|
||||
|
||||
const reader = new FetchUrlReader();
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"@types/json-schema": "^7.0.5",
|
||||
"@types/yup": "^0.29.13",
|
||||
"ajv": "^7.0.3",
|
||||
"json-schema": "^0.3.0",
|
||||
"json-schema": "^0.4.0",
|
||||
"lodash": "^4.17.21",
|
||||
"uuid": "^8.0.0",
|
||||
"yup": "^0.32.9"
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"@lerna/package-graph": "^4.0.0",
|
||||
"@lerna/project": "^4.0.0",
|
||||
"@octokit/request": "^5.4.12",
|
||||
"@rollup/plugin-commonjs": "^17.1.0",
|
||||
"@rollup/plugin-commonjs": "^21.0.1",
|
||||
"@rollup/plugin-json": "^4.0.2",
|
||||
"@rollup/plugin-node-resolve": "^13.0.0",
|
||||
"@rollup/plugin-yaml": "^3.0.0",
|
||||
@@ -81,7 +81,7 @@
|
||||
"inquirer": "^7.0.4",
|
||||
"jest": "^26.0.1",
|
||||
"jest-css-modules": "^2.1.0",
|
||||
"json-schema": "^0.3.0",
|
||||
"json-schema": "^0.4.0",
|
||||
"jest-transform-yaml": "^0.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"mini-css-extract-plugin": "^2.4.2",
|
||||
|
||||
@@ -263,7 +263,6 @@ export default async (cmd: Command) => {
|
||||
const pluginDir = isMonoRepo
|
||||
? paths.resolveTargetRoot('plugins', pluginId)
|
||||
: paths.resolveTargetRoot(pluginId);
|
||||
const ownerIds = parseOwnerIds(answers.owner);
|
||||
const { version: pluginVersion } = isMonoRepo
|
||||
? await fs.readJson(paths.resolveTargetRoot('lerna.json'))
|
||||
: { version: '0.1.0' };
|
||||
@@ -318,12 +317,8 @@ export default async (cmd: Command) => {
|
||||
await addPluginExtensionToApp(pluginId, extensionName, name);
|
||||
}
|
||||
|
||||
if (ownerIds && ownerIds.length) {
|
||||
await addCodeownersEntry(
|
||||
codeownersPath!,
|
||||
`/plugins/${pluginId}`,
|
||||
ownerIds,
|
||||
);
|
||||
if (answers.owner) {
|
||||
await addCodeownersEntry(`/plugins/${pluginId}`, answers.owner);
|
||||
}
|
||||
|
||||
Task.log();
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* 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 os from 'os';
|
||||
import fs from 'fs-extra';
|
||||
import { join as joinPath } from 'path';
|
||||
import { Command } from 'commander';
|
||||
import { FactoryRegistry } from '../../lib/create/FactoryRegistry';
|
||||
import { paths } from '../../lib/paths';
|
||||
import { assertError } from '@backstage/errors';
|
||||
import { Task } from '../../lib/tasks';
|
||||
|
||||
function parseOptions(optionStrings: string[]): Record<string, string> {
|
||||
const options: Record<string, string> = {};
|
||||
|
||||
for (const str of optionStrings) {
|
||||
const [key] = str.split('=', 1);
|
||||
const value = str.slice(key.length + 1);
|
||||
if (!key || str[key.length] !== '=') {
|
||||
throw new Error(
|
||||
`Invalid option '${str}', must be of the format <key>=<value>`,
|
||||
);
|
||||
}
|
||||
options[key] = value;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
export default async (cmd: Command) => {
|
||||
const cmdOpts = cmd.opts();
|
||||
|
||||
const factory = await FactoryRegistry.interactiveSelect(cmdOpts.select);
|
||||
|
||||
const providedOptions = parseOptions(cmdOpts.option);
|
||||
const options = await FactoryRegistry.populateOptions(
|
||||
factory,
|
||||
providedOptions,
|
||||
);
|
||||
|
||||
let isMonoRepo = false;
|
||||
try {
|
||||
const rootPackageJson = await fs.readJson(
|
||||
paths.resolveTargetRoot('package.json'),
|
||||
);
|
||||
if (rootPackageJson.workspaces) {
|
||||
isMonoRepo = true;
|
||||
}
|
||||
} catch (error) {
|
||||
assertError(error);
|
||||
if (error.code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
let defaultVersion = '0.1.0';
|
||||
try {
|
||||
const rootLernaJson = await fs.readJson(
|
||||
paths.resolveTargetRoot('lerna.json'),
|
||||
);
|
||||
if (rootLernaJson.version) {
|
||||
defaultVersion = rootLernaJson.version;
|
||||
}
|
||||
} catch (error) {
|
||||
assertError(error);
|
||||
if (error.code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const tempDirs = new Array<string>();
|
||||
async function createTemporaryDirectory(name: string): Promise<string> {
|
||||
const dir = await fs.mkdtemp(joinPath(os.tmpdir(), name));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
let modified = false;
|
||||
try {
|
||||
await factory.create(options, {
|
||||
isMonoRepo,
|
||||
defaultVersion,
|
||||
scope: cmdOpts.scope?.replace(/^@/, ''),
|
||||
npmRegistry: cmdOpts.npmRegistry,
|
||||
private: Boolean(cmdOpts.private),
|
||||
createTemporaryDirectory,
|
||||
markAsModified() {
|
||||
modified = true;
|
||||
},
|
||||
});
|
||||
|
||||
Task.log();
|
||||
Task.log(`🎉 Successfully created ${factory.name}`);
|
||||
Task.log();
|
||||
} catch (error) {
|
||||
assertError(error);
|
||||
Task.error(error.message);
|
||||
|
||||
if (modified) {
|
||||
Task.log('It seems that something went wrong in the creation process 🤔');
|
||||
Task.log();
|
||||
Task.log(
|
||||
'We have left the changes that were made intact in case you want to',
|
||||
);
|
||||
Task.log(
|
||||
'continue manually, but you can also revert the changes and try again.',
|
||||
);
|
||||
|
||||
Task.error(`🔥 Failed to create ${factory.name}!`);
|
||||
}
|
||||
} finally {
|
||||
for (const dir of tempDirs) {
|
||||
try {
|
||||
await fs.remove(dir);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to remove temporary directory '${dir}', ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -75,6 +75,30 @@ export function registerCommands(program: CommanderStatic) {
|
||||
.option(...configOption)
|
||||
.action(lazy(() => import('./backend/dev').then(m => m.default)));
|
||||
|
||||
program
|
||||
.command('create')
|
||||
.storeOptionsAsProperties(false)
|
||||
.description(
|
||||
'Open up an interactive guide to creating new things in your app',
|
||||
)
|
||||
.option(
|
||||
'--select <name>',
|
||||
'Select the thing you want to be creating upfront',
|
||||
)
|
||||
.option(
|
||||
'--option <name>=<value>',
|
||||
'Pre-fill options for the creation process',
|
||||
(opt, arr: string[]) => [...arr, opt],
|
||||
[],
|
||||
)
|
||||
.option('--scope <scope>', 'The scope to use for new packages')
|
||||
.option(
|
||||
'--npm-registry <URL>',
|
||||
'The package registry to use for new packages',
|
||||
)
|
||||
.option('--no-private', 'Do not mark new packages as private')
|
||||
.action(lazy(() => import('./create/create').then(m => m.default)));
|
||||
|
||||
program
|
||||
.command('create-plugin')
|
||||
.option(
|
||||
|
||||
@@ -177,9 +177,9 @@ describe('removePlugin', () => {
|
||||
fse.readFileSync(mockedCodeownersPath, 'utf8'),
|
||||
);
|
||||
await addCodeownersEntry(
|
||||
testFilePath!,
|
||||
path.join('plugins', testPluginName),
|
||||
['@thisIsAtestTeam', 'test@gmail.com'],
|
||||
'@thisIsAtestTeam test@gmail.com',
|
||||
testFilePath,
|
||||
);
|
||||
await removePluginFromCodeOwners(testFilePath, testPluginName);
|
||||
expect(testFileContent).toBe(codeOwnersFileContent);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { paths } from '../paths';
|
||||
|
||||
const TEAM_ID_RE = /^@[-\w]+\/[-\w]+$/;
|
||||
const USER_ID_RE = /^@[-\w]+$/;
|
||||
@@ -30,14 +31,14 @@ type CodeownersEntry = {
|
||||
export async function getCodeownersFilePath(
|
||||
rootDir: string,
|
||||
): Promise<string | undefined> {
|
||||
const paths = [
|
||||
const possiblePaths = [
|
||||
path.join(rootDir, '.github', 'CODEOWNERS'),
|
||||
path.join(rootDir, '.gitlab', 'CODEOWNERS'),
|
||||
path.join(rootDir, 'docs', 'CODEOWNERS'),
|
||||
path.join(rootDir, 'CODEOWNERS'),
|
||||
];
|
||||
|
||||
for (const p of paths) {
|
||||
for (const p of possiblePaths) {
|
||||
if (await fs.pathExists(p)) {
|
||||
return p;
|
||||
}
|
||||
@@ -55,7 +56,7 @@ export function isValidSingleOwnerId(id: string): boolean {
|
||||
}
|
||||
|
||||
export function parseOwnerIds(
|
||||
spaceSeparatedOwnerIds: string,
|
||||
spaceSeparatedOwnerIds: string | undefined,
|
||||
): string[] | undefined {
|
||||
if (!spaceSeparatedOwnerIds || typeof spaceSeparatedOwnerIds !== 'string') {
|
||||
return undefined;
|
||||
@@ -70,11 +71,24 @@ export function parseOwnerIds(
|
||||
}
|
||||
|
||||
export async function addCodeownersEntry(
|
||||
codeownersFilePath: string,
|
||||
ownedPath: string,
|
||||
ownerIds: string[],
|
||||
): Promise<void> {
|
||||
const allLines = (await fs.readFile(codeownersFilePath, 'utf8')).split('\n');
|
||||
ownerStr: string,
|
||||
codeownersFilePath?: string,
|
||||
): Promise<boolean> {
|
||||
const ownerIds = parseOwnerIds(ownerStr);
|
||||
if (!ownerIds || ownerIds.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let filePath = codeownersFilePath;
|
||||
if (!filePath) {
|
||||
filePath = await getCodeownersFilePath(paths.targetRoot);
|
||||
if (!filePath) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const allLines = (await fs.readFile(filePath, 'utf8')).split('\n');
|
||||
|
||||
// Only keep comments from the top of the file
|
||||
const commentLines = [];
|
||||
@@ -117,5 +131,7 @@ export async function addCodeownersEntry(
|
||||
|
||||
const newLines = [...commentLines, '', ...newDeclarationLines, ''];
|
||||
|
||||
await fs.writeFile(codeownersFilePath, newLines.join('\n'), 'utf8');
|
||||
await fs.writeFile(filePath, newLines.join('\n'), 'utf8');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2021 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 chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import { AnyFactory, Prompt } from './types';
|
||||
import * as factories from './factories';
|
||||
import partition from 'lodash/partition';
|
||||
|
||||
function applyPromptMessageTransforms<T>(
|
||||
prompt: Prompt<T>,
|
||||
transforms: {
|
||||
message: (msg: string) => string;
|
||||
error: (msg: string) => string;
|
||||
},
|
||||
): Prompt<T> {
|
||||
return {
|
||||
...prompt,
|
||||
message:
|
||||
prompt.message &&
|
||||
(async answers => {
|
||||
if (typeof prompt.message === 'function') {
|
||||
return transforms.message(await prompt.message(answers));
|
||||
}
|
||||
return transforms.message(await prompt.message!);
|
||||
}),
|
||||
validate:
|
||||
prompt.validate &&
|
||||
(async (...args) => {
|
||||
const result = await prompt.validate!(...args);
|
||||
if (typeof result === 'string') {
|
||||
return transforms.error(result);
|
||||
}
|
||||
return result;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export class FactoryRegistry {
|
||||
private static factoryMap = new Map<string, AnyFactory>(
|
||||
Object.values(factories).map(factory => [factory.name, factory]),
|
||||
);
|
||||
|
||||
static async interactiveSelect(preselected?: string): Promise<AnyFactory> {
|
||||
let selected = preselected;
|
||||
|
||||
if (!selected) {
|
||||
const answers = await inquirer.prompt<{ name: string }>([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'name',
|
||||
message: 'What do you want to create?',
|
||||
choices: Array.from(this.factoryMap.values()).map(factory => ({
|
||||
name: `${factory.name} - ${factory.description}`,
|
||||
value: factory.name,
|
||||
})),
|
||||
},
|
||||
]);
|
||||
selected = answers.name;
|
||||
}
|
||||
|
||||
const factory = this.factoryMap.get(selected);
|
||||
if (!factory) {
|
||||
throw new Error(`Unknown selection '${selected}'`);
|
||||
}
|
||||
return factory;
|
||||
}
|
||||
|
||||
static async populateOptions(
|
||||
factory: AnyFactory,
|
||||
provided: Record<string, string>,
|
||||
): Promise<Record<string, string>> {
|
||||
let currentOptions = provided;
|
||||
|
||||
if (factory.optionsDiscovery) {
|
||||
const discoveredOptions = await factory.optionsDiscovery();
|
||||
currentOptions = {
|
||||
...currentOptions,
|
||||
...(discoveredOptions as Record<string, string>),
|
||||
};
|
||||
}
|
||||
|
||||
if (factory.optionsPrompts) {
|
||||
const [hasAnswers, needsAnswers] = partition(
|
||||
factory.optionsPrompts,
|
||||
option => option.name in currentOptions,
|
||||
);
|
||||
|
||||
for (const option of hasAnswers) {
|
||||
const value = provided[option.name];
|
||||
|
||||
if (option.validate) {
|
||||
const result = option.validate(value);
|
||||
if (result !== true) {
|
||||
throw new Error(`Invalid option '${option.name}'. ${result}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentOptions = await inquirer.prompt(
|
||||
needsAnswers.map(option =>
|
||||
applyPromptMessageTransforms(option, {
|
||||
message: chalk.blue,
|
||||
error: chalk.red,
|
||||
}),
|
||||
),
|
||||
currentOptions,
|
||||
);
|
||||
}
|
||||
|
||||
return currentOptions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright 2021 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 fs from 'fs-extra';
|
||||
import mockFs from 'mock-fs';
|
||||
import { sep, resolve as resolvePath } from 'path';
|
||||
import { paths } from '../../paths';
|
||||
import { Task } from '../../tasks';
|
||||
import { FactoryRegistry } from '../FactoryRegistry';
|
||||
import { createMockOutputStream, mockPaths } from './common/testUtils';
|
||||
import { backendPlugin } from './backendPlugin';
|
||||
|
||||
describe('backendPlugin factory', () => {
|
||||
beforeEach(() => {
|
||||
mockPaths({
|
||||
targetRoot: '/root',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should create a backend plugin', async () => {
|
||||
mockFs({
|
||||
'/root': {
|
||||
packages: {
|
||||
backend: {
|
||||
'package.json': JSON.stringify({}),
|
||||
},
|
||||
},
|
||||
plugins: mockFs.directory(),
|
||||
},
|
||||
[paths.resolveOwn('templates')]: mockFs.load(
|
||||
paths.resolveOwn('templates'),
|
||||
),
|
||||
});
|
||||
|
||||
const options = await FactoryRegistry.populateOptions(backendPlugin, {
|
||||
id: 'test',
|
||||
});
|
||||
|
||||
let modified = false;
|
||||
|
||||
const [output, mockStream] = createMockOutputStream();
|
||||
jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream);
|
||||
jest.spyOn(Task, 'forCommand').mockResolvedValue();
|
||||
|
||||
await backendPlugin.create(options, {
|
||||
private: true,
|
||||
isMonoRepo: true,
|
||||
defaultVersion: '1.0.0',
|
||||
markAsModified: () => {
|
||||
modified = true;
|
||||
},
|
||||
createTemporaryDirectory: () => fs.mkdtemp('test'),
|
||||
});
|
||||
|
||||
expect(modified).toBe(true);
|
||||
|
||||
expect(output).toEqual([
|
||||
'',
|
||||
'Creating backend plugin backstage-plugin-test-backend',
|
||||
'Checking Prerequisites:',
|
||||
`availability plugins${sep}test-backend`,
|
||||
'creating temp dir',
|
||||
'Executing Template:',
|
||||
'copying .eslintrc.js',
|
||||
'templating README.md.hbs',
|
||||
'templating package.json.hbs',
|
||||
'copying tsconfig.json',
|
||||
'copying index.ts',
|
||||
'templating run.ts.hbs',
|
||||
'copying setupTests.ts',
|
||||
'copying router.test.ts',
|
||||
'copying router.ts',
|
||||
'templating standaloneServer.ts.hbs',
|
||||
'Installing:',
|
||||
`moving plugins${sep}test-backend`,
|
||||
'backend adding dependency',
|
||||
]);
|
||||
|
||||
await expect(
|
||||
fs.readJson('/root/packages/backend/package.json'),
|
||||
).resolves.toEqual({
|
||||
dependencies: {
|
||||
'backstage-plugin-test-backend': '^1.0.0',
|
||||
},
|
||||
});
|
||||
|
||||
expect(Task.forCommand).toHaveBeenCalledTimes(2);
|
||||
expect(Task.forCommand).toHaveBeenCalledWith('yarn install', {
|
||||
cwd: resolvePath('/root/plugins/test-backend'),
|
||||
optional: true,
|
||||
});
|
||||
expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', {
|
||||
cwd: resolvePath('/root/plugins/test-backend'),
|
||||
optional: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2021 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 fs from 'fs-extra';
|
||||
import chalk from 'chalk';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
import { paths } from '../../paths';
|
||||
import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners';
|
||||
import { createFactory, CreateContext } from '../types';
|
||||
import { addPackageDependency, Task } from '../../tasks';
|
||||
import { ownerPrompt, pluginIdPrompt } from './common/prompts';
|
||||
import { executePluginPackageTemplate } from './common/tasks';
|
||||
|
||||
type Options = {
|
||||
id: string;
|
||||
owner?: string;
|
||||
codeOwnersPath?: string;
|
||||
};
|
||||
|
||||
export const backendPlugin = createFactory<Options>({
|
||||
name: 'backend-plugin',
|
||||
description: 'A new backend plugin',
|
||||
optionsDiscovery: async () => ({
|
||||
codeOwnersPath: await getCodeownersFilePath(paths.targetRoot),
|
||||
}),
|
||||
optionsPrompts: [pluginIdPrompt(), ownerPrompt()],
|
||||
async create(options: Options, ctx: CreateContext) {
|
||||
const id = `${options.id}-backend`;
|
||||
const name = ctx.scope
|
||||
? `@${ctx.scope}/plugin-${id}`
|
||||
: `backstage-plugin-${id}`;
|
||||
|
||||
Task.log();
|
||||
Task.log(`Creating backend plugin ${chalk.cyan(name)}`);
|
||||
|
||||
const targetDir = ctx.isMonoRepo
|
||||
? paths.resolveTargetRoot('plugins', id)
|
||||
: paths.resolveTargetRoot(`backstage-plugin-${id}`);
|
||||
|
||||
await executePluginPackageTemplate(ctx, {
|
||||
targetDir,
|
||||
templateName: 'default-backend-plugin',
|
||||
values: {
|
||||
id,
|
||||
name,
|
||||
pluginVar: `${camelCase(id)}Plugin`,
|
||||
pluginVersion: ctx.defaultVersion,
|
||||
privatePackage: ctx.private,
|
||||
npmRegistry: ctx.npmRegistry,
|
||||
},
|
||||
});
|
||||
|
||||
if (await fs.pathExists(paths.resolveTargetRoot('packages/backend'))) {
|
||||
await Task.forItem('backend', 'adding dependency', async () => {
|
||||
await addPackageDependency(
|
||||
paths.resolveTargetRoot('packages/backend/package.json'),
|
||||
{
|
||||
dependencies: {
|
||||
[name]: `^${ctx.defaultVersion}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (options.owner) {
|
||||
await addCodeownersEntry(`/plugins/${id}`, options.owner);
|
||||
}
|
||||
|
||||
await Task.forCommand('yarn install', { cwd: targetDir, optional: true });
|
||||
await Task.forCommand('yarn lint --fix', {
|
||||
cwd: targetDir,
|
||||
optional: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2021 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 { Prompt } from '../../types';
|
||||
import { parseOwnerIds } from '../../../codeowners';
|
||||
|
||||
export function pluginIdPrompt(): Prompt<{ id: string }> {
|
||||
return {
|
||||
type: 'input',
|
||||
name: 'id',
|
||||
message: 'Enter the ID of the plugin [required]',
|
||||
validate: (value: string) => {
|
||||
if (!value) {
|
||||
return 'Please enter the ID of the plugin';
|
||||
} else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) {
|
||||
return 'Plugin IDs must be lowercase and contain only letters, digits, and dashes.';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function ownerPrompt(): Prompt<{
|
||||
owner?: string;
|
||||
codeOwnersPath?: string;
|
||||
}> {
|
||||
return {
|
||||
type: 'input',
|
||||
name: 'owner',
|
||||
message: 'Enter an owner to add to CODEOWNERS [optional]',
|
||||
when: opts => Boolean(opts.codeOwnersPath),
|
||||
validate: (value: string) => {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const ownerIds = parseOwnerIds(value);
|
||||
if (!ownerIds) {
|
||||
return 'The owner must be a space separated list of team names (e.g. @org/team-name), usernames (e.g. @username), or the email addresses (e.g. user@example.com).';
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright 2021 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 fs from 'fs-extra';
|
||||
import mockFs from 'mock-fs';
|
||||
import { sep } from 'path';
|
||||
import { createMockOutputStream, mockPaths } from './testUtils';
|
||||
import { CreateContext } from '../../types';
|
||||
import { executePluginPackageTemplate } from './tasks';
|
||||
|
||||
mockPaths({
|
||||
ownDir: '/own',
|
||||
targetRoot: '/root',
|
||||
});
|
||||
|
||||
describe('executePluginPackageTemplate', () => {
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should execute template', async () => {
|
||||
mockFs({
|
||||
'/root': {
|
||||
'yarn.lock': `
|
||||
some-package@^1.1.0:
|
||||
version "1.5.0"
|
||||
`,
|
||||
},
|
||||
'/own': {
|
||||
templates: {
|
||||
'test-template': {
|
||||
'package.json.hbs': `
|
||||
{
|
||||
"name": "my-{{id}}-plugin",
|
||||
{{#if makePrivate}}
|
||||
"private": true,
|
||||
{{/if}}
|
||||
"description": "testing",
|
||||
"dependencies": {
|
||||
"some-package": "{{ versionQuery 'some-package' '1.3.0' }}",
|
||||
"other-package": "{{ versionQuery 'other-package' '2.3.0' }}"
|
||||
}
|
||||
}
|
||||
`,
|
||||
subdir: {
|
||||
'templated.txt.hbs': 'Hello {{id}}!',
|
||||
'not-templated.txt': 'Hello {{id}}!',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const [output, mockStream] = createMockOutputStream();
|
||||
jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream);
|
||||
|
||||
let modified = false;
|
||||
await executePluginPackageTemplate(
|
||||
{
|
||||
createTemporaryDirectory: (name: string) => fs.mkdtemp(name),
|
||||
markAsModified: () => {
|
||||
modified = true;
|
||||
},
|
||||
} as CreateContext,
|
||||
{
|
||||
templateName: 'test-template',
|
||||
targetDir: '/target',
|
||||
values: {
|
||||
id: 'testing',
|
||||
makePrivate: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(modified).toBe(true);
|
||||
expect(output).toEqual([
|
||||
'Checking Prerequisites:',
|
||||
`availability ..${sep}target`,
|
||||
'creating temp dir',
|
||||
'Executing Template:',
|
||||
'templating package.json.hbs',
|
||||
'copying not-templated.txt',
|
||||
'templating templated.txt.hbs',
|
||||
'Installing:',
|
||||
`moving ..${sep}target`,
|
||||
]);
|
||||
await expect(fs.readFile('/target/package.json', 'utf8')).resolves.toBe(`{
|
||||
"name": "my-testing-plugin",
|
||||
"private": true,
|
||||
"description": "testing",
|
||||
"dependencies": {
|
||||
"some-package": "^1.1.0",
|
||||
"other-package": "^2.3.0"
|
||||
}
|
||||
}
|
||||
`);
|
||||
await expect(
|
||||
fs.readFile('/target/subdir/templated.txt', 'utf8'),
|
||||
).resolves.toBe('Hello testing!');
|
||||
await expect(
|
||||
fs.readFile('/target/subdir/not-templated.txt', 'utf8'),
|
||||
).resolves.toBe('Hello {{id}}!');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2021 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 fs from 'fs-extra';
|
||||
import chalk from 'chalk';
|
||||
import { resolve as resolvePath, relative as relativePath } from 'path';
|
||||
import { paths } from '../../../paths';
|
||||
import { Task, templatingTask } from '../../../tasks';
|
||||
import { Lockfile } from '../../../versioning';
|
||||
import { createPackageVersionProvider } from '../../../version';
|
||||
import { CreateContext } from '../../types';
|
||||
|
||||
export async function executePluginPackageTemplate(
|
||||
ctx: CreateContext,
|
||||
options: {
|
||||
templateName: string;
|
||||
targetDir: string;
|
||||
values: Record<string, unknown>;
|
||||
},
|
||||
) {
|
||||
const { targetDir } = options;
|
||||
|
||||
let lockfile: Lockfile | undefined;
|
||||
try {
|
||||
lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock'));
|
||||
} catch {
|
||||
/* ignored */
|
||||
}
|
||||
|
||||
Task.section('Checking Prerequisites');
|
||||
const shortPluginDir = relativePath(paths.targetRoot, targetDir);
|
||||
await Task.forItem('availability', shortPluginDir, async () => {
|
||||
if (await fs.pathExists(targetDir)) {
|
||||
throw new Error(
|
||||
`A package with the same plugin ID already exists at ${chalk.cyan(
|
||||
shortPluginDir,
|
||||
)}. Please try again with a different ID.`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const tempDir = await Task.forItem('creating', 'temp dir', async () => {
|
||||
return await ctx.createTemporaryDirectory('backstage-create');
|
||||
});
|
||||
|
||||
Task.section('Executing Template');
|
||||
await templatingTask(
|
||||
paths.resolveOwn('templates', options.templateName),
|
||||
tempDir,
|
||||
options.values,
|
||||
createPackageVersionProvider(lockfile),
|
||||
);
|
||||
|
||||
// Format package.json if it exists
|
||||
const pkgJsonPath = resolvePath(tempDir, 'package.json');
|
||||
if (await fs.pathExists(pkgJsonPath)) {
|
||||
const pkgJson = await fs.readJson(pkgJsonPath);
|
||||
await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
|
||||
}
|
||||
|
||||
Task.section('Installing');
|
||||
await Task.forItem('moving', shortPluginDir, async () => {
|
||||
await fs.move(tempDir, targetDir).catch(error => {
|
||||
throw new Error(
|
||||
`Failed to move package from ${tempDir} to ${targetDir}, ${error.message}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
ctx.markAsModified();
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2021 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-control-regex */
|
||||
|
||||
import { WriteStream } from 'tty';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { paths } from '../../../paths';
|
||||
|
||||
export function mockPaths(options: {
|
||||
ownDir?: string;
|
||||
ownRoot?: string;
|
||||
targetDir?: string;
|
||||
targetRoot?: string;
|
||||
}): void {
|
||||
const { ownDir, ownRoot, targetDir, targetRoot } = options;
|
||||
if (ownDir) {
|
||||
paths.ownDir = ownDir;
|
||||
jest
|
||||
.spyOn(paths, 'resolveOwn')
|
||||
.mockImplementation((...ps) => resolvePath(ownDir, ...ps));
|
||||
}
|
||||
if (ownRoot) {
|
||||
jest.spyOn(paths, 'ownRoot', 'get').mockReturnValue(ownRoot);
|
||||
jest
|
||||
.spyOn(paths, 'resolveOwnRoot')
|
||||
.mockImplementation((...ps) => resolvePath(ownRoot, ...ps));
|
||||
}
|
||||
if (targetDir) {
|
||||
paths.targetDir = targetDir;
|
||||
jest
|
||||
.spyOn(paths, 'resolveTarget')
|
||||
.mockImplementation((...ps) => resolvePath(targetDir, ...ps));
|
||||
}
|
||||
if (targetRoot) {
|
||||
jest.spyOn(paths, 'targetRoot', 'get').mockReturnValue(targetRoot);
|
||||
jest
|
||||
.spyOn(paths, 'resolveTargetRoot')
|
||||
.mockImplementation((...ps) => resolvePath(targetRoot, ...ps));
|
||||
}
|
||||
}
|
||||
|
||||
export function createMockOutputStream() {
|
||||
const output = new Array<string>();
|
||||
return [
|
||||
output,
|
||||
{
|
||||
cursorTo: () => {},
|
||||
clearLine: () => {},
|
||||
moveCursor: () => {},
|
||||
write: (msg: string) => {
|
||||
let clean = msg;
|
||||
// Remove terminal color escape sequences
|
||||
clean = clean.replace(/\x1B\[\d\dm/g, '');
|
||||
// Remove any non-ascii
|
||||
clean = clean.replace(/[^\x00-\x7F]+/g, '');
|
||||
clean = clean.trim();
|
||||
output.push(clean);
|
||||
},
|
||||
} as unknown as WriteStream & { fd: any },
|
||||
] as const;
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* Copyright 2021 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 fs from 'fs-extra';
|
||||
import mockFs from 'mock-fs';
|
||||
import { sep, resolve as resolvePath } from 'path';
|
||||
import { paths } from '../../paths';
|
||||
import { Task } from '../../tasks';
|
||||
import { FactoryRegistry } from '../FactoryRegistry';
|
||||
import { createMockOutputStream, mockPaths } from './common/testUtils';
|
||||
import { frontendPlugin } from './frontendPlugin';
|
||||
|
||||
const appTsxContent = `
|
||||
import { createApp } from '@backstage/app-defaults';
|
||||
|
||||
const router = (
|
||||
<FlatRoutes>
|
||||
<Route path="/" element={<Home />} />
|
||||
</FlatRoutes>
|
||||
)
|
||||
`;
|
||||
|
||||
describe('frontendPlugin factory', () => {
|
||||
beforeEach(() => {
|
||||
mockPaths({
|
||||
targetRoot: '/root',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should create a frontend plugin', async () => {
|
||||
mockFs({
|
||||
'/root': {
|
||||
packages: {
|
||||
app: {
|
||||
'package.json': JSON.stringify({}),
|
||||
src: {
|
||||
'App.tsx': appTsxContent,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: mockFs.directory(),
|
||||
},
|
||||
[paths.resolveOwn('templates')]: mockFs.load(
|
||||
paths.resolveOwn('templates'),
|
||||
),
|
||||
});
|
||||
|
||||
const options = await FactoryRegistry.populateOptions(frontendPlugin, {
|
||||
id: 'test',
|
||||
});
|
||||
|
||||
let modified = false;
|
||||
|
||||
const [output, mockStream] = createMockOutputStream();
|
||||
jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream);
|
||||
jest.spyOn(Task, 'forCommand').mockResolvedValue();
|
||||
|
||||
await frontendPlugin.create(options, {
|
||||
private: true,
|
||||
isMonoRepo: true,
|
||||
defaultVersion: '1.0.0',
|
||||
markAsModified: () => {
|
||||
modified = true;
|
||||
},
|
||||
createTemporaryDirectory: () => fs.mkdtemp('test'),
|
||||
});
|
||||
|
||||
expect(modified).toBe(true);
|
||||
|
||||
expect(output).toEqual([
|
||||
'',
|
||||
'Creating backend plugin backstage-plugin-test',
|
||||
'Checking Prerequisites:',
|
||||
`availability plugins${sep}test`,
|
||||
'creating temp dir',
|
||||
'Executing Template:',
|
||||
'copying .eslintrc.js',
|
||||
'templating README.md.hbs',
|
||||
'templating package.json.hbs',
|
||||
'copying tsconfig.json',
|
||||
'templating index.tsx.hbs',
|
||||
'templating index.ts.hbs',
|
||||
'templating plugin.test.ts.hbs',
|
||||
'templating plugin.ts.hbs',
|
||||
'templating routes.ts.hbs',
|
||||
'copying setupTests.ts',
|
||||
'templating ExampleComponent.test.tsx.hbs',
|
||||
'templating ExampleComponent.tsx.hbs',
|
||||
'copying index.ts',
|
||||
'templating ExampleFetchComponent.test.tsx.hbs',
|
||||
'templating ExampleFetchComponent.tsx.hbs',
|
||||
'copying index.ts',
|
||||
'Installing:',
|
||||
`moving plugins${sep}test`,
|
||||
'app adding dependency',
|
||||
'app adding import',
|
||||
]);
|
||||
|
||||
await expect(
|
||||
fs.readJson('/root/packages/app/package.json'),
|
||||
).resolves.toEqual({
|
||||
dependencies: {
|
||||
'backstage-plugin-test': '^1.0.0',
|
||||
},
|
||||
});
|
||||
|
||||
await expect(fs.readFile('/root/packages/app/src/App.tsx', 'utf8')).resolves
|
||||
.toBe(`
|
||||
import { createApp } from '@backstage/app-defaults';
|
||||
import { TestPage } from 'backstage-plugin-test';
|
||||
|
||||
const router = (
|
||||
<FlatRoutes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/test" element={<TestPage />} />
|
||||
</FlatRoutes>
|
||||
)
|
||||
`);
|
||||
|
||||
expect(Task.forCommand).toHaveBeenCalledTimes(2);
|
||||
expect(Task.forCommand).toHaveBeenCalledWith('yarn install', {
|
||||
cwd: resolvePath('/root/plugins/test'),
|
||||
optional: true,
|
||||
});
|
||||
expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', {
|
||||
cwd: resolvePath('/root/plugins/test'),
|
||||
optional: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a frontend plugin with more options and codeowners', async () => {
|
||||
mockFs({
|
||||
'/root': {
|
||||
CODEOWNERS: '',
|
||||
packages: {
|
||||
app: {
|
||||
'package.json': JSON.stringify({}),
|
||||
src: {
|
||||
'App.tsx': appTsxContent,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: mockFs.directory(),
|
||||
},
|
||||
[paths.resolveOwn('templates')]: mockFs.load(
|
||||
paths.resolveOwn('templates'),
|
||||
),
|
||||
});
|
||||
|
||||
const options = await FactoryRegistry.populateOptions(frontendPlugin, {
|
||||
id: 'test',
|
||||
owner: '@test-user',
|
||||
});
|
||||
|
||||
const [, mockStream] = createMockOutputStream();
|
||||
jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream);
|
||||
jest.spyOn(Task, 'forCommand').mockResolvedValue();
|
||||
|
||||
await frontendPlugin.create(options, {
|
||||
scope: 'internal',
|
||||
private: true,
|
||||
isMonoRepo: true,
|
||||
defaultVersion: '1.0.0',
|
||||
markAsModified: () => {},
|
||||
createTemporaryDirectory: () => fs.mkdtemp('test'),
|
||||
});
|
||||
|
||||
await expect(
|
||||
fs.readJson('/root/packages/app/package.json'),
|
||||
).resolves.toEqual({
|
||||
dependencies: {
|
||||
'@internal/plugin-test': '^1.0.0',
|
||||
},
|
||||
});
|
||||
|
||||
await expect(fs.readFile('/root/packages/app/src/App.tsx', 'utf8')).resolves
|
||||
.toBe(`
|
||||
import { createApp } from '@backstage/app-defaults';
|
||||
import { TestPage } from '@internal/plugin-test';
|
||||
|
||||
const router = (
|
||||
<FlatRoutes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/test" element={<TestPage />} />
|
||||
</FlatRoutes>
|
||||
)
|
||||
`);
|
||||
|
||||
expect(Task.forCommand).toHaveBeenCalledTimes(2);
|
||||
expect(Task.forCommand).toHaveBeenCalledWith('yarn install', {
|
||||
cwd: resolvePath('/root/plugins/test'),
|
||||
optional: true,
|
||||
});
|
||||
expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', {
|
||||
cwd: resolvePath('/root/plugins/test'),
|
||||
optional: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright 2021 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 fs from 'fs-extra';
|
||||
import chalk from 'chalk';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
import upperFirst from 'lodash/upperFirst';
|
||||
import { paths } from '../../paths';
|
||||
import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners';
|
||||
import { createFactory, CreateContext } from '../types';
|
||||
import { addPackageDependency, Task } from '../../tasks';
|
||||
import { ownerPrompt, pluginIdPrompt } from './common/prompts';
|
||||
import { executePluginPackageTemplate } from './common/tasks';
|
||||
|
||||
type Options = {
|
||||
id: string;
|
||||
owner?: string;
|
||||
codeOwnersPath?: string;
|
||||
};
|
||||
|
||||
export const frontendPlugin = createFactory<Options>({
|
||||
name: 'plugin',
|
||||
description: 'A new frontend plugin',
|
||||
optionsDiscovery: async () => ({
|
||||
codeOwnersPath: await getCodeownersFilePath(paths.targetRoot),
|
||||
}),
|
||||
optionsPrompts: [pluginIdPrompt(), ownerPrompt()],
|
||||
async create(options: Options, ctx: CreateContext) {
|
||||
const { id } = options;
|
||||
|
||||
const name = ctx.scope
|
||||
? `@${ctx.scope}/plugin-${id}`
|
||||
: `backstage-plugin-${id}`;
|
||||
const extensionName = `${upperFirst(camelCase(id))}Page`;
|
||||
|
||||
Task.log();
|
||||
Task.log(`Creating backend plugin ${chalk.cyan(name)}`);
|
||||
|
||||
const targetDir = ctx.isMonoRepo
|
||||
? paths.resolveTargetRoot('plugins', id)
|
||||
: paths.resolveTargetRoot(`backstage-plugin-${id}`);
|
||||
|
||||
await executePluginPackageTemplate(ctx, {
|
||||
targetDir,
|
||||
templateName: 'default-plugin',
|
||||
values: {
|
||||
id,
|
||||
name,
|
||||
extensionName,
|
||||
pluginVar: `${camelCase(id)}Plugin`,
|
||||
pluginVersion: ctx.defaultVersion,
|
||||
privatePackage: ctx.private,
|
||||
npmRegistry: ctx.npmRegistry,
|
||||
},
|
||||
});
|
||||
|
||||
if (await fs.pathExists(paths.resolveTargetRoot('packages/app'))) {
|
||||
await Task.forItem('app', 'adding dependency', async () => {
|
||||
await addPackageDependency(
|
||||
paths.resolveTargetRoot('packages/app/package.json'),
|
||||
{
|
||||
dependencies: {
|
||||
[name]: `^${ctx.defaultVersion}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
await Task.forItem('app', 'adding import', async () => {
|
||||
const pluginsFilePath = paths.resolveTargetRoot(
|
||||
'packages/app/src/App.tsx',
|
||||
);
|
||||
if (!(await fs.pathExists(pluginsFilePath))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const content = await fs.readFile(pluginsFilePath, 'utf8');
|
||||
const revLines = content.split('\n').reverse();
|
||||
|
||||
const lastImportIndex = revLines.findIndex(line =>
|
||||
line.match(/ from ("|').*("|')/),
|
||||
);
|
||||
const lastRouteIndex = revLines.findIndex(line =>
|
||||
line.match(/<\/FlatRoutes/),
|
||||
);
|
||||
|
||||
if (lastImportIndex !== -1 && lastRouteIndex !== -1) {
|
||||
const importLine = `import { ${extensionName} } from '${name}';`;
|
||||
if (!content.includes(importLine)) {
|
||||
revLines.splice(lastImportIndex, 0, importLine);
|
||||
}
|
||||
|
||||
const componentLine = `<Route path="/${id}" element={<${extensionName} />} />`;
|
||||
if (!content.includes(componentLine)) {
|
||||
const [indentation] =
|
||||
revLines[lastRouteIndex + 1].match(/^\s*/) ?? [];
|
||||
revLines.splice(lastRouteIndex + 1, 0, indentation + componentLine);
|
||||
}
|
||||
|
||||
const newContent = revLines.reverse().join('\n');
|
||||
await fs.writeFile(pluginsFilePath, newContent, 'utf8');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (options.owner) {
|
||||
await addCodeownersEntry(`/plugins/${id}`, options.owner);
|
||||
}
|
||||
|
||||
await Task.forCommand('yarn install', { cwd: targetDir, optional: true });
|
||||
await Task.forCommand('yarn lint --fix', {
|
||||
cwd: targetDir,
|
||||
optional: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2021 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 { frontendPlugin } from './frontendPlugin';
|
||||
export { backendPlugin } from './backendPlugin';
|
||||
export { pluginCommon } from './pluginCommon';
|
||||
export { scaffolderModule } from './scaffolderModule';
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2021 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 fs from 'fs-extra';
|
||||
import mockFs from 'mock-fs';
|
||||
import { sep, resolve as resolvePath } from 'path';
|
||||
import { paths } from '../../paths';
|
||||
import { Task } from '../../tasks';
|
||||
import { FactoryRegistry } from '../FactoryRegistry';
|
||||
import { createMockOutputStream, mockPaths } from './common/testUtils';
|
||||
import { pluginCommon } from './pluginCommon';
|
||||
|
||||
describe('pluginCommon factory', () => {
|
||||
beforeEach(() => {
|
||||
mockPaths({
|
||||
targetRoot: '/root',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should create a common plugin package', async () => {
|
||||
mockFs({
|
||||
'/root': {
|
||||
plugins: mockFs.directory(),
|
||||
},
|
||||
[paths.resolveOwn('templates')]: mockFs.load(
|
||||
paths.resolveOwn('templates'),
|
||||
),
|
||||
});
|
||||
|
||||
const options = await FactoryRegistry.populateOptions(pluginCommon, {
|
||||
id: 'test',
|
||||
});
|
||||
|
||||
let modified = false;
|
||||
|
||||
const [output, mockStream] = createMockOutputStream();
|
||||
jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream);
|
||||
jest.spyOn(Task, 'forCommand').mockResolvedValue();
|
||||
|
||||
await pluginCommon.create(options, {
|
||||
private: true,
|
||||
isMonoRepo: true,
|
||||
defaultVersion: '1.0.0',
|
||||
markAsModified: () => {
|
||||
modified = true;
|
||||
},
|
||||
createTemporaryDirectory: () => fs.mkdtemp('test'),
|
||||
});
|
||||
|
||||
expect(modified).toBe(true);
|
||||
|
||||
expect(output).toEqual([
|
||||
'',
|
||||
'Creating backend plugin backstage-plugin-test-common',
|
||||
'Checking Prerequisites:',
|
||||
`availability plugins${sep}test-common`,
|
||||
'creating temp dir',
|
||||
'Executing Template:',
|
||||
'copying .eslintrc.js',
|
||||
'templating README.md.hbs',
|
||||
'templating package.json.hbs',
|
||||
'copying tsconfig.json',
|
||||
'templating index.ts.hbs',
|
||||
'copying setupTests.ts',
|
||||
'Installing:',
|
||||
`moving plugins${sep}test-common`,
|
||||
]);
|
||||
|
||||
await expect(
|
||||
fs.readJson('/root/plugins/test-common/package.json'),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
name: 'backstage-plugin-test-common',
|
||||
description: 'Common functionalities for the test plugin',
|
||||
private: true,
|
||||
version: '1.0.0',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(Task.forCommand).toHaveBeenCalledTimes(2);
|
||||
expect(Task.forCommand).toHaveBeenCalledWith('yarn install', {
|
||||
cwd: resolvePath('/root/plugins/test-common'),
|
||||
optional: true,
|
||||
});
|
||||
expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', {
|
||||
cwd: resolvePath('/root/plugins/test-common'),
|
||||
optional: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2021 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 chalk from 'chalk';
|
||||
import { paths } from '../../paths';
|
||||
import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners';
|
||||
import { createFactory, CreateContext } from '../types';
|
||||
import { Task } from '../../tasks';
|
||||
import { ownerPrompt, pluginIdPrompt } from './common/prompts';
|
||||
import { executePluginPackageTemplate } from './common/tasks';
|
||||
|
||||
type Options = {
|
||||
id: string;
|
||||
owner?: string;
|
||||
codeOwnersPath?: string;
|
||||
};
|
||||
|
||||
export const pluginCommon = createFactory<Options>({
|
||||
name: 'plugin-common',
|
||||
description: 'A new isomorphic common plugin package',
|
||||
optionsDiscovery: async () => ({
|
||||
codeOwnersPath: await getCodeownersFilePath(paths.targetRoot),
|
||||
}),
|
||||
optionsPrompts: [pluginIdPrompt(), ownerPrompt()],
|
||||
async create(options: Options, ctx: CreateContext) {
|
||||
const { id } = options;
|
||||
const suffix = `${id}-common`;
|
||||
const name = ctx.scope
|
||||
? `@${ctx.scope}/plugin-${suffix}`
|
||||
: `backstage-plugin-${suffix}`;
|
||||
|
||||
Task.log();
|
||||
Task.log(`Creating backend plugin ${chalk.cyan(name)}`);
|
||||
|
||||
const targetDir = ctx.isMonoRepo
|
||||
? paths.resolveTargetRoot('plugins', suffix)
|
||||
: paths.resolveTargetRoot(`backstage-plugin-${suffix}`);
|
||||
|
||||
await executePluginPackageTemplate(ctx, {
|
||||
targetDir,
|
||||
templateName: 'default-common-plugin-package',
|
||||
values: {
|
||||
id,
|
||||
name,
|
||||
privatePackage: ctx.private,
|
||||
npmRegistry: ctx.npmRegistry,
|
||||
pluginVersion: ctx.defaultVersion,
|
||||
},
|
||||
});
|
||||
|
||||
if (options.owner) {
|
||||
await addCodeownersEntry(`/plugins/${suffix}`, options.owner);
|
||||
}
|
||||
|
||||
await Task.forCommand('yarn install', { cwd: targetDir, optional: true });
|
||||
await Task.forCommand('yarn lint --fix', {
|
||||
cwd: targetDir,
|
||||
optional: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2021 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 fs from 'fs-extra';
|
||||
import mockFs from 'mock-fs';
|
||||
import { sep, resolve as resolvePath } from 'path';
|
||||
import { paths } from '../../paths';
|
||||
import { Task } from '../../tasks';
|
||||
import { FactoryRegistry } from '../FactoryRegistry';
|
||||
import { createMockOutputStream, mockPaths } from './common/testUtils';
|
||||
import { scaffolderModule } from './scaffolderModule';
|
||||
|
||||
describe('scaffolderModule factory', () => {
|
||||
beforeEach(() => {
|
||||
mockPaths({
|
||||
targetRoot: '/root',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should create a scaffolder backend module package', async () => {
|
||||
mockFs({
|
||||
'/root': {
|
||||
plugins: mockFs.directory(),
|
||||
},
|
||||
[paths.resolveOwn('templates')]: mockFs.load(
|
||||
paths.resolveOwn('templates'),
|
||||
),
|
||||
});
|
||||
|
||||
const options = await FactoryRegistry.populateOptions(scaffolderModule, {
|
||||
id: 'test',
|
||||
});
|
||||
|
||||
let modified = false;
|
||||
|
||||
const [output, mockStream] = createMockOutputStream();
|
||||
jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream);
|
||||
jest.spyOn(Task, 'forCommand').mockResolvedValue();
|
||||
|
||||
await scaffolderModule.create(options, {
|
||||
private: true,
|
||||
isMonoRepo: true,
|
||||
defaultVersion: '1.0.0',
|
||||
markAsModified: () => {
|
||||
modified = true;
|
||||
},
|
||||
createTemporaryDirectory: (name: string) => fs.mkdtemp(name),
|
||||
});
|
||||
|
||||
expect(modified).toBe(true);
|
||||
|
||||
expect(output).toEqual([
|
||||
'',
|
||||
'Creating module backstage-plugin-scaffolder-backend-module-test',
|
||||
'Checking Prerequisites:',
|
||||
`availability plugins${sep}scaffolder-backend-module-test`,
|
||||
'creating temp dir',
|
||||
'Executing Template:',
|
||||
'copying .eslintrc.js',
|
||||
'templating README.md.hbs',
|
||||
'templating package.json.hbs',
|
||||
'copying tsconfig.json',
|
||||
'templating index.ts.hbs',
|
||||
'copying index.ts',
|
||||
'copying example.test.ts',
|
||||
'copying example.ts',
|
||||
'copying index.ts',
|
||||
'Installing:',
|
||||
`moving plugins${sep}scaffolder-backend-module-test`,
|
||||
]);
|
||||
|
||||
await expect(
|
||||
fs.readJson('/root/plugins/scaffolder-backend-module-test/package.json'),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
name: 'backstage-plugin-scaffolder-backend-module-test',
|
||||
description: 'The test module for @backstage/plugin-scaffolder-backend',
|
||||
private: true,
|
||||
version: '1.0.0',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(Task.forCommand).toHaveBeenCalledTimes(2);
|
||||
expect(Task.forCommand).toHaveBeenCalledWith('yarn install', {
|
||||
cwd: resolvePath('/root/plugins/scaffolder-backend-module-test'),
|
||||
optional: true,
|
||||
});
|
||||
expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', {
|
||||
cwd: resolvePath('/root/plugins/scaffolder-backend-module-test'),
|
||||
optional: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2021 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 chalk from 'chalk';
|
||||
import { paths } from '../../paths';
|
||||
import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners';
|
||||
import { createFactory, CreateContext } from '../types';
|
||||
import { Task } from '../../tasks';
|
||||
import { ownerPrompt } from './common/prompts';
|
||||
import { executePluginPackageTemplate } from './common/tasks';
|
||||
|
||||
type Options = {
|
||||
id: string;
|
||||
owner?: string;
|
||||
codeOwnersPath?: string;
|
||||
};
|
||||
|
||||
export const scaffolderModule = createFactory<Options>({
|
||||
name: 'scaffolder-module',
|
||||
description:
|
||||
'An module exporting custom actions for @backstage/plugin-scaffolder-backend',
|
||||
optionsDiscovery: async () => ({
|
||||
codeOwnersPath: await getCodeownersFilePath(paths.targetRoot),
|
||||
}),
|
||||
optionsPrompts: [
|
||||
{
|
||||
type: 'input',
|
||||
name: 'id',
|
||||
message: 'Enter the name of the module [required]',
|
||||
validate: (value: string) => {
|
||||
if (!value) {
|
||||
return 'Please enter the name of the module';
|
||||
} else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) {
|
||||
return 'Module names must be lowercase and contain only letters, digits, and dashes.';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
ownerPrompt(),
|
||||
],
|
||||
async create(options: Options, ctx: CreateContext) {
|
||||
const { id } = options;
|
||||
const slug = `scaffolder-backend-module-${id}`;
|
||||
|
||||
let name = `backstage-plugin-${slug}`;
|
||||
if (ctx.scope) {
|
||||
if (ctx.scope === 'backstage') {
|
||||
name = `@backstage/plugin-${slug}`;
|
||||
} else {
|
||||
name = `@${ctx.scope}/backstage-plugin-${slug}`;
|
||||
}
|
||||
}
|
||||
|
||||
Task.log();
|
||||
Task.log(`Creating module ${chalk.cyan(name)}`);
|
||||
|
||||
const targetDir = ctx.isMonoRepo
|
||||
? paths.resolveTargetRoot('plugins', slug)
|
||||
: paths.resolveTargetRoot(`backstage-plugin-${slug}`);
|
||||
|
||||
await executePluginPackageTemplate(ctx, {
|
||||
targetDir,
|
||||
templateName: 'scaffolder-module',
|
||||
values: {
|
||||
id,
|
||||
name,
|
||||
privatePackage: ctx.private,
|
||||
npmRegistry: ctx.npmRegistry,
|
||||
pluginVersion: ctx.defaultVersion,
|
||||
},
|
||||
});
|
||||
|
||||
if (options.owner) {
|
||||
await addCodeownersEntry(`/plugins/${slug}`, options.owner);
|
||||
}
|
||||
|
||||
await Task.forCommand('yarn install', { cwd: targetDir, optional: true });
|
||||
await Task.forCommand('yarn lint --fix', {
|
||||
cwd: targetDir,
|
||||
optional: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2021 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 { DistinctQuestion } from 'inquirer';
|
||||
|
||||
export interface CreateContext {
|
||||
/** The package scope to use for new packages */
|
||||
scope?: string;
|
||||
/** The NPM registry to use for new packages */
|
||||
npmRegistry?: string;
|
||||
/** Whether new packages should be marked as private */
|
||||
private: boolean;
|
||||
/** Whether we are creating something in a monorepo or not */
|
||||
isMonoRepo: boolean;
|
||||
/** The default version to use for new packages */
|
||||
defaultVersion: string;
|
||||
|
||||
/** Creates a temporary directory. This will always be deleted after creation is done. */
|
||||
createTemporaryDirectory(name: string): Promise<string>;
|
||||
|
||||
/** Signal that the creation process got to a point where permanent modifications were made */
|
||||
markAsModified(): void;
|
||||
}
|
||||
|
||||
export type AnyOptions = Record<string, string>;
|
||||
|
||||
export type Prompt<TOptions> = DistinctQuestion<TOptions> & { name: string };
|
||||
|
||||
export interface Factory<TOptions extends AnyOptions> {
|
||||
/**
|
||||
* The name used for this factory.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* A description that describes what this factory creates to the user.
|
||||
*/
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* An optional options discovery step that is run
|
||||
* before the prompts to potentially fill in some of the options.
|
||||
*/
|
||||
optionsDiscovery?(): Promise<Partial<TOptions>>;
|
||||
|
||||
/**
|
||||
* Inquirer prompts that will be filled in either interactively or
|
||||
* through command line arguments.
|
||||
*/
|
||||
optionsPrompts?: ReadonlyArray<Prompt<TOptions>>;
|
||||
|
||||
/**
|
||||
* The main method of the factory that handles creation.
|
||||
*/
|
||||
create(options: TOptions, context?: CreateContext): Promise<void>;
|
||||
}
|
||||
|
||||
export type AnyFactory = Factory<AnyOptions>;
|
||||
|
||||
export function createFactory<TOptions extends AnyOptions>(
|
||||
config: Factory<TOptions>,
|
||||
): AnyFactory {
|
||||
return config as AnyFactory;
|
||||
}
|
||||
@@ -18,35 +18,40 @@ import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import handlebars from 'handlebars';
|
||||
import ora from 'ora';
|
||||
import { promisify } from 'util';
|
||||
import { basename, dirname } from 'path';
|
||||
import recursive from 'recursive-readdir';
|
||||
import { exec as execCb } from 'child_process';
|
||||
import { paths } from './paths';
|
||||
import { assertError } from '@backstage/errors';
|
||||
|
||||
const exec = promisify(execCb);
|
||||
|
||||
const TASK_NAME_MAX_LENGTH = 14;
|
||||
|
||||
export class Task {
|
||||
static log(name: string = '') {
|
||||
process.stdout.write(`${chalk.green(name)}\n`);
|
||||
process.stderr.write(`${chalk.green(name)}\n`);
|
||||
}
|
||||
|
||||
static error(message: string = '') {
|
||||
process.stdout.write(`\n${chalk.red(message)}\n\n`);
|
||||
process.stderr.write(`\n${chalk.red(message)}\n\n`);
|
||||
}
|
||||
|
||||
static section(name: string) {
|
||||
const title = chalk.green(`${name}:`);
|
||||
process.stdout.write(`\n ${title}\n`);
|
||||
process.stderr.write(`\n ${title}\n`);
|
||||
}
|
||||
|
||||
static exit(code: number = 0) {
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
static async forItem(
|
||||
static async forItem<T = void>(
|
||||
task: string,
|
||||
item: string,
|
||||
taskFunc: () => Promise<void>,
|
||||
): Promise<void> {
|
||||
taskFunc: () => Promise<T>,
|
||||
): Promise<T> {
|
||||
const paddedTask = chalk.green(task.padEnd(TASK_NAME_MAX_LENGTH));
|
||||
|
||||
const spinner = ora({
|
||||
@@ -56,13 +61,40 @@ export class Task {
|
||||
}).start();
|
||||
|
||||
try {
|
||||
await taskFunc();
|
||||
const result = await taskFunc();
|
||||
spinner.succeed();
|
||||
return result;
|
||||
} catch (error) {
|
||||
spinner.fail();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async forCommand(
|
||||
command: string,
|
||||
options?: { cwd?: string; optional?: boolean },
|
||||
) {
|
||||
try {
|
||||
await Task.forItem('executing', command, async () => {
|
||||
await exec(command, { cwd: options?.cwd });
|
||||
});
|
||||
} catch (error) {
|
||||
assertError(error);
|
||||
if (error.stderr) {
|
||||
process.stderr.write(error.stderr as Buffer);
|
||||
}
|
||||
if (error.stdout) {
|
||||
process.stdout.write(error.stdout as Buffer);
|
||||
}
|
||||
if (options?.optional) {
|
||||
Task.error(`Warning: Failed to execute command ${chalk.cyan(command)}`);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Failed to execute command '${chalk.cyan(command)}', ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function templatingTask(
|
||||
@@ -85,7 +117,9 @@ export async function templatingTask(
|
||||
const destination = destinationFile.replace(/\.hbs$/, '');
|
||||
|
||||
const template = await fs.readFile(file);
|
||||
const compiled = handlebars.compile(template.toString());
|
||||
const compiled = handlebars.compile(template.toString(), {
|
||||
strict: true,
|
||||
});
|
||||
const contents = compiled(
|
||||
{ name: basename(destination), ...context },
|
||||
{
|
||||
@@ -122,3 +156,44 @@ export async function templatingTask(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function addPackageDependency(
|
||||
path: string,
|
||||
options: {
|
||||
dependencies?: Record<string, string>;
|
||||
devDependencies?: Record<string, string>;
|
||||
peerDependencies?: Record<string, string>;
|
||||
},
|
||||
) {
|
||||
try {
|
||||
const pkgJson = await fs.readJson(path);
|
||||
|
||||
const normalize = (obj: Record<string, string>) => {
|
||||
if (Object.keys(obj).length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.keys(obj)
|
||||
.sort()
|
||||
.map(key => [key, obj[key]]),
|
||||
);
|
||||
};
|
||||
|
||||
pkgJson.dependencies = normalize({
|
||||
...pkgJson.dependencies,
|
||||
...options.dependencies,
|
||||
});
|
||||
pkgJson.devDependencies = normalize({
|
||||
...pkgJson.devDependencies,
|
||||
...options.devDependencies,
|
||||
});
|
||||
pkgJson.peerDependencies = normalize({
|
||||
...pkgJson.peerDependencies,
|
||||
...options.peerDependencies,
|
||||
});
|
||||
|
||||
await fs.writeJson(path, pkgJson, { spaces: 2 });
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to add package dependencies, ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ import { version as corePluginApi } from '@backstage/core-plugin-api/package.jso
|
||||
import { version as devUtils } from '@backstage/dev-utils/package.json';
|
||||
import { version as testUtils } from '@backstage/test-utils/package.json';
|
||||
import { version as theme } from '@backstage/theme/package.json';
|
||||
import { version as scaffolderBackend } from '@backstage/plugin-scaffolder-backend/package.json';
|
||||
|
||||
export const packageVersions: Record<string, string> = {
|
||||
'@backstage/backend-common': backendCommon,
|
||||
@@ -53,6 +54,7 @@ export const packageVersions: Record<string, string> = {
|
||||
'@backstage/dev-utils': devUtils,
|
||||
'@backstage/test-utils': testUtils,
|
||||
'@backstage/theme': theme,
|
||||
'@backstage/plugin-scaffolder-backend': scaffolderBackend,
|
||||
};
|
||||
|
||||
export function findVersion() {
|
||||
|
||||
@@ -4,41 +4,43 @@
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
{{#if privatePackage}} "private": {{privatePackage}},
|
||||
{{/if}}
|
||||
{{#if privatePackage}}
|
||||
"private": {{privatePackage}},
|
||||
{{/if}}
|
||||
"publishConfig": {
|
||||
{{#if npmRegistry}} "registry": "{{npmRegistry}}",
|
||||
{{/if}}
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "backstage-cli backend:dev",
|
||||
"build": "backstage-cli backend:build",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "{{versionQuery '@backstage/backend-common'}}",
|
||||
"@backstage/config": "{{versionQuery '@backstage/config'}}",
|
||||
"@types/express": "{{versionQuery '@types/express' '4.17.6'}}",
|
||||
"express": "{{versionQuery 'express' '4.17.1'}}",
|
||||
"express-promise-router": "{{versionQuery 'express-promise-router' '4.1.0'}}",
|
||||
"winston": "{{versionQuery 'winston' '3.2.1'}}",
|
||||
"cross-fetch": "{{versionQuery 'cross-fetch' '3.0.6'}}",
|
||||
"yn": "{{versionQuery 'yn' '4.0.0'}}"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "{{versionQuery '@backstage/cli'}}",
|
||||
"@types/supertest": "{{versionQuery '@types/supertest' '2.0.8'}}",
|
||||
"supertest": "{{versionQuery 'supertest' '4.0.2'}}",
|
||||
"msw": "{{versionQuery 'msw' '0.35.0'}}"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
{{#if npmRegistry}}
|
||||
"registry": "{{npmRegistry}}",
|
||||
{{/if}}
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "backstage-cli backend:dev",
|
||||
"build": "backstage-cli backend:build",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "{{versionQuery '@backstage/backend-common'}}",
|
||||
"@backstage/config": "{{versionQuery '@backstage/config'}}",
|
||||
"@types/express": "{{versionQuery '@types/express' '4.17.6'}}",
|
||||
"express": "{{versionQuery 'express' '4.17.1'}}",
|
||||
"express-promise-router": "{{versionQuery 'express-promise-router' '4.1.0'}}",
|
||||
"winston": "{{versionQuery 'winston' '3.2.1'}}",
|
||||
"cross-fetch": "{{versionQuery 'cross-fetch' '3.0.6'}}",
|
||||
"yn": "{{versionQuery 'yn' '4.0.0'}}"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "{{versionQuery '@backstage/cli'}}",
|
||||
"@types/supertest": "{{versionQuery '@types/supertest' '2.0.8'}}",
|
||||
"supertest": "{{versionQuery 'supertest' '4.0.2'}}",
|
||||
"msw": "{{versionQuery 'msw' '0.35.0'}}"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
# {{name}}
|
||||
|
||||
Welcome to the common package for the {{id}} plugin!
|
||||
|
||||
_This plugin was created through the Backstage CLI_
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "{{name}}",
|
||||
"description": "Common functionalities for the {{id}} plugin",
|
||||
"version": "{{pluginVersion}}",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
{{#if privatePackage}}
|
||||
"private": {{privatePackage}},
|
||||
{{/if}}
|
||||
"publishConfig": {
|
||||
{{#if npmRegistry}}
|
||||
"registry": "{{npmRegistry}}",
|
||||
{{/if}}
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"module": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "backstage-cli build",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "{{versionQuery '@backstage/cli'}}"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/***/
|
||||
/**
|
||||
* Common functionalities for the {{id}} plugin.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
/**
|
||||
* In this package you might for example declare types that are common
|
||||
* between the frontend and backend plugin packages.
|
||||
*/
|
||||
export type CommonType = {
|
||||
field: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Or you might declare some common constants.
|
||||
*/
|
||||
export const COMMON_CONSTANT = 1
|
||||
@@ -0,0 +1 @@
|
||||
export {};
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@backstage/cli/config/tsconfig.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist-types",
|
||||
"rootDir": "."
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,12 @@
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
{{#if privatePackage}} "private": {{privatePackage}},
|
||||
{{#if privatePackage}}
|
||||
"private": {{privatePackage}},
|
||||
{{/if}}
|
||||
"publishConfig": {
|
||||
{{#if npmRegistry}} "registry": "{{npmRegistry}}",
|
||||
{{#if npmRegistry}}
|
||||
"registry": "{{npmRegistry}}",
|
||||
{{/if}}
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
# {{name}}
|
||||
|
||||
The {{id}} module for [@backstage/plugin-scaffolder-backend](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend).
|
||||
|
||||
_This plugin was created through the Backstage CLI_
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "{{name}}",
|
||||
"description": "The {{id}} module for @backstage/plugin-scaffolder-backend",
|
||||
"version": "{{pluginVersion}}",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
{{#if privatePackage}}
|
||||
"private": {{privatePackage}},
|
||||
{{/if}}
|
||||
"publishConfig": {
|
||||
{{#if npmRegistry}}
|
||||
"registry": "{{npmRegistry}}",
|
||||
{{/if}}
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "backstage-cli build --output cjs,types",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/plugin-scaffolder-backend": "{{versionQuery '@backstage/plugin-scaffolder-backend'}}"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-common": "{{versionQuery '@backstage/backend-common'}}",
|
||||
"@backstage/cli": "{{versionQuery '@backstage/cli'}}"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2021 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 { PassThrough } from 'stream';
|
||||
import { createAcmeExampleAction } from './example';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
|
||||
describe('acme:example', () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should call action', async () => {
|
||||
const action = createAcmeExampleAction();
|
||||
|
||||
const logger = getVoidLogger();
|
||||
jest.spyOn(logger, 'info');
|
||||
|
||||
await action.handler({
|
||||
input: {
|
||||
myParameter: 'test',
|
||||
},
|
||||
workspacePath: '/tmp',
|
||||
logger,
|
||||
logStream: new PassThrough(),
|
||||
output: jest.fn(),
|
||||
createTemporaryDirectory() {
|
||||
// Usage of mock-fs is recommended for testing of filesystem operations
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
});
|
||||
|
||||
expect(logger.info).toHaveBeenCalledWith(
|
||||
'Running example template with parameters: test',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2021 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 { createTemplateAction } from '@backstage/plugin-scaffolder-backend';
|
||||
|
||||
/**
|
||||
* Creates an `acme:example` Scaffolder action.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* See {@link https://example.com} for more information.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function createAcmeExampleAction() {
|
||||
// For more information on how to define custom actions, see
|
||||
// https://backstage.io/docs/features/software-templates/writing-custom-actions
|
||||
return createTemplateAction<{
|
||||
myParameter: string;
|
||||
}>({
|
||||
id: 'acme:example',
|
||||
description: 'Runs Yeoman on an installed Yeoman generator',
|
||||
schema: {
|
||||
input: {
|
||||
type: 'object',
|
||||
required: ['myParameter'],
|
||||
properties: {
|
||||
myParameter: {
|
||||
title: 'An example parameter',
|
||||
description: 'This is the schema for our example parameter',
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async handler(ctx) {
|
||||
ctx.logger.info(
|
||||
`Running example template with parameters: ${ctx.input.myParameter}`,
|
||||
);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { createAcmeExampleAction } from './example';
|
||||
@@ -0,0 +1 @@
|
||||
export * from './example';
|
||||
@@ -0,0 +1,8 @@
|
||||
/***/
|
||||
/**
|
||||
* The {{id}} module for @backstage/plugin-scaffolder-backend.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export * from './actions';
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@backstage/cli/config/tsconfig.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist-types",
|
||||
"rootDir": "."
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@
|
||||
"ajv": "^7.0.3",
|
||||
"chokidar": "^3.5.2",
|
||||
"fs-extra": "9.1.0",
|
||||
"json-schema": "^0.3.0",
|
||||
"json-schema": "^0.4.0",
|
||||
"json-schema-merge-allof": "^0.8.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"typescript-json-schema": "^0.51.0",
|
||||
|
||||
@@ -28,9 +28,9 @@ import { bitbucketAuthApiRef } from '@backstage/core-plugin-api';
|
||||
import { ComponentType } from 'react';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { DiscoveryApi } from '@backstage/core-plugin-api';
|
||||
import { Error as Error_2 } from '@backstage/core-plugin-api';
|
||||
import { ErrorApi } from '@backstage/core-plugin-api';
|
||||
import { ErrorContext } from '@backstage/core-plugin-api';
|
||||
import { ErrorApiError } from '@backstage/core-plugin-api';
|
||||
import { ErrorApiErrorContext } from '@backstage/core-plugin-api';
|
||||
import { ExternalRouteRef } from '@backstage/core-plugin-api';
|
||||
import { FeatureFlag } from '@backstage/core-plugin-api';
|
||||
import { FeatureFlagsApi } from '@backstage/core-plugin-api';
|
||||
@@ -95,7 +95,6 @@ export class ApiFactoryRegistry implements ApiFactoryHolder {
|
||||
| undefined;
|
||||
// (undocumented)
|
||||
getAllApis(): Set<AnyApiRef>;
|
||||
// Warning: (ae-forgotten-export) The symbol "ApiFactoryScope" needs to be exported by the entry point index.d.ts
|
||||
register<
|
||||
Api,
|
||||
Impl extends Api,
|
||||
@@ -105,6 +104,9 @@ export class ApiFactoryRegistry implements ApiFactoryHolder {
|
||||
>(scope: ApiFactoryScope, factory: ApiFactory<Api, Impl, Deps>): boolean;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type ApiFactoryScope = 'default' | 'app' | 'static';
|
||||
|
||||
// @public
|
||||
export const ApiProvider: {
|
||||
(props: PropsWithChildren<ApiProviderProps>): JSX.Element;
|
||||
@@ -118,6 +120,12 @@ export const ApiProvider: {
|
||||
};
|
||||
};
|
||||
|
||||
// @public
|
||||
export type ApiProviderProps = {
|
||||
apis: ApiHolder;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
// @public
|
||||
export class ApiRegistry implements ApiHolder {
|
||||
constructor(apis: Map<string, unknown>);
|
||||
@@ -197,7 +205,14 @@ export type AppOptions = {
|
||||
icons: AppIcons & {
|
||||
[key in string]: IconComponent;
|
||||
};
|
||||
plugins?: BackstagePluginWithAnyOutput[];
|
||||
plugins?: (Omit<BackstagePlugin<any, any>, 'output'> & {
|
||||
output(): (
|
||||
| PluginOutput
|
||||
| {
|
||||
type: string;
|
||||
}
|
||||
)[];
|
||||
})[];
|
||||
components: AppComponents;
|
||||
themes: (Partial<AppTheme> & Omit<AppTheme, 'theme'>)[];
|
||||
configLoader?: AppConfigLoader;
|
||||
@@ -238,8 +253,6 @@ export class AppThemeSelector implements AppThemeApi {
|
||||
|
||||
// @public
|
||||
export class AtlassianAuth {
|
||||
// Warning: (ae-forgotten-export) The symbol "OAuthApiCreateOptions" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
static create({
|
||||
discoveryApi,
|
||||
@@ -261,6 +274,15 @@ export class Auth0Auth {
|
||||
}: OAuthApiCreateOptions): typeof auth0AuthApiRef.T;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type AuthApiCreateOptions = {
|
||||
discoveryApi: DiscoveryApi;
|
||||
environment?: string;
|
||||
provider?: AuthProvider & {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
// @public
|
||||
export type BackstageApp = {
|
||||
getPlugins(): BackstagePlugin<any, any>[];
|
||||
@@ -269,7 +291,7 @@ export type BackstageApp = {
|
||||
getRouter(): ComponentType<{}>;
|
||||
};
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export type BackstagePluginWithAnyOutput = Omit<
|
||||
BackstagePlugin<any, any>,
|
||||
'output'
|
||||
@@ -327,11 +349,11 @@ export class ErrorAlerter implements ErrorApi {
|
||||
constructor(alertApi: AlertApi, errorApi: ErrorApi);
|
||||
// (undocumented)
|
||||
error$(): Observable<{
|
||||
error: Error_2;
|
||||
context?: ErrorContext | undefined;
|
||||
error: ErrorApiError;
|
||||
context?: ErrorApiErrorContext | undefined;
|
||||
}>;
|
||||
// (undocumented)
|
||||
post(error: Error, context?: ErrorContext): void;
|
||||
post(error: ErrorApiError, context?: ErrorApiErrorContext): void;
|
||||
}
|
||||
|
||||
// @public
|
||||
@@ -339,10 +361,10 @@ export class ErrorApiForwarder implements ErrorApi {
|
||||
// (undocumented)
|
||||
error$(): Observable<{
|
||||
error: Error;
|
||||
context?: ErrorContext;
|
||||
context?: ErrorApiErrorContext;
|
||||
}>;
|
||||
// (undocumented)
|
||||
post(error: Error, context?: ErrorContext): void;
|
||||
post(error: ErrorApiError, context?: ErrorApiErrorContext): void;
|
||||
}
|
||||
|
||||
// @public
|
||||
@@ -378,6 +400,8 @@ export type FlatRoutesProps = {
|
||||
// @public
|
||||
export class GithubAuth implements OAuthApi, SessionApi {
|
||||
// Warning: (ae-forgotten-export) The symbol "SessionManager" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// @deprecated
|
||||
constructor(sessionManager: SessionManager<GithubSession>);
|
||||
// (undocumented)
|
||||
static create({
|
||||
@@ -479,12 +503,11 @@ export class OAuth2
|
||||
BackstageIdentityApi,
|
||||
SessionApi
|
||||
{
|
||||
// @deprecated
|
||||
constructor(options: {
|
||||
sessionManager: SessionManager<OAuth2Session>;
|
||||
scopeTransform: (scopes: string[]) => string[];
|
||||
});
|
||||
// Warning: (ae-forgotten-export) The symbol "CreateOptions" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
static create({
|
||||
discoveryApi,
|
||||
@@ -493,7 +516,7 @@ export class OAuth2
|
||||
oauthRequestApi,
|
||||
defaultScopes,
|
||||
scopeTransform,
|
||||
}: CreateOptions): OAuth2;
|
||||
}: OAuth2CreateOptions): OAuth2;
|
||||
// (undocumented)
|
||||
getAccessToken(
|
||||
scope?: string | string[],
|
||||
@@ -515,6 +538,11 @@ export class OAuth2
|
||||
signOut(): Promise<void>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type OAuth2CreateOptions = OAuthApiCreateOptions & {
|
||||
scopeTransform?: (scopes: string[]) => string[];
|
||||
};
|
||||
|
||||
// @public
|
||||
export type OAuth2Session = {
|
||||
providerInfo: {
|
||||
@@ -527,6 +555,12 @@ export type OAuth2Session = {
|
||||
backstageIdentity: BackstageIdentity;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type OAuthApiCreateOptions = AuthApiCreateOptions & {
|
||||
oauthRequestApi: OAuthRequestApi;
|
||||
defaultScopes?: string[];
|
||||
};
|
||||
|
||||
// @public
|
||||
export class OAuthRequestManager implements OAuthRequestApi {
|
||||
// (undocumented)
|
||||
@@ -549,25 +583,31 @@ export class OktaAuth {
|
||||
|
||||
// @public
|
||||
export class OneLoginAuth {
|
||||
// Warning: (ae-forgotten-export) The symbol "CreateOptions" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
static create({
|
||||
discoveryApi,
|
||||
environment,
|
||||
provider,
|
||||
oauthRequestApi,
|
||||
}: CreateOptions_2): typeof oneloginAuthApiRef.T;
|
||||
}: OneLoginAuthCreateOptions): typeof oneloginAuthApiRef.T;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type OneLoginAuthCreateOptions = {
|
||||
discoveryApi: DiscoveryApi;
|
||||
oauthRequestApi: OAuthRequestApi;
|
||||
environment?: string;
|
||||
provider?: AuthProvider & {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
// @public
|
||||
export class SamlAuth
|
||||
implements ProfileInfoApi, BackstageIdentityApi, SessionApi
|
||||
{
|
||||
// Warning: (ae-forgotten-export) The symbol "SamlSession" needs to be exported by the entry point index.d.ts
|
||||
// @deprecated
|
||||
constructor(sessionManager: SessionManager<SamlSession>);
|
||||
// Warning: (ae-forgotten-export) The symbol "AuthApiCreateOptions" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
static create({
|
||||
discoveryApi,
|
||||
@@ -588,6 +628,13 @@ export class SamlAuth
|
||||
signOut(): Promise<void>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type SamlSession = {
|
||||
userId: string;
|
||||
profile: ProfileInfo;
|
||||
backstageIdentity: BackstageIdentity;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type SignInPageProps = {
|
||||
onResult(result: SignInResult): void;
|
||||
@@ -603,7 +650,7 @@ export type SignInResult = {
|
||||
|
||||
// @public
|
||||
export class UnhandledErrorForwarder {
|
||||
static forward(errorApi: ErrorApi, errorContext: ErrorContext): void;
|
||||
static forward(errorApi: ErrorApi, errorContext: ErrorApiErrorContext): void;
|
||||
}
|
||||
|
||||
// @public
|
||||
@@ -632,8 +679,4 @@ export class WebStorage implements StorageApi {
|
||||
// (undocumented)
|
||||
set<T>(key: string, data: T): Promise<void>;
|
||||
}
|
||||
|
||||
// Warnings were encountered during analysis:
|
||||
//
|
||||
// src/apis/system/ApiProvider.d.ts:15:5 - (ae-forgotten-export) The symbol "ApiProviderProps" needs to be exported by the entry point index.d.ts
|
||||
```
|
||||
|
||||
@@ -13,7 +13,12 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { ErrorApi, ErrorContext, AlertApi } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
ErrorApi,
|
||||
ErrorApiError,
|
||||
ErrorApiErrorContext,
|
||||
AlertApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
/**
|
||||
* Decorates an ErrorApi by also forwarding error messages
|
||||
@@ -27,7 +32,7 @@ export class ErrorAlerter implements ErrorApi {
|
||||
private readonly errorApi: ErrorApi,
|
||||
) {}
|
||||
|
||||
post(error: Error, context?: ErrorContext) {
|
||||
post(error: ErrorApiError, context?: ErrorApiErrorContext) {
|
||||
if (!context?.hidden) {
|
||||
this.alertApi.post({ message: error.message, severity: 'error' });
|
||||
}
|
||||
|
||||
@@ -14,7 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ErrorApi, ErrorContext } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
ErrorApi,
|
||||
ErrorApiError,
|
||||
ErrorApiErrorContext,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { Observable } from '@backstage/types';
|
||||
import { PublishSubject } from '../../../lib/subjects';
|
||||
|
||||
@@ -26,14 +30,14 @@ import { PublishSubject } from '../../../lib/subjects';
|
||||
export class ErrorApiForwarder implements ErrorApi {
|
||||
private readonly subject = new PublishSubject<{
|
||||
error: Error;
|
||||
context?: ErrorContext;
|
||||
context?: ErrorApiErrorContext;
|
||||
}>();
|
||||
|
||||
post(error: Error, context?: ErrorContext) {
|
||||
post(error: ErrorApiError, context?: ErrorApiErrorContext) {
|
||||
this.subject.next({ error, context });
|
||||
}
|
||||
|
||||
error$(): Observable<{ error: Error; context?: ErrorContext }> {
|
||||
error$(): Observable<{ error: Error; context?: ErrorApiErrorContext }> {
|
||||
return this.subject;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { ErrorApi, ErrorContext } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
ErrorApi,
|
||||
ErrorApiError,
|
||||
ErrorApiErrorContext,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
@@ -25,11 +29,11 @@ export class UnhandledErrorForwarder {
|
||||
/**
|
||||
* Add event listener, such that unhandled errors can be forwarded using an given `ErrorApi` instance
|
||||
*/
|
||||
static forward(errorApi: ErrorApi, errorContext: ErrorContext) {
|
||||
static forward(errorApi: ErrorApi, errorContext: ErrorApiErrorContext) {
|
||||
window.addEventListener(
|
||||
'unhandledrejection',
|
||||
(e: PromiseRejectionEvent) => {
|
||||
errorApi.post(e.reason as Error, errorContext);
|
||||
errorApi.post(e.reason as ErrorApiError, errorContext);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('GithubAuth', () => {
|
||||
const getSession = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ providerInfo: { accessToken: 'access-token' } });
|
||||
const githubAuth = new GithubAuth({ getSession } as any);
|
||||
const githubAuth = new (GithubAuth as any)({ getSession }) as GithubAuth;
|
||||
|
||||
expect(await githubAuth.getAccessToken()).toBe('access-token');
|
||||
expect(getSession).toBeCalledTimes(1);
|
||||
|
||||
@@ -116,6 +116,9 @@ export default class GithubAuth implements OAuthApi, SessionApi {
|
||||
return new GithubAuth(sessionManagerMux);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated will be made private in the future. Use create method instead.
|
||||
*/
|
||||
constructor(private readonly sessionManager: SessionManager<GithubSession>) {}
|
||||
|
||||
async signIn() {
|
||||
|
||||
@@ -25,3 +25,4 @@ export * from './microsoft';
|
||||
export * from './onelogin';
|
||||
export * from './bitbucket';
|
||||
export * from './atlassian';
|
||||
export type { OAuthApiCreateOptions, AuthApiCreateOptions } from './types';
|
||||
|
||||
@@ -32,7 +32,11 @@ import { Observable } from '@backstage/types';
|
||||
import { OAuth2Session } from './types';
|
||||
import { OAuthApiCreateOptions } from '../types';
|
||||
|
||||
type CreateOptions = OAuthApiCreateOptions & {
|
||||
/**
|
||||
* OAuth2 create options.
|
||||
* @public
|
||||
*/
|
||||
export type OAuth2CreateOptions = OAuthApiCreateOptions & {
|
||||
scopeTransform?: (scopes: string[]) => string[];
|
||||
};
|
||||
|
||||
@@ -73,7 +77,7 @@ export default class OAuth2
|
||||
oauthRequestApi,
|
||||
defaultScopes = [],
|
||||
scopeTransform = x => x,
|
||||
}: CreateOptions) {
|
||||
}: OAuth2CreateOptions) {
|
||||
const connector = new DefaultAuthConnector({
|
||||
discoveryApi,
|
||||
environment,
|
||||
@@ -114,6 +118,9 @@ export default class OAuth2
|
||||
private readonly sessionManager: SessionManager<OAuth2Session>;
|
||||
private readonly scopeTransform: (scopes: string[]) => string[];
|
||||
|
||||
/**
|
||||
* @deprecated will be made private in the future. Use create method instead.
|
||||
*/
|
||||
constructor(options: {
|
||||
sessionManager: SessionManager<OAuth2Session>;
|
||||
scopeTransform: (scopes: string[]) => string[];
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import { ProfileInfo, BackstageIdentity } from '@backstage/core-plugin-api';
|
||||
|
||||
export type { OAuth2CreateOptions } from './OAuth2';
|
||||
/**
|
||||
* Session information for generic OAuth2 auth.
|
||||
*
|
||||
|
||||
@@ -22,7 +22,11 @@ import {
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { OAuth2 } from '../oauth2';
|
||||
|
||||
type CreateOptions = {
|
||||
/**
|
||||
* OneLogin auth provider create options.
|
||||
* @public
|
||||
*/
|
||||
export type OneLoginAuthCreateOptions = {
|
||||
discoveryApi: DiscoveryApi;
|
||||
oauthRequestApi: OAuthRequestApi;
|
||||
environment?: string;
|
||||
@@ -58,7 +62,7 @@ export default class OneLoginAuth {
|
||||
environment = 'development',
|
||||
provider = DEFAULT_PROVIDER,
|
||||
oauthRequestApi,
|
||||
}: CreateOptions): typeof oneloginAuthApiRef.T {
|
||||
}: OneLoginAuthCreateOptions): typeof oneloginAuthApiRef.T {
|
||||
return OAuth2.create({
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
|
||||
@@ -15,3 +15,4 @@
|
||||
*/
|
||||
|
||||
export { default as OneLoginAuth } from './OneLoginAuth';
|
||||
export type { OneLoginAuthCreateOptions } from './OneLoginAuth';
|
||||
|
||||
@@ -79,6 +79,9 @@ export default class SamlAuth
|
||||
return this.sessionManager.sessionState$();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated will be made private in the future. Use create method instead.
|
||||
*/
|
||||
constructor(private readonly sessionManager: SessionManager<SamlSession>) {}
|
||||
|
||||
async signIn() {
|
||||
|
||||
@@ -14,3 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { default as SamlAuth } from './SamlAuth';
|
||||
export type { SamlSession } from './types';
|
||||
|
||||
@@ -20,11 +20,19 @@ import {
|
||||
OAuthRequestApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
/**
|
||||
* Create options for OAuth APIs.
|
||||
* @public
|
||||
*/
|
||||
export type OAuthApiCreateOptions = AuthApiCreateOptions & {
|
||||
oauthRequestApi: OAuthRequestApi;
|
||||
defaultScopes?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic create options for auth APIs.
|
||||
* @public
|
||||
*/
|
||||
export type AuthApiCreateOptions = {
|
||||
discoveryApi: DiscoveryApi;
|
||||
environment?: string;
|
||||
|
||||
@@ -22,7 +22,11 @@ import {
|
||||
AnyApiFactory,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
type ApiFactoryScope =
|
||||
/**
|
||||
* Scope type when registering API factories.
|
||||
* @public
|
||||
*/
|
||||
export type ApiFactoryScope =
|
||||
| 'default' // Default factories registered by core and plugins
|
||||
| 'app' // Factories registered in the app, overriding default ones
|
||||
| 'static'; // APIs that can't be overridden, e.g. config
|
||||
|
||||
@@ -23,7 +23,11 @@ import {
|
||||
createVersionedContext,
|
||||
} from '@backstage/version-bridge';
|
||||
|
||||
type ApiProviderProps = {
|
||||
/**
|
||||
* Prop types for the ApiProvider component.
|
||||
* @public
|
||||
*/
|
||||
export type ApiProviderProps = {
|
||||
apis: ApiHolder;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
*/
|
||||
|
||||
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';
|
||||
export * from './types';
|
||||
|
||||
@@ -200,6 +200,7 @@ export type AppRouteBinder = <
|
||||
*
|
||||
* @public
|
||||
* @remarks
|
||||
* @deprecated Will be removed
|
||||
*
|
||||
* The `type: string` type is there to handle output from newer or older plugin
|
||||
* API versions that might not be supported by this version of the app API, but
|
||||
@@ -246,7 +247,9 @@ export type AppOptions = {
|
||||
/**
|
||||
* A list of all plugins to include in the app.
|
||||
*/
|
||||
plugins?: BackstagePluginWithAnyOutput[];
|
||||
plugins?: (Omit<BackstagePlugin<any, any>, 'output'> & {
|
||||
output(): (PluginOutput | { type: string })[];
|
||||
})[];
|
||||
|
||||
/**
|
||||
* Supply components to the app to override the default ones.
|
||||
|
||||
@@ -44,7 +44,7 @@ const useStyles = makeStyles<BackstageTheme>(
|
||||
gridArea: 'pageHeader',
|
||||
padding: theme.spacing(3),
|
||||
width: '100%',
|
||||
boxShadow: '0 0 8px 3px rgba(20, 20, 20, 0.3)',
|
||||
boxShadow: theme.shadows[4],
|
||||
position: 'relative',
|
||||
zIndex: 100,
|
||||
display: 'flex',
|
||||
@@ -65,12 +65,12 @@ const useStyles = makeStyles<BackstageTheme>(
|
||||
title: {
|
||||
color: theme.palette.bursts.fontColor,
|
||||
wordBreak: 'break-all',
|
||||
fontSize: 'calc(24px + 6 * ((100vw - 320px) / 680))',
|
||||
fontSize: theme.typography.h3.fontSize,
|
||||
marginBottom: 0,
|
||||
},
|
||||
subtitle: {
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
lineHeight: '1.0em',
|
||||
color: theme.palette.common.white,
|
||||
opacity: 0.8,
|
||||
display: 'inline-block', // prevents margin collapse of adjacent siblings
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
@@ -82,7 +82,6 @@ const useStyles = makeStyles<BackstageTheme>(
|
||||
color: theme.palette.bursts.fontColor,
|
||||
},
|
||||
breadcrumb: {
|
||||
fontSize: 'calc(15px + 1 * ((100vw - 320px) / 680))',
|
||||
color: theme.palette.bursts.fontColor,
|
||||
},
|
||||
breadcrumbType: {
|
||||
|
||||
@@ -293,11 +293,13 @@ export const SidebarItem = forwardRef<any, SidebarItemProps>((props, ref) => {
|
||||
type SidebarSearchFieldProps = {
|
||||
onSearch: (input: string) => void;
|
||||
to?: string;
|
||||
icon?: IconComponent;
|
||||
};
|
||||
|
||||
export function SidebarSearchField(props: SidebarSearchFieldProps) {
|
||||
const [input, setInput] = useState('');
|
||||
const classes = useStyles();
|
||||
const Icon = props.icon ? props.icon : SearchIcon;
|
||||
|
||||
const search = () => {
|
||||
props.onSearch(input);
|
||||
@@ -329,7 +331,7 @@ export function SidebarSearchField(props: SidebarSearchFieldProps) {
|
||||
|
||||
return (
|
||||
<div className={classes.searchRoot}>
|
||||
<SidebarItem icon={SearchIcon} to={props.to} onClick={handleItemClick}>
|
||||
<SidebarItem icon={Icon} to={props.to} onClick={handleItemClick}>
|
||||
<TextField
|
||||
placeholder="Search"
|
||||
value={input}
|
||||
|
||||
@@ -49,7 +49,9 @@ const Component: ProviderComponent = ({ config, onResult }) => {
|
||||
userId: identity!.id,
|
||||
profile: profile!,
|
||||
getIdToken: () => {
|
||||
return authApi.getBackstageIdentity().then(i => i!.idToken);
|
||||
return authApi
|
||||
.getBackstageIdentity()
|
||||
.then(i => i!.token ?? i!.idToken);
|
||||
},
|
||||
signOut: async () => {
|
||||
await authApi.signOut();
|
||||
|
||||
@@ -404,23 +404,31 @@ export interface ElementCollection {
|
||||
}): ElementCollection;
|
||||
}
|
||||
|
||||
// @public
|
||||
type Error_2 = {
|
||||
name: string;
|
||||
message: string;
|
||||
stack?: string;
|
||||
};
|
||||
// @public @deprecated (undocumented)
|
||||
type Error_2 = ErrorApiError;
|
||||
export { Error_2 as Error };
|
||||
|
||||
// @public
|
||||
export type ErrorApi = {
|
||||
post(error: Error_2, context?: ErrorContext): void;
|
||||
post(error: ErrorApiError, context?: ErrorApiErrorContext): void;
|
||||
error$(): Observable_2<{
|
||||
error: Error_2;
|
||||
context?: ErrorContext;
|
||||
error: ErrorApiError;
|
||||
context?: ErrorApiErrorContext;
|
||||
}>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type ErrorApiError = {
|
||||
name: string;
|
||||
message: string;
|
||||
stack?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type ErrorApiErrorContext = {
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
// @public
|
||||
export const errorApiRef: ApiRef<ErrorApi>;
|
||||
|
||||
@@ -431,10 +439,8 @@ export type ErrorBoundaryFallbackProps = {
|
||||
resetError: () => void;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type ErrorContext = {
|
||||
hidden?: boolean;
|
||||
};
|
||||
// @public @deprecated (undocumented)
|
||||
export type ErrorContext = ErrorApiErrorContext;
|
||||
|
||||
// @public
|
||||
export type Extension<T> = {
|
||||
@@ -703,14 +709,6 @@ export type RouteFunc<Params extends AnyParams> = (
|
||||
...[params]: Params extends undefined ? readonly [] : readonly [Params]
|
||||
) => string;
|
||||
|
||||
// @public
|
||||
export type RouteOptions = {
|
||||
exact?: boolean;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type RoutePath = string;
|
||||
|
||||
// @public
|
||||
export type RouteRef<Params extends AnyParams = any> = {
|
||||
$$routeRefType: 'absolute';
|
||||
|
||||
@@ -23,22 +23,34 @@ import { Observable } from '@backstage/types';
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type Error = {
|
||||
export type ErrorApiError = {
|
||||
name: string;
|
||||
message: string;
|
||||
stack?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Use ErrorApiError instead
|
||||
*/
|
||||
export type Error = ErrorApiError;
|
||||
|
||||
/**
|
||||
* Provides additional information about an error that was posted to the application.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type ErrorContext = {
|
||||
export type ErrorApiErrorContext = {
|
||||
// If set to true, this error should not be displayed to the user. Defaults to false.
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Use ErrorApiErrorContext instead
|
||||
*/
|
||||
export type ErrorContext = ErrorApiErrorContext;
|
||||
|
||||
/**
|
||||
* The error API is used to report errors to the app, and display them to the user.
|
||||
*
|
||||
@@ -62,12 +74,15 @@ export type ErrorApi = {
|
||||
/**
|
||||
* Post an error for handling by the application.
|
||||
*/
|
||||
post(error: Error, context?: ErrorContext): void;
|
||||
post(error: ErrorApiError, context?: ErrorApiErrorContext): void;
|
||||
|
||||
/**
|
||||
* Observe errors posted by other parts of the application.
|
||||
*/
|
||||
error$(): Observable<{ error: Error; context?: ErrorContext }>;
|
||||
error$(): Observable<{
|
||||
error: ErrorApiError;
|
||||
context?: ErrorApiErrorContext;
|
||||
}>;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user