agentsclimarketplace

Codegen

Skill Shankulkarni/RN-Lib-Claude/plugins/rn-lib-claude/skills/codegen

Ship production-grade RN libraries faster — scaffold, audit, and publish with Claude Code.

Install
npx -y skills add Shankulkarni/RN-Lib-Claude --skill codegen

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Use when implementing native modules (TurboModules) or native views (Fabric) for a React Native library — Codegen TypeScript specs, threading model, iOS/Android registration, view flattening, New Architecture only.

SKILL.md

10.4 KB, as published. Nobody here has run it

Codegen — TurboModules + Fabric

New Architecture only. Codegen generates native interfaces from TypeScript specs.


Threading Model — Know This Before Writing Native Code

Sync vs Async Methods

Sync method  → blocks JS thread for its entire duration
               MUST complete in <16ms (one frame budget)
               Use only for simple reads/computations

Async method → runs on native module background thread
               Promise-based; safe for any duration
               Use for I/O, heavy computation, network
// TypeScript spec
export interface Spec extends TurboModule {
  // Sync — fast only, <16ms
  getDeviceId(): string
  multiply(a: number, b: number): number

  // Async — safe for heavy work
  processImage(uri: string): Promise<{ uri: string; size: number }>
  readFile(path: string): Promise<string>
}

iOS Threading

// Sync — runs on JS thread, keep fast
- (double)multiply:(double)a b:(double)b {
  return a * b; // pure computation, instant
}

// Async — dispatch to background
- (void)processImage:(NSString *)uri resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // heavy work here
    NSString *result = [self doHeavyWork:uri];
    resolve(result);
  });
}

// UI updates — must dispatch to main thread
- (void)updateUI:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
  dispatch_async(dispatch_get_main_queue(), ^{
    // UIKit operations here
    resolve(nil);
  });
}

Android Threading

// Sync — JS thread, keep fast
override fun multiply(a: Double, b: Double): Double = a * b

// Async — use coroutines on background dispatcher
override fun processImage(uri: String, promise: Promise) {
  CoroutineScope(Dispatchers.Default + Job()).launch {
    try {
      val result = doHeavyWork(uri)
      promise.resolve(result)
    } catch (e: Exception) {
      promise.reject("ERR_PROCESSING", e.message, e)
    }
  }
}

// Always clean up coroutines on module invalidation
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())

override fun invalidate() {
  super.invalidate()
  scope.cancel() // ← mandatory, prevents leaks
}

TurboModule — Full Setup

1. TypeScript Spec (src/NativeMyLib.ts)

import type { TurboModule } from 'react-native'
import { TurboModuleRegistry } from 'react-native'
import type { Double, Int32 } from 'react-native/Libraries/Types/CodegenTypes'

export interface Spec extends TurboModule {
  // Sync — primitives only, fast ops
  multiply(a: Double, b: Double): Double
  getVersion(): string

  // Async — I/O, heavy computation
  processImage(uri: string, options: {
    width: Int32
    height: Int32
    quality?: Double
  }): Promise<{ uri: string; size: Int32 }>

  // Events (if module emits)
  addListener(eventName: string): void
  removeListeners(count: Int32): void
}

export default TurboModuleRegistry.getEnforcing<Spec>('RNMyLib')

2. package.json Codegen Config

"codegenConfig": {
  "name": "RNMyLibSpec",
  "type": "modules",
  "jsSrcsDir": "src",
  "android": { "javaPackageName": "com.mylib" }
}

3. iOS (ios/RNMyLib.mm)

#import "RNMyLib.h"

@implementation RNMyLib

RCT_EXPORT_MODULE()

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
  return std::make_shared<facebook::react::NativeRNMyLibSpecJSI>(params);
}

- (double)multiply:(double)a b:(double)b { return a * b; }

- (void)processImage:(NSString *)uri options:(JS::NativeRNMyLib::SpecProcessImageOptions &)options
             resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // heavy work
    resolve(@{ @"uri": uri, @"size": @(1024) });
  });
}

@end

4. Android (android/src/main/java/com/mylib/MyLibModule.kt)

package com.mylib

import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReadableMap
import com.mylib.NativeRNMyLibSpec
import kotlinx.coroutines.*

class MyLibModule(reactContext: ReactApplicationContext) : NativeRNMyLibSpec(reactContext) {
  private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())

  override fun getName() = NAME

  override fun multiply(a: Double, b: Double): Double = a * b

  override fun processImage(uri: String, options: ReadableMap, promise: Promise) {
    scope.launch {
      try {
        promise.resolve(uri)
      } catch (e: Exception) {
        promise.reject("ERR_PROCESS", e.message, e)
      }
    }
  }

  override fun invalidate() {
    super.invalidate()
    scope.cancel()
  }

  companion object { const val NAME = "RNMyLib" }
}

Fabric Component — Full Setup

1. TypeScript Spec (src/MyLibNativeComponent.ts)

import type { HostComponent, ViewProps } from 'react-native'
import type { DirectEventHandler, Double, Int32 } from 'react-native/Libraries/Types/CodegenTypes'
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'

type OnLoadEvent = { width: Double; height: Double; duration: Double }

type NativeProps = ViewProps & {
  uri: string
  resizeMode?: 'cover' | 'contain' | 'stretch'
  autoPlay?: boolean
  muted?: boolean
  onLoad?: DirectEventHandler<OnLoadEvent>
  onError?: DirectEventHandler<{ message: string; code: Int32 }>
  onProgress?: DirectEventHandler<{ position: Double; duration: Double }>
}

export default codegenNativeComponent<NativeProps>('MyLibView') as HostComponent<NativeProps>

2. package.json Codegen Config for Views

"codegenConfig": {
  "name": "RNMyLibSpec",
  "type": "components",
  "jsSrcsDir": "src"
}

View Flattening — collapsable={false}

Fabric aggressively flattens layout-only views. If a native component expects a specific child at a specific index, the child may be removed silently.

// ❌ Fabric may remove this wrapper — native code breaks
<View style={styles.wrapper}>
  <NativeVideoView />
</View>

// ✅ Prevent flattening
<View style={styles.wrapper} collapsable={false}>
  <NativeVideoView />
</View>

Detect flattening issues with:

  • iOS: Xcode → Debug View Hierarchy (look for missing layers)
  • Android: Android Studio → Layout Inspector

Properties that prevent flattening (no need for collapsable): backgroundColor, borderWidth, shadowColor, event handlers, opacity < 1, overflow: 'hidden'.


Codegen Type Reference

TypeScriptImportNotes
DoubleCodegenTypes64-bit float
FloatCodegenTypes32-bit float
Int32CodegenTypes32-bit integer
stringnative string
booleannative bool
ReadonlyArray<T>typed array
DirectEventHandler<T>CodegenTypesfires on target only
BubblingEventHandler<T>CodegenTypesbubbles up tree


Expo Support for Native Libraries

Two separate concerns. Do not confuse them.

1. expo-module.config.json — Auto-linking

Tells Expo's auto-linker which native classes to register. Without this file, the library won't link in Expo managed or bare workflow.

The class names must match what you named your native classes. Use the actual class names from your iOS and Android implementation.

TurboModule — the iOS class is RNMyLib (from RCT_EXPORT_MODULE()), Android class is MyLibModule (from getName()):

{
  "platforms": ["apple", "android"],
  "ios": {
    "modules": ["RNMyLib"]
  },
  "android": {
    "modules": ["com.mylib.MyLibModule"]
  }
}

Fabric view — the iOS view component is RNMyLibView, Android is MyLibViewManager:

{
  "platforms": ["apple", "android"],
  "ios": {
    "components": ["RNMyLibView"]
  },
  "android": {
    "components": ["com.mylib.MyLibViewManager"]
  }
}

How to find the right class names:

  • iOS: look for RCT_EXPORT_MODULE() or RCT_EXPORT_VIEW_COMPONENT() in your .mm file — the argument (or class name if no argument) is what goes here
  • Android: look for the class extending NativeXXXSpec or SimpleViewManager — use the fully qualified class name

Add "expo-module.config.json" to files in package.json so it ships with the package.

2. app.plugin.js — Config Plugin (only when needed)

A config plugin modifies the consumer's native project at build time — adding permissions to AndroidManifest.xml, entries to Info.plist, Gradle dependencies, etc.

You only need this if your library requires native project configuration beyond just linking. Examples:

  • Library needs camera permission → add NSCameraUsageDescription to Info.plist
  • Library needs a background mode → add to UIBackgroundModes
  • Library needs a custom Gradle dependency not auto-linked

Most simple TurboModules and Fabric views do NOT need a config plugin — auto-linking is sufficient.

If you do need one:

// app.plugin.js
const { withInfoPlist } = require('@expo/config-plugins')

module.exports = function withMyLib(config) {
  return withInfoPlist(config, config => {
    config.modResults.NSCameraUsageDescription = 'Required for camera preview'
    return config
  })
}

Then add to package.json:

{
  "main": "app.plugin.js"
}

The directory entry field configPlugin: true refers to this app.plugin.js — not expo-module.config.json. Every native library should have expo-module.config.json. Only libraries that modify native project config need app.plugin.js.


Rules

  • TurboModuleRegistry.getEnforcing — never NativeModules
  • codegenNativeComponent — never requireNativeComponent
  • Sync methods must complete in <16ms — anything longer goes async
  • Android: always cancel coroutine scope in invalidate()
  • iOS: always dispatch_async for work >1ms
  • Never edit auto-generated files in build/ or android/build/
  • Module name in spec must exactly match getName() on Android, RCT_EXPORT_MODULE() on iOS
  • All methods declared in spec must be implemented — Codegen enforces at build time
  • Codegen specs in TypeScript — never Flow

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.