Skip to content

Commit 3adcbdc

Browse files
committed
👍 Add support for importMap property
This adds support for the `importMap` property in deno configuration files. When a deno.json or deno.jsonc file with an `importMap` property, the specified import map file will be loaded and used for module resolution.
1 parent 66f333f commit 3adcbdc

File tree

12 files changed

+190
-9
lines changed

12 files changed

+190
-9
lines changed

deno.jsonc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
"exclude": [
1616
".coverage/",
1717
"tests/denops/testdata/no_check/",
18+
"tests/denops/testdata/with_deno_json/",
19+
"tests/denops/testdata/with_deno_json2/",
1820
"tests/denops/testdata/with_import_map/"
1921
],
2022
"imports": {

denops/@denops-private/plugin.ts

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
import { isObjectOf } from "@core/unknownutil/is/object-of";
2+
import { isString } from "@core/unknownutil/is/string";
3+
import { isUndefined } from "@core/unknownutil/is/undefined";
14
import type { Denops, Entrypoint } from "@denops/core";
25
import {
36
type ImportMap,
47
ImportMapImporter,
58
isImportMap,
69
loadImportMap,
710
} from "@lambdalisue/import-map-importer";
8-
import { ensure } from "@core/unknownutil";
911
import { toFileUrl } from "@std/path/to-file-url";
1012
import { fromFileUrl } from "@std/path/from-file-url";
1113
import { parse as parseJsonc } from "@std/jsonc";
@@ -153,6 +155,19 @@ function isDenoCacheIssueError(e: unknown): boolean {
153155
return false;
154156
}
155157

158+
async function loadJson(fileUrl: URL): Promise<unknown> {
159+
const content = await Deno.readTextFile(fileUrl);
160+
// Always parse as JSONC to be more permissive
161+
return parseJsonc(content);
162+
}
163+
164+
const hasImportMapProperty = isObjectOf({
165+
importMap: isString,
166+
// If `imports` or `scopes` exists, they will be override `importMap`
167+
imports: isUndefined,
168+
scopes: isUndefined,
169+
});
170+
156171
async function tryLoadImportMap(
157172
scriptUrl: URL,
158173
): Promise<ImportMap | undefined> {
@@ -167,22 +182,37 @@ async function tryLoadImportMap(
167182
"import_map.jsonc",
168183
];
169184
for (const pattern of PATTERNS) {
170-
const importMapUrl = new URL(pattern, scriptUrl);
171-
const importMapPath = fromFileUrl(importMapUrl);
185+
let importMapUrl = new URL(pattern, scriptUrl);
186+
187+
// Try to load the import map or deno configuration file
188+
let jsonValue: unknown;
172189
try {
173-
return await loadImportMap(importMapPath, {
174-
loader: (path: string) => {
175-
const content = Deno.readTextFileSync(path);
176-
return ensure(parseJsonc(content), isImportMap);
177-
},
178-
});
190+
jsonValue = await loadJson(importMapUrl);
179191
} catch (err: unknown) {
180192
if (err instanceof Deno.errors.NotFound) {
181193
// Ignore NotFound errors and try the next pattern
182194
continue;
183195
}
184196
throw err; // Rethrow other errors
185197
}
198+
199+
// Resolve import map path in the deno configuration and load it
200+
if (
201+
/\/deno\.jsonc?$/.test(importMapUrl.pathname) &&
202+
hasImportMapProperty(jsonValue)
203+
) {
204+
importMapUrl = new URL(jsonValue.importMap, importMapUrl);
205+
jsonValue = await loadJson(importMapUrl);
206+
}
207+
208+
// Resolve relative paths in the import map and return it
209+
if (isImportMap(jsonValue)) {
210+
const importMapPath = fromFileUrl(importMapUrl);
211+
return await loadImportMap(importMapPath, { loader: () => jsonValue });
212+
}
213+
214+
// If the file exists but has no import map, do not try the next pattern
215+
break;
186216
}
187217
return undefined;
188218
}

denops/@denops-private/plugin_test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ const scriptInvalidConstraint2 = resolveTestDataURL(
2727
const scriptWithImportMap = resolveTestDataURL(
2828
"with_import_map/plugin_with_import_map.ts",
2929
);
30+
const scriptWithDenoJson = resolveTestDataURL(
31+
"with_deno_json/plugin_with_deno_json.ts",
32+
);
33+
const scriptWithDenoJson2 = resolveTestDataURL(
34+
"with_deno_json2/plugin_with_deno_json.ts",
35+
);
3036

3137
Deno.test("Plugin", async (t) => {
3238
const meta: Meta = {
@@ -563,4 +569,85 @@ Deno.test("Plugin", async (t) => {
563569
});
564570
});
565571
});
572+
573+
await t.step("importMap property support", async (t) => {
574+
await t.step("loads plugin with deno.json", async () => {
575+
const denops = createDenops();
576+
using _denops_call = stub(denops, "call");
577+
using _denops_cmd = stub(denops, "cmd");
578+
579+
const plugin = new Plugin(denops, "test-plugin", scriptWithDenoJson);
580+
581+
await plugin.waitLoaded();
582+
583+
// Should emit events
584+
assertSpyCalls(_denops_call, 2);
585+
assertSpyCall(_denops_call, 0, {
586+
args: [
587+
"denops#_internal#event#emit",
588+
"DenopsSystemPluginPre:test-plugin",
589+
],
590+
});
591+
assertSpyCall(_denops_call, 1, {
592+
args: [
593+
"denops#_internal#event#emit",
594+
"DenopsSystemPluginPost:test-plugin",
595+
],
596+
});
597+
598+
// Should call the plugin's main function
599+
assertSpyCalls(_denops_cmd, 1);
600+
assertSpyCall(_denops_cmd, 0, {
601+
args: ["echo 'Deno json plugin initialized'"],
602+
});
603+
});
604+
605+
await t.step("plugin can use mapped imports", async () => {
606+
const denops = createDenops();
607+
using _denops_call = stub(denops, "call");
608+
using _denops_cmd = stub(denops, "cmd");
609+
610+
const plugin = new Plugin(denops, "test-plugin", scriptWithDenoJson);
611+
await plugin.waitLoaded();
612+
613+
// Reset spy calls
614+
_denops_cmd.calls.length = 0;
615+
616+
// Call the dispatcher function
617+
const result = await plugin.call("test");
618+
619+
// Should execute the command with the message from the mapped import
620+
assertSpyCalls(_denops_cmd, 1);
621+
assertSpyCall(_denops_cmd, 0, {
622+
args: ["echo 'Relative import map works for test-plugin!'"],
623+
});
624+
625+
// Should return the greeting from the mapped import
626+
assertEquals(result, "Hello from relative import map!");
627+
});
628+
629+
await t.step("importMap is overridden by imports", async () => {
630+
const denops = createDenops();
631+
using _denops_call = stub(denops, "call");
632+
using _denops_cmd = stub(denops, "cmd");
633+
634+
const plugin = new Plugin(denops, "test-plugin", scriptWithDenoJson2);
635+
await plugin.waitLoaded();
636+
637+
// Reset spy calls
638+
_denops_cmd.calls.length = 0;
639+
640+
// Call the dispatcher function
641+
const result = await plugin.call("test");
642+
643+
// Should execute the command with the message from the mapped import
644+
assertSpyCalls(_denops_cmd, 1);
645+
assertSpyCall(_denops_cmd, 0, {
646+
args: ["echo 'Import map works for test-plugin!'"],
647+
});
648+
649+
// Should return the greeting from the mapped import
650+
assertEquals(result, "Hello from mapped import!");
651+
});
652+
});
566653
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"importMap": "./other_path/other_name.json"
3+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const greeting = "Hello from relative import map!";
2+
3+
export function getMessage(name: string): string {
4+
return `Relative import map works for ${name}!`;
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"imports": {
3+
"@test/helper": "./helper.ts"
4+
}
5+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { Entrypoint } from "jsr:@denops/core@^7.0.0";
2+
import { getMessage, greeting } from "@test/helper";
3+
4+
export const main: Entrypoint = async (denops) => {
5+
denops.dispatcher = {
6+
test: async () => {
7+
const message = getMessage("test-plugin");
8+
await denops.cmd(`echo '${message}'`);
9+
return greeting;
10+
},
11+
};
12+
await denops.cmd("echo 'Deno json plugin initialized'");
13+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
// `importMap` is defined, but ...
3+
"importMap": "./other_path/other_name.json",
4+
// `imports` is defined, so it will override the `importMap`
5+
"imports": {
6+
"@test/helper": "./helper.ts"
7+
}
8+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const greeting = "Hello from mapped import!";
2+
3+
export function getMessage(name: string): string {
4+
return `Import map works for ${name}!`;
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const greeting = "Hello from relative import map!";
2+
3+
export function getMessage(name: string): string {
4+
return `Relative import map works for ${name}!`;
5+
}

0 commit comments

Comments
 (0)