Files
cinny/vite.config.js
litruv 7d866404b3
All checks were successful
Trigger cinny-mobile / dispatch (push) Successful in 2s
Fix Android updates dialog, mobile About page, and user color crash guard.
Use stripBase for update static copy paths, show Settings detail on mobile
when nav is hidden, pause Settings focus trap during release notes, and guard
extractMemberColorPreference when room is not a Matrix Room instance.
2026-08-23 16:18:35 +10:00

363 lines
11 KiB
JavaScript

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { wasm } from '@rollup/plugin-wasm';
import { viteStaticCopy } from 'vite-plugin-static-copy';
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill';
import inject from '@rollup/plugin-inject';
import topLevelAwait from 'vite-plugin-top-level-await';
import { VitePWA } from 'vite-plugin-pwa';
import fs from 'fs';
import path from 'path';
import buildConfig from './build.config';
import { liveTsxPlugin, readDefaultLiveSource } from './playground-liveTsxPlugin';
const projectRoot = path.resolve();
const copyFiles = {
targets: [
{
src: 'node_modules/pdfjs-dist/build/pdf.worker.min.mjs',
dest: '',
rename: 'pdf.worker.min.js',
},
{
src: 'node_modules/gif.js/dist/gif.worker.js',
dest: '',
},
{
src: 'netlify.toml',
dest: '',
},
{
src: 'config.json',
dest: '',
},
{
src: 'public/manifest.json',
dest: '',
},
{
src: 'public/res/android',
dest: 'public/',
},
{
src: 'public/locales',
dest: 'public/',
},
{
src: 'public/sound',
dest: 'public/',
},
{
src: 'public/font',
dest: 'public/',
},
{
src: 'public/update/**/*',
dest: 'update',
// stripBase removes public/update from the matched path; rename alone only changes the filename.
rename: { stripBase: 2 },
},
],
};
function serverMatrixSdkCryptoWasm(wasmFilePath) {
return {
name: 'vite-plugin-serve-matrix-sdk-crypto-wasm',
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (req.url === wasmFilePath) {
const resolvedPath = path.join(path.resolve(), "/node_modules/@matrix-org/matrix-sdk-crypto-wasm/pkg/matrix_sdk_crypto_wasm_bg.wasm");
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();
}
});
},
};
}
function serveGifWorker() {
return {
name: 'vite-plugin-serve-gif-worker',
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (req.url === '/gif.worker.js') {
const resolvedPath = path.join(path.resolve(), '/node_modules/gif.js/dist/gif.worker.js');
if (fs.existsSync(resolvedPath)) {
res.setHeader('Content-Type', 'application/javascript');
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();
}
});
},
};
}
function servePublicAssets() {
return {
name: 'vite-plugin-serve-public-assets',
configureServer(server) {
server.middlewares.use((req, res, next) => {
// Serve sound files
if (req.url?.startsWith('/sound/')) {
const fileName = req.url.replace('/sound/', '');
const resolvedPath = path.join(path.resolve(), 'public', 'sound', fileName);
if (fs.existsSync(resolvedPath)) {
const ext = path.extname(fileName);
const contentType = ext === '.ogg' ? 'audio/ogg' : 'application/octet-stream';
res.setHeader('Content-Type', contentType);
res.setHeader('Cache-Control', 'no-cache');
const fileStream = fs.createReadStream(resolvedPath);
fileStream.pipe(res);
} else {
res.writeHead(404);
res.end('File not found');
}
} else if (req.url?.startsWith('/update/')) {
const fileName = req.url.replace('/update/', '');
const resolvedPath = path.join(path.resolve(), 'public', 'update', fileName);
if (fs.existsSync(resolvedPath) && !resolvedPath.includes('..')) {
const ext = path.extname(fileName).toLowerCase();
const contentType =
ext === '.md'
? 'text/markdown; charset=utf-8'
: ext === '.json'
? 'application/json'
: ext === '.svg'
? 'image/svg+xml'
: ext === '.png'
? 'image/png'
: ext === '.jpg' || ext === '.jpeg'
? 'image/jpeg'
: ext === '.webp'
? 'image/webp'
: ext === '.gif'
? 'image/gif'
: 'application/octet-stream';
res.setHeader('Content-Type', contentType);
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();
}
});
},
};
}
// CORS proxy for Matrix homeservers during development
function corsProxyMiddleware() {
return {
name: 'vite-plugin-cors-proxy',
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
// Proxy /.well-known requests: /_matrix-cors-proxy/homeserver.com/.well-known/matrix/client
const corsProxyMatch = req.url?.match(/^\/_matrix-cors-proxy\/([^/]+)(\/.*)/);
if (corsProxyMatch) {
const [, hostname, pathname] = corsProxyMatch;
const targetUrl = `https://${hostname}${pathname}`;
console.log(`[CORS Proxy] ${req.method} ${req.url}`);
console.log(`[CORS Proxy] Target: ${targetUrl}`);
// Handle preflight OPTIONS request
if (req.method === 'OPTIONS') {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.writeHead(204);
res.end();
return;
}
try {
// Collect request body for POST/PUT
let body = null;
if (req.method === 'POST' || req.method === 'PUT') {
const chunks = [];
for await (const chunk of req) {
chunks.push(chunk);
}
body = Buffer.concat(chunks).toString();
console.log(`[CORS Proxy] Request body:`, body);
}
// Forward request with method, headers, and body
const fetchOptions = {
method: req.method,
headers: {
'Content-Type': req.headers['content-type'] || 'application/json',
'Authorization': req.headers['authorization'] || '',
},
};
if (body) {
fetchOptions.body = body;
}
const response = await fetch(targetUrl, fetchOptions);
const data = await response.text();
console.log(`[CORS Proxy] Response status: ${response.status}`);
console.log(`[CORS Proxy] Response data:`, data.substring(0, 500));
// Set CORS headers
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Content-Type', response.headers.get('content-type') || 'application/json');
res.writeHead(response.status);
res.end(data);
} catch (error) {
console.error(`[CORS Proxy] Error for ${targetUrl}:`, error);
res.writeHead(500);
res.end(JSON.stringify({ error: error.message }));
}
} else {
next();
}
});
},
};
}
export default defineConfig({
appType: 'spa',
publicDir: false,
base: buildConfig.base,
assetsInclude: ['**/*.ogg', '**/*.mp3', '**/*.wav'],
resolve: {
// Keep a single React instance — dual copies cause resolveDispatcher() === null.
dedupe: ['react', 'react-dom'],
alias: [
{ find: '@cinny', replacement: path.resolve(projectRoot, 'src') },
{
find: '@playground/mocks',
replacement: path.resolve(projectRoot, 'src/playground/mocks/index.ts'),
},
// Exact match only — do not rewrite loglevel/lib/...
{
find: /^loglevel$/,
replacement: path.resolve(projectRoot, 'src/playground/shims/loglevel.js'),
},
{
find: /^react$/,
replacement: path.resolve(projectRoot, 'node_modules/react'),
},
{
find: /^react-dom$/,
replacement: path.resolve(projectRoot, 'node_modules/react-dom'),
},
],
},
server: {
port: 38347,
host: '0.0.0.0',
cors: true, // Enable CORS for dev server
open: false,
fs: {
// Allow serving files from one level up to the project root
allow: ['..'],
},
},
plugins: [
corsProxyMiddleware(), // Add CORS proxy for development
serverMatrixSdkCryptoWasm('/node_modules/.vite/deps/pkg/matrix_sdk_crypto_wasm_bg.wasm'),
serveGifWorker(),
servePublicAssets(),
liveTsxPlugin(projectRoot, readDefaultLiveSource(projectRoot)),
topLevelAwait({
// The export name of top-level await promise for each chunk module
promiseExportName: '__tla',
// The function to generate import names of top-level await promise in each chunk module
promiseImportName: (i) => `__tla_${i}`,
}),
viteStaticCopy(copyFiles),
vanillaExtractPlugin(),
wasm(),
react(),
VitePWA({
srcDir: 'src',
filename: 'sw.ts',
strategies: 'injectManifest',
injectRegister: false,
manifest: false,
injectManifest: {
injectionPoint: undefined,
},
devOptions: {
enabled: false,
type: 'module'
}
}),
],
optimizeDeps: {
include: [
'react',
'react-dom',
'react/jsx-runtime',
'react/jsx-dev-runtime',
'loglevel',
'matrix-js-sdk',
'@monaco-editor/react',
],
needsInterop: ['loglevel'],
rolldownOptions: {
define: {
global: 'globalThis',
},
plugins: [
// Enable polyfill plugins
NodeGlobalsPolyfillPlugin({
process: false,
buffer: true,
}),
],
},
},
build: {
target: 'es2022',
outDir: 'dist',
sourcemap: true,
copyPublicDir: false,
rollupOptions: {
input: {
main: path.resolve(projectRoot, 'index.html'),
playground: path.resolve(projectRoot, 'playground.html'),
},
plugins: [inject({ Buffer: ['buffer', 'Buffer'] })],
},
},
});