@sentry/wizard 3.16.5 → 3.18.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 (46) hide show
  1. package/CHANGELOG.md +13 -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/nextjs/nextjs-wizard.js +76 -21
  6. package/dist/src/nextjs/nextjs-wizard.js.map +1 -1
  7. package/dist/src/nextjs/templates.d.ts +2 -0
  8. package/dist/src/nextjs/templates.js +15 -2
  9. package/dist/src/nextjs/templates.js.map +1 -1
  10. package/dist/src/react-native/metro.d.ts +13 -0
  11. package/dist/src/react-native/metro.js +398 -0
  12. package/dist/src/react-native/metro.js.map +1 -0
  13. package/dist/src/react-native/react-native-wizard.d.ts +2 -0
  14. package/dist/src/react-native/react-native-wizard.js +139 -38
  15. package/dist/src/react-native/react-native-wizard.js.map +1 -1
  16. package/dist/src/react-native/uninstall.js +4 -0
  17. package/dist/src/react-native/uninstall.js.map +1 -1
  18. package/dist/src/react-native/xcode.d.ts +7 -3
  19. package/dist/src/react-native/xcode.js +43 -11
  20. package/dist/src/react-native/xcode.js.map +1 -1
  21. package/dist/src/remix/remix-wizard.js +80 -37
  22. package/dist/src/remix/remix-wizard.js.map +1 -1
  23. package/dist/src/remix/sdk-setup.d.ts +1 -0
  24. package/dist/src/remix/sdk-setup.js +21 -1
  25. package/dist/src/remix/sdk-setup.js.map +1 -1
  26. package/dist/src/utils/ast-utils.d.ts +14 -0
  27. package/dist/src/utils/ast-utils.js +49 -1
  28. package/dist/src/utils/ast-utils.js.map +1 -1
  29. package/dist/test/react-native/metro.test.d.ts +1 -0
  30. package/dist/test/react-native/metro.test.js +125 -0
  31. package/dist/test/react-native/metro.test.js.map +1 -0
  32. package/dist/test/react-native/xcode.test.js +40 -2
  33. package/dist/test/react-native/xcode.test.js.map +1 -1
  34. package/package.json +2 -2
  35. package/src/apple/cocoapod.ts +2 -0
  36. package/src/nextjs/nextjs-wizard.ts +99 -19
  37. package/src/nextjs/templates.ts +77 -0
  38. package/src/react-native/metro.ts +409 -0
  39. package/src/react-native/react-native-wizard.ts +103 -7
  40. package/src/react-native/uninstall.ts +3 -0
  41. package/src/react-native/xcode.ts +70 -12
  42. package/src/remix/remix-wizard.ts +51 -15
  43. package/src/remix/sdk-setup.ts +31 -0
  44. package/src/utils/ast-utils.ts +52 -0
  45. package/test/react-native/metro.test.ts +283 -0
  46. package/test/react-native/xcode.test.ts +76 -3
@@ -21,7 +21,10 @@ export function getValidExistingBuildPhases(xcodeProject: any): BuildPhaseMap {
21
21
  return map;
22
22
  }
23
23
 
24
- export function patchBundlePhase(bundlePhase: BuildPhase | undefined) {
24
+ export function patchBundlePhase(
25
+ bundlePhase: BuildPhase | undefined,
26
+ patch: (script: string) => string,
27
+ ) {
25
28
  if (!bundlePhase) {
26
29
  clack.log.warn(
27
30
  `Could not find ${chalk.cyan(
@@ -42,9 +45,7 @@ export function patchBundlePhase(bundlePhase: BuildPhase | undefined) {
42
45
  }
43
46
 
44
47
  const script: string = JSON.parse(bundlePhase.shellScript);
45
- bundlePhase.shellScript = JSON.stringify(
46
- addSentryToBundleShellScript(script),
47
- );
48
+ bundlePhase.shellScript = JSON.stringify(patch(script));
48
49
  clack.log.success(
49
50
  `Patched Build phase ${chalk.cyan('Bundle React Native code and images')}.`,
50
51
  );
@@ -60,7 +61,10 @@ export function unPatchBundlePhase(bundlePhase: BuildPhase | undefined) {
60
61
  return;
61
62
  }
62
63
 
63
- if (!bundlePhase.shellScript.match(/sentry-cli\s+react-native\s+xcode/i)) {
64
+ if (
65
+ !bundlePhase.shellScript.match(/sentry-cli\s+react-native\s+xcode/i) &&
66
+ !bundlePhase.shellScript.includes('sentry-xcode.sh')
67
+ ) {
64
68
  clack.log.success(
65
69
  `Build phase ${chalk.cyan(
66
70
  'Bundle React Native code and images',
@@ -97,6 +101,12 @@ export function removeSentryFromBundleShellScript(script: string): string {
97
101
  /\.\.\/node_modules\/@sentry\/cli\/bin\/sentry-cli\s+react-native\s+xcode\s+\$REACT_NATIVE_XCODE/i,
98
102
  '$REACT_NATIVE_XCODE',
99
103
  )
104
+ .replace(
105
+ // eslint-disable-next-line no-useless-escape
106
+ /\"\/bin\/sh.*?sentry-xcode.sh\s+\$REACT_NATIVE_XCODE/i,
107
+ // eslint-disable-next-line no-useless-escape
108
+ '"$REACT_NATIVE_XCODE',
109
+ )
100
110
  );
101
111
  }
102
112
 
@@ -107,10 +117,25 @@ export function findBundlePhase(buildPhases: BuildPhaseMap) {
107
117
  }
108
118
 
109
119
  export function doesBundlePhaseIncludeSentry(buildPhase: BuildPhase) {
110
- return !!buildPhase.shellScript.match(/sentry-cli\s+react-native\s+xcode/i);
120
+ const containsSentryCliRNCommand = !!buildPhase.shellScript.match(
121
+ /sentry-cli\s+react-native\s+xcode/i,
122
+ );
123
+ const containsBundledScript =
124
+ buildPhase.shellScript.includes('sentry-xcode.sh');
125
+ return containsSentryCliRNCommand || containsBundledScript;
126
+ }
127
+
128
+ export function addSentryWithBundledScriptsToBundleShellScript(
129
+ script: string,
130
+ ): string {
131
+ return script.replace(
132
+ '$REACT_NATIVE_XCODE',
133
+ // eslint-disable-next-line no-useless-escape
134
+ '\\"/bin/sh ../node_modules/@sentry/react-native/scripts/sentry-xcode.sh $REACT_NATIVE_XCODE\\"',
135
+ );
111
136
  }
112
137
 
113
- export function addSentryToBundleShellScript(script: string): string {
138
+ export function addSentryWithCliToBundleShellScript(script: string): string {
114
139
  return (
115
140
  'export SENTRY_PROPERTIES=sentry.properties\n' +
116
141
  'export EXTRA_PACKAGER_ARGS="--sourcemap-output $DERIVED_FILE_DIR/main.jsbundle.map"\n' +
@@ -124,7 +149,36 @@ export function addSentryToBundleShellScript(script: string): string {
124
149
  );
125
150
  }
126
151
 
127
- export function addDebugFilesUploadPhase(
152
+ export function addDebugFilesUploadPhaseWithBundledScripts(
153
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
154
+ xcodeProject: any,
155
+ { debugFilesUploadPhaseExists }: { debugFilesUploadPhaseExists: boolean },
156
+ ) {
157
+ if (debugFilesUploadPhaseExists) {
158
+ clack.log.warn(
159
+ `Build phase ${chalk.cyan(
160
+ 'Upload Debug Symbols to Sentry',
161
+ )} already exists.`,
162
+ );
163
+ return;
164
+ }
165
+
166
+ xcodeProject.addBuildPhase(
167
+ [],
168
+ 'PBXShellScriptBuildPhase',
169
+ 'Upload Debug Symbols to Sentry',
170
+ null,
171
+ {
172
+ shellPath: '/bin/sh',
173
+ shellScript: `/bin/sh ../node_modules/@sentry/react-native/scripts/sentry-xcode-debug-files.sh`,
174
+ },
175
+ );
176
+ clack.log.success(
177
+ `Added Build phase ${chalk.cyan('Upload Debug Symbols to Sentry')}.`,
178
+ );
179
+ }
180
+
181
+ export function addDebugFilesUploadPhaseWithCli(
128
182
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
129
183
  xcodeProject: any,
130
184
  { debugFilesUploadPhaseExists }: { debugFilesUploadPhaseExists: boolean },
@@ -204,13 +258,17 @@ export function unPatchDebugFilesUploadPhase(
204
258
  export function findDebugFilesUploadPhase(
205
259
  buildPhasesMap: Record<string, BuildPhase>,
206
260
  ): [key: string, buildPhase: BuildPhase] | undefined {
207
- return Object.entries(buildPhasesMap).find(
208
- ([_, buildPhase]) =>
261
+ return Object.entries(buildPhasesMap).find(([_, buildPhase]) => {
262
+ const containsCliDebugUpload =
209
263
  typeof buildPhase !== 'string' &&
210
264
  !!buildPhase.shellScript.match(
211
265
  /sentry-cli\s+(upload-dsym|debug-files upload)\b/,
212
- ),
213
- );
266
+ );
267
+ const containsBundledDebugUpload =
268
+ typeof buildPhase !== 'string' &&
269
+ buildPhase.shellScript.includes('sentry-xcode-debug-files.sh');
270
+ return containsCliDebugUpload || containsBundledDebugUpload;
271
+ });
214
272
  }
215
273
 
216
274
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -22,11 +22,14 @@ import {
22
22
  instrumentRootRoute,
23
23
  isRemixV2,
24
24
  loadRemixConfig,
25
+ runRemixReveal,
25
26
  } from './sdk-setup';
26
27
  import { debug } from '../utils/debug';
27
28
  import { traceStep, withTelemetry } from '../telemetry';
28
29
  import { isHydrogenApp } from './utils';
29
30
  import { DEFAULT_URL } from '../../lib/Constants';
31
+ import { findFile } from '../utils/ast-utils';
32
+ import { configureVitePlugin } from '../sourcemaps/tools/vite';
30
33
 
31
34
  export async function runRemixWizard(options: WizardOptions): Promise<void> {
32
35
  return withTelemetry(
@@ -55,7 +58,7 @@ async function runRemixWizardWithTelemetry(
55
58
  // We expect `@remix-run/dev` to be installed for every Remix project
56
59
  await ensurePackageIsInstalled(packageJson, '@remix-run/dev', 'Remix');
57
60
 
58
- const { selectedProject, authToken, sentryUrl } =
61
+ const { selectedProject, authToken, sentryUrl, selfHosted } =
59
62
  await getOrAskForProjectData(options, 'javascript-remix');
60
63
 
61
64
  await installPackage({
@@ -67,24 +70,47 @@ async function runRemixWizardWithTelemetry(
67
70
 
68
71
  const isTS = isUsingTypeScript();
69
72
  const isV2 = isRemixV2(remixConfig, packageJson);
73
+ const viteConfig = findFile('vite.config');
70
74
 
71
75
  await addSentryCliConfig({ authToken }, rcCliSetupConfig);
72
76
 
73
- await traceStep('Update build script for sourcemap uploads', async () => {
74
- try {
75
- await updateBuildScript({
76
- org: selectedProject.organization.slug,
77
- project: selectedProject.slug,
78
- url: sentryUrl === DEFAULT_URL ? undefined : sentryUrl,
79
- isHydrogen: isHydrogenApp(packageJson),
80
- });
81
- } catch (e) {
82
- clack.log
83
- .warn(`Could not update build script to generate and upload sourcemaps.
77
+ if (viteConfig) {
78
+ await traceStep(
79
+ 'Update vite configuration for sourcemap uploads',
80
+ async () => {
81
+ try {
82
+ await configureVitePlugin({
83
+ orgSlug: selectedProject.organization.slug,
84
+ projectSlug: selectedProject.slug,
85
+ url: sentryUrl,
86
+ selfHosted,
87
+ authToken,
88
+ });
89
+ } catch (e) {
90
+ clack.log
91
+ .warn(`Could not update vite configuration to generate and upload sourcemaps.
92
+ Please update your vite configuration manually using instructions from https://docs.sentry.io/platforms/javascript/guides/remix/sourcemaps/`);
93
+ debug(e);
94
+ }
95
+ },
96
+ );
97
+ } else {
98
+ await traceStep('Update build script for sourcemap uploads', async () => {
99
+ try {
100
+ await updateBuildScript({
101
+ org: selectedProject.organization.slug,
102
+ project: selectedProject.slug,
103
+ url: sentryUrl === DEFAULT_URL ? undefined : sentryUrl,
104
+ isHydrogen: isHydrogenApp(packageJson),
105
+ });
106
+ } catch (e) {
107
+ clack.log
108
+ .warn(`Could not update build script to generate and upload sourcemaps.
84
109
  Please update your build script manually using instructions from https://docs.sentry.io/platforms/javascript/guides/remix/sourcemaps/`);
85
- debug(e);
86
- }
87
- });
110
+ debug(e);
111
+ }
112
+ });
113
+ }
88
114
 
89
115
  await traceStep('Instrument root route', async () => {
90
116
  try {
@@ -96,6 +122,16 @@ async function runRemixWizardWithTelemetry(
96
122
  }
97
123
  });
98
124
 
125
+ traceStep('Reveal missing entry files', () => {
126
+ try {
127
+ runRemixReveal(isTS);
128
+ } catch (e) {
129
+ clack.log.warn(`Could not run 'npx remix reveal'.
130
+ Please create your entry files manually`);
131
+ debug(e);
132
+ }
133
+ });
134
+
99
135
  await traceStep('Initialize Sentry on client entry', async () => {
100
136
  try {
101
137
  await initializeSentryOnEntryClient(dsn, isTS);
@@ -8,6 +8,7 @@ import type { ProxifiedModule } from 'magicast';
8
8
  import * as fs from 'fs';
9
9
  import * as path from 'path';
10
10
  import * as url from 'url';
11
+ import * as childProcess from 'child_process';
11
12
 
12
13
  // @ts-expect-error - clack is ESM and TS complains about that. It works though
13
14
  import clack from '@clack/prompts';
@@ -36,6 +37,32 @@ export type PartialRemixConfig = {
36
37
  };
37
38
 
38
39
  const REMIX_CONFIG_FILE = 'remix.config.js';
40
+ const REMIX_REVEAL_COMMAND = 'npx remix reveal';
41
+
42
+ export function runRemixReveal(isTS: boolean): void {
43
+ // Check if entry files already exist
44
+ const clientEntryFilename = `entry.client.${isTS ? 'tsx' : 'jsx'}`;
45
+ const serverEntryFilename = `entry.server.${isTS ? 'tsx' : 'jsx'}`;
46
+
47
+ const clientEntryPath = path.join(process.cwd(), 'app', clientEntryFilename);
48
+ const serverEntryPath = path.join(process.cwd(), 'app', serverEntryFilename);
49
+
50
+ if (fs.existsSync(clientEntryPath) && fs.existsSync(serverEntryPath)) {
51
+ clack.log.info(
52
+ `Found entry files ${chalk.cyan(clientEntryFilename)} and ${chalk.cyan(
53
+ serverEntryFilename,
54
+ )}.`,
55
+ );
56
+ } else {
57
+ clack.log.info(
58
+ `Couldn't find entry files in your project. Trying to run ${chalk.cyan(
59
+ REMIX_REVEAL_COMMAND,
60
+ )}...`,
61
+ );
62
+
63
+ clack.log.info(childProcess.execSync(REMIX_REVEAL_COMMAND).toString());
64
+ }
65
+ }
39
66
 
40
67
  function insertClientInitCall(
41
68
  dsn: string,
@@ -192,6 +219,10 @@ export async function updateBuildScript(args: {
192
219
  buildCommand,
193
220
  instrumentedBuildCommand,
194
221
  );
222
+ } else {
223
+ throw new Error(
224
+ "`build` script doesn't contain a known build command. Please update it manually.",
225
+ );
195
226
  }
196
227
 
197
228
  await fs.promises.writeFile(
@@ -216,3 +216,55 @@ export function printJsonC(ast: t.Program): string {
216
216
  const js = recast.print(ast).code;
217
217
  return js.substring(1, js.length - 1);
218
218
  }
219
+
220
+ /**
221
+ * Walks the program body and returns index of the last variable assignment initialized by require statement.
222
+ * Only counts top level require statements.
223
+ *
224
+ * @returns index of the last `const foo = require('bar');` statement
225
+ */
226
+ export function getLastRequireIndex(program: t.Program): number {
227
+ let lastRequireIdex = 0;
228
+ program.body.forEach((s, i) => {
229
+ if (
230
+ s.type === 'VariableDeclaration' &&
231
+ s.declarations[0].type === 'VariableDeclarator' &&
232
+ s.declarations[0].init !== null &&
233
+ typeof s.declarations[0].init !== 'undefined' &&
234
+ s.declarations[0].init.type === 'CallExpression' &&
235
+ s.declarations[0].init.callee.type === 'Identifier' &&
236
+ s.declarations[0].init.callee.name === 'require'
237
+ ) {
238
+ lastRequireIdex = i;
239
+ }
240
+ });
241
+ return lastRequireIdex;
242
+ }
243
+
244
+ /**
245
+ * Walks the statements and removes require statements which first argument includes the predicate.
246
+ * Only removes top level require statements like `const foo = require('bar');`
247
+ *
248
+ * @returns True if any require statement was removed.
249
+ */
250
+ export function removeRequire(program: t.Program, predicate: string): boolean {
251
+ let removedAtLeastOne = false;
252
+ program.body = program.body.filter((s) => {
253
+ if (
254
+ s.type === 'VariableDeclaration' &&
255
+ s.declarations[0].type === 'VariableDeclarator' &&
256
+ s.declarations[0].init !== null &&
257
+ typeof s.declarations[0].init !== 'undefined' &&
258
+ s.declarations[0].init.type === 'CallExpression' &&
259
+ s.declarations[0].init.callee.type === 'Identifier' &&
260
+ s.declarations[0].init.callee.name === 'require' &&
261
+ s.declarations[0].init.arguments[0].type === 'StringLiteral' &&
262
+ s.declarations[0].init.arguments[0].value.includes(predicate)
263
+ ) {
264
+ removedAtLeastOne = true;
265
+ return false;
266
+ }
267
+ return true;
268
+ });
269
+ return removedAtLeastOne;
270
+ }
@@ -0,0 +1,283 @@
1
+ // @ts-ignore - magicast is ESM and TS complains about that. It works though
2
+ import { generateCode, type ProxifiedModule, parseModule } from 'magicast';
3
+
4
+ import * as recast from 'recast';
5
+ import x = recast.types;
6
+ import t = x.namedTypes;
7
+
8
+ import {
9
+ addSentrySerializerRequireToMetroConfig,
10
+ addSentrySerializerToMetroConfig,
11
+ getMetroConfigObject,
12
+ removeSentryRequire,
13
+ removeSentrySerializerFromMetroConfig,
14
+ } from '../../src/react-native/metro';
15
+
16
+ describe('patch metro config - sentry serializer', () => {
17
+ describe('addSentrySerializerToMetroConfig', () => {
18
+ it('add to empty config', () => {
19
+ const mod = parseModule(`module.exports = {
20
+ other: 'config'
21
+ }`);
22
+ const configObject = getModuleExportsObject(mod);
23
+ const result = addSentrySerializerToMetroConfig(configObject);
24
+ expect(result).toBe(true);
25
+ expect(generateCode(mod.$ast).code).toBe(`module.exports = {
26
+ other: 'config',
27
+
28
+ serializer: {
29
+ customSerializer: createSentryMetroSerializer()
30
+ }
31
+ }`);
32
+ });
33
+
34
+ it('add to existing serializer config', () => {
35
+ const mod = parseModule(`module.exports = {
36
+ other: 'config',
37
+ serializer: {
38
+ other: 'config'
39
+ }
40
+ }`);
41
+ const configObject = getModuleExportsObject(mod);
42
+ const result = addSentrySerializerToMetroConfig(configObject);
43
+ expect(result).toBe(true);
44
+ expect(generateCode(mod.$ast).code).toBe(`module.exports = {
45
+ other: 'config',
46
+ serializer: {
47
+ other: 'config',
48
+ customSerializer: createSentryMetroSerializer()
49
+ }
50
+ }`);
51
+ });
52
+
53
+ it('not add to existing customSerializer config', () => {
54
+ const mod = parseModule(`module.exports = {
55
+ other: 'config',
56
+ serializer: {
57
+ other: 'config',
58
+ customSerializer: 'existing-serializer'
59
+ }
60
+ }`);
61
+ const configObject = getModuleExportsObject(mod);
62
+ const result = addSentrySerializerToMetroConfig(configObject);
63
+ expect(result).toBe(false);
64
+ expect(generateCode(mod.$ast).code).toBe(`module.exports = {
65
+ other: 'config',
66
+ serializer: {
67
+ other: 'config',
68
+ customSerializer: 'existing-serializer'
69
+ }
70
+ }`);
71
+ });
72
+ });
73
+
74
+ describe('addSentrySerializerImportToMetroConfig', () => {
75
+ it('add import', () => {
76
+ const mod =
77
+ parseModule(`const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
78
+
79
+ module.exports = {
80
+ other: 'config'
81
+ }`);
82
+ const result = addSentrySerializerRequireToMetroConfig(
83
+ mod.$ast as t.Program,
84
+ );
85
+ expect(result).toBe(true);
86
+ expect(generateCode(mod.$ast).code)
87
+ .toBe(`const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
88
+
89
+ const {
90
+ createSentryMetroSerializer
91
+ } = require("@sentry/react-native/dist/js/tools/sentryMetroSerializer");
92
+
93
+ module.exports = {
94
+ other: 'config'
95
+ }`);
96
+ });
97
+ });
98
+
99
+ describe('getMetroConfigObject', () => {
100
+ it('get config object from variable called config', () => {
101
+ const mod = parseModule(`var config = { some: 'config' };`);
102
+ const configObject = getMetroConfigObject(mod.$ast as t.Program);
103
+ expect(
104
+ ((configObject?.properties[0] as t.ObjectProperty).key as t.Identifier)
105
+ .name,
106
+ ).toBe('some');
107
+ expect(
108
+ (
109
+ (configObject?.properties[0] as t.ObjectProperty)
110
+ .value as t.StringLiteral
111
+ ).value,
112
+ ).toBe('config');
113
+ });
114
+
115
+ it('get config object from const called config', () => {
116
+ const mod = parseModule(`const config = { some: 'config' };`);
117
+ const configObject = getMetroConfigObject(mod.$ast as t.Program);
118
+ expect(
119
+ ((configObject?.properties[0] as t.ObjectProperty).key as t.Identifier)
120
+ .name,
121
+ ).toBe('some');
122
+ expect(
123
+ (
124
+ (configObject?.properties[0] as t.ObjectProperty)
125
+ .value as t.StringLiteral
126
+ ).value,
127
+ ).toBe('config');
128
+ });
129
+
130
+ it('get config oject from let called config', () => {
131
+ const mod = parseModule(`let config = { some: 'config' };`);
132
+ const configObject = getMetroConfigObject(mod.$ast as t.Program);
133
+ expect(
134
+ ((configObject?.properties[0] as t.ObjectProperty).key as t.Identifier)
135
+ .name,
136
+ ).toBe('some');
137
+ expect(
138
+ (
139
+ (configObject?.properties[0] as t.ObjectProperty)
140
+ .value as t.StringLiteral
141
+ ).value,
142
+ ).toBe('config');
143
+ });
144
+
145
+ it('get config object from module.exports', () => {
146
+ const mod = parseModule(`module.exports = { some: 'config' };`);
147
+ const configObject = getMetroConfigObject(mod.$ast as t.Program);
148
+ expect(
149
+ ((configObject?.properties[0] as t.ObjectProperty).key as t.Identifier)
150
+ .name,
151
+ ).toBe('some');
152
+ expect(
153
+ (
154
+ (configObject?.properties[0] as t.ObjectProperty)
155
+ .value as t.StringLiteral
156
+ ).value,
157
+ ).toBe('config');
158
+ });
159
+ });
160
+
161
+ describe('remove @sentry require', () => {
162
+ it('nothing to remove', () => {
163
+ const mod = parseModule(`let config = { some: 'config' };`);
164
+ const result = removeSentryRequire(mod.$ast as t.Program);
165
+ expect(result).toBe(false);
166
+ expect(generateCode(mod.$ast).code).toBe(
167
+ `let config = { some: 'config' };`,
168
+ );
169
+ });
170
+
171
+ it('remove metro serializer import', () => {
172
+ const mod = parseModule(`const {
173
+ createSentryMetroSerializer,
174
+ } = require('@sentry/react-native/dist/js/tools/sentryMetroSerializer');
175
+ let config = { some: 'config' };`);
176
+ const result = removeSentryRequire(mod.$ast as t.Program);
177
+ expect(result).toBe(true);
178
+ expect(generateCode(mod.$ast).code).toBe(
179
+ `let config = { some: 'config' };`,
180
+ );
181
+ });
182
+
183
+ it('remove all sentry imports', () => {
184
+ const mod = parseModule(`const {
185
+ createSentryMetroSerializer,
186
+ } = require('@sentry/react-native/dist/js/tools/sentryMetroSerializer');
187
+ var Sentry = require('@sentry/react-native');
188
+ let SentryIntegrations = require('@sentry/integrations');
189
+
190
+ let config = { some: 'config' };`);
191
+ const result = removeSentryRequire(mod.$ast as t.Program);
192
+ expect(result).toBe(true);
193
+ expect(generateCode(mod.$ast).code).toBe(
194
+ `let config = { some: 'config' };`,
195
+ );
196
+ });
197
+ });
198
+
199
+ describe('remove sentryMetroSerializer', () => {
200
+ it('no custom serializer to remove', () => {
201
+ const mod = parseModule(`let config = { some: 'config' };`);
202
+ const result = removeSentrySerializerFromMetroConfig(
203
+ mod.$ast as t.Program,
204
+ );
205
+ expect(result).toBe(false);
206
+ expect(generateCode(mod.$ast).code).toBe(
207
+ `let config = { some: 'config' };`,
208
+ );
209
+ });
210
+
211
+ it('no Sentry custom serializer to remove', () => {
212
+ const mod = parseModule(`let config = {
213
+ serializer: {
214
+ customSerializer: 'existing-serializer',
215
+ other: 'config',
216
+ },
217
+ other: 'config',
218
+ };`);
219
+ const result = removeSentrySerializerFromMetroConfig(
220
+ mod.$ast as t.Program,
221
+ );
222
+ expect(result).toBe(false);
223
+ expect(generateCode(mod.$ast).code).toBe(`let config = {
224
+ serializer: {
225
+ customSerializer: 'existing-serializer',
226
+ other: 'config',
227
+ },
228
+ other: 'config',
229
+ };`);
230
+ });
231
+
232
+ it('Sentry serializer to remove', () => {
233
+ const mod = parseModule(`let config = {
234
+ serializer: {
235
+ customSerializer: createSentryMetroSerializer(),
236
+ other: 'config',
237
+ },
238
+ other: 'config',
239
+ };`);
240
+ const result = removeSentrySerializerFromMetroConfig(
241
+ mod.$ast as t.Program,
242
+ );
243
+ expect(result).toBe(true);
244
+ expect(generateCode(mod.$ast).code).toBe(`let config = {
245
+ serializer: {
246
+ other: 'config'
247
+ },
248
+ other: 'config',
249
+ };`);
250
+ });
251
+
252
+ it('Sentry serializer to remove with wrapped serializer', () => {
253
+ const mod = parseModule(`let config = {
254
+ serializer: {
255
+ customSerializer: createSentryMetroSerializer(wrappedSerializer()),
256
+ other: 'config',
257
+ },
258
+ other: 'config',
259
+ };`);
260
+ const result = removeSentrySerializerFromMetroConfig(
261
+ mod.$ast as t.Program,
262
+ );
263
+ expect(result).toBe(true);
264
+ expect(generateCode(mod.$ast).code).toBe(`let config = {
265
+ serializer: {
266
+ customSerializer: wrappedSerializer(),
267
+ other: 'config',
268
+ },
269
+ other: 'config',
270
+ };`);
271
+ });
272
+ });
273
+ });
274
+
275
+ function getModuleExportsObject(
276
+ mod: ProxifiedModule,
277
+ index = 0,
278
+ ): t.ObjectExpression {
279
+ return (
280
+ ((mod.$ast as t.Program).body[index] as t.ExpressionStatement)
281
+ .expression as t.AssignmentExpression
282
+ ).right as t.ObjectExpression;
283
+ }