From e5b0a75efbcd9fa91b40906997eb381996520517 Mon Sep 17 00:00:00 2001 From: Shinrai Date: Sat, 3 Oct 2026 10:39:16 -0700 Subject: [PATCH 1/3] fix(cjs): require the ESM entry directly and fail clearly on Node without require(esm) index.cjs used createRequire(__filename) to synchronously load index.mjs. That idiom predates Node's native require(esm) and breaks bundlers (esbuild, webpack) that don't follow a createRequire-constructed require the way they follow a literal require() call. - index.cjs: a plain require("./index.mjs") instead of createRequire. Where Node.js has no require(esm) (before 20.19 / 22.12) it now throws ERR_REQUIRE_ESM with a message pointing to import() instead of a bare loader error. - index.mjs already had no top-level await, so no change was needed there. - tests/cjs: node:test checks run by `npm test` and `npm run coverage` after Vitest: require() returns the same objects as import (type/identity only - createRemote opens a real ADB connection, so no call is made), and the version check fires when require(esm) is off. --- index.cjs | 15 ++++++++--- package.json | 5 ++-- tests/cjs/entry.test.cjs | 54 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 tests/cjs/entry.test.cjs diff --git a/index.cjs b/index.cjs index 59736d6..44f0d3f 100644 --- a/index.cjs +++ b/index.cjs @@ -21,11 +21,20 @@ * * @module @cldmv/node-android-tv-remote/cjs */ +"use strict"; -const { createRequire } = require("module"); -const requireESM = createRequire(__filename); +// index.cjs is a thin wrapper: it loads index.mjs through Node's synchronous require(esm). +// Node.js versions without require(esm) would fail with a bare ERR_REQUIRE_ESM, so fail +// early with a message that says what to do instead. +if (!process.features?.require_module) { + const error = new Error( + `@cldmv/node-android-tv-remote: 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; +} -const { default: createRemote } = requireESM("./index.mjs"); +const { default: createRemote } = require("./index.mjs"); module.exports = createRemote; module.exports.createRemote = createRemote; diff --git a/package.json b/package.json index 4a2e36f..74bc92f 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,10 @@ "setup-device": "node scripts/setup-device.mjs", "build": "echo '✓ no build step'", "build:ci": "echo '✓ no build step'", - "test": "node tests/run-vitest.mjs", + "test": "node tests/run-vitest.mjs && npm run test:cjs", + "test:cjs": "node --test tests/cjs/entry.test.cjs", "test:watch": "vitest --config .configs/vitest.config.mjs", - "coverage": "node tests/run-vitest.mjs --coverage-quiet", + "coverage": "node tests/run-vitest.mjs --coverage-quiet && npm run test:cjs", "ci:coverage": "npm run coverage", "lint": "eslint --config eslint.config.mjs .", "lint:fix": "eslint --config eslint.config.mjs . --fix", diff --git a/tests/cjs/entry.test.cjs b/tests/cjs/entry.test.cjs new file mode 100644 index 0000000..164d58b --- /dev/null +++ b/tests/cjs/entry.test.cjs @@ -0,0 +1,54 @@ +/** + * + * @Project: @cldmv/node-android-tv-remote + * @Filename: /tests/cjs/entry.test.cjs + * @Date: 2026-10-03T00:00:00-07:00 (1791010800) + * @Author: Nate Corcoran + * @Email: + * ----- + * @Last modified by: Nate Corcoran (Shinrai@users.noreply.github.com) + * @Last modified time: 2026-10-03T10:37:08-07:00 (1791049028) + * ----- + * @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 createRemote object as import", async () => { + const createRemote = require("../../index.cjs"); + const esm = await import("../../index.mjs"); + + // createRemote opens an ADB connection when called, so only identity/type is + // checked here - no sockets are opened in this test. + assert.equal(typeof createRemote, "function"); + assert.equal(createRemote, esm.default); + assert.equal(createRemote.createRemote, esm.default); + assert.equal(createRemote.default, esm.default); +}); + +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('./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\(\)/); +}); From 26d33560e332e6860a3f9d0a6178199907c26025 Mon Sep 17 00:00:00 2001 From: Shinrai Date: Sat, 3 Oct 2026 11:25:22 -0700 Subject: [PATCH 2/3] fix(cjs): re-export createAndroidTVRemote from the CommonJS entry index.mjs exports createAndroidTVRemote, but index.cjs only exposed the default (createRemote), so require() callers could not reach it. The CJS entry now carries every export the ESM entry has, and the CJS test checks it. --- index.cjs | 3 ++- tests/cjs/entry.test.cjs | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/index.cjs b/index.cjs index 44f0d3f..cfa2ff5 100644 --- a/index.cjs +++ b/index.cjs @@ -34,8 +34,9 @@ if (!process.features?.require_module) { throw error; } -const { default: createRemote } = require("./index.mjs"); +const { default: createRemote, createAndroidTVRemote } = require("./index.mjs"); module.exports = createRemote; module.exports.createRemote = createRemote; +module.exports.createAndroidTVRemote = createAndroidTVRemote; module.exports.default = createRemote; diff --git a/tests/cjs/entry.test.cjs b/tests/cjs/entry.test.cjs index 164d58b..35ac9e5 100644 --- a/tests/cjs/entry.test.cjs +++ b/tests/cjs/entry.test.cjs @@ -37,6 +37,8 @@ test("require() returns the same createRemote object as import", async () => { assert.equal(createRemote, esm.default); assert.equal(createRemote.createRemote, esm.default); assert.equal(createRemote.default, esm.default); + assert.equal(typeof createRemote.createAndroidTVRemote, "function"); + assert.equal(createRemote.createAndroidTVRemote, esm.createAndroidTVRemote); }); test("require() fails with a clear message where Node.js has no require(esm)", () => { From a1bf622d1352f8170bd0d99511953cb89aa418df Mon Sep 17 00:00:00 2001 From: "cldmv-bot[bot]" <230771808+cldmv-bot[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 18:35:31 +0000 Subject: [PATCH 3/3] chore: bump version to 2.1.7 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6933b2a..7bfd033 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cldmv/node-android-tv-remote", - "version": "2.1.6", + "version": "2.1.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@cldmv/node-android-tv-remote", - "version": "2.1.6", + "version": "2.1.7", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 74bc92f..061f8a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cldmv/node-android-tv-remote", - "version": "2.1.6", + "version": "2.1.7", "description": "A Node.js library for controlling Android TV devices via ADB keycodes and text input.", "type": "module", "exports": {