@sentry/wizard 3.16.4 → 3.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/package.json +2 -2
  3. package/dist/src/apple/cocoapod.js +2 -0
  4. package/dist/src/apple/cocoapod.js.map +1 -1
  5. package/dist/src/react-native/metro.d.ts +13 -0
  6. package/dist/src/react-native/metro.js +398 -0
  7. package/dist/src/react-native/metro.js.map +1 -0
  8. package/dist/src/react-native/react-native-wizard.d.ts +2 -0
  9. package/dist/src/react-native/react-native-wizard.js +145 -42
  10. package/dist/src/react-native/react-native-wizard.js.map +1 -1
  11. package/dist/src/react-native/uninstall.js +4 -0
  12. package/dist/src/react-native/uninstall.js.map +1 -1
  13. package/dist/src/react-native/xcode.d.ts +7 -3
  14. package/dist/src/react-native/xcode.js +43 -11
  15. package/dist/src/react-native/xcode.js.map +1 -1
  16. package/dist/src/remix/remix-wizard.js +80 -37
  17. package/dist/src/remix/remix-wizard.js.map +1 -1
  18. package/dist/src/remix/sdk-setup.d.ts +1 -0
  19. package/dist/src/remix/sdk-setup.js +21 -1
  20. package/dist/src/remix/sdk-setup.js.map +1 -1
  21. package/dist/src/sourcemaps/sourcemaps-wizard.js +2 -6
  22. package/dist/src/sourcemaps/sourcemaps-wizard.js.map +1 -1
  23. package/dist/src/utils/ast-utils.d.ts +14 -0
  24. package/dist/src/utils/ast-utils.js +49 -1
  25. package/dist/src/utils/ast-utils.js.map +1 -1
  26. package/dist/src/utils/clack-utils.js +0 -1
  27. package/dist/src/utils/clack-utils.js.map +1 -1
  28. package/dist/src/utils/types.d.ts +8 -4
  29. package/dist/src/utils/types.js.map +1 -1
  30. package/dist/src/utils/url.d.ts +10 -0
  31. package/dist/src/utils/url.js +19 -0
  32. package/dist/src/utils/url.js.map +1 -0
  33. package/dist/test/react-native/metro.test.d.ts +1 -0
  34. package/dist/test/react-native/metro.test.js +125 -0
  35. package/dist/test/react-native/metro.test.js.map +1 -0
  36. package/dist/test/react-native/xcode.test.js +40 -2
  37. package/dist/test/react-native/xcode.test.js.map +1 -1
  38. package/package.json +2 -2
  39. package/src/apple/cocoapod.ts +2 -0
  40. package/src/react-native/metro.ts +409 -0
  41. package/src/react-native/react-native-wizard.ts +115 -12
  42. package/src/react-native/uninstall.ts +3 -0
  43. package/src/react-native/xcode.ts +70 -12
  44. package/src/remix/remix-wizard.ts +51 -15
  45. package/src/remix/sdk-setup.ts +31 -0
  46. package/src/sourcemaps/sourcemaps-wizard.ts +2 -7
  47. package/src/utils/ast-utils.ts +52 -0
  48. package/src/utils/clack-utils.ts +0 -1
  49. package/src/utils/types.ts +8 -5
  50. package/src/utils/url.ts +23 -0
  51. package/test/react-native/metro.test.ts +283 -0
  52. package/test/react-native/xcode.test.ts +76 -3
@@ -0,0 +1,409 @@
1
+ // @ts-ignore - clack is ESM and TS complains about that. It works though
2
+ import * as clack from '@clack/prompts';
3
+ // @ts-ignore - magicast is ESM and TS complains about that. It works though
4
+ import { ProxifiedModule, parseModule, writeFile } from 'magicast';
5
+ import * as fs from 'fs';
6
+ import * as Sentry from '@sentry/node';
7
+
8
+ import {
9
+ getLastRequireIndex,
10
+ hasSentryContent,
11
+ removeRequire,
12
+ } from '../utils/ast-utils';
13
+ import {
14
+ abortIfCancelled,
15
+ makeCodeSnippet,
16
+ showCopyPasteInstructions,
17
+ } from '../utils/clack-utils';
18
+
19
+ import * as recast from 'recast';
20
+ import x = recast.types;
21
+ import t = x.namedTypes;
22
+ import chalk from 'chalk';
23
+
24
+ const b = recast.types.builders;
25
+
26
+ const metroConfigPath = 'metro.config.js';
27
+
28
+ export async function patchMetroConfig() {
29
+ const mod = await parseMetroConfig();
30
+
31
+ const showInstructions = () =>
32
+ showCopyPasteInstructions(metroConfigPath, getMetroConfigSnippet(true));
33
+
34
+ if (hasSentryContent(mod.$ast as t.Program)) {
35
+ const shouldContinue = await confirmPathMetroConfig();
36
+ if (!shouldContinue) {
37
+ return await showInstructions();
38
+ }
39
+ }
40
+
41
+ const configObj = getMetroConfigObject(mod.$ast as t.Program);
42
+ if (!configObj) {
43
+ clack.log.warn(
44
+ 'Could not find Metro config object, please follow the manual steps.',
45
+ );
46
+ return showInstructions();
47
+ }
48
+
49
+ const addedSentrySerializer = addSentrySerializerToMetroConfig(configObj);
50
+ if (!addedSentrySerializer) {
51
+ clack.log.warn(
52
+ 'Could not add Sentry serializer to Metro config, please follow the manual steps.',
53
+ );
54
+ return await showInstructions();
55
+ }
56
+
57
+ const addedSentrySerializerImport = addSentrySerializerRequireToMetroConfig(
58
+ mod.$ast as t.Program,
59
+ );
60
+ if (!addedSentrySerializerImport) {
61
+ clack.log.warn(
62
+ 'Could not add Sentry serializer import to Metro config, please follow the manual steps.',
63
+ );
64
+ return await showInstructions();
65
+ }
66
+
67
+ clack.log.success(
68
+ `Added Sentry Metro plugin to ${chalk.cyan(metroConfigPath)}.`,
69
+ );
70
+
71
+ const saved = await writeMetroConfig(mod);
72
+ if (saved) {
73
+ clack.log.success(
74
+ chalk.green(`${chalk.cyan(metroConfigPath)} changes saved.`),
75
+ );
76
+ } else {
77
+ clack.log.warn(
78
+ `Could not save changes to ${chalk.cyan(
79
+ metroConfigPath,
80
+ )}, please follow the manual steps.`,
81
+ );
82
+ return await showInstructions();
83
+ }
84
+ }
85
+
86
+ export async function unPatchMetroConfig() {
87
+ const mod = await parseMetroConfig();
88
+
89
+ const removedAtLeastOneRequire = removeSentryRequire(mod.$ast as t.Program);
90
+ const removedSerializerConfig = removeSentrySerializerFromMetroConfig(
91
+ mod.$ast as t.Program,
92
+ );
93
+
94
+ if (removedAtLeastOneRequire || removedSerializerConfig) {
95
+ const saved = await writeMetroConfig(mod);
96
+ if (saved) {
97
+ clack.log.success(
98
+ `Removed Sentry Metro plugin from ${chalk.cyan(metroConfigPath)}.`,
99
+ );
100
+ }
101
+ } else {
102
+ clack.log.warn(
103
+ `No Sentry Metro plugin found in ${chalk.cyan(metroConfigPath)}.`,
104
+ );
105
+ }
106
+ }
107
+
108
+ export function removeSentrySerializerFromMetroConfig(
109
+ program: t.Program,
110
+ ): boolean {
111
+ const configObject = getMetroConfigObject(program);
112
+ if (!configObject) {
113
+ return false;
114
+ }
115
+
116
+ const serializerProp = getSerializerProp(configObject);
117
+ if ('invalid' === serializerProp || 'undefined' === serializerProp) {
118
+ return false;
119
+ }
120
+
121
+ const customSerializerProp = getCustomSerializerProp(serializerProp);
122
+ if (
123
+ 'invalid' === customSerializerProp ||
124
+ 'undefined' === customSerializerProp
125
+ ) {
126
+ return false;
127
+ }
128
+
129
+ if (
130
+ serializerProp.value.type === 'ObjectExpression' &&
131
+ customSerializerProp.value.type === 'CallExpression' &&
132
+ customSerializerProp.value.callee.type === 'Identifier' &&
133
+ customSerializerProp.value.callee.name === 'createSentryMetroSerializer'
134
+ ) {
135
+ if (customSerializerProp.value.arguments.length === 0) {
136
+ // FROM serializer: { customSerializer: createSentryMetroSerializer() }
137
+ // TO serializer: {}
138
+ let removed = false;
139
+ serializerProp.value.properties = serializerProp.value.properties.filter(
140
+ (p) => {
141
+ if (
142
+ p.type === 'ObjectProperty' &&
143
+ p.key.type === 'Identifier' &&
144
+ p.key.name === 'customSerializer'
145
+ ) {
146
+ removed = true;
147
+ return false;
148
+ }
149
+ return true;
150
+ },
151
+ );
152
+
153
+ if (removed) {
154
+ return true;
155
+ }
156
+ } else {
157
+ if (customSerializerProp.value.arguments[0].type !== 'SpreadElement') {
158
+ // FROM serializer: { customSerializer: createSentryMetroSerializer(wrapperSerializer) }
159
+ // TO serializer: { customSerializer: wrapperSerializer }
160
+ customSerializerProp.value = customSerializerProp.value.arguments[0];
161
+ return true;
162
+ }
163
+ }
164
+ }
165
+
166
+ return false;
167
+ }
168
+
169
+ export function removeSentryRequire(program: t.Program): boolean {
170
+ return removeRequire(program, '@sentry');
171
+ }
172
+
173
+ async function parseMetroConfig(): Promise<ProxifiedModule> {
174
+ const metroConfigContent = (
175
+ await fs.promises.readFile(metroConfigPath)
176
+ ).toString();
177
+
178
+ return parseModule(metroConfigContent);
179
+ }
180
+
181
+ async function writeMetroConfig(mod: ProxifiedModule): Promise<boolean> {
182
+ try {
183
+ await writeFile(mod.$ast, metroConfigPath);
184
+ } catch (e) {
185
+ clack.log.error(
186
+ `Failed to write to ${chalk.cyan(metroConfigPath)}: ${JSON.stringify(e)}`,
187
+ );
188
+ return false;
189
+ }
190
+ return true;
191
+ }
192
+
193
+ export function addSentrySerializerToMetroConfig(
194
+ configObj: t.ObjectExpression,
195
+ ): boolean {
196
+ const serializerProp = getSerializerProp(configObj);
197
+ if ('invalid' === serializerProp) {
198
+ return false;
199
+ }
200
+
201
+ // case 1: serializer property doesn't exist yet, so we can just add it
202
+ if ('undefined' === serializerProp) {
203
+ configObj.properties.push(
204
+ b.objectProperty(
205
+ b.identifier('serializer'),
206
+ b.objectExpression([
207
+ b.objectProperty(
208
+ b.identifier('customSerializer'),
209
+ b.callExpression(b.identifier('createSentryMetroSerializer'), []),
210
+ ),
211
+ ]),
212
+ ),
213
+ );
214
+ return true;
215
+ }
216
+
217
+ const customSerializerProp = getCustomSerializerProp(serializerProp);
218
+ // case 2: serializer.customSerializer property doesn't exist yet, so we just add it
219
+ if (
220
+ 'undefined' === customSerializerProp &&
221
+ serializerProp.value.type === 'ObjectExpression'
222
+ ) {
223
+ serializerProp.value.properties.push(
224
+ b.objectProperty(
225
+ b.identifier('customSerializer'),
226
+ b.callExpression(b.identifier('createSentryMetroSerializer'), []),
227
+ ),
228
+ );
229
+ return true;
230
+ }
231
+
232
+ return false;
233
+ }
234
+
235
+ function getCustomSerializerProp(
236
+ prop: t.ObjectProperty,
237
+ ): t.ObjectProperty | 'undefined' | 'invalid' {
238
+ const customSerializerProp =
239
+ prop.value.type === 'ObjectExpression' &&
240
+ prop.value.properties.find(
241
+ (p: t.ObjectProperty) =>
242
+ p.key.type === 'Identifier' && p.key.name === 'customSerializer',
243
+ );
244
+
245
+ if (!customSerializerProp) {
246
+ return 'undefined';
247
+ }
248
+
249
+ if (customSerializerProp.type === 'ObjectProperty') {
250
+ return customSerializerProp;
251
+ }
252
+
253
+ return 'invalid';
254
+ }
255
+
256
+ function getSerializerProp(
257
+ obj: t.ObjectExpression,
258
+ ): t.ObjectProperty | 'undefined' | 'invalid' {
259
+ const serializerProp = obj.properties.find(
260
+ (p: t.ObjectProperty) =>
261
+ p.key.type === 'Identifier' && p.key.name === 'serializer',
262
+ );
263
+
264
+ if (!serializerProp) {
265
+ return 'undefined';
266
+ }
267
+
268
+ if (serializerProp.type === 'ObjectProperty') {
269
+ return serializerProp;
270
+ }
271
+
272
+ return 'invalid';
273
+ }
274
+
275
+ export function addSentrySerializerRequireToMetroConfig(
276
+ program: t.Program,
277
+ ): boolean {
278
+ const lastRequireIndex = getLastRequireIndex(program);
279
+ const sentrySerializerRequire = createSentrySerializerRequire();
280
+ const sentryImportIndex = lastRequireIndex + 1;
281
+ if (sentryImportIndex < program.body.length) {
282
+ // insert after last require
283
+ program.body.splice(lastRequireIndex + 1, 0, sentrySerializerRequire);
284
+ } else {
285
+ // insert at the end
286
+ program.body.push(sentrySerializerRequire);
287
+ }
288
+ return true;
289
+ }
290
+
291
+ /**
292
+ * Creates const {createSentryMetroSerializer} = require('@sentry/react-native/dist/js/tools/sentryMetroSerializer');
293
+ */
294
+ function createSentrySerializerRequire() {
295
+ return b.variableDeclaration('const', [
296
+ b.variableDeclarator(
297
+ b.objectPattern([
298
+ b.objectProperty.from({
299
+ key: b.identifier('createSentryMetroSerializer'),
300
+ value: b.identifier('createSentryMetroSerializer'),
301
+ shorthand: true,
302
+ }),
303
+ ]),
304
+ b.callExpression(b.identifier('require'), [
305
+ b.literal('@sentry/react-native/dist/js/tools/sentryMetroSerializer'),
306
+ ]),
307
+ ),
308
+ ]);
309
+ }
310
+
311
+ async function confirmPathMetroConfig() {
312
+ const shouldContinue = await abortIfCancelled(
313
+ clack.select({
314
+ message: `Metro Config already contains Sentry-related code. Should the wizard modify it anyway?`,
315
+ options: [
316
+ {
317
+ label: 'Yes, add the Sentry Metro plugin',
318
+ value: true,
319
+ },
320
+ {
321
+ label: 'No, show me instructions to manually add the plugin',
322
+ value: false,
323
+ },
324
+ ],
325
+ initialValue: true,
326
+ }),
327
+ );
328
+
329
+ if (!shouldContinue) {
330
+ Sentry.setTag('ast-mod-fail-reason', 'has-sentry-content');
331
+ }
332
+
333
+ return shouldContinue;
334
+ }
335
+
336
+ /**
337
+ * Returns value from `module.exports = value` or `const config = value`
338
+ */
339
+ export function getMetroConfigObject(
340
+ program: t.Program,
341
+ ): t.ObjectExpression | undefined {
342
+ // check config variable
343
+ const configVariable = program.body.find((s) => {
344
+ if (
345
+ s.type === 'VariableDeclaration' &&
346
+ s.declarations.length === 1 &&
347
+ s.declarations[0].type === 'VariableDeclarator' &&
348
+ s.declarations[0].id.type === 'Identifier' &&
349
+ s.declarations[0].id.name === 'config'
350
+ ) {
351
+ return true;
352
+ }
353
+ return false;
354
+ }) as t.VariableDeclaration | undefined;
355
+
356
+ if (
357
+ configVariable?.declarations[0].type === 'VariableDeclarator' &&
358
+ configVariable?.declarations[0].init?.type === 'ObjectExpression'
359
+ ) {
360
+ Sentry.setTag('metro-config', 'config-variable');
361
+ return configVariable.declarations[0].init;
362
+ }
363
+
364
+ // check module.exports
365
+ const moduleExports = program.body.find((s) => {
366
+ if (
367
+ s.type === 'ExpressionStatement' &&
368
+ s.expression.type === 'AssignmentExpression' &&
369
+ s.expression.left.type === 'MemberExpression' &&
370
+ s.expression.left.object.type === 'Identifier' &&
371
+ s.expression.left.object.name === 'module' &&
372
+ s.expression.left.property.type === 'Identifier' &&
373
+ s.expression.left.property.name === 'exports'
374
+ ) {
375
+ return true;
376
+ }
377
+ return false;
378
+ }) as t.ExpressionStatement | undefined;
379
+
380
+ if (
381
+ (moduleExports?.expression as t.AssignmentExpression).right.type ===
382
+ 'ObjectExpression'
383
+ ) {
384
+ Sentry.setTag('metro-config', 'module-exports');
385
+ return (moduleExports?.expression as t.AssignmentExpression)
386
+ .right as t.ObjectExpression;
387
+ }
388
+
389
+ Sentry.setTag('metro-config', 'not-found');
390
+ return undefined;
391
+ }
392
+
393
+ function getMetroConfigSnippet(colors: boolean) {
394
+ return makeCodeSnippet(colors, (unchanged, plus, _) =>
395
+ unchanged(`const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');";
396
+ ${plus(
397
+ "const {createSentryMetroSerializer} = require('@sentry/react-native/dist/js/tools/sentryMetroSerializer');",
398
+ )}
399
+
400
+ const config = {
401
+ ${plus(`serializer: {
402
+ customSerializer: createSentryMetroSerializer(),
403
+ },`)}
404
+ };
405
+
406
+ module.exports = mergeConfig(getDefaultConfig(__dirname), config);
407
+ `),
408
+ );
409
+ }
@@ -7,6 +7,7 @@ import * as path from 'path';
7
7
  import * as process from 'process';
8
8
  import {
9
9
  CliSetupConfigContent,
10
+ abortIfCancelled,
10
11
  addSentryCliConfig,
11
12
  confirmContinueIfNoOrDirtyGitRepo,
12
13
  confirmContinueIfPackageVersionNotSupported,
@@ -26,8 +27,11 @@ import {
26
27
  findBundlePhase,
27
28
  patchBundlePhase,
28
29
  findDebugFilesUploadPhase,
29
- addDebugFilesUploadPhase,
30
+ addDebugFilesUploadPhaseWithCli,
30
31
  writeXcodeProject,
32
+ addSentryWithCliToBundleShellScript,
33
+ addSentryWithBundledScriptsToBundleShellScript,
34
+ addDebugFilesUploadPhaseWithBundledScripts,
31
35
  } from './xcode';
32
36
  import {
33
37
  doesAppBuildGradleIncludeRNSentryGradlePlugin,
@@ -37,7 +41,6 @@ import {
37
41
  import { runReactNativeUninstall } from './uninstall';
38
42
  import { APP_BUILD_GRADLE, XCODE_PROJECT, getFirstMatchedPath } from './glob';
39
43
  import { ReactNativeWizardOptions } from './options';
40
- import { SentryProjectData } from '../utils/types';
41
44
  import {
42
45
  addSentryInitWithSdkImport,
43
46
  doesJsCodeIncludeSdkSentryImport,
@@ -45,6 +48,9 @@ import {
45
48
  } from './javascript';
46
49
  import { traceStep, withTelemetry } from '../telemetry';
47
50
  import * as Sentry from '@sentry/node';
51
+ import { fulfillsVersionRange } from '../utils/semver';
52
+ import { getIssueStreamUrl } from '../utils/url';
53
+ import { patchMetroConfig } from './metro';
48
54
 
49
55
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
50
56
  const xcode = require('xcode');
@@ -56,6 +62,13 @@ export const RN_HUMAN_NAME = 'React Native';
56
62
 
57
63
  export const SUPPORTED_RN_RANGE = '>=0.69.0';
58
64
 
65
+ // The following SDK version ship with bundled Xcode scripts
66
+ // which simplifies the Xcode Build Phases setup.
67
+ export const SDK_XCODE_SCRIPTS_SUPPORTED_SDK_RANGE = '>=5.11.0';
68
+
69
+ // The following SDK version ship with Sentry Metro plugin
70
+ export const SDK_SENTRY_METRO_PLUGIN_SUPPORTED_SDK_RANGE = '>=5.11.0';
71
+
59
72
  export type RNCliSetupConfigContent = Pick<
60
73
  Required<CliSetupConfigContent>,
61
74
  'authToken' | 'org' | 'project' | 'url'
@@ -107,6 +120,7 @@ export async function runReactNativeWizardWithTelemetry(
107
120
  await getOrAskForProjectData(options, 'react-native');
108
121
  const orgSlug = selectedProject.organization.slug;
109
122
  const projectSlug = selectedProject.slug;
123
+ const projectId = selectedProject.id;
110
124
  const cliConfig: RNCliSetupConfigContent = {
111
125
  authToken,
112
126
  org: orgSlug,
@@ -118,14 +132,24 @@ export async function runReactNativeWizardWithTelemetry(
118
132
  packageName: RN_SDK_PACKAGE,
119
133
  alreadyInstalled: hasPackageInstalled(RN_SDK_PACKAGE, packageJson),
120
134
  });
135
+ const sdkVersion = getPackageVersion(
136
+ RN_SDK_PACKAGE,
137
+ await getPackageDotJson(),
138
+ );
121
139
 
122
140
  await traceStep('patch-js', () =>
123
141
  addSentryInit({ dsn: selectedProject.keys[0].dsn.public }),
124
142
  );
125
143
 
144
+ await traceStep('patch-metro-config', () =>
145
+ addSentryToMetroConfig({ sdkVersion }),
146
+ );
147
+
126
148
  if (fs.existsSync('ios')) {
127
149
  Sentry.setTag('patch-ios', true);
128
- await traceStep('patch-xcode-files', () => patchXcodeFiles(cliConfig));
150
+ await traceStep('patch-xcode-files', () =>
151
+ patchXcodeFiles(cliConfig, { sdkVersion }),
152
+ );
129
153
  }
130
154
 
131
155
  if (fs.existsSync('android')) {
@@ -134,7 +158,9 @@ export async function runReactNativeWizardWithTelemetry(
134
158
  }
135
159
 
136
160
  const confirmedFirstException = await confirmFirstSentryException(
137
- selectedProject,
161
+ sentryUrl,
162
+ orgSlug,
163
+ projectId,
138
164
  );
139
165
  Sentry.setTag('user-confirmed-first-error', confirmedFirstException);
140
166
 
@@ -155,6 +181,25 @@ export async function runReactNativeWizardWithTelemetry(
155
181
  }
156
182
  }
157
183
 
184
+ async function addSentryToMetroConfig({
185
+ sdkVersion,
186
+ }: {
187
+ sdkVersion: string | undefined;
188
+ }) {
189
+ if (
190
+ !sdkVersion ||
191
+ !fulfillsVersionRange({
192
+ version: sdkVersion,
193
+ acceptableVersions: SDK_SENTRY_METRO_PLUGIN_SUPPORTED_SDK_RANGE,
194
+ canBeLatest: true,
195
+ })
196
+ ) {
197
+ return;
198
+ }
199
+
200
+ await patchMetroConfig();
201
+ }
202
+
158
203
  async function addSentryInit({ dsn }: { dsn: string }) {
159
204
  const prefixGlob = '{.,./src}';
160
205
  const suffixGlob = '@(j|t|cj|mj)s?(x)';
@@ -207,8 +252,12 @@ async function addSentryInit({ dsn }: { dsn: string }) {
207
252
  );
208
253
  }
209
254
 
210
- async function confirmFirstSentryException(project: SentryProjectData) {
211
- const projectsIssuesUrl = `${project.organization.links.organizationUrl}/issues/?project=${project.id}`;
255
+ async function confirmFirstSentryException(
256
+ url: string,
257
+ orgSlug: string,
258
+ projectId: string,
259
+ ) {
260
+ const issuesStreamUrl = getIssueStreamUrl({ url, orgSlug, projectId });
212
261
 
213
262
  clack.log
214
263
  .step(`To make sure everything is set up correctly, put the following code snippet into your application.
@@ -216,7 +265,7 @@ The snippet will create a button that, when tapped, sends a test event to Sentry
216
265
 
217
266
  After that check your project issues:
218
267
 
219
- ${chalk.cyan(projectsIssuesUrl)}`);
268
+ ${chalk.cyan(issuesStreamUrl)}`);
220
269
 
221
270
  // We want the code snippet to be easily copy-pasteable, without any clack artifacts
222
271
  // eslint-disable-next-line no-console
@@ -233,7 +282,12 @@ ${chalk.cyan(projectsIssuesUrl)}`);
233
282
  return firstErrorConfirmed;
234
283
  }
235
284
 
236
- async function patchXcodeFiles(config: RNCliSetupConfigContent) {
285
+ async function patchXcodeFiles(
286
+ config: RNCliSetupConfigContent,
287
+ context: {
288
+ sdkVersion: string | undefined;
289
+ },
290
+ ) {
237
291
  await addSentryCliConfig(config, {
238
292
  ...propertiesCliSetupConfig,
239
293
  name: 'source maps and iOS debug files',
@@ -241,9 +295,8 @@ async function patchXcodeFiles(config: RNCliSetupConfigContent) {
241
295
  gitignore: false,
242
296
  });
243
297
 
244
- if (platform() === 'darwin') {
298
+ if (platform() === 'darwin' && (await confirmPodInstall())) {
245
299
  await traceStep('pod-install', () => podInstall('ios'));
246
- Sentry.setTag('pods-installed', true);
247
300
  }
248
301
 
249
302
  const xcodeProjectPath = traceStep('find-xcode-project', () =>
@@ -281,7 +334,21 @@ async function patchXcodeFiles(config: RNCliSetupConfigContent) {
281
334
  'xcode-bundle-phase-status',
282
335
  bundlePhase ? 'found' : 'not-found',
283
336
  );
284
- patchBundlePhase(bundlePhase);
337
+ if (
338
+ context.sdkVersion &&
339
+ fulfillsVersionRange({
340
+ version: context.sdkVersion,
341
+ acceptableVersions: SDK_XCODE_SCRIPTS_SUPPORTED_SDK_RANGE,
342
+ canBeLatest: true,
343
+ })
344
+ ) {
345
+ patchBundlePhase(
346
+ bundlePhase,
347
+ addSentryWithBundledScriptsToBundleShellScript,
348
+ );
349
+ } else {
350
+ patchBundlePhase(bundlePhase, addSentryWithCliToBundleShellScript);
351
+ }
285
352
  Sentry.setTag('xcode-bundle-phase-status', 'patched');
286
353
  });
287
354
 
@@ -292,7 +359,22 @@ async function patchXcodeFiles(config: RNCliSetupConfigContent) {
292
359
  'xcode-debug-files-upload-phase-status',
293
360
  debugFilesUploadPhaseExists ? 'already-exists' : undefined,
294
361
  );
295
- addDebugFilesUploadPhase(xcodeProject, { debugFilesUploadPhaseExists });
362
+ if (
363
+ context.sdkVersion &&
364
+ fulfillsVersionRange({
365
+ version: context.sdkVersion,
366
+ acceptableVersions: SDK_XCODE_SCRIPTS_SUPPORTED_SDK_RANGE,
367
+ canBeLatest: true,
368
+ })
369
+ ) {
370
+ addDebugFilesUploadPhaseWithBundledScripts(xcodeProject, {
371
+ debugFilesUploadPhaseExists,
372
+ });
373
+ } else {
374
+ addDebugFilesUploadPhaseWithCli(xcodeProject, {
375
+ debugFilesUploadPhaseExists,
376
+ });
377
+ }
296
378
  Sentry.setTag('xcode-debug-files-upload-phase-status', 'added');
297
379
  });
298
380
 
@@ -367,3 +449,24 @@ async function patchAndroidFiles(config: RNCliSetupConfigContent) {
367
449
  chalk.green(`Android ${chalk.cyan('app/build.gradle')} saved.`),
368
450
  );
369
451
  }
452
+
453
+ async function confirmPodInstall(): Promise<boolean> {
454
+ return traceStep('confirm-pod-install', async () => {
455
+ const continueWithPodInstall = await abortIfCancelled(
456
+ clack.select({
457
+ message: 'Do you want to run `pod install` now?',
458
+ options: [
459
+ {
460
+ value: true,
461
+ label: 'Yes',
462
+ hint: 'Recommended for smaller projects, this might take several minutes',
463
+ },
464
+ { value: false, label: `No, I'll do it later` },
465
+ ],
466
+ initialValue: true,
467
+ }),
468
+ );
469
+ Sentry.setTag('continue-with-pod-install', continueWithPodInstall);
470
+ return continueWithPodInstall;
471
+ });
472
+ }
@@ -21,6 +21,7 @@ import {
21
21
  writeAppBuildGradle,
22
22
  } from './gradle';
23
23
  import { ReactNativeWizardOptions } from './options';
24
+ import { unPatchMetroConfig } from './metro';
24
25
 
25
26
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
26
27
  const xcode = require('xcode');
@@ -36,6 +37,8 @@ export async function runReactNativeUninstall(
36
37
 
37
38
  await confirmContinueIfNoOrDirtyGitRepo();
38
39
 
40
+ await unPatchMetroConfig();
41
+
39
42
  unPatchXcodeFiles();
40
43
 
41
44
  unPatchAndroidFiles();