From 50d3fa8cca199c70b39935d2d2b9d2e9c1fc1424 Mon Sep 17 00:00:00 2001 From: Eliot Gevers Date: Mon, 28 Sep 2026 22:53:50 +0200 Subject: [PATCH 1/2] fix(ios): Preserve quoted Expo bundle script invocations --- CHANGELOG.md | 4 +++ packages/core/plugin/src/withSentryIOS.ts | 15 ++++++----- .../expo-plugin/modifyXcodeProject.test.ts | 25 +++++++++++++++++++ 3 files changed, 36 insertions(+), 8 deletions(-) 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..6ba6a98303 100644 --- a/packages/core/plugin/src/withSentryIOS.ts +++ b/packages/core/plugin/src/withSentryIOS.ts @@ -99,14 +99,13 @@ 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}"`, - ); + // Keep the full invocation, including the closing backtick in Expo templates. + // Preserve existing double quotes: adding a second pair would expose spaces 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..475e7e9650 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/scripts/react-native-xcode.sh ', + ])('passes an optionally indented invocation as one quoted argument: %s', invocation => { + const result = addSentryWithBundledScriptsToBundleShellScript(invocation, true); + const expectedPath = invocation.includes('with spaces') + ? '"/project with spaces/scripts/react-native-xcode.sh"' + : '"/project/scripts/react-native-xcode.sh"'; + + 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 "`'); From 0f49028cb1104fb9af34ca9fcfc7bd4a1f9eef96 Mon Sep 17 00:00:00 2001 From: Eliot Gevers Date: Mon, 28 Sep 2026 23:03:46 +0200 Subject: [PATCH 2/2] chore: Refine comments and test cases --- packages/core/plugin/src/withSentryIOS.ts | 6 ++++-- .../test/expo-plugin/modifyXcodeProject.test.ts | 14 +++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/core/plugin/src/withSentryIOS.ts b/packages/core/plugin/src/withSentryIOS.ts index 6ba6a98303..a9947041fe 100644 --- a/packages/core/plugin/src/withSentryIOS.ts +++ b/packages/core/plugin/src/withSentryIOS.ts @@ -99,8 +99,10 @@ export function addSentryWithBundledScriptsToBundleShellScript( disableAutoUpload: boolean = false, ): string { const disableAutoUploadExport = disableAutoUpload ? `${SENTRY_DISABLE_AUTO_UPLOAD_EXPORT}\n` : ''; - // Keep the full invocation, including the closing backtick in Expo templates. - // Preserve existing double quotes: adding a second pair would expose spaces to word splitting. + // 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}"`; diff --git a/packages/core/test/expo-plugin/modifyXcodeProject.test.ts b/packages/core/test/expo-plugin/modifyXcodeProject.test.ts index 475e7e9650..ce5bc4e97c 100644 --- a/packages/core/test/expo-plugin/modifyXcodeProject.test.ts +++ b/packages/core/test/expo-plugin/modifyXcodeProject.test.ts @@ -541,14 +541,14 @@ export NODE_BINARY=node }); it.each([ - '"/project with spaces/scripts/react-native-xcode.sh"', - ' "/project with spaces/scripts/react-native-xcode.sh" ', - ' /project/scripts/react-native-xcode.sh ', - ])('passes an optionally indented invocation as one quoted argument: %s', invocation => { + ['"/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); - const expectedPath = invocation.includes('with spaces') - ? '"/project with spaces/scripts/react-native-xcode.sh"' - : '"/project/scripts/react-native-xcode.sh"'; 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}`,