UNPKG

betterpack

Version:

A Universal Node.js Package Manager CLI with automated agent capabilities

341 lines (281 loc) 10.2 kB
const fs = require("fs") const path = require("path") const { spawn, spawnSync } = require("child_process") const archiver = require("archiver") const tar = require("tar") class ProjectBundler { constructor() { this.supportedFormats = ["js", "zip", "tar", "exe", "msix", "sh"] this.tempDir = path.join(process.cwd(), ".bpack-temp") } async bundle(format, options = {}) { const { output, include, exclude, minify = true, platform = process.platform } = options console.log(`🔧 Starting bundle process for format: ${format}`) try { // Ensure temp directory exists if (!fs.existsSync(this.tempDir)) { fs.mkdirSync(this.tempDir, { recursive: true }) } switch (format) { case "js": return await this.bundleToJS(output, { minify, include, exclude }) case "zip": return await this.bundleToZip(output, { include, exclude }) case "tar": return await this.bundleToTar(output, { include, exclude }) case "exe": return await this.bundleToExe(output, { platform, minify }) case "msix": return await this.bundleToMSIX(output, options) case "sh": return await this.bundleToShellInstaller(output, options) default: throw new Error(`Unsupported format: ${format}`) } } catch (error) { console.error(`❌ Bundle failed: ${error.message}`) throw error } finally { // Cleanup temp directory this.cleanup() } } async bundleToJS(outputPath, options) { console.log("📦 Bundling to single JavaScript file...") const packageJson = this.getPackageJson() const entryPoint = packageJson.main || "index.js" // Use webpack or rollup for bundling const bundlerConfig = this.createBundlerConfig(entryPoint, outputPath, options) if (this.hasWebpack()) { return await this.bundleWithWebpack(bundlerConfig) } else if (this.hasRollup()) { return await this.bundleWithRollup(bundlerConfig) } else { // Fallback: simple concatenation return await this.simpleBundleJS(entryPoint, outputPath, options) } } async bundleToZip(outputPath, options) { console.log("📦 Creating ZIP archive...") const output = fs.createWriteStream(outputPath || "bundle.zip") const archive = archiver("zip", { zlib: { level: 9 } }) return new Promise((resolve, reject) => { output.on("close", () => { console.log(`✅ ZIP created: ${archive.pointer()} bytes`) resolve(outputPath || "bundle.zip") }) archive.on("error", reject) archive.pipe(output) // Add files based on include/exclude patterns this.addFilesToArchive(archive, options) archive.finalize() }) } async bundleToTar(outputPath, options) { console.log("📦 Creating TAR archive...") const files = this.getFilesToBundle(options) const tarPath = outputPath || "bundle.tar.gz" await tar.create( { gzip: true, file: tarPath, cwd: process.cwd(), }, files, ) console.log(`✅ TAR created: ${tarPath}`) return tarPath } async bundleToExe(outputPath, options) { console.log("📦 Creating executable...") // First bundle to JS const jsBundle = path.join(this.tempDir, "bundle.js") await this.bundleToJS(jsBundle, options) // Use nexe or pkg to create executable if (this.hasNexe()) { return await this.createExeWithNexe(jsBundle, outputPath, options) } else if (this.hasPkg()) { return await this.createExeWithPkg(jsBundle, outputPath, options) } else { throw new Error("No executable bundler found. Install nexe or pkg: npm install -g nexe pkg") } } async bundleToMSIX(outputPath, options) { if (process.platform !== "win32") { throw new Error("MSIX packages can only be created on Windows") } console.log("📦 Creating MSIX installer...") // Create app manifest and package structure const packageDir = path.join(this.tempDir, "msix-package") await this.createMSIXStructure(packageDir, options) // Use Windows SDK tools to create MSIX const msixPath = outputPath || "installer.msix" await this.createMSIXPackage(packageDir, msixPath) console.log(`✅ MSIX created: ${msixPath}`) return msixPath } async bundleToShellInstaller(outputPath, options) { console.log("📦 Creating shell installer...") const installerPath = outputPath || "install.sh" const packageJson = this.getPackageJson() const installerScript = this.generateShellInstaller(packageJson, options) fs.writeFileSync(installerPath, installerScript, { mode: 0o755 }) console.log(`✅ Shell installer created: ${installerPath}`) return installerPath } // Helper methods getPackageJson() { const packagePath = path.join(process.cwd(), "package.json") if (!fs.existsSync(packagePath)) { throw new Error("package.json not found") } return JSON.parse(fs.readFileSync(packagePath, "utf8")) } hasWebpack() { try { require.resolve("webpack") return true } catch { return false } } hasRollup() { try { require.resolve("rollup") return true } catch { return false } } hasNexe() { const result = spawnSync("nexe", ["--version"], { shell: true }) return result.status === 0 } hasPkg() { const result = spawnSync("pkg", ["--version"], { shell: true }) return result.status === 0 } getFilesToBundle(options) { const { include = ["**/*"], exclude = ["node_modules/**", ".git/**", "*.log"] } = options // Simple file matching - in production, use glob library const allFiles = this.getAllFiles(process.cwd()) return allFiles.filter((file) => { const relativePath = path.relative(process.cwd(), file) return this.matchesPattern(relativePath, include) && !this.matchesPattern(relativePath, exclude) }) } getAllFiles(dir, files = []) { const entries = fs.readdirSync(dir) for (const entry of entries) { const fullPath = path.join(dir, entry) const stat = fs.statSync(fullPath) if (stat.isDirectory()) { this.getAllFiles(fullPath, files) } else { files.push(fullPath) } } return files } matchesPattern(filePath, patterns) { // Simple pattern matching - in production, use minimatch return patterns.some((pattern) => { if (pattern.includes("**")) { const regex = new RegExp(pattern.replace("**", ".*").replace("*", "[^/]*")) return regex.test(filePath) } return filePath.includes(pattern.replace("*", "")) }) } generateShellInstaller(packageJson, options) { return `#!/bin/bash # Auto-generated installer for ${packageJson.name} # Version: ${packageJson.version} set -e echo "Installing ${packageJson.name}..." # Check for Node.js if ! command -v node &> /dev/null; then echo "Node.js is required but not installed." echo "Please install Node.js from https://nodejs.org/" exit 1 fi # Create installation directory INSTALL_DIR="/usr/local/lib/${packageJson.name}" sudo mkdir -p "$INSTALL_DIR" # Extract and install files echo "Extracting files..." # Add extraction logic here based on bundled format # Create symlink for global access sudo ln -sf "$INSTALL_DIR/bin/${packageJson.name}" "/usr/local/bin/${packageJson.name}" echo "✅ ${packageJson.name} installed successfully!" echo "Run '${packageJson.name} --help' to get started." ` } cleanup() { if (fs.existsSync(this.tempDir)) { fs.rmSync(this.tempDir, { recursive: true, force: true }) } } } function displayBundlerHelp() { console.log("\n\x1b[1mBetterpack Bundler\x1b[0m") console.log("\nUsage: bpack bundle <format> [options]") console.log("\n\x1b[1mSupported Formats:\x1b[0m") console.log(" \x1b[36mjs\x1b[0m Bundle to single JavaScript file") console.log(" \x1b[36mzip\x1b[0m Create ZIP archive") console.log(" \x1b[36mtar\x1b[0m Create TAR.GZ archive") console.log(" \x1b[36mexe\x1b[0m Create executable (requires nexe or pkg)") console.log(" \x1b[36mmsix\x1b[0m Create Windows MSIX installer") console.log(" \x1b[36msh\x1b[0m Create shell installer script") console.log("\n\x1b[1mOptions:\x1b[0m") console.log(" \x1b[33m-o, --output\x1b[0m Output file path") console.log(" \x1b[33m--include\x1b[0m Include patterns (comma-separated)") console.log(" \x1b[33m--exclude\x1b[0m Exclude patterns (comma-separated)") console.log(" \x1b[33m--no-minify\x1b[0m Disable minification") console.log(" \x1b[33m--platform\x1b[0m Target platform for executables") console.log("\n\x1b[1mExamples:\x1b[0m") console.log(" bpack bundle js -o dist/app.js") console.log(' bpack bundle zip --exclude "*.log,node_modules/**"') console.log(" bpack bundle exe --platform win32") console.log(" bpack bundle msix -o MyApp.msix") console.log("") } async function handleBundleCommand(args) { if (args.length === 0 || args.includes("--help") || args.includes("-h")) { displayBundlerHelp() return } const format = args[0] const bundler = new ProjectBundler() // Parse options const options = {} for (let i = 1; i < args.length; i++) { const arg = args[i] switch (arg) { case "-o": case "--output": options.output = args[++i] break case "--include": options.include = args[++i].split(",") break case "--exclude": options.exclude = args[++i].split(",") break case "--no-minify": options.minify = false break case "--platform": options.platform = args[++i] break } } try { const result = await bundler.bundle(format, options) console.log(`🎉 Bundle completed: ${result}`) } catch (error) { console.error(`❌ Bundle failed: ${error.message}`) process.exit(1) } } module.exports = { ProjectBundler, handleBundleCommand, displayBundlerHelp }