Files
cinny/vite.config.js
T

319 lines
12 KiB
JavaScript
Raw Normal View History

2022-12-20 20:47:51 +05:30
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { sentryVitePlugin } from '@sentry/vite-plugin';
2022-12-20 20:47:51 +05:30
import { wasm } from '@rollup/plugin-wasm';
import inject from '@rollup/plugin-inject';
2022-12-20 20:47:51 +05:30
import { viteStaticCopy } from 'vite-plugin-static-copy';
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
2024-09-07 21:45:55 +08:00
import { VitePWA } from 'vite-plugin-pwa';
2025-05-18 10:53:56 +05:30
import fs from 'fs';
import path from 'path';
import buildConfig from './build.config';
2022-12-20 20:47:51 +05:30
const copyFiles = {
targets: [
2026-03-07 18:03:32 +11:00
{
// Element Call's dist must land flat in public/element-call/ so the call
// widget URL (/public/element-call/index.html) resolves. v4.x of
// vite-plugin-static-copy preserves the full source path under dest, so
// we strip the 4 leading segments of the source base
// (node_modules/@element-hq/element-call-embedded/dist) — mirroring the
// stripBase pattern used by the android/locales targets below. The old
// `rename: 'element-call'` form silently produced
// public/node_modules/.../dist/ under v4.x, 404ing the widget (calls
// broke on cinny-desktop; web only worked because its deployed copy was
// a stale artifact from before the vite-plugin-static-copy v4 bump).
src: 'node_modules/@element-hq/element-call-embedded/dist',
dest: 'public/element-call',
rename: { stripBase: 4 },
2026-03-07 18:03:32 +11:00
},
2022-12-20 20:47:51 +05:30
{
src: 'config.json',
dest: '',
},
{
src: 'public/manifest.json',
dest: '',
rename: { stripBase: true },
},
2022-12-20 20:47:51 +05:30
{
src: 'public/res/android',
dest: 'public/',
rename: { stripBase: 2 },
},
{
src: 'public/locales',
dest: 'public/',
rename: { stripBase: 1 },
},
{
src: 'public/fonts',
dest: '',
rename: { stripBase: 1 },
},
2022-12-20 20:47:51 +05:30
],
};
2022-12-20 20:47:51 +05:30
function copyPdfWorker() {
return {
name: 'copy-pdf-worker',
closeBundle() {
const src = path.resolve('node_modules/pdfjs-dist/build/pdf.worker.min.mjs');
const dest = path.resolve('dist/pdf.worker.min.js');
if (fs.existsSync(src)) fs.copyFileSync(src, dest);
},
};
}
// Lotus ML noise suppression: ship the RNNoise worklet/wasm + our getUserMedia
// shim alongside the vendored Element Call bundle, and inject the shim <script>
// into EC's index.html so it runs before EC captures the mic. Runs after
// viteStaticCopy has populated dist/public/element-call (closeBundle order).
function lotusDenoise() {
return {
name: 'lotus-denoise',
closeBundle() {
const ecDir = path.resolve('dist/public/element-call');
if (!fs.existsSync(ecDir)) return;
const denoiseDir = path.join(ecDir, 'denoise');
fs.mkdirSync(denoiseDir, { recursive: true });
const sapphi = path.resolve('node_modules/@sapphi-red/web-noise-suppressor/dist');
// All bundled denoise assets are REQUIRED: every entry backs a model the
// UI can select (RNNoise, Speex) or the optional noise gate. A missing
// source means a partial/changed install would otherwise silently ship a
// broken ML feature (worklet 404 -> raw mic), so we fail the build instead.
const assets = [
[
path.join(sapphi, 'rnnoise/workletProcessor.js'),
path.join(denoiseDir, 'rnnoiseWorklet.js'),
],
[path.join(sapphi, 'rnnoise.wasm'), path.join(denoiseDir, 'rnnoise.wasm')],
[path.join(sapphi, 'rnnoise_simd.wasm'), path.join(denoiseDir, 'rnnoise_simd.wasm')],
[path.join(sapphi, 'speex/workletProcessor.js'), path.join(denoiseDir, 'speexWorklet.js')],
[path.join(sapphi, 'speex.wasm'), path.join(denoiseDir, 'speex.wasm')],
[
path.join(sapphi, 'noiseGate/workletProcessor.js'),
path.join(denoiseDir, 'noiseGateWorklet.js'),
],
];
const missing = assets.filter(([s]) => !fs.existsSync(s)).map(([s]) => s);
if (missing.length > 0) {
throw new Error(
`[lotus-denoise] Required denoise asset(s) missing — build aborted to avoid shipping a broken ML feature:\n ${missing.join('\n ')}`,
);
}
assets.forEach(([s, d]) => fs.copyFileSync(s, d));
// DTLN (@workadventure/noise-suppression): unlike the flat sapphi
// worklets, this package is a self-contained ES module that resolves its
// own AudioWorklet processor, LiteRT WASM runtime and TFLite models via
// `import.meta.url`. So we copy its whole dist/ tree intact to
// denoise/workadventure/ (preserving assets/ + vendor/) and let the shim
// dynamic-import denoise/workadventure/audio-worklet.js at runtime. The
// entry module is required — fail the build if the install is broken.
const dtlnSrc = path.resolve('node_modules/@workadventure/noise-suppression/dist');
const dtlnEntry = path.join(dtlnSrc, 'audio-worklet.js');
if (!fs.existsSync(dtlnEntry)) {
throw new Error(
`[lotus-denoise] DTLN entry missing (${dtlnEntry}) — build aborted. ` +
'Run `npm ci` to install @workadventure/noise-suppression.',
);
}
fs.cpSync(dtlnSrc, path.join(denoiseDir, 'workadventure'), { recursive: true });
// DeepFilterNet 3 (deepfilternet3-noise-filter): the npm package ships only
// its ESM (index.esm.js) with the AudioWorklet processor + wasm-bindgen glue
// INLINED as a string (loaded via a Blob URL, no CDN for the worklet). The
// only runtime CDN fetches are its single-threaded `df_bg.wasm` and the
// ONNX `DeepFilterNet3_onnx.tar.gz` model — which we VENDOR locally (under
// build/denoise-vendor/deepfilternet/) and serve, overriding the package's
// cdnUrl to our self-hosted base. This keeps the feature CDN-free / Tauri-CSP
// safe. We copy the ESM (the shim dynamic-imports it, mirroring DTLN) plus
// the vendored assets, preserving the package's expected v2/... layout. All
// are required — a missing entry means a broken install, so fail the build.
const dfnEsm = path.resolve('node_modules/deepfilternet3-noise-filter/dist/index.esm.js');
const dfnVendor = path.resolve('build/denoise-vendor/deepfilternet');
const dfnAssets = [
[dfnEsm, path.join(denoiseDir, 'deepfilternet/index.esm.js')],
[
path.join(dfnVendor, 'v2/pkg/df_bg.wasm'),
path.join(denoiseDir, 'deepfilternet/v2/pkg/df_bg.wasm'),
],
[
path.join(dfnVendor, 'v2/models/DeepFilterNet3_onnx.tar.gz'),
path.join(denoiseDir, 'deepfilternet/v2/models/DeepFilterNet3_onnx.tar.gz'),
],
];
const dfnMissing = dfnAssets.filter(([s]) => !fs.existsSync(s)).map(([s]) => s);
if (dfnMissing.length > 0) {
throw new Error(
'[lotus-denoise] DeepFilterNet 3 asset(s) missing — build aborted to avoid ' +
'shipping a broken ML feature:\n ' +
dfnMissing.join('\n ') +
'\n(Run `npm ci`; the vendored wasm/model live under build/denoise-vendor/deepfilternet/.)',
);
}
dfnAssets.forEach(([s, d]) => {
fs.mkdirSync(path.dirname(d), { recursive: true });
fs.copyFileSync(s, d);
});
const shimSrc = path.resolve('build/lotus-denoise.js');
if (!fs.existsSync(shimSrc)) {
throw new Error(`[lotus-denoise] Missing shim source ${shimSrc} — build aborted.`);
}
fs.copyFileSync(shimSrc, path.join(ecDir, 'lotus-denoise.js'));
// Inject the shim <script> into Element Call's index.html so it runs
// before EC captures the mic. Verify the injection actually landed —
// if EC's bundle ever drops its deferred module entry the replace would
// no-op and ML would silently never engage, so fail loudly.
const indexPath = path.join(ecDir, 'index.html');
if (fs.existsSync(indexPath)) {
let html = fs.readFileSync(indexPath, 'utf8');
if (!html.includes('lotus-denoise.js')) {
// Classic (non-deferred) script runs before EC's deferred module entry.
html = html.replace(
/<script type="module"/,
'<script src="./lotus-denoise.js"></script><script type="module"',
);
if (!html.includes('lotus-denoise.js')) {
throw new Error(
'[lotus-denoise] Failed to inject shim into Element Call index.html ' +
'(no `<script type="module">` entry found) — build aborted.',
);
}
fs.writeFileSync(indexPath, html);
}
}
},
};
}
2025-05-18 10:53:56 +05:30
function serverMatrixSdkCryptoWasm(wasmFilePath) {
return {
name: 'vite-plugin-serve-matrix-sdk-crypto-wasm',
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (req.url === wasmFilePath) {
2026-03-07 18:03:32 +11:00
const resolvedPath = path.join(
path.resolve(),
'/node_modules/@matrix-org/matrix-sdk-crypto-wasm/pkg/matrix_sdk_crypto_wasm_bg.wasm',
2026-03-07 18:03:32 +11:00
);
2025-05-18 10:53:56 +05:30
if (fs.existsSync(resolvedPath)) {
res.setHeader('Content-Type', 'application/wasm');
res.setHeader('Cache-Control', 'no-cache');
const fileStream = fs.createReadStream(resolvedPath);
fileStream.pipe(res);
} else {
res.writeHead(404);
res.end('File not found');
}
} else {
next();
}
});
},
};
}
const vendorChunks = (id) => {
if (id.includes('node_modules/matrix-js-sdk')) return 'matrix-sdk';
if (id.includes('node_modules/react-dom')) return 'react-dom';
if (
id.includes('node_modules/react-router-dom') ||
id.includes('node_modules/@remix-run') ||
id.includes('node_modules/react-router/')
)
return 'router';
if (id.includes('node_modules/@tanstack')) return 'react-query';
if (id.includes('node_modules/linkify')) return 'linkify';
if (id.includes('node_modules/dompurify')) return 'dompurify';
if (id.includes('node_modules/@sentry')) return 'sentry';
if (id.includes('node_modules/i18next') || id.includes('node_modules/react-i18next'))
return 'i18n';
if (id.includes('node_modules/jotai')) return 'jotai';
if (id.includes('node_modules/immer')) return 'immer';
if (id.includes('node_modules/folds')) return 'folds';
if (id.includes('node_modules/emojibase')) return 'emojibase';
};
2022-12-20 20:47:51 +05:30
export default defineConfig({
appType: 'spa',
publicDir: false,
2024-01-21 23:50:56 +11:00
base: buildConfig.base,
2022-12-20 20:47:51 +05:30
server: {
port: 8080,
host: true,
2025-05-18 10:53:56 +05:30
fs: {
allow: ['..'],
},
2022-12-20 20:47:51 +05:30
},
plugins: [
2025-05-18 10:53:56 +05:30
serverMatrixSdkCryptoWasm('/node_modules/.vite/deps/pkg/matrix_sdk_crypto_wasm_bg.wasm'),
2022-12-20 20:47:51 +05:30
viteStaticCopy(copyFiles),
2023-06-12 21:15:23 +10:00
vanillaExtractPlugin(),
2022-12-20 20:47:51 +05:30
wasm(),
react(),
copyPdfWorker(),
lotusDenoise(),
...(process.env.SENTRY_AUTH_TOKEN
? [
sentryVitePlugin({
org: 'lotus-guild',
project: 'javascript-react',
authToken: process.env.SENTRY_AUTH_TOKEN,
sourcemaps: {
filesToDeleteAfterUpload: ['./dist/**/*.map'],
},
release: { name: process.env.VITE_APP_VERSION ?? 'lotus' },
telemetry: false,
}),
]
: []),
2024-09-07 21:45:55 +08:00
VitePWA({
srcDir: 'src',
filename: 'sw.ts',
strategies: 'injectManifest',
injectRegister: false,
manifest: false,
injectManifest: {
injectionPoint: undefined,
// codeSplitting: false is not yet supported by vite-plugin-pwa 1.3.0;
// the inlineDynamicImports deprecation warning from Vite is from pwa internal build
2024-09-07 21:45:55 +08:00
},
2024-09-09 18:45:20 +10:00
devOptions: {
enabled: true,
2026-03-07 18:03:32 +11:00
type: 'module',
},
2024-09-07 21:45:55 +08:00
}),
2022-12-20 20:47:51 +05:30
],
2023-01-30 15:20:53 +11:00
optimizeDeps: {
rolldownOptions: {
define: {
global: 'globalThis',
},
},
2023-01-30 15:20:53 +11:00
},
2022-12-20 20:47:51 +05:30
build: {
target: 'esnext',
2022-12-20 20:47:51 +05:30
outDir: 'dist',
sourcemap: process.env.SENTRY_AUTH_TOKEN ? 'hidden' : false,
2022-12-20 20:47:51 +05:30
copyPublicDir: false,
// manualChunks must be in rolldownOptions (not rollupOptions) for Vite 8 / Rolldown
rolldownOptions: {
checks: { preferBuiltinFeature: false },
output: {
manualChunks: vendorChunks,
},
},
rollupOptions: {
plugins: [inject({ Buffer: ['buffer', 'Buffer'] })],
},
2022-12-20 20:47:51 +05:30
},
});