refactor(core): simplify plugin entrypoint resolution
This commit is contained in:
+20
-12
@@ -25,7 +25,10 @@ export interface EntryPoint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Interface {
|
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: (
|
readonly install: (
|
||||||
dir: string,
|
dir: string,
|
||||||
input?: {
|
input?: {
|
||||||
@@ -47,13 +50,18 @@ export function sanitize(pkg: string) {
|
|||||||
return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("")
|
return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("")
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolveEntryPoint = (name: string, dir: string): EntryPoint => {
|
const resolveEntryPoint = (name: string, dir: string, subpaths: readonly string[] = [""]): EntryPoint => {
|
||||||
let entrypoint: string | undefined
|
const entrypoint = subpaths
|
||||||
try {
|
.map((subpath) => {
|
||||||
entrypoint = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir)
|
try {
|
||||||
} catch {
|
return typeof Bun !== "undefined"
|
||||||
entrypoint = undefined
|
? import.meta.resolve([name, subpath].filter(Boolean).join("/"), dir)
|
||||||
}
|
: import.meta.resolve(dir)
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.find((entrypoint) => entrypoint !== undefined)
|
||||||
return {
|
return {
|
||||||
directory: dir,
|
directory: dir,
|
||||||
entrypoint,
|
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 dir = directory(pkg)
|
||||||
const name = (() => {
|
const name = (() => {
|
||||||
try {
|
try {
|
||||||
@@ -123,17 +131,17 @@ const layer = Layer.effect(
|
|||||||
})()
|
})()
|
||||||
|
|
||||||
if (yield* afs.existsSafe(path.join(dir, "node_modules", name))) {
|
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 tree = yield* reify({ dir, add: [pkg] })
|
||||||
const first = tree.edgesOut.values().next().value?.to
|
const first = tree.edgesOut.values().next().value?.to
|
||||||
if (!first) {
|
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
|
if (result.entrypoint) return result
|
||||||
return yield* new InstallFailedError({ add: [pkg], dir })
|
return yield* new InstallFailedError({ add: [pkg], dir })
|
||||||
}
|
}
|
||||||
return resolveEntryPoint(first.name, first.path)
|
return resolveEntryPoint(first.name, first.path, options?.subpaths)
|
||||||
}, Effect.scoped)
|
}, Effect.scoped)
|
||||||
|
|
||||||
const install: Interface["install"] = Effect.fn("Npm.install")(function* (dir, input) {
|
const install: Interface["install"] = Effect.fn("Npm.install")(function* (dir, input) {
|
||||||
|
|||||||
@@ -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 =
|
type Operation =
|
||||||
| {
|
| {
|
||||||
readonly type: "add"
|
readonly type: "add"
|
||||||
@@ -165,7 +159,7 @@ const load = Effect.fn("PluginSupervisor.load")(function* (operation: Extract<Op
|
|||||||
const npm = yield* Npm.Service
|
const npm = yield* Npm.Service
|
||||||
const entrypoint = path.isAbsolute(operation.target)
|
const entrypoint = path.isAbsolute(operation.target)
|
||||||
? pathToFileURL(operation.target).href
|
? pathToFileURL(operation.target).href
|
||||||
: (yield* npm.add(operation.target)).entrypoint
|
: (yield* npm.add(operation.target, { subpaths: ["server", ""] })).entrypoint
|
||||||
if (!entrypoint) return
|
if (!entrypoint) return
|
||||||
// Bun currently ignores query parameters when caching file:// imports.
|
// Bun currently ignores query parameters when caching file:// imports.
|
||||||
const source =
|
const source =
|
||||||
@@ -194,40 +188,10 @@ function discoverDirectory(fs: FSUtil.Interface, directory: string) {
|
|||||||
symlink: true,
|
symlink: true,
|
||||||
})
|
})
|
||||||
.pipe(Effect.orElseSucceed(() => []))
|
.pipe(Effect.orElseSucceed(() => []))
|
||||||
const directories = yield* fs
|
return files.sort().map((target): Operation => ({ type: "add", target, options: {} }))
|
||||||
.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: {} }))
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
export interface Interface {
|
||||||
/** Wait for the initial plugin generation and startup updates to settle. */
|
/** Wait for the initial plugin generation and startup updates to settle. */
|
||||||
readonly flush: Effect.Effect<void>
|
readonly flush: Effect.Effect<void>
|
||||||
|
|||||||
@@ -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"
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -134,7 +134,7 @@ describe("PluginSupervisor config", () => {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("loads auto-discovered plugin files and packages", () =>
|
it.live("loads auto-discovered plugin files", () =>
|
||||||
withLocation(
|
withLocation(
|
||||||
undefined,
|
undefined,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
@@ -143,9 +143,6 @@ describe("PluginSupervisor config", () => {
|
|||||||
expect(yield* agents.get(AgentV2.ID.make("directory"))).toMatchObject({
|
expect(yield* agents.get(AgentV2.ID.make("directory"))).toMatchObject({
|
||||||
description: "Loaded from plugin directory",
|
description: "Loaded from plugin directory",
|
||||||
})
|
})
|
||||||
expect(yield* agents.get(AgentV2.ID.make("folder"))).toMatchObject({
|
|
||||||
description: "Loaded from plugin folder",
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
@@ -195,7 +192,6 @@ describe("PluginSupervisor config", () => {
|
|||||||
yield* ready()
|
yield* ready()
|
||||||
const agents = yield* AgentV2.Service
|
const agents = yield* AgentV2.Service
|
||||||
expect(yield* agents.get(AgentV2.ID.make("directory"))).toBeUndefined()
|
expect(yield* agents.get(AgentV2.ID.make("directory"))).toBeUndefined()
|
||||||
expect(yield* agents.get(AgentV2.ID.make("folder"))).toBeUndefined()
|
|
||||||
}),
|
}),
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -41,19 +41,27 @@ describe("Npm.add", () => {
|
|||||||
await fs.mkdir(path.join(tmp.path, "fixture-provider"))
|
await fs.mkdir(path.join(tmp.path, "fixture-provider"))
|
||||||
await writePackage(path.join(tmp.path, "fixture-provider"), {
|
await writePackage(path.join(tmp.path, "fixture-provider"), {
|
||||||
name: "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", "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")}`
|
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 })
|
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
|
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)
|
}).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")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user