Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(react-native-cli): ensure kotlin files are updated #2092

Merged
merged 2 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions packages/react-native-cli/src/lib/Gradle.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Logger } from '../Logger'
import { promises as fs } from 'fs'
import path from 'path'
import { detectInstalledVersion } from './Npm'
import semver from 'semver'

const GRADLE_PLUGIN_IMPORT = (version: string) => `classpath("com.bugsnag:bugsnag-android-gradle-plugin:${version}")`
const GRADLE_PLUGIN_IMPORT_REGEX = /classpath\(["']com\.bugsnag:bugsnag-android-gradle-plugin:.*["']\)/
Expand All @@ -12,6 +14,9 @@ const BUGSNAG_CONFIGURATION_BLOCK = 'bugsnag {\n}\n'
const BUGSNAG_CONFIGURATION_BLOCK_REGEX = /^\s*bugsnag {[^}]*?}/m
const UPLOAD_ENDPOINT_REGEX = /^\s*bugsnag {[^}]*endpoint[^}]*?}/m
const BUILD_ENDPOINT_REGEX = /^\s*bugsnag {[^}]*releasesEndpoint[^}]*?}/m
const GRADLE_VERSION_FAIL_MSG = `Cannot determine an appropriate version of the Bugsnag Android Gradle plugin for use in this project.

Please see ${DOCS_LINK} for information on Gradle and the Android Gradle Plugin (AGP) compatibility`

export async function getSuggestedBugsnagGradleVersion (projectRoot: string, logger: Logger): Promise<string> {
let fileContents: string
Expand All @@ -30,14 +35,24 @@ export async function getSuggestedBugsnagGradleVersion (projectRoot: string, log
} else if (major === 7) {
return '7.+'
} else {
const versionMatchResult = fileContents.match(/classpath\(["']com.android.tools.build:gradle["']\)/)
if (versionMatchResult) {
// if the AGP version isn't set explicitly in the build.gradle file,
// try to suggest a version based on the detected react-native version
const noVersionMatchResult = fileContents.match(/classpath\(["']com.android.tools.build:gradle["']\)/)
let reactNativeVersion
try {
reactNativeVersion = await detectInstalledVersion('react-native', projectRoot)
} catch (e) {}

if (!noVersionMatchResult || !reactNativeVersion) {
logger.warn(GRADLE_VERSION_FAIL_MSG)
return ''
}

// RN 0.73+ requires AGP 8.+
if (semver.lt(reactNativeVersion, '0.73.0')) {
return '7.+'
} else {
logger.warn(`Cannot determine an appropriate version of the Bugsnag Android Gradle plugin for use in this project.

Please see ${DOCS_LINK} for information on Gradle and the Android Gradle Plugin (AGP) compatibility`)
return ''
return '8.+'
}
}
}
Expand Down
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/

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'))
}
}
28 changes: 28 additions & 0 deletions packages/react-native-cli/src/lib/__test__/Gradle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -512,3 +512,31 @@ test('getSuggestedBugsnagGradleVersion(): success with bracketed AGP version', a
const version = await getSuggestedBugsnagGradleVersion('/random/path', logger)
expect(version).toBe('7.+')
})

test('getSuggestedBugsnagGradleVersion(): success with unspecified AGP version', async () => {
const buildGradle = await loadFixture(path.join(__dirname, 'fixtures', 'root-build-before-without-agp-version.gradle'))

const readFileMock = fs.readFile as jest.MockedFunction<typeof fs.readFile>
readFileMock.mockResolvedValueOnce(buildGradle)
readFileMock.mockResolvedValueOnce('{"dependencies": { "react-native": "0.73.0"} }')

let version = await getSuggestedBugsnagGradleVersion('/random/path', logger)
expect(version).toBe('8.+')

readFileMock.mockResolvedValueOnce(buildGradle)
readFileMock.mockResolvedValueOnce('{"dependencies": { "react-native": "0.72.11"} }')

version = await getSuggestedBugsnagGradleVersion('/random/path', logger)
expect(version).toBe('7.+')
})

test('getSuggestedBugsnagGradleVersion(): null with unspecified AGP and react native versions', async () => {
const buildGradle = await loadFixture(path.join(__dirname, 'fixtures', 'root-build-before-without-agp-version.gradle'))

const readFileMock = fs.readFile as jest.MockedFunction<typeof fs.readFile>
readFileMock.mockResolvedValueOnce(buildGradle)
readFileMock.mockResolvedValueOnce('')

const version = await getSuggestedBugsnagGradleVersion('/random/path', logger)
expect(version).toBe('')
})
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)
}
}
Loading
Loading