Skip to content

add Expo config plugin #3391

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

Merged
merged 5 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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: 1 addition & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
"editor.formatOnType": true,
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.rulers": [
120
],
"editor.rulers": [120],
"editor.tabSize": 2,
"files.autoSave": "onWindowChange",
"files.trimTrailingWhitespace": true,
Expand Down
1 change: 1 addition & 0 deletions app.plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./plugin/build');
20 changes: 17 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@
},
"main": "dist/js/index.js",
"scripts": {
"build": "yarn build:sdk && yarn downlevel && yarn build:tools",
"build": "yarn build:sdk && yarn downlevel && yarn build:tools && yarn build:plugin",
"build:sdk": "tsc -p tsconfig.build.json",
"build:sdk:watch": "tsc -p tsconfig.build.json -w --preserveWatchOutput",
"build:tools": "tsc -p tsconfig.build.tools.json",
"build:plugin": "expo-module build plugin",
"downlevel": "downlevel-dts dist ts3.8/dist --to=3.8",
"clean": "rimraf dist coverage",
"clean:plugin": "expo-module clean plugin",
"prepare": "expo-module prepare",
"prepublishOnly": "expo-module prepublishOnly",
"test": "yarn test:sdk && yarn test:tools",
"test:sdk": "jest",
"test:tools": "jest --config jest.config.tools.js",
Expand All @@ -33,10 +37,12 @@
"lint": "yarn lint:eslint && yarn lint:prettier",
"lint:eslint": "eslint --config .eslintrc.js .",
"lint:prettier": "prettier --check \"{src,test,scripts}/**/**.ts\"",
"lint:plugin": "expo-module lint plugin",
"test:watch": "jest --watch",
"run-ios": "cd sample-new-architecture && yarn react-native run-ios",
"run-android": "cd sample-new-architecture && yarn react-native run-android",
"yalc:add:sentry-javascript": "yalc add @sentry/browser @sentry/core @sentry/hub @sentry/integrations @sentry/react @sentry/types @sentry/utils"
"yalc:add:sentry-javascript": "yalc add @sentry/browser @sentry/core @sentry/hub @sentry/integrations @sentry/react @sentry/types @sentry/utils",
"expo-module": "expo-module"
},
"keywords": [
"react-native",
Expand All @@ -55,7 +61,8 @@
],
"peerDependencies": {
"react": ">=17.0.0",
"react-native": ">=0.65.0"
"react-native": ">=0.65.0",
"expo": ">=47.0.0"
},
"dependencies": {
"@sentry/browser": "7.80.0",
Expand All @@ -81,6 +88,8 @@
"eslint": "^7.6.0",
"eslint-plugin-react": "^7.20.6",
"eslint-plugin-react-native": "^3.8.1",
"expo": "^47.0.0",
"expo-module-scripts": "^3.1.0",
"jest": "^29.6.2",
"jest-environment-jsdom": "^29.6.2",
"metro": "0.76",
Expand Down Expand Up @@ -109,5 +118,10 @@
"android": {
"javaPackageName": "io.sentry.react"
}
},
"peerDependenciesMeta": {
"expo": {
"optional": true
}
}
}
61 changes: 61 additions & 0 deletions plugin/src/__tests__/modifyAppBuildGradle-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { WarningAggregator } from 'expo/config-plugins';

import { modifyAppBuildGradle } from '../withSentryAndroid';

jest.mock('@expo/config-plugins', () => {
const plugins = jest.requireActual('expo/config-plugins');
return {
...plugins,
WarningAggregator: { addWarningAndroid: jest.fn() },
};
});

const buildGradleWithSentry = `
apply from: new File(["node", "--print", "require.resolve('@sentry/react-native/package.json')"].execute().text.trim(), "../sentry.gradle")

android {
}
`;

const buildGradleWithOutSentry = `
android {
}
`;

const monoRepoBuildGradleWithSentry = `
apply from: new File(["node", "--print", "require.resolve('@sentry/react-native/package.json')"].execute().text.trim(), "../sentry.gradle")

android {
}
`;

const monoRepoBuildGradleWithOutSentry = `
android {
}
`;

const buildGradleWithOutReactGradleScript = `
`;

describe('Configures Android native project correctly', () => {
it(`Non monorepo: Doesn't modify app/build.gradle if Sentry's already configured`, () => {
expect(modifyAppBuildGradle(buildGradleWithSentry)).toMatch(buildGradleWithSentry);
});

it(`Non monorepo: Adds sentry.gradle script if not present already`, () => {
expect(modifyAppBuildGradle(buildGradleWithOutSentry)).toMatch(buildGradleWithSentry);
});

it(`Monorepo: Doesn't modify app/build.gradle if Sentry's already configured`, () => {
expect(modifyAppBuildGradle(monoRepoBuildGradleWithSentry)).toMatch(monoRepoBuildGradleWithSentry);
});

it(`Monorepo: Adds sentry.gradle script if not present already`, () => {
expect(modifyAppBuildGradle(monoRepoBuildGradleWithOutSentry)).toMatch(monoRepoBuildGradleWithSentry);
});

it(`Warns to file a bug report if no react.gradle is found`, () => {
modifyAppBuildGradle(buildGradleWithOutReactGradleScript);
expect(WarningAggregator.addWarningAndroid).toHaveBeenCalled();
});
});
76 changes: 76 additions & 0 deletions plugin/src/__tests__/modifyXcodeProject-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { WarningAggregator } from 'expo/config-plugins';

import { modifyExistingXcodeBuildScript } from '../withSentryIOS';

jest.mock('@expo/config-plugins', () => {
const plugins = jest.requireActual('expo/config-plugins');
return {
...plugins,
WarningAggregator: { addWarningIOS: jest.fn(), addWarningAndroid: jest.fn() },
};
});

const buildScriptWithoutSentry = {
shellScript: '"export NODE_BINARY=node\\n../node_modules/react-native/scripts/react-native-xcode.sh"',
};

const buildScriptWithSentry = {
shellScript:
'"export SENTRY_PROPERTIES=sentry.properties\\nexport EXTRA_PACKAGER_ARGS=\\"--sourcemap-output $DERIVED_FILE_DIR/main.jsbundle.map\\"\\nexport NODE_BINARY=node\\n`node --print \\"require.resolve(\'@sentry/cli/package.json\').slice(0, -13) + \'/bin/sentry-cli\'\\"` react-native xcode --force-foreground ../node_modules/react-native/scripts/react-native-xcode.sh\\n\\n`node --print \\"require.resolve(\'@sentry/react-native/package.json\').slice(0, -13) + \'/scripts/collect-modules.sh\'\\"`"',
};

const monorepoBuildScriptWithoutSentry = {
shellScript:
'"export NODE_BINARY=node\\n`node --print \\"require.resolve(\'react-native/package.json\').slice(0, -13) + \'/scripts/react-native-xcode.sh\'\\"`"',
};

const monorepoBuildScriptWithSentry = {
shellScript:
"\"export SENTRY_PROPERTIES=sentry.properties\\nexport EXTRA_PACKAGER_ARGS=\\\"--sourcemap-output $DERIVED_FILE_DIR/main.jsbundle.map\\\"\\nexport NODE_BINARY=node\\n`node --print \\\"require.resolve('@sentry/cli/package.json').slice(0, -13) + '/bin/sentry-cli'\\\"` react-native xcode --force-foreground `node --print \\\"require.resolve('react-native/package.json').slice(0, -13) + '/scripts/react-native-xcode.sh'\\\"`\\n\\n`node --print \\\"require.resolve('@sentry/react-native/package.json').slice(0, -13) + '/scripts/collect-modules.sh'\\\"`\"",
};

const buildScriptWeDontExpect = {
shellScript: `
`,
};

describe('Configures iOS native project correctly', () => {
let consoleWarnMock;

beforeEach(() => {
consoleWarnMock = jest.spyOn(console, 'warn').mockImplementation();
});

afterEach(() => {
consoleWarnMock.mockRestore();
});

it(`Doesn't modify build script if Sentry's already configured`, () => {
const script = Object.assign({}, buildScriptWithSentry);
modifyExistingXcodeBuildScript(script);
expect(script).toStrictEqual(buildScriptWithSentry);
});

it(`Add Sentry configuration to 'Bundle React Native Code' build script`, () => {
const script = Object.assign({}, buildScriptWithoutSentry);
modifyExistingXcodeBuildScript(script);
expect(script).toStrictEqual(buildScriptWithSentry);
});

it(`Monorepo: doesn't modify build script if Sentry's already configured`, () => {
const script = Object.assign({}, monorepoBuildScriptWithSentry);
modifyExistingXcodeBuildScript(script);
expect(script).toStrictEqual(monorepoBuildScriptWithSentry);
});

it(`Monorepo: add Sentry configuration to 'Bundle React Native Code' build script`, () => {
const script = Object.assign({}, monorepoBuildScriptWithoutSentry);
modifyExistingXcodeBuildScript(script);
expect(script).toStrictEqual(monorepoBuildScriptWithSentry);
});

it(`Warns to file a bug report if build script isn't what we expect to find`, () => {
modifyExistingXcodeBuildScript(buildScriptWeDontExpect);
expect(WarningAggregator.addWarningIOS).toHaveBeenCalled();
});
});
65 changes: 65 additions & 0 deletions plugin/src/withSentry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { ConfigPlugin, createRunOncePlugin, WarningAggregator } from 'expo/config-plugins';

import { withSentryAndroid } from './withSentryAndroid';
import { withSentryIOS } from './withSentryIOS';

const pkg = require('../../package.json');

interface PluginProps {
organization?: string;
project?: string;
authToken?: string;
url?: string;
}

const withSentry: ConfigPlugin<PluginProps | void> = (config, props) => {
const sentryProperties = getSentryProperties(props);
if (sentryProperties !== null) {
try {
config = withSentryAndroid(config, sentryProperties);
} catch (e) {
WarningAggregator.addWarningAndroid(
'sentry-expo',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like we left some sentry-expo references in here, should update to this lib name

'There was a problem configuring sentry-expo in your native Android project: ' + e,
);
}
try {
config = withSentryIOS(config, sentryProperties);
} catch (e) {
WarningAggregator.addWarningIOS(
'sentry-expo',
'There was a problem configuring sentry-expo in your native iOS project: ' + e,
);
}
}
return config;
};

const missingAuthTokenMessage = `# auth.token is configured through SENTRY_AUTH_TOKEN environment variable`;
const missingProjectMessage = `# no project found, falling back to SENTRY_PROJECT environment variable`;
const missingOrgMessage = `# no org found, falling back to SENTRY_ORG environment variable`;

export function getSentryProperties(props: PluginProps | void): string | null {
const { organization, project, authToken, url = 'https://sentry.io/' } = props ?? {};
const missingProperties = ['organization', 'project'].filter(each => !props?.hasOwnProperty(each));

if (missingProperties.length) {
const warningMessage = `Missing Sentry configuration properties: ${missingProperties.join(
', ',
)} in config plugin. Builds will fall back to environment variables. Refer to @sentry/react-native docs for how to configure this.`;
WarningAggregator.addWarningAndroid('sentry-expo', warningMessage);
WarningAggregator.addWarningIOS('sentry-expo', warningMessage);
}

return `defaults.url=${url}
${organization ? `defaults.org=${organization}` : missingOrgMessage}
${project ? `defaults.project=${project}` : missingProjectMessage}
${
authToken
? `# Configure this value through \`SENTRY_AUTH_TOKEN\` environment variable instead. See:https://docs.expo.dev/guides/using-sentry/#app-configuration\nauth.token=${authToken}`
: missingAuthTokenMessage
}
`;
}

export default createRunOncePlugin(withSentry, pkg.name, pkg.version);
65 changes: 65 additions & 0 deletions plugin/src/withSentryAndroid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {
ConfigPlugin,
WarningAggregator,
withAppBuildGradle,
withDangerousMod,
} from 'expo/config-plugins';
import * as path from 'path';

import { writeSentryPropertiesTo } from './withSentryIOS';

export const withSentryAndroid: ConfigPlugin<string> = (config, sentryProperties: string) => {
config = withAppBuildGradle(config, (config) => {
if (config.modResults.language === 'groovy') {
config.modResults.contents = modifyAppBuildGradle(config.modResults.contents);
} else {
throw new Error(
'Cannot configure Sentry in the app gradle because the build.gradle is not groovy'
);
}
return config;
});
return withDangerousMod(config, [
'android',
(config) => {
writeSentryPropertiesTo(
path.resolve(config.modRequest.projectRoot, 'android'),
sentryProperties
);
return config;
},
]);
};

const resolveSentryReactNativePackageJsonPath = `["node", "--print", "require.resolve('@sentry/react-native/package.json')"].execute().text.trim()`;

/**
* Writes to projectDirectory/android/app/build.gradle,
* adding the relevant @sentry/react-native script.
*/
export function modifyAppBuildGradle(buildGradle: string) {
if (buildGradle.includes('/sentry.gradle"')) {
return buildGradle;
}

// Use the same location that sentry-wizard uses
// See: https://github.com/getsentry/sentry-wizard/blob/e9b4522f27a852069c862bd458bdf9b07cab6e33/lib/Steps/Integrations/ReactNative.ts#L232
const pattern = /^android {/m;

if (!buildGradle.match(pattern)) {
WarningAggregator.addWarningAndroid(
'sentry-expo',
'Could not find react.gradle script in android/app/build.gradle. Please open a bug report at https://github.com/expo/sentry-expo.'
);
}

const sentryOptions = !buildGradle.includes('project.ext.sentryCli')
? `project.ext.sentryCli=[collectModulesScript: new File(${resolveSentryReactNativePackageJsonPath}, "../dist/js/tools/collectModules.js")]`
: '';
const applyFrom = `apply from: new File(${resolveSentryReactNativePackageJsonPath}, "../sentry.gradle")`;

return buildGradle.replace(
pattern,
match => sentryOptions + '\n\n' + applyFrom + '\n\n' + match
);
}
Loading