/**
* Bundle script for website server
*
* Creates production-ready bundles using Rolldown and collects WASM files.
* Generates two separate bundles:
* - server.mjs: Full server bundle with all dependencies
* - worker.mjs: Minimal worker bundle for tinypool workers
*
* Usage: node scripts/bundle.mjs
*/
import { execSync } from 'node:child_process';
import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, statSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { rolldown } from 'rolldown';
const __dirname = dirname(fileURLToPath(import.meta.url));
const rootDir = join(__dirname, '..');
const distBundledDir = join(rootDir, 'dist-bundled');
const wasmDir = join(distBundledDir, 'wasm');
/**
* Clean dist-bundled directory
*/
function cleanDistBundled() {
console.log('Cleaning dist-bundled...');
if (existsSync(distBundledDir)) {
rmSync(distBundledDir, { recursive: true });
}
mkdirSync(distBundledDir, { recursive: true });
}
/**
* Build TypeScript to JavaScript
*/
function buildTypeScript() {
console.log('Building TypeScript...');
execSync('npm run build', { cwd: rootDir, stdio: 'inherit' });
}
/**
* Bundle server with Rolldown (full bundle with all server dependencies)
*/
async function bundleAll() {
console.log('Bundling with code splitting...');
const build = await rolldown({
input: {
server: join(rootDir, 'dist/index.js'),
worker: join(rootDir, 'dist/worker-entry.js'),
},
platform: 'node',
external: ['tinypool', 'tiktoken'],
});
await build.write({
dir: distBundledDir,
format: 'esm',
entryFileNames: '[name].mjs',
chunkFileNames: '[name]-[hash].mjs',
// Note: No banner - Rolldown generates necessary shims via rolldown-runtime chunk
minify: true,
legalComments: 'inline',
// Force code splitting with advancedChunks
advancedChunks: {
groups: [
{
name: 'shared',
minSize: 100_000, // 100KB minimum for shared chunks
minShareCount: 2, // Module must be used by at least 2 entry points
},
],
},
});
// Report bundle sizes
const files = readdirSync(distBundledDir).filter((f) => f.endsWith('.mjs'));
for (const file of files) {
const size = getFileSizeMB(join(distBundledDir, file));
console.log(`Bundle created: dist-bundled/${file} (${size} MB)`);
}
}
/**
* Get file size in MB
*/
function getFileSizeMB(filePath) {
const stats = statSync(filePath);
return (stats.size / 1024 / 1024).toFixed(2);
}
/**
* Collect WASM files from node_modules
*/
function collectWasmFiles() {
console.log('Collecting WASM files...');
// Create wasm directory
if (!existsSync(wasmDir)) {
mkdirSync(wasmDir, { recursive: true });
}
// Copy web-tree-sitter.wasm to dist-bundled root
// (web-tree-sitter looks for WASM file in the same directory as the JS file)
const webTreeSitterWasm = join(rootDir, 'node_modules/web-tree-sitter/web-tree-sitter.wasm');
if (existsSync(webTreeSitterWasm)) {
cpSync(webTreeSitterWasm, join(distBundledDir, 'web-tree-sitter.wasm'));
console.log('Copied web-tree-sitter.wasm to dist-bundled/');
} else {
console.warn('Warning: web-tree-sitter.wasm not found');
}
// Find and copy tree-sitter language WASM files
const treeSitterWasmsDir = join(rootDir, 'node_modules/@repomix/tree-sitter-wasms/out');
if (existsSync(treeSitterWasmsDir)) {
const wasmFiles = readdirSync(treeSitterWasmsDir).filter((f) => f.endsWith('.wasm'));
for (const file of wasmFiles) {
cpSync(join(treeSitterWasmsDir, file), join(wasmDir, file));
}
console.log(`Copied ${wasmFiles.length} language WASM files to dist-bundled/wasm/`);
} else {
console.warn('Warning: tree-sitter-wasms not found');
}
}
/**
* Main bundle process
*/
async function main() {
console.log('Starting bundle process...\n');
cleanDistBundled();
buildTypeScript();
await bundleAll();
collectWasmFiles();
console.log('\nBundle complete!');
}
main().catch((err) => {
console.error('Bundle failed:', err);
process.exit(1);
});