Folo/configs/vite.render.config.ts

189 lines
5.5 KiB
TypeScript

import fs, { readFileSync } from "node:fs"
import path, { resolve } from "node:path"
import * as babel from "@babel/core"
import generate from "@babel/generator"
import { parse } from "@babel/parser"
import * as t from "@babel/types"
import { sentryVitePlugin } from "@sentry/vite-plugin"
import react from "@vitejs/plugin-react"
import { prerelease } from "semver"
import type { Plugin, UserConfig } from "vite"
import { getGitHash } from "../scripts/lib"
import i18nCompleteness from "./i18n-completeness"
const pkg = JSON.parse(readFileSync("package.json", "utf8"))
const isCI = process.env.CI === "true" || process.env.CI === "1"
function localesPlugin(): Plugin {
return {
name: "locales-merge",
enforce: "post",
generateBundle(options, bundle) {
const localesDir = path.resolve(__dirname, "../locales")
const namespaces = fs.readdirSync(localesDir)
const languageResources = {}
namespaces.forEach((namespace) => {
const namespacePath = path.join(localesDir, namespace)
const files = fs.readdirSync(namespacePath).filter((file) => file.endsWith(".json"))
files.forEach((file) => {
const lang = path.basename(file, ".json")
const filePath = path.join(namespacePath, file)
const content = JSON.parse(fs.readFileSync(filePath, "utf-8"))
if (!languageResources[lang]) {
languageResources[lang] = {}
}
languageResources[lang][namespace] = content
})
})
Object.entries(languageResources).forEach(([lang, resources]) => {
const fileName = `locales/${lang}.js`
const content = `export default ${JSON.stringify(resources)};`
this.emitFile({
type: "asset",
fileName,
source: content,
})
})
// Remove original JSON chunks
Object.keys(bundle).forEach((key) => {
if (key.startsWith("locales/") && key.endsWith(".json")) {
delete bundle[key]
}
})
},
}
}
function customI18nHmrPlugin(): Plugin {
return {
name: "custom-i18n-hmr",
handleHotUpdate({ file, server }) {
if (file.endsWith(".json") && file.includes("locales")) {
server.ws.send({
type: "custom",
event: "i18n-update",
data: {
file,
content: readFileSync(file, "utf-8"),
},
})
// return empty array to prevent the default HMR
return []
}
},
}
}
export const viteRenderBaseConfig = {
resolve: {
alias: {
"@renderer": resolve("src/renderer/src"),
"@shared": resolve("src/shared/src"),
"@pkg": resolve("./package.json"),
"@env": resolve("./src/env.ts"),
"@locales": resolve("./locales"),
},
},
base: "/",
plugins: [
react(),
sentryVitePlugin({
org: "follow-rg",
project: "follow",
disable: !isCI,
bundleSizeOptimizations: {
excludeDebugStatements: true,
// Only relevant if you added `browserTracingIntegration`
excludePerformanceMonitoring: true,
// Only relevant if you added `replayIntegration`
excludeReplayIframe: true,
excludeReplayShadowDom: true,
excludeReplayWorker: true,
},
moduleMetadata: {
appVersion: process.env.NODE_ENV === "development" ? "dev" : pkg.version,
electron: false,
},
sourcemaps: {
filesToDeleteAfterUpload: ["out/web/assets/*.js.map"],
},
}),
localesPlugin(),
viteTwToRawString(),
customI18nHmrPlugin(),
],
define: {
APP_VERSION: JSON.stringify(pkg.version),
APP_NAME: JSON.stringify(pkg.name),
APP_DEV_CWD: JSON.stringify(process.cwd()),
GIT_COMMIT_SHA: JSON.stringify(process.env.VERCEL_GIT_COMMIT_SHA || getGitHash()),
RELEASE_CHANNEL: JSON.stringify((prerelease(pkg.version)?.[0] as string) || "stable"),
DEBUG: process.env.DEBUG === "true",
I18N_COMPLETENESS_MAP: JSON.stringify({ ...i18nCompleteness, en: 100 }),
},
} satisfies UserConfig
function viteTwToRawString(): Plugin {
return {
name: "vite-plugin-tw-to-raw-string",
transform(code, id) {
// Only Process .tsx .ts .jsx .js files
if (!/\.[jt]sx?$/.test(id)) {
return null
}
// Parse the code using Babel's parser with TypeScript support
const ast = parse(code, {
sourceType: "module",
plugins: ["jsx", "typescript"], // Add typescript support
})
babel.traverse(ast, {
TaggedTemplateExpression(path) {
if (t.isIdentifier(path.node.tag, { name: "tw" })) {
const { quasi } = path.node
if (t.isTemplateLiteral(quasi)) {
// Create a new template literal by combining quasis and expressions
const quasis = quasi.quasis.map((q) => q.value.raw)
// Replace the tagged template expression with the new template literal as a string
path.replaceWith(
t.templateLiteral(
quasis.map((q, i) =>
t.templateElement({ raw: q, cooked: q }, i === quasis.length - 1),
),
quasi.expressions,
),
)
}
}
},
})
// Generate the transformed code from the modified AST
// @ts-expect-error
const output = generate.default(ast, {}, code)
return {
code: output.code,
map: null, // Source map generation can be added if necessary
}
},
}
}