@sentry/wizard 3.40.0 → 3.41.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 (103) hide show
  1. package/CHANGELOG.md +8 -1
  2. package/README.md +19 -19
  3. package/bin.ts +5 -0
  4. package/codecov.yml +15 -0
  5. package/dist/bin.js +4 -0
  6. package/dist/bin.js.map +1 -1
  7. package/dist/e2e-tests/jest.config.d.ts +1 -0
  8. package/dist/e2e-tests/jest.config.js +1 -0
  9. package/dist/e2e-tests/jest.config.js.map +1 -1
  10. package/dist/package.json +3 -2
  11. package/dist/src/apple/apple-wizard.js +1 -2
  12. package/dist/src/apple/apple-wizard.js.map +1 -1
  13. package/dist/src/apple/code-tools.d.ts +10 -0
  14. package/dist/src/apple/code-tools.js +16 -12
  15. package/dist/src/apple/code-tools.js.map +1 -1
  16. package/dist/src/apple/fastlane.d.ts +23 -0
  17. package/dist/src/apple/fastlane.js +11 -7
  18. package/dist/src/apple/fastlane.js.map +1 -1
  19. package/dist/src/apple/templates.d.ts +1 -1
  20. package/dist/src/apple/templates.js +0 -2
  21. package/dist/src/apple/templates.js.map +1 -1
  22. package/dist/src/apple/xcode-manager.d.ts +10 -6
  23. package/dist/src/apple/xcode-manager.js +146 -61
  24. package/dist/src/apple/xcode-manager.js.map +1 -1
  25. package/dist/src/nextjs/nextjs-wizard.js +5 -3
  26. package/dist/src/nextjs/nextjs-wizard.js.map +1 -1
  27. package/dist/src/nuxt/nuxt-wizard.js +6 -4
  28. package/dist/src/nuxt/nuxt-wizard.js.map +1 -1
  29. package/dist/src/nuxt/sdk-setup.d.ts +1 -1
  30. package/dist/src/nuxt/sdk-setup.js +2 -1
  31. package/dist/src/nuxt/sdk-setup.js.map +1 -1
  32. package/dist/src/react-native/react-native-wizard.js +5 -3
  33. package/dist/src/react-native/react-native-wizard.js.map +1 -1
  34. package/dist/src/remix/remix-wizard.js +5 -3
  35. package/dist/src/remix/remix-wizard.js.map +1 -1
  36. package/dist/src/run.d.ts +1 -0
  37. package/dist/src/run.js +1 -0
  38. package/dist/src/run.js.map +1 -1
  39. package/dist/src/sveltekit/sveltekit-wizard.js +5 -3
  40. package/dist/src/sveltekit/sveltekit-wizard.js.map +1 -1
  41. package/dist/src/utils/clack-utils.d.ts +3 -1
  42. package/dist/src/utils/clack-utils.js +18 -12
  43. package/dist/src/utils/clack-utils.js.map +1 -1
  44. package/dist/src/utils/package-manager.d.ts +1 -0
  45. package/dist/src/utils/package-manager.js +5 -0
  46. package/dist/src/utils/package-manager.js.map +1 -1
  47. package/dist/src/utils/types.d.ts +9 -0
  48. package/dist/src/utils/types.js.map +1 -1
  49. package/dist/test/apple/cocoapod.test.d.ts +1 -0
  50. package/dist/test/apple/cocoapod.test.js +409 -0
  51. package/dist/test/apple/cocoapod.test.js.map +1 -0
  52. package/dist/test/apple/code-tools.test.d.ts +1 -0
  53. package/dist/test/apple/code-tools.test.js +673 -0
  54. package/dist/test/apple/code-tools.test.js.map +1 -0
  55. package/dist/test/apple/fastfile.test.d.ts +1 -0
  56. package/dist/test/apple/fastfile.test.js +431 -0
  57. package/dist/test/apple/fastfile.test.js.map +1 -0
  58. package/dist/test/apple/templates.test.d.ts +1 -0
  59. package/dist/test/apple/templates.test.js +73 -0
  60. package/dist/test/apple/templates.test.js.map +1 -0
  61. package/dist/test/apple/xcode-manager.test.d.ts +1 -0
  62. package/dist/test/apple/xcode-manager.test.js +834 -0
  63. package/dist/test/apple/xcode-manager.test.js.map +1 -0
  64. package/dist/test/utils/clack-utils.test.js +89 -0
  65. package/dist/test/utils/clack-utils.test.js.map +1 -1
  66. package/e2e-tests/jest.config.ts +1 -0
  67. package/e2e-tests/test-applications/apple/damaged-missing-configuration-list/Project.xcodeproj/project.pbxproj +52 -0
  68. package/e2e-tests/test-applications/apple/damaged-missing-configuration-list/Project.xcodeproj/xcshareddata/xcschemes/Project1.xcscheme +78 -0
  69. package/e2e-tests/test-applications/apple/no-targets/Project.xcodeproj/project.pbxproj +62 -0
  70. package/e2e-tests/test-applications/apple/no-targets/Project.xcodeproj/xcshareddata/xcschemes/Project1.xcscheme +78 -0
  71. package/e2e-tests/test-applications/apple/spm-swiftui-multi-targets/Project.xcodeproj/project.pbxproj +470 -0
  72. package/e2e-tests/test-applications/apple/spm-swiftui-multi-targets/Project.xcodeproj/xcshareddata/xcschemes/Project1.xcscheme +78 -0
  73. package/e2e-tests/test-applications/apple/spm-swiftui-multi-targets/Project1/ContentView.swift +7 -0
  74. package/e2e-tests/test-applications/apple/spm-swiftui-multi-targets/Project1/Project1App.swift +10 -0
  75. package/e2e-tests/test-applications/apple/spm-swiftui-multi-targets/Project2/ContentView.swift +7 -0
  76. package/e2e-tests/test-applications/apple/spm-swiftui-multi-targets/Project2/Project2App.swift +10 -0
  77. package/e2e-tests/test-applications/apple/spm-swiftui-single-target/Project.xcodeproj/project.pbxproj +382 -0
  78. package/e2e-tests/test-applications/apple/spm-swiftui-single-target/Project.xcodeproj/xcshareddata/xcschemes/Project.xcscheme +78 -0
  79. package/e2e-tests/test-applications/apple/spm-swiftui-single-target/Sources/ContentView.swift +7 -0
  80. package/e2e-tests/test-applications/apple/spm-swiftui-single-target/Sources/MainApp.swift +10 -0
  81. package/package.json +3 -2
  82. package/src/apple/apple-wizard.ts +1 -2
  83. package/src/apple/code-tools.ts +21 -6
  84. package/src/apple/fastlane.ts +18 -2
  85. package/src/apple/templates.ts +2 -2
  86. package/src/apple/xcode-manager.ts +181 -94
  87. package/src/nextjs/nextjs-wizard.ts +5 -2
  88. package/src/nuxt/nuxt-wizard.ts +6 -3
  89. package/src/nuxt/sdk-setup.ts +2 -0
  90. package/src/react-native/react-native-wizard.ts +5 -2
  91. package/src/remix/remix-wizard.ts +5 -2
  92. package/src/run.ts +2 -0
  93. package/src/sveltekit/sveltekit-wizard.ts +5 -2
  94. package/src/utils/clack-utils.ts +12 -2
  95. package/src/utils/package-manager.ts +6 -0
  96. package/src/utils/types.ts +10 -0
  97. package/test/apple/cocoapod.test.ts +306 -0
  98. package/test/apple/code-tools.test.ts +1042 -0
  99. package/test/apple/fastfile.test.ts +550 -0
  100. package/test/apple/templates.test.ts +191 -0
  101. package/test/apple/xcode-manager.test.ts +1066 -0
  102. package/test/utils/clack-utils.test.ts +92 -0
  103. package/types/xcode.d.ts +526 -0
@@ -5,10 +5,20 @@
5
5
  // @ts-ignore - clack is ESM and TS complains about that. It works though
6
6
  import clack from '@clack/prompts';
7
7
  import * as fs from 'fs';
8
+ import * as path from 'path';
8
9
  import { SentryProjectData } from '../utils/types';
9
10
  import * as templates from './templates';
10
- import * as path from 'path';
11
- const xcode = require('xcode');
11
+
12
+ import {
13
+ project as createXcodeProject,
14
+ PBXBuildFile,
15
+ PBXGroup,
16
+ PBXNativeTarget,
17
+ PBXObjects,
18
+ PBXSourcesBuildPhase,
19
+ Project,
20
+ XCConfigurationList,
21
+ } from 'xcode';
12
22
 
13
23
  interface ProjectFile {
14
24
  key: string;
@@ -16,77 +26,125 @@ interface ProjectFile {
16
26
  }
17
27
 
18
28
  function setDebugInformationFormatAndSandbox(
19
- proj: any,
29
+ proj: Project,
20
30
  targetName: string,
21
31
  ): void {
22
32
  const xcObjects = proj.hash.project.objects;
23
- const targetKey: string = Object.keys(xcObjects.PBXNativeTarget || {}).filter(
33
+ if (!xcObjects.PBXNativeTarget) {
34
+ xcObjects.PBXNativeTarget = {};
35
+ }
36
+ const targetKey: string = Object.keys(xcObjects.PBXNativeTarget).filter(
24
37
  (key) => {
38
+ const value = xcObjects.PBXNativeTarget?.[key];
25
39
  return (
26
40
  !key.endsWith('_comment') &&
27
- xcObjects.PBXNativeTarget[key].name === targetName
41
+ typeof value !== 'string' &&
42
+ value?.name === targetName
28
43
  );
29
44
  },
30
45
  )[0];
31
- const target = xcObjects.PBXNativeTarget[targetKey];
46
+ const target = xcObjects.PBXNativeTarget[targetKey] as
47
+ | PBXNativeTarget
48
+ | undefined;
32
49
 
33
- xcObjects.XCConfigurationList[
34
- target.buildConfigurationList
35
- ].buildConfigurations.forEach((buildConfig: { value: string }) => {
36
- const buildSettings =
37
- xcObjects.XCBuildConfiguration[buildConfig.value].buildSettings;
50
+ if (!xcObjects.XCBuildConfiguration) {
51
+ xcObjects.XCBuildConfiguration = {};
52
+ }
53
+ if (!xcObjects.XCConfigurationList) {
54
+ xcObjects.XCConfigurationList = {};
55
+ }
56
+ const buildConfigurationListId = target?.buildConfigurationList ?? '';
57
+ const configurationList = xcObjects.XCConfigurationList?.[
58
+ buildConfigurationListId
59
+ ] as XCConfigurationList | undefined;
60
+ const buildListConfigurationIds =
61
+ configurationList?.buildConfigurations ?? [];
62
+ for (const buildListConfigId of buildListConfigurationIds) {
63
+ const config =
64
+ xcObjects.XCBuildConfiguration[buildListConfigId.value] ?? {};
65
+ if (typeof config === 'string') {
66
+ // Ignore comments
67
+ continue;
68
+ }
69
+
70
+ const buildSettings = config.buildSettings ?? {};
38
71
  buildSettings.DEBUG_INFORMATION_FORMAT = '"dwarf-with-dsym"';
39
72
  buildSettings.ENABLE_USER_SCRIPT_SANDBOXING = '"NO"';
40
- });
73
+
74
+ config.buildSettings = buildSettings;
75
+ xcObjects.XCBuildConfiguration[buildListConfigId.value] = config;
76
+ }
41
77
  }
42
78
 
43
- function addSentrySPM(proj: any, targetName: string): void {
79
+ function addSentrySPM(proj: Project, targetName: string): void {
44
80
  const xcObjects = proj.hash.project.objects;
45
81
 
46
- const sentryFrameworkUUID = proj.generateUuid() as string;
47
- const sentrySPMUUID = proj.generateUuid() as string;
82
+ const sentryFrameworkUUID = proj.generateUuid();
83
+ const sentrySPMUUID = proj.generateUuid();
48
84
 
49
- //Check whether xcObjects already have sentry framework
85
+ // Check whether xcObjects already have sentry framework
50
86
  if (xcObjects.PBXFrameworksBuildPhase) {
51
87
  for (const key in xcObjects.PBXFrameworksBuildPhase || {}) {
52
- if (!key.endsWith('_comment')) {
53
- const frameworks = xcObjects.PBXFrameworksBuildPhase[key].files;
54
- for (const framework of frameworks) {
55
- if (framework.comment === 'Sentry in Frameworks') {
56
- return;
57
- }
88
+ const frameworkBuildPhase = xcObjects.PBXFrameworksBuildPhase[key];
89
+ if (key.endsWith('_comment') || typeof frameworkBuildPhase === 'string') {
90
+ // Ignore comments
91
+ continue;
92
+ }
93
+ for (const framework of frameworkBuildPhase.files ?? []) {
94
+ // We identify the Sentry framework by the comment "Sentry in Frameworks",
95
+ // which is set by this manager in previous runs.
96
+ if (framework.comment === 'Sentry in Frameworks') {
97
+ return;
58
98
  }
59
99
  }
60
100
  }
61
101
  }
62
102
 
103
+ if (!xcObjects.PBXBuildFile) {
104
+ xcObjects.PBXBuildFile = {};
105
+ }
63
106
  xcObjects.PBXBuildFile[sentryFrameworkUUID] = {
64
107
  isa: 'PBXBuildFile',
65
108
  productRef: sentrySPMUUID,
66
109
  productRef_comment: 'Sentry',
67
110
  };
68
- xcObjects.PBXBuildFile[sentryFrameworkUUID + '_comment'] =
111
+ xcObjects.PBXBuildFile[`${sentryFrameworkUUID}_comment`] =
69
112
  'Sentry in Frameworks';
70
113
 
71
- for (const key in xcObjects.PBXFrameworksBuildPhase || {}) {
72
- if (!key.endsWith('_comment')) {
73
- const frameworks = xcObjects.PBXFrameworksBuildPhase[key].files;
74
- frameworks.push({
75
- value: sentryFrameworkUUID,
76
- comment: 'Sentry in Frameworks',
77
- });
114
+ if (!xcObjects.PBXFrameworksBuildPhase) {
115
+ xcObjects.PBXFrameworksBuildPhase = {};
116
+ }
117
+ for (const key in xcObjects.PBXFrameworksBuildPhase) {
118
+ const value = xcObjects.PBXFrameworksBuildPhase[key];
119
+ if (key.endsWith('_comment') || typeof value === 'string') {
120
+ // Ignore comments
121
+ continue;
78
122
  }
123
+
124
+ const frameworks = value.files ?? [];
125
+ frameworks.push({
126
+ value: sentryFrameworkUUID,
127
+ comment: 'Sentry in Frameworks',
128
+ });
129
+ value.files = frameworks;
130
+
131
+ xcObjects.PBXFrameworksBuildPhase[key] = value;
79
132
  }
80
133
 
81
- const targetKey: string = Object.keys(xcObjects.PBXNativeTarget || {}).filter(
134
+ if (!xcObjects.PBXNativeTarget) {
135
+ xcObjects.PBXNativeTarget = {};
136
+ }
137
+ const targetKey = Object.keys(xcObjects.PBXNativeTarget || {}).filter(
82
138
  (key) => {
139
+ const value = xcObjects.PBXNativeTarget?.[key];
83
140
  return (
84
141
  !key.endsWith('_comment') &&
85
- xcObjects.PBXNativeTarget[key].name === targetName
142
+ typeof value !== 'string' &&
143
+ value?.name === targetName
86
144
  );
87
145
  },
88
146
  )[0];
89
- const target = xcObjects.PBXNativeTarget[targetKey];
147
+ const target = xcObjects.PBXNativeTarget[targetKey] as PBXNativeTarget;
90
148
 
91
149
  if (!target.packageProductDependencies) {
92
150
  target.packageProductDependencies = [];
@@ -96,7 +154,7 @@ function addSentrySPM(proj: any, targetName: string): void {
96
154
  comment: 'Sentry',
97
155
  });
98
156
 
99
- const sentrySwiftPackageUUID = proj.generateUuid() as string;
157
+ const sentrySwiftPackageUUID = proj.generateUuid();
100
158
  const xcProject = proj.getFirstProject().firstProject;
101
159
  if (!xcProject.packageReferences) {
102
160
  xcProject.packageReferences = [];
@@ -118,7 +176,7 @@ function addSentrySPM(proj: any, targetName: string): void {
118
176
  minimumVersion: '8.0.0',
119
177
  },
120
178
  };
121
- xcObjects.XCRemoteSwiftPackageReference[sentrySwiftPackageUUID + '_comment'] =
179
+ xcObjects.XCRemoteSwiftPackageReference[`${sentrySwiftPackageUUID}_comment`] =
122
180
  'XCRemoteSwiftPackageReference "sentry-cocoa"';
123
181
 
124
182
  if (!xcObjects.XCSwiftPackageProductDependency) {
@@ -130,38 +188,48 @@ function addSentrySPM(proj: any, targetName: string): void {
130
188
  package_comment: 'XCRemoteSwiftPackageReference "sentry-cocoa"',
131
189
  productName: 'Sentry',
132
190
  };
133
- xcObjects.XCSwiftPackageProductDependency[sentrySPMUUID + '_comment'] =
191
+ xcObjects.XCSwiftPackageProductDependency[`${sentrySPMUUID}_comment`] =
134
192
  'Sentry';
135
193
 
136
194
  clack.log.step('Added Sentry SPM dependency to your project');
137
195
  }
138
196
 
139
197
  function addUploadSymbolsScript(
140
- xcodeProject: any,
198
+ xcodeProject: Project,
141
199
  sentryProject: SentryProjectData,
142
200
  targetName: string,
143
- uploadSource = true,
201
+ uploadSource: boolean,
144
202
  ): void {
145
203
  const xcObjects = xcodeProject.hash.project.objects;
146
- const targetKey: string = Object.keys(xcObjects.PBXNativeTarget || {}).filter(
147
- (key) => {
148
- return (
149
- !key.endsWith('_comment') &&
150
- xcObjects.PBXNativeTarget[key].name === targetName
151
- );
152
- },
153
- )[0];
204
+ if (!xcObjects.PBXNativeTarget) {
205
+ xcObjects.PBXNativeTarget = {};
206
+ }
207
+ const targetKey = Object.keys(xcObjects.PBXNativeTarget).filter((key) => {
208
+ const value = xcObjects.PBXNativeTarget?.[key];
209
+ return (
210
+ !key.endsWith('_comment') &&
211
+ typeof value !== 'string' &&
212
+ value?.name === targetName
213
+ );
214
+ })[0];
215
+
216
+ if (!xcObjects.PBXShellScriptBuildPhase) {
217
+ xcObjects.PBXShellScriptBuildPhase = {};
218
+ }
219
+ for (const key in xcObjects.PBXShellScriptBuildPhase) {
220
+ const value = xcObjects.PBXShellScriptBuildPhase[key] ?? {};
221
+ if (typeof value === 'string') {
222
+ // Ignore comments
223
+ continue;
224
+ }
154
225
 
155
- for (const scriptKey in xcObjects.PBXShellScriptBuildPhase || {}) {
156
- if (!scriptKey.endsWith('_comment')) {
157
- const script = xcObjects.PBXShellScriptBuildPhase[scriptKey].shellScript;
158
- //Sentry script already exists, update it
159
- if (script.includes('sentry-cli')) {
160
- delete xcObjects.PBXShellScriptBuildPhase[scriptKey];
161
- delete xcObjects.PBXShellScriptBuildPhase[scriptKey + '_comment'];
162
- break;
163
- }
226
+ // Sentry script already exists, update it
227
+ if (value.shellScript?.includes('sentry-cli')) {
228
+ delete xcObjects.PBXShellScriptBuildPhase?.[key];
229
+ delete xcObjects.PBXShellScriptBuildPhase?.[`${key}_comment`];
230
+ break;
164
231
  }
232
+ xcObjects.PBXShellScriptBuildPhase[key] = value;
165
233
  }
166
234
 
167
235
  const isHomebrewInstalled = fs.existsSync('/opt/homebrew/bin/sentry-cli');
@@ -188,36 +256,41 @@ function addUploadSymbolsScript(
188
256
 
189
257
  export class XcodeProject {
190
258
  projectPath: string;
191
- project: any;
192
- objects: any;
259
+ project: Project;
260
+ objects: PBXObjects;
193
261
  files: ProjectFile[] | undefined;
194
262
 
263
+ /**
264
+ * Creates a new XcodeProject instance, a wrapper around the Xcode project file `<PROJECT>.xcodeproj/project.pbxproj`.
265
+ *
266
+ * @param projectPath - The path to the Xcode project file
267
+ */
195
268
  public constructor(projectPath: string) {
196
269
  this.projectPath = projectPath;
197
- this.project = xcode.project(projectPath);
270
+ this.project = createXcodeProject(projectPath);
198
271
  this.project.parseSync();
199
272
  this.objects = this.project.hash.project.objects;
200
273
  }
201
274
 
202
275
  public getAllTargets(): string[] {
203
- return Object.keys(this.objects.PBXNativeTarget || {})
276
+ const targets = this.objects.PBXNativeTarget ?? {};
277
+ return Object.keys(targets)
204
278
  .filter((key) => {
279
+ const value = targets[key];
205
280
  return (
206
281
  !key.endsWith('_comment') &&
207
- this.objects.PBXNativeTarget[key].productType.startsWith(
208
- '"com.apple.product-type.application',
209
- )
282
+ typeof value !== 'string' &&
283
+ value.productType.startsWith('"com.apple.product-type.application')
210
284
  );
211
285
  })
212
286
  .map((key) => {
213
- return this.objects.PBXNativeTarget[key].name as string;
287
+ return (targets[key] as PBXNativeTarget).name;
214
288
  });
215
289
  }
216
290
 
217
291
  public updateXcodeProject(
218
292
  sentryProject: SentryProjectData,
219
293
  target: string,
220
- apiKeys: { token: string },
221
294
  addSPMReference: boolean,
222
295
  uploadSource = true,
223
296
  ): void {
@@ -234,75 +307,89 @@ export class XcodeProject {
234
307
 
235
308
  public filesForTarget(target: string): string[] | undefined {
236
309
  const files = this.projectFiles();
237
- const fileDictionary: any = {};
310
+ const fileDictionary: Record<string, string> = {};
238
311
  files.forEach((file) => {
239
312
  fileDictionary[file.key] = file.path;
240
313
  });
241
314
 
242
- const nativeTarget = Object.keys(this.objects.PBXNativeTarget || {}).filter(
243
- (key) => {
244
- return (
245
- !key.endsWith('_comment') &&
246
- this.objects.PBXNativeTarget[key].name === target
247
- );
248
- },
249
- )[0];
315
+ const targets = this.objects.PBXNativeTarget || {};
316
+ const nativeTarget = Object.keys(targets).filter((key) => {
317
+ const value = targets[key];
318
+ return (
319
+ !key.endsWith('_comment') &&
320
+ typeof value !== 'string' &&
321
+ value.name === target
322
+ );
323
+ })[0];
250
324
 
251
325
  if (nativeTarget === undefined) {
252
326
  return undefined;
253
327
  }
254
328
 
255
- const buildPhaseKey = this.objects.PBXNativeTarget[
256
- nativeTarget
257
- ].buildPhases.filter((phase: any) => {
258
- return this.objects.PBXSourcesBuildPhase[phase.value] !== undefined;
329
+ const buildPhaseKey = (
330
+ targets[nativeTarget] as PBXNativeTarget
331
+ ).buildPhases?.filter((phase) => {
332
+ return this.objects.PBXSourcesBuildPhase?.[phase.value] !== undefined;
259
333
  })[0];
260
334
 
261
335
  if (buildPhaseKey === undefined) {
262
336
  return undefined;
263
337
  }
264
338
 
265
- const buildPhases = this.objects.PBXSourcesBuildPhase[buildPhaseKey.value];
266
- if (buildPhases === undefined) {
267
- return undefined;
268
- }
339
+ const buildPhase = this.objects.PBXSourcesBuildPhase?.[
340
+ buildPhaseKey.value
341
+ ] as PBXSourcesBuildPhase;
342
+ const buildPhaseFiles = buildPhase?.files ?? [];
269
343
 
270
344
  const baseDir = path.dirname(path.dirname(this.projectPath));
271
345
 
272
- return buildPhases.files
273
- .map((file: any) => {
274
- const buildFile = fileDictionary[
275
- this.objects.PBXBuildFile[file.value].fileRef
276
- ] as string;
346
+ return buildPhaseFiles
347
+ .map((file) => {
348
+ const fileRef = (
349
+ this.objects.PBXBuildFile?.[file.value] as PBXBuildFile
350
+ )?.fileRef;
351
+ if (!fileRef) {
352
+ return '';
353
+ }
354
+ const buildFile = fileDictionary[fileRef];
277
355
  if (!buildFile) {
278
356
  return '';
279
357
  }
280
358
  return path.join(baseDir, buildFile);
281
359
  })
282
- .filter((f: string) => f.length > 0) as string[];
360
+ .filter((f: string) => f.length > 0);
283
361
  }
284
362
 
285
363
  projectFiles(): ProjectFile[] {
286
364
  if (this.files === undefined) {
287
365
  const proj = this.project.getFirstProject();
288
366
  const mainGroupKey = proj.firstProject.mainGroup;
289
- const mainGroup = this.objects.PBXGroup[mainGroupKey];
367
+ const mainGroup = this.objects.PBXGroup?.[mainGroupKey];
368
+ if (!mainGroup || typeof mainGroup === 'string') {
369
+ return [];
370
+ }
290
371
  this.files = this.buildGroup(mainGroup);
291
372
  }
292
373
  return this.files;
293
374
  }
294
375
 
295
- buildGroup(group: any, path = ''): ProjectFile[] {
376
+ buildGroup(group: PBXGroup, path = ''): ProjectFile[] {
296
377
  const result: ProjectFile[] = [];
297
- for (const child of group.children) {
298
- if (this.objects.PBXFileReference[child.value]) {
299
- const fileReference = this.objects.PBXFileReference[child.value];
378
+ for (const child of group.children ?? []) {
379
+ const fileReference = this.objects.PBXFileReference?.[child.value];
380
+ const groupReference = this.objects.PBXGroup?.[child.value];
381
+ if (fileReference) {
382
+ if (typeof fileReference === 'string') {
383
+ continue;
384
+ }
300
385
  result.push({
301
386
  key: child.value,
302
387
  path: `${path}${fileReference.path.replace(/"/g, '')}`,
303
388
  });
304
- } else if (this.objects.PBXGroup[child.value]) {
305
- const groupReference = this.objects.PBXGroup[child.value];
389
+ } else if (groupReference) {
390
+ if (typeof groupReference === 'string') {
391
+ continue;
392
+ }
306
393
  const groupChildren = this.buildGroup(
307
394
  groupReference,
308
395
  groupReference.path
@@ -65,10 +65,12 @@ export function runNextjsWizard(options: WizardOptions) {
65
65
  export async function runNextjsWizardWithTelemetry(
66
66
  options: WizardOptions,
67
67
  ): Promise<void> {
68
+ const { promoCode, telemetryEnabled, forceInstall } = options;
69
+
68
70
  printWelcome({
69
71
  wizardName: 'Sentry Next.js Wizard',
70
- promoCode: options.promoCode,
71
- telemetryEnabled: options.telemetryEnabled,
72
+ promoCode,
73
+ telemetryEnabled,
72
74
  });
73
75
 
74
76
  const typeScriptDetected = isUsingTypeScript();
@@ -96,6 +98,7 @@ export async function runNextjsWizardWithTelemetry(
96
98
  packageName: '@sentry/nextjs@^8',
97
99
  packageNameDisplayLabel: '@sentry/nextjs',
98
100
  alreadyInstalled: !!packageJson?.dependencies?.['@sentry/nextjs'],
101
+ forceInstall,
99
102
  });
100
103
 
101
104
  await traceStep('configure-sdk', async () => {
@@ -50,10 +50,12 @@ export function runNuxtWizard(options: WizardOptions) {
50
50
  export async function runNuxtWizardWithTelemetry(
51
51
  options: WizardOptions,
52
52
  ): Promise<void> {
53
+ const { promoCode, telemetryEnabled, forceInstall } = options;
54
+
53
55
  printWelcome({
54
56
  wizardName: 'Sentry Nuxt Wizard',
55
- promoCode: options.promoCode,
56
- telemetryEnabled: options.telemetryEnabled,
57
+ promoCode,
58
+ telemetryEnabled,
57
59
  });
58
60
 
59
61
  await confirmContinueIfNoOrDirtyGitRepo();
@@ -95,7 +97,7 @@ export async function runNuxtWizardWithTelemetry(
95
97
 
96
98
  const packageManager = await getPackageManager();
97
99
 
98
- await addNuxtOverrides(packageJson, packageManager, minVer);
100
+ await addNuxtOverrides(packageJson, packageManager, minVer, forceInstall);
99
101
 
100
102
  const sdkAlreadyInstalled = hasPackageInstalled('@sentry/nuxt', packageJson);
101
103
  Sentry.setTag('sdk-already-installed', sdkAlreadyInstalled);
@@ -104,6 +106,7 @@ export async function runNuxtWizardWithTelemetry(
104
106
  packageName: '@sentry/nuxt',
105
107
  alreadyInstalled: sdkAlreadyInstalled,
106
108
  packageManager,
109
+ forceInstall,
107
110
  });
108
111
 
109
112
  await addDotEnvSentryBuildPluginFile(authToken);
@@ -250,6 +250,7 @@ export async function addNuxtOverrides(
250
250
  packageJson: PackageDotJson,
251
251
  packageManager: PackageManager,
252
252
  nuxtMinVer: SemVer | null,
253
+ forceInstall?: boolean,
253
254
  ) {
254
255
  const isPNPM = PNPM.detect();
255
256
 
@@ -306,6 +307,7 @@ export async function addNuxtOverrides(
306
307
  packageName: 'import-in-the-middle',
307
308
  alreadyInstalled: iitmAlreadyInstalled,
308
309
  packageManager,
310
+ forceInstall,
309
311
  });
310
312
  }
311
313
  }
@@ -111,10 +111,12 @@ export async function runReactNativeWizardWithTelemetry(
111
111
  return runReactNativeUninstall(options);
112
112
  }
113
113
 
114
+ const { promoCode, telemetryEnabled, forceInstall } = options;
115
+
114
116
  printWelcome({
115
117
  wizardName: 'Sentry React Native Wizard',
116
- promoCode: options.promoCode,
117
- telemetryEnabled: options.telemetryEnabled,
118
+ promoCode,
119
+ telemetryEnabled,
118
120
  });
119
121
 
120
122
  await confirmContinueIfNoOrDirtyGitRepo();
@@ -147,6 +149,7 @@ Or setup using ${chalk.cyan(
147
149
  await installPackage({
148
150
  packageName: RN_SDK_PACKAGE,
149
151
  alreadyInstalled: hasPackageInstalled(RN_SDK_PACKAGE, packageJson),
152
+ forceInstall,
150
153
  });
151
154
  const sdkVersion = getPackageVersion(
152
155
  RN_SDK_PACKAGE,
@@ -52,10 +52,12 @@ export async function runRemixWizard(options: WizardOptions): Promise<void> {
52
52
  async function runRemixWizardWithTelemetry(
53
53
  options: WizardOptions,
54
54
  ): Promise<void> {
55
+ const { promoCode, telemetryEnabled, forceInstall } = options;
56
+
55
57
  printWelcome({
56
58
  wizardName: 'Sentry Remix Wizard',
57
- promoCode: options.promoCode,
58
- telemetryEnabled: options.telemetryEnabled,
59
+ promoCode,
60
+ telemetryEnabled,
59
61
  });
60
62
 
61
63
  await confirmContinueIfNoOrDirtyGitRepo();
@@ -73,6 +75,7 @@ async function runRemixWizardWithTelemetry(
73
75
  packageName: '@sentry/remix@^8',
74
76
  packageNameDisplayLabel: '@sentry/remix',
75
77
  alreadyInstalled: hasPackageInstalled('@sentry/remix', packageJson),
78
+ forceInstall,
76
79
  });
77
80
 
78
81
  const dsn = selectedProject.keys[0].dsn.public;
package/src/run.ts CHANGED
@@ -56,6 +56,7 @@ type Args = {
56
56
  org?: string;
57
57
  project?: string;
58
58
  saas?: boolean;
59
+ forceInstall?: boolean;
59
60
  };
60
61
 
61
62
  function preSelectedProjectArgsToObject(
@@ -132,6 +133,7 @@ export async function run(argv: Args) {
132
133
  projectSlug: finalArgs.project,
133
134
  saas: finalArgs.saas,
134
135
  preSelectedProject: preSelectedProjectArgsToObject(finalArgs),
136
+ forceInstall: finalArgs.forceInstall,
135
137
  };
136
138
 
137
139
  switch (integration) {
@@ -41,10 +41,12 @@ export async function runSvelteKitWizard(
41
41
  export async function runSvelteKitWizardWithTelemetry(
42
42
  options: WizardOptions,
43
43
  ): Promise<void> {
44
+ const { promoCode, telemetryEnabled, forceInstall } = options;
45
+
44
46
  printWelcome({
45
47
  wizardName: 'Sentry SvelteKit Wizard',
46
- promoCode: options.promoCode,
47
- telemetryEnabled: options.telemetryEnabled,
48
+ promoCode,
49
+ telemetryEnabled,
48
50
  });
49
51
 
50
52
  await confirmContinueIfNoOrDirtyGitRepo();
@@ -98,6 +100,7 @@ export async function runSvelteKitWizardWithTelemetry(
98
100
  packageName: '@sentry/sveltekit@^8',
99
101
  packageNameDisplayLabel: '@sentry/sveltekit',
100
102
  alreadyInstalled: sdkAlreadyInstalled,
103
+ forceInstall,
101
104
  });
102
105
 
103
106
  await addDotEnvSentryBuildPluginFile(authToken);
@@ -207,6 +207,8 @@ export async function confirmContinueIfNoOrDirtyGitRepo(): Promise<void> {
207
207
  if (!continueWithoutGit) {
208
208
  await abort(undefined, 0);
209
209
  }
210
+ // return early to avoid checking for uncommitted files
211
+ return;
210
212
  }
211
213
 
212
214
  const uncommittedOrUntrackedFiles = getUncommittedOrUntrackedFiles();
@@ -247,7 +249,10 @@ export function isInGitRepo() {
247
249
  export function getUncommittedOrUntrackedFiles(): string[] {
248
250
  try {
249
251
  const gitStatus = childProcess
250
- .execSync('git status --porcelain=v1')
252
+ .execSync('git status --porcelain=v1', {
253
+ // we only care about stdout
254
+ stdio: ['ignore', 'pipe', 'ignore'],
255
+ })
251
256
  .toString();
252
257
 
253
258
  const files = gitStatus
@@ -356,6 +361,7 @@ export async function installPackage({
356
361
  askBeforeUpdating = true,
357
362
  packageNameDisplayLabel,
358
363
  packageManager,
364
+ forceInstall = false,
359
365
  }: {
360
366
  /** The string that is passed to the package manager CLI as identifier to install (e.g. `@sentry/nextjs`, or `@sentry/nextjs@^8`) */
361
367
  packageName: string;
@@ -364,6 +370,8 @@ export async function installPackage({
364
370
  /** Overrides what is shown in the installation logs in place of the `packageName` option. Useful if the `packageName` is ugly (e.g. `@sentry/nextjs@^8`) */
365
371
  packageNameDisplayLabel?: string;
366
372
  packageManager?: PackageManager;
373
+ /** Add force install flag to command to skip install precondition fails */
374
+ forceInstall?: boolean;
367
375
  }): Promise<{ packageManager?: PackageManager }> {
368
376
  return traceStep('install-package', async () => {
369
377
  if (alreadyInstalled && askBeforeUpdating) {
@@ -393,7 +401,9 @@ export async function installPackage({
393
401
  try {
394
402
  await new Promise<void>((resolve, reject) => {
395
403
  childProcess.exec(
396
- `${pkgManager.installCommand} ${packageName} ${pkgManager.flags}`,
404
+ `${pkgManager.installCommand} ${packageName} ${pkgManager.flags} ${
405
+ forceInstall ? pkgManager.forceInstallFlag : ''
406
+ }`,
397
407
  (err, stdout, stderr) => {
398
408
  if (err) {
399
409
  // Write a log file so we can better troubleshoot issues