Merge branch 'master' of https://github.com/backstage/backstage into bazaar-columns

This commit is contained in:
Lykke Axlin
2021-11-17 09:54:56 +01:00
129 changed files with 3143 additions and 343 deletions
+6
View File
@@ -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
+8
View File
@@ -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.
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/core-components': patch
'@backstage/plugin-user-settings': patch
---
Add Props Icon for Sidebar Item SidebarSearchField and Settings
+5
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/create-app': patch
---
Refactor and add regression tests for create-app tasks
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': patch
---
Paths can be specified in backend.reading.allow to further restrict allowed targets
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/core-app-api': patch
'@backstage/test-utils': patch
---
Migrated to using new `ErrorApiError` and `ErrorApiErrorContext` names.
+6
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Skip empty file names when scaffolding with nunjucks
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-app-api': patch
---
Deprecated the `BackstagePluginWithAnyOutput` type.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-app-api': patch
---
Start exporting and marking several types as public to address errors in the API report.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Added a scaffolder backend module template for the `create` command.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-plugin-api': minor
---
Remove exports of unused types(`RouteOptions` and `RoutePath`).
+5
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
bump `@rollup/plugin-commonjs` from 17.1.0 to 21.0.1
+5
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
adds getDefaultProcessor method to CatalogBuilder
+5
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
+6
View File
@@ -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:
+39
View File
@@ -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
View File
@@ -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 .",
+8
View File
@@ -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();
+1 -1
View File
@@ -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"
+2 -2
View File
@@ -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();
+135
View File
@@ -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}`,
);
}
}
}
};
+24
View File
@@ -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);
+24 -8
View File
@@ -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,
});
},
});
+77
View File
@@ -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;
}
+83 -8
View File
@@ -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}`);
}
}
+2
View File
@@ -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": "."
}
}
+1 -1
View File
@@ -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",
+69 -26
View File
@@ -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';
+4 -1
View File
@@ -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();
+19 -21
View File
@@ -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