From 7863a81423953c35f06028bc19b65d47b3a8f741 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Feb 2021 05:35:47 +0000 Subject: [PATCH 01/34] chore(deps): bump @lerna/project from 3.21.0 to 4.0.0 Bumps [@lerna/project](https://github.com/lerna/lerna/tree/HEAD/core/project) from 3.21.0 to 4.0.0. - [Release notes](https://github.com/lerna/lerna/releases) - [Changelog](https://github.com/lerna/lerna/blob/main/core/project/CHANGELOG.md) - [Commits](https://github.com/lerna/lerna/commits/v4.0.0/core/project) Signed-off-by: dependabot[bot] --- packages/cli/package.json | 2 +- yarn.lock | 120 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 117 insertions(+), 5 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index dde6bc34ba..109ce47325 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -33,7 +33,7 @@ "@backstage/config-loader": "^0.5.1", "@hot-loader/react-dom": "^16.13.0", "@lerna/package-graph": "^3.18.5", - "@lerna/project": "^3.18.0", + "@lerna/project": "^4.0.0", "@octokit/request": "^5.4.12", "@rollup/plugin-commonjs": "^17.1.0", "@rollup/plugin-json": "^4.0.2", diff --git a/yarn.lock b/yarn.lock index 475ac65b6b..db2e674120 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3688,6 +3688,15 @@ npm-package-arg "^6.1.0" write-pkg "^3.1.0" +"@lerna/package@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/package/-/package-4.0.0.tgz#1b4c259c4bcff45c876ee1d591a043aacbc0d6b7" + integrity sha512-l0M/izok6FlyyitxiQKr+gZLVFnvxRQdNhzmQ6nRnN9dvBJWn+IxxpM+cLqGACatTnyo9LDzNTOj2Db3+s0s8Q== + dependencies: + load-json-file "^6.2.0" + npm-package-arg "^8.1.0" + write-pkg "^4.0.0" + "@lerna/prerelease-id-from-version@3.16.0": version "3.16.0" resolved "https://registry.npmjs.org/@lerna/prerelease-id-from-version/-/prerelease-id-from-version-3.16.0.tgz#b24bfa789f5e1baab914d7b08baae9b7bd7d83a1" @@ -3705,7 +3714,7 @@ npmlog "^4.1.2" upath "^1.2.0" -"@lerna/project@3.21.0", "@lerna/project@^3.18.0": +"@lerna/project@3.21.0": version "3.21.0" resolved "https://registry.npmjs.org/@lerna/project/-/project-3.21.0.tgz#5d784d2d10c561a00f20320bcdb040997c10502d" integrity sha512-xT1mrpET2BF11CY32uypV2GPtPVm6Hgtha7D81GQP9iAitk9EccrdNjYGt5UBYASl4CIDXBRxwmTTVGfrCx82A== @@ -3723,6 +3732,24 @@ resolve-from "^4.0.0" write-json-file "^3.2.0" +"@lerna/project@^4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/project/-/project-4.0.0.tgz#ff84893935833533a74deff30c0e64ddb7f0ba6b" + integrity sha512-o0MlVbDkD5qRPkFKlBZsXZjoNTWPyuL58564nSfZJ6JYNmgAptnWPB2dQlAc7HWRZkmnC2fCkEdoU+jioPavbg== + dependencies: + "@lerna/package" "4.0.0" + "@lerna/validation-error" "4.0.0" + cosmiconfig "^7.0.0" + dedent "^0.7.0" + dot-prop "^6.0.1" + glob-parent "^5.1.1" + globby "^11.0.2" + load-json-file "^6.2.0" + npmlog "^4.1.2" + p-map "^4.0.0" + resolve-from "^5.0.0" + write-json-file "^4.3.0" + "@lerna/prompt@3.18.5": version "3.18.5" resolved "https://registry.npmjs.org/@lerna/prompt/-/prompt-3.18.5.tgz#628cd545f225887d060491ab95df899cfc5218a1" @@ -3870,6 +3897,13 @@ dependencies: npmlog "^4.1.2" +"@lerna/validation-error@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/validation-error/-/validation-error-4.0.0.tgz#af9d62fe8304eaa2eb9a6ba1394f9aa807026d35" + integrity sha512-1rBOM5/koiVWlRi3V6dB863E1YzJS8v41UtsHgMr6gB2ncJ2LsQtMKlJpi3voqcgh41H8UsPXR58RrrpPpufyw== + dependencies: + npmlog "^4.1.2" + "@lerna/version@3.22.1": version "3.22.1" resolved "https://registry.npmjs.org/@lerna/version/-/version-3.22.1.tgz#9805a9247a47ee62d6b81bd9fa5fb728b24b59e2" @@ -11581,6 +11615,13 @@ dot-prop@^5.2.0: dependencies: is-obj "^2.0.0" +dot-prop@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz#fc26b3cf142b9e59b74dbd39ed66ce620c681083" + integrity sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA== + dependencies: + is-obj "^2.0.0" + dotenv-defaults@^1.0.2: version "1.1.1" resolved "https://registry.npmjs.org/dotenv-defaults/-/dotenv-defaults-1.1.1.tgz#032c024f4b5906d9990eb06d722dc74cc60ec1bd" @@ -13628,7 +13669,7 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@~5.1.0: +glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@^5.1.1, glob-parent@~5.1.0: version "5.1.1" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== @@ -13740,7 +13781,7 @@ globalthis@^1.0.0: dependencies: define-properties "^1.1.3" -globby@11.0.1, globby@^11.0.0, globby@^11.0.1: +globby@11.0.1: version "11.0.1" resolved "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" integrity sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ== @@ -13779,6 +13820,18 @@ globby@^10.0.1: merge2 "^1.2.3" slash "^3.0.0" +globby@^11.0.0, globby@^11.0.1, globby@^11.0.2: + version "11.0.2" + resolved "https://registry.npmjs.org/globby/-/globby-11.0.2.tgz#1af538b766a3b540ebfb58a32b2e2d5897321d83" + integrity sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og== + dependencies: + 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" + globby@^6.1.0: version "6.1.0" resolved "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" @@ -14334,6 +14387,13 @@ hosted-git-info@^2.1.4, hosted-git-info@^2.7.1: resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== +hosted-git-info@^3.0.6: + version "3.0.8" + resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.8.tgz#6e35d4cc87af2c5f816e4cb9ce350ba87a3f370d" + integrity sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw== + dependencies: + lru-cache "^6.0.0" + hpack.js@^2.1.6: version "2.1.6" resolved "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" @@ -17015,6 +17075,16 @@ load-json-file@^5.3.0: strip-bom "^3.0.0" type-fest "^0.3.0" +load-json-file@^6.2.0: + version "6.2.0" + resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-6.2.0.tgz#5c7770b42cafa97074ca2848707c61662f4251a1" + integrity sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ== + dependencies: + graceful-fs "^4.1.15" + parse-json "^5.0.0" + strip-bom "^4.0.0" + type-fest "^0.6.0" + load-yaml-file@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/load-yaml-file/-/load-yaml-file-0.2.0.tgz#af854edaf2bea89346c07549122753c07372f64d" @@ -18748,6 +18818,15 @@ npm-normalize-package-bin@^1.0.0, npm-normalize-package-bin@^1.0.1: semver "^5.6.0" validate-npm-package-name "^3.0.0" +npm-package-arg@^8.1.0: + version "8.1.0" + resolved "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.1.0.tgz#b5f6319418c3246a1c38e1a8fbaa06231bc5308f" + integrity sha512-/ep6QDxBkm9HvOhOg0heitSd7JHA1U7y1qhhlRlteYYAi9Pdb/ZV7FW5aHpkrpM8+P+4p/jjR8zCyKPBMBjSig== + dependencies: + hosted-git-info "^3.0.6" + semver "^7.0.0" + validate-npm-package-name "^3.0.0" + npm-packlist@^1.1.6, npm-packlist@^1.4.4: version "1.4.8" resolved "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e" @@ -22608,7 +22687,7 @@ semver@7.0.0: resolved "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== -semver@7.x, semver@^7.2.1, semver@^7.3.2: +semver@7.x, semver@^7.0.0, semver@^7.2.1, semver@^7.3.2: version "7.3.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== @@ -23021,6 +23100,13 @@ sort-keys@^2.0.0: dependencies: is-plain-obj "^1.0.0" +sort-keys@^4.0.0: + version "4.2.0" + resolved "https://registry.npmjs.org/sort-keys/-/sort-keys-4.2.0.tgz#6b7638cee42c506fff8c1cecde7376d21315be18" + integrity sha512-aUYIEU/UviqPgc8mHR6IW1EGxkAXpeRETYcrzg8cLAvUPZcpAlleSXHV2mY7G12GphSH6Gzv+4MMVSSkbdteHg== + dependencies: + is-plain-obj "^2.0.0" + source-list-map@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" @@ -24674,6 +24760,11 @@ type-fest@^0.3.0: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" integrity sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ== +type-fest@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.4.1.tgz#8bdf77743385d8a4f13ba95f610f5ccd68c728f8" + integrity sha512-IwzA/LSfD2vC1/YDYMv/zHP4rDF1usCwllsDpbolT3D4fUepIO7f9K70jjmUewU/LmGUKJcwcVtDCpnKk4BPMw== + type-fest@^0.6.0: version "0.6.0" resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" @@ -25824,6 +25915,18 @@ write-json-file@^3.2.0: sort-keys "^2.0.0" write-file-atomic "^2.4.2" +write-json-file@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/write-json-file/-/write-json-file-4.3.0.tgz#908493d6fd23225344af324016e4ca8f702dd12d" + integrity sha512-PxiShnxf0IlnQuMYOPPhPkhExoCQuTUNPOa/2JWCYTmBquU9njyyDuwRKN26IZBlp4yn1nt+Agh2HOOBl+55HQ== + dependencies: + detect-indent "^6.0.0" + graceful-fs "^4.1.15" + is-plain-obj "^2.0.0" + make-dir "^3.0.0" + sort-keys "^4.0.0" + write-file-atomic "^3.0.0" + write-pkg@^3.1.0: version "3.2.0" resolved "https://registry.npmjs.org/write-pkg/-/write-pkg-3.2.0.tgz#0e178fe97820d389a8928bc79535dbe68c2cff21" @@ -25832,6 +25935,15 @@ write-pkg@^3.1.0: sort-keys "^2.0.0" write-json-file "^2.2.0" +write-pkg@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/write-pkg/-/write-pkg-4.0.0.tgz#675cc04ef6c11faacbbc7771b24c0abbf2a20039" + integrity sha512-v2UQ+50TNf2rNHJ8NyWttfm/EJUBWMJcx6ZTYZr6Qp52uuegWw/lBkCtCbnYZEmPRNL61m+u67dAmGxo+HTULA== + dependencies: + sort-keys "^2.0.0" + type-fest "^0.4.1" + write-json-file "^3.2.0" + write@1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" From 8155fad5378122d29175f8062c67a7820947c707 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 12 Feb 2021 09:46:15 +0100 Subject: [PATCH 02/34] deps: update the lerna package too --- packages/cli/package.json | 2 +- yarn.lock | 22 ++++++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 109ce47325..e54696ce43 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -32,7 +32,7 @@ "@backstage/config": "^0.1.2", "@backstage/config-loader": "^0.5.1", "@hot-loader/react-dom": "^16.13.0", - "@lerna/package-graph": "^3.18.5", + "@lerna/package-graph": "^4.0.0", "@lerna/project": "^4.0.0", "@octokit/request": "^5.4.12", "@rollup/plugin-commonjs": "^17.1.0", diff --git a/yarn.lock b/yarn.lock index db2e674120..070eb8b7f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3668,7 +3668,7 @@ tar "^4.4.10" temp-write "^3.4.0" -"@lerna/package-graph@3.18.5", "@lerna/package-graph@^3.18.5": +"@lerna/package-graph@3.18.5": version "3.18.5" resolved "https://registry.npmjs.org/@lerna/package-graph/-/package-graph-3.18.5.tgz#c740e2ea3578d059e551633e950690831b941f6b" integrity sha512-8QDrR9T+dBegjeLr+n9WZTVxUYUhIUjUgZ0gvNxUBN8S1WB9r6H5Yk56/MVaB64tA3oGAN9IIxX6w0WvTfFudA== @@ -3679,6 +3679,17 @@ npmlog "^4.1.2" semver "^6.2.0" +"@lerna/package-graph@^4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/package-graph/-/package-graph-4.0.0.tgz#16a00253a8ac810f72041481cb46bcee8d8123dd" + integrity sha512-QED2ZCTkfXMKFoTGoccwUzjHtZMSf3UKX14A4/kYyBms9xfFsesCZ6SLI5YeySEgcul8iuIWfQFZqRw+Qrjraw== + dependencies: + "@lerna/prerelease-id-from-version" "4.0.0" + "@lerna/validation-error" "4.0.0" + npm-package-arg "^8.1.0" + npmlog "^4.1.2" + semver "^7.3.4" + "@lerna/package@3.16.0": version "3.16.0" resolved "https://registry.npmjs.org/@lerna/package/-/package-3.16.0.tgz#7e0a46e4697ed8b8a9c14d59c7f890e0d38ba13c" @@ -3704,6 +3715,13 @@ dependencies: semver "^6.2.0" +"@lerna/prerelease-id-from-version@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/prerelease-id-from-version/-/prerelease-id-from-version-4.0.0.tgz#c7e0676fcee1950d85630e108eddecdd5b48c916" + integrity sha512-GQqguzETdsYRxOSmdFZ6zDBXDErIETWOqomLERRY54f4p+tk4aJjoVdd9xKwehC9TBfIFvlRbL1V9uQGHh1opg== + dependencies: + semver "^7.3.4" + "@lerna/profiler@3.20.0": version "3.20.0" resolved "https://registry.npmjs.org/@lerna/profiler/-/profiler-3.20.0.tgz#0f6dc236f4ea8f9ea5f358c6703305a4f32ad051" @@ -22687,7 +22705,7 @@ semver@7.0.0: resolved "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== -semver@7.x, semver@^7.0.0, semver@^7.2.1, semver@^7.3.2: +semver@7.x, semver@^7.0.0, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4: version "7.3.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== From 1d2bceafd5916c94bfee03d1eca1f088f72e81cd Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 12 Feb 2021 10:14:53 +0100 Subject: [PATCH 03/34] bug: Upgrade to Lerna 4 --- packages/cli/src/lib/bundler/config.ts | 2 +- packages/cli/src/lib/config.ts | 2 +- packages/cli/src/lib/packager/index.ts | 2 +- packages/cli/src/lib/versioning/packages.test.ts | 2 +- packages/cli/src/lib/versioning/packages.ts | 2 +- scripts/check-type-dependencies.js | 2 +- scripts/isolated-release.js | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 6fbcc65c9a..b168b75eb4 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -72,7 +72,7 @@ async function readBuildInfo() { } async function loadLernaPackages(): Promise { - const LernaProject = require('@lerna/project'); + const { LernaProject } = require('@lerna/project'); const project = new LernaProject(cliPaths.targetDir); return project.getPackages(); } diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index 30469421d3..b3e5f7844d 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -28,7 +28,7 @@ export async function loadCliConfig(options: Options) { const configPaths = options.args.map(arg => paths.resolveTarget(arg)); // Consider all packages in the monorepo when loading in config - const LernaProject = require('@lerna/project'); + const { LernaProject } = require('@lerna/project'); const project = new LernaProject(paths.targetDir); const packages = await project.getPackages(); diff --git a/packages/cli/src/lib/packager/index.ts b/packages/cli/src/lib/packager/index.ts index 3976998998..b9e5ab013d 100644 --- a/packages/cli/src/lib/packager/index.ts +++ b/packages/cli/src/lib/packager/index.ts @@ -219,7 +219,7 @@ async function moveToDistWorkspace( } async function findTargetPackages(pkgNames: string[]): Promise { - const LernaProject = require('@lerna/project'); + const { LernaProject } = require('@lerna/project'); const PackageGraph = require('@lerna/package-graph'); const project = new LernaProject(paths.targetDir); diff --git a/packages/cli/src/lib/versioning/packages.test.ts b/packages/cli/src/lib/versioning/packages.test.ts index 5af040809a..bd30015f06 100644 --- a/packages/cli/src/lib/versioning/packages.test.ts +++ b/packages/cli/src/lib/versioning/packages.test.ts @@ -59,7 +59,7 @@ describe('mapDependencies', () => { it('should read dependencies', async () => { // Make sure all modules involved in package discovery are in the module cache before we mock fs - const LernaProject = require('@lerna/project'); + const { LernaProject } = require('@lerna/project'); const project = new LernaProject(paths.targetDir); await project.getPackages(); diff --git a/packages/cli/src/lib/versioning/packages.ts b/packages/cli/src/lib/versioning/packages.ts index 991a4d23ef..207700f52a 100644 --- a/packages/cli/src/lib/versioning/packages.ts +++ b/packages/cli/src/lib/versioning/packages.ts @@ -67,7 +67,7 @@ export async function fetchPackageInfo( export async function mapDependencies( targetDir: string, ): Promise> { - const LernaProject = require('@lerna/project'); + const { LernaProject } = require('@lerna/project'); const project = new LernaProject(targetDir); const packages = await project.getPackages(); diff --git a/scripts/check-type-dependencies.js b/scripts/check-type-dependencies.js index 6ee4626a21..e0978d5951 100755 --- a/scripts/check-type-dependencies.js +++ b/scripts/check-type-dependencies.js @@ -24,7 +24,7 @@ const chalk = require('chalk'); async function main() { // This is from lerna, and cba polluting root package.json // eslint-disable-next-line import/no-extraneous-dependencies - const LernaProject = require('@lerna/project'); + const { LernaProject } = require('@lerna/project'); const project = new LernaProject(resolvePath('.')); const packages = await project.getPackages(); diff --git a/scripts/isolated-release.js b/scripts/isolated-release.js index 03c4e632c7..ea4ef32dc6 100755 --- a/scripts/isolated-release.js +++ b/scripts/isolated-release.js @@ -18,7 +18,7 @@ const path = require('path'); const childProcess = require('child_process'); // eslint-disable-next-line import/no-extraneous-dependencies -const LernaProject = require('@lerna/project'); +const { LernaProject } = require('@lerna/project'); // Prepare a release of the provided packages, e.g. @backstage/core async function main(args) { From dc9d67f288c166c721c2027302a141a7eed8c8c9 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 12 Feb 2021 10:48:58 +0100 Subject: [PATCH 04/34] chore: named export is Project not LernaProjcet --- packages/cli/src/lib/bundler/config.ts | 4 ++-- packages/cli/src/lib/config.ts | 4 ++-- packages/cli/src/lib/packager/index.ts | 4 ++-- packages/cli/src/lib/versioning/packages.test.ts | 4 ++-- packages/cli/src/lib/versioning/packages.ts | 4 ++-- scripts/check-type-dependencies.js | 4 ++-- scripts/isolated-release.js | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index b168b75eb4..f81e055f69 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -72,8 +72,8 @@ async function readBuildInfo() { } async function loadLernaPackages(): Promise { - const { LernaProject } = require('@lerna/project'); - const project = new LernaProject(cliPaths.targetDir); + const { Project } = require('@lerna/project'); + const project = new Project(cliPaths.targetDir); return project.getPackages(); } diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index b3e5f7844d..a48c4b1810 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -28,8 +28,8 @@ export async function loadCliConfig(options: Options) { const configPaths = options.args.map(arg => paths.resolveTarget(arg)); // Consider all packages in the monorepo when loading in config - const { LernaProject } = require('@lerna/project'); - const project = new LernaProject(paths.targetDir); + const { Project } = require('@lerna/project'); + const project = new Project(paths.targetDir); const packages = await project.getPackages(); const localPackageNames = options.fromPackage diff --git a/packages/cli/src/lib/packager/index.ts b/packages/cli/src/lib/packager/index.ts index b9e5ab013d..7a6fb494a4 100644 --- a/packages/cli/src/lib/packager/index.ts +++ b/packages/cli/src/lib/packager/index.ts @@ -219,10 +219,10 @@ async function moveToDistWorkspace( } async function findTargetPackages(pkgNames: string[]): Promise { - const { LernaProject } = require('@lerna/project'); + const { Project } = require('@lerna/project'); const PackageGraph = require('@lerna/package-graph'); - const project = new LernaProject(paths.targetDir); + const project = new Project(paths.targetDir); const packages = await project.getPackages(); const graph = new PackageGraph(packages); diff --git a/packages/cli/src/lib/versioning/packages.test.ts b/packages/cli/src/lib/versioning/packages.test.ts index bd30015f06..aca439a0e7 100644 --- a/packages/cli/src/lib/versioning/packages.test.ts +++ b/packages/cli/src/lib/versioning/packages.test.ts @@ -59,8 +59,8 @@ describe('mapDependencies', () => { it('should read dependencies', async () => { // Make sure all modules involved in package discovery are in the module cache before we mock fs - const { LernaProject } = require('@lerna/project'); - const project = new LernaProject(paths.targetDir); + const { Project } = require('@lerna/project'); + const project = new Project(paths.targetDir); await project.getPackages(); mockFs({ diff --git a/packages/cli/src/lib/versioning/packages.ts b/packages/cli/src/lib/versioning/packages.ts index 207700f52a..777dc72757 100644 --- a/packages/cli/src/lib/versioning/packages.ts +++ b/packages/cli/src/lib/versioning/packages.ts @@ -67,8 +67,8 @@ export async function fetchPackageInfo( export async function mapDependencies( targetDir: string, ): Promise> { - const { LernaProject } = require('@lerna/project'); - const project = new LernaProject(targetDir); + const { Project } = require('@lerna/project'); + const project = new Project(targetDir); const packages = await project.getPackages(); const dependencyMap = new Map(); diff --git a/scripts/check-type-dependencies.js b/scripts/check-type-dependencies.js index e0978d5951..f3862214a1 100755 --- a/scripts/check-type-dependencies.js +++ b/scripts/check-type-dependencies.js @@ -24,8 +24,8 @@ const chalk = require('chalk'); async function main() { // This is from lerna, and cba polluting root package.json // eslint-disable-next-line import/no-extraneous-dependencies - const { LernaProject } = require('@lerna/project'); - const project = new LernaProject(resolvePath('.')); + const { Project } = require('@lerna/project'); + const project = new Project(resolvePath('.')); const packages = await project.getPackages(); let hadErrors = false; diff --git a/scripts/isolated-release.js b/scripts/isolated-release.js index ea4ef32dc6..27dfd35b07 100755 --- a/scripts/isolated-release.js +++ b/scripts/isolated-release.js @@ -18,7 +18,7 @@ const path = require('path'); const childProcess = require('child_process'); // eslint-disable-next-line import/no-extraneous-dependencies -const { LernaProject } = require('@lerna/project'); +const { Project } = require('@lerna/project'); // Prepare a release of the provided packages, e.g. @backstage/core async function main(args) { @@ -28,7 +28,7 @@ async function main(args) { process.exit(1); } - const project = new LernaProject(__dirname); + const project = new Project(__dirname); const packages = await project.getPackages(); const ignoreArgs = packages .filter(p => !args.includes(p.name)) From ec7ba0058122abd205f7543acdd34711d638a828 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Fri, 12 Feb 2021 10:56:57 +0100 Subject: [PATCH 05/34] techdocs: Add build timestamp to techdocs_metadata.json Co-authored-by: Raghunandan Balachandran --- .../src/stages/generate/helpers.test.ts | 73 +++++++++++++++---- .../src/stages/generate/helpers.ts | 45 ++++++++++-- .../src/stages/generate/techdocs.ts | 22 ++++-- 3 files changed, 112 insertions(+), 28 deletions(-) diff --git a/packages/techdocs-common/src/stages/generate/helpers.test.ts b/packages/techdocs-common/src/stages/generate/helpers.test.ts index 4d62648a7d..6098e46c7f 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.test.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.test.ts @@ -13,22 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import fs from 'fs-extra'; -import os from 'os'; -import { resolve as resolvePath } from 'path'; -import Stream, { PassThrough } from 'stream'; +import { getVoidLogger } from '@backstage/backend-common'; import Docker from 'dockerode'; +import fs from 'fs-extra'; import mockFs from 'mock-fs'; -import * as winston from 'winston'; -import { - runDockerContainer, - getGeneratorKey, - isValidRepoUrlForMkdocs, - getRepoUrlFromLocationAnnotation, - patchMkdocsYmlPreBuild, -} from './helpers'; -import { RemoteProtocol } from '../prepare/types'; +import os from 'os'; +import path, { resolve as resolvePath } from 'path'; +import Stream, { PassThrough } from 'stream'; import { ParsedLocationAnnotation } from '../../helpers'; +import { RemoteProtocol } from '../prepare/types'; +import { + addBuildTimestampMetadata, + getGeneratorKey, + getRepoUrlFromLocationAnnotation, + isValidRepoUrlForMkdocs, + patchMkdocsYmlPreBuild, + runDockerContainer, +} from './helpers'; const mockEntity = { apiVersion: 'version', @@ -46,7 +47,8 @@ const mkdocsYml = fs.readFileSync( const mkdocsYmlWithRepoUrl = fs.readFileSync( resolvePath(__filename, '../__fixtures__/mkdocs_with_repo_url.yml'), ); -const mockLogger = winston.createLogger(); +const mockLogger = getVoidLogger(); +const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; describe('helpers', () => { describe('getGeneratorKey', () => { @@ -329,4 +331,47 @@ describe('helpers', () => { ); }); }); + + describe('addBuildTimestampMetadata', () => { + beforeEach(() => { + mockFs({ + [tmpDir]: { + 'invalid_techdocs_metadata.json': 'dsds', + 'techdocs_metadata.json': '{"site_name": "Tech Docs"}', + }, + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it('should create the file if it does not exist', async () => { + const filePath = path.join(tmpDir, 'wrong_techdocs_metadata.json'); + await addBuildTimestampMetadata(filePath, mockLogger); + + // Check if the file exists + await expect( + fs.access(filePath, fs.constants.F_OK), + ).resolves.not.toThrowError(); + }); + + it('should throw error when the JSON is invalid', async () => { + const filePath = path.join(tmpDir, 'invalid_techdocs_metadata.json'); + + // Check if the file exists + await expect( + addBuildTimestampMetadata(filePath, mockLogger), + ).rejects.toThrowError(); + }); + + it('should add build timestamp to the metadata json', async () => { + const filePath = path.join(tmpDir, 'techdocs_metadata.json'); + + await addBuildTimestampMetadata(filePath, mockLogger); + + const json = await fs.readJson(filePath); + expect(json.build_timestamp).toBeLessThanOrEqual(Date.now()); + }); + }); }); diff --git a/packages/techdocs-common/src/stages/generate/helpers.ts b/packages/techdocs-common/src/stages/generate/helpers.ts index e396157b92..15a1c57d41 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.ts @@ -14,16 +14,16 @@ * limitations under the License. */ -import fs from 'fs-extra'; -import { spawn } from 'child_process'; -import { Writable, PassThrough } from 'stream'; -import Docker from 'dockerode'; -import yaml from 'js-yaml'; -import { Logger } from 'winston'; import { Entity } from '@backstage/catalog-model'; -import { SupportedGeneratorKey } from './types'; +import { spawn } from 'child_process'; +import Docker from 'dockerode'; +import fs from 'fs-extra'; +import yaml from 'js-yaml'; +import { PassThrough, Writable } from 'stream'; +import { Logger } from 'winston'; import { ParsedLocationAnnotation } from '../../helpers'; import { RemoteProtocol } from '../prepare/types'; +import { SupportedGeneratorKey } from './types'; // TODO: Implement proper support for more generators. export function getGeneratorKey(entity: Entity): SupportedGeneratorKey { @@ -275,3 +275,34 @@ export const patchMkdocsYmlPreBuild = async ( return; } }; + +/** + * Update the techdocs_metadata.json to add a new build timestamp metadata. Create the .json file if it doesn't exist. + * + * @param {string} techdocsMetadataPath File path to techdocs_metadata.json + */ +export const addBuildTimestampMetadata = async ( + techdocsMetadataPath: string, + logger: Logger, +): Promise => { + // check if file exists, create if it does not. + try { + await fs.access(techdocsMetadataPath, fs.constants.F_OK); + } catch (err) { + // Bootstrap file with empty JSON + await fs.writeJson(techdocsMetadataPath, JSON.parse('{}')); + } + // check if valid Json + let json; + try { + json = await fs.readJson(techdocsMetadataPath); + } catch (err) { + const message = `Invalid JSON at ${techdocsMetadataPath} with error ${err.message}`; + logger.error(message); + throw new Error(message); + } + + json.build_timestamp = Date.now(); + await fs.writeJson(techdocsMetadataPath, json); + return; +}; diff --git a/packages/techdocs-common/src/stages/generate/techdocs.ts b/packages/techdocs-common/src/stages/generate/techdocs.ts index 507b9da6b8..b7b46b1b22 100644 --- a/packages/techdocs-common/src/stages/generate/techdocs.ts +++ b/packages/techdocs-common/src/stages/generate/techdocs.ts @@ -14,17 +14,17 @@ * limitations under the License. */ -import path from 'path'; -import { Logger } from 'winston'; -import { PassThrough } from 'stream'; import { Config } from '@backstage/config'; - -import { GeneratorBase, GeneratorRunOptions } from './types'; +import path from 'path'; +import { PassThrough } from 'stream'; +import { Logger } from 'winston'; import { - runDockerContainer, - runCommand, + addBuildTimestampMetadata, patchMkdocsYmlPreBuild, + runCommand, + runDockerContainer, } from './helpers'; +import { GeneratorBase, GeneratorRunOptions } from './types'; type TechdocsGeneratorOptions = { // This option enables users to configure if they want to use TechDocs container @@ -118,5 +118,13 @@ export class TechdocsGenerator implements GeneratorBase { `Failed to generate docs from ${inputDir} into ${outputDir} with error ${error.message}`, ); } + + // Post Generate steps + + // Add build timestamp to techdocs_metadata.json + addBuildTimestampMetadata( + path.join(outputDir, 'techdocs_metadata.json'), + this.logger, + ); } } From 26e143e6066fdfebb08a9f0eb55f8a26d579d8b5 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Fri, 12 Feb 2021 10:59:08 +0100 Subject: [PATCH 06/34] techdocs: Add changeset for build timestamp Co-authored-by: Raghunandan Balachandran --- .changeset/techdocs-fast-foxes-relate.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/techdocs-fast-foxes-relate.md diff --git a/.changeset/techdocs-fast-foxes-relate.md b/.changeset/techdocs-fast-foxes-relate.md new file mode 100644 index 0000000000..7377946f86 --- /dev/null +++ b/.changeset/techdocs-fast-foxes-relate.md @@ -0,0 +1,5 @@ +--- +'@backstage/techdocs-common': patch +--- + +After TechDocs generate step, insert build timestamp to techdocs_metadata.json From 4ddc18c849efac695bbdc0ccd1b03e604f73a1a5 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Fri, 12 Feb 2021 11:17:32 +0100 Subject: [PATCH 07/34] techdocs: cleanup mock fs before mounting --- packages/techdocs-common/src/stages/generate/helpers.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/techdocs-common/src/stages/generate/helpers.test.ts b/packages/techdocs-common/src/stages/generate/helpers.test.ts index 6098e46c7f..93abf51bb6 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.test.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.test.ts @@ -334,6 +334,7 @@ describe('helpers', () => { describe('addBuildTimestampMetadata', () => { beforeEach(() => { + mockFs.restore(); mockFs({ [tmpDir]: { 'invalid_techdocs_metadata.json': 'dsds', From d27c9a86ed8a08028af9e7264c48b2c281088e31 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Fri, 12 Feb 2021 11:34:36 +0100 Subject: [PATCH 08/34] techdocs: avoid collision with existing /tmp dir --- .../src/stages/generate/helpers.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/techdocs-common/src/stages/generate/helpers.test.ts b/packages/techdocs-common/src/stages/generate/helpers.test.ts index 93abf51bb6..10efb310e1 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.test.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.test.ts @@ -48,7 +48,7 @@ const mkdocsYmlWithRepoUrl = fs.readFileSync( resolvePath(__filename, '../__fixtures__/mkdocs_with_repo_url.yml'), ); const mockLogger = getVoidLogger(); -const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; +const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; describe('helpers', () => { describe('getGeneratorKey', () => { @@ -336,7 +336,7 @@ describe('helpers', () => { beforeEach(() => { mockFs.restore(); mockFs({ - [tmpDir]: { + [rootDir]: { 'invalid_techdocs_metadata.json': 'dsds', 'techdocs_metadata.json': '{"site_name": "Tech Docs"}', }, @@ -348,7 +348,7 @@ describe('helpers', () => { }); it('should create the file if it does not exist', async () => { - const filePath = path.join(tmpDir, 'wrong_techdocs_metadata.json'); + const filePath = path.join(rootDir, 'wrong_techdocs_metadata.json'); await addBuildTimestampMetadata(filePath, mockLogger); // Check if the file exists @@ -358,7 +358,7 @@ describe('helpers', () => { }); it('should throw error when the JSON is invalid', async () => { - const filePath = path.join(tmpDir, 'invalid_techdocs_metadata.json'); + const filePath = path.join(rootDir, 'invalid_techdocs_metadata.json'); // Check if the file exists await expect( @@ -367,7 +367,7 @@ describe('helpers', () => { }); it('should add build timestamp to the metadata json', async () => { - const filePath = path.join(tmpDir, 'techdocs_metadata.json'); + const filePath = path.join(rootDir, 'techdocs_metadata.json'); await addBuildTimestampMetadata(filePath, mockLogger); From 10e59af4d0a045059967ea65252f435eebab8761 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Feb 2021 14:36:46 +0100 Subject: [PATCH 09/34] auth-backend: refactor provider factories to accept options --- .../auth-backend/src/providers/auth0/index.ts | 1 + .../src/providers/auth0/provider.ts | 45 ++++++------ .../src/providers/aws-alb/index.ts | 1 + .../src/providers/aws-alb/provider.ts | 40 +++++----- .../auth-backend/src/providers/factories.ts | 22 +++--- .../src/providers/github/index.ts | 1 + .../src/providers/github/provider.ts | 73 ++++++++++--------- .../src/providers/gitlab/index.ts | 1 + .../src/providers/gitlab/provider.ts | 47 ++++++------ .../src/providers/google/index.ts | 1 + .../src/providers/google/provider.ts | 60 ++++++++------- .../src/providers/microsoft/index.ts | 1 + .../src/providers/microsoft/provider.ts | 53 +++++++------- .../src/providers/oauth2/index.ts | 1 + .../src/providers/oauth2/provider.ts | 53 +++++++------- .../auth-backend/src/providers/oidc/index.ts | 1 + .../src/providers/oidc/provider.test.ts | 2 +- .../src/providers/oidc/provider.ts | 61 ++++++++-------- .../auth-backend/src/providers/okta/index.ts | 2 + .../src/providers/okta/provider.ts | 45 ++++++------ .../src/providers/onelogin/index.ts | 1 + .../src/providers/onelogin/provider.ts | 49 +++++++------ .../auth-backend/src/providers/saml/index.ts | 1 + .../src/providers/saml/provider.ts | 67 ++++++++--------- 24 files changed, 329 insertions(+), 300 deletions(-) diff --git a/plugins/auth-backend/src/providers/auth0/index.ts b/plugins/auth-backend/src/providers/auth0/index.ts index 87c9aceaa4..480552a91d 100644 --- a/plugins/auth-backend/src/providers/auth0/index.ts +++ b/plugins/auth-backend/src/providers/auth0/index.ts @@ -15,3 +15,4 @@ */ export { createAuth0Provider } from './provider'; +export type { Auth0ProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index 8772842e2a..89f4f73300 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -148,28 +148,29 @@ export class Auth0AuthProvider implements OAuthHandlers { } } -export const createAuth0Provider: AuthProviderFactory = ({ - providerId, - globalConfig, - config, - tokenIssuer, -}) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const domain = envConfig.getString('domain'); - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; +export type Auth0ProviderOptions = {}; - const provider = new Auth0AuthProvider({ - clientId, - clientSecret, - callbackUrl, - domain, - }); +export const createAuth0Provider = ( + _options?: Auth0ProviderOptions, +): AuthProviderFactory => { + return ({ providerId, globalConfig, config, tokenIssuer }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const domain = envConfig.getString('domain'); + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: true, - providerId, - tokenIssuer, + const provider = new Auth0AuthProvider({ + clientId, + clientSecret, + callbackUrl, + domain, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: true, + providerId, + tokenIssuer, + }); }); - }); +}; diff --git a/plugins/auth-backend/src/providers/aws-alb/index.ts b/plugins/auth-backend/src/providers/aws-alb/index.ts index f8b5c9e5d7..cd6d41f580 100644 --- a/plugins/auth-backend/src/providers/aws-alb/index.ts +++ b/plugins/auth-backend/src/providers/aws-alb/index.ts @@ -14,3 +14,4 @@ * limitations under the License. */ export { createAwsAlbProvider } from './provider'; +export type { AwsAlbProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts index 61ea10947e..27baaded0f 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts @@ -106,22 +106,26 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { } } -export const createAwsAlbProvider = ({ - logger, - catalogApi, - config, - identityResolver, -}: AuthProviderFactoryOptions) => { - const region = config.getString('region'); - const issuer = config.getOptionalString('iss'); - if (identityResolver !== undefined) { - return new AwsAlbAuthProvider(logger, catalogApi, { - region, - issuer, - identityResolutionCallback: identityResolver, - }); - } - throw new Error( - 'Identity resolver is required to use this authentication provider', - ); +export type AwsAlbProviderOptions = {}; + +export const createAwsAlbProvider = (_options?: AwsAlbProviderOptions) => { + return ({ + logger, + catalogApi, + config, + identityResolver, + }: AuthProviderFactoryOptions) => { + const region = config.getString('region'); + const issuer = config.getOptionalString('iss'); + 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 619fb1c706..670dc84eec 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -28,15 +28,15 @@ import { AuthProviderFactory } from './types'; import { createAwsAlbProvider } from './aws-alb'; export const factories: { [providerId: string]: AuthProviderFactory } = { - google: createGoogleProvider, - github: createGithubProvider, - gitlab: createGitlabProvider, - saml: createSamlProvider, - okta: createOktaProvider, - auth0: createAuth0Provider, - microsoft: createMicrosoftProvider, - oauth2: createOAuth2Provider, - oidc: createOidcProvider, - onelogin: createOneLoginProvider, - awsalb: createAwsAlbProvider, + google: createGoogleProvider(), + github: createGithubProvider(), + gitlab: createGitlabProvider(), + saml: createSamlProvider(), + okta: createOktaProvider(), + auth0: createAuth0Provider(), + microsoft: createMicrosoftProvider(), + oauth2: createOAuth2Provider(), + oidc: createOidcProvider(), + onelogin: createOneLoginProvider(), + awsalb: createAwsAlbProvider(), }; diff --git a/plugins/auth-backend/src/providers/github/index.ts b/plugins/auth-backend/src/providers/github/index.ts index 60ad6998b7..f2ba73c2b9 100644 --- a/plugins/auth-backend/src/providers/github/index.ts +++ b/plugins/auth-backend/src/providers/github/index.ts @@ -15,3 +15,4 @@ */ export { createGithubProvider } from './provider'; +export type { GithubProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 6e3f9a6000..47d17fdac2 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -136,42 +136,43 @@ export class GithubAuthProvider implements OAuthHandlers { } } -export const createGithubProvider: AuthProviderFactory = ({ - providerId, - globalConfig, - config, - tokenIssuer, -}) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const enterpriseInstanceUrl = envConfig.getOptionalString( - 'enterpriseInstanceUrl', - ); - const authorizationUrl = enterpriseInstanceUrl - ? `${enterpriseInstanceUrl}/login/oauth/authorize` - : undefined; - const tokenUrl = enterpriseInstanceUrl - ? `${enterpriseInstanceUrl}/login/oauth/access_token` - : undefined; - const userProfileUrl = enterpriseInstanceUrl - ? `${enterpriseInstanceUrl}/api/v3/user` - : undefined; - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; +export type GithubProviderOptions = {}; - const provider = new GithubAuthProvider({ - clientId, - clientSecret, - callbackUrl, - tokenUrl, - userProfileUrl, - authorizationUrl, - }); +export const createGithubProvider = ( + _options?: GithubProviderOptions, +): AuthProviderFactory => { + return ({ providerId, globalConfig, config, tokenIssuer }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const enterpriseInstanceUrl = envConfig.getOptionalString( + 'enterpriseInstanceUrl', + ); + const authorizationUrl = enterpriseInstanceUrl + ? `${enterpriseInstanceUrl}/login/oauth/authorize` + : undefined; + const tokenUrl = enterpriseInstanceUrl + ? `${enterpriseInstanceUrl}/login/oauth/access_token` + : undefined; + const userProfileUrl = enterpriseInstanceUrl + ? `${enterpriseInstanceUrl}/api/v3/user` + : undefined; + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: true, - persistScopes: true, - providerId, - tokenIssuer, + const provider = new GithubAuthProvider({ + clientId, + clientSecret, + callbackUrl, + tokenUrl, + userProfileUrl, + authorizationUrl, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: true, + persistScopes: true, + providerId, + tokenIssuer, + }); }); - }); +}; diff --git a/plugins/auth-backend/src/providers/gitlab/index.ts b/plugins/auth-backend/src/providers/gitlab/index.ts index 7fba2dd95c..0a516021cd 100644 --- a/plugins/auth-backend/src/providers/gitlab/index.ts +++ b/plugins/auth-backend/src/providers/gitlab/index.ts @@ -15,3 +15,4 @@ */ export { createGitlabProvider } from './provider'; +export type { GitlabProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index df6d2daeb7..05a1ab2efe 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -139,29 +139,30 @@ export class GitlabAuthProvider implements OAuthHandlers { } } -export const createGitlabProvider: AuthProviderFactory = ({ - providerId, - globalConfig, - config, - tokenIssuer, -}) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const audience = envConfig.getString('audience'); - const baseUrl = audience || 'https://gitlab.com'; - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; +export type GitlabProviderOptions = {}; - const provider = new GitlabAuthProvider({ - clientId, - clientSecret, - callbackUrl, - baseUrl, - }); +export const createGitlabProvider = ( + _options?: GitlabProviderOptions, +): AuthProviderFactory => { + return ({ providerId, globalConfig, config, tokenIssuer }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const audience = envConfig.getString('audience'); + const baseUrl = audience || 'https://gitlab.com'; + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: true, - providerId, - tokenIssuer, + const provider = new GitlabAuthProvider({ + clientId, + clientSecret, + callbackUrl, + baseUrl, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: true, + providerId, + tokenIssuer, + }); }); - }); +}; diff --git a/plugins/auth-backend/src/providers/google/index.ts b/plugins/auth-backend/src/providers/google/index.ts index b2cd85e6de..2615a2d8e5 100644 --- a/plugins/auth-backend/src/providers/google/index.ts +++ b/plugins/auth-backend/src/providers/google/index.ts @@ -15,3 +15,4 @@ */ export { createGoogleProvider } from './provider'; +export type { GoogleProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 848a02bad6..fa42988c98 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -44,7 +44,7 @@ type PrivateInfo = { refreshToken: string; }; -export type GoogleAuthProviderOptions = OAuthProviderOptions & { +type Options = OAuthProviderOptions & { logger: Logger; identityClient: CatalogIdentityClient; tokenIssuer: TokenIssuer; @@ -56,7 +56,7 @@ export class GoogleAuthProvider implements OAuthHandlers { private readonly identityClient: CatalogIdentityClient; private readonly tokenIssuer: TokenIssuer; - constructor(options: GoogleAuthProviderOptions) { + constructor(options: Options) { this.logger = options.logger; this.identityClient = options.identityClient; this.tokenIssuer = options.tokenIssuer; @@ -184,31 +184,37 @@ export class GoogleAuthProvider implements OAuthHandlers { } } -export const createGoogleProvider: AuthProviderFactory = ({ - providerId, - globalConfig, - config, - logger, - tokenIssuer, - catalogApi, -}) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; +export type GoogleProviderOptions = {}; - const provider = new GoogleAuthProvider({ - clientId, - clientSecret, - callbackUrl, - logger, - tokenIssuer, - identityClient: new CatalogIdentityClient({ catalogApi }), - }); +export const createGoogleProvider = ( + _options?: GoogleProviderOptions, +): AuthProviderFactory => { + return ({ + providerId, + globalConfig, + config, + logger, + tokenIssuer, + catalogApi, + }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: false, - providerId, - tokenIssuer, + const provider = new GoogleAuthProvider({ + clientId, + clientSecret, + callbackUrl, + logger, + tokenIssuer, + identityClient: new CatalogIdentityClient({ catalogApi }), + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); }); - }); +}; diff --git a/plugins/auth-backend/src/providers/microsoft/index.ts b/plugins/auth-backend/src/providers/microsoft/index.ts index 2e4abd2d2c..f6c2ce40cd 100644 --- a/plugins/auth-backend/src/providers/microsoft/index.ts +++ b/plugins/auth-backend/src/providers/microsoft/index.ts @@ -15,3 +15,4 @@ */ export { createMicrosoftProvider } from './provider'; +export type { MicrosoftProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index b0c3635be8..57f2ffcbd5 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -205,32 +205,33 @@ export class MicrosoftAuthProvider implements OAuthHandlers { } } -export const createMicrosoftProvider: AuthProviderFactory = ({ - providerId, - globalConfig, - config, - tokenIssuer, -}) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const tenantID = envConfig.getString('tenantId'); +export type MicrosoftProviderOptions = {}; - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const authorizationUrl = `https://login.microsoftonline.com/${tenantID}/oauth2/v2.0/authorize`; - const tokenUrl = `https://login.microsoftonline.com/${tenantID}/oauth2/v2.0/token`; +export const createMicrosoftProvider = ( + _options?: MicrosoftProviderOptions, +): AuthProviderFactory => { + return ({ providerId, globalConfig, config, tokenIssuer }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const tenantID = envConfig.getString('tenantId'); - const provider = new MicrosoftAuthProvider({ - clientId, - clientSecret, - callbackUrl, - authorizationUrl, - tokenUrl, + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const authorizationUrl = `https://login.microsoftonline.com/${tenantID}/oauth2/v2.0/authorize`; + const tokenUrl = `https://login.microsoftonline.com/${tenantID}/oauth2/v2.0/token`; + + const provider = new MicrosoftAuthProvider({ + clientId, + clientSecret, + callbackUrl, + authorizationUrl, + tokenUrl, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); }); - - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: false, - providerId, - tokenIssuer, - }); - }); +}; diff --git a/plugins/auth-backend/src/providers/oauth2/index.ts b/plugins/auth-backend/src/providers/oauth2/index.ts index e607aa10e1..97848f4319 100644 --- a/plugins/auth-backend/src/providers/oauth2/index.ts +++ b/plugins/auth-backend/src/providers/oauth2/index.ts @@ -15,3 +15,4 @@ */ export { createOAuth2Provider } from './provider'; +export type { OAuth2ProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index a44c4fb50b..93f9fad2aa 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -158,32 +158,33 @@ export class OAuth2AuthProvider implements OAuthHandlers { } } -export const createOAuth2Provider: AuthProviderFactory = ({ - providerId, - globalConfig, - config, - tokenIssuer, -}) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const authorizationUrl = envConfig.getString('authorizationUrl'); - const tokenUrl = envConfig.getString('tokenUrl'); - const scope = envConfig.getOptionalString('scope'); +export type OAuth2ProviderOptions = {}; - const provider = new OAuth2AuthProvider({ - clientId, - clientSecret, - callbackUrl, - authorizationUrl, - tokenUrl, - scope, - }); +export const createOAuth2Provider = ( + _options?: OAuth2ProviderOptions, +): AuthProviderFactory => { + return ({ providerId, globalConfig, config, tokenIssuer }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const authorizationUrl = envConfig.getString('authorizationUrl'); + const tokenUrl = envConfig.getString('tokenUrl'); + const scope = envConfig.getOptionalString('scope'); - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: false, - providerId, - tokenIssuer, + const provider = new OAuth2AuthProvider({ + clientId, + clientSecret, + callbackUrl, + authorizationUrl, + tokenUrl, + scope, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); }); - }); +}; diff --git a/plugins/auth-backend/src/providers/oidc/index.ts b/plugins/auth-backend/src/providers/oidc/index.ts index acacbde73d..63c530acfc 100644 --- a/plugins/auth-backend/src/providers/oidc/index.ts +++ b/plugins/auth-backend/src/providers/oidc/index.ts @@ -15,3 +15,4 @@ */ export { createOidcProvider } from './provider'; +export type { OidcProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/oidc/provider.test.ts b/plugins/auth-backend/src/providers/oidc/provider.test.ts index fcbe9d7abc..a746c0573b 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.test.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.test.ts @@ -121,7 +121,7 @@ describe('OidcAuthProvider', () => { })), } as any) as Config, } as AuthProviderFactoryOptions; - const provider = createOidcProvider(options) as OAuthAdapter; + const provider = createOidcProvider()(options) as OAuthAdapter; expect(provider.start).toBeDefined(); await new Promise(resolve => process.nextTick(resolve)); // advance a tick to give nock a chance to intercept the request expect(scope.isDone()).toBeTruthy(); diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index 6ea7b8a8d3..b1bedd0f54 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -48,7 +48,7 @@ type OidcImpl = { client: Client; }; -export type OidcAuthProviderOptions = OAuthProviderOptions & { +export type Options = OAuthProviderOptions & { metadataUrl: string; tokenSignedResponseAlg?: string; }; @@ -56,7 +56,7 @@ export type OidcAuthProviderOptions = OAuthProviderOptions & { export class OidcAuthProvider implements OAuthHandlers { private readonly implementation: Promise; - constructor(options: OidcAuthProviderOptions) { + constructor(options: Options) { this.implementation = this.setupStrategy(options); } @@ -105,9 +105,7 @@ export class OidcAuthProvider implements OAuthHandlers { }); } - private async setupStrategy( - options: OidcAuthProviderOptions, - ): Promise { + private async setupStrategy(options: Options): Promise { const issuer = await Issuer.discover(options.metadataUrl); const client = new issuer.Client({ client_id: options.clientId, @@ -170,32 +168,33 @@ export class OidcAuthProvider implements OAuthHandlers { } } -export const createOidcProvider: AuthProviderFactory = ({ - providerId, - globalConfig, - config, - tokenIssuer, -}) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const metadataUrl = envConfig.getString('metadataUrl'); - const tokenSignedResponseAlg = envConfig.getString( - 'tokenSignedResponseAlg', - ); +export type OidcProviderOptions = {}; - const provider = new OidcAuthProvider({ - clientId, - clientSecret, - callbackUrl, - tokenSignedResponseAlg, - metadataUrl, - }); +export const createOidcProvider = ( + _options?: OidcProviderOptions, +): AuthProviderFactory => { + return ({ providerId, globalConfig, config, tokenIssuer }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const metadataUrl = envConfig.getString('metadataUrl'); + const tokenSignedResponseAlg = envConfig.getString( + 'tokenSignedResponseAlg', + ); - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: false, - providerId, - tokenIssuer, + const provider = new OidcAuthProvider({ + clientId, + clientSecret, + callbackUrl, + tokenSignedResponseAlg, + metadataUrl, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); }); - }); +}; diff --git a/plugins/auth-backend/src/providers/okta/index.ts b/plugins/auth-backend/src/providers/okta/index.ts index 05cc398f43..47a2549587 100644 --- a/plugins/auth-backend/src/providers/okta/index.ts +++ b/plugins/auth-backend/src/providers/okta/index.ts @@ -13,4 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { createOktaProvider } from './provider'; +export type { OktaProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 84b258e489..ba2c594550 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -167,28 +167,29 @@ export class OktaAuthProvider implements OAuthHandlers { } } -export const createOktaProvider: AuthProviderFactory = ({ - providerId, - globalConfig, - config, - tokenIssuer, -}) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const audience = envConfig.getString('audience'); - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; +export type OktaProviderOptions = {}; - const provider = new OktaAuthProvider({ - audience, - clientId, - clientSecret, - callbackUrl, - }); +export const createOktaProvider = ( + _options?: OktaProviderOptions, +): AuthProviderFactory => { + return ({ providerId, globalConfig, config, tokenIssuer }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const audience = envConfig.getString('audience'); + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: false, - providerId, - tokenIssuer, + const provider = new OktaAuthProvider({ + audience, + clientId, + clientSecret, + callbackUrl, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); }); - }); +}; diff --git a/plugins/auth-backend/src/providers/onelogin/index.ts b/plugins/auth-backend/src/providers/onelogin/index.ts index 79af226cd1..60fd2ffca4 100644 --- a/plugins/auth-backend/src/providers/onelogin/index.ts +++ b/plugins/auth-backend/src/providers/onelogin/index.ts @@ -15,3 +15,4 @@ */ export { createOneLoginProvider } from './provider'; +export type { OneLoginProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index 838b655d59..d23a48776e 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -41,14 +41,14 @@ type PrivateInfo = { refreshToken: string; }; -export type OneLoginProviderOptions = OAuthProviderOptions & { +export type Options = OAuthProviderOptions & { issuer: string; }; export class OneLoginProvider implements OAuthHandlers { private readonly _strategy: any; - constructor(options: OneLoginProviderOptions) { + constructor(options: Options) { this._strategy = new OneLoginStrategy( { issuer: options.issuer, @@ -146,28 +146,29 @@ export class OneLoginProvider implements OAuthHandlers { } } -export const createOneLoginProvider: AuthProviderFactory = ({ - providerId, - globalConfig, - config, - tokenIssuer, -}) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const issuer = envConfig.getString('issuer'); - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; +export type OneLoginProviderOptions = {}; - const provider = new OneLoginProvider({ - clientId, - clientSecret, - callbackUrl, - issuer, - }); +export const createOneLoginProvider = ( + _options?: OneLoginProviderOptions, +): AuthProviderFactory => { + return ({ providerId, globalConfig, config, tokenIssuer }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const issuer = envConfig.getString('issuer'); + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: false, - providerId, - tokenIssuer, + const provider = new OneLoginProvider({ + clientId, + clientSecret, + callbackUrl, + issuer, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); }); - }); +}; diff --git a/plugins/auth-backend/src/providers/saml/index.ts b/plugins/auth-backend/src/providers/saml/index.ts index 582deb1608..597b20ee8d 100644 --- a/plugins/auth-backend/src/providers/saml/index.ts +++ b/plugins/auth-backend/src/providers/saml/index.ts @@ -15,3 +15,4 @@ */ export { createSamlProvider } from './provider'; +export type { SamlProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 74541d8294..caeb86aa6b 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -39,12 +39,17 @@ type SamlInfo = { profile: ProfileInfo; }; +type Options = SamlConfig & { + tokenIssuer: TokenIssuer; + appUrl: string; +}; + export class SamlAuthProvider implements AuthProviderRouteHandlers { private readonly strategy: SamlStrategy; private readonly tokenIssuer: TokenIssuer; private readonly appUrl: string; - constructor(options: SAMLProviderOptions) { + constructor(options: Options) { this.appUrl = options.appUrl; this.tokenIssuer = options.tokenIssuer; this.strategy = new SamlStrategy({ ...options }, (( @@ -113,40 +118,36 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { } } -type SAMLProviderOptions = SamlConfig & { - tokenIssuer: TokenIssuer; - appUrl: string; -}; - type SignatureAlgorithm = 'sha1' | 'sha256' | 'sha512'; -export const createSamlProvider: AuthProviderFactory = ({ - providerId, - globalConfig, - config, - tokenIssuer, -}) => { - const opts = { - callbackUrl: `${globalConfig.baseUrl}/${providerId}/handler/frame`, - entryPoint: config.getString('entryPoint'), - logoutUrl: config.getOptionalString('logoutUrl'), - issuer: config.getString('issuer'), - cert: config.getOptionalString('cert'), - privateCert: config.getOptionalString('privateKey'), - decryptionPvk: config.getOptionalString('decryptionPvk'), - signatureAlgorithm: config.getOptionalString('signatureAlgorithm') as - | SignatureAlgorithm - | undefined, - digestAlgorithm: config.getOptionalString('digestAlgorithm'), +export type SamlProviderOptions = {}; - tokenIssuer, - appUrl: globalConfig.appUrl, +export const createSamlProvider = ( + _options?: SamlProviderOptions, +): AuthProviderFactory => { + return ({ providerId, globalConfig, config, tokenIssuer }) => { + const opts = { + callbackUrl: `${globalConfig.baseUrl}/${providerId}/handler/frame`, + entryPoint: config.getString('entryPoint'), + logoutUrl: config.getOptionalString('logoutUrl'), + issuer: config.getString('issuer'), + cert: config.getOptionalString('cert'), + privateCert: config.getOptionalString('privateKey'), + decryptionPvk: config.getOptionalString('decryptionPvk'), + signatureAlgorithm: config.getOptionalString('signatureAlgorithm') as + | SignatureAlgorithm + | undefined, + digestAlgorithm: config.getOptionalString('digestAlgorithm'), + + tokenIssuer, + appUrl: globalConfig.appUrl, + }; + + // passport-saml will return an error if the `cert` key is set, and the value is empty. + // Since we read from config (such as environment variables) an empty string should be equal to being unset. + if (!opts.cert) { + delete opts.cert; + } + return new SamlAuthProvider(opts); }; - - // passport-saml will return an error if the `cert` key is set, and the value is empty. - // Since we read from config (such as environment variables) an empty string should be equal to being unset. - if (!opts.cert) { - delete opts.cert; - } - return new SamlAuthProvider(opts); }; From 5a5ad7a26a4ec4bfc11aeb7db39d4f267d5aae03 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 12 Feb 2021 14:39:35 +0100 Subject: [PATCH 10/34] fix: lerna dependencies are also in the create-app part --- package.json | 2 +- .../templates/default-app/package.json.hbs | 2 +- yarn.lock | 2450 ++++++++--------- 3 files changed, 1177 insertions(+), 1277 deletions(-) diff --git a/package.json b/package.json index ad0ce2cd57..59fe9d8f18 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "concurrently": "^5.2.0", "fs-extra": "^9.0.0", "husky": "^4.2.3", - "lerna": "^3.20.2", + "lerna": "^4.0.0", "lint-staged": "^10.1.0", "prettier": "^2.0.5", "recursive-readdir": "^2.2.2", diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index 3fa350fa7c..58fb20ba05 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -30,7 +30,7 @@ "devDependencies": { "@backstage/cli": "^{{version '@backstage/cli'}}", "@spotify/prettier-config": "^7.0.0", - "lerna": "^3.20.2", + "lerna": "^4.0.0", "prettier": "^1.19.1" }, "prettier": "@spotify/prettier-config", diff --git a/yarn.lock b/yarn.lock index 070eb8b7f1..06e551dd7f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2299,80 +2299,6 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" -"@evocateur/libnpmaccess@^3.1.2": - version "3.1.2" - resolved "https://registry.npmjs.org/@evocateur/libnpmaccess/-/libnpmaccess-3.1.2.tgz#ecf7f6ce6b004e9f942b098d92200be4a4b1c845" - integrity sha512-KSCAHwNWro0CF2ukxufCitT9K5LjL/KuMmNzSu8wuwN2rjyKHD8+cmOsiybK+W5hdnwc5M1SmRlVCaMHQo+3rg== - dependencies: - "@evocateur/npm-registry-fetch" "^4.0.0" - aproba "^2.0.0" - figgy-pudding "^3.5.1" - get-stream "^4.0.0" - npm-package-arg "^6.1.0" - -"@evocateur/libnpmpublish@^1.2.2": - version "1.2.2" - resolved "https://registry.npmjs.org/@evocateur/libnpmpublish/-/libnpmpublish-1.2.2.tgz#55df09d2dca136afba9c88c759ca272198db9f1a" - integrity sha512-MJrrk9ct1FeY9zRlyeoyMieBjGDG9ihyyD9/Ft6MMrTxql9NyoEx2hw9casTIP4CdqEVu+3nQ2nXxoJ8RCXyFg== - dependencies: - "@evocateur/npm-registry-fetch" "^4.0.0" - aproba "^2.0.0" - figgy-pudding "^3.5.1" - get-stream "^4.0.0" - lodash.clonedeep "^4.5.0" - normalize-package-data "^2.4.0" - npm-package-arg "^6.1.0" - semver "^5.5.1" - ssri "^6.0.1" - -"@evocateur/npm-registry-fetch@^4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@evocateur/npm-registry-fetch/-/npm-registry-fetch-4.0.0.tgz#8c4c38766d8d32d3200fcb0a83f064b57365ed66" - integrity sha512-k1WGfKRQyhJpIr+P17O5vLIo2ko1PFLKwoetatdduUSt/aQ4J2sJrJwwatdI5Z3SiYk/mRH9S3JpdmMFd/IK4g== - dependencies: - JSONStream "^1.3.4" - bluebird "^3.5.1" - figgy-pudding "^3.4.1" - lru-cache "^5.1.1" - make-fetch-happen "^5.0.0" - npm-package-arg "^6.1.0" - safe-buffer "^5.1.2" - -"@evocateur/pacote@^9.6.3": - version "9.6.5" - resolved "https://registry.npmjs.org/@evocateur/pacote/-/pacote-9.6.5.tgz#33de32ba210b6f17c20ebab4d497efc6755f4ae5" - integrity sha512-EI552lf0aG2nOV8NnZpTxNo2PcXKPmDbF9K8eCBFQdIZwHNGN/mi815fxtmUMa2wTa1yndotICIDt/V0vpEx2w== - dependencies: - "@evocateur/npm-registry-fetch" "^4.0.0" - bluebird "^3.5.3" - cacache "^12.0.3" - chownr "^1.1.2" - figgy-pudding "^3.5.1" - get-stream "^4.1.0" - glob "^7.1.4" - infer-owner "^1.0.4" - lru-cache "^5.1.1" - make-fetch-happen "^5.0.0" - minimatch "^3.0.4" - minipass "^2.3.5" - mississippi "^3.0.0" - mkdirp "^0.5.1" - normalize-package-data "^2.5.0" - npm-package-arg "^6.1.0" - npm-packlist "^1.4.4" - npm-pick-manifest "^3.0.0" - osenv "^0.1.5" - promise-inflight "^1.0.1" - promise-retry "^1.1.1" - protoduck "^5.0.1" - rimraf "^2.6.3" - safe-buffer "^5.2.0" - semver "^5.7.0" - ssri "^6.0.1" - tar "^4.4.10" - unique-filename "^1.1.1" - which "^1.3.1" - "@fmvilas/pseudo-yaml-ast@^0.3.1": version "0.3.1" resolved "https://registry.npmjs.org/@fmvilas/pseudo-yaml-ast/-/pseudo-yaml-ast-0.3.1.tgz#66c5df2c2d76ba8571dc5bd98fda4d7dce6121de" @@ -3225,461 +3151,443 @@ dependencies: stream "^0.0.2" -"@lerna/add@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/add/-/add-3.21.0.tgz#27007bde71cc7b0a2969ab3c2f0ae41578b4577b" - integrity sha512-vhUXXF6SpufBE1EkNEXwz1VLW03f177G9uMOFMQkp6OJ30/PWg4Ekifuz9/3YfgB2/GH8Tu4Lk3O51P2Hskg/A== +"@lerna/add@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/add/-/add-4.0.0.tgz#c36f57d132502a57b9e7058d1548b7a565ef183f" + integrity sha512-cpmAH1iS3k8JBxNvnMqrGTTjbY/ZAiKa1ChJzFevMYY3eeqbvhsBKnBcxjRXtdrJ6bd3dCQM+ZtK+0i682Fhng== dependencies: - "@evocateur/pacote" "^9.6.3" - "@lerna/bootstrap" "3.21.0" - "@lerna/command" "3.21.0" - "@lerna/filter-options" "3.20.0" - "@lerna/npm-conf" "3.16.0" - "@lerna/validation-error" "3.13.0" + "@lerna/bootstrap" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/npm-conf" "4.0.0" + "@lerna/validation-error" "4.0.0" dedent "^0.7.0" - npm-package-arg "^6.1.0" - p-map "^2.1.0" - semver "^6.2.0" + npm-package-arg "^8.1.0" + p-map "^4.0.0" + pacote "^11.2.6" + semver "^7.3.4" -"@lerna/bootstrap@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/bootstrap/-/bootstrap-3.21.0.tgz#bcd1b651be5b0970b20d8fae04c864548123aed6" - integrity sha512-mtNHlXpmvJn6JTu0KcuTTPl2jLsDNud0QacV/h++qsaKbhAaJr/FElNZ5s7MwZFUM3XaDmvWzHKaszeBMHIbBw== +"@lerna/bootstrap@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/bootstrap/-/bootstrap-4.0.0.tgz#5f5c5e2c6cfc8fcec50cb2fbe569a8c607101891" + integrity sha512-RkS7UbeM2vu+kJnHzxNRCLvoOP9yGNgkzRdy4UV2hNalD7EP41bLvRVOwRYQ7fhc2QcbhnKNdOBihYRL0LcKtw== dependencies: - "@lerna/command" "3.21.0" - "@lerna/filter-options" "3.20.0" - "@lerna/has-npm-version" "3.16.5" - "@lerna/npm-install" "3.16.5" - "@lerna/package-graph" "3.18.5" - "@lerna/pulse-till-done" "3.13.0" - "@lerna/rimraf-dir" "3.16.5" - "@lerna/run-lifecycle" "3.16.2" - "@lerna/run-topologically" "3.18.5" - "@lerna/symlink-binary" "3.17.0" - "@lerna/symlink-dependencies" "3.17.0" - "@lerna/validation-error" "3.13.0" + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/has-npm-version" "4.0.0" + "@lerna/npm-install" "4.0.0" + "@lerna/package-graph" "4.0.0" + "@lerna/pulse-till-done" "4.0.0" + "@lerna/rimraf-dir" "4.0.0" + "@lerna/run-lifecycle" "4.0.0" + "@lerna/run-topologically" "4.0.0" + "@lerna/symlink-binary" "4.0.0" + "@lerna/symlink-dependencies" "4.0.0" + "@lerna/validation-error" "4.0.0" dedent "^0.7.0" - get-port "^4.2.0" - multimatch "^3.0.0" - npm-package-arg "^6.1.0" + get-port "^5.1.1" + multimatch "^5.0.0" + npm-package-arg "^8.1.0" npmlog "^4.1.2" - p-finally "^1.0.0" - p-map "^2.1.0" - p-map-series "^1.0.0" - p-waterfall "^1.0.0" - read-package-tree "^5.1.6" - semver "^6.2.0" + p-map "^4.0.0" + p-map-series "^2.1.0" + p-waterfall "^2.1.1" + read-package-tree "^5.3.1" + semver "^7.3.4" -"@lerna/changed@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/changed/-/changed-3.21.0.tgz#108e15f679bfe077af500f58248c634f1044ea0b" - integrity sha512-hzqoyf8MSHVjZp0gfJ7G8jaz+++mgXYiNs9iViQGA8JlN/dnWLI5sWDptEH3/B30Izo+fdVz0S0s7ydVE3pWIw== +"@lerna/changed@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/changed/-/changed-4.0.0.tgz#b9fc76cea39b9292a6cd263f03eb57af85c9270b" + integrity sha512-cD+KuPRp6qiPOD+BO6S6SN5cARspIaWSOqGBpGnYzLb4uWT8Vk4JzKyYtc8ym1DIwyoFXHosXt8+GDAgR8QrgQ== dependencies: - "@lerna/collect-updates" "3.20.0" - "@lerna/command" "3.21.0" - "@lerna/listable" "3.18.5" - "@lerna/output" "3.13.0" + "@lerna/collect-updates" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/listable" "4.0.0" + "@lerna/output" "4.0.0" -"@lerna/check-working-tree@3.16.5": - version "3.16.5" - resolved "https://registry.npmjs.org/@lerna/check-working-tree/-/check-working-tree-3.16.5.tgz#b4f8ae61bb4523561dfb9f8f8d874dd46bb44baa" - integrity sha512-xWjVBcuhvB8+UmCSb5tKVLB5OuzSpw96WEhS2uz6hkWVa/Euh1A0/HJwn2cemyK47wUrCQXtczBUiqnq9yX5VQ== +"@lerna/check-working-tree@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/check-working-tree/-/check-working-tree-4.0.0.tgz#257e36a602c00142e76082a19358e3e1ae8dbd58" + integrity sha512-/++bxM43jYJCshBiKP5cRlCTwSJdRSxVmcDAXM+1oUewlZJVSVlnks5eO0uLxokVFvLhHlC5kHMc7gbVFPHv6Q== dependencies: - "@lerna/collect-uncommitted" "3.16.5" - "@lerna/describe-ref" "3.16.5" - "@lerna/validation-error" "3.13.0" + "@lerna/collect-uncommitted" "4.0.0" + "@lerna/describe-ref" "4.0.0" + "@lerna/validation-error" "4.0.0" -"@lerna/child-process@3.16.5": - version "3.16.5" - resolved "https://registry.npmjs.org/@lerna/child-process/-/child-process-3.16.5.tgz#38fa3c18064aa4ac0754ad80114776a7b36a69b2" - integrity sha512-vdcI7mzei9ERRV4oO8Y1LHBZ3A5+ampRKg1wq5nutLsUA4mEBN6H7JqjWOMY9xZemv6+kATm2ofjJ3lW5TszQg== +"@lerna/child-process@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/child-process/-/child-process-4.0.0.tgz#341b96a57dffbd9705646d316e231df6fa4df6e1" + integrity sha512-XtCnmCT9eyVsUUHx6y/CTBYdV9g2Cr/VxyseTWBgfIur92/YKClfEtJTbOh94jRT62hlKLqSvux/UhxXVh613Q== dependencies: - chalk "^2.3.1" - execa "^1.0.0" - strong-log-transformer "^2.0.0" + chalk "^4.1.0" + execa "^5.0.0" + strong-log-transformer "^2.1.0" -"@lerna/clean@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/clean/-/clean-3.21.0.tgz#c0b46b5300cc3dae2cda3bec14b803082da3856d" - integrity sha512-b/L9l+MDgE/7oGbrav6rG8RTQvRiZLO1zTcG17zgJAAuhlsPxJExMlh2DFwJEVi2les70vMhHfST3Ue1IMMjpg== +"@lerna/clean@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/clean/-/clean-4.0.0.tgz#8f778b6f2617aa2a936a6b5e085ae62498e57dc5" + integrity sha512-uugG2iN9k45ITx2jtd8nEOoAtca8hNlDCUM0N3lFgU/b1mEQYAPRkqr1qs4FLRl/Y50ZJ41wUz1eazS+d/0osA== dependencies: - "@lerna/command" "3.21.0" - "@lerna/filter-options" "3.20.0" - "@lerna/prompt" "3.18.5" - "@lerna/pulse-till-done" "3.13.0" - "@lerna/rimraf-dir" "3.16.5" - p-map "^2.1.0" - p-map-series "^1.0.0" - p-waterfall "^1.0.0" + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/prompt" "4.0.0" + "@lerna/pulse-till-done" "4.0.0" + "@lerna/rimraf-dir" "4.0.0" + p-map "^4.0.0" + p-map-series "^2.1.0" + p-waterfall "^2.1.1" -"@lerna/cli@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/cli/-/cli-3.18.5.tgz#c90c461542fcd35b6d5b015a290fb0dbfb41d242" - integrity sha512-erkbxkj9jfc89vVs/jBLY/fM0I80oLmJkFUV3Q3wk9J3miYhP14zgVEBsPZY68IZlEjT6T3Xlq2xO1AVaatHsA== +"@lerna/cli@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/cli/-/cli-4.0.0.tgz#8eabd334558836c1664df23f19acb95e98b5bbf3" + integrity sha512-Neaw3GzFrwZiRZv2g7g6NwFjs3er1vhraIniEs0jjVLPMNC4eata0na3GfE5yibkM/9d3gZdmihhZdZ3EBdvYA== dependencies: - "@lerna/global-options" "3.13.0" + "@lerna/global-options" "4.0.0" dedent "^0.7.0" npmlog "^4.1.2" - yargs "^14.2.2" + yargs "^16.2.0" -"@lerna/collect-uncommitted@3.16.5": - version "3.16.5" - resolved "https://registry.npmjs.org/@lerna/collect-uncommitted/-/collect-uncommitted-3.16.5.tgz#a494d61aac31cdc7aec4bbe52c96550274132e63" - integrity sha512-ZgqnGwpDZiWyzIQVZtQaj9tRizsL4dUOhuOStWgTAw1EMe47cvAY2kL709DzxFhjr6JpJSjXV5rZEAeU3VE0Hg== +"@lerna/collect-uncommitted@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/collect-uncommitted/-/collect-uncommitted-4.0.0.tgz#855cd64612969371cfc2453b90593053ff1ba779" + integrity sha512-ufSTfHZzbx69YNj7KXQ3o66V4RC76ffOjwLX0q/ab//61bObJ41n03SiQEhSlmpP+gmFbTJ3/7pTe04AHX9m/g== dependencies: - "@lerna/child-process" "3.16.5" - chalk "^2.3.1" - figgy-pudding "^3.5.1" + "@lerna/child-process" "4.0.0" + chalk "^4.1.0" npmlog "^4.1.2" -"@lerna/collect-updates@3.20.0": - version "3.20.0" - resolved "https://registry.npmjs.org/@lerna/collect-updates/-/collect-updates-3.20.0.tgz#62f9d76ba21a25b7d9fbf31c02de88744a564bd1" - integrity sha512-qBTVT5g4fupVhBFuY4nI/3FSJtQVcDh7/gEPOpRxoXB/yCSnT38MFHXWl+y4einLciCjt/+0x6/4AG80fjay2Q== +"@lerna/collect-updates@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/collect-updates/-/collect-updates-4.0.0.tgz#8e208b1bafd98a372ff1177f7a5e288f6bea8041" + integrity sha512-bnNGpaj4zuxsEkyaCZLka9s7nMs58uZoxrRIPJ+nrmrZYp1V5rrd+7/NYTuunOhY2ug1sTBvTAxj3NZQ+JKnOw== dependencies: - "@lerna/child-process" "3.16.5" - "@lerna/describe-ref" "3.16.5" + "@lerna/child-process" "4.0.0" + "@lerna/describe-ref" "4.0.0" minimatch "^3.0.4" npmlog "^4.1.2" - slash "^2.0.0" + slash "^3.0.0" -"@lerna/command@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/command/-/command-3.21.0.tgz#9a2383759dc7b700dacfa8a22b2f3a6e190121f7" - integrity sha512-T2bu6R8R3KkH5YoCKdutKv123iUgUbW8efVjdGCDnCMthAQzoentOJfDeodBwn0P2OqCl3ohsiNVtSn9h78fyQ== +"@lerna/command@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/command/-/command-4.0.0.tgz#991c7971df8f5bf6ae6e42c808869a55361c1b98" + integrity sha512-LM9g3rt5FsPNFqIHUeRwWXLNHJ5NKzOwmVKZ8anSp4e1SPrv2HNc1V02/9QyDDZK/w+5POXH5lxZUI1CHaOK/A== dependencies: - "@lerna/child-process" "3.16.5" - "@lerna/package-graph" "3.18.5" - "@lerna/project" "3.21.0" - "@lerna/validation-error" "3.13.0" - "@lerna/write-log-file" "3.13.0" + "@lerna/child-process" "4.0.0" + "@lerna/package-graph" "4.0.0" + "@lerna/project" "4.0.0" + "@lerna/validation-error" "4.0.0" + "@lerna/write-log-file" "4.0.0" clone-deep "^4.0.1" dedent "^0.7.0" - execa "^1.0.0" + execa "^5.0.0" is-ci "^2.0.0" npmlog "^4.1.2" -"@lerna/conventional-commits@3.22.0": - version "3.22.0" - resolved "https://registry.npmjs.org/@lerna/conventional-commits/-/conventional-commits-3.22.0.tgz#2798f4881ee2ef457bdae027ab7d0bf0af6f1e09" - integrity sha512-z4ZZk1e8Mhz7+IS8NxHr64wyklHctCJyWpJKEZZPJiLFJ8yKto/x38O80R10pIzC0rr8Sy/OsjSH4bl0TbbgqA== +"@lerna/conventional-commits@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/conventional-commits/-/conventional-commits-4.0.0.tgz#660fb2c7b718cb942ead70110df61f18c6f99750" + integrity sha512-CSUQRjJHFrH8eBn7+wegZLV3OrNc0Y1FehYfYGhjLE2SIfpCL4bmfu/ViYuHh9YjwHaA+4SX6d3hR+xkeseKmw== dependencies: - "@lerna/validation-error" "3.13.0" - conventional-changelog-angular "^5.0.3" - conventional-changelog-core "^3.1.6" - conventional-recommended-bump "^5.0.0" - fs-extra "^8.1.0" - get-stream "^4.0.0" + "@lerna/validation-error" "4.0.0" + conventional-changelog-angular "^5.0.12" + conventional-changelog-core "^4.2.2" + conventional-recommended-bump "^6.1.0" + fs-extra "^9.1.0" + get-stream "^6.0.0" lodash.template "^4.5.0" - npm-package-arg "^6.1.0" + npm-package-arg "^8.1.0" npmlog "^4.1.2" - pify "^4.0.1" - semver "^6.2.0" + pify "^5.0.0" + semver "^7.3.4" -"@lerna/create-symlink@3.16.2": - version "3.16.2" - resolved "https://registry.npmjs.org/@lerna/create-symlink/-/create-symlink-3.16.2.tgz#412cb8e59a72f5a7d9463e4e4721ad2070149967" - integrity sha512-pzXIJp6av15P325sgiIRpsPXLFmkisLhMBCy4764d+7yjf2bzrJ4gkWVMhsv4AdF0NN3OyZ5jjzzTtLNqfR+Jw== +"@lerna/create-symlink@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/create-symlink/-/create-symlink-4.0.0.tgz#8c5317ce5ae89f67825443bd7651bf4121786228" + integrity sha512-I0phtKJJdafUiDwm7BBlEUOtogmu8+taxq6PtIrxZbllV9hWg59qkpuIsiFp+no7nfRVuaasNYHwNUhDAVQBig== dependencies: - "@zkochan/cmd-shim" "^3.1.0" - fs-extra "^8.1.0" + cmd-shim "^4.1.0" + fs-extra "^9.1.0" npmlog "^4.1.2" -"@lerna/create@3.22.0": - version "3.22.0" - resolved "https://registry.npmjs.org/@lerna/create/-/create-3.22.0.tgz#d6bbd037c3dc5b425fe5f6d1b817057c278f7619" - integrity sha512-MdiQQzCcB4E9fBF1TyMOaAEz9lUjIHp1Ju9H7f3lXze5JK6Fl5NYkouAvsLgY6YSIhXMY8AHW2zzXeBDY4yWkw== +"@lerna/create@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/create/-/create-4.0.0.tgz#b6947e9b5dfb6530321952998948c3e63d64d730" + integrity sha512-mVOB1niKByEUfxlbKTM1UNECWAjwUdiioIbRQZEeEabtjCL69r9rscIsjlGyhGWCfsdAG5wfq4t47nlDXdLLag== dependencies: - "@evocateur/pacote" "^9.6.3" - "@lerna/child-process" "3.16.5" - "@lerna/command" "3.21.0" - "@lerna/npm-conf" "3.16.0" - "@lerna/validation-error" "3.13.0" - camelcase "^5.0.0" + "@lerna/child-process" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/npm-conf" "4.0.0" + "@lerna/validation-error" "4.0.0" dedent "^0.7.0" - fs-extra "^8.1.0" - globby "^9.2.0" - init-package-json "^1.10.3" - npm-package-arg "^6.1.0" - p-reduce "^1.0.0" - pify "^4.0.1" - semver "^6.2.0" - slash "^2.0.0" - validate-npm-package-license "^3.0.3" + fs-extra "^9.1.0" + globby "^11.0.2" + init-package-json "^2.0.2" + npm-package-arg "^8.1.0" + p-reduce "^2.1.0" + pacote "^11.2.6" + pify "^5.0.0" + semver "^7.3.4" + slash "^3.0.0" + validate-npm-package-license "^3.0.4" validate-npm-package-name "^3.0.0" - whatwg-url "^7.0.0" + whatwg-url "^8.4.0" + yargs-parser "20.2.4" -"@lerna/describe-ref@3.16.5": - version "3.16.5" - resolved "https://registry.npmjs.org/@lerna/describe-ref/-/describe-ref-3.16.5.tgz#a338c25aaed837d3dc70b8a72c447c5c66346ac0" - integrity sha512-c01+4gUF0saOOtDBzbLMFOTJDHTKbDFNErEY6q6i9QaXuzy9LNN62z+Hw4acAAZuJQhrVWncVathcmkkjvSVGw== +"@lerna/describe-ref@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/describe-ref/-/describe-ref-4.0.0.tgz#53c53b4ea65fdceffa072a62bfebe6772c45d9ec" + integrity sha512-eTU5+xC4C5Gcgz+Ey4Qiw9nV2B4JJbMulsYJMW8QjGcGh8zudib7Sduj6urgZXUYNyhYpRs+teci9M2J8u+UvQ== dependencies: - "@lerna/child-process" "3.16.5" + "@lerna/child-process" "4.0.0" npmlog "^4.1.2" -"@lerna/diff@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/diff/-/diff-3.21.0.tgz#e6df0d8b9916167ff5a49fcb02ac06424280a68d" - integrity sha512-5viTR33QV3S7O+bjruo1SaR40m7F2aUHJaDAC7fL9Ca6xji+aw1KFkpCtVlISS0G8vikUREGMJh+c/VMSc8Usw== +"@lerna/diff@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/diff/-/diff-4.0.0.tgz#6d3071817aaa4205a07bf77cfc6e932796d48b92" + integrity sha512-jYPKprQVg41+MUMxx6cwtqsNm0Yxx9GDEwdiPLwcUTFx+/qKCEwifKNJ1oGIPBxyEHX2PFCOjkK39lHoj2qiag== dependencies: - "@lerna/child-process" "3.16.5" - "@lerna/command" "3.21.0" - "@lerna/validation-error" "3.13.0" + "@lerna/child-process" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/validation-error" "4.0.0" npmlog "^4.1.2" -"@lerna/exec@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/exec/-/exec-3.21.0.tgz#17f07533893cb918a17b41bcc566dc437016db26" - integrity sha512-iLvDBrIE6rpdd4GIKTY9mkXyhwsJ2RvQdB9ZU+/NhR3okXfqKc6py/24tV111jqpXTtZUW6HNydT4dMao2hi1Q== +"@lerna/exec@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/exec/-/exec-4.0.0.tgz#eb6cb95cb92d42590e9e2d628fcaf4719d4a8be6" + integrity sha512-VGXtL/b/JfY84NB98VWZpIExfhLOzy0ozm/0XaS4a2SmkAJc5CeUfrhvHxxkxiTBLkU+iVQUyYEoAT0ulQ8PCw== dependencies: - "@lerna/child-process" "3.16.5" - "@lerna/command" "3.21.0" - "@lerna/filter-options" "3.20.0" - "@lerna/profiler" "3.20.0" - "@lerna/run-topologically" "3.18.5" - "@lerna/validation-error" "3.13.0" - p-map "^2.1.0" + "@lerna/child-process" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/profiler" "4.0.0" + "@lerna/run-topologically" "4.0.0" + "@lerna/validation-error" "4.0.0" + p-map "^4.0.0" -"@lerna/filter-options@3.20.0": - version "3.20.0" - resolved "https://registry.npmjs.org/@lerna/filter-options/-/filter-options-3.20.0.tgz#0f0f5d5a4783856eece4204708cc902cbc8af59b" - integrity sha512-bmcHtvxn7SIl/R9gpiNMVG7yjx7WyT0HSGw34YVZ9B+3xF/83N3r5Rgtjh4hheLZ+Q91Or0Jyu5O3Nr+AwZe2g== +"@lerna/filter-options@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/filter-options/-/filter-options-4.0.0.tgz#ac94cc515d7fa3b47e2f7d74deddeabb1de5e9e6" + integrity sha512-vV2ANOeZhOqM0rzXnYcFFCJ/kBWy/3OA58irXih9AMTAlQLymWAK0akWybl++sUJ4HB9Hx12TOqaXbYS2NM5uw== dependencies: - "@lerna/collect-updates" "3.20.0" - "@lerna/filter-packages" "3.18.0" + "@lerna/collect-updates" "4.0.0" + "@lerna/filter-packages" "4.0.0" dedent "^0.7.0" - figgy-pudding "^3.5.1" npmlog "^4.1.2" -"@lerna/filter-packages@3.18.0": - version "3.18.0" - resolved "https://registry.npmjs.org/@lerna/filter-packages/-/filter-packages-3.18.0.tgz#6a7a376d285208db03a82958cfb8172e179b4e70" - integrity sha512-6/0pMM04bCHNATIOkouuYmPg6KH3VkPCIgTfQmdkPJTullERyEQfNUKikrefjxo1vHOoCACDpy65JYyKiAbdwQ== +"@lerna/filter-packages@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/filter-packages/-/filter-packages-4.0.0.tgz#b1f70d70e1de9cdd36a4e50caa0ac501f8d012f2" + integrity sha512-+4AJIkK7iIiOaqCiVTYJxh/I9qikk4XjNQLhE3kixaqgMuHl1NQ99qXRR0OZqAWB9mh8Z1HA9bM5K1HZLBTOqA== dependencies: - "@lerna/validation-error" "3.13.0" - multimatch "^3.0.0" + "@lerna/validation-error" "4.0.0" + multimatch "^5.0.0" npmlog "^4.1.2" -"@lerna/get-npm-exec-opts@3.13.0": - version "3.13.0" - resolved "https://registry.npmjs.org/@lerna/get-npm-exec-opts/-/get-npm-exec-opts-3.13.0.tgz#d1b552cb0088199fc3e7e126f914e39a08df9ea5" - integrity sha512-Y0xWL0rg3boVyJk6An/vurKzubyJKtrxYv2sj4bB8Mc5zZ3tqtv0ccbOkmkXKqbzvNNF7VeUt1OJ3DRgtC/QZw== +"@lerna/get-npm-exec-opts@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/get-npm-exec-opts/-/get-npm-exec-opts-4.0.0.tgz#dc955be94a4ae75c374ef9bce91320887d34608f" + integrity sha512-yvmkerU31CTWS2c7DvmAWmZVeclPBqI7gPVr5VATUKNWJ/zmVcU4PqbYoLu92I9Qc4gY1TuUplMNdNuZTSL7IQ== dependencies: npmlog "^4.1.2" -"@lerna/get-packed@3.16.0": - version "3.16.0" - resolved "https://registry.npmjs.org/@lerna/get-packed/-/get-packed-3.16.0.tgz#1b316b706dcee86c7baa55e50b087959447852ff" - integrity sha512-AjsFiaJzo1GCPnJUJZiTW6J1EihrPkc2y3nMu6m3uWFxoleklsSCyImumzVZJssxMi3CPpztj8LmADLedl9kXw== +"@lerna/get-packed@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/get-packed/-/get-packed-4.0.0.tgz#0989d61624ac1f97e393bdad2137c49cd7a37823" + integrity sha512-rfWONRsEIGyPJTxFzC8ECb3ZbsDXJbfqWYyeeQQDrJRPnEJErlltRLPLgC2QWbxFgFPsoDLeQmFHJnf0iDfd8w== dependencies: - fs-extra "^8.1.0" - ssri "^6.0.1" - tar "^4.4.8" + fs-extra "^9.1.0" + ssri "^8.0.1" + tar "^6.1.0" -"@lerna/github-client@3.22.0": - version "3.22.0" - resolved "https://registry.npmjs.org/@lerna/github-client/-/github-client-3.22.0.tgz#5d816aa4f76747ed736ae64ff962b8f15c354d95" - integrity sha512-O/GwPW+Gzr3Eb5bk+nTzTJ3uv+jh5jGho9BOqKlajXaOkMYGBELEAqV5+uARNGWZFvYAiF4PgqHb6aCUu7XdXg== +"@lerna/github-client@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/github-client/-/github-client-4.0.0.tgz#2ced67721363ef70f8e12ffafce4410918f4a8a4" + integrity sha512-2jhsldZtTKXYUBnOm23Lb0Fx8G4qfSXF9y7UpyUgWUj+YZYd+cFxSuorwQIgk5P4XXrtVhsUesIsli+BYSThiw== dependencies: - "@lerna/child-process" "3.16.5" + "@lerna/child-process" "4.0.0" "@octokit/plugin-enterprise-rest" "^6.0.1" - "@octokit/rest" "^16.28.4" - git-url-parse "^11.1.2" + "@octokit/rest" "^18.1.0" + git-url-parse "^11.4.4" npmlog "^4.1.2" -"@lerna/gitlab-client@3.15.0": - version "3.15.0" - resolved "https://registry.npmjs.org/@lerna/gitlab-client/-/gitlab-client-3.15.0.tgz#91f4ec8c697b5ac57f7f25bd50fe659d24aa96a6" - integrity sha512-OsBvRSejHXUBMgwWQqNoioB8sgzL/Pf1pOUhHKtkiMl6aAWjklaaq5HPMvTIsZPfS6DJ9L5OK2GGZuooP/5c8Q== +"@lerna/gitlab-client@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/gitlab-client/-/gitlab-client-4.0.0.tgz#00dad73379c7b38951d4b4ded043504c14e2b67d" + integrity sha512-OMUpGSkeDWFf7BxGHlkbb35T7YHqVFCwBPSIR6wRsszY8PAzCYahtH3IaJzEJyUg6vmZsNl0FSr3pdA2skhxqA== dependencies: - node-fetch "^2.5.0" + node-fetch "^2.6.1" npmlog "^4.1.2" - whatwg-url "^7.0.0" + whatwg-url "^8.4.0" -"@lerna/global-options@3.13.0": - version "3.13.0" - resolved "https://registry.npmjs.org/@lerna/global-options/-/global-options-3.13.0.tgz#217662290db06ad9cf2c49d8e3100ee28eaebae1" - integrity sha512-SlZvh1gVRRzYLVluz9fryY1nJpZ0FHDGB66U9tFfvnnxmueckRQxLopn3tXj3NU1kc3QANT2I5BsQkOqZ4TEFQ== +"@lerna/global-options@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/global-options/-/global-options-4.0.0.tgz#c7d8b0de6a01d8a845e2621ea89e7f60f18c6a5f" + integrity sha512-TRMR8afAHxuYBHK7F++Ogop2a82xQjoGna1dvPOY6ltj/pEx59pdgcJfYcynYqMkFIk8bhLJJN9/ndIfX29FTQ== -"@lerna/has-npm-version@3.16.5": - version "3.16.5" - resolved "https://registry.npmjs.org/@lerna/has-npm-version/-/has-npm-version-3.16.5.tgz#ab83956f211d8923ea6afe9b979b38cc73b15326" - integrity sha512-WL7LycR9bkftyqbYop5rEGJ9sRFIV55tSGmbN1HLrF9idwOCD7CLrT64t235t3t4O5gehDnwKI5h2U3oxTrF8Q== +"@lerna/has-npm-version@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/has-npm-version/-/has-npm-version-4.0.0.tgz#d3fc3292c545eb28bd493b36e6237cf0279f631c" + integrity sha512-LQ3U6XFH8ZmLCsvsgq1zNDqka0Xzjq5ibVN+igAI5ccRWNaUsE/OcmsyMr50xAtNQMYMzmpw5GVLAivT2/YzCg== dependencies: - "@lerna/child-process" "3.16.5" - semver "^6.2.0" + "@lerna/child-process" "4.0.0" + semver "^7.3.4" -"@lerna/import@3.22.0": - version "3.22.0" - resolved "https://registry.npmjs.org/@lerna/import/-/import-3.22.0.tgz#1a5f0394f38e23c4f642a123e5e1517e70d068d2" - integrity sha512-uWOlexasM5XR6tXi4YehODtH9Y3OZrFht3mGUFFT3OIl2s+V85xIGFfqFGMTipMPAGb2oF1UBLL48kR43hRsOg== +"@lerna/import@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/import/-/import-4.0.0.tgz#bde656c4a451fa87ae41733ff8a8da60547c5465" + integrity sha512-FaIhd+4aiBousKNqC7TX1Uhe97eNKf5/SC7c5WZANVWtC7aBWdmswwDt3usrzCNpj6/Wwr9EtEbYROzxKH8ffg== dependencies: - "@lerna/child-process" "3.16.5" - "@lerna/command" "3.21.0" - "@lerna/prompt" "3.18.5" - "@lerna/pulse-till-done" "3.13.0" - "@lerna/validation-error" "3.13.0" + "@lerna/child-process" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/prompt" "4.0.0" + "@lerna/pulse-till-done" "4.0.0" + "@lerna/validation-error" "4.0.0" dedent "^0.7.0" - fs-extra "^8.1.0" - p-map-series "^1.0.0" + fs-extra "^9.1.0" + p-map-series "^2.1.0" -"@lerna/info@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/info/-/info-3.21.0.tgz#76696b676fdb0f35d48c83c63c1e32bb5e37814f" - integrity sha512-0XDqGYVBgWxUquFaIptW2bYSIu6jOs1BtkvRTWDDhw4zyEdp6q4eaMvqdSap1CG+7wM5jeLCi6z94wS0AuiuwA== +"@lerna/info@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/info/-/info-4.0.0.tgz#b9fb0e479d60efe1623603958a831a88b1d7f1fc" + integrity sha512-8Uboa12kaCSZEn4XRfPz5KU9XXoexSPS4oeYGj76s2UQb1O1GdnEyfjyNWoUl1KlJ2i/8nxUskpXIftoFYH0/Q== dependencies: - "@lerna/command" "3.21.0" - "@lerna/output" "3.13.0" - envinfo "^7.3.1" + "@lerna/command" "4.0.0" + "@lerna/output" "4.0.0" + envinfo "^7.7.4" -"@lerna/init@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/init/-/init-3.21.0.tgz#1e810934dc8bf4e5386c031041881d3b4096aa5c" - integrity sha512-6CM0z+EFUkFfurwdJCR+LQQF6MqHbYDCBPyhu/d086LRf58GtYZYj49J8mKG9ktayp/TOIxL/pKKjgLD8QBPOg== +"@lerna/init@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/init/-/init-4.0.0.tgz#dadff67e6dfb981e8ccbe0e6a310e837962f6c7a" + integrity sha512-wY6kygop0BCXupzWj5eLvTUqdR7vIAm0OgyV9WHpMYQGfs1V22jhztt8mtjCloD/O0nEe4tJhdG62XU5aYmPNQ== dependencies: - "@lerna/child-process" "3.16.5" - "@lerna/command" "3.21.0" - fs-extra "^8.1.0" - p-map "^2.1.0" - write-json-file "^3.2.0" + "@lerna/child-process" "4.0.0" + "@lerna/command" "4.0.0" + fs-extra "^9.1.0" + p-map "^4.0.0" + write-json-file "^4.3.0" -"@lerna/link@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/link/-/link-3.21.0.tgz#8be68ff0ccee104b174b5bbd606302c2f06e9d9b" - integrity sha512-tGu9GxrX7Ivs+Wl3w1+jrLi1nQ36kNI32dcOssij6bg0oZ2M2MDEFI9UF2gmoypTaN9uO5TSsjCFS7aR79HbdQ== +"@lerna/link@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/link/-/link-4.0.0.tgz#c3a38aabd44279d714e90f2451e31b63f0fb65ba" + integrity sha512-KlvPi7XTAcVOByfaLlOeYOfkkDcd+bejpHMCd1KcArcFTwijOwXOVi24DYomIeHvy6HsX/IUquJ4PPUJIeB4+w== dependencies: - "@lerna/command" "3.21.0" - "@lerna/package-graph" "3.18.5" - "@lerna/symlink-dependencies" "3.17.0" - p-map "^2.1.0" - slash "^2.0.0" + "@lerna/command" "4.0.0" + "@lerna/package-graph" "4.0.0" + "@lerna/symlink-dependencies" "4.0.0" + p-map "^4.0.0" + slash "^3.0.0" -"@lerna/list@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/list/-/list-3.21.0.tgz#42f76fafa56dea13b691ec8cab13832691d61da2" - integrity sha512-KehRjE83B1VaAbRRkRy6jLX1Cin8ltsrQ7FHf2bhwhRHK0S54YuA6LOoBnY/NtA8bHDX/Z+G5sMY78X30NS9tg== +"@lerna/list@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/list/-/list-4.0.0.tgz#24b4e6995bd73f81c556793fe502b847efd9d1d7" + integrity sha512-L2B5m3P+U4Bif5PultR4TI+KtW+SArwq1i75QZ78mRYxPc0U/piau1DbLOmwrdqr99wzM49t0Dlvl6twd7GHFg== dependencies: - "@lerna/command" "3.21.0" - "@lerna/filter-options" "3.20.0" - "@lerna/listable" "3.18.5" - "@lerna/output" "3.13.0" + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/listable" "4.0.0" + "@lerna/output" "4.0.0" -"@lerna/listable@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/listable/-/listable-3.18.5.tgz#e82798405b5ed8fc51843c8ef1e7a0e497388a1a" - integrity sha512-Sdr3pVyaEv5A7ZkGGYR7zN+tTl2iDcinryBPvtuv20VJrXBE8wYcOks1edBTcOWsPjCE/rMP4bo1pseyk3UTsg== +"@lerna/listable@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/listable/-/listable-4.0.0.tgz#d00d6cb4809b403f2b0374fc521a78e318b01214" + integrity sha512-/rPOSDKsOHs5/PBLINZOkRIX1joOXUXEtyUs5DHLM8q6/RP668x/1lFhw6Dx7/U+L0+tbkpGtZ1Yt0LewCLgeQ== dependencies: - "@lerna/query-graph" "3.18.5" - chalk "^2.3.1" + "@lerna/query-graph" "4.0.0" + chalk "^4.1.0" columnify "^1.5.4" -"@lerna/log-packed@3.16.0": - version "3.16.0" - resolved "https://registry.npmjs.org/@lerna/log-packed/-/log-packed-3.16.0.tgz#f83991041ee77b2495634e14470b42259fd2bc16" - integrity sha512-Fp+McSNBV/P2mnLUYTaSlG8GSmpXM7krKWcllqElGxvAqv6chk2K3c2k80MeVB4WvJ9tRjUUf+i7HUTiQ9/ckQ== +"@lerna/log-packed@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/log-packed/-/log-packed-4.0.0.tgz#95168fe2e26ac6a71e42f4be857519b77e57a09f" + integrity sha512-+dpCiWbdzgMAtpajLToy9PO713IHoE6GV/aizXycAyA07QlqnkpaBNZ8DW84gHdM1j79TWockGJo9PybVhrrZQ== dependencies: - byte-size "^5.0.1" + byte-size "^7.0.0" columnify "^1.5.4" has-unicode "^2.0.1" npmlog "^4.1.2" -"@lerna/npm-conf@3.16.0": - version "3.16.0" - resolved "https://registry.npmjs.org/@lerna/npm-conf/-/npm-conf-3.16.0.tgz#1c10a89ae2f6c2ee96962557738685300d376827" - integrity sha512-HbO3DUrTkCAn2iQ9+FF/eisDpWY5POQAOF1m7q//CZjdC2HSW3UYbKEGsSisFxSfaF9Z4jtrV+F/wX6qWs3CuA== +"@lerna/npm-conf@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/npm-conf/-/npm-conf-4.0.0.tgz#b259fd1e1cee2bf5402b236e770140ff9ade7fd2" + integrity sha512-uS7H02yQNq3oejgjxAxqq/jhwGEE0W0ntr8vM3EfpCW1F/wZruwQw+7bleJQ9vUBjmdXST//tk8mXzr5+JXCfw== dependencies: - config-chain "^1.1.11" - pify "^4.0.1" + config-chain "^1.1.12" + pify "^5.0.0" -"@lerna/npm-dist-tag@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/npm-dist-tag/-/npm-dist-tag-3.18.5.tgz#9ef9abb7c104077b31f6fab22cc73b314d54ac55" - integrity sha512-xw0HDoIG6HreVsJND9/dGls1c+lf6vhu7yJoo56Sz5bvncTloYGLUppIfDHQr4ZvmPCK8rsh0euCVh2giPxzKQ== +"@lerna/npm-dist-tag@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/npm-dist-tag/-/npm-dist-tag-4.0.0.tgz#d1e99b4eccd3414142f0548ad331bf2d53f3257a" + integrity sha512-F20sg28FMYTgXqEQihgoqSfwmq+Id3zT23CnOwD+XQMPSy9IzyLf1fFVH319vXIw6NF6Pgs4JZN2Qty6/CQXGw== dependencies: - "@evocateur/npm-registry-fetch" "^4.0.0" - "@lerna/otplease" "3.18.5" - figgy-pudding "^3.5.1" - npm-package-arg "^6.1.0" + "@lerna/otplease" "4.0.0" + npm-package-arg "^8.1.0" + npm-registry-fetch "^9.0.0" npmlog "^4.1.2" -"@lerna/npm-install@3.16.5": - version "3.16.5" - resolved "https://registry.npmjs.org/@lerna/npm-install/-/npm-install-3.16.5.tgz#d6bfdc16f81285da66515ae47924d6e278d637d3" - integrity sha512-hfiKk8Eku6rB9uApqsalHHTHY+mOrrHeWEs+gtg7+meQZMTS3kzv4oVp5cBZigndQr3knTLjwthT/FX4KvseFg== +"@lerna/npm-install@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/npm-install/-/npm-install-4.0.0.tgz#31180be3ab3b7d1818a1a0c206aec156b7094c78" + integrity sha512-aKNxq2j3bCH3eXl3Fmu4D54s/YLL9WSwV8W7X2O25r98wzrO38AUN6AB9EtmAx+LV/SP15et7Yueg9vSaanRWg== dependencies: - "@lerna/child-process" "3.16.5" - "@lerna/get-npm-exec-opts" "3.13.0" - fs-extra "^8.1.0" - npm-package-arg "^6.1.0" + "@lerna/child-process" "4.0.0" + "@lerna/get-npm-exec-opts" "4.0.0" + fs-extra "^9.1.0" + npm-package-arg "^8.1.0" npmlog "^4.1.2" - signal-exit "^3.0.2" - write-pkg "^3.1.0" + signal-exit "^3.0.3" + write-pkg "^4.0.0" -"@lerna/npm-publish@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/npm-publish/-/npm-publish-3.18.5.tgz#240e4039959fd9816b49c5b07421e11b5cb000af" - integrity sha512-3etLT9+2L8JAx5F8uf7qp6iAtOLSMj+ZYWY6oUgozPi/uLqU0/gsMsEXh3F0+YVW33q0M61RpduBoAlOOZnaTg== +"@lerna/npm-publish@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/npm-publish/-/npm-publish-4.0.0.tgz#84eb62e876fe949ae1fd62c60804423dbc2c4472" + integrity sha512-vQb7yAPRo5G5r77DRjHITc9piR9gvEKWrmfCH7wkfBnGWEqu7n8/4bFQ7lhnkujvc8RXOsYpvbMQkNfkYibD/w== dependencies: - "@evocateur/libnpmpublish" "^1.2.2" - "@lerna/otplease" "3.18.5" - "@lerna/run-lifecycle" "3.16.2" - figgy-pudding "^3.5.1" - fs-extra "^8.1.0" - npm-package-arg "^6.1.0" + "@lerna/otplease" "4.0.0" + "@lerna/run-lifecycle" "4.0.0" + fs-extra "^9.1.0" + libnpmpublish "^4.0.0" + npm-package-arg "^8.1.0" npmlog "^4.1.2" - pify "^4.0.1" - read-package-json "^2.0.13" + pify "^5.0.0" + read-package-json "^3.0.0" -"@lerna/npm-run-script@3.16.5": - version "3.16.5" - resolved "https://registry.npmjs.org/@lerna/npm-run-script/-/npm-run-script-3.16.5.tgz#9c2ec82453a26c0b46edc0bb7c15816c821f5c15" - integrity sha512-1asRi+LjmVn3pMjEdpqKJZFT/3ZNpb+VVeJMwrJaV/3DivdNg7XlPK9LTrORuKU4PSvhdEZvJmSlxCKyDpiXsQ== +"@lerna/npm-run-script@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/npm-run-script/-/npm-run-script-4.0.0.tgz#dfebf4f4601442e7c0b5214f9fb0d96c9350743b" + integrity sha512-Jmyh9/IwXJjOXqKfIgtxi0bxi1pUeKe5bD3S81tkcy+kyng/GNj9WSqD5ZggoNP2NP//s4CLDAtUYLdP7CU9rA== dependencies: - "@lerna/child-process" "3.16.5" - "@lerna/get-npm-exec-opts" "3.13.0" + "@lerna/child-process" "4.0.0" + "@lerna/get-npm-exec-opts" "4.0.0" npmlog "^4.1.2" -"@lerna/otplease@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/otplease/-/otplease-3.18.5.tgz#b77b8e760b40abad9f7658d988f3ea77d4fd0231" - integrity sha512-S+SldXAbcXTEDhzdxYLU0ZBKuYyURP/ND2/dK6IpKgLxQYh/z4ScljPDMyKymmEvgiEJmBsPZAAPfmNPEzxjog== +"@lerna/otplease@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/otplease/-/otplease-4.0.0.tgz#84972eb43448f8a1077435ba1c5e59233b725850" + integrity sha512-Sgzbqdk1GH4psNiT6hk+BhjOfIr/5KhGBk86CEfHNJTk9BK4aZYyJD4lpDbDdMjIV4g03G7pYoqHzH765T4fxw== dependencies: - "@lerna/prompt" "3.18.5" - figgy-pudding "^3.5.1" + "@lerna/prompt" "4.0.0" -"@lerna/output@3.13.0": - version "3.13.0" - resolved "https://registry.npmjs.org/@lerna/output/-/output-3.13.0.tgz#3ded7cc908b27a9872228a630d950aedae7a4989" - integrity sha512-7ZnQ9nvUDu/WD+bNsypmPG5MwZBwu86iRoiW6C1WBuXXDxM5cnIAC1m2WxHeFnjyMrYlRXM9PzOQ9VDD+C15Rg== +"@lerna/output@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/output/-/output-4.0.0.tgz#b1d72215c0e35483e4f3e9994debc82c621851f2" + integrity sha512-Un1sHtO1AD7buDQrpnaYTi2EG6sLF+KOPEAMxeUYG5qG3khTs2Zgzq5WE3dt2N/bKh7naESt20JjIW6tBELP0w== dependencies: npmlog "^4.1.2" -"@lerna/pack-directory@3.16.4": - version "3.16.4" - resolved "https://registry.npmjs.org/@lerna/pack-directory/-/pack-directory-3.16.4.tgz#3eae5f91bdf5acfe0384510ed53faddc4c074693" - integrity sha512-uxSF0HZeGyKaaVHz5FroDY9A5NDDiCibrbYR6+khmrhZtY0Bgn6hWq8Gswl9iIlymA+VzCbshWIMX4o2O8C8ng== +"@lerna/pack-directory@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/pack-directory/-/pack-directory-4.0.0.tgz#8b617db95d20792f043aaaa13a9ccc0e04cb4c74" + integrity sha512-NJrmZNmBHS+5aM+T8N6FVbaKFScVqKlQFJNY2k7nsJ/uklNKsLLl6VhTQBPwMTbf6Tf7l6bcKzpy7aePuq9UiQ== dependencies: - "@lerna/get-packed" "3.16.0" - "@lerna/package" "3.16.0" - "@lerna/run-lifecycle" "3.16.2" - figgy-pudding "^3.5.1" - npm-packlist "^1.4.4" + "@lerna/get-packed" "4.0.0" + "@lerna/package" "4.0.0" + "@lerna/run-lifecycle" "4.0.0" + npm-packlist "^2.1.4" npmlog "^4.1.2" - tar "^4.4.10" - temp-write "^3.4.0" + tar "^6.1.0" + temp-write "^4.0.0" -"@lerna/package-graph@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/package-graph/-/package-graph-3.18.5.tgz#c740e2ea3578d059e551633e950690831b941f6b" - integrity sha512-8QDrR9T+dBegjeLr+n9WZTVxUYUhIUjUgZ0gvNxUBN8S1WB9r6H5Yk56/MVaB64tA3oGAN9IIxX6w0WvTfFudA== - dependencies: - "@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" - -"@lerna/package-graph@^4.0.0": +"@lerna/package-graph@4.0.0", "@lerna/package-graph@^4.0.0": version "4.0.0" resolved "https://registry.npmjs.org/@lerna/package-graph/-/package-graph-4.0.0.tgz#16a00253a8ac810f72041481cb46bcee8d8123dd" integrity sha512-QED2ZCTkfXMKFoTGoccwUzjHtZMSf3UKX14A4/kYyBms9xfFsesCZ6SLI5YeySEgcul8iuIWfQFZqRw+Qrjraw== @@ -3690,15 +3598,6 @@ npmlog "^4.1.2" semver "^7.3.4" -"@lerna/package@3.16.0": - version "3.16.0" - resolved "https://registry.npmjs.org/@lerna/package/-/package-3.16.0.tgz#7e0a46e4697ed8b8a9c14d59c7f890e0d38ba13c" - integrity sha512-2lHBWpaxcBoiNVbtyLtPUuTYEaB/Z+eEqRS9duxpZs6D+mTTZMNy6/5vpEVSCBmzvdYpyqhqaYjjSLvjjr5Riw== - dependencies: - load-json-file "^5.3.0" - npm-package-arg "^6.1.0" - write-pkg "^3.1.0" - "@lerna/package@4.0.0": version "4.0.0" resolved "https://registry.npmjs.org/@lerna/package/-/package-4.0.0.tgz#1b4c259c4bcff45c876ee1d591a043aacbc0d6b7" @@ -3708,13 +3607,6 @@ npm-package-arg "^8.1.0" write-pkg "^4.0.0" -"@lerna/prerelease-id-from-version@3.16.0": - version "3.16.0" - resolved "https://registry.npmjs.org/@lerna/prerelease-id-from-version/-/prerelease-id-from-version-3.16.0.tgz#b24bfa789f5e1baab914d7b08baae9b7bd7d83a1" - integrity sha512-qZyeUyrE59uOK8rKdGn7jQz+9uOpAaF/3hbslJVFL1NqF9ELDTqjCPXivuejMX/lN4OgD6BugTO4cR7UTq/sZA== - dependencies: - semver "^6.2.0" - "@lerna/prerelease-id-from-version@4.0.0": version "4.0.0" resolved "https://registry.npmjs.org/@lerna/prerelease-id-from-version/-/prerelease-id-from-version-4.0.0.tgz#c7e0676fcee1950d85630e108eddecdd5b48c916" @@ -3722,35 +3614,16 @@ dependencies: semver "^7.3.4" -"@lerna/profiler@3.20.0": - version "3.20.0" - resolved "https://registry.npmjs.org/@lerna/profiler/-/profiler-3.20.0.tgz#0f6dc236f4ea8f9ea5f358c6703305a4f32ad051" - integrity sha512-bh8hKxAlm6yu8WEOvbLENm42i2v9SsR4WbrCWSbsmOElx3foRnMlYk7NkGECa+U5c3K4C6GeBbwgqs54PP7Ljg== +"@lerna/profiler@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/profiler/-/profiler-4.0.0.tgz#8a53ab874522eae15d178402bff90a14071908e9" + integrity sha512-/BaEbqnVh1LgW/+qz8wCuI+obzi5/vRE8nlhjPzdEzdmWmZXuCKyWSEzAyHOJWw1ntwMiww5dZHhFQABuoFz9Q== dependencies: - figgy-pudding "^3.5.1" - fs-extra "^8.1.0" + fs-extra "^9.1.0" npmlog "^4.1.2" - upath "^1.2.0" + upath "^2.0.1" -"@lerna/project@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/project/-/project-3.21.0.tgz#5d784d2d10c561a00f20320bcdb040997c10502d" - integrity sha512-xT1mrpET2BF11CY32uypV2GPtPVm6Hgtha7D81GQP9iAitk9EccrdNjYGt5UBYASl4CIDXBRxwmTTVGfrCx82A== - dependencies: - "@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/project@^4.0.0": +"@lerna/project@4.0.0", "@lerna/project@^4.0.0": version "4.0.0" resolved "https://registry.npmjs.org/@lerna/project/-/project-4.0.0.tgz#ff84893935833533a74deff30c0e64ddb7f0ba6b" integrity sha512-o0MlVbDkD5qRPkFKlBZsXZjoNTWPyuL58564nSfZJ6JYNmgAptnWPB2dQlAc7HWRZkmnC2fCkEdoU+jioPavbg== @@ -3768,152 +3641,139 @@ resolve-from "^5.0.0" write-json-file "^4.3.0" -"@lerna/prompt@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/prompt/-/prompt-3.18.5.tgz#628cd545f225887d060491ab95df899cfc5218a1" - integrity sha512-rkKj4nm1twSbBEb69+Em/2jAERK8htUuV8/xSjN0NPC+6UjzAwY52/x9n5cfmpa9lyKf/uItp7chCI7eDmNTKQ== +"@lerna/prompt@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/prompt/-/prompt-4.0.0.tgz#5ec69a803f3f0db0ad9f221dad64664d3daca41b" + integrity sha512-4Ig46oCH1TH5M7YyTt53fT6TuaKMgqUUaqdgxvp6HP6jtdak6+amcsqB8YGz2eQnw/sdxunx84DfI9XpoLj4bQ== dependencies: - inquirer "^6.2.0" + inquirer "^7.3.3" npmlog "^4.1.2" -"@lerna/publish@3.22.1": - version "3.22.1" - resolved "https://registry.npmjs.org/@lerna/publish/-/publish-3.22.1.tgz#b4f7ce3fba1e9afb28be4a1f3d88222269ba9519" - integrity sha512-PG9CM9HUYDreb1FbJwFg90TCBQooGjj+n/pb3gw/eH5mEDq0p8wKdLFe0qkiqUkm/Ub5C8DbVFertIo0Vd0zcw== +"@lerna/publish@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/publish/-/publish-4.0.0.tgz#f67011305adeba120066a3b6d984a5bb5fceef65" + integrity sha512-K8jpqjHrChH22qtkytA5GRKIVFEtqBF6JWj1I8dWZtHs4Jywn8yB1jQ3BAMLhqmDJjWJtRck0KXhQQKzDK2UPg== dependencies: - "@evocateur/libnpmaccess" "^3.1.2" - "@evocateur/npm-registry-fetch" "^4.0.0" - "@evocateur/pacote" "^9.6.3" - "@lerna/check-working-tree" "3.16.5" - "@lerna/child-process" "3.16.5" - "@lerna/collect-updates" "3.20.0" - "@lerna/command" "3.21.0" - "@lerna/describe-ref" "3.16.5" - "@lerna/log-packed" "3.16.0" - "@lerna/npm-conf" "3.16.0" - "@lerna/npm-dist-tag" "3.18.5" - "@lerna/npm-publish" "3.18.5" - "@lerna/otplease" "3.18.5" - "@lerna/output" "3.13.0" - "@lerna/pack-directory" "3.16.4" - "@lerna/prerelease-id-from-version" "3.16.0" - "@lerna/prompt" "3.18.5" - "@lerna/pulse-till-done" "3.13.0" - "@lerna/run-lifecycle" "3.16.2" - "@lerna/run-topologically" "3.18.5" - "@lerna/validation-error" "3.13.0" - "@lerna/version" "3.22.1" - figgy-pudding "^3.5.1" - fs-extra "^8.1.0" - npm-package-arg "^6.1.0" + "@lerna/check-working-tree" "4.0.0" + "@lerna/child-process" "4.0.0" + "@lerna/collect-updates" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/describe-ref" "4.0.0" + "@lerna/log-packed" "4.0.0" + "@lerna/npm-conf" "4.0.0" + "@lerna/npm-dist-tag" "4.0.0" + "@lerna/npm-publish" "4.0.0" + "@lerna/otplease" "4.0.0" + "@lerna/output" "4.0.0" + "@lerna/pack-directory" "4.0.0" + "@lerna/prerelease-id-from-version" "4.0.0" + "@lerna/prompt" "4.0.0" + "@lerna/pulse-till-done" "4.0.0" + "@lerna/run-lifecycle" "4.0.0" + "@lerna/run-topologically" "4.0.0" + "@lerna/validation-error" "4.0.0" + "@lerna/version" "4.0.0" + fs-extra "^9.1.0" + libnpmaccess "^4.0.1" + npm-package-arg "^8.1.0" + npm-registry-fetch "^9.0.0" npmlog "^4.1.2" - p-finally "^1.0.0" - p-map "^2.1.0" - p-pipe "^1.2.0" - semver "^6.2.0" + p-map "^4.0.0" + p-pipe "^3.1.0" + pacote "^11.2.6" + semver "^7.3.4" -"@lerna/pulse-till-done@3.13.0": - version "3.13.0" - resolved "https://registry.npmjs.org/@lerna/pulse-till-done/-/pulse-till-done-3.13.0.tgz#c8e9ce5bafaf10d930a67d7ed0ccb5d958fe0110" - integrity sha512-1SOHpy7ZNTPulzIbargrgaJX387csN7cF1cLOGZiJQA6VqnS5eWs2CIrG8i8wmaUavj2QlQ5oEbRMVVXSsGrzA== +"@lerna/pulse-till-done@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/pulse-till-done/-/pulse-till-done-4.0.0.tgz#04bace7d483a8205c187b806bcd8be23d7bb80a3" + integrity sha512-Frb4F7QGckaybRhbF7aosLsJ5e9WuH7h0KUkjlzSByVycxY91UZgaEIVjS2oN9wQLrheLMHl6SiFY0/Pvo0Cxg== dependencies: npmlog "^4.1.2" -"@lerna/query-graph@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/query-graph/-/query-graph-3.18.5.tgz#df4830bb5155273003bf35e8dda1c32d0927bd86" - integrity sha512-50Lf4uuMpMWvJ306be3oQDHrWV42nai9gbIVByPBYJuVW8dT8O8pA3EzitNYBUdLL9/qEVbrR0ry1HD7EXwtRA== +"@lerna/query-graph@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/query-graph/-/query-graph-4.0.0.tgz#09dd1c819ac5ee3f38db23931143701f8a6eef63" + integrity sha512-YlP6yI3tM4WbBmL9GCmNDoeQyzcyg1e4W96y/PKMZa5GbyUvkS2+Jc2kwPD+5KcXou3wQZxSPzR3Te5OenaDdg== dependencies: - "@lerna/package-graph" "3.18.5" - figgy-pudding "^3.5.1" + "@lerna/package-graph" "4.0.0" -"@lerna/resolve-symlink@3.16.0": - version "3.16.0" - resolved "https://registry.npmjs.org/@lerna/resolve-symlink/-/resolve-symlink-3.16.0.tgz#37fc7095fabdbcf317c26eb74e0d0bde8efd2386" - integrity sha512-Ibj5e7njVHNJ/NOqT4HlEgPFPtPLWsO7iu59AM5bJDcAJcR96mLZ7KGVIsS2tvaO7akMEJvt2P+ErwCdloG3jQ== +"@lerna/resolve-symlink@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/resolve-symlink/-/resolve-symlink-4.0.0.tgz#6d006628a210c9b821964657a9e20a8c9a115e14" + integrity sha512-RtX8VEUzqT+uLSCohx8zgmjc6zjyRlh6i/helxtZTMmc4+6O4FS9q5LJas2uGO2wKvBlhcD6siibGt7dIC3xZA== dependencies: - fs-extra "^8.1.0" + fs-extra "^9.1.0" npmlog "^4.1.2" - read-cmd-shim "^1.0.1" + read-cmd-shim "^2.0.0" -"@lerna/rimraf-dir@3.16.5": - version "3.16.5" - resolved "https://registry.npmjs.org/@lerna/rimraf-dir/-/rimraf-dir-3.16.5.tgz#04316ab5ffd2909657aaf388ea502cb8c2f20a09" - integrity sha512-bQlKmO0pXUsXoF8lOLknhyQjOZsCc0bosQDoX4lujBXSWxHVTg1VxURtWf2lUjz/ACsJVDfvHZbDm8kyBk5okA== +"@lerna/rimraf-dir@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/rimraf-dir/-/rimraf-dir-4.0.0.tgz#2edf3b62d4eb0ef4e44e430f5844667d551ec25a" + integrity sha512-QNH9ABWk9mcMJh2/muD9iYWBk1oQd40y6oH+f3wwmVGKYU5YJD//+zMiBI13jxZRtwBx0vmBZzkBkK1dR11cBg== dependencies: - "@lerna/child-process" "3.16.5" + "@lerna/child-process" "4.0.0" npmlog "^4.1.2" - path-exists "^3.0.0" - rimraf "^2.6.2" + path-exists "^4.0.0" + rimraf "^3.0.2" -"@lerna/run-lifecycle@3.16.2": - version "3.16.2" - resolved "https://registry.npmjs.org/@lerna/run-lifecycle/-/run-lifecycle-3.16.2.tgz#67b288f8ea964db9ea4fb1fbc7715d5bbb0bce00" - integrity sha512-RqFoznE8rDpyyF0rOJy3+KjZCeTkO8y/OB9orPauR7G2xQ7PTdCpgo7EO6ZNdz3Al+k1BydClZz/j78gNCmL2A== +"@lerna/run-lifecycle@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/run-lifecycle/-/run-lifecycle-4.0.0.tgz#e648a46f9210a9bcd7c391df6844498cb5079334" + integrity sha512-IwxxsajjCQQEJAeAaxF8QdEixfI7eLKNm4GHhXHrgBu185JcwScFZrj9Bs+PFKxwb+gNLR4iI5rpUdY8Y0UdGQ== dependencies: - "@lerna/npm-conf" "3.16.0" - figgy-pudding "^3.5.1" - npm-lifecycle "^3.1.2" + "@lerna/npm-conf" "4.0.0" + npm-lifecycle "^3.1.5" npmlog "^4.1.2" -"@lerna/run-topologically@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/run-topologically/-/run-topologically-3.18.5.tgz#3cd639da20e967d7672cb88db0f756b92f2fdfc3" - integrity sha512-6N1I+6wf4hLOnPW+XDZqwufyIQ6gqoPfHZFkfWlvTQ+Ue7CuF8qIVQ1Eddw5HKQMkxqN10thKOFfq/9NQZ4NUg== +"@lerna/run-topologically@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/run-topologically/-/run-topologically-4.0.0.tgz#af846eeee1a09b0c2be0d1bfb5ef0f7b04bb1827" + integrity sha512-EVZw9hGwo+5yp+VL94+NXRYisqgAlj0jWKWtAIynDCpghRxCE5GMO3xrQLmQgqkpUl9ZxQFpICgYv5DW4DksQA== dependencies: - "@lerna/query-graph" "3.18.5" - figgy-pudding "^3.5.1" - p-queue "^4.0.0" + "@lerna/query-graph" "4.0.0" + p-queue "^6.6.2" -"@lerna/run@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/run/-/run-3.21.0.tgz#2a35ec84979e4d6e42474fe148d32e5de1cac891" - integrity sha512-fJF68rT3veh+hkToFsBmUJ9MHc9yGXA7LSDvhziAojzOb0AI/jBDp6cEcDQyJ7dbnplba2Lj02IH61QUf9oW0Q== +"@lerna/run@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/run/-/run-4.0.0.tgz#4bc7fda055a729487897c23579694f6183c91262" + integrity sha512-9giulCOzlMPzcZS/6Eov6pxE9gNTyaXk0Man+iCIdGJNMrCnW7Dme0Z229WWP/UoxDKg71F2tMsVVGDiRd8fFQ== dependencies: - "@lerna/command" "3.21.0" - "@lerna/filter-options" "3.20.0" - "@lerna/npm-run-script" "3.16.5" - "@lerna/output" "3.13.0" - "@lerna/profiler" "3.20.0" - "@lerna/run-topologically" "3.18.5" - "@lerna/timer" "3.13.0" - "@lerna/validation-error" "3.13.0" - p-map "^2.1.0" + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/npm-run-script" "4.0.0" + "@lerna/output" "4.0.0" + "@lerna/profiler" "4.0.0" + "@lerna/run-topologically" "4.0.0" + "@lerna/timer" "4.0.0" + "@lerna/validation-error" "4.0.0" + p-map "^4.0.0" -"@lerna/symlink-binary@3.17.0": - version "3.17.0" - resolved "https://registry.npmjs.org/@lerna/symlink-binary/-/symlink-binary-3.17.0.tgz#8f8031b309863814883d3f009877f82e38aef45a" - integrity sha512-RLpy9UY6+3nT5J+5jkM5MZyMmjNHxZIZvXLV+Q3MXrf7Eaa1hNqyynyj4RO95fxbS+EZc4XVSk25DGFQbcRNSQ== +"@lerna/symlink-binary@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/symlink-binary/-/symlink-binary-4.0.0.tgz#21009f62d53a425f136cb4c1a32c6b2a0cc02d47" + integrity sha512-zualodWC4q1QQc1pkz969hcFeWXOsVYZC5AWVtAPTDfLl+TwM7eG/O6oP+Rr3fFowspxo6b1TQ6sYfDV6HXNWA== dependencies: - "@lerna/create-symlink" "3.16.2" - "@lerna/package" "3.16.0" - fs-extra "^8.1.0" - p-map "^2.1.0" + "@lerna/create-symlink" "4.0.0" + "@lerna/package" "4.0.0" + fs-extra "^9.1.0" + p-map "^4.0.0" -"@lerna/symlink-dependencies@3.17.0": - version "3.17.0" - resolved "https://registry.npmjs.org/@lerna/symlink-dependencies/-/symlink-dependencies-3.17.0.tgz#48d6360e985865a0e56cd8b51b308a526308784a" - integrity sha512-KmjU5YT1bpt6coOmdFueTJ7DFJL4H1w5eF8yAQ2zsGNTtZ+i5SGFBWpb9AQaw168dydc3s4eu0W0Sirda+F59Q== +"@lerna/symlink-dependencies@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/symlink-dependencies/-/symlink-dependencies-4.0.0.tgz#8910eca084ae062642d0490d8972cf2d98e9ebbd" + integrity sha512-BABo0MjeUHNAe2FNGty1eantWp8u83BHSeIMPDxNq0MuW2K3CiQRaeWT3EGPAzXpGt0+hVzBrA6+OT0GPn7Yuw== dependencies: - "@lerna/create-symlink" "3.16.2" - "@lerna/resolve-symlink" "3.16.0" - "@lerna/symlink-binary" "3.17.0" - fs-extra "^8.1.0" - p-finally "^1.0.0" - p-map "^2.1.0" - p-map-series "^1.0.0" + "@lerna/create-symlink" "4.0.0" + "@lerna/resolve-symlink" "4.0.0" + "@lerna/symlink-binary" "4.0.0" + fs-extra "^9.1.0" + p-map "^4.0.0" + p-map-series "^2.1.0" -"@lerna/timer@3.13.0": - version "3.13.0" - resolved "https://registry.npmjs.org/@lerna/timer/-/timer-3.13.0.tgz#bcd0904551db16e08364d6c18e5e2160fc870781" - integrity sha512-RHWrDl8U4XNPqY5MQHkToWS9jHPnkLZEt5VD+uunCKTfzlxGnRCr3/zVr8VGy/uENMYpVP3wJa4RKGY6M0vkRw== - -"@lerna/validation-error@3.13.0": - version "3.13.0" - resolved "https://registry.npmjs.org/@lerna/validation-error/-/validation-error-3.13.0.tgz#c86b8f07c5ab9539f775bd8a54976e926f3759c3" - integrity sha512-SiJP75nwB8GhgwLKQfdkSnDufAaCbkZWJqEDlKOUPUvVOplRGnfL+BPQZH5nvq2BYSRXsksXWZ4UHVnQZI/HYA== - dependencies: - npmlog "^4.1.2" +"@lerna/timer@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/timer/-/timer-4.0.0.tgz#a52e51bfcd39bfd768988049ace7b15c1fd7a6da" + integrity sha512-WFsnlaE7SdOvjuyd05oKt8Leg3ENHICnvX3uYKKdByA+S3g+TCz38JsNs7OUZVt+ba63nC2nbXDlUnuT2Xbsfg== "@lerna/validation-error@4.0.0": version "4.0.0" @@ -3922,45 +3782,45 @@ dependencies: npmlog "^4.1.2" -"@lerna/version@3.22.1": - version "3.22.1" - resolved "https://registry.npmjs.org/@lerna/version/-/version-3.22.1.tgz#9805a9247a47ee62d6b81bd9fa5fb728b24b59e2" - integrity sha512-PSGt/K1hVqreAFoi3zjD0VEDupQ2WZVlVIwesrE5GbrL2BjXowjCsTDPqblahDUPy0hp6h7E2kG855yLTp62+g== +"@lerna/version@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/version/-/version-4.0.0.tgz#532659ec6154d8a8789c5ab53878663e244e3228" + integrity sha512-otUgiqs5W9zGWJZSCCMRV/2Zm2A9q9JwSDS7s/tlKq4mWCYriWo7+wsHEA/nPTMDyYyBO5oyZDj+3X50KDUzeA== dependencies: - "@lerna/check-working-tree" "3.16.5" - "@lerna/child-process" "3.16.5" - "@lerna/collect-updates" "3.20.0" - "@lerna/command" "3.21.0" - "@lerna/conventional-commits" "3.22.0" - "@lerna/github-client" "3.22.0" - "@lerna/gitlab-client" "3.15.0" - "@lerna/output" "3.13.0" - "@lerna/prerelease-id-from-version" "3.16.0" - "@lerna/prompt" "3.18.5" - "@lerna/run-lifecycle" "3.16.2" - "@lerna/run-topologically" "3.18.5" - "@lerna/validation-error" "3.13.0" - chalk "^2.3.1" + "@lerna/check-working-tree" "4.0.0" + "@lerna/child-process" "4.0.0" + "@lerna/collect-updates" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/conventional-commits" "4.0.0" + "@lerna/github-client" "4.0.0" + "@lerna/gitlab-client" "4.0.0" + "@lerna/output" "4.0.0" + "@lerna/prerelease-id-from-version" "4.0.0" + "@lerna/prompt" "4.0.0" + "@lerna/run-lifecycle" "4.0.0" + "@lerna/run-topologically" "4.0.0" + "@lerna/validation-error" "4.0.0" + chalk "^4.1.0" dedent "^0.7.0" - load-json-file "^5.3.0" + load-json-file "^6.2.0" minimatch "^3.0.4" npmlog "^4.1.2" - p-map "^2.1.0" - p-pipe "^1.2.0" - p-reduce "^1.0.0" - p-waterfall "^1.0.0" - semver "^6.2.0" - slash "^2.0.0" - temp-write "^3.4.0" - write-json-file "^3.2.0" + p-map "^4.0.0" + p-pipe "^3.1.0" + p-reduce "^2.1.0" + p-waterfall "^2.1.1" + semver "^7.3.4" + slash "^3.0.0" + temp-write "^4.0.0" + write-json-file "^4.3.0" -"@lerna/write-log-file@3.13.0": - version "3.13.0" - resolved "https://registry.npmjs.org/@lerna/write-log-file/-/write-log-file-3.13.0.tgz#b78d9e4cfc1349a8be64d91324c4c8199e822a26" - integrity sha512-RibeMnDPvlL8bFYW5C8cs4mbI3AHfQef73tnJCQ/SgrXZHehmHnsyWUiE7qDQCAo+B1RfTapvSyFF69iPj326A== +"@lerna/write-log-file@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/write-log-file/-/write-log-file-4.0.0.tgz#18221a38a6a307d6b0a5844dd592ad53fa27091e" + integrity sha512-XRG5BloiArpXRakcnPHmEHJp+4AtnhRtpDIHSghmXD5EichI1uD73J7FgPp30mm2pDRq3FdqB0NbwSEsJ9xFQg== dependencies: npmlog "^4.1.2" - write-file-atomic "^2.3.0" + write-file-atomic "^3.0.3" "@manypkg/find-root@^1.1.0": version "1.1.0" @@ -4144,6 +4004,34 @@ "@nodelib/fs.scandir" "2.1.3" fastq "^1.6.0" +"@npmcli/ci-detect@^1.0.0": + version "1.3.0" + resolved "https://registry.npmjs.org/@npmcli/ci-detect/-/ci-detect-1.3.0.tgz#6c1d2c625fb6ef1b9dea85ad0a5afcbef85ef22a" + integrity sha512-oN3y7FAROHhrAt7Rr7PnTSwrHrZVRTS2ZbyxeQwSSYD0ifwM3YNgQqbaRmjcWoPyq77MjchusjJDspbzMmip1Q== + +"@npmcli/git@^2.0.1": + version "2.0.6" + resolved "https://registry.npmjs.org/@npmcli/git/-/git-2.0.6.tgz#47b97e96b2eede3f38379262fa3bdfa6eae57bf2" + integrity sha512-a1MnTfeRPBaKbFY07fd+6HugY1WAkKJzdiJvlRub/9o5xz2F/JtPacZZapx5zRJUQFIzSL677vmTSxEcDMrDbg== + dependencies: + "@npmcli/promise-spawn" "^1.1.0" + lru-cache "^6.0.0" + mkdirp "^1.0.3" + npm-pick-manifest "^6.0.0" + promise-inflight "^1.0.1" + promise-retry "^2.0.1" + semver "^7.3.2" + unique-filename "^1.1.1" + which "^2.0.2" + +"@npmcli/installed-package-contents@^1.0.6": + version "1.0.7" + resolved "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-1.0.7.tgz#ab7408c6147911b970a8abe261ce512232a3f4fa" + integrity sha512-9rufe0wnJusCQoLpV9ZPKIVP55itrM5BxOXs10DmdbRfgWtHy1LDyskbwRnBghuB0PrF7pNPOqREVtpz4HqzKw== + dependencies: + npm-bundled "^1.1.1" + npm-normalize-package-bin "^1.0.1" + "@npmcli/move-file@^1.0.1": version "1.0.1" resolved "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.0.1.tgz#de103070dac0f48ce49cf6693c23af59c0f70464" @@ -4151,6 +4039,30 @@ dependencies: mkdirp "^1.0.4" +"@npmcli/node-gyp@^1.0.2": + version "1.0.2" + resolved "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-1.0.2.tgz#3cdc1f30e9736dbc417373ed803b42b1a0a29ede" + integrity sha512-yrJUe6reVMpktcvagumoqD9r08fH1iRo01gn1u0zoCApa9lnZGEigVKUd2hzsCId4gdtkZZIVscLhNxMECKgRg== + +"@npmcli/promise-spawn@^1.1.0", "@npmcli/promise-spawn@^1.2.0", "@npmcli/promise-spawn@^1.3.2": + version "1.3.2" + resolved "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-1.3.2.tgz#42d4e56a8e9274fba180dabc0aea6e38f29274f5" + integrity sha512-QyAGYo/Fbj4MXeGdJcFzZ+FkDkomfRBrPM+9QYJSg+PxgAUL+LU3FneQk37rKR2/zjqkCV1BLHccX98wRXG3Sg== + dependencies: + infer-owner "^1.0.4" + +"@npmcli/run-script@^1.8.2": + version "1.8.3" + resolved "https://registry.npmjs.org/@npmcli/run-script/-/run-script-1.8.3.tgz#07f440ed492400bb1114369bc37315eeaaae2bb3" + integrity sha512-ELPGWAVU/xyU+A+H3pEPj0QOvYwLTX71RArXcClFzeiyJ/b/McsZ+d0QxpznvfFtZzxGN/gz/1cvlqICR4/suQ== + dependencies: + "@npmcli/node-gyp" "^1.0.2" + "@npmcli/promise-spawn" "^1.3.2" + infer-owner "^1.0.4" + node-gyp "^7.1.0" + puka "^1.0.1" + read-package-json-fast "^2.0.1" + "@octokit/auth-app@^2.10.5": version "2.10.5" resolved "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-2.10.5.tgz#85d69cb96818f5da34bf0b81bb637d3675ad4e9a" @@ -4165,7 +4077,7 @@ universal-github-app-jwt "^1.0.1" universal-user-agent "^6.0.0" -"@octokit/auth-token@^2.4.0", "@octokit/auth-token@^2.4.4": +"@octokit/auth-token@^2.4.4": version "2.4.4" resolved "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.4.tgz#ee31c69b01d0378c12fd3ffe406030f3d94d3b56" integrity sha512-LNfGu3Ro9uFAYh10MUZVaT7X2CnNm2C8IDQmabx+3DygYIQjs9FwzFAHN/0t6mu5HEPhxcb1XOuxdpY82vCg2Q== @@ -4207,18 +4119,16 @@ resolved "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-2.2.0.tgz#123e0438a0bc718ccdac3b5a2e69b3dd00daa85b" integrity sha512-274lNUDonw10kT8wHg8fCcUc1ZjZHbWv0/TbAwb0ojhBQqZYc1cQ/4yqTVTtPMDeZ//g7xVEYe/s3vURkRghPg== +"@octokit/openapi-types@^4.0.2": + version "4.0.2" + resolved "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-4.0.2.tgz#4b2bb553a16ab9e0fdeb29bd453b1c88cf129929" + integrity sha512-quqmeGTjcVks8YaatVGCpt7QpUTs2PK0D3mW5aEQqmFKOuIZ/CxwWrgnggPjqP3CNp6eALdQRgf0jUpcG8X1/Q== + "@octokit/plugin-enterprise-rest@^6.0.1": version "6.0.1" resolved "https://registry.npmjs.org/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz#e07896739618dab8da7d4077c658003775f95437" integrity sha512-93uGjlhUD+iNg1iWhUENAtJata6w5nE+V4urXOAlIXdco6xNZtUSfYY8dzp3Udy74aqO/B5UZL80x/YMa5PKRw== -"@octokit/plugin-paginate-rest@^1.1.1": - version "1.1.2" - resolved "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-1.1.2.tgz#004170acf8c2be535aba26727867d692f7b488fc" - integrity sha512-jbsSoi5Q1pj63sC16XIUboklNw+8tL9VOnJsWycWYR78TKss5PVpIPb1TUUcMQ+bBh7cY579cVAWmf5qG+dw+Q== - dependencies: - "@octokit/types" "^2.0.1" - "@octokit/plugin-paginate-rest@^2.6.2": version "2.7.0" resolved "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.7.0.tgz#6bb7b043c246e0654119a6ec4e72a172c9e2c7f3" @@ -4226,17 +4136,17 @@ dependencies: "@octokit/types" "^6.0.1" -"@octokit/plugin-request-log@^1.0.0", "@octokit/plugin-request-log@^1.0.2": +"@octokit/plugin-request-log@^1.0.2": version "1.0.2" resolved "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.2.tgz#394d59ec734cd2f122431fbaf05099861ece3c44" integrity sha512-oTJSNAmBqyDR41uSMunLQKMX0jmEXbwD1fpz8FG27lScV3RhtGfBa1/BBLym+PxcC16IBlF7KH9vP1BUYxA+Eg== -"@octokit/plugin-rest-endpoint-methods@2.4.0": - version "2.4.0" - resolved "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-2.4.0.tgz#3288ecf5481f68c494dd0602fc15407a59faf61e" - integrity sha512-EZi/AWhtkdfAYi01obpX0DF7U6b1VRr30QNQ5xSFPITMdLSfhcBqjamE3F+sKcxPbD7eZuMHu3Qkk2V+JGxBDQ== +"@octokit/plugin-rest-endpoint-methods@4.10.1": + version "4.10.1" + resolved "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.10.1.tgz#b7a9181d1f52fef70a13945c5b49cffa51862da1" + integrity sha512-YGMiEidTORzgUmYZu0eH4q2k8kgQSHQMuBOBYiKxUYs/nXea4q/Ze6tDzjcRAPmHNJYXrENs1bEMlcdGKT+8ug== dependencies: - "@octokit/types" "^2.0.1" + "@octokit/types" "^6.8.2" deprecation "^2.3.1" "@octokit/plugin-rest-endpoint-methods@4.4.1": @@ -4247,15 +4157,6 @@ "@octokit/types" "^6.1.0" deprecation "^2.3.1" -"@octokit/request-error@^1.0.2": - version "1.2.1" - resolved "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.1.tgz#ede0714c773f32347576c25649dc013ae6b31801" - integrity sha512-+6yDyk1EES6WK+l3viRDElw96MvwfJxCt45GvmjDUKWjYIb3PJZQkq3i46TwGwoPD4h8NmTrENmtyA1FwbmhRA== - dependencies: - "@octokit/types" "^2.0.0" - deprecation "^2.0.0" - once "^1.4.0" - "@octokit/request-error@^2.0.0": version "2.0.2" resolved "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.2.tgz#0e76b83f5d8fdda1db99027ea5f617c2e6ba9ed0" @@ -4265,7 +4166,7 @@ deprecation "^2.0.0" once "^1.4.0" -"@octokit/request@^5.2.0", "@octokit/request@^5.3.0", "@octokit/request@^5.4.11", "@octokit/request@^5.4.12": +"@octokit/request@^5.3.0", "@octokit/request@^5.4.11", "@octokit/request@^5.4.12": version "5.4.13" resolved "https://registry.npmjs.org/@octokit/request/-/request-5.4.13.tgz#eec5987b3e96f984fc5f41967e001170c6d23a18" integrity sha512-WcNRH5XPPtg7i1g9Da5U9dvZ6YbTffw9BN2rVezYiE7couoSyaRsw0e+Tl8uk1fArHE7Dn14U7YqUDy59WaqEw== @@ -4279,28 +4180,6 @@ once "^1.4.0" universal-user-agent "^6.0.0" -"@octokit/rest@^16.28.4": - version "16.43.1" - resolved "https://registry.npmjs.org/@octokit/rest/-/rest-16.43.1.tgz#3b11e7d1b1ac2bbeeb23b08a17df0b20947eda6b" - integrity sha512-gfFKwRT/wFxq5qlNjnW2dh+qh74XgTQ2B179UX5K1HYCluioWj8Ndbgqw2PVqa1NnVJkGHp2ovMpVn/DImlmkw== - dependencies: - "@octokit/auth-token" "^2.4.0" - "@octokit/plugin-paginate-rest" "^1.1.1" - "@octokit/plugin-request-log" "^1.0.0" - "@octokit/plugin-rest-endpoint-methods" "2.4.0" - "@octokit/request" "^5.2.0" - "@octokit/request-error" "^1.0.2" - atob-lite "^2.0.0" - before-after-hook "^2.0.0" - btoa-lite "^1.0.0" - deprecation "^2.0.0" - lodash.get "^4.4.2" - lodash.set "^4.3.2" - lodash.uniq "^4.5.0" - octokit-pagination-methods "^1.1.0" - once "^1.4.0" - universal-user-agent "^4.0.0" - "@octokit/rest@^18.0.0", "@octokit/rest@^18.0.12": version "18.0.12" resolved "https://registry.npmjs.org/@octokit/rest/-/rest-18.0.12.tgz#278bd41358c56d87c201e787e8adc0cac132503a" @@ -4311,12 +4190,15 @@ "@octokit/plugin-request-log" "^1.0.2" "@octokit/plugin-rest-endpoint-methods" "4.4.1" -"@octokit/types@^2.0.0", "@octokit/types@^2.0.1": - version "2.5.0" - resolved "https://registry.npmjs.org/@octokit/types/-/types-2.5.0.tgz#f1bbd147e662ae2c79717d518aac686e58257773" - integrity sha512-KEnLwOfdXzxPNL34fj508bhi9Z9cStyN7qY1kOfVahmqtAfrWw6Oq3P4R+dtsg0lYtZdWBpUrS/Ixmd5YILSww== +"@octokit/rest@^18.1.0": + version "18.1.0" + resolved "https://registry.npmjs.org/@octokit/rest/-/rest-18.1.0.tgz#9bf72604911a3433165bcc924263c9a706d32804" + integrity sha512-YQfpTzWV3jdzDPyXQVO54f5I2t1zxk/S53Vbe+Aa5vQj6MdTx6sNEWzmUzUO8lSVowbGOnjcQHzW1A8ATr+/7g== dependencies: - "@types/node" ">= 8" + "@octokit/core" "^3.2.3" + "@octokit/plugin-paginate-rest" "^2.6.2" + "@octokit/plugin-request-log" "^1.0.2" + "@octokit/plugin-rest-endpoint-methods" "4.10.1" "@octokit/types@^5.0.0", "@octokit/types@^5.0.1": version "5.5.0" @@ -4333,6 +4215,14 @@ "@octokit/openapi-types" "^2.2.0" "@types/node" ">= 8" +"@octokit/types@^6.8.2": + version "6.8.3" + resolved "https://registry.npmjs.org/@octokit/types/-/types-6.8.3.tgz#1960951103c836ab2e55fe47a8da2bf76402824f" + integrity sha512-ZNAy8z77ewKZ5LCX0KaUm4tWdgloWQ6FWJCh06qgahq/MH13sQefIPKSo0dBdPU3bcioltyZUcC0k8oHHfjvnQ== + dependencies: + "@octokit/openapi-types" "^4.0.2" + "@types/node" ">= 8" + "@open-draft/until@^1.0.3": version "1.0.3" resolved "https://registry.npmjs.org/@open-draft/until/-/until-1.0.3.tgz#db9cc719191a62e7d9200f6e7bab21c5b848adca" @@ -6311,7 +6201,7 @@ dependencies: "@types/webpack" "*" -"@types/minimatch@*": +"@types/minimatch@*", "@types/minimatch@^3.0.3": version "3.0.3" resolved "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== @@ -7214,16 +7104,7 @@ resolved "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== -"@zkochan/cmd-shim@^3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@zkochan/cmd-shim/-/cmd-shim-3.1.0.tgz#2ab8ed81f5bb5452a85f25758eb9b8681982fd2e" - integrity sha512-o8l0+x7C7sMZU3v9GuJIAU10qQLtwR1dtRQIOmlNMtyaqhmpXOzx1HWiYoWfmmf9HHZoAkXpc9TM9PQYF9d4Jg== - dependencies: - is-windows "^1.0.0" - mkdirp-promise "^5.0.1" - mz "^2.5.0" - -JSONStream@^1.0.4, JSONStream@^1.3.4: +JSONStream@^1.0.4: version "1.3.5" resolved "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== @@ -7312,18 +7193,16 @@ acorn@^7.4.0: resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== +add-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" + integrity sha1-anmQQ3ynNtXhKI25K9MmbV9csqo= + address@1.1.2, address@^1.0.1: version "1.1.2" resolved "https://registry.npmjs.org/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6" integrity sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA== -agent-base@4, agent-base@^4.3.0: - version "4.3.0" - resolved "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" - integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== - dependencies: - es6-promisify "^5.0.0" - agent-base@6: version "6.0.1" resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz#808007e4e5867decb0ab6ab2f928fbdb5a596db4" @@ -7331,18 +7210,13 @@ agent-base@6: dependencies: debug "4" -agent-base@~4.2.1: - version "4.2.1" - resolved "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" - integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg== - dependencies: - es6-promisify "^5.0.0" - -agentkeepalive@^3.4.1: - version "3.5.2" - resolved "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-3.5.2.tgz#a113924dd3fa24a0bc3b78108c450c2abee00f67" - integrity sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ== +agentkeepalive@^4.1.3: + version "4.1.4" + resolved "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.1.4.tgz#d928028a4862cb11718e55227872e842a44c945b" + integrity sha512-+V/rGa3EuU74H6wR04plBb7Ks10FbtUQgRj/FQOG7uUIEuaINI+AiqJR1k6t3SVNs7o7ZjIdus6706qqzVq8jQ== dependencies: + debug "^4.1.0" + depd "^1.1.2" humanize-ms "^1.2.1" aggregate-error@3.0.1, aggregate-error@^3.0.0: @@ -7435,7 +7309,7 @@ ansi-colors@^4.1.1: resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== -ansi-escapes@^3.0.0, ansi-escapes@^3.2.0: +ansi-escapes@^3.0.0: version "3.2.0" resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== @@ -7837,10 +7711,10 @@ arr-union@^3.1.0: resolved "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= -array-differ@^2.0.3: - version "2.1.0" - resolved "https://registry.npmjs.org/array-differ/-/array-differ-2.1.0.tgz#4b9c1c3f14b906757082925769e8ab904f4801b1" - integrity sha512-KbUpJgx909ZscOc/7CLATBFam7P1Z1QRQInvgT0UztM9Q72aGKCunKASAl7WNW0tnPmPyEMeMhdsfWhfmW037w== +array-differ@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" + integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== array-each@^1.0.1: version "1.0.1" @@ -8070,11 +7944,6 @@ at-least-node@^1.0.0: resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== -atob-lite@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz#0fef5ad46f1bd7a8502c65727f0367d5ee43d696" - integrity sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY= - atob@^2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" @@ -8631,7 +8500,7 @@ bcrypt-pbkdf@^1.0.0, bcrypt-pbkdf@^1.0.2: dependencies: tweetnacl "^0.14.3" -before-after-hook@^2.0.0, before-after-hook@^2.1.0: +before-after-hook@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635" integrity sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A== @@ -8716,7 +8585,7 @@ block-stream@*: dependencies: inherits "~2.0.0" -bluebird@3.7.2, bluebird@^3.3.5, bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.5.5, bluebird@^3.7.2: +bluebird@3.7.2, bluebird@^3.3.5, bluebird@^3.5.1, bluebird@^3.5.5, bluebird@^3.7.2: version "3.7.2" resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -8948,11 +8817,6 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" -btoa-lite@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337" - integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc= - btoa@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73" @@ -9072,10 +8936,10 @@ byline@^5.0.0: resolved "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE= -byte-size@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/byte-size/-/byte-size-5.0.1.tgz#4b651039a5ecd96767e71a3d7ed380e48bed4191" - integrity sha512-/XuKeqWocKsYa/cBY1YbSJSWWqTi4cFgr9S6OyM7PBaPbr9zvNGwWP33vt0uqGhwDdN+y3yhbXVILEUpnwEWGw== +byte-size@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/byte-size/-/byte-size-7.0.0.tgz#36528cd1ca87d39bd9abd51f5715dc93b6ceb032" + integrity sha512-NNiBxKgxybMBtWdmvx7ZITJi4ZG+CYUgwOSZTfqB1qogkRHrhbQE/R2r5Fh94X+InN5MCYz6SvB/ejHMj/HbsQ== bytes@3.0.0: version "3.0.0" @@ -9087,7 +8951,7 @@ bytes@3.1.0: resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== -cacache@^12.0.0, cacache@^12.0.2, cacache@^12.0.3: +cacache@^12.0.2: version "12.0.3" resolved "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz#be99abba4e1bf5df461cd5a2c1071fc432573390" integrity sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw== @@ -9235,15 +9099,6 @@ camelcase-keys@^2.0.0: camelcase "^2.0.0" map-obj "^1.0.0" -camelcase-keys@^4.0.0: - version "4.2.0" - resolved "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz#a2aa5fb1af688758259c32c141426d78923b9b77" - integrity sha1-oqpfsa9oh1glnDLBQUJteJI7m3c= - dependencies: - camelcase "^4.1.0" - map-obj "^2.0.0" - quick-lru "^1.0.0" - camelcase-keys@^6.2.2: version "6.2.2" resolved "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" @@ -9263,7 +9118,7 @@ camelcase@^3.0.0: resolved "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo= -camelcase@^4.0.0, camelcase@^4.1.0: +camelcase@^4.0.0: version "4.1.0" resolved "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= @@ -9348,7 +9203,7 @@ chainsaw@~0.1.0: dependencies: traverse ">=0.3.0 <0.4" -chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.4.1, chalk@^2.4.2: +chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -9453,7 +9308,7 @@ chokidar@^3.2.2, chokidar@^3.3.0, chokidar@^3.3.1, chokidar@^3.4.1, chokidar@^3. optionalDependencies: fsevents "~2.1.2" -chownr@^1.1.1, chownr@^1.1.2: +chownr@^1.1.1: version "1.1.4" resolved "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== @@ -9696,6 +9551,13 @@ clsx@^1.0.1, clsx@^1.0.2, clsx@^1.0.4, clsx@^1.1.0: resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== +cmd-shim@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/cmd-shim/-/cmd-shim-4.1.0.tgz#b3a904a6743e9fede4148c6f3800bf2a08135bdd" + integrity sha512-lb9L7EM4I/ZRVuljLPEtUJOP+xiQVknZ4ZMpMgEp4JzNldPb27HU03hi6K1/6CoIuit/Zm/LQXySErFeXxDprw== + dependencies: + mkdirp-infer-owner "^2.0.0" + co@^4.6.0: version "4.6.0" resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -9887,13 +9749,13 @@ commondir@^1.0.1: resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= -compare-func@^1.3.1: - version "1.3.2" - resolved "https://registry.npmjs.org/compare-func/-/compare-func-1.3.2.tgz#99dd0ba457e1f9bc722b12c08ec33eeab31fa648" - integrity sha1-md0LpFfh+bxyKxLAjsM+6rMfpkg= +compare-func@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz#fb65e75edbddfd2e568554e8b5b05fff7a51fcb3" + integrity sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA== dependencies: array-ify "^1.0.0" - dot-prop "^3.0.0" + dot-prop "^5.1.0" compare-versions@^3.6.0: version "3.6.0" @@ -10006,7 +9868,7 @@ concurrently@^5.2.0: tree-kill "^1.2.2" yargs "^13.3.0" -config-chain@^1.1.11: +config-chain@^1.1.12: version "1.1.12" resolved "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa" integrity sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA== @@ -10077,87 +9939,89 @@ content-type@~1.0.4: resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== -conventional-changelog-angular@^5.0.3: - version "5.0.6" - resolved "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.6.tgz#269540c624553aded809c29a3508fdc2b544c059" - integrity sha512-QDEmLa+7qdhVIv8sFZfVxU1VSyVvnXPsxq8Vam49mKUcO1Z8VTLEJk9uI21uiJUsnmm0I4Hrsdc9TgkOQo9WSA== +conventional-changelog-angular@^5.0.12: + version "5.0.12" + resolved "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.12.tgz#c979b8b921cbfe26402eb3da5bbfda02d865a2b9" + integrity sha512-5GLsbnkR/7A89RyHLvvoExbiGbd9xKdKqDTrArnPbOqBqG/2wIosu0fHwpeIRI8Tl94MhVNBXcLJZl92ZQ5USw== dependencies: - compare-func "^1.3.1" + compare-func "^2.0.0" q "^1.5.1" -conventional-changelog-core@^3.1.6: - version "3.2.3" - resolved "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-3.2.3.tgz#b31410856f431c847086a7dcb4d2ca184a7d88fb" - integrity sha512-LMMX1JlxPIq/Ez5aYAYS5CpuwbOk6QFp8O4HLAcZxe3vxoCtABkhfjetk8IYdRB9CDQGwJFLR3Dr55Za6XKgUQ== +conventional-changelog-core@^4.2.2: + version "4.2.2" + resolved "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-4.2.2.tgz#f0897df6d53b5d63dec36b9442bd45354f8b3ce5" + integrity sha512-7pDpRUiobQDNkwHyJG7k9f6maPo9tfPzkSWbRq97GGiZqisElhnvUZSvyQH20ogfOjntB5aadvv6NNcKL1sReg== dependencies: - conventional-changelog-writer "^4.0.6" - conventional-commits-parser "^3.0.3" + add-stream "^1.0.0" + conventional-changelog-writer "^4.0.18" + conventional-commits-parser "^3.2.0" dateformat "^3.0.0" get-pkg-repo "^1.0.0" - git-raw-commits "2.0.0" + git-raw-commits "^2.0.8" git-remote-origin-url "^2.0.0" - git-semver-tags "^2.0.3" - lodash "^4.2.1" - normalize-package-data "^2.3.5" + git-semver-tags "^4.1.1" + lodash "^4.17.15" + normalize-package-data "^3.0.0" q "^1.5.1" read-pkg "^3.0.0" read-pkg-up "^3.0.0" - through2 "^3.0.0" + shelljs "^0.8.3" + through2 "^4.0.0" -conventional-changelog-preset-loader@^2.1.1: - version "2.3.0" - resolved "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.0.tgz#580fa8ab02cef22c24294d25e52d7ccd247a9a6a" - integrity sha512-/rHb32J2EJnEXeK4NpDgMaAVTFZS3o1ExmjKMtYVgIC4MQn0vkNSbYpdGRotkfGGRWiqk3Ri3FBkiZGbAfIfOQ== +conventional-changelog-preset-loader@^2.3.4: + version "2.3.4" + resolved "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.4.tgz#14a855abbffd59027fd602581f1f34d9862ea44c" + integrity sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g== -conventional-changelog-writer@^4.0.6: - version "4.0.11" - resolved "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-4.0.11.tgz#9f56d2122d20c96eb48baae0bf1deffaed1edba4" - integrity sha512-g81GQOR392I+57Cw3IyP1f+f42ME6aEkbR+L7v1FBBWolB0xkjKTeCWVguzRrp6UiT1O6gBpJbEy2eq7AnV1rw== +conventional-changelog-writer@^4.0.18: + version "4.1.0" + resolved "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-4.1.0.tgz#1ca7880b75aa28695ad33312a1f2366f4b12659f" + integrity sha512-WwKcUp7WyXYGQmkLsX4QmU42AZ1lqlvRW9mqoyiQzdD+rJWbTepdWoKJuwXTS+yq79XKnQNa93/roViPQrAQgw== dependencies: - compare-func "^1.3.1" - conventional-commits-filter "^2.0.2" + compare-func "^2.0.0" + conventional-commits-filter "^2.0.7" dateformat "^3.0.0" - handlebars "^4.4.0" + handlebars "^4.7.6" json-stringify-safe "^5.0.1" lodash "^4.17.15" - meow "^5.0.0" + meow "^8.0.0" semver "^6.0.0" split "^1.0.0" - through2 "^3.0.0" + through2 "^4.0.0" -conventional-commits-filter@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.2.tgz#f122f89fbcd5bb81e2af2fcac0254d062d1039c1" - integrity sha512-WpGKsMeXfs21m1zIw4s9H5sys2+9JccTzpN6toXtxhpw2VNF2JUXwIakthKBy+LN4DvJm+TzWhxOMWOs1OFCFQ== +conventional-commits-filter@^2.0.7: + version "2.0.7" + resolved "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.7.tgz#f8d9b4f182fce00c9af7139da49365b136c8a0b3" + integrity sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA== dependencies: lodash.ismatch "^4.4.0" modify-values "^1.0.0" -conventional-commits-parser@^3.0.3: - version "3.0.8" - resolved "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.0.8.tgz#23310a9bda6c93c874224375e72b09fb275fe710" - integrity sha512-YcBSGkZbYp7d+Cr3NWUeXbPDFUN6g3SaSIzOybi8bjHL5IJ5225OSCxJJ4LgziyEJ7AaJtE9L2/EU6H7Nt/DDQ== +conventional-commits-parser@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.2.0.tgz#9e261b139ca4b7b29bcebbc54460da36894004ca" + integrity sha512-XmJiXPxsF0JhAKyfA2Nn+rZwYKJ60nanlbSWwwkGwLQFbugsc0gv1rzc7VbbUWAzJfR1qR87/pNgv9NgmxtBMQ== dependencies: JSONStream "^1.0.4" is-text-path "^1.0.1" lodash "^4.17.15" - meow "^5.0.0" + meow "^8.0.0" split2 "^2.0.0" - through2 "^3.0.0" + through2 "^4.0.0" trim-off-newlines "^1.0.0" -conventional-recommended-bump@^5.0.0: - version "5.0.1" - resolved "https://registry.npmjs.org/conventional-recommended-bump/-/conventional-recommended-bump-5.0.1.tgz#5af63903947b6e089e77767601cb592cabb106ba" - integrity sha512-RVdt0elRcCxL90IrNP0fYCpq1uGt2MALko0eyeQ+zQuDVWtMGAy9ng6yYn3kax42lCj9+XBxQ8ZN6S9bdKxDhQ== +conventional-recommended-bump@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/conventional-recommended-bump/-/conventional-recommended-bump-6.1.0.tgz#cfa623285d1de554012f2ffde70d9c8a22231f55" + integrity sha512-uiApbSiNGM/kkdL9GTOLAqC4hbptObFo4wW2QRyHsKciGAfQuLU1ShZ1BIVI/+K2BE/W1AWYQMCXAsv4dyKPaw== dependencies: concat-stream "^2.0.0" - conventional-changelog-preset-loader "^2.1.1" - conventional-commits-filter "^2.0.2" - conventional-commits-parser "^3.0.3" - git-raw-commits "2.0.0" - git-semver-tags "^2.0.3" - meow "^4.0.0" + conventional-changelog-preset-loader "^2.3.4" + conventional-commits-filter "^2.0.7" + conventional-commits-parser "^3.2.0" + git-raw-commits "^2.0.8" + git-semver-tags "^4.1.1" + meow "^8.0.0" q "^1.5.1" convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: @@ -10266,7 +10130,7 @@ cosmiconfig@6.0.0, cosmiconfig@^6.0.0: path-type "^4.0.0" yaml "^1.7.2" -cosmiconfig@^5.0.0, cosmiconfig@^5.1.0: +cosmiconfig@^5.0.0: version "5.2.1" resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== @@ -10418,7 +10282,7 @@ cross-spawn@^6.0.0: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2: +cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -10963,12 +10827,10 @@ damerau-levenshtein@^1.0.4: resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz#143c1641cb3d85c60c32329e26899adea8701791" integrity sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug== -dargs@^4.0.1: - version "4.1.0" - resolved "https://registry.npmjs.org/dargs/-/dargs-4.1.0.tgz#03a9dbb4b5c2f139bf14ae53f0b8a2a6a86f4e17" - integrity sha1-A6nbtLXC8Tm/FK5T8LiipqhvThc= - dependencies: - number-is-nan "^1.0.0" +dargs@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" + integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg== dashdash@^1.12.0: version "1.14.1" @@ -11047,13 +10909,6 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9: dependencies: ms "2.0.0" -debug@3.1.0, debug@=3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" @@ -11068,6 +10923,13 @@ debug@4.1.1: dependencies: ms "^2.1.1" +debug@=3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6: version "3.2.6" resolved "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" @@ -11080,7 +10942,7 @@ debuglog@^1.0.1: resolved "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= -decamelize-keys@^1.0.0, decamelize-keys@^1.1.0: +decamelize-keys@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk= @@ -11262,7 +11124,7 @@ delegates@^1.0.0: resolved "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= -depd@~1.1.2: +depd@^1.1.2, depd@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= @@ -11612,19 +11474,12 @@ dot-case@^3.0.3: no-case "^3.0.3" tslib "^1.10.0" -dot-prop@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-3.0.0.tgz#1b708af094a49c9a0e7dbcad790aba539dac1177" - integrity sha1-G3CK8JSknJoOfbyteQq6U52sEXc= +dot-prop@^5.1.0: + version "5.3.0" + resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" + integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== dependencies: - is-obj "^1.0.0" - -dot-prop@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" - integrity sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ== - dependencies: - is-obj "^1.0.0" + is-obj "^2.0.0" dot-prop@^5.2.0: version "5.2.0" @@ -11841,12 +11696,12 @@ encodeurl@~1.0.2: resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= -encoding@^0.1.11: - version "0.1.12" - resolved "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" - integrity sha1-U4tm8+5izRq1HsMjgp0flIDHS+s= +encoding@^0.1.12: + version "0.1.13" + resolved "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== dependencies: - iconv-lite "~0.4.13" + iconv-lite "^0.6.2" end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" @@ -11905,15 +11760,15 @@ env-variable@0.0.x: resolved "https://registry.npmjs.org/env-variable/-/env-variable-0.0.6.tgz#74ab20b3786c545b62b4a4813ab8cf22726c9808" integrity sha512-bHz59NlBbtS0NhftmR8+ExBEekE7br0e01jw+kk0NDro7TtZzBYZ5ScGPs3OmwnpyfHTHOtr1Y6uedCdrIldtg== -envinfo@^7.3.1: - version "7.5.0" - resolved "https://registry.npmjs.org/envinfo/-/envinfo-7.5.0.tgz#91410bb6db262fb4f1409bd506e9ff57e91023f4" - integrity sha512-jDgnJaF/Btomk+m3PZDTTCb5XIIIX3zYItnCRfF73zVgvinLoRomuhi75Y4su0PtQxWz4v66XnLLckyvyJTOIQ== +envinfo@^7.7.4: + version "7.7.4" + resolved "https://registry.npmjs.org/envinfo/-/envinfo-7.7.4.tgz#c6311cdd38a0e86808c1c9343f667e4267c4a320" + integrity sha512-TQXTYFVVwwluWSFis6K2XKxgrD22jEv0FTuLCQI+OjH7rn93+iY0fSSFM5lrSxFY+H1+B0/cvvlamr3UsBivdQ== -err-code@^1.0.0: - version "1.1.2" - resolved "https://registry.npmjs.org/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960" - integrity sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA= +err-code@^2.0.2: + version "2.0.3" + resolved "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" + integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== errno@^0.1.3, errno@~0.1.7: version "0.1.7" @@ -12038,18 +11893,6 @@ es6-iterator@^2.0.3, es6-iterator@~2.0.3: es5-ext "^0.10.35" es6-symbol "^3.1.1" -es6-promise@^4.0.3: - version "4.2.8" - resolved "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" - integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== - -es6-promisify@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" - integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= - dependencies: - es6-promise "^4.0.3" - es6-shim@^0.35.5: version "0.35.5" resolved "https://registry.npmjs.org/es6-shim/-/es6-shim-0.35.5.tgz#46f59dc0a84a1c5029e8ff1166ca0a902077a9ab" @@ -12453,6 +12296,11 @@ eventemitter3@^4.0.0: resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb" integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg== +eventemitter3@^4.0.4: + version "4.0.7" + resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + events@1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" @@ -12540,6 +12388,21 @@ execa@^4.0.0, execa@^4.0.1: signal-exit "^3.0.2" strip-final-newline "^2.0.0" +execa@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/execa/-/execa-5.0.0.tgz#4029b0007998a841fbd1032e5f4de86a3c1e3376" + integrity sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + executable@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c" @@ -12904,7 +12767,7 @@ fetch-readablestream@^0.2.0: resolved "https://registry.npmjs.org/fetch-readablestream/-/fetch-readablestream-0.2.0.tgz#eaa6d1a76b12de2d4731a343393c6ccdcfe2c795" integrity sha512-qu4mXWf4wus4idBIN/kVH+XSer8IZ9CwHP+Pd7DL7TuKNC1hP7ykon4kkBjwJF3EMX2WsFp4hH7gU7CyL7ucXw== -figgy-pudding@^3.4.1, figgy-pudding@^3.5.1: +figgy-pudding@^3.5.1: version "3.5.1" resolved "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" integrity sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w== @@ -13325,6 +13188,16 @@ fs-extra@^7.0.1: jsonfile "^4.0.0" universalify "^0.1.0" +fs-extra@^9.1.0: + version "9.1.0" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-minipass@^1.2.5: version "1.2.7" resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" @@ -13332,7 +13205,7 @@ fs-minipass@^1.2.5: dependencies: minipass "^2.6.0" -fs-minipass@^2.0.0: +fs-minipass@^2.0.0, fs-minipass@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== @@ -13475,11 +13348,6 @@ generic-pool@2.4.3: resolved "https://registry.npmjs.org/generic-pool/-/generic-pool-2.4.3.tgz#780c36f69dfad05a5a045dd37be7adca11a4f6ff" integrity sha1-eAw29p360FpaBF3Te+etyhGk9v8= -genfun@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537" - integrity sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA== - gensync@^1.0.0-beta.1: version "1.0.0-beta.1" resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" @@ -13528,11 +13396,6 @@ get-pkg-repo@^1.0.0: parse-github-repo-url "^1.3.0" through2 "^2.0.0" -get-port@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/get-port/-/get-port-4.2.0.tgz#e37368b1e863b7629c43c5a323625f95cf24b119" - integrity sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw== - get-port@^5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" @@ -13596,16 +13459,16 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" -git-raw-commits@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.0.tgz#d92addf74440c14bcc5c83ecce3fb7f8a79118b5" - integrity sha512-w4jFEJFgKXMQJ0H0ikBk2S+4KP2VEjhCvLCNqbNRQC8BgGWgLKNCO7a9K9LI+TVT7Gfoloje502sEnctibffgg== +git-raw-commits@^2.0.8: + version "2.0.10" + resolved "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.10.tgz#e2255ed9563b1c9c3ea6bd05806410290297bbc1" + integrity sha512-sHhX5lsbG9SOO6yXdlwgEMQ/ljIn7qMpAbJZCGfXX2fq5T8M5SrDnpYk9/4HswTildcIqatsWa91vty6VhWSaQ== dependencies: - dargs "^4.0.1" - lodash.template "^4.0.2" - meow "^4.0.0" - split2 "^2.0.0" - through2 "^2.0.0" + dargs "^7.0.0" + lodash "^4.17.15" + meow "^8.0.0" + split2 "^3.0.0" + through2 "^4.0.0" git-remote-origin-url@^2.0.0: version "2.0.0" @@ -13615,12 +13478,12 @@ git-remote-origin-url@^2.0.0: gitconfiglocal "^1.0.0" pify "^2.3.0" -git-semver-tags@^2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-2.0.3.tgz#48988a718acf593800f99622a952a77c405bfa34" - integrity sha512-tj4FD4ww2RX2ae//jSrXZzrocla9db5h0V7ikPl1P/WwoZar9epdUhwR7XHXSgc+ZkNq72BEEerqQuicoEQfzA== +git-semver-tags@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-4.1.1.tgz#63191bcd809b0ec3e151ba4751c16c444e5b5780" + integrity sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA== dependencies: - meow "^4.0.0" + meow "^8.0.0" semver "^6.0.0" git-up@^4.0.0: @@ -13631,13 +13494,6 @@ git-up@^4.0.0: is-ssh "^1.3.0" parse-url "^5.0.0" -git-url-parse@^11.1.2: - version "11.1.3" - resolved "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.1.3.tgz#03625b6fc09905e9ad1da7bb2b84be1bf9123143" - integrity sha512-GPsfwticcu52WQ+eHp0IYkAyaOASgYdtsQDIt4rUp6GbiNt1P9ddrh3O0kQB0eD4UJZszVqNT3+9Zwcg40fywA== - dependencies: - git-up "^4.0.0" - git-url-parse@^11.4.4: version "11.4.4" resolved "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.4.4.tgz#5d747debc2469c17bc385719f7d0427802d83d77" @@ -13972,6 +13828,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.5 resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== +graceful-fs@^4.2.3: + version "4.2.6" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" + integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== + grapheme-splitter@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" @@ -14171,7 +14032,7 @@ handle-thing@^2.0.0: resolved "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== -handlebars@^4.4.0, handlebars@^4.7.3: +handlebars@^4.7.3, handlebars@^4.7.6: version "4.7.6" resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e" integrity sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA== @@ -14400,7 +14261,7 @@ hoopy@^0.1.4: resolved "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz#609207d661100033a9a9402ad3dea677381c1b1d" integrity sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ== -hosted-git-info@^2.1.4, hosted-git-info@^2.7.1: +hosted-git-info@^2.1.4: version "2.8.8" resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== @@ -14533,12 +14394,7 @@ htmlparser2@^4.0, htmlparser2@^4.1.0: domutils "^2.0.0" entities "^2.0.0" -http-cache-semantics@^3.8.1: - version "3.8.1" - resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" - integrity sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w== - -http-cache-semantics@^4.0.0: +http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== @@ -14596,14 +14452,6 @@ http-errors@~1.7.2: resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4" integrity sha1-ksnBN0w1CF912zWexWzCV8u5P6Q= -http-proxy-agent@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405" - integrity sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg== - dependencies: - agent-base "4" - debug "3.1.0" - http-proxy-agent@^4.0.0, http-proxy-agent@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" @@ -14664,14 +14512,6 @@ https-browserify@^1.0.0: resolved "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= -https-proxy-agent@^2.2.3: - version "2.2.4" - resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" - integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== - dependencies: - agent-base "^4.3.0" - debug "^3.1.0" - https-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" @@ -14690,6 +14530,11 @@ human-signals@^1.1.1: resolved "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + humanize-ms@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" @@ -14718,13 +14563,20 @@ hyphenate-style-name@^1.0.2, hyphenate-style-name@^1.0.3: resolved "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz#097bb7fa0b8f1a9cf0bd5c734cf95899981a9b48" integrity sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ== -iconv-lite@0.4.24, iconv-lite@^0.4.21, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: +iconv-lite@0.4.24, iconv-lite@^0.4.21, iconv-lite@^0.4.24, iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== dependencies: safer-buffer ">= 2.1.2 < 3" +iconv-lite@^0.6.2: + version "0.6.2" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz#ce13d1875b0c3a674bd6a04b7f76b01b1b6ded01" + integrity sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + icss-replace-symbols@1.1.0, icss-replace-symbols@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" @@ -14764,7 +14616,7 @@ ignore-by-default@^1.0.1: resolved "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= -ignore-walk@^3.0.1: +ignore-walk@^3.0.1, ignore-walk@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37" integrity sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw== @@ -14931,18 +14783,18 @@ ini@^1.3.2, ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== -init-package-json@^1.10.3: - version "1.10.3" - resolved "https://registry.npmjs.org/init-package-json/-/init-package-json-1.10.3.tgz#45ffe2f610a8ca134f2bd1db5637b235070f6cbe" - integrity sha512-zKSiXKhQveNteyhcj1CoOP8tqp1QuxPIPBl8Bid99DGLFqA1p87M6lNgfjJHSBoWJJlidGOv5rWjyYKEB3g2Jw== +init-package-json@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/init-package-json/-/init-package-json-2.0.2.tgz#d81a7e6775af9b618f20bba288e440b8d1ce05f3" + integrity sha512-PO64kVeArePvhX7Ff0jVWkpnE1DfGRvaWcStYrPugcJz9twQGYibagKJuIMHCX7ENcp0M6LJlcjLBuLD5KeJMg== dependencies: glob "^7.1.1" - npm-package-arg "^4.0.0 || ^5.0.0 || ^6.0.0" + npm-package-arg "^8.1.0" promzard "^0.3.0" read "~1.0.1" - read-package-json "1 || 2" - semver "2.x || 3.x || 4 || 5" - validate-npm-package-license "^3.0.1" + read-package-json "^3.0.0" + semver "^7.3.2" + validate-npm-package-license "^3.0.4" validate-npm-package-name "^3.0.0" inline-style-prefixer@^4.0.0: @@ -14972,25 +14824,6 @@ inquirer@7.0.4: strip-ansi "^5.1.0" through "^2.3.6" -inquirer@^6.2.0: - version "6.5.2" - resolved "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca" - integrity sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ== - dependencies: - ansi-escapes "^3.2.0" - chalk "^2.4.2" - cli-cursor "^2.1.0" - cli-width "^2.0.0" - external-editor "^3.0.3" - figures "^2.0.0" - lodash "^4.17.12" - mute-stream "0.0.7" - run-async "^2.2.0" - rxjs "^6.4.0" - string-width "^2.1.0" - strip-ansi "^5.1.0" - through "^2.3.6" - inquirer@^7.0.0, inquirer@^7.0.4, inquirer@^7.3.3: version "7.3.3" resolved "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" @@ -15054,7 +14887,7 @@ ip-regex@^2.1.0: resolved "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= -ip@1.1.5, ip@^1.1.0, ip@^1.1.5: +ip@^1.1.0, ip@^1.1.5: version "1.1.5" resolved "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= @@ -15342,6 +15175,11 @@ is-interactive@^1.0.0: resolved "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== +is-lambda@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" + integrity sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU= + is-map@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1" @@ -15374,7 +15212,7 @@ is-number@^7.0.0: resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-obj@^1.0.0, is-obj@^1.0.1: +is-obj@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= @@ -16380,11 +16218,16 @@ json-buffer@3.0.1: resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== -json-parse-better-errors@^1.0.0, json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: +json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + json-pointer@^0.6.0: version "0.6.0" resolved "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.0.tgz#8e500550a6aac5464a473377da57aa6cc22828d7" @@ -16517,7 +16360,7 @@ jsonify@~0.0.0: resolved "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= -jsonparse@^1.2.0: +jsonparse@^1.2.0, jsonparse@^1.3.1: version "1.3.1" resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= @@ -16884,28 +16727,28 @@ left-pad@^1.3.0: resolved "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e" integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA== -lerna@^3.20.2: - version "3.22.1" - resolved "https://registry.npmjs.org/lerna/-/lerna-3.22.1.tgz#82027ac3da9c627fd8bf02ccfeff806a98e65b62" - integrity sha512-vk1lfVRFm+UuEFA7wkLKeSF7Iz13W+N/vFd48aW2yuS7Kv0RbNm2/qcDPV863056LMfkRlsEe+QYOw3palj5Lg== +lerna@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/lerna/-/lerna-4.0.0.tgz#b139d685d50ea0ca1be87713a7c2f44a5b678e9e" + integrity sha512-DD/i1znurfOmNJb0OBw66NmNqiM8kF6uIrzrJ0wGE3VNdzeOhz9ziWLYiRaZDGGwgbcjOo6eIfcx9O5Qynz+kg== dependencies: - "@lerna/add" "3.21.0" - "@lerna/bootstrap" "3.21.0" - "@lerna/changed" "3.21.0" - "@lerna/clean" "3.21.0" - "@lerna/cli" "3.18.5" - "@lerna/create" "3.22.0" - "@lerna/diff" "3.21.0" - "@lerna/exec" "3.21.0" - "@lerna/import" "3.22.0" - "@lerna/info" "3.21.0" - "@lerna/init" "3.21.0" - "@lerna/link" "3.21.0" - "@lerna/list" "3.21.0" - "@lerna/publish" "3.22.1" - "@lerna/run" "3.21.0" - "@lerna/version" "3.22.1" - import-local "^2.0.0" + "@lerna/add" "4.0.0" + "@lerna/bootstrap" "4.0.0" + "@lerna/changed" "4.0.0" + "@lerna/clean" "4.0.0" + "@lerna/cli" "4.0.0" + "@lerna/create" "4.0.0" + "@lerna/diff" "4.0.0" + "@lerna/exec" "4.0.0" + "@lerna/import" "4.0.0" + "@lerna/info" "4.0.0" + "@lerna/init" "4.0.0" + "@lerna/link" "4.0.0" + "@lerna/list" "4.0.0" + "@lerna/publish" "4.0.0" + "@lerna/run" "4.0.0" + "@lerna/version" "4.0.0" + import-local "^3.0.2" npmlog "^4.1.2" leven@^3.1.0: @@ -16934,6 +16777,27 @@ li@^1.3.0: resolved "https://registry.npmjs.org/li/-/li-1.3.0.tgz#22c59bcaefaa9a8ef359cf759784e4bf106aea1b" integrity sha1-IsWbyu+qmo7zWc91l4TkvxBq6hs= +libnpmaccess@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/libnpmaccess/-/libnpmaccess-4.0.1.tgz#17e842e03bef759854adf6eb6c2ede32e782639f" + integrity sha512-ZiAgvfUbvmkHoMTzdwmNWCrQRsDkOC+aM5BDfO0C9aOSwF3R1LdFDBD+Rer1KWtsoQYO35nXgmMR7OUHpDRxyA== + dependencies: + aproba "^2.0.0" + minipass "^3.1.1" + npm-package-arg "^8.0.0" + npm-registry-fetch "^9.0.0" + +libnpmpublish@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/libnpmpublish/-/libnpmpublish-4.0.0.tgz#ad6413914e0dfd78df868ce14ba3d3a4cc8b385b" + integrity sha512-2RwYXRfZAB1x/9udKpZmqEzSqNd7ouBRU52jyG14/xG8EF+O9A62d7/XVR3iABEQHf1iYhkm0Oq9iXjrL3tsXA== + dependencies: + normalize-package-data "^3.0.0" + npm-package-arg "^8.1.0" + npm-registry-fetch "^9.0.0" + semver "^7.1.3" + ssri "^8.0.0" + liftoff@3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz#c9ba6081f908670607ee79062d700df062c52ed3" @@ -17082,17 +16946,6 @@ load-json-file@^4.0.0: pify "^3.0.0" strip-bom "^3.0.0" -load-json-file@^5.3.0: - version "5.3.0" - resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-5.3.0.tgz#4d3c1e01fa1c03ea78a60ac7af932c9ce53403f3" - integrity sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw== - dependencies: - graceful-fs "^4.1.15" - parse-json "^4.0.0" - pify "^4.0.1" - strip-bom "^3.0.0" - type-fest "^0.3.0" - load-json-file@^6.2.0: version "6.2.0" resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-6.2.0.tgz#5c7770b42cafa97074ca2848707c61662f4251a1" @@ -17225,11 +17078,6 @@ lodash.flattendeep@^4.0.0: resolved "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= -lodash.get@^4.4.2: - version "4.4.2" - resolved "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" - integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= - lodash.includes@^4.3.0: version "4.3.0" resolved "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" @@ -17290,7 +17138,7 @@ lodash.startcase@^4.4.0: resolved "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz#9436e34ed26093ed7ffae1936144350915d9add8" integrity sha1-lDbjTtJgk+1/+uGTYUQ1CRXZrdg= -lodash.template@^4.0.2, lodash.template@^4.5.0: +lodash.template@^4.5.0: version "4.5.0" resolved "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== @@ -17330,7 +17178,7 @@ lodash@4.17.15: resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== -lodash@^4.0.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@~4.17.20, lodash@~4.17.4: +lodash@^4.0.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@~4.17.20, lodash@~4.17.4: version "4.17.20" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== @@ -17495,13 +17343,6 @@ magic-string@^0.25.7: dependencies: sourcemap-codec "^1.4.4" -make-dir@^1.0.0: - version "1.3.0" - resolved "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" - integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== - dependencies: - pify "^3.0.0" - make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -17522,22 +17363,26 @@ make-error@1.x, make-error@^1.1.1, make-error@^1.3.6: resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== -make-fetch-happen@^5.0.0: - version "5.0.2" - resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-5.0.2.tgz#aa8387104f2687edca01c8687ee45013d02d19bd" - integrity sha512-07JHC0r1ykIoruKO8ifMXu+xEU8qOXDFETylktdug6vJDACnP+HKevOu3PXyNPzFyTSlz8vrBYlBO1JZRe8Cag== +make-fetch-happen@^8.0.9: + version "8.0.14" + resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-8.0.14.tgz#aaba73ae0ab5586ad8eaa68bd83332669393e222" + integrity sha512-EsS89h6l4vbfJEtBZnENTOFk8mCRpY5ru36Xe5bcX1KYIli2mkSHqoFsp5O1wMDvTJJzxe/4THpCTtygjeeGWQ== dependencies: - agentkeepalive "^3.4.1" - cacache "^12.0.0" - http-cache-semantics "^3.8.1" - http-proxy-agent "^2.1.0" - https-proxy-agent "^2.2.3" - lru-cache "^5.1.1" - mississippi "^3.0.0" - node-fetch-npm "^2.0.2" - promise-retry "^1.1.1" - socks-proxy-agent "^4.0.0" - ssri "^6.0.0" + agentkeepalive "^4.1.3" + cacache "^15.0.5" + http-cache-semantics "^4.1.0" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^6.0.0" + minipass "^3.1.3" + minipass-collect "^1.0.2" + minipass-fetch "^1.3.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + promise-retry "^2.0.1" + socks-proxy-agent "^5.0.0" + ssri "^8.0.0" make-iterator@^1.0.0: version "1.0.1" @@ -17563,11 +17408,6 @@ map-obj@^1.0.0, map-obj@^1.0.1: resolved "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= -map-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9" - integrity sha1-plzSkIepJZi4eRJXpSPgISIqwfk= - map-obj@^4.0.0: version "4.1.0" resolved "https://registry.npmjs.org/map-obj/-/map-obj-4.1.0.tgz#b91221b542734b9f14256c0132c897c5d7256fd5" @@ -17813,36 +17653,6 @@ meow@^3.3.0: redent "^1.0.0" trim-newlines "^1.0.0" -meow@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz#d48598f6f4b1472f35bf6317a95945ace347f975" - integrity sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A== - dependencies: - camelcase-keys "^4.0.0" - decamelize-keys "^1.0.0" - loud-rejection "^1.0.0" - minimist "^1.1.3" - minimist-options "^3.0.1" - normalize-package-data "^2.3.4" - read-pkg-up "^3.0.0" - redent "^2.0.0" - trim-newlines "^2.0.0" - -meow@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/meow/-/meow-5.0.0.tgz#dfc73d63a9afc714a5e371760eb5c88b91078aa4" - integrity sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig== - dependencies: - camelcase-keys "^4.0.0" - decamelize-keys "^1.0.0" - loud-rejection "^1.0.0" - minimist-options "^3.0.1" - normalize-package-data "^2.3.4" - read-pkg-up "^3.0.0" - redent "^2.0.0" - trim-newlines "^2.0.0" - yargs-parser "^10.0.0" - meow@^6.0.0: version "6.1.1" resolved "https://registry.npmjs.org/meow/-/meow-6.1.1.tgz#1ad64c4b76b2a24dfb2f635fddcadf320d251467" @@ -17860,6 +17670,23 @@ meow@^6.0.0: type-fest "^0.13.1" yargs-parser "^18.1.3" +meow@^8.0.0: + version "8.1.2" + resolved "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz#bcbe45bda0ee1729d350c03cffc8395a36c4e897" + integrity sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q== + dependencies: + "@types/minimist" "^1.2.0" + camelcase-keys "^6.2.2" + decamelize-keys "^1.1.0" + hard-rejection "^2.1.0" + minimist-options "4.1.0" + normalize-package-data "^3.0.0" + read-pkg-up "^7.0.1" + redent "^3.0.0" + trim-newlines "^3.0.0" + type-fest "^0.18.0" + yargs-parser "^20.2.3" + merge-deep@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.2.tgz#f39fa100a4f1bd34ff29f7d2bf4508fbb8d83ad2" @@ -18081,15 +17908,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: dependencies: brace-expansion "^1.1.7" -minimist-options@^3.0.1: - version "3.0.2" - resolved "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz#fba4c8191339e13ecf4d61beb03f070103f3d954" - integrity sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ== - dependencies: - arrify "^1.0.1" - is-plain-obj "^1.1.0" - -minimist-options@^4.0.2: +minimist-options@4.1.0, minimist-options@^4.0.2: version "4.1.0" resolved "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A== @@ -18117,6 +17936,17 @@ minipass-collect@^1.0.2: dependencies: minipass "^3.0.0" +minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: + version "1.3.3" + resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.3.3.tgz#34c7cea038c817a8658461bf35174551dce17a0a" + integrity sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ== + dependencies: + minipass "^3.1.0" + minipass-sized "^1.0.3" + minizlib "^2.0.0" + optionalDependencies: + encoding "^0.1.12" + minipass-flush@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" @@ -18124,6 +17954,14 @@ minipass-flush@^1.0.5: dependencies: minipass "^3.0.0" +minipass-json-stream@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz#7edbb92588fbfc2ff1db2fc10397acb7b6b44aa7" + integrity sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg== + dependencies: + jsonparse "^1.3.1" + minipass "^3.0.0" + minipass-pipeline@^1.2.2: version "1.2.2" resolved "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.2.tgz#3dcb6bb4a546e32969c7ad710f2c79a86abba93a" @@ -18131,7 +17969,21 @@ minipass-pipeline@^1.2.2: dependencies: minipass "^3.0.0" -minipass@^2.3.5, minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0: +minipass-pipeline@^1.2.4: + version "1.2.4" + resolved "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== + dependencies: + minipass "^3.0.0" + +minipass-sized@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70" + integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== + dependencies: + minipass "^3.0.0" + +minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0: version "2.9.0" resolved "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== @@ -18146,6 +17998,13 @@ minipass@^3.0.0, minipass@^3.1.1: dependencies: yallist "^4.0.0" +minipass@^3.1.0, minipass@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" + integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== + dependencies: + yallist "^4.0.0" + minizlib@^1.2.1: version "1.3.3" resolved "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" @@ -18153,7 +18012,7 @@ minizlib@^1.2.1: dependencies: minipass "^2.9.0" -minizlib@^2.1.1: +minizlib@^2.0.0, minizlib@^2.1.1: version "2.1.2" resolved "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== @@ -18208,14 +18067,16 @@ mkdirp-classic@^0.5.2: resolved "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.2.tgz#54c441ce4c96cd7790e10b41a87aa51068ecab2b" integrity sha512-ejdnDQcR75gwknmMw/tx02AuRs8jCtqFoFqDZMjiNxsu85sRIJVXDKHuLYvUUPRBUtV2FpSZa9bL1BUa3BdR2g== -mkdirp-promise@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/mkdirp-promise/-/mkdirp-promise-5.0.1.tgz#e9b8f68e552c68a9c1713b84883f7a1dd039b8a1" - integrity sha1-6bj2jlUsaKnBcTuEiD96HdA5uKE= +mkdirp-infer-owner@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/mkdirp-infer-owner/-/mkdirp-infer-owner-2.0.0.tgz#55d3b368e7d89065c38f32fd38e638f0ab61d316" + integrity sha512-sdqtiFt3lkOaYvTXSRIUjkIdPTcxgv5+fgqYE/5qgwdw12cOrAuzzgzvVExIkH/ul1oeHN3bCLOWSG3XOqbKKw== dependencies: - mkdirp "*" + chownr "^2.0.0" + infer-owner "^1.0.4" + mkdirp "^1.0.3" -mkdirp@*, mkdirp@1.x, mkdirp@^1.0.3, mkdirp@^1.0.4: +mkdirp@1.x, mkdirp@^1.0.3, mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== @@ -18367,21 +18228,17 @@ multicast-dns@^6.0.1: dns-packet "^1.3.1" thunky "^1.0.2" -multimatch@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/multimatch/-/multimatch-3.0.0.tgz#0e2534cc6bc238d9ab67e1b9cd5fcd85a6dbf70b" - integrity sha512-22foS/gqQfANZ3o+W7ST2x25ueHDVNWl/b9OlGcLpy/iKxjCpvcNCM51YCenUi7Mt/jAjjqv8JwZRs8YP5sRjA== +multimatch@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz#932b800963cea7a31a033328fa1e0c3a1874dbe6" + integrity sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA== dependencies: - array-differ "^2.0.3" - array-union "^1.0.2" - arrify "^1.0.1" + "@types/minimatch" "^3.0.3" + array-differ "^3.0.0" + array-union "^2.1.0" + arrify "^2.0.1" minimatch "^3.0.4" -mute-stream@0.0.7: - version "0.0.7" - resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" - integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= - mute-stream@0.0.8, mute-stream@~0.0.4: version "0.0.8" resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" @@ -18396,7 +18253,7 @@ mv@~2: ncp "~2.0.0" rimraf "~2.4.0" -mz@^2.5.0, mz@^2.7.0: +mz@^2.7.0: version "2.7.0" resolved "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== @@ -18551,16 +18408,7 @@ node-dir@^0.1.10: dependencies: minimatch "^3.0.2" -node-fetch-npm@^2.0.2: - version "2.0.3" - resolved "https://registry.npmjs.org/node-fetch-npm/-/node-fetch-npm-2.0.3.tgz#efae4aacb0500444e449a51fc1467397775ebc38" - integrity sha512-DgwoKEsqLnFZtk3ap7GWBHcHwnUhsNmQqEDcdjfQ8GofLEFJ081NAd4Uin3R7RFZBWVJCwHISw1oaEqPgSLloA== - dependencies: - encoding "^0.1.11" - 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.1.2, node-fetch@^2.2.0, node-fetch@^2.3.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== @@ -18610,6 +18458,22 @@ node-gyp@^5.0.2: tar "^4.4.12" which "^1.3.1" +node-gyp@^7.1.0: + version "7.1.2" + resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-7.1.2.tgz#21a810aebb187120251c3bcec979af1587b188ae" + integrity sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ== + dependencies: + env-paths "^2.2.0" + glob "^7.1.4" + graceful-fs "^4.2.3" + nopt "^5.0.0" + npmlog "^4.1.2" + request "^2.88.2" + rimraf "^3.0.2" + semver "^7.3.2" + tar "^6.0.2" + which "^2.0.2" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -18746,6 +18610,13 @@ nopt@^4.0.1: abbrev "1" osenv "^0.1.4" +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + dependencies: + abbrev "1" + nopt@~1.0.10: version "1.0.10" resolved "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" @@ -18753,7 +18624,7 @@ nopt@~1.0.10: dependencies: abbrev "1" -normalize-package-data@^2.0.0, normalize-package-data@^2.3.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.3.5, normalize-package-data@^2.4.0, normalize-package-data@^2.5.0: +normalize-package-data@^2.0.0, normalize-package-data@^2.3.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== @@ -18763,6 +18634,16 @@ normalize-package-data@^2.0.0, normalize-package-data@^2.3.0, normalize-package- semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" +normalize-package-data@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.0.tgz#1f8a7c423b3d2e85eb36985eaf81de381d01301a" + integrity sha512-6lUjEI0d3v6kFrtgA/lOx4zHCWULXsFNIjHolnZCKCTLA6m/G625cdn3O7eNmT0iD3jfo6HZ9cdImGZwf21prw== + dependencies: + hosted-git-info "^3.0.6" + resolve "^1.17.0" + semver "^7.3.2" + validate-npm-package-license "^3.0.1" + normalize-path@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" @@ -18800,17 +18681,24 @@ normalize-url@^4.1.0: resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== -npm-bundled@^1.0.1: +npm-bundled@^1.0.1, npm-bundled@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b" integrity sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA== dependencies: npm-normalize-package-bin "^1.0.1" -npm-lifecycle@^3.1.2: - version "3.1.4" - resolved "https://registry.npmjs.org/npm-lifecycle/-/npm-lifecycle-3.1.4.tgz#de6975c7d8df65f5150db110b57cce498b0b604c" - integrity sha512-tgs1PaucZwkxECGKhC/stbEgFyc3TGh2TJcg2CDr6jbvQRdteHNhmMeljRzpe4wgFAXQADoy1cSqqi7mtiAa5A== +npm-install-checks@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-4.0.0.tgz#a37facc763a2fde0497ef2c6d0ac7c3fbe00d7b4" + integrity sha512-09OmyDkNLYwqKPOnbI8exiOZU2GVVmQp7tgez2BPi5OZC8M82elDAps7sxC4l//uSUtotWqoEIDwjRvWH4qz8w== + dependencies: + semver "^7.1.1" + +npm-lifecycle@^3.1.5: + version "3.1.5" + resolved "https://registry.npmjs.org/npm-lifecycle/-/npm-lifecycle-3.1.5.tgz#9882d3642b8c82c815782a12e6a1bfeed0026309" + integrity sha512-lDLVkjfZmvmfvpvBzA4vzee9cn+Me4orq0QF8glbswJVEbIcSNWib7qGOffolysc3teCqbbPZZkzbr3GQZTL1g== dependencies: byline "^5.0.0" graceful-fs "^4.1.15" @@ -18826,17 +18714,7 @@ npm-normalize-package-bin@^1.0.0, npm-normalize-package-bin@^1.0.1: resolved "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== -"npm-package-arg@^4.0.0 || ^5.0.0 || ^6.0.0", npm-package-arg@^6.0.0, npm-package-arg@^6.1.0: - version "6.1.1" - resolved "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.1.tgz#02168cb0a49a2b75bf988a28698de7b529df5cb7" - integrity sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg== - dependencies: - hosted-git-info "^2.7.1" - osenv "^0.1.5" - semver "^5.6.0" - validate-npm-package-name "^3.0.0" - -npm-package-arg@^8.1.0: +npm-package-arg@^8.0.0, npm-package-arg@^8.0.1, npm-package-arg@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.1.0.tgz#b5f6319418c3246a1c38e1a8fbaa06231bc5308f" integrity sha512-/ep6QDxBkm9HvOhOg0heitSd7JHA1U7y1qhhlRlteYYAi9Pdb/ZV7FW5aHpkrpM8+P+4p/jjR8zCyKPBMBjSig== @@ -18845,7 +18723,7 @@ npm-package-arg@^8.1.0: semver "^7.0.0" validate-npm-package-name "^3.0.0" -npm-packlist@^1.1.6, npm-packlist@^1.4.4: +npm-packlist@^1.1.6: version "1.4.8" resolved "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e" integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A== @@ -18854,14 +18732,38 @@ npm-packlist@^1.1.6, npm-packlist@^1.4.4: npm-bundled "^1.0.1" npm-normalize-package-bin "^1.0.1" -npm-pick-manifest@^3.0.0: - version "3.0.2" - resolved "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-3.0.2.tgz#f4d9e5fd4be2153e5f4e5f9b7be8dc419a99abb7" - integrity sha512-wNprTNg+X5nf+tDi+hbjdHhM4bX+mKqv6XmPh7B5eG+QY9VARfQPfCEH013H5GqfNj6ee8Ij2fg8yk0mzps1Vw== +npm-packlist@^2.1.4: + version "2.1.4" + resolved "https://registry.npmjs.org/npm-packlist/-/npm-packlist-2.1.4.tgz#40e96b2b43787d0546a574542d01e066640d09da" + integrity sha512-Qzg2pvXC9U4I4fLnUrBmcIT4x0woLtUgxUi9eC+Zrcv1Xx5eamytGAfbDWQ67j7xOcQ2VW1I3su9smVTIdu7Hw== dependencies: - figgy-pudding "^3.5.1" - npm-package-arg "^6.0.0" - semver "^5.4.1" + glob "^7.1.6" + ignore-walk "^3.0.3" + npm-bundled "^1.1.1" + npm-normalize-package-bin "^1.0.1" + +npm-pick-manifest@^6.0.0: + version "6.1.0" + resolved "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-6.1.0.tgz#2befed87b0fce956790f62d32afb56d7539c022a" + integrity sha512-ygs4k6f54ZxJXrzT0x34NybRlLeZ4+6nECAIbr2i0foTnijtS1TJiyzpqtuUAJOps/hO0tNDr8fRV5g+BtRlTw== + dependencies: + npm-install-checks "^4.0.0" + npm-package-arg "^8.0.0" + semver "^7.0.0" + +npm-registry-fetch@^9.0.0: + version "9.0.0" + resolved "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-9.0.0.tgz#86f3feb4ce00313bc0b8f1f8f69daae6face1661" + integrity sha512-PuFYYtnQ8IyVl6ib9d3PepeehcUeHN9IO5N/iCRhyg9tStQcqGQBRVHmfmMWPDERU3KwZoHFvbJ4FPXPspvzbA== + dependencies: + "@npmcli/ci-detect" "^1.0.0" + lru-cache "^6.0.0" + make-fetch-happen "^8.0.9" + minipass "^3.1.3" + minipass-fetch "^1.3.0" + minipass-json-stream "^1.0.1" + minizlib "^2.0.0" + npm-package-arg "^8.0.0" npm-run-path@^2.0.0: version "2.0.2" @@ -18870,7 +18772,7 @@ npm-run-path@^2.0.0: dependencies: path-key "^2.0.0" -npm-run-path@^4.0.0: +npm-run-path@^4.0.0, npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== @@ -19072,11 +18974,6 @@ obuf@^1.0.0, obuf@^1.1.2: resolved "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== -octokit-pagination-methods@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz#cf472edc9d551055f9ef73f6e42b4dbb4c80bea4" - integrity sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ== - oidc-token-hash@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.0.tgz#acdfb1f4310f58e64d5d74a4e8671a426986e888" @@ -19125,6 +19022,13 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + open@^7.0.0: version "7.3.1" resolved "https://registry.npmjs.org/open/-/open-7.3.1.tgz#111119cb919ca1acd988f49685c4fdd0f4755356" @@ -19249,7 +19153,7 @@ os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: resolved "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= -osenv@0, osenv@^0.1.4, osenv@^0.1.5: +osenv@0, osenv@^0.1.4: version "0.1.5" resolved "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== @@ -19382,14 +19286,12 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" -p-map-series@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/p-map-series/-/p-map-series-1.0.0.tgz#bf98fe575705658a9e1351befb85ae4c1f07bdca" - integrity sha1-v5j+V1cFZYqeE1G++4WuTB8Hvco= - dependencies: - p-reduce "^1.0.0" +p-map-series@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/p-map-series/-/p-map-series-2.1.0.tgz#7560d4c452d9da0c07e692fdbfe6e2c81a2a91f2" + integrity sha512-RpYIIK1zXSNEOdwxcfe7FdvGcs7+y5n8rifMhMNWvaxRNMPINJHF5GDeuVxWqnfrcHPSCnp7Oo5yNXHId9Av2Q== -p-map@^2.0.0, p-map@^2.1.0: +p-map@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== @@ -19408,17 +19310,10 @@ p-map@^4.0.0: dependencies: aggregate-error "^3.0.0" -p-pipe@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/p-pipe/-/p-pipe-1.2.0.tgz#4b1a11399a11520a67790ee5a0c1d5881d6befe9" - integrity sha1-SxoROZoRUgpneQ7loMHViB1r7+k= - -p-queue@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/p-queue/-/p-queue-4.0.0.tgz#ed0eee8798927ed6f2c2f5f5b77fdb2061a5d346" - integrity sha512-3cRXXn3/O0o3+eVmUroJPSj/esxoEFIm0ZOno/T+NzG/VZgPOqQ8WKmlNqubSEpZmCIngEy34unkHGg83ZIBmg== - dependencies: - eventemitter3 "^3.1.0" +p-pipe@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/p-pipe/-/p-pipe-3.1.0.tgz#48b57c922aa2e1af6a6404cb7c6bf0eb9cc8e60e" + integrity sha512-08pj8ATpzMR0Y80x50yJHn37NF6vjrqHutASaX5LiH5npS9XPvrUmscd9MF5R4fuYRHOxQR1FfMIlF7AzwoPqw== p-queue@^6.3.0: version "6.4.0" @@ -19428,10 +19323,18 @@ p-queue@^6.3.0: eventemitter3 "^4.0.0" p-timeout "^3.1.0" -p-reduce@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa" - integrity sha1-GMKw3ZNqRpClKfgjH1ig/bakffo= +p-queue@^6.6.2: + version "6.6.2" + resolved "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426" + integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ== + dependencies: + eventemitter3 "^4.0.4" + p-timeout "^3.2.0" + +p-reduce@^2.0.0, p-reduce@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/p-reduce/-/p-reduce-2.1.0.tgz#09408da49507c6c274faa31f28df334bc712b64a" + integrity sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw== p-retry@^3.0.1: version "3.0.1" @@ -19448,7 +19351,7 @@ p-some@^5.0.0: aggregate-error "^3.0.0" p-cancelable "^2.0.0" -p-timeout@^3.1.0: +p-timeout@^3.1.0, p-timeout@^3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== @@ -19465,12 +19368,12 @@ p-try@^2.0.0: resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== -p-waterfall@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/p-waterfall/-/p-waterfall-1.0.0.tgz#7ed94b3ceb3332782353af6aae11aa9fc235bb00" - integrity sha1-ftlLPOszMngjU69qrhGqn8I1uwA= +p-waterfall@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/p-waterfall/-/p-waterfall-2.1.1.tgz#63153a774f472ccdc4eb281cdb2967fcf158b2ee" + integrity sha512-RRTnDb2TBG/epPRI2yYXsimO0v3BXC8Yd3ogr1545IaqKK17VGhbWVeGGN+XfCm/08OK8635nH31c8bATkHuSw== dependencies: - p-reduce "^1.0.0" + p-reduce "^2.0.0" package-json@^6.3.0: version "6.5.0" @@ -19492,6 +19395,31 @@ packet-reader@1.0.0: resolved "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74" integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ== +pacote@^11.2.6: + version "11.2.6" + resolved "https://registry.npmjs.org/pacote/-/pacote-11.2.6.tgz#c0426e5d5c8d33aeea3461a75e1390f1ba78f953" + integrity sha512-xCl++Hb3aBC7LaWMimbO4xUqZVsEbKDVc6KKDIIyAeBYrmMwY1yJC2nES/lsGd8sdQLUosgBxQyuVNncZ2Ru0w== + dependencies: + "@npmcli/git" "^2.0.1" + "@npmcli/installed-package-contents" "^1.0.6" + "@npmcli/promise-spawn" "^1.2.0" + "@npmcli/run-script" "^1.8.2" + cacache "^15.0.5" + chownr "^2.0.0" + fs-minipass "^2.1.0" + infer-owner "^1.0.4" + minipass "^3.1.3" + mkdirp "^1.0.3" + npm-package-arg "^8.0.1" + npm-packlist "^2.1.4" + npm-pick-manifest "^6.0.0" + npm-registry-fetch "^9.0.0" + promise-retry "^2.0.1" + read-package-json-fast "^2.0.1" + rimraf "^3.0.2" + ssri "^8.0.1" + tar "^6.1.0" + pako@^1.0.10, pako@~1.0.5: version "1.0.11" resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" @@ -20715,13 +20643,13 @@ promise-inflight@^1.0.1: resolved "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= -promise-retry@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/promise-retry/-/promise-retry-1.1.1.tgz#6739e968e3051da20ce6497fb2b50f6911df3d6d" - integrity sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0= +promise-retry@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" + integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== dependencies: - err-code "^1.0.0" - retry "^0.10.0" + err-code "^2.0.2" + retry "^0.12.0" promise.allsettled@^1.0.0: version "1.0.2" @@ -20813,13 +20741,6 @@ protocols@^1.1.0, protocols@^1.4.0: resolved "https://registry.npmjs.org/protocols/-/protocols-1.4.7.tgz#95f788a4f0e979b291ffefcf5636ad113d037d32" integrity sha512-Fx65lf9/YDn3hUX08XUc0J8rSux36rEsyiv21ZGUC1mOyeM3lTRpZLcrm8aAolzS4itwVfm7TAPyxC2E5zd6xg== -protoduck@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/protoduck/-/protoduck-5.0.1.tgz#03c3659ca18007b69a50fd82a7ebcc516261151f" - integrity sha512-WxoCeDCoCBY55BMvj4cAEjdVUFGRWed9ZxPlqTKYyw1nDDTQ4pqmnIMAGfJlg7Dx35uB/M+PHJPTmGOvaCaPTg== - dependencies: - genfun "^5.0.0" - proxy-addr@~2.0.5: version "2.0.6" resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" @@ -20867,6 +20788,11 @@ public-encrypt@^4.0.0: randombytes "^2.0.1" safe-buffer "^5.1.2" +puka@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/puka/-/puka-1.0.1.tgz#a2df782b7eb4cf9564e4c93a5da422de0dfacc02" + integrity sha512-ssjRZxBd7BT3dte1RR3VoeT2cT/ODH8x+h0rUF1rMqB0srHYf48stSDWfiYakTp5UBZMxroZhB2+ExLDHm7W3g== + pump@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" @@ -20985,11 +20911,6 @@ querystringify@^2.1.1: resolved "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== -quick-lru@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" - integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g= - quick-lru@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" @@ -21622,14 +21543,20 @@ reactcss@^1.2.0: dependencies: lodash "^4.0.1" -read-cmd-shim@^1.0.1: - version "1.0.5" - resolved "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-1.0.5.tgz#87e43eba50098ba5a32d0ceb583ab8e43b961c16" - integrity sha512-v5yCqQ/7okKoZZkBQUAfTsQ3sVJtXdNfbPnI5cceppoxEVLYA3k+VtV2omkeo8MS94JCy4fSiUwlRBAwCVRPUA== - dependencies: - graceful-fs "^4.1.2" +read-cmd-shim@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-2.0.0.tgz#4a50a71d6f0965364938e9038476f7eede3928d9" + integrity sha512-HJpV9bQpkl6KwjxlJcBoqu9Ba0PQg8TqSNIOrulGt54a0uup0HtevreFHzYzkm0lpnleRdNBzXznKrgxglEHQw== -"read-package-json@1 || 2", read-package-json@^2.0.0, read-package-json@^2.0.13: +read-package-json-fast@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-2.0.1.tgz#c767f6c634873ffb6bb73788191b65559734f555" + integrity sha512-bp6z0tdgLy9KzdfENDIw/53HWAolOVoQTRWXv7PUiqAo3YvvoUVeLr7RWPWq+mu7KUOu9kiT4DvxhUgNUBsvug== + dependencies: + json-parse-even-better-errors "^2.3.0" + npm-normalize-package-bin "^1.0.1" + +read-package-json@^2.0.0: version "2.1.1" resolved "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.1.tgz#16aa66c59e7d4dad6288f179dd9295fd59bb98f1" integrity sha512-dAiqGtVc/q5doFz6096CcnXhpYk0ZN8dEKVkGLU0CsASt8SrgF6SF7OTKAYubfvFhWaqofl+Y8HK19GR8jwW+A== @@ -21641,7 +21568,17 @@ read-cmd-shim@^1.0.1: optionalDependencies: graceful-fs "^4.1.2" -read-package-tree@^5.1.6: +read-package-json@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/read-package-json/-/read-package-json-3.0.0.tgz#2219328e77c9be34f035a4ce58d1fb8e2979adf9" + integrity sha512-4TnJZ5fnDs+/3deg1AuMExL4R1SFNRLQeOhV9c8oDKm3eoG6u8xU0r0mNNRJHi3K6B+jXmT7JOhwhAklWw9SSQ== + dependencies: + glob "^7.1.1" + json-parse-even-better-errors "^2.3.0" + normalize-package-data "^3.0.0" + npm-normalize-package-bin "^1.0.0" + +read-package-tree@^5.3.1: version "5.3.1" resolved "https://registry.npmjs.org/read-package-tree/-/read-package-tree-5.3.1.tgz#a32cb64c7f31eb8a6f31ef06f9cedf74068fe636" integrity sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw== @@ -21759,7 +21696,7 @@ read@1, read@~1.0.1: string_decoder "~1.1.1" util-deprecate "~1.0.1" -"readable-stream@2 || 3", readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: +readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -21847,14 +21784,6 @@ redent@^1.0.0: indent-string "^2.1.0" strip-indent "^1.0.1" -redent@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz#c1b2007b42d57eb1389079b3c8333639d5e1ccaa" - integrity sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo= - dependencies: - indent-string "^3.0.0" - strip-indent "^2.0.0" - redent@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" @@ -22354,11 +22283,6 @@ retry@0.12.0, retry@^0.12.0: resolved "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= -retry@^0.10.0: - version "0.10.1" - resolved "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4" - integrity sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q= - reusify@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" @@ -22391,7 +22315,7 @@ rifm@^0.7.0: dependencies: "@babel/runtime" "^7.3.1" -rimraf@2, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3: +rimraf@2, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -22549,7 +22473,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.0, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: +safe-buffer@5.2.0, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== @@ -22571,7 +22495,7 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -22690,7 +22614,7 @@ semver-regex@^3.1.2: resolved "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.2.tgz#34b4c0d361eef262e07199dbef316d0f2ab11807" integrity sha512-bXWyL6EAKOJa81XG1OZ/Yyuq+oT0b2YLlxx7c+mrdYPaPbnj6WgVULXhinMIeZGufuUBu/eVRqXEhiv4imfwxA== -"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: +"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -22705,7 +22629,7 @@ semver@7.0.0: resolved "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== -semver@7.x, semver@^7.0.0, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4: +semver@7.x, semver@^7.0.0, semver@^7.1.1, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4: version "7.3.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== @@ -22891,7 +22815,7 @@ shell-quote@1.7.2: resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== -shelljs@^0.8.2, shelljs@^0.8.4: +shelljs@^0.8.2, shelljs@^0.8.3, shelljs@^0.8.4: version "0.8.4" resolved "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ== @@ -22928,7 +22852,7 @@ side-channel@^1.0.2: es-abstract "^1.17.0-next.1" object-inspect "^1.7.0" -signal-exit@^3.0.0, signal-exit@^3.0.2: +signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== @@ -23088,20 +23012,21 @@ sockjs@0.3.20: uuid "^3.4.0" websocket-driver "0.6.5" -socks-proxy-agent@^4.0.0: - version "4.0.2" - resolved "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz#3c8991f3145b2799e70e11bd5fbc8b1963116386" - integrity sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg== +socks-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.0.tgz#7c0f364e7b1cf4a7a437e71253bed72e9004be60" + integrity sha512-lEpa1zsWCChxiynk+lCycKuC502RxDWLKJZoIhnxrWNjLSDGYRFflHA1/228VkRcnv9TIb8w98derGbpKxJRgA== dependencies: - agent-base "~4.2.1" - socks "~2.3.2" + agent-base "6" + debug "4" + socks "^2.3.3" -socks@~2.3.2: - version "2.3.3" - resolved "https://registry.npmjs.org/socks/-/socks-2.3.3.tgz#01129f0a5d534d2b897712ed8aceab7ee65d78e3" - integrity sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA== +socks@^2.3.3: + version "2.5.1" + resolved "https://registry.npmjs.org/socks/-/socks-2.5.1.tgz#7720640b6b5ec9a07d556419203baa3f0596df5f" + integrity sha512-oZCsJJxapULAYJaEYBSzMcz8m3jqgGrHaGhkmU/o/PQfFWYWxkAaA0UMGImb6s6tEXfKi959X6VJjMMQ3P6TTQ== dependencies: - ip "1.1.5" + ip "^1.1.5" smart-buffer "^4.1.0" sort-keys@^1.0.0: @@ -23278,6 +23203,13 @@ split2@^2.0.0: dependencies: through2 "^2.0.2" +split2@^3.0.0: + version "3.2.2" + resolved "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f" + integrity sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg== + dependencies: + readable-stream "^3.0.0" + split@0.3: version "0.3.3" resolved "https://registry.npmjs.org/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" @@ -23343,7 +23275,7 @@ sshpk@^1.7.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" -ssri@^6.0.0, ssri@^6.0.1: +ssri@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8" integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA== @@ -23357,6 +23289,13 @@ ssri@^8.0.0: dependencies: minipass "^3.1.1" +ssri@^8.0.1: + version "8.0.1" + resolved "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" + integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== + dependencies: + minipass "^3.1.1" + stable@^0.1.8: version "0.1.8" resolved "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" @@ -23586,7 +23525,7 @@ string-width@^1.0.1, string-width@^1.0.2: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: +"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== @@ -23757,11 +23696,6 @@ strip-indent@^1.0.1: dependencies: get-stdin "^4.0.1" -strip-indent@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" - integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g= - strip-indent@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" @@ -23779,7 +23713,7 @@ strip-json-comments@~2.0.1: resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= -strong-log-transformer@^2.0.0: +strong-log-transformer@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz#0f5ed78d325e0421ac6f90f7f10e691d6ae3ae10" integrity sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA== @@ -24108,7 +24042,7 @@ tar@^2.0.0: fstream "^1.0.12" inherits "2" -tar@^4, tar@^4.4.10, tar@^4.4.12, tar@^4.4.8: +tar@^4, tar@^4.4.12: version "4.4.13" resolved "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA== @@ -24133,6 +24067,18 @@ tar@^6.0.1, tar@^6.0.2, tar@^6.0.5: mkdirp "^1.0.3" yallist "^4.0.0" +tar@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83" + integrity sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA== + dependencies: + 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" + tarn@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/tarn/-/tarn-3.0.1.tgz#ebac2c6dbc6977d34d4526e0a7814200386a8aec" @@ -24168,17 +24114,16 @@ temp-dir@^1.0.0: resolved "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" integrity sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0= -temp-write@^3.4.0: - version "3.4.0" - resolved "https://registry.npmjs.org/temp-write/-/temp-write-3.4.0.tgz#8cff630fb7e9da05f047c74ce4ce4d685457d492" - integrity sha1-jP9jD7fp2gXwR8dM5M5NaFRX1JI= +temp-write@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/temp-write/-/temp-write-4.0.0.tgz#cd2e0825fc826ae72d201dc26eef3bf7e6fc9320" + integrity sha512-HIeWmj77uOOHb0QX7siN3OtwV3CTntquin6TNVg6SHOqCP3hYKmox90eeFOGaY1MqJ9WYDDjkyZrW6qS5AWpbw== dependencies: - graceful-fs "^4.1.2" - is-stream "^1.1.0" - make-dir "^1.0.0" - pify "^3.0.0" + graceful-fs "^4.1.15" + is-stream "^2.0.0" + make-dir "^3.0.0" temp-dir "^1.0.0" - uuid "^3.0.1" + uuid "^3.3.2" term-size@^1.2.0: version "1.2.0" @@ -24317,12 +24262,12 @@ through2@^2.0.0, through2@^2.0.2: readable-stream "~2.3.6" xtend "~4.0.1" -through2@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz#39276e713c3302edf9e388dd9c812dd3b825bd5a" - integrity sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww== +through2@^4.0.0: + version "4.0.2" + resolved "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz#a7ce3ac2a7a8b0b966c80e7c49f0484c3b239764" + integrity sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw== dependencies: - readable-stream "2 || 3" + readable-stream "3" through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.1: version "2.3.8" @@ -24556,11 +24501,6 @@ trim-newlines@^1.0.0: resolved "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= -trim-newlines@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz#b403d0b91be50c331dfc4b82eeceb22c3de16d20" - integrity sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA= - trim-newlines@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.0.tgz#79726304a6a898aa8373427298d54c2ee8b1cb30" @@ -24773,10 +24713,10 @@ type-fest@^0.13.1: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== -type-fest@^0.3.0: - version "0.3.1" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" - integrity sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ== +type-fest@^0.18.0: + version "0.18.1" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f" + integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw== type-fest@^0.4.1: version "0.4.1" @@ -25033,13 +24973,6 @@ universal-github-app-jwt@^1.0.1: "@types/jsonwebtoken" "^8.3.3" jsonwebtoken "^8.5.1" -universal-user-agent@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.1.tgz#fd8d6cb773a679a709e967ef8288a31fcc03e557" - integrity sha512-LnST3ebHwVL2aNe4mejI9IQh2HfZ1RLo8Io2HugSif8ekzD1TlWpHpColOB/eh8JHMLkGH3Akqf040I+4ylNxg== - dependencies: - os-name "^3.1.0" - universal-user-agent@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-5.0.0.tgz#a3182aa758069bf0e79952570ca757de3579c1d9" @@ -25062,6 +24995,11 @@ universalify@^1.0.0: resolved "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + unixify@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unixify/-/unixify-1.0.0.tgz#3a641c8c2ffbce4da683a5c70f03a462940c2090" @@ -25108,11 +25046,16 @@ unzipper@^0.10.11: readable-stream "~2.3.6" setimmediate "~1.0.4" -upath@^1.1.1, upath@^1.2.0: +upath@^1.1.1: version "1.2.0" resolved "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== +upath@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz#50c73dea68d6f6b990f51d279ce6081665d61a8b" + integrity sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w== + update-notifier@^4.1.0: version "4.1.3" resolved "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz#be86ee13e8ce48fb50043ff72057b5bd598e1ea3" @@ -25286,7 +25229,7 @@ uuid@3.3.2: resolved "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== -uuid@^3.0.1, uuid@^3.1.0, uuid@^3.3.2, uuid@^3.4.0: +uuid@^3.1.0, uuid@^3.3.2, uuid@^3.4.0: version "3.4.0" resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== @@ -25327,7 +25270,7 @@ valid-url@1.0.9, valid-url@^1.0.9: resolved "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200" integrity sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA= -validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.3: +validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== @@ -25725,6 +25668,15 @@ whatwg-url@^8.0.0: tr46 "^2.0.2" webidl-conversions "^5.0.0" +whatwg-url@^8.4.0: + version "8.4.0" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.4.0.tgz#50fb9615b05469591d2b2bd6dfaed2942ed72837" + integrity sha512-vwTUFf6V4zhcPkWp/4CQPr1TW9Ml6SF4lVyaIMBdJw5i6qUUJ1QWM4Z6YYVkfka0OUIzVo/0aNtGVGk256IKWw== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^2.0.2" + webidl-conversions "^6.1.0" + which-module@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" @@ -25890,7 +25842,7 @@ wrappy@1: resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -write-file-atomic@^2.0.0, write-file-atomic@^2.3.0, write-file-atomic@^2.4.2: +write-file-atomic@^2.4.2: version "2.4.3" resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481" integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ== @@ -25899,7 +25851,7 @@ write-file-atomic@^2.0.0, write-file-atomic@^2.3.0, write-file-atomic@^2.4.2: imurmurhash "^0.1.4" signal-exit "^3.0.2" -write-file-atomic@^3.0.0: +write-file-atomic@^3.0.0, write-file-atomic@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== @@ -25909,18 +25861,6 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" -write-json-file@^2.2.0: - version "2.3.0" - resolved "https://registry.npmjs.org/write-json-file/-/write-json-file-2.3.0.tgz#2b64c8a33004d54b8698c76d585a77ceb61da32f" - integrity sha1-K2TIozAE1UuGmMdtWFp3zrYdoy8= - dependencies: - 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" - write-json-file@^3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/write-json-file/-/write-json-file-3.2.0.tgz#65bbdc9ecd8a1458e15952770ccbadfcff5fe62a" @@ -25945,14 +25885,6 @@ write-json-file@^4.3.0: sort-keys "^4.0.0" write-file-atomic "^3.0.0" -write-pkg@^3.1.0: - version "3.2.0" - resolved "https://registry.npmjs.org/write-pkg/-/write-pkg-3.2.0.tgz#0e178fe97820d389a8928bc79535dbe68c2cff21" - integrity sha512-tX2ifZ0YqEFOF1wjRW2Pk93NLsj02+n1UP5RvO6rCs0K6R2g1padvf006cY74PQJKMGS2r42NK7FD0dG6Y6paw== - dependencies: - sort-keys "^2.0.0" - write-json-file "^2.2.0" - write-pkg@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/write-pkg/-/write-pkg-4.0.0.tgz#675cc04ef6c11faacbbc7771b24c0abbf2a20039" @@ -26132,18 +26064,16 @@ yaml@^1.10.0, yaml@^1.7.2, yaml@^1.9.2: resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== +yargs-parser@20.2.4, yargs-parser@^20.2.2, yargs-parser@^20.2.3: + version "20.2.4" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + yargs-parser@20.x: version "20.2.3" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.3.tgz#92419ba867b858c868acf8bae9bf74af0dd0ce26" integrity sha512-emOFRT9WVHw03QSvN5qor9QQT9+sw5vwxfYweivSMHTcAXPefwVae2FjO7JJjj8hCE4CzPOPeFM83VwT29HCww== -yargs-parser@^10.0.0: - version "10.1.0" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" - integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== - dependencies: - camelcase "^4.1.0" - yargs-parser@^13.1.2: version "13.1.2" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" @@ -26152,14 +26082,6 @@ yargs-parser@^13.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^15.0.1: - version "15.0.1" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz#54786af40b820dcb2fb8025b11b4d659d76323b3" - integrity sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - yargs-parser@^18.1.2, yargs-parser@^18.1.3: version "18.1.3" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" @@ -26168,11 +26090,6 @@ yargs-parser@^18.1.2, yargs-parser@^18.1.3: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^20.2.2: - version "20.2.4" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" - integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== - yargs-parser@^3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-3.2.0.tgz#5081355d19d9d0c8c5d81ada908cb4e6d186664f" @@ -26197,23 +26114,6 @@ yargs@^13.3.0, yargs@^13.3.2: y18n "^4.0.0" yargs-parser "^13.1.2" -yargs@^14.2.2: - version "14.2.3" - resolved "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz#1a1c3edced1afb2a2fea33604bc6d1d8d688a414" - integrity sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg== - dependencies: - cliui "^5.0.0" - decamelize "^1.2.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 "^15.0.1" - yargs@^15.1.0, yargs@^15.3.1, yargs@^15.4.1: version "15.4.1" resolved "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" From b61296b98d06a9a62e8543297e92f16e0e484f28 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 12 Feb 2021 14:55:18 +0100 Subject: [PATCH 11/34] chore: update cli to use named imports --- packages/cli/src/lib/config.ts | 2 +- packages/cli/src/lib/packager/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index a48c4b1810..b49b644e25 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -75,7 +75,7 @@ export async function loadCliConfig(options: Options) { } function findPackages(packages: any[], fromPackage: string): string[] { - const PackageGraph = require('@lerna/package-graph'); + const { PackageGraph } = require('@lerna/package-graph'); const graph = new PackageGraph(packages); diff --git a/packages/cli/src/lib/packager/index.ts b/packages/cli/src/lib/packager/index.ts index 7a6fb494a4..afe29fbd7c 100644 --- a/packages/cli/src/lib/packager/index.ts +++ b/packages/cli/src/lib/packager/index.ts @@ -220,7 +220,7 @@ async function moveToDistWorkspace( async function findTargetPackages(pkgNames: string[]): Promise { const { Project } = require('@lerna/project'); - const PackageGraph = require('@lerna/package-graph'); + const { PackageGraph } = require('@lerna/package-graph'); const project = new Project(paths.targetDir); const packages = await project.getPackages(); From 532bc0ec0c28fe496660489a539f461ab2a3d668 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 12 Feb 2021 15:14:58 +0100 Subject: [PATCH 12/34] chore: add changeset --- .changeset/wicked-forks-sleep.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/wicked-forks-sleep.md diff --git a/.changeset/wicked-forks-sleep.md b/.changeset/wicked-forks-sleep.md new file mode 100644 index 0000000000..2c1ba76a27 --- /dev/null +++ b/.changeset/wicked-forks-sleep.md @@ -0,0 +1,6 @@ +--- +'@backstage/cli': minor +'@backstage/create-app': minor +--- + +Upgrading the lerna@4.0.0 From 094c0812943c91183c5eaac06eb40b9acaa6108d Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 12 Feb 2021 15:19:49 +0100 Subject: [PATCH 13/34] chore: update changeset description to be a litle more heplful --- .changeset/wicked-forks-sleep.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.changeset/wicked-forks-sleep.md b/.changeset/wicked-forks-sleep.md index 2c1ba76a27..985d184f75 100644 --- a/.changeset/wicked-forks-sleep.md +++ b/.changeset/wicked-forks-sleep.md @@ -3,4 +3,11 @@ '@backstage/create-app': minor --- -Upgrading the lerna@4.0.0 +Upgrading to lerna@4.0.0. This changes the interface for importing, and because we can't run multiple versions of lerna, this is a breaking change. + +You'll need to update your root `package.json` like the following: + +```diff +- "lerna": "^3.20.2", ++ "lerna": "^4.0.0", +``` From 8f53351a32b28329bb50e940d2d53fd168bfb5c4 Mon Sep 17 00:00:00 2001 From: Esteban Barrios Date: Fri, 12 Feb 2021 16:08:46 +0100 Subject: [PATCH 14/34] Rebase from master (#19) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix client reference * Fix CatalogClient reference in create-app too * Move catalog-client dep to create-app * Add catalog-client dep again.. * Use RequestOptions * tsc * Backwards compatible scaffolder * Fix test * Avoid importing core * Use request options * Increase code coverage * Type those variables * use the second mock * Trying to make tests work on Github too * Get clean yarn from master * Only add authorization header if token exists * Use request options * Forward authorization header token * Refactor to use context argument * Allow the registration of optional locations via the catalog-client * Make EntityRefLink a React.forwardRef in order to use it as root component in other components like ListItem * Rework the user flow of the catalog-import plugin * Update the create-app template * Add luxon and remove moment * Port moment to luxon * Add change set * Split the step label and content to be able to refactor the stepper from vertical to horizontal in the future * Fix tsc, prettier and import issues * Update .changeset/strange-olives-unite.md Co-authored-by: Himanshu Mishra * Update plugins/auth-backend/src/identity/TokenFactory.ts Co-authored-by: Himanshu Mishra * Change from minor to patch * Update DatabaseKeyStore.ts * feat: added a test for running a simple cypress server * Add luxon and remove moment * Port from moment to Luxon * Add changeset * Fixes following CR * Rebased and reinstalled yarn.lock * Changed to fromSQL * Fix prettier formatting * chore: reworking the lock file for the cypress deps * chore: adding cypress.config * chore: reworking some more examples * chore: added some more simple stuff for e2e tests with cypress * chore: reworking again * chore: making cypress nice * chore: added a sample test that works * chore: reworking tests to use he cypress github-action instead * chore: reconfigure Cypress run * chore: don't install - we have deps * chore: dump video recordings too * chore: slimming down install script a little * chore: make pretty * chore: fixing syntax * chore: fixing syntax again * Align card elements with list * Align card elements with List * Add changeset * Removed unused import * chore: archive video footage with the correct path * chore: fix lint warnings * chore: fix prettier again * chore: move files around for ease * Added pattern for component name * Add changeset * stash: push to stash * Return AboutCard title to group name * Refactor children to use shared function * TechDocs: Add visbility to migrate away from basic setup * bugfix: [3310] Favoriting a component resets interface to your owned components update changeset remove unused import fix changeset message fix vale errors? * Create utc DateTime object directly * integration: update the gitlab config mandatory fields to match reality * Resolve review comments * Remove shadowed variables * generateEntityDefinitions should return a PartialEntity * Update roadmap with Backstage Community Sessions * Make sure that SidebarItems are also active when on sub route * Add whitespace around variable * Add changeset * Add word to dictionary * Smater duration display * Add changeset * chore(deps-dev): bump @graphql-codegen/typescript from 1.18.1 to 1.20.2 Bumps [@graphql-codegen/typescript](https://github.com/dotansimha/graphql-code-generator/tree/HEAD/packages/plugins/typescript/typescript) from 1.18.1 to 1.20.2. - [Release notes](https://github.com/dotansimha/graphql-code-generator/releases) - [Changelog](https://github.com/dotansimha/graphql-code-generator/blob/master/packages/plugins/typescript/typescript/CHANGELOG.md) - [Commits](https://github.com/dotansimha/graphql-code-generator/commits/@graphql-codegen/typescript@1.20.2/packages/plugins/typescript/typescript) Signed-off-by: dependabot[bot] * chore: reworking folder structure to move cypress outside of the project * chore: updating workflow to point at the right folder now * chore: fixing cypress build * microsite: Fix color contrast for pre tags * Add package name to lockfile.ts error * Create new-mangos-tap.md * Changed changeset bump to be patch * Pass registered Logger in ServiceBuilderImpl to requestLoggingHandler `requestLoggingHandler` takes an optional `logger` parameter that it can use to log incoming requests. The `ServiceBuilderImpl` was not passing on this logger, if set, to `requestLoggingHandler` middleware * Add indices on columns referring location(id) * Add changeset * Catch catalog errors * Add changeset * Fixed parseUrl to output catalogPaths beginning with '/' * Add configurable OAuth 2.0 scopes - Add oauth2 config for optional scopes - Document oauth2 config keys - Add OAuth2 to demo app list of identity providers * incorrectly added callbackUrl * chore: added a simple readme to run some simple tests against a backstage instance * chore(deps): bump @svgr/plugin-jsx from 5.4.0 to 5.5.0 Bumps [@svgr/plugin-jsx](https://github.com/gregberge/svgr) from 5.4.0 to 5.5.0. - [Release notes](https://github.com/gregberge/svgr/releases) - [Changelog](https://github.com/gregberge/svgr/blob/main/CHANGELOG.md) - [Commits](https://github.com/gregberge/svgr/compare/v5.4.0...v5.5.0) Signed-off-by: dependabot[bot] * chore(deps): bump @types/cors from 2.8.6 to 2.8.9 Bumps [@types/cors](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/cors) from 2.8.6 to 2.8.9. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/cors) Signed-off-by: dependabot[bot] * fix: removing test e2e-test tsconfig.json file * TechDocs: Preparers will take and return an etag for cache invalidation * TechDocs: Implement etag based caching for url preparer Signed-off-by: Himanshu Mishra * TechDocs: Implement etag based caching for git preparer Signed-off-by: Himanshu Mishra * TechDocs: Warn when using legacy git preparer and dir preparer in backstage.io/techdocs-ref Context: https://github.com/backstage/backstage/issues/4409 * TechDocs: Update tests for proper url preparer caching * TechDocs: Deduplicate git clone for cache check Signed-off-by: Himanshu Mishra * TechDocs: Add changesets for deprecation and proper caching * TechDocs: Implement caching for dir preparer * Update the changesets * scaffolder-backend: remove auth-backend dependency * catalog-client: rename ApiContext to CatalogRequestOptions + avoid export * refactor existing usage of ApiContext * techdocs: don't swallow errors other than NotModifiedError * catalog-import: removed bonus code * techdocs: meaningful logs when readTree starts * Flatten the options of the CatalogImportPage * inline optional id token auth headers * Define relationship to software catalog and loose coupling by convention. * Clarify intentions around bulk vs. incremental index management. * Break apart backend plugins & clarify indexer/plugin relationship. * update changeset bump levels * [Search] documentation update (#4459) * delete link to issue as it is closed * replace usage of easy as its very subjective * Update docs/features/search/architecture.md Co-authored-by: Adam Harvey * prettier....: Co-authored-by: Adam Harvey * Own it. * scaffolder: include backstage identity token in requests * review feedback tweaks * docs/apis: update to use named plugin var * sentry: update plugin instance export name * clear other field when toggling reason * changeset * fix(pagerduty): use the luxon date library * Don't pass default as a scope to OIDC providers * docs(TechDocs): Add GitHub Actions CI example with AWS S3 * tech-radar: migrated to new composability API * cost-insights: migrate to new composability API * Changeset * Fix line endings * Prettier * Remove unnecessary scopes from OIDC defaultApi * Bump plugin-auth-backend to a minor change, add documentation for fixing it * Fix md syntax error * Fix Vale spelling error * Remove defaultScopes from OIDC api * [ImgBot] Optimize images /plugins/catalog-import/docs/catalog-import-screenshot.png -- 613.39kb -> 356.48kb (41.88%) Signed-off-by: ImgBotApp * github/workflows: use lax config checks * Use commented-out example value for scope * feat(pagerduty): add changeset * Apply suggestions from code review Co-authored-by: Rémi Doreau <32459935+ayshiff@users.noreply.github.com> * feat(catalog): add entity links card component * update entity links changeset * Fix prettier * chore(deps-dev): bump @types/http-proxy from 1.17.4 to 1.17.5 Bumps [@types/http-proxy](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/http-proxy) from 1.17.4 to 1.17.5. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/http-proxy) Signed-off-by: dependabot[bot] * Revert "Merge pull request #3953 from ayshiff/feature/techdocs-aws-sdkv3" This reverts commit 1c7771871ec46bf55f87fba5991b95938bb57017, reversing changes made to 65d5c8e6fe4364d036d69a0592c87a9037f95b8b. * Revert "Merge pull request #4035 from ayshiff/feature/catalog-backend-aws-sdk-v3" This reverts commit 1df913464889cdf63a1484b06538b76a8baed495, reversing changes made to d45a5100695fb973ae38f74f23bbced9a0101840. * Add changeset * feat: support custom app icons * backend-common: implement UrlReader.search for the other providers too * Remove changeset * docs(TechDocs): Improvements to the CI example Signed-off-by: Himanshu Mishra * fix(catalog): entity links incorrect wrapping * Remove unused functions and luxon * Removed old imports * Fix broken links in documentation (#4418) * Update IdentityApi.md * Update docs/reference/utility-apis/IdentityApi.md * Update docs/reference/utility-apis/IdentityApi.md * ran yarn docgen * Use JS Date instead of Datetime * Add line * Prettier fix * Move parseDate function * Version Packages * chore: fix the create-app version * Limit the props that are forwarded to the Link component in the EntityRefLink * Use routed tabs to link to every settings page * fix up yarn after release * chore: add lockfile to words * catalog-info: add links * Export Select component from core I saw the Select component on storybook and went to use it but it seems it's not exported. Any chance it could be exported? * bug: filepath can be returned as undefined from `git-url-parse` let's default to empty * chore: might as well do this for all parsing * chore: changeset * Add changeset for fixing requestLoggingHandler * chore(deps-dev): bump @storybook/addon-actions from 6.1.11 to 6.1.17 Bumps [@storybook/addon-actions](https://github.com/storybookjs/storybook/tree/HEAD/addons/actions) from 6.1.11 to 6.1.17. - [Release notes](https://github.com/storybookjs/storybook/releases) - [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md) - [Commits](https://github.com/storybookjs/storybook/commits/v6.1.17/addons/actions) Signed-off-by: dependabot[bot] * Port AboutCard * Fix tests * Revert EntityPage changes * docs: Merge auth glossary with main glossary * Remove the "Move repository" menu entry from the catalog page, as it's just a placeholder It will be easy to bring it back later, but for now it just confuses users that it's not doing anything. It's also hard to remove for integrators. * make the template cards conform to mui standard Co-authored-by: Erik Larsson Co-authored-by: Dominik Henneke Co-authored-by: Nils Streijffert Co-authored-by: Nils Streijffert Co-authored-by: Himanshu Mishra Co-authored-by: blam Co-authored-by: Nir Gazit Co-authored-by: Adam Harvey Co-authored-by: NHI TRAN Co-authored-by: Fredrik Adelöw Co-authored-by: Oliver Sand Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Debajyoti Halder Co-authored-by: Patrik Oldsberg Co-authored-by: Gowind Co-authored-by: Alan Crosswell Co-authored-by: Eric Peterson Co-authored-by: Emma Indal Co-authored-by: Ryan Vazquez Co-authored-by: Ryan Vazquez Co-authored-by: Remi Co-authored-by: Ryan Manny Co-authored-by: ImgBotApp Co-authored-by: Rémi Doreau <32459935+ayshiff@users.noreply.github.com> Co-authored-by: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Co-authored-by: Joel Low Co-authored-by: Eric Peterson Co-authored-by: github-actions[bot] Co-authored-by: Iain Billett --- .changeset/afraid-dingos-own.md | 5 + .changeset/afraid-teachers-cross.md | 29 - .changeset/angry-walls-mate.md | 5 + .changeset/chilly-cars-shout.md | 5 + .changeset/cost-insights-tidy-geese-play.md | 5 - .changeset/curly-crabs-compete.md | 5 - .changeset/cyan-lies-flow.md | 5 - .changeset/dry-llamas-wave.md | 7 + .changeset/early-hotels-mate.md | 5 - .changeset/eight-doors-matter.md | 19 + .changeset/empty-cooks-laugh.md | 5 - .changeset/five-guests-promise.md | 5 + .changeset/fluffy-hats-hug.md | 5 - .changeset/four-rings-push.md | 5 - .changeset/funny-students-shout.md | 5 - .changeset/grumpy-cups-hope.md | 5 + .changeset/hot-pants-taste.md | 8 - .changeset/large-terms-invite.md | 7 - .changeset/new-mangos-tap.md | 5 + .changeset/ninety-houses-shout.md | 5 - .changeset/ninety-lemons-shake.md | 5 + .changeset/odd-buckets-compare.md | 5 - .changeset/old-tools-exist.md | 5 - .changeset/perfect-ladybugs-listen.md | 5 - .changeset/poor-sheep-give.md | 5 - .changeset/popular-cars-eat.md | 5 - .changeset/selfish-years-pump.md | 5 - .changeset/shiny-falcons-marry.md | 5 - .changeset/shiny-shirts-worry.md | 5 - .changeset/shy-maps-guess.md | 5 - .changeset/six-elephants-poke.md | 5 + .changeset/sixty-lemons-agree.md | 5 - .changeset/strange-cobras-unite.md | 5 - .changeset/strange-olives-unite.md | 5 + .changeset/stupid-maps-do.md | 5 + .changeset/techdocs-green-rabbits-burn.md | 5 - .changeset/thick-scissors-notice.md | 5 + .changeset/tough-worms-clap.md | 5 + .changeset/tricky-dancers-rush.md | 5 - .changeset/two-dogs-search.md | 5 - .github/CODEOWNERS | 1 + .github/styles/vocab.txt | 2 + .github/workflows/ci.yml | 2 +- .github/workflows/master.yml | 2 +- .github/workflows/tugboat.yml | 28 +- README.md | 1 + app-config.yaml | 7 + catalog-info.yaml | 9 + cypress/.eslintrc.js | 17 + cypress/README.md | 27 + cypress/cypress.json | 8 + cypress/package.json | 11 + cypress/src/fixtures/.gitkeep | 0 cypress/src/integration/catalog.ts | 29 + cypress/src/plugins/index.ts | 16 + cypress/src/support/index.ts | 25 + .../util/urls.ts => cypress/src/types.d.ts | 23 +- cypress/tsconfig.json | 37 + cypress/yarn.lock | 1409 +++++++++++++++++ docs/api/utility-apis.md | 2 +- docs/assets/search/architecture.drawio.svg | 71 +- docs/auth/auth-backend-classes.md | 30 +- docs/features/search/architecture.md | 29 +- docs/features/techdocs/configuring-ci-cd.md | 83 +- docs/features/techdocs/getting-started.md | 14 +- docs/glossary.md | 5 + docs/overview/roadmap.md | 12 +- docs/reference/utility-apis/AlertApi.md | 10 +- docs/reference/utility-apis/AppThemeApi.md | 20 +- .../utility-apis/BackstageIdentityApi.md | 12 +- docs/reference/utility-apis/Config.md | 10 +- docs/reference/utility-apis/DiscoveryApi.md | 4 +- docs/reference/utility-apis/ErrorApi.md | 12 +- .../reference/utility-apis/FeatureFlagsApi.md | 8 +- docs/reference/utility-apis/IdentityApi.md | 4 +- docs/reference/utility-apis/OAuthApi.md | 10 +- .../reference/utility-apis/OAuthRequestApi.md | 16 +- .../utility-apis/OpenIdConnectApi.md | 8 +- docs/reference/utility-apis/ProfileInfoApi.md | 10 +- docs/reference/utility-apis/README.md | 58 +- docs/reference/utility-apis/SessionApi.md | 14 +- docs/reference/utility-apis/StorageApi.md | 12 +- microsite/static/css/custom.css | 4 + packages/app/CHANGELOG.md | 86 + packages/app/package.json | 54 +- packages/app/src/App.tsx | 12 +- .../app/src/components/catalog/EntityPage.tsx | 9 +- packages/app/src/identityProviders.ts | 7 + packages/backend-common/CHANGELOG.md | 11 + packages/backend-common/package.json | 6 +- .../src/reading/AzureUrlReader.test.ts | 117 +- .../src/reading/AzureUrlReader.ts | 71 +- .../src/reading/BitbucketUrlReader.test.ts | 201 ++- .../src/reading/BitbucketUrlReader.ts | 92 +- .../src/reading/GithubUrlReader.test.ts | 76 +- .../src/reading/GithubUrlReader.ts | 30 +- .../src/reading/GitlabUrlReader.test.ts | 137 +- .../src/reading/GitlabUrlReader.ts | 85 +- .../src/reading/tree/TarArchiveResponse.ts | 9 +- .../src/reading/tree/ZipArchiveResponse.ts | 17 +- .../backend-common/src/reading/tree/util.ts | 25 + .../src/service/lib/ServiceBuilderImpl.ts | 2 +- packages/backend/CHANGELOG.md | 29 + packages/backend/package.json | 16 +- packages/catalog-client/CHANGELOG.md | 7 + packages/catalog-client/package.json | 2 +- .../catalog-client/src/CatalogClient.test.ts | 74 +- packages/catalog-client/src/CatalogClient.ts | 76 +- packages/catalog-client/src/index.ts | 8 +- packages/catalog-client/src/types.ts | 31 +- .../components/artist-lookup-component.yaml | 24 +- packages/cli/package.json | 2 +- packages/cli/src/lib/versioning/Lockfile.ts | 2 +- packages/core-api/src/app/App.tsx | 8 +- packages/core-api/src/app/types.ts | 8 +- packages/core-api/src/icons/icons.tsx | 12 +- packages/core-api/src/icons/types.ts | 6 +- packages/core/CHANGELOG.md | 6 + packages/core/package.json | 4 +- packages/core/src/components/index.ts | 1 + packages/core/src/layout/Sidebar/Items.tsx | 1 - packages/create-app/CHANGELOG.md | 96 ++ packages/create-app/package.json | 40 +- .../templates/default-app/app-config.yaml.hbs | 3 + .../default-app/packages/app/src/App.tsx | 14 +- packages/dev-utils/CHANGELOG.md | 11 + packages/dev-utils/package.json | 8 +- packages/e2e-test/.eslintrc.js | 1 - packages/integration/CHANGELOG.md | 32 + packages/integration/package.json | 4 +- packages/integration/src/ScmIntegrations.ts | 9 +- .../src/azure/AzureIntegration.test.ts | 10 + packages/integration/src/azure/index.ts | 3 +- .../src/bitbucket/BitbucketIntegration.ts | 6 +- packages/integration/src/bitbucket/index.ts | 1 + .../src/github/GitHubIntegration.ts | 6 +- packages/integration/src/github/index.ts | 1 + .../src/gitlab/GitLabIntegration.test.ts | 2 + .../src/gitlab/GitLabIntegration.ts | 6 +- .../integration/src/gitlab/config.test.ts | 10 +- packages/integration/src/gitlab/config.ts | 46 +- packages/integration/src/gitlab/core.test.ts | 4 + packages/integration/src/gitlab/index.ts | 1 + packages/integration/src/helpers.test.ts | 68 +- packages/integration/src/helpers.ts | 60 +- packages/integration/src/index.ts | 1 + packages/integration/src/types.ts | 27 +- packages/techdocs-common/CHANGELOG.md | 20 + .../{@aws-sdk/client-s3.ts => aws-sdk.ts} | 71 +- packages/techdocs-common/package.json | 9 +- packages/techdocs-common/src/helpers.test.ts | 5 +- packages/techdocs-common/src/helpers.ts | 38 +- .../src/stages/prepare/commonGit.test.ts | 15 +- .../src/stages/prepare/commonGit.ts | 44 +- .../src/stages/prepare/dir.test.ts | 16 +- .../techdocs-common/src/stages/prepare/dir.ts | 71 +- .../src/stages/prepare/types.ts | 30 +- .../techdocs-common/src/stages/prepare/url.ts | 28 +- .../src/stages/publish/awsS3.test.ts | 21 +- .../src/stages/publish/awsS3.ts | 95 +- .../src/stages/publish/local.ts | 2 +- packages/test-utils/CHANGELOG.md | 6 + packages/test-utils/package.json | 2 +- plugins/api-docs/CHANGELOG.md | 10 + plugins/api-docs/package.json | 10 +- plugins/app-backend/CHANGELOG.md | 11 + plugins/app-backend/package.json | 4 +- plugins/auth-backend/CHANGELOG.md | 72 + plugins/auth-backend/config.d.ts | 8 +- plugins/auth-backend/package.json | 8 +- .../src/identity/DatabaseKeyStore.test.ts | 8 +- .../src/identity/DatabaseKeyStore.ts | 21 +- .../src/identity/MemoryKeyStore.ts | 9 +- .../auth-backend/src/identity/TokenFactory.ts | 21 +- plugins/auth-backend/src/identity/types.ts | 2 +- .../lib/catalog/CatalogIdentityClient.test.ts | 13 +- .../src/lib/catalog/CatalogIdentityClient.ts | 7 +- .../src/providers/google/provider.ts | 19 +- .../src/providers/oauth2/provider.ts | 4 + .../src/providers/oidc/provider.ts | 2 +- plugins/catalog-backend/CHANGELOG.md | 29 + .../20210209121210_locations_fk_index.js | 45 + plugins/catalog-backend/package.json | 10 +- ...sOrganizationCloudAccountProcessor.test.ts | 66 +- .../AwsOrganizationCloudAccountProcessor.ts | 15 +- .../GithubDiscoveryProcessor.test.ts | 222 +++ .../processors/GithubDiscoveryProcessor.ts | 130 ++ .../processors/github/github.test.ts | 45 + .../src/ingestion/processors/github/github.ts | 39 + .../src/ingestion/processors/github/index.ts | 6 +- .../src/ingestion/processors/index.ts | 1 + .../src/service/CatalogBuilder.ts | 2 + plugins/catalog-import/CHANGELOG.md | 40 + plugins/catalog-import/README.md | 85 +- plugins/catalog-import/dev/index.tsx | 335 +++- .../docs/catalog-import-screenshot.png | Bin 0 -> 365037 bytes plugins/catalog-import/package.json | 16 +- .../src/api/CatalogImportApi.ts | 39 +- .../src/api/CatalogImportClient.test.ts | 358 ++++- .../src/api/CatalogImportClient.ts | 202 ++- plugins/catalog-import/src/api/GitHub.ts | 39 + .../src/assets/catalog-import-screenshot.png | Bin 22602 -> 0 bytes .../src/components/Buttons/index.tsx | 70 + .../src/components/ComponentConfigDisplay.tsx | 200 --- .../EntityListComponent.tsx | 165 ++ .../components/EntityListComponent/index.ts | 17 + .../components/ImportComponentForm.test.tsx | 90 -- .../src/components/ImportComponentForm.tsx | 154 -- .../components/ImportComponentPage.test.tsx | 260 +-- .../src/components/ImportComponentPage.tsx | 159 +- .../src/components/ImportFinished.tsx | 58 - .../src/components/ImportStepper.tsx | 42 - .../ImportStepper/ImportStepper.tsx | 104 ++ .../src/components/ImportStepper/defaults.tsx | 293 ++++ .../src/components/ImportStepper/index.ts | 18 + .../catalog-import/src/components/Router.tsx | 8 +- .../StepFinishImportLocation.tsx | 65 + .../StepFinishImportLocation/index.ts | 17 + .../StepInitAnalyzeUrl.test.tsx | 401 +++++ .../StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx | 159 ++ .../components/StepInitAnalyzeUrl/index.ts | 17 + .../AutocompleteTextField.tsx | 100 ++ .../PreparePullRequestForm.test.tsx | 113 ++ .../PreparePullRequestForm.tsx | 67 + .../PreviewCatalogInfoComponent.test.tsx | 101 ++ .../PreviewCatalogInfoComponent.tsx | 56 + .../PreviewPullRequestComponent.test.tsx | 83 + .../PreviewPullRequestComponent.tsx | 40 + .../StepPrepareCreatePullRequest.test.tsx | 287 ++++ .../StepPrepareCreatePullRequest.tsx | 249 +++ .../StepPrepareCreatePullRequest/index.ts | 21 + .../StepPrepareSelectLocations.test.tsx | 202 +++ .../StepPrepareSelectLocations.tsx | 124 ++ .../StepPrepareSelectLocations/index.ts | 17 + .../StepReviewLocation/StepReviewLocation.tsx | 134 ++ .../components/StepReviewLocation/index.ts | 17 + .../catalog-import/src/components/index.ts | 20 + .../src/components/useImportState.test.tsx | 372 +++++ .../src/components/useImportState.ts | 291 ++++ plugins/catalog-import/src/index.ts | 1 + .../mocks/analyze-location-POST-response.json | 22 - .../src/mocks/locations-POST-response.json | 39 - plugins/catalog-import/src/plugin.ts | 27 +- .../catalog-import/src/{util => }/types.ts | 0 .../catalog-import/src/util/useGithubRepos.ts | 121 -- .../catalog-import/src/util/validate.test.ts | 33 - plugins/catalog-react/CHANGELOG.md | 11 + plugins/catalog-react/package.json | 10 +- .../EntityRefLink/EntityRefLink.tsx | 74 +- plugins/catalog/CHANGELOG.md | 35 + plugins/catalog/package.json | 15 +- .../catalog/src/CatalogClientWrapper.test.ts | 169 ++ plugins/catalog/src/CatalogClientWrapper.ts | 97 ++ .../components/AboutCard/AboutCard.test.tsx | 21 +- .../src/components/AboutCard/AboutCard.tsx | 11 +- .../CatalogFilter/CatalogFilter.tsx | 8 + .../CatalogPage/CatalogPage.test.tsx | 22 +- .../components/CatalogPage/CatalogPage.tsx | 18 +- .../components/CatalogPage/utils/timeUtil.js | 26 - .../CatalogPage/utils/timeUtil.test.js | 15 +- .../EntityContextMenu/EntityContextMenu.tsx | 7 - .../EntityLinksCard/EntityLinksCard.test.tsx | 81 + .../EntityLinksCard/EntityLinksCard.tsx | 58 + .../EntityLinksCard/EntityLinksEmptyState.tsx | 65 + .../EntityLinksCard/IconLink.test.tsx | 38 + .../components/EntityLinksCard/IconLink.tsx | 58 + .../EntityLinksCard/LinksGridList.tsx | 47 + .../src/components/EntityLinksCard/index.ts} | 6 +- .../src/components/EntityLinksCard/types.ts | 19 + .../EntityLinksCard/useDynamicColumns.tsx | 49 + .../UnregisterEntityDialog.tsx | 2 +- .../src/filter/useEntityFilterGroup.ts | 10 +- plugins/catalog/src/index.ts | 2 + plugins/catalog/src/plugin.ts | 28 +- plugins/circleci/CHANGELOG.md | 10 + plugins/circleci/package.json | 10 +- plugins/cloudbuild/CHANGELOG.md | 10 + plugins/cloudbuild/package.json | 10 +- plugins/cost-insights/CHANGELOG.md | 10 + plugins/cost-insights/dev/index.tsx | 4 +- plugins/cost-insights/package.json | 8 +- .../src/forms/AlertDismissForm.tsx | 3 + plugins/cost-insights/src/index.ts | 8 +- plugins/cost-insights/src/plugin.test.ts | 4 +- plugins/cost-insights/src/plugin.ts | 59 +- plugins/explore/CHANGELOG.md | 10 + plugins/explore/package.json | 10 +- .../DomainExplorerContent.test.tsx | 13 + .../DomainExplorerContent.tsx | 10 +- plugins/fossa/CHANGELOG.md | 10 + plugins/fossa/package.json | 10 +- plugins/fossa/src/api/FossaClient.test.ts | 25 +- plugins/fossa/src/api/FossaClient.ts | 11 +- plugins/fossa/src/plugin.ts | 10 +- plugins/gcp-projects/package.json | 6 +- plugins/github-actions/CHANGELOG.md | 12 + plugins/github-actions/package.json | 12 +- plugins/gitops-profiles/CHANGELOG.md | 8 + plugins/gitops-profiles/package.json | 8 +- plugins/graphiql/package.json | 6 +- plugins/jenkins/CHANGELOG.md | 11 + plugins/jenkins/package.json | 10 +- .../jenkins/src/components/Cards/Cards.tsx | 14 +- plugins/kafka/CHANGELOG.md | 10 + plugins/kafka/package.json | 10 +- plugins/kubernetes/CHANGELOG.md | 11 + plugins/kubernetes/package.json | 10 +- .../src/api/KubernetesBackendClient.ts | 11 +- plugins/kubernetes/src/plugin.ts | 7 +- plugins/lighthouse/CHANGELOG.md | 10 + plugins/lighthouse/package.json | 10 +- plugins/newrelic/CHANGELOG.md | 8 + plugins/newrelic/package.json | 8 +- plugins/org/CHANGELOG.md | 10 + plugins/org/package.json | 10 +- .../Group/GroupProfile/GroupProfileCard.tsx | 107 +- .../User/UserProfileCard/UserProfileCard.tsx | 74 +- plugins/pagerduty/CHANGELOG.md | 11 + plugins/pagerduty/package.json | 16 +- .../components/Incident/IncidentListItem.tsx | 10 +- plugins/register-component/CHANGELOG.md | 10 + plugins/register-component/package.json | 10 +- plugins/rollbar/CHANGELOG.md | 13 + plugins/rollbar/package.json | 10 +- plugins/rollbar/src/api/RollbarClient.ts | 14 +- plugins/rollbar/src/plugin.ts | 6 +- plugins/scaffolder-backend/CHANGELOG.md | 23 + plugins/scaffolder-backend/package.json | 9 +- .../docs-template/template.yaml | 1 + .../src/lib/catalog/CatalogEntityClient.ts | 40 +- .../scaffolder/stages/prepare/bitbucket.ts | 2 +- .../src/scaffolder/stages/prepare/github.ts | 2 +- .../scaffolder/stages/prepare/gitlab.test.ts | 4 +- .../src/scaffolder/stages/prepare/gitlab.ts | 2 +- .../scaffolder/stages/publish/gitlab.test.ts | 3 + .../scaffolder/stages/publish/publishers.ts | 2 + .../scaffolder-backend/src/service/router.ts | 9 +- plugins/scaffolder/CHANGELOG.md | 13 + plugins/scaffolder/dev/index.tsx | 7 +- plugins/scaffolder/package.json | 12 +- plugins/scaffolder/src/api.ts | 16 +- .../components/TemplateCard/TemplateCard.tsx | 23 +- .../TemplatePage/TemplatePage.test.tsx | 6 +- .../components/TemplatePage/TemplatePage.tsx | 4 +- plugins/scaffolder/src/plugin.ts | 6 +- plugins/search/CHANGELOG.md | 13 + plugins/search/package.json | 10 +- plugins/sentry/CHANGELOG.md | 10 + plugins/sentry/package.json | 10 +- plugins/sentry/src/extensions.tsx | 6 +- plugins/sentry/src/index.ts | 2 +- plugins/sentry/src/plugin.test.ts | 4 +- plugins/sentry/src/plugin.ts | 2 +- plugins/sonarqube/CHANGELOG.md | 9 + plugins/sonarqube/package.json | 10 +- plugins/tech-radar/CHANGELOG.md | 8 + plugins/tech-radar/dev/index.tsx | 11 +- plugins/tech-radar/package.json | 8 +- plugins/tech-radar/src/index.ts | 6 +- plugins/tech-radar/src/plugin.test.ts | 4 +- plugins/tech-radar/src/plugin.ts | 24 +- plugins/techdocs-backend/CHANGELOG.md | 21 + plugins/techdocs-backend/package.json | 6 +- .../DocsBuilder/BuildMetadataStorage.test.ts | 8 +- .../src/DocsBuilder/BuildMetadataStorage.ts | 15 +- .../src/DocsBuilder/builder.ts | 165 +- .../techdocs-backend/src/service/router.ts | 39 +- plugins/techdocs/CHANGELOG.md | 18 + plugins/techdocs/package.json | 14 +- plugins/user-settings/package.json | 6 +- .../src/components/SettingsPage.tsx | 40 +- plugins/welcome/package.json | 6 +- yarn.lock | 1181 ++++---------- 373 files changed, 11341 insertions(+), 3680 deletions(-) create mode 100644 .changeset/afraid-dingos-own.md delete mode 100644 .changeset/afraid-teachers-cross.md create mode 100644 .changeset/angry-walls-mate.md create mode 100644 .changeset/chilly-cars-shout.md delete mode 100644 .changeset/cost-insights-tidy-geese-play.md delete mode 100644 .changeset/curly-crabs-compete.md delete mode 100644 .changeset/cyan-lies-flow.md create mode 100644 .changeset/dry-llamas-wave.md delete mode 100644 .changeset/early-hotels-mate.md create mode 100644 .changeset/eight-doors-matter.md delete mode 100644 .changeset/empty-cooks-laugh.md create mode 100644 .changeset/five-guests-promise.md delete mode 100644 .changeset/fluffy-hats-hug.md delete mode 100644 .changeset/four-rings-push.md delete mode 100644 .changeset/funny-students-shout.md create mode 100644 .changeset/grumpy-cups-hope.md delete mode 100644 .changeset/hot-pants-taste.md delete mode 100644 .changeset/large-terms-invite.md create mode 100644 .changeset/new-mangos-tap.md delete mode 100644 .changeset/ninety-houses-shout.md create mode 100644 .changeset/ninety-lemons-shake.md delete mode 100644 .changeset/odd-buckets-compare.md delete mode 100644 .changeset/old-tools-exist.md delete mode 100644 .changeset/perfect-ladybugs-listen.md delete mode 100644 .changeset/poor-sheep-give.md delete mode 100644 .changeset/popular-cars-eat.md delete mode 100644 .changeset/selfish-years-pump.md delete mode 100644 .changeset/shiny-falcons-marry.md delete mode 100644 .changeset/shiny-shirts-worry.md delete mode 100644 .changeset/shy-maps-guess.md create mode 100644 .changeset/six-elephants-poke.md delete mode 100644 .changeset/sixty-lemons-agree.md delete mode 100644 .changeset/strange-cobras-unite.md create mode 100644 .changeset/strange-olives-unite.md create mode 100644 .changeset/stupid-maps-do.md delete mode 100644 .changeset/techdocs-green-rabbits-burn.md create mode 100644 .changeset/thick-scissors-notice.md create mode 100644 .changeset/tough-worms-clap.md delete mode 100644 .changeset/tricky-dancers-rush.md delete mode 100644 .changeset/two-dogs-search.md create mode 100644 cypress/.eslintrc.js create mode 100644 cypress/README.md create mode 100644 cypress/cypress.json create mode 100644 cypress/package.json create mode 100644 cypress/src/fixtures/.gitkeep create mode 100644 cypress/src/integration/catalog.ts create mode 100644 cypress/src/plugins/index.ts create mode 100644 cypress/src/support/index.ts rename plugins/catalog-import/src/util/urls.ts => cypress/src/types.d.ts (62%) create mode 100644 cypress/tsconfig.json create mode 100644 cypress/yarn.lock create mode 100644 packages/backend-common/src/reading/tree/util.ts rename packages/techdocs-common/__mocks__/{@aws-sdk/client-s3.ts => aws-sdk.ts} (52%) mode change 100755 => 100644 packages/techdocs-common/src/stages/publish/awsS3.test.ts create mode 100644 plugins/catalog-backend/migrations/20210209121210_locations_fk_index.js create mode 100644 plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts create mode 100644 plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts create mode 100644 plugins/catalog-import/docs/catalog-import-screenshot.png create mode 100644 plugins/catalog-import/src/api/GitHub.ts delete mode 100644 plugins/catalog-import/src/assets/catalog-import-screenshot.png create mode 100644 plugins/catalog-import/src/components/Buttons/index.tsx delete mode 100644 plugins/catalog-import/src/components/ComponentConfigDisplay.tsx create mode 100644 plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx create mode 100644 plugins/catalog-import/src/components/EntityListComponent/index.ts delete mode 100644 plugins/catalog-import/src/components/ImportComponentForm.test.tsx delete mode 100644 plugins/catalog-import/src/components/ImportComponentForm.tsx delete mode 100644 plugins/catalog-import/src/components/ImportFinished.tsx delete mode 100644 plugins/catalog-import/src/components/ImportStepper.tsx create mode 100644 plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx create mode 100644 plugins/catalog-import/src/components/ImportStepper/defaults.tsx create mode 100644 plugins/catalog-import/src/components/ImportStepper/index.ts create mode 100644 plugins/catalog-import/src/components/StepFinishImportLocation/StepFinishImportLocation.tsx create mode 100644 plugins/catalog-import/src/components/StepFinishImportLocation/index.ts create mode 100644 plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx create mode 100644 plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx create mode 100644 plugins/catalog-import/src/components/StepInitAnalyzeUrl/index.ts create mode 100644 plugins/catalog-import/src/components/StepPrepareCreatePullRequest/AutocompleteTextField.tsx create mode 100644 plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.test.tsx create mode 100644 plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.tsx create mode 100644 plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx create mode 100644 plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.tsx create mode 100644 plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.test.tsx create mode 100644 plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.tsx create mode 100644 plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx create mode 100644 plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx create mode 100644 plugins/catalog-import/src/components/StepPrepareCreatePullRequest/index.ts create mode 100644 plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.test.tsx create mode 100644 plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.tsx create mode 100644 plugins/catalog-import/src/components/StepPrepareSelectLocations/index.ts create mode 100644 plugins/catalog-import/src/components/StepReviewLocation/StepReviewLocation.tsx create mode 100644 plugins/catalog-import/src/components/StepReviewLocation/index.ts create mode 100644 plugins/catalog-import/src/components/index.ts create mode 100644 plugins/catalog-import/src/components/useImportState.test.tsx create mode 100644 plugins/catalog-import/src/components/useImportState.ts delete mode 100644 plugins/catalog-import/src/mocks/analyze-location-POST-response.json delete mode 100644 plugins/catalog-import/src/mocks/locations-POST-response.json rename plugins/catalog-import/src/{util => }/types.ts (100%) delete mode 100644 plugins/catalog-import/src/util/useGithubRepos.ts delete mode 100644 plugins/catalog-import/src/util/validate.test.ts create mode 100644 plugins/catalog/src/CatalogClientWrapper.test.ts create mode 100644 plugins/catalog/src/CatalogClientWrapper.ts create mode 100644 plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.test.tsx create mode 100644 plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx create mode 100644 plugins/catalog/src/components/EntityLinksCard/EntityLinksEmptyState.tsx create mode 100644 plugins/catalog/src/components/EntityLinksCard/IconLink.test.tsx create mode 100644 plugins/catalog/src/components/EntityLinksCard/IconLink.tsx create mode 100644 plugins/catalog/src/components/EntityLinksCard/LinksGridList.tsx rename plugins/{catalog-import/src/util/validate.ts => catalog/src/components/EntityLinksCard/index.ts} (76%) create mode 100644 plugins/catalog/src/components/EntityLinksCard/types.ts create mode 100644 plugins/catalog/src/components/EntityLinksCard/useDynamicColumns.tsx diff --git a/.changeset/afraid-dingos-own.md b/.changeset/afraid-dingos-own.md new file mode 100644 index 0000000000..9f7010d330 --- /dev/null +++ b/.changeset/afraid-dingos-own.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +pass registered logger to requestLoggingHandler diff --git a/.changeset/afraid-teachers-cross.md b/.changeset/afraid-teachers-cross.md deleted file mode 100644 index 8fc376441f..0000000000 --- a/.changeset/afraid-teachers-cross.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Pass on plugin database management instance that is now required by the scaffolder plugin. - -To apply this change to an existing application, add the following to `src/plugins/scaffolder.ts`: - -```diff -export default async function createPlugin({ - logger, - config, -+ database, -}: PluginEnvironment) { - -// ...omitted... - - return await createRouter({ - preparers, - templaters, - publishers, - logger, - config, - dockerClient, - entityClient, -+ database, - }); -} -``` diff --git a/.changeset/angry-walls-mate.md b/.changeset/angry-walls-mate.md new file mode 100644 index 0000000000..5902168b2c --- /dev/null +++ b/.changeset/angry-walls-mate.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': minor +--- + +Make `ScmIntegration.resolveUrl` mandatory. diff --git a/.changeset/chilly-cars-shout.md b/.changeset/chilly-cars-shout.md new file mode 100644 index 0000000000..60871b07a3 --- /dev/null +++ b/.changeset/chilly-cars-shout.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Make the `TemplateCard` conform to what material-ui recommends in their examples. This fixes the extra padding around the buttons. diff --git a/.changeset/cost-insights-tidy-geese-play.md b/.changeset/cost-insights-tidy-geese-play.md deleted file mode 100644 index f8aa503ca6..0000000000 --- a/.changeset/cost-insights-tidy-geese-play.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-cost-insights': patch ---- - -Fix snooze quarter option diff --git a/.changeset/curly-crabs-compete.md b/.changeset/curly-crabs-compete.md deleted file mode 100644 index 552a41a9f1..0000000000 --- a/.changeset/curly-crabs-compete.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-circleci': patch ---- - -Migrated to new composability API, exporting the plugin instance as `circleCIPlugin`, the entity page content as `EntityCircleCIContent`, and entity conditional as `isCircleCIAvailable`. diff --git a/.changeset/cyan-lies-flow.md b/.changeset/cyan-lies-flow.md deleted file mode 100644 index e7da9fab0a..0000000000 --- a/.changeset/cyan-lies-flow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-search': minor ---- - -Migrated to new composability API, exporting the plugin instance as `searchPlugin`, and page as `SearchPage`. Due to the old router component also being called `SearchPage`, this is a breaking change. The old page component is now exported as `Router`, which can be used to maintain the old behavior. diff --git a/.changeset/dry-llamas-wave.md b/.changeset/dry-llamas-wave.md new file mode 100644 index 0000000000..fea9b122a6 --- /dev/null +++ b/.changeset/dry-llamas-wave.md @@ -0,0 +1,7 @@ +--- +'@backstage/backend-common': patch +--- + +Implement `UrlReader.search` for the other providers (Azure, Bitbucket, GitLab) as well. + +The `UrlReader` subclasses now are implemented in terms of the respective `Integration` class. diff --git a/.changeset/early-hotels-mate.md b/.changeset/early-hotels-mate.md deleted file mode 100644 index 2cda0344bf..0000000000 --- a/.changeset/early-hotels-mate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -use child logger, if provided, to log single location refresh diff --git a/.changeset/eight-doors-matter.md b/.changeset/eight-doors-matter.md new file mode 100644 index 0000000000..7dbb56b6c3 --- /dev/null +++ b/.changeset/eight-doors-matter.md @@ -0,0 +1,19 @@ +--- +'@backstage/core-api': patch +'@backstage/plugin-catalog': patch +--- + +Minor refactoring of BackstageApp.getSystemIcons to support custom registered +icons. Custom Icons can be added using: + +```tsx +import AlarmIcon from '@material-ui/icons/Alarm'; +import MyPersonIcon from './MyPerson'; + +const app = createApp({ + icons: { + user: MyPersonIcon // override system icon + alert: AlarmIcon, // Custom icon + }, +}); +``` diff --git a/.changeset/empty-cooks-laugh.md b/.changeset/empty-cooks-laugh.md deleted file mode 100644 index 8f509f08cd..0000000000 --- a/.changeset/empty-cooks-laugh.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-app-backend': patch ---- - -Added a new `disableConfigInjection` option, which can be used to disable the configuration injection in environments where it can't be used. diff --git a/.changeset/five-guests-promise.md b/.changeset/five-guests-promise.md new file mode 100644 index 0000000000..1d4d0ae303 --- /dev/null +++ b/.changeset/five-guests-promise.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Remove the "Move repository" menu entry from the catalog page, as it's just a placeholder. diff --git a/.changeset/fluffy-hats-hug.md b/.changeset/fluffy-hats-hug.md deleted file mode 100644 index b3d06337fd..0000000000 --- a/.changeset/fluffy-hats-hug.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Don't respond to a request twice if an entity has not been found. diff --git a/.changeset/four-rings-push.md b/.changeset/four-rings-push.md deleted file mode 100644 index 500dd0e812..0000000000 --- a/.changeset/four-rings-push.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-cloudbuild': patch ---- - -Migrate to new composability API, exporting the plugin instance as `cloudbuildPlugin`, the entity content as `EntityCloudbuildContent`, the entity conditional as `isCloudbuildAvailable`, and entity cards as `EntityLatestCloudbuildRunCard` and `EntityLatestCloudbuildsForBranchCard`. diff --git a/.changeset/funny-students-shout.md b/.changeset/funny-students-shout.md deleted file mode 100644 index 97b273c687..0000000000 --- a/.changeset/funny-students-shout.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-register-component': patch ---- - -Migrated to new composability API, exporting the plugin instance as `registerComponentPlugin`, and page as `RegisterComponentPage`. diff --git a/.changeset/grumpy-cups-hope.md b/.changeset/grumpy-cups-hope.md new file mode 100644 index 0000000000..6445c07efa --- /dev/null +++ b/.changeset/grumpy-cups-hope.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-user-settings': patch +--- + +Use routed tabs to link to every settings page. diff --git a/.changeset/hot-pants-taste.md b/.changeset/hot-pants-taste.md deleted file mode 100644 index 5cce26c5bf..0000000000 --- a/.changeset/hot-pants-taste.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/plugin-api-docs': patch -'@backstage/plugin-catalog': patch -'@backstage/plugin-lighthouse': patch -'@backstage/plugin-techdocs': patch ---- - -Improve display of error messages diff --git a/.changeset/large-terms-invite.md b/.changeset/large-terms-invite.md deleted file mode 100644 index 6653ac8e1e..0000000000 --- a/.changeset/large-terms-invite.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': minor ---- - -Introduced `v2` Scaffolder REST API, which uses an implementation that is database backed, making the scaffolder instances stateless. The `createRouter` function now requires a `PluginDatabaseManager` instance to be passed in, commonly available as `database` in the plugin environment in the backend. - -This API should be considered unstable until used by the scaffolder frontend. diff --git a/.changeset/new-mangos-tap.md b/.changeset/new-mangos-tap.md new file mode 100644 index 0000000000..1b57510417 --- /dev/null +++ b/.changeset/new-mangos-tap.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Tweak error message in lockfile parsing to include more information. diff --git a/.changeset/ninety-houses-shout.md b/.changeset/ninety-houses-shout.md deleted file mode 100644 index 845decc74c..0000000000 --- a/.changeset/ninety-houses-shout.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Migrated to new composability API, exporting the plugin instance as `techdocsPlugin`, the top-level page as `TechdocsPage`, and the entity content as `EntityTechdocsContent`. diff --git a/.changeset/ninety-lemons-shake.md b/.changeset/ninety-lemons-shake.md new file mode 100644 index 0000000000..960a4eaffe --- /dev/null +++ b/.changeset/ninety-lemons-shake.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Limit the props that are forwarded to the `Link` component in the `EntityRefLink`. diff --git a/.changeset/odd-buckets-compare.md b/.changeset/odd-buckets-compare.md deleted file mode 100644 index faf68fb4a0..0000000000 --- a/.changeset/odd-buckets-compare.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Set explicit content-type in error handler responses. diff --git a/.changeset/old-tools-exist.md b/.changeset/old-tools-exist.md deleted file mode 100644 index a9365cb784..0000000000 --- a/.changeset/old-tools-exist.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Slight refactoring in support of a future search implementation in `UrlReader`. Mostly moving code around. diff --git a/.changeset/perfect-ladybugs-listen.md b/.changeset/perfect-ladybugs-listen.md deleted file mode 100644 index 2f9e65c1d9..0000000000 --- a/.changeset/perfect-ladybugs-listen.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-pagerduty': patch ---- - -Migrated to new composability API, exporting the plugin instance as `pagerDutyPlugin`, entity card as `EntityPagerDutyCard`, and entity conditional as `isPagerDutyAvailable`. diff --git a/.changeset/poor-sheep-give.md b/.changeset/poor-sheep-give.md deleted file mode 100644 index 4c52594b35..0000000000 --- a/.changeset/poor-sheep-give.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/test-utils': patch ---- - -Added `mountedRoutes` option to `wrapInTestApp`, allowing routes to be associated to concrete paths to make `useRouteRef` usable in tested components. diff --git a/.changeset/popular-cars-eat.md b/.changeset/popular-cars-eat.md deleted file mode 100644 index 6d3bcdfab2..0000000000 --- a/.changeset/popular-cars-eat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-jenkins': patch ---- - -Migrate to new composability API, exporting the plugin instance as `jenkinsPlugin`, the entity content as `EntityJenkinsContent`, the entity conditional as `isJenkinsAvailable`, and the entity card as `EntityLatestJenkinsRunCard`. diff --git a/.changeset/selfish-years-pump.md b/.changeset/selfish-years-pump.md deleted file mode 100644 index e5cc5f3bc3..0000000000 --- a/.changeset/selfish-years-pump.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-github-actions': patch ---- - -Migrate to new composability API, exporting the plugin instance as `githubActionsPlugin`, the entity content as `EntityGithubActionsContent`, entity conditional as `isGithubActionsAvailable`, and entity cards as `EntityLatestGithubActionRunCard`, `EntityLatestGithubActionsForBranchCard`, and `EntityRecentGithubActionsRunsCard`. diff --git a/.changeset/shiny-falcons-marry.md b/.changeset/shiny-falcons-marry.md deleted file mode 100644 index 4dda68d693..0000000000 --- a/.changeset/shiny-falcons-marry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-newrelic': patch ---- - -Migrate to new composability API, exporting the plugin instance as `newRelicPlugin`, and the root page as `NewRelicPage`. diff --git a/.changeset/shiny-shirts-worry.md b/.changeset/shiny-shirts-worry.md deleted file mode 100644 index b4797b61b4..0000000000 --- a/.changeset/shiny-shirts-worry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Implement `UrlReader.search` which implements glob matching. diff --git a/.changeset/shy-maps-guess.md b/.changeset/shy-maps-guess.md deleted file mode 100644 index 2a50d3d461..0000000000 --- a/.changeset/shy-maps-guess.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-kubernetes': patch ---- - -Migrate to new composability API, exporting the plugin instance as `kubernetesPlugin` and entity content as `EntityKubernetesContent`. diff --git a/.changeset/six-elephants-poke.md b/.changeset/six-elephants-poke.md new file mode 100644 index 0000000000..23e575b67c --- /dev/null +++ b/.changeset/six-elephants-poke.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Migrate about card to new composability API, exporting the entity cards as `EntityAboutCard`. diff --git a/.changeset/sixty-lemons-agree.md b/.changeset/sixty-lemons-agree.md deleted file mode 100644 index 7fc438b433..0000000000 --- a/.changeset/sixty-lemons-agree.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-kafka': patch ---- - -Migrate to new composability API, exporting the plugin instance as `kafkaPlugin`, entity content as `EntityKafkaContent`, and entity conditional as `isKafkaAvailable`. diff --git a/.changeset/strange-cobras-unite.md b/.changeset/strange-cobras-unite.md deleted file mode 100644 index d53a324dab..0000000000 --- a/.changeset/strange-cobras-unite.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Refactored route response handling to use more explicit types and throw errors. diff --git a/.changeset/strange-olives-unite.md b/.changeset/strange-olives-unite.md new file mode 100644 index 0000000000..7490f6096a --- /dev/null +++ b/.changeset/strange-olives-unite.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Migrated the package from using moment to Luxon. #4278 diff --git a/.changeset/stupid-maps-do.md b/.changeset/stupid-maps-do.md new file mode 100644 index 0000000000..1cbac64f68 --- /dev/null +++ b/.changeset/stupid-maps-do.md @@ -0,0 +1,5 @@ +--- +'@backstage/core': patch +--- + +Export Select component diff --git a/.changeset/techdocs-green-rabbits-burn.md b/.changeset/techdocs-green-rabbits-burn.md deleted file mode 100644 index 5af68c2000..0000000000 --- a/.changeset/techdocs-green-rabbits-burn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Enhance API calls to support trapping 500 errors from techdocs-backend diff --git a/.changeset/thick-scissors-notice.md b/.changeset/thick-scissors-notice.md new file mode 100644 index 0000000000..ccf78f3abf --- /dev/null +++ b/.changeset/thick-scissors-notice.md @@ -0,0 +1,5 @@ +--- +'@backstage/core': patch +--- + +Make sure that SidebarItems are also active when on sub route. diff --git a/.changeset/tough-worms-clap.md b/.changeset/tough-worms-clap.md new file mode 100644 index 0000000000..3a18593549 --- /dev/null +++ b/.changeset/tough-worms-clap.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Fix parsing of the path to default to empty string not undefined if git-url-parse throws something we don't expect. Fixes the error `The "path" argument must be of type string.` when preparing. diff --git a/.changeset/tricky-dancers-rush.md b/.changeset/tricky-dancers-rush.md deleted file mode 100644 index 301b23d98e..0000000000 --- a/.changeset/tricky-dancers-rush.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core': patch ---- - -Fixed type inference of `createRouteRef`. diff --git a/.changeset/two-dogs-search.md b/.changeset/two-dogs-search.md deleted file mode 100644 index 5f673c0cfb..0000000000 --- a/.changeset/two-dogs-search.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-gitops-profiles': patch ---- - -Migrated to new composability API, exporting the plugin instance as `gitopsProfilesPlugin` and pages as `GitopsProfilesClusterListPage`, `GitopsProfilesClusterPage`, and `GitopsProfilesCreatePage`. diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9a5ef09554..371fae6e6b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,6 +7,7 @@ * @backstage/maintainers /docs/features/techdocs @backstage/techdocs-core /docs/features/search @backstage/techdocs-core +/docs/assets/search @backstage/techdocs-core /plugins/cost-insights @backstage/silver-lining /plugins/cloudbuild @trivago/ebarrios /plugins/search @backstage/techdocs-core diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 2991a6babd..519216ecd8 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -119,6 +119,7 @@ Kumar learnings lerna Lerna +lockfile Luxon magiclink mailto @@ -249,6 +250,7 @@ transpilation Tuite ui unmanaged +unregister untracked upvote url diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa2feebb3e..08ec67162b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,7 +77,7 @@ jobs: run: yarn prettier:check - name: validate config - run: yarn backstage-cli config:check + run: yarn backstage-cli config:check --lax - name: lint run: yarn lerna -- run lint --since origin/master diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 425e1172a4..60db9ac16b 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -65,7 +65,7 @@ jobs: COMMIT_SHA_BEFORE: '${{ github.event.before }}' - name: validate config - run: yarn backstage-cli config:check + run: yarn backstage-cli config:check --lax - name: lint run: yarn lerna -- run lint diff --git a/.github/workflows/tugboat.yml b/.github/workflows/tugboat.yml index fdb2e1ddc4..bb5257b7c6 100644 --- a/.github/workflows/tugboat.yml +++ b/.github/workflows/tugboat.yml @@ -48,11 +48,14 @@ jobs: target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}" }); - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: node-version: '14' + - name: yarn install + run: yarn --cwd cypress install + # This is required because the environment_url param that Tugboat uses # to tell us where the preview is located isn't supported unless you # specify the custom Accept header when getting the deployment_status, @@ -77,9 +80,25 @@ jobs: }); console.log(result); return result.data.environment_url; - - name: echo tugboat preview url - run: | - curl ${{steps.get-status-env.outputs.result}} + + - name: cypress run + uses: cypress-io/github-action@v2 + env: + CYPRESS_baseUrl: ${{steps.get-status-env.outputs.result}} + with: + config-file: ./cypress.json + working-directory: ./cypress + browser: chrome + install: false + headless: true + + - name: update artifact + if: ${{ always() }} + uses: actions/upload-artifact@v1 + with: + name: cypress-videos + path: ./cypress/cypress/videos + - name: set status if: ${{ failure() }} uses: actions/github-script@v3 @@ -94,6 +113,7 @@ jobs: context: 'Backstage Tugboat E2E Tests', target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}" }); + - name: set status if: ${{ success() }} uses: actions/github-script@v3 diff --git a/README.md b/README.md index 7c04672998..d01be3f54c 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ Check out [the documentation](https://backstage.io/docs/getting-started) on how - [Adopters](ADOPTERS.md) - Companies already using Backstage - [Blog](https://backstage.io/blog/) - Announcements and updates - [Newsletter](https://mailchi.mp/spotify/backstage-community) - Subscribe to our email newsletter +- [Backstage Community Sessions](https://github.com/backstage/community) - Join monthly meetup and explore Backstage community - Give us a star ⭐️ - If you are using Backstage or think it is an interesting project, we would love a star ❤️ ## License diff --git a/app-config.yaml b/app-config.yaml index 5240a050a8..9df6540a2c 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -73,6 +73,9 @@ organization: name: My Company # Reference documentation http://backstage.io/docs/features/techdocs/configuration +# Note: After experimenting with basic setup, use CI/CD to generate docs +# and an external cloud storage when deploying TechDocs for production use-case. +# https://backstage.io/docs/features/techdocs/how-to-guides#how-to-migrate-from-techdocs-basic-to-recommended-deployment-approach techdocs: builder: 'local' # Alternatives - 'external' generators: @@ -305,6 +308,10 @@ auth: $env: AUTH_OAUTH2_AUTH_URL tokenUrl: $env: AUTH_OAUTH2_TOKEN_URL + ### + # provide a list of scopes as needed for your OAuth2 Server: + # + # scope: saml-login-selector openid profile email oidc: development: metadataUrl: diff --git a/catalog-info.yaml b/catalog-info.yaml index 7e60af5755..0cab0c558a 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -4,6 +4,15 @@ metadata: name: backstage description: | Backstage is an open-source developer portal that puts the developer experience first. + links: + - title: Website + url: http://backstage.io + - title: Documentation + url: https://backstage.io/docs + - title: Storybook + url: https://backstage.io/storybook + - title: Discord Chat + url: https://discord.com/invite/EBHEGzX annotations: github.com/project-slug: backstage/backstage backstage.io/techdocs-ref: url:https://github.com/backstage/backstage diff --git a/cypress/.eslintrc.js b/cypress/.eslintrc.js new file mode 100644 index 0000000000..d7498649a0 --- /dev/null +++ b/cypress/.eslintrc.js @@ -0,0 +1,17 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], + rules: { + 'no-console': 0, + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: true, + optionalDependencies: false, + peerDependencies: false, + bundledDependencies: false, + }, + ], + 'jest/expect-expect': 'off', + 'no-restricted-syntax': 'off', + }, +}; diff --git a/cypress/README.md b/cypress/README.md new file mode 100644 index 0000000000..2bb310e300 --- /dev/null +++ b/cypress/README.md @@ -0,0 +1,27 @@ +# Cypress Tests for Backstage + +Hey 👋 Welcome to the Cypress tests for Backstage. They're designed to be run against the `packages/app` folder in the main repo, and be some form of smoke tests to make sure that we don't break any core functionality. + +They run part of the PR build, and are triggered from the `.github/workflows/tugboat.yml` file. + +The main app gets built up part of a [Tugboat Build](https://tugboat.qa), which when complete, sends a `deployment event` to the PR triggering the aforementioned workflow. + +### Running Locally + +In order to make typescript happy, this `cypress` package is separate from all the Jest dependencies in the monorepo workspaces setup. + +You can run the e2e tests locally pointing at your local running version like so: + +```sh +cd cypress +yarn install +yarn cypress run +``` + +You can open up the `cypress` console by using `yarn cypress open`. + +You can also run towards any Backstage installation by using the Cypress Environment Variable overrides. + +```sh +CYPRESS_baseUrl="http://demo.backstage.io" yarn cypress run +``` diff --git a/cypress/cypress.json b/cypress/cypress.json new file mode 100644 index 0000000000..ebbbe59901 --- /dev/null +++ b/cypress/cypress.json @@ -0,0 +1,8 @@ +{ + "baseUrl": "http://localhost:7000", + "integrationFolder": "./src/integration", + "supportFile": "./src/support", + "fixturesFolder": "./src/fixures", + "pluginsFile": "./src/plugins", + "defaultCommandTimeout": 10000 +} diff --git a/cypress/package.json b/cypress/package.json new file mode 100644 index 0000000000..8549386f7e --- /dev/null +++ b/cypress/package.json @@ -0,0 +1,11 @@ +{ + "name": "@backstage/cypress-tests", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "private": true, + "dependencies": { + "cypress": "^6.4.0", + "typescript": "^4.1.3" + } +} diff --git a/cypress/src/fixtures/.gitkeep b/cypress/src/fixtures/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cypress/src/integration/catalog.ts b/cypress/src/integration/catalog.ts new file mode 100644 index 0000000000..31d39e7efd --- /dev/null +++ b/cypress/src/integration/catalog.ts @@ -0,0 +1,29 @@ +/* + * 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 'os'; + +describe('Catalog', () => { + describe('default entities', () => { + it('displays the correct amount of entities from default config', () => { + cy.loginAsGuest(); + + cy.visit('/catalog'); + + cy.contains('Owned (7)').should('be.visible'); + }); + }); +}); diff --git a/cypress/src/plugins/index.ts b/cypress/src/plugins/index.ts new file mode 100644 index 0000000000..b90c276d71 --- /dev/null +++ b/cypress/src/plugins/index.ts @@ -0,0 +1,16 @@ +/* + * 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 default () => {}; diff --git a/cypress/src/support/index.ts b/cypress/src/support/index.ts new file mode 100644 index 0000000000..e17081831e --- /dev/null +++ b/cypress/src/support/index.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ +/// + +Cypress.Commands.add('loginAsGuest', () => { + cy.visit('/', { + onLoad: (win: Window) => + win.localStorage.setItem('@backstage/core:SignInPage:provider', 'guest'), + }); +}); + +export {}; diff --git a/plugins/catalog-import/src/util/urls.ts b/cypress/src/types.d.ts similarity index 62% rename from plugins/catalog-import/src/util/urls.ts rename to cypress/src/types.d.ts index c51dc0a999..361aaba9f3 100644 --- a/plugins/catalog-import/src/util/urls.ts +++ b/cypress/src/types.d.ts @@ -13,19 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import parseGitUrl from 'git-url-parse'; - -export type UrlType = 'file' | 'tree'; - -export function urlType(url: string): UrlType { - const { filepathtype, filepath } = parseGitUrl(url); - - if (filepathtype === 'tree' || filepathtype === 'file') { - return filepathtype; - } else if (filepath?.match(/\.ya?ml$/)) { - return 'file'; +declare module 'zombie'; +declare module 'pgtools'; +declare namespace Cypress { + interface Chainable { + /** + * Login as guest + * @example cy.loginAsGuests + */ + loginAsGuest(): Chainable; } - - return 'tree'; } diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json new file mode 100644 index 0000000000..d9b4869ecd --- /dev/null +++ b/cypress/tsconfig.json @@ -0,0 +1,37 @@ +{ + "compilerOptions": { + "allowJs": true, + "declaration": true, + "declarationMap": false, + "esModuleInterop": true, + "experimentalDecorators": false, + "forceConsistentCasingInFileNames": true, + "importHelpers": false, + "incremental": true, + "isolatedModules": true, + "jsx": "react", + "lib": ["DOM", "DOM.Iterable", "ScriptHost", "ES2020", "ESNext.Promise"], + "module": "ESNext", + "moduleResolution": "node", + "noEmit": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "pretty": true, + "removeComments": false, + "resolveJsonModule": true, + "sourceMap": false, + "skipLibCheck": false, + "strict": true, + "strictBindCallApply": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, + "stripInternal": true, + "target": "ES2019", + "types": ["node", "cypress"] + } +} diff --git a/cypress/yarn.lock b/cypress/yarn.lock new file mode 100644 index 0000000000..3c737f6ebc --- /dev/null +++ b/cypress/yarn.lock @@ -0,0 +1,1409 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@cypress/listr-verbose-renderer@^0.4.1": + version "0.4.1" + resolved "https://registry.npmjs.org/@cypress/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#a77492f4b11dcc7c446a34b3e28721afd33c642a" + integrity sha1-p3SS9LEdzHxEajSz4ochr9M8ZCo= + dependencies: + chalk "^1.1.3" + cli-cursor "^1.0.2" + date-fns "^1.27.2" + figures "^1.7.0" + +"@cypress/request@^2.88.5": + version "2.88.5" + resolved "https://registry.npmjs.org/@cypress/request/-/request-2.88.5.tgz#8d7ecd17b53a849cfd5ab06d5abe7d84976375d7" + integrity sha512-TzEC1XMi1hJkywWpRfD2clreTa/Z+lOrXDCxxBTBPEcY5azdPi56A6Xw+O4tWJnaJH3iIE7G5aDXZC6JgRZLcA== + dependencies: + 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" + +"@cypress/xvfb@^1.2.4": + version "1.2.4" + resolved "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz#2daf42e8275b39f4aa53c14214e557bd14e7748a" + integrity sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q== + dependencies: + debug "^3.1.0" + lodash.once "^4.1.1" + +"@samverschueren/stream-to-observable@^0.3.0": + version "0.3.1" + resolved "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz#a21117b19ee9be70c379ec1877537ef2e1c63301" + integrity sha512-c/qwwcHyafOQuVQJj0IlBjf5yYgBI7YPJ77k4fOJYesb41jio65eaJODRUmfYKhTOFBrIZ66kgvGPlNbjuoRdQ== + dependencies: + any-observable "^0.3.0" + +"@types/sinonjs__fake-timers@^6.0.1": + version "6.0.2" + resolved "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.2.tgz#3a84cf5ec3249439015e14049bd3161419bf9eae" + integrity sha512-dIPoZ3g5gcx9zZEszaxLSVTvMReD3xxyyDnQUjA6IYDG9Ba2AV0otMPs+77sG9ojB4Qr2N2Vk5RnKeuA0X/0bg== + +"@types/sizzle@^2.3.2": + version "2.3.2" + resolved "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" + integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg== + +ajv@^6.12.3: + version "6.12.6" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-escapes@^3.0.0: + version "3.2.0" + resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" + integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +any-observable@^0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b" + integrity sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog== + +arch@^2.1.2: + version "2.2.0" + resolved "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" + integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +async@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" + integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.11.0" + resolved "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" + integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +blob-util@2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb" + integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ== + +bluebird@^3.7.2: + version "3.7.2" + resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +cachedir@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" + integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw== + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +chalk@^1.0.0, chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= + dependencies: + 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" + +chalk@^2.4.1: + version "2.4.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0, chalk@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" + integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +check-more-types@^2.24.0: + version "2.24.0" + resolved "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" + integrity sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA= + +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + +cli-cursor@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" + integrity sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc= + dependencies: + restore-cursor "^1.0.1" + +cli-cursor@^2.0.0, cli-cursor@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" + integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= + dependencies: + restore-cursor "^2.0.0" + +cli-table3@~0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee" + integrity sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ== + dependencies: + object-assign "^4.1.0" + string-width "^4.2.0" + optionalDependencies: + colors "^1.1.2" + +cli-truncate@^0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574" + integrity sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ= + dependencies: + slice-ansi "0.0.4" + string-width "^1.0.1" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colors@^1.1.2: + version "1.4.0" + resolved "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" + integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== + +common-tags@^1.8.0: + version "1.8.0" + resolved "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" + integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +concat-stream@^1.6.2: + version "1.6.2" + resolved "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +cross-spawn@^7.0.0: + version "7.0.3" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +cypress@^6.4.0: + version "6.4.0" + resolved "https://registry.npmjs.org/cypress/-/cypress-6.4.0.tgz#432c516bf4f1a0f042a6aa1f2c3a4278fa35a8b2" + integrity sha512-SrsPsZ4IBterudkoFYBvkQmXOVxclh1/+ytbzpV8AH/D2FA+s2Qy5ISsaRzOFsbQa4KZWoi3AKwREmF1HucYkg== + dependencies: + "@cypress/listr-verbose-renderer" "^0.4.1" + "@cypress/request" "^2.88.5" + "@cypress/xvfb" "^1.2.4" + "@types/sinonjs__fake-timers" "^6.0.1" + "@types/sizzle" "^2.3.2" + arch "^2.1.2" + blob-util "2.0.2" + bluebird "^3.7.2" + cachedir "^2.3.0" + chalk "^4.1.0" + check-more-types "^2.24.0" + cli-table3 "~0.6.0" + commander "^5.1.0" + common-tags "^1.8.0" + dayjs "^1.9.3" + debug "^4.1.1" + eventemitter2 "^6.4.2" + execa "^4.0.2" + executable "^4.1.1" + extract-zip "^1.7.0" + fs-extra "^9.0.1" + getos "^3.2.1" + is-ci "^2.0.0" + is-installed-globally "^0.3.2" + lazy-ass "^1.6.0" + listr "^0.14.3" + lodash "^4.17.19" + log-symbols "^4.0.0" + minimist "^1.2.5" + moment "^2.29.1" + ospath "^1.2.2" + pretty-bytes "^5.4.1" + ramda "~0.26.1" + request-progress "^3.0.0" + supports-color "^7.2.0" + tmp "~0.2.1" + untildify "^4.0.0" + url "^0.11.0" + yauzl "^2.10.0" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +date-fns@^1.27.2: + version "1.30.1" + resolved "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" + integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== + +dayjs@^1.9.3: + version "1.10.4" + resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.10.4.tgz#8e544a9b8683f61783f570980a8a80eaf54ab1e2" + integrity sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw== + +debug@^2.6.9: + version "2.6.9" + resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^3.1.0: + version "3.2.7" + resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +debug@^4.1.1: + version "4.3.1" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" + integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== + dependencies: + ms "2.1.2" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +elegant-spinner@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" + integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4= + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +eventemitter2@^6.4.2: + version "6.4.3" + resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.3.tgz#35c563619b13f3681e7eb05cbdaf50f56ba58820" + integrity sha512-t0A2msp6BzOf+QAcI6z9XMktLj52OjGQg+8SJH6v5+3uxNpWYRR3wQmfA+6xtMU9kOC59qk9licus5dYcrYkMQ== + +execa@^4.0.2: + version "4.1.0" + resolved "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" + integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== + dependencies: + 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" + +executable@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c" + integrity sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg== + dependencies: + pify "^2.2.0" + +exit-hook@^1.0.0: + version "1.1.1" + resolved "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" + integrity sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g= + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extract-zip@^1.7.0: + version "1.7.0" + resolved "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" + integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== + dependencies: + concat-stream "^1.6.2" + debug "^2.6.9" + mkdirp "^0.5.4" + yauzl "^2.10.0" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= + dependencies: + pend "~1.2.0" + +figures@^1.7.0: + version "1.7.0" + resolved "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" + integrity sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4= + dependencies: + escape-string-regexp "^1.0.5" + object-assign "^4.1.0" + +figures@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" + integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= + dependencies: + escape-string-regexp "^1.0.5" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +fs-extra@^9.0.1: + version "9.1.0" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +get-stream@^5.0.0: + version "5.2.0" + resolved "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +getos@^3.2.1: + version "3.2.1" + resolved "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5" + integrity sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q== + dependencies: + async "^3.2.0" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +glob@^7.1.3: + version "7.1.6" + resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + 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" + +global-dirs@^2.0.1: + version "2.1.0" + resolved "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz#e9046a49c806ff04d6c1825e196c8f0091e8df4d" + integrity sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ== + dependencies: + ini "1.3.7" + +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.5" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.5.tgz#bc18864a6c9fc7b303f2e2abdb9155ad178fbe29" + integrity sha512-kBBSQbz2K0Nyn+31j/w36fUfxkBW9/gfwRWdUY1ULReH3iokVJgddZAFcD1D0xlgTmFxJCbUkUclAlc6/IDJkw== + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.3: + version "5.1.5" + resolved "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" + integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== + dependencies: + ajv "^6.12.3" + har-schema "^2.0.0" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= + dependencies: + ansi-regex "^2.0.0" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +human-signals@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" + integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== + +indent-string@^3.0.0: + version "3.2.0" + resolved "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" + integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok= + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@1.3.7: + version "1.3.7" + resolved "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84" + integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ== + +is-ci@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" + integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== + dependencies: + ci-info "^2.0.0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-installed-globally@^0.3.2: + version "0.3.2" + resolved "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141" + integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g== + dependencies: + global-dirs "^2.0.1" + is-path-inside "^3.0.1" + +is-observable@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz#b3e986c8f44de950867cab5403f5a3465005975e" + integrity sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA== + dependencies: + symbol-observable "^1.1.0" + +is-path-inside@^3.0.1: + version "3.0.2" + resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017" + integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg== + +is-promise@^2.1.0: + version "2.2.2" + resolved "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" + integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + +is-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" + integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +lazy-ass@^1.6.0: + version "1.6.0" + resolved "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" + integrity sha1-eZllXoZGwX8In90YfRUNMyTVRRM= + +listr-silent-renderer@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e" + integrity sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4= + +listr-update-renderer@^0.5.0: + version "0.5.0" + resolved "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz#4ea8368548a7b8aecb7e06d8c95cb45ae2ede6a2" + integrity sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA== + dependencies: + chalk "^1.1.3" + cli-truncate "^0.2.1" + elegant-spinner "^1.0.1" + figures "^1.7.0" + indent-string "^3.0.0" + log-symbols "^1.0.2" + log-update "^2.3.0" + strip-ansi "^3.0.1" + +listr-verbose-renderer@^0.5.0: + version "0.5.0" + resolved "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz#f1132167535ea4c1261102b9f28dac7cba1e03db" + integrity sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw== + dependencies: + chalk "^2.4.1" + cli-cursor "^2.1.0" + date-fns "^1.27.2" + figures "^2.0.0" + +listr@^0.14.3: + version "0.14.3" + resolved "https://registry.npmjs.org/listr/-/listr-0.14.3.tgz#2fea909604e434be464c50bddba0d496928fa586" + integrity sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA== + dependencies: + "@samverschueren/stream-to-observable" "^0.3.0" + is-observable "^1.1.0" + is-promise "^2.1.0" + is-stream "^1.1.0" + listr-silent-renderer "^1.1.1" + listr-update-renderer "^0.5.0" + listr-verbose-renderer "^0.5.0" + p-map "^2.0.0" + rxjs "^6.3.3" + +lodash.once@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= + +lodash@^4.17.19: + version "4.17.20" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" + integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== + +log-symbols@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" + integrity sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg= + dependencies: + chalk "^1.0.0" + +log-symbols@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" + integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== + dependencies: + chalk "^4.0.0" + +log-update@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz#88328fd7d1ce7938b29283746f0b1bc126b24708" + integrity sha1-iDKP19HOeTiykoN0bwsbwSayRwg= + dependencies: + ansi-escapes "^3.0.0" + cli-cursor "^2.0.0" + wrap-ansi "^3.0.1" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +mime-db@1.45.0: + version "1.45.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" + integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.28" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz#1160c4757eab2c5363888e005273ecf79d2a0ecd" + integrity sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ== + dependencies: + mime-db "1.45.0" + +mimic-fn@^1.0.0: + version "1.2.0" + resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" + integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +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== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +mkdirp@^0.5.4: + version "0.5.5" + resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + +moment@^2.29.1: + version "2.29.1" + resolved "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" + integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@^2.1.1: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +npm-run-path@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +onetime@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" + integrity sha1-ofeDj4MUxRbwXs78vEzP4EtO14k= + +onetime@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" + integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ= + dependencies: + mimic-fn "^1.0.0" + +onetime@^5.1.0: + version "5.1.2" + resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +ospath@^1.2.2: + version "1.2.2" + resolved "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b" + integrity sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs= + +p-map@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" + integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +pify@^2.2.0: + version "2.3.0" + resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + +pretty-bytes@^5.4.1: + version "5.5.0" + resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.5.0.tgz#0cecda50a74a941589498011cf23275aa82b339e" + integrity sha512-p+T744ZyjjiaFlMUZZv6YPC5JrkNj8maRmPaQCWFJFplUAzpIUTRaTcS+7wmZtUoFXHtESJb23ISliaWyz3SHA== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +psl@^1.1.28: + version "1.8.0" + resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" + integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= + +ramda@~0.26.1: + version "0.26.1" + resolved "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06" + integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ== + +readable-stream@^2.2.2: + 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== + dependencies: + 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" + +request-progress@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe" + integrity sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4= + dependencies: + throttleit "^1.0.0" + +restore-cursor@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" + integrity sha1-NGYfRohjJ/7SmRR5FSJS35LapUE= + dependencies: + exit-hook "^1.0.0" + onetime "^1.0.0" + +restore-cursor@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" + integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368= + dependencies: + onetime "^2.0.0" + signal-exit "^3.0.2" + +rimraf@^3.0.0: + version "3.0.2" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rxjs@^6.3.3: + version "6.6.3" + resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552" + integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ== + dependencies: + tslib "^1.9.0" + +safe-buffer@^5.0.1, safe-buffer@^5.1.2: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +signal-exit@^3.0.2: + version "3.0.3" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" + integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== + +slice-ansi@0.0.4: + version "0.0.4" + resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" + integrity sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU= + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + dependencies: + 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" + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string-width@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0, supports-color@^7.2.0: + version "7.2.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +symbol-observable@^1.1.0: + version "1.2.0" + resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" + integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== + +throttleit@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" + integrity sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw= + +tmp@~0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +tslib@^1.9.0: + version "1.14.1" + resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= + +typescript@^4.1.3: + version "4.1.3" + resolved "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7" + integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg== + +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + +untildify@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" + integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +url@^0.11.0: + version "0.11.0" + resolved "https://registry.npmjs.org/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +uuid@^3.3.2: + version "3.4.0" + resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wrap-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz#288a04d87eda5c286e060dfe8f135ce8d007f8ba" + integrity sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo= + dependencies: + string-width "^2.1.1" + strip-ansi "^4.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +yauzl@^2.10.0: + version "2.10.0" + resolved "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" diff --git a/docs/api/utility-apis.md b/docs/api/utility-apis.md index ff322c136f..5b5656f1b1 100644 --- a/docs/api/utility-apis.md +++ b/docs/api/utility-apis.md @@ -119,7 +119,7 @@ Plugins supply their APIs through the `apis` option of `createPlugin`, for example: ```ts -export const plugin = createPlugin({ +export const techdocsPlugin = createPlugin({ id: 'techdocs', apis: [ createApiFactory({ diff --git a/docs/assets/search/architecture.drawio.svg b/docs/assets/search/architecture.drawio.svg index af04ab63e8..a624a375e1 100644 --- a/docs/assets/search/architecture.drawio.svg +++ b/docs/assets/search/architecture.drawio.svg @@ -1,13 +1,13 @@ - + - + - + @@ -66,24 +66,26 @@ - + -
-
+
+
- @backstage/plugin-search-backend + @backstage/ +
+ plugin-search-backend
- - @backstage/plugin-search-backend + + @backstage/... - + @@ -138,9 +140,9 @@ - - - + + + @@ -266,7 +268,7 @@ -
+
Pass Search @@ -280,7 +282,7 @@
- + Pass Search... @@ -352,8 +354,8 @@
- - Gather Documents + + Gather Documents From Plugins
@@ -366,7 +368,7 @@ - + @@ -375,16 +377,14 @@
- Collate Documents -
- Or Metadata + Register Document / Metadata Collation Handler(s)
- Collate Docum... + Register Docu... @@ -427,11 +427,11 @@
- + -
+
@@ -441,7 +441,7 @@
- + Index Processi... @@ -529,10 +529,29 @@ + + + + +
+
+
+ @backstage/ +
+ plugin-search-indexer +
+
+
+
+ + @backstage/... + +
+
- + Viewer does not support full SVG 1.1 diff --git a/docs/auth/auth-backend-classes.md b/docs/auth/auth-backend-classes.md index 424869196d..a99ca119f0 100644 --- a/docs/auth/auth-backend-classes.md +++ b/docs/auth/auth-backend-classes.md @@ -76,14 +76,16 @@ by also providing the `cert` configuration. ### Configuration -Each authentication provider (except SAML) needs five parameters: an OAuth -client ID, a client secret, an authorization endpoint, a token endpoint, and an -app origin. The app origin is the URL at which the frontend of the application -is hosted, and it is read from the `app.baseUrl` config. This is required -because the application opens a popup window to perform the authentication, and -once the flow is completed, the popup window sends a `postMessage` to the -frontend application to indicate the result of the operation. Also this URL is -used to verify that authentication requests are coming from only this endpoint. +Each authentication provider (except SAML) needs six parameters: an OAuth client +ID, a client secret, an authorization endpoint, a token endpoint, an optional +list of scopes (as a string separated by spaces) that may be required by the +OAuth2 Server to enable end-user sign-on, and an app origin. The app origin is +the URL at which the frontend of the application is hosted, and it is read from +the `app.baseUrl` config. This is required because the application opens a popup +window to perform the authentication, and once the flow is completed, the popup +window sends a `postMessage` to the frontend application to indicate the result +of the operation. Also this URL is used to verify that authentication requests +are coming from only this endpoint. These values are configured via the `app-config.yaml` present in the root of your app folder. @@ -109,6 +111,18 @@ auth: development: clientId: $env: + oauth2: + development: + clientId: + $env: AUTH_OAUTH2_CLIENT_ID + clientSecret: + $env: AUTH_OAUTH2_CLIENT_SECRET + authorizationUrl: + $env: AUTH_OAUTH2_AUTH_URL + tokenUrl: + $env: AUTH_OAUTH2_TOKEN_URL + scope: + $env: AUTH_OAUTH2_SCOPE saml: entryPoint: $env: AUTH_SAML_ENTRY_POINT diff --git a/docs/features/search/architecture.md b/docs/features/search/architecture.md index 3075719b9d..118afa6710 100644 --- a/docs/features/search/architecture.md +++ b/docs/features/search/architecture.md @@ -6,9 +6,9 @@ description: Documentation on Search Architecture # Search Architecture -> _This is a proposed architecture which has not been implemented yet. We are -> still looking for feedback to improve the architecture to fit your use-case, -> see [this open issue](https://github.com/backstage/backstage/issues/4078)._ +> _This is a proposed architecture which has not been implemented yet. Find our +> milestones to follow our progress on the +> [Search Roadmap](./README.md#project-roadmap)._ Below you can explore the Search Architecture. Our aim with this architecture is to support a wide variety of search engines, while providing a simple developer @@ -20,7 +20,10 @@ Backstage end-users. At a base-level, we want to support the following: - We aim to enable the capability to search across the entire Backstage - ecosystem by decoupling search from content management. + ecosystem including, but not limited to, entities in the software catalog. + Searchable content won't be required to relate directly to the software + catalog, but by convention, we may encourage loose relationships using + well-known field names or attributes. - We aim to enable the capability to deploy Backstage using any search engine, by providing an integration and translation layer between the core search plugin and search engine specific logic that can be extended for different @@ -29,11 +32,17 @@ At a base-level, we want to support the following: More advanced use-cases we hope to support with this architecture include: -- It should be easy for any plugin to expose new content to search. (e.g. entity - metadata, documentation from TechDocs) -- It should be easy for any plugin to append relevant metadata to existing +- It should be possible for any plugin to expose new content to search. (e.g. + entity metadata, documentation from TechDocs) +- It should be possible for any plugin to append relevant metadata to existing content in search. (e.g. location (path) for TechDocs page) -- It should be easy to refine search queries (e.g. ranking, scoring, etc.) -- It should be easy to customize the search UI -- It should be easy to add search functionality to any Backstage plugin or +- It should be possible to refine search queries (e.g. ranking, scoring, etc.) +- It should be possible to customize the search UI +- It should be possible to add search functionality to any Backstage plugin or deployment + +Architecture non-goals: + +- At this time, we do not intend to directly support event-driven or incremental + index management. Instead, we'll be focused on scheduled, bulk index + management. diff --git a/docs/features/techdocs/configuring-ci-cd.md b/docs/features/techdocs/configuring-ci-cd.md index 5841a02fa8..a9844ef528 100644 --- a/docs/features/techdocs/configuring-ci-cd.md +++ b/docs/features/techdocs/configuring-ci-cd.md @@ -27,11 +27,15 @@ REPOSITORY_URL='https://github.com/org/repo' git clone $REPOSITORY_URL cd repo +# Install @techdocs/cli, mkdocs and mkdocs plugins +npm install -g @techdocs/cli +pip install mkdocs-techdocs-core==0.* + # Generate -npx @techdocs/cli generate +techdocs-cli generate --no-docker # Publish -npx @techdocs/cli publish --publisher-type awsS3 --storage-name --entity +techdocs-cli publish --publisher-type awsS3 --storage-name --entity ``` That's it! @@ -40,14 +44,16 @@ Take a look at [`techdocs-cli` README](https://github.com/backstage/techdocs-cli) for the complete command reference, details, and options. -## 1. Setup a workflow +## Steps + +### 1. Setup a workflow The TechDocs workflow should trigger on CI when any changes are made in the repository containing the documentation files. You can be specific and configure the workflow to be triggered only when files inside the `docs/` directory or `mkdocs.yml` are changed. -## 2. Prepare step +### 2. Prepare step The first step on the CI is to clone your documentation source repository in a working directory. This is almost always the first step in most CI workflows. @@ -62,7 +68,7 @@ step. Eventually we are trying to do a `git clone `. -## 3. Generate step +### 3. Generate step Install [`npx`](https://www.npmjs.com/package/npx) to use it for running `techdocs-cli`. Or you can install using `npm install -g @techdocs/cli`. @@ -78,7 +84,7 @@ npx @techdocs/cli generate --no-docker --source-dir PATH_TO_REPO --output-dir ./ `PATH_TO_REPO` should be the location in the file path where the prepare step above clones the repository. -## 4. Publish step +### 4. Publish step Depending on your cloud storage provider (AWS, Google Cloud, or Azure), set the necessary authentication environment variables. @@ -96,3 +102,68 @@ npx @techdocs/cli publish --publisher-type --storage-name Defined at -[packages/core-api/src/apis/definitions/AlertApi.ts:19](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/AlertApi.ts#L19). +[packages/core-api/src/apis/definitions/AlertApi.ts:19](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/AlertApi.ts#L19). Referenced by: [post](#post), [alert\$](#alert). @@ -67,7 +67,7 @@ export type Observable<T> = { Defined at -[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L53). +[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L53). Referenced by: [alert\$](#alert). @@ -86,7 +86,7 @@ export type Observer<T> = { Defined at -[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L24). +[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L24). Referenced by: [Observable](#observable). @@ -109,6 +109,6 @@ export type Subscription = { Defined at -[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L33). +[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L33). Referenced by: [Observable](#observable). diff --git a/docs/reference/utility-apis/AppThemeApi.md b/docs/reference/utility-apis/AppThemeApi.md index 0f5c095825..a662b4cb70 100644 --- a/docs/reference/utility-apis/AppThemeApi.md +++ b/docs/reference/utility-apis/AppThemeApi.md @@ -1,7 +1,7 @@ # AppThemeApi The AppThemeApi type is defined at -[packages/core-api/src/apis/definitions/AppThemeApi.ts:56](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/AppThemeApi.ts#L56). +[packages/core-api/src/apis/definitions/AppThemeApi.ts:56](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/AppThemeApi.ts#L56). The following Utility API implements this type: [appThemeApiRef](./README.md#apptheme) @@ -81,7 +81,7 @@ export type AppTheme = { Defined at -[packages/core-api/src/apis/definitions/AppThemeApi.ts:25](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/AppThemeApi.ts#L25). +[packages/core-api/src/apis/definitions/AppThemeApi.ts:25](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/AppThemeApi.ts#L25). Referenced by: [getInstalledThemes](#getinstalledthemes). @@ -92,7 +92,7 @@ export type BackstagePalette = Palette & Palette Defined at -[packages/theme/src/types.ts:74](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/theme/src/types.ts#L74). +[packages/theme/src/types.ts:74](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/theme/src/types.ts#L74). Referenced by: [BackstageTheme](#backstagetheme). @@ -107,7 +107,7 @@ export interface BackstageTheme extends Theme { Defined at -[packages/theme/src/types.ts:81](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/theme/src/types.ts#L81). +[packages/theme/src/types.ts:81](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/theme/src/types.ts#L81). Referenced by: [AppTheme](#apptheme). @@ -136,7 +136,7 @@ export type Observable<T> = { Defined at -[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L53). +[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L53). Referenced by: [activeThemeId\$](#activethemeid). @@ -155,7 +155,7 @@ export type Observer<T> = { Defined at -[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L24). +[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L24). Referenced by: [Observable](#observable). @@ -170,7 +170,7 @@ export type PageTheme = { Defined at -[packages/theme/src/types.ts:103](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/theme/src/types.ts#L103). +[packages/theme/src/types.ts:103](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/theme/src/types.ts#L103). Referenced by: [BackstageTheme](#backstagetheme). @@ -183,7 +183,7 @@ export type PageThemeSelector = { Defined at -[packages/theme/src/types.ts:77](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/theme/src/types.ts#L77). +[packages/theme/src/types.ts:77](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/theme/src/types.ts#L77). Referenced by: [BackstageTheme](#backstagetheme). @@ -243,7 +243,7 @@ type PaletteAdditions = { Defined at -[packages/theme/src/types.ts:23](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/theme/src/types.ts#L23). +[packages/theme/src/types.ts:23](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/theme/src/types.ts#L23). Referenced by: [BackstagePalette](#backstagepalette). @@ -266,6 +266,6 @@ export type Subscription = { Defined at -[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L33). +[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L33). Referenced by: [Observable](#observable). diff --git a/docs/reference/utility-apis/BackstageIdentityApi.md b/docs/reference/utility-apis/BackstageIdentityApi.md index 69fe5d26ba..80a40d427e 100644 --- a/docs/reference/utility-apis/BackstageIdentityApi.md +++ b/docs/reference/utility-apis/BackstageIdentityApi.md @@ -1,7 +1,7 @@ # BackstageIdentityApi The BackstageIdentityApi type is defined at -[packages/core-api/src/apis/definitions/auth.ts:134](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L134). +[packages/core-api/src/apis/definitions/auth.ts:134](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L134). The following Utility APIs implement this type: @@ -17,8 +17,12 @@ The following Utility APIs implement this type: - [oauth2ApiRef](./README.md#oauth2) +- [oidcAuthApiRef](./README.md#oidcauth) + - [oktaAuthApiRef](./README.md#oktaauth) +- [oneloginAuthApiRef](./README.md#oneloginauth) + - [samlAuthApiRef](./README.md#samlauth) ## Members @@ -70,7 +74,7 @@ export type AuthRequestOptions = { Defined at -[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L40). +[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L40). Referenced by: [getBackstageIdentity](#getbackstageidentity). @@ -79,7 +83,7 @@ Referenced by: [getBackstageIdentity](#getbackstageidentity).
 export type BackstageIdentity = {
   /**
-   * The Backstage user ID.
+   * The backstage user ID.
    */
   id: string;
 
@@ -91,6 +95,6 @@ export type BackstageIdentity = {
 
Defined at -[packages/core-api/src/apis/definitions/auth.ts:147](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L147). +[packages/core-api/src/apis/definitions/auth.ts:147](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L147). Referenced by: [getBackstageIdentity](#getbackstageidentity). diff --git a/docs/reference/utility-apis/Config.md b/docs/reference/utility-apis/Config.md index 2ae047a700..c5b20610e7 100644 --- a/docs/reference/utility-apis/Config.md +++ b/docs/reference/utility-apis/Config.md @@ -1,7 +1,7 @@ # Config The Config type is defined at -[packages/config/src/types.ts:32](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/config/src/types.ts#L32). +[packages/config/src/types.ts:32](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/config/src/types.ts#L32). The following Utility API implements this type: [configApiRef](./README.md#config) @@ -140,7 +140,7 @@ export type Config = { Defined at -[packages/config/src/types.ts:32](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/config/src/types.ts#L32). +[packages/config/src/types.ts:32](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/config/src/types.ts#L32). Referenced by: [getConfig](#getconfig), [getOptionalConfig](#getoptionalconfig), [getConfigArray](#getconfigarray), @@ -153,7 +153,7 @@ export type JsonArray =
JsonValue[] Defined at -[packages/config/src/types.ts:18](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/config/src/types.ts#L18). +[packages/config/src/types.ts:18](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/config/src/types.ts#L18). Referenced by: [JsonValue](#jsonvalue). @@ -164,7 +164,7 @@ export type JsonObject = { [key in string]?: JsonValue Defined at -[packages/config/src/types.ts:17](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/config/src/types.ts#L17). +[packages/config/src/types.ts:17](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/config/src/types.ts#L17). Referenced by: [JsonValue](#jsonvalue). @@ -181,7 +181,7 @@ export type JsonValue = Defined at -[packages/config/src/types.ts:19](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/config/src/types.ts#L19). +[packages/config/src/types.ts:19](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/config/src/types.ts#L19). Referenced by: [get](#get), [getOptional](#getoptional), [JsonObject](#jsonobject), [JsonArray](#jsonarray), [Config](#config). diff --git a/docs/reference/utility-apis/DiscoveryApi.md b/docs/reference/utility-apis/DiscoveryApi.md index ac278fdd97..0d888c1daa 100644 --- a/docs/reference/utility-apis/DiscoveryApi.md +++ b/docs/reference/utility-apis/DiscoveryApi.md @@ -1,7 +1,7 @@ # DiscoveryApi The DiscoveryApi type is defined at -[packages/core-api/src/apis/definitions/DiscoveryApi.ts:30](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/DiscoveryApi.ts#L30). +[packages/core-api/src/apis/definitions/DiscoveryApi.ts:30](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/DiscoveryApi.ts#L30). The following Utility API implements this type: [discoveryApiRef](./README.md#discovery) @@ -12,7 +12,7 @@ The following Utility API implements this type: Returns the HTTP base backend URL for a given plugin, without a trailing slash. -This method must always be called just before making a request. as opposed to +This method must always be called just before making a request, as opposed to fetching the URL when constructing an API client. That is to ensure that more flexible routing patterns can be supported. diff --git a/docs/reference/utility-apis/ErrorApi.md b/docs/reference/utility-apis/ErrorApi.md index 9bba0c76c6..1aaecdb47e 100644 --- a/docs/reference/utility-apis/ErrorApi.md +++ b/docs/reference/utility-apis/ErrorApi.md @@ -1,7 +1,7 @@ # ErrorApi The ErrorApi type is defined at -[packages/core-api/src/apis/definitions/ErrorApi.ts:53](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/ErrorApi.ts#L53). +[packages/core-api/src/apis/definitions/ErrorApi.ts:53](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/ErrorApi.ts#L53). The following Utility API implements this type: [errorApiRef](./README.md#error) @@ -41,7 +41,7 @@ type Error = { Defined at -[packages/core-api/src/apis/definitions/ErrorApi.ts:24](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/ErrorApi.ts#L24). +[packages/core-api/src/apis/definitions/ErrorApi.ts:24](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/ErrorApi.ts#L24). Referenced by: [post](#post), [error\$](#error). @@ -58,7 +58,7 @@ export type ErrorContext = { Defined at -[packages/core-api/src/apis/definitions/ErrorApi.ts:33](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/ErrorApi.ts#L33). +[packages/core-api/src/apis/definitions/ErrorApi.ts:33](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/ErrorApi.ts#L33). Referenced by: [post](#post), [error\$](#error). @@ -87,7 +87,7 @@ export type Observable<T> = { Defined at -[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L53). +[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L53). Referenced by: [error\$](#error). @@ -106,7 +106,7 @@ export type Observer<T> = { Defined at -[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L24). +[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L24). Referenced by: [Observable](#observable). @@ -129,6 +129,6 @@ export type Subscription = { Defined at -[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L33). +[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L33). Referenced by: [Observable](#observable). diff --git a/docs/reference/utility-apis/FeatureFlagsApi.md b/docs/reference/utility-apis/FeatureFlagsApi.md index 529d2ac5dc..1e8979c02e 100644 --- a/docs/reference/utility-apis/FeatureFlagsApi.md +++ b/docs/reference/utility-apis/FeatureFlagsApi.md @@ -1,7 +1,7 @@ # FeatureFlagsApi The FeatureFlagsApi type is defined at -[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:60](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L60). +[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:60](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L60). The following Utility API implements this type: [featureFlagsApiRef](./README.md#featureflags) @@ -68,7 +68,7 @@ export type FeatureFlag = { Defined at -[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:31](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L31). +[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:31](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L31). Referenced by: [registerFlag](#registerflag), [getRegisteredFlags](#getregisteredflags). @@ -83,7 +83,7 @@ export enum FeatureFlagState { Defined at -[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:36](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L36). +[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:36](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L36). Referenced by: [FeatureFlagsSaveOptions](#featureflagssaveoptions). @@ -108,6 +108,6 @@ export type FeatureFlagsSaveOptions = { Defined at -[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:44](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L44). +[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:44](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L44). Referenced by: [save](#save). diff --git a/docs/reference/utility-apis/IdentityApi.md b/docs/reference/utility-apis/IdentityApi.md index a88aaf0b8a..aa2a4dd9a0 100644 --- a/docs/reference/utility-apis/IdentityApi.md +++ b/docs/reference/utility-apis/IdentityApi.md @@ -1,7 +1,7 @@ # IdentityApi The IdentityApi type is defined at -[packages/core-api/src/apis/definitions/IdentityApi.ts:22](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/IdentityApi.ts#L22). +[packages/core-api/src/apis/definitions/IdentityApi.ts:22](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/IdentityApi.ts#L22). The following Utility API implements this type: [identityApiRef](./README.md#identity) @@ -76,6 +76,6 @@ export type ProfileInfo = { Defined at -[packages/core-api/src/apis/definitions/auth.ts:162](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L162). +[packages/core-api/src/apis/definitions/auth.ts:162](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L162). Referenced by: [getProfile](#getprofile). diff --git a/docs/reference/utility-apis/OAuthApi.md b/docs/reference/utility-apis/OAuthApi.md index 79b55812ef..9af6b3bb53 100644 --- a/docs/reference/utility-apis/OAuthApi.md +++ b/docs/reference/utility-apis/OAuthApi.md @@ -1,7 +1,7 @@ # OAuthApi The OAuthApi type is defined at -[packages/core-api/src/apis/definitions/auth.ts:67](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L67). +[packages/core-api/src/apis/definitions/auth.ts:67](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L67). The following Utility APIs implement this type: @@ -15,8 +15,12 @@ The following Utility APIs implement this type: - [oauth2ApiRef](./README.md#oauth2) +- [oidcAuthApiRef](./README.md#oidcauth) + - [oktaAuthApiRef](./README.md#oktaauth) +- [oneloginAuthApiRef](./README.md#oneloginauth) + ## Members ### getAccessToken() @@ -82,7 +86,7 @@ export type AuthRequestOptions = { Defined at -[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L40). +[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L40). Referenced by: [getAccessToken](#getaccesstoken). @@ -108,6 +112,6 @@ export type OAuthScope = string | string[] Defined at -[packages/core-api/src/apis/definitions/auth.ts:38](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L38). +[packages/core-api/src/apis/definitions/auth.ts:38](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L38). Referenced by: [getAccessToken](#getaccesstoken). diff --git a/docs/reference/utility-apis/OAuthRequestApi.md b/docs/reference/utility-apis/OAuthRequestApi.md index f1c2311ce9..5f521cf288 100644 --- a/docs/reference/utility-apis/OAuthRequestApi.md +++ b/docs/reference/utility-apis/OAuthRequestApi.md @@ -1,7 +1,7 @@ # OAuthRequestApi The OAuthRequestApi type is defined at -[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:99](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L99). +[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:99](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L99). The following Utility API implements this type: [oauthRequestApiRef](./README.md#oauthrequest) @@ -73,7 +73,7 @@ export type AuthProvider = { Defined at -[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:27](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L27). +[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:27](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L27). Referenced by: [AuthRequesterOptions](#authrequesteroptions), [PendingAuthRequest](#pendingauthrequest). @@ -97,7 +97,7 @@ export type AuthRequester<AuthResponse> = ( Defined at -[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:66](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L66). +[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:66](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L66). Referenced by: [createAuthRequester](#createauthrequester). @@ -122,7 +122,7 @@ export type AuthRequesterOptions<AuthResponse> = { Defined at -[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:43](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L43). +[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:43](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L43). Referenced by: [createAuthRequester](#createauthrequester). @@ -151,7 +151,7 @@ export type Observable<T> = { Defined at -[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L53). +[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L53). Referenced by: [authRequest\$](#authrequest). @@ -170,7 +170,7 @@ export type Observer<T> = { Defined at -[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L24). +[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L24). Referenced by: [Observable](#observable). @@ -205,7 +205,7 @@ export type PendingAuthRequest = { Defined at -[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:77](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L77). +[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:77](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L77). Referenced by: [authRequest\$](#authrequest). @@ -228,6 +228,6 @@ export type Subscription = { Defined at -[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L33). +[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L33). Referenced by: [Observable](#observable). diff --git a/docs/reference/utility-apis/OpenIdConnectApi.md b/docs/reference/utility-apis/OpenIdConnectApi.md index eaea530708..6d05af6189 100644 --- a/docs/reference/utility-apis/OpenIdConnectApi.md +++ b/docs/reference/utility-apis/OpenIdConnectApi.md @@ -1,7 +1,7 @@ # OpenIdConnectApi The OpenIdConnectApi type is defined at -[packages/core-api/src/apis/definitions/auth.ts:99](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L99). +[packages/core-api/src/apis/definitions/auth.ts:99](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L99). The following Utility APIs implement this type: @@ -13,8 +13,12 @@ The following Utility APIs implement this type: - [oauth2ApiRef](./README.md#oauth2) +- [oidcAuthApiRef](./README.md#oidcauth) + - [oktaAuthApiRef](./README.md#oktaauth) +- [oneloginAuthApiRef](./README.md#oneloginauth) + ## Members ### getIdToken() @@ -66,6 +70,6 @@ export type AuthRequestOptions = { Defined at -[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L40). +[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L40). Referenced by: [getIdToken](#getidtoken). diff --git a/docs/reference/utility-apis/ProfileInfoApi.md b/docs/reference/utility-apis/ProfileInfoApi.md index 76d04045e8..1a2f94d031 100644 --- a/docs/reference/utility-apis/ProfileInfoApi.md +++ b/docs/reference/utility-apis/ProfileInfoApi.md @@ -1,7 +1,7 @@ # ProfileInfoApi The ProfileInfoApi type is defined at -[packages/core-api/src/apis/definitions/auth.ts:117](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L117). +[packages/core-api/src/apis/definitions/auth.ts:117](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L117). The following Utility APIs implement this type: @@ -17,8 +17,12 @@ The following Utility APIs implement this type: - [oauth2ApiRef](./README.md#oauth2) +- [oidcAuthApiRef](./README.md#oidcauth) + - [oktaAuthApiRef](./README.md#oktaauth) +- [oneloginAuthApiRef](./README.md#oneloginauth) + - [samlAuthApiRef](./README.md#samlauth) ## Members @@ -67,7 +71,7 @@ export type AuthRequestOptions = { Defined at -[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L40). +[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L40). Referenced by: [getProfile](#getprofile). @@ -95,6 +99,6 @@ export type ProfileInfo = { Defined at -[packages/core-api/src/apis/definitions/auth.ts:162](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L162). +[packages/core-api/src/apis/definitions/auth.ts:162](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L162). Referenced by: [getProfile](#getprofile). diff --git a/docs/reference/utility-apis/README.md b/docs/reference/utility-apis/README.md index 1ef5b6197e..aefbb4b925 100644 --- a/docs/reference/utility-apis/README.md +++ b/docs/reference/utility-apis/README.md @@ -12,7 +12,7 @@ Used to report alerts and forward them to the app Implemented type: [AlertApi](./AlertApi.md) ApiRef: -[alertApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/AlertApi.ts#L41) +[alertApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/AlertApi.ts#L41) ### appTheme @@ -21,7 +21,7 @@ API Used to configure the app theme, and enumerate options Implemented type: [AppThemeApi](./AppThemeApi.md) ApiRef: -[appThemeApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/AppThemeApi.ts#L80) +[appThemeApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/AppThemeApi.ts#L80) ### auth0Auth @@ -32,7 +32,7 @@ Implemented types: [OpenIdConnectApi](./OpenIdConnectApi.md), [BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) ApiRef: -[auth0AuthApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L275) +[auth0AuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L275) ### config @@ -41,7 +41,7 @@ Used to access runtime configuration Implemented type: [Config](./Config.md) ApiRef: -[configApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/ConfigApi.ts#L22) +[configApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/ConfigApi.ts#L25) ### discovery @@ -50,7 +50,7 @@ Provides service discovery of backend plugins Implemented type: [DiscoveryApi](./DiscoveryApi.md) ApiRef: -[discoveryApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/DiscoveryApi.ts#L44) +[discoveryApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/DiscoveryApi.ts#L44) ### error @@ -59,7 +59,7 @@ Used to report errors and forward them to the app Implemented type: [ErrorApi](./ErrorApi.md) ApiRef: -[errorApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/ErrorApi.ts#L65) +[errorApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/ErrorApi.ts#L65) ### featureFlags @@ -68,7 +68,7 @@ Used to toggle functionality in features across Backstage Implemented type: [FeatureFlagsApi](./FeatureFlagsApi.md) ApiRef: -[featureFlagsApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L83) +[featureFlagsApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L83) ### githubAuth @@ -79,7 +79,7 @@ Implemented types: [OAuthApi](./OAuthApi.md), [BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) ApiRef: -[githubAuthApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L232) +[githubAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L232) ### gitlabAuth @@ -90,7 +90,7 @@ Implemented types: [OAuthApi](./OAuthApi.md), [BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) ApiRef: -[gitlabAuthApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L262) +[gitlabAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L262) ### googleAuth @@ -102,7 +102,7 @@ Implemented types: [OAuthApi](./OAuthApi.md), [BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) ApiRef: -[googleAuthApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L215) +[googleAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L215) ### identity @@ -111,7 +111,7 @@ Provides access to the identity of the signed in user Implemented type: [IdentityApi](./IdentityApi.md) ApiRef: -[identityApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/IdentityApi.ts#L54) +[identityApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/IdentityApi.ts#L53) ### microsoftAuth @@ -123,7 +123,7 @@ Implemented types: [OAuthApi](./OAuthApi.md), [BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) ApiRef: -[microsoftAuthApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L289) +[microsoftAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L289) ### oauth2 @@ -135,7 +135,7 @@ Implemented types: [OAuthApi](./OAuthApi.md), [BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) ApiRef: -[oauth2ApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L303) +[oauth2ApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L303) ### oauthRequest @@ -144,7 +144,19 @@ An API for implementing unified OAuth flows in Backstage Implemented type: [OAuthRequestApi](./OAuthRequestApi.md) ApiRef: -[oauthRequestApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L130) +[oauthRequestApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L130) + +### oidcAuth + +Example of how to use oidc custom provider + +Implemented types: [OAuthApi](./OAuthApi.md), +[OpenIdConnectApi](./OpenIdConnectApi.md), +[ProfileInfoApi](./ProfileInfoApi.md), +[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) + +ApiRef: +[oidcAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L317) ### oktaAuth @@ -156,7 +168,19 @@ Implemented types: [OAuthApi](./OAuthApi.md), [BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) ApiRef: -[oktaAuthApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L245) +[oktaAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L245) + +### oneloginAuth + +Provides authentication towards OneLogin APIs and identities + +Implemented types: [OAuthApi](./OAuthApi.md), +[OpenIdConnectApi](./OpenIdConnectApi.md), +[ProfileInfoApi](./ProfileInfoApi.md), +[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) + +ApiRef: +[oneloginAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L338) ### samlAuth @@ -166,7 +190,7 @@ Implemented types: [ProfileInfoApi](./ProfileInfoApi.md), [BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md) ApiRef: -[samlAuthApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L317) +[samlAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L331) ### storage @@ -175,4 +199,4 @@ Provides the ability to store data which is unique to the user Implemented type: [StorageApi](./StorageApi.md) ApiRef: -[storageApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/StorageApi.ts#L68) +[storageApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/StorageApi.ts#L68) diff --git a/docs/reference/utility-apis/SessionApi.md b/docs/reference/utility-apis/SessionApi.md index 7d271558d6..4b584d92c8 100644 --- a/docs/reference/utility-apis/SessionApi.md +++ b/docs/reference/utility-apis/SessionApi.md @@ -1,7 +1,7 @@ # SessionApi The SessionApi type is defined at -[packages/core-api/src/apis/definitions/auth.ts:190](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L190). +[packages/core-api/src/apis/definitions/auth.ts:190](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L190). The following Utility APIs implement this type: @@ -17,8 +17,12 @@ The following Utility APIs implement this type: - [oauth2ApiRef](./README.md#oauth2) +- [oidcAuthApiRef](./README.md#oidcauth) + - [oktaAuthApiRef](./README.md#oktaauth) +- [oneloginAuthApiRef](./README.md#oneloginauth) + - [samlAuthApiRef](./README.md#samlauth) ## Members @@ -77,7 +81,7 @@ export type Observable<T> = { Defined at -[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L53). +[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L53). Referenced by: [sessionState\$](#sessionstate). @@ -96,7 +100,7 @@ export type Observer<T> = { Defined at -[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L24). +[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L24). Referenced by: [Observable](#observable). @@ -112,7 +116,7 @@ export enum SessionState { Defined at -[packages/core-api/src/apis/definitions/auth.ts:182](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L182). +[packages/core-api/src/apis/definitions/auth.ts:182](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L182). Referenced by: [sessionState\$](#sessionstate). @@ -135,6 +139,6 @@ export type Subscription = { Defined at -[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L33). +[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L33). Referenced by: [Observable](#observable). diff --git a/docs/reference/utility-apis/StorageApi.md b/docs/reference/utility-apis/StorageApi.md index 6c5595d3d8..3247d28f60 100644 --- a/docs/reference/utility-apis/StorageApi.md +++ b/docs/reference/utility-apis/StorageApi.md @@ -1,7 +1,7 @@ # StorageApi The StorageApi type is defined at -[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/StorageApi.ts#L31). +[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/StorageApi.ts#L31). The following Utility API implements this type: [storageApiRef](./README.md#storage) @@ -79,7 +79,7 @@ export type Observable<T> = { Defined at -[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L53). +[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L53). Referenced by: [observe\$](#observe), [StorageApi](#storageapi). @@ -98,7 +98,7 @@ export type Observer<T> = { Defined at -[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L24). +[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L24). Referenced by: [Observable](#observable). @@ -144,7 +144,7 @@ export interface StorageApi { Defined at -[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/StorageApi.ts#L31). +[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/StorageApi.ts#L31). Referenced by: [forBucket](#forbucket). @@ -158,7 +158,7 @@ export type StorageValueChange<T = any> = { Defined at -[packages/core-api/src/apis/definitions/StorageApi.ts:21](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/StorageApi.ts#L21). +[packages/core-api/src/apis/definitions/StorageApi.ts:21](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/StorageApi.ts#L21). Referenced by: [observe\$](#observe), [StorageApi](#storageapi). @@ -181,6 +181,6 @@ export type Subscription = { Defined at -[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L33). +[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L33). Referenced by: [Observable](#observable). diff --git a/microsite/static/css/custom.css b/microsite/static/css/custom.css index c947c88e66..c54e0101d3 100644 --- a/microsite/static/css/custom.css +++ b/microsite/static/css/custom.css @@ -1115,6 +1115,10 @@ code { font-size: 115%; } +.post pre { + color: #e4e4e4; +} + .Block__GIF { width: 190px; height: auto; diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index fcd9b795a4..6190dda9da 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,91 @@ # example-app +## 0.2.15 + +### Patch Changes + +- 07bafa248: Add configurable `scope` for oauth2 auth provider. + + Some OAuth2 providers require certain scopes to facilitate a user sign-in using the Authorization Code flow. + This change adds the optional `scope` key to auth.providers.oauth2. An example is: + + ```yaml + auth: + providers: + oauth2: + development: + clientId: + $env: DEV_OAUTH2_CLIENT_ID + clientSecret: + $env: DEV_OAUTH2_CLIENT_SECRET + authorizationUrl: + $env: DEV_OAUTH2_AUTH_URL + tokenUrl: + $env: DEV_OAUTH2_TOKEN_URL + scope: saml-login-selector openid profile email + ``` + + This tells the OAuth 2.0 AS to perform a SAML login and return OIDC information include the `profile` + and `email` claims as part of the ID Token. + +- Updated dependencies [753bb4c40] +- Updated dependencies [6ed2b47d6] +- Updated dependencies [b33fa4cf4] +- Updated dependencies [d36660721] +- Updated dependencies [6b26c9f41] +- Updated dependencies [b3f0c3811] +- Updated dependencies [302795d10] +- Updated dependencies [9ec66c345] +- Updated dependencies [53d3e2d62] +- Updated dependencies [ca559171b] +- Updated dependencies [53348f0af] +- Updated dependencies [f5e564cd6] +- Updated dependencies [68dd79d83] +- Updated dependencies [29a138636] +- Updated dependencies [14aef4b94] +- Updated dependencies [41af18227] +- Updated dependencies [1df75733e] +- Updated dependencies [02d6803e8] +- Updated dependencies [b288a291e] +- Updated dependencies [025c0c7bf] +- Updated dependencies [e5da858d7] +- Updated dependencies [9230d07e7] +- Updated dependencies [f5f45744e] +- Updated dependencies [0fe8ff5be] +- Updated dependencies [c5ab91ce3] +- Updated dependencies [64b9efac2] +- Updated dependencies [19d354c78] +- Updated dependencies [7716d1d70] +- Updated dependencies [8f3443427] +- Updated dependencies [b51ee6ece] +- Updated dependencies [accdfeb30] +- Updated dependencies [804502a5c] + - @backstage/plugin-catalog-import@0.4.0 + - @backstage/plugin-catalog@0.3.1 + - @backstage/plugin-kubernetes@0.3.9 + - @backstage/plugin-rollbar@0.3.0 + - @backstage/plugin-scaffolder@0.5.0 + - @backstage/plugin-cost-insights@0.8.1 + - @backstage/plugin-circleci@0.2.8 + - @backstage/plugin-search@0.3.0 + - @backstage/plugin-cloudbuild@0.2.9 + - @backstage/plugin-register-component@0.2.9 + - @backstage/plugin-sentry@0.3.5 + - @backstage/plugin-jenkins@0.3.9 + - @backstage/plugin-api-docs@0.4.5 + - @backstage/plugin-lighthouse@0.2.10 + - @backstage/plugin-techdocs@0.5.6 + - @backstage/plugin-pagerduty@0.2.8 + - @backstage/plugin-org@0.3.6 + - @backstage/plugin-github-actions@0.3.2 + - @backstage/plugin-explore@0.2.5 + - @backstage/plugin-newrelic@0.2.5 + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/plugin-kafka@0.2.2 + - @backstage/core@0.6.1 + - @backstage/plugin-gitops-profiles@0.2.5 + - @backstage/plugin-tech-radar@0.3.5 + ## 0.2.14 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 859372b1f8..88d039e7c8 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,38 +1,38 @@ { "name": "example-app", - "version": "0.2.14", + "version": "0.2.15", "private": true, "bundled": true, "dependencies": { "@backstage/catalog-model": "^0.7.1", "@backstage/cli": "^0.6.0", - "@backstage/core": "^0.6.0", - "@backstage/plugin-api-docs": "^0.4.4", - "@backstage/plugin-catalog": "^0.3.0", - "@backstage/plugin-catalog-react": "^0.0.2", - "@backstage/plugin-catalog-import": "^0.3.7", - "@backstage/plugin-circleci": "^0.2.7", - "@backstage/plugin-cloudbuild": "^0.2.8", - "@backstage/plugin-cost-insights": "^0.8.0", - "@backstage/plugin-explore": "^0.2.4", + "@backstage/core": "^0.6.1", + "@backstage/plugin-api-docs": "^0.4.5", + "@backstage/plugin-catalog": "^0.3.1", + "@backstage/plugin-catalog-react": "^0.0.3", + "@backstage/plugin-catalog-import": "^0.4.0", + "@backstage/plugin-circleci": "^0.2.8", + "@backstage/plugin-cloudbuild": "^0.2.9", + "@backstage/plugin-cost-insights": "^0.8.1", + "@backstage/plugin-explore": "^0.2.5", "@backstage/plugin-gcp-projects": "^0.2.4", - "@backstage/plugin-github-actions": "^0.3.1", - "@backstage/plugin-gitops-profiles": "^0.2.4", + "@backstage/plugin-github-actions": "^0.3.2", + "@backstage/plugin-gitops-profiles": "^0.2.5", "@backstage/plugin-graphiql": "^0.2.7", - "@backstage/plugin-org": "^0.3.5", - "@backstage/plugin-jenkins": "^0.3.8", - "@backstage/plugin-kafka": "^0.2.1", - "@backstage/plugin-kubernetes": "^0.3.8", - "@backstage/plugin-lighthouse": "^0.2.9", - "@backstage/plugin-newrelic": "^0.2.4", - "@backstage/plugin-pagerduty": "0.2.7", - "@backstage/plugin-register-component": "^0.2.8", - "@backstage/plugin-rollbar": "^0.2.9", - "@backstage/plugin-scaffolder": "^0.4.2", - "@backstage/plugin-sentry": "^0.3.4", - "@backstage/plugin-search": "^0.2.7", - "@backstage/plugin-tech-radar": "^0.3.4", - "@backstage/plugin-techdocs": "^0.5.5", + "@backstage/plugin-org": "^0.3.6", + "@backstage/plugin-jenkins": "^0.3.9", + "@backstage/plugin-kafka": "^0.2.2", + "@backstage/plugin-kubernetes": "^0.3.9", + "@backstage/plugin-lighthouse": "^0.2.10", + "@backstage/plugin-newrelic": "^0.2.5", + "@backstage/plugin-pagerduty": "0.2.8", + "@backstage/plugin-register-component": "^0.2.9", + "@backstage/plugin-rollbar": "^0.3.0", + "@backstage/plugin-scaffolder": "^0.5.0", + "@backstage/plugin-sentry": "^0.3.5", + "@backstage/plugin-search": "^0.3.0", + "@backstage/plugin-tech-radar": "^0.3.5", + "@backstage/plugin-techdocs": "^0.5.6", "@backstage/plugin-user-settings": "^0.2.5", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", @@ -53,7 +53,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/test-utils": "^0.1.6", + "@backstage/test-utils": "^0.1.7", "@testing-library/cypress": "^7.0.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 00696736d1..cc673c7769 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -23,7 +23,7 @@ import { SignInPage, } from '@backstage/core'; import { Router as CatalogRouter } from '@backstage/plugin-catalog'; -import { Router as ImportComponentRouter } from '@backstage/plugin-catalog-import'; +import { CatalogImportPage } from '@backstage/plugin-catalog-import'; import { ExplorePage } from '@backstage/plugin-explore'; import { Router as GraphiQLRouter } from '@backstage/plugin-graphiql'; import { Router as LighthouseRouter } from '@backstage/plugin-lighthouse'; @@ -39,10 +39,15 @@ import { EntityPage } from './components/catalog/EntityPage'; import Root from './components/Root'; import { providers } from './identityProviders'; import * as plugins from './plugins'; +import AlarmIcon from '@material-ui/icons/Alarm'; const app = createApp({ apis, plugins: Object.values(plugins), + icons: { + // Custom icon example + alert: AlarmIcon, + }, components: { SignInPage: props => { return ( @@ -69,10 +74,7 @@ const catalogRouteRef = createRouteRef({ const routes = ( - } - /> + } /> } diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 5474afa72c..e849667c19 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -28,7 +28,11 @@ import { ProvidedApisCard, ProvidingComponentsCard, } from '@backstage/plugin-api-docs'; -import { AboutCard, EntityPageLayout } from '@backstage/plugin-catalog'; +import { + AboutCard, + EntityLinksCard, + EntityPageLayout, +} from '@backstage/plugin-catalog'; import { useEntity } from '@backstage/plugin-catalog-react'; import { isPluginApplicableToEntity as isCircleCIAvailable, @@ -177,6 +181,9 @@ const ComponentOverviewContent = ({ entity }: { entity: Entity }) => ( )} + + + {isGitHubAvailable(entity) && ( <> diff --git a/packages/app/src/identityProviders.ts b/packages/app/src/identityProviders.ts index 01828b1405..6f45f9ba32 100644 --- a/packages/app/src/identityProviders.ts +++ b/packages/app/src/identityProviders.ts @@ -22,6 +22,7 @@ import { samlAuthApiRef, microsoftAuthApiRef, oneloginAuthApiRef, + oauth2ApiRef, oidcAuthApiRef, } from '@backstage/core'; @@ -32,6 +33,12 @@ export const providers = [ message: 'Sign In using OpenId Connect', apiRef: oidcAuthApiRef, }, + { + id: 'oauth2-auth-provider', + title: 'OAuth 2.0', + message: 'Sign In using OAuth 2.0', + apiRef: oauth2ApiRef, + }, { id: 'google-auth-provider', title: 'Google', diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index cc33a7e81b..42f1fa4a42 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/backend-common +## 0.5.3 + +### Patch Changes + +- ffffea8e6: Minor updates to reflect the changes in `@backstage/integration` that made the fields `apiBaseUrl` and `apiUrl` mandatory. +- 82b2c11b6: Set explicit content-type in error handler responses. +- 965e200c6: Slight refactoring in support of a future search implementation in `UrlReader`. Mostly moving code around. +- 5a5163519: Implement `UrlReader.search` which implements glob matching. +- Updated dependencies [ffffea8e6] + - @backstage/integration@0.4.0 + ## 0.5.2 ### Patch Changes diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 895d241c6a..7b1c6870ab 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.5.2", + "version": "0.5.3", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -32,7 +32,7 @@ "@backstage/cli-common": "^0.1.1", "@backstage/config": "^0.1.2", "@backstage/config-loader": "^0.5.1", - "@backstage/integration": "^0.3.2", + "@backstage/integration": "^0.4.0", "@octokit/rest": "^18.0.12", "@types/cors": "^2.8.6", "@types/express": "^4.17.6", @@ -69,7 +69,7 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/test-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.7", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", "@types/concat-stream": "^1.6.0", diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts index c937f8a40c..b52097baf5 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.test.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts @@ -14,18 +14,22 @@ * limitations under the License. */ -import * as os from 'os'; +import { ConfigReader } from '@backstage/config'; +import { + AzureIntegration, + readAzureIntegrationConfig, +} from '@backstage/integration'; +import { msw } from '@backstage/test-utils'; import fs from 'fs-extra'; import mockFs from 'mock-fs'; -import path from 'path'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { ConfigReader } from '@backstage/config'; +import * as os from 'os'; +import path from 'path'; +import { NotModifiedError } from '../errors'; 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(); @@ -36,6 +40,16 @@ const treeResponseFactory = ReadTreeResponseFactory.create({ const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; describe('AzureUrlReader', () => { + beforeEach(() => { + mockFs({ + [tmpDir]: mockFs.directory(), + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + const worker = setupServer(); msw.setupDefaultHandlers(worker); @@ -143,22 +157,18 @@ describe('AzureUrlReader', () => { }); describe('readTree', () => { - beforeEach(() => { - mockFs({ - [tmpDir]: mockFs.directory(), - }); - }); - - afterEach(() => { - mockFs.restore(); - }); - const repoBuffer = fs.readFileSync( path.resolve('src', 'reading', '__fixtures__', 'mock-main.zip'), ); const processor = new AzureUrlReader( - { host: 'dev.azure.com' }, + new AzureIntegration( + readAzureIntegrationConfig( + new ConfigReader({ + host: 'dev.azure.com', + }), + ), + ), { treeResponseFactory }, ); @@ -257,4 +267,79 @@ describe('AzureUrlReader', () => { expect(indexMarkdownFile.toString()).toBe('# Test\n'); }); }); + + describe('search', () => { + const repoBuffer = fs.readFileSync( + path.resolve('src', 'reading', '__fixtures__', 'mock-main.zip'), + ); + + const processor = new AzureUrlReader( + new AzureIntegration( + readAzureIntegrationConfig( + new ConfigReader({ + host: 'dev.azure.com', + }), + ), + ), + { treeResponseFactory }, + ); + + beforeEach(() => { + worker.use( + rest.get( + 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/zip'), + 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/org-name/project-name/_apis/git/repositories/repo-name/commits', + (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + count: 2, + value: [ + { + commitId: '123abc2', + comment: 'second commit', + }, + { + commitId: '123abc1', + comment: 'first commit', + }, + ], + }), + ), + ), + ); + }); + + it('works for the naive case', async () => { + const result = await processor.search( + 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=%2F**%2Findex.*&version=GBmaster', + ); + expect(result.etag).toBe('123abc2'); + expect(result.files.length).toBe(1); + expect(result.files[0].url).toBe( + 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=%2Fdocs%2Findex.md&version=GBmaster', + ); + await expect(result.files[0].content()).resolves.toEqual( + Buffer.from('# Test\n'), + ); + }); + + it('throws NotModifiedError when same etag', async () => { + await expect( + processor.search( + 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=**/index.*&version=GBmaster', + { etag: '123abc2' }, + ), + ).rejects.toThrow(NotModifiedError); + }); + }); }); diff --git a/packages/backend-common/src/reading/AzureUrlReader.ts b/packages/backend-common/src/reading/AzureUrlReader.ts index bc1b61a041..21b988e5b0 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.ts @@ -15,39 +15,41 @@ */ import { - AzureIntegrationConfig, - readAzureIntegrationConfigs, - getAzureFileFetchUrl, - getAzureDownloadUrl, - getAzureRequestOptions, + AzureIntegration, getAzureCommitsUrl, + getAzureDownloadUrl, + getAzureFileFetchUrl, + getAzureRequestOptions, + ScmIntegrations, } from '@backstage/integration'; import fetch from 'cross-fetch'; +import parseGitUrl from 'git-url-parse'; +import { Minimatch } from 'minimatch'; import { Readable } from 'stream'; import { NotFoundError, NotModifiedError } from '../errors'; +import { ReadTreeResponseFactory } from './tree'; +import { stripFirstDirectoryFromPath } from './tree/util'; import { ReaderFactory, ReadTreeOptions, ReadTreeResponse, + SearchOptions, SearchResponse, UrlReader, } from './types'; -import { ReadTreeResponseFactory } from './tree'; export class AzureUrlReader implements UrlReader { static factory: ReaderFactory = ({ config, treeResponseFactory }) => { - const configs = readAzureIntegrationConfigs( - config.getOptionalConfigArray('integrations.azure') ?? [], - ); - return configs.map(options => { - const reader = new AzureUrlReader(options, { treeResponseFactory }); - const predicate = (url: URL) => url.host === options.host; + const integrations = ScmIntegrations.fromConfig(config); + return integrations.azure.list().map(integration => { + const reader = new AzureUrlReader(integration, { treeResponseFactory }); + const predicate = (url: URL) => url.host === integration.config.host; return { reader, predicate }; }); }; constructor( - private readonly options: AzureIntegrationConfig, + private readonly integration: AzureIntegration, private readonly deps: { treeResponseFactory: ReadTreeResponseFactory }, ) {} @@ -56,7 +58,10 @@ export class AzureUrlReader implements UrlReader { let response: Response; try { - response = await fetch(builtUrl, getAzureRequestOptions(this.options)); + response = await fetch( + builtUrl, + getAzureRequestOptions(this.integration.config), + ); } catch (e) { throw new Error(`Unable to read ${url}, ${e}`); } @@ -83,7 +88,7 @@ export class AzureUrlReader implements UrlReader { const commitsAzureResponse = await fetch( getAzureCommitsUrl(url), - getAzureRequestOptions(this.options), + getAzureRequestOptions(this.integration.config), ); if (!commitsAzureResponse.ok) { const message = `Failed to read tree from ${url}, ${commitsAzureResponse.status} ${commitsAzureResponse.statusText}`; @@ -100,7 +105,9 @@ export class AzureUrlReader implements UrlReader { const archiveAzureResponse = await fetch( getAzureDownloadUrl(url), - getAzureRequestOptions(this.options, { Accept: 'application/zip' }), + getAzureRequestOptions(this.integration.config, { + Accept: 'application/zip', + }), ); if (!archiveAzureResponse.ok) { const message = `Failed to read tree from ${url}, ${archiveAzureResponse.status} ${archiveAzureResponse.statusText}`; @@ -117,12 +124,38 @@ export class AzureUrlReader implements UrlReader { }); } - async search(): Promise { - throw new Error('AzureUrlReader does not implement search'); + async search(url: string, options?: SearchOptions): Promise { + const { filepath } = parseGitUrl(url); + const matcher = new Minimatch(filepath); + + // TODO(freben): For now, read the entire repo and filter through that. In + // a future improvement, we could be smart and try to deduce that non-glob + // prefixes (like for filepaths such as some-prefix/**/a.yaml) can be used + // to get just that part of the repo. + const treeUrl = new URL(url); + treeUrl.searchParams.delete('path'); + treeUrl.pathname = treeUrl.pathname.replace(/\/+$/, ''); + + const tree = await this.readTree(treeUrl.toString(), { + etag: options?.etag, + filter: path => matcher.match(stripFirstDirectoryFromPath(path)), + }); + const files = await tree.files(); + + return { + etag: tree.etag, + files: files.map(file => ({ + url: this.integration.resolveUrl({ + url: `/${file.path}`, + base: url, + }), + content: file.content, + })), + }; } toString() { - const { host, token } = this.options; + const { host, token } = this.integration.config; return `azure{host=${host},authed=${Boolean(token)}}`; } } diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts index 974c84b2c2..df9febb352 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts @@ -15,11 +15,16 @@ */ import { ConfigReader } from '@backstage/config'; +import { + BitbucketIntegration, + readBitbucketIntegrationConfig, +} from '@backstage/integration'; import { msw } from '@backstage/test-utils'; import fs from 'fs-extra'; import mockFs from 'mock-fs'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; +import os from 'os'; import path from 'path'; import { NotModifiedError } from '../errors'; import { BitbucketUrlReader } from './BitbucketUrlReader'; @@ -30,19 +35,45 @@ const treeResponseFactory = ReadTreeResponseFactory.create({ }); const bitbucketProcessor = new BitbucketUrlReader( - { host: 'bitbucket.org', apiBaseUrl: 'https://api.bitbucket.org/2.0' }, + new BitbucketIntegration( + readBitbucketIntegrationConfig( + new ConfigReader({ + 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', - }, + new BitbucketIntegration( + readBitbucketIntegrationConfig( + new ConfigReader({ + host: 'bitbucket.mycompany.net', + apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', + }), + ), + ), { treeResponseFactory }, ); +const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; + describe('BitbucketUrlReader', () => { + beforeEach(() => { + mockFs({ + [tmpDir]: mockFs.directory(), + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + const worker = setupServer(); + msw.setupDefaultHandlers(worker); + describe('implementation', () => { it('rejects unknown targets', async () => { await expect( @@ -54,19 +85,6 @@ describe('BitbucketUrlReader', () => { }); describe('readTree', () => { - beforeEach(() => { - mockFs({ - '/tmp': mockFs.directory(), - }); - }); - - afterEach(() => { - mockFs.restore(); - }); - - const worker = setupServer(); - msw.setupDefaultHandlers(worker); - const repoBuffer = fs.readFileSync( path.resolve( 'src', @@ -247,12 +265,153 @@ describe('BitbucketUrlReader', () => { expect(() => { /* eslint-disable no-new */ new BitbucketUrlReader( - { - host: 'bitbucket.mycompany.net', - }, + new BitbucketIntegration( + readBitbucketIntegrationConfig( + new ConfigReader({ + host: 'bitbucket.mycompany.net', + }), + ), + ), { treeResponseFactory }, ); }).toThrowError('must configure an explicit apiBaseUrl'); }); }); + + describe('search hosted', () => { + const repoBuffer = fs.readFileSync( + path.resolve( + 'src', + 'reading', + '__fixtures__', + 'bitbucket-repo-with-commit-hash.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) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/zip'), + ctx.set( + 'content-disposition', + 'attachment; filename=backstage-mock-12ab34cd56ef.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' }], + }), + ), + ), + ); + }); + + it('works for the naive case', async () => { + const result = await bitbucketProcessor.search( + 'https://bitbucket.org/backstage/mock/src/master/**/index.*', + ); + expect(result.etag).toBe('12ab34cd56ef'); + expect(result.files.length).toBe(1); + expect(result.files[0].url).toBe( + 'https://bitbucket.org/backstage/mock/src/master/docs/index.md', + ); + await expect(result.files[0].content()).resolves.toEqual( + Buffer.from('# Test\n'), + ); + }); + + it('throws NotModifiedError when same etag', async () => { + await expect( + bitbucketProcessor.search( + 'https://bitbucket.org/backstage/mock/src/master/**/index.*', + { etag: '12ab34cd56ef' }, + ), + ).rejects.toThrow(NotModifiedError); + }); + }); + + describe('search private', () => { + const privateBitbucketRepoBuffer = fs.readFileSync( + path.resolve( + 'src', + 'reading', + '__fixtures__', + 'bitbucket-server-repo.zip', + ), + ); + + beforeEach(() => { + 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.set( + 'content-disposition', + 'attachment; filename=backstage-mock.zip', + ), + ctx.body(privateBitbucketRepoBuffer), + ), + ), + rest.get( + 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/commits', + (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + values: [{ id: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }], + }), + ), + ), + ); + }); + + it('works for the naive case', async () => { + const result = await hostedBitbucketProcessor.search( + 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/**/index.*?at=master', + ); + expect(result.etag).toBe('12ab34cd56ef'); + expect(result.files.length).toBe(1); + expect(result.files[0].url).toBe( + 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.md?at=master', + ); + await expect(result.files[0].content()).resolves.toEqual( + Buffer.from('# Test\n'), + ); + }); + + it('throws NotModifiedError when same etag', async () => { + await expect( + hostedBitbucketProcessor.search( + 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/**/index.*?at=master', + { etag: '12ab34cd56ef' }, + ), + ).rejects.toThrow(NotModifiedError); + }); + }); }); diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.ts b/packages/backend-common/src/reading/BitbucketUrlReader.ts index e51c03e562..3954e7ee4a 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.ts @@ -15,22 +15,25 @@ */ import { - BitbucketIntegrationConfig, + BitbucketIntegration, getBitbucketDefaultBranch, getBitbucketDownloadUrl, getBitbucketFileFetchUrl, getBitbucketRequestOptions, - readBitbucketIntegrationConfigs, + ScmIntegrations, } from '@backstage/integration'; import fetch from 'cross-fetch'; import parseGitUrl from 'git-url-parse'; +import { Minimatch } from 'minimatch'; import { Readable } from 'stream'; import { NotFoundError, NotModifiedError } from '../errors'; import { ReadTreeResponseFactory } from './tree'; +import { stripFirstDirectoryFromPath } from './tree/util'; import { ReaderFactory, ReadTreeOptions, ReadTreeResponse, + SearchOptions, SearchResponse, UrlReader, } from './types'; @@ -40,45 +43,43 @@ import { * the one exposed by Bitbucket Cloud itself. */ export class BitbucketUrlReader implements UrlReader { - private readonly config: BitbucketIntegrationConfig; - private readonly treeResponseFactory: ReadTreeResponseFactory; - static factory: ReaderFactory = ({ config, treeResponseFactory }) => { - const configs = readBitbucketIntegrationConfigs( - config.getOptionalConfigArray('integrations.bitbucket') ?? [], - ); - return configs.map(provider => { - const reader = new BitbucketUrlReader(provider, { treeResponseFactory }); - const predicate = (url: URL) => url.host === provider.host; + const integrations = ScmIntegrations.fromConfig(config); + return integrations.bitbucket.list().map(integration => { + const reader = new BitbucketUrlReader(integration, { + treeResponseFactory, + }); + const predicate = (url: URL) => url.host === integration.config.host; return { reader, predicate }; }); }; constructor( - config: BitbucketIntegrationConfig, - deps: { treeResponseFactory: ReadTreeResponseFactory }, + private readonly integration: BitbucketIntegration, + private readonly deps: { treeResponseFactory: ReadTreeResponseFactory }, ) { - const { host, apiBaseUrl, token, username, appPassword } = config; + const { + host, + apiBaseUrl, + token, + username, + appPassword, + } = integration.config; if (!apiBaseUrl) { throw new Error( `Bitbucket integration for '${host}' must configure an explicit apiBaseUrl`, ); - } - - if (!token && username && !appPassword) { + } else if (!token && username && !appPassword) { throw new Error( `Bitbucket integration for '${host}' has configured a username but is missing a required appPassword.`, ); } - - this.config = config; - this.treeResponseFactory = deps.treeResponseFactory; } async read(url: string): Promise { - const bitbucketUrl = getBitbucketFileFetchUrl(url, this.config); - const options = getBitbucketRequestOptions(this.config); + const bitbucketUrl = getBitbucketFileFetchUrl(url, this.integration.config); + const options = getBitbucketRequestOptions(this.integration.config); let response: Response; try { @@ -109,10 +110,13 @@ export class BitbucketUrlReader implements UrlReader { throw new NotModifiedError(); } - const downloadUrl = await getBitbucketDownloadUrl(url, this.config); + const downloadUrl = await getBitbucketDownloadUrl( + url, + this.integration.config, + ); const archiveBitbucketResponse = await fetch( downloadUrl, - getBitbucketRequestOptions(this.config), + getBitbucketRequestOptions(this.integration.config), ); if (!archiveBitbucketResponse.ok) { const message = `Failed to read tree from ${url}, ${archiveBitbucketResponse.status} ${archiveBitbucketResponse.statusText}`; @@ -122,7 +126,7 @@ export class BitbucketUrlReader implements UrlReader { throw new Error(message); } - return await this.treeResponseFactory.fromZipArchive({ + return await this.deps.treeResponseFactory.fromZipArchive({ stream: (archiveBitbucketResponse.body as unknown) as Readable, subpath: filepath, etag: lastCommitShortHash, @@ -130,12 +134,36 @@ export class BitbucketUrlReader implements UrlReader { }); } - async search(): Promise { - throw new Error('BitbucketUrlReader does not implement search'); + async search(url: string, options?: SearchOptions): Promise { + const { filepath } = parseGitUrl(url); + const matcher = new Minimatch(filepath); + + // TODO(freben): For now, read the entire repo and filter through that. In + // a future improvement, we could be smart and try to deduce that non-glob + // prefixes (like for filepaths such as some-prefix/**/a.yaml) can be used + // to get just that part of the repo. + const treeUrl = url.replace(filepath, '').replace(/\/+$/, ''); + + const tree = await this.readTree(treeUrl, { + etag: options?.etag, + filter: path => matcher.match(stripFirstDirectoryFromPath(path)), + }); + const files = await tree.files(); + + return { + etag: tree.etag, + files: files.map(file => ({ + url: this.integration.resolveUrl({ + url: `/${file.path}`, + base: url, + }), + content: file.content, + })), + }; } toString() { - const { host, token, username, appPassword } = this.config; + const { host, token, username, appPassword } = this.integration.config; let authed = Boolean(token); if (!authed) { authed = Boolean(username && appPassword); @@ -148,18 +176,18 @@ export class BitbucketUrlReader implements UrlReader { let branch = ref; if (!branch) { - branch = await getBitbucketDefaultBranch(url, this.config); + branch = await getBitbucketDefaultBranch(url, this.integration.config); } const isHosted = resource === 'bitbucket.org'; // Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp222 const commitsApiUrl = isHosted - ? `${this.config.apiBaseUrl}/repositories/${project}/${repoName}/commits/${branch}` - : `${this.config.apiBaseUrl}/projects/${project}/repos/${repoName}/commits`; + ? `${this.integration.config.apiBaseUrl}/repositories/${project}/${repoName}/commits/${branch}` + : `${this.integration.config.apiBaseUrl}/projects/${project}/repos/${repoName}/commits`; const commitsResponse = await fetch( commitsApiUrl, - getBitbucketRequestOptions(this.config), + getBitbucketRequestOptions(this.integration.config), ); if (!commitsResponse.ok) { const message = `Failed to retrieve commits from ${commitsApiUrl}, ${commitsResponse.status} ${commitsResponse.statusText}`; diff --git a/packages/backend-common/src/reading/GithubUrlReader.test.ts b/packages/backend-common/src/reading/GithubUrlReader.test.ts index d4e579b143..72fbf83f6d 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.test.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.test.ts @@ -15,12 +15,17 @@ */ import { ConfigReader } from '@backstage/config'; -import { GithubCredentialsProvider } from '@backstage/integration'; +import { + GithubCredentialsProvider, + GitHubIntegration, + readGitHubIntegrationConfig, +} from '@backstage/integration'; import { msw } from '@backstage/test-utils'; import fs from 'fs-extra'; import mockFs from 'mock-fs'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; +import os from 'os'; import path from 'path'; import { NotFoundError, NotModifiedError } from '../errors'; import { @@ -41,26 +46,45 @@ const mockCredentialsProvider = ({ } as unknown) as GithubCredentialsProvider; const githubProcessor = new GithubUrlReader( - { - host: 'github.com', - apiBaseUrl: 'https://api.github.com', - }, + new GitHubIntegration( + readGitHubIntegrationConfig( + new ConfigReader({ + 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', - }, + new GitHubIntegration( + readGitHubIntegrationConfig( + new ConfigReader({ + host: 'ghe.github.com', + apiBaseUrl: 'https://ghe.github.com/api/v3', + }), + ), + ), { treeResponseFactory, credentialsProvider: mockCredentialsProvider }, ); +const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; + describe('GithubUrlReader', () => { const worker = setupServer(); - msw.setupDefaultHandlers(worker); + beforeEach(() => { + mockFs({ + [tmpDir]: mockFs.directory(), + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + beforeEach(() => { jest.clearAllMocks(); }); @@ -94,7 +118,7 @@ describe('GithubUrlReader', () => { worker.use( rest.get( - 'https://api.github.com/repos/backstage/mock/tree/contents/?ref=main', + 'https://ghe.github.com/api/v3/repos/backstage/mock/tree/contents/?ref=main', (req, res, ctx) => { expect(req.headers.get('authorization')).toBe( mockHeaders.Authorization, @@ -111,7 +135,7 @@ describe('GithubUrlReader', () => { ), ); - await githubProcessor.read( + await gheProcessor.read( 'https://github.com/backstage/mock/tree/blob/main', ); }); @@ -122,16 +146,6 @@ describe('GithubUrlReader', () => { */ describe('readTree', () => { - beforeEach(() => { - mockFs({ - '/tmp': mockFs.directory(), - }); - }); - - afterEach(() => { - mockFs.restore(); - }); - const repoBuffer = fs.readFileSync( path.resolve( 'src', @@ -397,9 +411,13 @@ describe('GithubUrlReader', () => { expect(() => { /* eslint-disable no-new */ new GithubUrlReader( - { - host: 'ghe.mycompany.net', - }, + new GitHubIntegration( + readGitHubIntegrationConfig( + new ConfigReader({ + host: 'ghe.mycompany.net', + }), + ), + ), { treeResponseFactory, credentialsProvider: mockCredentialsProvider, @@ -414,14 +432,6 @@ describe('GithubUrlReader', () => { */ describe('search', () => { - beforeEach(() => { - mockFs({ '/tmp': mockFs.directory() }); - }); - - afterEach(() => { - mockFs.restore(); - }); - const repoBuffer = fs.readFileSync( path.resolve( 'src', diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts index 291740a9fc..ac5a93bd5f 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.ts @@ -17,8 +17,8 @@ import { getGitHubFileFetchUrl, GithubCredentialsProvider, - GitHubIntegrationConfig, - readGitHubIntegrationConfigs, + GitHubIntegration, + ScmIntegrations, } from '@backstage/integration'; import { RestEndpointMethodTypes } from '@octokit/rest'; import fetch from 'cross-fetch'; @@ -48,36 +48,36 @@ export type GhBlobResponse = RestEndpointMethodTypes['git']['getBlob']['response */ export class GithubUrlReader implements UrlReader { static factory: ReaderFactory = ({ config, treeResponseFactory }) => { - const configs = readGitHubIntegrationConfigs( - config.getOptionalConfigArray('integrations.github') ?? [], - ); - return configs.map(provider => { - const credentialsProvider = GithubCredentialsProvider.create(provider); - const reader = new GithubUrlReader(provider, { + const integrations = ScmIntegrations.fromConfig(config); + return integrations.github.list().map(integration => { + const credentialsProvider = GithubCredentialsProvider.create( + integration.config, + ); + const reader = new GithubUrlReader(integration, { treeResponseFactory, credentialsProvider, }); - const predicate = (url: URL) => url.host === provider.host; + const predicate = (url: URL) => url.host === integration.config.host; return { reader, predicate }; }); }; constructor( - private readonly config: GitHubIntegrationConfig, + private readonly integration: GitHubIntegration, private readonly deps: { treeResponseFactory: ReadTreeResponseFactory; credentialsProvider: GithubCredentialsProvider; }, ) { - if (!config.apiBaseUrl && !config.rawBaseUrl) { + if (!integration.config.apiBaseUrl && !integration.config.rawBaseUrl) { throw new Error( - `GitHub integration for '${config.host}' must configure an explicit apiBaseUrl and rawBaseUrl`, + `GitHub integration '${integration.title}' must configure an explicit apiBaseUrl or rawBaseUrl`, ); } } async read(url: string): Promise { - const ghUrl = getGitHubFileFetchUrl(url, this.config); + const ghUrl = getGitHubFileFetchUrl(url, this.integration.config); const { headers } = await this.deps.credentialsProvider.getCredentials({ url, }); @@ -155,7 +155,7 @@ export class GithubUrlReader implements UrlReader { } toString() { - const { host, token } = this.config; + const { host, token } = this.integration.config; return `github{host=${host},authed=${Boolean(token)}}`; } @@ -258,7 +258,7 @@ export class GithubUrlReader implements UrlReader { }); const repo: GhRepoResponse = await this.fetchJson( - `${this.config.apiBaseUrl}/repos/${full_name}`, + `${this.integration.config.apiBaseUrl}/repos/${full_name}`, { headers }, ); diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts index b18e7a4294..636540caa8 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.test.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts @@ -20,11 +20,16 @@ import fs from 'fs-extra'; import mockFs from 'mock-fs'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; +import os from 'os'; import path from 'path'; import { getVoidLogger } from '../logging'; import { GitlabUrlReader } from './GitlabUrlReader'; import { ReadTreeResponseFactory } from './tree'; import { NotModifiedError, NotFoundError } from '../errors'; +import { + GitLabIntegration, + readGitLabIntegrationConfig, +} from '@backstage/integration'; const logger = getVoidLogger(); @@ -33,22 +38,44 @@ const treeResponseFactory = ReadTreeResponseFactory.create({ }); const gitlabProcessor = new GitlabUrlReader( - { - host: 'gitlab.com', - apiBaseUrl: 'https://gitlab.com/api/v4', - }, + new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader({ + host: 'gitlab.com', + apiBaseUrl: 'https://gitlab.com/api/v4', + baseUrl: 'https://gitlab.com', + }), + ), + ), { treeResponseFactory }, ); const hostedGitlabProcessor = new GitlabUrlReader( - { - host: 'gitlab.mycompany.com', - apiBaseUrl: 'https://gitlab.mycompany.com/api/v4', - }, + new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader({ + host: 'gitlab.mycompany.com', + apiBaseUrl: 'https://gitlab.mycompany.com/api/v4', + baseUrl: 'https://gitlab.mycompany.com', + }), + ), + ), { treeResponseFactory }, ); +const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; + describe('GitlabUrlReader', () => { + beforeEach(() => { + mockFs({ + [tmpDir]: mockFs.directory(), + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + const worker = setupServer(); msw.setupDefaultHandlers(worker); @@ -154,16 +181,6 @@ describe('GitlabUrlReader', () => { }); describe('readTree', () => { - beforeEach(() => { - mockFs({ - '/tmp': mockFs.directory(), - }); - }); - - afterEach(() => { - mockFs.restore(); - }); - const archiveBuffer = fs.readFileSync( path.resolve('src', 'reading', '__fixtures__', 'gitlab-archive.zip'), ); @@ -379,17 +396,81 @@ describe('GitlabUrlReader', () => { }; await expect(fnGithub).rejects.toThrow(NotFoundError); }); + }); - it('should throw error when apiBaseUrl is missing', () => { - expect(() => { - /* eslint-disable no-new */ - new GitlabUrlReader( - { - host: 'gitlab.mycompany.com', - }, - { treeResponseFactory }, - ); - }).toThrowError('must configure an explicit apiBaseUrl'); + describe('search', () => { + 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/api/v4/projects/backstage%2Fmock/repository/archive.zip?sha=main', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/zip'), + ctx.set( + 'content-disposition', + 'attachment; filename="mock-main-sha123abc.zip"', + ), + ctx.body(archiveBuffer), + ), + ), + 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), + ), + ), + 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), + ), + ), + ); + }); + + it('works for the naive case', async () => { + const result = await gitlabProcessor.search( + 'https://gitlab.com/backstage/mock/tree/main/**/index.*', + ); + expect(result.etag).toBe('sha123abc'); + expect(result.files.length).toBe(1); + expect(result.files[0].url).toBe( + 'https://gitlab.com/backstage/mock/tree/main/docs/index.md', + ); + await expect(result.files[0].content()).resolves.toEqual( + Buffer.from('# Test\n'), + ); + }); + + it('throws NotModifiedError when same etag', async () => { + await expect( + gitlabProcessor.search( + 'https://gitlab.com/backstage/mock/tree/main/**/index.*', + { etag: 'sha123abc' }, + ), + ).rejects.toThrow(NotModifiedError); }); }); }); diff --git a/packages/backend-common/src/reading/GitlabUrlReader.ts b/packages/backend-common/src/reading/GitlabUrlReader.ts index 5e02a9b8e6..66ceae05c6 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.ts @@ -17,55 +17,51 @@ import { getGitLabFileFetchUrl, getGitLabRequestOptions, - GitLabIntegrationConfig, - readGitLabIntegrationConfigs, + GitLabIntegration, + ScmIntegrations, } from '@backstage/integration'; import fetch from 'cross-fetch'; import parseGitUrl from 'git-url-parse'; +import { Minimatch } from 'minimatch'; import { Readable } from 'stream'; import { NotFoundError, NotModifiedError } from '../errors'; import { ReadTreeResponseFactory } from './tree'; +import { stripFirstDirectoryFromPath } from './tree/util'; import { ReaderFactory, ReadTreeOptions, ReadTreeResponse, + SearchOptions, SearchResponse, UrlReader, } from './types'; export class GitlabUrlReader implements UrlReader { - private readonly treeResponseFactory: ReadTreeResponseFactory; - static factory: ReaderFactory = ({ config, treeResponseFactory }) => { - const configs = readGitLabIntegrationConfigs( - config.getOptionalConfigArray('integrations.gitlab') ?? [], - ); - return configs.map(provider => { - const reader = new GitlabUrlReader(provider, { treeResponseFactory }); - const predicate = (url: URL) => url.host === provider.host; + const integrations = ScmIntegrations.fromConfig(config); + return integrations.gitlab.list().map(integration => { + const reader = new GitlabUrlReader(integration, { + treeResponseFactory, + }); + const predicate = (url: URL) => url.host === integration.config.host; return { reader, predicate }; }); }; constructor( - private readonly config: GitLabIntegrationConfig, - deps: { treeResponseFactory: ReadTreeResponseFactory }, - ) { - this.treeResponseFactory = deps.treeResponseFactory; - - if (!config.apiBaseUrl) { - throw new Error( - `GitLab integration for '${config.host}' must configure an explicit apiBaseUrl`, - ); - } - } + private readonly integration: GitLabIntegration, + private readonly deps: { treeResponseFactory: ReadTreeResponseFactory }, + ) {} async read(url: string): Promise { - const builtUrl = await getGitLabFileFetchUrl(url, this.config); + const builtUrl = await getGitLabFileFetchUrl(url, this.integration.config); let response: Response; try { - response = await fetch(builtUrl, getGitLabRequestOptions(this.config)); + response = await fetch( + builtUrl, + getGitLabRequestOptions(this.integration.config), + ); } catch (e) { throw new Error(`Unable to read ${url}, ${e}`); } @@ -92,9 +88,11 @@ export class GitlabUrlReader implements UrlReader { // https://docs.gitlab.com/ee/api/README.html#namespaced-path-encoding const projectGitlabResponse = await fetch( new URL( - `${this.config.apiBaseUrl}/projects/${encodeURIComponent(full_name)}`, + `${this.integration.config.apiBaseUrl}/projects/${encodeURIComponent( + full_name, + )}`, ).toString(), - getGitLabRequestOptions(this.config), + getGitLabRequestOptions(this.integration.config), ); if (!projectGitlabResponse.ok) { const msg = `Failed to read tree from ${url}, ${projectGitlabResponse.status} ${projectGitlabResponse.statusText}`; @@ -112,11 +110,11 @@ export class GitlabUrlReader implements UrlReader { // the provided sha. const branchGitlabResponse = await fetch( new URL( - `${this.config.apiBaseUrl}/projects/${encodeURIComponent( + `${this.integration.config.apiBaseUrl}/projects/${encodeURIComponent( full_name, )}/repository/branches/${branch}`, ).toString(), - getGitLabRequestOptions(this.config), + getGitLabRequestOptions(this.integration.config), ); if (!branchGitlabResponse.ok) { const message = `Failed to read tree (branch) from ${url}, ${branchGitlabResponse.status} ${branchGitlabResponse.statusText}`; @@ -134,10 +132,10 @@ export class GitlabUrlReader implements UrlReader { // https://docs.gitlab.com/ee/api/repositories.html#get-file-archive const archiveGitLabResponse = await fetch( - `${this.config.apiBaseUrl}/projects/${encodeURIComponent( + `${this.integration.config.apiBaseUrl}/projects/${encodeURIComponent( full_name, )}/repository/archive.zip?sha=${branch}`, - getGitLabRequestOptions(this.config), + getGitLabRequestOptions(this.integration.config), ); if (!archiveGitLabResponse.ok) { const message = `Failed to read tree (archive) from ${url}, ${archiveGitLabResponse.status} ${archiveGitLabResponse.statusText}`; @@ -147,7 +145,7 @@ export class GitlabUrlReader implements UrlReader { throw new Error(message); } - return await this.treeResponseFactory.fromZipArchive({ + return await this.deps.treeResponseFactory.fromZipArchive({ stream: (archiveGitLabResponse.body as unknown) as Readable, subpath: filepath, etag: commitSha, @@ -155,12 +153,33 @@ export class GitlabUrlReader implements UrlReader { }); } - async search(): Promise { - throw new Error('GitlabUrlReader does not implement search'); + async search(url: string, options?: SearchOptions): Promise { + const { filepath } = parseGitUrl(url); + const matcher = new Minimatch(filepath); + + // TODO(freben): For now, read the entire repo and filter through that. In + // a future improvement, we could be smart and try to deduce that non-glob + // prefixes (like for filepaths such as some-prefix/**/a.yaml) can be used + // to get just that part of the repo. + const treeUrl = url.replace(filepath, '').replace(/\/+$/, ''); + + const tree = await this.readTree(treeUrl, { + etag: options?.etag, + filter: path => matcher.match(stripFirstDirectoryFromPath(path)), + }); + const files = await tree.files(); + + return { + etag: tree.etag, + files: files.map(file => ({ + url: this.integration.resolveUrl({ url: `/${file.path}`, base: url }), + content: file.content, + })), + }; } toString() { - const { host, token } = this.config; + const { host, token } = this.integration.config; return `gitlab{host=${host},authed=${Boolean(token)}}`; } } diff --git a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts index d22529293f..e61a1df645 100644 --- a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts +++ b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts @@ -25,15 +25,12 @@ import { ReadTreeResponseDirOptions, ReadTreeResponseFile, } from '../types'; +import { stripFirstDirectoryFromPath } from './util'; // Tar types for `Parse` is not a proper constructor, but it should be const TarParseStream = (Parse as unknown) as { new (): ParseStream }; const pipeline = promisify(pipelineCb); -// Matches a directory name + one `/` at the start of any string, -// containing any character except `/` one or more times, and ending with a `/` -// e.g. Will match `dirA/` in `dirA/dirB/file.ext` -const directoryNameRegex = /^[^\/]+\//; /** * Wraps a tar archive stream into a tree response reader. @@ -84,7 +81,7 @@ export class TarArchiveResponse implements ReadTreeResponse { // File path relative to the root extracted directory. Will remove the // top level dir name from the path since its name is hard to predetermine. - const relativePath = entry.path.replace(directoryNameRegex, ''); + const relativePath = stripFirstDirectoryFromPath(entry.path); if (this.subPath) { if (!relativePath.startsWith(this.subPath)) { @@ -161,7 +158,7 @@ export class TarArchiveResponse implements ReadTreeResponse { filter: path => { // File path relative to the root extracted directory. Will remove the // top level dir name from the path since its name is hard to predetermine. - const relativePath = path.replace(directoryNameRegex, ''); + const relativePath = stripFirstDirectoryFromPath(path); if (this.subPath && !relativePath.startsWith(this.subPath)) { return false; } diff --git a/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts b/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts index 2547c7b0e4..4aebff5c84 100644 --- a/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts +++ b/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts @@ -24,11 +24,7 @@ import { ReadTreeResponseDirOptions, ReadTreeResponseFile, } from '../types'; - -// Matches a directory name + one `/` at the start of any string, -// containing any character except / one or more times, and ending with a `/` -// e.g. Will match `dirA/` in `dirA/dirB/file.ext` -const directoryNameRegex = /^[^\/]+\//; +import { stripFirstDirectoryFromPath } from './util'; /** * Wraps a zip archive stream into a tree response reader. @@ -65,18 +61,13 @@ export class ZipArchiveResponse implements ReadTreeResponse { this.read = true; } - // Will remove the top level dir name from the path since its name is hard to predetermine. - private stripTopDirectory(path: string): string { - return path.replace(directoryNameRegex, ''); - } - // File path relative to the root extracted directory or a sub directory if subpath is set. private getInnerPath(path: string): string { return path.slice(this.subPath.length); } private shouldBeIncluded(entry: Entry): boolean { - const strippedPath = this.stripTopDirectory(entry.path); + const strippedPath = stripFirstDirectoryFromPath(entry.path); if (this.subPath) { if (!strippedPath.startsWith(this.subPath)) { @@ -104,7 +95,7 @@ export class ZipArchiveResponse implements ReadTreeResponse { if (this.shouldBeIncluded(entry)) { files.push({ - path: this.getInnerPath(this.stripTopDirectory(entry.path)), + path: this.getInnerPath(stripFirstDirectoryFromPath(entry.path)), content: () => entry.buffer(), }); } else { @@ -153,7 +144,7 @@ export class ZipArchiveResponse implements ReadTreeResponse { // as a zip can have files with directories without directory entries if (entry.type === 'File' && this.shouldBeIncluded(entry)) { const entryPath = this.getInnerPath( - this.stripTopDirectory(entry.path), + stripFirstDirectoryFromPath(entry.path), ); const dirname = platformPath.dirname(entryPath); if (dirname) { diff --git a/packages/backend-common/src/reading/tree/util.ts b/packages/backend-common/src/reading/tree/util.ts new file mode 100644 index 0000000000..8e908a5e60 --- /dev/null +++ b/packages/backend-common/src/reading/tree/util.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +// Matches a directory name + one `/` at the start of any string, +// containing any character except `/` one or more times, and ending with a `/` +// e.g. Will match `dirA/` in `dirA/dirB/file.ext` +const directoryNameRegex = /^[^\/]+\//; + +// Removes the first segment of a forward-slash-separated path +export function stripFirstDirectoryFromPath(path: string): string { + return path.replace(directoryNameRegex, ''); +} diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts index 9c5ac20fa3..07da975184 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts @@ -161,7 +161,7 @@ export class ServiceBuilderImpl implements ServiceBuilder { app.use(cors(corsOptions)); } app.use(compression()); - app.use(requestLoggingHandler()); + app.use(requestLoggingHandler(logger)); for (const [root, route] of this.routers) { app.use(root, route); } diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index f85c22784d..05549b2085 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,34 @@ # example-backend +## 0.2.15 + +### Patch Changes + +- Updated dependencies [1deb31141] +- Updated dependencies [6ed2b47d6] +- Updated dependencies [77ad0003a] +- Updated dependencies [d2441aee3] +- Updated dependencies [727f0deec] +- Updated dependencies [fb53eb7cb] +- Updated dependencies [07bafa248] +- Updated dependencies [ffffea8e6] +- Updated dependencies [f3fbfb452] +- Updated dependencies [615103a63] +- Updated dependencies [84364b35c] +- Updated dependencies [82b2c11b6] +- Updated dependencies [965e200c6] +- Updated dependencies [5a5163519] +- Updated dependencies [82b2c11b6] +- Updated dependencies [08142b256] +- Updated dependencies [08142b256] + - @backstage/plugin-auth-backend@0.3.0 + - @backstage/plugin-scaffolder-backend@0.7.0 + - @backstage/plugin-catalog-backend@0.6.1 + - @backstage/plugin-app-backend@0.3.7 + - example-app@0.2.15 + - @backstage/backend-common@0.5.3 + - @backstage/plugin-techdocs-backend@0.6.0 + ## 0.2.14 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index 544bb976fe..e5271f0109 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.14", + "version": "0.2.15", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -27,24 +27,24 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.5.2", + "@backstage/backend-common": "^0.5.3", "@backstage/catalog-model": "^0.7.1", "@backstage/config": "^0.1.2", - "@backstage/plugin-app-backend": "^0.3.6", - "@backstage/plugin-auth-backend": "^0.2.12", - "@backstage/plugin-catalog-backend": "^0.6.0", + "@backstage/plugin-app-backend": "^0.3.7", + "@backstage/plugin-auth-backend": "^0.3.0", + "@backstage/plugin-catalog-backend": "^0.6.1", "@backstage/plugin-graphql-backend": "^0.1.5", "@backstage/plugin-kubernetes-backend": "^0.2.6", "@backstage/plugin-kafka-backend": "^0.2.0", "@backstage/plugin-proxy-backend": "^0.2.4", "@backstage/plugin-rollbar-backend": "^0.1.7", - "@backstage/plugin-scaffolder-backend": "^0.6.0", - "@backstage/plugin-techdocs-backend": "^0.5.5", + "@backstage/plugin-scaffolder-backend": "^0.7.0", + "@backstage/plugin-techdocs-backend": "^0.6.0", "@gitbeaker/node": "^28.0.2", "@octokit/rest": "^18.0.12", "azure-devops-node-api": "^10.1.1", "dockerode": "^3.2.1", - "example-app": "^0.2.14", + "example-app": "^0.2.15", "express": "^4.17.1", "express-promise-router": "^3.0.3", "knex": "^0.21.6", diff --git a/packages/catalog-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md index c8042b72e9..87d6e2cff0 100644 --- a/packages/catalog-client/CHANGELOG.md +++ b/packages/catalog-client/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/catalog-client +## 0.3.6 + +### Patch Changes + +- 6ed2b47d6: Include Backstage identity token in requests to backend plugins. +- 72b96e880: Add the `presence` argument to the `CatalogApi` to be able to register optional locations. + ## 0.3.5 ### Patch Changes diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index 00b05e37bf..1d0e890f4e 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-client", - "version": "0.3.5", + "version": "0.3.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/catalog-client/src/CatalogClient.test.ts b/packages/catalog-client/src/CatalogClient.test.ts index 6369f95b76..19da45e4ec 100644 --- a/packages/catalog-client/src/CatalogClient.test.ts +++ b/packages/catalog-client/src/CatalogClient.test.ts @@ -21,6 +21,7 @@ import { CatalogClient } from './CatalogClient'; import { CatalogListResponse, DiscoveryApi } from './types'; const server = setupServer(); +const token = 'fake-token'; const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base'; const discoveryApi: DiscoveryApi = { async getBaseUrl(_pluginId) { @@ -71,7 +72,7 @@ describe('CatalogClient', () => { }); it('should entities from correct endpoint', async () => { - const response = await client.getEntities(); + const response = await client.getEntities({}, { token }); expect(response).toEqual(defaultResponse); }); @@ -85,13 +86,16 @@ describe('CatalogClient', () => { }), ); - const response = await client.getEntities({ - filter: { - a: '1', - b: ['2', '3'], - ö: '=', + const response = await client.getEntities( + { + filter: { + a: '1', + b: ['2', '3'], + ö: '=', + }, }, - }); + { token }, + ); expect(response.items).toEqual([]); }); @@ -106,11 +110,61 @@ describe('CatalogClient', () => { }), ); - const response = await client.getEntities({ - fields: ['a.b', 'ö'], - }); + const response = await client.getEntities( + { + fields: ['a.b', 'ö'], + }, + { token }, + ); expect(response.items).toEqual([]); }); }); + + describe('getLocationById', () => { + const defaultResponse = { + data: { + id: '42', + }, + }; + + beforeEach(() => { + server.use( + rest.get(`${mockBaseUrl}/locations/42`, (_, res, ctx) => { + return res(ctx.json(defaultResponse)); + }), + ); + }); + + it('should locations from correct endpoint', async () => { + const response = await client.getLocationById('42', { token }); + expect(response).toEqual(defaultResponse); + }); + + it('forwards authorization token', async () => { + expect.assertions(1); + + server.use( + rest.get(`${mockBaseUrl}/locations/42`, (req, res, ctx) => { + expect(req.headers.get('authorization')).toBe(`Bearer ${token}`); + return res(ctx.json(defaultResponse)); + }), + ); + + await client.getLocationById('42', { token }); + }); + + it('skips authorization header if token is omitted', async () => { + expect.assertions(1); + + server.use( + rest.get(`${mockBaseUrl}/locations/42`, (req, res, ctx) => { + expect(req.headers.get('authorization')).toBeNull(); + return res(ctx.json(defaultResponse)); + }), + ); + + await client.getLocationById('42'); + }); + }); }); diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index 362b1e71da..04206edcdb 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -24,6 +24,7 @@ import fetch from 'cross-fetch'; import { AddLocationRequest, AddLocationResponse, + CatalogRequestOptions, CatalogApi, CatalogEntitiesRequest, CatalogListResponse, @@ -37,12 +38,16 @@ export class CatalogClient implements CatalogApi { this.discoveryApi = options.discoveryApi; } - async getLocationById(id: String): Promise { - return await this.getOptional(`/locations/${id}`); + async getLocationById( + id: String, + options?: CatalogRequestOptions, + ): Promise { + return await this.getOptional(`/locations/${id}`, options); } async getEntities( request?: CatalogEntitiesRequest, + options?: CatalogRequestOptions, ): Promise> { const { filter = {}, fields = [] } = request ?? {}; const params: string[] = []; @@ -62,20 +67,28 @@ export class CatalogClient implements CatalogApi { } const query = params.length ? `?${params.join('&')}` : ''; - const entities: Entity[] = await this.getRequired(`/entities${query}`); + const entities: Entity[] = await this.getRequired( + `/entities${query}`, + options, + ); return { items: entities }; } - async getEntityByName(compoundName: EntityName): Promise { + async getEntityByName( + compoundName: EntityName, + options?: CatalogRequestOptions, + ): Promise { const { kind, namespace = 'default', name } = compoundName; - return this.getOptional(`/entities/by-name/${kind}/${namespace}/${name}`); + return this.getOptional( + `/entities/by-name/${kind}/${namespace}/${name}`, + options, + ); } - async addLocation({ - type = 'url', - target, - dryRun, - }: AddLocationRequest): Promise { + async addLocation( + { type = 'url', target, dryRun, presence }: AddLocationRequest, + options?: CatalogRequestOptions, + ): Promise { const response = await fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/locations${ dryRun ? '?dryRun=true' : '' @@ -83,9 +96,10 @@ export class CatalogClient implements CatalogApi { { headers: { 'Content-Type': 'application/json', + ...(options?.token && { Authorization: `Bearer ${options?.token}` }), }, method: 'POST', - body: JSON.stringify({ type, target }), + body: JSON.stringify({ type, target, presence }), }, ); @@ -110,18 +124,30 @@ export class CatalogClient implements CatalogApi { }; } - async getLocationByEntity(entity: Entity): Promise { + async getLocationByEntity( + entity: Entity, + options?: CatalogRequestOptions, + ): Promise { const locationCompound = entity.metadata.annotations?.[LOCATION_ANNOTATION]; - const all: { data: Location }[] = await this.getRequired('/locations'); + const all: { data: Location }[] = await this.getRequired( + '/locations', + options, + ); return all .map(r => r.data) .find(l => locationCompound === `${l.type}:${l.target}`); } - async removeEntityByUid(uid: string): Promise { + async removeEntityByUid( + uid: string, + options?: CatalogRequestOptions, + ): Promise { const response = await fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/entities/by-uid/${uid}`, { + headers: options?.token + ? { Authorization: `Bearer ${options.token}` } + : {}, method: 'DELETE', }, ); @@ -138,9 +164,16 @@ export class CatalogClient implements CatalogApi { // Private methods // - private async getRequired(path: string): Promise { + private async getRequired( + path: string, + options?: CatalogRequestOptions, + ): Promise { const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`; - const response = await fetch(url); + const response = await fetch(url, { + headers: options?.token + ? { Authorization: `Bearer ${options.token}` } + : {}, + }); if (!response.ok) { const payload = await response.text(); @@ -151,9 +184,16 @@ export class CatalogClient implements CatalogApi { return await response.json(); } - private async getOptional(path: string): Promise { + private async getOptional( + path: string, + options?: CatalogRequestOptions, + ): Promise { const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`; - const response = await fetch(url); + const response = await fetch(url, { + headers: options?.token + ? { Authorization: `Bearer ${options.token}` } + : {}, + }); if (!response.ok) { if (response.status === 404) { diff --git a/packages/catalog-client/src/index.ts b/packages/catalog-client/src/index.ts index 9bd13f9b7c..c5a626e25b 100644 --- a/packages/catalog-client/src/index.ts +++ b/packages/catalog-client/src/index.ts @@ -15,4 +15,10 @@ */ export { CatalogClient } from './CatalogClient'; -export type { CatalogApi } from './types'; +export type { + AddLocationRequest, + AddLocationResponse, + CatalogApi, + CatalogEntitiesRequest, + CatalogListResponse, +} from './types'; diff --git a/packages/catalog-client/src/types.ts b/packages/catalog-client/src/types.ts index e72317d444..e460843657 100644 --- a/packages/catalog-client/src/types.ts +++ b/packages/catalog-client/src/types.ts @@ -25,21 +25,42 @@ export type CatalogListResponse = { items: T[]; }; +export type CatalogRequestOptions = { + token?: string; +}; + export interface CatalogApi { - getLocationById(id: String): Promise; - getEntityByName(name: EntityName): Promise; + getLocationById( + id: String, + options?: CatalogRequestOptions, + ): Promise; + getEntityByName( + name: EntityName, + options?: CatalogRequestOptions, + ): Promise; getEntities( request?: CatalogEntitiesRequest, + options?: CatalogRequestOptions, ): Promise>; - addLocation(location: AddLocationRequest): Promise; - getLocationByEntity(entity: Entity): Promise; - removeEntityByUid(uid: string): Promise; + addLocation( + location: AddLocationRequest, + options?: CatalogRequestOptions, + ): Promise; + getLocationByEntity( + entity: Entity, + options?: CatalogRequestOptions, + ): Promise; + removeEntityByUid( + uid: string, + options?: CatalogRequestOptions, + ): Promise; } export type AddLocationRequest = { type?: string; target: string; dryRun?: boolean; + presence?: 'optional' | 'required'; }; export type AddLocationResponse = { diff --git a/packages/catalog-model/examples/components/artist-lookup-component.yaml b/packages/catalog-model/examples/components/artist-lookup-component.yaml index fbf28d7fcb..edd9b8fcf9 100644 --- a/packages/catalog-model/examples/components/artist-lookup-component.yaml +++ b/packages/catalog-model/examples/components/artist-lookup-component.yaml @@ -7,11 +7,27 @@ metadata: - java - data links: - - url: https://example.com/apm/artists-lookup - title: APM + - url: https://example.com/user + title: Examples Users + icon: user + - url: https://example.com/group + title: Example Group + icon: group + - url: https://example.com/cloud + title: Link with Cloud Icon + icon: cloud + - url: https://example.com/dashboard + title: Dashboard icon: dashboard - - url: https://example.com/logs/artists-lookup - title: Logs + - url: https://example.com/help + title: Support + icon: help + - url: https://example.com/web + title: Website + icon: web + - url: https://example.com/alert + title: Alerts + icon: alert spec: type: service lifecycle: experimental diff --git a/packages/cli/package.json b/packages/cli/package.json index 9aaeb00459..dde6bc34ba 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -43,7 +43,7 @@ "@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-jsx": "5.5.x", "@svgr/plugin-svgo": "5.4.x", "@svgr/rollup": "5.5.x", "@svgr/webpack": "5.4.x", diff --git a/packages/cli/src/lib/versioning/Lockfile.ts b/packages/cli/src/lib/versioning/Lockfile.ts index f567786b7c..f0867c6408 100644 --- a/packages/cli/src/lib/versioning/Lockfile.ts +++ b/packages/cli/src/lib/versioning/Lockfile.ts @@ -153,7 +153,7 @@ export class Lockfile { const acceptedVersion = versions.find(v => semver.satisfies(v, range)); if (!acceptedVersion) { throw new Error( - `No existing version was accepted for range ${range}, searching through ${versions}`, + `No existing version was accepted for range ${range}, searching through ${versions}, for package ${name}`, ); } diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index 2aaeba9796..f50bcdd015 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -48,7 +48,7 @@ import { routeElementDiscoverer, traverseElementTree, } from '../extensions/traversal'; -import { IconComponent, SystemIconKey, SystemIcons } from '../icons'; +import { IconComponent, IconComponentMap, IconKey } from '../icons'; import { BackstagePlugin } from '../plugin'; import { RouteRef } from '../routing'; import { @@ -95,7 +95,7 @@ export function generateBoundRoutes( type FullAppOptions = { apis: Iterable; - icons: SystemIcons; + icons: IconComponentMap; plugins: BackstagePlugin[]; components: AppComponents; themes: AppTheme[]; @@ -144,7 +144,7 @@ export class PrivateAppImpl implements BackstageApp { private configApi?: ConfigApi; private readonly apis: Iterable; - private readonly icons: SystemIcons; + private readonly icons: IconComponentMap; private readonly plugins: BackstagePlugin[]; private readonly components: AppComponents; private readonly themes: AppTheme[]; @@ -169,7 +169,7 @@ export class PrivateAppImpl implements BackstageApp { return this.plugins; } - getSystemIcon(key: SystemIconKey): IconComponent { + getSystemIcon(key: IconKey): IconComponent { return this.icons[key]; } diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts index b6b1002d12..1970868ca6 100644 --- a/packages/core-api/src/app/types.ts +++ b/packages/core-api/src/app/types.ts @@ -15,7 +15,7 @@ */ import { ComponentType } from 'react'; -import { IconComponent, SystemIconKey, SystemIcons } from '../icons'; +import { IconComponent, IconComponentMap, IconKey } from '../icons'; import { BackstagePlugin, AnyExternalRoutes } from '../plugin/types'; import { RouteRef } from '../routing'; import { AnyApiFactory } from '../apis'; @@ -94,7 +94,7 @@ export type AppOptions = { /** * Supply icons to override the default ones. */ - icons?: Partial; + icons?: IconComponentMap; /** * A list of all plugins to include in the app. @@ -169,9 +169,9 @@ export type BackstageApp = { getPlugins(): BackstagePlugin[]; /** - * Get a common icon for this app. + * Get a common or custom icon for this app. */ - getSystemIcon(key: SystemIconKey): IconComponent; + getSystemIcon(key: IconKey): IconComponent; /** * Provider component that should wrap the Router created with getRouter() diff --git a/packages/core-api/src/icons/icons.tsx b/packages/core-api/src/icons/icons.tsx index 50c4b68e43..0c2b580ea7 100644 --- a/packages/core-api/src/icons/icons.tsx +++ b/packages/core-api/src/icons/icons.tsx @@ -15,15 +15,19 @@ */ import { SvgIconProps } from '@material-ui/core'; +import MuiDashboardIcon from '@material-ui/icons/Dashboard'; +import MuiHelpIcon from '@material-ui/icons/Help'; import PeopleIcon from '@material-ui/icons/People'; import PersonIcon from '@material-ui/icons/Person'; import React from 'react'; import { useApp } from '../app/AppContext'; -import { IconComponent, SystemIconKey, SystemIcons } from './types'; +import { IconComponent, SystemIconKey, IconComponentMap } from './types'; -export const defaultSystemIcons: SystemIcons = { +export const defaultSystemIcons: IconComponentMap = { user: PersonIcon, group: PeopleIcon, + dashboard: MuiDashboardIcon, + help: MuiHelpIcon, }; const overridableSystemIcon = (key: SystemIconKey): IconComponent => { @@ -35,5 +39,7 @@ const overridableSystemIcon = (key: SystemIconKey): IconComponent => { return Component; }; -export const UserIcon = overridableSystemIcon('user'); +export const DashboardIcon = overridableSystemIcon('dashboard'); export const GroupIcon = overridableSystemIcon('group'); +export const HelpIcon = overridableSystemIcon('help'); +export const UserIcon = overridableSystemIcon('user'); diff --git a/packages/core-api/src/icons/types.ts b/packages/core-api/src/icons/types.ts index 30e0ba53b2..be24b223ae 100644 --- a/packages/core-api/src/icons/types.ts +++ b/packages/core-api/src/icons/types.ts @@ -17,6 +17,8 @@ import { ComponentType } from 'react'; import { SvgIconProps } from '@material-ui/core'; +export type SystemIconKey = 'user' | 'group' | 'dashboard' | 'help'; + export type IconComponent = ComponentType; -export type SystemIconKey = 'user' | 'group'; -export type SystemIcons = { [key in SystemIconKey]: IconComponent }; +export type IconKey = SystemIconKey | string; +export type IconComponentMap = { [key in IconKey]: IconComponent }; diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 60a6e43654..a5a56f633a 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/core +## 0.6.1 + +### Patch Changes + +- b51ee6ece: Fixed type inference of `createRouteRef`. + ## 0.6.0 ### Minor Changes diff --git a/packages/core/package.json b/packages/core/package.json index 41dbc8ef14..c9045c6465 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core", "description": "Core API used by Backstage plugins and apps", - "version": "0.6.0", + "version": "0.6.1", "private": false, "publishConfig": { "access": "public", @@ -67,7 +67,7 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/test-utils": "^0.1.6", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/packages/core/src/components/index.ts b/packages/core/src/components/index.ts index 77eba9759a..28056b33bf 100644 --- a/packages/core/src/components/index.ts +++ b/packages/core/src/components/index.ts @@ -31,6 +31,7 @@ export * from './MarkdownContent'; export * from './OAuthRequestDialog'; export * from './Progress'; export * from './ProgressBars'; +export * from './Select'; export * from './SimpleStepper'; export * from './Status'; export * from './StructuredMetadataTable'; diff --git a/packages/core/src/layout/Sidebar/Items.tsx b/packages/core/src/layout/Sidebar/Items.tsx index 57cf435651..e41a45a225 100644 --- a/packages/core/src/layout/Sidebar/Items.tsx +++ b/packages/core/src/layout/Sidebar/Items.tsx @@ -215,7 +215,6 @@ export const SidebarItem = forwardRef((props, ref) => { {...childProps} activeClassName={classes.selected} to={props.to} - end ref={ref} > {content} diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 13b854972a..6a0f75ebaf 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,101 @@ # @backstage/create-app +## 0.3.9 + +### Patch Changes + +- 615103a63: Pass on plugin database management instance that is now required by the scaffolder plugin. + + To apply this change to an existing application, add the following to `src/plugins/scaffolder.ts`: + + ```diff + export default async function createPlugin({ + logger, + config, + + database, + }: PluginEnvironment) { + + // ...omitted... + + return await createRouter({ + preparers, + templaters, + publishers, + logger, + config, + dockerClient, + entityClient, + + database, + }); + } + ``` + +- 30e200d12: `@backstage/plugin-catalog-import` has been refactored, so the `App.tsx` of the backstage apps need to be updated: + + ```diff + // packages/app/src/App.tsx + + } + + element={} + /> + ``` + +- f4b576d0e: TechDocs: Add comments about migrating away from basic setup in app-config.yaml +- Updated dependencies [753bb4c40] +- Updated dependencies [1deb31141] +- Updated dependencies [6ed2b47d6] +- Updated dependencies [77ad0003a] +- Updated dependencies [6b26c9f41] +- Updated dependencies [b3f0c3811] +- Updated dependencies [d2441aee3] +- Updated dependencies [727f0deec] +- Updated dependencies [fb53eb7cb] +- Updated dependencies [07bafa248] +- Updated dependencies [ca559171b] +- Updated dependencies [ffffea8e6] +- Updated dependencies [f5e564cd6] +- Updated dependencies [f3fbfb452] +- Updated dependencies [615103a63] +- Updated dependencies [68dd79d83] +- Updated dependencies [84364b35c] +- Updated dependencies [41af18227] +- Updated dependencies [82b2c11b6] +- Updated dependencies [1df75733e] +- Updated dependencies [965e200c6] +- Updated dependencies [b51ee6ece] +- Updated dependencies [e5da858d7] +- Updated dependencies [9230d07e7] +- Updated dependencies [f5f45744e] +- Updated dependencies [0fe8ff5be] +- Updated dependencies [5a5163519] +- Updated dependencies [82b2c11b6] +- Updated dependencies [8f3443427] +- Updated dependencies [08142b256] +- Updated dependencies [08142b256] +- Updated dependencies [b51ee6ece] +- Updated dependencies [804502a5c] + - @backstage/plugin-catalog-import@0.4.0 + - @backstage/plugin-auth-backend@0.3.0 + - @backstage/plugin-catalog@0.3.1 + - @backstage/plugin-scaffolder@0.5.0 + - @backstage/plugin-scaffolder-backend@0.7.0 + - @backstage/plugin-catalog-backend@0.6.1 + - @backstage/plugin-circleci@0.2.8 + - @backstage/plugin-search@0.3.0 + - @backstage/plugin-app-backend@0.3.7 + - @backstage/backend-common@0.5.3 + - @backstage/plugin-api-docs@0.4.5 + - @backstage/plugin-lighthouse@0.2.10 + - @backstage/plugin-techdocs@0.5.6 + - @backstage/test-utils@0.1.7 + - @backstage/plugin-github-actions@0.3.2 + - @backstage/plugin-explore@0.2.5 + - @backstage/plugin-techdocs-backend@0.6.0 + - @backstage/core@0.6.1 + - @backstage/plugin-tech-radar@0.3.5 + ## 0.3.8 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 5f32a5d13d..5e4cf8ae11 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "Create app package for Backstage", - "version": "0.3.8", + "version": "0.3.9", "private": false, "publishConfig": { "access": "public" @@ -44,31 +44,31 @@ "ts-node": "^8.6.2" }, "peerDependencies": { - "@backstage/backend-common": "^0.5.2", + "@backstage/backend-common": "^0.5.3", "@backstage/catalog-model": "^0.7.1", "@backstage/cli": "^0.6.0", "@backstage/config": "^0.1.2", - "@backstage/core": "^0.6.0", - "@backstage/plugin-api-docs": "^0.4.4", - "@backstage/plugin-app-backend": "^0.3.6", - "@backstage/plugin-auth-backend": "^0.2.12", - "@backstage/plugin-catalog": "^0.3.0", - "@backstage/plugin-catalog-backend": "^0.6.0", - "@backstage/plugin-catalog-import": "^0.3.7", - "@backstage/plugin-circleci": "^0.2.7", - "@backstage/plugin-explore": "^0.2.4", - "@backstage/plugin-github-actions": "^0.3.1", - "@backstage/plugin-lighthouse": "^0.2.9", + "@backstage/core": "^0.6.1", + "@backstage/plugin-api-docs": "^0.4.5", + "@backstage/plugin-app-backend": "^0.3.7", + "@backstage/plugin-auth-backend": "^0.3.0", + "@backstage/plugin-catalog": "^0.3.1", + "@backstage/plugin-catalog-backend": "^0.6.1", + "@backstage/plugin-catalog-import": "^0.4.0", + "@backstage/plugin-circleci": "^0.2.8", + "@backstage/plugin-explore": "^0.2.5", + "@backstage/plugin-github-actions": "^0.3.2", + "@backstage/plugin-lighthouse": "^0.2.10", "@backstage/plugin-proxy-backend": "^0.2.4", "@backstage/plugin-rollbar-backend": "^0.1.7", - "@backstage/plugin-scaffolder": "^0.4.2", - "@backstage/plugin-search": "^0.2.7", - "@backstage/plugin-scaffolder-backend": "^0.6.0", - "@backstage/plugin-tech-radar": "^0.3.4", - "@backstage/plugin-techdocs": "^0.5.5", - "@backstage/plugin-techdocs-backend": "^0.5.5", + "@backstage/plugin-scaffolder": "^0.5.0", + "@backstage/plugin-search": "^0.3.0", + "@backstage/plugin-scaffolder-backend": "^0.7.0", + "@backstage/plugin-tech-radar": "^0.3.5", + "@backstage/plugin-techdocs": "^0.5.6", + "@backstage/plugin-techdocs-backend": "^0.6.0", "@backstage/plugin-user-settings": "^0.2.5", - "@backstage/test-utils": "^0.1.6", + "@backstage/test-utils": "^0.1.7", "@backstage/theme": "^0.2.3" }, "nodemonConfig": { diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index 495b30dd6f..f8a345cd1e 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -57,6 +57,9 @@ proxy: changeOrigin: true # Reference documentation http://backstage.io/docs/features/techdocs/configuration +# Note: After experimenting with basic setup, use CI/CD to generate docs +# and an external cloud storage when deploying TechDocs for production use-case. +# https://backstage.io/docs/features/techdocs/how-to-guides#how-to-migrate-from-techdocs-basic-to-recommended-deployment-approach techdocs: builder: 'local' # Alternatives - 'external' generators: diff --git a/packages/create-app/templates/default-app/packages/app/src/App.tsx b/packages/create-app/templates/default-app/packages/app/src/App.tsx index 6deedcd748..711e501224 100644 --- a/packages/create-app/templates/default-app/packages/app/src/App.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/App.tsx @@ -4,7 +4,6 @@ import { AlertDisplay, OAuthRequestDialog, SidebarPage, - createRouteRef, FlatRoutes, } from '@backstage/core'; import { apis } from './apis'; @@ -13,7 +12,7 @@ import { AppSidebar } from './sidebar'; import { Route, Navigate } from 'react-router'; import { Router as CatalogRouter } from '@backstage/plugin-catalog'; import { Router as DocsRouter } from '@backstage/plugin-techdocs'; -import { Router as ImportComponentRouter } from '@backstage/plugin-catalog-import'; +import { CatalogImportPage } from '@backstage/plugin-catalog-import'; import { Router as TechRadarRouter } from '@backstage/plugin-tech-radar'; import { SearchPage as SearchRouter } from '@backstage/plugin-search'; import { Router as SettingsRouter } from '@backstage/plugin-user-settings'; @@ -29,12 +28,6 @@ const AppProvider = app.getProvider(); const AppRouter = app.getRouter(); const deprecatedAppRoutes = app.getRoutes(); -const catalogRouteRef = createRouteRef({ - path: '/catalog', - title: 'Service Catalog', -}); - - const App = () => ( @@ -53,10 +46,7 @@ const App = () => ( path="/tech-radar" element={} /> - } - /> + } /> } diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index 251a4db7ee..4e16deebdc 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/dev-utils +## 0.1.10 + +### Patch Changes + +- Updated dependencies [b51ee6ece] +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/test-utils@0.1.7 + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.1.9 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 3a01306b46..7f53d029b3 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/dev-utils", "description": "Utilities for developing Backstage plugins.", - "version": "0.1.9", + "version": "0.1.10", "private": false, "publishConfig": { "access": "public", @@ -29,10 +29,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.6.0", + "@backstage/core": "^0.6.1", "@backstage/catalog-model": "^0.7.1", - "@backstage/plugin-catalog-react": "^0.0.2", - "@backstage/test-utils": "^0.1.5", + "@backstage/plugin-catalog-react": "^0.0.3", + "@backstage/test-utils": "^0.1.7", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/packages/e2e-test/.eslintrc.js b/packages/e2e-test/.eslintrc.js index ce2592e03b..7c8a092081 100644 --- a/packages/e2e-test/.eslintrc.js +++ b/packages/e2e-test/.eslintrc.js @@ -1,6 +1,5 @@ module.exports = { extends: [require.resolve('@backstage/cli/config/eslint.backend')], - ignorePatterns: ['templates/**'], rules: { 'no-console': 0, 'import/no-extraneous-dependencies': [ diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md index 11839d733b..7056976458 100644 --- a/packages/integration/CHANGELOG.md +++ b/packages/integration/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/integration +## 0.4.0 + +### Minor Changes + +- ffffea8e6: Update the `GitLabIntegrationConfig` to require the fields `apiBaseUrl` and `baseUrl`. The `readGitLabIntegrationConfig` function is now more strict and has better error reporting. This change mirrors actual reality in code more properly - the fields are actually necessary for many parts of code to actually function, so they should no longer be optional. + + Some checks that used to happen deep inside code that consumed config, now happen upfront at startup. This means that you may encounter new errors at backend startup, if you had actual mistakes in config but didn't happen to exercise the code paths that actually would break. But for most users, no change will be necessary. + + An example minimal GitLab config block that just adds a token to public GitLab would look similar to this: + + ```yaml + integrations: + gitlab: + - host: gitlab.com + token: + $env: GITLAB_TOKEN + ``` + + A full fledged config that points to a locally hosted GitLab could look like this: + + ```yaml + integrations: + gitlab: + - host: gitlab.my-company.com + apiBaseUrl: https://gitlab.my-company.com/api/v4 + baseUrl: https://gitlab.my-company.com + token: + $env: OUR_GITLAB_TOKEN + ``` + + In this case, the only optional field is `baseUrl` which is formed from the `host` if needed. + ## 0.3.2 ### Patch Changes diff --git a/packages/integration/package.json b/packages/integration/package.json index f82472ca97..b4134ab00e 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration", - "version": "0.3.2", + "version": "0.4.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -38,7 +38,7 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/test-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.7", "@types/jest": "^26.0.7", "@types/luxon": "^1.25.0", "msw": "^0.21.2" diff --git a/packages/integration/src/ScmIntegrations.ts b/packages/integration/src/ScmIntegrations.ts index c20fdcef9c..dddf6ed5d0 100644 --- a/packages/integration/src/ScmIntegrations.ts +++ b/packages/integration/src/ScmIntegrations.ts @@ -19,6 +19,7 @@ import { AzureIntegration } from './azure/AzureIntegration'; import { BitbucketIntegration } from './bitbucket/BitbucketIntegration'; import { GitHubIntegration } from './github/GitHubIntegration'; import { GitLabIntegration } from './gitlab/GitLabIntegration'; +import { defaultScmResolveUrl } from './helpers'; import { ScmIntegration, ScmIntegrationRegistry, @@ -83,11 +84,11 @@ export class ScmIntegrations implements ScmIntegrationRegistry { } resolveUrl(options: { url: string; base: string }): string { - const resolve = this.byUrl(options.base)?.resolveUrl; - if (!resolve) { - return new URL(options.url, options.base).toString(); + const integration = this.byUrl(options.base); + if (!integration) { + return defaultScmResolveUrl(options); } - return resolve(options); + return integration.resolveUrl(options); } } diff --git a/packages/integration/src/azure/AzureIntegration.test.ts b/packages/integration/src/azure/AzureIntegration.test.ts index 8a10e40bcf..3f9e165b90 100644 --- a/packages/integration/src/azure/AzureIntegration.test.ts +++ b/packages/integration/src/azure/AzureIntegration.test.ts @@ -58,6 +58,16 @@ describe('AzureIntegration', () => { 'https://dev.azure.com/organization/project/_git/repository?path=%2Fa.yaml', ); + expect( + integration.resolveUrl({ + url: '/a.yaml', + base: + 'https://dev.azure.com/organization/project/_git/repository?path=%2Ffolder%2Fcatalog-info.yaml', + }), + ).toBe( + 'https://dev.azure.com/organization/project/_git/repository?path=%2Fa.yaml', + ); + expect( integration.resolveUrl({ url: './a.yaml', diff --git a/packages/integration/src/azure/index.ts b/packages/integration/src/azure/index.ts index 6d57437779..2d95ef19b9 100644 --- a/packages/integration/src/azure/index.ts +++ b/packages/integration/src/azure/index.ts @@ -14,14 +14,15 @@ * limitations under the License. */ +export { AzureIntegration } from './AzureIntegration'; export { readAzureIntegrationConfig, readAzureIntegrationConfigs, } from './config'; export type { AzureIntegrationConfig } from './config'; export { + getAzureCommitsUrl, getAzureDownloadUrl, getAzureFileFetchUrl, getAzureRequestOptions, - getAzureCommitsUrl, } from './core'; diff --git a/packages/integration/src/bitbucket/BitbucketIntegration.ts b/packages/integration/src/bitbucket/BitbucketIntegration.ts index f3e69b946a..10b69877e4 100644 --- a/packages/integration/src/bitbucket/BitbucketIntegration.ts +++ b/packages/integration/src/bitbucket/BitbucketIntegration.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { basicIntegrations } from '../helpers'; +import { basicIntegrations, defaultScmResolveUrl } from '../helpers'; import { ScmIntegration, ScmIntegrationsFactory } from '../types'; import { BitbucketIntegrationConfig, @@ -47,4 +47,8 @@ export class BitbucketIntegration implements ScmIntegration { get config(): BitbucketIntegrationConfig { return this.integrationConfig; } + + resolveUrl(options: { url: string; base: string }): string { + return defaultScmResolveUrl(options); + } } diff --git a/packages/integration/src/bitbucket/index.ts b/packages/integration/src/bitbucket/index.ts index 9df4d3d3fe..124fe4a2a2 100644 --- a/packages/integration/src/bitbucket/index.ts +++ b/packages/integration/src/bitbucket/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +export { BitbucketIntegration } from './BitbucketIntegration'; export { readBitbucketIntegrationConfig, readBitbucketIntegrationConfigs, diff --git a/packages/integration/src/github/GitHubIntegration.ts b/packages/integration/src/github/GitHubIntegration.ts index c103597d74..c60ab462c9 100644 --- a/packages/integration/src/github/GitHubIntegration.ts +++ b/packages/integration/src/github/GitHubIntegration.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { basicIntegrations } from '../helpers'; +import { basicIntegrations, defaultScmResolveUrl } from '../helpers'; import { ScmIntegration, ScmIntegrationsFactory } from '../types'; import { GitHubIntegrationConfig, @@ -45,4 +45,8 @@ export class GitHubIntegration implements ScmIntegration { get config(): GitHubIntegrationConfig { return this.integrationConfig; } + + resolveUrl(options: { url: string; base: string }): string { + return defaultScmResolveUrl(options); + } } diff --git a/packages/integration/src/github/index.ts b/packages/integration/src/github/index.ts index 6491e8dcc5..99d2f56d0f 100644 --- a/packages/integration/src/github/index.ts +++ b/packages/integration/src/github/index.ts @@ -21,3 +21,4 @@ export { export type { GitHubIntegrationConfig } from './config'; export { getGitHubFileFetchUrl, getGitHubRequestOptions } from './core'; export { GithubCredentialsProvider } from './GithubCredentialsProvider'; +export { GitHubIntegration } from './GitHubIntegration'; diff --git a/packages/integration/src/gitlab/GitLabIntegration.test.ts b/packages/integration/src/gitlab/GitLabIntegration.test.ts index 8814e33302..6faae3a5e7 100644 --- a/packages/integration/src/gitlab/GitLabIntegration.test.ts +++ b/packages/integration/src/gitlab/GitLabIntegration.test.ts @@ -26,6 +26,8 @@ describe('GitLabIntegration', () => { { host: 'h.com', token: 't', + apiBaseUrl: 'https://h.com/api/v4', + baseUrl: 'https://h.com', }, ], }, diff --git a/packages/integration/src/gitlab/GitLabIntegration.ts b/packages/integration/src/gitlab/GitLabIntegration.ts index d939917366..131825edb5 100644 --- a/packages/integration/src/gitlab/GitLabIntegration.ts +++ b/packages/integration/src/gitlab/GitLabIntegration.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { basicIntegrations } from '../helpers'; +import { basicIntegrations, defaultScmResolveUrl } from '../helpers'; import { ScmIntegration, ScmIntegrationsFactory } from '../types'; import { GitLabIntegrationConfig, @@ -45,4 +45,8 @@ export class GitLabIntegration implements ScmIntegration { get config(): GitLabIntegrationConfig { return this.integrationConfig; } + + resolveUrl(options: { url: string; base: string }): string { + return defaultScmResolveUrl(options); + } } diff --git a/packages/integration/src/gitlab/config.test.ts b/packages/integration/src/gitlab/config.test.ts index 303b4bd270..bfa59d001e 100644 --- a/packages/integration/src/gitlab/config.test.ts +++ b/packages/integration/src/gitlab/config.test.ts @@ -31,6 +31,7 @@ describe('readGitLabIntegrationConfig', () => { buildConfig({ host: 'a.com', token: 't', + apiBaseUrl: 'https://a.com', baseUrl: 'https://baseurl.for.me/gitlab', }), ); @@ -38,12 +39,15 @@ describe('readGitLabIntegrationConfig', () => { expect(output).toEqual({ host: 'a.com', token: 't', + apiBaseUrl: 'https://a.com', baseUrl: 'https://baseurl.for.me/gitlab', }); }); it('inserts the defaults if missing', () => { - const output = readGitLabIntegrationConfig(buildConfig({})); + const output = readGitLabIntegrationConfig( + buildConfig({ host: 'gitlab.com' }), + ); expect(output).toEqual({ host: 'gitlab.com', apiBaseUrl: 'https://gitlab.com/api/v4', @@ -88,12 +92,15 @@ describe('readGitLabIntegrationConfigs', () => { { host: 'a.com', token: 't', + apiBaseUrl: 'https://a.com/api/v4', + baseUrl: 'https://a.com', }, ]), ); expect(output).toContainEqual({ host: 'a.com', token: 't', + apiBaseUrl: 'https://a.com/api/v4', baseUrl: 'https://a.com', }); }); @@ -104,6 +111,7 @@ describe('readGitLabIntegrationConfigs', () => { { host: 'gitlab.com', apiBaseUrl: 'https://gitlab.com/api/v4', + baseUrl: 'https://gitlab.com', }, ]); }); diff --git a/packages/integration/src/gitlab/config.ts b/packages/integration/src/gitlab/config.ts index 47269b4c32..cf717e08d2 100644 --- a/packages/integration/src/gitlab/config.ts +++ b/packages/integration/src/gitlab/config.ts @@ -15,7 +15,7 @@ */ import { Config } from '@backstage/config'; -import { isValidHost } from '../helpers'; +import { isValidHost, isValidUrl } from '../helpers'; const GITLAB_HOST = 'gitlab.com'; const GITLAB_API_BASE_URL = 'https://gitlab.com/api/v4'; @@ -38,7 +38,7 @@ export type GitLabIntegrationConfig = { * The API will always be preferred if both its base URL and a token are * present. */ - apiBaseUrl?: string; + apiBaseUrl: string; /** * The authorization token to use for requests this provider. @@ -53,7 +53,7 @@ export type GitLabIntegrationConfig = { * * If no baseUrl is provided, it will default to https://${host} */ - baseUrl?: string; + baseUrl: string; }; /** @@ -64,16 +64,10 @@ export type GitLabIntegrationConfig = { export function readGitLabIntegrationConfig( config: Config, ): GitLabIntegrationConfig { - const host = config.getOptionalString('host') ?? GITLAB_HOST; + const host = config.getString('host'); let apiBaseUrl = config.getOptionalString('apiBaseUrl'); const token = config.getOptionalString('token'); - const baseUrl = config.getOptionalString('baseUrl') ?? `https://${host}`; - - if (!isValidHost(host)) { - throw new Error( - `Invalid GitLab integration config, '${host}' is not a valid host`, - ); - } + let baseUrl = config.getOptionalString('baseUrl'); if (apiBaseUrl) { apiBaseUrl = apiBaseUrl.replace(/\/+$/, ''); @@ -81,6 +75,30 @@ export function readGitLabIntegrationConfig( apiBaseUrl = GITLAB_API_BASE_URL; } + if (baseUrl) { + baseUrl = baseUrl.replace(/\/+$/, ''); + } else { + baseUrl = `https://${host}`; + } + + if (host.includes(':')) { + throw new Error( + `Invalid GitLab integration config, host '${host}' should just be the host name (e.g. "github.com"), not a URL`, + ); + } else if (!isValidHost(host)) { + throw new Error( + `Invalid GitLab integration config, '${host}' is not a valid host`, + ); + } else if (!apiBaseUrl || !isValidUrl(apiBaseUrl)) { + throw new Error( + `Invalid GitLab integration config, '${apiBaseUrl}' is not a valid apiBaseUrl`, + ); + } else if (!isValidUrl(baseUrl)) { + throw new Error( + `Invalid GitLab integration config, '${baseUrl}' is not a valid baseUrl`, + ); + } + return { host, token, apiBaseUrl, baseUrl }; } @@ -99,7 +117,11 @@ 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, apiBaseUrl: GITLAB_API_BASE_URL }); + result.push({ + host: GITLAB_HOST, + apiBaseUrl: GITLAB_API_BASE_URL, + baseUrl: `https://${GITLAB_HOST}`, + }); } return result; diff --git a/packages/integration/src/gitlab/core.test.ts b/packages/integration/src/gitlab/core.test.ts index 2b4713604c..6836854eae 100644 --- a/packages/integration/src/gitlab/core.test.ts +++ b/packages/integration/src/gitlab/core.test.ts @@ -37,10 +37,14 @@ describe('gitlab core', () => { const configWithToken: GitLabIntegrationConfig = { host: 'g.com', token: '0123456789', + apiBaseUrl: '', + baseUrl: '', }; const configWithNoToken: GitLabIntegrationConfig = { host: 'g.com', + apiBaseUrl: '', + baseUrl: '', }; describe('getGitLabFileFetchUrl', () => { diff --git a/packages/integration/src/gitlab/index.ts b/packages/integration/src/gitlab/index.ts index 8dc4e90764..8886e0e3ce 100644 --- a/packages/integration/src/gitlab/index.ts +++ b/packages/integration/src/gitlab/index.ts @@ -20,3 +20,4 @@ export { } from './config'; export type { GitLabIntegrationConfig } from './config'; export { getGitLabFileFetchUrl, getGitLabRequestOptions } from './core'; +export { GitLabIntegration } from './GitLabIntegration'; diff --git a/packages/integration/src/helpers.test.ts b/packages/integration/src/helpers.test.ts index 9f4c531ba6..2899f2ed98 100644 --- a/packages/integration/src/helpers.test.ts +++ b/packages/integration/src/helpers.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { isValidHost } from './helpers'; +import { defaultScmResolveUrl, isValidHost } from './helpers'; describe('isValidHost', () => { it.each([ @@ -51,3 +51,69 @@ describe('isValidHost', () => { expect(isValidHost(str)).toBe(expected); }); }); + +describe('defaultScmResolveUrl', () => { + it('works for relative paths and retains query params', () => { + expect( + defaultScmResolveUrl({ + url: './b.yaml', + base: + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml', + }), + ).toBe( + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/b.yaml', + ); + + expect( + defaultScmResolveUrl({ + url: './b.yaml', + base: + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml?at=master', + }), + ).toBe( + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/b.yaml?at=master', + ); + + expect( + defaultScmResolveUrl({ + url: 'b.yaml', + base: + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml', + }), + ).toBe( + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/b.yaml', + ); + }); + + it('works for absolute paths and retains query params', () => { + expect( + defaultScmResolveUrl({ + url: '/other/b.yaml', + base: + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml', + }), + ).toBe( + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/other/b.yaml', + ); + + expect( + defaultScmResolveUrl({ + url: '/other/b.yaml', + base: + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml?at=master', + }), + ).toBe( + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/other/b.yaml?at=master', + ); + }); + + it('works for full urls and throws away query params', () => { + expect( + defaultScmResolveUrl({ + url: 'https://b.com/b.yaml', + base: + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml?at=master', + }), + ).toBe('https://b.com/b.yaml'); + }); +}); diff --git a/packages/integration/src/helpers.ts b/packages/integration/src/helpers.ts index cc1c59a238..c09f808011 100644 --- a/packages/integration/src/helpers.ts +++ b/packages/integration/src/helpers.ts @@ -14,13 +14,25 @@ * limitations under the License. */ +import parseGitUrl from 'git-url-parse'; import { ScmIntegration, ScmIntegrationsGroup } from './types'; -/** Checks whether the given url is a valid host */ -export function isValidHost(url: string): boolean { +/** Checks whether the given argument is a valid URL hostname */ +export function isValidHost(host: string): boolean { const check = new URL('http://example.com'); - check.host = url; - return check.host === url; + check.host = host; + return check.host === host; +} + +/** Checks whether the given argument is a valid URL */ +export function isValidUrl(url: string): boolean { + try { + // eslint-disable-next-line no-new + new URL(url); + return true; + } catch { + return false; + } } export function basicIntegrations( @@ -40,3 +52,43 @@ export function basicIntegrations( }, }; } + +/** + * Default implementation of ScmIntegration.resolveUrl, that only works with + * URL pathname based providers. + */ +export function defaultScmResolveUrl(options: { + url: string; + base: string; +}): string { + const { url, base } = options; + + // If it is a fully qualified URL - then return it verbatim + try { + // eslint-disable-next-line no-new + new URL(url); + return url; + } catch { + // ignore intentionally + } + + let updated: URL; + + if (url.startsWith('/')) { + // If it is an absolute path, move relative to the repo root + const { filepath } = parseGitUrl(base); + updated = new URL(base); + const repoRootPath = updated.pathname + .substring(0, updated.pathname.length - filepath.length) + .replace(/\/+$/, ''); + updated.pathname = `${repoRootPath}${url}`; + } else { + // For relative URLs, just let the default URL constructor handle the + // resolving. Note that this essentially will treat the last segment of the + // base as a file - NOT a folder - unless the url ends in a slash. + updated = new URL(url, base); + } + + updated.search = new URL(base).search; + return updated.toString(); +} diff --git a/packages/integration/src/index.ts b/packages/integration/src/index.ts index fdcd7da676..c39608fe8e 100644 --- a/packages/integration/src/index.ts +++ b/packages/integration/src/index.ts @@ -18,5 +18,6 @@ export * from './azure'; export * from './bitbucket'; export * from './github'; export * from './gitlab'; +export { defaultScmResolveUrl } from './helpers'; export { ScmIntegrations } from './ScmIntegrations'; export type { ScmIntegration, ScmIntegrationRegistry } from './types'; diff --git a/packages/integration/src/types.ts b/packages/integration/src/types.ts index 7d5b0fbe74..c63a323dc3 100644 --- a/packages/integration/src/types.ts +++ b/packages/integration/src/types.ts @@ -36,16 +36,21 @@ export interface ScmIntegration { title: string; /** - * Works like the two-argument form of the URL constructor, resolving an - * absolute or relative URL in relation to a base URL. + * Resolves an absolute or relative URL in relation to a base URL. * - * If this method is not implemented, the URL constructor is used instead for - * URLs that match this integration. + * This method is adapted for use within SCM systems, so relative URLs are + * within the context of the root of the hierarchy pointed to by the base + * URL. + * + * For example, if the base URL is `/folder/a.yaml`, i.e. + * within the file tree of a certain repo, an absolute path of `/b.yaml` does + * not resolve to `https://hostname/b.yaml` but rather to + * `/b.yaml` inside the file tree of that same repo. * * @param options.url The (absolute or relative) URL or path to resolve * @param options.base The base URL onto which this resolution happens */ - resolveUrl?(options: { url: string; base: string }): string; + resolveUrl(options: { url: string; base: string }): string; } /** @@ -83,8 +88,16 @@ export interface ScmIntegrationRegistry gitlab: ScmIntegrationsGroup; /** - * Works like the two-argument form of the URL constructor, resolving an - * absolute or relative URL in relation to a base URL. + * Resolves an absolute or relative URL in relation to a base URL. + * + * This method is adapted for use within SCM systems, so relative URLs are + * within the context of the root of the hierarchy pointed to by the base + * URL. + * + * For example, if the base URL is `/folder/a.yaml`, i.e. + * within the file tree of a certain repo, an absolute path of `/b.yaml` does + * not resolve to `https://hostname/b.yaml` but rather to + * `/b.yaml` inside the file tree of that same repo. * * @param options.url The (absolute or relative) URL or path to resolve * @param options.base The base URL onto which this resolution happens diff --git a/packages/techdocs-common/CHANGELOG.md b/packages/techdocs-common/CHANGELOG.md index baf7a380a2..9f2a8c0be4 100644 --- a/packages/techdocs-common/CHANGELOG.md +++ b/packages/techdocs-common/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/techdocs-common +## 0.4.0 + +### Minor Changes + +- 08142b256: URL Preparer will now use proper etag based caching introduced in https://github.com/backstage/backstage/pull/4120. Previously, builds used to be cached for 30 minutes. + +### Patch Changes + +- 77ad0003a: Revert AWS SDK version to v2 +- 08142b256: TechDocs will throw warning in backend logs when legacy git preparer or dir preparer is used to preparer docs. Migrate to URL Preparer by updating `backstage.io/techdocs-ref` annotation to be prefixed with `url:`. + Detailed docs are here https://backstage.io/docs/features/techdocs/how-to-guides#how-to-use-url-reader-in-techdocs-prepare-step + See benefits and reason for doing so https://github.com/backstage/backstage/issues/4409 +- Updated dependencies [ffffea8e6] +- Updated dependencies [82b2c11b6] +- Updated dependencies [965e200c6] +- Updated dependencies [ffffea8e6] +- Updated dependencies [5a5163519] + - @backstage/backend-common@0.5.3 + - @backstage/integration@0.4.0 + ## 0.3.7 ### Patch Changes diff --git a/packages/techdocs-common/__mocks__/@aws-sdk/client-s3.ts b/packages/techdocs-common/__mocks__/aws-sdk.ts similarity index 52% rename from packages/techdocs-common/__mocks__/@aws-sdk/client-s3.ts rename to packages/techdocs-common/__mocks__/aws-sdk.ts index 2d2581a6cd..987b3d2800 100644 --- a/packages/techdocs-common/__mocks__/@aws-sdk/client-s3.ts +++ b/packages/techdocs-common/__mocks__/aws-sdk.ts @@ -13,49 +13,74 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type { S3ClientConfig } from '@aws-sdk/client-s3'; +import type { S3 as S3Types } from 'aws-sdk'; import { EventEmitter } from 'events'; import fs from 'fs'; export class S3 { private readonly options; - constructor(options: S3ClientConfig) { + constructor(options: S3Types.ClientConfiguration) { this.options = options; } headObject({ Key }: { Key: string }) { + return { + promise: () => this.checkFileExists(Key), + }; + } + + getObject({ Key }: { Key: string }) { + return { + promise: () => this.checkFileExists(Key), + createReadStream: () => { + const emitter = new EventEmitter(); + process.nextTick(() => { + if (fs.existsSync(Key)) { + emitter.emit('data', Buffer.from(fs.readFileSync(Key))); + } else { + emitter.emit( + 'error', + new Error(`The file ${Key} doest not exist !`), + ); + } + emitter.emit('end'); + }); + return emitter; + }, + }; + } + + checkFileExists(Key: string) { return new Promise((resolve, reject) => { if (fs.existsSync(Key)) { resolve(''); } else { - reject({ message: `The file ${Key} doest not exist.` }); - } - }); - } - - getObject({ Key }: { Key: string }) { - return new Promise((resolve, reject) => { - if (fs.existsSync(Key)) { - const emitter = new EventEmitter(); - process.nextTick(() => { - emitter.emit('data', Buffer.from(fs.readFileSync(Key))); - emitter.emit('end'); - }); - resolve({ - Body: emitter, - }); - } else { - reject({ message: `The file ${Key} doest not exist.` }); + reject({ message: 'The object doest not exist !' }); } }); } headBucket() { - return ''; + return new Promise(resolve => { + resolve(''); + }); } - putObject() { - return ''; + upload({ Key }: { Key: string }) { + return { + promise: () => + new Promise((resolve, reject) => { + if (!fs.existsSync(Key)) { + reject(''); + } else { + resolve(''); + } + }), + }; } } + +export default { + S3, +}; diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index 9b0f7fbf9f..f1236cd6d5 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/techdocs-common", "description": "Common functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", - "version": "0.3.7", + "version": "0.4.0", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -36,16 +36,16 @@ "url": "https://github.com/backstage/backstage/issues" }, "dependencies": { - "@aws-sdk/client-s3": "^3.1.0", "@azure/identity": "^1.2.2", "@azure/storage-blob": "^12.4.0", - "@backstage/backend-common": "^0.5.2", + "@backstage/backend-common": "^0.5.3", "@backstage/catalog-model": "^0.7.1", "@backstage/config": "^0.1.2", - "@backstage/integration": "^0.3.2", + "@backstage/integration": "^0.4.0", "@google-cloud/storage": "^5.6.0", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", + "aws-sdk": "^2.840.0", "cross-fetch": "^3.0.6", "dockerode": "^3.2.1", "express": "^4.17.1", @@ -60,7 +60,6 @@ "winston": "^3.2.1" }, "devDependencies": { - "@aws-sdk/types": "3.1.0", "@backstage/cli": "^0.6.0", "@types/fs-extra": "^9.0.5", "@types/git-url-parse": "^9.0.0", diff --git a/packages/techdocs-common/src/helpers.test.ts b/packages/techdocs-common/src/helpers.test.ts index eba1386b11..6520f0f8e4 100644 --- a/packages/techdocs-common/src/helpers.test.ts +++ b/packages/techdocs-common/src/helpers.test.ts @@ -139,7 +139,7 @@ describe('getDocFilesFromRepository', () => { archive: async () => { return Readable.from(''); }, - etag: '', + etag: 'etag123abc', }; } @@ -156,6 +156,7 @@ describe('getDocFilesFromRepository', () => { mockEntityWithAnnotation, ); - expect(output).toBe('/tmp/testfolder'); + expect(output.preparedDir).toBe('/tmp/testfolder'); + expect(output.etag).toBe('etag123abc'); }); }); diff --git a/packages/techdocs-common/src/helpers.ts b/packages/techdocs-common/src/helpers.ts index f70b0f3840..beff0b2a6e 100644 --- a/packages/techdocs-common/src/helpers.ts +++ b/packages/techdocs-common/src/helpers.ts @@ -14,17 +14,17 @@ * limitations under the License. */ -import os from 'os'; -import path from 'path'; -import parseGitUrl from 'git-url-parse'; -import fs from 'fs-extra'; -import { InputError, UrlReader, Git } from '@backstage/backend-common'; +import { Git, InputError, UrlReader } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; +import fs from 'fs-extra'; +import parseGitUrl from 'git-url-parse'; +import os from 'os'; +import path from 'path'; +import { Logger } from 'winston'; import { getDefaultBranch } from './default-branch'; import { getGitRepoType, getTokenForGitRepo } from './git-auth'; -import { RemoteProtocol } from './stages/prepare/types'; -import { Logger } from 'winston'; +import { PreparerResponse, RemoteProtocol } from './stages/prepare/types'; export type ParsedLocationAnnotation = { type: RemoteProtocol; @@ -196,16 +196,9 @@ export const checkoutGitRepository = async ( }; export const getLastCommitTimestamp = async ( - repositoryUrl: string, - config: Config, + repositoryLocation: string, logger: Logger, ): Promise => { - const repositoryLocation = await checkoutGitRepository( - repositoryUrl, - config, - logger, - ); - const git = Git.fromAuth({ logger }); const sha = await git.resolveRef({ dir: repositoryLocation, ref: 'HEAD' }); const commit = await git.readCommit({ dir: repositoryLocation, sha }); @@ -216,13 +209,22 @@ export const getLastCommitTimestamp = async ( export const getDocFilesFromRepository = async ( reader: UrlReader, entity: Entity, -): Promise => { + opts?: { etag?: string; logger?: Logger }, +): Promise => { const { target } = parseReferenceAnnotation( 'backstage.io/techdocs-ref', entity, ); - const response = await reader.readTree(target); + opts?.logger?.info(`Reading files from ${target}`); + // readTree will throw NotModifiedError if etag has not changed. + const readTreeResponse = await reader.readTree(target, { etag: opts?.etag }); + const preparedDir = await readTreeResponse.dir(); - return await response.dir(); + opts?.logger?.info(`Tree downloaded and stored at ${preparedDir}`); + + return { + preparedDir, + etag: readTreeResponse.etag, + }; }; diff --git a/packages/techdocs-common/src/stages/prepare/commonGit.test.ts b/packages/techdocs-common/src/stages/prepare/commonGit.test.ts index 704cd23daa..dcc06313bd 100644 --- a/packages/techdocs-common/src/stages/prepare/commonGit.test.ts +++ b/packages/techdocs-common/src/stages/prepare/commonGit.test.ts @@ -16,8 +16,8 @@ import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; -import { CommonGitPreparer } from './commonGit'; import { checkoutGitRepository } from '../../helpers'; +import { CommonGitPreparer } from './commonGit'; function normalizePath(path: string) { return path @@ -29,6 +29,7 @@ function normalizePath(path: string) { jest.mock('../../helpers', () => ({ ...jest.requireActual<{}>('../../helpers'), checkoutGitRepository: jest.fn(() => '/tmp/backstage-repo/org/name/branch'), + getLastCommitTimestamp: jest.fn(() => 12345678), })); const createMockEntity = (annotations = {}) => { @@ -57,9 +58,9 @@ describe('commonGit preparer', () => { 'github:https://github.com/backstage/backstage/blob/master/plugins/techdocs-backend/examples/documented-component', }); - const tempDocsPath = await preparer.prepare(mockEntity); + const { preparedDir } = await preparer.prepare(mockEntity); expect(checkoutGitRepository).toHaveBeenCalledTimes(1); - expect(normalizePath(tempDocsPath)).toEqual( + expect(normalizePath(preparedDir)).toEqual( '/tmp/backstage-repo/org/name/branch/plugins/techdocs-backend/examples/documented-component', ); }); @@ -72,9 +73,9 @@ describe('commonGit preparer', () => { 'gitlab:https://gitlab.com/xesjkeee/go-logger/blob/master/catalog-info.yaml', }); - const tempDocsPath = await preparer.prepare(mockEntity); + const { preparedDir } = await preparer.prepare(mockEntity); expect(checkoutGitRepository).toHaveBeenCalledTimes(2); - expect(normalizePath(tempDocsPath)).toEqual( + expect(normalizePath(preparedDir)).toEqual( '/tmp/backstage-repo/org/name/branch/catalog-info.yaml', ); }); @@ -87,9 +88,9 @@ describe('commonGit preparer', () => { 'azure/api:https://dev.azure.com/backstage-org/backstage-project/_git/template-repo?path=%2Ftemplate.yaml', }); - const tempDocsPath = await preparer.prepare(mockEntity); + const { preparedDir } = await preparer.prepare(mockEntity); expect(checkoutGitRepository).toHaveBeenCalledTimes(3); - expect(normalizePath(tempDocsPath)).toEqual( + expect(normalizePath(preparedDir)).toEqual( '/tmp/backstage-repo/org/name/branch/template.yaml', ); }); diff --git a/packages/techdocs-common/src/stages/prepare/commonGit.ts b/packages/techdocs-common/src/stages/prepare/commonGit.ts index 7eac07d76f..46b96675f3 100644 --- a/packages/techdocs-common/src/stages/prepare/commonGit.ts +++ b/packages/techdocs-common/src/stages/prepare/commonGit.ts @@ -13,14 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import path from 'path'; -import parseGitUrl from 'git-url-parse'; +import { NotModifiedError } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { PreparerBase } from './types'; -import { parseReferenceAnnotation, checkoutGitRepository } from '../../helpers'; - +import parseGitUrl from 'git-url-parse'; +import path from 'path'; import { Logger } from 'winston'; +import { + checkoutGitRepository, + getLastCommitTimestamp, + parseReferenceAnnotation, +} from '../../helpers'; +import { PreparerBase, PreparerResponse } from './types'; export class CommonGitPreparer implements PreparerBase { private readonly config: Config; @@ -31,23 +35,45 @@ export class CommonGitPreparer implements PreparerBase { this.logger = logger; } - async prepare(entity: Entity): Promise { + async prepare( + entity: Entity, + options?: { etag?: string }, + ): Promise { + this.logger.warn( + 'You are using the legacy git preparer in TechDocs which will be removed in near future (March 2021). ' + + 'Migrate to URL reader by updating `backstage.io/techdocs-ref` annotation in `catalog-info.yaml` ' + + 'to be prefixed with `url:`. Read the migration guide and benefits at https://github.com/backstage/backstage/issues/4409 ', + ); + const { target } = parseReferenceAnnotation( 'backstage.io/techdocs-ref', entity, ); try { + // Update repository or do a fresh clone. const repoPath = await checkoutGitRepository( target, this.config, this.logger, ); - const parsedGitLocation = parseGitUrl(target); + // Check if etag has changed for cache invalidation. + const etag = await getLastCommitTimestamp(repoPath, this.logger); + if (options?.etag === etag.toString()) { + throw new NotModifiedError(); + } - return path.join(repoPath, parsedGitLocation.filepath); + const parsedGitLocation = parseGitUrl(target); + return { + preparedDir: path.join(repoPath, parsedGitLocation.filepath), + etag: etag.toString(), + }; } catch (error) { - this.logger.debug(`Repo checkout failed with error ${error.message}`); + if (error instanceof NotModifiedError) { + this.logger.debug(`Cache is valid for etag ${options?.etag}`); + } else { + this.logger.debug(`Repo checkout failed with error ${error.message}`); + } throw error; } } diff --git a/packages/techdocs-common/src/stages/prepare/dir.test.ts b/packages/techdocs-common/src/stages/prepare/dir.test.ts index cd8d358afe..67fa0dd21f 100644 --- a/packages/techdocs-common/src/stages/prepare/dir.test.ts +++ b/packages/techdocs-common/src/stages/prepare/dir.test.ts @@ -15,8 +15,8 @@ */ import { getVoidLogger, UrlReader } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; -import { DirectoryPreparer } from './dir'; import { checkoutGitRepository } from '../../helpers'; +import { DirectoryPreparer } from './dir'; function normalizePath(path: string) { return path @@ -28,6 +28,7 @@ function normalizePath(path: string) { jest.mock('../../helpers', () => ({ ...jest.requireActual<{}>('../../helpers'), checkoutGitRepository: jest.fn(() => '/tmp/backstage-repo/org/name/branch/'), + getLastCommitTimestamp: jest.fn(() => 12345678), })); const logger = getVoidLogger(); @@ -66,9 +67,8 @@ describe('directory preparer', () => { 'backstage.io/techdocs-ref': 'dir:./our-documentation', }); - expect(normalizePath(await directoryPreparer.prepare(mockEntity))).toEqual( - '/directory/our-documentation', - ); + const { preparedDir } = await directoryPreparer.prepare(mockEntity); + expect(normalizePath(preparedDir)).toEqual('/directory/our-documentation'); }); it('should merge managed-by-location and techdocs-ref when techdocs-ref is absolute', async () => { @@ -84,9 +84,8 @@ describe('directory preparer', () => { 'backstage.io/techdocs-ref': 'dir:/our-documentation/techdocs', }); - expect(normalizePath(await directoryPreparer.prepare(mockEntity))).toEqual( - '/our-documentation/techdocs', - ); + const { preparedDir } = await directoryPreparer.prepare(mockEntity); + expect(normalizePath(preparedDir)).toEqual('/our-documentation/techdocs'); }); it('should merge managed-by-location and techdocs-ref when managed-by-location is a git repository', async () => { @@ -102,7 +101,8 @@ describe('directory preparer', () => { 'backstage.io/techdocs-ref': 'dir:./docs', }); - expect(normalizePath(await directoryPreparer.prepare(mockEntity))).toEqual( + const { preparedDir } = await directoryPreparer.prepare(mockEntity); + expect(normalizePath(preparedDir)).toEqual( '/tmp/backstage-repo/org/name/branch/docs', ); expect(checkoutGitRepository).toHaveBeenCalledTimes(1); diff --git a/packages/techdocs-common/src/stages/prepare/dir.ts b/packages/techdocs-common/src/stages/prepare/dir.ts index ab277fda9a..08832938df 100644 --- a/packages/techdocs-common/src/stages/prepare/dir.ts +++ b/packages/techdocs-common/src/stages/prepare/dir.ts @@ -13,14 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { PreparerBase } from './types'; +import { + InputError, + NotModifiedError, + UrlReader, +} from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import path from 'path'; -import { parseReferenceAnnotation, checkoutGitRepository } from '../../helpers'; -import { UrlReader, InputError } from '@backstage/backend-common'; import parseGitUrl from 'git-url-parse'; +import path from 'path'; import { Logger } from 'winston'; +import { + checkoutGitRepository, + getLastCommitTimestamp, + parseReferenceAnnotation, +} from '../../helpers'; +import { PreparerBase, PreparerResponse } from './types'; export class DirectoryPreparer implements PreparerBase { constructor( @@ -33,7 +41,10 @@ export class DirectoryPreparer implements PreparerBase { this.reader = reader; } - private async resolveManagedByLocationToDir(entity: Entity) { + private async resolveManagedByLocationToDir( + entity: Entity, + options?: { etag?: string }, + ): Promise { const { type, target } = parseReferenceAnnotation( 'backstage.io/managed-by-location', entity, @@ -44,8 +55,14 @@ export class DirectoryPreparer implements PreparerBase { ); switch (type) { case 'url': { - const response = await this.reader.readTree(target); - return await response.dir(); + const response = await this.reader.readTree(target, { + etag: options?.etag, + }); + const preparedDir = await response.dir(); + return { + preparedDir, + etag: response.etag, + }; } case 'github': case 'gitlab': @@ -57,29 +74,47 @@ export class DirectoryPreparer implements PreparerBase { this.logger, ); - return path.dirname( - path.join(repoLocation, parsedGitLocation.filepath), - ); + // Check if etag has changed for cache invalidation. + const etag = await getLastCommitTimestamp(repoLocation, this.logger); + if (options?.etag === etag.toString()) { + throw new NotModifiedError(); + } + return { + preparedDir: path.dirname( + path.join(repoLocation, parsedGitLocation.filepath), + ), + etag: etag.toString(), + }; } case 'file': - return path.dirname(target); + return { + preparedDir: path.dirname(target), + // Instead of supporting caching on local sources, use techdocs-cli for local development and debugging. + etag: '', + }; default: throw new InputError(`Unable to resolve location type ${type}`); } } - async prepare(entity: Entity): Promise { + async prepare(entity: Entity): Promise { + this.logger.warn( + 'You are using the legacy dir preparer in TechDocs which will be removed in near future (March 2021). ' + + 'Migrate to URL reader by updating `backstage.io/techdocs-ref` annotation in `catalog-info.yaml` ' + + 'to be prefixed with `url:`. Read the migration guide and benefits at https://github.com/backstage/backstage/issues/4409 ', + ); + const { target } = parseReferenceAnnotation( 'backstage.io/techdocs-ref', entity, ); - const managedByLocationDirectory = await this.resolveManagedByLocationToDir( - entity, - ); + // This will throw NotModified error if etag has not changed. + const response = await this.resolveManagedByLocationToDir(entity); - return new Promise(resolve => { - resolve(path.resolve(managedByLocationDirectory, target)); - }); + return { + preparedDir: path.resolve(response.preparedDir, target), + etag: response.etag, + }; } } diff --git a/packages/techdocs-common/src/stages/prepare/types.ts b/packages/techdocs-common/src/stages/prepare/types.ts index 97e75306c3..96510e8eec 100644 --- a/packages/techdocs-common/src/stages/prepare/types.ts +++ b/packages/techdocs-common/src/stages/prepare/types.ts @@ -16,13 +16,31 @@ import type { Entity } from '@backstage/catalog-model'; import { Logger } from 'winston'; +export type PreparerResponse = { + /** + * The path to directory where the tree is downloaded. + */ + preparedDir: string; + /** + * A unique identifer of the tree blob, usually the commit SHA or etag from the target. + */ + etag: string; +}; + export type PreparerBase = { /** * Given an Entity definition from the Service Catalog, go and prepare a directory - * with contents from the location in temporary storage and return the path + * with contents from the location in temporary storage and return the path. + * * @param entity The entity from the Service Catalog + * @param options.etag (Optional) If etag is provider, it will be used to check if the target has + * updated since the last build. + * @throws {NotModifiedError} when the prepared directory has not been changed since the last build. */ - prepare(entity: Entity, opts?: { logger: Logger }): Promise; + prepare( + entity: Entity, + options?: { logger?: Logger; etag?: string }, + ): Promise; }; export type PreparerBuilder = { @@ -30,10 +48,14 @@ export type PreparerBuilder = { get(entity: Entity): PreparerBase; }; +/** + * Everything except `url` will be deprecated. + * Read more https://github.com/backstage/backstage/issues/4409 + */ export type RemoteProtocol = + | 'url' | 'dir' | 'github' | 'gitlab' | 'file' - | 'azure/api' - | 'url'; + | 'azure/api'; diff --git a/packages/techdocs-common/src/stages/prepare/url.ts b/packages/techdocs-common/src/stages/prepare/url.ts index b938c6b5bc..9e4715a4ee 100644 --- a/packages/techdocs-common/src/stages/prepare/url.ts +++ b/packages/techdocs-common/src/stages/prepare/url.ts @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Logger } from 'winston'; +import { NotModifiedError, UrlReader } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; -import { UrlReader } from '@backstage/backend-common'; -import { PreparerBase } from './types'; +import { Logger } from 'winston'; import { getDocFilesFromRepository } from '../../helpers'; +import { PreparerBase, PreparerResponse } from './types'; export class UrlPreparer implements PreparerBase { private readonly logger: Logger; @@ -28,13 +28,25 @@ export class UrlPreparer implements PreparerBase { this.reader = reader; } - async prepare(entity: Entity): Promise { + async prepare( + entity: Entity, + options?: { etag?: string }, + ): Promise { try { - return getDocFilesFromRepository(this.reader, entity); + return await getDocFilesFromRepository(this.reader, entity, { + etag: options?.etag, + logger: this.logger, + }); } catch (error) { - this.logger.debug( - `Unable to fetch files for building docs ${error.message}`, - ); + // NotModifiedError means that etag based cache is still valid. + if (error instanceof NotModifiedError) { + this.logger.debug(`Cache is valid for etag ${options?.etag}`); + } else { + this.logger.debug( + `Unable to fetch files for building docs ${error.message}`, + ); + } + throw error; } } diff --git a/packages/techdocs-common/src/stages/publish/awsS3.test.ts b/packages/techdocs-common/src/stages/publish/awsS3.test.ts old mode 100755 new mode 100644 index 3902aab21a..813f6b8554 --- a/packages/techdocs-common/src/stages/publish/awsS3.test.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.test.ts @@ -102,6 +102,12 @@ describe('AwsS3Publish', () => { }); it('should fail to publish a directory', async () => { + const wrongPathToGeneratedDirectory = path.join( + 'wrong', + 'path', + 'to', + 'generatedDirectory', + ); const entity = createMockEntity(); const entityRootDir = getEntityRootDir(entity); @@ -118,11 +124,13 @@ describe('AwsS3Publish', () => { await publisher .publish({ entity, - directory: '/wrong/path/to/generatedDirectory', + directory: wrongPathToGeneratedDirectory, }) .catch(error => - expect(error.message).toContain( - 'Unable to upload file(s) to AWS S3. Error Failed to read template directory', + expect(error).toEqual( + new Error( + `Unable to upload file(s) to AWS S3. Error Failed to read template directory: ENOENT, no such file or directory '${wrongPathToGeneratedDirectory}'`, + ), ), ); mockFs.restore(); @@ -198,17 +206,14 @@ describe('AwsS3Publish', () => { it('should return an error if the techdocs_metadata.json file is not present', async () => { const entityNameMock = createMockEntityName(); const entity = createMockEntity(); - const { - metadata: { name, namespace }, - kind, - } = entity; + const entityRootDir = getEntityRootDir(entity); await publisher .fetchTechDocsMetadata(entityNameMock) .catch(error => expect(error).toEqual( new Error( - `TechDocs metadata fetch failed, The file ${namespace}/${kind}/${name}/techdocs_metadata.json doest not exist.`, + `TechDocs metadata fetch failed, The file ${entityRootDir}/techdocs_metadata.json doest not exist !`, ), ), ); diff --git a/packages/techdocs-common/src/stages/publish/awsS3.ts b/packages/techdocs-common/src/stages/publish/awsS3.ts index 20e6bc424d..8eaf03eb2f 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.ts @@ -15,7 +15,8 @@ */ import path from 'path'; import express from 'express'; -import { PutObjectCommandOutput, S3 } from '@aws-sdk/client-s3'; +import aws from 'aws-sdk'; +import { ManagedUpload } from 'aws-sdk/clients/s3'; import { Logger } from 'winston'; import { Entity, EntityName } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; @@ -71,7 +72,7 @@ export class AwsS3Publish implements PublisherBase { // https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-region.html const region = config.getOptionalString('techdocs.publisher.awsS3.region'); - const storageClient = new S3({ + const storageClient = new aws.S3({ ...(credentials && accessKeyId && secretAccessKey && { @@ -113,7 +114,7 @@ export class AwsS3Publish implements PublisherBase { } constructor( - private readonly storageClient: S3, + private readonly storageClient: aws.S3, private readonly bucketName: string, private readonly logger: Logger, ) { @@ -133,7 +134,7 @@ export class AwsS3Publish implements PublisherBase { const allFilesToUpload = await getFileTreeRecursively(directory); const limiter = createLimiter(10); - const uploadPromises: Array> = []; + const uploadPromises: Array> = []; for (const filePath of allFilesToUpload) { // Remove the absolute path prefix of the source directory // Path of all files to upload, relative to the root of the source directory @@ -151,7 +152,9 @@ export class AwsS3Publish implements PublisherBase { }; // Rate limit the concurrent execution of file uploads to batches of 10 (per publish) - const uploadFile = limiter(() => this.storageClient.putObject(params)); + const uploadFile = limiter(() => + this.storageClient.upload(params).promise(), + ); uploadPromises.push(uploadFile); } await Promise.all(uploadPromises); @@ -170,34 +173,33 @@ export class AwsS3Publish implements PublisherBase { entityName: EntityName, ): Promise { try { - return await new Promise((resolve, reject) => { + return await new Promise(async (resolve, reject) => { const entityRootDir = `${entityName.namespace}/${entityName.kind}/${entityName.name}`; - this.storageClient + const stream = this.storageClient .getObject({ Bucket: this.bucketName, Key: `${entityRootDir}/techdocs_metadata.json`, }) - .then(async file => { - const techdocsMetadataJson = await streamToBuffer( - file.Body as Readable, - ); + .createReadStream(); - if (!techdocsMetadataJson) { - throw new Error( - `Unable to parse the techdocs metadata file ${entityRootDir}/techdocs_metadata.json.`, - ); - } - const techdocsMetadata = JSON5.parse( - techdocsMetadataJson.toString('utf-8'), + try { + const techdocsMetadataJson = await streamToBuffer(stream); + if (!techdocsMetadataJson) { + throw new Error( + `Unable to parse the techdocs metadata file ${entityRootDir}/techdocs_metadata.json.`, ); + } - resolve(techdocsMetadata); - }) - .catch(err => { - this.logger.error(err.message); - reject(new Error(err.message)); - }); + const techdocsMetadata = JSON5.parse( + techdocsMetadataJson.toString('utf-8'), + ); + + resolve(techdocsMetadata); + } catch (err) { + this.logger.error(err.message); + reject(new Error(err.message)); + } }); } catch (e) { throw new Error(`TechDocs metadata fetch failed, ${e.message}`); @@ -208,7 +210,7 @@ export class AwsS3Publish implements PublisherBase { * Express route middleware to serve static files on a route in techdocs-backend. */ docsRouter(): express.Handler { - return (req, res) => { + return async (req, res) => { // Trim the leading forward slash // filePath example - /default/Component/documented-component/index.html const filePath = req.path.replace(/^\//, ''); @@ -217,27 +219,22 @@ export class AwsS3Publish implements PublisherBase { const fileExtension = path.extname(filePath); const responseHeaders = getHeadersForFileExtension(fileExtension); - this.storageClient + const stream = this.storageClient .getObject({ Bucket: this.bucketName, Key: filePath }) - .then(async object => { - const fileContent = await streamToBuffer(object.Body as Readable); - if (!fileContent) { - throw new Error(`Unable to parse the file ${filePath}.`); - } + .createReadStream(); + try { + // Inject response headers + for (const [headerKey, headerValue] of Object.entries( + responseHeaders, + )) { + res.setHeader(headerKey, headerValue); + } - // Inject response headers - for (const [headerKey, headerValue] of Object.entries( - responseHeaders, - )) { - res.setHeader(headerKey, headerValue); - } - - res.send(fileContent); - }) - .catch(err => { - this.logger.warn(err.message); - res.status(404).send(err.message); - }); + res.send(await streamToBuffer(stream)); + } catch (err) { + this.logger.warn(err.message); + res.status(404).send(err.message); + } }; } @@ -248,10 +245,12 @@ export class AwsS3Publish implements PublisherBase { async hasDocsBeenGenerated(entity: Entity): Promise { try { const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`; - await this.storageClient.headObject({ - Bucket: this.bucketName, - Key: `${entityRootDir}/index.html`, - }); + await this.storageClient + .headObject({ + Bucket: this.bucketName, + Key: `${entityRootDir}/index.html`, + }) + .promise(); return Promise.resolve(true); } catch (e) { return Promise.resolve(false); diff --git a/packages/techdocs-common/src/stages/publish/local.ts b/packages/techdocs-common/src/stages/publish/local.ts index e349a2119a..ac09331dcd 100644 --- a/packages/techdocs-common/src/stages/publish/local.ts +++ b/packages/techdocs-common/src/stages/publish/local.ts @@ -88,7 +88,7 @@ export class LocalPublish implements PublisherBase { ); reject(err); } - + this.logger.info(`Published site stored at ${publishDir}`); this.discovery .getBaseUrl('techdocs') .then(techdocsApiUrl => { diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index 6d23f26691..a30b2b3f6a 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/test-utils +## 0.1.7 + +### Patch Changes + +- b51ee6ece: Added `mountedRoutes` option to `wrapInTestApp`, allowing routes to be associated to concrete paths to make `useRouteRef` usable in tested components. + ## 0.1.6 ### Patch Changes diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 5cea83fa03..5a4ad2b4a4 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils", "description": "Utilities to test Backstage plugins and apps.", - "version": "0.1.6", + "version": "0.1.7", "private": false, "publishConfig": { "access": "public", diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index 4735beb150..47379a9b18 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-api-docs +## 0.4.5 + +### Patch Changes + +- f5e564cd6: Improve display of error messages +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.4.4 ### Patch Changes diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 175b831ada..8df169c1d9 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-api-docs", - "version": "0.4.4", + "version": "0.4.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,8 +31,8 @@ "dependencies": { "@asyncapi/react-component": "^0.18.2", "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/core": "^0.6.1", + "@backstage/plugin-catalog-react": "^0.0.3", "@backstage/theme": "^0.2.3", "@material-icons/font": "^1.0.2", "@material-ui/core": "^4.11.0", @@ -50,8 +50,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index b57e0d25fb..f39dad2eb9 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-app-backend +## 0.3.7 + +### Patch Changes + +- 727f0deec: Added a new `disableConfigInjection` option, which can be used to disable the configuration injection in environments where it can't be used. +- Updated dependencies [ffffea8e6] +- Updated dependencies [82b2c11b6] +- Updated dependencies [965e200c6] +- Updated dependencies [5a5163519] + - @backstage/backend-common@0.5.3 + ## 0.3.6 ### Patch Changes diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index cca36a51f8..52ebab520d 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-backend", - "version": "0.3.6", + "version": "0.3.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,7 +29,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.5.2", + "@backstage/backend-common": "^0.5.3", "@backstage/config-loader": "^0.5.1", "@backstage/config": "^0.1.2", "@types/express": "^4.17.6", diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index e3ff6aaa43..e97d79c4ae 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,77 @@ # @backstage/plugin-auth-backend +## 0.3.0 + +### Minor Changes + +- 1deb31141: Remove undocumented scope (default) from the OIDC auth provider which was breaking some identity services. If your app relied on this scope, you can manually specify it by adding a new factory in `packages/app/src/apis.ts`: + + ``` + export const apis = [ + createApiFactory({ + api: oidcAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + OAuth2.create({ + discoveryApi, + oauthRequestApi, + provider: { + id: 'oidc', + title: 'Your Identity Provider', + icon: OAuth2Icon, + }, + defaultScopes: [ + 'default', + 'openid', + 'email', + 'offline_access', + ], + environment: configApi.getOptionalString('auth.environment'), + }), + }), + ]; + ``` + +### Patch Changes + +- 6ed2b47d6: Include Backstage identity token in requests to backend plugins. +- 07bafa248: Add configurable `scope` for oauth2 auth provider. + + Some OAuth2 providers require certain scopes to facilitate a user sign-in using the Authorization Code flow. + This change adds the optional `scope` key to auth.providers.oauth2. An example is: + + ```yaml + auth: + providers: + oauth2: + development: + clientId: + $env: DEV_OAUTH2_CLIENT_ID + clientSecret: + $env: DEV_OAUTH2_CLIENT_SECRET + authorizationUrl: + $env: DEV_OAUTH2_AUTH_URL + tokenUrl: + $env: DEV_OAUTH2_TOKEN_URL + scope: saml-login-selector openid profile email + ``` + + This tells the OAuth 2.0 AS to perform a SAML login and return OIDC information include the `profile` + and `email` claims as part of the ID Token. + +- Updated dependencies [6ed2b47d6] +- Updated dependencies [ffffea8e6] +- Updated dependencies [82b2c11b6] +- Updated dependencies [965e200c6] +- Updated dependencies [72b96e880] +- Updated dependencies [5a5163519] + - @backstage/catalog-client@0.3.6 + - @backstage/backend-common@0.5.3 + ## 0.2.12 ### Patch Changes diff --git a/plugins/auth-backend/config.d.ts b/plugins/auth-backend/config.d.ts index 84f71b52b9..9850e05235 100644 --- a/plugins/auth-backend/config.d.ts +++ b/plugins/auth-backend/config.d.ts @@ -58,7 +58,13 @@ export interface Config { development: { [key: string]: string }; }; oauth2?: { - development: { [key: string]: string }; + development: { + clientId: string; + clientSecret: string; + authorizationUrl: string; + tokenUrl: string; + scope?: string; + }; }; oidc?: { development: { [key: string]: string }; diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index e87c2b4a90..c50789d4c1 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend", - "version": "0.2.12", + "version": "0.3.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,8 +29,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.5.2", - "@backstage/catalog-client": "^0.3.5", + "@backstage/backend-common": "^0.5.3", + "@backstage/catalog-client": "^0.3.6", "@backstage/catalog-model": "^0.7.1", "@backstage/config": "^0.1.2", "@types/express": "^4.17.6", @@ -47,7 +47,7 @@ "jose": "^1.27.1", "jwt-decode": "^3.1.0", "knex": "^0.21.6", - "moment": "^2.26.0", + "luxon": "^1.25.0", "morgan": "^1.10.0", "node-cache": "^5.1.2", "openid-client": "^4.2.1", diff --git a/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts b/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts index b22ab0ecda..8896a0d158 100644 --- a/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts +++ b/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts @@ -15,8 +15,8 @@ */ import Knex from 'knex'; -import moment from 'moment'; import { DatabaseKeyStore } from './DatabaseKeyStore'; +import { DateTime } from 'luxon'; function createDB() { const knex = Knex({ @@ -51,7 +51,11 @@ describe('DatabaseKeyStore', () => { const { items } = await store.listKeys(); expect(items).toEqual([{ createdAt: expect.anything(), key }]); - expect(Math.abs(items[0].createdAt.diff(moment(), 's'))).toBeLessThan(10); + expect( + Math.abs( + DateTime.fromJSDate(items[0].createdAt).diffNow('seconds').seconds, + ), + ).toBeLessThan(10); }); it('should remove stored keys', async () => { diff --git a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts index c24cac55be..e53102e333 100644 --- a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts +++ b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts @@ -15,9 +15,9 @@ */ import Knex from 'knex'; -import { utc } from 'moment'; import { resolvePackagePath } from '@backstage/backend-common'; import { AnyJWK, KeyStore, StoredKey } from './types'; +import { DateTime } from 'luxon'; const migrationsDir = resolvePackagePath( '@backstage/plugin-auth-backend', @@ -27,7 +27,7 @@ const migrationsDir = resolvePackagePath( const TABLE = 'signing_keys'; type Row = { - created_at: Date; + created_at: Date; // row.created_at is a string after being returned from the database kid: string; key: string; }; @@ -36,6 +36,21 @@ type Options = { database: Knex; }; +const parseDate = (date: string | Date) => { + const parsedDate = + typeof date === 'string' + ? DateTime.fromSQL(date, { locale: 'UTC' }) + : DateTime.fromJSDate(date); + + if (!parsedDate.isValid) { + throw new Error( + `Failed to parse date, reason: ${parsedDate.invalidReason}, explanation: ${parsedDate.invalidExplanation}`, + ); + } + + return parsedDate.toJSDate(); +}; + export class DatabaseKeyStore implements KeyStore { static async create(options: Options): Promise { const { database } = options; @@ -66,7 +81,7 @@ export class DatabaseKeyStore implements KeyStore { return { items: rows.map(row => ({ key: JSON.parse(row.key), - createdAt: utc(row.created_at), + createdAt: parseDate(row.created_at), })), }; } diff --git a/plugins/auth-backend/src/identity/MemoryKeyStore.ts b/plugins/auth-backend/src/identity/MemoryKeyStore.ts index b35002b146..3dc1d777c9 100644 --- a/plugins/auth-backend/src/identity/MemoryKeyStore.ts +++ b/plugins/auth-backend/src/identity/MemoryKeyStore.ts @@ -14,18 +14,15 @@ * limitations under the License. */ -import { utc } from 'moment'; import { KeyStore, AnyJWK, StoredKey } from './types'; +import { DateTime } from 'luxon'; export class MemoryKeyStore implements KeyStore { - private readonly keys = new Map< - string, - { createdAt: moment.Moment; key: string } - >(); + private readonly keys = new Map(); async addKey(key: AnyJWK): Promise { this.keys.set(key.kid, { - createdAt: utc(), + createdAt: DateTime.utc().toJSDate(), key: JSON.stringify(key), }); } diff --git a/plugins/auth-backend/src/identity/TokenFactory.ts b/plugins/auth-backend/src/identity/TokenFactory.ts index 36622332e0..1c6192d5c5 100644 --- a/plugins/auth-backend/src/identity/TokenFactory.ts +++ b/plugins/auth-backend/src/identity/TokenFactory.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import moment from 'moment'; import { TokenIssuer, TokenParams, KeyStore, AnyJWK } from './types'; import { JSONWebKey, JWK, JWS } from 'jose'; import { Logger } from 'winston'; import { v4 as uuid } from 'uuid'; +import { DateTime } from 'luxon'; const MS_IN_S = 1000; @@ -52,7 +52,7 @@ export class TokenFactory implements TokenIssuer { private readonly keyStore: KeyStore; private readonly keyDurationSeconds: number; - private keyExpiry?: moment.Moment; + private keyExpiry?: Date; private privateKeyPromise?: Promise; constructor(options: Options) { @@ -90,8 +90,10 @@ export class TokenFactory implements TokenIssuer { for (const key of keys) { // Allow for a grace period of another full key duration before we remove the keys from the database - const expireAt = key.createdAt.add(3 * this.keyDurationSeconds, 's'); - if (expireAt.isBefore()) { + const expireAt = DateTime.fromJSDate(key.createdAt).plus({ + seconds: 3 * this.keyDurationSeconds, + }); + if (expireAt < DateTime.local()) { expiredKeys.push(key); } else { validKeys.push(key); @@ -117,14 +119,21 @@ export class TokenFactory implements TokenIssuer { private async getKey(): Promise { // Make sure that we only generate one key at a time if (this.privateKeyPromise) { - if (this.keyExpiry?.isAfter()) { + if ( + this.keyExpiry && + DateTime.fromJSDate(this.keyExpiry) > DateTime.local() + ) { return this.privateKeyPromise; } this.logger.info(`Signing key has expired, generating new key`); delete this.privateKeyPromise; } - this.keyExpiry = moment().add(this.keyDurationSeconds, 'seconds'); + this.keyExpiry = DateTime.utc() + .plus({ + seconds: this.keyDurationSeconds, + }) + .toJSDate(); 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', { diff --git a/plugins/auth-backend/src/identity/types.ts b/plugins/auth-backend/src/identity/types.ts index 827aba51ab..6e984d41cc 100644 --- a/plugins/auth-backend/src/identity/types.ts +++ b/plugins/auth-backend/src/identity/types.ts @@ -52,7 +52,7 @@ export type TokenIssuer = { */ export type StoredKey = { key: AnyJWK; - createdAt: moment.Moment; + createdAt: Date; }; /** diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts index ec6aa1b6ba..4d4bcaf2bf 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts @@ -38,11 +38,14 @@ describe('CatalogIdentityClient', () => { client.findUser({ annotations: { key: 'value' } }); - expect(catalogApi.getEntities).toBeCalledWith({ - filter: { - kind: 'user', - 'metadata.annotations.key': 'value', + expect(catalogApi.getEntities).toBeCalledWith( + { + filter: { + kind: 'user', + 'metadata.annotations.key': 'value', + }, }, - }); + undefined, + ); }); }); diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts index 5bc4e5de21..e1a3553394 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts @@ -37,7 +37,10 @@ export class CatalogIdentityClient { * * Throws a NotFoundError or ConflictError if 0 or multiple users are found. */ - async findUser(query: UserQuery): Promise { + async findUser( + query: UserQuery, + options?: { token?: string }, + ): Promise { const filter: Record = { kind: 'user', }; @@ -45,7 +48,7 @@ export class CatalogIdentityClient { filter[`metadata.annotations.${key}`] = value; } - const { items } = await this.catalogApi.getEntities({ filter }); + const { items } = await this.catalogApi.getEntities({ filter }, options); if (items.length !== 1) { if (items.length > 1) { diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index ae4f5fabd0..848a02bad6 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -38,6 +38,7 @@ import { PassportDoneCallback, } from '../../lib/passport'; import { AuthProviderFactory, RedirectInfo } from '../types'; +import { TokenIssuer } from '../../identity'; type PrivateInfo = { refreshToken: string; @@ -46,16 +47,19 @@ type PrivateInfo = { export type GoogleAuthProviderOptions = OAuthProviderOptions & { logger: Logger; identityClient: CatalogIdentityClient; + tokenIssuer: TokenIssuer; }; export class GoogleAuthProvider implements OAuthHandlers { private readonly _strategy: GoogleStrategy; private readonly logger: Logger; private readonly identityClient: CatalogIdentityClient; + private readonly tokenIssuer: TokenIssuer; constructor(options: GoogleAuthProviderOptions) { this.logger = options.logger; this.identityClient = options.identityClient; + this.tokenIssuer = options.tokenIssuer; // TODO: throw error if env variables not set? this._strategy = new GoogleStrategy( { @@ -150,11 +154,17 @@ export class GoogleAuthProvider implements OAuthHandlers { } try { - const user = await this.identityClient.findUser({ - annotations: { - 'google.com/email': profile.email, - }, + const token = await this.tokenIssuer.issueToken({ + claims: { sub: 'backstage.io/auth-backend' }, }); + const user = await this.identityClient.findUser( + { + annotations: { + 'google.com/email': profile.email, + }, + }, + { token }, + ); return { ...response, @@ -192,6 +202,7 @@ export const createGoogleProvider: AuthProviderFactory = ({ clientSecret, callbackUrl, logger, + tokenIssuer, identityClient: new CatalogIdentityClient({ catalogApi }), }); diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index 04cb7670f3..a44c4fb50b 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -44,6 +44,7 @@ type PrivateInfo = { export type OAuth2AuthProviderOptions = OAuthProviderOptions & { authorizationUrl: string; tokenUrl: string; + scope?: string; }; export class OAuth2AuthProvider implements OAuthHandlers { @@ -58,6 +59,7 @@ export class OAuth2AuthProvider implements OAuthHandlers { authorizationURL: options.authorizationUrl, tokenURL: options.tokenUrl, passReqToCallback: false as true, + scope: options.scope, }, ( accessToken: any, @@ -168,6 +170,7 @@ export const createOAuth2Provider: AuthProviderFactory = ({ const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; const authorizationUrl = envConfig.getString('authorizationUrl'); const tokenUrl = envConfig.getString('tokenUrl'); + const scope = envConfig.getOptionalString('scope'); const provider = new OAuth2AuthProvider({ clientId, @@ -175,6 +178,7 @@ export const createOAuth2Provider: AuthProviderFactory = ({ callbackUrl, authorizationUrl, tokenUrl, + scope, }); return OAuthAdapter.fromConfig(globalConfig, provider, { diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index 9e44a82c37..6ea7b8a8d3 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -65,7 +65,7 @@ export class OidcAuthProvider implements OAuthHandlers { return await executeRedirectStrategy(req, strategy, { accessType: 'offline', prompt: 'none', - scope: `${req.scope} default`, + scope: req.scope, state: encodeState(req.state), }); } diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index eb256d2c08..9ae261ecf3 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-catalog-backend +## 0.6.1 + +### Patch Changes + +- 77ad0003a: Revert AWS SDK version to v2 +- d2441aee3: use child logger, if provided, to log single location refresh +- fb53eb7cb: Don't respond to a request twice if an entity has not been found. +- f3fbfb452: add indices on columns referring locations(id) +- 84364b35c: Added an option to scan GitHub for repositories using a new location type `github-discovery`. + Example: + + ```yaml + type: 'github-discovery', + target: + 'https://github.com/backstage/techdocs-*/blob/master/catalog.yaml' + ``` + + You can use wildcards (`*`) as well. This will add `location` entities for each matching repository. + Currently though, you must specify the exact path of the `catalog.yaml` file in the repository. + +- 82b2c11b6: Refactored route response handling to use more explicit types and throw errors. +- Updated dependencies [ffffea8e6] +- Updated dependencies [82b2c11b6] +- Updated dependencies [965e200c6] +- Updated dependencies [ffffea8e6] +- Updated dependencies [5a5163519] + - @backstage/backend-common@0.5.3 + - @backstage/integration@0.4.0 + ## 0.6.0 ### Minor Changes diff --git a/plugins/catalog-backend/migrations/20210209121210_locations_fk_index.js b/plugins/catalog-backend/migrations/20210209121210_locations_fk_index.js new file mode 100644 index 0000000000..5591717c13 --- /dev/null +++ b/plugins/catalog-backend/migrations/20210209121210_locations_fk_index.js @@ -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. + */ + +// @ts-check + +/** + * @param {import('knex')} knex + */ +exports.up = async function up(knex) { + if (knex.client.config.client !== 'sqlite3') { + await knex.schema.alterTable('entities', table => { + table.index('location_id', 'entity_location_id_idx'); + }); + await knex.schema.alterTable('location_update_log', table => { + table.index('location_id', 'update_log_location_id_idx'); + }); + } +}; + +/** + * @param {import('knex')} knex + */ +exports.down = async function down(knex) { + if (knex.client.config.client !== 'sqlite3') { + await knex.schema.alterTable('entities', table => { + table.dropIndex([], 'entity_location_id_idx'); + }); + await knex.schema.alterTable('location_update_log', table => { + table.dropIndex([], 'update_log_location_id_idx'); + }); + } +}; diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index bb76441f3f..254898a086 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "0.6.0", + "version": "0.6.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,15 +29,15 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@aws-sdk/client-organizations": "^3.2.0", "@azure/msal-node": "^1.0.0-beta.3", - "@backstage/backend-common": "^0.5.2", + "@backstage/backend-common": "^0.5.3", "@backstage/catalog-model": "^0.7.1", "@backstage/config": "^0.1.2", - "@backstage/integration": "^0.3.2", + "@backstage/integration": "^0.4.0", "@octokit/graphql": "^4.5.8", "@types/express": "^4.17.6", "@types/ldapjs": "^1.0.9", + "aws-sdk": "^2.840.0", "codeowners-utils": "^1.0.2", "core-js": "^3.6.5", "cross-fetch": "^3.0.6", @@ -59,7 +59,7 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/test-utils": "^0.1.6", + "@backstage/test-utils": "^0.1.7", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", "@types/lodash": "^4.14.151", diff --git a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts index 28200021cc..ba163fa0c3 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts @@ -27,18 +27,22 @@ describe('AwsOrganizationCloudAccountProcessor', () => { afterEach(() => jest.resetAllMocks()); it('generates component entities for accounts', async () => { - listAccounts.mockImplementation(() => - Promise.resolve({ - Accounts: [ - { - Arn: - 'arn:aws:organizations::192594491037:account/o-1vl18kc5a3/957140518395', - Name: 'testaccount', - }, - ], - NextToken: undefined, - }), - ); + listAccounts.mockImplementation(() => { + return { + async promise() { + return { + Accounts: [ + { + Arn: + 'arn:aws:organizations::192594491037:account/o-1vl18kc5a3/957140518395', + Name: 'testaccount', + }, + ], + NextToken: undefined, + }; + }, + }; + }); await processor.readLocation(location, false, emit); expect(emit).toBeCalledWith({ type: 'entity', @@ -69,23 +73,27 @@ describe('AwsOrganizationCloudAccountProcessor', () => { type: 'aws-cloud-accounts', target: 'o-1vl18kc5a3', }; - listAccounts.mockImplementation(() => - Promise.resolve({ - Accounts: [ - { - Arn: - 'arn:aws:organizations::192594491037:account/o-1vl18kc5a3/957140518395', - Name: 'testaccount', - }, - { - Arn: - 'arn:aws:organizations::192594491037:account/o-zzzzzzzzz/957140518395', - Name: 'testaccount2', - }, - ], - NextToken: undefined, - }), - ); + listAccounts.mockImplementation(() => { + return { + async promise() { + return { + Accounts: [ + { + Arn: + 'arn:aws:organizations::192594491037:account/o-1vl18kc5a3/957140518395', + Name: 'testaccount', + }, + { + Arn: + 'arn:aws:organizations::192594491037:account/o-zzzzzzzzz/957140518395', + Name: 'testaccount2', + }, + ], + NextToken: undefined, + }; + }, + }; + }); await processor.readLocation(locationTest, false, emit); expect(emit).toBeCalledTimes(1); expect(emit).toBeCalledWith({ diff --git a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.ts index 40516f88f7..0e988e7307 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.ts @@ -14,11 +14,8 @@ * limitations under the License. */ import { LocationSpec, ResourceEntityV1alpha1 } from '@backstage/catalog-model'; -import { - Account, - Organizations, - ListAccountsCommandOutput, -} from '@aws-sdk/client-organizations'; +import AWS, { Organizations } from 'aws-sdk'; +import { Account, ListAccountsResponse } from 'aws-sdk/clients/organizations'; import * as results from './results'; import { CatalogProcessor, CatalogProcessorEmit } from './types'; @@ -38,7 +35,7 @@ const ORGANIZATION_ANNOTATION: string = 'amazonaws.com/organization-id'; export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { organizations: Organizations; constructor() { - this.organizations = new Organizations({ + this.organizations = new AWS.Organizations({ region: AWS_ORGANIZATION_REGION, }); // Only available in us-east-1 } @@ -67,9 +64,9 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { let nextToken = undefined; while (isInitialAttempt || nextToken) { isInitialAttempt = false; - const orgAccounts: ListAccountsCommandOutput = await this.organizations.listAccounts( - { NextToken: nextToken }, - ); + const orgAccounts: ListAccountsResponse = await this.organizations + .listAccounts({ NextToken: nextToken }) + .promise(); if (orgAccounts.Accounts) { awsAccounts = awsAccounts.concat(orgAccounts.Accounts); } diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts new file mode 100644 index 0000000000..41698e1d04 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts @@ -0,0 +1,222 @@ +/* + * 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 { LocationSpec } from '@backstage/catalog-model'; +import { GithubDiscoveryProcessor, parseUrl } from './GithubDiscoveryProcessor'; +import { getOrganizationRepositories } from './github'; +import { ConfigReader } from '@backstage/config'; + +jest.mock('./github'); +const mockGetOrganizationRepositories = getOrganizationRepositories as jest.MockedFunction< + typeof getOrganizationRepositories +>; + +describe('GithubDiscoveryProcessor', () => { + describe('parseUrl', () => { + it('parses well formed URLs', () => { + expect( + parseUrl('https://github.com/foo/proj/blob/master/catalog.yaml'), + ).toEqual({ + org: 'foo', + repoSearchPath: /^proj$/, + catalogPath: '/blob/master/catalog.yaml', + }); + expect( + parseUrl('https://github.com/foo/proj*/blob/master/catalog.yaml'), + ).toEqual({ + org: 'foo', + repoSearchPath: /^proj.*$/, + catalogPath: '/blob/master/catalog.yaml', + }); + }); + + it('throws on incorrectly formed URLs', () => { + expect(() => parseUrl('https://github.com')).toThrow(); + expect(() => parseUrl('https://github.com//')).toThrow(); + expect(() => parseUrl('https://github.com/foo')).toThrow(); + expect(() => parseUrl('https://github.com//foo')).toThrow(); + expect(() => parseUrl('https://github.com/org/teams')).toThrow(); + expect(() => parseUrl('https://github.com/org//teams')).toThrow(); + }); + }); + + describe('reject unrelated entries', () => { + it('rejects unknown types', async () => { + const processor = GithubDiscoveryProcessor.fromConfig( + new ConfigReader({ + integrations: { + github: [{ host: 'github.com', token: 'blob' }], + }, + }), + { logger: getVoidLogger() }, + ); + const location: LocationSpec = { + type: 'not-github-discovery', + target: 'https://github.com', + }; + await expect( + processor.readLocation(location, false, () => {}), + ).resolves.toBeFalsy(); + }); + + it('rejects unknown targets', async () => { + const processor = GithubDiscoveryProcessor.fromConfig( + new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'blob' }, + { host: 'ghe.example.net', token: 'blob' }, + ], + }, + }), + { logger: getVoidLogger() }, + ); + const location: LocationSpec = { + type: 'github-discovery', + target: 'https://not.github.com/apa', + }; + await expect( + processor.readLocation(location, false, () => {}), + ).rejects.toThrow( + /There is no GitHub integration that matches https:\/\/not.github.com\/apa/, + ); + }); + }); + + describe('handles repositories', () => { + const processor = GithubDiscoveryProcessor.fromConfig( + new ConfigReader({ + integrations: { + github: [{ host: 'github.com', token: 'blob' }], + }, + }), + { logger: getVoidLogger() }, + ); + + beforeEach(() => { + mockGetOrganizationRepositories.mockClear(); + }); + + it('output all repositories', async () => { + const location: LocationSpec = { + type: 'github-discovery', + target: 'https://github.com/backstage/*/blob/master/catalog.yaml', + }; + mockGetOrganizationRepositories.mockResolvedValueOnce({ + repositories: [ + { name: 'backstage', url: 'https://github.com/backstage/backstage' }, + { name: 'demo', url: 'https://github.com/backstage/demo' }, + ], + }); + const emitter = jest.fn(); + + await processor.readLocation(location, false, emitter); + + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://github.com/backstage/backstage/blob/master/catalog.yaml', + }, + optional: false, + }); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: 'https://github.com/backstage/demo/blob/master/catalog.yaml', + }, + optional: false, + }); + }); + + it('output repositories with wildcards', async () => { + const location: LocationSpec = { + type: 'github-discovery', + target: + 'https://github.com/backstage/techdocs-*/blob/master/catalog.yaml', + }; + mockGetOrganizationRepositories.mockResolvedValueOnce({ + repositories: [ + { name: 'backstage', url: 'https://github.com/backstage/backstage' }, + { + name: 'techdocs-cli', + url: 'https://github.com/backstage/techdocs-cli', + }, + { + name: 'techdocs-container', + url: 'https://github.com/backstage/techdocs-container', + }, + ], + }); + const emitter = jest.fn(); + + await processor.readLocation(location, false, emitter); + + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://github.com/backstage/techdocs-cli/blob/master/catalog.yaml', + }, + optional: false, + }); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://github.com/backstage/techdocs-container/blob/master/catalog.yaml', + }, + optional: false, + }); + }); + it('filter unrelated repositories', async () => { + const location: LocationSpec = { + type: 'github-discovery', + target: 'https://github.com/backstage/test/blob/master/catalog.yaml', + }; + mockGetOrganizationRepositories.mockResolvedValueOnce({ + repositories: [ + { name: 'abstest', url: 'https://github.com/backstage/abctest' }, + { + name: 'test', + url: 'https://github.com/backstage/test', + }, + { + name: 'testxyz', + url: 'https://github.com/backstage/testxyz', + }, + ], + }); + const emitter = jest.fn(); + + await processor.readLocation(location, false, emitter); + + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: 'https://github.com/backstage/test/blob/master/catalog.yaml', + }, + optional: false, + }); + }); + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts new file mode 100644 index 0000000000..9af247fd25 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts @@ -0,0 +1,130 @@ +/* + * 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 { LocationSpec } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { + GithubCredentialsProvider, + ScmIntegrations, +} from '@backstage/integration'; +import { graphql } from '@octokit/graphql'; +import { Logger } from 'winston'; +import { getOrganizationRepositories } from './github'; +import * as results from './results'; +import { CatalogProcessor, CatalogProcessorEmit } from './types'; + +/** + * Extracts repositories out of a GitHub org. + */ +export class GithubDiscoveryProcessor implements CatalogProcessor { + private readonly integrations: ScmIntegrations; + private readonly logger: Logger; + + static fromConfig(config: Config, options: { logger: Logger }) { + const integrations = ScmIntegrations.fromConfig(config); + + return new GithubDiscoveryProcessor({ + ...options, + integrations, + }); + } + + constructor(options: { integrations: ScmIntegrations; logger: Logger }) { + this.integrations = options.integrations; + this.logger = options.logger; + } + + async readLocation( + location: LocationSpec, + _optional: boolean, + emit: CatalogProcessorEmit, + ): Promise { + if (location.type !== 'github-discovery') { + return false; + } + + const gitHubConfig = this.integrations.github.byUrl(location.target) + ?.config; + if (!gitHubConfig) { + throw new Error( + `There is no GitHub integration that matches ${location.target}. Please add a configuration entry for it under integrations.github`, + ); + } + const { headers } = await GithubCredentialsProvider.create( + gitHubConfig, + ).getCredentials({ url: location.target }); + const { org, repoSearchPath, catalogPath } = parseUrl(location.target); + + const client = graphql.defaults({ + baseUrl: gitHubConfig.apiBaseUrl, + headers, + }); + + // Read out all of the raw data + const startTimestamp = Date.now(); + this.logger.info(`Reading GitHub repositories from ${location.target}`); + + const { repositories } = await getOrganizationRepositories(client, org); + + const duration = ((Date.now() - startTimestamp) / 1000).toFixed(1); + this.logger.debug( + `Read ${repositories.length} GitHub repositories in ${duration} seconds`, + ); + + for (const repository of repositories) { + if (!repoSearchPath.test(repository.name)) { + continue; + } + emit( + results.location( + { + type: 'url', + target: `${repository.url}${catalogPath}`, + }, + false, + ), + ); + } + + return true; + } +} + +/* + * Helpers + */ + +export function parseUrl( + urlString: string, +): { org: string; repoSearchPath: RegExp; catalogPath: string } { + const url = new URL(urlString); + const path = url.pathname.substr(1).split('/'); + + // /backstage/techdocs-*/blob/master/catalog-info.yaml + if (path.length > 2 && path[0].length && path[1].length) { + return { + org: decodeURIComponent(path[0]), + repoSearchPath: escapeRegExp(decodeURIComponent(path[1])), + catalogPath: `/${decodeURIComponent(path.slice(2).join('/'))}`, + }; + } + + throw new Error(`Failed to parse ${urlString}`); +} + +export function escapeRegExp(str: string): RegExp { + return new RegExp(`^${str.replace(/\*/g, '.*')}$`); +} diff --git a/plugins/catalog-backend/src/ingestion/processors/github/github.test.ts b/plugins/catalog-backend/src/ingestion/processors/github/github.test.ts index 8911e84d99..81b44c706d 100644 --- a/plugins/catalog-backend/src/ingestion/processors/github/github.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/github/github.test.ts @@ -22,6 +22,7 @@ import { getOrganizationTeams, getOrganizationUsers, getTeamMembers, + getOrganizationRepositories, QueryResponse, } from './github'; @@ -151,4 +152,48 @@ describe('github', () => { await expect(getTeamMembers(graphql, 'a', 'b')).resolves.toEqual(output); }); }); + + describe('getOrganizationRepositories', () => { + it('read repositories', async () => { + const input: QueryResponse = { + organization: { + repositories: { + nodes: [ + { + name: 'backstage', + url: 'https://github.com/backstage/backstage', + }, + { + name: 'demo', + url: 'https://github.com/backstage/demo', + }, + ], + pageInfo: { + hasNextPage: false, + }, + }, + }, + }; + + const output = { + repositories: [ + { name: 'backstage', url: 'https://github.com/backstage/backstage' }, + { + name: 'demo', + url: 'https://github.com/backstage/demo', + }, + ], + }; + + server.use( + graphqlMsw.query('repositories', (_req, res, ctx) => + res(ctx.data(input)), + ), + ); + + await expect(getOrganizationRepositories(graphql, 'a')).resolves.toEqual( + output, + ); + }); + }); }); diff --git a/plugins/catalog-backend/src/ingestion/processors/github/github.ts b/plugins/catalog-backend/src/ingestion/processors/github/github.ts index 2b33d72f65..d50887c592 100644 --- a/plugins/catalog-backend/src/ingestion/processors/github/github.ts +++ b/plugins/catalog-backend/src/ingestion/processors/github/github.ts @@ -27,6 +27,7 @@ export type Organization = { membersWithRole?: Connection; team?: Team; teams?: Connection; + repositories?: Connection; }; export type PageInfo = { @@ -52,6 +53,11 @@ export type Team = { members: Connection; }; +export type Repository = { + name: string; + url: string; +}; + export type Connection = { pageInfo: PageInfo; nodes: T[]; @@ -216,6 +222,39 @@ export async function getOrganizationTeams( return { groups, groupMemberUsers }; } +export async function getOrganizationRepositories( + client: typeof graphql, + org: string, +): Promise<{ repositories: Repository[] }> { + const query = ` + query repositories($org: String!, $cursor: String) { + organization(login: $org) { + name + repositories(first: 100, after: $cursor) { + nodes { + name + url + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + `; + + const repositories = await queryWithPaging( + client, + query, + r => r.organization?.repositories, + x => x, + { org }, + ); + + return { repositories }; +} + /** * Gets all the users out of a GitHub organization. * diff --git a/plugins/catalog-backend/src/ingestion/processors/github/index.ts b/plugins/catalog-backend/src/ingestion/processors/github/index.ts index e424d7bafb..2063e8c1b2 100644 --- a/plugins/catalog-backend/src/ingestion/processors/github/index.ts +++ b/plugins/catalog-backend/src/ingestion/processors/github/index.ts @@ -16,4 +16,8 @@ export { readGithubConfig } from './config'; export type { ProviderConfig } from './config'; -export { getOrganizationTeams, getOrganizationUsers } from './github'; +export { + getOrganizationTeams, + getOrganizationUsers, + getOrganizationRepositories, +} from './github'; diff --git a/plugins/catalog-backend/src/ingestion/processors/index.ts b/plugins/catalog-backend/src/ingestion/processors/index.ts index 05dd327f0e..736c64ea39 100644 --- a/plugins/catalog-backend/src/ingestion/processors/index.ts +++ b/plugins/catalog-backend/src/ingestion/processors/index.ts @@ -21,6 +21,7 @@ export { AwsOrganizationCloudAccountProcessor } from './AwsOrganizationCloudAcco export { BuiltinKindsEntityProcessor } from './BuiltinKindsEntityProcessor'; export { CodeOwnersProcessor } from './CodeOwnersProcessor'; export { FileReaderProcessor } from './FileReaderProcessor'; +export { GithubDiscoveryProcessor } from './GithubDiscoveryProcessor'; export { GithubOrgReaderProcessor } from './GithubOrgReaderProcessor'; export { LdapOrgReaderProcessor } from './LdapOrgReaderProcessor'; export { LocationEntityProcessor } from './LocationEntityProcessor'; diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index f7544ef42e..a75c7da6e8 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -43,6 +43,7 @@ import { CatalogProcessorParser, CodeOwnersProcessor, FileReaderProcessor, + GithubDiscoveryProcessor, GithubOrgReaderProcessor, HigherOrderOperation, HigherOrderOperations, @@ -303,6 +304,7 @@ export class CatalogBuilder { if (!this.processorsReplace) { processors.push( new FileReaderProcessor(), + GithubDiscoveryProcessor.fromConfig(config, { logger }), GithubOrgReaderProcessor.fromConfig(config, { logger }), LdapOrgReaderProcessor.fromConfig(config, { logger }), MicrosoftGraphOrgReaderProcessor.fromConfig(config, { logger }), diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index 0311d865a9..3966557555 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,45 @@ # @backstage/plugin-catalog-import +## 0.4.0 + +### Minor Changes + +- 6ed2b47d6: Include Backstage identity token in requests to backend plugins. +- 68dd79d83: The plugin has been refactored and is now based on a configurable state machine of 'analyze', 'prepare', 'review' & 'finish'. + Depending on the outcome of the 'analyze' stage, different flows are selected ('single-location', 'multiple-locations', 'no-location'). + Each flow can define it's own components that guide the user. + + During the refactoring, the `catalogRouteRef` property of the `CatalogImportPage` has been removed, so the `App.tsx` of the backstage apps need to be updated: + + ```diff + // packages/app/src/App.tsx + + } + + element={} + /> + ``` + +### Patch Changes + +- 753bb4c40: Flatten the options of the `CatalogImportPage` from the `options` property to the `pullRequest` property. + + ```diff + - + + + ``` + +- Updated dependencies [6ed2b47d6] +- Updated dependencies [ffffea8e6] +- Updated dependencies [72b96e880] +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/catalog-client@0.3.6 + - @backstage/integration@0.4.0 + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.3.7 ### Patch Changes diff --git a/plugins/catalog-import/README.md b/plugins/catalog-import/README.md index 1dc0d7cf89..97371ce0e7 100644 --- a/plugins/catalog-import/README.md +++ b/plugins/catalog-import/README.md @@ -1,21 +1,84 @@ -# Catalog import plugin +# Catalog Import -Welcome to the catalog-import plugin! +The Catalog Import Plugin provides a wizard to onboard projects with existing `catalog-info.yaml` files. +It also assists by creating pull requests in repositories where no `catalog-info.yaml` exists. -This plugin allows you to bootstrap a component-config YAML file for your repository and open a pull request to add it. +![Catalog Import Plugin](./docs/catalog-import-screenshot.png) -When installed it is accessible on [localhost:3000/catalog-import](localhost:3000/catalog-import). +Current features: - +- Import `catalog-info.yaml` files from a URL in a repository of one of the supported Git integrations (example `https://github.com/backstage/backstage/catalog-info.yaml`). +- _[GitHub only]_ Search for all `catalog-info.yaml` files in a Git repository (example: `https://github.com/backstage/backstage`). +- _[GitHub only]_ Analyze a repository, generate a Component entity, and create a Pull Request to onboard the repository. -## Running +Some features are not yet available for all supported Git providers. -Just run the backstage. +## Getting Started -``` -yarn start && yarn --cwd packages/backend start +1. Install the Catalog Import Plugin: + +```bash +# packages/app + +yarn add @backstage/plugin-catalog-import ``` -## Usage +2. Add the plugin to the app: -Pretty straightforward, navigate to [localhost:3000/catalog-import](localhost:3000/catalog-import) and enter your repo's URL. +```ts +// packages/app/src/plugins.ts + +export { catalogImportPlugin } from '@backstage/plugin-catalog-import'; +``` + +3. Register the `CatalogImportPage` at the `/catalog-import` path: + +```tsx +// packages/app/src/App.tsx + +import { CatalogImportPage } from '@backstage/plugin-catalog-import'; + +} />; +``` + +## Customizations + +### Disable the creation of Pull Requests + +The pull request feature can be disabled by options that are passed to the `CatalogImportPage`: + +```tsx +// packages/app/src/App.tsx + +} +/> +``` + +### Customize the title and body of the Pull Request + +The pull request form is filled with a default title and body. +This can be configured by options that are passed to the `CatalogImportPage`: + +```tsx +// packages/app/src/App.tsx + + ({ + title: 'My title', + body: 'My **markdown** body', + }), + }} + /> + } +/> +``` + +## Development + +Use `yarn start` to run a [development version](./dev/index.tsx) of the plugin that can be used to validate each flow with mocked data. diff --git a/plugins/catalog-import/dev/index.tsx b/plugins/catalog-import/dev/index.tsx index c9b46a7ed4..e794628e6c 100644 --- a/plugins/catalog-import/dev/index.tsx +++ b/plugins/catalog-import/dev/index.tsx @@ -14,7 +14,338 @@ * limitations under the License. */ +import { CatalogApi } from '@backstage/catalog-client'; +import { Entity, EntityName } from '@backstage/catalog-model'; +import { Content, Header, InfoCard, Page } from '@backstage/core'; import { createDevApp } from '@backstage/dev-utils'; -import { catalogImportPlugin } from '../src/plugin'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { Grid, ListItem, ListItemIcon, ListItemText } from '@material-ui/core'; +import AlarmIcon from '@material-ui/icons/Alarm'; +import LocationOnIcon from '@material-ui/icons/LocationOn'; +import React from 'react'; +import { + AnalyzeResult, + CatalogImportApi, + catalogImportApiRef, + EntityListComponent, + ImportStepper, +} from '../src'; +import { ImportComponentPage } from '../src/components/ImportComponentPage'; -createDevApp().registerPlugin(catalogImportPlugin).render(); +const getEntityNames = (url: string): EntityName[] => [ + { + kind: 'Component', + namespace: url.replace(/^.*(folder-[^/]+).*|.*()$/, '$1') || 'default', + name: 'component-a', + }, + { + kind: 'API', + namespace: url.replace(/^.*(folder-[^/]+).*|.*()$/, '$1') || 'default', + name: 'api-a', + }, +]; + +const getEntities = (url: string): Entity[] => [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: url.replace(/^.*(folder-[^/]+).*|.*()$/, '$1') || 'default', + name: 'component-a', + }, + }, + { + apiVersion: '1', + kind: 'API', + metadata: { + namespace: url.replace(/^.*(folder-[^/]+).*|.*()$/, '$1') || 'default', + name: 'api-a', + }, + }, +]; + +const locations = [ + { + target: 'https://my-location-1', + entities: [ + { + kind: 'Domain', + namespace: 'default', + name: 'my-domain', + }, + { + kind: 'Group', + namespace: 'groups', + name: 'my-group', + }, + { + kind: 'Location', + namespace: 'default', + name: 'my-location', + }, + { + kind: 'System', + namespace: 'default', + name: 'my-system', + }, + { + kind: 'User', + namespace: 'users', + name: 'my-api', + }, + ], + }, + { + target: 'https://my-location-2', + entities: [ + { + kind: 'API', + namespace: 'default', + name: 'my-api', + }, + { + kind: 'Component', + namespace: 'default', + name: 'my-component', + }, + { + kind: 'Location', + namespace: 'default', + name: 'my-location', + }, + ], + }, +]; + +createDevApp() + .registerApi({ + api: catalogApiRef, + deps: {}, + factory: () => + ({ + getEntities: async () => { + await new Promise(r => setTimeout(r, 1000)); + + return { + items: [ + { + apiVersion: '1', + kind: 'Group', + metadata: { + name: 'group-a', + namespace: 'default', + }, + spec: { + profile: { + displayName: 'Group A', + }, + }, + }, + { + apiVersion: '1', + kind: 'Group', + metadata: { + name: 'group-b', + namespace: 'default', + }, + spec: { + profile: { + displayName: 'Group B', + }, + }, + }, + { + apiVersion: '1', + kind: 'Group', + metadata: { + name: 'group-a', + namespace: 'other', + }, + spec: { + profile: { + displayName: 'Group A', + }, + }, + }, + ] as Entity[], + }; + }, + + addLocation: async location => { + await new Promise(r => setTimeout(r, 1000)); + + return { + location, + entities: getEntities(location.target), + }; + }, + } as CatalogApi), + }) + .registerApi({ + api: catalogImportApiRef, + deps: {}, + factory: () => + ({ + analyzeUrl: async (url: string): Promise => { + await new Promise(r => setTimeout(r, 500)); + + switch (url) { + case 'https://0': + return { + type: 'repository', + url, + integrationType: 'github', + generatedEntities: getEntities(url), + }; + + case 'https://1': + case 'https://2/catalog-info.yaml': + case 'https://2/folder-a/catalog-info.yaml': + case 'https://2/folder-b/catalog-info.yaml': + return { + type: 'locations', + locations: [ + { + target: url.includes('/catalog-info.yaml') + ? url + : `${url}/catalog-info.yaml`, + entities: getEntityNames(url), + }, + ], + }; + + case 'https://2': { + const urls = [ + `${url}/catalog-info.yaml`, + `${url}/folder-a/catalog-info.yaml`, + `${url}/folder-b/catalog-info.yaml`, + ]; + + return { + type: 'locations', + locations: urls.map(u => ({ + target: u, + entities: getEntityNames(u), + })), + }; + } + + default: + throw new Error(`Invalid url ${url}`); + } + }, + + submitPullRequest: async ({ + repositoryUrl, + }): Promise<{ + link: string; + location: string; + }> => { + await new Promise(r => setTimeout(r, 2500)); + + return { + link: `${repositoryUrl}/pulls/1`, + location: `${repositoryUrl}/blob/catalog-info.yaml`, + }; + }, + } as CatalogImportApi), + }) + .addPage({ + title: 'Catalog Import', + element: , + }) + .addPage({ + title: 'Catalog Import 2', + element: ( + +
+ + + + + + + + + + + + + + + + + + + + + ), + }) + .addPage({ + title: 'Components', + element: ( + +
+ + + + + } + /> + + + + + + + + + + + } + locations={locations} + locationListItemIcon={() => } + onItemClick={() => {}} + /> + + + + + } + /> + + + + + } + withLinks + /> + + + + + + ), + }) + .render(); diff --git a/plugins/catalog-import/docs/catalog-import-screenshot.png b/plugins/catalog-import/docs/catalog-import-screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..eb8348b7ef79d1c6cfeb5ec893ec58c70f9292a4 GIT binary patch literal 365037 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%0Di#5m}7P1t1%=W>~m*}p4Vhv3R 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

J~~STi;kiC5F74^ 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®%Zi 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 literal 0 HcmV?d00001 diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 2b2b016716..3016ada247 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-import", - "version": "0.3.7", + "version": "0.4.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,14 +31,16 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.0", - "@backstage/integration": "^0.3.2", - "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/catalog-client": "^0.3.6", + "@backstage/core": "^0.6.1", + "@backstage/integration": "^0.4.0", + "@backstage/plugin-catalog-react": "^0.0.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "@octokit/rest": "^18.0.12", + "@types/react": "^16.9", "git-url-parse": "^11.4.4", "react": "^16.13.1", "react-dom": "^16.13.1", @@ -49,12 +51,12 @@ "yaml": "^1.10.0" }, "devDependencies": { - "@backstage/catalog-client": "^0.3.5", "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", + "@testing-library/react-hooks": "^3.3.0", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", diff --git a/plugins/catalog-import/src/api/CatalogImportApi.ts b/plugins/catalog-import/src/api/CatalogImportApi.ts index 5abb0e3e53..5237e0339f 100644 --- a/plugins/catalog-import/src/api/CatalogImportApi.ts +++ b/plugins/catalog-import/src/api/CatalogImportApi.ts @@ -14,29 +14,38 @@ * limitations under the License. */ +import { EntityName } from '@backstage/catalog-model'; import { createApiRef } from '@backstage/core'; -import { PartialEntity } from '../util/types'; -import { GitHubIntegrationConfig } from '@backstage/integration'; +import { PartialEntity } from '../types'; export const catalogImportApiRef = createApiRef({ id: 'plugin.catalog-import.service', description: 'Used by the catalog import plugin to make requests', }); +// result of the analyze state +export type AnalyzeResult = + | { + type: 'locations'; + locations: Array<{ + target: string; + entities: EntityName[]; + }>; + } + | { + type: 'repository'; + url: string; + integrationType: string; + generatedEntities: PartialEntity[]; + }; + export interface CatalogImportApi { - submitPrToRepo(options: { - owner: string; - repo: string; + analyzeUrl(url: string): Promise; + + submitPullRequest(options: { + repositoryUrl: string; fileContent: string; - githubIntegrationConfig: GitHubIntegrationConfig; + title: string; + body: string; }): 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; - }): Promise; } diff --git a/plugins/catalog-import/src/api/CatalogImportClient.test.ts b/plugins/catalog-import/src/api/CatalogImportClient.test.ts index 0e0cf4b323..95082829c4 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.test.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.test.ts @@ -14,51 +14,331 @@ * limitations under the License. */ -import { CatalogImportClient } from './CatalogImportClient'; +const octokit = { + repos: { + get: () => Promise.resolve({ data: { default_branch: 'main' } }), + createOrUpdateFileContents: jest.fn().mockImplementation(async () => {}), + }, + search: { + code: jest.fn(), + }, + git: { + getRef: async () => ({ + data: { object: { sha: 'any' } }, + }), + createRef: jest.fn().mockImplementation(async () => {}), + }, + pulls: { + create: jest.fn().mockImplementation(async () => ({ + data: { + html_url: 'http://pull/request/0', + }, + })), + }, +}; -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' }, - ], - }, - }), - }, - }; - }), +jest.doMock('@octokit/rest', () => { + class Octokit { + constructor() { + return octokit; + } + } + return { Octokit }; +}); + +// Mock the value to control which integrations are activated +jest.mock('./GitHub', () => ({ + getGithubIntegrationConfig: jest.fn(), })); +import { ConfigReader, OAuthApi, UrlPatternDiscovery } from '@backstage/core'; +import { GitHubIntegrationConfig } from '@backstage/integration'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { msw } from '@backstage/test-utils'; +import { Octokit } from '@octokit/rest'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { CatalogImportClient } from './CatalogImportClient'; +import { getGithubIntegrationConfig } from './GitHub'; + +const server = setupServer(); + describe('CatalogImportClient', () => { - describe('checkForExistingCatalogInfo', () => { - const cic = new CatalogImportClient({ - discoveryApi: { getBaseUrl: () => Promise.resolve('base') }, - githubAuthApi: { getAccessToken: (_, __) => Promise.resolve('token') }, - configApi: {} as any, + msw.setupDefaultHandlers(server); + + const mockBaseUrl = 'http://backstage:9191/api/catalog'; + const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); + + const githubAuthApi: jest.Mocked = { + getAccessToken: jest.fn(), + }; + const identityApi = { + getUserId: () => { + return 'user'; + }, + getProfile: () => { + return {}; + }, + getIdToken: () => { + return Promise.resolve('token'); + }, + signOut: () => { + return Promise.resolve(); + }, + }; + + const configApi = new ConfigReader({}); + + const catalogApi: jest.Mocked = { + getEntities: jest.fn(), + addLocation: jest.fn(), + getEntityByName: jest.fn(), + getLocationByEntity: jest.fn(), + getLocationById: jest.fn(), + removeEntityByUid: jest.fn(), + }; + + let catalogImportClient: CatalogImportClient; + let getGithubIntegrationConfigFn: jest.Mock; + + beforeEach(() => { + catalogImportClient = new CatalogImportClient({ + discoveryApi, + githubAuthApi, + configApi, + identityApi, + catalogApi, }); - 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' }, + + getGithubIntegrationConfigFn = getGithubIntegrationConfig as jest.Mock; + getGithubIntegrationConfigFn.mockReset(); + }); + + describe('analyzeUrl', () => { + it('should add yaml location', async () => { + catalogApi.addLocation.mockResolvedValueOnce({ + location: { + id: 'id-0', + type: 'url', + target: 'http://example.com/folder/catalog-info.yaml', + }, + entities: [ + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'my-entity', + namespace: 'my-namespace', + }, + }, + ], + }); + + await expect( + catalogImportClient.analyzeUrl( + 'http://example.com/folder/catalog-info.yaml', + ), + ).resolves.toEqual({ + locations: [ + { + entities: [ + { + kind: 'Component', + name: 'my-entity', + namespace: 'my-namespace', + }, + ], + target: 'http://example.com/folder/catalog-info.yaml', + }, + ], + type: 'locations', + }); + + expect(catalogApi.addLocation).toBeCalledTimes(1); + expect(catalogApi.addLocation.mock.calls[0][0]).toEqual({ + type: 'url', + target: 'http://example.com/folder/catalog-info.yaml', + dryRun: true, + }); + }); + + it('should ignore missing github integration', async () => { + await expect( + catalogImportClient.analyzeUrl( + 'https://github.com/backstage/backstage', + ), + ).rejects.toThrow(new Error('Invalid url')); + }); + + it('should find locations from github', async () => { + getGithubIntegrationConfigFn.mockReturnValue({ + repo: 'backstage', + owner: 'backstage', + githubIntegrationConfig: {} as GitHubIntegrationConfig, + }); + + ((new Octokit().search.code as any) as jest.Mock).mockResolvedValueOnce({ + data: { + total_count: 2, + items: [ + { path: 'simple/path/catalog-info.yaml' }, + { path: 'co/mple/x/path/catalog-info.yaml' }, + { path: 'catalog-info.yaml' }, + ], + }, + }); + + catalogApi.addLocation.mockImplementation(async ({ type, target }) => ({ + location: { + id: 'id-0', + type: type ?? 'url', + target, + }, + entities: [ + { + apiVersion: '1', + kind: 'k', + metadata: { + name: 'e', + namespace: 'n', + }, + }, + ], + })); + + await expect( + catalogImportClient.analyzeUrl( + 'https://github.com/backstage/backstage', + ), + ).resolves.toEqual({ + locations: [ + { + entities: [{ kind: 'k', name: 'e', namespace: 'n' }], + target: + 'https://github.com/backstage/backstage/blob/main/simple/path/catalog-info.yaml', + }, + { + entities: [{ kind: 'k', name: 'e', namespace: 'n' }], + target: + 'https://github.com/backstage/backstage/blob/main/co/mple/x/path/catalog-info.yaml', + }, + { + entities: [{ kind: 'k', name: 'e', namespace: 'n' }], + target: + 'https://github.com/backstage/backstage/blob/main/catalog-info.yaml', + }, + ], + type: 'locations', + }); + }); + + it('should find repository from github', async () => { + getGithubIntegrationConfigFn.mockReturnValue({ + repo: 'backstage', + owner: 'backstage', + githubIntegrationConfig: {} as GitHubIntegrationConfig, + }); + + ((new Octokit().search.code as any) as jest.Mock).mockResolvedValueOnce({ + data: { total_count: 0, items: [] }, + }); + + server.use( + rest.post(`${mockBaseUrl}/analyze-location`, (req, res, ctx) => { + expect(req.body).toEqual({ + location: { + target: 'https://github.com/backstage/backstage', + type: 'url', + }, + }); + + return res( + ctx.json({ + generateEntities: [ + { + entity: { + kind: 'k', + metadata: { name: 'e', namespace: 'n' }, + }, + fields: [], + }, + ], + existingEntityFiles: [], + }), + ); + }), + ); + + await expect( + catalogImportClient.analyzeUrl( + 'https://github.com/backstage/backstage', + ), + ).resolves.toEqual({ + type: 'repository', + url: 'https://github.com/backstage/backstage', + integrationType: 'github', + generatedEntities: [ + { + kind: 'k', + metadata: { name: 'e', namespace: 'n' }, + }, + ], + }); + }); + }); + + describe('submitPullRequest', () => { + it('should create GitHub pull request', async () => { + getGithubIntegrationConfigFn.mockReturnValue({ + repo: 'backstage', + owner: 'backstage', + githubIntegrationConfig: { + host: 'github.com', + } as GitHubIntegrationConfig, + }); + + await expect( + catalogImportClient.submitPullRequest({ + repositoryUrl: 'https://github.com/backstage/backstage', + fileContent: 'some content', + title: 'A title', + body: 'A body', + }), + ).resolves.toEqual({ + link: 'http://pull/request/0', + location: + 'https://github.com/backstage/backstage/blob/main/catalog-info.yaml', + }); + + expect( + ((new Octokit().git.createRef as any) as jest.Mock).mock.calls[0][0], + ).toEqual({ + owner: 'backstage', + repo: 'backstage', + ref: 'refs/heads/backstage-integration', + sha: 'any', + }); + expect( + ((new Octokit().repos.createOrUpdateFileContents as any) as jest.Mock) + .mock.calls[0][0], + ).toEqual({ + owner: 'backstage', + repo: 'backstage', + path: 'catalog-info.yaml', + message: 'Add catalog-info.yaml config file', + content: 'c29tZSBjb250ZW50', + branch: 'backstage-integration', + }); + expect( + ((new Octokit().pulls.create as any) as jest.Mock).mock.calls[0][0], + ).toEqual({ + owner: 'backstage', + repo: 'backstage', + title: 'A title', + head: 'backstage-integration', + body: 'A body', + base: 'main', }); - 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 d1509c2105..b4b1ebe845 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.ts @@ -14,37 +14,131 @@ * limitations under the License. */ -import { Octokit } from '@octokit/rest'; -import { DiscoveryApi, OAuthApi, ConfigApi } from '@backstage/core'; -import { CatalogImportApi } from './CatalogImportApi'; -import { PartialEntity } from '../util/types'; +import { CatalogApi } from '@backstage/catalog-client'; +import { EntityName } from '@backstage/catalog-model'; +import { + ConfigApi, + DiscoveryApi, + IdentityApi, + OAuthApi, +} from '@backstage/core'; import { GitHubIntegrationConfig } from '@backstage/integration'; +import { Octokit } from '@octokit/rest'; +import { PartialEntity } from '../types'; +import { AnalyzeResult, CatalogImportApi } from './CatalogImportApi'; +import { getGithubIntegrationConfig } from './GitHub'; export class CatalogImportClient implements CatalogImportApi { private readonly discoveryApi: DiscoveryApi; + private readonly identityApi: IdentityApi; private readonly githubAuthApi: OAuthApi; private readonly configApi: ConfigApi; + private readonly catalogApi: CatalogApi; constructor(options: { discoveryApi: DiscoveryApi; githubAuthApi: OAuthApi; + identityApi: IdentityApi; configApi: ConfigApi; + catalogApi: CatalogApi; }) { this.discoveryApi = options.discoveryApi; this.githubAuthApi = options.githubAuthApi; + this.identityApi = options.identityApi; this.configApi = options.configApi; + this.catalogApi = options.catalogApi; } - async generateEntityDefinitions({ + async analyzeUrl(url: string): Promise { + if (url.match(/\.ya?ml$/)) { + const location = await this.catalogApi.addLocation({ + type: 'url', + target: url, + dryRun: true, + }); + + return { + type: 'locations', + locations: [ + { + target: location.location.target, + entities: location.entities.map(e => ({ + kind: e.kind, + namespace: e.metadata.namespace ?? 'default', + name: e.metadata.name, + })), + }, + ], + }; + } + + const ghConfig = getGithubIntegrationConfig(this.configApi, url); + + if (ghConfig) { + // TODO: this could be part of the analyze-location endpoint + const locations = await this.checkGitHubForExistingCatalogInfo({ + ...ghConfig, + url, + }); + + if (locations.length > 0) { + return { + type: 'locations', + locations, + }; + } + + return { + type: 'repository', + integrationType: 'github', + url: url, + generatedEntities: await this.generateEntityDefinitions({ + repo: url, + }), + }; + } + + throw new Error('Invalid url'); + } + + async submitPullRequest({ + repositoryUrl, + fileContent, + title, + body, + }: { + repositoryUrl: string; + fileContent: string; + title: string; + body: string; + }): Promise<{ link: string; location: string }> { + const ghConfig = getGithubIntegrationConfig(this.configApi, repositoryUrl); + + if (ghConfig) { + return await this.submitGitHubPrToRepo({ + ...ghConfig, + fileContent, + title, + body, + }); + } + + throw new Error('unimplemented!'); + } + + // TODO: this could be part of the catalog api + private async generateEntityDefinitions({ repo, }: { repo: string; }): Promise { + const idToken = await this.identityApi.getIdToken(); const response = await fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/analyze-location`, { headers: { 'Content-Type': 'application/json', + ...(idToken && { Authorization: `Bearer ${idToken}` }), }, method: 'POST', body: JSON.stringify({ @@ -64,41 +158,23 @@ export class CatalogImportClient implements CatalogImportApi { return payload.generateEntities.map((x: any) => x.entity); } - async createRepositoryLocation({ - location, - }: { - location: string; - }): Promise { - const response = await fetch( - `${await this.discoveryApi.getBaseUrl('catalog')}/locations`, - { - headers: { - 'Content-Type': 'application/json', - }, - method: 'POST', - body: JSON.stringify({ - type: 'url', - target: location, - presence: 'optional', - }), - }, - ); - if (!response.ok) { - throw new Error( - `Received http response ${response.status}: ${response.statusText}`, - ); - } - } - - async checkForExistingCatalogInfo({ + // TODO: this response should better be part of the analyze-locations response and scm-independent / implemented per scm + private async checkGitHubForExistingCatalogInfo({ + url, owner, repo, githubIntegrationConfig, }: { + url: string; owner: string; repo: string; githubIntegrationConfig: GitHubIntegrationConfig; - }): Promise<{ exists: boolean; url?: string }> { + }): Promise< + Array<{ + target: string; + entities: EntityName[]; + }> + > { const token = await this.githubAuthApi.getAccessToken(['repo']); const octo = new Octokit({ auth: token, @@ -122,28 +198,50 @@ export class CatalogImportClient implements CatalogImportApi { }); 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 { - url: `blob/${defaultBranch}/${catalogInfoItem}`, - exists, - }; + return await Promise.all( + searchResult.data.items + .map( + i => `${url.replace(/[\/]*$/, '')}/blob/${defaultBranch}/${i.path}`, + ) + .map( + async i => + ({ + target: i, + entities: ( + await this.catalogApi.addLocation({ + type: 'url', + target: i, + dryRun: true, + }) + ).entities.map(e => ({ + kind: e.kind, + namespace: e.metadata.namespace ?? 'default', + name: e.metadata.name, + })), + } as { + target: string; + entities: EntityName[]; + }), + ), + ); } - return { exists }; + + return []; } - async submitPrToRepo({ + // TODO: extract this function and implement for non-github + private async submitGitHubPrToRepo({ owner, repo, + title, + body, fileContent, githubIntegrationConfig, }: { owner: string; repo: string; + title: string; + body: string; fileContent: string; githubIntegrationConfig: GitHubIntegrationConfig; }): Promise<{ link: string; location: string }> { @@ -212,23 +310,13 @@ export class CatalogImportClient implements CatalogImportApi { ); }); - const appTitle = - this.configApi.getOptionalString('app.title') ?? 'Backstage'; - const appBaseUrl = this.configApi.getString('app.baseUrl'); - - const prBody = `This pull request adds a **Backstage entity metadata file** \ -to this repository so that the component can be added to the \ -[${appTitle} software catalog](${appBaseUrl}).\n\nAfter this pull request is merged, \ -the component will become available.\n\nFor more information, read an \ -[overview of the Backstage software catalog](https://backstage.io/docs/features/software-catalog/software-catalog-overview).`; - const pullRequestResponse = await octo.pulls .create({ owner, repo, - title: `Add ${fileName} config file`, + title, head: branchName, - body: prBody, + body, base: repoData.data.default_branch, }) .catch(e => { diff --git a/plugins/catalog-import/src/api/GitHub.ts b/plugins/catalog-import/src/api/GitHub.ts new file mode 100644 index 0000000000..6a798ec201 --- /dev/null +++ b/plugins/catalog-import/src/api/GitHub.ts @@ -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 { ConfigApi } from '@backstage/core'; +import { ScmIntegrations } from '@backstage/integration'; +import parseGitUrl from 'git-url-parse'; + +export const getGithubIntegrationConfig = ( + config: ConfigApi, + location: string, +) => { + const { name: repo, owner } = parseGitUrl(location); + + const scmIntegrations = ScmIntegrations.fromConfig(config); + const githubIntegrationConfig = scmIntegrations.github.byUrl(location); + + if (!githubIntegrationConfig) { + return undefined; + } + + return { + repo, + owner, + githubIntegrationConfig: githubIntegrationConfig.config, + }; +}; diff --git a/plugins/catalog-import/src/assets/catalog-import-screenshot.png b/plugins/catalog-import/src/assets/catalog-import-screenshot.png deleted file mode 100644 index 7ba8ae9eb53c078010bf2ea461f015783a46dced..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22602 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 diff --git a/plugins/catalog-import/src/components/Buttons/index.tsx b/plugins/catalog-import/src/components/Buttons/index.tsx new file mode 100644 index 0000000000..93dd96b399 --- /dev/null +++ b/plugins/catalog-import/src/components/Buttons/index.tsx @@ -0,0 +1,70 @@ +/* + * 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 { Button, CircularProgress } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import React, { ComponentProps } from 'react'; + +const useStyles = makeStyles(theme => ({ + wrapper: { + marginTop: theme.spacing(1), + marginRight: theme.spacing(1), + position: 'relative', + }, + buttonProgress: { + position: 'absolute', + top: '50%', + left: '50%', + marginTop: -12, + marginLeft: -12, + }, + button: { + marginTop: theme.spacing(1), + marginRight: theme.spacing(1), + }, +})); + +export const NextButton = ( + props: ComponentProps & { loading?: boolean }, +) => { + const { loading, ...buttonProps } = props; + const classes = useStyles(); + + return ( +

+ ); +}; + +export const BackButton = (props: ComponentProps) => { + const classes = useStyles(); + + return ( + + ); +}; diff --git a/plugins/catalog-import/src/components/ComponentConfigDisplay.tsx b/plugins/catalog-import/src/components/ComponentConfigDisplay.tsx deleted file mode 100644 index 9fb170c85c..0000000000 --- a/plugins/catalog-import/src/components/ComponentConfigDisplay.tsx +++ /dev/null @@ -1,200 +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 { Entity } from '@backstage/catalog-model'; -import { - errorApiRef, - RouteRef, - StructuredMetadataTable, - useApi, -} from '@backstage/core'; -import { - entityRoute, - entityRouteParams, -} from '@backstage/plugin-catalog-react'; -import { - Button, - CircularProgress, - Divider, - Grid, - Link, - List, - ListItem, - Typography, -} from '@material-ui/core'; -import React, { useCallback, useState } from 'react'; -import { generatePath, resolvePath } from 'react-router'; -import { Link as RouterLink } from 'react-router-dom'; -import * as YAML from 'yaml'; -import { PartialEntity } from '../util/types'; -import { urlType } from '../util/urls'; -import { useGithubRepos } from '../util/useGithubRepos'; -import { ConfigSpec } from './ImportComponentPage'; - -const getEntityCatalogPath = ({ - entity, - catalogRouteRef, -}: { - entity: PartialEntity; - catalogRouteRef: RouteRef; -}) => { - const relativeEntityPathInsideCatalog = generatePath( - entityRoute.path, - entityRouteParams(entity as Entity), - ); - - const resolvedAbsolutePath = resolvePath( - relativeEntityPathInsideCatalog, - catalogRouteRef.path, - )?.pathname; - - return resolvedAbsolutePath; -}; - -type Props = { - nextStep: (options?: { reset: boolean }) => void; - configFile: ConfigSpec; - savePRLink: (PRLink: string) => void; - catalogRouteRef: RouteRef; -}; - -const ComponentConfigDisplay = ({ - nextStep, - configFile, - savePRLink, - catalogRouteRef, -}: Props) => { - const [errorOccurred, setErrorOccurred] = useState(false); - const [submitting, setSubmitting] = useState(false); - const errorApi = useApi(errorApiRef); - const { submitPrToRepo, addLocation } = useGithubRepos(); - const onNext = useCallback(async () => { - try { - setSubmitting(true); - if (urlType(configFile.location) === 'tree') { - const result = await submitPrToRepo(configFile); - savePRLink(result.link); - setSubmitting(false); - nextStep(); - } else { - addLocation(configFile.location); - setSubmitting(false); - nextStep(); - } - } catch (e) { - setErrorOccurred(true); - setSubmitting(false); - errorApi.post(e); - } - }, [submitPrToRepo, configFile, nextStep, savePRLink, errorApi, addLocation]); - - return ( - - {urlType(configFile.location) === 'tree' ? ( - - Following config object will be submitted in a pull request to the - repository{' '} - - {configFile.location} - {' '} - and added as a new location to the backend - - ) : ( - - Following config object will be added as a new location to the backend{' '} - - {configFile.location} - - - )} - - - {urlType(configFile.location) === 'tree' ? ( -
{YAML.stringify(configFile.config)}
- ) : ( - - {configFile.config.map((entity: any, index: number) => { - const entityPath = getEntityCatalogPath({ - entity, - catalogRouteRef, - }); - return ( - - - - {entityPath} - - ), - }} - /> - - {index < configFile.config.length - 1 && ( - - )} - - ); - })} - - )} -
- - {submitting ? ( - - - - ) : null} - - - {errorOccurred ? ( - - ) : null} - - -
- ); -}; - -export default ComponentConfigDisplay; diff --git a/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx b/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx new file mode 100644 index 0000000000..d94a1cad19 --- /dev/null +++ b/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx @@ -0,0 +1,165 @@ +/* + * 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 { Entity, EntityName } from '@backstage/catalog-model'; +import { + EntityRefLink, + formatEntityRefTitle, +} from '@backstage/plugin-catalog-react'; +import { + Collapse, + IconButton, + List, + ListItem, + ListItemIcon, + ListItemSecondaryAction, + ListItemText, +} from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import ApartmentIcon from '@material-ui/icons/Apartment'; +import CategoryIcon from '@material-ui/icons/Category'; +import ExpandLessIcon from '@material-ui/icons/ExpandLess'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import ExtensionIcon from '@material-ui/icons/Extension'; +import GroupIcon from '@material-ui/icons/Group'; +import LocationOnIcon from '@material-ui/icons/LocationOn'; +import MemoryIcon from '@material-ui/icons/Memory'; +import PersonIcon from '@material-ui/icons/Person'; +import WorkIcon from '@material-ui/icons/Work'; +import React, { useState } from 'react'; + +const useStyles = makeStyles(theme => ({ + nested: { + paddingLeft: theme.spacing(4), + }, +})); + +function sortEntities(entities: Array) { + return entities.sort((a, b) => + formatEntityRefTitle(a).localeCompare(formatEntityRefTitle(b)), + ); +} + +function getEntityIcon(entity: { kind: string }): React.ReactElement { + switch (entity.kind.toLowerCase()) { + case 'api': + return ; + + case 'component': + return ; + + case 'domain': + return ; + + case 'group': + return ; + + case 'location': + return ; + + case 'system': + return ; + + case 'user': + return ; + + default: + return ; + } +} + +type Props = { + locations: Array<{ target: string; entities: (Entity | EntityName)[] }>; + locationListItemIcon: (target: string) => React.ReactElement; + collapsed?: boolean; + firstListItem?: React.ReactElement; + onItemClick?: (target: string) => void; + withLinks?: boolean; +}; + +export const EntityListComponent = ({ + locations, + collapsed = false, + locationListItemIcon, + onItemClick, + firstListItem, + withLinks = false, +}: Props) => { + const classes = useStyles(); + + const [expandedUrls, setExpandedUrls] = useState([]); + + const handleClick = (url: string) => { + setExpandedUrls(urls => + urls.includes(url) ? urls.filter(u => u !== url) : urls.concat(url), + ); + }; + + return ( + + {firstListItem} + {locations.map(r => ( + + onItemClick?.call(this, r.target)} + > + {locationListItemIcon(r.target)} + + + + {collapsed && ( + + handleClick(r.target)}> + {expandedUrls.includes(r.target) ? ( + + ) : ( + + )} + + + )} + + + + + {sortEntities(r.entities).map(entity => ( + + {getEntityIcon(entity)} + + + ))} + + + + ))} + + ); +}; diff --git a/plugins/catalog-import/src/components/EntityListComponent/index.ts b/plugins/catalog-import/src/components/EntityListComponent/index.ts new file mode 100644 index 0000000000..fc0efc5481 --- /dev/null +++ b/plugins/catalog-import/src/components/EntityListComponent/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { EntityListComponent } from './EntityListComponent'; diff --git a/plugins/catalog-import/src/components/ImportComponentForm.test.tsx b/plugins/catalog-import/src/components/ImportComponentForm.test.tsx deleted file mode 100644 index 38b1065325..0000000000 --- a/plugins/catalog-import/src/components/ImportComponentForm.test.tsx +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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 { CatalogClient } from '@backstage/catalog-client'; -import { - ApiProvider, - ApiRegistry, - DiscoveryApi, - errorApiRef, -} from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; -import { fireEvent, screen, waitFor } from '@testing-library/react'; -import React from 'react'; -import { catalogImportApiRef, CatalogImportClient } from '../api'; -import { RegisterComponentForm } from './ImportComponentForm'; - -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/ImportComponentForm.tsx b/plugins/catalog-import/src/components/ImportComponentForm.tsx deleted file mode 100644 index b3ec050ea3..0000000000 --- a/plugins/catalog-import/src/components/ImportComponentForm.tsx +++ /dev/null @@ -1,154 +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 { errorApiRef, useApi } from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { BackstageTheme } from '@backstage/theme'; -import { - Button, - FormControl, - FormHelperText, - TextField, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import React from 'react'; -import { useForm } from 'react-hook-form'; -import { useMountedState } from 'react-use'; -import { urlType } from '../util/urls'; -import { useGithubRepos } from '../util/useGithubRepos'; -import { ComponentIdValidators } from '../util/validate'; -import { ConfigSpec } from './ImportComponentPage'; - -const useStyles = makeStyles(theme => ({ - form: { - alignItems: 'flex-start', - display: 'flex', - flexFlow: 'column nowrap', - }, - submit: { - marginTop: theme.spacing(1), - }, -})); - -type Props = { - nextStep: () => void; - saveConfig: (configFile: ConfigSpec) => void; - repository: string; -}; - -export const RegisterComponentForm = ({ - nextStep, - saveConfig, - repository, -}: Props) => { - const { register, handleSubmit, errors, formState } = useForm({ - mode: 'onChange', - }); - const classes = useStyles(); - const hasErrors = !!errors.componentLocation; - const dirty = formState?.isDirty; - const catalogApi = useApi(catalogApiRef); - - const isMounted = useMountedState(); - const errorApi = useApi(errorApiRef); - const { - generateEntityDefinitions, - checkForExistingCatalogInfo, - } = useGithubRepos(); - - const onSubmit = async (formData: Record) => { - const { componentLocation: target } = formData; - async function saveCatalogFileConfig(targetString: string) { - const data = await catalogApi.addLocation({ target: targetString }); - saveConfig({ - type: 'file', - location: data.location.target, - config: data.entities, - }); - } - - async function trySaveRepositoryConfig(targetString: string) { - const existingCatalog = await checkForExistingCatalogInfo(targetString); - if (existingCatalog.exists) { - const targetUrl = targetString.endsWith('/') - ? `${targetString}${existingCatalog.url}` - : `${targetString}/${existingCatalog.url}`; - await saveCatalogFileConfig(targetUrl); - } else { - saveConfig({ - type: 'tree', - location: target, - config: await generateEntityDefinitions(target), - }); - } - } - - try { - if (!isMounted()) return; - const type = urlType(target); - if (type === 'tree') { - await trySaveRepositoryConfig(target); - } else { - await saveCatalogFileConfig(target); - } - nextStep(); - } catch (e) { - errorApi.post(e); - } - }; - - return ( -
- - - - {errors.componentLocation && ( - - {errors.componentLocation.message} - - )} - - - -
- ); -}; diff --git a/plugins/catalog-import/src/components/ImportComponentPage.test.tsx b/plugins/catalog-import/src/components/ImportComponentPage.test.tsx index f6ef07e1b2..4cadea85c0 100644 --- a/plugins/catalog-import/src/components/ImportComponentPage.test.tsx +++ b/plugins/catalog-import/src/components/ImportComponentPage.test.tsx @@ -13,254 +13,72 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { CatalogClient } from '@backstage/catalog-client'; import { ApiProvider, ApiRegistry, configApiRef, - errorApiRef, + ConfigReader, } from '@backstage/core'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { msw, renderInTestApp } from '@backstage/test-utils'; -import { fireEvent, screen, waitFor } from '@testing-library/react'; -import { rest } from 'msw'; -import { setupServer } from 'msw/node'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { act, render } from '@testing-library/react'; import React from 'react'; import { catalogImportApiRef, CatalogImportClient } from '../api'; import { ImportComponentPage } from './ImportComponentPage'; -let codeSearchMockResponse: () => Promise<{ - data: { - total_count: number; - items: Array<{ path: string }>; - }; -}>; - -let findGithubConfigMockResponse = () => ({ - host: 'test.localhost', - owner: 'someuser', -}); - -jest.mock('@backstage/integration', () => ({ - readGitHubIntegrationConfigs: () => ({ - find: findGithubConfigMockResponse, - }), -})); - -jest.mock('@octokit/rest', () => ({ - Octokit: jest.fn().mockImplementation(() => { - return { - repos: { - get: () => - Promise.resolve({ - data: { - default_branch: 'main', - }, - }), - }, - search: { - code: codeSearchMockResponse, - }, - }; - }), -})); - -const OUR_GITHUB_TEST_REPO = 'https://github.com/someuser/somerepo'; 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()); + const identityApi = { + getUserId: () => { + return 'user'; + }, + getProfile: () => { + return {}; + }, + getIdToken: () => { + return Promise.resolve('token'); + }, + signOut: () => { + return Promise.resolve(); + }, + }; 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 }, - }), - ], - [ + apis = ApiRegistry.with( + configApiRef, + new ConfigReader({ integrations: {} }), + ) + .with(catalogApiRef, new CatalogClient({ discoveryApi: {} as any })) + .with( catalogImportApiRef, new CatalogImportClient({ - discoveryApi: { getBaseUrl }, + discoveryApi: {} as any, githubAuthApi: { - getAccessToken: (_, __) => Promise.resolve('token'), + getAccessToken: async () => 'token', }, + identityApi, configApi: {} as any, + catalogApi: {} as any, }), - ], - [ - configApiRef, - { - getOptional: () => 'Title', - getOptionalConfigArray: () => [], - has: () => true, - getConfig: () => ({ - has: () => true, - }), - }, - ], - [errorApiRef, mockErrorApi], - ]); + ); }); - async function renderSUT() { - return await renderInTestApp( - - - , - ); - } - - it('Should not explode on non-Github URLs', async () => { - findGithubConfigMockResponse = () => undefined!!; - 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 () => { - findGithubConfigMockResponse = () => undefined!!; - 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 () => { - findGithubConfigMockResponse = () => ({ - host: 'test.localhost', - owner: 'someuser', - }); - 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: OUR_GITHUB_TEST_REPO } }, - ); - }); - - fireEvent.click(screen.getByText('Next')); - await waitFor(() => { - expect( - screen.getByText( - 'https://github.com/someusername/somerepo/blob/master/src/catalog-info.yaml', + it('renders without exploding', async () => { + await act(async () => { + const { getByText } = render( + wrapInTestApp( + + + , ), - ).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 () => { - findGithubConfigMockResponse = () => ({ - host: 'test.localhost', - owner: 'someuser', - }); - 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: OUR_GITHUB_TEST_REPO } }, ); - }); - fireEvent.click(screen.getByText('Next')); - await waitFor(() => { - expect(screen.getByText(OUR_GITHUB_TEST_REPO)).toBeInTheDocument(); + expect( + await getByText('Start tracking your component in Backstage'), + ).toBeInTheDocument(); }); - const textNode = container - .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://github.com/someusername/somerepo/blob/master/src/catalog-info.yaml', - ), - ).not.toBeInTheDocument(); }); }); diff --git a/plugins/catalog-import/src/components/ImportComponentPage.tsx b/plugins/catalog-import/src/components/ImportComponentPage.tsx index adae137a50..1d1a8056bd 100644 --- a/plugins/catalog-import/src/components/ImportComponentPage.tsx +++ b/plugins/catalog-import/src/components/ImportComponentPage.tsx @@ -14,35 +14,21 @@ * limitations under the License. */ -import React, { useState } from 'react'; -import { Grid, Typography } from '@material-ui/core'; import { + ConfigApi, + configApiRef, + Content, + ContentHeader, + Header, InfoCard, Page, - Content, - Header, SupportButton, - ContentHeader, - RouteRef, useApi, - configApiRef, - ConfigApi, } from '@backstage/core'; -import { RegisterComponentForm } from './ImportComponentForm'; -import ImportStepper from './ImportStepper'; -import ComponentConfigDisplay from './ComponentConfigDisplay'; -import { ImportFinished } from './ImportFinished'; -import { PartialEntity } from '../util/types'; - -export type ConfigSpec = { - type: 'tree' | 'file'; - location: string; - config: PartialEntity[]; -}; - -function manifestGenerationAvailable(configApi: ConfigApi): boolean { - return configApi.has('integrations.github'); -} +import { Grid, Typography } from '@material-ui/core'; +import React from 'react'; +import { ImportStepper } from './ImportStepper'; +import { StepperProviderOpts } from './ImportStepper/defaults'; function repositories(configApi: ConfigApi): string[] { const integrations = configApi.getConfig('integrations'); @@ -62,26 +48,13 @@ function repositories(configApi: ConfigApi): string[] { return repos; } -export const ImportComponentPage = ({ - catalogRouteRef, -}: { - catalogRouteRef: RouteRef; -}) => { - const [activeStep, setActiveStep] = useState(0); - const [configFile, setConfigFile] = useState({ - type: 'tree', - location: '', - config: [], - }); - const [endLink, setEndLink] = useState(''); - const nextStep = (options?: { reset: boolean }) => { - setActiveStep(step => (options?.reset ? 0 : step + 1)); - }; - +export const ImportComponentPage = (opts: StepperProviderOpts) => { const configApi = useApi(configApiRef); const appTitle = configApi.getOptional('app.title') || 'Backstage'; + const repos = repositories(configApi); const repositoryString = repos.join(', ').replace(/, (\w*)$/, ' or $1'); + return (
@@ -92,9 +65,11 @@ export const ImportComponentPage = ({ software catalog. - - + + + - Ways to register an existing component + Enter the URL to your SCM repository to add it to {appTitle}. - - {manifestGenerationAvailable(configApi) && ( - - GitHub Repo - - 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. - - - )} - {repos.length === 1 ? `${repos[0]} ` : ''} Repository & - Entity File + Link to an existing entity file + + + Example:{' '} + + https://github.com/backstage/backstage/blob/master/catalog-info.yaml + - If you've already created a {appTitle} metadata file and put it - in your {repositoryString} repository, you can enter the full - URL to that Entity File. + The wizard analyzes the file, previews the entities, and adds + them to the {appTitle} catalog. + {repos.length > 0 && ( + <> + + Link to a {repositoryString} repository + + + Example: https://github.com/backstage/backstage + + + The wizard discovers all catalog-info.yaml{' '} + files in the repository, previews the entities, and adds + them to the {appTitle} catalog. + + {!opts?.pullRequest?.disable && ( + + If no entities are found, the wizard will prepare a Pull + Request that adds an example{' '} + catalog-info.yaml and prepares the {appTitle}{' '} + catalog to load all entities as soon as the Pull Request + is merged. + + )} + + )} - - - - ), - }, - { - step: 'Review', - content: ( - - ), - }, - { - step: 'Finish', - content: ( - - ), - }, - ]} - activeStep={activeStep} - nextStep={nextStep} - /> - + + + diff --git a/plugins/catalog-import/src/components/ImportFinished.tsx b/plugins/catalog-import/src/components/ImportFinished.tsx deleted file mode 100644 index 9b0f8575a6..0000000000 --- a/plugins/catalog-import/src/components/ImportFinished.tsx +++ /dev/null @@ -1,58 +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 { Alert } from '@material-ui/lab'; -import { Button, Grid, Link } from '@material-ui/core'; - -type Props = { - type: 'tree' | 'file'; - nextStep: (options?: { reset: boolean }) => void; - PRLink: string; -}; - -export const ImportFinished = ({ nextStep, PRLink, type }: Props) => { - return ( - - - - {type === 'tree' - ? 'Pull requests have been successfully opened. You can start again to import more repositories' - : 'Entity added to catalog successfully'} - - - - {type === 'tree' ? ( - - View pull request on GitHub - - ) : null} - - - - ); -}; diff --git a/plugins/catalog-import/src/components/ImportStepper.tsx b/plugins/catalog-import/src/components/ImportStepper.tsx deleted file mode 100644 index 03752fa7d4..0000000000 --- a/plugins/catalog-import/src/components/ImportStepper.tsx +++ /dev/null @@ -1,42 +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 Stepper from '@material-ui/core/Stepper'; -import Step from '@material-ui/core/Step'; -import StepLabel from '@material-ui/core/StepLabel'; -import { StepContent } from '@material-ui/core'; - -type Props = { - steps: { step: string; content: React.ReactNode }[]; - activeStep: number; - nextStep: (options?: { reset: boolean }) => void; -}; - -export default function ImportStepper({ steps, activeStep }: Props) { - return ( - - {steps.map(({ step }) => { - const stepProps: { completed?: boolean } = {}; - return ( - - {step} - {steps[activeStep].content} - - ); - })} - - ); -} diff --git a/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx b/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx new file mode 100644 index 0000000000..f92f03c5cb --- /dev/null +++ b/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx @@ -0,0 +1,104 @@ +/* + * 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 { configApiRef, InfoCard, useApi } from '@backstage/core'; +import { Step, StepContent, Stepper } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import React, { useMemo } from 'react'; +import { ImportFlows, ImportState, useImportState } from '../useImportState'; +import { + defaultGenerateStepper, + defaultStepper, + StepConfiguration, + StepperProvider, + StepperProviderOpts, +} from './defaults'; + +const useStyles = makeStyles(() => ({ + stepperRoot: { + padding: 0, + }, +})); + +type Props = { + initialUrl?: string; + generateStepper?: ( + flow: ImportFlows, + defaults: StepperProvider, + ) => StepperProvider; + variant?: string; + opts?: StepperProviderOpts; +}; + +export const ImportStepper = ({ + initialUrl, + generateStepper = defaultGenerateStepper, + variant, + opts, +}: Props) => { + const configApi = useApi(configApiRef); + const classes = useStyles(); + const state = useImportState({ initialUrl }); + + const states = useMemo( + () => generateStepper(state.activeFlow, defaultStepper), + [generateStepper, state.activeFlow], + ); + + const render = (step: StepConfiguration) => { + return ( + + {step.stepLabel} + {step.content} + + ); + }; + + return ( + + + {render( + states.analyze( + state as Extract, + { apis: { configApi }, opts }, + ), + )} + {render( + states.prepare( + state as Extract, + { apis: { configApi }, opts }, + ), + )} + {render( + states.review( + state as Extract, + { apis: { configApi }, opts }, + ), + )} + {render( + states.finish( + state as Extract, + { apis: { configApi }, opts }, + ), + )} + + + ); +}; diff --git a/plugins/catalog-import/src/components/ImportStepper/defaults.tsx b/plugins/catalog-import/src/components/ImportStepper/defaults.tsx new file mode 100644 index 0000000000..f2dd1d62f4 --- /dev/null +++ b/plugins/catalog-import/src/components/ImportStepper/defaults.tsx @@ -0,0 +1,293 @@ +/* + * 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 { ConfigApi } from '@backstage/core'; +import { Box, StepLabel, TextField, Typography } from '@material-ui/core'; +import React from 'react'; +import { BackButton } from '../Buttons'; +import { StepFinishImportLocation } from '../StepFinishImportLocation'; +import { StepInitAnalyzeUrl } from '../StepInitAnalyzeUrl'; +import { + AutocompleteTextField, + StepPrepareCreatePullRequest, +} from '../StepPrepareCreatePullRequest'; +import { StepPrepareSelectLocations } from '../StepPrepareSelectLocations'; +import { StepReviewLocation } from '../StepReviewLocation'; +import { ImportFlows, ImportState } from '../useImportState'; + +export type StepperProviderOpts = { + pullRequest?: { + disable?: boolean; + preparePullRequest?: (apis: StepperApis) => { title: string; body: string }; + }; +}; + +type StepperApis = { + configApi: ConfigApi; +}; + +export type StepConfiguration = { + stepLabel: React.ReactElement; + content: React.ReactElement; +}; + +export type StepperProvider = { + analyze: ( + s: Extract, + opts: { apis: StepperApis; opts?: StepperProviderOpts }, + ) => StepConfiguration; + prepare: ( + s: Extract, + opts: { apis: StepperApis; opts?: StepperProviderOpts }, + ) => StepConfiguration; + review: ( + s: Extract, + opts: { apis: StepperApis; opts?: StepperProviderOpts }, + ) => StepConfiguration; + finish: ( + s: Extract, + opts: { apis: StepperApis; opts?: StepperProviderOpts }, + ) => StepConfiguration; +}; + +function defaultPreparePullRequest(apis: StepperApis) { + const appTitle = apis.configApi.getOptionalString('app.title') ?? 'Backstage'; + const appBaseUrl = apis.configApi.getString('app.baseUrl'); + + return { + title: 'Add catalog-info.yaml config file', + body: `This pull request adds a **Backstage entity metadata file** \ +to this repository so that the component can be added to the \ +[${appTitle} software catalog](${appBaseUrl}).\n\nAfter this pull request is merged, \ +the component will become available.\n\nFor more information, read an \ +[overview of the Backstage software catalog](https://backstage.io/docs/features/software-catalog/software-catalog-overview).`, + }; +} + +/** + * The default stepper generation function. + * + * Override this function to customize the import flow. Each flow should at + * least override the prepare operation. + * + * @param flow the name of the active flow + * @param defaults the default steps + */ +export function defaultGenerateStepper( + flow: ImportFlows, + defaults: StepperProvider, +): StepperProvider { + switch (flow) { + // the prepare step is skipped but the label of the step is updated + case 'single-location': + return { + ...defaults, + prepare: () => ({ + stepLabel: ( + + Discovered Locations: 1 + + } + > + Select Locations + + ), + content: <>, + }), + }; + + // let the user select one or more of the discovered locations in the prepare step + case 'multiple-locations': + return { + ...defaults, + prepare: (state, opts) => { + if (state.analyzeResult.type !== 'locations') { + return defaults.prepare(state, opts); + } + + return { + stepLabel: ( + + Discovered Locations: {state.analyzeResult.locations.length} + + } + > + Select Locations + + ), + content: ( + + ), + }; + }, + }; + + case 'no-location': + return { + ...defaults, + prepare: (state, opts) => { + if (state.analyzeResult.type !== 'repository') { + return defaults.prepare(state, opts); + } + + const { title, body } = ( + opts?.opts?.pullRequest?.preparePullRequest ?? + defaultPreparePullRequest + )(opts.apis); + + return { + stepLabel: Create Pull Request, + content: ( + ( + <> + + Pull Request Details + + + + + + + + Entity Configuration + + + + + + + )} + /> + ), + }; + }, + }; + + default: + return defaults; + } +} + +export const defaultStepper: StepperProvider = { + analyze: (state, { opts }) => ({ + stepLabel: Select URL, + content: ( + + ), + }), + + prepare: state => ({ + stepLabel: ( + Optional}> + Import Actions + + ), + content: , + }), + + review: state => ({ + stepLabel: Review, + content: ( + + ), + }), + + finish: state => ({ + stepLabel: Finish, + content: ( + + ), + }), +}; diff --git a/plugins/catalog-import/src/components/ImportStepper/index.ts b/plugins/catalog-import/src/components/ImportStepper/index.ts new file mode 100644 index 0000000000..8d205562a4 --- /dev/null +++ b/plugins/catalog-import/src/components/ImportStepper/index.ts @@ -0,0 +1,18 @@ +/* + * 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 { ImportStepper } from './ImportStepper'; +export { defaultGenerateStepper } from './defaults'; diff --git a/plugins/catalog-import/src/components/Router.tsx b/plugins/catalog-import/src/components/Router.tsx index 5175f8f077..fc74c32b30 100644 --- a/plugins/catalog-import/src/components/Router.tsx +++ b/plugins/catalog-import/src/components/Router.tsx @@ -14,15 +14,13 @@ * limitations under the License. */ -import { RouteRef } from '@backstage/core'; import React from 'react'; import { Route, Routes } from 'react-router-dom'; import { ImportComponentPage } from './ImportComponentPage'; +import { StepperProviderOpts } from './ImportStepper/defaults'; -export const Router = ({ catalogRouteRef }: { catalogRouteRef: RouteRef }) => ( +export const Router = (opts: StepperProviderOpts) => ( - } - /> + } /> ); diff --git a/plugins/catalog-import/src/components/StepFinishImportLocation/StepFinishImportLocation.tsx b/plugins/catalog-import/src/components/StepFinishImportLocation/StepFinishImportLocation.tsx new file mode 100644 index 0000000000..cc5eb9adef --- /dev/null +++ b/plugins/catalog-import/src/components/StepFinishImportLocation/StepFinishImportLocation.tsx @@ -0,0 +1,65 @@ +/* + * 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 { Link } from '@backstage/core'; +import { Grid, Typography } from '@material-ui/core'; +import LocationOnIcon from '@material-ui/icons/LocationOn'; +import React from 'react'; +import { BackButton } from '../Buttons'; +import { EntityListComponent } from '../EntityListComponent'; +import { ReviewResult } from '../useImportState'; + +type Props = { + reviewResult: ReviewResult; + onReset: () => void; +}; + +export const StepFinishImportLocation = ({ reviewResult, onReset }: Props) => ( + <> + {reviewResult.type === 'repository' && ( + <> + + The following Pull Request has been opened:{' '} + + {reviewResult.pullRequest.url} + + + + + Your entities will be imported as soon as the Pull Request is merged. + + + )} + + + The following entities have been added to the catalog: + + + } + withLinks + /> + + + Register another + + +); diff --git a/plugins/catalog-import/src/components/StepFinishImportLocation/index.ts b/plugins/catalog-import/src/components/StepFinishImportLocation/index.ts new file mode 100644 index 0000000000..5f3d9f425c --- /dev/null +++ b/plugins/catalog-import/src/components/StepFinishImportLocation/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { StepFinishImportLocation } from './StepFinishImportLocation'; diff --git a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx new file mode 100644 index 0000000000..bfb2434926 --- /dev/null +++ b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx @@ -0,0 +1,401 @@ +/* + * 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 { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; +import { act, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { AnalyzeResult, catalogImportApiRef } from '../../api/'; +import { StepInitAnalyzeUrl } from './StepInitAnalyzeUrl'; + +describe('', () => { + const catalogImportApi: jest.Mocked = { + analyzeUrl: jest.fn(), + submitPullRequest: jest.fn(), + }; + + const errorApi: jest.Mocked = { + post: jest.fn(), + error$: jest.fn(), + }; + + const Wrapper = ({ children }: { children?: React.ReactNode }) => ( + + {children} + + ); + + const location = { + target: 'url', + entities: [ + { + kind: 'component', + namespace: 'default', + name: 'name', + }, + ], + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('renders without exploding', async () => { + const { getByRole } = render( + undefined} />, + { + wrapper: Wrapper, + }, + ); + + expect(getByRole('textbox', { name: /Repository/i })).toBeInTheDocument(); + expect(getByRole('textbox', { name: /Repository/i })).toHaveValue(''); + }); + + it('should use default analysis url', async () => { + const { getByRole } = render( + undefined} + analysisUrl="https://default" + />, + { + wrapper: Wrapper, + }, + ); + + expect(getByRole('textbox', { name: /Repository/i })).toBeInTheDocument(); + expect(getByRole('textbox', { name: /Repository/i })).toHaveValue( + 'https://default', + ); + }); + + it('should not analyze without url', async () => { + const onAnalysisFn = jest.fn(); + + const { getByRole } = render( + , + { + wrapper: Wrapper, + }, + ); + + await act(async () => { + userEvent.click(getByRole('button', { name: /Analyze/i })); + }); + + expect(catalogImportApi.analyzeUrl).toBeCalledTimes(0); + expect(onAnalysisFn).toBeCalledTimes(0); + expect(errorApi.post).toBeCalledTimes(0); + }); + + it('should not analyze invalid value', async () => { + const onAnalysisFn = jest.fn(); + + const { getByRole, getByText } = render( + , + { + wrapper: Wrapper, + }, + ); + + await act(async () => { + await userEvent.type( + getByRole('textbox', { name: /Repository/i }), + 'http:/', + ); + userEvent.click(getByRole('button', { name: /Analyze/i })); + }); + + expect(catalogImportApi.analyzeUrl).toBeCalledTimes(0); + expect(onAnalysisFn).toBeCalledTimes(0); + expect(errorApi.post).toBeCalledTimes(0); + expect(getByText('Must start with https://.')).toBeInTheDocument(); + }); + + it('should analyze single location', async () => { + const onAnalysisFn = jest.fn(); + + const analyzeResult = { + type: 'locations', + locations: [location], + } as AnalyzeResult; + + const { getByRole } = render( + , + { + wrapper: Wrapper, + }, + ); + + catalogImportApi.analyzeUrl.mockReturnValueOnce( + Promise.resolve(analyzeResult), + ); + + await act(async () => { + await userEvent.type( + getByRole('textbox', { name: /Repository/i }), + 'https://my-repository', + ); + userEvent.click(getByRole('button', { name: /Analyze/i })); + }); + + expect(onAnalysisFn).toBeCalledTimes(1); + expect(onAnalysisFn.mock.calls[0]).toMatchObject([ + 'single-location', + 'https://my-repository', + analyzeResult, + { prepareResult: analyzeResult }, + ]); + expect(errorApi.post).toBeCalledTimes(0); + }); + + it('should analyze multiple locations', async () => { + const onAnalysisFn = jest.fn(); + + const analyzeResult = { + type: 'locations', + locations: [location, location], + } as AnalyzeResult; + + const { getByRole } = render( + , + { + wrapper: Wrapper, + }, + ); + + catalogImportApi.analyzeUrl.mockReturnValueOnce( + Promise.resolve(analyzeResult), + ); + + await act(async () => { + await userEvent.type( + getByRole('textbox', { name: /Repository/i }), + 'https://my-repository-1', + ); + userEvent.click(getByRole('button', { name: /Analyze/i })); + }); + + expect(onAnalysisFn).toBeCalledTimes(1); + expect(onAnalysisFn.mock.calls[0]).toMatchObject([ + 'multiple-locations', + 'https://my-repository-1', + analyzeResult, + ]); + expect(errorApi.post).toBeCalledTimes(0); + }); + + it('should not analyze with no locations', async () => { + const onAnalysisFn = jest.fn(); + + const analyzeResult = { + type: 'locations', + locations: [], + } as AnalyzeResult; + + const { getByRole, getByText } = render( + , + { + wrapper: Wrapper, + }, + ); + + catalogImportApi.analyzeUrl.mockReturnValueOnce( + Promise.resolve(analyzeResult), + ); + + await act(async () => { + await userEvent.type( + getByRole('textbox', { name: /Repository/i }), + 'https://my-repository-1', + ); + userEvent.click(getByRole('button', { name: /Analyze/i })); + }); + + expect(onAnalysisFn).toBeCalledTimes(0); + expect( + getByText('There are no entities at this location'), + ).toBeInTheDocument(); + expect(errorApi.post).toBeCalledTimes(0); + }); + + it('should analyze repository', async () => { + const onAnalysisFn = jest.fn(); + + const analyzeResult = { + type: 'repository', + url: 'https://my-repository-2', + integrationType: 'github', + generatedEntities: [ + { + apiVersion: '1', + kind: 'component', + metadata: { + name: 'component-a', + }, + }, + ], + } as AnalyzeResult; + + const { getByRole } = render( + , + { + wrapper: Wrapper, + }, + ); + + catalogImportApi.analyzeUrl.mockReturnValueOnce( + Promise.resolve(analyzeResult), + ); + + await act(async () => { + await userEvent.type( + getByRole('textbox', { name: /Repository/i }), + 'https://my-repository-2', + ); + userEvent.click(getByRole('button', { name: /Analyze/i })); + }); + + expect(onAnalysisFn).toBeCalledTimes(1); + expect(onAnalysisFn.mock.calls[0]).toMatchObject([ + 'no-location', + 'https://my-repository-2', + analyzeResult, + ]); + expect(errorApi.post).toBeCalledTimes(0); + }); + + it('should not analyze repository without entities', async () => { + const onAnalysisFn = jest.fn(); + + const analyzeResult = { + type: 'repository', + url: 'https://my-repository-2', + integrationType: 'github', + generatedEntities: [], + } as AnalyzeResult; + + const { getByRole, getByText } = render( + , + { + wrapper: Wrapper, + }, + ); + + catalogImportApi.analyzeUrl.mockReturnValueOnce( + Promise.resolve(analyzeResult), + ); + + await act(async () => { + await userEvent.type( + getByRole('textbox', { name: /Repository/i }), + 'https://my-repository-2', + ); + userEvent.click(getByRole('button', { name: /Analyze/i })); + }); + + expect(onAnalysisFn).toBeCalledTimes(0); + expect( + getByText("Couldn't generate entities for your repository"), + ).toBeInTheDocument(); + expect(errorApi.post).toBeCalledTimes(0); + }); + + it('should not analyze repository if disabled', async () => { + const onAnalysisFn = jest.fn(); + + const analyzeResult = { + type: 'repository', + url: 'https://my-repository-2', + integrationType: 'github', + generatedEntities: [ + { + apiVersion: '1', + kind: 'component', + metadata: { + name: 'component-a', + }, + }, + ], + } as AnalyzeResult; + + const { getByRole, getByText } = render( + , + { + wrapper: Wrapper, + }, + ); + + catalogImportApi.analyzeUrl.mockReturnValueOnce( + Promise.resolve(analyzeResult), + ); + + await act(async () => { + await userEvent.type( + getByRole('textbox', { name: /Repository/i }), + 'https://my-repository-2', + ); + userEvent.click(getByRole('button', { name: /Analyze/i })); + }); + + expect(onAnalysisFn).toBeCalledTimes(0); + expect( + getByText("Couldn't generate entities for your repository"), + ).toBeInTheDocument(); + expect(errorApi.post).toBeCalledTimes(0); + }); + + it('should report unknown type to the errorapi', async () => { + const onAnalysisFn = jest.fn(); + + const { getByRole, getByText } = render( + , + { + wrapper: Wrapper, + }, + ); + + catalogImportApi.analyzeUrl.mockReturnValueOnce( + Promise.resolve(({ type: 'unknown' } as any) as AnalyzeResult), + ); + + await act(async () => { + await userEvent.type( + getByRole('textbox', { name: /Repository/i }), + 'https://my-repository-2', + ); + userEvent.click(getByRole('button', { name: /Analyze/i })); + }); + + expect(onAnalysisFn).toBeCalledTimes(0); + expect( + getByText( + 'Received unknown analysis result of type unknown. Please contact the support team.', + ), + ).toBeInTheDocument(); + expect(errorApi.post).toBeCalledTimes(1); + expect(errorApi.post.mock.calls[0][0]).toMatchObject( + new Error( + 'Received unknown analysis result of type unknown. Please contact the support team.', + ), + ); + }); +}); diff --git a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx new file mode 100644 index 0000000000..a33fe388d8 --- /dev/null +++ b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx @@ -0,0 +1,159 @@ +/* + * 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 { errorApiRef, useApi } from '@backstage/core'; +import { FormHelperText, Grid, TextField } from '@material-ui/core'; +import React, { useCallback, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { AnalyzeResult, catalogImportApiRef } from '../../api'; +import { NextButton } from '../Buttons'; +import { ImportFlows, PrepareResult } from '../useImportState'; + +type FormData = { + url: string; +}; + +type Props = { + onAnalysis: ( + flow: ImportFlows, + url: string, + result: AnalyzeResult, + opts?: { prepareResult?: PrepareResult }, + ) => void; + disablePullRequest?: boolean; + analysisUrl?: string; +}; + +/** + * A form that lets the user input a url and analyze it for existing locations or potential entities. + * + * @param onAnalysis is called when the analysis was successful + * @param analysisUrl a url that can be used as a default value + * @param disablePullRequest if true, repositories without entities will abort the wizard + */ +export const StepInitAnalyzeUrl = ({ + onAnalysis, + analysisUrl = '', + disablePullRequest = false, +}: Props) => { + const errorApi = useApi(errorApiRef); + const catalogImportApi = useApi(catalogImportApiRef); + + const { register, handleSubmit, errors, watch } = useForm({ + mode: 'onTouched', + defaultValues: { + url: analysisUrl, + }, + }); + + const [submitted, setSubmitted] = useState(false); + const [error, setError] = useState(undefined); + + const handleResult = useCallback( + async ({ url }: FormData) => { + setSubmitted(true); + + try { + const analysisResult = await catalogImportApi.analyzeUrl(url); + + switch (analysisResult.type) { + case 'repository': + if ( + !disablePullRequest && + analysisResult.generatedEntities.length > 0 + ) { + onAnalysis('no-location', url, analysisResult); + } else { + setError("Couldn't generate entities for your repository"); + setSubmitted(false); + } + break; + + case 'locations': { + if (analysisResult.locations.length === 1) { + onAnalysis('single-location', url, analysisResult, { + prepareResult: analysisResult, + }); + } else if (analysisResult.locations.length > 1) { + onAnalysis('multiple-locations', url, analysisResult); + } else { + setError('There are no entities at this location'); + setSubmitted(false); + } + break; + } + + default: { + const err = `Received unknown analysis result of type ${ + (analysisResult as any).type + }. Please contact the support team.`; + setError(err); + setSubmitted(false); + + errorApi.post(new Error(err)); + break; + } + } + } catch (e) { + setError(e.message); + setSubmitted(false); + } + }, + [catalogImportApi, disablePullRequest, errorApi, onAnalysis], + ); + + return ( +
+ + (typeof value === 'string' && + value.match(/^https:\/\//) !== null) || + 'Must start with https://.', + }, + })} + required + /> + + {errors.url && ( + {errors.url.message} + )} + + {error && {error}} + + + + Analyze + + + + ); +}; diff --git a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/index.ts b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/index.ts new file mode 100644 index 0000000000..5d36c104c3 --- /dev/null +++ b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { StepInitAnalyzeUrl } from './StepInitAnalyzeUrl'; diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/AutocompleteTextField.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/AutocompleteTextField.tsx new file mode 100644 index 0000000000..3231c3c6a1 --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/AutocompleteTextField.tsx @@ -0,0 +1,100 @@ +/* + * 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 { CircularProgress, TextField } from '@material-ui/core'; +import { TextFieldProps } from '@material-ui/core/TextField/TextField'; +import { Autocomplete } from '@material-ui/lab'; +import React from 'react'; +import { + Control, + Controller, + FieldErrors, + ValidationRules, +} from 'react-hook-form'; + +type Props = { + name: TFieldValue; + options: string[]; + required?: boolean; + + control?: Control>; + errors?: FieldErrors>; + rules?: ValidationRules; + + loading?: boolean; + loadingText?: string; + + helperText?: React.ReactNode; + errorHelperText?: string; + + textFieldProps?: Omit; +}; + +export const AutocompleteTextField = ({ + name, + options, + required, + control, + errors, + rules, + loading = false, + loadingText, + helperText, + errorHelperText, + textFieldProps = {}, +}: Props) => { + return ( + ( + onChange(v || '')} + onBlur={onBlur} + value={value} + autoSelect + freeSolo + renderInput={params => ( + + {loading ? ( + + ) : null} + {params.InputProps.endAdornment} + + ), + }} + {...textFieldProps} + /> + )} + /> + )} + /> + ); +}; diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.test.tsx new file mode 100644 index 0000000000..0bf7746e6a --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.test.tsx @@ -0,0 +1,113 @@ +/* + * 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 { FormHelperText, TextField } from '@material-ui/core'; +import { act, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { PreparePullRequestForm } from './PreparePullRequestForm'; + +describe('', () => { + it('renders without exploding', async () => { + const onSubmitFn = jest.fn(); + + const { getByRole } = render( + ( + <> + + {' '} + + )} + onSubmit={onSubmitFn} + />, + ); + + await act(async () => { + userEvent.click(getByRole('button', { name: /submit/i })); + }); + + expect(onSubmitFn).toBeCalledTimes(1); + expect(onSubmitFn.mock.calls[0][0]).toMatchObject({ main: 'default' }); + }); + + it('should register a text field', async () => { + const onSubmitFn = jest.fn(); + + const { getByRole, getByLabelText } = render( + ( + <> + + + + )} + onSubmit={onSubmitFn} + />, + ); + + await act(async () => { + userEvent.clear(getByLabelText('Main Field')); + await userEvent.type(getByLabelText('Main Field'), 'My Text'); + userEvent.click(getByRole('button', { name: /submit/i })); + }); + + expect(onSubmitFn).toBeCalledTimes(1); + expect(onSubmitFn.mock.calls[0][0]).toMatchObject({ main: 'My Text' }); + }); + + it('registers required attribute', async () => { + const onSubmitFn = jest.fn(); + + const { queryByText, getByRole } = render( + ( + <> + + {errors.main && ( + + Error in required main field + + )} + {' '} + + )} + onSubmit={onSubmitFn} + />, + ); + + expect(queryByText('Error in required main field')).not.toBeInTheDocument(); + + await act(async () => { + userEvent.click(getByRole('button', { name: /submit/i })); + }); + + expect(onSubmitFn).not.toBeCalled(); + expect(queryByText('Error in required main field')).toBeInTheDocument(); + }); +}); diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.tsx new file mode 100644 index 0000000000..9d59f4e10e --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.tsx @@ -0,0 +1,67 @@ +/* + * 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 { + SubmitHandler, + UnpackNestedValue, + useForm, + UseFormMethods, + UseFormOptions, +} from 'react-hook-form'; + +type Props> = Pick< + UseFormOptions, + 'defaultValues' +> & { + onSubmit: SubmitHandler; + + render: ( + props: Pick< + UseFormMethods, + 'errors' | 'register' | 'control' + > & { + values: UnpackNestedValue; + }, + ) => React.ReactNode; +}; + +/** + * A form wrapper that creates a form that is used to prepare a pull request. It + * hosts the form logic. + * + * @param defaultValues the default values of the form + * @param onSubmit a callback that is executed when the form is submitted + * (initiated by a button of type="submit") + * @param render render the form elements + */ +export const PreparePullRequestForm = < + TFieldValues extends Record +>({ + defaultValues, + onSubmit, + render, +}: Props) => { + const { handleSubmit, watch, control, register, errors } = useForm< + TFieldValues + >({ mode: 'onTouched', defaultValues }); + + return ( +
+ {render({ values: watch(), errors, register, control })} +
+ ); +}; diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx new file mode 100644 index 0000000000..5bc22dbd42 --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.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 { Entity } from '@backstage/catalog-model'; +import { makeStyles } from '@material-ui/core'; +import { render } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import React from 'react'; +import { PreviewCatalogInfoComponent } from './PreviewCatalogInfoComponent'; + +const useStyles = makeStyles({ + displayNone: { + display: 'none', + }, +}); + +const entities: Entity[] = [ + { + apiVersion: '1', + kind: 'Kind', + metadata: { + name: 'name', + }, + }, + { + apiVersion: '1', + kind: 'Kind_2', + metadata: { + name: 'name', + }, + }, +]; + +describe('', () => { + it('renders without exploding', async () => { + const { getByText } = render( + , + ); + + const repositoryUrl = getByText('http://my-repository/a/catalog-info.yaml'); + const kindText = getByText('Kind_2'); + expect(repositoryUrl).toBeInTheDocument(); + expect(repositoryUrl).toBeVisible(); + expect(kindText).toBeInTheDocument(); + expect(kindText).toBeVisible(); + }); + + it('renders card with custom styles', async () => { + const { result } = renderHook(() => useStyles()); + + const { getByText } = render( + , + ); + + const repositoryUrl = getByText('http://my-repository/a/catalog-info.yaml'); + const kindText = getByText('Kind_2'); + expect(repositoryUrl).toBeInTheDocument(); + expect(repositoryUrl).not.toBeVisible(); + expect(kindText).toBeInTheDocument(); + expect(kindText).not.toBeVisible(); + }); + + it('renders with custom styles', async () => { + const { result } = renderHook(() => useStyles()); + + const { getByText } = render( + , + ); + + const repositoryUrl = getByText('http://my-repository/a/catalog-info.yaml'); + const kindText = getByText('Kind_2'); + expect(repositoryUrl).toBeInTheDocument(); + expect(repositoryUrl).toBeVisible(); + expect(kindText).toBeInTheDocument(); + expect(kindText).not.toBeVisible(); + }); +}); diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.tsx new file mode 100644 index 0000000000..c10c011820 --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.tsx @@ -0,0 +1,56 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; +import { CodeSnippet } from '@backstage/core'; +import { Card, CardContent, CardHeader } from '@material-ui/core'; +import React from 'react'; +import YAML from 'yaml'; + +type Props = { + repositoryUrl: string; + entities: Entity[]; + classes?: { card?: string; cardContent?: string }; +}; + +export const PreviewCatalogInfoComponent = ({ + repositoryUrl, + entities, + classes, +}: Props) => { + return ( + + {`${repositoryUrl.replace( + /[\/]*$/, + '', + )}/catalog-info.yaml`} + } + /> + + + YAML.stringify(e)) + .join('---\n') + .trim()} + language="yaml" + /> + + + ); +}; diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.test.tsx new file mode 100644 index 0000000000..1556bd9b41 --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.test.tsx @@ -0,0 +1,83 @@ +/* + * 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 { makeStyles } from '@material-ui/core'; +import { render } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import React from 'react'; +import { PreviewPullRequestComponent } from './PreviewPullRequestComponent'; + +const useStyles = makeStyles({ + displayNone: { + display: 'none', + }, +}); + +describe('', () => { + it('renders without exploding', async () => { + const { getByText } = render( + , + ); + + const title = getByText('My Title'); + const description = getByText('description', { selector: 'strong' }); + expect(title).toBeInTheDocument(); + expect(title).toBeVisible(); + expect(description).toBeInTheDocument(); + expect(description).toBeVisible(); + }); + + it('renders card with custom styles', async () => { + const { result } = renderHook(() => useStyles()); + + const { getByText } = render( + , + ); + + const title = getByText('My Title'); + const description = getByText('description', { selector: 'strong' }); + expect(title).toBeInTheDocument(); + expect(title).not.toBeVisible(); + expect(description).toBeInTheDocument(); + expect(description).not.toBeVisible(); + }); + + it('renders with custom styles', async () => { + const { result } = renderHook(() => useStyles()); + + const { getByText } = render( + , + ); + + const title = getByText('My Title'); + const description = getByText('description', { selector: 'strong' }); + expect(title).toBeInTheDocument(); + expect(title).toBeVisible(); + expect(description).toBeInTheDocument(); + expect(description).not.toBeVisible(); + }); +}); diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.tsx new file mode 100644 index 0000000000..01af05d43a --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.tsx @@ -0,0 +1,40 @@ +/* + * 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 { MarkdownContent } from '@backstage/core'; +import { Card, CardContent, CardHeader } from '@material-ui/core'; +import React from 'react'; + +type Props = { + title: string; + description: string; + classes?: { card?: string; cardContent?: string }; +}; + +export const PreviewPullRequestComponent = ({ + title, + description, + classes, +}: Props) => { + return ( + + + + + + + ); +}; diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx new file mode 100644 index 0000000000..0b38fdaf58 --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx @@ -0,0 +1,287 @@ +/* + * 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 { ApiProvider, ApiRegistry } from '@backstage/core'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { TextField } from '@material-ui/core'; +import { act, render, RenderResult } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { AnalyzeResult, catalogImportApiRef } from '../../api'; +import { StepPrepareCreatePullRequest } from './StepPrepareCreatePullRequest'; + +describe('', () => { + const catalogImportApi: jest.Mocked = { + analyzeUrl: jest.fn(), + submitPullRequest: jest.fn(), + }; + + const catalogApi: jest.Mocked = { + getEntities: jest.fn(), + addLocation: jest.fn(), + getEntityByName: jest.fn(), + getLocationByEntity: jest.fn(), + getLocationById: jest.fn(), + removeEntityByUid: jest.fn(), + }; + + const Wrapper = ({ children }: { children?: React.ReactNode }) => ( + + {children} + + ); + + const onPrepareFn = jest.fn(); + const analyzeResult = { + type: 'repository', + url: 'https://my-repository', + integrationType: 'github', + generatedEntities: [ + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'my-component', + }, + spec: { + owner: 'my-owner', + }, + }, + ], + } as Extract; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('renders without exploding', async () => { + catalogApi.getEntities.mockReturnValue(Promise.resolve({ items: [] })); + + await act(async () => { + const { getByText } = render( + { + return ( + <> + + + + + + ); + }} + />, + { + wrapper: Wrapper, + }, + ); + + const title = getByText('My title'); + const description = getByText('body', { selector: 'strong' }); + expect(title).toBeInTheDocument(); + expect(title).toBeVisible(); + expect(description).toBeInTheDocument(); + expect(description).toBeVisible(); + }); + }); + + it('should submit created PR', async () => { + catalogApi.getEntities.mockReturnValue(Promise.resolve({ items: [] })); + catalogImportApi.submitPullRequest.mockReturnValue( + Promise.resolve({ + location: 'https://my/location.yaml', + link: 'https://my/pull', + }), + ); + + let result: RenderResult; + await act(async () => { + result = await render( + { + return ( + <> + + + + + + ); + }} + />, + { + wrapper: Wrapper, + }, + ); + + await userEvent.type(await result.getByLabelText('name'), '-changed'); + await userEvent.type(await result.getByLabelText('owner'), '-changed'); + await userEvent.click( + await result.getByRole('button', { name: /Create PR/i }), + ); + }); + + expect(catalogImportApi.submitPullRequest).toBeCalledTimes(1); + expect(catalogImportApi.submitPullRequest.mock.calls[0]).toMatchObject([ + { + body: 'My **body**', + fileContent: `apiVersion: "1" +kind: Component +metadata: + name: my-component-changed + namespace: default +spec: + owner: my-owner-changed +`, + repositoryUrl: 'https://my-repository', + title: 'My title', + }, + ]); + expect(onPrepareFn).toBeCalledTimes(1); + expect(onPrepareFn.mock.calls[0]).toMatchObject([ + { + type: 'repository', + url: 'https://my-repository', + integrationType: 'github', + pullRequest: { + url: 'https://my/pull', + }, + locations: [ + { + entities: [ + { + kind: 'Component', + name: 'my-component-changed', + namespace: 'default', + }, + ], + target: 'https://my/location.yaml', + }, + ], + }, + { + notRepeatable: true, + }, + ]); + }); + + it('should show error message', async () => { + catalogApi.getEntities.mockResolvedValueOnce({ items: [] }); + catalogImportApi.submitPullRequest.mockRejectedValueOnce( + new Error('some error'), + ); + + let result: RenderResult; + await act(async () => { + result = await render( + { + return ( + <> + + + + + + ); + }} + />, + { + wrapper: Wrapper, + }, + ); + + await userEvent.click( + await result.getByRole('button', { name: /Create PR/i }), + ); + }); + + expect(result!.getByText('some error')).toBeInTheDocument(); + expect(catalogImportApi.submitPullRequest).toBeCalledTimes(1); + expect(onPrepareFn).toBeCalledTimes(0); + }); + + it('should load groups', async () => { + const renderFormFieldsFn = jest.fn(); + catalogApi.getEntities.mockReturnValue( + Promise.resolve({ + items: [ + { + apiVersion: '1', + kind: 'Group', + metadata: { + name: 'my-group', + }, + }, + ], + }), + ); + + await act(async () => { + await render( + , + { + wrapper: Wrapper, + }, + ); + }); + + expect(catalogApi.getEntities).toBeCalledTimes(1); + expect(renderFormFieldsFn).toBeCalled(); + expect(renderFormFieldsFn.mock.calls[0][0]).toMatchObject({ + groups: [], + groupsLoading: true, + }); + expect( + renderFormFieldsFn.mock.calls[ + renderFormFieldsFn.mock.calls.length - 1 + ][0], + ).toMatchObject({ groups: ['Group:my-group'], groupsLoading: false }); + }); +}); diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx new file mode 100644 index 0000000000..d9e7b962ab --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx @@ -0,0 +1,249 @@ +/* + * 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 { Entity, serializeEntityRef } from '@backstage/catalog-model'; +import { useApi } from '@backstage/core'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { Box, FormHelperText, Grid, Typography } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import React, { useCallback, useState } from 'react'; +import { UseFormMethods } from 'react-hook-form'; +import { useAsync } from 'react-use'; +import YAML from 'yaml'; +import { AnalyzeResult, catalogImportApiRef } from '../../api'; +import { PartialEntity } from '../../types'; +import { BackButton, NextButton } from '../Buttons'; +import { PrepareResult } from '../useImportState'; +import { PreparePullRequestForm } from './PreparePullRequestForm'; +import { PreviewCatalogInfoComponent } from './PreviewCatalogInfoComponent'; +import { PreviewPullRequestComponent } from './PreviewPullRequestComponent'; + +const useStyles = makeStyles(theme => ({ + previewCard: { + marginTop: theme.spacing(1), + }, + previewCardContent: { + paddingTop: 0, + }, +})); + +type FormData = { + title: string; + body: string; + componentName: string; + owner: string; +}; + +type Props = { + analyzeResult: Extract; + onPrepare: ( + result: PrepareResult, + opts?: { notRepeatable?: boolean }, + ) => void; + onGoBack?: () => void; + + defaultTitle: string; + defaultBody: string; + + renderFormFields: ( + props: Pick, 'errors' | 'register' | 'control'> & { + groups: string[]; + groupsLoading: boolean; + }, + ) => React.ReactNode; +}; + +function generateEntities( + entities: PartialEntity[], + componentName: string, + owner: string, +): Entity[] { + return entities.map(e => ({ + ...e, + apiVersion: e.apiVersion!, + kind: e.kind!, + metadata: { + ...e.metadata, + name: componentName, + namespace: e.metadata?.namespace ?? 'default', + }, + spec: { + ...e.spec, + owner, + }, + })); +} + +export const StepPrepareCreatePullRequest = ({ + analyzeResult, + onPrepare, + onGoBack, + renderFormFields, + defaultTitle, + defaultBody, +}: Props) => { + const classes = useStyles(); + const catalogApi = useApi(catalogApiRef); + const catalogInfoApi = useApi(catalogImportApiRef); + + const [submitted, setSubmitted] = useState(false); + const [error, setError] = useState(); + + const { loading: groupsLoading, value: groups } = useAsync(async () => { + const groupEntities = await catalogApi.getEntities({ + filter: { kind: 'group' }, + }); + + // TODO: defaultKind (=group), defaultNamespace (=same as entity) + return groupEntities.items.map(e => serializeEntityRef(e) as string).sort(); + }); + + const handleResult = useCallback( + async (data: FormData) => { + setSubmitted(true); + + try { + const pr = await catalogInfoApi.submitPullRequest({ + repositoryUrl: analyzeResult.url, + title: data.title, + body: data.body, + fileContent: generateEntities( + analyzeResult.generatedEntities, + data.componentName, + data.owner, + ) + .map(e => YAML.stringify(e)) + .join('---\n'), + }); + + onPrepare( + { + type: 'repository', + url: analyzeResult.url, + integrationType: analyzeResult.integrationType, + pullRequest: { + url: pr.link, + }, + locations: [ + { + target: pr.location, + entities: generateEntities( + analyzeResult.generatedEntities, + data.componentName, + data.owner, + ).map(e => ({ + kind: e.kind, + namespace: e.metadata.namespace!, + name: e.metadata.name, + })), + }, + ], + }, + { notRepeatable: true }, + ); + } catch (e) { + setError(e.message); + setSubmitted(false); + } + }, + [ + analyzeResult.generatedEntities, + analyzeResult.integrationType, + analyzeResult.url, + catalogInfoApi, + onPrepare, + ], + ); + + return ( + <> + + You entered a link to a {analyzeResult.integrationType} repository but + we didn't found a catalog-info.yaml. Use this form to + create a Pull Request that creates an initial{' '} + catalog-info.yaml. + + + + onSubmit={handleResult} + defaultValues={{ + title: defaultTitle, + body: defaultBody, + owner: + (analyzeResult.generatedEntities[0]?.spec?.owner as string) || '', + componentName: + analyzeResult.generatedEntities[0]?.metadata?.name || '', + }} + render={({ values, errors, control, register }) => ( + <> + {renderFormFields({ + errors, + register, + control, + groups: groups ?? [], + groupsLoading, + })} + + + Preview Pull Request + + + + + + Preview Entities + + + + + {error && {error}} + + + {onGoBack && ( + + )} + + Create PR + + + + )} + /> + + ); +}; diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/index.ts b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/index.ts new file mode 100644 index 0000000000..119feee4a1 --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/index.ts @@ -0,0 +1,21 @@ +/* + * 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 { AutocompleteTextField } from './AutocompleteTextField'; +export { PreparePullRequestForm } from './PreparePullRequestForm'; +export { PreviewCatalogInfoComponent } from './PreviewCatalogInfoComponent'; +export { PreviewPullRequestComponent } from './PreviewPullRequestComponent'; +export { StepPrepareCreatePullRequest } from './StepPrepareCreatePullRequest'; diff --git a/plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.test.tsx b/plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.test.tsx new file mode 100644 index 0000000000..82da9b9572 --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.test.tsx @@ -0,0 +1,202 @@ +/* + * 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 { act, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { AnalyzeResult } from '../../api'; +import { StepPrepareSelectLocations } from './StepPrepareSelectLocations'; + +describe('', () => { + const analyzeResult = { + type: 'locations', + locations: [ + { + target: 'url', + entities: [ + { + kind: 'component', + namespace: 'default', + name: 'name', + }, + ], + }, + { + target: 'url-2', + entities: [ + { + kind: 'component', + namespace: 'default', + name: 'name', + }, + { + kind: 'api', + namespace: 'default', + name: 'name', + }, + ], + }, + ], + } as Extract; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('renders without exploding', async () => { + const { getByRole } = render( + undefined} + onGoBack={() => undefined} + />, + ); + + expect(getByRole('button', { name: /Review/i })).toBeDisabled(); + }); + + it('should select and deselect all', async () => { + const { getByRole, getAllByRole } = render( + undefined} + onGoBack={() => undefined} + />, + ); + + const checkboxes = getAllByRole('checkbox'); + checkboxes.forEach(c => expect(c).not.toBeChecked()); + expect(getByRole('button', { name: /Review/i })).toBeDisabled(); + + await act(async () => { + userEvent.click(getByRole('button', { name: /Select All/i })); + }); + + checkboxes.forEach(c => expect(c).toBeChecked()); + expect(getByRole('button', { name: /Review/i })).not.toBeDisabled(); + + await act(async () => { + userEvent.click(getByRole('button', { name: /Select All/i })); + }); + + checkboxes.forEach(c => expect(c).not.toBeChecked()); + expect(getByRole('button', { name: /Review/i })).toBeDisabled(); + }); + + it('should preselect prepared locations', async () => { + const { getAllByRole } = render( + undefined} + onGoBack={() => undefined} + />, + ); + + const checkboxes = getAllByRole('checkbox'); + + expect(checkboxes[0]).not.toBeChecked(); + expect(checkboxes[1]).toBeChecked(); + expect(checkboxes[2]).not.toBeChecked(); + }); + + it('should select items', async () => { + const { getAllByRole } = render( + undefined} + onGoBack={() => undefined} + />, + ); + + const checkboxes = getAllByRole('checkbox'); + checkboxes.forEach(c => expect(c).not.toBeChecked()); + + await act(async () => { + userEvent.click(checkboxes[1]); + }); + + expect(checkboxes[0]).not.toBeChecked(); + expect(checkboxes[1]).toBeChecked(); + expect(checkboxes[2]).not.toBeChecked(); + + await act(async () => { + userEvent.click(checkboxes[1]); + }); + + checkboxes.forEach(c => expect(c).not.toBeChecked()); + }); + + it('should go back', async () => { + const onGoBack = jest.fn(); + + const { getByRole } = render( + undefined} + onGoBack={onGoBack} + />, + ); + + await act(async () => { + userEvent.click(getByRole('button', { name: /Back/i })); + }); + + expect(onGoBack).toBeCalledTimes(1); + }); + + it('should submit', async () => { + const onPrepare = jest.fn(); + + const { getAllByRole, getByRole } = render( + undefined} + />, + ); + + const checkboxes = getAllByRole('checkbox'); + + await act(async () => { + userEvent.click(checkboxes[1]); + }); + + await act(async () => { + userEvent.click(getByRole('button', { name: /Review/i })); + }); + + expect(onPrepare).toBeCalledTimes(1); + expect(onPrepare.mock.calls[0][0]).toMatchObject({ + type: 'locations', + locations: [ + { + target: 'url', + entities: [ + { + kind: 'component', + namespace: 'default', + name: 'name', + }, + ], + }, + ], + } as Extract); + }); +}); diff --git a/plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.tsx b/plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.tsx new file mode 100644 index 0000000000..d44472fb5a --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.tsx @@ -0,0 +1,124 @@ +/* + * 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 { + Checkbox, + Grid, + ListItem, + ListItemIcon, + ListItemText, + Typography, +} from '@material-ui/core'; +import React, { useCallback, useState } from 'react'; +import { AnalyzeResult } from '../../api'; +import { BackButton, NextButton } from '../Buttons'; +import { EntityListComponent } from '../EntityListComponent'; +import { PrepareResult } from '../useImportState'; + +type Props = { + analyzeResult: Extract; + prepareResult?: PrepareResult; + onPrepare: (result: PrepareResult) => void; + onGoBack?: () => void; +}; + +/** + * A form that lets a user select one of a list of locations to import + * + * @param analyzeResult the result of the analysis + * @param prepareResult the selectected locations from a previous step + * @param onPrepare called after the selection + * @param onGoBack called to go back to the previous step + */ +export const StepPrepareSelectLocations = ({ + analyzeResult, + prepareResult, + onPrepare, + onGoBack, +}: Props) => { + const [selectedUrls, setSelectedUrls] = useState( + prepareResult?.locations.map(l => l.target) || [], + ); + + const handleResult = useCallback(async () => { + onPrepare({ + type: 'locations', + locations: analyzeResult.locations.filter((l: any) => + selectedUrls.includes(l.target), + ), + }); + }, [analyzeResult.locations, onPrepare, selectedUrls]); + + const onItemClick = (url: string) => { + setSelectedUrls(urls => + urls.includes(url) ? urls.filter(u => u !== url) : urls.concat(url), + ); + }; + + const onSelectAll = () => { + setSelectedUrls(urls => + urls.length < analyzeResult.locations.length + ? analyzeResult.locations.map(l => l.target) + : [], + ); + }; + + return ( + <> + + Select one or more locations that are present in your git repository: + + + + + 0 && + selectedUrls.length < analyzeResult.locations.length + } + tabIndex={-1} + disableRipple + /> + + + + } + onItemClick={onItemClick} + locations={analyzeResult.locations} + locationListItemIcon={target => ( + + )} + collapsed + /> + + + {onGoBack && } + + Review + + + + ); +}; diff --git a/plugins/catalog-import/src/components/StepPrepareSelectLocations/index.ts b/plugins/catalog-import/src/components/StepPrepareSelectLocations/index.ts new file mode 100644 index 0000000000..db98a63be0 --- /dev/null +++ b/plugins/catalog-import/src/components/StepPrepareSelectLocations/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { StepPrepareSelectLocations } from './StepPrepareSelectLocations'; diff --git a/plugins/catalog-import/src/components/StepReviewLocation/StepReviewLocation.tsx b/plugins/catalog-import/src/components/StepReviewLocation/StepReviewLocation.tsx new file mode 100644 index 0000000000..eb2268f212 --- /dev/null +++ b/plugins/catalog-import/src/components/StepReviewLocation/StepReviewLocation.tsx @@ -0,0 +1,134 @@ +/* + * 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 { configApiRef, Link, useApi } from '@backstage/core'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { FormHelperText, Grid, Typography } from '@material-ui/core'; +import LocationOnIcon from '@material-ui/icons/LocationOn'; +import React, { useCallback, useState } from 'react'; +import { BackButton, NextButton } from '../Buttons'; +import { EntityListComponent } from '../EntityListComponent'; +import { PrepareResult, ReviewResult } from '../useImportState'; + +type Props = { + prepareResult: PrepareResult; + onReview: (result: ReviewResult) => void; + onGoBack?: () => void; +}; + +export const StepReviewLocation = ({ + prepareResult, + onReview, + onGoBack, +}: Props) => { + const catalogApi = useApi(catalogApiRef); + const configApi = useApi(configApiRef); + + const appTitle = configApi.getOptional('app.title') || 'Backstage'; + + const [submitted, setSubmitted] = useState(false); + const [error, setError] = useState(); + + const handleImport = useCallback(async () => { + setSubmitted(true); + try { + const result = await Promise.all( + prepareResult.locations.map(l => + catalogApi.addLocation({ + type: 'url', + target: l.target, + presence: + prepareResult.type === 'repository' ? 'optional' : 'required', + }), + ), + ); + + onReview({ + ...prepareResult, + locations: result.map(r => ({ + target: r.location.target, + entities: r.entities, + })), + }); + } catch (e) { + // TODO: this error should be handled differently. We add it as 'optional' and + // it is not uncommon that a PR has not been merged yet. + if ( + prepareResult.type === 'repository' && + e.message.startsWith( + 'Location was added but has no entities specified yet', + ) + ) { + onReview({ + ...prepareResult, + locations: prepareResult.locations.map(l => ({ + target: l.target, + entities: [], + })), + }); + } else { + setError(e.message); + setSubmitted(false); + } + } + }, [prepareResult, onReview, catalogApi]); + + return ( + <> + {prepareResult.type === 'repository' && ( + <> + + The following Pull Request has been opened:{' '} + + {prepareResult.pullRequest.url} + + + + + You can already import the location and {appTitle} will fetch the + entities as soon as the Pull Request is merged. + + + )} + + + The following entities will be added to the catalog: + + + } + /> + + {error && {error}} + + + {onGoBack && } + handleImport()} + > + Import + + + + ); +}; diff --git a/plugins/catalog-import/src/components/StepReviewLocation/index.ts b/plugins/catalog-import/src/components/StepReviewLocation/index.ts new file mode 100644 index 0000000000..2c93e98437 --- /dev/null +++ b/plugins/catalog-import/src/components/StepReviewLocation/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { StepReviewLocation } from './StepReviewLocation'; diff --git a/plugins/catalog-import/src/components/index.ts b/plugins/catalog-import/src/components/index.ts new file mode 100644 index 0000000000..180752c873 --- /dev/null +++ b/plugins/catalog-import/src/components/index.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 * from './ImportStepper'; +export * from './EntityListComponent'; +export * from './StepInitAnalyzeUrl'; +export * from './StepPrepareCreatePullRequest'; diff --git a/plugins/catalog-import/src/components/useImportState.test.tsx b/plugins/catalog-import/src/components/useImportState.test.tsx new file mode 100644 index 0000000000..d1ef3184ad --- /dev/null +++ b/plugins/catalog-import/src/components/useImportState.test.tsx @@ -0,0 +1,372 @@ +/* + * 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, EntityName } from '@backstage/catalog-model'; +import { cleanup } from '@testing-library/react'; +import { act, renderHook } from '@testing-library/react-hooks'; +import { AnalyzeResult } from '../api'; + +import { + ImportState, + PrepareResult, + ReviewResult, + useImportState, +} from './useImportState'; + +describe('useImportState', () => { + const as = ( + curr: ImportState, + _: T, + ) => curr as Extract; + + const locationAP: AnalyzeResult & PrepareResult = { + type: 'locations', + locations: [ + { + target: 'https://0', + entities: [] as EntityName[], + }, + ], + }; + + const locationR: ReviewResult = { + type: 'locations', + locations: [ + { + target: 'https://0', + entities: [] as Entity[], + }, + ], + }; + + it('should use initial url', async () => { + const { result } = renderHook(() => + useImportState({ initialUrl: 'http://my-url' }), + ); + await cleanup(); + + expect(result.current).toMatchObject({ + activeFlow: 'unknown', + activeStepNumber: 0, + analysisUrl: 'http://my-url', + activeState: 'analyze', + analyzeResult: undefined, + prepareResult: undefined, + reviewResult: undefined, + }); + }); + + describe('onAnalysis & onPrepare & onReview & onReset', () => { + it('should work', async () => { + const { result } = renderHook(() => useImportState()); + await cleanup(); + + expect(result.current).toMatchObject({ + activeFlow: 'unknown', + activeStepNumber: 0, + analysisUrl: undefined, + activeState: 'analyze', + analyzeResult: undefined, + prepareResult: undefined, + reviewResult: undefined, + }); + + act(() => { + as(result.current, 'analyze').onAnalysis( + 'single-location', + 'http://my-url', + locationAP, + ); + }); + + expect(result.current).toMatchObject({ + activeFlow: 'single-location', + activeStepNumber: 1, + analysisUrl: 'http://my-url', + activeState: 'prepare', + analyzeResult: locationAP, + prepareResult: undefined, + reviewResult: undefined, + }); + + act(() => { + as(result.current, 'prepare').onPrepare(locationAP); + }); + + expect(result.current).toMatchObject({ + activeFlow: 'single-location', + activeStepNumber: 2, + analysisUrl: 'http://my-url', + activeState: 'review', + analyzeResult: locationAP, + prepareResult: locationAP, + reviewResult: undefined, + }); + + act(() => { + as(result.current, 'review').onReview(locationR); + }); + + expect(result.current).toMatchObject({ + activeFlow: 'single-location', + activeStepNumber: 3, + analysisUrl: 'http://my-url', + activeState: 'finish', + analyzeResult: locationAP, + prepareResult: locationAP, + reviewResult: locationR, + }); + + act(() => result.current.onReset()); + + expect(result.current).toMatchObject({ + activeFlow: 'unknown', + activeStepNumber: 0, + analysisUrl: undefined, + activeState: 'analyze', + analyzeResult: undefined, + prepareResult: undefined, + reviewResult: locationR, + }); + }); + + it('should work skipped', async () => { + const { result } = renderHook(() => useImportState()); + await cleanup(); + + expect(result.current).toMatchObject({ + activeFlow: 'unknown', + activeStepNumber: 0, + analysisUrl: undefined, + activeState: 'analyze', + analyzeResult: undefined, + prepareResult: undefined, + reviewResult: undefined, + }); + + act(() => { + as(result.current, 'analyze').onAnalysis( + 'single-location', + 'http://my-url', + locationAP, + { + prepareResult: locationAP, + }, + ); + }); + + expect(result.current).toMatchObject({ + activeFlow: 'single-location', + activeStepNumber: 2, + analysisUrl: 'http://my-url', + activeState: 'review', + analyzeResult: locationAP, + prepareResult: locationAP, + reviewResult: undefined, + }); + + act(() => { + as(result.current, 'review').onReview(locationR); + }); + + expect(result.current).toMatchObject({ + activeFlow: 'single-location', + activeStepNumber: 3, + analysisUrl: 'http://my-url', + activeState: 'finish', + analyzeResult: locationAP, + prepareResult: locationAP, + reviewResult: locationR, + }); + }); + + it('should ignore on invalid state', async () => { + const { result } = renderHook(() => useImportState()); + await cleanup(); + + // state 'analyze' + act(() => { + as(result.current, 'prepare').onPrepare(locationAP); + as(result.current, 'review').onReview(locationR); + }); + + expect(result.current.activeState).toBe('analyze'); + expect(result.current.activeFlow).toBe('unknown'); + + // switch state to 'prepare' + act(() => + as(result.current, 'analyze').onAnalysis( + 'single-location', + 'http://my-url', + locationAP, + ), + ); + + // state 'prepare' + act(() => { + as(result.current, 'analyze').onAnalysis( + 'multiple-locations', + 'http://my-url', + locationAP, + ); + as(result.current, 'review').onReview(locationR); + }); + + expect(result.current.activeState).toBe('prepare'); + expect(result.current.activeFlow).toBe('single-location'); + + // switch to 'review' + act(() => as(result.current, 'prepare').onPrepare(locationAP)); + + // state 'review' + act(() => { + as(result.current, 'analyze').onAnalysis( + 'multiple-locations', + 'http://my-url', + locationAP, + ); + as(result.current, 'prepare').onPrepare({ + type: 'locations', + locations: [], + }); + }); + + expect(result.current.activeState).toBe('review'); + expect(result.current.activeFlow).toBe('single-location'); + expect( + as(result.current, 'prepare').prepareResult!.locations, + ).not.toEqual([]); + + // switch to 'finish' + act(() => as(result.current, 'review').onReview(locationR)); + + expect(result.current.activeState).toBe('finish'); + expect(result.current.activeFlow).toBe('single-location'); + }); + }); + + describe('onGoBack', () => { + it('should work', async () => { + const { result } = renderHook(() => useImportState()); + await cleanup(); + + expect(result.current.activeStepNumber).toBe(0); + expect(result.current.onGoBack).toBeUndefined(); + + act(() => + as(result.current, 'analyze').onAnalysis( + 'single-location', + 'http://my-url', + locationAP, + ), + ); + expect(result.current.activeStepNumber).toBe(1); + + expect(result.current.onGoBack).not.toBeUndefined(); + act(() => result.current.onGoBack!()); + expect(result.current.activeStepNumber).toBe(0); + + act(() => + as(result.current, 'analyze').onAnalysis( + 'single-location', + 'http://my-url', + locationAP, + ), + ); + act(() => as(result.current, 'prepare').onPrepare(locationAP)); + expect(result.current.activeStepNumber).toBe(2); + + expect(result.current.onGoBack).not.toBeUndefined(); + act(() => result.current.onGoBack!()); + expect(result.current.activeStepNumber).toBe(1); + + act(() => as(result.current, 'prepare').onPrepare(locationAP)); + act(() => as(result.current, 'review').onReview(locationR)); + expect(result.current.activeStepNumber).toBe(3); + }); + + it('should work for skipped', async () => { + const { result } = renderHook(() => useImportState()); + await cleanup(); + + expect(result.current.activeStepNumber).toBe(0); + expect(result.current.onGoBack).toBeUndefined(); + + act(() => + as(result.current, 'analyze').onAnalysis( + 'single-location', + 'http://my-url', + locationAP, + { + prepareResult: locationAP, + }, + ), + ); + expect(result.current.activeStepNumber).toBe(2); + expect(result.current.onGoBack).not.toBeUndefined(); + + act(() => result.current.onGoBack!()); + expect(result.current.activeStepNumber).toBe(0); + expect(result.current.onGoBack).toBeUndefined(); + }); + + describe('should consider prepareNotRepeatable', () => { + it('as true', async () => { + const { result } = renderHook(() => useImportState()); + await cleanup(); + + expect(result.current.onGoBack).toBeUndefined(); + + act(() => + as(result.current, 'analyze').onAnalysis( + 'multiple-locations', + 'http://my-url', + locationAP, + ), + ); + act(() => + as(result.current, 'prepare').onPrepare(locationAP, { + notRepeatable: true, + }), + ); + + expect(result.current.onGoBack).toBeUndefined(); + }); + + it('as false', async () => { + const { result } = renderHook(() => useImportState()); + await cleanup(); + + expect(result.current.onGoBack).toBeUndefined(); + + act(() => + as(result.current, 'analyze').onAnalysis( + 'multiple-locations', + 'http://my-url', + locationAP, + ), + ); + act(() => + as(result.current, 'prepare').onPrepare(locationAP, { + notRepeatable: false, + }), + ); + + expect(result.current.onGoBack).not.toBeUndefined(); + }); + }); + }); +}); diff --git a/plugins/catalog-import/src/components/useImportState.ts b/plugins/catalog-import/src/components/useImportState.ts new file mode 100644 index 0000000000..f042c991ef --- /dev/null +++ b/plugins/catalog-import/src/components/useImportState.ts @@ -0,0 +1,291 @@ +/* + * 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 { Entity, EntityName } from '@backstage/catalog-model'; +import { useReducer } from 'react'; +import { AnalyzeResult } from '../api'; + +// the configuration of the stepper +export type ImportFlows = + | 'unknown' + | 'single-location' + | 'multiple-locations' + | 'no-location'; + +// the available states of the stepper +type ImportStateTypes = 'analyze' | 'prepare' | 'review' | 'finish'; + +// result of the prepare state +export type PrepareResult = + | { + type: 'locations'; + locations: Array<{ + target: string; + entities: EntityName[]; + }>; + } + | { + type: 'repository'; + url: string; + integrationType: string; + pullRequest: { + url: string; + }; + locations: Array<{ + target: string; + entities: EntityName[]; + }>; + }; + +// result of the review result +export type ReviewResult = + | { + type: 'locations'; + locations: Array<{ + target: string; + entities: Entity[]; + }>; + } + | { + type: 'repository'; + url: string; + integrationType: string; + pullRequest: { + url: string; + }; + locations: Array<{ + target: string; + entities: Entity[]; + }>; + }; + +// function type for the 'analysis' -> 'prepare'/'review' transition +type onAnalysisFn = ( + flow: ImportFlows, + url: string, + result: AnalyzeResult, + opts?: { prepareResult?: PrepareResult }, +) => void; + +// function type for the 'prepare' -> 'review' transition +type onPrepareFn = ( + result: PrepareResult, + opts?: { notRepeatable?: boolean }, +) => void; + +// function type for the 'review' -> 'finish' transition +type onReviewFn = (result: ReviewResult) => void; + +// the type interfaces that are available in each state. every state provides +// already known information and means to go to the next, or the previous step. +type State = + | { + activeState: 'analyze'; + onAnalysis: onAnalysisFn; + } + | { + activeState: 'prepare'; + analyzeResult: AnalyzeResult; + prepareResult?: PrepareResult; + onPrepare: onPrepareFn; + } + | { + activeState: 'review'; + analyzeResult: AnalyzeResult; + prepareResult: PrepareResult; + onReview: onReviewFn; + } + | { + activeState: 'finish'; + analyzeResult: AnalyzeResult; + prepareResult: PrepareResult; + reviewResult: ReviewResult; + }; + +export type ImportState = State & { + activeFlow: ImportFlows; + activeStepNumber: number; + analysisUrl?: string; + + onGoBack?: () => void; + onReset: () => void; +}; + +type ReducerActions = + | { type: 'onAnalysis'; args: Parameters } + | { type: 'onPrepare'; args: Parameters } + | { type: 'onReview'; args: Parameters } + | { type: 'onGoBack' } + | { type: 'onReset'; initialUrl?: string }; + +type ReducerState = { + activeFlow: ImportFlows; + activeState: ImportStateTypes; + analysisUrl?: string; + analyzeResult?: AnalyzeResult; + prepareResult?: PrepareResult; + reviewResult?: ReviewResult; + + previousStates: ImportStateTypes[]; +}; + +function init(initialUrl?: string): ReducerState { + return { + activeFlow: 'unknown', + activeState: 'analyze', + analysisUrl: initialUrl, + previousStates: [], + }; +} + +function reducer(state: ReducerState, action: ReducerActions): ReducerState { + switch (action.type) { + case 'onAnalysis': { + if (state.activeState !== 'analyze') { + return state; + } + + const { activeState, previousStates } = state; + const [activeFlow, analysisUrl, analyzeResult, opts] = action.args; + + return { + ...state, + analysisUrl, + activeFlow, + analyzeResult, + prepareResult: opts?.prepareResult, + + activeState: opts?.prepareResult === undefined ? 'prepare' : 'review', + previousStates: previousStates.concat(activeState), + }; + } + + case 'onPrepare': { + if (state.activeState !== 'prepare') { + return state; + } + + const { activeState, previousStates } = state; + const [prepareResult, opts] = action.args; + + return { + ...state, + prepareResult, + + activeState: 'review', + previousStates: opts?.notRepeatable + ? [] + : previousStates.concat(activeState), + }; + } + + case 'onReview': { + if (state.activeState !== 'review') { + return state; + } + + const { activeState, previousStates } = state; + const [reviewResult] = action.args; + + return { + ...state, + reviewResult, + + activeState: 'finish', + previousStates: previousStates.concat(activeState), + }; + } + + case 'onGoBack': { + const { activeState, previousStates } = state; + + return { + ...state, + + activeState: + previousStates.length > 0 + ? previousStates[previousStates.length - 1] + : activeState, + previousStates: previousStates.slice(0, previousStates.length - 1), + }; + } + + case 'onReset': + return { + ...init(action.initialUrl), + + // we keep the old reviewResult since the form is animated and an + // undefined value might crash the last step. + reviewResult: state.reviewResult, + }; + + default: + throw new Error(); + } +} + +/** + * A hook that manages the state machine of the form. It handles different flows + * which each can implement up to four states: + * 1. analyze + * 2. preview (skippable from analyze->review) + * 3. review + * 4. finish + * + * @param options options + */ +export const useImportState = (options?: { + initialUrl?: string; +}): ImportState => { + const [state, dispatch] = useReducer(reducer, options?.initialUrl, init); + + const { activeFlow, activeState, analysisUrl, previousStates } = state; + + return { + activeFlow, + activeState, + activeStepNumber: ['analyze', 'prepare', 'review', 'finish'].indexOf( + activeState, + ), + analysisUrl: analysisUrl, + + analyzeResult: state.analyzeResult!, + prepareResult: state.prepareResult!, + reviewResult: state.reviewResult!, + + onAnalysis: (flow, url, result, opts) => + dispatch({ + type: 'onAnalysis', + args: [flow, url, result, opts], + }), + + onPrepare: (result, opts) => + dispatch({ + type: 'onPrepare', + args: [result, opts], + }), + + onReview: result => dispatch({ type: 'onReview', args: [result] }), + + onGoBack: + previousStates.length > 0 + ? () => dispatch({ type: 'onGoBack' }) + : undefined, + + onReset: () => + dispatch({ type: 'onReset', initialUrl: options?.initialUrl }), + }; +}; diff --git a/plugins/catalog-import/src/index.ts b/plugins/catalog-import/src/index.ts index 7b4fc17121..562e0499f3 100644 --- a/plugins/catalog-import/src/index.ts +++ b/plugins/catalog-import/src/index.ts @@ -20,4 +20,5 @@ export { CatalogImportPage, } from './plugin'; export { Router } from './components/Router'; +export * from './components'; export * from './api'; diff --git a/plugins/catalog-import/src/mocks/analyze-location-POST-response.json b/plugins/catalog-import/src/mocks/analyze-location-POST-response.json deleted file mode 100644 index eaadf1054a..0000000000 --- a/plugins/catalog-import/src/mocks/analyze-location-POST-response.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "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 deleted file mode 100644 index c44a0f4d61..0000000000 --- a/plugins/catalog-import/src/mocks/locations-POST-response.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "location": { - "id": "d4a64359-a709-4c91-a9de-0905a033bf22", - "type": "url", - "target": "https://github.com/someusername/somerepo/blob/master/src/catalog-info.yaml" - }, - "entities": [ - { - "metadata": { - "namespace": "default", - "annotations": { - "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", - "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/plugin.ts b/plugins/catalog-import/src/plugin.ts index 42210d0ed1..18b3124b3b 100644 --- a/plugins/catalog-import/src/plugin.ts +++ b/plugins/catalog-import/src/plugin.ts @@ -15,16 +15,17 @@ */ import { + identityApiRef, + configApiRef, createApiFactory, createPlugin, + createRoutableExtension, createRouteRef, discoveryApiRef, githubAuthApiRef, - configApiRef, - createRoutableExtension, } from '@backstage/core'; -import { catalogImportApiRef } from './api/CatalogImportApi'; -import { CatalogImportClient } from './api/CatalogImportClient'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { catalogImportApiRef, CatalogImportClient } from './api'; export const rootRouteRef = createRouteRef({ path: '', @@ -39,10 +40,24 @@ export const catalogImportPlugin = createPlugin({ deps: { discoveryApi: discoveryApiRef, githubAuthApi: githubAuthApiRef, + identityApi: identityApiRef, configApi: configApiRef, + catalogApi: catalogApiRef, }, - factory: ({ discoveryApi, githubAuthApi, configApi }) => - new CatalogImportClient({ discoveryApi, githubAuthApi, configApi }), + factory: ({ + discoveryApi, + githubAuthApi, + identityApi, + configApi, + catalogApi, + }) => + new CatalogImportClient({ + discoveryApi, + githubAuthApi, + configApi, + identityApi, + catalogApi, + }), }), ], routes: { diff --git a/plugins/catalog-import/src/util/types.ts b/plugins/catalog-import/src/types.ts similarity index 100% rename from plugins/catalog-import/src/util/types.ts rename to plugins/catalog-import/src/types.ts diff --git a/plugins/catalog-import/src/util/useGithubRepos.ts b/plugins/catalog-import/src/util/useGithubRepos.ts deleted file mode 100644 index a2d299b9f4..0000000000 --- a/plugins/catalog-import/src/util/useGithubRepos.ts +++ /dev/null @@ -1,121 +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 * as YAML from 'yaml'; -import { useApi, configApiRef } from '@backstage/core'; -import { catalogImportApiRef } from '../api/CatalogImportApi'; -import { ConfigSpec } from '../components/ImportComponentPage'; -import parseGitUrl 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 { - GitHubIntegrationConfig, - readGitHubIntegrationConfigs, -} from '@backstage/integration'; - -export function useGithubRepos() { - const api = useApi(catalogImportApiRef); - const config = useApi(configApiRef); - - const getGithubIntegrationConfig = (location: string) => { - const { - name: repoName, - owner: ownerName, - resource: hostname, - } = parseGitUrl(location); - - const configs = readGitHubIntegrationConfigs( - config.getOptionalConfigArray('integrations.github') ?? [], - ); - const githubIntegrationConfig = configs.find(v => v.host === hostname); - if (!githubIntegrationConfig) { - throw new Error( - `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, - repo: repoName, - fileContent: selectedRepo.config - .map(entity => `---\n${YAML.stringify(entity)}`) - .join('\n'), - githubIntegrationConfig, - }) - .catch(e => { - throw new Error(`Failed to submit PR to repo, ${e.message}`); - }); - - await api - .createRepositoryLocation({ - location: submitPRResponse.location, - }) - .catch(e => { - throw new Error(`Failed to create repository location, ${e.message}`); - }); - - return submitPRResponse; - }; - - const checkForExistingCatalogInfo = async ( - location: string, - ): Promise<{ exists: boolean; url?: string }> => { - let githubConfig: { - repoName: string; - ownerName: string; - githubIntegrationConfig: GitHubIntegrationConfig; - }; - try { - githubConfig = getGithubIntegrationConfig(location); - } catch (e) { - return { exists: false }; - } - return await api - .checkForExistingCatalogInfo({ - owner: githubConfig.ownerName, - repo: githubConfig.repoName, - githubIntegrationConfig: githubConfig.githubIntegrationConfig, - }) - .catch(e => { - throw new Error( - `Failed to inspect repository for existing catalog-info.yaml, ${e.message}`, - ); - }); - }; - - return { - submitPrToRepo, - checkForExistingCatalogInfo, - generateEntityDefinitions: (repo: string) => - api.generateEntityDefinitions({ repo }), - addLocation: (location: string) => - api.createRepositoryLocation({ location }), - }; -} diff --git a/plugins/catalog-import/src/util/validate.test.ts b/plugins/catalog-import/src/util/validate.test.ts deleted file mode 100644 index 139c7fb0f8..0000000000 --- a/plugins/catalog-import/src/util/validate.test.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 { ComponentIdValidators } from './validate'; - -describe('ComponentIdValidators', () => { - describe('httpsValidator validator', () => { - const errorMessage = 'Must start with https://.'; - test.each([ - [true, 'https://example.com'], - [errorMessage, 'http://example.com'], - [errorMessage, 'example.com'], - [errorMessage, 'www.example.com'], - [errorMessage, ''], - [errorMessage, undefined], - ])('should return %p for %s', (expected: string | boolean, arg: any) => { - expect(ComponentIdValidators.httpsValidator(arg)).toBe(expected); - }); - }); -}); diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index 90b7c80caf..38cc17e1ba 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-react +## 0.0.3 + +### Patch Changes + +- 19d354c78: Make `EntityRefLink` a `React.forwardRef` in order to use it as root component in other components like `ListItem`. +- Updated dependencies [6ed2b47d6] +- Updated dependencies [72b96e880] +- Updated dependencies [b51ee6ece] + - @backstage/catalog-client@0.3.6 + - @backstage/core@0.6.1 + ## 0.0.2 ### Patch Changes diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 64307f6846..649f3b2ff6 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-react", - "version": "0.0.2", + "version": "0.0.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -28,9 +28,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-client": "^0.3.5", + "@backstage/catalog-client": "^0.3.6", "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.0", + "@backstage/core": "^0.6.1", "@material-ui/core": "^4.11.0", "@types/react": "^16.9", "react": "^16.13.1", @@ -40,8 +40,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/react-hooks": "^3.3.0", diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx index ae6f807275..50a80af12a 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx @@ -15,13 +15,12 @@ */ import { Entity, - EntityName, ENTITY_DEFAULT_NAMESPACE, + EntityName, } from '@backstage/catalog-model'; -import { Link } from '@material-ui/core'; +import { Link } from '@backstage/core'; import React from 'react'; import { generatePath } from 'react-router'; -import { Link as RouterLink } from 'react-router-dom'; import { entityRoute } from '../../routes'; import { formatEntityRefTitle } from './format'; @@ -31,41 +30,42 @@ type EntityRefLinkProps = { children?: React.ReactNode; }; -export const EntityRefLink = ({ - entityRef, - defaultKind, - children, -}: EntityRefLinkProps) => { - let kind; - let namespace; - let name; +export const EntityRefLink = React.forwardRef( + (props, ref) => { + const { entityRef, defaultKind, children, ...linkProps } = props; - if ('metadata' in entityRef) { - kind = entityRef.kind; - namespace = entityRef.metadata.namespace; - name = entityRef.metadata.name; - } else { - kind = entityRef.kind; - namespace = entityRef.namespace; - name = entityRef.name; - } + let kind; + let namespace; + let name; - kind = kind.toLowerCase(); + if ('metadata' in entityRef) { + kind = entityRef.kind; + namespace = entityRef.metadata.namespace; + name = entityRef.metadata.name; + } else { + kind = entityRef.kind; + namespace = entityRef.namespace; + name = entityRef.name; + } - const routeParams = { - kind, - namespace: namespace?.toLowerCase() ?? ENTITY_DEFAULT_NAMESPACE, - name, - }; + kind = kind.toLowerCase(); - // TODO: Use useRouteRef here to generate the path - return ( - - {children} - {!children && formatEntityRefTitle(entityRef, { defaultKind })} - - ); -}; + const routeParams = { + kind, + namespace: namespace?.toLowerCase() ?? ENTITY_DEFAULT_NAMESPACE, + name, + }; + + // TODO: Use useRouteRef here to generate the path + return ( + + {children} + {!children && formatEntityRefTitle(entityRef, { defaultKind })} + + ); + }, +); diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index eb4c9a30d7..d786fc2f15 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/plugin-catalog +## 0.3.1 + +### Patch Changes + +- 6ed2b47d6: Include Backstage identity token in requests to backend plugins. +- ca559171b: bug fix: 3310 fixes reloading entities with the default owned filter +- f5e564cd6: Improve display of error messages +- 1df75733e: Adds an `EntityLinksCard` component to display `entity.metadata.links` on entity pages. The new component is a companion for the new [Entity Links](https://backstage.io/docs/features/software-catalog/descriptor-format#links-optional) catalog model addition. + + Here is an example usage within an `EntityPage.tsx`. + + ```tsx + // in packages/app/src/components/catalog/EntityPage.tsx + const ComponentOverviewContent = ({ entity }: { entity: Entity }) => ( + + + + // or ... + + + + ); + ``` + +- e5da858d7: Removed unused functions and the moment library. #4278 +- 9230d07e7: Fix whitespace around variable in unregister error dialog box +- Updated dependencies [6ed2b47d6] +- Updated dependencies [72b96e880] +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/catalog-client@0.3.6 + - @backstage/plugin-scaffolder@0.5.0 + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.3.0 ### Minor Changes diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index c806c58e67..072acc6472 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog", - "version": "0.3.0", + "version": "0.3.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,11 +30,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-client": "^0.3.5", + "@backstage/catalog-client": "^0.3.6", "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.0.2", - "@backstage/plugin-scaffolder": "^0.4.2", + "@backstage/core": "^0.6.1", + "@backstage/plugin-catalog-react": "^0.0.3", + "@backstage/plugin-scaffolder": "^0.5.0", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -42,7 +42,6 @@ "@types/react": "^16.9", "classnames": "^2.2.6", "git-url-parse": "^11.4.4", - "moment": "^2.26.0", "react": "^16.13.1", "react-dom": "^16.13.1", "react-helmet": "6.1.0", @@ -53,8 +52,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@microsoft/microsoft-graph-types": "^1.25.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/catalog/src/CatalogClientWrapper.test.ts b/plugins/catalog/src/CatalogClientWrapper.test.ts new file mode 100644 index 0000000000..87dcd8f74b --- /dev/null +++ b/plugins/catalog/src/CatalogClientWrapper.test.ts @@ -0,0 +1,169 @@ +/* + * 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 { CatalogClient } from '@backstage/catalog-client'; +import { DiscoveryApi, IdentityApi } from '@backstage/core'; +import { CatalogClientWrapper } from './CatalogClientWrapper'; + +jest.mock('@backstage/catalog-client'); +const MockedCatalogClient = CatalogClient as jest.Mock; + +const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base'; +const discoveryApi: DiscoveryApi = { + async getBaseUrl(_pluginId) { + return mockBaseUrl; + }, +}; +const identityApi: IdentityApi = { + getUserId() { + return 'jane-fonda'; + }, + getProfile() { + return { email: 'jane-fonda@spotify.com' }; + }, + async getIdToken() { + return Promise.resolve('fake-id-token'); + }, + async signOut() { + return Promise.resolve(); + }, +}; +const guestIdentityApi: IdentityApi = { + getUserId() { + return 'guest'; + }, + getProfile() { + return {}; + }, + async getIdToken() { + return Promise.resolve(undefined); + }, + async signOut() { + return Promise.resolve(); + }, +}; + +describe('CatalogClientWrapper', () => { + beforeEach(() => { + MockedCatalogClient.mockClear(); + }); + + describe('getEntities', () => { + it('injects authorization token', async () => { + const client = new CatalogClientWrapper({ + client: new MockedCatalogClient({ discoveryApi }), + identityApi, + }); + await client.getEntities(); + const getEntities = MockedCatalogClient.mock.instances[0].getEntities; + expect(getEntities).toHaveBeenCalledWith(undefined, { + token: 'fake-id-token', + }); + expect(getEntities).toHaveBeenCalledTimes(1); + }); + }); + + describe('getLocationById', () => { + it('omits authorization token when guest', async () => { + const client = new CatalogClientWrapper({ + client: new MockedCatalogClient({ discoveryApi }), + identityApi: guestIdentityApi, + }); + await client.getLocationById('42'); + const getLocationById = + MockedCatalogClient.mock.instances[0].getLocationById; + expect(getLocationById).toHaveBeenCalledWith('42', {}); + expect(getLocationById).toHaveBeenCalledTimes(1); + }); + }); + + describe('getEntityByName', () => { + const name = { + kind: 'kind', + namespace: 'namespace', + name: 'name', + }; + it('injects authorization token', async () => { + const client = new CatalogClientWrapper({ + client: new MockedCatalogClient({ discoveryApi }), + identityApi, + }); + await client.getEntityByName(name); + const getEntityByName = + MockedCatalogClient.mock.instances[0].getEntityByName; + expect(getEntityByName).toHaveBeenCalledWith(name, { + token: 'fake-id-token', + }); + expect(getEntityByName).toHaveBeenCalledTimes(1); + }); + }); + + describe('addLocation', () => { + const location = { target: 'target' }; + it('injects authorization token', async () => { + const client = new CatalogClientWrapper({ + client: new MockedCatalogClient({ discoveryApi }), + identityApi, + }); + await client.addLocation(location); + const addLocation = MockedCatalogClient.mock.instances[0].addLocation; + expect(addLocation).toHaveBeenCalledWith(location, { + token: 'fake-id-token', + }); + expect(addLocation).toHaveBeenCalledTimes(1); + }); + }); + + describe('getLocationByEntity', () => { + const entity = { + apiVersion: 'apiVersion', + kind: 'kind', + metadata: { + name: 'name', + }, + }; + it('injects authorization token', async () => { + const client = new CatalogClientWrapper({ + client: new MockedCatalogClient({ discoveryApi }), + identityApi, + }); + await client.getLocationByEntity(entity); + const getLocationByEntity = + MockedCatalogClient.mock.instances[0].getLocationByEntity; + expect(getLocationByEntity).toHaveBeenCalledWith(entity, { + token: 'fake-id-token', + }); + expect(getLocationByEntity).toHaveBeenCalledTimes(1); + }); + }); + + describe('removeEntityByUid', () => { + it('injects authorization token', async () => { + const uid = 'uid'; + const client = new CatalogClientWrapper({ + client: new MockedCatalogClient({ discoveryApi }), + identityApi, + }); + await client.removeEntityByUid(uid); + const removeEntityByUid = + MockedCatalogClient.mock.instances[0].removeEntityByUid; + expect(removeEntityByUid).toHaveBeenCalledWith(uid, { + token: 'fake-id-token', + }); + expect(removeEntityByUid).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/plugins/catalog/src/CatalogClientWrapper.ts b/plugins/catalog/src/CatalogClientWrapper.ts new file mode 100644 index 0000000000..42e9d872d4 --- /dev/null +++ b/plugins/catalog/src/CatalogClientWrapper.ts @@ -0,0 +1,97 @@ +/* + * 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, EntityName, Location } from '@backstage/catalog-model'; +import { + AddLocationRequest, + AddLocationResponse, + CatalogApi, + CatalogEntitiesRequest, + CatalogListResponse, + CatalogClient, +} from '@backstage/catalog-client'; +import { IdentityApi } from '@backstage/core'; + +type CatalogRequestOptions = { + token?: string; +}; + +/** + * CatalogClient wrapper that injects identity token for all requests + */ +export class CatalogClientWrapper implements CatalogApi { + private readonly identityApi: IdentityApi; + private readonly client: CatalogClient; + + constructor(options: { client: CatalogClient; identityApi: IdentityApi }) { + this.client = options.client; + this.identityApi = options.identityApi; + } + + async getLocationById( + id: String, + options?: CatalogRequestOptions, + ): Promise { + return await this.client.getLocationById(id, { + token: options?.token ?? (await this.identityApi.getIdToken()), + }); + } + + async getEntities( + request?: CatalogEntitiesRequest, + options?: CatalogRequestOptions, + ): Promise> { + return await this.client.getEntities(request, { + token: options?.token ?? (await this.identityApi.getIdToken()), + }); + } + + async getEntityByName( + compoundName: EntityName, + options?: CatalogRequestOptions, + ): Promise { + return await this.client.getEntityByName(compoundName, { + token: options?.token ?? (await this.identityApi.getIdToken()), + }); + } + + async addLocation( + request: AddLocationRequest, + options?: CatalogRequestOptions, + ): Promise { + return await this.client.addLocation(request, { + token: options?.token ?? (await this.identityApi.getIdToken()), + }); + } + + async getLocationByEntity( + entity: Entity, + options?: CatalogRequestOptions, + ): Promise { + return await this.client.getLocationByEntity(entity, { + token: options?.token ?? (await this.identityApi.getIdToken()), + }); + } + + async removeEntityByUid( + uid: string, + options?: CatalogRequestOptions, + ): Promise { + return await this.client.removeEntityByUid(uid, { + token: options?.token ?? (await this.identityApi.getIdToken()), + }); + } +} diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx index 3038fc3c5c..ada827115d 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx @@ -14,8 +14,9 @@ * limitations under the License. */ -import React from 'react'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; import { render } from '@testing-library/react'; +import React from 'react'; import { AboutCard } from './AboutCard'; describe(' GitHub', () => { @@ -36,7 +37,11 @@ describe(' GitHub', () => { lifecycle: 'production', }, }; - const { getByText } = render(); + const { getByText } = render( + + + , + ); expect(getByText('service')).toBeInTheDocument(); expect(getByText('View Source').closest('a')).toHaveAttribute( 'href', @@ -67,7 +72,11 @@ describe(' GitLab', () => { lifecycle: 'production', }, }; - const { getByText } = render(); + const { getByText } = render( + + + , + ); expect(getByText('service')).toBeInTheDocument(); expect(getByText('View Source').closest('a')).toHaveAttribute( 'href', @@ -98,7 +107,11 @@ describe(' BitBucket', () => { lifecycle: 'production', }, }; - const { getByText } = render(); + const { getByText } = render( + + + , + ); expect(getByText('service')).toBeInTheDocument(); expect(getByText('View Source').closest('a')).toHaveAttribute( 'href', diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index 1549d87b8a..04c2a78869 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -19,6 +19,8 @@ import { ENTITY_DEFAULT_NAMESPACE, RELATION_PROVIDES_API, } from '@backstage/catalog-model'; +import { HeaderIconLinkRow } from '@backstage/core'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { Card, CardContent, @@ -27,14 +29,13 @@ import { IconButton, makeStyles, } from '@material-ui/core'; -import ExtensionIcon from '@material-ui/icons/Extension'; import DocsIcon from '@material-ui/icons/Description'; import EditIcon from '@material-ui/icons/Edit'; +import ExtensionIcon from '@material-ui/icons/Extension'; import GitHubIcon from '@material-ui/icons/GitHub'; import React from 'react'; import { findLocationForEntityMeta } from '../../data/utils'; import { createEditLink, determineUrlType } from '../createEditLink'; -import { HeaderIconLinkRow } from '@backstage/core'; import { AboutContent } from './AboutContent'; const useStyles = makeStyles({ @@ -76,12 +77,14 @@ function getCodeLinkInfo(entity: Entity): CodeLinkInfo { } type AboutCardProps = { - entity: Entity; + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; variant?: string; }; -export function AboutCard({ entity, variant }: AboutCardProps) { +export function AboutCard({ variant }: AboutCardProps) { const classes = useStyles(); + const { entity } = useEntity(); const codeLink = getCodeLinkInfo(entity); // TODO: Also support RELATION_CONSUMES_API here const hasApis = entity.relations?.some(r => r.type === RELATION_PROVIDES_API); diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx index 90a1e15ad0..6de4c318b5 100644 --- a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx @@ -80,6 +80,14 @@ type Props = { onChange?: OnChangeCallback; }; +/** + * Sidebar filter type and human readable label for it. owned/starred/all + */ +export type CatalogFilterType = { + id: string; + label: string; +}; + /** * The main filter group in the sidebar, toggling owned/starred/all. */ diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index 42c9cf1117..6d4d18e4f2 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -30,7 +30,7 @@ import { } from '@backstage/core'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils'; -import { fireEvent, render } from '@testing-library/react'; +import { fireEvent, render, waitFor } from '@testing-library/react'; import React from 'react'; import { EntityFilterGroupsProvider } from '../../filter'; import { CatalogPage } from './CatalogPage'; @@ -128,4 +128,24 @@ describe('CatalogPage', () => { fireEvent.click(getByText(/All/)); expect(await findByText(/All \(2\)/)).toBeInTheDocument(); }); + // this test is for fixing the bug after favoriting an entity, the matching entities defaulting + // to "owned" filter and not based on the selected filter + it('should render the correct entities filtered on the selectedfilter', async () => { + const { findByText, findAllByTitle, getByText } = renderWrapped( + , + ); + expect(await findByText(/Owned \(1\)/)).toBeInTheDocument(); + expect(await findByText(/Starred/)).toBeInTheDocument(); + fireEvent.click(getByText(/Starred/)); + expect(await findByText(/Starred \(0\)/)).toBeInTheDocument(); + fireEvent.click(getByText(/All/)); + expect(await findByText(/All \(2\)/)).toBeInTheDocument(); + + const starredIcons = await findAllByTitle('Add to favorites'); + fireEvent.click(starredIcons[0]); + expect(await findByText(/All \(2\)/)).toBeInTheDocument(); + + fireEvent.click(getByText(/Starred/)); + waitFor(() => expect(findByText(/Starred \(1\)/)).toBeInTheDocument()); + }); }); diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index 1298d5e190..a2ebc63825 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -31,7 +31,11 @@ import React, { useCallback, useMemo, useState } from 'react'; import { Link as RouterLink } from 'react-router-dom'; import { EntityFilterGroupsProvider, useFilteredEntities } from '../../filter'; import { useStarredEntities } from '../../hooks/useStarredEntities'; -import { ButtonGroup, CatalogFilter } from '../CatalogFilter/CatalogFilter'; +import { + ButtonGroup, + CatalogFilter, + CatalogFilterType, +} from '../CatalogFilter/CatalogFilter'; import { CatalogTable } from '../CatalogTable/CatalogTable'; import { ResultsFilter } from '../ResultsFilter/ResultsFilter'; import { useOwnUser } from '../useOwnUser'; @@ -65,7 +69,9 @@ const CatalogPageContents = () => { const errorApi = useApi(errorApiRef); const { isStarredEntity } = useStarredEntities(); const [selectedTab, setSelectedTab] = useState(); - const [selectedSidebarItem, setSelectedSidebarItem] = useState(); + const [selectedSidebarItem, setSelectedSidebarItem] = useState< + CatalogFilterType + >(); const orgName = configApi.getOptionalString('organization.name') ?? 'Company'; const addMockData = useCallback(async () => { @@ -180,13 +186,15 @@ const CatalogPageContents = () => {
setSelectedSidebarItem(label)} - initiallySelected="owned" + onChange={({ label, id }) => + setSelectedSidebarItem({ label, id }) + } + initiallySelected={selectedSidebarItem?.id ?? 'owned'} />
{ - const valid = isValidDateAndFormat( - '1970-01-01T00:00:00', - 'YYYY-MM-DD[T]HH:mm:ss', - ); - const invalid = isValidDateAndFormat( - '1970/01/01T00:00:00', - 'YYYY-MM-DD[T]HH:mm:ss', - ); - expect(valid).toBe(true); - expect(invalid).toBe(false); -}); +import { getTimeBasedGreeting } from './timeUtil'; it('has greeting and language', () => { const greeting = getTimeBasedGreeting(); diff --git a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx index e30f54a275..82cf556281 100644 --- a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx +++ b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx @@ -25,7 +25,6 @@ import { import { makeStyles } from '@material-ui/core/styles'; import Cancel from '@material-ui/icons/Cancel'; import MoreVert from '@material-ui/icons/MoreVert'; -import SwapHoriz from '@material-ui/icons/SwapHoriz'; import React, { useState } from 'react'; // TODO(freben): It should probably instead be the case that Header sets the theme text color to white inside itself unconditionally instead @@ -82,12 +81,6 @@ export const EntityContextMenu = ({ onUnregisterEntity }: Props) => { Unregister entity - - - - - Move repository - diff --git a/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.test.tsx b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.test.tsx new file mode 100644 index 0000000000..d54eac788c --- /dev/null +++ b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.test.tsx @@ -0,0 +1,81 @@ +/* + * 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, EntityLink } from '@backstage/catalog-model'; +import { EntityContext } from '@backstage/plugin-catalog-react'; +import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; +import React from 'react'; +import { EntityLinksCard } from './EntityLinksCard'; + +describe('EntityLinksCard', () => { + const createEntity = (links: EntityLink[] = []): Entity => + ({ + metadata: { + links, + }, + } as Entity); + + const createLink = ({ + url = 'https://dashboard.dashexample.com', + title = 'admin dashboard', + icon = undefined, + }: Partial = {}): EntityLink => ({ + url, + title, + icon, + }); + + it('should render a link', async () => { + const links: EntityLink[] = [createLink()]; + const entityContextValue = { + entity: createEntity(links), + loading: false, + error: undefined, + }; + + const { queryByText } = await renderWithEffects( + wrapInTestApp( + + + , + ), + ); + + expect(queryByText('admin dashboard')).toBeInTheDocument(); + expect(queryByText('derp')).not.toBeInTheDocument(); + }); + + it('should show empty state', async () => { + const entityContextValue = { + entity: createEntity([]), + loading: false, + error: undefined, + }; + + const { queryByText } = await renderWithEffects( + wrapInTestApp( + + + , + ), + ); + + expect( + queryByText(/.*No links defined for this entity.*/), + ).toBeInTheDocument(); + expect(queryByText('admin dashboard')).not.toBeInTheDocument(); + }); +}); diff --git a/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx new file mode 100644 index 0000000000..b0c3db2333 --- /dev/null +++ b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx @@ -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 { Entity } from '@backstage/catalog-model'; +import { IconComponent, IconKey, InfoCard, useApp } from '@backstage/core'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import LanguageIcon from '@material-ui/icons/Language'; +import React from 'react'; +import { EntityLinksEmptyState } from './EntityLinksEmptyState'; +import { LinksGridList } from './LinksGridList'; +import { ColumnBreakpoints } from './types'; + +type Props = { + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; + cols?: ColumnBreakpoints | number; +}; + +export const EntityLinksCard = ({ cols = undefined }: Props) => { + const { entity } = useEntity(); + const app = useApp(); + + const iconResolver = (key: IconKey | undefined): IconComponent => { + return app.getSystemIcon(key ?? '') ?? LanguageIcon; + }; + + const links = entity?.metadata?.links; + + return ( + + {!links || links.length === 0 ? ( + + ) : ( + ({ + text: title ?? url, + href: url, + Icon: iconResolver(icon), + }))} + /> + )} + + ); +}; diff --git a/plugins/catalog/src/components/EntityLinksCard/EntityLinksEmptyState.tsx b/plugins/catalog/src/components/EntityLinksCard/EntityLinksEmptyState.tsx new file mode 100644 index 0000000000..3ced50decc --- /dev/null +++ b/plugins/catalog/src/components/EntityLinksCard/EntityLinksEmptyState.tsx @@ -0,0 +1,65 @@ +/* + * 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 { CodeSnippet } from '@backstage/core'; +import { BackstageTheme } from '@backstage/theme'; +import { Button, makeStyles, Typography } from '@material-ui/core'; +import React from 'react'; + +const ENTITY_YAML = `metadata: + name: example + links: + - url: https://dashboard.example.com + title: My Dashboard + icon: dashboard`; + +const useStyles = makeStyles(theme => ({ + code: { + borderRadius: 6, + margin: `${theme.spacing(2)}px 0px`, + background: theme.palette.type === 'dark' ? '#444' : '#fff', + }, +})); + +export const EntityLinksEmptyState = () => { + const classes = useStyles(); + + return ( + <> + + No links defined for this entity. You can add links to your entity YAML + as shown in the highlighted example below: + +
+ +
+ + + ); +}; diff --git a/plugins/catalog/src/components/EntityLinksCard/IconLink.test.tsx b/plugins/catalog/src/components/EntityLinksCard/IconLink.test.tsx new file mode 100644 index 0000000000..8caf37e70d --- /dev/null +++ b/plugins/catalog/src/components/EntityLinksCard/IconLink.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 { lightTheme } from '@backstage/theme'; +import { ThemeProvider } from '@material-ui/core'; +import CloudIcon from '@material-ui/icons/Cloud'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { IconLink } from './IconLink'; + +describe('IconLink', () => { + it('should render an icon link', () => { + const rendered = render( + + + , + ); + + expect(rendered.queryByText('I am Link')).toBeInTheDocument(); + }); +}); diff --git a/plugins/catalog/src/components/EntityLinksCard/IconLink.tsx b/plugins/catalog/src/components/EntityLinksCard/IconLink.tsx new file mode 100644 index 0000000000..32ea085889 --- /dev/null +++ b/plugins/catalog/src/components/EntityLinksCard/IconLink.tsx @@ -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 { Link, IconComponent } from '@backstage/core'; +import { Grid, makeStyles, Typography } from '@material-ui/core'; +import LanguageIcon from '@material-ui/icons/Language'; +import React from 'react'; + +const useStyles = makeStyles({ + svgIcon: { + display: 'inline-block', + '& svg': { + display: 'inline-block', + fontSize: 'inherit', + verticalAlign: 'baseline', + }, + }, +}); + +export const IconLink = ({ + href, + text, + Icon, +}: { + href: string; + text?: string; + Icon?: IconComponent; +}) => { + const classes = useStyles(); + + return ( + + + + {Icon ? : } + + + + + {text || href} + + + + ); +}; diff --git a/plugins/catalog/src/components/EntityLinksCard/LinksGridList.tsx b/plugins/catalog/src/components/EntityLinksCard/LinksGridList.tsx new file mode 100644 index 0000000000..9ef77a4bbf --- /dev/null +++ b/plugins/catalog/src/components/EntityLinksCard/LinksGridList.tsx @@ -0,0 +1,47 @@ +/* + * 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 { IconComponent } from '@backstage/core'; +import { GridList, GridListTile } from '@material-ui/core'; +import React from 'react'; +import { IconLink } from './IconLink'; +import { ColumnBreakpoints } from './types'; +import { useDynamicColumns } from './useDynamicColumns'; + +export type LinksGridListItem = { + href: string; + text?: string; + Icon?: IconComponent; +}; + +type Props = { + items: LinksGridListItem[]; + cols?: ColumnBreakpoints | number; +}; + +export const LinksGridList = ({ items, cols = undefined }: Props) => { + const numOfCols = useDynamicColumns(cols); + + return ( + + {items.map(({ text, href, Icon }, i) => ( + + + + ))} + + ); +}; diff --git a/plugins/catalog-import/src/util/validate.ts b/plugins/catalog/src/components/EntityLinksCard/index.ts similarity index 76% rename from plugins/catalog-import/src/util/validate.ts rename to plugins/catalog/src/components/EntityLinksCard/index.ts index 056131aaae..d893d3a233 100644 --- a/plugins/catalog-import/src/util/validate.ts +++ b/plugins/catalog/src/components/EntityLinksCard/index.ts @@ -14,8 +14,4 @@ * limitations under the License. */ -export const ComponentIdValidators = { - httpsValidator: (value: any) => - (typeof value === 'string' && value.match(/^https:\/\//) !== null) || - 'Must start with https://.', -}; +export { EntityLinksCard } from './EntityLinksCard'; diff --git a/plugins/catalog/src/components/EntityLinksCard/types.ts b/plugins/catalog/src/components/EntityLinksCard/types.ts new file mode 100644 index 0000000000..d7729fc216 --- /dev/null +++ b/plugins/catalog/src/components/EntityLinksCard/types.ts @@ -0,0 +1,19 @@ +/* + * 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 type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + +export type ColumnBreakpoints = Record; diff --git a/plugins/catalog/src/components/EntityLinksCard/useDynamicColumns.tsx b/plugins/catalog/src/components/EntityLinksCard/useDynamicColumns.tsx new file mode 100644 index 0000000000..d2b4168e0b --- /dev/null +++ b/plugins/catalog/src/components/EntityLinksCard/useDynamicColumns.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 { Theme, useMediaQuery } from '@material-ui/core'; +import { Breakpoint, ColumnBreakpoints } from './types'; + +const colDefaults: ColumnBreakpoints = { + xs: 1, + sm: 1, + md: 1, + lg: 2, + xl: 3, +}; + +export function useDynamicColumns( + cols: ColumnBreakpoints | number | undefined, +): number { + const matches: (Breakpoint | null)[] = [ + useMediaQuery((theme: Theme) => theme.breakpoints.up('xl')) ? 'xl' : null, + useMediaQuery((theme: Theme) => theme.breakpoints.up('lg')) ? 'lg' : null, + useMediaQuery((theme: Theme) => theme.breakpoints.up('md')) ? 'md' : null, + useMediaQuery((theme: Theme) => theme.breakpoints.up('sm')) ? 'sm' : null, + useMediaQuery((theme: Theme) => theme.breakpoints.up('xs')) ? 'xs' : null, + ]; + + let numOfCols = 1; + + if (typeof cols === 'number') { + numOfCols = cols; + } else { + const breakpoint = matches.find(k => k !== null) ?? 'xs'; + numOfCols = cols?.[breakpoint] ?? colDefaults[breakpoint]; + } + + return numOfCols; +} diff --git a/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx b/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx index 06ea1eee27..ef4196e9c5 100644 --- a/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx +++ b/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx @@ -110,7 +110,7 @@ export const UnregisterEntityDialog = ({ {error.name === 'DeniedLocationException' ? ( <> You cannot unregister this entity, since it originates from a - protected Backstage configuration (location + protected Backstage configuration (location{' '} {`"${(error as DeniedLocationException).locationName}"`}). If you believe this is in error, please contact the{' '} {configApi.getOptionalString('app.title') ?? 'Backstage'}{' '} diff --git a/plugins/catalog/src/filter/useEntityFilterGroup.ts b/plugins/catalog/src/filter/useEntityFilterGroup.ts index 242238e4f4..30214fad78 100644 --- a/plugins/catalog/src/filter/useEntityFilterGroup.ts +++ b/plugins/catalog/src/filter/useEntityFilterGroup.ts @@ -43,15 +43,19 @@ export const useEntityFilterGroup = ( filterGroupStates, } = context; - // Intentionally consider initial set only at mount time + // on state changes unregisters and registers the filtergroup + // ensure that it re-registers with the correct filter as the prop changes and not the default // eslint-disable-next-line react-hooks/exhaustive-deps - const initialMemo = useMemo(() => initialSelectedFilters?.slice(), []); + const initialMemo = useMemo(() => { + return initialSelectedFilters?.slice(); + }, [initialSelectedFilters]); // Register the group on mount, and unregister on unmount useEffect(() => { register(filterGroupId, filterGroup, initialMemo); return () => unregister(filterGroupId); - }, [register, unregister, filterGroupId, filterGroup, initialMemo]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [register, unregister, filterGroupId, filterGroup]); const setSelectedFilters = useCallback( (filters: string[]) => { diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index 3ef79680c4..ce8518eb55 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -24,4 +24,6 @@ export { catalogPlugin as plugin, CatalogIndexPage, CatalogEntityPage, + EntityAboutCard, + EntityLinksCard, } from './plugin'; diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index c19925775d..c2b910ce68 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -19,21 +19,28 @@ import { createApiFactory, createPlugin, discoveryApiRef, + createComponentExtension, createRoutableExtension, + identityApiRef, } from '@backstage/core'; import { catalogApiRef, catalogRouteRef, entityRouteRef, } from '@backstage/plugin-catalog-react'; +import { CatalogClientWrapper } from './CatalogClientWrapper'; export const catalogPlugin = createPlugin({ id: 'catalog', apis: [ createApiFactory({ api: catalogApiRef, - deps: { discoveryApi: discoveryApiRef }, - factory: ({ discoveryApi }) => new CatalogClient({ discoveryApi }), + deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, + factory: ({ discoveryApi, identityApi }) => + new CatalogClientWrapper({ + client: new CatalogClient({ discoveryApi }), + identityApi, + }), }), ], routes: { @@ -59,3 +66,20 @@ export const CatalogEntityPage = catalogPlugin.provide( mountPoint: entityRouteRef, }), ); + +export const EntityAboutCard = catalogPlugin.provide( + createComponentExtension({ + component: { + lazy: () => import('./components/AboutCard').then(m => m.AboutCard), + }, + }), +); + +export const EntityLinksCard = catalogPlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./components/EntityLinksCard').then(m => m.EntityLinksCard), + }, + }), +); diff --git a/plugins/circleci/CHANGELOG.md b/plugins/circleci/CHANGELOG.md index 99715821a6..1c5d35aa70 100644 --- a/plugins/circleci/CHANGELOG.md +++ b/plugins/circleci/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-circleci +## 0.2.8 + +### Patch Changes + +- 6b26c9f41: Migrated to new composability API, exporting the plugin instance as `circleCIPlugin`, the entity page content as `EntityCircleCIContent`, and entity conditional as `isCircleCIAvailable`. +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.2.7 ### Patch Changes diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 37e4425b23..6f741ab1ce 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-circleci", - "version": "0.2.7", + "version": "0.2.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,8 +32,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/core": "^0.6.1", + "@backstage/plugin-catalog-react": "^0.0.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -51,8 +51,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/cloudbuild/CHANGELOG.md b/plugins/cloudbuild/CHANGELOG.md index ae3f8faada..8dccd1dbbd 100644 --- a/plugins/cloudbuild/CHANGELOG.md +++ b/plugins/cloudbuild/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-cloudbuild +## 0.2.9 + +### Patch Changes + +- 302795d10: Migrate to new composability API, exporting the plugin instance as `cloudbuildPlugin`, the entity content as `EntityCloudbuildContent`, the entity conditional as `isCloudbuildAvailable`, and entity cards as `EntityLatestCloudbuildRunCard` and `EntityLatestCloudbuildsForBranchCard`. +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.2.8 ### Patch Changes diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index cbad4928b7..a00fe13fe0 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-cloudbuild", - "version": "0.2.8", + "version": "0.2.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,8 +31,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.1", - "@backstage/plugin-catalog-react": "^0.0.2", - "@backstage/core": "^0.6.0", + "@backstage/plugin-catalog-react": "^0.0.3", + "@backstage/core": "^0.6.1", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -48,8 +48,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/cost-insights/CHANGELOG.md b/plugins/cost-insights/CHANGELOG.md index 87bfd3af07..e672b33976 100644 --- a/plugins/cost-insights/CHANGELOG.md +++ b/plugins/cost-insights/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-cost-insights +## 0.8.1 + +### Patch Changes + +- b33fa4cf4: fixes a bug in default dismiss form where other text input persists between reason selections +- d36660721: Fix snooze quarter option +- 02d6803e8: Migrated to new composability API, exporting the plugin instance as `costInsightsPlugin`, the root `'/cost-insights'` page as `CostInsightsPage`, the `'/cost-insights/investigating-growth'` page as `CostInsightsProjectGrowthInstructionsPage`, and the `'/cost-insights/labeling-jobs'` page as `CostInsightsLabelDataflowInstructionsPage`. +- Updated dependencies [b51ee6ece] + - @backstage/core@0.6.1 + ## 0.8.0 ### Minor Changes diff --git a/plugins/cost-insights/dev/index.tsx b/plugins/cost-insights/dev/index.tsx index bdbb4450fa..e4732fc364 100644 --- a/plugins/cost-insights/dev/index.tsx +++ b/plugins/cost-insights/dev/index.tsx @@ -16,10 +16,10 @@ import { createDevApp } from '@backstage/dev-utils'; import { costInsightsApiRef } from '../src/api'; import { ExampleCostInsightsClient } from '../src/client'; -import { plugin } from '../src/plugin'; +import { costInsightsPlugin } from '../src/plugin'; createDevApp() - .registerPlugin(plugin) + .registerPlugin(costInsightsPlugin) .registerApi({ api: costInsightsApiRef, deps: {}, diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 0184d50517..2f0511c217 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-cost-insights", - "version": "0.8.0", + "version": "0.8.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,7 +31,7 @@ }, "dependencies": { "@backstage/config": "^0.1.2", - "@backstage/core": "^0.6.0", + "@backstage/core": "^0.6.1", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -56,8 +56,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/cost-insights/src/forms/AlertDismissForm.tsx b/plugins/cost-insights/src/forms/AlertDismissForm.tsx index 4271785958..47b4dc80cb 100644 --- a/plugins/cost-insights/src/forms/AlertDismissForm.tsx +++ b/plugins/cost-insights/src/forms/AlertDismissForm.tsx @@ -66,6 +66,9 @@ export const AlertDismissForm = forwardRef< }; const onReasonChange = (_: ChangeEvent, value: string) => { + if (other) { + setOther(null); + } setReason(value as AlertDismissReason); }; diff --git a/plugins/cost-insights/src/index.ts b/plugins/cost-insights/src/index.ts index 13ce929401..0ae9d05b09 100644 --- a/plugins/cost-insights/src/index.ts +++ b/plugins/cost-insights/src/index.ts @@ -14,7 +14,13 @@ * limitations under the License. */ -export { plugin } from './plugin'; +export { + costInsightsPlugin, + costInsightsPlugin as plugin, + CostInsightsPage, + CostInsightsProjectGrowthInstructionsPage, + CostInsightsLabelDataflowInstructionsPage, +} from './plugin'; export * from './client'; export * from './api'; export { ProjectGrowthAlert, UnlabeledDataflowAlert } from './alerts'; diff --git a/plugins/cost-insights/src/plugin.test.ts b/plugins/cost-insights/src/plugin.test.ts index 6210e11d4c..87be3ff8a2 100644 --- a/plugins/cost-insights/src/plugin.test.ts +++ b/plugins/cost-insights/src/plugin.test.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { plugin } from './plugin'; +import { costInsightsPlugin } from './plugin'; describe('cost-insights', () => { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect(costInsightsPlugin).toBeDefined(); }); }); diff --git a/plugins/cost-insights/src/plugin.ts b/plugins/cost-insights/src/plugin.ts index 7006d5bf5d..29e345532d 100644 --- a/plugins/cost-insights/src/plugin.ts +++ b/plugins/cost-insights/src/plugin.ts @@ -14,10 +14,14 @@ * limitations under the License. */ -import { createPlugin, createRouteRef } from '@backstage/core'; -import { CostInsightsPage } from './components/CostInsightsPage'; -import { ProjectGrowthInstructionsPage } from './components/ProjectGrowthInstructionsPage'; -import { LabelDataflowInstructionsPage } from './components/LabelDataflowInstructionsPage'; +import { + createPlugin, + createRouteRef, + createRoutableExtension, +} from '@backstage/core'; +import { CostInsightsPage as CostInsightsPageComponent } from './components/CostInsightsPage'; +import { ProjectGrowthInstructionsPage as ProjectGrowthInstructionsPageComponent } from './components/ProjectGrowthInstructionsPage'; +import { LabelDataflowInstructionsPage as LabelDataflowInstructionsPageComponent } from './components/LabelDataflowInstructionsPage'; export const rootRouteRef = createRouteRef({ path: '/cost-insights', @@ -34,12 +38,51 @@ export const unlabeledDataflowAlertRef = createRouteRef({ title: 'Labeling Dataflow Jobs', }); -export const plugin = createPlugin({ +export const costInsightsPlugin = createPlugin({ id: 'cost-insights', register({ router, featureFlags }) { - router.addRoute(rootRouteRef, CostInsightsPage); - router.addRoute(projectGrowthAlertRef, ProjectGrowthInstructionsPage); - router.addRoute(unlabeledDataflowAlertRef, LabelDataflowInstructionsPage); + router.addRoute(rootRouteRef, CostInsightsPageComponent); + router.addRoute( + projectGrowthAlertRef, + ProjectGrowthInstructionsPageComponent, + ); + router.addRoute( + unlabeledDataflowAlertRef, + LabelDataflowInstructionsPageComponent, + ); featureFlags.register('cost-insights-currencies'); }, + routes: { + root: rootRouteRef, + growthAlerts: projectGrowthAlertRef, + unlabeledDataflowAlerts: unlabeledDataflowAlertRef, + }, }); + +export const CostInsightsPage = costInsightsPlugin.provide( + createRoutableExtension({ + component: () => + import('./components/CostInsightsPage').then(m => m.CostInsightsPage), + mountPoint: rootRouteRef, + }), +); + +export const CostInsightsProjectGrowthInstructionsPage = costInsightsPlugin.provide( + createRoutableExtension({ + component: () => + import('./components/ProjectGrowthInstructionsPage').then( + m => m.ProjectGrowthInstructionsPage, + ), + mountPoint: projectGrowthAlertRef, + }), +); + +export const CostInsightsLabelDataflowInstructionsPage = costInsightsPlugin.provide( + createRoutableExtension({ + component: () => + import('./components/LabelDataflowInstructionsPage').then( + m => m.LabelDataflowInstructionsPage, + ), + mountPoint: unlabeledDataflowAlertRef, + }), +); diff --git a/plugins/explore/CHANGELOG.md b/plugins/explore/CHANGELOG.md index 0d2daf37b5..b0334effe1 100644 --- a/plugins/explore/CHANGELOG.md +++ b/plugins/explore/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-explore +## 0.2.5 + +### Patch Changes + +- 0fe8ff5be: Catch catalog errors and display to user +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.2.4 ### Patch Changes diff --git a/plugins/explore/package.json b/plugins/explore/package.json index e9d21cc221..497e108b6f 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-explore", - "version": "0.2.4", + "version": "0.2.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,8 +31,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/core": "^0.6.1", + "@backstage/plugin-catalog-react": "^0.0.3", "@backstage/plugin-explore-react": "^0.0.2", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", @@ -46,8 +46,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx index 4846565bdd..a52b0d8f2c 100644 --- a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx +++ b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx @@ -90,4 +90,17 @@ describe('', () => { expect(getByText('No domains to display')).toBeInTheDocument(), ); }); + + it('renders a friendly error if it cannot collect domains', async () => { + const catalogError = new Error('Network timeout'); + catalogApi.getEntities.mockRejectedValueOnce(catalogError); + + const { getByText } = render(, { + wrapper: Wrapper, + }); + + await waitFor(() => + expect(getByText(/Could not load domains/)).toBeInTheDocument(), + ); + }); }); diff --git a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx index ca00c525c9..182adf70ea 100644 --- a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx +++ b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx @@ -21,6 +21,7 @@ import { Progress, SupportButton, useApi, + WarningPanel, } from '@backstage/core'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { Button } from '@material-ui/core'; @@ -30,7 +31,7 @@ import { DomainCardGrid } from '../DomainCard'; export const DomainExplorerContent = () => { const catalogApi = useApi(catalogApiRef); - const { value: entities, loading } = useAsync(async () => { + const { value: entities, loading, error } = useAsync(async () => { const response = await catalogApi.getEntities({ filter: { kind: 'domain' }, }); @@ -45,7 +46,12 @@ export const DomainExplorerContent = () => { {loading && } - {!loading && (!entities || entities.length === 0) && ( + {error && ( + + {error.message} + + )} + {!loading && !error && (!entities || entities.length === 0) && ( { msw.setupDefaultHandlers(server); @@ -30,7 +45,11 @@ describe('FossaClient', () => { let client: FossaApi; beforeEach(() => { - client = new FossaClient({ discoveryApi, organizationId: '8736' }); + client = new FossaClient({ + discoveryApi, + identityApi, + organizationId: '8736', + }); }); it('should report finding summary', async () => { @@ -137,7 +156,7 @@ describe('FossaClient', () => { }); it('should skip organizationId', async () => { - client = new FossaClient({ discoveryApi }); + client = new FossaClient({ discoveryApi, identityApi }); server.use( rest.get(`${mockBaseUrl}/fossa/projects`, (req, res, ctx) => { diff --git a/plugins/fossa/src/api/FossaClient.ts b/plugins/fossa/src/api/FossaClient.ts index 05a889177a..5693895901 100644 --- a/plugins/fossa/src/api/FossaClient.ts +++ b/plugins/fossa/src/api/FossaClient.ts @@ -14,28 +14,35 @@ * limitations under the License. */ -import { DiscoveryApi } from '@backstage/core'; +import { DiscoveryApi, IdentityApi } from '@backstage/core'; import fetch from 'cross-fetch'; import { FindingSummary, FossaApi } from './FossaApi'; export class FossaClient implements FossaApi { discoveryApi: DiscoveryApi; + identityApi: IdentityApi; organizationId?: string; constructor({ discoveryApi, + identityApi, organizationId, }: { discoveryApi: DiscoveryApi; + identityApi: IdentityApi; organizationId?: string; }) { this.discoveryApi = discoveryApi; + this.identityApi = identityApi; this.organizationId = organizationId; } private async callApi(path: string): Promise { const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/fossa`; - const response = await fetch(`${apiUrl}/${path}`); + const idToken = await this.identityApi.getIdToken(); + const response = await fetch(`${apiUrl}/${path}`, { + headers: idToken ? { Authorization: `Bearer ${idToken}` } : {}, + }); if (response.status === 200) { return await response.json(); } diff --git a/plugins/fossa/src/plugin.ts b/plugins/fossa/src/plugin.ts index cb1de3911f..638be4a0bf 100644 --- a/plugins/fossa/src/plugin.ts +++ b/plugins/fossa/src/plugin.ts @@ -19,6 +19,7 @@ import { createApiFactory, createPlugin, discoveryApiRef, + identityApiRef, } from '@backstage/core'; import { fossaApiRef, FossaClient } from './api'; @@ -27,10 +28,15 @@ export const fossaPlugin = createPlugin({ apis: [ createApiFactory({ api: fossaApiRef, - deps: { configApi: configApiRef, discoveryApi: discoveryApiRef }, - factory: ({ configApi, discoveryApi }) => + deps: { + configApi: configApiRef, + discoveryApi: discoveryApiRef, + identityApi: identityApiRef, + }, + factory: ({ configApi, discoveryApi, identityApi }) => new FossaClient({ discoveryApi, + identityApi, organizationId: configApi.getOptionalString('fossa.organizationId'), }), }), diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index fced060d99..36dc403d7b 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.6.0", + "@backstage/core": "^0.6.1", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -42,8 +42,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/github-actions/CHANGELOG.md b/plugins/github-actions/CHANGELOG.md index a9bbf67a9a..8aba49fedc 100644 --- a/plugins/github-actions/CHANGELOG.md +++ b/plugins/github-actions/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-github-actions +## 0.3.2 + +### Patch Changes + +- f5f45744e: Migrate to new composability API, exporting the plugin instance as `githubActionsPlugin`, the entity content as `EntityGithubActionsContent`, entity conditional as `isGithubActionsAvailable`, and entity cards as `EntityLatestGithubActionRunCard`, `EntityLatestGithubActionsForBranchCard`, and `EntityRecentGithubActionsRunsCard`. +- Updated dependencies [ffffea8e6] +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/integration@0.4.0 + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.3.1 ### Patch Changes diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 7d05395b42..e5498563a5 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-github-actions", - "version": "0.3.1", + "version": "0.3.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,9 +33,9 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.1", - "@backstage/plugin-catalog-react": "^0.0.2", - "@backstage/core": "^0.6.0", - "@backstage/integration": "^0.3.2", + "@backstage/plugin-catalog-react": "^0.0.3", + "@backstage/core": "^0.6.1", + "@backstage/integration": "^0.4.0", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -51,8 +51,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/gitops-profiles/CHANGELOG.md b/plugins/gitops-profiles/CHANGELOG.md index 0fa9fa24a2..bb0834a03d 100644 --- a/plugins/gitops-profiles/CHANGELOG.md +++ b/plugins/gitops-profiles/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-gitops-profiles +## 0.2.5 + +### Patch Changes + +- accdfeb30: Migrated to new composability API, exporting the plugin instance as `gitopsProfilesPlugin` and pages as `GitopsProfilesClusterListPage`, `GitopsProfilesClusterPage`, and `GitopsProfilesCreatePage`. +- Updated dependencies [b51ee6ece] + - @backstage/core@0.6.1 + ## 0.2.4 ### Patch Changes diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index c25628fa62..251185b799 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-gitops-profiles", - "version": "0.2.4", + "version": "0.2.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,7 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.6.0", + "@backstage/core": "^0.6.1", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -43,8 +43,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index 9293a07dd7..307cef0dee 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -31,7 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.6.0", + "@backstage/core": "^0.6.1", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -44,8 +44,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/jenkins/CHANGELOG.md b/plugins/jenkins/CHANGELOG.md index 2199c58949..26c838d97a 100644 --- a/plugins/jenkins/CHANGELOG.md +++ b/plugins/jenkins/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-jenkins +## 0.3.9 + +### Patch Changes + +- 53348f0af: Improve display of duration in latest build card +- 025c0c7bf: Migrate to new composability API, exporting the plugin instance as `jenkinsPlugin`, the entity content as `EntityJenkinsContent`, the entity conditional as `isJenkinsAvailable`, and the entity card as `EntityLatestJenkinsRunCard`. +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.3.8 ### Patch Changes diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 5d59d2e5ab..fb3e719858 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-jenkins", - "version": "0.3.8", + "version": "0.3.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,8 +32,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/core": "^0.6.1", + "@backstage/plugin-catalog-react": "^0.0.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -48,8 +48,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/jenkins/src/components/Cards/Cards.tsx b/plugins/jenkins/src/components/Cards/Cards.tsx index 447afff266..6fc544bcea 100644 --- a/plugins/jenkins/src/components/Cards/Cards.tsx +++ b/plugins/jenkins/src/components/Cards/Cards.tsx @@ -40,13 +40,13 @@ const WidgetContent = ({ const classes = useStyles(); if (loading || !latestRun) return ; const displayDate = DateTime.fromMillis(latestRun.timestamp).toRelative(); - // TODO This works, but hard codes as minutes. Would prefer something smarter/relative. - const durationInMinutes = Math.round( - Duration.fromMillis(latestRun.duration).as('minutes'), - ); - const displayDuration = `${durationInMinutes} minute${ - durationInMinutes > 1 ? 's' : '' - }`; + const displayDuration = + (latestRun.building ? 'Running for ' : '') + + DateTime.local() + .minus(Duration.fromMillis(latestRun.duration)) + .toRelative({ locale: 'en' }) + ?.replace(' ago', ''); + return ( { const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}${path}`; + const idToken = await this.identityApi.getIdToken(); const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', + ...(idToken && { Authorization: `Bearer ${idToken}` }), }, body: JSON.stringify(requestBody), }); diff --git a/plugins/kubernetes/src/plugin.ts b/plugins/kubernetes/src/plugin.ts index af29df3dcc..24f51fe64a 100644 --- a/plugins/kubernetes/src/plugin.ts +++ b/plugins/kubernetes/src/plugin.ts @@ -18,6 +18,7 @@ import { createPlugin, createRouteRef, discoveryApiRef, + identityApiRef, googleAuthApiRef, createRoutableExtension, } from '@backstage/core'; @@ -36,9 +37,9 @@ export const kubernetesPlugin = createPlugin({ apis: [ createApiFactory({ api: kubernetesApiRef, - deps: { discoveryApi: discoveryApiRef }, - factory: ({ discoveryApi }) => - new KubernetesBackendClient({ discoveryApi }), + deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, + factory: ({ discoveryApi, identityApi }) => + new KubernetesBackendClient({ discoveryApi, identityApi }), }), createApiFactory({ api: kubernetesAuthProvidersApiRef, diff --git a/plugins/lighthouse/CHANGELOG.md b/plugins/lighthouse/CHANGELOG.md index 8fec8887c3..fab61283d9 100644 --- a/plugins/lighthouse/CHANGELOG.md +++ b/plugins/lighthouse/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-lighthouse +## 0.2.10 + +### Patch Changes + +- f5e564cd6: Improve display of error messages +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.2.9 ### Patch Changes diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 07e1ad8475..064e260389 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-lighthouse", - "version": "0.2.9", + "version": "0.2.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,8 +33,8 @@ "dependencies": { "@backstage/catalog-model": "^0.7.1", "@backstage/config": "^0.1.2", - "@backstage/core": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/core": "^0.6.1", + "@backstage/plugin-catalog-react": "^0.0.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -47,8 +47,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/newrelic/CHANGELOG.md b/plugins/newrelic/CHANGELOG.md index 6eae73bc86..ab8ad1c289 100644 --- a/plugins/newrelic/CHANGELOG.md +++ b/plugins/newrelic/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-newrelic +## 0.2.5 + +### Patch Changes + +- c5ab91ce3: Migrate to new composability API, exporting the plugin instance as `newRelicPlugin`, and the root page as `NewRelicPage`. +- Updated dependencies [b51ee6ece] + - @backstage/core@0.6.1 + ## 0.2.4 ### Patch Changes diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index ccd43c42c8..6ab18a9cf5 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-newrelic", - "version": "0.2.4", + "version": "0.2.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,7 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.6.0", + "@backstage/core": "^0.6.1", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -42,8 +42,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index 6101685e1d..898705cdc4 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-org +## 0.3.6 + +### Patch Changes + +- 14aef4b94: Visual updates to User and Group pages +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.3.5 ### Patch Changes diff --git a/plugins/org/package.json b/plugins/org/package.json index 0491554f77..4e12ad8837 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org", - "version": "0.3.5", + "version": "0.3.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/core": "^0.6.1", + "@backstage/plugin-catalog-react": "^0.0.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -34,8 +34,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx index 2f0f1e2e16..9a72606c16 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx @@ -21,8 +21,21 @@ import { RELATION_PARENT_OF, } from '@backstage/catalog-model'; import { Avatar, InfoCard } from '@backstage/core'; -import { useEntity, entityRouteParams } from '@backstage/plugin-catalog-react'; -import { Box, Grid, Link, Tooltip, Typography } from '@material-ui/core'; +import { + getEntityRelations, + entityRouteParams, + useEntity, +} from '@backstage/plugin-catalog-react'; +import { + Box, + Grid, + Link, + List, + ListItem, + ListItemIcon, + ListItemText, + Tooltip, +} from '@material-ui/core'; import AccountTreeIcon from '@material-ui/icons/AccountTree'; import EmailIcon from '@material-ui/icons/Email'; import GroupIcon from '@material-ui/icons/Group'; @@ -80,9 +93,9 @@ export const GroupProfileCard = ({ ?.map(groupItem => groupItem.target.name) .toString(); - const childrens = group?.relations - ?.filter(r => r.type === RELATION_PARENT_OF) - ?.map(groupItem => groupItem.target.name); + const childRelations = getEntityRelations(group, RELATION_PARENT_OF, { + kind: 'group', + }); const displayName = profile?.displayName ?? name; @@ -96,61 +109,51 @@ export const GroupProfileCard = ({ > - - - + - {profile?.email && ( - - - - - - - - {profile.email} - - - - )} - {parent ? ( - - - - - - + + {profile?.email && ( + + + + + + + {profile.email} + + )} + {parent ? ( + + + + + + + - - - - ) : null} - {childrens?.length ? ( - - - - - - - {childrens.map((children, index) => ( +
+ + ) : null} + {childRelations?.length ? ( + + + + + + + + {childRelations.map((children, index) => ( ))} - - - - ) : null} + + + ) : null} +
diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx index 1cf7c968b3..a4969f615b 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx @@ -19,8 +19,17 @@ import { UserEntity, } from '@backstage/catalog-model'; import { Avatar, InfoCard } from '@backstage/core'; -import { useEntity, entityRouteParams } from '@backstage/plugin-catalog-react'; -import { Box, Grid, Link, Tooltip, Typography } from '@material-ui/core'; +import { entityRouteParams, useEntity } from '@backstage/plugin-catalog-react'; +import { + Box, + Grid, + Link, + List, + ListItem, + ListItemIcon, + ListItemText, + Tooltip, +} from '@material-ui/core'; import EmailIcon from '@material-ui/icons/Email'; import GroupIcon from '@material-ui/icons/Group'; import PersonIcon from '@material-ui/icons/Person'; @@ -81,40 +90,35 @@ export const UserProfileCard = ({ return User not found; } + const emailHref = profile?.email ? `mailto:${profile.email}` : ''; + return ( } variant={variant}> - - - - - + + + - - {profile?.email && ( - - - - - - - {profile.email} - - - - )} - - - - - - + + + {profile?.email && ( + + + + + + {profile.email} + + + )} + + + + + + + + {groupNames.map((groupName, index) => ( ))} - - - + + + diff --git a/plugins/pagerduty/CHANGELOG.md b/plugins/pagerduty/CHANGELOG.md index eb00ce36be..3fe8e6db70 100644 --- a/plugins/pagerduty/CHANGELOG.md +++ b/plugins/pagerduty/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-pagerduty +## 0.2.8 + +### Patch Changes + +- 29a138636: Use the Luxon Date Library to follow the recommendations of ADR010. +- b288a291e: Migrated to new composability API, exporting the plugin instance as `pagerDutyPlugin`, entity card as `EntityPagerDutyCard`, and entity conditional as `isPagerDutyAvailable`. +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.2.7 ### Patch Changes diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index afd41dcc46..c430075b95 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-pagerduty", - "version": "0.2.7", + "version": "0.2.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,14 +31,14 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.1", - "@backstage/plugin-catalog-react": "^0.0.2", - "@backstage/core": "^0.6.0", + "@backstage/core": "^0.6.1", + "@backstage/plugin-catalog-react": "^0.0.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "classnames": "^2.2.6", - "date-fns": "^2.15.0", + "luxon": "1.25.0", "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", @@ -46,16 +46,16 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@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", - "node-fetch": "^2.6.1", - "cross-fetch": "^3.0.6" + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx b/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx index 860623dcd4..bf0d5d2587 100644 --- a/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx +++ b/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx @@ -27,7 +27,7 @@ import { Typography, } from '@material-ui/core'; import { StatusError, StatusWarning } from '@backstage/core'; -import { formatDistanceToNowStrict } from 'date-fns'; +import { DateTime, Duration } from 'luxon'; import { Incident } from '../types'; import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser'; @@ -53,8 +53,12 @@ type Props = { export const IncidentListItem = ({ incident }: Props) => { const classes = useStyles(); + const duration = + new Date().getTime() - new Date(incident.created_at).getTime(); + const createdAt = DateTime.local() + .minus(Duration.fromMillis(duration)) + .toRelative({ locale: 'en' }); const user = incident.assignments[0]?.assignee; - const createdAt = formatDistanceToNowStrict(new Date(incident.created_at)); return ( @@ -77,7 +81,7 @@ export const IncidentListItem = ({ incident }: Props) => { }} secondary={ - Created {createdAt} ago and assigned to{' '} + Created {createdAt} and assigned to{' '} { @@ -53,7 +58,10 @@ export class RollbarClient implements RollbarApi { private async get(path: string): Promise { const url = `${await this.discoveryApi.getBaseUrl('rollbar')}${path}`; - const response = await fetch(url); + const idToken = await this.identityApi.getIdToken(); + const response = await fetch(url, { + headers: idToken ? { Authorization: `Bearer ${idToken}` } : {}, + }); if (!response.ok) { const payload = await response.text(); diff --git a/plugins/rollbar/src/plugin.ts b/plugins/rollbar/src/plugin.ts index 216713b3fc..dc0ffa259a 100644 --- a/plugins/rollbar/src/plugin.ts +++ b/plugins/rollbar/src/plugin.ts @@ -20,6 +20,7 @@ import { createRoutableExtension, createRouteRef, discoveryApiRef, + identityApiRef, } from '@backstage/core'; import { rollbarApiRef } from './api/RollbarApi'; import { RollbarClient } from './api/RollbarClient'; @@ -34,8 +35,9 @@ export const rollbarPlugin = createPlugin({ apis: [ createApiFactory({ api: rollbarApiRef, - deps: { discoveryApi: discoveryApiRef }, - factory: ({ discoveryApi }) => new RollbarClient({ discoveryApi }), + deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, + factory: ({ discoveryApi, identityApi }) => + new RollbarClient({ discoveryApi, identityApi }), }), ], routes: { diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index e8526e0b56..5a3553dc79 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-scaffolder-backend +## 0.7.0 + +### Minor Changes + +- 615103a63: Introduced `v2` Scaffolder REST API, which uses an implementation that is database backed, making the scaffolder instances stateless. The `createRouter` function now requires a `PluginDatabaseManager` instance to be passed in, commonly available as `database` in the plugin environment in the backend. + + This API should be considered unstable until used by the scaffolder frontend. + +### Patch Changes + +- 6ed2b47d6: Include Backstage identity token in requests to backend plugins. +- ffffea8e6: Minor updates to reflect the changes in `@backstage/integration` that made the fields `apiBaseUrl` and `apiUrl` mandatory. +- Updated dependencies [6ed2b47d6] +- Updated dependencies [ffffea8e6] +- Updated dependencies [82b2c11b6] +- Updated dependencies [965e200c6] +- Updated dependencies [ffffea8e6] +- Updated dependencies [72b96e880] +- Updated dependencies [5a5163519] + - @backstage/catalog-client@0.3.6 + - @backstage/backend-common@0.5.3 + - @backstage/integration@0.4.0 + ## 0.6.0 ### Minor Changes diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index ecce161c8e..3b25fb3881 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "0.6.0", + "version": "0.7.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,10 +29,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.5.2", + "@backstage/backend-common": "^0.5.3", + "@backstage/catalog-client": "^0.3.6", "@backstage/catalog-model": "^0.7.1", "@backstage/config": "^0.1.2", - "@backstage/integration": "^0.3.2", + "@backstage/integration": "^0.4.0", "@gitbeaker/core": "^28.0.2", "@gitbeaker/node": "^28.0.2", "@octokit/rest": "^18.0.12", @@ -61,7 +62,7 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/test-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.7", "@types/fs-extra": "^9.0.1", "@types/mock-fs": "^4.13.0", "@types/supertest": "^2.0.8", diff --git a/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml b/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml index 36404f088c..168daffd20 100644 --- a/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml @@ -22,6 +22,7 @@ spec: component_id: title: Name type: string + pattern: ^[a-z0-9A-Z_.-]{1,63}$ description: Unique name of the component description: title: Description diff --git a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts index 4541f03a4a..827ed0559a 100644 --- a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts +++ b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import fetch from 'cross-fetch'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; +import { CatalogClient } from '@backstage/catalog-client'; import { ConflictError, NotFoundError, @@ -26,10 +26,12 @@ import { * A catalog client tailored for reading out entity data from the catalog. */ export class CatalogEntityClient { - private readonly discovery: PluginEndpointDiscovery; + private readonly catalogClient: CatalogClient; constructor(options: { discovery: PluginEndpointDiscovery }) { - this.discovery = options.discovery; + this.catalogClient = new CatalogClient({ + discoveryApi: options.discovery, + }); } /** @@ -37,25 +39,19 @@ export class CatalogEntityClient { * * Throws a NotFoundError or ConflictError if 0 or multiple templates are found. */ - async findTemplate(templateName: string): Promise { - const conditions = [ - 'kind=template', - `metadata.name=${encodeURIComponent(templateName)}`, - ]; - - const baseUrl = await this.discovery.getBaseUrl('catalog'); - const response = await fetch( - `${baseUrl}/entities?filter=${conditions.join(',')}`, - ); - - if (!response.ok) { - const text = await response.text(); - throw new Error( - `Request failed with ${response.status} ${response.statusText}, ${text}`, - ); - } - - const templates: TemplateEntityV1alpha1[] = await response.json(); + async findTemplate( + templateName: string, + options?: { token?: string }, + ): Promise { + const { items: templates } = (await this.catalogClient.getEntities( + { + filter: { + kind: 'template', + 'metadata.name': templateName, + }, + }, + options, + )) as { items: TemplateEntityV1alpha1[] }; if (templates.length !== 1) { if (templates.length > 1) { diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts index d473656863..02865f8529 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts @@ -43,7 +43,7 @@ export class BitbucketPreparer implements PreparerBase { const targetPath = path.join(workspacePath, 'template'); const fullPathToTemplate = path.resolve( checkoutPath, - parsedGitUrl.filepath, + parsedGitUrl.filepath ?? '', ); const git = Git.fromAuth({ logger, ...this.getAuth() }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts index 533aa8e005..77e6ec6d70 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts @@ -33,7 +33,7 @@ export class GithubPreparer implements PreparerBase { const targetPath = path.join(workspacePath, 'template'); const fullPathToTemplate = path.resolve( checkoutPath, - parsedGitUrl.filepath, + parsedGitUrl.filepath ?? '', ); const git = this.config.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 6ed4651e4f..2de2326505 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts @@ -37,8 +37,10 @@ describe('GitLabPreparer', () => { jest.clearAllMocks(); }); const preparer = GitlabPreparer.fromConfig({ - host: 'gitlab.com', + host: '', token: 'fake-token', + apiBaseUrl: '', + baseUrl: '', }); it(`calls the clone command with the correct arguments for a repository`, async () => { diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts index fa5c8c1325..e15de33ac6 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts @@ -33,7 +33,7 @@ export class GitlabPreparer implements PreparerBase { const targetPath = path.join(workspacePath, 'template'); const fullPathToTemplate = path.resolve( checkoutPath, - parsedGitUrl.filepath, + parsedGitUrl.filepath ?? '', ); parsedGitUrl.git_suffix = true; 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 61c8480efb..3826a54ddb 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts @@ -57,6 +57,7 @@ describe('GitLab Publisher', () => { const publisher = await GitlabPublisher.fromConfig( { host: 'gitlab.com', + apiBaseUrl: 'https://gitlab.com/api/v4', token: 'fake-token', baseUrl: 'https://gitlab.hosted.com', }, @@ -105,7 +106,9 @@ describe('GitLab Publisher', () => { const publisher = await GitlabPublisher.fromConfig( { host: 'gitlab.com', + apiBaseUrl: 'https://gitlab.com/api/v4', token: 'fake-token', + baseUrl: 'https://gitlab.com', }, { repoVisibility: 'public' }, ); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts index 30201e6dca..f5f7fc6361 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/publishers.ts @@ -126,6 +126,8 @@ export class Publishers implements PublisherBuilder { { token: config.getOptionalString('scaffolder.gitlab.token') ?? '', host: integration.config.host, + apiBaseUrl: ``, + baseUrl: `https://${integration.config.host}`, }, { repoVisibility }, ), diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 5821edfe8c..51425f75e8 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -133,7 +133,10 @@ export async function createRouter( }, }; - const template = await entityClient.findTemplate(templateName); + // Forward authorization from client + const template = await entityClient.findTemplate(templateName, { + token: getBearerToken(req.headers.authorization), + }); const validationResult: ValidatorResult = validate( values, @@ -295,3 +298,7 @@ export async function createRouter( return app; } + +function getBearerToken(header?: string): string | undefined { + return header?.match(/Bearer\s+(\S+)/i)?.[1]; +} diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index cd7c8fe685..2e0676e3aa 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-scaffolder +## 0.5.0 + +### Minor Changes + +- 6ed2b47d6: Include Backstage identity token in requests to backend plugins. + +### Patch Changes + +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.4.2 ### Patch Changes diff --git a/plugins/scaffolder/dev/index.tsx b/plugins/scaffolder/dev/index.tsx index eff16ab0f5..e75aadb03f 100644 --- a/plugins/scaffolder/dev/index.tsx +++ b/plugins/scaffolder/dev/index.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { createDevApp } from '@backstage/dev-utils'; -import { discoveryApiRef } from '@backstage/core'; +import { discoveryApiRef, identityApiRef } from '@backstage/core'; import { CatalogClient } from '@backstage/catalog-client'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { TemplateIndexPage, TemplatePage } from '../src/plugin'; @@ -30,8 +30,9 @@ createDevApp() }) .registerApi({ api: scaffolderApiRef, - deps: { discoveryApi: discoveryApiRef }, - factory: ({ discoveryApi }) => new ScaffolderApi({ discoveryApi }), + deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, + factory: ({ discoveryApi, identityApi }) => + new ScaffolderApi({ discoveryApi, identityApi }), }) .addPage({ path: '/create', diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 56beb7b10b..2b0e1a3771 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder", - "version": "0.4.2", + "version": "0.5.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,8 +31,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/core": "^0.6.1", + "@backstage/plugin-catalog-react": "^0.0.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -52,9 +52,9 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", - "@backstage/catalog-client": "^0.3.5", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", + "@backstage/catalog-client": "^0.3.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index ca52418e92..453b1b6a7e 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createApiRef, DiscoveryApi } from '@backstage/core'; +import { createApiRef, DiscoveryApi, IdentityApi } from '@backstage/core'; export const scaffolderApiRef = createApiRef({ id: 'plugin.scaffolder.service', @@ -23,9 +23,14 @@ export const scaffolderApiRef = createApiRef({ export class ScaffolderApi { private readonly discoveryApi: DiscoveryApi; + private readonly identityApi: IdentityApi; - constructor(options: { discoveryApi: DiscoveryApi }) { + constructor(options: { + discoveryApi: DiscoveryApi; + identityApi: IdentityApi; + }) { this.discoveryApi = options.discoveryApi; + this.identityApi = options.identityApi; } /** @@ -36,11 +41,13 @@ export class ScaffolderApi { * @param values Parameters for the template, e.g. name, description */ async scaffold(templateName: string, values: Record) { + const token = await this.identityApi.getIdToken(); const url = `${await this.discoveryApi.getBaseUrl('scaffolder')}/v1/jobs`; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', + ...(token && { Authorization: `Bearer ${token}` }), }, body: JSON.stringify({ templateName, values: { ...values } }), }); @@ -56,8 +63,11 @@ export class ScaffolderApi { } async getJob(jobId: string) { + const token = await this.identityApi.getIdToken(); const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder'); const url = `${baseUrl}/v1/job/${encodeURIComponent(jobId)}`; - return fetch(url).then(x => x.json()); + return fetch(url, { + headers: token ? { Authorization: `Bearer ${token}` } : {}, + }).then(x => x.json()); } } diff --git a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx index 732b5f3e44..ef17335b26 100644 --- a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx +++ b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx @@ -17,6 +17,8 @@ import { Button } from '@backstage/core'; import { BackstageTheme, pageTheme } from '@backstage/theme'; import { Card, + CardActions, + CardContent, Chip, makeStyles, Typography, @@ -34,18 +36,11 @@ const useStyles = makeStyles(theme => ({ props.backgroundImage, backgroundPosition: 0, }, - content: { - padding: theme.spacing(2), - }, description: { height: 175, overflow: 'hidden', textOverflow: 'ellipsis', }, - footer: { - display: 'flex', - flexDirection: 'row-reverse', - }, })); export type TemplateCardProps = { @@ -76,19 +71,19 @@ export const TemplateCard = ({ {type} {title}
-
+ {tags?.map(tag => ( ))} {description} -
- -
-
+ + + + ); }; diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx index f3f97be877..09df6dba1a 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx @@ -99,7 +99,7 @@ describe('TemplatePage', () => { , ); - expect(rendered.queryByText('Create a new component')).toBeInTheDocument(); + expect(rendered.queryByText('Create a New Component')).toBeInTheDocument(); expect(rendered.queryByText('React SSR Template')).toBeInTheDocument(); // await act(async () => await mutate('templates/test')); }); @@ -116,7 +116,7 @@ describe('TemplatePage', () => { , ); - expect(rendered.queryByText('Create a new component')).toBeInTheDocument(); + expect(rendered.queryByText('Create a New Component')).toBeInTheDocument(); expect(rendered.queryByTestId('loading-progress')).toBeInTheDocument(); // Need to cleanup the promise or will timeout act(() => { @@ -141,7 +141,7 @@ describe('TemplatePage', () => { ); expect( - rendered.queryByText('Create a new component'), + rendered.queryByText('Create a New Component'), ).not.toBeInTheDocument(); expect(rendered.queryByText('This is root')).toBeInTheDocument(); }); diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index e1422d4ec1..d779f8b42c 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -149,10 +149,10 @@ export const TemplatePage = () => { return (
- Create a new component + Create a New Component } subtitle="Create new software components using standard templates" diff --git a/plugins/scaffolder/src/plugin.ts b/plugins/scaffolder/src/plugin.ts index ed20705185..3e884d7683 100644 --- a/plugins/scaffolder/src/plugin.ts +++ b/plugins/scaffolder/src/plugin.ts @@ -18,6 +18,7 @@ import { createPlugin, createApiFactory, discoveryApiRef, + identityApiRef, createRoutableExtension, } from '@backstage/core'; import { ScaffolderPage as ScaffolderPageComponent } from './components/ScaffolderPage'; @@ -30,8 +31,9 @@ export const scaffolderPlugin = createPlugin({ apis: [ createApiFactory({ api: scaffolderApiRef, - deps: { discoveryApi: discoveryApiRef }, - factory: ({ discoveryApi }) => new ScaffolderApi({ discoveryApi }), + deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, + factory: ({ discoveryApi, identityApi }) => + new ScaffolderApi({ discoveryApi, identityApi }), }), ], register({ router }) { diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index 383cb3c484..38cbf202e1 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-search +## 0.3.0 + +### Minor Changes + +- b3f0c3811: Migrated to new composability API, exporting the plugin instance as `searchPlugin`, and page as `SearchPage`. Due to the old router component also being called `SearchPage`, this is a breaking change. The old page component is now exported as `Router`, which can be used to maintain the old behavior. + +### Patch Changes + +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.2.7 ### Patch Changes diff --git a/plugins/search/package.json b/plugins/search/package.json index 6142e65dd3..56f863a2a6 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search", - "version": "0.2.7", + "version": "0.3.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,9 +29,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.6.0", + "@backstage/core": "^0.6.1", "@backstage/catalog-model": "^0.7.1", - "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/plugin-catalog-react": "^0.0.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -44,8 +44,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/sentry/CHANGELOG.md b/plugins/sentry/CHANGELOG.md index 698f72020c..4c4d68c67b 100644 --- a/plugins/sentry/CHANGELOG.md +++ b/plugins/sentry/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-sentry +## 0.3.5 + +### Patch Changes + +- 53d3e2d62: Export the plugin instance as `sentryPlugin`. The plugin instance is still exported as `plugin` as well, but it will be removed in the future. +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.3.4 ### Patch Changes diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 06b538289b..b79b7613fe 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sentry", - "version": "0.3.4", + "version": "0.3.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,8 +32,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/core": "^0.6.1", + "@backstage/plugin-catalog-react": "^0.0.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -47,8 +47,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/sentry/src/extensions.tsx b/plugins/sentry/src/extensions.tsx index ea63cd472a..9a16a6a7da 100644 --- a/plugins/sentry/src/extensions.tsx +++ b/plugins/sentry/src/extensions.tsx @@ -20,9 +20,9 @@ import { } from '@backstage/core'; import { useEntity } from '@backstage/plugin-catalog-react'; import React from 'react'; -import { plugin, rootRouteRef } from './plugin'; +import { sentryPlugin, rootRouteRef } from './plugin'; -export const EntitySentryContent = plugin.provide( +export const EntitySentryContent = sentryPlugin.provide( createRoutableExtension({ mountPoint: rootRouteRef, component: () => @@ -38,7 +38,7 @@ export const EntitySentryContent = plugin.provide( }), ); -export const EntitySentryCard = plugin.provide( +export const EntitySentryCard = sentryPlugin.provide( createComponentExtension({ component: { lazy: () => diff --git a/plugins/sentry/src/index.ts b/plugins/sentry/src/index.ts index de53d7456a..2fc51715f6 100644 --- a/plugins/sentry/src/index.ts +++ b/plugins/sentry/src/index.ts @@ -16,6 +16,6 @@ export * from './api'; export * from './components'; -export { plugin } from './plugin'; +export { sentryPlugin, sentryPlugin as plugin } from './plugin'; export { EntitySentryCard, EntitySentryContent } from './extensions'; export { Router } from './components/Router'; diff --git a/plugins/sentry/src/plugin.test.ts b/plugins/sentry/src/plugin.test.ts index 8f24236586..af5feaa774 100644 --- a/plugins/sentry/src/plugin.test.ts +++ b/plugins/sentry/src/plugin.test.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { plugin } from './plugin'; +import { sentryPlugin } from './plugin'; describe('sentry', () => { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect(sentryPlugin).toBeDefined(); }); }); diff --git a/plugins/sentry/src/plugin.ts b/plugins/sentry/src/plugin.ts index 49ecfdb5f2..4c2a00de59 100644 --- a/plugins/sentry/src/plugin.ts +++ b/plugins/sentry/src/plugin.ts @@ -28,7 +28,7 @@ export const rootRouteRef = createRouteRef({ title: 'Sentry', }); -export const plugin = createPlugin({ +export const sentryPlugin = createPlugin({ id: 'sentry', apis: [ createApiFactory({ diff --git a/plugins/sonarqube/CHANGELOG.md b/plugins/sonarqube/CHANGELOG.md index c422f9d72e..11306bb5cd 100644 --- a/plugins/sonarqube/CHANGELOG.md +++ b/plugins/sonarqube/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-sonarqube +## 0.1.11 + +### Patch Changes + +- Updated dependencies [19d354c78] +- Updated dependencies [b51ee6ece] + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.1.10 ### Patch Changes diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index a31ee320f2..e8236fbfd6 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sonarqube", - "version": "0.1.10", + "version": "0.1.11", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,8 +33,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.1", - "@backstage/plugin-catalog-react": "^0.0.2", - "@backstage/core": "^0.6.0", + "@backstage/plugin-catalog-react": "^0.0.3", + "@backstage/core": "^0.6.1", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -48,8 +48,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/tech-radar/CHANGELOG.md b/plugins/tech-radar/CHANGELOG.md index 49a5f8bfcd..3c4e3a0706 100644 --- a/plugins/tech-radar/CHANGELOG.md +++ b/plugins/tech-radar/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-tech-radar +## 0.3.5 + +### Patch Changes + +- 804502a5c: Migrated to new composability API, exporting the plugin instance as `techRadarPlugin` and the page as `TechRadarPage`. +- Updated dependencies [b51ee6ece] + - @backstage/core@0.6.1 + ## 0.3.4 ### Patch Changes diff --git a/plugins/tech-radar/dev/index.tsx b/plugins/tech-radar/dev/index.tsx index 92eb6da567..0e9495394f 100644 --- a/plugins/tech-radar/dev/index.tsx +++ b/plugins/tech-radar/dev/index.tsx @@ -14,7 +14,14 @@ * limitations under the License. */ +import React from 'react'; import { createDevApp } from '@backstage/dev-utils'; -import { plugin } from '../src'; +import { techRadarPlugin, TechRadarPage } from '../src'; -createDevApp().registerPlugin(plugin).render(); +createDevApp() + .registerPlugin(techRadarPlugin) + .addPage({ + title: 'Tech Radar', + element: , + }) + .render(); diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index a5c9f7bb53..7a7c886372 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-radar", - "version": "0.3.4", + "version": "0.3.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,7 +30,7 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.6.0", + "@backstage/core": "^0.6.1", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -44,8 +44,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/tech-radar/src/index.ts b/plugins/tech-radar/src/index.ts index d7b4921e9a..b13e0fbeed 100644 --- a/plugins/tech-radar/src/index.ts +++ b/plugins/tech-radar/src/index.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -export { plugin } from './plugin'; +export { + techRadarPlugin, + techRadarPlugin as plugin, + TechRadarPage, +} from './plugin'; export { RadarPage as Router } from './components/RadarPage'; diff --git a/plugins/tech-radar/src/plugin.test.ts b/plugins/tech-radar/src/plugin.test.ts index 6ae65f8271..f3f2fdbd77 100644 --- a/plugins/tech-radar/src/plugin.test.ts +++ b/plugins/tech-radar/src/plugin.test.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { plugin } from './plugin'; +import { techRadarPlugin } from './plugin'; describe('tech-radar', () => { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect(techRadarPlugin).toBeDefined(); }); }); diff --git a/plugins/tech-radar/src/plugin.ts b/plugins/tech-radar/src/plugin.ts index d8dc41af2e..63af1f390f 100644 --- a/plugins/tech-radar/src/plugin.ts +++ b/plugins/tech-radar/src/plugin.ts @@ -14,8 +14,26 @@ * limitations under the License. */ -import { createPlugin } from '@backstage/core'; +import { + createPlugin, + createRouteRef, + createRoutableExtension, +} from '@backstage/core'; -export const plugin = createPlugin({ - id: 'tech-radar', +const rootRouteRef = createRouteRef({ + title: 'Tech Radar', }); + +export const techRadarPlugin = createPlugin({ + id: 'tech-radar', + routes: { + root: rootRouteRef, + }, +}); + +export const TechRadarPage = techRadarPlugin.provide( + createRoutableExtension({ + component: () => import('./components/RadarPage').then(m => m.RadarPage), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index 28ba4e032c..50925cec5f 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-techdocs-backend +## 0.6.0 + +### Minor Changes + +- 08142b256: URL Preparer will now use proper etag based caching introduced in https://github.com/backstage/backstage/pull/4120. Previously, builds used to be cached for 30 minutes. + +### Patch Changes + +- 08142b256: TechDocs will throw warning in backend logs when legacy git preparer or dir preparer is used to preparer docs. Migrate to URL Preparer by updating `backstage.io/techdocs-ref` annotation to be prefixed with `url:`. + Detailed docs are here https://backstage.io/docs/features/techdocs/how-to-guides#how-to-use-url-reader-in-techdocs-prepare-step + See benefits and reason for doing so https://github.com/backstage/backstage/issues/4409 +- Updated dependencies [77ad0003a] +- Updated dependencies [ffffea8e6] +- Updated dependencies [82b2c11b6] +- Updated dependencies [965e200c6] +- Updated dependencies [5a5163519] +- Updated dependencies [08142b256] +- Updated dependencies [08142b256] + - @backstage/techdocs-common@0.4.0 + - @backstage/backend-common@0.5.3 + ## 0.5.5 ### Patch Changes diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 36e5bd3bcd..2d4d1309c0 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-backend", - "version": "0.5.5", + "version": "0.6.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,10 +30,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.5.2", + "@backstage/backend-common": "^0.5.3", "@backstage/catalog-model": "^0.7.1", "@backstage/config": "^0.1.2", - "@backstage/techdocs-common": "^0.3.7", + "@backstage/techdocs-common": "^0.4.0", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", "cross-fetch": "^3.0.6", diff --git a/plugins/techdocs-backend/src/DocsBuilder/BuildMetadataStorage.test.ts b/plugins/techdocs-backend/src/DocsBuilder/BuildMetadataStorage.test.ts index 87de9d1d02..54ea860fb9 100644 --- a/plugins/techdocs-backend/src/DocsBuilder/BuildMetadataStorage.test.ts +++ b/plugins/techdocs-backend/src/DocsBuilder/BuildMetadataStorage.test.ts @@ -17,11 +17,11 @@ import { BuildMetadataStorage } from './BuildMetadataStorage'; describe('BuildMetadataStorage', () => { it('should return build timestamp', () => { - const newMetadataStorage = new BuildMetadataStorage('123abc'); - newMetadataStorage.storeBuildTimestamp(); + const newMetadataStorage = new BuildMetadataStorage('entityID123abc'); + newMetadataStorage.setEtag('etag123abc'); - const timestamp = newMetadataStorage.getTimestamp(); + const timestamp = newMetadataStorage.getEtag(); - expect(timestamp).toBeLessThanOrEqual(Date.now()); + expect(timestamp).toBe('etag123abc'); }); }); diff --git a/plugins/techdocs-backend/src/DocsBuilder/BuildMetadataStorage.ts b/plugins/techdocs-backend/src/DocsBuilder/BuildMetadataStorage.ts index d6a19764d9..a1bb918fae 100644 --- a/plugins/techdocs-backend/src/DocsBuilder/BuildMetadataStorage.ts +++ b/plugins/techdocs-backend/src/DocsBuilder/BuildMetadataStorage.ts @@ -14,10 +14,15 @@ * limitations under the License. */ type buildInfo = { - // uid: timestamp - [key: string]: number; + // Entity uid: etag + [key: string]: string; }; +// TODO: Build info should be part of TechDocs storage, inside `techdocs_metadata.json` +// instead of in-memory storage of the Backstage instance. +// In case of multi-region Backstage deployments, or even using multiple Kubernetes pods, +// if each instance creates its separate build info in-memory, it will result in duplicate +// builds per instance. Also if the pod restarts, all the sites will have to be re-built. const builds = {} as buildInfo; /** @@ -34,11 +39,11 @@ export class BuildMetadataStorage { this.builds = builds; } - storeBuildTimestamp() { - this.builds[this.entityUid] = Date.now(); + setEtag(etag: string): void { + this.builds[this.entityUid] = etag; } - getTimestamp() { + getEtag(): string | undefined { return this.builds[this.entityUid]; } } diff --git a/plugins/techdocs-backend/src/DocsBuilder/builder.ts b/plugins/techdocs-backend/src/DocsBuilder/builder.ts index 33d3120c47..8c54a2b1e4 100644 --- a/plugins/techdocs-backend/src/DocsBuilder/builder.ts +++ b/plugins/techdocs-backend/src/DocsBuilder/builder.ts @@ -13,22 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { NotModifiedError } from '@backstage/backend-common'; +import { Entity } from '@backstage/catalog-model'; +import { + GeneratorBase, + GeneratorBuilder, + getLocationForEntity, + PreparerBase, + PreparerBuilder, + PublisherBase, + UrlPreparer, +} from '@backstage/techdocs-common'; +import Docker from 'dockerode'; import fs from 'fs-extra'; import os from 'os'; import path from 'path'; -import Docker from 'dockerode'; import { Logger } from 'winston'; -import { Entity } from '@backstage/catalog-model'; -import { Config } from '@backstage/config'; -import { - PreparerBuilder, - PublisherBase, - GeneratorBuilder, - PreparerBase, - GeneratorBase, - getLocationForEntity, - getLastCommitTimestamp, -} from '@backstage/techdocs-common'; import { BuildMetadataStorage } from '.'; const getEntityId = (entity: Entity) => { @@ -44,7 +44,6 @@ type DocsBuilderArguments = { entity: Entity; logger: Logger; dockerClient: Docker; - config: Config; }; export class DocsBuilder { @@ -54,7 +53,6 @@ export class DocsBuilder { private entity: Entity; private logger: Logger; private dockerClient: Docker; - private config: Config; constructor({ preparers, @@ -63,7 +61,6 @@ export class DocsBuilder { entity, logger, dockerClient, - config, }: DocsBuilderArguments) { this.preparer = preparers.get(entity); this.generator = generators.get(entity); @@ -71,14 +68,53 @@ export class DocsBuilder { this.entity = entity; this.logger = logger; this.dockerClient = dockerClient; - this.config = config; } - public async build() { - this.logger.info(`Running preparer on entity ${getEntityId(this.entity)}`); - const preparedDir = await this.preparer.prepare(this.entity); + public async build(): Promise { + if (!this.entity.metadata.uid) { + throw new Error( + 'Trying to build documentation for entity not in service catalog', + ); + } - const parsedLocationAnnotation = getLocationForEntity(this.entity); + /** + * Prepare and cache check + */ + + // Use the in-memory storage for setting and getting etag for this entity. + const buildMetadataStorage = new BuildMetadataStorage( + this.entity.metadata.uid, + ); + + // TODO: As of now, this happens on each and every request to TechDocs. + // In a high traffic environment, this will cause a lot of requests to the source code provider. + // After Async build is implemented https://github.com/backstage/backstage/issues/3717, + // make sure to limit checking for cache invalidation to once per minute or so. + let preparedDir: string; + let etag: string; + try { + const preparerResponse = await this.preparer.prepare(this.entity, { + etag: buildMetadataStorage.getEtag(), + }); + + preparedDir = preparerResponse.preparedDir; + etag = preparerResponse.etag; + } catch (err) { + if (err instanceof NotModifiedError) { + // No need to prepare anymore since cache is valid. + return; + } + throw new Error(err.message); + } + + this.logger.info( + `TechDocs prepare step completed for entity ${getEntityId(this.entity)}.`, + ); + this.logger.debug(`Prepared files temporarily stored at ${preparedDir}`); + + /** + * Generate + */ this.logger.info(`Running generator on entity ${getEntityId(this.entity)}`); // Create a temporary directory to store the generated files in. @@ -88,7 +124,7 @@ export class DocsBuilder { const outputDir = await fs.mkdtemp( path.join(tmpdirResolvedPath, 'techdocs-tmp-'), ); - + const parsedLocationAnnotation = getLocationForEntity(this.entity); await this.generator.run({ inputDir: preparedDir, outputDir, @@ -96,69 +132,42 @@ export class DocsBuilder { parsedLocationAnnotation, }); + this.logger.debug(`Generated files temporarily stored at ${outputDir}`); + // Remove Prepared directory since it is no longer needed. + // Caveat: Can not remove prepared directory in case of git preparer since the + // local git repository is used to get etag on subsequent requests. + if (this.preparer instanceof UrlPreparer) { + this.logger.debug( + `Removing prepared directory ${preparedDir} since the site has been generated.`, + ); + try { + // Not a blocker hence no need to await this. + fs.remove(preparedDir); + } catch (error) { + this.logger.debug(`Error removing prepared directory ${error.message}`); + } + } + + /** + * Publish + */ + this.logger.info(`Running publisher on entity ${getEntityId(this.entity)}`); await this.publisher.publish({ entity: this.entity, directory: outputDir, }); - - // TODO: Remove the generated directory once published. - - if (!this.entity.metadata.uid) { - throw new Error( - 'Trying to build documentation for entity not in service catalog', - ); - } - - new BuildMetadataStorage(this.entity.metadata.uid).storeBuildTimestamp(); - } - - public async docsUpToDate() { - if (!this.entity.metadata.uid) { - throw new Error( - 'Trying to build documentation for entity not in service catalog', - ); - } - - const buildMetadataStorage = new BuildMetadataStorage( - this.entity.metadata.uid, - ); - const { type, target } = getLocationForEntity(this.entity); - - // Unless docs are stored locally - const nonAgeCheckTypes = ['dir', 'file', 'url']; - if (!nonAgeCheckTypes.includes(type)) { - const lastCommit = await getLastCommitTimestamp( - target, - this.config, - this.logger, - ); - const storageTimeStamp = buildMetadataStorage.getTimestamp(); - - // Check if documentation source is newer than what we have - if (storageTimeStamp && storageTimeStamp >= lastCommit) { - this.logger.debug( - `Docs for entity ${getEntityId(this.entity)} is up to date.`, - ); - return true; - } - } - - // Cache downloaded source files for 30 minutes. - // TODO: When urlReader/readTree supports some way to get latest commit timestamp, - // it should be used to invalidate cache. - if (type === 'url') { - const builtAt = buildMetadataStorage.getTimestamp(); - const now = Date.now(); - - if (builtAt > now - 1800000) { - return true; - } - } - this.logger.debug( - `Docs for entity ${getEntityId(this.entity)} was outdated.`, + `Removing generated directory ${outputDir} since the site has been published`, ); - return false; + try { + // Not a blocker hence no need to await this. + fs.remove(outputDir); + } catch (error) { + this.logger.debug(`Error removing generated directory ${error.message}`); + } + + // Store the latest build etag for the entity + new BuildMetadataStorage(this.entity.metadata.uid).setEtag(etag); } } diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index fe7b52ab52..ef032e9f8b 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -13,23 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Logger } from 'winston'; -import Router from 'express-promise-router'; -import express from 'express'; -import Knex from 'knex'; -import fetch from 'cross-fetch'; -import { Config } from '@backstage/config'; -import Docker from 'dockerode'; -import { - GeneratorBuilder, - PreparerBuilder, - PublisherBase, - getLocationForEntity, -} from '@backstage/techdocs-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; -import { getEntityNameFromUrlPath } from './helpers'; +import { Config } from '@backstage/config'; +import { + GeneratorBuilder, + getLocationForEntity, + PreparerBuilder, + PublisherBase, +} from '@backstage/techdocs-common'; +import fetch from 'cross-fetch'; +import Docker from 'dockerode'; +import express from 'express'; +import Router from 'express-promise-router'; +import Knex from 'knex'; +import { Logger } from 'winston'; import { DocsBuilder } from '../DocsBuilder'; +import { getEntityNameFromUrlPath } from './helpers'; type RouterOptions = { preparers: PreparerBuilder; @@ -141,13 +141,10 @@ export async function createRouter({ dockerClient, logger, entity, - config, }); switch (publisherType) { case 'local': - if (!(await docsBuilder.docsUpToDate())) { - await docsBuilder.build(); - } + await docsBuilder.build(); break; case 'awsS3': case 'azureBlobStorage': @@ -186,9 +183,11 @@ export async function createRouter({ 'Found pre-generated docs for this entity. Serving them.', ); // TODO: re-trigger build for cache invalidation. - // Compare the date modified of the requested file on storage and compare it against - // the last modified or last commit timestamp in the repository. + // Add build info in techdocs_metadata.json and compare it against + // the etag/commit in the repository. // Without this, docs will not be re-built once they have been generated. + // Although it is unconventional that anyone will face this issue - because + // if you have an external storage, you should be using CI/CD to build and publish docs. } break; default: diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index 69fe41dd32..f782dcb3ad 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-techdocs +## 0.5.6 + +### Patch Changes + +- f5e564cd6: Improve display of error messages +- 41af18227: Migrated to new composability API, exporting the plugin instance as `techdocsPlugin`, the top-level page as `TechdocsPage`, and the entity content as `EntityTechdocsContent`. +- 8f3443427: Enhance API calls to support trapping 500 errors from techdocs-backend +- Updated dependencies [77ad0003a] +- Updated dependencies [b51ee6ece] +- Updated dependencies [19d354c78] +- Updated dependencies [08142b256] +- Updated dependencies [08142b256] +- Updated dependencies [b51ee6ece] + - @backstage/techdocs-common@0.4.0 + - @backstage/test-utils@0.1.7 + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + ## 0.5.5 ### Patch Changes diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index ad9e1f7bef..afd829d94d 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs", - "version": "0.5.5", + "version": "0.5.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,11 +33,11 @@ "dependencies": { "@backstage/config": "^0.1.2", "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.0.2", - "@backstage/test-utils": "^0.1.6", + "@backstage/core": "^0.6.1", + "@backstage/plugin-catalog-react": "^0.0.3", + "@backstage/test-utils": "^0.1.7", "@backstage/theme": "^0.2.3", - "@backstage/techdocs-common": "^0.3.7", + "@backstage/techdocs-common": "^0.4.0", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -51,8 +51,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index 88453e8efb..493fb16a28 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.6.0", + "@backstage/core": "^0.6.1", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -42,8 +42,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/user-settings/src/components/SettingsPage.tsx b/plugins/user-settings/src/components/SettingsPage.tsx index 0b4a86c99d..9fb576b200 100644 --- a/plugins/user-settings/src/components/SettingsPage.tsx +++ b/plugins/user-settings/src/components/SettingsPage.tsx @@ -14,39 +14,35 @@ * limitations under the License. */ -import React, { useState } from 'react'; -import { Content, Header, HeaderTabs, Page } from '@backstage/core'; -import { General } from './General'; +import { Header, Page, TabbedLayout } from '@backstage/core'; +import React from 'react'; import { AuthProviders } from './AuthProviders'; import { FeatureFlags } from './FeatureFlags'; +import { General } from './General'; type Props = { providerSettings?: JSX.Element; }; export const SettingsPage = ({ providerSettings }: Props) => { - const [activeTab, setActiveTab] = useState(0); - const onTabChange = (index: number) => { - setActiveTab(index); - }; - - const tabs = [ - { id: 'general', label: 'General' }, - { id: 'auth-providers', label: 'Authentication Providers' }, - { id: 'feature-flags', label: 'Feature Flags' }, - ]; - - const content = [ - , - , - , - ]; - return (
- - {content[activeTab]} + + + + + + + + + + + + ); }; diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index f3d50dc675..b6744216f3 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -30,7 +30,7 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.6.0", + "@backstage/core": "^0.6.1", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -42,8 +42,8 @@ }, "devDependencies": { "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/yarn.lock b/yarn.lock index 05282e5285..3bfd57b222 100644 --- a/yarn.lock +++ b/yarn.lock @@ -102,182 +102,6 @@ resolved "https://registry.npmjs.org/@asyncapi/specs/-/specs-2.7.5.tgz#3a516d198fc41a1103695bd889fdd4fbbebe7f5d" integrity sha512-T1Ham9sqZKCtSowXRPaBCRy2oz3KHglqqrKiaO7lEudpP6lwH5SwXaq4qliyKzWaqd22srJHE4szdsorbFZKlw== -"@aws-crypto/crc32@^1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-1.0.0.tgz#6a0164fd92bb365860ba6afb5dfef449701eb8ca" - integrity sha512-wr4EyCv3ZfLH3Sg7FErV6e/cLhpk9rUP/l5322y8PRgpQsItdieaLbtE4aDOR+dxl8U7BG9FIwWXH4TleTDZ9A== - dependencies: - tslib "^1.11.1" - -"@aws-crypto/ie11-detection@^1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-1.0.0.tgz#d3a6af29ba7f15458f79c41d1cd8cac3925e726a" - integrity sha512-kCKVhCF1oDxFYgQrxXmIrS5oaWulkvRcPz+QBDMsUr2crbF4VGgGT6+uQhSwJFdUAQ2A//Vq+uT83eJrkzFgXA== - dependencies: - tslib "^1.11.1" - -"@aws-crypto/sha256-browser@^1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-1.0.0.tgz#9c34d3b829d922b2c8e077b30a60db53d6befcb1" - integrity sha512-uSufui4ZktC5lYX6bDxgBgNboxGyw9V9V+rlcNsNTxh4YPhOdCslwJMGntiWOBRGAgXhhvWi7FqnTS2SaT3cpg== - dependencies: - "@aws-crypto/ie11-detection" "^1.0.0" - "@aws-crypto/sha256-js" "^1.0.0" - "@aws-crypto/supports-web-crypto" "^1.0.0" - "@aws-sdk/types" "^1.0.0-rc.1" - "@aws-sdk/util-locate-window" "^1.0.0-rc.1" - "@aws-sdk/util-utf8-browser" "^1.0.0-rc.1" - tslib "^1.11.1" - -"@aws-crypto/sha256-js@^1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-1.0.0.tgz#ca788a3242a4024c386e6b9985da28f290a79ad7" - integrity sha512-89kqtFs/tdHBFHEBXZ4UXlCISswvEor3BVVOriR68Tbk1Qe1zBOZtfbSOt3CDT69z88x5uM558YW9k8I1xei5A== - dependencies: - "@aws-sdk/types" "^1.0.0-rc.1" - "@aws-sdk/util-utf8-browser" "^1.0.0-rc.1" - tslib "^1.11.1" - -"@aws-crypto/supports-web-crypto@^1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-1.0.0.tgz#c40901bc17ac1e875e248df16a2b47ad8bfd9a93" - integrity sha512-IHLfv+WmVH89EW4n6a5eE8/hUlz6qkWGMn/v4r5ZgzcXdTC5nolii2z3k46y01hWRiC2PPhOdeSLzMUCUMco7g== - dependencies: - tslib "^1.11.1" - -"@aws-sdk/abort-controller@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/abort-controller/-/abort-controller-3.1.0.tgz#6b1e6cfe7410ff38ff5efdfc2e16c309d7c4e233" - integrity sha512-rqE/+BBEvF4ZxT+J1hqaSDUzXTcdnyeHQ7vqEyB1UuAmMcq6nIi1rAQKqBXneaIC4WVER3K5f00NKZGLzzqnRg== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/chunked-blob-reader-native@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/chunked-blob-reader-native/-/chunked-blob-reader-native-3.1.0.tgz#49573fe4087f07894deef7a1bf184517c3fb8f24" - integrity sha512-ghBtZkhUWgy51/651l/GUR/qhdqjFR3GSCsz0B7qisrXc8ZNsd7OlXfnTfYNoySxD3XKpbcxsncytH4Hkxgi4A== - dependencies: - "@aws-sdk/util-base64-browser" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/chunked-blob-reader@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/chunked-blob-reader/-/chunked-blob-reader-3.1.0.tgz#0a17272040760ce54ee6cd5b7e56efaca81e07aa" - integrity sha512-/2fxbKwta8ZiSj59B8F3FyVRszo1/VOhpCeL16gmRRNV73rM3IqJD+xOaDdkc/sFYyBeWn/UhwgD98kxae9XsQ== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/client-organizations@^3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/client-organizations/-/client-organizations-3.2.0.tgz#f9fc15ecc61fdf31d1cc1eb5590e526b60bc83c0" - integrity sha512-zIFHW5tETCeoSkmLh227eG25B4HnngVDktXhbPmp1NJjT2pcZzL5QIXuNcPz3PTbkaDNRdB04uRdDqryK4kY/g== - dependencies: - "@aws-crypto/sha256-browser" "^1.0.0" - "@aws-crypto/sha256-js" "^1.0.0" - "@aws-sdk/config-resolver" "3.2.0" - "@aws-sdk/credential-provider-node" "3.1.0" - "@aws-sdk/fetch-http-handler" "3.2.0" - "@aws-sdk/hash-node" "3.1.0" - "@aws-sdk/invalid-dependency" "3.2.0" - "@aws-sdk/middleware-content-length" "3.2.0" - "@aws-sdk/middleware-host-header" "3.2.0" - "@aws-sdk/middleware-logger" "3.2.0" - "@aws-sdk/middleware-retry" "3.2.0" - "@aws-sdk/middleware-serde" "3.2.0" - "@aws-sdk/middleware-signing" "3.2.0" - "@aws-sdk/middleware-stack" "3.1.0" - "@aws-sdk/middleware-user-agent" "3.2.0" - "@aws-sdk/node-config-provider" "3.1.0" - "@aws-sdk/node-http-handler" "3.2.0" - "@aws-sdk/protocol-http" "3.2.0" - "@aws-sdk/smithy-client" "3.2.0" - "@aws-sdk/url-parser-browser" "3.1.0" - "@aws-sdk/url-parser-node" "3.1.0" - "@aws-sdk/util-base64-browser" "3.1.0" - "@aws-sdk/util-base64-node" "3.1.0" - "@aws-sdk/util-body-length-browser" "3.1.0" - "@aws-sdk/util-body-length-node" "3.1.0" - "@aws-sdk/util-user-agent-browser" "3.2.0" - "@aws-sdk/util-user-agent-node" "3.2.0" - "@aws-sdk/util-utf8-browser" "3.1.0" - "@aws-sdk/util-utf8-node" "3.1.0" - tslib "^2.0.0" - -"@aws-sdk/client-s3@^3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1.0.tgz#9c39f4588420696e1a4920f8315f778a7896b858" - integrity sha512-RX/eBi3mHoJGkS146tA4ScSdOwSLEkHWlETFkcqVYdH6IP5FT9CiwRqSf6aNRE+LQiZXoBxeOdvGFXg2jWg83w== - dependencies: - "@aws-crypto/sha256-browser" "^1.0.0" - "@aws-crypto/sha256-js" "^1.0.0" - "@aws-sdk/config-resolver" "3.1.0" - "@aws-sdk/credential-provider-node" "3.1.0" - "@aws-sdk/eventstream-serde-browser" "3.1.0" - "@aws-sdk/eventstream-serde-config-resolver" "3.1.0" - "@aws-sdk/eventstream-serde-node" "3.1.0" - "@aws-sdk/fetch-http-handler" "3.1.0" - "@aws-sdk/hash-blob-browser" "3.1.0" - "@aws-sdk/hash-node" "3.1.0" - "@aws-sdk/hash-stream-node" "3.1.0" - "@aws-sdk/invalid-dependency" "3.1.0" - "@aws-sdk/md5-js" "3.1.0" - "@aws-sdk/middleware-apply-body-checksum" "3.1.0" - "@aws-sdk/middleware-bucket-endpoint" "3.1.0" - "@aws-sdk/middleware-content-length" "3.1.0" - "@aws-sdk/middleware-expect-continue" "3.1.0" - "@aws-sdk/middleware-host-header" "3.1.0" - "@aws-sdk/middleware-location-constraint" "3.1.0" - "@aws-sdk/middleware-logger" "3.1.0" - "@aws-sdk/middleware-retry" "3.1.0" - "@aws-sdk/middleware-sdk-s3" "3.1.0" - "@aws-sdk/middleware-serde" "3.1.0" - "@aws-sdk/middleware-signing" "3.1.0" - "@aws-sdk/middleware-ssec" "3.1.0" - "@aws-sdk/middleware-stack" "3.1.0" - "@aws-sdk/middleware-user-agent" "3.1.0" - "@aws-sdk/node-config-provider" "3.1.0" - "@aws-sdk/node-http-handler" "3.1.0" - "@aws-sdk/protocol-http" "3.1.0" - "@aws-sdk/smithy-client" "3.1.0" - "@aws-sdk/url-parser-browser" "3.1.0" - "@aws-sdk/url-parser-node" "3.1.0" - "@aws-sdk/util-base64-browser" "3.1.0" - "@aws-sdk/util-base64-node" "3.1.0" - "@aws-sdk/util-body-length-browser" "3.1.0" - "@aws-sdk/util-body-length-node" "3.1.0" - "@aws-sdk/util-user-agent-browser" "3.1.0" - "@aws-sdk/util-user-agent-node" "3.1.0" - "@aws-sdk/util-utf8-browser" "3.1.0" - "@aws-sdk/util-utf8-node" "3.1.0" - "@aws-sdk/util-waiter" "3.1.0" - "@aws-sdk/xml-builder" "3.1.0" - fast-xml-parser "^3.16.0" - tslib "^2.0.0" - -"@aws-sdk/config-resolver@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.1.0.tgz#36987002c18884847aa1c96e0daf546b5f9caff6" - integrity sha512-/8hNlmeUPd1Ey9WqaZsydjWpDh1rcZpzOiiIhszOj3gGhUhsGsYGqP5oCwR9vMJf0Z5+o9diGT1QLMy9Rno6pw== - dependencies: - "@aws-sdk/signature-v4" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/config-resolver@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.2.0.tgz#c83de904f155282c91e0972b5053937ea1f05160" - integrity sha512-+PRjXpWq8JlN/ilO2F5sh/HAOkoCJkGnu/e8rfnND88tghVVauFPm7xTR2LHOGp1ugpUJm8lG/iEzpYP1EUcpA== - dependencies: - "@aws-sdk/signature-v4" "3.2.0" - tslib "^1.8.0" - -"@aws-sdk/credential-provider-env@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.1.0.tgz#eaa3ed04960faafd4536e9fbb051d5007985ba03" - integrity sha512-WUSrtg/on6bP6rjOPdjhFMkPZgDoFZZL2FST4y9K6TkRxcDHejgPZmP13L74RBGIOYlcflZAxE/936OpzpsAAA== - dependencies: - "@aws-sdk/property-provider" "3.1.0" - tslib "^1.8.0" - "@aws-sdk/credential-provider-env@3.3.0": version "3.3.0" resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.3.0.tgz#7930e504a7a79ab98a9fd34befc5c84b8c4df679" @@ -287,14 +111,6 @@ "@aws-sdk/types" "3.1.0" tslib "^1.8.0" -"@aws-sdk/credential-provider-imds@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.1.0.tgz#33d48753dc00bddce79d2aa8076a7cb5bf8562df" - integrity sha512-I8P0ASEjuYpcDMfU2QKlT8vE3nIo803ct4y5Q54Osh5K7+H8Y+raYK9mxxhjycriDXhcQrOe2Rfj+1ARXjdvGw== - dependencies: - "@aws-sdk/property-provider" "3.1.0" - tslib "^1.8.0" - "@aws-sdk/credential-provider-imds@3.3.0": version "3.3.0" resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.3.0.tgz#ff0cf5489853c16d23fc99d7bae425587e836c40" @@ -304,15 +120,6 @@ "@aws-sdk/types" "3.1.0" tslib "^1.8.0" -"@aws-sdk/credential-provider-ini@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.1.0.tgz#1a6cf9ab9fa1450d4472b9e371099b0c0283349b" - integrity sha512-6kPFcsnCR1tEaVQPJGY6z58XvbeVtjsdC2srYG/0y87zyWd9awGuvRN4OV6aOr24QmEVaFNyWuE5QBQJ1/7hkA== - dependencies: - "@aws-sdk/property-provider" "3.1.0" - "@aws-sdk/shared-ini-file-loader" "3.1.0" - tslib "^1.8.0" - "@aws-sdk/credential-provider-ini@3.3.0": version "3.3.0" resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.3.0.tgz#55fe8f391b72d30e650ba8bc680e82bbeacbbfe5" @@ -323,18 +130,6 @@ "@aws-sdk/types" "3.1.0" tslib "^1.8.0" -"@aws-sdk/credential-provider-node@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.1.0.tgz#89bc8803752a3e580c6f2410306c7edad6be7fa2" - integrity sha512-/HNq75cNiXzxm6F+ZeJ3awuqUofUtcr89ZnSnwB1pkicz81yrqAH8EN2dEj90eDo1MdBQHx6rhaKg4SJUaBy3Q== - dependencies: - "@aws-sdk/credential-provider-env" "3.1.0" - "@aws-sdk/credential-provider-imds" "3.1.0" - "@aws-sdk/credential-provider-ini" "3.1.0" - "@aws-sdk/credential-provider-process" "3.1.0" - "@aws-sdk/property-provider" "3.1.0" - tslib "^1.8.0" - "@aws-sdk/credential-provider-node@^3.3.0": version "3.3.0" resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.3.0.tgz#5c97323fa7b23590070d06aa7b1be8d93b2bf4be" @@ -348,16 +143,6 @@ "@aws-sdk/types" "3.1.0" tslib "^1.8.0" -"@aws-sdk/credential-provider-process@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.1.0.tgz#ff817b29a9760c463b77be3ce49375eaeb753ef3" - integrity sha512-pecJk5W9LYLk9KOwT5A3X6ECHoakQMvXL/whqDsKdXQ4c5C1pVoV67Jnp3ilMNfr6CauxU8gQs53iPS5LUHH4A== - dependencies: - "@aws-sdk/credential-provider-ini" "3.1.0" - "@aws-sdk/property-provider" "3.1.0" - "@aws-sdk/shared-ini-file-loader" "3.1.0" - tslib "^1.8.0" - "@aws-sdk/credential-provider-process@3.3.0": version "3.3.0" resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.3.0.tgz#9de0984bd6dd0f5e40cff3672d7dd19e8cd43074" @@ -369,338 +154,6 @@ "@aws-sdk/types" "3.1.0" tslib "^1.8.0" -"@aws-sdk/eventstream-marshaller@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/eventstream-marshaller/-/eventstream-marshaller-3.1.0.tgz#65a217e37abcaa162276ccb1d4487d42431d1534" - integrity sha512-ZfWK+QPB+nuKfd90ZWpkJtK4wSKYv5qzA59jP1wwEWZ2XzUyTs2FWq4rns4af2UHnd+r5+92V2r/AZUCzT9U8w== - dependencies: - "@aws-crypto/crc32" "^1.0.0" - "@aws-sdk/util-hex-encoding" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/eventstream-serde-browser@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/eventstream-serde-browser/-/eventstream-serde-browser-3.1.0.tgz#e62f745ecb5336a957fe645f6d596501051ab73a" - integrity sha512-a7P8JzJhHTFAc6sY2uBVMr8RpLap58LXOgWUEQmozFOK6Klu2NxsdzfNGJAURsc1tK6cw126fidA9UMH/NR20Q== - dependencies: - "@aws-sdk/eventstream-marshaller" "3.1.0" - "@aws-sdk/eventstream-serde-universal" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/eventstream-serde-config-resolver@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.1.0.tgz#f4f95c4770ea34b06d79ef61eb8cbd73ac8403bc" - integrity sha512-OxgQNKNKOuXAy4ID77EC7glHALGOGdDt+fDteeajEEZ+XXQ2q4maIAbQ8N44bZcy4E8D20YEesaHDAtsDiUvPA== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/eventstream-serde-node@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/eventstream-serde-node/-/eventstream-serde-node-3.1.0.tgz#23d6630385d7023d3da9c905174ee391b5d9e5d6" - integrity sha512-xn3SwHhi9UDi6gE9fQxD8j+fxRzfnMWiu688sneoGSSzZl4e5lZtrRbMGWKdtT840dp+GrEXE6BVobUelOyVHA== - dependencies: - "@aws-sdk/eventstream-marshaller" "3.1.0" - "@aws-sdk/eventstream-serde-universal" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/eventstream-serde-universal@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/eventstream-serde-universal/-/eventstream-serde-universal-3.1.0.tgz#c48f51c92711cf0ec3a8e93b466d4dde8128d936" - integrity sha512-dnrsnLqBnFddxLtFGzDE9SBNl7+xJmgsL7SI9ALqxgvJkK7MVVj9+kzCEyypNHcRCG6OUTypvY+wjgMXJAtHUg== - dependencies: - "@aws-sdk/eventstream-marshaller" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/fetch-http-handler@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.1.0.tgz#2e495cbd5a633c3a5f9935b59faa9c8c0ed8b5e2" - integrity sha512-QCYGCdQuV3XLgqvKoJD3C/GO+cEZQOazII/rPfaeVXquv8Xrn85UJ3PZ3uFCzVVmjnm1RidwseRQoYrSsjmRhQ== - dependencies: - "@aws-sdk/protocol-http" "3.1.0" - "@aws-sdk/querystring-builder" "3.1.0" - "@aws-sdk/util-base64-browser" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/fetch-http-handler@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.2.0.tgz#116bfc06b3de797d4c746962874ef00e66fda024" - integrity sha512-Y8cw7+HNhxSpYZmd78Bq46rYUUCqnRH9AgHXkLifsigt4RsEAfYhODhEUQhEhJ/zwgadvj/fIKJWKdSiCbiC1A== - dependencies: - "@aws-sdk/protocol-http" "3.2.0" - "@aws-sdk/querystring-builder" "3.1.0" - "@aws-sdk/util-base64-browser" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/hash-blob-browser@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/hash-blob-browser/-/hash-blob-browser-3.1.0.tgz#5c6548b05d155d3141b47da958be13043a96a17f" - integrity sha512-A/iLk2P/15G//YPlB9DwNPR3J3QlrmJSrHKKYLj/t1DnxiIu3DZNCZjhLmBuQpx14O1GcolmewqJRaAq1qtVcw== - dependencies: - "@aws-sdk/chunked-blob-reader" "3.1.0" - "@aws-sdk/chunked-blob-reader-native" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/hash-node@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/hash-node/-/hash-node-3.1.0.tgz#2d9eb6c0a5b65c179d2e60db941ba921a83e1861" - integrity sha512-zcXjzPuraq6EEEC0N2DI/ng554XiHtLsWGYK2TkW5yS6+2xXiJFny3V5wZHWXsidHOgwgML92r/pU0PeVcTCbg== - dependencies: - "@aws-sdk/util-buffer-from" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/hash-stream-node@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/hash-stream-node/-/hash-stream-node-3.1.0.tgz#05424ef3873be626091b55617fe1df135ee34dcd" - integrity sha512-GQ7EsdH9yIYIVPhtcuwpYaTS3MNq+3jo6mUF06m8YxlG58GNIVz9gG8oMy9ezP5rpIjJX9YhXKN8GgERNxfpvQ== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/invalid-dependency@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/invalid-dependency/-/invalid-dependency-3.1.0.tgz#834c4e4a34d481bd3d4467437441ab035634710d" - integrity sha512-PNKFfIC9w25MA/fV5FnjFfFpbNiLaojgS2RtT/+L5pyic2NJ3spGNlF0pMrnPPtbrFFHaOy/E7bRa0qVtNWEyw== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/invalid-dependency@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/invalid-dependency/-/invalid-dependency-3.2.0.tgz#2c2c19cbec17ff28333c68e28420db6902a4374b" - integrity sha512-8hlqE4pWo2DoM6pAqjL+A3zYwUMaxP/RCGO+0zKbZHVw2stdRDzpO+jUfzZHR+mPJ+L/2wPwhK8D9n3yBriNBQ== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/is-array-buffer@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/is-array-buffer/-/is-array-buffer-3.1.0.tgz#7ac296d6408e34083ac007630541a1cdf67387fd" - integrity sha512-wE6Am+/FKuINc/aypXiBiLAatlSyxYQ9wGGQHf2iYOX5d5bHLOVKPoRwcqSCaiaR32aRcS7R+IhgxeBy+ajsMQ== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/md5-js@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/md5-js/-/md5-js-3.1.0.tgz#ae0799f9d9eeab01f4dd72baaa6bbb63cd45ffed" - integrity sha512-QB8D7EdxjOih9NWL/NoJRkgE0RSSJTXQLuQGH9UMj66rUQfGpwvLJDL+PbvoSUJgM8+McEBGyvjLCrDCK5r+qA== - dependencies: - "@aws-sdk/util-utf8-browser" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/middleware-apply-body-checksum@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-apply-body-checksum/-/middleware-apply-body-checksum-3.1.0.tgz#85504e7e7883830ffa06954a07f57b1fdb003408" - integrity sha512-q1Jx8HhC7fQjfrCUjbdlRw5F0Vr+gwjHf4sKd+S+FYO+15+PVtukBiKfP3lEW4KKREjTv1h/qPEjhv+CK4nN7w== - dependencies: - "@aws-sdk/is-array-buffer" "3.1.0" - "@aws-sdk/protocol-http" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/middleware-bucket-endpoint@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.1.0.tgz#38fb980fe99c42ddc264db40b93f28f84b3cc1e2" - integrity sha512-/NZuSbqSNARgsZqrRZ8yDXGqF0MSdA/vhNwXAk8BVz/oav4fveJ4KiMMO7Pq5918ZWJ3v//4bSezTnKLDzKmAw== - dependencies: - "@aws-sdk/protocol-http" "3.1.0" - "@aws-sdk/util-arn-parser" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/middleware-content-length@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-content-length/-/middleware-content-length-3.1.0.tgz#af100e6f724a188bb2963372de8d40868905be51" - integrity sha512-+QGIYkAch0q3a8LrbCS21MEKlE0WgtZBOJl76M3LZtvnbvgKkAmL4oDc+HjGD1jYuNRSt+hQZgqofuBDe8fyyw== - dependencies: - "@aws-sdk/protocol-http" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/middleware-content-length@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-content-length/-/middleware-content-length-3.2.0.tgz#876943c7498e930595621395e5928fc59cf8ed68" - integrity sha512-xS7zdcRcubv7WKs5L/RG69EuuatuPxYNQT4O9epQyUaOpTHKzdVd1wnBK+otWYxI9qnCjIXwwmMyGbJXD9OHhw== - dependencies: - "@aws-sdk/protocol-http" "3.2.0" - tslib "^1.8.0" - -"@aws-sdk/middleware-expect-continue@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.1.0.tgz#3453aeb345ea852ebeefe392f7706b7573dd3c23" - integrity sha512-NCuF3BMf5QqXv4iYr4eGnmg7BfIpFjPvjxapB5cQbzUOXIyUtlYWUu7QrsdBxCsqgOhVJ4mAiqliVsdxML5yZA== - dependencies: - "@aws-sdk/middleware-header-default" "3.1.0" - "@aws-sdk/protocol-http" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/middleware-header-default@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-header-default/-/middleware-header-default-3.1.0.tgz#0b41d113f878d95132e10a48ca4c9667c28cf325" - integrity sha512-BJFQXxkkuIkN679bqqTo9hBUuTFtO4izzSY3WPnnYYcQwYTIH/4JSifCpJWw+AjbvTZ2rA2DrWaTNGz3zuiBGA== - dependencies: - "@aws-sdk/protocol-http" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/middleware-host-header@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.1.0.tgz#61f22f7d7ce8907d0dbc817ff67ec5545012dc51" - integrity sha512-x86fAcqhtK6ObgPJHFLRaZP+cC4gI185QwxqP72balLgIsJJ6InS7441sOMPr5AZ9xgLJyPPKXouQ5zFBYjwpw== - dependencies: - "@aws-sdk/protocol-http" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/middleware-host-header@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.2.0.tgz#cf5d0868f939834fb45c4490c1118eaf25317505" - integrity sha512-URRRaqO74DSdhqYyKbyCyadJ9No17KBMmgwz2OKecdwp6/pUVNQmlxhj1cG9leWUkBuOHo6weRvWc5zXwvfy1Q== - dependencies: - "@aws-sdk/protocol-http" "3.2.0" - tslib "^1.8.0" - -"@aws-sdk/middleware-location-constraint@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.1.0.tgz#181007815ca9167b333666eb073b131e17d86fe7" - integrity sha512-NR8us/ZVJlxg/10h+dyu6xnKofSBWTp+Ai4yvkEFBsxr9kruVqGMP/2IFXl6iZAY5uy/H+TUJzyYtd4/DUgNgg== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/middleware-logger@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.1.0.tgz#6356f6d79524493a0485bfd86f621d7a882daa1a" - integrity sha512-RYJbms7ECg1FgYmN/IyK9U9nzWZtUmt2ZPBunUqvab/ldjaXpAtJq3IYdl1E/rgbc0LSTRDjyGC4erMDOT8IJg== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/middleware-logger@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.2.0.tgz#9d4bf84fbf7a0ddc144e6c660f1b806a23a42a6e" - integrity sha512-vkex6bsYqafdvrLe4MXJDJrEfvRb5Z8z01lVlaEV2bzXFqw7WNuFVDRC80GE/YB+r2ds/n9QU4EYUSX9wewoJQ== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/middleware-retry@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-retry/-/middleware-retry-3.1.0.tgz#4df049c73121a83e19f5078e90a411b93a8cf7de" - integrity sha512-jNewsmLhuSHCtwpuQlsOuQ/Cig7BE4BIF7cfpWaczqYaG1F/rdTlAmb69ugg5pVby7ccj2jgA4aw+achjuzqSw== - dependencies: - "@aws-sdk/protocol-http" "3.1.0" - "@aws-sdk/service-error-classification" "3.1.0" - react-native-get-random-values "^1.4.0" - tslib "^1.8.0" - uuid "^3.0.0" - -"@aws-sdk/middleware-retry@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-retry/-/middleware-retry-3.2.0.tgz#72bf988d90d9b0192aa7b290fc340b9f2e57a193" - integrity sha512-roecjUD9di1kqyCewk8pGXcsiHyi8kte5Is5bo9kCPfqHhtDN9L/efOhC+rhT0N9nKplC4rZGE4aopxNhMbLtQ== - dependencies: - "@aws-sdk/protocol-http" "3.2.0" - "@aws-sdk/service-error-classification" "3.2.0" - react-native-get-random-values "^1.4.0" - tslib "^1.8.0" - uuid "^3.0.0" - -"@aws-sdk/middleware-sdk-s3@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.1.0.tgz#e602bde5b9b5abe33e9087f61cf0f0cae9157bad" - integrity sha512-qH2RBt9EdDIO/2PGl7/QyIt9C4WuY6kVfx/S1Bh26wSY9s11VU73CDanZtgNQwIiX20v8DaPlg5dp2hse7Nr7w== - dependencies: - "@aws-sdk/protocol-http" "3.1.0" - "@aws-sdk/util-arn-parser" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/middleware-serde@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-serde/-/middleware-serde-3.1.0.tgz#a80b96dc0008546d9e9e9bce7e9b1ec134edbc6d" - integrity sha512-vXdYlzeBoJCqe+xhlFE9J62EyREvU1MVC6p3m3QPtfXuYlQwRb8WhlFFDFrDdA7V2usBjx5aM67OZnapplyUfg== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/middleware-serde@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-serde/-/middleware-serde-3.2.0.tgz#aff46b0457f44738cdc542ea4bea01160d437300" - integrity sha512-dWTL5lIjosKCUDyaNtEolaGpo/KwoM1Zx4NdMUBLYBy9v0vKSnpRLPXxyM2EREVJYHTVrvm6UoW8dM+5RFiN7A== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/middleware-signing@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.1.0.tgz#d48c18f159f867ca5efe34c4993880e6820cc11e" - integrity sha512-3zNsonJ/nkBL8gUWr2v1vQ/NKIQQgSjmrxIa4xUxRcqOOO1qgZgjablxYHQlX07B6defhaOS2/p/EDmF+LfPqQ== - dependencies: - "@aws-sdk/protocol-http" "3.1.0" - "@aws-sdk/signature-v4" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/middleware-signing@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.2.0.tgz#2ad5288d6543281a7e704102f6d446acdb9efaaa" - integrity sha512-XkDu0lMkdlf95nE2aYyAFmXOzBD+noVebyB1dlJDQCZWNzGkefc+8Nez3lCgyd2+KuEyHXMmQsqZwnmygpRvLQ== - dependencies: - "@aws-sdk/protocol-http" "3.2.0" - "@aws-sdk/signature-v4" "3.2.0" - tslib "^1.8.0" - -"@aws-sdk/middleware-ssec@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.1.0.tgz#af57795facc29673568277d1d9aa090e57d69669" - integrity sha512-RGHWxNEMyYHv3et23XjMVS+9HhJIPR+oO21h82Eur4WhA6u3EuiZ+TKuoXZ9DA5V2wVJvPg33NHi0/uu1mah5w== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/middleware-stack@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-stack/-/middleware-stack-3.1.0.tgz#31a69784ff31d70e1176d948f94294563b23b36e" - integrity sha512-lin0C0xPspT/orPMWWHMYG/7Z128NsSj6Khs4G6TH+2rIixXxQtHLen8H2dSPNIYXnLaxvtUDl5VuqjRt+s2Ow== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/middleware-user-agent@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.1.0.tgz#b35ce7fae8f672aa9922bc28082137fa8c2f0486" - integrity sha512-ghUD5ZhGdvPEctacO+INnQpdpQUV8N9hC9We7ftAE75PGBG/aB8qnVWYV8uKAxx8ruNAi+iU4j+4B4qLFNMx2A== - dependencies: - "@aws-sdk/protocol-http" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/middleware-user-agent@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.2.0.tgz#19e64961feb6a41721e3164538031e21858ad7ee" - integrity sha512-rME49AFlVZSwgxTLQo/5Ok012HRcg9xhSFK4oOEkPJiYyGedvsF+Qy6sz3VjQRL2SvUvoAp03Wqbxe/Keewdbw== - dependencies: - "@aws-sdk/protocol-http" "3.2.0" - tslib "^1.8.0" - -"@aws-sdk/node-config-provider@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/node-config-provider/-/node-config-provider-3.1.0.tgz#74b37f65faa90fa6b4acb640e0f215f50aaa4904" - integrity sha512-zbV5Va40elco6IBddbDmhMQDWazQxHAk4trWFj3MrH36rpWZzWwsRbgjgGv0CcvSy5zP6PLHjdcBPeHS/KmViQ== - dependencies: - "@aws-sdk/property-provider" "3.1.0" - "@aws-sdk/shared-ini-file-loader" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/node-http-handler@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/node-http-handler/-/node-http-handler-3.1.0.tgz#b7fdbc3a1928127b2eb2064177502e11324f7bb8" - integrity sha512-T1yLundJ4tPeIt9IaOk24kBNBCe54UhCXXrh/0SSZZ26DLINPV5mF22wZKxgMpvKTbtJ6vlag+DhJe/ldqJ2qQ== - dependencies: - "@aws-sdk/abort-controller" "3.1.0" - "@aws-sdk/protocol-http" "3.1.0" - "@aws-sdk/querystring-builder" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/node-http-handler@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/node-http-handler/-/node-http-handler-3.2.0.tgz#cf6b12b021d61933202522179e5bd5710e565afa" - integrity sha512-C6bwM9RNINF2r7wTjQ7esjwlTnEIUZkJ+hyMGwOUxc4AqHCdc1PGEFwuMjQWhiyUtvR3hd7hfPj5GWvFP/PUTQ== - dependencies: - "@aws-sdk/abort-controller" "3.1.0" - "@aws-sdk/protocol-http" "3.2.0" - "@aws-sdk/querystring-builder" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/property-provider@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/property-provider/-/property-provider-3.1.0.tgz#19cf1da802a09be429e26f8eafec9b4c3473f8b0" - integrity sha512-NDCI7T8ytbzlXW8axDdtjMhJYRMVbOoPupM+HwR81hT7QoWWNrpCrjFqIv/gJet+ms5i4k80CWrBJd0L0kNl/Q== - dependencies: - tslib "^1.8.0" - "@aws-sdk/property-provider@3.3.0": version "3.3.0" resolved "https://registry.npmjs.org/@aws-sdk/property-provider/-/property-provider-3.3.0.tgz#49979cb1a3e5562d51807c7403c5fd48cb9f2cdc" @@ -709,45 +162,6 @@ "@aws-sdk/types" "3.1.0" tslib "^1.8.0" -"@aws-sdk/protocol-http@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-3.1.0.tgz#7da0ebcf02a40a8300f3bd52f9206f25fdf1ca7f" - integrity sha512-61qInY/AESslV6ZYTAgwoB172K/H+5EiXeWnmWExOGH3vkfkkxQBYCTcATdtasP6QYTfYiePhyjJ8eUyQL3C0w== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/protocol-http@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-3.2.0.tgz#0d352045bae38c66a2c97611983bfb545351c4e0" - integrity sha512-7FA23ABTZ18MsQD0GcICj6dU9HRInf6l6XvtnlPUS6ZvjDbsNRqEROUtvjl8dkDDP1mFpEQuHJ5e9MO5peEm7g== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/querystring-builder@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/querystring-builder/-/querystring-builder-3.1.0.tgz#6cb859cfd906faa21de32641a960f080be78f0f3" - integrity sha512-rfVJWrAT+PJ5Dx0arxK5RBMfETezAjKGHxXWf7/9rcxSqoEz8s3tYhZ7/CqcOQqN89X6gJ4veQwGprraIhOZIg== - dependencies: - "@aws-sdk/util-uri-escape" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/querystring-parser@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/querystring-parser/-/querystring-parser-3.1.0.tgz#eec9fcbfc1c907e19909b4436f93a6c9fb063041" - integrity sha512-tqwHKLKplyv940+cR8uW6w2K3IEcGOskJ/8oP1R3RSyyidONSVEShNlpApTQlccfYriieDuEDLVCo4pT7S55kw== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/service-error-classification@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/service-error-classification/-/service-error-classification-3.1.0.tgz#9471f761f36ebe3efeb0791d7e2af517f87ef4a2" - integrity sha512-zUNV9Fyguto8VOhinKvzIoQxwfYMSSLO6xTKJLyTB+cDv51SX5sh+lqX6IKGhuqz2Wse1ynuoLGOnmVoe4aUpQ== - -"@aws-sdk/service-error-classification@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/service-error-classification/-/service-error-classification-3.2.0.tgz#286ba27368276132db89f21b79724bf95cbd8d97" - integrity sha512-ZzeSZ0HnJq5zUkA466zVEZAE1SVqYuvGzxRGTEszbMW2eWPLonJgUwiT5nDScmJQcDju044iP//2KnBvn62Rhg== - "@aws-sdk/shared-ini-file-loader@3.1.0": version "3.1.0" resolved "https://registry.npmjs.org/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.1.0.tgz#b1bc7ed4e16535f20c788915060a121e457efdfb" @@ -755,205 +169,11 @@ dependencies: tslib "^1.8.0" -"@aws-sdk/signature-v4@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/signature-v4/-/signature-v4-3.1.0.tgz#b5d480af74629e86163ffe86a3f13a37f1f1b250" - integrity sha512-TFbe/Xf02a4baU1tg/S/w1A5AKaD7YHuqQg4Ak+HbRvAFe77/C/rbqWGnl66BDcukMvx13Ywd5ZkraAVSaTlzg== - dependencies: - "@aws-sdk/is-array-buffer" "3.1.0" - "@aws-sdk/util-hex-encoding" "3.1.0" - "@aws-sdk/util-uri-escape" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/signature-v4@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/signature-v4/-/signature-v4-3.2.0.tgz#9eb5c4c3f5c3d448f3929f310baa4ad3682d1ddc" - integrity sha512-Fb81gqaSnuCwO9HFYnztksyFud5X6/Ikr95X3pfrtLcHlHe/S079woWmlDUEgy7lw2X2GY94TcX9jlXuMhTfhA== - dependencies: - "@aws-sdk/is-array-buffer" "3.1.0" - "@aws-sdk/util-hex-encoding" "3.1.0" - "@aws-sdk/util-uri-escape" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/smithy-client@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-3.1.0.tgz#dc60223b9af1e99d076871e8138ea9b6ecbf737b" - integrity sha512-l/xCyQ0xLvIX/VZDYC3CGAphXtV7/t1gqfxfATbx1TD3E23PS0XvaV8JAmMWF+Q+rShk+otuYpdaRjGrygTK5w== - dependencies: - "@aws-sdk/middleware-stack" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/smithy-client@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-3.2.0.tgz#ba93d125f83f385ce6a4600e47339bf036fc7d0a" - integrity sha512-LpBGQ5m/oQ6QkMPQl7yxvBll/NRww4GUCEwpEXw412SUbX7aVTu+CNoUmF2lg8Gwp2XM3NgOkJ9DQ94YwZ/dug== - dependencies: - "@aws-sdk/middleware-stack" "3.1.0" - tslib "^1.8.0" - "@aws-sdk/types@3.1.0": version "3.1.0" resolved "https://registry.npmjs.org/@aws-sdk/types/-/types-3.1.0.tgz#04d77c37a80b422e8123f296338d129e51f3e1fc" integrity sha512-4Az7cemXCN4Qp8EheNkZTJJqIG0dvCT2KAreJLoclcVTcEFw2rzlATUnSeia1YTRsVd6aNxD001Ug7f3vYcQkw== -"@aws-sdk/types@^1.0.0-rc.1": - version "1.0.0-rc.10" - resolved "https://registry.npmjs.org/@aws-sdk/types/-/types-1.0.0-rc.10.tgz#729127fbfac5da1a3368ffe6ec2e90acc9ad69c3" - integrity sha512-9gwhYnkTNuYZ+etCtM4T8gjpZ0SWSXbzQxY34UjSS+dt3C/UnbX0J22tMahp/9Z1yCa9pihtXrkD+nO2xn7nVQ== - -"@aws-sdk/url-parser-browser@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/url-parser-browser/-/url-parser-browser-3.1.0.tgz#8e657d9e6cbde454df3b6e597e4fa4b690fe47af" - integrity sha512-XtiorfIxhbJuU1TLp7rL7qMzNY/+9FYisFtCFcrcy7/fsvXUTjjB2u3O4J01m/T6/HVmenV+SFQvs+fYC9U7dA== - dependencies: - "@aws-sdk/querystring-parser" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/url-parser-node@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/url-parser-node/-/url-parser-node-3.1.0.tgz#f49690405195f80157997fd48f99e1e036de3a3f" - integrity sha512-pDQekV4RK2hoiDB4YkMi00oTgU8MoHg+Sve9HaLYCtTuirHMVBswxzGg6MVDWo0tM6WSFbnQRXNZtioUo7ExVA== - dependencies: - "@aws-sdk/querystring-parser" "3.1.0" - tslib "^1.8.0" - url "^0.11.0" - -"@aws-sdk/util-arn-parser@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.1.0.tgz#56c462b2ef6d7cfa098e1a22e44fb3b52da0718c" - integrity sha512-xXL/nadq5mqEw6Mrv1ghoODuyWWsAxvr+rRNgDJOav6mypgEOiLb0ybkqinrH1ogTkAYbegs+uaWxgSPBe9ZSA== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/util-base64-browser@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-base64-browser/-/util-base64-browser-3.1.0.tgz#756253a3fc1ad58c38ad28ed664b701d850d3aa9" - integrity sha512-xkodj0VnkHl1gdYI9Nl4E2Ed+atM3xBTNaedoGnmqoyosMjPRJCpU8uFBmdiF4e+GGPsXlYe9oA/hLyJFxmeSQ== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/util-base64-node@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-base64-node/-/util-base64-node-3.1.0.tgz#027a9e854d204adda56d5f43f8ef4a20532b7ac9" - integrity sha512-FEtnINw2MeD3LAtyGcofah5D8j6OjpmwNKibr7mIgosRO++iVyXe2xa6iOoptZFn5pIU0C4fkJn5o+kjBhRafA== - dependencies: - "@aws-sdk/util-buffer-from" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/util-body-length-browser@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.1.0.tgz#b68018860cd5d63c9327c7a42323c5c75cf514bf" - integrity sha512-vzKDD/p1gcA05jeLmn6+6HdOY4G6Axyp6dj1R1nVeFpPPx6KkFsNGL9/CoaRT2TGv1fHBoDXsve9JRaCxrER4Q== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/util-body-length-node@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-body-length-node/-/util-body-length-node-3.1.0.tgz#c280223066f0ce8fadb002a668a5f5e6ee4de12b" - integrity sha512-MfJoU2wFWkOmbjWDepq5bDGYZlpvtBi2Vs8ZeTcm/4+q+3L9tJ/Zb/Ofx5oeRg9VhCsAjvceQTdX+CAyP8byXA== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/util-buffer-from@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-buffer-from/-/util-buffer-from-3.1.0.tgz#a68b2e191f3737bcd85c33af5a6b0e3e9f974f0e" - integrity sha512-UeC4VKmWYgTXjNdLVHfurrdhznnoxWLUFx8xspyRd58BhSZ5vc5HiiKTPX/CGxzAP/qZG668PaoOJucwmEam4g== - dependencies: - "@aws-sdk/is-array-buffer" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/util-hex-encoding@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.1.0.tgz#937440d60333c1b3e4fbe06012dfdc65c9e297bc" - integrity sha512-MPOsUY3USCUBaqZ3ifgE9il/liVxEKsz6dYQ08pdtWRzZx2CT7kWslQeNAT565pMvktnvdLjfzBw2FwnSI6nqg== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/util-locate-window@^1.0.0-rc.1": - version "1.0.0-rc.8" - resolved "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-1.0.0-rc.8.tgz#d28175aeb9c8ad3940242e615b1503632d3be33d" - integrity sha512-TvqeA4fgmZ0A0x3K+qVj/OSWEFHGZjzpVuyXlm1EYOf7NQ9VWRlokEn1MYKuL+t7al9ZeQyi16D8Dn7DW1eidw== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/util-uri-escape@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-uri-escape/-/util-uri-escape-3.1.0.tgz#1e4450c8e047b542f87172407e2fe0ade7c55227" - integrity sha512-1ZcXVJpsA6uW3tDTQI+Rpawqh76fyHpFc55ST8VGyMgmCzlJzBpYG0ck1kqVRSUP7YyvkJQvHfcm+U6doL5Xkw== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/util-user-agent-browser@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.1.0.tgz#471dec78305f39ac9a05968970917b5c66e4312b" - integrity sha512-w31WYjRTXbBn9z/sRy/4IyjIyEeTOon1JvRzlKDbEWm4JXarVbiJcZKs1U3q+9fS9la5uLM1NEQCsgdI1iYERA== - dependencies: - "@aws-sdk/types" "3.1.0" - bowser "^2.11.0" - tslib "^1.8.0" - -"@aws-sdk/util-user-agent-browser@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.2.0.tgz#f6fdee45817d80a637d7bd66985f516182b6f550" - integrity sha512-ugkLsntS2wpLWCjPs49wnAGhuIAFyHYzTznp0v+Qj/uOkm0ddq4kWp+FwtMxeQMRY0eoBdfqXJAbJLz8w4xlUQ== - dependencies: - bowser "^2.11.0" - tslib "^1.8.0" - -"@aws-sdk/util-user-agent-node@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.1.0.tgz#3fa66564a5fdb0e2851554688dc507127356f941" - integrity sha512-VyyZUR4vrHyT4aLH+ufoaTxFy65K9OtoCEdH1X748HinLZY9JObCCO6lZVNr5b26fGJrbqzoAqaXWgHkmEUpoQ== - dependencies: - "@aws-sdk/node-config-provider" "3.1.0" - "@aws-sdk/types" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/util-user-agent-node@3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.2.0.tgz#9190f69afdd81bacde16e253af2545a0e3f443cd" - integrity sha512-Ovyq6yc7SL0NWZWBrPKRc0D38xkMH2ulslcnukdNOitv0s8V2Ge09R4T63U0I8IJ7nrKa75Uus6A8vC8Zko3yg== - dependencies: - "@aws-sdk/node-config-provider" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/util-utf8-browser@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.1.0.tgz#7be17b545af101c320d34aace47139cf9987d796" - integrity sha512-vJP20me+Wc1RJHq+Y+gFD25aWhbQte+Qkyh3SOKQ+YvNaMcaeVwOV7b3Y3ItBuMdutHLJWmbJ2wF6dhhpy1kOA== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/util-utf8-browser@^1.0.0-rc.1": - version "1.0.0-rc.8" - resolved "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-1.0.0-rc.8.tgz#bf1f1cfed8c024f43a7c43b643fdf2b4523b5973" - integrity sha512-clncPMJ23rxCIkZ9LoUC8SowwZGxWyN2TwRb0XvW/Cv9EavkRgRCOrCpneGyC326lqtMKx36onnpaSRHxErUYw== - dependencies: - tslib "^1.8.0" - -"@aws-sdk/util-utf8-node@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-utf8-node/-/util-utf8-node-3.1.0.tgz#fd7e506b5fdf404fbbdd9bc46429afeea8114b25" - integrity sha512-lrBLkROMh9kTjHOguusqLvTX5+5O5CVpAGeISZlW6CCx2pMHtVRyE9cdNuRI8aJpyZsU12j8SoaKDUPGD+ixzw== - dependencies: - "@aws-sdk/util-buffer-from" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/util-waiter@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-waiter/-/util-waiter-3.1.0.tgz#e01dbd6bc903e03101152f65ab9d2e0613f08c2f" - integrity sha512-nMaE3aGCZGVQw10IUXBxIbCXkyaX8T1Fn7GvJxeFJflKcXuKFKBfvkhzimSe/hJDC6Ykn7gNuI+EY+ZoCVXqtQ== - dependencies: - "@aws-sdk/abort-controller" "3.1.0" - tslib "^1.8.0" - -"@aws-sdk/xml-builder@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.1.0.tgz#ad7113075416436d8c822199674c45fc6ef42441" - integrity sha512-F6liCbWPMbnJq8d0qgzuXwG5O7jg1hhgiG71TTn83rnc6vFzyw2o0C+ztiqSZsbAq7r2PlEfBPWVD32gTFIXXw== - dependencies: - tslib "^1.8.0" - "@azure/abort-controller@^1.0.0": version "1.0.2" resolved "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.0.2.tgz#822405c966b2aec16fb62c1b19d37eaccf231995" @@ -2546,7 +1766,7 @@ globals "^11.1.0" lodash "^4.17.19" -"@babel/types@7.11.5", "@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.8.6", "@babel/types@^7.9.5", "@babel/types@^7.9.6": +"@babel/types@7.11.5", "@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.8.6", "@babel/types@^7.9.6": version "7.11.5" resolved "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d" integrity sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q== @@ -2607,7 +1827,7 @@ yup "^0.29.3" "@backstage/core@^0.3.0": - version "0.6.0" + version "0.6.1" dependencies: "@backstage/config" "^0.1.2" "@backstage/core-api" "^0.2.8" @@ -2646,13 +1866,13 @@ zen-observable "^0.8.15" "@backstage/plugin-catalog@^0.2.0": - version "0.3.0" + version "0.3.1" dependencies: - "@backstage/catalog-client" "^0.3.5" + "@backstage/catalog-client" "^0.3.6" "@backstage/catalog-model" "^0.7.1" - "@backstage/core" "^0.6.0" - "@backstage/plugin-catalog-react" "^0.0.2" - "@backstage/plugin-scaffolder" "^0.4.2" + "@backstage/core" "^0.6.1" + "@backstage/plugin-catalog-react" "^0.0.3" + "@backstage/plugin-scaffolder" "^0.5.0" "@backstage/theme" "^0.2.3" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" @@ -2660,7 +1880,6 @@ "@types/react" "^16.9" classnames "^2.2.6" git-url-parse "^11.4.4" - moment "^2.26.0" react "^16.13.1" react-dom "^16.13.1" react-helmet "6.1.0" @@ -2670,13 +1889,13 @@ swr "^0.3.0" "@backstage/plugin-catalog@^0.2.1": - version "0.3.0" + version "0.3.1" dependencies: - "@backstage/catalog-client" "^0.3.5" + "@backstage/catalog-client" "^0.3.6" "@backstage/catalog-model" "^0.7.1" - "@backstage/core" "^0.6.0" - "@backstage/plugin-catalog-react" "^0.0.2" - "@backstage/plugin-scaffolder" "^0.4.2" + "@backstage/core" "^0.6.1" + "@backstage/plugin-catalog-react" "^0.0.3" + "@backstage/plugin-scaffolder" "^0.5.0" "@backstage/theme" "^0.2.3" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" @@ -2684,7 +1903,6 @@ "@types/react" "^16.9" classnames "^2.2.6" git-url-parse "^11.4.4" - moment "^2.26.0" react "^16.13.1" react-dom "^16.13.1" react-helmet "6.1.0" @@ -3340,30 +2558,30 @@ tslib "~2.0.1" "@graphql-codegen/typescript@^1.17.7", "@graphql-codegen/typescript@^1.18.1": - version "1.18.1" - resolved "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-1.18.1.tgz#3d730472a01f18aea6331046f4ebfe3d91326801" - integrity sha512-Ee37NutKmaNrgAo2d5mv42RqPd8jJ6zyUKAH669Gbv0dFn2EK3sdC9PYQC9gXptv+H/eQn2gYgaa2nVpEPAIzg== + version "1.20.2" + resolved "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-1.20.2.tgz#223327a8aea929e772f481e264c5aa3a36707fcb" + integrity sha512-D/DMUz4O2UEoFucUVu2K2xoaMPAn68BwYGnCAKnSDqtFKsOEqmTjHFwcgyEnpucQ5xFeG0pKGMb1SlS2Go9J8Q== dependencies: "@graphql-codegen/plugin-helpers" "^1.18.2" - "@graphql-codegen/visitor-plugin-common" "^1.17.20" + "@graphql-codegen/visitor-plugin-common" "^1.18.2" auto-bind "~4.0.0" - tslib "~2.0.1" + tslib "~2.1.0" -"@graphql-codegen/visitor-plugin-common@^1.17.20": - version "1.17.20" - resolved "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-1.17.20.tgz#cff95cdd49cef270b3811fdb141a412ffe2bdfd7" - integrity sha512-buIpcNNyTqVubknancX8m9jARCZsUA5eKuskg+CylWKL/8CSaD2Tiq7CfbbNO10o7XIgRrPtJMl1c9hQ6N4ytw== +"@graphql-codegen/visitor-plugin-common@^1.17.20", "@graphql-codegen/visitor-plugin-common@^1.18.2": + version "1.18.2" + resolved "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-1.18.2.tgz#1a30bd33f011b4fb976e0f8462d160126db875ea" + integrity sha512-A8yBJGq7A7gxaVVXK4QXwV1ZpzZ64fH7U7JTGeq86o3jA7QNV2rmCRXCY0JttS2fu+olV18NjsWRwAXuAb1g9A== dependencies: "@graphql-codegen/plugin-helpers" "^1.18.2" "@graphql-tools/optimize" "^1.0.1" "@graphql-tools/relay-operation-optimizer" "^6" array.prototype.flatmap "^1.2.4" auto-bind "~4.0.0" - dependency-graph "^0.9.0" + dependency-graph "^0.10.0" graphql-tag "^2.11.0" parse-filepath "^1.0.2" pascal-case "^3.1.1" - tslib "~2.0.1" + tslib "~2.1.0" "@graphql-modules/core@^0.7.17": version "0.7.17" @@ -5405,16 +4623,16 @@ integrity sha512-In1q0tIiqTYKAGe3KOHDcFDdZRFISyQeSeipeTHGfki23ebHRZcjxvqj5SSdBkw65D4VpSREMi0s9i5iJiMcTw== "@storybook/addon-actions@^6.1.11": - version "6.1.11" - resolved "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.1.11.tgz#73e91cc95c45ea477cfd4f3603f6b95f5829eab6" - integrity sha512-J44XLx2G732OG7Az79Cpk5UlI5SyXHeQqdykwT/4IEQXSBXAYWSTIJJjpJdcjR/D+zpklab1QDSiWxCrKbe81A== + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.1.17.tgz#9d32336284738cefa69b99acafa4b132d5533600" + integrity sha512-4hyAvmjnI4C1ZQ7/t21jKKXE0jO1zAk310BkYin0NJf77Qi0tUE1DNOwirJY/xzRih36wWi1V79c/ZOJNsLv9Q== dependencies: - "@storybook/addons" "6.1.11" - "@storybook/api" "6.1.11" - "@storybook/client-api" "6.1.11" - "@storybook/components" "6.1.11" - "@storybook/core-events" "6.1.11" - "@storybook/theming" "6.1.11" + "@storybook/addons" "6.1.17" + "@storybook/api" "6.1.17" + "@storybook/client-api" "6.1.17" + "@storybook/components" "6.1.17" + "@storybook/core-events" "6.1.17" + "@storybook/theming" "6.1.17" core-js "^3.0.1" fast-deep-equal "^3.1.1" global "^4.3.2" @@ -5480,7 +4698,7 @@ global "^4.3.2" regenerator-runtime "^0.13.7" -"@storybook/addons@6.1.15", "@storybook/addons@^6.1.11": +"@storybook/addons@6.1.15": version "6.1.15" resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.1.15.tgz#09eb8d962f58bd20b4ac2f83b515831c83226352" integrity sha512-ENyHapLFOG93VaoQXPX8O3IWjLRyVBox9C9P20LMruKX/SfXAXx20qsoAWKKPGssopyOin17aoQX9pj+lFmCZQ== @@ -5495,6 +4713,21 @@ global "^4.3.2" regenerator-runtime "^0.13.7" +"@storybook/addons@6.1.17", "@storybook/addons@^6.1.11": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.1.17.tgz#ab0666446acb9fc19c94d7204dc9aafdefb6c7c2" + integrity sha512-3upDPJPzUkls2V3Fozzg+JOcv138bF90pbdRe9YSNu37QvRIL+iQODY7oFygMl+kqjG2F1FGw5EvxAV1mnlwCw== + dependencies: + "@storybook/api" "6.1.17" + "@storybook/channels" "6.1.17" + "@storybook/client-logger" "6.1.17" + "@storybook/core-events" "6.1.17" + "@storybook/router" "6.1.17" + "@storybook/theming" "6.1.17" + core-js "^3.0.1" + global "^4.3.2" + regenerator-runtime "^0.13.7" + "@storybook/api@6.1.11": version "6.1.11" resolved "https://registry.npmjs.org/@storybook/api/-/api-6.1.11.tgz#1e0b798203df823ac21184386258cf8b5f17f440" @@ -5545,18 +4778,30 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/channel-postmessage@6.1.11": - version "6.1.11" - resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.1.11.tgz#62c1079f04870dd27925bd538a2020e7380daa2e" - integrity sha512-voW4Z2SUacDOxwN2q1NEBL//8OpgvL2C5CeoG1VQyEllKM8Vg9t1Nxo2FFTJBzv5LeEX7VIJKeBoB25DYvKyng== +"@storybook/api@6.1.17": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/api/-/api-6.1.17.tgz#50393ce9b718063b67680212df895eceacc0c11d" + integrity sha512-sthcfuk2EQ3F5R620PBqpI4Pno3g7KQm6YPZA0DXB+LD/z61xH9ToE1gTLF4nzlE6HwghwkXOZyRwDowRdG+7A== dependencies: - "@storybook/channels" "6.1.11" - "@storybook/client-logger" "6.1.11" - "@storybook/core-events" "6.1.11" + "@reach/router" "^1.3.3" + "@storybook/channels" "6.1.17" + "@storybook/client-logger" "6.1.17" + "@storybook/core-events" "6.1.17" + "@storybook/csf" "0.0.1" + "@storybook/router" "6.1.17" + "@storybook/semver" "^7.3.2" + "@storybook/theming" "6.1.17" + "@types/reach__router" "^1.3.7" core-js "^3.0.1" + fast-deep-equal "^3.1.1" global "^4.3.2" - qs "^6.6.0" + lodash "^4.17.15" + memoizerific "^1.11.3" + regenerator-runtime "^0.13.7" + store2 "^2.7.1" telejson "^5.0.2" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" "@storybook/channel-postmessage@6.1.15": version "6.1.15" @@ -5571,6 +4816,19 @@ qs "^6.6.0" telejson "^5.0.2" +"@storybook/channel-postmessage@6.1.17": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.1.17.tgz#309ce67c94637ec13319d4ce360a8f3742ddbaf4" + integrity sha512-2nVqxq4oZdSITqhFOnkh1rmDMjCwHuobnK5Fp3l7ftCkbmiZHMheKK9Tz4Rb803dhXvcGYs0zRS8NjKyxlOLsA== + dependencies: + "@storybook/channels" "6.1.17" + "@storybook/client-logger" "6.1.17" + "@storybook/core-events" "6.1.17" + core-js "^3.0.1" + global "^4.3.2" + qs "^6.6.0" + telejson "^5.0.2" + "@storybook/channels@6.1.11": version "6.1.11" resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.1.11.tgz#a93a83746ad78dd40e1c056029f6d93b17bb66bc" @@ -5589,27 +4847,12 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/client-api@6.1.11": - version "6.1.11" - resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.1.11.tgz#d25aac484ca84a1acb01d450e756a62408f00c1a" - integrity sha512-DodJQzGCR+PYs26klvbquTjfBgkw5nvCZd3jpgWQtOrYaY/cMY1LLkVkKqrm2ENW8f7vf7tiw78RtxaXy7xeIQ== +"@storybook/channels@6.1.17": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.1.17.tgz#2cc89a6b9727d19c24b15fa3cb15569b469db864" + integrity sha512-MUdj0eKr/AbxevHTSXX7AsgxAz6e5O4ZxoYX5G8ggoqSXrWzws6zRFmUmmTdjpIvVmP2M1Kh4SYFAKcS/AGw9w== dependencies: - "@storybook/addons" "6.1.11" - "@storybook/channel-postmessage" "6.1.11" - "@storybook/channels" "6.1.11" - "@storybook/client-logger" "6.1.11" - "@storybook/core-events" "6.1.11" - "@storybook/csf" "0.0.1" - "@types/qs" "^6.9.0" - "@types/webpack-env" "^1.15.3" core-js "^3.0.1" - global "^4.3.2" - lodash "^4.17.15" - memoizerific "^1.11.3" - qs "^6.6.0" - regenerator-runtime "^0.13.7" - stable "^0.1.8" - store2 "^2.7.1" ts-dedent "^2.0.0" util-deprecate "^1.0.2" @@ -5637,6 +4880,30 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" +"@storybook/client-api@6.1.17": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.1.17.tgz#3ced22f08a47af70ccf8929111bc44b79e9e8ec0" + integrity sha512-Loz/wdh0axgq0PS19tx0tGEFEkFWlYc6YauJGHjygYa1xX7mJ54hDoaTolySCXN1HtfZn08D847yjGSN2oIqVg== + dependencies: + "@storybook/addons" "6.1.17" + "@storybook/channel-postmessage" "6.1.17" + "@storybook/channels" "6.1.17" + "@storybook/client-logger" "6.1.17" + "@storybook/core-events" "6.1.17" + "@storybook/csf" "0.0.1" + "@types/qs" "^6.9.0" + "@types/webpack-env" "^1.15.3" + core-js "^3.0.1" + global "^4.3.2" + lodash "^4.17.15" + memoizerific "^1.11.3" + qs "^6.6.0" + regenerator-runtime "^0.13.7" + stable "^0.1.8" + store2 "^2.7.1" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" + "@storybook/client-logger@6.1.11": version "6.1.11" resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.1.11.tgz#5dd092e4293e5f58f7e89ddbc6eb2511b7d60954" @@ -5653,6 +4920,14 @@ core-js "^3.0.1" global "^4.3.2" +"@storybook/client-logger@6.1.17": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.1.17.tgz#0d89aaf824457f19bf9aa585bbcada57595e7d01" + integrity sha512-oqExrxhmws0ihB47sjdynZHd3OpUP4KWkx4udG+74lYIvBH+EZmQ9xF+UofeY3j5p1I9k8ugEcVKy0sqh1yR3w== + dependencies: + core-js "^3.0.1" + global "^4.3.2" + "@storybook/components@6.1.11": version "6.1.11" resolved "https://registry.npmjs.org/@storybook/components/-/components-6.1.11.tgz#edd5db7fe43f47b5a7ab515840795a89d931512e" @@ -5705,6 +4980,32 @@ react-textarea-autosize "^8.1.1" ts-dedent "^2.0.0" +"@storybook/components@6.1.17": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/components/-/components-6.1.17.tgz#f92d36e370ec6039d8c7cee9ef13dda866eed3da" + integrity sha512-rIEll0UTxEKmG4IsSS5K+6DjRLVtX8J+9cg79GSAC7N1ZHUR1UQmjjJaehJa5q/NQ5H8C39acxpT4Py/BcsL2g== + dependencies: + "@popperjs/core" "^2.5.4" + "@storybook/client-logger" "6.1.17" + "@storybook/csf" "0.0.1" + "@storybook/theming" "6.1.17" + "@types/overlayscrollbars" "^1.9.0" + "@types/react-color" "^3.0.1" + "@types/react-syntax-highlighter" "11.0.4" + core-js "^3.0.1" + fast-deep-equal "^3.1.1" + global "^4.3.2" + lodash "^4.17.15" + markdown-to-jsx "^6.11.4" + memoizerific "^1.11.3" + overlayscrollbars "^1.10.2" + polished "^3.4.4" + react-color "^2.17.0" + react-popper-tooltip "^3.1.1" + react-syntax-highlighter "^13.5.0" + react-textarea-autosize "^8.1.1" + ts-dedent "^2.0.0" + "@storybook/core-events@6.1.11": version "6.1.11" resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.1.11.tgz#d50e8ec90490f9a7180a8c8a83afb6dcfe47ed66" @@ -5719,6 +5020,13 @@ dependencies: core-js "^3.0.1" +"@storybook/core-events@6.1.17": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.1.17.tgz#697ed916fcb2a411bc9f8bdbfacd0eb9d394eb58" + integrity sha512-xBI7kmyROcqhYNmFv4QBjD77CzV+k/0F051YFS5WicEI4qDWPPvzaShhm96ZrGobUX3+di4pC11gqdsrFeNCEg== + dependencies: + core-js "^3.0.1" + "@storybook/core@6.1.15": version "6.1.15" resolved "https://registry.npmjs.org/@storybook/core/-/core-6.1.15.tgz#7ff8c314d3857497bf2e26c69a1fa93ef37301aa" @@ -5895,6 +5203,18 @@ memoizerific "^1.11.3" qs "^6.6.0" +"@storybook/router@6.1.17": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/router/-/router-6.1.17.tgz#96746878c50c6c97c7de5a1b23a9503c5d648775" + integrity sha512-wLqSOB5yLXgNyDGy008RUvjVRtVMq7lhmMRicSIxgJpkakPrMRN8n/nK7pxgQc/xDTphnS0u1nT01i97WszhCg== + dependencies: + "@reach/router" "^1.3.3" + "@types/reach__router" "^1.3.7" + core-js "^3.0.1" + global "^4.3.2" + memoizerific "^1.11.3" + qs "^6.6.0" + "@storybook/semver@^7.3.2": version "7.3.2" resolved "https://registry.npmjs.org/@storybook/semver/-/semver-7.3.2.tgz#f3b9c44a1c9a0b933c04e66d0048fcf2fa10dac0" @@ -5956,6 +5276,24 @@ resolve-from "^5.0.0" ts-dedent "^2.0.0" +"@storybook/theming@6.1.17": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/theming/-/theming-6.1.17.tgz#99cc120a230c30458d833b40c806b9b4dff7b34a" + integrity sha512-LpRuY2aIh2td+qZi7E8cp2oM88LudNMmTsBT6N2/Id69u/a9qQd2cYCA9k9fAsg7rjor+wR/N695jk3SGtoFTw== + dependencies: + "@emotion/core" "^10.1.1" + "@emotion/is-prop-valid" "^0.8.6" + "@emotion/styled" "^10.0.23" + "@storybook/client-logger" "6.1.17" + core-js "^3.0.1" + deep-object-diff "^1.1.0" + emotion-theming "^10.0.19" + global "^4.3.2" + memoizerific "^1.11.3" + polished "^3.4.4" + resolve-from "^5.0.0" + ts-dedent "^2.0.0" + "@storybook/ui@6.1.15": version "6.1.15" resolved "https://registry.npmjs.org/@storybook/ui/-/ui-6.1.15.tgz#a0f6c49fcf81cf172cd2de4c8dba2be1296891f6" @@ -6133,30 +5471,11 @@ resolved "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz#00bf9a7a73f1cad3948cdab1f8dfb774750f8c80" integrity sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q== -"@svgr/babel-plugin-transform-svg-component@^5.4.0": - version "5.4.0" - resolved "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.4.0.tgz#a2212b4d018e6075a058bb7e220a66959ef7a03c" - integrity sha512-zLl4Fl3NvKxxjWNkqEcpdSOpQ3LGVH2BNFQ6vjaK6sFo2IrSznrhURIPI0HAphKiiIwNYjAfE0TNoQDSZv0U9A== - "@svgr/babel-plugin-transform-svg-component@^5.5.0": version "5.5.0" resolved "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz#583a5e2a193e214da2f3afeb0b9e8d3250126b4a" integrity sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ== -"@svgr/babel-preset@^5.4.0": - version "5.4.0" - resolved "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.4.0.tgz#da21854643e1c4ad2279239baa7d5a8b128c1f15" - integrity sha512-Gyx7cCxua04DBtyILTYdQxeO/pwfTBev6+eXTbVbxe4HTGhOUW6yo7PSbG2p6eJMl44j6XSequ0ZDP7bl0nu9A== - dependencies: - "@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.4.0" - "@svgr/babel-preset@^5.5.0": version "5.5.0" resolved "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.5.0.tgz#8af54f3e0a8add7b1e2b0fcd5a882c55393df327" @@ -6180,13 +5499,6 @@ camelcase "^6.2.0" cosmiconfig "^7.0.0" -"@svgr/hast-util-to-babel-ast@^5.4.0": - version "5.4.0" - resolved "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.4.0.tgz#bb5d002e428f510aa5b53ec0a02377a95b367715" - integrity sha512-+U0TZZpPsP2V1WvVhqAOSTk+N+CjYHdZx+x9UBa1eeeZDXwH8pt0CrQf2+SvRl/h2CAPRFkm+Ey96+jKP8Bsgg== - dependencies: - "@babel/types" "^7.9.5" - "@svgr/hast-util-to-babel-ast@^5.5.0": version "5.5.0" resolved "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz#5ee52a9c2533f73e63f8f22b779f93cd432a5461" @@ -6194,17 +5506,7 @@ dependencies: "@babel/types" "^7.12.6" -"@svgr/plugin-jsx@5.4.x": - version "5.4.0" - resolved "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.4.0.tgz#ab47504c55615833c6db70fca2d7e489f509787c" - integrity sha512-SGzO4JZQ2HvGRKDzRga9YFSqOqaNrgLlQVaGvpZ2Iht2gwRp/tq+18Pvv9kS9ZqOMYgyix2LLxZMY1LOe9NPqw== - dependencies: - "@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-jsx@^5.4.0", "@svgr/plugin-jsx@^5.5.0": +"@svgr/plugin-jsx@5.5.x", "@svgr/plugin-jsx@^5.4.0", "@svgr/plugin-jsx@^5.5.0": version "5.5.0" resolved "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz#1aa8cd798a1db7173ac043466d7b52236b369000" integrity sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA== @@ -6600,11 +5902,9 @@ integrity sha512-Xwy8o12ak+iYgFr/KCVaVK5Sy+jFMiiPAID3+ObvMlBzy26XQJw5xu+a6rlHsrJENXj/AwJOGsJpVohUjAzSKQ== "@types/cors@^2.8.4", "@types/cors@^2.8.6": - version "2.8.6" - resolved "https://registry.npmjs.org/@types/cors/-/cors-2.8.6.tgz#cfaab33c49c15b1ded32f235111ce9123009bd02" - integrity sha512-invOmosX0DqbpA+cE2yoHGUlF/blyf7nB0OGYBBiH27crcVm5NmFaZkLP4Ta1hGaesckCi5lVLlydNJCxkTOSg== - dependencies: - "@types/express" "*" + version "2.8.9" + resolved "https://registry.npmjs.org/@types/cors/-/cors-2.8.9.tgz#4bd1fcac72eca8d5bec93e76c7fdcbdc1bc2cd4a" + integrity sha512-zurD1ibz21BRlAOIKP8yhrxlqKx6L9VCwkB5kMiP6nZAhoF5MvC7qS1qPA7nRcr1GJolfkQC7/EAL4hdYejLtg== "@types/cssnano@*": version "4.0.0" @@ -6868,9 +6168,9 @@ "@types/node" "*" "@types/http-proxy@*", "@types/http-proxy@^1.17.4": - version "1.17.4" - resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.4.tgz#e7c92e3dbe3e13aa799440ff42e6d3a17a9d045b" - integrity sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q== + version "1.17.5" + resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.5.tgz#c203c5e6e9dc6820d27a40eb1e511c70a220423d" + integrity sha512-GNkDE7bTv6Sf8JbV2GksknKOsk7OznNYHSdrtvPJXO0qJ9odZig6IZKUi5RFGi6d1bf6dgIAe4uXi3DBc7069Q== dependencies: "@types/node" "*" @@ -8873,6 +8173,21 @@ autoprefixer@^9.7.2: postcss "^7.0.32" postcss-value-parser "^4.1.0" +aws-sdk@^2.840.0: + version "2.840.0" + resolved "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.840.0.tgz#f5529c9bd3bf0be7f8e855a23ff9c12b1705418f" + integrity sha512-ngesHJqb0PXYjJNnCsAX4yLkR6JFQJB+3eDGwh3mYRjcq9voix5RfbCFQT1lwWu7bcMBPCrRIA2lJkkTMYXq+A== + dependencies: + buffer "4.9.2" + events "1.1.1" + ieee754 "1.1.13" + jmespath "0.15.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.3.2" + xml2js "0.4.19" + aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -9522,11 +8837,6 @@ bowser@^1.7.3: resolved "https://registry.npmjs.org/bowser/-/bowser-1.9.4.tgz#890c58a2813a9d3243704334fa81b96a5c150c9a" integrity sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ== -bowser@^2.11.0: - version "2.11.0" - resolved "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" - integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA== - boxen@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b" @@ -9756,7 +9066,7 @@ buffer-xor@^1.0.3: resolved "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= -buffer@^4.3.0: +buffer@4.9.2, buffer@^4.3.0: version "4.9.2" resolved "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== @@ -12030,6 +11340,11 @@ depd@~2.0.0: resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== +dependency-graph@^0.10.0: + version "0.10.0" + resolved "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.10.0.tgz#dfebe384f1f36faf7782be203a7a71102a6335a6" + integrity sha512-c9amUgpgxSi1bE5/sbLwcs5diLD0ygCQYmhfM5H1s5VH1mCsYkcmAL3CcNdv4kdSw6JuMoHeDGzLgj/gAXdWVg== + dependency-graph@^0.9.0: version "0.9.0" resolved "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.9.0.tgz#11aed7e203bc8b00f48356d92db27b265c445318" @@ -13199,6 +12514,11 @@ eventemitter3@^4.0.0: resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb" integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg== +events@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= + events@3.1.0, events@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/events/-/events-3.1.0.tgz#84279af1b34cb75aa88bf5ff291f6d0bd9b31a59" @@ -13496,11 +12816,6 @@ extsprintf@^1.2.0: resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= -fast-base64-decode@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz#b434a0dd7d92b12b43f26819300d2dafb83ee418" - integrity sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q== - fast-deep-equal@2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" @@ -13570,11 +12885,6 @@ fast-text-encoding@^1.0.0: resolved "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53" integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig== -fast-xml-parser@^3.16.0: - version "3.17.6" - resolved "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.17.6.tgz#4f5df8cf927c3e59a10362abcfb7335c34bc5c5f" - integrity sha512-40WHI/5d2MOzf1sD2bSaTXlPn1lueJLAX6j1xH5dSAr6tNeut8B9ktEL6sjAK9yVON4uNj9//axOdBJUuruCzw== - fastest-stable-stringify@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-1.0.1.tgz#9122d406d4c9d98bea644a6b6853d5874b87b028" @@ -15476,6 +14786,11 @@ identity-obj-proxy@3.0.0: dependencies: harmony-reflect "^1.4.6" +ieee754@1.1.13: + version "1.1.13" + resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== + ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -16947,6 +16262,11 @@ jest@^26.0.1: import-local "^3.0.2" jest-cli "^26.6.3" +jmespath@0.15.0: + version "0.15.0" + resolved "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" + integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc= + jose@^1.27.1: version "1.28.0" resolved "https://registry.npmjs.org/jose/-/jose-1.28.0.tgz#0803f8c71f43cd293a9d931c555c30531f5ca5dc" @@ -18190,7 +17510,7 @@ lru-queue@0.1: dependencies: es5-ext "~0.10.2" -luxon@^1.25.0: +luxon@1.25.0, luxon@^1.25.0: version "1.25.0" resolved "https://registry.npmjs.org/luxon/-/luxon-1.25.0.tgz#d86219e90bc0102c0eb299d65b2f5e95efe1fe72" integrity sha512-hEgLurSH8kQRjY6i4YLey+mcKVAWXbDNlZRmM6AgWDJ1cY3atl8Ztf5wEY7VBReFbmGnwQPz7KYJblL8B2k0jQ== @@ -21327,9 +20647,9 @@ prettier@^2.0.5, prettier@~2.0.5: integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg== pretty-bytes@^5.3.0: - version "5.3.0" - resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.3.0.tgz#f2849e27db79fb4d6cfe24764fc4134f165989f2" - integrity sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg== + version "5.5.0" + resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.5.0.tgz#0cecda50a74a941589498011cf23275aa82b339e" + integrity sha512-p+T744ZyjjiaFlMUZZv6YPC5JrkNj8maRmPaQCWFJFplUAzpIUTRaTcS+7wmZtUoFXHtESJb23ISliaWyz3SHA== pretty-error@^2.1.1: version "2.1.1" @@ -22069,13 +21389,6 @@ react-motion@^0.5.2: prop-types "^15.5.8" raf "^3.1.0" -react-native-get-random-values@^1.4.0: - version "1.5.0" - resolved "https://registry.npmjs.org/react-native-get-random-values/-/react-native-get-random-values-1.5.0.tgz#91cda18f0e66e3d9d7660ba80c61c914030c1e05" - integrity sha512-LK+Wb8dEimJkd/dub7qziDmr9Tw4chhpzVeQ6JDo4czgfG4VXbptRyOMdu8503RiMF6y9pTH6ZUTkrrpprqT7w== - dependencies: - fast-base64-decode "^1.0.0" - react-popper-tooltip@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/react-popper-tooltip/-/react-popper-tooltip-3.1.1.tgz#329569eb7b287008f04fcbddb6370452ad3f9eac" @@ -23313,6 +22626,11 @@ sanitize-html@^1.27.0: srcset "^2.0.1" xtend "^4.0.1" +sax@1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" + integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= + sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4: version "1.2.4" resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -25391,7 +24709,12 @@ tslib@^1.10.0, tslib@^1.11.1, tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1 resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.1, tslib@~2.0.0, tslib@~2.0.1: +tslib@^2.0.0, tslib@^2.0.1, tslib@~2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" + integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== + +tslib@~2.0.0, tslib@~2.0.1: version "2.0.3" resolved "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c" integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== @@ -25863,6 +25186,14 @@ url-parse@^1.4.3, url-parse@^1.4.7: querystringify "^2.1.1" requires-port "^1.0.0" +url@0.10.3: + version "0.10.3" + resolved "https://registry.npmjs.org/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" + integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ= + dependencies: + punycode "1.3.2" + querystring "0.2.0" + url@^0.11.0, url@~0.11.0: version "0.11.0" resolved "https://registry.npmjs.org/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" @@ -25961,7 +25292,12 @@ utils-merge@1.0.1, utils-merge@1.x.x: resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= -uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0, uuid@^3.3.2, uuid@^3.4.0: +uuid@3.3.2: + version "3.3.2" + resolved "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + +uuid@^3.0.1, uuid@^3.1.0, uuid@^3.3.2, uuid@^3.4.0: version "3.4.0" resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== @@ -26682,6 +26018,14 @@ xml-name-validator@^3.0.0: resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== +xml2js@0.4.19: + version "0.4.19" + resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" + integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== + dependencies: + sax ">=0.6.0" + xmlbuilder "~9.0.1" + xml2js@^0.4.19, xml2js@^0.4.23: version "0.4.23" resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" @@ -26695,6 +26039,11 @@ xmlbuilder@^11.0.0, xmlbuilder@~11.0.0: resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== +xmlbuilder@~9.0.1: + version "9.0.7" + resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" + integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= + xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" From f49998d408426dd60c13a3af6382ce7784612e40 Mon Sep 17 00:00:00 2001 From: Esteban Barrios Date: Fri, 12 Feb 2021 16:20:16 +0100 Subject: [PATCH 15/34] rebase master (#20) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix client reference * Fix CatalogClient reference in create-app too * Move catalog-client dep to create-app * Add catalog-client dep again.. * Use RequestOptions * tsc * Backwards compatible scaffolder * Fix test * Avoid importing core * Use request options * Increase code coverage * Type those variables * use the second mock * Trying to make tests work on Github too * Get clean yarn from master * Only add authorization header if token exists * Use request options * Forward authorization header token * Refactor to use context argument * Allow the registration of optional locations via the catalog-client * Make EntityRefLink a React.forwardRef in order to use it as root component in other components like ListItem * Rework the user flow of the catalog-import plugin * Update the create-app template * Add luxon and remove moment * Port moment to luxon * Add change set * Split the step label and content to be able to refactor the stepper from vertical to horizontal in the future * Fix tsc, prettier and import issues * Update .changeset/strange-olives-unite.md Co-authored-by: Himanshu Mishra * Update plugins/auth-backend/src/identity/TokenFactory.ts Co-authored-by: Himanshu Mishra * Change from minor to patch * Update DatabaseKeyStore.ts * feat: added a test for running a simple cypress server * Add luxon and remove moment * Port from moment to Luxon * Add changeset * Fixes following CR * Rebased and reinstalled yarn.lock * Changed to fromSQL * Fix prettier formatting * chore: reworking the lock file for the cypress deps * chore: adding cypress.config * chore: reworking some more examples * chore: added some more simple stuff for e2e tests with cypress * chore: reworking again * chore: making cypress nice * chore: added a sample test that works * chore: reworking tests to use he cypress github-action instead * chore: reconfigure Cypress run * chore: don't install - we have deps * chore: dump video recordings too * chore: slimming down install script a little * chore: make pretty * chore: fixing syntax * chore: fixing syntax again * Align card elements with list * Align card elements with List * Add changeset * Removed unused import * chore: archive video footage with the correct path * chore: fix lint warnings * chore: fix prettier again * chore: move files around for ease * Added pattern for component name * Add changeset * stash: push to stash * Return AboutCard title to group name * Refactor children to use shared function * TechDocs: Add visbility to migrate away from basic setup * bugfix: [3310] Favoriting a component resets interface to your owned components update changeset remove unused import fix changeset message fix vale errors? * Create utc DateTime object directly * integration: update the gitlab config mandatory fields to match reality * Resolve review comments * Remove shadowed variables * generateEntityDefinitions should return a PartialEntity * Update roadmap with Backstage Community Sessions * Make sure that SidebarItems are also active when on sub route * Add whitespace around variable * Add changeset * Add word to dictionary * Smater duration display * Add changeset * chore(deps-dev): bump @graphql-codegen/typescript from 1.18.1 to 1.20.2 Bumps [@graphql-codegen/typescript](https://github.com/dotansimha/graphql-code-generator/tree/HEAD/packages/plugins/typescript/typescript) from 1.18.1 to 1.20.2. - [Release notes](https://github.com/dotansimha/graphql-code-generator/releases) - [Changelog](https://github.com/dotansimha/graphql-code-generator/blob/master/packages/plugins/typescript/typescript/CHANGELOG.md) - [Commits](https://github.com/dotansimha/graphql-code-generator/commits/@graphql-codegen/typescript@1.20.2/packages/plugins/typescript/typescript) Signed-off-by: dependabot[bot] * chore: reworking folder structure to move cypress outside of the project * chore: updating workflow to point at the right folder now * chore: fixing cypress build * microsite: Fix color contrast for pre tags * Add package name to lockfile.ts error * Create new-mangos-tap.md * Changed changeset bump to be patch * Pass registered Logger in ServiceBuilderImpl to requestLoggingHandler `requestLoggingHandler` takes an optional `logger` parameter that it can use to log incoming requests. The `ServiceBuilderImpl` was not passing on this logger, if set, to `requestLoggingHandler` middleware * Add indices on columns referring location(id) * Add changeset * Catch catalog errors * Add changeset * Fixed parseUrl to output catalogPaths beginning with '/' * Add configurable OAuth 2.0 scopes - Add oauth2 config for optional scopes - Document oauth2 config keys - Add OAuth2 to demo app list of identity providers * incorrectly added callbackUrl * chore: added a simple readme to run some simple tests against a backstage instance * chore(deps): bump @svgr/plugin-jsx from 5.4.0 to 5.5.0 Bumps [@svgr/plugin-jsx](https://github.com/gregberge/svgr) from 5.4.0 to 5.5.0. - [Release notes](https://github.com/gregberge/svgr/releases) - [Changelog](https://github.com/gregberge/svgr/blob/main/CHANGELOG.md) - [Commits](https://github.com/gregberge/svgr/compare/v5.4.0...v5.5.0) Signed-off-by: dependabot[bot] * chore(deps): bump @types/cors from 2.8.6 to 2.8.9 Bumps [@types/cors](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/cors) from 2.8.6 to 2.8.9. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/cors) Signed-off-by: dependabot[bot] * fix: removing test e2e-test tsconfig.json file * TechDocs: Preparers will take and return an etag for cache invalidation * TechDocs: Implement etag based caching for url preparer Signed-off-by: Himanshu Mishra * TechDocs: Implement etag based caching for git preparer Signed-off-by: Himanshu Mishra * TechDocs: Warn when using legacy git preparer and dir preparer in backstage.io/techdocs-ref Context: https://github.com/backstage/backstage/issues/4409 * TechDocs: Update tests for proper url preparer caching * TechDocs: Deduplicate git clone for cache check Signed-off-by: Himanshu Mishra * TechDocs: Add changesets for deprecation and proper caching * TechDocs: Implement caching for dir preparer * Update the changesets * scaffolder-backend: remove auth-backend dependency * catalog-client: rename ApiContext to CatalogRequestOptions + avoid export * refactor existing usage of ApiContext * techdocs: don't swallow errors other than NotModifiedError * catalog-import: removed bonus code * techdocs: meaningful logs when readTree starts * Flatten the options of the CatalogImportPage * inline optional id token auth headers * Define relationship to software catalog and loose coupling by convention. * Clarify intentions around bulk vs. incremental index management. * Break apart backend plugins & clarify indexer/plugin relationship. * update changeset bump levels * [Search] documentation update (#4459) * delete link to issue as it is closed * replace usage of easy as its very subjective * Update docs/features/search/architecture.md Co-authored-by: Adam Harvey * prettier....: Co-authored-by: Adam Harvey * Own it. * scaffolder: include backstage identity token in requests * review feedback tweaks * docs/apis: update to use named plugin var * sentry: update plugin instance export name * clear other field when toggling reason * changeset * fix(pagerduty): use the luxon date library * Don't pass default as a scope to OIDC providers * docs(TechDocs): Add GitHub Actions CI example with AWS S3 * tech-radar: migrated to new composability API * cost-insights: migrate to new composability API * Changeset * Fix line endings * Prettier * Remove unnecessary scopes from OIDC defaultApi * Bump plugin-auth-backend to a minor change, add documentation for fixing it * Fix md syntax error * Fix Vale spelling error * Remove defaultScopes from OIDC api * [ImgBot] Optimize images /plugins/catalog-import/docs/catalog-import-screenshot.png -- 613.39kb -> 356.48kb (41.88%) Signed-off-by: ImgBotApp * github/workflows: use lax config checks * Use commented-out example value for scope * feat(pagerduty): add changeset * Apply suggestions from code review Co-authored-by: Rémi Doreau <32459935+ayshiff@users.noreply.github.com> * feat(catalog): add entity links card component * update entity links changeset * Fix prettier * chore(deps-dev): bump @types/http-proxy from 1.17.4 to 1.17.5 Bumps [@types/http-proxy](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/http-proxy) from 1.17.4 to 1.17.5. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/http-proxy) Signed-off-by: dependabot[bot] * Revert "Merge pull request #3953 from ayshiff/feature/techdocs-aws-sdkv3" This reverts commit 1c7771871ec46bf55f87fba5991b95938bb57017, reversing changes made to 65d5c8e6fe4364d036d69a0592c87a9037f95b8b. * Revert "Merge pull request #4035 from ayshiff/feature/catalog-backend-aws-sdk-v3" This reverts commit 1df913464889cdf63a1484b06538b76a8baed495, reversing changes made to d45a5100695fb973ae38f74f23bbced9a0101840. * Add changeset * feat: support custom app icons * backend-common: implement UrlReader.search for the other providers too * Remove changeset * docs(TechDocs): Improvements to the CI example Signed-off-by: Himanshu Mishra * fix(catalog): entity links incorrect wrapping * Remove unused functions and luxon * Removed old imports * Fix broken links in documentation (#4418) * Update IdentityApi.md * Update docs/reference/utility-apis/IdentityApi.md * Update docs/reference/utility-apis/IdentityApi.md * ran yarn docgen * Use JS Date instead of Datetime * Add line * Prettier fix * Move parseDate function * Version Packages * chore: fix the create-app version * Limit the props that are forwarded to the Link component in the EntityRefLink * Use routed tabs to link to every settings page * fix up yarn after release * chore: add lockfile to words * catalog-info: add links * Export Select component from core I saw the Select component on storybook and went to use it but it seems it's not exported. Any chance it could be exported? * bug: filepath can be returned as undefined from `git-url-parse` let's default to empty * chore: might as well do this for all parsing * chore: changeset * Add changeset for fixing requestLoggingHandler * chore(deps-dev): bump @storybook/addon-actions from 6.1.11 to 6.1.17 Bumps [@storybook/addon-actions](https://github.com/storybookjs/storybook/tree/HEAD/addons/actions) from 6.1.11 to 6.1.17. - [Release notes](https://github.com/storybookjs/storybook/releases) - [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md) - [Commits](https://github.com/storybookjs/storybook/commits/v6.1.17/addons/actions) Signed-off-by: dependabot[bot] * Port AboutCard * Fix tests * Revert EntityPage changes * docs: Merge auth glossary with main glossary * Remove the "Move repository" menu entry from the catalog page, as it's just a placeholder It will be easy to bring it back later, but for now it just confuses users that it's not doing anything. It's also hard to remove for integrators. * make the template cards conform to mui standard Co-authored-by: Erik Larsson Co-authored-by: Dominik Henneke Co-authored-by: Nils Streijffert Co-authored-by: Nils Streijffert Co-authored-by: Himanshu Mishra Co-authored-by: blam Co-authored-by: Nir Gazit Co-authored-by: Adam Harvey Co-authored-by: NHI TRAN Co-authored-by: Fredrik Adelöw Co-authored-by: Oliver Sand Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Debajyoti Halder Co-authored-by: Patrik Oldsberg Co-authored-by: Gowind Co-authored-by: Alan Crosswell Co-authored-by: Eric Peterson Co-authored-by: Emma Indal Co-authored-by: Ryan Vazquez Co-authored-by: Ryan Vazquez Co-authored-by: Remi Co-authored-by: Ryan Manny Co-authored-by: ImgBotApp Co-authored-by: Rémi Doreau <32459935+ayshiff@users.noreply.github.com> Co-authored-by: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Co-authored-by: Joel Low Co-authored-by: Eric Peterson Co-authored-by: github-actions[bot] Co-authored-by: Iain Billett From 3924be6479df2be82d5c7b5252c4987605f55370 Mon Sep 17 00:00:00 2001 From: Esteban Barrios Date: Fri, 12 Feb 2021 16:27:13 +0100 Subject: [PATCH 16/34] rebase master (#21) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix client reference * Fix CatalogClient reference in create-app too * Move catalog-client dep to create-app * Add catalog-client dep again.. * Use RequestOptions * tsc * Backwards compatible scaffolder * Fix test * Avoid importing core * Use request options * Increase code coverage * Type those variables * use the second mock * Trying to make tests work on Github too * Get clean yarn from master * Only add authorization header if token exists * Use request options * Forward authorization header token * Refactor to use context argument * Allow the registration of optional locations via the catalog-client * Make EntityRefLink a React.forwardRef in order to use it as root component in other components like ListItem * Rework the user flow of the catalog-import plugin * Update the create-app template * Add luxon and remove moment * Port moment to luxon * Add change set * Split the step label and content to be able to refactor the stepper from vertical to horizontal in the future * Fix tsc, prettier and import issues * Update .changeset/strange-olives-unite.md Co-authored-by: Himanshu Mishra * Update plugins/auth-backend/src/identity/TokenFactory.ts Co-authored-by: Himanshu Mishra * Change from minor to patch * Update DatabaseKeyStore.ts * feat: added a test for running a simple cypress server * Add luxon and remove moment * Port from moment to Luxon * Add changeset * Fixes following CR * Rebased and reinstalled yarn.lock * Changed to fromSQL * Fix prettier formatting * chore: reworking the lock file for the cypress deps * chore: adding cypress.config * chore: reworking some more examples * chore: added some more simple stuff for e2e tests with cypress * chore: reworking again * chore: making cypress nice * chore: added a sample test that works * chore: reworking tests to use he cypress github-action instead * chore: reconfigure Cypress run * chore: don't install - we have deps * chore: dump video recordings too * chore: slimming down install script a little * chore: make pretty * chore: fixing syntax * chore: fixing syntax again * Align card elements with list * Align card elements with List * Add changeset * Removed unused import * chore: archive video footage with the correct path * chore: fix lint warnings * chore: fix prettier again * chore: move files around for ease * Added pattern for component name * Add changeset * stash: push to stash * Return AboutCard title to group name * Refactor children to use shared function * TechDocs: Add visbility to migrate away from basic setup * bugfix: [3310] Favoriting a component resets interface to your owned components update changeset remove unused import fix changeset message fix vale errors? * Create utc DateTime object directly * integration: update the gitlab config mandatory fields to match reality * Resolve review comments * Remove shadowed variables * generateEntityDefinitions should return a PartialEntity * Update roadmap with Backstage Community Sessions * Make sure that SidebarItems are also active when on sub route * Add whitespace around variable * Add changeset * Add word to dictionary * Smater duration display * Add changeset * chore(deps-dev): bump @graphql-codegen/typescript from 1.18.1 to 1.20.2 Bumps [@graphql-codegen/typescript](https://github.com/dotansimha/graphql-code-generator/tree/HEAD/packages/plugins/typescript/typescript) from 1.18.1 to 1.20.2. - [Release notes](https://github.com/dotansimha/graphql-code-generator/releases) - [Changelog](https://github.com/dotansimha/graphql-code-generator/blob/master/packages/plugins/typescript/typescript/CHANGELOG.md) - [Commits](https://github.com/dotansimha/graphql-code-generator/commits/@graphql-codegen/typescript@1.20.2/packages/plugins/typescript/typescript) Signed-off-by: dependabot[bot] * chore: reworking folder structure to move cypress outside of the project * chore: updating workflow to point at the right folder now * chore: fixing cypress build * microsite: Fix color contrast for pre tags * Add package name to lockfile.ts error * Create new-mangos-tap.md * Changed changeset bump to be patch * Pass registered Logger in ServiceBuilderImpl to requestLoggingHandler `requestLoggingHandler` takes an optional `logger` parameter that it can use to log incoming requests. The `ServiceBuilderImpl` was not passing on this logger, if set, to `requestLoggingHandler` middleware * Add indices on columns referring location(id) * Add changeset * Catch catalog errors * Add changeset * Fixed parseUrl to output catalogPaths beginning with '/' * Add configurable OAuth 2.0 scopes - Add oauth2 config for optional scopes - Document oauth2 config keys - Add OAuth2 to demo app list of identity providers * incorrectly added callbackUrl * chore: added a simple readme to run some simple tests against a backstage instance * chore(deps): bump @svgr/plugin-jsx from 5.4.0 to 5.5.0 Bumps [@svgr/plugin-jsx](https://github.com/gregberge/svgr) from 5.4.0 to 5.5.0. - [Release notes](https://github.com/gregberge/svgr/releases) - [Changelog](https://github.com/gregberge/svgr/blob/main/CHANGELOG.md) - [Commits](https://github.com/gregberge/svgr/compare/v5.4.0...v5.5.0) Signed-off-by: dependabot[bot] * chore(deps): bump @types/cors from 2.8.6 to 2.8.9 Bumps [@types/cors](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/cors) from 2.8.6 to 2.8.9. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/cors) Signed-off-by: dependabot[bot] * fix: removing test e2e-test tsconfig.json file * TechDocs: Preparers will take and return an etag for cache invalidation * TechDocs: Implement etag based caching for url preparer Signed-off-by: Himanshu Mishra * TechDocs: Implement etag based caching for git preparer Signed-off-by: Himanshu Mishra * TechDocs: Warn when using legacy git preparer and dir preparer in backstage.io/techdocs-ref Context: https://github.com/backstage/backstage/issues/4409 * TechDocs: Update tests for proper url preparer caching * TechDocs: Deduplicate git clone for cache check Signed-off-by: Himanshu Mishra * TechDocs: Add changesets for deprecation and proper caching * TechDocs: Implement caching for dir preparer * Update the changesets * scaffolder-backend: remove auth-backend dependency * catalog-client: rename ApiContext to CatalogRequestOptions + avoid export * refactor existing usage of ApiContext * techdocs: don't swallow errors other than NotModifiedError * catalog-import: removed bonus code * techdocs: meaningful logs when readTree starts * Flatten the options of the CatalogImportPage * inline optional id token auth headers * Define relationship to software catalog and loose coupling by convention. * Clarify intentions around bulk vs. incremental index management. * Break apart backend plugins & clarify indexer/plugin relationship. * update changeset bump levels * [Search] documentation update (#4459) * delete link to issue as it is closed * replace usage of easy as its very subjective * Update docs/features/search/architecture.md Co-authored-by: Adam Harvey * prettier....: Co-authored-by: Adam Harvey * Own it. * scaffolder: include backstage identity token in requests * review feedback tweaks * docs/apis: update to use named plugin var * sentry: update plugin instance export name * clear other field when toggling reason * changeset * fix(pagerduty): use the luxon date library * Don't pass default as a scope to OIDC providers * docs(TechDocs): Add GitHub Actions CI example with AWS S3 * tech-radar: migrated to new composability API * cost-insights: migrate to new composability API * Changeset * Fix line endings * Prettier * Remove unnecessary scopes from OIDC defaultApi * Bump plugin-auth-backend to a minor change, add documentation for fixing it * Fix md syntax error * Fix Vale spelling error * Remove defaultScopes from OIDC api * [ImgBot] Optimize images /plugins/catalog-import/docs/catalog-import-screenshot.png -- 613.39kb -> 356.48kb (41.88%) Signed-off-by: ImgBotApp * github/workflows: use lax config checks * Use commented-out example value for scope * feat(pagerduty): add changeset * Apply suggestions from code review Co-authored-by: Rémi Doreau <32459935+ayshiff@users.noreply.github.com> * feat(catalog): add entity links card component * update entity links changeset * Fix prettier * chore(deps-dev): bump @types/http-proxy from 1.17.4 to 1.17.5 Bumps [@types/http-proxy](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/http-proxy) from 1.17.4 to 1.17.5. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/http-proxy) Signed-off-by: dependabot[bot] * Revert "Merge pull request #3953 from ayshiff/feature/techdocs-aws-sdkv3" This reverts commit 1c7771871ec46bf55f87fba5991b95938bb57017, reversing changes made to 65d5c8e6fe4364d036d69a0592c87a9037f95b8b. * Revert "Merge pull request #4035 from ayshiff/feature/catalog-backend-aws-sdk-v3" This reverts commit 1df913464889cdf63a1484b06538b76a8baed495, reversing changes made to d45a5100695fb973ae38f74f23bbced9a0101840. * Add changeset * feat: support custom app icons * backend-common: implement UrlReader.search for the other providers too * Remove changeset * docs(TechDocs): Improvements to the CI example Signed-off-by: Himanshu Mishra * fix(catalog): entity links incorrect wrapping * Remove unused functions and luxon * Removed old imports * Fix broken links in documentation (#4418) * Update IdentityApi.md * Update docs/reference/utility-apis/IdentityApi.md * Update docs/reference/utility-apis/IdentityApi.md * ran yarn docgen * Use JS Date instead of Datetime * Add line * Prettier fix * Move parseDate function * Version Packages * chore: fix the create-app version * Limit the props that are forwarded to the Link component in the EntityRefLink * Use routed tabs to link to every settings page * fix up yarn after release * chore: add lockfile to words * catalog-info: add links * Export Select component from core I saw the Select component on storybook and went to use it but it seems it's not exported. Any chance it could be exported? * bug: filepath can be returned as undefined from `git-url-parse` let's default to empty * chore: might as well do this for all parsing * chore: changeset * Add changeset for fixing requestLoggingHandler * chore(deps-dev): bump @storybook/addon-actions from 6.1.11 to 6.1.17 Bumps [@storybook/addon-actions](https://github.com/storybookjs/storybook/tree/HEAD/addons/actions) from 6.1.11 to 6.1.17. - [Release notes](https://github.com/storybookjs/storybook/releases) - [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md) - [Commits](https://github.com/storybookjs/storybook/commits/v6.1.17/addons/actions) Signed-off-by: dependabot[bot] * Port AboutCard * Fix tests * Revert EntityPage changes * docs: Merge auth glossary with main glossary * Remove the "Move repository" menu entry from the catalog page, as it's just a placeholder It will be easy to bring it back later, but for now it just confuses users that it's not doing anything. It's also hard to remove for integrators. * make the template cards conform to mui standard Co-authored-by: Erik Larsson Co-authored-by: Dominik Henneke Co-authored-by: Nils Streijffert Co-authored-by: Nils Streijffert Co-authored-by: Himanshu Mishra Co-authored-by: blam Co-authored-by: Nir Gazit Co-authored-by: Adam Harvey Co-authored-by: NHI TRAN Co-authored-by: Fredrik Adelöw Co-authored-by: Oliver Sand Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Debajyoti Halder Co-authored-by: Patrik Oldsberg Co-authored-by: Gowind Co-authored-by: Alan Crosswell Co-authored-by: Eric Peterson Co-authored-by: Emma Indal Co-authored-by: Ryan Vazquez Co-authored-by: Ryan Vazquez Co-authored-by: Remi Co-authored-by: Ryan Manny Co-authored-by: ImgBotApp Co-authored-by: Rémi Doreau <32459935+ayshiff@users.noreply.github.com> Co-authored-by: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Co-authored-by: Joel Low Co-authored-by: Eric Peterson Co-authored-by: github-actions[bot] Co-authored-by: Iain Billett From b9709efff894a85078a2220ca7c2024b2956eec4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Feb 2021 17:42:14 +0100 Subject: [PATCH 17/34] auth-backend: refactor user profile fetch to transform profile outside --- .../src/lib/passport/PassportStrategyHelper.ts | 10 ++++------ plugins/auth-backend/src/providers/auth0/provider.ts | 4 ++-- plugins/auth-backend/src/providers/google/provider.ts | 4 ++-- .../auth-backend/src/providers/microsoft/provider.ts | 4 ++-- plugins/auth-backend/src/providers/oauth2/provider.ts | 4 ++-- plugins/auth-backend/src/providers/okta/provider.ts | 4 ++-- .../auth-backend/src/providers/onelogin/provider.ts | 4 ++-- 7 files changed, 16 insertions(+), 18 deletions(-) diff --git a/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts index 28f15ee9e8..079f44151a 100644 --- a/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts @@ -190,19 +190,17 @@ type ProviderStrategy = { export const executeFetchUserProfileStrategy = async ( providerStrategy: passport.Strategy, accessToken: string, - idToken?: string, -): Promise => { +): Promise => { return new Promise((resolve, reject) => { const anyStrategy = (providerStrategy as unknown) as ProviderStrategy; anyStrategy.userProfile( accessToken, - (error: Error, passportProfile: passport.Profile) => { + (error: Error, rawProfile: passport.Profile) => { if (error) { reject(error); + } else { + resolve(rawProfile); } - - const profile = makeProfileInfo(passportProfile, idToken); - resolve(profile); }, ); }); diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index 89f4f73300..1244820139 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -114,11 +114,11 @@ export class Auth0AuthProvider implements OAuthHandlers { req.scope, ); - const profile = await executeFetchUserProfileStrategy( + const rawProfile = await executeFetchUserProfileStrategy( this._strategy, accessToken, - params.id_token, ); + const profile = makeProfileInfo(rawProfile, params.id_token); return this.populateIdentity({ providerInfo: { diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index fa42988c98..12d882cbb0 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -127,11 +127,11 @@ export class GoogleAuthProvider implements OAuthHandlers { req.scope, ); - const profile = await executeFetchUserProfileStrategy( + const rawProfile = await executeFetchUserProfileStrategy( this._strategy, accessToken, - params.id_token, ); + const profile = makeProfileInfo(rawProfile, params.id_token); return this.populateIdentity({ providerInfo: { diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index 57f2ffcbd5..7b110e5f1a 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -142,11 +142,11 @@ export class MicrosoftAuthProvider implements OAuthHandlers { req.scope, ); - const profile = await executeFetchUserProfileStrategy( + const rawProfile = await executeFetchUserProfileStrategy( this._strategy, accessToken, - params.id_token, ); + const profile = makeProfileInfo(rawProfile, params.id_token); const photo = await this.getUserPhoto(accessToken); if (photo) { profile.picture = photo; diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index 93f9fad2aa..a5d3f96fdf 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -124,11 +124,11 @@ export class OAuth2AuthProvider implements OAuthHandlers { refreshToken: updatedRefreshToken, } = refreshTokenResponse; - const profile = await executeFetchUserProfileStrategy( + const rawProfile = await executeFetchUserProfileStrategy( this._strategy, accessToken, - params.id_token, ); + const profile = makeProfileInfo(rawProfile, params.id_token); return this.populateIdentity({ providerInfo: { diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index ba2c594550..fbca780571 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -134,11 +134,11 @@ export class OktaAuthProvider implements OAuthHandlers { req.scope, ); - const profile = await executeFetchUserProfileStrategy( + const rawProfile = await executeFetchUserProfileStrategy( this._strategy, accessToken, - params.id_token, ); + const profile = makeProfileInfo(rawProfile, params.id_token); return this.populateIdentity({ providerInfo: { diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index d23a48776e..5549db06c6 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -114,11 +114,11 @@ export class OneLoginProvider implements OAuthHandlers { req.scope, ); - const profile = await executeFetchUserProfileStrategy( + const rawProfile = await executeFetchUserProfileStrategy( this._strategy, accessToken, - params.id_token, ); + const profile = makeProfileInfo(rawProfile, params.id_token); return this.populateIdentity({ providerInfo: { From 65fa16dee469792b1c6e890ed9c8f7291c2f5b58 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 14 Feb 2021 17:38:07 +0100 Subject: [PATCH 18/34] auth-backend: refactor profile transform to happen within handlers instead of verify callback --- plugins/auth-backend/src/lib/oauth/index.ts | 1 + plugins/auth-backend/src/lib/oauth/types.ts | 12 ++ .../passport/PassportStrategyHelper.test.ts | 2 +- .../lib/passport/PassportStrategyHelper.ts | 10 +- .../src/providers/auth0/provider.ts | 37 +++--- .../src/providers/github/provider.test.ts | 87 ++++++++------ .../src/providers/github/provider.ts | 94 +++++---------- .../src/providers/gitlab/provider.test.ts | 35 ++++-- .../src/providers/gitlab/provider.ts | 112 +++++++----------- .../src/providers/google/provider.ts | 37 +++--- .../src/providers/microsoft/provider.ts | 83 ++++++------- .../src/providers/oauth2/provider.ts | 34 +++--- .../src/providers/oidc/provider.ts | 56 +++++---- .../src/providers/okta/provider.ts | 38 +++--- .../src/providers/onelogin/provider.ts | 38 +++--- .../src/providers/saml/provider.ts | 34 +++--- 16 files changed, 351 insertions(+), 359 deletions(-) diff --git a/plugins/auth-backend/src/lib/oauth/index.ts b/plugins/auth-backend/src/lib/oauth/index.ts index 564a0e1e7c..bf3e0658e4 100644 --- a/plugins/auth-backend/src/lib/oauth/index.ts +++ b/plugins/auth-backend/src/lib/oauth/index.ts @@ -25,4 +25,5 @@ export type { OAuthState, OAuthStartRequest, OAuthRefreshRequest, + OAuthResult, } from './types'; diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts index b2b7915a4e..dd477388bf 100644 --- a/plugins/auth-backend/src/lib/oauth/types.ts +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -15,6 +15,7 @@ */ import express from 'express'; +import { Profile as PassportProfile } from 'passport'; import { AuthResponse, RedirectInfo } from '../../providers/types'; /** @@ -35,6 +36,17 @@ export type OAuthProviderOptions = { callbackUrl: string; }; +export type OAuthResult = { + fullProfile: PassportProfile; + params: { + id_token?: string; + scope: string; + expires_in: number; + }; + accessToken: string; + refreshToken?: string; +}; + export type OAuthResponse = AuthResponse; export type OAuthProviderInfo = { diff --git a/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.test.ts b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.test.ts index f7a22e9ad9..c27eebbd9a 100644 --- a/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.test.ts +++ b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.test.ts @@ -87,7 +87,7 @@ describe('PassportStrategyHelper', () => { expect(spyAuthenticate).toBeCalledTimes(1); await expect(frameHandlerStrategyPromise).resolves.toStrictEqual( expect.objectContaining({ - response: { accessToken: 'ACCESS_TOKEN' }, + result: { accessToken: 'ACCESS_TOKEN' }, privateInfo: { refreshToken: 'REFRESH_TOKEN' }, }), ); diff --git a/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts index 079f44151a..8819da0a2f 100644 --- a/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts @@ -39,7 +39,7 @@ export const makeProfileInfo = ( } let picture: string | undefined = undefined; - if (profile.photos) { + if (profile.photos && profile.photos.length > 0) { const [firstPhoto] = profile.photos; picture = firstPhoto.value; } @@ -80,15 +80,15 @@ export const executeRedirectStrategy = async ( }); }; -export const executeFrameHandlerStrategy = async ( +export const executeFrameHandlerStrategy = async ( req: express.Request, providerStrategy: passport.Strategy, ) => { - return new Promise<{ response: T; privateInfo: PrivateInfo }>( + return new Promise<{ result: Result; privateInfo: PrivateInfo }>( (resolve, reject) => { const strategy = Object.create(providerStrategy); - strategy.success = (response: any, privateInfo: any) => { - resolve({ response, privateInfo }); + strategy.success = (result: any, privateInfo: any) => { + resolve({ result, privateInfo }); }; strategy.fail = ( info: { type: 'success' | 'error'; message?: string }, diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index 1244820139..2a2f743d12 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -26,6 +26,7 @@ import { OAuthStartRequest, encodeState, OAuthRefreshRequest, + OAuthResult, } from '../../lib/oauth'; import { executeFetchUserProfileStrategy, @@ -61,20 +62,16 @@ export class Auth0AuthProvider implements OAuthHandlers { accessToken: any, refreshToken: any, params: any, - rawProfile: passport.Profile, - done: PassportDoneCallback, + fullProfile: passport.Profile, + done: PassportDoneCallback, ) => { - const profile = makeProfileInfo(rawProfile, params.id_token); done( undefined, { - providerInfo: { - idToken: params.id_token, - accessToken, - scope: params.scope, - expiresInSeconds: params.expires_in, - }, - profile, + fullProfile, + accessToken, + refreshToken, + params, }, { refreshToken, @@ -96,13 +93,23 @@ export class Auth0AuthProvider implements OAuthHandlers { async handler( req: express.Request, ): Promise<{ response: OAuthResponse; refreshToken: string }> { - const { response, privateInfo } = await executeFrameHandlerStrategy< - OAuthResponse, + const { result, privateInfo } = await executeFrameHandlerStrategy< + OAuthResult, PrivateInfo >(req, this._strategy); + const profile = makeProfileInfo(result.fullProfile, result.params.id_token); + return { - response: await this.populateIdentity(response), + response: await this.populateIdentity({ + profile, + providerInfo: { + idToken: result.params.id_token, + accessToken: result.accessToken, + scope: result.params.scope, + expiresInSeconds: result.params.expires_in, + }, + }), refreshToken: privateInfo.refreshToken, }; } @@ -114,11 +121,11 @@ export class Auth0AuthProvider implements OAuthHandlers { req.scope, ); - const rawProfile = await executeFetchUserProfileStrategy( + const fullProfile = await executeFetchUserProfileStrategy( this._strategy, accessToken, ); - const profile = makeProfileInfo(rawProfile, params.id_token); + const profile = makeProfileInfo(fullProfile, params.id_token); return this.populateIdentity({ providerInfo: { diff --git a/plugins/auth-backend/src/providers/github/provider.test.ts b/plugins/auth-backend/src/providers/github/provider.test.ts index 61d4bb887f..6394ed9f2a 100644 --- a/plugins/auth-backend/src/providers/github/provider.test.ts +++ b/plugins/auth-backend/src/providers/github/provider.test.ts @@ -14,13 +14,31 @@ * limitations under the License. */ +import { Profile as PassportProfile } from 'passport'; import { GithubAuthProvider } from './provider'; +import * as helpers from '../../lib/passport'; +import { OAuthResult } from '../../lib/oauth'; + +const mockFrameHandler = (jest.spyOn( + helpers, + 'executeFrameHandlerStrategy', +) as unknown) as jest.MockedFunction< + () => Promise<{ + result: Omit & { params: { scope: string } }; + }> +>; describe('GithubAuthProvider', () => { + const provider = new GithubAuthProvider({ + callbackUrl: 'mock', + clientId: 'mock', + clientSecret: 'mock', + }); + describe('should transform to type OAuthResponse', () => { - it('when all fields are present, it should be able to map them', () => { + it('when all fields are present, it should be able to map them', async () => { const accessToken = '19xasczxcm9n7gacn9jdgm19me'; - const rawProfile = { + const fullProfile = { id: 'uid-123', username: 'jimmymarkum', provider: 'github', @@ -59,18 +77,17 @@ describe('GithubAuthProvider', () => { 'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg', }, }; - expect( - GithubAuthProvider.transformOAuthResponse( - accessToken, - rawProfile, - params, - ), - ).toEqual(expected); + + mockFrameHandler.mockResolvedValueOnce({ + result: { fullProfile, accessToken, params }, + }); + const { response } = await provider.handler({} as any); + expect(response).toEqual(expected); }); - it('when "email" is missing, it should be able to create the profile without it', () => { + it('when "email" is missing, it should be able to create the profile without it', async () => { const accessToken = '19xasczxcm9n7gacn9jdgm19me'; - const rawProfile = { + const fullProfile = ({ id: 'uid-123', username: 'jimmymarkum', provider: 'github', @@ -82,7 +99,7 @@ describe('GithubAuthProvider', () => { 'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg', }, ], - }; + } as unknown) as PassportProfile; const params = { scope: 'read:scope', @@ -105,18 +122,16 @@ describe('GithubAuthProvider', () => { }, }; - expect( - GithubAuthProvider.transformOAuthResponse( - accessToken, - rawProfile, - params, - ), - ).toEqual(expected); + mockFrameHandler.mockResolvedValueOnce({ + result: { fullProfile, accessToken, params }, + }); + const { response } = await provider.handler({} as any); + expect(response).toEqual(expected); }); - it('when "displayName" is missing, it should be able to create the profile and map "displayName" with "username"', () => { + it('when "displayName" is missing, it should be able to create the profile and map "displayName" with "username"', async () => { const accessToken = '19xasczxcm9n7gacn9jdgm19me'; - const rawProfile = { + const fullProfile = ({ id: 'uid-123', username: 'jimmymarkum', provider: 'github', @@ -128,7 +143,7 @@ describe('GithubAuthProvider', () => { 'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg', }, ], - }; + } as unknown) as PassportProfile; const params = { scope: 'read:scope', @@ -150,19 +165,17 @@ describe('GithubAuthProvider', () => { }, }; - expect( - GithubAuthProvider.transformOAuthResponse( - accessToken, - rawProfile, - params, - ), - ).toEqual(expected); + mockFrameHandler.mockResolvedValueOnce({ + result: { fullProfile, accessToken, params }, + }); + const { response } = await provider.handler({} as any); + expect(response).toEqual(expected); }); - it('when "photos" is missing, it should be able to create the profile without it', () => { + it('when "photos" is missing, it should be able to create the profile without it', async () => { const accessToken = 'ajakljsdoiahoawxbrouawucmbawe.awkxjemaneasdxwe.sodijxqeqwexeqwxe'; - const rawProfile = { + const fullProfile = { id: 'ipd12039', username: 'daveboyle', provider: 'gitlab', @@ -195,13 +208,11 @@ describe('GithubAuthProvider', () => { }, }; - expect( - GithubAuthProvider.transformOAuthResponse( - accessToken, - rawProfile, - params, - ), - ).toEqual(expected); + mockFrameHandler.mockResolvedValueOnce({ + result: { fullProfile, accessToken, params }, + }); + const { response } = await provider.handler({} as any); + expect(response).toEqual(expected); }); }); }); diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 47d17fdac2..438cbca07b 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -27,12 +27,11 @@ import { OAuthAdapter, OAuthProviderOptions, OAuthHandlers, - OAuthResponse, OAuthEnvironmentHandler, OAuthStartRequest, encodeState, + OAuthResult, } from '../../lib/oauth'; -import passport from 'passport'; export type GithubAuthProviderOptions = OAuthProviderOptions & { tokenUrl?: string; @@ -43,55 +42,6 @@ export type GithubAuthProviderOptions = OAuthProviderOptions & { export class GithubAuthProvider implements OAuthHandlers { private readonly _strategy: GithubStrategy; - static transformPassportProfile(rawProfile: any): passport.Profile { - const profile: passport.Profile = { - id: rawProfile.username, - username: rawProfile.username, - provider: rawProfile.provider, - displayName: rawProfile.displayName || rawProfile.username, - photos: rawProfile.photos, - emails: rawProfile.emails, - }; - - return profile; - } - - static transformOAuthResponse( - accessToken: string, - rawProfile: any, - params: any = {}, - ): OAuthResponse { - const passportProfile = GithubAuthProvider.transformPassportProfile( - rawProfile, - ); - - const profile = makeProfileInfo(passportProfile, params.id_token); - const providerInfo = { - accessToken, - scope: params.scope, - expiresInSeconds: params.expires_in, - idToken: params.id_token, - }; - - // GitHub provides an id numeric value (123) - // as a fallback - const id = passportProfile!.id; - - if (params.expires_in) { - providerInfo.expiresInSeconds = params.expires_in; - } - if (params.id_token) { - providerInfo.idToken = params.id_token; - } - return { - providerInfo, - profile, - backstageIdentity: { - id, - }, - }; - } - constructor(options: GithubAuthProviderOptions) { this._strategy = new GithubStrategy( { @@ -104,17 +54,12 @@ export class GithubAuthProvider implements OAuthHandlers { }, ( accessToken: any, - _: any, + refreshToken: any, params: any, - rawProfile: any, - done: PassportDoneCallback, + fullProfile: any, + done: PassportDoneCallback, ) => { - const oauthResponse = GithubAuthProvider.transformOAuthResponse( - accessToken, - rawProfile, - params, - ); - done(undefined, oauthResponse); + done(undefined, { fullProfile, params, accessToken, refreshToken }); }, ); } @@ -127,12 +72,33 @@ export class GithubAuthProvider implements OAuthHandlers { } async handler(req: express.Request) { - const { response } = await executeFrameHandlerStrategy( - req, - this._strategy, + const { + result: { fullProfile, accessToken, params }, + } = await executeFrameHandlerStrategy(req, this._strategy); + + const profile = makeProfileInfo( + { + ...fullProfile, + id: fullProfile.username || fullProfile.id, + displayName: + fullProfile.displayName || fullProfile.username || fullProfile.id, + }, + params.id_token, ); - return { response }; + return { + response: { + profile, + providerInfo: { + accessToken, + scope: params.scope, + expiresInSeconds: params.expires_in, + }, + backstageIdentity: { + id: fullProfile.username || fullProfile.id, + }, + }, + }; } } diff --git a/plugins/auth-backend/src/providers/gitlab/provider.test.ts b/plugins/auth-backend/src/providers/gitlab/provider.test.ts index d4d1de17ec..b57d846f23 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.test.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.test.ts @@ -15,14 +15,21 @@ */ import { GitlabAuthProvider } from './provider'; +import * as helpers from '../../lib/passport'; +import { OAuthResult } from '../../lib/oauth'; + +const mockFrameHandler = (jest.spyOn( + helpers, + 'executeFrameHandlerStrategy', +) as unknown) as jest.MockedFunction<() => Promise<{ result: OAuthResult }>>; describe('GitlabAuthProvider', () => { - it('should transform to type OAuthResponse', () => { + it('should transform to type OAuthResponse', async () => { const tests = [ { - arguments: { + result: { accessToken: '19xasczxcm9n7gacn9jdgm19me', - rawProfile: { + fullProfile: { id: 'uid-123', username: 'jimmymarkum', provider: 'gitlab', @@ -58,10 +65,10 @@ describe('GitlabAuthProvider', () => { }, }, { - arguments: { + result: { accessToken: 'ajakljsdoiahoawxbrouawucmbawe.awkxjemaneasdxwe.sodijxqeqwexeqwxe', - rawProfile: { + fullProfile: { id: 'ipd12039', username: 'daveboyle', provider: 'gitlab', @@ -74,6 +81,7 @@ describe('GitlabAuthProvider', () => { }, params: { scope: 'read_repository', + expires_in: 200, }, }, expect: { @@ -83,6 +91,7 @@ describe('GitlabAuthProvider', () => { providerInfo: { accessToken: 'ajakljsdoiahoawxbrouawucmbawe.awkxjemaneasdxwe.sodijxqeqwexeqwxe', + expiresInSeconds: 200, scope: 'read_repository', }, profile: { @@ -93,14 +102,16 @@ describe('GitlabAuthProvider', () => { }, ]; + const provider = new GitlabAuthProvider({ + clientId: 'mock', + clientSecret: 'mock', + callbackUrl: 'mock', + baseUrl: 'mock', + }); for (const test of tests) { - expect( - GitlabAuthProvider.transformOAuthResponse( - test.arguments.accessToken, - test.arguments.rawProfile, - test.arguments.params, - ), - ).toEqual(test.expect); + mockFrameHandler.mockResolvedValueOnce({ result: test.result }); + const { response } = await provider.handler({} as any); + expect(response).toEqual(test.expect); } }); }); diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 05a1ab2efe..7c24757833 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -31,8 +31,8 @@ import { OAuthEnvironmentHandler, OAuthStartRequest, encodeState, + OAuthResult, } from '../../lib/oauth'; -import passport from 'passport'; export type GitlabAuthProviderOptions = OAuthProviderOptions & { baseUrl: string; @@ -41,64 +41,6 @@ export type GitlabAuthProviderOptions = OAuthProviderOptions & { export class GitlabAuthProvider implements OAuthHandlers { private readonly _strategy: GitlabStrategy; - static transformPassportProfile(rawProfile: any): passport.Profile { - const profile: passport.Profile = { - id: rawProfile.id, - username: rawProfile.username, - provider: rawProfile.provider, - displayName: rawProfile.displayName, - }; - - if (rawProfile.emails && rawProfile.emails.length > 0) { - profile.emails = rawProfile.emails; - } - if (rawProfile.avatarUrl) { - profile.photos = [{ value: rawProfile.avatarUrl }]; - } - - return profile; - } - - static transformOAuthResponse( - accessToken: string, - rawProfile: any, - params: any = {}, - ): OAuthResponse { - const passportProfile = GitlabAuthProvider.transformPassportProfile( - rawProfile, - ); - - const profile = makeProfileInfo(passportProfile, params.id_token); - const providerInfo = { - accessToken, - scope: params.scope, - expiresInSeconds: params.expires_in, - idToken: params.id_token, - }; - - // gitlab provides an id numeric value (123) - // as a fallback - let id = passportProfile!.id; - - if (profile.email) { - id = profile.email.split('@')[0]; - } - - if (params.expires_in) { - providerInfo.expiresInSeconds = params.expires_in; - } - if (params.id_token) { - providerInfo.idToken = params.id_token; - } - return { - providerInfo, - profile, - backstageIdentity: { - id, - }, - }; - } - constructor(options: GitlabAuthProviderOptions) { this._strategy = new GitlabStrategy( { @@ -109,17 +51,12 @@ export class GitlabAuthProvider implements OAuthHandlers { }, ( accessToken: any, - _: any, + refreshToken: any, params: any, - rawProfile: any, - done: PassportDoneCallback, + fullProfile: any, + done: PassportDoneCallback, ) => { - const oauthResponse = GitlabAuthProvider.transformOAuthResponse( - accessToken, - rawProfile, - params, - ); - done(undefined, oauthResponse); + done(undefined, { fullProfile, params, accessToken, refreshToken }); }, ); } @@ -132,10 +69,47 @@ export class GitlabAuthProvider implements OAuthHandlers { } async handler(req: express.Request): Promise<{ response: OAuthResponse }> { - return await executeFrameHandlerStrategy( + const { result } = await executeFrameHandlerStrategy( req, this._strategy, ); + const { accessToken, params } = result; + const fullProfile = result.fullProfile as OAuthResult['fullProfile'] & { + avatarUrl?: string; + }; + + const profile = makeProfileInfo( + { + ...fullProfile, + photos: [ + ...(fullProfile.photos ?? []), + ...(fullProfile.avatarUrl ? [{ value: fullProfile.avatarUrl }] : []), + ], + }, + params.id_token, + ); + + // gitlab provides an id numeric value (123) + // as a fallback + let id = fullProfile.id; + if (profile.email) { + id = profile.email.split('@')[0]; + } + + return { + response: { + profile, + providerInfo: { + accessToken, + scope: params.scope, + expiresInSeconds: params.expires_in, + idToken: params.id_token, + }, + backstageIdentity: { + id, + }, + }, + }; } } diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 12d882cbb0..a00644a90d 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -28,6 +28,7 @@ import { OAuthRefreshRequest, OAuthResponse, OAuthStartRequest, + OAuthResult, } from '../../lib/oauth'; import { executeFetchUserProfileStrategy, @@ -74,20 +75,16 @@ export class GoogleAuthProvider implements OAuthHandlers { accessToken: any, refreshToken: any, params: any, - rawProfile: passport.Profile, - done: PassportDoneCallback, + fullProfile: passport.Profile, + done: PassportDoneCallback, ) => { - const profile = makeProfileInfo(rawProfile, params.id_token); done( undefined, { - providerInfo: { - idToken: params.id_token, - accessToken, - scope: params.scope, - expiresInSeconds: params.expires_in, - }, - profile, + fullProfile, + params, + accessToken, + refreshToken, }, { refreshToken, @@ -109,13 +106,23 @@ export class GoogleAuthProvider implements OAuthHandlers { async handler( req: express.Request, ): Promise<{ response: OAuthResponse; refreshToken: string }> { - const { response, privateInfo } = await executeFrameHandlerStrategy< - OAuthResponse, + const { result, privateInfo } = await executeFrameHandlerStrategy< + OAuthResult, PrivateInfo >(req, this._strategy); + const profile = makeProfileInfo(result.fullProfile, result.params.id_token); + return { - response: await this.populateIdentity(response), + response: await this.populateIdentity({ + providerInfo: { + idToken: result.params.id_token, + accessToken: result.accessToken, + scope: result.params.scope, + expiresInSeconds: result.params.expires_in, + }, + profile, + }), refreshToken: privateInfo.refreshToken, }; } @@ -127,11 +134,11 @@ export class GoogleAuthProvider implements OAuthHandlers { req.scope, ); - const rawProfile = await executeFetchUserProfileStrategy( + const fullProfile = await executeFetchUserProfileStrategy( this._strategy, accessToken, ); - const profile = makeProfileInfo(rawProfile, params.id_token); + const profile = makeProfileInfo(fullProfile, params.id_token); return this.populateIdentity({ providerInfo: { diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index 7b110e5f1a..032af46804 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -38,6 +38,7 @@ import { OAuthStartRequest, encodeState, OAuthRefreshRequest, + OAuthResult, } from '../../lib/oauth'; import got from 'got'; @@ -54,32 +55,6 @@ export type MicrosoftAuthProviderOptions = OAuthProviderOptions & { export class MicrosoftAuthProvider implements OAuthHandlers { private readonly _strategy: MicrosoftStrategy; - static transformAuthResponse( - accessToken: string, - params: any, - rawProfile: any, - photoURL: any, - ): OAuthResponse { - let passportProfile: passport.Profile = rawProfile; - passportProfile = { - ...passportProfile, - photos: [{ value: photoURL }], - }; - - const profile = makeProfileInfo(passportProfile, params.id_token); - const providerInfo = { - idToken: params.id_token, - accessToken, - scope: params.scope, - expiresInSeconds: params.expires_in, - }; - - return { - providerInfo, - profile, - }; - } - constructor(options: MicrosoftAuthProviderOptions) { this._strategy = new MicrosoftStrategy( { @@ -94,22 +69,10 @@ export class MicrosoftAuthProvider implements OAuthHandlers { accessToken: any, refreshToken: any, params: any, - rawProfile: passport.Profile, - done: PassportDoneCallback, + fullProfile: passport.Profile, + done: PassportDoneCallback, ) => { - this.getUserPhoto(accessToken) - .then(photoURL => { - const authResponse = MicrosoftAuthProvider.transformAuthResponse( - accessToken, - params, - rawProfile, - photoURL, - ); - done(undefined, authResponse, { refreshToken }); - }) - .catch(error => { - throw new Error(`Error processing auth response: ${error}`); - }); + done(undefined, { fullProfile, accessToken, refreshToken, params }); }, ); } @@ -124,15 +87,37 @@ export class MicrosoftAuthProvider implements OAuthHandlers { async handler( req: express.Request, ): Promise<{ response: OAuthResponse; refreshToken: string }> { - const { response, privateInfo } = await executeFrameHandlerStrategy< - OAuthResponse, + const { result, privateInfo } = await executeFrameHandlerStrategy< + OAuthResult, PrivateInfo >(req, this._strategy); - return { - response: await this.populateIdentity(response), - refreshToken: privateInfo.refreshToken, - }; + try { + const photoUrl = await this.getUserPhoto(result.accessToken); + + const profile = makeProfileInfo( + { + ...result.fullProfile, + photos: photoUrl ? [{ value: photoUrl }] : undefined, + }, + result.params.id_token, + ); + + return { + response: await this.populateIdentity({ + profile, + providerInfo: { + idToken: result.params.id_token, + accessToken: result.accessToken, + scope: result.params.scope, + expiresInSeconds: result.params.expires_in, + }, + }), + refreshToken: privateInfo.refreshToken, + }; + } catch (error) { + throw new Error(`Error processing auth response: ${error}`); + } } async refresh(req: OAuthRefreshRequest): Promise { @@ -142,11 +127,11 @@ export class MicrosoftAuthProvider implements OAuthHandlers { req.scope, ); - const rawProfile = await executeFetchUserProfileStrategy( + const fullProfile = await executeFetchUserProfileStrategy( this._strategy, accessToken, ); - const profile = makeProfileInfo(rawProfile, params.id_token); + const profile = makeProfileInfo(fullProfile, params.id_token); const photo = await this.getUserPhoto(accessToken); if (photo) { profile.picture = photo; diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index a5d3f96fdf..fb347b836c 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -26,6 +26,7 @@ import { OAuthStartRequest, encodeState, OAuthRefreshRequest, + OAuthResult, } from '../../lib/oauth'; import { executeFetchUserProfileStrategy, @@ -65,21 +66,16 @@ export class OAuth2AuthProvider implements OAuthHandlers { accessToken: any, refreshToken: any, params: any, - rawProfile: passport.Profile, - done: PassportDoneCallback, + fullProfile: passport.Profile, + done: PassportDoneCallback, ) => { - const profile = makeProfileInfo(rawProfile, params.id_token); - done( undefined, { - providerInfo: { - idToken: params.id_token, - accessToken, - scope: params.scope, - expiresInSeconds: params.expires_in, - }, - profile, + fullProfile, + accessToken, + refreshToken, + params, }, { refreshToken, @@ -101,13 +97,23 @@ export class OAuth2AuthProvider implements OAuthHandlers { async handler( req: express.Request, ): Promise<{ response: OAuthResponse; refreshToken: string }> { - const { response, privateInfo } = await executeFrameHandlerStrategy< - OAuthResponse, + const { result, privateInfo } = await executeFrameHandlerStrategy< + OAuthResult, PrivateInfo >(req, this._strategy); + const profile = makeProfileInfo(result.fullProfile, result.params.id_token); + return { - response: await this.populateIdentity(response), + response: await this.populateIdentity({ + profile, + providerInfo: { + idToken: result.params.id_token, + accessToken: result.accessToken, + scope: result.params.scope, + expiresInSeconds: result.params.expires_in, + }, + }), refreshToken: privateInfo.refreshToken, }; } diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index b1bedd0f54..0ec8d0bbf7 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -37,10 +37,10 @@ import { executeRedirectStrategy, PassportDoneCallback, } from '../../lib/passport'; -import { RedirectInfo, AuthProviderFactory, ProfileInfo } from '../types'; +import { RedirectInfo, AuthProviderFactory } from '../types'; type PrivateInfo = { - refreshToken: string; + refreshToken?: string; }; type OidcImpl = { @@ -48,6 +48,11 @@ type OidcImpl = { client: Client; }; +type AuthResult = { + tokenset: TokenSet; + userinfo: UserinfoResponse; +}; + export type Options = OAuthProviderOptions & { metadataUrl: string; tokenSignedResponseAlg?: string; @@ -72,15 +77,30 @@ export class OidcAuthProvider implements OAuthHandlers { async handler( req: express.Request, - ): Promise<{ response: OAuthResponse; refreshToken: string }> { + ): Promise<{ response: OAuthResponse; refreshToken?: string }> { const { strategy } = await this.implementation; - const { response, privateInfo } = await executeFrameHandlerStrategy< - OAuthResponse, - PrivateInfo - >(req, strategy); + const { + result: { userinfo, tokenset }, + privateInfo, + } = await executeFrameHandlerStrategy( + req, + strategy, + ); return { - response: await this.populateIdentity(response), + response: await this.populateIdentity({ + profile: { + displayName: userinfo.name, + email: userinfo.email, + picture: userinfo.picture, + }, + providerInfo: { + idToken: tokenset.id_token, + accessToken: tokenset.access_token || '', + scope: tokenset.scope || '', + expiresInSeconds: tokenset.expires_in, + }, + }), refreshToken: privateInfo.refreshToken, }; } @@ -123,27 +143,13 @@ export class OidcAuthProvider implements OAuthHandlers { ( tokenset: TokenSet, userinfo: UserinfoResponse, - done: PassportDoneCallback, + done: PassportDoneCallback, ) => { - const profile: ProfileInfo = { - displayName: userinfo.name, - email: userinfo.email, - picture: userinfo.picture, - }; - done( undefined, + { tokenset, userinfo }, { - providerInfo: { - idToken: tokenset.id_token || '', - accessToken: tokenset.access_token || '', - scope: tokenset.scope || '', - expiresInSeconds: tokenset.expires_in, - }, - profile, - }, - { - refreshToken: tokenset.refresh_token || '', + refreshToken: tokenset.refresh_token, }, ); }, diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index fbca780571..6320230e67 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -23,6 +23,7 @@ import { OAuthStartRequest, encodeState, OAuthRefreshRequest, + OAuthResult, } from '../../lib/oauth'; import { Strategy as OktaStrategy } from 'passport-okta-oauth'; import passport from 'passport'; @@ -80,21 +81,16 @@ export class OktaAuthProvider implements OAuthHandlers { accessToken: any, refreshToken: any, params: any, - rawProfile: passport.Profile, - done: PassportDoneCallback, + fullProfile: passport.Profile, + done: PassportDoneCallback, ) => { - const profile = makeProfileInfo(rawProfile, params.id_token); - done( undefined, { - providerInfo: { - idToken: params.id_token, - accessToken, - scope: params.scope, - expiresInSeconds: params.expires_in, - }, - profile, + accessToken, + refreshToken, + params, + fullProfile, }, { refreshToken, @@ -116,13 +112,23 @@ export class OktaAuthProvider implements OAuthHandlers { async handler( req: express.Request, ): Promise<{ response: OAuthResponse; refreshToken: string }> { - const { response, privateInfo } = await executeFrameHandlerStrategy< - OAuthResponse, + const { result, privateInfo } = await executeFrameHandlerStrategy< + OAuthResult, PrivateInfo >(req, this._strategy); + const profile = makeProfileInfo(result.fullProfile, result.params.id_token); + return { - response: await this.populateIdentity(response), + response: await this.populateIdentity({ + profile, + providerInfo: { + idToken: result.params.id_token, + accessToken: result.accessToken, + scope: result.params.scope, + expiresInSeconds: result.params.expires_in, + }, + }), refreshToken: privateInfo.refreshToken, }; } @@ -134,11 +140,11 @@ export class OktaAuthProvider implements OAuthHandlers { req.scope, ); - const rawProfile = await executeFetchUserProfileStrategy( + const fullProfile = await executeFetchUserProfileStrategy( this._strategy, accessToken, ); - const profile = makeProfileInfo(rawProfile, params.id_token); + const profile = makeProfileInfo(fullProfile, params.id_token); return this.populateIdentity({ providerInfo: { diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index 5549db06c6..5cb0f84415 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -25,6 +25,7 @@ import { OAuthStartRequest, encodeState, OAuthRefreshRequest, + OAuthResult, } from '../../lib/oauth'; import passport from 'passport'; import { @@ -61,21 +62,16 @@ export class OneLoginProvider implements OAuthHandlers { accessToken: any, refreshToken: any, params: any, - rawProfile: passport.Profile, - done: PassportDoneCallback, + fullProfile: passport.Profile, + done: PassportDoneCallback, ) => { - const profile = makeProfileInfo(rawProfile, params.id_token); - done( undefined, { - providerInfo: { - idToken: params.id_token, - accessToken, - scope: params.scope, - expiresInSeconds: params.expires_in, - }, - profile, + accessToken, + refreshToken, + params, + fullProfile, }, { refreshToken, @@ -96,13 +92,23 @@ export class OneLoginProvider implements OAuthHandlers { async handler( req: express.Request, ): Promise<{ response: OAuthResponse; refreshToken: string }> { - const { response, privateInfo } = await executeFrameHandlerStrategy< - OAuthResponse, + const { result, privateInfo } = await executeFrameHandlerStrategy< + OAuthResult, PrivateInfo >(req, this._strategy); + const profile = makeProfileInfo(result.fullProfile, result.params.id_token); + return { - response: await this.populateIdentity(response), + response: await this.populateIdentity({ + profile, + providerInfo: { + idToken: result.params.id_token, + accessToken: result.accessToken, + scope: result.params.scope, + expiresInSeconds: result.params.expires_in, + }, + }), refreshToken: privateInfo.refreshToken, }; } @@ -114,11 +120,11 @@ export class OneLoginProvider implements OAuthHandlers { req.scope, ); - const rawProfile = await executeFetchUserProfileStrategy( + const fullProfile = await executeFetchUserProfileStrategy( this._strategy, accessToken, ); - const profile = makeProfileInfo(rawProfile, params.id_token); + const profile = makeProfileInfo(fullProfile, params.id_token); return this.populateIdentity({ providerInfo: { diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index caeb86aa6b..c3a91b3b77 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -26,17 +26,12 @@ import { executeRedirectStrategy, PassportDoneCallback, } from '../../lib/passport'; -import { - AuthProviderRouteHandlers, - ProfileInfo, - AuthProviderFactory, -} from '../types'; +import { AuthProviderRouteHandlers, AuthProviderFactory } from '../types'; import { postMessageResponse } from '../../lib/flow'; import { TokenIssuer } from '../../identity'; type SamlInfo = { - userId: string; - profile: ProfileInfo; + fullProfile: any; }; type Options = SamlConfig & { @@ -53,7 +48,7 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { this.appUrl = options.appUrl; this.tokenIssuer = options.tokenIssuer; this.strategy = new SamlStrategy({ ...options }, (( - profile: SamlProfile, + fullProfile: SamlProfile, done: PassportDoneCallback, ) => { // TODO: There's plenty more validation and profile handling to do here, @@ -61,13 +56,7 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { // for non-oauth auth flows. // TODO: This flow doesn't issue an identity token that can be used to validate // the identity of the user in other backends, which we need in some form. - done(undefined, { - userId: profile.nameID!, - profile: { - email: profile.email!, - displayName: profile.displayName as string, - }, - }); + done(undefined, { fullProfile }); }) as VerifyWithoutRequest); } @@ -81,11 +70,13 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { res: express.Response, ): Promise { try { - const { - response: { userId, profile }, - } = await executeFrameHandlerStrategy(req, this.strategy); + const { result } = await executeFrameHandlerStrategy( + req, + this.strategy, + ); + + const id = result.fullProfile.nameID; - const id = userId; const idToken = await this.tokenIssuer.issueToken({ claims: { sub: id }, }); @@ -93,8 +84,11 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { return postMessageResponse(res, this.appUrl, { type: 'authorization_response', response: { + profile: { + email: result.fullProfile.email, + displayName: result.fullProfile.displayName, + }, providerInfo: {}, - profile, backstageIdentity: { id, idToken }, }, }); From f9478cdd87d5a0082577f838d267cf017e6e281e Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Sun, 14 Feb 2021 19:29:41 +0100 Subject: [PATCH 19/34] chore: Use OS specific tmp dir to fix windows build https://github.com/backstage/backstage/runs/1891759720?check_suite_focus=true --- packages/backend-common/src/reading/GithubUrlReader.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend-common/src/reading/GithubUrlReader.test.ts b/packages/backend-common/src/reading/GithubUrlReader.test.ts index 72fbf83f6d..94e4d0fd31 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.test.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.test.ts @@ -273,7 +273,7 @@ describe('GithubUrlReader', () => { 'https://github.com/backstage/mock', ); - const dir = await response.dir({ targetDir: '/tmp' }); + const dir = await response.dir({ targetDir: tmpDir }); await expect( fs.readFile(path.join(dir, 'mkdocs.yml'), 'utf8'), @@ -354,7 +354,7 @@ describe('GithubUrlReader', () => { 'https://github.com/backstage/mock/tree/main/docs', ); - const dir = await response.dir({ targetDir: '/tmp' }); + const dir = await response.dir({ targetDir: tmpDir }); await expect( fs.readFile(path.join(dir, 'index.md'), 'utf8'), From 39144538500c7e9ead26e0660edc5e13a25a0362 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Sun, 14 Feb 2021 19:46:59 +0100 Subject: [PATCH 20/34] chore: TechDocs publisher does not work well with Windows paths --- packages/techdocs-common/__mocks__/aws-sdk.ts | 6 +----- .../src/stages/publish/awsS3.test.ts | 18 +++++++----------- .../src/stages/publish/awsS3.ts | 16 ++++++++-------- 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/packages/techdocs-common/__mocks__/aws-sdk.ts b/packages/techdocs-common/__mocks__/aws-sdk.ts index 987b3d2800..f0fb13c642 100644 --- a/packages/techdocs-common/__mocks__/aws-sdk.ts +++ b/packages/techdocs-common/__mocks__/aws-sdk.ts @@ -71,11 +71,7 @@ export class S3 { return { promise: () => new Promise((resolve, reject) => { - if (!fs.existsSync(Key)) { - reject(''); - } else { - resolve(''); - } + resolve(''); }), }; } diff --git a/packages/techdocs-common/src/stages/publish/awsS3.test.ts b/packages/techdocs-common/src/stages/publish/awsS3.test.ts index 813f6b8554..f8450a07a0 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.test.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.test.ts @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import type { Entity, EntityName } from '@backstage/catalog-model'; +import { ConfigReader } from '@backstage/config'; import mockFs from 'mock-fs'; import path from 'path'; import * as winston from 'winston'; -import { ConfigReader } from '@backstage/config'; import { AwsS3Publish } from './awsS3'; import { PublisherBase, TechDocsMetadata } from './types'; -import type { Entity, EntityName } from '@backstage/catalog-model'; const createMockEntity = (annotations = {}): Entity => { return { @@ -127,9 +127,9 @@ describe('AwsS3Publish', () => { directory: wrongPathToGeneratedDirectory, }) .catch(error => - expect(error).toEqual( - new Error( - `Unable to upload file(s) to AWS S3. Error Failed to read template directory: ENOENT, no such file or directory '${wrongPathToGeneratedDirectory}'`, + expect(error.message).toEqual( + expect.stringContaining( + 'Unable to upload file(s) to AWS S3. Error Failed to read template directory: ENOENT, no such file or directory', ), ), ); @@ -205,16 +205,12 @@ describe('AwsS3Publish', () => { it('should return an error if the techdocs_metadata.json file is not present', async () => { const entityNameMock = createMockEntityName(); - const entity = createMockEntity(); - const entityRootDir = getEntityRootDir(entity); await publisher .fetchTechDocsMetadata(entityNameMock) .catch(error => - expect(error).toEqual( - new Error( - `TechDocs metadata fetch failed, The file ${entityRootDir}/techdocs_metadata.json doest not exist !`, - ), + expect(error.message).toEqual( + expect.stringContaining('TechDocs metadata fetch'), ), ); }); diff --git a/packages/techdocs-common/src/stages/publish/awsS3.ts b/packages/techdocs-common/src/stages/publish/awsS3.ts index 8eaf03eb2f..6c0cadcfae 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.ts @@ -13,19 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import path from 'path'; -import express from 'express'; -import aws from 'aws-sdk'; -import { ManagedUpload } from 'aws-sdk/clients/s3'; -import { Logger } from 'winston'; import { Entity, EntityName } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { getHeadersForFileExtension, getFileTreeRecursively } from './helpers'; -import { PublisherBase, PublishRequest, TechDocsMetadata } from './types'; +import aws from 'aws-sdk'; +import { ManagedUpload } from 'aws-sdk/clients/s3'; +import express from 'express'; import fs from 'fs-extra'; -import { Readable } from 'stream'; import JSON5 from 'json5'; import createLimiter from 'p-limit'; +import path from 'path'; +import { Readable } from 'stream'; +import { Logger } from 'winston'; +import { getFileTreeRecursively, getHeadersForFileExtension } from './helpers'; +import { PublisherBase, PublishRequest, TechDocsMetadata } from './types'; const streamToBuffer = (stream: Readable): Promise => { return new Promise((resolve, reject) => { From 92f01d75c9f261880c5a20cd1271be6b48de2f90 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 14 Feb 2021 19:01:36 +0100 Subject: [PATCH 21/34] changesets: added changeset for auth-backend refactoring --- .changeset/brown-pumpkins-impress.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/brown-pumpkins-impress.md diff --git a/.changeset/brown-pumpkins-impress.md b/.changeset/brown-pumpkins-impress.md new file mode 100644 index 0000000000..5748a9860b --- /dev/null +++ b/.changeset/brown-pumpkins-impress.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Refactored auth provider factories to accept options along with other internal refactoring of the auth providers. From 6620965b4fea4ec4054f1db161907a0e187cc93e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Feb 2021 13:02:36 +0000 Subject: [PATCH 22/34] chore(deps): bump sanitize-html from 1.27.0 to 2.3.2 Bumps [sanitize-html](https://github.com/apostrophecms/sanitize-html) from 1.27.0 to 2.3.2. - [Release notes](https://github.com/apostrophecms/sanitize-html/releases) - [Changelog](https://github.com/apostrophecms/sanitize-html/blob/main/CHANGELOG.md) - [Commits](https://github.com/apostrophecms/sanitize-html/commits/2.3.2) Signed-off-by: dependabot[bot] --- plugins/techdocs/package.json | 2 +- yarn.lock | 97 +++++++++++++++++++++++++++++------ 2 files changed, 82 insertions(+), 17 deletions(-) diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index afd829d94d..32983270ad 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -47,7 +47,7 @@ "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^15.3.3", - "sanitize-html": "^1.27.0" + "sanitize-html": "^2.3.2" }, "devDependencies": { "@backstage/cli": "^0.6.0", diff --git a/yarn.lock b/yarn.lock index cfcdd3cf82..e2c8103d8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11508,6 +11508,15 @@ dom-serializer@0, dom-serializer@^0.2.1: domelementtype "^2.0.1" entities "^2.0.0" +dom-serializer@^1.0.1: + version "1.2.0" + resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.2.0.tgz#3433d9136aeb3c627981daa385fc7f32d27c48f1" + integrity sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.0.0" + entities "^2.0.0" + dom-walk@^0.1.0: version "0.1.1" resolved "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018" @@ -11528,6 +11537,11 @@ domelementtype@^2.0.1: resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d" integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ== +domelementtype@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e" + integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w== + domexception@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" @@ -11556,6 +11570,13 @@ domhandler@^3.0, domhandler@^3.0.0: dependencies: domelementtype "^2.0.1" +domhandler@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/domhandler/-/domhandler-4.0.0.tgz#01ea7821de996d85f69029e81fa873c21833098e" + integrity sha512-KPTbnGQ1JeEMQyO1iYXoagsI6so/C96HZiFyByU3T6iAzpXn8EGEvct6unm1ZGoed8ByO2oirxgwxBmqKF9haA== + dependencies: + domelementtype "^2.1.0" + dompurify@^2.0.12, dompurify@^2.1.1, dompurify@^2.2.3: version "2.2.3" resolved "https://registry.npmjs.org/dompurify/-/dompurify-2.2.3.tgz#ec653ba521b39f397c2ca045769438d593ea8a9f" @@ -11586,6 +11607,15 @@ domutils@^2.0.0: domelementtype "^2.0.1" domhandler "^3.0.0" +domutils@^2.4.4: + version "2.4.4" + resolved "https://registry.npmjs.org/domutils/-/domutils-2.4.4.tgz#282739c4b150d022d34699797369aad8d19bbbd3" + integrity sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.0.1" + domhandler "^4.0.0" + dot-case@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/dot-case/-/dot-case-3.0.3.tgz#21d3b52efaaba2ea5fda875bb1aa8124521cf4aa" @@ -12071,6 +12101,11 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + escodegen@^1.14.1, escodegen@^1.9.1: version "1.14.3" resolved "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" @@ -14504,7 +14539,7 @@ htmlparser2@^3.3.0: inherits "^2.0.1" readable-stream "^3.1.1" -htmlparser2@^4.0, htmlparser2@^4.1.0: +htmlparser2@^4.0: version "4.1.0" resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz#9a4ef161f2e4625ebf7dfbe6c0a2f52d18a59e78" integrity sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q== @@ -14514,6 +14549,16 @@ htmlparser2@^4.0, htmlparser2@^4.1.0: domutils "^2.0.0" entities "^2.0.0" +htmlparser2@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.0.0.tgz#c2da005030390908ca4c91e5629e418e0665ac01" + integrity sha512-numTQtDZMoh78zJpaNdJ9MXb2cv5G3jwUoe3dMQODubZvLoGvTE/Ofp6sHvH8OGKcN/8A47pGLi/k58xHP/Tfw== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.0.0" + domutils "^2.4.4" + entities "^2.0.0" + http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" @@ -16748,6 +16793,11 @@ kleur@^3.0.3: resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +klona@^2.0.3: + version "2.0.4" + resolved "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0" + integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA== + knex@^0.21.6: version "0.21.8" resolved "https://registry.npmjs.org/knex/-/knex-0.21.8.tgz#e5c07af61ee6aa006d3468e10e3a69351deb0c26" @@ -18406,6 +18456,11 @@ nanoid@^2.1.0: resolved "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280" integrity sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA== +nanoid@^3.1.20: + version "3.1.20" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788" + integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -19655,6 +19710,11 @@ parse-path@^4.0.0: is-ssh "^1.3.0" protocols "^1.4.0" +parse-srcset@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" + integrity sha1-8r0iH2zJcKk42IVWq8WJyqqiveE= + parse-url@^5.0.0: version "5.0.1" resolved "https://registry.npmjs.org/parse-url/-/parse-url-5.0.1.tgz#99c4084fc11be14141efa41b3d117a96fcb9527f" @@ -20578,6 +20638,15 @@ postcss@^6.0.1: source-map "^0.6.1" supports-color "^5.4.0" +postcss@^8.0.2: + version "8.2.6" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.2.6.tgz#5d69a974543b45f87e464bc4c3e392a97d6be9fe" + integrity sha512-xpB8qYxgPuly166AGlpRjUdEYtmOWx2iCwGmrv4vqZL9YPVviDVPZPRXxnXr6xPZOdxQ9lp3ZBFCRgWJ7LE3Sg== + dependencies: + colorette "^1.2.1" + nanoid "^3.1.20" + source-map "^0.6.1" + postgres-array@~1.0.0: version "1.0.3" resolved "https://registry.npmjs.org/postgres-array/-/postgres-array-1.0.3.tgz#c561fc3b266b21451fc6555384f4986d78ec80f5" @@ -22635,17 +22704,18 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" -sanitize-html@^1.27.0: - version "1.27.0" - resolved "https://registry.npmjs.org/sanitize-html/-/sanitize-html-1.27.0.tgz#42104a2d59f1a48b616b5165ad5349824861e580" - integrity sha512-U1btucGeYVpg0GoK43jPpe/bDCV4cBOGuxzv5NBd0bOjyZdMKY0n98S/vNlO1wVwre0VCj8H3hbzE7gD2+RjKA== +sanitize-html@^2.3.2: + version "2.3.2" + resolved "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.3.2.tgz#a1954aea877a096c408aca7b0c260bef6e4fc402" + integrity sha512-p7neuskvC8pSurUjdVmbWPXmc9A4+QpOXIL+4gwFC+av5h+lYCXFT8uEneqsFQg/wEA1IH+cKQA60AaQI6p3cg== dependencies: - chalk "^2.4.1" - htmlparser2 "^4.1.0" - lodash "^4.17.15" - postcss "^7.0.27" - srcset "^2.0.1" - xtend "^4.0.1" + deepmerge "^4.2.2" + escape-string-regexp "^4.0.0" + htmlparser2 "^6.0.0" + is-plain-object "^5.0.0" + klona "^2.0.3" + parse-srcset "^1.0.2" + postcss "^8.0.2" sax@1.2.1: version "1.2.1" @@ -23359,11 +23429,6 @@ sqlite3@^5.0.0: optionalDependencies: node-gyp "3.x" -srcset@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/srcset/-/srcset-2.0.1.tgz#8f842d357487eb797f413d9c309de7a5149df5ac" - integrity sha512-00kZI87TdRKwt+P8jj8UZxbfp7mK2ufxcIMWvhAOZNJTRROimpHeruWrGvCZneiuVDLqdyHefVp748ECTnyUBQ== - ssh2-streams@~0.4.10: version "0.4.10" resolved "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.10.tgz#48ef7e8a0e39d8f2921c30521d56dacb31d23a34" From bdf5c24328a67e06af62cc9ddfb4c72cbbc890f1 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Mon, 15 Feb 2021 14:50:44 +0100 Subject: [PATCH 23/34] The apps entry in the yaml file need to be at the same level as the host, if it is not the app will not start and complain about it --- docs/plugins/github-apps.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/github-apps.md b/docs/plugins/github-apps.md index d3b0e36cd9..1cac85fbce 100644 --- a/docs/plugins/github-apps.md +++ b/docs/plugins/github-apps.md @@ -77,6 +77,6 @@ method of distributing secrets. integrations: github: - host: github.com - apps: + apps: - $include: example-backstage-app-credentials.yaml ``` From 434b4e81a81301a4df9e54a97920cdd31efc7237 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Mon, 15 Feb 2021 15:12:06 +0100 Subject: [PATCH 24/34] Support globs in FileReaderProcessor Signed-off-by: Oliver Sand oliver.sand@sda-se.com --- .changeset/many-bags-sort.md | 5 ++ plugins/catalog-backend/package.json | 1 + .../processors/FileReaderProcessor.test.ts | 76 +++++++++++++++++++ .../processors/FileReaderProcessor.ts | 17 +++-- .../fileReaderProcessor/component.yaml | 1 + .../fileReaderProcessor/dir/api.yaml | 1 + .../__fixtures__/fileReaderProcessor/test.txt | 1 + 7 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 .changeset/many-bags-sort.md create mode 100644 plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.test.ts create mode 100644 plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/component.yaml create mode 100644 plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/dir/api.yaml create mode 100644 plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/test.txt diff --git a/.changeset/many-bags-sort.md b/.changeset/many-bags-sort.md new file mode 100644 index 0000000000..dbc8df8b0c --- /dev/null +++ b/.changeset/many-bags-sort.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Support globs in `FileReaderProcessor`. diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 254898a086..c595c8dd5a 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -45,6 +45,7 @@ "express-promise-router": "^3.0.3", "fs-extra": "^9.0.0", "git-url-parse": "^11.4.4", + "glob": "^7.1.6", "knex": "^0.21.6", "ldapjs": "^2.2.0", "lodash": "^4.17.15", diff --git a/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.test.ts new file mode 100644 index 0000000000..8ea165dcb8 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.test.ts @@ -0,0 +1,76 @@ +/* + * 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 { FileReaderProcessor } from './FileReaderProcessor'; +import { + CatalogProcessorEntityResult, + CatalogProcessorErrorResult, + CatalogProcessorResult, +} from './types'; + +describe('FileReaderProcessor', () => { + const fixturesRoot = + 'src/ingestion/processors/__fixtures__/fileReaderProcessor'; + + it('should load from file', async () => { + const processor = new FileReaderProcessor(); + const spec = { + type: 'file', + target: `${fixturesRoot}/component.yaml`, + }; + + const generated = (await new Promise(emit => + processor.readLocation(spec, false, emit), + )) as CatalogProcessorEntityResult; + + expect(generated.type).toBe('entity'); + expect(generated.location).toEqual(spec); + expect(generated.entity).toEqual({ kind: 'Component' }); + }); + + it('should fail load from file with error', async () => { + const processor = new FileReaderProcessor(); + const spec = { + type: 'file', + target: `${fixturesRoot}/missing.yaml`, + }; + + const generated = (await new Promise(emit => + processor.readLocation(spec, false, emit), + )) as CatalogProcessorErrorResult; + + expect(generated.type).toBe('error'); + expect(generated.location).toBe(spec); + expect(generated.error.name).toBe('NotFoundError'); + expect(generated.error.message).toBe( + `file ${fixturesRoot}/missing.yaml does not exist`, + ); + }); + + it('should support globs', async () => { + const processor = new FileReaderProcessor(); + + const emit = jest.fn(); + + await processor.readLocation( + { type: 'file', target: `${fixturesRoot}/**/*.yaml` }, + false, + emit, + ); + + expect(emit).toBeCalledTimes(2); + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.ts index 2328ec3c7a..db069913a2 100644 --- a/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.ts @@ -19,6 +19,10 @@ import fs from 'fs-extra'; import * as result from './results'; import { CatalogProcessor, CatalogProcessorEmit } from './types'; import { parseEntityYaml } from './util/parse'; +import { promisify } from 'util'; +import g from 'glob'; + +const glob = promisify(g); export class FileReaderProcessor implements CatalogProcessor { async readLocation( @@ -31,12 +35,15 @@ export class FileReaderProcessor implements CatalogProcessor { } try { - const exists = await fs.pathExists(location.target); - if (exists) { - const data = await fs.readFile(location.target); + const fileMatches = await glob(location.target); - for (const parseResult of parseEntityYaml(data, location)) { - emit(parseResult); + if (fileMatches.length > 0) { + for (const fileMatch of fileMatches) { + const data = await fs.readFile(fileMatch); + + for (const parseResult of parseEntityYaml(data, location)) { + emit(parseResult); + } } } else if (!optional) { const message = `${location.type} ${location.target} does not exist`; diff --git a/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/component.yaml b/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/component.yaml new file mode 100644 index 0000000000..8524aaf14a --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/component.yaml @@ -0,0 +1 @@ +kind: Component diff --git a/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/dir/api.yaml b/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/dir/api.yaml new file mode 100644 index 0000000000..a894e34f70 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/dir/api.yaml @@ -0,0 +1 @@ +kind: API diff --git a/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/test.txt b/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/test.txt new file mode 100644 index 0000000000..f332206601 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/test.txt @@ -0,0 +1 @@ +Just a text file From d0aec7d68941ee2350bbe93a158ad58c53ec4e92 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Mon, 15 Feb 2021 15:32:58 +0100 Subject: [PATCH 25/34] Make test run on Windows? Signed-off-by: Oliver Sand oliver.sand@sda-se.com --- .../processors/FileReaderProcessor.test.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.test.ts index 8ea165dcb8..3e2c62f7f0 100644 --- a/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.test.ts @@ -20,16 +20,22 @@ import { CatalogProcessorErrorResult, CatalogProcessorResult, } from './types'; +import path from 'path'; describe('FileReaderProcessor', () => { - const fixturesRoot = - 'src/ingestion/processors/__fixtures__/fileReaderProcessor'; + const fixturesRoot = path.join( + 'src', + 'ingestion', + 'processors', + '__fixtures__', + 'fileReaderProcessor', + ); it('should load from file', async () => { const processor = new FileReaderProcessor(); const spec = { type: 'file', - target: `${fixturesRoot}/component.yaml`, + target: `${path.join(fixturesRoot, 'component.yaml')}`, }; const generated = (await new Promise(emit => @@ -45,7 +51,7 @@ describe('FileReaderProcessor', () => { const processor = new FileReaderProcessor(); const spec = { type: 'file', - target: `${fixturesRoot}/missing.yaml`, + target: `${path.join(fixturesRoot, 'missing.yaml')}`, }; const generated = (await new Promise(emit => @@ -56,7 +62,7 @@ describe('FileReaderProcessor', () => { expect(generated.location).toBe(spec); expect(generated.error.name).toBe('NotFoundError'); expect(generated.error.message).toBe( - `file ${fixturesRoot}/missing.yaml does not exist`, + `file ${path.join(fixturesRoot, 'missing.yaml')} does not exist`, ); }); @@ -66,7 +72,7 @@ describe('FileReaderProcessor', () => { const emit = jest.fn(); await processor.readLocation( - { type: 'file', target: `${fixturesRoot}/**/*.yaml` }, + { type: 'file', target: `${path.join(fixturesRoot, '**', '*.yaml')}` }, false, emit, ); From fdbd02e08983ea490f067e50e50e7c7b6938efe2 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Mon, 15 Feb 2021 15:34:57 +0100 Subject: [PATCH 26/34] Improve glob test expectations Signed-off-by: Oliver Sand oliver.sand@sda-se.com --- .../src/ingestion/processors/FileReaderProcessor.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.test.ts index 3e2c62f7f0..0155f93916 100644 --- a/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.test.ts @@ -78,5 +78,7 @@ describe('FileReaderProcessor', () => { ); expect(emit).toBeCalledTimes(2); + expect(emit.mock.calls[0][0].entity).toEqual({ kind: 'Component' }); + expect(emit.mock.calls[1][0].entity).toEqual({ kind: 'API' }); }); }); From 07bca64b07408506e7be94801712c77d54a013ee Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Mon, 15 Feb 2021 15:50:43 +0100 Subject: [PATCH 27/34] techdocs: remove invalid comment --- packages/techdocs-common/src/stages/generate/helpers.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/techdocs-common/src/stages/generate/helpers.test.ts b/packages/techdocs-common/src/stages/generate/helpers.test.ts index 10efb310e1..14722150d1 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.test.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.test.ts @@ -360,7 +360,6 @@ describe('helpers', () => { it('should throw error when the JSON is invalid', async () => { const filePath = path.join(rootDir, 'invalid_techdocs_metadata.json'); - // Check if the file exists await expect( addBuildTimestampMetadata(filePath, mockLogger), ).rejects.toThrowError(); From 2f3be0a68b7f6b28fbfd1397015aeca44706cfae Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Mon, 15 Feb 2021 16:35:25 +0100 Subject: [PATCH 28/34] integration: Add test case for non URI encoded link --- packages/integration/src/gitlab/core.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/integration/src/gitlab/core.test.ts b/packages/integration/src/gitlab/core.test.ts index 6836854eae..8e48871423 100644 --- a/packages/integration/src/gitlab/core.test.ts +++ b/packages/integration/src/gitlab/core.test.ts @@ -50,6 +50,21 @@ describe('gitlab core', () => { describe('getGitLabFileFetchUrl', () => { it.each([ // Project URLs + { + config: configWithNoToken, + url: + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml', + result: + 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', + }, + { + config: configWithNoToken, + // Works with non URI encoded link + url: + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file with spaces.yaml', + result: + 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile%20with%20spaces.yaml/raw?ref=branch', + }, { config: configWithNoToken, url: From f7817546436c288bbb6f7f3d94afb25f27048a44 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 15 Feb 2021 17:26:24 +0100 Subject: [PATCH 29/34] auth-backend: move @types/passport to deps --- plugins/auth-backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index c50789d4c1..281ea063fd 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -34,6 +34,7 @@ "@backstage/catalog-model": "^0.7.1", "@backstage/config": "^0.1.2", "@types/express": "^4.17.6", + "@types/passport": "^1.0.3", "compression": "^1.7.4", "cookie-parser": "^1.4.5", "cors": "^2.8.5", @@ -70,7 +71,6 @@ "@types/cookie-parser": "^1.4.2", "@types/express-session": "^1.17.2", "@types/jwt-decode": "^3.1.0", - "@types/passport": "^1.0.3", "@types/passport-github2": "^1.2.4", "@types/passport-google-oauth20": "^2.0.3", "@types/passport-microsoft": "^0.0.0", From 2046701e96d5bc5903c0debfb222561eca7537a4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 15 Feb 2021 17:26:51 +0100 Subject: [PATCH 30/34] auth-backend: fix a camel --- plugins/auth-backend/src/providers/microsoft/provider.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index 032af46804..4dee90d4c6 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -199,11 +199,11 @@ export const createMicrosoftProvider = ( OAuthEnvironmentHandler.mapConfig(config, envConfig => { const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); - const tenantID = envConfig.getString('tenantId'); + const tenantId = envConfig.getString('tenantId'); const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const authorizationUrl = `https://login.microsoftonline.com/${tenantID}/oauth2/v2.0/authorize`; - const tokenUrl = `https://login.microsoftonline.com/${tenantID}/oauth2/v2.0/token`; + const authorizationUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`; + const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`; const provider = new MicrosoftAuthProvider({ clientId, From b16b096f58459d75cd4f2adc2cffb2517676243b Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Mon, 15 Feb 2021 19:38:07 +0100 Subject: [PATCH 31/34] TechDocs: Add etag to techdocs_metadata.json --- .../src/stages/generate/helpers.test.ts | 36 ++++++++++++++++++- .../src/stages/generate/helpers.ts | 17 +++++++++ .../src/stages/generate/techdocs.ts | 18 ++++++++-- .../src/stages/generate/types.ts | 6 ++-- .../src/DocsBuilder/builder.ts | 1 + 5 files changed, 73 insertions(+), 5 deletions(-) diff --git a/packages/techdocs-common/src/stages/generate/helpers.test.ts b/packages/techdocs-common/src/stages/generate/helpers.test.ts index 14722150d1..c4bf102610 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.test.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.test.ts @@ -29,6 +29,7 @@ import { isValidRepoUrlForMkdocs, patchMkdocsYmlPreBuild, runDockerContainer, + storeEtagMetadata, } from './helpers'; const mockEntity = { @@ -362,7 +363,7 @@ describe('helpers', () => { await expect( addBuildTimestampMetadata(filePath, mockLogger), - ).rejects.toThrowError(); + ).rejects.toThrowError('Unexpected token d in JSON at position 0'); }); it('should add build timestamp to the metadata json', async () => { @@ -374,4 +375,37 @@ describe('helpers', () => { expect(json.build_timestamp).toBeLessThanOrEqual(Date.now()); }); }); + + describe('storeEtagMetadata', () => { + beforeEach(() => { + mockFs.restore(); + mockFs({ + [rootDir]: { + 'invalid_techdocs_metadata.json': 'dsds', + 'techdocs_metadata.json': '{"site_name": "Tech Docs"}', + }, + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it('should throw error when the JSON is invalid', async () => { + const filePath = path.join(rootDir, 'invalid_techdocs_metadata.json'); + + await expect( + storeEtagMetadata(filePath, 'etag123abc'), + ).rejects.toThrowError('Unexpected token d in JSON at position 0'); + }); + + it('should add etag to the metadata json', async () => { + const filePath = path.join(rootDir, 'techdocs_metadata.json'); + + await storeEtagMetadata(filePath, 'etag123abc'); + + const json = await fs.readJson(filePath); + expect(json.etag).toBe('etag123abc'); + }); + }); }); diff --git a/packages/techdocs-common/src/stages/generate/helpers.ts b/packages/techdocs-common/src/stages/generate/helpers.ts index 15a1c57d41..32da5d19e8 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.ts @@ -306,3 +306,20 @@ export const addBuildTimestampMetadata = async ( await fs.writeJson(techdocsMetadataPath, json); return; }; + +/** + * Update the techdocs_metadata.json to add etag of the prepared tree (e.g. commit SHA or actual Etag of the resource). + * This is helpful to check if a TechDocs site in storage has gone outdated, without maintaining an in-memory build info + * per Backstage instance. + * + * @param {string} techdocsMetadataPath File path to techdocs_metadata.json + * @param {string} etag + */ +export const storeEtagMetadata = async ( + techdocsMetadataPath: string, + etag: string, +): Promise => { + const json = await fs.readJson(techdocsMetadataPath); + json.etag = etag; + await fs.writeJson(techdocsMetadataPath, json); +}; diff --git a/packages/techdocs-common/src/stages/generate/techdocs.ts b/packages/techdocs-common/src/stages/generate/techdocs.ts index b7b46b1b22..133755ecd2 100644 --- a/packages/techdocs-common/src/stages/generate/techdocs.ts +++ b/packages/techdocs-common/src/stages/generate/techdocs.ts @@ -23,6 +23,7 @@ import { patchMkdocsYmlPreBuild, runCommand, runDockerContainer, + storeEtagMetadata, } from './helpers'; import { GeneratorBase, GeneratorRunOptions } from './types'; @@ -62,6 +63,7 @@ export class TechdocsGenerator implements GeneratorBase { outputDir, dockerClient, parsedLocationAnnotation, + etag, }: GeneratorRunOptions): Promise { const [log, logStream] = createStream(); @@ -119,12 +121,24 @@ export class TechdocsGenerator implements GeneratorBase { ); } - // Post Generate steps + /** + * Post Generate steps + */ // Add build timestamp to techdocs_metadata.json - addBuildTimestampMetadata( + // Creates techdocs_metadata.json if file does not exist. + await addBuildTimestampMetadata( path.join(outputDir, 'techdocs_metadata.json'), this.logger, ); + + // Add etag of the prepared tree to techdocs_metadata.json + // Assumes that the file already exists. + if (etag) { + await storeEtagMetadata( + path.join(outputDir, 'techdocs_metadata.json'), + etag, + ); + } } } diff --git a/packages/techdocs-common/src/stages/generate/types.ts b/packages/techdocs-common/src/stages/generate/types.ts index 7724107dac..fda41ed442 100644 --- a/packages/techdocs-common/src/stages/generate/types.ts +++ b/packages/techdocs-common/src/stages/generate/types.ts @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Writable } from 'stream'; -import Docker from 'dockerode'; import { Entity } from '@backstage/catalog-model'; +import Docker from 'dockerode'; +import { Writable } from 'stream'; import { ParsedLocationAnnotation } from '../../helpers'; /** @@ -25,6 +25,7 @@ import { ParsedLocationAnnotation } from '../../helpers'; * @param {string} outputDir Directory to store generated docs in. Usually - a newly created temporary directory. * @param {Docker} dockerClient A docker client to run any generator on top of your directory * @param {ParsedLocationAnnotation} parsedLocationAnnotation backstage.io/techdocs-ref annotation of an entity + * @param {string} etag A unique identifier for the prepared tree e.g. commit SHA. If provided it will be stored in techdocs_metadata.json. * @param {Writable} [logStream] A dedicated log stream */ export type GeneratorRunOptions = { @@ -32,6 +33,7 @@ export type GeneratorRunOptions = { outputDir: string; dockerClient: Docker; parsedLocationAnnotation?: ParsedLocationAnnotation; + etag?: string; logStream?: Writable; }; diff --git a/plugins/techdocs-backend/src/DocsBuilder/builder.ts b/plugins/techdocs-backend/src/DocsBuilder/builder.ts index 8c54a2b1e4..51f4d212ba 100644 --- a/plugins/techdocs-backend/src/DocsBuilder/builder.ts +++ b/plugins/techdocs-backend/src/DocsBuilder/builder.ts @@ -130,6 +130,7 @@ export class DocsBuilder { outputDir, dockerClient: this.dockerClient, parsedLocationAnnotation, + etag, }); this.logger.debug(`Generated files temporarily stored at ${outputDir}`); From b0a41c707cd6eac9a834a91edb3971323b7bbccd Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Mon, 15 Feb 2021 19:39:16 +0100 Subject: [PATCH 32/34] TechDocs: changeset for the etag in storage change --- .changeset/techdocs-spotty-cooks-wait.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/techdocs-spotty-cooks-wait.md diff --git a/.changeset/techdocs-spotty-cooks-wait.md b/.changeset/techdocs-spotty-cooks-wait.md new file mode 100644 index 0000000000..9ec35b9257 --- /dev/null +++ b/.changeset/techdocs-spotty-cooks-wait.md @@ -0,0 +1,6 @@ +--- +'@backstage/techdocs-common': patch +'@backstage/plugin-techdocs-backend': patch +--- + +Add etag of the prepared file tree to techdocs_metadata.json in the storage From fb28da2123975cc9d455838cf4959b35e5bdff03 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 15 Feb 2021 20:26:12 +0100 Subject: [PATCH 33/34] integrations: switch to using x-access-token for github git auth --- .changeset/mean-grapes-march.md | 7 +++++++ packages/backend-common/src/scm/git.ts | 11 +++++------ packages/techdocs-common/src/helpers.ts | 6 +++--- .../src/scaffolder/stages/prepare/github.test.ts | 4 ++-- .../src/scaffolder/stages/prepare/github.ts | 4 ++-- .../src/scaffolder/stages/publish/github.test.ts | 10 +++++----- .../src/scaffolder/stages/publish/github.ts | 4 ++-- 7 files changed, 26 insertions(+), 20 deletions(-) create mode 100644 .changeset/mean-grapes-march.md diff --git a/.changeset/mean-grapes-march.md b/.changeset/mean-grapes-march.md new file mode 100644 index 0000000000..6065fe0892 --- /dev/null +++ b/.changeset/mean-grapes-march.md @@ -0,0 +1,7 @@ +--- +'@backstage/backend-common': patch +'@backstage/techdocs-common': patch +'@backstage/plugin-scaffolder-backend': patch +--- + +Switched to using `'x-access-token'` for authenticating Git over HTTPS towards GitHub. diff --git a/packages/backend-common/src/scm/git.ts b/packages/backend-common/src/scm/git.ts index 5b24d23b99..21aa9bf5d1 100644 --- a/packages/backend-common/src/scm/git.ts +++ b/packages/backend-common/src/scm/git.ts @@ -23,12 +23,11 @@ import fs from 'fs-extra'; import { Logger } from 'winston'; /* -provider username password -GitHub token 'x-oauth-basic' -GitHub App token 'x-access-token' -BitBucket 'x-token-auth' token -GitLab 'oauth2' token -From : https://isomorphic-git.org/docs/en/onAuth +provider username password +GitHub 'x-access-token' token +BitBucket 'x-token-auth' token +GitLab 'oauth2' token +From : https://isomorphic-git.org/docs/en/onAuth with fix for GitHub Azure 'notempty' token */ diff --git a/packages/techdocs-common/src/helpers.ts b/packages/techdocs-common/src/helpers.ts index beff0b2a6e..ec58bb7b40 100644 --- a/packages/techdocs-common/src/helpers.ts +++ b/packages/techdocs-common/src/helpers.ts @@ -133,11 +133,11 @@ export const checkoutGitRepository = async ( switch (type) { case 'github': git = Git.fromAuth({ - username: token, - password: 'x-oauth-basic', + username: 'x-access-token', + password: token, logger, }); - parsedGitLocation.token = `${token}:x-oauth-basic`; + parsedGitLocation.token = `x-access-token:${token}`; break; case 'gitlab': git = Git.fromAuth({ 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 301f015c1e..976370bd21 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts @@ -90,8 +90,8 @@ describe('GitHubPreparer', () => { expect(Git.fromAuth).toHaveBeenCalledWith({ logger, - username: 'fake-token', - password: 'x-oauth-basic', + username: 'x-access-token', + password: 'fake-token', }); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts index 77e6ec6d70..5b74ee81ca 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts @@ -38,8 +38,8 @@ export class GithubPreparer implements PreparerBase { const git = this.config.token ? Git.fromAuth({ - username: this.config.token, - password: 'x-oauth-basic', + username: 'x-access-token', + password: this.config.token, logger, }) : Git.fromAuth({ logger }); 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 30fe8fd65a..1dc53e5a5c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts @@ -97,7 +97,7 @@ describe('GitHub Publisher', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: resultPath, remoteUrl: 'https://github.com/backstage/backstage.git', - auth: { username: 'fake-token', password: 'x-oauth-basic' }, + auth: { username: 'x-access-token', password: 'fake-token' }, logger, }); }); @@ -148,7 +148,7 @@ describe('GitHub Publisher', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: resultPath, remoteUrl: 'https://github.com/backstage/backstage.git', - auth: { username: 'fake-token', password: 'x-oauth-basic' }, + auth: { username: 'x-access-token', password: 'fake-token' }, logger, }); }); @@ -206,7 +206,7 @@ describe('GitHub Publisher', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: resultPath, remoteUrl: 'https://github.com/backstage/backstage.git', - auth: { username: 'fake-token', password: 'x-oauth-basic' }, + auth: { username: 'x-access-token', password: 'fake-token' }, logger, }); }); @@ -257,7 +257,7 @@ describe('GitHub Publisher', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: resultPath, remoteUrl: 'https://github.com/backstage/backstage.git', - auth: { username: 'fake-token', password: 'x-oauth-basic' }, + auth: { username: 'x-access-token', password: 'fake-token' }, logger, }); }); @@ -307,7 +307,7 @@ describe('GitHub Publisher', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: resultPath, remoteUrl: 'https://github.com/backstage/backstage.git', - auth: { username: 'fake-token', password: 'x-oauth-basic' }, + auth: { username: 'x-access-token', password: 'fake-token' }, logger, }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts index 13aab7ff34..407a1633c2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts @@ -72,8 +72,8 @@ export class GithubPublisher implements PublisherBase { dir: path.join(workspacePath, 'result'), remoteUrl, auth: { - username: this.config.token, - password: 'x-oauth-basic', + username: 'x-access-token', + password: this.config.token, }, logger, }); From 88de6b6c81c9caf05a5a188ea7b891882f5ab96a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 16 Feb 2021 08:11:46 +0100 Subject: [PATCH 34/34] remove some occurrences of the old syntax --- app-config.yaml | 3 +- contrib/chart/backstage/values.yaml | 63 ++++++++++------------------- 2 files changed, 23 insertions(+), 43 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 9df6540a2c..672ba26cd2 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -167,7 +167,8 @@ catalog: # - target: ldaps://ds.example.net # bind: # dn: uid=ldap-reader-user,ou=people,ou=example,dc=example,dc=net - # secret: { $secret: { env: LDAP_SECRET } } + # secret: + # $env: LDAP_SECRET # users: # dn: ou=people,ou=example,dc=example,dc=net # options: diff --git a/contrib/chart/backstage/values.yaml b/contrib/chart/backstage/values.yaml index 261f352f93..7926ebbc0a 100644 --- a/contrib/chart/backstage/values.yaml +++ b/contrib/chart/backstage/values.yaml @@ -127,88 +127,67 @@ appConfig: appOrigin: 'http://localhost:3000/' secure: false clientId: - $secret: - env: AUTH_GOOGLE_CLIENT_ID + $env: AUTH_GOOGLE_CLIENT_ID clientSecret: - $secret: - env: AUTH_GOOGLE_CLIENT_SECRET + $env: AUTH_GOOGLE_CLIENT_SECRET github: development: appOrigin: 'http://localhost:3000/' secure: false clientId: - $secret: - env: AUTH_GITHUB_CLIENT_ID + $env: AUTH_GITHUB_CLIENT_ID clientSecret: - $secret: - env: AUTH_GITHUB_CLIENT_SECRET + $env: AUTH_GITHUB_CLIENT_SECRET enterpriseInstanceUrl: - $secret: - env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL + $env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL gitlab: development: appOrigin: 'http://localhost:3000/' secure: false clientId: - $secret: - env: AUTH_GITLAB_CLIENT_ID + $env: AUTH_GITLAB_CLIENT_ID clientSecret: - $secret: - env: AUTH_GITLAB_CLIENT_SECRET + $env: AUTH_GITLAB_CLIENT_SECRET audience: - $secret: - env: GITLAB_BASE_URL + $env: GITLAB_BASE_URL okta: development: appOrigin: 'http://localhost:3000/' secure: false clientId: - $secret: - env: AUTH_OKTA_CLIENT_ID + $env: AUTH_OKTA_CLIENT_ID clientSecret: - $secret: - env: AUTH_OKTA_CLIENT_SECRET + $env: AUTH_OKTA_CLIENT_SECRET audience: - $secret: - env: AUTH_OKTA_AUDIENCE + $env: AUTH_OKTA_AUDIENCE oauth2: development: appOrigin: 'http://localhost:3000/' secure: false clientId: - $secret: - env: AUTH_OAUTH2_CLIENT_ID + $env: AUTH_OAUTH2_CLIENT_ID clientSecret: - $secret: - env: AUTH_OAUTH2_CLIENT_SECRET + $env: AUTH_OAUTH2_CLIENT_SECRET authorizationURL: - $secret: - env: AUTH_OAUTH2_AUTH_URL + $env: AUTH_OAUTH2_AUTH_URL tokenURL: - $secret: - env: AUTH_OAUTH2_TOKEN_URL + $env: AUTH_OAUTH2_TOKEN_URL auth0: development: clientId: - $secret: - env: AUTH_AUTH0_CLIENT_ID + $env: AUTH_AUTH0_CLIENT_ID clientSecret: - $secret: - env: AUTH_AUTH0_CLIENT_SECRET + $env: AUTH_AUTH0_CLIENT_SECRET domain: - $secret: - env: AUTH_AUTH0_DOMAIN + $env: AUTH_AUTH0_DOMAIN microsoft: development: clientId: - $secret: - env: AUTH_MICROSOFT_CLIENT_ID + $env: AUTH_MICROSOFT_CLIENT_ID clientSecret: - $secret: - env: AUTH_MICROSOFT_CLIENT_SECRET + $env: AUTH_MICROSOFT_CLIENT_SECRET tenantId: - $secret: - env: AUTH_MICROSOFT_TENANT_ID + $env: AUTH_MICROSOFT_TENANT_ID auth: google: