Files
coder/site/vite.config.mts
T
Jake Howell ea8ba0c678 refactor: remove MUI and Emotion (#27821)
> 🤖 This PR was modified by Coder Agents on behalf of Jake Howell.

Until we meet again.

## Stack

- #27636
- #27718 
- #27719 
- #27722
- #27723
- #27724
- #27728
- #27730
- #27732
- #27762
- #27763
- #27786
- #27787
- #27788
- #27789
- #27790
- #27791 
- #27817 
- #27820
- #28009

## Final removal (`c39b664`)

Removes the last of MUI and Emotion now that every surface has been
migrated:

- **Dependencies**: drops `@mui/material` and
`@emotion/{cache,css,react,styled}` from `package.json` /
`pnpm-lock.yaml`, and deletes the `@types/emotion.d.ts` and
`@types/mui.d.ts` module augmentations.
- **Theming**: replaces the Emotion `CacheProvider`, MUI `ThemeProvider`
/ `StyledEngineProvider`, and `CssBaseline` in `ThemeProvider` with a
lightweight `theme/context.tsx` that exposes `ThemeContextProvider` and
a `useTheme` hook.
- **Global styles**: moves the base `body` styles (background, text
color, font, antialiasing) that `CssBaseline` previously provided into
`index.css`, and drops the temporary MUI modal/popover scrollbar-gutter
workaround.
- **Cleanup**: removes the MUI → shadcn / Emotion → Tailwind migration
guidance from `site/AGENTS.md`, updates the Storybook `preview.tsx`, and
adjusts assorted components (`Command`, `Slider`, `Switch`, `Tabs`,
`SyntaxHighlighter`, timing charts) and theme files to consume the new
context instead of MUI/Emotion.
2026-08-17 14:04:48 +07:00

250 lines
7.4 KiB
TypeScript

import * as path from "node:path";
import babel from "@rolldown/plugin-babel";
import { storybookTest } from "@storybook/addon-vitest/vitest-plugin";
import react, { reactCompilerPreset } from "@vitejs/plugin-react";
import { playwright } from "@vitest/browser-playwright";
import { visualizer } from "rollup-plugin-visualizer";
import type { PluginOption } from "vite";
import checker from "vite-plugin-checker";
import { defineConfig } from "vitest/config";
// We enable profiling and source maps for internal deployments (e.g. dogfood).
// The profiling build uses react-dom/profiling, which keeps optimizations but
// preserves performance instrumentation.
const isProfilingBuild = process.env.CODER_REACT_PROFILING === "true";
const compilerPreset = reactCompilerPreset();
compilerPreset.rolldown.filter = {
...compilerPreset.rolldown.filter,
id: {
// Keep in sync with targetDirs in scripts/check-compiler.mjs.
include: [/src\/pages\/AgentsPage\//, /src\/pages\/AIBridgePage\//],
},
};
const plugins: PluginOption[] = [
react(),
babel({ presets: [compilerPreset] }),
checker({
typescript: true,
}),
];
if (process.env.STATS !== undefined) {
plugins.push(
visualizer({
filename: "./stats/index.html",
gzipSize: true,
}),
);
}
export default defineConfig({
plugins,
worker: {
format: "es",
},
publicDir: path.resolve(__dirname, "./static"),
build: {
outDir: path.resolve(__dirname, "./out"),
emptyOutDir: false, // We need to keep the /bin folder and GITKEEP files
sourcemap: isProfilingBuild ? true : "hidden",
rolldownOptions: {
input: {
index: path.resolve(__dirname, "./index.html"),
serviceWorker: path.resolve(__dirname, "./src/serviceWorker.ts"),
},
output: {
entryFileNames: (chunkInfo) => {
return chunkInfo.name === "serviceWorker"
? "[name].js"
: "assets/[name]-[hash].js";
},
codeSplitting: {
groups: [
{ name: "monaco", test: /monaco-editor/ },
{ name: "xterm", test: /@xterm/ },
{ name: "emoji-mart", test: /emoji-mart/ },
{ name: "radix-ui", test: /radix-ui/ },
],
},
},
},
},
define: {
"process.env": {
NODE_ENV: process.env.NODE_ENV,
STORYBOOK: process.env.STORYBOOK,
},
},
server: {
port: process.env.PORT ? Number(process.env.PORT) : 8080,
headers: {
// This header corresponds to "src/api/api.ts"'s hardcoded FE token.
// This is the secret side of the CSRF double cookie submit method.
// This should be sent on **every** response from the webserver.
//
// This is required because in production, the Golang webserver generates
// this "Set-Cookie" header. The Vite webserver needs to replicate this
// behavior. Instead of implementing CSRF though, we just use static
// values for simplicity.
"Set-Cookie":
"csrf_token=JXm9hOUdZctWt0ZZGAy9xiS/gxMKYOThdxjjMnMUyn4=; Path=/; HttpOnly; SameSite=Lax",
},
// The proxy targets localhost:3000 (coderd). During tests no
// coderd is running, and the proxy's retry sockets keep the
// Node process alive after vitest finishes.
proxy: process.env.VITEST
? undefined
: {
"//": {
changeOrigin: true,
target: process.env.CODER_HOST || "http://localhost:3000",
secure: process.env.NODE_ENV === "production",
rewrite: (path) => path.replace(/\/+/g, "/"),
},
"/api": {
ws: true,
changeOrigin: true,
target: process.env.CODER_HOST || "http://localhost:3000",
secure: process.env.NODE_ENV === "production",
configure: (proxy) => {
if (process.env.CODER_SESSION_TOKEN) {
proxy.on("proxyReq", (proxyReq) => {
proxyReq.setHeader(
"Coder-Session-Token",
process.env.CODER_SESSION_TOKEN!,
);
});
}
// Vite does not catch socket errors, and stops the webserver.
// As /logs endpoint can return HTTP 4xx status, we need to embrace
// Vite with a custom error handler to prevent from quitting.
proxy.on("proxyReqWs", (proxyReq, _req, socket) => {
if (process.env.NODE_ENV === "development") {
proxyReq.setHeader(
"origin",
process.env.CODER_HOST || "http://localhost:3000",
);
if (process.env.CODER_SESSION_TOKEN) {
proxyReq.setHeader(
"Coder-Session-Token",
process.env.CODER_SESSION_TOKEN!,
);
}
}
socket.on("error", (error) => {
console.error(error);
});
});
},
},
"/swagger": {
target: process.env.CODER_HOST || "http://localhost:3000",
secure: process.env.NODE_ENV === "production",
},
"/healthz": {
target: process.env.CODER_HOST || "http://localhost:3000",
secure: process.env.NODE_ENV === "production",
},
"/serviceWorker.js": {
target: process.env.CODER_HOST || "http://localhost:3000",
secure: process.env.NODE_ENV === "production",
},
},
allowedHosts: [".coder", ".dev.coder.com"],
},
// Pre-bundle deps that Vite tends to discover late. Without this, Vite
// re-optimizes mid-session which returns 504 "Outdated Optimize Dep" for
// every previously served chunk, cascading into dynamic import failures.
optimizeDeps: {
include: [
// Discovered at runtime without this entry, triggering
// a mid-run dep re-optimization that breaks imports.
"@tanstack/react-query-devtools",
],
},
resolve: {
alias: {
// In profiling builds, swap the usual reconciler for the profiling
// variant so that <Profiler> receives actual timing data.
...(isProfilingBuild
? { "react-dom/client": "react-dom/profiling" }
: {}),
},
},
test: {
silent: "passed-only",
// Rolldown's native threads do not terminate on close,
// so vitest always hits this timeout. Keep it short.
teardownTimeout: 1000,
projects: [
{
extends: true,
test: {
name: "unit",
include: [
"src/**/*.test.?(m)ts?(x)",
"scripts/**/*.test.?(m)[jt]s?(x)",
],
globals: true,
environment: "jsdom",
setupFiles: [
"@testing-library/jest-dom/vitest",
"./test/vitestSetup.ts",
],
},
},
// Storybook story tests via Playwright browser mode.
// Discovery handled by the storybookTest plugin via
// .storybook/main.ts `stories` config.
{
extends: true,
plugins: [
storybookTest({
configDir: path.join(__dirname, ".storybook"),
}),
{
name: "storybook-test-setup",
// Return 502 for API routes. The proxy is disabled
// during tests (see above), so without this vite
// serves its HTML fallback for unmatched routes.
configureServer(server) {
server.middlewares.use((req, res, next) => {
const url = req.url ?? "";
if (
url.startsWith("/api/") ||
url.startsWith("/swagger/") ||
url.startsWith("/healthz")
) {
res.statusCode = 502;
res.end();
return;
}
next();
});
},
},
],
test: {
name: "storybook",
browser: {
enabled: true,
headless: true,
provider: playwright(),
instances: [{ browser: "chromium" }],
},
setupFiles: [".storybook/vitest.setup.ts"],
// Stop early on systemic failures.
bail: 5,
// Cap concurrent browser iframes. The default
// (os.availableParallelism, 96 on dev workspaces)
// overwhelms vite's transform pipeline on cold cache.
maxWorkers: 4,
},
},
],
},
});