Skip to content

Commit 1c8e9dc

Browse files
mbostockFil
andauthored
rewrite dynamic imports in ojs (#170)
* rewrite dynamic imports in ojs * adopt getStringLiteralValue * more dynamic imports + tests (#171) * clarifies resolveNpmImport, fixes a bug (?) with `import("prettier/plugins/acorn.js")` * adopt resolveNpmImport * tests * revert change to resolveNpmImport * prettier * /+esm for .js import in ojs --------- Co-authored-by: Mike Bostock <mbostock@gmail.com> --------- Co-authored-by: Philippe Rivière <fil@rezo.net>
1 parent 85b699a commit 1c8e9dc

6 files changed

Lines changed: 223 additions & 56 deletions

File tree

src/javascript/__snapshots__/transpile.test.ts.snap

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,85 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

3+
exports[`ignores non-bare Observable JavaScript dynamic imports 1`] = `
4+
{
5+
"autodisplay": true,
6+
"automutable": false,
7+
"autoview": false,
8+
"body": "function(){return(
9+
import("./local.js")
10+
)}",
11+
"databases": Set {},
12+
"files": Set {},
13+
"inputs": [],
14+
"output": undefined,
15+
"secrets": Set {},
16+
}
17+
`;
18+
19+
exports[`ignores non-bare Observable JavaScript dynamic imports 2`] = `
20+
{
21+
"autodisplay": true,
22+
"automutable": false,
23+
"autoview": false,
24+
"body": "function(){return(
25+
import("../sibling.js")
26+
)}",
27+
"databases": Set {},
28+
"files": Set {},
29+
"inputs": [],
30+
"output": undefined,
31+
"secrets": Set {},
32+
}
33+
`;
34+
35+
exports[`ignores non-bare Observable JavaScript dynamic imports 3`] = `
36+
{
37+
"autodisplay": true,
38+
"automutable": false,
39+
"autoview": false,
40+
"body": "function(){return(
41+
import("/abs.js")
42+
)}",
43+
"databases": Set {},
44+
"files": Set {},
45+
"inputs": [],
46+
"output": undefined,
47+
"secrets": Set {},
48+
}
49+
`;
50+
51+
exports[`ignores non-bare Observable JavaScript dynamic imports 4`] = `
52+
{
53+
"autodisplay": true,
54+
"automutable": false,
55+
"autoview": false,
56+
"body": "function(){return(
57+
import("https://cdn.skypack.dev/canvas-confetti")
58+
)}",
59+
"databases": Set {},
60+
"files": Set {},
61+
"inputs": [],
62+
"output": undefined,
63+
"secrets": Set {},
64+
}
65+
`;
66+
67+
exports[`ignores non-bare Observable JavaScript dynamic imports 5`] = `
68+
{
69+
"autodisplay": true,
70+
"automutable": false,
71+
"autoview": false,
72+
"body": "function(){return(
73+
import("npm:d3")
74+
)}",
75+
"databases": Set {},
76+
"files": Set {},
77+
"inputs": [],
78+
"output": undefined,
79+
"secrets": Set {},
80+
}
81+
`;
82+
383
exports[`transpiles JavaScript expressions 1`] = `
484
{
585
"autodisplay": true,
@@ -129,6 +209,102 @@ await z;
129209
}
130210
`;
131211

212+
exports[`transpiles Observable JavaScript dynamic imports 1`] = `
213+
{
214+
"autodisplay": true,
215+
"automutable": false,
216+
"autoview": false,
217+
"body": "function(){return(
218+
import("https://cdn.jsdelivr.net/npm/d3/+esm")
219+
)}",
220+
"databases": Set {},
221+
"files": Set {},
222+
"inputs": [],
223+
"output": undefined,
224+
"secrets": Set {},
225+
}
226+
`;
227+
228+
exports[`transpiles Observable JavaScript dynamic imports 2`] = `
229+
{
230+
"autodisplay": true,
231+
"automutable": false,
232+
"autoview": false,
233+
"body": "function(){return(
234+
import("https://cdn.jsdelivr.net/npm/d3@7/+esm")
235+
)}",
236+
"databases": Set {},
237+
"files": Set {},
238+
"inputs": [],
239+
"output": undefined,
240+
"secrets": Set {},
241+
}
242+
`;
243+
244+
exports[`transpiles Observable JavaScript dynamic imports 3`] = `
245+
{
246+
"autodisplay": true,
247+
"automutable": false,
248+
"autoview": false,
249+
"body": "function(){return(
250+
import("https://cdn.jsdelivr.net/npm/@observablehq/plot/+esm")
251+
)}",
252+
"databases": Set {},
253+
"files": Set {},
254+
"inputs": [],
255+
"output": undefined,
256+
"secrets": Set {},
257+
}
258+
`;
259+
260+
exports[`transpiles Observable JavaScript dynamic imports 4`] = `
261+
{
262+
"autodisplay": true,
263+
"automutable": false,
264+
"autoview": false,
265+
"body": "function(){return(
266+
import("https://cdn.jsdelivr.net/npm/lodash/fp/+esm")
267+
)}",
268+
"databases": Set {},
269+
"files": Set {},
270+
"inputs": [],
271+
"output": undefined,
272+
"secrets": Set {},
273+
}
274+
`;
275+
276+
exports[`transpiles Observable JavaScript dynamic imports 5`] = `
277+
{
278+
"autodisplay": true,
279+
"automutable": false,
280+
"autoview": false,
281+
"body": "function(){return(
282+
import("https://cdn.jsdelivr.net/npm/three@0.150.1/examples/jsm/controls/OrbitControls.js/+esm")
283+
)}",
284+
"databases": Set {},
285+
"files": Set {},
286+
"inputs": [],
287+
"output": undefined,
288+
"secrets": Set {},
289+
}
290+
`;
291+
292+
exports[`transpiles Observable JavaScript dynamic imports 6`] = `
293+
{
294+
"autodisplay": true,
295+
"automutable": false,
296+
"autoview": false,
297+
"body": "function(){return(
298+
import("https://cdn.jsdelivr.net/npm/leaflet/dist/leaflet.css")
299+
)}",
300+
"databases": Set {},
301+
"files": Set {},
302+
"inputs": [],
303+
"output": undefined,
304+
"secrets": Set {},
305+
}
306+
`;
307+
132308
exports[`transpiles Observable JavaScript import-withs 1`] = `
133309
{
134310
"autodisplay": false,

src/javascript/features.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type {Node, CallExpression} from "acorn";
2-
import {getStringLiteralValue, isStringLiteral} from "./literal.js";
2+
import {getStringLiteralValue, isStringLiteral} from "./strings.js";
33
import {findReferences} from "./references.js";
44
import {syntaxError} from "./syntaxError.js";
55
import {simple} from "./walk.js";

src/javascript/imports/npm.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ test("adds /+esm as expected", () => {
3434
resolveNpmImport("npm:prettier/plugins/acorn.js/+esm"),
3535
"https://cdn.jsdelivr.net/npm/prettier/plugins/acorn.js/+esm" // preserve existing /+esm
3636
);
37+
assert.strictEqual(
38+
resolveNpmImport("npm:leaflet/dist/leaflet.css"),
39+
"https://cdn.jsdelivr.net/npm/leaflet/dist/leaflet.css" // no /+esm because non-JS file
40+
);
41+
assert.strictEqual(
42+
resolveNpmImport("npm:foo/data.json"),
43+
"https://cdn.jsdelivr.net/npm/foo/data.json" // no /+esm because non-JS file
44+
);
3745
assert.strictEqual(
3846
resolveNpmImport("npm:prettier/plugins/estree"),
3947
"https://cdn.jsdelivr.net/npm/prettier/plugins/estree/+esm"

src/javascript/literal.ts

Lines changed: 0 additions & 55 deletions
This file was deleted.

src/javascript/observable.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import {parseCell} from "@observablehq/parser";
44
import type {Identifier, ImportDeclaration, Node} from "acorn";
55
import {rewriteFileExpressions} from "./files.js";
66
import {flatMapImportSpecifiers, rewriteImportDeclarations} from "./imports.js";
7+
import {resolveNpmImport} from "./imports/npm.js";
78
import {Sourcemap} from "./sourcemap.js";
9+
import {getStringLiteralValue, isStringLiteral} from "./strings.js";
810
import type {TranspiledJavaScript, TranspileOptions} from "./transpile.js";
911
import {transpileJavaScript} from "./transpile.js";
1012
import {simple} from "./walk.js";
@@ -19,6 +21,7 @@ export function transpileObservable(
1921
if (cell.tag) throw new Error("tagged ojs cells are not supported");
2022
const output = new Sourcemap(input).trim();
2123
rewriteSpecialReferences(output, cell.body);
24+
rewriteDynamicImports(output, cell.body);
2225
if (options?.resolveFiles) rewriteFileExpressions(output, cell.body);
2326
const inputs = Array.from(new Set(cell.references.map(asReference)));
2427
let start = "";
@@ -92,6 +95,24 @@ function transformObservableImport(body: ImportDeclaration): void {
9295
];
9396
}
9497

98+
/**
99+
* Rewrite a bare dynamic import such as import("d3") to a CDN URL, like
100+
* import("https://cdn.jsdelivr.net/npm/d3/+esm"), using the same resolution as
101+
* static imports. Protocol (npm:, https:, …) and local imports are left alone.
102+
*/
103+
function rewriteDynamicImports(output: Sourcemap, body: Node): void {
104+
simple(body, {
105+
ImportExpression({source}) {
106+
if (!isStringLiteral(source)) return;
107+
let value = getStringLiteralValue(source);
108+
if (/^(\w+:|\.?\.?\/)/.test(value)) return;
109+
if (/\.(js|mjs|cjs)$/.test(value)) value += "/+esm";
110+
const resolution = resolveNpmImport(`npm:${value}`);
111+
output.replaceLeft(source.start, source.end, JSON.stringify(resolution));
112+
}
113+
});
114+
}
115+
95116
/** Rewrite viewof x ↦ viewof$x, and mutable x ↦ mutable$x.value. */
96117
function rewriteSpecialReferences(output: Sourcemap, body: Node): void {
97118
simple(body, {

src/javascript/transpile.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,23 @@ it("transpiles Observable JavaScript import-withs of views and mutables", () =>
8484
expect(transpile('import {color} with {foo, viewof bar, mutable baz as qux} from "2d6bf7be248d66f3"', "ojs")).toMatchSnapshot();
8585
});
8686

87+
it("transpiles Observable JavaScript dynamic imports", () => {
88+
expect(transpile('import("d3")', "ojs")).toMatchSnapshot();
89+
expect(transpile('import("d3@7")', "ojs")).toMatchSnapshot();
90+
expect(transpile('import("@observablehq/plot")', "ojs")).toMatchSnapshot();
91+
expect(transpile('import("lodash/fp")', "ojs")).toMatchSnapshot();
92+
expect(transpile('import("three@0.150.1/examples/jsm/controls/OrbitControls.js")', "ojs")).toMatchSnapshot();
93+
expect(transpile('import("leaflet/dist/leaflet.css")', "ojs")).toMatchSnapshot();
94+
});
95+
96+
it("ignores non-bare Observable JavaScript dynamic imports", () => {
97+
expect(transpile('import("./local.js")', "ojs")).toMatchSnapshot();
98+
expect(transpile('import("../sibling.js")', "ojs")).toMatchSnapshot();
99+
expect(transpile('import("/abs.js")', "ojs")).toMatchSnapshot();
100+
expect(transpile('import("https://cdn.skypack.dev/canvas-confetti")', "ojs")).toMatchSnapshot();
101+
expect(transpile('import("npm:d3")', "ojs")).toMatchSnapshot();
102+
});
103+
87104
it("transpiles import.meta.resolve", () => {
88105
expect(transpile('import.meta.resolve("npm:d3")', "js")).toMatchSnapshot();
89106
expect(transpile('import.meta.resolve("./test")', "js", {resolveLocalImports: true})).toMatchSnapshot();

0 commit comments

Comments
 (0)