mirror of
https://github.com/coder/coder.git
synced 2026-11-02 13:33:30 +08:00
> 🤖 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.
250 lines
7.4 KiB
TypeScript
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,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|