summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorLuke Parker <[email protected]>2026-03-31 09:21:17 +1000
committerGitHub <[email protected]>2026-03-31 09:21:17 +1000
commit1de06452d39872e980ca3992a1aca9b87b4469ac (patch)
treef45028425a30b44f391b34a99a44090b7269eb0e
parent58f60629a1975fb67cedcbc5a2e0ca2322da3d68 (diff)
downloadopencode-1de06452d39872e980ca3992a1aca9b87b4469ac.tar.gz
opencode-1de06452d39872e980ca3992a1aca9b87b4469ac.zip
fix(plugin): properly resolve entrypoints without leading dot (#20140)
-rw-r--r--packages/opencode/src/plugin/shared.ts6
-rw-r--r--packages/opencode/test/plugin/loader-shared.test.ts111
2 files changed, 114 insertions, 3 deletions
diff --git a/packages/opencode/src/plugin/shared.ts b/packages/opencode/src/plugin/shared.ts
index 190d73301..116519143 100644
--- a/packages/opencode/src/plugin/shared.ts
+++ b/packages/opencode/src/plugin/shared.ts
@@ -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) {
diff --git a/packages/opencode/test/plugin/loader-shared.test.ts b/packages/opencode/test/plugin/loader-shared.test.ts
index d9ffa3950..ebc8daa24 100644
--- a/packages/opencode/test/plugin/loader-shared.test.ts
+++ b/packages/opencode/test/plugin/loader-shared.test.ts
@@ -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: ["[email protected]"] }, 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: ["[email protected]"] }, 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) => {