Skip to content

Commit 09ecb02

Browse files
committed
feat(CLI): add initialization command for CodePush setup
1 parent 6f6d90e commit 09ecb02

File tree

7 files changed

+337
-15
lines changed

7 files changed

+337
-15
lines changed

README.md

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,20 @@ The following changes are optional but recommended for cleaning up the old confi
3535
npm install @bravemobile/react-native-code-push
3636
```
3737

38-
### 2. iOS Setup
38+
### 2. Run init command
39+
40+
Run the following command to automatically configure your project for CodePush.
41+
42+
```bash
43+
npx code-push init
44+
```
45+
46+
This command will automatically edit your `AppDelegate` and `MainApplication` files to integrate CodePush.
47+
48+
<details><summary>Click to see the manual setup instructions.</summary>
49+
<p>
50+
51+
### iOS Setup
3952

4053
#### (1) Install CocoaPods Dependencies
4154

@@ -54,7 +67,7 @@ Run `cd ios && pod install && cd ..`
5467

5568
1. Open your project with Xcode (e.g. CodePushDemoApp.xcworkspace)
5669
2. File → New → File from Template
57-
3. Select 'Objective-C File' and click 'Next' and write any name as you like.
70+
3. Select 'Objective-C File' and click 'Next' and write any name as you like.
5871
4. Then Xcode will ask you to create a bridging header file. Click 'Create'.
5972
5. Delete the file created in step 3.
6073

@@ -110,7 +123,7 @@ Then, edit `AppDelegate.swift` like below.
110123
```
111124

112125

113-
### 3. Android Setup
126+
### Android Setup
114127

115128
#### (1) Edit `android/app/build.gradle`
116129

@@ -161,7 +174,10 @@ Add the following line to the end of the file.
161174
}
162175
```
163176

164-
### 4. Expo Setup
177+
</p>
178+
</details>
179+
180+
### 2-1. Expo Setup
165181
For Expo projects, you can use the automated config plugin instead of manual setup.
166182

167183
**Add plugin to your Expo configuration:**
@@ -185,7 +201,7 @@ npx expo prebuild
185201
**Requirements**
186202
Expo SDK: 50.0.0 or higher
187203

188-
### 5. "CodePush-ify" Your App
204+
### 3. "CodePush-ify" Your App
189205

190206
The root component of your app should be wrapped with a higher-order component.
191207

@@ -199,8 +215,8 @@ At runtime, the library fetches this information to keep the app up to date.
199215

200216
```typescript
201217
import CodePush, {
202-
ReleaseHistoryInterface,
203-
UpdateCheckRequest,
218+
ReleaseHistoryInterface,
219+
UpdateCheckRequest,
204220
} from "@bravemobile/react-native-code-push";
205221

206222
// ... MyApp Component
@@ -229,7 +245,7 @@ export default CodePush({
229245
> The URL for fetching the release history should point to the resource location generated by the CLI tool.
230246
231247

232-
#### 5-1. Telemetry Callbacks
248+
#### 3-1. Telemetry Callbacks
233249

234250
Please refer to the [CodePushOptions](https://github.com/Soomgo-Mobile/react-native-code-push/blob/f0d26f7614af41c6dd4daecd9f7146e2383b2b0d/typings/react-native-code-push.d.ts#L76-L95) type for more details.
235251
- **onUpdateSuccess:** Triggered when the update bundle is executed successfully.
@@ -239,7 +255,7 @@ Please refer to the [CodePushOptions](https://github.com/Soomgo-Mobile/react-nat
239255
- **onSyncError:** Triggered when an unknown error occurs during the update process. (`CodePush.SyncStatus.UNKNOWN_ERROR` status)
240256

241257

242-
### 6. Configure the CLI Tool
258+
### 4. Configure the CLI Tool
243259

244260
> [!TIP]
245261
> For a more detailed and practical example, refer to the `CodePushDemoApp` in `example` directory. ([link](https://github.com/Soomgo-Mobile/react-native-code-push/tree/master/Examples/CodePushDemoApp))
@@ -352,7 +368,7 @@ Create a new release history for a specific binary app version.
352368
This ensures that the library runtime recognizes the binary app as the latest version and determines that no CodePush update is available for it.
353369

354370
**Example:**
355-
- Create a new release history for the binary app version `1.0.0`.
371+
- Create a new release history for the binary app version `1.0.0`.
356372

357373
```bash
358374
npx code-push create-history --binary-version 1.0.0 --platform ios --identifier staging

cli/commands/initCommand/index.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const { initAndroid } = require('./initAndroid');
2+
const { initIos } = require('./initIos');
3+
const { program } = require('commander');
4+
5+
program
6+
.command('init')
7+
.description('Initialize CodePush project')
8+
.action(async () => {
9+
console.log('Start initializing CodePush...');
10+
await initAndroid();
11+
await initIos();
12+
console.log('CodePush has been successfully initialized.');
13+
});
14+
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
const path = require('path');
2+
const fs = require('fs');
3+
const { EOL } = require('os');
4+
5+
async function initAndroid() {
6+
console.log('Running Android setup...');
7+
await applyMainApplication();
8+
}
9+
10+
async function applyMainApplication() {
11+
const mainApplicationPath = await findMainApplication();
12+
if (!mainApplicationPath) {
13+
console.log('Could not find MainApplication.java or MainApplication.kt');
14+
return;
15+
}
16+
17+
const mainApplicationContent = fs.readFileSync(mainApplicationPath, 'utf-8');
18+
19+
if (mainApplicationPath.endsWith('.java')) {
20+
if (mainApplicationContent.includes('CodePush.getJSBundleFile()')) {
21+
console.log('MainApplication.java already has CodePush initialized.');
22+
return;
23+
}
24+
const newContent = mainApplicationContent
25+
.replace('import com.facebook.react.ReactApplication;', `import com.facebook.react.ReactApplication;${EOL}import com.microsoft.codepush.react.CodePush;`)
26+
.replace('new DefaultReactNativeHost(this) {', `new DefaultReactNativeHost(this) {${EOL} @Override${EOL} protected String getJSBundleFile() {${EOL} return CodePush.getJSBundleFile();${EOL} }${EOL}`)
27+
fs.writeFileSync(mainApplicationPath, newContent);
28+
console.log('Successfully updated MainApplication.java.');
29+
} else if (mainApplicationPath.endsWith('.kt')) {
30+
if (mainApplicationContent.includes('CodePush.getJSBundleFile()')) {
31+
console.log('MainApplication.kt already has CodePush initialized.');
32+
return;
33+
}
34+
const newContent = mainApplicationContent
35+
.replace('import com.facebook.react.ReactApplication', `import com.facebook.react.ReactApplication${EOL}import com.microsoft.codepush.react.CodePush`)
36+
.replace('override fun getJSMainModuleName(): String = "index"', `override fun getJSMainModuleName(): String = "index"${EOL} override fun getJSBundleFile(): String = CodePush.getJSBundleFile()`)
37+
fs.writeFileSync(mainApplicationPath, newContent);
38+
console.log('Successfully updated MainApplication.kt.');
39+
}
40+
}
41+
42+
async function findMainApplication() {
43+
const searchPath = path.join(process.cwd(), 'android', 'app', 'src', 'main', 'java');
44+
const files = fs.readdirSync(searchPath, { recursive: true });
45+
const mainApplicationFile = files.find(file => file.endsWith('MainApplication.java') || file.endsWith('MainApplication.kt'));
46+
return mainApplicationFile ? path.join(searchPath, mainApplicationFile) : null;
47+
}
48+
49+
module.exports = {
50+
initAndroid: initAndroid
51+
};
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
const path = require('path');
2+
const fs = require('fs');
3+
const xcode = require('xcode');
4+
5+
async function initIos() {
6+
console.log('Running iOS setup...');
7+
const projectDir = path.join(process.cwd(), 'ios');
8+
const files = fs.readdirSync(projectDir);
9+
const xcodeprojFile = files.find(file => file.endsWith('.xcodeproj'));
10+
if (!xcodeprojFile) {
11+
console.log('Could not find .xcodeproj file in ios directory');
12+
return;
13+
}
14+
const projectName = xcodeprojFile.replace('.xcodeproj', '');
15+
const appDelegatePath = findAppDelegate(path.join(projectDir, projectName));
16+
17+
if (!appDelegatePath) {
18+
console.log('Could not find AppDelegate file');
19+
return;
20+
}
21+
22+
if (appDelegatePath.endsWith('.swift')) {
23+
await setupSwift(appDelegatePath, projectDir, projectName);
24+
} else {
25+
await setupObjectiveC(appDelegatePath);
26+
}
27+
28+
console.log('Please run `cd ios && pod install` to complete the setup.');
29+
}
30+
31+
function findAppDelegate(searchPath) {
32+
if (!fs.existsSync(searchPath)) return null;
33+
const files = fs.readdirSync(searchPath);
34+
const appDelegateFile = files.find(file => file.startsWith('AppDelegate') && (file.endsWith('.m') || file.endsWith('.mm') || file.endsWith('.swift')));
35+
return appDelegateFile ? path.join(searchPath, appDelegateFile) : null;
36+
}
37+
38+
async function setupObjectiveC(appDelegatePath) {
39+
const appDelegateContent = fs.readFileSync(appDelegatePath, 'utf-8');
40+
const IMPORT_STATEMENT = '#import <CodePush/CodePush.h>';
41+
if (appDelegateContent.includes(IMPORT_STATEMENT)) {
42+
console.log('AppDelegate already has CodePush imported.');
43+
return;
44+
}
45+
46+
const newContent = appDelegateContent
47+
.replace('#import "AppDelegate.h"\n', `#import "AppDelegate.h"\n${IMPORT_STATEMENT}\n`)
48+
.replace('[[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];', '[CodePush bundleURL];');
49+
50+
fs.writeFileSync(appDelegatePath, newContent);
51+
console.log('Successfully updated AppDelegate.m/mm.');
52+
}
53+
54+
async function setupSwift(appDelegatePath, projectDir, projectName) {
55+
const bridgingHeaderPath = await ensureBridgingHeader(projectDir, projectName);
56+
if (!bridgingHeaderPath) {
57+
console.log('Failed to create or find bridging header.');
58+
return;
59+
}
60+
61+
const appDelegateContent = fs.readFileSync(appDelegatePath, 'utf-8');
62+
const CODEPUSH_CALL_STATEMENT = 'CodePush.bundleURL()';
63+
if (appDelegateContent.includes(CODEPUSH_CALL_STATEMENT)) {
64+
console.log('AppDelegate.swift already configured for CodePush.');
65+
return;
66+
}
67+
68+
const newContent = appDelegateContent
69+
.replace('Bundle.main.url(forResource: "main", withExtension: "jsbundle")', CODEPUSH_CALL_STATEMENT);
70+
fs.writeFileSync(appDelegatePath, newContent);
71+
console.log('Successfully updated AppDelegate.swift.');
72+
}
73+
74+
async function ensureBridgingHeader(projectDir, projectName) {
75+
const projectPath = path.join(projectDir, `${projectName}.xcodeproj`, 'project.pbxproj');
76+
const myProj = xcode.project(projectPath);
77+
78+
return new Promise((resolve, reject) => {
79+
myProj.parse(function (err) {
80+
if (err) {
81+
console.error(`Error parsing Xcode project: ${err}`);
82+
return reject(err);
83+
}
84+
85+
const bridgingHeaderRelativePath = `${projectName}/${projectName}-Bridging-Header.h`;
86+
const bridgingHeaderAbsolutePath = path.join(projectDir, bridgingHeaderRelativePath);
87+
88+
const configurations = myProj.pbxXCBuildConfigurationSection();
89+
for (const name in configurations) {
90+
const config = configurations[name];
91+
if (config.buildSettings) {
92+
config.buildSettings.SWIFT_OBJC_BRIDGING_HEADER = `"${bridgingHeaderRelativePath}"`;
93+
}
94+
}
95+
96+
if (!fs.existsSync(bridgingHeaderAbsolutePath)) {
97+
fs.mkdirSync(path.dirname(bridgingHeaderAbsolutePath), { recursive: true });
98+
fs.writeFileSync(bridgingHeaderAbsolutePath, '#import <CodePush/CodePush.h>\n');
99+
console.log(`Created bridging header at ${bridgingHeaderAbsolutePath}`);
100+
const groupKey = myProj.findPBXGroupKey({ name: projectName });
101+
myProj.addHeaderFile(bridgingHeaderRelativePath, { public: true }, groupKey);
102+
} else {
103+
const headerContent = fs.readFileSync(bridgingHeaderAbsolutePath, 'utf-8');
104+
if (!headerContent.includes('#import <CodePush/CodePush.h>')) {
105+
fs.appendFileSync(bridgingHeaderAbsolutePath, '\n#import <CodePush/CodePush.h>\n');
106+
console.log(`Updated bridging header at ${bridgingHeaderAbsolutePath}`);
107+
}
108+
}
109+
110+
fs.writeFileSync(projectPath, myProj.writeSync());
111+
console.log('Updated Xcode project with bridging header path.');
112+
resolve(bridgingHeaderAbsolutePath);
113+
});
114+
});
115+
}
116+
117+
module.exports = {
118+
initIos: initIos,
119+
}

cli/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,10 @@ require('./commands/releaseCommand');
4040
*/
4141
require('./commands/showHistoryCommand')
4242

43+
/**
44+
* npx code-push init
45+
*/
46+
require('./commands/initCommand')
47+
48+
4349
program.parse();

0 commit comments

Comments
 (0)