chore: upgrade to vite 6 (#14363)

Upgrades the frontend toolchain to Vite 6 and tidies up the build config
along the way. Behavior is unchanged for end users; this is dev/build
infra.

## What changed
- `vite` 5.4 → 6.4, `@vitejs/plugin-vue` → 5.2, `vite-plugin-ruby` → 5.2
(with matching `vite_rails`/`vite_ruby` gem bumps).
- Dropped the `vite-node` 2.0.1 pnpm override — no longer needed now
that vitest 3 runs on Vite 6 directly.
- Split the single `vite.config.ts` into:
- `vite.config.ts` (app), `vite.lib.config.ts` (SDK), `vite.shared.ts`
(aliases / Vue options), `vitest.config.ts` (tests).
- `pnpm build:sdk` now selects the SDK config explicitly instead of
branching on `BUILD_MODE=library`. SDK output path is unchanged
(`public/packs/js/sdk.js`).

No changes needed to Docker images, deployment scripts, or CI — Node 24
and pnpm 10 are already past Vite 6's floor, and the rake
`assets:precompile` hook still drives the SDK build via `pnpm`.

## How to test
- `pnpm dev` and verify the dashboard, widget, and survey routes load
and HMR works.
- Load a Chatwoot site widget on a test page and confirm `sdk.js` is
served and the widget mounts.
- `RAILS_ENV=production bundle exec rake assets:precompile` and confirm
`public/packs/js/sdk.js` plus the rest of the manifest are produced.
- `pnpm test` for the JS suite.

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
This commit is contained in:
Shivam Mishra 2026-06-02 17:01:37 +05:30 committed by GitHub
parent 95cb3a7ad8
commit 170b64d1f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 551 additions and 314 deletions

View File

@ -996,11 +996,13 @@ GEM
activemodel (>= 3.2)
mail (~> 2.5)
version_gem (1.1.4)
vite_rails (3.0.17)
railties (>= 5.1, < 8)
vite_rails (3.10.0)
railties (>= 5.1, < 9)
vite_ruby (~> 3.0, >= 3.2.2)
vite_ruby (3.8.0)
vite_ruby (3.10.2)
dry-cli (>= 0.7, < 2)
logger (~> 1.6)
mutex_m
rack-proxy (~> 0.6, >= 0.6.1)
zeitwerk (~> 2.2)
warden (1.2.9)

View File

@ -10,10 +10,12 @@ vi.mock('shared/helpers/mitt', () => ({
vi.mock('dashboard/helper/AnalyticsHelper/index', async importOriginal => {
const actual = await importOriginal();
actual.default = {
track: vi.fn(),
return {
...actual,
default: {
track: vi.fn(),
},
};
return actual;
});
describe('useTrack', () => {

View File

@ -16,10 +16,12 @@ vi.mock('vue-i18n');
vi.mock('dashboard/api/captain/tasks');
vi.mock('dashboard/helper/AnalyticsHelper/index', async importOriginal => {
const actual = await importOriginal();
actual.default = {
track: vi.fn(),
return {
...actual,
default: {
track: vi.fn(),
},
};
return actual;
});
vi.mock('dashboard/helper/AnalyticsHelper/events', () => ({
CAPTAIN_EVENTS: {

View File

@ -12,7 +12,7 @@
"start:test": "RAILS_ENV=test foreman start -f ./Procfile.test",
"dev": "overmind start -f ./Procfile.dev",
"ruby:prettier": "bundle exec rubocop -a",
"build:sdk": "BUILD_MODE=library vite build",
"build:sdk": "vite build --config vite.lib.config.ts",
"prepare": "husky install",
"size": "size-limit",
"story:dev": "histoire dev",
@ -53,7 +53,7 @@
"@tailwindcss/typography": "^0.5.19",
"@tanstack/vue-table": "^8.20.5",
"@twilio/voice-sdk": "^2.12.4",
"@vitejs/plugin-vue": "^5.1.4",
"@vitejs/plugin-vue": "^5.2.4",
"@vue/compiler-sfc": "^3.5.8",
"@vuelidate/core": "^2.0.3",
"@vuelidate/validators": "^2.0.4",
@ -146,8 +146,8 @@
"prosemirror-model": "^1.22.3",
"size-limit": "^8.2.4",
"tailwindcss": "^3.4.19",
"vite": "^5.4.21",
"vite-plugin-ruby": "^5.0.0",
"vite": "6.4.2",
"vite-plugin-ruby": "^5.2.1",
"vitest": "3.0.5"
},
"engines": {
@ -161,8 +161,7 @@
},
"pnpm": {
"overrides": {
"vite-node": "2.0.1",
"vite": "5.4.21",
"vite": "6.4.2",
"vitest": "3.0.5",
"minimatch@<4": "3.1.5",
"minimatch@>=9.0.0 <9.0.7": "9.0.9",

File diff suppressed because it is too large Load Diff

View File

@ -1,50 +1,11 @@
/// <reference types="vitest" />
/**
What's going on with library mode?
Glad you asked, here's a quick rundown:
1. vite-plugin-ruby will automatically bring all the entrypoints like dashbord and widget as input to vite.
2. vite needs to be in library mode to build the SDK as a single file. (UMD) format and set `inlineDynamicImports` to true.
3. But when setting `inlineDynamicImports` to true, vite will not be able to handle mutliple entrypoints.
This puts us in a deadlock, now there are two ways around this, either add another separate build pipeline to
the app using vanilla rollup or rspack or something. The second option is to remove sdk building from the main pipeline
and build it separately using Vite itself, toggled by an ENV variable.
`BUILD_MODE=library bin/vite build` should build only the SDK and save it to `public/packs/js/sdk.js`
`bin/vite build` will build the rest of the app as usual. But exclude the SDK.
We need to edit the `asset:precompile` rake task to include the SDK in the precompile list.
*/
import { defineConfig } from 'vite';
import ruby from 'vite-plugin-ruby';
import path from 'path';
import vue from '@vitejs/plugin-vue';
import { aliases, vueOptions } from './vite.shared';
import yaml from '@rollup/plugin-yaml';
const isLibraryMode = process.env.BUILD_MODE === 'library';
const isTestMode = process.env.TEST === 'true';
const vueOptions = {
template: {
compilerOptions: {
isCustomElement: tag => ['ninja-keys'].includes(tag),
},
},
};
let plugins = [ruby(), vue(vueOptions), yaml()];
if (isLibraryMode) {
plugins = [];
} else if (isTestMode) {
plugins = [vue(vueOptions), yaml()];
}
export default defineConfig({
plugins: plugins,
plugins: [ruby(), vue(vueOptions), yaml()],
css: {
preprocessorOptions: {
scss: {
@ -52,74 +13,5 @@ export default defineConfig({
},
},
},
build: {
rollupOptions: {
output: {
// [NOTE] when not in library mode, no new keys will be addedd or overwritten
// setting dir: isLibraryMode ? 'public/packs' : undefined will not work
...(isLibraryMode
? {
dir: 'public/packs',
entryFileNames: chunkInfo => {
if (chunkInfo.name === 'sdk') {
return 'js/sdk.js';
}
return '[name].js';
},
}
: {}),
inlineDynamicImports: isLibraryMode, // Disable code-splitting for SDK
},
},
lib: isLibraryMode
? {
entry: path.resolve(__dirname, './app/javascript/entrypoints/sdk.js'),
formats: ['iife'], // IIFE format for single file
name: 'sdk',
}
: undefined,
},
resolve: {
alias: {
vue: 'vue/dist/vue.esm-bundler.js',
components: path.resolve('./app/javascript/dashboard/components'),
next: path.resolve('./app/javascript/dashboard/components-next'),
v3: path.resolve('./app/javascript/v3'),
dashboard: path.resolve('./app/javascript/dashboard'),
helpers: path.resolve('./app/javascript/shared/helpers'),
shared: path.resolve('./app/javascript/shared'),
survey: path.resolve('./app/javascript/survey'),
widget: path.resolve('./app/javascript/widget'),
assets: path.resolve('./app/javascript/dashboard/assets'),
},
},
test: {
environment: 'jsdom',
include: ['app/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
coverage: {
reporter: ['lcov', 'text'],
include: ['app/**/*.js', 'app/**/*.vue'],
exclude: [
'app/**/*.@(spec|stories|routes).js',
'**/specs/**/*',
'**/i18n/**/*',
],
},
globals: true,
outputFile: 'coverage/sonar-report.xml',
pool: 'threads',
poolOptions: {
threads: {
singleThread: false,
},
},
server: {
deps: {
inline: ['tinykeys', '@material/mwc-icon'],
},
},
setupFiles: ['fake-indexeddb/auto', 'vitest.setup.js'],
mockReset: true,
clearMocks: true,
},
resolve: { alias: aliases },
});

33
vite.lib.config.ts Normal file
View File

@ -0,0 +1,33 @@
/*
* SDK library build.
*
* vite-plugin-ruby pulls every entrypoint as input, but the SDK needs to ship
* as a single IIFE file (`inlineDynamicImports: true`), which is incompatible
* with multiple entrypoints. So the SDK gets its own pipeline:
*
* vite build --config vite.lib.config.ts public/packs/js/sdk.js
*
* The `assets:precompile` rake task runs this alongside the main app build.
*/
import { defineConfig } from 'vite';
import path from 'path';
import { aliases } from './vite.shared';
export default defineConfig({
build: {
rollupOptions: {
output: {
dir: 'public/packs',
entryFileNames: chunkInfo =>
chunkInfo.name === 'sdk' ? 'js/sdk.js' : '[name].js',
inlineDynamicImports: true,
},
},
lib: {
entry: path.resolve(__dirname, './app/javascript/entrypoints/sdk.js'),
formats: ['iife'],
name: 'sdk',
},
},
resolve: { alias: aliases },
});

22
vite.shared.ts Normal file
View File

@ -0,0 +1,22 @@
import path from 'path';
export const aliases = {
vue: 'vue/dist/vue.esm-bundler.js',
components: path.resolve('./app/javascript/dashboard/components'),
next: path.resolve('./app/javascript/dashboard/components-next'),
v3: path.resolve('./app/javascript/v3'),
dashboard: path.resolve('./app/javascript/dashboard'),
helpers: path.resolve('./app/javascript/shared/helpers'),
shared: path.resolve('./app/javascript/shared'),
survey: path.resolve('./app/javascript/survey'),
widget: path.resolve('./app/javascript/widget'),
assets: path.resolve('./app/javascript/dashboard/assets'),
};
export const vueOptions = {
template: {
compilerOptions: {
isCustomElement: (tag: string) => ['ninja-keys'].includes(tag),
},
},
};

39
vitest.config.ts Normal file
View File

@ -0,0 +1,39 @@
/// <reference types="vitest" />
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';
import { aliases, vueOptions } from './vite.shared';
import yaml from '@rollup/plugin-yaml';
export default defineConfig({
plugins: [vue(vueOptions), yaml()],
resolve: { alias: aliases },
test: {
environment: 'jsdom',
include: ['app/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
coverage: {
reporter: ['lcov', 'text'],
include: ['app/**/*.js', 'app/**/*.vue'],
exclude: [
'app/**/*.@(spec|stories|routes).js',
'**/specs/**/*',
'**/i18n/**/*',
],
},
globals: true,
outputFile: 'coverage/sonar-report.xml',
pool: 'threads',
poolOptions: {
threads: {
singleThread: false,
},
},
server: {
deps: {
inline: ['tinykeys', '@material/mwc-icon'],
},
},
setupFiles: ['fake-indexeddb/auto', 'vitest.setup.js'],
mockReset: true,
clearMocks: true,
},
});