fix: support scoped npm plugins (#3785)

Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
This commit is contained in:
Err
2025-11-04 09:15:01 -06:00
committed by GitHub
parent aec44abcf6
commit 6f0028644e
3 changed files with 63 additions and 2 deletions

2
.gitignore vendored
View File

@@ -11,3 +11,5 @@ playground
tmp tmp
dist dist
.turbo .turbo
**/.serena
.serena/

View File

@@ -34,8 +34,10 @@ export namespace Plugin {
for (let plugin of plugins) { for (let plugin of plugins) {
log.info("loading plugin", { path: plugin }) log.info("loading plugin", { path: plugin })
if (!plugin.startsWith("file://")) { if (!plugin.startsWith("file://")) {
const [pkg, version] = plugin.split("@") const lastAtIndex = plugin.lastIndexOf("@")
plugin = await BunProc.install(pkg, version ?? "latest") const pkg = lastAtIndex > 0 ? plugin.substring(0, lastAtIndex) : plugin
const version = lastAtIndex > 0 ? plugin.substring(lastAtIndex + 1) : "latest"
plugin = await BunProc.install(pkg, version)
} }
const mod = await import(plugin) const mod = await import(plugin)
for (const [_name, fn] of Object.entries<PluginInstance>(mod)) { for (const [_name, fn] of Object.entries<PluginInstance>(mod)) {

View File

@@ -4,6 +4,7 @@ import { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture" import { tmpdir } from "../fixture/fixture"
import path from "path" import path from "path"
import fs from "fs/promises" import fs from "fs/promises"
import { pathToFileURL } from "url"
test("loads config with defaults when no files exist", async () => { test("loads config with defaults when no files exist", async () => {
await using tmp = await tmpdir() await using tmp = await tmpdir()
@@ -350,3 +351,59 @@ test("gets config directories", async () => {
}, },
}) })
}) })
test("resolves scoped npm plugins in config", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
const pluginDir = path.join(dir, "node_modules", "@scope", "plugin")
await fs.mkdir(pluginDir, { recursive: true })
await Bun.write(
path.join(dir, "package.json"),
JSON.stringify({ name: "config-fixture", version: "1.0.0", type: "module" }, null, 2),
)
await Bun.write(
path.join(pluginDir, "package.json"),
JSON.stringify(
{
name: "@scope/plugin",
version: "1.0.0",
type: "module",
main: "./index.js",
},
null,
2,
),
)
await Bun.write(path.join(pluginDir, "index.js"), "export default {}\n")
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify(
{ $schema: "https://opencode.ai/config.json", plugin: ["@scope/plugin"] },
null,
2,
),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const config = await Config.get()
const pluginEntries = config.plugin ?? []
const baseUrl = pathToFileURL(path.join(tmp.path, "opencode.json")).href
const expected = import.meta.resolve("@scope/plugin", baseUrl)
expect(pluginEntries.includes(expected)).toBe(true)
const scopedEntry = pluginEntries.find((entry) => entry === expected)
expect(scopedEntry).toBeDefined()
expect(scopedEntry?.includes("/node_modules/@scope/plugin/")).toBe(true)
},
})
})