Kaynağa Gözat

refactor(core): simplify plugin entrypoint resolution

Dax Raad 1 ay önce
ebeveyn
işleme
2a08cd3b96

+ 20 - 12
packages/core/src/npm.ts

@@ -25,7 +25,10 @@ export interface EntryPoint {
 }
 
 export interface Interface {
-  readonly add: (pkg: string) => Effect.Effect<EntryPoint, InstallFailedError | EffectFlock.LockError>
+  readonly add: (
+    pkg: string,
+    options?: { readonly subpaths?: readonly string[] },
+  ) => Effect.Effect<EntryPoint, InstallFailedError | EffectFlock.LockError>
   readonly install: (
     dir: string,
     input?: {
@@ -47,13 +50,18 @@ export function sanitize(pkg: string) {
   return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("")
 }
 
-const resolveEntryPoint = (name: string, dir: string): EntryPoint => {
-  let entrypoint: string | undefined
-  try {
-    entrypoint = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir)
-  } catch {
-    entrypoint = undefined
-  }
+const resolveEntryPoint = (name: string, dir: string, subpaths: readonly string[] = [""]): EntryPoint => {
+  const entrypoint = subpaths
+    .map((subpath) => {
+      try {
+        return typeof Bun !== "undefined"
+          ? import.meta.resolve([name, subpath].filter(Boolean).join("/"), dir)
+          : import.meta.resolve(dir)
+      } catch {
+        return undefined
+      }
+    })
+    .find((entrypoint) => entrypoint !== undefined)
   return {
     directory: dir,
     entrypoint,
@@ -112,7 +120,7 @@ const layer = Layer.effect(
         }),
       )
 
-    const add = Effect.fn("Npm.add")(function* (pkg: string) {
+    const add = Effect.fn("Npm.add")(function* (pkg: string, options?: { readonly subpaths?: readonly string[] }) {
       const dir = directory(pkg)
       const name = (() => {
         try {
@@ -123,17 +131,17 @@ const layer = Layer.effect(
       })()
 
       if (yield* afs.existsSafe(path.join(dir, "node_modules", name))) {
-        return resolveEntryPoint(name, path.join(dir, "node_modules", name))
+        return resolveEntryPoint(name, path.join(dir, "node_modules", name), options?.subpaths)
       }
 
       const tree = yield* reify({ dir, add: [pkg] })
       const first = tree.edgesOut.values().next().value?.to
       if (!first) {
-        const result = resolveEntryPoint(name, path.join(dir, "node_modules", name))
+        const result = resolveEntryPoint(name, path.join(dir, "node_modules", name), options?.subpaths)
         if (result.entrypoint) return result
         return yield* new InstallFailedError({ add: [pkg], dir })
       }
-      return resolveEntryPoint(first.name, first.path)
+      return resolveEntryPoint(first.name, first.path, options?.subpaths)
     }, Effect.scoped)
 
     const install: Interface["install"] = Effect.fn("Npm.install")(function* (dir, input) {

+ 2 - 38
packages/core/src/plugin/supervisor.ts

@@ -54,12 +54,6 @@ const PluginModule = Schema.Struct({
   ]),
 })
 
-const PluginPackage = Schema.Struct({
-  exports: Schema.optional(Schema.Unknown),
-  main: Schema.optional(Schema.String),
-  module: Schema.optional(Schema.String),
-})
-
 type Operation =
   | {
       readonly type: "add"
@@ -165,7 +159,7 @@ const load = Effect.fn("PluginSupervisor.load")(function* (operation: Extract<Op
   const npm = yield* Npm.Service
   const entrypoint = path.isAbsolute(operation.target)
     ? pathToFileURL(operation.target).href
-    : (yield* npm.add(operation.target)).entrypoint
+    : (yield* npm.add(operation.target, { subpaths: ["server", ""] })).entrypoint
   if (!entrypoint) return
   // Bun currently ignores query parameters when caching file:// imports.
   const source =
@@ -194,40 +188,10 @@ function discoverDirectory(fs: FSUtil.Interface, directory: string) {
         symlink: true,
       })
       .pipe(Effect.orElseSucceed(() => []))
-    const directories = yield* fs
-      .glob("{plugin,plugins}/*", {
-        cwd: directory,
-        absolute: true,
-        include: "all",
-        dot: true,
-        symlink: true,
-      })
-      .pipe(
-        Effect.flatMap((items) => Effect.filter(items, (item) => fs.isDir(item), { concurrency: "unbounded" })),
-        Effect.orElseSucceed(() => []),
-      )
-    const packages = yield* Effect.forEach(directories.sort(), (directory) => resolvePackageEntrypoint(fs, directory), {
-      concurrency: "unbounded",
-    }).pipe(Effect.map((items) => items.filter((item): item is string => item !== undefined)))
-    return [...files.sort(), ...packages].map((target): Operation => ({ type: "add", target, options: {} }))
+    return files.sort().map((target): Operation => ({ type: "add", target, options: {} }))
   })
 }
 
-const resolvePackageEntrypoint = Effect.fnUntraced(function* (fs: FSUtil.Interface, directory: string) {
-  const pkg = yield* fs.readJson(path.join(directory, "package.json")).pipe(
-    Effect.flatMap(Schema.decodeUnknownEffect(PluginPackage)),
-    Effect.catch(() => Effect.succeed(undefined)),
-  )
-  const exported = typeof pkg?.exports === "string" ? pkg.exports : undefined
-  const entries = [exported, pkg?.module, pkg?.main, "index.ts", "index.js"]
-
-  return yield* Effect.forEach(entries, (entry) => {
-    if (!entry) return Effect.succeed(undefined)
-    const file = path.resolve(directory, entry)
-    return fs.isFile(file).pipe(Effect.map((exists) => (exists ? file : undefined)))
-  }).pipe(Effect.map((items) => items.find((item): item is string => item !== undefined)))
-})
-
 export interface Interface {
   /** Wait for the initial plugin generation and startup updates to settle. */
   readonly flush: Effect.Effect<void>

+ 0 - 13
packages/core/test/config/fixtures/plugins/folder-plugin/index.ts

@@ -1,13 +0,0 @@
-import { Plugin } from "@opencode-ai/plugin/v2"
-
-export default Plugin.define({
-  id: "folder-plugin",
-  setup: async (ctx) => {
-    await ctx.agent.transform((agents) => {
-      agents.update("folder", (agent) => {
-        agent.description = "Loaded from plugin folder"
-        agent.mode = "subagent"
-      })
-    })
-  },
-})

+ 1 - 5
packages/core/test/config/plugin.test.ts

@@ -134,7 +134,7 @@ describe("PluginSupervisor config", () => {
     ),
   )
 
-  it.live("loads auto-discovered plugin files and packages", () =>
+  it.live("loads auto-discovered plugin files", () =>
     withLocation(
       undefined,
       Effect.gen(function* () {
@@ -143,9 +143,6 @@ describe("PluginSupervisor config", () => {
         expect(yield* agents.get(AgentV2.ID.make("directory"))).toMatchObject({
           description: "Loaded from plugin directory",
         })
-        expect(yield* agents.get(AgentV2.ID.make("folder"))).toMatchObject({
-          description: "Loaded from plugin folder",
-        })
       }),
       true,
     ),
@@ -195,7 +192,6 @@ describe("PluginSupervisor config", () => {
         yield* ready()
         const agents = yield* AgentV2.Service
         expect(yield* agents.get(AgentV2.ID.make("directory"))).toBeUndefined()
-        expect(yield* agents.get(AgentV2.ID.make("folder"))).toBeUndefined()
       }),
       true,
     ),

+ 12 - 4
packages/core/test/npm.test.ts

@@ -41,19 +41,27 @@ describe("Npm.add", () => {
     await fs.mkdir(path.join(tmp.path, "fixture-provider"))
     await writePackage(path.join(tmp.path, "fixture-provider"), {
       name: "fixture-provider",
-      main: "index.js",
+      exports: {
+        ".": "./index.js",
+        "./tui": "./tui.js",
+      },
     })
     await Bun.write(path.join(tmp.path, "fixture-provider", "index.js"), "export const fixture = true\n")
+    await Bun.write(path.join(tmp.path, "fixture-provider", "tui.js"), "export const tui = true\n")
 
     const spec = `fixture-provider@file:${path.join(tmp.path, "fixture-provider")}`
     await fs.mkdir(path.join(tmp.path, "cache", "packages", Npm.sanitize(spec)), { recursive: true })
 
-    const entry = await Effect.gen(function* () {
+    const entries = await Effect.gen(function* () {
       const npm = yield* Npm.Service
-      return yield* npm.add(spec)
+      return {
+        tui: yield* npm.add(spec, { subpaths: ["tui", ""] }),
+        fallback: yield* npm.add(spec, { subpaths: ["missing", ""] }),
+      }
     }).pipe(Effect.scoped, Effect.provide(npmLayer(path.join(tmp.path, "cache"))), Effect.runPromise)
 
-    expect(entry.entrypoint).toBeDefined()
+    expect(entries.tui.entrypoint).toEndWith("/tui.js")
+    expect(entries.fallback.entrypoint).toEndWith("/index.js")
   })
 })