Skip to content

Commit eea2f49

Browse files
authored
implicit /+esm if no extension; fix /+esm conflict (#1165)
* implicit /+esm if no extension * tidy imports * no /+esm if trailing slash * move trailing /+esm to leading /_esm * prettier-ignore * failing test case * back to _esm.js * inline fromJsDelivrPath
1 parent 0e6919d commit eea2f49

File tree

2 files changed

+86
-8
lines changed

2 files changed

+86
-8
lines changed

src/npm.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {existsSync} from "node:fs";
22
import {mkdir, readFile, readdir, writeFile} from "node:fs/promises";
3-
import {dirname, join} from "node:path/posix";
3+
import {dirname, extname, join} from "node:path/posix";
44
import type {CallExpression} from "acorn";
55
import {simple} from "acorn-walk";
66
import {rsort, satisfies} from "semver";
@@ -249,7 +249,16 @@ export async function resolveNpmImport(root: string, specifier: string): Promise
249249
? "dist/echarts.esm.min.js"
250250
: "+esm"
251251
} = parseNpmSpecifier(specifier);
252-
return `/_npm/${name}@${await resolveNpmVersion(root, {name, range})}/${path.replace(/\+esm$/, "_esm.js")}`;
252+
const version = await resolveNpmVersion(root, {name, range});
253+
return `/_npm/${name}@${version}/${
254+
extname(path) || // npm:foo/bar.js
255+
path === "" || // npm:foo/
256+
path.endsWith("/") // npm:foo/bar/
257+
? path
258+
: path === "+esm" // npm:foo/+esm
259+
? "_esm.js"
260+
: path.replace(/(?:\/\+esm)?$/, "._esm.js") // npm:foo/bar or npm:foo/bar/+esm
261+
}`;
253262
}
254263

255264
/**
@@ -264,18 +273,34 @@ export async function resolveNpmImports(root: string, path: string): Promise<Imp
264273

265274
/**
266275
* Given a local npm path such as "/_npm/[email protected]/_esm.js", returns the
267-
* corresponding npm specifier such as "[email protected]".
276+
* corresponding npm specifier such as "[email protected]/+esm". For example:
277+
*
278+
* /_npm/[email protected]/_esm.js → [email protected]/+esm
279+
* /_npm/[email protected]/lite._esm.js → [email protected]/lite/+esm
280+
* /_npm/[email protected]/lite.js._esm.js → [email protected]/lite.js/+esm
268281
*/
269282
export function extractNpmSpecifier(path: string): string {
270283
if (!path.startsWith("/_npm/")) throw new Error(`invalid npm path: ${path}`);
271-
return path.replace(/^\/_npm\//, "").replace(/\/_esm\.js$/, "/+esm");
284+
const parts = path.split("/"); // ["", "_npm", "[email protected]", "lite.js._esm.js"]
285+
const i = parts[2].startsWith("@") ? 4 : 3; // test for scoped package
286+
const namever = parts.slice(2, i).join("/"); // "[email protected]" or "@observablehq/[email protected]"
287+
const subpath = parts.slice(i).join("/"); // "_esm.js" or "lite._esm.js" or "lite.js._esm.js"
288+
return `${namever}/${subpath === "_esm.js" ? "+esm" : subpath.replace(/\._esm\.js$/, "/+esm")}`;
272289
}
273290

274291
/**
275292
* Given a jsDelivr path such as "/npm/[email protected]/+esm", returns the corresponding
276-
* local path such as "/_npm/[email protected]/_esm.js".
293+
* local path such as "/_npm/[email protected]/_esm.js". For example:
294+
*
295+
* /npm/[email protected]/+esm → /_npm/[email protected]/_esm.js
296+
* /npm/[email protected]/lite/+esm → /_npm/[email protected]/lite._esm.js
297+
* /npm/[email protected]/lite.js/+esm → /_npm/[email protected]/lite.js._esm.js
277298
*/
278299
export function fromJsDelivrPath(path: string): string {
279300
if (!path.startsWith("/npm/")) throw new Error(`invalid jsDelivr path: ${path}`);
280-
return path.replace(/^\/npm\//, "/_npm/").replace(/\/\+esm$/, "/_esm.js");
301+
const parts = path.split("/"); // e.g. ["", "npm", "[email protected]", "lite", "+esm"]
302+
const i = parts[2].startsWith("@") ? 4 : 3; // test for scoped package
303+
const namever = parts.slice(2, i).join("/"); // "[email protected]" or "@observablehq/[email protected]"
304+
const subpath = parts.slice(i).join("/"); // "+esm" or "lite/+esm" or "lite.js/+esm"
305+
return `/_npm/${namever}/${subpath === "+esm" ? "_esm.js" : subpath.replace(/\/\+esm$/, "._esm.js")}`;
281306
}

test/npm-test.ts

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import assert from "node:assert";
2-
import {extractNpmSpecifier, getDependencyResolver, rewriteNpmImports} from "../src/npm.js";
3-
import {fromJsDelivrPath} from "../src/npm.js";
2+
import {extractNpmSpecifier, parseNpmSpecifier} from "../src/npm.js";
3+
import {fromJsDelivrPath, getDependencyResolver, resolveNpmImport, rewriteNpmImports} from "../src/npm.js";
44
import {relativePath} from "../src/path.js";
55
import {mockJsDelivr} from "./mocks/jsdelivr.js";
66

@@ -20,10 +20,62 @@ describe("getDependencyResolver(root, path, input)", () => {
2020
});
2121
});
2222

23+
describe("parseNpmSpecifier(specifier)", () => {
24+
it("parses the name", () => {
25+
assert.deepStrictEqual(parseNpmSpecifier("d3-array"), {name: "d3-array", range: undefined, path: undefined});
26+
});
27+
it("parses the name and range", () => {
28+
assert.deepStrictEqual(parseNpmSpecifier("d3-array@1"), {name: "d3-array", range: "1", path: undefined});
29+
assert.deepStrictEqual(parseNpmSpecifier("d3-array@latest"), {name: "d3-array", range: "latest", path: undefined});
30+
});
31+
it("parses the name and path", () => {
32+
assert.deepStrictEqual(parseNpmSpecifier("d3-array"), {name: "d3-array", range: undefined, path: undefined});
33+
assert.deepStrictEqual(parseNpmSpecifier("d3-array/foo"), {name: "d3-array", range: undefined, path: "foo"});
34+
assert.deepStrictEqual(parseNpmSpecifier("d3-array/foo/bar"), {name: "d3-array", range: undefined, path: "foo/bar"}); // prettier-ignore
35+
assert.deepStrictEqual(parseNpmSpecifier("d3-array/foo.js"), {name: "d3-array", range: undefined, path: "foo.js"});
36+
assert.deepStrictEqual(parseNpmSpecifier("d3-array/foo/bar.js"), {name: "d3-array", range: undefined, path: "foo/bar.js"}); // prettier-ignore
37+
assert.deepStrictEqual(parseNpmSpecifier("d3-array/+esm"), {name: "d3-array", range: undefined, path: "+esm"});
38+
assert.deepStrictEqual(parseNpmSpecifier("d3-array/foo.js/+esm"), {name: "d3-array", range: undefined, path: "foo.js/+esm"}); // prettier-ignore
39+
assert.deepStrictEqual(parseNpmSpecifier("d3-array/foo/bar.js/+esm"), {name: "d3-array", range: undefined, path: "foo/bar.js/+esm"}); // prettier-ignore
40+
assert.deepStrictEqual(parseNpmSpecifier("d3-array/foo/+esm"), {name: "d3-array", range: undefined, path: "foo/+esm"}); // prettier-ignore
41+
assert.deepStrictEqual(parseNpmSpecifier("d3-array/foo/bar/+esm"), {name: "d3-array", range: undefined, path: "foo/bar/+esm"}); // prettier-ignore
42+
assert.deepStrictEqual(parseNpmSpecifier("d3-array/"), {name: "d3-array", range: undefined, path: ""});
43+
});
44+
it("parses the name, version, and path", () => {
45+
assert.deepStrictEqual(parseNpmSpecifier("d3-array@1/foo"), {name: "d3-array", range: "1", path: "foo"});
46+
});
47+
});
48+
49+
describe("resolveNpmImport(root, specifier)", () => {
50+
mockJsDelivr();
51+
const root = "test/input/build/simple";
52+
it("implicitly adds /_esm.js for specifiers without an extension", async () => {
53+
assert.strictEqual(await resolveNpmImport(root, "d3-array"), "/_npm/[email protected]/_esm.js");
54+
assert.strictEqual(await resolveNpmImport(root, "d3-array/src"), "/_npm/[email protected]/src._esm.js");
55+
assert.strictEqual(await resolveNpmImport(root, "d3-array/foo+bar"), "/_npm/[email protected]/foo+bar._esm.js");
56+
assert.strictEqual(await resolveNpmImport(root, "d3-array/foo+esm"), "/_npm/[email protected]/foo+esm._esm.js");
57+
});
58+
it("replaces /+esm with /_esm.js or ._esm.js", async () => {
59+
assert.strictEqual(await resolveNpmImport(root, "d3-array/+esm"), "/_npm/[email protected]/_esm.js");
60+
assert.strictEqual(await resolveNpmImport(root, "d3-array/src/+esm"), "/_npm/[email protected]/src._esm.js");
61+
assert.strictEqual(await resolveNpmImport(root, "d3-array/src/index.js/+esm"), "/_npm/[email protected]/src/index.js._esm.js"); // prettier-ignore
62+
});
63+
it("does not add /_esm.js if given a path with a file extension", async () => {
64+
assert.strictEqual(await resolveNpmImport(root, "d3-array/src/index.js"), "/_npm/[email protected]/src/index.js");
65+
});
66+
it("does not add /_esm.js if given a path with a trailing slash", async () => {
67+
assert.strictEqual(await resolveNpmImport(root, "d3-array/"), "/_npm/[email protected]/");
68+
assert.strictEqual(await resolveNpmImport(root, "d3-array/src/"), "/_npm/[email protected]/src/");
69+
});
70+
});
71+
2372
describe("extractNpmSpecifier(path)", () => {
2473
it("returns the npm specifier for the given local npm path", () => {
2574
assert.strictEqual(extractNpmSpecifier("/_npm/[email protected]/_esm.js"), "[email protected]/+esm");
2675
assert.strictEqual(extractNpmSpecifier("/_npm/[email protected]/dist/d3.js"), "[email protected]/dist/d3.js");
76+
assert.strictEqual(extractNpmSpecifier("/_npm/[email protected]/dist/d3.js._esm.js"), "[email protected]/dist/d3.js/+esm");
77+
assert.strictEqual(extractNpmSpecifier("/_npm/[email protected]/lite._esm.js"), "[email protected]/lite/+esm");
78+
assert.strictEqual(extractNpmSpecifier("/_npm/@uwdata/[email protected]/_esm.js"), "@uwdata/[email protected]/+esm");
2779
});
2880
it("throws if not given a local npm path", () => {
2981
assert.throws(() => extractNpmSpecifier("/npm/[email protected]/+esm"), /invalid npm path/);
@@ -35,6 +87,7 @@ describe("fromJsDelivrPath(path)", () => {
3587
it("returns the local npm path for the given jsDelivr path", () => {
3688
assert.strictEqual(fromJsDelivrPath("/npm/[email protected]/+esm"), "/_npm/[email protected]/_esm.js");
3789
assert.strictEqual(fromJsDelivrPath("/npm/[email protected]/dist/d3.js"), "/_npm/[email protected]/dist/d3.js");
90+
assert.strictEqual(fromJsDelivrPath("/npm/[email protected]/dist/d3.js/+esm"), "/_npm/[email protected]/dist/d3.js._esm.js");
3891
});
3992
it("throws if not given a jsDelivr path", () => {
4093
assert.throws(() => fromJsDelivrPath("/_npm/[email protected]/_esm.js"), /invalid jsDelivr path/);

0 commit comments

Comments
 (0)