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