Skip to content

Commit 96f11ac

Browse files
authored
Merge pull request #82 from Soomgo-Mobile/v12-cli-init-command
feat(CLI): add initialization command for CodePush setup
2 parents 6f6d90e + f229aec commit 96f11ac

File tree

9 files changed

+523
-12
lines changed

9 files changed

+523
-12
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+
### 3. 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+
### 4. "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+
#### 4-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+
### 5. 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('Automatically performs iOS/Android native configurations to initialize the CodePush project.')
8+
.action(async () => {
9+
console.log('log: Start initializing CodePush...');
10+
await initAndroid();
11+
await initIos();
12+
console.log('log: CodePush has been successfully initialized.');
13+
});
14+
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
const path = require('path');
2+
const fs = require('fs');
3+
const { EOL } = require('os');
4+
5+
function modifyMainApplicationKt(mainApplicationContent) {
6+
if (mainApplicationContent.includes('CodePush.getJSBundleFile()')) {
7+
console.log('log: MainApplication.kt already has CodePush initialized.');
8+
return mainApplicationContent;
9+
}
10+
return mainApplicationContent
11+
.replace('import com.facebook.react.ReactApplication', `import com.facebook.react.ReactApplication${EOL}import com.microsoft.codepush.react.CodePush`)
12+
.replace('override fun getJSMainModuleName(): String = "index"', `override fun getJSMainModuleName(): String = "index"${EOL} override fun getJSBundleFile(): String = CodePush.getJSBundleFile()`)
13+
}
14+
15+
async function initAndroid() {
16+
console.log('log: Running Android setup...');
17+
await applyMainApplication();
18+
}
19+
20+
async function applyMainApplication() {
21+
const mainApplicationPath = await findMainApplication();
22+
if (!mainApplicationPath) {
23+
console.log('log: Could not find MainApplication.kt');
24+
return;
25+
}
26+
27+
if (mainApplicationPath.endsWith('.java')) {
28+
console.log('log: MainApplication.java is not supported. Please migrate to MainApplication.kt.');
29+
return;
30+
}
31+
32+
const mainApplicationContent = fs.readFileSync(mainApplicationPath, 'utf-8');
33+
const newContent = modifyMainApplicationKt(mainApplicationContent);
34+
fs.writeFileSync(mainApplicationPath, newContent);
35+
console.log('log: Successfully updated MainApplication.kt.');
36+
}
37+
38+
async function findMainApplication() {
39+
const searchPath = path.join(process.cwd(), 'android', 'app', 'src', 'main', 'java');
40+
const files = fs.readdirSync(searchPath, { recursive: true });
41+
const mainApplicationFile = files.find(file => file.endsWith('MainApplication.java') || file.endsWith('MainApplication.kt'));
42+
return mainApplicationFile ? path.join(searchPath, mainApplicationFile) : null;
43+
}
44+
45+
module.exports = {
46+
initAndroid: initAndroid,
47+
modifyMainApplicationKt: modifyMainApplicationKt,
48+
};
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
const path = require('path');
2+
const fs = require('fs');
3+
const xcode = require('xcode');
4+
5+
async function initIos() {
6+
console.log('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('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('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('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+
function modifyObjectiveCAppDelegate(appDelegateContent) {
39+
const IMPORT_STATEMENT = '#import <CodePush/CodePush.h>';
40+
if (appDelegateContent.includes(IMPORT_STATEMENT)) {
41+
console.log('log: AppDelegate already has CodePush imported.');
42+
return appDelegateContent;
43+
}
44+
45+
return appDelegateContent
46+
.replace('#import "AppDelegate.h"\n', `#import "AppDelegate.h"\n${IMPORT_STATEMENT}\n`)
47+
.replace('[[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];', '[CodePush bundleURL];');
48+
}
49+
50+
function modifySwiftAppDelegate(appDelegateContent) {
51+
const CODEPUSH_CALL_STATEMENT = 'CodePush.bundleURL()';
52+
if (appDelegateContent.includes(CODEPUSH_CALL_STATEMENT)) {
53+
console.log('log: AppDelegate.swift already configured for CodePush.');
54+
return appDelegateContent;
55+
}
56+
57+
return appDelegateContent
58+
.replace('Bundle.main.url(forResource: "main", withExtension: "jsbundle")', CODEPUSH_CALL_STATEMENT);
59+
}
60+
61+
async function setupObjectiveC(appDelegatePath) {
62+
const appDelegateContent = fs.readFileSync(appDelegatePath, 'utf-8');
63+
const newContent = modifyObjectiveCAppDelegate(appDelegateContent);
64+
fs.writeFileSync(appDelegatePath, newContent);
65+
console.log('log: Successfully updated AppDelegate.m/mm.');
66+
}
67+
68+
async function setupSwift(appDelegatePath, projectDir, projectName) {
69+
const bridgingHeaderPath = await ensureBridgingHeader(projectDir, projectName);
70+
if (!bridgingHeaderPath) {
71+
console.log('log: Failed to create or find bridging header.');
72+
return;
73+
}
74+
75+
const appDelegateContent = fs.readFileSync(appDelegatePath, 'utf-8');
76+
const newContent = modifySwiftAppDelegate(appDelegateContent);
77+
fs.writeFileSync(appDelegatePath, newContent);
78+
console.log('log: Successfully updated AppDelegate.swift.');
79+
}
80+
81+
async function ensureBridgingHeader(projectDir, projectName) {
82+
const projectPath = path.join(projectDir, `${projectName}.xcodeproj`, 'project.pbxproj');
83+
const myProj = xcode.project(projectPath);
84+
85+
return new Promise((resolve, reject) => {
86+
myProj.parse(function (err) {
87+
if (err) {
88+
console.error(`Error parsing Xcode project: ${err}`);
89+
return reject(err);
90+
}
91+
92+
const bridgingHeaderRelativePath = `${projectName}/${projectName}-Bridging-Header.h`;
93+
const bridgingHeaderAbsolutePath = path.join(projectDir, bridgingHeaderRelativePath);
94+
95+
const configurations = myProj.pbxXCBuildConfigurationSection();
96+
for (const name in configurations) {
97+
const config = configurations[name];
98+
if (config.buildSettings) {
99+
config.buildSettings.SWIFT_OBJC_BRIDGING_HEADER = `"${bridgingHeaderRelativePath}"`;
100+
}
101+
}
102+
103+
if (!fs.existsSync(bridgingHeaderAbsolutePath)) {
104+
fs.mkdirSync(path.dirname(bridgingHeaderAbsolutePath), { recursive: true });
105+
fs.writeFileSync(bridgingHeaderAbsolutePath, '#import <CodePush/CodePush.h>\n');
106+
console.log(`log: Created bridging header at ${bridgingHeaderAbsolutePath}`);
107+
const groupKey = myProj.findPBXGroupKey({ name: projectName });
108+
myProj.addHeaderFile(bridgingHeaderRelativePath, { public: true }, groupKey);
109+
} else {
110+
const headerContent = fs.readFileSync(bridgingHeaderAbsolutePath, 'utf-8');
111+
if (!headerContent.includes('#import <CodePush/CodePush.h>')) {
112+
fs.appendFileSync(bridgingHeaderAbsolutePath, '\n#import <CodePush/CodePush.h>\n');
113+
console.log(`log: Updated bridging header at ${bridgingHeaderAbsolutePath}`);
114+
}
115+
}
116+
117+
fs.writeFileSync(projectPath, myProj.writeSync());
118+
console.log('log: Updated Xcode project with bridging header path.');
119+
resolve(bridgingHeaderAbsolutePath);
120+
});
121+
});
122+
}
123+
124+
module.exports = {
125+
initIos: initIos,
126+
modifyObjectiveCAppDelegate: modifyObjectiveCAppDelegate,
127+
modifySwiftAppDelegate: modifySwiftAppDelegate,
128+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const { initAndroid, modifyMainApplicationKt } = require('../initAndroid');
4+
5+
const tempDir = path.join(__dirname, 'temp');
6+
7+
// https://github.com/react-native-community/template/blob/0.80.2/template/android/app/src/main/java/com/helloworld/MainApplication.kt
8+
const ktTemplate = `
9+
package com.helloworld
10+
11+
import android.app.Application
12+
import com.facebook.react.PackageList
13+
import com.facebook.react.ReactApplication
14+
import com.facebook.react.ReactHost
15+
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
16+
import com.facebook.react.ReactNativeHost
17+
import com.facebook.react.ReactPackage
18+
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
19+
import com.facebook.react.defaults.DefaultReactNativeHost
20+
21+
class MainApplication : Application(), ReactApplication {
22+
23+
override val reactNativeHost: ReactNativeHost =
24+
object : DefaultReactNativeHost(this) {
25+
override fun getPackages(): List<ReactPackage> =
26+
PackageList(this).packages.apply {
27+
// Packages that cannot be autolinked yet can be added manually here, for example:
28+
// add(MyReactNativePackage())
29+
}
30+
31+
override fun getJSMainModuleName(): String = "index"
32+
33+
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
34+
35+
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
36+
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
37+
}
38+
39+
override val reactHost: ReactHost
40+
get() = getDefaultReactHost(applicationContext, reactNativeHost)
41+
42+
override fun onCreate() {
43+
super.onCreate()
44+
loadReactNative(this)
45+
}
46+
}
47+
`;
48+
49+
describe('Android init command', () => {
50+
it('should correctly modify Kotlin MainApplication content', () => {
51+
const modifiedContent = modifyMainApplicationKt(ktTemplate);
52+
expect(modifiedContent).toContain('import com.microsoft.codepush.react.CodePush');
53+
expect(modifiedContent).toContain('override fun getJSBundleFile(): String = CodePush.getJSBundleFile()');
54+
});
55+
56+
it('should log a message and exit if MainApplication.java is found', async () => {
57+
const originalCwd = process.cwd();
58+
59+
fs.mkdirSync(tempDir, { recursive: true });
60+
process.chdir(tempDir);
61+
62+
// Arrange
63+
const javaAppDir = path.join(tempDir, 'android', 'app', 'src', 'main', 'java', 'com', 'helloworld');
64+
fs.mkdirSync(javaAppDir, { recursive: true });
65+
const javaFilePath = path.join(javaAppDir, 'MainApplication.java');
66+
const originalContent = '// Java file content';
67+
fs.writeFileSync(javaFilePath, originalContent);
68+
69+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {
70+
});
71+
72+
// Act
73+
await initAndroid();
74+
75+
// Assert
76+
expect(consoleSpy).toHaveBeenCalledWith('MainApplication.java is not supported. Please migrate to MainApplication.kt.');
77+
const finalContent = fs.readFileSync(javaFilePath, 'utf-8');
78+
expect(finalContent).toBe(originalContent); // Ensure file is not modified
79+
80+
consoleSpy.mockRestore();
81+
82+
process.chdir(originalCwd);
83+
fs.rmSync(tempDir, { recursive: true, force: true });
84+
});
85+
});

0 commit comments

Comments
 (0)