@sentry/wizard
Version:
Sentry wizard helping you to configure your project
385 lines (328 loc) • 10.8 kB
text/typescript
/* 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',
)}`,
);
}