From 04feffef5eb5b51f1d31022f583c4e35deb009d4 Mon Sep 17 00:00:00 2001 From: Luca Casonato Date: Mon, 5 Feb 2024 16:13:32 -0800 Subject: [PATCH] feat: support embedded import map expansion (#111) This supports the embedded import map expansion feature from Deno 1.40: https://deno.com/blog/v1.40#simpler-imports-in-denojson --- mod_test.ts | 27 ++++++++++++++++++++++ src/plugin_deno_resolver.ts | 7 +++++- src/shared.ts | 33 ++++++++++++++++++++++++++- src/shared_test.ts | 29 +++++++++++++++++++++++ testdata/config_inline_expansion.json | 6 +++++ testdata/mapped_jsr.js | 1 + 6 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 testdata/config_inline_expansion.json create mode 100644 testdata/mapped_jsr.js diff --git a/mod_test.ts b/mod_test.ts index e15c7e0..4d6bbd2 100644 --- a/mod_test.ts +++ b/mod_test.ts @@ -674,6 +674,33 @@ Deno.test("bundle config ref import map", async (t) => { }); }); +Deno.test("bundle config inline import map with expansion", async (t) => { + await testLoader(t, LOADERS, async (esbuild, loader) => { + const configPath = join( + Deno.cwd(), + "testdata", + "config_inline_expansion.json", + ); + const res = await esbuild.build({ + ...DEFAULT_OPTS, + plugins: [ + ...denoPlugins({ configPath, loader }), + ], + bundle: true, + platform: "neutral", + entryPoints: ["./testdata/mapped_jsr.js"], + }); + assertEquals(res.warnings, []); + assertEquals(res.errors, []); + assertEquals(res.outputFiles.length, 1); + const output = res.outputFiles[0]; + assertEquals(output.path, ""); + const dataURL = `data:application/javascript;base64,${btoa(output.text)}`; + const ns = await import(dataURL); + assertEquals(ns.join("a", "b"), join("a", "b")); + }); +}); + const COMPUTED_PLUGIN: esbuild.Plugin = { name: "computed", setup(build) { diff --git a/src/plugin_deno_resolver.ts b/src/plugin_deno_resolver.ts index 249dafd..d37292a 100644 --- a/src/plugin_deno_resolver.ts +++ b/src/plugin_deno_resolver.ts @@ -7,7 +7,11 @@ import { SpecifierMap, toFileUrl, } from "../deps.ts"; -import { readDenoConfig, urlToEsbuildResolution } from "./shared.ts"; +import { + expandEmbeddedImportMap, + readDenoConfig, + urlToEsbuildResolution, +} from "./shared.ts"; export type { ImportMap, Scopes, SpecifierMap }; @@ -76,6 +80,7 @@ export function denoResolverPlugin( imports: config.imports, scopes: config.scopes, } as ImportMap; + expandEmbeddedImportMap(configImportMap); importMap = resolveImportMap( configImportMap, toFileUrl(options.configPath), diff --git a/src/shared.ts b/src/shared.ts index bb37ada..a4e8816 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -1,4 +1,11 @@ -import { esbuild, extname, fromFileUrl, JSONC, toFileUrl } from "../deps.ts"; +import { + esbuild, + extname, + fromFileUrl, + ImportMap, + JSONC, + toFileUrl, +} from "../deps.ts"; import { MediaType } from "./deno.ts"; export interface Loader { @@ -325,3 +332,27 @@ export function parseJsrSpecifier(specifier: URL): JsrSpecifier { path: pathStartIndex === path.length ? null : path.slice(pathStartIndex), }; } + +// For all pairs in `imports` where the specifier does not end in a /, and the +// target starts with `jsr:` or `npm:`, and no entry exists for `${specifier}/`, +// add an entry for `${specifier}/` pointing to the target with a / appended, +// and a `/` appended to the scheme, if none is present there. +export function expandEmbeddedImportMap(importMap: ImportMap) { + if (importMap.imports !== undefined) { + const newImports: [string, string | null][] = []; + for (const [specifier, target] of Object.entries(importMap.imports)) { + newImports.push([specifier, target]); + if ( + !specifier.endsWith("/") && target && + (target.startsWith("jsr:") || target.startsWith("npm:")) && + !importMap.imports[specifier + "/"] + ) { + const newSpecifier = specifier + "/"; + const newTarget = target.slice(0, 4) + "/" + + target.slice(target[4] === "/" ? 5 : 4) + "/"; + newImports.push([newSpecifier, newTarget]); + } + } + importMap.imports = Object.fromEntries(newImports); + } +} diff --git a/src/shared_test.ts b/src/shared_test.ts index 4049571..e8cc52e 100644 --- a/src/shared_test.ts +++ b/src/shared_test.ts @@ -1,10 +1,12 @@ import { + expandEmbeddedImportMap, NpmSpecifier, parseJsrSpecifier, parseNpmSpecifier, } from "./shared.ts"; import { assertEquals, assertThrows } from "../test_deps.ts"; import { JsrSpecifier } from "./shared.ts"; +import { ImportMap } from "../deps.ts"; interface NpmSpecifierTestCase extends NpmSpecifier { specifier: string; @@ -190,3 +192,30 @@ Deno.test("parseJsrSpecifier", async (t) => { }); } }); + +Deno.test("expandEmbeddedImportMap", () => { + const importMap: ImportMap = { + imports: { + "@std/path": "jsr:@std/path@1.2.3", + "preact": "npm:preact@^10.0.0", + "preact2": "npm:/preact2@^10.0.0", + + "preact-render-to-string": "jsr:preact-render-to-string", + "preact-render-to-string/": "jsr:preact-render-to-string2/", + }, + }; + expandEmbeddedImportMap(importMap); + assertEquals(importMap, { + imports: { + "@std/path": "jsr:@std/path@1.2.3", + "@std/path/": "jsr:/@std/path@1.2.3/", + "preact": "npm:preact@^10.0.0", + "preact/": "npm:/preact@^10.0.0/", + "preact2": "npm:/preact2@^10.0.0", + "preact2/": "npm:/preact2@^10.0.0/", + + "preact-render-to-string": "jsr:preact-render-to-string", + "preact-render-to-string/": "jsr:preact-render-to-string2/", + }, + }); +}); diff --git a/testdata/config_inline_expansion.json b/testdata/config_inline_expansion.json new file mode 100644 index 0000000..9a14228 --- /dev/null +++ b/testdata/config_inline_expansion.json @@ -0,0 +1,6 @@ +{ + "lock": "./jsr/deno.lock", + "imports": { + "@std/path": "jsr:@std/path@^0.213" + } +} diff --git a/testdata/mapped_jsr.js b/testdata/mapped_jsr.js new file mode 100644 index 0000000..7448409 --- /dev/null +++ b/testdata/mapped_jsr.js @@ -0,0 +1 @@ +export * from "@std/path/join";