diff --git a/packages/react-native-cli/src/lib/Xcode.ts b/packages/react-native-cli/src/lib/Xcode.ts index ea33f7143c..581e061b75 100644 --- a/packages/react-native-cli/src/lib/Xcode.ts +++ b/packages/react-native-cli/src/lib/Xcode.ts @@ -6,10 +6,9 @@ import xcode, { Project } from 'xcode' const DOCS_LINK = 'https://docs.bugsnag.com/platforms/react-native/react-native/showing-full-stacktraces/#ios' const UNLOCATED_PROJ_MSG = `The Xcode project was not in the expected location and so couldn't be updated automatically. -Update the "Bundle React Native Code And Images" build phase with the following environment variables: -export EXTRA_PACKAGER_ARGS="--sourcemap-output $TMPDIR/$(md5 -qs "$CONFIGURATION_BUILD_DIR")-main.jsbundle.map"" +Please see ${DOCS_LINK} for more information` -See ${DOCS_LINK} for more information` +const EXTRA_PACKAGER_ARGS = ['"$(SRCROOT)/.xcode.env.local"', '"$(SRCROOT)/.xcode.env"'] export async function updateXcodeProject (projectRoot: string, endpoint: string|undefined, logger: Logger) { const iosDir = path.join(projectRoot, 'ios') @@ -32,8 +31,8 @@ export async function updateXcodeProject (projectRoot: string, endpoint: string| const buildPhaseMap = proj?.hash?.project?.objects?.PBXShellScriptBuildPhase || [] logger.info('Ensuring React Native build phase outputs source maps') - const didUpdate = await updateXcodeEnv(iosDir, logger) + const didUpdate = await updateBuildReactNativeTask(buildPhaseMap, iosDir, logger) logger.info('Adding build phase to upload source maps to Bugsnag') const didAdd = await addUploadSourceMapsTask(proj, buildPhaseMap, endpoint, logger) @@ -45,6 +44,31 @@ export async function updateXcodeProject (projectRoot: string, endpoint: string| logger.success('Written changes to Xcode project') } +async function updateBuildReactNativeTask (buildPhaseMap: Record>, iosDir: string, logger: Logger): Promise { + let didAnythingUpdate = false + for (const shellBuildPhaseKey in buildPhaseMap) { + const phase = buildPhaseMap[shellBuildPhaseKey] + // The shell script can vary slightly... Vanilla RN projects contain + // ../node_modules/react-native/scripts/react-native-xcode.sh + // and ejected Expo projects contain + // `node --print "require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'"` + // so we need a little leniency + if (typeof phase.shellScript === 'string' && phase.shellScript.includes('/react-native-xcode.sh')) { + let didThisUpdate + [phase.inputPaths, didThisUpdate] = addExtraInputFiles(shellBuildPhaseKey, phase.inputPaths as string[], logger) + if (didThisUpdate) { + didAnythingUpdate = true + } + } + } + + if (didAnythingUpdate) { + await updateXcodeEnv(iosDir, logger) + } + + return didAnythingUpdate +} + async function addUploadSourceMapsTask ( proj: Project, buildPhaseMap: Record>, @@ -72,35 +96,42 @@ async function addUploadSourceMapsTask ( return true } +function addExtraInputFiles (phaseId: string, existingInputFiles: string[], logger: Logger): [string[], boolean] { + if (arrayContainsElements(existingInputFiles, EXTRA_PACKAGER_ARGS)) { + logger.warn(`The "Bundle React Native Code and Images" build phase (${phaseId}) already includes the required arguments`) + return [existingInputFiles, false] + } + return [EXTRA_PACKAGER_ARGS.concat(existingInputFiles), true] +} + +function arrayContainsElements (mainArray: any[], subArray: any[]): boolean { + return subArray.every(element => mainArray.some(mainElement => mainElement === element)) +} + async function updateXcodeEnv (iosDir: string, logger: Logger): Promise { const searchString = 'SOURCEMAP_FILE=' const sourceMapFilePath = 'ios/build/sourcemaps/main.jsbundle.map' const envFilePath = path.join(iosDir, '.xcode.env') try { - // Check if the file exists - await fs.stat(envFilePath) + await fs.readFile(envFilePath) - // If the file exists, read its content const xcodeEnvData = await fs.readFile(envFilePath, 'utf8') - if (xcodeEnvData.includes(searchString)) { + if (xcodeEnvData?.includes(searchString)) { logger.warn(`The .xcode.env file already contains a section for "${searchString}"`) return false } else { - // Append the new data to the existing content const newData = `${xcodeEnvData}\n\n# React Native Source Map File\nexport ${searchString}${sourceMapFilePath}` await fs.writeFile(envFilePath, newData, 'utf8') return true } } catch (error) { - // If the file doesn't exist, create it if (error.code === 'ENOENT') { const newData = `export NODE_BINARY=$(command -v node)\n# React Native Source Map File\nexport ${searchString}${sourceMapFilePath}` await fs.writeFile(envFilePath, newData, 'utf8') return true } else { - // Other error occurred console.error(`Error updating .xcode.env file: ${error.message}`) return false } diff --git a/packages/react-native-cli/src/lib/__test__/Xcode.test.ts b/packages/react-native-cli/src/lib/__test__/Xcode.test.ts index 5d0b0d838c..acd26d722a 100644 --- a/packages/react-native-cli/src/lib/__test__/Xcode.test.ts +++ b/packages/react-native-cli/src/lib/__test__/Xcode.test.ts @@ -52,10 +52,10 @@ test('updateXcodeProject(): success', async () => { await updateXcodeProject('/random/path', undefined, logger) expect(readFileSyncMock).toHaveBeenCalledWith('/random/path/ios/BugsnagReactNativeCliTest.xcodeproj/project.pbxproj', 'utf8') - expect(writeFileMock).toHaveBeenCalledTimes(1) + expect(writeFileMock).toHaveBeenCalledTimes(2) // the added build phase gets a generated build ID, so we need to figure out what that is before doing an exact string match - const matches = /([A-Z0-9]{24}) \/\* Upload source maps to Bugsnag \*\/ = \{/.exec(writeFileMock.mock.calls[0][1] as string) + const matches = /([A-Z0-9]{24}) \/\* Upload source maps to Bugsnag \*\/ = \{/.exec(writeFileMock.mock.calls[1] as unknown as string) if (!matches) throw new Error('Failed to detect build ID') const generatedPhaseId = matches[1] const expectedOutput = (await loadFixture(path.join(__dirname, 'fixtures', 'project-after.pbxproj'))) @@ -86,10 +86,10 @@ test('updateXcodeProject(): success with custom endpoint', async () => { await updateXcodeProject('/random/path', 'https://upload.example.com', logger) expect(readFileSyncMock).toHaveBeenCalledWith('/random/path/ios/BugsnagReactNativeCliTest.xcodeproj/project.pbxproj', 'utf8') - expect(writeFileMock).toHaveBeenCalledTimes(1) + expect(writeFileMock).toHaveBeenCalledTimes(2) // the added build phase gets a generated build ID, so we need to figure out what that is before doing an exact string match - const matches = /([A-Z0-9]{24}) \/\* Upload source maps to Bugsnag \*\/ = \{/.exec(writeFileMock.mock.calls[0][1] as string) + const matches = /([A-Z0-9]{24}) \/\* Upload source maps to Bugsnag \*\/ = \{/.exec(writeFileMock.mock.calls[1] as unknown as string) if (!matches) { throw new Error('Failed to detect build ID') } diff --git a/packages/react-native-cli/src/lib/__test__/fixtures/project-after-with-endpoint.pbxproj b/packages/react-native-cli/src/lib/__test__/fixtures/project-after-with-endpoint.pbxproj index e7fd6eaf5d..cccdb23a67 100644 --- a/packages/react-native-cli/src/lib/__test__/fixtures/project-after-with-endpoint.pbxproj +++ b/packages/react-native-cli/src/lib/__test__/fixtures/project-after-with-endpoint.pbxproj @@ -370,6 +370,8 @@ files = ( ); inputPaths = ( + "$(SRCROOT)/.xcode.env.local", + "$(SRCROOT)/.xcode.env", ); name = "Bundle React Native code and images"; outputPaths = ( @@ -384,6 +386,8 @@ files = ( ); inputPaths = ( + "$(SRCROOT)/.xcode.env.local", + "$(SRCROOT)/.xcode.env", ); name = "Bundle React Native Code And Images"; outputPaths = ( diff --git a/packages/react-native-cli/src/lib/__test__/fixtures/project-after.pbxproj b/packages/react-native-cli/src/lib/__test__/fixtures/project-after.pbxproj index e7fd6eaf5d..cccdb23a67 100644 --- a/packages/react-native-cli/src/lib/__test__/fixtures/project-after.pbxproj +++ b/packages/react-native-cli/src/lib/__test__/fixtures/project-after.pbxproj @@ -370,6 +370,8 @@ files = ( ); inputPaths = ( + "$(SRCROOT)/.xcode.env.local", + "$(SRCROOT)/.xcode.env", ); name = "Bundle React Native code and images"; outputPaths = ( @@ -384,6 +386,8 @@ files = ( ); inputPaths = ( + "$(SRCROOT)/.xcode.env.local", + "$(SRCROOT)/.xcode.env", ); name = "Bundle React Native Code And Images"; outputPaths = (