@sentry/wizard 3.12.0 → 3.14.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 (86) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/lib/Steps/ChooseIntegration.js +1 -0
  3. package/dist/lib/Steps/ChooseIntegration.js.map +1 -1
  4. package/dist/package.json +2 -2
  5. package/dist/src/android/android-wizard.js +8 -8
  6. package/dist/src/android/android-wizard.js.map +1 -1
  7. package/dist/src/apple/apple-wizard.js +1 -1
  8. package/dist/src/apple/apple-wizard.js.map +1 -1
  9. package/dist/src/nextjs/nextjs-wizard.d.ts +1 -0
  10. package/dist/src/nextjs/nextjs-wizard.js +257 -163
  11. package/dist/src/nextjs/nextjs-wizard.js.map +1 -1
  12. package/dist/src/nextjs/templates.d.ts +1 -1
  13. package/dist/src/nextjs/templates.js +2 -2
  14. package/dist/src/nextjs/templates.js.map +1 -1
  15. package/dist/src/nextjs/utils.d.ts +1 -0
  16. package/dist/src/nextjs/utils.js +25 -0
  17. package/dist/src/nextjs/utils.js.map +1 -0
  18. package/dist/src/remix/remix-wizard.js +13 -11
  19. package/dist/src/remix/remix-wizard.js.map +1 -1
  20. package/dist/src/remix/sdk-setup.d.ts +5 -1
  21. package/dist/src/remix/sdk-setup.js +13 -6
  22. package/dist/src/remix/sdk-setup.js.map +1 -1
  23. package/dist/src/sourcemaps/sourcemaps-wizard.js +1 -1
  24. package/dist/src/sourcemaps/sourcemaps-wizard.js.map +1 -1
  25. package/dist/src/sourcemaps/tools/sentry-cli.d.ts +9 -0
  26. package/dist/src/sourcemaps/tools/sentry-cli.js +26 -22
  27. package/dist/src/sourcemaps/tools/sentry-cli.js.map +1 -1
  28. package/dist/src/sourcemaps/tools/tsc.d.ts +6 -0
  29. package/dist/src/sourcemaps/tools/tsc.js +98 -17
  30. package/dist/src/sourcemaps/tools/tsc.js.map +1 -1
  31. package/dist/src/sourcemaps/tools/vite.js +3 -13
  32. package/dist/src/sourcemaps/tools/vite.js.map +1 -1
  33. package/dist/src/sourcemaps/tools/webpack.js +3 -13
  34. package/dist/src/sourcemaps/tools/webpack.js.map +1 -1
  35. package/dist/src/sveltekit/sdk-setup.js +122 -48
  36. package/dist/src/sveltekit/sdk-setup.js.map +1 -1
  37. package/dist/src/sveltekit/sveltekit-wizard.d.ts +1 -0
  38. package/dist/src/sveltekit/sveltekit-wizard.js +113 -42
  39. package/dist/src/sveltekit/sveltekit-wizard.js.map +1 -1
  40. package/dist/src/sveltekit/utils.d.ts +2 -0
  41. package/dist/src/sveltekit/utils.js +48 -0
  42. package/dist/src/sveltekit/utils.js.map +1 -0
  43. package/dist/src/telemetry.d.ts +1 -0
  44. package/dist/src/telemetry.js +27 -12
  45. package/dist/src/telemetry.js.map +1 -1
  46. package/dist/src/utils/ast-utils.d.ts +70 -0
  47. package/dist/src/utils/ast-utils.js +152 -1
  48. package/dist/src/utils/ast-utils.js.map +1 -1
  49. package/dist/src/utils/clack-utils.d.ts +49 -7
  50. package/dist/src/utils/clack-utils.js +238 -168
  51. package/dist/src/utils/clack-utils.js.map +1 -1
  52. package/dist/src/utils/package-manager.d.ts +5 -0
  53. package/dist/src/utils/package-manager.js +23 -14
  54. package/dist/src/utils/package-manager.js.map +1 -1
  55. package/dist/test/sourcemaps/tools/sentry-cli.test.d.ts +1 -0
  56. package/dist/test/sourcemaps/tools/sentry-cli.test.js +112 -0
  57. package/dist/test/sourcemaps/tools/sentry-cli.test.js.map +1 -0
  58. package/dist/test/sourcemaps/tools/tsc.test.d.ts +1 -0
  59. package/dist/test/sourcemaps/tools/tsc.test.js +121 -0
  60. package/dist/test/sourcemaps/tools/tsc.test.js.map +1 -0
  61. package/dist/test/utils/ast-utils.test.js +157 -26
  62. package/dist/test/utils/ast-utils.test.js.map +1 -1
  63. package/lib/Steps/ChooseIntegration.ts +1 -0
  64. package/package.json +2 -2
  65. package/src/android/android-wizard.ts +12 -10
  66. package/src/apple/apple-wizard.ts +2 -2
  67. package/src/nextjs/nextjs-wizard.ts +277 -198
  68. package/src/nextjs/templates.ts +3 -2
  69. package/src/nextjs/utils.ts +21 -0
  70. package/src/remix/remix-wizard.ts +15 -20
  71. package/src/remix/sdk-setup.ts +20 -5
  72. package/src/sourcemaps/sourcemaps-wizard.ts +2 -2
  73. package/src/sourcemaps/tools/sentry-cli.ts +16 -9
  74. package/src/sourcemaps/tools/tsc.ts +133 -28
  75. package/src/sourcemaps/tools/vite.ts +15 -39
  76. package/src/sourcemaps/tools/webpack.ts +16 -39
  77. package/src/sveltekit/sdk-setup.ts +109 -37
  78. package/src/sveltekit/sveltekit-wizard.ts +86 -21
  79. package/src/sveltekit/utils.ts +50 -0
  80. package/src/telemetry.ts +22 -11
  81. package/src/utils/ast-utils.ts +180 -0
  82. package/src/utils/clack-utils.ts +238 -149
  83. package/src/utils/package-manager.ts +24 -12
  84. package/test/sourcemaps/tools/sentry-cli.test.ts +51 -0
  85. package/test/sourcemaps/tools/tsc.test.ts +181 -0
  86. package/test/utils/ast-utils.test.ts +233 -32
@@ -5,6 +5,7 @@ import chalk from 'chalk';
5
5
  import * as childProcess from 'child_process';
6
6
  import * as fs from 'fs';
7
7
  import * as path from 'path';
8
+ import * as os from 'os';
8
9
  import { setInterval } from 'timers';
9
10
  import { URL } from 'url';
10
11
  import * as Sentry from '@sentry/node';
@@ -125,41 +126,95 @@ export function printWelcome(options: {
125
126
 
126
127
  let welcomeText =
127
128
  options.message ||
128
- 'This Wizard will help you set up Sentry for your application.\nThank you for using Sentry :)';
129
+ `The ${options.wizardName} will help you set up Sentry for your application.\nThank you for using Sentry :)`;
129
130
 
130
131
  if (options.promoCode) {
131
- welcomeText += `\n\nUsing promo-code: ${options.promoCode}`;
132
+ welcomeText = `${welcomeText}\n\nUsing promo-code: ${options.promoCode}`;
132
133
  }
133
134
 
134
135
  if (wizardPackage.version) {
135
- welcomeText += `\n\nVersion: ${wizardPackage.version}`;
136
+ welcomeText = `${welcomeText}\n\nVersion: ${wizardPackage.version}`;
136
137
  }
137
138
 
138
139
  if (options.telemetryEnabled) {
139
- welcomeText += `\n\nYou are using the Sentry Wizard with telemetry enabled. This helps us improve the Wizard.\nYou can disable it at any time by running \`sentry-wizard --disable-telemetry\`.`;
140
+ welcomeText = `${welcomeText}
141
+
142
+ This wizard sends telemetry data and crash reports to Sentry. This helps us improve the Wizard.
143
+ You can turn this off at any time by running ${chalk.cyanBright(
144
+ 'sentry-wizard --disable-telemetry',
145
+ )}.`;
140
146
  }
141
147
 
142
148
  clack.note(welcomeText);
143
149
  }
144
150
 
145
- export async function confirmContinueEvenThoughNoGitRepo(): Promise<void> {
151
+ export async function confirmContinueIfNoOrDirtyGitRepo(): Promise<void> {
152
+ return traceStep('check-git-status', async () => {
153
+ if (!isInGitRepo()) {
154
+ const continueWithoutGit = await abortIfCancelled(
155
+ clack.confirm({
156
+ message:
157
+ 'You are not inside a git repository. The wizard will create and update files. Do you want to continue anyway?',
158
+ }),
159
+ );
160
+
161
+ Sentry.setTag('continue-without-git', continueWithoutGit);
162
+
163
+ if (!continueWithoutGit) {
164
+ await abort(undefined, 0);
165
+ }
166
+ }
167
+
168
+ const uncommittedOrUntrackedFiles = getUncommittedOrUntrackedFiles();
169
+ if (uncommittedOrUntrackedFiles.length) {
170
+ clack.log.warn(
171
+ `You have uncommitted or untracked files in your repo:
172
+
173
+ ${uncommittedOrUntrackedFiles.join('\n')}
174
+
175
+ The wizard will create and update files.`,
176
+ );
177
+ const continueWithDirtyRepo = await abortIfCancelled(
178
+ clack.confirm({
179
+ message: 'Do you want to continue anyway?',
180
+ }),
181
+ );
182
+
183
+ Sentry.setTag('continue-with-dirty-repo', continueWithDirtyRepo);
184
+
185
+ if (!continueWithDirtyRepo) {
186
+ await abort(undefined, 0);
187
+ }
188
+ }
189
+ });
190
+ }
191
+
192
+ function isInGitRepo() {
146
193
  try {
147
194
  childProcess.execSync('git rev-parse --is-inside-work-tree', {
148
195
  stdio: 'ignore',
149
196
  });
197
+ return true;
150
198
  } catch {
151
- const continueWithoutGit = await abortIfCancelled(
152
- clack.confirm({
153
- message:
154
- 'You are not inside a git repository. The wizard will create and update files. Do you still want to continue?',
155
- }),
156
- );
199
+ return false;
200
+ }
201
+ }
157
202
 
158
- Sentry.setTag('continue-without-git', continueWithoutGit);
203
+ function getUncommittedOrUntrackedFiles(): string[] {
204
+ try {
205
+ const gitStatus = childProcess
206
+ .execSync('git status --porcelain=v1')
207
+ .toString();
159
208
 
160
- if (!continueWithoutGit) {
161
- await abort(undefined, 0);
162
- }
209
+ const files = gitStatus
210
+ .split(os.EOL)
211
+ .map((line) => line.trim())
212
+ .filter(Boolean)
213
+ .map((f) => `- ${f.split(/\s+/)[1]}`);
214
+
215
+ return files;
216
+ } catch {
217
+ return [];
163
218
  }
164
219
  }
165
220
 
@@ -202,161 +257,127 @@ export async function installPackage({
202
257
  alreadyInstalled: boolean;
203
258
  askBeforeUpdating?: boolean;
204
259
  }): Promise<void> {
205
- if (alreadyInstalled && askBeforeUpdating) {
206
- const shouldUpdatePackage = await abortIfCancelled(
207
- clack.confirm({
208
- message: `The ${chalk.bold.cyan(
209
- packageName,
210
- )} package is already installed. Do you want to update it to the latest version?`,
211
- }),
212
- );
260
+ return traceStep('install-package', async () => {
261
+ if (alreadyInstalled && askBeforeUpdating) {
262
+ const shouldUpdatePackage = await abortIfCancelled(
263
+ clack.confirm({
264
+ message: `The ${chalk.bold.cyan(
265
+ packageName,
266
+ )} package is already installed. Do you want to update it to the latest version?`,
267
+ }),
268
+ );
213
269
 
214
- if (!shouldUpdatePackage) {
215
- return;
270
+ if (!shouldUpdatePackage) {
271
+ return;
272
+ }
216
273
  }
217
- }
218
274
 
219
- const sdkInstallSpinner = clack.spinner();
275
+ const sdkInstallSpinner = clack.spinner();
220
276
 
221
- const packageManager = await getPackageManager();
222
-
223
- sdkInstallSpinner.start(
224
- `${alreadyInstalled ? 'Updating' : 'Installing'} ${chalk.bold.cyan(
225
- packageName,
226
- )} with ${chalk.bold(packageManager.label)}.`,
227
- );
277
+ const packageManager = await getPackageManager();
228
278
 
229
- try {
230
- await installPackageWithPackageManager(packageManager, packageName);
231
- } catch (e) {
232
- sdkInstallSpinner.stop('Installation failed.');
233
- clack.log.error(
234
- `${chalk.red(
235
- 'Encountered the following error during installation:',
236
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
237
- )}\n\n${e}\n\n${chalk.dim(
238
- 'If you think this issue is caused by the Sentry wizard, let us know here:\nhttps://github.com/getsentry/sentry-wizard/issues',
239
- )}`,
279
+ sdkInstallSpinner.start(
280
+ `${alreadyInstalled ? 'Updating' : 'Installing'} ${chalk.bold.cyan(
281
+ packageName,
282
+ )} with ${chalk.bold(packageManager.label)}.`,
240
283
  );
241
- await abort();
242
- }
243
-
244
- sdkInstallSpinner.stop(
245
- `${alreadyInstalled ? 'Updated' : 'Installed'} ${chalk.bold.cyan(
246
- packageName,
247
- )} with ${chalk.bold(packageManager.label)}.`,
248
- );
249
- }
250
284
 
251
- async function addOrgAndProjectToSentryCliRc(
252
- org: string,
253
- project: string,
254
- setupConfig: CliSetupConfig,
255
- ): Promise<void> {
256
- const configContents = fs.readFileSync(
257
- path.join(process.cwd(), setupConfig.filename),
258
- 'utf8',
259
- );
260
-
261
- if (setupConfig.likelyAlreadyHasOrgAndProject(configContents)) {
262
- clack.log.warn(
263
- `${chalk.bold(
264
- setupConfig.filename,
265
- )} already has org and project. Will not add them.`,
266
- );
267
- } else {
268
285
  try {
269
- await fs.promises.appendFile(
270
- path.join(process.cwd(), setupConfig.filename),
271
- `\n${setupConfig.orgAndProjContent(org, project)}\n`,
272
- );
286
+ await installPackageWithPackageManager(packageManager, packageName);
273
287
  } catch (e) {
274
- clack.log.warn(
275
- `${chalk.bold(
276
- setupConfig.filename,
277
- )} could not be updated with org and project.`,
288
+ sdkInstallSpinner.stop('Installation failed.');
289
+ clack.log.error(
290
+ `${chalk.red(
291
+ 'Encountered the following error during installation:',
292
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
293
+ )}\n\n${e}\n\n${chalk.dim(
294
+ 'If you think this issue is caused by the Sentry wizard, let us know here:\nhttps://github.com/getsentry/sentry-wizard/issues',
295
+ )}`,
278
296
  );
297
+ await abort();
279
298
  }
280
- }
299
+
300
+ sdkInstallSpinner.stop(
301
+ `${alreadyInstalled ? 'Updated' : 'Installed'} ${chalk.bold.cyan(
302
+ packageName,
303
+ )} with ${chalk.bold(packageManager.label)}.`,
304
+ );
305
+ });
281
306
  }
282
307
 
283
308
  export async function addSentryCliConfig(
284
309
  authToken: string,
285
310
  setupConfig: CliSetupConfig = sourceMapsCliSetupConfig,
286
- orgSlug?: string,
287
- projectSlug?: string,
288
311
  ): Promise<void> {
289
- const configExists = fs.existsSync(
290
- path.join(process.cwd(), setupConfig.filename),
291
- );
292
- if (configExists) {
293
- const configContents = fs.readFileSync(
312
+ return traceStep('add-sentry-cli-config', async () => {
313
+ const configExists = fs.existsSync(
294
314
  path.join(process.cwd(), setupConfig.filename),
295
- 'utf8',
296
315
  );
297
-
298
- if (setupConfig.likelyAlreadyHasAuthToken(configContents)) {
299
- clack.log.warn(
300
- `${chalk.bold(
301
- setupConfig.filename,
302
- )} already has auth token. Will not add one.`,
316
+ if (configExists) {
317
+ const configContents = fs.readFileSync(
318
+ path.join(process.cwd(), setupConfig.filename),
319
+ 'utf8',
303
320
  );
321
+
322
+ if (setupConfig.likelyAlreadyHasAuthToken(configContents)) {
323
+ clack.log.warn(
324
+ `${chalk.bold(
325
+ setupConfig.filename,
326
+ )} already has auth token. Will not add one.`,
327
+ );
328
+ } else {
329
+ try {
330
+ await fs.promises.writeFile(
331
+ path.join(process.cwd(), setupConfig.filename),
332
+ `${configContents}\n${setupConfig.tokenContent(authToken)}\n`,
333
+ { encoding: 'utf8', flag: 'w' },
334
+ );
335
+ clack.log.success(
336
+ chalk.greenBright(
337
+ `Added auth token to ${chalk.bold(
338
+ setupConfig.filename,
339
+ )} for you to test uploading ${setupConfig.name} locally.`,
340
+ ),
341
+ );
342
+ } catch {
343
+ clack.log.warning(
344
+ `Failed to add auth token to ${chalk.bold(
345
+ setupConfig.filename,
346
+ )}. Uploading ${
347
+ setupConfig.name
348
+ } during build will likely not work locally.`,
349
+ );
350
+ }
351
+ }
304
352
  } else {
305
353
  try {
306
354
  await fs.promises.writeFile(
307
355
  path.join(process.cwd(), setupConfig.filename),
308
- `${configContents}\n${setupConfig.tokenContent(authToken)}\n`,
356
+ `${setupConfig.tokenContent(authToken)}\n`,
309
357
  { encoding: 'utf8', flag: 'w' },
310
358
  );
311
359
  clack.log.success(
312
360
  chalk.greenBright(
313
- `Added auth token to ${chalk.bold(
361
+ `Created ${chalk.bold(
314
362
  setupConfig.filename,
315
- )} for you to test uploading ${setupConfig.name} locally.`,
363
+ )} with auth token for you to test uploading ${
364
+ setupConfig.name
365
+ } locally.`,
316
366
  ),
317
367
  );
318
368
  } catch {
319
369
  clack.log.warning(
320
- `Failed to add auth token to ${chalk.bold(
370
+ `Failed to create ${chalk.bold(
321
371
  setupConfig.filename,
322
- )}. Uploading ${
372
+ )} with auth token. Uploading ${
323
373
  setupConfig.name
324
374
  } during build will likely not work locally.`,
325
375
  );
326
376
  }
327
377
  }
328
- } else {
329
- try {
330
- await fs.promises.writeFile(
331
- path.join(process.cwd(), setupConfig.filename),
332
- `${setupConfig.tokenContent(authToken)}\n`,
333
- { encoding: 'utf8', flag: 'w' },
334
- );
335
- clack.log.success(
336
- chalk.greenBright(
337
- `Created ${chalk.bold(
338
- setupConfig.filename,
339
- )} with auth token for you to test uploading ${
340
- setupConfig.name
341
- } locally.`,
342
- ),
343
- );
344
- } catch {
345
- clack.log.warning(
346
- `Failed to create ${chalk.bold(
347
- setupConfig.filename,
348
- )} with auth token. Uploading ${
349
- setupConfig.name
350
- } during build will likely not work locally.`,
351
- );
352
- }
353
- }
354
378
 
355
- if (orgSlug && projectSlug) {
356
- await addOrgAndProjectToSentryCliRc(orgSlug, projectSlug, setupConfig);
357
- }
358
-
359
- await addAuthTokenFileToGitIgnore(setupConfig.filename);
379
+ await addAuthTokenFileToGitIgnore(setupConfig.filename);
380
+ });
360
381
  }
361
382
 
362
383
  export async function addDotEnvSentryBuildPluginFile(
@@ -451,23 +472,40 @@ async function addAuthTokenFileToGitIgnore(filename: string): Promise<void> {
451
472
  }
452
473
  }
453
474
 
475
+ /**
476
+ * Checks if @param packageId is listed as a dependency in @param packageJson.
477
+ * If not, it will ask users if they want to continue without the package.
478
+ *
479
+ * Use this function to check if e.g. a the framework of the SDK is installed
480
+ *
481
+ * @param packageJson the package.json object
482
+ * @param packageId the npm name of the package
483
+ * @param packageName a human readable name of the package
484
+ */
454
485
  export async function ensurePackageIsInstalled(
455
486
  packageJson: PackageDotJson,
456
487
  packageId: string,
457
488
  packageName: string,
458
- ) {
459
- if (!hasPackageInstalled(packageId, packageJson)) {
460
- const continueWithoutPackage = await abortIfCancelled(
461
- clack.confirm({
462
- message: `${packageName} does not seem to be installed. Do you still want to continue?`,
463
- initialValue: false,
464
- }),
465
- );
489
+ ): Promise<void> {
490
+ return traceStep('ensure-package-installed', async () => {
491
+ const installed = hasPackageInstalled(packageId, packageJson);
492
+
493
+ Sentry.setTag(`${packageName.toLowerCase()}-installed`, installed);
466
494
 
467
- if (!continueWithoutPackage) {
468
- await abort(undefined, 0);
495
+ if (!installed) {
496
+ Sentry.setTag(`${packageName.toLowerCase()}-installed`, false);
497
+ const continueWithoutPackage = await abortIfCancelled(
498
+ clack.confirm({
499
+ message: `${packageName} does not seem to be installed. Do you still want to continue?`,
500
+ initialValue: false,
501
+ }),
502
+ );
503
+
504
+ if (!continueWithoutPackage) {
505
+ await abort(undefined, 0);
506
+ }
469
507
  }
470
- }
508
+ });
471
509
  }
472
510
 
473
511
  export async function getPackageDotJson(): Promise<PackageDotJson> {
@@ -487,7 +525,9 @@ export async function getPackageDotJson(): Promise<PackageDotJson> {
487
525
  packageJson = JSON.parse(packageJsonFileContents);
488
526
  } catch {
489
527
  clack.log.error(
490
- 'Unable to parse your package.json. Make sure it has a valid format!',
528
+ `Unable to parse your ${chalk.cyan(
529
+ 'package.json',
530
+ )}. Make sure it has a valid format!`,
491
531
  );
492
532
 
493
533
  await abort();
@@ -826,7 +866,7 @@ export async function askForToolConfigPath(
826
866
  clack.confirm({
827
867
  message: `Do you have a ${toolName} config file (e.g. ${chalk.cyan(
828
868
  configFileName,
829
- )}?`,
869
+ )})?`,
830
870
  initialValue: true,
831
871
  }),
832
872
  );
@@ -866,27 +906,35 @@ export async function askForToolConfigPath(
866
906
  *
867
907
  * @param filename the name of the file to which the code snippet should be applied.
868
908
  * If a path is provided, only the filename will be used.
869
- * @param codeSnippet the snippet to be printed.
870
- * Make sure to follow the diff-like format of highlighting lines that require changes
871
- * and showing unchanged lines in gray.
872
909
  *
873
- * TODO: Link to wizard spec (develop) once it is live
910
+ * @param codeSnippet the snippet to be printed. Use {@link makeCodeSnippet} to create the
911
+ * diff-like format for visually highlighting unchanged or modified lines of code.
912
+ *
913
+ * @param hint (optional) a hint to be printed after the main instruction to add
914
+ * the code from @param codeSnippet to their @param filename.
915
+ *
916
+ * More guidelines on copy/paste instructions:
917
+ * @see {@link https://develop.sentry.dev/sdk/setup-wizards/#copy--paste-snippets}
918
+ *
874
919
  * TODO: refactor copy paste instructions across different wizards to use this function.
875
920
  * this might require adding a custom message parameter to the function
876
921
  */
877
922
  export async function showCopyPasteInstructions(
878
923
  filename: string,
879
924
  codeSnippet: string,
925
+ hint?: string,
880
926
  ): Promise<void> {
881
927
  clack.log.step(
882
928
  `Add the following code to your ${chalk.cyan(
883
929
  path.basename(filename),
884
- )} file:`,
930
+ )} file:${hint ? chalk.dim(` (${chalk.dim(hint)})`) : ''}`,
885
931
  );
886
932
 
933
+ // Padding the code snippet to be printed with a \n at the beginning and end
934
+ // This makes it easier to distinguish the snippet from the rest of the output
887
935
  // Intentionally logging directly to console here so that the code can be copied/pasted directly
888
936
  // eslint-disable-next-line no-console
889
- console.log(`\n${codeSnippet}`);
937
+ console.log(`\n${codeSnippet}\n`);
890
938
 
891
939
  await abortIfCancelled(
892
940
  clack.select({
@@ -897,6 +945,47 @@ export async function showCopyPasteInstructions(
897
945
  );
898
946
  }
899
947
 
948
+ /**
949
+ * Callback that exposes formatting helpers for a code snippet.
950
+ * @param unchanged - Formats text as old code.
951
+ * @param plus - Formats text as new code.
952
+ * @param minus - Formats text as removed code.
953
+ */
954
+ type CodeSnippetFormatter = (
955
+ unchanged: (txt: string) => string,
956
+ plus: (txt: string) => string,
957
+ minus: (txt: string) => string,
958
+ ) => string;
959
+
960
+ /**
961
+ * Crafts a code snippet that can be used to e.g.
962
+ * - print copy/paste instructions to the console
963
+ * - create a new config file.
964
+ *
965
+ * @param colors set this to true if you want the final snippet to be colored.
966
+ * This is useful for printing the snippet to the console as part of copy/paste instructions.
967
+ *
968
+ * @param callback the callback that returns the formatted code snippet.
969
+ * It exposes takes the helper functions for marking code as unchaned, new or removed.
970
+ * These functions no-op if no special formatting should be applied
971
+ * and otherwise apply the appropriate formatting/coloring.
972
+ * (@see {@link CodeSnippetFormatter})
973
+ *
974
+ * @see {@link showCopyPasteInstructions} for the helper with which to display the snippet in the console.
975
+ *
976
+ * @returns a string containing the final, formatted code snippet.
977
+ */
978
+ export function makeCodeSnippet(
979
+ colors: boolean,
980
+ callback: CodeSnippetFormatter,
981
+ ): string {
982
+ const unchanged = (txt: string) => (colors ? chalk.grey(txt) : txt);
983
+ const plus = (txt: string) => (colors ? chalk.greenBright(txt) : txt);
984
+ const minus = (txt: string) => (colors ? chalk.redBright(txt) : txt);
985
+
986
+ return callback(unchanged, plus, minus);
987
+ }
988
+
900
989
  /**
901
990
  * Creates a new config file with the given @param filepath and @param codeSnippet.
902
991
  *
@@ -4,53 +4,65 @@ import * as fs from 'fs';
4
4
  import * as path from 'path';
5
5
  import { promisify } from 'util';
6
6
 
7
+ import * as Sentry from '@sentry/node';
8
+ import { traceStep } from '../telemetry';
9
+
7
10
  export interface PackageManager {
8
11
  name: string;
9
12
  label: string;
10
13
  lockFile: string;
11
14
  installCommand: string;
12
15
  buildCommand: string;
16
+ /* The command that the package manager uses to run a script from package.json */
17
+ runScriptCommand: string;
13
18
  }
14
19
 
15
- const bun: PackageManager = {
20
+ export const BUN: PackageManager = {
16
21
  name: 'bun',
17
22
  label: 'Bun',
18
23
  lockFile: 'bun.lockb',
19
24
  installCommand: 'bun add',
20
- buildCommand: 'bun build',
25
+ buildCommand: 'bun run build',
26
+ runScriptCommand: 'bun run',
21
27
  };
22
- const yarn: PackageManager = {
28
+ export const YARN: PackageManager = {
23
29
  name: 'yarn',
24
30
  label: 'Yarn',
25
31
  lockFile: 'yarn.lock',
26
32
  installCommand: 'yarn add',
27
33
  buildCommand: 'yarn build',
34
+ runScriptCommand: 'yarn',
28
35
  };
29
- const pnpm: PackageManager = {
36
+ export const PNPM: PackageManager = {
30
37
  name: 'pnpm',
31
38
  label: 'PNPM',
32
39
  lockFile: 'pnpm-lock.yaml',
33
40
  installCommand: 'pnpm add',
34
41
  buildCommand: 'pnpm build',
42
+ runScriptCommand: 'pnpm',
35
43
  };
36
- const npm: PackageManager = {
44
+ export const NPM: PackageManager = {
37
45
  name: 'npm',
38
46
  label: 'NPM',
39
47
  lockFile: 'package-lock.json',
40
48
  installCommand: 'npm add',
41
49
  buildCommand: 'npm run build',
50
+ runScriptCommand: 'npm run',
42
51
  };
43
52
 
44
- export const packageManagers = [bun, yarn, pnpm, npm];
53
+ export const packageManagers = [BUN, YARN, PNPM, NPM];
45
54
 
46
55
  export function detectPackageManger(): PackageManager | null {
47
- for (const packageManager of packageManagers) {
48
- if (fs.existsSync(path.join(process.cwd(), packageManager.lockFile))) {
49
- return packageManager;
56
+ return traceStep('detect-package-manager', () => {
57
+ for (const packageManager of packageManagers) {
58
+ if (fs.existsSync(path.join(process.cwd(), packageManager.lockFile))) {
59
+ Sentry.setTag('package-manager', packageManager.name);
60
+ return packageManager;
61
+ }
50
62
  }
51
- }
52
- // We make the default NPM - it's weird if we don't find any lock file
53
- return null;
63
+ Sentry.setTag('package-manager', 'not-detected');
64
+ return null;
65
+ });
54
66
  }
55
67
 
56
68
  export async function installPackageWithPackageManager(
@@ -0,0 +1,51 @@
1
+ import * as fs from 'fs';
2
+
3
+ import { addSentryCommandToBuildCommand } from '../../../src/sourcemaps/tools/sentry-cli';
4
+
5
+ import * as packageManagerHelpers from '../../../src/utils/package-manager';
6
+
7
+ const writeFileSpy = jest
8
+ .spyOn(fs.promises, 'writeFile')
9
+ .mockImplementation(() => Promise.resolve());
10
+
11
+ jest.mock('@clack/prompts', () => {
12
+ return {
13
+ log: {
14
+ info: jest.fn(),
15
+ success: jest.fn(),
16
+ },
17
+ confirm: jest.fn().mockResolvedValue(true),
18
+ isCancel: jest.fn().mockReturnValue(false),
19
+ };
20
+ });
21
+
22
+ describe('addSentryCommandToBuildCommand', () => {
23
+ afterEach(() => {
24
+ jest.clearAllMocks();
25
+ });
26
+ it.each([
27
+ [
28
+ packageManagerHelpers.NPM,
29
+ packageManagerHelpers.PNPM,
30
+ packageManagerHelpers.YARN,
31
+ packageManagerHelpers.BUN,
32
+ ],
33
+ ])('adds the cli command to the script command (%s)', async (_, pacMan) => {
34
+ jest
35
+ .spyOn(packageManagerHelpers, 'detectPackageManger')
36
+ .mockReturnValue(pacMan);
37
+ const packageJson = {
38
+ scripts: {
39
+ build: 'tsc',
40
+ },
41
+ version: '1.0.0',
42
+ };
43
+ await addSentryCommandToBuildCommand(packageJson);
44
+ expect(writeFileSpy).toHaveBeenCalledWith(
45
+ expect.stringContaining('package.json'),
46
+ expect.stringContaining(
47
+ `tsc && ${pacMan.runScriptCommand} sentry:sourcemaps`,
48
+ ),
49
+ );
50
+ });
51
+ });