-
-
Notifications
You must be signed in to change notification settings - Fork 49
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(react-native): Add support for Expo projects (#505)
- Loading branch information
1 parent
583060f
commit fe849fe
Showing
13 changed files
with
896 additions
and
89 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
// @ts-ignore - clack is ESM and TS complains about that. It works though | ||
import * as clack from '@clack/prompts'; | ||
import chalk from 'chalk'; | ||
import fs from 'fs'; | ||
import * as Sentry from '@sentry/node'; | ||
import { RNCliSetupConfigContent } from './react-native-wizard'; | ||
import { addToGitignore } from './git'; | ||
|
||
const EXPO_ENV_LOCAL_FILE = '.env.local'; | ||
|
||
export async function addExpoEnvLocal( | ||
options: RNCliSetupConfigContent, | ||
): Promise<boolean> { | ||
const newContent = `#DO NOT COMMIT THIS\nSENTRY_AUTH_TOKEN=${options.authToken}\n`; | ||
|
||
const added = await addToGitignore(EXPO_ENV_LOCAL_FILE); | ||
if (added) { | ||
clack.log.success( | ||
`Added ${chalk.cyan(EXPO_ENV_LOCAL_FILE)} to .gitignore.`, | ||
); | ||
} else { | ||
clack.log.error( | ||
`Could not add ${chalk.cyan( | ||
EXPO_ENV_LOCAL_FILE, | ||
)} to .gitignore, please add it to not commit your auth key.`, | ||
); | ||
} | ||
|
||
if (!fs.existsSync(EXPO_ENV_LOCAL_FILE)) { | ||
try { | ||
await fs.promises.writeFile(EXPO_ENV_LOCAL_FILE, newContent); | ||
Sentry.setTag('expo-env-local', 'written'); | ||
clack.log.success(`Written ${chalk.cyan(EXPO_ENV_LOCAL_FILE)}.`); | ||
return true; | ||
} catch (error) { | ||
Sentry.setTag('expo-env-local', 'write-error'); | ||
clack.log.error(`Unable to write ${chalk.cyan(EXPO_ENV_LOCAL_FILE)}.`); | ||
return false; | ||
} | ||
} | ||
|
||
Sentry.setTag('expo-env-local', 'exists'); | ||
clack.log.info(`Updating existing ${chalk.cyan(EXPO_ENV_LOCAL_FILE)}.`); | ||
|
||
try { | ||
await fs.promises.appendFile(EXPO_ENV_LOCAL_FILE, newContent); | ||
Sentry.setTag('expo-env-local', 'updated'); | ||
clack.log.success(`Updated ${chalk.cyan(EXPO_ENV_LOCAL_FILE)}.`); | ||
return true; | ||
} catch (error) { | ||
Sentry.setTag('expo-env-local', 'update-error'); | ||
clack.log.error(`Unable to update ${chalk.cyan(EXPO_ENV_LOCAL_FILE)}.`); | ||
return false; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
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;`), | ||
); | ||
} |
Oops, something went wrong.