This commit is contained in:
Tim Hansen
2020-08-25 13:20:10 -06:00
50 changed files with 1174 additions and 426 deletions
+31 -1
View File
@@ -14,7 +14,37 @@ jobs:
- uses: actions/checkout@v2
with:
fetch-depth: 0 # Required to retrieve git history
- run: yarn install && yarn build-storybook
# Beginning of yarn setup, keep in sync between all workflows, see ci.yml
- name: use node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: cache all node_modules
id: cache-modules
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }}
- name: find location of global yarn cache
id: yarn-cache
if: steps.cache-modules.outputs.cache-hit != 'true'
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
if: steps.cache-modules.outputs.cache-hit != 'true'
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: yarn install
run: yarn install --frozen-lockfile
# End of yarn setup
- run: yarn build-storybook
- uses: chromaui/action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
+37 -23
View File
@@ -20,34 +20,52 @@ jobs:
- uses: actions/checkout@v2
- name: fetch branch master
run: git fetch origin master
- name: find location of global yarn cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: cache node_modules
uses: actions/cache@v2
with:
path: node_modules
key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
# Beginning of yarn setup, keep in sync between all workflows.
# TODO(Rugvip): move this to composite action once all features we use are supported
- name: use node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
# Cache every node_modules folder inside the monorepo
- name: cache all node_modules
id: cache-modules
uses: actions/cache@v2
with:
path: '**/node_modules'
# We use both yarn.lock and package.json as cache keys to ensure that
# changes to local monorepo packages bust the cache.
key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }}
# If we get a cache hit for node_modules, there's no need to bring in the global
# yarn cache or run yarn install, as all dependencies will be installed already.
- name: find location of global yarn cache
id: yarn-cache
if: steps.cache-modules.outputs.cache-hit != 'true'
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
if: steps.cache-modules.outputs.cache-hit != 'true'
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: yarn install
if: steps.cache-modules.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile
# End of yarn setup
- name: check for yarn.lock changes
id: yarn-lock
run: git diff --quiet origin/master HEAD -- yarn.lock
continue-on-error: true
- name: yarn install
run: yarn install --frozen-lockfile
- name: verify doc links
run: node docs/verify-links.js
@@ -59,8 +77,7 @@ jobs:
- name: build changed packages
if: ${{ steps.yarn-lock.outcome == 'success' }}
# Need to build all dependencies as well to be able to run tests later
run: yarn lerna -- run build --since origin/master --include-dependencies
run: yarn lerna -- run build --since origin/master
- name: build all packages
if: ${{ steps.yarn-lock.outcome == 'failure' }}
@@ -79,6 +96,3 @@ jobs:
- name: verify plugin template
run: yarn lerna -- run diff -- --check
- name: verify storybook
run: yarn workspace storybook build-storybook
+23 -10
View File
@@ -26,22 +26,35 @@ jobs:
name: Node ${{ matrix.node-version }} on ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: find location of global yarn cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
# Beginning of yarn setup, keep in sync between all workflows, see ci.yml
- name: use node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: cache all node_modules
id: cache-modules
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }}
- name: find location of global yarn cache
id: yarn-cache
if: steps.cache-modules.outputs.cache-hit != 'true'
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
if: steps.cache-modules.outputs.cache-hit != 'true'
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: yarn install
run: yarn install --frozen-lockfile
# End of yarn setup
- run: yarn tsc
- name: yarn build
run: yarn build --ignore example-app --ignore example-backend --ignore @techdocs/cli --ignore backstage-microsite
+23 -10
View File
@@ -33,22 +33,35 @@ jobs:
name: Node ${{ matrix.node-version }} on ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: find location of global yarn cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
# Beginning of yarn setup, keep in sync between all workflows, see ci.yml
- name: use node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: cache all node_modules
id: cache-modules
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }}
- name: find location of global yarn cache
id: yarn-cache
if: steps.cache-modules.outputs.cache-hit != 'true'
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
if: steps.cache-modules.outputs.cache-hit != 'true'
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: yarn install
run: yarn install --frozen-lockfile
# End of yarn setup
- run: yarn tsc
- name: yarn build
run: yarn build --ignore example-app --ignore example-backend --ignore @techdocs/cli --ignore backstage-microsite
+22 -16
View File
@@ -18,29 +18,35 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: find location of global yarn cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: cache node_modules
uses: actions/cache@v2
with:
path: node_modules
key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
# Beginning of yarn setup, keep in sync between all workflows, see ci.yml
- name: use node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: cache all node_modules
id: cache-modules
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }}
- name: find location of global yarn cache
id: yarn-cache
if: steps.cache-modules.outputs.cache-hit != 'true'
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
if: steps.cache-modules.outputs.cache-hit != 'true'
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: yarn install
run: yarn install --frozen-lockfile
# End of yarn setup
# Tests are broken on Windows, disabled for now
# - name: test
+21 -16
View File
@@ -18,29 +18,34 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: find location of global yarn cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: cache node_modules
uses: actions/cache@v2
with:
path: node_modules
key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
# Beginning of yarn setup, keep in sync between all workflows, see ci.yml
- name: use node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: cache all node_modules
id: cache-modules
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }}
- name: find location of global yarn cache
id: yarn-cache
if: steps.cache-modules.outputs.cache-hit != 'true'
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
if: steps.cache-modules.outputs.cache-hit != 'true'
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: yarn install
run: yarn install --frozen-lockfile
# End of yarn setup
- name: lint
run: yarn lerna -- run lint
+20 -18
View File
@@ -21,32 +21,34 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: find location of global yarn cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: cache node_modules
uses: actions/cache@v2
with:
path: node_modules
key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
# Beginning of yarn setup, keep in sync between all workflows, see ci.yml
- name: use node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: cache all node_modules
id: cache-modules
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }}
- name: find location of global yarn cache
id: yarn-cache
if: steps.cache-modules.outputs.cache-hit != 'true'
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
if: steps.cache-modules.outputs.cache-hit != 'true'
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: yarn install
run: yarn install --frozen-lockfile
# End of yarn setup
- name: build microsite
run: yarn workspace backstage-microsite build
@@ -25,32 +25,34 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: find location of global yarn cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: cache node_modules
uses: actions/cache@v2
with:
path: node_modules
key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
# Beginning of yarn setup, keep in sync between all workflows, see ci.yml
- name: use node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: cache all node_modules
id: cache-modules
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }}
- name: find location of global yarn cache
id: yarn-cache
if: steps.cache-modules.outputs.cache-hit != 'true'
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
if: steps.cache-modules.outputs.cache-hit != 'true'
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: yarn install
run: yarn install --frozen-lockfile
# End of yarn setup
- name: build microsite
run: yarn workspace backstage-microsite build
+13
View File
@@ -122,3 +122,16 @@ auth:
domain:
$secret:
env: AUTH_AUTH0_DOMAIN
microsoft:
development:
appOrigin: "http://localhost:3000/"
secure: false
clientId:
$secret:
env: AUTH_MICROSOFT_CLIENT_ID
clientSecret:
$secret:
env: AUTH_MICROSOFT_CLIENT_SECRET
tenantId:
$secret:
env: AUTH_MICROSOFT_TENANT_ID
Binary file not shown.

After

Width:  |  Height:  |  Size: 746 KiB

+1 -1
View File
@@ -42,7 +42,7 @@ sites with the Backstage UI.
The TechDocs Reader purpose is also to open up the opportunity to integrate
TechDocs widgets for a customized full-featured TechDocs experience.
([coming soon V.2](https://github.com/spotify/backstage/milestone/17))
([coming soon V.3](./README.md#project-roadmap))
[TechDocs Reader](https://github.com/spotify/backstage/blob/master/plugins/techdocs/src/reader/README.md)
@@ -6,26 +6,38 @@ sidebar_label: Creating and Publishing Documentation
This section will guide you through:
- Creating a basic setup for your documentation
- Writing and previewing your documentation in a local Backstage environment
- Creating a build ready for publication
- Publishing your documentation and making your Backstage instance read your
published docs.
- [Create a basic documentation setup](#create-a-basic-documentation-setup)
- [Use the documentation template](#use-the-documentation-template)
- [Manually add documentation setup to already existing repository](#manually-add-documentation-setup-to-already-existing-repository)
- [Writing and previewing your documentation](#writing-and-previewing-your-documentation)
## Prerequisities
- [Docker](https://docs.docker.com/get-docker/)
- Static file hosting
- A working Backstage instance with TechDocs installed (see
[TechDocs getting started](getting-started.md))
## Create a basic documentation setup
In your home directory (also known as `~`), create a directory that contains
your documentation (for example, `hello-docs`). Inside this directory, create a
file called `mkdocs.yml`. Below is a basic example of how it could look.
### Use the documentation template
The `~/hello-docs/mkdocs.yml` file should have the following content:
Your working Backstage instance should by default have a documentation template
added. If not, follow these
[instructions](../software-templates/installation.md#adding-templates) to add
the documentation template.
![Documentation Template](../../assets/techdocs/documentation-template.png)
Create an entity from the documentation template and you will get the needed
setup for free.
### Manually add documentation setup to already existing repository
Prerequisities:
- `catalog-info.yml` file registered to Backstage.
Create a `mkdocs.yml` file in the root of the repository with the following
content:
```yaml
site_name: 'example-docs'
@@ -37,7 +49,20 @@ plugins:
- techdocs-core
```
The `~/hello-docs/docs/index.md` should have the following content:
Update your `catalog-info.yaml` file in the root of the repository with the
following content:
```yaml
metadata:
annotations:
backstage.io/techdocs-ref: dir:./
```
Create a `/docs` folder in the root of the project with at least a `index.md`
file. _(If you add more markdown files, make sure to update the nav in the
mkdocs.yml file to get a proper navigation for your documentation.)_
The `docs/index.md` can for example have the following content:
```md
# example docs
@@ -45,6 +70,9 @@ The `~/hello-docs/docs/index.md` should have the following content:
This is a basic example of documentation.
```
Commit your changes, open a pull request and merge. You will now get your
updated documentation next time you run Backstage!
## Writing and previewing your documentation
Using the `techdocs-cli` you can preview your docs inside a local Backstage
@@ -54,83 +82,6 @@ want to write your documentation.
To do this you can run:
```bash
cd ~/hello-docs/
cd ~/<repository-path>/
npx techdocs-cli serve
```
## Build production ready documentation
To get a build suitable for publication you can build your docs using the
`spotify/techdocs` container:
```bash
cd ~/hello-docs/
docker run -it -w /content -v $(pwd):/content spotify/techdocs build
```
You should now have a folder called `~/hello-docs/site/`.
## Deploy to a file server
In order to serve documentation to TechDocs, our Backstage plugin needs to
download the HTML rendered from the previous step. This will likely exist on an
external file server, or a storage solution such as Google Cloud Storage.
When deploying documentation, it should be deployed on that file server/storage
solution with the following convention: `{id}/{file}`. For example, if you want
to upload the `getting-started/index.html` file for the `backstage`
documentation site, we would upload it to our file server as
`backstage/getting-started/index.html`.
To explain further what this would look like for multiple documentation sites,
take a look at this example file tree that would be represented on your file
server:
```md
/backstage/index.html /backstage/getting-started/index.html
/backstage/contributing/index.html /mkdocs/index.html
/mkdocs/plugin-development/index.html
/mkdocs/plugin-development/debugging/index.html
```
In this file tree, we have two documentation sites available: `backstage` and
`mkdocs`. Each of them expose several pages. Let's say both of these are hosted
on `http://example.com` as the server URL.
When you configure the TechDocs plugin in Backstage to use `http://example.com`
as the file server/storage solution, it will translate the following URLs to the
file server:
| Backstage URL | File Server URL |
| --------------------------------------------------------- | ------------------------------------------------------- |
| https://demo.backstage.io/docs/backstage/ | http://example.com/backstage/index.html |
| https://demo.backstage.io/docs/mkdocs/plugin-development/ | http://example.com/mkdocs/plugin-development/index.html |
Then deploying new sites is easy: simply copy over the `site/` folder produced
in the [Create documentation](#build-production-ready-documentation) step above
to the file server/storage solution under the ID of the documentation site. It
will then become immediately available in Backstage under the same ID as you can
see in the table above.
So, if the URL to your file server is `http://example.com/`, your
`~/hello-docs/site` folder containing the documentation should be accessible at
`http://example.com/hello-docs/`.
## Configure TechDocs to read from file server
In order for Backstage to show your documentation, it needs to know where you
uploaded it.
Make sure you have Backstage set up using
[TechDocs getting started](getting-started.md).
To point Backstage to your docs storage, add or change the following lines in
your Backstage `app-config.yaml`:
```yaml
techdocs:
storageUrl: http://example.com
```
You can now start Backstage using `yarn start` and open up your browser at
`http://localhost:3000/docs/hello-docs` to view your docs.
+17 -17
View File
@@ -3,16 +3,6 @@ id: getting-started
title: Getting Started
---
> TechDocs is not yet feature complete - currently you can't set up a complete
> end-to-end working TechDocs plugin without customizing the plugin itself.
> What you can expect from TechDocs V.0 is a demonstration of how to integrate
> docs into Backstage. TechDocs can create docs using
> [mkdocs](https://www.mkdocs.org/), as well as read published docs. If you
> publish generated docs and pass in a `storageUrl` in your `app-config.yaml`,
> you can view them in Backstage by going to
> `http://localhost:3000/docs/<remote-folder>`.
TechDocs functions as a plugin to Backstage, so you will need to use Backstage
to use TechDocs.
@@ -48,8 +38,8 @@ containing your new Backstage application.
## Installing TechDocs
TechDocs is not provided with the Backstage application by default, so you will
now need to set up TechDocs manually. It should take less than a minute.
TechDocs is provided with the Backstage application by default. If you want to
set up TechDocs manually, keep follow the instructions below.
### Adding the package
@@ -84,19 +74,29 @@ export { plugin as TechDocs } from '@backstage/plugin-techdocs';
### Setting the configuration
TechDocs allows for configuration of the docs storage URL through your
`app-config` file. The URL provided here is for demo docs to use for testing
purposes.
`app-config` file.
To use the demo docs, add the following lines to `app-config.yaml`:
The default storage URL:
```yaml
techdocs:
storageUrl: https://techdocs-mock-sites.storage.googleapis.com
storageUrl: http://localhost:7000/techdocs/static/docs
```
If you want to configure this to point to another storage URL, change the value
of `storageUrl`.
## Run Backstage locally
Change folder to your Backstage application root and run the following command:
Change folder to `<backstage-project-root>/packages/backend` and run the
following command:
```bash
yarn start
```
Open a new command line window. Change directory to your Backstage application
root and run the following command:
```bash
yarn start
+12
View File
@@ -0,0 +1,12 @@
const React = require('react');
const Redirect = require('../../core/Redirect.js');
const siteConfig = require(process.cwd() + '/siteConfig.js');
function Docs() {
return (
<Redirect redirect="/docs/overview/what-is-backstage" config={siteConfig} />
);
}
module.exports = Docs;
+1
View File
@@ -40,6 +40,7 @@ const siteConfig = {
},
{
doc: 'overview/what-is-backstage',
href: '/docs',
label: 'Docs',
},
{
+1 -5
View File
@@ -42,11 +42,7 @@ nav:
- Overview: 'features/techdocs/README.md'
- Getting Started: 'features/techdocs/getting-started.md'
- Concepts: 'features/techdocs/concepts.md'
- Reading Documentation: 'features/techdocs/reading-documentation.md'
- Writing Documentation: 'features/techdocs/writing-documentation.md'
- Publishing Documentation: 'features/techdocs/publishing-documentation.md'
- Contributing: 'features/techdocs/contributing.md'
- Debugging: 'features/techdocs/debugging.md'
- Creating and Publishing Documentation: 'features/techdocs/creating-and-publishing.md'
- FAQ: 'features/techdocs/FAQ.md'
- Plugins:
- Overview: 'plugins/index.md'
+15 -1
View File
@@ -30,6 +30,7 @@ import {
OktaAuth,
GitlabAuth,
Auth0Auth,
MicrosoftAuth,
oauthRequestApiRef,
OAuthRequestManager,
googleAuthApiRef,
@@ -38,6 +39,7 @@ import {
oktaAuthApiRef,
gitlabAuthApiRef,
auth0AuthApiRef,
microsoftAuthApiRef,
storageApiRef,
WebStorage,
} from '@backstage/core';
@@ -74,7 +76,10 @@ import {
TravisCIApi,
travisCIApiRef,
} from '@roadiehq/backstage-plugin-travis-ci';
import { GithubPullRequestsClient, githubPullRequestsApiRef } from '@roadiehq/backstage-plugin-github-pull-requests';
import {
GithubPullRequestsClient,
githubPullRequestsApiRef,
} from '@roadiehq/backstage-plugin-github-pull-requests';
export const apis = (config: ConfigApi) => {
// eslint-disable-next-line no-console
@@ -122,6 +127,15 @@ export const apis = (config: ConfigApi) => {
}),
);
builder.add(
microsoftAuthApiRef,
MicrosoftAuth.create({
backendUrl,
basePath: '/auth/',
oauthRequestApi,
}),
);
const githubAuthApi = builder.add(
githubAuthApiRef,
GithubAuth.create({
+7
View File
@@ -19,6 +19,7 @@ import {
gitlabAuthApiRef,
oktaAuthApiRef,
githubAuthApiRef,
microsoftAuthApiRef,
} from '@backstage/core';
export const providers = [
@@ -28,6 +29,12 @@ export const providers = [
message: 'Sign In using Google',
apiRef: googleAuthApiRef,
},
{
id: 'microsoft-auth-provider',
title: 'Microsoft',
message: 'Sign In using Microsoft Azure AD',
apiRef: microsoftAuthApiRef,
},
{
id: 'gitlab-auth-provider',
title: 'GitLab',
-1
View File
@@ -29,7 +29,6 @@
"clean": "backstage-cli clean"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.19",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0"
},
+1 -1
View File
@@ -22,7 +22,7 @@ import { buildBundle } from '../../lib/bundler';
export default async (cmd: Command) => {
const appConfigs = await loadConfig({
env: 'production',
env: process.env.NODE_ENV ?? 'production',
rootPaths: [paths.targetRoot, paths.targetDir],
});
await buildBundle({
+1 -1
View File
@@ -22,7 +22,7 @@ import { serveBundle } from '../../lib/bundler';
export default async (cmd: Command) => {
const appConfigs = await loadConfig({
env: 'development',
env: process.env.NODE_ENV ?? 'development',
rootPaths: [paths.targetRoot, paths.targetDir],
});
const waitForExit = await serveBundle({
+3 -1
View File
@@ -22,12 +22,14 @@ import { serveBackend } from '../../lib/bundler/backend';
export default async (cmd: Command) => {
const appConfigs = await loadConfig({
env: 'development',
env: process.env.NODE_ENV ?? 'development',
rootPaths: [paths.targetRoot, paths.targetDir],
});
const waitForExit = await serveBackend({
entry: 'src/index',
checksEnabled: cmd.check,
inspectEnabled: cmd.inspect,
config: ConfigReader.fromConfigs(appConfigs),
appConfigs,
});
+1 -1
View File
@@ -22,7 +22,7 @@ export default async (cmd: Command, cmdArgs: string[]) => {
const args = [
'--ext=js,jsx,ts,tsx',
'--max-warnings=0',
'--format=eslint-formatter-friendly',
`--format=${cmd.format}`,
...(cmdArgs ?? [paths.targetDir]),
];
if (cmd.fix) {
+1 -1
View File
@@ -22,7 +22,7 @@ import { buildBundle } from '../../lib/bundler';
export default async (cmd: Command) => {
const appConfigs = await loadConfig({
env: 'production',
env: process.env.NODE_ENV ?? 'production',
rootPaths: [paths.targetRoot, paths.targetDir],
});
await buildBundle({
+1 -1
View File
@@ -22,7 +22,7 @@ import { serveBundle } from '../../lib/bundler';
export default async (cmd: Command) => {
const appConfigs = await loadConfig({
env: 'development',
env: process.env.NODE_ENV ?? 'development',
rootPaths: [paths.targetRoot, paths.targetDir],
});
const waitForExit = await serveBundle({
+6
View File
@@ -52,6 +52,7 @@ const main = (argv: string[]) => {
.command('backend:dev')
.description('Start local development server with HMR for the backend')
.option('--check', 'Enable type checking and linting')
.option('--inspect', 'Enable debugger')
.action(lazyAction(() => import('./commands/backend/dev'), 'default'));
program
@@ -113,6 +114,11 @@ const main = (argv: string[]) => {
program
.command('lint')
.option(
'--format <format>',
'Lint report output format',
'eslint-formatter-friendly',
)
.option('--fix', 'Attempt to automatically fix violations')
.description('Lint a package')
.action(lazyAction(() => import('./commands/lint'), 'default'));
+5 -1
View File
@@ -19,7 +19,11 @@ import { createBackendConfig } from './config';
import { resolveBundlingPaths } from './paths';
import { ServeOptions } from './types';
export async function serveBackend(options: ServeOptions) {
export async function serveBackend(
options: ServeOptions & {
inspectEnabled: boolean;
},
) {
const paths = resolveBundlingPaths(options);
const config = createBackendConfig(paths, {
...options,
+4 -1
View File
@@ -205,7 +205,10 @@ export function createBackendConfig(
: {}),
},
plugins: [
new StartServerPlugin('main.js'),
new StartServerPlugin({
name: 'main.js',
nodeArgs: options.inspectEnabled ? ['--inspect'] : undefined,
}),
new webpack.HotModuleReplacementPlugin(),
...(checksEnabled
? [
+3 -1
View File
@@ -25,7 +25,9 @@ export type BundlingOptions = {
baseUrl: URL;
};
export type BackendBundlingOptions = Omit<BundlingOptions, 'baseUrl'>;
export type BackendBundlingOptions = Omit<BundlingOptions, 'baseUrl'> & {
inspectEnabled: boolean;
};
export type ServeOptions = BundlingPathsOptions & {
checksEnabled: boolean;
+4 -2
View File
@@ -30,10 +30,12 @@ export type AppConfig = {
};
export type Config = {
has(key: string): boolean;
keys(): string[];
get(key: string): JsonValue;
getOptional(key: string): JsonValue | undefined;
get(key?: string): JsonValue;
getOptional(key?: string): JsonValue | undefined;
getConfig(key: string): Config;
getOptionalConfig(key: string): Config | undefined;
@@ -277,6 +277,24 @@ export const auth0AuthApiRef = createApiRef<
description: 'Provides authentication towards Auth0 APIs',
});
/**
* Provides authentication towards Microsoft APIs and identities.
*
* For more info and a full list of supported scopes, see:
* - https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent
* - https://docs.microsoft.com/en-us/graph/permissions-reference
*/
export const microsoftAuthApiRef = createApiRef<
OAuthApi &
OpenIdConnectApi &
ProfileInfoApi &
BackstageIdentityApi &
SessionStateApi
>({
id: 'core.auth.microsoft',
description: 'Provides authentication towards Microsoft APIs and identities',
});
/**
* Provides authentication for custom identity providers.
*/
@@ -20,3 +20,4 @@ export * from './google';
export * from './oauth2';
export * from './okta';
export * from './auth0';
export * from './microsoft';
@@ -0,0 +1,172 @@
/*
* Copyright 2020 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 MicrosoftIcon from '@material-ui/icons/AcUnit';
import { DefaultAuthConnector } from '../../../../lib/AuthConnector';
import { MicrosoftSession } from './types';
import {
OAuthApi,
OpenIdConnectApi,
ProfileInfoApi,
ProfileInfo,
SessionStateApi,
SessionState,
BackstageIdentityApi,
AuthRequestOptions,
BackstageIdentity,
} from '../../../definitions/auth';
import { OAuthRequestApi, AuthProvider } from '../../../definitions';
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';
import { Observable } from '../../../../types';
type CreateOptions = {
backendUrl: string;
basePath: string;
oauthRequestApi: OAuthRequestApi;
environment?: string;
provider?: AuthProvider & { id: string };
};
export type MicrosoftAuthResponse = {
providerInfo: {
accessToken: string;
idToken: string;
scope: string;
expiresInSeconds: number;
};
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
};
const DEFAULT_PROVIDER = {
id: 'microsoft',
title: 'Microsoft',
icon: MicrosoftIcon,
};
class MicrosoftAuth
implements
OAuthApi,
OpenIdConnectApi,
ProfileInfoApi,
BackstageIdentityApi,
SessionStateApi {
static create({
backendUrl,
basePath,
environment = 'development',
provider = DEFAULT_PROVIDER,
oauthRequestApi,
}: CreateOptions) {
const connector = new DefaultAuthConnector({
backendUrl,
basePath,
environment,
provider,
oauthRequestApi: oauthRequestApi,
sessionTransform(res: MicrosoftAuthResponse): MicrosoftSession {
return {
...res,
providerInfo: {
idToken: res.providerInfo.idToken,
accessToken: res.providerInfo.accessToken,
scopes: MicrosoftAuth.normalizeScopes(res.providerInfo.scope),
expiresAt: new Date(
Date.now() + res.providerInfo.expiresInSeconds * 1000,
),
},
};
},
});
const sessionManager = new RefreshingAuthSessionManager({
connector,
defaultScopes: new Set([
'openid',
'offline_access',
'profile',
'email',
'User.Read',
]),
sessionScopes: (session: MicrosoftSession) => session.providerInfo.scopes,
sessionShouldRefresh: (session: MicrosoftSession) => {
const expiresInSec =
(session.providerInfo.expiresAt.getTime() - Date.now()) / 1000;
return expiresInSec < 60 * 5;
},
});
return new MicrosoftAuth(sessionManager);
}
sessionState$(): Observable<SessionState> {
return this.sessionManager.sessionState$();
}
constructor(
private readonly sessionManager: SessionManager<MicrosoftSession>,
) {}
async getAccessToken(
scope?: string | string[],
options?: AuthRequestOptions,
) {
const session = await this.sessionManager.getSession({
...options,
scopes: MicrosoftAuth.normalizeScopes(scope),
});
return session?.providerInfo.accessToken ?? '';
}
async getIdToken(options: AuthRequestOptions = {}) {
const session = await this.sessionManager.getSession(options);
return session?.providerInfo.idToken ?? '';
}
async logout() {
await this.sessionManager.removeSession();
}
async getBackstageIdentity(
options: AuthRequestOptions = {},
): Promise<BackstageIdentity | undefined> {
const session = await this.sessionManager.getSession(options);
return session?.backstageIdentity;
}
async getProfile(options: AuthRequestOptions = {}) {
const session = await this.sessionManager.getSession(options);
return session?.profile;
}
static normalizeScopes(scopes?: string | string[]): Set<string> {
if (!scopes) {
return new Set();
}
const scopeList = Array.isArray(scopes)
? scopes
: scopes.split(/[\s|,]/).filter(Boolean);
return new Set(scopeList);
}
}
export default MicrosoftAuth;
@@ -0,0 +1,18 @@
/*
* Copyright 2020 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 * from './types';
export { default as MicrosoftAuth } from './MicrosoftAuth';
@@ -0,0 +1,28 @@
/*
* Copyright 2020 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 { ProfileInfo, BackstageIdentity } from '../../../definitions';
export type MicrosoftSession = {
providerInfo: {
idToken: string;
accessToken: string;
scopes: Set<string>;
expiresAt: Date;
};
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
};
@@ -21,6 +21,7 @@ import {
identityApiRef,
oauth2ApiRef,
oktaAuthApiRef,
microsoftAuthApiRef,
useApi,
configApiRef,
} from '@backstage/core-api';
@@ -60,6 +61,13 @@ export function SidebarUserSettings() {
icon={Star}
/>
)}
{providers.includes('microsoft') && (
<OIDCProviderSettings
title="Microsoft"
apiRef={microsoftAuthApiRef}
icon={Star}
/>
)}
{providers.includes('github') && (
<OAuthProviderSettings
title="GitHub"
@@ -37,6 +37,7 @@ const columns: TableColumn<Entity>[] = [
.filter(Boolean)
.join(':'),
kind: entity.kind,
selectedTabId: 'overview',
})}
>
{entity.metadata.name}
+35
View File
@@ -74,6 +74,41 @@ export AUTH_AUTH0_CLIENT_ID=x
export AUTH_AUTH0_CLIENT_SECRET=x
```
### Microsoft
#### Creating an Azure AD App Registration
An Azure AD App Registration is required to be able to sign in using Azure AD and the Microsoft Graph API.
Click [here](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps) to create a new one.
- Click on the `New Registration` button.
- Give the app a name. e.g. `backstage-dev`
- Select `Accounts in this organizational directory only` under supported account types.
- Enter the callback URL for your backstage backend instance:
- For local development, this is likely `http://localhost:7000/auth/microsoft/handler/frame`
- For non-local deployments, this will be `https://{APP_FQDN}:{APP_BACKEND_PORT}/auth/microsoft/handler/frame`
- Click `Register`.
We also need to generate a client secret so Backstage can authenticate as this app.
- Click on the `Certificates & secrets` menu item.
- Under `Client secrets`, click on `New client secret`.
- Add a description for the new secret. e.g. `auth-backend-plugin`
- Select an expiry time; `1 Year`, `2 Years` or `Never`.
- Click `Add`.
The secret value will then be displayed on the screen. **You will not be able to retrieve it again after leaving the page**.
#### Starting the Auth Backend
```bash
cd packages/backend
export AUTH_MICROSOFT_CLIENT_ID=x
export AUTH_MICROSOFT_CLIENT_SECRET=x
export AUTH_MICROSOFT_TENANT_ID=x
yarn start
```
### SAML
To try out SAML, you can use the mock identity provider:
+3
View File
@@ -30,6 +30,7 @@
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"fs-extra": "^9.0.0",
"got": "^11.5.2",
"helmet": "^4.0.0",
"jose": "^1.27.1",
"jwt-decode": "2.2.0",
@@ -40,6 +41,7 @@
"passport-github2": "^0.1.12",
"passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0",
"passport-microsoft": "^0.1.0",
"passport-oauth2": "^1.5.0",
"passport-okta-oauth": "^0.0.1",
"passport-saml": "^1.3.3",
@@ -55,6 +57,7 @@
"@types/passport": "^1.0.3",
"@types/passport-github2": "^1.2.4",
"@types/passport-google-oauth20": "^2.0.3",
"@types/passport-microsoft": "^0.0.0",
"@types/passport-saml": "^1.1.2",
"jest-fetch-mock": "^3.0.3"
},
@@ -24,6 +24,7 @@ import { createOAuth2Provider } from './oauth2';
import { createOktaProvider } from './okta';
import { createSamlProvider } from './saml';
import { createAuth0Provider } from './auth0';
import { createMicrosoftProvider } from './microsoft';
import {
AuthProviderConfig,
AuthProviderFactory,
@@ -42,6 +43,7 @@ const factories: { [providerId: string]: AuthProviderFactory } = {
saml: createSamlProvider,
okta: createOktaProvider,
auth0: createAuth0Provider,
microsoft: createMicrosoftProvider,
oauth2: createOAuth2Provider,
};
@@ -0,0 +1,17 @@
/*
* Copyright 2020 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 { createMicrosoftProvider } from './provider';
@@ -0,0 +1,238 @@
/*
* Copyright 2020 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 express from 'express';
import passport from 'passport';
import { Strategy as MicrosoftStrategy } from 'passport-microsoft';
import {
executeFrameHandlerStrategy,
executeRedirectStrategy,
executeRefreshTokenStrategy,
makeProfileInfo,
executeFetchUserProfileStrategy,
} from '../../lib/PassportStrategyHelper';
import {
OAuthProviderHandlers,
RedirectInfo,
AuthProviderConfig,
OAuthProviderOptions,
OAuthResponse,
PassportDoneCallback,
} from '../types';
import { OAuthProvider } from '../../lib/OAuthProvider';
import { Logger } from 'winston';
import { TokenIssuer } from '../../identity';
import { Config } from '@backstage/config';
import got from 'got';
type PrivateInfo = {
refreshToken: string;
};
export type MicrosoftAuthProviderOptions = OAuthProviderOptions & {
authorizationUrl?: string;
tokenUrl?: string;
};
export class MicrosoftAuthProvider implements OAuthProviderHandlers {
private readonly _strategy: MicrosoftStrategy;
static transformAuthResponse(
accessToken: string,
params: any,
rawProfile: any,
photoURL: any,
): OAuthResponse {
let passportProfile: passport.Profile = rawProfile;
passportProfile = {
...passportProfile,
photos: [{ value: photoURL }],
};
const profile = makeProfileInfo(passportProfile, params.id_token);
const providerInfo = {
idToken: params.id_token,
accessToken,
scope: params.scope,
expiresInSeconds: params.expires_in,
};
return {
providerInfo,
profile,
};
}
constructor(options: MicrosoftAuthProviderOptions) {
this._strategy = new MicrosoftStrategy(
{
clientID: options.clientId,
clientSecret: options.clientSecret,
callbackURL: options.callbackUrl,
authorizationURL: options.authorizationUrl,
tokenURL: options.tokenUrl,
passReqToCallback: false as true,
},
(
accessToken: any,
refreshToken: any,
params: any,
rawProfile: passport.Profile,
done: PassportDoneCallback<OAuthResponse, PrivateInfo>,
) => {
this.getUserPhoto(accessToken)
.then(photoURL => {
const authResponse = MicrosoftAuthProvider.transformAuthResponse(
accessToken,
params,
rawProfile,
photoURL,
);
done(undefined, authResponse, { refreshToken });
})
.catch(error => {
throw new Error(`Error processing auth response: ${error}`);
});
},
);
}
async start(
req: express.Request,
options: Record<string, string>,
): Promise<RedirectInfo> {
return await executeRedirectStrategy(req, this._strategy, options);
}
async handler(
req: express.Request,
): Promise<{ response: OAuthResponse; refreshToken: string }> {
const { response, privateInfo } = await executeFrameHandlerStrategy<
OAuthResponse,
PrivateInfo
>(req, this._strategy);
return {
response: await this.populateIdentity(response),
refreshToken: privateInfo.refreshToken,
};
}
async refresh(refreshToken: string, scope: string): Promise<OAuthResponse> {
const { accessToken, params } = await executeRefreshTokenStrategy(
this._strategy,
refreshToken,
scope,
);
const profile = await executeFetchUserProfileStrategy(
this._strategy,
accessToken,
params.id_token,
);
const photo = await this.getUserPhoto(accessToken);
if (photo) {
profile.picture = photo;
}
return this.populateIdentity({
providerInfo: {
accessToken,
idToken: params.id_token,
expiresInSeconds: params.expires_in,
scope: params.scope,
},
profile,
});
}
private getUserPhoto(accessToken: string): Promise<string> {
return new Promise(resolve => {
got
.get('https://graph.microsoft.com/v1.0/me/photos/48x48/$value', {
encoding: 'binary',
responseType: 'buffer',
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
.then(photoData => {
const photoURL = `data:image/jpeg;base64,${Buffer.from(
photoData.body,
).toString('base64')}`;
resolve(photoURL);
})
.catch(error => {
console.log(
`Could not retrieve user profile photo from Microsoft Graph API: ${error}`,
);
// User profile photo is optional, ignore errors and resolve undefined
resolve();
});
});
}
private async populateIdentity(
response: OAuthResponse,
): Promise<OAuthResponse> {
const { profile } = response;
if (!profile.email) {
throw new Error('Microsoft profile contained no email');
}
// Like Google implementation, setting this to local part of email for now
const id = profile.email.split('@')[0];
return { ...response, backstageIdentity: { id } };
}
}
export function createMicrosoftProvider(
config: AuthProviderConfig,
_: string,
envConfig: Config,
_logger: Logger,
tokenIssuer: TokenIssuer,
) {
const providerId = 'microsoft';
const clientId = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const tenantID = envConfig.getString('tenantId');
const callbackUrl = `${config.baseUrl}/${providerId}/handler/frame`;
const authorizationUrl = `https://login.microsoftonline.com/${tenantID}/oauth2/v2.0/authorize`;
const tokenUrl = `https://login.microsoftonline.com/${tenantID}/oauth2/v2.0/token`;
const provider = new MicrosoftAuthProvider({
clientId,
clientSecret,
callbackUrl,
authorizationUrl,
tokenUrl,
});
return OAuthProvider.fromConfig(config, provider, {
disableRefresh: false,
providerId,
tokenIssuer,
});
}
@@ -5,9 +5,9 @@ metadata:
title: Documentation Template
description: Create a new standalone documentation project
tags:
- Experimental
- TechDocs
- MkDocs
- experimental
- techdocs
- mkdocs
spec:
owner: spotify/techdocs-core
templater: cookiecutter
@@ -26,4 +26,4 @@ spec:
title: Description
type: string
description: Description of the component
@@ -26,7 +26,7 @@ import { useJobPolling } from './useJobPolling';
import { Job } from '../../types';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { Button } from '@backstage/core';
import { entityRoute } from '@backstage/plugin-catalog';
import { entityRouteDefault } from '@backstage/plugin-catalog';
import { generatePath } from 'react-router-dom';
type Props = {
@@ -72,7 +72,7 @@ export const JobStatusModal = ({
{entity && (
<DialogActions>
<Button
to={generatePath(entityRoute.path, {
to={generatePath(entityRouteDefault.path, {
kind: entity.kind,
optionalNamespaceAndName: [
entity.metadata.namespace,
@@ -15,6 +15,12 @@
*/
import { DirectoryPreparer } from './dir';
import { getVoidLogger } from '@backstage/backend-common';
import { checkoutGitRepository } from './helpers';
jest.mock('./helpers', () => ({
...jest.requireActual<{}>('./helpers'),
checkoutGitRepository: jest.fn(() => '/tmp/backstage-repo/org/name/branch/'),
}));
const logger = getVoidLogger();
@@ -59,4 +65,19 @@ describe('directory preparer', () => {
'/our-documentation/techdocs',
);
});
it('should merge managed-by-location and techdocs-ref when managed-by-location is a git repository', async () => {
const directoryPreparer = new DirectoryPreparer(logger);
const mockEntity = createMockEntity({
'backstage.io/managed-by-location':
'github:https://github.com/spotify/backstage/blob/master/catalog-info.yaml',
'backstage.io/techdocs-ref': 'dir:./docs',
});
expect(await directoryPreparer.prepare(mockEntity)).toEqual(
'/tmp/backstage-repo/org/name/branch/docs',
);
expect(checkoutGitRepository).toHaveBeenCalledTimes(1);
});
});
@@ -13,14 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs-extra';
import os from 'os';
import { PreparerBase } from './types';
import { Entity } from '@backstage/catalog-model';
import path from 'path';
import { parseReferenceAnnotation } from './helpers';
import { parseReferenceAnnotation, checkoutGitRepository } from './helpers';
import { InputError } from '@backstage/backend-common';
import { Clone } from 'nodegit';
import parseGitUrl from 'git-url-parse';
import { Logger } from 'winston';
@@ -31,41 +28,6 @@ export class DirectoryPreparer implements PreparerBase {
this.logger = logger;
}
private async cloneGithubRepo(entity: Entity) {
const { type, target } = parseReferenceAnnotation(
'backstage.io/managed-by-location',
entity,
);
if (type !== 'github') {
throw new InputError(`Wrong target type: ${type}, should be 'github'`);
}
const parsedGitLocation = parseGitUrl(target);
const repositoryTmpPath = path.join(
// fs.realpathSync fixes a problem with macOS returning a path that is a symlink
fs.realpathSync(os.tmpdir()),
'backstage-repo',
parsedGitLocation.source,
parsedGitLocation.owner,
parsedGitLocation.name,
parsedGitLocation.ref,
);
if (fs.existsSync(repositoryTmpPath)) {
return repositoryTmpPath;
}
const repositoryCheckoutUrl = parsedGitLocation.toString('https');
this.logger.debug(
`[TechDocs] Checking out repository ${repositoryCheckoutUrl} to ${repositoryTmpPath}`,
);
fs.mkdirSync(repositoryTmpPath, { recursive: true });
await Clone.clone(repositoryCheckoutUrl, repositoryTmpPath, {});
return repositoryTmpPath;
}
private async resolveManagedByLocationToDir(entity: Entity) {
const { type, target } = parseReferenceAnnotation(
'backstage.io/managed-by-location',
@@ -78,7 +40,7 @@ export class DirectoryPreparer implements PreparerBase {
switch (type) {
case 'github': {
const parsedGitLocation = parseGitUrl(target);
const repoLocation = await this.cloneGithubRepo(entity);
const repoLocation = await checkoutGitRepository(target);
return path.dirname(
path.join(repoLocation, parsedGitLocation.filepath),
@@ -0,0 +1,56 @@
/*
* Copyright 2020 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 { getVoidLogger } from '@backstage/backend-common';
import { GithubPreparer } from './github';
import { checkoutGitRepository } from './helpers';
jest.mock('./helpers', () => ({
...jest.requireActual<{}>('./helpers'),
checkoutGitRepository: jest.fn(() => '/tmp/backstage-repo/org/name/branch'),
}));
const createMockEntity = (annotations = {}) => {
return {
apiVersion: 'version',
kind: 'TestKind',
metadata: {
name: 'test-component-name',
annotations: {
...annotations,
},
},
};
};
const logger = getVoidLogger();
describe('github preparer', () => {
it('should prepare temp docs path from github repo', async () => {
const preparer = new GithubPreparer(logger);
const mockEntity = createMockEntity({
'backstage.io/techdocs-ref':
'github:https://github.com/spotify/backstage/blob/master/plugins/techdocs-backend/examples/documented-component',
});
const tempDocsPath = await preparer.prepare(mockEntity);
expect(checkoutGitRepository).toHaveBeenCalledTimes(1);
expect(tempDocsPath).toEqual(
'/tmp/backstage-repo/org/name/branch/plugins/techdocs-backend/examples/documented-component',
);
});
});
@@ -13,15 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs-extra';
import path from 'path';
import os from 'os';
import { Entity } from '@backstage/catalog-model';
import { InputError } from '@backstage/backend-common';
import { PreparerBase } from './types';
import parseGitUrl from 'git-url-parse';
import { Clone } from 'nodegit';
import { parseReferenceAnnotation } from './helpers';
import { parseReferenceAnnotation, checkoutGitRepository } from './helpers';
import { Logger } from 'winston';
export class GithubPreparer implements PreparerBase {
@@ -41,32 +38,14 @@ export class GithubPreparer implements PreparerBase {
throw new InputError(`Wrong target type: ${type}, should be 'github'`);
}
const parsedGitLocation = parseGitUrl(target);
const repositoryTmpPath = path.join(
// fs.realpathSync fixes a problem with macOS returning a path that is a symlink
fs.realpathSync(os.tmpdir()),
'backstage-repo',
parsedGitLocation.source,
parsedGitLocation.owner,
parsedGitLocation.name,
parsedGitLocation.ref,
);
try {
const repoPath = await checkoutGitRepository(target);
if (fs.existsSync(repositoryTmpPath)) {
this.logger.debug(
`[TechDocs] Found repository already checked out at ${repositoryTmpPath}`,
);
return path.join(repositoryTmpPath, parsedGitLocation.filepath);
const parsedGitLocation = parseGitUrl(target);
return path.join(repoPath, parsedGitLocation.filepath);
} catch (error) {
this.logger.debug(`Repo checkout failed with error ${error.message}`);
throw error;
}
const repositoryCheckoutUrl = parsedGitLocation.toString('https');
this.logger.debug(
`[TechDocs] Checking out repository ${repositoryCheckoutUrl} to ${repositoryTmpPath}`,
);
fs.mkdirSync(repositoryTmpPath, { recursive: true });
await Clone.clone(repositoryCheckoutUrl, repositoryTmpPath, {});
return path.join(repositoryTmpPath, parsedGitLocation.filepath);
}
}
@@ -16,6 +16,11 @@
import { Entity } from '@backstage/catalog-model';
import { InputError } from '@backstage/backend-common';
import { RemoteProtocol } from './types';
import parseGitUrl from 'git-url-parse';
import { Clone, Repository } from 'nodegit';
import fs from 'fs-extra';
import os from 'os';
import path from 'path';
export type ParsedLocationAnnotation = {
type: RemoteProtocol;
@@ -52,3 +57,39 @@ export const parseReferenceAnnotation = (
target,
};
};
export const clearGithubRepositoryCache = () => {
fs.removeSync(path.join(fs.realpathSync(os.tmpdir()), 'backstage-repo'));
};
export const checkoutGitRepository = async (
repoUrl: string,
): Promise<string> => {
const parsedGitLocation = parseGitUrl(repoUrl);
const repositoryTmpPath = path.join(
// fs.realpathSync fixes a problem with macOS returning a path that is a symlink
fs.realpathSync(os.tmpdir()),
'backstage-repo',
parsedGitLocation.source,
parsedGitLocation.owner,
parsedGitLocation.name,
parsedGitLocation.ref,
);
if (fs.existsSync(repositoryTmpPath)) {
const repository = await Repository.open(repositoryTmpPath);
await repository.mergeBranches(
parsedGitLocation.ref,
`origin/${parsedGitLocation.ref}`,
);
return repositoryTmpPath;
}
const repositoryCheckoutUrl = parsedGitLocation.toString('https');
fs.mkdirSync(repositoryTmpPath, { recursive: true });
await Clone.clone(repositoryCheckoutUrl, repositoryTmpPath, {});
return repositoryTmpPath;
};
+164 -113
View File
@@ -1415,7 +1415,7 @@
pirates "^4.0.0"
source-map-support "^0.5.16"
"@babel/runtime-corejs2@^7.10.4", "@babel/runtime-corejs2@^7.8.7":
"@babel/runtime-corejs2@^7.10.4":
version "7.11.2"
resolved "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.11.2.tgz#700a03945ebad0d31ba6690fc8a6bcc9040faa47"
integrity sha512-AC/ciV28adSSpEkBglONBWq4/Lvm6GAZuxIoyVtsnUpZMl0bxLtoChEnYAkP+47KyOCayZanojtflUEUJtR/6Q==
@@ -1423,6 +1423,14 @@
core-js "^2.6.5"
regenerator-runtime "^0.13.4"
"@babel/runtime-corejs2@^7.8.7":
version "7.10.3"
resolved "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.10.3.tgz#81bc99a96bfcb6db3f0dcf73fdc577cc554d341b"
integrity sha512-enKvnR/kKFbZFgXYo165wtSA5OeiTlgsnU4jV3vpKRhfWUJjLS6dfVcjIPeRcgJbgEgdgu0I+UyBWqu6c0GumQ==
dependencies:
core-js "^2.6.5"
regenerator-runtime "^0.13.4"
"@babel/runtime-corejs3@^7.10.2":
version "7.10.3"
resolved "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.10.3.tgz#931ed6941d3954924a7aa967ee440e60c507b91a"
@@ -2866,9 +2874,9 @@
write-file-atomic "^2.3.0"
"@material-icons/font@^1.0.2":
version "1.0.4"
resolved "https://registry.npmjs.org/@material-icons/font/-/font-1.0.4.tgz#4bfc59dcaba90f6d71e4aeffd1ebb2ea6119c844"
integrity sha512-tt80HS8dDtx/jj6fXWi6CycCZWC1AY3YOXjHwxShwe/jrW/fRVdw0C9JOGo0ij5/7y+uG0Rp0c4plu7J4Cq7EA==
version "1.0.3"
resolved "https://registry.npmjs.org/@material-icons/font/-/font-1.0.3.tgz#f722e5a69a03f20ef47d015cb69420bebeeaabe5"
integrity sha512-aIRd0Z9b/HJ/O24KOaP7dNsXypMnXhpWrpLVYvQB/JesVqXYSbioYND200sR+C14a0LSCp+qWnWCnSXmN1hWGw==
"@material-ui/core@^4.9.1":
version "4.9.7"
@@ -3279,9 +3287,9 @@
react-lifecycles-compat "^3.0.4"
"@rjsf/core@^2.1.0":
version "2.3.0"
resolved "https://registry.npmjs.org/@rjsf/core/-/core-2.3.0.tgz#334c73d2262ef1a8cda477e238067af7336c5599"
integrity sha512-OZKYHt9tjKhzOH4CvsPiCwepuIacqI++cNmnL2fsxh1IF+uEWGlo3NLDWhhSaBbOv9jps6a5YQcLbLtjNuSwug==
version "2.2.2"
resolved "https://registry.npmjs.org/@rjsf/core/-/core-2.2.2.tgz#1ebb6fe47448998f3b54e2dea8d58de8a46014dc"
integrity sha512-4d6DHIiTJEkUq5vyl4LIxLGIYYKKnHcprf94oVchUtGQvRFjNUDFxeFQoyr90oaxcBMs2WDDcCgjcFaKVyfErg==
dependencies:
"@babel/runtime-corejs2" "^7.8.7"
"@types/json-schema" "^7.0.4"
@@ -3416,6 +3424,11 @@
resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-2.1.1.tgz#ceff6a28a5b4867c2dd4a1ba513de278ccbe8bb1"
integrity sha512-/aPsuoj/1Dw/kzhkgz+ES6TxG0zfTMGLwuK2ZG00k/iJzYHTLCE8mVU8EPqEOp/lmxPoq1C1C9RYToRKb2KEfg==
"@sindresorhus/is@^3.0.0":
version "3.1.1"
resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-3.1.1.tgz#6e39e4222add8b362da35720c9dd5d53345d5851"
integrity sha512-tLnujxFtfH7F+i5ghUfgGlJsvyCKvUnSMFMlWybFdX9/DdX8svb4Zwx1gV0gkkVCHXtmPSetoAR3QlKfOld6Tw==
"@sinonjs/commons@^1.7.0":
version "1.7.1"
resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.7.1.tgz#da5fd19a5f71177a53778073978873964f49acf1"
@@ -4299,7 +4312,7 @@
dependencies:
defer-to-connect "^1.0.1"
"@szmarczak/http-timer@^4.0.0":
"@szmarczak/http-timer@^4.0.0", "@szmarczak/http-timer@^4.0.5":
version "4.0.5"
resolved "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.5.tgz#bfbd50211e9dfa51ba07da58a14cdfd333205152"
integrity sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ==
@@ -5002,6 +5015,13 @@
"@types/passport" "*"
"@types/passport-oauth2" "*"
"@types/passport-microsoft@^0.0.0":
version "0.0.0"
resolved "https://registry.npmjs.org/@types/passport-microsoft/-/passport-microsoft-0.0.0.tgz#ba71bccdd793711239d6b02e8d5953c21abc1c8d"
integrity sha512-jfkltRosn+P/+RoFMTl+mCyBTgPTFhjDEF832j7fmlYpuf+5yuzPLz7Rm5XMKN/Gqpro6myCyGPTuCc4yBQ2jQ==
dependencies:
"@types/passport-oauth2" "*"
"@types/passport-oauth2@*":
version "1.4.9"
resolved "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.9.tgz#134007c4b505a82548c9cb19094c5baeb2205c92"
@@ -5176,7 +5196,7 @@
dependencies:
"@types/node" "*"
"@types/responselike@*":
"@types/responselike@*", "@types/responselike@^1.0.0":
version "1.0.0"
resolved "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29"
integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==
@@ -5845,7 +5865,17 @@ ajv@^5.0.0:
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.3.0"
ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.1, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.5.5, ajv@^6.7.0:
ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.5.5, ajv@^6.7.0:
version "6.12.2"
resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd"
integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==
dependencies:
fast-deep-equal "^3.1.1"
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ajv@^6.10.1:
version "6.12.3"
resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706"
integrity sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==
@@ -7420,6 +7450,11 @@ cacheable-lookup@^2.0.0:
"@types/keyv" "^3.1.1"
keyv "^4.0.0"
cacheable-lookup@^5.0.3:
version "5.0.3"
resolved "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.3.tgz#049fdc59dffdd4fc285e8f4f82936591bd59fec3"
integrity sha512-W+JBqF9SWe18A72XFzN/V/CULFzPm7sBXzzR6ekkE+3tLG72wFZrBiBZhrZuDoYexop4PHJVdFAKb/Nj9+tm9w==
cacheable-request@^2.1.1:
version "2.1.4"
resolved "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz#0d808801b6342ad33c91df9d0b44dc09b91e5c3d"
@@ -8466,11 +8501,6 @@ core-js-pure@^3.0.0, core-js-pure@^3.0.1:
resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.4.tgz#4bf1ba866e25814f149d4e9aaa08c36173506e3a"
integrity sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw==
core-js@^1.0.0:
version "1.2.7"
resolved "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=
core-js@^2.4.0, core-js@^2.5.7, core-js@^2.6.11, core-js@^2.6.5:
version "2.6.11"
resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
@@ -8554,15 +8584,6 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
create-react-class@^15.5.1:
version "15.6.3"
resolved "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.3.tgz#2d73237fb3f970ae6ebe011a9e66f46dbca80036"
integrity sha512-M+/3Q6E6DLO6Yx3OwrWjwHBnvfXXYA7W+dFjt/ZDBemHO1DDZhsalX/NUtnTYclN6GfnBDRh4qRHjcDHmlJBJg==
dependencies:
fbjs "^0.8.9"
loose-envify "^1.3.1"
object-assign "^4.1.1"
create-react-context@0.3.0, create-react-context@^0.3.0:
version "0.3.0"
resolved "https://registry.npmjs.org/create-react-context/-/create-react-context-0.3.0.tgz#546dede9dc422def0d3fc2fe03afe0bc0f4f7d8c"
@@ -9165,6 +9186,13 @@ decompress-response@^5.0.0:
dependencies:
mimic-response "^2.0.0"
decompress-response@^6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==
dependencies:
mimic-response "^3.1.0"
decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1:
version "4.1.1"
resolved "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz#718cbd3fcb16209716e70a26b84e7ba4592e5af1"
@@ -10915,19 +10943,6 @@ fb-watchman@^2.0.0:
dependencies:
bser "2.1.1"
fbjs@^0.8.9:
version "0.8.17"
resolved "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd"
integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=
dependencies:
core-js "^1.0.0"
isomorphic-fetch "^2.1.1"
loose-envify "^1.0.0"
object-assign "^4.1.0"
promise "^7.1.1"
setimmediate "^1.0.5"
ua-parser-js "^0.7.18"
fd-slicer@~1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
@@ -11964,6 +11979,23 @@ got@^10.7.0:
to-readable-stream "^2.0.0"
type-fest "^0.10.0"
got@^11.5.2:
version "11.5.2"
resolved "https://registry.npmjs.org/got/-/got-11.5.2.tgz#772e3f3a06d9c7589c7c94dc3c83cdb31ddbf742"
integrity sha512-yUhpEDLeuGiGJjRSzEq3kvt4zJtAcjKmhIiwNp/eUs75tRlXfWcHo5tcBaMQtnjHWC7nQYT5HkY/l0QOQTkVww==
dependencies:
"@sindresorhus/is" "^3.0.0"
"@szmarczak/http-timer" "^4.0.5"
"@types/cacheable-request" "^6.0.1"
"@types/responselike" "^1.0.0"
cacheable-lookup "^5.0.3"
cacheable-request "^7.0.1"
decompress-response "^6.0.0"
http2-wrapper "^1.0.0-beta.5.0"
lowercase-keys "^2.0.0"
p-cancelable "^2.0.0"
responselike "^2.0.0"
got@^7.0.0:
version "7.1.0"
resolved "https://registry.npmjs.org/got/-/got-7.1.0.tgz#05450fd84094e6bbea56f451a43a9c289166385a"
@@ -12386,6 +12418,11 @@ highlight.js@~9.13.0:
resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-9.13.1.tgz#054586d53a6863311168488a0f58d6c505ce641e"
integrity sha512-Sc28JNQNDzaH6PORtRLMvif9RSn1mYuOoX3omVjnb0+HbpPygU2ALBI0R/wsiqCb4/fcp07Gdo8g+fhtFrQl6A==
highlight.js@~9.15.0, highlight.js@~9.15.1:
version "9.15.10"
resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-9.15.10.tgz#7b18ed75c90348c045eef9ed08ca1319a2219ad2"
integrity sha512-RoV7OkQm0T3os3Dd2VHLNMoaoDVx77Wygln3n9l5YV172XonWG6rgQD3XnF/BuFFZw9A0TJgmMSO8FEWQgvcXw==
history@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/history/-/history-5.0.0.tgz#0cabbb6c4bbf835addb874f8259f6d25101efd08"
@@ -12660,6 +12697,14 @@ http-signature@~1.2.0:
jsprim "^1.2.2"
sshpk "^1.7.0"
http2-wrapper@^1.0.0-beta.5.0:
version "1.0.0-beta.5.2"
resolved "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.0-beta.5.2.tgz#8b923deb90144aea65cf834b016a340fc98556f3"
integrity sha512-xYz9goEyBnC8XwXDTuC/MZ6t+MrKVQZOk4s7+PaDkwIsQd8IwqvM+0M6bA/2lvG8GHXcPdf+MejTUeO2LCPCeQ==
dependencies:
quick-lru "^5.1.1"
resolve-alpn "^1.0.0"
https-browserify@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
@@ -13089,7 +13134,7 @@ into-stream@^3.1.0:
from2 "^2.1.1"
p-is-promise "^1.1.0"
invariant@^2.0.0, invariant@^2.2.2, invariant@^2.2.3, invariant@^2.2.4:
invariant@^2.2.2, invariant@^2.2.3, invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
@@ -13583,7 +13628,7 @@ is-ssh@^1.3.0:
dependencies:
protocols "^1.1.0"
is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0:
is-stream@^1.0.0, is-stream@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
@@ -13724,14 +13769,6 @@ isobject@^4.0.0:
resolved "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0"
integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==
isomorphic-fetch@^2.1.1:
version "2.2.1"
resolved "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
integrity sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=
dependencies:
node-fetch "^1.0.1"
whatwg-fetch ">=0.10.0"
isomorphic-form-data@~2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/isomorphic-form-data/-/isomorphic-form-data-2.0.0.tgz#9f6adf1c4c61ae3aefd8f110ab60fb9b143d6cec"
@@ -14283,7 +14320,15 @@ js-tokens@^3.0.2:
resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls=
js-yaml@^3.13.1, js-yaml@^3.14.0, js-yaml@^3.8.1, js-yaml@^3.8.3:
js-yaml@^3.13.1, js-yaml@^3.8.3:
version "3.13.1"
resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==
dependencies:
argparse "^1.0.7"
esprima "^4.0.0"
js-yaml@^3.14.0, js-yaml@^3.8.1:
version "3.14.0"
resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482"
integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==
@@ -15062,7 +15107,7 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
lodash-es@^4.17.11, lodash-es@^4.2.1:
lodash-es@^4.17.11:
version "4.17.15"
resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
@@ -15306,7 +15351,7 @@ longest@^1.0.0:
resolved "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
integrity sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@@ -15343,6 +15388,14 @@ lowercase-keys@^2.0.0:
resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
lowlight@1.12.1:
version "1.12.1"
resolved "https://registry.npmjs.org/lowlight/-/lowlight-1.12.1.tgz#014acf8dd73a370e02ff1cc61debcde3bb1681eb"
integrity sha512-OqaVxMGIESnawn+TU/QMV5BJLbUghUfjDWPAtFqDYDmDtr4FnB+op8xM+pR7nKlauHNUHXGt0VgWatFB8voS5w==
dependencies:
fault "^1.0.2"
highlight.js "~9.15.0"
lowlight@^1.14.0:
version "1.14.0"
resolved "https://registry.npmjs.org/lowlight/-/lowlight-1.14.0.tgz#83ebc143fec0f9e6c0d3deffe01be129ce56b108"
@@ -15848,6 +15901,11 @@ mimic-response@^2.0.0, mimic-response@^2.1.0:
resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43"
integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==
mimic-response@^3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
min-document@^2.19.0:
version "2.19.0"
resolved "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685"
@@ -16257,14 +16315,6 @@ node-fetch@2.6.0, node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@^2.3.0, node-
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
node-fetch@^1.0.1:
version "1.7.3"
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==
dependencies:
encoding "^0.1.11"
is-stream "^1.0.1"
node-forge@0.9.0:
version "0.9.0"
resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579"
@@ -17398,6 +17448,14 @@ passport-google-oauth20@^2.0.0:
dependencies:
passport-oauth2 "1.x.x"
passport-microsoft@^0.1.0:
version "0.1.0"
resolved "https://registry.npmjs.org/passport-microsoft/-/passport-microsoft-0.1.0.tgz#dc72c1a38b294d74f4dc55fe93f52e25cb9aa5b4"
integrity sha512-0giBDgE1fnR5X84zJZkQ11hnKVrzEgViwRO6RGsormK9zTxFQmN/UHMTDbIpvhk989VqALewB6Pk1R5vNr3GHw==
dependencies:
passport-oauth2 "1.2.0"
pkginfo "0.2.x"
passport-oauth1@1.x.x:
version "1.1.0"
resolved "https://registry.npmjs.org/passport-oauth1/-/passport-oauth1-1.1.0.tgz#a7de988a211f9cf4687377130ea74df32730c918"
@@ -17407,6 +17465,15 @@ passport-oauth1@1.x.x:
passport-strategy "1.x.x"
utils-merge "1.x.x"
passport-oauth2@1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.2.0.tgz#49613a3eca85c7a1e65bf1019e2b6b80a10c8ac2"
integrity sha1-SWE6PsqFx6HmW/EBnitrgKEMisI=
dependencies:
oauth "0.9.x"
passport-strategy "1.x.x"
uid2 "0.0.x"
passport-oauth2@1.x.x, passport-oauth2@^1.4.0, passport-oauth2@^1.5.0:
version "1.5.0"
resolved "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.5.0.tgz#64babbb54ac46a4dcab35e7f266ed5294e3c4108"
@@ -18443,13 +18510,6 @@ promise.series@^0.2.0:
resolved "https://registry.npmjs.org/promise.series/-/promise.series-0.2.0.tgz#2cc7ebe959fc3a6619c04ab4dbdc9e452d864bbd"
integrity sha1-LMfr6Vn8OmYZwEq029yeRS2GS70=
promise@^7.1.1:
version "7.3.1"
resolved "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==
dependencies:
asap "~2.0.3"
promise@^8.0.3:
version "8.1.0"
resolved "https://registry.npmjs.org/promise/-/promise-8.1.0.tgz#697c25c3dfe7435dd79fcd58c38a135888eaf05e"
@@ -18691,6 +18751,11 @@ quick-lru@^4.0.1:
resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
quick-lru@^5.1.1:
version "5.1.1"
resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
raf-schd@^4.0.2:
version "4.0.2"
resolved "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0"
@@ -19063,7 +19128,7 @@ react-inspector@^4.0.0:
is-dom "^1.0.9"
prop-types "^15.6.1"
react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0:
react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0:
version "16.13.1"
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -19083,7 +19148,7 @@ react-lazylog@^4.5.2:
text-encoding-utf-8 "^1.0.1"
whatwg-fetch "^2.0.4"
react-lifecycles-compat@^3.0.4:
react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4:
version "3.0.4"
resolved "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
@@ -19132,17 +19197,18 @@ react-popper@^1.3.6:
typed-styles "^0.0.7"
warning "^4.0.2"
react-redux@=4.4.10:
version "4.4.10"
resolved "https://registry.npmjs.org/react-redux/-/react-redux-4.4.10.tgz#ad57bd1db00c2d0aa7db992b360ce63dd0b80ec5"
integrity sha512-tjL0Bmpkj75Td0k+lXlF8Fc8a9GuXFv/3ahUOCXExWs/jhsKiQeTffdH0j5byejCGCRL4tvGFYlrwBF1X/Aujg==
react-redux@^5.1.2:
version "5.1.2"
resolved "https://registry.npmjs.org/react-redux/-/react-redux-5.1.2.tgz#b19cf9e21d694422727bf798e934a916c4080f57"
integrity sha512-Ns1G0XXc8hDyH/OcBHOxNgQx9ayH3SPxBnFCOidGKSle8pKihysQw2rG/PmciUQRoclhVBO8HMhiRmGXnDja9Q==
dependencies:
create-react-class "^15.5.1"
"@babel/runtime" "^7.1.2"
hoist-non-react-statics "^3.3.0"
invariant "^2.0.0"
lodash "^4.17.11"
loose-envify "^1.4.0"
prop-types "^15.7.2"
invariant "^2.2.4"
loose-envify "^1.1.0"
prop-types "^15.6.1"
react-is "^16.6.0"
react-lifecycles-compat "^3.0.0"
react-redux@^7.1.1:
version "7.2.0"
@@ -19199,16 +19265,16 @@ react-string-replace@^0.4.1:
dependencies:
lodash "^4.17.4"
react-syntax-highlighter@=13.5.0:
version "13.5.0"
resolved "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-13.5.0.tgz#c0f3fd826a52b2a6ce5786d4ce60c3e0665b65c0"
integrity sha512-2nKo8spFxe9shcjbdUiqxkrf/IMDqKUZLx7JVIxEJ17P+fYFGL4CRsZZC66UPeQ2o/f29eKu31CrkKGCK1RHuA==
react-syntax-highlighter@=12.2.1:
version "12.2.1"
resolved "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-12.2.1.tgz#14d78352da1c1c3f93c6698b70ec7c706b83493e"
integrity sha512-CTsp0ZWijwKRYFg9xhkWD4DSpQqE4vb2NKVMdPAkomnILSmsNBHE0n5GuI5zB+PU3ySVvXvdt9jo+ViD9XibCA==
dependencies:
"@babel/runtime" "^7.3.1"
highlight.js "^10.1.1"
lowlight "^1.14.0"
prismjs "^1.21.0"
refractor "^3.1.0"
highlight.js "~9.15.1"
lowlight "1.12.1"
prismjs "^1.8.4"
refractor "^2.4.1"
react-syntax-highlighter@^11.0.2:
version "11.0.2"
@@ -19545,17 +19611,7 @@ redux-immutable@3.1.0:
dependencies:
immutable "^3.8.1"
redux@=3.7.2:
version "3.7.2"
resolved "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b"
integrity sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==
dependencies:
lodash "^4.2.1"
lodash-es "^4.2.1"
loose-envify "^1.1.0"
symbol-observable "^1.0.3"
redux@^4.0.4:
redux@^4.0.4, redux@^4.0.5:
version "4.0.5"
resolved "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
@@ -19843,6 +19899,11 @@ resize-observer-polyfill@^1.5.1:
resolved "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
resolve-alpn@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.0.0.tgz#745ad60b3d6aff4b4a48e01b8c0bdc70959e0e8c"
integrity sha512-rTuiIEqFmGxne4IovivKSDzld2lWW9QCjqv80SYjPgf+gS35eaCAjaP54CCwGAwBtnCsvNLYtqxe1Nw+i6JEmA==
resolve-cwd@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
@@ -20431,7 +20492,7 @@ set-value@^2.0.0, set-value@^2.0.1:
is-plain-object "^2.0.3"
split-string "^3.0.1"
setimmediate@^1.0.4, setimmediate@^1.0.5:
setimmediate@^1.0.4:
version "1.0.5"
resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
@@ -21588,9 +21649,9 @@ swagger-client@=3.10.12:
url "~0.11.0"
swagger-ui-react@^3.31.1:
version "3.32.4"
resolved "https://registry.npmjs.org/swagger-ui-react/-/swagger-ui-react-3.32.4.tgz#cc2cef0e2b9faff7c0dab111e9b4b9c87d0a01a2"
integrity sha512-AbdeBVIV6oM4pXKEsGY2Ijs1ySmdVYP4cHdZYu0kWghxKVqskocsfB+SsvRTh6mOxdZ2YNAEhc7RMdSqaWnGgw==
version "3.31.1"
resolved "https://registry.npmjs.org/swagger-ui-react/-/swagger-ui-react-3.31.1.tgz#1770cfa1dc390f79887d641ea35ee654f4bf7d80"
integrity sha512-bGpNzizMT/7N4LTdw6yx+t3rpsEoalrn0VIfhUzv9zXVrUTl+3AKPLHpLBr0r4NvzvaZLrYuSjkatnLfngJ4vg==
dependencies:
"@babel/runtime-corejs2" "^7.10.4"
"@braintree/sanitize-url" "^4.0.0"
@@ -21616,9 +21677,9 @@ swagger-ui-react@^3.31.1:
react-immutable-pure-component "^1.1.1"
react-inspector "^2.3.0"
react-motion "^0.5.2"
react-redux "=4.4.10"
react-syntax-highlighter "=13.5.0"
redux "=3.7.2"
react-redux "^5.1.2"
react-syntax-highlighter "=12.2.1"
redux "^4.0.5"
redux-immutable "3.1.0"
remarkable "^2.0.1"
reselect "^4.0.0"
@@ -21636,7 +21697,7 @@ swr@^0.3.0:
dependencies:
fast-deep-equal "2.0.1"
symbol-observable@^1.0.3, symbol-observable@^1.0.4, symbol-observable@^1.1.0, symbol-observable@^1.2.0:
symbol-observable@^1.0.4, symbol-observable@^1.1.0, symbol-observable@^1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
@@ -22442,11 +22503,6 @@ typescript@^3.9.3:
resolved "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa"
integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==
ua-parser-js@^0.7.18:
version "0.7.21"
resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz#853cf9ce93f642f67174273cc34565ae6f308777"
integrity sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ==
uc.micro@^1.0.1, uc.micro@^1.0.5:
version "1.0.6"
resolved "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
@@ -23227,11 +23283,6 @@ whatwg-fetch@3.0.0, whatwg-fetch@^3.0.0:
resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb"
integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==
whatwg-fetch@>=0.10.0:
version "3.4.0"
resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.0.tgz#e11de14f4878f773fbebcde8871b2c0699af8b30"
integrity sha512-rsum2ulz2iuZH08mJkT0Yi6JnKhwdw4oeyMjokgxd+mmqYSd9cPpOQf01TIWgjxG/U4+QR+AwKq6lSbXVxkyoQ==
whatwg-fetch@^2.0.0, whatwg-fetch@^2.0.4:
version "2.0.4"
resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f"