-
-
Notifications
You must be signed in to change notification settings - Fork 245
Implement jscpd server #746
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3b2be71
2dc02ac
25ddc39
c183ce2
098c456
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| #!/usr/bin/env node | ||
|
|
||
| require("../dist/bin/jscpd-server"); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { runServer } from "../src"; | ||
|
|
||
| (async () => { | ||
| try { | ||
| await runServer(process.argv, process.exit) | ||
| } catch(e) { | ||
| console.error(e); | ||
| process.exit(1); | ||
| } | ||
| })() | ||
|
|
||
| export * from '../src' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "$schema": "https://json.schemastore.org/nodemon.json", | ||
| "watch": ["./src/**"], | ||
| "ignoreRoot": [], | ||
| "ext": "ts,js", | ||
| "exec": "pnpm build" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| { | ||
| "name": "jscpd-server", | ||
| "version": "4.0.5", | ||
| "description": "jscpd server application", | ||
| "author": "Andrey Kucherenko <kucherenko.andrey@gmail.com>", | ||
| "homepage": "https://github.com/kucherenko/jscpd#readme", | ||
| "license": "MIT", | ||
| "main": "dist/src/index.js", | ||
| "module": "dist/src/index.mjs", | ||
| "typings": "dist/src/index.d.mts", | ||
| "exports": { | ||
| ".": { | ||
| "types": "./dist/src/index.d.mts", | ||
| "import": "./dist/src/index.mjs", | ||
| "require": "./dist/src/index.js" | ||
| } | ||
| }, | ||
| "bin": { | ||
| "jscpd-server": "./bin/jscpd-server" | ||
| }, | ||
| "directories": { | ||
| "lib": "src", | ||
| "bin": "bin", | ||
| "test": "__tests__" | ||
| }, | ||
| "files": [ | ||
| "dist" | ||
| ], | ||
| "publishConfig": { | ||
| "access": "public" | ||
| }, | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+ssh://git@github.com/kucherenko/jscpd.git" | ||
| }, | ||
| "scripts": { | ||
| "build": "tsup-node --dts", | ||
| "dev": "nodemon", | ||
| "test": "vitest run", | ||
| "typecheck": "tsc", | ||
| "cleanup": "rimraf ./dist .turbo" | ||
| }, | ||
| "dependencies": { | ||
| "@jscpd/core": "workspace:*", | ||
| "@jscpd/finder": "workspace:*", | ||
| "@jscpd/html-reporter": "workspace:*", | ||
| "@jscpd/tokenizer": "workspace:*", | ||
| "colors": "^1.4.0", | ||
| "commander": "^5.1.0", | ||
| "express": "^4.22.1", | ||
| "fs-extra": "^11.3.3", | ||
| "gitignore-to-glob": "^0.3.0", | ||
| "jscpd-sarif-reporter": "workspace:*", | ||
| "morgan": "^1.10.1" | ||
| }, | ||
| "devDependencies": { | ||
| "@tsconfig/node20": "^20.1.8", | ||
| "@types/express": "^4.17.25", | ||
| "@types/fs-extra": "^11.0.4", | ||
| "@types/morgan": "^1.9.10", | ||
| "@types/node": "^24.10.4", | ||
| "@types/supertest": "^6.0.3", | ||
| "@vitest/coverage-v8": "^2.1.9", | ||
| "nodemon": "^3.1.11", | ||
| "supertest": "^7.2.2", | ||
| "ts-node": "^10.9.2", | ||
| "tsup": "^8.5.1", | ||
| "typescript": "^5.9.3", | ||
| "vitest": "^2.1.9" | ||
| }, | ||
| "preferGlobal": true | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import {getDefaultOptions, IClone, IMapFrame, IOptions, IStore, Statistic} from '@jscpd/core'; | ||
| import {grey, italic} from 'colors/safe'; | ||
| import {EntryWithContent, getFilesToDetect, InFilesDetector} from '@jscpd/finder'; | ||
| import {createHash} from 'crypto'; | ||
| import {getStore} from './setup/store'; | ||
| import {getSupportedFormats, Tokenizer} from '@jscpd/tokenizer'; | ||
| import {registerReporters} from './setup/reporters'; | ||
| import {registerSubscribers} from './setup/subscribers'; | ||
| import {registerHooks} from './setup/hooks'; | ||
|
|
||
| const TIMER_LABEL = 'Detection time:'; | ||
|
|
||
| export type DetectorContext = { | ||
| options: IOptions; | ||
| store: IStore<IMapFrame>; | ||
| statistic: Statistic; | ||
| tokenizer: Tokenizer; | ||
| detector: InFilesDetector; | ||
| files: EntryWithContent[]; | ||
| }; | ||
|
|
||
| export function createBaseDetectorContext( | ||
| opts: Partial<IOptions>, | ||
| providedStore?: IStore<IMapFrame> | ||
| ): DetectorContext { | ||
| const options = {...getDefaultOptions(), ...opts}; | ||
|
|
||
| if (!options.format) { | ||
| options.format = getSupportedFormats(); | ||
| } | ||
|
|
||
| if (!options.hashFunction) { | ||
| options.hashFunction = (value: string) => createHash('md5').update(value).digest('hex'); | ||
| } | ||
|
|
||
| const store = providedStore || getStore(options.store); | ||
| const files = getFilesToDetect(options as IOptions); | ||
| const statistic = new Statistic(); | ||
| const tokenizer = new Tokenizer(); | ||
| const detector = new InFilesDetector(tokenizer, store, statistic, options as IOptions); | ||
|
|
||
| return {options: options as IOptions, store, statistic, tokenizer, detector, files}; | ||
| } | ||
|
|
||
| export async function detectClones( | ||
| opts: IOptions, | ||
| store?: IStore<IMapFrame> | ||
| ): Promise<IClone[]> { | ||
| const context = createBaseDetectorContext(opts, store); | ||
|
|
||
| registerReporters(context.options, context.detector); | ||
| registerSubscribers(context.options, context.detector); | ||
| registerHooks(context.options, context.detector); | ||
|
|
||
| if (context.options.silent) { | ||
| return context.detector.detect(context.files); | ||
| } | ||
|
|
||
| console.time(italic(grey(TIMER_LABEL))); | ||
| const clones = await context.detector.detect(context.files); | ||
| console.timeEnd(italic(grey(TIMER_LABEL))); | ||
|
|
||
| return clones; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import { IOptions } from "@jscpd/core"; | ||
| import { Command } from "commander"; | ||
| import { | ||
| initOptionsFromCli, | ||
| readPackageJson, | ||
| createBaseCommand, | ||
| addCommonOptions, | ||
| getWorkingDirectory, | ||
| } from "./setup"; | ||
| import type { JscpdServer } from "./server/server"; | ||
|
|
||
| function initServerCli(packageJson: any, argv: string[]): Command { | ||
| const cli = createBaseCommand(packageJson); | ||
|
|
||
| cli | ||
| .usage("[options] <path>") | ||
| .description("Start jscpd as a server") | ||
| .helpOption("--help", "display help for command") | ||
| .option( | ||
| "-p, --port [number]", | ||
| "port to run the server on (Default is 3000)", | ||
| ) | ||
| .option( | ||
| "-H, --host [string]", | ||
| "host to bind the server to (Default is 0.0.0.0)", | ||
| ); | ||
|
|
||
| addCommonOptions(cli); | ||
|
|
||
| cli.parse(argv); | ||
|
|
||
| return cli as Command; | ||
| } | ||
|
|
||
| export async function runServer( | ||
| argv: string[], | ||
| exitCallback?: (code: number) => void, | ||
| ): Promise<JscpdServer | null> { | ||
| const packageJson = readPackageJson(); | ||
|
|
||
| const cli = initServerCli(packageJson, argv); | ||
| const options: IOptions = initOptionsFromCli(cli); | ||
|
|
||
| const serverOpts = cli.opts(); | ||
| const workingDirectory = getWorkingDirectory(cli); | ||
|
|
||
| try { | ||
| const { startServer } = await import("./server"); | ||
| const port = serverOpts.port ? parseInt(serverOpts.port, 10) : undefined; | ||
| if (port !== undefined && (isNaN(port) || port < 1 || port > 65535)) { | ||
| throw new Error(`Invalid port number: ${serverOpts.port}`); | ||
| } | ||
|
|
||
| const server = await startServer(workingDirectory, { | ||
| port, | ||
| host: serverOpts.host, | ||
| jscpdOptions: options, | ||
| }); | ||
|
|
||
| return server; | ||
| } catch (error) { | ||
| console.error("Failed to start server:", error); | ||
| exitCallback?.(1); | ||
| return null; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import { readPackageJson } from "../setup"; | ||
|
|
||
| const packageJson = readPackageJson(); | ||
|
|
||
| export const SERVER_DEFAULTS = { | ||
| PORT: 3000, | ||
| HOST: "0.0.0.0", | ||
| BODY_SIZE_LIMIT: "10mb", | ||
| } as const; | ||
|
|
||
| export const ERROR_MESSAGES = { | ||
| SCAN_IN_PROGRESS: "Please wait for initial scan to complete", | ||
| NOT_INITIALIZED: | ||
| "Server not initialized. Please wait for initial scan to complete.", | ||
| SOURCE_STORE_NOT_INITIALIZED: "Source store not initialized", | ||
| EMPTY_CODE: "Code snippet cannot be empty", | ||
| MISSING_REQUIRED_FIELD: (field: string) => `Missing required field: ${field}`, | ||
| INVALID_FIELD_TYPE: (field: string, expectedType: string) => | ||
| `Field "${field}" must be a ${expectedType}`, | ||
| FIELD_CANNOT_BE_EMPTY: (field: string) => `Field "${field}" cannot be empty`, | ||
| } as const; | ||
|
|
||
| export const API_INFO = { | ||
| NAME: "jscpd-server", | ||
| VERSION: packageJson.version, | ||
| DOCUMENTATION_URL: "https://github.com/kucherenko/jscpd", | ||
| } as const; | ||
|
|
||
| export const HTTP_STATUS = { | ||
| OK: 200, | ||
| BAD_REQUEST: 400, | ||
| NOT_FOUND: 404, | ||
| INTERNAL_SERVER_ERROR: 500, | ||
| SERVICE_UNAVAILABLE: 503, | ||
| } as const; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import { IMapFrame, IStore } from '@jscpd/core'; | ||
|
|
||
| /** | ||
| * A hybrid store that delegates reads to a source store (for project data) | ||
| * and writes to an ephemeral store (for snippet data), ensuring snippet | ||
| * tokens don't contaminate the shared project store and are automatically | ||
| * discarded after detection. | ||
| */ | ||
| export class EphemeralHybridStore implements IStore<IMapFrame> { | ||
| constructor( | ||
| private readonly sourceStore: IStore<IMapFrame>, | ||
| private readonly ephemeralStore: IStore<IMapFrame> | ||
| ) {} | ||
|
|
||
| namespace(name: string): void { | ||
| this.sourceStore.namespace(name); | ||
| this.ephemeralStore.namespace(name); | ||
| } | ||
|
|
||
| async get(key: string): Promise<IMapFrame> { | ||
| try { | ||
| return await this.ephemeralStore.get(key); | ||
| } catch { | ||
| return this.sourceStore.get(key); | ||
| } | ||
| } | ||
|
Comment on lines
+20
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Overly broad error handling masks legitimate errors. The empty catch block on Line 23 swallows all errors from 🛡️ Proposed fix to handle only "not found" errorsIf 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 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);
}
|
||
|
|
||
| async set(key: string, value: IMapFrame): Promise<IMapFrame> { | ||
| return this.ephemeralStore.set(key, value); | ||
| } | ||
|
|
||
| close(): void { | ||
| this.ephemeralStore.close(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Fix it with Roo Code or mention @roomote and request a fix. |
||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| export * from './server'; | ||
| export * from './service'; | ||
| export * from './types'; | ||
| export * from './routes'; | ||
| export * from './middleware'; | ||
| export * from './constants'; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,89 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Request, Response, NextFunction } from 'express'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { ErrorResponse } from './types'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { ERROR_MESSAGES, HTTP_STATUS } from './constants'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface FieldValidation { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'string' | 'number' | 'boolean'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| required: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| allowEmpty?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function sendValidationError(res: Response, message: string): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const error: ErrorResponse = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: 'ValidationError', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| statusCode: HTTP_STATUS.BAD_REQUEST, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.status(HTTP_STATUS.BAD_REQUEST).json(error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function validateField( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value: unknown, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| validation: FieldValidation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): string | null { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (validation.required && (value === undefined || value === null)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ERROR_MESSAGES.MISSING_REQUIRED_FIELD(validation.name); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (value !== undefined && value !== null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof value !== validation.type) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ERROR_MESSAGES.INVALID_FIELD_TYPE(validation.name, validation.type); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (validation.type === 'string' && !validation.allowEmpty) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ((value as string).trim().length === 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ERROR_MESSAGES.FIELD_CANNOT_BE_EMPTY(validation.name); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function validateCheckRequest( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| req: Request, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res: Response, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| next: NextFunction | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const validations: FieldValidation[] = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { name: 'code', type: 'string', required: true, allowEmpty: false }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { name: 'format', type: 'string', required: true, allowEmpty: false }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const validation of validations) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const error = validateField(req.body[validation.name], validation); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sendValidationError(res, error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| next(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+65
to
+80
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function notFoundHandler(req: Request, res: Response): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const error: ErrorResponse = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: 'NotFound', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message: `Route ${req.method} ${req.path} not found`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| statusCode: HTTP_STATUS.NOT_FOUND, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.status(HTTP_STATUS.NOT_FOUND).json(error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Module load failure if
package.jsonis 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;🤖 Prompt for AI Agents