Merge remote-tracking branch 'origin/master' into feat/bitrise_pagination

This commit is contained in:
GregoireW
2021-03-10 17:07:42 +01:00
24 changed files with 509 additions and 148 deletions
+10
View File
@@ -0,0 +1,10 @@
---
'@backstage/plugin-scaffolder': patch
'@backstage/plugin-scaffolder-backend': patch
---
Introduce scaffolder actions page which lists all available actions along with documentation about their input/output.
Allow for actions to be extended with a description.
The list actions page is by default available at `/create/actions`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Update GitHub publisher to display a more helpful error message when repository access update fails.
@@ -4,112 +4,13 @@ title: Builtin actions
description: Documentation describing the built-in template actions.
---
# Built-in Actions
The scaffolder comes with several built-in actions for fetching content,
registering in the catalog and of course actions for creating and publishing a
git repository.
This is the list of built-in template actions
There are several repository providers supported out of the box such as GitHub,
Azure, GitLab and Bitbucket.
## `fetch:plain`
Downloads content and places it in the workspacePath or optionally in a
subdirectory specified by the `targetPath` input option.
input:
- `url` (required) Relative path or absolute URL pointing to the directory tree
to fetch.
- `targetPath` Target path within the working directory to download the contents
to.
output: nothing
## `fetch:cookiecutter`
Downloads template from `url` and templates with cookiecutter
input:
- `url` (required) Relative path or absolute URL pointing to the directory tree
to fetch.
- `targetPath` Target path within the working directory to download the contents
to.
- `values` Values to pass on to cookiecutter for templating.
output: nothing
## `catalog:register`
Registers entity in the software catalog.
input:
- `catalogInfoUrl` (required) An absolute URL pointing to the catalog info file
location.
output:
- `repoContentsUrl` An absolute URL pointing to the root of a repository
directory tree.
- `catalogInfoPath` A relative path from the repo root pointing to the catalog
info file, defaults to /catalog-info.yaml
## `publish:github`
Initializes a git repository of contents in workspacePath and publishes to
GitHub.
input:
- `repoUrl` (required) Repository location
- `repoVisibility` (optional, default: 'private') Possible values: 'private',
'public' or 'internal'.
output:
- `remoteUrl` A URL to the repository with the provider
- `repoContentsUrl` A URL to the root of the repository
## `publish:gitlab`
Initializes a git repository of contents in workspacePath and publishes to
GitLab.
input:
- `repoUrl` (required) Repository location
- `repoVisibility` (optional, default: 'private') Possible values: 'private',
'public' or 'internal'.
output:
- `remoteUrl` A URL to the repository with the provider
- `repoContentsUrl` A URL to the root of the repository
## `publish:bitbucket`
Initializes a git repository of contents in workspacePath and publishes to
Bitbucket.
input:
- `repoUrl` (required) Repository location
- `repoVisibility` (optional, default: 'private') Possible values: 'private',
'public'.
output:
- `remoteUrl` A URL to the repository with the provider
- `repoContentsUrl` A URL to the root of the repository
## `publish:azure`
Initializes a git repository of contents in workspacePath and publishes to
Azure.
input:
- `repoUrl` (required) Repository location
output:
- `remoteUrl` A URL to the repository with the provider
- `repoContentsUrl` A URL to the root of the repository
A list of all registered actions can be found under `/create/actions`. For local
development you should be able to reach them at
`http://localhost:3000/create/actions`.
@@ -38,4 +38,8 @@ export class TemplateActionRegistry {
}
return action;
}
list(): TemplateAction<any>[] {
return [...this.actions.values()];
}
}
@@ -31,6 +31,8 @@ export function createCatalogRegisterAction(options: {
| { repoContentsUrl: string; catalogInfoPath?: string }
>({
id: 'catalog:register',
description:
'Registers entities from a catalog descriptor file in the workspace into the software catalog.',
schema: {
input: {
oneOf: [
@@ -38,6 +38,8 @@ export function createFetchCookiecutterAction(options: {
values: JsonObject;
}>({
id: 'fetch:cookiecutter',
description:
'Downloads a template from the given URL into the workspace, and runs cookiecutter on it.',
schema: {
input: {
type: 'object',
@@ -28,6 +28,8 @@ export function createFetchPlainAction(options: {
return createTemplateAction<{ url: string; targetPath?: string }>({
id: 'fetch:plain',
description:
"Downloads content and places it in the workspace, or optionally in a subdirectory specified by the 'targetPath' input option.",
schema: {
input: {
type: 'object',
@@ -32,6 +32,8 @@ export function createPublishAzureAction(options: {
description?: string;
}>({
id: 'publish:azure',
description:
'Initializes a git repository of the content in the workspace, and publishes it to Azure.',
schema: {
input: {
type: 'object',
@@ -167,6 +167,8 @@ export function createPublishBitbucketAction(options: {
repoVisibility: 'private' | 'public';
}>({
id: 'publish:bitbucket',
description:
'Initializes a git repository of the content in the workspace, and publishes it to Bitbucket.',
schema: {
input: {
type: 'object',
@@ -43,6 +43,8 @@ export function createPublishGithubAction(options: {
repoVisibility: 'private' | 'internal' | 'public';
}>({
id: 'publish:github',
description:
'Initializes a git repository of contents in workspace and publishes it to GitHub.',
schema: {
input: {
type: 'object',
@@ -31,6 +31,8 @@ export function createPublishGitlabAction(options: {
repoVisibility: 'private' | 'internal' | 'public';
}>({
id: 'publish:gitlab',
description:
'Initializes a git repository of the content in the workspace, and publishes it to GitLab.',
schema: {
input: {
type: 'object',
@@ -44,6 +44,7 @@ export type ActionContext<Input extends InputBase> = {
export type TemplateAction<Input extends InputBase> = {
id: string;
description?: string;
schema?: {
input?: Schema;
output?: Schema;
@@ -131,25 +131,30 @@ export class GithubPublisher implements PublisherBase {
const { data } = await repoCreationPromise;
if (access?.startsWith(`${owner}/`)) {
const [, team] = access.split('/');
await client.teams.addOrUpdateRepoPermissionsInOrg({
org: owner,
team_slug: team,
owner,
repo: name,
permission: 'admin',
});
// no need to add access if it's the person who own's the personal account
} else if (access && access !== owner) {
await client.repos.addCollaborator({
owner,
repo: name,
username: access,
permission: 'admin',
});
try {
if (access?.startsWith(`${owner}/`)) {
const [, team] = access.split('/');
await client.teams.addOrUpdateRepoPermissionsInOrg({
org: owner,
team_slug: team,
owner,
repo: name,
permission: 'admin',
});
// no need to add access if it's the person who owns the personal account
} else if (access && access !== owner) {
await client.repos.addCollaborator({
owner,
repo: name,
username: access,
permission: 'admin',
});
}
} catch (e) {
throw new Error(
`Failed to add access to '${access}'. Status ${e.status} ${e.message}`,
);
}
return data?.clone_url;
}
}
@@ -261,6 +261,15 @@ describe('createRouter', () => {
});
});
describe('GET /v2/actions', () => {
it('lists available actions', async () => {
const response = await request(app).get('/v2/actions').send();
expect(response.status).toEqual(200);
expect(response.body[0].id).toBeDefined();
expect(response.body.length).toBeGreaterThan(8);
});
});
describe('POST /v2/tasks', () => {
it('rejects template values which do not match the template schema definition', async () => {
const response = await request(app)
@@ -347,6 +347,16 @@ export async function createRouter(
}
},
)
.get('/v2/actions', async (_req, res) => {
const actionsList = actionRegistry.list().map(action => {
return {
id: action.id,
description: action.description,
schema: action.schema,
};
});
res.json(actionsList);
})
.post('/v2/tasks', async (req, res) => {
const templateName: string = req.body.templateName;
const values: TemplaterValues = req.body.values;
+1
View File
@@ -43,6 +43,7 @@
"@rjsf/core": "^2.4.0",
"@rjsf/material-ui": "^2.4.0",
"classnames": "^2.2.6",
"json-schema": "^0.2.5",
"git-url-parse": "^11.4.4",
"humanize-duration": "^3.25.1",
"luxon": "^1.25.0",
+13 -1
View File
@@ -25,7 +25,7 @@ import {
} from '@backstage/core';
import { ScmIntegrations } from '@backstage/integration';
import ObservableImpl from 'zen-observable';
import { ScaffolderTask, Status } from './types';
import { ListActionsResponse, ScaffolderTask, Status } from './types';
export const scaffolderApiRef = createApiRef<ScaffolderApi>({
id: 'plugin.scaffolder.service',
@@ -72,6 +72,9 @@ export interface ScaffolderApi {
allowedHosts: string[];
}): Promise<{ type: string; title: string; host: string }[]>;
// Returns a list of all installed actions.
listActions(): Promise<ListActionsResponse>;
streamLogs({
taskId,
after,
@@ -227,4 +230,13 @@ export class ScaffolderClient implements ScaffolderApi {
);
});
}
/**
* @returns ListActionsResponse containing all registered actions.
*/
async listActions(): Promise<ListActionsResponse> {
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
const response = await fetch(`${baseUrl}/v2/actions`);
return await response.json();
}
}
@@ -0,0 +1,167 @@
/*
* Copyright 2021 Spotify AB
*
* 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 { ApiRegistry, ApiProvider } from '@backstage/core';
import React from 'react';
import { ScaffolderApi, scaffolderApiRef } from '../../api';
import { ActionsPage } from './ActionsPage';
import { rootRouteRef } from '../../routes';
import { renderInTestApp } from '@backstage/test-utils';
const scaffolderApiMock: jest.Mocked<ScaffolderApi> = {
scaffold: jest.fn(),
getTemplateParameterSchema: jest.fn(),
getIntegrationsList: jest.fn(),
getTask: jest.fn(),
streamLogs: jest.fn(),
listActions: jest.fn(),
};
const apis = ApiRegistry.from([[scaffolderApiRef, scaffolderApiMock]]);
describe('TemplatePage', () => {
beforeEach(() => jest.resetAllMocks());
it('renders action with input', async () => {
scaffolderApiMock.listActions.mockResolvedValue([
{
id: 'test',
description: 'example description',
schema: {
input: {
type: 'object',
required: ['foobar'],
properties: {
foobar: {
title: 'Test title',
type: 'string',
},
},
},
},
},
]);
const rendered = await renderInTestApp(
<ApiProvider apis={apis}>
<ActionsPage />
</ApiProvider>,
{
mountedRoutes: {
'/create/actions': rootRouteRef,
},
},
);
expect(rendered.queryByText('Test title')).toBeInTheDocument();
expect(rendered.queryByText('example description')).toBeInTheDocument();
expect(rendered.queryByText('foobar')).toBeInTheDocument();
expect(rendered.queryByText('output')).not.toBeInTheDocument();
});
it('renders action with input and output', async () => {
scaffolderApiMock.listActions.mockResolvedValue([
{
id: 'test',
description: 'example description',
schema: {
input: {
type: 'object',
required: ['foobar'],
properties: {
foobar: {
title: 'Test title',
type: 'string',
},
},
},
output: {
type: 'object',
properties: {
buzz: {
title: 'Test output',
type: 'string',
},
},
},
},
},
]);
const rendered = await renderInTestApp(
<ApiProvider apis={apis}>
<ActionsPage />
</ApiProvider>,
{
mountedRoutes: {
'/create/actions': rootRouteRef,
},
},
);
expect(rendered.queryByText('Test title')).toBeInTheDocument();
expect(rendered.queryByText('example description')).toBeInTheDocument();
expect(rendered.queryByText('foobar')).toBeInTheDocument();
expect(rendered.queryByText('Test output')).toBeInTheDocument();
});
it('renders action with oneOf input', async () => {
scaffolderApiMock.listActions.mockResolvedValue([
{
id: 'test',
description: 'example description',
schema: {
input: {
oneOf: [
{
type: 'object',
required: ['foo'],
properties: {
foo: {
title: 'Foo title',
description: 'Foo description',
type: 'string',
},
},
},
{
type: 'object',
required: ['bar'],
properties: {
bar: {
title: 'Bar title',
description: 'Bar description',
type: 'string',
},
},
},
],
},
},
},
]);
const rendered = await renderInTestApp(
<ApiProvider apis={apis}>
<ActionsPage />
</ApiProvider>,
{
mountedRoutes: {
'/create/actions': rootRouteRef,
},
},
);
expect(rendered.queryByText('oneOf')).toBeInTheDocument();
expect(rendered.queryByText('Foo title')).toBeInTheDocument();
expect(rendered.queryByText('Foo description')).toBeInTheDocument();
expect(rendered.queryByText('Bar title')).toBeInTheDocument();
expect(rendered.queryByText('Bar description')).toBeInTheDocument();
});
});
@@ -0,0 +1,192 @@
/*
* Copyright 2021 Spotify AB
*
* 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 React from 'react';
import { useAsync } from 'react-use';
import {
useApi,
Progress,
Content,
Header,
Page,
ErrorPage,
} from '@backstage/core';
import { scaffolderApiRef } from '../../api';
import {
Typography,
Paper,
Table,
TableBody,
Box,
TableCell,
TableContainer,
TableHead,
TableRow,
makeStyles,
} from '@material-ui/core';
import { JSONSchema } from '@backstage/catalog-model';
import { JSONSchema7Definition } from 'json-schema';
import classNames from 'classnames';
const useStyles = makeStyles(theme => ({
code: {
fontFamily: 'Menlo, monospace',
padding: theme.spacing(1),
backgroundColor:
theme.palette.type === 'dark'
? theme.palette.grey[700]
: theme.palette.grey[300],
display: 'inline-block',
borderRadius: 5,
border: `1px solid ${theme.palette.grey[500]}`,
position: 'relative',
},
codeRequired: {
'&::after': {
position: 'absolute',
content: '"*"',
top: 0,
right: theme.spacing(0.5),
fontWeight: 'bolder',
color: theme.palette.error.light,
},
},
}));
export const ActionsPage = () => {
const api = useApi(scaffolderApiRef);
const classes = useStyles();
const { loading, value, error } = useAsync(async () => {
return api.listActions();
});
if (loading) {
return <Progress />;
}
if (error) {
return (
<ErrorPage
statusMessage="Failed to load installed actions"
status="500"
/>
);
}
const formatRows = (input: JSONSchema) => {
const properties = input.properties;
if (!properties) {
return undefined;
}
return Object.entries(properties).map(entry => {
const [key] = entry;
const props = (entry[1] as unknown) as JSONSchema;
const codeClassname = classNames(classes.code, {
[classes.codeRequired]: input.required?.includes(key),
});
return (
<TableRow key={key}>
<TableCell>
<div className={codeClassname}>{key}</div>
</TableCell>
<TableCell>{props.title}</TableCell>
<TableCell>{props.description}</TableCell>
<TableCell>
<span className={classes.code}>{props.type}</span>
</TableCell>
</TableRow>
);
});
};
const renderTable = (input: JSONSchema) => {
if (!input.properties) {
return undefined;
}
return (
<TableContainer component={Paper}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Title</TableCell>
<TableCell>Description</TableCell>
<TableCell>Type</TableCell>
</TableRow>
</TableHead>
<TableBody>{formatRows(input)}</TableBody>
</Table>
</TableContainer>
);
};
const renderTables = (name: string, input?: JSONSchema7Definition[]) => {
if (!input) {
return undefined;
}
return (
<>
<Typography variant="h6">{name}</Typography>
{input.map((i, index) => (
<div key={index}>{renderTable((i as unknown) as JSONSchema)}</div>
))}
</>
);
};
const items = value?.map(action => {
if (action.id.startsWith('legacy:')) {
return undefined;
}
const oneOf = renderTables('oneOf', action.schema?.input?.oneOf);
return (
<Box pb={4} key={action.id}>
<Typography variant="h4" className={classes.code}>
{action.id}
</Typography>
<Typography>{action.description}</Typography>
{action.schema?.input && (
<Box pb={2}>
<Typography variant="h5">Input</Typography>
{renderTable(action.schema.input)}
{oneOf}
</Box>
)}
{action.schema?.output && (
<Box pb={2}>
<Typography variant="h5">Output</Typography>
{renderTable(action.schema.output)}
</Box>
)}
</Box>
);
});
return (
<Page themeId="home">
<Header
pageTitleOverride="Create a New Component"
title="Installed actions"
subtitle="This is the collection of all installed actions"
/>
<Content>{items}</Content>
</Page>
);
};
@@ -0,0 +1,17 @@
/*
* Copyright 2021 Spotify AB
*
* 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 { ActionsPage } from './ActionsPage';
@@ -19,11 +19,13 @@ import { Routes, Route } from 'react-router';
import { ScaffolderPage } from './ScaffolderPage';
import { TemplatePage } from './TemplatePage';
import { TaskPage } from './TaskPage';
import { ActionsPage } from './ActionsPage';
export const Router = () => (
<Routes>
<Route path="/" element={<ScaffolderPage />} />
<Route path="/templates/:templateName" element={<TemplatePage />} />
<Route path="/tasks/:taskId" element={<TaskPage />} />
<Route path="/actions" element={<ActionsPage />} />
</Routes>
);
@@ -39,6 +39,7 @@ const scaffolderApiMock: jest.Mocked<ScaffolderApi> = {
getIntegrationsList: jest.fn(),
getTask: jest.fn(),
streamLogs: jest.fn(),
listActions: jest.fn(),
};
const errorApiMock = { post: jest.fn(), error$: jest.fn() };
+10
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { JSONSchema } from '@backstage/catalog-model';
import { JsonValue } from '@backstage/config';
export type Status = 'open' | 'processing' | 'failed' | 'completed';
@@ -54,3 +55,12 @@ export type ScaffolderTask = {
lastHeartbeatAt: string;
createdAt: string;
};
export type ListActionsResponse = Array<{
id: string;
description?: string;
schema?: {
input?: JSONSchema;
output?: JSONSchema;
};
}>;
+22 -22
View File
@@ -8884,10 +8884,10 @@ bluebird@~3.4.1:
resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3"
integrity sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
version "4.11.8"
resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.9:
version "4.12.0"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
body-parser@1.19.0, body-parser@^1.18.3:
version "1.19.0"
@@ -8992,9 +8992,9 @@ breakword@^1.0.5:
dependencies:
wcwidth "^1.0.1"
brorand@^1.0.1:
brorand@^1.0.1, brorand@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
browser-process-hrtime@^1.0.0:
@@ -12062,17 +12062,17 @@ element-resize-detector@^1.2.1:
batch-processor "1.0.0"
elliptic@^6.0.0:
version "6.5.3"
resolved "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6"
integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==
version "6.5.4"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==
dependencies:
bn.js "^4.4.0"
brorand "^1.0.1"
bn.js "^4.11.9"
brorand "^1.1.0"
hash.js "^1.0.0"
hmac-drbg "^1.0.0"
inherits "^2.0.1"
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.0"
hmac-drbg "^1.0.1"
inherits "^2.0.4"
minimalistic-assert "^1.0.1"
minimalistic-crypto-utils "^1.0.1"
emitter-component@^1.1.1:
version "1.1.1"
@@ -14673,7 +14673,7 @@ hash-stream-validation@^0.2.1, hash-stream-validation@^0.2.2:
hash.js@^1.0.0, hash.js@^1.0.3:
version "1.1.7"
resolved "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==
dependencies:
inherits "^2.0.3"
@@ -14735,9 +14735,9 @@ history@^5.0.0:
dependencies:
"@babel/runtime" "^7.7.6"
hmac-drbg@^1.0.0:
hmac-drbg@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=
dependencies:
hash.js "^1.0.3"
@@ -15283,7 +15283,7 @@ inflight@^1.0.4:
inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
version "2.0.4"
resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
inherits@2.0.1:
@@ -18480,12 +18480,12 @@ mini-css-extract-plugin@^0.9.0:
minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
minimalistic-crypto-utils@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
minimatch@0.3: