From 4a6b7d64103427eab15d76416d7c12ab1505ecbd Mon Sep 17 00:00:00 2001 From: Shinrai Date: Sat, 3 Oct 2026 10:24:25 -0700 Subject: [PATCH 1/2] fix(cjs): load the CommonJS entry without top-level await index.cjs loads index.mjs through Node's synchronous require(esm), and index.mjs used top-level await for the devcheck and the library import, so every require("@cldmv/uuid") failed with ERR_REQUIRE_ASYNC_MODULE. - index.mjs: devcheck runs inside an async function (it is never published, so a failed import is ignored) and the library is a static import, so the module graph has no top-level await. - index.cjs: a plain require of index.mjs. Where Node.js has no require(esm) (before 20.19 / 22.12) it throws ERR_REQUIRE_ESM with a message pointing to import() instead of a bare loader error. - tests/cjs: node:test checks run by `npm test` and `npm run coverage` after Vitest: require() returns the same objects as import, and the version check fires when require(esm) is off. Fixes #49 --- index.cjs | 16 +++++++++--- index.mjs | 20 +++++++++------ package.json | 5 ++-- tests/cjs/entry.test.cjs | 54 ++++++++++++++++++++++++++++++++++++++++ types/index.d.mts | 16 +++--------- types/index.d.mts.map | 2 +- 6 files changed, 87 insertions(+), 26 deletions(-) create mode 100644 tests/cjs/entry.test.cjs diff --git a/index.cjs b/index.cjs index 4fc2d64..a268096 100644 --- a/index.cjs +++ b/index.cjs @@ -21,10 +21,20 @@ * * @module uuid */ -const { createRequire } = require("module"); -const requireESM = createRequire(__filename); +"use strict"; -const { UUID, uuid, ISSUER_CATEGORIES } = requireESM("./index.mjs"); +// 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/uuid: 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 { UUID, uuid, ISSUER_CATEGORIES } = require("./index.mjs"); // Export UUID as default module.exports = UUID; diff --git a/index.mjs b/index.mjs index 1214d6c..4c8f455 100644 --- a/index.mjs +++ b/index.mjs @@ -13,19 +13,25 @@ * */ -// Development environment check (must happen before UUID imports) -try { - await import("./devcheck.mjs"); -} catch { - // ignore -} +// Development environment check. devcheck.mjs exists only in a source checkout; it is +// never published, so the import is allowed to fail. It runs inside an async function +// rather than as a top-level await: index.cjs loads this file through Node's synchronous +// require(esm), which rejects any module graph containing top-level await +// (ERR_REQUIRE_ASYNC_MODULE). +(async () => { + try { + await import("./devcheck.mjs"); + } catch { + // ignore - devcheck.mjs is not published + } +})(); /** * ESM entry point for UUID * * Re-exports all components from the main UUID module */ -const { UUID, ISSUER_CATEGORIES } = await import("@cldmv/uuid/main"); +import { UUID, ISSUER_CATEGORIES } from "@cldmv/uuid/main"; export { UUID, UUID as uuid, ISSUER_CATEGORIES }; export default UUID; diff --git a/package.json b/package.json index 7fb92f4..4a021c6 100644 --- a/package.json +++ b/package.json @@ -53,9 +53,10 @@ "build:dist": "node -e \"const fs = require('fs'); const path = require('path'); fs.rmSync('dist', {recursive: true, force: true}); fs.mkdirSync('dist', {recursive: true}); fs.cpSync('src', 'dist', {recursive: true});\"", "build:types": "node -e \"require('fs').rmSync('types', {recursive: true, force: true})\" && tsc --project .configs/tsconfig.dts.jsonc", "demo": "node scripts/demo-custom-uuids.mjs", - "test": "node tests/run-vitest.mjs", + "test": "node tests/run-vitest.mjs && npm run test:cjs", + "test:cjs": "node --conditions=uuid-dev --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", "fix:headers": "fix-headers --config .configs/fix-headers.json", "types:build": "tsc -p .configs/tsconfig.dts.jsonc --noCheck", diff --git a/tests/cjs/entry.test.cjs b/tests/cjs/entry.test.cjs new file mode 100644 index 0000000..1c25a39 --- /dev/null +++ b/tests/cjs/entry.test.cjs @@ -0,0 +1,54 @@ +/** + * + * @Project: @cldmv/uuid + * @Filename: /tests/cjs/entry.test.cjs + * @Date: 2026-10-03T10:18:01-07:00 (1791047881) + * @Author: Nate Corcoran + * @Email: + * ----- + * @Last modified by: Nate Corcoran (Shinrai@users.noreply.github.com) + * @Last modified time: 2026-10-03T10:24:06-07:00 (1791048246) + * ----- + * @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, "../.."); +const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; + +test("require() returns the same UUID object as import", async () => { + const UUID = require("../../index.cjs"); + const esm = await import("../../index.mjs"); + + assert.equal(UUID, esm.default); + assert.equal(UUID.UUID, esm.UUID); + assert.equal(UUID.uuid, esm.uuid); + assert.equal(UUID.ISSUER_CATEGORIES, esm.ISSUER_CATEGORIES); + assert.match(String(UUID.v4()), UUID_PATTERN); +}); + +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\(\)/); +}); diff --git a/types/index.d.mts b/types/index.d.mts index 5189d85..808e242 100644 --- a/types/index.d.mts +++ b/types/index.d.mts @@ -1,15 +1,5 @@ export default UUID; -export const UUID: typeof import("@cldmv/uuid/main").UUID; -export const ISSUER_CATEGORIES: { - UNASSIGNED: number; - DRAFTER_RESERVED: number; - CATEGORY_A_START: number; - CATEGORY_A_END: number; - CATEGORY_B_START: number; - CATEGORY_B_END: number; - SPEC_ORIGINATOR: number; - RFC_EXPANSION_START: number; - RFC_EXPANSION_END: number; -}; -export { UUID as uuid }; +import { UUID } from "@cldmv/uuid/main"; +import { ISSUER_CATEGORIES } from "@cldmv/uuid/main"; +export { UUID, UUID as uuid, ISSUER_CATEGORIES }; //# sourceMappingURL=index.d.mts.map \ No newline at end of file diff --git a/types/index.d.mts.map b/types/index.d.mts.map index 6d2496d..842020d 100644 --- a/types/index.d.mts.map +++ b/types/index.d.mts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../index.mjs"],"names":[],"mappings":""} \ No newline at end of file +{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../index.mjs"],"names":[],"mappings":";qBAiCwC,kBAAkB;kCAAlB,kBAAkB"} \ No newline at end of file From 646c97fa10a6a43d9b66e334f979517df63320cd Mon Sep 17 00:00:00 2001 From: Shinrai Date: Sat, 3 Oct 2026 11:14:12 -0700 Subject: [PATCH 2/2] fix(types): export the default from the named export list tsc emitted `export default UUID;` above `import { UUID }` in types/index.d.mts, which CodeQL flags as js/use-before-declaration. A single export list with `UUID as default` generates the import first. --- index.mjs | 3 +-- types/index.d.mts | 3 +-- types/index.d.mts.map | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/index.mjs b/index.mjs index 4c8f455..e3a333f 100644 --- a/index.mjs +++ b/index.mjs @@ -33,5 +33,4 @@ */ import { UUID, ISSUER_CATEGORIES } from "@cldmv/uuid/main"; -export { UUID, UUID as uuid, ISSUER_CATEGORIES }; -export default UUID; +export { UUID as default, UUID, UUID as uuid, ISSUER_CATEGORIES }; diff --git a/types/index.d.mts b/types/index.d.mts index 808e242..e778498 100644 --- a/types/index.d.mts +++ b/types/index.d.mts @@ -1,5 +1,4 @@ -export default UUID; import { UUID } from "@cldmv/uuid/main"; import { ISSUER_CATEGORIES } from "@cldmv/uuid/main"; -export { UUID, UUID as uuid, ISSUER_CATEGORIES }; +export { UUID as default, UUID, UUID as uuid, ISSUER_CATEGORIES }; //# sourceMappingURL=index.d.mts.map \ No newline at end of file diff --git a/types/index.d.mts.map b/types/index.d.mts.map index 842020d..fa661d6 100644 --- a/types/index.d.mts.map +++ b/types/index.d.mts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../index.mjs"],"names":[],"mappings":";qBAiCwC,kBAAkB;kCAAlB,kBAAkB"} \ No newline at end of file +{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../index.mjs"],"names":[],"mappings":"qBAiCwC,kBAAkB;kCAAlB,kBAAkB"} \ No newline at end of file