Skip to content

Commit 70ba9e1

Browse files
atljmrousavy
andauthored
feat(crnl): support nitro modules (#721)
### 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 <marcrousavy@hotmail.com>
1 parent ec2c874 commit 70ba9e1

File tree

22 files changed

+371
-84
lines changed

22 files changed

+371
-84
lines changed

.github/workflows/build-templates.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ jobs:
3434
- fabric-view
3535
- legacy-module
3636
- legacy-view
37+
- nitro-module
3738
language:
3839
- kotlin-objc
3940
- kotlin-swift
@@ -47,6 +48,10 @@ jobs:
4748
language: cpp
4849
- type: legacy-view
4950
language: cpp
51+
- type: nitro-module
52+
language: kotlin-objc
53+
- type: nitro-module
54+
language: cpp
5055
include:
5156
- os: ubuntu
5257
type: library

packages/create-react-native-library/src/exampleApp/dependencies.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import assert from 'node:assert';
22
import path from 'path';
33
import fs from 'fs-extra';
4-
import type { ExampleApp } from '../input';
4+
import type { TemplateConfiguration } from '../template';
55

66
export async function getDependencyVersionsFromExampleApp(
77
folder: string,
8-
exampleAppType: ExampleApp
8+
config: TemplateConfiguration
99
) {
1010
const examplePackageJson = await fs.readJSON(
1111
path.join(folder, 'example', 'package.json')
@@ -27,7 +27,11 @@ export async function getDependencyVersionsFromExampleApp(
2727
'react-native': reactNative,
2828
};
2929

30-
if (exampleAppType === 'vanilla') {
30+
if (
31+
config.example === 'vanilla' &&
32+
(config.project.moduleConfig === 'turbo-modules' ||
33+
config.project.viewConfig === 'fabric-view')
34+
) {
3135
// React Native doesn't provide the community CLI as a dependency.
3236
// We have to get read the version from the example app and put to the root package json
3337
const exampleCommunityCLIVersion =

packages/create-react-native-library/src/exampleApp/generateExampleApp.ts

Lines changed: 31 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import path from 'path';
33
import https from 'https';
44
import { spawn } from '../utils/spawn';
55
import sortObjectKeys from '../utils/sortObjectKeys';
6-
import type { ExampleApp } from '../input';
6+
import type { TemplateConfiguration } from '../template';
77

88
const FILES_TO_DELETE = [
99
'__tests__',
@@ -42,33 +42,23 @@ const PACKAGES_TO_ADD_WEB = {
4242
};
4343

4444
export default async function generateExampleApp({
45-
type,
46-
dest,
47-
arch,
48-
project,
49-
bobVersion,
45+
config,
46+
destination,
5047
reactNativeVersion = 'latest',
5148
}: {
52-
type: ExampleApp;
53-
dest: string;
54-
arch: 'new' | 'legacy';
55-
project: {
56-
slug: string;
57-
name: string;
58-
package: string;
59-
};
60-
bobVersion: string;
49+
config: TemplateConfiguration;
50+
destination: string;
6151
reactNativeVersion?: string;
6252
}) {
63-
const directory = path.join(dest, 'example');
53+
const directory = path.join(destination, 'example');
6454

6555
// `npx --package react-native-test-app@latest init --name ${projectName}Example --destination example --version ${reactNativeVersion}`
6656
const testAppArgs = [
6757
'--package',
6858
`react-native-test-app@latest`,
6959
'init',
7060
'--name',
71-
`${project.name}Example`,
61+
`${config.project.name}Example`,
7262
`--destination`,
7363
directory,
7464
...(reactNativeVersion !== 'latest'
@@ -84,9 +74,9 @@ export default async function generateExampleApp({
8474
const vanillaArgs = [
8575
`@react-native-community/cli`,
8676
'init',
87-
`${project.name}Example`,
77+
`${config.project.name}Example`,
8878
'--package-name',
89-
`${project.package}.example`,
79+
`${config.project.package}.example`,
9080
'--directory',
9181
directory,
9282
'--version',
@@ -107,7 +97,7 @@ export default async function generateExampleApp({
10797

10898
let args: string[] = [];
10999

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

134-
pkg.name = `${project.slug}-example`;
124+
pkg.name = `${config.project.slug}-example`;
135125

136126
// Remove Jest config for now
137127
delete pkg.jest;
@@ -144,12 +134,12 @@ export default async function generateExampleApp({
144134
const SCRIPTS_TO_ADD = {
145135
'build:android':
146136
'react-native build-android --extra-params "--no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a"',
147-
'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"`,
137+
'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"`,
148138
};
149139

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

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

175165
app.android = app.android || {};
176-
app.android.package = `${project.package}.example`;
166+
app.android.package = `${config.project.package}.example`;
177167
app.ios = app.ios || {};
178-
app.ios.bundleIdentifier = `${project.package}.example`;
168+
app.ios.bundleIdentifier = `${config.project.package}.example`;
179169

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

190180
const PACKAGES_TO_ADD_DEV = {
191-
'react-native-builder-bob': `^${bobVersion}`,
181+
'react-native-builder-bob': `^${config.versions.bob}`,
192182
};
193183

184+
if (config.project.moduleConfig === 'nitro-modules') {
185+
const packagesToAddNitro = {
186+
'react-native-nitro-modules': `^${config.versions.nitroModules}`,
187+
};
188+
Object.assign(dependencies, packagesToAddNitro);
189+
}
190+
194191
Object.assign(devDependencies, PACKAGES_TO_ADD_DEV);
195192

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

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

233230
app.expo.android = app.expo.android || {};
234-
app.expo.android.package = `${project.package}.example`;
231+
app.expo.android.package = `${config.project.package}.example`;
235232
app.expo.ios = app.expo.ios || {};
236-
app.expo.ios.bundleIdentifier = `${project.package}.example`;
233+
app.expo.ios.bundleIdentifier = `${config.project.package}.example`;
237234

238235
await fs.writeJSON(path.join(directory, 'app.json'), app, {
239236
spaces: 2,
240237
});
241238
}
242239

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

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

266265
// If the library is on new architecture, enable new arch for iOS and Android
267-
if (arch === 'new') {
266+
if (config.project.arch === 'new') {
268267
// iOS
269268
// Add ENV['RCT_NEW_ARCH_ENABLED'] = 1 on top of example/ios/Podfile
270269
const podfile = await fs.readFile(

packages/create-react-native-library/src/index.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { getDependencyVersionsFromExampleApp } from './exampleApp/dependencies';
2121
import { printErrorHelp, printNextSteps, printUsedRNVersion } from './inform';
2222

2323
const FALLBACK_BOB_VERSION = '0.36.0';
24+
const FALLBACK_NITRO_MODULES_VERSION = '0.22.1';
2425

2526
yargs
2627
.command(
@@ -48,6 +49,10 @@ async function create(_argv: yargs.Arguments<Args>) {
4849
'react-native-builder-bob',
4950
FALLBACK_BOB_VERSION
5051
);
52+
const nitroModulesVersionPromise = resolveNpmPackageVersion(
53+
'react-native-nitro-modules',
54+
FALLBACK_NITRO_MODULES_VERSION
55+
);
5156

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

7176
const bobVersion = await bobVersionPromise;
7277

78+
const nitroModulesVersion =
79+
answers.type === 'nitro-module'
80+
? await nitroModulesVersionPromise
81+
: undefined;
82+
7383
const config = generateTemplateConfiguration({
74-
bobVersion,
84+
versions: {
85+
bob: bobVersion,
86+
nitroModules: nitroModulesVersion,
87+
// Nitro codegen's version is always the same as nitro modules version.
88+
nitroCodegen: nitroModulesVersion,
89+
},
7590
basename,
7691
answers,
7792
});
@@ -88,12 +103,9 @@ async function create(_argv: yargs.Arguments<Args>) {
88103
spinner.text = 'Generating example app';
89104

90105
await generateExampleApp({
91-
type: config.example,
92-
dest: folder,
93-
arch: config.project.arch,
94-
project: config.project,
95-
bobVersion,
106+
destination: folder,
96107
reactNativeVersion: answers.reactNativeVersion,
108+
config,
97109
});
98110
}
99111

@@ -106,7 +118,7 @@ async function create(_argv: yargs.Arguments<Args>) {
106118
if (config.example !== 'none') {
107119
const { devDependencies } = await getDependencyVersionsFromExampleApp(
108120
folder,
109-
config.example
121+
config
110122
);
111123

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

120-
if (config.example === 'vanilla' && config.project.arch === 'new') {
132+
if (
133+
config.example === 'vanilla' &&
134+
(config.project.moduleConfig === 'turbo-modules' ||
135+
config.project.viewConfig === 'fabric-view')
136+
) {
121137
addCodegenBuildScript(folder);
122138
}
123139

packages/create-react-native-library/src/input.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,24 @@ export type ProjectType =
2525
| 'fabric-view'
2626
| 'legacy-module'
2727
| 'legacy-view'
28+
| 'nitro-module'
2829
| 'library';
2930

3031
const LANGUAGE_CHOICES: {
3132
title: string;
3233
value: ProjectLanguages;
3334
types: ProjectType[];
3435
}[] = [
36+
{
37+
title: 'Kotlin & Swift',
38+
value: 'kotlin-swift',
39+
types: ['nitro-module', 'legacy-module', 'legacy-view'],
40+
},
3541
{
3642
title: 'Kotlin & Objective-C',
3743
value: 'kotlin-objc',
3844
types: ['turbo-module', 'fabric-view', 'legacy-module', 'legacy-view'],
3945
},
40-
{
41-
title: 'Kotlin & Swift',
42-
value: 'kotlin-swift',
43-
types: ['legacy-module', 'legacy-view'],
44-
},
4546
{
4647
title: 'C++ for Android & iOS',
4748
value: 'cpp',
@@ -89,6 +90,12 @@ const TYPE_CHOICES: {
8990
value: 'turbo-module',
9091
description: 'integration for native APIs to JS',
9192
},
93+
{
94+
title: 'Nitro module',
95+
value: 'nitro-module',
96+
description:
97+
'type-safe, fast integration for native APIs to JS (experimental)',
98+
},
9299
{
93100
title: 'Fabric view',
94101
value: 'fabric-view',

0 commit comments

Comments
 (0)