diff --git a/.cursorindexingignore b/.cursorindexingignore deleted file mode 100644 index 953908e..0000000 --- a/.cursorindexingignore +++ /dev/null @@ -1,3 +0,0 @@ - -# Don't index SpecStory auto-save files, but allow explicit context inclusion via @ references -.specstory/** diff --git a/.github/cliff.toml b/.github/cliff.toml new file mode 100644 index 0000000..7f66dca --- /dev/null +++ b/.github/cliff.toml @@ -0,0 +1,74 @@ +# git-cliff configuration file for flutter_hooks_test +# https://git-cliff.org/docs/configuration + +[changelog] +# changelog header +header = """ +# Changelog + +All notable changes to this project will be documented in this file. + +""" +# template for the changelog body +# https://keats.github.io/tera/docs/ +body = """ +{% if version %}\ + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} +{% else %}\ + ## [unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | upper_first }} + {% for commit in commits %} + - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}](https://github.com/wasabeef/flutter_hooks_test/commit/{{ commit.id }})) + {%- endfor %} +{% endfor %}\n +""" +# remove the leading and trailing whitespace from the template +trim = true +# changelog footer +footer = """ + +""" + +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out the commits that are not conventional +filter_unconventional = true +# process each line of a commit as an individual commit +split_commits = false +# regex for preprocessing the commit messages +commit_preprocessors = [ + { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/wasabeef/flutter_hooks_test/issues/${2}))"}, +] +# regex for parsing and grouping commits +commit_parsers = [ + { message = "^feat", group = "Features"}, + { message = "^fix", group = "Bug Fixes"}, + { message = "^docs", group = "Documentation"}, + { message = "^perf", group = "Performance"}, + { message = "^refactor", group = "Refactor"}, + { message = "^style", group = "Styling"}, + { message = "^test", group = "Testing"}, + { message = "^chore\\(release\\): prepare for", skip = true}, + { message = "^chore", group = "Miscellaneous Tasks"}, + { body = ".*security", group = "Security"}, + { message = "^upgrade", group = "Dependencies"}, + { message = "^add", group = "Features"}, + { message = "^update", group = "Improvements"}, +] +# protect breaking changes from being skipped due to matching a skipping commit_parser +protect_breaking_commits = false +# filter out the commits that are not matched by commit parsers +filter_commits = false +# glob pattern for matching git tags +tag_pattern = "v[0-9]*" +# regex for skipping tags +skip_tags = "" +# regex for ignoring tags +ignore_tags = "" +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "oldest" \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7d4cd66..90d3540 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,7 +12,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest] + os: [ubuntu-latest, macos-latest] steps: - uses: actions/checkout@v4 @@ -26,15 +26,25 @@ jobs: flutter-version: ${{ steps.asdf.outputs.flutter }} cache: true + - uses: dart-lang/setup-dart@v1 + with: + sdk: ${{ steps.asdf.outputs.dart }} + - name: Set environment - run: echo "$HOME/.pub-cache/bin" >> $GITHUB_PATH + run: echo "$HOME/.pub-cache/bin" >> "$GITHUB_PATH" + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: ${{ steps.asdf.outputs.bun }} - name: Get dependencies run: | dart pub global activate melos melos get + bun install - - name: Run tests for our dart project. + - name: Run tests with coverage run: | melos test diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index d4d46f5..01b6aae 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,19 +1,23 @@ -name: Publish to pub.dev +name: Release on: push: tags: - - 'v[0-9]+.[0-9]+.[0-9]+' # Tag pattern to match for publishing + - 'v[0-9]+.[0-9]+.[0-9]+*' + +permissions: + contents: write + id-token: write # Required for OIDC authentication jobs: - publish: + release: + name: Release and Publish runs-on: ubuntu-latest - permissions: - id-token: write # Required for authentication using OIDC - contents: write steps: - - name: Checkout repository + - name: Checkout code uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Import .tool-versions uses: wasabeef/import-asdf-tool-versions-action@v1.1.0 @@ -25,22 +29,53 @@ jobs: flutter-version: ${{ steps.asdf.outputs.flutter }} cache: true - - name: Setup Dart for publishing + - name: Setup Dart uses: dart-lang/setup-dart@v1 with: sdk: ${{ steps.asdf.outputs.dart }} - - name: Setup Melos + - name: Set environment + run: echo "$HOME/.pub-cache/bin" >> "$GITHUB_PATH" + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: ${{ steps.asdf.outputs.bun }} + + - name: Install Melos run: dart pub global activate melos - - name: Get Melos packages - run: melos bootstrap + - name: Bootstrap packages + run: | + melos bootstrap + bun install - - name: Publish package - run: dart pub publish --force + - name: Verify all tests pass + run: melos test + + - name: Verify code formatting + run: melos format && melos analyze + + - name: Check package can be published (dry-run) + run: dart pub publish --dry-run + + - name: Generate release notes + id: release_notes + uses: orhun/git-cliff-action@v3 + with: + config: .github/cliff.toml + args: --latest --strip header - name: Create GitHub Release - uses: softprops/action-gh-release@v2 # Consider using a more recent version - if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref_name }} + name: ${{ github.ref_name }} + body: ${{ steps.release_notes.outputs.content }} + draft: false + prerelease: false + + - name: Publish to pub.dev + run: dart pub publish --force diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100755 index 52edcdb..0000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -# lint-staged -bun lint-staged --allow-empty --max-arg-length 1 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6b36cb2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,151 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Flutter Hooks Test is a testing utility library for Flutter hooks, inspired by React's `react-hooks-testing-library`. It provides a simple API to test custom hooks in isolation. + +## Development Commands + +### Essential Commands + +```bash +# Install dependencies +melos get +bun install + +# Run tests +melos test + +# Run code analysis +melos analyze + +# Format code (Dart + Prettier) +melos format + +# Run all checks (analyze + format + test) +melos analyze && melos format && melos test + +# Run a single test file +flutter test test/flutter_hooks_test_test.dart + +# Run tests with coverage +flutter test --coverage +``` + +### Additional Commands + +```bash +# Upgrade dependencies +melos upgrade + +# Clean build artifacts +melos clean + +# Format with Prettier only +bun run format + +# Setup git hooks +bun run prepare +``` + +## Architecture and Code Structure + +### Core API + +The library exports a single file `lib/flutter_hooks_test.dart` containing: + +1. **`buildHook`** - Main function to test hooks + - Generic `T`: Return type of the hook + - Generic `P`: Props type for parameterized hooks + - Returns `_HookTestingAction` with methods: + - `current`: Access current hook value + - `rebuild([props])`: Trigger rebuild with optional new props + - `unmount()`: Unmount the hook + +2. **`act`** - Wraps state changes to ensure proper Flutter test lifecycle + - Similar to React's `act` function + - Required when changing hook state + +### Testing Pattern + +```dart +// Basic hook test structure +final result = await buildHook((_) => useMyHook()); +await act(() => result.current.doSomething()); +expect(result.current.value, expectedValue); + +// With wrapper widget +final result = await buildHook( + (_) => useMyHook(), + wrapper: (child) => Provider(child: child), +); +``` + +### Internal Implementation + +- Uses `TestWidgetsFlutterBinding` for test environment +- Creates a minimal widget tree with `HookBuilder` +- Manages completer-based async operations for mount/unmount +- Preserves hook state across rebuilds using keys + +## Testing Approach + +- All tests go in `test/` directory +- Example hooks in `test/hooks/` demonstrate testable patterns +- Use `testWidgets` for widget-based tests +- Use Mockito for mocking dependencies + +## Code Quality + +- Flutter lints are enforced via `analysis_options.yaml` +- Example directory is excluded from analysis +- Pre-commit hooks format code automatically +- CI runs on Ubuntu with asdf version management + +## Important Conventions + +1. **Type Safety**: Always specify generic types when using `buildHook` +2. **Act Wrapper**: Always wrap state changes in `act()` +3. **Async Handling**: Most operations return Futures - use `await` +4. **Testing**: Test both happy paths and edge cases (mount/unmount/rebuild) + +## Version Requirements + +- Dart SDK: `>=2.17.0 <4.0.0` +- Flutter: `>=3.20.0` +- Uses Flutter hooks `^0.21.2` + +## Release Process + +Releases are fully automated via GitHub Actions: + +### Creating a Release + +1. **Update version**: Update version in `pubspec.yaml` +2. **Update changelog**: Run `git cliff --unreleased --tag v1.0.1 --output CHANGELOG.md` +3. **Commit changes**: `git commit -am "chore(release): prepare for v1.0.1"` +4. **Create tag**: `git tag v1.0.1` +5. **Push**: `git push origin main --tags` + +### Automated Release Steps + +When a tag matching `v[0-9]+.[0-9]+.[0-9]+*` is pushed: + +1. **CI Validation**: All tests, formatting, and analysis must pass +2. **Dry Run**: Package publication is tested +3. **Release Notes**: Auto-generated using git-cliff from conventional commits +4. **GitHub Release**: Created with generated release notes +5. **pub.dev Publication**: Package is published automatically + +### Commit Convention + +Use [Conventional Commits](https://www.conventionalcommits.org/) for automatic release note generation: + +- `feat:` - New features +- `fix:` - Bug fixes +- `docs:` - Documentation changes +- `test:` - Test improvements +- `refactor:` - Code refactoring +- `perf:` - Performance improvements diff --git a/README.md b/README.md index d7482e5..4f0e02b 100644 --- a/README.md +++ b/README.md @@ -26,42 +26,42 @@ This package is heavily inspired by [react-hooks-testing-library](https://github ## The problem -You're writing an awesome custom hook and you want to test it, but as soon as you call it you see +You're writing a custom hook and you want to test it, but as soon as you call it you see the following error: -> Invariant Violation: Hooks can only be called inside the body of a function component. +> Bad state: This hook is called outside of a hook context. -You don't really want to write a component solely for testing this hook and have to work out how you +You don't really want to write a widget solely for testing this hook and have to work out how you were going to trigger all the various ways the hook can be updated, especially given the complexities of how you've wired the whole thing together. ## The solution The `Flutter Hooks Testing Library` allows you to create a simple test harness for Flutter hooks that -handles running them within the body of a function component, as well as providing various useful -utility functions for updating the inputs and retrieving the outputs of your amazing custom hook. +handles running them within the body of a widget, as well as providing various useful +utility functions for updating the inputs and retrieving the outputs of your custom hook. This library aims to provide a testing experience as close as possible to natively using your hook -from within a real component. +from within a real widget. Using this library, you do not have to concern yourself with how to construct, render or interact -with the Flutter component in order to test your hook. You can just use the hook directly and assert +with the Flutter widget in order to test your hook. You can just use the hook directly and assert the results. ## When to use this library -1. You're writing a library with one or more custom hooks that are not directly tied to a component -2. You have a complex hook that is difficult to test through component interactions +1. You're writing a library with one or more custom hooks that are not directly tied to a widget +2. You have a complex hook that is difficult to test through widget interactions ## When not to use this library -1. Your hook is defined alongside a component and is only used there -2. Your hook is easy to test by just testing the components using it +1. Your hook is defined alongside a widget and is only used there +2. Your hook is easy to test by just testing the widgets using it ## Installation -```sh +```yaml dev_dependencies: - flutter_hooks_test: + flutter_hooks_test: ^1.0.0 ``` ## Example @@ -80,7 +80,7 @@ VoidCallback useUpdate() { **Not using** ```dart -testWidgets('should re-build component each time returned function is called', (tester) async { +testWidgets('should rebuild widget each time returned function is called', (tester) async { // Before const key = Key('button'); var buildCount = 0; @@ -104,11 +104,11 @@ testWidgets('should re-build component each time returned function is called', ( **Using** ```dart -testWidgets('should re-build component each time returned function is called', (tester) async { +testWidgets('should rebuild widget each time returned function is called', (tester) async { // After var buildCount = 0; final result = await buildHook( - (_) { + () { buildCount++; return useUpdate(); }, @@ -130,6 +130,6 @@ Plugin issues that are not specific to [Flutter Hooks Testing Library](https://g ## Contributing -If you wish to contribute a change to any of the existing plugins in this repo, -please review our [contribution guide](https://github.com/wasabeef/flutter_hooks_test/blob/master/CONTRIBUTING.md) +If you wish to contribute a change to this project, +please review our [contribution guide](https://github.com/wasabeef/flutter_hooks_test/blob/main/CONTRIBUTING.md) and open a [pull request](https://github.com/wasabeef/flutter_hooks_test/pulls). diff --git a/bun.lock b/bun.lock index 20a2314..da004a9 100644 --- a/bun.lock +++ b/bun.lock @@ -4,227 +4,14 @@ "": { "name": "flutter_hooks_test", "devDependencies": { - "@prettier/plugin-xml": "^3.0.0", - "husky": "^8.0.3", - "lint-staged": "13.1.0", - "prettier": "^3.0.0", - "prettier-plugin-packagejson": "^2.4.0", + "@evilmartians/lefthook": "^1.5.5", + "prettier": "^3.6.1", }, }, }, "packages": { - "@pkgr/core": ["@pkgr/core@0.2.7", "", {}, "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg=="], + "@evilmartians/lefthook": ["@evilmartians/lefthook@1.11.14", "", { "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "ia32", "arm64", ], "bin": { "lefthook": "bin/index.js" } }, "sha512-vh6lqXVwh7uhI5C/gKB7InW8RyMFYXN27U4Hlum8ZBcDrOviF9fmcBJf4C6ZboWkD3j8v1qp4psc3bwynhPlTQ=="], - "@prettier/plugin-xml": ["@prettier/plugin-xml@3.4.1", "", { "dependencies": { "@xml-tools/parser": "^1.0.11" }, "peerDependencies": { "prettier": "^3.0.0" } }, "sha512-Uf/6/+9ez6z/IvZErgobZ2G9n1ybxF5BhCd7eMcKqfoWuOzzNUxBipNo3QAP8kRC1VD18TIo84no7LhqtyDcTg=="], - - "@xml-tools/parser": ["@xml-tools/parser@1.0.11", "", { "dependencies": { "chevrotain": "7.1.1" } }, "sha512-aKqQ077XnR+oQtHJlrAflaZaL7qZsulWc/i/ZEooar5JiWj1eLt0+Wg28cpa+XLney107wXqneC+oG1IZvxkTA=="], - - "aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="], - - "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], - - "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], - - "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], - - "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], - - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - - "chevrotain": ["chevrotain@7.1.1", "", { "dependencies": { "regexp-to-ast": "0.5.0" } }, "sha512-wy3mC1x4ye+O+QkEinVJkPf5u2vsrDIYW9G7ZuwFl6v/Yu0LwUuT2POsb+NUWApebyxfkQq6+yDfRExbnI5rcw=="], - - "clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="], - - "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], - - "cli-truncate": ["cli-truncate@3.1.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^5.0.0" } }, "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA=="], - - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], - - "commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], - - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - - "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], - - "detect-indent": ["detect-indent@7.0.1", "", {}, "sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g=="], - - "detect-newline": ["detect-newline@4.0.1", "", {}, "sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog=="], - - "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], - - "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - - "execa": ["execa@6.1.0", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.1", "human-signals": "^3.0.1", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^3.0.7", "strip-final-newline": "^3.0.0" } }, "sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA=="], - - "fdir": ["fdir@6.4.5", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw=="], - - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - - "get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], - - "git-hooks-list": ["git-hooks-list@4.1.1", "", {}, "sha512-cmP497iLq54AZnv4YRAEMnEyQ1eIn4tGKbmswqwmFV4GBnAqE8NLtWxxdXa++AalfgL5EBH4IxTPyquEuGY/jA=="], - - "human-signals": ["human-signals@3.0.1", "", {}, "sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ=="], - - "husky": ["husky@8.0.3", "", { "bin": { "husky": "lib/bin.js" } }, "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg=="], - - "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], - - "is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], - - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - - "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - - "is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], - - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - - "lilconfig": ["lilconfig@2.0.6", "", {}, "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg=="], - - "lint-staged": ["lint-staged@13.1.0", "", { "dependencies": { "cli-truncate": "^3.1.0", "colorette": "^2.0.19", "commander": "^9.4.1", "debug": "^4.3.4", "execa": "^6.1.0", "lilconfig": "2.0.6", "listr2": "^5.0.5", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-inspect": "^1.12.2", "pidtree": "^0.6.0", "string-argv": "^0.3.1", "yaml": "^2.1.3" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-pn/sR8IrcF/T0vpWLilih8jmVouMlxqXxKuAojmbiGX5n/gDnz+abdPptlj0vYnbfE0SQNl3CY/HwtM0+yfOVQ=="], - - "listr2": ["listr2@5.0.8", "", { "dependencies": { "cli-truncate": "^2.1.0", "colorette": "^2.0.19", "log-update": "^4.0.0", "p-map": "^4.0.0", "rfdc": "^1.3.0", "rxjs": "^7.8.0", "through": "^2.3.8", "wrap-ansi": "^7.0.0" }, "peerDependencies": { "enquirer": ">= 2.3.0 < 3" }, "optionalPeers": ["enquirer"] }, "sha512-mC73LitKHj9w6v30nLNGPetZIlfpUniNSsxxrbaPcWOjDb92SHPzJPi/t+v1YC/lxKz/AJ9egOjww0qUuFxBpA=="], - - "log-update": ["log-update@4.0.0", "", { "dependencies": { "ansi-escapes": "^4.3.0", "cli-cursor": "^3.1.0", "slice-ansi": "^4.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg=="], - - "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], - - "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - - "mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], - - "npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="], - - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - - "onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="], - - "p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="], - - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - - "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - - "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], - - "prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="], - - "prettier-plugin-packagejson": ["prettier-plugin-packagejson@2.5.15", "", { "dependencies": { "sort-package-json": "3.2.1", "synckit": "0.11.8" }, "peerDependencies": { "prettier": ">= 1.16.0" }, "optionalPeers": ["prettier"] }, "sha512-2QSx6y4IT6LTwXtCvXAopENW5IP/aujC8fobEM2pDbs0IGkiVjW/ipPuYAHuXigbNe64aGWF7vIetukuzM3CBw=="], - - "regexp-to-ast": ["regexp-to-ast@0.5.0", "", {}, "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw=="], - - "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], - - "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], - - "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], - - "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - - "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - - "slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], - - "sort-object-keys": ["sort-object-keys@1.1.3", "", {}, "sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg=="], - - "sort-package-json": ["sort-package-json@3.2.1", "", { "dependencies": { "detect-indent": "^7.0.1", "detect-newline": "^4.0.1", "git-hooks-list": "^4.0.0", "is-plain-obj": "^4.1.0", "semver": "^7.7.1", "sort-object-keys": "^1.1.3", "tinyglobby": "^0.2.12" }, "bin": { "sort-package-json": "cli.js" } }, "sha512-rTfRdb20vuoAn7LDlEtCqOkYfl2X+Qze6cLbNOzcDpbmKEhJI30tTN44d5shbKJnXsvz24QQhlCm81Bag7EOKg=="], - - "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], - - "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], - - "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], - - "strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], - - "synckit": ["synckit@0.11.8", "", { "dependencies": { "@pkgr/core": "^0.2.4" } }, "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A=="], - - "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], - - "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], - - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], - - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - - "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "yaml": ["yaml@2.8.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ=="], - - "listr2/cli-truncate": ["cli-truncate@2.1.0", "", { "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" } }, "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg=="], - - "log-update/slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="], - - "log-update/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], - - "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], - - "restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], - - "tinyglobby/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], - - "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "listr2/cli-truncate/slice-ansi": ["slice-ansi@3.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="], - - "listr2/cli-truncate/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "log-update/slice-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "log-update/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "log-update/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "log-update/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], - - "wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "wrap-ansi/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "listr2/cli-truncate/slice-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "listr2/cli-truncate/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "listr2/cli-truncate/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "listr2/cli-truncate/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "listr2/cli-truncate/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "log-update/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "log-update/wrap-ansi/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "log-update/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "listr2/cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "prettier": ["prettier@3.6.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A=="], } } diff --git a/example/lib/main.dart b/example/lib/main.dart index 634348d..b2737e2 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -38,7 +38,7 @@ void main() { testWidgets('Using flutter_hooks_test', (tester) async { // After var buildCount = 0; - final result = await buildHook((_) { + final result = await buildHook(() { buildCount++; return useUpdate(); }); @@ -50,14 +50,14 @@ void main() { }); testWidgets('should rebuild after act()', (tester) async { - final result = await buildHook((_) => useCounter(5)); + final result = await buildHook(() => useCounter(5)); await act(() => result.current.inc()); expect(result.current.value, 6); }); testWidgets('should unmount after unmount()', (tester) async { final effect = MockEffect(); - final result = await buildHook((_) => useMount(() => effect())); + final result = await buildHook(() => useMount(() => effect())); verify(effect()).called(1); verifyNoMoreInteractions(effect); await result.unmount(); @@ -67,19 +67,58 @@ void main() { testWidgets('should rebuild after rebuild()', (tester) async { final effect = MockEffect(); - final result = await buildHook((_) => useMount(() => effect())); + final result = await buildHook(() => useMount(() => effect())); await result.rebuild(); verify(effect()).called(1); verifyNoMoreInteractions(effect); }); testWidgets('should rebuild after rebuild() with parameter', (tester) async { - final result = await buildHook( + final result = await buildHookWithProps( (count) => useLatest(count), initialProps: 123, ); expect(result.current, 123); - await result.rebuild(456); + await result.rebuildWithProps(456); expect(result.current, 456); }); + + testWidgets('should track build history with new API', (tester) async { + final result = await buildHook(() => useCounter(0)); + + // Debug information + result.debug(); + + // Initial build + expect(result.buildCount, 1); + expect(result.all.length, 1); + expect(result.all.first.value, 0); + + // After increment + await act(() => result.current.inc()); + expect(result.buildCount, 2); + expect(result.all.length, 2); + expect(result.all.last.value, 1); + }); + + testWidgets('should demonstrate waitFor utilities', (tester) async { + final result = await buildHook(() => useCounter(0)); + + // Wait for initial condition + await waitFor(() => result.current.value == 0); + + // Increment and wait for change + await act(() => result.current.inc()); + await waitFor(() => result.current.value == 1); + + // Use extension method to wait for specific condition + await act(() => result.current.inc()); + await act(() => result.current.inc()); + + final finalValue = await result.waitForValueToMatch( + (counter) => counter.value >= 3, + ); + + expect(finalValue.value, 3); + }); } diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 0000000..b1a3ded --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,14 @@ +# EXAMPLE USAGE: +# Refer for explanation to following link: +# https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md + +pre-commit: + commands: + format: + glob: '*.dart' + run: dart format {staged_files} + stage_fixed: true + prettier: + glob: '*.{md,yaml,yml,json}' + run: bunx prettier --write {staged_files} + stage_fixed: true diff --git a/lib/flutter_hooks_test.dart b/lib/flutter_hooks_test.dart index 35d37b0..d1a354c 100644 --- a/lib/flutter_hooks_test.dart +++ b/lib/flutter_hooks_test.dart @@ -4,8 +4,152 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_test/flutter_test.dart'; +/// Base class for hook test results providing access to hook state and lifecycle methods. +abstract class HookResult { + /// The current value returned by the hook. + T get current; + + /// Rebuilds the hook widget and adds the result to history. + Future rebuild(); + + /// Unmounts the hook widget, triggering cleanup effects. + Future unmount(); + + /// All values returned by the hook after each build. + /// Each call to rebuild() adds a new entry to this list. + List get all; + + /// The number of times the hook has been built (length of history). + int get buildCount => all.length; +} + +/// Base implementation for hook results with history tracking. +abstract class _BaseHookResult extends HookResult { + _BaseHookResult(this._current, this._unmount) { + _addToHistory(); + } + + final T Function() _current; + final Future Function() _unmount; + final List _all = []; + + @override + T get current => _current(); + + @override + List get all => List.unmodifiable(_all); + + @override + Future unmount() => _unmount(); + + /// Adds the current hook value to build history. + /// Called automatically during construction and rebuild operations. + void _addToHistory() { + final value = _current(); + // Always add to history on rebuild to track state changes + _all.add(value); + } +} + +/// Result for hooks without props. +class _SimpleHookResult extends _BaseHookResult { + _SimpleHookResult(super.current, this._rebuild, super.unmount); + + final Future Function() _rebuild; + + @override + Future rebuild() async { + await _rebuild(); + _addToHistory(); + } +} + +/// Result for hooks with props. +class HookResultWithProps extends _BaseHookResult { + HookResultWithProps( + super.current, this._rebuildWithProps, super.unmount, this._initialProps); + + final Future Function(P props) _rebuildWithProps; + final P _initialProps; + + @override + Future rebuild() async { + await _rebuildWithProps(_initialProps); + _addToHistory(); + } + + /// Rebuilds the hook with different props and updates history. + Future rebuildWithProps(P props) async { + await _rebuildWithProps(props); + _addToHistory(); + } +} + +/// Default empty widget used as a placeholder in hook tests. +const Widget _kEmptyWidget = SizedBox.shrink(); + +/// Applies a wrapper to a child widget, or returns the child if no wrapper is provided. +Widget _applyWrapper(Widget child, Widget Function(Widget)? wrapper) { + return wrapper?.call(child) ?? child; +} + +/// Creates a test harness for a hook that doesn't require props. +Future> buildHook( + T Function() hook, { + Widget Function(Widget child)? wrapper, +}) async { + late T result; + + Widget builder() { + return HookBuilder(builder: (context) { + result = hook(); + return _kEmptyWidget; + }); + } + + final wrappedBuilder = _applyWrapper(builder(), wrapper); + await _build(wrappedBuilder); + + Future rebuild() => _build(_applyWrapper(builder(), wrapper)); + Future unmount() => _build(_kEmptyWidget); + + return _SimpleHookResult(() => result, rebuild, unmount); +} + +/// Creates a test harness for a hook that requires props. +Future> buildHookWithProps( + T Function(P props) hook, { + required P initialProps, + Widget Function(Widget child)? wrapper, +}) async { + late T result; + + Widget builder(P props) { + return HookBuilder(builder: (context) { + result = hook(props); + return _kEmptyWidget; + }); + } + + final wrappedBuilder = _applyWrapper(builder(initialProps), wrapper); + await _build(wrappedBuilder); + + Future rebuildWithProps(P props) => + _build(_applyWrapper(builder(props), wrapper)); + Future unmount() => _build(_kEmptyWidget); + + return HookResultWithProps( + () => result, rebuildWithProps, unmount, initialProps); +} + +const String _kDeprecationMessage = + 'Use buildHook(() => hook()) for hooks without props ' + 'or buildHookWithProps((props) => hook(props), initialProps: props) for hooks with props'; + +/// Deprecated: Use buildHook() or buildHookWithProps() instead. +@Deprecated(_kDeprecationMessage) // ignore: library_private_types_in_public_api -Future<_HookTestingAction> buildHook( +Future<_HookTestingAction> buildHookLegacy( T Function(P? props) hook, { P? initialProps, Widget Function(Widget child)? wrapper, @@ -15,18 +159,16 @@ Future<_HookTestingAction> buildHook( Widget builder([P? props]) { return HookBuilder(builder: (context) { result = hook(props); - return Container(); + return _kEmptyWidget; }); } - Widget wrappedBuilder([P? props]) => - wrapper == null ? builder(props) : wrapper(builder(props)); - - await _build(wrappedBuilder(initialProps)); + final wrappedBuilder = _applyWrapper(builder(initialProps), wrapper); + await _build(wrappedBuilder); - Future rebuild([P? props]) => _build(wrappedBuilder(props)); - - Future unmount() => _build(Container()); + Future rebuild([P? props]) => + _build(_applyWrapper(builder(props), wrapper)); + Future unmount() => _build(_kEmptyWidget); return _HookTestingAction(() => result, rebuild, unmount); } @@ -40,6 +182,88 @@ Future act(void Function() fn) { }); } +/// Waits for a condition to become true by repeatedly pumping the widget tree. +/// +/// This function checks the [condition] after each pump until it returns true. +/// Use this to wait for asynchronous state changes in your hooks. +/// +/// Example: +/// ```dart +/// await waitFor(() => result.current.value > 5); +/// ``` +Future waitFor(bool Function() condition) async { + final binding = TestWidgetsFlutterBinding.ensureInitialized(); + + while (!condition()) { + await binding.pump(); + } +} + +/// Waits for a value to change by comparing with its initial state. +/// +/// Captures the current value and waits until [getValue] returns a different value. +/// +/// Example: +/// ```dart +/// await waitForValueToChange(() => result.current.count); +/// ``` +Future waitForValueToChange(T Function() getValue) async { + final initialValue = getValue(); + await waitFor(() => getValue() != initialValue); + return getValue(); +} + +/// Extension methods for HookResult to provide additional waiting utilities +extension HookResultWaitExtensions on HookResult { + /// Waits for the next update to occur on this hook result. + /// + /// This method waits until the build count increases, indicating that + /// the hook has been rebuilt. + /// + /// Example: + /// ```dart + /// await act(() => result.current.increment()); + /// await result.waitForNextUpdate(); + /// ``` + Future waitForNextUpdate() async { + final currentBuildCount = buildCount; + await waitFor(() => buildCount > currentBuildCount); + } + + /// Waits for the current value to change. + /// + /// This method captures the current value and waits until it changes + /// to a different value. + /// + /// Example: + /// ```dart + /// await act(() => result.current.increment()); + /// await result.waitForValueToChange(); + /// ``` + Future waitForValueToChange() async { + final initialValue = current; + await waitFor(() => current != initialValue); + return current; + } + + /// Waits for a specific condition to be met on the current value. + /// + /// This method repeatedly checks the current value against the provided + /// predicate until it returns true. + /// + /// Example: + /// ```dart + /// await act(() => result.current.increment()); + /// await result.waitForValueToMatch((value) => value > 10); + /// ``` + Future waitForValueToMatch(bool Function(T value) predicate) async { + await waitFor(() => predicate(current)); + return current; + } +} + +/// Legacy class for backward compatibility +@Deprecated('Use HookResult or HookResultWithProps instead') class _HookTestingAction { const _HookTestingAction(this._current, this.rebuild, this.unmount); @@ -48,12 +272,10 @@ class _HookTestingAction { final T Function() _current; T get current => _current(); - /// A function to rebuild the test component, causing any hooks to be - /// recalculated. + /// Rebuilds the test widget with optional new props. final Future Function([P? props]) rebuild; - /// A function to unmount the test component. This is commonly used to trigger - /// cleanup effects for useEffect hooks. + /// Unmounts the test widget, triggering cleanup effects. final Future Function() unmount; } diff --git a/melos.yaml b/melos.yaml index 4628326..b3b8fd1 100644 --- a/melos.yaml +++ b/melos.yaml @@ -15,7 +15,13 @@ scripts: analyze: melos exec -- flutter analyze . - format: melos exec -- dart format . + format: + run: | + melos exec -- dart format . + bunx prettier --write . test: run: melos exec -- flutter test + + clean: + run: melos exec -- flutter clean diff --git a/package.json b/package.json index bdb5b25..65f384e 100644 --- a/package.json +++ b/package.json @@ -3,20 +3,11 @@ "private": false, "author": "wasabeef", "scripts": { - "format": "prettier --config prettier.config.js --write '**/*.+(md|yml|yaml|json|xml)'", - "prepare": "husky install" - }, - "lint-staged": { - "*.dart": "dart format", - "*.@(json|yaml|yml)": [ - "prettier --write" - ] + "prepare": "lefthook install", + "format": "prettier --write ." }, "devDependencies": { - "@prettier/plugin-xml": "^3.0.0", - "husky": "^8.0.3", - "lint-staged": "13.1.0", - "prettier": "^3.0.0", - "prettier-plugin-packagejson": "^2.4.0" + "@evilmartians/lefthook": "^1.5.5", + "prettier": "^3.6.1" } } diff --git a/prettier.config.js b/prettier.config.js index c733211..29ed013 100644 --- a/prettier.config.js +++ b/prettier.config.js @@ -9,6 +9,4 @@ export default { trailingComma: 'all', arrowParens: 'always', endOfLine: 'lf', - xmlWhitespaceSensitivity: 'ignore', - plugins: ['@prettier/plugin-xml', 'prettier-plugin-packagejson'], }; diff --git a/pubspec.yaml b/pubspec.yaml index a5ab0f5..4b13eb9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,5 +20,5 @@ dependencies: dev_dependencies: flutter_lints: ^6.0.0 - mockito: ^5.4.6 + mockito: ^5.4.4 melos: ^6.3.3 diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..2ee48bd --- /dev/null +++ b/renovate.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:base"], + "schedule": ["before 10am on monday"], + "timezone": "Asia/Tokyo", + "packageRules": [ + { + "matchPackageNames": ["flutter", "dart"], + "enabled": false + } + ] +} diff --git a/test/flutter_hooks_test_test.dart b/test/flutter_hooks_test_test.dart index 23e50df..2f44f8c 100644 --- a/test/flutter_hooks_test_test.dart +++ b/test/flutter_hooks_test_test.dart @@ -39,7 +39,7 @@ void main() { // After var buildCount = 0; final result = await buildHook( - (_) { + () { buildCount++; return useUpdate(); }, @@ -53,7 +53,7 @@ void main() { }); testWidgets('should rebuild after act()', (tester) async { - final result = await buildHook((_) => useCounter(5)); + final result = await buildHook(() => useCounter(5)); await act(() => result.current.inc()); expect(result.current.value, 6); }); @@ -61,7 +61,7 @@ void main() { testWidgets('should unmount after unmount()', (tester) async { final effect = MockEffect(); final result = await buildHook( - (_) => useMount( + () => useMount( () => effect(), ), ); @@ -74,19 +74,83 @@ void main() { testWidgets('should rebuild after rebuild()', (tester) async { final effect = MockEffect(); - final result = await buildHook((_) => useMount(() => effect())); + final result = await buildHook(() => useMount(() => effect())); await result.rebuild(); verify(effect()).called(1); verifyNoMoreInteractions(effect); }); testWidgets('should rebuild after rebuild() with parameter', (tester) async { - final result = await buildHook( + final result = await buildHookWithProps( (count) => useLatest(count), initialProps: 123, ); expect(result.current, 123); - await result.rebuild(456); + await result.rebuildWithProps(456); expect(result.current, 456); }); + + testWidgets('should track build history', (tester) async { + final result = await buildHook(() => useCounter(0)); + + // Initial build + expect(result.buildCount, 1); + expect(result.all.length, 1); + expect(result.all.first.value, 0); + + // Trigger state change and rebuild + result.current.inc(); + await result.rebuild(); + + // History should track the new state + expect(result.buildCount, 2); + expect(result.all.length, 2); + expect(result.all.last.value, 1); + }); + + testWidgets('should provide build count and history', (tester) async { + final result = await buildHook(() => useCounter(5)); + + expect(result.current.value, 5); + expect(result.buildCount, 1); + expect(result.all.length, 1); + }); + + testWidgets('should work with new API for props', (tester) async { + final result = await buildHookWithProps( + (count) => useLatest(count), + initialProps: 100, + ); + + expect(result.current, 100); + await result.rebuildWithProps(200); + expect(result.current, 200); + }); + + group('waitFor utilities', () { + testWidgets('waitFor basic functionality demonstration', (tester) async { + final result = await buildHook(() => useCounter(0)); + + // Demonstrate waitFor with immediate condition + await waitFor(() => result.current.value == 0); + expect(result.current.value, 0); + + // Increment and wait for the change to be reflected + await act(() => result.current.inc()); + await waitFor(() => result.current.value == 1); + expect(result.current.value, 1); + }); + + testWidgets('waitForValueToMatch should work with predicate', + (tester) async { + final result = await buildHook(() => useCounter(5)); + + // Wait for a condition that's already true + final value = await result.waitForValueToMatch( + (counter) => counter.value >= 5, + ); + + expect(value.value, 5); + }); + }); }