Recipe — frida-compile integration¶
The build pipeline that turns a TypeScript hook source plus a JSON
map into a single self-contained Frida bundle. This page covers the
V1 workflow — including the manual marker-block wrapping step
required until the frida-compile plugin lands.
The shape of a hook bundle¶
flowchart TB
A[hook.ts] -->|frida-compile| B[hook.compiled.js]
M[map.json] -.->|imported by hook.ts
inlined by frida-compile| B
C[marker emit step] -->|prepend| B
M -.->|read by emit step| C
B -->|frida -l| F[Frida runtime]
Two artifacts: your hook source (TypeScript or modern JS) and the
JSON map. frida-compile resolves the import map from
'./x.json' and inlines the value. The compiled bundle is a single
.js file that Frida loads unchanged.
Getting the map: pull → bundle¶
The JSON map normally comes from the community
rosetta-maps repo via
rosetta pull, which runs once at build time on your
machine:
npx rosetta pull com.example.app@30405
# → maps/com.example.app/30405.json
npx frida-compile hook.ts -o hook.bundle.js
pull validates the fetched map against the schema_version: 3 schema
before writing it. There is no byte-hash sidecar to verify: a map is pure
data (a lookup table the resolver only reads), so a tampered map is at worst
a correctness bug, never code execution — and transport integrity already
comes for free from the channel, since maps are pulled over git-over-HTTPS
from a content-addressed (SHA-named) store. See
rosetta pull
and the rosetta-maps docs/reference/integrity.md for the full safety model.
The simplest pipeline¶
This produces a working bundle. Run it:
The map is embedded inside the compiled bundle as a JS object literal
(courtesy of frida-compile's --inline default for JSON imports).
The hook resolves real names against it at runtime.
You do not need a marker block to run the hook. The marker block is a separate concern — see below.
Why the marker block matters¶
Without a marker block, the embedded map is opaque from outside the script. Three CLI commands depend on the marker block to work:
rosetta inspect <bundle.js>— what does this bundle target?rosetta extract <bundle.js> -o out.json— pull the map out.rosetta patch <bundle.js> --map new.json— swap the map without recompiling.
If you don't need those operations, skip the marker-block step. Plenty of users will be fine without it — the hook runs either way.
If you do want them — and especially if your CI compiles the bundle once and patches in per-environment maps — you need the marker block.
Manual marker wrapping¶
In V1 there's no frida-compile plugin yet. The marker-block step
runs as a separate post-compile pass. The recipe:
Step 1 — compile normally¶
Step 2 — emit the marker block¶
node --input-type=module -e "
import { emitMarkerBlock, loadMap } from 'rosetta-frida';
const map = await loadMap('maps/com.example.app/30405.json');
process.stdout.write(emitMarkerBlock(map) + '\n');
" > marker.js
The result is a 4-line JS snippet that looks like:
/*! -----BEGIN ROSETTA MAP----- */
/*! app: com.example.app | version: 3.4.5 | schema: 5 | classes: 15 */
const __rosetta_map = { /* ... pretty-printed JSON ... */ };
/*! -----END ROSETTA MAP----- */
Step 3 — concatenate¶
The marker block goes at the very top. The CLI tools find it by regex, so position-in-file doesn't matter — but top of file is conventional.
Step 4 — verify¶
npx rosetta inspect hook.bundle.js
# [email protected], schema_version 5, 15 classes
The marker is there. extract and patch work too.
Wrapping it in a package.json script¶
{
"scripts": {
"build:hook": "frida-compile hook.ts -o hook.compiled.js && npm run embed:marker && cat marker.js hook.compiled.js > hook.bundle.js && rm marker.js hook.compiled.js",
"embed:marker": "node --input-type=module -e \"import('rosetta-frida').then(async ({ emitMarkerBlock, loadMap }) => { const m = await loadMap('maps/com.example.app/30405.json'); require('fs').writeFileSync('marker.js', emitMarkerBlock(m)); });\""
}
}
Then npm run build:hook does the whole pipeline.
Wrapping it in a Node script¶
For non-trivial builds, a small Node script is cleaner than chained
bash:
// scripts/build-bundle.ts
import { execSync } from 'node:child_process';
import { emitMarkerBlock, loadMap } from 'rosetta-frida';
import { readFileSync, writeFileSync, unlinkSync } from 'node:fs';
const MAP_PATH = process.env.ROSETTA_MAP ?? 'maps/com.example.app/30405.json';
const HOOK_SRC = 'hook.ts';
const OUT = 'hook.bundle.js';
execSync(`npx frida-compile ${HOOK_SRC} -o hook.compiled.js`, { stdio: 'inherit' });
const compiled = readFileSync('hook.compiled.js', 'utf8');
const map = await loadMap(MAP_PATH);
const marker = emitMarkerBlock(map);
writeFileSync(OUT, marker + '\n' + compiled, 'utf8');
unlinkSync('hook.compiled.js');
console.log(`built ${OUT}; ${marker.split('\n').length} marker lines, ${compiled.length} bytes compiled`);
Registry-bundle variant¶
For a multi-version bundle, swap emitMarkerBlock(map) for
emitMarkerRegistry(registry):
import { emitMarkerRegistry, loadMap } from 'rosetta-frida';
const registry = {
'3.4.5': await loadMap('maps/com.example.app/30405.json'),
'3.4.6': await loadMap('maps/com.example.app/3.4.6.json'),
'3.5.0': await loadMap('maps/com.example.app/3.5.0.json'),
};
const marker = emitMarkerRegistry(registry);
See Multi-version bundles recipe for the runtime semantics.
V1.5 — the frida-compile plugin¶
The plan: a small plugin that hooks frida-compile's JSON-import
pipeline, intercepts import map from './x.json' calls where the
JSON validates as a RosettaMap, and emits the marker block
automatically.
The user-facing surface will be:
Or, more likely, an entry in a frida-compile config file:
Once that lands, the manual two-step recipe goes away — every bundle gets a marker block automatically.
Until then, the manual step works fine.
Building with esbuild instead¶
If you've replaced frida-compile with esbuild (e.g. for faster
incremental builds or for a more standard JS toolchain), the same
manual marker-wrapping step works — esbuild emits the same kind of
single-file bundle, and the marker emit + concat pass operates on
the bytes regardless of what produced them.
npx esbuild hook.ts --bundle --format=iife --outfile=hook.compiled.js
node --import tsx scripts/build-bundle.ts
The IIFE format matters here — Frida loads a Java.perform-shaped
script, not a module. frida-compile handles that for you; with
esbuild you need to opt in via --format=iife.
Watch mode¶
For dev-loop iteration, run frida-compile in watch mode and
re-prepend the marker on save:
Where watch-bundle.ts uses fs.watch on hook.compiled.js and
re-runs the marker emit + concat. Or use chokidar
for a more robust watcher.
V1.5's frida-compile plugin will fold this into the regular watch
mode.
Caveats¶
- Minifier preservation. If you minify the compiled bundle
before prepending the marker, the marker stays readable (it's
outside the minified region). If you minify after, make sure
your minifier preserves
/*! ... */comments (terser does by default; esbuild does with--legal-comments=inline). - Source maps. The marker block lives at the top of the file, which shifts all source-mapped line numbers by ~4 lines. Frida's error reporting normally tracks the runtime address, not the source line, so this rarely matters in practice.
- Two bundles in one file. Don't concatenate two compiled bundles each carrying a marker block — the extraction regex matches the first marker non-greedily and ignores the second.