Skip to content

Commit

Permalink
fix(react-native-cli): ensure kotlin files are updated
Browse files Browse the repository at this point in the history
  • Loading branch information
yousif-bugsnag committed Mar 5, 2024
1 parent 030ac98 commit 8d2c830
Show file tree
Hide file tree
Showing 6 changed files with 275 additions and 21 deletions.
32 changes: 18 additions & 14 deletions packages/react-native-cli/src/lib/Insert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ const BUGSNAG_JAVA_IMPORT = 'import com.bugsnag.android.Bugsnag;'
const BUGSNAG_JAVA_INIT = 'Bugsnag.start(this);'
const JAVA_APP_ON_CREATE_REGEX = /(public void onCreate\s*\(\)\s*\{[^]*super\.onCreate\(\);(\s*))\S/

const BUGSNAG_KOTLIN_IMPORT = 'import com.bugsnag.android.Bugsnag'
const BUGSNAG_KOTLIN_INIT = 'Bugsnag.start(this)'
const KOTLIN_APP_ON_CREATE_REGEX = /(override fun onCreate\s*\(\)\s*\{[^]*super\.onCreate\(\)(\s*))\S/ // /(override fun onCreate\(savedInstanceState: Bundle\?\) \{\s*super\.onCreate\(savedInstanceState\)\s*\n)/

const DOCS_LINK = 'https://docs.bugsnag.com/platforms/react-native/react-native/#basic-configuration'
const FAIL_MSG = (filename: string) =>
`Failed to update "${filename}" automatically. The file may not exist or it may be in an unexpected format or location.
Expand Down Expand Up @@ -114,39 +118,39 @@ export async function insertAndroid (projectRoot: string, logger: Logger): Promi
cwd: javaDir
}))[0]

const relativeMainApplicationPathOther = (await asyncGlob('**/*/MainApplication', {
cwd: javaDir
}))[0]

const relativeMainApplicationPath = relativeMainApplicationPathJava || relativeMainApplicationPathKotlin || relativeMainApplicationPathOther
const relativeMainApplicationPath = relativeMainApplicationPathJava || relativeMainApplicationPathKotlin

if (!relativeMainApplicationPath) {
return logger.warn(FAIL_MSG('MainApplication.java, MainApplication.kt or MainApplication'))
return logger.warn(FAIL_MSG('MainApplication'))
}
mainApplicationPath = path.join(javaDir, relativeMainApplicationPath)
} catch (e) {
logger.warn(FAIL_MSG('MainApplication.java, MainApplication.kt or MainApplication'))
logger.warn(FAIL_MSG('MainApplication'))
return
}

try {
const mainApplication = await fs.readFile(mainApplicationPath, 'utf8')
const isKotlin = path.extname(mainApplicationPath) === '.kt'
const bugsnagImport = isKotlin ? BUGSNAG_KOTLIN_IMPORT : BUGSNAG_JAVA_IMPORT
const bugsnagInit = isKotlin ? BUGSNAG_KOTLIN_INIT : BUGSNAG_JAVA_INIT
const onCreateRegex = isKotlin ? KOTLIN_APP_ON_CREATE_REGEX : JAVA_APP_ON_CREATE_REGEX

if (mainApplication.includes(BUGSNAG_JAVA_IMPORT) || mainApplication.includes(BUGSNAG_JAVA_INIT)) {
const mainApplication = await fs.readFile(mainApplicationPath, 'utf8')
if (mainApplication.includes(bugsnagImport) || mainApplication.includes(bugsnagInit)) {
logger.warn('Bugsnag is already included, skipping')
return
}

const mainApplicationWithImport = mainApplication.replace('import', `${BUGSNAG_JAVA_IMPORT}\nimport`)
const onCreateRes = JAVA_APP_ON_CREATE_REGEX.exec(mainApplicationWithImport)
const mainApplicationWithImport = mainApplication.replace('import', `${bugsnagImport}\nimport`)
const onCreateRes = onCreateRegex.exec(mainApplicationWithImport)
if (!onCreateRes) {
logger.warn(FAIL_MSG('MainApplication.java, MainApplication.kt or MainApplication'))
logger.warn(FAIL_MSG('MainApplication'))
return
}

await fs.writeFile(mainApplicationPath, mainApplicationWithImport.replace(onCreateRes[1], `${onCreateRes[1]}${BUGSNAG_JAVA_INIT}${onCreateRes[2]}`), 'utf8')
await fs.writeFile(mainApplicationPath, mainApplicationWithImport.replace(onCreateRes[1], `${onCreateRes[1]}${bugsnagInit}${onCreateRes[2]}`), 'utf8')
logger.success('Done')
} catch (e) {
logger.error(FAIL_MSG('MainApplication.java, MainApplication.kt or MainApplication'))
logger.error(FAIL_MSG('MainApplication'))
}
}
78 changes: 71 additions & 7 deletions packages/react-native-cli/src/lib/__test__/Insert.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ test('insertIos(): no identifiable app launch method', async () => {
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('Failed to update "AppDelegate.mm" automatically.'))
})

test('insertAndroid(): success', async () => {
test('insertAndroid(): success (java)', async () => {
const globMock = glob as unknown as jest.MockedFunction<typeof glob>
globMock.mockImplementation((glob, opts, cb) => cb(null, ['com/bugsnagreactnativeclitest/MainApplication.java']))

Expand All @@ -207,7 +207,29 @@ test('insertAndroid(): success', async () => {
)
})

test('insertAndroid(): success, tolerates some differences in source', async () => {
test('insertAndroid(): success (kotlin)', async () => {
const globMock = glob as unknown as jest.MockedFunction<typeof glob>
globMock.mockImplementation((glob, opts, cb) => cb(null, ['com/bugsnagreactnativeclitest/MainApplication.kt']))

const mainApplication = await loadFixture(path.join(__dirname, 'fixtures', 'MainApplication-before.kt'))
const readFileMock = fs.readFile as jest.MockedFunction<typeof fs.readFile>
readFileMock.mockResolvedValue(mainApplication)

const writeFileMock = fs.writeFile as jest.MockedFunction<typeof fs.writeFile>

await insertAndroid('/random/path', logger)
expect(readFileMock).toHaveBeenCalledWith(
'/random/path/android/app/src/main/java/com/bugsnagreactnativeclitest/MainApplication.kt',
'utf8'
)
expect(writeFileMock).toHaveBeenCalledWith(
'/random/path/android/app/src/main/java/com/bugsnagreactnativeclitest/MainApplication.kt',
await loadFixture(path.join(__dirname, 'fixtures', 'MainApplication-after.kt')),
'utf8'
)
})

test('insertAndroid(): success, tolerates some differences in source (java)', async () => {
const globMock = glob as unknown as jest.MockedFunction<typeof glob>
globMock.mockImplementation((glob, opts, cb) => cb(null, ['com/bugsnagreactnativeclitest/MainApplication.java']))

Expand All @@ -229,7 +251,29 @@ test('insertAndroid(): success, tolerates some differences in source', async ()
)
})

test('insertAndroid(): already present', async () => {
test('insertAndroid(): success, tolerates some differences in source (kotlin)', async () => {
const globMock = glob as unknown as jest.MockedFunction<typeof glob>
globMock.mockImplementation((glob, opts, cb) => cb(null, ['com/bugsnagreactnativeclitest/MainApplication.kt']))

const mainApplication = await loadFixture(path.join(__dirname, 'fixtures', 'MainApplication-before-2.kt'))
const readFileMock = fs.readFile as jest.MockedFunction<typeof fs.readFile>
readFileMock.mockResolvedValue(mainApplication)

const writeFileMock = fs.writeFile as jest.MockedFunction<typeof fs.writeFile>

await insertAndroid('/random/path', logger)
expect(readFileMock).toHaveBeenCalledWith(
'/random/path/android/app/src/main/java/com/bugsnagreactnativeclitest/MainApplication.kt',
'utf8'
)
expect(writeFileMock).toHaveBeenCalledWith(
'/random/path/android/app/src/main/java/com/bugsnagreactnativeclitest/MainApplication.kt',
await loadFixture(path.join(__dirname, 'fixtures', 'MainApplication-after-2.kt')),
'utf8'
)
})

test('insertAndroid(): already present (java)', async () => {
const globMock = glob as unknown as jest.MockedFunction<typeof glob>
globMock.mockImplementation((glob, opts, cb) => cb(null, ['com/bugsnagreactnativeclitest/MainApplication.java']))

Expand All @@ -249,6 +293,26 @@ test('insertAndroid(): already present', async () => {
expect(logger.warn).toHaveBeenCalledWith('Bugsnag is already included, skipping')
})

test('insertAndroid(): already present (kotlin)', async () => {
const globMock = glob as unknown as jest.MockedFunction<typeof glob>
globMock.mockImplementation((glob, opts, cb) => cb(null, ['com/bugsnagreactnativeclitest/MainApplication.kt']))

const mainApplication = await loadFixture(path.join(__dirname, 'fixtures', 'MainApplication-after.kt'))
const readFileMock = fs.readFile as jest.MockedFunction<typeof fs.readFile>
readFileMock.mockResolvedValue(mainApplication)

const writeFileMock = fs.writeFile as jest.MockedFunction<typeof fs.writeFile>

await insertAndroid('/random/path', logger)
expect(readFileMock).toHaveBeenCalledWith(
'/random/path/android/app/src/main/java/com/bugsnagreactnativeclitest/MainApplication.kt',
'utf8'
)
expect(writeFileMock).not.toHaveBeenCalled()

expect(logger.warn).toHaveBeenCalledWith('Bugsnag is already included, skipping')
})

test('insertAndroid(): failure to locate file', async () => {
const globMock = glob as unknown as jest.MockedFunction<typeof glob>
globMock.mockImplementation((glob, opts, cb) => cb(null, ['com/bugsnagreactnativeclitest/MainApplication.java']))
Expand All @@ -265,7 +329,7 @@ test('insertAndroid(): failure to locate file', async () => {
)
expect(writeFileMock).not.toHaveBeenCalled()

expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('Failed to update "MainApplication.java, MainApplication.kt or MainApplication" automatically.'))
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('Failed to update "MainApplication" automatically.'))
})

test('insertAndroid(): failure to locate package directory', async () => {
Expand All @@ -279,7 +343,7 @@ test('insertAndroid(): failure to locate package directory', async () => {
expect(readFileMock).not.toHaveBeenCalled()
expect(writeFileMock).not.toHaveBeenCalled()

expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('Failed to update "MainApplication.java, MainApplication.kt or MainApplication" automatically.'))
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('Failed to update "MainApplication" automatically.'))
})

test('insertAndroid(): project directory error', async () => {
Expand All @@ -293,7 +357,7 @@ test('insertAndroid(): project directory error', async () => {
expect(readFileMock).not.toHaveBeenCalled()
expect(writeFileMock).not.toHaveBeenCalled()

expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('Failed to update "MainApplication.java, MainApplication.kt or MainApplication" automatically.'))
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('Failed to update "MainApplication" automatically.'))
})

test('insertAndroid(): no identifiable onCreate method', async () => {
Expand All @@ -311,5 +375,5 @@ test('insertAndroid(): no identifiable onCreate method', async () => {
)
expect(writeFileMock).not.toHaveBeenCalled()

expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('Failed to update "MainApplication.java, MainApplication.kt or MainApplication" automatically.'))
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('Failed to update "MainApplication" automatically.'))
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.bugsnagreactnativeclitest

import com.bugsnag.android.Bugsnag
import android.app.Application
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.flipper.ReactNativeFlipper
import com.facebook.soloader.SoLoader

class MainApplication : Application(), ReactApplication {

override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
}

override fun getJSMainModuleName(): String = "index"

override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG

override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
}

override val reactHost: ReactHost
get() = getDefaultReactHost(this.applicationContext, reactNativeHost)

override fun onCreate() {
// something between curly brace and super call
super.onCreate()
Bugsnag.start(this)
SoLoader.init(this, false)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
load()
}
ReactNativeFlipper.initializeFlipper(this, reactNativeHost.reactInstanceManager)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.bugsnagreactnativeclitest

import com.bugsnag.android.Bugsnag
import android.app.Application
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.flipper.ReactNativeFlipper
import com.facebook.soloader.SoLoader

class MainApplication : Application(), ReactApplication {

override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
}

override fun getJSMainModuleName(): String = "index"

override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG

override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
}

override val reactHost: ReactHost
get() = getDefaultReactHost(this.applicationContext, reactNativeHost)

override fun onCreate() {
super.onCreate()
Bugsnag.start(this)
SoLoader.init(this, false)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
load()
}
ReactNativeFlipper.initializeFlipper(this, reactNativeHost.reactInstanceManager)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.bugsnagreactnativeclitest

import android.app.Application
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.flipper.ReactNativeFlipper
import com.facebook.soloader.SoLoader

class MainApplication : Application(), ReactApplication {

override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
}

override fun getJSMainModuleName(): String = "index"

override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG

override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
}

override val reactHost: ReactHost
get() = getDefaultReactHost(this.applicationContext, reactNativeHost)

override fun onCreate() {
// something between curly brace and super call
super.onCreate()
SoLoader.init(this, false)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
load()
}
ReactNativeFlipper.initializeFlipper(this, reactNativeHost.reactInstanceManager)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.bugsnagreactnativeclitest

import android.app.Application
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.flipper.ReactNativeFlipper
import com.facebook.soloader.SoLoader

class MainApplication : Application(), ReactApplication {

override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
}

override fun getJSMainModuleName(): String = "index"

override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG

override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
}

override val reactHost: ReactHost
get() = getDefaultReactHost(this.applicationContext, reactNativeHost)

override fun onCreate() {
super.onCreate()
SoLoader.init(this, false)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
load()
}
ReactNativeFlipper.initializeFlipper(this, reactNativeHost.reactInstanceManager)
}
}

0 comments on commit 8d2c830

Please sign in to comment.