opencode/packages/app/vite.js

141 lines
4.7 KiB
JavaScript

import { readFileSync } from "node:fs"
import { createRequire } from "node:module"
import solidPlugin from "vite-plugin-solid"
import tailwindcss from "@tailwindcss/vite"
import { fileURLToPath } from "url"
const theme = fileURLToPath(new URL("./public/oc-theme-preload.js", import.meta.url))
const themeScript = readFileSync(theme, "utf8")
const tailwind = tailwindcss()
const tailwindGenerate = tailwind.find((plugin) => plugin.name === "@tailwindcss/vite:generate:serve")
const tailwindHotUpdate = tailwindGenerate?.hotUpdate
// Tailwind 4.3.3 expects a server that Vite's bundled dev hook does not provide.
if (tailwindGenerate && typeof tailwindHotUpdate === "function") {
tailwindGenerate.hotUpdate = function (context) {
if (!context.server) return
return tailwindHotUpdate.call(this, context)
}
}
// The markdown worker imports these directly, so they are served unbundled to keep worker startup
// stable.
const workerDeps = ["@pierre/highlights", "marked"]
// The Office previews load their wasm and fonts through `new URL(..., import.meta.url)`, which a pre-bundle would move
// away from the files.
const officeDeps = ["@betteroffice/docx", "@betteroffice/fonts", "@betteroffice/pptx", "@betteroffice/xlsx"]
// The build emits the Office engines' wasm and worker files for every `new URL(..., import.meta.url)` it sees, even in
// code that tree shaking drops. A file that no other emitted file names never loads, so it is left out of the build.
const officeEmitted = /^(?:docx_\w+_bg|ooxml_opc_bg|pptx_wasm_bg|xlsx_wasm_bg|residentEngineWorker)\b/
/** @type {import("vite").Plugin} */
const dropUnloadedOfficeFiles = {
name: "opencode-desktop:drop-unloaded-office-files",
apply: "build",
generateBundle: {
order: "post",
handler(_, bundle) {
const files = Object.values(bundle).filter((file) => !file.fileName.endsWith(".map"))
const name = (file) => file.fileName.split("/").pop() ?? file.fileName
const source = (file) => (file.type === "chunk" ? file.code : typeof file.source === "string" ? file.source : "")
const named = (file) => files.some((other) => other !== file && source(other).includes(name(file)))
files
.filter((file) => officeEmitted.test(name(file)) && !named(file))
.forEach((file) => {
delete bundle[file.fileName]
delete bundle[`${file.fileName}.map`]
})
},
},
}
/** @type {import("rolldown").Plugin} */
const bundleNestedWorkerDeps = {
name: "opencode-desktop:bundle-nested-worker-deps",
resolveId(id, importer) {
if (!importer || !workerDeps.includes(id) || !importer.includes("node_modules")) return
try {
return createRequire(importer).resolve(id)
} catch {
return
}
},
}
// The components lab globs every story, and the dependency scan cannot parse a .tsx story that calls import.meta.glob
// itself. The lab is a development tool, so its dependencies are found when it first opens rather than at startup.
/** @type {import("rolldown").Plugin} */
const skipComponentsLab = {
name: "opencode-desktop:skip-components-lab",
resolveId(id) {
if (id === "@/components-lab/lab") return { id, external: true }
},
}
export const channel = (() => {
const raw = process.env.OPENCODE_CHANNEL
if (raw === "local" || raw === "dev" || raw === "beta" || raw === "prod") return raw
if (process.env.OPENCODE_CHANNEL === "latest") return "prod"
return "dev"
})()
/**
* @type {import("vite").PluginOption}
*/
export default [
{
name: "opencode-desktop:config",
config() {
return {
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
// The font package imports its 33 MB Chinese, Japanese and Korean add-on on demand; the app does not ship it.
"@betteroffice/fonts-cjk": fileURLToPath(
new URL("../gui-extensions/src/microsoft-office/fonts-cjk.ts", import.meta.url),
),
},
},
define: {
"import.meta.env.VITE_OPENCODE_CHANNEL": JSON.stringify(channel),
},
worker: {
format: "es",
},
optimizeDeps: {
exclude: [...workerDeps, ...officeDeps],
rolldownOptions: { plugins: [bundleNestedWorkerDeps, skipComponentsLab] },
},
}
},
},
dropUnloadedOfficeFiles,
{
name: "opencode-desktop:theme-preload",
transformIndexHtml: {
order: "pre",
handler: inlineThemePreload,
},
},
...tailwind,
solidPlugin(),
]
export function inlineThemePreload(html) {
return html.replace(
/<script id="oc-theme-preload-script" src="(?:\.\/|\/)oc-theme-preload\.js"><\/script>/,
`<script id="oc-theme-preload-script">${themeScript}</script>`,
)
}