/** * UI Bundler Script for opencdn * Encodes all files from dist_serve/ as base64 * and generates ts/embedded-ui.generated.ts * * Usage: * pnpm bundleUI # One-time bundle */ import * as fs from 'fs'; import * as path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const PROJECT_ROOT = path.join(__dirname, '..'); const UI_DIST_PATH = path.join(PROJECT_ROOT, 'dist_bundle'); const HTML_PATH = path.join(PROJECT_ROOT, 'html'); const OUTPUT_PATH = path.join(PROJECT_ROOT, 'ts', 'embedded-ui.generated.ts'); const CONTENT_TYPES: Record = { '.html': 'text/html', '.js': 'application/javascript', '.mjs': 'application/javascript', '.css': 'text/css', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.eot': 'application/vnd.ms-fontobject', '.otf': 'font/otf', '.map': 'application/json', '.txt': 'text/plain', '.xml': 'application/xml', '.webp': 'image/webp', '.webmanifest': 'application/manifest+json', }; interface IEmbeddedFile { path: string; base64: string; contentType: string; size: number; } function walkDir(dir: string, baseDir: string, files: IEmbeddedFile[] = []): IEmbeddedFile[] { if (!fs.existsSync(dir)) { return files; } const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { walkDir(fullPath, baseDir, files); } else if (entry.isFile()) { const relativePath = '/' + path.relative(baseDir, fullPath).replace(/\\/g, '/'); const ext = path.extname(entry.name).toLowerCase(); const contentType = CONTENT_TYPES[ext] || 'application/octet-stream'; const content = fs.readFileSync(fullPath); const base64 = content.toString('base64'); files.push({ path: relativePath, base64, contentType, size: content.length, }); console.log(`[bundle-ui] Encoded: ${relativePath} (${formatSize(content.length)})`); } } return files; } function bundleUI(): void { console.log('[bundle-ui] Starting UI bundling...'); console.log(`[bundle-ui] Source (dist): ${UI_DIST_PATH}`); console.log(`[bundle-ui] Source (html): ${HTML_PATH}`); console.log(`[bundle-ui] Output: ${OUTPUT_PATH}`); const files: IEmbeddedFile[] = []; let totalSize = 0; // Walk through dist_serve directory (bundled JS) if (fs.existsSync(UI_DIST_PATH)) { walkDir(UI_DIST_PATH, UI_DIST_PATH, files); } else { console.log('[bundle-ui] WARNING: dist_serve not found, skipping...'); } // Walk through html directory (static HTML) if (fs.existsSync(HTML_PATH)) { walkDir(HTML_PATH, HTML_PATH, files); } else { console.log('[bundle-ui] WARNING: html directory not found, skipping...'); } if (files.length === 0) { console.error('[bundle-ui] ERROR: No files found to bundle!'); console.error('[bundle-ui] Run "pnpm build" first to build the UI'); process.exit(1); } // Calculate total size for (const file of files) { totalSize += file.size; } // Sort files for consistent output files.sort((a, b) => a.path.localeCompare(b.path)); // Generate TypeScript module const tsContent = generateTypeScript(files, totalSize); // Write output file fs.writeFileSync(OUTPUT_PATH, tsContent); console.log(`[bundle-ui] Generated ${OUTPUT_PATH}`); console.log(`[bundle-ui] Total files: ${files.length}`); console.log(`[bundle-ui] Total size: ${formatSize(totalSize)}`); console.log(`[bundle-ui] Bundling complete!`); } function generateTypeScript(files: IEmbeddedFile[], totalSize: number): string { const fileEntries = files .map( (f) => ` ['${f.path}', { base64: '${f.base64}', contentType: '${f.contentType}' }]` ) .join(',\n'); return `// AUTO-GENERATED FILE - DO NOT EDIT // Generated by scripts/bundle-ui.ts // Total files: ${files.length} // Total size: ${formatSize(totalSize)} // Generated at: ${new Date().toISOString()} interface IEmbeddedFile { base64: string; contentType: string; } const EMBEDDED_FILES: Map = new Map([ ${fileEntries} ]); /** * Get an embedded file by path * @param path - The file path (e.g., '/index.html') * @returns The file data and content type, or null if not found */ export function getEmbeddedFile(path: string): { data: Buffer; contentType: string } | null { const file = EMBEDDED_FILES.get(path); if (!file) return null; // Decode base64 to Buffer const data = Buffer.from(file.base64, 'base64'); return { data, contentType: file.contentType }; } /** * Check if an embedded file exists * @param path - The file path to check */ export function hasEmbeddedFile(path: string): boolean { return EMBEDDED_FILES.has(path); } /** * List all embedded file paths */ export function listEmbeddedFiles(): string[] { return Array.from(EMBEDDED_FILES.keys()); } /** * Get the total number of embedded files */ export function getEmbeddedFileCount(): number { return EMBEDDED_FILES.size; } `; } function formatSize(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; } // Run bundler bundleUI();