-
Notifications
You must be signed in to change notification settings - Fork 251
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1144 from bugsnag/feature/rn-cli-configure-cmd
[PLAT-4934] Add configure command to RN CLI
- Loading branch information
Showing
12 changed files
with
482 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
30 changes: 30 additions & 0 deletions
30
packages/react-native-cli/src/commands/ConfigureCommand.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import prompts from 'prompts' | ||
import logger from '../Logger' | ||
import onCancel from '../lib/OnCancel' | ||
import { addApiKey as addApiKeyAndroid } from '../lib/AndroidManifest' | ||
import { addApiKey as addApiKeyIos } from '../lib/InfoPlist' | ||
|
||
export default async function run (argv: string[], opts: Record<string, unknown>): Promise<void> { | ||
const projectRoot = process.cwd() | ||
|
||
try { | ||
const { apiKey } = await prompts({ | ||
type: 'text', | ||
name: 'apiKey', | ||
message: 'What is your Bugsnag API key?', | ||
validate: value => { | ||
return /[A-Fa-f0-9]{32}/.test(value) | ||
? true | ||
: 'API key is required. You can find it by going to https://app.bugsnag.com/settings/ > Projects' | ||
} | ||
}, { onCancel }) | ||
|
||
logger.info('Adding API key to AndroidManifest.xml') | ||
await addApiKeyAndroid(projectRoot, apiKey, logger) | ||
|
||
logger.info('Adding API key to Info.plist') | ||
await addApiKeyIos(projectRoot, apiKey, logger) | ||
} catch (e) { | ||
logger.error(e) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import path from 'path' | ||
import { Logger } from '../Logger' | ||
import { promises as fs } from 'fs' | ||
|
||
const DOCS_LINK = 'https://docs.bugsnag.com/platforms/react-native/react-native/#android' | ||
const UNLOCATED_PROJ_MSG = `The Android configuration was not in the expected location and so couldn't be updated automatically. | ||
Add your API key to the AndroidManifest.xml in your project. | ||
See ${DOCS_LINK} for more information` | ||
|
||
const MATCH_FAIL_MSG = `The project's AndroidManifest.xml couldn't be updated automatically as it was in an unexpected format. | ||
Add your API key to the AndroidManifest.xml in your project. | ||
See ${DOCS_LINK} for more information` | ||
|
||
const APP_END_REGEX = /\n\s*<\/application>/ | ||
|
||
export async function addApiKey (projectRoot: string, apiKey: string, logger: Logger): Promise<void> { | ||
const manifestPath = path.join(projectRoot, 'android', 'app', 'src', 'main', 'AndroidManifest.xml') | ||
try { | ||
const manifest = await fs.readFile(manifestPath, 'utf8') | ||
const activityStartMatch = /(\s*)<activity/.exec(manifest) | ||
const appEndMatch = APP_END_REGEX.exec(manifest) | ||
if (manifest.includes('com.bugsnag.android.API_KEY')) { | ||
logger.warn('API key is already present, skipping') | ||
return | ||
} | ||
if (!activityStartMatch || !appEndMatch) { | ||
logger.warn(MATCH_FAIL_MSG) | ||
return | ||
} | ||
const activityStartIndent = activityStartMatch[1] | ||
const updatedManifest = manifest.replace( | ||
APP_END_REGEX, | ||
`${activityStartIndent}<meta-data android:name="com.bugsnag.android.API_KEY" android:value="${apiKey}" />${appEndMatch}` | ||
) | ||
await fs.writeFile(manifestPath, updatedManifest, 'utf8') | ||
logger.success('Updated AndroidManifest.xml') | ||
} catch (e) { | ||
logger.warn(UNLOCATED_PROJ_MSG) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { Logger } from '../Logger' | ||
import plist from 'plist' | ||
import path from 'path' | ||
import { promises as fs } from 'fs' | ||
|
||
const DOCS_LINK = 'https://docs.bugsnag.com/platforms/react-native/react-native/#ios' | ||
const UNLOCATED_PROJ_MSG = `The Xcode configuration was not in the expected location and so couldn't be updated automatically. | ||
Add your API key to the Info.plist in your project. | ||
See ${DOCS_LINK} for more information` | ||
|
||
const PLIST_FAIL_MSG = `The project's Info.plist couldn't be updated automatically. The plist file may not be valid XML. | ||
Add your API key to the Info.plist in your project manually. | ||
See ${DOCS_LINK} for more information` | ||
|
||
export async function addApiKey (projectRoot: string, apiKey: string, logger: Logger): Promise<void> { | ||
const iosDir = path.join(projectRoot, 'ios') | ||
let xcodeprojDir | ||
try { | ||
xcodeprojDir = (await fs.readdir(iosDir)).find(p => p.endsWith('.xcodeproj')) | ||
if (!xcodeprojDir) { | ||
logger.warn(UNLOCATED_PROJ_MSG) | ||
return | ||
} | ||
} catch (e) { | ||
logger.warn(UNLOCATED_PROJ_MSG) | ||
return | ||
} | ||
const plistPath = path.join(iosDir, xcodeprojDir.replace(/\.xcodeproj$/, ''), 'Info.plist') | ||
try { | ||
const infoPlist = plist.parse(await fs.readFile(plistPath, 'utf8')) | ||
infoPlist.bugsnag = { apiKey } | ||
await fs.writeFile(plistPath, `${plist.build(infoPlist, { indent: '\t', indentSize: 1, offset: -1 })}\n`, 'utf8') | ||
logger.success('Updating Info.plist') | ||
} catch (e) { | ||
logger.warn(PLIST_FAIL_MSG) | ||
} | ||
} |
90 changes: 90 additions & 0 deletions
90
packages/react-native-cli/src/lib/__test__/AndroidManifest.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import { addApiKey } from '../AndroidManifest' | ||
import logger from '../../Logger' | ||
import path from 'path' | ||
import { promises as fs } from 'fs' | ||
|
||
async function loadFixture (fixture: string) { | ||
return jest.requireActual('fs').promises.readFile(fixture, 'utf8') | ||
} | ||
|
||
async function generateNotFoundError () { | ||
try { | ||
await jest.requireActual('fs').promises.readFile(path.join(__dirname, 'does-not-exist.txt')) | ||
} catch (e) { | ||
return e | ||
} | ||
} | ||
|
||
jest.mock('../../Logger') | ||
jest.mock('fs', () => { | ||
return { promises: { readFile: jest.fn(), writeFile: jest.fn() } } | ||
}) | ||
|
||
afterEach(() => jest.resetAllMocks()) | ||
|
||
test('addApiKey(): success', async () => { | ||
const androidManifest = await loadFixture(path.join(__dirname, 'fixtures', 'AndroidManifest-before.xml')) | ||
const readFileMock = fs.readFile as jest.MockedFunction<typeof fs.readFile> | ||
readFileMock.mockResolvedValue(androidManifest) | ||
|
||
const writeFileMock = fs.writeFile as jest.MockedFunction<typeof fs.writeFile> | ||
writeFileMock.mockResolvedValue() | ||
|
||
await addApiKey('/random/path', 'API_KEY_GOES_HERE', logger) | ||
expect(readFileMock).toHaveBeenCalledWith('/random/path/android/app/src/main/AndroidManifest.xml', 'utf8') | ||
expect(writeFileMock).toHaveBeenCalledWith( | ||
'/random/path/android/app/src/main/AndroidManifest.xml', | ||
await loadFixture(path.join(__dirname, 'fixtures', 'AndroidManifest-after.xml')), | ||
'utf8' | ||
) | ||
}) | ||
|
||
test('addApiKey(): already present', async () => { | ||
const androidManifest = await loadFixture(path.join(__dirname, 'fixtures', 'AndroidManifest-after.xml')) | ||
const readFileMock = fs.readFile as jest.MockedFunction<typeof fs.readFile> | ||
readFileMock.mockResolvedValue(androidManifest) | ||
|
||
const writeFileMock = fs.writeFile as jest.MockedFunction<typeof fs.writeFile> | ||
writeFileMock.mockResolvedValue() | ||
|
||
await addApiKey('/random/path', 'API_KEY_GOES_HERE', logger) | ||
expect(readFileMock).toHaveBeenCalledWith('/random/path/android/app/src/main/AndroidManifest.xml', 'utf8') | ||
expect(writeFileMock).not.toHaveBeenCalled() | ||
|
||
expect(logger.warn).toHaveBeenCalledWith('API key is already present, skipping') | ||
}) | ||
|
||
test('addApiKey(): self closing application tag', async () => { | ||
const androidManifest = | ||
`<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.bugsnagreactnativeclitest"> | ||
<application/> | ||
</manifest>` | ||
|
||
const readFileMock = fs.readFile as jest.MockedFunction<typeof fs.readFile> | ||
readFileMock.mockResolvedValue(androidManifest) | ||
|
||
const writeFileMock = fs.writeFile as jest.MockedFunction<typeof fs.writeFile> | ||
|
||
await addApiKey('/random/path', 'API_KEY_GOES_HERE', logger) | ||
expect(readFileMock).toHaveBeenCalledWith('/random/path/android/app/src/main/AndroidManifest.xml', 'utf8') | ||
expect(writeFileMock).not.toHaveBeenCalled() | ||
|
||
expect(logger.warn).toHaveBeenCalledWith( | ||
expect.stringContaining('The project\'s AndroidManifest.xml couldn\'t be updated automatically as it was in an unexpected format.') | ||
) | ||
}) | ||
|
||
test('addApiKey(): missing file', async () => { | ||
const readFileMock = fs.readFile as jest.MockedFunction<typeof fs.readFile> | ||
readFileMock.mockRejectedValue(await generateNotFoundError()) | ||
|
||
const writeFileMock = fs.writeFile as jest.MockedFunction<typeof fs.writeFile> | ||
|
||
await addApiKey('/random/path', 'API_KEY_GOES_HERE', logger) | ||
expect(readFileMock).toHaveBeenCalledWith('/random/path/android/app/src/main/AndroidManifest.xml', 'utf8') | ||
expect(writeFileMock).not.toHaveBeenCalled() | ||
|
||
expect(logger.warn).toHaveBeenCalledWith( | ||
expect.stringContaining('The Android configuration was not in the expected location') | ||
) | ||
}) |
91 changes: 91 additions & 0 deletions
91
packages/react-native-cli/src/lib/__test__/InfoPlist.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import { addApiKey } from '../InfoPlist' | ||
import logger from '../../Logger' | ||
import path from 'path' | ||
import { promises as fs } from 'fs' | ||
|
||
async function loadFixture (fixture: string) { | ||
return jest.requireActual('fs').promises.readFile(fixture, 'utf8') | ||
} | ||
|
||
async function generateNotFoundError () { | ||
try { | ||
await jest.requireActual('fs').promises.readFile(path.join(__dirname, 'does-not-exist.txt')) | ||
} catch (e) { | ||
return e | ||
} | ||
} | ||
|
||
jest.mock('../../Logger') | ||
jest.mock('fs', () => { | ||
return { promises: { readFile: jest.fn(), writeFile: jest.fn(), readdir: jest.fn() } } | ||
}) | ||
|
||
afterEach(() => jest.resetAllMocks()) | ||
|
||
test('addApiKey(): success', async () => { | ||
type readdir = (path: string) => Promise<string[]> | ||
const readdirMock = fs.readdir as unknown as jest.MockedFunction<readdir> | ||
readdirMock.mockResolvedValue(['BugsnagReactNativeCliTest.xcodeproj']) | ||
|
||
const infoPlist = await loadFixture(path.join(__dirname, 'fixtures', 'Info-before.plist')) | ||
const readFileMock = fs.readFile as jest.MockedFunction<typeof fs.readFile> | ||
readFileMock.mockResolvedValue(infoPlist) | ||
|
||
const writeFileMock = fs.writeFile as jest.MockedFunction<typeof fs.writeFile> | ||
writeFileMock.mockResolvedValue() | ||
|
||
await addApiKey('/random/path', 'API_KEY_GOES_HERE', logger) | ||
expect(readFileMock).toHaveBeenCalledWith('/random/path/ios/BugsnagReactNativeCliTest/Info.plist', 'utf8') | ||
expect(writeFileMock).toHaveBeenCalledWith( | ||
'/random/path/ios/BugsnagReactNativeCliTest/Info.plist', | ||
await loadFixture(path.join(__dirname, 'fixtures', 'Info-after.plist')), | ||
'utf8' | ||
) | ||
}) | ||
|
||
test('addApiKey(): unlocated project', async () => { | ||
type readdir = (path: string) => Promise<string[]> | ||
const readdirMock = fs.readdir as unknown as jest.MockedFunction<readdir> | ||
readdirMock.mockResolvedValue(['floop']) | ||
|
||
const readFileMock = fs.readFile as jest.MockedFunction<typeof fs.readFile> | ||
const writeFileMock = fs.writeFile as jest.MockedFunction<typeof fs.writeFile> | ||
|
||
await addApiKey('/random/path', 'API_KEY_GOES_HERE', logger) | ||
expect(readFileMock).not.toHaveBeenCalled() | ||
expect(writeFileMock).not.toHaveBeenCalled() | ||
|
||
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('The Xcode configuration was not in the expected location')) | ||
}) | ||
|
||
test('addApiKey(): unlocated project #2', async () => { | ||
type readdir = (path: string) => Promise<string[]> | ||
const readdirMock = fs.readdir as unknown as jest.MockedFunction<readdir> | ||
readdirMock.mockRejectedValue(await generateNotFoundError()) | ||
|
||
const readFileMock = fs.readFile as jest.MockedFunction<typeof fs.readFile> | ||
const writeFileMock = fs.writeFile as jest.MockedFunction<typeof fs.writeFile> | ||
|
||
await addApiKey('/random/path', 'API_KEY_GOES_HERE', logger) | ||
expect(readFileMock).not.toHaveBeenCalled() | ||
expect(writeFileMock).not.toHaveBeenCalled() | ||
|
||
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('The Xcode configuration was not in the expected location')) | ||
}) | ||
|
||
test('addApiKey(): bad xml', async () => { | ||
type readdir = (path: string) => Promise<string[]> | ||
const readdirMock = fs.readdir as unknown as jest.MockedFunction<readdir> | ||
readdirMock.mockResolvedValue(['BugsnagReactNativeCliTest.xcodeproj']) | ||
|
||
const infoPlist = 'not xml' | ||
const readFileMock = fs.readFile as jest.MockedFunction<typeof fs.readFile> | ||
readFileMock.mockResolvedValue(infoPlist) | ||
|
||
const writeFileMock = fs.writeFile as jest.MockedFunction<typeof fs.writeFile> | ||
|
||
await addApiKey('/random/path', 'API_KEY_GOES_HERE', logger) | ||
expect(writeFileMock).not.toHaveBeenCalled() | ||
|
||
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('The project\'s Info.plist couldn\'t be updated automatically.')) | ||
}) |
28 changes: 28 additions & 0 deletions
28
packages/react-native-cli/src/lib/__test__/fixtures/AndroidManifest-after.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||
package="com.bugsnagreactnativeclitest"> | ||
|
||
<uses-permission android:name="android.permission.INTERNET" /> | ||
|
||
<application | ||
android:name=".MainApplication" | ||
android:label="@string/app_name" | ||
android:icon="@mipmap/ic_launcher" | ||
android:roundIcon="@mipmap/ic_launcher_round" | ||
android:allowBackup="false" | ||
android:theme="@style/AppTheme"> | ||
<activity | ||
android:name=".MainActivity" | ||
android:label="@string/app_name" | ||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode" | ||
android:launchMode="singleTask" | ||
android:windowSoftInputMode="adjustResize"> | ||
<intent-filter> | ||
<action android:name="android.intent.action.MAIN" /> | ||
<category android:name="android.intent.category.LAUNCHER" /> | ||
</intent-filter> | ||
</activity> | ||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" /> | ||
<meta-data android:name="com.bugsnag.android.API_KEY" android:value="API_KEY_GOES_HERE" /> | ||
</application> | ||
|
||
</manifest> |
27 changes: 27 additions & 0 deletions
27
packages/react-native-cli/src/lib/__test__/fixtures/AndroidManifest-before.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||
package="com.bugsnagreactnativeclitest"> | ||
|
||
<uses-permission android:name="android.permission.INTERNET" /> | ||
|
||
<application | ||
android:name=".MainApplication" | ||
android:label="@string/app_name" | ||
android:icon="@mipmap/ic_launcher" | ||
android:roundIcon="@mipmap/ic_launcher_round" | ||
android:allowBackup="false" | ||
android:theme="@style/AppTheme"> | ||
<activity | ||
android:name=".MainActivity" | ||
android:label="@string/app_name" | ||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode" | ||
android:launchMode="singleTask" | ||
android:windowSoftInputMode="adjustResize"> | ||
<intent-filter> | ||
<action android:name="android.intent.action.MAIN" /> | ||
<category android:name="android.intent.category.LAUNCHER" /> | ||
</intent-filter> | ||
</activity> | ||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" /> | ||
</application> | ||
|
||
</manifest> |
Oops, something went wrong.