├── .all-contributorsrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.cjs ├── .github ├── dependabot.yml └── workflows │ ├── auto-merge.yml │ ├── ci-build.yml │ └── dependabot-rebase.yml ├── .gitignore ├── .prettierrc.js ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── RELEASE.md ├── VERSIONING_POLICY.md ├── checkup.code-workspace ├── docs ├── SPEC.md ├── checkup-run-output.png └── logo.png ├── package.json ├── packages ├── checkup-plugin-ember │ ├── README.md │ ├── docs │ │ └── tasks │ │ │ ├── ember-dependencies-task.md │ │ │ ├── ember-in-repo-addons-engines-task.md │ │ │ ├── ember-octane-migration-status-task.md │ │ │ ├── ember-template-lint-disable-task.md │ │ │ ├── ember-template-lint-summary-task.md │ │ │ ├── ember-test-types-task.md │ │ │ └── ember-types-task.md │ ├── package.json │ ├── src │ │ ├── actions │ │ │ ├── ember-template-lint-disable-actions.ts │ │ │ ├── ember-template-lint-summary-actions.ts │ │ │ └── ember-test-types-actions.ts │ │ ├── eslint │ │ │ └── rules │ │ │ │ ├── package.json │ │ │ │ └── test-types.js │ │ ├── index.ts │ │ ├── tasks │ │ │ ├── ember-dependencies-task.ts │ │ │ ├── ember-in-repo-addons-engines-task.ts │ │ │ ├── ember-octane-migration-status-task.ts │ │ │ ├── ember-template-lint-disable-task.ts │ │ │ ├── ember-template-lint-summary-task.ts │ │ │ ├── ember-test-types-task.ts │ │ │ └── ember-types-task.ts │ │ └── types │ │ │ └── index.ts │ ├── tests │ │ ├── __fixtures__ │ │ │ ├── addon-package.json │ │ │ └── app-package.json │ │ ├── __snapshots__ │ │ │ ├── ember-octane-migration-status-task-test.ts.snap │ │ │ └── ember-test-types-task-test.ts.snap │ │ ├── ember-dependencies-task-test.ts │ │ ├── ember-in-repo-addons-engines-task-test.ts │ │ ├── ember-octane-migration-status-task-test.ts │ │ ├── ember-template-lint-disable-task-test.ts │ │ ├── ember-template-lint-summary-task-test.ts │ │ ├── ember-test-types-task-test.ts │ │ ├── ember-types-task-test.ts │ │ ├── eslint-rule-tests │ │ │ └── test-types.test.js │ │ └── tsconfig.json │ ├── tsconfig.json │ └── vitest.config.ts ├── checkup-plugin-javascript │ ├── README.md │ ├── docs │ │ └── tasks │ │ │ ├── eslint-disable-task.md │ │ │ ├── eslint-summary-task.md │ │ │ ├── lines-of-code-task.md │ │ │ └── outdated-dependencies-task.md │ ├── package.json │ ├── src │ │ ├── actions │ │ │ ├── eslint-disable-actions.ts │ │ │ ├── eslint-summary-actions.ts │ │ │ └── outdated-dependency-actions.ts │ │ ├── index.ts │ │ └── tasks │ │ │ ├── eslint-disable-task.ts │ │ │ ├── eslint-summary-task.ts │ │ │ ├── lines-of-code-task.ts │ │ │ ├── outdated-dependencies-task.ts │ │ │ └── valid-esm-package-task.ts │ ├── tests │ │ ├── eslint-disable-task-test.ts │ │ ├── eslint-summary-task-test.ts │ │ ├── lines-of-code-task-test.ts │ │ ├── outdated-dependencies-task-test.ts │ │ ├── tsconfig.json │ │ └── valid-esm-package-task-test.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── cli │ ├── README.md │ ├── bin │ │ └── checkup.js │ ├── package.json │ ├── src │ │ ├── api │ │ │ ├── checkup-task-runner.ts │ │ │ └── generator.ts │ │ ├── checkup.ts │ │ ├── commands │ │ │ ├── generate.ts │ │ │ ├── generate │ │ │ │ ├── actions.ts │ │ │ │ ├── config.ts │ │ │ │ ├── plugin.ts │ │ │ │ └── task.ts │ │ │ └── run.ts │ │ ├── formatters │ │ │ ├── available-tasks.ts │ │ │ ├── base-formatter.ts │ │ │ ├── components │ │ │ │ ├── Pretty.tsx │ │ │ │ └── Summary.tsx │ │ │ ├── get-formatter.ts │ │ │ ├── json.ts │ │ │ ├── pretty.ts │ │ │ ├── sonarqube.ts │ │ │ ├── stylish.ts │ │ │ └── summary.ts │ │ ├── generators │ │ │ ├── actions.ts │ │ │ ├── base-generator.ts │ │ │ ├── config.ts │ │ │ ├── plugin.ts │ │ │ └── task.ts │ │ ├── index.ts │ │ ├── task-list.ts │ │ ├── task-result-comparator.ts │ │ └── utils │ │ │ └── get-version.ts │ ├── templates │ │ └── src │ │ │ ├── actions │ │ │ └── src │ │ │ │ └── actions │ │ │ │ ├── actions.js.ejs │ │ │ │ └── actions.ts.ejs │ │ │ ├── plugin │ │ │ ├── .eslintignore.ejs │ │ │ ├── .eslintrc.js.ejs │ │ │ ├── .eslintrc.ts.ejs │ │ │ ├── .gitignore.ejs │ │ │ ├── .prettierrc.js.ejs │ │ │ ├── README.md.ejs │ │ │ ├── __tests__ │ │ │ │ └── .gitkeep │ │ │ ├── jest.config.js.ejs │ │ │ ├── jest.config.ts.ejs │ │ │ ├── package.json.js.ejs │ │ │ ├── package.json.ts.ejs │ │ │ ├── src │ │ │ │ ├── index.js.ejs │ │ │ │ ├── index.ts.ejs │ │ │ │ ├── results │ │ │ │ │ └── .gitkeep │ │ │ │ ├── tasks │ │ │ │ │ └── .gitkeep │ │ │ │ └── types │ │ │ │ │ ├── index.js.ejs │ │ │ │ │ └── index.ts.ejs │ │ │ └── tsconfig.json.ejs │ │ │ └── task │ │ │ ├── __tests__ │ │ │ ├── task.js.ejs │ │ │ └── task.ts.ejs │ │ │ └── src │ │ │ └── tasks │ │ │ ├── task.js.ejs │ │ │ └── task.ts.ejs │ ├── tests │ │ ├── __fixtures__ │ │ │ ├── checkup-formatter-test │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── checkup-no-result-found.sarif │ │ │ ├── checkup-result-short.sarif │ │ │ └── checkup-result.sarif │ │ ├── __utils__ │ │ │ ├── fake-project.ts │ │ │ ├── generator-utils.ts │ │ │ ├── get-fixture.ts │ │ │ └── mock-task-result.ts │ │ ├── checkup-task-runner-test.ts │ │ ├── cli-test.ts │ │ ├── formatters │ │ │ ├── json-test.ts │ │ │ ├── pretty-test.ts │ │ │ ├── sonarqube-test.ts │ │ │ ├── stylish-test.ts │ │ │ └── summary-test.ts │ │ ├── generators │ │ │ ├── __snapshots__ │ │ │ │ ├── generate-actions-test.ts.snap │ │ │ │ ├── generate-plugin-test.ts.snap │ │ │ │ └── generate-task-test.ts.snap │ │ │ ├── generate-actions-test.ts │ │ │ ├── generate-config-test.ts │ │ │ ├── generate-plugin-test.ts │ │ │ └── generate-task-test.ts │ │ ├── task-list-test.ts │ │ ├── task-result-comparator-test.ts │ │ └── tsconfig.json │ ├── tsconfig.json │ └── vitest.config.ts ├── core │ ├── README.md │ ├── jest.config.cjs │ ├── jest.setup.ts │ ├── package.json │ ├── src │ │ ├── actions │ │ │ ├── actions-evaluator.ts │ │ │ └── registered-actions.ts │ │ ├── analyzers │ │ │ ├── ast-analyzer.ts │ │ │ ├── dependency-analyzer.ts │ │ │ ├── ember-template-lint-analyzer.ts │ │ │ ├── eslint-analyzer.ts │ │ │ ├── handlebars-analyzer.ts │ │ │ ├── javascript-analyzer.ts │ │ │ ├── json-analyzer.ts │ │ │ ├── stylelint-analyzer.ts │ │ │ └── typescript-analyzer.ts │ │ ├── ast │ │ │ └── ast-transformer.ts │ │ ├── base-migration-task.ts │ │ ├── base-task.ts │ │ ├── base-validation-task.ts │ │ ├── config.ts │ │ ├── data │ │ │ ├── checkup-log-builder.ts │ │ │ ├── checkup-log-parser.ts │ │ │ ├── formatters.ts │ │ │ ├── lint.ts │ │ │ ├── path.ts │ │ │ └── sarif-log-builder.ts │ │ ├── errors │ │ │ ├── checkup-error.ts │ │ │ ├── error-kind.ts │ │ │ └── task-error.ts │ │ ├── index.ts │ │ ├── schemas │ │ │ ├── config-schema.json │ │ │ └── task-result-schema.json │ │ ├── today-format.ts │ │ ├── types │ │ │ ├── analyzers.ts │ │ │ ├── checkup-log.ts │ │ │ ├── checkup-result.ts │ │ │ ├── cli.ts │ │ │ ├── config.ts │ │ │ ├── dependency.ts │ │ │ ├── ember-template-lint.ts │ │ │ └── tasks.ts │ │ └── utils │ │ │ ├── base-output-writer.ts │ │ │ ├── buffered-writer.ts │ │ │ ├── console-writer.ts │ │ │ ├── exec.ts │ │ │ ├── extract-stack.ts │ │ │ ├── file-path-array.ts │ │ │ ├── file-writer.ts │ │ │ ├── get-package-json.ts │ │ │ ├── get-paths.ts │ │ │ ├── get-version.ts │ │ │ ├── merge-lint-config.ts │ │ │ ├── normalize-package-name.ts │ │ │ ├── path.ts │ │ │ ├── plugin-name.ts │ │ │ ├── repository.ts │ │ │ ├── resolve-module-path.ts │ │ │ └── type-guards.ts │ ├── tests │ │ ├── __fixtures__ │ │ │ ├── .checkuprc.json │ │ │ ├── checkup-result-all-tasks.sarif │ │ │ ├── checkup-result-single-task.sarif │ │ │ ├── package.json │ │ │ ├── simple.css │ │ │ └── simple.js │ │ ├── __utils__ │ │ │ └── tmp-dir.ts │ │ ├── actions-evaluator-test.ts │ │ ├── analyzers │ │ │ ├── dependency-analyzer-test.ts │ │ │ ├── ember-template-lint-analyzer-test.ts │ │ │ ├── eslint-analyzer-test.ts │ │ │ ├── javascript-analyzer-test.ts │ │ │ ├── json-analyzer-test.ts │ │ │ └── stylelint-analyzer-test.ts │ │ ├── base-task-test.ts │ │ ├── base-validation-task-test.ts │ │ ├── config-test.ts │ │ ├── data │ │ │ ├── checkup-log-builder-test.ts │ │ │ ├── checkup-log-parser-test.ts │ │ │ └── sarif-log-builder-test.ts │ │ ├── tsconfig.json │ │ └── utils │ │ │ ├── __snapshots__ │ │ │ └── calculate-section-bar-test.ts.snap │ │ │ ├── buffered-writer-test.ts │ │ │ ├── calculate-section-bar-test.ts │ │ │ ├── console-writer-test.ts │ │ │ ├── file-paths-array-test.ts │ │ │ ├── get-paths-test.ts │ │ │ ├── merge-lint-config-test.ts │ │ │ └── normalize-package-name-test.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── plugin │ ├── bin │ │ └── checkup-plugin.js │ ├── package.json │ ├── src │ │ └── generate-docs.ts │ ├── templates │ │ └── task-template.md │ ├── tsconfig.json │ └── vitest.config.ts ├── test-helpers │ ├── package.json │ ├── src │ │ ├── checkup-fixturify-project.ts │ │ ├── ember-cli-fixturify-project.ts │ │ ├── get-task-context.ts │ │ ├── index.ts │ │ ├── test-root.ts │ │ └── tmp-dir.ts │ ├── static │ │ └── templates │ │ │ └── register-tasks.hbs │ ├── tsconfig.json │ └── vitest.config.ts └── ui │ ├── package.json │ ├── src │ ├── base-ui-formatter.ts │ ├── component-provider.ts │ ├── components │ │ ├── Actions.tsx │ │ ├── Bar.tsx │ │ ├── CLIInfo.tsx │ │ ├── List.tsx │ │ ├── MetaData.tsx │ │ ├── Migration.tsx │ │ ├── NoResults.tsx │ │ ├── ResultsToFile.tsx │ │ ├── SectionedBar.tsx │ │ ├── Table.tsx │ │ ├── TaskDisplayName.tsx │ │ ├── TaskErrors.tsx │ │ ├── TaskTiming.tsx │ │ └── Validation.tsx │ ├── get-options.ts │ ├── get-sorter.ts │ ├── index.ts │ └── types │ │ └── index.ts │ ├── tests │ ├── __fixtures__ │ │ ├── checkup-no-result-found.sarif │ │ ├── checkup-result.sarif │ │ └── invalid-custom-components.tsx │ └── components │ │ ├── bar-test.tsx │ │ ├── list-test.tsx │ │ ├── migration-test.tsx │ │ ├── no-results-found-test.tsx │ │ ├── sectioned-bar-test.tsx │ │ ├── table-test.tsx │ │ ├── task-errors-test.tsx │ │ └── validation-test.tsx │ ├── tsconfig.json │ └── vitest.config.ts ├── tsconfig-base.json ├── tsconfig.json ├── types ├── ember-template-lint.d.ts ├── es-main.d.ts ├── npm-check.d.ts └── yargs-unparser.d.ts └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | packages/*/lib 2 | packages/*/node_modules 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: eslint-plugin-ember 10 | versions: 11 | - "^9.0.0" 12 | - dependency-name: "@types/node" 13 | versions: 14 | - 14.14.37 15 | - 14.14.41 16 | - 15.0.0 17 | - dependency-name: "@babel/traverse" 18 | versions: 19 | - 7.13.15 20 | - dependency-name: ajv 21 | versions: 22 | - 8.1.0 23 | - dependency-name: eslint-plugin-unicorn 24 | versions: 25 | - 25.0.1 26 | - dependency-name: release-it 27 | versions: 28 | - 14.5.1 29 | - 14.6.1 30 | - dependency-name: "@types/eslint" 31 | versions: 32 | - 7.2.6 33 | - 7.2.7 34 | - 7.2.8 35 | - 7.2.9 36 | - dependency-name: type-fest 37 | versions: 38 | - 1.0.1 39 | - dependency-name: y18n 40 | versions: 41 | - 4.0.1 42 | - 4.0.2 43 | - dependency-name: "@types/yeoman-generator" 44 | versions: 45 | - 4.11.3 46 | - dependency-name: "@oclif/config" 47 | versions: 48 | - 1.17.0 49 | - dependency-name: "@oclif/dev-cli" 50 | versions: 51 | - 1.26.0 52 | - dependency-name: "@babel/parser" 53 | versions: 54 | - 7.12.16 55 | - 7.12.17 56 | - 7.13.0 57 | - 7.13.10 58 | - 7.13.11 59 | - 7.13.12 60 | - 7.13.4 61 | - 7.13.9 62 | - dependency-name: chai 63 | versions: 64 | - 4.3.0 65 | - 4.3.1 66 | - 4.3.3 67 | - 4.3.4 68 | - dependency-name: "@types/resolve" 69 | versions: 70 | - 1.19.0 71 | - 1.20.0 72 | - dependency-name: "@typescript-eslint/parser" 73 | versions: 74 | - 4.15.0 75 | - dependency-name: ember-template-lint 76 | versions: 77 | - 2.17.0 78 | - 2.18.0 79 | - dependency-name: eslint-plugin-ember 80 | versions: 81 | - 10.1.2 82 | - dependency-name: "@types/yeoman-environment" 83 | versions: 84 | - 2.10.2 85 | - dependency-name: "@types/fs-extra" 86 | versions: 87 | - 9.0.6 88 | - dependency-name: "@babel/types" 89 | versions: 90 | - 7.12.12 91 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: auto-merge 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | auto-merge: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: ahmadnassri/action-dependabot-auto-merge@v2 12 | with: 13 | target: minor 14 | github-token: ${{ secrets.AUTO_MERGE }} -------------------------------------------------------------------------------- /.github/workflows/ci-build.yml: -------------------------------------------------------------------------------- 1 | name: CI Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - 'v*' 8 | pull_request: {} 9 | schedule: 10 | - cron: '0 3 * * *' # daily, at 3am 11 | 12 | jobs: 13 | test: 14 | name: Tests 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node: ['16', '18'] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - uses: volta-cli/action@v1 24 | with: 25 | node-version: ${{ matrix.node }} 26 | - name: install dependencies 27 | run: yarn install 28 | - name: lint 29 | run: yarn lint 30 | - name: test 31 | run: yarn test 32 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-rebase.yml: -------------------------------------------------------------------------------- 1 | name: rebase pull requests 2 | on: 3 | push: 4 | release: 5 | types: [published] 6 | jobs: 7 | auto-rebase: 8 | name: rebase dependabot PRs 9 | runs-on: ubuntu-latest 10 | if: github.ref == 'refs/heads/main' || github.event == 'release' 11 | timeout-minutes: 5 12 | steps: 13 | - name: rebase 14 | uses: bbeesley/gha-auto-dependabot-rebase@main 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *-debug.log 2 | *-error.log 3 | packages/*/lib 4 | packages/*/.nyc_output 5 | packages/*/node_modules 6 | packages/*/tsconfig.tsbuildinfo 7 | /tmp 8 | node_modules 9 | .eslintcache 10 | .vscode 11 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: 'es5', 4 | printWidth: 100, 5 | }; 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 checkupjs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release 2 | 3 | Releases are mostly automated using 4 | [release-it](https://github.com/release-it/release-it/) and 5 | [lerna-changelog](https://github.com/lerna/lerna-changelog/). 6 | 7 | ## Preparation 8 | 9 | Since the majority of the actual release process is automated, the primary 10 | remaining task prior to releasing is confirming that all pull requests that 11 | have been merged since the last release have been labeled with the appropriate 12 | `lerna-changelog` labels and the titles have been updated to ensure they 13 | represent something that would make sense to our users. Some great information 14 | on why this is important can be found at 15 | [keepachangelog.com](https://keepachangelog.com/en/1.0.0/), but the overall 16 | guiding principle here is that changelogs are for humans, not machines. 17 | 18 | When reviewing merged PR's the labels to be used are: 19 | 20 | - breaking - Used when the PR is considered a breaking change. 21 | - enhancement - Used when the PR adds a new feature or enhancement. 22 | - bug - Used when the PR fixes a bug included in a previous release. 23 | - documentation - Used when the PR adds or updates documentation. 24 | - internal - Used for internal changes that still require a mention in the 25 | changelog/release notes. 26 | 27 | ## Release 28 | 29 | Once the prep work is completed, the actual release is straight forward: 30 | 31 | - First ensure that you have `release-it` installed globally, generally done by 32 | using one of the following commands: 33 | 34 | ``` 35 | # using https://volta.sh 36 | volta install release-it 37 | 38 | # using Yarn 39 | yarn global add release-it 40 | 41 | # using npm 42 | npm install --global release-it 43 | ``` 44 | 45 | - Second, ensure that you have installed your projects dependencies: 46 | 47 | ``` 48 | yarn install 49 | ``` 50 | 51 | - And last (but not least 😁) do your release. It requires a 52 | [GitHub personal access token](https://github.com/settings/tokens) as 53 | `$GITHUB_AUTH` environment variable. Only "repo" access is needed; no "admin" 54 | or other scopes are required. 55 | 56 | ``` 57 | export GITHUB_AUTH="f941e0..." 58 | release-it 59 | ``` 60 | 61 | [release-it](https://github.com/release-it/release-it/) manages the actual 62 | release process. It will prompt you to to choose the version number after which 63 | you will have the chance to hand tweak the changelog to be used (for the 64 | `CHANGELOG.md` and GitHub release), then `release-it` continues on to tagging, 65 | pushing the tag and commits, etc. 66 | -------------------------------------------------------------------------------- /VERSIONING_POLICY.md: -------------------------------------------------------------------------------- 1 | # Semantic Versioning Policy 2 | 3 | Checkup follows the [Semantic Versioning](http://semver.org/) specification. However, due to the nature of Checkup as a code quality tool, it's not always clear when a minor or major version bump occurs. To help clarify this, we have a [Semantic Versioning Policy](VERSIONING_POLICY.md) that describes the rules for version bumps. 4 | 5 | ## Patch release (intended to not break your Checkup build) 6 | 7 | - A bug fix that may reduce the results reported by Checkup. 8 | - Changes to the SARIF log output format. 9 | - A bug fix to the CLI or core. 10 | - Improvements to documentation. 11 | - Non-user-facing changes such as refactoring code, adding, deleting, or modifying tests, and increasing test coverage. 12 | - Re-releasing after a failed release (i.e., publishing a release that doesn't work for anyone). 13 | 14 | ## Minor release (may break your Checkup build) 15 | 16 | - A bug fix that may increase the results reported by Checkup. 17 | - Changes to the SARIF log output format. 18 | - The public API is changed in a compatible way. 19 | 20 | ## Major release (likely to break your Checkup build) 21 | 22 | - A new CLI capability is created. 23 | - New capabilities to the public API are added (new classes, new methods, new arguments to existing methods, etc.). 24 | - Part of the public API is removed or changed in an incompatible way. The public API includes: 25 | - Configuration schema 26 | - Command-line options 27 | - Node.js API 28 | - Task, formatter, analyzer, plugin APIs 29 | 30 | ## Special Note about SARIF 31 | 32 | Checkup natively uses the [SARIF specification](https://docs.oasis-open.org/sarif/sarif/v2.0/sarif-v2.0.html) for its output format. While the SARIF format uses both semantic and non-semantic properties, Checkup makes no guarantees about the semantic properties of the SARIF log, or whether they'll be preserved in future releases. Therefore, Checkup does not guarantee that the SARIF log will be compatible with future releases of Checkup - it only guarantees that valid SARIF logs will be produced. 33 | -------------------------------------------------------------------------------- /checkup.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "extensions": { 8 | "recommendations": [ 9 | "dbaeumer.vscode-eslint", 10 | "editorconfig.editorconfig", 11 | "esbenp.prettier-vscode" 12 | ] 13 | }, 14 | "settings": { 15 | "files.exclude": { 16 | "**/node_modules/**": true, 17 | "**/lib/**": true 18 | }, 19 | "typescript.tsdk": "node_modules/typescript/lib", 20 | "editor.formatOnSave": true 21 | }, 22 | "files.associations": {} 23 | } 24 | -------------------------------------------------------------------------------- /docs/checkup-run-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/checkupjs/checkup/f7f715e4c38b826bc069f99a752724f017c4d78a/docs/checkup-run-output.png -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/checkupjs/checkup/f7f715e4c38b826bc069f99a752724f017c4d78a/docs/logo.png -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/README.md: -------------------------------------------------------------------------------- 1 | # checkup-plugin-ember 2 | 3 | A checkup plugin for Ember project tasks 4 | 5 | ![CI Build](https://github.com/checkupjs/checkup/workflows/CI%20Build/badge.svg) 6 | [![Version](https://img.shields.io/npm/v/checkup-plugin-ember.svg)](https://npmjs.org/package/checkup-plugin-ember) 7 | [![Downloads/week](https://img.shields.io/npm/dw/checkup-plugin-ember.svg)](https://npmjs.org/package/checkup-plugin-ember) 8 | [![License](https://img.shields.io/npm/l/checkup-plugin-ember.svg)](https://github.com/checkupjs/checkup/blob/master/package.json) 9 | 10 | - [checkup-plugin-ember](#checkup-plugin-ember) 11 | - [Usage](#usage) 12 | 13 | ## Usage 14 | 15 | 1. Install [@checkup/cli](https://github.com/checkupjs/checkup/blob/master/packages/cli/README.md) globally following the README. 16 | 17 | 2. Install `checkup-plugin-ember` 18 | 19 | ```sh-session 20 | $ npm install --save-dev checkup-plugin-ember 21 | 22 | # or 23 | 24 | $ yarn add --dev checkup-plugin-ember 25 | ``` 26 | 27 | 3. Add `checkup-plugin-ember` as a plugin to your config. 28 | 29 | ```json 30 | { 31 | "plugins": [ 32 | ... 33 | "checkup-plugin-ember" 34 | ... 35 | ], 36 | "tasks": { 37 | ... 38 | } 39 | } 40 | ``` 41 | 42 | 4. Run checkup. 43 | 44 | ```sh-session 45 | $ checkup 46 | ``` 47 | -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/docs/tasks/ember-dependencies-task.md: -------------------------------------------------------------------------------- 1 | 2 | # ember/ember-dependencies 3 | 4 | 5 | 6 | Finds Ember-specific dependencies and their versions in an Ember.js project 7 | 8 | 9 | 10 | ## To run this task 11 | 12 | ```bash 13 | checkup run --task ember/ember-dependencies 14 | ``` 15 | 16 | -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/docs/tasks/ember-in-repo-addons-engines-task.md: -------------------------------------------------------------------------------- 1 | 2 | # ember/ember-in-repo-addons-engines 3 | 4 | 5 | 6 | Finds all in-repo engines and addons in an Ember.js project 7 | 8 | 9 | 10 | ## To run this task 11 | 12 | ```bash 13 | checkup run --task ember/ember-in-repo-addons-engines 14 | ``` 15 | 16 | -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/docs/tasks/ember-octane-migration-status-task.md: -------------------------------------------------------------------------------- 1 | 2 | # ember/ember-octane-migration-status 3 | 4 | 5 | 6 | Tracks the migration status when moving from Ember Classic to Ember Octane in an Ember.js project 7 | 8 | 9 | 10 | ## To run this task 11 | 12 | ```bash 13 | checkup run --task ember/ember-octane-migration-status 14 | ``` 15 | 16 | -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/docs/tasks/ember-template-lint-disable-task.md: -------------------------------------------------------------------------------- 1 | 2 | # ember/ember-template-lint-disables 3 | 4 | 5 | 6 | Finds all disabled ember-template-lint rules in an Ember.js project 7 | 8 | 9 | 10 | ## To run this task 11 | 12 | ```bash 13 | checkup run --task ember/ember-template-lint-disables 14 | ``` 15 | 16 | -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/docs/tasks/ember-template-lint-summary-task.md: -------------------------------------------------------------------------------- 1 | 2 | # ember/ember-template-lint-summary 3 | 4 | 5 | 6 | Gets a summary of all ember-template-lint results in an Ember.js project 7 | 8 | 9 | 10 | ## To run this task 11 | 12 | ```bash 13 | checkup run --task ember/ember-template-lint-summary 14 | ``` 15 | 16 | -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/docs/tasks/ember-test-types-task.md: -------------------------------------------------------------------------------- 1 | 2 | # ember/ember-test-types 3 | 4 | 5 | 6 | Gets a breakdown of all test types in an Ember.js project 7 | 8 | 9 | 10 | ## To run this task 11 | 12 | ```bash 13 | checkup run --task ember/ember-test-types 14 | ``` 15 | 16 | -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/docs/tasks/ember-types-task.md: -------------------------------------------------------------------------------- 1 | 2 | # ember/ember-types 3 | 4 | 5 | 6 | Gets a breakdown of all Ember types in an Ember.js project 7 | 8 | 9 | 10 | ## To run this task 11 | 12 | ```bash 13 | checkup run --task ember/ember-types 14 | ``` 15 | 16 | -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "checkup-plugin-ember", 3 | "version": "3.0.1", 4 | "description": "A checkup plugin for Ember project tasks", 5 | "keywords": [ 6 | "checkup-plugin" 7 | ], 8 | "homepage": "https://github.com/checkupjs/checkup", 9 | "bugs": "https://github.com/checkupjs/checkup/issues", 10 | "repository": "https://github.com/checkupjs/checkup", 11 | "license": "MIT", 12 | "author": "Steve Calvert @scalvert", 13 | "type": "module", 14 | "main": "lib/index.js", 15 | "types": "lib/index.d.ts", 16 | "files": [ 17 | "/lib", 18 | "/yarn.lock" 19 | ], 20 | "scripts": { 21 | "build": "tsc --build", 22 | "copy:package": "cp ./src/eslint/rules/package.json ./lib/eslint/rules/package.json", 23 | "docs:generate": "checkup-plugin docs", 24 | "test": "vitest run" 25 | }, 26 | "dependencies": { 27 | "@babel/eslint-parser": "^7.17.0", 28 | "@babel/parser": "^7.18.0", 29 | "@babel/traverse": "^7.14.2", 30 | "@babel/types": "7.18.8", 31 | "@checkup/core": "^3.0.1", 32 | "debug": "^4.3.1", 33 | "ember-template-recast": "^6.1.3", 34 | "eslint-plugin-ember": "^10.6.1", 35 | "fs-extra": "^9.1.0", 36 | "lodash.kebabcase": "^4.1.1", 37 | "tslib": "^2" 38 | }, 39 | "devDependencies": { 40 | "@checkup/plugin": "^3.0.1", 41 | "@checkup/test-helpers": "^3.0.1", 42 | "@types/lodash.kebabcase": "^4.1.7", 43 | "eslint": "^8.22.0", 44 | "fixturify-project": "^2.1.1", 45 | "rimraf": "^3.0.2" 46 | }, 47 | "engines": { 48 | "node": ">= 16" 49 | }, 50 | "publishConfig": { 51 | "access": "public" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/src/actions/ember-template-lint-disable-actions.ts: -------------------------------------------------------------------------------- 1 | import { ActionsEvaluator, TaskConfig } from '@checkup/core'; 2 | import { Result } from 'sarif'; 3 | 4 | export function evaluateActions(taskResults: Result[], taskConfig: TaskConfig) { 5 | let actionsEvaluator = new ActionsEvaluator(); 6 | let templateLintDisableUsages = taskResults.length; 7 | 8 | actionsEvaluator.add({ 9 | taskName: 'ember-template-lint-disable', 10 | name: 'reduce-template-lint-disable-usages', 11 | summary: 'Reduce number of template-lint-disable usages', 12 | details: `${templateLintDisableUsages} usages of template-lint-disable`, 13 | defaultThreshold: 2, 14 | items: [`Total template-lint-disable usages: ${templateLintDisableUsages}`], 15 | input: templateLintDisableUsages, 16 | }); 17 | 18 | return actionsEvaluator.evaluate(taskConfig); 19 | } 20 | -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/src/actions/ember-template-lint-summary-actions.ts: -------------------------------------------------------------------------------- 1 | import { ActionsEvaluator, TaskConfig } from '@checkup/core'; 2 | import { Result } from 'sarif'; 3 | 4 | export function evaluateActions(taskResults: Result[], taskConfig: TaskConfig) { 5 | let actionsEvaluator = new ActionsEvaluator(); 6 | 7 | let errors = taskResults.filter((result: Result) => result.level === 'error')!; 8 | let warnings = taskResults.filter((result: Result) => result.level === 'warning')!; 9 | 10 | let errorCount = errors.length; 11 | let warningCount = warnings.length; 12 | 13 | actionsEvaluator.add({ 14 | taskName: 'ember-template-lint-summary', 15 | name: 'reduce-template-lint-errors', 16 | summary: 'Reduce number of template-lint errors', 17 | details: `${errorCount} total errors`, 18 | defaultThreshold: 20, 19 | items: [`Total template-lint errors: ${errorCount}`], 20 | input: errorCount, 21 | }); 22 | 23 | actionsEvaluator.add({ 24 | taskName: 'ember-template-lint-summary', 25 | name: 'reduce-template-lint-warnings', 26 | summary: 'Reduce number of template-lint warnings', 27 | details: `${warningCount} total warnings`, 28 | defaultThreshold: 20, 29 | items: [`Total template-lint warnings: ${warningCount}`], 30 | input: warningCount, 31 | }); 32 | 33 | return actionsEvaluator.evaluate(taskConfig); 34 | } 35 | -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/src/actions/ember-test-types-actions.ts: -------------------------------------------------------------------------------- 1 | import { ActionsEvaluator, toPercent, TaskConfig } from '@checkup/core'; 2 | import { Result } from 'sarif'; 3 | 4 | export function evaluateActions(taskResults: Result[], taskConfig: TaskConfig) { 5 | let actionsEvaluator = new ActionsEvaluator(); 6 | 7 | let totalSkippedTests = taskResults.filter( 8 | (taskResult) => taskResult.properties?.testMethod === 'skip' 9 | ).length; 10 | let totalTests = taskResults.length; 11 | 12 | actionsEvaluator.add({ 13 | taskName: 'ember-test-types', 14 | name: 'reduce-skipped-tests', 15 | summary: 'Reduce number of skipped tests', 16 | details: `${toPercent(totalSkippedTests, totalTests)} of tests are skipped`, 17 | defaultThreshold: 0.01, 18 | 19 | items: [`Total skipped tests: ${totalSkippedTests}`], 20 | input: totalSkippedTests / totalTests, 21 | }); 22 | 23 | return actionsEvaluator.evaluate(taskConfig); 24 | } 25 | -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/src/eslint/rules/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } 4 | -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/src/eslint/rules/test-types.js: -------------------------------------------------------------------------------- 1 | /* eslint unicorn/prefer-module: "off" */ 2 | module.exports = { 3 | create: function (context) { 4 | let testType = 'unit'; 5 | 6 | return { 7 | 'CallExpression > Identifier[name=setupRenderingTest]'() { 8 | testType = 'rendering'; 9 | }, 10 | 11 | 'CallExpression > Identifier[name=setupApplicationTest]'() { 12 | testType = 'application'; 13 | }, 14 | 15 | 'CallExpression > Identifier[name=test]'(node) { 16 | context.report({ 17 | node, 18 | message: `${testType}|test`, 19 | }); 20 | }, 21 | 22 | 'CallExpression > Identifier[name=skip]'(node) { 23 | context.report({ 24 | node, 25 | message: `${testType}|skip`, 26 | }); 27 | }, 28 | 29 | 'CallExpression > Identifier[name=only]'(node) { 30 | context.report({ 31 | node, 32 | message: `${testType}|only`, 33 | }); 34 | }, 35 | 36 | 'CallExpression > Identifier[name=todo]'(node) { 37 | context.report({ 38 | node, 39 | message: `${testType}|todo`, 40 | }); 41 | }, 42 | }; 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/src/index.ts: -------------------------------------------------------------------------------- 1 | import EmberDependenciesTask from './tasks/ember-dependencies-task.js'; 2 | import EmberInRepoAddonsEnginesTask from './tasks/ember-in-repo-addons-engines-task.js'; 3 | import EmberTestTypesTaskTask from './tasks/ember-test-types-task.js'; 4 | import EmberTypesTask from './tasks/ember-types-task.js'; 5 | import EmberTemplateLintDisableTask from './tasks/ember-template-lint-disable-task.js'; 6 | import EmberTemplateLintSummaryTask from './tasks/ember-template-lint-summary-task.js'; 7 | import EmberOctaneMigrationStatusTask from './tasks/ember-octane-migration-status-task.js'; 8 | import { evaluateActions as evaluateTemplateLintDisables } from './actions/ember-template-lint-disable-actions.js'; 9 | import { evaluateActions as evaluateTemplateLintSummary } from './actions/ember-template-lint-summary-actions.js'; 10 | import { evaluateActions as evaluateTestTypes } from './actions/ember-test-types-actions.js'; 11 | 12 | export default { 13 | tasks: { 14 | 'ember-types': EmberTypesTask, 15 | 'ember-dependencies': EmberDependenciesTask, 16 | 'ember-in-repo-addons-engines': EmberInRepoAddonsEnginesTask, 17 | 'ember-test-types': EmberTestTypesTaskTask, 18 | 'ember-template-lint-disables': EmberTemplateLintDisableTask, 19 | 'ember-template-lint-summary': EmberTemplateLintSummaryTask, 20 | 'ember-octane-migration-status': EmberOctaneMigrationStatusTask, 21 | }, 22 | actions: { 23 | 'ember-template-lint-disables': evaluateTemplateLintDisables, 24 | 'ember-template-lint-summary': evaluateTemplateLintSummary, 25 | 'ember-test-types': evaluateTestTypes, 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/src/tasks/ember-dependencies-task.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { BaseTask, Task, TaskContext, DependencyAnalyzer } from '@checkup/core'; 3 | import { Result } from 'sarif'; 4 | 5 | export default class EmberDependenciesTask extends BaseTask implements Task { 6 | taskName = 'ember-dependencies'; 7 | taskDisplayName = 'Ember Dependencies'; 8 | description = 'Finds Ember-specific dependencies and their versions in an Ember.js project'; 9 | category = 'dependencies'; 10 | group = 'ember'; 11 | 12 | constructor(pluginName: string, context: TaskContext) { 13 | super(pluginName, context); 14 | 15 | this.addRule({ 16 | properties: { 17 | component: { 18 | name: 'table', 19 | options: { 20 | rows: { 21 | Dependency: 'properties.packageName', 22 | Installed: 'properties.packageVersion', 23 | Latest: 'properties.latestVersion', 24 | }, 25 | }, 26 | }, 27 | }, 28 | }); 29 | } 30 | 31 | async run(): Promise { 32 | let analyzer = new DependencyAnalyzer(this.context.options.cwd); 33 | let dependencies = await analyzer.analyze(); 34 | 35 | dependencies 36 | .filter((dependency) => isEmberDependency(dependency.packageName)) 37 | .forEach((dependency) => { 38 | this.addResult( 39 | `Ember dependency information for ${dependency.packageName}`, 40 | 'review', 41 | 'note', 42 | { 43 | location: { 44 | uri: join(this.context.options.cwd, 'package.json'), 45 | startLine: dependency.startLine, 46 | startColumn: dependency.startColumn, 47 | endColumn: dependency.endColumn, 48 | endLine: dependency.endLine, 49 | }, 50 | properties: { 51 | packageName: dependency.packageName, 52 | packageVersion: dependency.packageVersion, 53 | latestVersion: dependency.latestVersion, 54 | type: dependency.type, 55 | }, 56 | } 57 | ); 58 | }); 59 | 60 | return this.results; 61 | } 62 | } 63 | 64 | function isEmberDependency(dependency: string) { 65 | return dependency.startsWith('ember-') && !dependency.startsWith('ember-cli'); 66 | } 67 | -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/src/tasks/ember-in-repo-addons-engines-task.ts: -------------------------------------------------------------------------------- 1 | import { Task, BaseTask, TaskError, TaskContext, isErrnoException } from '@checkup/core'; 2 | 3 | import { PackageJson } from 'type-fest'; 4 | import fs from 'fs-extra'; 5 | 6 | import { Result } from 'sarif'; 7 | 8 | export default class EmberInRepoAddonsEnginesTask extends BaseTask implements Task { 9 | taskName = 'ember-in-repo-addons-engines'; 10 | taskDisplayName = 'Ember In-Repo Addons / Engines'; 11 | description = 'Finds all in-repo engines and addons in an Ember.js project'; 12 | category = 'metrics'; 13 | group = 'ember'; 14 | 15 | constructor(pluginName: string, context: TaskContext) { 16 | super(pluginName, context); 17 | 18 | this.addRule({ 19 | properties: { 20 | component: { 21 | name: 'list', 22 | options: { 23 | items: { 24 | Engine: { 25 | groupBy: 'properties.type', 26 | value: 'engine', 27 | }, 28 | Addon: { 29 | groupBy: 'properties.type', 30 | value: 'addon', 31 | }, 32 | }, 33 | }, 34 | }, 35 | }, 36 | }); 37 | } 38 | 39 | async run(): Promise { 40 | let packageJsonPaths: string[] = this.context.paths.filterByGlob('**/*package.json'); 41 | 42 | for (let pathName of packageJsonPaths) { 43 | let packageJson: PackageJson = await this.getPackageJson(pathName); 44 | let isEngine = packageJson.keywords?.includes('ember-engine') && packageJson.name; 45 | let isAddon = packageJson.keywords?.includes('ember-addon') && packageJson.name; 46 | 47 | if (isEngine || isAddon) { 48 | let type = isEngine ? 'engine' : 'addon'; 49 | 50 | this.addResult(`${packageJson.name} Ember ${type} found.`, 'review', 'note', { 51 | location: { 52 | uri: pathName, 53 | }, 54 | properties: { 55 | type, 56 | }, 57 | }); 58 | } 59 | } 60 | 61 | return this.results; 62 | } 63 | 64 | async getPackageJson(packageJsonPath: string): Promise { 65 | let package_ = {}; 66 | 67 | try { 68 | package_ = await fs.readJson(packageJsonPath); 69 | } catch (error) { 70 | if (isErrnoException(error) && error.code === 'ENOENT') { 71 | throw new TaskError({ 72 | taskName: this.taskName, 73 | taskErrorMessage: `No package.json file detected at ${packageJsonPath}`, 74 | }); 75 | } 76 | } 77 | 78 | return package_; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/src/tasks/ember-types-task.ts: -------------------------------------------------------------------------------- 1 | import { Task, BaseTask, trimCwd, TaskContext } from '@checkup/core'; 2 | import { Result } from 'sarif'; 3 | 4 | const SEARCH_PATTERNS = [ 5 | { type: 'components', pattern: ['**/components/**/*.js'] }, 6 | { type: 'controllers', pattern: ['**/controllers/**/*.js'] }, 7 | { type: 'helpers', pattern: ['**/helpers/**/*.js'] }, 8 | { type: 'initializers', pattern: ['**/initializers/**/*.js'] }, 9 | { type: 'instance-initializers', pattern: ['**/instance-initializers/**/*.js'] }, 10 | { type: 'mixins', pattern: ['**/mixins/**/*.js'] }, 11 | { type: 'models', pattern: ['**/models/**/*.js'] }, 12 | { type: 'routes', pattern: ['**/routes/**/*.js'] }, 13 | { type: 'services', pattern: ['**/services/**/*.js'] }, 14 | { type: 'templates', pattern: ['**/templates/**/*.hbs'] }, 15 | ]; 16 | 17 | export default class EmberTypesTask extends BaseTask implements Task { 18 | taskName = 'ember-types'; 19 | taskDisplayName = 'Ember Types'; 20 | description = 'Gets a breakdown of all Ember types in an Ember.js project'; 21 | category = 'metrics'; 22 | group = 'ember'; 23 | 24 | constructor(pluginName: string, context: TaskContext) { 25 | super(pluginName, context); 26 | 27 | this.addRule({ 28 | properties: { 29 | component: { 30 | name: 'list', 31 | options: { 32 | items: SEARCH_PATTERNS.reduce((total, pattern) => { 33 | total[pattern.type] = { 34 | groupBy: 'properties.type', 35 | value: pattern.type, 36 | }; 37 | return total; 38 | }, {} as Record), 39 | }, 40 | }, 41 | }, 42 | }); 43 | } 44 | 45 | async run(): Promise { 46 | SEARCH_PATTERNS.map((pattern) => { 47 | let files = this.context.paths.filterByGlob(pattern.pattern); 48 | 49 | files.forEach((file: string) => { 50 | let uri = trimCwd(file, this.context.options.cwd); 51 | this.addResult( 52 | `Located ${pattern.type.slice(0, Math.max(0, pattern.type.length - 1))} at ${uri}`, 53 | 'informational', 54 | 'note', 55 | { 56 | location: { 57 | uri, 58 | }, 59 | properties: { 60 | type: pattern.type, 61 | }, 62 | } 63 | ); 64 | }); 65 | }); 66 | 67 | return this.results; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export const enum ProjectType { 2 | App = 'application', 3 | Addon = 'addon', 4 | Engine = 'engine', 5 | Unknown = 'unknown', 6 | } 7 | 8 | export enum TestType { 9 | Application = 'application', 10 | Rendering = 'rendering', 11 | Unit = 'unit', 12 | } 13 | 14 | export interface TestTypeInfo { 15 | type: TestType; 16 | skip: number; 17 | only: number; 18 | todo: number; 19 | test: number; 20 | total: number; 21 | } 22 | -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/tests/__fixtures__/addon-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-addon", 3 | "version": "0.0.0", 4 | "description": "The default blueprint for ember-cli addons.", 5 | "keywords": ["ember-addon"], 6 | "repository": "", 7 | "license": "MIT", 8 | "author": "", 9 | "directories": { 10 | "doc": "doc", 11 | "test": "tests" 12 | }, 13 | "scripts": { 14 | "build": "ember build", 15 | "lint:hbs": "ember-template-lint .", 16 | "lint:js": "eslint .", 17 | "start": "ember serve", 18 | "test": "ember test", 19 | "test:all": "ember try:each" 20 | }, 21 | "dependencies": { 22 | "ember-cli-babel": "^7.13.0", 23 | "ember-cli-htmlbars": "^4.2.0" 24 | }, 25 | "devDependencies": { 26 | "@babel/eslint-parser": "^7.17.0", 27 | "@ember/optional-features": "^1.1.0", 28 | "@glimmer/component": "^1.0.0", 29 | "broccoli-asset-rev": "^3.0.0", 30 | "ember-auto-import": "^1.5.3", 31 | "ember-cli": "~3.15.0-beta.3", 32 | "ember-cli-dependency-checker": "^3.2.0", 33 | "ember-cli-eslint": "^5.1.0", 34 | "ember-cli-inject-live-reload": "^2.0.1", 35 | "ember-cli-sri": "^2.1.1", 36 | "ember-cli-template-lint": "^1.0.0-beta.3", 37 | "ember-cli-uglify": "^3.0.0", 38 | "ember-disable-prototype-extensions": "^1.1.3", 39 | "ember-export-application-global": "^2.0.1", 40 | "ember-load-initializers": "^2.1.1", 41 | "ember-maybe-import-regenerator": "^0.1.6", 42 | "ember-qunit": "^4.6.0", 43 | "ember-resolver": "^6.0.0", 44 | "ember-source": "~3.15.0", 45 | "ember-source-channel-url": "^2.0.1", 46 | "ember-try": "^1.4.0", 47 | "eslint-plugin-ember": "^7.7.1", 48 | "eslint-plugin-node": "^10.0.0", 49 | "loader.js": "^4.7.0", 50 | "qunit-dom": "^0.9.2" 51 | }, 52 | "engines": { 53 | "node": ">= 16" 54 | }, 55 | "ember": { 56 | "edition": "octane" 57 | }, 58 | "ember-addon": { 59 | "configPath": "tests/dummy/config" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/tests/__fixtures__/app-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "Small description for my-app goes here", 6 | "repository": "", 7 | "license": "MIT", 8 | "author": "", 9 | "directories": { 10 | "doc": "doc", 11 | "test": "tests" 12 | }, 13 | "scripts": { 14 | "build": "ember build", 15 | "lint:hbs": "ember-template-lint .", 16 | "lint:js": "eslint .", 17 | "start": "ember serve", 18 | "test": "ember test" 19 | }, 20 | "devDependencies": { 21 | "@babel/eslint-parser": "^7.17.0", 22 | "@ember/optional-features": "^1.1.0", 23 | "@glimmer/component": "^1.0.0", 24 | "broccoli-asset-rev": "^3.0.0", 25 | "ember-auto-import": "^1.5.3", 26 | "ember-cli": "~3.15.0-beta.3", 27 | "ember-cli-app-version": "^3.2.0", 28 | "ember-cli-babel": "^7.13.0", 29 | "ember-cli-dependency-checker": "^3.2.0", 30 | "ember-cli-eslint": "^5.1.0", 31 | "ember-cli-htmlbars": "^4.2.0", 32 | "ember-cli-inject-live-reload": "^2.0.1", 33 | "ember-cli-sri": "^2.1.1", 34 | "ember-cli-template-lint": "^1.0.0-beta.3", 35 | "ember-cli-uglify": "^3.0.0", 36 | "ember-data": "~3.15.0-beta.0", 37 | "ember-export-application-global": "^2.0.1", 38 | "ember-fetch": "^7.0.0", 39 | "ember-load-initializers": "^2.1.1", 40 | "ember-maybe-import-regenerator": "^0.1.6", 41 | "ember-qunit": "^4.6.0", 42 | "ember-resolver": "^6.0.0", 43 | "ember-source": "~3.15.0", 44 | "ember-welcome-page": "^4.0.0", 45 | "eslint-plugin-ember": "^7.7.1", 46 | "eslint-plugin-node": "^10.0.0", 47 | "loader.js": "^4.7.0", 48 | "qunit-dom": "^0.9.2" 49 | }, 50 | "engines": { 51 | "node": ">= 16" 52 | }, 53 | "ember": { 54 | "edition": "octane" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/tests/ember-dependencies-task-test.ts: -------------------------------------------------------------------------------- 1 | import '@microsoft/jest-sarif'; 2 | import { EmberProject, getTaskContext } from '@checkup/test-helpers'; 3 | 4 | import { getPluginName } from '@checkup/core'; 5 | import EmberDependenciesTask from '../src/tasks/ember-dependencies-task'; 6 | 7 | describe('dependencies-task', () => { 8 | let emberProject: EmberProject; 9 | let pluginName = getPluginName(import.meta.url); 10 | 11 | beforeEach(function () { 12 | emberProject = new EmberProject('checkup-app', '0.0.0', (project) => { 13 | project.addDependency('ember-source', '^3.15.0'); 14 | project.addDependency('ember-cli', '^3.15.0'); 15 | project.addDevDependency('ember-cli-string-utils', 'latest'); 16 | }); 17 | 18 | emberProject.addAddon('ember-cli-blueprint-test-helpers', 'latest'); 19 | emberProject.writeSync(); 20 | }); 21 | 22 | afterEach(function () { 23 | emberProject.dispose(); 24 | }); 25 | 26 | it('produces valid SARIF for detected dependencies', async () => { 27 | const results = await new EmberDependenciesTask( 28 | pluginName, 29 | getTaskContext({ 30 | options: { cwd: emberProject.baseDir }, 31 | pkg: emberProject.pkg, 32 | paths: emberProject.filePaths, 33 | }) 34 | ).run(); 35 | 36 | for (let result of results) { 37 | expect(result).toBeValidSarifFor('result'); 38 | } 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/tests/ember-in-repo-addons-engines-task-test.ts: -------------------------------------------------------------------------------- 1 | import '@microsoft/jest-sarif'; 2 | import { getPluginName } from '@checkup/core'; 3 | import { EmberProject, getTaskContext } from '@checkup/test-helpers'; 4 | import EmberInRepoAddonEnginesTask from '../src/tasks/ember-in-repo-addons-engines-task'; 5 | 6 | describe('ember-in-repo-addons-engines-task', () => { 7 | let emberProject: EmberProject; 8 | let pluginName = getPluginName(import.meta.url); 9 | 10 | beforeEach(() => { 11 | emberProject = new EmberProject('checkup-app', '0.0.0', (emberProject) => { 12 | emberProject.addDependency('ember-cli', '^3.15.0'); 13 | }); 14 | emberProject.addInRepoAddon('admin', 'latest'); 15 | emberProject.addInRepoAddon('shopping-cart', 'latest'); 16 | emberProject.addInRepoEngine('foo-engine', 'latest'); 17 | emberProject.addInRepoEngine('shmoo-engine', 'latest'); 18 | emberProject.writeSync(); 19 | }); 20 | 21 | afterEach(() => { 22 | emberProject.dispose(); 23 | }); 24 | 25 | it('can read task as JSON', async () => { 26 | const results = await new EmberInRepoAddonEnginesTask( 27 | pluginName, 28 | getTaskContext({ 29 | pkg: emberProject.pkg, 30 | options: { cwd: emberProject.baseDir }, 31 | paths: emberProject.filePaths, 32 | }) 33 | ).run(); 34 | 35 | for (let result of results) { 36 | expect(result).toBeValidSarifFor('result'); 37 | } 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/tests/ember-template-lint-disable-task-test.ts: -------------------------------------------------------------------------------- 1 | import '@microsoft/jest-sarif'; 2 | import { CheckupProject, getTaskContext } from '@checkup/test-helpers'; 3 | import TemplateLintDisableTask from '../src/tasks/ember-template-lint-disable-task'; 4 | import { evaluateActions } from '../src/actions/ember-template-lint-disable-actions'; 5 | 6 | describe('ember-template-lint-disable-task', () => { 7 | let project: CheckupProject; 8 | 9 | beforeEach(function () { 10 | project = new CheckupProject('foo', '0.0.0'); 11 | project.files['index.hbs'] = ` 12 | {{! template-lint-disable no-inline-styles }} 13 |
14 |

Checkup

17 | 18 | {{! template-lint-disable img-alt-attributes }} 19 | 20 |
21 | 22 | {{! template-lint-disable bare-strings }} 23 | WHATEVER MAN 24 | `; 25 | 26 | project.writeSync(); 27 | }); 28 | 29 | afterEach(function () { 30 | project.dispose(); 31 | }); 32 | 33 | it('finds all instances of template-lint-disable and outputs to json', async () => { 34 | const results = await new TemplateLintDisableTask( 35 | 'internal', 36 | getTaskContext({ 37 | paths: project.filePaths, 38 | options: { cwd: project.baseDir }, 39 | }) 40 | ).run(); 41 | 42 | for (let result of results) { 43 | expect(result).toBeValidSarifFor('result'); 44 | } 45 | }); 46 | 47 | it('returns action item if there are more than 2 instances of template-lint-disable', async () => { 48 | const task = new TemplateLintDisableTask( 49 | 'internal', 50 | getTaskContext({ 51 | paths: project.filePaths, 52 | options: { cwd: project.baseDir }, 53 | }) 54 | ); 55 | const result = await task.run(); 56 | 57 | let actions = evaluateActions(result, task.config); 58 | 59 | expect(actions).toHaveLength(1); 60 | expect(actions).toMatchInlineSnapshot(` 61 | [ 62 | { 63 | "defaultThreshold": 2, 64 | "details": "3 usages of template-lint-disable", 65 | "input": 3, 66 | "items": [ 67 | "Total template-lint-disable usages: 3", 68 | ], 69 | "name": "reduce-template-lint-disable-usages", 70 | "summary": "Reduce number of template-lint-disable usages", 71 | "taskName": "ember-template-lint-disable", 72 | }, 73 | ] 74 | `); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/tests/ember-types-task-test.ts: -------------------------------------------------------------------------------- 1 | import '@microsoft/jest-sarif'; 2 | import { EmberProject, getTaskContext } from '@checkup/test-helpers'; 3 | import { getPluginName } from '@checkup/core'; 4 | import EmberTypesTask from '../src/tasks/ember-types-task'; 5 | 6 | const TYPES = { 7 | components: { 8 | 'my-component.js': '', 9 | }, 10 | controllers: { 11 | 'my-controller.js': '', 12 | }, 13 | helpers: { 14 | 'my-helper.js': '', 15 | }, 16 | initializers: { 17 | 'my-initializer.js': '', 18 | }, 19 | 'instance-initializers': { 20 | 'my-helper.js': '', 21 | }, 22 | mixins: { 23 | 'my-mixin.js': '', 24 | }, 25 | models: { 26 | 'my-model.js': '', 27 | }, 28 | routes: { 29 | 'my-route.js': '', 30 | }, 31 | services: { 32 | 'my-service.js': '', 33 | }, 34 | templates: { 35 | 'my-component.hbs': '', 36 | }, 37 | }; 38 | 39 | describe('types-task', () => { 40 | let project: EmberProject; 41 | let pluginName = getPluginName(import.meta.url); 42 | 43 | beforeEach(function () { 44 | project = new EmberProject('checkup-app', '0.0.0'); 45 | }); 46 | 47 | afterEach(function () { 48 | project.dispose(); 49 | }); 50 | 51 | it('returns all the types found in the app and outputs to JSON', async () => { 52 | project.files = Object.assign(project.files, { 53 | 'index.js': 'index js file', 54 | addon: TYPES, 55 | }); 56 | 57 | project.writeSync(); 58 | 59 | const results = await new EmberTypesTask( 60 | pluginName, 61 | getTaskContext({ options: { cwd: project.baseDir }, paths: project.filePaths }) 62 | ).run(); 63 | 64 | for (let result of results) { 65 | expect(result).toBeValidSarifFor('result'); 66 | } 67 | }); 68 | 69 | it('returns all the types (including nested) found in the app and outputs to JSON', async () => { 70 | project.files = Object.assign(project.files, { 71 | 'index.js': 'index js file', 72 | addon: TYPES, 73 | }); 74 | 75 | project.addInRepoAddon('ember-super-button', 'latest'); 76 | 77 | (project.files.lib as any)['ember-super-button'].addon = TYPES; 78 | 79 | project.writeSync(); 80 | 81 | const results = await new EmberTypesTask( 82 | pluginName, 83 | getTaskContext({ options: { cwd: project.baseDir }, paths: project.filePaths }) 84 | ).run(); 85 | 86 | for (let result of results) { 87 | expect(result).toBeValidSarifFor('result'); 88 | } 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/tests/eslint-rule-tests/test-types.test.js: -------------------------------------------------------------------------------- 1 | import { RuleTester } from 'eslint'; 2 | import rule from '../../src/eslint/rules/test-types.js'; 3 | 4 | const ruleTester = new RuleTester(); 5 | 6 | ruleTester.run('test-type-counts', rule, { 7 | valid: [ 8 | // if there are no tests or skips in the module, it will be "valid" 9 | ` 10 | module('test', function(hooks) { 11 | setupApplicationTest(hooks); 12 | }); 13 | `, 14 | ], 15 | invalid: [ 16 | // correctly identifies application tests / skips / only / todo 17 | { 18 | code: ` 19 | module('test', function(hooks) { 20 | setupApplicationTest(hooks); 21 | 22 | test('testing foo'); 23 | skip('testing shmoo'); 24 | todo('blue goo'); 25 | only('macaroni'); 26 | }); 27 | `, 28 | errors: [ 29 | { message: 'application|test' }, 30 | { message: 'application|skip' }, 31 | { message: 'application|todo' }, 32 | { message: 'application|only' }, 33 | ], 34 | }, 35 | // correctly identifies rendering tests / skips / only / todo 36 | { 37 | code: ` 38 | module('test', function(hooks) { 39 | setupRenderingTest(hooks); 40 | 41 | test('testing foo'); 42 | test('testing foo'); 43 | skip('testing shmoo'); 44 | todo('blue goo'); 45 | todo('blue goo'); 46 | only('macaroni'); 47 | }); 48 | `, 49 | errors: [ 50 | { message: 'rendering|test' }, 51 | { message: 'rendering|test' }, 52 | { message: 'rendering|skip' }, 53 | { message: 'rendering|todo' }, 54 | { message: 'rendering|todo' }, 55 | { message: 'rendering|only' }, 56 | ], 57 | }, 58 | 59 | // correctly identifies unit tests / skips / only / todo 60 | { 61 | code: ` 62 | module('test', function(hooks) { 63 | setupTest(hooks); 64 | 65 | test('testing foo'); 66 | skip('testing shmoo'); 67 | skip('testing shmoo'); 68 | todo('blue goo'); 69 | only('macaroni'); 70 | only('macaroni'); 71 | }); 72 | `, 73 | errors: [ 74 | { message: 'unit|test' }, 75 | { message: 'unit|skip' }, 76 | { message: 'unit|skip' }, 77 | { message: 'unit|todo' }, 78 | { message: 'unit|only' }, 79 | { message: 'unit|only' }, 80 | ], 81 | }, 82 | ], 83 | }); 84 | -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "noEmit": true 5 | }, 6 | "references": [{ "path": ".." }] 7 | } 8 | -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-base.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "allowJs": true, 6 | "rootDir": "src" 7 | }, 8 | "references": [ 9 | { 10 | "path": "../core" 11 | }, 12 | { 13 | "path": "../test-helpers" 14 | } 15 | ], 16 | "include": ["src/**/*"] 17 | } 18 | -------------------------------------------------------------------------------- /packages/checkup-plugin-ember/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ['tests/**/*-test.ts'], 6 | globals: true, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /packages/checkup-plugin-javascript/README.md: -------------------------------------------------------------------------------- 1 | # checkup-plugin-javascript 2 | 3 | A checkup plugin for Javascript project tasks 4 | 5 | ![CI Build](https://github.com/checkupjs/checkup/workflows/CI%20Build/badge.svg) 6 | [![Version](https://img.shields.io/npm/v/checkup-plugin-javascript.svg)](https://npmjs.org/package/checkup-plugin-javascript) 7 | [![Downloads/week](https://img.shields.io/npm/dw/checkup-plugin-javascript.svg)](https://npmjs.org/package/checkup-plugin-javascript) 8 | [![License](https://img.shields.io/npm/l/checkup-plugin-javascript.svg)](https://github.com/checkupjs/checkup/blob/master/package.json) 9 | 10 | - [Usage](#usage) 11 | 12 | ## Usage 13 | 14 | 1. Install [@checkup/cli](https://github.com/checkupjs/checkup/blob/master/packages/cli/README.md) globally following the README. 15 | 16 | 2. Install `checkup-plugin-javascript` 17 | 18 | ```sh-session 19 | $ npm install --save-dev checkup-plugin-javascript 20 | 21 | # or 22 | 23 | $ yarn add --dev checkup-plugin-javascript 24 | ``` 25 | 26 | 3. Add `checkup-plugin-javascript` as a plugin to your config. 27 | 28 | ```json 29 | { 30 | "plugins": [ 31 | ... 32 | "checkup-plugin-javascript" 33 | ... 34 | ], 35 | "tasks": { 36 | ... 37 | } 38 | } 39 | ``` 40 | 41 | 4. Run checkup. 42 | 43 | ```sh-session 44 | $ checkup 45 | ``` 46 | -------------------------------------------------------------------------------- /packages/checkup-plugin-javascript/docs/tasks/eslint-disable-task.md: -------------------------------------------------------------------------------- 1 | 2 | # javascript/eslint-disables 3 | 4 | 5 | 6 | Finds all disabled eslint rules in a project 7 | 8 | 9 | 10 | ## To run this task 11 | 12 | ```bash 13 | checkup run --task javascript/eslint-disables 14 | ``` 15 | 16 | -------------------------------------------------------------------------------- /packages/checkup-plugin-javascript/docs/tasks/eslint-summary-task.md: -------------------------------------------------------------------------------- 1 | 2 | # javascript/eslint-summary 3 | 4 | 5 | 6 | Gets a summary of all eslint results in a project 7 | 8 | 9 | 10 | ## To run this task 11 | 12 | ```bash 13 | checkup run --task javascript/eslint-summary 14 | ``` 15 | 16 | -------------------------------------------------------------------------------- /packages/checkup-plugin-javascript/docs/tasks/lines-of-code-task.md: -------------------------------------------------------------------------------- 1 | 2 | # javascript/lines-of-code 3 | 4 | 5 | 6 | Counts lines of code within a project 7 | 8 | 9 | 10 | ## To run this task 11 | 12 | ```bash 13 | checkup run --task javascript/lines-of-code 14 | ``` 15 | 16 | -------------------------------------------------------------------------------- /packages/checkup-plugin-javascript/docs/tasks/outdated-dependencies-task.md: -------------------------------------------------------------------------------- 1 | 2 | # javascript/outdated-dependencies 3 | 4 | 5 | 6 | Gets a summary of all outdated dependencies in a project 7 | 8 | 9 | 10 | ## To run this task 11 | 12 | ```bash 13 | checkup run --task javascript/outdated-dependencies 14 | ``` 15 | 16 | -------------------------------------------------------------------------------- /packages/checkup-plugin-javascript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "checkup-plugin-javascript", 3 | "version": "3.0.1", 4 | "description": "A checkup plugin for Javascript project tasks", 5 | "keywords": [ 6 | "checkup-plugin" 7 | ], 8 | "homepage": "https://github.com/checkupjs/checkup", 9 | "bugs": "https://github.com/checkupjs/checkup/issues", 10 | "repository": "https://github.com/checkupjs/checkup", 11 | "license": "MIT", 12 | "author": "Cara Kessler @carakessler", 13 | "type": "module", 14 | "main": "lib/index.js", 15 | "types": "lib/index.d.ts", 16 | "files": [ 17 | "/lib", 18 | "/yarn.lock" 19 | ], 20 | "scripts": { 21 | "build": "tsc --build", 22 | "docs:generate": "checkup-plugin docs", 23 | "test": "vitest run" 24 | }, 25 | "dependencies": { 26 | "@babel/eslint-parser": "^7.17.0", 27 | "@babel/parser": "^7.18.0", 28 | "@checkup/core": "^3.0.1", 29 | "ember-template-recast": "^6.1.3", 30 | "npm-check": "^6.0.1", 31 | "recast": "^0.20.5", 32 | "semver": "^7.5.2", 33 | "sloc": "^0.2.1", 34 | "tslib": "^2" 35 | }, 36 | "devDependencies": { 37 | "@checkup/plugin": "^3.0.1", 38 | "@checkup/test-helpers": "^3.0.1", 39 | "@types/semver": "^7.3.9", 40 | "fixturify-project": "^2.1.1", 41 | "rimraf": "^3.0.2" 42 | }, 43 | "engines": { 44 | "node": ">= 16" 45 | }, 46 | "publishConfig": { 47 | "access": "public" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/checkup-plugin-javascript/src/actions/eslint-disable-actions.ts: -------------------------------------------------------------------------------- 1 | import { ActionsEvaluator, TaskConfig } from '@checkup/core'; 2 | import { Result } from 'sarif'; 3 | 4 | export function evaluateActions(taskResults: Result[], taskConfig: TaskConfig) { 5 | let actionsEvaluator = new ActionsEvaluator(); 6 | let eslintDisableUsages = taskResults.length; 7 | 8 | actionsEvaluator.add({ 9 | taskName: 'eslint-disable', 10 | name: 'reduce-eslint-disable-usages', 11 | summary: 'Reduce number of eslint-disable usages', 12 | details: `${eslintDisableUsages} usages of eslint-disable`, 13 | defaultThreshold: 2, 14 | items: [`Total eslint-disable usages: ${eslintDisableUsages}`], 15 | input: eslintDisableUsages, 16 | }); 17 | 18 | return actionsEvaluator.evaluate(taskConfig); 19 | } 20 | -------------------------------------------------------------------------------- /packages/checkup-plugin-javascript/src/actions/eslint-summary-actions.ts: -------------------------------------------------------------------------------- 1 | import { ActionsEvaluator, TaskConfig } from '@checkup/core'; 2 | import { Result } from 'sarif'; 3 | 4 | export function evaluateActions(taskResults: Result[], taskConfig: TaskConfig) { 5 | let actionsEvaluator = new ActionsEvaluator(); 6 | 7 | let errors = taskResults.filter((result: Result) => result.level === 'error')!; 8 | let warnings = taskResults.filter((result: Result) => result.level === 'warning')!; 9 | 10 | let errorCount = errors.length; 11 | let warningCount = warnings.length; 12 | 13 | actionsEvaluator.add({ 14 | taskName: 'eslint-summary', 15 | name: 'reduce-eslint-errors', 16 | summary: 'Reduce number of eslint errors', 17 | details: `${errorCount} total errors`, 18 | defaultThreshold: 20, 19 | items: [`Total eslint errors: ${errorCount}`], 20 | input: errorCount, 21 | }); 22 | 23 | actionsEvaluator.add({ 24 | taskName: 'eslint-summary', 25 | name: 'reduce-eslint-warnings', 26 | summary: 'Reduce number of eslint warnings', 27 | details: `${warningCount} total warnings`, 28 | defaultThreshold: 20, 29 | items: [`Total eslint warnings: ${warningCount}`], 30 | input: warningCount, 31 | }); 32 | 33 | return actionsEvaluator.evaluate(taskConfig); 34 | } 35 | -------------------------------------------------------------------------------- /packages/checkup-plugin-javascript/src/actions/outdated-dependency-actions.ts: -------------------------------------------------------------------------------- 1 | import { ActionsEvaluator, toPercent, TaskConfig } from '@checkup/core'; 2 | import { Result } from 'sarif'; 3 | 4 | export function evaluateActions(taskResults: Result[], taskConfig: TaskConfig) { 5 | let totalDependencies = taskResults.length; 6 | 7 | let outdatedDependencies = { 8 | major: taskResults.filter((taskResult) => taskResult.level === 'error').length, 9 | minor: taskResults.filter((taskResult) => taskResult.level === 'warning').length, 10 | }; 11 | 12 | let actionsEvaluator = new ActionsEvaluator(); 13 | 14 | actionsEvaluator.add({ 15 | taskName: 'outdated-dependencies', 16 | name: 'reduce-outdated-major-dependencies', 17 | summary: 'Update outdated major versions', 18 | details: `${outdatedDependencies.major} major versions outdated`, 19 | defaultThreshold: 0.05, 20 | items: [], 21 | input: outdatedDependencies.major / totalDependencies, 22 | }); 23 | actionsEvaluator.add({ 24 | taskName: 'outdated-dependencies', 25 | name: 'reduce-outdated-minor-dependencies', 26 | summary: 'Update outdated minor versions', 27 | details: `${outdatedDependencies.minor} minor versions outdated`, 28 | defaultThreshold: 0.05, 29 | items: [], 30 | input: outdatedDependencies.minor / totalDependencies, 31 | }); 32 | actionsEvaluator.add({ 33 | taskName: 'outdated-dependencies', 34 | name: 'reduce-outdated-dependencies', 35 | summary: 'Update outdated versions', 36 | details: `${toPercent( 37 | (outdatedDependencies.major + outdatedDependencies.minor) / totalDependencies 38 | )} of versions outdated`, 39 | defaultThreshold: 0.2, 40 | items: [], 41 | input: (outdatedDependencies.major + outdatedDependencies.minor) / totalDependencies, 42 | }); 43 | 44 | return actionsEvaluator.evaluate(taskConfig); 45 | } 46 | -------------------------------------------------------------------------------- /packages/checkup-plugin-javascript/src/index.ts: -------------------------------------------------------------------------------- 1 | import EslintDisableTask from './tasks/eslint-disable-task.js'; 2 | import EslintSummaryTask from './tasks/eslint-summary-task.js'; 3 | import OutdatedDependencyTask from './tasks/outdated-dependencies-task.js'; 4 | import LinesOfCodeTask from './tasks/lines-of-code-task.js'; 5 | import ValidEsmPackageTask from './tasks/valid-esm-package-task.js'; 6 | import { evaluateActions as evaluateESLintDisables } from './actions/eslint-disable-actions.js'; 7 | import { evaluateActions as evaluateESLintSummary } from './actions/eslint-summary-actions.js'; 8 | import { evaluateActions as evaluateOutdatedDependencies } from './actions/outdated-dependency-actions.js'; 9 | 10 | export default { 11 | tasks: { 12 | 'eslint-summary': EslintSummaryTask, 13 | 'eslint-disables': EslintDisableTask, 14 | 'outdated-dependencies': OutdatedDependencyTask, 15 | 'lines-of-code': LinesOfCodeTask, 16 | 'valid-esm-package': ValidEsmPackageTask, 17 | }, 18 | 19 | actions: { 20 | 'eslint-disables': evaluateESLintDisables, 21 | 'eslint-summary': evaluateESLintSummary, 22 | 'outdated-dependencies': evaluateOutdatedDependencies, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /packages/checkup-plugin-javascript/tests/lines-of-code-task-test.ts: -------------------------------------------------------------------------------- 1 | import { CheckupProject, getTaskContext } from '@checkup/test-helpers'; 2 | import { getPluginName } from '@checkup/core'; 3 | import LinesOfCodeTask from '../src/tasks/lines-of-code-task'; 4 | 5 | describe('lines-of-code-task', () => { 6 | let project: CheckupProject; 7 | let pluginName = getPluginName(import.meta.url); 8 | 9 | beforeEach(() => { 10 | project = new CheckupProject('checkup-app', '0.0.0', (project) => { 11 | project.files['index.js'] = ''; 12 | project.files['index.hbs'] = '
Checkup App
'; 13 | }); 14 | 15 | project.writeSync(); 16 | project.gitInit(); 17 | }); 18 | 19 | afterEach(() => { 20 | project.dispose(); 21 | }); 22 | 23 | it('can read task as JSON', async () => { 24 | const result = await new LinesOfCodeTask( 25 | pluginName, 26 | getTaskContext({ 27 | options: { cwd: project.baseDir }, 28 | pkg: project.pkg, 29 | }) 30 | ).run(); 31 | 32 | expect(result).toMatchInlineSnapshot(` 33 | [ 34 | { 35 | "kind": "informational", 36 | "level": "note", 37 | "locations": [ 38 | { 39 | "physicalLocation": { 40 | "artifactLocation": { 41 | "uri": "index.hbs", 42 | }, 43 | }, 44 | }, 45 | ], 46 | "message": { 47 | "text": "Lines of code count for index.hbs - total lines: 1", 48 | }, 49 | "properties": { 50 | "extension": "hbs", 51 | "filePath": "index.hbs", 52 | "lines": 1, 53 | }, 54 | "ruleId": "javascript/lines-of-code", 55 | "ruleIndex": 0, 56 | }, 57 | { 58 | "kind": "informational", 59 | "level": "note", 60 | "locations": [ 61 | { 62 | "physicalLocation": { 63 | "artifactLocation": { 64 | "uri": "index.js", 65 | }, 66 | }, 67 | }, 68 | ], 69 | "message": { 70 | "text": "Lines of code count for index.js - total lines: 1", 71 | }, 72 | "properties": { 73 | "extension": "js", 74 | "filePath": "index.js", 75 | "lines": 1, 76 | }, 77 | "ruleId": "javascript/lines-of-code", 78 | "ruleIndex": 0, 79 | }, 80 | ] 81 | `); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /packages/checkup-plugin-javascript/tests/outdated-dependencies-task-test.ts: -------------------------------------------------------------------------------- 1 | import '@microsoft/jest-sarif'; 2 | import { CheckupProject, getTaskContext } from '@checkup/test-helpers'; 3 | import { getPluginName, Task } from '@checkup/core'; 4 | import { Result } from 'sarif'; 5 | import OutdatedDependenciesTask from '../src/tasks/outdated-dependencies-task'; 6 | import { evaluateActions } from '../src/actions/outdated-dependency-actions'; 7 | 8 | // this test actually checks if dependencies are out of date, and will fail if new versions of react and react-dom are released. 9 | describe('outdated-dependencies-task', () => { 10 | let project: CheckupProject; 11 | let pluginName = getPluginName(import.meta.url); 12 | let task: Task; 13 | let results: Result[]; 14 | 15 | beforeAll(async () => { 16 | project = new CheckupProject('checkup-app', '0.0.0', (project) => { 17 | project.addDependency('react', '16.0.0'); 18 | project.addDependency('ember-cli', '3.20.0'); 19 | }); 20 | 21 | project.writeSync(); 22 | project.gitInit(); 23 | project.install(); 24 | 25 | task = new OutdatedDependenciesTask( 26 | pluginName, 27 | getTaskContext({ 28 | options: { cwd: project.baseDir }, 29 | pkg: project.pkg, 30 | }) 31 | ); 32 | 33 | results = await task.run(); 34 | }); 35 | 36 | afterAll(() => { 37 | project.dispose(); 38 | }); 39 | 40 | it('detects outdated dependencies as JSON', async () => { 41 | for (let result of results) { 42 | expect(result).toBeValidSarifFor('result'); 43 | } 44 | }); 45 | 46 | it('returns correct action items if too many dependencies are out of date (and additional actions for minor/major out of date)', async () => { 47 | let actions = evaluateActions(results, task.config); 48 | 49 | expect(actions).toHaveLength(2); 50 | expect(actions).toMatchInlineSnapshot(` 51 | [ 52 | { 53 | "defaultThreshold": 0.05, 54 | "details": "2 major versions outdated", 55 | "input": 1, 56 | "items": [], 57 | "name": "reduce-outdated-major-dependencies", 58 | "summary": "Update outdated major versions", 59 | "taskName": "outdated-dependencies", 60 | }, 61 | { 62 | "defaultThreshold": 0.2, 63 | "details": "100% of versions outdated", 64 | "input": 1, 65 | "items": [], 66 | "name": "reduce-outdated-dependencies", 67 | "summary": "Update outdated versions", 68 | "taskName": "outdated-dependencies", 69 | }, 70 | ] 71 | `); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /packages/checkup-plugin-javascript/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "noEmit": true 5 | }, 6 | "references": [{ "path": ".." }] 7 | } 8 | -------------------------------------------------------------------------------- /packages/checkup-plugin-javascript/tests/valid-esm-package-task-test.ts: -------------------------------------------------------------------------------- 1 | import { CheckupProject, getTaskContext } from '@checkup/test-helpers'; 2 | import { getPluginName } from '@checkup/core'; 3 | import ValidEsmPackageTask from '../src/tasks/valid-esm-package-task'; 4 | 5 | describe('valid-esm-package-task', () => { 6 | let project: CheckupProject; 7 | let pluginName = getPluginName(import.meta.url); 8 | 9 | beforeEach(() => { 10 | project = new CheckupProject('checkup-app', '0.0.0'); 11 | 12 | project.writeSync(); 13 | project.gitInit(); 14 | }); 15 | 16 | afterEach(() => { 17 | project.dispose(); 18 | }); 19 | 20 | it('valid esm package passes validation', async () => { 21 | project.write({ 22 | 'index.js': `function foo() { console.log('foo'); }`, 23 | }); 24 | project.updatePackageJson({ 25 | name: 'fake-package', 26 | type: 'module', 27 | exports: './index.js', 28 | description: '', 29 | version: '0.1.0', 30 | engines: { 31 | node: '>=13.2.0', 32 | }, 33 | dependencies: {}, 34 | devDependencies: {}, 35 | }); 36 | 37 | const results = await new ValidEsmPackageTask( 38 | pluginName, 39 | getTaskContext({ 40 | options: { cwd: project.baseDir }, 41 | pkg: project.pkg, 42 | }) 43 | ).run(); 44 | 45 | expect(results.filter((result) => result.kind === 'pass')).toHaveLength(5); 46 | }); 47 | 48 | it('invalid esm package fails validation', async () => { 49 | project.write({ 50 | 'index.js': `"use strict"; 51 | function foo() { console.log('foo'); }`, 52 | }); 53 | project.updatePackageJson({ 54 | name: 'fake-package', 55 | main: './index.js', 56 | description: '', 57 | version: '0.1.0', 58 | dependencies: {}, 59 | devDependencies: {}, 60 | }); 61 | 62 | const results = await new ValidEsmPackageTask( 63 | pluginName, 64 | getTaskContext({ 65 | options: { cwd: project.baseDir }, 66 | pkg: project.pkg, 67 | }) 68 | ).run(); 69 | 70 | expect(results.filter((result) => result.kind === 'fail')).toHaveLength(5); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /packages/checkup-plugin-javascript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-base.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "rootDir": "src" 6 | }, 7 | "references": [ 8 | { 9 | "path": "../core" 10 | }, 11 | { 12 | "path": "../test-helpers" 13 | } 14 | ], 15 | "include": ["src/**/*"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/checkup-plugin-javascript/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ['tests/**/*-test.ts'], 6 | globals: true, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /packages/cli/bin/checkup.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { createRequire } from 'module'; 4 | import importLocal from 'import-local'; 5 | 6 | process.env.NODE_ENV = 'production'; 7 | 8 | const require = createRequire(import.meta.url); 9 | const checkup = importLocal(require.resolve('../lib/checkup.js')); 10 | 11 | if (checkup) { 12 | checkup.run(); 13 | } else { 14 | // eslint-disable-next-line import/extensions 15 | const { run } = await import('../lib/checkup.js'); 16 | run(); 17 | } 18 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@checkup/cli", 3 | "version": "3.0.1", 4 | "description": "A health checkup for your project", 5 | "keywords": [ 6 | "checkup" 7 | ], 8 | "homepage": "https://github.com/checkupjs/checkup", 9 | "bugs": "https://github.com/checkupjs/checkup/issues", 10 | "repository": "https://github.com/checkupjs/checkup", 11 | "license": "MIT", 12 | "author": "Steve Calvert ", 13 | "type": "module", 14 | "main": "lib/index.js", 15 | "types": "lib/index.d.ts", 16 | "bin": { 17 | "checkup": "./bin/checkup.js" 18 | }, 19 | "files": [ 20 | "/bin", 21 | "/lib", 22 | "/templates", 23 | "/static" 24 | ], 25 | "scripts": { 26 | "build": "tsc --build", 27 | "test": "vitest run --threads false" 28 | }, 29 | "dependencies": { 30 | "@babel/parser": "^7.18.0", 31 | "@babel/traverse": "^7.13.17", 32 | "@babel/types": "7.18.8", 33 | "@checkup/core": "3.0.1", 34 | "@checkup/ui": "3.0.1", 35 | "chalk": "^4.0.0", 36 | "clean-stack": "^4.2.0", 37 | "convert-hrtime": "^3.0.0", 38 | "debug": "^4.3.1", 39 | "fs-extra": "^9.1.0", 40 | "import-local": "^3.1.0", 41 | "inquirer": "^8.2.6", 42 | "json-stable-stringify": "^1.0.1", 43 | "lodash": "^4.17.21", 44 | "log-symbols": "^4.0.0", 45 | "ora": "^5.3.0", 46 | "p-map": "^4.0.0", 47 | "react": "^17.0.2", 48 | "recast": "^0.20.5", 49 | "resolve": "^1.22.0", 50 | "shorthash": "^0.0.2", 51 | "stdout-stderr": "^0.1.13", 52 | "strip-ansi": "^6.0.0", 53 | "text-table": "^0.2.0", 54 | "tmp": "^0.2.1", 55 | "tslib": "^2", 56 | "wrap-ansi": "^7.0.0", 57 | "yargs": "^16.2.0", 58 | "yargs-unparser": "^2.0.0", 59 | "yeoman-environment": "^3.10.0", 60 | "yeoman-generator": "^5.4.2" 61 | }, 62 | "devDependencies": { 63 | "@checkup/test-helpers": "3.0.1", 64 | "execa": "^5.0.0", 65 | "yeoman-test": "^6.3.0" 66 | }, 67 | "engines": { 68 | "node": ">= 16" 69 | }, 70 | "publishConfig": { 71 | "access": "public" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/cli/src/api/generator.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { createRequire } from 'module'; 3 | import debug from 'debug'; 4 | import { CheckupError, ErrorKind } from '@checkup/core'; 5 | import yeomanEnv from 'yeoman-environment'; 6 | import fs from 'fs-extra'; 7 | 8 | export type GenerateOptions = { 9 | generator: string; 10 | name: string; 11 | path: string; 12 | defaults?: boolean; 13 | }; 14 | 15 | const require = createRequire(import.meta.url); 16 | const { existsSync, rmdirSync } = fs; 17 | const VALID_GENERATORS = ['config', 'plugin', 'task', 'actions']; 18 | 19 | export default class Generator { 20 | options: GenerateOptions; 21 | debug: debug.Debugger; 22 | 23 | constructor(options: GenerateOptions) { 24 | this.options = options; 25 | this.debug = debug('checkup:generator'); 26 | } 27 | 28 | async run() { 29 | this.debug('available generators', VALID_GENERATORS); 30 | 31 | if (!VALID_GENERATORS.includes(this.options.generator)) { 32 | throw new CheckupError(ErrorKind.GeneratorNotFound); 33 | } 34 | 35 | await this.generate(this.options.generator, { 36 | name: this.options.name, 37 | path: this.options.path === '.' ? process.cwd() : this.options.path, 38 | defaults: this.options.defaults, 39 | } as GenerateOptions); 40 | } 41 | 42 | async generate(type: string, generatorOptions: GenerateOptions) { 43 | this.debug('generatorOptions', generatorOptions); 44 | 45 | const env = yeomanEnv.createEnv(); 46 | 47 | env.register(require.resolve(`../generators/${type}`), `checkup:${type}`); 48 | 49 | try { 50 | // @ts-ignore 51 | await env.run(`checkup:${type}`, generatorOptions, null); 52 | 53 | // this is ugly, but I couldn't find the correct configuration to ignore 54 | // generating the yeoman repository directory in the cwd 55 | let yoRepoPath = join(this.options.path, '.yo-repository'); 56 | 57 | if (existsSync(yoRepoPath)) { 58 | rmdirSync(yoRepoPath); 59 | } 60 | } catch (error: unknown) { 61 | if (error instanceof CheckupError) { 62 | throw error; 63 | } 64 | 65 | throw new CheckupError(ErrorKind.Unknown, { error: error as Error }); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/cli/src/checkup.ts: -------------------------------------------------------------------------------- 1 | import yargs from 'yargs'; 2 | import { OutputFormat, ConsoleWriter, CheckupConfig } from '@checkup/core'; 3 | import { runCommand } from './commands/run.js'; 4 | import { generateCommand } from './commands/generate.js'; 5 | 6 | interface CheckupArguments { 7 | [x: string]: unknown; 8 | paths: string[]; 9 | excludePaths: string[]; 10 | config: CheckupConfig; 11 | configPath: string; 12 | cwd: string; 13 | category: string[]; 14 | group: string[]; 15 | task: string[]; 16 | pluginBaseDir: string; 17 | listTasks: boolean; 18 | format: OutputFormat; 19 | outputFile: string; 20 | } 21 | 22 | export type CLIOptions = CheckupArguments & yargs.ArgumentsCamelCase; 23 | 24 | export const consoleWriter = new ConsoleWriter(); 25 | export let parser: yargs.Argv<{}>; 26 | 27 | export async function run(argv: string[] = process.argv.slice(2)) { 28 | parser = yargs(argv) 29 | .scriptName('checkup') 30 | .usage( 31 | ` 32 | A health checkup for your project ✅ 33 | 34 | checkup [options]` 35 | ) 36 | .command(runCommand) 37 | .command(generateCommand) 38 | .showHelpOnFail(false) 39 | .help() 40 | .version(); 41 | 42 | parser.wrap(parser.terminalWidth()); 43 | 44 | if (argv.length === 0) { 45 | parser.showHelp(); 46 | } else { 47 | parser.parse(argv); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/cli/src/commands/generate.ts: -------------------------------------------------------------------------------- 1 | import yargs from 'yargs'; 2 | import { parser } from '../checkup.js'; 3 | import { generatePluginCommand } from './generate/plugin.js'; 4 | import { generateTaskCommand } from './generate/task.js'; 5 | import { generateActionsCommand } from './generate/actions.js'; 6 | import { generateConfigCommand } from './generate/config.js'; 7 | 8 | export const generateCommand: yargs.CommandModule = { 9 | command: 'generate', 10 | aliases: ['g'], 11 | describe: 'Runs a generator to scaffold Checkup code', 12 | builder: (yargs: any) => { 13 | return yargs 14 | .command(generatePluginCommand) 15 | .command(generateTaskCommand) 16 | .command(generateActionsCommand) 17 | .command(generateConfigCommand); 18 | }, 19 | handler: async () => { 20 | parser.showHelp(); 21 | process.exitCode = 1; 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /packages/cli/src/commands/generate/actions.ts: -------------------------------------------------------------------------------- 1 | import * as yargs from 'yargs'; 2 | import Generator from '../../api/generator.js'; 3 | import { consoleWriter } from '../../checkup.js'; 4 | 5 | export const generateActionsCommand = { 6 | command: 'actions [options]', 7 | describe: 'Generates checkup actions within a project', 8 | builder: (yargs: any) => { 9 | return yargs 10 | .positional('name', { 11 | description: 'Name of the actions (foo-task-actions)', 12 | default: '', 13 | }) 14 | .options({ 15 | defaults: { 16 | alias: 'd', 17 | description: 'Use defaults for every setting', 18 | boolean: true, 19 | }, 20 | path: { 21 | alias: 'p', 22 | default: '.', 23 | description: 'The path referring to the directory that the generator will run in', 24 | }, 25 | }); 26 | }, 27 | handler: async (argv: yargs.Arguments) => { 28 | try { 29 | let generator = new Generator({ 30 | path: argv.path as string, 31 | generator: 'actions', 32 | name: argv.name as string, 33 | defaults: argv.defaults as boolean, 34 | }); 35 | 36 | await generator.run(); 37 | } catch (error: unknown) { 38 | consoleWriter.error(error as Error); 39 | } 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /packages/cli/src/commands/generate/config.ts: -------------------------------------------------------------------------------- 1 | import * as yargs from 'yargs'; 2 | import Generator from '../../api/generator.js'; 3 | import { consoleWriter } from '../../checkup.js'; 4 | 5 | export const generateConfigCommand = { 6 | command: 'config', 7 | describe: 'Generates a .checkuprc within a project', 8 | builder: (yargs: any) => { 9 | return yargs.options({ 10 | path: { 11 | alias: 'p', 12 | default: '.', 13 | description: 'The path referring to the directory that the generator will run in', 14 | }, 15 | }); 16 | }, 17 | handler: async (argv: yargs.Arguments) => { 18 | try { 19 | let generator = new Generator({ 20 | path: argv.path as string, 21 | generator: 'config', 22 | name: 'config', 23 | defaults: false, 24 | }); 25 | 26 | await generator.run(); 27 | } catch (error: unknown) { 28 | consoleWriter.error(error as Error); 29 | } 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /packages/cli/src/commands/generate/plugin.ts: -------------------------------------------------------------------------------- 1 | import * as yargs from 'yargs'; 2 | import Generator from '../../api/generator.js'; 3 | 4 | export const generatePluginCommand = { 5 | command: 'plugin [options]', 6 | describe: 'Generates a checkup plugin project', 7 | builder: (yargs: any) => { 8 | return yargs 9 | .positional('name', { 10 | description: 'Name of the plugin (eg. checkup-plugin-myplugin)', 11 | default: '', 12 | }) 13 | .options({ 14 | defaults: { 15 | alias: 'd', 16 | description: 'Use defaults for every setting', 17 | boolean: true, 18 | }, 19 | path: { 20 | alias: 'p', 21 | default: '.', 22 | description: 'The path referring to the directory that the generator will run in', 23 | }, 24 | }); 25 | }, 26 | handler: async (argv: yargs.Arguments) => { 27 | let generator = new Generator({ 28 | path: argv.path as string, 29 | generator: 'plugin', 30 | name: argv.name as string, 31 | defaults: argv.defaults as boolean, 32 | }); 33 | await generator.run(); 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /packages/cli/src/commands/generate/task.ts: -------------------------------------------------------------------------------- 1 | import * as yargs from 'yargs'; 2 | import Generator from '../../api/generator.js'; 3 | import { consoleWriter } from '../../checkup.js'; 4 | 5 | export const generateTaskCommand = { 6 | command: 'task [options]', 7 | describe: 'Generates a checkup task within a project', 8 | builder: (yargs: any) => { 9 | return yargs 10 | .positional('name', { 11 | description: 'Name of the task (foo-task)', 12 | default: '', 13 | }) 14 | .options({ 15 | defaults: { 16 | alias: 'd', 17 | description: 'Use defaults for every setting', 18 | boolean: true, 19 | }, 20 | path: { 21 | alias: 'p', 22 | default: '.', 23 | description: 'The path referring to the directory that the generator will run in', 24 | }, 25 | }); 26 | }, 27 | handler: async (argv: yargs.Arguments) => { 28 | try { 29 | let generator = new Generator({ 30 | path: argv.path as string, 31 | generator: 'task', 32 | name: argv.name as string, 33 | defaults: argv.defaults as boolean, 34 | }); 35 | 36 | await generator.run(); 37 | } catch (error: unknown) { 38 | consoleWriter.error(error as Error); 39 | } 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /packages/cli/src/formatters/available-tasks.ts: -------------------------------------------------------------------------------- 1 | import { ConsoleWriter } from '@checkup/core'; 2 | 3 | export function reportAvailableTasks(availableTasks: string[]) { 4 | let consoleWriter = new ConsoleWriter(); 5 | 6 | consoleWriter.blankLine(); 7 | consoleWriter.log(consoleWriter.emphasize('AVAILABLE TASKS')); 8 | consoleWriter.blankLine(); 9 | if (availableTasks.length > 0) { 10 | availableTasks.forEach((taskName) => { 11 | consoleWriter.log(` ${taskName}`); 12 | }); 13 | } else { 14 | consoleWriter.log(` No tasks found`); 15 | } 16 | consoleWriter.blankLine(); 17 | } 18 | -------------------------------------------------------------------------------- /packages/cli/src/formatters/base-formatter.ts: -------------------------------------------------------------------------------- 1 | import { BaseOutputWriter, FormatterOptions, CheckupMetadata } from '@checkup/core'; 2 | import { Notification } from 'sarif'; 3 | import chalk from 'chalk'; 4 | 5 | export default abstract class BaseFormatter { 6 | writer!: T; 7 | 8 | constructor(public options: FormatterOptions) {} 9 | 10 | renderMetadata(metaData: CheckupMetadata) { 11 | let { analyzedFilesCount, project } = metaData; 12 | let { name, version, repository } = project; 13 | 14 | let analyzedFilesMessage = 15 | repository.totalFiles !== analyzedFilesCount 16 | ? ` (${this.writer.emphasize(`${analyzedFilesCount} files`)} analyzed)` 17 | : ''; 18 | 19 | this.writer.blankLine(); 20 | this.writer.log( 21 | `Checkup report generated for ${this.writer.emphasize( 22 | `${name} v${version}` 23 | )}${analyzedFilesMessage}` 24 | ); 25 | this.writer.blankLine(); 26 | this.writer.log( 27 | `This project is ${this.writer.emphasize( 28 | `${repository.age} old` 29 | )}, with ${this.writer.emphasize( 30 | `${repository.activeDays} active days` 31 | )}, ${this.writer.emphasize( 32 | `${repository.totalCommits} commits` 33 | )} and ${this.writer.emphasize(`${repository.totalFiles} files`)}.` 34 | ); 35 | this.writer.blankLine(); 36 | } 37 | 38 | renderActions(actions: Notification[]): void { 39 | if (actions && actions.length > 0) { 40 | this.writer.categoryHeader('Actions'); 41 | actions.forEach((action: Notification) => { 42 | this.writer.log(`${chalk.yellow('■')} ${action.message.text}`); 43 | }); 44 | this.writer.blankLine(); 45 | } 46 | } 47 | 48 | renderCLIInfo(metadata: CheckupMetadata) { 49 | let { version, configHash } = metadata.cli; 50 | 51 | this.writer.dimmed(`checkup v${version}`); 52 | this.writer.dimmed(`config ${configHash}`); 53 | this.writer.blankLine(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/cli/src/formatters/components/Summary.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import logSymbols from 'log-symbols'; 3 | import { CheckupLogParser, FormatterOptions, TaskName } from '@checkup/core'; 4 | import { ReportingDescriptor } from 'sarif'; 5 | import { 6 | Box, 7 | Text, 8 | MetaData, 9 | TaskTiming, 10 | CLIInfo, 11 | Actions, 12 | ResultsToFile, 13 | TaskErrors, 14 | } from '@checkup/ui'; 15 | 16 | export const Summary: React.FC<{ 17 | logParser: CheckupLogParser; 18 | options: FormatterOptions; 19 | }> = ({ logParser, options }) => { 20 | let { actions, executedTasks, log, metaData, timings, tasksWithExceptions } = logParser; 21 | 22 | return ( 23 | 24 | 25 | 26 | 27 | 0} /> 28 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | const TaskResults: React.FC<{ 36 | executedTasks: ReportingDescriptor[]; 37 | tasksWithExceptions: Set; 38 | }> = ({ executedTasks, tasksWithExceptions }) => { 39 | return ( 40 | <> 41 | 42 | Checkup ran the following task(s): 43 | 44 | 45 | {executedTasks 46 | .map((rule) => rule.id) 47 | .sort() 48 | .map((taskName) => { 49 | return ( 50 | 51 | {`${tasksWithExceptions.has(taskName) ? logSymbols.error : logSymbols.success} `} 52 | {taskName} 53 | 54 | ); 55 | })} 56 | 57 | 58 | ); 59 | }; 60 | 61 | export default Summary; 62 | -------------------------------------------------------------------------------- /packages/cli/src/formatters/json.ts: -------------------------------------------------------------------------------- 1 | import { CheckupLogParser, Formatter, FormatterOptions } from '@checkup/core'; 2 | 3 | export default class JsonFormatter implements Formatter { 4 | shouldWrite = true; 5 | options: FormatterOptions; 6 | 7 | constructor(options: FormatterOptions) { 8 | this.options = options; 9 | } 10 | 11 | format(logParser: CheckupLogParser) { 12 | return JSON.stringify(logParser.log, null, 2); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/cli/src/formatters/pretty.ts: -------------------------------------------------------------------------------- 1 | import { FormatterOptions } from '@checkup/core'; 2 | import { BaseUIFormatter } from '@checkup/ui'; 3 | import Pretty from './components/Pretty.js'; 4 | 5 | export default class PrettyFormatter extends BaseUIFormatter { 6 | shouldWrite = true; 7 | 8 | constructor(options: FormatterOptions) { 9 | super(options, Pretty); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/cli/src/formatters/summary.ts: -------------------------------------------------------------------------------- 1 | import { FormatterOptions } from '@checkup/core'; 2 | import { BaseUIFormatter } from '@checkup/ui'; 3 | import Summary from './components/Summary.js'; 4 | 5 | export default class SummaryFormatter extends BaseUIFormatter { 6 | shouldWrite = true; 7 | 8 | constructor(options: FormatterOptions) { 9 | super(options, Summary); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/cli/src/generators/config.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | import { writeConfig, CheckupError, ErrorKind } from '@checkup/core'; 4 | import BaseGenerator, { Works } from './base-generator.js'; 5 | 6 | export default class ConfigGenerator extends BaseGenerator { 7 | works: Works = Works.OutsideProject; 8 | 9 | async initializing() { 10 | if (!this.canRunGenerator) { 11 | throw new CheckupError(ErrorKind.ConfigFileExists, { 12 | configDestination: this.destinationRoot(), 13 | }); 14 | } 15 | } 16 | 17 | async prompting() { 18 | this.headline('checkup config'); 19 | } 20 | 21 | async writing() { 22 | writeConfig(this.destinationRoot()); 23 | 24 | this.log(` ${chalk.green('create')} ${this.destinationPath('.checkuprc')}`); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/cli/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CheckupTaskRunner } from './api/checkup-task-runner.js'; 2 | export { getFormatter } from './formatters/get-formatter.js'; 3 | -------------------------------------------------------------------------------- /packages/cli/src/task-result-comparator.ts: -------------------------------------------------------------------------------- 1 | import { Result } from 'sarif'; 2 | 3 | const DEFAULT_CATEGORIES: Record = { 4 | metrics: 6, 5 | 'best practices': 5, 6 | dependencies: 4, 7 | linting: 3, 8 | testing: 2, 9 | migrations: 1, 10 | }; 11 | 12 | function getCategorySort(category: string): number { 13 | return DEFAULT_CATEGORIES[category] ?? -1; 14 | } 15 | 16 | export function taskResultComparator(first: T, second: T) { 17 | let firstC = first.properties?.category; 18 | let firstGroup = first.properties?.group || ''; 19 | let secondC = second.properties?.category; 20 | let secondGroup = second.properties?.group || ''; 21 | 22 | let firstCategory = getCategorySort(firstC); 23 | let secondCategory = getCategorySort(secondC); 24 | 25 | if (firstCategory === secondCategory) { 26 | // eslint-disable-next-line unicorn/no-nested-ternary 27 | return firstGroup > secondGroup ? -1 : firstGroup < secondGroup ? 1 : 0; 28 | } else { 29 | return firstCategory > secondCategory ? -1 : 1; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/cli/src/utils/get-version.ts: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'module'; 2 | 3 | const require = createRequire(import.meta.url); 4 | const { version } = require('../../package.json'); 5 | 6 | export function getVersion(fakeVersion: string = '0.0.0') { 7 | if (process.env.JEST_WORKER_ID !== undefined) { 8 | return fakeVersion; 9 | } 10 | 11 | return version; 12 | } 13 | -------------------------------------------------------------------------------- /packages/cli/templates/src/actions/src/actions/actions.js.ejs: -------------------------------------------------------------------------------- 1 | const { ActionsEvaluator } = require('@checkup/core'); 2 | 3 | module.exports = function evaluateActions(taskResult, taskConfig) { 4 | let actionsEvaluator = new ActionsEvaluator(); 5 | 6 | return actionsEvaluator.evaluate(taskConfig); 7 | } -------------------------------------------------------------------------------- /packages/cli/templates/src/actions/src/actions/actions.ts.ejs: -------------------------------------------------------------------------------- 1 | import { TaskConfig, ActionsEvaluator } from '@checkup/core'; 2 | import { Result } from 'sarif'; 3 | 4 | export function evaluateActions(taskResults: Result[], taskConfig: TaskConfig) { 5 | let actionsEvaluator = new ActionsEvaluator(); 6 | 7 | return actionsEvaluator.evaluate(taskConfig); 8 | } 9 | -------------------------------------------------------------------------------- /packages/cli/templates/src/plugin/.eslintignore.ejs: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | -------------------------------------------------------------------------------- /packages/cli/templates/src/plugin/.eslintrc.js.ejs: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@babel/eslint-parser", 4 | "parserOptions": { 5 | "ecmaVersion": 2017, 6 | "sourceType": "module" 7 | }, 8 | "plugins": ["jest", "prettier"], 9 | "extends": [ 10 | "eslint:recommended", 11 | "prettier", 12 | "plugin:jest/recommended", 13 | "plugin:jest/style" 14 | ], 15 | "env": { 16 | "browser": false, 17 | "node": true, 18 | "es6": true 19 | }, 20 | "rules": { 21 | "prettier/prettier": "error", 22 | "no-unused-vars": "off", 23 | "no-global-assign": [ 24 | "error", 25 | { 26 | "exceptions": ["console"] 27 | } 28 | ] 29 | }, 30 | "overrides": [ 31 | { 32 | "files": ["__tests__/**/*.js"], 33 | "env": { 34 | "jest": true 35 | } 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /packages/cli/templates/src/plugin/.eslintrc.ts.ejs: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 2017, 6 | "sourceType": "module" 7 | }, 8 | "plugins": ["@typescript-eslint", "jest", "prettier"], 9 | "extends": [ 10 | "eslint:recommended", 11 | "prettier", 12 | "prettier/@typescript-eslint", 13 | "plugin:jest/recommended", 14 | "plugin:jest/style" 15 | ], 16 | "env": { 17 | "browser": false, 18 | "node": true, 19 | "es6": true 20 | }, 21 | "rules": { 22 | "prettier/prettier": "error", 23 | "no-unused-vars": "off", 24 | "@typescript-eslint/no-unused-vars": "error", 25 | "no-global-assign": [ 26 | "error", 27 | { 28 | "exceptions": ["console"] 29 | } 30 | ] 31 | }, 32 | "overrides": [ 33 | { 34 | "files": ["__tests__/**/*.ts"], 35 | "env": { 36 | "jest": true 37 | } 38 | } 39 | ] 40 | } -------------------------------------------------------------------------------- /packages/cli/templates/src/plugin/.gitignore.ejs: -------------------------------------------------------------------------------- 1 | lib 2 | .eslintcache -------------------------------------------------------------------------------- /packages/cli/templates/src/plugin/.prettierrc.js.ejs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: 'es5', 4 | printWidth: 100, 5 | }; -------------------------------------------------------------------------------- /packages/cli/templates/src/plugin/README.md.ejs: -------------------------------------------------------------------------------- 1 | # <%- name %> 2 | 3 | <%- description %> 4 | 5 | [![Version](https://img.shields.io/npm/v/plugin-default.svg)](https://npmjs.org/package/plugin-default) 6 | [![Downloads/week](https://img.shields.io/npm/dw/plugin-default.svg)](https://npmjs.org/package/plugin-default) 7 | [![License](https://img.shields.io/npm/l/plugin-default.svg)](https://github.com/checkupjs/checkup/blob/master/package.json) 8 | 9 | 10 | 11 | # Usage 12 | 13 | 14 | 15 | # Commands 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/cli/templates/src/plugin/__tests__/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/checkupjs/checkup/f7f715e4c38b826bc069f99a752724f017c4d78a/packages/cli/templates/src/plugin/__tests__/.gitkeep -------------------------------------------------------------------------------- /packages/cli/templates/src/plugin/jest.config.js.ejs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | moduleFileExtensions: ['js', 'json'], 4 | coverageReporters: ['lcov', 'text-summary'], 5 | collectCoverageFrom: ['src/**/*.js'], 6 | coveragePathIgnorePatterns: ['/templates/'], 7 | coverageThreshold: { 8 | global: { 9 | branches: 100, 10 | functions: 100, 11 | lines: 100, 12 | statements: 100, 13 | }, 14 | }, 15 | testPathIgnorePatterns: ['__fixtures__'], 16 | }; 17 | -------------------------------------------------------------------------------- /packages/cli/templates/src/plugin/jest.config.ts.ejs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | moduleFileExtensions: ['ts', 'js', 'json'], 4 | transform: { '\\.ts$': 'ts-jest' }, 5 | coverageReporters: ['lcov', 'text-summary'], 6 | collectCoverageFrom: ['src/**/*.ts'], 7 | coveragePathIgnorePatterns: ['/templates/'], 8 | coverageThreshold: { 9 | global: { 10 | branches: 100, 11 | functions: 100, 12 | lines: 100, 13 | statements: 100, 14 | }, 15 | }, 16 | testPathIgnorePatterns: ['__fixtures__'], 17 | }; 18 | -------------------------------------------------------------------------------- /packages/cli/templates/src/plugin/package.json.js.ejs: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%- name %>", 3 | "description": "<%- description %>", 4 | "version": "0.0.0", 5 | "author": "<%- author %>", 6 | "dependencies": { 7 | "@checkup/core": "^<%- checkupVersion %>" 8 | }, 9 | "devDependencies": { 10 | "@checkup/test-helpers": "^<%- checkupVersion %>", 11 | "eslint": "^7.5.0", 12 | "eslint-config-prettier": "^6.11.0", 13 | "eslint-plugin-jest": "^23.8.2", 14 | "eslint-plugin-node": "^11.1.0", 15 | "eslint-plugin-prettier": "^3.1.3", 16 | "jest": "^25.1.0", 17 | "prettier": "^2.0.5" 18 | }, 19 | "type": "module", 20 | "engines": { 21 | "node": ">= 16" 22 | }, 23 | "files": [ 24 | "/lib" 25 | ], 26 | "keywords": [ 27 | "checkup-plugin" 28 | ], 29 | "license": "MIT", 30 | "repository": "<%- repository %>", 31 | "scripts": { 32 | "lint": "eslint . --cache", 33 | "test": "jest --no-cache" 34 | }, 35 | "main": "lib/index.js" 36 | } 37 | -------------------------------------------------------------------------------- /packages/cli/templates/src/plugin/package.json.ts.ejs: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%- name %>", 3 | "description": "<%- description %>", 4 | "version": "0.0.0", 5 | "author": "<%- author %>", 6 | "dependencies": { 7 | "@checkup/core": "^<%- checkupVersion %>", 8 | "tslib": "^1" 9 | }, 10 | "devDependencies": { 11 | "@checkup/plugin": "^<%- checkupVersion %>", 12 | "@checkup/test-helpers": "^<%- checkupVersion %>", 13 | "@types/jest": "^25.1.3", 14 | "@types/node": "^13", 15 | "@typescript-eslint/eslint-plugin": "^3.5.0", 16 | "@typescript-eslint/parser": "^3.5.0", 17 | "eslint": "^7.5.0", 18 | "eslint-config-prettier": "^6.11.0", 19 | "eslint-plugin-jest": "^23.8.2", 20 | "eslint-plugin-node": "^11.1.0", 21 | "eslint-plugin-prettier": "^3.1.3", 22 | "jest": "^25.1.0", 23 | "prettier": "^2.0.5", 24 | "ts-jest": "^25.2.1", 25 | "ts-node": "^8", 26 | "typescript": "^3.8" 27 | }, 28 | "type": "module", 29 | "engines": { 30 | "node": ">= 16" 31 | }, 32 | "files": [ 33 | "/lib" 34 | ], 35 | "keywords": [ 36 | "checkup-plugin" 37 | ], 38 | "license": "MIT", 39 | "repository": "<%- repository %>", 40 | "scripts": { 41 | "build": "yarn clean && tsc", 42 | "build:watch": "yarn build -w", 43 | "clean": "rm -rf lib", 44 | "docs:generate": "checkup-plugin docs", 45 | "lint": "eslint . --cache --ext .ts", 46 | "test": "jest --no-cache" 47 | }, 48 | "types": "lib/index.d.ts", 49 | "main": "lib/index.js" 50 | } 51 | -------------------------------------------------------------------------------- /packages/cli/templates/src/plugin/src/index.js.ejs: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /packages/cli/templates/src/plugin/src/index.ts.ejs: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /packages/cli/templates/src/plugin/src/results/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/checkupjs/checkup/f7f715e4c38b826bc069f99a752724f017c4d78a/packages/cli/templates/src/plugin/src/results/.gitkeep -------------------------------------------------------------------------------- /packages/cli/templates/src/plugin/src/tasks/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/checkupjs/checkup/f7f715e4c38b826bc069f99a752724f017c4d78a/packages/cli/templates/src/plugin/src/tasks/.gitkeep -------------------------------------------------------------------------------- /packages/cli/templates/src/plugin/src/types/index.js.ejs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/checkupjs/checkup/f7f715e4c38b826bc069f99a752724f017c4d78a/packages/cli/templates/src/plugin/src/types/index.js.ejs -------------------------------------------------------------------------------- /packages/cli/templates/src/plugin/src/types/index.ts.ejs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/checkupjs/checkup/f7f715e4c38b826bc069f99a752724f017c4d78a/packages/cli/templates/src/plugin/src/types/index.ts.ejs -------------------------------------------------------------------------------- /packages/cli/templates/src/plugin/tsconfig.json.ejs: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "importHelpers": true, 5 | "module": "ESNext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "target": "es2019", 9 | "allowSyntheticDefaultImports": true, 10 | "sourceMap": true, 11 | "outDir": "lib", 12 | "rootDir": "src", 13 | "types": ["jest", "node"] 14 | }, 15 | "include": ["src/**/*"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/cli/templates/src/task/__tests__/task.js.ejs: -------------------------------------------------------------------------------- 1 | <%_ const taskClass = `${_.upperFirst(_.camelCase(name))}Task` _%> 2 | <%_ const resultClass = `${taskClass}Result` _%> 3 | import { CheckupProject, getTaskContext } from '@checkup/test-helpers'; 4 | import { getPluginName } from '@checkup/core'; 5 | import <%- taskClass %> from '../src/tasks/<%- name %>-task'; 6 | 7 | describe('<%- name %>-task', () => { 8 | let project; 9 | let pluginName = getPluginName(import.meta.url); 10 | 11 | beforeEach(() => { 12 | project = new CheckupProject('checkup-app', '0.0.0', project => { 13 | // add dependencies here 14 | }); 15 | 16 | project.writeSync(); 17 | project.gitInit(); 18 | }); 19 | 20 | afterEach(() => { 21 | project.dispose(); 22 | }); 23 | 24 | it('can read task as JSON', async () => { 25 | const result = await new <%- taskClass %>( 26 | pluginName, 27 | getTaskContext({ 28 | options: { cwd: project.baseDir }, 29 | pkg: project.pkg, 30 | })).run(); 31 | 32 | expect(result).toMatchSnapshot(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/cli/templates/src/task/__tests__/task.ts.ejs: -------------------------------------------------------------------------------- 1 | <%_ const taskClass = `${_.upperFirst(_.camelCase(name))}Task` _%> 2 | <%_ const resultClass = `${taskClass}Result` _%> 3 | import { CheckupProject, getTaskContext } from '@checkup/test-helpers'; 4 | import { getPluginName } from '@checkup/core'; 5 | import <%- taskClass %> from '../src/tasks/<%- name %>-task'; 6 | 7 | describe('<%- name %>-task', () => { 8 | let project: CheckupProject; 9 | let pluginName = getPluginName(import.meta.url); 10 | 11 | beforeEach(() => { 12 | project = new CheckupProject('checkup-app', '0.0.0', project => { 13 | // add dependencies here 14 | }); 15 | 16 | project.writeSync(); 17 | project.gitInit(); 18 | }); 19 | 20 | afterEach(() => { 21 | project.dispose(); 22 | }); 23 | 24 | it('can read task as JSON', async () => { 25 | const result = await new <%- taskClass %>( 26 | pluginName, 27 | getTaskContext({ 28 | options: { cwd: project.baseDir }, 29 | pkg: project.pkg, 30 | })).run(); 31 | 32 | expect(result).toMatchSnapshot(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/cli/templates/src/task/src/tasks/task.js.ejs: -------------------------------------------------------------------------------- 1 | import { BaseTask } from '@checkup/core'; 2 | 3 | export default class <%- taskClass %> extends BaseTask { 4 | taskName = '<%- name %>'; 5 | taskDisplayName = '<%- _.startCase(name.split('-').join(' ')) %>'; 6 | description = '<%- description %>'; 7 | category = '<%- category %>'; 8 | <%_ if (group) { _%> 9 | group = '<%- group %>'; 10 | <%_ } _%> 11 | 12 | constructor(pluginName, context) { 13 | super(pluginName, context); 14 | 15 | this.addRule(); 16 | } 17 | 18 | async run() { 19 | this.addResult( 20 | `${this.taskName} result`, 21 | 'informational', 22 | 'note' 23 | ); 24 | 25 | return this.results; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/cli/templates/src/task/src/tasks/task.ts.ejs: -------------------------------------------------------------------------------- 1 | import { BaseTask, Task, TaskContext } from '@checkup/core'; 2 | import { Result } from 'sarif'; 3 | 4 | export default class <%- taskClass %> extends BaseTask implements Task { 5 | taskName = '<%- name %>'; 6 | taskDisplayName = '<%- _.startCase(name.split('-').join(' ')) %>'; 7 | description = '<%- description %>'; 8 | category = '<%- category %>'; 9 | <%_ if (group) { _%> 10 | group = '<%- group %>'; 11 | <%_ } _%> 12 | 13 | constructor(pluginName: string, context: TaskContext) { 14 | super(pluginName, context); 15 | 16 | this.addRule(); 17 | } 18 | 19 | async run(): Promise { 20 | this.addResult( 21 | `${this.taskName} result`, 22 | 'informational', 23 | 'note' 24 | ); 25 | 26 | return this.results; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/cli/tests/__fixtures__/checkup-formatter-test/index.js: -------------------------------------------------------------------------------- 1 | export default class CustomFormatter { 2 | constructor(options = {}) { 3 | this.options = options; 4 | } 5 | 6 | format(logParser) { 7 | return `Custom formatter output ${logParser.log}`; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/cli/tests/__fixtures__/checkup-formatter-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "checkup-formatter-test", 3 | "version": "1.0.0", 4 | "description": "test formatter to show how to package formatters and use them.", 5 | "type": "module" 6 | } 7 | -------------------------------------------------------------------------------- /packages/cli/tests/__utils__/fake-project.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import * as helpers from 'yeoman-test'; 3 | import { CheckupProject } from '@checkup/test-helpers'; 4 | import { dirname } from '@checkup/core'; 5 | import type { Answers } from 'inquirer'; 6 | import { generatePlugin, generateTask } from './generator-utils'; 7 | 8 | export class FakeProject extends CheckupProject { 9 | symlinkCorePackage(baseDir: string = this.baseDir) { 10 | let source = join(dirname(import.meta), '../../..', 'core'); 11 | let target = join(baseDir, 'node_modules', '@checkup', 'core'); 12 | 13 | // we create a self-referential link to the core package within the generated plugin. This 14 | // allows us to generate plugins, and test the APIs for the latest version of core that is 15 | // referenced via the plugins' node_modules. 16 | this.symlinkPackage(source, target); 17 | } 18 | 19 | async addPlugin(options: helpers.Dictionary = {}, prompts: Answers = {}) { 20 | let pluginDir = await generatePlugin(options, prompts, join(this.baseDir, 'node_modules')); 21 | 22 | this.symlinkCorePackage(pluginDir); 23 | 24 | return pluginDir; 25 | } 26 | 27 | async addTask(options: helpers.Dictionary = {}, prompts: Answers = {}, pluginDir: string) { 28 | await generateTask(options, prompts, pluginDir); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/cli/tests/__utils__/generator-utils.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import * as helpers from 'yeoman-test'; 3 | 4 | import { Answers } from 'inquirer'; 5 | import { createTmpDir } from '@checkup/test-helpers'; 6 | import PluginGenerator from '../../src/generators/plugin'; 7 | import TaskGenerator from '../../src/generators/task'; 8 | 9 | const DEFAULT_PLUGIN_OPTIONS = { 10 | name: 'my-plugin', 11 | defaults: true, 12 | }; 13 | 14 | const DEFAULT_PLUGIN_PROMPTS = { 15 | typescript: true, 16 | description: '', 17 | author: '', 18 | repository: '', 19 | }; 20 | 21 | const DEFAULT_TASK_OPTIONS = { 22 | name: 'my-task', 23 | path: '.', 24 | defaults: true, 25 | }; 26 | 27 | const DEFAULT_TASK_PROMPTS = { 28 | typescript: true, 29 | category: 'best practices', 30 | group: '', 31 | }; 32 | 33 | export async function generatePlugin( 34 | options: helpers.Dictionary = {}, 35 | prompts: Answers = {}, 36 | tmp: string = createTmpDir() 37 | ) { 38 | let mergedOptions = Object.assign({ path: '.' }, DEFAULT_PLUGIN_OPTIONS, options); 39 | let mergedPrompts = Object.assign({}, DEFAULT_PLUGIN_PROMPTS, prompts); 40 | let dir = await helpers 41 | .run(PluginGenerator, { namespace: 'checkup:plugin' }) 42 | .cd(tmp) 43 | .withOptions(mergedOptions) 44 | .withPrompts(mergedPrompts); 45 | 46 | return options.path 47 | ? join(dir.cwd, options.path, `checkup-plugin-${mergedOptions.name}`) 48 | : join(dir.cwd, `checkup-plugin-${mergedOptions.name}`); 49 | } 50 | 51 | export async function generateTask( 52 | options: helpers.Dictionary = {}, 53 | prompts: Answers = {}, 54 | tmp: string = createTmpDir() 55 | ) { 56 | let mergedOptions = Object.assign({}, DEFAULT_TASK_OPTIONS, options); 57 | let mergedPrompts = Object.assign({}, DEFAULT_TASK_PROMPTS, prompts); 58 | 59 | return await helpers 60 | .run(TaskGenerator, { namespace: 'checkup:task' }) 61 | .cd(tmp) 62 | .withOptions(mergedOptions) 63 | .withPrompts(mergedPrompts); 64 | } 65 | -------------------------------------------------------------------------------- /packages/cli/tests/__utils__/get-fixture.ts: -------------------------------------------------------------------------------- 1 | import { isAbsolute, resolve } from 'path'; 2 | import { readJsonSync } from 'fs-extra'; 3 | import { dirname } from '@checkup/core'; 4 | 5 | export function getFixture(fixturePath: string) { 6 | let path: string = isAbsolute(fixturePath) 7 | ? fixturePath 8 | : resolve(dirname(import.meta), '..', '__fixtures__', fixturePath); 9 | 10 | return readJsonSync(path); 11 | } 12 | -------------------------------------------------------------------------------- /packages/cli/tests/__utils__/mock-task-result.ts: -------------------------------------------------------------------------------- 1 | import { Result } from 'sarif'; 2 | 3 | export function getMockResult( 4 | taskName: string, 5 | category: string, 6 | group: string = '', 7 | // eslint-disable-next-line unicorn/no-object-as-default-parameter 8 | result: Result = { message: { text: 'hey' } } 9 | ): Result { 10 | result.properties = { 11 | ...result.properties, 12 | taskName: taskName, 13 | taskDisplayName: taskName, 14 | category: category, 15 | group: group, 16 | }; 17 | return result; 18 | } 19 | -------------------------------------------------------------------------------- /packages/cli/tests/formatters/json-test.ts: -------------------------------------------------------------------------------- 1 | import '@microsoft/jest-sarif'; 2 | import { CheckupLogParser, FormatterOptions } from '@checkup/core'; 3 | import stripAnsi from 'strip-ansi'; 4 | import JsonFormatter from '../../src/formatters/json.js'; 5 | import { getFixture } from '../__utils__/get-fixture.js'; 6 | 7 | describe('Json formatter', () => { 8 | it('can generate string from format', async () => { 9 | const log = getFixture('checkup-result.sarif'); 10 | const logParser = new CheckupLogParser(log); 11 | const options: FormatterOptions = { 12 | cwd: '', 13 | format: 'json', 14 | }; 15 | 16 | let formatter = new JsonFormatter(options); 17 | 18 | const result = stripAnsi(formatter.format(logParser)); 19 | let formattedLog = JSON.parse(result); 20 | 21 | expect(formattedLog).toBeValidSarifLog(); 22 | expect(formattedLog).toStrictEqual(log); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/cli/tests/formatters/sonarqube-test.ts: -------------------------------------------------------------------------------- 1 | import { isAbsolute } from 'path'; 2 | import { CheckupLogParser, FormatterOptions } from '@checkup/core'; 3 | import { createTmpDir } from '@checkup/test-helpers'; 4 | import stripAnsi from 'strip-ansi'; 5 | import SonarQubeFormatter from '../../src/formatters/sonarqube.js'; 6 | import { getFixture } from '../__utils__/get-fixture.js'; 7 | 8 | describe('SonarQube formatter', () => { 9 | let tmpDir: string; 10 | 11 | beforeEach(() => { 12 | tmpDir = createTmpDir(); 13 | }); 14 | 15 | it('can generate string from format', async () => { 16 | const log = getFixture('checkup-result.sarif'); 17 | const logParser = new CheckupLogParser(log); 18 | const options: FormatterOptions = { 19 | cwd: tmpDir, 20 | format: 'json', 21 | }; 22 | 23 | let formatter = new SonarQubeFormatter(options); 24 | const result = stripAnsi(formatter.format(logParser)); 25 | const formatted = JSON.parse(result); 26 | const firstIssue = formatted.issues[0]; 27 | 28 | expect(formatted.issues).toHaveLength(5016); 29 | expect(firstIssue.engineId).toEqual('checkup'); 30 | expect(isAbsolute(firstIssue.primaryLocation.filePath)).toEqual(true); 31 | expect(firstIssue.severity).toEqual('INFO'); 32 | expect(firstIssue.type).toEqual('CODE_SMELL'); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/cli/tests/formatters/summary-test.ts: -------------------------------------------------------------------------------- 1 | import { CheckupLogParser, FormatterOptions } from '@checkup/core'; 2 | import { createTmpDir } from '@checkup/test-helpers'; 3 | import stripAnsi from 'strip-ansi'; 4 | import SummaryFormatter from '../../src/formatters/summary.js'; 5 | import { getFixture } from '../__utils__/get-fixture.js'; 6 | 7 | describe('Summary formatter', () => { 8 | let tmpDir: string; 9 | 10 | beforeEach(() => { 11 | tmpDir = createTmpDir(); 12 | process.env.TESTING_TMP_DIR = tmpDir; 13 | }); 14 | 15 | afterEach(() => { 16 | process.env.TESTING_TMP_DIR = undefined; 17 | }); 18 | 19 | it('can generate string from format', async () => { 20 | const log = getFixture('checkup-result.sarif'); 21 | const logParser = new CheckupLogParser(log); 22 | const options: FormatterOptions = { 23 | cwd: '', 24 | format: 'summary', 25 | }; 26 | 27 | let formatter = new SummaryFormatter(options); 28 | 29 | const result = stripAnsi(formatter.format(logParser)); 30 | 31 | expect(result).toContain('Checkup report generated for travis v0.0.1 (1765 files analyzed)'); 32 | expect(result).toContain( 33 | 'This project is 9 years old, with 1470 active days, 6012 commits and 1692 files' 34 | ); 35 | expect(result).toContain('Checkup ran the following task(s):'); 36 | expect(result).toContain('Results have been saved to the following file:'); 37 | expect(result).toContain('checkup v1.0.0-beta.14'); 38 | expect(result).toContain('config 257cda6f6d50eeef891fc6ec8d808bdb'); 39 | }); 40 | 41 | it('should render timing if CHECKUP_TIMING=1', async () => { 42 | process.env.CHECKUP_TIMING = '1'; 43 | 44 | const log = getFixture('checkup-result.sarif'); 45 | const logParser = new CheckupLogParser(log); 46 | const options: FormatterOptions = { 47 | cwd: '', 48 | format: 'summary', 49 | }; 50 | 51 | let formatter = new SummaryFormatter(options); 52 | 53 | const result = formatter.format(logParser); 54 | 55 | delete process.env.CHECKUP_TIMING; 56 | 57 | expect(stripAnsi(result)).toContain('Task Timings'); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /packages/cli/tests/generators/generate-config-test.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { writeFileSync } from 'fs'; 3 | import * as helpers from 'yeoman-test'; 4 | 5 | import { createTmpDir, testRoot } from '@checkup/test-helpers'; 6 | 7 | import ConfigGenerator from '../../src/generators/config'; 8 | 9 | describe('config-init-generator', () => { 10 | it('should write a config', async () => { 11 | let tmp = createTmpDir(); 12 | 13 | const dir = await helpers.run(ConfigGenerator).cd(tmp).withOptions({ path: tmp }); 14 | 15 | expect(testRoot(dir.cwd).file('.checkuprc').contents).toMatchInlineSnapshot(` 16 | "{ 17 | \\"$schema\\": \\"https://raw.githubusercontent.com/checkupjs/checkup/master/packages/core/src/schemas/config-schema.json\\", 18 | \\"excludePaths\\": [], 19 | \\"plugins\\": [], 20 | \\"tasks\\": {} 21 | } 22 | " 23 | `); 24 | }); 25 | 26 | it('should write a config in custom path', async () => { 27 | let tmp = createTmpDir(); 28 | 29 | const dir = await helpers.run(ConfigGenerator).cd(tmp).withOptions({ path: './lib' }); 30 | 31 | expect(testRoot(join(dir.cwd, 'lib')).file('.checkuprc').contents).toMatchInlineSnapshot(` 32 | "{ 33 | \\"$schema\\": \\"https://raw.githubusercontent.com/checkupjs/checkup/master/packages/core/src/schemas/config-schema.json\\", 34 | \\"excludePaths\\": [], 35 | \\"plugins\\": [], 36 | \\"tasks\\": {} 37 | } 38 | " 39 | `); 40 | }); 41 | 42 | it('should error if a checkuprc file is already present', async () => { 43 | let tmp = createTmpDir(); 44 | 45 | writeFileSync(join(tmp, '.checkuprc'), JSON.stringify({})); 46 | 47 | await expect(helpers.run(ConfigGenerator).withOptions({ path: tmp })).rejects.toThrow(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /packages/cli/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "noEmit": true 5 | }, 6 | "references": [{ "path": ".." }] 7 | } 8 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-base.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "jsx": "react", 6 | "rootDir": "src" 7 | }, 8 | "references": [ 9 | { 10 | "path": "../core" 11 | }, 12 | { 13 | "path": "../ui" 14 | }, 15 | { 16 | "path": "../checkup-plugin-ember" 17 | }, 18 | { 19 | "path": "../checkup-plugin-javascript" 20 | }, 21 | { 22 | "path": "../test-helpers" 23 | } 24 | ], 25 | "include": ["src/**/*"] 26 | } 27 | -------------------------------------------------------------------------------- /packages/cli/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ['tests/**/*-test.{ts,tsx}'], 6 | globals: true, 7 | testTimeout: 200_000, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/checkupjs/checkup/f7f715e4c38b826bc069f99a752724f017c4d78a/packages/core/README.md -------------------------------------------------------------------------------- /packages/core/jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | moduleFileExtensions: ['ts', 'js', 'json'], 4 | transform: { '\\.ts$': 'ts-jest' }, 5 | coverageReporters: ['lcov', 'text-summary'], 6 | // collectCoverage: !!process.env.CI, 7 | collectCoverageFrom: ['src/**/*.ts'], 8 | coveragePathIgnorePatterns: ['/templates/'], 9 | coverageThreshold: { 10 | global: { 11 | branches: 100, 12 | functions: 100, 13 | lines: 100, 14 | statements: 100, 15 | }, 16 | }, 17 | setupFilesAfterEnv: ['/jest.setup.ts'], 18 | snapshotFormat: { 19 | printBasicPrototype: false, 20 | }, 21 | testPathIgnorePatterns: ['/__fixtures__/', '/__utils__/'], 22 | extensionsToTreatAsEsm: ['.ts'], 23 | globals: { 24 | 'ts-jest': { 25 | useESM: true, 26 | }, 27 | }, 28 | moduleNameMapper: { 29 | '^(\\.{1,2}/.*)\\.js$': '$1', 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /packages/core/jest.setup.ts: -------------------------------------------------------------------------------- 1 | import '@microsoft/jest-sarif'; 2 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@checkup/core", 3 | "version": "3.0.1", 4 | "description": "Checkup's core library", 5 | "homepage": "https://github.com/checkupjs/checkup", 6 | "bugs": "https://github.com/checkupjs/checkup/issues", 7 | "repository": "https://github.com/checkupjs/checkup", 8 | "license": "MIT", 9 | "author": "Steve Calvert ", 10 | "type": "module", 11 | "main": "lib/index.js", 12 | "types": "lib/index.d.ts", 13 | "files": [ 14 | "/lib" 15 | ], 16 | "scripts": { 17 | "build": "tsc --build && yarn build:copy-schema", 18 | "build:copy-schema": "cp -r ./src/schemas ./lib/schemas", 19 | "test": "vitest run" 20 | }, 21 | "dependencies": { 22 | "@babel/eslint-parser": "^7.17.0", 23 | "@babel/parser": "^7.18.0", 24 | "@glimmer/syntax": "^0.84.2", 25 | "@types/debug": "^4.1.7", 26 | "@types/micromatch": "^4.0.1", 27 | "@types/sarif": "^2.1.4", 28 | "@types/stylelint": "^9.10.1", 29 | "ajv": "^6.12.6", 30 | "ast-types": "^0.14.2", 31 | "chalk": "^4.0.0", 32 | "ci-info": "^3.1.1", 33 | "clean-stack": "^4.2.0", 34 | "cli-table3": "^0.6.0", 35 | "date-and-time": "^1.0.0", 36 | "debug": "^4.3.1", 37 | "deepmerge": "^4.2.2", 38 | "dirname-filename-esm": "^1.1.1", 39 | "ember-template-lint": "^5.0.0", 40 | "ember-template-recast": "^6.1.3", 41 | "es-main": "^1.2.0", 42 | "eslint": "^8.22.0", 43 | "fs-extra": "^9.1.0", 44 | "globby": "^13.1.2", 45 | "is-glob": "^4.0.1", 46 | "json-stable-stringify": "^1.0.1", 47 | "lodash": "^4.17.21", 48 | "lodash.merge": "^4.6.2", 49 | "micromatch": "^4.0.8", 50 | "node-fetch": "^3.2.10", 51 | "npm-check": "^6.0.1", 52 | "object-path": "^0.11.8", 53 | "ow": "^0.28.1", 54 | "pkg-up": "^3.1.0", 55 | "promise.hash.helper": "^1.0.8", 56 | "recast": "^0.20.5", 57 | "resolve": "^1.22.0", 58 | "strip-ansi": "^6.0.0", 59 | "stylelint": "^14.8.5", 60 | "tmp": "^0.2.1", 61 | "type-fest": "^1.0.2", 62 | "walk-sync": "^3.0.0", 63 | "wrap-ansi": "^7.0.0", 64 | "yargs-unparser": "^2.0.0" 65 | }, 66 | "devDependencies": { 67 | "@types/chalk": "^2.2.0", 68 | "@types/eslint": "^8.4.6", 69 | "@types/lodash.merge": "^4.6.7", 70 | "@types/object-path": "^0.11.1", 71 | "@types/resolve": "^1.20.2", 72 | "@types/wrap-ansi": "^3.0.0", 73 | "mockdate": "^3.0.5" 74 | }, 75 | "engines": { 76 | "node": ">= 16" 77 | }, 78 | "publishConfig": { 79 | "access": "public" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /packages/core/src/actions/actions-evaluator.ts: -------------------------------------------------------------------------------- 1 | import { TaskConfig, ActionConfig } from '../types/config.js'; 2 | import { parseConfigTuple } from '../config.js'; 3 | import { TaskAction } from '../types/tasks.js'; 4 | 5 | export default class TaskActionsEvaluator { 6 | private actions: TaskAction[] = []; 7 | 8 | add(action: TaskAction) { 9 | this.actions.push(action); 10 | } 11 | 12 | evaluate(config: TaskConfig): TaskAction[] { 13 | let actionConfig: ActionConfig = config.actions ?? {}; 14 | 15 | return this.actions.filter((action: TaskAction) => { 16 | let [enabled, value] = parseConfigTuple<{ threshold: number }>(actionConfig[action.name]); 17 | let threshold = 18 | value && typeof value.threshold === 'number' ? value.threshold : action.defaultThreshold; 19 | 20 | return enabled && action.input >= threshold; 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/core/src/actions/registered-actions.ts: -------------------------------------------------------------------------------- 1 | import { TaskName, TaskActionsEvaluator } from '../types/tasks.js'; 2 | 3 | const registeredActions = new Map(); 4 | 5 | export function getRegisteredActions() { 6 | return registeredActions; 7 | } 8 | 9 | export function registerActions(taskName: TaskName, evaluate: TaskActionsEvaluator) { 10 | registeredActions.set(taskName, evaluate); 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/src/analyzers/ast-analyzer.ts: -------------------------------------------------------------------------------- 1 | type ParserWithOptions = (source: string, parserOptions?: any) => TAst; 2 | type Parser = (source: string) => TAst; 3 | 4 | /** 5 | * A class for generic AST analysis. 6 | * 7 | * @export 8 | * @class AstAnalyzer 9 | * @template TAst 10 | * @template TVisitors 11 | * @template TParse 12 | * @template TTraverse 13 | */ 14 | export default class AstAnalyzer< 15 | TAst, 16 | TVisitors, 17 | TParse extends Parser | ParserWithOptions, 18 | TTraverse extends (ast: TAst, visitors: TVisitors) => any 19 | > { 20 | ast: TAst; 21 | 22 | constructor( 23 | public source: string, 24 | private parser: TParse, 25 | private traverser: TTraverse, 26 | private parserOptions: object = {} 27 | ) { 28 | this.ast = this._parse(source); 29 | } 30 | 31 | private _parse(source: string): TAst { 32 | return this.parser(source, this.parserOptions); 33 | } 34 | 35 | analyze(visitors: TVisitors) { 36 | this.traverser(this.ast, visitors); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/core/src/analyzers/ember-template-lint-analyzer.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | 3 | import { extname } from 'path'; 4 | 5 | import TemplateLinter from 'ember-template-lint'; 6 | import { 7 | TemplateLintConfig, 8 | TemplateLintMessage, 9 | TemplateLintReport, 10 | TemplateLintResult, 11 | } from '../types/ember-template-lint'; 12 | import { TaskConfig } from '../types/config.js'; 13 | import { mergeLintConfig } from '../utils/merge-lint-config.js'; 14 | 15 | /** 16 | * A class for analyzing .hbs files using ember-template-lint 17 | * 18 | * @export 19 | * @class EmberTemplateLintAnalyzer 20 | */ 21 | export default class EmberTemplateLintAnalyzer { 22 | engine: typeof TemplateLinter; 23 | 24 | constructor(config: TemplateLintConfig, taskConfig?: TaskConfig) { 25 | if (taskConfig && taskConfig.emberTemplateLintConfig) { 26 | config = mergeLintConfig(config, taskConfig.emberTemplateLintConfig); 27 | } 28 | 29 | this.engine = new TemplateLinter({ 30 | config, 31 | }); 32 | } 33 | 34 | async loadConfig() { 35 | await this.engine.loadConfig(); 36 | } 37 | 38 | async analyze(paths: string[]): Promise { 39 | let sources = paths.map((path) => ({ 40 | path, 41 | template: fs.readFileSync(path, { encoding: 'utf8' }), 42 | })); 43 | 44 | let results: TemplateLintResult[] = []; 45 | 46 | for (let { path, template } of sources) { 47 | let messages: TemplateLintMessage[] = await this.engine.verify({ 48 | source: template, 49 | filePath: path, 50 | moduleId: this.removeExt(path), // this can be removed when https://github.com/ember-template-lint/ember-template-lint/issues/2128 is resolved 51 | }); 52 | 53 | results.push({ 54 | messages, 55 | errorCount: messages.length, 56 | filePath: path, 57 | source: template, 58 | }); 59 | } 60 | 61 | let errorCount = results 62 | .map(({ errorCount }) => errorCount) 63 | .reduce((totalErrorCount, currentErrorCount) => totalErrorCount + currentErrorCount, 0); 64 | 65 | return { 66 | errorCount, 67 | results, 68 | }; 69 | } 70 | 71 | // copied from ember-template-lint https://github.com/ember-template-lint/ember-template-lint/blob/master/bin/ember-template-lint.js 72 | removeExt(filePath: string) { 73 | return filePath.slice(0, -extname(filePath).length); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/core/src/analyzers/eslint-analyzer.ts: -------------------------------------------------------------------------------- 1 | import { ESLint, Linter, Rule } from 'eslint'; 2 | import { mergeLintConfig } from '../utils/merge-lint-config.js'; 3 | import { TaskConfig } from '../types/config.js'; 4 | 5 | /** 6 | * A class for analyzing JavaScript/TypeScript files using eslint. 7 | * 8 | * @export 9 | * @class ESLintAnalyzer 10 | */ 11 | export default class ESLintAnalyzer { 12 | engine: ESLint; 13 | rules: Map; 14 | 15 | constructor(options: ESLint.Options, taskConfig?: TaskConfig) { 16 | if (taskConfig && taskConfig.eslintConfig) { 17 | options.baseConfig = mergeLintConfig>( 18 | options.baseConfig!, 19 | taskConfig.eslintConfig 20 | ); 21 | } 22 | 23 | this.engine = new ESLint(options); 24 | this.rules = new Linter().getRules(); 25 | } 26 | 27 | async analyze(paths: string[]): Promise { 28 | return this.engine.lintFiles(paths); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/core/src/analyzers/handlebars-analyzer.ts: -------------------------------------------------------------------------------- 1 | import { AST, NodeVisitor, parse, traverse } from 'ember-template-recast'; 2 | import AstAnalyzer from './ast-analyzer.js'; 3 | 4 | /** 5 | * A class for analyzing .hbs files using ember-template-recast. 6 | * 7 | * @export 8 | * @class HandlebarsAnalyzer 9 | * @extends {AstAnalyzer} 10 | */ 11 | export default class HandlebarsAnalyzer extends AstAnalyzer< 12 | AST.Template, 13 | NodeVisitor, 14 | typeof parse, 15 | typeof traverse 16 | > { 17 | constructor(source: string) { 18 | super(source, parse, traverse); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/core/src/analyzers/javascript-analyzer.ts: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'module'; 2 | import * as t from '@babel/types'; 3 | import { parse, visit } from 'recast'; 4 | import { Visitor } from 'ast-types'; 5 | import AstAnalyzer from './ast-analyzer.js'; 6 | 7 | const require = createRequire(import.meta.url); 8 | 9 | /** 10 | * A class for analyzing JavaScript files. 11 | * 12 | * @export 13 | * @class JavaScriptAnalyzer 14 | * @extends {AstAnalyzer} 15 | */ 16 | export default class JavaScriptAnalyzer extends AstAnalyzer< 17 | t.File, 18 | Visitor, 19 | typeof parse, 20 | typeof visit 21 | > { 22 | constructor(source: string) { 23 | super(source, parse, visit, { 24 | parser: require('recast/parsers/babel'), 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/src/analyzers/json-analyzer.ts: -------------------------------------------------------------------------------- 1 | import JavaScriptAnalyzer from './javascript-analyzer.js'; 2 | 3 | /** 4 | * A class for analyzing JSON files. 5 | * 6 | * @export 7 | * @class JsonAnalyzer 8 | * @extends {JavaScriptAnalyzer} 9 | */ 10 | export default class JsonAnalyzer extends JavaScriptAnalyzer { 11 | constructor(source: string) { 12 | try { 13 | JSON.parse(source); 14 | } catch { 15 | throw new Error('The JsonAnalyzer `source` parameter should be a valid JSON string'); 16 | } 17 | 18 | // In order to process JSON, we need to convert it to a module, 19 | // as babel cannot parse JSON natively. 20 | let jsonSource = `module.exports = ${source}`; 21 | 22 | super(jsonSource); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/core/src/analyzers/stylelint-analyzer.ts: -------------------------------------------------------------------------------- 1 | import styleLint from 'stylelint'; 2 | import { mergeLintConfig } from '../utils/merge-lint-config.js'; 3 | import { TaskConfig } from '../types/config.js'; 4 | 5 | const { lint } = styleLint; 6 | 7 | /** 8 | * A class for analyzing .css files using stylelint. 9 | * 10 | * @export 11 | * @class StyleLintAnalyzer 12 | */ 13 | export default class StyleLintAnalyzer { 14 | config: Partial; 15 | 16 | constructor(config: Partial, taskConfig?: TaskConfig) { 17 | if (taskConfig && taskConfig.stylelintConfig) { 18 | config = mergeLintConfig(config, taskConfig.stylelintConfig) as styleLint.LinterOptions; 19 | } 20 | 21 | this.config = config; 22 | } 23 | 24 | async analyze(paths: string[]): Promise { 25 | this.config.files = paths; 26 | 27 | return lint(this.config); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/core/src/analyzers/typescript-analyzer.ts: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'module'; 2 | import * as recast from 'recast'; 3 | import { File } from '@babel/types'; 4 | import traverse, { TraverseOptions } from '@babel/traverse'; 5 | import AstAnalyzer from './ast-analyzer.js'; 6 | 7 | const require = createRequire(import.meta.url); 8 | 9 | /** 10 | * A class for analyzing TypeScript files. 11 | * 12 | * @export 13 | * @class TypeScriptAnalyzer 14 | * @extends {AstAnalyzer} 15 | */ 16 | export default class TypeScriptAnalyzer extends AstAnalyzer< 17 | File, 18 | TraverseOptions, 19 | typeof recast.parse, 20 | typeof traverse 21 | > { 22 | constructor(source: string) { 23 | super(source, recast.parse, (traverse).default, { 24 | parser: require('recast/parsers/typescript'), 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/src/ast/ast-transformer.ts: -------------------------------------------------------------------------------- 1 | import * as recast from 'recast'; 2 | import { File } from '@babel/types'; 3 | import traverse, { TraverseOptions } from '@babel/traverse'; 4 | 5 | import AstAnalyzer from '../analyzers/ast-analyzer.js'; 6 | 7 | type RecastParse = typeof recast.parse; 8 | type BabelTraverse = typeof traverse; 9 | 10 | /** 11 | * A class used for code generation. 12 | * 13 | * @export 14 | * @class AstTransformer 15 | * @extends {AstAnalyzer} 16 | */ 17 | export default class AstTransformer extends AstAnalyzer< 18 | File, 19 | TraverseOptions, 20 | RecastParse, 21 | BabelTraverse 22 | > { 23 | generate(): string { 24 | return recast.print(this.ast, { quote: 'single', wrapColumn: 100 }).code; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/src/data/formatters.ts: -------------------------------------------------------------------------------- 1 | export function toPercent(numeratorOrValue: number, denominator?: number): string { 2 | let value: number = 3 | typeof denominator === 'number' ? numeratorOrValue / denominator : numeratorOrValue; 4 | 5 | return `${(value * 100).toFixed(0)}%`; 6 | } 7 | -------------------------------------------------------------------------------- /packages/core/src/data/lint.ts: -------------------------------------------------------------------------------- 1 | import { LintMessage, LintResult } from '../types/analyzers.js'; 2 | import { NormalizedLintResult } from '../types/tasks.js'; 3 | import { trimCwd } from './path.js'; 4 | 5 | export function toLintResult( 6 | message: LintMessage, 7 | cwd: string, 8 | filePath: string, 9 | additionalData: object = {} 10 | ): NormalizedLintResult { 11 | return { 12 | filePath: trimCwd(filePath, cwd), 13 | lintRuleId: getLintRuleId(message), 14 | message: message.message, 15 | severity: message.severity, 16 | line: message.line, 17 | column: message.column, 18 | endLine: 'endLine' in message ? message.endLine! : message.line, 19 | endColumn: 'endColumn' in message ? message.endColumn! : message.column, 20 | ...additionalData, 21 | }; 22 | } 23 | 24 | export function toLintResults(results: LintResult[], cwd: string): NormalizedLintResult[] { 25 | return results.reduce((transformed, lintingResults) => { 26 | const messages = (lintingResults.messages) 27 | .filter( 28 | (lintMessage: LintMessage) => 29 | ('ruleId' in lintMessage && lintMessage.ruleId !== undefined) || 30 | ('rule' in lintMessage && lintMessage.rule !== undefined) 31 | ) 32 | .filter((lintMessage: LintMessage) => { 33 | return ( 34 | !lintMessage.message.includes('Parsing error') && getLintRuleId(lintMessage) !== 'global' 35 | ); 36 | }) 37 | .map((lintMessage: LintMessage) => { 38 | return toLintResult(lintMessage, cwd, lintingResults.filePath); 39 | }); 40 | 41 | transformed.push(...messages); 42 | 43 | return transformed; 44 | }, [] as NormalizedLintResult[]); 45 | } 46 | 47 | export const lintBuilder = { 48 | toLintResult, 49 | toLintResults, 50 | }; 51 | 52 | function getLintRuleId(message: any) { 53 | if (typeof message.ruleId !== 'undefined') { 54 | return message.ruleId; 55 | } else if (typeof message.rule !== 'undefined') { 56 | return message.rule; 57 | } 58 | return ''; 59 | } 60 | -------------------------------------------------------------------------------- /packages/core/src/data/path.ts: -------------------------------------------------------------------------------- 1 | export function trimCwd(path: string, cwd: string) { 2 | return path.replace(`${cwd}/`, ''); 3 | } 4 | 5 | export function trimAllCwd(paths: string[], cwd: string) { 6 | return paths.map((path) => trimCwd(path, cwd)); 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/src/errors/checkup-error.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { dirname } from 'dirname-filename-esm'; 3 | import wrap from 'wrap-ansi'; 4 | import ci from 'ci-info'; 5 | import chalk from 'chalk'; 6 | import fs from 'fs-extra'; 7 | import stripAnsi from 'strip-ansi'; 8 | import clean from 'clean-stack'; 9 | import { todayFormat } from '../today-format.js'; 10 | import { ErrorDetails, ErrorDetailOptions, ErrorKind, ERROR_BY_KIND } from './error-kind.js'; 11 | 12 | /** 13 | * A custom Error class that outputs additional information by ErrorKind. 14 | * 15 | * @export 16 | * @class CheckupError 17 | * @extends {Error} 18 | */ 19 | export default class CheckupError extends Error { 20 | private details: ErrorDetails; 21 | private options: ErrorDetailOptions; 22 | 23 | constructor(kind: ErrorKind, options: ErrorDetailOptions = {}) { 24 | let details = ERROR_BY_KIND[kind]; 25 | if (!details) { 26 | throw new Error(`ErrorKind provided missing from ERROR_BY_KIND map: ${ErrorKind}`); 27 | } 28 | 29 | super(details.message(options)); 30 | 31 | this.name = 'CheckupError'; 32 | this.details = details; 33 | this.options = options; 34 | 35 | // prevent this class from appearing in the stack 36 | Error.captureStackTrace(this, CheckupError); 37 | } 38 | 39 | render(): string { 40 | process.exitCode = this.details.errorCode; 41 | 42 | let details: string[] = []; 43 | 44 | details.push( 45 | `${chalk.red('Checkup Error')}: ${this.message}`, 46 | `${this.details.callToAction(this.options)}` 47 | ); 48 | 49 | if (ci.isCI) { 50 | return details.join('\n'); 51 | } else { 52 | let logFilePath = this.writeErrorLog(details); 53 | 54 | details.push(`Error details written to ${logFilePath}`); 55 | return wrap(details.join('\n'), 80, { trim: false, hard: true }); 56 | } 57 | } 58 | 59 | writeErrorLog(details: string[]) { 60 | let logFileName = `checkup-error-${todayFormat()}.log`; 61 | let logPath = join(process.cwd(), '.checkup'); 62 | let logFilePath = join(logPath, logFileName); 63 | let logOutput: string[] = []; 64 | let version = fs.readJsonSync(join(dirname(import.meta), '../../package.json')).version; 65 | 66 | logOutput.push( 67 | `Checkup v${version}`, 68 | '', 69 | stripAnsi(details.join('\n')), 70 | '', 71 | clean(this.stack || 'No stack available') 72 | ); 73 | 74 | fs.ensureDirSync(logPath); 75 | 76 | fs.writeFileSync(logFilePath, logOutput.join('\n'), { encoding: 'utf-8' }); 77 | 78 | return logFilePath; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/core/src/errors/task-error.ts: -------------------------------------------------------------------------------- 1 | import CheckupError from './checkup-error.js'; 2 | import { ErrorKind, ErrorDetailOptions } from './error-kind.js'; 3 | 4 | export type TaskErrorDetailOptions = ErrorDetailOptions & { taskName: string }; 5 | 6 | /** 7 | * A custom error class that outputs Task specific error information. 8 | * 9 | * @export 10 | * @class TaskError 11 | * @extends {CheckupError} 12 | */ 13 | export default class TaskError extends CheckupError { 14 | constructor(options: TaskErrorDetailOptions) { 15 | super(ErrorKind.TaskError, options); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/src/schemas/config-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "definitions": { 4 | "taskConfig": { 5 | "type": "object", 6 | "patternProperties": { 7 | "^[a-zA-Z0-9-_]*/?[a-zA-Z0-9-_]*$": { 8 | "oneOf": [ 9 | { "type": "string", "enum": ["on", "off"] }, 10 | { 11 | "type": "array", 12 | "minItems": 2, 13 | "items": [ 14 | { 15 | "type": "string", 16 | "enum": ["on", "off"] 17 | }, 18 | { 19 | "type": "object" 20 | } 21 | ], 22 | "additionalItems": false 23 | } 24 | ] 25 | } 26 | } 27 | } 28 | }, 29 | "properties": { 30 | "excludePaths": { 31 | "items": { 32 | "type": "string" 33 | }, 34 | "type": "array" 35 | }, 36 | "plugins": { 37 | "items": { 38 | "type": "string" 39 | }, 40 | "type": "array" 41 | }, 42 | "tasks": { 43 | "$ref": "#/definitions/taskConfig" 44 | } 45 | }, 46 | "type": "object", 47 | "required": ["plugins", "tasks"] 48 | } 49 | -------------------------------------------------------------------------------- /packages/core/src/today-format.ts: -------------------------------------------------------------------------------- 1 | import date from 'date-and-time'; 2 | 3 | export const todayFormat = () => date.format(new Date(), 'YYYY-MM-DD-HH_mm_ss'); 4 | -------------------------------------------------------------------------------- /packages/core/src/types/analyzers.ts: -------------------------------------------------------------------------------- 1 | import { ESLint, Linter } from 'eslint'; 2 | import EmberTemplateLinter from 'ember-template-lint'; 3 | import { TemplateLintMessage, TemplateLintResult } from './ember-template-lint.js'; 4 | 5 | export type AnalyzerReport = any; 6 | export interface LintAnalyzer { 7 | analyze(paths: string[]): Promise; 8 | } 9 | 10 | export type LintMessage = ESLintMessage | TemplateLintMessage; 11 | export type LintResult = ESLint.LintResult | TemplateLintResult; 12 | 13 | export type TemplateLinter = typeof EmberTemplateLinter; 14 | 15 | export type ESLintOptions = ESLint.Options; 16 | export type ESLintMessage = Linter.LintMessage; 17 | -------------------------------------------------------------------------------- /packages/core/src/types/checkup-log.ts: -------------------------------------------------------------------------------- 1 | import { PackageJson, SetRequired } from 'type-fest'; 2 | import { Run, Result, ReportingDescriptor } from 'sarif'; 3 | import { RunOptions } from '../types/cli.js'; 4 | import { TaskListError, TaskAction, Task } from '../types/tasks.js'; 5 | import { CheckupConfig } from '../types/config.js'; 6 | import { FilePathArray } from '../utils/file-path-array.js'; 7 | 8 | export type RequiredRun = SetRequired; 9 | export type RequiredResult = SetRequired; 10 | 11 | export interface CheckupLogBuilderArgs { 12 | analyzedPackageJson: PackageJson; 13 | options: RunOptions; 14 | paths?: FilePathArray; 15 | } 16 | 17 | export type RuleResults = { 18 | rule: ReportingDescriptor; 19 | results: Result[]; 20 | }; 21 | 22 | export interface AnnotationArgs { 23 | config: CheckupConfig; 24 | executedTasks: Task[]; 25 | actions: TaskAction[]; 26 | errors: TaskListError[]; 27 | timings: Record; 28 | } 29 | -------------------------------------------------------------------------------- /packages/core/src/types/checkup-result.ts: -------------------------------------------------------------------------------- 1 | import { RunOptions } from './cli.js'; 2 | import { CheckupConfig } from './config.js'; 3 | 4 | export type IndexableObject = { [key: string]: any }; 5 | 6 | export type DataSummary = { 7 | values: Record; 8 | dataKey: string; 9 | total: number; 10 | units?: string; 11 | }; 12 | 13 | export type RepositoryInfo = { 14 | totalCommits: number; 15 | totalFiles: number; 16 | age: string; 17 | activeDays: string; 18 | }; 19 | 20 | export interface CheckupMetadata { 21 | project: { 22 | name: string; 23 | version: string; 24 | repository: RepositoryInfo; 25 | }; 26 | cli: { 27 | schema: number; 28 | configHash: string; 29 | config: CheckupConfig; 30 | version: string; 31 | options: RunOptions; 32 | }; 33 | analyzedFiles: string[]; 34 | analyzedFilesCount: number; 35 | } 36 | -------------------------------------------------------------------------------- /packages/core/src/types/cli.ts: -------------------------------------------------------------------------------- 1 | import CheckupLogParser from '../data/checkup-log-parser.js'; 2 | import { Task, TaskName, OutputFormat } from './tasks'; 3 | import { CheckupConfig } from './config.js'; 4 | 5 | export type RunOptions = { 6 | cwd: string; 7 | config?: CheckupConfig; 8 | configPath?: string; 9 | categories?: string[]; 10 | excludePaths?: string[]; 11 | groups?: string[]; 12 | listTasks?: boolean; 13 | paths?: string[]; 14 | tasks?: string[]; 15 | pluginBaseDir?: string; 16 | }; 17 | 18 | export interface RegisterableTaskList { 19 | timings: Record; 20 | registerTask(task: Task): void; 21 | } 22 | 23 | export interface Formatter { 24 | shouldWrite: boolean; 25 | format(logParser: CheckupLogParser): string; 26 | } 27 | 28 | export interface FormatterCtor { 29 | new (options: FormatterOptions): Formatter; 30 | } 31 | 32 | export interface FormatterOptions { 33 | cwd: string; 34 | format: OutputFormat | string; 35 | outputFile?: string; 36 | } 37 | -------------------------------------------------------------------------------- /packages/core/src/types/config.ts: -------------------------------------------------------------------------------- 1 | export type ConfigValue = 'on' | 'off' | ['on' | 'off', T]; 2 | export type ActionConfig = Record>; 3 | export type TaskConfig = { actions?: ActionConfig; [key: string]: any }; 4 | export type CheckupConfig = { 5 | $schema: string; 6 | excludePaths: string[]; 7 | plugins: string[]; 8 | tasks: Record>; 9 | }; 10 | -------------------------------------------------------------------------------- /packages/core/src/types/dependency.ts: -------------------------------------------------------------------------------- 1 | export interface DependencyInfo { 2 | packageName: string; 3 | packageVersion: string; 4 | type: 'dependency' | 'devDependency'; 5 | startLine: number; 6 | startColumn: number; 7 | endLine: number; 8 | endColumn: number; 9 | } 10 | 11 | export type Dependency = DependencyInfo & { 12 | latestVersion: string; 13 | installedVersion: string; 14 | wantedVersion: string; 15 | semverBump: string; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/core/src/types/ember-template-lint.ts: -------------------------------------------------------------------------------- 1 | import type Prepend from 'eslint'; 2 | 3 | type Severity = 0 | 1 | 2; 4 | type RuleLevel = Severity | 'off' | 'warn' | 'error'; 5 | type RuleLevelAndOptions = Prepend, RuleLevel>; 6 | type TemplateLintRuleDefinition = RuleLevel | RuleLevelAndOptions; 7 | type TemplateLintPending = string | TemplateLintPendingWithExclusions; 8 | type TemplateLintPlugin = string | TemplateLintPluginObject; 9 | 10 | interface TemplateLintPluginObject { 11 | name: string; 12 | plugins: TemplateLintPlugin[]; 13 | rules: { 14 | [ruleName: string]: TemplateLintRuleDefinition; 15 | }; 16 | configurations: { 17 | [configuration: string]: TemplateLintConfig; 18 | }; 19 | } 20 | 21 | interface TemplateLintOverride { 22 | files: string[]; 23 | rule: { 24 | [ruleName: string]: RuleLevel | RuleLevelAndOptions; 25 | }; 26 | } 27 | 28 | interface TemplateLintPendingWithExclusions { 29 | moduleId: string; 30 | only: string[]; 31 | } 32 | 33 | export interface TemplateLintMessage { 34 | rule: string; 35 | severity: Severity; 36 | moduleId: string; 37 | message: string; 38 | line: number; 39 | column: number; 40 | source: string; 41 | } 42 | 43 | export interface TemplateLintResult { 44 | errorCount: number; 45 | filePath: string; 46 | messages: TemplateLintMessage[]; 47 | source: string; 48 | } 49 | 50 | export interface TemplateLintReport { 51 | results: TemplateLintResult[]; 52 | errorCount: number; 53 | } 54 | 55 | export interface TemplateLintConfig { 56 | extends?: string | string[]; 57 | rules?: { 58 | [ruleName: string]: TemplateLintRuleDefinition; 59 | }; 60 | pending?: TemplateLintPending[]; 61 | ignore?: string[]; 62 | plugins?: TemplateLintPlugin[]; 63 | overrides?: TemplateLintOverride[]; 64 | } 65 | -------------------------------------------------------------------------------- /packages/core/src/utils/buffered-writer.ts: -------------------------------------------------------------------------------- 1 | import stripAnsi from 'strip-ansi'; 2 | import BaseOutputWriter from './base-output-writer.js'; 3 | 4 | export default class BufferedWriter extends BaseOutputWriter { 5 | buffer: string = ''; 6 | 7 | get escapedBuffer() { 8 | return stripAnsi(this.buffer); 9 | } 10 | 11 | write(content: string): void { 12 | this.buffer += content; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/src/utils/console-writer.ts: -------------------------------------------------------------------------------- 1 | import BaseOutputWriter from './base-output-writer.js'; 2 | export default class ConsoleWriter extends BaseOutputWriter { 3 | write(content: string): void { 4 | process.stdout.write(content); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/core/src/utils/exec.ts: -------------------------------------------------------------------------------- 1 | import util from 'util'; 2 | import { exec as cpExec } from 'child_process'; 3 | const execAsPromise = util.promisify(cpExec); 4 | 5 | /** 6 | * @param {string} cmd - The command to run 7 | * @param {any} options - Options passed to the command 8 | * @param {string|number} defaultValue - Default value returned if the command returns no value 9 | * @param {Function} toType - A function used to convert a result to a specific type 10 | * @returns {string|number} - The result of the command 11 | */ 12 | export async function exec( 13 | cmd: string, 14 | options: any, 15 | defaultValue: string | number, 16 | toType: Function = String 17 | ) { 18 | const { stdout } = await execAsPromise(cmd, options); 19 | 20 | return toType(stdout.toString().trim()) || defaultValue; 21 | } 22 | -------------------------------------------------------------------------------- /packages/core/src/utils/extract-stack.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Extracted from https://github.com/sindresorhus/extract-stack due to that package 3 | * using ESM. We're not using ESM yet, and using it caused issues with jest's test env. 4 | */ 5 | const stackRegex = /(?:\n {4}at .*)+/; 6 | 7 | const extractStack = (error: Error | string) => { 8 | const stack = error instanceof Error ? error.stack : error; 9 | 10 | if (!stack) { 11 | return ''; 12 | } 13 | 14 | const match = stack.match(stackRegex); 15 | 16 | if (!match) { 17 | return ''; 18 | } 19 | 20 | return match[0].slice(1); 21 | }; 22 | 23 | extractStack.lines = (stack: Error) => 24 | extractStack(stack) 25 | .replace(/^ {4}at /gm, '') 26 | .split('\n'); 27 | 28 | export default extractStack; 29 | -------------------------------------------------------------------------------- /packages/core/src/utils/file-path-array.ts: -------------------------------------------------------------------------------- 1 | import { extname } from 'path'; 2 | 3 | import micromatch from 'micromatch'; 4 | 5 | export class FilePathArray extends Array { 6 | filterByGlob(glob: string | string[]) { 7 | return micromatch(this, glob); 8 | } 9 | 10 | get extensions() { 11 | return [ 12 | ...this.reduce((uniqueExtensions: Set, path: string) => { 13 | uniqueExtensions.add(extname(path).replace('.', '')); 14 | return uniqueExtensions; 15 | }, new Set()), 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/core/src/utils/file-writer.ts: -------------------------------------------------------------------------------- 1 | import { isAbsolute, dirname, resolve, extname } from 'path'; 2 | import { Log } from 'sarif'; 3 | import fs from 'fs-extra'; 4 | import stripAnsi from 'strip-ansi'; 5 | import { todayFormat } from '../today-format.js'; 6 | 7 | const { existsSync, mkdirpSync, writeFileSync, writeJsonSync } = fs; 8 | 9 | export const DEFAULT_OUTPUT_FILENAME = `checkup-report-${todayFormat()}`; 10 | 11 | /** 12 | * A utility function to write results to an output file. If no `outputFile` is given, 13 | * it uses a default output file name in the format "checkup-report-YYYY-MM-DD-HH_mm_ss". 14 | * If result is a string the extension is .txt, otherwise .sarif is used. 15 | * 16 | * @param {(Log | string)} result - The result to be output, either a SARIF log or a string. 17 | * @param {string} cwd - The current working directory to write to. 18 | * @param {string} [outputFile=DEFAULT_OUTPUT_FILENAME] - The output filename format. 19 | * @return {*} {string} 20 | */ 21 | export function writeResultsToFile( 22 | result: Log | string, 23 | cwd: string, 24 | outputFile: string = DEFAULT_OUTPUT_FILENAME 25 | ): string { 26 | let fileType: 'sarif' | 'txt' = typeof result === 'string' ? 'txt' : 'sarif'; 27 | let outputPath = getOutputPath(cwd, outputFile, fileType); 28 | 29 | if (fileType === 'txt') { 30 | writeFileSync(outputPath, stripAnsi(result.toString())); 31 | } else { 32 | writeJsonSync(outputPath, result); 33 | } 34 | 35 | return outputPath; 36 | } 37 | 38 | export function getOutputPath(cwd: string = '', outputFile: string, fileType: 'sarif' | 'txt') { 39 | let outputPath = isAbsolute(outputFile) 40 | ? outputFile 41 | : resolve( 42 | cwd, 43 | outputFile 44 | ? // eslint-disable-next-line unicorn/no-nested-ternary 45 | extname(outputFile) 46 | ? outputFile 47 | : `${outputFile}.${fileType}` 48 | : `${DEFAULT_OUTPUT_FILENAME}.${fileType}` 49 | ); 50 | 51 | let dir = dirname(outputPath); 52 | 53 | if (!existsSync(dir)) { 54 | mkdirpSync(dir); 55 | } 56 | 57 | return outputPath; 58 | } 59 | -------------------------------------------------------------------------------- /packages/core/src/utils/get-package-json.ts: -------------------------------------------------------------------------------- 1 | import { resolve, join } from 'path'; 2 | import fs from 'fs-extra'; 3 | import { PackageJson } from 'type-fest'; 4 | import { isErrnoException } from './type-guards.js'; 5 | 6 | /** 7 | * Gets the package.json source 8 | * 9 | * @param {string} baseDir - The base directory 10 | * @param {string} [pathName='package.json'] - The path to the package.json file 11 | * @returns {string} - The package.json source 12 | */ 13 | export function getPackageJsonSource(baseDir: string, pathName: string = 'package.json'): string { 14 | let source: string = ''; 15 | let packageJsonPath = join(resolve(baseDir), pathName); 16 | 17 | try { 18 | source = fs.readFileSync(packageJsonPath, { encoding: 'utf-8' }); 19 | } catch (error: unknown) { 20 | if (isErrnoException(error) && error.code === 'ENOENT') { 21 | throw new Error( 22 | `The ${resolve( 23 | baseDir 24 | )} directory found through the 'cwd' option does not contain a package.json file. You must run checkup in a directory with a package.json file.` 25 | ); 26 | } 27 | } 28 | 29 | return source; 30 | } 31 | 32 | /** 33 | * Gets the package.json file as an object 34 | * 35 | * @param {string} baseDir - The base directory 36 | * @param {string} [pathName='package.json'] - The path to the package.json file 37 | * @returns {PackageJson} - An object representing the package.json contents 38 | */ 39 | export function getPackageJson(baseDir: string, pathName: string = 'package.json'): PackageJson { 40 | return JSON.parse(getPackageJsonSource(baseDir, pathName)); 41 | } 42 | -------------------------------------------------------------------------------- /packages/core/src/utils/get-version.ts: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'module'; 2 | import { join } from 'path'; 3 | 4 | const require = createRequire(import.meta.url); 5 | /** 6 | * Gets the version from the package.json file. 7 | * 8 | * @param {string} [cwd] - The optional current working directory 9 | * @param {string} [fakeVersion='0.0.0'] - An optional fake version (used for testing) 10 | * @returns {string} - A version number string 11 | */ 12 | export function getVersion(cwd?: string, fakeVersion: string = '0.0.0') { 13 | if (process.env.VITEST !== undefined) { 14 | return fakeVersion; 15 | } 16 | 17 | let packagePath = join(cwd ?? '../..', 'package.json'); 18 | 19 | return require(packagePath).version; 20 | } 21 | -------------------------------------------------------------------------------- /packages/core/src/utils/merge-lint-config.ts: -------------------------------------------------------------------------------- 1 | import deepmerge from 'deepmerge'; 2 | 3 | /** 4 | * Merges a task-specific lint configuration into the Task's lint configuration. 5 | * 6 | * @param {(TemplateLintConfig | CLIEngine.Options)} config - The linting configuration to merge into. 7 | * @param {Record} taskLintConfig - The task's specific lint configuration. 8 | * @returns {*} - The combined configs 9 | */ 10 | export function mergeLintConfig(config: T, taskLintConfig: Record) { 11 | let combinedConfigs = deepmerge(config, taskLintConfig); 12 | 13 | if ('rules' in combinedConfigs) { 14 | let rules = combinedConfigs.rules; 15 | let ruleIds = Object.keys(rules); 16 | 17 | ruleIds.forEach((ruleId: string) => { 18 | let ruleConfig = rules[ruleId]; 19 | let firstTuplePart = ''; 20 | let secondTuplePart = {}; 21 | 22 | if (Array.isArray(ruleConfig) && ruleConfig.length > 2) { 23 | for (const [i, element] of ruleConfig.entries()) { 24 | if (i % 2 === 0) { 25 | firstTuplePart = element; 26 | } else { 27 | secondTuplePart = Object.assign(secondTuplePart, element); 28 | } 29 | } 30 | 31 | rules[ruleId] = [firstTuplePart, secondTuplePart]; 32 | } 33 | }); 34 | } 35 | 36 | return combinedConfigs; 37 | } 38 | -------------------------------------------------------------------------------- /packages/core/src/utils/path.ts: -------------------------------------------------------------------------------- 1 | import { isAbsolute, join, relative } from 'path'; 2 | 3 | export function toAbsolute(baseDir: string, filePath: string) { 4 | let absolutePath = filePath; 5 | 6 | if (!isAbsolute(filePath)) { 7 | absolutePath = join(baseDir, filePath); 8 | } 9 | 10 | return absolutePath; 11 | } 12 | 13 | export function toRelative(baseDir: string, filePath: string) { 14 | let relativePath = filePath; 15 | 16 | if (isAbsolute(relativePath)) { 17 | relativePath = relative(baseDir, filePath); 18 | } 19 | 20 | return relativePath; 21 | } 22 | -------------------------------------------------------------------------------- /packages/core/src/utils/plugin-name.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | import { PackageJson } from 'type-fest'; 4 | import fs from 'fs-extra'; 5 | import { sync } from 'pkg-up'; 6 | 7 | /** 8 | * When inside a checkup plugin, gets the plugin's name. 9 | * 10 | * @param {string} cwd - The current working directory from which to find the plugin's name 11 | * @returns {*} {string} 12 | */ 13 | export function getPluginName(url: string): string { 14 | let cwd = dirname(fileURLToPath(url)); 15 | let packageJsonPath = sync({ cwd }); 16 | let packageJson: PackageJson = fs.readJsonSync(packageJsonPath!); 17 | 18 | if (!packageJson.keywords?.includes('checkup-plugin')) { 19 | throw new Error( 20 | `You tried to retrieve a pluginName from a package.json that isn't from a checkup plugin. 21 | Path: ${packageJsonPath}` 22 | ); 23 | } 24 | 25 | return packageJson.name!; 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/src/utils/repository.ts: -------------------------------------------------------------------------------- 1 | import hash from 'promise.hash.helper'; 2 | import { RepositoryInfo } from '../types/checkup-result'; 3 | import { exec } from './exec.js'; 4 | 5 | const COMMIT_COUNT = "git log --oneline $commit | wc -l | tr -d ' '"; 6 | const FILE_COUNT = "git ls-files | wc -l | tr -d ' '"; 7 | const REPO_AGE = 8 | 'git log --reverse --pretty=oneline --format=" % ar" | head -n 1 | LC_ALL=C sed \'s/ago//\''; 9 | const ACTIVE_DAYS = `git log --pretty='format: %ai' $1 | cut -d ' ' -f 2 | sort -r | uniq | awk ' 10 | { sum += 1 } 11 | END { print sum } 12 | '`; 13 | 14 | /** 15 | * Gets the git repository info 16 | * 17 | * @param {string} baseDir - The base directory from which to gather the repository info 18 | * @returns {*} {Promise} 19 | */ 20 | export function getRepositoryInfo(baseDir: string): Promise { 21 | return hash({ 22 | totalCommits: exec(COMMIT_COUNT, { cwd: baseDir }, 0, Number), 23 | totalFiles: exec(FILE_COUNT, { cwd: baseDir }, 0, Number), 24 | age: exec(REPO_AGE, { cwd: baseDir }, '0 days'), 25 | activeDays: exec(ACTIVE_DAYS, { cwd: baseDir }, '0 days'), 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/src/utils/resolve-module-path.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'dirname-filename-esm'; 2 | import resolve from 'resolve'; 3 | 4 | export function resolveModulePath(moduleName: string, baseDir: string = dirname(import.meta)) { 5 | return resolve.sync(moduleName, { 6 | basedir: baseDir, 7 | extensions: ['.js', '.mjs', '.cjs'], 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/src/utils/type-guards.ts: -------------------------------------------------------------------------------- 1 | function isObject(e: unknown): e is Object { 2 | return e !== null && typeof e === 'object' && !Array.isArray(e); 3 | } 4 | 5 | export function isErrnoException(e: unknown): e is NodeJS.ErrnoException { 6 | return isObject(e) && 'code' in e; 7 | } 8 | 9 | export function isError(e: unknown): e is Error { 10 | return isObject(e) && 'message' in e; 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/tests/__fixtures__/.checkuprc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/checkupjs/checkup/master/packages/core/src/schemas/config-schema.json", 3 | "excludePaths": ["**/foo"], 4 | "plugins": ["@foo/checkup-plugin-bar"], 5 | "tasks": {} 6 | } 7 | -------------------------------------------------------------------------------- /packages/core/tests/__fixtures__/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fake/package", 3 | "description": "The fakest of fake packages", 4 | "version": "1.0.0", 5 | "author": "Zee Faker ", 6 | "dependencies": { 7 | "chalk": "^4.0.0", 8 | "ci-info": "^3.1.1", 9 | "debug": "^4.3.1", 10 | "fs-extra": "^9.1.0" 11 | }, 12 | "devDependencies": { 13 | "globby": "^11.0.1", 14 | "lodash": "^4.17.21", 15 | "micromatch": "^4.0.2", 16 | "resolve": "^1.19.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/core/tests/__fixtures__/simple.css: -------------------------------------------------------------------------------- 1 | a { 2 | font-family: serif, serif; 3 | } 4 | -------------------------------------------------------------------------------- /packages/core/tests/__fixtures__/simple.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 2 | function foo() { 3 | return 'foo'; 4 | } 5 | -------------------------------------------------------------------------------- /packages/core/tests/__utils__/tmp-dir.ts: -------------------------------------------------------------------------------- 1 | import { realpathSync } from 'fs'; 2 | import tmp = require('tmp'); 3 | 4 | export function createTmpDir() { 5 | return realpathSync(tmp.dirSync({ unsafeCleanup: true }).name); 6 | } 7 | -------------------------------------------------------------------------------- /packages/core/tests/analyzers/dependency-analyzer-test.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { dirname } from '@checkup/core'; 3 | import DependencyAnalyzer from '../../src/analyzers/dependency-analyzer'; 4 | 5 | describe('dependency-analyzer', () => { 6 | it('can load dependencies for a package.json', async () => { 7 | let packageJsonPath = resolve(dirname(import.meta), '..', '__fixtures__'); 8 | let analyzer = new DependencyAnalyzer(packageJsonPath); 9 | let result = await analyzer.analyze(); 10 | 11 | expect(result).toContainEqual( 12 | expect.objectContaining({ 13 | installedVersion: expect.any(String), 14 | latestVersion: expect.any(String), 15 | packageName: expect.any(String), 16 | packageVersion: expect.any(String), 17 | semverBump: expect.any(String), 18 | startColumn: expect.any(Number), 19 | startLine: expect.any(Number), 20 | type: expect.any(String), 21 | wantedVersion: expect.any(String), 22 | }) 23 | ); 24 | 25 | expect(result).toHaveLength(8); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/core/tests/analyzers/ember-template-lint-analyzer-test.ts: -------------------------------------------------------------------------------- 1 | import TemplateLinter from 'ember-template-lint'; 2 | import { TemplateLintConfig } from '../../src/types/ember-template-lint'; 3 | import EmberTemplateLintAnalyzer from '../../src/analyzers/ember-template-lint-analyzer'; 4 | 5 | describe('ember-template-lint-analyzer', () => { 6 | it('can create an ember-template-lint analyzer', () => { 7 | let config: TemplateLintConfig = {}; 8 | let analyzer: EmberTemplateLintAnalyzer = new EmberTemplateLintAnalyzer(config); 9 | 10 | analyzer.loadConfig(); 11 | 12 | expect(analyzer.engine).toBeInstanceOf(TemplateLinter); 13 | expect(Object.keys(analyzer.engine.options.config)).toEqual([]); 14 | }); 15 | 16 | it('can create an ember-template-lint analyzer with custom rule configuration', async () => { 17 | let config: TemplateLintConfig = { 18 | rules: { 19 | 'block-indentation': ['error', 6], 20 | }, 21 | }; 22 | 23 | let analyzer: EmberTemplateLintAnalyzer = new EmberTemplateLintAnalyzer(config); 24 | 25 | await analyzer.loadConfig(); 26 | 27 | let optionsForRule = analyzer.engine.config.rules['block-indentation']; 28 | 29 | expect(analyzer.engine).toBeInstanceOf(TemplateLinter); 30 | expect(optionsForRule).toMatchInlineSnapshot(` 31 | { 32 | "config": 6, 33 | "severity": 2, 34 | } 35 | `); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /packages/core/tests/analyzers/eslint-analyzer-test.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { ESLint } from 'eslint'; 3 | import ESLintAnalyzer from '../../src/analyzers/eslint-analyzer'; 4 | 5 | const SIMPLE_FILE_PATH = resolve('..', '__fixtures__/simple.js'); 6 | 7 | describe('eslint-analyzer', () => { 8 | it('can create an eslint analyzer', async () => { 9 | let options: ESLint.Options = { 10 | baseConfig: { 11 | parser: '@babel/eslint-parser', 12 | parserOptions: { 13 | ecmaVersion: 2018, 14 | sourceType: 'module', 15 | ecmaFeatures: { 16 | legacyDecorators: true, 17 | }, 18 | requireConfigFile: false, 19 | }, 20 | env: { 21 | browser: true, 22 | }, 23 | }, 24 | }; 25 | 26 | let analyzer: ESLintAnalyzer = new ESLintAnalyzer(options); 27 | let configForFile = await analyzer.engine.calculateConfigForFile(SIMPLE_FILE_PATH); 28 | 29 | expect(analyzer.engine).toBeInstanceOf(ESLint); 30 | expect(Object.keys(configForFile)).toMatchInlineSnapshot(` 31 | [ 32 | "env", 33 | "globals", 34 | "noInlineConfig", 35 | "parser", 36 | "parserOptions", 37 | "plugins", 38 | "reportUnusedDisableDirectives", 39 | "rules", 40 | "settings", 41 | "ignorePatterns", 42 | ] 43 | `); 44 | }); 45 | 46 | it('can create an eslint analyzer with custom rule configuration', async () => { 47 | let options: ESLint.Options = { 48 | baseConfig: { 49 | parser: '@babel/eslint-parser', 50 | parserOptions: { 51 | ecmaVersion: 2018, 52 | sourceType: 'module', 53 | ecmaFeatures: { 54 | legacyDecorators: true, 55 | }, 56 | requireConfigFile: false, 57 | }, 58 | env: { browser: true }, 59 | rules: { 60 | 'no-tabs': [ 61 | 'error', 62 | { 63 | allowIndentationTabs: true, 64 | }, 65 | ], 66 | }, 67 | }, 68 | }; 69 | 70 | let analyzer: ESLintAnalyzer = new ESLintAnalyzer(options); 71 | let configForFile = await analyzer.engine.calculateConfigForFile(SIMPLE_FILE_PATH); 72 | 73 | expect(analyzer.engine).toBeInstanceOf(ESLint); 74 | expect(configForFile.rules!['no-tabs']).toMatchInlineSnapshot(` 75 | [ 76 | 0, 77 | { 78 | "allowIndentationTabs": true, 79 | }, 80 | ] 81 | `); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /packages/core/tests/analyzers/javascript-analyzer-test.ts: -------------------------------------------------------------------------------- 1 | import JavaScriptAnalyzer from '../../src/analyzers/javascript-analyzer'; 2 | 3 | describe('javascript-analyzer', () => { 4 | it('can parse cjs modules', () => { 5 | let source = ` 6 | const foo = require('foo'); 7 | 8 | foo(); 9 | `; 10 | 11 | expect(() => { 12 | new JavaScriptAnalyzer(source); 13 | }).not.toThrow(); 14 | }); 15 | 16 | it('can parse esm modules', () => { 17 | let source = ` 18 | import foo from './foo'; 19 | 20 | foo(); 21 | `; 22 | 23 | expect(() => { 24 | new JavaScriptAnalyzer(source); 25 | }).not.toThrow(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/core/tests/analyzers/json-analyzer-test.ts: -------------------------------------------------------------------------------- 1 | import JsonAnalyzer from '../../src/analyzers/json-analyzer'; 2 | 3 | describe('json-analyzer', () => { 4 | it('throws when given invalid JSON string', () => { 5 | expect(() => { 6 | new JsonAnalyzer('not valid'); 7 | }).toThrow(); 8 | }); 9 | 10 | it('can traverse JSON', () => { 11 | expect(() => { 12 | new JsonAnalyzer(JSON.stringify(['an', 'array'], null, 2)); 13 | }).not.toThrow(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/core/tests/analyzers/stylelint-analyzer-test.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { dirname } from '@checkup/core'; 3 | import { LinterOptions } from 'stylelint'; 4 | import StylelintAnalyzer from '../../src/analyzers/stylelint-analyzer'; 5 | import { TaskConfig } from '../../src/types/config'; 6 | 7 | describe('stylelint-analyzer', () => { 8 | it('can create a stylelint analyzer', () => { 9 | let config: Partial = { 10 | config: { 11 | rules: { 12 | 'font-family-no-duplicate-names': true, 13 | }, 14 | }, 15 | }; 16 | 17 | let analyzer: StylelintAnalyzer = new StylelintAnalyzer(config); 18 | 19 | expect(analyzer.config).toEqual(config); 20 | }); 21 | 22 | it('can create a stylelint analyzer with custom rule configuration', () => { 23 | let customConfig: TaskConfig = { 24 | stylelintConfig: { 25 | config: { 26 | rules: { 27 | 'font-family-no-duplicate-names': false, 28 | }, 29 | }, 30 | }, 31 | }; 32 | let config: Partial = { 33 | config: { 34 | rules: { 35 | 'font-family-no-duplicate-names': true, 36 | }, 37 | }, 38 | }; 39 | 40 | let analyzer: StylelintAnalyzer = new StylelintAnalyzer(config, customConfig); 41 | 42 | expect(analyzer.config).toEqual(customConfig.stylelintConfig); 43 | }); 44 | 45 | it('can run on files and return a result', async () => { 46 | let config: Partial = { 47 | config: { 48 | rules: { 49 | 'font-family-no-duplicate-names': true, 50 | }, 51 | }, 52 | }; 53 | 54 | let analyzer: StylelintAnalyzer = new StylelintAnalyzer(config); 55 | 56 | let filePath = resolve(dirname(import.meta), '..', '__fixtures__/simple.css'); 57 | let result = await analyzer.analyze([filePath]); 58 | 59 | expect(result.errored).toEqual(true); 60 | expect(result.results).toHaveLength(1); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /packages/core/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true 5 | }, 6 | "references": [{ "path": ".." }] 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/tests/utils/__snapshots__/calculate-section-bar-test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`calculate-sectioned-bar > calculateSectionBar > should calculateSectionBar segments correctly 1`] = ` 4 | { 5 | "completedSegments": [ 6 | { 7 | "completed": 50, 8 | "count": 495, 9 | "title": "bar", 10 | }, 11 | { 12 | "completed": 1, 13 | "count": 5, 14 | "title": "foo", 15 | }, 16 | ], 17 | "incompleteSegments": 0, 18 | } 19 | `; 20 | 21 | exports[`calculate-sectioned-bar > calculateSectionBar > should calculateSectionBar segments correctly 3`] = ` 22 | { 23 | "completedSegments": [ 24 | { 25 | "completed": 25, 26 | "count": 10, 27 | "title": "bar", 28 | }, 29 | { 30 | "completed": 25, 31 | "count": 10, 32 | "title": "foo", 33 | }, 34 | ], 35 | "incompleteSegments": 0, 36 | } 37 | `; 38 | 39 | exports[`calculate-sectioned-bar > calculateSectionBar > should calculateSectionBar segments correctly 4`] = ` 40 | { 41 | "completedSegments": [ 42 | { 43 | "completed": 17, 44 | "count": 10, 45 | "title": "bar", 46 | }, 47 | { 48 | "completed": 17, 49 | "count": 10, 50 | "title": "foo", 51 | }, 52 | { 53 | "completed": 17, 54 | "count": 10, 55 | "title": "moo", 56 | }, 57 | ], 58 | "incompleteSegments": 0, 59 | } 60 | `; 61 | 62 | exports[`calculate-sectioned-bar > calculateSectionBar > should calculateSectionBar segments correctly 5`] = ` 63 | { 64 | "completedSegments": [ 65 | { 66 | "completed": 17, 67 | "count": 50, 68 | "title": "bar", 69 | }, 70 | { 71 | "completed": 17, 72 | "count": 50, 73 | "title": "foo", 74 | }, 75 | { 76 | "completed": 17, 77 | "count": 50, 78 | "title": "foo", 79 | }, 80 | ], 81 | "incompleteSegments": 0, 82 | } 83 | `; 84 | 85 | exports[`calculate-sectioned-bar > calculateSectionBar > should calculateSectionBar segments correctly 6`] = ` 86 | { 87 | "completedSegments": [ 88 | { 89 | "completed": 17, 90 | "count": 50, 91 | "title": "foo", 92 | }, 93 | { 94 | "completed": 17, 95 | "count": 50, 96 | "title": "foo", 97 | }, 98 | { 99 | "completed": 7, 100 | "count": 20, 101 | "title": "bar", 102 | }, 103 | ], 104 | "incompleteSegments": 10, 105 | } 106 | `; 107 | -------------------------------------------------------------------------------- /packages/core/tests/utils/file-paths-array-test.ts: -------------------------------------------------------------------------------- 1 | import { FilePathArray } from '../../src/utils/file-path-array'; 2 | 3 | describe('FilePathsArray', function () { 4 | it('returns all files when no patterns are provided', function () { 5 | // eslint-disable-next-line unicorn/no-useless-spread 6 | let files = new FilePathArray(...['foo.js', 'blue.hbs', 'goo.hbs']); 7 | 8 | expect(files.filterByGlob('**.js')).toMatchInlineSnapshot(` 9 | [ 10 | "foo.js", 11 | ] 12 | `); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /packages/core/tests/utils/normalize-package-name-test.ts: -------------------------------------------------------------------------------- 1 | import { getShorthandName, normalizePackageName } from '../../src/utils/normalize-package-name'; 2 | 3 | describe('normalize-package-name', () => { 4 | it.each([ 5 | ['foo', 'checkup-plugin-foo'], 6 | ['checkup-plugin-foo', 'checkup-plugin-foo'], 7 | ['@z/foo', '@z/checkup-plugin-foo'], 8 | ['@z\\foo', '@z/checkup-plugin-foo'], 9 | ['@z\\foo\\bar.js', '@z/checkup-plugin-foo/bar.js'], 10 | ['@z/checkup-plugin', '@z/checkup-plugin'], 11 | ['@z/checkup-plugin-foo', '@z/checkup-plugin-foo'], 12 | ])('normalizes plugin name from %s to %s', (input, expected) => { 13 | let pluginName = normalizePackageName(input); 14 | expect(pluginName).toEqual(expected); 15 | }); 16 | 17 | it.each([ 18 | ['foo', 'checkup-formatter-foo'], 19 | ['checkup-formatter-foo', 'checkup-formatter-foo'], 20 | ['@z/foo', '@z/checkup-formatter-foo'], 21 | ['@z\\foo', '@z/checkup-formatter-foo'], 22 | ['@z\\foo\\bar.js', '@z/checkup-formatter-foo/bar.js'], 23 | ['@z/checkup-formatter', '@z/checkup-formatter'], 24 | ['@z/checkup-formatter-foo', '@z/checkup-formatter-foo'], 25 | ])('normalizes formatter name from %s to %s', (input, expected) => { 26 | let pluginName = normalizePackageName(input, 'checkup-formatter'); 27 | expect(pluginName).toEqual(expected); 28 | }); 29 | 30 | it.each([ 31 | ['foo', 'foo'], 32 | ['checkup-plugin-foo', 'foo'], 33 | ['@z', '@z'], 34 | ['@z/checkup-plugin', '@z'], 35 | ['@z/foo', '@z/foo'], 36 | ['@z/checkup-plugin-foo', '@z/foo'], 37 | ])('gets short plugin name from %s to %s', (input, expected) => { 38 | let pluginName = getShorthandName(input); 39 | expect(pluginName).toEqual(expected); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-base.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "rootDir": "src" 6 | }, 7 | "include": ["./src", "./src/schemas/config-schema.json"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ['tests/**/*-test.ts'], 6 | globals: true, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /packages/plugin/bin/checkup-plugin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | let args = process.argv.slice(2); 4 | 5 | if (args[0] === 'docs') { 6 | const { generate } = await import('../lib/generate-docs'); 7 | 8 | generate(); 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@checkup/plugin", 3 | "version": "3.0.1", 4 | "description": "Checkup plugin utility library", 5 | "homepage": "https://github.com/checkupjs/checkup", 6 | "bugs": "https://github.com/checkupjs/checkup/issues", 7 | "repository": "https://github.com/checkupjs/checkup", 8 | "license": "MIT", 9 | "author": "Steve Calvert ", 10 | "main": "lib/index.js", 11 | "types": "lib/index.d.ts", 12 | "bin": { 13 | "checkup-plugin": "./bin/checkup-plugin.js" 14 | }, 15 | "files": [ 16 | "/bin", 17 | "/lib", 18 | "/templates" 19 | ], 20 | "scripts": { 21 | "build": "tsc --build", 22 | "test": "echo \"No tests\"" 23 | }, 24 | "dependencies": { 25 | "@babel/traverse": "^7.13.17", 26 | "@babel/types": "7.18.8", 27 | "@checkup/core": "^3.0.1", 28 | "fs-extra": "^9.1.0", 29 | "recast": "^0.20.5" 30 | }, 31 | "engines": { 32 | "node": ">= 16" 33 | }, 34 | "publishConfig": { 35 | "access": "public" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/plugin/templates/task-template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-base.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "rootDir": "src" 6 | }, 7 | "include": ["./src"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/plugin/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ['tests/**/*-test.ts'], 6 | global: true, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /packages/test-helpers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@checkup/test-helpers", 3 | "version": "3.0.1", 4 | "description": "Checkup's test helper library", 5 | "homepage": "https://github.com/checkupjs/checkup", 6 | "bugs": "https://github.com/checkupjs/checkup/issues", 7 | "repository": "https://github.com/checkupjs/checkup", 8 | "license": "MIT", 9 | "author": "Steve Calvert ", 10 | "type": "module", 11 | "main": "lib/index.js", 12 | "types": "lib/index.d.ts", 13 | "files": [ 14 | "/lib", 15 | "/static" 16 | ], 17 | "scripts": { 18 | "build": "tsc --build", 19 | "test": "echo \"No tests\"" 20 | }, 21 | "dependencies": { 22 | "@checkup/core": "3.0.1", 23 | "fixturify-project": "^2.1.1", 24 | "fs-extra": "^10.1.0", 25 | "handlebars": "^4.7.7", 26 | "json-stable-stringify": "^1.0.1", 27 | "tmp": "^0.2.1", 28 | "walk-sync": "^3.0.0" 29 | }, 30 | "engines": { 31 | "node": ">= 16" 32 | }, 33 | "publishConfig": { 34 | "access": "public" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/test-helpers/src/get-task-context.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CheckupConfig, 3 | FilePathArray, 4 | TaskContext, 5 | RunOptions, 6 | CheckupLogBuilder, 7 | CONFIG_SCHEMA_URL, 8 | getFilePaths, 9 | } from '@checkup/core'; 10 | 11 | import { PackageJson } from 'type-fest'; 12 | 13 | type TaskContextArgs = { 14 | cliArguments: string[]; 15 | options: Partial; 16 | config: Partial; 17 | pkg: PackageJson; 18 | paths: FilePathArray; 19 | }; 20 | 21 | const DEFAULT_OPTIONS: RunOptions = { 22 | paths: ['.'], 23 | config: undefined, 24 | excludePaths: undefined, 25 | cwd: process.cwd(), 26 | categories: undefined, 27 | groups: undefined, 28 | tasks: undefined, 29 | listTasks: false, 30 | }; 31 | 32 | const DEFAULT_CONFIG: CheckupConfig = { 33 | $schema: CONFIG_SCHEMA_URL, 34 | excludePaths: [], 35 | plugins: [], 36 | tasks: {}, 37 | }; 38 | 39 | const DEFAULT_PACKAGE_JSON: PackageJson = { 40 | name: 'foo-project', 41 | version: '0.0.0', 42 | keywords: [], 43 | }; 44 | 45 | /** 46 | * Gets a fake task context. 47 | * 48 | * @param {Partial} taskContextArgs - An object containing properties contained in a TaskContext 49 | * @param {Partial} taskContextArgs.options - The CLI options 50 | * @param {Partial} taskContextArgs.config - The CLI config 51 | * @param {PackageJson} taskContextArgs.pkg - The package.json from the base directory checking is run from 52 | * @param {FilePathArray} taskContextArgs.paths - The paths checkup is operating on 53 | * @returns {TaskContext} - A task context instance 54 | */ 55 | export function getTaskContext({ 56 | options, 57 | config, 58 | pkg = DEFAULT_PACKAGE_JSON, 59 | paths, 60 | }: Partial = {}): TaskContext { 61 | let opts = Object.assign({}, DEFAULT_OPTIONS, options) as RunOptions; 62 | let c = Object.assign({}, DEFAULT_CONFIG, config) as CheckupConfig; 63 | 64 | let taskContext = { 65 | options: opts, 66 | config: c, 67 | logBuilder: new CheckupLogBuilder({ 68 | analyzedPackageJson: { 69 | name: pkg.name ?? '', 70 | version: pkg.version ?? '', 71 | }, 72 | options: opts, 73 | }), 74 | pkg: pkg, 75 | pkgSource: JSON.stringify(pkg, null, 2), 76 | paths: paths || getFilePaths(options?.cwd ?? process.cwd(), ['.']), 77 | }; 78 | 79 | taskContext.logBuilder.config; 80 | taskContext.logBuilder.actions = []; 81 | taskContext.logBuilder.errors = []; 82 | taskContext.logBuilder.timings = {}; 83 | taskContext.logBuilder.executedTasks = []; 84 | 85 | return taskContext; 86 | } 87 | -------------------------------------------------------------------------------- /packages/test-helpers/src/index.ts: -------------------------------------------------------------------------------- 1 | export { createTmpDir } from './tmp-dir.js'; 2 | export { testRoot } from './test-root.js'; 3 | export { getTaskContext } from './get-task-context.js'; 4 | export { default as CheckupProject } from './checkup-fixturify-project.js'; 5 | export { default as EmberProject } from './ember-cli-fixturify-project.js'; 6 | -------------------------------------------------------------------------------- /packages/test-helpers/src/test-root.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync, readdirSync } from 'fs'; 2 | 3 | import { join } from 'path'; 4 | 5 | class TestRoot { 6 | constructor(public baseDir: string, ...paths: string[]) { 7 | this.baseDir = join(baseDir, ...paths); 8 | } 9 | 10 | file(...paths: string[]) { 11 | return new TestFile(this.baseDir, ...paths); 12 | } 13 | 14 | directory(...paths: string[]) { 15 | return new TestDirectory(this.baseDir, ...paths); 16 | } 17 | } 18 | 19 | class TestFile { 20 | filePath: string; 21 | contents: string; 22 | 23 | constructor(public baseDir: string, ...paths: string[]) { 24 | this.filePath = join(this.baseDir, ...paths); 25 | this.contents = readFileSync(this.filePath, 'utf-8'); 26 | } 27 | } 28 | 29 | class TestDirectory { 30 | dirPath: string; 31 | contents: string[]; 32 | 33 | constructor(public baseDir: string, ...paths: string[]) { 34 | this.dirPath = join(this.baseDir, ...paths); 35 | this.contents = readdirSync(this.dirPath); 36 | } 37 | } 38 | 39 | /** 40 | * Creates an instance of a class that can be used for assserting specific files under a root directory. 41 | * 42 | * @param {string} baseDir - The base directory to assert from. 43 | * @param {...string[]} paths - The paths to use for assertions. 44 | * @returns {TestRoot} - An instance of a TestRoot 45 | */ 46 | export function testRoot(baseDir: string, ...paths: string[]) { 47 | return new TestRoot(baseDir, ...paths); 48 | } 49 | -------------------------------------------------------------------------------- /packages/test-helpers/src/tmp-dir.ts: -------------------------------------------------------------------------------- 1 | import { realpathSync } from 'fs'; 2 | import tmp from 'tmp'; 3 | 4 | /** 5 | * Creates a tmp directory with unsafe cleanup allowed. 6 | * 7 | * @returns {string} The path of the tmp directory 8 | */ 9 | export function createTmpDir() { 10 | return realpathSync(tmp.dirSync({ unsafeCleanup: true }).name); 11 | } 12 | -------------------------------------------------------------------------------- /packages/test-helpers/static/templates/register-tasks.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{#each classes as |class|}} 3 | {{{class}}} 4 | {{/each}} 5 | 6 | const hook = async function({ context, tasks }) { 7 | {{#each taskNames as |taskName|}} 8 | tasks.registerTask(new {{taskName}}(context)); 9 | {{/each}} 10 | }; 11 | 12 | exports.default = hook; -------------------------------------------------------------------------------- /packages/test-helpers/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-base.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "rootDir": "src", 6 | "esModuleInterop": true 7 | }, 8 | "references": [ 9 | { 10 | "path": "../core" 11 | } 12 | ], 13 | "include": ["src/**/*"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/test-helpers/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ['tests/**/*-test.ts'], 6 | global: true, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@checkup/ui", 3 | "version": "3.0.1", 4 | "description": "Checkup ui library", 5 | "bugs": "https://github.com/checkupjs/checkup/issues", 6 | "repository": "https://github.com/checkupjs/checkup", 7 | "license": "MIT", 8 | "author": "Steve Calvert ", 9 | "type": "module", 10 | "main": "lib/index.js", 11 | "scripts": { 12 | "build": "tsc --build", 13 | "test": "vitest run" 14 | }, 15 | "dependencies": { 16 | "@checkup/core": "3.0.1", 17 | "@types/react": "^17.0.15", 18 | "chalk": "^4.0.0", 19 | "events": "^3.3.0", 20 | "fs-extra": "^10.0.0", 21 | "ink": "^3.2.0", 22 | "ink-render-string": "^1.0.0", 23 | "ink-table": "^3.0.0", 24 | "ink-task-list": "^1.1.0", 25 | "lodash.startcase": "^4.4.0", 26 | "log-symbols": "^4.0.0", 27 | "object-path": "^0.11.8", 28 | "react": "^17.0.2", 29 | "strip-ansi": "^6.0.0" 30 | }, 31 | "devDependencies": { 32 | "@types/fs-extra": "^9.0.12", 33 | "@types/lodash.startcase": "^4.4.7", 34 | "@types/object-path": "^0.11.1", 35 | "@types/react": "^17.0.15", 36 | "@types/resolve": "^1.20.2", 37 | "ink-testing-library": "^2.1.0", 38 | "lodash.merge": "^4.6.2" 39 | }, 40 | "engines": { 41 | "node": ">= 16" 42 | }, 43 | "publishConfig": { 44 | "access": "public" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/ui/src/base-ui-formatter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Formatter, 3 | FormatterOptions, 4 | CheckupLogParser, 5 | CheckupError, 6 | ErrorKind, 7 | } from '@checkup/core'; 8 | import * as React from 'react'; 9 | import { render } from 'ink-render-string'; 10 | 11 | export default class BaseUIFormatter implements Formatter { 12 | shouldWrite = false; 13 | options: FormatterOptions; 14 | component: any; 15 | 16 | constructor(options: FormatterOptions, component: any) { 17 | this.options = options; 18 | this.component = component; 19 | } 20 | 21 | format(logParser: CheckupLogParser): string { 22 | try { 23 | const result = render( 24 | React.createElement(this.component, { logParser, options: this.options }) 25 | ); 26 | 27 | if (result.output.includes('ERROR')) { 28 | throw result; 29 | } else { 30 | return result.output; 31 | } 32 | } catch (error) { 33 | throw new CheckupError(ErrorKind.InvalidCustomComponent, { 34 | details: error, 35 | }); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/ui/src/component-provider.ts: -------------------------------------------------------------------------------- 1 | import { readdirSync } from 'fs'; 2 | import { join, parse } from 'path'; 3 | import { dirname } from '@checkup/core'; 4 | import * as React from 'react'; 5 | 6 | export const registeredComponents = new Map(); 7 | const __dirname = dirname(import.meta); 8 | 9 | export async function registerDefaultComponents(): Promise> { 10 | let builtInComponents = new Set( 11 | readdirSync(join(__dirname, 'components'), { withFileTypes: true }) 12 | .filter((file) => file.isFile()) 13 | .map((file) => { 14 | return parse(file.name).base.split('.')[0]; 15 | }) 16 | ); 17 | 18 | for (let component of builtInComponents) { 19 | registeredComponents.set( 20 | component.toLocaleLowerCase(), 21 | Object.values( 22 | await import(join(__dirname, '../lib', 'components', `${component}.js`)) 23 | ).pop() as React.FC 24 | ); 25 | } 26 | 27 | return registeredComponents; 28 | } 29 | 30 | await registerDefaultComponents(); 31 | 32 | export function registerCustomComponent(componentName: string, component: React.FC) { 33 | registeredComponents.set(componentName, component); 34 | } 35 | -------------------------------------------------------------------------------- /packages/ui/src/components/Actions.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Box, Text } from 'ink'; 3 | import chalk from 'chalk'; 4 | import { Notification } from 'sarif'; 5 | 6 | export const Actions: React.FC<{ actions: Notification[] }> = ({ actions }) => { 7 | if (!actions || actions.length === 0) { 8 | return <>; 9 | } 10 | 11 | return ( 12 | 13 | {actions.map((action: Notification) => { 14 | return ( 15 | 16 | {chalk.yellow('■')} {action.message.text} 17 | 18 | ); 19 | })} 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/ui/src/components/Bar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { RuleResults } from '@checkup/core'; 3 | import { BarData } from '../types'; 4 | import { SectionedBar } from '../components/SectionedBar.js'; 5 | 6 | /** 7 | * // TODO: Group result by data field 8 | * that provided by checkup task 9 | */ 10 | export const Bar: React.FC<{ taskResult: RuleResults }> = ({ taskResult }) => { 11 | let barData: BarData = { 12 | name: taskResult.rule.id, 13 | value: taskResult.results.length, 14 | total: taskResult.results.length, 15 | }; 16 | return ; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/ui/src/components/CLIInfo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { CheckupMetadata } from '@checkup/core'; 3 | import { Box, Text } from 'ink'; 4 | 5 | export const CLIInfo: React.FC<{ metaData: CheckupMetadata }> = ({ metaData }) => { 6 | let { version, configHash } = metaData.cli; 7 | 8 | return ( 9 | 10 | checkup v{version} 11 | config {configHash} 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/ui/src/components/List.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Box, Text } from 'ink'; 3 | import { RuleResults } from '@checkup/core'; 4 | import objectPath from 'object-path'; 5 | import startCase from 'lodash.startcase'; 6 | import { TaskDisplayName } from '../components/TaskDisplayName.js'; 7 | import { getOptions } from '../get-options.js'; 8 | 9 | type ListOptions = { 10 | items: Record; 11 | }; 12 | 13 | type ListItem = { 14 | text: string; 15 | value: string | number; 16 | }; 17 | 18 | export const List: React.FC<{ taskResult: RuleResults }> = ({ taskResult }) => { 19 | let listItems = buildListData(taskResult); 20 | 21 | return ( 22 | <> 23 | 24 | 25 | {listItems.map((item: any) => { 26 | return ( 27 | 28 | {startCase(item.text)} {item.value} 29 | 30 | ); 31 | })} 32 | 33 | 34 | ); 35 | }; 36 | 37 | function buildListData(taskResult: RuleResults) { 38 | let { rule, results } = taskResult; 39 | let { items } = getOptions(rule); 40 | let listItems: ListItem[] = []; 41 | 42 | for (let item of Object.keys(items)) { 43 | listItems.push({ 44 | text: item, 45 | value: results.filter( 46 | (result) => objectPath.get(result, items[item].groupBy) === items[item].value 47 | ).length, 48 | }); 49 | } 50 | 51 | return listItems; 52 | } 53 | -------------------------------------------------------------------------------- /packages/ui/src/components/MetaData.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { CheckupMetadata } from '@checkup/core'; 3 | import { Box, Text } from 'ink'; 4 | 5 | export const MetaData: React.FC<{ metaData: CheckupMetadata }> = ({ metaData }) => { 6 | let { analyzedFilesCount, project } = metaData; 7 | let { name, version, repository } = project; 8 | let analyzedFilesMessage = 9 | repository.totalFiles !== analyzedFilesCount ? `(${analyzedFilesCount} files analyzed)` : ''; 10 | 11 | return ( 12 | <> 13 | 14 | 15 | 16 | Checkup report generated for {name} v{version} {analyzedFilesMessage} 17 | 18 | 19 | 20 | 21 | This project is {repository.age} old, with {repository.activeDays} active days,{' '} 22 | {repository.totalCommits} commits and {repository.totalFiles} files 23 | 24 | 25 | 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/ui/src/components/Migration.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Box, Text } from 'ink'; 3 | import { RuleResults } from '@checkup/core'; 4 | import { Result } from 'sarif'; 5 | import { TaskDisplayName } from '../components/TaskDisplayName.js'; 6 | import { getOptions } from '../get-options.js'; 7 | import { getSorter, SortBy, SortDirection } from '../get-sorter.js'; 8 | 9 | type MigrationOptions = { 10 | sortBy: SortBy; 11 | sortDirection: SortDirection; 12 | }; 13 | 14 | export const Migration: React.FC<{ taskResult: RuleResults }> = ({ taskResult }) => { 15 | let featureStatus = buildMigrationData(taskResult); 16 | let outstandingFeatureCount = taskResult.results.length; 17 | 18 | return ( 19 | <> 20 | 21 | Outstanding features to be migrated: {outstandingFeatureCount} 22 | 23 | {[...featureStatus].map(([feature, count]) => { 24 | return ( 25 | 26 | {feature} {count} 27 | 28 | ); 29 | })} 30 | 31 | 32 | ); 33 | }; 34 | 35 | function buildMigrationData(taskResult: RuleResults) { 36 | const features = taskResult.rule.properties?.features || []; 37 | const { rule, results } = taskResult; 38 | const options = getOptions(rule); 39 | 40 | let aggregatedFeatureResults = results.reduce((features: Map, result: Result) => { 41 | // Tasks inheriting from BaseMigrationTask use featureName for their migration property key. Generic BaseTasks do not. 42 | let feature = result.properties?.migration.featureName ?? result.properties?.migration.feature; 43 | 44 | features.set(feature, (features.get(feature) ?? 0) + 1); 45 | 46 | return features; 47 | }, new Map()); 48 | 49 | // Any feature that doesn't have results associated with it should indicate that 50 | // it's complete, or has no outstanding work. 51 | for (let feature of features) { 52 | if (!aggregatedFeatureResults.get(feature)) { 53 | aggregatedFeatureResults.set(feature, 0); 54 | } 55 | } 56 | 57 | if (options.sortBy) { 58 | let sort = getSorter(options.sortBy); 59 | 60 | aggregatedFeatureResults = sort(aggregatedFeatureResults, options.sortDirection); 61 | } 62 | 63 | return aggregatedFeatureResults; 64 | } 65 | -------------------------------------------------------------------------------- /packages/ui/src/components/NoResults.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Text } from 'ink'; 3 | 4 | export const NoResults: React.FC<{}> = () => { 5 | return ( 6 | <> 7 | No results found. 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /packages/ui/src/components/ResultsToFile.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Box, Text } from 'ink'; 3 | import { Log } from 'sarif'; 4 | import { FormatterOptions, writeResultsToFile } from '@checkup/core'; 5 | 6 | export const ResultsToFile: React.FC<{ log: Log; options: FormatterOptions }> = ({ 7 | log, 8 | options, 9 | }) => { 10 | let cwd = process.env.TESTING_TMP_DIR !== undefined ? process.env.TESTING_TMP_DIR : options.cwd; 11 | let resultsFilePath = writeResultsToFile(log, cwd, options.outputFile); 12 | 13 | return ( 14 | 15 | 16 | Results have been saved to the following file: 17 | 18 | 19 | 20 | {resultsFilePath} 21 | 22 | 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /packages/ui/src/components/SectionedBar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Text } from 'ink'; 3 | import { BarData } from '../types'; 4 | 5 | export const SectionedBar: React.FC<{ data: BarData }> = ({ data }) => { 6 | const barTick = '■'; 7 | // 16777215 == ffffff in decimal 8 | const randomColor = '#' + Math.floor(Math.random() * 16_777_215).toString(16); 9 | const width: number = 50; 10 | 11 | const normalizeSegment = function (amount: number) { 12 | return Math.ceil( 13 | data.total < width ? amount * (width / data.total) : amount / (data.total / width) 14 | ); 15 | }; 16 | 17 | return ( 18 | 19 | {barTick.repeat(normalizeSegment(data.value))} {data.name} ( 20 | {data.value}) 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/ui/src/components/Table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { default as InkTable } from 'ink-table'; 3 | import { Box } from 'ink'; 4 | import objectPath from 'object-path'; 5 | import { Result } from 'sarif'; 6 | import { RuleResults } from '@checkup/core'; 7 | import { TaskDisplayName } from '../components/TaskDisplayName.js'; 8 | import { getOptions } from '../get-options.js'; 9 | import { NoResults } from './NoResults.js'; 10 | 11 | type TableOptions = { 12 | sumBy: { 13 | findGroupBy: string; 14 | sumValueBy: string; 15 | }; 16 | rows: Record; 17 | }; 18 | 19 | export const Table: React.FC<{ taskResult: RuleResults }> = ({ taskResult }) => { 20 | let rowData = buildTableData(taskResult); 21 | 22 | return ( 23 | <> 24 | 25 | 26 | {rowData.length === 0 ? ( 27 | 28 | ) : ( 29 | 30 | 31 | 32 | )} 33 | 34 | ); 35 | }; 36 | 37 | function buildTableData(taskResult: RuleResults): any[] { 38 | let { rule, results } = taskResult; 39 | let { rows, sumBy } = getOptions(rule); 40 | 41 | return sumBy !== undefined 42 | ? results.reduce((groups, result) => { 43 | let summedRow = groups.find((group) => { 44 | let propLookup = rows[sumBy.findGroupBy]; 45 | return group[sumBy.findGroupBy] === objectPath.get(result, propLookup); 46 | }); 47 | 48 | if (summedRow) { 49 | summedRow[sumBy.sumValueBy] = summedRow[sumBy.sumValueBy] += objectPath.get( 50 | result, 51 | rows[sumBy.sumValueBy] 52 | ); 53 | } else { 54 | groups.push(buildRow(rows, result)); 55 | } 56 | 57 | return groups; 58 | }, [] as Record[]) 59 | : results.map((result: Result) => { 60 | return buildRow(rows, result); 61 | }); 62 | } 63 | 64 | function buildRow(rows: Record, result: Result) { 65 | let rowData: Record = {}; 66 | 67 | for (let column of Object.keys(rows)) { 68 | rowData[column] = objectPath.get(result, rows[column as any]); 69 | } 70 | 71 | return rowData; 72 | } 73 | -------------------------------------------------------------------------------- /packages/ui/src/components/TaskDisplayName.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Text } from 'ink'; 3 | import { RuleResults } from '@checkup/core'; 4 | 5 | export const TaskDisplayName: React.FC<{ taskResult: RuleResults }> = ({ taskResult }) => { 6 | return ( 7 | <> 8 | {taskResult.rule.properties?.taskDisplayName} 9 | 10 | {Array.from({ length: taskResult.rule.properties?.taskDisplayName.length }).fill('=')} 11 | 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/ui/src/components/TaskErrors.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Box, Text } from 'ink'; 3 | 4 | export const TaskErrors: React.FC<{ hasErrors: boolean }> = ({ hasErrors }) => { 5 | if (!hasErrors) { 6 | return <>; 7 | } 8 | 9 | return ( 10 | 11 | 12 | Some tasks ran with errors. Use the "summary" formatter or use the `--output-file` option 13 | and review the "invocation.toolExecutionNotifications" property in the SARIF log for more 14 | details. 15 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/ui/src/components/TaskTiming.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Text } from 'ink'; 3 | import { default as InkTable } from 'ink-table'; 4 | 5 | export const TaskTiming: React.FC<{ timings: Record }> = ({ timings }) => { 6 | let total = Object.values(timings).reduce((total, timing) => (total += timing), 0); 7 | let tableData: any[] = []; 8 | 9 | if (process.env.CHECKUP_TIMING !== '1') { 10 | return <>; 11 | } 12 | 13 | Object.keys(timings).map((taskName) => { 14 | let timing = Number.parseFloat(timings[taskName].toFixed(2)); 15 | let relavtive = `${((timings[taskName] * 100) / total).toFixed(1)}%`; 16 | tableData.push({ 17 | 'Task Name': taskName, 18 | 'Time (sec)': timing, 19 | 'Relative: ': relavtive, 20 | }); 21 | }); 22 | 23 | return ( 24 | <> 25 | Task Timings 26 | 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /packages/ui/src/components/Validation.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Box, Text } from 'ink'; 3 | import { RuleResults } from '@checkup/core'; 4 | import { Result } from 'sarif'; 5 | import logSymbols from 'log-symbols'; 6 | import { TaskDisplayName } from '../components/TaskDisplayName.js'; 7 | 8 | export const Validation: React.FC<{ taskResult: RuleResults }> = ({ taskResult }) => { 9 | let isValid = taskResult.results.every((result) => result.kind === 'pass'); 10 | 11 | return ( 12 | <> 13 | 14 | Validation {isValid ? 'passed' : 'failed'} 15 | 16 | {[...taskResult.results].map((result) => { 17 | return ; 18 | })} 19 | 20 | 21 | ); 22 | }; 23 | 24 | const ValidationStepItem: React.FC<{ result: Result }> = ({ result }) => { 25 | let { message, kind } = result; 26 | 27 | return ( 28 | 29 | {kind === 'pass' ? logSymbols.success : logSymbols.error} {message.text} 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/ui/src/get-options.ts: -------------------------------------------------------------------------------- 1 | import { ReportingDescriptor } from 'sarif'; 2 | 3 | export function getOptions(rule: ReportingDescriptor): T { 4 | return rule.properties?.component.options ?? {}; 5 | } 6 | -------------------------------------------------------------------------------- /packages/ui/src/get-sorter.ts: -------------------------------------------------------------------------------- 1 | export type SortBy = 'key' | 'value'; 2 | export type SortDirection = 'asc' | 'desc'; 3 | 4 | export function getSorter(sortBy: SortBy = 'key') { 5 | if (sortBy === 'key') { 6 | // sorts aphabetically by key, ascending by default (normal alphabtical order) 7 | return (subject: Map, sortDirection: SortDirection = 'asc') => { 8 | let sorted = [...subject.keys()].sort(); 9 | 10 | if (sortDirection === 'desc') { 11 | sorted.reverse(); 12 | } 13 | 14 | return new Map(sorted.map((key) => [key, subject.get(key)!])); 15 | }; 16 | } 17 | 18 | // Sorts by the numerical value, descending by default 19 | return (subject: Map, sortDirection: SortDirection = 'desc') => { 20 | let sorted = [...subject.entries()].sort((a, b) => a[1] - b[1]); 21 | 22 | if (sortDirection === 'desc') { 23 | sorted.reverse(); 24 | } 25 | 26 | return new Map(sorted); 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /packages/ui/src/index.ts: -------------------------------------------------------------------------------- 1 | export { registeredComponents, registerCustomComponent } from './component-provider.js'; 2 | export { default as BaseUIFormatter } from './base-ui-formatter.js'; 3 | 4 | export { Box, Text, Newline } from 'ink'; 5 | export { Actions } from './components/Actions.js'; 6 | export { Bar } from './components/Bar.js'; 7 | export { CLIInfo } from './components/CLIInfo.js'; 8 | export { List } from './components/List.js'; 9 | export { MetaData } from './components/MetaData.js'; 10 | export { Migration } from './components/Migration.js'; 11 | export { NoResults } from './components/NoResults.js'; 12 | export { ResultsToFile } from './components/ResultsToFile.js'; 13 | export { SectionedBar } from './components/SectionedBar.js'; 14 | export { Table } from './components/Table.js'; 15 | export { TaskDisplayName } from './components/TaskDisplayName.js'; 16 | export { TaskErrors } from './components/TaskErrors.js'; 17 | export { TaskTiming } from './components/TaskTiming.js'; 18 | export { Validation } from './components/Validation.js'; 19 | -------------------------------------------------------------------------------- /packages/ui/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface BarData { 2 | name: string; 3 | value: number; 4 | total: number; 5 | } 6 | -------------------------------------------------------------------------------- /packages/ui/tests/__fixtures__/invalid-custom-components.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Text } from 'ink'; 3 | 4 | export const InvalidComponent: React.FC<{ data: any }> = ({ data }) => { 5 | return {data.properties.foo}; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/ui/tests/components/bar-test.tsx: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import * as React from 'react'; 3 | import { render } from 'ink-testing-library'; 4 | import { readJsonSync } from 'fs-extra'; 5 | import { CheckupLogParser, dirname } from '@checkup/core'; 6 | import stripAnsi from 'strip-ansi'; 7 | import { Bar } from '../../src/components/Bar'; 8 | 9 | describe('Bar', () => { 10 | it('can render task result as expected via bar component', async () => { 11 | const log = readJsonSync(resolve(dirname(import.meta), '../__fixtures__/checkup-result.sarif')); 12 | const logParser = new CheckupLogParser(log); 13 | const taskResults = logParser.resultsByRule; 14 | const taskResult = [...taskResults.values()][0]; 15 | 16 | const { stdout } = render(); 17 | 18 | expect(stripAnsi(stdout.lastFrame()!)).toMatchInlineSnapshot( 19 | `"■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ ember-types (839)"` 20 | ); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/ui/tests/components/list-test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render } from 'ink-testing-library'; 3 | import { RuleResults } from '@checkup/core'; 4 | import stripAnsi from 'strip-ansi'; 5 | import { List } from '../../src/components/List'; 6 | 7 | describe('List', () => { 8 | it('can render task result as expected via list component', async () => { 9 | const taskResult: RuleResults = { 10 | rule: { 11 | id: 'ember-template-lint-summary', 12 | shortDescription: { 13 | text: 'Gets a summary of all ember-template-lint results in an Ember.js project', 14 | }, 15 | properties: { 16 | taskDisplayName: 'Template Lint Summary', 17 | category: 'linting', 18 | component: { 19 | name: 'list', 20 | options: { 21 | items: { 22 | Errors: { 23 | groupBy: 'level', 24 | value: 'error', 25 | }, 26 | Warnings: { 27 | groupBy: 'level', 28 | value: 'warning', 29 | }, 30 | }, 31 | }, 32 | }, 33 | }, 34 | }, 35 | results: [ 36 | { 37 | message: { 38 | text: 'Do not use `action` as