vite.node.config.ts 11 KB

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