import { defineConfig, loadEnv, UserConfig, BuildEnvironmentOptions, PluginOption, Plugin, CSSOptions, } from "vite"; import path from "node:path"; import injectHTML from "vite-plugin-html-inject"; import childProcess from "child_process"; import autoprefixer from "autoprefixer"; import { Fonts } from "./src/ts/constants/fonts"; import { fontawesomeSubset } from "./vite-plugins/fontawesome-subset"; import { fontPreview } from "./vite-plugins/font-preview"; import { envConfig } from "./vite-plugins/env-config"; import { languageHashes } from "./vite-plugins/language-hashes"; import { minifyJson } from "./vite-plugins/minify-json"; import { versionFile } from "./vite-plugins/version-file"; import { jqueryInject } from "./vite-plugins/jquery-inject"; import { checker } from "vite-plugin-checker"; import Inspect from "vite-plugin-inspect"; import { ViteMinifyPlugin } from "vite-plugin-minify"; import { VitePWA } from "vite-plugin-pwa"; import { sentryVitePlugin } from "@sentry/vite-plugin"; import replace from "vite-plugin-filter-replace"; // eslint-disable-next-line import/no-unresolved import UnpluginInjectPreload from "unplugin-inject-preload/vite"; import { KnownFontName } from "@monkeytype/schemas/fonts"; import supportedBrowser from "vite-plugin-supported-browserslist"; export default defineConfig(({ mode }): UserConfig => { const env = loadEnv(mode, process.cwd(), ""); const useSentry = env["SENTRY"] !== undefined; const isDevelopment = mode !== "production"; if (!isDevelopment) { if (env["RECAPTCHA_SITE_KEY"] === undefined) { throw new Error(`${mode}: RECAPTCHA_SITE_KEY is not defined`); } if (useSentry && env["SENTRY_AUTH_TOKEN"] === undefined) { throw new Error(`${mode}: SENTRY_AUTH_TOKEN is not defined`); } } return { plugins: getPlugins({ isDevelopment, useSentry: useSentry, env }), build: getBuildOptions({ enableSourceMaps: useSentry }), css: getCssOptions({ isDevelopment }), server: { open: env["SERVER_OPEN"] !== "false", port: 3000, host: env["BACKEND_URL"] !== undefined, watch: { //we rebuild the whole contracts package when a file changes //so we only want to watch one file ignored: [/.*\/packages\/contracts\/dist\/(?!configs).*/], }, }, clearScreen: false, root: "src", publicDir: "../static", optimizeDeps: { include: ["jquery"], exclude: ["@fortawesome/fontawesome-free"], }, }; }); function getPlugins({ isDevelopment, env, useSentry, }: { isDevelopment: boolean; env: Record; useSentry: boolean; }): PluginOption[] { const clientVersion = getClientVersion(isDevelopment); const plugins: PluginOption[] = [ envConfig({ isDevelopment, clientVersion, env }), languageHashes({ skip: isDevelopment }), checker({ typescript: { tsconfigPath: path.resolve(__dirname, "./tsconfig.json"), }, oxlint: isDevelopment, eslint: isDevelopment ? { lintCommand: `eslint "${path.resolve(__dirname, "./src/ts/**/*.ts")}"`, watchPath: path.resolve(__dirname, "./src/"), } : false, overlay: { initialIsOpen: false, }, }), jqueryInject(), injectHTML(), supportedBrowser(), ]; const devPlugins: PluginOption[] = [Inspect()]; const prodPlugins: PluginOption[] = [ fontPreview(), fontawesomeSubset(), versionFile({ clientVersion }), ViteMinifyPlugin(), VitePWA({ // injectRegister: "networkfirst", injectRegister: null, registerType: "autoUpdate", manifest: { short_name: "Monkeytype", name: "Monkeytype", start_url: "/", icons: [ { src: "/images/icons/maskable_icon_x512.png", sizes: "512x512", type: "image/png", purpose: "maskable", }, { src: "/images/icons/general_icon_x512.png", sizes: "512x512", type: "image/png", purpose: "any", }, ], background_color: "#323437", display: "standalone", theme_color: "#323437", }, manifestFilename: "manifest.json", workbox: { clientsClaim: true, cleanupOutdatedCaches: true, globIgnores: ["**/.*"], globPatterns: [], navigateFallback: "", runtimeCaching: [ { urlPattern: (options) => { const isApi = options.url.hostname === "api.monkeytype.com"; return options.sameOrigin && !isApi; }, handler: "NetworkFirst", options: {}, }, { urlPattern: (options) => { //disable caching for version.json return options.url.pathname === "/version.json"; }, handler: "NetworkOnly", options: {}, }, ], }, }), useSentry ? (sentryVitePlugin({ authToken: env["SENTRY_AUTH_TOKEN"], org: "monkeytype", project: "frontend", release: { name: clientVersion, }, applicationKey: "monkeytype-frontend", }) as Plugin) : null, replace([ { filter: ["src/ts/firebase.ts"], replace: { from: `"./constants/firebase-config.ts"`, to: `"./constants/firebase-config-live.ts"`, }, }, { filter: ["src/email-handler.html"], replace: { from: `"./ts/constants/firebase-config"`, to: `"./ts/constants/firebase-config-live"`, }, }, ]), UnpluginInjectPreload({ files: [ { outputMatch: /css\/vendor.*\.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", }), minifyJson(), ]; return [...plugins, ...(isDevelopment ? devPlugins : prodPlugins)].filter( (it) => it !== null, ); } function getBuildOptions({ enableSourceMaps, }: { enableSourceMaps: boolean; }): BuildEnvironmentOptions { return { sourcemap: enableSourceMaps, emptyOutDir: true, outDir: "../dist", assetsInlineLimit: 0, //dont inline small files as data rollupOptions: { input: { monkeytype: path.resolve(__dirname, "src/index.html"), email: path.resolve(__dirname, "src/email-handler.html"), privacy: path.resolve(__dirname, "src/privacy-policy.html"), security: path.resolve(__dirname, "src/security-policy.html"), terms: path.resolve(__dirname, "src/terms-of-service.html"), 404: path.resolve(__dirname, "src/404.html"), }, output: { assetFileNames: (assetInfo: { name: string }) => { let extType = assetInfo.name.split(".").at(1) as string; if (/png|jpe?g|svg|gif|tiff|bmp|ico/i.test(extType)) { extType = "images"; } if (/\.(woff|woff2|eot|ttf|otf)$/.test(assetInfo.name)) { return `webfonts/[name]-[hash].${extType}`; } return `${extType}/[name].[hash][extname]`; }, chunkFileNames: "js/[name].[hash].js", entryFileNames: "js/[name].[hash].js", manualChunks: (id) => { if (id.includes("@sentry")) { return "vendor-sentry"; } if (id.includes("jquery")) { return "vendor-jquery"; } if (id.includes("@firebase")) { return "vendor-firebase"; } if (id.includes("monkeytype/packages")) { return "monkeytype-packages"; } if (id.includes("node_modules")) { return "vendor"; } return; }, }, }, } as BuildEnvironmentOptions; } function getCssOptions({ isDevelopment, }: { isDevelopment: boolean; }): CSSOptions { return { devSourcemap: true, postcss: { plugins: [ // @ts-expect-error this is fine autoprefixer({}), ], }, preprocessorOptions: { scss: { additionalData(source, fp) { if (fp.endsWith("index.scss")) { /** Enable for font awesome v6 */ /* const fontawesomeClasses = getFontawesomeConfig(); //inject variables into sass context $fontawesomeBrands: ${sassList( fontawesomeClasses.brands )}; $fontawesomeSolid: ${sassList(fontawesomeClasses.solid)}; */ const bypassFonts = isDevelopment ? ` $fontAwesomeOverride:"@fortawesome/fontawesome-free/webfonts"; $previewFontsPath:"webfonts";` : ""; const fonts = ` ${bypassFonts} $fonts: (${getFontsConfig()}); `; return ` //inject variables into sass context ${fonts} ${source}`; } else { return source; } }, }, }, }; } function getFontsConfig(): string { return ( "\n" + Object.keys(Fonts) .sort() .map((name: string) => { const config = Fonts[name as KnownFontName]; if (config.systemFont === true) return ""; return `"${name.replaceAll("_", " ")}": ( "src": "${config.fileName}", "weight": ${config.weight ?? 400}, ),`; }) .join("\n") + "\n" ); } function pad( numbers: number[], maxLength: number, fillString: string, ): string[] { return numbers.map((number) => number.toString().padStart(maxLength, fillString), ); } /** Enable for font awesome v6 */ /* function sassList(values) { return values.map((it) => `"${it}"`).join(","); } */ function getClientVersion(isDevelopment: boolean): string { if (isDevelopment) { return "DEVELOPMENT_CLIENT"; } const date = new Date(); const versionPrefix = pad( [date.getFullYear(), date.getMonth() + 1, date.getDate()], 2, "0", ).join("."); const versionSuffix = pad([date.getHours(), date.getMinutes()], 2, "0").join( ".", ); const version = [versionPrefix, versionSuffix].join("_"); try { const commitHash = childProcess .execSync("git rev-parse --short HEAD") .toString(); return `${version}_${commitHash}`.replace(/\n/g, ""); } catch (e) { return `${version}_unknown-hash`; } }