rn-typed-assets
v1.4.0
Published
React Native typed asset registry — generates type-safe Assets/Svgs/Lotties constants, audits unused files, and rewrites stale imports automatically
Downloads
654
Maintainers
Readme
rn-typed-assets
Get rid of all string-based asset references in React Native — forever.
rn-typed-assets scans your asset directories and generates a typed TypeScript registry, so that every image, SVG, and Lottie animation is accessed through a named constant instead of a brittle require('../../../assets/icon.png') path. If the file doesn't exist, the import fails at generation time — not at runtime.
// Before
<Image source={require('../../../assets/toast/info.png')} />
<LottieView source={require('../../utils/loading.json')} />
// After
import { Assets, Lotties } from '../generated/assets.gen';
<Image source={Assets.toast.info} />
<LottieView source={Lotties.loading} />Inspiration
This tool is directly inspired by two codegen tools from other ecosystems:
SwiftGen — the canonical Swift code generator for Xcode resources. SwiftGen pioneered the pattern of scanning asset catalogs and emitting fully type-safe Swift enums, eliminating string-based
UIImage(named:)calls entirely.FlutterGen — the Flutter equivalent. FlutterGen reads
pubspec.yamlassets and emits Dart classes with typed getters, soAssets.images.profile.image()replacesImage.asset('assets/images/profile.jpg').
rn-typed-assets brings the same discipline to React Native: a manifest-driven, deterministic generator that makes unused or mistyped asset references a compile-time (or generation-time) problem rather than a runtime crash.
Features
- Zero-config for standard RN projects — works out of the box with the default
src/assetslayout - Three built-in asset types —
image(PNG/JPG/WebP),svg,lottie(JSON) - Deterministic, sorted output — generated files are stable across runs and friendly to code review
- Collision detection — files that normalize to the same key (e.g.
harini-cry.pngandharini_cry.png) are caught at generation time with a clear error - Manifest-backed audit — find and optionally delete unused assets that are no longer referenced anywhere in source
- Automatic source rewriting —
generate --inplaceandorganizerewrite everyrequire()call, ES moduleimportstatement, and stale dotted reference in your source files to match the regenerated manifest - Content-hash diffing — each manifest entry now carries a SHA-1 hash of the file's bytes, enabling the codemod to track files that move or are renamed without content changes
- Asset organization —
organizemigrates flat or legacy asset directories into canonical subdirectories (images/,svg/,lottie/) in one command - Configurable — override paths, export names, TypeScript type imports, or add entirely new asset types via
rn-typed-assets.config.js - Programmatic API — every function is exported; integrate the generator into your own scripts or build tools
- Lightweight — zero runtime dependencies;
typescriptis a peer dependency used only by the audit command
How It Works
src/assets/ commands/
toast/info.png ──┐ generate
toast/warning.png ──┤ scan ──► assets.gen.ts (typed require() registry)
lottie/loading.json─┤ assets.manifest.json (path ↔ key + contentHash)
svg/logo.svg ───────┘
generate --inplace
src/**/*.{ts,tsx,js,jsx} ◄────── rewrite require() → Assets.*
migrate import X → Assets.*
update stale dotted references
organize src/assets
svg/logo.svg ───────► svg/logo.svg (move to canonical dir)
→ regenerate + rewrite sources
audit
src/**/*.{ts,tsx,js,jsx} ──────► compare usages in source
vs. entries in manifest
→ report (or delete) unused assetsGeneration pipeline
- Scan — For each enabled asset type, recursively list files under the configured
rootDir. - Normalize — Convert each filename to a stable camelCase key (
harini-cry.png→hariniCry,1.png→n1). - Build registry tree — Assemble a nested object tree from path segments. Detect and resolve branch/leaf collisions automatically (e.g. a file named
point.pngalongside apoint/directory becomespointAsset). - Emit — Write
assets.gen.ts(a typedas constobject) andassets.manifest.json(a stable index of every key ↔ file mapping).
Audit pipeline
- Parse source files — Use the TypeScript Compiler API to walk the AST of every
.ts/.tsx/.js/.jsxfile undersourceRoots. - Collect usages — Detect
Assets.*,Lotties.*, andSvgs.*property-access chains. Also collect legacyrequire('../assets/...')calls for files still in migration. - Compare against manifest — Any key present in the manifest but unreferenced in source is reported as unused. Any key used in source but absent from the manifest is reported as unknown.
- Optionally fix — With
--fix, delete the unused files and regenerate the manifest.
Installation
npm install --save-dev rn-typed-assetstypescript must be available in the project (it is a peerDependency). Most React Native projects already have it as a dev dependency.
Quick Start
1. Add scripts to package.json
{
"scripts": {
"assets:generate": "rn-typed-assets generate",
"assets:generate:inplace": "rn-typed-assets generate --inplace",
"assets:organize": "rn-typed-assets organize src/assets",
"assets:audit": "rn-typed-assets audit",
"assets:audit:fix": "rn-typed-assets audit --fix"
}
}2. Run the generator
npm run assets:generateThis writes two files to src/generated/:
assets.gen.ts— the typed registry you import in your componentsassets.manifest.json— the index used by the audit command (commit this file)
3. Import the registry
import { Assets, Lotties, Svgs } from './generated/assets.gen';
// Images
<Image source={Assets.toast.info} />
<Image source={Assets.coupang.hariniCry} />
// Lottie
<LottieView source={Lotties.loading} autoPlay loop />
// SVG (with react-native-svg)
<SvgUri source={Svgs.logo} />4. Audit for unused assets
npm run assets:audit # report unused entries
npm run assets:audit -- --fix # delete unused files and regenerateCLI Reference
rn-typed-assets <command> [options]generate
Scan asset directories and emit assets.gen.ts + assets.manifest.json.
| Flag | Description | Default |
| ----------------- | ------------------------------------------------------------ | ----------------------------- |
| --types <types> | Comma-separated list of asset types to include | image,svg,lottie |
| --inplace | Rewrite source files to replace stale references after regen | false |
| --root <path> | Project root directory | cwd |
| --config <path> | Path to config file | ./rn-typed-assets.config.js |
rn-typed-assets generate
rn-typed-assets generate --inplace # rewrite sources after regen
rn-typed-assets generate --types=image,lottie
rn-typed-assets generate --root=/path/to/projectWhen --inplace is set, the tool loads the previous manifest before writing the new one, diffs them by content hash, and rewrites every source file that contains a stale require() path, an ES module import statement for an asset file, or a renamed dotted symbol reference.
ES module imports are fully migrated: the import declaration is removed, all usages of the imported binding are replaced with the generated symbol, and the import { … } from '…/assets.gen' statement is added or extended automatically.
// Before
import logo from '../assets/logo.png';
const el = <Image source={logo} />;
// After (generated by --inplace)
import { Assets } from '../generated/assets.gen';
const el = <Image source={Assets.logo} />;organize
Move asset files from legacy or flat directories into canonical subdirectories, then regenerate and rewrite sources.
rn-typed-assets organize <assetsDir>| Argument | Description |
| ------------- | ---------------------------------------------------- |
| <assetsDir> | Path to the asset root to organize (relative to cwd) |
| Flag | Description | Default |
| ----------------- | ---------------------- | ----------------------------- |
| --types <types> | Asset types to move | image,svg,lottie |
| --root <path> | Project root directory | cwd |
| --config <path> | Path to config file | ./rn-typed-assets.config.js |
rn-typed-assets organize src/assetsCanonical subdirectory layout after organize:
src/assets/
images/ ← PNG/JPG/WebP
svg/ ← SVG
lottie/ ← Lottie JSONFiles already in the canonical location are left untouched. Source files with require() calls pointing to the old paths are rewritten automatically.
audit
Compare manifest against actual source-file usages.
| Flag | Description | Default |
| ----------------- | ----------------------------------------------------- | ----------------------------- |
| --types <types> | Asset types to include in the audit | image,svg,lottie |
| --fix | Delete unused asset files and regenerate the manifest | false |
| --root <path> | Project root directory | cwd |
| --config <path> | Path to config file | ./rn-typed-assets.config.js |
rn-typed-assets audit
rn-typed-assets audit --fix
rn-typed-assets audit --types=imageConfiguration
Create rn-typed-assets.config.js in your project root to override any default. The file is optional — omitting it is equivalent to accepting all defaults.
// rn-typed-assets.config.js
module.exports = {
// Where to write assets.gen.ts and assets.manifest.json
// Default: 'src/generated'
outputDir: 'src/generated',
// Directories and entry files scanned by the audit command
// Default: ['src', 'App.tsx', 'index.js']
sourceRoots: ['src', 'App.tsx', 'index.js'],
// Per-type configuration (all fields are optional overrides)
types: {
image: {
rootDir: 'src/assets', // scan root
extensions: ['.png', '.jpg', '.jpeg', '.webp'], // included extensions
exportName: 'Assets', // export const Assets = ...
typeImport: {
typeName: 'ImageRequireSource', // TypeScript type name
from: 'react-native', // import source
},
},
svg: {
rootDir: 'src/assets/svg',
extensions: ['.svg'],
exportName: 'Svgs',
inlineType: 'unknown', // emits: export type SvgsAssetSource = unknown
},
lottie: {
rootDir: 'src/assets/lottie',
extensions: ['.json'],
exportName: 'Lotties',
typeImport: {
typeName: 'AnimationObject',
from: 'lottie-react-native',
},
},
},
};Adding a custom asset type
Any type not in the defaults can be added under types. The audit command discovers it automatically via the exportName → type reverse map.
module.exports = {
types: {
font: {
rootDir: 'src/assets/fonts',
extensions: ['.ttf', '.otf'],
exportName: 'Fonts',
inlineType: 'string', // emits: export type FontsAssetSource = string
},
},
};Generated Output
Given this asset tree:
src/assets/
toast/
info.png
warning.png
coupang/
harini-cry.png
lottie/
loading.json
svg/
logo.svgRunning rn-typed-assets generate produces:
// src/generated/assets.gen.ts
/* eslint-disable @typescript-eslint/no-require-imports -- require() is intentional for React Native static asset bundling */
// Auto-generated by rn-typed-assets. Do not edit manually.
import type { ImageRequireSource } from 'react-native';
import type { AnimationObject } from 'lottie-react-native';
export type SvgsAssetSource = unknown;
export const Assets = {
coupang: {
hariniCry:
require('../assets/coupang/harini-cry.png') as ImageRequireSource,
},
toast: {
info: require('../assets/toast/info.png') as ImageRequireSource,
warning: require('../assets/toast/warning.png') as ImageRequireSource,
},
} as const;
export const Lotties = {
loading: require('../assets/lottie/loading.json') as AnimationObject,
} as const;
export const Svgs = {
logo: require('../assets/svg/logo.svg') as SvgsAssetSource,
} as const;Key normalization rules
| Filename | Generated key |
| ---------------------------------- | ------------------------------------ |
| harini-cry.png | hariniCry |
| camera_guide.png | cameraGuide |
| Info-Filled.png | infoFilled |
| 1.png | n1 (numeric prefix → n) |
| point.png alongside point/ dir | pointAsset (leaf/branch collision) |
Programmatic API
All functions are available for use in custom build scripts:
const {
loadConfig,
collectAssetEntries,
generateAssetsModule,
generateAssetsManifest,
writeGeneratedAssets,
auditAssetUsage,
collectGeneratedAssetUsages,
// Codemod
collectAssetImportBindings,
diffAssetManifests,
rewriteTypedAssetSource,
} = require('rn-typed-assets');
const config = loadConfig(projectRoot);
// Generate
const { entries, moduleContent, manifest } = writeGeneratedAssets({
projectRoot,
types: ['image', 'lottie'],
config,
});
// Audit (requires typescript peer dep)
const usages = collectGeneratedAssetUsages(sourceCode, filePath, config);
const report = auditAssetUsage({
manifest,
generatedUsages: usages,
requirePaths: [],
config,
});
// Codemod — rewrite a source file after a manifest change
const { renamedSymbols, currentSymbolsByFilePath } = diffAssetManifests({
previousManifest,
nextManifest,
config,
});
const { changed, code } = rewriteTypedAssetSource({
code: fs.readFileSync(filePath, 'utf8'),
filePath, // relative to projectRoot
previousManifest,
nextManifest,
projectRoot,
config,
});
if (changed) {
fs.writeFileSync(filePath, code);
}See src/index.js for the full list of exported functions.
CI Integration
Add generation and audit to your CI pipeline to catch drift between the manifest and the filesystem:
# .github/workflows/ci.yml
- name: Verify asset manifest is up to date
run: |
npm run assets:generate
git diff --exit-code src/generated/Or use a pre-commit hook via Husky:
# .husky/pre-commit
npm run assets:generate
git add src/generated/assets.gen.ts src/generated/assets.manifest.jsonFuture Plans
Watch mode
Automatically re-run generate when files under any rootDir are added, removed, or renamed. Useful during active development without having to manually invoke the CLI.
rn-typed-assets generate --watch # plannedMetro plugin integration
A Metro resolver plugin that runs generation as part of the bundler's startup, ensuring assets.gen.ts is always in sync before the first bundle is produced — eliminating the need for a separate pre-build step.
Husky / lint-staged recipe
A first-class init command that scaffolds a Husky pre-commit hook and lint-staged configuration so that generation runs automatically whenever asset files are staged.
rn-typed-assets init --hooks # plannedFont and audio asset types
Extend the default type set to cover fonts (.ttf, .otf) and audio files (.mp3, .wav, .aac) with appropriate TypeScript types (FontSource, NodeRequire). These types already work today via the custom-type config API; the plan is to promote them to built-in defaults.
Strict audit mode for CI
A --strict flag for the audit command that exits non-zero if the manifest is stale, if any unknown keys are in use, or if any unused entries remain — making the full audit a one-command CI gate.
rn-typed-assets audit --strict # plannedMonorepo support
Allow multiple packages in a workspace to share a single invocation, with per-package output directories and per-package manifests, controlled by a root-level config or workspace glob.
Output template customization
Inspired by SwiftGen's Stencil templates, allow users to supply a custom EJS or Handlebars template for assets.gen.ts, enabling alternative output styles — for example, emitting functions instead of require() literals, or targeting a custom asset loader.
License
MIT
