adding monkeytype
Some checks failed
Mark Stale PRs / stale (push) Has been cancelled

This commit is contained in:
Benjamin Falch
2026-04-23 13:53:44 +02:00
parent e214a2fd35
commit 2bc741fb78
1930 changed files with 7590652 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
import { Plugin } from "vite";
import { EnvConfig } from "virtual:env-config";
const virtualModuleId = "virtual:env-config";
const resolvedVirtualModuleId = "\0" + virtualModuleId;
function fallback(value: string | undefined | null, fallback: string): string {
if (value === null || value === undefined || value === "") return fallback;
return value;
}
export function envConfig(options: {
isDevelopment: boolean;
clientVersion: string;
env: Record<string, string>;
}): Plugin {
return {
name: "virtual-env-config",
resolveId(id) {
if (id === virtualModuleId) return resolvedVirtualModuleId;
return;
},
load(id) {
if (id === resolvedVirtualModuleId) {
const devConfig: EnvConfig = {
isDevelopment: true,
backendUrl: fallback(
options.env["BACKEND_URL"],
"http://localhost:5005",
),
clientVersion: options.clientVersion,
recaptchaSiteKey: "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI",
quickLoginEmail: options.env["QUICK_LOGIN_EMAIL"],
quickLoginPassword: options.env["QUICK_LOGIN_PASSWORD"],
};
const prodConfig: EnvConfig = {
isDevelopment: false,
backendUrl: fallback(
options.env["BACKEND_URL"],
"https://api.monkeytype.com",
),
recaptchaSiteKey: options.env["RECAPTCHA_SITE_KEY"] ?? "",
quickLoginEmail: undefined,
quickLoginPassword: undefined,
clientVersion: options.clientVersion,
};
const envConfig = options.isDevelopment ? devConfig : prodConfig;
return `
export const envConfig = ${JSON.stringify(envConfig)};
`;
}
return;
},
};
}

View File

@@ -0,0 +1,78 @@
import { Plugin } from "vite";
import * as fs from "fs";
import * as path from "path";
import { fileURLToPath } from "url";
import subsetFont from "subset-font";
import { Fonts } from "../src/ts/constants/fonts";
import { KnownFontName } from "@monkeytype/schemas/fonts";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
async function generateSubset(
source: string,
target: string,
includedCharacters: string,
): Promise<void> {
const font = fs.readFileSync(source);
const subset = await subsetFont(font, includedCharacters, {
targetFormat: "woff2",
});
fs.writeFileSync(target, subset);
}
async function generatePreviewFonts(debug: boolean = false): Promise<void> {
const srcDir = __dirname + "/../static/webfonts";
const targetDir = __dirname + "/../static/webfonts-preview";
fs.mkdirSync(targetDir, { recursive: true });
for (const name of Object.keys(Fonts)) {
const font = Fonts[name as KnownFontName];
if (font.systemFont) continue;
const includedCharacters =
(font.display ?? name.replaceAll("_", " ")) + "Fontfamily";
const fileName = font.fileName;
await generateSubset(
srcDir + "/" + fileName,
targetDir + "/" + fileName,
includedCharacters,
);
if (debug) {
console.log(
`Processing ${name} with file ${fileName} to display "${includedCharacters}".`,
);
}
}
}
/**
* Generate a preview file from each font in `static/webfonts` into `static/webfonts-preview`.
* A preview file only contains the characters needed to show the preview.
* @returns
*/
export function fontPreview(): Plugin {
return {
name: "vite-plugin-webfonts-preview",
apply: "build",
async buildStart() {
const start = performance.now();
console.log("\nCreating webfonts preview...");
await generatePreviewFonts();
const end = performance.now();
console.log(
`Creating webfonts preview took ${Math.round(end - start)} ms`,
);
},
};
}
//detect if we run this as a main
if (import.meta.url.endsWith(process.argv[1] as string)) {
void generatePreviewFonts(true);
}

View File

@@ -0,0 +1,207 @@
import { Plugin } from "vite";
import * as fs from "fs";
import { createRequire } from "module";
import * as path from "path";
import { fontawesomeSubset as createFontawesomeSubset } from "fontawesome-subset";
function parseIcons(iconSet: string): string[] {
const require = createRequire(import.meta.url);
const path = require.resolve(
`@fortawesome/fontawesome-free/js/${iconSet}.js`,
);
const file: string | null = fs.readFileSync(path).toString();
return file
?.match(/"(.*)": \[.*\],/g)
?.map((it) => it.substring(1, it.indexOf(":") - 1)) as string[];
}
type FontawesomeConfig = {
/* used regular icons without `fa-` prefix*/
regular: string[];
/* used solid icons without `fa-` prefix*/
solid: string[];
/* used brands icons without `fa-` prefix*/
brands: string[];
};
type FileObject = { name: string; isDirectory: boolean };
const iconSet = {
solid: parseIcons("solid"),
regular: parseIcons("regular"),
brands: parseIcons("brands"),
};
/**
* Map containing reserved classes by module
*/
const modules2 = {
animated: ["spin", "pulse"],
"bordererd-pulled": ["border", "pull-left", "pull-right"],
"fixed-width": ["fw"],
larger: [
"lg",
"xs",
"sm",
"1x",
"2x",
"3x",
"4x",
"5x",
"6x",
"7x",
"8x",
"9x",
"10x",
],
"rotated-flipped": [
"rotate-90",
"rotate-180",
"rotate-270",
"flip-horizontal",
"flip-vertical",
"flip-both",
],
stacked: ["stack", "stack-1x", "stack-2x", "inverse"],
};
function toFileAndDir(dir: string, file: string): FileObject {
const name = path.join(dir, file);
return { name, isDirectory: fs.statSync(name).isDirectory() };
}
function findAllFiles(
dir: string,
filter: (filename: string) => boolean = (_it): boolean => true,
): string[] {
const files = fs
.readdirSync(dir)
.map((it) => toFileAndDir(dir, it))
.filter((file) => file.isDirectory || filter(file.name));
const out: string[] = [];
for (const file of files) {
if (file.isDirectory) {
out.push(...findAllFiles(file.name, filter));
} else {
out.push(file.name);
}
}
return out;
}
/**
* Detect used fontawesome icons in the directories `src/**` and `static/**{.html|.css}`
* @param {boolean} debug - Enable debug output
* @returns {FontawesomeConfig} - used icons
*/
function getFontawesomeConfig(debug = false): FontawesomeConfig {
const time = Date.now();
const srcFiles = findAllFiles(
"./src",
(filename) =>
!filename.endsWith("fontawesome-5.scss") &&
!filename.endsWith("fontawesome-6.scss"), //ignore our own css
);
const staticFiles = findAllFiles(
"./static",
(filename) => filename.endsWith(".html") || filename.endsWith(".css"),
);
const allFiles = [...srcFiles, ...staticFiles];
const usedClassesSet: Set<string> = new Set();
const regex = /\bfa-[a-z0-9-]+\b/g;
for (const file of allFiles) {
const fileContent = fs.readFileSync("./" + file).toString();
const matches = fileContent.match(regex);
if (matches) {
matches.forEach((match) => {
const [icon] = match.split(" ");
usedClassesSet.add((icon as string).substring(3));
});
}
}
const usedClasses = [...usedClassesSet].sort();
const allModuleClasses = new Set(Object.values(modules2).flatMap((it) => it));
const icons = usedClasses.filter((it) => !allModuleClasses.has(it));
const solid = icons.filter((it) => iconSet.solid.includes(it));
const regular = icons.filter((it) => iconSet.regular.includes(it));
const brands = usedClasses.filter((it) => iconSet.brands.includes(it));
const leftOvers = icons.filter(
(it) =>
!(solid.includes(it) || regular.includes(it) || brands.includes(it)),
);
if (leftOvers.length !== 0) {
throw new Error(
"Fontawesome failed with unknown icons: " + leftOvers.toString(),
);
}
if (debug) {
console.debug(
"Make sure fontawesome modules are active: ",
Object.entries(modules2)
.filter((it) => usedClasses.filter((c) => it[1].includes(c)).length > 0)
.map((it) => it[0])
.filter((it) => it !== "brands")
.join(", "),
);
console.debug(
"Here is your config: \n",
JSON.stringify({
regular,
solid,
brands,
}),
);
console.debug("Detected fontawesome classes in", Date.now() - time, "ms");
}
return {
regular,
solid,
brands,
};
}
/**
* Detect fontawesome icons used by the application and creates subset font files only containing the used icons.
* @param options
* @returns
*/
export function fontawesomeSubset(): Plugin {
return {
name: "vite-plugin-fontawesome-subset",
apply: "build",
async buildStart() {
const start = performance.now();
console.log("\nCreating fontawesome subset...");
const fontawesomeClasses = getFontawesomeConfig();
await createFontawesomeSubset(
fontawesomeClasses,
"src/webfonts-generated",
{
targetFormats: ["woff2"],
},
);
const end = performance.now();
console.log(
`Creating fontawesome subset took ${Math.round(end - start)} ms`,
);
},
};
}
//detect if we run this as a main
if (import.meta.url.endsWith(process.argv[1] as string)) {
getFontawesomeConfig(true);
}

View File

@@ -0,0 +1,48 @@
import { IndexHtmlTransformContext, Plugin } from "vite";
// eslint-disable-next-line import/no-unresolved
import UnpluginInjectPreload from "unplugin-inject-preload/vite";
import { basename } from "node:path";
export function injectPreload(): Plugin {
const base = UnpluginInjectPreload({
files: [
{
outputMatch: /css\/.*\.css$/,
attributes: {
as: "style",
type: "text/css",
rel: "preload",
crossorigin: true,
},
},
{
outputMatch: /.*\.woff2$/,
attributes: {
as: "font",
type: "font/woff2",
rel: "preload",
crossorigin: true,
},
},
],
injectTo: "head-prepend",
}) as {
name: string;
vite: {
transformIndexHtml: {
handler: (html: string, ctx: IndexHtmlTransformContext) => string;
};
};
};
return {
name: base.name,
...base.vite,
transformIndexHtml(html, ctx) {
//only add preload to the index.html file
if (basename(ctx.filename) !== "index.html") {
return html;
}
return base.vite.transformIndexHtml.handler(html, ctx);
},
};
}

View File

@@ -0,0 +1,63 @@
import { Plugin } from "vite";
import { readdirSync, readFileSync } from "fs";
import { TextEncoder } from "util";
import { createHash } from "crypto";
const virtualModuleId = "virtual:language-hashes";
const resolvedVirtualModuleId = "\0" + virtualModuleId;
function calcHash(file: string): string {
const currentLanguage = JSON.stringify(
JSON.parse(readFileSync("./static/languages/" + file).toString()),
null,
0,
);
const encoder = new TextEncoder();
const data = encoder.encode(currentLanguage);
return createHash("sha256").update(data).digest("hex");
}
function getHashes(): Record<string, string> {
const start = performance.now();
console.log("\nHashing languages...");
const hashes = Object.fromEntries(
readdirSync("./static/languages").map((file) => {
return [file.slice(0, -5), calcHash(file)];
}),
);
const end = performance.now();
console.log(`Creating language hashes took ${Math.round(end - start)} ms`);
return hashes;
}
export function languageHashes(options?: { skip: boolean }): Plugin {
return {
name: "virtual-language-hashes",
resolveId(id) {
if (id === virtualModuleId) return resolvedVirtualModuleId;
return;
},
load(id) {
if (id === resolvedVirtualModuleId) {
if (options?.skip) {
console.log("Skipping language hashing.");
}
const hashes: Record<string, string> = options?.skip ? {} : getHashes();
return `
export const languageHashes = ${JSON.stringify(hashes)};
`;
}
return;
},
};
}
if (import.meta.url.endsWith(process.argv[1] as string)) {
console.log(JSON.stringify(getHashes(), null, 4));
}

View File

@@ -0,0 +1,76 @@
import { Plugin } from "vite";
import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import path from "node:path";
/**
* Minifies all json files in the `dist` directory
* @returns
*/
export function minifyJson(): Plugin {
return {
name: "minify-json",
apply: "build",
generateBundle() {
let totalOriginalSize = 0;
let totalMinifiedSize = 0;
const minifyJsonFiles = (dir: string): void => {
readdirSync(dir).forEach((file) => {
const sourcePath = path.join(dir, file);
const stat = statSync(sourcePath);
if (stat.isDirectory()) {
minifyJsonFiles(sourcePath);
} else if (path.extname(file) === ".json") {
const originalContent = readFileSync(sourcePath, "utf8");
const originalSize = Buffer.byteLength(originalContent, "utf8");
const minifiedContent = JSON.stringify(JSON.parse(originalContent));
const minifiedSize = Buffer.byteLength(minifiedContent, "utf8");
totalOriginalSize += originalSize;
totalMinifiedSize += minifiedSize;
writeFileSync(sourcePath, minifiedContent);
/*
const savings =
((originalSize - minifiedSize) / originalSize) * 100;
console.log(
`\x1b[0m \x1b[36m${sourcePath}\x1b[0m | ` +
`\x1b[90mOriginal: ${originalSize} bytes\x1b[0m | ` +
`\x1b[90mMinified: ${minifiedSize} bytes\x1b[0m | ` +
`\x1b[32mSavings: ${savings.toFixed(2)}%\x1b[0m`
);
*/
}
});
};
// console.log("\n\x1b[1mMinifying JSON files...\x1b[0m\n");
const start = performance.now();
minifyJsonFiles("./dist");
const end = performance.now();
const totalSavings =
((totalOriginalSize - totalMinifiedSize) / totalOriginalSize) * 100;
console.log(
`\n\n\x1b[1mJSON Minification Summary:\x1b[0m\n` +
` \x1b[90mTotal original size: ${(
totalOriginalSize /
1024 /
1024
).toFixed(2)} mB\x1b[0m\n` +
` \x1b[90mTotal minified size: ${(
totalMinifiedSize /
1024 /
1024
).toFixed(2)} mB\x1b[0m\n` +
` \x1b[32mTotal savings: ${totalSavings.toFixed(
2,
)}%\x1b[0m took ${Math.round(end - start)} ms\n`,
);
},
};
}

View File

@@ -0,0 +1,314 @@
import { Plugin, ViteDevServer, normalizePath } from "vite";
import { spawn, execSync, ChildProcess } from "child_process";
import { fileURLToPath } from "url";
export type OxlintCheckerOptions = {
/** Debounce delay in milliseconds before running lint after file changes. @default 125 */
debounceDelay?: number;
/** Run type-aware checks (slower but more thorough). @default true */
typeAware?: boolean;
/** Show browser overlay with lint status. @default true */
overlay?: boolean;
/** File extensions to watch for changes. @default ['.ts', '.tsx', '.js', '.jsx'] */
extensions?: string[];
};
type LintResult = {
errorCount: number;
warningCount: number;
running: boolean;
hadIssues: boolean;
typeAware?: boolean;
};
const OXLINT_SUMMARY_REGEX = /Found (\d+) warnings? and (\d+) errors?/;
export function oxlintChecker(options: OxlintCheckerOptions = {}): Plugin {
const {
debounceDelay = 125,
typeAware = true,
overlay = true,
extensions = [".ts", ".tsx", ".js", ".jsx"],
} = options;
let currentProcess: ChildProcess | null = null;
let debounceTimer: NodeJS.Timeout | null = null;
let server: ViteDevServer | null = null;
let isProduction = false;
let currentRunId = 0;
let lastLintResult: LintResult = {
errorCount: 0,
warningCount: 0,
running: false,
hadIssues: false,
};
const killCurrentProcess = (): boolean => {
if ((currentProcess && !currentProcess.killed) || currentProcess !== null) {
currentProcess.kill();
currentProcess = null;
return true;
}
return false;
};
const clearDebounceTimer = (): void => {
if (debounceTimer) {
clearTimeout(debounceTimer);
debounceTimer = null;
}
};
const parseLintOutput = (
output: string,
): Pick<LintResult, "errorCount" | "warningCount"> => {
const summaryMatch = OXLINT_SUMMARY_REGEX.exec(output);
if (summaryMatch?.[1] !== undefined && summaryMatch?.[2] !== undefined) {
return {
warningCount: parseInt(summaryMatch[1], 10),
errorCount: parseInt(summaryMatch[2], 10),
};
}
return { errorCount: 0, warningCount: 0 };
};
const sendLintResult = (result: Partial<LintResult>): void => {
const previousHadIssues = lastLintResult.hadIssues;
const payload: LintResult = {
errorCount: result.errorCount ?? lastLintResult.errorCount,
warningCount: result.warningCount ?? lastLintResult.warningCount,
running: result.running ?? false,
hadIssues: previousHadIssues,
typeAware: result.typeAware,
};
// Only update hadIssues when we have actual lint results (not just running status)
if (result.running === false) {
const currentHasIssues =
(result.errorCount ?? 0) > 0 || (result.warningCount ?? 0) > 0;
lastLintResult = { ...payload, hadIssues: currentHasIssues };
} else {
// Keep hadIssues unchanged when just updating running status
lastLintResult = { ...payload, hadIssues: previousHadIssues };
}
if (server) {
server.ws.send("vite-plugin-oxlint", payload);
}
};
/**
* Runs an oxlint process with the given arguments and captures its combined output.
*
* This function is responsible for managing the lifecycle of the current lint process:
* - It spawns a new child process via `npx oxlint . ...args`.
* - It assigns the spawned process to the shared {@link currentProcess} variable so that
* other parts of the plugin can cancel or track the active lint run.
* - On process termination (either "error" or "close"), it clears {@link currentProcess}
* if it still refers to this child, avoiding interference with any newer process that
* may have started in the meantime.
*
* @param args Additional command-line arguments to pass to `oxlint`.
* @returns A promise that resolves with the process exit code (or `null` if
* the process exited due to a signal) and the full stdout/stderr output
* produced by the lint run.
*/
const runLintProcess = async (
args: string[],
): Promise<{ code: number | null; output: string }> => {
return new Promise((resolve) => {
const childProcess = spawn("npx", ["oxlint", ".", ...args], {
cwd: process.cwd(),
shell: true,
env: { ...process.env, FORCE_COLOR: "3" },
});
currentProcess = childProcess;
let output = "";
childProcess.stdout?.on("data", (data: Buffer) => {
output += data.toString();
});
childProcess.stderr?.on("data", (data: Buffer) => {
output += data.toString();
});
childProcess.on("error", (error: Error) => {
output += `\nError: ${error.message}`;
if (currentProcess === childProcess) {
currentProcess = null;
}
resolve({ code: 1, output });
});
childProcess.on("close", (code: number | null) => {
if (currentProcess === childProcess) {
currentProcess = null;
}
resolve({ code, output });
});
});
};
const runOxlint = async (): Promise<void> => {
const wasKilled = killCurrentProcess();
const runId = ++currentRunId;
console.log(
wasKilled
? "\x1b[36mRunning oxlint...\x1b[0m \x1b[90m(killed previous process)\x1b[0m"
: "\x1b[36mRunning oxlint...\x1b[0m",
);
sendLintResult({ running: true });
// First pass: fast oxlint without type checking
const { code, output } = await runLintProcess([]);
// Check if we were superseded by a newer run
if (runId !== currentRunId) {
return;
}
if (output) {
console.log(output);
}
// If first pass had errors, send them immediately (fast-fail)
if (code !== 0) {
const counts = parseLintOutput(output);
if (counts.errorCount > 0 || counts.warningCount > 0) {
sendLintResult({ ...counts, running: false });
return;
}
}
// Run type-aware check if enabled
if (!typeAware) {
sendLintResult({ errorCount: 0, warningCount: 0, running: false });
return;
}
console.log("\x1b[36mRunning type-aware checks...\x1b[0m");
sendLintResult({ running: true, typeAware: true });
const typeResult = await runLintProcess(["--type-check", "--type-aware"]);
// Check if we were superseded by a newer run
if (runId !== currentRunId) {
return;
}
if (typeResult.output) {
console.log(typeResult.output);
}
const counts =
typeResult.code !== 0
? parseLintOutput(typeResult.output)
: { errorCount: 0, warningCount: 0 };
sendLintResult({ ...counts, running: false });
};
const debouncedLint = (): void => {
clearDebounceTimer();
sendLintResult({ running: true });
debounceTimer = setTimeout(() => void runOxlint(), debounceDelay);
};
return {
name: "vite-plugin-oxlint-checker",
config(_, { mode }) {
isProduction = mode === "production";
},
configureServer(devServer: ViteDevServer) {
server = devServer;
// Send current lint status to new clients on connection
devServer.ws.on("connection", () => {
devServer.ws.send("vite-plugin-oxlint", lastLintResult);
});
// Run initial lint
void runOxlint();
// Listen for file changes
devServer.watcher.on("change", (file: string) => {
// Only lint on relevant file changes
if (extensions.some((ext) => file.endsWith(ext))) {
debouncedLint();
}
});
},
transformIndexHtml() {
if (isProduction || !overlay) {
return [];
}
// Inject import to the overlay module (actual .ts file processed by Vite)
const overlayPath = normalizePath(
fileURLToPath(new URL("./oxlint-overlay.ts", import.meta.url)),
);
return [
{
tag: "script",
attrs: {
type: "module",
src: `/@fs${overlayPath}`,
},
injectTo: "body-prepend",
},
];
},
buildStart() {
// Only run during production builds, not dev server startup
if (!isProduction) {
return;
}
// Run oxlint synchronously during build
console.log("\n\x1b[1mRunning oxlint...\x1b[0m");
try {
const commands = ["npx oxlint ."];
if (typeAware) {
commands.push("npx oxlint . --type-aware --type-check");
}
const output = execSync(commands.join(" && "), {
cwd: process.cwd(),
encoding: "utf-8",
env: { ...process.env, FORCE_COLOR: "3" },
});
if (output) {
console.log(output);
}
console.log(` \x1b[32m✓ No linting issues found\x1b[0m\n`);
} catch (error) {
// execSync throws on non-zero exit code (linting errors found)
if (error instanceof Error && "stdout" in error) {
const execError = error as Error & {
stdout?: string;
stderr?: string;
};
if (execError.stdout !== undefined) console.log(execError.stdout);
if (execError.stderr !== undefined) console.error(execError.stderr);
}
console.error("\n\x1b[31mBuild aborted due to linting errors\x1b[0m\n");
process.exit(1);
}
},
closeBundle() {
// Cleanup on server close
killCurrentProcess();
clearDebounceTimer();
},
};
}

View File

@@ -0,0 +1,126 @@
// Oxlint overlay client-side code
let overlay: HTMLDivElement | null = null;
let hideTimeout: ReturnType<typeof setTimeout> | null = null;
function createOverlay(): HTMLDivElement {
if (overlay) return overlay;
overlay = document.createElement("div");
overlay.id = "oxlint-error-overlay";
overlay.style.cssText = `
position: fixed;
bottom: 20px;
left: 20px;
background: #323437;
color: #e4dec8ff;
padding: 12px 16px;
border-radius: 8px;
font-family: 'Roboto Mono', monospace;
font-size: 14px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 10000;
display: none;
align-items: center;
gap: 8px;
cursor: pointer;
transition: opacity 0.2s ease;
`;
overlay.addEventListener("mouseenter", () => {
if (overlay) overlay.style.opacity = "0.5";
});
overlay.addEventListener("mouseleave", () => {
if (overlay) overlay.style.opacity = "1";
});
overlay.addEventListener("click", () => {
if (overlay) overlay.style.display = "none";
});
document.body.appendChild(overlay);
return overlay;
}
function updateOverlay(data: {
errorCount?: number;
warningCount?: number;
running?: boolean;
hadIssues?: boolean;
typeAware?: boolean;
}): void {
const overlayEl = createOverlay();
// Clear any pending hide timeout
if (hideTimeout !== null) {
clearTimeout(hideTimeout);
hideTimeout = null;
}
// Show running icon if linting is running and there were issues before
if (data.running) {
if (data.hadIssues) {
const message = data.typeAware ? "checking type aware..." : "checking...";
overlayEl.innerHTML = `
<span style="font-size: 18px;">⏳</span>
<span>oxlint: ${message}</span>
`;
overlayEl.style.display = "flex";
overlayEl.style.color = "#e4dec8ff";
} else {
overlayEl.style.display = "none";
}
return;
}
const { errorCount = 0, warningCount = 0, hadIssues = false } = data;
const total = errorCount + warningCount;
if (total > 0) {
overlayEl.innerHTML = `
<span style="font-size: 18px;">🚨</span>
<span>oxlint: ${errorCount} error${errorCount !== 1 ? "s" : ""}, ${warningCount} warning${warningCount !== 1 ? "s" : ""}</span>
`;
overlayEl.style.display = "flex";
if (errorCount > 0) {
overlayEl.style.color = "#e4dec8ff";
}
} else {
// Only show success if the previous lint had issues
if (hadIssues) {
overlayEl.innerHTML = `
<span style="font-size: 18px;">✅</span>
<span>oxlint: ok</span>
`;
overlayEl.style.display = "flex";
overlayEl.style.color = "#e4dec8ff";
// Hide after 3 seconds
hideTimeout = setTimeout(() => {
overlayEl.style.display = "none";
hideTimeout = null;
}, 3000);
} else {
// Two good lints in a row - don't show anything
overlayEl.style.display = "none";
}
}
}
// Initialize overlay on load
createOverlay();
if (import.meta.hot) {
import.meta.hot.on(
"vite-plugin-oxlint",
(data: {
errorCount?: number;
warningCount?: number;
running?: boolean;
hadIssues?: boolean;
}) => {
updateOverlay(data);
},
);
}

View File

@@ -0,0 +1,20 @@
import { Plugin } from "vite";
import path from "node:path";
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
export function versionFile(options: { clientVersion: string }): Plugin {
return {
name: "generate-version-json",
apply: "build",
closeBundle() {
const distPath = path.resolve("./dist");
if (!existsSync(distPath)) {
mkdirSync(distPath, { recursive: true });
}
const versionJson = JSON.stringify({ version: options.clientVersion });
const versionPath = path.resolve(distPath, "version.json");
writeFileSync(versionPath, versionJson);
},
};
}