Skip to content

[REVIEW] CLI: getVersion() uses require() in ESM module — --version and --help always display 0.0.0 #24

@BodenMcHale

Description

@BodenMcHale

Summary

getVersion() in src/cli.ts calls require('../package.json') to read the package version, but the project compiles to ES modules ("type": "module" in package.json, "module": "NodeNext" in tsconfig.json). In ESM, require is undefined at runtime, so the try block throws silently and the catch always returns the hard-coded fallback '0.0.0'. Every user who runs security-headers --version or security-headers --help sees 0.0.0 instead of the actual published version (1.0.1).

Investigation Path

  1. Read src/cli.ts in full — noticed getVersion() at lines 20–26 uses require('../package.json') inside a try/catch that swallows all errors.
  2. Read package.json — confirmed "type": "module" (line 4), making all compiled .js output ES modules.
  3. Read tsconfig.json — confirmed "module": "NodeNext" (line 4), which outputs native ESM for .js files when "type": "module" is present in package.json.
  4. Verified require is unavailable in ESM context: node --input-type=module -e "console.log(typeof require)"undefined.
  5. Built the project (npm install && npm run build) and ran the compiled CLI directly:
$ node dist/cli.js --version
0.0.0
  1. Inspected dist/cli.js — confirmed require('../package.json') is emitted verbatim into the ESM output, where it throws ReferenceError: require is not defined and the catch block silently returns '0.0.0'.
  2. Searched all open and recently-closed issues for "require", "ESM", "version 0.0.0", "getVersion", and "cli --version" — no existing issue found.

Evidence

  • File: src/cli.ts lines 20–26, commit 81f8735d167b597ed7a7a88cc7f89ba0b440b07d
function getVersion(): string {
  try {
    const pkg = require('../package.json');  // ← require is undefined in ESM
    return pkg.version;
  } catch {
    return '0.0.0';  // ← always reached at runtime
  }
}
  • File: package.json line 4 — "type": "module"
  • File: tsconfig.json line 4 — "module": "NodeNext"

Current behavior: node dist/cli.js --version prints 0.0.0

Expected behavior: node dist/cli.js --version prints 1.0.1 (the version from package.json)

Reference: Node.js ESM documentation — require: "require, exports, module.exports, __filename, __dirname — These CommonJS variables are not available in ES modules."

Impact

  1. Every CLI user is affected. Anyone who runs security-headers --version — to check their installed version, to include in a bug report, or to verify a CI pipeline is using the expected release — sees 0.0.0.
  2. --help also shows 0.0.0. The version banner in printHelp() calls getVersion(), so the help output also displays the wrong version.
  3. Audit trails are broken. Security tooling is often invoked in CI with version-pinning checks. A tool that cannot report its own version undermines the reproducibility guarantee that version checks are meant to provide.
  4. The try/catch masks the error silently. There is no warning, no log, and no non-zero exit — the user has no indication that the version being printed is wrong.

Suggested Remediation

Replace require() with a Node.js ESM-compatible approach. Two options:

Option A — module.createRequire (minimal diff):

import { createRequire } from 'node:module';
const _require = createRequire(import.meta.url);

function getVersion(): string {
  try {
    const pkg = _require('../package.json') as { version: string };
    return pkg.version;
  } catch {
    return '0.0.0';
  }
}

Option B — readFileSync + JSON.parse (no implicit CJS shim):

import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { join, dirname } from 'node:path';

function getVersion(): string {
  try {
    const __dirname = dirname(fileURLToPath(import.meta.url));
    const raw = readFileSync(join(__dirname, '../package.json'), 'utf8');
    return (JSON.parse(raw) as { version: string }).version;
  } catch {
    return '0.0.0';
  }
}

Option C — hardcode via a generated constant (cleanest for published packages):

Create src/version.ts containing export const VERSION = '1.0.1';, add a build:version script that writes this file from package.json before tsc, and import it in cli.ts. This avoids any runtime file I/O and cannot fail.

Option A is the smallest change; Option B avoids the CJS require bridge entirely; Option C is the most robust but requires a build-step addition.

Acceptance Criteria

  • node dist/cli.js --version prints the version from package.json (e.g. 1.0.1), not 0.0.0
  • node dist/cli.js --help displays the correct version in the banner line
  • The fix does not use require() as a bare global (which is unavailable in ESM)
  • npm run build succeeds with no new TypeScript errors
  • npm test continues to exit 0 (no tests broken by the change)

Claude Code review routine · commit 81f8735d167b597ed7a7a88cc7f89ba0b440b07d · 2026-05-26T11:15:00Z

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinggood first issueGood for newcomers

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions