#!/usr/bin/env -S deno run --allow-all /** * UI Bundler Script * Encodes all files from ui/dist/registry-ui/browser/ as base64 * and generates ts/embedded-ui.generated.ts * * Usage: * deno task bundle-ui # One-time bundle * deno task bundle-ui:watch # Watch mode for development */ import { walk } from 'jsr:@std/fs@1/walk'; import { extname, relative } from 'jsr:@std/path@1'; import { encodeBase64 } from 'jsr:@std/encoding@1/base64'; const UI_DIST_PATH = './ui/dist/registry-ui/browser'; const OUTPUT_PATH = './ts/embedded-ui.generated.ts'; const CONTENT_TYPES: Record = { '.html': 'text/html', '.js': '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; } async function bundleUI(): Promise { console.log('[bundle-ui] Starting UI bundling...'); console.log(`[bundle-ui] Source: ${UI_DIST_PATH}`); console.log(`[bundle-ui] Output: ${OUTPUT_PATH}`); // Check if UI dist exists try { await Deno.stat(UI_DIST_PATH); } catch { console.error(`[bundle-ui] ERROR: UI dist not found at ${UI_DIST_PATH}`); console.error('[bundle-ui] Run "deno task build" first to build the UI'); Deno.exit(1); } const files: IEmbeddedFile[] = []; let totalSize = 0; // Walk through all files in the dist directory for await (const entry of walk(UI_DIST_PATH, { includeFiles: true, includeDirs: false })) { const relativePath = '/' + relative(UI_DIST_PATH, entry.path).replace(/\\/g, '/'); const ext = extname(entry.path).toLowerCase(); const contentType = CONTENT_TYPES[ext] || 'application/octet-stream'; // Read file and encode as base64 const content = await Deno.readFile(entry.path); const base64 = encodeBase64(content); files.push({ path: relativePath, base64, contentType, size: content.length, }); totalSize += content.length; console.log(`[bundle-ui] Encoded: ${relativePath} (${formatSize(content.length)})`); } // 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 await Deno.writeTextFile(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: Uint8Array; contentType: string } | null { const file = EMBEDDED_FILES.get(path); if (!file) return null; // Decode base64 to Uint8Array const binaryString = atob(file.base64); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } return { data: bytes, 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`; } async function watchMode(): Promise { console.log('[bundle-ui] Starting watch mode...'); console.log(`[bundle-ui] Watching: ${UI_DIST_PATH}`); console.log('[bundle-ui] Press Ctrl+C to stop'); console.log(''); // Initial bundle await bundleUI(); // Watch for changes const watcher = Deno.watchFs(UI_DIST_PATH); let debounceTimer: number | null = null; for await (const event of watcher) { if (event.kind === 'modify' || event.kind === 'create' || event.kind === 'remove') { // Debounce - wait 500ms after last change if (debounceTimer) { clearTimeout(debounceTimer); } debounceTimer = setTimeout(async () => { console.log(''); console.log(`[bundle-ui] Change detected: ${event.kind}`); try { await bundleUI(); } catch (error) { console.error('[bundle-ui] Error during rebundle:', error); } }, 500); } } } // Main entry point const args = Deno.args; const isWatch = args.includes('--watch') || args.includes('-w'); if (isWatch) { await watchMode(); } else { await bundleUI(); }