Internals & Contributing
How the package is built, how the pieces fit on disk, and how to make a change with confidence. If you want to contribute, start here.
For the day-to-day workflow commands (running the example app on a device,
linting), see CONTRIBUTING.md. This document is the
mental model behind those commands.
Repository layout
react-native-nitro-thumbnail/
βββ src/ # TypeScript β the only code you import
β βββ index.ts # public createThumbnail (native platforms)
β βββ index.web.ts # public createThumbnail (web, pure DOM)
β βββ types.ts # CreateThumbnailOptions, Thumbnail, error codes
β βββ errors.ts # ThumbnailError + toThumbnailError (code parsing)
β βββ native.ts # lazy HybridObject accessor
β βββ specs/
β βββ Thumbnail.nitro.ts # β the JSβnative contract (nitrogen input)
β
βββ ios/
β βββ HybridThumbnail.swift # AVFoundation implementation
β βββ ThumbnailEncoder.swift # pure sizing/encoding/eviction helpers
β βββ Tests/ # XCTest for the pure helpers (not shipped)
β
βββ android/src/
β βββ main/.../HybridThumbnail.kt # MediaMetadataRetriever implementation
β βββ main/.../ThumbnailEncoderKt.kt # pure sizing/encoding/eviction helpers
β βββ main/.../NitroThumbnailPackage.kt # autolinking package
β βββ test/.../*.kt # JUnit for the pure helpers (not shipped)
β
βββ __tests__/ # Jest (node + jsdom) for the TS layer
βββ example/ # runnable demo app (Yarn workspace)
βββ docs/ # β you are here
βββ nitrogen/ # GENERATED by nitrogen (gitignored, built on prepare)
βββ lib/ # GENERATED by bob (the published JS/types)
β
βββ Thumbnail spec & config
β βββ nitro.json # nitrogen config: module names, autolinking map
β βββ NitroThumbnail.podspec # iOS pod definition
β βββ package.json # bob build targets, scripts, depsThe rule of thumb: src/ is the contract, ios/ + android/ implement it,
nitrogen/ + lib/ are generated and never hand-edited.
The build pipeline
The package is built by react-native-builder-bob
(βbobβ), driven by yarn prepare (which npm/yarn run automatically on install
and publish). Bob runs three targets in order:
nitrogen(custom target) reads every*.nitro.tsspec and generates the nativeHybridThumbnailSpecprotocol (Swift), abstract class (Kotlin), and the C++ JSI bindings intonitrogen/generated/. You implement the generated spec β you never write the bridge by hand.moduletranspilessrc/to ESM JavaScript inlib/module/.typescriptemits.d.tsdeclarations tolib/typescript/.
package.json points consumers at the generated outputs:
"main": "./lib/module/index.js",
"types": "./lib/typescript/src/index.d.ts",When to re-run nitrogen
Run yarn nitrogen whenever you:
- change
src/specs/Thumbnail.nitro.ts(new method, new field, new type), or - build the project for the first time (the generated files are not committed).
The native code wonβt compile against an out-of-date generated spec β thatβs the point. The spec is the single source of truth, and codegen keeps both native sides honest.
Testing strategy
The library is tested at three levels, matching the three layers of the architecture. The principle throughout: extract the pure logic so it can be tested without a device.
| Layer | Whatβs tested | How | Where |
|---|---|---|---|
| TS public API | validation, defaults, error mapping, web pipeline | Jest (node + jsdom) | __tests__/ |
| iOS pure helpers | targetSize, encode, filesToEvict | XCTest / SwiftPM | ios/Tests/ |
| Android pure helpers | targetSize, mimeFor, encode, filesToEvict | JUnit | android/src/test/ |
| End-to-end | real decode on real media | example app | example/ |
yarn test # Jest β the TypeScript layer
yarn typecheck # tsc, no emit
yarn lint # eslintThe Jest suite covers createThumbnail validation/defaults, toThumbnailError
parsing (including the [CODE] prefix and the funcName: wrapper), and the full
web <video>/<canvas> path with jsdom fakes. The native helper tests cover the
sizing math and LRU eviction with hand-built inputs β no video files required.
Why split pure helpers out (ThumbnailEncoder.swift,
ThumbnailEncoderKt.kt, fitSize)? Because the interesting logic β aspect-fit
math and eviction ordering β has nothing to do with AVFoundation or
MediaMetadataRetriever. Pulling it into side-effect-free functions makes it
fast and deterministic to test, and keeps the platform classes thin.
Making a change: worked examples
Add a new option (e.g. backgroundColor)
- Add it to
CreateThumbnailOptionsinsrc/types.tsand toNativeThumbnailOptionsinsrc/specs/Thumbnail.nitro.ts. - Normalize/default it in
src/index.ts(andindex.web.ts). yarn nitrogento regenerate the native specs.- Implement it in
HybridThumbnail.swift,HybridThumbnail.kt, and the web path. - Add Jest tests for the TS normalization; add native tests if it touches the pure helpers.
- Verify in the example app on a simulator/emulator.
Fix the sizing math
Edit only the pure helpers (targetSize / fitSize) and their unit tests β no
device needed. The platform classes call into them unchanged.
Add an error code
- Add it to the
ThumbnailErrorCodeunion andTHUMBNAIL_ERROR_CODESarray insrc/types.ts(both β the array is the runtime allow-listtoThumbnailErrorchecks against). - Throw it from native via the
err("NEW_CODE", β¦)helper, or from the web/TS path vianew ThumbnailError('NEW_CODE', β¦). - Add a Jest test asserting it round-trips.
- Document it in the Error Handling guide and the API reference.
Coding conventions
- Validate in TypeScript, act dumbly in native. Donβt push input validation into Swift/Kotlin β the native side should receive a complete, normalized struct. (See architecture β design principles.)
- Keep pure logic pure. Sizing/eviction/encoding decisions go in the
*Encoder*helpers, free of I/O, so they stay unit-testable. - One code path per error. Every failure maps to exactly one
ThumbnailErrorCode; encode it at the throw site with theerr()helper. - Match the surrounding style. Prettier + ESLint configs are in the repo; run
yarn lint --fixbefore committing.
Contributing checklist
-
yarnto install (uses Yarn 4 workspaces β not npm) -
yarn nitrogenif you touched a*.nitro.tsspec -
yarn test,yarn typecheck,yarn lintall green - New behavior has tests (pure helpers where possible)
- Verified in the example app if native code changed
- Docs updated (API reference / relevant guide)
- Small, focused PR β discuss API/implementation changes in an issue first
Friendly reminder: the code of conduct applies to all interactions. New contributors are genuinely welcome β a good first PR is fixing or extending a doc you found confusing while reading these.