Skip to content

Commit ac096b5

Browse files
committed
Support import_map.json[c] in denops plugin directory
Now if the plugin has `import_map.json` or `import_map.jsonc` in the same directory with `main.ts`, it will be loaded automatically and used to resolve imports.
1 parent 1d0e08b commit ac096b5

File tree

6 files changed

+160
-2
lines changed

6 files changed

+160
-2
lines changed

denops/@denops-private/plugin.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
import type { Denops, Entrypoint } from "jsr:@denops/core@^7.0.0";
2+
import {
3+
type ImportMap,
4+
ImportMapImporter,
5+
isImportMap,
6+
loadImportMap,
7+
} from "jsr:@lambdalisue/import-map-importer@^0.3.0";
28
import { toFileUrl } from "jsr:@std/path@^1.0.2/to-file-url";
9+
import { join } from "jsr:@std/path@^1.0.2/join";
10+
import { dirname } from "jsr:@std/path@^1.0.2/dirname";
11+
import { parse } from "jsr:@std/[email protected]/parse";
12+
import { maybe } from "jsr:@core/unknownutil/maybe";
313

414
type PluginModule = {
515
main: Entrypoint;
@@ -26,10 +36,9 @@ export class Plugin {
2636
}
2737

2838
async #load(): Promise<void> {
29-
const suffix = createScriptSuffix(this.script);
3039
await emit(this.#denops, `DenopsSystemPluginPre:${this.name}`);
3140
try {
32-
const mod: PluginModule = await import(`${this.script}${suffix}`);
41+
const mod: PluginModule = await importPlugin(this.script);
3342
this.#disposable = await mod.main(this.#denops) ?? voidAsyncDisposable;
3443
} catch (e) {
3544
// Show a warning message when Deno module cache issue is detected
@@ -136,3 +145,39 @@ function isDenoCacheIssueError(e: unknown): boolean {
136145
}
137146
return false;
138147
}
148+
149+
async function tryLoadImportMap(
150+
scriptUrl: string,
151+
): Promise<ImportMap | undefined> {
152+
const PATTERNS = [
153+
"import_map.json",
154+
"import_map.jsonc",
155+
];
156+
// Convert file URL to path for file operations
157+
const scriptPath = new URL(scriptUrl).pathname;
158+
const parentDir = dirname(scriptPath);
159+
for (const pattern of PATTERNS) {
160+
const importMapPath = join(parentDir, pattern);
161+
try {
162+
return await loadImportMap(importMapPath);
163+
} catch (err: unknown) {
164+
if (err instanceof Deno.errors.NotFound) {
165+
// Ignore NotFound errors and try the next pattern
166+
continue;
167+
}
168+
throw err; // Rethrow other errors
169+
}
170+
}
171+
return undefined;
172+
}
173+
174+
async function importPlugin(script: string): Promise<PluginModule> {
175+
const suffix = createScriptSuffix(script);
176+
const importMap = await tryLoadImportMap(script);
177+
if (importMap) {
178+
const importer = new ImportMapImporter(importMap);
179+
return await importer.import<PluginModule>(`${script}${suffix}`);
180+
} else {
181+
return await import(`${script}${suffix}`);
182+
}
183+
}

denops/@denops-private/plugin_test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ const scriptInvalidConstraint = resolveTestDataURL(
2929
const scriptInvalidConstraint2 = resolveTestDataURL(
3030
"dummy_invalid_constraint_plugin2.ts",
3131
);
32+
const scriptWithImportMap = resolveTestDataURL(
33+
"with_import_map/plugin_with_import_map.ts",
34+
);
3235

3336
Deno.test("Plugin", async (t) => {
3437
const meta: Meta = {
@@ -490,4 +493,79 @@ Deno.test("Plugin", async (t) => {
490493
});
491494
});
492495
});
496+
497+
await t.step("import map support", async (t) => {
498+
await t.step("loads plugin with import_map.json", async () => {
499+
const denops = createDenops();
500+
using _denops_call = stub(denops, "call");
501+
using _denops_cmd = stub(denops, "cmd");
502+
503+
const plugin = new Plugin(denops, "test-plugin", scriptWithImportMap);
504+
505+
await plugin.waitLoaded();
506+
507+
// Should emit events
508+
assertSpyCalls(_denops_call, 2);
509+
assertSpyCall(_denops_call, 0, {
510+
args: [
511+
"denops#_internal#event#emit",
512+
"DenopsSystemPluginPre:test-plugin",
513+
],
514+
});
515+
assertSpyCall(_denops_call, 1, {
516+
args: [
517+
"denops#_internal#event#emit",
518+
"DenopsSystemPluginPost:test-plugin",
519+
],
520+
});
521+
522+
// Should call the plugin's main function
523+
assertSpyCalls(_denops_cmd, 1);
524+
assertSpyCall(_denops_cmd, 0, {
525+
args: ["echo 'Import map plugin initialized'"],
526+
});
527+
});
528+
529+
await t.step("plugin can use mapped imports", async () => {
530+
const denops = createDenops();
531+
using _denops_call = stub(denops, "call");
532+
using _denops_cmd = stub(denops, "cmd");
533+
534+
const plugin = new Plugin(denops, "test-plugin", scriptWithImportMap);
535+
await plugin.waitLoaded();
536+
537+
// Reset spy calls
538+
_denops_cmd.calls.length = 0;
539+
540+
// Call the dispatcher function
541+
const result = await plugin.call("test");
542+
543+
// Should execute the command with the message from the mapped import
544+
assertSpyCalls(_denops_cmd, 1);
545+
assertSpyCall(_denops_cmd, 0, {
546+
args: ["echo 'Import map works for test-plugin!'"],
547+
});
548+
549+
// Should return the greeting from the mapped import
550+
assertEquals(result, "Hello from mapped import!");
551+
});
552+
553+
await t.step("works without import map", async () => {
554+
const denops = createDenops();
555+
using _denops_call = stub(denops, "call");
556+
using _denops_cmd = stub(denops, "cmd");
557+
558+
// Use a regular plugin without import map
559+
const plugin = new Plugin(denops, "test-plugin", scriptValid);
560+
561+
await plugin.waitLoaded();
562+
563+
// Should load normally
564+
assertSpyCalls(_denops_call, 2);
565+
assertSpyCalls(_denops_cmd, 1);
566+
assertSpyCall(_denops_cmd, 0, {
567+
args: ["echo 'Hello, Denops!'"],
568+
});
569+
});
570+
});
493571
});
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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"imports": {
3+
"@test/": "../",
4+
"@test/lib": "../dummy_valid_plugin.ts",
5+
"@test/helper": "./helper.ts"
6+
}
7+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
// This is a JSONC import map with comments
3+
"imports": {
4+
"@test/": "../",
5+
// Map to a specific module
6+
"@test/lib": "../dummy_valid_plugin.ts",
7+
// Map to the helper module
8+
"@test/helper": "./helper.ts"
9+
}
10+
}
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 'Import map plugin initialized'");
13+
};

0 commit comments

Comments
 (0)