diff --git a/package.json b/package.json index 84b47c82..beb9539a 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@clack/prompts": "0.7.0", "@sentry/cli": "^1.77.3", "@sentry/node": "^7.119.2", + "@types/resolve": "^1.20.6", "axios": "1.7.4", "chalk": "^2.4.1", "glob": "^8.1.0", @@ -37,6 +38,7 @@ "r2": "^2.0.1", "read-env": "^1.3.0", "recast": "^0.23.3", + "resolve": "^1.22.8", "semver": "^7.5.3", "xcode": "3.0.1", "xml-js": "^1.6.11", diff --git a/src/nuxt/nuxt-wizard.ts b/src/nuxt/nuxt-wizard.ts index e4262747..709a8adf 100644 --- a/src/nuxt/nuxt-wizard.ts +++ b/src/nuxt/nuxt-wizard.ts @@ -1,7 +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 { lt, minVersion } from 'semver'; +import { gte, lt, lte, minVersion } from 'semver'; import type { WizardOptions } from '../utils/types'; import { traceStep, withTelemetry } from '../telemetry'; import { @@ -19,7 +19,12 @@ import { runPrettierIfInstalled, } from '../utils/clack-utils'; import { getPackageVersion, hasPackageInstalled } from '../utils/package-json'; -import { addSDKModule, getNuxtConfig, createConfigFiles } from './sdk-setup'; +import { + addSDKModule, + getNuxtConfig, + createConfigFiles, + installExtraDepsIfNeeded, +} from './sdk-setup'; import { createExampleComponent, createExamplePage, @@ -93,6 +98,8 @@ export async function runNuxtWizardWithTelemetry( alreadyInstalled: sdkAlreadyInstalled, }); + await installExtraDepsIfNeeded(minVer); + await addDotEnvSentryBuildPluginFile(authToken); const nuxtConfig = await traceStep('load-nuxt-config', getNuxtConfig); diff --git a/src/nuxt/sdk-setup.ts b/src/nuxt/sdk-setup.ts index 39cab4c9..073df15b 100644 --- a/src/nuxt/sdk-setup.ts +++ b/src/nuxt/sdk-setup.ts @@ -17,10 +17,15 @@ import { import { abort, abortIfCancelled, + askShouldInstallPackage, featureSelectionPrompt, + installPackage, isUsingTypeScript, } from '../utils/clack-utils'; import { traceStep } from '../telemetry'; +import { getInstalledPackageVersion } from '../utils/package-version'; +import { gte, lt, minVersion, SemVer } from 'semver'; +import { detectPackageManger, PackageManager } from '../utils/package-manager'; const possibleNuxtConfig = [ 'nuxt.config.js', @@ -207,3 +212,53 @@ export async function createConfigFiles(dsn: string) { }); } } + +export async function installExtraDepsIfNeeded(nuxtMinVer: SemVer | null) { + // We currently have some restrictions on the dependencies nuxt ships with + // and try to resolve these here for users if they agree. + // See: https://github.com/getsentry/sentry-javascript/issues/14514 + await installExtraDepIfNeeded('nitropack', '2.9.7', (minVer) => + gte(minVer, '2.10.0'), + ); + + await installExtraDepIfNeeded('@vercel/nft', '^0.27.4', (minVer) => + lt(minVer, '0.27.4'), + ); + + if (nuxtMinVer && lt(nuxtMinVer, '3.14.0')) { + await installExtraDepIfNeeded('ofetch', '^1.4.0', (minVer) => + lt(minVer, '1.4.0'), + ); + } +} + +export async function installExtraDepIfNeeded( + pkgName: string, + pkgVersion: string, + isNeeded: (minVer: SemVer) => boolean, +) { + const installedVersion = + (await getInstalledPackageVersion(pkgName)) || '0.0.0'; + const minVer = minVersion(installedVersion); + + if (!minVer || isNeeded(minVer)) { + clack.log.warn( + `You have version ${chalk.cyan(installedVersion)} of ${chalk.cyan( + pkgName, + )} installed.\nCurrently, we do not properly support this version (see https://github.com/getsentry/sentry-javascript/issues/14514).`, + ); + + const shouldInstall = await askShouldInstallPackage(pkgName, pkgVersion); + + if (shouldInstall) { + const { packageManager } = await installPackage({ + packageName: `${pkgName}@${pkgVersion}`, + alreadyInstalled: false, + askBeforeUpdating: false, + packageNameDisplayLabel: `${pkgName}@${pkgVersion}`, + }); + + await packageManager?.addOverride(pkgName, pkgVersion); + } + } +} diff --git a/src/utils/clack-utils.ts b/src/utils/clack-utils.ts index 5a398536..9f9f672d 100644 --- a/src/utils/clack-utils.ts +++ b/src/utils/clack-utils.ts @@ -808,6 +808,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 +1489,20 @@ export async function featureSelectionPrompt>( return selectedFeatures as { [key in F[number]['id']]: boolean }; }); } + +export async function askShouldInstallPackage( + pkgName: string, + pkgVersion: string, +): Promise { + return traceStep(`ask-install-package-${pkgName}`, () => + abortIfCancelled( + clack.confirm({ + message: `Do you want to install version ${chalk.cyan( + pkgVersion, + )} of ${chalk.cyan(pkgName)} and add an override to ${chalk.cyan( + 'package.json', + )}?`, + }), + ), + ); +} 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]; diff --git a/src/utils/package-version.ts b/src/utils/package-version.ts new file mode 100644 index 00000000..0419bea9 --- /dev/null +++ b/src/utils/package-version.ts @@ -0,0 +1,46 @@ +import resolve from 'resolve'; +import * as path from 'path'; +import * as fs from 'fs'; +// @ts-expect-error - clack is ESM and TS complains about that. It works though +import clack from '@clack/prompts'; +import chalk from 'chalk'; +import * as Sentry from '@sentry/node'; +import { abort } from './clack-utils'; +import { PNPM } from './package-manager'; + +/** + * Unlike `getPackageVersion`, this helper uses the `resolve` + * npm package to resolve the actually installed version of + * a package and is not limited to direct dependencies. + */ +export async function getInstalledPackageVersion(pkg: string) { + const isPnpm = PNPM.detect(); + try { + const pkgJson: { version: string } = JSON.parse( + fs + .readFileSync( + resolve.sync(`${pkg}/package.json`, { + basedir: isPnpm + ? path.join(process.cwd(), 'node_modules', '.pnpm') + : process.cwd(), + preserveSymlinks: isPnpm, + }), + ) + .toString(), + ); + return pkgJson.version; + } catch (e: unknown) { + clack.log.error(`Error while evaluating version of package ${pkg}.`); + clack.log.info( + chalk.dim( + typeof e === 'object' && e != null && 'toString' in e + ? e.toString() + : typeof e === 'string' + ? e + : 'Unknown error', + ), + ); + Sentry.captureException('Error while setting up the Nuxt SDK'); + await abort('Exiting Wizard'); + } +} diff --git a/yarn.lock b/yarn.lock index 839c4c4d..44b2f6cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1166,6 +1166,11 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.2.tgz#6c2324641cc4ba050a8c710b2b251b377581fbf0" integrity sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg== +"@types/resolve@^1.20.6": + version "1.20.6" + resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.6.tgz#e6e60dad29c2c8c206c026e6dd8d6d1bdda850b8" + integrity sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ== + "@types/rimraf@^3.0.2": version "3.0.2" resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-3.0.2.tgz#a63d175b331748e5220ad48c901d7bbf1f44eef8" @@ -2512,6 +2517,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -2684,6 +2694,13 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + hosted-git-info@^2.1.4: version "2.8.9" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" @@ -2819,6 +2836,13 @@ is-core-module@^2.11.0: dependencies: has "^1.0.3" +is-core-module@^2.13.0: + version "2.15.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" + integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== + dependencies: + hasown "^2.0.2" + is-core-module@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" @@ -4085,6 +4109,15 @@ resolve@^1.20.0: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.22.8: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + restore-cursor@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"