Files
flying-jib/scripts/bare-standalone.cjs
T
2026-07-31 00:37:53 -04:00

291 lines
8.3 KiB
JavaScript

#!/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 <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)
})