vite.node.config.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. import path from "node:path"
  2. import { readFile } from "node:fs/promises"
  3. import { createRequire } from "node:module"
  4. import { defineConfig, type Plugin, type UserConfig } from "vite"
  5. import solid from "vite-plugin-solid"
  6. import { nodeExecArgv, nodeTarget, type NodeTarget, photonWasmAsset, shellParserWasmAssets } from "./src/node/target"
  7. import { verifySimulationGraph } from "./script/verify-artifact"
  8. const dir = import.meta.dirname
  9. function rawTextPlugin(): Plugin {
  10. return {
  11. name: "opencode:raw-text",
  12. // "pre" is load-bearing for .txt: Vite's built-in asset plugin claims
  13. // known asset types (.txt among them) ahead of normal-priority plugins,
  14. // replacing the import with an asset URL string instead of the content.
  15. // .md only ever worked without it because .md is not a known asset type.
  16. enforce: "pre",
  17. async load(id) {
  18. if (!id.endsWith(".md") && !id.endsWith(".txt")) return
  19. return `export default ${JSON.stringify(await readFile(id, "utf8"))}`
  20. },
  21. }
  22. }
  23. function appAssetsPlugin(archive: string): Plugin {
  24. return {
  25. name: "opencode:app-assets",
  26. resolveId(id) {
  27. if (id === "virtual:opencode-app-assets") return "\0virtual:opencode-app-assets"
  28. },
  29. load(id) {
  30. if (id !== "\0virtual:opencode-app-assets") return
  31. return `export default ${JSON.stringify(archive)}`
  32. },
  33. }
  34. }
  35. function runtimeRequirePlugin(): Plugin {
  36. return {
  37. name: "opencode:runtime-require",
  38. enforce: "pre",
  39. transform(code, id) {
  40. if (!id.endsWith("turndown/lib/turndown.es.js")) return
  41. const transformed = code.replace(" var domino = require('@mixmark-io/domino');", "")
  42. if (transformed === code) this.error("Failed to rewrite Turndown's Domino require")
  43. return `import domino from "@mixmark-io/domino"\n${transformed}`
  44. },
  45. }
  46. }
  47. function simulationGraphPlugin(): Plugin {
  48. return {
  49. name: "opencode:simulation-graph",
  50. generateBundle() {
  51. verifySimulationGraph(this.getModuleIds())
  52. },
  53. }
  54. }
  55. function fffNodePlugin(): Plugin {
  56. return {
  57. name: "opencode:fff-node",
  58. enforce: "pre",
  59. transform(code, id) {
  60. const normalized = id.replaceAll("\\", "/")
  61. if (normalized.endsWith("/ffi-rs/index.js")) {
  62. const start = code.indexOf("if (!nativeBinding) {")
  63. if (start === -1) this.error("Failed to rewrite ffi-rs native binding loader")
  64. return `const unavailable = () => { throw new Error("ffi-rs native binding unavailable") }
  65. const nativeBinding = globalThis.__OPENCODE_FFF_FFI ?? {
  66. DataType: new Proxy({}, { get: (target, key) => target[key] ?? key }),
  67. PointerType: {},
  68. FFITypeTag: {},
  69. open: unavailable,
  70. close: unavailable,
  71. load: unavailable,
  72. isNullPointer: unavailable,
  73. createPointer: unavailable,
  74. restorePointer: unavailable,
  75. unwrapPointer: unavailable,
  76. wrapPointer: unavailable,
  77. freePointer: unavailable,
  78. }
  79. const loadError = undefined
  80. ${code.slice(start)}`
  81. }
  82. if (!normalized.endsWith("/fff-node/dist/src/binary.js")) return
  83. const transformed = code.replace(
  84. "export function findBinary() {",
  85. "export function findBinary() { if (process.env.FFF_BINARY_PATH) return process.env.FFF_BINARY_PATH;",
  86. )
  87. if (transformed === code) this.error("Failed to rewrite FFF binary loader")
  88. return transformed
  89. },
  90. }
  91. }
  92. const resolve = {
  93. alias: [
  94. { find: /^solid-js\/store$/, replacement: "solid-js/store/dist/store.js" },
  95. { find: /^solid-js$/, replacement: "solid-js/dist/solid.js" },
  96. {
  97. find: /^ws$/,
  98. replacement: path.join(path.dirname(createRequire(import.meta.url).resolve("ws/package.json")), "wrapper.mjs"),
  99. },
  100. ],
  101. conditions: ["node"],
  102. }
  103. const output = (entryFileNames: string, banner?: string) => ({
  104. format: "esm" as const,
  105. entryFileNames,
  106. inlineDynamicImports: true,
  107. banner,
  108. })
  109. function nodePrelude(input: NodeBuildInput) {
  110. const nodePtySpawnHelper =
  111. input.target.platform === "darwin"
  112. ? `${input.target.nodePtyPackage}/prebuilds/darwin-${input.target.arch}/spawn-helper`
  113. : undefined
  114. const promiseModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.promise")]
  115. if (!sdk) throw new Error("OpenCode Promise plugin SDK is unavailable")
  116. export const Agent = sdk.Agent
  117. export const Command = sdk.Command
  118. export const Connection = sdk.Connection
  119. export const Credential = sdk.Credential
  120. export const Integration = sdk.Integration
  121. export const Model = sdk.Model
  122. export const Plugin = sdk.Plugin
  123. export const Provider = sdk.Provider
  124. export const Reference = sdk.Reference
  125. export const Skill = sdk.Skill`
  126. const effectModule = promiseModule
  127. .replace("opencode.plugin.v2.promise", "opencode.plugin.v2.effect")
  128. .replace("Promise plugin", "Effect plugin")
  129. const promisePluginModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.promise")]
  130. if (!sdk) throw new Error("OpenCode Promise plugin SDK is unavailable")
  131. export const define = sdk.Plugin.define`
  132. const effectPluginModule = promisePluginModule
  133. .replace("opencode.plugin.v2.promise", "opencode.plugin.v2.effect")
  134. .replace("Promise plugin", "Effect plugin")
  135. const promiseToolModule = `export {}`
  136. const effectToolModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.effect")]
  137. if (!sdk) throw new Error("OpenCode Effect plugin SDK is unavailable")
  138. export const Error = sdk.Tool.Error
  139. `
  140. return `#!/usr/bin/env -S node ${nodeExecArgv.join(" ")}
  141. import __cjs_mod__ from "node:module"
  142. import { chmodSync as __ocChmod, existsSync as __ocExists, lstatSync as __ocLstat, mkdirSync as __ocMkdir, renameSync as __ocRename, rmSync as __ocRm, writeFileSync as __ocWrite } from "node:fs"
  143. import { tmpdir as __ocTmpdir } from "node:os"
  144. import __ocPath from "node:path"
  145. import { getAssetKeys as __ocAssetKeys, getRawAsset as __ocRawAsset, isSea as __ocIsSea } from "node:sea"
  146. import { fileURLToPath as __ocFileURLToPath } from "node:url"
  147. const __filename = import.meta.filename
  148. const __dirname = import.meta.dirname
  149. const require = __cjs_mod__.createRequire(import.meta.url)
  150. const __ocPluginModules = ${JSON.stringify({
  151. "@opencode-ai/plugin": "opencode:plugin-v2",
  152. "@opencode-ai/plugin/promise/plugin": "opencode:plugin-promise-plugin",
  153. "@opencode-ai/plugin/promise/tool": "opencode:plugin-promise-tool",
  154. "@opencode-ai/plugin/effect": "opencode:plugin-v2-effect",
  155. "@opencode-ai/plugin/effect/plugin": "opencode:plugin-v2-effect-plugin",
  156. "@opencode-ai/plugin/effect/tool": "opencode:plugin-v2-effect-tool",
  157. })}
  158. const __ocPluginSources = ${JSON.stringify({
  159. "opencode:plugin-v2": promiseModule,
  160. "opencode:plugin-promise-plugin": promisePluginModule,
  161. "opencode:plugin-promise-tool": promiseToolModule,
  162. "opencode:plugin-v2-effect": effectModule,
  163. "opencode:plugin-v2-effect-plugin": effectPluginModule,
  164. "opencode:plugin-v2-effect-tool": effectToolModule,
  165. })}
  166. __cjs_mod__.registerHooks({
  167. resolve(__ocSpecifier, __ocContext, __ocNextResolve) {
  168. const __ocUrl = __ocPluginModules[__ocSpecifier]
  169. return __ocUrl ? { url: __ocUrl, shortCircuit: true } : __ocNextResolve(__ocSpecifier, __ocContext)
  170. },
  171. load(__ocUrl, __ocContext, __ocNextLoad) {
  172. const __ocSource = __ocPluginSources[__ocUrl]
  173. return __ocSource
  174. ? { format: "module", source: __ocSource, shortCircuit: true }
  175. : __ocNextLoad(__ocUrl, __ocContext)
  176. },
  177. })
  178. const __ocUid = typeof process.getuid === "function" ? process.getuid() : undefined
  179. const __ocCacheRoot = __ocPath.join(__ocTmpdir(), \`opencode-node-\${__ocUid ?? "user"}\`)
  180. if (__ocIsSea()) {
  181. try {
  182. __ocMkdir(__ocCacheRoot, { mode: 0o700 })
  183. } catch (__ocError) {
  184. if (!__ocExists(__ocCacheRoot)) throw __ocError
  185. }
  186. const __ocCacheInfo = __ocLstat(__ocCacheRoot)
  187. if (!__ocCacheInfo.isDirectory() || __ocCacheInfo.isSymbolicLink()) throw new Error("Unsafe Node asset cache path")
  188. if (__ocUid !== undefined && __ocCacheInfo.uid !== __ocUid) throw new Error("Node asset cache is owned by another user")
  189. if (__ocUid !== undefined) __ocChmod(__ocCacheRoot, 0o700)
  190. }
  191. const __ocAssetRoot = __ocIsSea()
  192. ? __ocPath.join(__ocCacheRoot, ${JSON.stringify(`${input.assetHash}-${input.target.platform}-${input.target.arch}`)})
  193. : __ocFileURLToPath(new URL("./assets/", import.meta.url))
  194. if (__ocIsSea()) {
  195. for (const __ocKey of __ocAssetKeys()) {
  196. const __ocTarget = __ocPath.join(__ocAssetRoot, __ocKey)
  197. if (__ocExists(__ocTarget)) continue
  198. __ocMkdir(__ocPath.dirname(__ocTarget), { recursive: true })
  199. const __ocTemporary = \`${"${__ocTarget}"}.${"${process.pid}"}.${"${crypto.randomUUID()}"}.tmp\`
  200. __ocWrite(__ocTemporary, new Uint8Array(__ocRawAsset(__ocKey)))
  201. try {
  202. __ocRename(__ocTemporary, __ocTarget)
  203. } catch (__ocError) {
  204. __ocRm(__ocTemporary, { force: true })
  205. if (!__ocExists(__ocTarget)) throw __ocError
  206. }
  207. }
  208. const __ocPtySpawnHelper = ${JSON.stringify(nodePtySpawnHelper)}
  209. if (__ocPtySpawnHelper) __ocChmod(__ocPath.join(__ocAssetRoot, __ocPtySpawnHelper), 0o755)
  210. }
  211. process.env.OPENCODE_NODE_ASSETS_DIR = __ocAssetRoot
  212. process.env.OTUI_ASSET_ROOT = __ocAssetRoot
  213. process.env.OPENCODE_NODE_PTY_PATH = __ocPath.join(__ocAssetRoot, ${JSON.stringify(input.target.nodePtyEntryAsset)})
  214. process.env.OPENCODE_PARCEL_WATCHER_PATH = __ocPath.join(__ocAssetRoot, ${JSON.stringify(input.target.parcelWatcherAsset)})
  215. process.env.OPENCODE_PHOTON_WASM_PATH = __ocPath.join(__ocAssetRoot, ${JSON.stringify(photonWasmAsset)})
  216. process.env.OPENCODE_TREE_SITTER_WASM_PATH = __ocPath.join(__ocAssetRoot, ${JSON.stringify(shellParserWasmAssets.runtime)})
  217. process.env.OPENCODE_TREE_SITTER_BASH_WASM_PATH = __ocPath.join(__ocAssetRoot, ${JSON.stringify(shellParserWasmAssets.bash)})
  218. process.env.OPENCODE_TREE_SITTER_POWERSHELL_WASM_PATH = __ocPath.join(__ocAssetRoot, ${JSON.stringify(shellParserWasmAssets.powershell)})
  219. process.env.FFF_BINARY_PATH = __ocPath.join(__ocAssetRoot, ${JSON.stringify(input.target.fffAsset)})
  220. process.env.OPENCODE_FFF_FFI_PATH = __ocPath.join(__ocAssetRoot, ${JSON.stringify(input.target.fffFfiAsset)})
  221. try {
  222. globalThis.__OPENCODE_FFF_FFI = require(process.env.OPENCODE_FFF_FFI_PATH)
  223. } catch {}
  224. globalThis.__OPENCODE_PHOTON_WASM_PATH = process.env.OPENCODE_PHOTON_WASM_PATH
  225. if (process.platform === "linux") process.env.OPENTUI_LIBC = "glibc"`
  226. }
  227. export type NodeBuildInput = {
  228. readonly version: string
  229. readonly channel: string
  230. readonly assetHash: string
  231. readonly target: NodeTarget
  232. readonly appArchive: string
  233. }
  234. export function mainConfig(input: NodeBuildInput): UserConfig {
  235. return defineConfig({
  236. root: dir,
  237. plugins: [
  238. appAssetsPlugin(input.appArchive),
  239. rawTextPlugin(),
  240. runtimeRequirePlugin(),
  241. fffNodePlugin(),
  242. simulationGraphPlugin(),
  243. solid({
  244. solid: {
  245. generate: "universal",
  246. moduleName: "@opentui/solid",
  247. },
  248. }),
  249. ],
  250. resolve,
  251. esbuild: { jsx: "automatic" },
  252. define: {
  253. OPENCODE_VERSION: JSON.stringify(input.version),
  254. OPENCODE_CLI_NAME: JSON.stringify("opencode2-node"),
  255. OPENCODE_CHANNEL: JSON.stringify(input.channel),
  256. OPENCODE_LIBC: input.target.platform === "linux" ? JSON.stringify("glibc") : "undefined",
  257. FFF_LIBC: input.target.platform === "linux" ? JSON.stringify("gnu") : "undefined",
  258. "process.env.WS_NO_BUFFER_UTIL": JSON.stringify("1"),
  259. },
  260. ssr: { noExternal: true },
  261. build: {
  262. ssr: "src/node/index.ts",
  263. target: "node26",
  264. outDir: "dist-node",
  265. emptyOutDir: false,
  266. minify: true,
  267. rollupOptions: {
  268. output: output("opencode.mjs", nodePrelude(input)),
  269. },
  270. },
  271. })
  272. }
  273. export default mainConfig({
  274. version: process.env.OPENCODE_VERSION ?? "local",
  275. channel: process.env.OPENCODE_CHANNEL ?? "local",
  276. assetHash: "local",
  277. target: nodeTarget(process.platform, process.arch),
  278. appArchive: "",
  279. })