// KidLisp Mini Bundler // Assembles minimal interpreter into a self-contained HTML file // ~150 lines import { promises as fs } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { MongoClient } from 'mongodb'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); async function fetchPieceSource(pieceName) { // Try to fetch from MongoDB const mongoUri = process.env.MONGODB_CONNECTION_STRING; const mongoDb = process.env.MONGODB_NAME; if (!mongoUri || !mongoDb) { throw new Error('MongoDB not configured (MONGODB_CONNECTION_STRING, MONGODB_NAME)'); } const client = new MongoClient(mongoUri); try { await client.connect(); const db = client.db(mongoDb); const kidlispCollection = db.collection('kidlisp'); // Look up by code field (handle $code style names) const cleanName = pieceName.replace(/^\$/, ''); const piece = await kidlispCollection.findOne({ code: cleanName }); if (!piece || !piece.source) { throw new Error(`Piece not found: ${pieceName}`); } return piece.source; } finally { await client.close(); } } // Fix KidLisp source format: wrap unparenthesized commands in parentheses function fixKidLispSourceFormat(source) { const lines = source.split('\n'); const fixed = lines.map(line => { line = line.trim(); if (!line) return ''; // Skip lines that already have parens or special prefixes if (line.startsWith('(') || line.startsWith('fade:')) { return line; } // Wrap atoms followed by other expressions in parens // This handles: "ink (? rainbow white 0) (1s... 24 64)" → "(ink (? rainbow white 0) (1s... 24 64))" return '(' + line + ')'; }); return fixed.join('\n'); } async function bundleMini(source, seed = null) { // Fix source format: wrap unparenthesized commands in parentheses source = fixKidLispSourceFormat(source); // Read all module files const evalCode = await fs.readFile(path.join(__dirname, 'eval.mjs'), 'utf-8'); const renderCode = await fs.readFile(path.join(__dirname, 'render.mjs'), 'utf-8'); const colorCode = await fs.readFile(path.join(__dirname, 'color.mjs'), 'utf-8'); const transformsCode = await fs.readFile(path.join(__dirname, 'transforms.mjs'), 'utf-8'); const timingCode = await fs.readFile(path.join(__dirname, 'timing.mjs'), 'utf-8'); // Concatenate all modules (removing imports/exports for bundling) const bundledCode = stripModuleSyntax( colorCode + '\n' + timingCode + '\n' + transformsCode + '\n' + renderCode + '\n' + evalCode ); // Minify const minified = await minifyCode(bundledCode); // Escape source for inline JavaScript const escapedSource = source .replace(/\\/g, '\\\\') .replace(/`/g, '\\`') .replace(/\$/g, '\\$'); // Generate HTML const html = generateHTML(minified, escapedSource, seed); return html; } function stripModuleSyntax(code) { // Remove import statements code = code.replace(/import\s+.*?from\s+['"].*?['"];?/g, ''); // Remove export statements, keep the content code = code.replace(/export\s+\{[^}]*\}/g, ''); code = code.replace(/export\s+function\s+/g, 'function '); code = code.replace(/export\s+class\s+/g, 'class '); code = code.replace(/export\s+const\s+/g, 'const '); return code; } async function minifyCode(code) { // Simple minification: remove comments and excess whitespace // Not as good as SWC but doesn't require external dependencies return code .replace(/\/\*[\s\S]*?\*\//g, '') // Remove /* */ comments .replace(/\/\/.*$/gm, '') // Remove // comments .replace(/\n\s*\n/g, '\n') // Remove blank lines .split('\n') .map(line => line.trim()) .filter(line => line && !line.startsWith('//')) .join('\n'); } function generateHTML(minifiedCode, source, seed) { const seedValue = seed !== null ? seed : Math.floor(Math.random() * 0xFFFFFFFF); return `