Repository navigation
fix(cli): give plugins the host's Effect - #53422
Merged
Merged
Conversation
… use OpenTUI preserve
# ------------------------ >8 ------------------------ # Do not modify or remove the line above. # Everything below it will be ignored. # # Conflicts: # bun.lock # package.json # packages/core/test/plugin/module.test.ts # packages/plugin/package.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
When a server or TUI plugin (or one of its
node_modulesdependencies) resolves its own copy ofeffector@opencode/plugininstead of the host's runtime instances, the two copies cannot share module-private symbols, fiber internals, orSchemaAST sentinels. In practice, loading a plugin with its owneffectcopy produces four concrete runtime failures:Effect.logfrom the plugin's copy crashes inside a host fiber (TypeError: undefined is not an object (evaluating 'logLevel.toUpperCase')).Effect.runPromiseof a hostEffectcrashes (TypeError: fiber.succeedWith is not a function).Schema.withDecodingDefaultcrash when decoded by the host (TypeError: transformations[i] is not a function).effectversion reject valid inputs onSchema.Int,Schema.Trim, andSchema.isPatternchecks acrossToolRuntime.executeandRpc.call(Expected an integer).Following the prior art of name-based host-provided module tables in extension hosts (such as VS Code providing
vscodeand Atom providingatomby module name), OpenCode provides its hosteffectand@opencode/pluginruntime modules to plugins via Bun's nativebuild.moduletable. (Once oven-sh/bun#40398 lands so runtime resolution hooks apply uniformly across nestednode_modulesstatic imports, the per-specifierbuild.moduletable +build.onLoadguard can simplify further.)Supersedes #53296.
What Changes
node_moduleseffect, exportedeffect/*subpaths (effect/Option,effect/unstable/http,effect/testing, …)node_modules/effectwhen presenteffectmodule instance@opencode/plugin, exported@opencode/plugin/*subpaths (@opencode/plugin/effect,@opencode/plugin/rpc,@opencode/plugin/tui, …)node_modules/@opencode/pluginwhen present@opencode/pluginmodule instanceeffect/package.jsononLoadguard excludes.json)effect/*/@opencode/plugin/*module (e.g.effect/RemovedSubpath,effect/internal/*) present in plugin'snode_modulesCannot load "<pkg>/<path>" from plugin node_modules: "<pkg>/<path>" is not provided by OpenCodeHttpApiScalar/HttpApiSwaggerinline UI assets in compiled CLI binary"Scalar/Swagger UI assets are not bundled in OpenCode") without bundling the 5.1 MiB inline browser scriptsflowchart TD A["Plugin or transitive node_modules import"] --> B{"provides(specifier)?<br/>(effect, effect/*, @opencode/plugin, @opencode/plugin/*)"} B -- "Yes (preserved by OpenTUI 0.5.15 preserve: provides)" --> C["build.module(specifier)<br/>returns host module instance"] B -- "No" --> D{"Resolves to foreign<br/>node_modules/{effect,@opencode/plugin}/*.{js,mjs,cjs,ts,tsx}?"} D -- "Yes" --> E["build.onLoad guard throws loud load error<br/>(no silent Effect mixing)"] D -- "No (e.g. effect/package.json)" --> F["Native Bun loader"]Host Module Redirection
@opencode/plugin/runtime(packages/plugin/src/runtime.ts,packages/plugin/src/runtime-modules.ts): Owned by@opencode/plugin(the package that loads plugins viaHost.load).ensurePluginRuntime()runs once at CLI startup (packages/cli/src/index.ts) and in TUI plugin support (packages/tui/src/plugin/runtime-plugin-support.bun.ts), registering every hosteffect/*and@opencode/plugin/*specifier withbuild.module(specifier, ...)and exportingprovides(specifier)for direct use bysource.bun.tsandruntime-plugin-support.bun.ts(without anyglobalThissymbol).packages/plugin/src/runtime-modules.ts):discoverPluginRuntimeSpecifiers()derives the scanned source tree and file extension from the package's resolved root entrypoint (Bun.resolveSync(pkgName, from)), scans that tree (dist/**/*.jsorsrc/**/*.ts, excludinginternal/**,source.*, andruntime*), and resolves every candidate throughBun.resolveSync(specifier, from)so unexported files and stalepackages/plugin/distbuilds are ignored. Workspace@opencode/pluginpaths are resolved through theirnode_modules/@opencode/pluginsymlink (findNodeModulesDir) so OpenTUI'sisNodeModulesPathcheck does not wrap host workspace files in async rewrite loaders.preserve: providesintegration (packages/tui/src/plugin/runtime-plugin-support.bun.ts): Bumps@opentui/*to0.5.15and passespreserve: providesalongsideadditional: { "@opencode/plugin/tui": ... }toensureRuntimePluginSupport. OpenTUI leaveseffectand@opencode/pluginbare specifiers untouched and skips following them duringnode_modulesprescanning, so Bun'sbuild.moduletable resolves staticimport, dynamicimport(), and CJSrequire()directly without anyfile://or absolute-pathbuild.onResolveworkaround.node_modulesload guard: Because Bun's runtime resolver does not invoke JSbuild.onResolvehooks for nested static imports innode_modules,ensurePluginRuntime()installs a narrowbuild.onLoadguard matching foreignnode_modules/{effect,@opencode/plugin}and Bun auto-install cache ({effect,@opencode/plugin}@<ver>@@@<n>) JS/TS files while excluding the host's own package roots (separator-agnostic on Windows) and.jsonmanifests.packages/plugin/src/source.bun.ts):prepareSourcechecksprovides(item.path)before probingBun.resolveSyncso zero-install local plugins importingeffector@opencode/plugindo not register missing-package watchers onnode_modules/effectornode_modules/@opencode/plugin.packages/cli/script/build.ts): At compile time,pluginRuntimePluginswapspackages/plugin/src/runtime-modules.tsviabuild.onLoadwith the generatedloadRuntimeModulestable, routingeffect/<parent>/<member>lookups through their parent barrel (() => require(parent)[member]) only whenrequire(parentResolved)[member] === require(resolved)(preserving side-effect and non-re-exported subpaths across4.0.0-rc.112,4.0.0-rc.118, and4.0.0), and replacesinternal/httpApiScalar.jsandinternal/httpApiSwagger.jswith a 1-line notice stub while keepingeffect/testing/FastCheckreal.Scope
build.moduleis process-global and importer-blind, so plugins and all dependencies inside theirnode_modulesalways receive OpenCode's hosteffectinstance. Libraries inside a plugin'snode_modulesbuilt on anothereffectmajor (such as Effect 3) also receive the host's Effect 4 and are not supported (pinned by test and documented inpackages/plugin/README.mdandservices/www/src/docs/content/build/plugins/effect.mdx).effect/*and@opencode/plugin/*module paths fail loudly at plugin load rather than silently loading a second copy from the plugin'snode_modules.packages/cli/src/index.ts) and TUI runtime plugin support; SDK and Node embedders are unaffected.Verification
packages/core/test/plugin/module.test.ts, commenting outensurePluginRuntime()causes the plugin integration tests to fail withTypeError: undefined is not an object (evaluating 'logLevel.toUpperCase'), silentRemovedLegacySubpath.jsloading, and{ major: 3 }resolution; testing the pre-fixdiscoverPluginRuntimeSpecifierswithpackages/plugin/distpresent splits@opencode/plugin/effect(src/effect/index.ts) from@opencode/plugin/effect/plugin(dist/effect/plugin.js); testing pre-fixmember in require(parent)misrouteseffect/schema/SchemaJITCompiler/enable.packages/tui/src/plugin/runtime-plugin-support.bun.ts, omittingpreserve: providesfails TUI plugin.tsloads withCannot load "effect/dist/esm/index.js" from plugin node_modules; omittingfindNodeModulesDir/toLoadPathfailscreateRequire(import.meta.url)("@opencode/plugin/effect")withTypeError: require() async module ".../packages/plugin/src/effect/index.ts" is unsupported; omitting the"effect"pre-require fails standalone coldrequire("effect")withTypeError: require() async module "effect" is unsupported.darwin-arm64):dist/cli-darwin-arm64/bin/opencode serve --stdio --port=0AND launched the compiled TUI in a PTY (opencode --server=...) against a fixture plugin containing bothindex.ts(server) andtui.ts(TUI), a sabotagednode_modules/effect(4.0.0-rc.111), a foreignnode_modules/@opencode/plugin, atransitive-depusingSchema.withDecodingDefault, dynamicawait import("effect/Option")andawait import("@opencode/plugin/effect"), and a localhelper.tsimporting"zod"from the plugin'snode_modules/zod: both server and TUI plugins activated against the host'sEffect,Schema,Option.some,FastCheck,HttpApiScalar/HttpApiSwagger, and@opencode/pluginwhile resolving"zod"from the plugin's ownnode_modules/zodand failing the unprovided-subpath plugin cleanly at load.154,066,674 bytes(146.93 MiB) onorigin/v2→162,207,090 bytes(154.69 MiB,+7.76 MiBvs+25.00 MiBin fix(plugin): load Effect from the host for plugins #53296).opencode --version, 11-run median):53.69 msonorigin/v2→62.03 ms(58.26 msmin).