UNPKG

@sentry/wizard

Version:

Sentry wizard helping you to configure your project

213 lines (184 loc) 6.12 kB
import * as fs from 'fs'; // @ts-ignore - clack is ESM and TS complains about that. It works though import * as clack from '@clack/prompts'; // @ts-ignore - magicast is ESM and TS complains about that. It works though import { ProxifiedModule } from 'magicast'; import chalk from 'chalk'; import * as Sentry from '@sentry/node'; import { getLastRequireIndex, hasSentryContent } from '../utils/ast-utils'; import { makeCodeSnippet, showCopyPasteInstructions, } from '../utils/clack-utils'; import { metroConfigPath, parseMetroConfig, writeMetroConfig } from './metro'; import * as recast from 'recast'; import x = recast.types; import t = x.namedTypes; const b = recast.types.builders; export async function addSentryToExpoMetroConfig() { if (!fs.existsSync(metroConfigPath)) { const success = await createSentryExpoMetroConfig(); if (!success) { Sentry.setTag('expo-metro-config', 'create-new-error'); return await showInstructions(); } Sentry.setTag('expo-metro-config', 'created-new'); return undefined; } const mod = await parseMetroConfig(); let didPatch = false; try { didPatch = patchMetroInMemory(mod); } catch (e) { // noop } if (!didPatch) { Sentry.setTag('expo-metro-config', 'patch-error'); clack.log.error( `Could not patch ${chalk.cyan( metroConfigPath, )} with Sentry configuration.`, ); return await showInstructions(); } const saved = await writeMetroConfig(mod); if (saved) { Sentry.setTag('expo-metro-config', 'patch-saved'); clack.log.success( chalk.green(`${chalk.cyan(metroConfigPath)} changes saved.`), ); } else { Sentry.setTag('expo-metro-config', 'patch-save-error'); clack.log.warn( `Could not save changes to ${chalk.cyan( metroConfigPath, )}, please follow the manual steps.`, ); return await showInstructions(); } } export function patchMetroInMemory(mod: ProxifiedModule): boolean { const ast = mod.$ast as t.Program; if (hasSentryContent(ast)) { clack.log.warn( `The ${chalk.cyan( metroConfigPath, )} file already has Sentry configuration.`, ); return false; } let didReplaceDefaultConfigCall = false; recast.visit(ast, { visitVariableDeclaration(path) { const { node } = path; if ( // path is require("expo/metro-config") // and only getDefaultConfig is being destructured // then remove the entire declaration node.declarations.length > 0 && node.declarations[0].type === 'VariableDeclarator' && node.declarations[0].init && node.declarations[0].init.type === 'CallExpression' && node.declarations[0].init.callee && node.declarations[0].init.callee.type === 'Identifier' && node.declarations[0].init.callee.name === 'require' && node.declarations[0].init.arguments[0].type === 'StringLiteral' && node.declarations[0].init.arguments[0].value === 'expo/metro-config' && node.declarations[0].id.type === 'ObjectPattern' && node.declarations[0].id.properties.length === 1 && node.declarations[0].id.properties[0].type === 'ObjectProperty' && node.declarations[0].id.properties[0].key.type === 'Identifier' && node.declarations[0].id.properties[0].key.name === 'getDefaultConfig' ) { path.prune(); return false; } this.traverse(path); }, visitCallExpression(path) { const { node } = path; if ( // path is getDefaultConfig // then rename it to getSentryExpoConfig node.callee.type === 'Identifier' && node.callee.name === 'getDefaultConfig' ) { node.callee.name = 'getSentryExpoConfig'; didReplaceDefaultConfigCall = true; return false; } this.traverse(path); }, }); if (!didReplaceDefaultConfigCall) { clack.log.warn( `Could not find \`getDefaultConfig\` in ${chalk.cyan(metroConfigPath)}.`, ); return false; } addSentryExpoConfigRequire(ast); return true; } export function addSentryExpoConfigRequire(program: t.Program) { const lastRequireIndex = getLastRequireIndex(program); const sentryExpoConfigRequire = createSentryExpoConfigRequire(); program.body.splice(lastRequireIndex + 1, 0, sentryExpoConfigRequire); } /** * Creates const { getSentryExpoConfig } = require("@sentry/react-native/metro"); */ function createSentryExpoConfigRequire() { return b.variableDeclaration('const', [ b.variableDeclarator( b.objectPattern([ b.objectProperty.from({ key: b.identifier('getSentryExpoConfig'), value: b.identifier('getSentryExpoConfig'), shorthand: true, }), ]), b.callExpression(b.identifier('require'), [ b.literal('@sentry/react-native/metro'), ]), ), ]); } async function createSentryExpoMetroConfig(): Promise<boolean> { const snippet = `const { getSentryExpoConfig } = require("@sentry/react-native/metro"); const config = getSentryExpoConfig(__dirname); module.exports = config; `; try { await fs.promises.writeFile(metroConfigPath, snippet); } catch (e) { clack.log.error( `Could not create ${chalk.cyan( metroConfigPath, )} with Sentry configuration.`, ); return false; } clack.log.success( `Created ${chalk.cyan(metroConfigPath)} with Sentry configuration.`, ); return true; } function showInstructions() { return showCopyPasteInstructions( metroConfigPath, getMetroWithSentryExpoConfigSnippet(true), ); } function getMetroWithSentryExpoConfigSnippet(colors: boolean): string { return makeCodeSnippet(colors, (unchanged, plus, minus) => unchanged(`${minus( `// const { getDefaultConfig } = require("expo/metro-config");`, )} ${plus( `const { getSentryExpoConfig } = require("@sentry/react-native/metro");`, )} ${minus(`// const config = getDefaultConfig(__dirname);`)} ${plus(`const config = getSentryExpoConfig(__dirname);`)} module.exports = config;`), ); }