├── .github └── workflows │ ├── force-publish.yml │ ├── manual-crates-publish.yml │ ├── release.yml │ ├── rust.yml │ └── semantic-release.yml ├── .gitignore ├── .releaserc.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── screenshot.png ├── scripts └── sync-crates-version.sh └── src └── main.rs /.github/workflows/force-publish.yml: -------------------------------------------------------------------------------- 1 | name: Force Publish to Crates.io 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Version to publish (must match Cargo.toml)' 8 | required: true 9 | type: string 10 | 11 | jobs: 12 | force-publish: 13 | name: Force Publish to crates.io 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v3 18 | 19 | - name: Setup Rust 20 | uses: actions-rs/toolchain@v1 21 | with: 22 | profile: minimal 23 | toolchain: stable 24 | 25 | - name: Verify version matches 26 | run: | 27 | CARGO_VERSION=$(grep '^version =' Cargo.toml | head -n 1 | cut -d '"' -f 2) 28 | if [ "$CARGO_VERSION" != "${{ inputs.version }}" ]; then 29 | echo "Error: Version in Cargo.toml ($CARGO_VERSION) does not match input version (${{ inputs.version }})" 30 | exit 1 31 | fi 32 | echo "Version validated: $CARGO_VERSION" 33 | 34 | - name: Update registry 35 | run: cargo update 36 | 37 | - name: Force publish to crates.io 38 | uses: nick-fields/retry@v2 39 | with: 40 | timeout_minutes: 10 41 | max_attempts: 5 42 | retry_wait_seconds: 45 43 | command: cargo publish --allow-dirty --no-verify 44 | on_retry_command: cargo update && sleep 45 45 | env: 46 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 47 | 48 | - name: Verify package on crates.io 49 | run: | 50 | echo "Waiting for package to be available on crates.io..." 51 | sleep 45 52 | MAX_ATTEMPTS=12 53 | attempt=1 54 | 55 | while [ $attempt -le $MAX_ATTEMPTS ]; do 56 | echo "Checking attempt $attempt of $MAX_ATTEMPTS..." 57 | RESPONSE=$(curl -s https://crates.io/api/v1/crates/node-size-analyzer/${{ inputs.version }}) 58 | if echo "$RESPONSE" | grep -q "\"version\":\"${{ inputs.version }}\""; then 59 | echo "✅ Package node-size-analyzer v${{ inputs.version }} successfully published to crates.io" 60 | exit 0 61 | fi 62 | 63 | echo "Package not found yet, waiting 30 seconds..." 64 | sleep 30 65 | attempt=$((attempt + 1)) 66 | done 67 | 68 | echo "⚠️ Package may still be processing on crates.io. Check manually at https://crates.io/crates/node-size-analyzer" -------------------------------------------------------------------------------- /.github/workflows/manual-crates-publish.yml: -------------------------------------------------------------------------------- 1 | name: Manual Crates.io Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version_check: 7 | description: 'Skip version check (use when manually updating)' 8 | required: false 9 | type: boolean 10 | default: false 11 | 12 | jobs: 13 | publish: 14 | name: Publish to crates.io 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - uses: actions-rs/toolchain@v1 20 | with: 21 | profile: minimal 22 | toolchain: stable 23 | 24 | - name: Get current version 25 | id: get_version 26 | run: | 27 | VERSION=$(grep '^version =' Cargo.toml | head -n 1 | cut -d '"' -f 2) 28 | echo "VERSION=$VERSION" >> $GITHUB_ENV 29 | echo "Current version is $VERSION" 30 | 31 | - name: Update crates.io registry 32 | run: cargo update 33 | 34 | - name: Check if package is publishable 35 | run: cargo publish --dry-run 36 | env: 37 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 38 | 39 | - name: Check if version exists on crates.io 40 | if: ${{ !inputs.version_check }} 41 | run: | 42 | RESPONSE=$(curl -s https://crates.io/api/v1/crates/node-size-analyzer/${{ env.VERSION }}) 43 | if echo "$RESPONSE" | grep -q "\"version\":\"${{ env.VERSION }}\""; then 44 | echo "Error: Version ${{ env.VERSION }} already exists on crates.io" 45 | exit 1 46 | else 47 | echo "Version ${{ env.VERSION }} is not on crates.io yet. Proceeding with publish." 48 | fi 49 | 50 | - name: Publish to crates.io 51 | uses: nick-fields/retry@v2 52 | with: 53 | timeout_minutes: 10 54 | max_attempts: 3 55 | retry_wait_seconds: 30 56 | command: cargo publish 57 | on_retry_command: cargo update && sleep 30 58 | env: 59 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 60 | 61 | - name: Verify package on crates.io 62 | run: | 63 | echo "Waiting for package to be available on crates.io..." 64 | sleep 30 65 | MAX_ATTEMPTS=10 66 | attempt=1 67 | 68 | while [ $attempt -le $MAX_ATTEMPTS ]; do 69 | echo "Checking attempt $attempt of $MAX_ATTEMPTS..." 70 | if curl -s https://crates.io/api/v1/crates/node-size-analyzer/${{ env.VERSION }} | grep -q "\"version\":\"${{ env.VERSION }}\""; then 71 | echo "✅ Package node-size-analyzer v${{ env.VERSION }} successfully published to crates.io" 72 | exit 0 73 | fi 74 | 75 | echo "Package not found yet, waiting 30 seconds..." 76 | sleep 30 77 | attempt=$((attempt + 1)) 78 | done 79 | 80 | echo "⚠️ Package may still be processing on crates.io. Check manually at https://crates.io/crates/node-size-analyzer" -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | publish: 10 | name: Publish for ${{ matrix.os }} 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | include: 15 | - os: ubuntu-latest 16 | artifact_name: node-size 17 | asset_name: node-size-linux 18 | - os: windows-latest 19 | artifact_name: node-size.exe 20 | asset_name: node-size-windows.exe 21 | - os: macos-latest 22 | artifact_name: node-size 23 | asset_name: node-size-macos 24 | 25 | steps: 26 | - uses: actions/checkout@v3 27 | - uses: actions-rs/toolchain@v1 28 | with: 29 | profile: minimal 30 | toolchain: stable 31 | 32 | - run: cargo build --release 33 | 34 | - name: Get version from tag 35 | id: get_version 36 | run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV 37 | shell: bash 38 | 39 | - name: Upload binaries to release 40 | uses: softprops/action-gh-release@v1 41 | with: 42 | files: target/release/${{ matrix.artifact_name }} 43 | name: Release v${{ env.VERSION }} 44 | body_path: CHANGELOG.md 45 | token: ${{ secrets.GITHUB_TOKEN }} 46 | 47 | crates: 48 | name: Publish to crates.io 49 | needs: publish 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@v3 53 | - uses: actions-rs/toolchain@v1 54 | with: 55 | profile: minimal 56 | toolchain: stable 57 | 58 | - name: Get version from tag 59 | id: get_version 60 | run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV 61 | shell: bash 62 | 63 | - name: Update crates.io registry 64 | run: cargo update 65 | 66 | - name: Check if package is publishable 67 | run: cargo publish --dry-run 68 | env: 69 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 70 | 71 | - name: Verify version matches tag 72 | run: | 73 | CARGO_VERSION=$(grep '^version =' Cargo.toml | head -n 1 | cut -d '"' -f 2) 74 | if [ "$CARGO_VERSION" != "$VERSION" ]; then 75 | echo "Error: Version in Cargo.toml ($CARGO_VERSION) does not match tag version ($VERSION)" 76 | exit 1 77 | fi 78 | echo "Version validated: $VERSION" 79 | 80 | - name: Publish to crates.io with retry 81 | uses: nick-fields/retry@v2 82 | with: 83 | timeout_minutes: 10 84 | max_attempts: 3 85 | retry_wait_seconds: 30 86 | command: cargo publish 87 | on_retry_command: cargo update && sleep 30 88 | env: 89 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 90 | 91 | - name: Verify package on crates.io 92 | run: | 93 | echo "Waiting for package to be available on crates.io..." 94 | sleep 30 95 | # Check if the package is available 96 | curl -s https://crates.io/api/v1/crates/node-size-analyzer/$VERSION | grep -q "\"version\":\"$VERSION\"" 97 | if [ $? -eq 0 ]; then 98 | echo "✅ Package node-size-analyzer v$VERSION successfully published to crates.io" 99 | else 100 | echo "⚠️ Package may still be processing on crates.io. Check manually at https://crates.io/crates/node-size-analyzer" 101 | fi 102 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /.github/workflows/semantic-release.yml: -------------------------------------------------------------------------------- 1 | name: Semantic Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | semantic-release: 10 | name: Semantic Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | token: ${{ secrets.GITHUB_TOKEN }} 18 | 19 | - name: Setup Node.js 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: '20' 23 | 24 | - name: Install dependencies 25 | run: | 26 | npm install -g semantic-release @semantic-release/git @semantic-release/changelog @semantic-release/exec conventional-changelog-conventionalcommits 27 | 28 | - name: Set up Git 29 | run: | 30 | git config --local user.email "action@github.com" 31 | git config --local user.name "GitHub Action" 32 | 33 | # We'll use the checked-in .releaserc.json file instead of creating it dynamically 34 | 35 | - name: Run semantic-release 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 39 | run: | 40 | # Fix up the .releaserc.json to avoid shell expansion issues 41 | sed -i 's/\$\${nextRelease\.version}/\${nextRelease.version}/g' .releaserc.json 42 | sed -i 's/\$\${nextRelease\.notes}/\${nextRelease.notes}/g' .releaserc.json 43 | npx semantic-release 44 | 45 | # This job will be triggered by the tag created by semantic-release 46 | trigger-release: 47 | name: Trigger Release Workflow 48 | needs: semantic-release 49 | if: success() 50 | runs-on: ubuntu-latest 51 | steps: 52 | - name: Checkout code 53 | uses: actions/checkout@v3 54 | with: 55 | ref: main 56 | 57 | - name: Sleep for 10 seconds 58 | run: sleep 10 59 | shell: bash 60 | 61 | - name: Setup GitHub CLI 62 | run: | 63 | curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg 64 | echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null 65 | sudo apt update 66 | sudo apt install gh 67 | 68 | - name: Verify release and crates.io sync 69 | env: 70 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | run: | 72 | # Get the current version from Cargo.toml 73 | CURRENT_VERSION=$(grep '^version =' Cargo.toml | head -n 1 | cut -d '"' -f 2) 74 | echo "Latest version from Cargo.toml: $CURRENT_VERSION" 75 | 76 | # Check if this version exists on crates.io 77 | RESPONSE=$(curl -s https://crates.io/api/v1/crates/node-size-analyzer/$CURRENT_VERSION) 78 | 79 | if echo "$RESPONSE" | grep -q "\"version\":\"$CURRENT_VERSION\""; then 80 | echo "Version $CURRENT_VERSION already exists on crates.io. No additional action needed." 81 | else 82 | echo "Version $CURRENT_VERSION does not exist on crates.io." 83 | echo "Triggering GitHub workflow to publish the new version..." 84 | gh workflow run force-publish.yml -f version=$CURRENT_VERSION 85 | echo "Workflow triggered. Check GitHub Actions for progress." 86 | fi 87 | 88 | - name: Notify completion 89 | run: echo "Semantic release complete. Release workflow should be triggered automatically." -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | 5 | # IDE 6 | .idea/ 7 | .vscode/ 8 | *.swp 9 | *.swo 10 | 11 | # OS 12 | .DS_Store 13 | Thumbs.db 14 | 15 | # Debug files 16 | *.pdb 17 | 18 | # Binary files 19 | node-size 20 | node-size.exe 21 | node-size-* 22 | 23 | **/.claude/settings.local.json 24 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": ["main"], 3 | "plugins": [ 4 | ["@semantic-release/commit-analyzer", { 5 | "preset": "conventionalcommits", 6 | "releaseRules": [ 7 | {"type": "feat", "release": "minor"}, 8 | {"type": "fix", "release": "patch"}, 9 | {"type": "docs", "release": "patch"}, 10 | {"type": "style", "release": "patch"}, 11 | {"type": "refactor", "release": "patch"}, 12 | {"type": "perf", "release": "patch"}, 13 | {"type": "test", "release": "patch"}, 14 | {"type": "build", "release": "patch"}, 15 | {"type": "ci", "release": "patch"}, 16 | {"type": "chore", "release": "patch"}, 17 | {"scope": "breaking", "release": "major"} 18 | ] 19 | }], 20 | "@semantic-release/release-notes-generator", 21 | "@semantic-release/changelog", 22 | ["@semantic-release/exec", { 23 | "prepareCmd": "sed -i 's/^version = \".*\"/version = \"$${nextRelease.version}\"/' Cargo.toml" 24 | }], 25 | ["@semantic-release/git", { 26 | "assets": ["Cargo.toml", "CHANGELOG.md"], 27 | "message": "chore(release): $${nextRelease.version} [skip ci]\n\n$${nextRelease.notes}" 28 | }], 29 | "@semantic-release/github" 30 | ] 31 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.7.1](https://github.com/sparkforge/node-size-analyzer/compare/v0.7.0...v0.7.1) (2025-05-07) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * replace cli/setup-gh@v1 with manual GitHub CLI installation ([4a3dbc2](https://github.com/sparkforge/node-size-analyzer/commit/4a3dbc22d6398262acd4a0693beccf0a7849d4dd)) 7 | 8 | # [0.7.0](https://github.com/sparkforge/node-size-analyzer/compare/v0.6.0...v0.7.0) (2025-05-07) 9 | 10 | 11 | ### Features 12 | 13 | * add detailed view for module inspection ([81edcfa](https://github.com/sparkforge/node-size-analyzer/commit/81edcfa13ec4e80e7651d3af71da4db190be7185)) 14 | 15 | # [0.6.0](https://github.com/sparkforge/node-size-analyzer/compare/v0.5.0...v0.6.0) (2025-05-07) 16 | 17 | 18 | ### Features 19 | 20 | * add force publish workflow and version sync tools ([d1458c5](https://github.com/sparkforge/node-size-analyzer/commit/d1458c570d432d094c49e095ed15ca38c7283914)) 21 | 22 | # [0.5.0](https://github.com/sparkforge/node-size-analyzer/compare/v0.4.1...v0.5.0) (2025-05-06) 23 | 24 | 25 | ### Features 26 | 27 | * add manual crates.io publishing workflow ([aaf8aaf](https://github.com/sparkforge/node-size-analyzer/commit/aaf8aaf8349f2f1b7b1c0dd394d56e7a654f0db2)) 28 | 29 | ## [0.4.1](https://github.com/sparkforge/node-size-analyzer/compare/v0.4.0...v0.4.1) (2025-05-06) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * update workflows to use CARGO_REGISTRY_TOKEN ([0941fe0](https://github.com/sparkforge/node-size-analyzer/commit/0941fe05675a1242e44844f5f6b9a4dd3c01ed5a)) 35 | 36 | # [0.4.0](https://github.com/sparkforge/node-size-analyzer/compare/v0.3.0...v0.4.0) (2025-05-06) 37 | 38 | 39 | ### Features 40 | 41 | * enhance crates.io automated publishing ([d755266](https://github.com/sparkforge/node-size-analyzer/commit/d75526682d39ee55874e688f6966e3db99daa326)) 42 | 43 | # [0.3.0](https://github.com/sparkforge/node-size-analyzer/compare/v0.2.0...v0.3.0) (2025-05-06) 44 | 45 | 46 | ### Features 47 | 48 | * add scrolling controls for module list ([e928f07](https://github.com/sparkforge/node-size-analyzer/commit/e928f075680ebd7dc75091ca28eb3bdb48fe8197)) 49 | 50 | # [0.2.0](https://github.com/sparkforge/node-size-analyzer/compare/v0.1.0...v0.2.0) (2025-05-06) 51 | 52 | 53 | ### Bug Fixes 54 | 55 | * resolve shell substitution issues in semantic-release workflow ([99fa410](https://github.com/sparkforge/node-size-analyzer/commit/99fa410117e5d4677be9a8664834961c7e60bb24)) 56 | 57 | 58 | ### Features 59 | 60 | * configure semantic versioning and automated releases ([eab1b42](https://github.com/sparkforge/node-size-analyzer/commit/eab1b42dd47ca2e68e2c462c956c4157ecf85f99)) 61 | 62 | # Changelog 63 | 64 | All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/). 65 | 66 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 67 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 68 | 69 | ## [0.1.0] - 2024-05-06 70 | 71 | ### Added 72 | - Initial release 73 | - Interactive terminal UI using ratatui 74 | - Real-time size calculation of node_modules 75 | - Sorted display by size (largest modules first) 76 | - Human-readable size formatting (B, KB, MB) 77 | - Cross-platform support (Windows, MacOS, Linux) 78 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Node Size Analyzer 2 | 3 | Thank you for considering contributing to Node Size Analyzer! This document provides guidelines for contributing to the project and explains our semantic release process. 4 | 5 | ## Commit Message Guidelines 6 | 7 | We use semantic versioning and conventional commits to automatically determine version numbers and generate changelogs. Please follow these rules when writing commit messages: 8 | 9 | ### Commit Message Format 10 | 11 | ``` 12 | (): 13 | 14 | [optional body] 15 | 16 | [optional footer(s)] 17 | ``` 18 | 19 | ### Types 20 | 21 | The `type` field must be one of the following: 22 | 23 | - **feat**: A new feature (triggers a MINOR version bump) 24 | - **fix**: A bug fix (triggers a PATCH version bump) 25 | - **docs**: Documentation only changes (triggers a PATCH version bump) 26 | - **style**: Changes that do not affect the meaning of the code (triggers a PATCH version bump) 27 | - **refactor**: A code change that neither fixes a bug nor adds a feature (triggers a PATCH version bump) 28 | - **perf**: A code change that improves performance (triggers a PATCH version bump) 29 | - **test**: Adding missing tests or correcting existing tests (triggers a PATCH version bump) 30 | - **build**: Changes that affect the build system or external dependencies (triggers a PATCH version bump) 31 | - **ci**: Changes to our CI configuration files and scripts (triggers a PATCH version bump) 32 | - **chore**: Other changes that don't modify src or test files (triggers a PATCH version bump) 33 | 34 | ### Breaking Changes 35 | 36 | Breaking changes should be indicated by adding `BREAKING CHANGE:` in the commit message body, or by appending `!` after the type/scope. This will trigger a MAJOR version bump. 37 | 38 | Example: 39 | ``` 40 | feat(api)!: change API response format 41 | 42 | BREAKING CHANGE: The API response format has been completely redesigned. 43 | ``` 44 | 45 | ### Examples 46 | 47 | ``` 48 | feat: add support for nested modules scanning 49 | 50 | fix(output): correct size calculation for symlinks 51 | 52 | docs: update installation instructions 53 | 54 | test: add unit tests for directory traversal 55 | 56 | refactor!: completely redesign the CLI interface 57 | 58 | BREAKING CHANGE: The CLI interface has been redesigned with new flag names. 59 | ``` 60 | 61 | ## Pull Request Process 62 | 63 | 1. Fork the repository 64 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 65 | 3. Add tests for your changes 66 | 4. Ensure all tests pass (`cargo test`) 67 | 5. Commit your changes following the semantic commit message format 68 | 6. Push to the branch (`git push origin feature/amazing-feature`) 69 | 7. Open a Pull Request 70 | 71 | When your PR is merged to main, our fully automated semantic release process will: 72 | 73 | 1. Analyze commit messages to determine the next version number 74 | 2. Update the version in Cargo.toml 75 | 3. Create/update the CHANGELOG.md entry 76 | 4. Create a Git tag for the new version 77 | 5. Create a new GitHub release with binaries for all platforms 78 | 6. Automatically publish to crates.io 79 | 80 | The entire process is automated through GitHub Actions workflows: 81 | 82 | - **Semantic Release Workflow**: Triggered when code is pushed to main. It analyzes commits, determines the next version, updates files, and creates a tag. 83 | - **Release Workflow**: Triggered by the new tag. It builds binaries, creates a GitHub release, and publishes to crates.io. 84 | 85 | ### Publishing to crates.io 86 | 87 | Publishing to crates.io is fully automated. The workflow: 88 | 89 | 1. Verifies the package is publishable with a dry run 90 | 2. Validates that the version in Cargo.toml matches the Git tag 91 | 3. Publishes to crates.io with automatic retries if needed 92 | 4. Verifies the package is available on crates.io 93 | 94 | No manual action is required for releases if your commits follow the conventional commit format. 95 | 96 | ## Development Setup 97 | 98 | ```bash 99 | git clone https://github.com/Caryyon/node-size-analyzer.git 100 | cd node-size-analyzer 101 | cargo build 102 | ``` 103 | 104 | ## Running Tests 105 | 106 | ```bash 107 | cargo test 108 | ``` 109 | 110 | Thank you for contributing to Node Size Analyzer! -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "allocator-api2" 16 | version = "0.2.21" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 19 | 20 | [[package]] 21 | name = "android-tzdata" 22 | version = "0.1.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 25 | 26 | [[package]] 27 | name = "android_system_properties" 28 | version = "0.1.5" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 31 | dependencies = [ 32 | "libc", 33 | ] 34 | 35 | [[package]] 36 | name = "autocfg" 37 | version = "1.4.0" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 40 | 41 | [[package]] 42 | name = "bitflags" 43 | version = "2.6.0" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 46 | 47 | [[package]] 48 | name = "bumpalo" 49 | version = "3.17.0" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 52 | 53 | [[package]] 54 | name = "cassowary" 55 | version = "0.3.0" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 58 | 59 | [[package]] 60 | name = "cc" 61 | version = "1.2.21" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "8691782945451c1c383942c4874dbe63814f61cb57ef773cda2972682b7bb3c0" 64 | dependencies = [ 65 | "shlex", 66 | ] 67 | 68 | [[package]] 69 | name = "cfg-if" 70 | version = "1.0.0" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 73 | 74 | [[package]] 75 | name = "chrono" 76 | version = "0.4.41" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" 79 | dependencies = [ 80 | "android-tzdata", 81 | "iana-time-zone", 82 | "js-sys", 83 | "num-traits", 84 | "wasm-bindgen", 85 | "windows-link", 86 | ] 87 | 88 | [[package]] 89 | name = "core-foundation-sys" 90 | version = "0.8.7" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 93 | 94 | [[package]] 95 | name = "crossterm" 96 | version = "0.27.0" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" 99 | dependencies = [ 100 | "bitflags", 101 | "crossterm_winapi", 102 | "libc", 103 | "mio", 104 | "parking_lot", 105 | "signal-hook", 106 | "signal-hook-mio", 107 | "winapi", 108 | ] 109 | 110 | [[package]] 111 | name = "crossterm_winapi" 112 | version = "0.9.1" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 115 | dependencies = [ 116 | "winapi", 117 | ] 118 | 119 | [[package]] 120 | name = "either" 121 | version = "1.13.0" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 124 | 125 | [[package]] 126 | name = "equivalent" 127 | version = "1.0.1" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 130 | 131 | [[package]] 132 | name = "errno" 133 | version = "0.3.11" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" 136 | dependencies = [ 137 | "libc", 138 | "windows-sys 0.59.0", 139 | ] 140 | 141 | [[package]] 142 | name = "fastrand" 143 | version = "2.3.0" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 146 | 147 | [[package]] 148 | name = "foldhash" 149 | version = "0.1.4" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" 152 | 153 | [[package]] 154 | name = "getrandom" 155 | version = "0.3.2" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 158 | dependencies = [ 159 | "cfg-if", 160 | "libc", 161 | "r-efi", 162 | "wasi 0.14.2+wasi-0.2.4", 163 | ] 164 | 165 | [[package]] 166 | name = "hashbrown" 167 | version = "0.15.2" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 170 | dependencies = [ 171 | "allocator-api2", 172 | "equivalent", 173 | "foldhash", 174 | ] 175 | 176 | [[package]] 177 | name = "heck" 178 | version = "0.4.1" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 181 | 182 | [[package]] 183 | name = "iana-time-zone" 184 | version = "0.1.63" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 187 | dependencies = [ 188 | "android_system_properties", 189 | "core-foundation-sys", 190 | "iana-time-zone-haiku", 191 | "js-sys", 192 | "log", 193 | "wasm-bindgen", 194 | "windows-core", 195 | ] 196 | 197 | [[package]] 198 | name = "iana-time-zone-haiku" 199 | version = "0.1.2" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 202 | dependencies = [ 203 | "cc", 204 | ] 205 | 206 | [[package]] 207 | name = "indoc" 208 | version = "2.0.5" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" 211 | 212 | [[package]] 213 | name = "itertools" 214 | version = "0.11.0" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" 217 | dependencies = [ 218 | "either", 219 | ] 220 | 221 | [[package]] 222 | name = "itoa" 223 | version = "1.0.15" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 226 | 227 | [[package]] 228 | name = "js-sys" 229 | version = "0.3.77" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 232 | dependencies = [ 233 | "once_cell", 234 | "wasm-bindgen", 235 | ] 236 | 237 | [[package]] 238 | name = "libc" 239 | version = "0.2.169" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 242 | 243 | [[package]] 244 | name = "linux-raw-sys" 245 | version = "0.9.4" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 248 | 249 | [[package]] 250 | name = "lock_api" 251 | version = "0.4.12" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 254 | dependencies = [ 255 | "autocfg", 256 | "scopeguard", 257 | ] 258 | 259 | [[package]] 260 | name = "log" 261 | version = "0.4.22" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 264 | 265 | [[package]] 266 | name = "lru" 267 | version = "0.12.5" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" 270 | dependencies = [ 271 | "hashbrown", 272 | ] 273 | 274 | [[package]] 275 | name = "memchr" 276 | version = "2.7.4" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 279 | 280 | [[package]] 281 | name = "mio" 282 | version = "0.8.11" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 285 | dependencies = [ 286 | "libc", 287 | "log", 288 | "wasi 0.11.0+wasi-snapshot-preview1", 289 | "windows-sys 0.48.0", 290 | ] 291 | 292 | [[package]] 293 | name = "node-size-analyzer" 294 | version = "0.5.0" 295 | dependencies = [ 296 | "chrono", 297 | "crossterm", 298 | "ratatui", 299 | "regex", 300 | "serde", 301 | "serde_json", 302 | "tempfile", 303 | "walkdir", 304 | ] 305 | 306 | [[package]] 307 | name = "num-traits" 308 | version = "0.2.19" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 311 | dependencies = [ 312 | "autocfg", 313 | ] 314 | 315 | [[package]] 316 | name = "once_cell" 317 | version = "1.21.3" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 320 | 321 | [[package]] 322 | name = "parking_lot" 323 | version = "0.12.3" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 326 | dependencies = [ 327 | "lock_api", 328 | "parking_lot_core", 329 | ] 330 | 331 | [[package]] 332 | name = "parking_lot_core" 333 | version = "0.9.10" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 336 | dependencies = [ 337 | "cfg-if", 338 | "libc", 339 | "redox_syscall", 340 | "smallvec", 341 | "windows-targets 0.52.6", 342 | ] 343 | 344 | [[package]] 345 | name = "paste" 346 | version = "1.0.15" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 349 | 350 | [[package]] 351 | name = "proc-macro2" 352 | version = "1.0.92" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 355 | dependencies = [ 356 | "unicode-ident", 357 | ] 358 | 359 | [[package]] 360 | name = "quote" 361 | version = "1.0.37" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 364 | dependencies = [ 365 | "proc-macro2", 366 | ] 367 | 368 | [[package]] 369 | name = "r-efi" 370 | version = "5.2.0" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 373 | 374 | [[package]] 375 | name = "ratatui" 376 | version = "0.24.0" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "0ebc917cfb527a566c37ecb94c7e3fd098353516fb4eb6bea17015ade0182425" 379 | dependencies = [ 380 | "bitflags", 381 | "cassowary", 382 | "crossterm", 383 | "indoc", 384 | "itertools", 385 | "lru", 386 | "paste", 387 | "strum", 388 | "unicode-segmentation", 389 | "unicode-width", 390 | ] 391 | 392 | [[package]] 393 | name = "redox_syscall" 394 | version = "0.5.8" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" 397 | dependencies = [ 398 | "bitflags", 399 | ] 400 | 401 | [[package]] 402 | name = "regex" 403 | version = "1.11.1" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 406 | dependencies = [ 407 | "aho-corasick", 408 | "memchr", 409 | "regex-automata", 410 | "regex-syntax", 411 | ] 412 | 413 | [[package]] 414 | name = "regex-automata" 415 | version = "0.4.9" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 418 | dependencies = [ 419 | "aho-corasick", 420 | "memchr", 421 | "regex-syntax", 422 | ] 423 | 424 | [[package]] 425 | name = "regex-syntax" 426 | version = "0.8.5" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 429 | 430 | [[package]] 431 | name = "rustix" 432 | version = "1.0.7" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" 435 | dependencies = [ 436 | "bitflags", 437 | "errno", 438 | "libc", 439 | "linux-raw-sys", 440 | "windows-sys 0.59.0", 441 | ] 442 | 443 | [[package]] 444 | name = "rustversion" 445 | version = "1.0.18" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" 448 | 449 | [[package]] 450 | name = "ryu" 451 | version = "1.0.20" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 454 | 455 | [[package]] 456 | name = "same-file" 457 | version = "1.0.6" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 460 | dependencies = [ 461 | "winapi-util", 462 | ] 463 | 464 | [[package]] 465 | name = "scopeguard" 466 | version = "1.2.0" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 469 | 470 | [[package]] 471 | name = "serde" 472 | version = "1.0.219" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 475 | dependencies = [ 476 | "serde_derive", 477 | ] 478 | 479 | [[package]] 480 | name = "serde_derive" 481 | version = "1.0.219" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 484 | dependencies = [ 485 | "proc-macro2", 486 | "quote", 487 | "syn", 488 | ] 489 | 490 | [[package]] 491 | name = "serde_json" 492 | version = "1.0.140" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 495 | dependencies = [ 496 | "itoa", 497 | "memchr", 498 | "ryu", 499 | "serde", 500 | ] 501 | 502 | [[package]] 503 | name = "shlex" 504 | version = "1.3.0" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 507 | 508 | [[package]] 509 | name = "signal-hook" 510 | version = "0.3.17" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 513 | dependencies = [ 514 | "libc", 515 | "signal-hook-registry", 516 | ] 517 | 518 | [[package]] 519 | name = "signal-hook-mio" 520 | version = "0.2.4" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 523 | dependencies = [ 524 | "libc", 525 | "mio", 526 | "signal-hook", 527 | ] 528 | 529 | [[package]] 530 | name = "signal-hook-registry" 531 | version = "1.4.2" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 534 | dependencies = [ 535 | "libc", 536 | ] 537 | 538 | [[package]] 539 | name = "smallvec" 540 | version = "1.13.2" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 543 | 544 | [[package]] 545 | name = "strum" 546 | version = "0.25.0" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" 549 | dependencies = [ 550 | "strum_macros", 551 | ] 552 | 553 | [[package]] 554 | name = "strum_macros" 555 | version = "0.25.3" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" 558 | dependencies = [ 559 | "heck", 560 | "proc-macro2", 561 | "quote", 562 | "rustversion", 563 | "syn", 564 | ] 565 | 566 | [[package]] 567 | name = "syn" 568 | version = "2.0.91" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035" 571 | dependencies = [ 572 | "proc-macro2", 573 | "quote", 574 | "unicode-ident", 575 | ] 576 | 577 | [[package]] 578 | name = "tempfile" 579 | version = "3.19.1" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" 582 | dependencies = [ 583 | "fastrand", 584 | "getrandom", 585 | "once_cell", 586 | "rustix", 587 | "windows-sys 0.59.0", 588 | ] 589 | 590 | [[package]] 591 | name = "unicode-ident" 592 | version = "1.0.14" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 595 | 596 | [[package]] 597 | name = "unicode-segmentation" 598 | version = "1.12.0" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 601 | 602 | [[package]] 603 | name = "unicode-width" 604 | version = "0.1.14" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 607 | 608 | [[package]] 609 | name = "walkdir" 610 | version = "2.5.0" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 613 | dependencies = [ 614 | "same-file", 615 | "winapi-util", 616 | ] 617 | 618 | [[package]] 619 | name = "wasi" 620 | version = "0.11.0+wasi-snapshot-preview1" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 623 | 624 | [[package]] 625 | name = "wasi" 626 | version = "0.14.2+wasi-0.2.4" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 629 | dependencies = [ 630 | "wit-bindgen-rt", 631 | ] 632 | 633 | [[package]] 634 | name = "wasm-bindgen" 635 | version = "0.2.100" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 638 | dependencies = [ 639 | "cfg-if", 640 | "once_cell", 641 | "rustversion", 642 | "wasm-bindgen-macro", 643 | ] 644 | 645 | [[package]] 646 | name = "wasm-bindgen-backend" 647 | version = "0.2.100" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 650 | dependencies = [ 651 | "bumpalo", 652 | "log", 653 | "proc-macro2", 654 | "quote", 655 | "syn", 656 | "wasm-bindgen-shared", 657 | ] 658 | 659 | [[package]] 660 | name = "wasm-bindgen-macro" 661 | version = "0.2.100" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 664 | dependencies = [ 665 | "quote", 666 | "wasm-bindgen-macro-support", 667 | ] 668 | 669 | [[package]] 670 | name = "wasm-bindgen-macro-support" 671 | version = "0.2.100" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 674 | dependencies = [ 675 | "proc-macro2", 676 | "quote", 677 | "syn", 678 | "wasm-bindgen-backend", 679 | "wasm-bindgen-shared", 680 | ] 681 | 682 | [[package]] 683 | name = "wasm-bindgen-shared" 684 | version = "0.2.100" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 687 | dependencies = [ 688 | "unicode-ident", 689 | ] 690 | 691 | [[package]] 692 | name = "winapi" 693 | version = "0.3.9" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 696 | dependencies = [ 697 | "winapi-i686-pc-windows-gnu", 698 | "winapi-x86_64-pc-windows-gnu", 699 | ] 700 | 701 | [[package]] 702 | name = "winapi-i686-pc-windows-gnu" 703 | version = "0.4.0" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 706 | 707 | [[package]] 708 | name = "winapi-util" 709 | version = "0.1.9" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 712 | dependencies = [ 713 | "windows-sys 0.59.0", 714 | ] 715 | 716 | [[package]] 717 | name = "winapi-x86_64-pc-windows-gnu" 718 | version = "0.4.0" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 721 | 722 | [[package]] 723 | name = "windows-core" 724 | version = "0.61.0" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" 727 | dependencies = [ 728 | "windows-implement", 729 | "windows-interface", 730 | "windows-link", 731 | "windows-result", 732 | "windows-strings", 733 | ] 734 | 735 | [[package]] 736 | name = "windows-implement" 737 | version = "0.60.0" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 740 | dependencies = [ 741 | "proc-macro2", 742 | "quote", 743 | "syn", 744 | ] 745 | 746 | [[package]] 747 | name = "windows-interface" 748 | version = "0.59.1" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 751 | dependencies = [ 752 | "proc-macro2", 753 | "quote", 754 | "syn", 755 | ] 756 | 757 | [[package]] 758 | name = "windows-link" 759 | version = "0.1.1" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" 762 | 763 | [[package]] 764 | name = "windows-result" 765 | version = "0.3.2" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" 768 | dependencies = [ 769 | "windows-link", 770 | ] 771 | 772 | [[package]] 773 | name = "windows-strings" 774 | version = "0.4.0" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" 777 | dependencies = [ 778 | "windows-link", 779 | ] 780 | 781 | [[package]] 782 | name = "windows-sys" 783 | version = "0.48.0" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 786 | dependencies = [ 787 | "windows-targets 0.48.5", 788 | ] 789 | 790 | [[package]] 791 | name = "windows-sys" 792 | version = "0.59.0" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 795 | dependencies = [ 796 | "windows-targets 0.52.6", 797 | ] 798 | 799 | [[package]] 800 | name = "windows-targets" 801 | version = "0.48.5" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 804 | dependencies = [ 805 | "windows_aarch64_gnullvm 0.48.5", 806 | "windows_aarch64_msvc 0.48.5", 807 | "windows_i686_gnu 0.48.5", 808 | "windows_i686_msvc 0.48.5", 809 | "windows_x86_64_gnu 0.48.5", 810 | "windows_x86_64_gnullvm 0.48.5", 811 | "windows_x86_64_msvc 0.48.5", 812 | ] 813 | 814 | [[package]] 815 | name = "windows-targets" 816 | version = "0.52.6" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 819 | dependencies = [ 820 | "windows_aarch64_gnullvm 0.52.6", 821 | "windows_aarch64_msvc 0.52.6", 822 | "windows_i686_gnu 0.52.6", 823 | "windows_i686_gnullvm", 824 | "windows_i686_msvc 0.52.6", 825 | "windows_x86_64_gnu 0.52.6", 826 | "windows_x86_64_gnullvm 0.52.6", 827 | "windows_x86_64_msvc 0.52.6", 828 | ] 829 | 830 | [[package]] 831 | name = "windows_aarch64_gnullvm" 832 | version = "0.48.5" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 835 | 836 | [[package]] 837 | name = "windows_aarch64_gnullvm" 838 | version = "0.52.6" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 841 | 842 | [[package]] 843 | name = "windows_aarch64_msvc" 844 | version = "0.48.5" 845 | source = "registry+https://github.com/rust-lang/crates.io-index" 846 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 847 | 848 | [[package]] 849 | name = "windows_aarch64_msvc" 850 | version = "0.52.6" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 853 | 854 | [[package]] 855 | name = "windows_i686_gnu" 856 | version = "0.48.5" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 859 | 860 | [[package]] 861 | name = "windows_i686_gnu" 862 | version = "0.52.6" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 865 | 866 | [[package]] 867 | name = "windows_i686_gnullvm" 868 | version = "0.52.6" 869 | source = "registry+https://github.com/rust-lang/crates.io-index" 870 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 871 | 872 | [[package]] 873 | name = "windows_i686_msvc" 874 | version = "0.48.5" 875 | source = "registry+https://github.com/rust-lang/crates.io-index" 876 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 877 | 878 | [[package]] 879 | name = "windows_i686_msvc" 880 | version = "0.52.6" 881 | source = "registry+https://github.com/rust-lang/crates.io-index" 882 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 883 | 884 | [[package]] 885 | name = "windows_x86_64_gnu" 886 | version = "0.48.5" 887 | source = "registry+https://github.com/rust-lang/crates.io-index" 888 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 889 | 890 | [[package]] 891 | name = "windows_x86_64_gnu" 892 | version = "0.52.6" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 895 | 896 | [[package]] 897 | name = "windows_x86_64_gnullvm" 898 | version = "0.48.5" 899 | source = "registry+https://github.com/rust-lang/crates.io-index" 900 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 901 | 902 | [[package]] 903 | name = "windows_x86_64_gnullvm" 904 | version = "0.52.6" 905 | source = "registry+https://github.com/rust-lang/crates.io-index" 906 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 907 | 908 | [[package]] 909 | name = "windows_x86_64_msvc" 910 | version = "0.48.5" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 913 | 914 | [[package]] 915 | name = "windows_x86_64_msvc" 916 | version = "0.52.6" 917 | source = "registry+https://github.com/rust-lang/crates.io-index" 918 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 919 | 920 | [[package]] 921 | name = "wit-bindgen-rt" 922 | version = "0.39.0" 923 | source = "registry+https://github.com/rust-lang/crates.io-index" 924 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 925 | dependencies = [ 926 | "bitflags", 927 | ] 928 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "node-size-analyzer" 3 | version = "0.7.1" 4 | edition = "2021" 5 | description = "CLI tool to analyze node_modules sizes" 6 | authors = ["Cary Wolff "] 7 | license = "MIT" 8 | repository = "https://github.com/Caryyon/node-size-analyzer" 9 | 10 | [dependencies] 11 | ratatui = "0.24" 12 | crossterm = "0.27" 13 | serde = { version = "1.0", features = ["derive"] } 14 | serde_json = "1.0" 15 | chrono = "0.4" 16 | walkdir = "2.4" 17 | regex = "1.10" 18 | 19 | [dev-dependencies] 20 | tempfile = "3.8" 21 | 22 | [[bin]] 23 | name = "node-size" 24 | path = "src/main.rs" 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 node-size-analyzer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node Size Analyzer 2 | 3 | A fast CLI tool to analyze and visualize the size of your node_modules dependencies using a terminal UI. 4 | 5 | ![Screenshot of Node Size Analyzer](screenshot.png) 6 | 7 | ## Features 8 | 9 | - Interactive terminal UI using ratatui 10 | - Real-time size calculation of node_modules 11 | - Sorted display by size (largest modules first) 12 | - Human-readable size formatting (B, KB, MB) 13 | - Cross-platform support (Windows, MacOS, Linux) 14 | - Fast directory traversal for quick analysis 15 | 16 | ## Installation 17 | 18 | ### Using Cargo 19 | 20 | The latest stable version is automatically published to crates.io: 21 | 22 | ```bash 23 | cargo install node-size-analyzer 24 | ``` 25 | 26 | ### From GitHub Releases 27 | 28 | Pre-built binaries for all major platforms are automatically generated for each release and available from the [releases page](https://github.com/Caryyon/node-size-analyzer/releases). 29 | 30 | #### Linux/MacOS 31 | 32 | ```bash 33 | # Download the latest release for your platform 34 | curl -L https://github.com/Caryyon/node-size-analyzer/releases/latest/download/node-size-linux -o node-size 35 | # or for macOS: 36 | # curl -L https://github.com/Caryyon/node-size-analyzer/releases/latest/download/node-size-macos -o node-size 37 | chmod +x node-size 38 | ./node-size 39 | ``` 40 | 41 | #### Windows 42 | 43 | ```bash 44 | # Download using PowerShell 45 | Invoke-WebRequest -Uri https://github.com/Caryyon/node-size-analyzer/releases/latest/download/node-size-windows.exe -OutFile node-size.exe 46 | .\node-size.exe 47 | ``` 48 | 49 | New releases are automatically published when changes are merged to the main branch. 50 | 51 | ## Usage 52 | 53 | 1. Navigate to your project directory containing node_modules 54 | 2. Run `node-size` 55 | 3. The terminal UI will display all modules sorted by size 56 | 4. Use the following key controls: 57 | - Press 'q' to exit 58 | - Arrow Up/Down or 'k'/'j' to scroll one line 59 | - Page Up/Down to scroll a full page 60 | - Home/End to jump to beginning/end of the list 61 | 62 | ### Example Output 63 | 64 | The tool displays a table with: 65 | - Module names (left column) 66 | - Size in human-readable format (right column) 67 | - Sorted from largest to smallest 68 | 69 | ## Building from Source 70 | 71 | ```bash 72 | git clone https://github.com/Caryyon/node-size-analyzer.git 73 | cd node-size-analyzer 74 | cargo build --release 75 | ``` 76 | 77 | The compiled binary will be available at `target/release/node-size`. 78 | 79 | ## Development 80 | 81 | ### Project Structure 82 | 83 | - `src/main.rs` - Main application code 84 | - `Cargo.toml` - Project dependencies and configuration 85 | 86 | ### Running Tests 87 | 88 | This project includes unit tests for core functionality. To run the tests: 89 | 90 | ```bash 91 | cargo test 92 | ``` 93 | 94 | The test suite includes: 95 | - Tests for size formatting 96 | - Tests for directory size calculation 97 | - Tests for module scanning and sorting 98 | 99 | ### Adding Features 100 | 101 | When adding new features, please ensure: 102 | 1. All tests pass (run `cargo test`) 103 | 2. Code follows Rust formatting standards (run `cargo fmt`) 104 | 3. No clippy warnings (run `cargo clippy`) 105 | 106 | ## Contributing 107 | 108 | This project uses [Semantic Versioning](https://semver.org/) and [Conventional Commits](https://www.conventionalcommits.org/). 109 | 110 | 1. Fork the repository 111 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 112 | 3. Add tests for your changes 113 | 4. Ensure all tests pass (`cargo test`) 114 | 5. Commit your changes using the conventional commit format: 115 | - `feat: add new feature` (triggers minor version bump) 116 | - `fix: resolve bug issue` (triggers patch version bump) 117 | - `docs: update documentation` (triggers patch version bump) 118 | - `feat!: redesign API` (triggers major version bump) 119 | 6. Push to the branch (`git push origin feature/amazing-feature`) 120 | 7. Open a Pull Request 121 | 122 | See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines on commit messages and the release process. 123 | 124 | ## How It Works 125 | 126 | The tool: 127 | 1. Scans your `node_modules` directory recursively 128 | 2. Calculates the size of each top-level module 129 | 3. Sorts modules by size (largest first) 130 | 4. Renders an interactive table UI with the results 131 | 132 | ## License 133 | 134 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 135 | 136 | ## Acknowledgments 137 | 138 | - Built with [ratatui](https://github.com/ratatui-org/ratatui) for the terminal UI 139 | - Terminal handling by [crossterm](https://github.com/crossterm-rs/crossterm) 140 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkforge/node-size-analyzer/067e4b79ac957100b76b7d2a911b63ee9ae47b68/screenshot.png -------------------------------------------------------------------------------- /scripts/sync-crates-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script is designed to check if the current package version exists on crates.io 4 | # If it doesn't, it will trigger the workflow to publish it 5 | 6 | set -e 7 | 8 | # Get the current version from Cargo.toml 9 | CURRENT_VERSION=$(grep '^version =' Cargo.toml | head -n 1 | cut -d '"' -f 2) 10 | 11 | echo "Current version in Cargo.toml: $CURRENT_VERSION" 12 | 13 | # Check if this version exists on crates.io 14 | RESPONSE=$(curl -s https://crates.io/api/v1/crates/node-size-analyzer/$CURRENT_VERSION) 15 | 16 | if echo "$RESPONSE" | grep -q "\"version\":\"$CURRENT_VERSION\""; then 17 | echo "Version $CURRENT_VERSION already exists on crates.io. No action needed." 18 | exit 0 19 | else 20 | echo "Version $CURRENT_VERSION does not exist on crates.io." 21 | 22 | # Check if gh CLI is available 23 | if command -v gh &> /dev/null; then 24 | echo "Triggering GitHub workflow to publish the new version..." 25 | gh workflow run force-publish.yml -f version=$CURRENT_VERSION 26 | echo "Workflow triggered. Check GitHub Actions for progress." 27 | else 28 | echo "GitHub CLI not available. Please manually trigger the 'Force Publish to Crates.io' workflow with version: $CURRENT_VERSION" 29 | fi 30 | fi -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | backend::CrosstermBackend, 3 | layout::{Constraint, Direction, Layout, Rect}, 4 | style::{Color, Modifier, Style}, 5 | text::{Line, Span, Text}, 6 | widgets::{Block, Borders, Clear, Paragraph, Row, Table, Wrap}, 7 | Terminal, 8 | }; 9 | use std::{collections::HashMap, fs, io, path::Path}; 10 | use crossterm::{ 11 | event::{self, Event, KeyCode}, 12 | execute, 13 | terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 14 | }; 15 | use serde::{Deserialize, Serialize}; 16 | use walkdir::WalkDir; 17 | 18 | #[derive(Debug, Deserialize, Serialize)] 19 | struct PackageJson { 20 | name: Option, 21 | version: Option, 22 | description: Option, 23 | author: Option, 24 | license: Option, 25 | homepage: Option, 26 | repository: Option, 27 | dependencies: Option>, 28 | #[serde(rename = "devDependencies")] 29 | dev_dependencies: Option>, 30 | #[serde(rename = "peerDependencies")] 31 | peer_dependencies: Option>, 32 | #[serde(rename = "optionalDependencies")] 33 | optional_dependencies: Option>, 34 | #[serde(rename = "publishConfig")] 35 | publish_config: Option>, 36 | } 37 | 38 | #[derive(Debug, Deserialize, Serialize)] 39 | struct Repository { 40 | #[serde(rename = "type")] 41 | repo_type: Option, 42 | url: Option, 43 | } 44 | 45 | impl Repository { 46 | fn to_string(&self) -> Option { 47 | match &self.url { 48 | Some(url) => Some(url.clone()), 49 | None => None, 50 | } 51 | } 52 | } 53 | 54 | #[derive(Debug, Clone)] 55 | pub struct ModuleInfo { 56 | pub name: String, 57 | pub size: u64, 58 | pub dependency_count: Option, 59 | pub last_updated: Option, 60 | pub license: Option, 61 | pub version: Option, 62 | pub description: Option, 63 | pub author: Option, 64 | pub homepage: Option, 65 | pub repository: Option, 66 | pub files_count: Option, 67 | pub file_types: Option>, // (extension, count) 68 | pub is_dev_dependency: bool, 69 | } 70 | 71 | pub fn get_dir_size(path: &Path) -> io::Result { 72 | let mut total = 0; 73 | for entry in fs::read_dir(path)? { 74 | let entry = entry?; 75 | let path = entry.path(); 76 | if path.is_file() { 77 | total += fs::metadata(&path)?.len(); 78 | } else if path.is_dir() { 79 | total += get_dir_size(&path)?; 80 | } 81 | } 82 | Ok(total) 83 | } 84 | 85 | pub fn format_size(size: u64) -> String { 86 | const KB: u64 = 1024; 87 | const MB: u64 = KB * 1024; 88 | 89 | if size >= MB { 90 | format!("{:.2} MB", size as f64 / MB as f64) 91 | } else if size >= KB { 92 | format!("{:.2} KB", size as f64 / KB as f64) 93 | } else { 94 | format!("{} B", size) 95 | } 96 | } 97 | 98 | pub fn scan_node_modules() -> io::Result> { 99 | scan_modules_dir(Path::new("node_modules")) 100 | } 101 | 102 | pub fn scan_modules_dir(node_modules: &Path) -> io::Result> { 103 | let mut modules = Vec::new(); 104 | 105 | for entry in fs::read_dir(node_modules)? { 106 | let entry = entry?; 107 | let path = entry.path(); 108 | if path.is_dir() { 109 | let size = get_dir_size(&path)?; 110 | let name = path.file_name().unwrap().to_string_lossy().into_owned(); 111 | 112 | // Create a basic module info 113 | let mut module = ModuleInfo { 114 | name, 115 | size, 116 | dependency_count: None, 117 | last_updated: None, 118 | license: None, 119 | version: None, 120 | description: None, 121 | author: None, 122 | homepage: None, 123 | repository: None, 124 | files_count: None, 125 | file_types: None, 126 | is_dev_dependency: false, 127 | }; 128 | 129 | // Try to get additional info from package.json 130 | let package_json_path = path.join("package.json"); 131 | if package_json_path.exists() { 132 | if let Ok(json_content) = fs::read_to_string(&package_json_path) { 133 | if let Ok(package_json) = serde_json::from_str::(&json_content) { 134 | module.version = package_json.version; 135 | module.description = package_json.description; 136 | module.license = package_json.license; 137 | module.author = package_json.author; 138 | module.homepage = package_json.homepage; 139 | module.repository = package_json.repository.and_then(|r| r.to_string()); 140 | 141 | // Count dependencies 142 | let mut dep_count = 0; 143 | if let Some(deps) = &package_json.dependencies { 144 | dep_count += deps.len(); 145 | } 146 | if let Some(deps) = &package_json.dev_dependencies { 147 | dep_count += deps.len(); 148 | } 149 | if let Some(deps) = &package_json.peer_dependencies { 150 | dep_count += deps.len(); 151 | } 152 | if let Some(deps) = &package_json.optional_dependencies { 153 | dep_count += deps.len(); 154 | } 155 | 156 | module.dependency_count = Some(dep_count); 157 | } 158 | } 159 | } 160 | 161 | // Count files and get file types 162 | let mut files_count = 0; 163 | let mut file_extensions: HashMap = HashMap::new(); 164 | 165 | for entry in WalkDir::new(&path).into_iter().filter_map(|e| e.ok()) { 166 | if entry.file_type().is_file() { 167 | files_count += 1; 168 | 169 | if let Some(extension) = entry.path().extension() { 170 | let ext = extension.to_string_lossy().to_string().to_lowercase(); 171 | *file_extensions.entry(ext).or_insert(0) += 1; 172 | } else { 173 | *file_extensions.entry("(no extension)".to_string()).or_insert(0) += 1; 174 | } 175 | } 176 | } 177 | 178 | module.files_count = Some(files_count); 179 | 180 | // Convert file_extensions HashMap to Vec and sort by count 181 | let mut file_types: Vec<(String, usize)> = file_extensions.into_iter().collect(); 182 | file_types.sort_by(|a, b| b.1.cmp(&a.1)); 183 | module.file_types = Some(file_types); 184 | 185 | // Get last modified time 186 | if let Ok(metadata) = fs::metadata(&path) { 187 | if let Ok(modified) = metadata.modified() { 188 | if let Ok(modified_time) = modified.elapsed() { 189 | let seconds_ago = modified_time.as_secs(); 190 | let last_updated = if seconds_ago < 60 { 191 | format!("{} seconds ago", seconds_ago) 192 | } else if seconds_ago < 3600 { 193 | format!("{} minutes ago", seconds_ago / 60) 194 | } else if seconds_ago < 86400 { 195 | format!("{} hours ago", seconds_ago / 3600) 196 | } else { 197 | format!("{} days ago", seconds_ago / 86400) 198 | }; 199 | module.last_updated = Some(last_updated); 200 | } 201 | } 202 | } 203 | 204 | modules.push(module); 205 | } 206 | } 207 | 208 | modules.sort_by(|a, b| b.size.cmp(&a.size)); 209 | Ok(modules) 210 | } 211 | 212 | enum AppMode { 213 | List, 214 | Detail, 215 | } 216 | 217 | struct AppState { 218 | modules: Vec, 219 | scroll_offset: usize, 220 | selected_index: Option, 221 | mode: AppMode, 222 | } 223 | 224 | fn render_detail_view(module: &ModuleInfo, area: Rect, f: &mut ratatui::Frame) { 225 | let block = Block::default() 226 | .title(format!("Module Details: {}", module.name)) 227 | .borders(Borders::ALL); 228 | 229 | let inner_area = block.inner(area); 230 | f.render_widget(block, area); 231 | 232 | let chunks = Layout::default() 233 | .direction(Direction::Vertical) 234 | .constraints([ 235 | Constraint::Length(8), // Basic info 236 | Constraint::Length(1), // Separator 237 | Constraint::Min(5), // File types 238 | ].as_ref()) 239 | .split(inner_area); 240 | 241 | // Basic info section 242 | let mut info_text = Vec::new(); 243 | info_text.push(Line::from(vec![ 244 | Span::styled("Size: ", Style::default().fg(Color::Yellow)), 245 | Span::raw(format_size(module.size)), 246 | ])); 247 | 248 | if let Some(version) = &module.version { 249 | info_text.push(Line::from(vec![ 250 | Span::styled("Version: ", Style::default().fg(Color::Yellow)), 251 | Span::raw(version), 252 | ])); 253 | } 254 | 255 | if let Some(license) = &module.license { 256 | info_text.push(Line::from(vec![ 257 | Span::styled("License: ", Style::default().fg(Color::Yellow)), 258 | Span::raw(license), 259 | ])); 260 | } 261 | 262 | if let Some(deps) = module.dependency_count { 263 | info_text.push(Line::from(vec![ 264 | Span::styled("Dependencies: ", Style::default().fg(Color::Yellow)), 265 | Span::raw(deps.to_string()), 266 | ])); 267 | } 268 | 269 | if let Some(files) = module.files_count { 270 | info_text.push(Line::from(vec![ 271 | Span::styled("Files: ", Style::default().fg(Color::Yellow)), 272 | Span::raw(files.to_string()), 273 | ])); 274 | } 275 | 276 | if let Some(last_updated) = &module.last_updated { 277 | info_text.push(Line::from(vec![ 278 | Span::styled("Last Updated: ", Style::default().fg(Color::Yellow)), 279 | Span::raw(last_updated), 280 | ])); 281 | } 282 | 283 | if let Some(description) = &module.description { 284 | info_text.push(Line::from(vec![ 285 | Span::styled("Description: ", Style::default().fg(Color::Yellow)), 286 | Span::raw(description), 287 | ])); 288 | } 289 | 290 | let basic_info = Paragraph::new(info_text) 291 | .block(Block::default().borders(Borders::NONE)) 292 | .wrap(Wrap { trim: true }); 293 | 294 | f.render_widget(basic_info, chunks[0]); 295 | 296 | // File types section 297 | let mut file_type_text = Vec::new(); 298 | file_type_text.push(Line::from( 299 | Span::styled("File Types:", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) 300 | )); 301 | 302 | if let Some(file_types) = &module.file_types { 303 | for (ext, count) in file_types.iter().take(10) { // Limit to top 10 types 304 | file_type_text.push(Line::from(vec![ 305 | Span::styled(format!("{}: ", ext), Style::default().fg(Color::Blue)), 306 | Span::raw(count.to_string()), 307 | Span::raw(" files"), 308 | ])); 309 | } 310 | 311 | if file_types.len() > 10 { 312 | file_type_text.push(Line::from( 313 | Span::styled("(and more...)", Style::default().fg(Color::DarkGray)) 314 | )); 315 | } 316 | } else { 317 | file_type_text.push(Line::from( 318 | Span::styled("No file type information available", Style::default().fg(Color::DarkGray)) 319 | )); 320 | } 321 | 322 | let file_types_info = Paragraph::new(file_type_text) 323 | .block(Block::default().borders(Borders::NONE)) 324 | .wrap(Wrap { trim: true }); 325 | 326 | f.render_widget(file_types_info, chunks[2]); 327 | 328 | // Links and navigation help at the bottom 329 | let help_text = Text::from(vec![ 330 | Line::from(vec![ 331 | Span::styled("ESC", Style::default().fg(Color::Yellow)), 332 | Span::raw(" to return to list view | "), 333 | Span::styled("q", Style::default().fg(Color::Yellow)), 334 | Span::raw(" to quit"), 335 | ]), 336 | ]); 337 | 338 | let help_paragraph = Paragraph::new(help_text) 339 | .style(Style::default().fg(Color::White)) 340 | .alignment(ratatui::layout::Alignment::Center); 341 | 342 | let help_area = Rect::new( 343 | area.x + 1, 344 | area.y + area.height - 2, 345 | area.width - 2, 346 | 1, 347 | ); 348 | 349 | f.render_widget(help_paragraph, help_area); 350 | } 351 | 352 | fn run_app() -> io::Result<()> { 353 | enable_raw_mode()?; 354 | let mut stdout = io::stdout(); 355 | execute!(stdout, EnterAlternateScreen)?; 356 | let backend = CrosstermBackend::new(stdout); 357 | let mut terminal = Terminal::new(backend)?; 358 | 359 | let modules = scan_node_modules()?; 360 | 361 | let mut app_state = AppState { 362 | modules, 363 | scroll_offset: 0, 364 | selected_index: None, 365 | mode: AppMode::List, 366 | }; 367 | 368 | loop { 369 | terminal.draw(|f| { 370 | let size = f.size(); 371 | 372 | match app_state.mode { 373 | AppMode::List => { 374 | let chunks = Layout::default() 375 | .direction(Direction::Vertical) 376 | .constraints([Constraint::Percentage(100)].as_ref()) 377 | .split(size); 378 | 379 | // Calculate visible area based on terminal size 380 | // Subtract 4 for header row and borders 381 | let max_visible_items = (chunks[0].height as usize).saturating_sub(4); 382 | 383 | // Ensure scroll offset doesn't go beyond available items 384 | let total_items = app_state.modules.len(); 385 | if app_state.scroll_offset > total_items.saturating_sub(max_visible_items) { 386 | app_state.scroll_offset = total_items.saturating_sub(max_visible_items); 387 | } 388 | 389 | // Create rows from visible range of modules 390 | let selected_style = Style::default().bg(Color::DarkGray); 391 | 392 | let rows: Vec = app_state.modules 393 | .iter() 394 | .enumerate() 395 | .skip(app_state.scroll_offset) 396 | .take(max_visible_items) 397 | .map(|(i, m)| { 398 | let style = match app_state.selected_index { 399 | Some(selected) if selected == i => selected_style, 400 | _ => Style::default(), 401 | }; 402 | 403 | Row::new(vec![ 404 | m.name.clone(), 405 | format_size(m.size), 406 | ]).style(style) 407 | }) 408 | .collect(); 409 | 410 | // Create scroll indicator for title 411 | let scroll_indicator = if total_items > max_visible_items { 412 | format!(" [{}-{}/{}]", 413 | app_state.scroll_offset + 1, 414 | (app_state.scroll_offset + rows.len()).min(total_items), 415 | total_items) 416 | } else { 417 | String::new() 418 | }; 419 | 420 | let title = format!("Node Modules Size{}", scroll_indicator); 421 | 422 | let table = Table::new(rows) 423 | .header(Row::new(vec!["Module", "Size"]).style(Style::default().fg(Color::Yellow))) 424 | .block(Block::default() 425 | .title(title) 426 | .borders(Borders::ALL)) 427 | .widths(&[ 428 | Constraint::Percentage(70), 429 | Constraint::Percentage(30), 430 | ]); 431 | 432 | f.render_widget(table, chunks[0]); 433 | 434 | // Add help text at the bottom 435 | let help_text = Text::from(vec![ 436 | Line::from(vec![ 437 | Span::styled(" ↑/↓: Navigate | ", Style::default().fg(Color::Gray)), 438 | Span::styled("Enter: ", Style::default().fg(Color::Yellow)), 439 | Span::styled("View Details | ", Style::default().fg(Color::Gray)), 440 | Span::styled("q: ", Style::default().fg(Color::Yellow)), 441 | Span::styled("Quit", Style::default().fg(Color::Gray)), 442 | ]), 443 | ]); 444 | 445 | let help_paragraph = Paragraph::new(help_text) 446 | .style(Style::default().fg(Color::White)) 447 | .alignment(ratatui::layout::Alignment::Center); 448 | 449 | let help_area = Rect::new( 450 | chunks[0].x, 451 | chunks[0].y + chunks[0].height - 1, 452 | chunks[0].width, 453 | 1, 454 | ); 455 | 456 | f.render_widget(help_paragraph, help_area); 457 | }, 458 | AppMode::Detail => { 459 | if let Some(idx) = app_state.selected_index { 460 | if let Some(module) = app_state.modules.get(idx) { 461 | // Add 10% padding on all sides 462 | let detail_area = Rect::new( 463 | size.x + size.width / 10, 464 | size.y + size.height / 10, 465 | size.width * 8 / 10, 466 | size.height * 8 / 10, 467 | ); 468 | 469 | // First render background 470 | f.render_widget(Clear, detail_area); 471 | 472 | // Then render detail view 473 | render_detail_view(module, detail_area, f); 474 | } 475 | } 476 | }, 477 | } 478 | })?; 479 | 480 | if let Event::Key(key) = event::read()? { 481 | match app_state.mode { 482 | AppMode::List => match key.code { 483 | KeyCode::Char('q') => break, 484 | KeyCode::Up | KeyCode::Char('k') => { 485 | if app_state.selected_index.is_none() { 486 | app_state.selected_index = Some(app_state.scroll_offset); 487 | } else if let Some(selected) = app_state.selected_index { 488 | if selected > 0 { 489 | app_state.selected_index = Some(selected - 1); 490 | 491 | // Adjust scroll if necessary 492 | if selected < app_state.scroll_offset + 1 { 493 | app_state.scroll_offset = app_state.scroll_offset.saturating_sub(1); 494 | } 495 | } 496 | } 497 | }, 498 | KeyCode::Down | KeyCode::Char('j') => { 499 | if app_state.selected_index.is_none() { 500 | app_state.selected_index = Some(app_state.scroll_offset); 501 | } else if let Some(selected) = app_state.selected_index { 502 | if selected < app_state.modules.len() - 1 { 503 | app_state.selected_index = Some(selected + 1); 504 | 505 | // Get visible height 506 | let visible_height = terminal.size()?.height as usize - 4; 507 | 508 | // Adjust scroll if necessary 509 | if selected >= app_state.scroll_offset + visible_height - 1 { 510 | app_state.scroll_offset += 1; 511 | } 512 | } 513 | } 514 | }, 515 | KeyCode::PageUp => { 516 | // Terminal size - 4 (header + borders) 517 | let page_size = terminal.size()?.height as usize - 4; 518 | app_state.scroll_offset = app_state.scroll_offset.saturating_sub(page_size); 519 | 520 | // Also adjust selected item 521 | if let Some(selected) = app_state.selected_index { 522 | let new_selected = selected.saturating_sub(page_size); 523 | app_state.selected_index = Some(new_selected); 524 | } 525 | }, 526 | KeyCode::PageDown => { 527 | // Terminal size - 4 (header + borders) 528 | let page_size = terminal.size()?.height as usize - 4; 529 | let max_scroll = app_state.modules.len().saturating_sub(page_size); 530 | 531 | app_state.scroll_offset = (app_state.scroll_offset + page_size).min(max_scroll); 532 | 533 | // Also adjust selected item 534 | if let Some(selected) = app_state.selected_index { 535 | let new_selected = (selected + page_size).min(app_state.modules.len() - 1); 536 | app_state.selected_index = Some(new_selected); 537 | } 538 | }, 539 | KeyCode::Home => { 540 | app_state.scroll_offset = 0; 541 | if app_state.selected_index.is_some() { 542 | app_state.selected_index = Some(0); 543 | } 544 | }, 545 | KeyCode::End => { 546 | // Go to last page 547 | let max_visible_items = terminal.size()?.height as usize - 4; 548 | app_state.scroll_offset = app_state.modules.len().saturating_sub(max_visible_items); 549 | 550 | if app_state.selected_index.is_some() { 551 | app_state.selected_index = Some(app_state.modules.len() - 1); 552 | } 553 | }, 554 | KeyCode::Enter => { 555 | if app_state.selected_index.is_some() { 556 | app_state.mode = AppMode::Detail; 557 | } 558 | }, 559 | _ => {} 560 | }, 561 | AppMode::Detail => match key.code { 562 | KeyCode::Char('q') => break, 563 | KeyCode::Esc => app_state.mode = AppMode::List, 564 | _ => {} 565 | }, 566 | } 567 | } 568 | } 569 | 570 | disable_raw_mode()?; 571 | execute!(terminal.backend_mut(), LeaveAlternateScreen)?; 572 | Ok(()) 573 | } 574 | 575 | fn main() -> io::Result<()> { 576 | run_app() 577 | } 578 | 579 | #[cfg(test)] 580 | mod tests { 581 | use super::*; 582 | use std::fs::{self, File}; 583 | use std::io::Write; 584 | use tempfile::tempdir; 585 | 586 | #[test] 587 | fn test_format_size() { 588 | assert_eq!(format_size(500), "500 B"); 589 | assert_eq!(format_size(1024), "1.00 KB"); 590 | assert_eq!(format_size(1500), "1.46 KB"); 591 | assert_eq!(format_size(1024 * 1024), "1.00 MB"); 592 | assert_eq!(format_size(1024 * 1024 * 2 + 1024 * 100), "2.10 MB"); 593 | } 594 | 595 | #[test] 596 | fn test_get_dir_size() -> io::Result<()> { 597 | // Create a temporary directory 598 | let temp_dir = tempdir()?; 599 | let temp_path = temp_dir.path(); 600 | 601 | // Create a file with known content 602 | let file_path = temp_path.join("test_file.txt"); 603 | let content = "Hello, world!"; 604 | let mut file = File::create(&file_path)?; 605 | file.write_all(content.as_bytes())?; 606 | 607 | // Create a subdirectory with a file 608 | let subdir_path = temp_path.join("subdir"); 609 | fs::create_dir(&subdir_path)?; 610 | let subfile_path = subdir_path.join("subfile.txt"); 611 | let subcontent = "This is a test file in a subdirectory"; 612 | let mut subfile = File::create(&subfile_path)?; 613 | subfile.write_all(subcontent.as_bytes())?; 614 | 615 | // Expected size is the sum of both file contents 616 | let expected_size = (content.len() + subcontent.len()) as u64; 617 | let actual_size = get_dir_size(temp_path)?; 618 | 619 | assert_eq!(actual_size, expected_size); 620 | Ok(()) 621 | } 622 | 623 | #[test] 624 | fn test_scan_modules_dir() -> io::Result<()> { 625 | // Create a mock node_modules directory structure 626 | let temp_dir = tempdir()?; 627 | let mock_node_modules = temp_dir.path(); 628 | 629 | // Create a few mock modules with different sizes 630 | let modules = vec![ 631 | ("small-module", 100), 632 | ("medium-module", 500), 633 | ("large-module", 1000) 634 | ]; 635 | 636 | for (name, size) in &modules { 637 | let module_path = mock_node_modules.join(name); 638 | fs::create_dir(&module_path)?; 639 | let file_path = module_path.join("index.js"); 640 | let content = "a".repeat(*size); 641 | let mut file = File::create(file_path)?; 642 | file.write_all(content.as_bytes())?; 643 | } 644 | 645 | // Scan the mock node_modules directory 646 | let result = scan_modules_dir(mock_node_modules)?; 647 | 648 | // Check that we have all expected modules 649 | assert_eq!(result.len(), modules.len()); 650 | 651 | // Check that they're sorted by size (largest first) 652 | assert_eq!(result[0].name, "large-module"); 653 | assert_eq!(result[1].name, "medium-module"); 654 | assert_eq!(result[2].name, "small-module"); 655 | 656 | // Check actual sizes 657 | assert_eq!(result[0].size, 1000); 658 | assert_eq!(result[1].size, 500); 659 | assert_eq!(result[2].size, 100); 660 | 661 | Ok(()) 662 | } 663 | 664 | #[test] 665 | fn test_module_info_with_package_json() -> io::Result<()> { 666 | // Create a temporary directory 667 | let temp_dir = tempdir()?; 668 | let mock_node_modules = temp_dir.path(); 669 | 670 | // Create a module with package.json 671 | let module_name = "test-module"; 672 | let module_path = mock_node_modules.join(module_name); 673 | fs::create_dir(&module_path)?; 674 | 675 | // Create some files to count 676 | fs::create_dir_all(&module_path.join("src"))?; 677 | let js_file_path = module_path.join("src/index.js"); 678 | let js_content = "console.log('Hello, World!');"; 679 | let mut js_file = File::create(js_file_path)?; 680 | js_file.write_all(js_content.as_bytes())?; 681 | 682 | let ts_file_path = module_path.join("src/types.ts"); 683 | let ts_content = "export type Test = { name: string; };"; 684 | let mut ts_file = File::create(ts_file_path)?; 685 | ts_file.write_all(ts_content.as_bytes())?; 686 | 687 | // Create a package.json with test data 688 | let package_json_path = module_path.join("package.json"); 689 | let package_json_content = r#"{ 690 | "name": "test-module", 691 | "version": "1.0.0", 692 | "description": "A test module", 693 | "author": "Test Author", 694 | "license": "MIT", 695 | "homepage": "https://example.com", 696 | "repository": { 697 | "type": "git", 698 | "url": "https://github.com/test/test-module" 699 | }, 700 | "dependencies": { 701 | "dep1": "^1.0.0", 702 | "dep2": "^2.0.0" 703 | }, 704 | "devDependencies": { 705 | "devdep1": "^1.0.0" 706 | } 707 | }"#; 708 | let mut package_json_file = File::create(package_json_path)?; 709 | package_json_file.write_all(package_json_content.as_bytes())?; 710 | 711 | // Scan the mock node_modules directory 712 | let result = scan_modules_dir(mock_node_modules)?; 713 | 714 | // Check that we have our module 715 | assert_eq!(result.len(), 1); 716 | let module = &result[0]; 717 | 718 | // Check basic info 719 | assert_eq!(module.name, module_name); 720 | 721 | // Check package.json derived info 722 | assert_eq!(module.version, Some("1.0.0".to_string())); 723 | assert_eq!(module.description, Some("A test module".to_string())); 724 | assert_eq!(module.author, Some("Test Author".to_string())); 725 | assert_eq!(module.license, Some("MIT".to_string())); 726 | assert_eq!(module.homepage, Some("https://example.com".to_string())); 727 | assert_eq!(module.repository, Some("https://github.com/test/test-module".to_string())); 728 | 729 | // Check dependency count (2 deps + 1 dev dep = 3) 730 | assert_eq!(module.dependency_count, Some(3)); 731 | 732 | // Check files count (package.json + 2 source files = 3) 733 | assert_eq!(module.files_count, Some(3)); 734 | 735 | // Check file types 736 | if let Some(file_types) = &module.file_types { 737 | // Convert to HashMap for easier checking 738 | let file_types_map: HashMap<_, _> = file_types.iter().cloned().collect(); 739 | 740 | // Should have .js and .ts files 741 | assert_eq!(file_types_map.get("js"), Some(&1)); 742 | assert_eq!(file_types_map.get("ts"), Some(&1)); 743 | assert_eq!(file_types_map.get("json"), Some(&1)); 744 | } else { 745 | panic!("No file types found"); 746 | } 747 | 748 | Ok(()) 749 | } 750 | 751 | #[test] 752 | fn test_app_state_init() { 753 | let modules = vec![ 754 | ModuleInfo { 755 | name: "test1".to_string(), 756 | size: 100, 757 | dependency_count: None, 758 | last_updated: None, 759 | license: None, 760 | version: None, 761 | description: None, 762 | author: None, 763 | homepage: None, 764 | repository: None, 765 | files_count: None, 766 | file_types: None, 767 | is_dev_dependency: false, 768 | }, 769 | ModuleInfo { 770 | name: "test2".to_string(), 771 | size: 200, 772 | dependency_count: None, 773 | last_updated: None, 774 | license: None, 775 | version: None, 776 | description: None, 777 | author: None, 778 | homepage: None, 779 | repository: None, 780 | files_count: None, 781 | file_types: None, 782 | is_dev_dependency: false, 783 | } 784 | ]; 785 | 786 | let app_state = AppState { 787 | modules: modules.clone(), 788 | scroll_offset: 0, 789 | selected_index: None, 790 | mode: AppMode::List, 791 | }; 792 | 793 | // Check initial state 794 | assert_eq!(app_state.modules.len(), 2); 795 | assert_eq!(app_state.scroll_offset, 0); 796 | assert_eq!(app_state.selected_index, None); 797 | 798 | // Check that we're in list mode 799 | match app_state.mode { 800 | AppMode::List => {}, 801 | _ => panic!("Expected AppMode::List"), 802 | } 803 | } 804 | } 805 | --------------------------------------------------------------------------------