From 5ac1779f1a0261251172b428953da866cbd18e15 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Mon, 21 Dec 2020 13:21:48 +0200 Subject: [PATCH 001/209] Added a scaffolded new plugin --- .github/CODEOWNERS | 19 ++-- packages/app/package.json | 5 +- packages/app/src/plugins.ts | 1 + plugins/kafka/.eslintrc.js | 3 + plugins/kafka/README.md | 13 +++ plugins/kafka/dev/index.tsx | 21 ++++ plugins/kafka/package.json | 48 ++++++++ .../ExampleComponent.test.tsx | 45 ++++++++ .../ExampleComponent/ExampleComponent.tsx | 55 +++++++++ .../src/components/ExampleComponent/index.ts | 16 +++ .../ExampleFetchComponent.test.tsx | 40 +++++++ .../ExampleFetchComponent.tsx | 107 ++++++++++++++++++ .../components/ExampleFetchComponent/index.ts | 16 +++ plugins/kafka/src/index.ts | 16 +++ plugins/kafka/src/plugin.test.ts | 22 ++++ plugins/kafka/src/plugin.ts | 29 +++++ plugins/kafka/src/setupTests.ts | 17 +++ yarn.lock | 28 +++-- 18 files changed, 481 insertions(+), 20 deletions(-) create mode 100644 plugins/kafka/.eslintrc.js create mode 100644 plugins/kafka/README.md create mode 100644 plugins/kafka/dev/index.tsx create mode 100644 plugins/kafka/package.json create mode 100644 plugins/kafka/src/components/ExampleComponent/ExampleComponent.test.tsx create mode 100644 plugins/kafka/src/components/ExampleComponent/ExampleComponent.tsx create mode 100644 plugins/kafka/src/components/ExampleComponent/index.ts create mode 100644 plugins/kafka/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx create mode 100644 plugins/kafka/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx create mode 100644 plugins/kafka/src/components/ExampleFetchComponent/index.ts create mode 100644 plugins/kafka/src/index.ts create mode 100644 plugins/kafka/src/plugin.test.ts create mode 100644 plugins/kafka/src/plugin.ts create mode 100644 plugins/kafka/src/setupTests.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 495f1ff78d..ada20f6931 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,12 +4,13 @@ # The last matching pattern takes precedence. # https://help.github.com/articles/about-codeowners/ -* @backstage/maintainers -/docs/features/techdocs @backstage/techdocs-core -/plugins/cost-insights @backstage/silver-lining -/plugins/cloudbuild @trivago/ebarrios -/plugins/search @backstage/techdocs-core -/plugins/techdocs @backstage/techdocs-core -/plugins/techdocs-backend @backstage/techdocs-core -/packages/techdocs-common @backstage/techdocs-core -/.changeset/cost-insights-* @backstage/silver-lining +* @backstage/maintainers +/.changeset/cost-insights-* @backstage/silver-lining +/docs/features/techdocs @backstage/techdocs-core +/packages/techdocs-common @backstage/techdocs-core +/plugins/cloudbuild @trivago/ebarrios +/plugins/cost-insights @backstage/silver-lining +/plugins/kafka @fiverr/platform +/plugins/search @backstage/techdocs-core +/plugins/techdocs @backstage/techdocs-core +/plugins/techdocs-backend @backstage/techdocs-core diff --git a/packages/app/package.json b/packages/app/package.json index 63710a5c28..1488ee5505 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -18,17 +18,18 @@ "@backstage/plugin-github-actions": "^0.2.5", "@backstage/plugin-gitops-profiles": "^0.2.2", "@backstage/plugin-graphiql": "^0.2.2", - "@backstage/plugin-org": "^0.3.2", "@backstage/plugin-jenkins": "^0.3.4", + "@backstage/plugin-kafka": "^0.1.1", "@backstage/plugin-kubernetes": "^0.3.3", "@backstage/plugin-lighthouse": "^0.2.6", "@backstage/plugin-newrelic": "^0.2.2", + "@backstage/plugin-org": "^0.3.2", "@backstage/plugin-pagerduty": "0.2.3", "@backstage/plugin-register-component": "^0.2.5", "@backstage/plugin-rollbar": "^0.2.7", "@backstage/plugin-scaffolder": "^0.3.4", - "@backstage/plugin-sentry": "^0.3.1", "@backstage/plugin-search": "^0.2.4", + "@backstage/plugin-sentry": "^0.3.1", "@backstage/plugin-tech-radar": "^0.3.1", "@backstage/plugin-techdocs": "^0.5.0", "@backstage/plugin-user-settings": "^0.2.3", diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index 6a4924cb0e..f07be07ec1 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -43,3 +43,4 @@ export { plugin as PagerDuty } from '@backstage/plugin-pagerduty'; export { plugin as Buildkite } from '@roadiehq/backstage-plugin-buildkite'; export { plugin as Search } from '@backstage/plugin-search'; export { plugin as Org } from '@backstage/plugin-org'; +export { plugin as Kafka } from '@backstage/plugin-kafka'; diff --git a/plugins/kafka/.eslintrc.js b/plugins/kafka/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/kafka/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/kafka/README.md b/plugins/kafka/README.md new file mode 100644 index 0000000000..35a62758f3 --- /dev/null +++ b/plugins/kafka/README.md @@ -0,0 +1,13 @@ +# kafka + +Welcome to the kafka plugin! + +_This plugin was created through the Backstage CLI_ + +## Getting started + +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/kafka](http://localhost:3000/kafka). + +You can also serve the plugin in isolation by running `yarn start` in the plugin directory. +This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. +It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. diff --git a/plugins/kafka/dev/index.tsx b/plugins/kafka/dev/index.tsx new file mode 100644 index 0000000000..8890c7e3c4 --- /dev/null +++ b/plugins/kafka/dev/index.tsx @@ -0,0 +1,21 @@ +/* + * 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 { createDevApp } from '@backstage/dev-utils'; +import { plugin } from '../src/plugin'; + +createDevApp() + .registerPlugin(plugin) + .render(); diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json new file mode 100644 index 0000000000..f5dc01ec45 --- /dev/null +++ b/plugins/kafka/package.json @@ -0,0 +1,48 @@ +{ + "name": "@backstage/plugin-kafka", + "version": "0.1.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/core": "^0.4.1", + "@backstage/theme": "^0.2.2", + "@material-ui/core": "^4.11.0", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "kafkajs": "^1.15.0", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-use": "^15.3.3" + }, + "devDependencies": { + "@backstage/cli": "^0.4.2", + "@backstage/dev-utils": "^0.1.6", + "@backstage/test-utils": "^0.1.5", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", + "@types/jest": "^26.0.7", + "@types/node": "^12.0.0", + "cross-fetch": "^3.0.6", + "msw": "^0.21.2" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/kafka/src/components/ExampleComponent/ExampleComponent.test.tsx b/plugins/kafka/src/components/ExampleComponent/ExampleComponent.test.tsx new file mode 100644 index 0000000000..680714eeda --- /dev/null +++ b/plugins/kafka/src/components/ExampleComponent/ExampleComponent.test.tsx @@ -0,0 +1,45 @@ +/* + * 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 React from 'react'; +import { render } from '@testing-library/react'; +import ExampleComponent from './ExampleComponent'; +import { ThemeProvider } from '@material-ui/core'; +import { lightTheme } from '@backstage/theme'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { msw } from '@backstage/test-utils'; + +describe('ExampleComponent', () => { + const server = setupServer(); + // Enable sane handlers for network requests + msw.setupDefaultHandlers(server); + + // setup mock response + beforeEach(() => { + server.use( + rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))), + ); + }); + + it('should render', () => { + const rendered = render( + + + , + ); + expect(rendered.getByText('Welcome to kafka!')).toBeInTheDocument(); + }); +}); diff --git a/plugins/kafka/src/components/ExampleComponent/ExampleComponent.tsx b/plugins/kafka/src/components/ExampleComponent/ExampleComponent.tsx new file mode 100644 index 0000000000..cfa36acd7b --- /dev/null +++ b/plugins/kafka/src/components/ExampleComponent/ExampleComponent.tsx @@ -0,0 +1,55 @@ +/* + * 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 React from 'react'; +import { Typography, Grid } from '@material-ui/core'; +import { + InfoCard, + Header, + Page, + Content, + ContentHeader, + HeaderLabel, + SupportButton, +} from '@backstage/core'; +import ExampleFetchComponent from '../ExampleFetchComponent'; + +const ExampleComponent = () => ( + +
+ + +
+ + + A description of your plugin goes here. + + + + + + All content should be wrapped in a card like this. + + + + + + + + +
+); + +export default ExampleComponent; diff --git a/plugins/kafka/src/components/ExampleComponent/index.ts b/plugins/kafka/src/components/ExampleComponent/index.ts new file mode 100644 index 0000000000..2c6f338ec7 --- /dev/null +++ b/plugins/kafka/src/components/ExampleComponent/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { default } from './ExampleComponent'; diff --git a/plugins/kafka/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx b/plugins/kafka/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx new file mode 100644 index 0000000000..0a73ed0fc1 --- /dev/null +++ b/plugins/kafka/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx @@ -0,0 +1,40 @@ +/* + * 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 React from 'react'; +import { render } from '@testing-library/react'; +import ExampleFetchComponent from './ExampleFetchComponent'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { msw } from '@backstage/test-utils'; + +describe('ExampleFetchComponent', () => { + const server = setupServer(); + // Enable sane handlers for network requests + msw.setupDefaultHandlers(server); + + // setup mock response + beforeEach(() => { + server.use( + rest.get('https://randomuser.me/*', (_, res, ctx) => + res(ctx.status(200), ctx.delay(2000), ctx.json({})), + ), + ); + }); + it('should render', async () => { + const rendered = render(); + expect(await rendered.findByTestId('progress')).toBeInTheDocument(); + }); +}); diff --git a/plugins/kafka/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx b/plugins/kafka/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx new file mode 100644 index 0000000000..8a4aef11b8 --- /dev/null +++ b/plugins/kafka/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx @@ -0,0 +1,107 @@ +/* + * 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 React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import { Table, TableColumn, Progress } from '@backstage/core'; +import Alert from '@material-ui/lab/Alert'; +import { useAsync } from 'react-use'; + +const useStyles = makeStyles({ + avatar: { + height: 32, + width: 32, + borderRadius: '50%', + }, +}); + +type User = { + gender: string; // "male" + name: { + title: string; // "Mr", + first: string; // "Duane", + last: string; // "Reed" + }; + location: object; // {street: {number: 5060, name: "Hickory Creek Dr"}, city: "Albany", state: "New South Wales",…} + email: string; // "duane.reed@example.com" + login: object; // {uuid: "4b785022-9a23-4ab9-8a23-cb3fb43969a9", username: "blackdog796", password: "patch",…} + dob: object; // {date: "1983-06-22T12:30:23.016Z", age: 37} + registered: object; // {date: "2006-06-13T18:48:28.037Z", age: 14} + phone: string; // "07-2154-5651" + cell: string; // "0405-592-879" + id: { + name: string; // "TFN", + value: string; // "796260432" + }; + picture: { medium: string }; // {medium: "https://randomuser.me/api/portraits/men/95.jpg",…} + nat: string; // "AU" +}; + +type DenseTableProps = { + users: User[]; +}; + +export const DenseTable = ({ users }: DenseTableProps) => { + const classes = useStyles(); + + const columns: TableColumn[] = [ + { title: 'Avatar', field: 'avatar' }, + { title: 'Name', field: 'name' }, + { title: 'Email', field: 'email' }, + { title: 'Nationality', field: 'nationality' }, + ]; + + const data = users.map(user => { + return { + avatar: ( + {user.name.first} + ), + name: `${user.name.first} ${user.name.last}`, + email: user.email, + nationality: user.nat, + }; + }); + + return ( + + ); +}; + +const ExampleFetchComponent = () => { + const { value, loading, error } = useAsync(async (): Promise => { + const response = await fetch('https://randomuser.me/api/?results=20'); + const data = await response.json(); + return data.results; + }, []); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + + return ; +}; + +export default ExampleFetchComponent; diff --git a/plugins/kafka/src/components/ExampleFetchComponent/index.ts b/plugins/kafka/src/components/ExampleFetchComponent/index.ts new file mode 100644 index 0000000000..e0d021564d --- /dev/null +++ b/plugins/kafka/src/components/ExampleFetchComponent/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { default } from './ExampleFetchComponent'; diff --git a/plugins/kafka/src/index.ts b/plugins/kafka/src/index.ts new file mode 100644 index 0000000000..224e293890 --- /dev/null +++ b/plugins/kafka/src/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { plugin } from './plugin'; diff --git a/plugins/kafka/src/plugin.test.ts b/plugins/kafka/src/plugin.test.ts new file mode 100644 index 0000000000..8d13aa1727 --- /dev/null +++ b/plugins/kafka/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * 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 { plugin } from './plugin'; + +describe('kafka', () => { + it('should export plugin', () => { + expect(plugin).toBeDefined(); + }); +}); diff --git a/plugins/kafka/src/plugin.ts b/plugins/kafka/src/plugin.ts new file mode 100644 index 0000000000..f0bc22481e --- /dev/null +++ b/plugins/kafka/src/plugin.ts @@ -0,0 +1,29 @@ +/* + * 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 { createPlugin, createRouteRef } from '@backstage/core'; +import ExampleComponent from './components/ExampleComponent'; + +export const rootRouteRef = createRouteRef({ + path: '/kafka', + title: 'kafka', +}); + +export const plugin = createPlugin({ + id: 'kafka', + register({ router }) { + router.addRoute(rootRouteRef, ExampleComponent); + }, +}); diff --git a/plugins/kafka/src/setupTests.ts b/plugins/kafka/src/setupTests.ts new file mode 100644 index 0000000000..43b8421558 --- /dev/null +++ b/plugins/kafka/src/setupTests.ts @@ -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. + */ +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/yarn.lock b/yarn.lock index 0923d3ed70..bb8a845850 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1644,9 +1644,11 @@ to-fast-properties "^2.0.0" "@backstage/catalog-model@^0.2.0": - version "0.6.0" + version "0.2.0" + resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.2.0.tgz#e3fe2a4ddeb6a9b6ec480c80cb2b9c39cb245576" + integrity sha512-Y1ocdRpBlxK/VrJQjHlQd0bgADECd1B2NRjwd8ss46ibT5hwLvMOfD80+Fa7oPLu0ktJrH4lq0pNIIJIml48zA== dependencies: - "@backstage/config" "^0.1.2" + "@backstage/config" "^0.1.1" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" json-schema "^0.2.5" @@ -1655,9 +1657,11 @@ yup "^0.29.3" "@backstage/catalog-model@^0.3.0": - version "0.6.0" + version "0.3.1" + resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.3.1.tgz#45d08e2f333c9c566b2bf2629fd707fe989bb404" + integrity sha512-9XhV7c4rmVW+Yzj2PiwTQ7DsegWGB3C4ELsDRExuEVZONdqNcC02cyJtrt3fT5F31ZS3tHkB9bMUymFOBLqUSA== dependencies: - "@backstage/config" "^0.1.2" + "@backstage/config" "^0.1.1" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" json-schema "^0.2.5" @@ -1666,16 +1670,17 @@ yup "^0.29.3" "@backstage/core@^0.3.0": - version "0.4.1" + version "0.3.2" + resolved "https://registry.npmjs.org/@backstage/core/-/core-0.3.2.tgz#a8209126d5076cf4a8b9bd632fe4e5e2edb62916" + integrity sha512-i5d+Wh8js4qEWoAsPY5L7HVSWpumr1OhfF2dUCGYdyW6AMqVJPca6+n6zp1Rg2CO+J9norp44XAVVCbyhtUpig== dependencies: - "@backstage/config" "^0.1.2" - "@backstage/core-api" "^0.2.6" - "@backstage/theme" "^0.2.2" + "@backstage/config" "^0.1.1" + "@backstage/core-api" "^0.2.1" + "@backstage/theme" "^0.2.1" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" "@material-ui/lab" "4.0.0-alpha.45" "@types/dagre" "^0.7.44" - "@types/prop-types" "^15.7.3" "@types/react" "^16.9" "@types/react-sparklines" "^1.7.0" classnames "^2.2.6" @@ -16181,6 +16186,11 @@ jwt-decode@*, jwt-decode@^3.1.0: resolved "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59" integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A== +kafkajs@^1.15.0: + version "1.15.0" + resolved "https://registry.npmjs.org/kafkajs/-/kafkajs-1.15.0.tgz#a5ada0d933edca2149177393562be6fb0875ec3a" + integrity sha512-yjPyEnQCkPxAuQLIJnY5dI+xnmmgXmhuOQ1GVxClG5KTOV/rJcW1qA3UfvyEJKTp/RTSqQnUR3HJsKFvHyTpNg== + keyv@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" From b06f6767988416879d64db04a1ed344720e0cbc5 Mon Sep 17 00:00:00 2001 From: blam Date: Sat, 2 Jan 2021 14:50:45 +0100 Subject: [PATCH 002/209] feat: reworking some of the preparers to support the integration config and add deprecation for old scaffolder config --- packages/integration/src/gitlab/config.ts | 2 +- .../scaffolder/stages/prepare/azure.test.ts | 57 +++++++++++++------ .../src/scaffolder/stages/prepare/azure.ts | 53 +++++++++++++---- .../scaffolder/stages/prepare/github.test.ts | 52 ++++++++++++++--- .../src/scaffolder/stages/prepare/github.ts | 50 +++++++++++++--- .../scaffolder/stages/prepare/gitlab.test.ts | 27 ++++----- .../src/scaffolder/stages/prepare/gitlab.ts | 25 ++++++-- .../scaffolder/stages/prepare/preparers.ts | 24 ++------ .../src/scaffolder/stages/prepare/types.ts | 8 +-- 9 files changed, 209 insertions(+), 89 deletions(-) diff --git a/packages/integration/src/gitlab/config.ts b/packages/integration/src/gitlab/config.ts index 2d9f4b39ab..28687399e1 100644 --- a/packages/integration/src/gitlab/config.ts +++ b/packages/integration/src/gitlab/config.ts @@ -31,7 +31,7 @@ export type GitLabIntegrationConfig = { /** * The base URL of the API of this provider, e.g. "https://gitlab.com/api/v4", - * with no trailing slash. + * with no trailing slash.s * * May be omitted specifically for GitLab; then it will be deduced. * diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts index 39efd1c62a..38c262eaaa 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts @@ -33,6 +33,8 @@ describe('AzurePreparer', () => { clone: jest.fn(), }; + const logger = getVoidLogger(); + jest.spyOn(Git, 'fromAuth').mockReturnValue(mockGitClient as any); let mockEntity: TemplateEntityV1alpha1; @@ -78,31 +80,55 @@ describe('AzurePreparer', () => { }; }); - it('initializes git client with the correct arguments if an access token is provided for a repository', async () => { + // TODO(blam): Here's a test that will fail when the deprecation is complete + it('calls the clone command with deprecated token', async () => { const preparer = new AzurePreparer( new ConfigReader({ scaffolder: { azure: { api: { - token: 'fake-token', + token: 'fake-azure-token', }, }, }, }), + { logger }, ); - const logger = getVoidLogger(); - await preparer.prepare(mockEntity, { logger }); + + await preparer.prepare(mockEntity); expect(Git.fromAuth).toHaveBeenCalledWith({ - username: 'notempty', - password: 'fake-token', logger, + password: 'fake-azure-token', + username: 'notempty', }); }); - it('calls the clone command with the correct arguments for a repository', async () => { - const preparer = new AzurePreparer(new ConfigReader({})); - await preparer.prepare(mockEntity, { logger: getVoidLogger() }); + it('calls the clone command with token from integrations config', async () => { + const preparer = new AzurePreparer( + new ConfigReader({ + integrations: { + azure: [ + { host: 'dev.azure.com', token: 'fake-azure-token-integration' }, + ], + }, + }), + { logger }, + ); + + await preparer.prepare(mockEntity); + + expect(Git.fromAuth).toHaveBeenCalledWith({ + logger, + password: 'fake-azure-token-integration', + username: 'notempty', + }); + }); + + it('calls the clone command with the correct arguments for a repository', async () => { + const preparer = new AzurePreparer(new ConfigReader({}), { logger }); + + await preparer.prepare(mockEntity); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: @@ -112,10 +138,10 @@ describe('AzurePreparer', () => { }); it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { - const preparer = new AzurePreparer(new ConfigReader({})); + const preparer = new AzurePreparer(new ConfigReader({}), { logger }); delete mockEntity.spec.path; - await preparer.prepare(mockEntity, { logger: getVoidLogger() }); + await preparer.prepare(mockEntity); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: @@ -125,12 +151,10 @@ describe('AzurePreparer', () => { }); it('return the temp directory with the path to the folder if it is specified', async () => { - const preparer = new AzurePreparer(new ConfigReader({})); + const preparer = new AzurePreparer(new ConfigReader({}), { logger }); mockEntity.spec.path = './template/test/1/2/3'; - const response = await preparer.prepare(mockEntity, { - logger: getVoidLogger(), - }); + const response = await preparer.prepare(mockEntity); expect(response.split('\\').join('/')).toMatch( /\/template\/test\/1\/2\/3$/, @@ -138,11 +162,10 @@ describe('AzurePreparer', () => { }); it('return the working directory with the path to the folder if it is specified', async () => { - const preparer = new AzurePreparer(new ConfigReader({})); + const preparer = new AzurePreparer(new ConfigReader({}), { logger }); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { - logger: getVoidLogger(), workingDirectory: '/workDir', }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts index a38c5f4fdc..bb2257dfa7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts @@ -22,22 +22,46 @@ import { InputError, Git } from '@backstage/backend-common'; import { PreparerBase, PreparerOptions } from './types'; import GitUriParser from 'git-url-parse'; import { Config } from '@backstage/config'; +import { Logger } from 'winston'; +import { + readAzureIntegrationConfigs, + AzureIntegrationConfig, +} from '@backstage/integration'; export class AzurePreparer implements PreparerBase { - private readonly privateToken: string; + private readonly integrations: AzureIntegrationConfig[]; + private readonly scaffolderToken: string | undefined; + private readonly logger: Logger; - constructor(config: Config) { - this.privateToken = - config.getOptionalString('scaffolder.azure.api.token') ?? ''; + constructor(config: Config, { logger }: { logger: Logger }) { + this.logger = logger; + this.integrations = readAzureIntegrationConfigs( + config.getOptionalConfigArray('integrations.azure') ?? [], + ); + + if (!this.integrations.length) { + this.logger.warn( + 'Integrations for Azure in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', + ); + } + + this.scaffolderToken = config.getOptionalString( + 'scaffolder.azure.api.token', + ); + + if (this.scaffolderToken) { + this.logger.warn( + "DEPRECATION: Using the token format under 'scaffolder.azure.api.token' will not be respected in future releases. Please consider using integrations config instead", + ); + } } async prepare( template: TemplateEntityV1alpha1, - opts: PreparerOptions, + opts?: PreparerOptions, ): Promise { const { protocol, location } = parseLocationAnnotation(template); const workingDirectory = opts?.workingDirectory ?? os.tmpdir(); - const { logger } = opts; if (!['azure/api', 'url'].includes(protocol)) { throw new InputError( @@ -57,15 +81,17 @@ export class AzurePreparer implements PreparerBase { template.spec.path ?? '.', ); + const token = this.getToken(parsedGitLocation.resource); + // Username can be anything but the empty string according to: // https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page#use-a-pat - const git = this.privateToken + const git = token ? Git.fromAuth({ - password: this.privateToken, + password: token, username: 'notempty', - logger, + logger: this.logger, }) - : Git.fromAuth({ logger }); + : Git.fromAuth({ logger: this.logger }); await git.clone({ url: repositoryCheckoutUrl, @@ -74,4 +100,11 @@ export class AzurePreparer implements PreparerBase { return path.resolve(tempDir, templateDirectory); } + + private getToken(host: string): string | undefined { + return ( + this.scaffolderToken || + this.integrations.find(c => c.host === host)?.token + ); + } } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts index aa040bfe58..2b3e9698b2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts @@ -26,12 +26,14 @@ import { LOCATION_ANNOTATION, } from '@backstage/catalog-model'; import { getVoidLogger, Git } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; describe('GitHubPreparer', () => { let mockEntity: TemplateEntityV1alpha1; const mockGitClient = { clone: jest.fn(), }; + const logger = getVoidLogger(); jest.spyOn(Git, 'fromAuth').mockReturnValue(mockGitClient as any); @@ -77,9 +79,18 @@ describe('GitHubPreparer', () => { }; }); it('calls the clone command with the correct arguments for a repository', async () => { - const preparer = new GithubPreparer(); + const preparer = new GithubPreparer( + new ConfigReader({ + scaffolder: { + github: { + token: 'fake-token', + }, + }, + }), + { logger }, + ); - await preparer.prepare(mockEntity, { logger: getVoidLogger() }); + await preparer.prepare(mockEntity); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://github.com/benjdlambert/backstage-graphql-template', @@ -123,15 +134,42 @@ describe('GitHubPreparer', () => { ); }); - it('calls the clone command with the token when provided', async () => { - const preparer = new GithubPreparer({ token: 'abc' }); - const logger = getVoidLogger(); + it('calls the clone command with deprecated token', async () => { + const preparer = new GithubPreparer( + new ConfigReader({ + scaffolder: { + github: { + token: 'fake-token', + }, + }, + }), + { logger }, + ); - await preparer.prepare(mockEntity, { logger }); + await preparer.prepare(mockEntity); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, - username: 'abc', + username: 'fake-token', + password: 'x-oauth-basic', + }); + }); + + it('calls the clone command with token from integrations config', async () => { + const preparer = new GithubPreparer( + new ConfigReader({ + integrations: { + github: [{ host: 'github.com', token: 'fake-me' }], + }, + }), + { logger }, + ); + + await preparer.prepare(mockEntity); + + expect(Git.fromAuth).toHaveBeenCalledWith({ + logger, + username: 'fake-me', password: 'x-oauth-basic', }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts index e7fa0c9b72..46080c85a7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts @@ -20,22 +20,46 @@ import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { parseLocationAnnotation } from '../helpers'; import { InputError, Git } from '@backstage/backend-common'; import { PreparerBase, PreparerOptions } from './types'; +import { Logger } from 'winston'; import GitUriParser from 'git-url-parse'; +import { Config } from '@backstage/config'; +import { + GitHubIntegrationConfig, + readGitHubIntegrationConfigs, +} from '@backstage/integration'; export class GithubPreparer implements PreparerBase { - token?: string; + private readonly integrations: GitHubIntegrationConfig[]; + private readonly scaffolderToken: string | undefined; + private readonly logger: Logger; - constructor(params: { token?: string } = {}) { - this.token = params.token; + constructor(config: Config, { logger }: { logger: Logger }) { + this.logger = logger; + this.integrations = readGitHubIntegrationConfigs( + config.getOptionalConfigArray('integrations.github') ?? [], + ); + + if (!this.integrations.length) { + this.logger.warn( + 'Integrations for Github in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', + ); + } + + this.scaffolderToken = config.getOptionalString('scaffolder.github.token'); + + if (this.scaffolderToken) { + this.logger.warn( + "DEPRECATION: Using the token format under 'scaffolder.github.token' will not be respected in future releases. Please consider using integrations config instead", + ); + } } async prepare( template: TemplateEntityV1alpha1, - opts: PreparerOptions, + opts?: PreparerOptions, ): Promise { const { protocol, location } = parseLocationAnnotation(template); const workingDirectory = opts?.workingDirectory ?? os.tmpdir(); - const { logger } = opts; if (!['github', 'url'].includes(protocol)) { throw new InputError( @@ -57,13 +81,15 @@ export class GithubPreparer implements PreparerBase { const checkoutLocation = path.resolve(tempDir, templateDirectory); - const git = this.token + const token = this.getToken(parsedGitLocation.resource); + + const git = token ? Git.fromAuth({ - username: this.token, + username: token, password: 'x-oauth-basic', - logger, + logger: this.logger, }) - : Git.fromAuth({ logger }); + : Git.fromAuth({ logger: this.logger }); await git.clone({ url: repositoryCheckoutUrl, @@ -72,4 +98,10 @@ export class GithubPreparer implements PreparerBase { return checkoutLocation; } + private getToken(host: string): string | undefined { + return ( + this.scaffolderToken || + this.integrations.find(c => c.host === host)?.token + ); + } } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts index 8dad349a13..8cb54ceb7a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts @@ -70,6 +70,7 @@ describe('GitLabPreparer', () => { const mockGitClient = { clone: jest.fn(), }; + const logger = getVoidLogger(); jest.spyOn(Git, 'fromAuth').mockReturnValue(mockGitClient as any); @@ -79,10 +80,10 @@ describe('GitLabPreparer', () => { ['gitlab', 'gitlab/api'].forEach(protocol => { it(`calls the clone command with the correct arguments for a repository using the ${protocol} protocol`, async () => { - const preparer = new GitlabPreparer(new ConfigReader({})); + const preparer = new GitlabPreparer(new ConfigReader({}), { logger }); mockEntity = mockEntityWithProtocol(protocol); - await preparer.prepare(mockEntity, { logger: getVoidLogger() }); + await preparer.prepare(mockEntity); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://gitlab.com/benjdlambert/backstage-graphql-template', @@ -102,11 +103,11 @@ describe('GitLabPreparer', () => { ], }, }), + { logger }, ); mockEntity = mockEntityWithProtocol(protocol); - const logger = getVoidLogger(); - await preparer.prepare(mockEntity, { logger }); + await preparer.prepare(mockEntity); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, @@ -122,11 +123,11 @@ describe('GitLabPreparer', () => { gitlab: { api: { token: 'fake-token' } }, }, }), + { logger }, ); mockEntity = mockEntityWithProtocol(protocol); - const logger = getVoidLogger(); - await preparer.prepare(mockEntity, { logger }); + await preparer.prepare(mockEntity); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, @@ -136,11 +137,11 @@ describe('GitLabPreparer', () => { }); it(`calls the clone command with the correct arguments for a repository when no path is provided using the ${protocol} protocol`, async () => { - const preparer = new GitlabPreparer(new ConfigReader({})); + const preparer = new GitlabPreparer(new ConfigReader({}), { logger }); mockEntity = mockEntityWithProtocol(protocol); delete mockEntity.spec.path; - await preparer.prepare(mockEntity, { logger: getVoidLogger() }); + await preparer.prepare(mockEntity); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://gitlab.com/benjdlambert/backstage-graphql-template', @@ -149,23 +150,19 @@ describe('GitLabPreparer', () => { }); it(`return the temp directory with the path to the folder if it is specified using the ${protocol} protocol`, async () => { - const preparer = new GitlabPreparer(new ConfigReader({})); + const preparer = new GitlabPreparer(new ConfigReader({}), { logger }); mockEntity = mockEntityWithProtocol(protocol); mockEntity.spec.path = './template/test/1/2/3'; - const response = await preparer.prepare(mockEntity, { - logger: getVoidLogger(), - }); - + const response = await preparer.prepare(mockEntity); expect(response.split('\\').join('/')).toMatch( /\/template\/test\/1\/2\/3$/, ); }); it('return the working directory with the path to the folder if it is specified', async () => { - const preparer = new GitlabPreparer(new ConfigReader({})); + const preparer = new GitlabPreparer(new ConfigReader({}), { logger }); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { - logger: getVoidLogger(), workingDirectory: '/workDir', }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts index 6e169baa98..d0ce4ff3f7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts @@ -26,26 +26,41 @@ import os from 'os'; import path from 'path'; import { parseLocationAnnotation } from '../helpers'; import { PreparerBase, PreparerOptions } from './types'; +import { Logger } from 'winston'; export class GitlabPreparer implements PreparerBase { private readonly integrations: GitLabIntegrationConfig[]; private readonly scaffolderToken: string | undefined; + private readonly logger: Logger; - constructor(config: Config) { + constructor(config: Config, { logger }: { logger: Logger }) { + this.logger = logger; this.integrations = readGitLabIntegrationConfigs( config.getOptionalConfigArray('integrations.gitlab') ?? [], ); + + if (!this.integrations.length) { + this.logger.warn( + 'Integrations for GitLab in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', + ); + } + this.scaffolderToken = config.getOptionalString( 'scaffolder.gitlab.api.token', ); + + if (this.scaffolderToken) { + this.logger.warn( + "DEPRECATION: Using the token format under 'scaffolder.gitlab.api.token' will not be respected in future releases. Please consider using integrations config instead", + ); + } } async prepare( template: TemplateEntityV1alpha1, - opts: PreparerOptions, + opts?: PreparerOptions, ): Promise { const { protocol, location } = parseLocationAnnotation(template); - const { logger } = opts; const workingDirectory = opts?.workingDirectory ?? os.tmpdir(); if (!['gitlab', 'gitlab/api', 'url'].includes(protocol)) { @@ -71,9 +86,9 @@ export class GitlabPreparer implements PreparerBase { ? Git.fromAuth({ password: token, username: 'oauth2', - logger, + logger: this.logger, }) - : Git.fromAuth({ logger }); + : Git.fromAuth({ logger: this.logger }); await git.clone({ url: repositoryCheckoutUrl, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts index 84be130b79..c91711f85a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts @@ -77,31 +77,15 @@ export class Preparers implements PreparerBuilder { const preparers = new Preparers(typeDetector); const filePreparer = new FilePreparer(); - const gitlabPreparer = new GitlabPreparer(config); - const azurePreparer = new AzurePreparer(config); + const gitlabPreparer = new GitlabPreparer(config, { logger }); + const azurePreparer = new AzurePreparer(config, { logger }); + const githubPreparer = new GithubPreparer(config, { logger }); preparers.register('file', filePreparer); preparers.register('gitlab', gitlabPreparer); preparers.register('gitlab/api', gitlabPreparer); preparers.register('azure/api', azurePreparer); - - const githubConfig = config.getOptionalConfig('scaffolder.github'); - if (githubConfig) { - try { - const githubToken = githubConfig.getString('token'); - const githubPreparer = new GithubPreparer({ token: githubToken }); - - preparers.register('github', githubPreparer); - } catch (e) { - if (process.env.NODE_ENV !== 'development') { - throw new Error( - `Failed to initialize github scaffolding provider, ${e.message}`, - ); - } - - logger.warn(`Skipping github scaffolding provider, ${e.message}`); - } - } + preparers.register('github', githubPreparer); return preparers; } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts index 4936461c5c..34e0fe3696 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts @@ -14,15 +14,13 @@ * limitations under the License. */ import type { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -import { Logger } from 'winston'; import { RemoteProtocol } from '../types'; export type PreparerOptions = { - logger: Logger; workingDirectory?: string; }; -export type PreparerBase = { +export interface PreparerBase { /** * Given an Entity definition from the Service Catalog, go and prepare a directory * with contents from the remote location in temporary storage and return the path @@ -30,9 +28,9 @@ export type PreparerBase = { */ prepare( template: TemplateEntityV1alpha1, - opts: PreparerOptions, + opts?: PreparerOptions, ): Promise; -}; +} export type PreparerBuilder = { register(protocol: RemoteProtocol, preparer: PreparerBase): void; From 19741d2e67a516e8c38eec90900c7b3aed21deb9 Mon Sep 17 00:00:00 2001 From: blam Date: Sat, 2 Jan 2021 16:45:21 +0100 Subject: [PATCH 003/209] feat(scaffolder-backend/preparers): finished the deprecation of the scaffolder config object for preparers --- .../src/scaffolder/stages/prepare/github.test.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts index 2b3e9698b2..6598a8734a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts @@ -98,10 +98,10 @@ describe('GitHubPreparer', () => { }); }); it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { - const preparer = new GithubPreparer(); + const preparer = new GithubPreparer(new ConfigReader({}), { logger }); delete mockEntity.spec.path; - await preparer.prepare(mockEntity, { logger: getVoidLogger() }); + await preparer.prepare(mockEntity); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://github.com/benjdlambert/backstage-graphql-template', @@ -110,22 +110,18 @@ describe('GitHubPreparer', () => { }); it('return the temp directory with the path to the folder if it is specified', async () => { - const preparer = new GithubPreparer(); + const preparer = new GithubPreparer(new ConfigReader({}), { logger }); mockEntity.spec.path = './template/test/1/2/3'; - const response = await preparer.prepare(mockEntity, { - logger: getVoidLogger(), - }); - + const response = await preparer.prepare(mockEntity); expect(response.split('\\').join('/')).toMatch( /\/template\/test\/1\/2\/3$/, ); }); it('return the working directory with the path to the folder if it is specified', async () => { - const preparer = new GithubPreparer(); + const preparer = new GithubPreparer(new ConfigReader({}), { logger }); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { - logger: getVoidLogger(), workingDirectory: '/workDir', }); From c5cdc7e74d79421e6b11f0a7dd1583099414b752 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 4 Jan 2021 10:56:46 +0100 Subject: [PATCH 004/209] chore: starting the migration of a publisher --- .../src/scaffolder/stages/publish/azure.ts | 4 +- .../src/scaffolder/stages/publish/gitlab.ts | 37 +++++++++++++++---- .../src/scaffolder/stages/publish/types.ts | 1 - 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts index ffe6845f01..8947c09e60 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts @@ -17,7 +17,7 @@ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; import { GitApi } from 'azure-devops-node-api/GitApi'; import { GitRepositoryCreateOptions } from 'azure-devops-node-api/interfaces/GitInterfaces'; -import { JsonValue } from '@backstage/config'; +import { JsonValue, Config } from '@backstage/config'; import { RequiredTemplateValues } from '../templater'; import { initRepoAndPush } from './helpers'; @@ -25,7 +25,7 @@ export class AzurePublisher implements PublisherBase { private readonly client: GitApi; private readonly token: string; - constructor(client: GitApi, token: string) { + constructor(config: Config) { this.client = client; this.token = token; } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts index 156a4a249c..66d110d2d7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts @@ -16,23 +16,46 @@ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; import { Gitlab } from '@gitbeaker/core'; -import { JsonValue } from '@backstage/config'; +import { Config, JsonValue } from '@backstage/config'; +import { Logger } from 'winston'; import { initRepoAndPush } from './helpers'; import { RequiredTemplateValues } from '../templater'; +import { + GitLabIntegrationConfig, + readGitLabIntegrationConfigs, +} from '@backstage/integration'; export class GitlabPublisher implements PublisherBase { - private readonly client: Gitlab; - private readonly token: string; + private readonly integrations: GitLabIntegrationConfig[]; + private readonly scaffolderToken: string | undefined; + private readonly logger: Logger; - constructor(client: Gitlab, token: string) { - this.client = client; - this.token = token; + constructor(config: Config, { logger }: { logger: Logger }) { + this.logger = logger; + this.integrations = readGitLabIntegrationConfigs( + config.getOptionalConfigArray('integrations.gitlab') ?? [], + ); + + if (!this.integrations.length) { + this.logger.warn( + 'Integrations for GitLab in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', + ); + } + + this.scaffolderToken = config.getOptionalString( + 'scaffolder.gitlab.api.token', + ); + + if (this.scaffolderToken) { + this.logger.warn( + "DEPRECATION: Using the token format under 'scaffolder.gitlab.api.token' will not be respected in future releases. Please consider using integrations config instead", + ); + } } async publish({ values, directory, - logger, }: PublisherOptions): Promise { const remoteUrl = await this.createRemote(values); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts index bc6b63cc57..807d7c82cf 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts @@ -35,7 +35,6 @@ export type PublisherBase = { export type PublisherOptions = { values: RequiredTemplateValues & Record; - logger: Logger; directory: string; }; From b617083159f4f61327bdc2b3839bc246acd8b3ff Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Tue, 5 Jan 2021 12:46:05 +0200 Subject: [PATCH 005/209] Added a table for showing consumer group topic offsets --- plugins/kafka/package.json | 4 +- plugins/kafka/src/Router.tsx | 38 ++++++ plugins/kafka/src/api/KafkaApi.ts | 112 ++++++++++++++++++ .../ExampleComponent => api}/index.ts | 3 +- .../ConsumerGroupOffsets.tsx | 111 +++++++++++++++++ .../useConsumerGroupOffsets.ts | 63 ++++++++++ .../useConsumerGroupsFromEntity.ts | 24 ++++ .../ExampleComponent.test.tsx | 45 ------- .../ExampleComponent/ExampleComponent.tsx | 55 --------- .../ExampleFetchComponent.test.tsx | 40 ------- .../ExampleFetchComponent.tsx | 107 ----------------- .../index.ts => constants.ts} | 3 +- plugins/kafka/src/index.ts | 3 + plugins/kafka/src/plugin.ts | 21 +++- yarn.lock | 12 ++ 15 files changed, 386 insertions(+), 255 deletions(-) create mode 100644 plugins/kafka/src/Router.tsx create mode 100644 plugins/kafka/src/api/KafkaApi.ts rename plugins/kafka/src/{components/ExampleComponent => api}/index.ts (92%) create mode 100644 plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.tsx create mode 100644 plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupOffsets.ts create mode 100644 plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsFromEntity.ts delete mode 100644 plugins/kafka/src/components/ExampleComponent/ExampleComponent.test.tsx delete mode 100644 plugins/kafka/src/components/ExampleComponent/ExampleComponent.tsx delete mode 100644 plugins/kafka/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx delete mode 100644 plugins/kafka/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx rename plugins/kafka/src/{components/ExampleFetchComponent/index.ts => constants.ts} (87%) diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index f5dc01ec45..098d9a426c 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -23,9 +23,10 @@ "@backstage/core": "^0.4.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", - "@material-ui/icons": "^4.9.1", + "@material-ui/icons": "^4.11.2", "@material-ui/lab": "4.0.0-alpha.45", "kafkajs": "^1.15.0", + "lodash": "^4.17.20", "react": "^16.13.1", "react-dom": "^16.13.1", "react-use": "^15.3.3" @@ -38,6 +39,7 @@ "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", + "@types/lodash": "^4.14.166", "@types/node": "^12.0.0", "cross-fetch": "^3.0.6", "msw": "^0.21.2" diff --git a/plugins/kafka/src/Router.tsx b/plugins/kafka/src/Router.tsx new file mode 100644 index 0000000000..9de9588bd6 --- /dev/null +++ b/plugins/kafka/src/Router.tsx @@ -0,0 +1,38 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; +import { MissingAnnotationEmptyState } from '@backstage/core'; +import React from 'react'; +import { Route, Routes } from 'react-router'; +import { KAFKA_CONSUMER_GROUP_ANNOTATION } from './constants'; +import { KafkaTopicsForConsumer } from './components/ConsumerGroupOffsets/ConsumerGroupOffsets'; + +export const isPluginApplicableToEntity = (entity: Entity) => + Boolean(entity.metadata.annotations?.[KAFKA_CONSUMER_GROUP_ANNOTATION]); + +export const Router = ({ entity }: { entity: Entity }) => { + return !isPluginApplicableToEntity(entity) ? ( + + ) : ( + + } + /> + + ); +}; diff --git a/plugins/kafka/src/api/KafkaApi.ts b/plugins/kafka/src/api/KafkaApi.ts new file mode 100644 index 0000000000..63ac948047 --- /dev/null +++ b/plugins/kafka/src/api/KafkaApi.ts @@ -0,0 +1,112 @@ +/* + * 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 { ConfigApi, DiscoveryApi, createApiRef } from '@backstage/core'; +import { Kafka, SeekEntry } from 'kafkajs'; + +export const kafkaApiRef = createApiRef({ + id: 'plugin.kafka.service', + description: 'Used by the Kafka plugin to connect to the Kafka cluster', +}); + +export type TopicOffsets = { + topic: string; + partitions: { + id: number; + offset: string; + }[]; +}; + +export class KafkaApi { + static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi) { + const clientId = + configApi.getOptionalString('kafka.clientId') ?? 'kafka-backstage'; + const brokers = configApi.getOptionalStringArray('kafka.brokers') ?? [ + '/kafka/broker', + ]; + + return new KafkaApi(discoveryApi, clientId, brokers); + } + + constructor( + private readonly discoveryApi: DiscoveryApi, + private readonly clientId: string, + private readonly brokers: string[], + ) {} + + async fetchTopicsOffsets(topics: string[]): Promise> { + const admin = await this.getAdmin(); + + await admin.connect(); + try { + return await Promise.all( + topics.map(async topic => + admin + .fetchTopicOffsets(topic) + .then(result => KafkaApi.toTopicOffsets(topic, result)), + ), + ); + } finally { + await admin.disconnect(); + } + } + + async fetchGroupOffsets(groupId: string): Promise> { + const admin = await this.getAdmin(); + + await admin.connect(); + try { + let groupOffsets = [ + { + topic: 'shula', + partitions: await admin.fetchOffsets({ groupId, topic: 'shula' }), + }, + { + topic: 'test', + partitions: await admin.fetchOffsets({ groupId, topic: 'test' }), + }, + ]; + + return groupOffsets.map(topicOffset => + KafkaApi.toTopicOffsets(topicOffset.topic, topicOffset.partitions), + ); + } finally { + await admin.disconnect(); + } + } + + private async getAdmin() { + const kafka = await this.getKafka(); + return kafka.admin(); + } + + private async getKafka() { + const proxyUrl = await this.discoveryApi.getBaseUrl('proxy'); + + return new Kafka({ + clientId: this.clientId, + brokers: this.brokers.map(brokerUrl => proxyUrl + brokerUrl), + }); + } + + private static toTopicOffsets(topic: string, result: Array) { + const partitions = result.map(seekEntry => ({ + id: seekEntry.partition, + offset: seekEntry.offset, + })); + return { topic, partitions }; + } +} diff --git a/plugins/kafka/src/components/ExampleComponent/index.ts b/plugins/kafka/src/api/index.ts similarity index 92% rename from plugins/kafka/src/components/ExampleComponent/index.ts rename to plugins/kafka/src/api/index.ts index 2c6f338ec7..cd78c09cad 100644 --- a/plugins/kafka/src/components/ExampleComponent/index.ts +++ b/plugins/kafka/src/api/index.ts @@ -13,4 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { default } from './ExampleComponent'; + +export { KafkaApi } from './KafkaApi'; diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.tsx b/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.tsx new file mode 100644 index 0000000000..0f697e4994 --- /dev/null +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.tsx @@ -0,0 +1,111 @@ +/* + * 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 { Table, TableColumn } from '@backstage/core'; +import { Box, Typography } from '@material-ui/core'; +import RetryIcon from '@material-ui/icons/Replay'; +import React from 'react'; +import { useConsumerGroupFromEntity } from './useConsumerGroupsFromEntity'; +import { useConsumerGroupOffsets } from './useConsumerGroupOffsets'; + +export type TopicPartitionInfo = { + topic: string; + partitionId: number; + topicOffset: string; + groupOffset: string; +}; + +const generatedColumns: TableColumn[] = [ + { + title: 'Topic', + field: 'topic', + highlight: true, + render: (row: Partial) => { + return <>{row.topic ?? ''}; + }, + }, + { + title: 'Partition', + field: 'partitionId', + render: (row: Partial) => { + return <>{row.partitionId ?? ''}; + }, + }, + { + title: 'Topic Offset', + field: 'topicOffset', + render: (row: Partial) => { + return <>{row.topicOffset ?? ''}; + }, + }, + { + title: 'Group Offset', + field: 'groupOffset', + render: (row: Partial) => { + return <>{row.groupOffset ?? ''}; + }, + }, +]; + +type Props = { + loading: boolean; + retry: () => void; + consumerGroup: string; + topics?: TopicPartitionInfo[]; +}; + +export const ConsumerGroupOffsets = ({ + loading, + topics, + consumerGroup, + retry, +}: Props) => { + return ( +
, + tooltip: 'Refresh Data', + isFreeAction: true, + onClick: () => retry(), + }, + ]} + data={topics ?? []} + title={ + + + Consumed Topics for {consumerGroup} + + + } + columns={generatedColumns} + /> + ); +}; + +export const KafkaTopicsForConsumer = () => { + const consumerGroup = useConsumerGroupFromEntity(); + const [tableProps, { retry }] = useConsumerGroupOffsets(consumerGroup); + + return ( + + ); +}; diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupOffsets.ts b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupOffsets.ts new file mode 100644 index 0000000000..95c6e0892d --- /dev/null +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupOffsets.ts @@ -0,0 +1,63 @@ +/* + * 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 { errorApiRef, useApi } from '@backstage/core'; +import { useAsyncRetry } from 'react-use'; +import { kafkaApiRef } from '../../api/KafkaApi'; +import _ from 'lodash'; + +export function useConsumerGroupOffsets(groupId: string) { + const api = useApi(kafkaApiRef); + const errorApi = useApi(errorApiRef); + + const { loading, value: topics, retry } = useAsyncRetry(async () => { + try { + const groupOnlyOffsets = await api.fetchGroupOffsets(groupId); + const topicOffsets = _.keyBy( + await api.fetchTopicsOffsets( + groupOnlyOffsets.map(value => value.topic), + ), + offsets => offsets.topic, + ); + + return groupOnlyOffsets.flatMap(value => { + let topicPartitionOffsets = _.keyBy( + topicOffsets[value.topic].partitions, + partition => partition.id, + ); + return value.partitions.map(partition => ({ + topic: value.topic, + partitionId: partition.id, + groupOffset: partition.offset, + topicOffset: topicPartitionOffsets[partition.id].offset, + })); + }); + } catch (e) { + errorApi.post(e); + throw e; + } + }, [api, errorApi, groupId]); + + return [ + { + loading, + topics, + }, + { + retry, + }, + ] as const; +} diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsFromEntity.ts b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsFromEntity.ts new file mode 100644 index 0000000000..8bad96b61c --- /dev/null +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsFromEntity.ts @@ -0,0 +1,24 @@ +/* + * 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 { useEntity } from '@backstage/plugin-catalog'; +import { KAFKA_CONSUMER_GROUP_ANNOTATION } from '../../constants'; + +export const useConsumerGroupFromEntity = () => { + const { entity } = useEntity(); + + return entity.metadata.annotations?.[KAFKA_CONSUMER_GROUP_ANNOTATION] ?? ''; +}; diff --git a/plugins/kafka/src/components/ExampleComponent/ExampleComponent.test.tsx b/plugins/kafka/src/components/ExampleComponent/ExampleComponent.test.tsx deleted file mode 100644 index 680714eeda..0000000000 --- a/plugins/kafka/src/components/ExampleComponent/ExampleComponent.test.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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 React from 'react'; -import { render } from '@testing-library/react'; -import ExampleComponent from './ExampleComponent'; -import { ThemeProvider } from '@material-ui/core'; -import { lightTheme } from '@backstage/theme'; -import { rest } from 'msw'; -import { setupServer } from 'msw/node'; -import { msw } from '@backstage/test-utils'; - -describe('ExampleComponent', () => { - const server = setupServer(); - // Enable sane handlers for network requests - msw.setupDefaultHandlers(server); - - // setup mock response - beforeEach(() => { - server.use( - rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))), - ); - }); - - it('should render', () => { - const rendered = render( - - - , - ); - expect(rendered.getByText('Welcome to kafka!')).toBeInTheDocument(); - }); -}); diff --git a/plugins/kafka/src/components/ExampleComponent/ExampleComponent.tsx b/plugins/kafka/src/components/ExampleComponent/ExampleComponent.tsx deleted file mode 100644 index cfa36acd7b..0000000000 --- a/plugins/kafka/src/components/ExampleComponent/ExampleComponent.tsx +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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 React from 'react'; -import { Typography, Grid } from '@material-ui/core'; -import { - InfoCard, - Header, - Page, - Content, - ContentHeader, - HeaderLabel, - SupportButton, -} from '@backstage/core'; -import ExampleFetchComponent from '../ExampleFetchComponent'; - -const ExampleComponent = () => ( - -
- - -
- - - A description of your plugin goes here. - - - - - - All content should be wrapped in a card like this. - - - - - - - - -
-); - -export default ExampleComponent; diff --git a/plugins/kafka/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx b/plugins/kafka/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx deleted file mode 100644 index 0a73ed0fc1..0000000000 --- a/plugins/kafka/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 React from 'react'; -import { render } from '@testing-library/react'; -import ExampleFetchComponent from './ExampleFetchComponent'; -import { rest } from 'msw'; -import { setupServer } from 'msw/node'; -import { msw } from '@backstage/test-utils'; - -describe('ExampleFetchComponent', () => { - const server = setupServer(); - // Enable sane handlers for network requests - msw.setupDefaultHandlers(server); - - // setup mock response - beforeEach(() => { - server.use( - rest.get('https://randomuser.me/*', (_, res, ctx) => - res(ctx.status(200), ctx.delay(2000), ctx.json({})), - ), - ); - }); - it('should render', async () => { - const rendered = render(); - expect(await rendered.findByTestId('progress')).toBeInTheDocument(); - }); -}); diff --git a/plugins/kafka/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx b/plugins/kafka/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx deleted file mode 100644 index 8a4aef11b8..0000000000 --- a/plugins/kafka/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx +++ /dev/null @@ -1,107 +0,0 @@ -/* - * 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 React from 'react'; -import { makeStyles } from '@material-ui/core/styles'; -import { Table, TableColumn, Progress } from '@backstage/core'; -import Alert from '@material-ui/lab/Alert'; -import { useAsync } from 'react-use'; - -const useStyles = makeStyles({ - avatar: { - height: 32, - width: 32, - borderRadius: '50%', - }, -}); - -type User = { - gender: string; // "male" - name: { - title: string; // "Mr", - first: string; // "Duane", - last: string; // "Reed" - }; - location: object; // {street: {number: 5060, name: "Hickory Creek Dr"}, city: "Albany", state: "New South Wales",…} - email: string; // "duane.reed@example.com" - login: object; // {uuid: "4b785022-9a23-4ab9-8a23-cb3fb43969a9", username: "blackdog796", password: "patch",…} - dob: object; // {date: "1983-06-22T12:30:23.016Z", age: 37} - registered: object; // {date: "2006-06-13T18:48:28.037Z", age: 14} - phone: string; // "07-2154-5651" - cell: string; // "0405-592-879" - id: { - name: string; // "TFN", - value: string; // "796260432" - }; - picture: { medium: string }; // {medium: "https://randomuser.me/api/portraits/men/95.jpg",…} - nat: string; // "AU" -}; - -type DenseTableProps = { - users: User[]; -}; - -export const DenseTable = ({ users }: DenseTableProps) => { - const classes = useStyles(); - - const columns: TableColumn[] = [ - { title: 'Avatar', field: 'avatar' }, - { title: 'Name', field: 'name' }, - { title: 'Email', field: 'email' }, - { title: 'Nationality', field: 'nationality' }, - ]; - - const data = users.map(user => { - return { - avatar: ( - {user.name.first} - ), - name: `${user.name.first} ${user.name.last}`, - email: user.email, - nationality: user.nat, - }; - }); - - return ( -
- ); -}; - -const ExampleFetchComponent = () => { - const { value, loading, error } = useAsync(async (): Promise => { - const response = await fetch('https://randomuser.me/api/?results=20'); - const data = await response.json(); - return data.results; - }, []); - - if (loading) { - return ; - } else if (error) { - return {error.message}; - } - - return ; -}; - -export default ExampleFetchComponent; diff --git a/plugins/kafka/src/components/ExampleFetchComponent/index.ts b/plugins/kafka/src/constants.ts similarity index 87% rename from plugins/kafka/src/components/ExampleFetchComponent/index.ts rename to plugins/kafka/src/constants.ts index e0d021564d..652911a91f 100644 --- a/plugins/kafka/src/components/ExampleFetchComponent/index.ts +++ b/plugins/kafka/src/constants.ts @@ -13,4 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { default } from './ExampleFetchComponent'; +export const KAFKA_CONSUMER_GROUP_ANNOTATION = + 'kafka.apache.org/consumer-groups'; diff --git a/plugins/kafka/src/index.ts b/plugins/kafka/src/index.ts index 224e293890..80670acb81 100644 --- a/plugins/kafka/src/index.ts +++ b/plugins/kafka/src/index.ts @@ -14,3 +14,6 @@ * limitations under the License. */ export { plugin } from './plugin'; +export { KAFKA_CONSUMER_GROUP_ANNOTATION } from './constants'; +export { Router, isPluginApplicableToEntity } from './Router'; +export * from './api'; diff --git a/plugins/kafka/src/plugin.ts b/plugins/kafka/src/plugin.ts index f0bc22481e..a666defa44 100644 --- a/plugins/kafka/src/plugin.ts +++ b/plugins/kafka/src/plugin.ts @@ -13,8 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createPlugin, createRouteRef } from '@backstage/core'; -import ExampleComponent from './components/ExampleComponent'; +import { + configApiRef, + createApiFactory, + createPlugin, + createRouteRef, + discoveryApiRef, +} from '@backstage/core'; +import { KafkaApi, kafkaApiRef } from './api/KafkaApi'; export const rootRouteRef = createRouteRef({ path: '/kafka', @@ -23,7 +29,12 @@ export const rootRouteRef = createRouteRef({ export const plugin = createPlugin({ id: 'kafka', - register({ router }) { - router.addRoute(rootRouteRef, ExampleComponent); - }, + apis: [ + createApiFactory({ + api: kafkaApiRef, + deps: { discoveryApi: discoveryApiRef, configApi: configApiRef }, + factory: ({ configApi, discoveryApi }) => + KafkaApi.fromConfig(configApi, discoveryApi), + }), + ], }); diff --git a/yarn.lock b/yarn.lock index bb8a845850..0ad51ca447 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3734,6 +3734,13 @@ react-is "^16.8.0" react-transition-group "^4.4.0" +"@material-ui/icons@^4.11.2": + version "4.11.2" + resolved "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.2.tgz#b3a7353266519cd743b6461ae9fdfcb1b25eb4c5" + integrity sha512-fQNsKX2TxBmqIGJCSi3tGTO/gZ+eJgWmMJkgDiOfyNaunNaxcklJQFaFogYcFl0qFuaEz1qaXYXboa/bUXVSOQ== + dependencies: + "@babel/runtime" "^7.4.4" + "@material-ui/icons@^4.9.1": version "4.9.1" resolved "https://registry.npmjs.org/@material-ui/icons/-/icons-4.9.1.tgz#fdeadf8cb3d89208945b33dbc50c7c616d0bd665" @@ -5866,6 +5873,11 @@ resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.165.tgz#74d55d947452e2de0742bad65270433b63a8c30f" integrity sha512-tjSSOTHhI5mCHTy/OOXYIhi2Wt1qcbHmuXD1Ha7q70CgI/I71afO4XtLb/cVexki1oVYchpul/TOuu3Arcdxrg== +"@types/lodash@^4.14.166": + version "4.14.166" + resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.166.tgz#07e7f2699a149219dbc3c35574f126ec8737688f" + integrity sha512-A3YT/c1oTlyvvW/GQqG86EyqWNrT/tisOIh2mW3YCgcx71TNjiTZA3zYZWA5BCmtsOTXjhliy4c4yEkErw6njA== + "@types/long@^4.0.0": version "4.0.1" resolved "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" From 343026aade36b60f0394fc4503131b0dd4a31773 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Tue, 5 Jan 2021 19:08:33 +0200 Subject: [PATCH 006/209] small integration fixes to kafka client plugin --- plugins/kafka/src/Router.tsx | 6 ++++-- plugins/kafka/src/plugin.ts | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/plugins/kafka/src/Router.tsx b/plugins/kafka/src/Router.tsx index 9de9588bd6..b445f8f408 100644 --- a/plugins/kafka/src/Router.tsx +++ b/plugins/kafka/src/Router.tsx @@ -15,11 +15,13 @@ */ import { Entity } from '@backstage/catalog-model'; -import { MissingAnnotationEmptyState } from '@backstage/core'; import React from 'react'; import { Route, Routes } from 'react-router'; + +import { rootCatalogKafkaRouteRef } from './plugin'; import { KAFKA_CONSUMER_GROUP_ANNOTATION } from './constants'; import { KafkaTopicsForConsumer } from './components/ConsumerGroupOffsets/ConsumerGroupOffsets'; +import { MissingAnnotationEmptyState } from '@backstage/core'; export const isPluginApplicableToEntity = (entity: Entity) => Boolean(entity.metadata.annotations?.[KAFKA_CONSUMER_GROUP_ANNOTATION]); @@ -30,7 +32,7 @@ export const Router = ({ entity }: { entity: Entity }) => { ) : ( } /> diff --git a/plugins/kafka/src/plugin.ts b/plugins/kafka/src/plugin.ts index a666defa44..40c288e671 100644 --- a/plugins/kafka/src/plugin.ts +++ b/plugins/kafka/src/plugin.ts @@ -22,9 +22,9 @@ import { } from '@backstage/core'; import { KafkaApi, kafkaApiRef } from './api/KafkaApi'; -export const rootRouteRef = createRouteRef({ - path: '/kafka', - title: 'kafka', +export const rootCatalogKafkaRouteRef = createRouteRef({ + path: '*', + title: 'Kafka', }); export const plugin = createPlugin({ From ed4eaa79351ba47276a8fe13b202210b5732ace2 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 7 Jan 2021 02:34:22 +0100 Subject: [PATCH 007/209] chore: some work to more bb to integrations --- .../src/scaffolder/stages/prepare/bitbucket.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts index 5b244d24ce..8ee39f41fa 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts @@ -20,14 +20,21 @@ import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { parseLocationAnnotation } from '../helpers'; import { InputError, Git } from '@backstage/backend-common'; import { PreparerBase, PreparerOptions } from './types'; +import { readBitbucketIntegrationConfigs, BitbucketIntegrationConfig } from '@backstage/integration'; import GitUriParser from 'git-url-parse'; import { Config } from '@backstage/config'; +import { Logger } from 'winston'; export class BitbucketPreparer implements PreparerBase { private readonly privateToken: string; private readonly username: string; - - constructor(config: Config) { + private readonly logger: Logger; + private readonly integrations: BitbucketIntegrationConfig[]; + constructor(config: Config, { logger }: { logger: Logger}) { + this.logger = logger; + this.integrations = readBitbucketIntegrationConfigs( + config.getOptionalConfigArray('integrations.bitbucket') ??Takj + ) this.username = config.getOptionalString('scaffolder.bitbucket.api.username') ?? ''; this.privateToken = From 6547e5aa94e2ef44c96fcd1f492b746694b77dcd Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 8 Jan 2021 15:43:38 +0100 Subject: [PATCH 008/209] chore: updating some more things --- .../scaffolder/stages/prepare/bitbucket.ts | 26 +++++++++++++++---- .../src/scaffolder/stages/publish/helpers.ts | 2 +- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts index 8ee39f41fa..5bc58e34f0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts @@ -20,7 +20,10 @@ import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { parseLocationAnnotation } from '../helpers'; import { InputError, Git } from '@backstage/backend-common'; import { PreparerBase, PreparerOptions } from './types'; -import { readBitbucketIntegrationConfigs, BitbucketIntegrationConfig } from '@backstage/integration'; +import { + readBitbucketIntegrationConfigs, + BitbucketIntegrationConfig, +} from '@backstage/integration'; import GitUriParser from 'git-url-parse'; import { Config } from '@backstage/config'; import { Logger } from 'winston'; @@ -29,16 +32,29 @@ export class BitbucketPreparer implements PreparerBase { private readonly privateToken: string; private readonly username: string; private readonly logger: Logger; - private readonly integrations: BitbucketIntegrationConfig[]; - constructor(config: Config, { logger }: { logger: Logger}) { + private readonly integrations: BitbucketIntegrationConfig[]; + constructor(config: Config, { logger }: { logger: Logger }) { this.logger = logger; this.integrations = readBitbucketIntegrationConfigs( - config.getOptionalConfigArray('integrations.bitbucket') ??Takj - ) + config.getOptionalConfigArray('integrations.bitbucket') ?? [], + ); + + if (!this.integrations.length) { + this.logger.warn( + 'Integrations for BitBucket in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', + ); + } + this.username = config.getOptionalString('scaffolder.bitbucket.api.username') ?? ''; this.privateToken = config.getOptionalString('scaffolder.bitbucket.api.token') ?? ''; + + if (this.username || this.privateToken) { + this.logger.warn( + "DEPRECATION: Using the token format under 'scaffolder.github.token' will not be respected in future releases. Please consider using integrations config instead", + ); + } } async prepare( diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/helpers.ts index 796e48b8f8..ffefb88cf0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/helpers.ts @@ -39,7 +39,7 @@ export async function initRepoAndPush({ dir, }); - const paths = await globby(['./**', './**/.*'], { + const paths = await globby(['./**', './**/.*', '!.git'], { cwd: dir, gitignore: true, dot: true, From c139285492e4ff1f1932aaef8ad73eac709dbb76 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Sat, 9 Jan 2021 21:18:41 +0200 Subject: [PATCH 009/209] Split to kafka backend plugin --- plugins/kafka-backend/.eslintrc.js | 3 + plugins/kafka-backend/config.d.ts | 21 + plugins/kafka-backend/package-lock.json | 18259 ++++++++++++++++ plugins/kafka-backend/package.json | 48 + plugins/kafka-backend/src/index.test.ts | 23 + .../src/api => kafka-backend/src}/index.ts | 3 +- plugins/kafka-backend/src/run.ts | 33 + plugins/kafka-backend/src/service/KafkaApi.ts | 82 + plugins/kafka-backend/src/service/router.ts | 75 + .../src/service/standaloneApplication.ts | 54 + .../src/service/standaloneServer.ts | 50 + plugins/kafka-backend/src/setupTests.ts | 17 + plugins/kafka-backend/src/types/types.ts | 20 + plugins/kafka/package.json | 2 +- plugins/kafka/src/api/KafkaApi.ts | 112 - plugins/kafka/src/api/KafkaBackendClient.ts | 58 + plugins/kafka/src/api/types.ts | 32 + .../useConsumerGroupOffsets.ts | 40 +- plugins/kafka/src/index.ts | 1 - plugins/kafka/src/plugin.ts | 6 +- yarn.lock | 105 +- 21 files changed, 18886 insertions(+), 158 deletions(-) create mode 100644 plugins/kafka-backend/.eslintrc.js create mode 100644 plugins/kafka-backend/config.d.ts create mode 100644 plugins/kafka-backend/package-lock.json create mode 100644 plugins/kafka-backend/package.json create mode 100644 plugins/kafka-backend/src/index.test.ts rename plugins/{kafka/src/api => kafka-backend/src}/index.ts (90%) create mode 100644 plugins/kafka-backend/src/run.ts create mode 100644 plugins/kafka-backend/src/service/KafkaApi.ts create mode 100644 plugins/kafka-backend/src/service/router.ts create mode 100644 plugins/kafka-backend/src/service/standaloneApplication.ts create mode 100644 plugins/kafka-backend/src/service/standaloneServer.ts create mode 100644 plugins/kafka-backend/src/setupTests.ts create mode 100644 plugins/kafka-backend/src/types/types.ts delete mode 100644 plugins/kafka/src/api/KafkaApi.ts create mode 100644 plugins/kafka/src/api/KafkaBackendClient.ts create mode 100644 plugins/kafka/src/api/types.ts diff --git a/plugins/kafka-backend/.eslintrc.js b/plugins/kafka-backend/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/kafka-backend/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/kafka-backend/config.d.ts b/plugins/kafka-backend/config.d.ts new file mode 100644 index 0000000000..692c8754d2 --- /dev/null +++ b/plugins/kafka-backend/config.d.ts @@ -0,0 +1,21 @@ +/* + * 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 interface Config { + kafka?: { + clientId: string; + brokers: string[]; + }; +} diff --git a/plugins/kafka-backend/package-lock.json b/plugins/kafka-backend/package-lock.json new file mode 100644 index 0000000000..71b7da3a8b --- /dev/null +++ b/plugins/kafka-backend/package-lock.json @@ -0,0 +1,18259 @@ +{ + "name": "kafka-backend", + "version": "0.1.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/compat-data": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.12.7.tgz", + "integrity": "sha512-YaxPMGs/XIWtYqrdEOZOCPsVWfEoriXopnsz3/i7apYPXQ3698UFhS6dVT1KN5qOsWmVgw/FOrmQgpRaZayGsw==", + "dev": true + }, + "@babel/core": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.10.tgz", + "integrity": "sha512-eTAlQKq65zHfkHZV0sIVODCPGVgoo1HdBlbSLi9CqOzuZanMv2ihzY+4paiKr1mH+XmYESMAmJ/dpZ68eN6d8w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.12.10", + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helpers": "^7.12.5", + "@babel/parser": "^7.12.10", + "@babel/template": "^7.12.7", + "@babel/traverse": "^7.12.10", + "@babel/types": "^7.12.10", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.19", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.11.tgz", + "integrity": "sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==", + "dev": true, + "requires": { + "@babel/types": "^7.12.11", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.10.tgz", + "integrity": "sha512-XplmVbC1n+KY6jL8/fgLVXXUauDIB+lD5+GsQEh6F6GBF1dq1qy4DP4yXWzDKcoqXB3X58t61e85Fitoww4JVQ==", + "dev": true, + "requires": { + "@babel/types": "^7.12.10" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz", + "integrity": "sha512-L0zGlFrGWZK4PbT8AszSfLTM5sDU1+Az/En9VrdT8/LmEiJt4zXt+Jve9DCAnQcbqDhCI+29y/L93mrDzddCcg==", + "dev": true, + "requires": { + "@babel/helper-explode-assignable-expression": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.12.5.tgz", + "integrity": "sha512-+qH6NrscMolUlzOYngSBMIOQpKUGPPsc61Bu5W10mg84LxZ7cmvnBHzARKbDoFxVvqqAbj6Tg6N7bSrWSPXMyw==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.12.5", + "@babel/helper-validator-option": "^7.12.1", + "browserslist": "^4.14.5", + "semver": "^5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.1.tgz", + "integrity": "sha512-hkL++rWeta/OVOBTRJc9a5Azh5mt5WgZUGAKMD8JM141YsE08K//bp1unBBieO6rUKkIPyUE0USQ30jAy3Sk1w==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-member-expression-to-functions": "^7.12.1", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/helper-replace-supers": "^7.12.1", + "@babel/helper-split-export-declaration": "^7.10.4" + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.7.tgz", + "integrity": "sha512-idnutvQPdpbduutvi3JVfEgcVIHooQnhvhx0Nk9isOINOIGYkZea1Pk2JlJRiUnMefrlvr0vkByATBY/mB4vjQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.10.4", + "regexpu-core": "^4.7.1" + } + }, + "@babel/helper-define-map": { + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz", + "integrity": "sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.10.4", + "@babel/types": "^7.10.5", + "lodash": "^4.17.19" + } + }, + "@babel/helper-explode-assignable-expression": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.12.1.tgz", + "integrity": "sha512-dmUwH8XmlrUpVqgtZ737tK88v07l840z9j3OEhCLwKTkjlvKpfqXVIZ0wpK3aeOxspwGrf/5AP5qLx4rO3w5rA==", + "dev": true, + "requires": { + "@babel/types": "^7.12.1" + } + }, + "@babel/helper-function-name": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz", + "integrity": "sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.12.10", + "@babel/template": "^7.12.7", + "@babel/types": "^7.12.11" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz", + "integrity": "sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag==", + "dev": true, + "requires": { + "@babel/types": "^7.12.10" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz", + "integrity": "sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz", + "integrity": "sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw==", + "dev": true, + "requires": { + "@babel/types": "^7.12.7" + } + }, + "@babel/helper-module-imports": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz", + "integrity": "sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA==", + "dev": true, + "requires": { + "@babel/types": "^7.12.5" + } + }, + "@babel/helper-module-transforms": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz", + "integrity": "sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.12.1", + "@babel/helper-replace-supers": "^7.12.1", + "@babel/helper-simple-access": "^7.12.1", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/helper-validator-identifier": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.12.1", + "@babel/types": "^7.12.1", + "lodash": "^4.17.19" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz", + "integrity": "sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==", + "dev": true, + "requires": { + "@babel/types": "^7.12.10" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.12.1.tgz", + "integrity": "sha512-9d0KQCRM8clMPcDwo8SevNs+/9a8yWVVmaE80FGJcEP8N1qToREmWEGnBn8BUlJhYRFz6fqxeRL1sl5Ogsed7A==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-wrap-function": "^7.10.4", + "@babel/types": "^7.12.1" + } + }, + "@babel/helper-replace-supers": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz", + "integrity": "sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.12.7", + "@babel/helper-optimise-call-expression": "^7.12.10", + "@babel/traverse": "^7.12.10", + "@babel/types": "^7.12.11" + } + }, + "@babel/helper-simple-access": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz", + "integrity": "sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA==", + "dev": true, + "requires": { + "@babel/types": "^7.12.1" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz", + "integrity": "sha512-Mf5AUuhG1/OCChOJ/HcADmvcHM42WJockombn8ATJG3OnyiSxBK/Mm5x78BQWvmtXZKHgbjdGL2kin/HOLlZGA==", + "dev": true, + "requires": { + "@babel/types": "^7.12.1" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz", + "integrity": "sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g==", + "dev": true, + "requires": { + "@babel/types": "^7.12.11" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.11.tgz", + "integrity": "sha512-TBFCyj939mFSdeX7U7DDj32WtzYY7fDcalgq8v3fBZMNOJQNn7nOYzMaUCiPxPYfCup69mtIpqlKgMZLvQ8Xhw==", + "dev": true + }, + "@babel/helper-wrap-function": { + "version": "7.12.3", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.12.3.tgz", + "integrity": "sha512-Cvb8IuJDln3rs6tzjW3Y8UeelAOdnpB8xtQ4sme2MSZ9wOxrbThporC0y/EtE16VAtoyEfLM404Xr1e0OOp+ow==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helpers": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.5.tgz", + "integrity": "sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA==", + "dev": true, + "requires": { + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.12.5", + "@babel/types": "^7.12.5" + } + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + } + } + }, + "@babel/parser": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz", + "integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==", + "dev": true + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.12.tgz", + "integrity": "sha512-nrz9y0a4xmUrRq51bYkWJIO5SBZyG2ys2qinHsN0zHDHVsUaModrkpyWWWXfGqYQmOL3x9sQIcTNN/pBGpo09A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-remap-async-to-generator": "^7.12.1", + "@babel/plugin-syntax-async-generators": "^7.8.0" + } + }, + "@babel/plugin-proposal-class-properties": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.12.1.tgz", + "integrity": "sha512-cKp3dlQsFsEs5CWKnN7BnSHOd0EOW8EKpEjkoz1pO2E5KzIDNV9Ros1b0CnmbVgAGXJubOYVBOGCT1OmJwOI7w==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.12.1.tgz", + "integrity": "sha512-a4rhUSZFuq5W8/OO8H7BL5zspjnc1FLd9hlOxIK/f7qG4a0qsqk8uvF/ywgBA8/OmjsapjpvaEOYItfGG1qIvQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-dynamic-import": "^7.8.0" + } + }, + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.12.1.tgz", + "integrity": "sha512-6CThGf0irEkzujYS5LQcjBx8j/4aQGiVv7J9+2f7pGfxqyKh3WnmVJYW3hdrQjyksErMGBPQrCnHfOtna+WLbw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.12.1.tgz", + "integrity": "sha512-GoLDUi6U9ZLzlSda2Df++VSqDJg3CG+dR0+iWsv6XRw1rEq+zwt4DirM9yrxW6XWaTpmai1cWJLMfM8qQJf+yw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.0" + } + }, + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.12.1.tgz", + "integrity": "sha512-k8ZmVv0JU+4gcUGeCDZOGd0lCIamU/sMtIiX3UWnUc5yzgq6YUGyEolNYD+MLYKfSzgECPcqetVcJP9Afe/aCA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.12.1.tgz", + "integrity": "sha512-nZY0ESiaQDI1y96+jk6VxMOaL4LPo/QDHBqL+SF3/vl6dHkTwHlOI8L4ZwuRBHgakRBw5zsVylel7QPbbGuYgg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0" + } + }, + "@babel/plugin-proposal-numeric-separator": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.7.tgz", + "integrity": "sha512-8c+uy0qmnRTeukiGsjLGy6uVs/TFjJchGXUeBqlG4VWYOdJWkhhVPdQ3uHwbmalfJwv2JsV0qffXP4asRfL2SQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.12.1.tgz", + "integrity": "sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.0", + "@babel/plugin-transform-parameters": "^7.12.1" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.12.1.tgz", + "integrity": "sha512-hFvIjgprh9mMw5v42sJWLI1lzU5L2sznP805zeT6rySVRA0Y18StRhDqhSxlap0oVgItRsB6WSROp4YnJTJz0g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.0" + } + }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.7.tgz", + "integrity": "sha512-4ovylXZ0PWmwoOvhU2vhnzVNnm88/Sm9nx7V8BPgMvAzn5zDou3/Awy0EjglyubVHasJj+XCEkr/r1X3P5elCA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1", + "@babel/plugin-syntax-optional-chaining": "^7.8.0" + } + }, + "@babel/plugin-proposal-private-methods": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.12.1.tgz", + "integrity": "sha512-mwZ1phvH7/NHK6Kf8LP7MYDogGV+DKB1mryFOEwx5EBNQrosvIczzZFTUmWaeujd5xT6G1ELYWUz3CutMhjE1w==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.12.1.tgz", + "integrity": "sha512-MYq+l+PvHuw/rKUz1at/vb6nCnQ2gmJBNaM62z0OgH7B2W1D9pvkpYtlti9bGtizNIU1K3zm4bZF9F91efVY0w==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz", + "integrity": "sha512-U40A76x5gTwmESz+qiqssqmeEsKvcSyvtgktrm0uzcARAmM9I1jR221f6Oq+GmHrcD+LvZDag1UTOTe2fL3TeA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz", + "integrity": "sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.1.tgz", + "integrity": "sha512-i7ooMZFS+a/Om0crxZodrTzNEPJHZrlMVGMTEpFAj6rYY/bKCddB0Dk/YxfPuYXOopuhKk/e1jV6h+WUU9XN3A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.12.1.tgz", + "integrity": "sha512-5QB50qyN44fzzz4/qxDPQMBCTHgxg3n0xRBLJUmBlLoU/sFvxVWGZF/ZUfMVDQuJUKXaBhbupxIzIfZ6Fwk/0A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.12.1.tgz", + "integrity": "sha512-SDtqoEcarK1DFlRJ1hHRY5HvJUj5kX4qmtpMAm2QnhOlyuMC4TMdCRgW6WXpv93rZeYNeLP22y8Aq2dbcDRM1A==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-remap-async-to-generator": "^7.12.1" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.12.1.tgz", + "integrity": "sha512-5OpxfuYnSgPalRpo8EWGPzIYf0lHBWORCkj5M0oLBwHdlux9Ri36QqGW3/LR13RSVOAoUUMzoPI/jpE4ABcHoA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.12.tgz", + "integrity": "sha512-VOEPQ/ExOVqbukuP7BYJtI5ZxxsmegTwzZ04j1aF0dkSypGo9XpDHuOrABsJu+ie+penpSJheDJ11x1BEZNiyQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.12.1.tgz", + "integrity": "sha512-/74xkA7bVdzQTBeSUhLLJgYIcxw/dpEpCdRDiHgPJ3Mv6uC11UhjpOhl72CgqbBCmt1qtssCyB2xnJm1+PFjog==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-define-map": "^7.10.4", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-replace-supers": "^7.12.1", + "@babel/helper-split-export-declaration": "^7.10.4", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.12.1.tgz", + "integrity": "sha512-vVUOYpPWB7BkgUWPo4C44mUQHpTZXakEqFjbv8rQMg7TC6S6ZhGZ3otQcRH6u7+adSlE5i0sp63eMC/XGffrzg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.12.1.tgz", + "integrity": "sha512-fRMYFKuzi/rSiYb2uRLiUENJOKq4Gnl+6qOv5f8z0TZXg3llUwUhsNNwrwaT/6dUhJTzNpBr+CUvEWBtfNY1cw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.12.1.tgz", + "integrity": "sha512-B2pXeRKoLszfEW7J4Hg9LoFaWEbr/kzo3teWHmtFCszjRNa/b40f9mfeqZsIDLLt/FjwQ6pz/Gdlwy85xNckBA==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.12.1.tgz", + "integrity": "sha512-iRght0T0HztAb/CazveUpUQrZY+aGKKaWXMJ4uf9YJtqxSUe09j3wteztCUDRHs+SRAL7yMuFqUsLoAKKzgXjw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.12.1.tgz", + "integrity": "sha512-7tqwy2bv48q+c1EHbXK0Zx3KXd2RVQp6OC7PbwFNt/dPTAV3Lu5sWtWuAj8owr5wqtWnqHfl2/mJlUmqkChKug==", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.12.1.tgz", + "integrity": "sha512-Zaeq10naAsuHo7heQvyV0ptj4dlZJwZgNAtBYBnu5nNKJoW62m0zKcIEyVECrUKErkUkg6ajMy4ZfnVZciSBhg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.12.1.tgz", + "integrity": "sha512-JF3UgJUILoFrFMEnOJLJkRHSk6LUSXLmEFsA23aR2O5CSLUxbeUX1IZ1YQ7Sn0aXb601Ncwjx73a+FVqgcljVw==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.12.1.tgz", + "integrity": "sha512-+PxVGA+2Ag6uGgL0A5f+9rklOnnMccwEBzwYFL3EUaKuiyVnUipyXncFcfjSkbimLrODoqki1U9XxZzTvfN7IQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.12.1.tgz", + "integrity": "sha512-1sxePl6z9ad0gFMB9KqmYofk34flq62aqMt9NqliS/7hPEpURUCMbyHXrMPlo282iY7nAvUB1aQd5mg79UD9Jg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.12.1.tgz", + "integrity": "sha512-tDW8hMkzad5oDtzsB70HIQQRBiTKrhfgwC/KkJeGsaNFTdWhKNt/BiE8c5yj19XiGyrxpbkOfH87qkNg1YGlOQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.12.1.tgz", + "integrity": "sha512-dY789wq6l0uLY8py9c1B48V8mVL5gZh/+PQ5ZPrylPYsnAvnEMjqsUXkuoDVPeVK+0VyGar+D08107LzDQ6pag==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-simple-access": "^7.12.1", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.12.1.tgz", + "integrity": "sha512-Hn7cVvOavVh8yvW6fLwveFqSnd7rbQN3zJvoPNyNaQSvgfKmDBO9U1YL9+PCXGRlZD9tNdWTy5ACKqMuzyn32Q==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.10.4", + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-validator-identifier": "^7.10.4", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.12.1.tgz", + "integrity": "sha512-aEIubCS0KHKM0zUos5fIoQm+AZUMt1ZvMpqz0/H5qAQ7vWylr9+PLYurT+Ic7ID/bKLd4q8hDovaG3Zch2uz5Q==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.12.1.tgz", + "integrity": "sha512-tB43uQ62RHcoDp9v2Nsf+dSM8sbNodbEicbQNA53zHz8pWUhsgHSJCGpt7daXxRydjb0KnfmB+ChXOv3oADp1Q==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.12.1" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.12.1.tgz", + "integrity": "sha512-+eW/VLcUL5L9IvJH7rT1sT0CzkdUTvPrXC2PXTn/7z7tXLBuKvezYbGdxD5WMRoyvyaujOq2fWoKl869heKjhw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.12.1.tgz", + "integrity": "sha512-AvypiGJH9hsquNUn+RXVcBdeE3KHPZexWRdimhuV59cSoOt5kFBmqlByorAeUlGG2CJWd0U+4ZtNKga/TB0cAw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-replace-supers": "^7.12.1" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.12.1.tgz", + "integrity": "sha512-xq9C5EQhdPK23ZeCdMxl8bbRnAgHFrw5EOC3KJUsSylZqdkCaFEXxGSBuTSObOpiiHHNyb82es8M1QYgfQGfNg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.12.1.tgz", + "integrity": "sha512-6MTCR/mZ1MQS+AwZLplX4cEySjCpnIF26ToWo942nqn8hXSm7McaHQNeGx/pt7suI1TWOWMfa/NgBhiqSnX0cQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-react-constant-elements": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.12.1.tgz", + "integrity": "sha512-KOHd0tIRLoER+J+8f9DblZDa1fLGPwaaN1DI1TVHuQFOpjHV22C3CUB3obeC4fexHY9nx+fH0hQNvLFFfA1mxA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-react-display-name": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.12.1.tgz", + "integrity": "sha512-cAzB+UzBIrekfYxyLlFqf/OagTvHLcVBb5vpouzkYkBclRPraiygVnafvAoipErZLI8ANv8Ecn6E/m5qPXD26w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-react-jsx": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.12.12.tgz", + "integrity": "sha512-JDWGuzGNWscYcq8oJVCtSE61a5+XAOos+V0HrxnDieUus4UMnBEosDnY1VJqU5iZ4pA04QY7l0+JvHL1hZEfsw==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.12.10", + "@babel/helper-module-imports": "^7.12.5", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-jsx": "^7.12.1", + "@babel/types": "^7.12.12" + } + }, + "@babel/plugin-transform-react-jsx-development": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.12.tgz", + "integrity": "sha512-i1AxnKxHeMxUaWVXQOSIco4tvVvvCxMSfeBMnMM06mpaJt3g+MpxYQQrDfojUQldP1xxraPSJYSMEljoWM/dCg==", + "dev": true, + "requires": { + "@babel/plugin-transform-react-jsx": "^7.12.12" + } + }, + "@babel/plugin-transform-react-pure-annotations": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.12.1.tgz", + "integrity": "sha512-RqeaHiwZtphSIUZ5I85PEH19LOSzxfuEazoY7/pWASCAIBuATQzpSVD+eT6MebeeZT2F4eSL0u4vw6n4Nm0Mjg==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.12.1.tgz", + "integrity": "sha512-gYrHqs5itw6i4PflFX3OdBPMQdPbF4bj2REIUxlMRUFk0/ZOAIpDFuViuxPjUL7YC8UPnf+XG7/utJvqXdPKng==", + "dev": true, + "requires": { + "regenerator-transform": "^0.14.2" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.12.1.tgz", + "integrity": "sha512-pOnUfhyPKvZpVyBHhSBoX8vfA09b7r00Pmm1sH+29ae2hMTKVmSp4Ztsr8KBKjLjx17H0eJqaRC3bR2iThM54A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.12.1.tgz", + "integrity": "sha512-GFZS3c/MhX1OusqB1MZ1ct2xRzX5ppQh2JU1h2Pnfk88HtFTM+TWQqJNfwkmxtPQtb/s1tk87oENfXJlx7rSDw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.12.1.tgz", + "integrity": "sha512-vuLp8CP0BE18zVYjsEBZ5xoCecMK6LBMMxYzJnh01rxQRvhNhH1csMMmBfNo5tGpGO+NhdSNW2mzIvBu3K1fng==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.7.tgz", + "integrity": "sha512-VEiqZL5N/QvDbdjfYQBhruN0HYjSPjC4XkeqW4ny/jNtH9gcbgaqBIXYEZCNnESMAGs0/K/R7oFGMhOyu/eIxg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.12.1.tgz", + "integrity": "sha512-b4Zx3KHi+taXB1dVRBhVJtEPi9h1THCeKmae2qP0YdUHIFhVjtpqqNfxeVAa1xeHVhAy4SbHxEwx5cltAu5apw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.10.tgz", + "integrity": "sha512-JQ6H8Rnsogh//ijxspCjc21YPd3VLVoYtAwv3zQmqAt8YGYUtdo5usNhdl4b9/Vir2kPFZl6n1h0PfUz4hJhaA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.1.tgz", + "integrity": "sha512-I8gNHJLIc7GdApm7wkVnStWssPNbSRMPtgHdmH3sRM1zopz09UWPS4x5V4n1yz/MIWTVnJ9sp6IkuXdWM4w+2Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.12.1.tgz", + "integrity": "sha512-SqH4ClNngh/zGwHZOOQMTD+e8FGWexILV+ePMyiDJttAWRh5dhDL8rcl5lSgU3Huiq6Zn6pWTMvdPAb21Dwdyg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/preset-env": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.12.11.tgz", + "integrity": "sha512-j8Tb+KKIXKYlDBQyIOy4BLxzv1NUOwlHfZ74rvW+Z0Gp4/cI2IMDPBWAgWceGcE7aep9oL/0K9mlzlMGxA8yNw==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.12.7", + "@babel/helper-compilation-targets": "^7.12.5", + "@babel/helper-module-imports": "^7.12.5", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-validator-option": "^7.12.11", + "@babel/plugin-proposal-async-generator-functions": "^7.12.1", + "@babel/plugin-proposal-class-properties": "^7.12.1", + "@babel/plugin-proposal-dynamic-import": "^7.12.1", + "@babel/plugin-proposal-export-namespace-from": "^7.12.1", + "@babel/plugin-proposal-json-strings": "^7.12.1", + "@babel/plugin-proposal-logical-assignment-operators": "^7.12.1", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1", + "@babel/plugin-proposal-numeric-separator": "^7.12.7", + "@babel/plugin-proposal-object-rest-spread": "^7.12.1", + "@babel/plugin-proposal-optional-catch-binding": "^7.12.1", + "@babel/plugin-proposal-optional-chaining": "^7.12.7", + "@babel/plugin-proposal-private-methods": "^7.12.1", + "@babel/plugin-proposal-unicode-property-regex": "^7.12.1", + "@babel/plugin-syntax-async-generators": "^7.8.0", + "@babel/plugin-syntax-class-properties": "^7.12.1", + "@babel/plugin-syntax-dynamic-import": "^7.8.0", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.0", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.0", + "@babel/plugin-syntax-top-level-await": "^7.12.1", + "@babel/plugin-transform-arrow-functions": "^7.12.1", + "@babel/plugin-transform-async-to-generator": "^7.12.1", + "@babel/plugin-transform-block-scoped-functions": "^7.12.1", + "@babel/plugin-transform-block-scoping": "^7.12.11", + "@babel/plugin-transform-classes": "^7.12.1", + "@babel/plugin-transform-computed-properties": "^7.12.1", + "@babel/plugin-transform-destructuring": "^7.12.1", + "@babel/plugin-transform-dotall-regex": "^7.12.1", + "@babel/plugin-transform-duplicate-keys": "^7.12.1", + "@babel/plugin-transform-exponentiation-operator": "^7.12.1", + "@babel/plugin-transform-for-of": "^7.12.1", + "@babel/plugin-transform-function-name": "^7.12.1", + "@babel/plugin-transform-literals": "^7.12.1", + "@babel/plugin-transform-member-expression-literals": "^7.12.1", + "@babel/plugin-transform-modules-amd": "^7.12.1", + "@babel/plugin-transform-modules-commonjs": "^7.12.1", + "@babel/plugin-transform-modules-systemjs": "^7.12.1", + "@babel/plugin-transform-modules-umd": "^7.12.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.12.1", + "@babel/plugin-transform-new-target": "^7.12.1", + "@babel/plugin-transform-object-super": "^7.12.1", + "@babel/plugin-transform-parameters": "^7.12.1", + "@babel/plugin-transform-property-literals": "^7.12.1", + "@babel/plugin-transform-regenerator": "^7.12.1", + "@babel/plugin-transform-reserved-words": "^7.12.1", + "@babel/plugin-transform-shorthand-properties": "^7.12.1", + "@babel/plugin-transform-spread": "^7.12.1", + "@babel/plugin-transform-sticky-regex": "^7.12.7", + "@babel/plugin-transform-template-literals": "^7.12.1", + "@babel/plugin-transform-typeof-symbol": "^7.12.10", + "@babel/plugin-transform-unicode-escapes": "^7.12.1", + "@babel/plugin-transform-unicode-regex": "^7.12.1", + "@babel/preset-modules": "^0.1.3", + "@babel/types": "^7.12.11", + "core-js-compat": "^3.8.0", + "semver": "^5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "@babel/preset-modules": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.4.tgz", + "integrity": "sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + } + }, + "@babel/preset-react": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.12.10.tgz", + "integrity": "sha512-vtQNjaHRl4DUpp+t+g4wvTHsLQuye+n0H/wsXIZRn69oz/fvNC7gQ4IK73zGJBaxvHoxElDvnYCthMcT7uzFoQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-transform-react-display-name": "^7.12.1", + "@babel/plugin-transform-react-jsx": "^7.12.10", + "@babel/plugin-transform-react-jsx-development": "^7.12.7", + "@babel/plugin-transform-react-pure-annotations": "^7.12.1" + } + }, + "@babel/runtime": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", + "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@babel/runtime-corejs3": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.12.5.tgz", + "integrity": "sha512-roGr54CsTmNPPzZoCP1AmDXuBoNao7tnSA83TXTwt+UK5QVyh1DIJnrgYRPWKCF2flqZQXwa7Yr8v7VmLzF0YQ==", + "dev": true, + "requires": { + "core-js-pure": "^3.0.0", + "regenerator-runtime": "^0.13.4" + } + }, + "@babel/template": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz", + "integrity": "sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.12.7", + "@babel/types": "^7.12.7" + } + }, + "@babel/traverse": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.12.tgz", + "integrity": "sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.11", + "@babel/generator": "^7.12.11", + "@babel/helper-function-name": "^7.12.11", + "@babel/helper-split-export-declaration": "^7.12.11", + "@babel/parser": "^7.12.11", + "@babel/types": "^7.12.12", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", + "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "@backstage/backend-common": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@backstage/backend-common/-/backend-common-0.4.1.tgz", + "integrity": "sha512-tfQXg0KlqQ8MXi7h4vxGywMEt5JcMvnLfcupyUxWCjex0g6JkGUKIHwgmDDrjAFSW+mIXQT1BXJ4uxX7OYOnHA==", + "requires": { + "@backstage/cli-common": "^0.1.1", + "@backstage/config": "^0.1.2", + "@backstage/config-loader": "^0.4.0", + "@backstage/integration": "^0.1.4", + "@types/cors": "^2.8.6", + "@types/express": "^4.17.6", + "archiver": "^5.0.2", + "compression": "^1.7.4", + "concat-stream": "^2.0.0", + "cors": "^2.8.5", + "cross-fetch": "^3.0.6", + "express": "^4.17.1", + "express-promise-router": "^3.0.3", + "fs-extra": "^9.0.1", + "git-url-parse": "^11.4.3", + "helmet": "^4.0.0", + "knex": "^0.21.6", + "lodash": "^4.17.15", + "logform": "^2.1.1", + "minimist": "^1.2.5", + "morgan": "^1.10.0", + "selfsigned": "^1.10.7", + "stoppable": "^1.1.0", + "tar": "^6.0.5", + "unzipper": "^0.10.11", + "winston": "^3.2.1" + } + }, + "@backstage/catalog-model": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.6.0.tgz", + "integrity": "sha512-VTcghDw2uJ451rnOf7+QLoWwkZVq9v/7fzfDgg01ItEz/AfA67zqRcXwygdk2kI0wnhciMwb912ZueGQNbOXPQ==", + "requires": { + "@backstage/config": "^0.1.2", + "@types/json-schema": "^7.0.5", + "@types/yup": "^0.29.8", + "json-schema": "^0.2.5", + "lodash": "^4.17.15", + "uuid": "^8.0.0", + "yup": "^0.29.3" + } + }, + "@backstage/cli": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@backstage/cli/-/cli-0.4.4.tgz", + "integrity": "sha512-ktCGbXjIa/xOQyxlIVPGUnekdnLMvCENhmQ7eX3Yjavux4YBeARmqBscmey+TpvSSYotQetbNwTaJyj1K3yxSw==", + "dev": true, + "requires": { + "@backstage/cli-common": "^0.1.1", + "@backstage/config": "^0.1.2", + "@backstage/config-loader": "^0.4.0", + "@hot-loader/react-dom": "^16.13.0", + "@lerna/package-graph": "^3.18.5", + "@lerna/project": "^3.18.0", + "@rollup/plugin-commonjs": "^16.0.0", + "@rollup/plugin-json": "^4.0.2", + "@rollup/plugin-node-resolve": "^9.0.0", + "@rollup/plugin-yaml": "^2.1.1", + "@spotify/eslint-config-base": "^9.0.0", + "@spotify/eslint-config-react": "^9.0.0", + "@spotify/eslint-config-typescript": "^9.0.0", + "@sucrase/webpack-loader": "^2.0.0", + "@svgr/plugin-jsx": "5.4.x", + "@svgr/plugin-svgo": "5.4.x", + "@svgr/rollup": "5.5.x", + "@svgr/webpack": "5.4.x", + "@types/start-server-webpack-plugin": "^2.2.0", + "@types/webpack-env": "^1.15.2", + "@types/webpack-node-externals": "^2.5.0", + "@typescript-eslint/eslint-plugin": "^v3.10.1", + "@typescript-eslint/parser": "^v3.10.1", + "@yarnpkg/lockfile": "^1.1.0", + "bfj": "^7.0.2", + "chalk": "^4.0.0", + "chokidar": "^3.3.1", + "commander": "^6.1.0", + "css-loader": "^3.5.3", + "dashify": "^2.0.0", + "diff": "^4.0.2", + "esbuild": "^0.8.16", + "eslint": "^7.1.0", + "eslint-config-prettier": "^6.0.0", + "eslint-formatter-friendly": "^7.0.0", + "eslint-plugin-import": "^2.20.2", + "eslint-plugin-jest": "^24.1.0", + "eslint-plugin-jsx-a11y": "^6.2.1", + "eslint-plugin-monorepo": "^0.3.2", + "eslint-plugin-react": "^7.12.4", + "eslint-plugin-react-hooks": "^4.0.0", + "fork-ts-checker-webpack-plugin": "^4.0.5", + "fs-extra": "^9.0.0", + "handlebars": "^4.7.3", + "html-webpack-plugin": "^4.3.0", + "inquirer": "^7.0.4", + "jest": "^26.0.1", + "jest-css-modules": "^2.1.0", + "jest-esm-transformer": "^1.0.0", + "lodash": "^4.17.19", + "mini-css-extract-plugin": "^0.9.0", + "ora": "^4.0.3", + "raw-loader": "^4.0.1", + "react": "^16.0.0", + "react-dev-utils": "^10.2.1", + "react-hot-loader": "^4.12.21", + "recursive-readdir": "^2.2.2", + "replace-in-file": "^6.0.0", + "rollup": "2.33.x", + "rollup-plugin-dts": "1.4.13", + "rollup-plugin-esbuild": "2.6.x", + "rollup-plugin-peer-deps-external": "^2.2.2", + "rollup-plugin-postcss": "^3.1.1", + "rollup-plugin-typescript2": "^0.27.3", + "rollup-pluginutils": "^2.8.2", + "semver": "^7.3.2", + "start-server-webpack-plugin": "^2.2.5", + "style-loader": "^1.2.1", + "sucrase": "^3.16.0", + "tar": "^6.0.1", + "terser-webpack-plugin": "^1.4.3", + "ts-jest": "^26.4.3", + "ts-loader": "^7.0.4", + "typescript": "^4.0.3", + "url-loader": "^4.1.0", + "webpack": "^4.41.6", + "webpack-dev-server": "^3.11.0", + "webpack-node-externals": "^2.5.0", + "yaml": "^1.10.0", + "yml-loader": "^2.1.0", + "yn": "^4.0.0" + } + }, + "@backstage/cli-common": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@backstage/cli-common/-/cli-common-0.1.1.tgz", + "integrity": "sha512-JxbSUkJIXQqamgiE38MtlMn2/BdpLaqMwwi429wZfNg8upfONIrN2BW9qMb8G9CkWdI4ssUfIem1HAtDqhdDvQ==" + }, + "@backstage/config": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@backstage/config/-/config-0.1.2.tgz", + "integrity": "sha512-JI92C2YbchNzByowdDzZlULh1hwP1Q0ax6DqbwBPJ2BrOrM+9fCJxFQx4xVm2b+OILpIZZKX77GxO0PbamxnZg==", + "requires": { + "lodash": "^4.17.15" + } + }, + "@backstage/config-loader": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@backstage/config-loader/-/config-loader-0.4.0.tgz", + "integrity": "sha512-MSV3Tk1PyvE54KFPPZuSK0QDJ9LNG3pAYePxGOZf152hpbBEH0krHuz4OJ+AjkqyO1B0Epm4LnmW4Xu7aVZP/Q==", + "requires": { + "@backstage/cli-common": "^0.1.1", + "@backstage/config": "^0.1.1", + "ajv": "^6.12.5", + "fs-extra": "^9.0.0", + "json-schema": "^0.2.5", + "json-schema-merge-allof": "^0.7.0", + "typescript-json-schema": "^0.45.0", + "yaml": "^1.9.2", + "yup": "^0.29.3" + } + }, + "@backstage/integration": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@backstage/integration/-/integration-0.1.4.tgz", + "integrity": "sha512-FbIX2R9Roi+FyaWWaDJ3Xbkq0Sc5vKZPb/tGy6FVnG4KJ9o2pncZDzdMC743nBlIv74V5LYoKJ8YmVbNmud6Ew==", + "requires": { + "@backstage/config": "^0.1.2", + "cross-fetch": "^3.0.6", + "git-url-parse": "^11.4.3" + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "@cnakazawa/watch": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", + "integrity": "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==", + "dev": true, + "requires": { + "exec-sh": "^0.3.2", + "minimist": "^1.2.0" + } + }, + "@dabh/diagnostics": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz", + "integrity": "sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q==", + "requires": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "@eslint/eslintrc": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.2.2.tgz", + "integrity": "sha512-EfB5OHNYp1F4px/LI/FEnGylop7nOqkQ1LRzCM0KccA2U8tvV8w01KBv37LbO7nW4H+YhKyo2LcJhRwjjV17QQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } + } + }, + "@hot-loader/react-dom": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/@hot-loader/react-dom/-/react-dom-16.14.0.tgz", + "integrity": "sha512-EN9czvcLsMYmSDo5yRKZOAq3ZGRlDpad1gPtX0NdMMomJXcPE3yFSeFzE94X/NjOaiSVimB7LuqPYpkWVaIi4Q==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.19.1" + } + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", + "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", + "dev": true + }, + "@jest/console": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-26.6.2.tgz", + "integrity": "sha512-IY1R2i2aLsLr7Id3S6p2BA82GNWryt4oSvEXLAKc+L2zdi89dSkE8xC1C+0kpATG4JhBJREnQOH7/zmccM2B0g==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^26.6.2", + "jest-util": "^26.6.2", + "slash": "^3.0.0" + }, + "dependencies": { + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + } + } + }, + "@jest/core": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-26.6.3.tgz", + "integrity": "sha512-xvV1kKbhfUqFVuZ8Cyo+JPpipAHHAV3kcDBftiduK8EICXmTFddryy3P7NfZt8Pv37rA9nEJBKCCkglCPt/Xjw==", + "dev": true, + "requires": { + "@jest/console": "^26.6.2", + "@jest/reporters": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/transform": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "jest-changed-files": "^26.6.2", + "jest-config": "^26.6.3", + "jest-haste-map": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-regex-util": "^26.0.0", + "jest-resolve": "^26.6.2", + "jest-resolve-dependencies": "^26.6.3", + "jest-runner": "^26.6.3", + "jest-runtime": "^26.6.3", + "jest-snapshot": "^26.6.2", + "jest-util": "^26.6.2", + "jest-validate": "^26.6.2", + "jest-watcher": "^26.6.2", + "micromatch": "^4.0.2", + "p-each-series": "^2.1.0", + "rimraf": "^3.0.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "@jest/environment": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-26.6.2.tgz", + "integrity": "sha512-nFy+fHl28zUrRsCeMB61VDThV1pVTtlEokBRgqPrcT1JNq4yRNIyTHfyht6PqtUvY9IsuLGTrbG8kPXjSZIZwA==", + "dev": true, + "requires": { + "@jest/fake-timers": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "jest-mock": "^26.6.2" + } + }, + "@jest/fake-timers": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-26.6.2.tgz", + "integrity": "sha512-14Uleatt7jdzefLPYM3KLcnUl1ZNikaKq34enpb5XG9i81JpppDb5muZvonvKyrl7ftEHkKS5L5/eB/kxJ+bvA==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@sinonjs/fake-timers": "^6.0.1", + "@types/node": "*", + "jest-message-util": "^26.6.2", + "jest-mock": "^26.6.2", + "jest-util": "^26.6.2" + } + }, + "@jest/globals": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-26.6.2.tgz", + "integrity": "sha512-85Ltnm7HlB/KesBUuALwQ68YTU72w9H2xW9FjZ1eL1U3lhtefjjl5c2MiUbpXt/i6LaPRvoOFJ22yCBSfQ0JIA==", + "dev": true, + "requires": { + "@jest/environment": "^26.6.2", + "@jest/types": "^26.6.2", + "expect": "^26.6.2" + } + }, + "@jest/reporters": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-26.6.2.tgz", + "integrity": "sha512-h2bW53APG4HvkOnVMo8q3QXa6pcaNt1HkwVsOPMBV6LD/q9oSpxNSYZQYkAnjdMjrJ86UuYeLo+aEZClV6opnw==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/transform": "^26.6.2", + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.2", + "graceful-fs": "^4.2.4", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^4.0.3", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "jest-haste-map": "^26.6.2", + "jest-resolve": "^26.6.2", + "jest-util": "^26.6.2", + "jest-worker": "^26.6.2", + "node-notifier": "^8.0.0", + "slash": "^3.0.0", + "source-map": "^0.6.0", + "string-length": "^4.0.1", + "terminal-link": "^2.0.0", + "v8-to-istanbul": "^7.0.0" + }, + "dependencies": { + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@jest/source-map": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-26.6.2.tgz", + "integrity": "sha512-YwYcCwAnNmOVsZ8mr3GfnzdXDAl4LaenZP5z+G0c8bzC9/dugL8zRmxZzdoTl4IaS3CryS1uWnROLPFmb6lVvA==", + "dev": true, + "requires": { + "callsites": "^3.0.0", + "graceful-fs": "^4.2.4", + "source-map": "^0.6.0" + }, + "dependencies": { + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@jest/test-result": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-26.6.2.tgz", + "integrity": "sha512-5O7H5c/7YlojphYNrK02LlDIV2GNPYisKwHm2QTKjNZeEzezCbwYs9swJySv2UfPMyZ0VdsmMv7jIlD/IKYQpQ==", + "dev": true, + "requires": { + "@jest/console": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-26.6.3.tgz", + "integrity": "sha512-YHlVIjP5nfEyjlrSr8t/YdNfU/1XEt7c5b4OxcXCjyRhjzLYu/rO69/WHPuYcbCWkz8kAeZVZp2N2+IOLLEPGw==", + "dev": true, + "requires": { + "@jest/test-result": "^26.6.2", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^26.6.2", + "jest-runner": "^26.6.3", + "jest-runtime": "^26.6.3" + } + }, + "@jest/transform": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-26.6.2.tgz", + "integrity": "sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/types": "^26.6.2", + "babel-plugin-istanbul": "^6.0.0", + "chalk": "^4.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^26.6.2", + "jest-regex-util": "^26.0.0", + "jest-util": "^26.6.2", + "micromatch": "^4.0.2", + "pirates": "^4.0.1", + "slash": "^3.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "^3.0.0" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + } + } + }, + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@lerna/package": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@lerna/package/-/package-3.16.0.tgz", + "integrity": "sha512-2lHBWpaxcBoiNVbtyLtPUuTYEaB/Z+eEqRS9duxpZs6D+mTTZMNy6/5vpEVSCBmzvdYpyqhqaYjjSLvjjr5Riw==", + "dev": true, + "requires": { + "load-json-file": "^5.3.0", + "npm-package-arg": "^6.1.0", + "write-pkg": "^3.1.0" + } + }, + "@lerna/package-graph": { + "version": "3.18.5", + "resolved": "https://registry.npmjs.org/@lerna/package-graph/-/package-graph-3.18.5.tgz", + "integrity": "sha512-8QDrR9T+dBegjeLr+n9WZTVxUYUhIUjUgZ0gvNxUBN8S1WB9r6H5Yk56/MVaB64tA3oGAN9IIxX6w0WvTfFudA==", + "dev": true, + "requires": { + "@lerna/prerelease-id-from-version": "3.16.0", + "@lerna/validation-error": "3.13.0", + "npm-package-arg": "^6.1.0", + "npmlog": "^4.1.2", + "semver": "^6.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@lerna/prerelease-id-from-version": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@lerna/prerelease-id-from-version/-/prerelease-id-from-version-3.16.0.tgz", + "integrity": "sha512-qZyeUyrE59uOK8rKdGn7jQz+9uOpAaF/3hbslJVFL1NqF9ELDTqjCPXivuejMX/lN4OgD6BugTO4cR7UTq/sZA==", + "dev": true, + "requires": { + "semver": "^6.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@lerna/project": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@lerna/project/-/project-3.21.0.tgz", + "integrity": "sha512-xT1mrpET2BF11CY32uypV2GPtPVm6Hgtha7D81GQP9iAitk9EccrdNjYGt5UBYASl4CIDXBRxwmTTVGfrCx82A==", + "dev": true, + "requires": { + "@lerna/package": "3.16.0", + "@lerna/validation-error": "3.13.0", + "cosmiconfig": "^5.1.0", + "dedent": "^0.7.0", + "dot-prop": "^4.2.0", + "glob-parent": "^5.0.0", + "globby": "^9.2.0", + "load-json-file": "^5.3.0", + "npmlog": "^4.1.2", + "p-map": "^2.1.0", + "resolve-from": "^4.0.0", + "write-json-file": "^3.2.0" + } + }, + "@lerna/validation-error": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@lerna/validation-error/-/validation-error-3.13.0.tgz", + "integrity": "sha512-SiJP75nwB8GhgwLKQfdkSnDufAaCbkZWJqEDlKOUPUvVOplRGnfL+BPQZH5nvq2BYSRXsksXWZ4UHVnQZI/HYA==", + "dev": true, + "requires": { + "npmlog": "^4.1.2" + } + }, + "@mrmlnc/readdir-enhanced": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", + "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", + "dev": true, + "requires": { + "call-me-maybe": "^1.0.1", + "glob-to-regexp": "^0.3.0" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", + "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.4", + "run-parallel": "^1.1.9" + }, + "dependencies": { + "@nodelib/fs.stat": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", + "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==", + "dev": true + } + } + }, + "@nodelib/fs.stat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", + "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz", + "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.4", + "fastq": "^1.6.0" + } + }, + "@rollup/plugin-commonjs": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-16.0.0.tgz", + "integrity": "sha512-LuNyypCP3msCGVQJ7ki8PqYdpjfEkE/xtFa5DqlF+7IBD0JsfMZ87C58heSwIMint58sAUZbt3ITqOmdQv/dXw==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "commondir": "^1.0.1", + "estree-walker": "^2.0.1", + "glob": "^7.1.6", + "is-reference": "^1.2.1", + "magic-string": "^0.25.7", + "resolve": "^1.17.0" + } + }, + "@rollup/plugin-json": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-4.1.0.tgz", + "integrity": "sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.0.8" + } + }, + "@rollup/plugin-node-resolve": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-9.0.0.tgz", + "integrity": "sha512-gPz+utFHLRrd41WMP13Jq5mqqzHL3OXrfj3/MkSyB6UBIcuNt9j60GCbarzMzdf1VHFpOxfQh/ez7wyadLMqkg==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "builtin-modules": "^3.1.0", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.17.0" + } + }, + "@rollup/plugin-yaml": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-yaml/-/plugin-yaml-2.1.1.tgz", + "integrity": "sha512-CnGD3dbDhP+JeKFX7kJuaBOa7jVzXpl3G8lXp1hasB87cDYOz7qNWd4cFPmYbxMsT/8OCVsceIpRtQ+cj9uxBw==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.0.1", + "js-yaml": "^3.13.1", + "tosource": "^1.0.0" + } + }, + "@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "requires": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "dependencies": { + "estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + } + } + }, + "@sinonjs/commons": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz", + "integrity": "sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", + "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@spotify/eslint-config-base": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@spotify/eslint-config-base/-/eslint-config-base-9.0.0.tgz", + "integrity": "sha512-LNGY6bBM0IlosvMIx9Ic5gqatpa5LPvrJhN4RnrUnP9oaXNLgPTKDE89p3eC5Tnh78ADdX4HD+rbHDSyP2tBgg==", + "dev": true + }, + "@spotify/eslint-config-react": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@spotify/eslint-config-react/-/eslint-config-react-9.0.0.tgz", + "integrity": "sha512-3w+cmbUljBLxZGLk4dn1oJD8v4uxW7bKackkcpZhac6VkQmg01EDN04Bx9qmbNe5S/2uHwrOJGcWlge5R8Ml1Q==", + "dev": true + }, + "@spotify/eslint-config-typescript": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@spotify/eslint-config-typescript/-/eslint-config-typescript-9.0.0.tgz", + "integrity": "sha512-ZsXTwMA68ZCz943U4N8XwprdWcc7ErOO/IW8PewLK5lycCZtLnmRkOvAbae7O5qNJPD8b/l0iUMTLaZuwjXWwg==", + "dev": true + }, + "@sucrase/webpack-loader": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sucrase/webpack-loader/-/webpack-loader-2.0.0.tgz", + "integrity": "sha512-KUfWr83g70Qm+ZqjGL+M4tX01taDP3BldQcI6NSMlDf7WTDfuo0RvLlS0ekF6dPVslNyZhbFFBy2OBTB6Sa6+Q==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0" + } + }, + "@svgr/babel-plugin-add-jsx-attribute": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz", + "integrity": "sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg==", + "dev": true + }, + "@svgr/babel-plugin-remove-jsx-attribute": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz", + "integrity": "sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg==", + "dev": true + }, + "@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz", + "integrity": "sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA==", + "dev": true + }, + "@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz", + "integrity": "sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ==", + "dev": true + }, + "@svgr/babel-plugin-svg-dynamic-title": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz", + "integrity": "sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg==", + "dev": true + }, + "@svgr/babel-plugin-svg-em-dimensions": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz", + "integrity": "sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw==", + "dev": true + }, + "@svgr/babel-plugin-transform-react-native-svg": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz", + "integrity": "sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q==", + "dev": true + }, + "@svgr/babel-plugin-transform-svg-component": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz", + "integrity": "sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ==", + "dev": true + }, + "@svgr/babel-preset": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.5.0.tgz", + "integrity": "sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig==", + "dev": true, + "requires": { + "@svgr/babel-plugin-add-jsx-attribute": "^5.4.0", + "@svgr/babel-plugin-remove-jsx-attribute": "^5.4.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "^5.0.1", + "@svgr/babel-plugin-replace-jsx-attribute-value": "^5.0.1", + "@svgr/babel-plugin-svg-dynamic-title": "^5.4.0", + "@svgr/babel-plugin-svg-em-dimensions": "^5.4.0", + "@svgr/babel-plugin-transform-react-native-svg": "^5.4.0", + "@svgr/babel-plugin-transform-svg-component": "^5.5.0" + } + }, + "@svgr/core": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-5.5.0.tgz", + "integrity": "sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ==", + "dev": true, + "requires": { + "@svgr/plugin-jsx": "^5.5.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^7.0.0" + }, + "dependencies": { + "@svgr/plugin-jsx": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz", + "integrity": "sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@svgr/babel-preset": "^5.5.0", + "@svgr/hast-util-to-babel-ast": "^5.5.0", + "svg-parser": "^2.0.2" + } + }, + "cosmiconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", + "integrity": "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "parse-json": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", + "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + } + } + }, + "@svgr/hast-util-to-babel-ast": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz", + "integrity": "sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ==", + "dev": true, + "requires": { + "@babel/types": "^7.12.6" + } + }, + "@svgr/plugin-jsx": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.4.0.tgz", + "integrity": "sha512-SGzO4JZQ2HvGRKDzRga9YFSqOqaNrgLlQVaGvpZ2Iht2gwRp/tq+18Pvv9kS9ZqOMYgyix2LLxZMY1LOe9NPqw==", + "dev": true, + "requires": { + "@babel/core": "^7.7.5", + "@svgr/babel-preset": "^5.4.0", + "@svgr/hast-util-to-babel-ast": "^5.4.0", + "svg-parser": "^2.0.2" + } + }, + "@svgr/plugin-svgo": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-5.4.0.tgz", + "integrity": "sha512-3Cgv3aYi1l6SHyzArV9C36yo4kgwVdF3zPQUC6/aCDUeXAofDYwE5kk3e3oT5ZO2a0N3lB+lLGvipBG6lnG8EA==", + "dev": true, + "requires": { + "cosmiconfig": "^6.0.0", + "merge-deep": "^3.0.2", + "svgo": "^1.2.2" + }, + "dependencies": { + "cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + } + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "parse-json": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", + "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + } + } + }, + "@svgr/rollup": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/rollup/-/rollup-5.5.0.tgz", + "integrity": "sha512-EiZmH2VTr+Xzyb6Ga8XtGa9MEbiU3WQnB5vHmqhwAUqibU3uwuwr7MN+QwIh/gtBk1ucMim8BCfcRTlLVREM8A==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/plugin-transform-react-constant-elements": "^7.12.1", + "@babel/preset-env": "^7.12.1", + "@babel/preset-react": "^7.12.5", + "@svgr/core": "^5.5.0", + "@svgr/plugin-jsx": "^5.5.0", + "@svgr/plugin-svgo": "^5.5.0", + "rollup-pluginutils": "^2.8.2" + }, + "dependencies": { + "@svgr/plugin-jsx": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz", + "integrity": "sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@svgr/babel-preset": "^5.5.0", + "@svgr/hast-util-to-babel-ast": "^5.5.0", + "svg-parser": "^2.0.2" + } + }, + "@svgr/plugin-svgo": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz", + "integrity": "sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ==", + "dev": true, + "requires": { + "cosmiconfig": "^7.0.0", + "deepmerge": "^4.2.2", + "svgo": "^1.2.2" + } + }, + "cosmiconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", + "integrity": "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "parse-json": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", + "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + } + } + }, + "@svgr/webpack": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-5.4.0.tgz", + "integrity": "sha512-LjepnS/BSAvelnOnnzr6Gg0GcpLmnZ9ThGFK5WJtm1xOqdBE/1IACZU7MMdVzjyUkfFqGz87eRE4hFaSLiUwYg==", + "dev": true, + "requires": { + "@babel/core": "^7.9.0", + "@babel/plugin-transform-react-constant-elements": "^7.9.0", + "@babel/preset-env": "^7.9.5", + "@babel/preset-react": "^7.9.4", + "@svgr/core": "^5.4.0", + "@svgr/plugin-jsx": "^5.4.0", + "@svgr/plugin-svgo": "^5.4.0", + "loader-utils": "^2.0.0" + }, + "dependencies": { + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + } + } + }, + "@types/anymatch": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz", + "integrity": "sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==", + "dev": true + }, + "@types/babel__core": { + "version": "7.1.12", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.12.tgz", + "integrity": "sha512-wMTHiiTiBAAPebqaPiPDLFA4LYPKr6Ph0Xq/6rq1Ur3v66HXyG+clfR9CNETkD7MQS8ZHvpQOtA53DLws5WAEQ==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.2.tgz", + "integrity": "sha512-MdSJnBjl+bdwkLskZ3NGFp9YcXGx5ggLpQQPqtgakVhsWK0hTtNYhjpZLlWQTviGTvF8at+Bvli3jV7faPdgeQ==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.0.tgz", + "integrity": "sha512-NTPErx4/FiPCGScH7foPyr+/1Dkzkni+rHiYHHoTjvwou7AQzJkNeD60A9CXRy+ZEN2B1bggmkTMCDb+Mv5k+A==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.11.0.tgz", + "integrity": "sha512-kSjgDMZONiIfSH1Nxcr5JIRMwUetDki63FSQfpTCz8ogF3Ulqm8+mr5f78dUYs6vMiB6gBusQqfQmBvHZj/lwg==", + "dev": true, + "requires": { + "@babel/types": "^7.3.0" + } + }, + "@types/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", + "integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==", + "requires": { + "@types/node": "*" + } + }, + "@types/cors": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.9.tgz", + "integrity": "sha512-zurD1ibz21BRlAOIKP8yhrxlqKx6L9VCwkB5kMiP6nZAhoF5MvC7qS1qPA7nRcr1GJolfkQC7/EAL4hdYejLtg==" + }, + "@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true + }, + "@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "@types/express": { + "version": "4.17.9", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.9.tgz", + "integrity": "sha512-SDzEIZInC4sivGIFY4Sz1GG6J9UObPwCInYJjko2jzOf/Imx/dlpume6Xxwj1ORL82tBbmN4cPDIDkLbWHk9hw==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.17", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.17.tgz", + "integrity": "sha512-YYlVaCni5dnHc+bLZfY908IG1+x5xuibKZMGv8srKkvtul3wUuanYvpIj9GXXoWkQbaAdR+kgX46IETKUALWNQ==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", + "dev": true, + "requires": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/graceful-fs": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.4.tgz", + "integrity": "sha512-mWA/4zFQhfvOA8zWkXobwJvBD7vzcxgrOQ0J5CH1votGqdq9m7+FwtGaqyCZqC3NyyBkc9z4m+iry4LlqcMWJg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/html-minifier-terser": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", + "integrity": "sha512-giAlZwstKbmvMk1OO7WXSj4OZ0keXAcl2TQq4LWHiiPH2ByaH7WeUzng+Qej8UPxxv+8lRTuouo0iaNDBuzIBA==", + "dev": true + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", + "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "26.0.19", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.19.tgz", + "integrity": "sha512-jqHoirTG61fee6v6rwbnEuKhpSKih0tuhqeFbCmMmErhtu3BYlOZaXWjffgOstMM4S/3iQD31lI5bGLTrs97yQ==", + "dev": true, + "requires": { + "jest-diff": "^26.0.0", + "pretty-format": "^26.0.0" + } + }, + "@types/json-schema": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", + "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==" + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true + }, + "@types/mime": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", + "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==" + }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true + }, + "@types/node": { + "version": "14.14.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.20.tgz", + "integrity": "sha512-Y93R97Ouif9JEOWPIUyU+eyIdyRqQR0I8Ez1dzku4hDx34NWh4HbtIc3WNzwB1Y9ULvNGeu5B8h8bVL5cAk4/A==" + }, + "@types/normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", + "dev": true + }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, + "@types/prettier": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.1.6.tgz", + "integrity": "sha512-6gOkRe7OIioWAXfnO/2lFiv+SJichKVSys1mSsgyrYHSEjk8Ctv4tSR/Odvnu+HWlH2C8j53dahU03XmQdd5fA==", + "dev": true + }, + "@types/q": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz", + "integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==", + "dev": true + }, + "@types/qs": { + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz", + "integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==" + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + }, + "@types/resolve": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/serve-static": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.8.tgz", + "integrity": "sha512-MoJhSQreaVoL+/hurAZzIm8wafFR6ajiTM1m4A0kv6AGeVBl4r4pOV8bGFrjjq1sGxDTnCoF8i22o0/aE5XCyA==", + "requires": { + "@types/mime": "*", + "@types/node": "*" + } + }, + "@types/source-list-map": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", + "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", + "dev": true + }, + "@types/stack-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz", + "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==", + "dev": true + }, + "@types/start-server-webpack-plugin": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/start-server-webpack-plugin/-/start-server-webpack-plugin-2.2.0.tgz", + "integrity": "sha512-TFiZWMPuiMR/utvjk6ENi0HPtQl38HnPMYfJqm04ztpzITHzTCXt7T7LyXnP9eTwg4lLQkmRUaFy04iEyjoJmw==", + "dev": true, + "requires": { + "@types/webpack": "*" + } + }, + "@types/tapable": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.6.tgz", + "integrity": "sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA==", + "dev": true + }, + "@types/uglify-js": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.11.1.tgz", + "integrity": "sha512-7npvPKV+jINLu1SpSYVWG8KvyJBhBa8tmzMMdDoVc2pWUYHN8KIXlPJhjJ4LT97c4dXJA2SHL/q6ADbDriZN+Q==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@types/webpack": { + "version": "4.41.25", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.25.tgz", + "integrity": "sha512-cr6kZ+4m9lp86ytQc1jPOJXgINQyz3kLLunZ57jznW+WIAL0JqZbGubQk4GlD42MuQL5JGOABrxdpqqWeovlVQ==", + "dev": true, + "requires": { + "@types/anymatch": "*", + "@types/node": "*", + "@types/tapable": "*", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@types/webpack-env": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.16.0.tgz", + "integrity": "sha512-Fx+NpfOO0CpeYX2g9bkvX8O5qh9wrU1sOF4g8sft4Mu7z+qfe387YlyY8w8daDyDsKY5vUxM0yxkAYnbkRbZEw==", + "dev": true + }, + "@types/webpack-node-externals": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@types/webpack-node-externals/-/webpack-node-externals-2.5.0.tgz", + "integrity": "sha512-KaWfhUQlpWknM/CMBKhV7i0vxX/N2xEy3WeaE500s4ZNxC4nLnKB+0F3gD3Fg+5octPq0nn8ZlfFR/P3dSkXpw==", + "dev": true, + "requires": { + "@types/webpack": "*" + } + }, + "@types/webpack-sources": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-2.1.0.tgz", + "integrity": "sha512-LXn/oYIpBeucgP1EIJbKQ2/4ZmpvRl+dlrFdX7+94SKRUV3Evy3FsfMZY318vGhkWUS5MPhtOM3w1/hCOAOXcg==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/source-list-map": "*", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + } + } + }, + "@types/yargs": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.12.tgz", + "integrity": "sha512-f+fD/fQAo3BCbCDlrUpznF1A5Zp9rB0noS5vnoormHSIPFKL0Z2DcUJ3Gxp5ytH4uLRNxy7AwYUC9exZzqGMAw==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.0.tgz", + "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==", + "dev": true + }, + "@types/yup": { + "version": "0.29.11", + "resolved": "https://registry.npmjs.org/@types/yup/-/yup-0.29.11.tgz", + "integrity": "sha512-9cwk3c87qQKZrT251EDoibiYRILjCmxBvvcb4meofCmx1vdnNcR9gyildy5vOHASpOKMsn42CugxUvcwK5eu1g==" + }, + "@typescript-eslint/eslint-plugin": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.10.1.tgz", + "integrity": "sha512-PQg0emRtzZFWq6PxBcdxRH3QIQiyFO3WCVpRL3fgj5oQS3CDs3AeAKfv4DxNhzn8ITdNJGJ4D3Qw8eAJf3lXeQ==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "3.10.1", + "debug": "^4.1.1", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.0.0", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@typescript-eslint/experimental-utils": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz", + "integrity": "sha512-DewqIgscDzmAfd5nOGe4zm6Bl7PKtMG2Ad0KG8CUZAHlXfAKTF9Ol5PXhiMh39yRL2ChRH1cuuUGOcVyyrhQIw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/typescript-estree": "3.10.1", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/parser": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.10.1.tgz", + "integrity": "sha512-Ug1RcWcrJP02hmtaXVS3axPPTTPnZjupqhgj+NnZ6BCkwSImWk/283347+x9wN+lqOdK9Eo3vsyiyDHgsmiEJw==", + "dev": true, + "requires": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "3.10.1", + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/typescript-estree": "3.10.1", + "eslint-visitor-keys": "^1.1.0" + } + }, + "@typescript-eslint/scope-manager": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.12.0.tgz", + "integrity": "sha512-QVf9oCSVLte/8jvOsxmgBdOaoe2J0wtEmBr13Yz0rkBNkl5D8bfnf6G4Vhox9qqMIoG7QQoVwd2eG9DM/ge4Qg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.12.0", + "@typescript-eslint/visitor-keys": "4.12.0" + }, + "dependencies": { + "@typescript-eslint/types": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.12.0.tgz", + "integrity": "sha512-N2RhGeheVLGtyy+CxRmxdsniB7sMSCfsnbh8K/+RUIXYYq3Ub5+sukRCjVE80QerrUBvuEvs4fDhz5AW/pcL6g==", + "dev": true + }, + "@typescript-eslint/visitor-keys": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.12.0.tgz", + "integrity": "sha512-hVpsLARbDh4B9TKYz5cLbcdMIOAoBYgFPCSP9FFS/liSF+b33gVNq8JHY3QGhHNVz85hObvL7BEYLlgx553WCw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.12.0", + "eslint-visitor-keys": "^2.0.0" + } + }, + "eslint-visitor-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "dev": true + } + } + }, + "@typescript-eslint/types": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.10.1.tgz", + "integrity": "sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz", + "integrity": "sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w==", + "dev": true, + "requires": { + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/visitor-keys": "3.10.1", + "debug": "^4.1.1", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@typescript-eslint/visitor-keys": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz", + "integrity": "sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "@webassemblyjs/ast": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", + "integrity": "sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==", + "dev": true, + "requires": { + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz", + "integrity": "sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz", + "integrity": "sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz", + "integrity": "sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==", + "dev": true + }, + "@webassemblyjs/helper-code-frame": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz", + "integrity": "sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==", + "dev": true, + "requires": { + "@webassemblyjs/wast-printer": "1.9.0" + } + }, + "@webassemblyjs/helper-fsm": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz", + "integrity": "sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==", + "dev": true + }, + "@webassemblyjs/helper-module-context": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz", + "integrity": "sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz", + "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz", + "integrity": "sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz", + "integrity": "sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.9.0.tgz", + "integrity": "sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.9.0.tgz", + "integrity": "sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz", + "integrity": "sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/helper-wasm-section": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-opt": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "@webassemblyjs/wast-printer": "1.9.0" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz", + "integrity": "sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz", + "integrity": "sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz", + "integrity": "sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" + } + }, + "@webassemblyjs/wast-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz", + "integrity": "sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/floating-point-hex-parser": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-code-frame": "1.9.0", + "@webassemblyjs/helper-fsm": "1.9.0", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz", + "integrity": "sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0", + "@xtuc/long": "4.2.2" + } + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true + }, + "abab": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", + "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", + "dev": true + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "acorn-globals": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", + "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1" + } + }, + "acorn-jsx": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", + "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", + "dev": true + }, + "acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true + }, + "address": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/address/-/address-1.1.2.tgz", + "integrity": "sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA==", + "dev": true + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "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-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true + }, + "alphanum-sort": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", + "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", + "dev": true + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } + } + }, + "ansi-html": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", + "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=", + "dev": true + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", + "dev": true + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "archiver": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.1.0.tgz", + "integrity": "sha512-iKuQUP1nuKzBC2PFlGet5twENzCfyODmvkxwDV0cEFXavwcLrIW5ssTuHi9dyTPvpWr6Faweo2eQaQiLIwyXTA==", + "requires": { + "archiver-utils": "^2.1.0", + "async": "^3.2.0", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.0.0", + "tar-stream": "^2.1.4", + "zip-stream": "^4.0.4" + } + }, + "archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "requires": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "dev": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "aria-query": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.10.2", + "@babel/runtime-corejs3": "^7.10.2" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + }, + "array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=" + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "array-includes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.2.tgz", + "integrity": "sha512-w2GspexNQpx+PutG3QpT437/BenZBj0M/MZGn5mzv/MofYqo0xmRHzn4lFsoDlWJ+THYsGJmFlW68WlDFx7VRw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1", + "get-intrinsic": "^1.0.1", + "is-string": "^1.0.5" + } + }, + "array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==" + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + }, + "array.prototype.flat": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz", + "integrity": "sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + } + }, + "array.prototype.flatmap": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz", + "integrity": "sha512-r9Z0zYoxqHz60vvQbWEdXIEtCwHF0yxaWfno9qzXeNHvfyl3BZqygmGzb84dsubyaXLH4husF+NFgMSdpZhk2Q==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1", + "function-bind": "^1.1.1" + } + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } + } + }, + "assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", + "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" + }, + "ast-types-flow": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", + "dev": true + }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, + "async": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", + "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" + }, + "async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true + }, + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "dev": true + }, + "axe-core": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.1.1.tgz", + "integrity": "sha512-5Kgy8Cz6LPC9DJcNb3yjAXTu3XihQgEdnIg50c//zOC/MyLP0Clg+Y8Sh9ZjjnvBrDZU4DgXS9C3T9r4/scGZQ==", + "dev": true + }, + "axobject-query": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", + "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==", + "dev": true + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "babel-jest": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz", + "integrity": "sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA==", + "dev": true, + "requires": { + "@jest/transform": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/babel__core": "^7.1.7", + "babel-plugin-istanbul": "^6.0.0", + "babel-preset-jest": "^26.6.2", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "slash": "^3.0.0" + }, + "dependencies": { + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + } + } + }, + "babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "dev": true, + "requires": { + "object.assign": "^4.1.0" + } + }, + "babel-plugin-istanbul": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", + "integrity": "sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^4.0.0", + "test-exclude": "^6.0.0" + } + }, + "babel-plugin-jest-hoist": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz", + "integrity": "sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw==", + "dev": true, + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.0.0", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + } + }, + "babel-preset-jest": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz", + "integrity": "sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "^26.6.2", + "babel-preset-current-node-syntax": "^1.0.0" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "bfj": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/bfj/-/bfj-7.0.2.tgz", + "integrity": "sha512-+e/UqUzwmzJamNF50tBV6tZPTORow7gQ96iFow+8b562OdMpEK0BcJEq2OSPEDmAbSMBQ7PKZ87ubFkgxpYWgw==", + "dev": true, + "requires": { + "bluebird": "^3.5.5", + "check-types": "^11.1.1", + "hoopy": "^0.1.4", + "tryer": "^1.0.1" + }, + "dependencies": { + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + } + } + }, + "big-integer": { + "version": "1.6.48", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", + "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==" + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", + "requires": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + } + }, + "binary-extensions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", + "dev": true + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bl": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", + "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=" + }, + "bn.js": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.3.tgz", + "integrity": "sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==", + "dev": true + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "dependencies": { + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + } + } + }, + "bonjour": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", + "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", + "dev": true, + "requires": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "^6.0.1", + "multicast-dns-service-types": "^1.1.0" + }, + "dependencies": { + "array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + } + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", + "dev": true + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-rsa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", + "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", + "dev": true, + "requires": { + "bn.js": "^5.0.0", + "randombytes": "^2.0.1" + } + }, + "browserify-sign": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", + "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", + "dev": true, + "requires": { + "bn.js": "^5.1.1", + "browserify-rsa": "^4.0.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.3", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.5", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "~1.0.5" + } + }, + "browserslist": { + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.0.tgz", + "integrity": "sha512-/j6k8R0p3nxOC6kx5JGAxsnhc9ixaWJfYc+TNTzxg6+ARaESAvQGV7h0uNOB4t+pLQJZWzcrMxXOxjgsCj3dqQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001165", + "colorette": "^1.2.1", + "electron-to-chromium": "^1.3.621", + "escalade": "^3.1.1", + "node-releases": "^1.1.67" + } + }, + "bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "requires": { + "fast-json-stable-stringify": "2.x" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "requires": { + "node-int64": "^0.4.0" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "buffer-indexof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", + "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", + "dev": true + }, + "buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==" + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=" + }, + "builtin-modules": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", + "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "builtins": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", + "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=", + "dev": true + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "cacache": { + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", + "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", + "dev": true, + "requires": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + }, + "dependencies": { + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "y18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "call-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz", + "integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.0" + } + }, + "call-me-maybe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", + "dev": true + }, + "caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "dev": true, + "requires": { + "callsites": "^2.0.0" + } + }, + "caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "dev": true, + "requires": { + "caller-callsite": "^2.0.0" + } + }, + "callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", + "dev": true + }, + "camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "requires": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==", + "dev": true + } + } + }, + "camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true + }, + "caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "caniuse-lite": { + "version": "1.0.30001173", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001173.tgz", + "integrity": "sha512-R3aqmjrICdGCTAnSXtNyvWYMK3YtV5jwudbq0T7nN9k4kmE4CBuwPqyJ+KBzepSTh0huivV2gLbSMEzTTmfeYw==", + "dev": true + }, + "capture-exit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", + "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", + "dev": true, + "requires": { + "rsvp": "^4.8.4" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", + "requires": { + "traverse": ">=0.3.0 <0.4" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "check-types": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz", + "integrity": "sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ==", + "dev": true + }, + "chokidar": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz", + "integrity": "sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + }, + "chrome-trace-event": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", + "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "cjs-module-lexer": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz", + "integrity": "sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw==", + "dev": true + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "clean-css": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", + "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", + "dev": true, + "requires": { + "source-map": "~0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-spinners": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.5.0.tgz", + "integrity": "sha512-PC+AmIuK04E6aeSs/pUccSujsTzBhu4HzC2dL+CfJB/Jcc2qTRbEwZQDfIUpt2Xl8BodYBEq8w4fc0kU2I9DjQ==", + "dev": true + }, + "cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true + }, + "clone-deep": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz", + "integrity": "sha1-TnPdCen7lxzDhnDF3O2cGJZIHMY=", + "dev": true, + "requires": { + "for-own": "^0.1.3", + "is-plain-object": "^2.0.1", + "kind-of": "^3.0.2", + "lazy-cache": "^1.0.3", + "shallow-clone": "^0.1.2" + }, + "dependencies": { + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "coa": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", + "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", + "dev": true, + "requires": { + "@types/q": "^1.5.1", + "chalk": "^2.4.1", + "q": "^1.1.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + } + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "collect-v8-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", + "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", + "dev": true + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", + "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + }, + "dependencies": { + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + } + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "color-string": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz", + "integrity": "sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colorette": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", + "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==" + }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" + }, + "colorspace": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz", + "integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==", + "requires": { + "color": "3.0.x", + "text-hex": "1.0.x" + } + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==" + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "compress-commons": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.0.2.tgz", + "integrity": "sha512-qhd32a9xgzmpfoga1VQEiLEwdKZ6Plnpx5UCgIsf89FSolyJ7WnifY4Gtjgv5WR6hWAyRaHxC5MiEhU/38U70A==", + "requires": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + } + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + } + }, + "compute-gcd": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/compute-gcd/-/compute-gcd-1.2.1.tgz", + "integrity": "sha512-TwMbxBNz0l71+8Sc4czv13h4kEqnchV9igQZBi6QUaz09dnz13juGnnaWWJTRsP3brxOoxeB4SA2WELLw1hCtg==", + "requires": { + "validate.io-array": "^1.0.3", + "validate.io-function": "^1.0.2", + "validate.io-integer-array": "^1.0.0" + } + }, + "compute-lcm": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/compute-lcm/-/compute-lcm-1.1.2.tgz", + "integrity": "sha512-OFNPdQAXnQhDSKioX8/XYT6sdUlXwpeMjfd6ApxMJfyZ4GxmLR1xvMERctlYhlHwIiz6CSpBc2+qYKjHGZw4TQ==", + "requires": { + "compute-gcd": "^1.2.1", + "validate.io-array": "^1.0.3", + "validate.io-function": "^1.0.2", + "validate.io-integer-array": "^1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "concat-with-sourcemaps": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz", + "integrity": "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "dev": true + }, + "console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", + "dev": true + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", + "dev": true + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + } + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" + }, + "core-js-compat": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.8.2.tgz", + "integrity": "sha512-LO8uL9lOIyRRrQmZxHZFl1RV+ZbcsAkFWTktn5SmH40WgLtSNYN4m4W2v9ONT147PxBY/XrRhrWq8TlvObyUjQ==", + "dev": true, + "requires": { + "browserslist": "^4.16.0", + "semver": "7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true + } + } + }, + "core-js-pure": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.8.2.tgz", + "integrity": "sha512-v6zfIQqL/pzTVAbZvYUozsxNfxcFb6Ks3ZfEbuneJl3FW9Jb8F6vLWB6f+qTmAu72msUdyb84V8d/yBFf7FNnw==", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "dev": true, + "requires": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + } + }, + "crc-32": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz", + "integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==", + "requires": { + "exit-on-epipe": "~1.0.1", + "printj": "~1.1.0" + } + }, + "crc32-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.1.tgz", + "integrity": "sha512-FN5V+weeO/8JaXsamelVYO1PHyeCsuL3HcG4cqsj0ceARcocxalaShCsohZMSAF+db7UYFwBy1rARK/0oFItUw==", + "requires": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + } + }, + "create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "cross-fetch": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.0.6.tgz", + "integrity": "sha512-KBPUbqgFjzWlVcURG+Svp9TlhA5uliYtiNx/0r8nv0pdypeQCRJ9IaSIc3q/x3q8t3F75cHuwxVql1HFGHCNJQ==", + "requires": { + "node-fetch": "2.6.1" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "dependencies": { + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "css-color-names": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", + "dev": true + }, + "css-declaration-sorter": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz", + "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==", + "dev": true, + "requires": { + "postcss": "^7.0.1", + "timsort": "^0.3.0" + } + }, + "css-loader": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.6.0.tgz", + "integrity": "sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "cssesc": "^3.0.0", + "icss-utils": "^4.1.1", + "loader-utils": "^1.2.3", + "normalize-path": "^3.0.0", + "postcss": "^7.0.32", + "postcss-modules-extract-imports": "^2.0.0", + "postcss-modules-local-by-default": "^3.0.2", + "postcss-modules-scope": "^2.2.0", + "postcss-modules-values": "^3.0.0", + "postcss-value-parser": "^4.1.0", + "schema-utils": "^2.7.0", + "semver": "^6.3.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "css-modules-loader-core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/css-modules-loader-core/-/css-modules-loader-core-1.1.0.tgz", + "integrity": "sha1-WQhmgpShvs0mGuCkziGwtVHyHRY=", + "dev": true, + "requires": { + "icss-replace-symbols": "1.1.0", + "postcss": "6.0.1", + "postcss-modules-extract-imports": "1.1.0", + "postcss-modules-local-by-default": "1.2.0", + "postcss-modules-scope": "1.1.0", + "postcss-modules-values": "1.3.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "postcss": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.1.tgz", + "integrity": "sha1-AA29H47vIXqjaLmiEsX8QLKo8/I=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "postcss-modules-extract-imports": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.1.0.tgz", + "integrity": "sha1-thTJcgvmgW6u41+zpfqh26agXds=", + "dev": true, + "requires": { + "postcss": "^6.0.1" + } + }, + "postcss-modules-local-by-default": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz", + "integrity": "sha1-99gMOYxaOT+nlkRmvRlQCn1hwGk=", + "dev": true, + "requires": { + "css-selector-tokenizer": "^0.7.0", + "postcss": "^6.0.1" + } + }, + "postcss-modules-scope": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz", + "integrity": "sha1-1upkmUx5+XtipytCb75gVqGUu5A=", + "dev": true, + "requires": { + "css-selector-tokenizer": "^0.7.0", + "postcss": "^6.0.1" + } + }, + "postcss-modules-values": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz", + "integrity": "sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA=", + "dev": true, + "requires": { + "icss-replace-symbols": "^1.1.0", + "postcss": "^6.0.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "css-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", + "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^3.2.1", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" + } + }, + "css-select-base-adapter": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", + "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", + "dev": true + }, + "css-selector-tokenizer": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.3.tgz", + "integrity": "sha512-jWQv3oCEL5kMErj4wRnK/OPoBi0D+P1FR2cDCKYPaMeD2eW3/mttav8HT4hT1CKopiJI/psEULjkClhvJo4Lvg==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "fastparse": "^1.1.2" + } + }, + "css-tree": { + "version": "1.0.0-alpha.37", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", + "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", + "dev": true, + "requires": { + "mdn-data": "2.0.4", + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "css-what": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", + "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", + "dev": true + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "cssnano": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz", + "integrity": "sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==", + "dev": true, + "requires": { + "cosmiconfig": "^5.0.0", + "cssnano-preset-default": "^4.0.7", + "is-resolvable": "^1.0.0", + "postcss": "^7.0.0" + } + }, + "cssnano-preset-default": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz", + "integrity": "sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==", + "dev": true, + "requires": { + "css-declaration-sorter": "^4.0.1", + "cssnano-util-raw-cache": "^4.0.1", + "postcss": "^7.0.0", + "postcss-calc": "^7.0.1", + "postcss-colormin": "^4.0.3", + "postcss-convert-values": "^4.0.1", + "postcss-discard-comments": "^4.0.2", + "postcss-discard-duplicates": "^4.0.2", + "postcss-discard-empty": "^4.0.1", + "postcss-discard-overridden": "^4.0.1", + "postcss-merge-longhand": "^4.0.11", + "postcss-merge-rules": "^4.0.3", + "postcss-minify-font-values": "^4.0.2", + "postcss-minify-gradients": "^4.0.2", + "postcss-minify-params": "^4.0.2", + "postcss-minify-selectors": "^4.0.2", + "postcss-normalize-charset": "^4.0.1", + "postcss-normalize-display-values": "^4.0.2", + "postcss-normalize-positions": "^4.0.2", + "postcss-normalize-repeat-style": "^4.0.2", + "postcss-normalize-string": "^4.0.2", + "postcss-normalize-timing-functions": "^4.0.2", + "postcss-normalize-unicode": "^4.0.1", + "postcss-normalize-url": "^4.0.1", + "postcss-normalize-whitespace": "^4.0.2", + "postcss-ordered-values": "^4.1.2", + "postcss-reduce-initial": "^4.0.3", + "postcss-reduce-transforms": "^4.0.2", + "postcss-svgo": "^4.0.2", + "postcss-unique-selectors": "^4.0.1" + } + }, + "cssnano-util-get-arguments": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz", + "integrity": "sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=", + "dev": true + }, + "cssnano-util-get-match": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz", + "integrity": "sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=", + "dev": true + }, + "cssnano-util-raw-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz", + "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "cssnano-util-same-parent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz", + "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==", + "dev": true + }, + "csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "dev": true, + "requires": { + "css-tree": "^1.1.2" + }, + "dependencies": { + "css-tree": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.2.tgz", + "integrity": "sha512-wCoWush5Aeo48GLhfHPbmvZs59Z+M7k5+B1xDnXbdWNcEF423DoFdqSWE0PM5aNk5nI5cp1q7ms36zGApY/sKQ==", + "dev": true, + "requires": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + } + }, + "mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "cssom": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", + "dev": true + }, + "cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "requires": { + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + } + } + }, + "cyclist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", + "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", + "dev": true + }, + "damerau-levenshtein": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz", + "integrity": "sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug==", + "dev": true + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "dashify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dashify/-/dashify-2.0.0.tgz", + "integrity": "sha512-hpA5C/YrPjucXypHPPc0oJ1l9Hf6wWbiOL7Ik42cxnsUOhWiCB/fylKbKqqJalW9FgkNQCw16YO8uW9Hs0Iy1A==", + "dev": true + }, + "data-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", + "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "dev": true, + "requires": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decimal.js": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.1.tgz", + "integrity": "sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + }, + "dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", + "dev": true + }, + "deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "dev": true, + "requires": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true + }, + "default-gateway": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", + "integrity": "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "ip-regex": "^2.1.0" + } + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "dev": true, + "requires": { + "clone": "^1.0.2" + } + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "del": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", + "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "globby": "^6.1.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" + }, + "dependencies": { + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "des.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", + "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=" + }, + "detect-indent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz", + "integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50=", + "dev": true + }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true + }, + "detect-node": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", + "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", + "dev": true + }, + "detect-port-alt": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", + "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", + "dev": true, + "requires": { + "address": "^1.0.1", + "debug": "^2.6.0" + } + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "diff-sequences": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", + "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", + "dev": true + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } + } + }, + "dir-glob": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", + "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", + "dev": true, + "requires": { + "path-type": "^3.0.0" + } + }, + "dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", + "dev": true + }, + "dns-packet": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz", + "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==", + "dev": true, + "requires": { + "ip": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, + "dns-txt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", + "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", + "dev": true, + "requires": { + "buffer-indexof": "^1.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "requires": { + "utila": "~0.4" + } + }, + "dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz", + "integrity": "sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==", + "dev": true + } + } + }, + "dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==", + "dev": true + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + }, + "domexception": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", + "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", + "dev": true, + "requires": { + "webidl-conversions": "^5.0.0" + }, + "dependencies": { + "webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", + "dev": true + } + } + }, + "domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "dev": true, + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==", + "dev": true + } + } + }, + "dot-prop": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz", + "integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==", + "dev": true, + "requires": { + "is-obj": "^1.0.0" + } + }, + "duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "requires": { + "readable-stream": "^2.0.2" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "electron-to-chromium": { + "version": "1.3.634", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.634.tgz", + "integrity": "sha512-QPrWNYeE/A0xRvl/QP3E0nkaEvYUvH3gM04ZWYtIa6QlSpEetRlRI1xvQ7hiMIySHHEV+mwDSX8Kj4YZY6ZQAw==", + "dev": true + }, + "elliptic": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "dev": true, + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "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" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } + } + }, + "emittery": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.7.2.tgz", + "integrity": "sha512-A8OG5SR/ij3SsJdWDJdkkSYUjQdCUx6APQXem0SaEePBSRg4eymGYwBkKo1Y6DU+af/Jn2dBQqDBvjnr9Vi8nQ==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true + }, + "enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "enhanced-resolve": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz", + "integrity": "sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" + } + }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + } + }, + "entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "dev": true + }, + "errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "requires": { + "prr": "~1.0.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + } + } + }, + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "esbuild": { + "version": "0.8.29", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.8.29.tgz", + "integrity": "sha512-UDsEoeXuctVgG2hEts1Hwq2jYDGqV7nksEHEZaiCy2v+lXF5ButX4ErPAJAFi5ZNKKW+6Pom93pArV7hki6HnQ==", + "dev": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + } + } + }, + "eslint": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.17.0.tgz", + "integrity": "sha512-zJk08MiBgwuGoxes5sSQhOtibZ75pz0J35XTRlZOk9xMffhpA9BTbQZxoXZzOl5zMbleShbGwtw+1kGferfFwQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@eslint/eslintrc": "^0.2.2", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^6.0.0", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.4", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "eslint-visitor-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "dev": true + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } + } + }, + "eslint-config-prettier": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz", + "integrity": "sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw==", + "dev": true, + "requires": { + "get-stdin": "^6.0.0" + } + }, + "eslint-formatter-friendly": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/eslint-formatter-friendly/-/eslint-formatter-friendly-7.0.0.tgz", + "integrity": "sha512-WXg2D5kMHcRxIZA3ulxdevi8/BGTXu72pfOO5vXHqcAfClfIWDSlOljROjCSOCcKvilgmHz1jDWbvFCZHjMQ5w==", + "dev": true, + "requires": { + "@babel/code-frame": "7.0.0", + "chalk": "2.4.2", + "extend": "3.0.2", + "strip-ansi": "5.2.0", + "text-table": "0.2.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", + "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "eslint-import-resolver-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz", + "integrity": "sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "resolve": "^1.13.1" + } + }, + "eslint-module-utils": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz", + "integrity": "sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "pkg-dir": "^2.0.0" + } + }, + "eslint-plugin-import": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz", + "integrity": "sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw==", + "dev": true, + "requires": { + "array-includes": "^3.1.1", + "array.prototype.flat": "^1.2.3", + "contains-path": "^0.1.0", + "debug": "^2.6.9", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "^0.3.4", + "eslint-module-utils": "^2.6.0", + "has": "^1.0.3", + "minimatch": "^3.0.4", + "object.values": "^1.1.1", + "read-pkg-up": "^2.0.0", + "resolve": "^1.17.0", + "tsconfig-paths": "^3.9.0" + }, + "dependencies": { + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + } + } + } + }, + "eslint-plugin-jest": { + "version": "24.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-24.1.3.tgz", + "integrity": "sha512-dNGGjzuEzCE3d5EPZQ/QGtmlMotqnYWD/QpCZ1UuZlrMAdhG5rldh0N0haCvhGnUkSeuORS5VNROwF9Hrgn3Lg==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "^4.0.1" + }, + "dependencies": { + "@nodelib/fs.stat": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", + "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==", + "dev": true + }, + "@typescript-eslint/experimental-utils": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.12.0.tgz", + "integrity": "sha512-MpXZXUAvHt99c9ScXijx7i061o5HEjXltO+sbYfZAAHxv3XankQkPaNi5myy0Yh0Tyea3Hdq1pi7Vsh0GJb0fA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/scope-manager": "4.12.0", + "@typescript-eslint/types": "4.12.0", + "@typescript-eslint/typescript-estree": "4.12.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/types": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.12.0.tgz", + "integrity": "sha512-N2RhGeheVLGtyy+CxRmxdsniB7sMSCfsnbh8K/+RUIXYYq3Ub5+sukRCjVE80QerrUBvuEvs4fDhz5AW/pcL6g==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.12.0.tgz", + "integrity": "sha512-gZkFcmmp/CnzqD2RKMich2/FjBTsYopjiwJCroxqHZIY11IIoN0l5lKqcgoAPKHt33H2mAkSfvzj8i44Jm7F4w==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.12.0", + "@typescript-eslint/visitor-keys": "4.12.0", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.12.0.tgz", + "integrity": "sha512-hVpsLARbDh4B9TKYz5cLbcdMIOAoBYgFPCSP9FFS/liSF+b33gVNq8JHY3QGhHNVz85hObvL7BEYLlgx553WCw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.12.0", + "eslint-visitor-keys": "^2.0.0" + } + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "eslint-visitor-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "dev": true + }, + "fast-glob": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", + "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", + "micromatch": "^4.0.2", + "picomatch": "^2.2.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "globby": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz", + "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + } + }, + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "eslint-plugin-jsx-a11y": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.4.1.tgz", + "integrity": "sha512-0rGPJBbwHoGNPU73/QCLP/vveMlM1b1Z9PponxO87jfr6tuH5ligXbDT6nHSSzBC8ovX2Z+BQu7Bk5D/Xgq9zg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.11.2", + "aria-query": "^4.2.2", + "array-includes": "^3.1.1", + "ast-types-flow": "^0.0.7", + "axe-core": "^4.0.2", + "axobject-query": "^2.2.0", + "damerau-levenshtein": "^1.0.6", + "emoji-regex": "^9.0.0", + "has": "^1.0.3", + "jsx-ast-utils": "^3.1.0", + "language-tags": "^1.0.5" + }, + "dependencies": { + "emoji-regex": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.0.tgz", + "integrity": "sha512-DNc3KFPK18bPdElMJnf/Pkv5TXhxFU3YFDEuGLDRtPmV4rkmCjBkCSEp22u6rBHdSN9Vlp/GK7k98prmE1Jgug==", + "dev": true + } + } + }, + "eslint-plugin-monorepo": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-monorepo/-/eslint-plugin-monorepo-0.3.2.tgz", + "integrity": "sha512-CypTAqHjTR05XxzqDj7x88oVu2GiqqQA/datD9kIwciHzpj0oE4YbTdyEFFKADgd7dbd21KliSlUpOvo626FBw==", + "dev": true, + "requires": { + "eslint-module-utils": "^2.1.1", + "get-monorepo-packages": "^1.1.0", + "globby": "^7.1.1", + "load-json-file": "^4.0.0", + "minimatch": "^3.0.4", + "parse-package-name": "^0.1.0", + "path-is-inside": "^1.0.2" + }, + "dependencies": { + "globby": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz", + "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "dir-glob": "^2.0.0", + "glob": "^7.1.2", + "ignore": "^3.3.5", + "pify": "^3.0.0", + "slash": "^1.0.0" + } + }, + "ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + } + } + }, + "eslint-plugin-react": { + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.22.0.tgz", + "integrity": "sha512-p30tuX3VS+NWv9nQot9xIGAHBXR0+xJVaZriEsHoJrASGCJZDJ8JLNM0YqKqI0AKm6Uxaa1VUHoNEibxRCMQHA==", + "dev": true, + "requires": { + "array-includes": "^3.1.1", + "array.prototype.flatmap": "^1.2.3", + "doctrine": "^2.1.0", + "has": "^1.0.3", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "object.entries": "^1.1.2", + "object.fromentries": "^2.0.2", + "object.values": "^1.1.1", + "prop-types": "^15.7.2", + "resolve": "^1.18.1", + "string.prototype.matchall": "^4.0.2" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + } + } + }, + "eslint-plugin-react-hooks": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz", + "integrity": "sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ==", + "dev": true + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, + "esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==" + }, + "espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "requires": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "events": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", + "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==", + "dev": true + }, + "eventsource": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz", + "integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==", + "dev": true, + "requires": { + "original": "^1.0.0" + } + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "exec-sh": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz", + "integrity": "sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A==", + "dev": true + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + } + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "exit-on-epipe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz", + "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==" + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "expect": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-26.6.2.tgz", + "integrity": "sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "ansi-styles": "^4.0.0", + "jest-get-type": "^26.3.0", + "jest-matcher-utils": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-regex-util": "^26.0.0" + } + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "express-promise-router": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/express-promise-router/-/express-promise-router-3.0.3.tgz", + "integrity": "sha1-Xm0ipaPwE9cYMxcv6NereAw/a3A=", + "requires": { + "is-promise": "^2.1.0", + "lodash.flattendeep": "^4.0.0", + "methods": "^1.0.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-glob": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", + "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", + "dev": true, + "requires": { + "@mrmlnc/readdir-enhanced": "^2.2.1", + "@nodelib/fs.stat": "^1.1.2", + "glob-parent": "^3.1.0", + "is-glob": "^4.0.0", + "merge2": "^1.2.3", + "micromatch": "^3.1.10" + }, + "dependencies": { + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + } + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fast-safe-stringify": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", + "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" + }, + "fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "dev": true + }, + "fastq": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.10.0.tgz", + "integrity": "sha512-NL2Qc5L3iQEsyYzweq7qfgy5OtXCmGzGvhElGEd/SoFWEMOEczNh5s5ocaF01HDetxz+p8ecjNPA6cZxxIHmzA==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "faye-websocket": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", + "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "fb-watchman": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", + "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", + "dev": true, + "requires": { + "bser": "2.1.1" + } + }, + "fecha": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.0.tgz", + "integrity": "sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg==" + }, + "figgy-pudding": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", + "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==", + "dev": true + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.0.tgz", + "integrity": "sha512-fqoO76jZ3ZnYrXLDRxBR1YvOvc0k844kcOg40bgsPrE25LAb/PDqTY+ho64Xh2c8ZXgIKldchCFHczG2UVRcWA==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, + "filesize": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.0.1.tgz", + "integrity": "sha512-u4AYWPgbI5GBhs6id1KdImZWn5yfyFrrQ8OWZdN7ZMfA8Bf4HcO0BGo9bmUIEV8yrp8I1xVfJ/dn90GtFNNJcg==", + "dev": true + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "find-cache-dir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "findup-sync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", + "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + } + }, + "fined": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", + "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "requires": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" + } + }, + "flagged-respawn": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", + "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==" + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "flatted": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.0.tgz", + "integrity": "sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA==", + "dev": true + }, + "flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "fn-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fn-name/-/fn-name-3.0.0.tgz", + "integrity": "sha512-eNMNr5exLoavuAMhIUVsOKF79SWd/zG104ef6sxBTSw+cZc6BXdQXDvYcGvp0VbxVVSp1XDUNoz7mg1xMtSznA==" + }, + "fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, + "follow-redirects": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz", + "integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==", + "dev": true + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "requires": { + "for-in": "^1.0.1" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "fork-ts-checker-webpack-plugin": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-4.1.6.tgz", + "integrity": "sha512-DUxuQaKoqfNne8iikd14SAkh5uw4+8vNifp6gmA73yYNS6ywLIWSLD/n/mBzHQRpW3J7rbATEakmiA8JvkTyZw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.5.5", + "chalk": "^2.4.1", + "micromatch": "^3.1.10", + "minimatch": "^3.0.4", + "semver": "^5.6.0", + "tapable": "^1.0.0", + "worker-rpc": "^0.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "formidable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", + "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==", + "dev": true + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "fs-extra": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", + "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^1.0.0" + } + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, + "fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "requires": { + "minimist": "^1.2.5" + } + } + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "generic-names": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generic-names/-/generic-names-2.0.1.tgz", + "integrity": "sha512-kPCHWa1m9wGG/OwQpeweTwM/PYiQLrUIxXbt/P4Nic3LbGjCP0YwrALHW1uNLKZ0LIMg+RF+XRlj2ekT9ZlZAQ==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0" + } + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-intrinsic": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.2.tgz", + "integrity": "sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "get-monorepo-packages": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-monorepo-packages/-/get-monorepo-packages-1.2.0.tgz", + "integrity": "sha512-aDP6tH+eM3EuVSp3YyCutOcFS4Y9AhRRH9FAd+cjtR/g63Hx+DCXdKoP1ViRPUJz5wm+BOEXB4FhoffGHxJ7jQ==", + "dev": true, + "requires": { + "globby": "^7.1.1", + "load-json-file": "^4.0.0" + }, + "dependencies": { + "globby": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz", + "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "dir-glob": "^2.0.0", + "glob": "^7.1.2", + "ignore": "^3.3.5", + "pify": "^3.0.0", + "slash": "^1.0.0" + } + }, + "ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + } + } + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, + "get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" + }, + "getopts": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.2.5.tgz", + "integrity": "sha512-9jb7AW5p3in+IiJWhQiZmmwkpLaR/ccTWdWQCtZM66HJcHHLegowh4q4tSD7gouUyeNvFWRavfK9GXosQHDpFA==" + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "git-up": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/git-up/-/git-up-4.0.2.tgz", + "integrity": "sha512-kbuvus1dWQB2sSW4cbfTeGpCMd8ge9jx9RKnhXhuJ7tnvT+NIrTVfYZxjtflZddQYcmdOTlkAcjmx7bor+15AQ==", + "requires": { + "is-ssh": "^1.3.0", + "parse-url": "^5.0.0" + } + }, + "git-url-parse": { + "version": "11.4.3", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.4.3.tgz", + "integrity": "sha512-LZTTk0nqJnKN48YRtOpR8H5SEfp1oM2tls90NuZmBxN95PnCvmuXGzqQ4QmVirBgKx2KPYfPGteX3/raWjKenQ==", + "requires": { + "git-up": "^4.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "glob-to-regexp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", + "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=", + "dev": true + }, + "global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "dev": true, + "requires": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, + "global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "requires": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + } + }, + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "requires": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "globby": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-9.2.0.tgz", + "integrity": "sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "array-union": "^1.0.2", + "dir-glob": "^2.2.2", + "fast-glob": "^2.2.6", + "glob": "^7.1.3", + "ignore": "^4.0.3", + "pify": "^4.0.1", + "slash": "^2.0.0" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + }, + "growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", + "dev": true, + "optional": true + }, + "gzip-size": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", + "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==", + "dev": true, + "requires": { + "duplexer": "^0.1.1", + "pify": "^4.0.1" + } + }, + "handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "handlebars": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", + "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", + "dev": true, + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "dev": true, + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + } + }, + "harmony-reflect": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.1.tgz", + "integrity": "sha512-WJTeyp0JzGtHcuMsi7rw2VwtkvLa+JyfEKJCFyfcS0+CDkjQ5lHPu7zEhFZP+PDSRrEgXa5Ah0l1MbgbE41XjA==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + } + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "helmet": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-4.3.1.tgz", + "integrity": "sha512-WsafDyKsIexB0+pUNkq3rL1rB5GVAghR68TP8ssM9DPEMzfBiluEQlVzJ/FEj6Vq2Ag3CNuxf7aYMjXrN0X49Q==" + }, + "hex-color-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", + "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", + "dev": true + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dev": true, + "requires": { + "react-is": "^16.7.0" + } + }, + "homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "requires": { + "parse-passwd": "^1.0.0" + } + }, + "hoopy": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", + "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==", + "dev": true + }, + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "dev": true + }, + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "hsl-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", + "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=", + "dev": true + }, + "hsla-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", + "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=", + "dev": true + }, + "html-comment-regex": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz", + "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==", + "dev": true + }, + "html-encoding-sniffer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", + "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "dev": true, + "requires": { + "whatwg-encoding": "^1.0.5" + } + }, + "html-entities": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz", + "integrity": "sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==", + "dev": true + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "html-minifier-terser": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", + "integrity": "sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==", + "dev": true, + "requires": { + "camel-case": "^4.1.1", + "clean-css": "^4.2.3", + "commander": "^4.1.1", + "he": "^1.2.0", + "param-case": "^3.0.3", + "relateurl": "^0.2.7", + "terser": "^4.6.3" + }, + "dependencies": { + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true + } + } + }, + "html-webpack-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.5.1.tgz", + "integrity": "sha512-yzK7RQZwv9xB+pcdHNTjcqbaaDZ+5L0zJHXfi89iWIZmb/FtzxhLk0635rmJihcQbs3ZUF27Xp4oWGx6EK56zg==", + "dev": true, + "requires": { + "@types/html-minifier-terser": "^5.0.0", + "@types/tapable": "^1.0.5", + "@types/webpack": "^4.41.8", + "html-minifier-terser": "^5.0.1", + "loader-utils": "^1.2.3", + "lodash": "^4.17.20", + "pretty-error": "^2.1.1", + "tapable": "^1.1.3", + "util.promisify": "1.0.0" + }, + "dependencies": { + "util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" + } + } + } + }, + "htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "dev": true, + "requires": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "dev": true + } + } + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", + "dev": true + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + } + } + }, + "http-parser-js": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.3.tgz", + "integrity": "sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==", + "dev": true + }, + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-proxy-middleware": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz", + "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==", + "dev": true, + "requires": { + "http-proxy": "^1.17.0", + "is-glob": "^4.0.0", + "lodash": "^4.17.11", + "micromatch": "^3.1.10" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "icss-replace-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", + "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=", + "dev": true + }, + "icss-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", + "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", + "dev": true, + "requires": { + "postcss": "^7.0.14" + } + }, + "identity-obj-proxy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", + "integrity": "sha1-lNK9qWCERT7zb7xarsN+D3nx/BQ=", + "dev": true, + "requires": { + "harmony-reflect": "^1.4.6" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "dev": true + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "immer": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz", + "integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==", + "dev": true + }, + "import-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz", + "integrity": "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==", + "dev": true, + "requires": { + "import-from": "^3.0.0" + } + }, + "import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "dev": true, + "requires": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + } + } + }, + "import-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz", + "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "import-local": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", + "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + } + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", + "dev": true + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "inquirer": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + } + }, + "internal-ip": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", + "integrity": "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==", + "dev": true, + "requires": { + "default-gateway": "^4.2.0", + "ipaddr.js": "^1.9.0" + } + }, + "internal-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.2.tgz", + "integrity": "sha512-2cQNfwhAfJIkU4KZPkDI+Gj5yNNnbqi40W9Gge6dfnk4TocEVm00B3bdiL+JINrbGJil2TeHvM4rETGzk/f/0g==", + "dev": true, + "requires": { + "es-abstract": "^1.17.0-next.1", + "has": "^1.0.3", + "side-channel": "^1.0.2" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + }, + "interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==" + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true + }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "dev": true + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "requires": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + } + }, + "is-absolute-url": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", + "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arguments": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.0.tgz", + "integrity": "sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==", + "dev": true, + "requires": { + "call-bind": "^1.0.0" + } + }, + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", + "dev": true + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "requires": { + "ci-info": "^2.0.0" + } + }, + "is-color-stop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", + "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=", + "dev": true, + "requires": { + "css-color-names": "^0.0.4", + "hex-color-regex": "^1.1.0", + "hsl-regex": "^1.0.0", + "hsla-regex": "^1.0.0", + "rgb-regex": "^1.0.1", + "rgba-regex": "^1.0.0" + } + }, + "is-core-module": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", + "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", + "requires": { + "has": "^1.0.3" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "dev": true + }, + "is-docker": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz", + "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==", + "dev": true + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true + }, + "is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", + "dev": true + }, + "is-negative-zero": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", + "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, + "is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true + }, + "is-path-in-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", + "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", + "dev": true, + "requires": { + "is-path-inside": "^2.1.0" + } + }, + "is-path-inside": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "dev": true, + "requires": { + "path-is-inside": "^1.0.2" + } + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "isobject": "^3.0.1" + } + }, + "is-potential-custom-element-name": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz", + "integrity": "sha1-DFLlS8yjkbssSUsh6GJtczbG45c=", + "dev": true + }, + "is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" + }, + "is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "requires": { + "@types/estree": "*" + } + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "requires": { + "is-unc-path": "^1.0.0" + } + }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true + }, + "is-root": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", + "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==", + "dev": true + }, + "is-ssh": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.3.2.tgz", + "integrity": "sha512-elEw0/0c2UscLrNG+OAorbP539E3rhliKPg+hDMWN9VwrDXfYK+4PBEykDPfxlYYtQvl84TascnQyobfQLHEhQ==", + "requires": { + "protocols": "^1.1.0" + } + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" + }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", + "dev": true + }, + "is-svg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz", + "integrity": "sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==", + "dev": true, + "requires": { + "html-comment-regex": "^1.1.0" + } + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "requires": { + "unc-path-regex": "^0.1.2" + } + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "requires": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", + "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jest": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest/-/jest-26.6.3.tgz", + "integrity": "sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q==", + "dev": true, + "requires": { + "@jest/core": "^26.6.3", + "import-local": "^3.0.2", + "jest-cli": "^26.6.3" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "jest-cli": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-26.6.3.tgz", + "integrity": "sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg==", + "dev": true, + "requires": { + "@jest/core": "^26.6.3", + "@jest/test-result": "^26.6.2", + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "import-local": "^3.0.2", + "is-ci": "^2.0.0", + "jest-config": "^26.6.3", + "jest-util": "^26.6.2", + "jest-validate": "^26.6.2", + "prompts": "^2.0.1", + "yargs": "^15.4.1" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", + "dev": true + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "jest-changed-files": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.6.2.tgz", + "integrity": "sha512-fDS7szLcY9sCtIip8Fjry9oGf3I2ht/QT21bAHm5Dmf0mD4X3ReNUf17y+bO6fR8WgbIZTlbyG1ak/53cbRzKQ==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "execa": "^4.0.0", + "throat": "^5.0.0" + }, + "dependencies": { + "execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + } + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + } + } + }, + "jest-config": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-26.6.3.tgz", + "integrity": "sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/test-sequencer": "^26.6.3", + "@jest/types": "^26.6.2", + "babel-jest": "^26.6.3", + "chalk": "^4.0.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.1", + "graceful-fs": "^4.2.4", + "jest-environment-jsdom": "^26.6.2", + "jest-environment-node": "^26.6.2", + "jest-get-type": "^26.3.0", + "jest-jasmine2": "^26.6.3", + "jest-regex-util": "^26.0.0", + "jest-resolve": "^26.6.2", + "jest-util": "^26.6.2", + "jest-validate": "^26.6.2", + "micromatch": "^4.0.2", + "pretty-format": "^26.6.2" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "jest-css-modules": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/jest-css-modules/-/jest-css-modules-2.1.0.tgz", + "integrity": "sha512-my3Scnt6l2tOll/eGwNZeh1KLAFkNzdl4MyZRdpl46GO6/93JcKKdTjNqK6Nokg8A8rT84MFLOpY1pzqKBEqMw==", + "dev": true, + "requires": { + "identity-obj-proxy": "3.0.0" + } + }, + "jest-diff": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", + "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + } + }, + "jest-docblock": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-26.0.0.tgz", + "integrity": "sha512-RDZ4Iz3QbtRWycd8bUEPxQsTlYazfYn/h5R65Fc6gOfwozFhoImx+affzky/FFBuqISPTqjXomoIGJVKBWoo0w==", + "dev": true, + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-26.6.2.tgz", + "integrity": "sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", + "jest-get-type": "^26.3.0", + "jest-util": "^26.6.2", + "pretty-format": "^26.6.2" + } + }, + "jest-environment-jsdom": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz", + "integrity": "sha512-jgPqCruTlt3Kwqg5/WVFyHIOJHsiAvhcp2qiR2QQstuG9yWox5+iHpU3ZrcBxW14T4fe5Z68jAfLRh7joCSP2Q==", + "dev": true, + "requires": { + "@jest/environment": "^26.6.2", + "@jest/fake-timers": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "jest-mock": "^26.6.2", + "jest-util": "^26.6.2", + "jsdom": "^16.4.0" + } + }, + "jest-environment-node": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-26.6.2.tgz", + "integrity": "sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag==", + "dev": true, + "requires": { + "@jest/environment": "^26.6.2", + "@jest/fake-timers": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "jest-mock": "^26.6.2", + "jest-util": "^26.6.2" + } + }, + "jest-esm-transformer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jest-esm-transformer/-/jest-esm-transformer-1.0.0.tgz", + "integrity": "sha512-FoPgeMMwy1/CEsc8tBI41i83CEO3x85RJuZi5iAMmWoARXhfgk6Jd7y+4d+z+HCkTKNVDvSWKGRhwjzU9PUbrw==", + "dev": true, + "requires": { + "@babel/core": "^7.4.4", + "@babel/plugin-transform-modules-commonjs": "^7.4.4" + } + }, + "jest-get-type": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "dev": true + }, + "jest-haste-map": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.6.2.tgz", + "integrity": "sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/graceful-fs": "^4.1.2", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.1.2", + "graceful-fs": "^4.2.4", + "jest-regex-util": "^26.0.0", + "jest-serializer": "^26.6.2", + "jest-util": "^26.6.2", + "jest-worker": "^26.6.2", + "micromatch": "^4.0.2", + "sane": "^4.0.3", + "walker": "^1.0.7" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "jest-jasmine2": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz", + "integrity": "sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg==", + "dev": true, + "requires": { + "@babel/traverse": "^7.1.0", + "@jest/environment": "^26.6.2", + "@jest/source-map": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "expect": "^26.6.2", + "is-generator-fn": "^2.0.0", + "jest-each": "^26.6.2", + "jest-matcher-utils": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-runtime": "^26.6.3", + "jest-snapshot": "^26.6.2", + "jest-util": "^26.6.2", + "pretty-format": "^26.6.2", + "throat": "^5.0.0" + } + }, + "jest-leak-detector": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz", + "integrity": "sha512-i4xlXpsVSMeKvg2cEKdfhh0H39qlJlP5Ex1yQxwF9ubahboQYMgTtz5oML35AVA3B4Eu+YsmwaiKVev9KCvLxg==", + "dev": true, + "requires": { + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + } + }, + "jest-matcher-utils": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz", + "integrity": "sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + } + }, + "jest-message-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz", + "integrity": "sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@jest/types": "^26.6.2", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.2", + "pretty-format": "^26.6.2", + "slash": "^3.0.0", + "stack-utils": "^2.0.2" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "jest-mock": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-26.6.2.tgz", + "integrity": "sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*" + } + }, + "jest-pnp-resolver": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", + "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", + "dev": true + }, + "jest-regex-util": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", + "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", + "dev": true + }, + "jest-resolve": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-26.6.2.tgz", + "integrity": "sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^26.6.2", + "read-pkg-up": "^7.0.1", + "resolve": "^1.18.1", + "slash": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parse-json": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", + "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } + } + }, + "jest-resolve-dependencies": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz", + "integrity": "sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "jest-regex-util": "^26.0.0", + "jest-snapshot": "^26.6.2" + } + }, + "jest-runner": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-26.6.3.tgz", + "integrity": "sha512-atgKpRHnaA2OvByG/HpGA4g6CSPS/1LK0jK3gATJAoptC1ojltpmVlYC3TYgdmGp+GLuhzpH30Gvs36szSL2JQ==", + "dev": true, + "requires": { + "@jest/console": "^26.6.2", + "@jest/environment": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.7.1", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "jest-config": "^26.6.3", + "jest-docblock": "^26.0.0", + "jest-haste-map": "^26.6.2", + "jest-leak-detector": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-resolve": "^26.6.2", + "jest-runtime": "^26.6.3", + "jest-util": "^26.6.2", + "jest-worker": "^26.6.2", + "source-map-support": "^0.5.6", + "throat": "^5.0.0" + } + }, + "jest-runtime": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-26.6.3.tgz", + "integrity": "sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw==", + "dev": true, + "requires": { + "@jest/console": "^26.6.2", + "@jest/environment": "^26.6.2", + "@jest/fake-timers": "^26.6.2", + "@jest/globals": "^26.6.2", + "@jest/source-map": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/transform": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0", + "cjs-module-lexer": "^0.6.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.4", + "jest-config": "^26.6.3", + "jest-haste-map": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-mock": "^26.6.2", + "jest-regex-util": "^26.0.0", + "jest-resolve": "^26.6.2", + "jest-snapshot": "^26.6.2", + "jest-util": "^26.6.2", + "jest-validate": "^26.6.2", + "slash": "^3.0.0", + "strip-bom": "^4.0.0", + "yargs": "^15.4.1" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", + "dev": true + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "jest-serializer": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.6.2.tgz", + "integrity": "sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==", + "dev": true, + "requires": { + "@types/node": "*", + "graceful-fs": "^4.2.4" + } + }, + "jest-snapshot": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-26.6.2.tgz", + "integrity": "sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0", + "@jest/types": "^26.6.2", + "@types/babel__traverse": "^7.0.4", + "@types/prettier": "^2.0.0", + "chalk": "^4.0.0", + "expect": "^26.6.2", + "graceful-fs": "^4.2.4", + "jest-diff": "^26.6.2", + "jest-get-type": "^26.3.0", + "jest-haste-map": "^26.6.2", + "jest-matcher-utils": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-resolve": "^26.6.2", + "natural-compare": "^1.4.0", + "pretty-format": "^26.6.2", + "semver": "^7.3.2" + } + }, + "jest-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", + "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "jest-validate": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-26.6.2.tgz", + "integrity": "sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "camelcase": "^6.0.0", + "chalk": "^4.0.0", + "jest-get-type": "^26.3.0", + "leven": "^3.1.0", + "pretty-format": "^26.6.2" + } + }, + "jest-watcher": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-26.6.2.tgz", + "integrity": "sha512-WKJob0P/Em2csiVthsI68p6aGKTIcsfjH9Gsx1f0A3Italz43e3ho0geSAVsmj09RWOELP1AZ/DXyJgOgDKxXQ==", + "dev": true, + "requires": { + "@jest/test-result": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "jest-util": "^26.6.2", + "string-length": "^4.0.1" + } + }, + "jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "joycon": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-2.2.5.tgz", + "integrity": "sha512-YqvUxoOcVPnCp0VU1/56f+iKSdvIRJYPznH22BdXV3xMk75SFXhWeJkZ8C9XxUWt1b5x2X1SxuFygW1U0FmkEQ==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true + }, + "jsdom": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.4.0.tgz", + "integrity": "sha512-lYMm3wYdgPhrl7pDcRmvzPhhrGVBeVhPIqeHjzeiHN3DFmD1RBpbExbi8vU7BJdH8VAZYovR8DMt0PNNDM7k8w==", + "dev": true, + "requires": { + "abab": "^2.0.3", + "acorn": "^7.1.1", + "acorn-globals": "^6.0.0", + "cssom": "^0.4.4", + "cssstyle": "^2.2.0", + "data-urls": "^2.0.0", + "decimal.js": "^10.2.0", + "domexception": "^2.0.1", + "escodegen": "^1.14.1", + "html-encoding-sniffer": "^2.0.1", + "is-potential-custom-element-name": "^1.0.0", + "nwsapi": "^2.2.0", + "parse5": "5.1.1", + "request": "^2.88.2", + "request-promise-native": "^1.0.8", + "saxes": "^5.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^3.0.1", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^2.0.0", + "webidl-conversions": "^6.1.0", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0", + "ws": "^7.2.3", + "xml-name-validator": "^3.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json-schema": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.5.tgz", + "integrity": "sha512-gWJOWYFrhQ8j7pVm0EM8Slr+EPVq1Phf6lvzvD/WCeqkrx/f2xBI0xOsRRS9xCn3I4vKtP519dvs3TP09r24wQ==" + }, + "json-schema-compare": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/json-schema-compare/-/json-schema-compare-0.2.2.tgz", + "integrity": "sha512-c4WYmDKyJXhs7WWvAWm3uIYnfyWFoIp+JEoX34rctVvEkMYCPGhXtvmFFXiffBbxfZsvQ0RNnV5H7GvDF5HCqQ==", + "requires": { + "lodash": "^4.17.4" + } + }, + "json-schema-merge-allof": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/json-schema-merge-allof/-/json-schema-merge-allof-0.7.0.tgz", + "integrity": "sha512-kvsuSVnl1n5xnNEu5ed4o8r8ujSA4/IgRtHmpgfMfa7FOMIRAzN4F9qbuklouTn5J8bi83y6MQ11n+ERMMTXZg==", + "requires": { + "compute-lcm": "^1.1.0", + "json-schema-compare": "^0.2.2", + "lodash": "^4.17.4" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "requires": { + "jsonify": "~0.0.0" + } + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json3": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz", + "integrity": "sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + }, + "dependencies": { + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + } + } + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + }, + "dependencies": { + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + } + } + }, + "jsx-ast-utils": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz", + "integrity": "sha512-EIsmt3O3ljsU6sot/J4E1zDRxfBNrhjyf/OKjlydwgEimQuznlM4Wv7U+ueONJMyEn1WRE0K8dhi3dVAXYT24Q==", + "dev": true, + "requires": { + "array-includes": "^3.1.2", + "object.assign": "^4.1.2" + } + }, + "kafkajs": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/kafkajs/-/kafkajs-1.15.0.tgz", + "integrity": "sha512-yjPyEnQCkPxAuQLIJnY5dI+xnmmgXmhuOQ1GVxClG5KTOV/rJcW1qA3UfvyEJKTp/RTSqQnUR3HJsKFvHyTpNg==" + }, + "killable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", + "integrity": "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==", + "dev": true + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, + "knex": { + "version": "0.21.15", + "resolved": "https://registry.npmjs.org/knex/-/knex-0.21.15.tgz", + "integrity": "sha512-STHnPIIkExZVz0X3zIDaWC/q9EcTAfuuRE5Rev7NpOhF1QVh24K5iT2FXD0nWoJ1BUSeDs5QdgdTaz9H8oEtfg==", + "requires": { + "colorette": "1.2.1", + "commander": "^6.2.0", + "debug": "4.3.1", + "esm": "^3.2.25", + "getopts": "2.2.5", + "interpret": "^2.2.0", + "liftoff": "3.1.0", + "lodash": "^4.17.20", + "pg-connection-string": "2.4.0", + "tarn": "^3.0.1", + "tildify": "2.0.0", + "v8flags": "^3.2.0" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, + "language-subtag-registry": { + "version": "0.3.21", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz", + "integrity": "sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg==", + "dev": true + }, + "language-tags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", + "integrity": "sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=", + "dev": true, + "requires": { + "language-subtag-registry": "~0.3.2" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true + }, + "lazystream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", + "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "requires": { + "readable-stream": "^2.0.5" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "liftoff": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", + "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==", + "requires": { + "extend": "^3.0.0", + "findup-sync": "^3.0.0", + "fined": "^1.0.1", + "flagged-respawn": "^1.0.0", + "is-plain-object": "^2.0.4", + "object.map": "^1.0.0", + "rechoir": "^0.6.2", + "resolve": "^1.1.7" + } + }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, + "listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=" + }, + "load-json-file": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-5.3.0.tgz", + "integrity": "sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "parse-json": "^4.0.0", + "pify": "^4.0.1", + "strip-bom": "^3.0.0", + "type-fest": "^0.3.0" + } + }, + "loader-runner": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", + "dev": true + }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + }, + "lodash-es": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.20.tgz", + "integrity": "sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA==" + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", + "dev": true + }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" + }, + "lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=" + }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" + }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "dev": true + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "dev": true + }, + "lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=" + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "dev": true + }, + "log-symbols": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", + "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + } + } + }, + "logform": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.2.0.tgz", + "integrity": "sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==", + "requires": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "triple-beam": "^1.3.0" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "loglevel": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz", + "integrity": "sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "requires": { + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==", + "dev": true + } + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "make-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", + "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "requires": { + "kind-of": "^6.0.2" + } + }, + "makeerror": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", + "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", + "dev": true, + "requires": { + "tmpl": "1.0.x" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "requires": { + "object-visit": "^1.0.0" + } + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "mdn-data": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", + "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", + "dev": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "merge-deep": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.2.tgz", + "integrity": "sha512-T7qC8kg4Zoti1cFd8Cr0M+qaZfOwjlPDEdZIIPPB2JZctjaPM4fX+i7HOId69tAti2fvO6X5ldfYUONDODsrkA==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "clone-deep": "^0.2.4", + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "microevent.ts": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.1.tgz", + "integrity": "sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz", + "integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==" + }, + "mime-types": { + "version": "2.1.28", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz", + "integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==", + "requires": { + "mime-db": "1.45.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", + "dev": true, + "requires": { + "dom-walk": "^0.1.0" + } + }, + "mini-css-extract-plugin": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz", + "integrity": "sha512-lp3GeY7ygcgAmVIcRPBVhIkf8Us7FZjA+ILpal44qLdSu11wmjKQ3d9k15lfD7pO4esu9eUIAW7qiYIBppv40A==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "normalize-url": "1.9.1", + "schema-utils": "^1.0.0", + "webpack-sources": "^1.1.0" + }, + "dependencies": { + "normalize-url": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", + "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", + "dev": true, + "requires": { + "object-assign": "^4.0.1", + "prepend-http": "^1.0.0", + "query-string": "^4.1.0", + "sort-keys": "^1.0.0" + } + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + } + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "mississippi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "dev": true, + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + }, + "dependencies": { + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mixin-object": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", + "integrity": "sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=", + "dev": true, + "requires": { + "for-in": "^0.1.3", + "is-extendable": "^0.1.1" + }, + "dependencies": { + "for-in": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", + "integrity": "sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=", + "dev": true + } + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, + "morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "requires": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "dependencies": { + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + } + } + }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "multicast-dns": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", + "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", + "dev": true, + "requires": { + "dns-packet": "^1.3.1", + "thunky": "^1.0.2" + } + }, + "multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", + "dev": true + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "requires": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "nan": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "dev": true, + "optional": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "requires": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==", + "dev": true + } + } + }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, + "node-forge": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", + "dev": true + }, + "node-libs-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", + "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", + "dev": true, + "requires": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.1", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" + }, + "dependencies": { + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "node-modules-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", + "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", + "dev": true + }, + "node-notifier": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz", + "integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==", + "dev": true, + "optional": true, + "requires": { + "growly": "^1.3.0", + "is-wsl": "^2.2.0", + "semver": "^7.3.2", + "shellwords": "^0.1.1", + "uuid": "^8.3.0", + "which": "^2.0.2" + }, + "dependencies": { + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "optional": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "node-releases": { + "version": "1.1.69", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.69.tgz", + "integrity": "sha512-DGIjo79VDEyAnRlfSqYTsy+yoHd2IOjJiKUozD2MV2D85Vso6Bug56mb9tT/fY5Urt0iqk01H7x+llAruDR2zA==", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "normalize-url": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", + "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==" + }, + "npm-package-arg": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.1.tgz", + "integrity": "sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg==", + "dev": true, + "requires": { + "hosted-git-info": "^2.7.1", + "osenv": "^0.1.5", + "semver": "^5.6.0", + "validate-npm-package-name": "^3.0.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + }, + "dependencies": { + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + } + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "dev": true, + "requires": { + "boolbase": "~1.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "nwsapi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", + "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-inspect": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", + "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==", + "dev": true + }, + "object-is": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.4.tgz", + "integrity": "sha512-1ZvAZ4wlF7IyPVOcE1Omikt7UpaFlOQq0HlSti+ZvDH3UiD2brwGMwDbyV43jao2bKJ+4+WdPJHSd7kgzKYVqg==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "requires": { + "isobject": "^3.0.0" + } + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "requires": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "object.entries": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.3.tgz", + "integrity": "sha512-ym7h7OZebNS96hn5IJeyUmaWhaSM4SVtAPPfNLQEI2MYWCO2egsITb9nab2+i/Pwibx+R0mtn+ltKJXRSeTMGg==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1", + "has": "^1.0.3" + } + }, + "object.fromentries": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.3.tgz", + "integrity": "sha512-IDUSMXs6LOSJBWE++L0lzIbSqHl9KDCfff2x/JSEIDtEUavUnyMYC2ZGay/04Zq4UT8lvd4xNhU4/YHKibAOlw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1", + "has": "^1.0.3" + } + }, + "object.getownpropertydescriptors": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz", + "integrity": "sha512-6DtXgZ/lIZ9hqx4GtZETobXLR/ZLaa0aqV0kzbn80Rf8Z2e/XFnhA0I7p07N2wH8bBBltr2xQPi6sbKWAY2Eng==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + } + }, + "object.map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", + "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", + "requires": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "requires": { + "isobject": "^3.0.1" + } + }, + "object.values": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.2.tgz", + "integrity": "sha512-MYC0jvJopr8EK6dPBiO8Nb9mvjdypOachO5REGk6MXzujbBrAisKo3HmdEI6kZDL6fC31Mwee/5YbtMebixeag==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1", + "has": "^1.0.3" + } + }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "requires": { + "fn.name": "1.x.x" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "open": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/open/-/open-7.3.0.tgz", + "integrity": "sha512-mgLwQIx2F/ye9SmbrUkurZCnkoXyXyu9EbHtJZrICjVAJfyMArdHp3KkixGdZx1ZHFPNIwl0DDM1dFFqXbTLZw==", + "dev": true, + "requires": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + } + }, + "opn": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", + "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", + "dev": true, + "requires": { + "is-wsl": "^1.1.0" + }, + "dependencies": { + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true + } + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "ora": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-4.1.1.tgz", + "integrity": "sha512-sjYP8QyVWBpBZWD6Vr1M/KwknSw6kJOz41tvGMlwWeClHBtYKTbHMki1PsLZnxKpXMPbTKv9b3pjQu3REib96A==", + "dev": true, + "requires": { + "chalk": "^3.0.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.2.0", + "is-interactive": "^1.0.0", + "log-symbols": "^3.0.0", + "mute-stream": "0.0.8", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "dependencies": { + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "original": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", + "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", + "dev": true, + "requires": { + "url-parse": "^1.4.3" + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "p-each-series": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz", + "integrity": "sha512-ycIL2+1V32th+8scbpTvyHNaHe02z0sjgh91XXjAk+ZeXoPN4Z46DVUnzdso0aX4KckKw0FNNFHdjZ2UsZvxiA==", + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + }, + "p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "dev": true, + "requires": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + } + }, + "p-retry": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz", + "integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==", + "dev": true, + "requires": { + "retry": "^0.12.0" + } + }, + "p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dev": true, + "requires": { + "p-finally": "^1.0.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, + "parallel-transform": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", + "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", + "dev": true, + "requires": { + "cyclist": "^1.0.1", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "requires": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==", + "dev": true + } + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + }, + "dependencies": { + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + } + } + }, + "parse-asn1": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", + "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", + "dev": true, + "requires": { + "asn1.js": "^5.2.0", + "browserify-aes": "^1.0.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", + "requires": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "parse-package-name": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/parse-package-name/-/parse-package-name-0.1.0.tgz", + "integrity": "sha1-P0Tdg4/rTCvkvzGLrkR313BrreQ=", + "dev": true + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=" + }, + "parse-path": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-4.0.2.tgz", + "integrity": "sha512-HSqVz6iuXSiL8C1ku5Gl1Z5cwDd9Wo0q8CoffdAghP6bz8pJa1tcMC+m4N+z6VAS8QdksnIGq1TB6EgR4vPR6w==", + "requires": { + "is-ssh": "^1.3.0", + "protocols": "^1.4.0" + } + }, + "parse-url": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-5.0.2.tgz", + "integrity": "sha512-Czj+GIit4cdWtxo3ISZCvLiUjErSo0iI3wJ+q9Oi3QuMYTI6OZu+7cewMWZ+C1YAnKhYTk6/TLuhIgCypLthPA==", + "requires": { + "is-ssh": "^1.3.0", + "normalize-url": "^3.3.0", + "parse-path": "^4.0.0", + "protocols": "^1.4.0" + } + }, + "parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "dev": true + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==", + "dev": true + } + } + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" + }, + "path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, + "path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", + "requires": { + "path-root-regex": "^0.1.0" + } + }, + "path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "pbkdf2": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz", + "integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==", + "dev": true, + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "pg-connection-string": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.4.0.tgz", + "integrity": "sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ==" + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pirates": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", + "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", + "dev": true, + "requires": { + "node-modules-regexp": "^1.0.0" + } + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + } + }, + "pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + } + } + }, + "portfinder": { + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", + "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==", + "dev": true, + "requires": { + "async": "^2.6.2", + "debug": "^3.1.1", + "mkdirp": "^0.5.5" + }, + "dependencies": { + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" + }, + "postcss": { + "version": "7.0.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", + "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-calc": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.5.tgz", + "integrity": "sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg==", + "dev": true, + "requires": { + "postcss": "^7.0.27", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.0.2" + } + }, + "postcss-colormin": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-4.0.3.tgz", + "integrity": "sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "color": "^3.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-convert-values": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz", + "integrity": "sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==", + "dev": true, + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-discard-comments": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz", + "integrity": "sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-duplicates": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz", + "integrity": "sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-empty": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz", + "integrity": "sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-overridden": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz", + "integrity": "sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-load-config": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.1.2.tgz", + "integrity": "sha512-/rDeGV6vMUo3mwJZmeHfEDvwnTKKqQ0S7OHUi/kJvvtx3aWtyWG2/0ZWnzCt2keEclwN6Tf0DST2v9kITdOKYw==", + "dev": true, + "requires": { + "cosmiconfig": "^5.0.0", + "import-cwd": "^2.0.0" + }, + "dependencies": { + "import-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", + "integrity": "sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=", + "dev": true, + "requires": { + "import-from": "^2.1.0" + } + }, + "import-from": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz", + "integrity": "sha1-M1238qev/VOqpHHUuAId7ja387E=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + } + } + }, + "postcss-merge-longhand": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz", + "integrity": "sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==", + "dev": true, + "requires": { + "css-color-names": "0.0.4", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "stylehacks": "^4.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-merge-rules": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz", + "integrity": "sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-api": "^3.0.0", + "cssnano-util-same-parent": "^4.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0", + "vendors": "^1.0.0" + }, + "dependencies": { + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "requires": { + "is-obj": "^2.0.0" + } + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true + }, + "postcss-selector-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", + "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", + "dev": true, + "requires": { + "dot-prop": "^5.2.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "postcss-minify-font-values": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz", + "integrity": "sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==", + "dev": true, + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-minify-gradients": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz", + "integrity": "sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "is-color-stop": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-minify-params": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz", + "integrity": "sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.0", + "browserslist": "^4.0.0", + "cssnano-util-get-arguments": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "uniqs": "^2.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-minify-selectors": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz", + "integrity": "sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0" + }, + "dependencies": { + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "requires": { + "is-obj": "^2.0.0" + } + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true + }, + "postcss-selector-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", + "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", + "dev": true, + "requires": { + "dot-prop": "^5.2.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "postcss-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-2.0.0.tgz", + "integrity": "sha512-eqp+Bva+U2cwQO7dECJ8/V+X+uH1HduNeITB0CPPFAu6d/8LKQ32/j+p9rQ2YL1QytVcrNU0X+fBqgGmQIA1Rw==", + "dev": true, + "requires": { + "css-modules-loader-core": "^1.1.0", + "generic-names": "^2.0.1", + "lodash.camelcase": "^4.3.0", + "postcss": "^7.0.1", + "string-hash": "^1.1.1" + } + }, + "postcss-modules-extract-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", + "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", + "dev": true, + "requires": { + "postcss": "^7.0.5" + } + }, + "postcss-modules-local-by-default": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz", + "integrity": "sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==", + "dev": true, + "requires": { + "icss-utils": "^4.1.1", + "postcss": "^7.0.32", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-modules-scope": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz", + "integrity": "sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==", + "dev": true, + "requires": { + "postcss": "^7.0.6", + "postcss-selector-parser": "^6.0.0" + } + }, + "postcss-modules-values": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz", + "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==", + "dev": true, + "requires": { + "icss-utils": "^4.0.0", + "postcss": "^7.0.6" + } + }, + "postcss-normalize-charset": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz", + "integrity": "sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-normalize-display-values": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz", + "integrity": "sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==", + "dev": true, + "requires": { + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-positions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz", + "integrity": "sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-repeat-style": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz", + "integrity": "sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-string": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz", + "integrity": "sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==", + "dev": true, + "requires": { + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-timing-functions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz", + "integrity": "sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==", + "dev": true, + "requires": { + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-unicode": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz", + "integrity": "sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-url": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz", + "integrity": "sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==", + "dev": true, + "requires": { + "is-absolute-url": "^2.0.0", + "normalize-url": "^3.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-whitespace": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz", + "integrity": "sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==", + "dev": true, + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-ordered-values": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz", + "integrity": "sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-reduce-initial": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz", + "integrity": "sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-api": "^3.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0" + } + }, + "postcss-reduce-transforms": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz", + "integrity": "sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==", + "dev": true, + "requires": { + "cssnano-util-get-match": "^4.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-selector-parser": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz", + "integrity": "sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1", + "util-deprecate": "^1.0.2" + } + }, + "postcss-svgo": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.2.tgz", + "integrity": "sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==", + "dev": true, + "requires": { + "is-svg": "^3.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "svgo": "^1.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-unique-selectors": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz", + "integrity": "sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.0", + "postcss": "^7.0.0", + "uniqs": "^2.0.0" + } + }, + "postcss-value-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", + "dev": true + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "dev": true + }, + "pretty-error": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.2.tgz", + "integrity": "sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==", + "dev": true, + "requires": { + "lodash": "^4.17.20", + "renderkid": "^2.0.4" + } + }, + "pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + }, + "dependencies": { + "react-is": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz", + "integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==", + "dev": true + } + } + }, + "printj": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", + "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==" + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "promise.series": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/promise.series/-/promise.series-0.2.0.tgz", + "integrity": "sha1-LMfr6Vn8OmYZwEq029yeRS2GS70=", + "dev": true + }, + "prompts": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.0.tgz", + "integrity": "sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + } + }, + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "dev": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "property-expr": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.4.tgz", + "integrity": "sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg==" + }, + "protocols": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/protocols/-/protocols-1.4.8.tgz", + "integrity": "sha512-IgjKyaUSjsROSO8/D49Ab7hP8mJgTYcqApOqdPhLoPxAplXmkp+zRvsrSQjFn5by0rhm4VH0GAUELIPpx7B1yg==" + }, + "proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "dev": true + }, + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "dev": true, + "requires": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + } + } + }, + "raw-loader": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz", + "integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "react": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", + "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + } + }, + "react-dev-utils": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.2.1.tgz", + "integrity": "sha512-XxTbgJnYZmxuPtY3y/UV0D8/65NKkmaia4rXzViknVnZeVlklSh8u6TnaEYPfAi/Gh1TP4mEOXHI6jQOPbeakQ==", + "dev": true, + "requires": { + "@babel/code-frame": "7.8.3", + "address": "1.1.2", + "browserslist": "4.10.0", + "chalk": "2.4.2", + "cross-spawn": "7.0.1", + "detect-port-alt": "1.1.6", + "escape-string-regexp": "2.0.0", + "filesize": "6.0.1", + "find-up": "4.1.0", + "fork-ts-checker-webpack-plugin": "3.1.1", + "global-modules": "2.0.0", + "globby": "8.0.2", + "gzip-size": "5.1.1", + "immer": "1.10.0", + "inquirer": "7.0.4", + "is-root": "2.1.0", + "loader-utils": "1.2.3", + "open": "^7.0.2", + "pkg-up": "3.1.0", + "react-error-overlay": "^6.0.7", + "recursive-readdir": "2.2.2", + "shell-quote": "1.7.2", + "strip-ansi": "6.0.0", + "text-table": "0.2.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "browserslist": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.10.0.tgz", + "integrity": "sha512-TpfK0TDgv71dzuTsEAlQiHeWQ/tiPqgNZVdv046fvNtBZrjbv2O3TsWCDU0AWGJJKCF/KsjNdLzR9hXOsh/CfA==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001035", + "electron-to-chromium": "^1.3.378", + "node-releases": "^1.1.52", + "pkg-up": "^3.1.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + } + } + }, + "cli-width": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", + "dev": true + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "cross-spawn": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", + "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "dir-glob": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.0.0.tgz", + "integrity": "sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag==", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "path-type": "^3.0.0" + } + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true + }, + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "fork-ts-checker-webpack-plugin": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-3.1.1.tgz", + "integrity": "sha512-DuVkPNrM12jR41KM2e+N+styka0EgLkTnXmNcXdgOM37vtGeY+oCBK/Jx0hzSeEU6memFCtWb4htrHPMDfwwUQ==", + "dev": true, + "requires": { + "babel-code-frame": "^6.22.0", + "chalk": "^2.4.1", + "chokidar": "^3.3.0", + "micromatch": "^3.1.10", + "minimatch": "^3.0.4", + "semver": "^5.6.0", + "tapable": "^1.0.0", + "worker-rpc": "^0.1.0" + } + }, + "global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "requires": { + "global-prefix": "^3.0.0" + } + }, + "global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "requires": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "dependencies": { + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "globby": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-8.0.2.tgz", + "integrity": "sha512-yTzMmKygLp8RUpG1Ymu2VXPSJQZjNAZPD4ywgYEaG7e4tBJeUQBO8OpXrf1RCNcEs5alsoJYPAMiIHP0cmeC7w==", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "dir-glob": "2.0.0", + "fast-glob": "^2.0.2", + "glob": "^7.1.2", + "ignore": "^3.3.5", + "pify": "^3.0.0", + "slash": "^1.0.0" + } + }, + "ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, + "inquirer": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz", + "integrity": "sha512-Bu5Td5+j11sCkqfqmUTiwv+tWisMtP0L7Q8WrqA2C/BbBhy1YTdFrvjjlrKq8oagA/tLQBski2Gcx/Sqyi2qSQ==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^2.4.2", + "cli-cursor": "^3.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.15", + "mute-stream": "0.0.8", + "run-async": "^2.2.0", + "rxjs": "^6.5.3", + "string-width": "^4.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + }, + "dependencies": { + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "react-error-overlay": { + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.8.tgz", + "integrity": "sha512-HvPuUQnLp5H7TouGq3kzBeioJmXms1wHy9EGjz2OURWBp4qZO6AfGEcnxts1D/CbwPLRAgTMPCEgYhA3sEM4vw==", + "dev": true + }, + "react-hot-loader": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.13.0.tgz", + "integrity": "sha512-JrLlvUPqh6wIkrK2hZDfOyq/Uh/WeVEr8nc7hkn2/3Ul0sx1Kr5y4kOGNacNRoj7RhwLNcQ3Udf1KJXrqc0ZtA==", + "dev": true, + "requires": { + "fast-levenshtein": "^2.0.6", + "global": "^4.3.0", + "hoist-non-react-statics": "^3.3.0", + "loader-utils": "^1.1.0", + "prop-types": "^15.6.1", + "react-lifecycles-compat": "^3.0.4", + "shallowequal": "^1.1.0", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + } + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "dev": true + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + }, + "dependencies": { + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdir-glob": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.1.tgz", + "integrity": "sha512-91/k1EzZwDx6HbERR+zucygRFfiPl2zkIYZtv3Jjr6Mn7SkKcVct8aVO+sSRiGMc6fLf72du3d92/uY63YPdEA==", + "requires": { + "minimatch": "^3.0.4" + } + }, + "readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "requires": { + "resolve": "^1.1.6" + } + }, + "recursive-readdir": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz", + "integrity": "sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg==", + "dev": true, + "requires": { + "minimatch": "3.0.4" + } + }, + "regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "regenerate-unicode-properties": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz", + "integrity": "sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==", + "dev": true, + "requires": { + "regenerate": "^1.4.0" + } + }, + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" + }, + "regenerator-transform": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", + "integrity": "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.8.4" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regexp.prototype.flags": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", + "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + }, + "regexpp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "dev": true + }, + "regexpu-core": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.1.tgz", + "integrity": "sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==", + "dev": true, + "requires": { + "regenerate": "^1.4.0", + "regenerate-unicode-properties": "^8.2.0", + "regjsgen": "^0.5.1", + "regjsparser": "^0.6.4", + "unicode-match-property-ecmascript": "^1.0.4", + "unicode-match-property-value-ecmascript": "^1.2.0" + } + }, + "regjsgen": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", + "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==", + "dev": true + }, + "regjsparser": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.4.tgz", + "integrity": "sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw==", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + } + } + }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", + "dev": true + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "renderkid": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.5.tgz", + "integrity": "sha512-ccqoLg+HLOHq1vdfYNm4TBeaCDIi1FLt3wGojTDSvdewUv65oTmI3cnT2E4hRjl1gzKZIPK+KZrXzlUYKnR+vQ==", + "dev": true, + "requires": { + "css-select": "^2.0.2", + "dom-converter": "^0.2", + "htmlparser2": "^3.10.1", + "lodash": "^4.17.20", + "strip-ansi": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "replace-in-file": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-6.1.0.tgz", + "integrity": "sha512-URzjyF3nucvejuY13HFd7O+Q6tFJRLKGHLYVvSh+LiZj3gFXzSYGnIkQflnJJulCAI2/RTZaZkpOtdVdW0EhQA==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "glob": "^7.1.6", + "yargs": "^15.3.1" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", + "dev": true + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, + "request-promise-core": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", + "dev": true, + "requires": { + "lodash": "^4.17.19" + } + }, + "request-promise-native": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", + "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", + "dev": true, + "requires": { + "request-promise-core": "1.1.4", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + }, + "dependencies": { + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + } + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "dev": true + }, + "resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "requires": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "requires": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rgb-regex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", + "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=", + "dev": true + }, + "rgba-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", + "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "requires": { + "glob": "^7.1.3" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "rollup": { + "version": "2.33.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.33.3.tgz", + "integrity": "sha512-RpayhPTe4Gu/uFGCmk7Gp5Z9Qic2VsqZ040G+KZZvsZYdcuWaJg678JeDJJvJeEQXminu24a2au+y92CUWVd+w==", + "dev": true, + "requires": { + "fsevents": "~2.1.2" + } + }, + "rollup-plugin-dts": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-1.4.13.tgz", + "integrity": "sha512-7mxoQ6PcmCkBE5ZhrjGDL4k42XLy8BkSqpiRi1MipwiGs+7lwi4mQkp2afX+OzzLjJp/TGM8llfe8uayIUhPEw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4" + } + }, + "rollup-plugin-esbuild": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-esbuild/-/rollup-plugin-esbuild-2.6.1.tgz", + "integrity": "sha512-hskMEQQ4Vxlyoeg1OWlFTwWHIhpNaw6q+diOT7p9pdkk34m9Mbk3aymS/JbTqLXy/AbJi22iuXrucknKpeczfg==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^4.1.0", + "joycon": "^2.2.5", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "@rollup/pluginutils": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.1.0.tgz", + "integrity": "sha512-TrBhfJkFxA+ER+ew2U2/fHbebhLT/l/2pRk0hfj9KusXUuRXd2v0R58AfaZK9VXDQ4TogOSEmICVrQAA3zFnHQ==", + "dev": true, + "requires": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + } + } + } + }, + "rollup-plugin-peer-deps-external": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/rollup-plugin-peer-deps-external/-/rollup-plugin-peer-deps-external-2.2.4.tgz", + "integrity": "sha512-AWdukIM1+k5JDdAqV/Cxd+nejvno2FVLVeZ74NKggm3Q5s9cbbcOgUPGdbxPi4BXu7xGaZ8HG12F+thImYu/0g==", + "dev": true + }, + "rollup-plugin-postcss": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/rollup-plugin-postcss/-/rollup-plugin-postcss-3.1.8.tgz", + "integrity": "sha512-JHnGfW8quNc6ePxEkZ05HEZ1YiRxDgY9RKEetMfsrwxR2kh/d90OVScTc6b1c2Q17Cs/5TRYL+1uddG21lQe3w==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "concat-with-sourcemaps": "^1.1.0", + "cssnano": "^4.1.10", + "import-cwd": "^3.0.0", + "p-queue": "^6.3.0", + "pify": "^5.0.0", + "postcss": "^7.0.27", + "postcss-load-config": "^2.1.0", + "postcss-modules": "^2.0.0", + "promise.series": "^0.2.0", + "resolve": "^1.16.1", + "rollup-pluginutils": "^2.8.2", + "safe-identifier": "^0.4.1", + "style-inject": "^0.3.0" + }, + "dependencies": { + "pify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", + "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==", + "dev": true + } + } + }, + "rollup-plugin-typescript2": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.27.3.tgz", + "integrity": "sha512-gmYPIFmALj9D3Ga1ZbTZAKTXq1JKlTQBtj299DXhqYz9cL3g/AQfUvbb2UhH+Nf++cCq941W2Mv7UcrcgLzJJg==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "find-cache-dir": "^3.3.1", + "fs-extra": "8.1.0", + "resolve": "1.17.0", + "tslib": "2.0.1" + }, + "dependencies": { + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "tslib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==", + "dev": true + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + } + } + }, + "rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "requires": { + "estree-walker": "^0.6.1" + }, + "dependencies": { + "estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true + } + } + }, + "rsvp": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", + "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", + "dev": true + }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, + "run-parallel": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.10.tgz", + "integrity": "sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw==", + "dev": true + }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "dev": true, + "requires": { + "aproba": "^1.1.1" + } + }, + "rxjs": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-identifier": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/safe-identifier/-/safe-identifier-0.4.2.tgz", + "integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==", + "dev": true + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sane": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", + "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", + "dev": true, + "requires": { + "@cnakazawa/watch": "^1.0.3", + "anymatch": "^2.0.0", + "capture-exit": "^2.0.0", + "exec-sh": "^0.3.2", + "execa": "^1.0.0", + "fb-watchman": "^2.0.0", + "micromatch": "^3.1.4", + "minimist": "^1.1.1", + "walker": "~1.0.5" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "dev": true, + "requires": { + "xmlchars": "^2.2.0" + } + }, + "scheduler": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", + "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + } + }, + "select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", + "dev": true + }, + "selfsigned": { + "version": "1.10.8", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.8.tgz", + "integrity": "sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w==", + "requires": { + "node-forge": "^0.10.0" + } + }, + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "dependencies": { + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shallow-clone": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz", + "integrity": "sha1-WQnodLp3EG1zrEFM/sH/yofZcGA=", + "dev": true, + "requires": { + "is-extendable": "^0.1.1", + "kind-of": "^2.0.1", + "lazy-cache": "^0.2.3", + "mixin-object": "^2.0.1" + }, + "dependencies": { + "kind-of": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", + "integrity": "sha1-AY7HpM5+OobLkUG+UZ0kyPqpgbU=", + "dev": true, + "requires": { + "is-buffer": "^1.0.2" + } + }, + "lazy-cache": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", + "integrity": "sha1-f+3fLctu23fRHvHRF6tf/fCrG2U=", + "dev": true + } + } + }, + "shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "shell-quote": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", + "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", + "dev": true + }, + "shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "dev": true, + "optional": true + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "requires": { + "is-arrayish": "^0.3.1" + } + }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "sockjs": { + "version": "0.3.21", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.21.tgz", + "integrity": "sha512-DhbPFGpxjc6Z3I+uX07Id5ZO2XwYsWOrYjaSeieES78cq+JaJvVe5q/m1uvjIQhXinhIeCFRH6JgXe+mvVMyXw==", + "dev": true, + "requires": { + "faye-websocket": "^0.11.3", + "uuid": "^3.4.0", + "websocket-driver": "^0.7.4" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, + "sockjs-client": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.5.0.tgz", + "integrity": "sha512-8Dt3BDi4FYNrCFGTL/HtwVzkARrENdwOUf1ZoW/9p3M8lZdFT35jVdrHza+qgxuG9H3/shR4cuX/X9umUrjP8Q==", + "dev": true, + "requires": { + "debug": "^3.2.6", + "eventsource": "^1.0.7", + "faye-websocket": "^0.11.3", + "inherits": "^2.0.4", + "json3": "^3.3.3", + "url-parse": "^1.4.7" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "sort-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", + "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" + }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz", + "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==", + "dev": true + }, + "spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "ssri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1" + } + }, + "stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "dev": true + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" + }, + "stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-gL//fkxfWUsIlFL2Tl42Cl6+HFALEaB1FU76I/Fy+oZjRreP7OPMXFlGbxM7NQsI0ZpUfw76sHnv0WNYuTb7Iw==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + } + } + }, + "start-server-webpack-plugin": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/start-server-webpack-plugin/-/start-server-webpack-plugin-2.2.5.tgz", + "integrity": "sha512-DRCkciwCJoCFZ+wt3wWMkR1M2mpVhJbUKFXqhK3FWyIUKYb42NnocH5sMwqgo+nPNHupqNwK/v8lgfBbr2NKdg==", + "dev": true + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "dev": true + }, + "stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==" + }, + "stream-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "stream-each": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "dev": true, + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "dev": true + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "dev": true + }, + "string-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", + "integrity": "sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs=", + "dev": true + }, + "string-length": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.1.tgz", + "integrity": "sha512-PKyXUd0LK0ePjSOnWn34V2uD6acUWev9uy0Ft05k0E8xRW+SKcA0F7eMr7h5xlzfn+4O3N+55rduYyet3Jk+jw==", + "dev": true, + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + } + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "string.prototype.matchall": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.3.tgz", + "integrity": "sha512-OBxYDA2ifZQ2e13cP82dWFMaCV9CGF8GzmN4fljBVw5O5wep0lu4gacm1OL6MjROoUnB8VbkWRThqkV2YFLNxw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1", + "has-symbols": "^1.0.1", + "internal-slot": "^1.0.2", + "regexp.prototype.flags": "^1.3.0", + "side-channel": "^1.0.3" + } + }, + "string.prototype.trimend": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz", + "integrity": "sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz", + "integrity": "sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "style-inject": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/style-inject/-/style-inject-0.3.0.tgz", + "integrity": "sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==", + "dev": true + }, + "style-loader": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.3.0.tgz", + "integrity": "sha512-V7TCORko8rs9rIqkSrlMfkqA63DfoGBBJmK1kKGCcSi+BWb4cqz0SRsnp4l6rU5iwOEd0/2ePv68SV22VXon4Q==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^2.7.0" + }, + "dependencies": { + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + } + } + }, + "stylehacks": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz", + "integrity": "sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0" + }, + "dependencies": { + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "requires": { + "is-obj": "^2.0.0" + } + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true + }, + "postcss-selector-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", + "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", + "dev": true, + "requires": { + "dot-prop": "^5.2.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "sucrase": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.17.0.tgz", + "integrity": "sha512-wtiqaokYRjFSSrv8fQu7pThKTIZSLwiffW+PHQG52hlI8eJO47v1tXbKt6fYb8Z1kCyuCkNH9etpTUebb7g+pA==", + "dev": true, + "requires": { + "commander": "^4.0.0", + "glob": "7.1.6", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "dependencies": { + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true + } + } + }, + "superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "dev": true, + "requires": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.2.0", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.3.5" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "supertest": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-4.0.2.tgz", + "integrity": "sha512-1BAbvrOZsGA3YTCWqbmh14L0YEq0EGICX/nBnfkfVJn7SrxQV1I3pMYjSzG9y/7ZU2V9dWqyqk2POwxlb09duQ==", + "dev": true, + "requires": { + "methods": "^1.1.2", + "superagent": "^3.8.3" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "supports-hyperlinks": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz", + "integrity": "sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA==", + "dev": true, + "requires": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "dev": true + }, + "svgo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", + "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "coa": "^2.0.2", + "css-select": "^2.0.0", + "css-select-base-adapter": "^0.1.1", + "css-tree": "1.0.0-alpha.37", + "csso": "^4.0.2", + "js-yaml": "^3.13.1", + "mkdirp": "~0.5.1", + "object.values": "^1.1.0", + "sax": "~1.2.4", + "stable": "^0.1.8", + "unquote": "~1.1.1", + "util.promisify": "~1.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + } + } + }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "synchronous-promise": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.15.tgz", + "integrity": "sha512-k8uzYIkIVwmT+TcglpdN50pS2y1BDcUnBPK9iJeGu0Pl1lOI8pD6wtzgw91Pjpe+RxtTncw32tLxs/R0yNL2Mg==" + }, + "table": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/table/-/table-6.0.7.tgz", + "integrity": "sha512-rxZevLGTUzWna/qBLObOe16kB2RTnnbhciwgPbMMlazz1yZGVEgnZK762xyVdVznhqxrfCeBMmMkgOOaPwjH7g==", + "dev": true, + "requires": { + "ajv": "^7.0.2", + "lodash": "^4.17.20", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.0" + }, + "dependencies": { + "ajv": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-7.0.3.tgz", + "integrity": "sha512-R50QRlXSxqXcQP5SvKUrw8VZeypvo12i2IX0EeR5PiZ7bEKeHWgzgo264LDadUsCU42lTJVhFikTqJwNeH34gQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true + }, + "tar": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.0.5.tgz", + "integrity": "sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, + "tarn": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.1.tgz", + "integrity": "sha512-6usSlV9KyHsspvwu2duKH+FMUhqJnAh6J5J/4MITl8s94iSUQTLkJggdiewKv4RyARQccnigV48Z+khiuVZDJw==" + }, + "terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + } + }, + "terser": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", + "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", + "dev": true, + "requires": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "terser-webpack-plugin": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz", + "integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==", + "dev": true, + "requires": { + "cacache": "^12.0.2", + "find-cache-dir": "^2.1.0", + "is-wsl": "^1.1.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^4.0.0", + "source-map": "^0.6.1", + "terser": "^4.1.2", + "webpack-sources": "^1.4.0", + "worker-farm": "^1.7.0" + }, + "dependencies": { + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "requires": { + "any-promise": "^1.0.0" + } + }, + "thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", + "dev": true, + "requires": { + "thenify": ">= 3.1.0 < 4" + } + }, + "throat": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", + "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "tildify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", + "integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==" + }, + "timers-browserify": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", + "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", + "dev": true, + "requires": { + "setimmediate": "^1.0.4" + } + }, + "timsort": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", + "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "tmpl": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", + "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", + "dev": true + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=" + }, + "tosource": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tosource/-/tosource-1.0.0.tgz", + "integrity": "sha1-QtiN0RZhi88A1hBt1URvNCeQL/E=", + "dev": true + }, + "tough-cookie": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", + "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", + "dev": true, + "requires": { + "ip-regex": "^2.1.0", + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tr46": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.0.2.tgz", + "integrity": "sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==", + "dev": true, + "requires": { + "punycode": "^2.1.1" + } + }, + "traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=" + }, + "triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" + }, + "tryer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", + "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==", + "dev": true + }, + "ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "ts-jest": { + "version": "26.4.4", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.4.4.tgz", + "integrity": "sha512-3lFWKbLxJm34QxyVNNCgXX1u4o/RV0myvA2y2Bxm46iGIjKlaY0own9gIckbjZJPn+WaJEnfPPJ20HHGpoq4yg==", + "dev": true, + "requires": { + "@types/jest": "26.x", + "bs-logger": "0.x", + "buffer-from": "1.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^26.1.0", + "json5": "2.x", + "lodash.memoize": "4.x", + "make-error": "1.x", + "mkdirp": "1.x", + "semver": "7.x", + "yargs-parser": "20.x" + }, + "dependencies": { + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + } + } + }, + "ts-loader": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-7.0.5.tgz", + "integrity": "sha512-zXypEIT6k3oTc+OZNx/cqElrsbBtYqDknf48OZos0NQ3RTt045fBIU8RRSu+suObBzYB355aIPGOe/3kj9h7Ig==", + "dev": true, + "requires": { + "chalk": "^2.3.0", + "enhanced-resolve": "^4.0.0", + "loader-utils": "^1.0.2", + "micromatch": "^4.0.0", + "semver": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "tsconfig-paths": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", + "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.18.0.tgz", + "integrity": "sha512-D9Tu8nE3E7D1Bsf/V29oMHceMf+gnVO+pDguk/A5YRo1cLpkiQ48ZnbbS57pvvHeY+OIeNQx1vf4ASPlEtRpcA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", + "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "typescript": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz", + "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==" + }, + "typescript-json-schema": { + "version": "0.45.1", + "resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.45.1.tgz", + "integrity": "sha512-Iz1NKVtJi09iZSzXj3J350GekBrZ1TiNw6Cbf42jLOGyooh9BWoi8XP6XlTIMOI3aMD8XxuL9hEvIEq8FD/WUw==", + "requires": { + "@types/json-schema": "^7.0.6", + "glob": "^7.1.6", + "json-stable-stringify": "^1.0.1", + "typescript": "^4.1.3", + "yargs": "^16.2.0" + } + }, + "uglify-js": { + "version": "3.12.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.12.4.tgz", + "integrity": "sha512-L5i5jg/SHkEqzN18gQMTWsZk3KelRsfD1wUVNqtq0kzqWQqcJjyL8yc1o8hJgRrWqrAl2mUFbhfznEIoi7zi2A==", + "dev": true, + "optional": true + }, + "unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=" + }, + "unicode-canonical-property-names-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", + "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", + "dev": true + }, + "unicode-match-property-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", + "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", + "dev": true, + "requires": { + "unicode-canonical-property-names-ecmascript": "^1.0.4", + "unicode-property-aliases-ecmascript": "^1.0.4" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz", + "integrity": "sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==", + "dev": true + }, + "unicode-property-aliases-ecmascript": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz", + "integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==", + "dev": true + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "dev": true + }, + "uniqs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", + "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=", + "dev": true + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "universalify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", + "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "unquote": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", + "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=", + "dev": true + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" + } + } + }, + "unzipper": { + "version": "0.10.11", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz", + "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==", + "requires": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true + }, + "uri-js": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", + "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "url-loader": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz", + "integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "mime-types": "^2.1.27", + "schema-utils": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "url-parse": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", + "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", + "dev": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" + }, + "util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "dev": true, + "requires": { + "inherits": "2.0.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "util.promisify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", + "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.2", + "has-symbols": "^1.0.1", + "object.getownpropertydescriptors": "^2.1.0" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + }, + "utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "v8-compile-cache": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz", + "integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==", + "dev": true + }, + "v8-to-istanbul": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-7.1.0.tgz", + "integrity": "sha512-uXUVqNUCLa0AH1vuVxzi+MI4RfxEOKt9pBgKwHbgH7st8Kv2P1m+jvWNnektzBh5QShF3ODgKmUFCf38LnVz1g==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + } + } + }, + "v8flags": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", + "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "validate-npm-package-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", + "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=", + "dev": true, + "requires": { + "builtins": "^1.0.3" + } + }, + "validate.io-array": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/validate.io-array/-/validate.io-array-1.0.6.tgz", + "integrity": "sha1-W1osr9j4uFq7L4hroVPy2Tond00=" + }, + "validate.io-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/validate.io-function/-/validate.io-function-1.0.2.tgz", + "integrity": "sha1-NDoZgC7TsZaCaceA5VjpNBHAutc=" + }, + "validate.io-integer": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/validate.io-integer/-/validate.io-integer-1.0.5.tgz", + "integrity": "sha1-FoSWSAuVviJH7EQ/IjPeT4mHgGg=", + "requires": { + "validate.io-number": "^1.0.3" + } + }, + "validate.io-integer-array": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/validate.io-integer-array/-/validate.io-integer-array-1.0.0.tgz", + "integrity": "sha1-LKveAzKTpry+Bj/q/pHq9GsToIk=", + "requires": { + "validate.io-array": "^1.0.3", + "validate.io-integer": "^1.0.4" + } + }, + "validate.io-number": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/validate.io-number/-/validate.io-number-1.0.3.tgz", + "integrity": "sha1-9j/+2iSL8opnqNSODjtGGhZluvg=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "vendors": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz", + "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", + "dev": true + }, + "w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "dev": true, + "requires": { + "browser-process-hrtime": "^1.0.0" + } + }, + "w3c-xmlserializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", + "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "dev": true, + "requires": { + "xml-name-validator": "^3.0.0" + } + }, + "walker": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", + "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", + "dev": true, + "requires": { + "makeerror": "1.0.x" + } + }, + "watchpack": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", + "integrity": "sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==", + "dev": true, + "requires": { + "chokidar": "^3.4.1", + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0", + "watchpack-chokidar2": "^2.0.1" + } + }, + "watchpack-chokidar2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz", + "integrity": "sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==", + "dev": true, + "optional": true, + "requires": { + "chokidar": "^2.1.8" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "optional": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "optional": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true, + "optional": true + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "optional": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "dev": true, + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "optional": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "optional": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "optional": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "optional": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + } + } + }, + "wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "requires": { + "minimalistic-assert": "^1.0.0" + } + }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "dev": true, + "requires": { + "defaults": "^1.0.3" + } + }, + "webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "dev": true + }, + "webpack": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.44.2.tgz", + "integrity": "sha512-6KJVGlCxYdISyurpQ0IPTklv+DULv05rs2hseIXer6D7KrUicRDLFb4IUM1S6LUAKypPM/nSiVSuv8jHu1m3/Q==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/wasm-edit": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "acorn": "^6.4.1", + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^4.3.0", + "eslint-scope": "^4.0.3", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^2.4.0", + "loader-utils": "^1.2.3", + "memory-fs": "^0.4.1", + "micromatch": "^3.1.10", + "mkdirp": "^0.5.3", + "neo-async": "^2.6.1", + "node-libs-browser": "^2.2.1", + "schema-utils": "^1.0.0", + "tapable": "^1.1.3", + "terser-webpack-plugin": "^1.4.3", + "watchpack": "^1.7.4", + "webpack-sources": "^1.4.1" + }, + "dependencies": { + "acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "dev": true + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, + "webpack-dev-middleware": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz", + "integrity": "sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ==", + "dev": true, + "requires": { + "memory-fs": "^0.4.1", + "mime": "^2.4.4", + "mkdirp": "^0.5.1", + "range-parser": "^1.2.1", + "webpack-log": "^2.0.0" + }, + "dependencies": { + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "mime": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.7.tgz", + "integrity": "sha512-dhNd1uA2u397uQk3Nv5LM4lm93WYDUXFn3Fu291FJerns4jyTudqhIWe4W04YLy7Uk1tm1Ore04NpjRvQp/NPA==", + "dev": true + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "webpack-dev-server": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.1.tgz", + "integrity": "sha512-u4R3mRzZkbxQVa+MBWi2uVpB5W59H3ekZAJsQlKUTdl7Elcah2EhygTPLmeFXybQkf9i2+L0kn7ik9SnXa6ihQ==", + "dev": true, + "requires": { + "ansi-html": "0.0.7", + "bonjour": "^3.5.0", + "chokidar": "^2.1.8", + "compression": "^1.7.4", + "connect-history-api-fallback": "^1.6.0", + "debug": "^4.1.1", + "del": "^4.1.1", + "express": "^4.17.1", + "html-entities": "^1.3.1", + "http-proxy-middleware": "0.19.1", + "import-local": "^2.0.0", + "internal-ip": "^4.3.0", + "ip": "^1.1.5", + "is-absolute-url": "^3.0.3", + "killable": "^1.0.1", + "loglevel": "^1.6.8", + "opn": "^5.5.0", + "p-retry": "^3.0.1", + "portfinder": "^1.0.26", + "schema-utils": "^1.0.0", + "selfsigned": "^1.10.8", + "semver": "^6.3.0", + "serve-index": "^1.9.1", + "sockjs": "^0.3.21", + "sockjs-client": "^1.5.0", + "spdy": "^4.0.2", + "strip-ansi": "^3.0.1", + "supports-color": "^6.1.0", + "url": "^0.11.0", + "webpack-dev-middleware": "^3.7.2", + "webpack-log": "^2.0.0", + "ws": "^6.2.1", + "yargs": "^13.3.2" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "dev": true, + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "dev": true, + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + } + }, + "is-absolute-url": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", + "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==", + "dev": true + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "ws": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", + "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", + "dev": true, + "requires": { + "async-limiter": "~1.0.0" + } + }, + "y18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", + "dev": true + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "webpack-log": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", + "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", + "dev": true, + "requires": { + "ansi-colors": "^3.0.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, + "webpack-node-externals": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-2.5.2.tgz", + "integrity": "sha512-aHdl/y2N7PW2Sx7K+r3AxpJO+aDMcYzMQd60Qxefq3+EwhewSbTBqNumOsCE1JsCUNoyfGj5465N0sSf6hc/5w==", + "dev": true + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true + }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "dev": true, + "requires": { + "iconv-lite": "0.4.24" + } + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "dev": true + }, + "whatwg-url": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.4.0.tgz", + "integrity": "sha512-vwTUFf6V4zhcPkWp/4CQPr1TW9Ml6SF4lVyaIMBdJw5i6qUUJ1QWM4Z6YYVkfka0OUIzVo/0aNtGVGk256IKWw==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^2.0.2", + "webidl-conversions": "^6.1.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "winston": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz", + "integrity": "sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==", + "requires": { + "@dabh/diagnostics": "^2.0.2", + "async": "^3.1.0", + "is-stream": "^2.0.0", + "logform": "^2.2.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.4.0" + } + }, + "winston-transport": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz", + "integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==", + "requires": { + "readable-stream": "^2.3.7", + "triple-beam": "^1.2.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "worker-farm": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", + "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", + "dev": true, + "requires": { + "errno": "~0.1.7" + } + }, + "worker-rpc": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/worker-rpc/-/worker-rpc-0.1.1.tgz", + "integrity": "sha512-P1WjMrUB3qgJNI9jfmpZ/htmBEjFh//6l/5y8SD9hg1Ef5zTTVVoRjTrTEzPrNBQvmhMxkoTsjOXN10GWU7aCg==", + "dev": true, + "requires": { + "microevent.ts": "~0.1.1" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "write-json-file": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/write-json-file/-/write-json-file-3.2.0.tgz", + "integrity": "sha512-3xZqT7Byc2uORAatYiP3DHUUAVEkNOswEWNs9H5KXiicRTvzYzYqKjYc4G7p+8pltvAw641lVByKVtMpf+4sYQ==", + "dev": true, + "requires": { + "detect-indent": "^5.0.0", + "graceful-fs": "^4.1.15", + "make-dir": "^2.1.0", + "pify": "^4.0.1", + "sort-keys": "^2.0.0", + "write-file-atomic": "^2.4.2" + }, + "dependencies": { + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "write-pkg": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/write-pkg/-/write-pkg-3.2.0.tgz", + "integrity": "sha512-tX2ifZ0YqEFOF1wjRW2Pk93NLsj02+n1UP5RvO6rCs0K6R2g1padvf006cY74PQJKMGS2r42NK7FD0dG6Y6paw==", + "dev": true, + "requires": { + "sort-keys": "^2.0.0", + "write-json-file": "^2.2.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "write-json-file": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/write-json-file/-/write-json-file-2.3.0.tgz", + "integrity": "sha1-K2TIozAE1UuGmMdtWFp3zrYdoy8=", + "dev": true, + "requires": { + "detect-indent": "^5.0.0", + "graceful-fs": "^4.1.2", + "make-dir": "^1.0.0", + "pify": "^3.0.0", + "sort-keys": "^2.0.0", + "write-file-atomic": "^2.0.0" + } + } + } + }, + "ws": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.2.tgz", + "integrity": "sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==", + "dev": true + }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "dev": true + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, + "y18n": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz", + "integrity": "sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yaml": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", + "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==" + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==" + }, + "yml-loader": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/yml-loader/-/yml-loader-2.1.0.tgz", + "integrity": "sha512-mo42d5FQWlXxpyTEpYywPu1LzK3F69pPPCOB8WKgJi8s+aqaogQP7XnXTjSobbKzzlZ/wXm7kg9CkP4x4ZnVMw==", + "dev": true, + "requires": { + "js-yaml": "^3.8.3", + "loader-utils": "^1.1.0" + } + }, + "yn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-4.0.0.tgz", + "integrity": "sha512-huWiiCS4TxKc4SfgmTwW1K7JmXPPAmuXWYy4j9qjQo4+27Kni8mGhAAi1cloRWmBe2EqcLgt3IGqQoRL/MtPgg==", + "dev": true + }, + "yup": { + "version": "0.29.3", + "resolved": "https://registry.npmjs.org/yup/-/yup-0.29.3.tgz", + "integrity": "sha512-RNUGiZ/sQ37CkhzKFoedkeMfJM0vNQyaz+wRZJzxdKE7VfDeVKH8bb4rr7XhRLbHJz5hSjoDNwMEIaKhuMZ8gQ==", + "requires": { + "@babel/runtime": "^7.10.5", + "fn-name": "~3.0.0", + "lodash": "^4.17.15", + "lodash-es": "^4.17.11", + "property-expr": "^2.0.2", + "synchronous-promise": "^2.0.13", + "toposort": "^2.0.2" + } + }, + "zip-stream": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.0.4.tgz", + "integrity": "sha512-a65wQ3h5gcQ/nQGWV1mSZCEzCML6EK/vyVPcrPNynySP1j3VBbQKh3nhC8CbORb+jfl2vXvh56Ul5odP1bAHqw==", + "requires": { + "archiver-utils": "^2.1.0", + "compress-commons": "^4.0.2", + "readable-stream": "^3.6.0" + } + } + } +} diff --git a/plugins/kafka-backend/package.json b/plugins/kafka-backend/package.json new file mode 100644 index 0000000000..a77d7981c9 --- /dev/null +++ b/plugins/kafka-backend/package.json @@ -0,0 +1,48 @@ +{ + "name": "@backstage/plugin-kafka-backend", + "version": "0.1.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/kafka-backend" + }, + "keywords": [ + "backstage", + "kafka" + ], + "configSchema": "config.d.ts", + "scripts": { + "start": "backstage-cli backend:dev", + "build": "backstage-cli backend:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/backend-common": "^0.4.1", + "@backstage/catalog-model": "^0.6.0", + "@backstage/config": "^0.1.2", + "kafkajs": "^1.16.0-beta.6", + "winston": "^3.3.3" + }, + "devDependencies": { + "@backstage/cli": "^0.4.2", + "supertest": "^4.0.2" + }, + "files": [ + "dist", + "schema.d.ts" + ] +} diff --git a/plugins/kafka-backend/src/index.test.ts b/plugins/kafka-backend/src/index.test.ts new file mode 100644 index 0000000000..4fca4ca746 --- /dev/null +++ b/plugins/kafka-backend/src/index.test.ts @@ -0,0 +1,23 @@ +/* + * 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 '@backstage/backend-common'; + +describe('test', () => { + it('unbreaks the test runner', () => { + expect(true).toBeTruthy(); + }); +}); diff --git a/plugins/kafka/src/api/index.ts b/plugins/kafka-backend/src/index.ts similarity index 90% rename from plugins/kafka/src/api/index.ts rename to plugins/kafka-backend/src/index.ts index cd78c09cad..96f070ff1e 100644 --- a/plugins/kafka/src/api/index.ts +++ b/plugins/kafka-backend/src/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ -export { KafkaApi } from './KafkaApi'; +export * from './service/router'; +export * from './types/types'; diff --git a/plugins/kafka-backend/src/run.ts b/plugins/kafka-backend/src/run.ts new file mode 100644 index 0000000000..688191d406 --- /dev/null +++ b/plugins/kafka-backend/src/run.ts @@ -0,0 +1,33 @@ +/* + * 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 yn from 'yn'; +import { getRootLogger } from '@backstage/backend-common'; +import { startStandaloneServer } from './service/standaloneServer'; + +const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7000; +const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); +const logger = getRootLogger(); + +startStandaloneServer({ port, enableCors, logger }).catch(err => { + logger.error(err); + process.exit(1); +}); + +process.on('SIGINT', () => { + logger.info('CTRL+C pressed; exiting.'); + process.exit(0); +}); diff --git a/plugins/kafka-backend/src/service/KafkaApi.ts b/plugins/kafka-backend/src/service/KafkaApi.ts new file mode 100644 index 0000000000..8b51b41439 --- /dev/null +++ b/plugins/kafka-backend/src/service/KafkaApi.ts @@ -0,0 +1,82 @@ +/* + * 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 { Kafka, SeekEntry } from 'kafkajs'; +import { Logger } from 'winston'; + +export type PartitionOffset = { + id: number; + offset: string; +}; + +export type TopicOffset = { + topic: string; + partitions: PartitionOffset[]; +}; + +export class KafkaApi { + private readonly kafka: Kafka; + private readonly logger: Logger; + + constructor(clientId: string, brokers: string[], logger: Logger) { + logger.info( + `creating kafka client with clientId=${clientId} and brokers=${brokers}`, + ); + + this.kafka = new Kafka({ clientId, brokers }); + this.logger = logger; + } + + async fetchTopicOffsets(topic: string): Promise> { + this.logger.info(`fetching topic offsets for ${topic}`); + + const admin = this.kafka.admin(); + await admin.connect(); + + try { + return KafkaApi.toPartitionOffsets(await admin.fetchTopicOffsets(topic)); + } finally { + await admin.disconnect(); + } + } + + async fetchGroupOffsets(groupId: string): Promise> { + this.logger.info(`fetching consumer group offsets for ${groupId}`); + + const admin = this.kafka.admin(); + await admin.connect(); + + try { + let groupOffsets = await admin.fetchOffsets({ groupId }); + + return groupOffsets.map(topicOffset => ({ + topic: topicOffset.topic, + partitions: KafkaApi.toPartitionOffsets(topicOffset.partitions), + })); + } finally { + await admin.disconnect(); + } + } + + private static toPartitionOffsets( + result: Array, + ): Array { + return result.map(seekEntry => ({ + id: seekEntry.partition, + offset: seekEntry.offset, + })); + } +} diff --git a/plugins/kafka-backend/src/service/router.ts b/plugins/kafka-backend/src/service/router.ts new file mode 100644 index 0000000000..a1a60a4fdb --- /dev/null +++ b/plugins/kafka-backend/src/service/router.ts @@ -0,0 +1,75 @@ +/* + * 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 Router from 'express-promise-router'; +import { Logger } from 'winston'; +import { Config } from '@backstage/config'; +import { KafkaApi } from './KafkaApi'; + +export interface RouterOptions { + logger: Logger; + config: Config; +} + +export const makeRouter = ( + logger: Logger, + kafkaApi: KafkaApi, +): express.Router => { + const router = Router(); + router.use(express.json()); + + router.get('/topic/:topicId/offsets', async (req, res) => { + const topicId = req.params.topicId; + try { + const response = await kafkaApi.fetchTopicOffsets(topicId); + res.send(response); + } catch (e) { + logger.error(`action=fetchTopicOffsets topicId=${topicId}, error=${e}`); + res.status(500).send({ error: e.message }); + } + }); + + router.get('/consumer/:consumerId/offsets', async (req, res) => { + const consumerId = req.params.consumerId; + try { + const response = await kafkaApi.fetchGroupOffsets(consumerId); + res.send(response); + } catch (e) { + logger.error( + `action=fetchGroupOffsets consumerId=${consumerId}, error=${e}`, + ); + res.status(500).send({ error: e.message }); + } + }); + + return router; +}; + +export async function createRouter( + options: RouterOptions, +): Promise { + const logger = options.logger; + + logger.info('Initializing Kafka backend'); + + const clientId = options.config.getString('kafka.clientId'); + const brokers = options.config.getStringArray('kafka.brokers'); + + const kafkaApi = new KafkaApi(clientId, brokers, logger); + + return makeRouter(logger, kafkaApi); +} diff --git a/plugins/kafka-backend/src/service/standaloneApplication.ts b/plugins/kafka-backend/src/service/standaloneApplication.ts new file mode 100644 index 0000000000..50563023d0 --- /dev/null +++ b/plugins/kafka-backend/src/service/standaloneApplication.ts @@ -0,0 +1,54 @@ +/* + * 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 { + errorHandler, + notFoundHandler, + requestLoggingHandler, +} from '@backstage/backend-common'; +import compression from 'compression'; +import cors from 'cors'; +import express from 'express'; +import helmet from 'helmet'; +import { Logger } from 'winston'; +import { createRouter } from './router'; +import { ConfigReader } from '@backstage/config'; + +export interface ApplicationOptions { + enableCors: boolean; + logger: Logger; +} + +export async function createStandaloneApplication( + options: ApplicationOptions, +): Promise { + const { enableCors, logger } = options; + const config = new ConfigReader({}); + const app = express(); + + app.use(helmet()); + if (enableCors) { + app.use(cors()); + } + app.use(compression()); + app.use(express.json()); + app.use(requestLoggingHandler()); + app.use('/', await createRouter({ logger, config })); + app.use(notFoundHandler()); + app.use(errorHandler()); + + return app; +} diff --git a/plugins/kafka-backend/src/service/standaloneServer.ts b/plugins/kafka-backend/src/service/standaloneServer.ts new file mode 100644 index 0000000000..6459995928 --- /dev/null +++ b/plugins/kafka-backend/src/service/standaloneServer.ts @@ -0,0 +1,50 @@ +/* + * 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 { Server } from 'http'; +import { Logger } from 'winston'; +import { createStandaloneApplication } from './standaloneApplication'; + +export interface ServerOptions { + port: number; + enableCors: boolean; + logger: Logger; +} + +export async function startStandaloneServer( + options: ServerOptions, +): Promise { + const logger = options.logger.child({ service: 'kafka-backend' }); + + logger.debug('Creating application...'); + const app = await createStandaloneApplication({ + enableCors: options.enableCors, + logger, + }); + + logger.debug('Starting application server...'); + return await new Promise((resolve, reject) => { + const server = app.listen(options.port, (err?: Error) => { + if (err) { + reject(err); + return; + } + + logger.info(`Listening on port ${options.port}`); + resolve(server); + }); + }); +} diff --git a/plugins/kafka-backend/src/setupTests.ts b/plugins/kafka-backend/src/setupTests.ts new file mode 100644 index 0000000000..ba33cf996b --- /dev/null +++ b/plugins/kafka-backend/src/setupTests.ts @@ -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 {}; diff --git a/plugins/kafka-backend/src/types/types.ts b/plugins/kafka-backend/src/types/types.ts new file mode 100644 index 0000000000..0fb6dde77f --- /dev/null +++ b/plugins/kafka-backend/src/types/types.ts @@ -0,0 +1,20 @@ +/* + * 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 { TopicOffset, PartitionOffset } from '../service/KafkaApi'; + +export type TopicOffsetsResponse = Array; +export type ConsumerGroupOffsetsResponse = Array; diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index 098d9a426c..ed256f51a3 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -25,7 +25,6 @@ "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.11.2", "@material-ui/lab": "4.0.0-alpha.45", - "kafkajs": "^1.15.0", "lodash": "^4.17.20", "react": "^16.13.1", "react-dom": "^16.13.1", @@ -35,6 +34,7 @@ "@backstage/cli": "^0.4.2", "@backstage/dev-utils": "^0.1.6", "@backstage/test-utils": "^0.1.5", + "@backstage/plugin-kafka-backend": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/kafka/src/api/KafkaApi.ts b/plugins/kafka/src/api/KafkaApi.ts deleted file mode 100644 index 63ac948047..0000000000 --- a/plugins/kafka/src/api/KafkaApi.ts +++ /dev/null @@ -1,112 +0,0 @@ -/* - * 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 { ConfigApi, DiscoveryApi, createApiRef } from '@backstage/core'; -import { Kafka, SeekEntry } from 'kafkajs'; - -export const kafkaApiRef = createApiRef({ - id: 'plugin.kafka.service', - description: 'Used by the Kafka plugin to connect to the Kafka cluster', -}); - -export type TopicOffsets = { - topic: string; - partitions: { - id: number; - offset: string; - }[]; -}; - -export class KafkaApi { - static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi) { - const clientId = - configApi.getOptionalString('kafka.clientId') ?? 'kafka-backstage'; - const brokers = configApi.getOptionalStringArray('kafka.brokers') ?? [ - '/kafka/broker', - ]; - - return new KafkaApi(discoveryApi, clientId, brokers); - } - - constructor( - private readonly discoveryApi: DiscoveryApi, - private readonly clientId: string, - private readonly brokers: string[], - ) {} - - async fetchTopicsOffsets(topics: string[]): Promise> { - const admin = await this.getAdmin(); - - await admin.connect(); - try { - return await Promise.all( - topics.map(async topic => - admin - .fetchTopicOffsets(topic) - .then(result => KafkaApi.toTopicOffsets(topic, result)), - ), - ); - } finally { - await admin.disconnect(); - } - } - - async fetchGroupOffsets(groupId: string): Promise> { - const admin = await this.getAdmin(); - - await admin.connect(); - try { - let groupOffsets = [ - { - topic: 'shula', - partitions: await admin.fetchOffsets({ groupId, topic: 'shula' }), - }, - { - topic: 'test', - partitions: await admin.fetchOffsets({ groupId, topic: 'test' }), - }, - ]; - - return groupOffsets.map(topicOffset => - KafkaApi.toTopicOffsets(topicOffset.topic, topicOffset.partitions), - ); - } finally { - await admin.disconnect(); - } - } - - private async getAdmin() { - const kafka = await this.getKafka(); - return kafka.admin(); - } - - private async getKafka() { - const proxyUrl = await this.discoveryApi.getBaseUrl('proxy'); - - return new Kafka({ - clientId: this.clientId, - brokers: this.brokers.map(brokerUrl => proxyUrl + brokerUrl), - }); - } - - private static toTopicOffsets(topic: string, result: Array) { - const partitions = result.map(seekEntry => ({ - id: seekEntry.partition, - offset: seekEntry.offset, - })); - return { topic, partitions }; - } -} diff --git a/plugins/kafka/src/api/KafkaBackendClient.ts b/plugins/kafka/src/api/KafkaBackendClient.ts new file mode 100644 index 0000000000..4a895c5556 --- /dev/null +++ b/plugins/kafka/src/api/KafkaBackendClient.ts @@ -0,0 +1,58 @@ +/* + * 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 { DiscoveryApi } from '@backstage/core'; +import { KafkaApi } from './types'; +import { + ConsumerGroupOffsetsResponse, + TopicOffsetsResponse, +} from '@backstage/plugin-kafka-backend'; + +export class KafkaBackendClient implements KafkaApi { + private readonly discoveryApi: DiscoveryApi; + + constructor(options: { discoveryApi: DiscoveryApi }) { + this.discoveryApi = options.discoveryApi; + } + + private async getRequired(path: string): Promise { + const url = `${await this.discoveryApi.getBaseUrl('kafka')}${path}`; + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + const payload = await response.text(); + const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`; + throw new Error(message); + } + + return await response.json(); + } + + async getTopicOffsets(topic: string): Promise { + return await this.getRequired(`/topic/${topic}/offsets`); + } + + async getConsumerGroupOffsets( + consumerGroup: string, + ): Promise { + return await this.getRequired(`/consumer/${consumerGroup}/offsets`); + } +} diff --git a/plugins/kafka/src/api/types.ts b/plugins/kafka/src/api/types.ts new file mode 100644 index 0000000000..f745c75fcc --- /dev/null +++ b/plugins/kafka/src/api/types.ts @@ -0,0 +1,32 @@ +/* + * 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 { createApiRef } from '@backstage/core'; +import { + ConsumerGroupOffsetsResponse, + TopicOffsetsResponse, +} from '@backstage/plugin-kafka-backend'; + +export const kafkaApiRef = createApiRef({ + id: 'plugin.kafka.service', + description: + 'Used by the Kafka plugin to make requests to accompanying backend', +}); + +export interface KafkaApi { + getTopicOffsets(topic: string): Promise; + getConsumerGroupOffsets(consumerGroup: string): Promise; +} diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupOffsets.ts b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupOffsets.ts index 95c6e0892d..9ec0d33b56 100644 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupOffsets.ts +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupOffsets.ts @@ -16,7 +16,7 @@ import { errorApiRef, useApi } from '@backstage/core'; import { useAsyncRetry } from 'react-use'; -import { kafkaApiRef } from '../../api/KafkaApi'; +import { kafkaApiRef } from '../../api/types'; import _ from 'lodash'; export function useConsumerGroupOffsets(groupId: string) { @@ -25,32 +25,32 @@ export function useConsumerGroupOffsets(groupId: string) { const { loading, value: topics, retry } = useAsyncRetry(async () => { try { - const groupOnlyOffsets = await api.fetchGroupOffsets(groupId); - const topicOffsets = _.keyBy( - await api.fetchTopicsOffsets( - groupOnlyOffsets.map(value => value.topic), - ), - offsets => offsets.topic, - ); + const groupOffsets = await api.getConsumerGroupOffsets(groupId); + const groupWithTopicOffsets = await Promise.all( + groupOffsets.map(async ({ topic, partitions }) => { + debugger; + const topicOffsets = _.keyBy( + await api.getTopicOffsets(topic), + partition => partition.id, + ); - return groupOnlyOffsets.flatMap(value => { - let topicPartitionOffsets = _.keyBy( - topicOffsets[value.topic].partitions, - partition => partition.id, - ); - return value.partitions.map(partition => ({ - topic: value.topic, - partitionId: partition.id, - groupOffset: partition.offset, - topicOffset: topicPartitionOffsets[partition.id].offset, - })); - }); + return partitions.map(partition => ({ + topic: topic, + partitionId: partition.id, + groupOffset: partition.offset, + topicOffset: topicOffsets[partition.id].offset, + })); + }), + ); + return groupWithTopicOffsets.flat(); } catch (e) { errorApi.post(e); throw e; } }, [api, errorApi, groupId]); + debugger; + return [ { loading, diff --git a/plugins/kafka/src/index.ts b/plugins/kafka/src/index.ts index 80670acb81..0cc1957faf 100644 --- a/plugins/kafka/src/index.ts +++ b/plugins/kafka/src/index.ts @@ -16,4 +16,3 @@ export { plugin } from './plugin'; export { KAFKA_CONSUMER_GROUP_ANNOTATION } from './constants'; export { Router, isPluginApplicableToEntity } from './Router'; -export * from './api'; diff --git a/plugins/kafka/src/plugin.ts b/plugins/kafka/src/plugin.ts index 40c288e671..3f7e41e105 100644 --- a/plugins/kafka/src/plugin.ts +++ b/plugins/kafka/src/plugin.ts @@ -20,7 +20,8 @@ import { createRouteRef, discoveryApiRef, } from '@backstage/core'; -import { KafkaApi, kafkaApiRef } from './api/KafkaApi'; +import { KafkaBackendClient } from './api/KafkaBackendClient'; +import { kafkaApiRef } from './api/types'; export const rootCatalogKafkaRouteRef = createRouteRef({ path: '*', @@ -33,8 +34,7 @@ export const plugin = createPlugin({ createApiFactory({ api: kafkaApiRef, deps: { discoveryApi: discoveryApiRef, configApi: configApiRef }, - factory: ({ configApi, discoveryApi }) => - KafkaApi.fromConfig(configApi, discoveryApi), + factory: ({ discoveryApi }) => new KafkaBackendClient({ discoveryApi }), }), ], }); diff --git a/yarn.lock b/yarn.lock index 0ad51ca447..60980ff41d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1644,11 +1644,9 @@ to-fast-properties "^2.0.0" "@backstage/catalog-model@^0.2.0": - version "0.2.0" - resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.2.0.tgz#e3fe2a4ddeb6a9b6ec480c80cb2b9c39cb245576" - integrity sha512-Y1ocdRpBlxK/VrJQjHlQd0bgADECd1B2NRjwd8ss46ibT5hwLvMOfD80+Fa7oPLu0ktJrH4lq0pNIIJIml48zA== + version "0.6.0" dependencies: - "@backstage/config" "^0.1.1" + "@backstage/config" "^0.1.2" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" json-schema "^0.2.5" @@ -1657,11 +1655,9 @@ yup "^0.29.3" "@backstage/catalog-model@^0.3.0": - version "0.3.1" - resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.3.1.tgz#45d08e2f333c9c566b2bf2629fd707fe989bb404" - integrity sha512-9XhV7c4rmVW+Yzj2PiwTQ7DsegWGB3C4ELsDRExuEVZONdqNcC02cyJtrt3fT5F31ZS3tHkB9bMUymFOBLqUSA== + version "0.6.0" dependencies: - "@backstage/config" "^0.1.1" + "@backstage/config" "^0.1.2" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" json-schema "^0.2.5" @@ -1670,17 +1666,16 @@ yup "^0.29.3" "@backstage/core@^0.3.0": - version "0.3.2" - resolved "https://registry.npmjs.org/@backstage/core/-/core-0.3.2.tgz#a8209126d5076cf4a8b9bd632fe4e5e2edb62916" - integrity sha512-i5d+Wh8js4qEWoAsPY5L7HVSWpumr1OhfF2dUCGYdyW6AMqVJPca6+n6zp1Rg2CO+J9norp44XAVVCbyhtUpig== + version "0.4.1" dependencies: - "@backstage/config" "^0.1.1" - "@backstage/core-api" "^0.2.1" - "@backstage/theme" "^0.2.1" + "@backstage/config" "^0.1.2" + "@backstage/core-api" "^0.2.6" + "@backstage/theme" "^0.2.2" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" "@material-ui/lab" "4.0.0-alpha.45" "@types/dagre" "^0.7.44" + "@types/prop-types" "^15.7.3" "@types/react" "^16.9" "@types/react-sparklines" "^1.7.0" classnames "^2.2.6" @@ -1958,6 +1953,15 @@ debug "^3.1.0" lodash.once "^4.1.1" +"@dabh/diagnostics@^2.0.2": + version "2.0.2" + resolved "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz#290d08f7b381b8f94607dc8f471a12c675f9db31" + integrity sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q== + dependencies: + colorspace "1.1.x" + enabled "2.0.x" + kuler "^2.0.0" + "@date-io/core@1.x", "@date-io/core@^1.3.13": version "1.3.13" resolved "https://registry.npmjs.org/@date-io/core/-/core-1.3.13.tgz#90c71da493f20204b7a972929cc5c482d078b3fa" @@ -7679,7 +7683,7 @@ async@^2.6.1, async@^2.6.2: dependencies: lodash "^4.17.14" -async@^3.2.0: +async@^3.1.0, async@^3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== @@ -11432,6 +11436,11 @@ enabled@1.0.x: dependencies: env-variable "0.0.x" +enabled@2.0.x: + version "2.0.0" + resolved "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" + integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== + encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -12468,6 +12477,11 @@ fecha@^2.3.3: resolved "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz#948e74157df1a32fd1b12c3a3c3cdcb6ec9d96cd" integrity sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg== +fecha@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/fecha/-/fecha-4.2.0.tgz#3ffb6395453e3f3efff850404f0a59b6747f5f41" + integrity sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg== + fetch-readablestream@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/fetch-readablestream/-/fetch-readablestream-0.2.0.tgz#eaa6d1a76b12de2d4731a343393c6ccdcfe2c795" @@ -12705,6 +12719,11 @@ fn-name@~3.0.0: resolved "https://registry.npmjs.org/fn-name/-/fn-name-3.0.0.tgz#0596707f635929634d791f452309ab41558e3c5c" integrity sha512-eNMNr5exLoavuAMhIUVsOKF79SWd/zG104ef6sxBTSw+cZc6BXdQXDvYcGvp0VbxVVSp1XDUNoz7mg1xMtSznA== +fn.name@1.x.x: + version "1.1.0" + resolved "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" + integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== + follow-redirects@1.5.10: version "1.5.10" resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" @@ -16198,10 +16217,10 @@ jwt-decode@*, jwt-decode@^3.1.0: resolved "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59" integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A== -kafkajs@^1.15.0: - version "1.15.0" - resolved "https://registry.npmjs.org/kafkajs/-/kafkajs-1.15.0.tgz#a5ada0d933edca2149177393562be6fb0875ec3a" - integrity sha512-yjPyEnQCkPxAuQLIJnY5dI+xnmmgXmhuOQ1GVxClG5KTOV/rJcW1qA3UfvyEJKTp/RTSqQnUR3HJsKFvHyTpNg== +kafkajs@^1.16.0-beta.6: + version "1.16.0-beta.6" + resolved "https://registry.npmjs.org/kafkajs/-/kafkajs-1.16.0-beta.6.tgz#650f4d16abd60516aa0c375be613368391216df1" + integrity sha512-R4DT3s7oCAoxdq3tN8w2WkUxLXcZgLQSjQVSmEZpEmT6hVdZ5AUZWqyLYr1IRbQowsJK4Z7eWJAk68J0JiRlUw== keyv@^3.0.0: version "3.1.0" @@ -16291,6 +16310,11 @@ kuler@1.0.x: dependencies: colornames "^1.1.1" +kuler@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" + integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== + latest-version@5.1.0, latest-version@^5.0.0: version "5.1.0" resolved "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" @@ -16856,6 +16880,17 @@ logform@^2.1.1: ms "^2.1.1" triple-beam "^1.3.0" +logform@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/logform/-/logform-2.2.0.tgz#40f036d19161fc76b68ab50fdc7fe495544492f2" + integrity sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg== + dependencies: + colors "^1.2.1" + fast-safe-stringify "^2.0.4" + fecha "^4.2.0" + ms "^2.1.1" + triple-beam "^1.3.0" + loglevel@^1.6.7, loglevel@^1.6.8: version "1.6.8" resolved "https://registry.npmjs.org/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171" @@ -18540,6 +18575,13 @@ one-time@0.0.4: resolved "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz#f8cdf77884826fe4dff93e3a9cc37b1e4480742e" integrity sha1-+M33eISCb+Tf+T46nMN7HkSAdC4= +one-time@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45" + integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g== + dependencies: + fn.name "1.x.x" + onetime@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" @@ -21188,7 +21230,7 @@ read@1, read@~1.0.1: dependencies: mute-stream "~0.0.4" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@^2.3.7, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -25211,6 +25253,14 @@ winston-transport@^4.3.0: readable-stream "^2.3.6" triple-beam "^1.2.0" +winston-transport@^4.4.0: + version "4.4.0" + resolved "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz#17af518daa690d5b2ecccaa7acf7b20ca7925e59" + integrity sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw== + dependencies: + readable-stream "^2.3.7" + triple-beam "^1.2.0" + winston@^3.2.1: version "3.2.1" resolved "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz#63061377976c73584028be2490a1846055f77f07" @@ -25226,6 +25276,21 @@ winston@^3.2.1: triple-beam "^1.3.0" winston-transport "^4.3.0" +winston@^3.3.3: + version "3.3.3" + resolved "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz#ae6172042cafb29786afa3d09c8ff833ab7c9170" + integrity sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw== + dependencies: + "@dabh/diagnostics" "^2.0.2" + async "^3.1.0" + is-stream "^2.0.0" + logform "^2.2.0" + one-time "^1.0.0" + readable-stream "^3.4.0" + stack-trace "0.0.x" + triple-beam "^1.3.0" + winston-transport "^4.4.0" + word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" From 16cd6b8e25f48c584ee1666f2fbcaa0e1c97fa4b Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Sat, 9 Jan 2021 21:56:54 +0200 Subject: [PATCH 010/209] Added documentation, addd plugin to sample app --- .changeset/warm-gifts-study.md | 6 ++ packages/app/package.json | 2 +- .../app/src/components/catalog/EntityPage.tsx | 6 ++ packages/backend/package.json | 1 + packages/backend/src/index.ts | 3 + packages/backend/src/plugins/kafka.ts | 25 +++++ plugins/kafka-backend/README.md | 24 +++++ plugins/kafka-backend/package.json | 2 +- plugins/kafka/README.md | 90 ++++++++++++++++-- plugins/kafka/package.json | 4 +- plugins/kafka/src/assets/screenshot-1.png | Bin 0 -> 465801 bytes plugins/kafka/src/constants.ts | 2 +- 12 files changed, 152 insertions(+), 13 deletions(-) create mode 100644 .changeset/warm-gifts-study.md create mode 100644 packages/backend/src/plugins/kafka.ts create mode 100644 plugins/kafka-backend/README.md create mode 100644 plugins/kafka/src/assets/screenshot-1.png diff --git a/.changeset/warm-gifts-study.md b/.changeset/warm-gifts-study.md new file mode 100644 index 0000000000..fcfee164f7 --- /dev/null +++ b/.changeset/warm-gifts-study.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kafka': minor +'@backstage/plugin-kafka-backend': minor +--- + +Created alpha version of Kafka plugin, supporting listing consumer group for a Backstage configured service. diff --git a/packages/app/package.json b/packages/app/package.json index 1488ee5505..66471b981c 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -19,7 +19,7 @@ "@backstage/plugin-gitops-profiles": "^0.2.2", "@backstage/plugin-graphiql": "^0.2.2", "@backstage/plugin-jenkins": "^0.3.4", - "@backstage/plugin-kafka": "^0.1.1", + "@backstage/plugin-kafka": "^0.1.0", "@backstage/plugin-kubernetes": "^0.3.3", "@backstage/plugin-lighthouse": "^0.2.6", "@backstage/plugin-newrelic": "^0.2.2", diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index a4d097b1d4..a1ab181211 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -63,6 +63,7 @@ import { UserProfileCard, } from '@backstage/plugin-org'; import { Router as SentryRouter } from '@backstage/plugin-sentry'; +import { Router as KafkaRouter } from '@backstage/plugin-kafka'; import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs'; import { Button, Grid } from '@material-ui/core'; import { @@ -243,6 +244,11 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => ( title="Code Insights" element={} /> + } + /> ); diff --git a/packages/backend/package.json b/packages/backend/package.json index 718b008c7b..707c14e33c 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -35,6 +35,7 @@ "@backstage/plugin-catalog-backend": "^0.5.0", "@backstage/plugin-graphql-backend": "^0.1.4", "@backstage/plugin-kubernetes-backend": "^0.2.3", + "@backstage/plugin-kafka-backend": "^0.1.0", "@backstage/plugin-proxy-backend": "^0.2.3", "@backstage/plugin-rollbar-backend": "^0.1.5", "@backstage/plugin-scaffolder-backend": "^0.3.5", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 68cc170901..81d0bb96d2 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -38,6 +38,7 @@ import healthcheck from './plugins/healthcheck'; import auth from './plugins/auth'; import catalog from './plugins/catalog'; import kubernetes from './plugins/kubernetes'; +import kafka from './plugins/kafka'; import rollbar from './plugins/rollbar'; import scaffolder from './plugins/scaffolder'; import proxy from './plugins/proxy'; @@ -77,6 +78,7 @@ async function main() { const rollbarEnv = useHotMemoize(module, () => createEnv('rollbar')); const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs')); const kubernetesEnv = useHotMemoize(module, () => createEnv('kubernetes')); + const kafkaEnv = useHotMemoize(module, () => createEnv('kafka')); const graphqlEnv = useHotMemoize(module, () => createEnv('graphql')); const appEnv = useHotMemoize(module, () => createEnv('app')); @@ -87,6 +89,7 @@ async function main() { apiRouter.use('/auth', await auth(authEnv)); apiRouter.use('/techdocs', await techdocs(techdocsEnv)); apiRouter.use('/kubernetes', await kubernetes(kubernetesEnv)); + apiRouter.use('/kafka', await kafka(kafkaEnv)); apiRouter.use('/proxy', await proxy(proxyEnv)); apiRouter.use('/graphql', await graphql(graphqlEnv)); apiRouter.use(notFoundHandler()); diff --git a/packages/backend/src/plugins/kafka.ts b/packages/backend/src/plugins/kafka.ts new file mode 100644 index 0000000000..e65ce6719c --- /dev/null +++ b/packages/backend/src/plugins/kafka.ts @@ -0,0 +1,25 @@ +/* + * 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 { createRouter } from '@backstage/plugin-kafka-backend'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin({ + logger, + config, +}: PluginEnvironment) { + return await createRouter({ logger, config }); +} diff --git a/plugins/kafka-backend/README.md b/plugins/kafka-backend/README.md new file mode 100644 index 0000000000..0f2dece2e5 --- /dev/null +++ b/plugins/kafka-backend/README.md @@ -0,0 +1,24 @@ +# Kafka Backend + +This is the backend part of the Kafka plugin. It responds to Kafka requests from the frontend. + +## Configuration + +This configures how to connect to the brokers in your Kafka cluster. + +### clientId + +The name of the client to use when connecting to the cluster. + +### brokers + +A list of the brokers' hostnames and ports to connect to. + +Example: + +```yaml +kafka: + clientId: backstage + brokers: + - localhost:9092 +``` diff --git a/plugins/kafka-backend/package.json b/plugins/kafka-backend/package.json index a77d7981c9..93ec810ca2 100644 --- a/plugins/kafka-backend/package.json +++ b/plugins/kafka-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kafka-backend", - "version": "0.1.1", + "version": "0.1.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/kafka/README.md b/plugins/kafka/README.md index 35a62758f3..52dbd16520 100644 --- a/plugins/kafka/README.md +++ b/plugins/kafka/README.md @@ -1,13 +1,87 @@ -# kafka +# Kafka Plugin -Welcome to the kafka plugin! + -_This plugin was created through the Backstage CLI_ +## Setup -## Getting started +1. Run: -Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/kafka](http://localhost:3000/kafka). +```bash +yarn add @backstage/plugin-kafka @backstage/plugin-kafka-backend +``` -You can also serve the plugin in isolation by running `yarn start` in the plugin directory. -This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. -It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. +2. Add the plugin backend: + +In a new file named `kafka.js` under `backend/src/plugins`: + +```js +import { createRouter } from '@backstage/plugin-kafka-backend'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin({ + logger, + config, +}: PluginEnvironment) { + return await createRouter({ logger, config }); +} +``` + +And then add to `packages/backend/src/index.ts`: + +```js +async function main() { + // ... + const kafkaEnv = useHotMemoize(module, () => createEnv('kafka')); + // ... + + const apiRouter = Router(); + // ... + apiRouter.use('/kafka', await kafka(kafkaEnv)); + // ... +``` + +3. Add the plugin frontend to `packages/app/src/plugin.ts`: + +```js +export { plugin as Kafka } from '@backstage/plugin-kafka'; +``` + +4. Register the plugin frontend router in `packages/app/src/components/catalog/EntityPage.tsx`: + +```jsx +import { Router as KafkaRouter } from '@backstage/plugin-kafka'; + +// Then, somewhere inside + +} +/>; +``` + +5. Add broker configs for the backend in your `app-config.yaml`: + +```yaml +kafka: + clientId: backstage + brokers: + - localhost:9092 +``` + +6. Add `kafka.apache.org/consumer-group` annotation to your services: + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + # ... + annotations: + kafka.apache.org/consumer-group: consumer-group-name +spec: + type: service +``` + +## Features + +- List topics offsets and consumer group offsets for configured services. diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index ed256f51a3..62a79823bf 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kafka", - "version": "0.1.1", + "version": "0.1.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,7 +34,7 @@ "@backstage/cli": "^0.4.2", "@backstage/dev-utils": "^0.1.6", "@backstage/test-utils": "^0.1.5", - "@backstage/plugin-kafka-backend": "^0.1.1", + "@backstage/plugin-kafka-backend": "^0.1.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/kafka/src/assets/screenshot-1.png b/plugins/kafka/src/assets/screenshot-1.png new file mode 100644 index 0000000000000000000000000000000000000000..0afae0b5d37ec51d7f289c5e601a70c1f7b72f04 GIT binary patch literal 465801 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

f3$3lElDa#d4*kA71(Cf9LrxE786 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 Date: Sat, 9 Jan 2021 22:00:26 +0200 Subject: [PATCH 011/209] regenerated yarn.lock --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 1e98fd0b7b..32cd00d25e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21151,7 +21151,7 @@ read@1, read@~1.0.1: dependencies: mute-stream "~0.0.4" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@^2.3.7, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== From 0577b5e6fd69634f1af0f65bfbd449add5ca0bf1 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Sat, 9 Jan 2021 22:05:47 +0200 Subject: [PATCH 012/209] Reverted changes mistakenly added --- .github/CODEOWNERS | 19 +++++++++---------- local.yaml | 14 -------------- 2 files changed, 9 insertions(+), 24 deletions(-) delete mode 100644 local.yaml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ada20f6931..495f1ff78d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,13 +4,12 @@ # The last matching pattern takes precedence. # https://help.github.com/articles/about-codeowners/ -* @backstage/maintainers -/.changeset/cost-insights-* @backstage/silver-lining -/docs/features/techdocs @backstage/techdocs-core -/packages/techdocs-common @backstage/techdocs-core -/plugins/cloudbuild @trivago/ebarrios -/plugins/cost-insights @backstage/silver-lining -/plugins/kafka @fiverr/platform -/plugins/search @backstage/techdocs-core -/plugins/techdocs @backstage/techdocs-core -/plugins/techdocs-backend @backstage/techdocs-core +* @backstage/maintainers +/docs/features/techdocs @backstage/techdocs-core +/plugins/cost-insights @backstage/silver-lining +/plugins/cloudbuild @trivago/ebarrios +/plugins/search @backstage/techdocs-core +/plugins/techdocs @backstage/techdocs-core +/plugins/techdocs-backend @backstage/techdocs-core +/packages/techdocs-common @backstage/techdocs-core +/.changeset/cost-insights-* @backstage/silver-lining diff --git a/local.yaml b/local.yaml deleted file mode 100644 index afc4c21cfe..0000000000 --- a/local.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: backstage.io/v1alpha1 -kind: Component -metadata: - name: kafka-test - description: Kafka Test - annotations: - kafka.apache.org/consumer-group: consumer - tags: - - java - - data -spec: - type: service - lifecycle: experimental - owner: architecture From 83719211eb772ad7551487d838a7c7a1e6e1d9d2 Mon Sep 17 00:00:00 2001 From: blam Date: Sat, 9 Jan 2021 22:44:29 +0100 Subject: [PATCH 013/209] chore(scaffolder/bitbucket): updating bitbucket preparer to use integrations config --- .../scaffolder/stages/prepare/bitbucket.ts | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts index 5bc58e34f0..b992861c57 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts @@ -63,7 +63,6 @@ export class BitbucketPreparer implements PreparerBase { ): Promise { const { protocol, location } = parseLocationAnnotation(template); const workingDirectory = opts?.workingDirectory ?? os.tmpdir(); - const { logger } = opts; if (!['bitbucket', 'url'].includes(protocol)) { throw new InputError( @@ -86,13 +85,14 @@ export class BitbucketPreparer implements PreparerBase { const checkoutLocation = path.resolve(tempDir, templateDirectory); - const git = this.privateToken + const auth = this.getAuth(repo.resource); + + const git = auth ? Git.fromAuth({ - username: this.username, - password: this.privateToken, - logger, + ...auth, + logger: this.logger, }) - : Git.fromAuth({ logger }); + : Git.fromAuth({ logger: this.logger }); await git.clone({ url: repositoryCheckoutUrl, @@ -101,4 +101,32 @@ export class BitbucketPreparer implements PreparerBase { return checkoutLocation; } + + private getAuth( + host: string, + ): { username: string; password: string } | undefined { + if (this.username && this.privateToken) { + return { username: this.username, password: this.privateToken }; + } + + const bitbucketIntegrationConfig = this.integrations.find( + c => c.host === host, + ); + + // TODO(blam): Not sure how appPassword fits in here. Just doing the most simple of + // implementations with the intergations config for now but can maybe fallback to + // appPassword instead maybe at a later stage. + if ( + !bitbucketIntegrationConfig || + !bitbucketIntegrationConfig.username || + !bitbucketIntegrationConfig.token + ) { + return undefined; + } + + return { + username: bitbucketIntegrationConfig.username, + password: bitbucketIntegrationConfig.token, + }; + } } From 4330a51457bf4f12cc5e0a40756ac076eec008d6 Mon Sep 17 00:00:00 2001 From: blam Date: Sun, 10 Jan 2021 00:59:44 +0100 Subject: [PATCH 014/209] feat: porting bitbucket preparer to use integrations cofnig --- .../stages/prepare/bitbucket.test.ts | 104 +++++++++++++++--- .../scaffolder/stages/prepare/bitbucket.ts | 19 ++-- 2 files changed, 102 insertions(+), 21 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts index 809d18c808..0be61a9f82 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts @@ -30,6 +30,7 @@ import { ConfigReader } from '@backstage/config'; describe('BitbucketPreparer', () => { let mockEntity: TemplateEntityV1alpha1; + const logger = getVoidLogger(); const mockGitClient = { clone: jest.fn(), }; @@ -79,8 +80,8 @@ describe('BitbucketPreparer', () => { }); it('calls the clone command with the correct arguments for a repository', async () => { - const preparer = new BitbucketPreparer(new ConfigReader({})); - await preparer.prepare(mockEntity, { logger: getVoidLogger() }); + const preparer = new BitbucketPreparer(new ConfigReader({}), { logger }); + await preparer.prepare(mockEntity); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://bitbucket.org/backstage-project/backstage-repo', dir: expect.any(String), @@ -100,18 +101,22 @@ describe('BitbucketPreparer', () => { ], }, }), + { logger }, ); - await preparer.prepare(mockEntity, { logger: getVoidLogger() }); - expect(mockGitClient.clone).toHaveBeenCalledWith({ - url: 'https://bitbucket.org/backstage-project/backstage-repo', - dir: expect.any(String), + + await preparer.prepare(mockEntity); + + expect(Git.fromAuth).toHaveBeenCalledWith({ + logger, + username: 'fake-user', + password: 'fake-password', }); }); it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { - const preparer = new BitbucketPreparer(new ConfigReader({})); + const preparer = new BitbucketPreparer(new ConfigReader({}), { logger }); delete mockEntity.spec.path; - await preparer.prepare(mockEntity, { logger: getVoidLogger() }); + await preparer.prepare(mockEntity); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://bitbucket.org/backstage-project/backstage-repo', dir: expect.any(String), @@ -119,22 +124,93 @@ describe('BitbucketPreparer', () => { }); it('return the temp directory with the path to the folder if it is specified', async () => { - const preparer = new BitbucketPreparer(new ConfigReader({})); + const preparer = new BitbucketPreparer(new ConfigReader({}), { logger }); mockEntity.spec.path = './template/test/1/2/3'; - const response = await preparer.prepare(mockEntity, { - logger: getVoidLogger(), - }); + const response = await preparer.prepare(mockEntity); expect(response.split('\\').join('/')).toMatch( /\/template\/test\/1\/2\/3$/, ); }); + it('calls the clone command with deprecated auth method', async () => { + const preparer = new BitbucketPreparer( + new ConfigReader({ + scaffolder: { + bitbucket: { + api: { + username: 'fakeusername', + token: 'faketoken', + }, + }, + }, + }), + { logger }, + ); + + await preparer.prepare(mockEntity); + + expect(Git.fromAuth).toHaveBeenCalledWith({ + logger, + username: 'fakeusername', + password: 'faketoken', + }); + }); + + it('calls the clone command with integrations config for auth method', async () => { + const preparer = new BitbucketPreparer( + new ConfigReader({ + integrations: { + bitbucket: [ + { + host: 'bitbucket.org', + username: 'asd3', + token: 'faketoken', + }, + ], + }, + }), + { logger }, + ); + + await preparer.prepare(mockEntity); + + expect(Git.fromAuth).toHaveBeenCalledWith({ + logger, + username: 'asd3', + password: 'faketoken', + }); + }); + + it('calls the clone command with integrations config with appPassword for auth method', async () => { + const preparer = new BitbucketPreparer( + new ConfigReader({ + integrations: { + bitbucket: [ + { + host: 'bitbucket.org', + username: 'asd3', + appPassword: 'myapppassword', + }, + ], + }, + }), + { logger }, + ); + + await preparer.prepare(mockEntity); + + expect(Git.fromAuth).toHaveBeenCalledWith({ + logger, + username: 'asd3', + password: 'myapppassword', + }); + }); + it('return the working directory with the path to the folder if it is specified', async () => { - const preparer = new BitbucketPreparer(new ConfigReader({})); + const preparer = new BitbucketPreparer(new ConfigReader({}), { logger }); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { - logger: getVoidLogger(), workingDirectory: '/workDir', }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts index b992861c57..cc647982d8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts @@ -59,7 +59,7 @@ export class BitbucketPreparer implements PreparerBase { async prepare( template: TemplateEntityV1alpha1, - opts: PreparerOptions, + opts?: PreparerOptions, ): Promise { const { protocol, location } = parseLocationAnnotation(template); const workingDirectory = opts?.workingDirectory ?? os.tmpdir(); @@ -113,20 +113,25 @@ export class BitbucketPreparer implements PreparerBase { c => c.host === host, ); - // TODO(blam): Not sure how appPassword fits in here. Just doing the most simple of - // implementations with the intergations config for now but can maybe fallback to - // appPassword instead maybe at a later stage. + if (!bitbucketIntegrationConfig) { + return undefined; + } + if ( - !bitbucketIntegrationConfig || !bitbucketIntegrationConfig.username || - !bitbucketIntegrationConfig.token + !( + bitbucketIntegrationConfig.token || + bitbucketIntegrationConfig.appPassword + ) ) { return undefined; } return { username: bitbucketIntegrationConfig.username, - password: bitbucketIntegrationConfig.token, + password: + bitbucketIntegrationConfig.token! || + bitbucketIntegrationConfig.appPassword!, }; } } From f5ff55b73a2ec35b3a5570bdab6375f70f790a5b Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Sun, 10 Jan 2021 10:47:09 +0200 Subject: [PATCH 015/209] Fixed typo in Kafka Backend readme --- plugins/kafka-backend/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/kafka-backend/README.md b/plugins/kafka-backend/README.md index 0f2dece2e5..862a4e95b8 100644 --- a/plugins/kafka-backend/README.md +++ b/plugins/kafka-backend/README.md @@ -12,7 +12,7 @@ The name of the client to use when connecting to the cluster. ### brokers -A list of the brokers' hostnames and ports to connect to. +A list of the brokers' host names and ports to connect to. Example: From dfcbe6aa31c91732e93a694042d5148ea7c01dbe Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Sun, 10 Jan 2021 10:51:14 +0200 Subject: [PATCH 016/209] downgraded winston version to 3.2.1 to match other packages --- plugins/kafka-backend/package.json | 2 +- yarn.lock | 74 +----------------------------- 2 files changed, 3 insertions(+), 73 deletions(-) diff --git a/plugins/kafka-backend/package.json b/plugins/kafka-backend/package.json index 93ec810ca2..fc2ff4ac78 100644 --- a/plugins/kafka-backend/package.json +++ b/plugins/kafka-backend/package.json @@ -35,7 +35,7 @@ "@backstage/catalog-model": "^0.6.0", "@backstage/config": "^0.1.2", "kafkajs": "^1.16.0-beta.6", - "winston": "^3.3.3" + "winston": "^3.2.1" }, "devDependencies": { "@backstage/cli": "^0.4.2", diff --git a/yarn.lock b/yarn.lock index 32cd00d25e..e16e4c6857 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1953,15 +1953,6 @@ debug "^3.1.0" lodash.once "^4.1.1" -"@dabh/diagnostics@^2.0.2": - version "2.0.2" - resolved "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz#290d08f7b381b8f94607dc8f471a12c675f9db31" - integrity sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q== - dependencies: - colorspace "1.1.x" - enabled "2.0.x" - kuler "^2.0.0" - "@date-io/core@1.x", "@date-io/core@^1.3.13": version "1.3.13" resolved "https://registry.npmjs.org/@date-io/core/-/core-1.3.13.tgz#90c71da493f20204b7a972929cc5c482d078b3fa" @@ -7646,7 +7637,7 @@ async@^2.6.1, async@^2.6.2: dependencies: lodash "^4.17.14" -async@^3.1.0, async@^3.2.0: +async@^3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== @@ -11383,11 +11374,6 @@ enabled@1.0.x: dependencies: env-variable "0.0.x" -enabled@2.0.x: - version "2.0.0" - resolved "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" - integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== - encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -12434,11 +12420,6 @@ fecha@^2.3.3: resolved "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz#948e74157df1a32fd1b12c3a3c3cdcb6ec9d96cd" integrity sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg== -fecha@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/fecha/-/fecha-4.2.0.tgz#3ffb6395453e3f3efff850404f0a59b6747f5f41" - integrity sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg== - fetch-readablestream@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/fetch-readablestream/-/fetch-readablestream-0.2.0.tgz#eaa6d1a76b12de2d4731a343393c6ccdcfe2c795" @@ -12676,11 +12657,6 @@ fn-name@~3.0.0: resolved "https://registry.npmjs.org/fn-name/-/fn-name-3.0.0.tgz#0596707f635929634d791f452309ab41558e3c5c" integrity sha512-eNMNr5exLoavuAMhIUVsOKF79SWd/zG104ef6sxBTSw+cZc6BXdQXDvYcGvp0VbxVVSp1XDUNoz7mg1xMtSznA== -fn.name@1.x.x: - version "1.1.0" - resolved "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" - integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== - follow-redirects@1.5.10: version "1.5.10" resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" @@ -16280,11 +16256,6 @@ kuler@1.0.x: dependencies: colornames "^1.1.1" -kuler@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" - integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== - latest-version@5.1.0, latest-version@^5.0.0: version "5.1.0" resolved "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" @@ -16850,17 +16821,6 @@ logform@^2.1.1: ms "^2.1.1" triple-beam "^1.3.0" -logform@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/logform/-/logform-2.2.0.tgz#40f036d19161fc76b68ab50fdc7fe495544492f2" - integrity sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg== - dependencies: - colors "^1.2.1" - fast-safe-stringify "^2.0.4" - fecha "^4.2.0" - ms "^2.1.1" - triple-beam "^1.3.0" - loglevel@^1.6.7, loglevel@^1.6.8: version "1.6.8" resolved "https://registry.npmjs.org/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171" @@ -18504,13 +18464,6 @@ one-time@0.0.4: resolved "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz#f8cdf77884826fe4dff93e3a9cc37b1e4480742e" integrity sha1-+M33eISCb+Tf+T46nMN7HkSAdC4= -one-time@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45" - integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g== - dependencies: - fn.name "1.x.x" - onetime@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" @@ -21151,7 +21104,7 @@ read@1, read@~1.0.1: dependencies: mute-stream "~0.0.4" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@^2.3.7, readable-stream@~2.3.6: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -25145,14 +25098,6 @@ winston-transport@^4.3.0: readable-stream "^2.3.6" triple-beam "^1.2.0" -winston-transport@^4.4.0: - version "4.4.0" - resolved "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz#17af518daa690d5b2ecccaa7acf7b20ca7925e59" - integrity sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw== - dependencies: - readable-stream "^2.3.7" - triple-beam "^1.2.0" - winston@^3.2.1: version "3.2.1" resolved "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz#63061377976c73584028be2490a1846055f77f07" @@ -25168,21 +25113,6 @@ winston@^3.2.1: triple-beam "^1.3.0" winston-transport "^4.3.0" -winston@^3.3.3: - version "3.3.3" - resolved "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz#ae6172042cafb29786afa3d09c8ff833ab7c9170" - integrity sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw== - dependencies: - "@dabh/diagnostics" "^2.0.2" - async "^3.1.0" - is-stream "^2.0.0" - logform "^2.2.0" - one-time "^1.0.0" - readable-stream "^3.4.0" - stack-trace "0.0.x" - triple-beam "^1.3.0" - winston-transport "^4.4.0" - word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" From d75b149004bb16ea372ecdb6c7e495b83fda979b Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Sun, 10 Jan 2021 10:52:49 +0200 Subject: [PATCH 017/209] updated changeset to include changes to example app --- .changeset/warm-gifts-study.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.changeset/warm-gifts-study.md b/.changeset/warm-gifts-study.md index fcfee164f7..4bd13b9f76 100644 --- a/.changeset/warm-gifts-study.md +++ b/.changeset/warm-gifts-study.md @@ -1,6 +1,8 @@ --- '@backstage/plugin-kafka': minor '@backstage/plugin-kafka-backend': minor +'example-app': patch +'example-backend': patch --- Created alpha version of Kafka plugin, supporting listing consumer group for a Backstage configured service. From 428a5035624093bf9c87f6821ee4311d14a7bb40 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Sun, 10 Jan 2021 12:05:39 +0200 Subject: [PATCH 018/209] Run prettier on 2 non pretty files --- plugins/kafka/dev/index.tsx | 4 +--- plugins/kafka/src/api/types.ts | 4 +++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/kafka/dev/index.tsx b/plugins/kafka/dev/index.tsx index 8890c7e3c4..264d6f801f 100644 --- a/plugins/kafka/dev/index.tsx +++ b/plugins/kafka/dev/index.tsx @@ -16,6 +16,4 @@ import { createDevApp } from '@backstage/dev-utils'; import { plugin } from '../src/plugin'; -createDevApp() - .registerPlugin(plugin) - .render(); +createDevApp().registerPlugin(plugin).render(); diff --git a/plugins/kafka/src/api/types.ts b/plugins/kafka/src/api/types.ts index f745c75fcc..ec74dd25a2 100644 --- a/plugins/kafka/src/api/types.ts +++ b/plugins/kafka/src/api/types.ts @@ -28,5 +28,7 @@ export const kafkaApiRef = createApiRef({ export interface KafkaApi { getTopicOffsets(topic: string): Promise; - getConsumerGroupOffsets(consumerGroup: string): Promise; + getConsumerGroupOffsets( + consumerGroup: string, + ): Promise; } From 5a714abfb2e452f37ab498349430d6813c478818 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Sun, 10 Jan 2021 13:09:27 +0200 Subject: [PATCH 019/209] Fixed ESLint errors --- plugins/kafka-backend/package.json | 9 ++++++++- plugins/kafka-backend/src/service/KafkaApi.ts | 2 +- plugins/kafka/package.json | 5 ++++- .../ConsumerGroupOffsets/useConsumerGroupOffsets.ts | 3 --- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/plugins/kafka-backend/package.json b/plugins/kafka-backend/package.json index fc2ff4ac78..b9c5f89d4f 100644 --- a/plugins/kafka-backend/package.json +++ b/plugins/kafka-backend/package.json @@ -31,14 +31,21 @@ "clean": "backstage-cli clean" }, "dependencies": { + "compression": "^1.7.4", + "cors": "^2.8.5", + "express": "^4.17.1", + "express-promise-router": "^3.0.3", + "helmet": "^4.0.0", "@backstage/backend-common": "^0.4.1", "@backstage/catalog-model": "^0.6.0", "@backstage/config": "^0.1.2", "kafkajs": "^1.16.0-beta.6", - "winston": "^3.2.1" + "winston": "^3.2.1", + "yn": "^4.0.0" }, "devDependencies": { "@backstage/cli": "^0.4.2", + "@types/express": "^4.17.6", "supertest": "^4.0.2" }, "files": [ diff --git a/plugins/kafka-backend/src/service/KafkaApi.ts b/plugins/kafka-backend/src/service/KafkaApi.ts index 8b51b41439..17595fcd12 100644 --- a/plugins/kafka-backend/src/service/KafkaApi.ts +++ b/plugins/kafka-backend/src/service/KafkaApi.ts @@ -60,7 +60,7 @@ export class KafkaApi { await admin.connect(); try { - let groupOffsets = await admin.fetchOffsets({ groupId }); + const groupOffsets = await admin.fetchOffsets({ groupId }); return groupOffsets.map(topicOffset => ({ topic: topicOffset.topic, diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index 62a79823bf..bdb0c0880e 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -20,13 +20,17 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@backstage/catalog-model": "^0.6.0", "@backstage/core": "^0.4.1", "@backstage/theme": "^0.2.2", + "@backstage/plugin-catalog": "^0.2.7", + "@backstage/plugin-kafka-backend": "^0.1.0", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.11.2", "@material-ui/lab": "4.0.0-alpha.45", "lodash": "^4.17.20", "react": "^16.13.1", + "react-router": "6.0.0-beta.0", "react-dom": "^16.13.1", "react-use": "^15.3.3" }, @@ -34,7 +38,6 @@ "@backstage/cli": "^0.4.2", "@backstage/dev-utils": "^0.1.6", "@backstage/test-utils": "^0.1.5", - "@backstage/plugin-kafka-backend": "^0.1.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupOffsets.ts b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupOffsets.ts index 9ec0d33b56..575f962b4d 100644 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupOffsets.ts +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupOffsets.ts @@ -28,7 +28,6 @@ export function useConsumerGroupOffsets(groupId: string) { const groupOffsets = await api.getConsumerGroupOffsets(groupId); const groupWithTopicOffsets = await Promise.all( groupOffsets.map(async ({ topic, partitions }) => { - debugger; const topicOffsets = _.keyBy( await api.getTopicOffsets(topic), partition => partition.id, @@ -49,8 +48,6 @@ export function useConsumerGroupOffsets(groupId: string) { } }, [api, errorApi, groupId]); - debugger; - return [ { loading, From 2a2234a2a55b8230754d89590c0ffbc237e1f0f1 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Sun, 10 Jan 2021 13:45:26 +0200 Subject: [PATCH 020/209] Fixed lint:type-deps error --- plugins/kafka-backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/kafka-backend/package.json b/plugins/kafka-backend/package.json index b9c5f89d4f..d66cd5f751 100644 --- a/plugins/kafka-backend/package.json +++ b/plugins/kafka-backend/package.json @@ -39,13 +39,13 @@ "@backstage/backend-common": "^0.4.1", "@backstage/catalog-model": "^0.6.0", "@backstage/config": "^0.1.2", + "@types/express": "^4.17.6", "kafkajs": "^1.16.0-beta.6", "winston": "^3.2.1", "yn": "^4.0.0" }, "devDependencies": { "@backstage/cli": "^0.4.2", - "@types/express": "^4.17.6", "supertest": "^4.0.2" }, "files": [ From af5e32771db56609a09376d11b3243bad7fad316 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Sun, 10 Jan 2021 13:55:04 +0200 Subject: [PATCH 021/209] Removed example app from changeset --- .changeset/warm-gifts-study.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/.changeset/warm-gifts-study.md b/.changeset/warm-gifts-study.md index 4bd13b9f76..fcfee164f7 100644 --- a/.changeset/warm-gifts-study.md +++ b/.changeset/warm-gifts-study.md @@ -1,8 +1,6 @@ --- '@backstage/plugin-kafka': minor '@backstage/plugin-kafka-backend': minor -'example-app': patch -'example-backend': patch --- Created alpha version of Kafka plugin, supporting listing consumer group for a Backstage configured service. From ba80eaa1f344f46095044fad7d3c3646c5cf63f5 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Sun, 10 Jan 2021 15:12:20 +0200 Subject: [PATCH 022/209] updated package version to make yarn run diff succeed --- plugins/kafka-backend/package.json | 2 +- plugins/kafka/package.json | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/kafka-backend/package.json b/plugins/kafka-backend/package.json index d66cd5f751..fb255ab157 100644 --- a/plugins/kafka-backend/package.json +++ b/plugins/kafka-backend/package.json @@ -45,7 +45,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.4.2", + "@backstage/cli": "^0.4.3", "supertest": "^4.0.2" }, "files": [ diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index bdb0c0880e..bc809cac90 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -21,12 +21,12 @@ }, "dependencies": { "@backstage/catalog-model": "^0.6.0", - "@backstage/core": "^0.4.1", + "@backstage/core": "^0.4.3", "@backstage/theme": "^0.2.2", "@backstage/plugin-catalog": "^0.2.7", "@backstage/plugin-kafka-backend": "^0.1.0", "@material-ui/core": "^4.11.0", - "@material-ui/icons": "^4.11.2", + "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "lodash": "^4.17.20", "react": "^16.13.1", @@ -35,9 +35,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.2", - "@backstage/dev-utils": "^0.1.6", - "@backstage/test-utils": "^0.1.5", + "@backstage/cli": "^0.4.5", + "@backstage/dev-utils": "^0.1.7", + "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", From 9d1d1138eb011cb5a60713fe7112da7758023b64 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 10 Jan 2021 18:05:13 +0100 Subject: [PATCH 023/209] create-app: move version marker dependencies to peer deps --- .changeset/tasty-dolls-dance.md | 5 +++++ packages/create-app/package.json | 14 ++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 .changeset/tasty-dolls-dance.md diff --git a/.changeset/tasty-dolls-dance.md b/.changeset/tasty-dolls-dance.md new file mode 100644 index 0000000000..3627e918b4 --- /dev/null +++ b/.changeset/tasty-dolls-dance.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Ensured that versions bumps of packages used in the app template trigger a release of this package when needed. diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 70ce66de11..3e7d5972cf 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -37,6 +37,13 @@ "recursive-readdir": "^2.2.2" }, "devDependencies": { + "@types/fs-extra": "^9.0.1", + "@types/inquirer": "^7.3.1", + "@types/react-dev-utils": "^9.0.4", + "@types/recursive-readdir": "^2.2.0", + "ts-node": "^8.6.2" + }, + "peerDependencies": { "@backstage/backend-common": "^0.4.2", "@backstage/catalog-model": "^0.6.0", "@backstage/cli": "^0.4.5", @@ -62,12 +69,7 @@ "@backstage/plugin-techdocs-backend": "^0.5.2", "@backstage/plugin-user-settings": "^0.2.3", "@backstage/test-utils": "^0.1.6", - "@backstage/theme": "^0.2.2", - "@types/fs-extra": "^9.0.1", - "@types/inquirer": "^7.3.1", - "@types/react-dev-utils": "^9.0.4", - "@types/recursive-readdir": "^2.2.0", - "ts-node": "^8.6.2" + "@backstage/theme": "^0.2.2" }, "nodemonConfig": { "watch": "./src", From d5fa852aa2f8dac3f6464c41ca849978f9198963 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 11 Jan 2021 15:52:14 +0100 Subject: [PATCH 024/209] chore: more work to including the integrations config everywhere --- .../scaffolder/stages/prepare/bitbucket.ts | 2 +- .../src/scaffolder/stages/publish/azure.ts | 7 ++- .../src/scaffolder/stages/publish/gitlab.ts | 57 +++++++++++++++++-- .../components/TemplatePage/TemplatePage.tsx | 4 +- 4 files changed, 59 insertions(+), 11 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts index cc647982d8..8a9e4f452d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts @@ -52,7 +52,7 @@ export class BitbucketPreparer implements PreparerBase { if (this.username || this.privateToken) { this.logger.warn( - "DEPRECATION: Using the token format under 'scaffolder.github.token' will not be respected in future releases. Please consider using integrations config instead", + "DEPRECATION: Using the token format under 'scaffolder.bitbucket.token' will not be respected in future releases. Please consider using integrations config instead", ); } } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts index 8947c09e60..fe4637b971 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts @@ -20,12 +20,15 @@ import { GitRepositoryCreateOptions } from 'azure-devops-node-api/interfaces/Git import { JsonValue, Config } from '@backstage/config'; import { RequiredTemplateValues } from '../templater'; import { initRepoAndPush } from './helpers'; - +import { Config } from '@backstage/config'; +import { Logger } from 'winston'; export class AzurePublisher implements PublisherBase { private readonly client: GitApi; private readonly token: string; + private readonly logger: Logger; - constructor(config: Config) { + constructor(config: Config, { logger }: { logger: Logger }) { + this.logger = logger; this.client = client; this.token = token; } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts index 4fb47813b2..cda9a83019 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts @@ -28,6 +28,7 @@ import { export class GitlabPublisher implements PublisherBase { private readonly integrations: GitLabIntegrationConfig[]; private readonly scaffolderToken: string | undefined; + private readonly apiBaseUrl: string | undefined; private readonly logger: Logger; constructor(config: Config, { logger }: { logger: Logger }) { @@ -46,11 +47,19 @@ export class GitlabPublisher implements PublisherBase { 'scaffolder.gitlab.api.token', ); + this.apiBaseUrl = config.getOptionalString('scaffolder.gitlab.api.baseUrl'); + if (this.scaffolderToken) { this.logger.warn( "DEPRECATION: Using the token format under 'scaffolder.gitlab.api.token' will not be respected in future releases. Please consider using integrations config instead", ); } + + if (this.apiBaseUrl) { + this.logger.warn( + "DEPRECATION: Using the apiBaseUrl format under 'scaffolder.gitlab.api.baseUrl' will not be respected in future releases. Please consider using integrations config instead", + ); + } } async publish({ @@ -58,20 +67,47 @@ export class GitlabPublisher implements PublisherBase { directory, }: PublisherOptions): Promise { const remoteUrl = await this.createRemote(values); + const { host } = new URL(remoteUrl); + const token = this.getToken(host); + + if (!token) { + throw new Error('No token provided to create the remote repository'); + } await initRepoAndPush({ dir: directory, remoteUrl, auth: { username: 'oauth2', - password: this.token, + password: token, }, - logger, + logger: this.logger, }); return { remoteUrl }; } + private getToken(host: string): string | undefined { + return ( + this.scaffolderToken || + this.integrations.find(c => c.host === host)?.token + ); + } + + private getBaseUrl(host: string): string | undefined { + return ( + this.apiBaseUrl || + this.integrations.find(c => c.host === host)?.apiBaseUrl + ); + } + + private getConfig(host: string): { baseUrl?: string; token?: string } { + return { + baseUrl: this.getBaseUrl(host), + token: this.getToken(host), + }; + } + private async createRemote( values: RequiredTemplateValues & Record, ) { @@ -80,15 +116,24 @@ export class GitlabPublisher implements PublisherBase { pathElements.pop(); const owner = pathElements.join('/'); - let targetNamespace = ((await this.client.Namespaces.show(owner)) as { + const config = this.getConfig(); + + if (!config.token) { + throw new Error( + 'No authentication set for Gitlab publisher. Creating the remote repository is not possible without a token', + ); + } + + const client = new Gitlab({ host: config.baseUrl, token: config.token }); + + let targetNamespace = ((await client.Namespaces.show(owner)) as { id: number; }).id; if (!targetNamespace) { - targetNamespace = ((await this.client.Users.current()) as { id: number }) - .id; + targetNamespace = ((await client.Users.current()) as { id: number }).id; } - const project = (await this.client.Projects.create({ + const project = (await client.Projects.create({ namespace_id: targetNamespace, name: name, })) as { http_url_to_repo: string }; diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index fd4e4c3ded..aceaf673e2 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -63,7 +63,7 @@ const OWNER_REPO_SCHEMA = { description: 'Who is going to own this component', }, storePath: { - format: 'GitHub user or org / Repo name', + format: 'storeLocation', type: 'string' as const, title: 'Store path', description: 'GitHub store path in org/repo format', @@ -77,7 +77,7 @@ const OWNER_REPO_SCHEMA = { }; const REPO_FORMAT = { - 'GitHub user or org / Repo name': /[^\/]*\/[^\/]*/, + storeLocation: /[^\/]*\/[^\/]*/, }; export const TemplatePage = () => { From 19355928c004cc494a9e531c3afa310c0ad27585 Mon Sep 17 00:00:00 2001 From: Jussi Hallila Date: Tue, 12 Jan 2021 17:29:44 +0100 Subject: [PATCH 025/209] Modifies component registration to use existing file if present Creates additional check on catalog import form to check for an existing catalog-info.yaml file if repo URL put in. Registers an existing file instead of creating pull request in case catalog file is present. Defaults to the root-most catalog-info.yaml file if multiple defined in the repository. --- .../src/api/CatalogImportApi.ts | 5 ++ .../src/api/CatalogImportClient.test.ts | 64 +++++++++++++++++++ .../src/api/CatalogImportClient.ts | 48 ++++++++++++++ .../src/components/ImportComponentForm.tsx | 42 ++++++++---- .../src/components/ImportComponentPage.tsx | 5 +- .../catalog-import/src/util/useGithubRepos.ts | 39 ++++++++++- 6 files changed, 186 insertions(+), 17 deletions(-) create mode 100644 plugins/catalog-import/src/api/CatalogImportClient.test.ts diff --git a/plugins/catalog-import/src/api/CatalogImportApi.ts b/plugins/catalog-import/src/api/CatalogImportApi.ts index 99526a3eaa..5abb0e3e53 100644 --- a/plugins/catalog-import/src/api/CatalogImportApi.ts +++ b/plugins/catalog-import/src/api/CatalogImportApi.ts @@ -30,6 +30,11 @@ export interface CatalogImportApi { fileContent: string; githubIntegrationConfig: GitHubIntegrationConfig; }): Promise<{ link: string; location: string }>; + checkForExistingCatalogInfo(options: { + owner: string; + repo: string; + githubIntegrationConfig: GitHubIntegrationConfig; + }): Promise<{ exists: boolean; url?: string }>; createRepositoryLocation(options: { location: string }): Promise; generateEntityDefinitions(options: { repo: string; diff --git a/plugins/catalog-import/src/api/CatalogImportClient.test.ts b/plugins/catalog-import/src/api/CatalogImportClient.test.ts new file mode 100644 index 0000000000..0e0cf4b323 --- /dev/null +++ b/plugins/catalog-import/src/api/CatalogImportClient.test.ts @@ -0,0 +1,64 @@ +/* + * 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 { CatalogImportClient } from './CatalogImportClient'; + +jest.mock('@octokit/rest', () => ({ + Octokit: jest.fn().mockImplementation(() => { + return { + repos: { + get: () => + Promise.resolve({ + data: { + default_branch: 'main', + }, + }), + }, + search: { + code: () => + Promise.resolve({ + data: { + total_count: 2, + items: [ + { path: 'simple/path/catalog-info.yaml' }, + { path: 'co/mple/x/path/catalog-info.yaml' }, + { path: 'catalog-info.yaml' }, + ], + }, + }), + }, + }; + }), +})); + +describe('CatalogImportClient', () => { + describe('checkForExistingCatalogInfo', () => { + const cic = new CatalogImportClient({ + discoveryApi: { getBaseUrl: () => Promise.resolve('base') }, + githubAuthApi: { getAccessToken: (_, __) => Promise.resolve('token') }, + configApi: {} as any, + }); + it('should return the closest-to-root catalog-info from multiple responses', async () => { + const respo = await cic.checkForExistingCatalogInfo({ + owner: 'test-user', + repo: 'rest-repo', + githubIntegrationConfig: { host: 'https://github.com' }, + }); + expect(respo.exists).toBe(true); + expect(respo.url).toBe('blob/main/catalog-info.yaml'); + }); + }); +}); diff --git a/plugins/catalog-import/src/api/CatalogImportClient.ts b/plugins/catalog-import/src/api/CatalogImportClient.ts index 0e28042db3..af429f748a 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.ts @@ -91,6 +91,54 @@ export class CatalogImportClient implements CatalogImportApi { } } + async checkForExistingCatalogInfo({ + owner, + repo, + githubIntegrationConfig, + }: { + owner: string; + repo: string; + githubIntegrationConfig: GitHubIntegrationConfig; + }): Promise<{ exists: boolean; url?: string }> { + const token = await this.githubAuthApi.getAccessToken(['repo']); + const octo = new Octokit({ + auth: token, + baseUrl: githubIntegrationConfig.apiBaseUrl, + }); + const catalogFileName = 'catalog-info.yaml'; + const query = `repo:${owner}/${repo}+filename:${catalogFileName}`; + + const searchResult = await octo.search.code({ q: query }).catch(e => { + throw new Error( + formatHttpErrorMessage( + "Couldn't search repository for metadata file.", + e, + ), + ); + }); + const exists = searchResult.data.total_count > 0; + if (exists) { + const repoInformation = await octo.repos.get({ owner, repo }).catch(e => { + throw new Error(formatHttpErrorMessage("Couldn't fetch repo data", e)); + }); + const defaultBranch = repoInformation.data.default_branch; + + // Github search sorts returned values with 'best match' using 'multiple factors to boost the most relevant item', + // aka magic. + // Sorting to use the shortest item, closest to the repository root. + const catalogInfoItem = searchResult.data.items + .map(it => it.path) + .sort((a, b) => a.length - b.length)[0]; + return Promise.resolve({ + url: `blob/${defaultBranch}/${catalogInfoItem}`, + exists, + }); + } + return Promise.resolve({ + exists, + }); + } + async submitPrToRepo({ owner, repo, diff --git a/plugins/catalog-import/src/components/ImportComponentForm.tsx b/plugins/catalog-import/src/components/ImportComponentForm.tsx index dab5ccbea2..b7dc82c9b1 100644 --- a/plugins/catalog-import/src/components/ImportComponentForm.tsx +++ b/plugins/catalog-import/src/components/ImportComponentForm.tsx @@ -59,27 +59,45 @@ export const RegisterComponentForm = ({ nextStep, saveConfig }: Props) => { const isMounted = useMountedState(); const errorApi = useApi(errorApiRef); - const { generateEntityDefinitions } = useGithubRepos(); + const { + generateEntityDefinitions, + checkForExistingCatalogInfo, + } = useGithubRepos(); const onSubmit = async (formData: Record) => { const { componentLocation: target } = formData; - try { - if (!isMounted()) return; - const type = !parseGitUri(target).filepathtype ? 'repo' : 'file'; + async function saveCatalogFileConfig(target: string) { + const data = await catalogApi.addLocation({ target }); + saveConfig({ + type: 'file', + location: data.location.target, + config: data.entities, + }); + } - if (type === 'repo') { + async function trySaveRepositoryConfig(target: string) { + const existingCatalog = await checkForExistingCatalogInfo(target); + if (existingCatalog.exists) { + const targetUrl = target.endsWith('/') + ? `${target}${existingCatalog.url}` + : `${target}/${existingCatalog.url}`; + await saveCatalogFileConfig(targetUrl); + } else { saveConfig({ - type, + type: 'repo', location: target, config: await generateEntityDefinitions(target), }); + } + } + + try { + if (!isMounted()) return; + const type = !parseGitUri(target).filepathtype ? 'repo' : 'file'; + if (type === 'repo') { + await trySaveRepositoryConfig(target); } else { - const data = await catalogApi.addLocation({ target }); - saveConfig({ - type, - location: data.location.target, - config: data.entities, - }); + await saveCatalogFileConfig(target); } nextStep(); } catch (e) { diff --git a/plugins/catalog-import/src/components/ImportComponentPage.tsx b/plugins/catalog-import/src/components/ImportComponentPage.tsx index 9fdb465dd1..8b3b90003d 100644 --- a/plugins/catalog-import/src/components/ImportComponentPage.tsx +++ b/plugins/catalog-import/src/components/ImportComponentPage.tsx @@ -77,8 +77,9 @@ export const ImportComponentPage = ({ GitHub Repo - If you already have code in a GitHub repository, enter the full - URL to your repo and a new pull request with a sample Backstage + If you already have code in a GitHub repository without + Backstage metadata file set up for it, enter the full URL to + your repo and a new pull request with a sample Backstage metadata Entity File (catalog-info.yaml) will be opened for you. diff --git a/plugins/catalog-import/src/util/useGithubRepos.ts b/plugins/catalog-import/src/util/useGithubRepos.ts index 30e77e18a8..56cf101c35 100644 --- a/plugins/catalog-import/src/util/useGithubRepos.ts +++ b/plugins/catalog-import/src/util/useGithubRepos.ts @@ -28,12 +28,12 @@ export function useGithubRepos() { const api = useApi(catalogImportApiRef); const config = useApi(configApiRef); - const submitPrToRepo = async (selectedRepo: ConfigSpec) => { + const getGithubIntegrationConfig = (location: string) => { const { name: repoName, owner: ownerName, resource: hostname, - } = parseGitUri(selectedRepo.location); + } = parseGitUri(location); const configs = readGitHubIntegrationConfigs( config.getOptionalConfigArray('integrations.github') ?? [], @@ -41,9 +41,22 @@ export function useGithubRepos() { const githubIntegrationConfig = configs.find(v => v.host === hostname); if (!githubIntegrationConfig) { throw new Error( - `Unable to locate github-integration for repo-location: ${selectedRepo.location}`, + `Unable to locate github-integration for repo-location: ${location}`, ); } + return { + repoName, + ownerName, + githubIntegrationConfig, + }; + }; + + const submitPrToRepo = async (selectedRepo: ConfigSpec) => { + const { + repoName, + ownerName, + githubIntegrationConfig, + } = getGithubIntegrationConfig(selectedRepo.location); const submitPRResponse = await api .submitPrToRepo({ owner: ownerName, @@ -68,8 +81,28 @@ export function useGithubRepos() { return submitPRResponse; }; + const checkForExistingCatalogInfo = async (location: string) => { + const { + repoName, + ownerName, + githubIntegrationConfig, + } = getGithubIntegrationConfig(location); + return await api + .checkForExistingCatalogInfo({ + owner: ownerName, + repo: repoName, + githubIntegrationConfig, + }) + .catch(e => { + throw new Error( + `Failed to inspect repository for existing catalog-info.yaml:\n${e.message}`, + ); + }); + }; + return { submitPrToRepo, + checkForExistingCatalogInfo, generateEntityDefinitions: (repo: string) => api.generateEntityDefinitions({ repo }), addLocation: (location: string) => From 44f841959b945c34376fae0c0ae2b2d49d3506db Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Tue, 12 Jan 2021 19:45:48 +0200 Subject: [PATCH 026/209] Added backend tests; Fixes following CR --- plugins/kafka-backend/package.json | 13 +- plugins/kafka-backend/src/index.ts | 3 +- plugins/kafka-backend/src/service/KafkaApi.ts | 19 +- .../kafka-backend/src/service/router.test.ts | 105 +++++++ plugins/kafka-backend/src/service/router.ts | 44 +-- plugins/kafka-backend/src/setupTests.ts | 9 +- plugins/kafka-backend/src/types/types.ts | 20 -- plugins/kafka/package.json | 3 - plugins/kafka/src/api/KafkaBackendClient.ts | 10 +- plugins/kafka/src/api/types.ts | 15 +- .../ConsumerGroupOffsets.test.tsx | 39 +++ .../__fixtures__/consumerGroupOffsets.json | 20 ++ .../useConsumerGroupOffsets.ts | 20 +- plugins/kafka/src/plugin.ts | 3 +- yarn.lock | 277 ++++++++++++++++-- 15 files changed, 483 insertions(+), 117 deletions(-) create mode 100644 plugins/kafka-backend/src/service/router.test.ts delete mode 100644 plugins/kafka-backend/src/types/types.ts create mode 100644 plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.test.tsx create mode 100644 plugins/kafka/src/components/ConsumerGroupOffsets/__fixtures__/consumerGroupOffsets.json diff --git a/plugins/kafka-backend/package.json b/plugins/kafka-backend/package.json index fb255ab157..381a6abbe8 100644 --- a/plugins/kafka-backend/package.json +++ b/plugins/kafka-backend/package.json @@ -31,21 +31,26 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@backstage/backend-common": "^0.4.1", + "@backstage/catalog-model": "^0.6.0", + "@backstage/config": "^0.1.2", + "@types/express": "^4.17.6", "compression": "^1.7.4", "cors": "^2.8.5", "express": "^4.17.1", "express-promise-router": "^3.0.3", "helmet": "^4.0.0", - "@backstage/backend-common": "^0.4.1", - "@backstage/catalog-model": "^0.6.0", - "@backstage/config": "^0.1.2", - "@types/express": "^4.17.6", "kafkajs": "^1.16.0-beta.6", + "lodash": "^4.17.15", "winston": "^3.2.1", "yn": "^4.0.0" }, "devDependencies": { "@backstage/cli": "^0.4.3", + "@types/jest-when": "^2.7.2", + "@types/lodash": "^4.14.151", + "jest-extended": "^0.11.5", + "jest-when": "^3.1.0", "supertest": "^4.0.2" }, "files": [ diff --git a/plugins/kafka-backend/src/index.ts b/plugins/kafka-backend/src/index.ts index 96f070ff1e..bf496e05bf 100644 --- a/plugins/kafka-backend/src/index.ts +++ b/plugins/kafka-backend/src/index.ts @@ -14,5 +14,4 @@ * limitations under the License. */ -export * from './service/router'; -export * from './types/types'; +export { createRouter } from './service/router'; diff --git a/plugins/kafka-backend/src/service/KafkaApi.ts b/plugins/kafka-backend/src/service/KafkaApi.ts index 17595fcd12..b979796f4a 100644 --- a/plugins/kafka-backend/src/service/KafkaApi.ts +++ b/plugins/kafka-backend/src/service/KafkaApi.ts @@ -27,12 +27,17 @@ export type TopicOffset = { partitions: PartitionOffset[]; }; -export class KafkaApi { +export interface KafkaApi { + fetchTopicOffsets(topic: string): Promise>; + fetchGroupOffsets(groupId: string): Promise>; +} + +export class KafkaJsApiImpl implements KafkaApi { private readonly kafka: Kafka; private readonly logger: Logger; constructor(clientId: string, brokers: string[], logger: Logger) { - logger.info( + logger.debug( `creating kafka client with clientId=${clientId} and brokers=${brokers}`, ); @@ -41,20 +46,22 @@ export class KafkaApi { } async fetchTopicOffsets(topic: string): Promise> { - this.logger.info(`fetching topic offsets for ${topic}`); + this.logger.debug(`fetching topic offsets for ${topic}`); const admin = this.kafka.admin(); await admin.connect(); try { - return KafkaApi.toPartitionOffsets(await admin.fetchTopicOffsets(topic)); + return KafkaJsApiImpl.toPartitionOffsets( + await admin.fetchTopicOffsets(topic), + ); } finally { await admin.disconnect(); } } async fetchGroupOffsets(groupId: string): Promise> { - this.logger.info(`fetching consumer group offsets for ${groupId}`); + this.logger.debug(`fetching consumer group offsets for ${groupId}`); const admin = this.kafka.admin(); await admin.connect(); @@ -64,7 +71,7 @@ export class KafkaApi { return groupOffsets.map(topicOffset => ({ topic: topicOffset.topic, - partitions: KafkaApi.toPartitionOffsets(topicOffset.partitions), + partitions: KafkaJsApiImpl.toPartitionOffsets(topicOffset.partitions), })); } finally { await admin.disconnect(); diff --git a/plugins/kafka-backend/src/service/router.test.ts b/plugins/kafka-backend/src/service/router.test.ts new file mode 100644 index 0000000000..5e9de6cf2f --- /dev/null +++ b/plugins/kafka-backend/src/service/router.test.ts @@ -0,0 +1,105 @@ +/* + * 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 request from 'supertest'; +import express from 'express'; +import { makeRouter } from './router'; +import { getVoidLogger } from '@backstage/backend-common'; +import { KafkaApi } from './KafkaApi'; +import { when } from 'jest-when'; + +describe('router', () => { + let app: express.Express; + let kafkaApi: jest.Mocked; + + beforeAll(async () => { + kafkaApi = { + fetchTopicOffsets: jest.fn(), + fetchGroupOffsets: jest.fn(), + }; + + const router = makeRouter(getVoidLogger(), kafkaApi); + app = express().use(router); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('get /consumer/:consumerId/offsets', () => { + it('returns topic and group offsets', async () => { + const topic1Offsets = [ + { id: 1, offset: '500' }, + { id: 2, offset: '1000' }, + ]; + const topic2Offsets = [{ id: 1, offset: '456' }]; + + const groupOffsets = [ + { + topic: 'topic1', + partitions: [ + { id: 1, offset: '100' }, + { id: 2, offset: '213' }, + ], + }, + { + topic: 'topic2', + partitions: [{ id: 1, offset: '456' }], + }, + ]; + when(kafkaApi.fetchTopicOffsets) + .calledWith('topic1') + .mockResolvedValue(topic1Offsets); + when(kafkaApi.fetchTopicOffsets) + .calledWith('topic2') + .mockResolvedValue(topic2Offsets); + kafkaApi.fetchGroupOffsets.mockResolvedValue(groupOffsets); + + const response = await request(app).get('/consumer/hey/offsets'); + + expect(response.status).toEqual(200); + expect(response.body.consumerId).toEqual('hey'); + expect(response.body.offsets).toIncludeSameMembers([ + { + topic: 'topic1', + partitionId: 1, + groupOffset: '100', + topicOffset: '500', + }, + { + topic: 'topic1', + partitionId: 2, + groupOffset: '213', + topicOffset: '1000', + }, + { + topic: 'topic2', + partitionId: 1, + groupOffset: '456', + topicOffset: '456', + }, + ]); + }); + + it('handles internal error correctly', async () => { + kafkaApi.fetchGroupOffsets.mockRejectedValue(Error('oh no')); + + const response = await request(app).get('/consumer/hey/offsets'); + + expect(response.status).toEqual(500); + }); + }); +}); diff --git a/plugins/kafka-backend/src/service/router.ts b/plugins/kafka-backend/src/service/router.ts index a1a60a4fdb..9c61e05bcd 100644 --- a/plugins/kafka-backend/src/service/router.ts +++ b/plugins/kafka-backend/src/service/router.ts @@ -18,7 +18,8 @@ import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; import { Config } from '@backstage/config'; -import { KafkaApi } from './KafkaApi'; +import { KafkaApi, KafkaJsApiImpl } from './KafkaApi'; +import _ from 'lodash'; export interface RouterOptions { logger: Logger; @@ -32,28 +33,27 @@ export const makeRouter = ( const router = Router(); router.use(express.json()); - router.get('/topic/:topicId/offsets', async (req, res) => { - const topicId = req.params.topicId; - try { - const response = await kafkaApi.fetchTopicOffsets(topicId); - res.send(response); - } catch (e) { - logger.error(`action=fetchTopicOffsets topicId=${topicId}, error=${e}`); - res.status(500).send({ error: e.message }); - } - }); - router.get('/consumer/:consumerId/offsets', async (req, res) => { const consumerId = req.params.consumerId; - try { - const response = await kafkaApi.fetchGroupOffsets(consumerId); - res.send(response); - } catch (e) { - logger.error( - `action=fetchGroupOffsets consumerId=${consumerId}, error=${e}`, - ); - res.status(500).send({ error: e.message }); - } + + const groupOffsets = await kafkaApi.fetchGroupOffsets(consumerId); + + const groupWithTopicOffsets = await Promise.all( + groupOffsets.map(async ({ topic, partitions }) => { + const topicOffsets = _.keyBy( + await kafkaApi.fetchTopicOffsets(topic), + partition => partition.id, + ); + + return partitions.map(partition => ({ + topic: topic, + partitionId: partition.id, + groupOffset: partition.offset, + topicOffset: topicOffsets[partition.id].offset, + })); + }), + ); + res.send({ consumerId, offsets: groupWithTopicOffsets.flat() }); }); return router; @@ -69,7 +69,7 @@ export async function createRouter( const clientId = options.config.getString('kafka.clientId'); const brokers = options.config.getStringArray('kafka.brokers'); - const kafkaApi = new KafkaApi(clientId, brokers, logger); + const kafkaApi = new KafkaJsApiImpl(clientId, brokers, logger); return makeRouter(logger, kafkaApi); } diff --git a/plugins/kafka-backend/src/setupTests.ts b/plugins/kafka-backend/src/setupTests.ts index ba33cf996b..dc3cedf4e3 100644 --- a/plugins/kafka-backend/src/setupTests.ts +++ b/plugins/kafka-backend/src/setupTests.ts @@ -14,4 +14,11 @@ * limitations under the License. */ -export {}; +import type { Config } from '@jest/types'; +import 'jest-extended'; + +const config: Config.InitialOptions = { + setupFilesAfterEnv: ['jest-extended'], +}; + +export default config; diff --git a/plugins/kafka-backend/src/types/types.ts b/plugins/kafka-backend/src/types/types.ts deleted file mode 100644 index 0fb6dde77f..0000000000 --- a/plugins/kafka-backend/src/types/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 { TopicOffset, PartitionOffset } from '../service/KafkaApi'; - -export type TopicOffsetsResponse = Array; -export type ConsumerGroupOffsetsResponse = Array; diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index bc809cac90..8b4e84ab2d 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -24,11 +24,9 @@ "@backstage/core": "^0.4.3", "@backstage/theme": "^0.2.2", "@backstage/plugin-catalog": "^0.2.7", - "@backstage/plugin-kafka-backend": "^0.1.0", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", - "lodash": "^4.17.20", "react": "^16.13.1", "react-router": "6.0.0-beta.0", "react-dom": "^16.13.1", @@ -42,7 +40,6 @@ "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", - "@types/lodash": "^4.14.166", "@types/node": "^12.0.0", "cross-fetch": "^3.0.6", "msw": "^0.21.2" diff --git a/plugins/kafka/src/api/KafkaBackendClient.ts b/plugins/kafka/src/api/KafkaBackendClient.ts index 4a895c5556..2112dd0f17 100644 --- a/plugins/kafka/src/api/KafkaBackendClient.ts +++ b/plugins/kafka/src/api/KafkaBackendClient.ts @@ -15,11 +15,7 @@ */ import { DiscoveryApi } from '@backstage/core'; -import { KafkaApi } from './types'; -import { - ConsumerGroupOffsetsResponse, - TopicOffsetsResponse, -} from '@backstage/plugin-kafka-backend'; +import { KafkaApi, ConsumerGroupOffsetsResponse } from './types'; export class KafkaBackendClient implements KafkaApi { private readonly discoveryApi: DiscoveryApi; @@ -46,10 +42,6 @@ export class KafkaBackendClient implements KafkaApi { return await response.json(); } - async getTopicOffsets(topic: string): Promise { - return await this.getRequired(`/topic/${topic}/offsets`); - } - async getConsumerGroupOffsets( consumerGroup: string, ): Promise { diff --git a/plugins/kafka/src/api/types.ts b/plugins/kafka/src/api/types.ts index ec74dd25a2..9c2239156b 100644 --- a/plugins/kafka/src/api/types.ts +++ b/plugins/kafka/src/api/types.ts @@ -15,10 +15,6 @@ */ import { createApiRef } from '@backstage/core'; -import { - ConsumerGroupOffsetsResponse, - TopicOffsetsResponse, -} from '@backstage/plugin-kafka-backend'; export const kafkaApiRef = createApiRef({ id: 'plugin.kafka.service', @@ -26,8 +22,17 @@ export const kafkaApiRef = createApiRef({ 'Used by the Kafka plugin to make requests to accompanying backend', }); +export type ConsumerGroupOffsetsResponse = { + consumerId: string; + offsets: { + topic: string; + partitionId: number; + topicOffset: string; + groupOffset: string; + }[]; +}; + export interface KafkaApi { - getTopicOffsets(topic: string): Promise; getConsumerGroupOffsets( consumerGroup: string, ): Promise; diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.test.tsx b/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.test.tsx new file mode 100644 index 0000000000..3c499f1621 --- /dev/null +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.test.tsx @@ -0,0 +1,39 @@ +/* + * 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 React from 'react'; +import { render } from '@testing-library/react'; +import { ConsumerGroupOffsets } from './ConsumerGroupOffsets'; +import * as testOffsets from './__fixtures__/consumerGroupoffsets.json'; +import { wrapInTestApp } from '@backstage/test-utils'; + +describe('ConfigMaps', () => { + it('should render consumer group offsets', async () => { + const { getByText } = render( + wrapInTestApp(), + ); + + // title + expect(getByText('dice-roller')).toBeInTheDocument(); + expect(getByText('Config Map')).toBeInTheDocument(); + + // values + expect(getByText('Immutable')).toBeInTheDocument(); + expect(getByText('false')).toBeInTheDocument(); + expect(getByText('Data')).toBeInTheDocument(); + expect(getByText('Foo: bar')).toBeInTheDocument(); // TODO wish this wasn't upper case + }); +}); diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/__fixtures__/consumerGroupOffsets.json b/plugins/kafka/src/components/ConsumerGroupOffsets/__fixtures__/consumerGroupOffsets.json new file mode 100644 index 0000000000..4409542787 --- /dev/null +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/__fixtures__/consumerGroupOffsets.json @@ -0,0 +1,20 @@ +[ + { + "topic": "topic1", + "partitionId": 1, + "groupOffset": "100", + "topicOffset": "500" + }, + { + "topic": "topic1", + "partitionId": 2, + "groupOffset": "213", + "topicOffset": "1000" + }, + { + "topic": "topic2", + "partitionId": 1, + "groupOffset": "456", + "topicOffset": "456" + } +] diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupOffsets.ts b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupOffsets.ts index 575f962b4d..74d408fe86 100644 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupOffsets.ts +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupOffsets.ts @@ -17,7 +17,6 @@ import { errorApiRef, useApi } from '@backstage/core'; import { useAsyncRetry } from 'react-use'; import { kafkaApiRef } from '../../api/types'; -import _ from 'lodash'; export function useConsumerGroupOffsets(groupId: string) { const api = useApi(kafkaApiRef); @@ -25,23 +24,8 @@ export function useConsumerGroupOffsets(groupId: string) { const { loading, value: topics, retry } = useAsyncRetry(async () => { try { - const groupOffsets = await api.getConsumerGroupOffsets(groupId); - const groupWithTopicOffsets = await Promise.all( - groupOffsets.map(async ({ topic, partitions }) => { - const topicOffsets = _.keyBy( - await api.getTopicOffsets(topic), - partition => partition.id, - ); - - return partitions.map(partition => ({ - topic: topic, - partitionId: partition.id, - groupOffset: partition.offset, - topicOffset: topicOffsets[partition.id].offset, - })); - }), - ); - return groupWithTopicOffsets.flat(); + const response = await api.getConsumerGroupOffsets(groupId); + return response.offsets; } catch (e) { errorApi.post(e); throw e; diff --git a/plugins/kafka/src/plugin.ts b/plugins/kafka/src/plugin.ts index 3f7e41e105..878a7f56e9 100644 --- a/plugins/kafka/src/plugin.ts +++ b/plugins/kafka/src/plugin.ts @@ -14,7 +14,6 @@ * limitations under the License. */ import { - configApiRef, createApiFactory, createPlugin, createRouteRef, @@ -33,7 +32,7 @@ export const plugin = createPlugin({ apis: [ createApiFactory({ api: kafkaApiRef, - deps: { discoveryApi: discoveryApiRef, configApi: configApiRef }, + deps: { discoveryApi: discoveryApiRef }, factory: ({ discoveryApi }) => new KafkaBackendClient({ discoveryApi }), }), ], diff --git a/yarn.lock b/yarn.lock index e16e4c6857..4104875b4e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1644,9 +1644,11 @@ to-fast-properties "^2.0.0" "@backstage/catalog-model@^0.2.0": - version "0.6.0" + version "0.2.0" + resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.2.0.tgz#e3fe2a4ddeb6a9b6ec480c80cb2b9c39cb245576" + integrity sha512-Y1ocdRpBlxK/VrJQjHlQd0bgADECd1B2NRjwd8ss46ibT5hwLvMOfD80+Fa7oPLu0ktJrH4lq0pNIIJIml48zA== dependencies: - "@backstage/config" "^0.1.2" + "@backstage/config" "^0.1.1" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" json-schema "^0.2.5" @@ -1655,9 +1657,11 @@ yup "^0.29.3" "@backstage/catalog-model@^0.3.0": - version "0.6.0" + version "0.3.1" + resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.3.1.tgz#45d08e2f333c9c566b2bf2629fd707fe989bb404" + integrity sha512-9XhV7c4rmVW+Yzj2PiwTQ7DsegWGB3C4ELsDRExuEVZONdqNcC02cyJtrt3fT5F31ZS3tHkB9bMUymFOBLqUSA== dependencies: - "@backstage/config" "^0.1.2" + "@backstage/config" "^0.1.1" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" json-schema "^0.2.5" @@ -1666,16 +1670,17 @@ yup "^0.29.3" "@backstage/core@^0.3.0": - version "0.4.3" + version "0.3.2" + resolved "https://registry.npmjs.org/@backstage/core/-/core-0.3.2.tgz#a8209126d5076cf4a8b9bd632fe4e5e2edb62916" + integrity sha512-i5d+Wh8js4qEWoAsPY5L7HVSWpumr1OhfF2dUCGYdyW6AMqVJPca6+n6zp1Rg2CO+J9norp44XAVVCbyhtUpig== dependencies: - "@backstage/config" "^0.1.2" - "@backstage/core-api" "^0.2.8" - "@backstage/theme" "^0.2.2" + "@backstage/config" "^0.1.1" + "@backstage/core-api" "^0.2.1" + "@backstage/theme" "^0.2.1" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" "@material-ui/lab" "4.0.0-alpha.45" "@types/dagre" "^0.7.44" - "@types/prop-types" "^15.7.3" "@types/react" "^16.9" "@types/react-sparklines" "^1.7.0" classnames "^2.2.6" @@ -2753,6 +2758,15 @@ resolved "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== +"@jest/console@^24.9.0": + version "24.9.0" + resolved "https://registry.npmjs.org/@jest/console/-/console-24.9.0.tgz#79b1bc06fb74a8cfb01cbdedf945584b1b9707f0" + integrity sha512-Zuj6b8TnKXi3q4ymac8EQfc3ea/uhLeCGThFqXeC8H9/raaH8ARPUTdId+XyGd03Z4In0/VjD2OYFcBF09fNLQ== + dependencies: + "@jest/source-map" "^24.9.0" + chalk "^2.0.1" + slash "^2.0.0" + "@jest/console@^26.6.2": version "26.6.2" resolved "https://registry.npmjs.org/@jest/console/-/console-26.6.2.tgz#4e04bc464014358b03ab4937805ee36a0aeb98f2" @@ -2867,6 +2881,15 @@ optionalDependencies: node-notifier "^8.0.0" +"@jest/source-map@^24.9.0": + version "24.9.0" + resolved "https://registry.npmjs.org/@jest/source-map/-/source-map-24.9.0.tgz#0e263a94430be4b41da683ccc1e6bffe2a191714" + integrity sha512-/Xw7xGlsZb4MJzNDgB7PW5crou5JqWiBQaz6xyPd3ArOg2nfn/PunV8+olXbbEZzNl591o5rWKE9BRDaFAuIBg== + dependencies: + callsites "^3.0.0" + graceful-fs "^4.1.15" + source-map "^0.6.0" + "@jest/source-map@^26.6.2": version "26.6.2" resolved "https://registry.npmjs.org/@jest/source-map/-/source-map-26.6.2.tgz#29af5e1e2e324cafccc936f218309f54ab69d535" @@ -2876,6 +2899,15 @@ graceful-fs "^4.2.4" source-map "^0.6.0" +"@jest/test-result@^24.9.0": + version "24.9.0" + resolved "https://registry.npmjs.org/@jest/test-result/-/test-result-24.9.0.tgz#11796e8aa9dbf88ea025757b3152595ad06ba0ca" + integrity sha512-XEFrHbBonBJ8dGp2JmF8kP/nQI/ImPpygKHwQ/SY+es59Z3L5PI4Qb9TQQMAEeYsThG1xF0k6tmG0tIKATNiiA== + dependencies: + "@jest/console" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/istanbul-lib-coverage" "^2.0.0" + "@jest/test-result@^26.6.2": version "26.6.2" resolved "https://registry.npmjs.org/@jest/test-result/-/test-result-26.6.2.tgz#55da58b62df134576cc95476efa5f7949e3f5f18" @@ -2918,6 +2950,15 @@ source-map "^0.6.1" write-file-atomic "^3.0.0" +"@jest/types@^24.9.0": + version "24.9.0" + resolved "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz#63cb26cb7500d069e5a389441a7c6ab5e909fc59" + integrity sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^1.1.1" + "@types/yargs" "^13.0.0" + "@jest/types@^26.6.1": version "26.6.1" resolved "https://registry.npmjs.org/@jest/types/-/types-26.6.1.tgz#2638890e8031c0bc8b4681e0357ed986e2f866c5" @@ -3729,13 +3770,6 @@ react-is "^16.8.0" react-transition-group "^4.4.0" -"@material-ui/icons@^4.11.2": - version "4.11.2" - resolved "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.2.tgz#b3a7353266519cd743b6461ae9fdfcb1b25eb4c5" - integrity sha512-fQNsKX2TxBmqIGJCSi3tGTO/gZ+eJgWmMJkgDiOfyNaunNaxcklJQFaFogYcFl0qFuaEz1qaXYXboa/bUXVSOQ== - dependencies: - "@babel/runtime" "^7.4.4" - "@material-ui/icons@^4.9.1": version "4.9.1" resolved "https://registry.npmjs.org/@material-ui/icons/-/icons-4.9.1.tgz#fdeadf8cb3d89208945b33dbc50c7c616d0bd665" @@ -5751,6 +5785,14 @@ dependencies: "@types/istanbul-lib-coverage" "*" +"@types/istanbul-reports@^1.1.1": + version "1.1.2" + resolved "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz#e875cc689e47bce549ec81f3df5e6f6f11cfaeb2" + integrity sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw== + dependencies: + "@types/istanbul-lib-coverage" "*" + "@types/istanbul-lib-report" "*" + "@types/istanbul-reports@^3.0.0": version "3.0.0" resolved "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz#508b13aa344fa4976234e75dddcc34925737d821" @@ -5758,6 +5800,13 @@ dependencies: "@types/istanbul-lib-report" "*" +"@types/jest-when@^2.7.2": + version "2.7.2" + resolved "https://registry.npmjs.org/@types/jest-when/-/jest-when-2.7.2.tgz#619fbc5f623bcd0b29efde0e4993c7f0d50d026d" + integrity sha512-vOtj0cev6vO1VX7Jbfg/qvy+sfLI64STsHbKVkggK+1kd11rcMGzFpZKBxUvQfsm4JRULCBISu+qrfs7fYZFGg== + dependencies: + "@types/jest" "*" + "@types/jest@*", "@types/jest@26.x", "@types/jest@^26.0.7": version "26.0.15" resolved "https://registry.npmjs.org/@types/jest/-/jest-26.0.15.tgz#12e02c0372ad0548e07b9f4e19132b834cb1effe" @@ -5863,11 +5912,6 @@ resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.165.tgz#74d55d947452e2de0742bad65270433b63a8c30f" integrity sha512-tjSSOTHhI5mCHTy/OOXYIhi2Wt1qcbHmuXD1Ha7q70CgI/I71afO4XtLb/cVexki1oVYchpul/TOuu3Arcdxrg== -"@types/lodash@^4.14.166": - version "4.14.166" - resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.166.tgz#07e7f2699a149219dbc3c35574f126ec8737688f" - integrity sha512-A3YT/c1oTlyvvW/GQqG86EyqWNrT/tisOIh2mW3YCgcx71TNjiTZA3zYZWA5BCmtsOTXjhliy4c4yEkErw6njA== - "@types/long@^4.0.0": version "4.0.1" resolved "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" @@ -6288,6 +6332,11 @@ resolved "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA== +"@types/stack-utils@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" + integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== + "@types/stack-utils@^2.0.0": version "2.0.0" resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff" @@ -6505,6 +6554,13 @@ resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" integrity sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw== +"@types/yargs@^13.0.0": + version "13.0.11" + resolved "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz#def2f0c93e4bdf2c61d7e34899b17e34be28d3b1" + integrity sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ== + dependencies: + "@types/yargs-parser" "*" + "@types/yargs@^15.0.0": version "15.0.4" resolved "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.4.tgz#7e5d0f8ca25e9d5849f2ea443cf7c402decd8299" @@ -7044,7 +7100,7 @@ ansi-regex@^3.0.0: resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= -ansi-regex@^4.1.0: +ansi-regex@^4.0.0, ansi-regex@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== @@ -8627,6 +8683,16 @@ builtins@^1.0.3: resolved "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" integrity sha1-y5T662HIaWRR2zZTThQi+U8K7og= +bunyan@^1.8.12: + version "1.8.15" + resolved "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz#8ce34ca908a17d0776576ca1b2f6cbd916e93b46" + integrity sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig== + optionalDependencies: + dtrace-provider "~0.8" + moment "^2.19.3" + mv "~2" + safe-json-stringify "~1" + busboy@^0.3.1: version "0.3.1" resolved "https://registry.npmjs.org/busboy/-/busboy-0.3.1.tgz#170899274c5bf38aae27d5c62b71268cd585fd1b" @@ -10932,6 +10998,11 @@ dicer@0.3.0: dependencies: streamsearch "0.1.2" +diff-sequences@^24.9.0: + version "24.9.0" + resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" + integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew== + diff-sequences@^26.5.0: version "26.5.0" resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.5.0.tgz#ef766cf09d43ed40406611f11c6d8d9dd8b2fefd" @@ -11224,6 +11295,13 @@ downshift@^6.0.6: prop-types "^15.7.2" react-is "^17.0.1" +dtrace-provider@~0.8: + version "0.8.8" + resolved "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz#2996d5490c37e1347be263b423ed7b297fb0d97e" + integrity sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg== + dependencies: + nan "^2.14.0" + duplexer2@~0.1.4: version "0.1.4" resolved "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" @@ -12120,6 +12198,18 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" +expect@^24.1.0, expect@^24.8.0: + version "24.9.0" + resolved "https://registry.npmjs.org/expect/-/expect-24.9.0.tgz#b75165b4817074fa4a157794f46fe9f1ba15b6ca" + integrity sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q== + dependencies: + "@jest/types" "^24.9.0" + ansi-styles "^3.2.0" + jest-get-type "^24.9.0" + jest-matcher-utils "^24.9.0" + jest-message-util "^24.9.0" + jest-regex-util "^24.9.0" + expect@^26.6.2: version "26.6.2" resolved "https://registry.npmjs.org/expect/-/expect-26.6.2.tgz#c6b996bf26bf3fe18b67b2d0f51fc981ba934417" @@ -13234,6 +13324,17 @@ glob@7.1.6, glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glo once "^1.3.0" path-is-absolute "^1.0.0" +glob@^6.0.1: + version "6.0.4" + resolved "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" + integrity sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI= + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + global-dirs@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/global-dirs/-/global-dirs-2.0.1.tgz#acdf3bb6685bcd55cb35e8a052266569e9469201" @@ -15318,6 +15419,16 @@ jest-css-modules@^2.1.0: dependencies: identity-obj-proxy "3.0.0" +jest-diff@^24.9.0: + version "24.9.0" + resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-24.9.0.tgz#931b7d0d5778a1baf7452cb816e325e3724055da" + integrity sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ== + dependencies: + chalk "^2.0.1" + diff-sequences "^24.9.0" + jest-get-type "^24.9.0" + pretty-format "^24.9.0" + jest-diff@^26.0.0: version "26.6.1" resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.1.tgz#38aa194979f454619bb39bdee299fb64ede5300c" @@ -15389,6 +15500,25 @@ jest-esm-transformer@^1.0.0: "@babel/core" "^7.4.4" "@babel/plugin-transform-modules-commonjs" "^7.4.4" +jest-extended@^0.11.5: + version "0.11.5" + resolved "https://registry.npmjs.org/jest-extended/-/jest-extended-0.11.5.tgz#f063b3f1eaadad8d7c13a01f0dfe0f538d498ccf" + integrity sha512-3RsdFpLWKScpsLD6hJuyr/tV5iFOrw7v6YjA3tPdda9sJwoHwcMROws5gwiIZfcwhHlJRwFJB2OUvGmF3evV/Q== + dependencies: + expect "^24.1.0" + jest-get-type "^22.4.3" + jest-matcher-utils "^22.0.0" + +jest-get-type@^22.4.3: + version "22.4.3" + resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-22.4.3.tgz#e3a8504d8479342dd4420236b322869f18900ce4" + integrity sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w== + +jest-get-type@^24.9.0: + version "24.9.0" + resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e" + integrity sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q== + jest-get-type@^26.3.0: version "26.3.0" resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" @@ -15447,6 +15577,25 @@ jest-leak-detector@^26.6.2: jest-get-type "^26.3.0" pretty-format "^26.6.2" +jest-matcher-utils@^22.0.0: + version "22.4.3" + resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-22.4.3.tgz#4632fe428ebc73ebc194d3c7b65d37b161f710ff" + integrity sha512-lsEHVaTnKzdAPR5t4B6OcxXo9Vy4K+kRRbG5gtddY8lBEC+Mlpvm1CJcsMESRjzUhzkz568exMV1hTB76nAKbA== + dependencies: + chalk "^2.0.1" + jest-get-type "^22.4.3" + pretty-format "^22.4.3" + +jest-matcher-utils@^24.9.0: + version "24.9.0" + resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz#f5b3661d5e628dffe6dd65251dfdae0e87c3a073" + integrity sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA== + dependencies: + chalk "^2.0.1" + jest-diff "^24.9.0" + jest-get-type "^24.9.0" + pretty-format "^24.9.0" + jest-matcher-utils@^26.6.2: version "26.6.2" resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz#8e6fd6e863c8b2d31ac6472eeb237bc595e53e7a" @@ -15457,6 +15606,20 @@ jest-matcher-utils@^26.6.2: jest-get-type "^26.3.0" pretty-format "^26.6.2" +jest-message-util@^24.9.0: + version "24.9.0" + resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-24.9.0.tgz#527f54a1e380f5e202a8d1149b0ec872f43119e3" + integrity sha512-oCj8FiZ3U0hTP4aSui87P4L4jC37BtQwUMqk+zk/b11FR19BJDeZsZAvIHutWnmtw7r85UmR3CEWZ0HWU2mAlw== + dependencies: + "@babel/code-frame" "^7.0.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/stack-utils" "^1.0.1" + chalk "^2.0.1" + micromatch "^3.1.10" + slash "^2.0.0" + stack-utils "^1.0.1" + jest-message-util@^26.6.2: version "26.6.2" resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz#58173744ad6fc0506b5d21150b9be56ef001ca07" @@ -15485,6 +15648,11 @@ jest-pnp-resolver@^1.2.2: resolved "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== +jest-regex-util@^24.9.0: + version "24.9.0" + resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.9.0.tgz#c13fb3380bde22bf6575432c493ea8fe37965636" + integrity sha512-05Cmb6CuxaA+Ys6fjr3PhvV3bGQmO+2p2La4hFbU+W5uOc479f7FdLXUWXw4pYMAhhSZIuKHwSXSu6CsSBAXQA== + jest-regex-util@^26.0.0: version "26.0.0" resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz#d25e7184b36e39fd466c3bc41be0971e821fee28" @@ -15651,6 +15819,14 @@ jest-watcher@^26.6.2: jest-util "^26.6.2" string-length "^4.0.1" +jest-when@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/jest-when/-/jest-when-3.1.0.tgz#d041191b8d4a8ad3fd9c4dc2eda3237eaeec4004" + integrity sha512-oLANZfF60yxheShbbke3+XXosZhge8Va2xiwIddF8DF5l996Ldy8bL6hT319ovSyjrhBWLOEESKfANyM0Eo/SQ== + dependencies: + bunyan "^1.8.12" + expect "^24.8.0" + jest-worker@^26.2.1: version "26.3.0" resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-26.3.0.tgz#7c8a97e4f4364b4f05ed8bca8ca0c24de091871f" @@ -17495,7 +17671,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: resolved "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= -minimatch@3.0.4, minimatch@^3.0.2, minimatch@^3.0.4: +"minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.2, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== @@ -17649,7 +17825,7 @@ modify-values@^1.0.0: resolved "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== -moment@^2.25.3, moment@^2.26.0, moment@^2.27.0: +moment@^2.19.3, moment@^2.25.3, moment@^2.26.0, moment@^2.27.0: version "2.29.1" resolved "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== @@ -17792,6 +17968,15 @@ mute-stream@0.0.8, mute-stream@~0.0.4: resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== +mv@~2: + version "2.1.1" + resolved "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz#ae6ce0d6f6d5e0a4f7d893798d03c1ea9559b6a2" + integrity sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI= + dependencies: + mkdirp "~0.5.1" + ncp "~2.0.0" + rimraf "~2.4.0" + mz@^2.5.0, mz@^2.7.0: version "2.7.0" resolved "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" @@ -17854,6 +18039,11 @@ natural-compare@^1.4.0: resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +ncp@~2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3" + integrity sha1-GVoh1sRuNh0vsSgbo4uR6d9727M= + needle@^2.2.1: version "2.4.1" resolved "https://registry.npmjs.org/needle/-/needle-2.4.1.tgz#14af48732463d7475696f937626b1b993247a56a" @@ -19980,6 +20170,24 @@ pretty-error@^2.1.1: renderkid "^2.0.1" utila "~0.4" +pretty-format@^22.4.3: + version "22.4.3" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-22.4.3.tgz#f873d780839a9c02e9664c8a082e9ee79eaac16f" + integrity sha512-S4oT9/sT6MN7/3COoOy+ZJeA92VmOnveLHgrwBE3Z1W5N9S2A1QGNYiE1z75DAENbJrXXUb+OWXhpJcg05QKQQ== + dependencies: + ansi-regex "^3.0.0" + ansi-styles "^3.2.0" + +pretty-format@^24.9.0: + version "24.9.0" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9" + integrity sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA== + dependencies: + "@jest/types" "^24.9.0" + ansi-regex "^4.0.0" + ansi-styles "^3.2.0" + react-is "^16.8.4" + pretty-format@^26.0.0, pretty-format@^26.4.2, pretty-format@^26.6.1: version "26.6.1" resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.1.tgz#af9a2f63493a856acddeeb11ba6bcf61989660a8" @@ -20640,7 +20848,7 @@ react-inspector@^5.0.1: is-dom "^1.1.0" prop-types "^15.6.1" -react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.6, react-is@^16.9.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== @@ -21775,6 +21983,13 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" +rimraf@~2.4.0: + version "2.4.5" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz#ee710ce5d93a8fdb856fb5ea8ff0e2d75934b2da" + integrity sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto= + dependencies: + glob "^6.0.1" + ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" resolved "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" @@ -21915,6 +22130,11 @@ safe-identifier@^0.4.1: resolved "https://registry.npmjs.org/safe-identifier/-/safe-identifier-0.4.1.tgz#b6516bf72594f03142b5f914f4c01842ccb1b678" integrity sha512-73tOz5TXsq3apuCc3vC8c9QRhhdNZGiBhHmPPjqpH4TO5oCDqk8UIsDcSs/RG6dYcFAkOOva0pqHS3u7hh7XXA== +safe-json-stringify@~1: + version "1.2.0" + resolved "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz#356e44bc98f1f93ce45df14bcd7c01cda86e0afd" + integrity sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg== + safe-regex@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" @@ -22718,6 +22938,13 @@ stack-trace@0.0.x: resolved "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= +stack-utils@^1.0.1: + version "1.0.4" + resolved "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.4.tgz#4b600971dcfc6aed0cbdf2a8268177cc916c87c8" + integrity sha512-IPDJfugEGbfizBwBZRZ3xpccMdRyP5lqsBWXGQWimVjua/ccLCeMOAVjlc1R7LxFjo5sEDhyNIXd8mo/AiDS9w== + dependencies: + escape-string-regexp "^2.0.0" + stack-utils@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.2.tgz#5cf48b4557becb4638d0bc4f21d23f5d19586593" From e86876b25225f421fa4b5b6f376aebae5d91f4f7 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Tue, 12 Jan 2021 19:53:28 +0200 Subject: [PATCH 027/209] Added logging in backend router --- plugins/kafka-backend/src/service/router.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/kafka-backend/src/service/router.ts b/plugins/kafka-backend/src/service/router.ts index 9c61e05bcd..621de690d3 100644 --- a/plugins/kafka-backend/src/service/router.ts +++ b/plugins/kafka-backend/src/service/router.ts @@ -36,6 +36,8 @@ export const makeRouter = ( router.get('/consumer/:consumerId/offsets', async (req, res) => { const consumerId = req.params.consumerId; + logger.debug(`Fetch consumer group ${consumerId} offsets`); + const groupOffsets = await kafkaApi.fetchGroupOffsets(consumerId); const groupWithTopicOffsets = await Promise.all( From 846cd36f63d19edef4f1fbe62b7bb6729d36b5e5 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Tue, 12 Jan 2021 19:59:44 +0200 Subject: [PATCH 028/209] removed accidently added half-finished tests --- .../ConsumerGroupOffsets.test.tsx | 39 ------------------- .../__fixtures__/consumerGroupOffsets.json | 20 ---------- 2 files changed, 59 deletions(-) delete mode 100644 plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.test.tsx delete mode 100644 plugins/kafka/src/components/ConsumerGroupOffsets/__fixtures__/consumerGroupOffsets.json diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.test.tsx b/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.test.tsx deleted file mode 100644 index 3c499f1621..0000000000 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.test.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 React from 'react'; -import { render } from '@testing-library/react'; -import { ConsumerGroupOffsets } from './ConsumerGroupOffsets'; -import * as testOffsets from './__fixtures__/consumerGroupoffsets.json'; -import { wrapInTestApp } from '@backstage/test-utils'; - -describe('ConfigMaps', () => { - it('should render consumer group offsets', async () => { - const { getByText } = render( - wrapInTestApp(), - ); - - // title - expect(getByText('dice-roller')).toBeInTheDocument(); - expect(getByText('Config Map')).toBeInTheDocument(); - - // values - expect(getByText('Immutable')).toBeInTheDocument(); - expect(getByText('false')).toBeInTheDocument(); - expect(getByText('Data')).toBeInTheDocument(); - expect(getByText('Foo: bar')).toBeInTheDocument(); // TODO wish this wasn't upper case - }); -}); diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/__fixtures__/consumerGroupOffsets.json b/plugins/kafka/src/components/ConsumerGroupOffsets/__fixtures__/consumerGroupOffsets.json deleted file mode 100644 index 4409542787..0000000000 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/__fixtures__/consumerGroupOffsets.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "topic": "topic1", - "partitionId": 1, - "groupOffset": "100", - "topicOffset": "500" - }, - { - "topic": "topic1", - "partitionId": 2, - "groupOffset": "213", - "topicOffset": "1000" - }, - { - "topic": "topic2", - "partitionId": 1, - "groupOffset": "456", - "topicOffset": "456" - } -] From 31a7ab935ff6a3e922a8ae3784fc04a5ab8445a0 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Wed, 13 Jan 2021 09:56:31 +0200 Subject: [PATCH 029/209] changed kafka consumer group annotation to plural --- plugins/kafka/src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/kafka/src/constants.ts b/plugins/kafka/src/constants.ts index c19651ef04..652911a91f 100644 --- a/plugins/kafka/src/constants.ts +++ b/plugins/kafka/src/constants.ts @@ -14,4 +14,4 @@ * limitations under the License. */ export const KAFKA_CONSUMER_GROUP_ANNOTATION = - 'kafka.apache.org/consumer-group'; + 'kafka.apache.org/consumer-groups'; From 2b06b618d3188fbe288a19c1dc5a1e4fcd70b580 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Wed, 13 Jan 2021 10:16:48 +0200 Subject: [PATCH 030/209] Updated README --- plugins/kafka/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/kafka/README.md b/plugins/kafka/README.md index 52dbd16520..7d947ae41b 100644 --- a/plugins/kafka/README.md +++ b/plugins/kafka/README.md @@ -69,7 +69,7 @@ kafka: - localhost:9092 ``` -6. Add `kafka.apache.org/consumer-group` annotation to your services: +6. Add `kafka.apache.org/consumer-groups` annotation to your services: ```yaml apiVersion: backstage.io/v1alpha1 @@ -77,7 +77,7 @@ kind: Component metadata: # ... annotations: - kafka.apache.org/consumer-group: consumer-group-name + kafka.apache.org/consumer-groups: consumer-group-name spec: type: service ``` From 2b514d53234744d5ea067175778532028793b725 Mon Sep 17 00:00:00 2001 From: Jussi Hallila Date: Wed, 13 Jan 2021 14:21:50 +0100 Subject: [PATCH 031/209] Adding test setup for import page and first form Creating tests to check that correct action is proposed when user tries to add Github repos. --- .changeset/eleven-tables-tease.md | 5 + .../components/ImportComponentForm.test.tsx | 85 ++++++++ .../components/ImportComponentPage.test.tsx | 189 ++++++++++++++++++ .../mocks/analyze-location-POST-response.json | 22 ++ .../src/mocks/locations-POST-response.json | 39 ++++ plugins/catalog-import/src/setupTests.ts | 3 + 6 files changed, 343 insertions(+) create mode 100644 .changeset/eleven-tables-tease.md create mode 100644 plugins/catalog-import/src/components/ImportComponentForm.test.tsx create mode 100644 plugins/catalog-import/src/components/ImportComponentPage.test.tsx create mode 100644 plugins/catalog-import/src/mocks/analyze-location-POST-response.json create mode 100644 plugins/catalog-import/src/mocks/locations-POST-response.json diff --git a/.changeset/eleven-tables-tease.md b/.changeset/eleven-tables-tease.md new file mode 100644 index 0000000000..177211b4db --- /dev/null +++ b/.changeset/eleven-tables-tease.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-import': patch +--- + +Modifying import functionality to register existing catalog-info.yaml if one exists in given Github repository diff --git a/plugins/catalog-import/src/components/ImportComponentForm.test.tsx b/plugins/catalog-import/src/components/ImportComponentForm.test.tsx new file mode 100644 index 0000000000..dcacfcc76f --- /dev/null +++ b/plugins/catalog-import/src/components/ImportComponentForm.test.tsx @@ -0,0 +1,85 @@ +/* + * 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 { renderInTestApp } from '@backstage/test-utils'; +import { RegisterComponentForm } from './ImportComponentForm'; +import { + ApiProvider, + ApiRegistry, + DiscoveryApi, + errorApiRef, +} from '@backstage/core'; +import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog'; +import { catalogImportApiRef, CatalogImportClient } from '../api'; +import { fireEvent, waitFor, screen } from '@testing-library/react'; + +describe('', () => { + let apis: ApiRegistry; + + const mockErrorApi: jest.Mocked = { + post: jest.fn(), + error$: jest.fn(), + }; + + beforeEach(() => { + apis = ApiRegistry.from([ + [catalogApiRef, new CatalogClient({ discoveryApi: {} as DiscoveryApi })], + [ + catalogImportApiRef, + new CatalogImportClient({ + discoveryApi: { getBaseUrl: () => Promise.resolve('base') }, + githubAuthApi: { + getAccessToken: (_, __) => Promise.resolve('token'), + }, + configApi: {} as any, + }), + ], + [errorApiRef, mockErrorApi], + ]); + }); + + async function renderSUT( + nextStep: () => void = () => {}, + saveConfig: () => void = () => {}, + ) { + return await renderInTestApp( + + + , + ); + } + + it('Renders without exploding', async () => { + await renderSUT(); + expect( + screen.getByPlaceholderText('https://github.com/backstage/backstage'), + ).toBeInTheDocument(); + }); + + it('Should have basic URL validation for input', async () => { + await renderSUT(); + await waitFor(() => { + fireEvent.input( + screen.getByPlaceholderText('https://github.com/backstage/backstage'), + { target: { value: 'not a url' } }, + ); + }); + await waitFor(() => { + fireEvent.click(screen.getByText('Next')); + }); + expect(screen.getByText('Must start with https://.')).toBeInTheDocument(); + }); +}); diff --git a/plugins/catalog-import/src/components/ImportComponentPage.test.tsx b/plugins/catalog-import/src/components/ImportComponentPage.test.tsx new file mode 100644 index 0000000000..0e03a467e5 --- /dev/null +++ b/plugins/catalog-import/src/components/ImportComponentPage.test.tsx @@ -0,0 +1,189 @@ +/* + * 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 { msw, renderInTestApp } from '@backstage/test-utils'; +import { ImportComponentPage } from './ImportComponentPage'; +import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; +import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog'; +import { catalogImportApiRef, CatalogImportClient } from '../api'; +import { fireEvent, screen, waitFor } from '@testing-library/react'; + +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; + +let codeSearchMockResponse: () => Promise<{ + data: { + total_count: number; + items: Array<{ path: string }>; + }; +}>; + +jest.mock('@backstage/integration', () => ({ + readGitHubIntegrationConfigs: () => ({ + find: () => ({ + host: 'test.localhost', + owner: 'someuser', + }), + }), +})); + +jest.mock('@octokit/rest', () => ({ + Octokit: jest.fn().mockImplementation(() => { + return { + repos: { + get: () => + Promise.resolve({ + data: { + default_branch: 'main', + }, + }), + }, + search: { + code: codeSearchMockResponse, + }, + }; + }), +})); + +describe('', () => { + const server = setupServer(); + msw.setupDefaultHandlers(server); + + beforeEach(() => { + server.use( + rest.post('https://backend.localhost/locations', (_, res, ctx) => { + return res( + ctx.status(201), + ctx.json(require('../mocks/locations-POST-response.json')), + ); + }), + rest.post('https://backend.localhost/analyze-location', (_, res, ctx) => { + return res( + ctx.json(require('../mocks/analyze-location-POST-response.json')), + ); + }), + ); + }); + beforeAll(() => server.listen()); + afterEach(() => server.resetHandlers()); + afterAll(() => server.close()); + + let apis: ApiRegistry; + + const mockErrorApi: jest.Mocked = { + post: jest.fn(), + error$: jest.fn(), + }; + + beforeEach(() => { + const getBaseUrl = () => Promise.resolve('https://backend.localhost'); + apis = ApiRegistry.from([ + [ + catalogApiRef, + new CatalogClient({ + discoveryApi: { getBaseUrl }, + }), + ], + [ + catalogImportApiRef, + new CatalogImportClient({ + discoveryApi: { getBaseUrl }, + githubAuthApi: { + getAccessToken: (_, __) => Promise.resolve('token'), + }, + configApi: {} as any, + }), + ], + [errorApiRef, mockErrorApi], + ]); + }); + + async function renderSUT() { + return await renderInTestApp( + + + , + ); + } + + it('Should use found yaml file directly and not create a pull request if GitHub api returns one', async () => { + codeSearchMockResponse = () => + Promise.resolve({ + data: { + total_count: 3, + items: [ + { path: 'simple/path/catalog-info.yaml' }, + { path: 'co/mple/x/path/catalog-info.yaml' }, + { path: 'catalog-info.yaml' }, + ], + }, + }); + await renderSUT(); + await waitFor(() => { + fireEvent.input( + screen.getByPlaceholderText('https://github.com/backstage/backstage'), + { target: { value: 'https://test.localhost/someuser/somerepo' } }, + ); + }); + + fireEvent.click(screen.getByText('Next')); + await waitFor(() => { + expect( + screen.getByText( + 'https://test.localhost/someusername/somerepo/blob/master/src/catalog-info.yaml', + ), + ).toBeInTheDocument(); + + const pullReqText = screen.queryByText('pull request'); + expect(pullReqText).not.toBeInTheDocument(); + }); + }); + + it('Should indicate a pull request creation when no yaml file found in the repo', async () => { + codeSearchMockResponse = () => + Promise.resolve({ + data: { + total_count: 0, + items: [], + }, + }); + const { container } = await renderSUT(); + await waitFor(() => { + fireEvent.input( + screen.getByPlaceholderText('https://github.com/backstage/backstage'), + { target: { value: 'https://test.localhost/someuser/somerepo' } }, + ); + }); + + fireEvent.click(screen.getByText('Next')); + await waitFor(() => { + expect( + screen.getByText('https://test.localhost/someuser/somerepo'), + ).toBeInTheDocument(); + }); + const textNode = container + .querySelector('a[href="https://test.localhost/someuser/somerepo"]') + ?.closest('p'); + expect(textNode?.innerHTML).toContain( + 'Following config object will be submitted in a pull request to the repository', + ); + expect( + screen.queryByText( + 'https://test.localhost/someusername/somerepo/blob/master/src/catalog-info.yaml', + ), + ).not.toBeInTheDocument(); + }); +}); diff --git a/plugins/catalog-import/src/mocks/analyze-location-POST-response.json b/plugins/catalog-import/src/mocks/analyze-location-POST-response.json new file mode 100644 index 0000000000..eaadf1054a --- /dev/null +++ b/plugins/catalog-import/src/mocks/analyze-location-POST-response.json @@ -0,0 +1,22 @@ +{ + "existingEntityFiles": [], + "generateEntities": [ + { + "entity": { + "apiVersion": "backstage.io/v1alpha1", + "kind": "Component", + "metadata": { + "name": "somerepo", + "annotations": { + "github.com/project-slug": "someuser/somerepo" + } + }, + "spec": { + "type": "other", + "lifecycle": "unknown" + } + }, + "fields": [] + } + ] +} diff --git a/plugins/catalog-import/src/mocks/locations-POST-response.json b/plugins/catalog-import/src/mocks/locations-POST-response.json new file mode 100644 index 0000000000..20e4e1582c --- /dev/null +++ b/plugins/catalog-import/src/mocks/locations-POST-response.json @@ -0,0 +1,39 @@ +{ + "location": { + "id": "d4a64359-a709-4c91-a9de-0905a033bf22", + "type": "url", + "target": "https://test.localhost/someusername/somerepo/blob/master/src/catalog-info.yaml" + }, + "entities": [ + { + "metadata": { + "namespace": "default", + "annotations": { + "backstage.io/managed-by-location": "url:https://test.localhost/someusername/somerepo/blob/master/src/catalog-info.yaml", + "github.com/project-slug": "someusername/somerepo" + }, + "name": "somerepo", + "uid": "e992d5ee-7c70-4316-90cf-325f1a0a5146", + "etag": "YWE2M2Q5MzgtNjdkNi00N2QwLWJkZjYtNDM0MTMzMDI4Y2I0", + "generation": 1 + }, + "apiVersion": "backstage.io/v1alpha1", + "kind": "Component", + "spec": { + "type": "other", + "lifecycle": "unknown", + "owner": "unknown" + }, + "relations": [ + { + "target": { + "kind": "group", + "namespace": "default", + "name": "unknown" + }, + "type": "ownedBy" + } + ] + } + ] +} diff --git a/plugins/catalog-import/src/setupTests.ts b/plugins/catalog-import/src/setupTests.ts index 825bcd4115..fba7d7a957 100644 --- a/plugins/catalog-import/src/setupTests.ts +++ b/plugins/catalog-import/src/setupTests.ts @@ -15,3 +15,6 @@ */ import '@testing-library/jest-dom'; +import fetch from 'cross-fetch'; + +global.fetch = fetch; From 5805251b73c45138f9caf63abc016408705293c7 Mon Sep 17 00:00:00 2001 From: Jussi Hallila Date: Wed, 13 Jan 2021 15:50:00 +0100 Subject: [PATCH 032/209] Fix tests after changes from master. --- .../components/ImportComponentForm.test.tsx | 6 +++++- .../components/ImportComponentPage.test.tsx | 19 ++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-import/src/components/ImportComponentForm.test.tsx b/plugins/catalog-import/src/components/ImportComponentForm.test.tsx index dcacfcc76f..91a5208bee 100644 --- a/plugins/catalog-import/src/components/ImportComponentForm.test.tsx +++ b/plugins/catalog-import/src/components/ImportComponentForm.test.tsx @@ -57,7 +57,11 @@ describe('', () => { ) { return await renderInTestApp( - + , ); } diff --git a/plugins/catalog-import/src/components/ImportComponentPage.test.tsx b/plugins/catalog-import/src/components/ImportComponentPage.test.tsx index 0e03a467e5..3f6f878b29 100644 --- a/plugins/catalog-import/src/components/ImportComponentPage.test.tsx +++ b/plugins/catalog-import/src/components/ImportComponentPage.test.tsx @@ -16,9 +16,15 @@ import React from 'react'; import { msw, renderInTestApp } from '@backstage/test-utils'; import { ImportComponentPage } from './ImportComponentPage'; -import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; +import { + ApiProvider, + ApiRegistry, + configApiRef, + errorApiRef, +} from '@backstage/core'; import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog'; import { catalogImportApiRef, CatalogImportClient } from '../api'; + import { fireEvent, screen, waitFor } from '@testing-library/react'; import { rest } from 'msw'; @@ -107,6 +113,17 @@ describe('', () => { configApi: {} as any, }), ], + [ + configApiRef, + { + getOptional: () => 'Title', + getOptionalConfigArray: () => [], + has: () => true, + getConfig: () => ({ + has: () => true, + }), + }, + ], [errorApiRef, mockErrorApi], ]); }); From f5bd375bef2c4b76af015553d5564a11daea500a Mon Sep 17 00:00:00 2001 From: Jussi Hallila Date: Wed, 13 Jan 2021 16:04:39 +0100 Subject: [PATCH 033/209] Remove unneeded empty string. --- plugins/catalog-import/src/components/ImportComponentPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/catalog-import/src/components/ImportComponentPage.tsx b/plugins/catalog-import/src/components/ImportComponentPage.tsx index 60766ebaf2..e284e95456 100644 --- a/plugins/catalog-import/src/components/ImportComponentPage.tsx +++ b/plugins/catalog-import/src/components/ImportComponentPage.tsx @@ -107,7 +107,6 @@ export const ImportComponentPage = ({ {manifestGenerationAvailable(configApi) && ( - {' '} GitHub Repo If you already have code in a GitHub repository without From 8fcfd7b31668857e310afa761987a1c323044902 Mon Sep 17 00:00:00 2001 From: Jussi Hallila Date: Wed, 13 Jan 2021 16:36:50 +0100 Subject: [PATCH 034/209] Add logic to not contact GitHub on non-GitHub urls Wrap in tests. --- .../components/ImportComponentPage.test.tsx | 65 ++++++++++++++++--- .../src/mocks/locations-POST-response.json | 4 +- .../catalog-import/src/util/useGithubRepos.ts | 8 ++- 3 files changed, 66 insertions(+), 11 deletions(-) diff --git a/plugins/catalog-import/src/components/ImportComponentPage.test.tsx b/plugins/catalog-import/src/components/ImportComponentPage.test.tsx index 3f6f878b29..5c4b2e8538 100644 --- a/plugins/catalog-import/src/components/ImportComponentPage.test.tsx +++ b/plugins/catalog-import/src/components/ImportComponentPage.test.tsx @@ -64,6 +64,7 @@ jest.mock('@octokit/rest', () => ({ }), })); +const OUR_GITHUB_TEST_REPO = 'https://github.com/someuser/somerepo'; describe('', () => { const server = setupServer(); msw.setupDefaultHandlers(server); @@ -136,6 +137,56 @@ describe('', () => { ); } + it('Should not explode on non-Github URLs', async () => { + await renderSUT(); + await waitFor(() => { + fireEvent.input( + screen.getByPlaceholderText('https://github.com/backstage/backstage'), + { + target: { + value: 'https://test-git-provider.localhost/someuser/somerepo', + }, + }, + ); + }); + + fireEvent.click(screen.getByText('Next')); + await waitFor(() => { + const firstStepInput = screen.queryByPlaceholderText( + 'https://github.com/backstage/backstage', + ); + expect(firstStepInput).not.toBeInTheDocument(); + }); + }); + + it('Should offer direct file import from non-Github URLs', async () => { + await renderSUT(); + await waitFor(() => { + fireEvent.input( + screen.getByPlaceholderText('https://github.com/backstage/backstage'), + { + target: { + value: + 'https://test-git-provider.localhost/someuser/somerepo/catalog-info.yaml', + }, + }, + ); + }); + + fireEvent.click(screen.getByText('Next')); + await waitFor(() => { + const firstStepInput = screen.queryByPlaceholderText( + 'https://github.com/backstage/backstage', + ); + expect(firstStepInput).not.toBeInTheDocument(); + }); + expect( + screen.getByText( + 'https://test-git-provider.localhost/someuser/somerepo/catalog-info.yaml', + ), + ).toBeInTheDocument(); + }); + it('Should use found yaml file directly and not create a pull request if GitHub api returns one', async () => { codeSearchMockResponse = () => Promise.resolve({ @@ -152,7 +203,7 @@ describe('', () => { await waitFor(() => { fireEvent.input( screen.getByPlaceholderText('https://github.com/backstage/backstage'), - { target: { value: 'https://test.localhost/someuser/somerepo' } }, + { target: { value: OUR_GITHUB_TEST_REPO } }, ); }); @@ -160,7 +211,7 @@ describe('', () => { await waitFor(() => { expect( screen.getByText( - 'https://test.localhost/someusername/somerepo/blob/master/src/catalog-info.yaml', + 'https://github.com/someusername/somerepo/blob/master/src/catalog-info.yaml', ), ).toBeInTheDocument(); @@ -181,25 +232,23 @@ describe('', () => { await waitFor(() => { fireEvent.input( screen.getByPlaceholderText('https://github.com/backstage/backstage'), - { target: { value: 'https://test.localhost/someuser/somerepo' } }, + { target: { value: OUR_GITHUB_TEST_REPO } }, ); }); fireEvent.click(screen.getByText('Next')); await waitFor(() => { - expect( - screen.getByText('https://test.localhost/someuser/somerepo'), - ).toBeInTheDocument(); + expect(screen.getByText(OUR_GITHUB_TEST_REPO)).toBeInTheDocument(); }); const textNode = container - .querySelector('a[href="https://test.localhost/someuser/somerepo"]') + .querySelector('a[href="https://github.com/someuser/somerepo"]') ?.closest('p'); expect(textNode?.innerHTML).toContain( 'Following config object will be submitted in a pull request to the repository', ); expect( screen.queryByText( - 'https://test.localhost/someusername/somerepo/blob/master/src/catalog-info.yaml', + 'https://github.com/someusername/somerepo/blob/master/src/catalog-info.yaml', ), ).not.toBeInTheDocument(); }); diff --git a/plugins/catalog-import/src/mocks/locations-POST-response.json b/plugins/catalog-import/src/mocks/locations-POST-response.json index 20e4e1582c..c44a0f4d61 100644 --- a/plugins/catalog-import/src/mocks/locations-POST-response.json +++ b/plugins/catalog-import/src/mocks/locations-POST-response.json @@ -2,14 +2,14 @@ "location": { "id": "d4a64359-a709-4c91-a9de-0905a033bf22", "type": "url", - "target": "https://test.localhost/someusername/somerepo/blob/master/src/catalog-info.yaml" + "target": "https://github.com/someusername/somerepo/blob/master/src/catalog-info.yaml" }, "entities": [ { "metadata": { "namespace": "default", "annotations": { - "backstage.io/managed-by-location": "url:https://test.localhost/someusername/somerepo/blob/master/src/catalog-info.yaml", + "backstage.io/managed-by-location": "url:https://github.com/someusername/somerepo/blob/master/src/catalog-info.yaml", "github.com/project-slug": "someusername/somerepo" }, "name": "somerepo", diff --git a/plugins/catalog-import/src/util/useGithubRepos.ts b/plugins/catalog-import/src/util/useGithubRepos.ts index 56cf101c35..319457dfe8 100644 --- a/plugins/catalog-import/src/util/useGithubRepos.ts +++ b/plugins/catalog-import/src/util/useGithubRepos.ts @@ -81,7 +81,13 @@ export function useGithubRepos() { return submitPRResponse; }; - const checkForExistingCatalogInfo = async (location: string) => { + const checkForExistingCatalogInfo = async ( + location: string, + ): Promise<{ exists: boolean; url?: string }> => { + const { source } = parseGitUri(location); + if (source !== 'github.com') { + return Promise.resolve({ exists: false }); + } const { repoName, ownerName, From f8dd991767262247d60992e4445ce18b1cbfc6d1 Mon Sep 17 00:00:00 2001 From: Jussi Hallila Date: Wed, 13 Jan 2021 16:59:00 +0100 Subject: [PATCH 035/209] Stylize the spelling of GitHub --- .changeset/eleven-tables-tease.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/eleven-tables-tease.md b/.changeset/eleven-tables-tease.md index 177211b4db..3f95fbb9d2 100644 --- a/.changeset/eleven-tables-tease.md +++ b/.changeset/eleven-tables-tease.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog-import': patch --- -Modifying import functionality to register existing catalog-info.yaml if one exists in given Github repository +Modifying import functionality to register existing catalog-info.yaml if one exists in given GitHub repository From 5224a49a26849ce71a14b3def3e54012ff677eba Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 14 Jan 2021 13:17:38 +0100 Subject: [PATCH 036/209] chore: parse the URL to get the resource in the gitlab preparer for now --- .../src/scaffolder/stages/publish/gitlab.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts index cda9a83019..c592b06807 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts @@ -20,6 +20,8 @@ import { Config, JsonValue } from '@backstage/config'; import { Logger } from 'winston'; import { initRepoAndPush } from './helpers'; import { RequiredTemplateValues } from '../templater'; +import gitUrlParse from 'git-url-parse'; + import { GitLabIntegrationConfig, readGitLabIntegrationConfigs, @@ -116,7 +118,9 @@ export class GitlabPublisher implements PublisherBase { pathElements.pop(); const owner = pathElements.join('/'); - const config = this.getConfig(); + const { resource: host } = gitUrlParse(values.storePath); + + const config = this.getConfig(host); if (!config.token) { throw new Error( From 1f26905024852f9821a418349a821615fbbb3260 Mon Sep 17 00:00:00 2001 From: Ihor Dvoretskyi Date: Thu, 14 Jan 2021 15:17:43 +0200 Subject: [PATCH 037/209] FOSSA scan enabled Signed-off-by: Ihor Dvoretskyi --- .github/workflows/fossa.yml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/fossa.yml diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml new file mode 100644 index 0000000000..c018bc2deb --- /dev/null +++ b/.github/workflows/fossa.yml @@ -0,0 +1,34 @@ +name: FOSSA +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [10.x, 12.x, 14.x, 15.x] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm ci + - run: npm run build --if-present + - run: npm test + # Runs a set of commands to initialize and analyze with FOSSA + - name: run FOSSA analysis + env: + # FOSSA Push-Only API Token + FOSSA_API_KEY: '9ee7e8893660832a7387dcc32377fb61' + run: | + curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | bash + fossa init + fossa analyze From 4dc5cf516c605438c1a025256a8f717ee5d9cef4 Mon Sep 17 00:00:00 2001 From: Ihor Dvoretskyi Date: Thu, 14 Jan 2021 15:53:05 +0200 Subject: [PATCH 038/209] Formatting fixes Signed-off-by: Ihor Dvoretskyi --- .github/workflows/fossa.yml | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml index c018bc2deb..ae01683b3c 100644 --- a/.github/workflows/fossa.yml +++ b/.github/workflows/fossa.yml @@ -1,13 +1,12 @@ name: FOSSA on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] jobs: build: - runs-on: ubuntu-latest strategy: @@ -15,19 +14,19 @@ jobs: node-version: [10.x, 12.x, 14.x, 15.x] steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - run: npm ci - - run: npm run build --if-present - - run: npm test - # Runs a set of commands to initialize and analyze with FOSSA + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm ci + - run: npm run build --if-present + - run: npm test + # Runs a set of commands to initialize and analyze with FOSSA - name: run FOSSA analysis env: - # FOSSA Push-Only API Token - FOSSA_API_KEY: '9ee7e8893660832a7387dcc32377fb61' + # FOSSA Push-Only API Token + FOSSA_API_KEY: '9ee7e8893660832a7387dcc32377fb61' run: | curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | bash fossa init From 56ba877a7b1cc51d892555c31e63d76f2c6f9943 Mon Sep 17 00:00:00 2001 From: Ihor Dvoretskyi Date: Thu, 14 Jan 2021 16:33:01 +0200 Subject: [PATCH 039/209] Formatting fixes Signed-off-by: Ihor Dvoretskyi --- .github/workflows/fossa.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml index ae01683b3c..cd17eb2e49 100644 --- a/.github/workflows/fossa.yml +++ b/.github/workflows/fossa.yml @@ -25,8 +25,8 @@ jobs: # Runs a set of commands to initialize and analyze with FOSSA - name: run FOSSA analysis env: - # FOSSA Push-Only API Token - FOSSA_API_KEY: '9ee7e8893660832a7387dcc32377fb61' + # FOSSA Push-Only API Token + FOSSA_API_KEY: '9ee7e8893660832a7387dcc32377fb61' run: | curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | bash fossa init From 4d3ed6bef514969d2f24f4bb8da9283b298424da Mon Sep 17 00:00:00 2001 From: Ihor Dvoretskyi Date: Thu, 14 Jan 2021 16:37:20 +0200 Subject: [PATCH 040/209] fossa.yml uses yarn instead of npm Signed-off-by: Ihor Dvoretskyi --- .github/workflows/fossa.yml | 38 +++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml index cd17eb2e49..854c52832c 100644 --- a/.github/workflows/fossa.yml +++ b/.github/workflows/fossa.yml @@ -13,21 +13,23 @@ jobs: matrix: node-version: [10.x, 12.x, 14.x, 15.x] - steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - run: npm ci - - run: npm run build --if-present - - run: npm test - # Runs a set of commands to initialize and analyze with FOSSA - - name: run FOSSA analysis - env: - # FOSSA Push-Only API Token - FOSSA_API_KEY: '9ee7e8893660832a7387dcc32377fb61' - run: | - curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | bash - fossa init - fossa analyze +steps: + - uses: actions/checkout@v2 + - uses: borales/actions-yarn@v2.0.0 + with: + cmd: install + - uses: borales/actions-yarn@v2.0.0 + with: + cmd: build + - uses: borales/actions-yarn@v2.0.0 + with: + cmd: test + # Runs a set of commands to initialize and analyze with FOSSA + - name: run FOSSA analysis + env: + # FOSSA Push-Only API Token + FOSSA_API_KEY: '9ee7e8893660832a7387dcc32377fb61' + run: | + curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | bash + fossa init + fossa analyze From 2249b22e3ee965796279376ca562ff45b12f73c5 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 14 Jan 2021 18:51:03 +0100 Subject: [PATCH 041/209] chore: added a validate function for storepath --- plugins/scaffolder/package.json | 1 + .../components/TemplatePage/TemplatePage.tsx | 13 +++++++++++ yarn.lock | 23 +++++++++++-------- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 0d6c1bdb37..658e176c6c 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -40,6 +40,7 @@ "@rjsf/core": "^2.4.0", "@rjsf/material-ui": "^2.4.0", "classnames": "^2.2.6", + "git-url-parse": "^11.4.3", "moment": "^2.26.0", "react": "^16.13.1", "react-dom": "^16.13.1", diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index aceaf673e2..f703c9dea6 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -39,6 +39,7 @@ import { rootRoute } from '../../routes'; import { JobStatusModal } from '../JobStatusModal'; import { MultistepJsonForm } from '../MultistepJsonForm'; import { useJobPolling } from '../hooks/useJobPolling'; +import gitParse from 'git-url-parse'; const useTemplate = ( templateName: string, @@ -183,6 +184,18 @@ export const TemplatePage = () => { label: 'Choose owner and repo', schema: OWNER_REPO_SCHEMA, customFormats: REPO_FORMAT, + validate: (formData, errors) => { + const { storePath } = formData; + const parsedUrl = gitParse(storePath); + + if (!parsedUrl.resource) { + errors.storePath.addError( + 'There needs to be a hostname in the storePath', + ); + } + + return errors; + }, }, ]} /> diff --git a/yarn.lock b/yarn.lock index 557a8e60e5..3f37440667 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1644,9 +1644,11 @@ to-fast-properties "^2.0.0" "@backstage/catalog-model@^0.2.0": - version "0.6.0" + version "0.2.0" + resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.2.0.tgz#e3fe2a4ddeb6a9b6ec480c80cb2b9c39cb245576" + integrity sha512-Y1ocdRpBlxK/VrJQjHlQd0bgADECd1B2NRjwd8ss46ibT5hwLvMOfD80+Fa7oPLu0ktJrH4lq0pNIIJIml48zA== dependencies: - "@backstage/config" "^0.1.2" + "@backstage/config" "^0.1.1" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" json-schema "^0.2.5" @@ -1655,9 +1657,11 @@ yup "^0.29.3" "@backstage/catalog-model@^0.3.0": - version "0.6.0" + version "0.3.1" + resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.3.1.tgz#45d08e2f333c9c566b2bf2629fd707fe989bb404" + integrity sha512-9XhV7c4rmVW+Yzj2PiwTQ7DsegWGB3C4ELsDRExuEVZONdqNcC02cyJtrt3fT5F31ZS3tHkB9bMUymFOBLqUSA== dependencies: - "@backstage/config" "^0.1.2" + "@backstage/config" "^0.1.1" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" json-schema "^0.2.5" @@ -1666,16 +1670,17 @@ yup "^0.29.3" "@backstage/core@^0.3.0": - version "0.4.3" + version "0.3.2" + resolved "https://registry.npmjs.org/@backstage/core/-/core-0.3.2.tgz#a8209126d5076cf4a8b9bd632fe4e5e2edb62916" + integrity sha512-i5d+Wh8js4qEWoAsPY5L7HVSWpumr1OhfF2dUCGYdyW6AMqVJPca6+n6zp1Rg2CO+J9norp44XAVVCbyhtUpig== dependencies: - "@backstage/config" "^0.1.2" - "@backstage/core-api" "^0.2.8" - "@backstage/theme" "^0.2.2" + "@backstage/config" "^0.1.1" + "@backstage/core-api" "^0.2.1" + "@backstage/theme" "^0.2.1" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" "@material-ui/lab" "4.0.0-alpha.45" "@types/dagre" "^0.7.44" - "@types/prop-types" "^15.7.3" "@types/react" "^16.9" "@types/react-sparklines" "^1.7.0" classnames "^2.2.6" From 548dbf154ee42f9dc7d29b985c2067ef11c7cc6c Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 15 Jan 2021 14:34:21 +0100 Subject: [PATCH 042/209] Fix prepare, e2e tested with GitHub --- .../src/scaffolder/stages/prepare/azure.ts | 15 +- .../scaffolder/stages/prepare/bitbucket.ts | 15 +- .../src/scaffolder/stages/prepare/github.ts | 15 +- .../src/scaffolder/stages/prepare/gitlab.ts | 15 +- .../src/scaffolder/stages/prepare/types.ts | 2 + .../src/scaffolder/stages/publish/github.ts | 130 +++++++++++++----- .../scaffolder/stages/publish/publishers.ts | 29 +--- .../src/scaffolder/stages/publish/types.ts | 1 + .../components/TemplatePage/TemplatePage.tsx | 2 +- 9 files changed, 131 insertions(+), 93 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts index bb2257dfa7..b2eab7bb85 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts @@ -31,16 +31,14 @@ import { export class AzurePreparer implements PreparerBase { private readonly integrations: AzureIntegrationConfig[]; private readonly scaffolderToken: string | undefined; - private readonly logger: Logger; constructor(config: Config, { logger }: { logger: Logger }) { - this.logger = logger; this.integrations = readAzureIntegrationConfigs( config.getOptionalConfigArray('integrations.azure') ?? [], ); if (!this.integrations.length) { - this.logger.warn( + logger.warn( 'Integrations for Azure in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', ); } @@ -50,7 +48,7 @@ export class AzurePreparer implements PreparerBase { ); if (this.scaffolderToken) { - this.logger.warn( + logger.warn( "DEPRECATION: Using the token format under 'scaffolder.azure.api.token' will not be respected in future releases. Please consider using integrations config instead", ); } @@ -58,10 +56,11 @@ export class AzurePreparer implements PreparerBase { async prepare( template: TemplateEntityV1alpha1, - opts?: PreparerOptions, + opts: PreparerOptions, ): Promise { const { protocol, location } = parseLocationAnnotation(template); - const workingDirectory = opts?.workingDirectory ?? os.tmpdir(); + const workingDirectory = opts.workingDirectory ?? os.tmpdir(); + const logger = opts.logger; if (!['azure/api', 'url'].includes(protocol)) { throw new InputError( @@ -89,9 +88,9 @@ export class AzurePreparer implements PreparerBase { ? Git.fromAuth({ password: token, username: 'notempty', - logger: this.logger, + logger, }) - : Git.fromAuth({ logger: this.logger }); + : Git.fromAuth({ logger }); await git.clone({ url: repositoryCheckoutUrl, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts index 8a9e4f452d..9dcc61d6fe 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts @@ -31,16 +31,14 @@ import { Logger } from 'winston'; export class BitbucketPreparer implements PreparerBase { private readonly privateToken: string; private readonly username: string; - private readonly logger: Logger; private readonly integrations: BitbucketIntegrationConfig[]; constructor(config: Config, { logger }: { logger: Logger }) { - this.logger = logger; this.integrations = readBitbucketIntegrationConfigs( config.getOptionalConfigArray('integrations.bitbucket') ?? [], ); if (!this.integrations.length) { - this.logger.warn( + logger.warn( 'Integrations for BitBucket in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', ); } @@ -51,7 +49,7 @@ export class BitbucketPreparer implements PreparerBase { config.getOptionalString('scaffolder.bitbucket.api.token') ?? ''; if (this.username || this.privateToken) { - this.logger.warn( + logger.warn( "DEPRECATION: Using the token format under 'scaffolder.bitbucket.token' will not be respected in future releases. Please consider using integrations config instead", ); } @@ -59,10 +57,11 @@ export class BitbucketPreparer implements PreparerBase { async prepare( template: TemplateEntityV1alpha1, - opts?: PreparerOptions, + opts: PreparerOptions, ): Promise { const { protocol, location } = parseLocationAnnotation(template); - const workingDirectory = opts?.workingDirectory ?? os.tmpdir(); + const workingDirectory = opts.workingDirectory ?? os.tmpdir(); + const logger = opts.logger; if (!['bitbucket', 'url'].includes(protocol)) { throw new InputError( @@ -90,9 +89,9 @@ export class BitbucketPreparer implements PreparerBase { const git = auth ? Git.fromAuth({ ...auth, - logger: this.logger, + logger, }) - : Git.fromAuth({ logger: this.logger }); + : Git.fromAuth({ logger }); await git.clone({ url: repositoryCheckoutUrl, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts index 46080c85a7..f7b0fd801f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts @@ -31,16 +31,14 @@ import { export class GithubPreparer implements PreparerBase { private readonly integrations: GitHubIntegrationConfig[]; private readonly scaffolderToken: string | undefined; - private readonly logger: Logger; constructor(config: Config, { logger }: { logger: Logger }) { - this.logger = logger; this.integrations = readGitHubIntegrationConfigs( config.getOptionalConfigArray('integrations.github') ?? [], ); if (!this.integrations.length) { - this.logger.warn( + logger.warn( 'Integrations for Github in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', ); } @@ -48,7 +46,7 @@ export class GithubPreparer implements PreparerBase { this.scaffolderToken = config.getOptionalString('scaffolder.github.token'); if (this.scaffolderToken) { - this.logger.warn( + logger.warn( "DEPRECATION: Using the token format under 'scaffolder.github.token' will not be respected in future releases. Please consider using integrations config instead", ); } @@ -56,10 +54,11 @@ export class GithubPreparer implements PreparerBase { async prepare( template: TemplateEntityV1alpha1, - opts?: PreparerOptions, + opts: PreparerOptions, ): Promise { const { protocol, location } = parseLocationAnnotation(template); - const workingDirectory = opts?.workingDirectory ?? os.tmpdir(); + const workingDirectory = opts.workingDirectory ?? os.tmpdir(); + const logger = opts.logger; if (!['github', 'url'].includes(protocol)) { throw new InputError( @@ -87,9 +86,9 @@ export class GithubPreparer implements PreparerBase { ? Git.fromAuth({ username: token, password: 'x-oauth-basic', - logger: this.logger, + logger, }) - : Git.fromAuth({ logger: this.logger }); + : Git.fromAuth({ logger }); await git.clone({ url: repositoryCheckoutUrl, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts index d0ce4ff3f7..caf45db37f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts @@ -31,16 +31,14 @@ import { Logger } from 'winston'; export class GitlabPreparer implements PreparerBase { private readonly integrations: GitLabIntegrationConfig[]; private readonly scaffolderToken: string | undefined; - private readonly logger: Logger; constructor(config: Config, { logger }: { logger: Logger }) { - this.logger = logger; this.integrations = readGitLabIntegrationConfigs( config.getOptionalConfigArray('integrations.gitlab') ?? [], ); if (!this.integrations.length) { - this.logger.warn( + logger.warn( 'Integrations for GitLab in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', ); } @@ -50,7 +48,7 @@ export class GitlabPreparer implements PreparerBase { ); if (this.scaffolderToken) { - this.logger.warn( + logger.warn( "DEPRECATION: Using the token format under 'scaffolder.gitlab.api.token' will not be respected in future releases. Please consider using integrations config instead", ); } @@ -58,10 +56,11 @@ export class GitlabPreparer implements PreparerBase { async prepare( template: TemplateEntityV1alpha1, - opts?: PreparerOptions, + opts: PreparerOptions, ): Promise { const { protocol, location } = parseLocationAnnotation(template); - const workingDirectory = opts?.workingDirectory ?? os.tmpdir(); + const logger = opts.logger; + const workingDirectory = opts.workingDirectory ?? os.tmpdir(); if (!['gitlab', 'gitlab/api', 'url'].includes(protocol)) { throw new InputError( @@ -86,9 +85,9 @@ export class GitlabPreparer implements PreparerBase { ? Git.fromAuth({ password: token, username: 'oauth2', - logger: this.logger, + logger, }) - : Git.fromAuth({ logger: this.logger }); + : Git.fromAuth({ logger }); await git.clone({ url: repositoryCheckoutUrl, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts index 34e0fe3696..2974b833a2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts @@ -15,9 +15,11 @@ */ import type { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { RemoteProtocol } from '../types'; +import { Logger } from 'winston'; export type PreparerOptions = { workingDirectory?: string; + logger: Logger; }; export interface PreparerBase { diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts index 39a588882f..902dc72a77 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts @@ -15,32 +15,55 @@ */ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; -import { Octokit } from '@octokit/rest'; import { initRepoAndPush } from './helpers'; -import { JsonValue } from '@backstage/config'; -import { RequiredTemplateValues } from '../templater'; +import { Config } from '@backstage/config'; +import { Logger } from 'winston'; +import { + GitHubIntegrationConfig, + readGitHubIntegrationConfigs, +} from '@backstage/integration'; +import gitUrlParse from 'git-url-parse'; +import { Octokit } from '@octokit/rest'; export type RepoVisibilityOptions = 'private' | 'internal' | 'public'; - -interface GithubPublisherParams { - client: Octokit; - token: string; - repoVisibility: RepoVisibilityOptions; -} - export class GithubPublisher implements PublisherBase { - private client: Octokit; - private token: string; - private repoVisibility: RepoVisibilityOptions; + private scaffolderToken: string | undefined; + private readonly integrations: GitHubIntegrationConfig[]; + private readonly apiBaseUrl: string | undefined; + private readonly repoVisibility: RepoVisibilityOptions; - constructor({ - client, - token, - repoVisibility = 'public', - }: GithubPublisherParams) { - this.client = client; - this.token = token; - this.repoVisibility = repoVisibility; + constructor(config: Config, { logger }: { logger: Logger }) { + this.integrations = readGitHubIntegrationConfigs( + config.getOptionalConfigArray('integrations.github') ?? [], + ); + + if (!this.integrations.length) { + logger.warn( + 'Integrations for GitHub in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', + ); + } + + this.scaffolderToken = config.getOptionalString( + 'scaffolder.github.api.token', + ); + + this.apiBaseUrl = config.getOptionalString('scaffolder.github.api.baseUrl'); + + if (this.scaffolderToken) { + logger.warn( + "DEPRECATION: Using the token format under 'scaffolder.github.api.token' will not be respected in future releases. Please consider using integrations config instead", + ); + } + + if (this.apiBaseUrl) { + logger.warn( + "DEPRECATION: Using the apiBaseUrl format under 'scaffolder.github.api.baseUrl' will not be respected in future releases. Please consider using integrations config instead", + ); + } + + this.repoVisibility = (config.getOptionalString( + 'scaffolder.github.visibility', + ) ?? 'public') as RepoVisibilityOptions; } async publish({ @@ -48,13 +71,29 @@ export class GithubPublisher implements PublisherBase { directory, logger, }: PublisherOptions): Promise { - const remoteUrl = await this.createRemote(values); + const { resource: host, owner, name } = gitUrlParse(values.storePath); + const token = this.getToken(host); + + if (!token) { + throw new Error('No token provided to create the remote repository'); + } + + const description = values.description as string; + const access = values.access as string; + const remoteUrl = await this.createRemote({ + description, + access, + host, + name, + owner, + token, + }); await initRepoAndPush({ dir: directory, remoteUrl, auth: { - username: this.token, + username: token ?? '', password: 'x-oauth-basic', }, logger, @@ -68,24 +107,34 @@ export class GithubPublisher implements PublisherBase { return { remoteUrl, catalogInfoUrl }; } - private async createRemote( - values: RequiredTemplateValues & Record, - ) { - const [owner, name] = values.storePath.split('/'); - const description = values.description as string; + private async createRemote(opts: { + access: string; + name: string; + owner: string; + host: string; + token: string; + description: string; + }) { + const { access, description, host, owner, name, token } = opts; - const user = await this.client.users.getByUsername({ username: owner }); + // create a github client with the config + const githubClient = new Octokit({ + auth: token, + baseUrl: this.getBaseUrl(host), + }); + + const user = await githubClient.users.getByUsername({ username: owner }); const repoCreationPromise = user.data.type === 'Organization' - ? this.client.repos.createInOrg({ + ? githubClient.repos.createInOrg({ name, org: owner, private: this.repoVisibility !== 'public', visibility: this.repoVisibility, description, }) - : this.client.repos.createForAuthenticatedUser({ + : githubClient.repos.createForAuthenticatedUser({ name, private: this.repoVisibility === 'private', description, @@ -93,10 +142,9 @@ export class GithubPublisher implements PublisherBase { const { data } = await repoCreationPromise; - const access = values.access as string; if (access?.startsWith(`${owner}/`)) { const [, team] = access.split('/'); - await this.client.teams.addOrUpdateRepoPermissionsInOrg({ + await githubClient.teams.addOrUpdateRepoPermissionsInOrg({ org: owner, team_slug: team, owner, @@ -105,7 +153,7 @@ export class GithubPublisher implements PublisherBase { }); // no need to add access if it's the person who own's the personal account } else if (access && access !== owner) { - await this.client.repos.addCollaborator({ + await githubClient.repos.addCollaborator({ owner, repo: name, username: access, @@ -115,4 +163,18 @@ export class GithubPublisher implements PublisherBase { return data?.clone_url; } + + private getToken(host: string): string | undefined { + return ( + this.scaffolderToken || + this.integrations.find(c => c.host === host)?.token + ); + } + + private getBaseUrl(host: string): string | undefined { + return ( + this.apiBaseUrl || + this.integrations.find(c => c.host === host)?.apiBaseUrl + ); + } } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts index 75243aa610..15e350c450 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts @@ -15,8 +15,6 @@ */ import { Logger } from 'winston'; -import { Octokit } from '@octokit/rest'; -import { Gitlab } from '@gitbeaker/node'; import { getPersonalAccessTokenHandler, WebApi } from 'azure-devops-node-api'; import { Config } from '@backstage/config'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; @@ -27,7 +25,7 @@ import { } from '../helpers'; import { PublisherBase, PublisherBuilder } from './types'; import { RemoteProtocol } from '../types'; -import { GithubPublisher, RepoVisibilityOptions } from './github'; +import { GithubPublisher } from './github'; import { GitlabPublisher } from './gitlab'; import { AzurePublisher } from './azure'; import { BitbucketPublisher } from './bitbucket'; @@ -80,23 +78,7 @@ export class Publishers implements PublisherBuilder { const githubConfig = config.getOptionalConfig('scaffolder.github'); if (githubConfig) { try { - const repoVisibility = githubConfig.getString( - 'visibility', - ) as RepoVisibilityOptions; - - const githubToken = githubConfig.getString('token'); - const githubHost = - githubConfig.getOptionalString('host') ?? 'https://api.github.com'; - const githubClient = new Octokit({ - auth: githubToken, - baseUrl: githubHost, - }); - const githubPublisher = new GithubPublisher({ - client: githubClient, - token: githubToken, - repoVisibility, - }); - + const githubPublisher = new GithubPublisher(config, { logger }); publishers.register('file', githubPublisher); publishers.register('github', githubPublisher); } catch (e) { @@ -116,12 +98,7 @@ export class Publishers implements PublisherBuilder { const gitLabConfig = config.getOptionalConfig('scaffolder.gitlab'); if (gitLabConfig) { try { - const gitLabToken = gitLabConfig.getConfig('api').getString('token'); - const gitLabClient = new Gitlab({ - host: gitLabConfig.getConfig('api').getOptionalString('baseUrl'), - token: gitLabToken, - }); - const gitLabPublisher = new GitlabPublisher(gitLabClient, gitLabToken); + const gitLabPublisher = new GitlabPublisher(config, { logger }); publishers.register('gitlab', gitLabPublisher); publishers.register('gitlab/api', gitLabPublisher); } catch (e) { diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts index 807d7c82cf..4164a6a602 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts @@ -36,6 +36,7 @@ export type PublisherBase = { export type PublisherOptions = { values: RequiredTemplateValues & Record; directory: string; + logger: Logger; }; export type PublisherResult = { diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index f703c9dea6..4fc2e98bb0 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -190,7 +190,7 @@ export const TemplatePage = () => { if (!parsedUrl.resource) { errors.storePath.addError( - 'There needs to be a hostname in the storePath', + 'There needs to be a hostname in the store path', ); } From 8ef84c67a316e451ffa4f45c4f1dd0e5cae5d28c Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 15 Jan 2021 16:07:29 +0100 Subject: [PATCH 043/209] Update storePath error and description Co-authored-by: blam --- .../src/components/TemplatePage/TemplatePage.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index 4fc2e98bb0..856ff3d0e2 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -67,7 +67,8 @@ const OWNER_REPO_SCHEMA = { format: 'storeLocation', type: 'string' as const, title: 'Store path', - description: 'GitHub store path in org/repo format', + description: + 'A full URL to the repository that should be created. e.g https://github.com/backstage/new-repo', }, access: { type: 'string' as const, @@ -188,9 +189,13 @@ export const TemplatePage = () => { const { storePath } = formData; const parsedUrl = gitParse(storePath); - if (!parsedUrl.resource) { + if ( + !parsedUrl.resource || + !parsedUrl.owner || + !parsedUrl.name + ) { errors.storePath.addError( - 'There needs to be a hostname in the store path', + 'The store path should be a complete Git URL to the new repository location. For example https://github.com/backstage/new-repo', ); } From fd876c36155a05ce04e0c8fa71526aafa23937df Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 15 Jan 2021 16:08:10 +0100 Subject: [PATCH 044/209] Refactor azure publisher to use integrations Co-authored-by: blam --- .../src/scaffolder/stages/publish/azure.ts | 87 +++++++++++++++---- .../scaffolder/stages/publish/publishers.ts | 9 +- 2 files changed, 72 insertions(+), 24 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts index fe4637b971..ac49a9e4c8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts @@ -15,22 +15,48 @@ */ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; -import { GitApi } from 'azure-devops-node-api/GitApi'; +import { IGitApi } from 'azure-devops-node-api/GitApi'; import { GitRepositoryCreateOptions } from 'azure-devops-node-api/interfaces/GitInterfaces'; -import { JsonValue, Config } from '@backstage/config'; -import { RequiredTemplateValues } from '../templater'; -import { initRepoAndPush } from './helpers'; import { Config } from '@backstage/config'; +import { initRepoAndPush } from './helpers'; import { Logger } from 'winston'; +import { + AzureIntegrationConfig, + readAzureIntegrationConfigs, +} from '@backstage/integration'; +import gitUrlParse from 'git-url-parse'; +import { getPersonalAccessTokenHandler, WebApi } from 'azure-devops-node-api'; + export class AzurePublisher implements PublisherBase { - private readonly client: GitApi; - private readonly token: string; - private readonly logger: Logger; + private readonly integrations: AzureIntegrationConfig[]; + private readonly apiBaseUrl?: string; + private readonly token?: string; constructor(config: Config, { logger }: { logger: Logger }) { - this.logger = logger; - this.client = client; - this.token = token; + this.integrations = readAzureIntegrationConfigs( + config.getOptionalConfigArray('integrations.azure') ?? [], + ); + + if (!this.integrations.length) { + logger.warn( + 'Integrations for Azure in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', + ); + } + + this.token = config.getOptionalString('scaffolder.azure.api.token'); + if (this.token) { + logger.warn( + "DEPRECATION: Using the token format under 'scaffolder.github.api.token' will not be respected in future releases. Please consider using integrations config instead", + ); + } + + this.apiBaseUrl = config.getOptionalString('scaffolder.azure.api.baseUrl'); + + if (this.apiBaseUrl) { + logger.warn( + "DEPRECATION: Using the apiBaseUrl format under 'scaffolder.azure.api.baseUrl' will not be respected in future releases. Please consider using integrations config instead", + ); + } } async publish({ @@ -38,7 +64,25 @@ export class AzurePublisher implements PublisherBase { directory, logger, }: PublisherOptions): Promise { - const remoteUrl = await this.createRemote(values); + const { resource: host, owner, name } = gitUrlParse(values.storePath); + + const token = this.getToken(host); + if (!token) { + throw new Error('No token provided to create the remote repository'); + } + const baseUrl = this.getBaseUrl(host); + if (!baseUrl) { + throw new Error('No baseUrl provided to create the remote repository'); + } + + const authHandler = getPersonalAccessTokenHandler(token); + const webApi = new WebApi(baseUrl, authHandler); + const azureClient = await webApi.getGitApi(); + + const remoteUrl = await this.createRemote(azureClient, { + project: owner, + name, + }); const catalogInfoUrl = `${remoteUrl}?path=%2Fcatalog-info.yaml`; await initRepoAndPush({ @@ -46,7 +90,7 @@ export class AzurePublisher implements PublisherBase { remoteUrl, auth: { username: 'notempty', - password: this.token, + password: token, }, logger, }); @@ -55,13 +99,24 @@ export class AzurePublisher implements PublisherBase { } private async createRemote( - values: RequiredTemplateValues & Record, + client: IGitApi, + opts: { name: string; project: string }, ) { - const [project, name] = values.storePath.split('/'); - + // const [project, name] = values.storePath.split('/'); + const { name, project } = opts; const createOptions: GitRepositoryCreateOptions = { name }; - const repo = await this.client.createRepository(createOptions, project); + const repo = await client.createRepository(createOptions, project); return repo.remoteUrl || ''; } + + private getToken(host: string): string | undefined { + return this.token || this.integrations.find(c => c.host === host)?.token; + } + + private getBaseUrl(host: string): string | undefined { + return ( + this.apiBaseUrl || this.integrations.find(c => c.host === host)?.host + ); + } } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts index 15e350c450..3d6c557714 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts @@ -118,14 +118,7 @@ export class Publishers implements PublisherBuilder { const azureConfig = config.getOptionalConfig('scaffolder.azure'); if (azureConfig) { try { - const baseUrl = azureConfig.getString('baseUrl'); - const azureToken = azureConfig.getConfig('api').getString('token'); - - const authHandler = getPersonalAccessTokenHandler(azureToken); - const webApi = new WebApi(baseUrl, authHandler); - const azureClient = await webApi.getGitApi(); - - const azurePublisher = new AzurePublisher(azureClient, azureToken); + const azurePublisher = new AzurePublisher(config, { logger }); publishers.register('azure/api', azurePublisher); } catch (e) { const providerName = 'azure'; From a5e27d5c1d87ce2b30078e32e0aa50005250686c Mon Sep 17 00:00:00 2001 From: Matei David Date: Fri, 8 Jan 2021 11:21:50 +0000 Subject: [PATCH 045/209] Introduce techdocs metadata in techdocs-common * Add TechdocsMetadata type in backend plugin endpoint * Introduce TechDocsMetadata type in frontend * Add changeset * Remove old thennable metadata resp * Address PR feedback - Remove explicit type annotation on TechDocsMetadata - Reintroduce res.send instead of throwing an error - Change logger info to logger error * Add TechDocsMetadata type in frontend plugin * Commit yarn.lock * Introduce JSON5 and remove parsing in local pub Signed-off-by: Matei David --- .changeset/techdocs-healthy-kiwis-mix.md | 9 +++++ packages/techdocs-common/package.json | 1 + .../src/stages/publish/awsS3.test.ts | 36 ++++++++++++++++--- .../src/stages/publish/awsS3.ts | 16 ++++++--- .../src/stages/publish/googleStorage.ts | 7 ++-- .../src/stages/publish/index.ts | 2 +- .../src/stages/publish/local.ts | 11 ++++-- .../src/stages/publish/types.ts | 10 +++++- .../techdocs-backend/src/service/router.ts | 24 ++++++++----- plugins/techdocs/src/api.ts | 3 +- .../reader/components/TechDocsPageHeader.tsx | 3 +- plugins/techdocs/src/types.ts | 20 +++++++++++ yarn.lock | 2 +- 13 files changed, 115 insertions(+), 29 deletions(-) create mode 100644 .changeset/techdocs-healthy-kiwis-mix.md create mode 100644 plugins/techdocs/src/types.ts diff --git a/.changeset/techdocs-healthy-kiwis-mix.md b/.changeset/techdocs-healthy-kiwis-mix.md new file mode 100644 index 0000000000..9415b43bca --- /dev/null +++ b/.changeset/techdocs-healthy-kiwis-mix.md @@ -0,0 +1,9 @@ +--- +'@backstage/techdocs-common': patch +'@backstage/plugin-techdocs': patch +'@backstage/plugin-techdocs-backend': patch +--- + +Create type for TechDocsMetadata (#3716) + +This change introduces a new type (TechDocsMetadata) in packages/techdocs-common. This type is then introduced in the endpoint response in techdocs-backend and in the api interface in techdocs (frontend). diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index 387c75241c..7f39bc6907 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -50,6 +50,7 @@ "fs-extra": "^9.0.1", "git-url-parse": "^11.4.3", "js-yaml": "^4.0.0", + "json5": "^2.1.3", "mime-types": "^2.1.27", "mock-fs": "^4.13.0", "recursive-readdir": "^2.2.2", diff --git a/packages/techdocs-common/src/stages/publish/awsS3.test.ts b/packages/techdocs-common/src/stages/publish/awsS3.test.ts index c157a7c205..3902aab21a 100755 --- a/packages/techdocs-common/src/stages/publish/awsS3.test.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.test.ts @@ -18,7 +18,7 @@ import path from 'path'; import * as winston from 'winston'; import { ConfigReader } from '@backstage/config'; import { AwsS3Publish } from './awsS3'; -import { PublisherBase } from './types'; +import { PublisherBase, TechDocsMetadata } from './types'; import type { Entity, EntityName } from '@backstage/catalog-model'; const createMockEntity = (annotations = {}): Entity => { @@ -159,13 +159,39 @@ describe('AwsS3Publish', () => { mockFs({ [entityRootDir]: { - 'techdocs_metadata.json': 'file-content', + 'techdocs_metadata.json': + '{"site_name": "backstage", "site_description": "site_content"}', }, }); - expect(await publisher.fetchTechDocsMetadata(entityNameMock)).toBe( - 'file-content', - ); + const expectedMetadata: TechDocsMetadata = { + site_name: 'backstage', + site_description: 'site_content', + }; + expect( + await publisher.fetchTechDocsMetadata(entityNameMock), + ).toStrictEqual(expectedMetadata); + mockFs.restore(); + }); + + it('should return tech docs metadata when json encoded with single quotes', async () => { + const entityNameMock = createMockEntityName(); + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); + + mockFs({ + [entityRootDir]: { + 'techdocs_metadata.json': `{'site_name': 'backstage', 'site_description': 'site_content'}`, + }, + }); + + const expectedMetadata: TechDocsMetadata = { + site_name: 'backstage', + site_description: 'site_content', + }; + expect( + await publisher.fetchTechDocsMetadata(entityNameMock), + ).toStrictEqual(expectedMetadata); mockFs.restore(); }); diff --git a/packages/techdocs-common/src/stages/publish/awsS3.ts b/packages/techdocs-common/src/stages/publish/awsS3.ts index 3f7a0e3d81..b9e9fe12e6 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.ts @@ -20,9 +20,10 @@ import { Logger } from 'winston'; import { Entity, EntityName } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { getHeadersForFileExtension, getFileTreeRecursively } from './helpers'; -import { PublisherBase, PublishRequest } from './types'; +import { PublisherBase, PublishRequest, TechDocsMetadata } from './types'; import fs from 'fs-extra'; import { Readable } from 'stream'; +import JSON5 from 'json5'; const streamToBuffer = (stream: Readable): Promise => { return new Promise((resolve, reject) => { @@ -32,7 +33,7 @@ const streamToBuffer = (stream: Readable): Promise => { stream.on('error', reject); stream.on('end', () => resolve(Buffer.concat(chunks))); } catch (e) { - throw new Error(`Unable to parse the response data, ${e.message}`); + throw new Error(`Unable to parse the response data ${e.message}`); } }); }; @@ -162,9 +163,11 @@ export class AwsS3Publish implements PublisherBase { } } - async fetchTechDocsMetadata(entityName: EntityName): Promise { + async fetchTechDocsMetadata( + entityName: EntityName, + ): Promise { try { - return await new Promise((resolve, reject) => { + return await new Promise((resolve, reject) => { const entityRootDir = `${entityName.namespace}/${entityName.kind}/${entityName.name}`; this.storageClient @@ -182,8 +185,11 @@ export class AwsS3Publish implements PublisherBase { `Unable to parse the techdocs metadata file ${entityRootDir}/techdocs_metadata.json.`, ); } + const techdocsMetadata = JSON5.parse( + techdocsMetadataJson.toString('utf-8'), + ); - resolve(techdocsMetadataJson.toString('utf-8')); + resolve(techdocsMetadata); }) .catch(err => { this.logger.error(err.message); diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.ts b/packages/techdocs-common/src/stages/publish/googleStorage.ts index e6b2c26943..f75a4b3bfd 100644 --- a/packages/techdocs-common/src/stages/publish/googleStorage.ts +++ b/packages/techdocs-common/src/stages/publish/googleStorage.ts @@ -24,7 +24,8 @@ import { Logger } from 'winston'; import { Entity, EntityName } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { getHeadersForFileExtension, getFileTreeRecursively } from './helpers'; -import { PublisherBase, PublishRequest } from './types'; +import { PublisherBase, PublishRequest, TechDocsMetadata } from './types'; +import JSON5 from 'json5'; export class GoogleGCSPublish implements PublisherBase { static async fromConfig( @@ -132,7 +133,7 @@ export class GoogleGCSPublish implements PublisherBase { }); } - fetchTechDocsMetadata(entityName: EntityName): Promise { + fetchTechDocsMetadata(entityName: EntityName): Promise { return new Promise((resolve, reject) => { const entityRootDir = `${entityName.namespace}/${entityName.kind}/${entityName.name}`; @@ -152,7 +153,7 @@ export class GoogleGCSPublish implements PublisherBase { const techdocsMetadataJson = Buffer.concat( fileStreamChunks, ).toString(); - resolve(techdocsMetadataJson); + resolve(JSON5.parse(techdocsMetadataJson)); }); }); } diff --git a/packages/techdocs-common/src/stages/publish/index.ts b/packages/techdocs-common/src/stages/publish/index.ts index 494efe5ac1..dbf4ff732a 100644 --- a/packages/techdocs-common/src/stages/publish/index.ts +++ b/packages/techdocs-common/src/stages/publish/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ export { Publisher } from './publish'; -export type { PublisherBase, PublisherType } from './types'; +export type { PublisherBase, PublisherType, TechDocsMetadata } from './types'; diff --git a/packages/techdocs-common/src/stages/publish/local.ts b/packages/techdocs-common/src/stages/publish/local.ts index de670cfba5..d07472e17d 100644 --- a/packages/techdocs-common/src/stages/publish/local.ts +++ b/packages/techdocs-common/src/stages/publish/local.ts @@ -25,7 +25,12 @@ import { PluginEndpointDiscovery, } from '@backstage/backend-common'; import { Config } from '@backstage/config'; -import { PublisherBase, PublishRequest, PublishResponse } from './types'; +import { + PublisherBase, + PublishRequest, + PublishResponse, + TechDocsMetadata, +} from './types'; // TODO: Use a more persistent storage than node_modules or /tmp directory. // Make it configurable with techdocs.publisher.local.publishDirectory @@ -102,7 +107,7 @@ export class LocalPublish implements PublisherBase { }); } - fetchTechDocsMetadata(entityName: EntityName): Promise { + fetchTechDocsMetadata(entityName: EntityName): Promise { return new Promise((resolve, reject) => { this.discovery.getBaseUrl('techdocs').then(techdocsApiUrl => { const storageUrl = new URL( @@ -116,7 +121,7 @@ export class LocalPublish implements PublisherBase { .then(response => response .json() - .then(techdocsMetadataJson => resolve(techdocsMetadataJson)) + .then(techdocsMetadata => resolve(techdocsMetadata)) .catch(err => { reject( `Unable to parse metadata JSON for ${entityRootDir}. Error: ${err}`, diff --git a/packages/techdocs-common/src/stages/publish/types.ts b/packages/techdocs-common/src/stages/publish/types.ts index 24a9d05adf..db6a075d43 100644 --- a/packages/techdocs-common/src/stages/publish/types.ts +++ b/packages/techdocs-common/src/stages/publish/types.ts @@ -32,6 +32,14 @@ export type PublishResponse = { remoteUrl?: string; } | void; +/** + * Type to hold metadata found in techdocs_metadata.json and associated with each site + */ +export type TechDocsMetadata = { + site_name: string; + site_description: string; +}; + /** * Base class for a TechDocs publisher (e.g. Local, Google GCS Bucket, AWS S3, etc.) * The publisher handles publishing of the generated static files after the prepare and generate steps of TechDocs. @@ -50,7 +58,7 @@ export interface PublisherBase { * Retrieve TechDocs Metadata about a site e.g. name, contributors, last updated, etc. * This API uses the techdocs_metadata.json file that co-exists along with the generated docs. */ - fetchTechDocsMetadata(entityName: EntityName): Promise; + fetchTechDocsMetadata(entityName: EntityName): Promise; /** * Route middleware to serve static documentation files for an entity. diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index c2ecaab61b..5ad23cde84 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -58,14 +58,22 @@ export async function createRouter({ const { '0': path } = req.params; const entityName = getEntityNameFromUrlPath(path); - publisher - .fetchTechDocsMetadata(entityName) - .then(techdocsMetadataJson => { - res.send(techdocsMetadataJson); - }) - .catch(reason => { - res.status(500).send(`Unable to get Metadata. Reason: ${reason}`); - }); + try { + const techdocsMetadata = await publisher.fetchTechDocsMetadata( + entityName, + ); + + res.send(techdocsMetadata); + } catch (err) { + logger.error( + `Unable to get metadata for ${entityName.namespace}/${entityName.name} with error ${err}`, + ); + res + .status(500) + .send( + `Unable to get metadata for $${entityName.namespace}/${entityName.name}, reason: ${err}`, + ); + } }); router.get('/metadata/entity/:namespace/:kind/:name', async (req, res) => { diff --git a/plugins/techdocs/src/api.ts b/plugins/techdocs/src/api.ts index c8581ec5d3..3a950aeeb3 100644 --- a/plugins/techdocs/src/api.ts +++ b/plugins/techdocs/src/api.ts @@ -16,6 +16,7 @@ import { createApiRef } from '@backstage/core'; import { EntityName } from '@backstage/catalog-model'; +import { TechDocsMetadata } from './types'; export const techdocsStorageApiRef = createApiRef({ id: 'plugin.techdocs.storageservice', @@ -33,7 +34,7 @@ export interface TechDocsStorage { } export interface TechDocs { - getTechDocsMetadata(entityId: EntityName): Promise; + getTechDocsMetadata(entityId: EntityName): Promise; getEntityMetadata(entityId: EntityName): Promise; } diff --git a/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx b/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx index ac9d21d319..c994b52422 100644 --- a/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx @@ -19,12 +19,13 @@ import { AsyncState } from 'react-use/lib/useAsync'; import CodeIcon from '@material-ui/icons/Code'; import { EntityName } from '@backstage/catalog-model'; import { Header, HeaderLabel, Link } from '@backstage/core'; +import { TechDocsMetadata } from '../../types'; type TechDocsPageHeaderProps = { entityId: EntityName; metadataRequest: { entity: AsyncState; - techdocs: AsyncState; + techdocs: AsyncState; }; }; diff --git a/plugins/techdocs/src/types.ts b/plugins/techdocs/src/types.ts new file mode 100644 index 0000000000..36cc4e187b --- /dev/null +++ b/plugins/techdocs/src/types.ts @@ -0,0 +1,20 @@ +/* + * 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 type TechDocsMetadata = { + site_name: string; + site_description: string; +}; diff --git a/yarn.lock b/yarn.lock index 7fdc2c3a62..9db2ecd70a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16734,7 +16734,7 @@ json3@^3.3.2: resolved "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81" integrity sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA== -json5@2.x, json5@^2.1.1, json5@^2.1.2: +json5@2.x, json5@^2.1.1, json5@^2.1.2, json5@^2.1.3: version "2.1.3" resolved "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== From 66025c7d59985a28125c45bf02933463cdda7a88 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Sun, 17 Jan 2021 14:45:29 +0200 Subject: [PATCH 046/209] Added support for SSL connection to the Kafka cluster --- plugins/kafka-backend/README.md | 4 ++++ plugins/kafka-backend/config.d.ts | 5 +++++ plugins/kafka-backend/src/service/KafkaApi.ts | 18 +++++++++++++----- plugins/kafka-backend/src/service/router.ts | 6 +++++- yarn.lock | 11 ++++++----- 5 files changed, 33 insertions(+), 11 deletions(-) diff --git a/plugins/kafka-backend/README.md b/plugins/kafka-backend/README.md index 862a4e95b8..e9b73abd35 100644 --- a/plugins/kafka-backend/README.md +++ b/plugins/kafka-backend/README.md @@ -14,6 +14,10 @@ The name of the client to use when connecting to the cluster. A list of the brokers' host names and ports to connect to. +### ssl (optional) + +Configure TLS connection to the Kafka cluster. The options are passed directly to [tls.connect] and used to create the TLS secure context. Normally these would include `key` and `cert`. + Example: ```yaml diff --git a/plugins/kafka-backend/config.d.ts b/plugins/kafka-backend/config.d.ts index 692c8754d2..0eeeb42197 100644 --- a/plugins/kafka-backend/config.d.ts +++ b/plugins/kafka-backend/config.d.ts @@ -17,5 +17,10 @@ export interface Config { kafka?: { clientId: string; brokers: string[]; + ssl?: { + ca: string[]; + key: string; + cert: string; + }; }; } diff --git a/plugins/kafka-backend/src/service/KafkaApi.ts b/plugins/kafka-backend/src/service/KafkaApi.ts index b979796f4a..1eade40bf4 100644 --- a/plugins/kafka-backend/src/service/KafkaApi.ts +++ b/plugins/kafka-backend/src/service/KafkaApi.ts @@ -16,6 +16,7 @@ import { Kafka, SeekEntry } from 'kafkajs'; import { Logger } from 'winston'; +import { ConnectionOptions } from 'tls'; export type PartitionOffset = { id: number; @@ -27,6 +28,13 @@ export type TopicOffset = { partitions: PartitionOffset[]; }; +export type Options = { + clientId: string; + brokers: string[]; + ssl?: ConnectionOptions; + logger: Logger; +}; + export interface KafkaApi { fetchTopicOffsets(topic: string): Promise>; fetchGroupOffsets(groupId: string): Promise>; @@ -36,13 +44,13 @@ export class KafkaJsApiImpl implements KafkaApi { private readonly kafka: Kafka; private readonly logger: Logger; - constructor(clientId: string, brokers: string[], logger: Logger) { - logger.debug( - `creating kafka client with clientId=${clientId} and brokers=${brokers}`, + constructor(options: Options) { + options.logger.debug( + `creating kafka client with clientId=${options.clientId} and brokers=${options.brokers}`, ); - this.kafka = new Kafka({ clientId, brokers }); - this.logger = logger; + this.kafka = new Kafka(options); + this.logger = options.logger; } async fetchTopicOffsets(topic: string): Promise> { diff --git a/plugins/kafka-backend/src/service/router.ts b/plugins/kafka-backend/src/service/router.ts index 621de690d3..2bbc877fe2 100644 --- a/plugins/kafka-backend/src/service/router.ts +++ b/plugins/kafka-backend/src/service/router.ts @@ -20,6 +20,7 @@ import { Logger } from 'winston'; import { Config } from '@backstage/config'; import { KafkaApi, KafkaJsApiImpl } from './KafkaApi'; import _ from 'lodash'; +import { ConnectionOptions } from 'tls'; export interface RouterOptions { logger: Logger; @@ -71,7 +72,10 @@ export async function createRouter( const clientId = options.config.getString('kafka.clientId'); const brokers = options.config.getStringArray('kafka.brokers'); - const kafkaApi = new KafkaJsApiImpl(clientId, brokers, logger); + const sslConfig = options.config.getOptional('kafka.ssl'); + const ssl = sslConfig ? (sslConfig as ConnectionOptions) : undefined; + + const kafkaApi = new KafkaJsApiImpl({ clientId, brokers, logger, ssl }); return makeRouter(logger, kafkaApi); } diff --git a/yarn.lock b/yarn.lock index 71fb15d9dd..cb8aed7d86 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2437,7 +2437,7 @@ "@backstage/catalog-model@^0.2.0": version "0.6.1" dependencies: - "@backstage/config" "^0.1.1" + "@backstage/config" "^0.1.2" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" json-schema "^0.2.5" @@ -2448,7 +2448,7 @@ "@backstage/catalog-model@^0.3.0": version "0.6.1" dependencies: - "@backstage/config" "^0.1.1" + "@backstage/config" "^0.1.2" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" json-schema "^0.2.5" @@ -2459,13 +2459,14 @@ "@backstage/core@^0.3.0": version "0.4.4" dependencies: - "@backstage/config" "^0.1.1" - "@backstage/core-api" "^0.2.1" - "@backstage/theme" "^0.2.1" + "@backstage/config" "^0.1.2" + "@backstage/core-api" "^0.2.8" + "@backstage/theme" "^0.2.2" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" "@material-ui/lab" "4.0.0-alpha.45" "@types/dagre" "^0.7.44" + "@types/prop-types" "^15.7.3" "@types/react" "^16.9" "@types/react-sparklines" "^1.7.0" classnames "^2.2.6" From a35862c67d1aaf30b9e2f2204ed4a668447e12af Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Sun, 17 Jan 2021 14:47:44 +0200 Subject: [PATCH 047/209] Fixed spelling --- plugins/kafka-backend/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/kafka-backend/README.md b/plugins/kafka-backend/README.md index e9b73abd35..b2271a2911 100644 --- a/plugins/kafka-backend/README.md +++ b/plugins/kafka-backend/README.md @@ -14,7 +14,7 @@ The name of the client to use when connecting to the cluster. A list of the brokers' host names and ports to connect to. -### ssl (optional) +### SSL (optional) Configure TLS connection to the Kafka cluster. The options are passed directly to [tls.connect] and used to create the TLS secure context. Normally these would include `key` and `cert`. From 95cb0211d1a8f456512c10d94c5b87222ef5b8d8 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Sun, 17 Jan 2021 15:49:03 +0200 Subject: [PATCH 048/209] Updated infra versions --- plugins/kafka/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index 8b4e84ab2d..c0a5de2d34 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@backstage/catalog-model": "^0.6.0", - "@backstage/core": "^0.4.3", + "@backstage/core": "^0.4.4", "@backstage/theme": "^0.2.2", "@backstage/plugin-catalog": "^0.2.7", "@material-ui/core": "^4.11.0", @@ -33,7 +33,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.5", + "@backstage/cli": "^0.4.6", "@backstage/dev-utils": "^0.1.7", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", From c794d23f8e285b4d82a4ff9045e7037b8c61c9ea Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Sun, 17 Jan 2021 20:03:52 +0200 Subject: [PATCH 049/209] Added UI test for consumer table --- .../ConsumerGroupOffsets.test.tsx | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.test.tsx diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.test.tsx b/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.test.tsx new file mode 100644 index 0000000000..629f7a9a7a --- /dev/null +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.test.tsx @@ -0,0 +1,49 @@ +/* + * 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 React from 'react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { ConsumerGroupOffsets } from './ConsumerGroupOffsets'; + +describe('ConsumerGroupOffsets', () => { + it('should render consumer group table', async () => { + const rendered = await renderInTestApp( + {}} + />, + ); + + expect(rendered.getByText(/consumer/)).toBeInTheDocument(); + expect(rendered.getByText('topic1')).toBeInTheDocument(); + expect(rendered.getByText('topic2')).toBeInTheDocument(); + }); +}); From 8671313795c5c1a72b29904164f69f0f315370de Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Sun, 17 Jan 2021 20:44:51 +0200 Subject: [PATCH 050/209] removed standalone kafka backend plugin --- plugins/kafka-backend/src/run.ts | 33 ------------ .../src/service/standaloneApplication.ts | 54 ------------------- .../src/service/standaloneServer.ts | 50 ----------------- 3 files changed, 137 deletions(-) delete mode 100644 plugins/kafka-backend/src/run.ts delete mode 100644 plugins/kafka-backend/src/service/standaloneApplication.ts delete mode 100644 plugins/kafka-backend/src/service/standaloneServer.ts diff --git a/plugins/kafka-backend/src/run.ts b/plugins/kafka-backend/src/run.ts deleted file mode 100644 index 688191d406..0000000000 --- a/plugins/kafka-backend/src/run.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 yn from 'yn'; -import { getRootLogger } from '@backstage/backend-common'; -import { startStandaloneServer } from './service/standaloneServer'; - -const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7000; -const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); -const logger = getRootLogger(); - -startStandaloneServer({ port, enableCors, logger }).catch(err => { - logger.error(err); - process.exit(1); -}); - -process.on('SIGINT', () => { - logger.info('CTRL+C pressed; exiting.'); - process.exit(0); -}); diff --git a/plugins/kafka-backend/src/service/standaloneApplication.ts b/plugins/kafka-backend/src/service/standaloneApplication.ts deleted file mode 100644 index 50563023d0..0000000000 --- a/plugins/kafka-backend/src/service/standaloneApplication.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 { - errorHandler, - notFoundHandler, - requestLoggingHandler, -} from '@backstage/backend-common'; -import compression from 'compression'; -import cors from 'cors'; -import express from 'express'; -import helmet from 'helmet'; -import { Logger } from 'winston'; -import { createRouter } from './router'; -import { ConfigReader } from '@backstage/config'; - -export interface ApplicationOptions { - enableCors: boolean; - logger: Logger; -} - -export async function createStandaloneApplication( - options: ApplicationOptions, -): Promise { - const { enableCors, logger } = options; - const config = new ConfigReader({}); - const app = express(); - - app.use(helmet()); - if (enableCors) { - app.use(cors()); - } - app.use(compression()); - app.use(express.json()); - app.use(requestLoggingHandler()); - app.use('/', await createRouter({ logger, config })); - app.use(notFoundHandler()); - app.use(errorHandler()); - - return app; -} diff --git a/plugins/kafka-backend/src/service/standaloneServer.ts b/plugins/kafka-backend/src/service/standaloneServer.ts deleted file mode 100644 index 6459995928..0000000000 --- a/plugins/kafka-backend/src/service/standaloneServer.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 { Server } from 'http'; -import { Logger } from 'winston'; -import { createStandaloneApplication } from './standaloneApplication'; - -export interface ServerOptions { - port: number; - enableCors: boolean; - logger: Logger; -} - -export async function startStandaloneServer( - options: ServerOptions, -): Promise { - const logger = options.logger.child({ service: 'kafka-backend' }); - - logger.debug('Creating application...'); - const app = await createStandaloneApplication({ - enableCors: options.enableCors, - logger, - }); - - logger.debug('Starting application server...'); - return await new Promise((resolve, reject) => { - const server = app.listen(options.port, (err?: Error) => { - if (err) { - reject(err); - return; - } - - logger.info(`Listening on port ${options.port}`); - resolve(server); - }); - }); -} From 4a438d589c7685c369ccd132508374ea0cbebf59 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Sun, 17 Jan 2021 20:50:20 +0200 Subject: [PATCH 051/209] Removed unneeded deps in kafka backend --- plugins/kafka-backend/package.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/plugins/kafka-backend/package.json b/plugins/kafka-backend/package.json index 381a6abbe8..785a065d3e 100644 --- a/plugins/kafka-backend/package.json +++ b/plugins/kafka-backend/package.json @@ -35,15 +35,11 @@ "@backstage/catalog-model": "^0.6.0", "@backstage/config": "^0.1.2", "@types/express": "^4.17.6", - "compression": "^1.7.4", - "cors": "^2.8.5", "express": "^4.17.1", "express-promise-router": "^3.0.3", - "helmet": "^4.0.0", "kafkajs": "^1.16.0-beta.6", "lodash": "^4.17.15", - "winston": "^3.2.1", - "yn": "^4.0.0" + "winston": "^3.2.1" }, "devDependencies": { "@backstage/cli": "^0.4.3", From 7c52806a48dcab14bdcbd8143692c0008c1e0374 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 18 Jan 2021 10:11:52 +0100 Subject: [PATCH 052/209] remove commented code --- .../scaffolder-backend/src/scaffolder/stages/publish/azure.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts index ac49a9e4c8..50de3331d8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts @@ -102,7 +102,6 @@ export class AzurePublisher implements PublisherBase { client: IGitApi, opts: { name: string; project: string }, ) { - // const [project, name] = values.storePath.split('/'); const { name, project } = opts; const createOptions: GitRepositoryCreateOptions = { name }; const repo = await client.createRepository(createOptions, project); From fdecdfd747ab79f598fb57731e4ede375ca28822 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 18 Jan 2021 10:13:07 +0100 Subject: [PATCH 053/209] Use integration config in bitbucket preparer --- .../scaffolder/stages/publish/bitbucket.ts | 135 ++++++++++++++---- 1 file changed, 107 insertions(+), 28 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts index ad83ad54e8..b07e77e49a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts @@ -16,19 +16,54 @@ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; import { initRepoAndPush } from './helpers'; -import { RequiredTemplateValues } from '../templater'; -import { JsonValue } from '../../../../../../packages/config/src'; import fetch from 'cross-fetch'; +import { Config } from '@backstage/config'; +import { + BitbucketIntegrationConfig, + readBitbucketIntegrationConfigs, +} from '@backstage/integration'; +import { Logger } from 'winston'; +import gitUrlParse from 'git-url-parse'; export class BitbucketPublisher implements PublisherBase { - private readonly host: string; - private readonly username: string; - private readonly token: string; + private readonly host?: string; + private readonly username?: string; + private readonly token?: string; + private readonly integrations: BitbucketIntegrationConfig[]; - constructor(host: string, username: string, token: string) { - this.host = host; - this.username = username; - this.token = token; + constructor(config: Config, { logger }: { logger: Logger }) { + this.integrations = readBitbucketIntegrationConfigs( + config.getOptionalConfigArray('integrations.bitbucket') ?? [], + ); + + if (!this.integrations.length) { + logger.warn( + 'Integrations for Bitbucket in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', + ); + } + + this.token = config.getOptionalString('scaffolder.bitbucket.api.token'); + if (this.token) { + logger.warn( + "DEPRECATION: Using the token format under 'scaffolder.bitbucket.api.token' will not be respected in future releases. Please consider using integrations config instead", + ); + } + + this.host = config.getOptionalString('scaffolder.bitbucket.api.host'); + if (this.host) { + logger.warn( + "DEPRECATION: Using the apiBaseUrl format under 'scaffolder.bitbucket.api.host' will not be respected in future releases. Please consider using integrations config instead", + ); + } + + this.username = config.getOptionalString( + 'scaffolder.bitbucket.api.username', + ); + if (this.username) { + logger.warn( + "DEPRECATION: Using the apiBaseUrl format under 'scaffolder.bitbucket.api.username' will not be respected in future releases. Please consider using integrations config instead", + ); + } } async publish({ @@ -36,33 +71,61 @@ export class BitbucketPublisher implements PublisherBase { directory, logger, }: PublisherOptions): Promise { - const result = await this.createRemote(values); + const { resource: host, owner: project, name } = gitUrlParse( + values.storePath, + ); + + const token = this.getToken(host); + if (!token) { + throw new Error('No token provided to create the remote repository'); + } + const username = this.getUsername(host); + if (!username) { + throw new Error('No username provided to create the remote repository'); + } + const apiUrl = this.getHost(host); + if (!apiUrl) { + throw new Error('No host provided to create the remote repository'); + } + + const description = values.description as string; + const result = await this.createRemote({ + project, + name, + description, + host, + }); await initRepoAndPush({ dir: directory, remoteUrl: result.remoteUrl, auth: { - username: this.username, - password: this.token, + username: username, + password: token, }, logger, }); return result; } - private async createRemote( - values: RequiredTemplateValues & Record, - ): Promise { - if (this.host === 'https://bitbucket.org') { - return this.createBitbucketCloudRepository(values); + private async createRemote(opts: { + project: string; + name: string; + description: string; + host: string; + }): Promise { + if (opts.host === 'https://bitbucket.org') { + return this.createBitbucketCloudRepository(opts); } - return this.createBitbucketServerRepository(values); + return this.createBitbucketServerRepository(opts); } - private async createBitbucketCloudRepository( - values: RequiredTemplateValues & Record, - ): Promise { - const [project, name] = values.storePath.split('/'); + private async createBitbucketCloudRepository(opts: { + project: string; + name: string; + description: string; + }): Promise { + const { project, name, description } = opts; let response: Response; const buffer = Buffer.from(`${this.username}:${this.token}`, 'utf8'); @@ -71,7 +134,7 @@ export class BitbucketPublisher implements PublisherBase { method: 'POST', body: JSON.stringify({ scm: 'git', - description: values.description, + description: description, }), headers: { Authorization: `Basic ${buffer.toString('base64')}`, @@ -102,17 +165,19 @@ export class BitbucketPublisher implements PublisherBase { throw new Error(`Not a valid response code ${await response.text()}`); } - private async createBitbucketServerRepository( - values: RequiredTemplateValues & Record, - ): Promise { - const [project, name] = values.storePath.split('/'); + private async createBitbucketServerRepository(opts: { + project: string; + name: string; + description: string; + }): Promise { + const { project, name, description } = opts; let response: Response; const options: RequestInit = { method: 'POST', body: JSON.stringify({ name: name, - description: values.description, + description: description, }), headers: { Authorization: `Bearer ${this.token}`, @@ -140,4 +205,18 @@ export class BitbucketPublisher implements PublisherBase { } throw new Error(`Not a valid response code ${await response.text()}`); } + + private getToken(host: string): string | undefined { + return this.token || this.integrations.find(c => c.host === host)?.token; + } + + private getUsername(host: string): string | undefined { + return ( + this.username || this.integrations.find(c => c.host === host)?.username + ); + } + + private getHost(host: string): string | undefined { + return this.host || this.integrations.find(c => c.host === host)?.host; + } } From ca957da3e364f997487af48dc427fefe67f58e97 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 18 Jan 2021 10:25:13 +0100 Subject: [PATCH 054/209] Update packages/integration/src/gitlab/config.ts Co-authored-by: Himanshu Mishra --- packages/integration/src/gitlab/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integration/src/gitlab/config.ts b/packages/integration/src/gitlab/config.ts index 28687399e1..2d9f4b39ab 100644 --- a/packages/integration/src/gitlab/config.ts +++ b/packages/integration/src/gitlab/config.ts @@ -31,7 +31,7 @@ export type GitLabIntegrationConfig = { /** * The base URL of the API of this provider, e.g. "https://gitlab.com/api/v4", - * with no trailing slash.s + * with no trailing slash. * * May be omitted specifically for GitLab; then it will be deduced. * From 62fa5496c1e350a6758ab502b3ffe5f4def44b3c Mon Sep 17 00:00:00 2001 From: Jussi Hallila Date: Mon, 18 Jan 2021 11:09:08 +0100 Subject: [PATCH 055/209] Address issues from code review --- .../src/api/CatalogImportClient.ts | 4 +-- .../components/ImportComponentPage.test.tsx | 20 +++++++++++--- .../catalog-import/src/util/useGithubRepos.ts | 26 +++++++++++-------- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/plugins/catalog-import/src/api/CatalogImportClient.ts b/plugins/catalog-import/src/api/CatalogImportClient.ts index 706943a6aa..4ce25e8fc2 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.ts @@ -128,10 +128,10 @@ export class CatalogImportClient implements CatalogImportApi { const catalogInfoItem = searchResult.data.items .map(it => it.path) .sort((a, b) => a.length - b.length)[0]; - return Promise.resolve({ + return { url: `blob/${defaultBranch}/${catalogInfoItem}`, exists, - }); + }; } return Promise.resolve({ exists, diff --git a/plugins/catalog-import/src/components/ImportComponentPage.test.tsx b/plugins/catalog-import/src/components/ImportComponentPage.test.tsx index 5c4b2e8538..932f9aafa0 100644 --- a/plugins/catalog-import/src/components/ImportComponentPage.test.tsx +++ b/plugins/catalog-import/src/components/ImportComponentPage.test.tsx @@ -37,12 +37,14 @@ let codeSearchMockResponse: () => Promise<{ }; }>; +let findGithubConfigMockResponse = () => ({ + host: 'test.localhost', + owner: 'someuser', +}); + jest.mock('@backstage/integration', () => ({ readGitHubIntegrationConfigs: () => ({ - find: () => ({ - host: 'test.localhost', - owner: 'someuser', - }), + find: findGithubConfigMockResponse, }), })); @@ -138,6 +140,7 @@ describe('', () => { } it('Should not explode on non-Github URLs', async () => { + findGithubConfigMockResponse = () => undefined!!; await renderSUT(); await waitFor(() => { fireEvent.input( @@ -160,6 +163,7 @@ describe('', () => { }); it('Should offer direct file import from non-Github URLs', async () => { + findGithubConfigMockResponse = () => undefined!!; await renderSUT(); await waitFor(() => { fireEvent.input( @@ -188,6 +192,10 @@ describe('', () => { }); it('Should use found yaml file directly and not create a pull request if GitHub api returns one', async () => { + findGithubConfigMockResponse = () => ({ + host: 'test.localhost', + owner: 'someuser', + }); codeSearchMockResponse = () => Promise.resolve({ data: { @@ -221,6 +229,10 @@ describe('', () => { }); it('Should indicate a pull request creation when no yaml file found in the repo', async () => { + findGithubConfigMockResponse = () => ({ + host: 'test.localhost', + owner: 'someuser', + }); codeSearchMockResponse = () => Promise.resolve({ data: { diff --git a/plugins/catalog-import/src/util/useGithubRepos.ts b/plugins/catalog-import/src/util/useGithubRepos.ts index 319457dfe8..704ad974a5 100644 --- a/plugins/catalog-import/src/util/useGithubRepos.ts +++ b/plugins/catalog-import/src/util/useGithubRepos.ts @@ -22,7 +22,10 @@ import parseGitUri from 'git-url-parse'; // TODO: (O5ten) Refactor into a core API instead of direct usage like this // https://github.com/backstage/backstage/pull/3613#issuecomment-7408929430 -import { readGitHubIntegrationConfigs } from '@backstage/integration'; +import { + GitHubIntegrationConfig, + readGitHubIntegrationConfigs, +} from '@backstage/integration'; export function useGithubRepos() { const api = useApi(catalogImportApiRef); @@ -84,20 +87,21 @@ export function useGithubRepos() { const checkForExistingCatalogInfo = async ( location: string, ): Promise<{ exists: boolean; url?: string }> => { - const { source } = parseGitUri(location); - if (source !== 'github.com') { + let githubConfig: { + repoName: string; + ownerName: string; + githubIntegrationConfig: GitHubIntegrationConfig; + }; + try { + githubConfig = getGithubIntegrationConfig(location); + } catch (e) { return Promise.resolve({ exists: false }); } - const { - repoName, - ownerName, - githubIntegrationConfig, - } = getGithubIntegrationConfig(location); return await api .checkForExistingCatalogInfo({ - owner: ownerName, - repo: repoName, - githubIntegrationConfig, + owner: githubConfig.ownerName, + repo: githubConfig.repoName, + githubIntegrationConfig: githubConfig.githubIntegrationConfig, }) .catch(e => { throw new Error( From 7b038dd16c7e1edd745026e46b8187e1dd2b49d1 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 18 Jan 2021 11:37:11 +0100 Subject: [PATCH 056/209] bump gitbeaker --- plugins/scaffolder-backend/package.json | 4 +- yarn.lock | 52 ++++++++++++++++++------- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index c2cef2f73e..efbc73ff4b 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -33,8 +33,8 @@ "@backstage/catalog-model": "^0.6.0", "@backstage/config": "^0.1.2", "@backstage/integration": "^0.1.5", - "@gitbeaker/core": "^25.2.0", - "@gitbeaker/node": "^25.2.0", + "@gitbeaker/core": "^28.0.2", + "@gitbeaker/node": "^28.0.2", "@octokit/rest": "^18.0.0", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", diff --git a/yarn.lock b/yarn.lock index 3f37440667..32ebc261c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1644,11 +1644,9 @@ to-fast-properties "^2.0.0" "@backstage/catalog-model@^0.2.0": - version "0.2.0" - resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.2.0.tgz#e3fe2a4ddeb6a9b6ec480c80cb2b9c39cb245576" - integrity sha512-Y1ocdRpBlxK/VrJQjHlQd0bgADECd1B2NRjwd8ss46ibT5hwLvMOfD80+Fa7oPLu0ktJrH4lq0pNIIJIml48zA== + version "0.6.0" dependencies: - "@backstage/config" "^0.1.1" + "@backstage/config" "^0.1.2" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" json-schema "^0.2.5" @@ -1657,11 +1655,9 @@ yup "^0.29.3" "@backstage/catalog-model@^0.3.0": - version "0.3.1" - resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.3.1.tgz#45d08e2f333c9c566b2bf2629fd707fe989bb404" - integrity sha512-9XhV7c4rmVW+Yzj2PiwTQ7DsegWGB3C4ELsDRExuEVZONdqNcC02cyJtrt3fT5F31ZS3tHkB9bMUymFOBLqUSA== + version "0.6.0" dependencies: - "@backstage/config" "^0.1.1" + "@backstage/config" "^0.1.2" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" json-schema "^0.2.5" @@ -1670,17 +1666,16 @@ yup "^0.29.3" "@backstage/core@^0.3.0": - version "0.3.2" - resolved "https://registry.npmjs.org/@backstage/core/-/core-0.3.2.tgz#a8209126d5076cf4a8b9bd632fe4e5e2edb62916" - integrity sha512-i5d+Wh8js4qEWoAsPY5L7HVSWpumr1OhfF2dUCGYdyW6AMqVJPca6+n6zp1Rg2CO+J9norp44XAVVCbyhtUpig== + version "0.4.3" dependencies: - "@backstage/config" "^0.1.1" - "@backstage/core-api" "^0.2.1" - "@backstage/theme" "^0.2.1" + "@backstage/config" "^0.1.2" + "@backstage/core-api" "^0.2.8" + "@backstage/theme" "^0.2.2" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" "@material-ui/lab" "4.0.0-alpha.45" "@types/dagre" "^0.7.44" + "@types/prop-types" "^15.7.3" "@types/react" "^16.9" "@types/react-sparklines" "^1.7.0" classnames "^2.2.6" @@ -2191,6 +2186,16 @@ li "^1.3.0" xcase "^2.0.1" +"@gitbeaker/core@^28.0.2": + version "28.0.2" + resolved "https://registry.npmjs.org/@gitbeaker/core/-/core-28.0.2.tgz#5c2cbe27a20426a96d9511642153ead18ff88877" + integrity sha512-H+0ChYSxLh+EVNzmOguHaJMR7xB9FlVZGi89JQ7BJ2meq0h1TQl+QaGqTyTNM4GYhn7cYrl7ebGoU0yrnYUWSg== + dependencies: + "@gitbeaker/requester-utils" "^28.0.2" + form-data "^3.0.0" + li "^1.3.0" + xcase "^2.0.1" + "@gitbeaker/node@^25.2.0": version "25.2.0" resolved "https://registry.npmjs.org/@gitbeaker/node/-/node-25.2.0.tgz#cc91e83328ec32de0b1a0dac23accd2385734a66" @@ -2201,6 +2206,16 @@ got "^11.7.0" xcase "^2.0.1" +"@gitbeaker/node@^28.0.2": + version "28.0.2" + resolved "https://registry.npmjs.org/@gitbeaker/node/-/node-28.0.2.tgz#ead4eb9e1b4400d3a6fe9769b50e43e84fc99747" + integrity sha512-FCILxXiRep3PUpe/P2+Ak1KdwzIz3azxf1OY8Jb72gKjNNTgCi4ByFCIpHkvbFZlr7kDKTtcPyEN6oA6qnrCdQ== + dependencies: + "@gitbeaker/core" "^28.0.2" + "@gitbeaker/requester-utils" "^28.0.2" + got "^11.7.0" + xcase "^2.0.1" + "@gitbeaker/requester-utils@^25.2.0", "@gitbeaker/requester-utils@^25.6.0": version "25.6.0" resolved "https://registry.npmjs.org/@gitbeaker/requester-utils/-/requester-utils-25.6.0.tgz#001a432a48460bb5196a02ed71763eb707a1a01e" @@ -2210,6 +2225,15 @@ query-string "^6.13.3" xcase "^2.0.1" +"@gitbeaker/requester-utils@^28.0.2": + version "28.0.2" + resolved "https://registry.npmjs.org/@gitbeaker/requester-utils/-/requester-utils-28.0.2.tgz#3c1bf97e6b73053ef3c38a2871cbc8e1a2e7501a" + integrity sha512-S851vQi0x9Sg5Tx0BcwzkG6OTIG0X4qYTBaA9kPM2sEKg2OOsd8X0HRhj/TbFGr/YVz+3Ubu7oueeWSyr5jvfQ== + dependencies: + form-data "^3.0.0" + query-string "^6.13.3" + xcase "^2.0.1" + "@google-cloud/common@^3.5.0": version "3.5.0" resolved "https://registry.npmjs.org/@google-cloud/common/-/common-3.5.0.tgz#0959e769e8075a06eb0823cc567eef00fd0c2d02" From 931f0c6b89a861e1f37b07cbd9df75a72a79672d Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 18 Jan 2021 11:37:50 +0100 Subject: [PATCH 057/209] Publish to provider based on url --- .../src/scaffolder/jobs/processor.ts | 1 + .../src/scaffolder/stages/publish/gitlab.ts | 71 ++++++++++--------- .../scaffolder/stages/publish/publishers.ts | 56 ++++++--------- .../src/scaffolder/stages/publish/types.ts | 3 +- .../scaffolder-backend/src/service/router.ts | 4 +- 5 files changed, 62 insertions(+), 73 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts b/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts index 25f9b23e59..fc11545e04 100644 --- a/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts +++ b/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts @@ -120,6 +120,7 @@ export class JobProcessor implements Processor { // Log to the current stage the error that occurred and fail the stage. stage.status = 'FAILED'; logger.error(`Stage failed with error: ${error.message}`); + logger.error(error.stack); // Throw the error so the job can be failed too. throw error; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts index c592b06807..71a38f1079 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts @@ -15,11 +15,10 @@ */ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; -import { Gitlab } from '@gitbeaker/core'; -import { Config, JsonValue } from '@backstage/config'; +import { Gitlab } from '@gitbeaker/node'; +import { Config } from '@backstage/config'; import { Logger } from 'winston'; import { initRepoAndPush } from './helpers'; -import { RequiredTemplateValues } from '../templater'; import gitUrlParse from 'git-url-parse'; import { @@ -67,15 +66,31 @@ export class GitlabPublisher implements PublisherBase { async publish({ values, directory, + logger, }: PublisherOptions): Promise { - const remoteUrl = await this.createRemote(values); - const { host } = new URL(remoteUrl); - const token = this.getToken(host); + const { resource: host, owner, name } = gitUrlParse(values.storePath); + const token = this.getToken(host); if (!token) { - throw new Error('No token provided to create the remote repository'); + throw new Error( + 'No authentication set for Gitlab publisher. Creating the remote repository is not possible without a token', + ); } + const baseUrl = this.getBaseUrl(host); + if (!baseUrl) { + throw new Error( + 'No host set for Gitlab publisher. Creating the remote repository is not possible without a host', + ); + } + + const remoteUrl = await this.createRemote({ + host: baseUrl, + owner, + name, + token, + }); + await initRepoAndPush({ dir: directory, remoteUrl, @@ -83,10 +98,14 @@ export class GitlabPublisher implements PublisherBase { username: 'oauth2', password: token, }, - logger: this.logger, + logger, }); - return { remoteUrl }; + const catalogInfoUrl = remoteUrl.replace( + /\.git$/, + '/-/blob/master/catalog-info.yaml', + ); + return { remoteUrl, catalogInfoUrl }; } private getToken(host: string): string | undefined { @@ -103,33 +122,15 @@ export class GitlabPublisher implements PublisherBase { ); } - private getConfig(host: string): { baseUrl?: string; token?: string } { - return { - baseUrl: this.getBaseUrl(host), - token: this.getToken(host), - }; - } - - private async createRemote( - values: RequiredTemplateValues & Record, - ) { - const pathElements = values.storePath.split('/'); - const name = pathElements[pathElements.length - 1]; - pathElements.pop(); - const owner = pathElements.join('/'); - - const { resource: host } = gitUrlParse(values.storePath); - - const config = this.getConfig(host); - - if (!config.token) { - throw new Error( - 'No authentication set for Gitlab publisher. Creating the remote repository is not possible without a token', - ); - } - - const client = new Gitlab({ host: config.baseUrl, token: config.token }); + private async createRemote(opts: { + host: string; + name: string; + owner: string; + token: string; + }) { + const { owner, name, host, token } = opts; + const client = new Gitlab({ host: host, token: token }); let targetNamespace = ((await client.Namespaces.show(owner)) as { id: number; }).id; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts index 3d6c557714..78cc85a6c4 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts @@ -15,13 +15,10 @@ */ import { Logger } from 'winston'; -import { getPersonalAccessTokenHandler, WebApi } from 'azure-devops-node-api'; import { Config } from '@backstage/config'; -import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { DeprecatedLocationTypeDetector, makeDeprecatedLocationTypeDetector, - parseLocationAnnotation, } from '../helpers'; import { PublisherBase, PublisherBuilder } from './types'; import { RemoteProtocol } from '../types'; @@ -39,32 +36,29 @@ export class Publishers implements PublisherBuilder { this.publisherMap.set(protocol, publisher); } - get(template: TemplateEntityV1alpha1): PublisherBase { - const { protocol, location } = parseLocationAnnotation(template); - const publisher = this.publisherMap.get(protocol); + get(storePath: string, { logger }: { logger: Logger }): PublisherBase { + const protocol = this.typeDetector?.(storePath); - if (!publisher) { - if ((protocol as string) === 'url') { - const type = this.typeDetector?.(location); - const detected = type && this.publisherMap.get(type as RemoteProtocol); - if (detected) { - return detected; - } - if (type) { - throw new Error( - `No publisher configuration available for type '${type}' with url "${location}". ` + - "Make sure you've added appropriate configuration in the 'scaffolder' configuration section", - ); - } else { - throw new Error( - `Failed to detect publisher type. Unable to determine integration type for location "${location}". ` + - "Please add appropriate configuration to the 'integrations' configuration section", - ); - } - } - throw new Error(`No publisher registered for type: "${protocol}"`); + if (!protocol) { + throw new Error( + `No matching publisher detected for "${storePath}". Please make sure this host is registered in the integration config`, + ); } + logger.info( + `Selected publisher ${protocol} for publishing to URL ${storePath}`, + ); + + const publisher = this.publisherMap.get(protocol as RemoteProtocol); + if (!publisher) { + throw new Error( + `Failed to detect publisher type. Unable to determine integration type for location "${location}". ` + + "Please add appropriate configuration to the 'integrations' configuration section", + ); + } + + logger.info(`Selected publisher for protocol ${protocol}`); + return publisher; } @@ -139,15 +133,7 @@ export class Publishers implements PublisherBuilder { ); if (bitbucketConfig) { try { - const baseUrl = bitbucketConfig.getString('host'); - const bitbucketUsername = bitbucketConfig.getString('username'); - const bitbucketToken = bitbucketConfig.getString('token'); - - const bitbucketPublisher = new BitbucketPublisher( - baseUrl, - bitbucketUsername, - bitbucketToken, - ); + const bitbucketPublisher = new BitbucketPublisher(config, { logger }); publishers.register('bitbucket', bitbucketPublisher); } catch (e) { const providerName = 'bitbucket'; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts index 4164a6a602..cb726dfe09 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { RequiredTemplateValues } from '../templater'; import { JsonValue } from '@backstage/config'; import { RemoteProtocol } from '../types'; @@ -46,5 +45,5 @@ export type PublisherResult = { export type PublisherBuilder = { register(protocol: RemoteProtocol, publisher: PublisherBase): void; - get(template: TemplateEntityV1alpha1): PublisherBase; + get(storePath: string, { logger }: { logger: Logger }): PublisherBase; }; diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index ffd4d6487c..1ca82d4736 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -154,7 +154,9 @@ export async function createRouter( { name: 'Publish template', handler: async (ctx: StageContext<{ resultDir: string }>) => { - const publisher = publishers.get(ctx.entity); + const publisher = publishers.get(ctx.values.storePath, { + logger: ctx.logger, + }); ctx.logger.info('Will now store the template'); const result = await publisher.publish({ values: ctx.values, From 70255c7fff4ec2e9b0654de52644d298a43017b2 Mon Sep 17 00:00:00 2001 From: Jussi Hallila Date: Mon, 18 Jan 2021 14:36:09 +0100 Subject: [PATCH 058/209] Address issues from code review. --- plugins/catalog-import/src/util/useGithubRepos.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/catalog-import/src/util/useGithubRepos.ts b/plugins/catalog-import/src/util/useGithubRepos.ts index 704ad974a5..db3ee9aa09 100644 --- a/plugins/catalog-import/src/util/useGithubRepos.ts +++ b/plugins/catalog-import/src/util/useGithubRepos.ts @@ -70,7 +70,7 @@ export function useGithubRepos() { githubIntegrationConfig, }) .catch(e => { - throw new Error(`Failed to submit PR to repo:\n${e.message}`); + throw new Error(`Failed to submit PR to repo: ${e.message}`); }); await api @@ -78,7 +78,7 @@ export function useGithubRepos() { location: submitPRResponse.location, }) .catch(e => { - throw new Error(`Failed to create repository location:\n${e.message}`); + throw new Error(`Failed to create repository location: ${e.message}`); }); return submitPRResponse; @@ -95,7 +95,7 @@ export function useGithubRepos() { try { githubConfig = getGithubIntegrationConfig(location); } catch (e) { - return Promise.resolve({ exists: false }); + return { exists: false }; } return await api .checkForExistingCatalogInfo({ @@ -105,7 +105,7 @@ export function useGithubRepos() { }) .catch(e => { throw new Error( - `Failed to inspect repository for existing catalog-info.yaml:\n${e.message}`, + `Failed to inspect repository for existing catalog-info.yaml: ${e.message}`, ); }); }; From a81bc5280126f2347e81578fd4cdbebc61eb29c8 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Mon, 18 Jan 2021 16:13:40 +0200 Subject: [PATCH 059/209] Added test for consumer group offset hooks --- plugins/kafka/package.json | 6 +- .../ConsumerGroupOffsets.test.tsx | 21 ++-- .../ConsumerGroupOffsets.tsx | 14 +-- .../__fixtures__/consumer-group-offsets.json | 17 ++++ .../useConsumerGroupsForEntity.test.tsx | 53 ++++++++++ ...ntity.ts => useConsumerGroupsForEntity.ts} | 2 +- ...useConsumerGroupsOffsetsForEntity.test.tsx | 98 +++++++++++++++++++ ...s => useConsumerGroupsOffsetsForEntity.ts} | 11 ++- 8 files changed, 189 insertions(+), 33 deletions(-) create mode 100644 plugins/kafka/src/components/ConsumerGroupOffsets/__fixtures__/consumer-group-offsets.json create mode 100644 plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.test.tsx rename plugins/kafka/src/components/ConsumerGroupOffsets/{useConsumerGroupsFromEntity.ts => useConsumerGroupsForEntity.ts} (94%) create mode 100644 plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.test.tsx rename plugins/kafka/src/components/ConsumerGroupOffsets/{useConsumerGroupOffsets.ts => useConsumerGroupsOffsetsForEntity.ts} (77%) diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index c0a5de2d34..448adf7f91 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -22,14 +22,14 @@ "dependencies": { "@backstage/catalog-model": "^0.6.0", "@backstage/core": "^0.4.4", - "@backstage/theme": "^0.2.2", "@backstage/plugin-catalog": "^0.2.7", + "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "react": "^16.13.1", - "react-router": "6.0.0-beta.0", "react-dom": "^16.13.1", + "react-router": "6.0.0-beta.0", "react-use": "^15.3.3" }, "devDependencies": { @@ -38,10 +38,12 @@ "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", + "@testing-library/react-hooks": "^3.4.2", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "cross-fetch": "^3.0.6", + "jest-when": "^3.1.0", "msw": "^0.21.2" }, "files": [ diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.test.tsx b/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.test.tsx index 629f7a9a7a..ea0f7e1ddb 100644 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.test.tsx +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.test.tsx @@ -17,26 +17,17 @@ import React from 'react'; import { renderInTestApp } from '@backstage/test-utils'; import { ConsumerGroupOffsets } from './ConsumerGroupOffsets'; +import * as data from './__fixtures__/consumer-group-offsets.json'; +import { ConsumerGroupOffsetsResponse } from '../../api/types'; + +const consumerGroupOffsets = data as ConsumerGroupOffsetsResponse; describe('ConsumerGroupOffsets', () => { it('should render consumer group table', async () => { const rendered = await renderInTestApp( {}} />, diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.tsx b/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.tsx index 0f697e4994..448d420bc0 100644 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.tsx +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.tsx @@ -18,8 +18,7 @@ import { Table, TableColumn } from '@backstage/core'; import { Box, Typography } from '@material-ui/core'; import RetryIcon from '@material-ui/icons/Replay'; import React from 'react'; -import { useConsumerGroupFromEntity } from './useConsumerGroupsFromEntity'; -import { useConsumerGroupOffsets } from './useConsumerGroupOffsets'; +import { useConsumerGroupsOffsetsForEntity } from './useConsumerGroupsOffsetsForEntity'; export type TopicPartitionInfo = { topic: string; @@ -98,14 +97,7 @@ export const ConsumerGroupOffsets = ({ }; export const KafkaTopicsForConsumer = () => { - const consumerGroup = useConsumerGroupFromEntity(); - const [tableProps, { retry }] = useConsumerGroupOffsets(consumerGroup); + const [tableProps, { retry }] = useConsumerGroupsOffsetsForEntity(); - return ( - - ); + return ; }; diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/__fixtures__/consumer-group-offsets.json b/plugins/kafka/src/components/ConsumerGroupOffsets/__fixtures__/consumer-group-offsets.json new file mode 100644 index 0000000000..ed4b81c066 --- /dev/null +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/__fixtures__/consumer-group-offsets.json @@ -0,0 +1,17 @@ +{ + "consumerId": "consumer", + "offsets": [ + { + "topic": "topic1", + "partitionId": 1, + "topicOffset": "100", + "groupOffset": "50" + }, + { + "topic": "topic2", + "partitionId": 1, + "topicOffset": "2340", + "groupOffset": "1234" + } + ] +} diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.test.tsx b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.test.tsx new file mode 100644 index 0000000000..44693d5bbc --- /dev/null +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.test.tsx @@ -0,0 +1,53 @@ +/* + * 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 React, { PropsWithChildren } from 'react'; +import { renderHook } from '@testing-library/react-hooks'; +import { useConsumerGroupsForEntity } from './useConsumerGroupsForEntity'; +import { EntityContext } from '@backstage/plugin-catalog'; +import { Entity } from '@backstage/catalog-model'; + +describe('useConsumerGroupOffsets', () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'test', + annotations: { + 'kafka.apache.org/consumer-groups': 'consumer', + }, + }, + spec: { + owner: 'guest', + type: 'Website', + lifecycle: 'development', + }, + }; + + const wrapper = ({ children }: PropsWithChildren<{}>) => { + return ( + + {children} + + ); + }; + + const subject = () => renderHook(useConsumerGroupsForEntity, { wrapper }); + + it('returns correct consumer group for annotation', async () => { + const { result } = subject(); + expect(result.current).toBe('consumer'); + }); +}); diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsFromEntity.ts b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.ts similarity index 94% rename from plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsFromEntity.ts rename to plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.ts index 8bad96b61c..5003c2f5b5 100644 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsFromEntity.ts +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.ts @@ -17,7 +17,7 @@ import { useEntity } from '@backstage/plugin-catalog'; import { KAFKA_CONSUMER_GROUP_ANNOTATION } from '../../constants'; -export const useConsumerGroupFromEntity = () => { +export const useConsumerGroupsForEntity = () => { const { entity } = useEntity(); return entity.metadata.annotations?.[KAFKA_CONSUMER_GROUP_ANNOTATION] ?? ''; diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.test.tsx b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.test.tsx new file mode 100644 index 0000000000..28f0ff7c28 --- /dev/null +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.test.tsx @@ -0,0 +1,98 @@ +/* + * 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 React, { PropsWithChildren } from 'react'; +import { renderHook } from '@testing-library/react-hooks'; +import { EntityContext } from '@backstage/plugin-catalog'; +import { Entity } from '@backstage/catalog-model'; +import * as data from './__fixtures__/consumer-group-offsets.json'; +import { + ConsumerGroupOffsetsResponse, + KafkaApi, + kafkaApiRef, +} from '../../api/types'; +import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; +import { useConsumerGroupsOffsetsForEntity } from './useConsumerGroupsOffsetsForEntity'; +import { when } from 'jest-when'; + +const consumerGroupOffsets = data as ConsumerGroupOffsetsResponse; + +const mockErrorApi: jest.Mocked = { + post: jest.fn(), + error$: jest.fn(), +}; + +const mockKafkaApi: jest.Mocked = { + getConsumerGroupOffsets: jest.fn(), +}; + +describe('useConsumerGroupOffsets', () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'test', + annotations: { + 'kafka.apache.org/consumer-groups': consumerGroupOffsets.consumerId, + }, + }, + spec: { + owner: 'guest', + type: 'Website', + lifecycle: 'development', + }, + }; + + const wrapper = ({ children }: PropsWithChildren<{}>) => { + return ( + + + {children} + + + ); + }; + + const subject = () => + renderHook(useConsumerGroupsOffsetsForEntity, { wrapper }); + + it('returns correct consumer group for annotation', async () => { + when(mockKafkaApi.getConsumerGroupOffsets) + .calledWith(consumerGroupOffsets.consumerId) + .mockResolvedValue(consumerGroupOffsets); + + const { result, waitForNextUpdate } = subject(); + await waitForNextUpdate(); + const [tableProps] = result.current; + + expect(tableProps.consumerGroup).toBe(consumerGroupOffsets.consumerId); + expect(tableProps.topics).toBe(consumerGroupOffsets.offsets); + }); + + it('posts an error to the error api', async () => { + const error = new Error('error!'); + mockKafkaApi.getConsumerGroupOffsets.mockRejectedValueOnce(error); + + const { waitForNextUpdate } = subject(); + await waitForNextUpdate(); + + expect(mockErrorApi.post).toHaveBeenCalledWith(error); + }); +}); diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupOffsets.ts b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.ts similarity index 77% rename from plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupOffsets.ts rename to plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.ts index 74d408fe86..eb7e105989 100644 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupOffsets.ts +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.ts @@ -17,28 +17,31 @@ import { errorApiRef, useApi } from '@backstage/core'; import { useAsyncRetry } from 'react-use'; import { kafkaApiRef } from '../../api/types'; +import { useConsumerGroupsForEntity } from './useConsumerGroupsForEntity'; -export function useConsumerGroupOffsets(groupId: string) { +export const useConsumerGroupsOffsetsForEntity = () => { + const consumerGroup = useConsumerGroupsForEntity(); const api = useApi(kafkaApiRef); const errorApi = useApi(errorApiRef); const { loading, value: topics, retry } = useAsyncRetry(async () => { try { - const response = await api.getConsumerGroupOffsets(groupId); + const response = await api.getConsumerGroupOffsets(consumerGroup); return response.offsets; } catch (e) { errorApi.post(e); throw e; } - }, [api, errorApi, groupId]); + }, [api, errorApi, consumerGroup]); return [ { loading, + consumerGroup, topics, }, { retry, }, ] as const; -} +}; From 02e0db404e253102e2e841c88a8a05ce6e4427e5 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 18 Jan 2021 16:39:03 +0100 Subject: [PATCH 060/209] Make azure validation message more helpful Co-authored-by: blam --- .../components/TemplatePage/TemplatePage.tsx | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index 856ff3d0e2..11c1029c40 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -64,7 +64,6 @@ const OWNER_REPO_SCHEMA = { description: 'Who is going to own this component', }, storePath: { - format: 'storeLocation', type: 'string' as const, title: 'Store path', description: @@ -77,11 +76,6 @@ const OWNER_REPO_SCHEMA = { }, }, }; - -const REPO_FORMAT = { - storeLocation: /[^\/]*\/[^\/]*/, -}; - export const TemplatePage = () => { const errorApi = useApi(errorApiRef); const catalogApi = useApi(catalogApiRef); @@ -184,7 +178,6 @@ export const TemplatePage = () => { { label: 'Choose owner and repo', schema: OWNER_REPO_SCHEMA, - customFormats: REPO_FORMAT, validate: (formData, errors) => { const { storePath } = formData; const parsedUrl = gitParse(storePath); @@ -194,9 +187,15 @@ export const TemplatePage = () => { !parsedUrl.owner || !parsedUrl.name ) { - errors.storePath.addError( - 'The store path should be a complete Git URL to the new repository location. For example https://github.com/backstage/new-repo', - ); + if (parsedUrl.resource === 'dev.azure.com') { + errors.storePath.addError( + "The store path should be formatted like https://dev.azure.com/{org}/{project}/_git/{repo} for Azure URL's", + ); + } else { + errors.storePath.addError( + 'The store path should be a complete Git URL to the new repository location. For example: https://github.com/{owner}/{repo}', + ); + } } return errors; From 8fe334a7ebd938c6676d26e195b7c6f70819e6ef Mon Sep 17 00:00:00 2001 From: Jussi Hallila Date: Mon, 18 Jan 2021 16:40:11 +0100 Subject: [PATCH 061/209] Modify error messages based on PR comment. --- plugins/catalog-import/src/util/useGithubRepos.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/catalog-import/src/util/useGithubRepos.ts b/plugins/catalog-import/src/util/useGithubRepos.ts index db3ee9aa09..6c11ca1e8b 100644 --- a/plugins/catalog-import/src/util/useGithubRepos.ts +++ b/plugins/catalog-import/src/util/useGithubRepos.ts @@ -44,7 +44,7 @@ export function useGithubRepos() { const githubIntegrationConfig = configs.find(v => v.host === hostname); if (!githubIntegrationConfig) { throw new Error( - `Unable to locate github-integration for repo-location: ${location}`, + `Unable to locate github-integration for repo-location, ${location}`, ); } return { @@ -70,7 +70,7 @@ export function useGithubRepos() { githubIntegrationConfig, }) .catch(e => { - throw new Error(`Failed to submit PR to repo: ${e.message}`); + throw new Error(`Failed to submit PR to repo, ${e.message}`); }); await api @@ -78,7 +78,7 @@ export function useGithubRepos() { location: submitPRResponse.location, }) .catch(e => { - throw new Error(`Failed to create repository location: ${e.message}`); + throw new Error(`Failed to create repository location, ${e.message}`); }); return submitPRResponse; @@ -105,7 +105,7 @@ export function useGithubRepos() { }) .catch(e => { throw new Error( - `Failed to inspect repository for existing catalog-info.yaml: ${e.message}`, + `Failed to inspect repository for existing catalog-info.yaml, ${e.message}`, ); }); }; From a33996edcc1cea29337c6d64473d4fdd471219f1 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 18 Jan 2021 16:41:01 +0100 Subject: [PATCH 062/209] Scaffolder: Refactor publish and prepare Co-authored-by: blam --- .../src/scaffolder/stages/helpers.test.ts | 11 +- .../src/scaffolder/stages/helpers.ts | 5 +- .../scaffolder/stages/prepare/azure.test.ts | 15 +- .../src/scaffolder/stages/prepare/azure.ts | 2 +- .../stages/prepare/bitbucket.test.ts | 17 +- .../scaffolder/stages/prepare/github.test.ts | 13 +- .../scaffolder/stages/prepare/gitlab.test.ts | 15 +- .../scaffolder/stages/prepare/preparers.ts | 3 +- .../scaffolder/stages/publish/azure.test.ts | 102 +++++++++-- .../src/scaffolder/stages/publish/azure.ts | 1 + .../stages/publish/bitbucket.test.ts | 41 ++++- .../scaffolder/stages/publish/bitbucket.ts | 78 ++++++-- .../scaffolder/stages/publish/github.test.ts | 84 ++++++--- .../scaffolder/stages/publish/gitlab.test.ts | 73 ++++++-- .../stages/publish/publishers.test.ts | 167 ++++++++---------- .../scaffolder/stages/publish/publishers.ts | 86 ++------- .../src/scaffolder/stages/types.ts | 3 +- 17 files changed, 425 insertions(+), 291 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/helpers.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/helpers.test.ts index 09bd6b7885..d02a0a5ee6 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/helpers.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/helpers.test.ts @@ -30,10 +30,7 @@ describe('Helpers', () => { apiVersion: 'backstage.io/v1alpha1', kind: 'Template', metadata: { - annotations: { - // [LOCATION_ANNOTATION]: - // 'github:https://github.com/benjdlambert/backstage-graphql-template/blob/master/template.yaml', - }, + annotations: {}, name: 'graphql-starter', title: 'GraphQL Service', description: @@ -275,11 +272,11 @@ describe('Helpers', () => { expect(detector('http://derp.org:80/wat')).toBe('gitlab'); expect(detector('https://foo.org/wat')).toBe('gitlab'); expect(detector('http://not.derp.net')).toBe(undefined); - expect(detector('http://derp.net')).toBe('azure/api'); - expect(detector('http://derp.net:8080/wat')).toBe('azure/api'); + expect(detector('http://derp.net')).toBe('azure'); + expect(detector('http://derp.net:8080/wat')).toBe('azure'); expect(detector('http://github.com')).toBe('github'); expect(detector('http://gitlab.com')).toBe('gitlab'); - expect(detector('http://dev.azure.com')).toBe('azure/api'); + expect(detector('http://dev.azure.com')).toBe('azure'); }); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/helpers.ts index 21b63609e8..c3ef01b359 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/helpers.ts @@ -74,7 +74,8 @@ export function makeDeprecatedLocationTypeDetector( // These are installed by default by the integrations hostMap.set('github.com', 'github'); hostMap.set('gitlab.com', 'gitlab'); - hostMap.set('dev.azure.com', 'azure/api'); + hostMap.set('dev.azure.com', 'azure'); + hostMap.set('bitbucket.org', 'bitbucket'); config.getOptionalConfigArray('integrations.github')?.forEach(sub => { hostMap.set(sub.getString('host'), 'github'); @@ -83,7 +84,7 @@ export function makeDeprecatedLocationTypeDetector( hostMap.set(sub.getString('host'), 'gitlab'); }); config.getOptionalConfigArray('integrations.azure')?.forEach(sub => { - hostMap.set(sub.getString('host'), 'azure/api'); + hostMap.set(sub.getString('host'), 'azure'); }); config.getOptionalConfigArray('integrations.bitbucket')?.forEach(sub => { hostMap.set(sub.getString('host'), 'bitbucket'); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts index 38c262eaaa..896d91f4ed 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts @@ -46,7 +46,7 @@ describe('AzurePreparer', () => { metadata: { annotations: { [LOCATION_ANNOTATION]: - 'azure/api:https://dev.azure.com/backstage-org/backstage-project/_git/template-repo?path=%2Ftemplate.yaml', + 'url:https://dev.azure.com/backstage-org/backstage-project/_git/template-repo?path=%2Ftemplate.yaml', }, name: 'graphql-starter', title: 'GraphQL Service', @@ -95,7 +95,7 @@ describe('AzurePreparer', () => { { logger }, ); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, @@ -116,7 +116,7 @@ describe('AzurePreparer', () => { { logger }, ); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, @@ -128,7 +128,7 @@ describe('AzurePreparer', () => { it('calls the clone command with the correct arguments for a repository', async () => { const preparer = new AzurePreparer(new ConfigReader({}), { logger }); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: @@ -141,7 +141,7 @@ describe('AzurePreparer', () => { const preparer = new AzurePreparer(new ConfigReader({}), { logger }); delete mockEntity.spec.path; - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: @@ -154,7 +154,9 @@ describe('AzurePreparer', () => { const preparer = new AzurePreparer(new ConfigReader({}), { logger }); mockEntity.spec.path = './template/test/1/2/3'; - const response = await preparer.prepare(mockEntity); + const response = await preparer.prepare(mockEntity, { + logger: getVoidLogger(), + }); expect(response.split('\\').join('/')).toMatch( /\/template\/test\/1\/2\/3$/, @@ -167,6 +169,7 @@ describe('AzurePreparer', () => { const response = await preparer.prepare(mockEntity, { workingDirectory: '/workDir', + logger: getVoidLogger(), }); expect(response.split('\\').join('/')).toMatch( diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts index b2eab7bb85..35fa02e8ce 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts @@ -62,7 +62,7 @@ export class AzurePreparer implements PreparerBase { const workingDirectory = opts.workingDirectory ?? os.tmpdir(); const logger = opts.logger; - if (!['azure/api', 'url'].includes(protocol)) { + if (!['azure', 'url'].includes(protocol)) { throw new InputError( `Wrong location protocol: ${protocol}, should be 'url'`, ); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts index 0be61a9f82..d55ca7a01d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts @@ -81,7 +81,7 @@ describe('BitbucketPreparer', () => { it('calls the clone command with the correct arguments for a repository', async () => { const preparer = new BitbucketPreparer(new ConfigReader({}), { logger }); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://bitbucket.org/backstage-project/backstage-repo', dir: expect.any(String), @@ -104,7 +104,7 @@ describe('BitbucketPreparer', () => { { logger }, ); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, @@ -116,7 +116,7 @@ describe('BitbucketPreparer', () => { it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { const preparer = new BitbucketPreparer(new ConfigReader({}), { logger }); delete mockEntity.spec.path; - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://bitbucket.org/backstage-project/backstage-repo', dir: expect.any(String), @@ -126,7 +126,9 @@ describe('BitbucketPreparer', () => { it('return the temp directory with the path to the folder if it is specified', async () => { const preparer = new BitbucketPreparer(new ConfigReader({}), { logger }); mockEntity.spec.path = './template/test/1/2/3'; - const response = await preparer.prepare(mockEntity); + const response = await preparer.prepare(mockEntity, { + logger: getVoidLogger(), + }); expect(response.split('\\').join('/')).toMatch( /\/template\/test\/1\/2\/3$/, @@ -148,7 +150,7 @@ describe('BitbucketPreparer', () => { { logger }, ); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, @@ -173,7 +175,7 @@ describe('BitbucketPreparer', () => { { logger }, ); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, @@ -198,7 +200,7 @@ describe('BitbucketPreparer', () => { { logger }, ); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, @@ -212,6 +214,7 @@ describe('BitbucketPreparer', () => { mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { workingDirectory: '/workDir', + logger: getVoidLogger(), }); expect(response.split('\\').join('/')).toMatch( diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts index 6598a8734a..122a1abb4c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts @@ -90,7 +90,7 @@ describe('GitHubPreparer', () => { { logger }, ); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://github.com/benjdlambert/backstage-graphql-template', @@ -101,7 +101,7 @@ describe('GitHubPreparer', () => { const preparer = new GithubPreparer(new ConfigReader({}), { logger }); delete mockEntity.spec.path; - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://github.com/benjdlambert/backstage-graphql-template', @@ -112,7 +112,9 @@ describe('GitHubPreparer', () => { it('return the temp directory with the path to the folder if it is specified', async () => { const preparer = new GithubPreparer(new ConfigReader({}), { logger }); mockEntity.spec.path = './template/test/1/2/3'; - const response = await preparer.prepare(mockEntity); + const response = await preparer.prepare(mockEntity, { + logger: getVoidLogger(), + }); expect(response.split('\\').join('/')).toMatch( /\/template\/test\/1\/2\/3$/, ); @@ -123,6 +125,7 @@ describe('GitHubPreparer', () => { mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { workingDirectory: '/workDir', + logger: getVoidLogger(), }); expect(response.split('\\').join('/')).toMatch( @@ -142,7 +145,7 @@ describe('GitHubPreparer', () => { { logger }, ); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, @@ -161,7 +164,7 @@ describe('GitHubPreparer', () => { { logger }, ); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts index 8cb54ceb7a..c1e42920ae 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts @@ -78,12 +78,12 @@ describe('GitLabPreparer', () => { jest.clearAllMocks(); }); - ['gitlab', 'gitlab/api'].forEach(protocol => { + ['gitlab'].forEach(protocol => { it(`calls the clone command with the correct arguments for a repository using the ${protocol} protocol`, async () => { const preparer = new GitlabPreparer(new ConfigReader({}), { logger }); mockEntity = mockEntityWithProtocol(protocol); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://gitlab.com/benjdlambert/backstage-graphql-template', @@ -107,7 +107,7 @@ describe('GitLabPreparer', () => { ); mockEntity = mockEntityWithProtocol(protocol); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, @@ -127,7 +127,7 @@ describe('GitLabPreparer', () => { ); mockEntity = mockEntityWithProtocol(protocol); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, @@ -141,7 +141,7 @@ describe('GitLabPreparer', () => { mockEntity = mockEntityWithProtocol(protocol); delete mockEntity.spec.path; - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://gitlab.com/benjdlambert/backstage-graphql-template', @@ -153,7 +153,9 @@ describe('GitLabPreparer', () => { const preparer = new GitlabPreparer(new ConfigReader({}), { logger }); mockEntity = mockEntityWithProtocol(protocol); mockEntity.spec.path = './template/test/1/2/3'; - const response = await preparer.prepare(mockEntity); + const response = await preparer.prepare(mockEntity, { + logger: getVoidLogger(), + }); expect(response.split('\\').join('/')).toMatch( /\/template\/test\/1\/2\/3$/, ); @@ -164,6 +166,7 @@ describe('GitLabPreparer', () => { mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { workingDirectory: '/workDir', + logger: getVoidLogger(), }); expect(response.split('\\').join('/')).toMatch( diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts index ff23aa2990..251e8b0ae3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts @@ -85,8 +85,7 @@ export class Preparers implements PreparerBuilder { preparers.register('file', filePreparer); preparers.register('gitlab', gitlabPreparer); - preparers.register('gitlab/api', gitlabPreparer); - preparers.register('azure/api', azurePreparer); + preparers.register('azure', azurePreparer); preparers.register('github', githubPreparer); preparers.register('bitbucket', bitbucketPreparer); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts index f1e0cd6175..9aaee4bcb8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts @@ -15,28 +15,45 @@ */ jest.mock('./helpers'); +jest.mock('azure-devops-node-api', () => ({ + WebApi: jest.fn(), + getPersonalAccessTokenHandler: jest.fn(), +})); + import { AzurePublisher } from './azure'; -import { GitApi } from 'azure-devops-node-api/GitApi'; +import { WebApi } from 'azure-devops-node-api'; import * as helpers from './helpers'; import { getVoidLogger } from '@backstage/backend-common'; - -const { mockGitApi } = require('azure-devops-node-api/GitApi') as { - mockGitApi: { - createRepository: jest.MockedFunction; - }; -}; +import { ConfigReader } from '@backstage/config'; describe('Azure Publisher', () => { - const publisher = new AzurePublisher(new GitApi('', []), 'fake-token'); const logger = getVoidLogger(); - beforeEach(() => { - jest.clearAllMocks(); - }); - describe('publish: createRemoteInAzure', () => { it('should use azure-devops-node-api to create a repo in the given project', async () => { - mockGitApi.createRepository.mockResolvedValue({ + const mockGitClient = { + createRepository: jest.fn(), + }; + const mockGitApi = { + getGitApi: jest.fn().mockReturnValue(mockGitClient), + }; + + ((WebApi as unknown) as jest.Mock).mockImplementation(() => mockGitApi); + + const publisher = new AzurePublisher( + new ConfigReader({ + scaffolder: { + azure: { + api: { + baseUrl: 'https://dev.azure.com/myorg', + token: 'fake-azure-token', + }, + }, + }, + }), + { logger }, + ); + mockGitClient.createRepository.mockResolvedValue({ remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', } as { remoteUrl: string }); @@ -54,7 +71,7 @@ describe('Azure Publisher', () => { catalogInfoUrl: 'https://dev.azure.com/organization/project/_git/repo?path=%2Fcatalog-info.yaml', }); - expect(mockGitApi.createRepository).toHaveBeenCalledWith( + expect(mockGitClient.createRepository).toHaveBeenCalledWith( { name: 'repo', }, @@ -63,7 +80,62 @@ describe('Azure Publisher', () => { expect(helpers.initRepoAndPush).toHaveBeenCalledWith({ dir: '/tmp/test', remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', - auth: { username: 'notempty', password: 'fake-token' }, + auth: { username: 'notempty', password: 'fake-azure-token' }, + logger, + }); + }); + + it('should use azure-devops-node-api with integrations config', async () => { + const mockGitClient = { + createRepository: jest.fn(), + }; + const mockGitApi = { + getGitApi: jest.fn().mockReturnValue(mockGitClient), + }; + + ((WebApi as unknown) as jest.Mock).mockImplementation(() => mockGitApi); + + const publisher = new AzurePublisher( + new ConfigReader({ + integrations: { + azure: [ + { + host: 'dev.azure.com', + token: 'fake-azure-token', + }, + ], + }, + }), + { logger }, + ); + mockGitClient.createRepository.mockResolvedValue({ + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + } as { remoteUrl: string }); + + const result = await publisher.publish({ + values: { + storePath: 'https://dev.azure.com/organization/project/_git/repo', + owner: 'bob', + }, + directory: '/tmp/test', + logger, + }); + + expect(result).toEqual({ + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + catalogInfoUrl: + 'https://dev.azure.com/organization/project/_git/repo?path=%2Fcatalog-info.yaml', + }); + expect(mockGitClient.createRepository).toHaveBeenCalledWith( + { + name: 'repo', + }, + 'project', + ); + expect(helpers.initRepoAndPush).toHaveBeenCalledWith({ + dir: '/tmp/test', + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + auth: { username: 'notempty', password: 'fake-azure-token' }, logger, }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts index 50de3331d8..4172f28f6b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts @@ -83,6 +83,7 @@ export class AzurePublisher implements PublisherBase { project: owner, name, }); + const catalogInfoUrl = `${remoteUrl}?path=%2Fcatalog-info.yaml`; await initRepoAndPush({ diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts index 3b084f1bf9..6cfcc7d69e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts @@ -22,6 +22,7 @@ import { getVoidLogger } from '@backstage/backend-common'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { msw } from '@backstage/test-utils'; +import { ConfigReader } from '@backstage/config'; describe('Bitbucket Publisher', () => { const logger = getVoidLogger(); @@ -59,14 +60,25 @@ describe('Bitbucket Publisher', () => { ); const publisher = new BitbucketPublisher( - 'https://bitbucket.org', - 'fake-user', - 'fake-token', + new ConfigReader({ + integrations: { + bitbucket: [ + { + host: 'bitbucket.org', + username: 'fake-user', + appPassword: 'fake-token', + }, + ], + }, + }), + { + logger: getVoidLogger(), + }, ); const result = await publisher.publish({ values: { - storePath: 'project/repo', + storePath: 'https://bitbucket.org/project/repo', owner: 'bob', }, directory: '/tmp/test', @@ -87,6 +99,7 @@ describe('Bitbucket Publisher', () => { }); }); }); + describe('publish: createRemoteInBitbucketServer', () => { it('should create repo in bitbucket server', async () => { server.use( @@ -117,14 +130,24 @@ describe('Bitbucket Publisher', () => { ); const publisher = new BitbucketPublisher( - 'https://bitbucket.mycompany.com', - 'fake-user', - 'fake-token', + new ConfigReader({ + integrations: { + bitbucket: [ + { + host: 'bitbucket.mycompany.com', + token: 'fake-token', + }, + ], + }, + }), + { + logger: getVoidLogger(), + }, ); const result = await publisher.publish({ values: { - storePath: 'project/repo', + storePath: 'https://bitbucket.mycompany.com/project/repo', owner: 'bob', }, directory: '/tmp/test', @@ -140,7 +163,7 @@ describe('Bitbucket Publisher', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: '/tmp/test', remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo', - auth: { username: 'fake-user', password: 'fake-token' }, + auth: { username: 'x-token-auth', password: 'fake-token' }, logger: logger, }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts index b07e77e49a..4cc7654136 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts @@ -25,10 +25,14 @@ import { import { Logger } from 'winston'; import gitUrlParse from 'git-url-parse'; +// TODO(blam): We should probably start to use a bitbucket client here that we can change +// the baseURL to point at on-prem or public bitbucket versions like we do for +// github and ghe export class BitbucketPublisher implements PublisherBase { private readonly host?: string; private readonly username?: string; private readonly token?: string; + private readonly appPassword?: string; private readonly integrations: BitbucketIntegrationConfig[]; constructor(config: Config, { logger }: { logger: Logger }) { @@ -64,6 +68,15 @@ export class BitbucketPublisher implements PublisherBase { "DEPRECATION: Using the apiBaseUrl format under 'scaffolder.bitbucket.api.username' will not be respected in future releases. Please consider using integrations config instead", ); } + + this.appPassword = config.getOptionalString( + 'scaffolder.bitbucket.api.appPassword', + ); + if (this.appPassword) { + logger.warn( + "DEPRECATION: Using the appPassword format under 'scaffolder.bitbucket.api.appassword' will not be respected in future releases. Please consider using integrations config instead", + ); + } } async publish({ @@ -71,20 +84,20 @@ export class BitbucketPublisher implements PublisherBase { directory, logger, }: PublisherOptions): Promise { - const { resource: host, owner: project, name } = gitUrlParse( + const { resource: hostname, owner: project, name } = gitUrlParse( values.storePath, ); - const token = this.getToken(host); - if (!token) { - throw new Error('No token provided to create the remote repository'); + const token = this.getToken(hostname); + const appPassword = this.getAppPassword(hostname); + const username = this.getUsername(hostname); + + if (!username && !appPassword && !token) { + throw new Error('Cannot create repository without bitbucket credentials'); } - const username = this.getUsername(host); - if (!username) { - throw new Error('No username provided to create the remote repository'); - } - const apiUrl = this.getHost(host); - if (!apiUrl) { + const host = this.getHost(hostname); + + if (!host) { throw new Error('No host provided to create the remote repository'); } @@ -94,14 +107,17 @@ export class BitbucketPublisher implements PublisherBase { name, description, host, + username, + token, + appPassword, }); await initRepoAndPush({ dir: directory, remoteUrl: result.remoteUrl, auth: { - username: username, - password: token, + username: username ? username : 'x-token-auth', + password: appPassword ? appPassword : token ?? '', }, logger, }); @@ -109,12 +125,15 @@ export class BitbucketPublisher implements PublisherBase { } private async createRemote(opts: { + username?: string; + token?: string; + appPassword?: string; project: string; name: string; description: string; host: string; }): Promise { - if (opts.host === 'https://bitbucket.org') { + if (opts.host === 'bitbucket.org') { return this.createBitbucketCloudRepository(opts); } return this.createBitbucketServerRepository(opts); @@ -124,11 +143,22 @@ export class BitbucketPublisher implements PublisherBase { project: string; name: string; description: string; + username?: string; + appPassword?: string; }): Promise { - const { project, name, description } = opts; + const { project, name, description, username, appPassword } = opts; + if (!appPassword) { + throw new Error( + 'appPassword is required to create the remote repository', + ); + } + + if (!username) { + throw new Error('username is required to create the remote repository'); + } let response: Response; - const buffer = Buffer.from(`${this.username}:${this.token}`, 'utf8'); + const buffer = Buffer.from(`${username}:${appPassword}`, 'utf8'); const options: RequestInit = { method: 'POST', @@ -169,8 +199,13 @@ export class BitbucketPublisher implements PublisherBase { project: string; name: string; description: string; + token?: string; + host: string; }): Promise { - const { project, name, description } = opts; + const { project, name, description, token, host } = opts; + if (!token) { + throw new Error('No token provided to create the remote repository'); + } let response: Response; const options: RequestInit = { @@ -180,13 +215,13 @@ export class BitbucketPublisher implements PublisherBase { description: description, }), headers: { - Authorization: `Bearer ${this.token}`, + Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, }; try { response = await fetch( - `${this.host}/rest/api/1.0/projects/${project}/repos`, + `https://${host}/rest/api/1.0/projects/${project}/repos`, options, ); } catch (e) { @@ -216,6 +251,13 @@ export class BitbucketPublisher implements PublisherBase { ); } + private getAppPassword(host: string): string | undefined { + return ( + this.appPassword || + this.integrations.find(c => c.host === host)?.appPassword + ); + } + private getHost(host: string): string | undefined { return this.host || this.integrations.find(c => c.host === host)?.host; } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts index 22f1cd3172..effed4763d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts @@ -26,6 +26,7 @@ import { import { GithubPublisher } from './github'; import { initRepoAndPush } from './helpers'; import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; const { mockGithubClient } = require('@octokit/rest') as { mockGithubClient: { @@ -42,11 +43,21 @@ describe('GitHub Publisher', () => { }); describe('with public repo visibility', () => { - const publisher = new GithubPublisher({ - client: new Octokit(), - token: 'abc', - repoVisibility: 'public', - }); + const publisher = new GithubPublisher( + new ConfigReader({ + integrations: { + github: [ + { + token: 'fake-token', + host: 'github.com', + }, + ], + }, + }), + { + logger, + }, + ); describe('publish: createRemoteInGithub', () => { it('should use octokit to create a repo in an organisation if the organisation property is set', async () => { @@ -63,7 +74,7 @@ describe('GitHub Publisher', () => { const result = await publisher.publish({ values: { - storePath: 'blam/test', + storePath: 'https://github.com/blam/test', owner: 'bob', access: 'blam/team', }, @@ -94,7 +105,7 @@ describe('GitHub Publisher', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: '/tmp/test', remoteUrl: 'https://github.com/backstage/backstage.git', - auth: { username: 'abc', password: 'x-oauth-basic' }, + auth: { username: 'fake-token', password: 'x-oauth-basic' }, logger, }); }); @@ -113,7 +124,7 @@ describe('GitHub Publisher', () => { const result = await publisher.publish({ values: { - storePath: 'blam/test', + storePath: 'https://github.com/blam/test', owner: 'bob', access: 'blam', }, @@ -137,7 +148,7 @@ describe('GitHub Publisher', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: '/tmp/test', remoteUrl: 'https://github.com/backstage/backstage.git', - auth: { username: 'abc', password: 'x-oauth-basic' }, + auth: { username: 'fake-token', password: 'x-oauth-basic' }, logger, }); }); @@ -157,7 +168,7 @@ describe('GitHub Publisher', () => { const result = await publisher.publish({ values: { - storePath: 'blam/test', + storePath: 'https://github.com/blam/test', owner: 'bob', access: 'bob', description: 'description', @@ -187,18 +198,26 @@ describe('GitHub Publisher', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: '/tmp/test', remoteUrl: 'https://github.com/backstage/backstage.git', - auth: { username: 'abc', password: 'x-oauth-basic' }, + auth: { username: 'fake-token', password: 'x-oauth-basic' }, logger, }); }); }); describe('with internal repo visibility', () => { - const publisher = new GithubPublisher({ - client: new Octokit(), - token: 'abc', - repoVisibility: 'internal', - }); + const publisher = new GithubPublisher( + new ConfigReader({ + integrations: { + github: [{ host: 'github.com', token: 'fake-token' }], + }, + scaffolder: { + github: { + visibility: 'internal', + }, + }, + }), + { logger }, + ); it('creates a private repository in the organization with visibility set to internal', async () => { mockGithubClient.repos.createInOrg.mockResolvedValue({ @@ -215,7 +234,7 @@ describe('GitHub Publisher', () => { const result = await publisher.publish({ values: { isOrg: true, - storePath: 'blam/test', + storePath: 'https://github.com/blam/test', owner: 'bob', }, directory: '/tmp/test', @@ -236,18 +255,33 @@ describe('GitHub Publisher', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: '/tmp/test', remoteUrl: 'https://github.com/backstage/backstage.git', - auth: { username: 'abc', password: 'x-oauth-basic' }, + auth: { username: 'fake-token', password: 'x-oauth-basic' }, logger, }); }); }); describe('private visibility in a user account', () => { - const publisher = new GithubPublisher({ - client: new Octokit(), - token: 'abc', - repoVisibility: 'private', - }); + const publisher = new GithubPublisher( + new ConfigReader({ + integrations: { + github: [ + { + token: 'fake-token', + host: 'github.com', + }, + ], + }, + scaffolder: { + github: { + visibility: 'private', + }, + }, + }), + { + logger, + }, + ); it('creates a private repository', async () => { mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ @@ -263,7 +297,7 @@ describe('GitHub Publisher', () => { const result = await publisher.publish({ values: { - storePath: 'blam/test', + storePath: 'https://github.com/blam/test', owner: 'bob', }, directory: '/tmp/test', @@ -284,7 +318,7 @@ describe('GitHub Publisher', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: '/tmp/test', remoteUrl: 'https://github.com/backstage/backstage.git', - auth: { username: 'abc', password: 'x-oauth-basic' }, + auth: { username: 'fake-token', password: 'x-oauth-basic' }, logger, }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts index 4894d99291..67887f579f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts @@ -14,33 +14,56 @@ * limitations under the License. */ -jest.mock('@gitbeaker/node'); +jest.mock('@gitbeaker/node', () => ({ + Gitlab: jest.fn(), +})); + jest.mock('./helpers'); import { GitlabPublisher } from './gitlab'; -import { Gitlab as GitlabAPI } from '@gitbeaker/core'; import { Gitlab } from '@gitbeaker/node'; import { initRepoAndPush } from './helpers'; import { getVoidLogger } from '@backstage/backend-common'; - -const { mockGitlabClient } = require('@gitbeaker/node') as { - mockGitlabClient: { - Namespaces: jest.Mocked; - Projects: jest.Mocked; - Users: jest.Mocked; - }; -}; +import { ConfigReader } from '@backstage/config'; describe('GitLab Publisher', () => { const logger = getVoidLogger(); - const publisher = new GitlabPublisher(new Gitlab({}), 'fake-token'); + const mockGitlabClient = { + Namespaces: { + show: jest.fn(), + }, + Projects: { + create: jest.fn(), + }, + Users: { + current: jest.fn(), + }, + }; beforeEach(() => { jest.clearAllMocks(); + + ((Gitlab as unknown) as jest.Mock).mockImplementation( + () => mockGitlabClient, + ); }); describe('publish: createRemoteInGitLab', () => { it('should use gitbeaker to create a repo in a namespace if the namespace property is set', async () => { + const publisher = new GitlabPublisher( + new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'fake-token', + }, + ], + }, + }), + { logger }, + ); + mockGitlabClient.Namespaces.show.mockResolvedValue({ id: 42, } as { id: number }); @@ -51,14 +74,17 @@ describe('GitLab Publisher', () => { const result = await publisher.publish({ values: { isOrg: true, - storePath: 'bloum/blam/test', + storePath: 'https://gitlab.com/blam/test', owner: 'bob', }, directory: '/tmp/test', logger, }); - expect(result).toEqual({ remoteUrl: 'mockclone' }); + expect(result).toEqual({ + remoteUrl: 'mockclone', + catalogInfoUrl: 'mockclone', + }); expect(mockGitlabClient.Projects.create).toHaveBeenCalledWith({ namespace_id: 42, name: 'test', @@ -72,6 +98,20 @@ describe('GitLab Publisher', () => { }); it('should use gitbeaker to create a repo in the authed user if the namespace property is not set', async () => { + const publisher = new GitlabPublisher( + new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'fake-token', + }, + ], + }, + }), + { logger }, + ); + mockGitlabClient.Namespaces.show.mockResolvedValue({}); mockGitlabClient.Users.current.mockResolvedValue({ id: 21, @@ -82,14 +122,17 @@ describe('GitLab Publisher', () => { const result = await publisher.publish({ values: { - storePath: 'bloum/blam/test', + storePath: 'https://gitlab.com/blam/test', owner: 'bob', }, directory: '/tmp/test', logger, }); - expect(result).toEqual({ remoteUrl: 'mockclone' }); + expect(result).toEqual({ + remoteUrl: 'mockclone', + catalogInfoUrl: 'mockclone', + }); expect(mockGitlabClient.Users.current).toHaveBeenCalled(); expect(mockGitlabClient.Projects.create).toHaveBeenCalledWith({ namespace_id: 21, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.test.ts index b8181b0134..fdbc283f67 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.test.ts @@ -14,120 +14,97 @@ * limitations under the License. */ import { Publishers } from './publishers'; -import { - LOCATION_ANNOTATION, - TemplateEntityV1alpha1, -} from '@backstage/catalog-model'; import { GithubPublisher } from './github'; -import { Octokit } from '@octokit/rest'; +import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { AzurePublisher } from './azure'; +import { GitlabPublisher } from './gitlab'; +import { BitbucketPublisher } from './bitbucket'; jest.mock('@octokit/rest'); describe('Publishers', () => { - const mockTemplate: TemplateEntityV1alpha1 = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Template', - metadata: { - annotations: { - [LOCATION_ANNOTATION]: - 'github:https://github.com/benjdlambert/backstage-graphql-template/blob/master/template.yaml', - }, - name: 'graphql-starter', - title: 'GraphQL Service', - description: - 'A GraphQL starter template for backstage to get you up and running\nthe best pracices with GraphQL\n', - uid: '9cf16bad-16e0-4213-b314-c4eec773c50b', - etag: 'ZTkxMjUxMjUtYWY3Yi00MjU2LWFkYWMtZTZjNjU5ZjJhOWM2', - generation: 1, - }, - spec: { - type: 'website', - templater: 'cookiecutter', - path: './template', - schema: { - $schema: 'http://json-schema.org/draft-07/schema#', - required: ['storePath', 'owner'], - properties: { - owner: { - type: 'string', - title: 'Owner', - description: 'Who is going to own this component', - }, - storePath: { - type: 'string', - title: 'Store path', - description: 'GitHub store path in org/repo format', - }, - }, - }, - }, - }; - it('should throw an error when the publisher for the source location is not registered', () => { const publishers = new Publishers(); - expect(() => publishers.get(mockTemplate)).toThrow( + expect(() => + publishers.get('https://github.com/org/repo', { + logger: getVoidLogger(), + }), + ).toThrow( expect.objectContaining({ - message: 'No publisher registered for type: "github"', + message: + 'No matching publisher detected for "https://github.com/org/repo". Please make sure this host is registered in the integration config', }), ); }); - it('should return the correct preparer when the source matches', () => { - const publishers = new Publishers(); - const publisher = new GithubPublisher({ - client: new Octokit(), - token: 'fake', - repoVisibility: 'public', + it('should return the correct preparer when the source matches for github', async () => { + const publishers = await Publishers.fromConfig(new ConfigReader({}), { + logger: getVoidLogger(), }); - publishers.register('github', publisher); - expect(publishers.get(mockTemplate)).toBe(publisher); + expect( + publishers.get('https://github.com/org/repo', { + logger: getVoidLogger(), + }), + ).toBeInstanceOf(GithubPublisher); }); - it('should throw an error if the metadata tag does not exist in the entity', () => { - const brokenTemplate: TemplateEntityV1alpha1 = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Template', - metadata: { - annotations: {}, - name: 'react-ssr-template', - title: 'React SSR Template', - description: - 'Next.js application skeleton for creating isomorphic web applications.', - uid: '7357f4c5-aa58-4a1e-9670-18931eef771f', - etag: 'YWUxZWQyY2EtZDkxMC00MDM0LWI0ODAtMDgwMWY0YzdlMWIw', - generation: 1, - }, - spec: { - type: 'website', - templater: 'cookiecutter', - path: '.', - schema: { - $schema: 'http://json-schema.org/draft-07/schema#', - required: ['storePath', 'owner'], - properties: { - owner: { - type: 'string', - title: 'Owner', - description: 'Who is going to own this component', - }, - storePath: { - type: 'string', - title: 'Store path', - description: 'GitHub store path in org/repo format', - }, - }, - }, - }, - }; + it('should return the correct preparer when the source matches for azure', async () => { + const publishers = await Publishers.fromConfig(new ConfigReader({}), { + logger: getVoidLogger(), + }); - const publishers = new Publishers(); - - expect(() => publishers.get(brokenTemplate)).toThrow( - expect.objectContaining({ - message: expect.stringContaining('No location annotation provided'), + expect( + publishers.get('https://dev.azure.com/org/project/_git/repo', { + logger: getVoidLogger(), }), + ).toBeInstanceOf(AzurePublisher); + }); + + it('should return the correct preparer when the source matches for bitbucket', async () => { + const publishers = await Publishers.fromConfig(new ConfigReader({}), { + logger: getVoidLogger(), + }); + + expect( + publishers.get('https://bitbucket.org/owner/repo', { + logger: getVoidLogger(), + }), + ).toBeInstanceOf(BitbucketPublisher); + }); + + it('should return the correct preparer when the source matches for gitlab', async () => { + const publishers = await Publishers.fromConfig(new ConfigReader({}), { + logger: getVoidLogger(), + }); + + expect( + publishers.get('https://gitlab.com/owner/repo', { + logger: getVoidLogger(), + }), + ).toBeInstanceOf(GitlabPublisher); + }); + + it('should respect registrations for custom URLs for providers using the integrations config', async () => { + const publishers = await Publishers.fromConfig( + new ConfigReader({ + integrations: { + github: [ + { host: 'my.special.github.enterprise.thing', token: 'lolghe' }, + ], + }, + }), + { + logger: getVoidLogger(), + }, ); + + expect( + publishers.get('https://my.special.github.enterprise.thing/org/repo', { + logger: getVoidLogger(), + }), + ).toBeInstanceOf(GithubPublisher); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts index 78cc85a6c4..1fafc33c68 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts @@ -52,7 +52,7 @@ export class Publishers implements PublisherBuilder { const publisher = this.publisherMap.get(protocol as RemoteProtocol); if (!publisher) { throw new Error( - `Failed to detect publisher type. Unable to determine integration type for location "${location}". ` + + `Failed to detect publisher type. Unable to determine integration type for location "${protocol}". ` + "Please add appropriate configuration to the 'integrations' configuration section", ); } @@ -69,85 +69,19 @@ export class Publishers implements PublisherBuilder { const typeDetector = makeDeprecatedLocationTypeDetector(config); const publishers = new Publishers(typeDetector); - const githubConfig = config.getOptionalConfig('scaffolder.github'); - if (githubConfig) { - try { - const githubPublisher = new GithubPublisher(config, { logger }); - publishers.register('file', githubPublisher); - publishers.register('github', githubPublisher); - } catch (e) { - const providerName = 'github'; - if (process.env.NODE_ENV !== 'development') { - throw new Error( - `Failed to initialize ${providerName} scaffolding provider, ${e.message}`, - ); - } + const githubPublisher = new GithubPublisher(config, { logger }); + publishers.register('file', githubPublisher); + publishers.register('github', githubPublisher); - logger.warn( - `Skipping ${providerName} scaffolding provider, ${e.message}`, - ); - } - } + const gitLabPublisher = new GitlabPublisher(config, { logger }); + publishers.register('gitlab', gitLabPublisher); - const gitLabConfig = config.getOptionalConfig('scaffolder.gitlab'); - if (gitLabConfig) { - try { - const gitLabPublisher = new GitlabPublisher(config, { logger }); - publishers.register('gitlab', gitLabPublisher); - publishers.register('gitlab/api', gitLabPublisher); - } catch (e) { - const providerName = 'gitlab'; - if (process.env.NODE_ENV !== 'development') { - throw new Error( - `Failed to initialize ${providerName} scaffolding provider, ${e.message}`, - ); - } + const azurePublisher = new AzurePublisher(config, { logger }); + publishers.register('azure', azurePublisher); - logger.warn( - `Skipping ${providerName} scaffolding provider, ${e.message}`, - ); - } - } + const bitbucketPublisher = new BitbucketPublisher(config, { logger }); + publishers.register('bitbucket', bitbucketPublisher); - const azureConfig = config.getOptionalConfig('scaffolder.azure'); - if (azureConfig) { - try { - const azurePublisher = new AzurePublisher(config, { logger }); - publishers.register('azure/api', azurePublisher); - } catch (e) { - const providerName = 'azure'; - if (process.env.NODE_ENV !== 'development') { - throw new Error( - `Failed to initialize ${providerName} scaffolding provider, ${e.message}`, - ); - } - - logger.warn( - `Skipping ${providerName} scaffolding provider, ${e.message}`, - ); - } - } - - const bitbucketConfig = config.getOptionalConfig( - 'scaffolder.bitbucket.api', - ); - if (bitbucketConfig) { - try { - const bitbucketPublisher = new BitbucketPublisher(config, { logger }); - publishers.register('bitbucket', bitbucketPublisher); - } catch (e) { - const providerName = 'bitbucket'; - if (process.env.NODE_ENV !== 'development') { - throw new Error( - `Failed to initialize ${providerName} scaffolding provider, ${e.message}`, - ); - } - - logger.warn( - `Skipping ${providerName} scaffolding provider, ${e.message}`, - ); - } - } return publishers; } } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/types.ts b/plugins/scaffolder-backend/src/scaffolder/stages/types.ts index 56111337fe..7f12eb71db 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/types.ts @@ -17,6 +17,5 @@ export type RemoteProtocol = | 'file' | 'github' | 'gitlab' - | 'gitlab/api' - | 'azure/api' + | 'azure' | 'bitbucket'; From 46a3fea2ba3cec45284fcd4c114800b8ef137f8f Mon Sep 17 00:00:00 2001 From: Jussi Hallila Date: Mon, 18 Jan 2021 19:37:50 +0100 Subject: [PATCH 063/209] Fixing erroneus test setup. --- plugins/catalog-import/package.json | 2 +- plugins/catalog-import/src/api/CatalogImportClient.ts | 5 ++--- plugins/catalog-import/src/setupTests.ts | 3 --- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 5e601ad896..dfd5414ad1 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -39,6 +39,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "@octokit/rest": "^18.0.12", + "cross-fetch": "^3.0.6", "git-url-parse": "^11.4.3", "react": "^16.13.1", "react-dom": "^16.13.1", @@ -57,7 +58,6 @@ "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "cross-fetch": "^3.0.6", "msw": "^0.21.2" }, "files": [ diff --git a/plugins/catalog-import/src/api/CatalogImportClient.ts b/plugins/catalog-import/src/api/CatalogImportClient.ts index 4ce25e8fc2..385fecd1ab 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.ts @@ -19,6 +19,7 @@ import { DiscoveryApi, OAuthApi, ConfigApi } from '@backstage/core'; import { CatalogImportApi } from './CatalogImportApi'; import { PartialEntity } from '../util/types'; import { GitHubIntegrationConfig } from '@backstage/integration'; +import fetch from 'cross-fetch'; export class CatalogImportClient implements CatalogImportApi { private readonly discoveryApi: DiscoveryApi; @@ -133,9 +134,7 @@ export class CatalogImportClient implements CatalogImportApi { exists, }; } - return Promise.resolve({ - exists, - }); + return { exists }; } async submitPrToRepo({ diff --git a/plugins/catalog-import/src/setupTests.ts b/plugins/catalog-import/src/setupTests.ts index fba7d7a957..825bcd4115 100644 --- a/plugins/catalog-import/src/setupTests.ts +++ b/plugins/catalog-import/src/setupTests.ts @@ -15,6 +15,3 @@ */ import '@testing-library/jest-dom'; -import fetch from 'cross-fetch'; - -global.fetch = fetch; From 68adcb0c4219c34c5306d324238ba97188e307bc Mon Sep 17 00:00:00 2001 From: Jussi Hallila Date: Mon, 18 Jan 2021 20:09:13 +0100 Subject: [PATCH 064/209] Reorder package.json and rerun install for lockfile. --- plugins/catalog-import/package.json | 2 +- yarn.lock | 23 ++++++++++++++--------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index dfd5414ad1..5931d59a0a 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -32,8 +32,8 @@ "dependencies": { "@backstage/catalog-model": "^0.6.1", "@backstage/core": "^0.4.4", - "@backstage/plugin-catalog": "^0.2.11", "@backstage/integration": "^0.2.0", + "@backstage/plugin-catalog": "^0.2.11", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/yarn.lock b/yarn.lock index 6710c7e8e7..88938f86d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2398,9 +2398,11 @@ to-fast-properties "^2.0.0" "@backstage/catalog-model@^0.2.0": - version "0.6.1" + version "0.2.0" + resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.2.0.tgz#e3fe2a4ddeb6a9b6ec480c80cb2b9c39cb245576" + integrity sha512-Y1ocdRpBlxK/VrJQjHlQd0bgADECd1B2NRjwd8ss46ibT5hwLvMOfD80+Fa7oPLu0ktJrH4lq0pNIIJIml48zA== dependencies: - "@backstage/config" "^0.1.2" + "@backstage/config" "^0.1.1" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" json-schema "^0.2.5" @@ -2409,9 +2411,11 @@ yup "^0.29.3" "@backstage/catalog-model@^0.3.0": - version "0.6.1" + version "0.3.1" + resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.3.1.tgz#45d08e2f333c9c566b2bf2629fd707fe989bb404" + integrity sha512-9XhV7c4rmVW+Yzj2PiwTQ7DsegWGB3C4ELsDRExuEVZONdqNcC02cyJtrt3fT5F31ZS3tHkB9bMUymFOBLqUSA== dependencies: - "@backstage/config" "^0.1.2" + "@backstage/config" "^0.1.1" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" json-schema "^0.2.5" @@ -2420,16 +2424,17 @@ yup "^0.29.3" "@backstage/core@^0.3.0": - version "0.4.4" + version "0.3.2" + resolved "https://registry.npmjs.org/@backstage/core/-/core-0.3.2.tgz#a8209126d5076cf4a8b9bd632fe4e5e2edb62916" + integrity sha512-i5d+Wh8js4qEWoAsPY5L7HVSWpumr1OhfF2dUCGYdyW6AMqVJPca6+n6zp1Rg2CO+J9norp44XAVVCbyhtUpig== dependencies: - "@backstage/config" "^0.1.2" - "@backstage/core-api" "^0.2.8" - "@backstage/theme" "^0.2.2" + "@backstage/config" "^0.1.1" + "@backstage/core-api" "^0.2.1" + "@backstage/theme" "^0.2.1" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" "@material-ui/lab" "4.0.0-alpha.45" "@types/dagre" "^0.7.44" - "@types/prop-types" "^15.7.3" "@types/react" "^16.9" "@types/react-sparklines" "^1.7.0" classnames "^2.2.6" From b1c82487dd1cbbba95032bc4463aa2a6f358c551 Mon Sep 17 00:00:00 2001 From: Jussi Hallila Date: Mon, 18 Jan 2021 20:47:19 +0100 Subject: [PATCH 065/209] Update yarn lock. --- yarn.lock | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/yarn.lock b/yarn.lock index 88938f86d6..6710c7e8e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2398,11 +2398,9 @@ to-fast-properties "^2.0.0" "@backstage/catalog-model@^0.2.0": - version "0.2.0" - resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.2.0.tgz#e3fe2a4ddeb6a9b6ec480c80cb2b9c39cb245576" - integrity sha512-Y1ocdRpBlxK/VrJQjHlQd0bgADECd1B2NRjwd8ss46ibT5hwLvMOfD80+Fa7oPLu0ktJrH4lq0pNIIJIml48zA== + version "0.6.1" dependencies: - "@backstage/config" "^0.1.1" + "@backstage/config" "^0.1.2" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" json-schema "^0.2.5" @@ -2411,11 +2409,9 @@ yup "^0.29.3" "@backstage/catalog-model@^0.3.0": - version "0.3.1" - resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.3.1.tgz#45d08e2f333c9c566b2bf2629fd707fe989bb404" - integrity sha512-9XhV7c4rmVW+Yzj2PiwTQ7DsegWGB3C4ELsDRExuEVZONdqNcC02cyJtrt3fT5F31ZS3tHkB9bMUymFOBLqUSA== + version "0.6.1" dependencies: - "@backstage/config" "^0.1.1" + "@backstage/config" "^0.1.2" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" json-schema "^0.2.5" @@ -2424,17 +2420,16 @@ yup "^0.29.3" "@backstage/core@^0.3.0": - version "0.3.2" - resolved "https://registry.npmjs.org/@backstage/core/-/core-0.3.2.tgz#a8209126d5076cf4a8b9bd632fe4e5e2edb62916" - integrity sha512-i5d+Wh8js4qEWoAsPY5L7HVSWpumr1OhfF2dUCGYdyW6AMqVJPca6+n6zp1Rg2CO+J9norp44XAVVCbyhtUpig== + version "0.4.4" dependencies: - "@backstage/config" "^0.1.1" - "@backstage/core-api" "^0.2.1" - "@backstage/theme" "^0.2.1" + "@backstage/config" "^0.1.2" + "@backstage/core-api" "^0.2.8" + "@backstage/theme" "^0.2.2" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" "@material-ui/lab" "4.0.0-alpha.45" "@types/dagre" "^0.7.44" + "@types/prop-types" "^15.7.3" "@types/react" "^16.9" "@types/react-sparklines" "^1.7.0" classnames "^2.2.6" From e7846659e01726a69253a82002ccc61a637f7ca7 Mon Sep 17 00:00:00 2001 From: Ihor Dvoretskyi Date: Mon, 18 Jan 2021 22:28:04 +0200 Subject: [PATCH 066/209] fossa.yml cleanup Signed-off-by: Ihor Dvoretskyi --- .github/workflows/fossa.yml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml index 854c52832c..7707c355de 100644 --- a/.github/workflows/fossa.yml +++ b/.github/workflows/fossa.yml @@ -11,19 +11,10 @@ jobs: strategy: matrix: - node-version: [10.x, 12.x, 14.x, 15.x] + node-version: [14.x] steps: - uses: actions/checkout@v2 - - uses: borales/actions-yarn@v2.0.0 - with: - cmd: install - - uses: borales/actions-yarn@v2.0.0 - with: - cmd: build - - uses: borales/actions-yarn@v2.0.0 - with: - cmd: test # Runs a set of commands to initialize and analyze with FOSSA - name: run FOSSA analysis env: From 2305296073b4a32ca096ceba46dd49c66173385f Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Sat, 16 Jan 2021 22:10:43 +0100 Subject: [PATCH 067/209] backend-common: Support SHA based caching in URL Reader's readTree readTree now takes a SHA in options, and throws a NotModifiedError if the SHA matches with the latest commit SHA on the tree. readTree response now contains a new 'sha' parameter. Also, the default branch will now be used if no branch is provided in the url's target --- catalog-info.yaml | 2 +- packages/backend-common/src/errors.ts | 5 + .../src/reading/AzureUrlReader.ts | 17 +- .../src/reading/BitbucketUrlReader.ts | 17 +- .../src/reading/GithubUrlReader.test.ts | 272 +++++++++++------- .../src/reading/GithubUrlReader.ts | 81 ++++-- .../src/reading/GitlabUrlReader.ts | 17 +- .../src/reading/UrlReaderPredicateMux.ts | 7 +- .../src/reading/__fixtures__/mock-main.tar.gz | Bin 0 -> 230 bytes .../src/reading/__fixtures__/repo.tar.gz | Bin 232 -> 0 bytes .../reading/tree/ReadTreeResponseFactory.ts | 10 +- .../reading/tree/TarArchiveResponse.test.ts | 16 +- .../src/reading/tree/TarArchiveResponse.ts | 4 +- .../src/reading/tree/ZipArchiveResponse.ts | 4 +- packages/backend-common/src/reading/types.ts | 19 +- packages/techdocs-common/src/helpers.test.ts | 1 + 16 files changed, 305 insertions(+), 167 deletions(-) create mode 100644 packages/backend-common/src/reading/__fixtures__/mock-main.tar.gz delete mode 100644 packages/backend-common/src/reading/__fixtures__/repo.tar.gz diff --git a/catalog-info.yaml b/catalog-info.yaml index 2144d1709c..7e60af5755 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -6,7 +6,7 @@ metadata: Backstage is an open-source developer portal that puts the developer experience first. annotations: github.com/project-slug: backstage/backstage - backstage.io/techdocs-ref: url:https://github.com/backstage/backstage/tree/master + backstage.io/techdocs-ref: url:https://github.com/backstage/backstage lighthouse.com/website-url: https://backstage.io spec: type: library diff --git a/packages/backend-common/src/errors.ts b/packages/backend-common/src/errors.ts index b68dc8e3f0..39703127e6 100644 --- a/packages/backend-common/src/errors.ts +++ b/packages/backend-common/src/errors.ts @@ -75,3 +75,8 @@ export class NotFoundError extends CustomErrorBase {} * resource. */ export class ConflictError extends CustomErrorBase {} + +/** + * The requested resource has not changed since last request. + */ +export class NotModifiedError extends CustomErrorBase {} diff --git a/packages/backend-common/src/reading/AzureUrlReader.ts b/packages/backend-common/src/reading/AzureUrlReader.ts index ea716a1d4a..4535b329d7 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.ts @@ -75,22 +75,27 @@ export class AzureUrlReader implements UrlReader { url: string, options?: ReadTreeOptions, ): Promise { - const response = await fetch( + const archiveAzureResponse = await fetch( getAzureDownloadUrl(url), getAzureRequestOptions(this.options, { Accept: 'application/zip' }), ); - if (!response.ok) { - const message = `Failed to read tree from ${url}, ${response.status} ${response.statusText}`; - if (response.status === 404) { + if (!archiveAzureResponse.ok) { + const message = `Failed to read tree from ${url}, ${archiveAzureResponse.status} ${archiveAzureResponse.statusText}`; + if (archiveAzureResponse.status === 404) { throw new NotFoundError(message); } throw new Error(message); } - return this.deps.treeResponseFactory.fromZipArchive({ - stream: (response.body as unknown) as Readable, + const archiveResponse = await this.deps.treeResponseFactory.fromZipArchive({ + stream: (archiveAzureResponse.body as unknown) as Readable, filter: options?.filter, }); + + const response = archiveResponse as ReadTreeResponse; + // TODO: Just a placeholder for now. + response.sha = ''; + return response; } toString() { diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.ts b/packages/backend-common/src/reading/BitbucketUrlReader.ts index 4578436521..7f462e5bf0 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.ts @@ -108,13 +108,13 @@ export class BitbucketUrlReader implements UrlReader { const isHosted = resource === 'bitbucket.org'; const downloadUrl = await getBitbucketDownloadUrl(url, this.config); - const response = await fetch( + const archiveBitbucketResponse = await fetch( downloadUrl, getBitbucketRequestOptions(this.config), ); - if (!response.ok) { - const message = `Failed to read tree from ${url}, ${response.status} ${response.statusText}`; - if (response.status === 404) { + if (!archiveBitbucketResponse.ok) { + const message = `Failed to read tree from ${url}, ${archiveBitbucketResponse.status} ${archiveBitbucketResponse.statusText}`; + if (archiveBitbucketResponse.status === 404) { throw new NotFoundError(message); } throw new Error(message); @@ -126,11 +126,16 @@ export class BitbucketUrlReader implements UrlReader { folderPath = `${project}-${repoName}-${lastCommitShortHash}`; } - return this.treeResponseFactory.fromZipArchive({ - stream: (response.body as unknown) as Readable, + const archiveResponse = await this.treeResponseFactory.fromZipArchive({ + stream: (archiveBitbucketResponse.body as unknown) as Readable, path: `${folderPath}/${filepath}`, filter: options?.filter, }); + + const response = archiveResponse as ReadTreeResponse; + // TODO: Just a placeholder for now. + response.sha = ''; + return response; } toString() { diff --git a/packages/backend-common/src/reading/GithubUrlReader.test.ts b/packages/backend-common/src/reading/GithubUrlReader.test.ts index 1a0f27d28f..6fdab9dfa1 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.test.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.test.ts @@ -21,6 +21,7 @@ import fs from 'fs'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import path from 'path'; +import { NotModifiedError } from '../errors'; import { GithubUrlReader } from './GithubUrlReader'; import { ReadTreeResponseFactory } from './tree'; @@ -28,11 +29,27 @@ const treeResponseFactory = ReadTreeResponseFactory.create({ config: new ConfigReader({}), }); -describe('GithubUrlReader', () => { - const mockCredentialsProvider = ({ - getCredentials: jest.fn().mockResolvedValue({ headers: {} }), - } as unknown) as GithubCredentialsProvider; +const mockCredentialsProvider = ({ + getCredentials: jest.fn().mockResolvedValue({ headers: {} }), +} as unknown) as GithubCredentialsProvider; +const githubProcessor = new GithubUrlReader( + { + host: 'github.com', + apiBaseUrl: 'https://api.github.com', + }, + { treeResponseFactory, credentialsProvider: mockCredentialsProvider }, +); + +const gheProcessor = new GithubUrlReader( + { + host: 'ghe.github.com', + apiBaseUrl: 'https://ghe.github.com/api/v3', + }, + { treeResponseFactory, credentialsProvider: mockCredentialsProvider }, +); + +describe('GithubUrlReader', () => { const worker = setupServer(); msw.setupDefaultHandlers(worker); @@ -43,15 +60,8 @@ describe('GithubUrlReader', () => { describe('implementation', () => { it('rejects unknown targets', async () => { - const processor = new GithubUrlReader( - { - host: 'github.com', - apiBaseUrl: 'https://api.github.com', - }, - { treeResponseFactory, credentialsProvider: mockCredentialsProvider }, - ); await expect( - processor.read('https://not.github.com/apa'), + githubProcessor.read('https://not.github.com/apa'), ).rejects.toThrow( 'Incorrect URL: https://not.github.com/apa, Error: Invalid GitHub URL or file path', ); @@ -73,7 +83,7 @@ describe('GithubUrlReader', () => { worker.use( rest.get( - 'https://api.github.com/repos/backstage/mock/tree/contents/?ref=repo', + 'https://api.github.com/repos/backstage/mock/tree/contents/?ref=main', (req, res, ctx) => { expect(req.headers.get('authorization')).toBe( mockHeaders.Authorization, @@ -90,28 +100,43 @@ describe('GithubUrlReader', () => { ), ); - const processor = new GithubUrlReader( - { - host: 'ghe.github.com', - apiBaseUrl: 'https://api.github.com', - }, - { treeResponseFactory, credentialsProvider: mockCredentialsProvider }, - ); - await processor.read( - 'https://ghe.github.com/backstage/mock/tree/blob/repo', + await githubProcessor.read( + 'https://github.com/backstage/mock/tree/blob/main', ); }); }); describe('readTree', () => { const repoBuffer = fs.readFileSync( - path.resolve('src', 'reading', '__fixtures__', 'repo.tar.gz'), + path.resolve('src', 'reading', '__fixtures__', 'mock-main.tar.gz'), ); + const reposGithubApiResponse = { + id: '123', + full_name: 'backstage/mock', + default_branch: 'main', + branches_url: + 'https://api.github.com/repos/backstage/mock/branches{/branch}', + }; + + const reposGheApiResponse = { + ...reposGithubApiResponse, + branches_url: + 'https://ghe.github.com/api/v3/repos/backstage/mock/branches{/branch}', + }; + + const branchesApiResponse = { + name: 'main', + commit: { + sha: 'sha123abc', + }, + }; + beforeEach(() => { + // For github.com host worker.use( rest.get( - 'https://github.com/backstage/mock/archive/repo.tar.gz', + 'https://github.com/backstage/mock/archive/main.tar.gz', (_, res, ctx) => res( ctx.status(200), @@ -120,20 +145,73 @@ describe('GithubUrlReader', () => { ), ), ); + + worker.use( + rest.get('https://api.github.com/repos/backstage/mock', (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(reposGithubApiResponse), + ), + ), + ); + + worker.use( + rest.get( + 'https://api.github.com/repos/backstage/mock/branches/main', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(branchesApiResponse), + ), + ), + ); + + // For a GHE host + worker.use( + rest.get( + 'https://ghe.github.com/backstage/mock/archive/main.tar.gz', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/x-gzip'), + ctx.body(repoBuffer), + ), + ), + ); + + worker.use( + rest.get( + 'https://ghe.github.com/api/v3/repos/backstage/mock', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(reposGheApiResponse), + ), + ), + ); + + worker.use( + rest.get( + 'https://ghe.github.com/api/v3/repos/backstage/mock/branches/main', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(branchesApiResponse), + ), + ), + ); }); it('returns the wanted files from an archive', async () => { - const processor = new GithubUrlReader( - { - host: 'github.com', - apiBaseUrl: 'https://api.github.com', - }, - { treeResponseFactory, credentialsProvider: mockCredentialsProvider }, + const response = await githubProcessor.readTree( + 'https://github.com/backstage/mock/tree/main', ); - const response = await processor.readTree( - 'https://github.com/backstage/mock/tree/repo', - ); + expect(response.sha).toBe('sha123abc'); const files = await response.files(); @@ -145,40 +223,6 @@ describe('GithubUrlReader', () => { expect(indexMarkdownFile.toString()).toBe('# Test\n'); }); - it('includes the subdomain in the github url', async () => { - worker.resetHandlers(); - worker.use( - rest.get( - 'https://ghe.github.com/backstage/mock/archive/repo.tar.gz', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/x-gzip'), - ctx.body(repoBuffer), - ), - ), - ); - - const processor = new GithubUrlReader( - { - host: 'ghe.github.com', - apiBaseUrl: 'https://api.github.com', - }, - { treeResponseFactory, credentialsProvider: mockCredentialsProvider }, - ); - - const response = await processor.readTree( - 'https://ghe.github.com/backstage/mock/tree/repo/docs', - ); - - const files = await response.files(); - - expect(files.length).toBe(1); - const indexMarkdownFile = await files[0].content(); - - expect(indexMarkdownFile.toString()).toBe('# Test\n'); - }); - it('should use the headers from the credentials provider to the fetch request', async () => { expect.assertions(2); @@ -193,7 +237,7 @@ describe('GithubUrlReader', () => { worker.use( rest.get( - 'https://ghe.github.com/backstage/mock/archive/repo.tar.gz', + 'https://ghe.github.com/backstage/mock/archive/main.tar.gz', (req, res, ctx) => { expect(req.headers.get('authorization')).toBe( mockHeaders.Authorization, @@ -210,46 +254,14 @@ describe('GithubUrlReader', () => { ), ); - const processor = new GithubUrlReader( - { - host: 'ghe.github.com', - apiBaseUrl: 'https://api.github.com', - }, - { treeResponseFactory, credentialsProvider: mockCredentialsProvider }, - ); - - await processor.readTree( - 'https://ghe.github.com/backstage/mock/tree/repo/docs', + await gheProcessor.readTree( + 'https://ghe.github.com/backstage/mock/tree/main', ); }); - it('must specify a branch', async () => { - const processor = new GithubUrlReader( - { - host: 'github.com', - apiBaseUrl: 'https://api.github.com', - }, - { treeResponseFactory, credentialsProvider: mockCredentialsProvider }, - ); - - await expect( - processor.readTree('https://github.com/backstage/mock'), - ).rejects.toThrow( - 'GitHub URL must contain branch to be able to fetch tree', - ); - }); - - it('returns the wanted files from an archive with a subpath', async () => { - const processor = new GithubUrlReader( - { - host: 'github.com', - apiBaseUrl: 'https://api.github.com', - }, - { treeResponseFactory, credentialsProvider: mockCredentialsProvider }, - ); - - const response = await processor.readTree( - 'https://github.com/backstage/mock/tree/repo/docs', + it('includes the subdomain in the github url', async () => { + const response = await gheProcessor.readTree( + 'https://ghe.github.com/backstage/mock/tree/main/docs', ); const files = await response.files(); @@ -259,5 +271,55 @@ describe('GithubUrlReader', () => { expect(indexMarkdownFile.toString()).toBe('# Test\n'); }); + + it('returns the wanted files from an archive with a subpath', async () => { + const response = await githubProcessor.readTree( + 'https://github.com/backstage/mock/tree/main/docs', + ); + + const files = await response.files(); + + expect(files.length).toBe(1); + const indexMarkdownFile = await files[0].content(); + + expect(indexMarkdownFile.toString()).toBe('# Test\n'); + }); + + it('throws a NotModifiedError when given a sha in options', async () => { + const fnGithub = async () => { + await githubProcessor.readTree('https://github.com/backstage/mock', { + sha: 'sha123abc', + }); + }; + + const fnGhe = async () => { + await gheProcessor.readTree( + 'https://ghe.github.com/backstage/mock/tree/main/docs', + { + sha: 'sha123abc', + }, + ); + }; + + await expect(fnGithub).rejects.toThrow(NotModifiedError); + await expect(fnGhe).rejects.toThrow(NotModifiedError); + }); + + it('should not throw error when given an outdated sha in options', async () => { + const response = await githubProcessor.readTree( + 'https://github.com/backstage/mock/tree/main', + { + sha: 'outdatedSha123abc', + }, + ); + expect((await response.files()).length).toBe(2); + }); + + it('should detect the default branch', async () => { + const response = await githubProcessor.readTree( + 'https://github.com/backstage/mock', + ); + expect((await response.files()).length).toBe(2); + }); }); }); diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts index 45ce8336e6..2acca10d4f 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.ts @@ -23,7 +23,7 @@ import { import fetch from 'cross-fetch'; import parseGitUrl from 'git-url-parse'; import { Readable } from 'stream'; -import { InputError, NotFoundError } from '../errors'; +import { NotFoundError, NotModifiedError } from '../errors'; import { ReadTreeResponseFactory } from './tree'; import { ReaderFactory, @@ -99,52 +99,83 @@ export class GithubUrlReader implements UrlReader { options?: ReadTreeOptions, ): Promise { const { - name: repoName, - ref, protocol, resource, - full_name, + name: repoName, + ref, filepath, + full_name, } = parseGitUrl(url); - if (!ref) { - // TODO(Rugvip): We should add support for defaulting to the default branch - throw new InputError( - 'GitHub URL must contain branch to be able to fetch tree', - ); - } - const { headers } = await this.deps.credentialsProvider.getCredentials({ url, }); - // TODO(Rugvip): use API to fetch URL instead - const response = await fetch( - new URL( - `${protocol}://${resource}/${full_name}/archive/${ref}.tar.gz`, - ).toString(), + + // Get GitHub API urls for the repository + const repoGitHubResponse = await fetch( + new URL(`${this.config.apiBaseUrl}/repos/${full_name}`).toString(), { - headers: { - ...headers, - }, + headers, }, ); - if (!response.ok) { - const message = `Failed to read tree from ${url}, ${response.status} ${response.statusText}`; - if (response.status === 404) { + if (!repoGitHubResponse.ok) { + const message = `Failed to read tree from ${url}, ${repoGitHubResponse.status} ${repoGitHubResponse.statusText}`; + if (repoGitHubResponse.status === 404) { throw new NotFoundError(message); } throw new Error(message); } - const path = `${repoName}-${ref}/${filepath}`; + const repoResponseJson = await repoGitHubResponse.json(); - return this.deps.treeResponseFactory.fromTarArchive({ + // ref is an empty string if no branch is set in provided url to readTree. + // Use GitHub API to get the default branch of the repository. + const branch = ref === '' ? repoResponseJson.default_branch : ref; + const branchesApiUrl = repoResponseJson.branches_url; + + // Fetch the latest commit in the provided or default branch to compare against + // the provided sha. + const branchGitHubResponse = await fetch( + // branchesApiUrl looks like "https://api.github.com/repos/owner/repo/branches{/branch}" + branchesApiUrl.replace('{/branch}', `/${branch}`), + { + headers, + }, + ); + const commitSha = (await branchGitHubResponse.json()).commit.sha; + + if (options?.sha && options.sha === commitSha) { + throw new NotModifiedError(); + } + + // Note: the API way of downloading an archive URL does not return a real time archive. + // https://github.community/t/archive-downloaded-via-v3-rest-api-is-not-real-time/14827 + // It looks like this https://api.github.com/repos/owner/repo/{archive_format}{/ref} + // and can be used from `repoResponseJson.archive_url`. + // Continue using the "direct" way i.e. https://github.com/:owner/:repo/archive/branch.tar.gz + // until the bug? is fixed. + const archive = await fetch( + new URL( + `${protocol}://${resource}/${full_name}/archive/${branch}.tar.gz`, + ).toString(), + { + headers, + }, + ); + + const path = `${repoName}-${branch}/${filepath}`; + + const archiveResponse = await this.deps.treeResponseFactory.fromTarArchive({ // TODO(Rugvip): Underlying implementation of fetch will be node-fetch, we probably want // to stick to using that in exclusively backend code. - stream: (response.body as unknown) as Readable, + stream: (archive.body as unknown) as Readable, path, filter: options?.filter, }); + + const response = archiveResponse as ReadTreeResponse; + response.sha = commitSha; + return response; } toString() { diff --git a/packages/backend-common/src/reading/GitlabUrlReader.ts b/packages/backend-common/src/reading/GitlabUrlReader.ts index 299bbc783b..76cfea7c38 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.ts @@ -94,13 +94,13 @@ export class GitlabUrlReader implements UrlReader { } const archive = `${protocol}://${resource}/${full_name}/-/archive/${ref}/${repoName}-${ref}.zip`; - const response = await fetch( + const archiveGitLabResponse = await fetch( archive, getGitLabRequestOptions(this.options), ); - if (!response.ok) { - const msg = `Failed to read tree from ${url}, ${response.status} ${response.statusText}`; - if (response.status === 404) { + if (!archiveGitLabResponse.ok) { + const msg = `Failed to read tree from ${url}, ${archiveGitLabResponse.status} ${archiveGitLabResponse.statusText}`; + if (archiveGitLabResponse.status === 404) { throw new NotFoundError(msg); } throw new Error(msg); @@ -108,11 +108,16 @@ export class GitlabUrlReader implements UrlReader { const path = filepath ? `${repoName}-${ref}/${filepath}/` : ''; - return this.treeResponseFactory.fromZipArchive({ - stream: (response.body as unknown) as Readable, + const archiveResponse = await this.treeResponseFactory.fromZipArchive({ + stream: (archiveGitLabResponse.body as unknown) as Readable, path, filter: options?.filter, }); + + const response = archiveResponse as ReadTreeResponse; + // TODO: Just a placeholder for now. + response.sha = ''; + return response; } toString() { diff --git a/packages/backend-common/src/reading/UrlReaderPredicateMux.ts b/packages/backend-common/src/reading/UrlReaderPredicateMux.ts index ca11400e4a..3183aa0c28 100644 --- a/packages/backend-common/src/reading/UrlReaderPredicateMux.ts +++ b/packages/backend-common/src/reading/UrlReaderPredicateMux.ts @@ -45,12 +45,15 @@ export class UrlReaderPredicateMux implements UrlReader { throw new NotAllowedError(`Reading from '${url}' is not allowed`); } - readTree(url: string, options?: ReadTreeOptions): Promise { + async readTree( + url: string, + options?: ReadTreeOptions, + ): Promise { const parsed = new URL(url); for (const { predicate, reader } of this.readers) { if (predicate(parsed)) { - return reader.readTree(url, options); + return await reader.readTree(url, options); } } diff --git a/packages/backend-common/src/reading/__fixtures__/mock-main.tar.gz b/packages/backend-common/src/reading/__fixtures__/mock-main.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..291690b447eae0995eda9c4215ffc21636d516c1 GIT binary patch literal 230 zcmV>lPUp5kJIk}HdFl55zWvjD0$=4n1-t*;egQ83=b-1m(n)&pF&VYT g$NUG`^WT<-F8}9X?PA~Ia5xsp4OF6Z-vAH*0G5MqdH?_b literal 0 HcmV?d00001 diff --git a/packages/backend-common/src/reading/__fixtures__/repo.tar.gz b/packages/backend-common/src/reading/__fixtures__/repo.tar.gz deleted file mode 100644 index 7a8e9902a24232a5f7130637a05304134abc3fec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 232 zcmb2|=3oE==C@Zbay1!XSu&B+beL(LRxe2^Idk!eV446aOJ+o+S-o>~??20b^`}#YbLB3%Cll?@@AuK( zf9m={{U?{EUwv`;^MMVwzTe;Tuda?M|Lgvh`?I~>7XNQAm|j0i{htM|=koGBZ|a}E f`_yk@`Q^XBu@$utzd*@``2}nqTB%DJG#D5FAn { + async fromTarArchive( + options: FromArchiveOptions, + ): Promise { return new TarArchiveResponse( options.stream, options.path ?? '', @@ -49,7 +51,9 @@ export class ReadTreeResponseFactory { ); } - async fromZipArchive(options: FromArchiveOptions): Promise { + async fromZipArchive( + options: FromArchiveOptions, + ): Promise { return new ZipArchiveResponse( options.stream, options.path ?? '', diff --git a/packages/backend-common/src/reading/tree/TarArchiveResponse.test.ts b/packages/backend-common/src/reading/tree/TarArchiveResponse.test.ts index 1bc0d3a386..bbeee00c70 100644 --- a/packages/backend-common/src/reading/tree/TarArchiveResponse.test.ts +++ b/packages/backend-common/src/reading/tree/TarArchiveResponse.test.ts @@ -20,7 +20,7 @@ import { resolve as resolvePath } from 'path'; import { TarArchiveResponse } from './TarArchiveResponse'; const archiveData = fs.readFileSync( - resolvePath(__filename, '../../__fixtures__/repo.tar.gz'), + resolvePath(__filename, '../../__fixtures__/mock-main.tar.gz'), ); describe('TarArchiveResponse', () => { @@ -38,7 +38,7 @@ describe('TarArchiveResponse', () => { it('should read files', async () => { const stream = fs.createReadStream('/test-archive.tar.gz'); - const res = new TarArchiveResponse(stream, 'mock-repo/', '/tmp'); + const res = new TarArchiveResponse(stream, 'mock-main/', '/tmp'); const files = await res.files(); expect(files).toEqual([ @@ -61,7 +61,7 @@ describe('TarArchiveResponse', () => { it('should read files with filter', async () => { const stream = fs.createReadStream('/test-archive.tar.gz'); - const res = new TarArchiveResponse(stream, 'mock-repo/', '/tmp', path => + const res = new TarArchiveResponse(stream, 'mock-main/', '/tmp', path => path.endsWith('.yml'), ); const files = await res.files(); @@ -79,7 +79,7 @@ describe('TarArchiveResponse', () => { it('should read as archive and files', async () => { const stream = fs.createReadStream('/test-archive.tar.gz'); - const res = new TarArchiveResponse(stream, 'mock-repo/', '/tmp'); + const res = new TarArchiveResponse(stream, 'mock-main/', '/tmp'); const buffer = await res.archive(); await expect(res.archive()).rejects.toThrow( @@ -113,17 +113,17 @@ describe('TarArchiveResponse', () => { const dir = await res.dir(); await expect( - fs.readFile(resolvePath(dir, 'mock-repo/mkdocs.yml'), 'utf8'), + fs.readFile(resolvePath(dir, 'mock-main/mkdocs.yml'), 'utf8'), ).resolves.toBe('site_name: Test\n'); await expect( - fs.readFile(resolvePath(dir, 'mock-repo/docs/index.md'), 'utf8'), + fs.readFile(resolvePath(dir, 'mock-main/docs/index.md'), 'utf8'), ).resolves.toBe('# Test\n'); }); it('should extract archive into directory with a subpath', async () => { const stream = fs.createReadStream('/test-archive.tar.gz'); - const res = new TarArchiveResponse(stream, 'mock-repo/docs/', '/tmp'); + const res = new TarArchiveResponse(stream, 'mock-main/docs/', '/tmp'); const dir = await res.dir(); expect(dir).toMatch(/^[\/\\]tmp[\/\\].*$/); @@ -135,7 +135,7 @@ describe('TarArchiveResponse', () => { it('should extract archive into directory with a subpath and filter', async () => { const stream = fs.createReadStream('/test-archive.tar.gz'); - const res = new TarArchiveResponse(stream, 'mock-repo/', '/tmp', path => + const res = new TarArchiveResponse(stream, 'mock-main/', '/tmp', path => path.endsWith('.yml'), ); const dir = await res.dir({ targetDir: '/tmp' }); diff --git a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts index 5d18ec7dc6..e8929fbe9b 100644 --- a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts +++ b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts @@ -21,7 +21,7 @@ import { Readable, pipeline as pipelineCb } from 'stream'; import { promisify } from 'util'; import concatStream from 'concat-stream'; import { - ReadTreeResponse, + ReadTreeArchiveResponse, ReadTreeResponseFile, ReadTreeResponseDirOptions, } from '../types'; @@ -34,7 +34,7 @@ const pipeline = promisify(pipelineCb); /** * Wraps a tar archive stream into a tree response reader. */ -export class TarArchiveResponse implements ReadTreeResponse { +export class TarArchiveResponse implements ReadTreeArchiveResponse { private read = false; constructor( diff --git a/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts b/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts index 4106d49a11..48ba4f19ee 100644 --- a/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts +++ b/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts @@ -20,7 +20,7 @@ import unzipper, { Entry } from 'unzipper'; import archiver from 'archiver'; import { Readable } from 'stream'; import { - ReadTreeResponse, + ReadTreeArchiveResponse, ReadTreeResponseFile, ReadTreeResponseDirOptions, } from '../types'; @@ -28,7 +28,7 @@ import { /** * Wraps a zip archive stream into a tree response reader. */ -export class ZipArchiveResponse implements ReadTreeResponse { +export class ZipArchiveResponse implements ReadTreeArchiveResponse { private read = false; constructor( diff --git a/packages/backend-common/src/reading/types.ts b/packages/backend-common/src/reading/types.ts index f9dca3e1d5..c727ba078b 100644 --- a/packages/backend-common/src/reading/types.ts +++ b/packages/backend-common/src/reading/types.ts @@ -32,6 +32,19 @@ export type ReadTreeOptions = { * If no filter is provided all files are extracted. */ filter?(path: string): boolean; + + /** + * A commit SHA can be provided to check whether readTree's response has changed from a previous execution. + * + * In the readTree() response, a SHA is returned along with the tree blob. The SHA belongs to the + * latest commit on the target repository's branch that was used to read the blob. + * + * When a SHA is given in ReadTreeOptions, readTree will first compare the SHA against the latest commit + * on the target branch. If they match, it will throw a NotModifiedError indicating that the readTree + * response will not differ from the previous response which included this particular SHA. If they mismatch, + * readTree will return a new SHA along with the rest of ReadTreeResponse. + */ + sha?: string; }; /** @@ -67,8 +80,12 @@ export type ReadTreeResponseDirOptions = { targetDir?: string; }; -export type ReadTreeResponse = { +export type ReadTreeArchiveResponse = { files(): Promise; archive(): Promise; dir(options?: ReadTreeResponseDirOptions): Promise; }; + +export interface ReadTreeResponse extends ReadTreeArchiveResponse { + sha: string; +} diff --git a/packages/techdocs-common/src/helpers.test.ts b/packages/techdocs-common/src/helpers.test.ts index 88c4d8829e..88fdeed498 100644 --- a/packages/techdocs-common/src/helpers.test.ts +++ b/packages/techdocs-common/src/helpers.test.ts @@ -135,6 +135,7 @@ describe('getDocFilesFromRepository', () => { archive: async () => { return Readable.from(''); }, + sha: '', }; } } From 294a70caba78729627b96759a61881b454069546 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Sun, 17 Jan 2021 09:28:54 +0100 Subject: [PATCH 068/209] backend-common: Add changeset about SHA based caching in URL Reader --- .changeset/khaki-icons-trade.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .changeset/khaki-icons-trade.md diff --git a/.changeset/khaki-icons-trade.md b/.changeset/khaki-icons-trade.md new file mode 100644 index 0000000000..37bafd8d33 --- /dev/null +++ b/.changeset/khaki-icons-trade.md @@ -0,0 +1,22 @@ +--- +'@backstage/backend-common': patch +--- + +1. URL Reader's `readTree` method now returns a `sha` in the response along with the blob. The SHA belongs to the latest commit on the target. `readTree` also takes an optional `sha` in its options and throws a `NotModifiedError` if the SHA matches with the latest commit SHA on the url's target. This can be used in building a cache when working with URL Reader. + +An example - + +```ts +const response = await reader.readTree( + 'https://github.com/backstage/backstage', +); + +const commitSha = response.sha; + +// Will throw a new NotModifiedError (exported from @backstage/backstage-common) +await reader.readTree('https://github.com/backstage/backstage', { + sha: commitSha, +}); +``` + +2. URL Reader's readTree method can now detect the default branch. So, `url:https://github.com/org/repo/tree/master` can be replaced with `url:https://github.com/org/repo` in places like `backstage.io/techdocs-ref`. From fa8ba330a86b518981fd6177203dc45590b65d3e Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Sun, 17 Jan 2021 10:43:39 +0100 Subject: [PATCH 069/209] integration: GitLab API should be added for default and should have a protocol --- .changeset/nervous-mails-repair.md | 5 +++++ packages/integration/src/gitlab/config.test.ts | 14 +++++++++++++- packages/integration/src/gitlab/config.ts | 4 ++-- 3 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 .changeset/nervous-mails-repair.md diff --git a/.changeset/nervous-mails-repair.md b/.changeset/nervous-mails-repair.md new file mode 100644 index 0000000000..f72e2b5e66 --- /dev/null +++ b/.changeset/nervous-mails-repair.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': patch +--- + +Fix GitLab API base URL and add it by default to the gitlab.com host diff --git a/packages/integration/src/gitlab/config.test.ts b/packages/integration/src/gitlab/config.test.ts index e817465b83..31643d74a6 100644 --- a/packages/integration/src/gitlab/config.test.ts +++ b/packages/integration/src/gitlab/config.test.ts @@ -43,7 +43,18 @@ describe('readGitLabIntegrationConfig', () => { const output = readGitLabIntegrationConfig(buildConfig({})); expect(output).toEqual({ host: 'gitlab.com', - apiBaseUrl: 'gitlab.com/api/v4', + apiBaseUrl: 'https://gitlab.com/api/v4', + }); + }); + + it('injects the correct GitLab API base URL when missing', () => { + const output = readGitLabIntegrationConfig( + buildConfig({ host: 'gitlab.com' }), + ); + + expect(output).toEqual({ + host: 'gitlab.com', + apiBaseUrl: 'https://gitlab.com/api/v4', }); }); @@ -86,6 +97,7 @@ describe('readGitLabIntegrationConfigs', () => { expect(output).toEqual([ { host: 'gitlab.com', + apiBaseUrl: 'https://gitlab.com/api/v4', }, ]); }); diff --git a/packages/integration/src/gitlab/config.ts b/packages/integration/src/gitlab/config.ts index 2d9f4b39ab..fd52a436b6 100644 --- a/packages/integration/src/gitlab/config.ts +++ b/packages/integration/src/gitlab/config.ts @@ -18,7 +18,7 @@ import { Config } from '@backstage/config'; import { isValidHost } from '../helpers'; const GITLAB_HOST = 'gitlab.com'; -const GITLAB_API_BASE_URL = 'gitlab.com/api/v4'; +const GITLAB_API_BASE_URL = 'https://gitlab.com/api/v4'; /** * The configuration parameters for a single GitLab integration. @@ -89,7 +89,7 @@ export function readGitLabIntegrationConfigs( // As a convenience we always make sure there's at least an unauthenticated // reader for public gitlab repos. if (!result.some(c => c.host === GITLAB_HOST)) { - result.push({ host: GITLAB_HOST }); + result.push({ host: GITLAB_HOST, apiBaseUrl: GITLAB_API_BASE_URL }); } return result; From f9ca2a3769989f653d3a439607099dd6369b4c71 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Sun, 17 Jan 2021 12:10:32 +0100 Subject: [PATCH 070/209] backend-common: UrlReader/GitLab implement Sha based caching Also use API to fetch archive.zip --- .../src/reading/AzureUrlReader.test.ts | 6 +- .../src/reading/GithubUrlReader.test.ts | 18 +- .../src/reading/GithubUrlReader.ts | 16 +- .../src/reading/GitlabUrlReader.test.ts | 197 ++++++++++++++---- .../src/reading/GitlabUrlReader.ts | 99 ++++++--- .../reading/__fixtures__/gitlab-archive.zip | Bin 0 -> 857 bytes .../src/reading/__fixtures__/mock-main.zip | Bin 0 -> 777 bytes .../src/reading/__fixtures__/repo.zip | Bin 387 -> 0 bytes .../reading/tree/ZipArchiveResponse.test.ts | 28 +-- packages/backend-common/src/reading/types.ts | 4 + 10 files changed, 275 insertions(+), 93 deletions(-) create mode 100644 packages/backend-common/src/reading/__fixtures__/gitlab-archive.zip create mode 100644 packages/backend-common/src/reading/__fixtures__/mock-main.zip delete mode 100644 packages/backend-common/src/reading/__fixtures__/repo.zip diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts index 616cbaaadc..f729316223 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.test.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts @@ -139,7 +139,7 @@ describe('AzureUrlReader', () => { describe('readTree', () => { const repoBuffer = fs.readFileSync( - path.resolve('src', 'reading', '__fixtures__', 'repo.zip'), + path.resolve('src', 'reading', '__fixtures__', 'mock-main.zip'), ); beforeEach(() => { @@ -169,8 +169,8 @@ describe('AzureUrlReader', () => { const files = await response.files(); expect(files.length).toBe(2); - const mkDocsFile = await files[1].content(); - const indexMarkdownFile = await files[0].content(); + const mkDocsFile = await files[0].content(); + const indexMarkdownFile = await files[1].content(); expect(mkDocsFile.toString()).toBe('site_name: Test\n'); expect(indexMarkdownFile.toString()).toBe('# Test\n'); diff --git a/packages/backend-common/src/reading/GithubUrlReader.test.ts b/packages/backend-common/src/reading/GithubUrlReader.test.ts index 6fdab9dfa1..24a49cf81c 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.test.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.test.ts @@ -21,7 +21,7 @@ import fs from 'fs'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import path from 'path'; -import { NotModifiedError } from '../errors'; +import { NotFoundError, NotModifiedError } from '../errors'; import { GithubUrlReader } from './GithubUrlReader'; import { ReadTreeResponseFactory } from './tree'; @@ -168,6 +168,13 @@ describe('GithubUrlReader', () => { ), ); + worker.use( + rest.get( + 'https://api.github.com/repos/backstage/mock/branches/branchDoesNotExist', + (_, res, ctx) => res(ctx.status(404)), + ), + ); + // For a GHE host worker.use( rest.get( @@ -321,5 +328,14 @@ describe('GithubUrlReader', () => { ); expect((await response.files()).length).toBe(2); }); + + it('should throw error on missing branch', async () => { + const fnGithub = async () => { + await githubProcessor.readTree( + 'https://github.com/backstage/mock/tree/branchDoesNotExist', + ); + }; + await expect(fnGithub).rejects.toThrow(NotFoundError); + }); }); }); diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts index 2acca10d4f..8e0410642b 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.ts @@ -119,7 +119,7 @@ export class GithubUrlReader implements UrlReader { }, ); if (!repoGitHubResponse.ok) { - const message = `Failed to read tree from ${url}, ${repoGitHubResponse.status} ${repoGitHubResponse.statusText}`; + const message = `Failed to read tree (repository) from ${url}, ${repoGitHubResponse.status} ${repoGitHubResponse.statusText}`; if (repoGitHubResponse.status === 404) { throw new NotFoundError(message); } @@ -142,6 +142,13 @@ export class GithubUrlReader implements UrlReader { headers, }, ); + if (!branchGitHubResponse.ok) { + const message = `Failed to read tree (branch) from ${url}, ${branchGitHubResponse.status} ${branchGitHubResponse.statusText}`; + if (branchGitHubResponse.status === 404) { + throw new NotFoundError(message); + } + throw new Error(message); + } const commitSha = (await branchGitHubResponse.json()).commit.sha; if (options?.sha && options.sha === commitSha) { @@ -162,6 +169,13 @@ export class GithubUrlReader implements UrlReader { headers, }, ); + if (!archive.ok) { + const message = `Failed to read tree (archive) from ${url}, ${archive.status} ${archive.statusText}`; + if (archive.status === 404) { + throw new NotFoundError(message); + } + throw new Error(message); + } const path = `${repoName}-${branch}/${filepath}`; diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts index 6cc5e30dbd..a2dda48b16 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.test.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts @@ -23,6 +23,7 @@ import path from 'path'; import { getVoidLogger } from '../logging'; import { GitlabUrlReader } from './GitlabUrlReader'; import { ReadTreeResponseFactory } from './tree'; +import { NotModifiedError, NotFoundError } from '../errors'; const logger = getVoidLogger(); @@ -30,6 +31,22 @@ const treeResponseFactory = ReadTreeResponseFactory.create({ config: new ConfigReader({}), }); +const gitlabProcessor = new GitlabUrlReader( + { + host: 'gitlab.com', + apiBaseUrl: 'https://gitlab.com/api/v4', + }, + { treeResponseFactory }, +); + +const hostedGitlabProcessor = new GitlabUrlReader( + { + host: 'gitlab.mycompany.com', + apiBaseUrl: 'https://gitlab.mycompany.com/api/v4', + }, + { treeResponseFactory }, +); + describe('GitlabUrlReader', () => { const worker = setupServer(); msw.setupDefaultHandlers(worker); @@ -136,39 +153,112 @@ describe('GitlabUrlReader', () => { }); describe('readTree', () => { - const repoBuffer = fs.readFileSync( - path.resolve('src', 'reading', '__fixtures__', 'repo.zip'), + const archiveBuffer = fs.readFileSync( + path.resolve('src', 'reading', '__fixtures__', 'gitlab-archive.zip'), ); + const projectGitlabApiResponse = { + id: 11111111, + default_branch: 'main', + }; + + const branchGitlabApiResponse = { + commit: { + id: 'sha123abc', + }, + }; + beforeEach(() => { worker.use( rest.get( - 'https://gitlab.com/backstage/mock/-/archive/repo/mock-repo.zip', + 'https://gitlab.com/api/v4/projects/backstage%2Fmock/repository/archive.zip?sha=main', (_, res, ctx) => res( ctx.status(200), ctx.set('Content-Type', 'application/zip'), - ctx.body(repoBuffer), + ctx.body(archiveBuffer), + ), + ), + ); + + worker.use( + rest.get( + 'https://gitlab.com/api/v4/projects/backstage%2Fmock', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(projectGitlabApiResponse), + ), + ), + ); + + worker.use( + rest.get( + 'https://gitlab.com/api/v4/projects/backstage%2Fmock/repository/branches/main', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(branchGitlabApiResponse), + ), + ), + ); + + worker.use( + rest.get( + 'https://gitlab.com/api/v4/projects/backstage%2Fmock/repository/branches/branchDoesNotExist', + (_, res, ctx) => res(ctx.status(404)), + ), + ); + + worker.use( + rest.get( + 'https://gitlab.mycompany.com/api/v4/projects/backstage%2Fmock', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(projectGitlabApiResponse), + ), + ), + ); + + worker.use( + rest.get( + 'https://gitlab.mycompany.com/api/v4/projects/backstage%2Fmock/repository/branches/main', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(branchGitlabApiResponse), + ), + ), + ); + + worker.use( + rest.get( + 'https://gitlab.mycompany.com/api/v4/projects/backstage%2Fmock/repository/archive.zip?sha=main', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/zip'), + ctx.body(archiveBuffer), ), ), ); }); it('returns the wanted files from an archive', async () => { - const processor = new GitlabUrlReader( - { host: 'gitlab.com' }, - { treeResponseFactory }, - ); - - const response = await processor.readTree( - 'https://gitlab.com/backstage/mock/tree/repo', + const response = await gitlabProcessor.readTree( + 'https://gitlab.com/backstage/mock/tree/main', ); const files = await response.files(); expect(files.length).toBe(2); - const indexMarkdownFile = await files[0].content(); - const mkDocsFile = await files[1].content(); + const mkDocsFile = await files[0].content(); + const indexMarkdownFile = await files[1].content(); expect(mkDocsFile.toString()).toBe('site_name: Test\n'); expect(indexMarkdownFile.toString()).toBe('# Test\n'); @@ -177,23 +267,18 @@ describe('GitlabUrlReader', () => { it('returns the wanted files from hosted gitlab', async () => { worker.use( rest.get( - 'https://git.mycompany.com/backstage/mock/-/archive/repo/mock-repo.zip', + 'https://gitlab.mycompany.com/backstage/mock/-/archive/main.zip', (_, res, ctx) => res( ctx.status(200), ctx.set('Content-Type', 'application/zip'), - ctx.body(repoBuffer), + ctx.body(archiveBuffer), ), ), ); - const processor = new GitlabUrlReader( - { host: 'git.mycompany.com' }, - { treeResponseFactory }, - ); - - const response = await processor.readTree( - 'https://git.mycompany.com/backstage/mock/tree/repo/docs', + const response = await hostedGitlabProcessor.readTree( + 'https://gitlab.mycompany.com/backstage/mock/tree/main/docs', ); const files = await response.files(); @@ -204,27 +289,9 @@ describe('GitlabUrlReader', () => { expect(indexMarkdownFile.toString()).toBe('# Test\n'); }); - it('throws an error when branch is not specified', async () => { - const processor = new GitlabUrlReader( - { host: 'gitlab.com' }, - { treeResponseFactory }, - ); - - await expect( - processor.readTree('https://gitlab.com/backstage/mock'), - ).rejects.toThrow( - 'GitLab URL must contain a branch to be able to fetch its tree', - ); - }); - it('returns the wanted files from an archive with a subpath', async () => { - const processor = new GitlabUrlReader( - { host: 'gitlab.com' }, - { treeResponseFactory }, - ); - - const response = await processor.readTree( - 'https://gitlab.com/backstage/mock/tree/repo/docs', + const response = await gitlabProcessor.readTree( + 'https://gitlab.com/backstage/mock/tree/main/docs', ); const files = await response.files(); @@ -234,5 +301,51 @@ describe('GitlabUrlReader', () => { expect(indexMarkdownFile.toString()).toBe('# Test\n'); }); + + it('throws a NotModifiedError when given a sha in options', async () => { + const fnGitlab = async () => { + await gitlabProcessor.readTree('https://gitlab.com/backstage/mock', { + sha: 'sha123abc', + }); + }; + + const fnHostedGitlab = async () => { + await hostedGitlabProcessor.readTree( + 'https://gitlab.mycompany.com/backstage/mock', + { + sha: 'sha123abc', + }, + ); + }; + + await expect(fnGitlab).rejects.toThrow(NotModifiedError); + await expect(fnHostedGitlab).rejects.toThrow(NotModifiedError); + }); + + it('should not throw error when given an outdated sha in options', async () => { + const response = await gitlabProcessor.readTree( + 'https://gitlab.com/backstage/mock/tree/main', + { + sha: 'outdatedSha123abc', + }, + ); + expect((await response.files()).length).toBe(2); + }); + + it('should detect the default branch', async () => { + const response = await gitlabProcessor.readTree( + 'https://gitlab.com/backstage/mock', + ); + expect((await response.files()).length).toBe(2); + }); + + it('should throw error on missing branch', async () => { + const fnGithub = async () => { + await gitlabProcessor.readTree( + 'https://gitlab.com/backstage/mock/tree/branchDoesNotExist', + ); + }; + await expect(fnGithub).rejects.toThrow(NotFoundError); + }); }); }); diff --git a/packages/backend-common/src/reading/GitlabUrlReader.ts b/packages/backend-common/src/reading/GitlabUrlReader.ts index 76cfea7c38..0c0ce4f565 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.ts @@ -21,7 +21,7 @@ import { readGitLabIntegrationConfigs, } from '@backstage/integration'; import fetch from 'cross-fetch'; -import { InputError, NotFoundError } from '../errors'; +import { NotFoundError, NotModifiedError } from '../errors'; import { ReadTreeResponseFactory } from './tree'; import { ReaderFactory, @@ -39,26 +39,26 @@ export class GitlabUrlReader implements UrlReader { const configs = readGitLabIntegrationConfigs( config.getOptionalConfigArray('integrations.gitlab') ?? [], ); - return configs.map(options => { - const reader = new GitlabUrlReader(options, { treeResponseFactory }); - const predicate = (url: URL) => url.host === options.host; + return configs.map(provider => { + const reader = new GitlabUrlReader(provider, { treeResponseFactory }); + const predicate = (url: URL) => url.host === provider.host; return { reader, predicate }; }); }; constructor( - private readonly options: GitLabIntegrationConfig, + private readonly config: GitLabIntegrationConfig, deps: { treeResponseFactory: ReadTreeResponseFactory }, ) { this.treeResponseFactory = deps.treeResponseFactory; } async read(url: string): Promise { - const builtUrl = await getGitLabFileFetchUrl(url, this.options); + const builtUrl = await getGitLabFileFetchUrl(url, this.config); let response: Response; try { - response = await fetch(builtUrl, getGitLabRequestOptions(this.options)); + response = await fetch(builtUrl, getGitLabRequestOptions(this.config)); } catch (e) { throw new Error(`Unable to read ${url}, ${e}`); } @@ -78,35 +78,71 @@ export class GitlabUrlReader implements UrlReader { url: string, options?: ReadTreeOptions, ): Promise { - const { - name: repoName, - ref, - protocol, - resource, - full_name, - filepath, - } = parseGitUrl(url); + const { name: repoName, ref, full_name, filepath } = parseGitUrl(url); - if (!ref) { - throw new InputError( - 'GitLab URL must contain a branch to be able to fetch its tree', - ); - } - - const archive = `${protocol}://${resource}/${full_name}/-/archive/${ref}/${repoName}-${ref}.zip`; - const archiveGitLabResponse = await fetch( - archive, - getGitLabRequestOptions(this.options), + // Use GitLab API to get the default branch + // encodeURIComponent is required for GitLab API + // https://docs.gitlab.com/ee/api/README.html#namespaced-path-encoding + const projectGitlabResponse = await fetch( + new URL( + `${this.config.apiBaseUrl}/projects/${encodeURIComponent(full_name)}`, + ).toString(), + getGitLabRequestOptions(this.config), ); - if (!archiveGitLabResponse.ok) { - const msg = `Failed to read tree from ${url}, ${archiveGitLabResponse.status} ${archiveGitLabResponse.statusText}`; - if (archiveGitLabResponse.status === 404) { + if (!projectGitlabResponse.ok) { + const msg = `Failed to read tree from ${url}, ${projectGitlabResponse.status} ${projectGitlabResponse.statusText}`; + if (projectGitlabResponse.status === 404) { throw new NotFoundError(msg); } throw new Error(msg); } + const projectGitlabResponseJson = await projectGitlabResponse.json(); - const path = filepath ? `${repoName}-${ref}/${filepath}/` : ''; + // ref is an empty string if no branch is set in provided url to readTree. + const branch = ref === '' ? projectGitlabResponseJson.default_branch : ref; + + // Fetch the latest commit in the provided or default branch to compare against + // the provided sha. + const branchGitlabResponse = await fetch( + new URL( + `${this.config.apiBaseUrl}/projects/${encodeURIComponent( + full_name, + )}/repository/branches/${branch}`, + ).toString(), + getGitLabRequestOptions(this.config), + ); + if (!branchGitlabResponse.ok) { + const message = `Failed to read tree (branch) from ${url}, ${branchGitlabResponse.status} ${branchGitlabResponse.statusText}`; + if (branchGitlabResponse.status === 404) { + throw new NotFoundError(message); + } + throw new Error(message); + } + + const commitSha = (await branchGitlabResponse.json()).commit.id; + + if (options?.sha && options.sha === commitSha) { + throw new NotModifiedError(); + } + + // https://docs.gitlab.com/ee/api/repositories.html#get-file-archive + const archiveGitLabResponse = await fetch( + `${this.config.apiBaseUrl}/projects/${encodeURIComponent( + full_name, + )}/repository/archive.zip?sha=${branch}`, + getGitLabRequestOptions(this.config), + ); + if (!archiveGitLabResponse.ok) { + const message = `Failed to read tree (archive) from ${url}, ${archiveGitLabResponse.status} ${archiveGitLabResponse.statusText}`; + if (archiveGitLabResponse.status === 404) { + throw new NotFoundError(message); + } + throw new Error(message); + } + + const path = filepath + ? `${repoName}-${branch}-${commitSha}/${filepath}/` + : ''; const archiveResponse = await this.treeResponseFactory.fromZipArchive({ stream: (archiveGitLabResponse.body as unknown) as Readable, @@ -115,13 +151,12 @@ export class GitlabUrlReader implements UrlReader { }); const response = archiveResponse as ReadTreeResponse; - // TODO: Just a placeholder for now. - response.sha = ''; + response.sha = commitSha; return response; } toString() { - const { host, token } = this.options; + const { host, token } = this.config; return `gitlab{host=${host},authed=${Boolean(token)}}`; } } diff --git a/packages/backend-common/src/reading/__fixtures__/gitlab-archive.zip b/packages/backend-common/src/reading/__fixtures__/gitlab-archive.zip new file mode 100644 index 0000000000000000000000000000000000000000..884ec200048ae719af7b6e9c352ce5ab062c0da2 GIT binary patch literal 857 zcmWIWW@Zs#0D;aZ!yqsNN{BEhFy!VZXY1xBX6ES@XCxXL87C$s>xYK$GO#D}vm}7< z32~N$(h6<{MwYLP3=CkC0>CD6FmN!euZ<3bnJ55c$l)+CH#;Rixmd3>OX)-s}?snh&xAVmruIbpJ@=upMMK zs;976jPTSpBu}vetx?2hY-V0cYK2~I3fNyWfc^p*jm7xjFeL9x6FDEj2{ajGdVn`0 zlL#~J2m&ergSU<#ioEE8*Z_+#paHV_|u_`hUFdG-N3j!RBdQ0mScmYyHY5+Q z0}U6)G%PbOCAC5?HwEm689+b4LI%wb!C^>FpC)oXf)i*S$jkt5MkWzv+yM_%0tRm# zK@=&`05KO95y-&>iU=53(&&L=F7eTV&*h+Chk>__dr)j3G7=EZ2So#Nkb$BB29`8( dG9m{H*l=PaCBU1N4P+1t5Y_;VDF0=yB1 jV>%w$@CX#ck-Y*m8RQiVlUdn74q^hr?Lc}Ph{FH?Ktx*D diff --git a/packages/backend-common/src/reading/tree/ZipArchiveResponse.test.ts b/packages/backend-common/src/reading/tree/ZipArchiveResponse.test.ts index 6c2592ffce..345ec7a783 100644 --- a/packages/backend-common/src/reading/tree/ZipArchiveResponse.test.ts +++ b/packages/backend-common/src/reading/tree/ZipArchiveResponse.test.ts @@ -20,7 +20,7 @@ import { resolve as resolvePath } from 'path'; import { ZipArchiveResponse } from './ZipArchiveResponse'; const archiveData = fs.readFileSync( - resolvePath(__filename, '../../__fixtures__/repo.zip'), + resolvePath(__filename, '../../__fixtures__/mock-main.zip'), ); describe('ZipArchiveResponse', () => { @@ -38,30 +38,30 @@ describe('ZipArchiveResponse', () => { it('should read files', async () => { const stream = fs.createReadStream('/test-archive.zip'); - const res = new ZipArchiveResponse(stream, 'mock-repo/', '/tmp'); + const res = new ZipArchiveResponse(stream, 'mock-main/', '/tmp'); const files = await res.files(); expect(files).toEqual([ { - path: 'docs/index.md', + path: 'mkdocs.yml', content: expect.any(Function), }, { - path: 'mkdocs.yml', + path: 'docs/index.md', content: expect.any(Function), }, ]); const contents = await Promise.all(files.map(f => f.content())); expect(contents.map(c => c.toString('utf8').trim())).toEqual([ - '# Test', 'site_name: Test', + '# Test', ]); }); it('should read files with filter', async () => { const stream = fs.createReadStream('/test-archive.zip'); - const res = new ZipArchiveResponse(stream, 'mock-repo/', '/tmp', path => + const res = new ZipArchiveResponse(stream, 'mock-main/', '/tmp', path => path.endsWith('.yml'), ); const files = await res.files(); @@ -79,7 +79,7 @@ describe('ZipArchiveResponse', () => { it('should read as archive and files', async () => { const stream = fs.createReadStream('/test-archive.zip'); - const res = new ZipArchiveResponse(stream, 'mock-repo/', '/tmp'); + const res = new ZipArchiveResponse(stream, 'mock-main/', '/tmp'); const buffer = await res.archive(); await expect(res.archive()).rejects.toThrow( @@ -91,18 +91,18 @@ describe('ZipArchiveResponse', () => { expect(files).toEqual([ { - path: 'docs/index.md', + path: 'mkdocs.yml', content: expect.any(Function), }, { - path: 'mkdocs.yml', + path: 'docs/index.md', content: expect.any(Function), }, ]); const contents = await Promise.all(files.map(f => f.content())); expect(contents.map(c => c.toString('utf8').trim())).toEqual([ - '# Test', 'site_name: Test', + '# Test', ]); }); @@ -113,17 +113,17 @@ describe('ZipArchiveResponse', () => { const dir = await res.dir(); await expect( - fs.readFile(resolvePath(dir, 'mock-repo/mkdocs.yml'), 'utf8'), + fs.readFile(resolvePath(dir, 'mock-main/mkdocs.yml'), 'utf8'), ).resolves.toBe('site_name: Test\n'); await expect( - fs.readFile(resolvePath(dir, 'mock-repo/docs/index.md'), 'utf8'), + fs.readFile(resolvePath(dir, 'mock-main/docs/index.md'), 'utf8'), ).resolves.toBe('# Test\n'); }); it('should extract archive into directory with a subpath', async () => { const stream = fs.createReadStream('/test-archive.zip'); - const res = new ZipArchiveResponse(stream, 'mock-repo/docs/', '/tmp'); + const res = new ZipArchiveResponse(stream, 'mock-main/docs/', '/tmp'); const dir = await res.dir(); expect(dir).toMatch(/^[\/\\]tmp[\/\\].*$/); @@ -135,7 +135,7 @@ describe('ZipArchiveResponse', () => { it('should extract archive into directory with a subpath and filter', async () => { const stream = fs.createReadStream('/test-archive.zip'); - const res = new ZipArchiveResponse(stream, 'mock-repo/', '/tmp', path => + const res = new ZipArchiveResponse(stream, 'mock-main/', '/tmp', path => path.endsWith('.yml'), ); const dir = await res.dir({ targetDir: '/tmp' }); diff --git a/packages/backend-common/src/reading/types.ts b/packages/backend-common/src/reading/types.ts index c727ba078b..d5c24ed1db 100644 --- a/packages/backend-common/src/reading/types.ts +++ b/packages/backend-common/src/reading/types.ts @@ -83,6 +83,10 @@ export type ReadTreeResponseDirOptions = { export type ReadTreeArchiveResponse = { files(): Promise; archive(): Promise; + + /** + * dir() extracts the tree response into a directory and returns the path of the directory. + */ dir(options?: ReadTreeResponseDirOptions): Promise; }; From 7078d35e0af4c0ef44cd63ae47f2f1361bab460c Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Sun, 17 Jan 2021 14:03:43 +0100 Subject: [PATCH 071/209] backend-common: UrlReader/Bitbucket implement SHA based caching Default branch detection was already implemented --- .../src/reading/BitbucketUrlReader.test.ts | 157 ++++++++++-------- .../src/reading/BitbucketUrlReader.ts | 13 +- .../src/reading/GithubUrlReader.test.ts | 20 --- .../src/reading/GitlabUrlReader.test.ts | 18 -- 4 files changed, 96 insertions(+), 112 deletions(-) diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts index e327770114..111aff753c 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts @@ -20,6 +20,7 @@ import fs from 'fs'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import path from 'path'; +import { NotModifiedError } from '../errors'; import { BitbucketUrlReader } from './BitbucketUrlReader'; import { ReadTreeResponseFactory } from './tree'; @@ -27,15 +28,24 @@ const treeResponseFactory = ReadTreeResponseFactory.create({ config: new ConfigReader({}), }); +const bitbucketProcessor = new BitbucketUrlReader( + { host: 'bitbucket.org', apiBaseUrl: 'https://api.bitbucket.org/2.0' }, + { treeResponseFactory }, +); + +const hostedBitbucketProcessor = new BitbucketUrlReader( + { + host: 'bitbucket.mycompany.net', + apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', + }, + { treeResponseFactory }, +); + describe('BitbucketUrlReader', () => { describe('implementation', () => { it('rejects unknown targets', async () => { - const processor = new BitbucketUrlReader( - { host: 'bitbucket.org', apiBaseUrl: 'https://api.bitbucket.org/2.0' }, - { treeResponseFactory }, - ); await expect( - processor.read('https://not.bitbucket.com/apa'), + bitbucketProcessor.read('https://not.bitbucket.com/apa'), ).rejects.toThrow( 'Incorrect URL: https://not.bitbucket.com/apa, Error: Invalid Bitbucket URL or file path', ); @@ -55,8 +65,30 @@ describe('BitbucketUrlReader', () => { ), ); - it('returns the wanted files from an archive', async () => { + const privateBitbucketRepoBuffer = fs.readFileSync( + path.resolve( + 'src', + 'reading', + '__fixtures__', + 'bitbucket-server-repo.zip', + ), + ); + + beforeEach(() => { worker.use( + rest.get( + 'https://api.bitbucket.org/2.0/repositories/backstage/mock', + (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + mainbranch: { + type: 'branch', + name: 'master', + }, + }), + ), + ), rest.get( 'https://bitbucket.org/backstage/mock/get/master.zip', (_, res, ctx) => @@ -76,17 +108,35 @@ describe('BitbucketUrlReader', () => { }), ), ), + rest.get( + 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive?format=zip&prefix=mock&path=docs', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/zip'), + ctx.body(privateBitbucketRepoBuffer), + ), + ), + rest.get( + 'https://api.bitbucket.mycompany.net/rest/api/1.0/repositories/backstage/mock/commits/some-branch', + (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + values: [{ hash: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }], + }), + ), + ), ); + }); - const processor = new BitbucketUrlReader( - { host: 'bitbucket.org', apiBaseUrl: 'https://api.bitbucket.org/2.0' }, - { treeResponseFactory }, - ); - - const response = await processor.readTree( + it('returns the wanted files from an archive', async () => { + const response = await bitbucketProcessor.readTree( 'https://bitbucket.org/backstage/mock/src/master', ); + expect(response.sha).toBe('12ab34cd56ef'); + const files = await response.files(); expect(files.length).toBe(2); @@ -98,38 +148,12 @@ describe('BitbucketUrlReader', () => { }); it('uses private bitbucket host', async () => { - const privateBitbucketRepoBuffer = fs.readFileSync( - path.resolve( - 'src', - 'reading', - '__fixtures__', - 'bitbucket-server-repo.zip', - ), - ); - worker.use( - rest.get( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive?format=zip&prefix=mock&path=docs', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/zip'), - ctx.body(privateBitbucketRepoBuffer), - ), - ), - ); - - const processor = new BitbucketUrlReader( - { - host: 'bitbucket.mycompany.net', - apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', - }, - { treeResponseFactory }, - ); - - const response = await processor.readTree( + const response = await hostedBitbucketProcessor.readTree( 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs?at=some-branch', ); + expect(response.sha).toBe('12ab34cd56ef'); + const files = await response.files(); expect(files.length).toBe(1); @@ -139,37 +163,12 @@ describe('BitbucketUrlReader', () => { }); it('returns the wanted files from an archive with a subpath', async () => { - worker.use( - rest.get( - 'https://bitbucket.org/backstage/mock/get/master.zip', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/zip'), - ctx.body(repoBuffer), - ), - ), - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage/mock/commits/master', - (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - values: [{ hash: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }], - }), - ), - ), - ); - - const processor = new BitbucketUrlReader( - { host: 'bitbucket.org', apiBaseUrl: 'https://api.bitbucket.org/2.0' }, - { treeResponseFactory }, - ); - - const response = await processor.readTree( + const response = await bitbucketProcessor.readTree( 'https://bitbucket.org/backstage/mock/src/master/docs', ); + expect(response.sha).toBe('12ab34cd56ef'); + const files = await response.files(); expect(files.length).toBe(1); @@ -177,5 +176,25 @@ describe('BitbucketUrlReader', () => { expect(indexMarkdownFile.toString()).toBe('# Test\n'); }); + + it('throws a NotModifiedError when given a sha in options', async () => { + const fnBitbucket = async () => { + await bitbucketProcessor.readTree( + 'https://bitbucket.org/backstage/mock', + { sha: '12ab34cd56ef' }, + ); + }; + + await expect(fnBitbucket).rejects.toThrow(NotModifiedError); + }); + + it('should not throw a NotModifiedError when given an outdated sha in options', async () => { + const response = await bitbucketProcessor.readTree( + 'https://bitbucket.org/backstage/mock', + { sha: 'outdatedSha123abc' }, + ); + + expect(response.sha).toBe('12ab34cd56ef'); + }); }); }); diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.ts b/packages/backend-common/src/reading/BitbucketUrlReader.ts index 7f462e5bf0..606aecd023 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.ts @@ -25,7 +25,7 @@ import { import fetch from 'cross-fetch'; import parseGitUrl from 'git-url-parse'; import { Readable } from 'stream'; -import { NotFoundError } from '../errors'; +import { NotFoundError, NotModifiedError } from '../errors'; import { ReadTreeResponseFactory } from './tree'; import { ReaderFactory, @@ -105,6 +105,11 @@ export class BitbucketUrlReader implements UrlReader { url, ); + const lastCommitShortHash = await this.getLastCommitShortHash(url); + if (options?.sha && options.sha === lastCommitShortHash) { + throw new NotModifiedError(); + } + const isHosted = resource === 'bitbucket.org'; const downloadUrl = await getBitbucketDownloadUrl(url, this.config); @@ -122,7 +127,6 @@ export class BitbucketUrlReader implements UrlReader { let folderPath = `${project}-${repoName}`; if (isHosted) { - const lastCommitShortHash = await this.getLastCommitShortHash(url); folderPath = `${project}-${repoName}-${lastCommitShortHash}`; } @@ -133,8 +137,7 @@ export class BitbucketUrlReader implements UrlReader { }); const response = archiveResponse as ReadTreeResponse; - // TODO: Just a placeholder for now. - response.sha = ''; + response.sha = lastCommitShortHash; return response; } @@ -147,7 +150,7 @@ export class BitbucketUrlReader implements UrlReader { return `bitbucket{host=${host},authed=${authed}}`; } - private async getLastCommitShortHash(url: string): Promise { + private async getLastCommitShortHash(url: string): Promise { const { name: repoName, owner: project, ref } = parseGitUrl(url); let branch = ref; diff --git a/packages/backend-common/src/reading/GithubUrlReader.test.ts b/packages/backend-common/src/reading/GithubUrlReader.test.ts index 24a49cf81c..a311da910a 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.test.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.test.ts @@ -133,7 +133,6 @@ describe('GithubUrlReader', () => { }; beforeEach(() => { - // For github.com host worker.use( rest.get( 'https://github.com/backstage/mock/archive/main.tar.gz', @@ -144,9 +143,6 @@ describe('GithubUrlReader', () => { ctx.body(repoBuffer), ), ), - ); - - worker.use( rest.get('https://api.github.com/repos/backstage/mock', (_, res, ctx) => res( ctx.status(200), @@ -154,9 +150,6 @@ describe('GithubUrlReader', () => { ctx.json(reposGithubApiResponse), ), ), - ); - - worker.use( rest.get( 'https://api.github.com/repos/backstage/mock/branches/main', (_, res, ctx) => @@ -166,17 +159,10 @@ describe('GithubUrlReader', () => { ctx.json(branchesApiResponse), ), ), - ); - - worker.use( rest.get( 'https://api.github.com/repos/backstage/mock/branches/branchDoesNotExist', (_, res, ctx) => res(ctx.status(404)), ), - ); - - // For a GHE host - worker.use( rest.get( 'https://ghe.github.com/backstage/mock/archive/main.tar.gz', (_, res, ctx) => @@ -186,9 +172,6 @@ describe('GithubUrlReader', () => { ctx.body(repoBuffer), ), ), - ); - - worker.use( rest.get( 'https://ghe.github.com/api/v3/repos/backstage/mock', (_, res, ctx) => @@ -198,9 +181,6 @@ describe('GithubUrlReader', () => { ctx.json(reposGheApiResponse), ), ), - ); - - worker.use( rest.get( 'https://ghe.github.com/api/v3/repos/backstage/mock/branches/main', (_, res, ctx) => diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts index a2dda48b16..823555d37d 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.test.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts @@ -179,9 +179,6 @@ describe('GitlabUrlReader', () => { ctx.body(archiveBuffer), ), ), - ); - - worker.use( rest.get( 'https://gitlab.com/api/v4/projects/backstage%2Fmock', (_, res, ctx) => @@ -191,9 +188,6 @@ describe('GitlabUrlReader', () => { ctx.json(projectGitlabApiResponse), ), ), - ); - - worker.use( rest.get( 'https://gitlab.com/api/v4/projects/backstage%2Fmock/repository/branches/main', (_, res, ctx) => @@ -203,16 +197,10 @@ describe('GitlabUrlReader', () => { ctx.json(branchGitlabApiResponse), ), ), - ); - - worker.use( rest.get( 'https://gitlab.com/api/v4/projects/backstage%2Fmock/repository/branches/branchDoesNotExist', (_, res, ctx) => res(ctx.status(404)), ), - ); - - worker.use( rest.get( 'https://gitlab.mycompany.com/api/v4/projects/backstage%2Fmock', (_, res, ctx) => @@ -222,9 +210,6 @@ describe('GitlabUrlReader', () => { ctx.json(projectGitlabApiResponse), ), ), - ); - - worker.use( rest.get( 'https://gitlab.mycompany.com/api/v4/projects/backstage%2Fmock/repository/branches/main', (_, res, ctx) => @@ -234,9 +219,6 @@ describe('GitlabUrlReader', () => { ctx.json(branchGitlabApiResponse), ), ), - ); - - worker.use( rest.get( 'https://gitlab.mycompany.com/api/v4/projects/backstage%2Fmock/repository/archive.zip?sha=main', (_, res, ctx) => From 79f9a97428ea781136f46773c23769a52dfd460b Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Sun, 17 Jan 2021 15:02:55 +0100 Subject: [PATCH 072/209] backend-common: UrlReader/Azure implement SHA based caching --- .../src/reading/AzureUrlReader.test.ts | 62 +++++++++++++++++-- .../src/reading/AzureUrlReader.ts | 25 +++++++- packages/integration/src/azure/core.ts | 56 +++++++++++++++++ packages/integration/src/azure/index.ts | 1 + 4 files changed, 136 insertions(+), 8 deletions(-) diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts index f729316223..b492b26609 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.test.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts @@ -23,6 +23,7 @@ import { getVoidLogger } from '../logging'; import { AzureUrlReader } from './AzureUrlReader'; import { msw } from '@backstage/test-utils'; import { ReadTreeResponseFactory } from './tree'; +import { NotModifiedError } from '../errors'; const logger = getVoidLogger(); @@ -142,6 +143,11 @@ describe('AzureUrlReader', () => { path.resolve('src', 'reading', '__fixtures__', 'mock-main.zip'), ); + const processor = new AzureUrlReader( + { host: 'dev.azure.com' }, + { treeResponseFactory }, + ); + beforeEach(() => { worker.use( rest.get( @@ -153,19 +159,65 @@ describe('AzureUrlReader', () => { ctx.body(repoBuffer), ), ), + rest.get( + // https://docs.microsoft.com/en-us/rest/api/azure/devops/git/commits/get%20commits?view=azure-devops-rest-6.0#on-a-branch + 'https://dev.azure.com/organization/project/_apis/git/repositories/repository/commits', + (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + count: 2, + value: [ + { + commitId: '123abc2', + comment: 'second commit', + }, + { + commitId: '123abc1', + comment: 'first commit', + }, + ], + }), + ), + ), ); }); it('returns the wanted files from an archive', async () => { - const processor = new AzureUrlReader( - { host: 'dev.azure.com' }, - { treeResponseFactory }, - ); - const response = await processor.readTree( 'https://dev.azure.com/organization/project/_git/repository', ); + expect(response.sha).toBe('123abc2'); + + const files = await response.files(); + + expect(files.length).toBe(2); + const mkDocsFile = await files[0].content(); + const indexMarkdownFile = await files[1].content(); + + expect(mkDocsFile.toString()).toBe('site_name: Test\n'); + expect(indexMarkdownFile.toString()).toBe('# Test\n'); + }); + + it('throws a NotModifiedError when given a sha in options', async () => { + const fnAzure = async () => { + await processor.readTree( + 'https://dev.azure.com/organization/project/_git/repository', + { sha: '123abc2' }, + ); + }; + + await expect(fnAzure).rejects.toThrow(NotModifiedError); + }); + + it('should not throw a NotModifiedError when given an outdated sha in options', async () => { + const response = await processor.readTree( + 'https://dev.azure.com/organization/project/_git/repository', + { sha: 'outdated123abc' }, + ); + + expect(response.sha).toBe('123abc2'); const files = await response.files(); expect(files.length).toBe(2); diff --git a/packages/backend-common/src/reading/AzureUrlReader.ts b/packages/backend-common/src/reading/AzureUrlReader.ts index 4535b329d7..c40291ec85 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.ts @@ -20,10 +20,11 @@ import { getAzureFileFetchUrl, getAzureDownloadUrl, getAzureRequestOptions, + getAzureCommitsUrl, } from '@backstage/integration'; import fetch from 'cross-fetch'; import { Readable } from 'stream'; -import { NotFoundError } from '../errors'; +import { NotFoundError, NotModifiedError } from '../errors'; import { ReaderFactory, ReadTreeOptions, @@ -75,6 +76,25 @@ export class AzureUrlReader implements UrlReader { url: string, options?: ReadTreeOptions, ): Promise { + // Get latest commit SHA + + const commitsAzureResponse = await fetch( + getAzureCommitsUrl(url), + getAzureRequestOptions(this.options), + ); + if (!commitsAzureResponse.ok) { + const message = `Failed to read tree from ${url}, ${commitsAzureResponse.status} ${commitsAzureResponse.statusText}`; + if (commitsAzureResponse.status === 404) { + throw new NotFoundError(message); + } + throw new Error(message); + } + + const commitSha = (await commitsAzureResponse.json()).value[0].commitId; + if (options?.sha && options.sha === commitSha) { + throw new NotModifiedError(); + } + const archiveAzureResponse = await fetch( getAzureDownloadUrl(url), getAzureRequestOptions(this.options, { Accept: 'application/zip' }), @@ -93,8 +113,7 @@ export class AzureUrlReader implements UrlReader { }); const response = archiveResponse as ReadTreeResponse; - // TODO: Just a placeholder for now. - response.sha = ''; + response.sha = commitSha; return response; } diff --git a/packages/integration/src/azure/core.ts b/packages/integration/src/azure/core.ts index 7900774c79..73ce4f83d7 100644 --- a/packages/integration/src/azure/core.ts +++ b/packages/integration/src/azure/core.ts @@ -108,6 +108,62 @@ export function getAzureDownloadUrl(url: string): string { return `${protocol}://${resource}/${organization}/${project}/_apis/git/repositories/${repoName}/items?recursionLevel=full&download=true&api-version=6.0${scopePath}`; } +/** + * Given a URL, return the API URL to fetch commits on the branch. + * + * @param url A URL pointing to a repository or a sub-path + */ +export function getAzureCommitsUrl(url: string): string { + try { + const parsedUrl = new URL(url); + + const [ + empty, + userOrOrg, + project, + srcKeyword, + repoName, + ] = parsedUrl.pathname.split('/'); + + // Remove the "GB" from "GBmain" for example. + const ref = parsedUrl.searchParams.get('version')?.substr(2); + + if ( + empty !== '' || + userOrOrg === '' || + project === '' || + srcKeyword !== '_git' || + repoName === '' + ) { + throw new Error('Wrong Azure Devops URL'); + } + + // transform to commits api + parsedUrl.pathname = [ + empty, + userOrOrg, + project, + '_apis', + 'git', + 'repositories', + repoName, + 'commits', + ].join('/'); + + const queryParams = []; + if (ref) { + queryParams.push(`searchCriteria.itemVersion.version=${ref}`); + } + parsedUrl.search = queryParams.join('&'); + + parsedUrl.protocol = 'https'; + + return parsedUrl.toString(); + } catch (e) { + throw new Error(`Incorrect URL: ${url}, ${e}`); + } +} + /** * Gets the request options necessary to make requests to a given provider. * diff --git a/packages/integration/src/azure/index.ts b/packages/integration/src/azure/index.ts index 365e4cdcdc..6d57437779 100644 --- a/packages/integration/src/azure/index.ts +++ b/packages/integration/src/azure/index.ts @@ -23,4 +23,5 @@ export { getAzureDownloadUrl, getAzureFileFetchUrl, getAzureRequestOptions, + getAzureCommitsUrl, } from './core'; From 8565ad2279c157cd0c404499efe51b6942a6b685 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Mon, 18 Jan 2021 22:50:47 +0100 Subject: [PATCH 073/209] 1. Use etag over sha for the identifier 2. Re-combine Resonse types into one --- .changeset/khaki-icons-trade.md | 10 ++++-- .github/styles/vocab.txt | 1 + .../src/reading/AzureUrlReader.test.ts | 12 +++---- .../src/reading/AzureUrlReader.ts | 9 ++---- .../src/reading/BitbucketUrlReader.test.ts | 16 +++++----- .../src/reading/BitbucketUrlReader.ts | 9 ++---- .../src/reading/GithubUrlReader.test.ts | 14 ++++----- .../src/reading/GithubUrlReader.ts | 9 ++---- .../src/reading/GitlabUrlReader.test.ts | 10 +++--- .../src/reading/GitlabUrlReader.ts | 9 ++---- .../reading/tree/ReadTreeResponseFactory.ts | 14 ++++----- .../reading/tree/TarArchiveResponse.test.ts | 31 +++++++++++++------ .../src/reading/tree/TarArchiveResponse.ts | 8 +++-- .../reading/tree/ZipArchiveResponse.test.ts | 31 +++++++++++++------ .../src/reading/tree/ZipArchiveResponse.ts | 8 +++-- packages/backend-common/src/reading/types.ts | 27 ++++++++-------- packages/techdocs-common/src/helpers.test.ts | 2 +- 17 files changed, 124 insertions(+), 96 deletions(-) diff --git a/.changeset/khaki-icons-trade.md b/.changeset/khaki-icons-trade.md index 37bafd8d33..4d30c3977e 100644 --- a/.changeset/khaki-icons-trade.md +++ b/.changeset/khaki-icons-trade.md @@ -2,7 +2,11 @@ '@backstage/backend-common': patch --- -1. URL Reader's `readTree` method now returns a `sha` in the response along with the blob. The SHA belongs to the latest commit on the target. `readTree` also takes an optional `sha` in its options and throws a `NotModifiedError` if the SHA matches with the latest commit SHA on the url's target. This can be used in building a cache when working with URL Reader. +1. URL Reader's `readTree` method now returns an `etag` in the response along with the blob. The etag is an identifier of the blob and will only change if the blob is modified on the target. Usually it is set to the latest commit SHA on the target. + +`readTree` also takes an optional `etag` in its options and throws a `NotModifiedError` if the etag matches with the etag of the resource. + +So, the `etag` can be used in building a cache when working with URL Reader. An example - @@ -11,11 +15,11 @@ const response = await reader.readTree( 'https://github.com/backstage/backstage', ); -const commitSha = response.sha; +const etag = response.etag; // Will throw a new NotModifiedError (exported from @backstage/backstage-common) await reader.readTree('https://github.com/backstage/backstage', { - sha: commitSha, + etag, }); ``` diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index c3c67ab66e..96581d4b79 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -67,6 +67,7 @@ Dominik dtuite dzolotusky Ek +etag env Env eslint diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts index b492b26609..20f8feba42 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.test.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts @@ -188,7 +188,7 @@ describe('AzureUrlReader', () => { 'https://dev.azure.com/organization/project/_git/repository', ); - expect(response.sha).toBe('123abc2'); + expect(response.etag).toBe('123abc2'); const files = await response.files(); @@ -200,24 +200,24 @@ describe('AzureUrlReader', () => { expect(indexMarkdownFile.toString()).toBe('# Test\n'); }); - it('throws a NotModifiedError when given a sha in options', async () => { + it('throws a NotModifiedError when given a etag in options', async () => { const fnAzure = async () => { await processor.readTree( 'https://dev.azure.com/organization/project/_git/repository', - { sha: '123abc2' }, + { etag: '123abc2' }, ); }; await expect(fnAzure).rejects.toThrow(NotModifiedError); }); - it('should not throw a NotModifiedError when given an outdated sha in options', async () => { + it('should not throw a NotModifiedError when given an outdated etag in options', async () => { const response = await processor.readTree( 'https://dev.azure.com/organization/project/_git/repository', - { sha: 'outdated123abc' }, + { etag: 'outdated123abc' }, ); - expect(response.sha).toBe('123abc2'); + expect(response.etag).toBe('123abc2'); const files = await response.files(); expect(files.length).toBe(2); diff --git a/packages/backend-common/src/reading/AzureUrlReader.ts b/packages/backend-common/src/reading/AzureUrlReader.ts index c40291ec85..59e437607f 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.ts @@ -91,7 +91,7 @@ export class AzureUrlReader implements UrlReader { } const commitSha = (await commitsAzureResponse.json()).value[0].commitId; - if (options?.sha && options.sha === commitSha) { + if (options?.etag && options.etag === commitSha) { throw new NotModifiedError(); } @@ -107,14 +107,11 @@ export class AzureUrlReader implements UrlReader { throw new Error(message); } - const archiveResponse = await this.deps.treeResponseFactory.fromZipArchive({ + return await this.deps.treeResponseFactory.fromZipArchive({ stream: (archiveAzureResponse.body as unknown) as Readable, + etag: commitSha, filter: options?.filter, }); - - const response = archiveResponse as ReadTreeResponse; - response.sha = commitSha; - return response; } toString() { diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts index 111aff753c..3542e822e1 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts @@ -135,7 +135,7 @@ describe('BitbucketUrlReader', () => { 'https://bitbucket.org/backstage/mock/src/master', ); - expect(response.sha).toBe('12ab34cd56ef'); + expect(response.etag).toBe('12ab34cd56ef'); const files = await response.files(); @@ -152,7 +152,7 @@ describe('BitbucketUrlReader', () => { 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs?at=some-branch', ); - expect(response.sha).toBe('12ab34cd56ef'); + expect(response.etag).toBe('12ab34cd56ef'); const files = await response.files(); @@ -167,7 +167,7 @@ describe('BitbucketUrlReader', () => { 'https://bitbucket.org/backstage/mock/src/master/docs', ); - expect(response.sha).toBe('12ab34cd56ef'); + expect(response.etag).toBe('12ab34cd56ef'); const files = await response.files(); @@ -177,24 +177,24 @@ describe('BitbucketUrlReader', () => { expect(indexMarkdownFile.toString()).toBe('# Test\n'); }); - it('throws a NotModifiedError when given a sha in options', async () => { + it('throws a NotModifiedError when given a etag in options', async () => { const fnBitbucket = async () => { await bitbucketProcessor.readTree( 'https://bitbucket.org/backstage/mock', - { sha: '12ab34cd56ef' }, + { etag: '12ab34cd56ef' }, ); }; await expect(fnBitbucket).rejects.toThrow(NotModifiedError); }); - it('should not throw a NotModifiedError when given an outdated sha in options', async () => { + it('should not throw a NotModifiedError when given an outdated etag in options', async () => { const response = await bitbucketProcessor.readTree( 'https://bitbucket.org/backstage/mock', - { sha: 'outdatedSha123abc' }, + { etag: 'outdatedetag123abc' }, ); - expect(response.sha).toBe('12ab34cd56ef'); + expect(response.etag).toBe('12ab34cd56ef'); }); }); }); diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.ts b/packages/backend-common/src/reading/BitbucketUrlReader.ts index 606aecd023..869870f248 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.ts @@ -106,7 +106,7 @@ export class BitbucketUrlReader implements UrlReader { ); const lastCommitShortHash = await this.getLastCommitShortHash(url); - if (options?.sha && options.sha === lastCommitShortHash) { + if (options?.etag && options.etag === lastCommitShortHash) { throw new NotModifiedError(); } @@ -130,15 +130,12 @@ export class BitbucketUrlReader implements UrlReader { folderPath = `${project}-${repoName}-${lastCommitShortHash}`; } - const archiveResponse = await this.treeResponseFactory.fromZipArchive({ + return await this.treeResponseFactory.fromZipArchive({ stream: (archiveBitbucketResponse.body as unknown) as Readable, path: `${folderPath}/${filepath}`, + etag: lastCommitShortHash, filter: options?.filter, }); - - const response = archiveResponse as ReadTreeResponse; - response.sha = lastCommitShortHash; - return response; } toString() { diff --git a/packages/backend-common/src/reading/GithubUrlReader.test.ts b/packages/backend-common/src/reading/GithubUrlReader.test.ts index a311da910a..26ba91bd29 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.test.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.test.ts @@ -128,7 +128,7 @@ describe('GithubUrlReader', () => { const branchesApiResponse = { name: 'main', commit: { - sha: 'sha123abc', + sha: 'etag123abc', }, }; @@ -198,7 +198,7 @@ describe('GithubUrlReader', () => { 'https://github.com/backstage/mock/tree/main', ); - expect(response.sha).toBe('sha123abc'); + expect(response.etag).toBe('etag123abc'); const files = await response.files(); @@ -272,10 +272,10 @@ describe('GithubUrlReader', () => { expect(indexMarkdownFile.toString()).toBe('# Test\n'); }); - it('throws a NotModifiedError when given a sha in options', async () => { + it('throws a NotModifiedError when given a etag in options', async () => { const fnGithub = async () => { await githubProcessor.readTree('https://github.com/backstage/mock', { - sha: 'sha123abc', + etag: 'etag123abc', }); }; @@ -283,7 +283,7 @@ describe('GithubUrlReader', () => { await gheProcessor.readTree( 'https://ghe.github.com/backstage/mock/tree/main/docs', { - sha: 'sha123abc', + etag: 'etag123abc', }, ); }; @@ -292,11 +292,11 @@ describe('GithubUrlReader', () => { await expect(fnGhe).rejects.toThrow(NotModifiedError); }); - it('should not throw error when given an outdated sha in options', async () => { + it('should not throw error when given an outdated etag in options', async () => { const response = await githubProcessor.readTree( 'https://github.com/backstage/mock/tree/main', { - sha: 'outdatedSha123abc', + etag: 'outdatedetag123abc', }, ); expect((await response.files()).length).toBe(2); diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts index 8e0410642b..1a8133fceb 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.ts @@ -151,7 +151,7 @@ export class GithubUrlReader implements UrlReader { } const commitSha = (await branchGitHubResponse.json()).commit.sha; - if (options?.sha && options.sha === commitSha) { + if (options?.etag && options.etag === commitSha) { throw new NotModifiedError(); } @@ -179,17 +179,14 @@ export class GithubUrlReader implements UrlReader { const path = `${repoName}-${branch}/${filepath}`; - const archiveResponse = await this.deps.treeResponseFactory.fromTarArchive({ + return await this.deps.treeResponseFactory.fromTarArchive({ // TODO(Rugvip): Underlying implementation of fetch will be node-fetch, we probably want // to stick to using that in exclusively backend code. stream: (archive.body as unknown) as Readable, path, + etag: commitSha, filter: options?.filter, }); - - const response = archiveResponse as ReadTreeResponse; - response.sha = commitSha; - return response; } toString() { diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts index 823555d37d..2e66794397 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.test.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts @@ -284,10 +284,10 @@ describe('GitlabUrlReader', () => { expect(indexMarkdownFile.toString()).toBe('# Test\n'); }); - it('throws a NotModifiedError when given a sha in options', async () => { + it('throws a NotModifiedError when given a etag in options', async () => { const fnGitlab = async () => { await gitlabProcessor.readTree('https://gitlab.com/backstage/mock', { - sha: 'sha123abc', + etag: 'sha123abc', }); }; @@ -295,7 +295,7 @@ describe('GitlabUrlReader', () => { await hostedGitlabProcessor.readTree( 'https://gitlab.mycompany.com/backstage/mock', { - sha: 'sha123abc', + etag: 'sha123abc', }, ); }; @@ -304,11 +304,11 @@ describe('GitlabUrlReader', () => { await expect(fnHostedGitlab).rejects.toThrow(NotModifiedError); }); - it('should not throw error when given an outdated sha in options', async () => { + it('should not throw error when given an outdated etag in options', async () => { const response = await gitlabProcessor.readTree( 'https://gitlab.com/backstage/mock/tree/main', { - sha: 'outdatedSha123abc', + etag: 'outdatedsha123abc', }, ); expect((await response.files()).length).toBe(2); diff --git a/packages/backend-common/src/reading/GitlabUrlReader.ts b/packages/backend-common/src/reading/GitlabUrlReader.ts index 0c0ce4f565..647c6fd644 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.ts @@ -121,7 +121,7 @@ export class GitlabUrlReader implements UrlReader { const commitSha = (await branchGitlabResponse.json()).commit.id; - if (options?.sha && options.sha === commitSha) { + if (options?.etag && options.etag === commitSha) { throw new NotModifiedError(); } @@ -144,15 +144,12 @@ export class GitlabUrlReader implements UrlReader { ? `${repoName}-${branch}-${commitSha}/${filepath}/` : ''; - const archiveResponse = await this.treeResponseFactory.fromZipArchive({ + return await this.treeResponseFactory.fromZipArchive({ stream: (archiveGitLabResponse.body as unknown) as Readable, path, + etag: commitSha, filter: options?.filter, }); - - const response = archiveResponse as ReadTreeResponse; - response.sha = commitSha; - return response; } toString() { diff --git a/packages/backend-common/src/reading/tree/ReadTreeResponseFactory.ts b/packages/backend-common/src/reading/tree/ReadTreeResponseFactory.ts index cc229e9d5a..7332154d09 100644 --- a/packages/backend-common/src/reading/tree/ReadTreeResponseFactory.ts +++ b/packages/backend-common/src/reading/tree/ReadTreeResponseFactory.ts @@ -17,7 +17,7 @@ import os from 'os'; import { Readable } from 'stream'; import { Config } from '@backstage/config'; -import { ReadTreeArchiveResponse } from '../types'; +import { ReadTreeResponse } from '../types'; import { TarArchiveResponse } from './TarArchiveResponse'; import { ZipArchiveResponse } from './ZipArchiveResponse'; @@ -26,6 +26,8 @@ type FromArchiveOptions = { stream: Readable; // If set, the root of the tree will be set to the given directory path. path?: string; + // etag of the blob + etag: string; // Filter passed on from the ReadTreeOptions filter?: (path: string) => boolean; }; @@ -40,24 +42,22 @@ export class ReadTreeResponseFactory { constructor(private readonly workDir: string) {} - async fromTarArchive( - options: FromArchiveOptions, - ): Promise { + async fromTarArchive(options: FromArchiveOptions): Promise { return new TarArchiveResponse( options.stream, options.path ?? '', this.workDir, + options.etag, options.filter, ); } - async fromZipArchive( - options: FromArchiveOptions, - ): Promise { + async fromZipArchive(options: FromArchiveOptions): Promise { return new ZipArchiveResponse( options.stream, options.path ?? '', this.workDir, + options.etag, options.filter, ); } diff --git a/packages/backend-common/src/reading/tree/TarArchiveResponse.test.ts b/packages/backend-common/src/reading/tree/TarArchiveResponse.test.ts index bbeee00c70..2cbfc4a89e 100644 --- a/packages/backend-common/src/reading/tree/TarArchiveResponse.test.ts +++ b/packages/backend-common/src/reading/tree/TarArchiveResponse.test.ts @@ -38,7 +38,7 @@ describe('TarArchiveResponse', () => { it('should read files', async () => { const stream = fs.createReadStream('/test-archive.tar.gz'); - const res = new TarArchiveResponse(stream, 'mock-main/', '/tmp'); + const res = new TarArchiveResponse(stream, 'mock-main/', '/tmp', 'etag'); const files = await res.files(); expect(files).toEqual([ @@ -61,8 +61,12 @@ describe('TarArchiveResponse', () => { it('should read files with filter', async () => { const stream = fs.createReadStream('/test-archive.tar.gz'); - const res = new TarArchiveResponse(stream, 'mock-main/', '/tmp', path => - path.endsWith('.yml'), + const res = new TarArchiveResponse( + stream, + 'mock-main/', + '/tmp', + 'etag', + path => path.endsWith('.yml'), ); const files = await res.files(); @@ -79,14 +83,14 @@ describe('TarArchiveResponse', () => { it('should read as archive and files', async () => { const stream = fs.createReadStream('/test-archive.tar.gz'); - const res = new TarArchiveResponse(stream, 'mock-main/', '/tmp'); + const res = new TarArchiveResponse(stream, 'mock-main/', '/tmp', 'etag'); const buffer = await res.archive(); await expect(res.archive()).rejects.toThrow( 'Response has already been read', ); - const res2 = new TarArchiveResponse(buffer, '', '/tmp'); + const res2 = new TarArchiveResponse(buffer, '', '/tmp', 'etag'); const files = await res2.files(); expect(files).toEqual([ @@ -109,7 +113,7 @@ describe('TarArchiveResponse', () => { it('should extract entire archive into directory', async () => { const stream = fs.createReadStream('/test-archive.tar.gz'); - const res = new TarArchiveResponse(stream, '', '/tmp'); + const res = new TarArchiveResponse(stream, '', '/tmp', 'etag'); const dir = await res.dir(); await expect( @@ -123,7 +127,12 @@ describe('TarArchiveResponse', () => { it('should extract archive into directory with a subpath', async () => { const stream = fs.createReadStream('/test-archive.tar.gz'); - const res = new TarArchiveResponse(stream, 'mock-main/docs/', '/tmp'); + const res = new TarArchiveResponse( + stream, + 'mock-main/docs/', + '/tmp', + 'etag', + ); const dir = await res.dir(); expect(dir).toMatch(/^[\/\\]tmp[\/\\].*$/); @@ -135,8 +144,12 @@ describe('TarArchiveResponse', () => { it('should extract archive into directory with a subpath and filter', async () => { const stream = fs.createReadStream('/test-archive.tar.gz'); - const res = new TarArchiveResponse(stream, 'mock-main/', '/tmp', path => - path.endsWith('.yml'), + const res = new TarArchiveResponse( + stream, + 'mock-main/', + '/tmp', + 'etag', + path => path.endsWith('.yml'), ); const dir = await res.dir({ targetDir: '/tmp' }); diff --git a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts index e8929fbe9b..f44adcc7b2 100644 --- a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts +++ b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts @@ -21,7 +21,7 @@ import { Readable, pipeline as pipelineCb } from 'stream'; import { promisify } from 'util'; import concatStream from 'concat-stream'; import { - ReadTreeArchiveResponse, + ReadTreeResponse, ReadTreeResponseFile, ReadTreeResponseDirOptions, } from '../types'; @@ -34,13 +34,15 @@ const pipeline = promisify(pipelineCb); /** * Wraps a tar archive stream into a tree response reader. */ -export class TarArchiveResponse implements ReadTreeArchiveResponse { +export class TarArchiveResponse implements ReadTreeResponse { private read = false; + public readonly etag; constructor( private readonly stream: Readable, private readonly subPath: string, private readonly workDir: string, + etag: string, private readonly filter?: (path: string) => boolean, ) { if (subPath) { @@ -53,6 +55,8 @@ export class TarArchiveResponse implements ReadTreeArchiveResponse { ); } } + + this.etag = etag; } // Make sure the input stream is only read once diff --git a/packages/backend-common/src/reading/tree/ZipArchiveResponse.test.ts b/packages/backend-common/src/reading/tree/ZipArchiveResponse.test.ts index 345ec7a783..b42ec79d81 100644 --- a/packages/backend-common/src/reading/tree/ZipArchiveResponse.test.ts +++ b/packages/backend-common/src/reading/tree/ZipArchiveResponse.test.ts @@ -38,7 +38,7 @@ describe('ZipArchiveResponse', () => { it('should read files', async () => { const stream = fs.createReadStream('/test-archive.zip'); - const res = new ZipArchiveResponse(stream, 'mock-main/', '/tmp'); + const res = new ZipArchiveResponse(stream, 'mock-main/', '/tmp', 'etag'); const files = await res.files(); expect(files).toEqual([ @@ -61,8 +61,12 @@ describe('ZipArchiveResponse', () => { it('should read files with filter', async () => { const stream = fs.createReadStream('/test-archive.zip'); - const res = new ZipArchiveResponse(stream, 'mock-main/', '/tmp', path => - path.endsWith('.yml'), + const res = new ZipArchiveResponse( + stream, + 'mock-main/', + '/tmp', + 'etag', + path => path.endsWith('.yml'), ); const files = await res.files(); @@ -79,14 +83,14 @@ describe('ZipArchiveResponse', () => { it('should read as archive and files', async () => { const stream = fs.createReadStream('/test-archive.zip'); - const res = new ZipArchiveResponse(stream, 'mock-main/', '/tmp'); + const res = new ZipArchiveResponse(stream, 'mock-main/', '/tmp', 'etag'); const buffer = await res.archive(); await expect(res.archive()).rejects.toThrow( 'Response has already been read', ); - const res2 = new ZipArchiveResponse(buffer, '', '/tmp'); + const res2 = new ZipArchiveResponse(buffer, '', '/tmp', 'etag'); const files = await res2.files(); expect(files).toEqual([ @@ -109,7 +113,7 @@ describe('ZipArchiveResponse', () => { it('should extract entire archive into directory', async () => { const stream = fs.createReadStream('/test-archive.zip'); - const res = new ZipArchiveResponse(stream, '', '/tmp'); + const res = new ZipArchiveResponse(stream, '', '/tmp', 'etag'); const dir = await res.dir(); await expect( @@ -123,7 +127,12 @@ describe('ZipArchiveResponse', () => { it('should extract archive into directory with a subpath', async () => { const stream = fs.createReadStream('/test-archive.zip'); - const res = new ZipArchiveResponse(stream, 'mock-main/docs/', '/tmp'); + const res = new ZipArchiveResponse( + stream, + 'mock-main/docs/', + '/tmp', + 'etag', + ); const dir = await res.dir(); expect(dir).toMatch(/^[\/\\]tmp[\/\\].*$/); @@ -135,8 +144,12 @@ describe('ZipArchiveResponse', () => { it('should extract archive into directory with a subpath and filter', async () => { const stream = fs.createReadStream('/test-archive.zip'); - const res = new ZipArchiveResponse(stream, 'mock-main/', '/tmp', path => - path.endsWith('.yml'), + const res = new ZipArchiveResponse( + stream, + 'mock-main/', + '/tmp', + 'etag', + path => path.endsWith('.yml'), ); const dir = await res.dir({ targetDir: '/tmp' }); diff --git a/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts b/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts index 48ba4f19ee..7550dc1259 100644 --- a/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts +++ b/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts @@ -20,7 +20,7 @@ import unzipper, { Entry } from 'unzipper'; import archiver from 'archiver'; import { Readable } from 'stream'; import { - ReadTreeArchiveResponse, + ReadTreeResponse, ReadTreeResponseFile, ReadTreeResponseDirOptions, } from '../types'; @@ -28,13 +28,15 @@ import { /** * Wraps a zip archive stream into a tree response reader. */ -export class ZipArchiveResponse implements ReadTreeArchiveResponse { +export class ZipArchiveResponse implements ReadTreeResponse { private read = false; + public readonly etag; constructor( private readonly stream: Readable, private readonly subPath: string, private readonly workDir: string, + etag: string, private readonly filter?: (path: string) => boolean, ) { if (subPath) { @@ -47,6 +49,8 @@ export class ZipArchiveResponse implements ReadTreeArchiveResponse { ); } } + + this.etag = etag; } // Make sure the input stream is only read once diff --git a/packages/backend-common/src/reading/types.ts b/packages/backend-common/src/reading/types.ts index d5c24ed1db..e98f760d8f 100644 --- a/packages/backend-common/src/reading/types.ts +++ b/packages/backend-common/src/reading/types.ts @@ -34,17 +34,17 @@ export type ReadTreeOptions = { filter?(path: string): boolean; /** - * A commit SHA can be provided to check whether readTree's response has changed from a previous execution. + * An etag can be provided to check whether readTree's response has changed from a previous execution. * - * In the readTree() response, a SHA is returned along with the tree blob. The SHA belongs to the - * latest commit on the target repository's branch that was used to read the blob. + * In the readTree() response, an etag is returned along with the tree blob. The etag is a unique identifer + * of the tree blob, usually the commit SHA or etag from the target. * - * When a SHA is given in ReadTreeOptions, readTree will first compare the SHA against the latest commit - * on the target branch. If they match, it will throw a NotModifiedError indicating that the readTree - * response will not differ from the previous response which included this particular SHA. If they mismatch, - * readTree will return a new SHA along with the rest of ReadTreeResponse. + * When a etag is given in ReadTreeOptions, readTree will first compare the etag against the etag + * on the target branch. If they match, readTree will throw a NotModifiedError indicating that the readTree + * response will not differ from the previous response which included this particular etag. If they mismatch, + * readTree will return the rest of ReadTreeResponse along with a new etag. */ - sha?: string; + etag?: string; }; /** @@ -80,7 +80,7 @@ export type ReadTreeResponseDirOptions = { targetDir?: string; }; -export type ReadTreeArchiveResponse = { +export type ReadTreeResponse = { files(): Promise; archive(): Promise; @@ -88,8 +88,9 @@ export type ReadTreeArchiveResponse = { * dir() extracts the tree response into a directory and returns the path of the directory. */ dir(options?: ReadTreeResponseDirOptions): Promise; -}; -export interface ReadTreeResponse extends ReadTreeArchiveResponse { - sha: string; -} + /** + * A unique identifer of the tree blob, usually the commit SHA or etag from the target. + */ + etag: string; +}; diff --git a/packages/techdocs-common/src/helpers.test.ts b/packages/techdocs-common/src/helpers.test.ts index 88fdeed498..740a08aaa2 100644 --- a/packages/techdocs-common/src/helpers.test.ts +++ b/packages/techdocs-common/src/helpers.test.ts @@ -135,7 +135,7 @@ describe('getDocFilesFromRepository', () => { archive: async () => { return Readable.from(''); }, - sha: '', + etag: '', }; } } From baeed36324f6a275f3885da8a86aea78b0cbd8d8 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Mon, 18 Jan 2021 22:59:36 +0100 Subject: [PATCH 074/209] Convert NotModifiedError to 304 HTTP status code --- packages/backend-common/src/middleware/errorHandler.test.ts | 4 ++++ packages/backend-common/src/middleware/errorHandler.ts | 2 ++ 2 files changed, 6 insertions(+) diff --git a/packages/backend-common/src/middleware/errorHandler.test.ts b/packages/backend-common/src/middleware/errorHandler.test.ts index a7a3d64bd1..6d22c2f175 100644 --- a/packages/backend-common/src/middleware/errorHandler.test.ts +++ b/packages/backend-common/src/middleware/errorHandler.test.ts @@ -72,6 +72,9 @@ describe('errorHandler', () => { it('handles well-known error classes', async () => { const app = express(); + app.use('/NotModifiedError', () => { + throw new errors.NotModifiedError(); + }); app.use('/InputError', () => { throw new errors.InputError(); }); @@ -90,6 +93,7 @@ describe('errorHandler', () => { app.use(errorHandler()); const r = request(app); + expect((await r.get('/NotModifiedError')).status).toBe(304); expect((await r.get('/InputError')).status).toBe(400); expect((await r.get('/AuthenticationError')).status).toBe(401); expect((await r.get('/NotAllowedError')).status).toBe(403); diff --git a/packages/backend-common/src/middleware/errorHandler.ts b/packages/backend-common/src/middleware/errorHandler.ts index 7365ce8b93..a08849813d 100644 --- a/packages/backend-common/src/middleware/errorHandler.ts +++ b/packages/backend-common/src/middleware/errorHandler.ts @@ -101,6 +101,8 @@ function getStatusCode(error: Error): number { // Handle well-known error types switch (error.name) { + case errors.NotModifiedError.name: + return 304; case errors.InputError.name: return 400; case errors.AuthenticationError.name: From 380dd626fb1f42c24c8ce77eddfc5b93caa389e5 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Mon, 18 Jan 2021 23:04:31 +0100 Subject: [PATCH 075/209] Safe nullish check with git-url-parse library responses --- packages/backend-common/src/reading/GithubUrlReader.ts | 2 +- packages/backend-common/src/reading/GitlabUrlReader.ts | 2 +- packages/integration/src/azure/core.ts | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts index 1a8133fceb..d6612a9da2 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.ts @@ -130,7 +130,7 @@ export class GithubUrlReader implements UrlReader { // ref is an empty string if no branch is set in provided url to readTree. // Use GitHub API to get the default branch of the repository. - const branch = ref === '' ? repoResponseJson.default_branch : ref; + const branch = ref || repoResponseJson.default_branch; const branchesApiUrl = repoResponseJson.branches_url; // Fetch the latest commit in the provided or default branch to compare against diff --git a/packages/backend-common/src/reading/GitlabUrlReader.ts b/packages/backend-common/src/reading/GitlabUrlReader.ts index 647c6fd644..72db901ac1 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.ts @@ -99,7 +99,7 @@ export class GitlabUrlReader implements UrlReader { const projectGitlabResponseJson = await projectGitlabResponse.json(); // ref is an empty string if no branch is set in provided url to readTree. - const branch = ref === '' ? projectGitlabResponseJson.default_branch : ref; + const branch = ref || projectGitlabResponseJson.default_branch; // Fetch the latest commit in the provided or default branch to compare against // the provided sha. diff --git a/packages/integration/src/azure/core.ts b/packages/integration/src/azure/core.ts index 73ce4f83d7..b2878af89e 100644 --- a/packages/integration/src/azure/core.ts +++ b/packages/integration/src/azure/core.ts @@ -129,11 +129,11 @@ export function getAzureCommitsUrl(url: string): string { const ref = parsedUrl.searchParams.get('version')?.substr(2); if ( - empty !== '' || - userOrOrg === '' || - project === '' || + !!empty || + !userOrOrg || + !project || srcKeyword !== '_git' || - repoName === '' + !repoName ) { throw new Error('Wrong Azure Devops URL'); } From aca5158c7f03b3d7439aee9d162891fcb8fb5bed Mon Sep 17 00:00:00 2001 From: Jonah Back Date: Tue, 12 Jan 2021 14:50:04 -0800 Subject: [PATCH 076/209] feature: add auth backend for AWS ALB --- plugins/auth-backend/package.json | 8 +- .../src/providers/aws-alb/index.ts | 15 ++ .../src/providers/aws-alb/provider.test.ts | 225 ++++++++++++++++++ .../src/providers/aws-alb/provider.ts | 138 +++++++++++ yarn.lock | 27 ++- 5 files changed, 402 insertions(+), 11 deletions(-) create mode 100644 plugins/auth-backend/src/providers/aws-alb/index.ts create mode 100644 plugins/auth-backend/src/providers/aws-alb/provider.test.ts create mode 100644 plugins/auth-backend/src/providers/aws-alb/provider.ts diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 8f88c69487..f8de2bc6f1 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -16,6 +16,11 @@ "url": "https://github.com/backstage/backstage", "directory": "plugins/auth-backend" }, + "jest": { + "moduleNameMapper": { + "jose/jwt/verify": "/../../../node_modules/jose/lib/jwt/verify" + } + }, "keywords": [ "backstage" ], @@ -44,7 +49,7 @@ "fs-extra": "^9.0.0", "got": "^11.5.2", "helmet": "^4.0.0", - "jose": "^1.27.1", + "jose": "^3.5.1", "jwt-decode": "^3.1.0", "knex": "^0.21.6", "moment": "^2.26.0", @@ -59,6 +64,7 @@ "passport-okta-oauth": "^0.0.1", "passport-onelogin-oauth": "^0.0.1", "passport-saml": "^2.0.0", + "r2": "^2.0.1", "uuid": "^8.0.0", "winston": "^3.2.1", "yn": "^4.0.0" diff --git a/plugins/auth-backend/src/providers/aws-alb/index.ts b/plugins/auth-backend/src/providers/aws-alb/index.ts new file mode 100644 index 0000000000..863d6e76e1 --- /dev/null +++ b/plugins/auth-backend/src/providers/aws-alb/index.ts @@ -0,0 +1,15 @@ +/* + * 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. + */ diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts new file mode 100644 index 0000000000..55c27fd7ff --- /dev/null +++ b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts @@ -0,0 +1,225 @@ +/* + * 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 express from 'express'; +import * as jwtVerify from 'jose/jwt/verify'; + +import { AwsAlbAuthProvider } from './provider'; +import { UserEntityV1alpha1 } from '@backstage/catalog-model'; + +const mockKey = async () => { + return `-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnuN4LlaJhaUpx+qZFTzYCrSBLk0I +yOlxJ2VW88mLAQGJ7HPAvOdylxZsItMnzCuqNzZvie8m/NJsOjhDncVkrw== +-----END PUBLIC KEY----- +`; +}; + +jest.mock('r2', () => ({ + __esModule: true, + default: () => { + return { + text: mockKey(), + }; + }, +})); + +jest.mock('jose/jwt/verify', () => { + return { + __esModule: true, + default: jest.fn(), + }; +}); + +const identityResolutionCallbackMock = async (): Promise< + UserEntityV1alpha1 +> => { + return { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'foo', + }, + spec: { + memberOf: [], + profile: { + displayName: 'Foo Bar', + }, + }, + }; +}; + +const identityResolutionCallbackRejectedMock = async (): Promise< + UserEntityV1alpha1 +> => { + throw new Error('failed'); +}; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('AwsALBAuthProvider', () => { + const catalogApi = { + /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ + addLocation: jest.fn(), + getEntities: jest.fn(), + getLocationByEntity: jest.fn(), + getLocationById: jest.fn(), + removeEntityByUid: jest.fn(), + getEntityByName: jest.fn(), + }; + + const tokenIssuer = { + issueToken: async () => { + return ''; + }, + listPublicKeys: jest.fn(), + }; + + const mockResponseSend = jest.fn(); + const mockRequest = ({ + header: jest.fn(() => { + return 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImZvbyIsImlzc3VlciI6ImZvbyJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.zUkMYAuMwC1T0tyHMpxXrkbFDa4aGhB8d9um_tI2hsI'; + }), + } as unknown) as express.Request; + const mockRequestWithoutJwt = ({ + header: jest.fn(() => { + return undefined; + }), + } as unknown) as express.Request; + const mockResponse = ({ + header: () => jest.fn(), + send: mockResponseSend, + } as unknown) as express.Response; + + describe('should transform to type OAuthResponse', () => { + it('when JWT is valid and identity is resolved successfully', async () => { + const provider = new AwsAlbAuthProvider( + getVoidLogger(), + catalogApi, + tokenIssuer, + { + region: 'us-west-2', + identityResolutionCallback: identityResolutionCallbackMock, + issuer: 'foo', + }, + ); + + jwtVerify.default.mockImplementationOnce(async () => { + return { + payload: { + sub: 'foo', + }, + }; + }); + + await provider.refresh(mockRequest, mockResponse); + + expect(mockResponseSend.mock.calls[0][0]).toEqual({ + backstageIdentity: { + id: 'foo', + idToken: '', + }, + profile: { + displayName: 'Foo Bar', + }, + providerInfo: {}, + }); + }); + }); + describe('should fail when', () => { + it('JWT is missing', async () => { + const provider = new AwsAlbAuthProvider( + getVoidLogger(), + catalogApi, + tokenIssuer, + { + region: 'us-west-2', + identityResolutionCallback: identityResolutionCallbackMock, + issuer: 'foo', + }, + ); + + await provider.refresh(mockRequestWithoutJwt, mockResponse); + + expect(mockResponseSend.mock.calls[0][0]).toEqual(401); + }); + + it('JWT is invalid', async () => { + const provider = new AwsAlbAuthProvider( + getVoidLogger(), + catalogApi, + tokenIssuer, + { + region: 'us-west-2', + identityResolutionCallback: identityResolutionCallbackMock, + issuer: 'foo', + }, + ); + + jwtVerify.default.mockImplementationOnce(async () => { + throw new Error('bad JWT'); + }); + + await provider.refresh(mockRequest, mockResponse); + + expect(mockResponseSend.mock.calls[0][0]).toEqual(401); + }); + + it('issuer is invalid', async () => { + const provider = new AwsAlbAuthProvider( + getVoidLogger(), + catalogApi, + tokenIssuer, + { + region: 'us-west-2', + identityResolutionCallback: identityResolutionCallbackMock, + issuer: 'foobar', + }, + ); + + jwtVerify.default.mockImplementationOnce(async () => { + return {}; + }); + + await provider.refresh(mockRequest, mockResponse); + + expect(mockResponseSend.mock.calls[0][0]).toEqual(401); + }); + + it('identity resolution callback rejects', async () => { + const provider = new AwsAlbAuthProvider( + getVoidLogger(), + catalogApi, + tokenIssuer, + { + region: 'us-west-2', + identityResolutionCallback: identityResolutionCallbackRejectedMock, + issuer: 'foo', + }, + ); + + jwtVerify.default.mockImplementationOnce(async () => { + return {}; + }); + + await provider.refresh(mockRequest, mockResponse); + + expect(mockResponseSend.mock.calls[0][0]).toEqual(401); + }); + }); +}); diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts new file mode 100644 index 0000000000..f00543302b --- /dev/null +++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts @@ -0,0 +1,138 @@ +/* + * 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 { + AuthProviderFactoryOptions, + AuthProviderRouteHandlers, +} from '../types'; +import { TokenIssuer } from '../../identity'; +import express from 'express'; +import r2 from 'r2'; +import * as crypto from 'crypto'; +import { KeyObject } from 'crypto'; +import { Logger } from 'winston'; +import jwtVerify from 'jose/jwt/verify'; +import { CatalogApi } from '@backstage/catalog-client'; +import { UserEntityV1alpha1 } from '@backstage/catalog-model'; + +const ALB_JWT_HEADER = 'x-amzn-oidc-data'; +/** + * A callback function that receives a verified JWT and returns a UserEntity + * @param {payload} The verified JWT payload + */ +type IdentityResolutionCallback = ( + payload: object, + catalogApi: CatalogApi, +) => Promise; +type AwsAlbAuthProviderOptions = { + region: string; + issuer: string; + identityResolutionCallback: IdentityResolutionCallback; +}; +export const getJWTHeaders = (input: string) => { + const encoded = input.split('.')[0]; + return JSON.parse(Buffer.from(encoded, 'base64').toString('utf8')); +}; + +export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { + private logger: Logger; + private readonly catalogClient: CatalogApi; + private tokenIssuer: TokenIssuer; + private options: AwsAlbAuthProviderOptions; + private readonly keyCache: { [key: string]: KeyObject }; + + constructor( + logger: Logger, + catalogClient: CatalogApi, + tokenIssuer: TokenIssuer, + options: AwsAlbAuthProviderOptions, + ) { + this.logger = logger; + this.catalogClient = catalogClient; + this.tokenIssuer = tokenIssuer; + this.options = options; + this.keyCache = {}; + } + frameHandler(): Promise { + return Promise.resolve(undefined); + } + + async refresh(req: express.Request, res: express.Response): Promise { + const jwt = req.header(ALB_JWT_HEADER); + if (jwt !== undefined) { + try { + const headers = getJWTHeaders(jwt); + const key = await this.getKey(headers.kid); + const verifiedToken = await jwtVerify(jwt, key, {}); + + if ( + this.options.issuer !== '' && + headers.issuer !== this.options.issuer + ) { + throw new Error('issuer mismatch on JWT'); + } + + const resolvedEntity = await this.options.identityResolutionCallback( + verifiedToken.payload, + this.catalogClient, + ); + res.send({ + providerInfo: {}, + profile: resolvedEntity?.spec?.profile, + backstageIdentity: { + id: resolvedEntity?.metadata?.name, + idToken: await this.tokenIssuer.issueToken({ + claims: { sub: resolvedEntity?.metadata?.name }, + }), + }, + }); + } catch (e) { + this.logger.error('exception occurred during JWT processing', e); + res.send(401); + } + } else { + res.send(401); + } + } + + start(): Promise { + return Promise.resolve(undefined); + } + + async getKey(keyId: string): Promise { + if (this.keyCache[keyId]) { + return this.keyCache[keyId]; + } + const keyText: string = await r2( + `https://public-keys.auth.elb.${this.options.region}.amazonaws.com/${keyId}`, + ).text; + const keyValue = crypto.createPublicKey(keyText); + this.keyCache[keyId] = keyValue; + return keyValue; + } +} + +export const createAwsAlbProvider = ( + { logger, catalogApi, tokenIssuer, config }: AuthProviderFactoryOptions, + identityResolver: IdentityResolutionCallback, +) => { + const region = config.getString('region'); + const issuer = config.getString('iss'); + return new AwsAlbAuthProvider(logger, catalogApi, tokenIssuer, { + region, + issuer, + identityResolutionCallback: identityResolver, + }); +}; diff --git a/yarn.lock b/yarn.lock index 6710c7e8e7..0098954754 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9781,7 +9781,7 @@ case-sensitive-paths-webpack-plugin@^2.2.0: resolved "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.3.0.tgz#23ac613cc9a856e4f88ff8bb73bbb5e989825cf7" integrity sha512-/4YgnZS8y1UXXmC02xD5rRrBEu6T5ub+mQHLNRj0fzTRbgdBYhsNo2V5EqwgqrExjxsjtF/OpAKAMkKsxbD5XQ== -caseless@~0.12.0: +caseless@^0.12.0, caseless@~0.12.0: version "0.12.0" resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= @@ -16549,13 +16549,6 @@ jest@^26.0.1: import-local "^3.0.2" jest-cli "^26.6.3" -jose@^1.27.1: - version "1.27.1" - resolved "https://registry.npmjs.org/jose/-/jose-1.27.1.tgz#a1de2ecb5b3ae1ae28f0d9d0cc536349ada27ec8" - integrity sha512-VyHM6IJPw0TTGqHVNlPWg16/ASDPAmcChcLqSb3WNBvwWFoWPeFqlmAUCm8/oIG1GjZwAlUDuRKFfycowarcVA== - dependencies: - "@panva/asn1.js" "^1.0.0" - jose@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/jose/-/jose-2.0.2.tgz#fb22385b80c658cc7a0cae05b7086c04c6be49f4" @@ -16563,6 +16556,11 @@ jose@^2.0.2: dependencies: "@panva/asn1.js" "^1.0.0" +jose@^3.5.1: + version "3.5.1" + resolved "https://registry.npmjs.org/jose/-/jose-3.5.1.tgz#adc0d5000dbf64a7f4db171d449dbcf6b556b6f0" + integrity sha512-BQQJafCDvsmtc/LaK57cjfhU/1AKBhJSbJhKPq+uhrjMoV/sh3/dbk9cm08nAeSGS6j+sjhWoY+LZPUXiKzYzw== + joycon@^2.2.5: version "2.2.5" resolved "https://registry.npmjs.org/joycon/-/joycon-2.2.5.tgz#8d4cf4cbb2544d7b7583c216fcdfec19f6be1615" @@ -18824,7 +18822,7 @@ node-fetch-npm@^2.0.2: json-parse-better-errors "^1.0.0" safe-buffer "^5.1.1" -node-fetch@2.6.1, node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@^2.3.0, node-fetch@^2.5.0, node-fetch@^2.6.0, node-fetch@^2.6.1: +node-fetch@2.6.1, node-fetch@^2.0.0-alpha.8, node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@^2.3.0, node-fetch@^2.5.0, node-fetch@^2.6.0, node-fetch@^2.6.1: version "2.6.1" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== @@ -21206,6 +21204,15 @@ quick-lru@^5.1.1: resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== +r2@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/r2/-/r2-2.0.1.tgz#94cd802ecfce9a622549c8182032d8e4a2b2e612" + integrity sha512-EEmxoxYCe3LHzAUhRIRxdCKERpeRNmlLj6KLUSORqnK6dWl/K5ShmDGZqM2lRZQeqJgF+wyqk0s1M7SWUveNOQ== + dependencies: + caseless "^0.12.0" + node-fetch "^2.0.0-alpha.8" + typedarray-to-buffer "^3.1.2" + raf-schd@^4.0.2: version "4.0.2" resolved "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0" @@ -25002,7 +25009,7 @@ typed-rest-client@^1.8.0: tunnel "0.0.6" underscore "1.8.3" -typedarray-to-buffer@^3.1.5: +typedarray-to-buffer@^3.1.2, typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== From 6ef5428e73bd9b52818f2357422d433be697221f Mon Sep 17 00:00:00 2001 From: Jonah Back Date: Tue, 12 Jan 2021 14:53:15 -0800 Subject: [PATCH 077/209] fix: add new processor to index --- plugins/auth-backend/src/providers/aws-alb/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/auth-backend/src/providers/aws-alb/index.ts b/plugins/auth-backend/src/providers/aws-alb/index.ts index 863d6e76e1..f8b5c9e5d7 100644 --- a/plugins/auth-backend/src/providers/aws-alb/index.ts +++ b/plugins/auth-backend/src/providers/aws-alb/index.ts @@ -13,3 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +export { createAwsAlbProvider } from './provider'; From 0d6ad133c75d0274695f59fd696718dbaa72f3b9 Mon Sep 17 00:00:00 2001 From: Jonah Back Date: Tue, 12 Jan 2021 15:17:28 -0800 Subject: [PATCH 078/209] fix: simplify identity resolver --- .../src/providers/aws-alb/provider.test.ts | 115 ++++++------------ .../src/providers/aws-alb/provider.ts | 49 +++----- .../auth-backend/src/providers/factories.ts | 2 + plugins/auth-backend/src/providers/types.ts | 6 + 4 files changed, 68 insertions(+), 104 deletions(-) diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts index 55c27fd7ff..93649ce8d7 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts @@ -18,7 +18,9 @@ import express from 'express'; import * as jwtVerify from 'jose/jwt/verify'; import { AwsAlbAuthProvider } from './provider'; -import { UserEntityV1alpha1 } from '@backstage/catalog-model'; +import { AuthResponse } from '../types'; + +const mockedJwtVerify = jwtVerify as jest.Mocked; const mockKey = async () => { return `-----BEGIN PUBLIC KEY----- @@ -44,26 +46,21 @@ jest.mock('jose/jwt/verify', () => { }; }); -const identityResolutionCallbackMock = async (): Promise< - UserEntityV1alpha1 -> => { +const identityResolutionCallbackMock = async (): Promise> => { return { - apiVersion: 'backstage.io/v1alpha1', - kind: 'User', - metadata: { - name: 'foo', + backstageIdentity: { + id: 'foo', + idToken: '', }, - spec: { - memberOf: [], - profile: { - displayName: 'Foo Bar', - }, + profile: { + displayName: 'Foo Bar', }, + providerInfo: {}, }; }; const identityResolutionCallbackRejectedMock = async (): Promise< - UserEntityV1alpha1 + AuthResponse > => { throw new Error('failed'); }; @@ -83,13 +80,6 @@ describe('AwsALBAuthProvider', () => { getEntityByName: jest.fn(), }; - const tokenIssuer = { - issueToken: async () => { - return ''; - }, - listPublicKeys: jest.fn(), - }; - const mockResponseSend = jest.fn(); const mockRequest = ({ header: jest.fn(() => { @@ -108,18 +98,13 @@ describe('AwsALBAuthProvider', () => { describe('should transform to type OAuthResponse', () => { it('when JWT is valid and identity is resolved successfully', async () => { - const provider = new AwsAlbAuthProvider( - getVoidLogger(), - catalogApi, - tokenIssuer, - { - region: 'us-west-2', - identityResolutionCallback: identityResolutionCallbackMock, - issuer: 'foo', - }, - ); + const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, { + region: 'us-west-2', + identityResolutionCallback: identityResolutionCallbackMock, + issuer: 'foo', + }); - jwtVerify.default.mockImplementationOnce(async () => { + mockedJwtVerify.default.mockImplementationOnce(async () => { return { payload: { sub: 'foo', @@ -143,16 +128,11 @@ describe('AwsALBAuthProvider', () => { }); describe('should fail when', () => { it('JWT is missing', async () => { - const provider = new AwsAlbAuthProvider( - getVoidLogger(), - catalogApi, - tokenIssuer, - { - region: 'us-west-2', - identityResolutionCallback: identityResolutionCallbackMock, - issuer: 'foo', - }, - ); + const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, { + region: 'us-west-2', + identityResolutionCallback: identityResolutionCallbackMock, + issuer: 'foo', + }); await provider.refresh(mockRequestWithoutJwt, mockResponse); @@ -160,18 +140,13 @@ describe('AwsALBAuthProvider', () => { }); it('JWT is invalid', async () => { - const provider = new AwsAlbAuthProvider( - getVoidLogger(), - catalogApi, - tokenIssuer, - { - region: 'us-west-2', - identityResolutionCallback: identityResolutionCallbackMock, - issuer: 'foo', - }, - ); + const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, { + region: 'us-west-2', + identityResolutionCallback: identityResolutionCallbackMock, + issuer: 'foo', + }); - jwtVerify.default.mockImplementationOnce(async () => { + mockedJwtVerify.default.mockImplementationOnce(async () => { throw new Error('bad JWT'); }); @@ -181,18 +156,13 @@ describe('AwsALBAuthProvider', () => { }); it('issuer is invalid', async () => { - const provider = new AwsAlbAuthProvider( - getVoidLogger(), - catalogApi, - tokenIssuer, - { - region: 'us-west-2', - identityResolutionCallback: identityResolutionCallbackMock, - issuer: 'foobar', - }, - ); + const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, { + region: 'us-west-2', + identityResolutionCallback: identityResolutionCallbackMock, + issuer: 'foobar', + }); - jwtVerify.default.mockImplementationOnce(async () => { + mockedJwtVerify.default.mockImplementationOnce(async () => { return {}; }); @@ -202,18 +172,13 @@ describe('AwsALBAuthProvider', () => { }); it('identity resolution callback rejects', async () => { - const provider = new AwsAlbAuthProvider( - getVoidLogger(), - catalogApi, - tokenIssuer, - { - region: 'us-west-2', - identityResolutionCallback: identityResolutionCallbackRejectedMock, - issuer: 'foo', - }, - ); + const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, { + region: 'us-west-2', + identityResolutionCallback: identityResolutionCallbackRejectedMock, + issuer: 'foo', + }); - jwtVerify.default.mockImplementationOnce(async () => { + mockedJwtVerify.default.mockImplementationOnce(async () => { return {}; }); diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts index f00543302b..568fc4dd46 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts @@ -16,30 +16,26 @@ import { AuthProviderFactoryOptions, AuthProviderRouteHandlers, + IdentityResolver, } from '../types'; -import { TokenIssuer } from '../../identity'; import express from 'express'; +// @ts-ignore no types available for R2 import r2 from 'r2'; import * as crypto from 'crypto'; import { KeyObject } from 'crypto'; import { Logger } from 'winston'; import jwtVerify from 'jose/jwt/verify'; import { CatalogApi } from '@backstage/catalog-client'; -import { UserEntityV1alpha1 } from '@backstage/catalog-model'; const ALB_JWT_HEADER = 'x-amzn-oidc-data'; /** * A callback function that receives a verified JWT and returns a UserEntity * @param {payload} The verified JWT payload */ -type IdentityResolutionCallback = ( - payload: object, - catalogApi: CatalogApi, -) => Promise; type AwsAlbAuthProviderOptions = { region: string; issuer: string; - identityResolutionCallback: IdentityResolutionCallback; + identityResolutionCallback: IdentityResolver; }; export const getJWTHeaders = (input: string) => { const encoded = input.split('.')[0]; @@ -49,19 +45,16 @@ export const getJWTHeaders = (input: string) => { export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { private logger: Logger; private readonly catalogClient: CatalogApi; - private tokenIssuer: TokenIssuer; private options: AwsAlbAuthProviderOptions; private readonly keyCache: { [key: string]: KeyObject }; constructor( logger: Logger, catalogClient: CatalogApi, - tokenIssuer: TokenIssuer, options: AwsAlbAuthProviderOptions, ) { this.logger = logger; this.catalogClient = catalogClient; - this.tokenIssuer = tokenIssuer; this.options = options; this.keyCache = {}; } @@ -88,16 +81,7 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { verifiedToken.payload, this.catalogClient, ); - res.send({ - providerInfo: {}, - profile: resolvedEntity?.spec?.profile, - backstageIdentity: { - id: resolvedEntity?.metadata?.name, - idToken: await this.tokenIssuer.issueToken({ - claims: { sub: resolvedEntity?.metadata?.name }, - }), - }, - }); + res.send(resolvedEntity); } catch (e) { this.logger.error('exception occurred during JWT processing', e); res.send(401); @@ -124,15 +108,22 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { } } -export const createAwsAlbProvider = ( - { logger, catalogApi, tokenIssuer, config }: AuthProviderFactoryOptions, - identityResolver: IdentityResolutionCallback, -) => { +export const createAwsAlbProvider = ({ + logger, + catalogApi, + config, + identityResolver, +}: AuthProviderFactoryOptions) => { const region = config.getString('region'); const issuer = config.getString('iss'); - return new AwsAlbAuthProvider(logger, catalogApi, tokenIssuer, { - region, - issuer, - identityResolutionCallback: identityResolver, - }); + if (identityResolver !== undefined) { + return new AwsAlbAuthProvider(logger, catalogApi, { + region, + issuer, + identityResolutionCallback: identityResolver, + }); + } + throw new Error( + 'Identity resolver is required to use this authentication provider', + ); }; diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index 7bad4f4d81..619fb1c706 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -25,6 +25,7 @@ import { createAuth0Provider } from './auth0'; import { createMicrosoftProvider } from './microsoft'; import { createOneLoginProvider } from './onelogin'; import { AuthProviderFactory } from './types'; +import { createAwsAlbProvider } from './aws-alb'; export const factories: { [providerId: string]: AuthProviderFactory } = { google: createGoogleProvider, @@ -37,4 +38,5 @@ export const factories: { [providerId: string]: AuthProviderFactory } = { oauth2: createOAuth2Provider, oidc: createOidcProvider, onelogin: createOneLoginProvider, + awsalb: createAwsAlbProvider, }; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index a40f2a96ee..51c1ee336e 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -112,6 +112,11 @@ export interface AuthProviderRouteHandlers { logout?(req: express.Request, res: express.Response): Promise; } +export type IdentityResolver = ( + payload: object, + catalogApi: CatalogApi, +) => Promise>; + export type AuthProviderFactoryOptions = { providerId: string; globalConfig: AuthProviderConfig; @@ -120,6 +125,7 @@ export type AuthProviderFactoryOptions = { tokenIssuer: TokenIssuer; discovery: PluginEndpointDiscovery; catalogApi: CatalogApi; + identityResolver?: IdentityResolver; }; export type AuthProviderFactory = ( From 0643a3336ab141cd2a87ea31d8bde6f01c32e276 Mon Sep 17 00:00:00 2001 From: Jonah Back Date: Tue, 12 Jan 2021 15:19:02 -0800 Subject: [PATCH 079/209] chore: add changeset --- .changeset/twelve-ants-sort.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/twelve-ants-sort.md diff --git a/.changeset/twelve-ants-sort.md b/.changeset/twelve-ants-sort.md new file mode 100644 index 0000000000..978047ab8e --- /dev/null +++ b/.changeset/twelve-ants-sort.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Add AWS ALB OIDC reverse proxy authentication provider From 2335fa9d174995c800920b76cdfcb117ef308c56 Mon Sep 17 00:00:00 2001 From: Jonah Back Date: Tue, 12 Jan 2021 15:21:44 -0800 Subject: [PATCH 080/209] chore: add comment around payload passed in identity resolver --- plugins/auth-backend/src/providers/types.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 51c1ee336e..a31ef85a7d 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -113,6 +113,9 @@ export interface AuthProviderRouteHandlers { } export type IdentityResolver = ( + /** + * An object containing information specific to the auth provider. + */ payload: object, catalogApi: CatalogApi, ) => Promise>; From 1f275fe31a4585bf6beb72e5cd4d3a9ef346cbe3 Mon Sep 17 00:00:00 2001 From: Jonah Back Date: Wed, 13 Jan 2021 20:18:07 -0800 Subject: [PATCH 081/209] Upgrade TokenFactory to latest jose library --- plugins/auth-backend/package.json | 8 +++- .../src/identity/TokenFactory.test.ts | 41 +++++++++++++------ .../auth-backend/src/identity/TokenFactory.ts | 40 ++++++++++-------- yarn.lock | 6 +-- 4 files changed, 61 insertions(+), 34 deletions(-) diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index f8de2bc6f1..f189f8078f 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -18,7 +18,13 @@ }, "jest": { "moduleNameMapper": { - "jose/jwt/verify": "/../../../node_modules/jose/lib/jwt/verify" + "jose/jwt/sign": "/../node_modules/jose/dist/node/cjs/jwt/sign", + "jose/jwt/verify": "/../node_modules/jose/dist/node/cjs/jwt/verify", + "jose/jwk/from_key_like": "/../node_modules/jose/dist/node/cjs/jwk/from_key_like", + "jose/jwk/parse": "/../node_modules/jose/dist/node/cjs/jwk/parse", + "jose/util/base64url": "/../node_modules/jose/dist/node/cjs/util/base64url", + "jose/util/decode_protected_header": "/../node_modules/jose/dist/node/cjs/util/decode_protected_header", + "jose/util/generate_secret": "/../node_modules/jose/dist/node/cjs/util/generate_secret" } }, "keywords": [ diff --git a/plugins/auth-backend/src/identity/TokenFactory.test.ts b/plugins/auth-backend/src/identity/TokenFactory.test.ts index 8b719799b1..7058dbb0f5 100644 --- a/plugins/auth-backend/src/identity/TokenFactory.test.ts +++ b/plugins/auth-backend/src/identity/TokenFactory.test.ts @@ -13,12 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { TextDecoder, TextEncoder } from 'util'; + +// These two statements are structured like this because Jest doesn't include these in the default +// test environment, even though they exist in Node. +global.TextEncoder = TextEncoder; +// @ts-ignore +global.TextDecoder = TextDecoder; import { utc } from 'moment'; import { TokenFactory } from './TokenFactory'; import { getVoidLogger } from '@backstage/backend-common'; -import { KeyStore, AnyJWK, StoredKey } from './types'; -import { JWKS, JSONWebKey, JWT } from 'jose'; +import { AnyJWK, KeyStore, StoredKey } from './types'; +import jwtVerify from 'jose/jwt/verify'; +import parseJwk from 'jose/jwk/parse'; +import decodeProtectedHeader, { + ProtectedHeaderParameters, +} from 'jose/util/decode_protected_header'; const logger = getVoidLogger(); @@ -52,10 +63,8 @@ class MemoryKeyStore implements KeyStore { } function jwtKid(jwt: string): string { - const { header } = JWT.decode(jwt, { complete: true }) as { - header: { kid: string }; - }; - return header.kid; + const header = decodeProtectedHeader(jwt) as ProtectedHeaderParameters; + return header.kid as string; } describe('TokenFactory', () => { @@ -72,14 +81,20 @@ describe('TokenFactory', () => { const token = await factory.issueToken({ claims: { sub: 'foo' } }); const { keys } = await factory.listPublicKeys(); - const keyStore = JWKS.asKeyStore({ - keys: keys.map(key => key as JSONWebKey), + const keyMap: { + [key: string]: AnyJWK; + } = {}; + + keys.forEach(key => { + keyMap[key.kid] = key; }); - const payload = JWT.verify(token, keyStore) as object & { - iat: number; - exp: number; - }; + const payload = ( + await jwtVerify(token, async header => { + const kid = header.kid as string; + return await parseJwk(keyMap[kid]); + }) + ).payload; expect(payload).toEqual({ iss: 'my-issuer', aud: 'backstage', @@ -87,7 +102,7 @@ describe('TokenFactory', () => { iat: expect.any(Number), exp: expect.any(Number), }); - expect(payload.exp).toBe(payload.iat + keyDurationSeconds); + expect(payload.exp).toBe((payload.iat as number) + keyDurationSeconds); }); it('should generate new signing keys when the current one expires', async () => { diff --git a/plugins/auth-backend/src/identity/TokenFactory.ts b/plugins/auth-backend/src/identity/TokenFactory.ts index 36622332e0..8cf7c358d6 100644 --- a/plugins/auth-backend/src/identity/TokenFactory.ts +++ b/plugins/auth-backend/src/identity/TokenFactory.ts @@ -16,7 +16,10 @@ import moment from 'moment'; import { TokenIssuer, TokenParams, KeyStore, AnyJWK } from './types'; -import { JSONWebKey, JWK, JWS } from 'jose'; +import parseJwk, { JWK } from 'jose/jwk/parse'; +import SignJWT from 'jose/jwt/sign'; +import generateSecret from 'jose/util/generate_secret'; +import fromKeyLike from 'jose/jwk/from_key_like'; import { Logger } from 'winston'; import { v4 as uuid } from 'uuid'; @@ -53,7 +56,7 @@ export class TokenFactory implements TokenIssuer { private readonly keyDurationSeconds: number; private keyExpiry?: moment.Moment; - private privateKeyPromise?: Promise; + private privateKeyPromise?: Promise; constructor(options: Options) { this.issuer = options.issuer; @@ -64,7 +67,7 @@ export class TokenFactory implements TokenIssuer { async issueToken(params: TokenParams): Promise { const key = await this.getKey(); - + const keyLike = await parseJwk(key); const iss = this.issuer; const sub = params.claims.sub; const aud = 'backstage'; @@ -72,11 +75,9 @@ export class TokenFactory implements TokenIssuer { const exp = iat + this.keyDurationSeconds; this.logger.info(`Issuing token for ${sub}`); - - return JWS.sign({ iss, sub, aud, iat, exp }, key, { - alg: key.alg, - kid: key.kid, - }); + return new SignJWT({ iss, sub, aud, iat, exp }) + .setProtectedHeader({ alg: key.alg, typ: 'JWT', kid: key.kid }) + .sign(keyLike); } // This will be called by other services that want to verify ID tokens. @@ -114,7 +115,7 @@ export class TokenFactory implements TokenIssuer { return { keys: validKeys.map(({ key }) => key) }; } - private async getKey(): Promise { + private async getKey(): Promise { // Make sure that we only generate one key at a time if (this.privateKeyPromise) { if (this.keyExpiry?.isAfter()) { @@ -127,11 +128,12 @@ export class TokenFactory implements TokenIssuer { this.keyExpiry = moment().add(this.keyDurationSeconds, 'seconds'); const promise = (async () => { // This generates a new signing key to be used to sign tokens until the next key rotation - const key = await JWK.generate('EC', 'P-256', { - use: 'sig', - kid: uuid(), - alg: 'ES256', - }); + + const key = await generateSecret('HS256'); + const kid = uuid(); + const jwk = await fromKeyLike(key); + jwk.kid = kid; + jwk.alg = 'HS256'; // We're not allowed to use the key until it has been successfully stored // TODO: some token verification implementations aggressively cache the list of keys, and @@ -139,11 +141,15 @@ export class TokenFactory implements TokenIssuer { // may want to keep using the existing key for some period of time until we switch to // the new one. This also needs to be implemented cross-service though, meaning new services // that boot up need to be able to grab an existing key to use for signing. - this.logger.info(`Created new signing key ${key.kid}`); - await this.keyStore.addKey((key.toJWK(false) as unknown) as AnyJWK); + this.logger.info(`Created new signing key ${jwk.kid}`); + await this.keyStore.addKey({ + // @ts-ignore + use: 'sig', + ...jwk, + }); // At this point we are allowed to start using the new key - return key as JSONWebKey; + return jwk; })(); this.privateKeyPromise = promise; diff --git a/yarn.lock b/yarn.lock index 0098954754..8ce5543164 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16550,9 +16550,9 @@ jest@^26.0.1: jest-cli "^26.6.3" jose@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/jose/-/jose-2.0.2.tgz#fb22385b80c658cc7a0cae05b7086c04c6be49f4" - integrity sha512-yD93lsiMA1go/qxSY/vXWBodmIZJIxeB7QhFi8z1yQ3KUwKENqI9UA8VCHlQ5h3x1zWuWZjoY87ByQzkQbIrQg== + version "2.0.3" + resolved "https://registry.npmjs.org/jose/-/jose-2.0.3.tgz#9c931ab3e13e2d16a5b9e6183e60b2fc40a8e1b8" + integrity sha512-L+RlDgjO0Tk+Ki6/5IXCSEnmJCV8iMFZoBuEgu2vPQJJ4zfG/k3CAqZUMKDYNRHIDyy0QidJpOvX0NgpsAqFlw== dependencies: "@panva/asn1.js" "^1.0.0" From dfce32b3567e02d93241fdca623e0ec8193db3fb Mon Sep 17 00:00:00 2001 From: Jonah Back Date: Mon, 18 Jan 2021 14:09:23 -0800 Subject: [PATCH 082/209] Address review comments around algorithm being used, typescript ignore --- plugins/auth-backend/package.json | 8 +----- .../auth-backend/src/identity/TokenFactory.ts | 27 ++++++++++--------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index f189f8078f..380b7da6b0 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -18,13 +18,7 @@ }, "jest": { "moduleNameMapper": { - "jose/jwt/sign": "/../node_modules/jose/dist/node/cjs/jwt/sign", - "jose/jwt/verify": "/../node_modules/jose/dist/node/cjs/jwt/verify", - "jose/jwk/from_key_like": "/../node_modules/jose/dist/node/cjs/jwk/from_key_like", - "jose/jwk/parse": "/../node_modules/jose/dist/node/cjs/jwk/parse", - "jose/util/base64url": "/../node_modules/jose/dist/node/cjs/util/base64url", - "jose/util/decode_protected_header": "/../node_modules/jose/dist/node/cjs/util/decode_protected_header", - "jose/util/generate_secret": "/../node_modules/jose/dist/node/cjs/util/generate_secret" + "^jose/(.*)$": "/../node_modules/jose/dist/node/cjs/$1" } }, "keywords": [ diff --git a/plugins/auth-backend/src/identity/TokenFactory.ts b/plugins/auth-backend/src/identity/TokenFactory.ts index 8cf7c358d6..65f9a07eb1 100644 --- a/plugins/auth-backend/src/identity/TokenFactory.ts +++ b/plugins/auth-backend/src/identity/TokenFactory.ts @@ -18,7 +18,7 @@ import moment from 'moment'; import { TokenIssuer, TokenParams, KeyStore, AnyJWK } from './types'; import parseJwk, { JWK } from 'jose/jwk/parse'; import SignJWT from 'jose/jwt/sign'; -import generateSecret from 'jose/util/generate_secret'; +import generateKeyPair from 'jose/util/generate_key_pair'; import fromKeyLike from 'jose/jwk/from_key_like'; import { Logger } from 'winston'; import { v4 as uuid } from 'uuid'; @@ -129,12 +129,20 @@ export class TokenFactory implements TokenIssuer { const promise = (async () => { // This generates a new signing key to be used to sign tokens until the next key rotation - const key = await generateSecret('HS256'); + const key = await generateKeyPair('ES256'); const kid = uuid(); - const jwk = await fromKeyLike(key); - jwk.kid = kid; - jwk.alg = 'HS256'; + const jwk = await fromKeyLike(key.privateKey); + // @ts-ignore https://github.com/microsoft/TypeScript/issues/13195 - + // JOSE Library provides optional for most fields - and TS does not distinguish between missing/undefined. + // Because AnyJWK requires keys to have type "string", this throws a TypeError - though in practice, if the field + // is undefined, JOSE will not send it back as key. + const storedJwk: AnyJWK = { + ...jwk, + alg: 'ES256', + kid: kid, + use: 'sig', + }; // We're not allowed to use the key until it has been successfully stored // TODO: some token verification implementations aggressively cache the list of keys, and // don't attempt to fetch new ones even if they encounter an unknown kid. Therefore we @@ -142,14 +150,9 @@ export class TokenFactory implements TokenIssuer { // the new one. This also needs to be implemented cross-service though, meaning new services // that boot up need to be able to grab an existing key to use for signing. this.logger.info(`Created new signing key ${jwk.kid}`); - await this.keyStore.addKey({ - // @ts-ignore - use: 'sig', - ...jwk, - }); - + await this.keyStore.addKey(storedJwk); // At this point we are allowed to start using the new key - return jwk; + return storedJwk; })(); this.privateKeyPromise = promise; From 147648a2d105a4d242861dcbe7129ab6ada3878b Mon Sep 17 00:00:00 2001 From: Jonah Back Date: Mon, 18 Jan 2021 14:12:12 -0800 Subject: [PATCH 083/209] Update type definition and docs to be clear about experimental status --- plugins/auth-backend/src/providers/types.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index a31ef85a7d..1a4e7b118a 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -112,7 +112,12 @@ export interface AuthProviderRouteHandlers { logout?(req: express.Request, res: express.Response): Promise; } -export type IdentityResolver = ( +/** + * EXPERIMENTAL - this will almost certainly break in a future release. + * + * Used to resolve an identity from auth information in some auth providers. + */ +export type ExperimentalIdentityResolver = ( /** * An object containing information specific to the auth provider. */ @@ -128,7 +133,7 @@ export type AuthProviderFactoryOptions = { tokenIssuer: TokenIssuer; discovery: PluginEndpointDiscovery; catalogApi: CatalogApi; - identityResolver?: IdentityResolver; + identityResolver?: ExperimentalIdentityResolver; }; export type AuthProviderFactory = ( From 02140eb89ad056c52876352319991b5b95da738d Mon Sep 17 00:00:00 2001 From: Jonah Back Date: Mon, 18 Jan 2021 14:22:38 -0800 Subject: [PATCH 084/209] Add basic node-cache with 1 hour TTL for caching keys retrieved from AWS public key endpoint --- plugins/auth-backend/package.json | 1 + .../src/identity/TokenFactory.test.ts | 2 +- .../src/providers/aws-alb/provider.ts | 16 +++++++++------- yarn.lock | 12 ++++++++++++ 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 380b7da6b0..ad54e35b28 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -54,6 +54,7 @@ "knex": "^0.21.6", "moment": "^2.26.0", "morgan": "^1.10.0", + "node-cache": "^5.1.2", "openid-client": "^4.2.1", "passport": "^0.4.1", "passport-github2": "^0.1.12", diff --git a/plugins/auth-backend/src/identity/TokenFactory.test.ts b/plugins/auth-backend/src/identity/TokenFactory.test.ts index 7058dbb0f5..abbeecb40c 100644 --- a/plugins/auth-backend/src/identity/TokenFactory.test.ts +++ b/plugins/auth-backend/src/identity/TokenFactory.test.ts @@ -102,7 +102,7 @@ describe('TokenFactory', () => { iat: expect.any(Number), exp: expect.any(Number), }); - expect(payload.exp).toBe((payload.iat as number) + keyDurationSeconds); + expect(payload.exp).toBe(Number(payload.iat) + keyDurationSeconds); }); it('should generate new signing keys when the current one expires', async () => { diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts index 568fc4dd46..6bb2e9fe7b 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts @@ -16,7 +16,7 @@ import { AuthProviderFactoryOptions, AuthProviderRouteHandlers, - IdentityResolver, + ExperimentalIdentityResolver, } from '../types'; import express from 'express'; // @ts-ignore no types available for R2 @@ -24,6 +24,7 @@ import r2 from 'r2'; import * as crypto from 'crypto'; import { KeyObject } from 'crypto'; import { Logger } from 'winston'; +import NodeCache from 'node-cache'; import jwtVerify from 'jose/jwt/verify'; import { CatalogApi } from '@backstage/catalog-client'; @@ -35,7 +36,7 @@ const ALB_JWT_HEADER = 'x-amzn-oidc-data'; type AwsAlbAuthProviderOptions = { region: string; issuer: string; - identityResolutionCallback: IdentityResolver; + identityResolutionCallback: ExperimentalIdentityResolver; }; export const getJWTHeaders = (input: string) => { const encoded = input.split('.')[0]; @@ -46,7 +47,7 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { private logger: Logger; private readonly catalogClient: CatalogApi; private options: AwsAlbAuthProviderOptions; - private readonly keyCache: { [key: string]: KeyObject }; + private readonly keyCache: NodeCache; constructor( logger: Logger, @@ -56,7 +57,7 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { this.logger = logger; this.catalogClient = catalogClient; this.options = options; - this.keyCache = {}; + this.keyCache = new NodeCache({ stdTTL: 3600 }); } frameHandler(): Promise { return Promise.resolve(undefined); @@ -96,14 +97,15 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { } async getKey(keyId: string): Promise { - if (this.keyCache[keyId]) { - return this.keyCache[keyId]; + const optionalCacheKey = this.keyCache.get(keyId); + if (optionalCacheKey) { + return optionalCacheKey; } const keyText: string = await r2( `https://public-keys.auth.elb.${this.options.region}.amazonaws.com/${keyId}`, ).text; const keyValue = crypto.createPublicKey(keyText); - this.keyCache[keyId] = keyValue; + this.keyCache.set(keyId, keyValue); return keyValue; } } diff --git a/yarn.lock b/yarn.lock index 8ce5543164..b8531ef248 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10126,6 +10126,11 @@ clone-response@^1.0.2: dependencies: mimic-response "^1.0.0" +clone@2.x: + version "2.1.2" + resolved "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= + clone@^1.0.2: version "1.0.4" resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" @@ -18806,6 +18811,13 @@ node-addon-api@2.0.0: resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.0.tgz#f9afb8d777a91525244b01775ea0ddbe1125483b" integrity sha512-ASCL5U13as7HhOExbT6OlWJJUV/lLzL2voOSP1UVehpRD8FbSrSDjfScK/KwAvVTI5AS6r4VwbOMlIqtvRidnA== +node-cache@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d" + integrity sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg== + dependencies: + clone "2.x" + node-dir@^0.1.10: version "0.1.17" resolved "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz#5f5665d93351335caabef8f1c554516cf5f1e4e5" From c43d1485560158fc54eb40acc42c8791531be27b Mon Sep 17 00:00:00 2001 From: Jonah Back Date: Mon, 18 Jan 2021 14:39:53 -0800 Subject: [PATCH 085/209] Fix usage of JOSE library in OIDC test as part of JOSE upgrade --- plugins/auth-backend/package.json | 2 +- plugins/auth-backend/src/providers/oidc/provider.test.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index ad54e35b28..a0c7cf1d84 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -18,7 +18,7 @@ }, "jest": { "moduleNameMapper": { - "^jose/(.*)$": "/../node_modules/jose/dist/node/cjs/$1" + "^jose/(.*)$": "/../../../node_modules/jose/dist/node/cjs/$1" } }, "keywords": [ diff --git a/plugins/auth-backend/src/providers/oidc/provider.test.ts b/plugins/auth-backend/src/providers/oidc/provider.test.ts index f2bd8dcb90..ae0ca2da2b 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.test.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.test.ts @@ -13,13 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { TextDecoder, TextEncoder } from 'util'; +// @ts-ignore +global.TextDecoder = TextDecoder; +global.TextEncoder = TextEncoder; import express from 'express'; import { Session } from 'express-session'; import nock from 'nock'; import { ClientMetadata, IssuerMetadata } from 'openid-client'; import { createOidcProvider, OidcAuthProvider } from './provider'; -import { JWT, JWK } from 'jose'; +import UnsecuredJWT from 'jose/jwt/unsecured'; import { AuthProviderFactoryOptions } from '../types'; import { Config } from '@backstage/config'; import { OAuthAdapter } from '../../lib/oauth'; @@ -71,6 +75,7 @@ describe('OidcAuthProvider', () => { const jwt = { sub: 'alice', iss: 'https://oidc.test', + iat: Date.now(), aud: clientMetadata.clientId, exp: Date.now() + 10000, }; @@ -79,7 +84,7 @@ describe('OidcAuthProvider', () => { .reply(200, issuerMetadata) .post('/as/token.oauth2') .reply(200, { - id_token: JWT.sign(jwt, JWK.None), + id_token: new UnsecuredJWT(jwt).encode(), access_token: 'test', authorization_signed_response_alg: 'HS256', }) From d7726cdfebbada542ea5f8ac060e977a8595bed3 Mon Sep 17 00:00:00 2001 From: Jonah Back Date: Mon, 18 Jan 2021 17:37:00 -0800 Subject: [PATCH 086/209] Make keyMap generation more concise Co-authored-by: Patrik Oldsberg --- plugins/auth-backend/src/identity/TokenFactory.test.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/plugins/auth-backend/src/identity/TokenFactory.test.ts b/plugins/auth-backend/src/identity/TokenFactory.test.ts index abbeecb40c..e28ded2bf2 100644 --- a/plugins/auth-backend/src/identity/TokenFactory.test.ts +++ b/plugins/auth-backend/src/identity/TokenFactory.test.ts @@ -81,13 +81,7 @@ describe('TokenFactory', () => { const token = await factory.issueToken({ claims: { sub: 'foo' } }); const { keys } = await factory.listPublicKeys(); - const keyMap: { - [key: string]: AnyJWK; - } = {}; - - keys.forEach(key => { - keyMap[key.kid] = key; - }); + const keyMap = Object.fromEntries(keys.map(key => [key.kid, key])); const payload = ( await jwtVerify(token, async header => { From d1fa548bd1a20f1c5d61476503d7b3b85936b1b5 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 19 Jan 2021 08:56:13 +0100 Subject: [PATCH 087/209] Rename azure/api to azure --- plugins/scaffolder-backend/src/service/router.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 4d610299eb..bbc054e519 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -47,7 +47,7 @@ describe('createRouter - working directory', () => { const mockPreparer = { prepare: mockPrepare, }; - mockPreparers.register('azure/api', mockPreparer); + mockPreparers.register('azure', mockPreparer); }); beforeEach(() => { @@ -65,7 +65,7 @@ describe('createRouter - working directory', () => { kind: 'Template', metadata: { annotations: { - 'backstage.io/managed-by-location': 'azure/api:dev.azure.com', + 'backstage.io/managed-by-location': 'azure:dev.azure.com', }, }, spec: { From d014185dbdc7d92d8ad3fa3932140350bfab423a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Wed, 13 Jan 2021 23:23:00 +0100 Subject: [PATCH 088/209] Show kubernetes services --- .changeset/olive-zebras-itch.md | 5 + .../KubernetesContent/KubernetesContent.tsx | 17 ++- .../ServicesAccordions/ServiceDrawer.test.tsx | 39 ++++++ .../ServicesAccordions/ServiceDrawer.tsx | 60 +++++++++ .../ServicesAccordions.test.tsx | 37 ++++++ .../ServicesAccordions/ServicesAccordions.tsx | 123 ++++++++++++++++++ .../__fixtures__/2-services.json | 83 ++++++++++++ .../components/ServicesAccordions/index.ts | 16 +++ 8 files changed, 376 insertions(+), 4 deletions(-) create mode 100644 .changeset/olive-zebras-itch.md create mode 100644 plugins/kubernetes/src/components/ServicesAccordions/ServiceDrawer.test.tsx create mode 100644 plugins/kubernetes/src/components/ServicesAccordions/ServiceDrawer.tsx create mode 100644 plugins/kubernetes/src/components/ServicesAccordions/ServicesAccordions.test.tsx create mode 100644 plugins/kubernetes/src/components/ServicesAccordions/ServicesAccordions.tsx create mode 100644 plugins/kubernetes/src/components/ServicesAccordions/__fixtures__/2-services.json create mode 100644 plugins/kubernetes/src/components/ServicesAccordions/index.ts diff --git a/.changeset/olive-zebras-itch.md b/.changeset/olive-zebras-itch.md new file mode 100644 index 0000000000..5e67673e8c --- /dev/null +++ b/.changeset/olive-zebras-itch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes': patch +--- + +Show Kubernetes Service manifests diff --git a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx index 39e73782f1..9716616009 100644 --- a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx +++ b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx @@ -47,6 +47,7 @@ import { DeploymentsAccordions } from '../DeploymentsAccordions'; import { ErrorReporting } from '../ErrorReporting'; import { groupResponses } from '../../utils/response'; import { DetectedError, detectErrors } from '../../error-detection'; +import { ServicesAccordions } from '../ServicesAccordions'; type KubernetesContentProps = { entity: Entity; children?: React.ReactNode }; @@ -187,10 +188,18 @@ const Cluster = ({ clusterObjects, detectedErrors }: ClusterProps) => { /> - + + + + + + + + + diff --git a/plugins/kubernetes/src/components/ServicesAccordions/ServiceDrawer.test.tsx b/plugins/kubernetes/src/components/ServicesAccordions/ServiceDrawer.test.tsx new file mode 100644 index 0000000000..d9082ff632 --- /dev/null +++ b/plugins/kubernetes/src/components/ServicesAccordions/ServiceDrawer.test.tsx @@ -0,0 +1,39 @@ +/* + * 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 React from 'react'; +import { render } from '@testing-library/react'; +import * as services from './__fixtures__/2-services.json'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { ServiceDrawer } from './ServiceDrawer'; + +describe('ServiceDrawer', () => { + it('should render deployment drawer', async () => { + const { getByText, getAllByText } = render( + wrapInTestApp( + , + ), + ); + + expect(getAllByText('awesome-service-grpc')).toHaveLength(2); + expect(getAllByText('Service')).toHaveLength(2); + expect(getByText('YAML')).toBeInTheDocument(); + expect(getByText('Cluster IP')).toBeInTheDocument(); + expect(getByText('Ports')).toBeInTheDocument(); + expect(getByText('Target Port: 1997')).toBeInTheDocument(); + expect(getByText('App: awesome-service')).toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes/src/components/ServicesAccordions/ServiceDrawer.tsx b/plugins/kubernetes/src/components/ServicesAccordions/ServiceDrawer.tsx new file mode 100644 index 0000000000..a93a9153d9 --- /dev/null +++ b/plugins/kubernetes/src/components/ServicesAccordions/ServiceDrawer.tsx @@ -0,0 +1,60 @@ +/* + * 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 React from 'react'; +import { V1Service } from '@kubernetes/client-node'; +import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer'; +import { Typography, Grid } from '@material-ui/core'; + +export const ServiceDrawer = ({ + service, + expanded, +}: { + service: V1Service; + expanded?: boolean; +}) => { + return ( + { + return { + ...service.spec, + }; + }} + > + + + + {service.metadata?.name ?? 'unknown object'} + + + + + Service + + + + + ); +}; diff --git a/plugins/kubernetes/src/components/ServicesAccordions/ServicesAccordions.test.tsx b/plugins/kubernetes/src/components/ServicesAccordions/ServicesAccordions.test.tsx new file mode 100644 index 0000000000..46172b4150 --- /dev/null +++ b/plugins/kubernetes/src/components/ServicesAccordions/ServicesAccordions.test.tsx @@ -0,0 +1,37 @@ +/* + * 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 React from 'react'; +import { render } from '@testing-library/react'; +import * as twoDeployFixture from './__fixtures__/2-services.json'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { ServicesAccordions } from './ServicesAccordions'; + +describe('ServicesAccordions', () => { + it('should render 2 services', async () => { + const { getByText } = render( + wrapInTestApp( + , + ), + ); + + expect(getByText('awesome-service-grpc')).toBeInTheDocument(); + expect(getByText('Type: ClusterIP')).toBeInTheDocument(); + + expect(getByText('awesome-service-pg')).toBeInTheDocument(); + expect(getByText('Type: ExternalName')).toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes/src/components/ServicesAccordions/ServicesAccordions.tsx b/plugins/kubernetes/src/components/ServicesAccordions/ServicesAccordions.tsx new file mode 100644 index 0000000000..6484f2211d --- /dev/null +++ b/plugins/kubernetes/src/components/ServicesAccordions/ServicesAccordions.tsx @@ -0,0 +1,123 @@ +/* + * 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 { GroupedResponses } from '../../types/types'; +import React from 'react'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + Divider, + Grid, + Typography, +} from '@material-ui/core'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { V1Service } from '@kubernetes/client-node'; +import { StructuredMetadataTable } from '@backstage/core'; +import { ServiceDrawer } from './ServiceDrawer'; + +type ServicesAccordionsProps = { + deploymentResources: GroupedResponses; +}; + +export const ServicesAccordions = ({ + deploymentResources, +}: ServicesAccordionsProps) => { + return ( + + {deploymentResources.services.map((service, i) => ( + + + + ))} + + ); +}; + +type ServiceAccordionProps = { + service: V1Service; +}; + +const ServiceAccordion = ({ service }: ServiceAccordionProps) => { + return ( + + }> + + + + + + + ); +}; + +type ServiceSummaryProps = { + service: V1Service; +}; + +const ServiceSummary = ({ service }: ServiceSummaryProps) => { + return ( + + + + + + + + + + + + Type: {service.spec?.type ?? '?'} + + + + ); +}; + +type ServiceCardProps = { + service: V1Service; +}; + +const ServiceCard = ({ service }: ServiceCardProps) => { + const metadata: any = {}; + + if (service.status?.loadBalancer?.ingress?.length ?? -1 > 0) { + metadata.loadbalancer = service.status?.loadBalancer; + } + + if (service.spec?.type === 'ClusterIP') { + metadata.clusterIP = service.spec.clusterIP; + } + if (service.spec?.type === 'ExternalName') { + metadata.externalName = service.spec.externalName; + } + + return ( + + ); +}; diff --git a/plugins/kubernetes/src/components/ServicesAccordions/__fixtures__/2-services.json b/plugins/kubernetes/src/components/ServicesAccordions/__fixtures__/2-services.json new file mode 100644 index 0000000000..f982ebf164 --- /dev/null +++ b/plugins/kubernetes/src/components/ServicesAccordions/__fixtures__/2-services.json @@ -0,0 +1,83 @@ +{ + "services": [ + { + "metadata": { + "annotations": { + "artifact.spinnaker.io/location": "default", + "artifact.spinnaker.io/name": "awesome-service-grpc", + "artifact.spinnaker.io/type": "kubernetes/service", + "moniker.spinnaker.io/application": "awesome-service", + "moniker.spinnaker.io/cluster": "service awesome-service-grpc" + }, + "creationTimestamp": "2021-01-04T16:35:04.000Z", + "labels": { + "app": "awesome-service", + "app.kubernetes.io/managed-by": "spinnaker", + "app.kubernetes.io/name": "awesome-service" + }, + "name": "awesome-service-grpc", + "namespace": "default", + "resourceVersion": "548901649", + "selfLink": "/api/v1/namespaces/default/services/awesome-service-grpc", + "uid": "461cdcd7-8c61-4125-91f9-e03d745f2f2c" + }, + "spec": { + "clusterIP": "None", + "ports": [ + { + "name": "grpc", + "port": 1997, + "protocol": "TCP", + "targetPort": 1997 + } + ], + "selector": { + "app": "awesome-service" + }, + "sessionAffinity": "None", + "type": "ClusterIP" + }, + "status": { + "loadBalancer": {} + } + }, + { + "metadata": { + "annotations": { + "artifact.spinnaker.io/location": "default", + "artifact.spinnaker.io/name": "awesome-service-pg", + "artifact.spinnaker.io/type": "kubernetes/service", + "moniker.spinnaker.io/application": "awesome-service", + "moniker.spinnaker.io/cluster": "service awesome-service-pg" + }, + "creationTimestamp": "2021-01-04T16:35:02.000Z", + "labels": { + "app": "awesome-service", + "app.kubernetes.io/managed-by": "spinnaker", + "app.kubernetes.io/name": "awesome-service" + }, + "name": "awesome-service-pg", + "namespace": "default", + "resourceVersion": "548901625", + "selfLink": "/api/v1/namespaces/default/services/awesome-service-pg", + "uid": "7d7ff8f2-6caa-4888-ae55-b6d41833ab92" + }, + "spec": { + "externalName": "10.244.0.5", + "ports": [ + { + "name": "pg", + "port": 5432, + "protocol": "TCP", + "targetPort": 5432 + } + ], + "sessionAffinity": "None", + "type": "ExternalName" + }, + "status": { + "loadBalancer": {} + } + } + ] +} diff --git a/plugins/kubernetes/src/components/ServicesAccordions/index.ts b/plugins/kubernetes/src/components/ServicesAccordions/index.ts new file mode 100644 index 0000000000..a3c392c743 --- /dev/null +++ b/plugins/kubernetes/src/components/ServicesAccordions/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { ServicesAccordions } from './ServicesAccordions'; From da9e5eb4b9b33a42c50596992d35b2a185af5554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Wed, 13 Jan 2021 23:50:09 +0100 Subject: [PATCH 089/209] Show kubernetes ingresses --- .changeset/olive-zebras-itch.md | 3 +- .../IngressDrawer.test.tsx | 38 +++++++ .../IngressesAccordions/IngressDrawer.tsx | 60 +++++++++++ .../IngressesAccordions.test.tsx | 34 ++++++ .../IngressesAccordions.tsx | 101 ++++++++++++++++++ .../__fixtures__/2-ingresses.json | 57 ++++++++++ .../components/IngressesAccordions/index.ts | 16 +++ .../KubernetesContent/KubernetesContent.tsx | 5 + 8 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.test.tsx create mode 100644 plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.tsx create mode 100644 plugins/kubernetes/src/components/IngressesAccordions/IngressesAccordions.test.tsx create mode 100644 plugins/kubernetes/src/components/IngressesAccordions/IngressesAccordions.tsx create mode 100644 plugins/kubernetes/src/components/IngressesAccordions/__fixtures__/2-ingresses.json create mode 100644 plugins/kubernetes/src/components/IngressesAccordions/index.ts diff --git a/.changeset/olive-zebras-itch.md b/.changeset/olive-zebras-itch.md index 5e67673e8c..6a12e35c87 100644 --- a/.changeset/olive-zebras-itch.md +++ b/.changeset/olive-zebras-itch.md @@ -2,4 +2,5 @@ '@backstage/plugin-kubernetes': patch --- -Show Kubernetes Service manifests +Show Kubernetes Service manifests. +Show Kubernetes Ingress manifests. diff --git a/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.test.tsx b/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.test.tsx new file mode 100644 index 0000000000..cf37eb3e79 --- /dev/null +++ b/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.test.tsx @@ -0,0 +1,38 @@ +/* + * 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 React from 'react'; +import { render } from '@testing-library/react'; +import * as ingresses from './__fixtures__/2-ingresses.json'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { IngressDrawer } from './IngressDrawer'; + +describe('IngressDrawer', () => { + it('should render ingress drawer', async () => { + const { getByText, getAllByText } = render( + wrapInTestApp( + , + ), + ); + + expect(getAllByText('awesome-service')).toHaveLength(2); + expect(getByText('YAML')).toBeInTheDocument(); + expect(getByText('Rules')).toBeInTheDocument(); + expect(getByText('Host: api.awesome-host.io')).toBeInTheDocument(); + expect(getAllByText('Service Port: 80')).toHaveLength(2); + expect(getAllByText('Service Name: awesome-service')).toHaveLength(2); + }); +}); diff --git a/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.tsx b/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.tsx new file mode 100644 index 0000000000..2fbccf7968 --- /dev/null +++ b/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.tsx @@ -0,0 +1,60 @@ +/* + * 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 React from 'react'; +import { ExtensionsV1beta1Ingress } from '@kubernetes/client-node'; +import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer'; +import { Typography, Grid } from '@material-ui/core'; + +export const IngressDrawer = ({ + ingress, + expanded, +}: { + ingress: ExtensionsV1beta1Ingress; + expanded?: boolean; +}) => { + return ( + { + return { + ...ingress.spec, + }; + }} + > + + + + {ingress.metadata?.name ?? 'unknown object'} + + + + + Ingress + + + + + ); +}; diff --git a/plugins/kubernetes/src/components/IngressesAccordions/IngressesAccordions.test.tsx b/plugins/kubernetes/src/components/IngressesAccordions/IngressesAccordions.test.tsx new file mode 100644 index 0000000000..580561095e --- /dev/null +++ b/plugins/kubernetes/src/components/IngressesAccordions/IngressesAccordions.test.tsx @@ -0,0 +1,34 @@ +/* + * 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 React from 'react'; +import { render } from '@testing-library/react'; +import * as oneIngressFixture from './__fixtures__/2-ingresses.json'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { IngressesAccordions } from './IngressesAccordions'; + +describe('IngressesAccordions', () => { + it('should render 1 ingress', async () => { + const { getByText } = render( + wrapInTestApp( + , + ), + ); + + expect(getByText('awesome-service')).toBeInTheDocument(); + expect(getByText('Ingress')).toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes/src/components/IngressesAccordions/IngressesAccordions.tsx b/plugins/kubernetes/src/components/IngressesAccordions/IngressesAccordions.tsx new file mode 100644 index 0000000000..a49f966ffd --- /dev/null +++ b/plugins/kubernetes/src/components/IngressesAccordions/IngressesAccordions.tsx @@ -0,0 +1,101 @@ +/* + * 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 { GroupedResponses } from '../../types/types'; +import React from 'react'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + Divider, + Grid, +} from '@material-ui/core'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { ExtensionsV1beta1Ingress } from '@kubernetes/client-node'; +import { StructuredMetadataTable } from '@backstage/core'; +import { IngressDrawer } from './IngressDrawer'; + +type IngressesAccordionsProps = { + deploymentResources: GroupedResponses; +}; + +export const IngressesAccordions = ({ + deploymentResources, +}: IngressesAccordionsProps) => { + return ( + + {deploymentResources.ingresses.map((ingress, i) => ( + + + + ))} + + ); +}; + +type IngressAccordionProps = { + ingress: ExtensionsV1beta1Ingress; +}; + +const IngressAccordion = ({ ingress }: IngressAccordionProps) => { + return ( + + }> + + + + + + + ); +}; + +type IngressSummaryProps = { + ingress: ExtensionsV1beta1Ingress; +}; + +const IngressSummary = ({ ingress }: IngressSummaryProps) => { + return ( + + + + + + + + + + ); +}; + +type IngressCardProps = { + ingress: ExtensionsV1beta1Ingress; +}; + +const IngressCard = ({ ingress }: IngressCardProps) => { + return ( + + ); +}; diff --git a/plugins/kubernetes/src/components/IngressesAccordions/__fixtures__/2-ingresses.json b/plugins/kubernetes/src/components/IngressesAccordions/__fixtures__/2-ingresses.json new file mode 100644 index 0000000000..136c70697d --- /dev/null +++ b/plugins/kubernetes/src/components/IngressesAccordions/__fixtures__/2-ingresses.json @@ -0,0 +1,57 @@ +{ + "ingresses": [ + { + "metadata": { + "annotations": { + "artifact.spinnaker.io/location": "default", + "artifact.spinnaker.io/name": "awesome-service", + "artifact.spinnaker.io/type": "kubernetes/ingress", + "kubernetes.io/ingress.class": "traefik", + "kubernetes.io/ingress.global-static-ip-name": "traefik-tcp-lb", + "moniker.spinnaker.io/application": "awesome-service", + "moniker.spinnaker.io/cluster": "ingress awesome-service" + }, + "creationTimestamp": "2018-11-16T14:00:13.000Z", + "generation": 11, + "labels": { + "app": "awesome-service", + "app.kubernetes.io/managed-by": "spinnaker", + "app.kubernetes.io/name": "awesome-service" + }, + "name": "awesome-service", + "namespace": "default", + "resourceVersion": "564824116", + "selfLink": "/apis/networking.k8s.io/v1beta1/namespaces/default/ingresses/awesome-service", + "uid": "f072e0b4-e9a7-11e8-af65-42010a9c0022" + }, + "spec": { + "rules": [ + { + "host": "api.awesome-host.io", + "http": { + "paths": [ + { + "backend": { + "serviceName": "awesome-service", + "servicePort": 80 + }, + "path": "/v1/awesome-service" + }, + { + "backend": { + "serviceName": "awesome-service", + "servicePort": 80 + }, + "path": "/v1/awesome-services" + } + ] + } + } + ] + }, + "status": { + "loadBalancer": {} + } + } + ] +} diff --git a/plugins/kubernetes/src/components/IngressesAccordions/index.ts b/plugins/kubernetes/src/components/IngressesAccordions/index.ts new file mode 100644 index 0000000000..a71f53fac7 --- /dev/null +++ b/plugins/kubernetes/src/components/IngressesAccordions/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { IngressesAccordions } from './IngressesAccordions'; diff --git a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx index 9716616009..092ada0724 100644 --- a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx +++ b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx @@ -47,6 +47,7 @@ import { DeploymentsAccordions } from '../DeploymentsAccordions'; import { ErrorReporting } from '../ErrorReporting'; import { groupResponses } from '../../utils/response'; import { DetectedError, detectErrors } from '../../error-detection'; +import { IngressesAccordions } from '../IngressesAccordions'; import { ServicesAccordions } from '../ServicesAccordions'; type KubernetesContentProps = { entity: Entity; children?: React.ReactNode }; @@ -196,6 +197,10 @@ const Cluster = ({ clusterObjects, detectedErrors }: ClusterProps) => { /> + + + + From 5880c280d6d9fde798e5495c909150d7646bfaa2 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 19 Jan 2021 10:19:53 +0100 Subject: [PATCH 090/209] Use correct versions --- plugins/scaffolder-backend/package.json | 4 ++-- yarn.lock | 9 --------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index ffce3590d0..134f63f96a 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -32,10 +32,10 @@ "@backstage/backend-common": "^0.4.3", "@backstage/catalog-model": "^0.6.1", "@backstage/config": "^0.1.2", - "@backstage/integration": "^0.1.5", + "@backstage/integration": "^0.2.0", "@gitbeaker/core": "^28.0.2", "@gitbeaker/node": "^28.0.2", - "@octokit/rest": "^18.0.0", + "@octokit/rest": "^18.0.12", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", "azure-devops-node-api": "^10.1.1", diff --git a/yarn.lock b/yarn.lock index 9172d827f0..fa2f5aca5e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2494,15 +2494,6 @@ remark-gfm "^1.0.0" zen-observable "^0.8.15" -"@backstage/integration@^0.1.5": - version "0.1.5" - resolved "https://registry.npmjs.org/@backstage/integration/-/integration-0.1.5.tgz#281042b4c49939eea7d3dacb77345bb8a0d7ce01" - integrity sha512-ts+47TInn86SNUlEOSsC8t6iwJU4A9ACutLNpnlnqW1hVuAxEy9c+H1pp2UTe+bIuaop0XYvkVIQl7qYUF478Q== - dependencies: - "@backstage/config" "^0.1.2" - cross-fetch "^3.0.6" - git-url-parse "^11.4.3" - "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" From 3c75afde45e23992410726325470276112efda0d Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 19 Jan 2021 10:21:52 +0100 Subject: [PATCH 091/209] Remove stacktrace from logs --- plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts b/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts index fc11545e04..25f9b23e59 100644 --- a/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts +++ b/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts @@ -120,7 +120,6 @@ export class JobProcessor implements Processor { // Log to the current stage the error that occurred and fail the stage. stage.status = 'FAILED'; logger.error(`Stage failed with error: ${error.message}`); - logger.error(error.stack); // Throw the error so the job can be failed too. throw error; From 4a3a9ff637b127a44c58dc7d184370da9b529e57 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 19 Jan 2021 10:24:28 +0100 Subject: [PATCH 092/209] Log stacktrace in debug --- plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts b/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts index 25f9b23e59..4ea0b4371b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts +++ b/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts @@ -120,7 +120,7 @@ export class JobProcessor implements Processor { // Log to the current stage the error that occurred and fail the stage. stage.status = 'FAILED'; logger.error(`Stage failed with error: ${error.message}`); - + logger.debug(error.stack); // Throw the error so the job can be failed too. throw error; } finally { From 61abb1a500f1084ca865daa07e99065a62113f8e Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Tue, 19 Jan 2021 10:36:27 +0100 Subject: [PATCH 093/209] Update adr006 on using PropsWithChildren --- docs/architecture-decisions/adr006-avoid-react-fc.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/architecture-decisions/adr006-avoid-react-fc.md b/docs/architecture-decisions/adr006-avoid-react-fc.md index 51dcf042da..eecb4ad87a 100644 --- a/docs/architecture-decisions/adr006-avoid-react-fc.md +++ b/docs/architecture-decisions/adr006-avoid-react-fc.md @@ -43,6 +43,15 @@ const GoodComponent = ({ text, children }: GoodProps) => ( {children} ); + +/* Or as a shorthand, if no specifc child type is required */ +type GoodProps = PropsWithChildren<{ text: string }>; +const GoodComponent = ({ text, children }: GoodProps) => ( +
+
{text}
+ {children} +
+); ``` ## Consequences From a65c4516ec854875e7d21139f9418bcc2bad1c11 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Tue, 19 Jan 2021 10:41:40 +0100 Subject: [PATCH 094/209] Use GitHub API to download the archive --- .../src/reading/GithubUrlReader.test.ts | 33 +++++++++----- .../src/reading/GithubUrlReader.ts | 42 +++++++++--------- .../backstage-mock-etag123.tar.gz | Bin 0 -> 240 bytes 3 files changed, 42 insertions(+), 33 deletions(-) create mode 100644 packages/backend-common/src/reading/__fixtures__/backstage-mock-etag123.tar.gz diff --git a/packages/backend-common/src/reading/GithubUrlReader.test.ts b/packages/backend-common/src/reading/GithubUrlReader.test.ts index 26ba91bd29..0b7d56480f 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.test.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.test.ts @@ -108,7 +108,12 @@ describe('GithubUrlReader', () => { describe('readTree', () => { const repoBuffer = fs.readFileSync( - path.resolve('src', 'reading', '__fixtures__', 'mock-main.tar.gz'), + path.resolve( + 'src', + 'reading', + '__fixtures__', + 'backstage-mock-etag123.tar.gz', + ), ); const reposGithubApiResponse = { @@ -117,12 +122,16 @@ describe('GithubUrlReader', () => { default_branch: 'main', branches_url: 'https://api.github.com/repos/backstage/mock/branches{/branch}', + archive_url: + 'https://api.github.com/repos/backstage/mock/{archive_format}{/ref}', }; const reposGheApiResponse = { ...reposGithubApiResponse, branches_url: 'https://ghe.github.com/api/v3/repos/backstage/mock/branches{/branch}', + archive_url: + 'https://ghe.github.com/api/v3/repos/backstage/mock/{archive_format}{/ref}', }; const branchesApiResponse = { @@ -134,15 +143,6 @@ describe('GithubUrlReader', () => { beforeEach(() => { worker.use( - rest.get( - 'https://github.com/backstage/mock/archive/main.tar.gz', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/x-gzip'), - ctx.body(repoBuffer), - ), - ), rest.get('https://api.github.com/repos/backstage/mock', (_, res, ctx) => res( ctx.status(200), @@ -159,12 +159,21 @@ describe('GithubUrlReader', () => { ctx.json(branchesApiResponse), ), ), + rest.get( + 'https://api.github.com/repos/backstage/mock/tarball/etag123abc', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/x-gzip'), + ctx.body(repoBuffer), + ), + ), rest.get( 'https://api.github.com/repos/backstage/mock/branches/branchDoesNotExist', (_, res, ctx) => res(ctx.status(404)), ), rest.get( - 'https://ghe.github.com/backstage/mock/archive/main.tar.gz', + 'https://ghe.github.com/api/v3/repos/backstage/mock/tarball/etag123abc', (_, res, ctx) => res( ctx.status(200), @@ -224,7 +233,7 @@ describe('GithubUrlReader', () => { worker.use( rest.get( - 'https://ghe.github.com/backstage/mock/archive/main.tar.gz', + 'https://ghe.github.com/api/v3/repos/backstage/mock/tarball/etag123abc', (req, res, ctx) => { expect(req.headers.get('authorization')).toBe( mockHeaders.Authorization, diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts index d6612a9da2..b5b4e9c1a7 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.ts @@ -98,14 +98,9 @@ export class GithubUrlReader implements UrlReader { url: string, options?: ReadTreeOptions, ): Promise { - const { - protocol, - resource, - name: repoName, - ref, - filepath, - full_name, - } = parseGitUrl(url); + const { ref, filepath, full_name } = parseGitUrl(url); + // Caveat: The ref will totally be incorrect if the branch name includes a / + // Thus, readTree can not work on url containing branch name that has a / const { headers } = await this.deps.credentialsProvider.getCredentials({ url, @@ -132,6 +127,7 @@ export class GithubUrlReader implements UrlReader { // Use GitHub API to get the default branch of the repository. const branch = ref || repoResponseJson.default_branch; const branchesApiUrl = repoResponseJson.branches_url; + const archiveApiUrl = repoResponseJson.archive_url; // Fetch the latest commit in the provided or default branch to compare against // the provided sha. @@ -155,19 +151,12 @@ export class GithubUrlReader implements UrlReader { throw new NotModifiedError(); } - // Note: the API way of downloading an archive URL does not return a real time archive. - // https://github.community/t/archive-downloaded-via-v3-rest-api-is-not-real-time/14827 - // It looks like this https://api.github.com/repos/owner/repo/{archive_format}{/ref} - // and can be used from `repoResponseJson.archive_url`. - // Continue using the "direct" way i.e. https://github.com/:owner/:repo/archive/branch.tar.gz - // until the bug? is fixed. const archive = await fetch( - new URL( - `${protocol}://${resource}/${full_name}/archive/${branch}.tar.gz`, - ).toString(), - { - headers, - }, + // archiveApiUrl looks like "https://api.github.com/repos/owner/repo/{archive_format}{/ref}" + archiveApiUrl + .replace('{archive_format}', 'tarball') + .replace('{/ref}', `/${commitSha}`), + { headers }, ); if (!archive.ok) { const message = `Failed to read tree (archive) from ${url}, ${archive.status} ${archive.statusText}`; @@ -177,7 +166,18 @@ export class GithubUrlReader implements UrlReader { throw new Error(message); } - const path = `${repoName}-${branch}/${filepath}`; + // Note that repoResponseJson.full_name must be used over full_name because the path + // is case sensitive and full_name may not be inq the correct case. + // TODO(OrkoHunter): The directory name inside the tarball should be retrieved from the tar + // instead of being constructed here. Same goes for GitLab, Bitbucket and Azure. + const extractedDirName = `${repoResponseJson.full_name.replace( + '/', + '-', + )}-${commitSha.substr(0, 7)}`; + + // The path includes the name of the directory inside the tarball and a sub path + // if requested in readTree. + const path = `${extractedDirName}/${filepath}`; return await this.deps.treeResponseFactory.fromTarArchive({ // TODO(Rugvip): Underlying implementation of fetch will be node-fetch, we probably want diff --git a/packages/backend-common/src/reading/__fixtures__/backstage-mock-etag123.tar.gz b/packages/backend-common/src/reading/__fixtures__/backstage-mock-etag123.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..e1ac2579de3999e2847c562c9d0882f486b81e22 GIT binary patch literal 240 zcmVS&>cxevkEi2meR ze~QeW5auDUP#vJO?yE@2E!s)ltbW~(V_pW{|Lyf#`vgAne+tI`LoC4g{~V0z qUsS0)2P*xx#-#s4_^Lkv0mAwJ9ITz~I~)$jBDn%0oP%uu5C8yN%yPE? literal 0 HcmV?d00001 From a41a94b4b5a570604eafc0f26d09be56421b51fb Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 19 Jan 2021 11:20:40 +0100 Subject: [PATCH 095/209] register preparers with fromConfig Co-authored-by: blam --- .../scaffolder/stages/prepare/azure.test.ts | 10 +++--- .../src/scaffolder/stages/prepare/azure.ts | 31 ++++++++++-------- .../stages/prepare/bitbucket.test.ts | 12 +++---- .../scaffolder/stages/prepare/bitbucket.ts | 28 ++++++++++------ .../scaffolder/stages/prepare/github.test.ts | 9 ++---- .../src/scaffolder/stages/prepare/github.ts | 27 +++++++++------- .../scaffolder/stages/prepare/gitlab.test.ts | 10 +++--- .../src/scaffolder/stages/prepare/gitlab.ts | 32 +++++++++++-------- .../scaffolder/stages/prepare/preparers.ts | 8 ++--- 9 files changed, 90 insertions(+), 77 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts index 896d91f4ed..d0860ea96c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts @@ -92,7 +92,6 @@ describe('AzurePreparer', () => { }, }, }), - { logger }, ); await preparer.prepare(mockEntity, { logger }); @@ -113,7 +112,6 @@ describe('AzurePreparer', () => { ], }, }), - { logger }, ); await preparer.prepare(mockEntity, { logger }); @@ -126,7 +124,7 @@ describe('AzurePreparer', () => { }); it('calls the clone command with the correct arguments for a repository', async () => { - const preparer = new AzurePreparer(new ConfigReader({}), { logger }); + const preparer = new AzurePreparer(new ConfigReader({})); await preparer.prepare(mockEntity, { logger: getVoidLogger() }); @@ -138,7 +136,7 @@ describe('AzurePreparer', () => { }); it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { - const preparer = new AzurePreparer(new ConfigReader({}), { logger }); + const preparer = new AzurePreparer(new ConfigReader({})); delete mockEntity.spec.path; await preparer.prepare(mockEntity, { logger: getVoidLogger() }); @@ -151,7 +149,7 @@ describe('AzurePreparer', () => { }); it('return the temp directory with the path to the folder if it is specified', async () => { - const preparer = new AzurePreparer(new ConfigReader({}), { logger }); + const preparer = new AzurePreparer(new ConfigReader({})); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { @@ -164,7 +162,7 @@ describe('AzurePreparer', () => { }); it('return the working directory with the path to the folder if it is specified', async () => { - const preparer = new AzurePreparer(new ConfigReader({}), { logger }); + const preparer = new AzurePreparer(new ConfigReader({})); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts index d39b1f9e2d..001626f417 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts @@ -32,26 +32,31 @@ export class AzurePreparer implements PreparerBase { private readonly integrations: AzureIntegrationConfig[]; private readonly scaffolderToken: string | undefined; - constructor(config: Config, { logger }: { logger: Logger }) { + static fromConfig(config: Config, { logger }: { logger: Logger }) { + const integrations = readAzureIntegrationConfigs( + config.getOptionalConfigArray('integrations.azure') ?? [], + ); + + if ( + config.getOptionalString('scaffolder.azure.api.token') && + !integrations.length + ) { + logger.warn( + "DEPRECATION: Using the token format under 'scaffolder.azure.api.token' will not be respected in future releases. Please consider using integrations config instead", + 'Please migrate to using integrations config and specifying tokens under hostnames', + ); + } + return new AzurePreparer(config); + } + + constructor(config: Config) { this.integrations = readAzureIntegrationConfigs( config.getOptionalConfigArray('integrations.azure') ?? [], ); - if (!this.integrations.length) { - logger.warn( - 'Integrations for Azure in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', - ); - } - this.scaffolderToken = config.getOptionalString( 'scaffolder.azure.api.token', ); - - if (this.scaffolderToken) { - logger.warn( - "DEPRECATION: Using the token format under 'scaffolder.azure.api.token' will not be respected in future releases. Please consider using integrations config instead", - ); - } } async prepare( diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts index d55ca7a01d..05b64ca777 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts @@ -80,7 +80,7 @@ describe('BitbucketPreparer', () => { }); it('calls the clone command with the correct arguments for a repository', async () => { - const preparer = new BitbucketPreparer(new ConfigReader({}), { logger }); + const preparer = new BitbucketPreparer(new ConfigReader({})); await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://bitbucket.org/backstage-project/backstage-repo', @@ -101,7 +101,6 @@ describe('BitbucketPreparer', () => { ], }, }), - { logger }, ); await preparer.prepare(mockEntity, { logger }); @@ -114,7 +113,7 @@ describe('BitbucketPreparer', () => { }); it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { - const preparer = new BitbucketPreparer(new ConfigReader({}), { logger }); + const preparer = new BitbucketPreparer(new ConfigReader({})); delete mockEntity.spec.path; await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ @@ -124,7 +123,7 @@ describe('BitbucketPreparer', () => { }); it('return the temp directory with the path to the folder if it is specified', async () => { - const preparer = new BitbucketPreparer(new ConfigReader({}), { logger }); + const preparer = new BitbucketPreparer(new ConfigReader({})); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { logger: getVoidLogger(), @@ -147,7 +146,6 @@ describe('BitbucketPreparer', () => { }, }, }), - { logger }, ); await preparer.prepare(mockEntity, { logger }); @@ -172,7 +170,6 @@ describe('BitbucketPreparer', () => { ], }, }), - { logger }, ); await preparer.prepare(mockEntity, { logger }); @@ -197,7 +194,6 @@ describe('BitbucketPreparer', () => { ], }, }), - { logger }, ); await preparer.prepare(mockEntity, { logger }); @@ -210,7 +206,7 @@ describe('BitbucketPreparer', () => { }); it('return the working directory with the path to the folder if it is specified', async () => { - const preparer = new BitbucketPreparer(new ConfigReader({}), { logger }); + const preparer = new BitbucketPreparer(new ConfigReader({})); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { workingDirectory: '/workDir', diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts index 26d9b476ee..7cd724cbc1 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts @@ -32,27 +32,35 @@ export class BitbucketPreparer implements PreparerBase { private readonly privateToken: string; private readonly username: string; private readonly integrations: BitbucketIntegrationConfig[]; - constructor(config: Config, { logger }: { logger: Logger }) { - this.integrations = readBitbucketIntegrationConfigs( + + static fromConfig(config: Config, { logger }: { logger: Logger }) { + const integrations = readBitbucketIntegrationConfigs( config.getOptionalConfigArray('integrations.bitbucket') ?? [], ); - if (!this.integrations.length) { + const user = config.getOptionalString('scaffolder.bitbucket.api.username'); + const token = config.getOptionalString('scaffolder.bitbucket.api.token'); + const password = config.getOptionalString( + 'scaffolder.bitbucket.api.appPassword', + ); + + if (!integrations && (user || token || password)) { logger.warn( - 'Integrations for BitBucket in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', + "DEPRECATION: Setting credentials under 'scaffolder.bitbucket.api' will not be respected in future releases. Please consider using integrations config instead", + 'Please migrate to using integrations config and specifying tokens under hostnames', ); } + return new BitbucketPreparer(config); + } + constructor(config: Config) { + this.integrations = readBitbucketIntegrationConfigs( + config.getOptionalConfigArray('integrations.bitbucket') ?? [], + ); this.username = config.getOptionalString('scaffolder.bitbucket.api.username') ?? ''; this.privateToken = config.getOptionalString('scaffolder.bitbucket.api.token') ?? ''; - - if (this.username || this.privateToken) { - logger.warn( - "DEPRECATION: Using the token format under 'scaffolder.bitbucket.token' will not be respected in future releases. Please consider using integrations config instead", - ); - } } async prepare( diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts index 122a1abb4c..68178610b5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts @@ -87,7 +87,6 @@ describe('GitHubPreparer', () => { }, }, }), - { logger }, ); await preparer.prepare(mockEntity, { logger: getVoidLogger() }); @@ -98,7 +97,7 @@ describe('GitHubPreparer', () => { }); }); it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { - const preparer = new GithubPreparer(new ConfigReader({}), { logger }); + const preparer = new GithubPreparer(new ConfigReader({})); delete mockEntity.spec.path; await preparer.prepare(mockEntity, { logger: getVoidLogger() }); @@ -110,7 +109,7 @@ describe('GitHubPreparer', () => { }); it('return the temp directory with the path to the folder if it is specified', async () => { - const preparer = new GithubPreparer(new ConfigReader({}), { logger }); + const preparer = new GithubPreparer(new ConfigReader({})); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { logger: getVoidLogger(), @@ -121,7 +120,7 @@ describe('GitHubPreparer', () => { }); it('return the working directory with the path to the folder if it is specified', async () => { - const preparer = new GithubPreparer(new ConfigReader({}), { logger }); + const preparer = new GithubPreparer(new ConfigReader({})); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { workingDirectory: '/workDir', @@ -142,7 +141,6 @@ describe('GitHubPreparer', () => { }, }, }), - { logger }, ); await preparer.prepare(mockEntity, { logger }); @@ -161,7 +159,6 @@ describe('GitHubPreparer', () => { github: [{ host: 'github.com', token: 'fake-me' }], }, }), - { logger }, ); await preparer.prepare(mockEntity, { logger }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts index 81b212baaf..6ba341ad2c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts @@ -32,24 +32,29 @@ export class GithubPreparer implements PreparerBase { private readonly integrations: GitHubIntegrationConfig[]; private readonly scaffolderToken: string | undefined; - constructor(config: Config, { logger }: { logger: Logger }) { - this.integrations = readGitHubIntegrationConfigs( + static fromConfig(config: Config, { logger }: { logger: Logger }) { + const integrations = readGitHubIntegrationConfigs( config.getOptionalConfigArray('integrations.github') ?? [], ); - if (!this.integrations.length) { - logger.warn( - 'Integrations for Github in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', - ); - } - - this.scaffolderToken = config.getOptionalString('scaffolder.github.token'); - - if (this.scaffolderToken) { + if ( + config.getOptionalString('scaffolder.github.token') && + !integrations.length + ) { logger.warn( "DEPRECATION: Using the token format under 'scaffolder.github.token' will not be respected in future releases. Please consider using integrations config instead", + 'Please migrate to using integrations config and specifying tokens under hostnames', ); } + + return new GithubPreparer(config); + } + + constructor(config: Config) { + this.integrations = readGitHubIntegrationConfigs( + config.getOptionalConfigArray('integrations.github') ?? [], + ); + this.scaffolderToken = config.getOptionalString('scaffolder.github.token'); } async prepare( diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts index c1e42920ae..f9e79e6c71 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts @@ -80,7 +80,7 @@ describe('GitLabPreparer', () => { ['gitlab'].forEach(protocol => { it(`calls the clone command with the correct arguments for a repository using the ${protocol} protocol`, async () => { - const preparer = new GitlabPreparer(new ConfigReader({}), { logger }); + const preparer = new GitlabPreparer(new ConfigReader({})); mockEntity = mockEntityWithProtocol(protocol); await preparer.prepare(mockEntity, { logger: getVoidLogger() }); @@ -103,7 +103,6 @@ describe('GitLabPreparer', () => { ], }, }), - { logger }, ); mockEntity = mockEntityWithProtocol(protocol); @@ -123,7 +122,6 @@ describe('GitLabPreparer', () => { gitlab: { api: { token: 'fake-token' } }, }, }), - { logger }, ); mockEntity = mockEntityWithProtocol(protocol); @@ -137,7 +135,7 @@ describe('GitLabPreparer', () => { }); it(`calls the clone command with the correct arguments for a repository when no path is provided using the ${protocol} protocol`, async () => { - const preparer = new GitlabPreparer(new ConfigReader({}), { logger }); + const preparer = new GitlabPreparer(new ConfigReader({})); mockEntity = mockEntityWithProtocol(protocol); delete mockEntity.spec.path; @@ -150,7 +148,7 @@ describe('GitLabPreparer', () => { }); it(`return the temp directory with the path to the folder if it is specified using the ${protocol} protocol`, async () => { - const preparer = new GitlabPreparer(new ConfigReader({}), { logger }); + const preparer = new GitlabPreparer(new ConfigReader({})); mockEntity = mockEntityWithProtocol(protocol); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { @@ -162,7 +160,7 @@ describe('GitLabPreparer', () => { }); it('return the working directory with the path to the folder if it is specified', async () => { - const preparer = new GitlabPreparer(new ConfigReader({}), { logger }); + const preparer = new GitlabPreparer(new ConfigReader({})); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { workingDirectory: '/workDir', diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts index 7f9d9c7d3a..4e5cdb9cc7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts @@ -32,26 +32,32 @@ export class GitlabPreparer implements PreparerBase { private readonly integrations: GitLabIntegrationConfig[]; private readonly scaffolderToken: string | undefined; - constructor(config: Config, { logger }: { logger: Logger }) { + static fromConfig(config: Config, { logger }: { logger: Logger }) { + const integrations = readGitLabIntegrationConfigs( + config.getOptionalConfigArray('integrations.gitlab') ?? [], + ); + + if ( + config.getOptionalString('scaffolder.gitlab.api.token') && + !integrations.length + ) { + logger.warn( + "DEPRECATION: Using the token format under 'scaffolder.gitlab.token' will not be respected in future releases. Please consider using integrations config instead", + 'Please migrate to using integrations config and specifying tokens under hostnames', + ); + } + + return new GitlabPreparer(config); + } + + constructor(config: Config) { this.integrations = readGitLabIntegrationConfigs( config.getOptionalConfigArray('integrations.gitlab') ?? [], ); - if (!this.integrations.length) { - logger.warn( - 'Integrations for GitLab in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', - ); - } - this.scaffolderToken = config.getOptionalString( 'scaffolder.gitlab.api.token', ); - - if (this.scaffolderToken) { - logger.warn( - "DEPRECATION: Using the token format under 'scaffolder.gitlab.api.token' will not be respected in future releases. Please consider using integrations config instead", - ); - } } async prepare( diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts index 251e8b0ae3..94d2706cd8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts @@ -78,10 +78,10 @@ export class Preparers implements PreparerBuilder { const preparers = new Preparers(typeDetector); const filePreparer = new FilePreparer(); - const gitlabPreparer = new GitlabPreparer(config, { logger }); - const azurePreparer = new AzurePreparer(config, { logger }); - const githubPreparer = new GithubPreparer(config, { logger }); - const bitbucketPreparer = new BitbucketPreparer(config, { logger }); + const gitlabPreparer = GitlabPreparer.fromConfig(config, { logger }); + const azurePreparer = AzurePreparer.fromConfig(config, { logger }); + const githubPreparer = GithubPreparer.fromConfig(config, { logger }); + const bitbucketPreparer = BitbucketPreparer.fromConfig(config, { logger }); preparers.register('file', filePreparer); preparers.register('gitlab', gitlabPreparer); From a532d9d124b20d836a211fab95354aee310041a8 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 19 Jan 2021 11:51:19 +0100 Subject: [PATCH 096/209] Add fromConfig remove logging --- packages/backend/src/plugins/scaffolder.ts | 2 +- .../src/scaffolder/stages/prepare/azure.ts | 10 ++---- .../scaffolder/stages/prepare/bitbucket.ts | 6 +--- .../src/scaffolder/stages/prepare/github.ts | 9 +---- .../src/scaffolder/stages/prepare/gitlab.ts | 9 +---- .../scaffolder/stages/publish/azure.test.ts | 2 -- .../src/scaffolder/stages/publish/azure.ts | 26 +++----------- .../stages/publish/bitbucket.test.ts | 6 ---- .../scaffolder/stages/publish/bitbucket.ts | 36 +++---------------- .../scaffolder/stages/publish/github.test.ts | 7 ---- .../src/scaffolder/stages/publish/github.ts | 24 +++---------- .../scaffolder/stages/publish/gitlab.test.ts | 2 -- .../src/scaffolder/stages/publish/gitlab.ts | 26 +++----------- .../stages/publish/publishers.test.ts | 19 +++------- .../scaffolder/stages/publish/publishers.ts | 13 +++---- 15 files changed, 32 insertions(+), 165 deletions(-) diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index 5d36d508a5..70a961819f 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -38,7 +38,7 @@ export default async function createPlugin({ templaters.register('cra', craTemplater); const preparers = await Preparers.fromConfig(config, { logger }); - const publishers = await Publishers.fromConfig(config, { logger }); + const publishers = await Publishers.fromConfig(config); const dockerClient = new Docker(); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts index 001626f417..5064c8ff1e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts @@ -33,19 +33,13 @@ export class AzurePreparer implements PreparerBase { private readonly scaffolderToken: string | undefined; static fromConfig(config: Config, { logger }: { logger: Logger }) { - const integrations = readAzureIntegrationConfigs( - config.getOptionalConfigArray('integrations.azure') ?? [], - ); - - if ( - config.getOptionalString('scaffolder.azure.api.token') && - !integrations.length - ) { + if (config.getOptionalString('scaffolder.azure.api.token')) { logger.warn( "DEPRECATION: Using the token format under 'scaffolder.azure.api.token' will not be respected in future releases. Please consider using integrations config instead", 'Please migrate to using integrations config and specifying tokens under hostnames', ); } + return new AzurePreparer(config); } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts index 7cd724cbc1..cce0689ac9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts @@ -34,17 +34,13 @@ export class BitbucketPreparer implements PreparerBase { private readonly integrations: BitbucketIntegrationConfig[]; static fromConfig(config: Config, { logger }: { logger: Logger }) { - const integrations = readBitbucketIntegrationConfigs( - config.getOptionalConfigArray('integrations.bitbucket') ?? [], - ); - const user = config.getOptionalString('scaffolder.bitbucket.api.username'); const token = config.getOptionalString('scaffolder.bitbucket.api.token'); const password = config.getOptionalString( 'scaffolder.bitbucket.api.appPassword', ); - if (!integrations && (user || token || password)) { + if (user || token || password) { logger.warn( "DEPRECATION: Setting credentials under 'scaffolder.bitbucket.api' will not be respected in future releases. Please consider using integrations config instead", 'Please migrate to using integrations config and specifying tokens under hostnames', diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts index 6ba341ad2c..49858adf42 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts @@ -33,14 +33,7 @@ export class GithubPreparer implements PreparerBase { private readonly scaffolderToken: string | undefined; static fromConfig(config: Config, { logger }: { logger: Logger }) { - const integrations = readGitHubIntegrationConfigs( - config.getOptionalConfigArray('integrations.github') ?? [], - ); - - if ( - config.getOptionalString('scaffolder.github.token') && - !integrations.length - ) { + if (config.getOptionalString('scaffolder.github.token')) { logger.warn( "DEPRECATION: Using the token format under 'scaffolder.github.token' will not be respected in future releases. Please consider using integrations config instead", 'Please migrate to using integrations config and specifying tokens under hostnames', diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts index 4e5cdb9cc7..49168deeff 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts @@ -33,14 +33,7 @@ export class GitlabPreparer implements PreparerBase { private readonly scaffolderToken: string | undefined; static fromConfig(config: Config, { logger }: { logger: Logger }) { - const integrations = readGitLabIntegrationConfigs( - config.getOptionalConfigArray('integrations.gitlab') ?? [], - ); - - if ( - config.getOptionalString('scaffolder.gitlab.api.token') && - !integrations.length - ) { + if (config.getOptionalString('scaffolder.gitlab.api.token')) { logger.warn( "DEPRECATION: Using the token format under 'scaffolder.gitlab.token' will not be respected in future releases. Please consider using integrations config instead", 'Please migrate to using integrations config and specifying tokens under hostnames', diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts index 9aaee4bcb8..2784a1f9bb 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts @@ -51,7 +51,6 @@ describe('Azure Publisher', () => { }, }, }), - { logger }, ); mockGitClient.createRepository.mockResolvedValue({ remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', @@ -106,7 +105,6 @@ describe('Azure Publisher', () => { ], }, }), - { logger }, ); mockGitClient.createRepository.mockResolvedValue({ remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts index 4172f28f6b..d3f900d706 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts @@ -19,7 +19,6 @@ import { IGitApi } from 'azure-devops-node-api/GitApi'; import { GitRepositoryCreateOptions } from 'azure-devops-node-api/interfaces/GitInterfaces'; import { Config } from '@backstage/config'; import { initRepoAndPush } from './helpers'; -import { Logger } from 'winston'; import { AzureIntegrationConfig, readAzureIntegrationConfigs, @@ -32,33 +31,18 @@ export class AzurePublisher implements PublisherBase { private readonly apiBaseUrl?: string; private readonly token?: string; - constructor(config: Config, { logger }: { logger: Logger }) { + static fromConfig(config: Config) { + return new AzurePublisher(config); + } + + constructor(config: Config) { this.integrations = readAzureIntegrationConfigs( config.getOptionalConfigArray('integrations.azure') ?? [], ); - if (!this.integrations.length) { - logger.warn( - 'Integrations for Azure in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', - ); - } - this.token = config.getOptionalString('scaffolder.azure.api.token'); - if (this.token) { - logger.warn( - "DEPRECATION: Using the token format under 'scaffolder.github.api.token' will not be respected in future releases. Please consider using integrations config instead", - ); - } - this.apiBaseUrl = config.getOptionalString('scaffolder.azure.api.baseUrl'); - - if (this.apiBaseUrl) { - logger.warn( - "DEPRECATION: Using the apiBaseUrl format under 'scaffolder.azure.api.baseUrl' will not be respected in future releases. Please consider using integrations config instead", - ); - } } - async publish({ values, directory, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts index 6cfcc7d69e..c4ef86786e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.test.ts @@ -71,9 +71,6 @@ describe('Bitbucket Publisher', () => { ], }, }), - { - logger: getVoidLogger(), - }, ); const result = await publisher.publish({ @@ -140,9 +137,6 @@ describe('Bitbucket Publisher', () => { ], }, }), - { - logger: getVoidLogger(), - }, ); const result = await publisher.publish({ diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts index 4cc7654136..ea21f89834 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts @@ -22,7 +22,6 @@ import { BitbucketIntegrationConfig, readBitbucketIntegrationConfigs, } from '@backstage/integration'; -import { Logger } from 'winston'; import gitUrlParse from 'git-url-parse'; // TODO(blam): We should probably start to use a bitbucket client here that we can change @@ -35,48 +34,21 @@ export class BitbucketPublisher implements PublisherBase { private readonly appPassword?: string; private readonly integrations: BitbucketIntegrationConfig[]; - constructor(config: Config, { logger }: { logger: Logger }) { + static fromConfig(config: Config) { + return new BitbucketPublisher(config); + } + constructor(config: Config) { this.integrations = readBitbucketIntegrationConfigs( config.getOptionalConfigArray('integrations.bitbucket') ?? [], ); - - if (!this.integrations.length) { - logger.warn( - 'Integrations for Bitbucket in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', - ); - } - this.token = config.getOptionalString('scaffolder.bitbucket.api.token'); - if (this.token) { - logger.warn( - "DEPRECATION: Using the token format under 'scaffolder.bitbucket.api.token' will not be respected in future releases. Please consider using integrations config instead", - ); - } - this.host = config.getOptionalString('scaffolder.bitbucket.api.host'); - if (this.host) { - logger.warn( - "DEPRECATION: Using the apiBaseUrl format under 'scaffolder.bitbucket.api.host' will not be respected in future releases. Please consider using integrations config instead", - ); - } - this.username = config.getOptionalString( 'scaffolder.bitbucket.api.username', ); - if (this.username) { - logger.warn( - "DEPRECATION: Using the apiBaseUrl format under 'scaffolder.bitbucket.api.username' will not be respected in future releases. Please consider using integrations config instead", - ); - } - this.appPassword = config.getOptionalString( 'scaffolder.bitbucket.api.appPassword', ); - if (this.appPassword) { - logger.warn( - "DEPRECATION: Using the appPassword format under 'scaffolder.bitbucket.api.appassword' will not be respected in future releases. Please consider using integrations config instead", - ); - } } async publish({ diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts index 7402dc5ef5..c5b8187bc4 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts @@ -49,9 +49,6 @@ describe('GitHub Publisher', () => { ], }, }), - { - logger, - }, ); describe('publish: createRemoteInGithub', () => { @@ -211,7 +208,6 @@ describe('GitHub Publisher', () => { }, }, }), - { logger }, ); it('creates a private repository in the organization with visibility set to internal', async () => { @@ -273,9 +269,6 @@ describe('GitHub Publisher', () => { }, }, }), - { - logger, - }, ); it('creates a private repository', async () => { diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts index 902dc72a77..a9004966a4 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts @@ -17,7 +17,6 @@ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; import { initRepoAndPush } from './helpers'; import { Config } from '@backstage/config'; -import { Logger } from 'winston'; import { GitHubIntegrationConfig, readGitHubIntegrationConfigs, @@ -32,35 +31,20 @@ export class GithubPublisher implements PublisherBase { private readonly apiBaseUrl: string | undefined; private readonly repoVisibility: RepoVisibilityOptions; - constructor(config: Config, { logger }: { logger: Logger }) { + static fromConfig(config: Config) { + return new GithubPublisher(config); + } + constructor(config: Config) { this.integrations = readGitHubIntegrationConfigs( config.getOptionalConfigArray('integrations.github') ?? [], ); - if (!this.integrations.length) { - logger.warn( - 'Integrations for GitHub in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', - ); - } - this.scaffolderToken = config.getOptionalString( 'scaffolder.github.api.token', ); this.apiBaseUrl = config.getOptionalString('scaffolder.github.api.baseUrl'); - if (this.scaffolderToken) { - logger.warn( - "DEPRECATION: Using the token format under 'scaffolder.github.api.token' will not be respected in future releases. Please consider using integrations config instead", - ); - } - - if (this.apiBaseUrl) { - logger.warn( - "DEPRECATION: Using the apiBaseUrl format under 'scaffolder.github.api.baseUrl' will not be respected in future releases. Please consider using integrations config instead", - ); - } - this.repoVisibility = (config.getOptionalString( 'scaffolder.github.visibility', ) ?? 'public') as RepoVisibilityOptions; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts index 67887f579f..fe997f295d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts @@ -61,7 +61,6 @@ describe('GitLab Publisher', () => { ], }, }), - { logger }, ); mockGitlabClient.Namespaces.show.mockResolvedValue({ @@ -109,7 +108,6 @@ describe('GitLab Publisher', () => { ], }, }), - { logger }, ); mockGitlabClient.Namespaces.show.mockResolvedValue({}); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts index 71a38f1079..76167cf20f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts @@ -17,7 +17,6 @@ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; import { Gitlab } from '@gitbeaker/node'; import { Config } from '@backstage/config'; -import { Logger } from 'winston'; import { initRepoAndPush } from './helpers'; import gitUrlParse from 'git-url-parse'; @@ -30,37 +29,20 @@ export class GitlabPublisher implements PublisherBase { private readonly integrations: GitLabIntegrationConfig[]; private readonly scaffolderToken: string | undefined; private readonly apiBaseUrl: string | undefined; - private readonly logger: Logger; - constructor(config: Config, { logger }: { logger: Logger }) { - this.logger = logger; + constructor(config: Config) { this.integrations = readGitLabIntegrationConfigs( config.getOptionalConfigArray('integrations.gitlab') ?? [], ); - - if (!this.integrations.length) { - this.logger.warn( - 'Integrations for GitLab in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames', - ); - } - this.scaffolderToken = config.getOptionalString( 'scaffolder.gitlab.api.token', ); this.apiBaseUrl = config.getOptionalString('scaffolder.gitlab.api.baseUrl'); + } - if (this.scaffolderToken) { - this.logger.warn( - "DEPRECATION: Using the token format under 'scaffolder.gitlab.api.token' will not be respected in future releases. Please consider using integrations config instead", - ); - } - - if (this.apiBaseUrl) { - this.logger.warn( - "DEPRECATION: Using the apiBaseUrl format under 'scaffolder.gitlab.api.baseUrl' will not be respected in future releases. Please consider using integrations config instead", - ); - } + static fromConfig(config: Config) { + return new GitlabPublisher(config); } async publish({ diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.test.ts index fdbc283f67..869d1fcd03 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.test.ts @@ -40,9 +40,7 @@ describe('Publishers', () => { }); it('should return the correct preparer when the source matches for github', async () => { - const publishers = await Publishers.fromConfig(new ConfigReader({}), { - logger: getVoidLogger(), - }); + const publishers = await Publishers.fromConfig(new ConfigReader({})); expect( publishers.get('https://github.com/org/repo', { @@ -52,9 +50,7 @@ describe('Publishers', () => { }); it('should return the correct preparer when the source matches for azure', async () => { - const publishers = await Publishers.fromConfig(new ConfigReader({}), { - logger: getVoidLogger(), - }); + const publishers = await Publishers.fromConfig(new ConfigReader({})); expect( publishers.get('https://dev.azure.com/org/project/_git/repo', { @@ -64,9 +60,7 @@ describe('Publishers', () => { }); it('should return the correct preparer when the source matches for bitbucket', async () => { - const publishers = await Publishers.fromConfig(new ConfigReader({}), { - logger: getVoidLogger(), - }); + const publishers = await Publishers.fromConfig(new ConfigReader({})); expect( publishers.get('https://bitbucket.org/owner/repo', { @@ -76,9 +70,7 @@ describe('Publishers', () => { }); it('should return the correct preparer when the source matches for gitlab', async () => { - const publishers = await Publishers.fromConfig(new ConfigReader({}), { - logger: getVoidLogger(), - }); + const publishers = await Publishers.fromConfig(new ConfigReader({})); expect( publishers.get('https://gitlab.com/owner/repo', { @@ -96,9 +88,6 @@ describe('Publishers', () => { ], }, }), - { - logger: getVoidLogger(), - }, ); expect( diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts index 1fafc33c68..2527ad12b4 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts @@ -62,24 +62,21 @@ export class Publishers implements PublisherBuilder { return publisher; } - static async fromConfig( - config: Config, - { logger }: { logger: Logger }, - ): Promise { + static async fromConfig(config: Config): Promise { const typeDetector = makeDeprecatedLocationTypeDetector(config); const publishers = new Publishers(typeDetector); - const githubPublisher = new GithubPublisher(config, { logger }); + const githubPublisher = GithubPublisher.fromConfig(config); publishers.register('file', githubPublisher); publishers.register('github', githubPublisher); - const gitLabPublisher = new GitlabPublisher(config, { logger }); + const gitLabPublisher = GitlabPublisher.fromConfig(config); publishers.register('gitlab', gitLabPublisher); - const azurePublisher = new AzurePublisher(config, { logger }); + const azurePublisher = AzurePublisher.fromConfig(config); publishers.register('azure', azurePublisher); - const bitbucketPublisher = new BitbucketPublisher(config, { logger }); + const bitbucketPublisher = BitbucketPublisher.fromConfig(config); publishers.register('bitbucket', bitbucketPublisher); return publishers; From d095cbf3d356759a8478e5e310073685141004e6 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 19 Jan 2021 13:03:34 +0100 Subject: [PATCH 097/209] Delete log argument when calling fromConfig --- .../default-app/packages/backend/src/plugins/scaffolder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts index 196d48ec78..a0ee16d4ba 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts @@ -22,7 +22,7 @@ export default async function createPlugin({ templaters.register('cra', craTemplater); const preparers = await Preparers.fromConfig(config, { logger }); - const publishers = await Publishers.fromConfig(config, { logger }); + const publishers = await Publishers.fromConfig(config); const dockerClient = new Docker(); From 9d0733de1f358197e33beca39f245c41ea702d77 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 19 Jan 2021 13:19:14 +0100 Subject: [PATCH 098/209] Cleanup gitlab tests --- .../scaffolder/stages/prepare/gitlab.test.ts | 183 +++++++++--------- 1 file changed, 91 insertions(+), 92 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts index f9e79e6c71..af9d2bac42 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts @@ -27,12 +27,13 @@ import { import { ConfigReader } from '@backstage/config'; import { getVoidLogger, Git } from '@backstage/backend-common'; -const mockEntityWithProtocol = (protocol: string): TemplateEntityV1alpha1 => ({ +const mockTemplate = (): TemplateEntityV1alpha1 => ({ apiVersion: 'backstage.io/v1alpha1', kind: 'Template', metadata: { annotations: { - [LOCATION_ANNOTATION]: `${protocol}:https://gitlab.com/benjdlambert/backstage-graphql-template/-/blob/master/template.yaml`, + [LOCATION_ANNOTATION]: + 'gitlab:https://gitlab.com/benjdlambert/backstage-graphql-template/-/blob/master/template.yaml', }, name: 'graphql-starter', title: 'GraphQL Service', @@ -78,98 +79,96 @@ describe('GitLabPreparer', () => { jest.clearAllMocks(); }); - ['gitlab'].forEach(protocol => { - it(`calls the clone command with the correct arguments for a repository using the ${protocol} protocol`, async () => { - const preparer = new GitlabPreparer(new ConfigReader({})); - mockEntity = mockEntityWithProtocol(protocol); + it(`calls the clone command with the correct arguments for a repository`, async () => { + const preparer = new GitlabPreparer(new ConfigReader({})); + mockEntity = mockTemplate(); - await preparer.prepare(mockEntity, { logger: getVoidLogger() }); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); - expect(mockGitClient.clone).toHaveBeenCalledWith({ - url: 'https://gitlab.com/benjdlambert/backstage-graphql-template', - dir: expect.any(String), - }); - }); - - it(`calls the clone command with the correct arguments if an access token is provided in integrations for a repository using the ${protocol} protocol`, async () => { - const preparer = new GitlabPreparer( - new ConfigReader({ - integrations: { - gitlab: [ - { - host: 'gitlab.com', - token: 'fake-token', - }, - ], - }, - }), - ); - mockEntity = mockEntityWithProtocol(protocol); - - await preparer.prepare(mockEntity, { logger }); - - expect(Git.fromAuth).toHaveBeenCalledWith({ - logger, - username: 'oauth2', - password: 'fake-token', - }); - }); - - it(`calls the clone command with the correct arguments if an access token is provided in scaffolder for a repository using the ${protocol} protocol`, async () => { - const preparer = new GitlabPreparer( - new ConfigReader({ - scaffolder: { - gitlab: { api: { token: 'fake-token' } }, - }, - }), - ); - mockEntity = mockEntityWithProtocol(protocol); - - await preparer.prepare(mockEntity, { logger }); - - expect(Git.fromAuth).toHaveBeenCalledWith({ - logger, - username: 'oauth2', - password: 'fake-token', - }); - }); - - it(`calls the clone command with the correct arguments for a repository when no path is provided using the ${protocol} protocol`, async () => { - const preparer = new GitlabPreparer(new ConfigReader({})); - mockEntity = mockEntityWithProtocol(protocol); - delete mockEntity.spec.path; - - await preparer.prepare(mockEntity, { logger: getVoidLogger() }); - - expect(mockGitClient.clone).toHaveBeenCalledWith({ - url: 'https://gitlab.com/benjdlambert/backstage-graphql-template', - dir: expect.any(String), - }); - }); - - it(`return the temp directory with the path to the folder if it is specified using the ${protocol} protocol`, async () => { - const preparer = new GitlabPreparer(new ConfigReader({})); - mockEntity = mockEntityWithProtocol(protocol); - mockEntity.spec.path = './template/test/1/2/3'; - const response = await preparer.prepare(mockEntity, { - logger: getVoidLogger(), - }); - expect(response.split('\\').join('/')).toMatch( - /\/template\/test\/1\/2\/3$/, - ); - }); - - it('return the working directory with the path to the folder if it is specified', async () => { - const preparer = new GitlabPreparer(new ConfigReader({})); - mockEntity.spec.path = './template/test/1/2/3'; - const response = await preparer.prepare(mockEntity, { - workingDirectory: '/workDir', - logger: getVoidLogger(), - }); - - expect(response.split('\\').join('/')).toMatch( - /\/workDir\/graphql-starter-static\/template\/test\/1\/2\/3$/, - ); + expect(mockGitClient.clone).toHaveBeenCalledWith({ + url: 'https://gitlab.com/benjdlambert/backstage-graphql-template', + dir: expect.any(String), }); }); + + it(`calls the clone command with the correct arguments if an access token is provided in integrations for a repository`, async () => { + const preparer = new GitlabPreparer( + new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'fake-token', + }, + ], + }, + }), + ); + mockEntity = mockTemplate(); + + await preparer.prepare(mockEntity, { logger }); + + expect(Git.fromAuth).toHaveBeenCalledWith({ + logger, + username: 'oauth2', + password: 'fake-token', + }); + }); + + it(`calls the clone command with the correct arguments if an access token is provided in scaffolder for a repository`, async () => { + const preparer = new GitlabPreparer( + new ConfigReader({ + scaffolder: { + gitlab: { api: { token: 'fake-token' } }, + }, + }), + ); + mockEntity = mockTemplate(); + + await preparer.prepare(mockEntity, { logger }); + + expect(Git.fromAuth).toHaveBeenCalledWith({ + logger, + username: 'oauth2', + password: 'fake-token', + }); + }); + + it(`calls the clone command with the correct arguments for a repository when no path is provided`, async () => { + const preparer = new GitlabPreparer(new ConfigReader({})); + mockEntity = mockTemplate(); + delete mockEntity.spec.path; + + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); + + expect(mockGitClient.clone).toHaveBeenCalledWith({ + url: 'https://gitlab.com/benjdlambert/backstage-graphql-template', + dir: expect.any(String), + }); + }); + + it(`return the temp directory with the path to the folder if it is specified`, async () => { + const preparer = new GitlabPreparer(new ConfigReader({})); + mockEntity = mockTemplate(); + mockEntity.spec.path = './template/test/1/2/3'; + const response = await preparer.prepare(mockEntity, { + logger: getVoidLogger(), + }); + expect(response.split('\\').join('/')).toMatch( + /\/template\/test\/1\/2\/3$/, + ); + }); + + it('return the working directory with the path to the folder if it is specified', async () => { + const preparer = new GitlabPreparer(new ConfigReader({})); + mockEntity.spec.path = './template/test/1/2/3'; + const response = await preparer.prepare(mockEntity, { + workingDirectory: '/workDir', + logger: getVoidLogger(), + }); + + expect(response.split('\\').join('/')).toMatch( + /\/workDir\/graphql-starter-static\/template\/test\/1\/2\/3$/, + ); + }); }); From 544139ed16c31a0791428a9d33d5b4b596c9616a Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 19 Jan 2021 15:31:07 +0100 Subject: [PATCH 099/209] Refactor preparers --- .../scaffolder/stages/prepare/azure.test.ts | 35 +-- .../src/scaffolder/stages/prepare/azure.ts | 53 +---- .../stages/prepare/bitbucket.test.ts | 98 ++------ .../scaffolder/stages/prepare/bitbucket.ts | 82 ++----- .../scaffolder/stages/prepare/github.test.ts | 60 ++--- .../src/scaffolder/stages/prepare/github.ts | 40 +--- .../scaffolder/stages/prepare/gitlab.test.ts | 40 +--- .../src/scaffolder/stages/prepare/gitlab.ts | 43 +--- .../stages/prepare/preparers.test.ts | 220 ++++++++++-------- .../scaffolder/stages/prepare/preparers.ts | 99 ++++---- .../src/scaffolder/stages/prepare/types.ts | 4 +- .../scaffolder/stages/publish/azure.test.ts | 74 +----- .../src/scaffolder/stages/publish/azure.ts | 70 ++---- .../scaffolder-backend/src/service/router.ts | 9 +- 14 files changed, 297 insertions(+), 630 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts index d0860ea96c..8dd72c35e9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts @@ -26,7 +26,6 @@ import { LOCATION_ANNOTATION, } from '@backstage/catalog-model'; import { getVoidLogger, Git } from '@backstage/backend-common'; -import { ConfigReader } from '@backstage/config'; describe('AzurePreparer', () => { const mockGitClient = { @@ -80,20 +79,13 @@ describe('AzurePreparer', () => { }; }); + const preparer = AzurePreparer.fromConfig({ + host: 'dev.azure.com', + token: 'fake-azure-token', + }); + // TODO(blam): Here's a test that will fail when the deprecation is complete it('calls the clone command with deprecated token', async () => { - const preparer = new AzurePreparer( - new ConfigReader({ - scaffolder: { - azure: { - api: { - token: 'fake-azure-token', - }, - }, - }, - }), - ); - await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ @@ -104,28 +96,16 @@ describe('AzurePreparer', () => { }); it('calls the clone command with token from integrations config', async () => { - const preparer = new AzurePreparer( - new ConfigReader({ - integrations: { - azure: [ - { host: 'dev.azure.com', token: 'fake-azure-token-integration' }, - ], - }, - }), - ); - await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, - password: 'fake-azure-token-integration', + password: 'fake-azure-token', username: 'notempty', }); }); it('calls the clone command with the correct arguments for a repository', async () => { - const preparer = new AzurePreparer(new ConfigReader({})); - await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ @@ -136,7 +116,6 @@ describe('AzurePreparer', () => { }); it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { - const preparer = new AzurePreparer(new ConfigReader({})); delete mockEntity.spec.path; await preparer.prepare(mockEntity, { logger: getVoidLogger() }); @@ -149,7 +128,6 @@ describe('AzurePreparer', () => { }); it('return the temp directory with the path to the folder if it is specified', async () => { - const preparer = new AzurePreparer(new ConfigReader({})); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { @@ -162,7 +140,6 @@ describe('AzurePreparer', () => { }); it('return the working directory with the path to the folder if it is specified', async () => { - const preparer = new AzurePreparer(new ConfigReader({})); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts index 5064c8ff1e..df127d22e7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts @@ -18,54 +18,26 @@ import fs from 'fs-extra'; import path from 'path'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { parseLocationAnnotation } from '../helpers'; -import { InputError, Git } from '@backstage/backend-common'; +import { Git } from '@backstage/backend-common'; import { PreparerBase, PreparerOptions } from './types'; import parseGitUrl from 'git-url-parse'; -import { Config } from '@backstage/config'; -import { Logger } from 'winston'; -import { - readAzureIntegrationConfigs, - AzureIntegrationConfig, -} from '@backstage/integration'; +import { AzureIntegrationConfig } from '@backstage/integration'; export class AzurePreparer implements PreparerBase { - private readonly integrations: AzureIntegrationConfig[]; - private readonly scaffolderToken: string | undefined; - - static fromConfig(config: Config, { logger }: { logger: Logger }) { - if (config.getOptionalString('scaffolder.azure.api.token')) { - logger.warn( - "DEPRECATION: Using the token format under 'scaffolder.azure.api.token' will not be respected in future releases. Please consider using integrations config instead", - 'Please migrate to using integrations config and specifying tokens under hostnames', - ); - } - - return new AzurePreparer(config); + static fromConfig(config: AzureIntegrationConfig) { + return new AzurePreparer(config.token); } - constructor(config: Config) { - this.integrations = readAzureIntegrationConfigs( - config.getOptionalConfigArray('integrations.azure') ?? [], - ); - - this.scaffolderToken = config.getOptionalString( - 'scaffolder.azure.api.token', - ); - } + constructor(private readonly token?: string) {} async prepare( template: TemplateEntityV1alpha1, opts: PreparerOptions, ): Promise { - const { protocol, location } = parseLocationAnnotation(template); + const { location } = parseLocationAnnotation(template); const workingDirectory = opts.workingDirectory ?? os.tmpdir(); const logger = opts.logger; - if (!['azure', 'url'].includes(protocol)) { - throw new InputError( - `Wrong location protocol: ${protocol}, should be 'url'`, - ); - } const templateId = template.metadata.name; const parsedGitLocation = parseGitUrl(location); @@ -79,13 +51,11 @@ export class AzurePreparer implements PreparerBase { template.spec.path ?? '.', ); - const token = this.getToken(parsedGitLocation.resource); - // Username can be anything but the empty string according to: // https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page#use-a-pat - const git = token + const git = this.token ? Git.fromAuth({ - password: token, + password: this.token, username: 'notempty', logger, }) @@ -98,11 +68,4 @@ export class AzurePreparer implements PreparerBase { return path.resolve(tempDir, templateDirectory); } - - private getToken(host: string): string | undefined { - return ( - this.scaffolderToken || - this.integrations.find(c => c.host === host)?.token - ); - } } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts index 05b64ca777..ff74fffdd7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts @@ -26,7 +26,6 @@ import { LOCATION_ANNOTATION, } from '@backstage/catalog-model'; import { getVoidLogger, Git } from '@backstage/backend-common'; -import { ConfigReader } from '@backstage/config'; describe('BitbucketPreparer', () => { let mockEntity: TemplateEntityV1alpha1; @@ -79,8 +78,13 @@ describe('BitbucketPreparer', () => { }; }); + const preparer = BitbucketPreparer.fromConfig({ + host: 'bitbucket.org', + username: 'fake-user', + appPassword: 'fake-password', + }); + it('calls the clone command with the correct arguments for a repository', async () => { - const preparer = new BitbucketPreparer(new ConfigReader({})); await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://bitbucket.org/backstage-project/backstage-repo', @@ -89,20 +93,11 @@ describe('BitbucketPreparer', () => { }); it('calls the clone command with the correct arguments if an app password is provided for a repository', async () => { - const preparer = new BitbucketPreparer( - new ConfigReader({ - integrations: { - bitbucket: [ - { - host: 'bitbucket.org', - username: 'fake-user', - appPassword: 'fake-password', - }, - ], - }, - }), - ); - + const preparer = BitbucketPreparer.fromConfig({ + host: 'bitbucket.org', + username: 'fake-user', + appPassword: 'fake-password', + }); await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ @@ -113,7 +108,6 @@ describe('BitbucketPreparer', () => { }); it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { - const preparer = new BitbucketPreparer(new ConfigReader({})); delete mockEntity.spec.path; await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ @@ -123,7 +117,6 @@ describe('BitbucketPreparer', () => { }); it('return the temp directory with the path to the folder if it is specified', async () => { - const preparer = new BitbucketPreparer(new ConfigReader({})); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { logger: getVoidLogger(), @@ -134,79 +127,22 @@ describe('BitbucketPreparer', () => { ); }); - it('calls the clone command with deprecated auth method', async () => { - const preparer = new BitbucketPreparer( - new ConfigReader({ - scaffolder: { - bitbucket: { - api: { - username: 'fakeusername', - token: 'faketoken', - }, - }, - }, - }), - ); - - await preparer.prepare(mockEntity, { logger }); - - expect(Git.fromAuth).toHaveBeenCalledWith({ - logger, - username: 'fakeusername', - password: 'faketoken', + it('calls the clone command with with token for auth method', async () => { + const preparer = BitbucketPreparer.fromConfig({ + host: 'bitbucket.org', + token: 'fake-token', }); - }); - - it('calls the clone command with integrations config for auth method', async () => { - const preparer = new BitbucketPreparer( - new ConfigReader({ - integrations: { - bitbucket: [ - { - host: 'bitbucket.org', - username: 'asd3', - token: 'faketoken', - }, - ], - }, - }), - ); await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, - username: 'asd3', - password: 'faketoken', - }); - }); - - it('calls the clone command with integrations config with appPassword for auth method', async () => { - const preparer = new BitbucketPreparer( - new ConfigReader({ - integrations: { - bitbucket: [ - { - host: 'bitbucket.org', - username: 'asd3', - appPassword: 'myapppassword', - }, - ], - }, - }), - ); - - await preparer.prepare(mockEntity, { logger }); - - expect(Git.fromAuth).toHaveBeenCalledWith({ - logger, - username: 'asd3', - password: 'myapppassword', + username: 'x-token-auth', + password: 'fake-token', }); }); it('return the working directory with the path to the folder if it is specified', async () => { - const preparer = new BitbucketPreparer(new ConfigReader({})); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { workingDirectory: '/workDir', diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts index cce0689ac9..5aafb2730f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts @@ -20,44 +20,23 @@ import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { parseLocationAnnotation } from '../helpers'; import { InputError, Git } from '@backstage/backend-common'; import { PreparerBase, PreparerOptions } from './types'; -import { - readBitbucketIntegrationConfigs, - BitbucketIntegrationConfig, -} from '@backstage/integration'; +import { BitbucketIntegrationConfig } from '@backstage/integration'; import parseGitUrl from 'git-url-parse'; -import { Config } from '@backstage/config'; -import { Logger } from 'winston'; export class BitbucketPreparer implements PreparerBase { - private readonly privateToken: string; - private readonly username: string; - private readonly integrations: BitbucketIntegrationConfig[]; - - static fromConfig(config: Config, { logger }: { logger: Logger }) { - const user = config.getOptionalString('scaffolder.bitbucket.api.username'); - const token = config.getOptionalString('scaffolder.bitbucket.api.token'); - const password = config.getOptionalString( - 'scaffolder.bitbucket.api.appPassword', + static fromConfig(config: BitbucketIntegrationConfig) { + return new BitbucketPreparer( + config.username, + config.token, + config.appPassword, ); - - if (user || token || password) { - logger.warn( - "DEPRECATION: Setting credentials under 'scaffolder.bitbucket.api' will not be respected in future releases. Please consider using integrations config instead", - 'Please migrate to using integrations config and specifying tokens under hostnames', - ); - } - return new BitbucketPreparer(config); } - constructor(config: Config) { - this.integrations = readBitbucketIntegrationConfigs( - config.getOptionalConfigArray('integrations.bitbucket') ?? [], - ); - this.username = - config.getOptionalString('scaffolder.bitbucket.api.username') ?? ''; - this.privateToken = - config.getOptionalString('scaffolder.bitbucket.api.token') ?? ''; - } + constructor( + private readonly username?: string, + private readonly token?: string, + private readonly appPassword?: string, + ) {} async prepare( template: TemplateEntityV1alpha1, @@ -88,8 +67,7 @@ export class BitbucketPreparer implements PreparerBase { const checkoutLocation = path.resolve(tempDir, templateDirectory); - const auth = this.getAuth(repo.resource); - + const auth = this.getAuth(); const git = auth ? Git.fromAuth({ ...auth, @@ -105,36 +83,18 @@ export class BitbucketPreparer implements PreparerBase { return checkoutLocation; } - private getAuth( - host: string, - ): { username: string; password: string } | undefined { - if (this.username && this.privateToken) { - return { username: this.username, password: this.privateToken }; + private getAuth(): { username: string; password: string } | undefined { + if (this.username && this.appPassword) { + return { username: this.username, password: this.appPassword }; } - const bitbucketIntegrationConfig = this.integrations.find( - c => c.host === host, - ); - - if (!bitbucketIntegrationConfig) { - return undefined; + if (this.token) { + return { + username: 'x-token-auth', + password: this.token! || this.appPassword!, + }; } - if ( - !bitbucketIntegrationConfig.username || - !( - bitbucketIntegrationConfig.token || - bitbucketIntegrationConfig.appPassword - ) - ) { - return undefined; - } - - return { - username: bitbucketIntegrationConfig.username, - password: - bitbucketIntegrationConfig.token! || - bitbucketIntegrationConfig.appPassword!, - }; + return undefined; } } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts index 68178610b5..b43545f5fc 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts @@ -26,7 +26,6 @@ import { LOCATION_ANNOTATION, } from '@backstage/catalog-model'; import { getVoidLogger, Git } from '@backstage/backend-common'; -import { ConfigReader } from '@backstage/config'; describe('GitHubPreparer', () => { let mockEntity: TemplateEntityV1alpha1; @@ -78,17 +77,13 @@ describe('GitHubPreparer', () => { }, }; }); - it('calls the clone command with the correct arguments for a repository', async () => { - const preparer = new GithubPreparer( - new ConfigReader({ - scaffolder: { - github: { - token: 'fake-token', - }, - }, - }), - ); + const preparer = GithubPreparer.fromConfig({ + host: 'github.com', + token: 'fake-token', + }); + + it('calls the clone command with the correct arguments for a repository', async () => { await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ @@ -96,8 +91,8 @@ describe('GitHubPreparer', () => { dir: expect.any(String), }); }); + it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { - const preparer = new GithubPreparer(new ConfigReader({})); delete mockEntity.spec.path; await preparer.prepare(mockEntity, { logger: getVoidLogger() }); @@ -109,7 +104,10 @@ describe('GitHubPreparer', () => { }); it('return the temp directory with the path to the folder if it is specified', async () => { - const preparer = new GithubPreparer(new ConfigReader({})); + const preparer = GithubPreparer.fromConfig({ + host: 'github.com', + token: 'fake-token', + }); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { logger: getVoidLogger(), @@ -120,7 +118,11 @@ describe('GitHubPreparer', () => { }); it('return the working directory with the path to the folder if it is specified', async () => { - const preparer = new GithubPreparer(new ConfigReader({})); + const preparer = GithubPreparer.fromConfig({ + host: 'github.com', + token: 'fake-token', + }); + mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { workingDirectory: '/workDir', @@ -132,17 +134,7 @@ describe('GitHubPreparer', () => { ); }); - it('calls the clone command with deprecated token', async () => { - const preparer = new GithubPreparer( - new ConfigReader({ - scaffolder: { - github: { - token: 'fake-token', - }, - }, - }), - ); - + it('calls the clone command with token', async () => { await preparer.prepare(mockEntity, { logger }); expect(Git.fromAuth).toHaveBeenCalledWith({ @@ -151,22 +143,4 @@ describe('GitHubPreparer', () => { password: 'x-oauth-basic', }); }); - - it('calls the clone command with token from integrations config', async () => { - const preparer = new GithubPreparer( - new ConfigReader({ - integrations: { - github: [{ host: 'github.com', token: 'fake-me' }], - }, - }), - ); - - await preparer.prepare(mockEntity, { logger }); - - expect(Git.fromAuth).toHaveBeenCalledWith({ - logger, - username: 'fake-me', - password: 'x-oauth-basic', - }); - }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts index 49858adf42..e0d669c211 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts @@ -20,35 +20,15 @@ import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { parseLocationAnnotation } from '../helpers'; import { InputError, Git } from '@backstage/backend-common'; import { PreparerBase, PreparerOptions } from './types'; -import { Logger } from 'winston'; import parseGitUrl from 'git-url-parse'; -import { Config } from '@backstage/config'; -import { - GitHubIntegrationConfig, - readGitHubIntegrationConfigs, -} from '@backstage/integration'; +import { GitHubIntegrationConfig } from '@backstage/integration'; export class GithubPreparer implements PreparerBase { - private readonly integrations: GitHubIntegrationConfig[]; - private readonly scaffolderToken: string | undefined; - - static fromConfig(config: Config, { logger }: { logger: Logger }) { - if (config.getOptionalString('scaffolder.github.token')) { - logger.warn( - "DEPRECATION: Using the token format under 'scaffolder.github.token' will not be respected in future releases. Please consider using integrations config instead", - 'Please migrate to using integrations config and specifying tokens under hostnames', - ); - } - - return new GithubPreparer(config); + static fromConfig(config: GitHubIntegrationConfig) { + return new GithubPreparer(config.token); } - constructor(config: Config) { - this.integrations = readGitHubIntegrationConfigs( - config.getOptionalConfigArray('integrations.github') ?? [], - ); - this.scaffolderToken = config.getOptionalString('scaffolder.github.token'); - } + constructor(private readonly token?: string) {} async prepare( template: TemplateEntityV1alpha1, @@ -78,11 +58,9 @@ export class GithubPreparer implements PreparerBase { const checkoutLocation = path.resolve(tempDir, templateDirectory); - const token = this.getToken(parsedGitLocation.resource); - - const git = token + const git = this.token ? Git.fromAuth({ - username: token, + username: this.token, password: 'x-oauth-basic', logger, }) @@ -95,10 +73,4 @@ export class GithubPreparer implements PreparerBase { return checkoutLocation; } - private getToken(host: string): string | undefined { - return ( - this.scaffolderToken || - this.integrations.find(c => c.host === host)?.token - ); - } } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts index af9d2bac42..b2f1ac7fd9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts @@ -78,9 +78,11 @@ describe('GitLabPreparer', () => { beforeEach(() => { jest.clearAllMocks(); }); - + const preparer = GitlabPreparer.fromConfig({ + host: 'gitlab.com', + token: 'fake-token', + }); it(`calls the clone command with the correct arguments for a repository`, async () => { - const preparer = new GitlabPreparer(new ConfigReader({})); mockEntity = mockTemplate(); await preparer.prepare(mockEntity, { logger: getVoidLogger() }); @@ -92,37 +94,6 @@ describe('GitLabPreparer', () => { }); it(`calls the clone command with the correct arguments if an access token is provided in integrations for a repository`, async () => { - const preparer = new GitlabPreparer( - new ConfigReader({ - integrations: { - gitlab: [ - { - host: 'gitlab.com', - token: 'fake-token', - }, - ], - }, - }), - ); - mockEntity = mockTemplate(); - - await preparer.prepare(mockEntity, { logger }); - - expect(Git.fromAuth).toHaveBeenCalledWith({ - logger, - username: 'oauth2', - password: 'fake-token', - }); - }); - - it(`calls the clone command with the correct arguments if an access token is provided in scaffolder for a repository`, async () => { - const preparer = new GitlabPreparer( - new ConfigReader({ - scaffolder: { - gitlab: { api: { token: 'fake-token' } }, - }, - }), - ); mockEntity = mockTemplate(); await preparer.prepare(mockEntity, { logger }); @@ -135,7 +106,6 @@ describe('GitLabPreparer', () => { }); it(`calls the clone command with the correct arguments for a repository when no path is provided`, async () => { - const preparer = new GitlabPreparer(new ConfigReader({})); mockEntity = mockTemplate(); delete mockEntity.spec.path; @@ -148,7 +118,6 @@ describe('GitLabPreparer', () => { }); it(`return the temp directory with the path to the folder if it is specified`, async () => { - const preparer = new GitlabPreparer(new ConfigReader({})); mockEntity = mockTemplate(); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { @@ -160,7 +129,6 @@ describe('GitLabPreparer', () => { }); it('return the working directory with the path to the folder if it is specified', async () => { - const preparer = new GitlabPreparer(new ConfigReader({})); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { workingDirectory: '/workDir', diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts index 49168deeff..0bdecab794 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts @@ -15,43 +15,20 @@ */ import { InputError, Git } from '@backstage/backend-common'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -import { Config } from '@backstage/config'; -import { - GitLabIntegrationConfig, - readGitLabIntegrationConfigs, -} from '@backstage/integration'; +import { GitLabIntegrationConfig } from '@backstage/integration'; import fs from 'fs-extra'; import parseGitUrl from 'git-url-parse'; import os from 'os'; import path from 'path'; import { parseLocationAnnotation } from '../helpers'; import { PreparerBase, PreparerOptions } from './types'; -import { Logger } from 'winston'; export class GitlabPreparer implements PreparerBase { - private readonly integrations: GitLabIntegrationConfig[]; - private readonly scaffolderToken: string | undefined; - - static fromConfig(config: Config, { logger }: { logger: Logger }) { - if (config.getOptionalString('scaffolder.gitlab.api.token')) { - logger.warn( - "DEPRECATION: Using the token format under 'scaffolder.gitlab.token' will not be respected in future releases. Please consider using integrations config instead", - 'Please migrate to using integrations config and specifying tokens under hostnames', - ); - } - - return new GitlabPreparer(config); + static fromConfig(config: GitLabIntegrationConfig) { + return new GitlabPreparer(config.token); } - constructor(config: Config) { - this.integrations = readGitLabIntegrationConfigs( - config.getOptionalConfigArray('integrations.gitlab') ?? [], - ); - - this.scaffolderToken = config.getOptionalString( - 'scaffolder.gitlab.api.token', - ); - } + constructor(private readonly token?: string) {} async prepare( template: TemplateEntityV1alpha1, @@ -79,10 +56,9 @@ export class GitlabPreparer implements PreparerBase { template.spec.path ?? '.', ); - const token = this.getToken(parsedGitLocation.resource); - const git = token + const git = this.token ? Git.fromAuth({ - password: token, + password: this.token, username: 'oauth2', logger, }) @@ -95,11 +71,4 @@ export class GitlabPreparer implements PreparerBase { return path.resolve(tempDir, templateDirectory); } - - private getToken(host: string): string | undefined { - return ( - this.scaffolderToken || - this.integrations.find(c => c.host === host)?.token - ); - } } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts index 7c2d7e0a47..db6160c276 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts @@ -16,107 +16,143 @@ import { Preparers } from '.'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { FilePreparer } from './file'; +import { GithubPreparer } from './github'; +import { ConfigReader } from '@backstage/config'; describe('Preparers', () => { - const mockTemplate: TemplateEntityV1alpha1 = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Template', - metadata: { - annotations: { - 'backstage.io/managed-by-location': - 'file:/Users/bingo/backstage/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml', - }, - name: 'react-ssr-template', - title: 'React SSR Template', - description: - 'Next.js application skeleton for creating isomorphic web applications.', - uid: '7357f4c5-aa58-4a1e-9670-18931eef771f', - etag: 'YWUxZWQyY2EtZDkxMC00MDM0LWI0ODAtMDgwMWY0YzdlMWIw', - generation: 1, - }, - spec: { - templater: 'cookiecutter', - path: '.', - type: 'website', - schema: { - $schema: 'http://json-schema.org/draft-07/schema#', - required: ['storePath', 'owner'], - properties: { - owner: { - type: 'string', - title: 'Owner', - description: 'Who is going to own this component', - }, - storePath: { - type: 'string', - title: 'Store path', - description: 'GitHub store path in org/repo format', - }, - }, - }, - }, - }; - it('should throw an error when the preparer for the source location is not registered', () => { - const preparers = new Preparers(); + // const mockTemplate: TemplateEntityV1alpha1 = { + // apiVersion: 'backstage.io/v1alpha1', + // kind: 'Template', + // metadata: { + // annotations: { + // 'backstage.io/managed-by-location': + // 'file:/Users/bingo/backstage/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml', + // }, + // name: 'react-ssr-template', + // title: 'React SSR Template', + // description: + // 'Next.js application skeleton for creating isomorphic web applications.', + // uid: '7357f4c5-aa58-4a1e-9670-18931eef771f', + // etag: 'YWUxZWQyY2EtZDkxMC00MDM0LWI0ODAtMDgwMWY0YzdlMWIw', + // generation: 1, + // }, + // spec: { + // templater: 'cookiecutter', + // path: '.', + // type: 'website', + // schema: { + // $schema: 'http://json-schema.org/draft-07/schema#', + // required: ['storePath', 'owner'], + // properties: { + // owner: { + // type: 'string', + // title: 'Owner', + // description: 'Who is going to own this component', + // }, + // storePath: { + // type: 'string', + // title: 'Store path', + // description: 'GitHub store path in org/repo format', + // }, + // }, + // }, + // }, + // }; + // it('should throw an error when the preparer for the source location is not registered', () => { + // const preparers = new Preparers(); - expect(() => preparers.get(mockTemplate)).toThrow( - expect.objectContaining({ - message: 'No preparer registered for type: "file"', + // expect(() => preparers.get(mockTemplate)).toThrow( + // expect.objectContaining({ + // message: 'No preparer registered for type: "file"', + // }), + // ); + // }); + // it('should return the correct preparer when the source matches', () => { + // const preparers = new Preparers(); + // const preparer = new FilePreparer(); + + // preparers.register('file', preparer); + + // expect(preparers.get(mockTemplate)).toBe(preparer); + // }); + + // it('should throw an error if the metadata tag does not exist in the entity', () => { + // const brokenTemplate: TemplateEntityV1alpha1 = { + // apiVersion: 'backstage.io/v1alpha1', + // kind: 'Template', + // metadata: { + // annotations: {}, + // name: 'react-ssr-template', + // title: 'React SSR Template', + // description: + // 'Next.js application skeleton for creating isomorphic web applications.', + // uid: '7357f4c5-aa58-4a1e-9670-18931eef771f', + // etag: 'YWUxZWQyY2EtZDkxMC00MDM0LWI0ODAtMDgwMWY0YzdlMWIw', + // generation: 1, + // }, + // spec: { + // type: 'website', + // templater: 'cookiecutter', + // path: '.', + // schema: { + // $schema: 'http://json-schema.org/draft-07/schema#', + // required: ['storePath', 'owner'], + // properties: { + // owner: { + // type: 'string', + // title: 'Owner', + // description: 'Who is going to own this component', + // }, + // storePath: { + // type: 'string', + // title: 'Store path', + // description: 'GitHub store path in org/repo format', + // }, + // }, + // }, + // }, + // }; + + // const preparers = new Preparers(); + + // expect(() => preparers.get(brokenTemplate)).toThrow( + // expect.objectContaining({ + // message: expect.stringContaining('No location annotation provided'), + // }), + // ); + // }); + + it('should return the correct preparer based on the hostname', async () => { + const preparer = new GithubPreparer( + new ConfigReader({ + host: 'github.com', + apiBaseUrl: 'lols', + token: 'something else yo', }), ); - }); - it('should return the correct preparer when the source matches', () => { - const preparers = new Preparers(); - const preparer = new FilePreparer(); - - preparers.register('file', preparer); - - expect(preparers.get(mockTemplate)).toBe(preparer); - }); - - it('should throw an error if the metadata tag does not exist in the entity', () => { - const brokenTemplate: TemplateEntityV1alpha1 = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Template', - metadata: { - annotations: {}, - name: 'react-ssr-template', - title: 'React SSR Template', - description: - 'Next.js application skeleton for creating isomorphic web applications.', - uid: '7357f4c5-aa58-4a1e-9670-18931eef771f', - etag: 'YWUxZWQyY2EtZDkxMC00MDM0LWI0ODAtMDgwMWY0YzdlMWIw', - generation: 1, - }, - spec: { - type: 'website', - templater: 'cookiecutter', - path: '.', - schema: { - $schema: 'http://json-schema.org/draft-07/schema#', - required: ['storePath', 'owner'], - properties: { - owner: { - type: 'string', - title: 'Owner', - description: 'Who is going to own this component', - }, - storePath: { - type: 'string', - title: 'Store path', - description: 'GitHub store path in org/repo format', - }, - }, - }, - }, - }; const preparers = new Preparers(); + preparers.register('github.com', preparer); - expect(() => preparers.get(brokenTemplate)).toThrow( - expect.objectContaining({ - message: expect.stringContaining('No location annotation provided'), + expect( + preparers.get('https://github.com/please/find/me/something/from/github'), + ).toBe(preparer); + }); + + it('should throw an error if there is nothing that will match the url provided', async () => { + const preparer = new GithubPreparer( + new ConfigReader({ + host: 'github.com', + apiBaseUrl: 'lols', + token: 'something else yo', }), ); + + const preparers = new Preparers(); + preparers.register('github.com', preparer); + + expect(() => preparers.get('https://404.com')).toThrow( + `Unable to find a preparer for URL: https://404.com. Please make sure to register this host under an integration in app-config`, + ); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts index 94d2706cd8..b1d1edf00b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts @@ -15,79 +15,74 @@ */ import { Config } from '@backstage/config'; -import { Logger } from 'winston'; import { PreparerBase, PreparerBuilder } from './types'; -import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -import { - DeprecatedLocationTypeDetector, - makeDeprecatedLocationTypeDetector, - parseLocationAnnotation, -} from '../helpers'; -import { RemoteProtocol } from '../types'; -import { FilePreparer } from './file'; + import { GitlabPreparer } from './gitlab'; import { AzurePreparer } from './azure'; import { GithubPreparer } from './github'; import { BitbucketPreparer } from './bitbucket'; +import { + readAzureIntegrationConfigs, + readBitbucketIntegrationConfigs, + readGitHubIntegrationConfigs, + readGitLabIntegrationConfigs, + ScmIntegrations, +} from '@backstage/integration'; export class Preparers implements PreparerBuilder { - private preparerMap = new Map(); + private preparerMap = new Map(); - constructor(private readonly typeDetector?: DeprecatedLocationTypeDetector) {} - - register(protocol: RemoteProtocol, preparer: PreparerBase) { - this.preparerMap.set(protocol, preparer); + register(host: string, preparer: PreparerBase) { + this.preparerMap.set(host, preparer); } - get(template: TemplateEntityV1alpha1): PreparerBase { - const { protocol, location } = parseLocationAnnotation(template); - - const preparer = this.preparerMap.get(protocol); - + get(url: string): PreparerBase { + const preparer = this.preparerMap.get(new URL(url).host); if (!preparer) { - if ((protocol as string) === 'url') { - const type = this.typeDetector?.(location); - const detected = type && this.preparerMap.get(type as RemoteProtocol); - if (detected) { - return detected; - } - if (type) { - throw new Error( - `No preparer configuration available for type '${type}' with url "${location}". ` + - "Make sure you've added appropriate configuration in the 'scaffolder' configuration section", - ); - } else { - throw new Error( - `Failed to detect preparer type. Unable to determine integration type for location "${location}". ` + - "Please add appropriate configuration to the 'integrations' configuration section", - ); - } - } - throw new Error(`No preparer registered for type: "${protocol}"`); + throw new Error( + `Unable to find a preparer for URL: ${url}. Please make sure to register this host under an integration in app-config`, + ); } - return preparer; } static async fromConfig( config: Config, - { logger }: { logger: Logger }, + // { logger }: { logger: Logger }, ): Promise { - const typeDetector = makeDeprecatedLocationTypeDetector(config); + // TODO(blam/jhaals) + // iterate through the integration configs for each provider, and create the preparer + // register the preparer to hostname. + // if no preparer is returned for the hostname fromConfig, use the backup token credentials from scaffolder block and warn + const preparers = new Preparers(); + const scm = ScmIntegrations.fromConfig(config); + for (const integration of scm.azure.list()) { + preparers.register( + integration.config.host, + AzurePreparer.fromConfig(integration.config), + ); + } - const preparers = new Preparers(typeDetector); + for (const integration of scm.github.list()) { + preparers.register( + integration.config.host, + GithubPreparer.fromConfig(integration.config), + ); + } - const filePreparer = new FilePreparer(); - const gitlabPreparer = GitlabPreparer.fromConfig(config, { logger }); - const azurePreparer = AzurePreparer.fromConfig(config, { logger }); - const githubPreparer = GithubPreparer.fromConfig(config, { logger }); - const bitbucketPreparer = BitbucketPreparer.fromConfig(config, { logger }); + for (const integration of scm.gitlab.list()) { + preparers.register( + integration.config.host, + GitlabPreparer.fromConfig(integration), + ); + } - preparers.register('file', filePreparer); - preparers.register('gitlab', gitlabPreparer); - preparers.register('azure', azurePreparer); - preparers.register('github', githubPreparer); - preparers.register('bitbucket', bitbucketPreparer); + for (const integration of scm.bitbucket.list()) { + preparers.register( + integration.config.host, + BitbucketPreparer.fromConfig(integration.config), + ); + } return preparers; } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts index 2974b833a2..8ee78eab3c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts @@ -35,6 +35,6 @@ export interface PreparerBase { } export type PreparerBuilder = { - register(protocol: RemoteProtocol, preparer: PreparerBase): void; - get(template: TemplateEntityV1alpha1): PreparerBase; + register(host: string, preparer: PreparerBase): void; + get(url: string): PreparerBase; }; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts index 2784a1f9bb..0251b3d983 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts @@ -24,7 +24,6 @@ import { AzurePublisher } from './azure'; import { WebApi } from 'azure-devops-node-api'; import * as helpers from './helpers'; import { getVoidLogger } from '@backstage/backend-common'; -import { ConfigReader } from '@backstage/config'; describe('Azure Publisher', () => { const logger = getVoidLogger(); @@ -40,23 +39,16 @@ describe('Azure Publisher', () => { ((WebApi as unknown) as jest.Mock).mockImplementation(() => mockGitApi); - const publisher = new AzurePublisher( - new ConfigReader({ - scaffolder: { - azure: { - api: { - baseUrl: 'https://dev.azure.com/myorg', - token: 'fake-azure-token', - }, - }, - }, - }), - ); + const publisher = await AzurePublisher.fromConfig({ + host: 'dev.azure.com', + token: 'fake-azure-token', + }); + mockGitClient.createRepository.mockResolvedValue({ remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', } as { remoteUrl: string }); - const result = await publisher.publish({ + const result = await publisher!.publish({ values: { storePath: 'project/repo', owner: 'bob', @@ -83,59 +75,5 @@ describe('Azure Publisher', () => { logger, }); }); - - it('should use azure-devops-node-api with integrations config', async () => { - const mockGitClient = { - createRepository: jest.fn(), - }; - const mockGitApi = { - getGitApi: jest.fn().mockReturnValue(mockGitClient), - }; - - ((WebApi as unknown) as jest.Mock).mockImplementation(() => mockGitApi); - - const publisher = new AzurePublisher( - new ConfigReader({ - integrations: { - azure: [ - { - host: 'dev.azure.com', - token: 'fake-azure-token', - }, - ], - }, - }), - ); - mockGitClient.createRepository.mockResolvedValue({ - remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', - } as { remoteUrl: string }); - - const result = await publisher.publish({ - values: { - storePath: 'https://dev.azure.com/organization/project/_git/repo', - owner: 'bob', - }, - directory: '/tmp/test', - logger, - }); - - expect(result).toEqual({ - remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', - catalogInfoUrl: - 'https://dev.azure.com/organization/project/_git/repo?path=%2Fcatalog-info.yaml', - }); - expect(mockGitClient.createRepository).toHaveBeenCalledWith( - { - name: 'repo', - }, - 'project', - ); - expect(helpers.initRepoAndPush).toHaveBeenCalledWith({ - dir: '/tmp/test', - remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', - auth: { username: 'notempty', password: 'fake-azure-token' }, - logger, - }); - }); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts index d3f900d706..6d2aeea305 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts @@ -17,53 +17,35 @@ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; import { IGitApi } from 'azure-devops-node-api/GitApi'; import { GitRepositoryCreateOptions } from 'azure-devops-node-api/interfaces/GitInterfaces'; -import { Config } from '@backstage/config'; import { initRepoAndPush } from './helpers'; -import { - AzureIntegrationConfig, - readAzureIntegrationConfigs, -} from '@backstage/integration'; +import { AzureIntegrationConfig } from '@backstage/integration'; import gitUrlParse from 'git-url-parse'; import { getPersonalAccessTokenHandler, WebApi } from 'azure-devops-node-api'; export class AzurePublisher implements PublisherBase { - private readonly integrations: AzureIntegrationConfig[]; - private readonly apiBaseUrl?: string; - private readonly token?: string; - - static fromConfig(config: Config) { - return new AzurePublisher(config); + static async fromConfig(config: AzureIntegrationConfig) { + if (!config.token) { + return undefined; + } + const authHandler = getPersonalAccessTokenHandler(config.token); + const webApi = new WebApi(config.host, authHandler); + const azureClient = await webApi.getGitApi(); + return new AzurePublisher(config.token, azureClient); } - constructor(config: Config) { - this.integrations = readAzureIntegrationConfigs( - config.getOptionalConfigArray('integrations.azure') ?? [], - ); + constructor( + private readonly token: string, + private readonly azureClient: IGitApi, + ) {} - this.token = config.getOptionalString('scaffolder.azure.api.token'); - this.apiBaseUrl = config.getOptionalString('scaffolder.azure.api.baseUrl'); - } async publish({ values, directory, logger, }: PublisherOptions): Promise { - const { resource: host, owner, name } = gitUrlParse(values.storePath); + const { owner, name } = gitUrlParse(values.storePath); - const token = this.getToken(host); - if (!token) { - throw new Error('No token provided to create the remote repository'); - } - const baseUrl = this.getBaseUrl(host); - if (!baseUrl) { - throw new Error('No baseUrl provided to create the remote repository'); - } - - const authHandler = getPersonalAccessTokenHandler(token); - const webApi = new WebApi(baseUrl, authHandler); - const azureClient = await webApi.getGitApi(); - - const remoteUrl = await this.createRemote(azureClient, { + const remoteUrl = await this.createRemote({ project: owner, name, }); @@ -75,7 +57,7 @@ export class AzurePublisher implements PublisherBase { remoteUrl, auth: { username: 'notempty', - password: token, + password: this.token, }, logger, }); @@ -83,24 +65,14 @@ export class AzurePublisher implements PublisherBase { return { remoteUrl, catalogInfoUrl }; } - private async createRemote( - client: IGitApi, - opts: { name: string; project: string }, - ) { + private async createRemote(opts: { name: string; project: string }) { const { name, project } = opts; const createOptions: GitRepositoryCreateOptions = { name }; - const repo = await client.createRepository(createOptions, project); + const repo = await this.azureClient.createRepository( + createOptions, + project, + ); return repo.remoteUrl || ''; } - - private getToken(host: string): string | undefined { - return this.token || this.integrations.find(c => c.host === host)?.token; - } - - private getBaseUrl(host: string): string | undefined { - return ( - this.apiBaseUrl || this.integrations.find(c => c.host === host)?.host - ); - } } diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 1ca82d4736..fd051b257c 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -27,6 +27,7 @@ import { StageContext, TemplaterBuilder, PublisherBuilder, + parseLocationAnnotation, } from '../scaffolder'; import { CatalogEntityClient } from '../lib/catalog'; import { validate, ValidatorResult } from 'jsonschema'; @@ -129,7 +130,13 @@ export async function createRouter( { name: 'Prepare the skeleton', handler: async ctx => { - const preparer = preparers.get(ctx.entity); + const { protocol, location: pullPath } = parseLocationAnnotation( + ctx.entity, + ); + + const preparer = + protcol === file ? new FilePreparer() : preparers.get(pullPath); + const skeletonDir = await preparer.prepare(ctx.entity, { logger: ctx.logger, workingDirectory, From 450686ca2734ea2b46bfb26badc0a9596b4a4566 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 19 Jan 2021 15:32:09 +0100 Subject: [PATCH 100/209] Remove duplicated tests --- .../stages/prepare/preparers.test.ts | 102 ------------------ 1 file changed, 102 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts index db6160c276..7023f292f3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts @@ -20,108 +20,6 @@ import { GithubPreparer } from './github'; import { ConfigReader } from '@backstage/config'; describe('Preparers', () => { - // const mockTemplate: TemplateEntityV1alpha1 = { - // apiVersion: 'backstage.io/v1alpha1', - // kind: 'Template', - // metadata: { - // annotations: { - // 'backstage.io/managed-by-location': - // 'file:/Users/bingo/backstage/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml', - // }, - // name: 'react-ssr-template', - // title: 'React SSR Template', - // description: - // 'Next.js application skeleton for creating isomorphic web applications.', - // uid: '7357f4c5-aa58-4a1e-9670-18931eef771f', - // etag: 'YWUxZWQyY2EtZDkxMC00MDM0LWI0ODAtMDgwMWY0YzdlMWIw', - // generation: 1, - // }, - // spec: { - // templater: 'cookiecutter', - // path: '.', - // type: 'website', - // schema: { - // $schema: 'http://json-schema.org/draft-07/schema#', - // required: ['storePath', 'owner'], - // properties: { - // owner: { - // type: 'string', - // title: 'Owner', - // description: 'Who is going to own this component', - // }, - // storePath: { - // type: 'string', - // title: 'Store path', - // description: 'GitHub store path in org/repo format', - // }, - // }, - // }, - // }, - // }; - // it('should throw an error when the preparer for the source location is not registered', () => { - // const preparers = new Preparers(); - - // expect(() => preparers.get(mockTemplate)).toThrow( - // expect.objectContaining({ - // message: 'No preparer registered for type: "file"', - // }), - // ); - // }); - // it('should return the correct preparer when the source matches', () => { - // const preparers = new Preparers(); - // const preparer = new FilePreparer(); - - // preparers.register('file', preparer); - - // expect(preparers.get(mockTemplate)).toBe(preparer); - // }); - - // it('should throw an error if the metadata tag does not exist in the entity', () => { - // const brokenTemplate: TemplateEntityV1alpha1 = { - // apiVersion: 'backstage.io/v1alpha1', - // kind: 'Template', - // metadata: { - // annotations: {}, - // name: 'react-ssr-template', - // title: 'React SSR Template', - // description: - // 'Next.js application skeleton for creating isomorphic web applications.', - // uid: '7357f4c5-aa58-4a1e-9670-18931eef771f', - // etag: 'YWUxZWQyY2EtZDkxMC00MDM0LWI0ODAtMDgwMWY0YzdlMWIw', - // generation: 1, - // }, - // spec: { - // type: 'website', - // templater: 'cookiecutter', - // path: '.', - // schema: { - // $schema: 'http://json-schema.org/draft-07/schema#', - // required: ['storePath', 'owner'], - // properties: { - // owner: { - // type: 'string', - // title: 'Owner', - // description: 'Who is going to own this component', - // }, - // storePath: { - // type: 'string', - // title: 'Store path', - // description: 'GitHub store path in org/repo format', - // }, - // }, - // }, - // }, - // }; - - // const preparers = new Preparers(); - - // expect(() => preparers.get(brokenTemplate)).toThrow( - // expect.objectContaining({ - // message: expect.stringContaining('No location annotation provided'), - // }), - // ); - // }); - it('should return the correct preparer based on the hostname', async () => { const preparer = new GithubPreparer( new ConfigReader({ From ff59698e4e6910739eab7d42150d27bd8d144402 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 19 Jan 2021 15:33:39 +0100 Subject: [PATCH 101/209] Remove old comments --- .../scaffolder/stages/prepare/preparers.ts | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts index b1d1edf00b..9a209f4860 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.ts @@ -21,13 +21,7 @@ import { GitlabPreparer } from './gitlab'; import { AzurePreparer } from './azure'; import { GithubPreparer } from './github'; import { BitbucketPreparer } from './bitbucket'; -import { - readAzureIntegrationConfigs, - readBitbucketIntegrationConfigs, - readGitHubIntegrationConfigs, - readGitLabIntegrationConfigs, - ScmIntegrations, -} from '@backstage/integration'; +import { ScmIntegrations } from '@backstage/integration'; export class Preparers implements PreparerBuilder { private preparerMap = new Map(); @@ -46,14 +40,7 @@ export class Preparers implements PreparerBuilder { return preparer; } - static async fromConfig( - config: Config, - // { logger }: { logger: Logger }, - ): Promise { - // TODO(blam/jhaals) - // iterate through the integration configs for each provider, and create the preparer - // register the preparer to hostname. - // if no preparer is returned for the hostname fromConfig, use the backup token credentials from scaffolder block and warn + static async fromConfig(config: Config): Promise { const preparers = new Preparers(); const scm = ScmIntegrations.fromConfig(config); for (const integration of scm.azure.list()) { @@ -73,7 +60,7 @@ export class Preparers implements PreparerBuilder { for (const integration of scm.gitlab.list()) { preparers.register( integration.config.host, - GitlabPreparer.fromConfig(integration), + GitlabPreparer.fromConfig(integration.config), ); } From def2307f3367d2143b31ca45dd8bfc15729709b8 Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Tue, 19 Jan 2021 14:05:52 +0100 Subject: [PATCH 102/209] Add a managed-by-origin-location annotation --- .changeset/angry-flowers-yawn.md | 29 +++++++++ .../well-known-annotations.md | 17 +++++ .../catalog-model/src/location/annotation.ts | 2 + packages/catalog-model/src/location/index.ts | 2 +- .../src/ingestion/LocationReaders.ts | 20 +++--- .../AnnotateLocationEntityProcessor.test.ts | 62 +++++++++++++++++++ .../AnnotateLocationEntityProcessor.ts | 14 ++++- .../src/ingestion/processors/types.ts | 4 ++ 8 files changed, 139 insertions(+), 11 deletions(-) create mode 100644 .changeset/angry-flowers-yawn.md create mode 100644 plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.test.ts diff --git a/.changeset/angry-flowers-yawn.md b/.changeset/angry-flowers-yawn.md new file mode 100644 index 0000000000..8f478b1eab --- /dev/null +++ b/.changeset/angry-flowers-yawn.md @@ -0,0 +1,29 @@ +--- +'@backstage/catalog-model': patch +'@backstage/plugin-catalog-backend': patch +--- + +Adds a `backstage.io/managed-by-origin-location` annotation to all entities. It links to the +location that was registered to the catalog and which emitted this entity. It has a different +semantic than the existing `backstage.io/managed-by-location` annotation, which tells the direct +parent location that created this entity. + +Consider this example: The Backstage operator adds a location of type `github-org` in the +`app-config.yaml`. This setting will be added to a `bootstrap:boostrap` location. The processor +discovers the entities in the following branch +`Location bootstrap:bootstrap -> Location github-org:… -> User xyz`. The user `xyz` will be: + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: xyz + annotations: + # This entity was added by the 'github-org:…' location + backstage.io/managed-by-location: github-org:… + # The entity was added because the 'bootstrap:boostrap' was added to the catalog + backstage.io/managed-by-origin-location: bootstrap:bootstrap + # ... +spec: + # ... +``` diff --git a/docs/features/software-catalog/well-known-annotations.md b/docs/features/software-catalog/well-known-annotations.md index 3627c74b9a..e3f9e86fae 100644 --- a/docs/features/software-catalog/well-known-annotations.md +++ b/docs/features/software-catalog/well-known-annotations.md @@ -40,6 +40,23 @@ expecting a two-item array out of it. The format of the target part is type-dependent and could conceivably even be an empty string, but the separator colon is always present. +### backstage.io/managed-by-origin-location + +```yaml +# Example: +metadata: + annotations: + backstage.io/managed-by-origin-location: github:http://github.com/backstage/backstage/catalog-info.yaml +``` + +The value of this annotation is a location reference string (see above). It +points to the location, which registration lead to the creation of the entity. +In most cases, the `backstage.io/managed-by-location` and +`backstage.io/managed-by-origin-location` will be equal. It will be different if +the original location delegates to another location. A common case is, that a +location is registered via the `bootstrap:boostrap` which means that is part of +the `app-config.yml` of a backstage installation. + ### backstage.io/techdocs-ref ```yaml diff --git a/packages/catalog-model/src/location/annotation.ts b/packages/catalog-model/src/location/annotation.ts index 371d095685..93f2fabea4 100644 --- a/packages/catalog-model/src/location/annotation.ts +++ b/packages/catalog-model/src/location/annotation.ts @@ -15,3 +15,5 @@ */ export const LOCATION_ANNOTATION = 'backstage.io/managed-by-location'; +export const ORIGIN_LOCATION_ANNOTATION = + 'backstage.io/managed-by-origin-location'; diff --git a/packages/catalog-model/src/location/index.ts b/packages/catalog-model/src/location/index.ts index ce64b988a6..8fd516120a 100644 --- a/packages/catalog-model/src/location/index.ts +++ b/packages/catalog-model/src/location/index.ts @@ -20,4 +20,4 @@ export { locationSpecSchema, analyzeLocationSchema, } from './validation'; -export { LOCATION_ANNOTATION } from './annotation'; +export { LOCATION_ANNOTATION, ORIGIN_LOCATION_ANNOTATION } from './annotation'; diff --git a/plugins/catalog-backend/src/ingestion/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/LocationReaders.ts index a9248bb3fb..cb703b8e15 100644 --- a/plugins/catalog-backend/src/ingestion/LocationReaders.ts +++ b/plugins/catalog-backend/src/ingestion/LocationReaders.ts @@ -78,13 +78,17 @@ export class LocationReaders implements LocationReader { if (rulesEnforcer.isAllowed(item.entity, item.location)) { const relations = Array(); - const entity = await this.handleEntity(item, emitResult => { - if (emitResult.type === 'relation') { - relations.push(emitResult.relation); - return; - } - emit(emitResult); - }); + const entity = await this.handleEntity( + item, + emitResult => { + if (emitResult.type === 'relation') { + relations.push(emitResult.relation); + return; + } + emit(emitResult); + }, + location, + ); if (entity) { output.entities.push({ @@ -165,6 +169,7 @@ export class LocationReaders implements LocationReader { private async handleEntity( item: CatalogProcessorEntityResult, emit: CatalogProcessorEmit, + originLocation: LocationSpec, ): Promise { const { processors, logger } = this.options; @@ -185,6 +190,7 @@ export class LocationReaders implements LocationReader { current, item.location, emit, + originLocation, ); } catch (e) { const message = `Processor ${processor.constructor.name} threw an error while preprocessing entity ${kind}:${namespace}/${name} at ${item.location.type} ${item.location.target}, ${e}`; diff --git a/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.test.ts new file mode 100644 index 0000000000..cfd98e9e6a --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.test.ts @@ -0,0 +1,62 @@ +/* + * 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 { Entity, LocationSpec } from '@backstage/catalog-model'; +import { AnnotateLocationEntityProcessor } from './AnnotateLocationEntityProcessor'; + +describe('AnnotateLocationEntityProcessor', () => { + describe('preProcessEntity', () => { + it('adds annotations', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component', + }, + }; + + const location: LocationSpec = { + type: 'url', + target: 'my-location', + }; + const originLocation: LocationSpec = { + type: 'url', + target: 'my-origin-location', + }; + + const processor = new AnnotateLocationEntityProcessor(); + + expect( + await processor.preProcessEntity( + entity, + location, + () => {}, + originLocation, + ), + ).toEqual({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component', + annotations: { + 'backstage.io/managed-by-location': 'url:my-location', + 'backstage.io/managed-by-origin-location': 'url:my-origin-location', + }, + }, + }); + }); + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts index ea8afcddc5..b40378226a 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts @@ -14,20 +14,28 @@ * limitations under the License. */ -import { Entity, LocationSpec } from '@backstage/catalog-model'; +import { + Entity, + LOCATION_ANNOTATION, + LocationSpec, + ORIGIN_LOCATION_ANNOTATION, +} from '@backstage/catalog-model'; import lodash from 'lodash'; -import { CatalogProcessor } from './types'; +import { CatalogProcessor, CatalogProcessorEmit } from './types'; export class AnnotateLocationEntityProcessor implements CatalogProcessor { async preProcessEntity( entity: Entity, location: LocationSpec, + _: CatalogProcessorEmit, + originLocation: LocationSpec, ): Promise { return lodash.merge( { metadata: { annotations: { - 'backstage.io/managed-by-location': `${location.type}:${location.target}`, + [LOCATION_ANNOTATION]: `${location.type}:${location.target}`, + [ORIGIN_LOCATION_ANNOTATION]: `${originLocation.type}:${originLocation.target}`, }, }, }, diff --git a/plugins/catalog-backend/src/ingestion/processors/types.ts b/plugins/catalog-backend/src/ingestion/processors/types.ts index 5da3510f7a..1bf30567bc 100644 --- a/plugins/catalog-backend/src/ingestion/processors/types.ts +++ b/plugins/catalog-backend/src/ingestion/processors/types.ts @@ -46,12 +46,16 @@ export type CatalogProcessor = { * @param entity The (possibly partial) entity to process * @param location The location that the entity came from * @param emit A sink for auxiliary items resulting from the processing + * @param originLocation The location that the entity originally came from. + * While location resolves to the direct parent location, originLocation + * tells which location was used to start the ingestion loop. * @returns The same entity or a modified version of it */ preProcessEntity?( entity: Entity, location: LocationSpec, emit: CatalogProcessorEmit, + originLocation: LocationSpec, ): Promise; /** From 593632f0787c008d7ff8089bf7b05049a1cb44cc Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Tue, 19 Jan 2021 15:51:40 +0100 Subject: [PATCH 103/209] Block deleting entities from the bootstrap location in the unregister dialog and list the correct delete preview --- .changeset/cool-horses-applaud.md | 6 ++ .../UnregisterEntityDialog.tsx | 56 +++++++++++++++---- 2 files changed, 51 insertions(+), 11 deletions(-) create mode 100644 .changeset/cool-horses-applaud.md diff --git a/.changeset/cool-horses-applaud.md b/.changeset/cool-horses-applaud.md new file mode 100644 index 0000000000..f934634225 --- /dev/null +++ b/.changeset/cool-horses-applaud.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Derive the list of to-deleted entities in the `UnregisterEntityDialog` from the `backstage.io/managed-by-origin-location` annotation. +The dialog also rejects deleting entities that are created by the `bootstrap:bootstrap` location. diff --git a/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx b/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx index ce6e63da1b..0c52b24d90 100644 --- a/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx +++ b/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity, LOCATION_ANNOTATION } from '@backstage/catalog-model'; +import { Entity, ORIGIN_LOCATION_ANNOTATION } from '@backstage/catalog-model'; import { alertApiRef, Progress, useApi } from '@backstage/core'; import { Button, @@ -32,6 +32,7 @@ import React from 'react'; import { useAsync } from 'react-use'; import { AsyncState } from 'react-use/lib/useAsync'; import { catalogApiRef } from '../../plugin'; +import { formatEntityRefTitle } from '../EntityRefLink'; type Props = { open: boolean; @@ -40,15 +41,30 @@ type Props = { entity: Entity; }; +class DeniedLocationException extends Error { + constructor(public readonly locationName: string) { + super(`You may not remove the location ${locationName}`); + this.name = 'DeniedLocationException'; + } +} + function useColocatedEntities(entity: Entity): AsyncState { const catalogApi = useApi(catalogApiRef); return useAsync(async () => { - const myLocation = entity.metadata.annotations?.[LOCATION_ANNOTATION]; + const myLocation = + entity.metadata.annotations?.[ORIGIN_LOCATION_ANNOTATION]; if (!myLocation) { return []; } + + if (myLocation === 'bootstrap:bootstrap') { + throw new DeniedLocationException(myLocation); + } + const response = await catalogApi.getEntities({ - filter: { [LOCATION_ANNOTATION]: myLocation }, + filter: { + [`metadata.annotations.${ORIGIN_LOCATION_ANNOTATION}`]: myLocation, + }, }); return response.items; }, [catalogApi, entity]); @@ -82,13 +98,25 @@ export const UnregisterEntityDialog = ({ Are you sure you want to unregister this entity? + {loading ? : null} + {error ? ( - {error.toString()} + {error instanceof DeniedLocationException ? ( + <> + You cannot unregister this entity, since it originates from a + protected Backstage configuration (location + {`"${error.locationName}"`}). If you believe this is in error, + please contact your Backstage operator. + + ) : ( + error.toString() + )} ) : null} + {entities?.length ? ( <> @@ -96,9 +124,10 @@ export const UnregisterEntityDialog = ({
    - {entities.map(e => ( -
  • {e.metadata.name}
  • - ))} + {entities.map(e => { + const fullName = formatEntityRefTitle(e); + return
  • {fullName}
  • ; + })}
@@ -107,16 +136,21 @@ export const UnregisterEntityDialog = ({
  • - {entities[0]?.metadata.annotations?.[LOCATION_ANNOTATION]} + { + entities[0]?.metadata.annotations?.[ + ORIGIN_LOCATION_ANNOTATION + ] + }
+ + To undo, just re-register the entity in Backstage. + ) : null} - - To undo, just re-register the entity in Backstage. -
+