#!/usr/bin/env node // Single-file PACK bundler - creates one self-contained HTML file // Excludes image assets and generates directly from source // For KidLisp pieces, fetches and embeds source code at build time // Usage: node ac-pack-single-file.mjs import { promises as fs } from "fs"; import path from "path"; import { fileURLToPath } from "url"; import { execSync } from "child_process"; import https from "https"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const PIECE_NAME = process.argv[2] || "$eel"; const OUTPUT_DIR = path.join(__dirname, "output"); const SOURCE_DIR = path.resolve(__dirname, ".."); // Get git info for timestamp const gitHash = execSync("git rev-parse --short HEAD", { encoding: "utf8" }).trim(); const gitDirty = execSync("git diff --quiet || echo dirty", { encoding: "utf8" }).trim(); const gitVersion = gitHash + (gitDirty ? " (dirty)" : ""); const packDate = new Date().toLocaleString("en-US", { month: "long", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit", second: "2-digit" }); async function fetchKidLispSource(piece) { console.log(`šŸ“” Fetching KidLisp source for: ${piece}...`); const url = `https://aesthetic.computer/.netlify/functions/store-kidlisp?code=${piece}`; return new Promise((resolve, reject) => { https.get(url, (res) => { let data = ''; res.on('data', (chunk) => data += chunk); res.on('end', () => { try { const json = JSON.parse(data); if (json.source) { console.log(`āœ… KidLisp source: ${json.source.substring(0, 80)}...`); resolve(json.source); } else { console.log(`āš ļø No KidLisp source found for ${piece}`); resolve(null); } } catch (e) { console.log(`āš ļø Not a KidLisp piece: ${piece}`); resolve(null); } }); }).on('error', (e) => { console.log(`āš ļø Could not fetch KidLisp source: ${e.message}`); resolve(null); }); }); } // Recursively fetch all embedded KidLisp pieces async function fetchAllEmbeddedPieces(mainPiece) { const pieces = new Map(); // Map of piece name -> source const toFetch = [mainPiece]; const fetched = new Set(); console.log(`\nšŸ” Scanning for embedded KidLisp pieces...`); while (toFetch.length > 0) { const piece = toFetch.shift(); if (fetched.has(piece)) continue; fetched.add(piece); const source = await fetchKidLispSource(piece); if (source) { pieces.set(piece, source); // Scan for embedded $codes in the source const embeddedMatches = source.matchAll(/\$([a-z0-9]+)/gi); for (const match of embeddedMatches) { const embeddedPiece = match[1]; if (!fetched.has(embeddedPiece) && !toFetch.includes(embeddedPiece)) { console.log(` šŸ“Ž Found embedded piece: $${embeddedPiece}`); toFetch.push(embeddedPiece); } } } } console.log(`āœ… Fetched ${pieces.size} total piece(s): ${Array.from(pieces.keys()).map(p => '$' + p).join(', ')}`); return pieces; } async function inlineFile(filePath, type = "text") { try { if (type === "binary") { const buffer = await fs.readFile(filePath); return buffer.toString("base64"); } return await fs.readFile(filePath, "utf8"); } catch (error) { console.warn(`āš ļø Could not read ${filePath}: ${error.message}`); return ""; } } async function inlineDirectory(dirPath, basePath = dirPath, excludeImages = false) { const files = {}; const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); const relativePath = path.relative(basePath, fullPath); if (entry.isDirectory()) { const subFiles = await inlineDirectory(fullPath, basePath, excludeImages); Object.assign(files, subFiles); } else { // Determine if binary const ext = path.extname(entry.name).toLowerCase(); const isBinary = [".png", ".gif", ".jpg", ".jpeg", ".ico", ".woff", ".woff2", ".ttf", ".otf"].includes(ext); const isImage = [".png", ".gif", ".jpg", ".jpeg", ".ico"].includes(ext); // Skip images if excludeImages is true if (excludeImages && isImage) { continue; } files[relativePath] = { content: await inlineFile(fullPath, isBinary ? "binary" : "text"), binary: isBinary, type: ext.slice(1) || "txt" }; } } return files; } async function createSingleFile(kidlispSource, allPieces = new Map()) { console.log(`\nšŸ“¦ Creating single-file HTML for ${PIECE_NAME}...`); console.log(`šŸ“… Pack date: ${packDate}`); console.log(`šŸ”§ Git version: ${gitVersion}`); // Inline essential files from source ac/ directory (excluding images and heavy deps) console.log("\nšŸ“ Inlining ac/ directory..."); const acDir = path.join(SOURCE_DIR, "system/public/aesthetic.computer"); const essentialDirs = ["boot.mjs", "bios.mjs", "lib", "disks", "systems", "style.css", "cursors"]; const acFiles = {}; for (const item of essentialDirs) { const itemPath = path.join(acDir, item); const stats = await fs.stat(itemPath).catch(() => null); if (!stats) continue; if (stats.isDirectory()) { const dirFiles = await inlineDirectory(itemPath, acDir, false); // Include cursors (SVG files) Object.assign(acFiles, dirFiles); } else { // Single file const relativePath = path.relative(acDir, itemPath); const ext = path.extname(item).toLowerCase(); const isBinary = [".png", ".gif", ".jpg", ".jpeg", ".ico", ".woff", ".woff2", ".ttf", ".otf"].includes(ext); acFiles[relativePath] = { content: await inlineFile(itemPath, isBinary ? "binary" : "text"), binary: isBinary, type: ext.slice(1) || "txt" }; } } // Add only essential dependencies from dep/ console.log("šŸ“ Inlining dep/gl-matrix..."); const glMatrixDir = path.join(acDir, "dep/gl-matrix"); const glMatrixFiles = await inlineDirectory(glMatrixDir, acDir, true); Object.assign(acFiles, glMatrixFiles); console.log("šŸ“ Inlining dep/@akamfoad/qr..."); const qrDir = path.join(acDir, "dep/@akamfoad/qr"); const qrFiles = await inlineDirectory(qrDir, acDir, true); Object.assign(acFiles, qrFiles); console.log("šŸ“ Inlining dep/idb.js..."); const idbPath = path.join(acDir, "dep/idb.js"); const idbContent = await fs.readFile(idbPath, "utf8"); acFiles["dep/idb.js"] = { content: idbContent, binary: false, type: "js" }; console.log("šŸ“ Inlining dep/nanoid..."); const nanoidDir = path.join(acDir, "dep/nanoid"); const nanoidFiles = await inlineDirectory(nanoidDir, acDir, true); Object.assign(acFiles, nanoidFiles); console.log("šŸ“ Inlining dep/geckos.io-client..."); const geckosPath = path.join(acDir, "dep/geckos.io-client.2.3.2.min.js"); const geckosContent = await fs.readFile(geckosPath, "utf8"); acFiles["dep/geckos.io-client.2.3.2.min.js"] = { content: geckosContent, binary: false, type: "js" }; console.log(` āœ“ Found ${Object.keys(acFiles).length} files`); // Inline type/ directory (fonts) console.log("šŸ“ Inlining type/ directory..."); const typeDir = path.join(SOURCE_DIR, "system/public/type"); const typeFiles = await inlineDirectory(typeDir, typeDir, false); console.log(` āœ“ Found ${Object.keys(typeFiles).length} files`); // Create virtual filesystem (no images!) const vfs = { ...Object.fromEntries(Object.entries(acFiles).map(([k, v]) => [`ac/${k}`, v])), ...Object.fromEntries(Object.entries(typeFiles).map(([k, v]) => [`type/${k}`, v])) }; console.log(`\nšŸ“Š Total files in VFS: ${Object.keys(vfs).length}`); // Prepare KidLisp embedding const kidlispSourceEscaped = kidlispSource ? kidlispSource.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n') : ''; if (kidlispSource) { console.log(`šŸ“ Embedded KidLisp source: ${kidlispSource.length} chars`); } // Create the single file HTML // CRITICAL: Escape in JSON to prevent premature script tag closure const vfsJson = JSON.stringify(vfs).replace(/<\/script>/gi, '<\\/script>'); // Build list of all module paths for runtime import map generation const modulePaths = Object.keys(vfs).filter(path => path.endsWith('.js') || path.endsWith('.mjs') ); console.log(`šŸ“¦ Found ${modulePaths.length} JavaScript modules for import map`); const modulePathsJson = JSON.stringify(modulePaths); const singleFileHtml = ` ${PIECE_NAME} - Aesthetic Computer
`; // Write single file const outputPath = path.join(OUTPUT_DIR, `${PIECE_NAME.replace(/\$/g, '')}-single-file.html`); await fs.writeFile(outputPath, singleFileHtml); const stats = await fs.stat(outputPath); const sizeMB = (stats.size / 1024 / 1024).toFixed(2); console.log(`\nāœ… Single file created!`); console.log(` šŸ“„ ${outputPath}`); console.log(` šŸ’¾ Size: ${sizeMB} MB`); console.log(` šŸ–¼ļø No images included (code only)`); console.log(`\nšŸš€ This file is completely self-contained!`); console.log(` • No external dependencies`); console.log(` • No IPFS/CDN required`); console.log(` • Works on objkt.com`); console.log(` • Can be minted directly`); if (kidlispSource) { console.log(` • KidLisp source embedded (${kidlispSource.length} chars)`); } console.log(` • Current timestamp: ${packDate}`); } // Main - fetch all KidLisp sources (including embedded pieces), then create bundle (async () => { try { const allPieces = await fetchAllEmbeddedPieces(PIECE_NAME.replace(/^\$/, '')); const mainSource = allPieces.get(PIECE_NAME.replace(/^\$/, '')); await createSingleFile(mainSource, allPieces); } catch (error) { console.error("āŒ Error:", error); process.exit(1); } })();