perf: upgrade to vite 8, migrate rollup to rolldown, and optimise build pipeline

- Upgrade vite from v7 to v8 (Rolldown-based bundler)
- Migrate server build from rollup to rolldown, eliminating 5 plugins
  (rolldown handles TS, JSX, JSON, CJS, and node resolution natively)
- Replace vite-plugin-babel-macros with targeted lingui macro plugin
  that skips files without macro imports (~90% of files)
- Replace vite-tsconfig-paths with native resolve.tsconfigPaths
- Remove redundant lingui error-reporter plugin (51% of plugin time)
- Narrow optimizeDeps.entries from ~1060 to ~460 files
- Move typecheck out of build into separate CI job (43s -> 24s build)
- Fix card.tsx var() typo exposed by LightningCSS
- Remove 30 unused packages (rollup, babel presets, rollup plugins, etc.)
- Add vite override for peer dep compatibility
- Patch @lingui/vite-plugin with moduleType for Rolldown compat
This commit is contained in:
Lucas Smith
2026-03-13 10:06:15 +11:00
parent 03ca3971a0
commit b3ef92feb9
10 changed files with 791 additions and 901 deletions
+14
View File
@@ -12,6 +12,20 @@ concurrency:
cancel-in-progress: true
jobs:
typecheck:
name: Typecheck
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 2
- uses: ./.github/actions/node-install
- name: Typecheck
run: npm run typecheck -w @documenso/remix
build_app:
name: Build App
runs-on: ubuntu-latest
+2 -1
View File
@@ -28,7 +28,8 @@ npm run build:server
# Copy over the entry point for the server.
cp server/main.js build/server/main.js
# Copy over all web.js translations
# Copy over all web.js translations.
# Rolldown preserveModules mirrors the source tree under build/server/hono/.
cp -r ../../packages/lib/translations build/server/hono/packages/lib/translations
# Time taken
+4 -13
View File
@@ -4,8 +4,8 @@
"type": "module",
"scripts": {
"build": "./.bin/build.sh",
"build:app": "npm run typecheck && cross-env NODE_ENV=production react-router build",
"build:server": "cross-env NODE_ENV=production rollup -c rollup.config.mjs",
"build:app": "react-router typegen && cross-env NODE_ENV=production react-router build",
"build:server": "cross-env NODE_ENV=production rolldown -c rolldown.config.mjs",
"dev": "npm run with:env -- react-router dev",
"dev:billing": "bash .bin/stripe-dev.sh",
"start": "npm run with:env -- cross-env NODE_ENV=production node build/server/main.js",
@@ -76,17 +76,11 @@
},
"devDependencies": {
"@babel/core": "^7.28.5",
"@babel/preset-react": "^7.28.5",
"@babel/preset-typescript": "^7.28.5",
"@lingui/babel-plugin-lingui-macro": "^5.6.0",
"@lingui/vite-plugin": "^5.6.0",
"@react-router/dev": "^7.12.0",
"@react-router/remix-routes-option-adapter": "^7.12.0",
"@rollup/plugin-babel": "^6.1.0",
"@rollup/plugin-commonjs": "^28.0.9",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-typescript": "^12.3.0",
"@types/babel__core": "^7.20.5",
"@types/content-disposition": "^0.5.9",
"@types/formidable": "^3.4.6",
"@types/luxon": "^3.7.1",
@@ -98,12 +92,9 @@
"cross-env": "^10.1.0",
"esbuild": "^0.27.0",
"remix-flat-routes": "^0.8.5",
"rollup": "^4.53.3",
"tsx": "^4.20.6",
"typescript": "5.6.2",
"vite": "^7.2.4",
"vite-plugin-babel-macros": "^1.0.6",
"vite-tsconfig-paths": "^5.1.4"
"vite": "^8.0.0"
},
"version": "2.8.0"
}
+57
View File
@@ -0,0 +1,57 @@
import { transformAsync } from '@babel/core';
import linguiMacroPlugin from '@lingui/babel-plugin-lingui-macro';
import { defineConfig } from 'rolldown';
const linguiMacroRE = /@lingui\/(core\/macro|react\/macro|macro)/;
/**
* Rolldown config for building the Hono server entry.
*
* Replaces the previous rollup.config.mjs. Rolldown provides built-in
* TypeScript, JSX (automatic runtime), JSON, CJS interop, and node
* resolution — so we only need a single plugin for lingui macro compilation.
*/
export default defineConfig({
input: 'server/router.ts',
output: {
dir: 'build/server/hono',
format: 'esm',
sourcemap: true,
preserveModules: true,
preserveModulesRoot: '.',
},
// Externalize all third-party deps from node_modules.
// Workspace packages (@documenso/*) resolve to their actual source paths
// (outside node_modules) via npm workspace symlinks, so they get bundled.
external: [/node_modules/],
platform: 'node',
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
tsconfigFilename: 'tsconfig.json',
},
plugins: [
{
name: 'lingui-macro',
async transform(code, id) {
if (!/\.(tsx?|jsx?)$/.test(id)) return;
if (!linguiMacroRE.test(code)) return;
const result = await transformAsync(code, {
babelrc: false,
configFile: false,
filename: id,
parserOpts: {
sourceType: 'module',
allowAwaitOutsideFunction: true,
plugins: ['typescript', 'jsx'],
},
plugins: [linguiMacroPlugin],
sourceMaps: true,
});
if (!result?.code) return;
return { code: result.code, map: result.map };
},
},
],
});
-54
View File
@@ -1,54 +0,0 @@
import linguiMacro from '@lingui/babel-plugin-lingui-macro';
import babel from '@rollup/plugin-babel';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import resolve from '@rollup/plugin-node-resolve';
import typescript from '@rollup/plugin-typescript';
import path from 'node:path';
/** @type {import('rollup').RollupOptions} */
const config = {
/**
* We specifically target the router.ts instead of the entry point so the rollup doesn't go through the
* already prebuilt RR7 server files.
*/
input: 'server/router.ts',
output: {
dir: 'build/server/hono',
format: 'esm',
sourcemap: true,
preserveModules: true,
preserveModulesRoot: '.',
},
external: [/node_modules/],
plugins: [
typescript({
noEmitOnError: true,
moduleResolution: 'bundler',
include: ['server/**/*', '../../packages/**/*', '../../packages/lib/translations/**/*'],
jsx: 'preserve',
}),
resolve({
rootDir: path.join(process.cwd(), '../..'),
preferBuiltins: true,
resolveOnly: [
'@documenso/api/*',
'@documenso/auth/*',
'@documenso/lib/*',
'@documenso/trpc/*',
'@documenso/email/*',
],
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
}),
json(),
commonjs(),
babel({
babelHelpers: 'bundled',
extensions: ['.ts', '.tsx'],
presets: ['@babel/preset-typescript', ['@babel/preset-react', { runtime: 'automatic' }]],
plugins: [linguiMacro],
}),
],
};
export default config;
+66 -10
View File
@@ -1,3 +1,5 @@
import { transformAsync } from '@babel/core';
import linguiMacroPlugin from '@lingui/babel-plugin-lingui-macro';
import { lingui } from '@lingui/vite-plugin';
import { reactRouter } from '@react-router/dev/vite';
import autoprefixer from 'autoprefixer';
@@ -6,15 +8,59 @@ import { createRequire } from 'node:module';
import path from 'node:path';
import tailwindcss from 'tailwindcss';
import { defineConfig, normalizePath } from 'vite';
import macrosPlugin from 'vite-plugin-babel-macros';
import type { Plugin } from 'vite';
import { viteStaticCopy } from 'vite-plugin-static-copy';
import tsconfigPaths from 'vite-tsconfig-paths';
const require = createRequire(import.meta.url);
const pdfjsDistPath = path.dirname(require.resolve('pdfjs-dist/package.json'));
const cMapsDir = normalizePath(path.join(pdfjsDistPath, 'cmaps'));
/**
* Targeted Lingui macro compilation plugin.
*
* Replaces the generic `vite-plugin-babel-macros` which ran a full Babel
* parse + transform on every single .ts/.tsx file. This plugin:
*
* 1. Skips files that don't contain lingui macro imports (fast string check).
* 2. Uses `@lingui/babel-plugin-lingui-macro` directly instead of going
* through the generic `babel-plugin-macros` wrapper.
*/
const linguiMacroRE = /@lingui\/(core\/macro|react\/macro|macro)/;
function linguiMacro(): Plugin {
return {
name: 'vite-plugin-lingui-macro',
enforce: 'pre',
async transform(code, id) {
if (id.includes('/node_modules/')) return;
const [filepath] = id.split('?');
if (!/\.(tsx?|jsx?)$/.test(filepath)) return;
// Fast bail-out: skip files that don't reference lingui macros.
if (!linguiMacroRE.test(code)) return;
const result = await transformAsync(code, {
babelrc: false,
configFile: false,
filename: id,
sourceFileName: filepath,
parserOpts: {
sourceType: 'module',
allowAwaitOutsideFunction: true,
plugins: ['typescript', 'jsx'],
},
plugins: [linguiMacroPlugin],
sourceMaps: true,
});
if (!result?.code) return;
return { code: result.code, map: result.map };
},
};
}
/**
* Note: We load the env variables externally so we can have runtime enviroment variables
* for docker.
@@ -41,15 +87,18 @@ export default defineConfig({
],
}),
reactRouter(),
macrosPlugin(),
lingui(),
tsconfigPaths(),
linguiMacro(),
// lingui() returns two plugins: a macro error reporter and the .po catalog
// compiler. The error reporter adds a resolveId hook on every module to
// detect uncompiled macros — redundant since linguiMacro() already compiles
// them, and it was consuming 51% of plugin time in the build.
...lingui().filter((p) => 'name' in p && p.name !== 'vite-plugin-lingui-report-macro-error'),
serverAdapter({
entry: 'server/router.ts',
}),
],
ssr: {
noExternal: ['react-dropzone', 'plausible-tracker'],
noExternal: ['react-dropzone'],
external: [
'@napi-rs/canvas',
'@node-rs/bcrypt',
@@ -62,10 +111,12 @@ export default defineConfig({
],
},
optimizeDeps: {
entries: ['./app/**/*', '../../packages/ui/**/*', '../../packages/lib/**/*'],
// Only scan the app directory — workspace packages (ui, lib) are
// discovered transitively through app imports. The previous config
// scanned ~1,000 files including server-only code from packages/lib.
entries: ['./app/**/*'],
include: ['prop-types', 'file-selector', 'attr-accept'],
exclude: [
'node_modules',
'@napi-rs/canvas',
'@node-rs/bcrypt',
'sharp',
@@ -75,6 +126,7 @@ export default defineConfig({
],
},
resolve: {
tsconfigPaths: true,
alias: {
https: 'node:https',
'.prisma/client/default': path.resolve(
@@ -89,11 +141,15 @@ export default defineConfig({
},
},
/**
* Note: Re run rollup again to build the server afterwards.
* Note: Re run rolldown again to build the server afterwards.
*
* See rollup.config.mjs which is used for that.
* See rolldown.config.mjs which is used for that.
*/
build: {
// LightningCSS (Vite 8 default) is stricter and rejects nested @page
// rules and some Tailwind theme() edge cases. Use esbuild for CSS
// minification until LightningCSS support matures or the CSS is updated.
cssMinify: 'esbuild',
rollupOptions: {
external: [
'@napi-rs/canvas',
+607 -818
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -79,7 +79,7 @@
"superjson": "^2.2.5",
"syncpack": "^14.0.0-alpha.27",
"turbo": "^1.13.4",
"vite": "^7.2.4",
"vite": "^8.0.0",
"vite-plugin-static-copy": "^3.1.4",
"zod-openapi": "^4.2.4",
"zod-prisma-types": "3.3.5"
@@ -104,6 +104,7 @@
"lodash": "4.17.23",
"pdfjs-dist": "5.4.296",
"typescript": "5.6.2",
"vite": "$vite",
"zod": "$zod",
"fumadocs-mdx": {
"zod": "^4.3.5"
+3 -4
View File
@@ -30,15 +30,14 @@ const Card = React.forwardRef<HTMLDivElement, CardProps>(
} as React.CSSProperties
}
className={cn(
'bg-background text-foreground group relative rounded-lg border-2',
'group relative rounded-lg border-2 bg-background text-foreground',
{
'backdrop-blur-[2px]': backdropBlur,
'gradient-border-mask before:pointer-events-none before:absolute before:-inset-[2px] before:rounded-lg before:p-[2px] before:[background:linear-gradient(var(--card-gradient-degrees),theme(colors.primary.DEFAULT/50%)_5%,theme(colors.border/80%)_30%)]':
gradient,
'dark:gradient-border-mask before:pointer-events-none before:absolute before:-inset-[2px] before:rounded-lg before:p-[2px] before:[background:linear-gradient(var(--card-gradient-degrees),theme(colors.primary.DEFAULT/70%)_5%,theme(colors.border/80%)_30%)]':
gradient,
'shadow-[0_0_0_4px_theme(colors.gray.100/70%),0_0_0_1px_theme(colors.gray.100/70%),0_0_0_0.5px_var(colors.primary.DEFAULT/70%)]':
true,
'shadow-[0_0_0_4px_theme(colors.gray.100/70%),0_0_0_1px_theme(colors.gray.100/70%),0_0_0_0.5px_theme(colors.primary.DEFAULT/70%)]': true,
'dark:shadow-[0]': true,
},
className,
@@ -77,7 +76,7 @@ const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p ref={ref} className={cn('text-muted-foreground text-sm', className)} {...props} />
<p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
));
CardDescription.displayName = 'CardDescription';
+36
View File
@@ -0,0 +1,36 @@
diff --git a/node_modules/@lingui/vite-plugin/dist/index.cjs b/node_modules/@lingui/vite-plugin/dist/index.cjs
index cb4db5d..c0a881a 100644
--- a/node_modules/@lingui/vite-plugin/dist/index.cjs
+++ b/node_modules/@lingui/vite-plugin/dist/index.cjs
@@ -114,8 +114,12 @@ You see this error because \`failOnMissing=true\` in Vite Plugin configuration.`
}
return {
code,
- map: null
+ map: null,
// provide source map if available
+ // Vite 8+ (Rolldown) auto-detects module types by file extension.
+ // Since .po files are transformed to JS, we must explicitly declare
+ // the module type to avoid misinterpretation.
+ moduleType: "js"
};
}
}
diff --git a/node_modules/@lingui/vite-plugin/dist/index.mjs b/node_modules/@lingui/vite-plugin/dist/index.mjs
index 609d48d..66fa6cc 100644
--- a/node_modules/@lingui/vite-plugin/dist/index.mjs
+++ b/node_modules/@lingui/vite-plugin/dist/index.mjs
@@ -106,8 +106,12 @@ You see this error because \`failOnMissing=true\` in Vite Plugin configuration.`
}
return {
code,
- map: null
+ map: null,
// provide source map if available
+ // Vite 8+ (Rolldown) auto-detects module types by file extension.
+ // Since .po files are transformed to JS, we must explicitly declare
+ // the module type to avoid misinterpretation.
+ moduleType: "js"
};
}
}