Skip to content

Commit 81b4c40

Browse files
authored
fix(compartment-mapper): infer module type from the import key in package.json exports (#3055)
- add a failing test exposing a cjs/esm compatibility issue in compartment-mapper - fix missing inference from package.json->exports `import` field The way it fixes the test is not exactly matching what node.js would do in the same situation. Node.js loads the file in `package.json->exports['./sha2'].require` but Endo loads the one from `package.json->exports['./sha2'].import`. All this fix is doing is correcting the module type inference for `package.json->exports` in general. Changing the behavior of selecting import vs require could happen elsewhere, but it seems unimportant in Endo (until we find a package that ships breaking changes in one version across esm and cjs variants)
1 parent 8936480 commit 81b4c40

14 files changed

Lines changed: 225 additions & 8 deletions

File tree

.changeset/two-cups-explode.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@endo/compartment-mapper": patch
3+
---
4+
5+
- Introduces additional signal to consider an export from a package an ESM module when it's selected via an `import` key in `exports` in package.json in case no other indication of it being an ESM module is present.

packages/compartment-mapper/src/infer-exports.js

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,15 @@ function* interpretBrowserField(name, browser, main = 'index.js') {
6363
* @param {object} exports - the `exports` field from a package.json.
6464
* @param {Set<string>} conditions - build conditions about the target environment
6565
* for selecting relevant exports, e.g., "browser" or "node".
66+
* @param {LanguageForExtension} types - an object to populate
67+
* with any recognized module's type, if implied by a tag.
6668
* @yields {[string, string]}
6769
* @returns {Generator<[string, string]>}
6870
*/
69-
function* interpretExports(name, exports, conditions) {
71+
function* interpretExports(name, exports, conditions, types) {
7072
if (isArray(exports)) {
7173
for (const section of exports) {
72-
const results = [...interpretExports(name, section, conditions)];
74+
const results = [...interpretExports(name, section, conditions, types)];
7375
if (results.length > 0) {
7476
yield* results;
7577
break;
@@ -93,12 +95,20 @@ function* interpretExports(name, exports, conditions) {
9395
continue; // or no-op
9496
} else if (key.startsWith('./') || key === '.') {
9597
if (name === '.') {
96-
yield* interpretExports(key, value, conditions);
98+
yield* interpretExports(key, value, conditions, types);
9799
} else {
98-
yield* interpretExports(join(name, key), value, conditions);
100+
yield* interpretExports(join(name, key), value, conditions, types);
99101
}
100102
} else if (conditions.has(key)) {
101-
yield* interpretExports(name, value, conditions);
103+
if (types && key === 'import' && typeof value === 'string') {
104+
// In this one case, the key "import" has carried a hint that the
105+
// referenced module is an ECMASCript module, and that hint may be
106+
// necessary to override whatever type might be inferred from the module
107+
// specifier extension.
108+
const spec = relativize(value);
109+
types[spec] = 'mjs';
110+
}
111+
yield* interpretExports(name, value, conditions, types);
102112
// Take only the first matching tag.
103113
break;
104114
}
@@ -141,7 +151,7 @@ export const inferExportsEntries = function* inferExportsEntries(
141151
yield ['.', relativize(main)];
142152
}
143153
if (exports !== undefined) {
144-
yield* interpretExports('.', exports, conditions);
154+
yield* interpretExports('.', exports, conditions, types);
145155
}
146156
// TODO Otherwise, glob 'files' for all '.js', '.cjs', and '.mjs' entry
147157
// modules, taking care to exclude node_modules.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This is a narrowed-down copy of actual noble hashes package to capture an issue in loading it from cjs.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// eslint-disable-next-line no-undef
2+
const { sha256 } = require('@noble/hashes/sha2');
3+
4+
// Example data to hash
5+
const textData = new TextEncoder().encode('Hello, world!');
6+
7+
// SHA256 hashing
8+
const hash256 = sha256(textData);
9+
console.log('SHA256 Hash:', hash256);

packages/compartment-mapper/test/fixtures-noble/node_modules/.package-lock.json

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/compartment-mapper/test/fixtures-noble/node_modules/@noble/hashes/LICENSE

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/compartment-mapper/test/fixtures-noble/node_modules/@noble/hashes/esm/sha2.js

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/compartment-mapper/test/fixtures-noble/node_modules/@noble/hashes/package.json

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/compartment-mapper/test/fixtures-noble/node_modules/@noble/hashes/sha2.js

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/compartment-mapper/test/fixtures-noble/package-lock.json

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)