fix(plugin): properly resolve entrypoints without leading dot (#20140)

pull/20159/head
Luke Parker 2026-03-31 09:21:17 +10:00 committed by GitHub
parent 58f60629a1
commit 1de06452d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 114 additions and 3 deletions

View File

@ -45,9 +45,9 @@ export function pluginSource(spec: string): PluginSource {
}
function resolveExportPath(raw: string, dir: string) {
if (raw.startsWith("./") || raw.startsWith("../")) return path.resolve(dir, raw)
if (raw.startsWith("file://")) return fileURLToPath(raw)
return raw
if (path.isAbsolute(raw)) return raw
return path.resolve(dir, raw)
}
function extractExportValue(value: unknown): string | undefined {
@ -93,7 +93,7 @@ function resolvePackageEntrypoint(spec: string, kind: PluginKind, pkg: PluginPac
function targetPath(target: string) {
if (target.startsWith("file://")) return fileURLToPath(target)
if (path.isAbsolute(target) || /^[A-Za-z]:[\\/]/.test(target)) return target
if (path.isAbsolute(target)) return target
}
async function resolveDirectoryIndex(dir: string) {

View File

@ -331,6 +331,117 @@ describe("plugin.loader.shared", () => {
}
})
test("loads npm server plugin from package server export without leading dot", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
const mod = path.join(dir, "mods", "acme-plugin")
const dist = path.join(mod, "dist")
const mark = path.join(dir, "server-called.txt")
await fs.mkdir(dist, { recursive: true })
await Bun.write(
path.join(mod, "package.json"),
JSON.stringify(
{
name: "acme-plugin",
type: "module",
exports: {
".": "./index.js",
"./server": "dist/server.js",
},
},
null,
2,
),
)
await Bun.write(path.join(mod, "index.js"), 'import "./main-throws.js"\nexport default {}\n')
await Bun.write(path.join(mod, "main-throws.js"), 'throw new Error("main loaded")\n')
await Bun.write(
path.join(dist, "server.js"),
[
"export default {",
" server: async () => {",
` await Bun.write(${JSON.stringify(mark)}, "called")`,
" return {}",
" },",
"}",
"",
].join("\n"),
)
await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ plugin: ["acme-plugin@1.0.0"] }, null, 2))
return {
mod,
mark,
}
},
})
const install = spyOn(BunProc, "install").mockResolvedValue(tmp.extra.mod)
try {
const errors = await errs(tmp.path)
expect(errors).toHaveLength(0)
expect(await Bun.file(tmp.extra.mark).text()).toBe("called")
} finally {
install.mockRestore()
}
})
test("loads npm server plugin from package main without leading dot", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
const mod = path.join(dir, "mods", "acme-plugin")
const dist = path.join(mod, "dist")
const mark = path.join(dir, "main-called.txt")
await fs.mkdir(dist, { recursive: true })
await Bun.write(
path.join(mod, "package.json"),
JSON.stringify(
{
name: "acme-plugin",
type: "module",
main: "dist/index.js",
},
null,
2,
),
)
await Bun.write(
path.join(dist, "index.js"),
[
"export default {",
" server: async () => {",
` await Bun.write(${JSON.stringify(mark)}, "called")`,
" return {}",
" },",
"}",
"",
].join("\n"),
)
await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ plugin: ["acme-plugin@1.0.0"] }, null, 2))
return {
mod,
mark,
}
},
})
const install = spyOn(BunProc, "install").mockResolvedValue(tmp.extra.mod)
try {
const errors = await errs(tmp.path)
expect(errors).toHaveLength(0)
expect(await Bun.file(tmp.extra.mark).text()).toBe("called")
} finally {
install.mockRestore()
}
})
test("does not use npm package exports dot for server entry", async () => {
await using tmp = await tmpdir({
init: async (dir) => {