-
Notifications
You must be signed in to change notification settings - Fork 251
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
[PLAT-4934] Add configure command to RN CLI #1144
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This works fine with react native's project template but may break if developers reorganise their project structure. Determining the path 100% correctly would involve parsing the Xcode project and finding the
INFOPLIST_FILE
build setting for the app target... tricky to implement, so this current solution is probably good enough!