Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- fix(react-native): Add `use_modular_headers!` to the iOS `Podfile` so `pod install` succeeds with the Swift-based RNSentry pod
- feat(react-native): Prompt for Logs first in the feature selection flow ([#1308](https://github.com/getsentry/sentry-wizard/pull/1308))
- fix(react-native): Use User Feedback Widget terminology in the setup flow
- ref(react-router): Install SDK package with version `@^10` ([#1303](https://github.com/getsentry/sentry-wizard/pull/1303))
Expand Down
7 changes: 5 additions & 2 deletions e2e-tests/tests/react-native.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ import { afterAll, beforeAll, describe, test, expect } from 'vitest';
//@ts-expect-error - clifty is ESM only
import { KEYS, withEnv } from 'clifty';

// TODO: remove skipIf when macOS CI is fixed
describe.skipIf(process.platform === 'darwin')('ReactNative', () => {
describe('ReactNative', () => {
const integration = Integration.reactNative;
let wizardExitCode: number;
const { projectDir, cleanup } = createIsolatedTestEnv(
Expand Down Expand Up @@ -147,6 +146,10 @@ defaults.url=https://sentry.io/`,
);
});

test('ios/Podfile is updated with use_modular_headers!', () => {
checkFileContents(`${projectDir}/ios/Podfile`, `use_modular_headers!`);
});

test('build.gradle is updated correctly', () => {
checkFileContents(
`${projectDir}/android/app/build.gradle`,
Expand Down
62 changes: 62 additions & 0 deletions src/react-native/cocoapod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,68 @@ export function usesCocoaPod(projPath: string): boolean {
return fs.existsSync(path.join(projPath, 'Podfile'));
}

/**
* Ensures `use_modular_headers!` is present in the Podfile.
*
* As of `@sentry/react-native` 8.19.0 the `RNSentry` pod ships Swift code (the
* `RNSentryInternal` bridge over `SentrySDK.internal`, part of the migration to
* the prebuilt `Sentry.xcframework`). CocoaPods refuses to integrate a Swift pod
* that depends on a non-modular Objective-C pod (e.g. `React-hermes` on React
* Native versions that don't modularize it by default) unless the Podfile opts
* into module maps. Without it, `pod install` fails with:
*
* [!] The following Swift pods cannot yet be integrated as static libraries:
* The Swift pod `RNSentry` depends upon `React-hermes`, which does not define
* modules. [...] you may set `use_modular_headers!` globally in your Podfile
*
* The SDK's own docs instruct users to add this line manually; we do it for them.
*
* @returns `true` if the Podfile already had (or now has) `use_modular_headers!`,
* `false` if there is no Podfile to patch.
*/
export function addModularHeaders(projPath: string): boolean {
const podfile = path.join(projPath, 'Podfile');

if (!fs.existsSync(podfile)) {
return false;
}

const podContent = fs.readFileSync(podfile, 'utf8');

if (/^\s*use_modular_headers!/m.test(podContent)) {
// Already opted into modular headers, nothing to do.
return true;
}

// Insert above the first `target '...' do` block, matching the placement the
// SDK docs recommend. The `^\s*` anchor avoids matching commented-out targets.
const targetMatch = /^([ \t]*)target\s+['"][^'"]+['"]\s+do/m.exec(podContent);

let newFileContent: string;
if (targetMatch) {
const insertIndex = targetMatch.index;
newFileContent =
podContent.slice(0, insertIndex) +
'use_modular_headers!\n\n' +
podContent.slice(insertIndex);
} else {
// No target block found, append to the end as a safe fallback.
const separator =
podContent.endsWith('\n') || podContent.length === 0 ? '' : '\n';
newFileContent = `${podContent}${separator}use_modular_headers!\n`;
}

fs.writeFileSync(podfile, newFileContent, 'utf8');

clack.log.step(
`Added ${chalk.cyan('use_modular_headers!')} to the iOS ${chalk.cyan(
'Podfile',
)} (required by the Sentry React Native SDK).`,
);

return true;
}

export async function addCocoaPods(projPath: string): Promise<boolean> {
const podfile = path.join(projPath, 'Podfile');

Expand Down
9 changes: 8 additions & 1 deletion src/react-native/react-native-wizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as fs from 'fs';

import * as Sentry from '@sentry/node';
import { platform } from 'os';
import { podInstall } from './cocoapod';
import { addModularHeaders, podInstall, usesCocoaPod } from './cocoapod';
import { traceStep, withTelemetry } from '../telemetry';
import { offerProjectScopedMcpConfig } from '../utils/clack/mcp-config';
import {
Expand Down Expand Up @@ -349,6 +349,13 @@ async function patchXcodeFiles(config: RNCliSetupConfigContent) {
gitignore: false,
});

// The RNSentry pod is a Swift pod and requires `use_modular_headers!` in the
// Podfile to integrate as a static library. Patch it before `pod install` so
// the install succeeds now and any later manual `pod install` keeps working.
if (usesCocoaPod('ios')) {
traceStep('add-modular-headers', () => addModularHeaders('ios'));
}

if (platform() === 'darwin' && (await confirmPodInstall())) {
await traceStep('pod-install', () => podInstall('ios'));
}
Expand Down
103 changes: 103 additions & 0 deletions test/react-native/cocoapod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as os from 'os';
import * as path from 'path';
import {
addCocoaPods,
addModularHeaders,
podInstall,
usesCocoaPod,
} from '../../src/react-native/cocoapod';
Expand Down Expand Up @@ -218,6 +219,108 @@ describe('cocoapod', () => {
});
});

describe('addModularHeaders', () => {
describe('Podfile does not exist', () => {
it('should return false and not create a Podfile', () => {
// -- Arrange --
const projPath = fs.mkdtempSync(path.join(os.tmpdir(), 'project'));

// -- Act --
const result = addModularHeaders(projPath);

// -- Assert --
expect(result).toBe(false);
expect(fs.existsSync(path.join(projPath, 'Podfile'))).toBe(false);
});
});

describe('Podfile already includes use_modular_headers!', () => {
const variations = [
{ case: 'simple', content: 'use_modular_headers!\n' },
{ case: 'leading-space', content: ' use_modular_headers!\n' },
{
case: 'within-target',
content: `target 'App' do\n use_modular_headers!\nend\n`,
},
];
for (const variation of variations) {
it(`should not change the Podfile for ${variation.case}`, () => {
// -- Arrange --
const projPath = fs.mkdtempSync(path.join(os.tmpdir(), 'project'));
const podfile = path.join(projPath, 'Podfile');
fs.writeFileSync(podfile, variation.content, 'utf8');

// -- Act --
const result = addModularHeaders(projPath);

// -- Assert --
expect(result).toBe(true);
expect(fs.readFileSync(podfile, 'utf8')).toBe(variation.content);
});
}
});

describe('Podfile includes a target block', () => {
it('should insert use_modular_headers! above the target block', () => {
// -- Arrange --
const projPath = fs.mkdtempSync(path.join(os.tmpdir(), 'project'));
const podfile = path.join(projPath, 'Podfile');
fs.writeFileSync(
podfile,
`platform :ios, min_ios_version_supported\n\ntarget 'App' do\n use_react_native!\nend\n`,
'utf8',
);

// -- Act --
const result = addModularHeaders(projPath);

// -- Assert --
expect(result).toBe(true);
expect(fs.readFileSync(podfile, 'utf8')).toBe(
`platform :ios, min_ios_version_supported\n\nuse_modular_headers!\n\ntarget 'App' do\n use_react_native!\nend\n`,
);
});

it('should ignore commented-out target blocks', () => {
// -- Arrange --
const projPath = fs.mkdtempSync(path.join(os.tmpdir(), 'project'));
const podfile = path.join(projPath, 'Podfile');
fs.writeFileSync(
podfile,
`# target 'Commented' do\ntarget 'App' do\nend\n`,
'utf8',
);

// -- Act --
const result = addModularHeaders(projPath);

// -- Assert --
expect(result).toBe(true);
expect(fs.readFileSync(podfile, 'utf8')).toBe(
`# target 'Commented' do\nuse_modular_headers!\n\ntarget 'App' do\nend\n`,
);
});
});

describe('Podfile has no target block', () => {
it('should append use_modular_headers! to the end', () => {
// -- Arrange --
const projPath = fs.mkdtempSync(path.join(os.tmpdir(), 'project'));
const podfile = path.join(projPath, 'Podfile');
fs.writeFileSync(podfile, `platform :ios, '15.1'`, 'utf8');

// -- Act --
const result = addModularHeaders(projPath);

// -- Assert --
expect(result).toBe(true);
expect(fs.readFileSync(podfile, 'utf8')).toBe(
`platform :ios, '15.1'\nuse_modular_headers!\n`,
);
});
});
});

describe('podInstall', () => {
let workDir: string;

Expand Down
Loading