Skip to content

Commit 1de0645

Browse files
authored
fix(plugin): properly resolve entrypoints without leading dot (#20140)
1 parent 58f6062 commit 1de0645

2 files changed

Lines changed: 114 additions & 3 deletions

File tree

packages/opencode/src/plugin/shared.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ export function pluginSource(spec: string): PluginSource {
4545
}
4646

4747
function resolveExportPath(raw: string, dir: string) {
48-
if (raw.startsWith("./") || raw.startsWith("../")) return path.resolve(dir, raw)
4948
if (raw.startsWith("file://")) return fileURLToPath(raw)
50-
return raw
49+
if (path.isAbsolute(raw)) return raw
50+
return path.resolve(dir, raw)
5151
}
5252

5353
function extractExportValue(value: unknown): string | undefined {
@@ -93,7 +93,7 @@ function resolvePackageEntrypoint(spec: string, kind: PluginKind, pkg: PluginPac
9393

9494
function targetPath(target: string) {
9595
if (target.startsWith("file://")) return fileURLToPath(target)
96-
if (path.isAbsolute(target) || /^[A-Za-z]:[\\/]/.test(target)) return target
96+
if (path.isAbsolute(target)) return target
9797
}
9898

9999
async function resolveDirectoryIndex(dir: string) {

packages/opencode/test/plugin/loader-shared.test.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,117 @@ describe("plugin.loader.shared", () => {
331331
}
332332
})
333333

334+
test("loads npm server plugin from package server export without leading dot", async () => {
335+
await using tmp = await tmpdir({
336+
init: async (dir) => {
337+
const mod = path.join(dir, "mods", "acme-plugin")
338+
const dist = path.join(mod, "dist")
339+
const mark = path.join(dir, "server-called.txt")
340+
await fs.mkdir(dist, { recursive: true })
341+
342+
await Bun.write(
343+
path.join(mod, "package.json"),
344+
JSON.stringify(
345+
{
346+
name: "acme-plugin",
347+
type: "module",
348+
exports: {
349+
".": "./index.js",
350+
"./server": "dist/server.js",
351+
},
352+
},
353+
null,
354+
2,
355+
),
356+
)
357+
await Bun.write(path.join(mod, "index.js"), 'import "./main-throws.js"\nexport default {}\n')
358+
await Bun.write(path.join(mod, "main-throws.js"), 'throw new Error("main loaded")\n')
359+
await Bun.write(
360+
path.join(dist, "server.js"),
361+
[
362+
"export default {",
363+
" server: async () => {",
364+
` await Bun.write(${JSON.stringify(mark)}, "called")`,
365+
" return {}",
366+
" },",
367+
"}",
368+
"",
369+
].join("\n"),
370+
)
371+
372+
await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ plugin: ["acme-plugin@1.0.0"] }, null, 2))
373+
374+
return {
375+
mod,
376+
mark,
377+
}
378+
},
379+
})
380+
381+
const install = spyOn(BunProc, "install").mockResolvedValue(tmp.extra.mod)
382+
383+
try {
384+
const errors = await errs(tmp.path)
385+
expect(errors).toHaveLength(0)
386+
expect(await Bun.file(tmp.extra.mark).text()).toBe("called")
387+
} finally {
388+
install.mockRestore()
389+
}
390+
})
391+
392+
test("loads npm server plugin from package main without leading dot", async () => {
393+
await using tmp = await tmpdir({
394+
init: async (dir) => {
395+
const mod = path.join(dir, "mods", "acme-plugin")
396+
const dist = path.join(mod, "dist")
397+
const mark = path.join(dir, "main-called.txt")
398+
await fs.mkdir(dist, { recursive: true })
399+
400+
await Bun.write(
401+
path.join(mod, "package.json"),
402+
JSON.stringify(
403+
{
404+
name: "acme-plugin",
405+
type: "module",
406+
main: "dist/index.js",
407+
},
408+
null,
409+
2,
410+
),
411+
)
412+
await Bun.write(
413+
path.join(dist, "index.js"),
414+
[
415+
"export default {",
416+
" server: async () => {",
417+
` await Bun.write(${JSON.stringify(mark)}, "called")`,
418+
" return {}",
419+
" },",
420+
"}",
421+
"",
422+
].join("\n"),
423+
)
424+
425+
await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ plugin: ["acme-plugin@1.0.0"] }, null, 2))
426+
427+
return {
428+
mod,
429+
mark,
430+
}
431+
},
432+
})
433+
434+
const install = spyOn(BunProc, "install").mockResolvedValue(tmp.extra.mod)
435+
436+
try {
437+
const errors = await errs(tmp.path)
438+
expect(errors).toHaveLength(0)
439+
expect(await Bun.file(tmp.extra.mark).text()).toBe("called")
440+
} finally {
441+
install.mockRestore()
442+
}
443+
})
444+
334445
test("does not use npm package exports dot for server entry", async () => {
335446
await using tmp = await tmpdir({
336447
init: async (dir) => {

0 commit comments

Comments
 (0)