410 lines
12 KiB
JavaScript
410 lines
12 KiB
JavaScript
/**
|
|
* Host-resolved Bare / companion npm modules for in-image scripts (`ctx.bare`),
|
|
* plus optional merge from trusted IIFE bundles on the system drive.
|
|
*/
|
|
|
|
import { readFileSync, statSync } from 'fs'
|
|
import { dirname, join, resolve } from 'path'
|
|
import { fileURLToPath } from 'url'
|
|
|
|
/**
|
|
* `fileURLToPath` only accepts `file:` URLs. Pear uses `pear://dev/...` for `import.meta.url`,
|
|
* so we must not call it at load time or when running under `pear run`.
|
|
* @returns {string | null}
|
|
*/
|
|
function bareOsBooterPackageJsonPathForCreateRequire() {
|
|
const href = String(import.meta.url)
|
|
if (!href.startsWith('file:')) return null
|
|
try {
|
|
return fileURLToPath(new URL('../package.json', import.meta.url))
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Drive IIFEs call `require("events")`, `require("bare-stream")`, …; `new Function` has no
|
|
* lexical `require`, so they look at **`globalThis.require`**. On **`file:`** dev trees, install
|
|
* Node's `createRequire` for the booter package for the duration of bundle eval. Under Pear
|
|
* (`pear://`), skip — runtime `require` or `tryBareFetchImportWhenDriveMissing` covers fetch.
|
|
* @param {() => void | Promise<void>} fn
|
|
*/
|
|
async function withDriveBundleGlobalRequire(fn) {
|
|
if (typeof globalThis.require === 'function') return fn()
|
|
const pkgJson = bareOsBooterPackageJsonPathForCreateRequire()
|
|
let installed = false
|
|
if (pkgJson) {
|
|
try {
|
|
const { createRequire } = await import('node:module')
|
|
globalThis.require = createRequire(pkgJson)
|
|
installed = true
|
|
} catch {
|
|
/* no node:module / createRequire */
|
|
}
|
|
}
|
|
try {
|
|
return await fn()
|
|
} finally {
|
|
if (installed) {
|
|
delete globalThis.require
|
|
}
|
|
}
|
|
}
|
|
|
|
export const BARE_OS_STDLIB_GLOBAL = '__bare_os_stdlib__'
|
|
|
|
const MANIFEST_NAME = 'bare-module-manifest.json'
|
|
|
|
/** @param {string} root */
|
|
function libDirIfManifestAtRoot(root) {
|
|
if (!root || root === '/' || root === '') return null
|
|
const tries = [
|
|
join(root, 'lib', MANIFEST_NAME),
|
|
join(root, 'packages', 'bare-os-booter', 'lib', MANIFEST_NAME)
|
|
]
|
|
for (const f of tries) {
|
|
try {
|
|
if (statSync(f).isFile()) return dirname(f)
|
|
} catch {
|
|
/* continue */
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* `pear://app/lib/foo.js` → try **`join(mount, 'lib')`** for each bundle mount (manifest must exist).
|
|
* @param {string} href
|
|
* @param {string[]} mounts
|
|
*/
|
|
function pearUrlLibDir(href, mounts) {
|
|
if (!href.startsWith('pear:')) return null
|
|
let u
|
|
try {
|
|
u = new URL(href)
|
|
} catch {
|
|
return null
|
|
}
|
|
const segs = u.pathname.split('/').filter(Boolean)
|
|
if (segs.length < 2) return null
|
|
segs.pop()
|
|
const rel = segs.join('/')
|
|
if (!rel) return null
|
|
for (const m of mounts) {
|
|
if (!m || m === '/') continue
|
|
const candidate = resolve(join(String(m), rel))
|
|
try {
|
|
if (statSync(join(candidate, MANIFEST_NAME)).isFile()) return candidate
|
|
} catch {
|
|
/* continue */
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Directory containing `bare-module-manifest.json`. Resolved lazily so Pear **`cwd`** / mounts are valid.
|
|
* Never fall back to **`/lib`** (happens when **`join('/', 'lib')`** after a bogus **`/`** candidate).
|
|
*/
|
|
function resolveBareCtxLibDir() {
|
|
const href = String(import.meta.url)
|
|
if (href.startsWith('file:')) {
|
|
return dirname(fileURLToPath(href))
|
|
}
|
|
|
|
const rti = globalThis.Pear?.constructor?.RTI?.mount
|
|
const swap = globalThis.Pear?.config?.swapDir
|
|
const cwdRaw =
|
|
typeof globalThis.process?.cwd === 'function'
|
|
? globalThis.process.cwd()
|
|
: ''
|
|
const cwd = cwdRaw ? resolve(String(cwdRaw)) : resolve('.')
|
|
|
|
let d = cwd
|
|
for (let i = 0; i < 48; i++) {
|
|
const found = libDirIfManifestAtRoot(d)
|
|
if (found) return found
|
|
const parent = dirname(d)
|
|
if (parent === d) break
|
|
d = parent
|
|
}
|
|
|
|
const mounts = []
|
|
const seenM = new Set()
|
|
for (const c of [rti, swap].filter(Boolean)) {
|
|
const p = resolve(String(c))
|
|
if (p === '/' || !p || seenM.has(p)) continue
|
|
seenM.add(p)
|
|
mounts.push(p)
|
|
}
|
|
const fromPear = pearUrlLibDir(href, mounts)
|
|
if (fromPear) return fromPear
|
|
|
|
const seen = new Set()
|
|
for (const c of [cwd, ...mounts].filter(Boolean)) {
|
|
const p = resolve(String(c))
|
|
if (p === '/' || !p || seen.has(p)) continue
|
|
seen.add(p)
|
|
const found = libDirIfManifestAtRoot(p)
|
|
if (found) return found
|
|
}
|
|
|
|
throw new Error(
|
|
`[bare-os-booter] Cannot find ${MANIFEST_NAME}. ` +
|
|
`cwd=${JSON.stringify(cwdRaw)} pearMount=${JSON.stringify(rti ?? null)} import.meta.url=${JSON.stringify(href.slice(0, 120))}`
|
|
)
|
|
}
|
|
|
|
/** @type {{ version: number, entries: Array<{ ctxKey: string, package: string, export?: string, bundle?: boolean, optional?: boolean, tier?: string }> }} */
|
|
let _cachedManifest = null
|
|
|
|
export function loadBareModuleManifest() {
|
|
if (_cachedManifest) return _cachedManifest
|
|
const libDir = resolveBareCtxLibDir()
|
|
const raw = readFileSync(join(libDir, MANIFEST_NAME), 'utf8')
|
|
_cachedManifest = JSON.parse(raw)
|
|
return _cachedManifest
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, string>} shellEnv
|
|
*/
|
|
export function bareOsBareModulesEnabled(shellEnv) {
|
|
if (!shellEnv || typeof shellEnv !== 'object') return true
|
|
const v = shellEnv.BARE_OS_BARE_MODULES
|
|
return v !== '0' && v !== 'false'
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, string>} shellEnv
|
|
*/
|
|
export function bareOsBareDriveBundlesEnabled(shellEnv) {
|
|
if (!shellEnv || typeof shellEnv !== 'object') return true
|
|
const v = shellEnv.BARE_OS_BARE_DRIVE_BUNDLES
|
|
if (v === '0' || v === 'false') return false
|
|
return bareOsBareModulesEnabled(shellEnv)
|
|
}
|
|
|
|
/**
|
|
* When false, skip host `import(pkg)` for ctx.bare (use only `/lib/bare` IIFEs).
|
|
* Set **`BARE_OS_BARE_HOST_IMPORTS=0`** for a fully image-local stdlib.
|
|
*/
|
|
export function bareOsBareHostImportsEnabled(shellEnv) {
|
|
if (!shellEnv || typeof shellEnv !== 'object') return true
|
|
const v = shellEnv.BARE_OS_BARE_HOST_IMPORTS
|
|
if (v === '0' || v === 'false') return false
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* Populate object with host `import()` results (per manifest). Mutates `target`.
|
|
* @param {Record<string, string>} shellEnv
|
|
* @param {Record<string, unknown>} target
|
|
*/
|
|
function bareHostRuntime() {
|
|
if (typeof globalThis.Bare !== 'undefined') return true
|
|
const v = globalThis.process?.versions
|
|
return Boolean(v && typeof v.bare === 'string')
|
|
}
|
|
|
|
export async function buildBareCtxObjectFromHost(shellEnv, target) {
|
|
if (!bareOsBareModulesEnabled(shellEnv)) return
|
|
if (!bareOsBareHostImportsEnabled(shellEnv)) return
|
|
const { entries } = loadBareModuleManifest()
|
|
const onBare = bareHostRuntime()
|
|
const tasks = entries.map(async (ent) => {
|
|
if (!onBare && ent.nativeHint === true) return null
|
|
const key = ent.ctxKey
|
|
if (!key || target[key] !== undefined) return null
|
|
try {
|
|
const mod = await import(/* webpackIgnore: true */ ent.package)
|
|
const val = ent.sideEffectImport
|
|
? mod?.default !== undefined
|
|
? mod.default
|
|
: true
|
|
: ent.export === '*'
|
|
? mod
|
|
: mod?.default !== undefined
|
|
? mod.default
|
|
: mod
|
|
return { key, ent, val }
|
|
} catch (err) {
|
|
return { key, ent, err }
|
|
}
|
|
})
|
|
const settled = await Promise.all(tasks)
|
|
for (const r of settled) {
|
|
if (!r) continue
|
|
const { key, ent, val, err } = r
|
|
if (err) {
|
|
if (!ent.optional) {
|
|
console.warn(
|
|
`[bare-os-booter] ctx.bare.${key}: failed to load "${ent.package}":`,
|
|
err?.message || err
|
|
)
|
|
}
|
|
continue
|
|
}
|
|
if (val !== undefined) target[key] = val
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute trusted bundle sources that assign `globalThis.__bare_os_stdlib__[ctxKey]`.
|
|
* Fills missing keys on `target` only.
|
|
* @param {Record<string, string>} shellEnv
|
|
* @param {{ readFile: (p: string, opts?: unknown) => Promise<Uint8Array | null> }} vfs
|
|
* @param {Record<string, unknown>} target
|
|
*/
|
|
/**
|
|
* Optional `/lib/bare/bare-module-lock.json` on the system image: `{ "pins": { "ctxKey": "semver" } }`.
|
|
* Emits warnings for pins whose `ctxKey` is still missing after drive merge + host import.
|
|
* @param {{ readFile: (p: string) => Promise<Uint8Array | null> }} vfs
|
|
* @param {Record<string, unknown>} target
|
|
* @returns {Promise<string[]>}
|
|
*/
|
|
export async function verifyBareModuleLockfile(vfs, target) {
|
|
/** @type {string[]} */
|
|
const warnings = []
|
|
if (!vfs || typeof vfs.readFile !== 'function') return warnings
|
|
let buf
|
|
try {
|
|
buf = await vfs.readFile('/lib/bare/bare-module-lock.json')
|
|
} catch {
|
|
return warnings
|
|
}
|
|
if (!buf || !buf.byteLength) return warnings
|
|
let j
|
|
try {
|
|
j = JSON.parse(new TextDecoder().decode(buf))
|
|
} catch {
|
|
warnings.push('bare-module-lock.json: invalid JSON')
|
|
return warnings
|
|
}
|
|
const pins = j && typeof j === 'object' ? j.pins : null
|
|
if (!pins || typeof pins !== 'object') return warnings
|
|
for (const [k, ver] of Object.entries(pins)) {
|
|
if (target[k] === undefined) {
|
|
warnings.push(
|
|
`bare-module-lock: ctx.bare.${k} missing (pinned ${String(ver)})`
|
|
)
|
|
}
|
|
}
|
|
return warnings
|
|
}
|
|
|
|
/**
|
|
* esbuild IIFE assigns `__bare_os_bundle_exports__` (CJS interop object), not the
|
|
* default export itself. Entries use `export default <binding>` → `{ default: binding }`.
|
|
* @param {unknown} val
|
|
*/
|
|
function unwrapDriveBundleExport(val) {
|
|
if (val === null || val === undefined) return val
|
|
if (typeof val !== 'object') return val
|
|
if (Object.prototype.hasOwnProperty.call(val, 'default')) {
|
|
const d = /** @type {{ default?: unknown }} */ (val).default
|
|
if (d !== undefined) return d
|
|
}
|
|
return val
|
|
}
|
|
|
|
export async function maybeMergeBareFromDrive(shellEnv, vfs, target) {
|
|
if (!bareOsBareModulesEnabled(shellEnv)) return
|
|
if (!bareOsBareDriveBundlesEnabled(shellEnv)) return
|
|
if (!vfs || typeof vfs.readFile !== 'function') return
|
|
|
|
let metaBuf
|
|
try {
|
|
metaBuf = await vfs.readFile('/lib/bare/manifest.json')
|
|
} catch {
|
|
metaBuf = null
|
|
}
|
|
if (!metaBuf || metaBuf.byteLength === 0) return
|
|
|
|
let meta
|
|
try {
|
|
const text = new TextDecoder().decode(metaBuf)
|
|
meta = JSON.parse(text)
|
|
} catch {
|
|
return
|
|
}
|
|
const bundles = meta?.bundles
|
|
if (!Array.isArray(bundles)) return
|
|
|
|
const g = globalThis
|
|
const sym = BARE_OS_STDLIB_GLOBAL
|
|
g[sym] = g[sym] && typeof g[sym] === 'object' ? g[sym] : {}
|
|
|
|
await withDriveBundleGlobalRequire(async () => {
|
|
for (const b of bundles) {
|
|
const path = typeof b?.path === 'string' ? b.path : ''
|
|
const keys = Array.isArray(b?.keys) ? b.keys : []
|
|
if (!path.startsWith('/lib/bare/')) continue
|
|
|
|
let srcBuf
|
|
try {
|
|
srcBuf = await vfs.readFile(path)
|
|
} catch {
|
|
srcBuf = null
|
|
}
|
|
if (!srcBuf || srcBuf.byteLength === 0) continue
|
|
|
|
const source = new TextDecoder().decode(srcBuf)
|
|
if (source.length > 12 * 1024 * 1024) continue
|
|
|
|
try {
|
|
const run = new Function(
|
|
`"use strict"; ${source}\n//# sourceURL=bare-drive-bundle:${path}`
|
|
)
|
|
run()
|
|
} catch (err) {
|
|
console.warn(
|
|
`[bare-os-booter] drive bundle failed ${path}:`,
|
|
err?.message || err
|
|
)
|
|
continue
|
|
}
|
|
|
|
const snap = g[sym]
|
|
if (!snap || typeof snap !== 'object') continue
|
|
|
|
for (const k of keys) {
|
|
if (typeof k !== 'string' || !k) continue
|
|
if (target[k] !== undefined) continue
|
|
if (Object.prototype.hasOwnProperty.call(snap, k)) {
|
|
target[k] = unwrapDriveBundleExport(snap[k])
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
await tryBareFetchImportWhenDriveMissing(target)
|
|
}
|
|
|
|
/**
|
|
* When drive `fetch.js` eval fails (no `globalThis.require` or native addons), Bare can still
|
|
* `import('bare-fetch')` from the booter — same as `buildBareCtxObjectFromHost`, but runs even
|
|
* when the manifest marks the entry host-only.
|
|
* @param {Record<string, unknown>} target
|
|
*/
|
|
async function tryBareFetchImportWhenDriveMissing(target) {
|
|
if (target.fetch !== undefined) return
|
|
if (!bareHostRuntime()) return
|
|
try {
|
|
let mod
|
|
if (typeof import.meta.resolve === 'function') {
|
|
try {
|
|
mod = await import(import.meta.resolve('bare-fetch'))
|
|
} catch {
|
|
mod = await import('bare-fetch')
|
|
}
|
|
} else {
|
|
mod = await import('bare-fetch')
|
|
}
|
|
const f = mod?.default
|
|
if (typeof f === 'function') target.fetch = f
|
|
} catch {
|
|
/* optional */
|
|
}
|
|
}
|