Skip to content

Implement jscpd server#746

Merged
kucherenko merged 5 commits into
masterfrom
feature/server
Jan 11, 2026
Merged

Implement jscpd server#746
kucherenko merged 5 commits into
masterfrom
feature/server

Conversation

@kucherenko

@kucherenko kucherenko commented Jan 8, 2026

Copy link
Copy Markdown
Owner
  • What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)

Feature related to #740 and #743

  • What is the current behavior? (You can also link to an open issue here)

Currently, jscpd operates primarily as a CLI tool. There is no built-in HTTP server or RESTful API for integrating code duplication detection into other services or workflows that require a persistent service or real-time checks.

  • What is the new behavior (if this is a feature change)?

This PR introduces a new package @jscpd/server (located in apps/jscpd-server) which implements a RESTful API server for code duplication detection.

Key Features:

  • RESTful API:
    • POST /api/check: Check a code snippet for duplications against the codebase.
    • GET /api/stats: Retrieve project statistics (duplicated lines, files, formats, etc.).
    • GET /api/health: Health check endpoint.
    • GET /api/info: Server version and API information.
  • Flexible Storage: Supports both ephemeral (in-memory) and persistent (LevelDB) storage for duplication data.
  • Configurable: Fully configurable via command-line arguments (port, host, max size, etc.).
  • Integration Ready: designed for easy integration with CI/CD pipelines and external tools.
  • Other information:
  • Detailed documentation for the new server application is available in apps/jscpd-server/README.md.
  • Includes minor updates to packages/finder dependencies.

This change is Reviewable

Summary by CodeRabbit

  • New Features

    • Introduced JSCPD Server REST API with CLI entry, server lifecycle, and endpoints: /api/check, /api/stats, /api/health, and root info.
    • Per-request isolation, concurrent handling, and persistent storage support.
  • Documentation

    • Comprehensive server README with usage, API examples, schemas, troubleshooting, and best practices.
  • Tests

    • Full test suite covering initialization, all endpoints, validation, error cases, concurrency, and edge conditions.
  • Chores

    • Packaging, build and config updates for the new server project.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Jan 8, 2026

Copy link
Copy Markdown

Walkthrough

Adds a new Express-based JSCPD Server app: CLI entry, HTTP API (POST /api/check, GET /api/stats, GET /api/health, root), per-request ephemeral isolation, detector orchestration, store selection, request validation/middleware, types, tests, and docs.

Changes

Cohort / File(s) Summary
Server Core
apps/jscpd-server/src/server/server.ts, apps/jscpd-server/src/server/service.ts, apps/jscpd-server/src/server/constants.ts
New JscpdServer class and startServer helper; JscpdServerService manages lifecycle, initialization, per-request ephemeral stores, snippet checking, statistics, and state; central constants and error messages.
API Router & Middleware
apps/jscpd-server/src/server/routes.ts, apps/jscpd-server/src/server/middleware.ts
Express router factory wiring POST /api/check, GET /api/stats, GET /api/health; request validation, standardized error responses, and not-found handler.
Detection Pipeline
apps/jscpd-server/src/detect.ts
createBaseDetectorContext and detectClones exported, builds detector context, registers hooks/reporters/subscribers, runs clone detection and returns results.
Ephemeral Store
apps/jscpd-server/src/server/ephemeral-store.ts
EphemeralHybridStore combining source and ephemeral stores: read-fallback to source, writes to ephemeral, namespace forwarding, close behavior.
Types
apps/jscpd-server/src/server/types/* (requests.ts, responses.ts, state.ts, index.ts)
Request/response/state interfaces for API (CheckSnippetRequest, CheckSnippetResponse, ErrorResponse, StatsResponse, HealthResponse, Duplication structures, ServerState).
Setup & CLI
apps/jscpd-server/src/index.ts, apps/jscpd-server/bin/jscpd-server.ts, apps/jscpd-server/bin/jscpd-server, apps/jscpd-server/src/setup/*
CLI wiring (Commander), runServer entry, bin wrapper, CLI utilities (package.json reading, option conversion), options preparation, ignore/.gitignore handling, reporter/subscriber/hook registration, dynamic store selection.
Reporters & Subscribers
apps/jscpd-server/src/setup/reporters.ts, apps/jscpd-server/src/setup/subscribers.ts, apps/jscpd-server/src/setup/hooks.ts
Reporter registration (static map + dynamic require fallback), subscriber registration (verbose/progress), hooks registration (FragmentsHook, conditional BlamerHook).
Config & Build
apps/jscpd-server/package.json, apps/jscpd-server/tsconfig.json, apps/jscpd-server/tsconfig.build.json, apps/jscpd-server/tsup.config.ts, apps/jscpd-server/nodemon.json
New package metadata, exports/bin, TypeScript strict configs, tsup build config, nodemon watch config.
Tests & Docs
apps/jscpd-server/__tests__/server.test.ts, apps/jscpd-server/README.md
Comprehensive tests covering lifecycle, endpoints, validation, isolation, concurrency, and edge cases; detailed README documenting API, schemas, examples, and troubleshooting.
Workspace & Misc
pnpm-workspace.yaml, package.json, apps/jscpd/package.json, packages/finder/src/files.ts
Workspace glob quoting changed, onlyBuiltDependencies added, dependency bumps, added @types/node in app, guard added in skipBigFiles to handle missing stats.
Exports / Barrels
apps/jscpd-server/src/server/index.ts, apps/jscpd-server/src/setup/index.ts, apps/jscpd-server/src/setup/store.ts
Barrel re-exports and getStore runtime store selection (dynamic require fallback to MemoryStore).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Express as Express Server
    participant Middleware
    participant Router
    participant Service as JscpdServerService
    participant Detector
    participant Store as EphemeralHybridStore
    participant Response

    Client->>Express: POST /api/check { code, format }
    Express->>Middleware: validateCheckRequest(req)
    Middleware-->>Router: next()
    Router->>Service: checkSnippet(request)
    Service->>Store: create ephemeral store (seed from source)
    Service->>Detector: detectClones(opts, ephemeralStore)
    Detector->>Store: read frames (ephemeral -> source fallback)
    Store-->>Detector: frames
    Detector->>Detector: locate clones
    Detector-->>Service: clones
    Service->>Service: filter & map clones, calc stats
    Service-->>Response: CheckSnippetResponse
    Response->>Client: 200 { duplications, statistics }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Poem

🐰 I hopped a server into view,

Snippets safe in stores anew,
Routes that listen, tests that cheer,
Clones detected, stats appear,
A tiny rabbit says: fetch and review!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Implement jscpd server' accurately describes the main objective of the PR, which introduces a new server package (@jscpd/server) with RESTful API endpoints for code duplication detection.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c183ce2 and 098c456.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (1)
  • apps/jscpd/package.json
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Rooview

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ghost

ghost commented Jan 8, 2026

Copy link
Copy Markdown

Rooviewer Clock   Follow along on Roo Cloud

Reviewing your PR now. Feedback on the way!

}

close(): void {
this.ephemeralStore.close();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The close() method doesn't await the ephemeral store's close operation. If ephemeralStore.close() returns a Promise (which some store implementations might), this could lead to incomplete cleanup and potential resource leaks. The method should be async and await the close operation to ensure proper cleanup.

Fix it with Roo Code or mention @roomote and request a fix.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🤖 Fix all issues with AI agents
In @apps/jscpd-server/README.md:
- Around line 453-464: The README shows an inconsistent CLI invocation: it
documents `jscpd server /path/to/project` but the project exposes the CLI
entrypoint as `jscpd-server`; update the example and any other occurrences to
use the correct command `jscpd-server` (or vice versa if the actual binary
should be `jscpd`) so they match; search for the strings `jscpd server` and
`jscpd-server` (e.g., the "Starting the Server" section and the example under
"Start server with LevelDB persistence") and make them consistent.
- Around line 586-589: The CI example uses the wrong executable name; update the
GitHub Actions step under the "Start jscpd server" job to call the correct
binary `jscpd-server` (replace `jscpd server . --port 3000 &` with `jscpd-server
. --port 3000 &`) and scan the README for other occurrences of `jscpd server` to
make them consistent with the `jscpd-server` command used elsewhere.

In @apps/jscpd-server/src/server/constants.ts:
- Around line 1-3: The module currently calls readPackageJson() at import time
which can throw and crash startup; change constants.ts to call readPackageJson()
inside a try/catch (or create a small helper like getPackageJsonSafe or
getPackageVersion) and fall back to a safe default version string (e.g. "0.0.0"
or undefined) when the file is missing or parsing fails, then export the same
constant names (e.g. packageJson or PACKAGE_VERSION) so callers aren’t broken
but the import no longer throws.

In @apps/jscpd-server/src/server/middleware.ts:
- Around line 65-80: The errorHandler currently logs the full error object which
may include sensitive user-submitted code; change the logging to only emit safe
fields (e.g., err.name and err.message) and avoid printing err.stack or the
entire err object. In the errorHandler function, replace console.error('Error:',
err) with a focused log such as console.error('Error:', err.name, '-',
err.message) or use the app's logger to record only those fields, and keep
constructing the ErrorResponse (error, message, statusCode) unchanged so the
response behavior remains the same.

In @apps/jscpd-server/src/server/service.ts:
- Around line 206-218: The close() method currently cleans up many fields but
omits resetting the Statistic reference; update the async close() implementation
(method close in the service class) to set this.statistic = null as part of
teardown (place it alongside the other null assignments such as this.store,
this.options, this.tokenizer, this.detector) so the Statistic object can be
garbage-collected.
- Around line 65-68: Remove the unused instance field this.detector and its
instantiation via new Detector(...) in the constructor/initializer; instead rely
on context.detector (used by initialize()) and the ephemeralDetector created in
checkSnippet(). Also update any null-guard that checks this.detector (e.g., the
guard referenced around line 156) to check context.detector or otherwise handle
the absence, and delete the this.detector property declaration so no unused
member remains.

In @apps/jscpd-server/src/setup/ignore.ts:
- Line 15: The current call to ignore.map(...) discards the mapped array so
leading '!' negation markers are not removed; update the code to reassign the
result back to ignore (e.g., ignore = ignore.map(...)) and use replace(/^!/, '')
instead of replace('!', '') so only a leading negation marker is stripped while
preserving other '!' characters.

In @apps/jscpd-server/src/setup/reporters.ts:
- Around line 35-36: The code unguardedly calls options.reporters.forEach which
will throw if options.reporters is undefined; replace the @ts-ignore with a safe
guard or default (e.g., use (options.reporters ?? []) or conditional check)
before iterating so that Reporter processing logic (the forEach over
options.reporters and any handling of reporter strings) only runs when reporters
is an array; update references to the reporter iteration site
(options.reporters.forEach(...)) accordingly and remove the @ts-ignore.

In @apps/jscpd-server/tsconfig.json:
- Around line 21-28: The JSON in the tsconfig has an extraneous trailing comma
after the "ts-node" object which breaks parsing; remove the comma immediately
following the closing brace of the "ts-node" block (the comma after the object
that contains "compilerOptions": { "module": "commonjs" }) so the file becomes
valid JSON.

In @packages/finder/src/files.ts:
- Line 50: The current fallback of '0' causes all non-empty files to be skipped;
change the logic around shouldSkip so that when getOption('maxSize', options) is
falsy you use a very large numeric limit instead of '0' (e.g.,
Number.MAX_SAFE_INTEGER) and compare raw byte counts (use bytes.parse on the
option when present, otherwise use the large numeric limit) so that
bytes.parse(stats.size) > maxBytesOnly skips files correctly; update any
associated debug log to reflect the actual maxSize used (option value or the
large-limit sentinel) and reference the shouldSkip variable, bytes.parse and
getOption('maxSize', options) when making the change.
🧹 Nitpick comments (18)
apps/jscpd-server/nodemon.json (1)

1-7: Consider optimizing the development workflow.

The current configuration runs pnpm build on every file change, which may be slow during active development. For faster iteration, consider using ts-node or a similar tool to execute the TypeScript directly without a full build step.

Additionally, the ignoreRoot: [] property can be removed as an empty array serves no purpose.

⚡ Suggested optimization for faster development
 {
   "$schema": "https://json.schemastore.org/nodemon.json",
   "watch": ["./src/**"],
-  "ignoreRoot": [],
   "ext": "ts,js",
-  "exec": "pnpm build"
+  "exec": "tsx bin/jscpd-server.ts"
 }

Note: This assumes you have tsx installed. Alternatively, you could use ts-node or keep the current approach if full builds are acceptable during development.

apps/jscpd-server/package.json (1)

10-10: Consider using .d.ts extension for broader tooling compatibility.

While .d.mts is valid for ESM declaration files, the more conventional approach is to use .d.ts with "type": "module" in package.json. Some older TypeScript tooling may not recognize .d.mts files.

Also applies to: 13-13

apps/jscpd-server/src/setup/store.ts (2)

7-12: Enhance error logging for better debugging.

The catch block doesn't log the actual error details, making it difficult to diagnose why the store failed to load. Consider logging the error and adding a semicolon for consistency.

📝 Proposed improvements to error handling
    try {
      const store = require(packageName).default;
      return new store();
    } catch (e) {
-     console.error(red('store name ' + storeName + ' not installed.'))
+     console.error(red(`Store "${storeName}" not installed or failed to load.`));
+     console.error(red('Error details:'), e);
    }

4-15: Consider logging the fallback to MemoryStore.

After the error is logged, the function silently falls back to MemoryStore. While this provides graceful degradation, explicitly logging the fallback would make the behavior more transparent to users.

export function getStore(storeName: string | undefined): IStore<IMapFrame> {
  if (storeName) {
    const packageName = '@jscpd/' + storeName + '-store';
    try {
      const store = require(packageName).default;
      return new store();
    } catch (e) {
      console.error(red(`Store "${storeName}" not installed or failed to load.`));
      console.error(red('Error details:'), e);
      console.log('Falling back to in-memory store...');
    }
  }
  return new MemoryStore<IMapFrame>();
}
apps/jscpd-server/src/setup/cli-utils.ts (1)

6-24: Improve error message with attempted paths.

The error message on Line 23 doesn't indicate which paths were attempted, making it harder to debug when package.json cannot be found.

📝 Proposed improvement to error message
  for (const path of possiblePaths) {
    try {
      return readJSONSync(path);
    } catch (e) {
      // Continue to next path
    }
  }

- throw new Error('Could not find package.json');
+ throw new Error(
+   `Could not find package.json. Attempted paths:\n${possiblePaths.map(p => `  - ${p}`).join('\n')}`
+ );
apps/jscpd-server/bin/jscpd-server.ts (1)

3-12: Reconsider re-exporting from a bin script with side effects.

The IIFE on lines 3-10 executes immediately when this file is loaded, meaning any module that imports from this bin script will trigger the server startup as a side effect. The export * from '../src' at line 12 makes this file importable, which could lead to unintended behavior.

If the intent is to expose the API for programmatic use, consumers should import directly from ../src. Consider removing the re-export or restructuring so that the side-effect-free exports and the CLI entry point are separate.

♻️ Suggested fix
 import { runServer } from "../src";
 
 (async () => {
   try {
     await runServer(process.argv, process.exit)
   } catch(e) {
     console.error(e);
     process.exit(1);
   }
 })()
-
-export * from '../src'
apps/jscpd-server/README.md (1)

593-603: Shell script example may fail with special characters.

The jq -Rs . approach for escaping file contents can still fail if the file contains characters that break the outer JSON structure or if the file is binary. Consider noting this limitation or using a more robust approach.

apps/jscpd-server/src/setup/ignore.ts (1)

9-12: Use path.join and replace deprecated substr.

Two minor improvements:

  1. Line 9: Use path.join(process.cwd(), '.gitignore') for cross-platform path handling
  2. Line 12: substr is deprecated; use slice(-1) or substring
♻️ Suggested improvements
+import {join} from "path";
 import {existsSync} from "fs";

 const gitignoreToGlob = require('gitignore-to-glob');

 export function initIgnore(options: IOptions): string[] {
 	const ignore: string[] = options.ignore || [];

-	if (options.gitignore && existsSync(process.cwd() + '/.gitignore')) {
-		let gitignorePatterns: string[] = gitignoreToGlob(process.cwd() + '/.gitignore') || [];
+	if (options.gitignore && existsSync(join(process.cwd(), '.gitignore'))) {
+		let gitignorePatterns: string[] = gitignoreToGlob(join(process.cwd(), '.gitignore')) || [];
 		gitignorePatterns = gitignorePatterns.map((pattern) =>
-			pattern.substr(pattern.length - 1) === '/' ? `${pattern}**/*` : pattern,
+			pattern.slice(-1) === '/' ? `${pattern}**/*` : pattern,
 		);
apps/jscpd-server/src/setup/reporters.ts (1)

43-50: Consider renaming shadowed catch variable.

The inner catch (e) shadows the outer catch (e). While this works correctly due to block scoping, using distinct names like _outerErr and innerErr (or _e for ignored errors) would improve readability.

apps/jscpd-server/src/server/routes.ts (1)

58-65: Consider adding error handling for consistency.

While getState() is unlikely to throw, wrapping this in a try-catch would maintain consistency with other endpoints and guard against future changes.

♻️ Suggested improvement
   router.get('/health', (_req: Request, res: Response) => {
+    try {
       const state = service.getState();
       res.json({
         status: state.isScanning ? 'initializing' : 'ready',
         workingDirectory: state.workingDirectory,
         lastScanTime: state.lastScanTime,
       });
+    } catch (err) {
+      handleRouteError(res, err, 'HealthError', HTTP_STATUS.INTERNAL_SERVER_ERROR);
+    }
   });
apps/jscpd-server/src/server/server.ts (1)

31-40: Content-Type header override may cause issues with non-JSON responses.

The middleware sets Content-Type: application/json for all responses. This could cause issues if you later add endpoints that return other content types (e.g., HTML, plain text, or files).

💡 Consideration

If all endpoints are intended to return JSON, this is fine. Otherwise, consider setting the Content-Type per-response or only for /api routes:

-    this.app.use((_req, res, next) => {
-      res.header("Content-Type", "application/json");
-      next();
-    });
+    // Content-Type is automatically set by res.json()
apps/jscpd-server/src/setup/options.ts (4)

1-1: Avoid using @ts-nocheck in production code.

Disabling TypeScript checks defeats the purpose of type safety. Instead, address the specific type issues with targeted @ts-ignore comments or proper type definitions for the Command object properties.

Consider creating a typed interface for CLI options
interface CliOptions {
  minTokens?: string;
  minLines?: string;
  maxLines?: string;
  maxSize?: string;
  // ... other CLI properties
}

const convertCliToOptions = (cli: Command & CliOptions): Partial<IOptions> => {
  // ...
}

13-15: Use explicit radix with parseInt.

Always pass radix 10 to parseInt for decimal parsing to avoid unexpected behavior with strings starting with 0.

Proposed fix
-    minTokens: cli.minTokens ? parseInt(cli.minTokens) : undefined,
-    minLines: cli.minLines ? parseInt(cli.minLines) : undefined,
-    maxLines: cli.maxLines ? parseInt(cli.maxLines) : undefined,
+    minTokens: cli.minTokens ? parseInt(cli.minTokens, 10) : undefined,
+    minLines: cli.minLines ? parseInt(cli.minLines, 10) : undefined,
+    maxLines: cli.maxLines ? parseInt(cli.maxLines, 10) : undefined,

26-26: Redundant assignment: format is assigned twice.

Line 26 assigns cli.format directly, but lines 46-48 always overwrite it with cli.format.split(',') when cli.format is truthy. Remove the redundant assignment on line 26.

Proposed fix
     output: cli.output,
-    format: cli.format,
     formatsExts: parseFormatsExtensions(cli.formatsExts),

Also applies to: 46-48


84-84: Use path.join for cross-platform path construction.

String concatenation with / may cause issues on Windows.

Proposed fix
-  const config = resolve(process.cwd() + '/package.json');
+  const config = resolve(process.cwd(), 'package.json');
apps/jscpd-server/__tests__/server.test.ts (2)

180-204: Test uses Date.now() which may produce flaky behavior.

The unique code generation relies on Date.now() in template literals, but these are evaluated once when the test runs. If the test executes quickly, Date.now() values could theoretically collide across test runs or be cached. Consider using a counter or UUID for more reliable uniqueness.


322-350: Good lifecycle tests, but consider adding error case for double-start.

The test for multiple stop() calls is good. Consider also adding a test that verifies behavior when calling start() twice on the same server instance (e.g., port already in use error handling).

packages/finder/src/files.ts (1)

49-50: Replace @ts-expect-error with a safer type assertion.

Using @ts-expect-error to suppress TypeScript errors is risky as it can hide legitimate type errors in the future. Consider using a non-null assertion or type guard instead.

♻️ Proposed fix using non-null assertion
-    // @ts-expect-error - stats is checked above, but DTS build doesn't recognize control flow
-    const shouldSkip = bytes.parse(stats.size) > bytes.parse(getOption('maxSize', options) || '0');
+    const shouldSkip = bytes.parse(stats!.size) > bytes.parse(getOption('maxSize', options) || '0');

Or use a type guard for better safety:

     if (!stats) {
       return true;
     }
-    // @ts-expect-error - stats is checked above, but DTS build doesn't recognize control flow
-    const shouldSkip = bytes.parse(stats.size) > bytes.parse(getOption('maxSize', options) || '0');
+    const size = stats.size;
+    const shouldSkip = bytes.parse(size) > bytes.parse(getOption('maxSize', options) || '0');
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e8381ad and c183ce2.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (34)
  • apps/jscpd-server/README.md
  • apps/jscpd-server/__tests__/server.test.ts
  • apps/jscpd-server/bin/jscpd-server
  • apps/jscpd-server/bin/jscpd-server.ts
  • apps/jscpd-server/nodemon.json
  • apps/jscpd-server/package.json
  • apps/jscpd-server/src/detect.ts
  • apps/jscpd-server/src/index.ts
  • apps/jscpd-server/src/server/constants.ts
  • apps/jscpd-server/src/server/ephemeral-store.ts
  • apps/jscpd-server/src/server/index.ts
  • apps/jscpd-server/src/server/middleware.ts
  • apps/jscpd-server/src/server/routes.ts
  • apps/jscpd-server/src/server/server.ts
  • apps/jscpd-server/src/server/service.ts
  • apps/jscpd-server/src/server/types/index.ts
  • apps/jscpd-server/src/server/types/requests.ts
  • apps/jscpd-server/src/server/types/responses.ts
  • apps/jscpd-server/src/server/types/state.ts
  • apps/jscpd-server/src/setup/cli-utils.ts
  • apps/jscpd-server/src/setup/hooks.ts
  • apps/jscpd-server/src/setup/ignore.ts
  • apps/jscpd-server/src/setup/index.ts
  • apps/jscpd-server/src/setup/options.ts
  • apps/jscpd-server/src/setup/reporters.ts
  • apps/jscpd-server/src/setup/store.ts
  • apps/jscpd-server/src/setup/subscribers.ts
  • apps/jscpd-server/tsconfig.build.json
  • apps/jscpd-server/tsconfig.json
  • apps/jscpd-server/tsup.config.ts
  • apps/jscpd/package.json
  • package.json
  • packages/finder/src/files.ts
  • pnpm-workspace.yaml
🧰 Additional context used
🧬 Code graph analysis (14)
apps/jscpd-server/src/setup/hooks.ts (3)
packages/core/src/interfaces/options.interface.ts (1)
  • IOptions (1-37)
packages/finder/src/in-files-detector.ts (1)
  • InFilesDetector (18-113)
packages/finder/src/hooks/blamer.ts (1)
  • BlamerHook (6-37)
apps/jscpd-server/src/setup/subscribers.ts (4)
packages/core/src/interfaces/options.interface.ts (1)
  • IOptions (1-37)
packages/finder/src/in-files-detector.ts (1)
  • InFilesDetector (18-113)
packages/finder/src/subscribers/verbose.ts (1)
  • VerboseSubscriber (4-32)
packages/finder/src/subscribers/progress.ts (1)
  • ProgressSubscriber (4-14)
apps/jscpd-server/src/server/middleware.ts (2)
apps/jscpd-server/src/server/types/responses.ts (1)
  • ErrorResponse (37-41)
apps/jscpd-server/src/server/constants.ts (2)
  • HTTP_STATUS (29-35)
  • ERROR_MESSAGES (11-21)
apps/jscpd-server/src/server/types/responses.ts (1)
packages/core/src/interfaces/statistic.interface.ts (1)
  • IStatistic (19-23)
apps/jscpd-server/src/setup/cli-utils.ts (1)
packages/core/src/options.ts (1)
  • getOption (34-37)
apps/jscpd-server/src/server/constants.ts (1)
apps/jscpd-server/src/setup/cli-utils.ts (1)
  • readPackageJson (6-24)
apps/jscpd-server/__tests__/server.test.ts (1)
apps/jscpd-server/src/server/server.ts (1)
  • JscpdServer (15-110)
apps/jscpd-server/src/server/routes.ts (5)
apps/jscpd-server/src/server/constants.ts (1)
  • HTTP_STATUS (29-35)
apps/jscpd-server/src/server/types/responses.ts (1)
  • ErrorResponse (37-41)
apps/jscpd-server/src/server/service.ts (1)
  • JscpdServerService (27-219)
apps/jscpd-server/src/server/middleware.ts (1)
  • validateCheckRequest (44-63)
apps/jscpd-server/src/server/types/requests.ts (1)
  • CheckSnippetRequest (1-4)
packages/finder/src/files.ts (1)
packages/core/src/options.ts (1)
  • getOption (34-37)
apps/jscpd-server/src/server/types/state.ts (1)
packages/core/src/interfaces/statistic.interface.ts (1)
  • IStatistic (19-23)
apps/jscpd-server/src/server/ephemeral-store.ts (1)
packages/core/src/interfaces/map-frame.interface.ts (1)
  • IMapFrame (3-10)
apps/jscpd-server/src/detect.ts (5)
packages/core/src/interfaces/options.interface.ts (1)
  • IOptions (1-37)
packages/finder/src/in-files-detector.ts (1)
  • InFilesDetector (18-113)
packages/tokenizer/src/formats.ts (1)
  • getSupportedFormats (467-469)
apps/jscpd-server/src/setup/store.ts (1)
  • getStore (4-15)
packages/finder/src/files.ts (1)
  • getFilesToDetect (80-113)
apps/jscpd-server/bin/jscpd-server.ts (1)
apps/jscpd-server/src/index.ts (1)
  • runServer (35-66)
apps/jscpd-server/src/setup/ignore.ts (1)
packages/core/src/interfaces/options.interface.ts (1)
  • IOptions (1-37)
🔇 Additional comments (46)
pnpm-workspace.yaml (2)

2-3: LGTM: Cosmetic formatting change.

Removing quotes from package patterns is a valid stylistic choice. Both formats work identically in YAML.


5-7: Remove non-existent packages from onlyBuiltDependencies configuration.

The field onlyBuiltDependencies is valid pnpm configuration for whitelisting packages allowed to run lifecycle scripts. However, neither classic-level nor esbuild appear as dependencies in any workspace packages (package.json files across apps/* and packages/*). These entries should either be:

  • Removed if no longer needed
  • Added as actual workspace dependencies if intentional
package.json (2)

32-32: Verify dependency placement of @vitest/coverage-v8.

The @vitest/coverage-v8 package is listed under dependencies rather than devDependencies, which is unusual for a test coverage tool. In monorepo root package.json files, dependencies are typically avoided unless they need to be shared across all workspaces at runtime.

Please confirm this placement is intentional. If it's only needed for testing, consider moving it to devDependencies.


20-23: All tooling versions are available on npm.

The updated devDependencies (@changesets/cli@2.29.8, commitizen@4.3.1, cz-conventional-changelog@3.3.0, turbo@1.13.4) are all valid and accessible from the npm registry.

apps/jscpd-server/tsup.config.ts (1)

1-9: LGTM!

The tsup configuration is well-structured with appropriate settings for a server package that provides both a CLI tool and a library API. Dual ESM/CJS output, source maps, and code splitting are all correctly configured.

apps/jscpd-server/src/setup/hooks.ts (1)

4-9: LGTM!

The hook registration logic is correct and follows the expected pattern from the @jscpd/finder package. The unconditional registration of FragmentsHook and conditional registration of BlamerHook based on the options.blame flag is appropriate.

apps/jscpd/package.json (1)

58-58: The version @types/node@24.10.0 is valid and published. No issues found.

apps/jscpd-server/package.json (2)

18-20: LGTM! Binary configuration follows npm conventions.

The bin entry correctly points to the executable file. Ensure the target file has a proper shebang (#!/usr/bin/env node) and execute permissions.


49-49: Commander 5.1.0 has no known security vulnerabilities and is compatible with the project.

The version is confirmed to have zero recorded security issues according to Snyk and npm. While commander is significantly outdated (current version is 14.0.2), the project's caret constraint maintains stability within the v5.x line, and no action is required.

apps/jscpd-server/src/server/ephemeral-store.ts (2)

32-34: Verify that not closing sourceStore is intentional.

The close() method only closes the ephemeral store. If sourceStore is a shared resource across multiple instances, this is correct. However, if each instance owns its sourceStore, not closing it could lead to resource leaks (e.g., open file handles, database connections).

Consider adding a comment documenting this behavior, or verify that sourceStore lifecycle is managed elsewhere:

close(): void {
  // Only close ephemeral store; sourceStore is shared and managed externally
  this.ephemeralStore.close();
}

9-18: LGTM! Clean hybrid store implementation.

The class structure and namespace delegation are well-designed. The pattern of layering an ephemeral store over a source store is appropriate for the use case described in the documentation.

apps/jscpd-server/src/setup/cli-utils.ts (2)

26-28: LGTM! Helper functions are clean and concise.

Both createBaseCommand and getWorkingDirectory are well-implemented with appropriate fallback behavior.

Also applies to: 61-63


30-59: Code is safe; all getOption() calls return defined values.

All five option keys (minLines, minTokens, maxLines, maxSize, mode) have defined defaults in getDefaultOptions() at packages/core/src/options.ts:5-31. The getOption() function always falls back to these defaults, ensuring the string concatenations on lines 38, 42, 44, 47, and 51 will never receive undefined or null values.

apps/jscpd-server/src/server/types/responses.ts (1)

1-59: LGTM! Well-structured API response types.

The type definitions are clean, comprehensive, and follow TypeScript best practices:

  • Clear separation of concerns with distinct interfaces
  • Appropriate use of optional fields (e.g., fragment?)
  • Type-safe literal unions (e.g., 'initializing' | 'ready')
  • Proper reuse of imported types from @jscpd/core

These interfaces provide a solid contract for the REST API.

apps/jscpd-server/src/server/index.ts (1)

1-6: LGTM!

Standard barrel file pattern for consolidating the server module's public exports.

apps/jscpd-server/tsconfig.build.json (1)

1-11: LGTM!

Clean build configuration that appropriately extends the parent config and sets up compilation output.

apps/jscpd-server/bin/jscpd-server (1)

1-3: LGTM!

Standard CLI wrapper that correctly delegates to the compiled output.

apps/jscpd-server/src/setup/index.ts (1)

1-3: LGTM!

Clean barrel file for setup utilities.

apps/jscpd-server/tsconfig.json (1)

1-20: LGTM!

Well-configured TypeScript setup with appropriate strict mode flags for a Node.js 20 project.

apps/jscpd-server/src/server/types/index.ts (1)

1-3: LGTM!

Clean barrel export pattern that consolidates the type definitions for the server API surface.

apps/jscpd-server/src/server/types/requests.ts (1)

1-4: LGTM!

The request interface is clean and matches the documented API contract for the /api/check endpoint.

apps/jscpd-server/src/setup/subscribers.ts (1)

4-12: LGTM!

The subscriber registration logic correctly applies VerboseSubscriber when verbose mode is enabled and ProgressSubscriber when silent mode is not active. This centralizes the subscriber hookup logic cleanly.

apps/jscpd-server/src/server/types/state.ts (1)

1-8: LGTM!

The ServerState interface cleanly captures the server's runtime state, supporting the health check endpoint's ability to report initialization status and scan timestamps.

apps/jscpd-server/src/index.ts (2)

12-33: LGTM!

The CLI initialization is well-structured with proper option definitions. The packageJson: any type is acceptable for this use case since the JSON structure is read dynamically.


35-66: LGTM!

The runServer function has solid error handling:

  • Port validation correctly rejects NaN and out-of-range values
  • Dynamic import allows lazy loading of server dependencies
  • Errors are logged and exit callback is invoked appropriately
  • Returns null on failure for graceful handling by callers
apps/jscpd-server/src/detect.ts (2)

22-43: LGTM!

The createBaseDetectorContext function properly merges defaults, initializes all required components, and returns a well-typed context object. The use of MD5 for content hashing is appropriate for duplicate detection purposes (non-cryptographic use case).


45-64: LGTM!

The detectClones function is well-structured with proper separation between silent and verbose modes. The timing instrumentation is appropriately guarded by the silent option.

apps/jscpd-server/src/server/routes.ts (3)

7-23: LGTM!

The handleRouteError helper is well-designed, properly handling both Error instances and string/unknown error types with a consistent ErrorResponse structure.


28-36: LGTM!

The /check endpoint properly validates input via middleware, handles async errors, and returns structured responses.


38-56: LGTM!

The /stats endpoint correctly handles the "not ready" state with a 503 response and has proper error handling for unexpected failures.

apps/jscpd-server/src/server/middleware.ts (3)

5-42: LGTM!

The validation helpers are well-designed with a clean FieldValidation interface and reusable validateField function that properly handles required fields, type checking, and empty string validation.


44-63: LGTM!

The validateCheckRequest middleware is well-structured, validating both required fields (code and format) and returning early on validation failures.


82-89: LGTM!

The notFoundHandler provides a clear, consistent 404 response with useful route information for debugging.

apps/jscpd-server/src/server/constants.ts (1)

5-35: LGTM!

The constants are well-organized with proper use of as const for type narrowing. The centralized configuration makes maintenance easier.

apps/jscpd-server/src/server/server.ts (4)

15-29: LGTM!

The JscpdServer class is well-structured with proper separation of concerns. The constructor correctly initializes the service and Express app, then sets up middleware, routes, and error handlers in the correct order.


65-85: LGTM!

The start() method properly initializes the service before starting the HTTP server and handles both synchronous and asynchronous errors appropriately via the Promise pattern.


87-101: LGTM!

The stop() method implements graceful shutdown correctly, closing the HTTP server first then cleaning up the service. It also handles the case where the server wasn't started (line 100).


112-125: LGTM!

The startServer helper function provides a clean public API for starting the server programmatically.

apps/jscpd-server/src/setup/options.ts (1)

95-137: LGTM!

The configuration merging logic correctly applies precedence (CLI > stored config > package.json > defaults), and the post-processing for reporters, mode, formats, and ignore rules is well-structured.

apps/jscpd-server/__tests__/server.test.ts (2)

10-25: Setup correctly initializes service without starting HTTP server.

The pattern of calling getService().initialize() directly (instead of server.start()) is appropriate for supertest-based testing, as it avoids binding to a real port while still initializing the service state. This is a valid approach.


547-701: Excellent isolation and concurrency test coverage.

These tests thoroughly validate that:

  • Snippet tokens don't contaminate the project store
  • Concurrent requests don't cross-contaminate each other
  • Sequential requests produce consistent results

The assertions checking for <snippet> paths in codebaseLocation.file are particularly valuable for ensuring proper isolation.

apps/jscpd-server/src/server/service.ts (4)

77-82: Snippet counter is safe but consider edge cases.

The snippetCounter++ is safe in Node.js's single-threaded event loop. However, after many requests (Number.MAX_SAFE_INTEGER), the counter could overflow. For a long-running server, consider resetting on close() or using a more robust ID scheme.

The counter is reset in close() (line 215), which handles service restarts appropriately.


151-193: Solid ephemeral store pattern for request isolation.

The try/finally ensures ephemeralStore.close() is always called, preventing resource leaks. The ephemeral hybrid store approach correctly prevents snippet tokens from contaminating the shared project store.


23-25: LGTM!

The percentage calculation correctly handles the zero-division edge case and uses integer rounding for precision.


139-149: No issue found - the code is correct.

The namespace() method mutates the store in-place and returns void, so discarding the return value is the proper usage pattern. No changes needed.

packages/finder/src/files.ts (1)

46-48: The guard for undefined stats is defensive and likely unnecessary given fast-glob's guarantees.

Since fast-glob is configured with stats: true, it guarantees that the stats field will always be present as an fs.Stats object. The if (!stats) { return true; } guard appears to be defensive programming for an edge case that shouldn't occur in normal operation. If this guard is needed, it suggests either:

  1. The stats: true configuration is not being respected, or
  2. The guard is masking a TypeScript type-checking issue (as evidenced by the @ts-expect-error comment)

Consider removing this guard and instead addressing the TypeScript type narrowing issue directly, or document why this defensive guard is necessary if there are edge cases where fast-glob might return undefined stats despite the configuration.

Comment on lines +453 to +464
# Start server with LevelDB persistence
jscpd server /path/to/project --store leveldb --port 3000

# On first start, the server will:
# 1. Scan the codebase
# 2. Store results in .jscpd/ directory
# 3. Accept requests

# On subsequent restarts, the server will:
# 1. Load cached data from .jscpd/ directory
# 2. Accept requests immediately (no re-scan needed)
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent command name in documentation.

The command here uses jscpd server but the CLI entry point established in this PR is jscpd-server (as shown in the "Starting the Server" section, lines 13-25). This inconsistency could confuse users.

📝 Suggested fix
-jscpd server /path/to/project --store leveldb --port 3000
+jscpd-server /path/to/project --store leveldb --port 3000
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Start server with LevelDB persistence
jscpd server /path/to/project --store leveldb --port 3000
# On first start, the server will:
# 1. Scan the codebase
# 2. Store results in .jscpd/ directory
# 3. Accept requests
# On subsequent restarts, the server will:
# 1. Load cached data from .jscpd/ directory
# 2. Accept requests immediately (no re-scan needed)
```
# Start server with LevelDB persistence
jscpd-server /path/to/project --store leveldb --port 3000
# On first start, the server will:
# 1. Scan the codebase
# 2. Store results in .jscpd/ directory
# 3. Accept requests
# On subsequent restarts, the server will:
# 1. Load cached data from .jscpd/ directory
# 2. Accept requests immediately (no re-scan needed)
🤖 Prompt for AI Agents
In @apps/jscpd-server/README.md around lines 453 - 464, The README shows an
inconsistent CLI invocation: it documents `jscpd server /path/to/project` but
the project exposes the CLI entrypoint as `jscpd-server`; update the example and
any other occurrences to use the correct command `jscpd-server` (or vice versa
if the actual binary should be `jscpd`) so they match; search for the strings
`jscpd server` and `jscpd-server` (e.g., the "Starting the Server" section and
the example under "Start server with LevelDB persistence") and make them
consistent.

Comment on lines +586 to +589
- name: Start jscpd server
run: |
jscpd server . --port 3000 &
sleep 10 # Wait for server to initialize

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same command inconsistency in CI/CD example.

The GitHub Actions example uses jscpd server instead of jscpd-server.

📝 Suggested fix
       - name: Start jscpd server
         run: |
-          jscpd server . --port 3000 &
+          jscpd-server . --port 3000 &
           sleep 10  # Wait for server to initialize
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Start jscpd server
run: |
jscpd server . --port 3000 &
sleep 10 # Wait for server to initialize
- name: Start jscpd server
run: |
jscpd-server . --port 3000 &
sleep 10 # Wait for server to initialize
🤖 Prompt for AI Agents
In @apps/jscpd-server/README.md around lines 586 - 589, The CI example uses the
wrong executable name; update the GitHub Actions step under the "Start jscpd
server" job to call the correct binary `jscpd-server` (replace `jscpd server .
--port 3000 &` with `jscpd-server . --port 3000 &`) and scan the README for
other occurrences of `jscpd server` to make them consistent with the
`jscpd-server` command used elsewhere.

Comment on lines +1 to +3
import { readPackageJson } from "../setup";

const packageJson = readPackageJson();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Module load failure if package.json is not found.

readPackageJson() is called at module load time. If it throws (package.json not found), the entire module fails to import, which will crash the application during startup. Consider handling this gracefully with a fallback version.

♻️ Suggested defensive approach
 import { readPackageJson } from "../setup";

-const packageJson = readPackageJson();
+let packageVersion = 'unknown';
+try {
+  packageVersion = readPackageJson().version;
+} catch {
+  console.warn('Could not read package.json for version info');
+}

 // ...

 export const API_INFO = {
   NAME: "jscpd-server",
-  VERSION: packageJson.version,
+  VERSION: packageVersion,
   DOCUMENTATION_URL: "https://github.com/kucherenko/jscpd",
 } as const;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @apps/jscpd-server/src/server/constants.ts around lines 1 - 3, The module
currently calls readPackageJson() at import time which can throw and crash
startup; change constants.ts to call readPackageJson() inside a try/catch (or
create a small helper like getPackageJsonSafe or getPackageVersion) and fall
back to a safe default version string (e.g. "0.0.0" or undefined) when the file
is missing or parsing fails, then export the same constant names (e.g.
packageJson or PACKAGE_VERSION) so callers aren’t broken but the import no
longer throws.

Comment on lines +20 to +26
async get(key: string): Promise<IMapFrame> {
try {
return await this.ephemeralStore.get(key);
} catch {
return this.sourceStore.get(key);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Overly broad error handling masks legitimate errors.

The empty catch block on Line 23 swallows all errors from ephemeralStore.get(), not just "not found" errors. This could hide legitimate issues like permission errors, corruption, or connection failures. Additionally, sourceStore.get() on Line 24 can also throw but isn't handled.

🛡️ Proposed fix to handle only "not found" errors

If IStore.get() throws a specific error type for missing keys, catch only that error:

  async get(key: string): Promise<IMapFrame> {
    try {
      return await this.ephemeralStore.get(key);
-   } catch {
-     return this.sourceStore.get(key);
+   } catch (error) {
+     // Only fallback for "not found" errors; propagate others
+     if (isNotFoundError(error)) {
+       return this.sourceStore.get(key);
+     }
+     throw error;
    }
  }

Alternatively, if the store implementation returns undefined or null for missing keys instead of throwing:

  async get(key: string): Promise<IMapFrame> {
-   try {
-     return await this.ephemeralStore.get(key);
-   } catch {
-     return this.sourceStore.get(key);
-   }
+   const ephemeralResult = await this.ephemeralStore.get(key);
+   if (ephemeralResult) {
+     return ephemeralResult;
+   }
+   return this.sourceStore.get(key);
  }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +65 to +80
export function errorHandler(
err: Error,
_req: Request,
res: Response,
_next: NextFunction
): void {
console.error('Error:', err);

const error: ErrorResponse = {
error: err.name || 'InternalServerError',
message: err.message || 'An unexpected error occurred',
statusCode: HTTP_STATUS.INTERNAL_SERVER_ERROR,
};

res.status(error.statusCode).json(error);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid logging potentially sensitive code snippets in errors.

The error handler logs the full error object. If errors contain user-submitted code snippets (e.g., from validation failures or processing errors), this could inadvertently log sensitive content. Consider logging only error name and message rather than the full error object.

♻️ Suggested improvement
   console.error('Error:', err);
+  // Consider: console.error('Error:', err.name, err.message);
+  // to avoid logging potentially sensitive stack traces or data
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function errorHandler(
err: Error,
_req: Request,
res: Response,
_next: NextFunction
): void {
console.error('Error:', err);
const error: ErrorResponse = {
error: err.name || 'InternalServerError',
message: err.message || 'An unexpected error occurred',
statusCode: HTTP_STATUS.INTERNAL_SERVER_ERROR,
};
res.status(error.statusCode).json(error);
}
export function errorHandler(
err: Error,
_req: Request,
res: Response,
_next: NextFunction
): void {
console.error('Error:', err.name, err.message);
const error: ErrorResponse = {
error: err.name || 'InternalServerError',
message: err.message || 'An unexpected error occurred',
statusCode: HTTP_STATUS.INTERNAL_SERVER_ERROR,
};
res.status(error.statusCode).json(error);
}
🤖 Prompt for AI Agents
In @apps/jscpd-server/src/server/middleware.ts around lines 65 - 80, The
errorHandler currently logs the full error object which may include sensitive
user-submitted code; change the logging to only emit safe fields (e.g., err.name
and err.message) and avoid printing err.stack or the entire err object. In the
errorHandler function, replace console.error('Error:', err) with a focused log
such as console.error('Error:', err.name, '-', err.message) or use the app's
logger to record only those fields, and keep constructing the ErrorResponse
(error, message, statusCode) unchanged so the response behavior remains the
same.

Comment on lines +206 to +218
async close(): Promise<void> {
if (this.store) {
await this.store.close();
}

this.store = null;
this.options = null;
this.tokenizer = null;
this.detector = null;
this.snippetCounter = 0;
this.state.statistics = null;
this.state.lastScanTime = null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

this.statistic field not reset in close().

The statistic field (line 31) holds a reference to a Statistic object but is not set to null during cleanup, potentially preventing garbage collection.

Proposed fix
   async close(): Promise<void> {
     if (this.store) {
       await this.store.close();
     }

     this.store = null;
     this.options = null;
+    this.statistic = null;
     this.tokenizer = null;
     this.detector = null;
     this.snippetCounter = 0;
     this.state.statistics = null;
     this.state.lastScanTime = null;
   }
🤖 Prompt for AI Agents
In @apps/jscpd-server/src/server/service.ts around lines 206 - 218, The close()
method currently cleans up many fields but omits resetting the Statistic
reference; update the async close() implementation (method close in the service
class) to set this.statistic = null as part of teardown (place it alongside the
other null assignments such as this.store, this.options, this.tokenizer,
this.detector) so the Statistic object can be garbage-collected.

pattern.substr(pattern.length - 1) === '/' ? `${pattern}**/*` : pattern,
);
ignore.push(...gitignorePatterns);
ignore.map((pattern) => pattern.replace('!', ''));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: map result is discarded, negation markers are not removed.

The ignore.map(...) call returns a new array with patterns stripped of !, but the result is never assigned. The original ignore array remains unchanged.

🐛 Proposed fix
-		ignore.map((pattern) => pattern.replace('!', ''));
+		const cleanedIgnore = ignore.map((pattern) => pattern.replace(/^!/, ''));
+		return cleanedIgnore;
 	}
-	return ignore;
+	return ignore;

Or more concisely, reassign the result:

-		ignore.map((pattern) => pattern.replace('!', ''));
+		return ignore.map((pattern) => pattern.replace(/^!/, ''));

Note: Also changed replace('!', '') to replace(/^!/, '') to only strip leading negation markers.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ignore.map((pattern) => pattern.replace('!', ''));
return ignore.map((pattern) => pattern.replace(/^!/, ''));
🤖 Prompt for AI Agents
In @apps/jscpd-server/src/setup/ignore.ts at line 15, The current call to
ignore.map(...) discards the mapped array so leading '!' negation markers are
not removed; update the code to reassign the result back to ignore (e.g., ignore
= ignore.map(...)) and use replace(/^!/, '') instead of replace('!', '') so only
a leading negation marker is stripped while preserving other '!' characters.

Comment on lines +35 to +36
// @ts-ignore
options.reporters.forEach((reporter: string) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard against undefined options.reporters.

The @ts-ignore comment suppresses the type error, but if options.reporters is undefined, calling .forEach will throw a runtime error. Add a guard or default to an empty array.

🐛 Proposed fix
-  // @ts-ignore
-  options.reporters.forEach((reporter: string) => {
+  const reporterList = options.reporters ?? [];
+  reporterList.forEach((reporter: string) => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// @ts-ignore
options.reporters.forEach((reporter: string) => {
const reporterList = options.reporters ?? [];
reporterList.forEach((reporter: string) => {
🤖 Prompt for AI Agents
In @apps/jscpd-server/src/setup/reporters.ts around lines 35 - 36, The code
unguardedly calls options.reporters.forEach which will throw if
options.reporters is undefined; replace the @ts-ignore with a safe guard or
default (e.g., use (options.reporters ?? []) or conditional check) before
iterating so that Reporter processing logic (the forEach over options.reporters
and any handling of reporter strings) only runs when reporters is an array;
update references to the reporter iteration site
(options.reporters.forEach(...)) accordingly and remove the @ts-ignore.

Comment on lines +21 to +28
"ts-node": {
// these options are overrides used only by ts-node
// same as the --compilerOptions flag and the TS_NODE_COMPILER_OPTIONS environment variable
"compilerOptions": {
"module": "commonjs"
}
},
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Remove the trailing comma on line 28.

Line 28 contains a trailing comma after the closing brace, which is invalid JSON syntax and will cause parsing errors.

🐛 Proposed fix
   "ts-node": {
     // these options are overrides used only by ts-node
     // same as the --compilerOptions flag and the TS_NODE_COMPILER_OPTIONS environment variable
     "compilerOptions": {
       "module": "commonjs"
     }
-  },
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"ts-node": {
// these options are overrides used only by ts-node
// same as the --compilerOptions flag and the TS_NODE_COMPILER_OPTIONS environment variable
"compilerOptions": {
"module": "commonjs"
}
},
}
"ts-node": {
// these options are overrides used only by ts-node
// same as the --compilerOptions flag and the TS_NODE_COMPILER_OPTIONS environment variable
"compilerOptions": {
"module": "commonjs"
}
}
}
🤖 Prompt for AI Agents
In @apps/jscpd-server/tsconfig.json around lines 21 - 28, The JSON in the
tsconfig has an extraneous trailing comma after the "ts-node" object which
breaks parsing; remove the comma immediately following the closing brace of the
"ts-node" block (the comma after the object that contains "compilerOptions": {
"module": "commonjs" }) so the file becomes valid JSON.

return true;
}
// @ts-expect-error - stats is checked above, but DTS build doesn't recognize control flow
const shouldSkip = bytes.parse(stats.size) > bytes.parse(getOption('maxSize', options) || '0');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Wrong fallback value for maxSize will filter out all files.

The fallback value '0' is incorrect. When getOption('maxSize', options) returns a falsy value, bytes.parse('0') returns 0, making the comparison stats.size > 0. This would skip all non-empty files, effectively breaking the file detection.

If maxSize is not specified, the intent should be to have no size limit. Use a very large fallback value instead.

🐛 Proposed fix using a large fallback value
-    const shouldSkip = bytes.parse(stats!.size) > bytes.parse(getOption('maxSize', options) || '0');
+    const shouldSkip = bytes.parse(stats!.size) > bytes.parse(getOption('maxSize', options) || '999GB');

Also update the debug log for consistency:

     if (options.debug && shouldSkip) {
-      console.log(`File ${path} skipped! Size more then limit (${bytes(stats.size)} > ${getOption('maxSize', options)})`);
+      console.log(`File ${path} skipped! Size more then limit (${bytes(stats.size)} > ${getOption('maxSize', options) || '999GB'})`);
     }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @packages/finder/src/files.ts at line 50, The current fallback of '0' causes
all non-empty files to be skipped; change the logic around shouldSkip so that
when getOption('maxSize', options) is falsy you use a very large numeric limit
instead of '0' (e.g., Number.MAX_SAFE_INTEGER) and compare raw byte counts (use
bytes.parse on the option when present, otherwise use the large numeric limit)
so that bytes.parse(stats.size) > maxBytesOnly skips files correctly; update any
associated debug log to reflect the actual maxSize used (option value or the
large-limit sentinel) and reference the shouldSkip variable, bytes.parse and
getOption('maxSize', options) when making the change.

@kucherenko kucherenko changed the title Feature/server Implement jscpd server Jan 9, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/jscpd/package.json (2)

44-55: Move @jscpd/badge-reporter to optionalDependencies.

The badge-reporter package is not statically imported in the codebase and is only loaded dynamically when explicitly requested by users (via the reporter loading mechanism at src/init/reporters.ts lines 40–51). Unlike @jscpd/html-reporter and jscpd-sarif-reporter, which are hardcoded into the reporters registry and statically imported, badge-reporter is an optional plugin and should not be a required dependency. Moving it to optionalDependencies prevents unnecessary installations in minimal environments and CI pipelines.


56-66: @types/node@^24.10.0 conflicts with @tsconfig/node20—align to Node 20.

@types/node@24.10.0 targets Node.js v24, but @tsconfig/node20 targets Node.js v20. Either downgrade @types/node to ^20.x.x (the v20 series) or remove/update @tsconfig/node20 to match the intended target. Explicitly declare Node engine support in engines field to clarify intent.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c183ce2 and 098c456.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (1)
  • apps/jscpd/package.json
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Rooview

@kucherenko kucherenko merged commit 6d3c2e4 into master Jan 11, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant