From 10dbdffdc2ada61c809f52af5fe3a857b3f5f8df Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Mon, 19 Feb 2024 11:29:40 +0000 Subject: [PATCH] chore: migrate very_good_dart_cli --- .github/workflows/very_good_dart_cli.yaml | 68 ++++++ .../workflows/very_good_dart_cli_hooks.yaml | 21 ++ mason.yaml | 4 +- very_good_core/analysis_options.yaml | 3 +- very_good_dart_cli/.gitignore | 15 ++ very_good_dart_cli/CHANGELOG.md | 73 ++++++ very_good_dart_cli/CONTRIBUTING.md | 110 +++++++++ very_good_dart_cli/LICENSE | 21 ++ very_good_dart_cli/README.md | 67 ++++++ .../.github/ISSUE_TEMPLATE/ config.yml | 1 + .../.github/ISSUE_TEMPLATE/bug_report.md | 29 +++ .../.github/ISSUE_TEMPLATE/feature_request.md | 18 ++ .../.github/PULL_REQUEST_TEMPLATE.md | 23 ++ .../.github/cspell.json | 20 ++ .../.github}/dependabot.yaml | 3 +- .../{{project_name.snakeCase()}}.yaml | 55 +++++ .../{{project_name.snakeCase()}}/.gitignore | 15 ++ .../{{project_name.snakeCase()}}/README.md | 71 ++++++ .../analysis_options.yaml | 4 + .../bin/{{executable_name.snakeCase()}}.dart | 18 ++ .../coverage_badge.svg | 20 ++ .../dart_test.yaml | 3 + .../lib/src/command_runner.dart | 144 ++++++++++++ .../lib/src/commands/commands.dart | 2 + .../lib/src/commands/sample_command.dart | 39 ++++ .../lib/src/commands/update_command.dart | 74 ++++++ .../lib/src/version.dart | 2 + .../lib/{{project_name.snakeCase()}}.dart | 10 + .../{{project_name.snakeCase()}}/pubspec.yaml | 24 ++ .../test/ensure_build_test.dart | 9 + .../test/src/command_runner_test.dart | 174 +++++++++++++++ .../src/commands/sample_command_test.dart | 58 +++++ .../src/commands/update_command_test.dart | 185 +++++++++++++++ .../publishable}} | 3 + very_good_dart_cli/analysis_options.yaml | 3 + very_good_dart_cli/brick.yaml | 29 +++ very_good_dart_cli/config.json | 6 + very_good_dart_cli/hooks/.gitignore | 4 + .../hooks/analysis_options.yaml | 1 + very_good_dart_cli/hooks/post_gen.dart | 55 +++++ very_good_dart_cli/hooks/pubspec.yaml | 13 ++ .../hooks/test/post_gen_test.dart | 210 ++++++++++++++++++ very_good_wear_app/analysis_options.yaml | 3 + 43 files changed, 1707 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/very_good_dart_cli.yaml create mode 100644 .github/workflows/very_good_dart_cli_hooks.yaml create mode 100644 very_good_dart_cli/.gitignore create mode 100644 very_good_dart_cli/CHANGELOG.md create mode 100644 very_good_dart_cli/CONTRIBUTING.md create mode 100644 very_good_dart_cli/LICENSE create mode 100644 very_good_dart_cli/README.md create mode 100644 very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.github/ISSUE_TEMPLATE/ config.yml create mode 100644 very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.github/ISSUE_TEMPLATE/bug_report.md create mode 100644 very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.github/ISSUE_TEMPLATE/feature_request.md create mode 100644 very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.github/PULL_REQUEST_TEMPLATE.md create mode 100644 very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.github/cspell.json rename {.github => very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.github}/dependabot.yaml (79%) create mode 100644 very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.github/workflows/{{project_name.snakeCase()}}.yaml create mode 100644 very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.gitignore create mode 100644 very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/README.md create mode 100644 very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/analysis_options.yaml create mode 100644 very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/bin/{{executable_name.snakeCase()}}.dart create mode 100644 very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/coverage_badge.svg create mode 100644 very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/dart_test.yaml create mode 100644 very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/lib/src/command_runner.dart create mode 100644 very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/lib/src/commands/commands.dart create mode 100644 very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/lib/src/commands/sample_command.dart create mode 100644 very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/lib/src/commands/update_command.dart create mode 100644 very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/lib/src/version.dart create mode 100644 very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/lib/{{project_name.snakeCase()}}.dart create mode 100644 very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/pubspec.yaml create mode 100644 very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/test/ensure_build_test.dart create mode 100644 very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/test/src/command_runner_test.dart create mode 100644 very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/test/src/commands/sample_command_test.dart create mode 100644 very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/test/src/commands/update_command_test.dart create mode 100644 very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/{{#publishable}}CHANGELOG.md{{/publishable}} create mode 100644 very_good_dart_cli/analysis_options.yaml create mode 100644 very_good_dart_cli/brick.yaml create mode 100644 very_good_dart_cli/config.json create mode 100644 very_good_dart_cli/hooks/.gitignore create mode 100644 very_good_dart_cli/hooks/analysis_options.yaml create mode 100644 very_good_dart_cli/hooks/post_gen.dart create mode 100644 very_good_dart_cli/hooks/pubspec.yaml create mode 100644 very_good_dart_cli/hooks/test/post_gen_test.dart create mode 100644 very_good_wear_app/analysis_options.yaml diff --git a/.github/workflows/very_good_dart_cli.yaml b/.github/workflows/very_good_dart_cli.yaml new file mode 100644 index 0000000..cb1dcc4 --- /dev/null +++ b/.github/workflows/very_good_dart_cli.yaml @@ -0,0 +1,68 @@ +name: very_good_dart_cli + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + paths: + - .github/workflows/very_good_dart_cli.yaml + - "very_good_dart_cli/**" + branches: + - main + pull_request: + paths: + - .github/workflows/very_good_dart_cli.yaml + - "very_good_dart_cli/**" + branches: + - main + +jobs: + brick: + runs-on: ubuntu-latest + + strategy: + matrix: + dart-version: + # The minimum Dart SDK version supported by the package, + # refer to https://docs.flutter.dev/development/tools/sdk/releases. + - "3.0.0" + - "stable" + + steps: + - name: πŸ“š Git Checkout + uses: actions/checkout@v4 + + - name: 🎯 Setup Dart + uses: dart-lang/setup-dart@v1 + with: + sdk: ${{ matrix.dart-version }} + + - name: 🧱 Mason Make + run: | + dart pub global activate mason_cli + mason get + mason make very_good_dart_cli -c very_good_dart_cli/config.json -o output --on-conflict overwrite + + - name: πŸ“¦ Install Dependencies + run: dart pub get --directory output/test_cli + + - name: ✨ Check Formatting + run: dart format --set-exit-if-changed output/test_cli + + - name: πŸ•΅οΈ Analyze + run: dart analyze --fatal-infos --fatal-warnings output/test_cli + + - name: πŸ§ͺ Run Tests + run: | + dart pub global activate coverage 1.2.0 + cd output/test_cli + dart test -j 4 --coverage=coverage + dart pub global run coverage:format_coverage --lcov --check-ignore --in=coverage --out=coverage/lcov.info --packages=.dart_tool/package_config.json --report-on="lib" + cd ../../ + + - name: πŸ“Š Check Code Coverage + uses: VeryGoodOpenSource/very_good_coverage@v2 + with: + path: output/test_cli/coverage/lcov.info diff --git a/.github/workflows/very_good_dart_cli_hooks.yaml b/.github/workflows/very_good_dart_cli_hooks.yaml new file mode 100644 index 0000000..90e67d8 --- /dev/null +++ b/.github/workflows/very_good_dart_cli_hooks.yaml @@ -0,0 +1,21 @@ +name: very_good_dart_cli_hooks + +on: + pull_request: + paths: + - ".github/workflows/very_good_dart_cli_hooks.yaml" + - "very_good_dart_cli/hooks/**" + push: + branches: + - main + paths: + - ".github/workflows/very_good_dart_cli_hooks.yaml" + - "very_good_dart_cli/hooks/**" + +jobs: + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/dart_package.yml@v1 + with: + working_directory: "very_good_dart_cli/hooks" + analyze_directories: "test" + report_on: "post_gen.dart" diff --git a/mason.yaml b/mason.yaml index 37b0717..fc6f6a3 100644 --- a/mason.yaml +++ b/mason.yaml @@ -2,4 +2,6 @@ bricks: very_good_core: path: very_good_core very_good_wear_app: - path: very_good_wear_app \ No newline at end of file + path: very_good_wear_app + very_good_dart_cli: + path: very_good_dart_cli \ No newline at end of file diff --git a/very_good_core/analysis_options.yaml b/very_good_core/analysis_options.yaml index aa45c4f..41ffce1 100644 --- a/very_good_core/analysis_options.yaml +++ b/very_good_core/analysis_options.yaml @@ -1,2 +1,3 @@ analyzer: - exclude: [brick/__brick__/**] + exclude: + - __brick__/** diff --git a/very_good_dart_cli/.gitignore b/very_good_dart_cli/.gitignore new file mode 100644 index 0000000..c2fd2cc --- /dev/null +++ b/very_good_dart_cli/.gitignore @@ -0,0 +1,15 @@ +# Files and directories created by pub +.dart_tool/ +.packages +pubspec.lock + +# Android studio and IntelliJ +.idea + +# Files and directories created by mason +.mason/ +mason-lock.json +output/ + +# Files and directories created by MacOS +.DS_Store \ No newline at end of file diff --git a/very_good_dart_cli/CHANGELOG.md b/very_good_dart_cli/CHANGELOG.md new file mode 100644 index 0000000..c0121e7 --- /dev/null +++ b/very_good_dart_cli/CHANGELOG.md @@ -0,0 +1,73 @@ +# 0.5.1 + +- feat: upgrade to very_good_analysis ^5.1.0 + +# 0.5.0 + +- feat: update very good analysis to v5 and fix patch +- feat: bump Dart SDK to >=3.0.0 <4.0.0 + +# 0.4.0 + +- feat!: bump min Dart SDK to 2.19.0 + +# 0.3.3 + +- chore: support dart sdk 2.18.0 + +# 0.3.2 + +- feat: update workflows, add spellcheck +- fix: remove unused pub updater from test + +# 0.3.1 + +- feat: upgrade to `very_good_analysis 4.0.0` + +# 0.3.0 + +- feat: add completion + +# 0.2.3 + +- feat: upgrade to Dart 2.19 + +# 0.2.2 + +- fix: update silently fails when the sdk doesnt support most recent version + +# 0.2.1 + +- fix: windows path resolution +- feat: add ci concurrency group to workflow + +# 0.2.0 + +- **BREAKING** feat: add `publishable` flag (defaults to `false`) + +# 0.1.4 + +- docs: remove copyright header and license from generated code + +# 0.1.3 + +- fix: update prompt on update + +# 0.1.2 + +- chore(deps): upgrade to mason_logger 0.2.0 +- fix: use available global constant for executable name + +# 0.1.1 + +- feat: upgrade to very_good_analysis 3.1.0 + +# 0.1.0 + +- feat: upgrade to Dart 2.18 +- feat: upgrade to very_good_analysis 3.0.2 +- feat: add dependabot integration + +# 0.0.1 + +- feat: initial release πŸŽ‰ diff --git a/very_good_dart_cli/CONTRIBUTING.md b/very_good_dart_cli/CONTRIBUTING.md new file mode 100644 index 0000000..43ef01f --- /dev/null +++ b/very_good_dart_cli/CONTRIBUTING.md @@ -0,0 +1,110 @@ +# πŸ¦„ Contributing to Very Good Dart CLI + +This CONTRIBUTING file is for a developer that wants to modify or contribute to the Very Good Dart CLI template. If you are interested in solely generating a project using Very Good Dart CLI, please refer to the [README](README.md) file. + +## Opening an issue + +We highly recommend [creating an issue][bug_report_link] if you have found a bug, want to suggest a feature, or recommend a change. Please do not immediately open a pull request. Opening an issue first allows us to reach an agreement on a fix before you put significant effort into a pull request. + +When reporting a bug, please use the built-in [Bug Report](https://github.com/VeryGoodOpenSource/very_good_dart_cli/issues/new/choose) template and provide as much information as possible including detailed reproduction steps. Once one of the package maintainers has reviewed the issue and we reach an agreement on the fix, open a pull request. + +[bug_report_link]: https://github.com/VeryGoodOpenSource/very_good_dart_cli/issues + +## Developing for Very Good Dart CLI + +To develop for Very Good Dart CLI you will need to become familiar with Very Good Ventures processes and conventions: + +### Setting up your local development environment + +1. Install a valid [Dart SDK](https://dart.dev/get-dart) in your local environment. If you have Flutter installed you'll have the Dart SDK. Compatible Flutter SDK versions with Very Good Dart CLI can be found within the [Flutter release archive](https://docs.flutter.dev/release/archive), ensure it has a Dart version compatible with [Very Good Dart CLI's Dart version constraint](). + +2. Install [Mason](https://github.com/felangel/mason/tree/master/packages/mason_cli#installation) in your local environment: + +```sh +# 🎯 Activate Mason from https://pub.dev +dart pub global activate mason_cli +``` + +πŸ’‘ **Note**: If you're not familiar with Mason, read its [documentation](https://docs.brickhub.dev/) or watch our [Mason Demonstration](https://www.youtube.com/watch?v=G4PTjA6tpTU). + +3. Get all bricks in Very Good Dart CLI's `mason.yaml`: + +```sh +# πŸ“‚ Get all bricks in Very Good Dart CLI (from project root): +mason get +``` + +4. Generate the template locally: + +```sh +# 🧱 Generate a project using the local Very Good Dart CLI version +mason make very_good_dart_cli --config-path brick/config.json --output-dir output --watch +``` + +This will generate a project using Very Good Dart CLI under [`output`](output) with the variables specified by the [configuration](brick/config.json) file. When any file under [`__brick__`](brick/__brick__/) is changed the project will be regenerated. + +### Creating a Pull Request + +Before creating a Pull Request please: + +1. [Fork](https://docs.github.com/en/get-started/quickstart/contributing-to-projects) the [GitHub repository](https://github.com/VeryGoodOpenSource/very_good_dart_cli) and create your branch from `main`: + +```sh +# πŸͺ΅ Branch from `main` +git branch +git checkout +``` + +Where `` is an appropriate name describing your change. + +2. Get all bricks in Very Good Dart CLI's `mason.yaml`: + +```sh +# πŸ“‚ Get all bricks in Very Good Dart CLI (from project root): +mason get +``` + +3. Generate the template locally: + +```sh +# 🧱 Generate a project using the local Very Good Dart CLI brick (from project root) +mason make very_good_dart_cli --config-path brick/config.json --output-dir output +``` + +4. Add tests! Pull Requests without 100% test coverage will **not** be merged. If you're unsure on how to do so watch our [Testing Fundamentals Course](https://www.youtube.com/watch?v=M_eZg-X789w&list=PLprI2satkVdFwpxo_bjFkCxXz5RluG8FY). + +```sh +# πŸ“Š Generate coverage report (from output/test_cli) +dart test -j 4 --coverage=coverage --platform="vm" +dart pub global run coverage:format_coverage --lcov --check-ignore --in=coverage --out=coverage/lcov.info --package="." --report-on="lib" + +# πŸ•ΈοΈ Generate a readable HTML website (from output/test_cli) +genhtml -o coverage/html coverage/lcov.info + +# πŸ‘€ Open the coverage report HTML website (from output/test_cli) +open coverage/html/index.html +``` + +5. Ensure the generated project is well formatted: + +```sh +# 🧼 Run Dart's formatter (from output/test_cli) +dart format lib test --set-exit-if-changed +``` + +6. Ensure the generated project has no analysis issues: + +```sh +# πŸ” Run Dart's analyzer (from output/test_cli) +dart analyze --fatal-infos --fatal-warnings . +``` + +πŸ’‘ **Note**: Our repositories use [Very Good Analysis](https://github.com/VeryGoodOpenSource/very_good_analysis). + +7. Ensure you have a meaningful [semantic](https://www.conventionalcommits.org/en/v1.0.0) commit message. + +8. Create the Pull Request with a meaningful description, linking to the original issue where possible. + +9. Verify that all [status checks](https://github.com/VeryGoodOpenSource/very_good_dart_cli /actions/) are passing for your Pull Request once they have been approved to run by a maintainer. + +πŸ’‘ **Note**: While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional work, tests, or other changes before your pull request can be accepted. diff --git a/very_good_dart_cli/LICENSE b/very_good_dart_cli/LICENSE new file mode 100644 index 0000000..bba8e50 --- /dev/null +++ b/very_good_dart_cli/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Very Good Ventures + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/very_good_dart_cli/README.md b/very_good_dart_cli/README.md new file mode 100644 index 0000000..c52766c --- /dev/null +++ b/very_good_dart_cli/README.md @@ -0,0 +1,67 @@ +# Very Good Dart CLI + +[![Very Good Ventures][logo_white]][very_good_ventures_link_dark] + +Developed with πŸ’™ by [Very Good Ventures][very_good_ventures_link] πŸ¦„ + +[![License: MIT][license_badge]][license_link] +[![Powered by Mason](https://img.shields.io/endpoint?url=https%3A%2F%2Ftinyurl.com%2Fmason-badge)](https://github.com/felangel/mason) + +A Very Good Dart CLI created by Very Good Ventures πŸ¦„. + +## What's Included ✨ + +- βœ… Argument parsing +- βœ… A sample command +- βœ… Colorful logging +- βœ… Automatic update prompts +- βœ… Tab completion for bash and zsh +- βœ… GitHub Workflow powered by [Very Good Workflows][very_good_workflows_link] +- βœ… Strict lint rules powered by [Very Good Analysis][very_good_analysis_link] +- βœ… Dependabot Integration +- βœ… 100% Test Coverage +- βœ… Fully Documented Public API +- βœ… MIT License +- βœ… Changelog + +## Output πŸ“¦ + +```sh +β”œβ”€β”€ .github +β”‚ β”œβ”€β”€ dependabot.yaml +β”‚ └── workflows +β”‚ └── my_cli.yaml +β”œβ”€β”€ .gitignore +β”œβ”€β”€ LICENSE +β”œβ”€β”€ README.md +β”œβ”€β”€ analysis_options.yaml +β”œβ”€β”€ bin +β”‚ └── my_cli.dart +β”œβ”€β”€ coverage_badge.svg +β”œβ”€β”€ dart_test.yaml +β”œβ”€β”€ lib +β”‚ β”œβ”€β”€ my_cli.dart +β”‚ └── src +β”‚ β”œβ”€β”€ command_runner.dart +β”‚ β”œβ”€β”€ commands +β”‚ β”‚ β”œβ”€β”€ commands.dart +β”‚ β”‚ β”œβ”€β”€ sample_command.dart +β”‚ β”‚ └── update_command.dart +β”‚ └── version.dart +β”œβ”€β”€ pubspec.yaml +└── test + β”œβ”€β”€ ensure_build_test.dart + └── src + β”œβ”€β”€ command_runner_test.dart + └── commands + β”œβ”€β”€ sample_command_test.dart + └── update_command_test.dart +``` + +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[logo_white]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_white.png#gh-dark-mode-only +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis +[very_good_ventures_link_dark]: https://verygood.ventures#gh-dark-mode-only +[very_good_ventures_link]: https://verygood.ventures +[very_good_workflows_link]: https://github.com/VeryGoodOpenSource/very_good_workflows diff --git a/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.github/ISSUE_TEMPLATE/ config.yml b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.github/ISSUE_TEMPLATE/ config.yml new file mode 100644 index 0000000..3ba13e0 --- /dev/null +++ b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.github/ISSUE_TEMPLATE/ config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.github/ISSUE_TEMPLATE/bug_report.md b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..50a4c7b --- /dev/null +++ b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug Report +about: Create a report to help us improve +title: "fix: " +labels: bug +--- + +**Description** + +A clear and concise description of what the bug is. + +**Steps To Reproduce** + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected Behavior** + +A clear and concise description of what you expected to happen. + +**Screenshots** + +If applicable, add screenshots to help explain your problem. + +**Additional Context** + +Add any other context about the problem here. diff --git a/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.github/ISSUE_TEMPLATE/feature_request.md b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..ddd2fcc --- /dev/null +++ b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,18 @@ +--- +name: Feature Request +about: A new feature to be added to the project +title: "feat: " +labels: feature +--- + +**Description** + +Clearly describe what you are looking to add. The more context the better. + +**Requirements** + +- [ ] Checklist of requirements to be fulfilled + +**Additional Context** + +Add any other context or screenshots about the feature request go here. diff --git a/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.github/PULL_REQUEST_TEMPLATE.md b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..6b9372e --- /dev/null +++ b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,23 @@ + + +## Description + + + +## Type of Change + + + +- [ ] ✨ New feature (non-breaking change which adds functionality) +- [ ] πŸ› οΈ Bug fix (non-breaking change which fixes an issue) +- [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change) +- [ ] 🧹 Code refactor +- [ ] βœ… Build configuration change +- [ ] πŸ“ Documentation +- [ ] πŸ—‘οΈ Chore diff --git a/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.github/cspell.json b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.github/cspell.json new file mode 100644 index 0000000..1b78898 --- /dev/null +++ b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.github/cspell.json @@ -0,0 +1,20 @@ +{ + "version": "0.2", + "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", + "dictionaries": ["vgv_allowed", "vgv_forbidden"], + "dictionaryDefinitions": [ + { + "name": "vgv_allowed", + "path": "https://raw.githubusercontent.com/verygoodopensource/very_good_dictionaries/main/allowed.txt", + "description": "Allowed VGV Spellings" + }, + { + "name": "vgv_forbidden", + "path": "https://raw.githubusercontent.com/verygoodopensource/very_good_dictionaries/main/forbidden.txt", + "description": "Forbidden VGV Spellings" + } + ], + "useGitignore": true, + "words": [ + ] +} diff --git a/.github/dependabot.yaml b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.github/dependabot.yaml similarity index 79% rename from .github/dependabot.yaml rename to very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.github/dependabot.yaml index 60e4880..63b035c 100644 --- a/.github/dependabot.yaml +++ b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.github/dependabot.yaml @@ -1,10 +1,11 @@ version: 2 +enable-beta-ecosystems: true updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" - package-ecosystem: "pub" - directory: "very_good_core/brick/hooks" + directory: "/" schedule: interval: "daily" diff --git a/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.github/workflows/{{project_name.snakeCase()}}.yaml b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.github/workflows/{{project_name.snakeCase()}}.yaml new file mode 100644 index 0000000..e4d2cbd --- /dev/null +++ b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.github/workflows/{{project_name.snakeCase()}}.yaml @@ -0,0 +1,55 @@ +name: {{project_name.snakeCase()}} + +concurrency: + group: ${{#mustacheCase}}github.workflow{{/mustacheCase}}-${{#mustacheCase}}github.ref{{/mustacheCase}} + cancel-in-progress: true + +on: + pull_request: + paths: + - ".github/workflows/{{project_name.snakeCase()}}.yaml" + - "lib/**" + - "test/**" + - "pubspec.yaml" + push: + branches: + - main + paths: + - ".github/workflows/{{project_name.snakeCase()}}.yaml" + - "lib/**" + - "test/**" + - "pubspec.yaml" + +jobs: + semantic-pull-request: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1 + + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/dart_package.yml@v1 + + spell-check: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/spell_check.yml@v1 + with: + includes: | + **/*.md + !brick/**/*.md + .*/**/*.md + modified_files_only: false + + verify-version: + runs-on: ubuntu-latest + steps: + - name: πŸ“š Git Checkout + uses: actions/checkout@v2 + + - name: 🎯 Setup Dart + uses: dart-lang/setup-dart@v1 + with: + sdk: "stable" + + - name: πŸ“¦ Install Dependencies + run: | + dart pub get + + - name: πŸ”Ž Verify version + run: dart run test --run-skipped -t version-verify diff --git a/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.gitignore b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.gitignore new file mode 100644 index 0000000..9f6ee8a --- /dev/null +++ b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/.gitignore @@ -0,0 +1,15 @@ +# See https://www.dartlang.org/guides/libraries/private-files + +# Files and directories created by pub +.dart_tool/ +.packages +build/ +pubspec.lock + +# Files generated during tests +.test_coverage.dart +coverage/ +.test_runner.dart + +# Android studio and IntelliJ +.idea \ No newline at end of file diff --git a/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/README.md b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/README.md new file mode 100644 index 0000000..b2ef29a --- /dev/null +++ b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/README.md @@ -0,0 +1,71 @@ +## {{project_name.snakeCase()}} + +![coverage][coverage_badge] +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![License: MIT][license_badge]][license_link] + +Generated by the [Very Good CLI][very_good_cli_link] πŸ€– + +{{description}}. + +--- + +## Getting Started πŸš€ + +If the CLI application is available on [pub](https://pub.dev), activate globally via: + +```sh +dart pub global activate {{project_name.snakeCase()}} +``` + +Or locally via: + +```sh +dart pub global activate --source=path +``` + +## Usage + +```sh +# Sample command +$ {{executable_name.snakeCase()}} sample + +# Sample command option +$ {{executable_name.snakeCase()}} sample --cyan + +# Show CLI version +$ {{executable_name.snakeCase()}} --version + +# Show usage help +$ {{executable_name.snakeCase()}} --help +``` + +## Running Tests with coverage πŸ§ͺ + +To run all unit tests use the following command: + +```sh +$ dart pub global activate coverage 1.2.0 +$ dart test --coverage=coverage +$ dart pub global run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info +``` + +To view the generated coverage report you can use [lcov](https://github.com/linux-test-project/lcov) +. + +```sh +# Generate Coverage Report +$ genhtml coverage/lcov.info -o coverage/ + +# Open Coverage Report +$ open coverage/index.html +``` + +--- + +[coverage_badge]: coverage_badge.svg +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis +[very_good_cli_link]: https://github.com/VeryGoodOpenSource/very_good_cli \ No newline at end of file diff --git a/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/analysis_options.yaml b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/analysis_options.yaml new file mode 100644 index 0000000..fa798a8 --- /dev/null +++ b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:very_good_analysis/analysis_options.5.1.0.yaml +linter: + rules: + public_member_api_docs: false diff --git a/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/bin/{{executable_name.snakeCase()}}.dart b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/bin/{{executable_name.snakeCase()}}.dart new file mode 100644 index 0000000..305358c --- /dev/null +++ b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/bin/{{executable_name.snakeCase()}}.dart @@ -0,0 +1,18 @@ +import 'dart:io'; + +import 'package:{{project_name.snakeCase()}}/src/command_runner.dart'; + +Future main(List args) async { + await _flushThenExit(await {{project_name.pascalCase()}}CommandRunner().run(args)); +} + +/// Flushes the stdout and stderr streams, then exits the program with the given +/// status code. +/// +/// This returns a Future that will never complete, since the program will have +/// exited already. This is useful to prevent Future chains from proceeding +/// after you've decided to exit. +Future _flushThenExit(int status) { + return Future.wait([stdout.close(), stderr.close()]) + .then((_) => exit(status)); +} diff --git a/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/coverage_badge.svg b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/coverage_badge.svg new file mode 100644 index 0000000..88bfadf --- /dev/null +++ b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/coverage_badge.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + coverage + coverage + 100% + 100% + + \ No newline at end of file diff --git a/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/dart_test.yaml b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/dart_test.yaml new file mode 100644 index 0000000..2f46c7e --- /dev/null +++ b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/dart_test.yaml @@ -0,0 +1,3 @@ +tags: + version-verify: + skip: "Should only be run during pull request. Verifies if version file is updated." \ No newline at end of file diff --git a/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/lib/src/command_runner.dart b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/lib/src/command_runner.dart new file mode 100644 index 0000000..072e223 --- /dev/null +++ b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/lib/src/command_runner.dart @@ -0,0 +1,144 @@ +import 'package:args/args.dart'; +import 'package:args/command_runner.dart'; +import 'package:cli_completion/cli_completion.dart'; +import 'package:mason_logger/mason_logger.dart'; +import 'package:{{project_name.snakeCase()}}/src/commands/commands.dart'; +import 'package:{{project_name.snakeCase()}}/src/version.dart'; +import 'package:pub_updater/pub_updater.dart'; + +const executableName = '{{executable_name.snakeCase()}}'; +const packageName = '{{project_name.snakeCase()}}'; +const description = '{{description}}'; + +/// {@template {{project_name.snakeCase()}}_command_runner} +/// A [CommandRunner] for the CLI. +/// +/// ``` +/// $ {{executable_name.snakeCase()}} --version +/// ``` +/// {@endtemplate} +class {{project_name.pascalCase()}}CommandRunner extends CompletionCommandRunner { + /// {@macro {{project_name.snakeCase()}}_command_runner} + {{project_name.pascalCase()}}CommandRunner({ + Logger? logger, + PubUpdater? pubUpdater, + }) : _logger = logger ?? Logger(), + _pubUpdater = pubUpdater ?? PubUpdater(), + super(executableName, description) { + // Add root options and flags + argParser + ..addFlag( + 'version', + abbr: 'v', + negatable: false, + help: 'Print the current version.', + ) + ..addFlag( + 'verbose', + help: 'Noisy logging, including all shell commands executed.', + ); + + // Add sub commands + addCommand(SampleCommand(logger: _logger)); + addCommand(UpdateCommand(logger: _logger, pubUpdater: _pubUpdater)); + } + + @override + void printUsage() => _logger.info(usage); + + final Logger _logger; + final PubUpdater _pubUpdater; + + @override + Future run(Iterable args) async { + try { + final topLevelResults = parse(args); + if (topLevelResults['verbose'] == true) { + _logger.level = Level.verbose; + } + return await runCommand(topLevelResults) ?? ExitCode.success.code; + } on FormatException catch (e, stackTrace) { + // On format errors, show the commands error message, root usage and + // exit with an error code + _logger + ..err(e.message) + ..err('$stackTrace') + ..info('') + ..info(usage); + return ExitCode.usage.code; + } on UsageException catch (e) { + // On usage errors, show the commands usage message and + // exit with an error code + _logger + ..err(e.message) + ..info('') + ..info(e.usage); + return ExitCode.usage.code; + } + } + + @override + Future runCommand(ArgResults topLevelResults) async { + // Fast track completion command + if (topLevelResults.command?.name == 'completion') { + await super.runCommand(topLevelResults); + return ExitCode.success.code; + } + + // Verbose logs + _logger + ..detail('Argument information:') + ..detail(' Top level options:'); + for (final option in topLevelResults.options) { + if (topLevelResults.wasParsed(option)) { + _logger.detail(' - $option: ${topLevelResults[option]}'); + } + } + if (topLevelResults.command != null) { + final commandResult = topLevelResults.command!; + _logger + ..detail(' Command: ${commandResult.name}') + ..detail(' Command options:'); + for (final option in commandResult.options) { + if (commandResult.wasParsed(option)) { + _logger.detail(' - $option: ${commandResult[option]}'); + } + } + } + + // Run the command or show version + final int? exitCode; + if (topLevelResults['version'] == true) { + _logger.info(packageVersion); + exitCode = ExitCode.success.code; + } else { + exitCode = await super.runCommand(topLevelResults); + } + + // Check for updates + if (topLevelResults.command?.name != UpdateCommand.commandName) { + await _checkForUpdates(); + } + + return exitCode; + } + + /// Checks if the current version (set by the build runner on the + /// version.dart file) is the most recent one. If not, show a prompt to the + /// user. + Future _checkForUpdates() async { + try { + final latestVersion = await _pubUpdater.getLatestVersion(packageName); + final isUpToDate = packageVersion == latestVersion; + if (!isUpToDate) { + _logger + ..info('') + ..info( + ''' +${lightYellow.wrap('Update available!')} ${lightCyan.wrap(packageVersion)} \u2192 ${lightCyan.wrap(latestVersion)} +Run ${lightCyan.wrap('$executableName update')} to update''', + ); + } + } catch (_) {} + } +} diff --git a/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/lib/src/commands/commands.dart b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/lib/src/commands/commands.dart new file mode 100644 index 0000000..eec317e --- /dev/null +++ b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/lib/src/commands/commands.dart @@ -0,0 +1,2 @@ +export 'sample_command.dart'; +export 'update_command.dart'; diff --git a/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/lib/src/commands/sample_command.dart b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/lib/src/commands/sample_command.dart new file mode 100644 index 0000000..cc93cd8 --- /dev/null +++ b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/lib/src/commands/sample_command.dart @@ -0,0 +1,39 @@ +import 'package:args/command_runner.dart'; +import 'package:mason_logger/mason_logger.dart'; + +/// {@template sample_command} +/// +/// `{{executable_name.snakeCase()}} sample` +/// A [Command] to exemplify a sub command +/// {@endtemplate} +class SampleCommand extends Command { + /// {@macro sample_command} + SampleCommand({ + required Logger logger, + }) : _logger = logger { + argParser.addFlag( + 'cyan', + abbr: 'c', + help: 'Prints the same joke, but in cyan', + negatable: false, + ); + } + + @override + String get description => 'A sample sub command that just prints one joke'; + + @override + String get name => 'sample'; + + final Logger _logger; + + @override + Future run() async { + var output = 'Which unicorn has a cold? The Achoo-nicorn!'; + if (argResults?['cyan'] == true) { + output = lightCyan.wrap(output)!; + } + _logger.info(output); + return ExitCode.success.code; + } +} diff --git a/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/lib/src/commands/update_command.dart b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/lib/src/commands/update_command.dart new file mode 100644 index 0000000..b16aa9e --- /dev/null +++ b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/lib/src/commands/update_command.dart @@ -0,0 +1,74 @@ +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:mason_logger/mason_logger.dart'; +import 'package:{{project_name.snakeCase()}}/src/command_runner.dart'; +import 'package:{{project_name.snakeCase()}}/src/version.dart'; +import 'package:pub_updater/pub_updater.dart'; + +/// {@template update_command} +/// A command which updates the CLI. +/// {@endtemplate} +class UpdateCommand extends Command { + /// {@macro update_command} + UpdateCommand({ + required Logger logger, + PubUpdater? pubUpdater, + }) : _logger = logger, + _pubUpdater = pubUpdater ?? PubUpdater(); + + final Logger _logger; + final PubUpdater _pubUpdater; + + @override + String get description => 'Update the CLI.'; + + static const String commandName = 'update'; + + @override + String get name => commandName; + + @override + Future run() async { + final updateCheckProgress = _logger.progress('Checking for updates'); + late final String latestVersion; + try { + latestVersion = await _pubUpdater.getLatestVersion(packageName); + } catch (error) { + updateCheckProgress.fail(); + _logger.err('$error'); + return ExitCode.software.code; + } + updateCheckProgress.complete('Checked for updates'); + + final isUpToDate = packageVersion == latestVersion; + if (isUpToDate) { + _logger.info('CLI is already at the latest version.'); + return ExitCode.success.code; + } + + final updateProgress = _logger.progress('Updating to $latestVersion'); + + late final ProcessResult result; + try { + result = await _pubUpdater.update( + packageName: packageName, + versionConstraint: latestVersion, + ); + } catch (error) { + updateProgress.fail(); + _logger.err('$error'); + return ExitCode.software.code; + } + + if (result.exitCode != ExitCode.success.code) { + updateProgress.fail(); + _logger.err('Error updating CLI: ${result.stderr}'); + return ExitCode.software.code; + } + + updateProgress.complete('Updated to $latestVersion'); + + return ExitCode.success.code; + } +} diff --git a/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/lib/src/version.dart b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/lib/src/version.dart new file mode 100644 index 0000000..67a7647 --- /dev/null +++ b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/lib/src/version.dart @@ -0,0 +1,2 @@ +// Generated code. Do not modify. +const packageVersion = '0.0.1'; diff --git a/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/lib/{{project_name.snakeCase()}}.dart b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/lib/{{project_name.snakeCase()}}.dart new file mode 100644 index 0000000..337dd02 --- /dev/null +++ b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/lib/{{project_name.snakeCase()}}.dart @@ -0,0 +1,10 @@ +/// {{project_name.snakeCase()}}, {{description}} +/// +/// ```sh +/// # activate {{project_name.snakeCase()}} +/// dart pub global activate {{project_name.snakeCase()}} +/// +/// # see usage +/// {{executable_name.snakeCase()}} --help +/// ``` +library {{project_name.snakeCase()}}; diff --git a/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/pubspec.yaml b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/pubspec.yaml new file mode 100644 index 0000000..f9ad5d8 --- /dev/null +++ b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/pubspec.yaml @@ -0,0 +1,24 @@ +name: {{project_name.snakeCase()}} +description: {{description}} +version: 0.0.1 +{{^publishable}}publish_to: none{{/publishable}} + +environment: + sdk: ">=3.0.0 <4.0.0" + +dependencies: + args: ^2.4.1 + cli_completion: ^0.3.0 + mason_logger: ^0.2.5 + pub_updater: ^0.4.0 + +dev_dependencies: + build_runner: ^2.4.4 + build_verify: ^3.1.0 + build_version: ^2.1.1 + mocktail: ^1.0.0 + test: ^1.24.6 + very_good_analysis: ^5.1.0 + +executables: + {{executable_name.snakeCase()}}: diff --git a/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/test/ensure_build_test.dart b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/test/ensure_build_test.dart new file mode 100644 index 0000000..4b619ed --- /dev/null +++ b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/test/ensure_build_test.dart @@ -0,0 +1,9 @@ +@Tags(['version-verify']) +library ensure_build_test; + +import 'package:build_verify/build_verify.dart'; +import 'package:test/test.dart'; + +void main() { + test('ensure_build', expectBuildClean); +} diff --git a/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/test/src/command_runner_test.dart b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/test/src/command_runner_test.dart new file mode 100644 index 0000000..4e0ffe0 --- /dev/null +++ b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/test/src/command_runner_test.dart @@ -0,0 +1,174 @@ +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:cli_completion/cli_completion.dart'; +import 'package:mason_logger/mason_logger.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:{{project_name.snakeCase()}}/src/command_runner.dart'; +import 'package:{{project_name.snakeCase()}}/src/version.dart'; +import 'package:pub_updater/pub_updater.dart'; +import 'package:test/test.dart'; + +class _MockLogger extends Mock implements Logger {} + +class _MockProgress extends Mock implements Progress {} + +class _MockPubUpdater extends Mock implements PubUpdater {} + +const latestVersion = '0.0.0'; + +final updatePrompt = ''' +${lightYellow.wrap('Update available!')} ${lightCyan.wrap(packageVersion)} \u2192 ${lightCyan.wrap(latestVersion)} +Run ${lightCyan.wrap('$executableName update')} to update'''; + +void main() { + group('{{project_name.pascalCase()}}CommandRunner', () { + late PubUpdater pubUpdater; + late Logger logger; + late {{project_name.pascalCase()}}CommandRunner commandRunner; + + setUp(() { + pubUpdater = _MockPubUpdater(); + + when( + () => pubUpdater.getLatestVersion(any()), + ).thenAnswer((_) async => packageVersion); + + logger = _MockLogger(); + + commandRunner = {{project_name.pascalCase()}}CommandRunner( + logger: logger, + pubUpdater: pubUpdater, + ); + }); + + test('shows update message when newer version exists', () async { + when( + () => pubUpdater.getLatestVersion(any()), + ).thenAnswer((_) async => latestVersion); + + final result = await commandRunner.run(['--version']); + expect(result, equals(ExitCode.success.code)); + verify(() => logger.info(updatePrompt)).called(1); + }); + + test( + 'Does not show update message when the shell calls the ' + 'completion command', + () async { + when( + () => pubUpdater.getLatestVersion(any()), + ).thenAnswer((_) async => latestVersion); + + final result = await commandRunner.run(['completion']); + expect(result, equals(ExitCode.success.code)); + verifyNever(() => logger.info(updatePrompt)); + }, + ); + + test('does not show update message when using update command', () async { + when( + () => pubUpdater.getLatestVersion(any()), + ).thenAnswer((_) async => latestVersion); + when( + () => pubUpdater.update( + packageName: packageName, + versionConstraint: any(named: 'versionConstraint'), + ), + ).thenAnswer( + (_) async => ProcessResult(0, ExitCode.success.code, null, null), + ); + when( + () => pubUpdater.isUpToDate( + packageName: any(named: 'packageName'), + currentVersion: any(named: 'currentVersion'), + ), + ).thenAnswer((_) async => true); + + final progress = _MockProgress(); + final progressLogs = []; + when(() => progress.complete(any())).thenAnswer((_) { + final message = _.positionalArguments.elementAt(0) as String?; + if (message != null) progressLogs.add(message); + }); + when(() => logger.progress(any())).thenReturn(progress); + + final result = await commandRunner.run(['update']); + expect(result, equals(ExitCode.success.code)); + verifyNever(() => logger.info(updatePrompt)); + }); + + test('can be instantiated without an explicit analytics/logger instance', + () { + final commandRunner = {{project_name.pascalCase()}}CommandRunner(); + expect(commandRunner, isNotNull); + expect(commandRunner, isA>()); + }); + + test('handles FormatException', () async { + const exception = FormatException('oops!'); + var isFirstInvocation = true; + when(() => logger.info(any())).thenAnswer((_) { + if (isFirstInvocation) { + isFirstInvocation = false; + throw exception; + } + }); + final result = await commandRunner.run(['--version']); + expect(result, equals(ExitCode.usage.code)); + verify(() => logger.err(exception.message)).called(1); + verify(() => logger.info(commandRunner.usage)).called(1); + }); + + test('handles UsageException', () async { + final exception = UsageException('oops!', 'exception usage'); + var isFirstInvocation = true; + when(() => logger.info(any())).thenAnswer((_) { + if (isFirstInvocation) { + isFirstInvocation = false; + throw exception; + } + }); + final result = await commandRunner.run(['--version']); + expect(result, equals(ExitCode.usage.code)); + verify(() => logger.err(exception.message)).called(1); + verify(() => logger.info('exception usage')).called(1); + }); + + group('--version', () { + test('outputs current version', () async { + final result = await commandRunner.run(['--version']); + expect(result, equals(ExitCode.success.code)); + verify(() => logger.info(packageVersion)).called(1); + }); + }); + + group('--verbose', () { + test('enables verbose logging', () async { + final result = await commandRunner.run(['--verbose']); + expect(result, equals(ExitCode.success.code)); + + verify(() => logger.detail('Argument information:')).called(1); + verify(() => logger.detail(' Top level options:')).called(1); + verify(() => logger.detail(' - verbose: true')).called(1); + verifyNever(() => logger.detail(' Command options:')); + }); + + test('enables verbose logging for sub commands', () async { + final result = await commandRunner.run([ + '--verbose', + 'sample', + '--cyan', + ]); + expect(result, equals(ExitCode.success.code)); + + verify(() => logger.detail('Argument information:')).called(1); + verify(() => logger.detail(' Top level options:')).called(1); + verify(() => logger.detail(' - verbose: true')).called(1); + verify(() => logger.detail(' Command: sample')).called(1); + verify(() => logger.detail(' Command options:')).called(1); + verify(() => logger.detail(' - cyan: true')).called(1); + }); + }); + }); +} diff --git a/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/test/src/commands/sample_command_test.dart b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/test/src/commands/sample_command_test.dart new file mode 100644 index 0000000..e7d902d --- /dev/null +++ b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/test/src/commands/sample_command_test.dart @@ -0,0 +1,58 @@ +import 'package:mason_logger/mason_logger.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:{{project_name.snakeCase()}}/src/command_runner.dart'; +import 'package:test/test.dart'; + +class _MockLogger extends Mock implements Logger {} + +void main() { + group('sample', () { + late Logger logger; + late {{project_name.pascalCase()}}CommandRunner commandRunner; + + setUp(() { + logger = _MockLogger(); + commandRunner = {{project_name.pascalCase()}}CommandRunner(logger: logger); + }); + + test('tells a joke', () async { + final exitCode = await commandRunner.run(['sample']); + + expect(exitCode, ExitCode.success.code); + + verify( + () => logger.info('Which unicorn has a cold? The Achoo-nicorn!'), + ).called(1); + }); + test('tells a joke in cyan', () async { + final exitCode = await commandRunner.run(['sample', '-c']); + + expect(exitCode, ExitCode.success.code); + + verify( + () => logger.info( + lightCyan.wrap('Which unicorn has a cold? The Achoo-nicorn!'), + ), + ).called(1); + }); + + test('wrong usage', () async { + final exitCode = await commandRunner.run(['sample', '-p']); + + expect(exitCode, ExitCode.usage.code); + + verify(() => logger.err('Could not find an option or flag "-p".')) + .called(1); + verify( + () => logger.info( + ''' +Usage: $executableName sample [arguments] +-h, --help Print this usage information. +-c, --cyan Prints the same joke, but in cyan + +Run "$executableName help" to see global options.''', + ), + ).called(1); + }); + }); +} diff --git a/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/test/src/commands/update_command_test.dart b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/test/src/commands/update_command_test.dart new file mode 100644 index 0000000..a5f3e32 --- /dev/null +++ b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/test/src/commands/update_command_test.dart @@ -0,0 +1,185 @@ +import 'dart:io'; + +import 'package:mason_logger/mason_logger.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:{{project_name.snakeCase()}}/src/command_runner.dart'; +import 'package:{{project_name.snakeCase()}}/src/commands/commands.dart'; +import 'package:{{project_name.snakeCase()}}/src/version.dart'; +import 'package:pub_updater/pub_updater.dart'; +import 'package:test/test.dart'; + +class _MockLogger extends Mock implements Logger {} + +class _MockProgress extends Mock implements Progress {} + +class _MockPubUpdater extends Mock implements PubUpdater {} + +void main() { + const latestVersion = '0.0.0'; + + group('update', () { + late PubUpdater pubUpdater; + late Logger logger; + late {{project_name.pascalCase()}}CommandRunner commandRunner; + + setUp(() { + final progress = _MockProgress(); + final progressLogs = []; + pubUpdater = _MockPubUpdater(); + logger = _MockLogger(); + commandRunner = {{project_name.pascalCase()}}CommandRunner( + logger: logger, + pubUpdater: pubUpdater, + ); + + when( + () => pubUpdater.getLatestVersion(any()), + ).thenAnswer((_) async => packageVersion); + when( + () => pubUpdater.update( + packageName: packageName, + versionConstraint: latestVersion, + ), + ).thenAnswer( + (_) async => ProcessResult(0, ExitCode.success.code, null, null), + ); + when( + () => pubUpdater.isUpToDate( + packageName: any(named: 'packageName'), + currentVersion: any(named: 'currentVersion'), + ), + ).thenAnswer((_) async => true); + when(() => progress.complete(any())).thenAnswer((_) { + final message = _.positionalArguments.elementAt(0) as String?; + if (message != null) progressLogs.add(message); + }); + when(() => logger.progress(any())).thenReturn(progress); + }); + + test('can be instantiated without a pub updater', () { + final command = UpdateCommand(logger: logger); + expect(command, isNotNull); + }); + + test( + 'handles pub latest version query errors', + () async { + when( + () => pubUpdater.getLatestVersion(any()), + ).thenThrow(Exception('oops')); + final result = await commandRunner.run(['update']); + expect(result, equals(ExitCode.software.code)); + verify(() => logger.progress('Checking for updates')).called(1); + verify(() => logger.err('Exception: oops')); + verifyNever( + () => pubUpdater.update( + packageName: any(named: 'packageName'), + versionConstraint: any(named: 'versionConstraint'), + ), + ); + }, + ); + + test( + 'handles pub update errors', + () async { + when( + () => pubUpdater.getLatestVersion(any()), + ).thenAnswer((_) async => latestVersion); + when( + () => pubUpdater.update( + packageName: any(named: 'packageName'), + versionConstraint: any(named: 'versionConstraint'), + ), + ).thenThrow(Exception('oops')); + final result = await commandRunner.run(['update']); + expect(result, equals(ExitCode.software.code)); + verify(() => logger.progress('Checking for updates')).called(1); + verify(() => logger.err('Exception: oops')); + verify( + () => pubUpdater.update( + packageName: any(named: 'packageName'), + versionConstraint: any(named: 'versionConstraint'), + ), + ).called(1); + }, + ); + + test('handles pub update process errors', () async { + const error = 'Oh no! Installing this is not possible right now!'; + + when( + () => pubUpdater.getLatestVersion(any()), + ).thenAnswer((_) async => latestVersion); + + when( + () => pubUpdater.update( + packageName: any(named: 'packageName'), + versionConstraint: any(named: 'versionConstraint'), + ), + ).thenAnswer((_) async => ProcessResult(0, 1, null, error)); + + final result = await commandRunner.run(['update']); + + expect(result, equals(ExitCode.software.code)); + verify(() => logger.progress('Checking for updates')).called(1); + verify(() => logger.err('Error updating CLI: $error')); + verify( + () => pubUpdater.update( + packageName: any(named: 'packageName'), + versionConstraint: any(named: 'versionConstraint'), + ), + ).called(1); + }); + + test( + 'updates when newer version exists', + () async { + when( + () => pubUpdater.getLatestVersion(any()), + ).thenAnswer((_) async => latestVersion); + when( + () => pubUpdater.update( + packageName: any(named: 'packageName'), + versionConstraint: any(named: 'versionConstraint'), + ), + ).thenAnswer( + (_) async => ProcessResult(0, ExitCode.success.code, null, null), + ); + when(() => logger.progress(any())).thenReturn(_MockProgress()); + final result = await commandRunner.run(['update']); + expect(result, equals(ExitCode.success.code)); + verify(() => logger.progress('Checking for updates')).called(1); + verify(() => logger.progress('Updating to $latestVersion')).called(1); + verify( + () => pubUpdater.update( + packageName: packageName, + versionConstraint: latestVersion, + ), + ).called(1); + }, + ); + + test( + 'does not update when already on latest version', + () async { + when( + () => pubUpdater.getLatestVersion(any()), + ).thenAnswer((_) async => packageVersion); + when(() => logger.progress(any())).thenReturn(_MockProgress()); + final result = await commandRunner.run(['update']); + expect(result, equals(ExitCode.success.code)); + verify( + () => logger.info('CLI is already at the latest version.'), + ).called(1); + verifyNever(() => logger.progress('Updating to $latestVersion')); + verifyNever( + () => pubUpdater.update( + packageName: any(named: 'packageName'), + versionConstraint: any(named: 'versionConstraint'), + ), + ); + }, + ); + }); +} diff --git a/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/{{#publishable}}CHANGELOG.md{{/publishable}} b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/{{#publishable}}CHANGELOG.md{{/publishable}} new file mode 100644 index 0000000..4fff97b --- /dev/null +++ b/very_good_dart_cli/__brick__/{{project_name.snakeCase()}}/{{#publishable}}CHANGELOG.md{{/publishable}} @@ -0,0 +1,3 @@ +# 0.0.1 + +- feat: initial commit πŸŽ‰ diff --git a/very_good_dart_cli/analysis_options.yaml b/very_good_dart_cli/analysis_options.yaml new file mode 100644 index 0000000..41ffce1 --- /dev/null +++ b/very_good_dart_cli/analysis_options.yaml @@ -0,0 +1,3 @@ +analyzer: + exclude: + - __brick__/** diff --git a/very_good_dart_cli/brick.yaml b/very_good_dart_cli/brick.yaml new file mode 100644 index 0000000..b7ec18a --- /dev/null +++ b/very_good_dart_cli/brick.yaml @@ -0,0 +1,29 @@ +name: very_good_dart_cli +description: A Very Good Dart CLI created by Very Good Ventures. +repository: https://github.com/VeryGoodOpenSource/very_good_dart_cli +version: 0.5.1 + +environment: + mason: ">=0.1.0-dev.50 <0.1.0" + +vars: + project_name: + type: string + description: The name of the Dart CLI (it will also be used for the package name) + default: my_cli + prompt: What is the name of the Dart CLI? + executable_name: + type: string + description: The name of the executable (a.k.a the name of the root command) + default: my_cli + prompt: What is the name of the executable? + description: + type: string + description: A short description for the CLI + default: A Very Good Dart CLI + prompt: Please enter a description for the CLI. + publishable: + type: boolean + description: Whether the generated package is intended to be published. + default: false + prompt: Will the package be published? diff --git a/very_good_dart_cli/config.json b/very_good_dart_cli/config.json new file mode 100644 index 0000000..7f364ba --- /dev/null +++ b/very_good_dart_cli/config.json @@ -0,0 +1,6 @@ +{ + "project_name": "test_cli", + "executable_name": "very_good_ventures", + "description": "very_good_core test configuration", + "publishable": false +} diff --git a/very_good_dart_cli/hooks/.gitignore b/very_good_dart_cli/hooks/.gitignore new file mode 100644 index 0000000..7b37acf --- /dev/null +++ b/very_good_dart_cli/hooks/.gitignore @@ -0,0 +1,4 @@ +.dart_tool +.packages +pubspec.lock +build diff --git a/very_good_dart_cli/hooks/analysis_options.yaml b/very_good_dart_cli/hooks/analysis_options.yaml new file mode 100644 index 0000000..799268d --- /dev/null +++ b/very_good_dart_cli/hooks/analysis_options.yaml @@ -0,0 +1 @@ +include: package:very_good_analysis/analysis_options.5.1.0.yaml diff --git a/very_good_dart_cli/hooks/post_gen.dart b/very_good_dart_cli/hooks/post_gen.dart new file mode 100644 index 0000000..7520ca2 --- /dev/null +++ b/very_good_dart_cli/hooks/post_gen.dart @@ -0,0 +1,55 @@ +import 'dart:io'; + +import 'package:mason/mason.dart'; +import 'package:meta/meta.dart'; + +/// Type definition for [Process.run]. +typedef RunProcess = Future Function( + String executable, + List arguments, { + String? workingDirectory, + bool runInShell, +}); + +Future run(HookContext context, +// We intentionally ignore the trailing comma until the following mason issue is +// fixed: https://github.com/felangel/mason/pull/1164 +// ignore: require_trailing_commas + {@visibleForTesting RunProcess runProcess = Process.run}) async { + final projectName = context.vars['project_name'] as String; + + final progress = context.logger.progress('Getting Dart dependencies...'); + + // We have to `pub get` the generated project to ensure that the analysis + // is able to fix the imports with the correct analysis options. + await runProcess( + 'dart', + [ + 'pub', + 'get', + '--directory=$projectName', + ], + workingDirectory: Directory.current.path, + ); + + progress.update('Fixing Dart imports ordering...'); + + // Some imports are relative to the user specified package name, hence + // we try to fix the import directive ordering after the template has + // been generated. + // + // We only fix for the [directives_ordering](https://dart.dev/tools/linter-rules/directives_ordering) + // linter rules, as the other rule should be tackled by the template itself. + await runProcess( + 'dart', + [ + 'fix', + projectName, + '--apply', + '--code=directives_ordering', + ], + workingDirectory: Directory.current.path, + ); + + progress.complete('Completed post generation'); +} diff --git a/very_good_dart_cli/hooks/pubspec.yaml b/very_good_dart_cli/hooks/pubspec.yaml new file mode 100644 index 0000000..7f7f614 --- /dev/null +++ b/very_good_dart_cli/hooks/pubspec.yaml @@ -0,0 +1,13 @@ +name: very_good_dart_cli_hooks + +environment: + sdk: ">=2.12.0 <3.0.0" + +dependencies: + mason: ">=0.1.0-dev.51 <0.1.0" + meta: ^1.11.0 + +dev_dependencies: + mocktail: ^1.0.0 + test: ^1.19.2 + very_good_analysis: ^5.1.0 diff --git a/very_good_dart_cli/hooks/test/post_gen_test.dart b/very_good_dart_cli/hooks/test/post_gen_test.dart new file mode 100644 index 0000000..3a0b84f --- /dev/null +++ b/very_good_dart_cli/hooks/test/post_gen_test.dart @@ -0,0 +1,210 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:mason/mason.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +import '../post_gen.dart' as post_gen; + +class _MockHookContext extends Mock implements HookContext {} + +class _MockLogger extends Mock implements Logger {} + +class _MockProgress extends Mock implements Progress {} + +class _MockProcessResult extends Mock implements ProcessResult {} + +void main() { + group('post_gen', () { + late HookContext context; + late Logger logger; + late Progress progress; + late ProcessResult processResult; + late List invocations; + + /// The value of the `project_name` context variable. + const projectName = 'project_name'; + + setUp(() { + context = _MockHookContext(); + when(() => context.vars).thenReturn({'project_name': projectName}); + + logger = _MockLogger(); + when(() => context.logger).thenReturn(logger); + + progress = _MockProgress(); + when(() => logger.progress(any())).thenReturn(progress); + + processResult = _MockProcessResult(); + invocations = []; + }); + + Future runProcess( + String executable, + List arguments, { + String? workingDirectory, + bool runInShell = false, + }) async { + final positionalArguments = [executable, arguments]; + final namedArguments = { + const Symbol('workingDirectory'): workingDirectory, + const Symbol('runInShell'): runInShell, + }; + final invocation = Invocation.method( + const Symbol('runProcess'), + positionalArguments, + namedArguments, + ); + invocations.add(invocation); + + return processResult; + } + + test( + '''fixes `directives_ordering` Dart linter rule after `pub get`''', + () async { + await post_gen.run(context, runProcess: runProcess); + + expect(invocations[0], isDartPubGet(directory: projectName)); + expect(invocations[1], isDartDirectiveOrderingFix(path: projectName)); + }, + ); + + test('logs progress', () async { + final pubGetCompleter = Completer(); + final fixCompleter = Completer(); + + // ignore: prefer_function_declarations_over_variables + final runProcess = ( + String executable, + List arguments, { + String? workingDirectory, + bool runInShell = false, + }) async { + switch (arguments.first) { + case 'pub': + await pubGetCompleter.future; + break; + case 'fix': + await fixCompleter.future; + break; + } + return processResult; + }; + + final postGen = post_gen.run(context, runProcess: runProcess); + + verify(() => logger.progress('Getting Dart dependencies...')).called(1); + + pubGetCompleter.complete(); + await Future.delayed(Duration.zero); + + verify(() => progress.update('Fixing Dart imports ordering...')) + .called(1); + + fixCompleter.complete(); + await Future.delayed(Duration.zero); + + verify(() => progress.complete('Completed post generation')).called(1); + + await postGen; + }); + }); +} + +Matcher isDartDirectiveOrderingFix({required String path}) { + return _IsDartDirectiveOrderingFix(path: path); +} + +class _IsDartDirectiveOrderingFix extends Matcher { + const _IsDartDirectiveOrderingFix({required String path}) : _path = path; + + /// The value of the path to apply the `dart fix` to. + final String _path; + + @override + bool matches(dynamic item, Map matchState) { + if (item is! Invocation) { + return false; + } + + final invocation = item; + final executableName = invocation.positionalArguments[0] as String; + final arguments = invocation.positionalArguments[1] as List; + final workingDirectory = + invocation.namedArguments[const Symbol('workingDirectory')] as String?; + + return executableName == 'dart' && + arguments.contains('fix') && + arguments.contains(_path) && + arguments.contains('--apply') && + arguments.contains('--code=directives_ordering') && + workingDirectory == Directory.current.path; + } + + @override + Description describe(Description description) { + return description.add('is a `dart fix` for directives_ordering'); + } + + @override + Description describeMismatch( + dynamic item, + Description mismatchDescription, + Map matchState, + bool verbose, + ) { + return mismatchDescription + .add('is not a `dart fix` for directives_ordering'); + } +} + +Matcher isDartPubGet({required String directory}) { + return _IsDartPubGet(directory: directory); +} + +class _IsDartPubGet extends Matcher { + const _IsDartPubGet({ + required String directory, + }) : _directory = directory; + + /// The value of the `--directory` argument passed to `dart pub get`. + final String _directory; + + @override + bool matches(dynamic item, Map matchState) { + if (item is! Invocation) { + return false; + } + + final invocation = item; + final executableName = invocation.positionalArguments[0] as String; + final arguments = invocation.positionalArguments[1] as List; + final workingDirectory = + invocation.namedArguments[const Symbol('workingDirectory')] as String?; + + return executableName == 'dart' && + arguments.contains('pub') && + arguments.contains('get') && + arguments.contains('--directory=$_directory') && + workingDirectory == Directory.current.path; + } + + @override + Description describe(Description description) { + return description.add('is a `dart pub get --directory=$_directory`'); + } + + @override + Description describeMismatch( + dynamic item, + Description mismatchDescription, + Map matchState, + bool verbose, + ) { + return mismatchDescription.add( + 'is not a `dart pub get --directory=$_directory`', + ); + } +} diff --git a/very_good_wear_app/analysis_options.yaml b/very_good_wear_app/analysis_options.yaml new file mode 100644 index 0000000..41ffce1 --- /dev/null +++ b/very_good_wear_app/analysis_options.yaml @@ -0,0 +1,3 @@ +analyzer: + exclude: + - __brick__/**