This is an npm-workspace monorepo:
packages/
vto-core-native/ private — shared native code + assets (single source of truth)
react-native-nitro-vto/ published — new-arch (Fabric) wrapper
react-native-vto/ published — old-arch (Paper) wrapper
examples/
example-new-arch/ new-arch Expo demo app
example-old-arch/ old-arch Expo demo app
Native sources live in vto-core-native/. The two wrapper packages each hold only their arch-specific bridge code (Nitro HybridView vs. RCTViewManager). The shared core is copied into each wrapper by scripts/bundle.ts — those copied paths are gitignored; do not edit them.
git clone git@github.com:alan-eu/react-native-nitro-vto.git
cd react-native-nitro-vto
npm installnpm install runs a postinstall hook that bundles core into both wrappers, so the repo is immediately buildable.
Download Filament 1.71.4 binaries from filament releases, then at the repo root copy .env.example to .env and set:
MATC_PATH=/path/to/filament/bin/matc
CMGEN_PATH=/path/to/filament/bin/cmgen
Recompile when you touch a .mat or .hdr:
npm run matc --workspace=@alaneu/vto-core-native # .mat → .filamat
npm run cmgen --workspace=@alaneu/vto-core-native # .hdr → .ktx + _sh.txtNew-arch (Nitro):
cd examples/example-new-arch
npm run ios # or npm run androidOld-arch (classic):
cd examples/example-old-arch
npm run ios # or npm run androidBoth apps install on the same device side-by-side (distinct bundle IDs).
Run the watcher once in a terminal:
npm run watch:coreEdits under packages/vto-core-native/ (Swift / Kotlin / Obj-C++ / headers / compiled .filamat / .ktx / src/types.ts / src/expo.ts) re-bundle into both wrappers within ~150 ms. You still need to rebuild on the consumer side — Gradle and Xcode don't notice file replacements on their own. On iOS, a Metro reload is not enough for native changes; reinstall via npm run ios.
- All shared native code belongs in
packages/vto-core-native/. Never edit.kt/.swift/.mm/.hinside a wrapper's bundled paths (packages/react-native-*-vto/ios/*for the core renderer files,packages/react-native-*-vto/android/src/main/java/eu/alan/vto/core/, or theandroid/src/main/assets/dirs) — those are git-ignored and overwritten by every bundle run. - Each wrapper keeps only its arch-specific bridge code:
react-native-nitro-vto:HybridNitroVtoView.{kt,swift}, theNitroVto.humbrella,nitrogen/,src/specs/.react-native-vto:VtoViewManager.{kt,mm,h},VtoPackage.kt,VtoBridgeView.swift,src/VtoView.tsx.
The two wrappers expose the same props / methods / callbacks. Any surface change (new prop, renamed method, new callback signature) must land in all of:
packages/vto-core-native/src/types.ts— shared TS typedefs (bundled into each wrapper assrc/types.ts)packages/react-native-nitro-vto/src/specs/NitroVtoView.nitro.ts— Nitro spec (re-runnpm run specsfrom the Nitro package)packages/react-native-vto/src/VtoView.tsx— old-arch wrapper (requireNativeComponent +useImperativeHandle)- Both native view managers:
HybridNitroVtoView.{kt,swift}andVtoViewManager.kt/VtoBridgeView.swift/VtoViewManager.mm examples/example-new-arch/app/index.tsxandexamples/example-old-arch/app/index.tsx— exercise the new surface- Both READMEs (
packages/react-native-nitro-vto/README.mdandpackages/react-native-vto/README.md)
- iOS: core renderers are Objective-C++ (
.mm/.h) using Filament's C++ API directly; theVtoViewfacade is Swift. Keep the public Swift APIpublicso it reaches each wrapper's auto-generated<Module>-Swift.h— that's what the Nitro HybridView and the old-arch RCTViewManager both import. - Android: core is Kotlin, package
eu.alan.vto.core. Do not put anything incom.margelo.nitro.nitrovto— that namespace is reserved for Nitro-specific bridge code. - Filament: version is pinned at
1.71.4in both podspecs and bothandroid/build.gradlefiles; don't bump one without the other. - Assets: source
.mat/.hdrlive inpackages/vto-core-native/assets/; compiled.filamat/.ktx/.txtare checked in underpackages/vto-core-native/android/src/main/assets/andpackages/vto-core-native/ios/assets/. Always recompile and commit both source and compiled forms together. - Resource bundle lookup on iOS:
LoaderUtils.loadAssetNamed:tries bothNitroVtoAssets.bundleandReactNativeVtoAssets.bundle(each wrapper podspec names itsresource_bundlesdifferently). If you add a third wrapper, extend that list.
- Rebuild both example apps on a physical device (ARKit face tracking needs a TrueDepth camera; simulator doesn't count).
- Confirm glasses render, face occlusion works, and model switching works on both arches.
npm pack --dry-run --workspace=packages/react-native-nitro-vtoand--workspace=packages/react-native-vto: every bundled file should appear in the listing. If a core file is missing, the bundle step inprepublishOnlyisn't picking it up.
The two wrappers are released in lockstep at the same version. vto-core-native is private and never published — its code ships embedded inside each wrapper's tarball via prepublishOnly.
- Both example apps build + run on a physical device for both platforms.
- Working tree is clean (or only expected changes).
- You are logged into npm:
npm whoamireturns the account with publish rights to the@alaneuscope. - Dry-run both tarballs and verify bundled native sources are present:
cd packages/react-native-nitro-vto && npm pack --dry-run cd ../react-native-vto && npm pack --dry-run
From the repo root:
npm run releaseThis runs, in order:
release-itinsidepackages/react-native-nitro-vto— builds viabob(theprepackhook re-runsbundlefirst), publishes to npm.release-itinsidepackages/react-native-vto— same flow for the old-arch wrapper.release-itat the root — bumps the version across every relevantpackage.json(root, both wrappers,vto-core-native, both examples), creates a signed git tagvX.Y.Z, and opens a GitHub release with the conventional-commits changelog.
Each step prompts for the version bump type (patch / minor / major). Use the same answer for all three so the wrappers and the git tag stay aligned.
git push --follow-tags origin main- Verify both packages on npm:
npm view @alaneu/react-native-nitro-vto versionandnpm view @alaneu/react-native-vto version. - Smoke-test a fresh install in a scratch RN project for at least one arch (install the wrapper,
pod installon iOS, run on a device — catches packaging regressions thatnpm pack --dry-runmisses).
If only one wrapper needs a patch and you want to skip the lockstep, run npm run release from that wrapper's directory directly. Remember to manually bump the other package.json files afterward if you want versions to stay aligned — the root release-it is what normally handles that.