From 6ac7776b48db1c7e546008dd671de02998c543f2 Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Fri, 29 Nov 2024 11:10:49 +0100 Subject: [PATCH] feat(nuxt): Add downgrade path to nitro 2.9.7 (#725) * feat(nuxt): Add downgrade path to nitro 2.9.7 Also adds automatic resolutions for @vercel/nft and ofetch. See: https://github.com/getsentry/sentry-javascript/issues/14514 * Update changelog * Rewrite logic to just add overrides and not install deps * Make clack utility for package overrides generic * Pin nitropack version to ~2.9.7 --- CHANGELOG.md | 1 + e2e-tests/tests/nuxt-3.test.ts | 29 +++++++++++++-- e2e-tests/tests/nuxt-4.test.ts | 27 ++++++++++++-- e2e-tests/utils/index.ts | 5 ++- src/nuxt/nuxt-wizard.ts | 15 ++++++-- src/nuxt/sdk-setup.ts | 39 ++++++++++++++++++++ src/utils/clack-utils.ts | 47 +++++++++++++++++++++--- src/utils/package-json.ts | 5 +++ src/utils/package-manager.ts | 66 ++++++++++++++++++++++++++++++++++ 9 files changed, 221 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f366b805..b62bb779 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - Remove Profiling for Android ([#720](https://github.com/getsentry/sentry-wizard/pull/720)) +- Add downgrade path to nitro 2.9.7 ([#725](https://github.com/getsentry/sentry-wizard/pull/725)) ## 3.35.0 diff --git a/e2e-tests/tests/nuxt-3.test.ts b/e2e-tests/tests/nuxt-3.test.ts index 47091875..d79ee318 100644 --- a/e2e-tests/tests/nuxt-3.test.ts +++ b/e2e-tests/tests/nuxt-3.test.ts @@ -44,13 +44,36 @@ async function runWizardOnNuxtProject(projectDir: string): Promise { 'Please select your package manager.', ); - const tracingOptionPrompted = + const nitropackOverridePrompted = packageManagerPrompted && (await wizardInstance.sendStdinAndWaitForOutput( // Selecting `yarn` as the package manager [KEYS.DOWN, KEYS.ENTER], + // Do you want to install version 2.9.7 of nitropack and add an override to package.json? + 'Do you want to add an override for nitropack version ~2.9.7?', + { + timeout: 240_000, + }, + )); + + const nftOverridePrompted = + nitropackOverridePrompted && + (await wizardInstance.sendStdinAndWaitForOutput( + // Selecting `yes` to downgrade nitropack + KEYS.ENTER, + 'Do you want to add an override for @vercel/nft version ^0.27.4?', + // 'Do you want to install version', + { + timeout: 240_000, + }, + )); + + const tracingOptionPrompted = + nftOverridePrompted && + (await wizardInstance.sendStdinAndWaitForOutput( + KEYS.ENTER, // "Do you want to enable Tracing", sometimes doesn't work as `Tracing` can be printed in bold. - 'to track the performance of your application?', + 'Do you want to enable', { timeout: 240_000, }, @@ -59,7 +82,7 @@ async function runWizardOnNuxtProject(projectDir: string): Promise { const replayOptionPrompted = tracingOptionPrompted && (await wizardInstance.sendStdinAndWaitForOutput( - [KEYS.ENTER], + KEYS.ENTER, // "Do you want to enable Sentry Session Replay", sometimes doesn't work as `Sentry Session Replay` can be printed in bold. 'to get a video-like reproduction of errors during a user session?', )); diff --git a/e2e-tests/tests/nuxt-4.test.ts b/e2e-tests/tests/nuxt-4.test.ts index 3c1e560b..b255d8cf 100644 --- a/e2e-tests/tests/nuxt-4.test.ts +++ b/e2e-tests/tests/nuxt-4.test.ts @@ -43,13 +43,36 @@ async function runWizardOnNuxtProject(projectDir: string): Promise { 'Please select your package manager.', ); - const tracingOptionPrompted = + const nitropackOverridePrompted = packageManagerPrompted && (await wizardInstance.sendStdinAndWaitForOutput( // Selecting `yarn` as the package manager [KEYS.DOWN, KEYS.ENTER], + // Do you want to install version 2.9.7 of nitropack and add an override to package.json? + 'Do you want to add an override for nitropack version ~2.9.7?', + { + timeout: 240_000, + }, + )); + + const nftOverridePrompted = + nitropackOverridePrompted && + (await wizardInstance.sendStdinAndWaitForOutput( + // Selecting `yes` to downgrade nitropack + KEYS.ENTER, + 'Do you want to add an override for @vercel/nft version ^0.27.4?', + // 'Do you want to install version', + { + timeout: 240_000, + }, + )); + + const tracingOptionPrompted = + nftOverridePrompted && + (await wizardInstance.sendStdinAndWaitForOutput( + KEYS.ENTER, // "Do you want to enable Tracing", sometimes doesn't work as `Tracing` can be printed in bold. - 'to track the performance of your application?', + 'Do you want to enable', { timeout: 240_000, }, diff --git a/e2e-tests/utils/index.ts b/e2e-tests/utils/index.ts index 2327d25d..fda8155e 100644 --- a/e2e-tests/utils/index.ts +++ b/e2e-tests/utils/index.ts @@ -243,7 +243,10 @@ export function createFile(filePath: string, content?: string) { * @param oldContent * @param newContent */ -export function modifyFile(filePath: string, replaceMap: Record) { +export function modifyFile( + filePath: string, + replaceMap: Record, +) { const fileContent = fs.readFileSync(filePath, 'utf-8'); let newFileContent = fileContent; diff --git a/src/nuxt/nuxt-wizard.ts b/src/nuxt/nuxt-wizard.ts index e4262747..8823535d 100644 --- a/src/nuxt/nuxt-wizard.ts +++ b/src/nuxt/nuxt-wizard.ts @@ -1,6 +1,7 @@ // @ts-ignore - clack is ESM and TS complains about that. It works though import * as clack from '@clack/prompts'; import * as Sentry from '@sentry/node'; +import chalk from 'chalk'; import { lt, minVersion } from 'semver'; import type { WizardOptions } from '../utils/types'; import { traceStep, withTelemetry } from '../telemetry'; @@ -14,19 +15,24 @@ import { ensurePackageIsInstalled, getOrAskForProjectData, getPackageDotJson, + getPackageManager, installPackage, printWelcome, runPrettierIfInstalled, } from '../utils/clack-utils'; import { getPackageVersion, hasPackageInstalled } from '../utils/package-json'; -import { addSDKModule, getNuxtConfig, createConfigFiles } from './sdk-setup'; +import { + addSDKModule, + getNuxtConfig, + createConfigFiles, + addNuxtOverrides, +} from './sdk-setup'; import { createExampleComponent, createExamplePage, supportsExamplePage, } from './sdk-example'; import { isNuxtV4 } from './utils'; -import chalk from 'chalk'; export function runNuxtWizard(options: WizardOptions) { return withTelemetry( @@ -85,12 +91,17 @@ export async function runNuxtWizardWithTelemetry( const { authToken, selectedProject, selfHosted, sentryUrl } = await getOrAskForProjectData(options, 'javascript-nuxt'); + const packageManager = await getPackageManager(); + + await addNuxtOverrides(packageManager, minVer); + const sdkAlreadyInstalled = hasPackageInstalled('@sentry/nuxt', packageJson); Sentry.setTag('sdk-already-installed', sdkAlreadyInstalled); await installPackage({ packageName: '@sentry/nuxt', alreadyInstalled: sdkAlreadyInstalled, + packageManager, }); await addDotEnvSentryBuildPluginFile(authToken); diff --git a/src/nuxt/sdk-setup.ts b/src/nuxt/sdk-setup.ts index 39cab4c9..c8df09d9 100644 --- a/src/nuxt/sdk-setup.ts +++ b/src/nuxt/sdk-setup.ts @@ -17,10 +17,13 @@ import { import { abort, abortIfCancelled, + askShouldAddPackageOverride, featureSelectionPrompt, isUsingTypeScript, } from '../utils/clack-utils'; import { traceStep } from '../telemetry'; +import { lt, SemVer } from 'semver'; +import { PackageManager } from '../utils/package-manager'; const possibleNuxtConfig = [ 'nuxt.config.js', @@ -207,3 +210,39 @@ export async function createConfigFiles(dsn: string) { }); } } + +export async function addNuxtOverrides( + packageManager: PackageManager, + nuxtMinVer: SemVer | null, +) { + const overrides = [ + { + pkgName: 'nitropack', + pkgVersion: '~2.9.7', + }, + { + pkgName: '@vercel/nft', + pkgVersion: '^0.27.4', + }, + ...(nuxtMinVer && lt(nuxtMinVer, '3.14.0') + ? [{ pkgName: 'ofetch', pkgVersion: '^1.4.0' }] + : []), + ]; + + clack.log.warn( + `To ensure Sentry can properly instrument your code it needs to add version overrides for some Nuxt dependencies.\n\nFor more info see: ${chalk.cyan( + 'https://github.com/getsentry/sentry-javascript/issues/14514', + )}`, + ); + + for (const { pkgName, pkgVersion } of overrides) { + const shouldAddOverride = await askShouldAddPackageOverride( + pkgName, + pkgVersion, + ); + + if (shouldAddOverride) { + await packageManager.addOverride(pkgName, pkgVersion); + } + } +} diff --git a/src/utils/clack-utils.ts b/src/utils/clack-utils.ts index 5a398536..94b5cf5b 100644 --- a/src/utils/clack-utils.ts +++ b/src/utils/clack-utils.ts @@ -355,6 +355,7 @@ export async function installPackage({ alreadyInstalled, askBeforeUpdating = true, packageNameDisplayLabel, + packageManager, }: { /** The string that is passed to the package manager CLI as identifier to install (e.g. `@sentry/nextjs`, or `@sentry/nextjs@^8`) */ packageName: string; @@ -362,6 +363,7 @@ export async function installPackage({ askBeforeUpdating?: boolean; /** 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`) */ packageNameDisplayLabel?: string; + packageManager?: PackageManager; }): Promise<{ packageManager?: PackageManager }> { return traceStep('install-package', async () => { if (alreadyInstalled && askBeforeUpdating) { @@ -380,18 +382,18 @@ export async function installPackage({ const sdkInstallSpinner = clack.spinner(); - const packageManager = await getPackageManager(); + const pkgManager = packageManager || (await getPackageManager()); sdkInstallSpinner.start( `${alreadyInstalled ? 'Updating' : 'Installing'} ${chalk.bold.cyan( packageNameDisplayLabel ?? packageName, - )} with ${chalk.bold(packageManager.label)}.`, + )} with ${chalk.bold(pkgManager.label)}.`, ); try { await new Promise((resolve, reject) => { childProcess.exec( - `${packageManager.installCommand} ${packageName} ${packageManager.flags}`, + `${pkgManager.installCommand} ${packageName} ${pkgManager.flags}`, (err, stdout, stderr) => { if (err) { // Write a log file so we can better troubleshoot issues @@ -430,10 +432,10 @@ export async function installPackage({ sdkInstallSpinner.stop( `${alreadyInstalled ? 'Updated' : 'Installed'} ${chalk.bold.cyan( packageNameDisplayLabel ?? packageName, - )} with ${chalk.bold(packageManager.label)}.`, + )} with ${chalk.bold(pkgManager.label)}.`, ); - return { packageManager }; + return { packageManager: pkgManager }; }); } @@ -808,6 +810,26 @@ export async function getPackageDotJson(): Promise { return packageJson || {}; } +export async function updatePackageDotJson( + packageDotJson: PackageDotJson, +): Promise { + try { + await fs.promises.writeFile( + path.join(process.cwd(), 'package.json'), + // TODO: maybe figure out the original indentation + JSON.stringify(packageDotJson, null, 2), + { + encoding: 'utf8', + flag: 'w', + }, + ); + } catch { + clack.log.error(`Unable to update your ${chalk.cyan('package.json')}.`); + + await abort(); + } +} + export async function getPackageManager(): Promise { const detectedPackageManager = detectPackageManger(); @@ -1469,3 +1491,18 @@ export async function featureSelectionPrompt>( return selectedFeatures as { [key in F[number]['id']]: boolean }; }); } + +export async function askShouldAddPackageOverride( + pkgName: string, + pkgVersion: string, +): Promise { + return traceStep(`ask-add-package-override`, () => + abortIfCancelled( + clack.confirm({ + message: `Do you want to add an override for ${chalk.cyan( + pkgName, + )} version ${chalk.cyan(pkgVersion)}?`, + }), + ), + ); +} diff --git a/src/utils/package-json.ts b/src/utils/package-json.ts index be8f646e..9320c484 100644 --- a/src/utils/package-json.ts +++ b/src/utils/package-json.ts @@ -3,6 +3,11 @@ export type PackageDotJson = { scripts?: Record; dependencies?: Record; devDependencies?: Record; + resolutions?: Record; + overrides?: Record; + pnpm?: { + overrides?: Record; + }; }; type NpmPackage = { diff --git a/src/utils/package-manager.ts b/src/utils/package-manager.ts index 3c7aaeae..fc7efcc9 100644 --- a/src/utils/package-manager.ts +++ b/src/utils/package-manager.ts @@ -4,6 +4,7 @@ import * as path from 'path'; import * as Sentry from '@sentry/node'; import { traceStep } from '../telemetry'; +import { getPackageDotJson, updatePackageDotJson } from './clack-utils'; export interface PackageManager { name: string; @@ -15,6 +16,7 @@ export interface PackageManager { runScriptCommand: string; flags: string; detect: () => boolean; + addOverride: (pkgName: string, pkgVersion: string) => Promise; } export const BUN: PackageManager = { @@ -26,6 +28,18 @@ export const BUN: PackageManager = { runScriptCommand: 'bun run', flags: '', detect: () => fs.existsSync(path.join(process.cwd(), BUN.lockFile)), + addOverride: async (pkgName, pkgVersion): Promise => { + const packageDotJson = await getPackageDotJson(); + const overrides = packageDotJson.overrides || {}; + + await updatePackageDotJson({ + ...packageDotJson, + overrides: { + ...overrides, + [pkgName]: pkgVersion, + }, + }); + }, }; export const YARN_V1: PackageManager = { name: 'yarn', @@ -45,6 +59,18 @@ export const YARN_V1: PackageManager = { return false; } }, + addOverride: async (pkgName, pkgVersion): Promise => { + const packageDotJson = await getPackageDotJson(); + const resolutions = packageDotJson.resolutions || {}; + + await updatePackageDotJson({ + ...packageDotJson, + resolutions: { + ...resolutions, + [pkgName]: pkgVersion, + }, + }); + }, }; /** YARN V2/3/4 */ export const YARN_V2: PackageManager = { @@ -65,6 +91,18 @@ export const YARN_V2: PackageManager = { return false; } }, + addOverride: async (pkgName, pkgVersion): Promise => { + const packageDotJson = await getPackageDotJson(); + const resolutions = packageDotJson.resolutions || {}; + + await updatePackageDotJson({ + ...packageDotJson, + resolutions: { + ...resolutions, + [pkgName]: pkgVersion, + }, + }); + }, }; export const PNPM: PackageManager = { name: 'pnpm', @@ -75,6 +113,22 @@ export const PNPM: PackageManager = { runScriptCommand: 'pnpm', flags: '--ignore-workspace-root-check', detect: () => fs.existsSync(path.join(process.cwd(), PNPM.lockFile)), + addOverride: async (pkgName, pkgVersion): Promise => { + const packageDotJson = await getPackageDotJson(); + const pnpm = packageDotJson.pnpm || {}; + const overrides = pnpm.overrides || {}; + + await updatePackageDotJson({ + ...packageDotJson, + pnpm: { + ...pnpm, + overrides: { + ...overrides, + [pkgName]: pkgVersion, + }, + }, + }); + }, }; export const NPM: PackageManager = { name: 'npm', @@ -85,6 +139,18 @@ export const NPM: PackageManager = { runScriptCommand: 'npm run', flags: '', detect: () => fs.existsSync(path.join(process.cwd(), NPM.lockFile)), + addOverride: async (pkgName, pkgVersion): Promise => { + const packageDotJson = await getPackageDotJson(); + const overrides = packageDotJson.overrides || {}; + + await updatePackageDotJson({ + ...packageDotJson, + overrides: { + ...overrides, + [pkgName]: pkgVersion, + }, + }); + }, }; export const packageManagers = [BUN, YARN_V1, YARN_V2, PNPM, NPM];