Migrating from v4 to v5
jscpd v5 is a complete Rust rewrite that replaces the TypeScript engine with a native binary. This guide covers what changed, what's compatible, and how to run both versions side by side.
Why v5?
| v4 (TypeScript) | v5 (Rust) | |
|---|---|---|
| Speed | Baseline | 24-37x faster |
| Runtime | Requires Node.js | Self-contained binary |
| Install size | ~15 MB (node_modules) | ~5 MB (single binary) |
| JS/TS tokenizer | PrismJS | OXC parser |
| Git blame | Shells out to git CLI | In-process via gitoxide |
| Parallel detection | No | Yes (--workers) |
Detection Speed
| Target | Files | Size | v4 (TypeScript) | v5 (Rust) | Speedup |
|---|---|---|---|---|---|
| fixtures | 548 | 1.5 MB | 1.03s | 0.03s | 34.3x |
| Svelte | 8,963 | 38 MB | 15.80s | 0.43s | 36.9x |
| CopilotKit | 17,092 | 159 MB | 82.89s | 3.44s | 24.1x |
Benchmarked on macOS (Apple Silicon), 10 runs for fixtures/Svelte, 3 for CopilotKit.
Git Blame Speed
The --blame flag enriches clones with git author data. v4 shells out to git blame per file; v5 uses gitoxide for in-process blame.
| Target | v4 --blame | v5 --blame | Speedup |
|---|---|---|---|
| fixtures (548 files) | 3.57s | 0.13s | 27.5x |
v4's blame mode is 3.5x slower than its own non-blame mode (3.57s vs 1.03s). v5's blame adds only ~0.10s (0.13s vs 0.03s).
Detailed Timing
fixtures (548 files, 1.5 MB)
| Metric | v4 | v5 |
|---|---|---|
| Mean real time | 1.030s | 0.030s |
| Std dev | 0.042s | 0.000s |
| Mean user time | 1.174s | 0.085s |
| Mean sys time | 0.074s | 0.050s |
Svelte (8,963 files, 38 MB)
| Metric | v4 | v5 |
|---|---|---|
| Mean real time | 15.803s | 0.428s |
| Std dev | 1.010s | 0.021s |
| Mean user time | 16.075s | 0.553s |
| Mean sys time | 0.738s | 1.110s |
CopilotKit (17,092 files, 159 MB)
| Metric | v4 | v5 |
|---|---|---|
| Mean real time | 82.890s | 3.440s |
| Std dev | 4.086s | 0.699s |
| Mean user time | 100.020s | 7.323s |
| Mean sys time | 18.263s | 3.100s |
Key Observations
- Startup overhead: v5's native binary has near-zero startup cost. v4's Node.js runtime adds ~1s even for tiny fixtures.
- Scaling: CopilotKit (159 MB) takes only 3.4s with v5 vs 83s with v4.
- CPU utilization: v5's higher user time relative to real time (e.g., CopilotKit: 7.3s user vs 3.4s real) shows effective multi-threading. v4 is single-threaded (user ≈ real).
- Consistency: v5 has tighter variance across all runs.
Breaking Changes
Node.js API Removed
The v4 programmatic API is not available in v5:
// ❌ v4 only — not available in v5
import { jscpd } from 'jscpd';
const clones = await jscpd(['', '', './src', '-r', 'json']);
Alternatives:
- CLI: Use
jscpdas a subprocess and parse JSON output - Rust crate: Use
cpd-finderin Rust applications - v4 package: Install
jscpd@4for the Node.js API (see coexistence below) - Server: Use the jscpd-server REST API
--store leveldb Removed
The external store backend (LevelDB/Redis) is not supported in v5 — the Rust engine is fast enough that caching isn't needed. The --store and --store-path flags are accepted for compatibility but do nothing.
Reporter Name Change
- v4's
fullreporter → v5'sconsoleFull
Output Filenames
Some reporter output filenames changed:
| Reporter | v4 | v5 |
|---|---|---|
| HTML | html/index.html | jscpd-report.html |
| JSON | jscpd-report.json | jscpd-report.json (unchanged) |
| XML | jscpd-report.xml | jscpd-report.xml (unchanged) |
Token Counts
Token counts may differ by 1-2% from v4 due to the OXC tokenizer handling JS/TS/JSX/TSX differently than PrismJS.
CLI Flag Changes
| v4 | v5 | Notes |
|---|---|---|
--noSymlinks | (removed) | Symlinks are not followed by default; use --follow-symlinks to follow |
--gitignore | (removed) | .gitignore is respected by default; use --no-gitignore to disable |
-g | (removed) | Use --no-gitignore to invert |
-n | (removed) | Symlinks are not followed by default |
-d / --debug | (removed) | Use --verbose instead |
--verbose | -v / --verbose | Short flag -v now shows version; use --verbose for verbose output |
--store leveldb | accepted, ignored | No external store in v5 |
--store-path | accepted, ignored | No external store in v5 |
| — | -w / --workers | New: control parallel detection threads |
| — | --no-colors | New: disable ANSI color output |
--skipComments | --skip-comments | Kebab-case (camelCase still works in config) |
--skipLocal | --skip-local | Kebab-case (camelCase still works in config) |
--noTips | --no-tips | Kebab-case (camelCase still works in config) |
--ignoreCase | --ignore-case | Kebab-case (camelCase still works in config) |
--formatsExts | --formats-exts | Kebab-case (camelCase still works in config) |
--formatsNames | --formats-names | Kebab-case (camelCase still works in config) |
--exitCode | --exit-code | Kebab-case (camelCase still works in config) |
Note: CamelCase names still work in
.jscpd.jsonconfig files. The kebab-case change only affects CLI flags.
@jscpd/* Packages
The v4 ecosystem packages are replaced by Rust crates:
| v4 Package | v5 Replacement |
|---|---|
jscpd | jscpd@5 (npm, installs native binary) or cargo install jscpd |
@jscpd/core | cpd-core (Rust crate) |
@jscpd/finder | cpd-finder (Rust crate) |
@jscpd/tokenizer | cpd-tokenizer (Rust crate) |
@jscpd/html-reporter | Built into cpd-reporter (Rust crate) |
@jscpd/badge-reporter | Built into cpd-reporter (Rust crate) |
@jscpd/sarif-reporter | Built into cpd-reporter (Rust crate) |
@jscpd/leveldb-store | Removed (not needed) |
jscpd-server | Still available (Node.js based, unchanged) |
Compatible Features
These work the same in both v4 and v5:
- CLI interface: Same command name (
jscpd), same flags structure .jscpd.jsonconfig: Fully compatible (usesjscpdas the config key)- Detection modes:
strict,mild,weak— identical behavior - Cross-format detection: Vue SFC, Svelte, Astro, Markdown — same support
- Shebang detection: Same behavior
- Reporters:
console,json,xml,csv,html,markdown,sarif,ai,badge,threshold,silent— all present in both versions --ignore-pattern: Same inline ignore blocks (jscpd:ignore-start/jscpd:ignore-end)- Exit codes:
--thresholdand--exit-codework the same
Coexistence
You can run both v4 and v5 in the same project:
Install v5 globally, v4 as a project dependency
# Global: v5 (Rust binary)
npm install -g jscpd
# Project: v4 (Node.js API)
npm install --save-dev jscpd@4
Then use v5 from the CLI and v4 from Node.js scripts:
# CLI uses v5 (fast)
jscpd ./src --reporters json
# Node.js script uses v4
node scripts/detect.js
// scripts/detect.js — uses v4 Node.js API
import { jscpd } from 'jscpd';
const clones = await jscpd(['', '', './src', '-r', 'json']);
console.log(clones);
CI/CD: Pin a specific version
# GitHub Actions — v5
- name: Install jscpd
run: npm install -g jscpd@5
- name: Check duplications
run: jscpd --threshold 5 ./src
# GitHub Actions — v4 (if you need the Node.js API)
- name: Install jscpd
run: npm install -g jscpd@4
- name: Check duplications
run: jscpd --threshold 5 ./src
Configuration Compatibility
.jscpd.json files work with both v4 and v5 without changes:
{
"threshold": 0,
"reporters": ["html", "console"],
"ignore": ["**/node_modules/**"],
"absolute": true,
"minLines": 5,
"minTokens": 50
}
v5 ignores store and storePath fields if present (they're not needed with the Rust engine). Everything else maps directly.
Migration Checklist
- Install v5:
npm install -g jscpd@5orcargo install jscpd - Replace
--reporters fullwith--reporters consoleFull - Update HTML report output path from
html/index.htmltojscpd-report.html - Remove
--store leveldb/--store-pathflags (accepted but ignored) - Replace
--noSymlinkswith--follow-symlinks(inverted behavior) - Replace
--gitignorewith default behavior (now automatic) - Replace
--debugwith--verbose - Update Node.js API calls to use CLI, Rust crates, or jscpd-server
- Pin
jscpd@4as a project dependency if you need the Node.js API