Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(react-native): Add support for Expo managed projects #505

Merged
merged 35 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
1d4c17f
feat(react-native): Add support for Expo managed projects
krystofwoldrich Nov 29, 2023
684e84e
Fix metro config for expo default
krystofwoldrich Nov 29, 2023
aa3dd60
Add tests for mergeConfig
krystofwoldrich Nov 29, 2023
5ce9c8a
Add expo app config patch
krystofwoldrich Nov 30, 2023
fe9b715
fix lint and add tests
krystofwoldrich Dec 1, 2023
452508a
Add sdk expo min version check, patch missing metro config in expo pr…
krystofwoldrich Dec 4, 2023
7fee158
Fix lint
krystofwoldrich Dec 4, 2023
7101fc4
Fix create app.config.json if none existing, allow metro on unsupport…
krystofwoldrich Dec 4, 2023
d8e999a
Merge remote-tracking branch 'origin/master' into kw-add-rn-expo
krystofwoldrich Dec 4, 2023
f438db7
fix clack import
krystofwoldrich Dec 4, 2023
8ec303f
Fix tests
krystofwoldrich Dec 4, 2023
b3a3aa4
Update src/react-native/metro.ts
krystofwoldrich Dec 5, 2023
81bdb2a
Fix review comments
krystofwoldrich Dec 5, 2023
35c330b
Merge commit 'b3a3aa4604272837831d0324cb7e7b6a3989a186' into kw-add-r…
krystofwoldrich Dec 5, 2023
13dd0fc
Merge branch 'master' into kw-add-rn-expo
krystofwoldrich Jun 21, 2024
3da2893
fix bad merge build
krystofwoldrich Jun 21, 2024
14cbb95
simplify expo app.json, fix expo metro,
krystofwoldrich Jun 24, 2024
6f7df17
Merge remote-tracking branch 'origin/master' into kw-add-rn-expo
krystofwoldrich Jul 4, 2024
0e44f60
Merge remote-tracking branch 'origin/master' into kw-add-rn-expo
krystofwoldrich Jul 5, 2024
480cc6f
remove sentry token env note
krystofwoldrich Jul 5, 2024
7b2849b
make metro config file easier to copy
krystofwoldrich Jul 5, 2024
ef96e28
add lookup for app/_layout.tsx file
krystofwoldrich Jul 5, 2024
a4ee93c
add empty new line to changed app json
krystofwoldrich Jul 5, 2024
a5c86f3
do not add auth token note
krystofwoldrich Jul 5, 2024
10d07b1
add test and patch xcode expo build phase
krystofwoldrich Jul 5, 2024
d82cee8
use git ignore logs from clack
krystofwoldrich Jul 5, 2024
850f77a
fix expo metro patch
krystofwoldrich Jul 5, 2024
748b514
fix lint
krystofwoldrich Jul 5, 2024
5fdae6b
add more tests
krystofwoldrich Jul 5, 2024
23edcb4
fix lint
krystofwoldrich Jul 5, 2024
4eea17d
fix changelog
krystofwoldrich Jul 8, 2024
b94a0e5
clean up expo ts
krystofwoldrich Jul 8, 2024
69a7020
remove unused package json main
krystofwoldrich Jul 8, 2024
616b9f8
add telemetry
krystofwoldrich Jul 8, 2024
d4513bf
fix review comment
krystofwoldrich Jul 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased

- feat(react-native): Add support for Expo projects (#505)

## 3.24.1

- fix(nextjs): Add trailing comma to `sentryUrl` option in `withSentryConfig` template (#601)
Expand Down
53 changes: 53 additions & 0 deletions src/react-native/expo-env-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// @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 ${EXPO_ENV_LOCAL_FILE} to .gitignore, please add it to not commit your auth key.`,
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
);
}

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;
}
}
212 changes: 212 additions & 0 deletions src/react-native/expo-metro.ts
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;`),
);
}
Loading
Loading