/** * Bundle every bare-module-manifest entry into IIFE scripts under bundles/ so the * system image can populate ctx.bare without relying on the Pear host node_modules. * Fail-fast: any esbuild error aborts the build (no stub placeholders). */ import { readFile, writeFile, mkdir, copyFile, readdir, unlink } from 'node:fs/promises' import { execSync } from 'node:child_process' import { dirname, join } from 'node:path' import { fileURLToPath, pathToFileURL } from 'node:url' import * as esbuild from 'esbuild' const __dirname = dirname(fileURLToPath(import.meta.url)) const repoRoot = join(__dirname, '..', '..') const booterNodeModules = join(repoRoot, 'packages/bare-os-booter/node_modules') /** * esbuild `platform: 'node'` marks builtins external; drive IIFEs run under `new Function` * on Pear with no `globalThis.require`. Map Node core modules to Bare shims so they bundle in. * Use package names (not absolute paths) so esbuild resolves them via `nodePaths`. */ function bareDriveBundleNodeBuiltinAliases() { return { assert: 'bare-assert', crypto: 'bare-crypto', events: 'bare-node-events', fs: 'bare-node-fs', net: 'bare-node-net', url: 'bare-url' } } const manifestPath = join( repoRoot, 'packages/bare-os-booter/lib/bare-module-manifest.json' ) const kernelLibBare = join(repoRoot, 'kernel/lib/bare') const seederLibBare = join(repoRoot, 'packages/bare-os-seeder/kernel/lib/bare') const bundlesKernel = join(kernelLibBare, 'bundles') const bundlesSeeder = join(seederLibBare, 'bundles') const STDLIB_GLOBAL = '__bare_os_stdlib__' const IIFE_GLOBAL = '__bare_os_bundle_exports__' const BUNDLE_CONCURRENCY = 6 /** * @param {{ optional?: unknown, nativeHint?: unknown }} ent */ function bundleTierForEntry(ent) { if (!ent || typeof ent !== 'object') return 'core' const optional = !!ent.optional const nativeHint = !!ent.nativeHint if (!optional) return 'core' return nativeHint ? 'optional_native' : 'optional_js' } const BUNDLE_TIER_THRESHOLDS = { core: { maxGrowthPct: 12, minPaddingBytes: 4096 }, optional_js: { maxGrowthPct: 20, minPaddingBytes: 3072 }, optional_native: { maxGrowthPct: 30, minPaddingBytes: 4096 } } function readGitHead(cwd) { try { return execSync('git rev-parse HEAD', { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() } catch { return '' } } /** Resolve bare-native `imports` subpath specifiers (#web-view / #window) for the host OS. */ function bareNativeConditionalImportsPlugin() { const bareNativeRoot = join(repoRoot, 'node_modules/bare-native') function webViewAbs() { const p = process.platform if (p === 'darwin' || p === 'ios') return join(bareNativeRoot, 'lib/web-view/apple.js') if (p === 'win32') return join(bareNativeRoot, 'lib/web-view/win32.js') if (p === 'android') return join(bareNativeRoot, 'lib/web-view/android.js') return join(bareNativeRoot, 'lib/web-view/linux.js') } function windowAbs() { const p = process.platform if (p === 'darwin') return join(bareNativeRoot, 'lib/window/darwin.js') if (p === 'ios') return join(bareNativeRoot, 'lib/window/ios.js') if (p === 'win32') return join(bareNativeRoot, 'lib/window/win32.js') if (p === 'android') return join(bareNativeRoot, 'lib/window/android.js') return join(bareNativeRoot, 'lib/window/linux.js') } return { name: 'bare-native-subpath-imports', setup(build) { build.onResolve({ filter: /^#web-view$/ }, () => ({ path: webViewAbs() })) build.onResolve({ filter: /^#window$/ }, () => ({ path: windowAbs() })) } } } /** * @template T * @param {T[]} items * @param {number} concurrency * @param {(item: T, index: number) => Promise} fn */ async function runPool(items, concurrency, fn) { let i = 0 async function worker() { while (i < items.length) { const idx = i++ await fn(items[idx], idx) } } await Promise.all(Array.from({ length: concurrency }, () => worker())) } function stdinForEntry(ent) { const pkg = ent.package if (ent.sideEffectImport) { return `import ${JSON.stringify(pkg)};\nexport default true;\n` } if (ent.export === '*') { return `import * as _m from ${JSON.stringify(pkg)};\nexport default _m;\n` } return `import _m from ${JSON.stringify(pkg)};\nexport default _m;\n` } export async function buildBareLibs() { const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) const entries = Array.isArray(manifest.entries) ? manifest.entries : [] const entryByCtxKey = new Map( entries .filter((e) => e && typeof e === 'object' && e.ctxKey) .map((e) => [String(e.ctxKey), e]) ) const tierFilter = String(process.env.BARE_OS_BUNDLE_TIER || '') .trim() .toLowerCase() const toBundle = entries .filter((e) => e.ctxKey && e.package) .filter((e) => { if (!tierFilter || tierFilter === 'all') return true const t = (e.tier && String(e.tier).toLowerCase()) || 'core' return t === tierFilter }) await mkdir(bundlesKernel, { recursive: true }) await mkdir(bundlesSeeder, { recursive: true }) /** @type {{ path: string, keys: string[] }[]} */ const bundles = [] /** @type {{ ctxKey: string, package: string, path: string, bytes: number }[]} */ const bundleDiagnostics = [] let ok = 0 await runPool(toBundle, BUNDLE_CONCURRENCY, async (ent) => { const ctxKey = ent.ctxKey const pkg = ent.package const outfile = join(bundlesKernel, `${ctxKey}.js`) const outfileSeeder = join(bundlesSeeder, `${ctxKey}.js`) const footer = `;(function(){var g=globalThis;var s=${JSON.stringify(STDLIB_GLOBAL)};g[s]=g[s]||{};var e=typeof ${IIFE_GLOBAL}!=="undefined"?${IIFE_GLOBAL}:void 0;var v=e!=null&&typeof e==="object"&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e;g[s][${JSON.stringify(ctxKey)}]=v;})();` try { await esbuild.build({ stdin: { contents: stdinForEntry(ent), resolveDir: repoRoot, sourcefile: `bare-lib-entry-${ctxKey}.js`, loader: 'js' }, bundle: true, format: 'iife', globalName: IIFE_GLOBAL, platform: 'node', alias: bareDriveBundleNodeBuiltinAliases(), nodePaths: [booterNodeModules, join(repoRoot, 'node_modules')], plugins: [bareNativeConditionalImportsPlugin()], outfile, footer: { js: footer }, logLevel: 'silent' }) } catch (err) { const msg = err?.message || String(err) console.error( '[bare-os-bare-libs] bundle failed', ctxKey, '(' + pkg + '):', msg ) throw new Error( `[bare-os-bare-libs] esbuild failed for ${ctxKey} (${pkg}): ${msg}` ) } ok++ bundles.push({ path: `/lib/bare/bundles/${ctxKey}.js`, keys: [ctxKey] }) const built = await readFile(outfile) await writeFile(outfileSeeder, built) bundleDiagnostics.push({ ctxKey, package: pkg, path: `/lib/bare/bundles/${ctxKey}.js`, bytes: built.length, tier: bundleTierForEntry(ent) }) }) bundleDiagnostics.sort((a, b) => a.ctxKey.localeCompare(b.ctxKey)) const builtNames = new Set(bundleDiagnostics.map((d) => `${d.ctxKey}.js`)) async function pruneStaleBundles(dir) { let names try { names = await readdir(dir) } catch { return } for (const name of names) { if (!name.endsWith('.js')) continue if (builtNames.has(name)) continue await unlink(join(dir, name)) console.warn('[bare-os-bare-libs] removed stale bundle', name) } } await pruneStaleBundles(bundlesKernel) await pruneStaleBundles(bundlesSeeder) const { sanitizeBareBundlesInDir } = await import( pathToFileURL(join(repoRoot, 'scripts/sanitize-bare-bundles.mjs')).href ) sanitizeBareBundlesInDir(bundlesKernel) sanitizeBareBundlesInDir(bundlesSeeder) for (const row of bundleDiagnostics) { const rel = join(bundlesKernel, `${row.ctxKey}.js`) try { const buf = await readFile(rel) row.bytes = buf.length } catch { /* keep prior */ } } const driveManifest = { version: 1, bundles, bundleStats: { ok, failed: 0, attempted: toBundle.length }, bundleDiagnostics, bundleProvenance: { schemaVersion: 1, generatedAt: new Date().toISOString(), gitCommit: readGitHead(repoRoot), nodeVersion: process.version, bundleTier: tierFilter && tierFilter !== 'all' ? tierFilter : 'all', normativeManifest: 'packages/bare-os-booter/lib/bare-module-manifest.json', buildScript: 'packages/bare-os-bare-libs/build.mjs' } } const json = JSON.stringify(driveManifest, null, 2) + '\n' await writeFile(join(kernelLibBare, 'manifest.json'), json) await writeFile(join(seederLibBare, 'manifest.json'), json) const bundleHealthPath = join(repoRoot, 'docs/audit/bundle-health.json') const bundleRegressionThresholds = { schemaVersion: 1, note: 'Generated thresholds by package tier. verify-bundle-health fails when disk bytes exceed maxBytes for a ctxKey.', tierPolicies: BUNDLE_TIER_THRESHOLDS, byCtxKey: bundleDiagnostics.map((row) => { const ent = entryByCtxKey.get(row.ctxKey) const tier = row.tier || bundleTierForEntry(ent) const pol = BUNDLE_TIER_THRESHOLDS[tier] || BUNDLE_TIER_THRESHOLDS.optional_native const pct = Number(pol.maxGrowthPct) || 20 const pad = Number(pol.minPaddingBytes) || 4096 const growth = Math.ceil((Number(row.bytes) * pct) / 100) return { ctxKey: row.ctxKey, tier, baselineBytes: row.bytes, maxBytes: row.bytes + Math.max(growth, pad) } }) } const bundleHealth = { schemaVersion: 1, generatedAt: new Date().toISOString(), normativeManifest: 'packages/bare-os-booter/lib/bare-module-manifest.json', buildTool: 'packages/bare-os-bare-libs/build.mjs', bundles: bundleDiagnostics, bundleStats: { ok, failed: 0, attempted: toBundle.length }, bundleRegressionThresholds } await writeFile( bundleHealthPath, JSON.stringify(bundleHealth, null, 2) + '\n' ) await copyFile( manifestPath, join(kernelLibBare, 'bare-module-manifest.json') ) await copyFile( manifestPath, join(seederLibBare, 'bare-module-manifest.json') ) const readmeSrc = join(__dirname, 'README.kernel-lib-bare.md') const readmeDstKernel = join(kernelLibBare, 'README.md') const readmeDstSeeder = join(seederLibBare, 'README.md') try { await copyFile(readmeSrc, readmeDstKernel) await copyFile(readmeSrc, readmeDstSeeder) } catch { /* optional */ } console.log( '[bare-os-bare-libs] bundles:', ok, 'ok,', toBundle.length, 'attempted →', kernelLibBare ) } if ( process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href ) { await buildBareLibs() }