diff --git a/package.json b/package.json index 451ccd9..4145667 100644 --- a/package.json +++ b/package.json @@ -31,10 +31,11 @@ "lint": "eslint --config .configs/eslint.config.mjs .", "format": "prettier --write . --config .configs/.prettierrc", "format:check": "prettier --check . --config .configs/.prettierrc", - "test": "vitest run --config .configs/vitest.config.mjs", + "test": "vitest run --config .configs/vitest.config.mjs && npm run test:cjs", + "test:cjs": "npm run build && node --test tests/cjs/entry.test.cjs", "test:watch": "vitest --config .configs/vitest.config.mjs", - "test:coverage": "vitest run --coverage --config .configs/vitest.config.mjs", - "ci:coverage": "vitest run --coverage --reporter=dot --maxWorkers=1 --config .configs/vitest.config.mjs", + "test:coverage": "vitest run --coverage --config .configs/vitest.config.mjs && npm run test:cjs", + "ci:coverage": "vitest run --coverage --reporter=dot --maxWorkers=1 --config .configs/vitest.config.mjs && npm run test:cjs", "types:build": "tsc -p .configs/tsconfig.json", "types:check": "tsc -p .configs/tsconfig.json --noEmit", "build": "tsup", diff --git a/src/cjs-shim.cjs b/src/cjs-shim.cjs index bf3b04a..274890a 100644 --- a/src/cjs-shim.cjs +++ b/src/cjs-shim.cjs @@ -7,7 +7,7 @@ * @Email: * ----- * @Last modified by: Nate Corcoran (Shinrai@users.noreply.github.com) - * @Last modified time: 2026-10-02T15:35:18-07:00 (1790980518) + * @Last modified time: 2026-10-03T10:36:56-07:00 (1791049016) * ----- * @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. * @@ -21,7 +21,14 @@ * side) as long as the target module doesn't itself use top-level await — * `src/runner.mjs` doesn't. This mirrors @cldmv/uuid's index.cjs pattern * instead of tsup bundling a second, independent copy of the whole module - * for the CJS format. + * for the CJS format. In a .cjs file `require` already exists, so a plain + * `require("./index.mjs")` is used — `createRequire` is unnecessary here and + * breaks bundling by tools (esbuild/webpack) that need a static `require()` + * call to detect the dependency. + * + * Node.js versions without require(esm) (before 20.19 / 22.12) would fail with + * a bare ERR_REQUIRE_ESM, so the check below fails early with a message that + * says what to do instead. * * This file is copied verbatim into dist/index.cjs by tsup's onSuccess hook * (see tsup.config.mjs) — it never passes through esbuild itself, so it @@ -30,7 +37,13 @@ * @module @cldmv/vitest-runner */ "use strict"; -const { createRequire } = require("node:module"); -const requireESM = createRequire(__filename); -module.exports = requireESM("./index.mjs"); +if (!process.features?.require_module) { + const error = new Error( + `@cldmv/vitest-runner: require() needs Node.js ^20.19.0 or >=22.12.0 (this is ${process.version}). On older Node.js, load the package with import() instead.` + ); + error.code = "ERR_REQUIRE_ESM"; + throw error; +} + +module.exports = require("./index.mjs"); diff --git a/tests/cjs/entry.test.cjs b/tests/cjs/entry.test.cjs new file mode 100644 index 0000000..26f768c --- /dev/null +++ b/tests/cjs/entry.test.cjs @@ -0,0 +1,54 @@ +/** + * + * @Project: @cldmv/vitest-runner + * @Filename: /tests/cjs/entry.test.cjs + * @Date: 2026-10-03T10:33:01-07:00 (1791048781) + * @Author: Nate Corcoran + * @Email: + * ----- + * @Last modified by: Nate Corcoran (Shinrai@users.noreply.github.com) + * @Last modified time: 2026-10-03T10:34:18-07:00 (1791048858) + * ----- + * @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. + * + */ + +/** + * CommonJS entry tests. These run under Node's own test runner (`node --test`), not Vitest: + * Vitest loads files through its own module runner, so it cannot show whether a plain + * `require()` of the package works the way it does for a CommonJS consumer. + */ +"use strict"; + +const { test } = require("node:test"); +const assert = require("node:assert/strict"); +const { spawnSync } = require("node:child_process"); +const path = require("node:path"); + +const repoRoot = path.resolve(__dirname, "../.."); + +test("require() returns the same named exports as import", async () => { + const required = require("../../dist/index.cjs"); + const esm = await import("../../dist/index.mjs"); + + assert.equal(typeof required.run, "function"); + assert.equal(required.run, esm.run); + assert.equal(required.resolveBin, esm.resolveBin); + assert.equal(required.discoverVitestFiles, esm.discoverVitestFiles); + assert.equal(required.formatDuration, esm.formatDuration); + assert.deepEqual(Object.keys(required).sort(), Object.keys(esm).sort()); +}); + +test("require() fails with a clear message where Node.js has no require(esm)", () => { + // --no-experimental-require-module turns require(esm) off, which is what Node.js + // versions before 20.19 / 22.12 look like to the entry. + const res = spawnSync(process.execPath, ["--no-experimental-require-module", "-e", "require('./dist/index.cjs')"], { + cwd: repoRoot, + encoding: "utf8" + }); + + assert.notEqual(res.status, 0); + assert.match(res.stderr, /ERR_REQUIRE_ESM/); + assert.match(res.stderr, /require\(\) needs Node\.js \^20\.19\.0 or >=22\.12\.0/); + assert.match(res.stderr, /import\(\)/); +});