Skip to content

Commit

Permalink
feat(crnl): support nitro modules (#721)
Browse files Browse the repository at this point in the history
### Summary

Adds [Nitro Modules](https://nitro.margelo.com/) support to
`create-react-native-library`.
![Screenshot 2024-12-06 at 20 05
42](https://github.com/user-attachments/assets/57f2f7ac-a0a2-472f-8446-ea1296ce59ff)

### What's next?

### Test plan

Prior to all cases:
1. Clone this branch
2. Call `yarn` and `yarn prepare`
3. Call `bin/create-react-native-library`

#### Example app

1. Create a new nitro module
4. Go to the module and install node dependencies
5. Call `yarn nitrogen` to create nitro bindings
6. Build the Android app and make sure it works properly
7. Install the pods and build the iOS app and make sure it works
properly

#### Published lib

1. Publish the library from the previous case using
`https://github.com/wclr/yalc` (`yalc publish`)
2. Create a new React Native app
3. Call `yalc add your-lib`
4. Make sure the React Native app builds and works fine with your lib.

---------

Co-authored-by: Marc Rousavy <[email protected]>
  • Loading branch information
atlj and mrousavy authored Feb 7, 2025
1 parent ec2c874 commit 70ba9e1
Show file tree
Hide file tree
Showing 22 changed files with 371 additions and 84 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/build-templates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ jobs:
- fabric-view
- legacy-module
- legacy-view
- nitro-module
language:
- kotlin-objc
- kotlin-swift
Expand All @@ -47,6 +48,10 @@ jobs:
language: cpp
- type: legacy-view
language: cpp
- type: nitro-module
language: kotlin-objc
- type: nitro-module
language: cpp
include:
- os: ubuntu
type: library
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import assert from 'node:assert';
import path from 'path';
import fs from 'fs-extra';
import type { ExampleApp } from '../input';
import type { TemplateConfiguration } from '../template';

export async function getDependencyVersionsFromExampleApp(
folder: string,
exampleAppType: ExampleApp
config: TemplateConfiguration
) {
const examplePackageJson = await fs.readJSON(
path.join(folder, 'example', 'package.json')
Expand All @@ -27,7 +27,11 @@ export async function getDependencyVersionsFromExampleApp(
'react-native': reactNative,
};

if (exampleAppType === 'vanilla') {
if (
config.example === 'vanilla' &&
(config.project.moduleConfig === 'turbo-modules' ||
config.project.viewConfig === 'fabric-view')
) {
// React Native doesn't provide the community CLI as a dependency.
// We have to get read the version from the example app and put to the root package json
const exampleCommunityCLIVersion =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import path from 'path';
import https from 'https';
import { spawn } from '../utils/spawn';
import sortObjectKeys from '../utils/sortObjectKeys';
import type { ExampleApp } from '../input';
import type { TemplateConfiguration } from '../template';

const FILES_TO_DELETE = [
'__tests__',
Expand Down Expand Up @@ -42,33 +42,23 @@ const PACKAGES_TO_ADD_WEB = {
};

export default async function generateExampleApp({
type,
dest,
arch,
project,
bobVersion,
config,
destination,
reactNativeVersion = 'latest',
}: {
type: ExampleApp;
dest: string;
arch: 'new' | 'legacy';
project: {
slug: string;
name: string;
package: string;
};
bobVersion: string;
config: TemplateConfiguration;
destination: string;
reactNativeVersion?: string;
}) {
const directory = path.join(dest, 'example');
const directory = path.join(destination, 'example');

// `npx --package react-native-test-app@latest init --name ${projectName}Example --destination example --version ${reactNativeVersion}`
const testAppArgs = [
'--package',
`react-native-test-app@latest`,
'init',
'--name',
`${project.name}Example`,
`${config.project.name}Example`,
`--destination`,
directory,
...(reactNativeVersion !== 'latest'
Expand All @@ -84,9 +74,9 @@ export default async function generateExampleApp({
const vanillaArgs = [
`@react-native-community/cli`,
'init',
`${project.name}Example`,
`${config.project.name}Example`,
'--package-name',
`${project.package}.example`,
`${config.project.package}.example`,
'--directory',
directory,
'--version',
Expand All @@ -107,7 +97,7 @@ export default async function generateExampleApp({

let args: string[] = [];

switch (type) {
switch (config.example) {
case 'vanilla':
args = vanillaArgs;
break;
Expand All @@ -131,7 +121,7 @@ export default async function generateExampleApp({
// Patch the example app's package.json
const pkg = await fs.readJSON(path.join(directory, 'package.json'));

pkg.name = `${project.slug}-example`;
pkg.name = `${config.project.slug}-example`;

// Remove Jest config for now
delete pkg.jest;
Expand All @@ -144,12 +134,12 @@ export default async function generateExampleApp({
const SCRIPTS_TO_ADD = {
'build:android':
'react-native build-android --extra-params "--no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a"',
'build:ios': `react-native build-ios --scheme ${project.name}Example --mode Debug --extra-params "-sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO"`,
'build:ios': `react-native build-ios --scheme ${config.project.name}Example --mode Debug --extra-params "-sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO"`,
};

if (type === 'vanilla') {
if (config.example === 'vanilla') {
Object.assign(scripts, SCRIPTS_TO_ADD);
} else if (type === 'test-app') {
} else if (config.example === 'test-app') {
// `react-native-test-app` doesn't bundle application by default in 'Release' mode and also `bundle` command doesn't create a directory.
// `mkdist` script should be removed after stable React Native major contains this fix: https://github.com/facebook/react-native/pull/45182.

Expand All @@ -173,9 +163,9 @@ export default async function generateExampleApp({
const app = await fs.readJSON(path.join(directory, 'app.json'));

app.android = app.android || {};
app.android.package = `${project.package}.example`;
app.android.package = `${config.project.package}.example`;
app.ios = app.ios || {};
app.ios.bundleIdentifier = `${project.package}.example`;
app.ios.bundleIdentifier = `${config.project.package}.example`;

await fs.writeJSON(path.join(directory, 'app.json'), app, {
spaces: 2,
Expand All @@ -188,12 +178,19 @@ export default async function generateExampleApp({
});

const PACKAGES_TO_ADD_DEV = {
'react-native-builder-bob': `^${bobVersion}`,
'react-native-builder-bob': `^${config.versions.bob}`,
};

if (config.project.moduleConfig === 'nitro-modules') {
const packagesToAddNitro = {
'react-native-nitro-modules': `^${config.versions.nitroModules}`,
};
Object.assign(dependencies, packagesToAddNitro);
}

Object.assign(devDependencies, PACKAGES_TO_ADD_DEV);

if (type === 'expo') {
if (config.example === 'expo') {
const sdkVersion = dependencies.expo.split('.')[0].replace(/[^\d]/, '');

let bundledNativeModules: Record<string, string>;
Expand Down Expand Up @@ -231,15 +228,17 @@ export default async function generateExampleApp({
const app = await fs.readJSON(path.join(directory, 'app.json'));

app.expo.android = app.expo.android || {};
app.expo.android.package = `${project.package}.example`;
app.expo.android.package = `${config.project.package}.example`;
app.expo.ios = app.expo.ios || {};
app.expo.ios.bundleIdentifier = `${project.package}.example`;
app.expo.ios.bundleIdentifier = `${config.project.package}.example`;

await fs.writeJSON(path.join(directory, 'app.json'), app, {
spaces: 2,
});
}

// Sort the deps by name to match behavior of package managers
// This way the package.json doesn't get updated when installing deps
for (const field of ['dependencies', 'devDependencies']) {
if (pkg[field]) {
pkg[field] = sortObjectKeys(pkg[field]);
Expand All @@ -250,7 +249,7 @@ export default async function generateExampleApp({
spaces: 2,
});

if (type !== 'expo') {
if (config.example !== 'expo') {
let gradleProperties = await fs.readFile(
path.join(directory, 'android', 'gradle.properties'),
'utf8'
Expand All @@ -264,7 +263,7 @@ export default async function generateExampleApp({
);

// If the library is on new architecture, enable new arch for iOS and Android
if (arch === 'new') {
if (config.project.arch === 'new') {
// iOS
// Add ENV['RCT_NEW_ARCH_ENABLED'] = 1 on top of example/ios/Podfile
const podfile = await fs.readFile(
Expand Down
32 changes: 24 additions & 8 deletions packages/create-react-native-library/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { getDependencyVersionsFromExampleApp } from './exampleApp/dependencies';
import { printErrorHelp, printNextSteps, printUsedRNVersion } from './inform';

const FALLBACK_BOB_VERSION = '0.36.0';
const FALLBACK_NITRO_MODULES_VERSION = '0.22.1';

yargs
.command(
Expand Down Expand Up @@ -48,6 +49,10 @@ async function create(_argv: yargs.Arguments<Args>) {
'react-native-builder-bob',
FALLBACK_BOB_VERSION
);
const nitroModulesVersionPromise = resolveNpmPackageVersion(
'react-native-nitro-modules',
FALLBACK_NITRO_MODULES_VERSION
);

const local = await promptLocalLibrary(argv);
const folder = await promptPath(argv, local);
Expand All @@ -70,8 +75,18 @@ async function create(_argv: yargs.Arguments<Args>) {

const bobVersion = await bobVersionPromise;

const nitroModulesVersion =
answers.type === 'nitro-module'
? await nitroModulesVersionPromise
: undefined;

const config = generateTemplateConfiguration({
bobVersion,
versions: {
bob: bobVersion,
nitroModules: nitroModulesVersion,
// Nitro codegen's version is always the same as nitro modules version.
nitroCodegen: nitroModulesVersion,
},
basename,
answers,
});
Expand All @@ -88,12 +103,9 @@ async function create(_argv: yargs.Arguments<Args>) {
spinner.text = 'Generating example app';

await generateExampleApp({
type: config.example,
dest: folder,
arch: config.project.arch,
project: config.project,
bobVersion,
destination: folder,
reactNativeVersion: answers.reactNativeVersion,
config,
});
}

Expand All @@ -106,7 +118,7 @@ async function create(_argv: yargs.Arguments<Args>) {
if (config.example !== 'none') {
const { devDependencies } = await getDependencyVersionsFromExampleApp(
folder,
config.example
config
);

rootPackageJson.devDependencies = rootPackageJson.devDependencies
Expand All @@ -117,7 +129,11 @@ async function create(_argv: yargs.Arguments<Args>) {
: devDependencies;
}

if (config.example === 'vanilla' && config.project.arch === 'new') {
if (
config.example === 'vanilla' &&
(config.project.moduleConfig === 'turbo-modules' ||
config.project.viewConfig === 'fabric-view')
) {
addCodegenBuildScript(folder);
}

Expand Down
17 changes: 12 additions & 5 deletions packages/create-react-native-library/src/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,24 @@ export type ProjectType =
| 'fabric-view'
| 'legacy-module'
| 'legacy-view'
| 'nitro-module'
| 'library';

const LANGUAGE_CHOICES: {
title: string;
value: ProjectLanguages;
types: ProjectType[];
}[] = [
{
title: 'Kotlin & Swift',
value: 'kotlin-swift',
types: ['nitro-module', 'legacy-module', 'legacy-view'],
},
{
title: 'Kotlin & Objective-C',
value: 'kotlin-objc',
types: ['turbo-module', 'fabric-view', 'legacy-module', 'legacy-view'],
},
{
title: 'Kotlin & Swift',
value: 'kotlin-swift',
types: ['legacy-module', 'legacy-view'],
},
{
title: 'C++ for Android & iOS',
value: 'cpp',
Expand Down Expand Up @@ -89,6 +90,12 @@ const TYPE_CHOICES: {
value: 'turbo-module',
description: 'integration for native APIs to JS',
},
{
title: 'Nitro module',
value: 'nitro-module',
description:
'type-safe, fast integration for native APIs to JS (experimental)',
},
{
title: 'Fabric view',
value: 'fabric-view',
Expand Down
Loading

0 comments on commit 70ba9e1

Please sign in to comment.