Files
zammad/vite.config.mjs
Vladimir Sheremet 4e489121c5 Maintenance: Upgrade to Vitest 5 and make the frontend tests 4 times faster.
Vitest is updated to v5 and the pool is switched to `vmForks`, which cuts the
runtime of the frontend suite from ~250s to ~69s.

The new jsdom no longer allows `window.navigation` to be overridden, so the
tests that need it now go through a small `#shared/utils/navigation.ts`
indirection which can be mocked instead.

Co-authored-by: Vladimir Sheremet <vsheremet@cloudflare.com>
2026-09-04 11:37:22 +02:00

246 lines
8.4 KiB
JavaScript

// Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
/// <reference types="vitest/config" />
import { createRequire } from 'module'
import { readFileSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { homedir } from 'os'
import tailwindcss from '@tailwindcss/vite'
import VuePlugin from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
import { VitePWA } from 'vite-plugin-pwa'
import { discoverAddonWeaveRules } from './app/frontend/build/addonWeave/discoverRules.mjs'
import addonWeavePlugin from './app/frontend/build/addonWeave/plugin.mjs'
import svgIconsPlugin from './app/frontend/build/iconsPlugin.mjs'
import { discoverSkinMarkerClashes } from './app/frontend/build/skinMarker/discoverClashes.mjs'
import skinMarkerPlugin from './app/frontend/build/skinMarker/plugin.mjs'
const dir = dirname(fileURLToPath(import.meta.url))
const SSL_PATH = resolve(homedir(), '.local/state/localhost.rb')
const isEnvBooleanSet = (value) => ['true', '1'].includes(value)
export default defineConfig(async ({ mode, command }) => {
const isTesting = ['test', 'cypress'].includes(mode)
const isBuild = command === 'build'
const require = createRequire(import.meta.url)
// Build-time addon weave: discover every installed addon's *.weave.mjs manifest
// and rewrite the targeted core SFC source before the Vue compiler runs.
const addonWeaveRules = await discoverAddonWeaveRules()
const plugins = [
// enforce: 'pre' → both run before VuePlugin compiles the SFC.
addonWeavePlugin(addonWeaveRules),
// Auto-stamp data-zammad-target on native SFC roots — the stable custom-CSS
// hook. Skipped under test, where it would perturb component DOM snapshots.
...(isTesting || isEnvBooleanSet(process.env.VITE_TEST_MODE)
? []
: [skinMarkerPlugin(discoverSkinMarkerClashes())]),
tailwindcss(),
VuePlugin({
template: {
compilerOptions: {
nodeTransforms:
isTesting || isEnvBooleanSet(process.env.VITE_TEST_MODE)
? []
: [require('./app/frontend/build/transforms/transformTestId.js')],
},
},
}),
svgIconsPlugin(),
]
if (!isTesting || isBuild) {
// Ruby plugin is not needed inside of the vitest context and has some side effects.
const { default: RubyPlugin } = require('vite-plugin-ruby')
plugins.push(RubyPlugin())
plugins.push(
...VitePWA({
disable: isTesting || isEnvBooleanSet(process.env.VITE_TEST_MODE),
// should be generated on ruby side
manifest: false,
registerType: 'prompt',
srcDir: 'apps/mobile/sw',
filename: 'sw.ts',
includeManifestIcons: false,
injectRegister: null,
strategies: 'injectManifest',
}),
)
}
if (isTesting) {
// make sure timezone is always correct - in CLI and in the extension
process.env.TZ = 'utc'
}
let https = false
// vite-ruby controlls this variable, it's either "true" or "false"
if (isEnvBooleanSet(process.env.VITE_RUBY_HTTPS)) {
const SSL_CERT = readFileSync(resolve(SSL_PATH, 'localhost.crt'))
const SSL_KEY = readFileSync(resolve(SSL_PATH, 'localhost.key'))
https = {
cert: SSL_CERT,
key: SSL_KEY,
}
}
let publicDir
if (!isBuild) {
publicDir = resolve(dir, 'public')
}
return {
publicDir,
build: {
rolldownOptions: {
output: {
manualChunks(id) {
const manualChunksMap = {
lodash: ['lodash-es'],
vue: ['vue', 'vue-router', 'pinia'],
datepicker: ['@vuepic/vue-datepicker'],
linkifyjs: ['linkifyjs', 'linkify-string'],
graphql: [
'graphql',
// 🚨 'graphql-ruby-client',
// Important: don't include the package root here, it pulls in the Node-only `sync` entry
// which imports fs/path/crypto/http/... and triggers Vite "externalized for browser" warnings.
'graphql-ruby-client/subscriptions/ActionCableLink',
'graphql-tag',
'@apollo/client',
'@vue/apollo-composable',
'@rails/actioncable',
],
formkit: [
'@formkit/core',
'@formkit/dev',
// '@formkit/drag-and-drop', # is not used in mobile
'@formkit/i18n',
'@formkit/inputs',
'@formkit/rules',
'@formkit/tailwindcss',
'@formkit/themes',
'@formkit/utils',
'@formkit/validation',
'@formkit/vue',
],
}
if (!id.includes('node_modules')) return undefined
for (const [chunkName, packages] of Object.entries(manualChunksMap)) {
if (
packages.some(
(pkg) =>
id.includes(`node_modules/${pkg}/`) ||
id.includes(`node_modules/.pnpm/${pkg.replace('/', '+').replace('@', '')}`),
)
) {
return chunkName
}
}
return undefined
},
},
},
},
resolve: {
preserveSymlinks: isEnvBooleanSet(process.env.PRESERVE_SYMLINKS),
alias: {
'^vue-easy-lightbox$': 'vue-easy-lightbox/dist/external-css/vue-easy-lightbox.esm.min.js',
// In non-test builds, alias fake-timers to a tiny shim so Vite doesn't resolve Node core deps
...(isTesting
? {}
: {
'@sinonjs/fake-timers': resolve(dir, 'app/frontend/build/shims/fake-timers.ts'),
}),
},
},
server: {
https,
// The HMR websocket cannot ride the Rails asset proxy (no upgrade support) — point
// the client directly at the Vite dev server port instead of the page origin.
hmr: {
host: process.env.VITE_RUBY_HOST || 'localhost',
clientPort: Number(process.env.VITE_RUBY_PORT || 3036),
},
watch: {
ignored: isTesting
? []
: [
'**/*.spec.*',
'**/__tests__/**/*',
(path) => !path.includes('app/frontend') || path.includes('frontend/tests'),
],
},
},
define: {
VITE_TEST_MODE:
isEnvBooleanSet(process.env.VITEST) || isEnvBooleanSet(process.env.VITE_TEST_MODE),
},
test: {
globals: true,
// narrowing down test folder speeds up fast-glob in Vitest
root: './app/frontend',
setupFiles: ['./tests/vitest.setup.ts'],
environment: 'jsdom',
pool: 'vmForks',
env: {
SA11Y_RULESET: 'full',
},
clearMocks: true,
css: false,
testTimeout: isEnvBooleanSet(process.env.CI) ? 30_000 : 5_000,
unstubGlobals: true,
// Persistent cache between reruns, invalidates if any dependency is updated.
// Vitest doesn't cache files with `import.meta.glob` inside, so it might be a good
// idea to put them all inside as few files as possible just for a small perf boost.
// The perf difference is noticeable when running a single/few tests, but has
// negligible impact when running the whole suite due to parallelisation.
fsModuleCache: true,
// Node v25+ enables experimental webstorage by default (stability: release candidate).
// Without --localstorage-file, Node provides localStorage as an empty object (no methods).
// This conflicts with jsdom's full localStorage implementation needed for tests.
...(parseInt(process.versions.node, 10) >= 25
? { execArgv: ['--no-experimental-webstorage'] }
: {}),
onConsoleLog(log) {
if (
log.includes('Not implemented: navigation') ||
log.includes('<Suspense> is an experimental feature')
)
return false
},
// perf improvements
server: {
deps: {
// vue-datepicker imports from date-fns and we don't want it to import ES version
inline: ['@vuepic/vue-datepicker'],
},
},
alias: [
// ESM version of date-fns exports a lot of extra files which takes 500ms to load on M4 Mac
// CJS version takes ~175ms which is also a lot, but not as much
{
find: /^date-fns$/,
replacement: join(dirname(require.resolve('date-fns/package.json')), 'index.cjs'),
},
],
},
plugins,
}
})