diff --git a/CHANGELOG.md b/CHANGELOG.md index d712d13913..15bbbb0040 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - Fix duplicate JS error reporting on iOS New Architecture when the native SDK is initialized early via `sentry.options.json` ("Capture App Start Errors"). It's done by applying the `ExceptionsManager.reportException` C++ wrapper filter in both init paths ([#6145](https://github.com/getsentry/sentry-react-native/pull/6145)) - Fix boolean options from `sentry.options.json` being ignored on Android when using `RNSentrySDK.init` ([#6130](https://github.com/getsentry/sentry-react-native/pull/6130)) +- Fix `sentry-expo-upload-sourcemaps` failing for projects with `devEngines.packageManager` set to non-npm managers ([#6155](https://github.com/getsentry/sentry-react-native/pull/6155)) ### Dependencies diff --git a/packages/core/test/scripts/expo-upload-sourcemaps.test.ts b/packages/core/test/scripts/expo-upload-sourcemaps.test.ts index 61973a54a8..c53474da77 100644 --- a/packages/core/test/scripts/expo-upload-sourcemaps.test.ts +++ b/packages/core/test/scripts/expo-upload-sourcemaps.test.ts @@ -295,19 +295,6 @@ process.exit(exitCode); }); describe('sentry.properties fallback', () => { - let mockNpxScript: string; - let mockBinDir: string; - - beforeEach(() => { - // Create a mock npx that makes `expo config --json` fail fast - // so the script falls through to the sentry.properties fallback - mockBinDir = path.join(tempDir, 'mock-bin'); - fs.mkdirSync(mockBinDir, { recursive: true }); - mockNpxScript = path.join(mockBinDir, 'npx'); - fs.writeFileSync(mockNpxScript, '#!/usr/bin/env node\nprocess.exit(1);\n'); - fs.chmodSync(mockNpxScript, '755'); - }); - const createSentryProperties = (dir: string, content: string) => { fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(path.join(dir, 'sentry.properties'), content); @@ -320,8 +307,7 @@ process.exit(exitCode); const defaultEnv = { SENTRY_AUTH_TOKEN: 'test-token', SENTRY_CLI_EXECUTABLE: mockSentryCliScript, - // Put mock npx first in PATH so expo config fails fast - PATH: `${mockBinDir}:${process.env.PATH}`, + NODE_PATH: path.join(tempDir, 'nonexistent_modules'), }; const result = spawnSync(process.execPath, [EXPO_UPLOAD_SCRIPT, outputDir], { @@ -415,28 +401,30 @@ process.exit(exitCode); expoConfig: Record, env: Record = {}, ): { stdout: string; stderr: string; exitCode: number } => { - // Create a mock npx that outputs the given expo config as JSON - const mockBinDir = path.join(tempDir, 'mock-bin-expo'); - fs.mkdirSync(mockBinDir, { recursive: true }); - const mockNpxScript = path.join(mockBinDir, 'npx'); - // The mock npx script outputs the config JSON when called with 'expo config --json' + // Create a mock expo/bin/cli that outputs the given expo config as JSON + const mockExpoCliDir = path.join(tempDir, 'node_modules', 'expo', 'bin'); + fs.mkdirSync(mockExpoCliDir, { recursive: true }); fs.writeFileSync( - mockNpxScript, + path.join(mockExpoCliDir, 'cli'), `#!/usr/bin/env node const args = process.argv.slice(2); -if (args.includes('expo') && args.includes('config') && args.includes('--json')) { +if (args.includes('config') && args.includes('--json')) { process.stdout.write(${JSON.stringify(JSON.stringify(expoConfig))}); process.exit(0); } process.exit(1); `, ); - fs.chmodSync(mockNpxScript, '755'); + fs.chmodSync(path.join(mockExpoCliDir, 'cli'), '755'); + fs.writeFileSync( + path.join(tempDir, 'node_modules', 'expo', 'package.json'), + JSON.stringify({ name: 'expo', version: '0.0.0', bin: { expo: 'bin/cli' } }), + ); const defaultEnv = { SENTRY_AUTH_TOKEN: 'test-token', SENTRY_CLI_EXECUTABLE: mockSentryCliScript, - PATH: `${mockBinDir}:${process.env.PATH}`, + NODE_PATH: path.join(tempDir, 'node_modules'), }; const result = spawnSync(process.execPath, [EXPO_UPLOAD_SCRIPT, outputDir], { @@ -536,6 +524,57 @@ process.exit(1); }); }); + describe('expo resolution via cwd fallback (pnpm strict isolation)', () => { + it('resolves expo from cwd when not resolvable from the script location', () => { + createAssets(['bundle.js', 'bundle.js.map']); + + const expoConfig = { + plugins: [ + ['@sentry/react-native/expo', { organization: 'cwd-org', project: 'cwd-project', url: 'https://sentry.io/' }], + ], + }; + + // Place mock expo only under tempDir/node_modules — reachable via cwd, not via NODE_PATH + const mockExpoCliDir = path.join(tempDir, 'node_modules', 'expo', 'bin'); + fs.mkdirSync(mockExpoCliDir, { recursive: true }); + fs.writeFileSync( + path.join(mockExpoCliDir, 'cli'), + `#!/usr/bin/env node +const args = process.argv.slice(2); +if (args.includes('config') && args.includes('--json')) { + process.stdout.write(${JSON.stringify(JSON.stringify(expoConfig))}); + process.exit(0); +} +process.exit(1); +`, + ); + fs.chmodSync(path.join(mockExpoCliDir, 'cli'), '755'); + fs.writeFileSync( + path.join(tempDir, 'node_modules', 'expo', 'package.json'), + JSON.stringify({ name: 'expo', version: '0.0.0', bin: { expo: 'bin/cli' } }), + ); + + const result = spawnSync(process.execPath, [EXPO_UPLOAD_SCRIPT, outputDir], { + cwd: tempDir, + env: { + ...process.env, + SENTRY_AUTH_TOKEN: 'test-token', + SENTRY_CLI_EXECUTABLE: mockSentryCliScript, + SENTRY_ORG: undefined, + SENTRY_PROJECT: undefined, + SENTRY_URL: undefined, + MOCK_CLI_EXIT_CODE: '0', + NODE_PATH: path.join(tempDir, 'nonexistent_modules'), + }, + encoding: 'utf8', + timeout: 10000, + }); + + expect(result.status).toBe(0); + expect(result.stdout).toContain('SENTRY_ORG resolved to cwd-org'); + }); + }); + describe('sourcemap processing', () => { it('converts debugId to debug_id in sourcemaps', () => { createAssets(['bundle.js', 'bundle.js.map']); diff --git a/packages/expo-upload-sourcemaps/cli.js b/packages/expo-upload-sourcemaps/cli.js index 28cd4ac3b8..98e1057cfb 100755 --- a/packages/expo-upload-sourcemaps/cli.js +++ b/packages/expo-upload-sourcemaps/cli.js @@ -16,7 +16,15 @@ function getEnvVar(varname) { function getSentryPluginPropertiesFromExpoConfig() { try { - const result = spawnSync('npx', ['expo', 'config', '--json'], { encoding: 'utf8' }); + let expoPkgJson; + try { + expoPkgJson = require.resolve('expo/package.json'); + } catch { + expoPkgJson = require.resolve('expo/package.json', { paths: [process.cwd()] }); + } + const expoPkg = JSON.parse(fs.readFileSync(expoPkgJson, 'utf8')); + const expoCli = path.resolve(path.dirname(expoPkgJson), expoPkg.bin.expo); + const result = spawnSync(process.execPath, [expoCli, 'config', '--json'], { encoding: 'utf8' }); if (result.error || result.status !== 0) { throw result.error || new Error(`expo config exited with status ${result.status}`); }