import { dirname, extname, join, relative, sep } from 'node:path'; import { existsSync, readdirSync, writeFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; const MIME_TYPES: Record = { '.css': 'text/css; charset=utf-8', '.gif': 'image/gif', '.html': 'text/html; charset=utf-8', '.ico': 'image/x-icon', '.jpeg': 'image/jpeg', '.jpg': 'image/jpeg', '.js': 'text/javascript; charset=utf-8', '.json': 'application/json; charset=utf-8', '.map': 'application/json; charset=utf-8', '.mjs': 'text/javascript; charset=utf-8', '.otf': 'font/otf', '.png': 'image/png', '.svg': 'image/svg+xml', '.ttf': 'font/ttf', '.txt': 'text/plain; charset=utf-8', '.wasm': 'application/wasm', '.webmanifest': 'application/manifest+json; charset=utf-8', '.webp': 'image/webp', '.woff': 'font/woff', '.woff2': 'font/woff2', '.xml': 'application/xml; charset=utf-8' }; function toPosixPath(filePath: string): string { return filePath.split(sep).join('/'); } function listFiles(rootDir: string, currentDir = rootDir): string[] { const entries = readdirSync(currentDir, { withFileTypes: true }); const files: string[] = []; for (const entry of entries) { const entryPath = join(currentDir, entry.name); if (entry.isDirectory()) { files.push(...listFiles(rootDir, entryPath)); continue; } if (entry.isFile()) { files.push(entryPath); } } return files; } function resolveMimeType(filePath: string): string { const ext = extname(filePath).toLowerCase(); return MIME_TYPES[ext] ?? 'application/octet-stream'; } function main(): void { const scriptDir = dirname(fileURLToPath(import.meta.url)); const workspaceRoot = join(scriptDir, '..', '..'); const webDistDir = join(workspaceRoot, 'web', 'dist'); const outputPath = join(workspaceRoot, 'hub', 'src', 'web', 'embeddedAssets.generated.ts'); const outputDir = dirname(outputPath); if (!existsSync(webDistDir)) { throw new Error(`Missing web/dist directory: ${webDistDir}. Run bun run build:web first.`); } const indexHtmlPath = join(webDistDir, 'index.html'); if (!existsSync(indexHtmlPath)) { throw new Error(`Missing web/dist/index.html. Run bun run build:web first.`); } const files = listFiles(webDistDir).sort((a, b) => a.localeCompare(b)); if (files.length === 0) { throw new Error(`No files found in web/dist: ${webDistDir}.`); } const imports: string[] = []; const manifestLines: string[] = []; files.forEach((filePath, index) => { const relativeToDist = toPosixPath(relative(webDistDir, filePath)); const requestPath = `/${relativeToDist}`; const importPath = toPosixPath(relative(outputDir, filePath)); const importName = `asset${index}`; const mimeType = resolveMimeType(filePath); imports.push(`import ${importName} from '${importPath}' assert { type: 'file' };`); manifestLines.push(` { path: '${requestPath}', sourcePath: ${importName}, mimeType: '${mimeType}' },`); }); const output = [ '// This file is generated by hub/scripts/generate-embedded-web-assets.ts.', '// Do not edit by hand.', '', ...imports, '', 'export interface EmbeddedWebAsset {', ' path: string;', ' sourcePath: string;', ' mimeType: string;', '}', '', 'export const embeddedAssets: EmbeddedWebAsset[] = [', ...manifestLines, '];', '' ].join('\n'); writeFileSync(outputPath, output, 'utf-8'); console.log(`[embedded-assets] Wrote ${files.length} assets to ${outputPath}`); } main();