From e621d9658b48ae39c4648b7dfb2769789f9a7362 Mon Sep 17 00:00:00 2001 From: Frank Cycan Date: Tue, 22 Feb 2022 11:53:46 -0500 Subject: [PATCH] [Plugin] Code Climate Signed-off-by: Frank Cycan --- .changeset/poor-icons-flow.md | 5 + plugins/code-climate/.eslintrc.js | 3 + plugins/code-climate/README.md | 85 +++++++ plugins/code-climate/api-report.md | 87 +++++++ plugins/code-climate/dev/index.tsx | 93 ++++++++ .../code-climate/docs/code-climate-card.png | Bin 0 -> 65389 bytes plugins/code-climate/package.json | 56 +++++ .../code-climate/src/api/code-climate-api.ts | 26 +++ .../code-climate/src/api/code-climate-data.ts | 184 +++++++++++++++ plugins/code-climate/src/api/index.ts | 21 ++ .../code-climate-maintainability-mock.json | 49 ++++ .../mock/code-climate-test-coverage-mock.json | 21 ++ plugins/code-climate/src/api/mock/index.ts | 18 ++ plugins/code-climate/src/api/mock/mock-api.ts | 56 +++++ .../code-climate/src/api/production-api.ts | 160 +++++++++++++ .../CodeClimateCard/CodeClimateCard.tsx | 25 +++ .../src/components/CodeClimateCard/index.ts | 17 ++ .../CodeClimateCardContents.tsx | 64 ++++++ .../CodeClimateCardContents/index.ts | 17 ++ .../CodeClimateTable.test.tsx | 37 +++ .../CodeClimateTable/CodeClimateTable.tsx | 212 ++++++++++++++++++ .../src/components/CodeClimateTable/index.ts | 17 ++ plugins/code-climate/src/index.ts | 24 ++ plugins/code-climate/src/plugin.test.ts | 23 ++ plugins/code-climate/src/plugin.ts | 58 +++++ plugins/code-climate/src/setupTests.ts | 20 ++ yarn.lock | 12 +- 27 files changed, 1389 insertions(+), 1 deletion(-) create mode 100644 .changeset/poor-icons-flow.md create mode 100644 plugins/code-climate/.eslintrc.js create mode 100644 plugins/code-climate/README.md create mode 100644 plugins/code-climate/api-report.md create mode 100644 plugins/code-climate/dev/index.tsx create mode 100644 plugins/code-climate/docs/code-climate-card.png create mode 100644 plugins/code-climate/package.json create mode 100644 plugins/code-climate/src/api/code-climate-api.ts create mode 100644 plugins/code-climate/src/api/code-climate-data.ts create mode 100644 plugins/code-climate/src/api/index.ts create mode 100644 plugins/code-climate/src/api/mock/code-climate-maintainability-mock.json create mode 100644 plugins/code-climate/src/api/mock/code-climate-test-coverage-mock.json create mode 100644 plugins/code-climate/src/api/mock/index.ts create mode 100644 plugins/code-climate/src/api/mock/mock-api.ts create mode 100644 plugins/code-climate/src/api/production-api.ts create mode 100644 plugins/code-climate/src/components/CodeClimateCard/CodeClimateCard.tsx create mode 100644 plugins/code-climate/src/components/CodeClimateCard/index.ts create mode 100644 plugins/code-climate/src/components/CodeClimateCardContents/CodeClimateCardContents.tsx create mode 100644 plugins/code-climate/src/components/CodeClimateCardContents/index.ts create mode 100644 plugins/code-climate/src/components/CodeClimateTable/CodeClimateTable.test.tsx create mode 100644 plugins/code-climate/src/components/CodeClimateTable/CodeClimateTable.tsx create mode 100644 plugins/code-climate/src/components/CodeClimateTable/index.ts create mode 100644 plugins/code-climate/src/index.ts create mode 100644 plugins/code-climate/src/plugin.test.ts create mode 100644 plugins/code-climate/src/plugin.ts create mode 100644 plugins/code-climate/src/setupTests.ts diff --git a/.changeset/poor-icons-flow.md b/.changeset/poor-icons-flow.md new file mode 100644 index 0000000000..f3cc22c339 --- /dev/null +++ b/.changeset/poor-icons-flow.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-code-climate': minor +--- + +Added Code Climate plugin diff --git a/plugins/code-climate/.eslintrc.js b/plugins/code-climate/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/code-climate/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/code-climate/README.md b/plugins/code-climate/README.md new file mode 100644 index 0000000000..a67e70b11a --- /dev/null +++ b/plugins/code-climate/README.md @@ -0,0 +1,85 @@ +# Code Climate Plugin + +The Code Climate Plugin displays a few stats from the quality section from [Code Climate](https://codeclimate.com). + +![Code Climate Card](./docs/code-climate-card.png) + +## Getting Started + +1. Install the Code Climate Plugin: + +```bash +# From your Backstage root directory +cd packages/app +yarn add @backstage/plugin-code-climate +``` + +2. Add the `EntityCodeClimateCard` to the EntityPage: + +```jsx +// packages/app/src/components/catalog/EntityPage.tsx + +import { EntityCodeClimateCard } from '@backstage/plugin-code-climate'; + +const overviewContent = ( + + // ... + + + + // ... + +); +``` + +3. Add the proxy config: + +```yaml +# app-config.yaml + +proxy: + '/codeclimate/api': + target: https://api.codeclimate.com/v1 + headers: + Authorization: Token token=${CODECLIMATE_TOKEN} +``` + +4. Create a new API access token (https://codeclimate.com/profile/tokens) and provide `CODECLIMATE_TOKEN` as an env variable. + +5. Add the `codeclimate.com/repo-id` annotation to your `catalog-info.yaml` file: + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage + description: | + Backstage is an open-source developer portal that puts the developer experience first. + annotations: + codeclimate.com/repo-id: YOUR_REPO_ID +spec: + type: library + owner: CNCF + lifecycle: experimental +``` + +### Demo Mode + +The plugin provides a MockAPI that always returns dummy data instead of talking to the Code Climate backend. +You can add it by overriding the `codeClimateApiRef`: + +```ts +// packages/app/src/apis.ts + +import { createApiFactory } from '@backstage/core-plugin-api'; +import { + MockCodeClimateApi, + codeClimateApiRef, +} from '@backstage/plugin-code-climate'; + +export const apis = [ + // ... + + createApiFactory(codeClimateApiRef, new MockCodeClimateApi()), +]; +``` diff --git a/plugins/code-climate/api-report.md b/plugins/code-climate/api-report.md new file mode 100644 index 0000000000..11366384d3 --- /dev/null +++ b/plugins/code-climate/api-report.md @@ -0,0 +1,87 @@ +## API Report File for "@backstage/plugin-code-climate" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { ApiRef } from '@backstage/core-plugin-api'; +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { RouteRef } from '@backstage/core-plugin-api'; + +// Warning: (ae-missing-release-tag) "CodeClimateApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface CodeClimateApi { + // (undocumented) + fetchData(repoID: string): Promise; +} + +// Warning: (ae-missing-release-tag) "codeClimateApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const codeClimateApiRef: ApiRef; + +// Warning: (ae-missing-release-tag) "CodeClimateData" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type CodeClimateData = { + repoID: string; + maintainability: { + letter: string; + value: string; + }; + testCoverage: { + letter: string; + value: string; + }; + numberOfCodeSmells: number; + numberOfDuplication: number; + numberOfOtherIssues: number; +}; + +// Warning: (ae-missing-release-tag) "codeClimatePlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const codeClimatePlugin: BackstagePlugin< + { + root: RouteRef; + }, + {} +>; + +// Warning: (ae-missing-release-tag) "EntityCodeClimateCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const EntityCodeClimateCard: () => JSX.Element; + +// Warning: (ae-missing-release-tag) "MockCodeClimateApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class MockCodeClimateApi implements CodeClimateApi { + // (undocumented) + fetchData(): Promise; +} + +// Warning: (ae-missing-release-tag) "mockData" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const mockData: CodeClimateData; + +// Warning: (ae-missing-release-tag) "ProductionCodeClimateApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class ProductionCodeClimateApi implements CodeClimateApi { + constructor(discoveryApi: DiscoveryApi); + // (undocumented) + fetchAllData(options: { + apiUrl: string; + repoID: string; + snapshotID: string; + testReportID: string; + }): Promise; + // (undocumented) + fetchData(repoID: string): Promise; +} +``` diff --git a/plugins/code-climate/dev/index.tsx b/plugins/code-climate/dev/index.tsx new file mode 100644 index 0000000000..d5fc4c28e3 --- /dev/null +++ b/plugins/code-climate/dev/index.tsx @@ -0,0 +1,93 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { createDevApp, EntityGridItem } from '@backstage/dev-utils'; +import { Grid } from '@material-ui/core'; +import React from 'react'; +import { + EntityCodeClimateCard, + MockCodeClimateApi, + CodeClimateApi, + codeClimateApiRef, +} from '../src'; +import { CODECLIMATE_REPO_ID_ANNOTATION } from '../src/plugin'; +import { Content, Header, Page } from '@backstage/core-components'; + +const entity = (name?: string) => + ({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + annotations: { + [CODECLIMATE_REPO_ID_ANNOTATION]: name, + }, + name: name, + }, + } as Entity); + +createDevApp() + .registerApi({ + api: codeClimateApiRef, + deps: {}, + factory: () => + ({ + fetchData: async (repoID: string) => { + switch (repoID) { + case 'error': + throw new Error('Error!'); + + case 'never': + return new Promise(() => {}); + + case 'no-values': + return undefined; + + case 'with-values': + return new MockCodeClimateApi().fetchData(); + + default: + return []; + } + }, + } as CodeClimateApi), + }) + .addPage({ + title: 'Cards', + element: ( + +
+ + + + + + + + + + + + + + + + + + ), + }) + .render(); diff --git a/plugins/code-climate/docs/code-climate-card.png b/plugins/code-climate/docs/code-climate-card.png new file mode 100644 index 0000000000000000000000000000000000000000..b94e23eebe4ce2dd54ec40d3910ad48ecd54ef7d GIT binary patch literal 65389 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℘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 literal 0 HcmV?d00001 diff --git a/plugins/code-climate/package.json b/plugins/code-climate/package.json new file mode 100644 index 0000000000..ef1c83c522 --- /dev/null +++ b/plugins/code-climate/package.json @@ -0,0 +1,56 @@ +{ + "name": "@backstage/plugin-code-climate", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/catalog-model": "^0.10.0", + "@backstage/core-components": "^0.8.9", + "@backstage/core-plugin-api": "^0.6.1", + "@backstage/plugin-catalog-react": "^0.6.15", + "@backstage/theme": "^0.2.15", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.57", + "humanize-duration": "^3.27.1", + "luxon": "^2.0.2", + "react-router": "6.0.0-beta.0", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.14.0", + "@backstage/dev-utils": "^0.2.22", + "@backstage/test-utils": "^0.2.5", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^11.2.5", + "@testing-library/user-event": "^13.1.8", + "@types/humanize-duration": "^3.27.1", + "@types/jest": "^26.0.7", + "@types/luxon": "^2.0.9", + "@types/node": "^14.14.32", + "cross-fetch": "^3.1.5", + "msw": "^0.35.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/code-climate/src/api/code-climate-api.ts b/plugins/code-climate/src/api/code-climate-api.ts new file mode 100644 index 0000000000..fb4f41ebb5 --- /dev/null +++ b/plugins/code-climate/src/api/code-climate-api.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CodeClimateData } from './code-climate-data'; +import { createApiRef } from '@backstage/core-plugin-api'; + +export const codeClimateApiRef = createApiRef({ + id: 'plugin.code-climate.service', +}); + +export interface CodeClimateApi { + fetchData(repoID: string): Promise; +} diff --git a/plugins/code-climate/src/api/code-climate-data.ts b/plugins/code-climate/src/api/code-climate-data.ts new file mode 100644 index 0000000000..9271905157 --- /dev/null +++ b/plugins/code-climate/src/api/code-climate-data.ts @@ -0,0 +1,184 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type CodeClimateRepoData = { + data: { + id: string; + type: string; + attributes: { + analysis_version: number; + badge_token: string; + branch: string; + created_at: string; + delegated_config_repo_id: string; + diff_coverage_enforced: boolean; + diff_coverage_threshold: number; + enable_notifications: boolean; + github_slug: string; + human_name: string; + last_activity_at: string; + test_reporter_id: string | null; + total_coverage_enforced: boolean; + vcs_database_id: string; + vcs_host: string; + score: string | null; + }; + relationships: { + latest_default_branch_snapshot: { + data: { id: string; type: string }; + }; + latest_default_branch_test_report: { + data: { id: string; type: string }; + }; + account: { + data: { id: string; type: string }; + }; + }; + links: { + self: string; + services: string; + web_coverage: string; + web_issues: string; + maintainability_badge: string; + test_coverage_badge: string; + }; + meta: { permissions: { admin: boolean } }; + }; +}; + +export type CodeClimateMaintainabilityData = { + data: { + id: string; + type: string; + attributes: { + commit_sha: string; + committed_at: string; + created_at: string; + lines_of_code: number; + ratings: [ + { + path: string; + letter: string; + measure: { + value: number; + unit: string; + meta: { + remediation_time: { value: number; unit: string }; + implementation_time: { + value: number; + unit: string; + }; + }; + }; + pillar: string; + }, + ]; + gpa: string | null; + worker_version: number; + }; + meta: { + issues_count: number; + measures: { + remediation: { value: number; unit: string }; + technical_debt_ratio: { + value: number; + unit: string; + meta: { + remediation_time: { value: number; unit: string }; + implementation_time: { + value: number; + unit: string; + }; + }; + }; + }; + }; + }; +}; + +export type CodeClimateTestCoverageData = { + data: { + id: string; + type: string; + attributes: { + branch: string; + commit_sha: string; + committed_at: string; + covered_percent: number; + lines_of_code: number; + rating: { + path: string; + letter: string; + measure: { value: number; unit: string }; + pillar: string; + }; + received_at: string; + state: string; + }; + }; +}; + +export type CodeClimateIssuesData = { + data: [ + { + id: string; + type: string; + attributes: { + categories: string[]; + check_name: string; + constant_name: string; + content: { body: string }; + description: string; + engine_name: string; + fingerprint: string; + location: { + path: string; + end_line: number; + start_line: number; + }; + other_locations: string[]; + remediation_points: number; + severity: string; + }; + meta: { permissions: { manageable: boolean } }; + }, + ]; + links: { + self: string; + next: string; + last: string; + }; + meta: { current_page: number; total_pages: number; total_count: number }; +}; + +export type CodeClimateData = { + repoID: string; + maintainability: { + letter: string; + value: string; + }; + testCoverage: { + letter: string; + value: string; + }; + numberOfCodeSmells: number; + numberOfDuplication: number; + numberOfOtherIssues: number; +}; + +export type CodeClimateApiError = { + detail: string; +}; diff --git a/plugins/code-climate/src/api/index.ts b/plugins/code-climate/src/api/index.ts new file mode 100644 index 0000000000..60fc079698 --- /dev/null +++ b/plugins/code-climate/src/api/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './mock'; +export type { CodeClimateApi } from './code-climate-api'; +export { codeClimateApiRef } from './code-climate-api'; +export type { CodeClimateData } from './code-climate-data'; +export { ProductionCodeClimateApi } from './production-api'; diff --git a/plugins/code-climate/src/api/mock/code-climate-maintainability-mock.json b/plugins/code-climate/src/api/mock/code-climate-maintainability-mock.json new file mode 100644 index 0000000000..46a7905e4b --- /dev/null +++ b/plugins/code-climate/src/api/mock/code-climate-maintainability-mock.json @@ -0,0 +1,49 @@ +{ + "data": { + "id": "6b8cc37a64b741dd9d516119", + "type": "snapshots", + "attributes": { + "commit_sha": "d670460b4b4aece5915caf5c68d12f560a9fe3e4", + "committed_at": "2022-01-14T15:17:29.311Z", + "created_at": "2022-01-31T10:07:40.415Z", + "lines_of_code": 8854, + "ratings": [ + { + "path": "/", + "letter": "B", + "measure": { + "value": 5.264879308188034, + "unit": "percent", + "meta": { + "remediation_time": { "value": 7186.52, "unit": "minute" }, + "implementation_time": { + "value": 136499.2353922225, + "unit": "minute" + } + } + }, + "pillar": "Maintainability" + } + ], + "gpa": null, + "worker_version": 69440 + }, + "meta": { + "issues_count": 127, + "measures": { + "remediation": { "value": 7186.52, "unit": "minute" }, + "technical_debt_ratio": { + "value": 5.264879308188034, + "unit": "percent", + "meta": { + "remediation_time": { "value": 7186.52, "unit": "minute" }, + "implementation_time": { + "value": 136499.2353922225, + "unit": "minute" + } + } + } + } + } + } +} diff --git a/plugins/code-climate/src/api/mock/code-climate-test-coverage-mock.json b/plugins/code-climate/src/api/mock/code-climate-test-coverage-mock.json new file mode 100644 index 0000000000..203dd81776 --- /dev/null +++ b/plugins/code-climate/src/api/mock/code-climate-test-coverage-mock.json @@ -0,0 +1,21 @@ +{ + "data": { + "id": "6b8cc37a64b741dd9d516119", + "type": "test_reports", + "attributes": { + "branch": "master", + "commit_sha": "d670460b4b4aece5915caf5c68d12f560a9fe3e4", + "committed_at": "2022-01-14T15:17:26.000Z", + "covered_percent": 88.41059602649007, + "lines_of_code": 8854, + "rating": { + "path": "/", + "letter": "B", + "measure": { "value": 88.41059602649007, "unit": "percent" }, + "pillar": "Test Coverage" + }, + "received_at": "2022-01-14T15:25:38.352Z", + "state": "done" + } + } +} diff --git a/plugins/code-climate/src/api/mock/index.ts b/plugins/code-climate/src/api/mock/index.ts new file mode 100644 index 0000000000..56ba9c7974 --- /dev/null +++ b/plugins/code-climate/src/api/mock/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { mockData } from './mock-api'; +export { MockCodeClimateApi } from './mock-api'; diff --git a/plugins/code-climate/src/api/mock/mock-api.ts b/plugins/code-climate/src/api/mock/mock-api.ts new file mode 100644 index 0000000000..74e4d83027 --- /dev/null +++ b/plugins/code-climate/src/api/mock/mock-api.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CodeClimateData } from '../code-climate-data'; +import { CodeClimateApi } from '../code-climate-api'; +import maintainabilityMock from './code-climate-maintainability-mock.json'; +import testCoverageMock from './code-climate-test-coverage-mock.json'; +import { Duration } from 'luxon'; +import humanizeDuration from 'humanize-duration'; + +const maintainabilityData = maintainabilityMock.data.attributes.ratings[0]; +const testCoverageData = testCoverageMock.data.attributes.rating; + +const maintainabilityValue: any = {}; +maintainabilityValue[ + maintainabilityData.measure.meta.implementation_time.unit +] = maintainabilityData.measure.meta.implementation_time.value.toFixed(); + +export const mockData: CodeClimateData = { + repoID: '6b8cc37a64b741dd9d516119', + maintainability: { + letter: maintainabilityData.letter, + value: humanizeDuration( + Duration.fromObject(maintainabilityValue).toMillis(), + { largest: 1 }, + ), + }, + testCoverage: { + letter: testCoverageData.letter, + value: testCoverageData.measure.value.toFixed(), + }, + numberOfCodeSmells: 97, + numberOfDuplication: 49, + numberOfOtherIssues: 26, +}; + +export class MockCodeClimateApi implements CodeClimateApi { + fetchData(): Promise { + return new Promise(resolve => { + setTimeout(() => resolve(mockData), 800); + }); + } +} diff --git a/plugins/code-climate/src/api/production-api.ts b/plugins/code-climate/src/api/production-api.ts new file mode 100644 index 0000000000..665d7e9f63 --- /dev/null +++ b/plugins/code-climate/src/api/production-api.ts @@ -0,0 +1,160 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + CodeClimateData, + CodeClimateRepoData, + CodeClimateMaintainabilityData, + CodeClimateTestCoverageData, + CodeClimateIssuesData, +} from './code-climate-data'; +import { CodeClimateApi } from './code-climate-api'; +import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { Duration } from 'luxon'; +import humanizeDuration from 'humanize-duration'; + +const pageQuery = encodeURIComponent('page[size]'); +const statusFilter = encodeURIComponent('filter[status][$in][]'); +const categoriesFilter = encodeURIComponent('filter[categories][$in][]'); +const basicIssuesOptions = `${pageQuery}=1&${statusFilter}=open&${statusFilter}=confirmed`; +const codeSmellsQuery = `${basicIssuesOptions}&${categoriesFilter}=Complexity`; +const duplicationQuery = `${basicIssuesOptions}&${categoriesFilter}=Duplication`; +const otherIssuesQuery = `${basicIssuesOptions}&${categoriesFilter}=Bug%20Risk`; + +export class ProductionCodeClimateApi implements CodeClimateApi { + constructor(private readonly discoveryApi: DiscoveryApi) {} + + async fetchAllData(options: { + apiUrl: string; + repoID: string; + snapshotID: string; + testReportID: string; + }): Promise { + const { apiUrl, repoID, snapshotID, testReportID } = options; + + const [ + maintainabilityResponse, + testCoverageResponse, + codeSmellsResponse, + duplicationResponse, + otherIssuesResponse, + ] = await Promise.all([ + await fetch(`${apiUrl}/repos/${repoID}/snapshots/${snapshotID}`), + await fetch(`${apiUrl}/repos/${repoID}/test_reports/${testReportID}`), + await fetch( + `${apiUrl}/repos/${repoID}/snapshots/${snapshotID}/issues?${codeSmellsQuery}`, + ), + await fetch( + `${apiUrl}/repos/${repoID}/snapshots/${snapshotID}/issues?${duplicationQuery}`, + ), + await fetch( + `${apiUrl}/repos/${repoID}/snapshots/${snapshotID}/issues?${otherIssuesQuery}`, + ), + ]); + + if ( + !maintainabilityResponse.ok || + !testCoverageResponse.ok || + !codeSmellsResponse.ok || + !duplicationResponse.ok || + !otherIssuesResponse.ok + ) { + throw new Error('Failed fetching Code Climate info'); + } + + const maintainabilityData = ( + (await maintainabilityResponse.json()) as CodeClimateMaintainabilityData + ).data.attributes.ratings[0]; + const testCoverageData = ( + (await testCoverageResponse.json()) as CodeClimateTestCoverageData + ).data.attributes.rating; + const codeSmellsData = ( + (await codeSmellsResponse.json()) as CodeClimateIssuesData + ).meta.total_count; + const duplicationData = ( + (await duplicationResponse.json()) as CodeClimateIssuesData + ).meta.total_count; + const otherIssuesData = ( + (await otherIssuesResponse.json()) as CodeClimateIssuesData + ).meta.total_count; + + return [ + maintainabilityData, + testCoverageData, + codeSmellsData, + duplicationData, + otherIssuesData, + ]; + } + + async fetchData(repoID: string): Promise { + if (!repoID) { + throw new Error('No Repo id found'); + } + + const apiUrl = `${await this.discoveryApi.getBaseUrl( + 'proxy', + )}/codeclimate/api`; + + const repoResponse = await fetch(`${apiUrl}/repos/${repoID}`); + + if (!repoResponse.ok) { + throw new Error('Failed fetching Code Climate info'); + } + + const repoData = ((await repoResponse.json()) as CodeClimateRepoData).data; + const snapshotID = + repoData.relationships.latest_default_branch_snapshot.data.id; + const testReportID = + repoData.relationships.latest_default_branch_test_report.data.id; + + const [ + maintainabilityData, + testCoverageData, + codeSmellsData, + duplicationData, + otherIssuesData, + ] = await this.fetchAllData({ + apiUrl, + repoID, + snapshotID, + testReportID, + }); + + const maintainabilityValue: any = {}; + maintainabilityValue[ + maintainabilityData.measure.meta.implementation_time.unit + ] = maintainabilityData.measure.meta.implementation_time.value.toFixed(); + + return { + repoID, + maintainability: { + letter: maintainabilityData.letter, + value: humanizeDuration( + Duration.fromObject(maintainabilityValue).toMillis(), + { largest: 1 }, + ), + }, + testCoverage: { + letter: testCoverageData.letter, + value: testCoverageData.measure.value.toFixed(), + }, + numberOfCodeSmells: codeSmellsData, + numberOfDuplication: duplicationData, + numberOfOtherIssues: otherIssuesData, + }; + } +} diff --git a/plugins/code-climate/src/components/CodeClimateCard/CodeClimateCard.tsx b/plugins/code-climate/src/components/CodeClimateCard/CodeClimateCard.tsx new file mode 100644 index 0000000000..82af326690 --- /dev/null +++ b/plugins/code-climate/src/components/CodeClimateCard/CodeClimateCard.tsx @@ -0,0 +1,25 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { InfoCard } from '@backstage/core-components'; +import { CodeClimateCardContents } from '../CodeClimateCardContents'; + +export const CodeClimateCard = () => ( + + + +); diff --git a/plugins/code-climate/src/components/CodeClimateCard/index.ts b/plugins/code-climate/src/components/CodeClimateCard/index.ts new file mode 100644 index 0000000000..51eaaf71af --- /dev/null +++ b/plugins/code-climate/src/components/CodeClimateCard/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { CodeClimateCard } from './CodeClimateCard'; diff --git a/plugins/code-climate/src/components/CodeClimateCardContents/CodeClimateCardContents.tsx b/plugins/code-climate/src/components/CodeClimateCardContents/CodeClimateCardContents.tsx new file mode 100644 index 0000000000..c9dc1894fc --- /dev/null +++ b/plugins/code-climate/src/components/CodeClimateCardContents/CodeClimateCardContents.tsx @@ -0,0 +1,64 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import useAsync from 'react-use/lib/useAsync'; +import { codeClimateApiRef } from '../../api'; +import { CodeClimateTable } from '../CodeClimateTable'; +import { CODECLIMATE_REPO_ID_ANNOTATION } from '../../plugin'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { + EmptyState, + ErrorPanel, + MissingAnnotationEmptyState, + Progress, +} from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; + +export const CodeClimateCardContents = () => { + const { entity } = useEntity(); + const codeClimateApi = useApi(codeClimateApiRef); + + const repoID = + entity?.metadata.annotations?.[CODECLIMATE_REPO_ID_ANNOTATION] ?? ''; + + const { loading, value, error } = useAsync( + () => codeClimateApi.fetchData(repoID), + [codeClimateApi, repoID], + ); + + if (loading) { + return ; + } else if (!repoID) { + return ( + + ); + } else if (error) { + return ; + } else if (!value) { + return ( + + ); + } + + return ; +}; diff --git a/plugins/code-climate/src/components/CodeClimateCardContents/index.ts b/plugins/code-climate/src/components/CodeClimateCardContents/index.ts new file mode 100644 index 0000000000..29f59be2a3 --- /dev/null +++ b/plugins/code-climate/src/components/CodeClimateCardContents/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { CodeClimateCardContents } from './CodeClimateCardContents'; diff --git a/plugins/code-climate/src/components/CodeClimateTable/CodeClimateTable.test.tsx b/plugins/code-climate/src/components/CodeClimateTable/CodeClimateTable.test.tsx new file mode 100644 index 0000000000..a2ced6f5a2 --- /dev/null +++ b/plugins/code-climate/src/components/CodeClimateTable/CodeClimateTable.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { CodeClimateTable } from './CodeClimateTable'; +import { mockData } from '../../api/mock'; +import { ThemeProvider } from '@material-ui/core'; +import { lightTheme } from '@backstage/theme'; + +describe('CodeClimateTable', () => { + it('should render values in a table', async () => { + const table = await render( + + + , + ); + expect(await table.findByText('3 months')).toBeInTheDocument(); + expect(await table.findByText('88%')).toBeInTheDocument(); + expect(await table.findByText('97')).toBeInTheDocument(); + expect(await table.findByText('49')).toBeInTheDocument(); + expect(await table.findByText('26')).toBeInTheDocument(); + }); +}); diff --git a/plugins/code-climate/src/components/CodeClimateTable/CodeClimateTable.tsx b/plugins/code-climate/src/components/CodeClimateTable/CodeClimateTable.tsx new file mode 100644 index 0000000000..e8e204ff9a --- /dev/null +++ b/plugins/code-climate/src/components/CodeClimateTable/CodeClimateTable.tsx @@ -0,0 +1,212 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { CodeClimateData } from '../../api'; +import { Link } from '@backstage/core-components'; +import { Box, makeStyles, Typography } from '@material-ui/core'; +import { BackstageTheme } from '@backstage/theme'; + +const letterStyle = { + color: 'white', + border: 0, + borderRadius: '3px', + fontSize: '40px', + padding: '5px 20px', +}; + +const fontSize = { + fontSize: '25px', +}; + +const letterColor = (letter: string) => { + if (letter === 'A') { + return '#45d298'; + } else if (letter === 'B') { + return '#a5d86e'; + } else if (letter === 'C') { + return '#f1ce0c'; + } else if (letter === 'D') { + return '#f29141'; + } else if (letter === 'F') { + return '#df5869'; + } + + return '#45d298'; +}; + +const useStyles = makeStyles< + BackstageTheme, + { + maintainabilityLetter: string; + testCoverageLetter: string; + } +>({ + spaceAround: { + display: 'flex', + justifyContent: 'space-around', + }, + spaceBetween: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + }, + maintainabilityLetterColor: { + ...letterStyle, + backgroundColor: props => letterColor(props.maintainabilityLetter), + }, + testCoverageLetterColor: { + ...letterStyle, + backgroundColor: props => letterColor(props.testCoverageLetter), + }, + fontSize: { + ...fontSize, + }, + letterDetails: { + ...fontSize, + paddingLeft: '10px', + }, + paddingSides20: { + padding: '0px 20px', + }, +}); + +export const CodeClimateTable = ({ + codeClimateData, +}: { + codeClimateData: CodeClimateData; +}) => { + const { + repoID, + maintainability: { + letter: maintainabilityLetter, + value: maintainabilityValue, + }, + testCoverage: { letter: testCoverageLetter, value: testCoverageValue }, + numberOfCodeSmells, + numberOfDuplication, + numberOfOtherIssues, + } = codeClimateData; + + const classes = useStyles({ maintainabilityLetter, testCoverageLetter }); + + if (!codeClimateData) { + return null; + } + + return ( + <> +
+
+ + Maintainability + +
+ + {maintainabilityLetter} + + + + {maintainabilityValue} + + +
+
+
+ + Test Coverage + +
+ + {testCoverageLetter} + + + + {testCoverageValue}% + + +
+
+
+ +
+ + Code Smells: + + + + {numberOfCodeSmells} + + +
+ + + Duplication: + + + + {numberOfDuplication} + + + +
+ + Other Issues: + + + + {numberOfOtherIssues} + + +
+
+ + ); +}; diff --git a/plugins/code-climate/src/components/CodeClimateTable/index.ts b/plugins/code-climate/src/components/CodeClimateTable/index.ts new file mode 100644 index 0000000000..bdf79bb94b --- /dev/null +++ b/plugins/code-climate/src/components/CodeClimateTable/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { CodeClimateTable } from './CodeClimateTable'; diff --git a/plugins/code-climate/src/index.ts b/plugins/code-climate/src/index.ts new file mode 100644 index 0000000000..b6ef174b8f --- /dev/null +++ b/plugins/code-climate/src/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * A Backstage plugin that integrates towards Code Climate + * + * @packageDocumentation + */ + +export * from './api'; +export { codeClimatePlugin, EntityCodeClimateCard } from './plugin'; diff --git a/plugins/code-climate/src/plugin.test.ts b/plugins/code-climate/src/plugin.test.ts new file mode 100644 index 0000000000..ea791f2c87 --- /dev/null +++ b/plugins/code-climate/src/plugin.test.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { codeClimatePlugin } from './plugin'; + +describe('codeclimate', () => { + it('should export plugin', () => { + expect(codeClimatePlugin).toBeDefined(); + }); +}); diff --git a/plugins/code-climate/src/plugin.ts b/plugins/code-climate/src/plugin.ts new file mode 100644 index 0000000000..dcf86f1e19 --- /dev/null +++ b/plugins/code-climate/src/plugin.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ProductionCodeClimateApi, codeClimateApiRef } from './api'; +import { + createApiFactory, + createPlugin, + createRouteRef, + discoveryApiRef, + identityApiRef, + createComponentExtension, +} from '@backstage/core-plugin-api'; + +export const CODECLIMATE_REPO_ID_ANNOTATION = 'codeclimate.com/repo-id'; + +export const rootRouteRef = createRouteRef({ + id: 'code-climate', +}); + +export const codeClimatePlugin = createPlugin({ + id: 'code-climate', + apis: [ + createApiFactory({ + api: codeClimateApiRef, + deps: { + discoveryApi: discoveryApiRef, + identityApi: identityApiRef, + }, + factory: ({ discoveryApi }) => new ProductionCodeClimateApi(discoveryApi), + }), + ], + routes: { + root: rootRouteRef, + }, +}); + +export const EntityCodeClimateCard = codeClimatePlugin.provide( + createComponentExtension({ + name: 'EntityCodeClimateCard', + component: { + lazy: () => + import('./components/CodeClimateCard').then(m => m.CodeClimateCard), + }, + }), +); diff --git a/plugins/code-climate/src/setupTests.ts b/plugins/code-climate/src/setupTests.ts new file mode 100644 index 0000000000..43d9e09713 --- /dev/null +++ b/plugins/code-climate/src/setupTests.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable import/no-extraneous-dependencies */ + +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/yarn.lock b/yarn.lock index bbd75035ce..a3adc4d7bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5785,6 +5785,11 @@ resolved "https://registry.npmjs.org/@types/humanize-duration/-/humanize-duration-3.25.1.tgz#b6140d5fc00ff3917b3f521784abef4bc0387ccc" integrity sha512-WZU/4bb+lvzyDmZzjJtp++9mfKy6B3lH6gGISgkcz6SU8hMILKRM0vi08TxIsb0dQB4Gzo68MWLmctu6xqUi9g== +"@types/humanize-duration@^3.27.1": + version "3.27.1" + resolved "https://registry.npmjs.org/@types/humanize-duration/-/humanize-duration-3.27.1.tgz#f14740d1f585a0a8e3f46359b62fda8b0eaa31e7" + integrity sha512-K3e+NZlpCKd6Bd/EIdqjFJRFHbrq5TzPPLwREk5Iv/YoIjQrs6ljdAUCo+Lb2xFlGNOjGSE0dqsVD19cZL137w== + "@types/inquirer@^7.3.3": version "7.3.3" resolved "https://registry.npmjs.org/@types/inquirer/-/inquirer-7.3.3.tgz#92e6676efb67fa6925c69a2ee638f67a822952ac" @@ -5954,6 +5959,11 @@ resolved "https://registry.npmjs.org/@types/luxon/-/luxon-2.0.9.tgz#782a0edfa6d699191292c13168bd496cd66b87c6" integrity sha512-ZuzIc7aN+i2ZDMWIiSmMdubR9EMMSTdEzF6R+FckP4p6xdnOYKqknTo/k+xXQvciSXlNGIwA4OPU5X7JIFzYdA== +"@types/luxon@^2.0.9": + version "2.0.9" + resolved "https://registry.npmjs.org/@types/luxon/-/luxon-2.0.9.tgz#782a0edfa6d699191292c13168bd496cd66b87c6" + integrity sha512-ZuzIc7aN+i2ZDMWIiSmMdubR9EMMSTdEzF6R+FckP4p6xdnOYKqknTo/k+xXQvciSXlNGIwA4OPU5X7JIFzYdA== + "@types/mdast@^3.0.0", "@types/mdast@^3.0.3": version "3.0.3" resolved "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.3.tgz#2d7d671b1cd1ea3deb306ea75036c2a0407d2deb" @@ -13948,7 +13958,7 @@ human-signals@^2.1.0: resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== -humanize-duration@^3.25.1, humanize-duration@^3.26.0, humanize-duration@^3.27.0: +humanize-duration@^3.25.1, humanize-duration@^3.26.0, humanize-duration@^3.27.0, humanize-duration@^3.27.1: version "3.27.1" resolved "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.27.1.tgz#2cd4ea4b03bd92184aee6d90d77a8f3d7628df69" integrity sha512-jCVkMl+EaM80rrMrAPl96SGG4NRac53UyI1o/yAzebDntEY6K6/Fj2HOjdPg8omTqIe5Y0wPBai2q5xXrIbarA==