-
Notifications
You must be signed in to change notification settings - Fork 3
fix: add CJS require() support to package exports #472
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0e4d911
1970265
39092fd
483f0bf
cda361f
a02f519
939963c
94b84bf
491a629
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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'); | ||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Promise rejected on import failure is permanently cached by CJS module system
Any subsequent This is an accepted trade-off for the standard async CJS→ESM wrapper pattern, but worth documenting in a comment so future maintainers don't expect a retry-on-next-require behaviour:
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed — added a comment documenting the Promise caching behavior. |
||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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'); | ||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test bypasses the package exports map The test resolves the CJS wrapper by relative file path ( Consider resolving through the package name to exercise the full exports resolution:
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch. Using |
||||||||||||||||||||||||||||||
| 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]); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+33
to
+37
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test does not check for extra keys on the CJS side The loop iterates In practice this shouldn't happen here because
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed — added symmetric key check to verify CJS has no extra keys beyond ESM 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')); | ||||||||||||||||||||||||||||||
| expect(cjsKeys).toEqual(esmKeys); | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Destructured
require()silently returnsundefinedfor all named exportsBecause
module.exportsis set to the Promise returned byimport(), any CJS consumer that uses the most common require pattern will get silentundefineds:buildGraphdestructures from the Promise object, not from the resolved module namespace, so it comes backundefined. The failure is silent — no error is thrown at therequire()call site.The comment and PR description only show the
await require(...)pattern. Consider adding an explicit warning in the JSDoc comment to prevent this common pitfall:A note like
// Named destructuring at require-time does NOT work — always await the full result first.would save consumers from a hard-to-diagnose bug.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed — added an explicit warning comment that named destructuring at require-time does not work and consumers must always await the result first.