Files
trezor-suite/packages/suite-build/vite.config.mts
2026-01-26 13:55:00 +01:00

496 lines
17 KiB
TypeScript

import { viteCommonjs } from '@originjs/vite-plugin-commonjs';
import react from '@vitejs/plugin-react';
import { execSync } from 'child_process';
import fs, { readdirSync } from 'fs';
import { createRequire } from 'module';
import { resolve } from 'path';
import { Plugin, ViteDevServer, build, defineConfig } from 'vite';
import wasm from 'vite-plugin-wasm';
import { suiteVersion } from '../suite/package.json';
import { assetPrefix, isTanstackReactQueryDevTools, project } from './utils/env';
const require = createRequire(import.meta.url);
// Plugin to serve static files with /static prefix
const staticAliasPlugin = (): Plugin => ({
name: 'static-alias',
configureServer(server: ViteDevServer) {
// Middleware to handle /static requests
server.middlewares.use((req, _res, next) => {
if (req.url?.startsWith('/static/')) {
// Rewrite the URL to access the file from the public directory
req.url = req.url.replace('/static/', '/');
}
next();
});
},
});
const trezorLogosRequirePlugin = (): Plugin => ({
name: 'trezor-logos-require',
enforce: 'pre',
transform(code, id) {
const cleanId = id.split('?')[0];
if (
!cleanId.includes(
'packages/product-components/src/components/TrezorLogo/trezorLogos.ts',
)
) {
return null;
}
const transformed = code.replace(
/require\((['"`])([^'"`]+\.svg)\1\)/g,
'new URL($1$2$1, import.meta.url).href',
);
return {
code: transformed,
map: null,
};
},
});
// Function to process the HTML template with template variables
const processTemplate = (template: string): string =>
template
// Hardcoded replace for the only used webpack template variable. If we need more in future, we may develop a more generic code
.replace(/<%=\s*assetPrefix\s*%>/g, assetPrefix)
// Remove the webpack template conditional (opening + closing statements as well as the HTML in between)
.replace(/<%\s*if\([^%]*%>[\s\S]*?<%\s*}\s*%>/g, '')
// Add the script tag for vite-index.ts
.replace('</head>', '<script type="module" src="./vite-index.ts"></script></head>')
// Add the app div to the body, the browser detection does work in vite
.replace('</body>', '<div id="app"></div></body>')
// in case if the id="app" is added multiple times
.replace('<div id="app"></div><div id="app"></div>', '<div id="app"></div>');
// Custom plugin to use the same template as webpack
const htmlTemplatePlugin = (): Plugin => ({
name: 'transform-html',
// This hook runs before Vite processes the HTML
transformIndexHtml: {
order: 'pre',
handler: (html: string) => processTemplate(html),
},
});
// This helper creates aliases for all workspace packages
const createWorkspaceAliases = () => {
const suiteCommonAliases = readdirSync(resolve(__dirname, '../../suite-common'), {
withFileTypes: true,
})
.filter(dirent => dirent.isDirectory())
.map(dirent => ({
find: `@suite-common/${dirent.name}`,
replacement: resolve(__dirname, '../../suite-common', dirent.name),
}));
const trezorPackagesAliases = readdirSync(resolve(__dirname, '../'), { withFileTypes: true })
.filter(dirent => dirent.isDirectory() && dirent.name !== 'suite-web')
.map(dirent => ({
find: `@trezor/${dirent.name}`,
replacement: resolve(__dirname, '../', dirent.name),
}));
return [...suiteCommonAliases, ...trezorPackagesAliases];
};
const alias = [
{
find: 'core-js/actual',
replacement: 'noop-core-js-actual',
},
{
find: 'src',
replacement: resolve(__dirname, '../suite/src'),
},
{
find: 'crypto',
replacement: require.resolve('crypto-browserify'),
},
{
find: 'buffer',
replacement: require.resolve('buffer'),
},
{
find: 'stream',
replacement: require.resolve('stream-browserify'),
},
{
find: 'vm',
replacement: require.resolve('vm-browserify'),
},
...createWorkspaceAliases(),
];
// Plugin to serve sessions-background-sharedworker.js as a complete bundle to be used directly as a web worker
const sessionsSharedWorkerPlugin = () => {
const workerOutDir = resolve(__dirname, '../suite-web/dist/workers');
const workerEntryPath = resolve(
__dirname,
'../transport/src/sessions/background-sharedworker.ts',
);
const workerFileName = 'sessions-background-sharedworker';
const workerOutputPath = resolve(workerOutDir, `${workerFileName}.js`);
let workerPath: string | null = null;
const buildWorker = async () => {
if (workerPath) {
return workerPath;
}
if (!fs.existsSync(workerOutDir)) {
fs.mkdirSync(workerOutDir, { recursive: true });
}
console.log(`Building shared worker from ${workerEntryPath}...`);
try {
await build({
configFile: false,
resolve: {
alias,
},
build: {
outDir: workerOutDir,
emptyOutDir: false,
lib: {
entry: workerEntryPath,
formats: ['iife'],
fileName: () => `${workerFileName}.js`,
name: 'TrezorSharedWorker',
},
rollupOptions: {
output: {
inlineDynamicImports: true,
},
},
minify: false,
target: 'es2020',
write: true,
},
define: {
'process.env.NODE_ENV': JSON.stringify('development'),
},
});
console.log(`SharedWorker built successfully at ${workerOutputPath}`);
workerPath = workerOutputPath;
return workerPath;
} catch (error) {
console.error('Failed to build shared worker:', error);
return null;
}
};
return {
name: 'sessions-shared-worker',
async configureServer(server: ViteDevServer) {
await buildWorker();
server.watcher.add(workerEntryPath);
server.watcher.on('change', async (changedPath: string) => {
if (changedPath === workerEntryPath) {
console.log('Shared worker source changed, rebuilding...');
await buildWorker();
}
});
// Create middleware to serve the built worker file
server.middlewares.use(async (req, res, next) => {
if (req.url && /workers\/sessions-background-sharedworker\.js/.test(req.url)) {
console.log('Serving shared worker from middleware');
const actualPath = await buildWorker();
try {
if (!actualPath) {
throw new Error('Failed to build shared worker!!');
}
if (fs.existsSync(actualPath)) {
const workerCode = fs.readFileSync(actualPath, 'utf-8');
res.setHeader('Content-Type', 'application/javascript');
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
res.end(workerCode);
return;
} else {
throw new Error(`Worker file not found at ${actualPath}`);
}
} catch (error) {
console.error('Error serving shared worker:', error);
res.statusCode = 500;
res.end(`Error serving shared worker: ${error.message}`);
return;
}
}
next();
});
},
};
};
// Plugin to handle workers similar to webpack's worker-loader
const workerPlugin = (): Plugin => ({
name: 'worker-loader',
transform(_code, id) {
if (/\/workers\/[^/]+\/index\.ts$/.test(id) || /pinger\/pingWorker.ts$/.test(id)) {
// Return a virtual module that creates a web worker
console.log('[VITE] Transforming worker:', id);
return {
code: `
const worker = () => {
console.log('[VITE] Creating worker from:', '${id}');
return new Worker(new URL('${id}', import.meta.url), { type: 'module' });
};
export default worker;
`,
// Use an empty source map to preserve the original file's mapping
map: { mappings: '' },
};
}
return null;
},
});
const commitId = execSync('git rev-parse HEAD').toString().trim();
// Plugin to provide a no-op replacement for core-js/actual as a virtual module
const noopCoreJsPlugin = (): Plugin => {
const virtualModuleId = 'noop-core-js-actual';
const resolvedVirtualModuleId = '\0' + virtualModuleId;
return {
name: 'noop-core-js-actual',
resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId;
}
},
load(id) {
if (id === resolvedVirtualModuleId) {
return '// No-op replacement for core-js/actual\nexport default {};';
}
},
};
};
// Plugin to provide Buffer polyfill via a virtual module
const bufferPolyfillPlugin = (): Plugin => {
const virtualModuleId = 'virtual:buffer-polyfill';
const resolvedVirtualModuleId = '\0' + virtualModuleId;
const polyfillCode = `
// Ensure Buffer is available globally
import { Buffer as ImportedBuffer } from 'buffer';
const base64UrlToBase64 = (input) => {
const base64 = input.replace(/-/g, '+').replace(/_/g, '/');
const pad = base64.length % 4;
if (pad === 0) return base64;
return base64 + '='.repeat(4 - pad);
};
const base64ToBase64Url = (input) => input.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/g, '');
const patchBase64Url = (BufferCtor) => {
if (!BufferCtor?.prototype?.toString) return;
const originalToString = BufferCtor.prototype.toString;
if (!BufferCtor.prototype.__trezorPatchedBase64UrlToString) {
Object.defineProperty(BufferCtor.prototype, '__trezorPatchedBase64UrlToString', {
value: true,
enumerable: false,
});
BufferCtor.prototype.toString = function (encoding, start, end) {
if (encoding === 'base64url') {
return base64ToBase64Url(originalToString.call(this, 'base64', start, end));
}
return originalToString.call(this, encoding, start, end);
};
}
const originalFrom = BufferCtor.from;
if (!BufferCtor.__trezorPatchedBase64UrlFrom) {
Object.defineProperty(BufferCtor, '__trezorPatchedBase64UrlFrom', {
value: true,
enumerable: false,
});
BufferCtor.from = function (value, encodingOrOffset, length) {
if (encodingOrOffset === 'base64url' && typeof value === 'string') {
return originalFrom.call(this, base64UrlToBase64(value), 'base64');
}
return originalFrom.call(this, value, encodingOrOffset, length);
};
}
};
// Define Buffer in all possible global scopes
if (typeof window !== 'undefined') {
window.Buffer = window.Buffer || ImportedBuffer;
};
if (typeof global !== 'undefined') {
global.Buffer = global.Buffer || ImportedBuffer;
};
if (typeof globalThis !== 'undefined') {
globalThis.Buffer = globalThis.Buffer || ImportedBuffer;
};
patchBase64Url(ImportedBuffer);
if (typeof window !== 'undefined' && window.Buffer) patchBase64Url(window.Buffer);
if (typeof global !== 'undefined' && global.Buffer) patchBase64Url(global.Buffer);
if (typeof globalThis !== 'undefined' && globalThis.Buffer) patchBase64Url(globalThis.Buffer);
// Make sure global is defined
if (typeof window !== 'undefined' && typeof global === 'undefined') {
window.global = window;
};
// Make sure globalThis is defined
if (typeof window !== 'undefined' && typeof globalThis === 'undefined') {
window.globalThis = window;
};
// Polyfill process.nextTick for jws.createVerify
if (
typeof window !== 'undefined' &&
typeof window.process !== 'undefined' &&
typeof window.process.nextTick === 'undefined'
) {
window.process.nextTick = cb => Promise.resolve().then(cb);
}
// Export nothing - this module is only for side effects
export {};
`;
return {
name: 'buffer-polyfill',
resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId;
}
},
load(id) {
if (id === resolvedVirtualModuleId) {
return polyfillCode;
}
},
// Add import to the virtual module at the entry points
transform(code, id) {
// Only process entry points or index files
if (id.includes('vite-index.ts') || id.includes('index.ts') || id.includes('main.ts')) {
// Add import at the top of the file
return {
code: `import '${virtualModuleId}';
${code}`,
map: { mappings: '' }, // Let Vite handle the source map
};
}
},
};
};
// TODO: after migration to Vite, remove this completely and FIX THE GUIDE LOADING
// Temporary and hacky plugin to handle the markdown guide apperance in vite dev env
// It finds and transforms the code to use fetch instead of imports so
// when we run vite dev the .md files are properly loaded
const guideMarkdownPlugin = (): Plugin => ({
name: 'guide-md-dev',
apply: 'serve',
enforce: 'pre',
transform(code, id) {
if (!id.endsWith('useGuideLoadArticle.ts')) return null;
// This transform the hook logic to use fetch so that
// with Vite build md guides are displayed properly
const transformed = code.replace(
/const\s+file\s*=\s*await\s*import\([^)]*`@trezor\/suite-data\/files\/guide\/\$\{language\.toLowerCase\(\)\}\$\{id\}`[^)]*\);\s*const\s+md\s*=\s*(?:await\s*)?file\.default;?\s*return\s+md;?/s,
`
const response = await fetch(\`/guide/\${language.toLowerCase()}\${id}\`);
if (!response.ok) throw new Error('Failed to load markdown');
return await response.text();
`.trim(),
);
return {
code: transformed,
map: null,
};
},
});
export default defineConfig({
root: '../suite-web/src/static',
cacheDir: resolve(__dirname, '../../node_modules/.vite'),
base: assetPrefix,
// Use suite-data/files as the public directory
publicDir: resolve(__dirname, '../suite-data/files'),
plugins: [
htmlTemplatePlugin(),
bufferPolyfillPlugin(),
noopCoreJsPlugin(),
guideMarkdownPlugin(),
trezorLogosRequirePlugin(),
staticAliasPlugin(),
sessionsSharedWorkerPlugin(),
viteCommonjs(),
workerPlugin(),
wasm(),
react({
babel: {
plugins: [
[
'babel-plugin-styled-components',
{
displayName: true,
fileName: false,
},
],
],
},
}),
],
resolve: {
alias,
preserveSymlinks: true,
},
define: {
'process.browser': true,
'process.env.VERSION': JSON.stringify(suiteVersion),
'process.env.COMMIT_HASH': JSON.stringify(commitId),
'process.env.COMMITHASH': JSON.stringify(commitId),
'process.env.SUITE_TYPE': JSON.stringify(project ?? 'web'),
'process.env.NODE_ENV': JSON.stringify('development'),
'process.env.ASSET_PREFIX': JSON.stringify(assetPrefix),
'process.env.TANSTACK_REACT_QUERY_DEV_TOOLS': JSON.stringify(isTanstackReactQueryDevTools),
global: 'globalThis',
__DEV__: true,
ENABLE_REDUX_LOGGER: true,
},
optimizeDeps: {
include: ['@trezor/connect', '@trezor/suite', 'buffer'],
exclude: [
// Exclude WebAssembly modules
'@trezor/crypto-utils',
'@trezor/utxo-lib',
// Exclude transport to prevent pre-bundling issues with bridge URL construction
'@trezor/transport',
],
},
server: {
port: 8000,
open: false,
host: true,
},
});