// The worklet entry that preloads OTA-staged addons and then runs the real bundle.
//
// ⛔ ORDER IS THE WHOLE POINT. Addons must be in Addon.cache BEFORE the bundle's modules evaluate —
// a `require.addon` reached first reads the cache, misses, and falls straight through to
// dlopen-by-soname against the APK. That is not recoverable afterwards, so the ordering is asserted
// here rather than assumed from the shape of the code.
import test from 'brittle'
import { loaderArgs, runLoader } from '../lib/host/loader-core.js'
const argv = (...a) => a
test('the bundle and addon paths are read by TAG, not position', (t) => {
// ⛔ NOT POSITIONAL. bare-kit calls NativeBareKit.startFile(handle, filename, args) and whether the
// native side prepends the filename to Bare.argv is decided in native code we cannot see from JS.
// lib/node.js already works around this by reading the LAST argv; tags make the loader immune to it
// in both directions.
const a = loaderArgs(argv('/some/loader.bundle', '--bundle=/s/bundles/abc.bundle', '--addons=/s/bundles/dd.addons', '/storage'))
t.is(a.bundle, '/s/bundles/abc.bundle')
t.is(a.addons, '/s/bundles/dd.addons')
})
test('storage stays LAST so every bundle, old or new, still finds it', (t) => {
// lib/node.js reads Bare.argv[Bare.argv.length - 1]. A bundle staged before the loader existed does
// the same, so it keeps working unchanged under the loader — which is what makes this safe to ship
// to devices holding older staged bundles.
const args = ['--bundle=/s/abc.bundle', '--addons=/s/dd.addons', '/data/user/0/app/files']
t.is(args[args.length - 1], '/data/user/0/app/files', 'storage is last')
t.is(loaderArgs(argv(...args)).bundle, '/s/abc.bundle', 'and the tags still parse')
})
test('no --addons= is null, not a crash', (t) => {
const a = loaderArgs(argv('--bundle=/s/abc.bundle', '/storage'))
t.is(a.bundle, '/s/abc.bundle')
t.is(a.addons, null)
})
test('no --bundle= is a THROW, never a guess', (t) => {
// There is no safe default. Guessing a path here would run something nobody chose, on the one code
// path that decides what executes on the device.
t.exception(() => loaderArgs(argv('/storage')), /no --bundle=/)
})
test('addons are preloaded BEFORE the bundle is loaded', (t) => {
const order = []
runLoader({
argv: argv('--bundle=/s/abc.bundle', '--addons=/s/dd.addons', '/storage'),
preload: ({ dir }) => { order.push('preload:' + dir); return 3 },
load: (p) => { order.push('load:' + p) }
})
t.alike(order, ['preload:/s/dd.addons', 'load:/s/abc.bundle'], 'preload first, then load')
})
test('a release with no addons still loads its bundle', (t) => {
// Every release published so far. The loader runs on EVERY boot, including the APK's own bundle.
const order = []
runLoader({
argv: argv('--bundle=/s/worklet.bundle', '/storage'),
preload: ({ dir }) => { order.push('preload:' + String(dir)); return 0 },
load: (p) => { order.push('load:' + p) }
})
t.alike(order, ['preload:null', 'load:/s/worklet.bundle'])
})
test('a bundle that throws is NOT caught — the watchdog must see it', (t) => {
// ⛔ NO FALLBACK HERE, DELIBERATELY. worklet.js writes pending.json before starting and clears it on
// ready; a marker that survives a boot condemns that bundle. If the loader quietly ran the APK's
// bundle instead, the node WOULD reach ready, pending.json would be cleared, and the broken bundle
// would never be condemned — it would be chosen again on every boot, for ever. Failing loudly is what
// makes the existing watchdog work.
t.exception(() => runLoader({
argv: argv('--bundle=/s/abc.bundle', '/storage'),
preload: () => 0,
load: () => { throw new Error('bundle is corrupt') }
}), /bundle is corrupt/)
})
// ── the in-bundle bootstrap ───────────────────────────────────────────────────────────────────────
//
// ⛔ THE APK LOADER IS NOT ENOUGH, AND THAT WAS THE WHOLE POINT. A preloader that only ships inside the
// APK means gaining the capability costs a sideload — exactly the cost this work exists to remove. It
// has to be possible to ship the preloader ITSELF as an ordinary JS release over the OTA that already
// works today.
//
// It is, because `new Addon(fileURL)` needs no filesystem module to dlopen a path, and a module inside
// a bundle knows where its bundle is: import.meta.url is `file:///…/<hash>.bundle/lib/bootstrap.js`.
// Measured on a real packed bundle, not assumed. So a staged bundle can find the addons staged beside
// it with no help from the shell — which is what makes this deliverable to the APK already on a device.
import { bundlePathFromUrl, addonsDirFor } from '../lib/host/loader-core.js'
test('a module inside a bundle can find its own bundle file', (t) => {
t.is(
bundlePathFromUrl('file:///data/user/0/app/files/bundles/abc123.bundle/lib/bootstrap.js'),
'/data/user/0/app/files/bundles/abc123.bundle'
)
t.is(
bundlePathFromUrl('file:///d/b/x.bundle/node_modules/deep/nested/mod.js'),
'/d/b/x.bundle',
'however deep the module sits'
)
})
test('running from SOURCE is not a bundle, and says so', (t) => {
// The test suite itself, and any laptop run. Null rather than a guess: preloading from a directory
// invented out of a source path would be a silent no-op at best.
t.is(bundlePathFromUrl('file:///repo/favela-surface-android/lib/bootstrap.js'), null)
t.is(bundlePathFromUrl(''), null)
t.is(bundlePathFromUrl(null), null)
})
test('a path with .bundle inside a DIRECTORY name is not mistaken for the bundle', (t) => {
t.is(bundlePathFromUrl('file:///x/my.bundles/lib/a.js'), null, 'my.bundles is not a bundle')
t.is(bundlePathFromUrl('file:///x/a.bundle.old/lib/a.js'), null, 'a.bundle.old is not a bundle')
})
test('the addon directory is derived from the bundle FILE, so nothing has to be told', (t) => {
t.is(
addonsDirFor('file:///data/user/0/app/files/bundles/abc123.bundle/lib/bootstrap.js'),
'/data/user/0/app/files/bundles/abc123.bundle.addons'
)
t.is(addonsDirFor('file:///repo/lib/bootstrap.js'), null, 'no bundle, no addons')
})