diff --git a/CHANGELOG.md b/CHANGELOG.md index 774b6f78d2..16b1d5d77b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ - Populate `trace_ids` in mobile replays so replays can be searched by trace ID ([#6786](https://github.com/getsentry/sentry-react-native/pull/6786)) +### Fixes + +- Preserve already-quoted React Native bundle script paths in the Expo iOS plugin ([#6796](https://github.com/getsentry/sentry-react-native/pull/6796)) + ### Dependencies - Bump Android SDK from v8.57.0 to v8.58.0 ([#6778](https://github.com/getsentry/sentry-react-native/pull/6778)) diff --git a/packages/core/plugin/src/withSentryIOS.ts b/packages/core/plugin/src/withSentryIOS.ts index 6c56dfbb84..a9947041fe 100644 --- a/packages/core/plugin/src/withSentryIOS.ts +++ b/packages/core/plugin/src/withSentryIOS.ts @@ -99,14 +99,15 @@ export function addSentryWithBundledScriptsToBundleShellScript( disableAutoUpload: boolean = false, ): string { const disableAutoUploadExport = disableAutoUpload ? `${SENTRY_DISABLE_AUTO_UPLOAD_EXPORT}\n` : ''; - // Match through end-of-line so the full react-native-xcode.sh invocation (which in the bare/monorepo - // templates is a backtick command substitution ending in `'"` + backtick) stays inside `${match}`. - // Both the Sentry script path and the original invocation are wrapped in double quotes so paths - // containing spaces are passed as single arguments instead of being word-split. See issue #6583. - return script.replace( - /^.*?(packager|scripts)\/react-native-xcode\.sh.*$/m, - (match: string) => `${disableAutoUploadExport}/bin/sh "${SENTRY_REACT_NATIVE_XCODE_PATH}" "${match}"`, - ); + // Match through end-of-line so command substitutions in Expo templates stay intact. + // Quote unquoted invocations so paths containing spaces remain one argument (#6583). + // Preserve existing outer double quotes: adding another pair would expose the + // command substitution's result to word splitting. + return script.replace(/^.*?(packager|scripts)\/react-native-xcode\.sh.*$/m, (match: string) => { + const invocation = match.trim(); + const argument = invocation.startsWith('"') && invocation.endsWith('"') ? invocation : `"${invocation}"`; + return `${disableAutoUploadExport}/bin/sh "${SENTRY_REACT_NATIVE_XCODE_PATH}" ${argument}`; + }); } export function getDebugFilesUploadScript(disableAutoUpload: boolean = false): string { diff --git a/packages/core/test/expo-plugin/modifyXcodeProject.test.ts b/packages/core/test/expo-plugin/modifyXcodeProject.test.ts index 3e7c33abec..ce5bc4e97c 100644 --- a/packages/core/test/expo-plugin/modifyXcodeProject.test.ts +++ b/packages/core/test/expo-plugin/modifyXcodeProject.test.ts @@ -530,6 +530,31 @@ export NODE_BINARY=node expect((result.match(/`/g) || []).length % 2).toBe(0); }); + it('preserves the quoted command substitution from the Expo template', () => { + const invocation = `"\`"$NODE_BINARY" --print "require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'"\`"`; + + const result = addSentryWithBundledScriptsToBundleShellScript(invocation); + + expect(result).toBe( + `/bin/sh "\`"$NODE_BINARY" --print "require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode.sh'"\`" ${invocation}`, + ); + }); + + it.each([ + ['"/project with spaces/scripts/react-native-xcode.sh"', '"/project with spaces/scripts/react-native-xcode.sh"'], + [ + ' "/project with spaces/scripts/react-native-xcode.sh" ', + '"/project with spaces/scripts/react-native-xcode.sh"', + ], + [' /project/scripts/react-native-xcode.sh ', '"/project/scripts/react-native-xcode.sh"'], + ])('generates a single pair of outer quotes without surrounding whitespace: %s', (invocation, expectedPath) => { + const result = addSentryWithBundledScriptsToBundleShellScript(invocation, true); + + expect(result).toBe( + `export SENTRY_DISABLE_AUTO_UPLOAD=true\n/bin/sh "\`"$NODE_BINARY" --print "require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode.sh'"\`" ${expectedPath}`, + ); + }); + it('quotes the debug files upload script path', () => { const result = getDebugFilesUploadScript(); expect(result).toContain('/bin/sh "`');