Files
cinny/vite.config.js
T

328 lines
13 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 { 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: [
+4
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/@lotusguild/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).
// Source is our self-built fork (LotusGuild/element-call) published to
// the Gitea npm registry; see HANDOFF_ELEMENT_CALL_FORK.md.
src: 'node_modules/@lotusguild/element-call-embedded/dist',
dest: 'public/element-call',
rename: { stripBase: 4 },
+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
{
// All of public/res/ (android + apple icons, logos, svg) → dist/public/res/,
// matching the /public/res/... paths used by index.html and manifest.json.
// (The old android-only target stripped `res/` and landed at /public/android/,
// so every icon 404'd — apple/ + favicon weren't copied at all.)
src: 'public/res',
2022-12-20 20:47:51 +05:30
dest: 'public/',
rename: { stripBase: 1 },
},
{
src: 'public/favicon.ico',
dest: 'public/',
rename: { stripBase: 1 },
},
{
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() {
// Never throw from here: closeBundle also runs when the build FAILED
// mid-render (dist/ absent) and an exception here MASKS the real build
// error in vite's report (seen on the Windows CI runner). Warn and skip.
try {
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)) {
console.warn('[copy-pdf-worker] source worker missing, skipped:', src);
return;
}
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.copyFileSync(src, dest);
} catch (err) {
console.warn('[copy-pdf-worker] skipped:', err?.message ?? err);
}
},
};
}
// The denoise assets the fork loads from ./denoise/ at runtime, as
// [source, path under denoise/] pairs; `dir: true` entries are whole trees.
// Shared by the build copy and the dev server (Gitea #293).
function denoiseSources() {
const sapphi = path.resolve('node_modules/@sapphi-red/web-noise-suppressor/dist');
const dfnVendor = path.resolve('build/denoise-vendor/deepfilternet');
return {
// All bundled denoise assets are REQUIRED: every entry backs a model the
// UI can select (RNNoise, Speex) or the optional noise gate.
sapphi: [
[path.join(sapphi, 'rnnoise/workletProcessor.js'), 'rnnoiseWorklet.js'],
[path.join(sapphi, 'rnnoise.wasm'), 'rnnoise.wasm'],
[path.join(sapphi, 'rnnoise_simd.wasm'), 'rnnoise_simd.wasm'],
[path.join(sapphi, 'speex/workletProcessor.js'), 'speexWorklet.js'],
[path.join(sapphi, 'speex.wasm'), 'speex.wasm'],
[path.join(sapphi, 'noiseGate/workletProcessor.js'), 'noiseGateWorklet.js'],
],
// DTLN (@workadventure/noise-suppression): a self-contained ES module that
// resolves its own worklet, LiteRT WASM runtime and TFLite models via
// `import.meta.url`, so its whole dist/ tree is served intact.
dtln: [path.resolve('node_modules/@workadventure/noise-suppression/dist'), 'workadventure'],
// DeepFilterNet 3: the ESM (worklet inlined) plus the vendored wasm and ONNX
// model (CDN-free / Tauri-CSP safe), in the package's expected v2/ layout.
dfn: [
[
path.resolve('node_modules/deepfilternet3-noise-filter/dist/index.esm.js'),
'deepfilternet/index.esm.js',
],
[path.join(dfnVendor, 'v2/pkg/df_bg.wasm'), 'deepfilternet/v2/pkg/df_bg.wasm'],
[
path.join(dfnVendor, 'v2/models/DeepFilterNet3_onnx.tar.gz'),
'deepfilternet/v2/models/DeepFilterNet3_onnx.tar.gz',
],
],
};
}
// Lotus ML noise suppression: ship the denoise worklets/wasm/models alongside
// the vendored Element Call bundle. Runs after viteStaticCopy has populated
// dist/public/element-call (closeBundle order). [Gitea #293] The dev server
// serves the same files at the same URLs, so `npm start` can test denoise.
function lotusDenoise() {
const DEV_PREFIX = '/public/element-call/denoise/';
return {
name: 'lotus-denoise',
configureServer(server) {
const { sapphi, dtln, dfn } = denoiseSources();
const files = new Map([...sapphi, ...dfn].map(([src, rel]) => [rel, src]));
const types = { '.js': 'text/javascript', '.wasm': 'application/wasm' };
server.middlewares.use((req, res, next) => {
const url = (req.url ?? '').split('?')[0];
if (!url.startsWith(DEV_PREFIX)) return next();
const rel = decodeURIComponent(url.slice(DEV_PREFIX.length));
let file = files.get(rel);
if (!file && rel.startsWith(`${dtln[1]}/`)) {
const candidate = path.resolve(dtln[0], rel.slice(dtln[1].length + 1));
if (candidate.startsWith(dtln[0] + path.sep)) file = candidate;
}
if (!file || !fs.existsSync(file)) return next();
res.setHeader('Content-Type', types[path.extname(file)] ?? 'application/octet-stream');
fs.createReadStream(file).pipe(res);
return undefined;
});
},
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, dtln, dfn } = denoiseSources();
// 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 missing = sapphi.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 ')}`,
);
}
sapphi.forEach(([s, rel]) => fs.copyFileSync(s, path.join(denoiseDir, rel)));
const dtlnEntry = path.join(dtln[0], '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(dtln[0], path.join(denoiseDir, dtln[1]), { recursive: true });
const dfnMissing = dfn.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/.)`,
);
}
dfn.forEach(([s, rel]) => {
const d = path.join(denoiseDir, rel);
fs.mkdirSync(path.dirname(d), { recursive: true });
fs.copyFileSync(s, d);
});
// [lotus] DENOISE CUTOVER: the getUserMedia shim is no longer injected.
// Our forked Element Call now runs ML denoise in-source as a real LiveKit
// audio TrackProcessor (activated by lotusDenoiseSource=1 in CallEmbed),
// which survives reconnects (fixes A7). We still copy the denoise/ assets
// above because the in-source processor loads its worklets/wasm from
// ./denoise/ at runtime. To roll back to the shim: restore the
// copy+inject of build/lotus-denoise.js here and swap lotusDenoiseSource
// back to lotusDenoise=ml in CallEmbed.getWidget.
},
};
}
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) {
+4
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',
+4
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(),
2024-09-07 21:45:55 +08:00
VitePWA({
srcDir: 'src',
filename: 'sw.ts',
strategies: 'injectManifest',
injectRegister: false,
manifest: false,
injectManifest: {
// PRECACHE (P5): emit `self.__WB_MANIFEST` into src/sw.ts so it can
// precacheAndRoute the hashed build assets. index.html is deliberately
// EXCLUDED from the manifest (globs only `assets/**`) so navigations
// stay network-first and a new deploy is picked up immediately — see
// the deploy-safety invariants documented in src/sw.ts.
injectionPoint: 'self.__WB_MANIFEST',
globPatterns: ['assets/**/*.{js,css,wasm}'],
// Assets are content-hashed, so the filename is the cache key — don't
// append a revision cache-busting param.
dontCacheBustURLsMatching: /assets\//,
// Raised above the 2 MB default so the ~5.5 MB matrix-sdk crypto wasm
// (hash-busted and hot on every session) is precached deliberately.
maximumFileSizeToCacheInBytes: 6 * 1024 * 1024,
// 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,
+4
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',
2026-06-28 22:21:09 -04:00
sourcemap: 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
},
});