Skip to content

Commit 9567adf

Browse files
Tal500benmccannRich-Harris
authored
feat: add support for exports field to REPL (#445)
* better external package path resolving, including respecting the modern `exports` field * Update packages/repl/src/lib/workers/bundler/index.js Co-authored-by: Ben McCann <[email protected]> * Update index.js * Update packages/repl/src/lib/workers/bundler/index.js Co-authored-by: Ben McCann <[email protected]> * Update index.js * fix typo * update pnpm-lock.yaml * put `importee_package_name` inside the `if (match)` * give another chance to load from `./index.{mjs|js}` * throw errors on package import resolving * simplify error messages * add a comment, because i was confused about what this was for * move resolution logic into separate function, match rollup semantics (pkg.svelte first) --------- Co-authored-by: Ben McCann <[email protected]> Co-authored-by: Rich Harris <[email protected]>
1 parent 21642c5 commit 9567adf

File tree

3 files changed

+95
-13
lines changed

3 files changed

+95
-13
lines changed

packages/repl/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"acorn": "^8.8.2",
3131
"codemirror": "5.65.1",
3232
"estree-walker": "^3.0.3",
33+
"resolve.exports": "^2.0.0",
3334
"svelte-json-tree": "^1.0.0",
3435
"yootils": "^0.3.1"
3536
},

packages/repl/src/lib/workers/bundler/index.js

Lines changed: 87 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { rollup } from '@rollup/browser';
2+
import * as resolve from 'resolve.exports';
23
import { sleep } from 'yootils';
34
import commonjs from './plugins/commonjs.js';
45
import glsl from './plugins/glsl.js';
@@ -106,9 +107,63 @@ function has_loopGuardTimeout_feature() {
106107
return compare_to_version(3, 14, 0) >= 0;
107108
}
108109

110+
async function resolve_from_pkg(pkg, subpath) {
111+
// match legacy Rollup logic — pkg.svelte takes priority over pkg.exports
112+
if (typeof pkg.svelte === 'string' && subpath === '.') {
113+
return pkg.svelte;
114+
}
115+
116+
// modern
117+
if (pkg.exports) {
118+
try {
119+
const [resolved] = resolve.exports(pkg, subpath, {
120+
browser: true,
121+
conditions: ['svelte', 'production']
122+
});
123+
124+
return resolved;
125+
} catch {
126+
throw `no matched export path was found in "${pkg_name}/package.json"`;
127+
}
128+
}
129+
130+
// legacy
131+
if (subpath === '.') {
132+
const resolved_id = resolve.legacy(pkg, {
133+
fields: ['browser', 'module', 'main']
134+
});
135+
136+
if (!resolved_id) {
137+
// last ditch — try to match index.js/index.mjs
138+
for (const index_file of ['index.mjs', 'index.js']) {
139+
try {
140+
const indexUrl = new URL(index_file, `${pkg_url_base}/`).href;
141+
return await follow_redirects(indexUrl, uid);
142+
} catch {
143+
// maybe the next option will be successful
144+
}
145+
}
146+
147+
throw `could not find entry point in "${pkg_name}/package.json"`;
148+
}
149+
150+
return resolved_id;
151+
}
152+
153+
if (typeof pkg.browser === 'object') {
154+
// this will either return `pkg.browser[subpath]` or `subpath`
155+
return resolve.legacy(pkg, {
156+
browser: subpath
157+
});
158+
}
159+
160+
return subpath;
161+
}
162+
109163
async function get_bundle(uid, mode, cache, lookup) {
110164
let bundle;
111165

166+
/** A set of package names (without subpaths) to include in pkg.devDependencies when downloading an app */
112167
const imports = new Set();
113168
const warnings = [];
114169
const all_warnings = [];
@@ -155,25 +210,44 @@ async function get_bundle(uid, mode, cache, lookup) {
155210
// fetch from unpkg
156211
self.postMessage({ type: 'status', uid, message: `resolving ${importee}` });
157212

213+
const match = /^((?:@[^/]+\/)?[^/]+)(\/.+)?$/.exec(importee);
214+
if (!match) {
215+
throw new Error(`Invalid import "${importee}"`);
216+
}
217+
218+
const pkg_name = match[1];
219+
const subpath = `.${match[2] ?? ''}`;
220+
221+
// if this was imported by one of our files, add it to the `imports` set
158222
if (importer in lookup) {
159-
const match = /^(@[^/]+\/)?[^/]+/.exec(importee);
160-
if (match) imports.add(match[0]);
223+
imports.add(pkg_name);
161224
}
162225

163-
try {
164-
const pkg_url = await follow_redirects(`${packagesUrl}/${importee}/package.json`, uid);
165-
const pkg_json = (await fetch_if_uncached(pkg_url, uid)).body;
166-
const pkg = JSON.parse(pkg_json);
226+
const fetch_package_info = async () => {
227+
try {
228+
const pkg_url = await follow_redirects(`${packagesUrl}/${pkg_name}/package.json`, uid);
229+
const pkg_json = (await fetch_if_uncached(pkg_url, uid)).body;
230+
const pkg = JSON.parse(pkg_json);
167231

168-
if (pkg.svelte || pkg.module || pkg.main) {
169-
const url = pkg_url.replace(/\/package\.json$/, '');
170-
return new URL(pkg.svelte || pkg.module || pkg.main, `${url}/`).href;
232+
const pkg_url_base = pkg_url.replace(/\/package\.json$/, '');
233+
234+
return {
235+
pkg,
236+
pkg_url_base
237+
};
238+
} catch (_e) {
239+
throw new Error(`Error fetching "${pkg_name}" from unpkg. Does the package exist?`);
171240
}
172-
} catch (err) {
173-
// ignore
174-
}
241+
};
242+
243+
const { pkg, pkg_url_base } = await fetch_package_info();
175244

176-
return await follow_redirects(`${packagesUrl}/${importee}`, uid);
245+
try {
246+
const resolved_id = await resolve_from_pkg(pkg, subpath);
247+
return new URL(resolved_id, `${pkg_url_base}/`).href;
248+
} catch (reason) {
249+
throw new Error(`Cannot import "${importee}": ${reason}.`);
250+
}
177251
}
178252
},
179253
async load(resolved) {

pnpm-lock.yaml

Lines changed: 7 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)