-
Notifications
You must be signed in to change notification settings - Fork 3
Fix local plugin loading to preserve OAuth assets #287
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -83,14 +83,18 @@ export namespace Plugin { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function isModuleResolutionError(err: Error): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return err.message?.includes("Cannot find module") || err.message?.includes("Cannot find package") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const state = Instance.state(async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const client = createOpencodeClient({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| baseUrl: "http://localhost:4096", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // @ts-ignore - fetch type incompatibility | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fetch: async (...args) => Server.App().fetch(...args), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const config = await Config.get() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const hooks = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const hooks: Hooks[] = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const input: PluginInput = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| client, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| project: Instance.project, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -106,6 +110,7 @@ export namespace Plugin { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (let plugin of plugins) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.info("loading plugin", { path: plugin }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let pluginUrl: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let localPluginPath: string | undefined | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!plugin.startsWith("file://")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const lastAtIndex = plugin.lastIndexOf("@") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const pkg = lastAtIndex > 0 ? plugin.substring(0, lastAtIndex) : plugin | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -122,14 +127,14 @@ export namespace Plugin { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Resolve relative file:// paths against the working directory | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const filePath = plugin.substring("file://".length) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(Instance.directory, filePath) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Bundle local plugins with their dependencies for compiled binary compatibility | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const bundledPath = await bundleLocalPlugin(absolutePath) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pluginUrl = pathToFileURL(bundledPath).href | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| localPluginPath = absolutePath | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pluginUrl = pathToFileURL(absolutePath).href | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const loadPluginModule = async (url: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Use dynamic import() with absolute file:// URLs for ES module compatibility | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // pathToFileURL ensures proper URL encoding regardless of import.meta.url context | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const mod = await import(pluginUrl) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const mod = await import(url) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Prevent duplicate initialization when plugins export the same function | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // as both a named export and default export (e.g., `export const X` and `export default X`). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Object.entries(mod) would return both entries pointing to the same function reference. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -141,10 +146,23 @@ export namespace Plugin { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| const init = await fn(input) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hooks.push(init) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await loadPluginModule(pluginUrl) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const err = e as Error | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (localPluginPath && isModuleResolutionError(err)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.warn("failed to load local plugin directly, bundling fallback", { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| plugin, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: err.message, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const bundledPath = await bundleLocalPlugin(localPluginPath) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await loadPluginModule(pathToFileURL(bundledPath).href) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+155
to
+162
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The second This means:
Suggested change
Prompt To Fix With AIThis is a comment left during a code review.
Path: packages/opencode/src/plugin/index.ts
Line: 155:162
Comment:
The second `loadPluginModule` call on line 161 is not wrapped in a try-catch block. If the bundled plugin fails to load (for any reason - not just module resolution errors), the error will not be caught, and it will propagate uncaught, bypassing the error logging and hint system below.
This means:
1. The error won't be logged with the helpful "Make sure all plugin dependencies are installed" hint
2. The entire plugin loading will fail instead of just this plugin
3. Debugging will be harder because the error won't go through the standardized error handling
```suggestion
if (localPluginPath && isModuleResolutionError(err)) {
log.warn("failed to load local plugin directly, bundling fallback", {
plugin,
error: err.message,
})
try {
const bundledPath = await bundleLocalPlugin(localPluginPath)
await loadPluginModule(pathToFileURL(bundledPath).href)
continue
} catch (bundleErr) {
const bErr = bundleErr as Error
log.error("failed to load bundled plugin", {
plugin,
error: bErr.message,
})
throw bErr
}
}
```
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Check for module resolution issues | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (err.message?.includes("Cannot find module") || err.message?.includes("Cannot find package")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isModuleResolutionError(err)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.error("failed to load plugin", { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| plugin, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: err.message, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -175,7 +193,7 @@ export namespace Plugin { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const hook of await state().then((x) => x.hooks)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fn = hook[name] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!fn) continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // @ts-expect-error if you feel adventurous, please fix the typing, make sure to bump the try-counter if you | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // @ts-ignore if you feel adventurous, please fix the typing, make sure to bump the try-counter if you | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // give up. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // try-counter: 2 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await fn(input, output) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+196
to
199
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changing from While both work, Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! Prompt To Fix With AIThis is a comment left during a code review.
Path: packages/opencode/src/plugin/index.ts
Line: 196:199
Comment:
Changing from `@ts-expect-error` to `@ts-ignore` makes the type checking less strict. `@ts-expect-error` will warn if there's no type error, helping catch when the typing issue is fixed. `@ts-ignore` will silently suppress errors even when the typing is correct.
While both work, `@ts-expect-error` is generally preferred because it provides better type safety feedback. Was there a specific reason for this change, or should we keep the stricter `@ts-expect-error`?
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -195,7 +213,7 @@ export namespace Plugin { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| const hooks = await state().then((x) => x.hooks) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const config = await Config.get() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const hook of hooks) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // @ts-expect-error this is because we haven't moved plugin to sdk v2 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // @ts-ignore this is because we haven't moved plugin to sdk v2 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same concern as the previous Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! Prompt To Fix With AIThis is a comment left during a code review.
Path: packages/opencode/src/plugin/index.ts
Line: 216:216
Comment:
Same concern as the previous `@ts-ignore` comment - `@ts-expect-error` would be stricter and provide better feedback when the typing is eventually fixed.
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await hook.config?.(config) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Bus.subscribeAll(async (input) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
isModuleResolutionErrorfunction uses string matching to detect module resolution errors. While this works for common cases, it could potentially match false positives where a plugin's error message contains these strings but isn't actually a module resolution error.Consider also checking the error type or code if available:
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI