Skip to content

Commit d4cc548

Browse files
authored
_esm.js (#1056)
1 parent 669f833 commit d4cc548

File tree

12 files changed

+74
-40
lines changed

12 files changed

+74
-40
lines changed

src/build.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {transpileModule} from "./javascript/transpile.js";
1010
import type {Logger, Writer} from "./logger.js";
1111
import type {MarkdownPage} from "./markdown.js";
1212
import {parseMarkdown} from "./markdown.js";
13-
import {populateNpmCache, resolveNpmImport, resolveNpmSpecifier} from "./npm.js";
13+
import {extractNpmSpecifier, populateNpmCache, resolveNpmImport} from "./npm.js";
1414
import {isPathImport, relativePath, resolvePath} from "./path.js";
1515
import {renderPage} from "./render.js";
1616
import type {Resolvers} from "./resolvers.js";
@@ -176,7 +176,7 @@ export async function build(
176176
// doesn’t let you pass in a resolver.
177177
for (const path of globalImports) {
178178
if (!path.startsWith("/_npm/")) continue; // skip _observablehq
179-
effects.output.write(`${faint("copy")} npm:${resolveNpmSpecifier(path)} ${faint("→")} `);
179+
effects.output.write(`${faint("copy")} npm:${extractNpmSpecifier(path)} ${faint("→")} `);
180180
const sourcePath = await populateNpmCache(root, path); // TODO effects
181181
await effects.copyFile(sourcePath, path);
182182
}

src/npm.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export async function populateNpmCache(root: string, path: string): Promise<stri
8080
let promise = npmRequests.get(path);
8181
if (promise) return promise; // coalesce concurrent requests
8282
promise = (async function () {
83-
const specifier = resolveNpmSpecifier(path);
83+
const specifier = extractNpmSpecifier(path);
8484
const href = `https://cdn.jsdelivr.net/npm/${specifier}`;
8585
process.stdout.write(`npm:${specifier} ${faint("→")} `);
8686
const response = await fetch(href);
@@ -117,7 +117,7 @@ export async function getDependencyResolver(
117117
): Promise<(specifier: string) => string> {
118118
const body = parseProgram(input);
119119
const dependencies = new Set<string>();
120-
const {name, range} = parseNpmSpecifier(resolveNpmSpecifier(path));
120+
const {name, range} = parseNpmSpecifier(extractNpmSpecifier(path));
121121

122122
simple(body, {
123123
ImportDeclaration: findImport,
@@ -172,7 +172,7 @@ export async function getDependencyResolver(
172172
return (specifier: string) => {
173173
if (!specifier.startsWith("/npm/")) return specifier;
174174
if (resolutions.has(specifier)) specifier = resolutions.get(specifier)!;
175-
else specifier = `/_npm/${specifier.slice("/npm/".length)}${specifier.endsWith("/+esm") ? ".js" : ""}`;
175+
else specifier = fromJsDelivrPath(specifier);
176176
return relativePath(path, specifier);
177177
};
178178
}
@@ -249,14 +249,14 @@ 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+
return `/_npm/${name}@${await resolveNpmVersion(root, {name, range})}/${path.replace(/\+esm$/, "_esm.js")}`;
253253
}
254254

255255
const npmImportsCache = new Map<string, Promise<ImportReference[]>>();
256256

257257
/**
258258
* Resolves the direct dependencies of the specified npm path, such as
259-
* "/_npm/[email protected]/+esm.js", returning the corresponding set of npm paths.
259+
* "/_npm/[email protected]/_esm.js", returning the corresponding set of npm paths.
260260
*/
261261
export async function resolveNpmImports(root: string, path: string): Promise<ImportReference[]> {
262262
if (!path.startsWith("/_npm/")) throw new Error(`invalid npm path: ${path}`);
@@ -278,6 +278,20 @@ export async function resolveNpmImports(root: string, path: string): Promise<Imp
278278
return promise;
279279
}
280280

281-
export function resolveNpmSpecifier(path: string): string {
282-
return path.replace(/^\/_npm\//, "").replace(/\/\+esm\.js$/, "/+esm");
281+
/**
282+
* Given a local npm path such as "/_npm/[email protected]/_esm.js", returns the
283+
* corresponding npm specifier such as "[email protected]".
284+
*/
285+
export function extractNpmSpecifier(path: string): string {
286+
if (!path.startsWith("/_npm/")) throw new Error(`invalid npm path: ${path}`);
287+
return path.replace(/^\/_npm\//, "").replace(/\/_esm\.js$/, "/+esm");
288+
}
289+
290+
/**
291+
* Given a jsDelivr path such as "/npm/[email protected]/+esm", returns the corresponding
292+
* local path such as "/_npm/[email protected]/_esm.js".
293+
*/
294+
export function fromJsDelivrPath(path: string): string {
295+
if (!path.startsWith("/npm/")) throw new Error(`invalid jsDelivr path: ${path}`);
296+
return path.replace(/^\/npm\//, "/_npm/").replace(/\/\+esm$/, "/_esm.js");
283297
}

src/resolvers.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {getImplicitDependencies, getImplicitDownloads} from "./libraries.js";
88
import {getImplicitFileImports, getImplicitInputImports} from "./libraries.js";
99
import {getImplicitStylesheets} from "./libraries.js";
1010
import type {MarkdownPage} from "./markdown.js";
11-
import {populateNpmCache, resolveNpmImport, resolveNpmImports, resolveNpmSpecifier} from "./npm.js";
11+
import {extractNpmSpecifier, populateNpmCache, resolveNpmImport, resolveNpmImports} from "./npm.js";
1212
import {isPathImport, relativePath, resolvePath} from "./path.js";
1313

1414
export interface Resolvers {
@@ -171,7 +171,7 @@ export async function getResolvers(
171171
for (const i of await resolveNpmImports(root, value)) {
172172
if (i.type === "local") {
173173
const path = resolvePath(value, i.name);
174-
const specifier = `npm:${resolveNpmSpecifier(path)}`;
174+
const specifier = `npm:${extractNpmSpecifier(path)}`;
175175
globalImports.add(specifier);
176176
resolutions.set(specifier, path);
177177
}
@@ -188,7 +188,7 @@ export async function getResolvers(
188188
for (const i of await resolveNpmImports(root, value)) {
189189
if (i.type === "local" && i.method === "static") {
190190
const path = resolvePath(value, i.name);
191-
const specifier = `npm:${resolveNpmSpecifier(path)}`;
191+
const specifier = `npm:${extractNpmSpecifier(path)}`;
192192
staticImports.add(specifier);
193193
npmStaticResolutions.add(path);
194194
}

test/javascript/npm-test.ts

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import assert from "node:assert";
2-
import {getDependencyResolver, rewriteNpmImports} from "../../src/npm.js";
2+
import {extractNpmSpecifier, getDependencyResolver, rewriteNpmImports} from "../../src/npm.js";
3+
import {fromJsDelivrPath} from "../../src/npm.js";
34
import {relativePath} from "../../src/path.js";
45
import {mockJsDelivr} from "../mocks/jsdelivr.js";
56

@@ -8,67 +9,86 @@ describe("getDependencyResolver(root, path, input)", () => {
89
it("finds /npm/ imports and re-resolves their versions", async () => {
910
const root = "test/input/build/simple-public";
1011
const specifier = "/npm/[email protected]/dist/d3-array.js";
11-
const resolver = await getDependencyResolver(root, "/_npm/[email protected]/+esm.js", `import '${specifier}';\n`); // prettier-ignore
12+
const resolver = await getDependencyResolver(root, "/_npm/[email protected]/_esm.js", `import '${specifier}';\n`); // prettier-ignore
1213
assert.strictEqual(resolver(specifier), "../[email protected]/dist/d3-array.js");
1314
});
1415
it("finds /npm/ import resolutions and re-resolves their versions", async () => {
1516
const root = "test/input/build/simple-public";
1617
const specifier = "/npm/[email protected]/dist/d3-array.js";
17-
const resolver = await getDependencyResolver(root, "/_npm/[email protected]/+esm.js", `import.meta.resolve('${specifier}');\n`); // prettier-ignore
18+
const resolver = await getDependencyResolver(root, "/_npm/[email protected]/_esm.js", `import.meta.resolve('${specifier}');\n`); // prettier-ignore
1819
assert.strictEqual(resolver(specifier), "../[email protected]/dist/d3-array.js");
1920
});
2021
});
2122

23+
describe("extractNpmSpecifier(path)", () => {
24+
it("returns the npm specifier for the given local npm path", () => {
25+
assert.strictEqual(extractNpmSpecifier("/_npm/[email protected]/_esm.js"), "[email protected]/+esm");
26+
assert.strictEqual(extractNpmSpecifier("/_npm/[email protected]/dist/d3.js"), "[email protected]/dist/d3.js");
27+
});
28+
it("throws if not given a local npm path", () => {
29+
assert.throws(() => extractNpmSpecifier("/npm/[email protected]/+esm"), /invalid npm path/);
30+
assert.throws(() => extractNpmSpecifier("[email protected]"), /invalid npm path/);
31+
});
32+
});
33+
34+
describe("fromJsDelivrPath(path)", () => {
35+
it("returns the local npm path for the given jsDelivr path", () => {
36+
assert.strictEqual(fromJsDelivrPath("/npm/[email protected]/+esm"), "/_npm/[email protected]/_esm.js");
37+
assert.strictEqual(fromJsDelivrPath("/npm/[email protected]/dist/d3.js"), "/_npm/[email protected]/dist/d3.js");
38+
});
39+
it("throws if not given a jsDelivr path", () => {
40+
assert.throws(() => fromJsDelivrPath("/_npm/[email protected]/_esm.js"), /invalid jsDelivr path/);
41+
assert.throws(() => fromJsDelivrPath("[email protected]"), /invalid jsDelivr path/);
42+
});
43+
});
44+
2245
// prettier-ignore
2346
describe("rewriteNpmImports(input, resolve)", () => {
2447
it("rewrites /npm/ imports to /_npm/", () => {
2548
assert.strictEqual(rewriteNpmImports('export * from "/npm/[email protected]/dist/d3-array.js";\n', (v) => resolve("/_npm/[email protected]/dist/d3.js", v)), 'export * from "../../[email protected]/dist/d3-array.js";\n');
2649
});
27-
it("rewrites /npm/…+esm imports to +esm.js", () => {
28-
assert.strictEqual(rewriteNpmImports('export * from "/npm/[email protected]/+esm";\n', (v) => resolve("/_npm/[email protected]/+esm.js", v)), 'export * from "../[email protected]/+esm.js";\n');
50+
it("rewrites /npm/…+esm imports to _esm.js", () => {
51+
assert.strictEqual(rewriteNpmImports('export * from "/npm/[email protected]/+esm";\n', (v) => resolve("/_npm/[email protected]/_esm.js", v)), 'export * from "../[email protected]/_esm.js";\n');
2952
});
3053
it("rewrites /npm/ imports to a relative path", () => {
3154
assert.strictEqual(rewriteNpmImports('import "/npm/[email protected]/dist/d3-array.js";\n', (v) => resolve("/_npm/[email protected]/dist/d3.js", v)), 'import "../../[email protected]/dist/d3-array.js";\n');
3255
assert.strictEqual(rewriteNpmImports('import "/npm/[email protected]/dist/d3-array.js";\n', (v) => resolve("/_npm/[email protected]/d3.js", v)), 'import "../[email protected]/dist/d3-array.js";\n');
3356
});
3457
it("rewrites named imports", () => {
35-
assert.strictEqual(rewriteNpmImports('import {sort} from "/npm/[email protected]/+esm";\n', (v) => resolve("/_npm/[email protected]/+esm.js", v)), 'import {sort} from "../[email protected]/+esm.js";\n');
58+
assert.strictEqual(rewriteNpmImports('import {sort} from "/npm/[email protected]/+esm";\n', (v) => resolve("/_npm/[email protected]/_esm.js", v)), 'import {sort} from "../[email protected]/_esm.js";\n');
3659
});
3760
it("rewrites empty imports", () => {
38-
assert.strictEqual(rewriteNpmImports('import "/npm/[email protected]/+esm";\n', (v) => resolve("/_npm/[email protected]/+esm.js", v)), 'import "../[email protected]/+esm.js";\n');
61+
assert.strictEqual(rewriteNpmImports('import "/npm/[email protected]/+esm";\n', (v) => resolve("/_npm/[email protected]/_esm.js", v)), 'import "../[email protected]/_esm.js";\n');
3962
});
4063
it("rewrites default imports", () => {
41-
assert.strictEqual(rewriteNpmImports('import d3 from "/npm/[email protected]/+esm";\n', (v) => resolve("/_npm/[email protected]/+esm.js", v)), 'import d3 from "../[email protected]/+esm.js";\n');
64+
assert.strictEqual(rewriteNpmImports('import d3 from "/npm/[email protected]/+esm";\n', (v) => resolve("/_npm/[email protected]/_esm.js", v)), 'import d3 from "../[email protected]/_esm.js";\n');
4265
});
4366
it("rewrites namespace imports", () => {
44-
assert.strictEqual(rewriteNpmImports('import * as d3 from "/npm/[email protected]/+esm";\n', (v) => resolve("/_npm/[email protected]/+esm.js", v)), 'import * as d3 from "../[email protected]/+esm.js";\n');
67+
assert.strictEqual(rewriteNpmImports('import * as d3 from "/npm/[email protected]/+esm";\n', (v) => resolve("/_npm/[email protected]/_esm.js", v)), 'import * as d3 from "../[email protected]/_esm.js";\n');
4568
});
4669
it("rewrites named exports", () => {
47-
assert.strictEqual(rewriteNpmImports('export {sort} from "/npm/[email protected]/+esm";\n', (v) => resolve("/_npm/[email protected]/+esm.js", v)), 'export {sort} from "../[email protected]/+esm.js";\n');
70+
assert.strictEqual(rewriteNpmImports('export {sort} from "/npm/[email protected]/+esm";\n', (v) => resolve("/_npm/[email protected]/_esm.js", v)), 'export {sort} from "../[email protected]/_esm.js";\n');
4871
});
4972
it("rewrites namespace exports", () => {
50-
assert.strictEqual(rewriteNpmImports('export * from "/npm/[email protected]/+esm";\n', (v) => resolve("/_npm/[email protected]/+esm.js", v)), 'export * from "../[email protected]/+esm.js";\n');
73+
assert.strictEqual(rewriteNpmImports('export * from "/npm/[email protected]/+esm";\n', (v) => resolve("/_npm/[email protected]/_esm.js", v)), 'export * from "../[email protected]/_esm.js";\n');
5174
});
5275
it("rewrites dynamic imports with static module specifiers", () => {
53-
assert.strictEqual(rewriteNpmImports('import("/npm/[email protected]/+esm");\n', (v) => resolve("/_npm/[email protected]/+esm.js", v)), 'import("../[email protected]/+esm.js");\n');
54-
assert.strictEqual(rewriteNpmImports("import(`/npm/[email protected]/+esm`);\n", (v) => resolve("/_npm/[email protected]/+esm.js", v)), 'import("../[email protected]/+esm.js");\n');
55-
assert.strictEqual(rewriteNpmImports("import('/npm/[email protected]/+esm');\n", (v) => resolve("/_npm/[email protected]/+esm.js", v)), 'import("../[email protected]/+esm.js");\n');
76+
assert.strictEqual(rewriteNpmImports('import("/npm/[email protected]/+esm");\n', (v) => resolve("/_npm/[email protected]/_esm.js", v)), 'import("../[email protected]/_esm.js");\n');
77+
assert.strictEqual(rewriteNpmImports("import(`/npm/[email protected]/+esm`);\n", (v) => resolve("/_npm/[email protected]/_esm.js", v)), 'import("../[email protected]/_esm.js");\n');
78+
assert.strictEqual(rewriteNpmImports("import('/npm/[email protected]/+esm');\n", (v) => resolve("/_npm/[email protected]/_esm.js", v)), 'import("../[email protected]/_esm.js");\n');
5679
});
5780
it("ignores dynamic imports with dynamic module specifiers", () => {
58-
assert.strictEqual(rewriteNpmImports('import(`/npm/d3-array@${"3.2.4"}/+esm`);\n', (v) => resolve("/_npm/[email protected]/+esm.js", v)), 'import(`/npm/d3-array@${"3.2.4"}/+esm`);\n');
81+
assert.strictEqual(rewriteNpmImports('import(`/npm/d3-array@${"3.2.4"}/+esm`);\n', (v) => resolve("/_npm/[email protected]/_esm.js", v)), 'import(`/npm/d3-array@${"3.2.4"}/+esm`);\n');
5982
});
6083
it("ignores dynamic imports with dynamic module specifiers", () => {
61-
assert.strictEqual(rewriteNpmImports('import(`/npm/d3-array@${"3.2.4"}/+esm`);\n', (v) => resolve("/_npm/[email protected]/+esm.js", v)), 'import(`/npm/d3-array@${"3.2.4"}/+esm`);\n');
84+
assert.strictEqual(rewriteNpmImports('import(`/npm/d3-array@${"3.2.4"}/+esm`);\n', (v) => resolve("/_npm/[email protected]/_esm.js", v)), 'import(`/npm/d3-array@${"3.2.4"}/+esm`);\n');
6285
});
6386
it("strips the sourceMappingURL declaration", () => {
64-
assert.strictEqual(rewriteNpmImports('import(`/npm/d3-array@${"3.2.4"}/+esm`);\n//# sourceMappingURL=index.js.map', (v) => resolve("/_npm/[email protected]/+esm.js", v)), 'import(`/npm/d3-array@${"3.2.4"}/+esm`);\n');
65-
assert.strictEqual(rewriteNpmImports('import(`/npm/d3-array@${"3.2.4"}/+esm`);\n//# sourceMappingURL=index.js.map\n', (v) => resolve("/_npm/[email protected]/+esm.js", v)), 'import(`/npm/d3-array@${"3.2.4"}/+esm`);\n');
87+
assert.strictEqual(rewriteNpmImports('import(`/npm/d3-array@${"3.2.4"}/+esm`);\n//# sourceMappingURL=index.js.map', (v) => resolve("/_npm/[email protected]/_esm.js", v)), 'import(`/npm/d3-array@${"3.2.4"}/+esm`);\n');
88+
assert.strictEqual(rewriteNpmImports('import(`/npm/d3-array@${"3.2.4"}/+esm`);\n//# sourceMappingURL=index.js.map\n', (v) => resolve("/_npm/[email protected]/_esm.js", v)), 'import(`/npm/d3-array@${"3.2.4"}/+esm`);\n');
6689
});
6790
});
6891

6992
function resolve(path: string, specifier: string): string {
70-
if (!specifier.startsWith("/npm/")) return specifier;
71-
specifier = `/_npm/${specifier.slice("/npm/".length)}`;
72-
if (specifier.endsWith("/+esm")) specifier += ".js";
73-
return relativePath(path, specifier);
93+
return specifier.startsWith("/npm/") ? relativePath(path, fromJsDelivrPath(specifier)) : specifier;
7494
}

test/output/build/files/files.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<link rel="modulepreload" href="./_observablehq/client.js">
1010
<link rel="modulepreload" href="./_observablehq/runtime.js">
1111
<link rel="modulepreload" href="./_observablehq/stdlib.js">
12-
<link rel="modulepreload" href="./_npm/[email protected]/+esm.js">
12+
<link rel="modulepreload" href="./_npm/[email protected]/_esm.js">
1313
<script type="module">
1414

1515
import {define} from "./_observablehq/client.js";

test/output/build/files/subsection/subfiles.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<link rel="modulepreload" href="../_observablehq/client.js">
1010
<link rel="modulepreload" href="../_observablehq/runtime.js">
1111
<link rel="modulepreload" href="../_observablehq/stdlib.js">
12-
<link rel="modulepreload" href="../_npm/[email protected]/+esm.js">
12+
<link rel="modulepreload" href="../_npm/[email protected]/_esm.js">
1313
<script type="module">
1414

1515
import {define} from "../_observablehq/client.js";

test/output/build/imports/_import/foo/foo.821499d0.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import "../../_npm/[email protected]/+esm.js";
1+
import "../../_npm/[email protected]/_esm.js";
22
import {bar} from "../bar/bar.13bb8056.js";
33
export {top} from "../top.160847a6.js";
44

test/output/build/imports/foo/foo.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<link rel="modulepreload" href="../_observablehq/client.js">
1111
<link rel="modulepreload" href="../_observablehq/runtime.js">
1212
<link rel="modulepreload" href="../_observablehq/stdlib.js">
13-
<link rel="modulepreload" href="../_npm/[email protected]/+esm.js">
13+
<link rel="modulepreload" href="../_npm/[email protected]/_esm.js">
1414
<link rel="modulepreload" href="../_import/bar/bar.13bb8056.js">
1515
<link rel="modulepreload" href="../_import/top.160847a6.js">
1616
<link rel="modulepreload" href="../_import/foo/foo bar.b173d3de.js">
@@ -24,7 +24,7 @@
2424
registerFile("../top.js", {"name":"../top.js","mimeType":"text/javascript","path":"../_file/top.a53c5d5b.js"});
2525

2626
define({id: "261e010e", inputs: ["display","FileAttachment"], outputs: ["d3","bar","top"], body: async (display,FileAttachment) => {
27-
const [d3, {bar}, {top}] = await Promise.all([import("../_npm/[email protected]/+esm.js"), import("../_import/bar/bar.13bb8056.js"), import("../_import/top.160847a6.js")]);
27+
const [d3, {bar}, {top}] = await Promise.all([import("../_npm/[email protected]/_esm.js"), import("../_import/bar/bar.13bb8056.js"), import("../_import/top.160847a6.js")]);
2828

2929
display(bar);
3030
display(top);

0 commit comments

Comments
 (0)