From 0e4d91178562b840a514e2baee7d571c4d9c19c5 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Mon, 16 Mar 2026 05:26:27 -0600 Subject: [PATCH 1/7] fix: add CJS require() support to package exports (#455) Add `require` and `default` conditions to the exports field and a thin CJS wrapper (`src/index.cjs`) that delegates to ESM via dynamic import(). CJS consumers can now `await require('@optave/codegraph')`. Closes #455 --- package.json | 4 +++- src/index.cjs | 10 ++++++++++ tests/unit/index-exports.test.js | 12 ++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 src/index.cjs diff --git a/package.json b/package.json index fd29cee8..95c927a7 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "main": "src/index.js", "exports": { ".": { - "import": "./src/index.js" + "import": "./src/index.js", + "require": "./src/index.cjs", + "default": "./src/index.js" }, "./cli": { "import": "./src/cli.js" diff --git a/src/index.cjs b/src/index.cjs new file mode 100644 index 00000000..47649798 --- /dev/null +++ b/src/index.cjs @@ -0,0 +1,10 @@ +/** + * CJS compatibility wrapper — delegates to ESM via dynamic import(). + * + * Usage (async): + * const codegraph = await require('@optave/codegraph'); + * + * If you are on Node >= 22, synchronous require() of ESM may work + * automatically. On older versions, await the result. + */ +module.exports = import('./index.js'); diff --git a/tests/unit/index-exports.test.js b/tests/unit/index-exports.test.js index a5a912b7..424b4d99 100644 --- a/tests/unit/index-exports.test.js +++ b/tests/unit/index-exports.test.js @@ -1,3 +1,4 @@ +import { createRequire } from 'module'; import { describe, expect, it } from 'vitest'; describe('index.js re-exports', () => { @@ -9,4 +10,15 @@ describe('index.js re-exports', () => { expect(mod).toBeDefined(); expect(typeof mod).toBe('object'); }); + + it('CJS wrapper resolves to the same exports', async () => { + const require = createRequire(import.meta.url); + const cjs = await require('../../src/index.cjs'); + const esm = await import('../../src/index.js'); + // Every named ESM export should be present in the CJS wrapper result + for (const key of Object.keys(esm)) { + if (key === 'default') continue; + expect(cjs).toHaveProperty(key); + } + }); }); From 1970265e473789f935de330a4bd97b5fde462a26 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Mon, 16 Mar 2026 05:26:43 -0600 Subject: [PATCH 2/7] style: use node: protocol for module import in test --- tests/unit/index-exports.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/index-exports.test.js b/tests/unit/index-exports.test.js index 424b4d99..6972c2ef 100644 --- a/tests/unit/index-exports.test.js +++ b/tests/unit/index-exports.test.js @@ -1,4 +1,4 @@ -import { createRequire } from 'module'; +import { createRequire } from 'node:module'; import { describe, expect, it } from 'vitest'; describe('index.js re-exports', () => { From 39092fd7b1cc6044f053966d14b9c6b7fc78b8e6 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Mon, 16 Mar 2026 05:32:56 -0600 Subject: [PATCH 3/7] test: strengthen CJS export assertion to verify value identity --- tests/unit/index-exports.test.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/unit/index-exports.test.js b/tests/unit/index-exports.test.js index 6972c2ef..9b6c6dd1 100644 --- a/tests/unit/index-exports.test.js +++ b/tests/unit/index-exports.test.js @@ -15,10 +15,14 @@ describe('index.js re-exports', () => { const require = createRequire(import.meta.url); const cjs = await require('../../src/index.cjs'); const esm = await import('../../src/index.js'); - // Every named ESM export should be present in the CJS wrapper result + // Every named ESM export should resolve to a real value, not undefined. + // CJS import() produces a separate module namespace so reference equality + // (toBe) is not possible, but we verify the export exists, is defined, + // and has the same type as its ESM counterpart. for (const key of Object.keys(esm)) { if (key === 'default') continue; - expect(cjs).toHaveProperty(key); + expect(cjs[key], `CJS export "${key}" is missing or undefined`).toBeDefined(); + expect(typeof cjs[key]).toBe(typeof esm[key]); } }); }); From 483f0bfd88ca2aa4574838a249d854f10fdd7c43 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Mon, 16 Mar 2026 05:39:04 -0600 Subject: [PATCH 4/7] test: verify package.json exports map points to CJS wrapper --- tests/unit/index-exports.test.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/unit/index-exports.test.js b/tests/unit/index-exports.test.js index 9b6c6dd1..1d6f795e 100644 --- a/tests/unit/index-exports.test.js +++ b/tests/unit/index-exports.test.js @@ -1,7 +1,18 @@ +import { readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; +const __dirname = dirname(fileURLToPath(import.meta.url)); +const pkg = JSON.parse(readFileSync(resolve(__dirname, '../../package.json'), 'utf8')); + describe('index.js re-exports', () => { + it('package.json exports map points to CJS wrapper', () => { + expect(pkg.exports['.']).toBeDefined(); + expect(pkg.exports['.'].require).toBe('./src/index.cjs'); + }); + it('all re-exports resolve without errors', async () => { // Dynamic import validates that every re-exported module exists and // all named exports are resolvable. If any source file is missing, From cda361fd275bb491ac0fa30149b8d986cdb65136 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Mon, 16 Mar 2026 05:44:35 -0600 Subject: [PATCH 5/7] fix: correct JSDoc in CJS wrapper and add destructuring warning Clarify that import() always returns a Promise on all Node versions, and warn that named destructuring at require-time silently returns undefined. --- src/index.cjs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/index.cjs b/src/index.cjs index 47649798..1024eb41 100644 --- a/src/index.cjs +++ b/src/index.cjs @@ -1,10 +1,13 @@ /** * CJS compatibility wrapper — delegates to ESM via dynamic import(). * - * Usage (async): + * This wrapper always returns a Promise on every Node version, because + * import() is unconditionally async. You must always await the result: + * * const codegraph = await require('@optave/codegraph'); * - * If you are on Node >= 22, synchronous require() of ESM may work - * automatically. On older versions, await the result. + * // Named destructuring at require-time does NOT work — always await the full result first. + * // BAD: const { buildGraph } = require('@optave/codegraph'); // buildGraph is undefined + * // GOOD: const { buildGraph } = await require('@optave/codegraph'); */ module.exports = import('./index.js'); From a02f519e83a4119727f8c4067d031fabd73289ad Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Mon, 16 Mar 2026 05:53:59 -0600 Subject: [PATCH 6/7] fix: address Greptile review feedback on CJS exports - Point `default` condition in exports map to `./src/index.cjs` so CJS-only tools get a loadable wrapper instead of ESM - Document that rejected import() Promises are cached by the CJS module system - Add symmetric key check in test to verify CJS has no extra exports --- package.json | 2 +- src/index.cjs | 3 +++ tests/unit/index-exports.test.js | 5 +++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 95c927a7..88fd7b91 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ ".": { "import": "./src/index.js", "require": "./src/index.cjs", - "default": "./src/index.js" + "default": "./src/index.cjs" }, "./cli": { "import": "./src/cli.js" diff --git a/src/index.cjs b/src/index.cjs index 1024eb41..811e449c 100644 --- a/src/index.cjs +++ b/src/index.cjs @@ -10,4 +10,7 @@ * // BAD: const { buildGraph } = require('@optave/codegraph'); // buildGraph is undefined * // GOOD: const { buildGraph } = await require('@optave/codegraph'); */ +// Note: if import() rejects (e.g. missing dependency), the rejected Promise is cached +// by the CJS module system and every subsequent require() call will re-surface the same +// rejection without re-attempting the load. module.exports = import('./index.js'); diff --git a/tests/unit/index-exports.test.js b/tests/unit/index-exports.test.js index 1d6f795e..31b3c3b3 100644 --- a/tests/unit/index-exports.test.js +++ b/tests/unit/index-exports.test.js @@ -35,5 +35,10 @@ describe('index.js re-exports', () => { expect(cjs[key], `CJS export "${key}" is missing or undefined`).toBeDefined(); expect(typeof cjs[key]).toBe(typeof esm[key]); } + + // Symmetric check: CJS should not have extra keys beyond ESM exports. + const esmKeys = new Set(Object.keys(esm).filter(k => k !== 'default')); + const cjsKeys = new Set(Object.keys(cjs).filter(k => k !== 'default')); + expect(cjsKeys).toEqual(esmKeys); }); }); From 94b84bffcbd079194b7a49ef29e3a342bb886bb1 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Mon, 16 Mar 2026 16:34:56 -0600 Subject: [PATCH 7/7] fix: add missing arrow function parens for Biome lint Biome requires parentheses around single arrow function parameters. --- tests/unit/index-exports.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/index-exports.test.js b/tests/unit/index-exports.test.js index 31b3c3b3..ecd06bdb 100644 --- a/tests/unit/index-exports.test.js +++ b/tests/unit/index-exports.test.js @@ -37,8 +37,8 @@ describe('index.js re-exports', () => { } // Symmetric check: CJS should not have extra keys beyond ESM exports. - const esmKeys = new Set(Object.keys(esm).filter(k => k !== 'default')); - const cjsKeys = new Set(Object.keys(cjs).filter(k => k !== 'default')); + const esmKeys = new Set(Object.keys(esm).filter((k) => k !== 'default')); + const cjsKeys = new Set(Object.keys(cjs).filter((k) => k !== 'default')); expect(cjsKeys).toEqual(esmKeys); }); });