UNPKG

@sentry/wizard

Version:

Sentry wizard helping you to configure your project

385 lines (328 loc) 10.8 kB
/* eslint-disable max-lines */ // @ts-ignore - clack is ESM and TS complains about that. It works though import clack from '@clack/prompts'; import chalk from 'chalk'; import * as fs from 'fs'; // @ts-ignore - magicast is ESM and TS complains about that. It works though import { builders, generateCode, parseModule } from 'magicast'; import * as path from 'path'; import { abort, abortIfCancelled, addSentryCliRc, askForProjectSelection, askForSelfHosted, askForWizardLogin, confirmContinueEvenThoughNoGitRepo, ensurePackageIsInstalled, getPackageDotJson, installPackage, printWelcome, } from '../utils/clack-utils'; import { getNextjsConfigCjsAppendix, getNextjsConfigCjsTemplate, getNextjsConfigEsmCopyPasteSnippet, getNextjsSentryBuildOptionsTemplate, getNextjsWebpackPluginOptionsTemplate, getSentryConfigContents, getSentryExampleApiRoute, getSentryExamplePageContents, } from './templates'; interface NextjsWizardOptions { promoCode?: string; } // eslint-disable-next-line complexity export async function runNextjsWizard( options: NextjsWizardOptions, ): Promise<void> { printWelcome({ wizardName: 'Sentry Next.js Wizard', promoCode: options.promoCode, }); await confirmContinueEvenThoughNoGitRepo(); const packageJson = await getPackageDotJson(); await ensurePackageIsInstalled(packageJson, 'next', 'Next.js'); const { url: sentryUrl, selfHosted } = await askForSelfHosted(); const { projects, apiKeys } = await askForWizardLogin({ promoCode: options.promoCode, url: sentryUrl, platform: 'javascript-nextjs', }); const selectedProject = await askForProjectSelection(projects); await installPackage({ packageName: '@sentry/nextjs', alreadyInstalled: !!packageJson?.dependencies?.['@sentry/nextjs'], }); let isUsingTypescript = false; try { isUsingTypescript = fs.existsSync( path.join(process.cwd(), 'tsconfig.json'), ); } catch { // noop - Default to assuming user is not using typescript } const configVariants = ['server', 'client', 'edge'] as const; for (const configVariant of configVariants) { const jsConfig = `sentry.${configVariant}.config.js`; const tsConfig = `sentry.${configVariant}.config.ts`; const jsConfigExists = fs.existsSync(path.join(process.cwd(), jsConfig)); const tsConfigExists = fs.existsSync(path.join(process.cwd(), tsConfig)); let shouldWriteFile = true; if (jsConfigExists || tsConfigExists) { const existingConfigs = []; if (jsConfigExists) { existingConfigs.push(jsConfig); } if (tsConfigExists) { existingConfigs.push(tsConfig); } const overwriteExistingConfigs = await abortIfCancelled( clack.confirm({ message: `Found existing Sentry ${configVariant} config (${existingConfigs.join( ', ', )}). Overwrite ${existingConfigs.length > 1 ? 'them' : 'it'}?`, }), ); shouldWriteFile = overwriteExistingConfigs; if (overwriteExistingConfigs) { if (jsConfigExists) { fs.unlinkSync(path.join(process.cwd(), jsConfig)); clack.log.warn(`Removed existing ${chalk.bold(jsConfig)}.`); } if (tsConfigExists) { fs.unlinkSync(path.join(process.cwd(), tsConfig)); clack.log.warn(`Removed existing ${chalk.bold(tsConfig)}.`); } } } if (shouldWriteFile) { await fs.promises.writeFile( path.join(process.cwd(), isUsingTypescript ? tsConfig : jsConfig), getSentryConfigContents( selectedProject.keys[0].dsn.public, configVariant, ), { encoding: 'utf8', flag: 'w' }, ); clack.log.success( `Created fresh ${chalk.bold(isUsingTypescript ? tsConfig : jsConfig)}.`, ); } } const sentryWebpackOptionsTemplate = getNextjsWebpackPluginOptionsTemplate( selectedProject.organization.slug, selectedProject.slug, ); const sentryBuildOptionsTemplate = getNextjsSentryBuildOptionsTemplate(); const nextConfigJs = 'next.config.js'; const nextConfigMjs = 'next.config.mjs'; const nextConfigJsExists = fs.existsSync( path.join(process.cwd(), nextConfigJs), ); const nextConfigMjsExists = fs.existsSync( path.join(process.cwd(), nextConfigMjs), ); if (!nextConfigJsExists && !nextConfigMjsExists) { await fs.promises.writeFile( path.join(process.cwd(), nextConfigJs), getNextjsConfigCjsTemplate( sentryWebpackOptionsTemplate, sentryBuildOptionsTemplate, ), { encoding: 'utf8', flag: 'w' }, ); clack.log.success( `Created ${chalk.bold('next.config.js')} with Sentry configuration.`, ); } if (nextConfigJsExists) { const nextConfgiJsContent = fs.readFileSync( path.join(process.cwd(), nextConfigJs), 'utf8', ); const probablyIncludesSdk = nextConfgiJsContent.includes('@sentry/nextjs') && nextConfgiJsContent.includes('withSentryConfig'); let shouldInject = true; if (probablyIncludesSdk) { const injectAnyhow = await abortIfCancelled( clack.confirm({ message: `${chalk.bold( nextConfigJs, )} already contains Sentry SDK configuration. Should the wizard modify it anyways?`, }), ); shouldInject = injectAnyhow; } if (shouldInject) { await fs.promises.appendFile( path.join(process.cwd(), nextConfigJs), getNextjsConfigCjsAppendix( sentryWebpackOptionsTemplate, sentryBuildOptionsTemplate, ), 'utf8', ); clack.log.success( `Added Sentry configuration to ${chalk.bold(nextConfigJs)}. ${chalk.dim( '(you probably want to clean this up a bit!)', )}`, ); } } if (nextConfigMjsExists) { const nextConfgiMjsContent = fs.readFileSync( path.join(process.cwd(), nextConfigMjs), 'utf8', ); const probablyIncludesSdk = nextConfgiMjsContent.includes('@sentry/nextjs') && nextConfgiMjsContent.includes('withSentryConfig'); let shouldInject = true; if (probablyIncludesSdk) { const injectAnyhow = await abortIfCancelled( clack.confirm({ message: `${chalk.bold( nextConfigMjs, )} already contains Sentry SDK configuration. Should the wizard modify it anyways?`, }), ); shouldInject = injectAnyhow; } try { if (shouldInject) { const mod = parseModule(nextConfgiMjsContent); mod.imports.$add({ from: '@sentry/nextjs', imported: 'withSentryConfig', local: 'withSentryConfig', }); // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access const expressionToWrap = generateCode(mod.exports.default.$ast).code; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment mod.exports.default = builders.raw(`withSentryConfig( ${expressionToWrap}, ${sentryWebpackOptionsTemplate}, ${sentryBuildOptionsTemplate} )`); const newCode = mod.generate().code; await fs.promises.writeFile( path.join(process.cwd(), nextConfigMjs), newCode, { encoding: 'utf8', flag: 'w', }, ); clack.log.success( `Added Sentry configuration to ${chalk.bold( nextConfigMjs, )}. ${chalk.dim('(you probably want to clean this up a bit!)')}`, ); } } catch { clack.log.warn( chalk.yellow( `Something went wrong writing to ${chalk.bold(nextConfigMjs)}`, ), ); clack.log.info( `Please put the following code snippet into ${chalk.bold( nextConfigMjs, )}: ${chalk.dim('You probably have to clean it up a bit.')}\n`, ); // eslint-disable-next-line no-console console.log( getNextjsConfigEsmCopyPasteSnippet( sentryWebpackOptionsTemplate, sentryBuildOptionsTemplate, ), ); const shouldContinue = await abortIfCancelled( clack.confirm({ message: `Are you done putting the snippet above into ${chalk.bold( nextConfigMjs, )}?`, active: 'Yes', inactive: 'No, get me out of here', }), ); if (!shouldContinue) { await abort(); } } } const maybePagesDirPath = path.join(process.cwd(), 'pages'); const maybeSrcPagesDirPath = path.join(process.cwd(), 'src', 'pages'); let pagesLocation = fs.existsSync(maybePagesDirPath) && fs.lstatSync(maybePagesDirPath).isDirectory() ? ['pages'] : fs.existsSync(maybeSrcPagesDirPath) && fs.lstatSync(maybeSrcPagesDirPath).isDirectory() ? ['src', 'pages'] : undefined; if (!pagesLocation) { pagesLocation = ['pages']; fs.mkdirSync(path.join(process.cwd(), ...pagesLocation), { recursive: true, }); } if (pagesLocation) { const examplePageContents = getSentryExamplePageContents({ selfHosted, orgSlug: selectedProject.organization.slug, projectId: selectedProject.id, url: sentryUrl, }); await fs.promises.writeFile( path.join(process.cwd(), ...pagesLocation, 'sentry-example-page.js'), examplePageContents, { encoding: 'utf8', flag: 'w' }, ); clack.log.success( `Created ${chalk.bold( path.join(...pagesLocation, 'sentry-example-page.js'), )}.`, ); fs.mkdirSync(path.join(process.cwd(), ...pagesLocation, 'api'), { recursive: true, }); await fs.promises.writeFile( path.join( process.cwd(), ...pagesLocation, 'api', 'sentry-example-api.js', ), getSentryExampleApiRoute(), { encoding: 'utf8', flag: 'w' }, ); clack.log.success( `Created ${chalk.bold( path.join(...pagesLocation, 'api', 'sentry-example-api.js'), )}.`, ); } await addSentryCliRc(apiKeys.token); const mightBeUsingVercel = fs.existsSync( path.join(process.cwd(), 'vercel.json'), ); clack.outro( `${chalk.green('Everything is set up!')} ${chalk.cyan( 'You can validate your setup by starting your dev environment (`next dev`) and visiting "/sentry-example-page".', )} ${ mightBeUsingVercel ? ` ▲ It seems like you're using Vercel. We recommend using the Sentry Vercel integration: https://vercel.com/integrations/sentry ` : '' } ${chalk.dim( 'If you encounter any issues, let us know here: https://github.com/getsentry/sentry-javascript/issues', )}`, ); }