All checks were successful
Trigger cinny-mobile / dispatch (push) Successful in 2s
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.
363 lines
11 KiB
JavaScript
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'] })],
|
|
},
|
|
},
|
|
});
|