Explorar o código

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

Luke Parker hai 5 meses
pai
achega
1de06452d3

+ 3 - 3
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) {

+ 111 - 0
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: ["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) => {