Skip to main content
Jakub Mazurek
Software Engineer
View all authors

Jakub MazurekOne min read

Rev-dep 3.3.0 is here!

This version brings baseline snapshots for config checks.

Adding checks to a large codebase can uncover more issues than you can fix right away. You can now acknowledge existing findings and keep CI passing while you work through them gradually.

Add baselineSnapshotPath to your config, and record the current findings:

rev-dep config run --update-snapshot

Commit the snapshot alongside your config. CI keeps running rev-dep config run as usual. New findings fail the run, and fixed issues become obsolete findings that also require a snapshot update.

Findings are saved in formatting-agnostic way so moving an import (eg. of missing dependency) to another line doesn't trigger new violations to the baseline.

Duplicated code keeps its existing snapshot mechanism; the same update flag handles both.

Read more in the findings baseline docs.


That's it for this update, see you in the next one πŸ‘‹

Jakub Mazurek5 min read

Rev-dep 3.2.1 release

This patch version bump brings:

  • a fix for the npm wrapper truncating output and exiting with code 0 on failure
  • validation of config run flag values
  • --lint-config support in the json and issues-list output formats
  • the same --fix exit code in every output format
  • better detection of test files in config init
  • a fix for the --follow-monorepo-packages syntax in the docs and hints

GitHub Release

The first three fixes can make a CI run that passed before fail after the upgrade. In each case the run should have been failing all along - the problem was hidden. Please read these sections before upgrading.

npm wrapper output and exit code​

When you run rev-dep installed from npm, a small Node.js script starts the native binary for your platform. Previously, that script collected the whole output in memory before printing it, which caused three problems:

  • output larger than 1 MiB (Node.js implicit default) was cut off, and the process exited with code 0 even when checks failed
  • when the output was piped, for example to tee, jq or a CI log processor, a failing run was cut off after 64 KB
  • arguments containing ", $ or backticks were interpreted by a shell instead of being passed to the binary

On a large repository, this meant config run could fail and CI would still pass, and --format json could produce a truncated, invalid document.

Now the binary writes directly to the terminal, pipe or file, so the output is never cut off and the exit code is always the binary's own. Arguments are passed to the binary exactly as given.

If your CI started failing after the upgrade, check the output - those failures were there before, the wrapper just hid them.

Validation of config run flags​

Some config run flag values were silently ignored. Now they fail with an error before any check runs:

  • --format accepts only json and issues-list. Previously, any other value, including a different case like JSON, fell back to the default output.
  • --workspaces fails when any listed path doesn't match a workspace in the config. Previously, a typo was only reported when none of the paths matched, so --workspaces packages/app,packages/tpyo silently ran only packages/app. Paths must be written exactly as in the config file.
  • --lint-config-rules is validated before the run. Previously, an unknown rule name was reported only after all checks had finished.

Config lint in machine-readable output​

Previously, --lint-config and --lint-config-rules were silently skipped when used with --format json or --format issues-list. A CI step combining them passed even when the config lint found errors.

Now the config is linted in every output format, and lint errors fail the run. With --format issues-list, the same summary line as in the default output is printed after the issues. With --format json, the counts are reported in a new top-level lint field:

{
"version": "2.2",
"hasFailures": false,
"workspaces": [...],
"fixSummary": {...},
"lint": { "errors": 1, "warnings": 0 }
}

The field is present only when the config was linted. Lint findings don't set hasFailures, which still reports check failures only. The JSON output version is now 2.2 - validate against the 2.2 schema. Read more about linting during a config run.

The same --fix exit code in every format​

With --fix, the default output exits with code 0 when every issue found was fixable and has been fixed. The json and issues-list formats exited with code 1 in that case, unless --recheck was also used.

Now all formats follow the same rule: 0 when everything found was fixed, 1 when an issue remains that --fix can't fix. In JSON output, hasFailures still reports that the checks found issues before they were fixed. Read more about exit codes with --fix and --recheck.

Test files detection in config init​

config init sorts the detected entry points into prodEntryPoints and devEntryPoints. Some common test file names were not recognized as development files, so they ended up in prodEntryPoints:

  • *.e2e-spec.ts, used by NestJS
  • *.e2e-test.ts, *.integration-test.ts and *.integration-spec.ts
  • Jest config and setup files like jest-e2e.ts or jest.setup.ts

A generated config then treated test code as production code - in one large project, 100 test files ended up as production entry points. Now these files are classified as development entry points. This affects only newly generated configs.

--follow-monorepo-packages syntax​

To follow only selected packages, the list has to be passed with =:

rev-dep resolve --file packages/app/src/main.tsx --follow-monorepo-packages=@acme/shared,@acme/ui

Written with a space, the list is not read as the flag's value, and every workspace package is followed. The docs and the hint printed after a restricted-imports failure showed the form with a space. Both are fixed, and the flag's help text now shows the = form. Read more about following monorepo packages.


That's it for this update. See you in the next one πŸ‘‹

Jakub Mazurek3 min read

Rev-dep 3.2.0 release

This minor version bump brings:

  • a fix for .gitignore lookup in git worktrees and submodules
  • a new failOnEmptyWorkspace config option
  • improvements to telemetry reporting

GitHub Release

This release exists thanks to a community contribution. Big thanks for reporting issue #46 - it came with a minimal reproduction, the root cause, and a suggestion for how to prevent this kind of problem in the future. Both the fix and the suggestion are part of this release.

Gitignore lookup in worktrees and submodules​

Rev-dep reads .gitignore files from parent folders, up to the repository root. Previously, it recognized the root only by a .git directory. Linked git worktrees and submodules use a .git file instead, so the lookup continued into the enclosing checkout and applied its ignore rules.

When a worktree was nested in a folder ignored by the main checkout - for example .agent/worktrees/, where an AI agent creates its worktrees - every source file was excluded. config run reported zero files and still passed.

Now the lookup stops at a .git file the same way it stops at a .git directory, which is how git itself treats repository boundaries.

Fail on empty workspace​

The issue also pointed out a bigger problem: a check that runs on zero files proves nothing, but it still passes. That is easy to miss in CI or in AI agent runs, where nobody reads the warnings.

The new failOnEmptyWorkspace option makes such a workspace fail the run:

{
"configVersion": "2.1",
"failOnEmptyWorkspace": true,
"workspaces": [
{ "path": "packages/app" },
{ "path": "packages/placeholder", "failOnEmptyWorkspace": false }
]
}

The option is set at the config root and can be overridden per workspace. It is opt-in, so existing configs behave the same unless you enable it.

New configs generated by rev-dep config init enable it by default.

Without the option, an empty workspace is still reported as a warning - and it is now also listed in the summary at the end of config run, so it doesn't get lost in the output.

The option requires config version 2.1. Read more in the config file structure docs.

Telemetry improvements​

The anonymized repository identifier is now resolved correctly in worktrees and submodules, and it falls back to the repository field from package.json when no git remote is available. Events also report where the identifier came from. Read more about telemetry.


That's it for this update. See you in the next one πŸ‘‹

Jakub Mazurek2 min read

Rev-dep 3.1.0 release

This minor version bump brings:

  • parsing template string literals inside require and import calls
  • ability to detect non-literal import targets
  • improved support for built-in modules detection
  • improvements to telemetry reporting
  • various documentation improvements

GitHub Release

Template literal import targets​

Previously, imports with template string literals, for example import(`someModule.ts`), were not parsed. Right now they are, as long as the target value can be statically analysed.

Template literals that cannot be statically analysed, for example import(`${dir}/someModule.ts`), will be internally represented as NonLiteralModule. Such imports cannot be followed, but they can be detected as a violation by the unresolvedImports detector.

{
"unresolvedImportsDetection": {
"enabled": true,
"reportNonLiteralImports": true
}
}

The detector will throw when there is any non-literal import in the analysed workspace.

Improved built-in modules support​

Previously, Rev-dep recognized built-in modules like fs or bun:sqlite, but it was not possible to make assertions against them.

Now you can refer to them by name in the following detectors:

You can also refer to any built-in module with the builtin:* keyword - useful for restricting imports in front-end code.

Read more about built-in modules support in docs

Telemetry improvements​

Added event reporting to exploratory toolkit commands. Added reporting of an anonymized repository identifier to all events.

Docs improvements​

Added Newsletter and Blog pages. Added llms.txt to improve discoverability by AI agents. Improved the integration guides and the comparison with other tools page.


That's it for this update. See you in the next one πŸ‘‹

Jakub Mazurek6 min read

Rev-dep 3.0 is here!

This version brings performance improvements, duplicate code detection, and glob matcher behavior parity with gitignore.

Make sure to check Short Story of Rev-dep at the end of this post!

Useful links:

Performance Improvements​

On the codebase I use for benchmarking, which has around 600k LoC, Rev-dep config run was executing for ~300ms prior to the performance improvements; now it executes for ~150ms.

It got 50% faster and now there is likely no more room for improvements.

The gap was mostly caused by inefficient data-structure locks, which effectively made the resolution phase single-threaded. There were also some improvements in the parser.

This was also delivered to version 2.x in release 2.9.1.

Duplicate Code Detection​

Duplicate code detection is a new feature that I was excited to build. I've tried to do it a few years ago in my other tool CodeQue, but due to JavaScript threading limitations, it was not feasible to implement it in a performant way.

I took lessons from that approach and implemented it within Rev-dep, utilizing the full ownership of the parser and Go's concurrency model. The result is that you can add detection on top of existing Rev-dep checks without sacrificing performance.

What's different in Rev-dep's duplicate code identification approach is that it focuses on code chunks that can be refactored right away. It captures duplicated code blocks and JSX expressions, while other tools focus on detecting chunks of duplicated tokens. But those tokens can start in the middle of some function and end in the middle of another, which makes the findings harder to work with.

Read more about duplicate code detection in duplicated code detection docs.

Glob Matchers Behavior Parity With Gitignore​

Anyone who has built a static analysis tool knows how tricky it is to implement glob matchers that behave the same way as gitignore. The 2.x approach had some gaps in the implementation, and they couldn't be fixed without breaking changes.

I believe choosing to be compliant with gitignore is a good choice because you have to support it anyway. Otherwise, projects might need to adjust their gitignore files, and it has become an industry standard due to Git's adoption.

All glob matchers in Rev-dep now follow gitignore behavior. Matching is still very fast thanks to the gobwas/glob library, which can parse glob expressions into data structures that enable very efficient matching.

Short Story of Rev-dep​

This release is a great opportunity to look back at the origin of Rev-dep.

It started as a CLI with just the rev-dep resolve command, serving the purpose of finding where a given React component was used in a server-side React application.

Back then I was working on a project where we migrated a CSS solution in a multi-page React product, and we needed a way to shortlist all pages affected by a given batch of changes.

The initial release happened in November 2020.

The 0.x version was working, but was not robust enough to work on very large codebases with complex dependency graphs.

It was based on dependency-cruiser, which delivered the dependency graph. Later it also used dpdm, which added support for TS codebases (yes, at that time people still worked on JS-only codebases πŸ˜€).

Now Rev-dep aims to replace these tools.

1.x Version (2022-2025)​

Version 1.0 included a performance improvement to the poorly designed internal graph structure that was actually a tree with a lot of duplicate nodes instead of a graph that connects unique nodes. Before the fix, the tool was allocating a lot of memory and was not able to finish a task on more complex codebases.

This release also brought more commands, such as getting project entry points and listing files reachable from a given entry point.

As part of Rev-dep, I've also written a Babel-based codemod, purpose-built to refactor imports from barrel files (export * from './x') into direct imports. That was built to solve dev server performance issues in another project I was working on.

At that time, Rev-dep still had a small scope and the CLI tool was not fast enough to use conveniently.

2.x Version - Revamp in Go (2025-2026)​

In 2025, Microsoft announced that they were porting the TypeScript compiler to Go and had achieved around a 10x performance improvement.

At that time, I was learning Go and built a side project with it. The TypeScript story inspired me to try to port Rev-dep to Go and see if I could achieve similar performance improvements.

Initially, I thought that I would be able to use their brand new TypeScript parser written in Go, but it turned out they hadn't exposed a public API for it yet. In fact, even now with the TS 7.0 release, there is no public parser API. Instead of forking their WIP parser, I looked for some alternatives and reminded myself about ESbuild, which is a fast JS/TS bundler written in Go. I found a way to get the dependency graph from esbuild compilation, and I implemented a prototype of Rev-depΒ resolve based on it. It was about 10 times faster than the previous JS version, and I was impressed.

Building on top of ESbuild was not feasible for all features, as it only exposes runtime dependencies - all type-only imports have been dropped. Ultimately, it is a bundler, and I used bundling metadata for my PoC. But I already knew that reimplementing Rev-dep in Go would make it much faster and pleasant to use day-to-day. With the help of Gen AI, I was able to implement a parser from scratch. It was the best possible parser for the job - it was purely focused on extracting imports from source code files and did not even build the AST. After the resolver implementation and parallelization of the processing, the end result was a CLI tool that was 40 times faster than the previous JS version.

Fast forward to today, Rev-dep 3.0 is now a fully featured tool that can be used to analyze and enforce dependency hygiene in large codebases. It has a rich set of checks, including circular dependencies, orphan files, unused exports, and more. The tool is also highly configurable, allowing users to define their own rules and checks. Even with Gen AI, it took a lot of time and effort to get there, but I'm really happy with the result.


That's it for this update, see you in the next one πŸ‘‹