跪拜 Guibai
← Back to the summary

The CLI Toolkit for Developers Who Sweat the Invisible Mess in Their Projects


theme: minimalism

🎙️ Foreword

In today's highly mature frontend engineering standardization, whether you have a code cleanliness obsession or not, you definitely know these tools that enforce explicit code standards:

We agonize over a blank line, a space, a semicolon, unused variables, or non-standard naming. We can't tolerate any yellow squiggly lines or background highlights in the IDE.

However, the truly dirty engineering problems are not in the code writing style, but hidden in invisible corners like project dependencies, version redundancy, junk files, and code volume. These problems are often ignored or overlooked by most developers.

Writing elegant code is only surface-level tidiness; a clean project structure, healthy dependencies, controllable versions, and transparent volume are the ultimate pursuits of a high-level frontend cleanliness obsession.

Many projects appear to run, but actually harbor massive technical debt:

None of these problems are easy to solve manually. This article discusses several minimalist, efficient CLI tools that the author (a severe code-cleanliness obsessive) heavily relies on to cure project messiness.

TL;DR

In the era of prevalent AI development tools, sticking to the old ways of coding is no longer realistic. People are increasingly turning to AI for every problem, like "update all project dependencies to the latest," "check if all packages in the workspace have redundant or missing dependencies," or "calculate the code volume for me." You might first think of AI, and indeed AI seems suitable for such drudgery, but...

"There is nothing new under the sun." The problems you think of have likely already been explored by others, and mature solutions exist.

Yes, there are already very mature solutions. They are not only free but also far more efficient than AI tools (when solving problems with a clear direction, traditional solutions are still much more efficient than AI). Why waste tokens? Ultimately, AI might just end up calling traditional tools to do the drudgery you wanted it to do.

This article will introduce the following CLI tools:

Tool Function Notes
taze Update dependencies Once you use it, you'll forget the old flame ncu
ncu Update dependencies A veteran tool, used for many years
knip Check for redundant or missing dependencies (and much more) depcheck recommended it before going dormant; powerful enough to have its own website
depcheck Check for redundant or missing dependencies Went dormant in 2023
syncpack Unify dependencies A pleasant surprise discovery while writing this article; also powerful enough to have its own website
tokei Code line counting Faster and smarter
cloc Code line counting A veteran tool, used for many years

🥦 Keeping Dependencies Fresh - taze / ncu

Whether dependency versions are kept fresh is an important basis for judging the health of a frontend or Node project.

Every time I take over an old project, the first thing I do is check how stale and decayed the project dependencies are, then formulate a stable upgrade plan. A long-standing habit of mine is to check and update project dependency versions first thing every day when I open my workspace.

This habit has brought me many benefits, allowing me to perceive updates from well-known frameworks, libraries, and toolchains in the community firsthand, and learn new APIs. However, it also means being the first to be affected by issues from second and third-party packages. Unstable, you say? Upgrading is a double-edged sword; we need to upgrade to enjoy the community's dividends (bug fixes, feature enhancements, performance improvements), but we must also bear the potential introduction of new bugs.

In fact, by perceiving the iteration of second and third-party package versions, I can confine problematic versions to a smaller scope and provide more precise and timely feedback to colleagues on second-party packages or authors of third-party packages, preventing problems from expanding. Long-term stability is true stability.

Maintaining inert stability and refusing to upgrade leads to outdated dependency versions. Besides not being able to enjoy the community's dividends, more seriously, it can render the project unmaintainable. I have seen many such projects where development dependencies are completely out of sync with the current Node environment, even throwing errors during the installation phase.

You can use the following two tools:

  1. ncu A veteran tool over ten years old, still actively maintained. I used it for many years.
  2. taze Pronounced /ta:zei/, Persian for "fresh". Developed by antfu. I have now abandoned my old flame ncu and switched to it.

Global Installation

Not required, but recommended.

Install globally to get the global commands taze and ncu:

npm install -g taze
npm install -g npm-check-updates     # Note: not ncu

A Quick Trial

Let's do a simple comparison.

Below, we want to check and update all dependencies across the entire Workspace (including the root directory) into packages.json. The commands used are taze -r -w --force and ncu -w -u respectively:

ncu is slightly slower, but the difference is not obvious. However, if you use pnpm -r exec, the difference is very noticeable (taze 28s, ncu about 4min):

Postscript: This is the biggest reason I switched from ncu to taze. Previously, I was using pnpm -r exec ncu to update the entire Workspace and didn't know ncu had a -w parameter. This highlights the importance of writing articles; to be more rigorous, I carefully reviewed ncu's parameters and found I had misunderstood it all along.

Common Parameters

Both taze and ncu offer a very rich set of command-line parameters. Below is a comparison of commonly used ones:

Function taze ncu Notes
Recursive Scan -r / --recursive w / --workspaces --deep taze -r and ncu --deep are equivalent, scanning even directories not part of the Workspace
Write Mode -w / --write ‑u / --upgrade Updates all package version numbers in package.json to the latest within the specified range
Update Global ‑g / --global ‑g / --global ncu explicitly states it won't update globally but will give the update command; taze can update with one click using taze -g --install
Ignore Cache --force / -f --cacheClear taze caches by default, needs --force to bypass and force fetch new data; ncu needs --cache to actively enable caching. Unlike taze --force which bypasses without clearing, ncu --cacheClear clears the cache
Interactive Mode ‑I / --interactive ‑i / --interactive Interactive mode, letting you decide which upgraded packages' latest versions to write to package.json, i.e., default write mode; note taze uses uppercase I
Ignore Packages --exclude pkg1,pkg2 ‑x / --reject pkg1,pkg2 Exclude packages from upgrade; taze supports pkg@versionRange for fine-grained exclusion of specific version ranges, e.g., taze --exclude typescript@7 only blocks v7, allowing v6 minor upgrades
Specify Packages --include pkg1,pkg2 ‑f / --filter pkg1,pkg2 Only update specific packages; both support regex
Ignore Paths --ignore-paths <paths> None
Group Display --group --format group More intuitive and aesthetically pleasing; default for taze, requires parameter for ncu
Timeout Setting --request-timeout 120000 --timeout 120000 Set request timeout (ms), default is 30 seconds
Update Peer --peer --dep peer ncu allows finer control over which dependency types to update via --dep; taze only has the --peer switch
Execute Install --install --install <always|never|prompt> Whether to execute install after updating versions; ncu offers finer control
Concurrency Control --concurrency N --concurrency N
Log Control --loglevel <level> -l, --loglevel <level> Set log output level to control the amount of terminal output information
Output JSON --json --jsonUpgraded Output upgrade content in JSON format (machine-readable); requires some waiting time after command execution

Some other differences:

Configuring taze

Step 1, install as a dev dependency:

pnpm -w add -D taze

Step 2, add a command to scripts in package.json:

{
  ...
  "scripts": {
    "taze": "taze -r"
  }
}

Step 3, add a configuration file taze.config.ts (or .tazerc, .tazerc.json, taze.config.{js,mjs,cjs}) in the root directory:

import {
  defineConfig
} from 'taze';
  
// https://github.com/antfu-collective/taze?tab=readme-ov-file#config-file
export default defineConfig({
  mode: 'major', // Update version range
  write: true, // Write to package.json
  exclude: [ // Packages to ignore; recommend adding comments
    'typescript', // Don't upgrade to 7 yet, @typescript-eslint doesn't support it, causing Eslint errors
    'eslint', // TODO up 10
    '@babel/*' // Don't upgrade to 8 yet, indirect dependency @babel/runtime causes build failures
  ]
});

Configuring ncu

Step 1, install as a dev dependency:

pnpm -w add -D npm-check-updates

Step 2, add a command to scripts in package.json:

{
  ...
  "scripts": {
    "ncu": "ncu -w"
  }
}

Step 3, add a configuration file .ncurc.yaml (or .ncurc.yml, .ncurc, .ncurc.json, .ncurc.{js,mjs,cjs}) in the root directory:

# Write to package.json
upgrade: true
# Update dependencies and devDependencies (ignore peerDependencies, etc.)
dep: dev,prod
# Group display (more aesthetically pleasing)
format: group
# Timeout setting
timeout: 120000
# Don't prompt for install to save time
install: never
# Packages to temporarily not upgrade (can add comments explaining why)
reject:
  - 'typescript', # Don't upgrade to 7 yet, @typescript-eslint doesn't support it, causing Eslint errors
  - 'eslint' # TODO up 10
  - '@babel/*' # Don't upgrade to 8 yet, indirect dependency @babel/runtime causes build failures

Summary

Both taze and ncu are very powerful and easy to use for updating dependencies. I have personally fully switched to taze, but I still keep both installed globally.

After each upgrade, carefully review the output, paying special attention to major version updates. If a major version is incompatible, it's recommended to first add the corresponding package to the ignore list.

🫟 Keeping Dependencies Clean - knip / depcheck

Another important but easily overlooked issue with project dependencies is dependency chaos:

The problem of version number chaos can be solved with taze and ncu. The remaining issues of "too many or too few" can be addressed with the following two tools:

  1. knip Not only checks dependencies but also finds unreferenced files, exports, etc. (Among the small tools recommended in this article, it's the only one with its own website, which shows how powerful it is.)
  2. depcheck Checks for redundant and missing dependencies. Went dormant in 2025/07 and recommended knip.

Global Installation

Not required, but recommended.

Install globally to get the global commands knip and depcheck:

npm install -g knip
npm install -g depcheck

A Quick Trial

Use both tools for a similar task: checking the entire Workspace (including the root directory).

You can see knip wins outright—simple, efficient, and with more features—while depcheck needs to leverage pnpm's capabilities (and requires the --no-bail parameter to prevent premature exit).

Common Parameters

depcheck's functionality is relatively simple with fewer parameters. Below is a comparison of commonly used ones:

Function knip depcheck Notes
Specify Directory Pass path directly knip ./packages/a depcheck ./packages/a Both support directly passing the project directory path as the first positional argument
Recursive Scan Automatically recognizes Workspaces ❌ Not supported, requires Shell loop or pnpm -r
Output JSON --reporter json --json knip supports multiple output formats; depcheck only has simple JSON
Ignore Packages Configure ignoreDependencies (no direct CLI flag, config file recommended) --ignores="pkg1,@scope/*"
Ignore Paths Configure ignore --ignore-patterns=dist,covera
Ignore Bin Packages Configure ignoreBinaries --ignore-bin-package knip automatically ignores common ones like cross-env, rimraf, while depcheck can only configure in ignores (this config item seems ineffective) Can only configure in
Read .xxignore No CLI parameter, set in config --ignore-path=.gitignore
Skip Dependencies --include dependencies to see only unused deps --skip-missing=trueGitHub
Only Check Certain Issues --include dependencies/files/exports None
Write Mode --fix ❌ Not supported Unique to knip, removes unused dependencies from package.json; not recommended for use

Configuring knip

knip provides a command to initialize in a project: pnpm create @knip/config, but the generated config file is knip.json. Since the configuration is relatively simple, you can easily do it manually.

Step 1, install as a dev dependency:

pnpm -w add -D knip

Step 2, add a command to scripts in package.json:

{
  ...
  "scripts": {
    "knip": "knip"
  }
}

It's necessary to explain the --strict parameter, which is a command-line-only parameter with no place in the configuration file.

Sometimes using --strict can align with pnpm's strict phantom dependency detection, but the implicit meaning of --strict is to only scan production code. What does that mean? It means config files, unit tests, Storybook, etc., will not be checked.

Because of this, it's not recommended to add --strict to your NPM scripts. Instead, you can occasionally run it with this parameter to check for phantom dependencies.

Step 3, add a configuration file knip.config.ts (or .knip.{json,jsonc}, knip.{json,jsonc}, knip.{ts,js}, knip.config.{ts,js}, or add a knip field in package.json) in the root directory:

import {
  KnipConfig
} from 'knip';

export default {
  workspaces: {
    '.': {
      entry: [ // This is a Taro mini-program, so it has multiple entries
        'src/app.tsx',
        'src/app.config.ts',
        'src/{pages,subpackages}/**/index.{ts,tsx,config.ts}'
      ],
      project: [
        'src/**/*.{ts,tsx,less,css}'
      ],
      paths: {
        '@/*': ['./src/*']
      }
    }
  },
  tags: ['-lintignore'],
  ignoreDependencies: [/^@tarojs\//]
} satisfies KnipConfig;

For those wishing to use knip in VSCode or WebStorm, the official team also provides plugins, and even an MCP Server for AI, etc. See Knip - Integrations for details.

Configuring depcheck

Step 1, install as a dev dependency:

pnpm -w add -D depcheck

Step 2, add a command to scripts in package.json:

{
  ...
  "scripts": {
    "depcheck": "pnpm -r --include-workspace-root --no-bail exec depcheck"
  }
}

Step 3, add a configuration file .depcheck.yaml (or .depcheck.yml, .depcheck.json) in the root directory:

ignorePatterns:
  - dist
  - __*
ignores:
  - '@/*'
  - '@babel/*'
  - '@commitlint/*'
  - '@kcuf/*config'
  - '@tarojs/*'
  - '@vitest/coverage-v8'
  - eslint-import-resolver-custom-alias
  - cross-env
  - lint-staged
  - rimraf
  - markdownlint-cli2
  - npm-package-json-lint
  - typescript
  - autoprefixer
  - depcheck
  - husky
  - lerna
  - less
  - postcss
  - react-refresh
  - stylelint

You can see that to avoid interference, many ignore items need to be written. depcheck only scans import/require in TS/JS code by default and cannot recognize implicit dependencies in package.json#scripts and other config files, thus generating many false positives. Although it also provides --ignore-bin-package and --specials parameters, the latter seems ineffective, meaning you still need to write a long list of ignores.

Summary

Whether in terms of powerful functionality or configuration simplicity, knip surpasses depcheck by several levels.

Although I recommend enabling the configuration items that update package.json (write: true for taze, update: true for ncu), be sure to carefully review which packages have been upgraded before committing changes to Git, and pay close attention to major versions. If you find issues running locally, roll back the version changes promptly.

If you find a problem with a minor version upgrade of a package, you can set an override or package.json#resolutions (pnpm <11) to temporarily bypass it.

🪢 Version Chaos - syncpack

While writing this article, I learned about syncpack from knip's documentation, a CLI tool that can sort out and resolve dependency version chaos. It's also a tool worthy of its own website.

syncpack's functionality overlaps somewhat with knip; it can also check and upgrade updates syncpack update, but its core capability is checking whether dependencies are consistent across large Monorepo projects, which is exactly where its name comes from.

Global Installation

Not required, but recommended.

Install globally to get the global command syncpack:

npm install -g syncpack

A Quick Trial

Unlike the other small tools introduced above, syncpack primarily operates through subcommands:

Command Function Notes
list List all dependencies and their occurrence count
lint List version inconsistency issues
fix Fix issues found by lint
format Modify the format of package.json It modifies field order, not version numbers. I usually leave field order management to npmpackagejsonlint
update Update dependency version numbers Leave this to taze

Below are the effects of syncpack list and syncpack lint:

Configuring syncpack

Since I just got acquainted with syncpack, and it has some functional overlap with taze, I haven't added it to my NPM scripts yet. But you can still try installing it as a project dependency.

Step 1, install as a dev dependency:

pnpm -w add -D syncpack

Step 2, add a command to scripts in package.json:

{
  ...
  "scripts": {
    "syncpack": "syncpack lint"
  }
}

Step 3, add a configuration file syncpack.config.ts (or .syncpackrc, .syncpackrc.config.{json,yaml,yml,js,ts,mjs,cjs}, syncpack.config.{js,mjs,cjs}, or add a syncpack or config.syncpack field in package.json) in the root directory:

export default {
  indent: "    ",
} satisfies import("syncpack").RcFile;

Summary

syncpack can help you check how many total dependencies your project has, how many times each dependency is declared, and which dependency declarations might be inconsistent or problematic, thus making your project dependencies cleaner. It also has the ability to update dependency versions (similar to taze).

🧮 Project Volume - tokei / cloc

I quite like counting code:

Available tools:

I used cloc for a long time, but cloc isn't smart enough. Firstly, it must have a path parameter; entering cloc alone yields no result. And cloc . will include the huge node_modules, which is time-consuming and not what we want. I needed a smarter, faster tool, which is tokei. You just need to execute tokei in the project directory to get the answer, with no waiting and no need for complex parameters.

Besides being written in Rust, tokei is fast because it actively ignores node_modules and the contents of Ignore files like .gitignore, so build logic, etc., are also ignored.

Another important reason, actually the primary reason, I switched to tokei is that cloc distinguishes between JS and JSX but not between TS and TSX, whereas tokei does.

Installation

cloc is written in Perl, while tokei is in Rust. Both can be installed via brew:

brew install tokei
brew install cloc

A Quick Trial

Let's check all source code parts of a project, including engineering configs and documentation, but excluding node_modules and build output directories:

tokei
cloc . --exclude-dir=node_modules,dist

It can be seen that tokei slightly outperforms the veteran cloc in terms of performance, ease of use, and aesthetics. There are slight differences in the data between the two. Also, it's best to exclude pnpm-lock.yaml, as this file can have tens of thousands of lines.

Common Parameters

Full disclosure: the table below was compiled by AI (but has been reviewed).

Function Description tokei cloc Notes
Directory / File tokei src cloc src tokei defaults to the current directory, can be omitted; but cloc must have a path
Exclude -e, --exclude <glob> --exclude-dir=dir1,dir2
--exclude-ext=js,min.js
tokei supports glob; cloc distinguishes directories and extensions, does not support glob
Detail -f, --files --by-file Output statistics for each file
Specify Language -t, --type TypeScript,TSX --include-lang=TypeScript tokei language names are case-sensitive
Exclude Specified Language No native parameter --exclude-lang=Markdown tokei can filter by extension using --exclude
Output Format -o,--output json/yaml/cbor --json / --yaml / --xml / --csv / --sql cloc has more output formats, including SQL, XML
Output File Redirect > out.json --report-file=out.txt cloc natively supports an output file parameter
Sort Output -s,--sort code/files/lines/comments/blanks --sort=code tokei can sort by blank lines, comments; cloc only supports files, code, comment
Read .gitignore Reads by default, can use --no-ignore to turn off default behavior Does not manage tokei respects .gitignore etc., won't touch files and directories not considered source code, hence fast; cloc needs manual exclusion of node_modules, dist, etc.
Count Hidden --hidden --include-hidden Both skip hidden files by default
Non-recursive None, always recursive --no-recurse tokei cannot disable recursive subdirectory scanning, can only -e exclude subdirectories
View Supported Language List -l, --languages --show-ext Print all recognized language and extension mappings
Diff Comparison Does not support diff --diff dirA dirB cloc's killer feature, compares line count changes between two codebases, supports git commit
Process Archives Not supported Natively supports zip/tar.gz etc. archives cloc can directly count code inside archives; tokei needs them extracted first

Summary

Neither of these two tools needs to be installed or configured within the project (as they are not npm packages). Although there are npm packages with the same names, don't confuse them. Although I am a long-time user of cloc, tokei is smarter, simpler, and faster, making it hard not to "switch affections."

🏓 Summary of Tips

taze

taze -r           # Recursive scan, including root and all Workspace packages
taze --force      # Bypass local npm meta cache (not force upgrade version)
taze --maturity‑period‑exclude         # Ignore package publish cooldown period, directly check latest version

ncu

ncu --registry https://registry.npmmirror.com     # Specify Registry to speed up
ncu -w                  # Scan entire Workspace, including root and all Workspace Packages
ncu -u                  # Scan current directory and update
ncu -wu                 # Scan entire Workspace and update
ncu -ui                 # Interactive update
ncu -u --prod           # Only upgrade production dependencies (dependencies)
ncu -u --dev            # Only upgrade development dependencies (devDependencies)
ncu -u vite,vitest      # Only update specified packages

knip

knip --include unlisted         # Phantom dependencies
knip --include dependencies     # Unused dependencies
knip --include files            # Unused files
knip --include exports          # Unused exports

tokei

tokei -f -s lines    # Project file size details, sorted by lines of code to find massive files
tokei -t TypeScript,TSX -f -s lines        # More precise, find massive TS or TSX files

cloc

cloc src packages-*/*/src    # Count code in all src directories
cloc src --by-file           # Project file size details, automatically sorted by lines of code

🙋 FAQ

❓ Why didn't taze find a package I just published?

It might be cached, use taze --force.

❓ How to ignore a specific Workspace when using depcheck with pnpm -r?

Use pnpm --filter for reverse filtering:

pnpm -r --no-bail --filter '!documentation' exec depcheck

❓ How to upgrade to pre-release versions like alpha, beta, rc?

Use taze newest.

ncu provides a --target parameter, with values latest, newest, greatest, minor, patch, semver, @[tag], and also a --pre parameter specifically for upgrading to pre-release versions.

📌 Links

🪭 Final Words

Don't let project dependencies become a black box. As a qualified developer, 0999

If ESLint and Prettier are code-level cleanliness obsessions, then CLI tools like taze, ncu, knip, depcheck, tokei are the engineering-level cleanliness cures.

They don't format your code or constrain syntax styles, but they can help you clean up project redundancy, update dependency versions, and count code structure, transforming the entire project from "just runnable" to "clean, transparent, healthy, and maintainable."