diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b46c844..17404593 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,19 @@ # Changelog +## v48.0.0 (2023-03-07) + +This release adds support for expo 48 + +### Fixed + +- (bugsnag-expo-cli) CLI tool now installs a sourcemap plugin version that matches the Expo SDK version [#111](https://github.com/bugsnag/bugsnag-expo/pull/111) +- (plugin-expo-eas-sourcemaps) Use EAS Build lifecycle hook for Android source map uploads [#112](https://github.com/bugsnag/bugsnag-expo/pull/112) + ## v47.1.1 (2023-03-02) -(plugin-expo-eas-sourcemaps) Restrict Bugsnag Android Gradle Plugin dependency to v7 [#104](https://github.com/bugsnag/bugsnag-expo/pull/104) +### Fixed + +- (plugin-expo-eas-sourcemaps) Restrict Bugsnag Android Gradle Plugin dependency to v7 [#104](https://github.com/bugsnag/bugsnag-expo/pull/104) ## v47.1.0 (2023-01-09) diff --git a/features/fixtures/test-app/package.json b/features/fixtures/test-app/package.json index 10d71c3c..671d4090 100644 --- a/features/fixtures/test-app/package.json +++ b/features/fixtures/test-app/package.json @@ -9,23 +9,23 @@ "web": "expo start --web" }, "dependencies": { - "@react-native-community/netinfo": "9.3.5", - "expo": "~47.0.3", - "expo-application": "~5.0.1", - "expo-constants": "~14.0.2", - "expo-crypto": "~12.0.0", - "expo-device": "~5.0.0", - "expo-file-system": "~15.1.1", - "expo-screen-orientation": "~5.0.1", - "expo-status-bar": "~1.4.2", - "react": "18.1.0", - "react-native": "0.70.5" + "@react-native-community/netinfo": "9.3.7", + "expo": "~48.0.4", + "expo-application": "~5.1.1", + "expo-constants": "~14.2.1", + "expo-crypto": "~12.2.1", + "expo-device": "~5.2.1", + "expo-file-system": "~15.2.2", + "expo-screen-orientation": "~5.1.1", + "expo-status-bar": "~1.4.4", + "react": "18.2.0", + "react-native": "0.71.3" }, "resolutions": { - "expo-modules-core": "1.0.4" + "expo-modules-core": "1.2.3" }, "devDependencies": { - "@babel/core": "^7.19.3" + "@babel/core": "^7.20.0" }, "private": true } diff --git a/features/fixtures/test-app/run-bugsnag-expo-cli-install b/features/fixtures/test-app/run-bugsnag-expo-cli-install index 9948135c..e05e0a3a 100755 --- a/features/fixtures/test-app/run-bugsnag-expo-cli-install +++ b/features/fixtures/test-app/run-bugsnag-expo-cli-install @@ -5,7 +5,7 @@ set timeout -1 # add-hook spawn npx bugsnag-expo-cli upload-sourcemaps -expect "Do you want to automatically upload source maps to Bugsnag? (this will modify your app.json)" +expect "Do you want to automatically upload source maps to Bugsnag? (this will modify your app.json and package.json)" send -- "\r" expect eof diff --git a/package.json b/package.json index 87dbbe10..143b2789 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,12 @@ "eslint-plugin-promise": "^4.2.1", "eslint-plugin-react": "^7.18.3", "eslint-plugin-standard": "^4.0.1", - "expo": "^47.0.0", + "expo": "^48.0.0", "jest": "^26.6.3", - "jest-expo": "^44.0.1", + "jest-expo": "^48.0.1", "lerna": "^6.0.1", - "react": "18.1.0", - "react-native": "0.70.5", + "react": "18.2.0", + "react-native": "0.71.3", "verdaccio": "^5.10.2" }, "scripts": { diff --git a/packages/delivery-expo/package.json b/packages/delivery-expo/package.json index f08b48b7..265bc9c5 100644 --- a/packages/delivery-expo/package.json +++ b/packages/delivery-expo/package.json @@ -18,14 +18,14 @@ "license": "MIT", "devDependencies": { "@bugsnag/core": "^7.16.0", - "@react-native-community/netinfo": "9.3.5", - "expo-crypto": "~12.0.0", - "expo-file-system": "~15.1.1" + "@react-native-community/netinfo": "9.3.7", + "expo-crypto": "~12.2.1", + "expo-file-system": "~15.2.2" }, "peerDependencies": { "@bugsnag/core": "^7.0.0", - "@react-native-community/netinfo": "9.3.5", - "expo-crypto": "~12.0.0", - "expo-file-system": "~15.1.1" + "@react-native-community/netinfo": "9.3.7", + "expo-crypto": "~12.2.1", + "expo-file-system": "~15.2.2" } } diff --git a/packages/expo-cli/commands/upload-sourcemaps.js b/packages/expo-cli/commands/upload-sourcemaps.js index 6d65ef92..22dc2323 100644 --- a/packages/expo-cli/commands/upload-sourcemaps.js +++ b/packages/expo-cli/commands/upload-sourcemaps.js @@ -2,7 +2,7 @@ const prompts = require('prompts') const addPlugin = require('../lib/configure-plugin') const installPlugin = require('../lib/install-plugin') const { onCancel, getDependencies } = require('../lib/utils') -const { isEarlierVersionThan } = require('../lib/version-information') +const { isEarlierVersionThan, getBugsnagVersionForExpoVersion } = require('../lib/version-information') const { blue, yellow } = require('kleur') const PLUGIN_NAME = '@bugsnag/plugin-expo-eas-sourcemaps' @@ -23,7 +23,7 @@ module.exports = async (argv, globalOpts) => { const res = await prompts({ type: 'confirm', name: 'addPlugin', - message: 'Do you want to automatically upload source maps to Bugsnag? (this will modify your app.json)', + message: 'Do you want to automatically upload source maps to Bugsnag? (this will modify your app.json and package.json)', initial: true }, { onCancel }) @@ -38,7 +38,12 @@ module.exports = async (argv, globalOpts) => { yarn: globalOpts.yarn } - await installPlugin(projectRoot, options) + // install a plugin version that matches the SDK version, i.e. SDK 48 -> @bugsnag/plugin-expo-eas-sourcemaps@^48.0.0 + // if there is no suitable bugsnag version we haven't yet released support for this Expo version, so install the latest + const versionInformation = getBugsnagVersionForExpoVersion(installedExpoVersion) + const pluginVersion = versionInformation ? versionInformation.bugsnagVersion : 'latest' + + await installPlugin(pluginVersion, projectRoot, options) } console.log(blue('> Inserting EAS plugin into app.json')) diff --git a/packages/expo-cli/lib/configure-plugin.js b/packages/expo-cli/lib/configure-plugin.js index 0573b3aa..c06183e9 100644 --- a/packages/expo-cli/lib/configure-plugin.js +++ b/packages/expo-cli/lib/configure-plugin.js @@ -59,23 +59,38 @@ module.exports = async (projectRoot) => { conf.expo = conf.expo || {} conf.expo.plugins = conf.expo.plugins || [] if (conf.expo.plugins.includes(plugin)) { - return plugin + ' is already installed' + console.log(blue('Plugin is already configured in app.json')) + } else { + conf.expo.plugins.push(plugin) + await promisify(writeFile)(appJsonPath, JSON.stringify(conf, null, 2), 'utf8') } - conf.expo.plugins.push(plugin) - await promisify(writeFile)(appJsonPath, JSON.stringify(conf, null, 2), 'utf8') + // update package.json + try { + const packageJsonPath = join(projectRoot, 'package.json') + const packageJson = JSON.parse(await promisify(readFile)(packageJsonPath)) + + // add the post-build hook (if it doesn't already exist) + const sourceMapBuildHook = 'npx bugsnag-eas-build-on-success' + packageJson.scripts = packageJson.scripts || {} + const existingBuildHook = packageJson.scripts['eas-build-on-success'] + + if (existingBuildHook && existingBuildHook.includes(sourceMapBuildHook)) { + console.log(blue('EAS Build hook already configured in package.json')) + } else if (existingBuildHook) { + packageJson.scripts['eas-build-on-success'] = `${existingBuildHook} && ${sourceMapBuildHook}` + } else { + packageJson.scripts['eas-build-on-success'] = sourceMapBuildHook + } - // do we need to add monorepo configuration? - const withYarnClassic = await usingYarnClassic(projectRoot) - const addMonorepoConfig = await usingWorkspaces(projectRoot, withYarnClassic) + // do we need to add monorepo configuration? + const withYarnClassic = await usingYarnClassic(projectRoot) + const addMonorepoConfig = await usingWorkspaces(projectRoot, withYarnClassic) - if (addMonorepoConfig) { - console.log(blue('> yarn workspaces detected, updating config')) + if (addMonorepoConfig) { + console.log(blue('> yarn workspaces detected, updating config')) - try { const sourceMaps = '@bugsnag/source-maps' - const packageJsonPath = join(projectRoot, 'package.json') - const packageJson = JSON.parse(await promisify(readFile)(packageJsonPath)) if (withYarnClassic) { packageJson.workspaces = packageJson.workspaces || {} @@ -86,17 +101,17 @@ module.exports = async (projectRoot) => { packageJson.installConfig = packageJson.installConfig || {} packageJson.installConfig.hoistingLimits = 'workspaces' } + } - await promisify(writeFile)(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf8') - } catch (e) { - // swallow and rethrow for errors that we can produce better messaging - if (e.code === 'ENOENT') { - throw new Error(`Couldn’t find package.json in "${projectRoot}".`) - } - if (e.name === 'SyntaxError') { - throw new Error(`Couldn’t parse package.json because it wasn’t valid JSON: "${e.message}"`) - } - throw e + await promisify(writeFile)(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf8') + } catch (e) { + // swallow and rethrow for errors that we can produce better messaging + if (e.code === 'ENOENT') { + throw new Error(`Couldn’t find package.json in "${projectRoot}".`) + } + if (e.name === 'SyntaxError') { + throw new Error(`Couldn’t parse package.json because it wasn’t valid JSON: "${e.message}"`) } + throw e } } diff --git a/packages/expo-cli/lib/install-plugin.js b/packages/expo-cli/lib/install-plugin.js index 3cd4f1a7..8b9b3989 100644 --- a/packages/expo-cli/lib/install-plugin.js +++ b/packages/expo-cli/lib/install-plugin.js @@ -1,46 +1,14 @@ -const { spawn } = require('child_process') - -function resolveCommand (options) { - const command = ['install', '@bugsnag/plugin-expo-eas-sourcemaps', '@bugsnag/source-maps'] - - if (options.npm) { - command.push('--npm') - command.push('--') - command.push('--save-dev') - } - - if (options.yarn) { - command.push('--yarn') - command.push('--') - command.push('--dev') - } - - return command -} - -module.exports = (projectRoot, options) => { - return new Promise((resolve, reject) => { - const command = resolveCommand(options) - const proc = spawn('expo', command, { cwd: projectRoot }) - - // buffer output in case of an error - let stdout = '' - let stderr = '' - proc.stdout.on('data', d => { stdout += d }) - proc.stderr.on('data', d => { stderr += d }) - - proc.on('error', err => { reject(err) }) - - proc.on('close', code => { - if (code === 0) { - return resolve() - } - - reject( - new Error( - `Command exited with non-zero exit code (${code}) "expo ${command.join(' ')}"\nstdout:\n${stdout}\n\nstderr:\n${stderr}` - ) - ) - }) +const { createForProject } = require('@expo/package-manager') +const { resolvePackageName } = require('./utils') + +module.exports = (version, projectRoot, options) => { + const packages = [resolvePackageName('@bugsnag/plugin-expo-eas-sourcemaps', version), '@bugsnag/source-maps'] + + // Expo's package manager will reject with an error if the child process exits with a non-zero code + // it also buffers the output and attaches it to any errors - https://github.com/expo/spawn-async/blob/main/src/spawnAsync.ts + const packageManager = createForProject(projectRoot, options) + return packageManager.addDevAsync(packages).catch(error => { + error.message += `\nstdout:\n${error.stdout}\n\nstderr:\n${error.stderr}` + throw error }) } diff --git a/packages/expo-cli/lib/install.js b/packages/expo-cli/lib/install.js index 1425a2e5..1f2bd098 100644 --- a/packages/expo-cli/lib/install.js +++ b/packages/expo-cli/lib/install.js @@ -1,8 +1,8 @@ const { spawn } = require('child_process') -const { DEPENDENCIES } = require('./utils') +const { DEPENDENCIES, resolvePackageName } = require('./utils') function resolveCommand (version, options) { - const command = ['install', resolvePackageName(version)].concat(DEPENDENCIES) + const command = ['install', resolvePackageName('@bugsnag/expo', version)].concat(DEPENDENCIES) if (options.npm) { command.push('--npm') @@ -15,14 +15,6 @@ function resolveCommand (version, options) { return command } -function resolvePackageName (version) { - if (version === 'latest') { - return '@bugsnag/expo' - } - - return `@bugsnag/expo@${version}` -} - module.exports = (version, projectRoot, options) => { return new Promise((resolve, reject) => { const command = resolveCommand(version, options) diff --git a/packages/expo-cli/lib/test/configure-plugin.test.js b/packages/expo-cli/lib/test/configure-plugin.test.js index f406ed0c..d5d5e1d5 100644 --- a/packages/expo-cli/lib/test/configure-plugin.test.js +++ b/packages/expo-cli/lib/test/configure-plugin.test.js @@ -1,6 +1,7 @@ const withFixture = require('./lib/with-fixture') const configurePlugin = require('../configure-plugin') const { readFile } = require('fs/promises') +const { blue } = require('kleur') describe('expo-cli: upload sourcemaps configure-plugin', () => { it('should work on a fresh project', async () => { @@ -10,25 +11,58 @@ describe('expo-cli: upload sourcemaps configure-plugin', () => { const appJsonRaw = await readFile(`${projectRoot}/app.json`, 'utf8') const appJson = JSON.parse(appJsonRaw) - expect(appJson.expo.plugins).toContain('@bugsnag/plugin-expo-eas-sourcemaps') + + const packageJsonRaw = await readFile(`${projectRoot}/package.json`, 'utf8') + const packageJson = JSON.parse(packageJsonRaw) + expect(packageJson.scripts['eas-build-on-success']).toContain('npx bugsnag-eas-build-on-success') }) }) it('shouldn’t duplicate the hook config', async () => { await withFixture('already-installed-02', async (projectRoot) => { - const msg = await configurePlugin(projectRoot) - expect(msg).toMatch(/ is already installed/) + const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}) + + await configurePlugin(projectRoot) + expect(logSpy).toHaveBeenCalledWith(blue('Plugin is already configured in app.json')) const appJsonRaw = await readFile(`${projectRoot}/app.json`, 'utf8') const appJson = JSON.parse(appJsonRaw) - expect(appJson.expo.plugins.length).toBe(1) }) }) - it('should create a basic file when there is no app.json', async () => { + it('shouldn’t duplicate the EAS build hook', async () => { + await withFixture('already-installed-01', async (projectRoot) => { + const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}) + + await configurePlugin(projectRoot) + expect(logSpy).toHaveBeenCalledWith(blue('EAS Build hook already configured in package.json')) + + const packageJsonRaw = await readFile(`${projectRoot}/package.json`, 'utf8') + const packageJson = JSON.parse(packageJsonRaw) + expect(packageJson.scripts['eas-build-on-success']).toStrictEqual('npx bugsnag-eas-build-on-success') + }) + }) + + it('should chain to an existing EAS build hook if present', async () => { + await withFixture('already-installed-02', async (projectRoot) => { + await configurePlugin(projectRoot) + + const packageJsonRaw = await readFile(`${projectRoot}/package.json`, 'utf8') + const packageJson = JSON.parse(packageJsonRaw) + expect(packageJson.scripts['eas-build-on-success']).toStrictEqual('pre-existing-command && npx bugsnag-eas-build-on-success') + }) + }) + + it('should provide a reasonable error when there is no package.json', async () => { await withFixture('empty-00', async (projectRoot) => { + await expect(configurePlugin(projectRoot)).rejects.toThrow(/Couldn’t find package\.json/) + }) + }) + + it('should create a basic file when there is no app.json', async () => { + await withFixture('empty-01', async (projectRoot) => { const msg = await configurePlugin(projectRoot) expect(msg).toBe(undefined) diff --git a/packages/expo-cli/lib/test/fixtures/already-installed-01/package.json b/packages/expo-cli/lib/test/fixtures/already-installed-01/package.json index c74db847..1deb11ba 100644 --- a/packages/expo-cli/lib/test/fixtures/already-installed-01/package.json +++ b/packages/expo-cli/lib/test/fixtures/already-installed-01/package.json @@ -10,5 +10,8 @@ "expo-crypto": "*", "expo-device": "*", "expo-file-system": "*" + }, + "scripts": { + "eas-build-on-success": "npx bugsnag-eas-build-on-success" } } diff --git a/packages/expo-cli/lib/test/fixtures/already-installed-02/package.json b/packages/expo-cli/lib/test/fixtures/already-installed-02/package.json index fb4e3011..e0da8ba1 100644 --- a/packages/expo-cli/lib/test/fixtures/already-installed-02/package.json +++ b/packages/expo-cli/lib/test/fixtures/already-installed-02/package.json @@ -11,5 +11,8 @@ "expo-crypto": "*", "expo-device": "*", "expo-file-system": "*" + }, + "scripts": { + "eas-build-on-success": "pre-existing-command" } } diff --git a/packages/expo-cli/lib/test/fixtures/empty-00/README.md b/packages/expo-cli/lib/test/fixtures/empty-00/README.md index 1dc26376..4f78e47e 100644 --- a/packages/expo-cli/lib/test/fixtures/empty-00/README.md +++ b/packages/expo-cli/lib/test/fixtures/empty-00/README.md @@ -1 +1 @@ -This project has no app.json or App.js +This project has no app.json, App.js or package.json diff --git a/packages/expo-cli/lib/test/fixtures/empty-01/README.md b/packages/expo-cli/lib/test/fixtures/empty-01/README.md new file mode 100644 index 00000000..1dc26376 --- /dev/null +++ b/packages/expo-cli/lib/test/fixtures/empty-01/README.md @@ -0,0 +1 @@ +This project has no app.json or App.js diff --git a/packages/expo-cli/lib/test/fixtures/empty-01/package.json b/packages/expo-cli/lib/test/fixtures/empty-01/package.json new file mode 100644 index 00000000..e11fc6c0 --- /dev/null +++ b/packages/expo-cli/lib/test/fixtures/empty-01/package.json @@ -0,0 +1,5 @@ +{ + "name": "empty-01", + "private": "true", + "version": "0.0.0" +} diff --git a/packages/expo-cli/lib/test/install-plugin.test.js b/packages/expo-cli/lib/test/install-plugin.test.js index 87be295c..6ba0de79 100644 --- a/packages/expo-cli/lib/test/install-plugin.test.js +++ b/packages/expo-cli/lib/test/install-plugin.test.js @@ -1,6 +1,4 @@ const withFixture = require('./lib/with-fixture') -const { EventEmitter } = require('events') -const { Readable } = require('stream') describe('expo-cli: upload-sourcemaps install plugin', () => { beforeEach(() => { @@ -9,187 +7,167 @@ describe('expo-cli: upload-sourcemaps install plugin', () => { it('should work on a fresh project', async () => { await withFixture('blank-00', async (projectRoot) => { - const spawn = (cmd, args, opts) => { - expect(cmd).toBe('expo') - expect(args).toEqual(['install', '@bugsnag/plugin-expo-eas-sourcemaps', '@bugsnag/source-maps']) - expect(opts).toEqual({ cwd: projectRoot }) - - const proc = new EventEmitter() - proc.stdout = new Readable({ - read () {} - }) - proc.stderr = new Readable({ - read () {} - }) - setTimeout(() => proc.emit('close', 0), 10) - return proc + const packageManager = { + addDevAsync: async (packages) => { + expect(packages).toEqual(['@bugsnag/plugin-expo-eas-sourcemaps', '@bugsnag/source-maps']) + return Promise.resolve() + } + } + + const createForProject = (root, options) => { + expect(root).toEqual(projectRoot) + expect(options).toEqual({ npm: false, yarn: false }) + return packageManager } - jest.doMock('child_process', () => ({ spawn })) + jest.doMock('@expo/package-manager', () => ({ createForProject })) const installPlugin = require('../install-plugin') - const msg = await installPlugin(projectRoot, { npm: false, yarn: false }) + const msg = await installPlugin('latest', projectRoot, { npm: false, yarn: false }) expect(msg).toBe(undefined) }) }) it('should allow forcing install with NPM', async () => { await withFixture('blank-00', async (projectRoot) => { - const spawn = (cmd, args, opts) => { - expect(cmd).toBe('expo') - expect(args).toEqual([ - 'install', - '@bugsnag/plugin-expo-eas-sourcemaps', - '@bugsnag/source-maps', - '--npm', - '--', - '--save-dev' - ]) - expect(opts).toEqual({ cwd: projectRoot }) - - const proc = new EventEmitter() - proc.stdout = new Readable({ - read () {} - }) - proc.stderr = new Readable({ - read () {} - }) - setTimeout(() => proc.emit('close', 0), 10) - return proc + const packageManager = { + addDevAsync: async (packages) => { + expect(packages).toEqual(['@bugsnag/plugin-expo-eas-sourcemaps', '@bugsnag/source-maps']) + return Promise.resolve() + } + } + + const createForProject = (root, options) => { + expect(root).toEqual(projectRoot) + expect(options).toEqual({ npm: true }) + return packageManager } - jest.doMock('child_process', () => ({ spawn })) + jest.doMock('@expo/package-manager', () => ({ createForProject })) const installPlugin = require('../install-plugin') - const msg = await installPlugin(projectRoot, { npm: true }) + const msg = await installPlugin('latest', projectRoot, { npm: true }) expect(msg).toBe(undefined) }) }) it('should allow forcing install with Yarn', async () => { await withFixture('blank-00', async (projectRoot) => { - const spawn = (cmd, args, opts) => { - expect(cmd).toBe('expo') - expect(args).toEqual([ - 'install', - '@bugsnag/plugin-expo-eas-sourcemaps', - '@bugsnag/source-maps', - '--yarn', - '--', - '--dev' - ]) - expect(opts).toEqual({ cwd: projectRoot }) - - const proc = new EventEmitter() - proc.stdout = new Readable({ - read () {} - }) - proc.stderr = new Readable({ - read () {} - }) - setTimeout(() => proc.emit('close', 0), 10) - return proc + const packageManager = { + addDevAsync: async (packages) => { + expect(packages).toEqual(['@bugsnag/plugin-expo-eas-sourcemaps', '@bugsnag/source-maps']) + return Promise.resolve() + } + } + + const createForProject = (root, options) => { + expect(root).toEqual(projectRoot) + expect(options).toEqual({ yarn: true }) + return packageManager } - jest.doMock('child_process', () => ({ spawn })) + jest.doMock('@expo/package-manager', () => ({ createForProject })) const installPlugin = require('../install-plugin') - const msg = await installPlugin(projectRoot, { yarn: true }) + const msg = await installPlugin('latest', projectRoot, { yarn: true }) expect(msg).toBe(undefined) }) }) - // highly doubt this will ever fail, assuming one of npm or yarn will take precedence over the other - // if test begins to fail, might need to consider additonal error messages + // not sure if this test is really necessary any more? it('should allow forcing install with both NPM and Yarn', async () => { await withFixture('blank-00', async (projectRoot) => { - const spawn = (cmd, args, opts) => { - expect(cmd).toBe('expo') - expect(args).toEqual([ - 'install', - '@bugsnag/plugin-expo-eas-sourcemaps', - '@bugsnag/source-maps', - '--npm', - '--', - '--save-dev', - '--yarn', - '--', - '--dev' - ]) - - expect(opts).toEqual({ cwd: projectRoot }) - - const proc = new EventEmitter() - proc.stdout = new Readable({ - read () {} - }) - proc.stderr = new Readable({ - read () {} - }) - setTimeout(() => proc.emit('close', 0), 10) - return proc + const packageManager = { + addDevAsync: async (packages) => { + expect(packages).toEqual(['@bugsnag/plugin-expo-eas-sourcemaps', '@bugsnag/source-maps']) + return Promise.resolve() + } } - jest.doMock('child_process', () => ({ spawn })) + const createForProject = (root, options) => { + expect(root).toEqual(projectRoot) + expect(options).toEqual({ npm: true, yarn: true }) + return packageManager + } + + jest.doMock('@expo/package-manager', () => ({ createForProject })) const installPlugin = require('../install-plugin') - const msg = await installPlugin(projectRoot, { npm: true, yarn: true }) + const msg = await installPlugin('latest', projectRoot, { npm: true, yarn: true }) expect(msg).toBe(undefined) }) }) - it('should add stderr/stdout output onto error if there is one (non-zero exit code)', async () => { - const spawn = (cmd, args, opts) => { - const proc = new EventEmitter() - proc.stdout = new Readable({ - read () { - this.push('some data on stdout') - this.push(null) - } - }) - proc.stderr = new Readable({ - read () { - this.push('some data on stderr') - this.push(null) + it('should allow specifying a package version', async () => { + await withFixture('blank-00', async (projectRoot) => { + const packageManager = { + addDevAsync: async (packages) => { + expect(packages).toEqual(['@bugsnag/plugin-expo-eas-sourcemaps@^48.0.0', '@bugsnag/source-maps']) + return Promise.resolve() } - }) - setTimeout(() => proc.emit('close', 1), 10) - return proc - } + } - jest.doMock('child_process', () => ({ spawn })) - const installPlugin = require('../install-plugin') + const createForProject = (root, options) => { + expect(root).toEqual(projectRoot) + expect(options).toEqual({ yarn: false }) + return packageManager + } + + jest.doMock('@expo/package-manager', () => ({ createForProject })) + const installPlugin = require('../install-plugin') + const msg = await installPlugin('^48.0.0', projectRoot, { yarn: false }) + expect(msg).toBe(undefined) + }) + }) + it('should throw an error if the command does', async () => { await withFixture('blank-00', async (projectRoot) => { - const expected = `Command exited with non-zero exit code (1) "expo install @bugsnag/plugin-expo-eas-sourcemaps @bugsnag/source-maps" -stdout: -some data on stdout + const packageManager = { + addDevAsync: async (packages) => { + expect(packages).toEqual(['@bugsnag/plugin-expo-eas-sourcemaps', '@bugsnag/source-maps']) + return Promise.reject(new Error('floop')) + } + } -stderr: -some data on stderr` + const createForProject = (root, options) => { + expect(root).toEqual(projectRoot) + expect(options).toEqual({ yarn: false }) + return packageManager + } - await expect(installPlugin(projectRoot, { npm: false })).rejects.toThrow(expected) + jest.doMock('@expo/package-manager', () => ({ createForProject })) + const installPlugin = require('../install-plugin') + await expect(installPlugin('latest', projectRoot, { yarn: false })).rejects.toThrow(/floop/) }) }) - it('should throw an error if the command does', async () => { - const spawn = (cmd, args, opts) => { - const proc = new EventEmitter() - proc.stdout = new Readable({ - read () {} - }) - proc.stderr = new Readable({ - read () {} - }) - setTimeout(() => proc.emit('error', new Error('floop')), 10) - return proc + it('should add stderr/stdout output onto error if there is one', async () => { + const packageManager = { + addDevAsync: async (packages) => { + expect(packages).toEqual(['@bugsnag/plugin-expo-eas-sourcemaps', '@bugsnag/source-maps']) + const error = new Error('floop') + error.stdout = 'some data on stdout' + error.stderr = 'some data on stderr' + return Promise.reject(error) + } } - jest.doMock('child_process', () => ({ spawn })) + const createForProject = (root, options) => { + return packageManager + } + + jest.doMock('@expo/package-manager', () => ({ createForProject })) const installPlugin = require('../install-plugin') await withFixture('blank-00', async (projectRoot) => { - await expect(installPlugin(projectRoot, { yarn: false })).rejects.toThrow(/floop/) + const expected = `floop +stdout: +some data on stdout + +stderr: +some data on stderr` + + await expect(installPlugin('latest', projectRoot, { npm: false })).rejects.toThrow(expected) }) }) }) diff --git a/packages/expo-cli/lib/utils.js b/packages/expo-cli/lib/utils.js index 60b9e0cd..953ec2a1 100644 --- a/packages/expo-cli/lib/utils.js +++ b/packages/expo-cli/lib/utils.js @@ -22,9 +22,18 @@ async function getDependencies (directory) { return cachedDependencies.get(directory) } +function resolvePackageName (packageName, version) { + if (version === 'latest') { + return packageName + } + + return `${packageName}@${version}` +} + module.exports = { onCancel: () => process.exit(), getDependencies, + resolvePackageName, DEPENDENCIES: [ '@react-native-community/netinfo', 'expo-application', diff --git a/packages/expo-cli/lib/version-information.js b/packages/expo-cli/lib/version-information.js index cd2e8ce5..38ca2fa4 100644 --- a/packages/expo-cli/lib/version-information.js +++ b/packages/expo-cli/lib/version-information.js @@ -1,7 +1,7 @@ const semver = require('semver') // the major version number of the latest Expo SDK we support -const LATEST_SUPPORTED_EXPO_SDK = 47 +const LATEST_SUPPORTED_EXPO_SDK = 48 class Version { constructor (expoSdkVersion, bugsnagVersion, isLegacy = false) { diff --git a/packages/expo-cli/package.json b/packages/expo-cli/package.json index 98bbcd68..609467ce 100644 --- a/packages/expo-cli/package.json +++ b/packages/expo-cli/package.json @@ -11,6 +11,7 @@ "author": "Bugsnag", "license": "MIT", "dependencies": { + "@expo/package-manager": "^1.0.1", "command-line-args": "^5.0.2", "kleur": "^3.0.2", "prompts": "^2.0.4", diff --git a/packages/expo/package.json b/packages/expo/package.json index 384f2c58..ccf0537e 100644 --- a/packages/expo/package.json +++ b/packages/expo/package.json @@ -49,12 +49,12 @@ "bugsnag-build-reporter": "^1.0.1" }, "devDependencies": { - "expo-constants": "~14.0.2" + "expo-constants": "~14.2.1" }, "peerDependencies": { - "expo": "^47.0.0", - "expo-constants": "~14.0.2", - "promise": "^8", + "expo": "^48.0.0", + "expo-constants": "~14.2.1", + "promise": "^8.3.0", "react": "*" } } diff --git a/packages/plugin-expo-app/package.json b/packages/plugin-expo-app/package.json index 8cfa5449..374d0932 100644 --- a/packages/plugin-expo-app/package.json +++ b/packages/plugin-expo-app/package.json @@ -18,12 +18,12 @@ "license": "MIT", "devDependencies": { "@bugsnag/core": "^7.16.0", - "expo-application": "~5.0.1", - "expo-constants": "~14.0.2" + "expo-application": "~5.1.1", + "expo-constants": "~14.2.1" }, "peerDependencies": { "@bugsnag/core": "^7.0.0", - "expo-application": "~5.0.1", - "expo-constants": "~14.0.2" + "expo-application": "~5.1.1", + "expo-constants": "~14.2.1" } } diff --git a/packages/plugin-expo-connectivity-breadcrumbs/package.json b/packages/plugin-expo-connectivity-breadcrumbs/package.json index 38f6d21d..765f3212 100644 --- a/packages/plugin-expo-connectivity-breadcrumbs/package.json +++ b/packages/plugin-expo-connectivity-breadcrumbs/package.json @@ -18,10 +18,10 @@ "license": "MIT", "devDependencies": { "@bugsnag/core": "^7.16.0", - "@react-native-community/netinfo": "9.3.5" + "@react-native-community/netinfo": "9.3.7" }, "peerDependencies": { "@bugsnag/core": "^7.0.0", - "@react-native-community/netinfo": "9.3.5" + "@react-native-community/netinfo": "9.3.7" } } diff --git a/packages/plugin-expo-device/package.json b/packages/plugin-expo-device/package.json index c62debe4..7984a557 100644 --- a/packages/plugin-expo-device/package.json +++ b/packages/plugin-expo-device/package.json @@ -18,12 +18,12 @@ "license": "MIT", "devDependencies": { "@bugsnag/core": "^7.16.0", - "expo-constants": "~14.0.2", - "expo-device": "~5.0.0" + "expo-constants": "~14.2.1", + "expo-device": "~5.2.1" }, "peerDependencies": { "@bugsnag/core": "^7.0.0", - "expo-constants": "~14.0.2", - "expo-device": "~5.0.0" + "expo-constants": "~14.2.1", + "expo-device": "~5.2.1" } } diff --git a/packages/plugin-expo-eas-sourcemaps/index.js b/packages/plugin-expo-eas-sourcemaps/index.js index 37b88c42..8d27fee1 100644 --- a/packages/plugin-expo-eas-sourcemaps/index.js +++ b/packages/plugin-expo-eas-sourcemaps/index.js @@ -1,4 +1,3 @@ -const { withAndroidPlugin } = require('./src/android') const { withIosPlugin } = require('./src/ios') const { createRunOncePlugin, WarningAggregator } = require('@expo/config-plugins') @@ -7,7 +6,6 @@ const pkg = require('./package.json') function withSourcemapUploads (config) { const onPremConfig = getOnPremConfig(config) - config = withAndroidPlugin(config, onPremConfig) config = withIosPlugin(config, onPremConfig) return config } diff --git a/packages/plugin-expo-eas-sourcemaps/lib/eas-build-on-success.js b/packages/plugin-expo-eas-sourcemaps/lib/eas-build-on-success.js new file mode 100755 index 00000000..526d1b4a --- /dev/null +++ b/packages/plugin-expo-eas-sourcemaps/lib/eas-build-on-success.js @@ -0,0 +1,60 @@ +#!/usr/bin/env node + +const { access } = require('fs').promises +const { reactNative } = require('@bugsnag/source-maps') +const { exit } = require('process') + +const PROJECT_ROOT = process.cwd() + +if (process.env.EAS_BUILD_PLATFORM !== 'android') { + console.log('Skipping Android source map upload: Android build not detected') + exit(0) +} else if (process.env.EAS_BUILD_PROFILE === 'development') { + console.log('Skipping Android source map upload: Development build detected') + exit(0) +} + +const uploadSourceMaps = async () => { + const bundle = `${PROJECT_ROOT}/android/app/build/generated/assets/createBundleReleaseJsAndAssets/index.android.bundle` + await access(bundle).catch((error) => { + console.log(`Skipping Android source map upload: App bundle ${bundle} could not be found.\n${error}`) + exit(0) + }) + + const sourceMap = `${PROJECT_ROOT}/android/app/build/generated/sourcemaps/react/release/index.android.bundle.map` + await access(sourceMap).catch((error) => { + console.error(`Error: source map ${sourceMap} could not be found.\n${error}`) + exit(1) + }) + + let appConfig, apiKey + try { + appConfig = require(`${PROJECT_ROOT}/app.json`) + apiKey = appConfig?.expo?.extra?.bugsnag?.apiKey + } catch (error) { + console.error(`Error: Failed to load app.json file ${PROJECT_ROOT}/app.json.\n${error}`) + exit(1) + } + + if (!apiKey) { + console.error('Error: No Bugsnag API key detected in app.json') + exit(1) + } + + console.log('Uploading Android source map to Bugsnag...') + await reactNative.uploadOne({ + apiKey, + bundle, + sourceMap, + platform: 'android', + appVersion: appConfig?.expo?.version, + appVersionCode: appConfig?.expo?.android?.versionCode?.toString() + }).then(() => { + console.log(`Successfully uploaded the following files:\n${[bundle, sourceMap].join('\n')}`) + }).catch(error => { + console.error(`Error uploading source map: ${error}`) + exit(1) + }) +} + +uploadSourceMaps() diff --git a/packages/plugin-expo-eas-sourcemaps/package.json b/packages/plugin-expo-eas-sourcemaps/package.json index bd379d51..517bdfdf 100644 --- a/packages/plugin-expo-eas-sourcemaps/package.json +++ b/packages/plugin-expo-eas-sourcemaps/package.json @@ -3,6 +3,9 @@ "version": "47.1.1", "description": "Plugin to handle uploading sourcemaps to bugsnag when using the EAS build system", "main": "index.js", + "bin": { + "bugsnag-eas-build-on-success": "lib/eas-build-on-success.js" + }, "homepage": "https://www.bugsnag.com/", "repository": { "type": "git", diff --git a/packages/plugin-expo-eas-sourcemaps/src/android.js b/packages/plugin-expo-eas-sourcemaps/src/android.js deleted file mode 100644 index ea87bf15..00000000 --- a/packages/plugin-expo-eas-sourcemaps/src/android.js +++ /dev/null @@ -1,70 +0,0 @@ -const { - AndroidConfig, - withAppBuildGradle, - withAndroidManifest -} = require('@expo/config-plugins') - -// Using helpers keeps error messages unified and helps cut down on XML format changes. -const { addMetaDataItemToMainApplication, getMainApplicationOrThrow } = AndroidConfig.Manifest - -const withAndroidPlugin = (config, onPremConfig) => { - // Update android manifest with bugsnag config - config = withAndroidManifest(config, config => { - config.modResults = setBugsnagConfig(config, config.modResults) - return config - }) - - // Inject gradle dependencies - config = withAppBuildGradle(config, (config) => { - if (config.modResults.language === 'groovy') { - config.modResults.contents = injectDependencies(config.modResults.contents) - } else { - throw new Error( - 'Cannot configure Bugsnag in the app gradle because the build.gradle is not groovy' - ) - } - return config - }) - - return config -} - -// Splitting this function out of the mod makes it easier to test. -function setBugsnagConfig (config, androidManifest) { - const apiKeyName = 'com.bugsnag.android.API_KEY' - const apiKeyValue = config?.extra?.bugsnag?.apiKey - - // Get the tag and assert if it doesn't exist. - const mainApplication = getMainApplicationOrThrow(androidManifest) - - addMetaDataItemToMainApplication( - mainApplication, - apiKeyName, - apiKeyValue - ) - - return androidManifest -} - -function injectDependencies (script) { - return `buildscript { - repositories { - mavenCentral() - } - dependencies { - classpath 'com.bugsnag:bugsnag-android-gradle-plugin:7.+' - } - } - - ${script} - - apply plugin: 'com.bugsnag.android.gradle' - - bugsnag { - uploadReactNativeMappings = true - }` -} - -module.exports = { - withAndroidPlugin -}