#!/usr/bin/env node /** * Pack Flying Jib as a Bare standalone executable (peardock pattern). * * bare-build CLI does not accept --imports; this script uses bare-pack with * build/squid-imports.json + package.json imports, then embeds into bare-runtime. * * Usage: * node scripts/bare-standalone.cjs * node scripts/bare-standalone.cjs --host darwin-arm64 * node scripts/bare-standalone.cjs --host all */ 'use strict' const path = require('path') const fs = require('fs') const { pathToFileURL } = require('url') const pack = require('bare-pack') const { readModule, listPrefix } = require('bare-pack/fs') const traverse = require('bare-module-traverse') const id = require('bare-bundle-id') const root = path.resolve(__dirname, '..') const pkg = require(path.join(root, 'package.json')) const ALL_HOSTS = [ 'darwin-arm64', 'darwin-x64', 'linux-arm64', 'linux-x64', 'win32-arm64', 'win32-x64' ] function parseArgs(argv) { const out = { hosts: [], outRoot: path.join(root, 'out') } for (let i = 0; i < argv.length; i++) { const a = argv[i] if (a === '--host') { const h = argv[++i] if (h === 'all') out.hosts.push(...ALL_HOSTS) else out.hosts.push(h) } else if (a === '--out') out.outRoot = path.resolve(argv[++i]) else if (a === '--help' || a === '-h') out.help = true } if (!out.hosts.length) out.hosts.push(`${process.platform}-${process.arch}`) return out } function fileURL(rel) { return pathToFileURL(path.join(root, rel)).href } function buildImportsMap() { // Prefer generated squid-imports (bnr + shims) const squidImportsPath = path.join(root, 'build', 'squid-imports.json') let map = {} if (fs.existsSync(squidImportsPath)) { map = JSON.parse(fs.readFileSync(squidImportsPath, 'utf8')) } else { try { map = { ...require('bare-node-runtime/imports') } } catch { map = {} } } // Merge package.json imports (relative bare paths need absolutizing) const pkgImports = pkg.imports || {} for (const [spec, target] of Object.entries(pkgImports)) { if (typeof target === 'string') { map[spec] = target continue } if (target && typeof target === 'object') { map[spec] = { ...target } } } for (const [spec, target] of Object.entries(map)) { if (!target || typeof target !== 'object') continue if (typeof target.bare === 'string' && target.bare.startsWith('./')) { map[spec] = { ...target, bare: fileURL(target.bare.replace(/^\.\//, '')) } } } // Explicit stubs for optional / browser-only / missing optional deps const stubs = { 'esbuild-import-glob(path:.,skipFiles:index.js,external.js)': 'build/stubs/esbuild-import-glob.cjs', 'supports-color': 'build/stubs/supports-color.cjs', 'cpu-features': 'build/stubs/supports-color.cjs', 'encoding': 'build/stubs/auto-encoding.cjs', 'bufferutil': 'build/stubs/auto-bufferutil.cjs', 'utf-8-validate': 'build/stubs/auto-utf-8-validate.cjs', 'canvas': 'build/stubs/auto-canvas.cjs', 'sqlite3': 'build/stubs/auto-sqlite3.cjs', 'better-sqlite3': 'build/stubs/auto-better-sqlite3.cjs' } for (const [spec, rel] of Object.entries(stubs)) { map[spec] = { bare: fileURL(rel), default: fileURL(rel) } } return map } function platformForHost(host) { const bareBuildRoot = path.dirname(require.resolve('bare-build/package')) const load = (name) => require(path.join(bareBuildRoot, 'lib', 'platform', name)) if (host.startsWith('darwin') || host.startsWith('ios')) return load('apple') if (host.startsWith('linux')) return load('linux') if (host.startsWith('win32')) return load('windows') if (host.startsWith('android')) return load('android') throw new Error(`Unknown host '${host}'`) } function walkFind(dir, pred) { const stack = [dir] while (stack.length) { const d = stack.pop() let entries try { entries = fs.readdirSync(d, { withFileTypes: true }) } catch { continue } for (const ent of entries) { const p = path.join(d, ent.name) if (ent.isDirectory()) stack.push(p) else if (pred(p)) return p } } return null } function runNodeScript(rel, extraArgs = []) { require('child_process').execSync( `node ${JSON.stringify(path.join(root, rel))}${extraArgs.map((a) => ' ' + JSON.stringify(a)).join('')}`, { cwd: root, stdio: 'inherit' } ) } async function buildOne(host, outRoot) { const name = pkg.productName ? 'flying-jib' : pkg.name const outDir = path.join(outRoot, `${name}-${host}`) fs.rmSync(outDir, { recursive: true, force: true }) fs.mkdirSync(outDir, { recursive: true }) // Ensure postinstall artifacts runNodeScript('scripts/generate-squid-imports.js') runNodeScript('scripts/patch-engines-for-bare.js') // Shrink pack graph: PC Squid versions only, no bedrock (~hundreds of MiB) const skipPrune = process.env.FJ_SKIP_MC_DATA_PRUNE === '1' if (!skipPrune) { console.log('[bare-standalone] pruning minecraft-data for pack…') runNodeScript('scripts/prune-minecraft-data.js') } else { console.log('[bare-standalone] FJ_SKIP_MC_DATA_PRUNE=1 — packing full minecraft-data') } let entry try { const entryPath = path.join(root, 'bin.mjs') const imports = buildImportsMap() console.log(`[bare-standalone] packing ${name} for ${host}…`) entry = await pack( pathToFileURL(entryPath), { hosts: [host], linked: false, resolve: traverse.resolve.bare, imports }, readModule, listPrefix ) const baseURL = pathToFileURL(root + path.sep) entry = entry.unmount(baseURL) entry.id = id(entry).toString('hex') const platform = platformForHost(host) const opts = { name, version: pkg.version || '0.0.0', description: pkg.description || 'Flying Jib', author: pkg.author || '', identifier: 'dev.flyingjib.app', hosts: [host], out: outDir, standalone: true, package: false, base: root } console.log(`[bare-standalone] embedding bare-runtime for ${host}…`) for await (const resource of platform(root, entry, null, opts)) { if (resource && resource.path) { console.log(`[bare-standalone] resource ${resource.path}`) } } } finally { // Always restore full data.js so local bare/npm start keep multi-version support if (!skipPrune) { try { runNodeScript('scripts/prune-minecraft-data.js', ['--restore']) } catch (e) { console.warn('[bare-standalone] WARN: could not restore minecraft-data:', e.message) } } } const binName = host.startsWith('win32') ? `${name}.exe` : name let binary = path.join(outDir, binName) if (!fs.existsSync(binary)) { const found = walkFind(outDir, (f) => { const base = path.basename(f) return base === name || base === `${name}.exe` || base === 'flying-jib' }) if (found) binary = found } if (fs.existsSync(binary)) { try { fs.chmodSync(binary, 0o755) } catch { /* win */ } const flat = path.join(outDir, path.basename(binary)) if (path.resolve(binary) !== path.resolve(flat)) { fs.copyFileSync(binary, flat) binary = flat } console.log(`[bare-standalone] wrote ${binary}`) } else { console.warn(`[bare-standalone] WARN: binary not found under ${outDir}`) try { console.warn( ' contents:', fs.readdirSync(outDir, { recursive: true }).slice(0, 40).join(', ') ) } catch { /* ignore */ } } fs.writeFileSync( path.join(outDir, 'build-info.json'), JSON.stringify( { host, name, version: pkg.version, builtAt: new Date().toISOString(), entry: 'bin.mjs', bundleId: entry && entry.id, minecraftDataPruned: !skipPrune }, null, 2 ) + '\n' ) return outDir } async function main() { const opts = parseArgs(process.argv.slice(2)) if (opts.help) { console.log(`Usage: node scripts/bare-standalone.cjs [--host |all] [--out dir] Hosts: ${ALL_HOSTS.join(', ')}`) process.exit(0) } const results = [] for (const host of opts.hosts) { results.push(await buildOne(host, opts.outRoot)) } console.log('[bare-standalone] done:', results.join(', ')) } main().catch((err) => { console.error(err) process.exit(1) })