Skip to content

Commit fe51b7e

Browse files
Merge pull request #389 from preactjs/default-over-node
(wmr) - resolve default over node
2 parents 1b52512 + d0f5fe8 commit fe51b7e

File tree

11 files changed

+73
-73
lines changed

11 files changed

+73
-73
lines changed

.changeset/strange-beers-play.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'wmr': minor
3+
---
4+
5+
Use resolve.exports for export map resolving in the npm-plugin

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ wmr.cjs
66
package-lock.json
77
!packages/wmr/test/fixtures/commonjs/node_modules
88
!packages/wmr/test/fixtures/package-exports/node_modules
9+
!packages/wmr/test/fixtures/exports/node_modules
910
.DS_Store

packages/wmr/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@
139139
"premove": "^3.0.1",
140140
"prettier": "^2.0.5",
141141
"puppeteer": "^3.3.0",
142+
"resolve.exports": "^1.0.2",
142143
"rollup": "^2.39.0",
143144
"rollup-plugin-preserve-shebang": "^1.0.1",
144145
"sade": "^1.7.3",
Lines changed: 25 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,21 @@
1+
import { resolve as _resolveExports, legacy as _resolveLegacyEntry } from 'resolve.exports';
2+
3+
function resolveExports(pkg, key) {
4+
return _resolveExports(pkg, key, {
5+
browser: true,
6+
conditions: [process.env.NODE_ENV === 'production' ? 'production' : 'development', 'esmodules', 'module']
7+
});
8+
}
9+
10+
function resolveLegacyEntry(pkg, path) {
11+
const entry =
12+
_resolveLegacyEntry(pkg, {
13+
browser: true,
14+
fields: ['esmodules', 'modern', 'module', 'jsnext:main', 'browser', 'main']
15+
}) || 'index.js';
16+
return '/' + entry.replace(/^\.?\//, '');
17+
}
18+
119
/**
220
* @param {string} path
321
* @param {object} context
@@ -21,23 +39,16 @@ export async function resolveModule(path, { readFile, hasFile, module, internal
2139

2240
// Package Export Maps
2341
if (!internal && pkg.exports) {
24-
const entry = path ? `./${path}` : '.';
25-
26-
const mapped = resolveExportMap(pkg.exports, entry, ENV_KEYS);
27-
28-
if (!mapped) {
29-
throw new Error(`Unknown package export ${entry} in ${module}.\n\n${JSON.stringify(pkg.exports, null, 2)}`);
30-
}
31-
32-
// `mapped:true` means directory access was allowed for this entry, but it was not resolved.
33-
if (mapped !== true && !internal) {
34-
return mapped.replace(/^\./, '');
35-
}
42+
// will normalize entry & will throw error if no match
43+
const mapped = resolveExports(pkg, path || '.');
44+
// An entry ending in `/` remaps to a directory, but is not considered resolved.
45+
if (mapped.endsWith('/')) path = mapped;
46+
else return mapped.replace(/^\./, '');
3647
}
3748

3849
// path is a bare import of a package, use its legacy exports (module/main):
3950
if (!path) {
40-
path = getLegacyEntry(pkg);
51+
path = resolveLegacyEntry(pkg, path || '.');
4152
}
4253

4354
// fallback: implement basic commonjs-style resolution
@@ -50,7 +61,7 @@ export async function resolveModule(path, { readFile, hasFile, module, internal
5061
if (!isExportMappedSpecifier) {
5162
try {
5263
const subPkg = JSON.parse(await readFile(path + '/package.json'));
53-
path += getLegacyEntry(subPkg);
64+
path += resolveLegacyEntry(subPkg, '.');
5465
} catch (e) {}
5566
}
5667

@@ -66,60 +77,3 @@ export async function resolveModule(path, { readFile, hasFile, module, internal
6677

6778
return path;
6879
}
69-
70-
/**
71-
* Get the best possible entry from a package.json that doesn't have an Export Map
72-
* @TODO this does not currently support {"browser":{"./foo.js":"./browser-foo.js"}}
73-
*/
74-
function getLegacyEntry(pkg) {
75-
const mainFields = [pkg.esmodules, pkg.modern, pkg.module, pkg['jsnext:main'], pkg.browser, pkg.main, 'index.js'];
76-
const entry = mainFields.find(p => p && typeof p === 'string');
77-
return '/' + entry.replace(/^\.?\//, '');
78-
}
79-
80-
const ENV_KEYS = ['esmodules', 'import', 'module', 'require', 'browser', 'node', 'default'];
81-
82-
/** Get the best resolution for an entry from an Export Map
83-
* @param {Object} exp `package.exports`
84-
* @param {string} entry `./foo` or `.`
85-
* @param {string[]} envKeys package environment keys
86-
* @returns {string | boolean} a resolved path, or a boolean indicating if the given entry is exposed
87-
*/
88-
function resolveExportMap(exp, entry, envKeys) {
89-
if (typeof exp === 'string') {
90-
// {"exports":"./foo.js"}
91-
// {"exports":{"./foo":"./foo.js"}}
92-
return exp;
93-
}
94-
let isFileListing;
95-
let isDirectoryExposed = false;
96-
97-
let fallbacks = [];
98-
for (let i in exp) {
99-
if (isFileListing === undefined) isFileListing = i[0] === '.';
100-
if (isFileListing) {
101-
// {"exports":{".":"./index.js"}}
102-
if (i === entry) {
103-
return resolveExportMap(exp[i], entry, envKeys);
104-
}
105-
if (!isDirectoryExposed && i.endsWith('/') && entry.startsWith(i)) {
106-
isDirectoryExposed = true;
107-
}
108-
} else if (envKeys.includes(i)) {
109-
// intentionally de-prioritize "require" and "default" keys
110-
if (i === 'require' || i === 'default') {
111-
fallbacks.push(i);
112-
} else {
113-
// {"exports":{"import":"./foo.js"}}
114-
return resolveExportMap(exp[i], entry, envKeys);
115-
}
116-
}
117-
}
118-
119-
// None of the in-order keys matched - fall back to require/default in the order specified
120-
for (let i of fallbacks) {
121-
return resolveExportMap(exp[i], entry, envKeys);
122-
}
123-
124-
return isDirectoryExposed;
125-
}

packages/wmr/test/fixtures.test.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,20 @@ describe('fixtures', () => {
482482
});
483483
});
484484

485+
describe('export-map', () => {
486+
beforeEach(async () => {
487+
await loadFixture('exports', env);
488+
instance = await runWmrFast(env.tmp.path);
489+
await env.page.goto(await instance.address);
490+
});
491+
492+
it('should not pick node for a browser', async () => {
493+
const test = await env.page.$('.test');
494+
let text = test ? await test.evaluate(el => el.textContent) : null;
495+
expect(text).toEqual('Browser implementation');
496+
});
497+
});
498+
485499
describe('package-exports', () => {
486500
beforeEach(async () => {
487501
await loadFixture('package-exports', env);
@@ -521,12 +535,12 @@ describe('fixtures', () => {
521535
default: 'import'
522536
});
523537
expect(await env.page.evaluate(`import('/@npm/exports-fallbacks-defaultfirst')`)).toEqual({
524-
default: 'import'
538+
default: 'default'
525539
});
526540

527541
// When import/module/browser isn't present (but a random other one is!), we fall back to require/default:
528542
expect(await env.page.evaluate(`import('/@npm/exports-fallbacks-requirefallback')`)).toEqual({
529-
default: 'require'
543+
default: 'default'
530544
});
531545
expect(await env.page.evaluate(`import('/@npm/exports-fallbacks-defaultfallback')`)).toEqual({
532546
default: 'default'

packages/wmr/test/fixtures/exports/node_modules/test/browser.mjs

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

packages/wmr/test/fixtures/exports/node_modules/test/node.mjs

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

packages/wmr/test/fixtures/exports/node_modules/test/package.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf8" />
5+
<title>Exports</title>
6+
</head>
7+
<body>
8+
<div id="root"></div>
9+
<script src="./index.js" type="module"></script>
10+
</body>
11+
</html>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import value from 'test';
2+
3+
const div = document.createElement('div');
4+
div.setAttribute('class', 'test');
5+
div.innerText = value;
6+
document.body.appendChild(div);

0 commit comments

Comments
 (0)