diff --git a/package.json b/package.json index fd29cee8..88fd7b91 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.cjs" }, "./cli": { "import": "./src/cli.js" diff --git a/src/index.cjs b/src/index.cjs new file mode 100644 index 00000000..811e449c --- /dev/null +++ b/src/index.cjs @@ -0,0 +1,16 @@ +/** + * CJS compatibility wrapper — delegates to ESM via dynamic import(). + * + * 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'); + * + * // 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'); + */ +// 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 a5a912b7..ecd06bdb 100644 --- a/tests/unit/index-exports.test.js +++ b/tests/unit/index-exports.test.js @@ -1,6 +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, @@ -9,4 +21,24 @@ 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 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[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); + }); });