react-native-now
v0.1.0
Published
CLI for exporting and serving React Native guest apps as Gen UX static bundles
Readme
@genux/cli (genux)
Command-line tool for turning an Expo / React Native project into a Gen UX static bundle and serving it to the Gen UX iOS shell (gen-ux-shell). The shell loads guest apps at runtime from a manifest.json + Hermes bundle + assets; this CLI produces and hosts that layout.
Install
cd gen-ux-cli
bun install
bun run build # syncs the shell catalog, compiles to dist/
bun link # exposes `genux` globally
genux --helpWithout linking: node dist/index.js <command>.
Scripts:
| script | what it does |
| --- | --- |
| bun run build | sync-catalog + tsc + copy catalog.json into dist/ |
| bun run dev | tsc --watch |
| bun run sync-catalog | copy ../gen-ux-shell/catalog/packages.json -> src/catalog.json |
Requires Node 20+. The guest project must have its dependencies installed (bun install) so npx expo export can run inside it.
Commands
All projectDir arguments default to the current directory.
genux export [projectDir]
Runs npx expo export --platform ios --dump-assetmap --output-dir <out> in the project, then post-processes the output into the static bundle contract below. doctor runs first (see below).
| option | description |
| --- | --- |
| -o, --out <dir> | output directory (default <project>/dist-genux) |
| --dev | pass --dev to expo export (produces a .js bundle instead of .hbc) |
| --clear | clear the Metro cache before exporting |
| --name <name> / --slug <slug> / --bundle-id <id> | override app metadata |
| --force | continue even when doctor reports missing native packages |
| --skip-doctor | do not run doctor |
App metadata (name, slug, version, ios.bundleIdentifier, icon, sdkVersion, runtimeVersion) comes from npx expo config --json --type public, falling back to app.json, then package.json. Missing bundle identifiers become com.genux.guest.<slug>.
genux serve [projectDir]
Serves an exported directory over HTTP and prints the manifest URL, the deep link, a QR code encoding the deep link, and the simulator command. Runs until Ctrl-C.
| option | description |
| --- | --- |
| -o, --out <dir> | directory to serve (default <project>/dist-genux) |
| -p, --port <n> | port (default 8790) |
| -H, --host <ip> | host advertised in URLs (default: auto-detected LAN IPv4) |
| --sim | also run xcrun simctl openurl booted "<deep link>" |
Server behaviour: static files only, Cache-Control: no-store, Access-Control-Allow-Origin: *, every request logged, path traversal rejected. Content types: .hbc/.js -> application/javascript, .json -> application/json, images and fonts by extension.
Example output:
Gen UX static bundle server
serving: /path/to/app/dist-genux
listening: http://0.0.0.0:8790 (all interfaces)
Manifest URL: http://192.168.86.145:8790/manifest.json
Deep link: nativenow://open?url=http%3A%2F%2F192.168.86.145%3A8790%2Fmanifest.json
Simulator: xcrun simctl openurl booted 'nativenow://open?url=http%3A%2F%2F192.168.86.145%3A8790%2Fmanifest.json'
Or: genux open http://192.168.86.145:8790/manifest.json
Scan with the Gen UX shell (QR encodes the deep link):
[QR code]genux start [projectDir]
export followed by serve; accepts the union of both commands' options.
genux open <manifestUrl>
Opens nativenow://open?url=<encoded manifestUrl> in the shell on the booted iOS simulator via xcrun simctl openurl booted.
genux doctor [projectDir]
Compares the guest's package.json dependencies with the shell package catalog:
- error - a native package used by the guest is not compiled into the shell (the guest would crash when it loads that module).
export/startabort on this unless--force. - warning - version mismatch on a native package (shows the shell's version), or
expo/react-native/reactmismatch.
A guest dependency counts as native when node_modules/<pkg> contains an ios/ directory or a *.podspec (fallback: the catalog's native flag). Exit code 1 when errors are found.
genux catalog
Prints the shell catalog as a table (name, version, native).
Catalog resolution
bun run build copies gen-ux-shell/catalog/packages.json into the package (src/catalog.json -> dist/catalog.json). At runtime the loader tries, in order: dist/catalog.json, src/catalog.json, ../gen-ux-shell/catalog/packages.json (monorepo layout). If none exists it derives a minimal catalog from ../gen-ux-shell/package.json and prints a hint to run bun run catalog:generate in the shell.
Static bundle contract
genux export writes this layout (default <project>/dist-genux). Every URL in manifest.json is relative to the manifest's location, so the directory can be served from any base URL or copied anywhere.
manifest.json # Expo-updates-style manifest (below)
genux.json # app metadata = manifest.extra.genux + "id" + "manifest": "manifest.json"
assetmap.json # from expo export --dump-assetmap
metadata.json # from expo export
_expo/static/js/ios/index-<hash>.hbc # Hermes bytecode (or .js with --dev)
assets/<hash>.<ext> # expo export writes assets/<hash> with no extension;
# the CLI renames each using assetmap/metadata types
icon.png # optional, copied from the app's icon (app.json expo.icon)manifest.json:
{
"id": "<uuid v4>",
"createdAt": "2026-09-06T12:46:11.923Z",
"runtimeVersion": "1.0.0",
"launchAsset": {
"hash": "<sha256 base64url, no padding>",
"key": "bundle",
"contentType": "application/javascript",
"url": "_expo/static/js/ios/index-12d4ec0d97201f2eee1e7ae5a3174e84.hbc"
},
"assets": [
{
"hash": "<sha256 base64url, no padding>",
"key": "0747a1317bbe9c6fc340b889ef8ab3ae",
"contentType": "image/png",
"fileExtension": "png",
"url": "assets/0747a1317bbe9c6fc340b889ef8ab3ae.png"
}
],
"metadata": { "bundler": "metro", "version": "0", "platform": "ios", "assetsCount": "17" },
"extra": {
"genux": {
"name": "hello-samples",
"slug": "hello-samples",
"bundleIdentifier": "com.genux.guest.hellosamples",
"version": "1.0.0",
"sdkVersion": "57.0.0",
"icon": "icon.png",
"exportedAt": "2026-09-06T12:46:11.923Z"
}
}
}Rules the shell relies on:
- Hashes are SHA-256, base64url without padding (
crypto.createHash('sha256').digest('base64url')), matchingStaticBundle.checkAssetIntegrityin the shell. - All
metadatavalues are strings. runtimeVersioncomes from the app config (runtimeVersionstring, or the app version for theappVersionpolicy), default"1.0.0".- Asset
keyis the content hashexpo exportassigned; the served file isassets/<key>.<fileExtension>.
Deep link format
The shell registers the genux URL scheme and accepts:
nativenow://open?url=<url-encoded manifest URL>For example nativenow://open?url=http%3A%2F%2F192.168.86.145%3A8790%2Fmanifest.json. The QR code printed by serve/start encodes exactly this string. On a simulator: xcrun simctl openurl booted '<deep link>' (or genux open <manifestUrl>).
Typical workflow
cd my-expo-app && bun install
genux doctor # check native deps against the shell
genux start --sim # export + serve + open in the booted simulator
# or step by step
genux export
genux serve --port 8790
genux open http://<lan-ip>:8790/manifest.jsonNotes
- Expo CLI refuses to bundle
@react-navigation/*imports when it can resolveexpo-routerfrom any parentnode_modules(this happens for sample apps nested inside the shell repo). When the guest's ownpackage.jsondoes not depend onexpo-router, the CLI setsEXPO_ROUTER_DISABLE_RN_NAVIGATION_CHECK=1for the export; set the variable yourself to override. - The output directory is cleaned of previous contract files (
manifest.json,genux.json,_expo/,assets/, ...) before each export; other files in it are left alone.
