From 13c83491641f8d068e18a27b551708e7c9ce15a9 Mon Sep 17 00:00:00 2001 From: Hamed Mohamed Date: Wed, 3 Jun 2026 18:13:41 +0300 Subject: [PATCH 1/3] fix(cli): add early Node.js version check wrapper This adds a CJS wrapper to cleanly validate the Node.js version against engines.node before executing the ESM entry point. Resolves #763. --- bin/check-and-run.cjs | 24 ++++++++++++++++++ package.json | 4 +-- test/commands/bin.test.js | 51 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 bin/check-and-run.cjs create mode 100644 test/commands/bin.test.js diff --git a/bin/check-and-run.cjs b/bin/check-and-run.cjs new file mode 100644 index 00000000..4214ff4c --- /dev/null +++ b/bin/check-and-run.cjs @@ -0,0 +1,24 @@ +#!/usr/bin/env node + +"use strict"; + +const { createRequire } = require("node:module"); +const localRequire = createRequire(__filename); + +const manifest = localRequire("../package.json"); +const semver = localRequire("semver"); + +const currentVersion = process.versions.node; +const requiredRange = manifest.engines.node; + +if (!semver.satisfies(currentVersion, requiredRange)) { + console.error( + `\n @nodesecure/cli requires Node.js ${requiredRange}.` + + `\n Current version: v${currentVersion}\n` + ); + process.exit(1); +} + +if (require.main === module) { + import("./index.js"); +} diff --git a/package.json b/package.json index 75220e5e..06d658eb 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "Node.js security CLI", "main": "./bin/index.js", "bin": { - "node-secure": "./bin/index.js", - "nsecure": "./bin/index.js" + "node-secure": "./bin/check-and-run.cjs", + "nsecure": "./bin/check-and-run.cjs" }, "type": "module", "engines": { diff --git a/test/commands/bin.test.js b/test/commands/bin.test.js new file mode 100644 index 00000000..2a990ebc --- /dev/null +++ b/test/commands/bin.test.js @@ -0,0 +1,51 @@ +// Import Node.js Dependencies +import assert from "node:assert"; +import { createRequire } from "node:module"; +import { describe, it, mock, afterEach } from "node:test"; + +const require = createRequire(import.meta.url); + +describe("bin/check-and-run.cjs", () => { + afterEach(() => { + mock.restoreAll(); + // Clear the require cache so the file executes again for each test + delete require.cache[require.resolve("../../bin/check-and-run.cjs")]; + }); + + it("should exit with code 1 if semver.satisfies returns false", (ctx) => { + const logs = []; + ctx.mock.method(console, "error", (msg) => logs.push(msg)); + + ctx.mock.method(process, "exit", (code) => { + throw new Error(`process.exit(${code}) called`); + }); + + // Mock semver to simulate a failed version check + const semver = require("semver"); + ctx.mock.method(semver, "satisfies", () => false); + + assert.throws( + () => require("../../bin/check-and-run.cjs"), + { message: "process.exit(1) called" }, + "should halt execution and exit with code 1" + ); + + const allLogs = logs.join(""); + assert.ok(allLogs.includes("@nodesecure/cli requires Node.js"), "should print required Node.js version message"); + }); + + it("should not exit if semver.satisfies returns true", (ctx) => { + let exitCode; + ctx.mock.method(process, "exit", (code) => { + exitCode = code; + }); + + // Mock semver to simulate a successful version check + const semver = require("semver"); + ctx.mock.method(semver, "satisfies", () => true); + + require("../../bin/check-and-run.cjs"); + + assert.strictEqual(exitCode, undefined, "should not call process.exit"); + }); +}); From d06b5e3d1d7e2657e4d627d0183a371d2523b9e6 Mon Sep 17 00:00:00 2001 From: Hamed Mohamed Date: Thu, 4 Jun 2026 09:44:11 +0300 Subject: [PATCH 2/3] fix: remove trailing space in bin.test.js line 32 --- test/commands/bin.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/commands/bin.test.js b/test/commands/bin.test.js index 2a990ebc..b0fae5f8 100644 --- a/test/commands/bin.test.js +++ b/test/commands/bin.test.js @@ -29,7 +29,7 @@ describe("bin/check-and-run.cjs", () => { { message: "process.exit(1) called" }, "should halt execution and exit with code 1" ); - + const allLogs = logs.join(""); assert.ok(allLogs.includes("@nodesecure/cli requires Node.js"), "should print required Node.js version message"); }); From bd1c7d8e0a50f1c38d306ed45c9ba086a1caebe2 Mon Sep 17 00:00:00 2001 From: Hamed Mohamed Date: Sat, 13 Jun 2026 19:41:22 +0300 Subject: [PATCH 3/3] refactor: convert check-and-run wrapper from CJS to ESM - Replace check-and-run.cjs with check-and-run.js (pure ESM) - Use import attributes ({ type: 'json' }) for package.json - Update bin entries in package.json - Rewrite tests to use fork() with subprocess --- bin/check-and-run.cjs | 24 ------------ bin/check-and-run.js | 20 ++++++++++ package.json | 4 +- test/commands/bin.test.js | 69 ++++++++++++++++------------------- test/process/check-and-run.js | 9 +++++ 5 files changed, 63 insertions(+), 63 deletions(-) delete mode 100644 bin/check-and-run.cjs create mode 100644 bin/check-and-run.js create mode 100644 test/process/check-and-run.js diff --git a/bin/check-and-run.cjs b/bin/check-and-run.cjs deleted file mode 100644 index 4214ff4c..00000000 --- a/bin/check-and-run.cjs +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env node - -"use strict"; - -const { createRequire } = require("node:module"); -const localRequire = createRequire(__filename); - -const manifest = localRequire("../package.json"); -const semver = localRequire("semver"); - -const currentVersion = process.versions.node; -const requiredRange = manifest.engines.node; - -if (!semver.satisfies(currentVersion, requiredRange)) { - console.error( - `\n @nodesecure/cli requires Node.js ${requiredRange}.` + - `\n Current version: v${currentVersion}\n` - ); - process.exit(1); -} - -if (require.main === module) { - import("./index.js"); -} diff --git a/bin/check-and-run.js b/bin/check-and-run.js new file mode 100644 index 00000000..52e9cdb9 --- /dev/null +++ b/bin/check-and-run.js @@ -0,0 +1,20 @@ +#!/usr/bin/env node + +import semver from "semver"; + +const { default: packageJSON } = await import("../package.json", { + with: { type: "json" } +}); + +const currentVersion = process.versions.node; +const requiredRange = packageJSON.engines.node; + +if (!semver.satisfies(currentVersion, requiredRange)) { + console.error( + `\n @nodesecure/cli requires Node.js ${requiredRange}.` + + `\n Current version: v${currentVersion}\n` + ); + process.exit(1); +} + +await import("./index.js"); diff --git a/package.json b/package.json index 06d658eb..daa2a534 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "Node.js security CLI", "main": "./bin/index.js", "bin": { - "node-secure": "./bin/check-and-run.cjs", - "nsecure": "./bin/check-and-run.cjs" + "node-secure": "./bin/check-and-run.js", + "nsecure": "./bin/check-and-run.js" }, "type": "module", "engines": { diff --git a/test/commands/bin.test.js b/test/commands/bin.test.js index b0fae5f8..ff39841a 100644 --- a/test/commands/bin.test.js +++ b/test/commands/bin.test.js @@ -1,51 +1,46 @@ // Import Node.js Dependencies import assert from "node:assert"; -import { createRequire } from "node:module"; -import { describe, it, mock, afterEach } from "node:test"; +import { fork } from "node:child_process"; +import path from "node:path"; +import { describe, it } from "node:test"; -const require = createRequire(import.meta.url); +// CONSTANTS +const kProcessPath = path.join(import.meta.dirname, "..", "process", "check-and-run.js"); -describe("bin/check-and-run.cjs", () => { - afterEach(() => { - mock.restoreAll(); - // Clear the require cache so the file executes again for each test - delete require.cache[require.resolve("../../bin/check-and-run.cjs")]; - }); - - it("should exit with code 1 if semver.satisfies returns false", (ctx) => { - const logs = []; - ctx.mock.method(console, "error", (msg) => logs.push(msg)); +describe("bin/check-and-run.js", () => { + it("should exit with code 1 and print a friendly message for an unsupported Node version", async() => { + const { exitCode, stderr } = await runSubprocess("0.10.0"); - ctx.mock.method(process, "exit", (code) => { - throw new Error(`process.exit(${code}) called`); - }); + assert.strictEqual(exitCode, 1, "should exit with code 1"); + assert.ok( + stderr.includes("@nodesecure/cli requires Node.js"), + "should print required Node.js version message" + ); + }); - // Mock semver to simulate a failed version check - const semver = require("semver"); - ctx.mock.method(semver, "satisfies", () => false); + it("should pass the version gate when Node version satisfies engines range", async() => { + const { stderr } = await runSubprocess("24.0.0"); - assert.throws( - () => require("../../bin/check-and-run.cjs"), - { message: "process.exit(1) called" }, - "should halt execution and exit with code 1" + assert.ok( + !stderr.includes("@nodesecure/cli requires Node.js"), + "should not print a version mismatch message" ); - - const allLogs = logs.join(""); - assert.ok(allLogs.includes("@nodesecure/cli requires Node.js"), "should print required Node.js version message"); }); +}); - it("should not exit if semver.satisfies returns true", (ctx) => { - let exitCode; - ctx.mock.method(process, "exit", (code) => { - exitCode = code; +function runSubprocess(version) { + return new Promise((resolve) => { + const child = fork(kProcessPath, [version], { + stdio: ["ignore", "pipe", "pipe", "ipc"] }); - // Mock semver to simulate a successful version check - const semver = require("semver"); - ctx.mock.method(semver, "satisfies", () => true); - - require("../../bin/check-and-run.cjs"); + let stderr = ""; + child.stderr.on("data", (chunk) => { + stderr += chunk; + }); - assert.strictEqual(exitCode, undefined, "should not call process.exit"); + child.on("close", (code) => { + resolve({ exitCode: code, stderr }); + }); }); -}); +} diff --git a/test/process/check-and-run.js b/test/process/check-and-run.js new file mode 100644 index 00000000..e3f8a9ac --- /dev/null +++ b/test/process/check-and-run.js @@ -0,0 +1,9 @@ +const fakeVersion = process.argv[2]; + +if (fakeVersion) { + Object.defineProperty(process, "versions", { + value: { ...process.versions, node: fakeVersion } + }); +} + +await import("../../bin/check-and-run.js");