#!/usr/bin/env node /** * Simple ES-module → IIFE bundler for GrechkaCraft * Resolves import/export, wraps in single IIFE, no external deps. * Usage: node bundle.js > ../game-bundled.js */ const fs = require('fs'); const path = require('path'); const SRC = path.join(__dirname, 'src'); const ENTRY = 'main.js'; const visited = new Set(); const chunks = []; // Named export → variable name mapping per file const fileExports = {}; // file -> [{local, exported}] const fileImports = {}; // file -> [{names, from}] function resolveImportPath(fromPath, importerDir) { let resolved = path.resolve(importerDir, fromPath); if (!fs.existsSync(resolved) && fs.existsSync(resolved + '.js')) { resolved += '.js'; } return resolved; } function collectExports(content) { const exports = []; // export { a, b as c } const re1 = /export\s*\{([^}]+)\}/g; let m; while ((m = re1.exec(content)) !== null) { const names = m[1].split(',').map(s => { const parts = s.trim().split(/\s+as\s+/); return { local: parts[0].trim(), exported: (parts[1] || parts[0]).trim() }; }); exports.push(...names); } // export const/let/var/function/class name const re2 = /export\s+(?:const|let|var|function|class)\s+(\w+)/g; while ((m = re2.exec(content)) !== null) { exports.push({ local: m[1], exported: m[1] }); } // export default ... if (/export\s+default\s+/.test(content)) { // Find the expression after export default // Simpler: just use __default as name exports.push({ local: '__default__', exported: 'default' }); } return exports; } function collectImports(content) { const imports = []; const re = /import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g; let m; while ((m = re.exec(content)) !== null) { const names = m[1].split(',').map(s => { const parts = s.trim().split(/\s+as\s+/); return { local: (parts[1] || parts[0]).trim(), imported: parts[0].trim() }; }); imports.push({ names, from: m[2] }); } // import default const re2 = /import\s+(\w+)\s+from\s*['"]([^'"]+)['"]/g; while ((m = re2.exec(content)) !== null) { imports.push({ names: [{ local: m[1], imported: 'default' }], from: m[2] }); } return imports; } function processFile(filePath) { const relPath = path.relative(SRC, filePath); if (visited.has(relPath)) return; visited.add(relPath); let content = fs.readFileSync(filePath, 'utf8'); const dir = path.dirname(filePath); // Collect exports/imports BEFORE stripping fileExports[relPath] = collectExports(content); fileImports[relPath] = collectImports(content); // Process dependencies first (depth-first) for (const imp of fileImports[relPath]) { const depPath = resolveImportPath(imp.from, dir); processFile(depPath); } // Strip import statements content = content.replace(/import\s*\{[^}]+\}\s*from\s*['"][^'"]+['"];?/g, ''); content = content.replace(/import\s+\w+\s+from\s*['"][^'"]+['"];?/g, ''); // Strip export keyword but keep declarations content = content.replace(/export\s+default\s+/, 'const __default__ = '); content = content.replace(/export\s+(const|let|var|function|class)\s/g, '$1 '); content = content.replace(/export\s*\{[^}]*\};?/g, ''); chunks.push({ relPath, content }); } // Phase 1: Collect all files, build dependency graph processFile(path.join(SRC, ENTRY)); // Phase 2: Build rename map // For each file's import { X as Y } from './other.js', we need to know: // What is X called in other.js? Then rename Y → X_original // Build: originalName (in source file) → what it's called elsewhere // Simpler approach: since we wrap everything in one scope, // we just need to ensure no name collisions. // For now: trust that most names are unique across modules. // If collision, prefix with module path. // Phase 3: Emit let output = '// GrechkaCraft — auto-bundled from ES modules\n'; output += '(function() {\n'; output += '"use strict";\n\n'; for (const chunk of chunks) { output += `// === ${chunk.relPath} ===\n`; output += chunk.content.trim(); output += '\n\n'; } output += '})();\n'; process.stdout.write(output);