Skip to content

Commit abc20cb

Browse files
committed
feat(react-native): add Expo config plugin
1 parent 6e3d7b7 commit abc20cb

7 files changed

Lines changed: 272 additions & 0 deletions

File tree

examples/expo-demo/App.tsx

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import React, {useEffect, useMemo, useState} from 'react';
2+
import {Pressable, ScrollView, Text, View} from 'react-native';
3+
import NativeScript, {defineUIKitView} from '@nativescript/react-native';
4+
5+
NativeScript.init();
6+
7+
type BadgeProps = {
8+
title: string;
9+
tone: 'blue' | 'green';
10+
};
11+
12+
const NativeBadge = defineUIKitView<BadgeProps, UIView>({
13+
displayName: 'NativeBadge',
14+
create() {
15+
const view = UIView.alloc().initWithFrame(CGRectZero);
16+
view.clipsToBounds = true;
17+
18+
const label = UILabel.alloc().initWithFrame(CGRectZero);
19+
label.tag = 1;
20+
label.textAlignment = NSTextAlignment.Center;
21+
label.textColor = UIColor.whiteColor;
22+
label.autoresizingMask =
23+
UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
24+
view.addSubview(label);
25+
26+
return view;
27+
},
28+
mounted(view) {
29+
view.layer.cornerRadius = 14;
30+
},
31+
update(view, props) {
32+
view.backgroundColor =
33+
props.tone === 'green' ? UIColor.systemGreenColor : UIColor.systemBlueColor;
34+
const label = view.viewWithTag(1) as UILabel;
35+
label.text = props.title;
36+
},
37+
});
38+
39+
async function readNativeSummary() {
40+
const api = (globalThis as any).__nativeScriptNativeApi;
41+
let ranOnMainThread = false;
42+
43+
await NativeScript.runOnUI(() => {
44+
ranOnMainThread = NSThread.isMainThread === true;
45+
UIApplication.sharedApplication.keyWindow.tintColor =
46+
UIColor.systemPinkColor;
47+
});
48+
49+
return {
50+
backend: api?.backend,
51+
classes: api?.metadata?.classes ?? 0,
52+
constants: api?.metadata?.constants ?? 0,
53+
enums: api?.metadata?.enums ?? 0,
54+
ranOnMainThread,
55+
timeoutConstant: NSURLErrorTimedOut,
56+
darkStyle: UIUserInterfaceStyle.Dark,
57+
};
58+
}
59+
export default function App() {
60+
const [tone, setTone] = useState<'blue' | 'green'>('blue');
61+
const [summary, setSummary] = useState('Loading NativeScript...');
62+
63+
useEffect(() => {
64+
readNativeSummary()
65+
.then((value) => setSummary(JSON.stringify(value, null, 2)))
66+
.catch((error) => {
67+
setSummary(error instanceof Error ? error.message : String(error));
68+
});
69+
}, []);
70+
71+
const title = useMemo(
72+
() => (tone === 'blue' ? 'UIKit view from Expo' : 'Updated from React state'),
73+
[tone],
74+
);
75+
76+
return (
77+
<ScrollView
78+
contentInsetAdjustmentBehavior="automatic"
79+
contentContainerStyle={{gap: 18, padding: 24}}>
80+
<View style={{gap: 8}}>
81+
<Text style={{fontSize: 28, fontWeight: '800'}}>
82+
NativeScript Expo
83+
</Text>
84+
<Text selectable style={{fontSize: 16, color: '#394150'}}>
85+
Define UIKit views directly in JavaScript and mount them in an Expo
86+
development build.
87+
</Text>
88+
</View>
89+
90+
<NativeBadge title={title} tone={tone} style={{height: 56}} />
91+
92+
<Pressable
93+
onPress={() => setTone((value) => (value === 'blue' ? 'green' : 'blue'))}
94+
style={{
95+
alignItems: 'center',
96+
borderRadius: 8,
97+
backgroundColor: '#111827',
98+
minHeight: 48,
99+
justifyContent: 'center',
100+
paddingHorizontal: 16,
101+
}}>
102+
<Text style={{color: 'white', fontSize: 16, fontWeight: '700'}}>
103+
Toggle Native Badge
104+
</Text>
105+
</Pressable>
106+
107+
<Text
108+
selectable
109+
style={{
110+
borderRadius: 8,
111+
backgroundColor: '#f3f4f6',
112+
color: '#111827',
113+
fontFamily: 'Menlo',
114+
fontSize: 13,
115+
lineHeight: 18,
116+
padding: 14,
117+
}}>
118+
{summary}
119+
</Text>
120+
</ScrollView>
121+
);
122+
}

examples/expo-demo/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# NativeScript Expo Demo
2+
3+
This example is meant to be copied into a generated Expo app after installing
4+
`@nativescript/react-native`.
5+
6+
```sh
7+
npx create-expo-app NativeScriptExpoDemo --template blank-typescript
8+
cd NativeScriptExpoDemo
9+
npm install /path/to/nativescript-react-native-0.0.1.tgz
10+
cp /path/to/napi-ios/examples/expo-demo/app.config.js ./app.config.js
11+
cp /path/to/napi-ios/examples/expo-demo/App.tsx ./App.tsx
12+
npx expo prebuild --platform ios
13+
npx expo run:ios
14+
```
15+
16+
The package config plugin enables the iOS New Architecture and Hermes during
17+
prebuild. This custom native module cannot run inside Expo Go.

examples/expo-demo/app.config.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module.exports = {
2+
expo: {
3+
name: 'NativeScript Expo Demo',
4+
slug: 'nativescript-expo-demo',
5+
scheme: 'nativescriptexpodemo',
6+
ios: {
7+
bundleIdentifier: 'org.nativescript.expo.demo',
8+
},
9+
plugins: ['@nativescript/react-native'],
10+
},
11+
};

packages/react-native/README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,64 @@ npm run test-rn-turbomodule
127127
UIColor.systemPinkColor;
128128
});
129129
```
130+
131+
## Using the package in an Expo app
132+
133+
Expo Go cannot load this package because it contains custom native code. Use an
134+
Expo development build, EAS Build, or `npx expo run:ios`.
135+
136+
1. Install the package:
137+
138+
```sh
139+
npx expo install @nativescript/react-native
140+
```
141+
142+
When testing a local tarball:
143+
144+
```sh
145+
npm install /path/to/nativescript-react-native-0.0.1.tgz
146+
```
147+
148+
2. Add the config plugin to `app.json` or `app.config.js`:
149+
150+
```json
151+
{
152+
"expo": {
153+
"plugins": ["@nativescript/react-native"]
154+
}
155+
}
156+
```
157+
158+
The plugin configures iOS for Hermes and the React Native New Architecture,
159+
which are required by this JSI TurboModule.
160+
161+
3. Prebuild and run the iOS development build:
162+
163+
```sh
164+
npx expo prebuild --platform ios
165+
npx expo run:ios
166+
```
167+
168+
4. Initialize NativeScript in app code before using native APIs:
169+
170+
```tsx
171+
import NativeScript, {defineUIKitView} from "@nativescript/react-native";
172+
173+
NativeScript.init();
174+
175+
const NativeBadge = defineUIKitView<{title: string}, UIView>({
176+
create() {
177+
const view = UIView.alloc().initWithFrame(CGRectZero);
178+
const label = UILabel.alloc().initWithFrame(CGRectZero);
179+
label.tag = 1;
180+
label.textAlignment = NSTextAlignment.Center;
181+
view.addSubview(label);
182+
return view;
183+
},
184+
update(view, props) {
185+
view.backgroundColor = UIColor.systemBlueColor;
186+
const label = view.viewWithTag(1) as UILabel;
187+
label.text = props.title;
188+
},
189+
});
190+
```
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('./plugin/withNativeScriptReactNative');

packages/react-native/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
"react-native": "src/index.ts",
2424
"types": "src/index.d.ts",
2525
"files": [
26+
"app.plugin.js",
27+
"plugin",
2628
"src",
2729
"types",
2830
"ios",
@@ -33,9 +35,15 @@
3335
"LICENSE"
3436
],
3537
"peerDependencies": {
38+
"expo": "*",
3639
"react": "*",
3740
"react-native": ">=0.79"
3841
},
42+
"peerDependenciesMeta": {
43+
"expo": {
44+
"optional": true
45+
}
46+
},
3947
"codegenConfig": {
4048
"name": "NativeScriptNativeApiSpec",
4149
"type": "all",
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
const pkg = require('../package.json');
2+
3+
const DEFAULTS = {
4+
ios: {
5+
hermes: true,
6+
newArchitecture: true,
7+
},
8+
};
9+
10+
function readBoolean(value, fallback) {
11+
return typeof value === 'boolean' ? value : fallback;
12+
}
13+
14+
function normalizeOptions(options = {}) {
15+
const ios = options.ios || {};
16+
return {
17+
ios: {
18+
hermes: readBoolean(
19+
ios.hermes,
20+
readBoolean(options.hermes, DEFAULTS.ios.hermes),
21+
),
22+
newArchitecture: readBoolean(
23+
ios.newArchitecture,
24+
readBoolean(options.newArchitecture, DEFAULTS.ios.newArchitecture),
25+
),
26+
},
27+
};
28+
}
29+
30+
function withNativeScriptReactNative(config, options) {
31+
const normalized = normalizeOptions(options);
32+
33+
config.ios = config.ios || {};
34+
35+
if (normalized.ios.hermes) {
36+
config.ios.jsEngine = 'hermes';
37+
}
38+
39+
if (normalized.ios.newArchitecture) {
40+
// Expo SDKs have accepted both the root and iOS-scoped keys over time.
41+
// Set both so CNG and existing native projects agree on the required RN mode.
42+
config.newArchEnabled = true;
43+
config.ios.newArchEnabled = true;
44+
}
45+
46+
return config;
47+
}
48+
49+
module.exports = withNativeScriptReactNative;
50+
module.exports.default = withNativeScriptReactNative;
51+
module.exports.withNativeScriptReactNative = withNativeScriptReactNative;
52+
module.exports.pkg = pkg;

0 commit comments

Comments
 (0)