diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 8484a9ff65..809aa802d1 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -263,6 +263,7 @@ jobs: run: | patch --verbose --strip=0 --force --ignore-whitespace --fuzz 4 < ../../../rn.patch ../../../rn.patch.app.js --app . + ../../../rn.patch.metro.config.js --path metro.config.js - name: Patch Android App RN if: ${{ matrix.platform == 'android' }} diff --git a/.npmignore b/.npmignore index ad2bd3e10d..c8508435dc 100644 --- a/.npmignore +++ b/.npmignore @@ -13,3 +13,7 @@ !/android/**/* !src/js/NativeRNSentry.ts !scripts/collect-modules.sh +!scripts/copy-debugid.js +!scripts/has-sourcemap-debugid.js +!scripts/sentry-xcode.sh +!scripts/sentry-xcode-debug-files.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index cbc43ae7b9..ab9ebe68a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,32 @@ ### Features - Add `buildFeatures.buildConfig=true` to support AGP 8 ([#3298](https://github.com/getsentry/sentry-react-native/pull/3298)) +- Add Debug ID support ([#3164](https://github.com/getsentry/sentry-react-native/pull/3164)) + + This is optional to use Debug IDs. Your current setup will keep working as is. + + Add Sentry Metro Serializer to `metro.config.js` to generate Debug ID for the application bundle and source map. + + ```javascript + const {createSentryMetroSerializer} = require('@sentry/react-native/dist/js/tools/sentryMetroSerializer'); + const config = {serializer: createSentryMetroSerializer()}; + ``` + + On iOS update `Bundle React Native Code and Images` and `Upload Debug Symbols to Sentry` build phases. + + ```bash + set -e + WITH_ENVIRONMENT="../node_modules/react-native/scripts/xcode/with-environment.sh" + REACT_NATIVE_XCODE="../node_modules/react-native/scripts/react-native-xcode.sh" + + /bin/sh -c "$WITH_ENVIRONMENT \"/bin/sh ../scripts/sentry-xcode.sh $REACT_NATIVE_XCODE\"" + ``` + + ```bash + /bin/sh ../../scripts/sentry-xcode-debug-files.sh + ``` + + More information about the new setup [can be found here](https://docs.sentry.io/platforms/react-native/manual-setup/manual-setup/). ### Fixes diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000000..d8df7d57a8 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,16 @@ +module.exports = { + collectCoverage: true, + preset: 'react-native', + setupFilesAfterEnv: ['/test/mockConsole.ts'], + globals: { + __DEV__: true, + 'ts-jest': { + tsConfig: './tsconfig.json', + diagnostics: false, + }, + }, + moduleFileExtensions: ['ts', 'tsx', 'js'], + testPathIgnorePatterns: ['/test/e2e/', '/test/tools/', '/test/react-native/versions'], + testEnvironment: 'node', + testMatch: ['**/*.test.(ts|tsx)'], +}; diff --git a/jest.config.tools.js b/jest.config.tools.js new file mode 100644 index 0000000000..5c5902d8a7 --- /dev/null +++ b/jest.config.tools.js @@ -0,0 +1,18 @@ +module.exports = { + collectCoverage: true, + preset: 'ts-jest', + setupFilesAfterEnv: ['/test/mockConsole.ts'], + globals: { + __DEV__: true, + }, + testMatch: ['**/test/tools/**/*.ts'], + transform: { + '^.+\\.(ts|tsx)$': [ + 'ts-jest', + { + tsconfig: './tsconfig.build.tools.json', + diagnostics: false, + }, + ], + }, +}; diff --git a/package.json b/package.json index 613d289fa1..7af9e67216 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,9 @@ "build:tools": "tsc -p tsconfig.build.tools.json", "downlevel": "downlevel-dts dist ts3.8/dist --to=3.8", "clean": "rimraf dist coverage", - "test": "jest", + "test": "yarn test:sdk && yarn test:tools", + "test:sdk": "jest", + "test:tools": "jest --config jest.config.tools.js", "fix": "yarn fix:eslint && yarn fix:prettier", "fix:eslint": "eslint --config .eslintrc.js --fix .", "fix:prettier": "prettier --write \"{src,test,scripts}/**/**.ts\"", @@ -72,6 +74,8 @@ "@sentry/wizard": "3.13.0", "@types/jest": "^29.5.3", "@types/react": "^18.2.14", + "@types/uglify-js": "^3.17.2", + "@types/uuid": "^9.0.4", "babel-jest": "^29.6.2", "downlevel-dts": "^0.11.0", "eslint": "^7.6.0", @@ -79,13 +83,16 @@ "eslint-plugin-react-native": "^3.8.1", "jest": "^29.6.2", "jest-environment-jsdom": "^29.6.2", + "metro": "0.76", "prettier": "^2.0.5", "react": "18.2.0", "react-native": "0.72.4", "replace-in-file": "^7.0.1", "rimraf": "^4.1.1", "ts-jest": "^29.1.1", - "typescript": "4.9.5" + "typescript": "4.9.5", + "uglify-js": "^3.17.4", + "uuid": "^9.0.1" }, "rnpm": { "commands": {}, @@ -95,33 +102,6 @@ }, "ios": {} }, - "jest": { - "collectCoverage": true, - "preset": "react-native", - "setupFilesAfterEnv": [ - "/test/mockConsole.ts" - ], - "globals": { - "__DEV__": true, - "ts-jest": { - "tsConfig": "./tsconfig.json", - "diagnostics": false - } - }, - "moduleFileExtensions": [ - "ts", - "tsx", - "js" - ], - "testPathIgnorePatterns": [ - "/test/e2e/", - "/test/react-native/versions" - ], - "testEnvironment": "node", - "testMatch": [ - "**/*.test.(ts|tsx)" - ] - }, "codegenConfig": { "name": "RNSentrySpec", "type": "modules", diff --git a/sample-new-architecture/android/app/build.gradle b/sample-new-architecture/android/app/build.gradle index e66f107efd..4f753e08bb 100644 --- a/sample-new-architecture/android/app/build.gradle +++ b/sample-new-architecture/android/app/build.gradle @@ -74,6 +74,8 @@ project.ext.sentryCli = [ "../..", ], skipCollectModules: false, + copyDebugIdScript: "../../../scripts/copy-debugid.js", + hasSourceMapDebugIdScript: "../../../scripts/has-sourcemap-debugid.js", ] apply from: "../../../sentry.gradle" diff --git a/sample-new-architecture/android/gradle.properties b/sample-new-architecture/android/gradle.properties index 885445bfef..7661dfcf3e 100644 --- a/sample-new-architecture/android/gradle.properties +++ b/sample-new-architecture/android/gradle.properties @@ -11,6 +11,7 @@ # The setting is particularly useful for tweaking memory settings. # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m +org.gradle.logging.level=lifecycle # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit diff --git a/sample-new-architecture/ios/.xcode.env b/sample-new-architecture/ios/.xcode.env index dcc18e1756..b2f8e82e61 100644 --- a/sample-new-architecture/ios/.xcode.env +++ b/sample-new-architecture/ios/.xcode.env @@ -9,3 +9,11 @@ # For example, to use nvm with brew, add the following line #. "$(brew --prefix nvm)/nvm.sh" --no-use export NODE_BINARY=$(command -v node) + +export EXTRA_COMPILER_ARGS="-w" + +export SENTRY_CLI_EXECUTABLE="../../node_modules/@sentry/cli/bin/sentry-cli" +export SENTRY_CLI_EXTRA_ARGS="--force-foreground" +export SENTRY_CLI_DEBUG_FILES_UPLOAD_EXTRA_ARGS="" +export SENTRY_CLI_RN_XCODE_EXTRA_ARGS="" +export MODULES_PATHS="$PWD/../node_modules,$PWD/../../.." diff --git a/sample-new-architecture/ios/sampleNewArchitecture.xcodeproj/project.pbxproj b/sample-new-architecture/ios/sampleNewArchitecture.xcodeproj/project.pbxproj index 1387b931ec..9d1c7662bd 100644 --- a/sample-new-architecture/ios/sampleNewArchitecture.xcodeproj/project.pbxproj +++ b/sample-new-architecture/ios/sampleNewArchitecture.xcodeproj/project.pbxproj @@ -271,7 +271,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "set -e\nWITH_ENVIRONMENT=\"../node_modules/react-native/scripts/xcode/with-environment.sh\"\n\nexport SENTRY_PROPERTIES=sentry.properties\nexport EXTRA_PACKAGER_ARGS=\"--sourcemap-output $DERIVED_FILE_DIR/main.jsbundle.map\"\n\nREACT_NATIVE_XCODE=\"../node_modules/react-native/scripts/react-native-xcode.sh\"\nSENTRY_CLI=\"../../node_modules/@sentry/cli/bin/sentry-cli\"\nBUNDLE_REACT_NATIVE=\"$SENTRY_CLI react-native xcode --force-foreground $REACT_NATIVE_XCODE\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT \\\"$BUNDLE_REACT_NATIVE\\\"\"\n\nexport MODULES_PATHS=\"$PWD/../node_modules,$PWD/../../..\"\nCOLLECT_MODULES=\"../../scripts/collect-modules.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT \\\"$COLLECT_MODULES\\\"\"\n"; + shellScript = "set -e\nWITH_ENVIRONMENT=\"../node_modules/react-native/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"../node_modules/react-native/scripts/react-native-xcode.sh\"\nBUNDLE_REACT_NATIVE=\"/bin/sh ../../scripts/sentry-xcode.sh $REACT_NATIVE_XCODE\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT \\\"$BUNDLE_REACT_NATIVE\\\"\"\n"; }; 00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; @@ -303,7 +303,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "set -e\nWITH_ENVIRONMENT=\"../node_modules/react-native/scripts/xcode/with-environment.sh\"\n\nexport SENTRY_PROPERTIES=sentry.properties\n\n[[ $SENTRY_INCLUDE_NATIVE_SOURCES == \"true\" ]] && INCLUDE_SOURCES_FLAG=\"--include-sources\" || INCLUDE_SOURCES_FLAG=\"\"\nSENTRY_CLI=\"../../node_modules/@sentry/cli/bin/sentry-cli\"\nFLAGS=\"--force-foreground $INCLUDE_SOURCES_FLAG\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT \\\"$UPLOAD_DEBUG_FILES\\\"\"\n"; + shellScript = "/bin/sh ../../scripts/sentry-xcode-debug-files.sh\n"; }; A55EABD7B0C7F3A422A6CC61 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; diff --git a/sample-new-architecture/metro.config.js b/sample-new-architecture/metro.config.js index 4ea8ab6b3e..7ef4237470 100644 --- a/sample-new-architecture/metro.config.js +++ b/sample-new-architecture/metro.config.js @@ -2,6 +2,9 @@ const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); const path = require('path'); const blacklist = require('metro-config/src/defaults/exclusionList'); +const { + createSentryMetroSerializer, +} = require('../dist/js/tools/sentryMetroSerializer'); const parentDir = path.resolve(__dirname, '..'); /** @@ -41,6 +44,10 @@ const config = { }, ), }, + serializer: { + customSerializer: createSentryMetroSerializer(), + }, }; -module.exports = mergeConfig(getDefaultConfig(__dirname), config); +const m = mergeConfig(getDefaultConfig(__dirname), config); +module.exports = m; diff --git a/scripts/copy-debugid.js b/scripts/copy-debugid.js new file mode 100644 index 0000000000..7df5fe105b --- /dev/null +++ b/scripts/copy-debugid.js @@ -0,0 +1,52 @@ +const process = require('process'); +const fs = require('fs'); + +console.log('Copy `debugId` from packager source map to Hermes source map...'); + +const packagerSourceMapPath = process.argv[2]; +const hermesSourceMapPath = process.argv[3]; + +if (!packagerSourceMapPath) { + console.log('Please provide packager source map path (A path to copy `debugId` from).'); + process.exit(0); +} +if (!hermesSourceMapPath) { + console.log('Please provide Hermes source map path. ((A path to copy `debugId` to))'); + process.exit(0); +} +if (!fs.existsSync(packagerSourceMapPath)) { + console.log('Packager source map path (A path to copy `debugId` from).'); + process.exit(0); +} +if (!fs.existsSync(hermesSourceMapPath)) { + console.log('Hermes source map not found. ((A path to copy `debugId` to))'); + process.exit(0); +} + +const from = fs.readFileSync(process.argv[2], 'utf8'); +const to = fs.readFileSync(process.argv[3], 'utf8'); + +const fromParsed = JSON.parse(from); +const toParsed = JSON.parse(to); + +if (!fromParsed.debugId && !fromParsed.debug_id) { + console.log('Packager source map does not have `debugId`.'); + process.exit(0); +} + +if (toParsed.debugId || toParsed.debug_id) { + console.log('Hermes combined source map already has `debugId`.'); + process.exit(0); +} + +if (fromParsed.debugId) { + toParsed.debugId = fromParsed.debugId; + toParsed.debug_id = fromParsed.debugId; +} else if (fromParsed.debug_id) { + toParsed.debugId = fromParsed.debug_id; + toParsed.debug_id = fromParsed.debug_id; +} + +fs.writeFileSync(process.argv[3], JSON.stringify(toParsed)); + +console.log('Done.'); diff --git a/scripts/has-sourcemap-debugid.js b/scripts/has-sourcemap-debugid.js new file mode 100644 index 0000000000..4ca526b49b --- /dev/null +++ b/scripts/has-sourcemap-debugid.js @@ -0,0 +1,31 @@ +const process = require('process'); +const fs = require('fs'); + +const sourceMapPath = process.argv[2]; + +if (!sourceMapPath) { + console.log('Add source map path as first argument of the script.'); + process.exit(1); +} + +if (!fs.existsSync(sourceMapPath)) { + console.log(`${sourceMapPath} does not exist.`); + process.exit(1); +} + +let sourceMap; +try { + sourceMap = JSON.parse(fs.readFileSync(sourceMapPath, 'utf8')); +} catch (e) { + console.log(`Sourcemap at ${sourceMapPath} was unable to be read.`, e); + process.exist(1); +} + +if (typeof sourceMap.debugId === 'string' && sourceMap.debugId.length > 0) { + console.log(sourceMap.debugId); +} else if (typeof sourceMap.debug_id === 'string' && sourceMap.debug_id.length > 0) { + console.log(sourceMap.debug_id); +} else { + console.log(`${sourceMapPath} does not contain 'debugId' nor 'debug_id'.`); + process.exist(1); +} diff --git a/scripts/sentry-debugid-injection-snippet.js b/scripts/sentry-debugid-injection-snippet.js new file mode 100644 index 0000000000..83a309bacd --- /dev/null +++ b/scripts/sentry-debugid-injection-snippet.js @@ -0,0 +1,18 @@ +// This is non minified version the debug id injection snippet used in the Metro plugin. +var _sentryDebugIds = {}; +var _sentryDebugIdIdentifier = ''; + +if (typeof _sentryDebugIds === 'undefined') { + _sentryDebugIds = {}; +} + +try { + var stack = new Error().stack; + if (stack) { + _sentryDebugIds[stack] = '__SENTRY_DEBUG_ID__'; + // eslint-disable-next-line no-unused-vars + _sentryDebugIdIdentifier = 'sentry-dbid-__SENTRY_DEBUG_ID__'; + } +} catch (e) { + /**/ +} diff --git a/scripts/sentry-xcode-debug-files.sh b/scripts/sentry-xcode-debug-files.sh new file mode 100755 index 0000000000..f2bae94741 --- /dev/null +++ b/scripts/sentry-xcode-debug-files.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# Upload Debug Symbols to Sentry Xcode Build Phase +# PWD=ios + +# print commands before executing them and stop on first error +set -x -e + +# load envs if loader file exists (since rn 0.68) +WITH_ENVIRONMENT="../node_modules/react-native/scripts/xcode/with-environment.sh" +if [ -f "$WITH_ENVIRONMENT" ]; then + . "$WITH_ENVIRONMENT" +fi + +[ -z "$SENTRY_PROPERTIES" ] && export SENTRY_PROPERTIES=sentry.properties +[ -z "$SENTRY_CLI_EXECUTABLE" ] && SENTRY_CLI_EXECUTABLE="../node_modules/@sentry/cli/bin/sentry-cli" + +[[ $SENTRY_INCLUDE_NATIVE_SOURCES == "true" ]] && INCLUDE_SOURCES_FLAG="--include-sources" || INCLUDE_SOURCES_FLAG="" + +EXTRA_ARGS="$SENTRY_CLI_EXTRA_ARGS $SENTRY_CLI_DEBUG_FILES_UPLOAD_EXTRA_ARGS $INCLUDE_SOURCES_FLAG" + +UPLOAD_DEBUG_FILES="\"$SENTRY_CLI_EXECUTABLE\" debug-files upload $EXTRA_ARGS \"$DWARF_DSYM_FOLDER_PATH\"" +/bin/sh -c "$UPLOAD_DEBUG_FILES" diff --git a/scripts/sentry-xcode.sh b/scripts/sentry-xcode.sh new file mode 100755 index 0000000000..abb5238f75 --- /dev/null +++ b/scripts/sentry-xcode.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# Sentry Bundle React Native code and images +# PWD=ios + +# print commands before executing them and stop on first error +set -x -e + +# WITH_ENVIRONMENT is executed by React Native + +[ -z "$SENTRY_PROPERTIES" ] && export SENTRY_PROPERTIES=sentry.properties +[ -z "$SOURCEMAP_FILE" ] && export SOURCEMAP_FILE="$DERIVED_FILE_DIR/main.jsbundle.map" +[ -z "$SENTRY_CLI_EXECUTABLE" ] && SENTRY_CLI_EXECUTABLE="../node_modules/@sentry/cli/bin/sentry-cli" + +REACT_NATIVE_XCODE=$1 + +[[ "$AUTO_RELEASE" != true ]] && [[ -z "$BUNDLE_COMMAND" || "$BUNDLE_COMMAND" != "ram-bundle" ]] && NO_AUTO_RELEASE="--no-auto-release" +ARGS="$NO_AUTO_RELEASE $SENTRY_CLI_EXTRA_ARGS $SENTRY_CLI_RN_XCODE_EXTRA_ARGS" + +BUNDLE_REACT_NATIVE="\"$SENTRY_CLI_EXECUTABLE\" react-native xcode $ARGS \"$REACT_NATIVE_XCODE\"" + +/bin/sh -c "$BUNDLE_REACT_NATIVE" + +[ -z "$SENTRY_COLLECT_MODULES" ] && SENTRY_COLLECT_MODULES="../../scripts/collect-modules.sh" + +if [ -f "$SENTRY_COLLECT_MODULES" ]; then + /bin/sh "$SENTRY_COLLECT_MODULES" +fi diff --git a/sentry.gradle b/sentry.gradle index 5dba7912df..b7fea4e3d1 100644 --- a/sentry.gradle +++ b/sentry.gradle @@ -39,6 +39,8 @@ gradle.projectsEvaluated { def shouldCleanUp def sourcemapOutput def bundleOutput + def packagerSourcemapOutput + def bundleCommand def props = bundleTask.getProperties() def reactRoot = props.get("workingDir") if (reactRoot == null) { @@ -47,7 +49,7 @@ gradle.projectsEvaluated { def modulesOutput = "$reactRoot/android/app/src/main/assets/modules.json" def modulesTask = null - (shouldCleanUp, bundleOutput, sourcemapOutput) = forceSourceMapOutputFromBundleTask(bundleTask) + (shouldCleanUp, bundleOutput, sourcemapOutput, packagerSourcemapOutput, bundleCommand) = forceSourceMapOutputFromBundleTask(bundleTask) // Lets leave this here if we need to debug // println bundleTask.properties @@ -94,71 +96,107 @@ gradle.projectsEvaluated { try { tasks.named(nameCliTask); return } catch (Exception e) {} /** Upload source map file to the sentry server via CLI call. */ - def cliTask = tasks.create(nameCliTask, Exec) { - description = "upload debug symbols to sentry" + def cliTask = tasks.create(nameCliTask) { + description = "upload source maps to Sentry" group = 'sentry.io' - workingDir reactRoot - - def propertiesFile = config.sentryProperties - ? config.sentryProperties - : "$reactRoot/android/sentry.properties" - - if (config.flavorAware) { - propertiesFile = "$reactRoot/android/sentry-${variant}.properties" - project.logger.info("For $variant using: $propertiesFile") - } else { - environment("SENTRY_PROPERTIES", propertiesFile) + def extraArgs = [] + + def sentryPackage = resolveSentryReactNativeSDKPath(reactRoot) + def copyDebugIdScript = config.copyDebugIdScript + ? file(config.copyDebugIdScript).getAbsolutePath() + : "$sentryPackage/scripts/copy-debugid.js" + def hasSourceMapDebugIdScript = config.hasSourceMapDebugIdScript + ? file(config.hasSourceMapDebugIdScript).getAbsolutePath() + : "$sentryPackage/scripts/has-sourcemap-debugid.js" + + doFirst { + // Copy Debug ID from packager source map to Hermes composed source map + exec { + def args = ["node", + copyDebugIdScript, + packagerSourcemapOutput, + sourcemapOutput] + def osCompatibilityCopyCommand = Os.isFamily(Os.FAMILY_WINDOWS) ? ['cmd', '/c'] : [] + commandLine(*osCompatibilityCopyCommand, *args) + } + + // Add release and dist for backward compatibility if no Debug ID detected in output soruce map + def process = ["node", hasSourceMapDebugIdScript, sourcemapOutput].execute(null, new File("$reactRoot")) + project.logger.lifecycle("Check generated source map for Debug ID: ${process.text}") + def notIncludeRelease = "$bundleCommand" == "bundle" && process.exitValue() == 0 + def not = notIncludeRelease ? 'not ' : '' + project.logger.lifecycle("Sentry Source Maps upload will ${not}include the release name and dist.") + extraArgs.addAll(notIncludeRelease ? [] : [ + "--release", releaseName, + "--dist", versionCode + ]) } - Properties sentryProps = new Properties() - try { - sentryProps.load(new FileInputStream(propertiesFile)) - } catch (FileNotFoundException e) { - project.logger.info("file not found '$propertiesFile' for '$variant'") + doLast { + exec { + workingDir reactRoot + + def propertiesFile = config.sentryProperties + ? config.sentryProperties + : "$reactRoot/android/sentry.properties" + + if (config.flavorAware) { + propertiesFile = "$reactRoot/android/sentry-${variant}.properties" + project.logger.info("For $variant using: $propertiesFile") + } else { + environment("SENTRY_PROPERTIES", propertiesFile) + } + + Properties sentryProps = new Properties() + try { + sentryProps.load(new FileInputStream(propertiesFile)) + } catch (FileNotFoundException e) { + project.logger.info("file not found '$propertiesFile' for '$variant'") + } + + def resolvedCliPackage = null + try { + resolvedCliPackage = new File(["node", "--print", "require.resolve('@sentry/cli/package.json')"].execute(null, rootDir).text.trim()).getParentFile(); + } catch (Throwable ignored) {} + def cliPackage = resolvedCliPackage != null && resolvedCliPackage.exists() ? resolvedCliPackage.getAbsolutePath() : "$reactRoot/node_modules/@sentry/cli" + def cliExecutable = sentryProps.get("cli.executable", "$cliPackage/bin/sentry-cli") + + // fix path separator for Windows + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + cliExecutable = cliExecutable.replaceAll("/", "\\\\") + } + + // + // based on: + // https://github.com/getsentry/sentry-cli/blob/master/src/commands/react_native_gradle.rs + // + def args = [cliExecutable] + + args.addAll(!config.logLevel ? [] : [ + "--log-level", config.logLevel // control verbosity of the output + ]) + args.addAll(!config.flavorAware ? [] : [ + "--url", sentryProps.get("defaults.url"), + "--auth-token", sentryProps.get("auth.token") + ]) + args.addAll(["react-native", "gradle", + "--bundle", bundleOutput, // The path to a bundle that should be uploaded. + "--sourcemap", sourcemapOutput // The path to a sourcemap that should be uploaded. + ]) + args.addAll(!config.flavorAware ? [] : [ + "--org", sentryProps.get("defaults.org"), + "--project", sentryProps.get("defaults.project") + ]) + + args.addAll(extraArgs) + + project.logger.lifecycle("Sentry-CLI arguments: ${args}") + def osCompatibility = Os.isFamily(Os.FAMILY_WINDOWS) ? ['cmd', '/c', 'node'] : [] + commandLine(*osCompatibility, *args) + } } - def resolvedCliPackage = null - try { - resolvedCliPackage = new File(["node", "--print", "require.resolve('@sentry/cli/package.json')"].execute(null, rootDir).text.trim()).getParentFile(); - } catch (Throwable ignored) {} - def cliPackage = resolvedCliPackage != null && resolvedCliPackage.exists() ? resolvedCliPackage.getAbsolutePath() : "$reactRoot/node_modules/@sentry/cli" - def cliExecutable = sentryProps.get("cli.executable", "$cliPackage/bin/sentry-cli") - - // fix path separator for Windows - if (Os.isFamily(Os.FAMILY_WINDOWS)) { - cliExecutable = cliExecutable.replaceAll("/", "\\\\") - } - - // - // based on: - // https://github.com/getsentry/sentry-cli/blob/master/src/commands/react_native_gradle.rs - // - def args = [cliExecutable] - - args.addAll(!config.logLevel ? [] : [ - "--log-level", config.logLevel // control verbosity of the output - ]) - args.addAll(!config.flavorAware ? [] : [ - "--url", sentryProps.get("defaults.url"), - "--auth-token", sentryProps.get("auth.token") - ]) - args.addAll(["react-native", "gradle", - "--bundle", bundleOutput, // The path to a bundle that should be uploaded. - "--sourcemap", sourcemapOutput, // The path to a sourcemap that should be uploaded. - "--release", releaseName, // The name of the release to publish. - "--dist", versionCode - ]) - args.addAll(!config.flavorAware ? [] : [ - "--org", sentryProps.get("defaults.org"), - "--project", sentryProps.get("defaults.project") - ]) - - project.logger.info("Sentry-CLI arguments: ${args}") - - def osCompatibility = Os.isFamily(Os.FAMILY_WINDOWS) ? ['cmd', '/c', 'node'] : [] - commandLine(*osCompatibility, *args) - enabled true } @@ -168,11 +206,7 @@ gradle.projectsEvaluated { workingDir reactRoot - def resolvedSentryPath = null - try { - resolvedSentryPath = new File(["node", "--print", "require.resolve('@sentry/react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile(); - } catch (Throwable ignored) {} // if the resolve fails we fallback to the default path - def sentryPackage = resolvedSentryPath != null && resolvedSentryPath.exists() ? resolvedSentryPath.getAbsolutePath() : "$reactRoot/node_modules/@sentry/react-native" + def sentryPackage = resolveSentryReactNativeSDKPath(reactRoot) def collectModulesScript = config.collectModulesScript ? file(config.collectModulesScript).getAbsolutePath() @@ -248,6 +282,15 @@ gradle.projectsEvaluated { } } +def resolveSentryReactNativeSDKPath(reactRoot) { + def resolvedSentryPath = null + try { + resolvedSentryPath = new File(["node", "--print", "require.resolve('@sentry/react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile(); + } catch (Throwable ignored) {} // if the resolve fails we fallback to the default path + def sentryPackage = resolvedSentryPath != null && resolvedSentryPath.exists() ? resolvedSentryPath.getAbsolutePath() : "$reactRoot/node_modules/@sentry/react-native" + return sentryPackage +} + /** Compose lookup map of build variants - to - outputs. */ def extractReleasesInfo() { def releases = [:] @@ -275,6 +318,8 @@ def extractReleasesInfo() { static extractBundleTaskArgumentsLegacy(cmdArgs, Project project) { def bundleOutput = null def sourcemapOutput = null + def packagerSourcemapOutput = null + // packagerBundleOutput doesn't exist, because packager output is overwritten by Hermes cmdArgs.eachWithIndex { String arg, int i -> if (arg == "--bundle-output") { @@ -282,6 +327,7 @@ static extractBundleTaskArgumentsLegacy(cmdArgs, Project project) { project.logger.info("--bundle-output: `${bundleOutput}`") } else if (arg == "--sourcemap-output") { sourcemapOutput = cmdArgs[i + 1] + packagerSourcemapOutput = sourcemapOutput project.logger.info("--sourcemap-output param: `${sourcemapOutput}`") } } @@ -313,7 +359,11 @@ static extractBundleTaskArgumentsLegacy(cmdArgs, Project project) { } } - return [bundleOutput, sourcemapOutput] + // get the current bundle command, if not peresent use default plain "bundle" + // we use this later to decide how to upload source maps + def bundleCommand = project.ext.react.get("bundleCommand", "bundle") + + return [bundleOutput, sourcemapOutput, packagerSourcemapOutput, bundleCommand] } /** Extract bundle and sourcemap paths from bundle task props. @@ -328,12 +378,15 @@ static extractBundleTaskArgumentsRN71AndAbove(bundleTask, logger) { return [null, null] } + def bundleCommand = props.bundleCommand.get() def bundleFile = new File(props.jsBundleDir.get().asFile.absolutePath, bundleAssetName) def outputSourceMap = new File(props.jsSourceMapsDir.get().asFile.absolutePath, "${bundleAssetName}.map") + def packagerOutputSourceMap = new File(props.jsIntermediateSourceMapsDir.get().asFile.absolutePath, "${bundleAssetName}.packager.map") logger.info("bundleFile: `${bundleFile}`") logger.info("outputSourceMap: `${outputSourceMap}`") - return [bundleFile, outputSourceMap] + logger.info("packagerOutputSourceMap: `${packagerOutputSourceMap}`") + return [bundleFile, outputSourceMap, packagerOutputSourceMap, bundleCommand] } /** Force Bundle task to produce sourcemap files if they are not pre-configured by user yet. */ @@ -344,10 +397,12 @@ def forceSourceMapOutputFromBundleTask(bundleTask) { def shouldCleanUp = false def bundleOutput = null def sourcemapOutput = null + def packagerSourcemapOutput = null + def bundleCommand = null - (bundleOutput, sourcemapOutput) = extractBundleTaskArgumentsRN71AndAbove(bundleTask, logger) + (bundleOutput, sourcemapOutput, packagerSourcemapOutput, bundleCommand) = extractBundleTaskArgumentsRN71AndAbove(bundleTask, logger) if (bundleOutput == null) { - (bundleOutput, sourcemapOutput) = extractBundleTaskArgumentsLegacy(cmdArgs, project) + (bundleOutput, sourcemapOutput, packagerSourcemapOutput, bundleCommand) = extractBundleTaskArgumentsLegacy(cmdArgs, project) } if (sourcemapOutput == null) { @@ -366,7 +421,7 @@ def forceSourceMapOutputFromBundleTask(bundleTask) { project.logger.info("Info: used pre-configured source map files: ${sourcemapOutput}") } - return [shouldCleanUp, bundleOutput, sourcemapOutput] + return [shouldCleanUp, bundleOutput, sourcemapOutput, packagerSourcemapOutput, bundleCommand] } /** compose array with one item - current build flavor name */ diff --git a/src/js/tools/sentryMetroSerializer.ts b/src/js/tools/sentryMetroSerializer.ts new file mode 100644 index 0000000000..43559b2b41 --- /dev/null +++ b/src/js/tools/sentryMetroSerializer.ts @@ -0,0 +1,171 @@ +import * as crypto from 'crypto'; +import type { MixedOutput, Module } from 'metro'; +import CountingSet from 'metro/src/lib/CountingSet'; +import * as countLines from 'metro/src/lib/countLines'; + +import type { Bundle, MetroSerializer, MetroSerializerOutput, SerializedBundle, VirtualJSOutput } from './utils'; +import { createDebugIdSnippet, determineDebugIdFromBundleSource, stringToUUID } from './utils'; +import { createDefaultMetroSerializer } from './vendor/metro/utils'; + +type SourceMap = Record; + +const DEBUG_ID_PLACE_HOLDER = '__debug_id_place_holder__'; +const DEBUG_ID_MODULE_PATH = '__debugid__'; +const PRELUDE_MODULE_PATH = '__prelude__'; +const SOURCE_MAP_COMMENT = '//# sourceMappingURL='; +const DEBUG_ID_COMMENT = '//# debugId='; + +/** + * Creates a Metro serializer that adds Debug ID module to the plain bundle. + * The Debug ID module is a virtual module that provides a debug ID in runtime. + * + * RAM Bundles do not support custom serializers. + */ +export const createSentryMetroSerializer = (customSerializer?: MetroSerializer): MetroSerializer => { + const serializer = customSerializer || createDefaultMetroSerializer(); + return async function (entryPoint, preModules, graph, options) { + if (graph.transformOptions.hot) { + return serializer(entryPoint, preModules, graph, options); + } + + const debugIdModuleExists = preModules.findIndex(module => module.path === DEBUG_ID_MODULE_PATH) != -1; + if (debugIdModuleExists) { + // eslint-disable-next-line no-console + console.warn('Debug ID module found. Skipping Sentry Debug ID module...'); + return serializer(entryPoint, preModules, graph, options); + } + + const debugIdModule = createDebugIdModule(DEBUG_ID_PLACE_HOLDER); + options.sentryBundleCallback = createSentryBundleCallback(debugIdModule); + const modifiedPreModules = addDebugIdModule(preModules, debugIdModule); + + // Run wrapped serializer + const serializerResult = serializer(entryPoint, modifiedPreModules, graph, options); + const { code: bundleCode, map: bundleMapString } = await extractSerializerResult(serializerResult); + + // Add debug id comment to the bundle + const debugId = determineDebugIdFromBundleSource(bundleCode); + if (!debugId) { + throw new Error( + 'Debug ID was not found in the bundle. Call `options.sentryBundleCallback` if you are using a custom serializer.', + ); + } + // Only print debug id for command line builds => not hot reload from dev server + // eslint-disable-next-line no-console + console.log('info ' + `Bundle Debug ID: ${debugId}`); + + const debugIdComment = `${DEBUG_ID_COMMENT}${debugId}`; + const indexOfSourceMapComment = bundleCode.lastIndexOf(SOURCE_MAP_COMMENT); + const bundleCodeWithDebugId = + indexOfSourceMapComment === -1 + ? // If source map comment is missing lets just add the debug id comment + `${bundleCode}\n${debugIdComment}` + : // If source map comment is present lets add the debug id comment before it + `${bundleCode.substring(0, indexOfSourceMapComment) + debugIdComment}\n${bundleCode.substring( + indexOfSourceMapComment, + )}`; + + const bundleMap: SourceMap = JSON.parse(bundleMapString); + // For now we write both fields until we know what will become the standard - if ever. + bundleMap['debug_id'] = debugId; + bundleMap['debugId'] = debugId; + + return { + code: bundleCodeWithDebugId, + map: JSON.stringify(bundleMap), + }; + }; +}; + +/** + * This function is expected to be called after serializer creates the final bundle object + * and before the source maps are generated. + * + * It injects a debug ID into the bundle and returns the modified bundle. + * + * Access it via `options.sentryBundleCallback` in your custom serializer. + */ +function createSentryBundleCallback(debugIdModule: Module & { setSource: (code: string) => void }) { + return (bundle: Bundle) => { + const debugId = calculateDebugId(bundle); + debugIdModule.setSource(injectDebugId(debugIdModule.getSource().toString(), debugId)); + bundle.pre = injectDebugId(bundle.pre, debugId); + return bundle; + }; +} + +function addDebugIdModule( + preModules: readonly Module[], + debugIdModule: Module, +): readonly Module[] { + const modifiedPreModules = [...preModules]; + if ( + modifiedPreModules.length > 0 && + modifiedPreModules[0] !== undefined && + modifiedPreModules[0].path === PRELUDE_MODULE_PATH + ) { + // prelude module must be first as it measures the bundle startup time + modifiedPreModules.unshift(preModules[0]); + modifiedPreModules[1] = debugIdModule; + } else { + modifiedPreModules.unshift(debugIdModule); + } + return modifiedPreModules; +} + +async function extractSerializerResult(serializerResult: MetroSerializerOutput): Promise { + if (typeof serializerResult === 'string') { + return { code: serializerResult, map: '{}' }; + } + + if ('map' in serializerResult) { + return { code: serializerResult.code, map: serializerResult.map }; + } + + const awaitedResult = await serializerResult; + if (typeof awaitedResult === 'string') { + return { code: awaitedResult, map: '{}' }; + } + + return { code: awaitedResult.code, map: awaitedResult.map }; +} + +function createDebugIdModule(debugId: string): Module & { setSource: (code: string) => void } { + let debugIdCode = createDebugIdSnippet(debugId); + + return { + setSource: (code: string) => { + debugIdCode = code; + }, + dependencies: new Map(), + getSource: () => Buffer.from(debugIdCode), + inverseDependencies: new CountingSet(), + path: DEBUG_ID_MODULE_PATH, + output: [ + { + type: 'js/script/virtual', + data: { + code: debugIdCode, + lineCount: countLines(debugIdCode), + map: [], + }, + }, + ], + }; +} + +function calculateDebugId(bundle: Bundle): string { + const hash = crypto.createHash('md5'); + hash.update(bundle.pre); + for (const [, code] of bundle.modules) { + hash.update(code); + } + hash.update(bundle.post); + + const debugId = stringToUUID(hash.digest('hex')); + return debugId; +} + +function injectDebugId(code: string, debugId: string): string { + return code.replace(new RegExp(DEBUG_ID_PLACE_HOLDER, 'g'), debugId); +} diff --git a/src/js/tools/utils.ts b/src/js/tools/utils.ts new file mode 100644 index 0000000000..42778f75a5 --- /dev/null +++ b/src/js/tools/utils.ts @@ -0,0 +1,76 @@ +import * as crypto from 'crypto'; +import type { Module, ReadOnlyGraph, SerializerOptions } from 'metro'; + +// Variant of MixedOutput +// https://github.com/facebook/metro/blob/9b85f83c9cc837d8cd897aa7723be7da5b296067/packages/metro/src/DeltaBundler/types.flow.js#L21 +export type VirtualJSOutput = { + type: 'js/script/virtual'; + data: { + code: string; + lineCount: number; + map: []; + }; +}; + +export type Bundle = { + modules: Array<[id: number, code: string]>; + post: string; + pre: string; +}; + +export type SentryMetroSerializerOptionsExtras = { + sentryBundleCallback?: (bundle: Bundle) => Bundle; +}; + +export type SerializedBundle = { code: string; map: string }; + +export type MetroSerializerOutput = string | SerializedBundle | Promise; + +export type MetroSerializer = ( + entryPoint: string, + preModules: ReadonlyArray, + graph: ReadOnlyGraph, + options: SerializerOptions & SentryMetroSerializerOptionsExtras, +) => MetroSerializerOutput; + +/** + * Returns minified Debug ID code snippet. + */ +export function createDebugIdSnippet(debugId: string): string { + return `var _sentryDebugIds={},_sentryDebugIdIdentifier="";void 0===_sentryDebugIds&&(_sentryDebugIds={});try{var stack=(new Error).stack;stack&&(_sentryDebugIds[stack]="${debugId}",_sentryDebugIdIdentifier="sentry-dbid-${debugId}")}catch(e){}`; +} + +/** + * Deterministically hashes a string and turns the hash into a uuid. + * + * https://github.com/getsentry/sentry-javascript-bundler-plugins/blob/58271f1af2ade6b3e64d393d70376ae53bc5bd2f/packages/bundler-plugin-core/src/utils.ts#L174 + */ +export function stringToUUID(str: string): string { + const md5sum = crypto.createHash('md5'); + md5sum.update(str); + const md5Hash = md5sum.digest('hex'); + + // Position 16 is fixed to either 8, 9, a, or b in the uuid v4 spec (10xx in binary) + // RFC 4122 section 4.4 + const v4variant = ['8', '9', 'a', 'b'][md5Hash.substring(16, 17).charCodeAt(0) % 4] as string; + + return `${md5Hash.substring(0, 8)}-${md5Hash.substring(8, 12)}-4${md5Hash.substring( + 13, + 16, + )}-${v4variant}${md5Hash.substring(17, 20)}-${md5Hash.substring(20)}`.toLowerCase(); +} + +/** + * Looks for a particular string pattern (`sdbid-[debug ID]`) in the bundle + * source and extracts the bundle's debug ID from it. + * + * The string pattern is injected via the debug ID injection snipped. + * + * https://github.com/getsentry/sentry-javascript-bundler-plugins/blob/40f918458ed449d8b3eabaf64d13c08218213f65/packages/bundler-plugin-core/src/debug-id-upload.ts#L293-L294 + */ +export function determineDebugIdFromBundleSource(code: string): string | undefined { + const match = code.match( + /sentry-dbid-([0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12})/, + ); + return match ? match[1] : undefined; +} diff --git a/src/js/tools/vendor/metro/metro.d.ts b/src/js/tools/vendor/metro/metro.d.ts new file mode 100644 index 0000000000..8a0f34c839 --- /dev/null +++ b/src/js/tools/vendor/metro/metro.d.ts @@ -0,0 +1,71 @@ +// Vendored / modified from @facebook/metro + +// https://github.com/facebook/metro/commit/9b85f83c9cc837d8cd897aa7723be7da5b296067 + +// MIT License + +// Copyright (c) Meta Platforms, Inc. and affiliates. + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +declare module 'metro/src/DeltaBundler/Serializers/baseJSBundle' { + // https://github.com/facebook/metro/blob/9b85f83c9cc837d8cd897aa7723be7da5b296067/packages/metro/src/DeltaBundler/Serializers/baseJSBundle.js#L25 + const baseJSBundle: ( + entryPoint: string, + preModules: ReadonlyArray, + graph: ReadOnlyGraph, + options: SerializerOptions, + ) => { + modules: [number, string][]; + post: string; + pre: string; + }; + export = baseJSBundle; +} + +declare module 'metro/src/lib/bundleToString' { + // https://github.com/facebook/metro/blob/9b85f83c9cc837d8cd897aa7723be7da5b296067/packages/metro/src/lib/bundleToString.js#L22 + const baseJSBundle: (bundle: { modules: [number, string][]; post: string; pre: string }) => { + code: string; + metadata: BundleMetadata; + }; + + export = baseJSBundle; +} + +declare module 'metro/src/lib/countLines' { + // https://github.com/facebook/metro/blob/9b85f83c9cc837d8cd897aa7723be7da5b296067/packages/metro/src/lib/countLines.js#L16 + const countLines: (code: string) => number; + export = countLines; +} + +declare module 'metro/src/DeltaBundler/Serializers/sourceMapString' { + import type { MixedOutput, Module } from 'metro'; + + // https://github.com/facebook/metro/blob/9b85f83c9cc837d8cd897aa7723be7da5b296067/packages/metro/src/DeltaBundler/Serializers/sourceMapString.js#L19 + const sourceMapString: ( + bundle: Module[], + options: { + excludeSource?: boolean; + processModuleFilter?: (module: Module) => boolean; + shouldAddToIgnoreList?: (module: Module) => boolean; + }, + ) => string; + export = sourceMapString; +} diff --git a/src/js/tools/vendor/metro/utils.ts b/src/js/tools/vendor/metro/utils.ts new file mode 100644 index 0000000000..7f57aa540b --- /dev/null +++ b/src/js/tools/vendor/metro/utils.ts @@ -0,0 +1,85 @@ +// Vendored / modified from @facebook/metro + +// https://github.com/facebook/metro/commit/9b85f83c9cc837d8cd897aa7723be7da5b296067 + +// MIT License + +// Copyright (c) Meta Platforms, Inc. and affiliates. + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import type { MixedOutput, Module, ReadOnlyGraph } from 'metro'; +import * as baseJSBundle from 'metro/src/DeltaBundler/Serializers/baseJSBundle'; +import * as sourceMapString from 'metro/src/DeltaBundler/Serializers/sourceMapString'; +import * as bundleToString from 'metro/src/lib/bundleToString'; + +import type { MetroSerializer } from '../../utils'; + +/** + * This function ensures that modules in source maps are sorted in the same + * order as in a plain JS bundle. + * + * https://github.com/facebook/metro/blob/9b85f83c9cc837d8cd897aa7723be7da5b296067/packages/metro/src/Server.js#L984 + */ +export const getSortedModules = ( + graph: ReadOnlyGraph, + { + createModuleId, + }: { + createModuleId: (file: string) => number; + }, +): readonly Module[] => { + const modules = [...graph.dependencies.values()]; + // Sort by IDs + return modules.sort( + (a: Module, b: Module) => createModuleId(a.path) - createModuleId(b.path), + ); +}; + +/** + * Creates the default Metro plain bundle serializer. + * Because Metro exports only the intermediate serializer functions, we need to + * assemble the final serializer ourselves. We have to work with the modules the same as Metro does + * to avoid unexpected changes in the final bundle. + * + * This is used when the user does not provide a custom serializer. + * + * https://github.com/facebook/metro/blob/9b85f83c9cc837d8cd897aa7723be7da5b296067/packages/metro/src/Server.js#L244-L277 + */ +export const createDefaultMetroSerializer = (): MetroSerializer => { + return (entryPoint, preModules, graph, options) => { + // baseJSBundle assigns IDs to modules in a consistent order + let bundle = baseJSBundle(entryPoint, preModules, graph, options); + if (options.sentryBundleCallback && !graph.transformOptions.hot) { + bundle = options.sentryBundleCallback(bundle); + } + const { code } = bundleToString(bundle); + if (graph.transformOptions.hot) { + // Hot means running in dev server, sourcemaps are generated on demand + return code; + } + + // Always generate source maps, can't use Sentry without source maps + const map = sourceMapString([...preModules, ...getSortedModules(graph, options)], { + processModuleFilter: options.processModuleFilter, + shouldAddToIgnoreList: options.shouldAddToIgnoreList, + }); + return { code, map }; + }; +}; diff --git a/test/react-native/rn.patch.metro.config.js b/test/react-native/rn.patch.metro.config.js new file mode 100755 index 0000000000..d7e2d1db39 --- /dev/null +++ b/test/react-native/rn.patch.metro.config.js @@ -0,0 +1,37 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const { argv } = require('process'); + +const parseArgs = require('minimist'); +const { logger } = require('@sentry/utils'); +logger.enable(); + +const args = parseArgs(argv.slice(2)); +if (!args.path) { + throw new Error('Missing --path'); +} + +logger.info('Patching Metro config: ', args.path); + +const configFilePath = args.path; + +const importSerializer = + "const {createSentryMetroSerializer} = require('@sentry/react-native/dist/js/tools/sentryMetroSerializer');"; +const serializerValue = 'serializer: { customSerializer: createSentryMetroSerializer(), },'; +const enterSerializerBefore = '};'; + +let config = fs.readFileSync(configFilePath, 'utf8').split('\n'); + +const isPatched = config.includes(line => line.includes(importSerializer)); +if (!isPatched) { + config = [importSerializer, ...config]; + const lineIndex = config.findIndex(line => line.includes(enterSerializerBefore)); + const lineParsed = config[lineIndex].split(enterSerializerBefore); + lineParsed.push(serializerValue, enterSerializerBefore); + config[lineIndex] = lineParsed.join(''); + fs.writeFileSync(configFilePath, config.join('\n'), 'utf8'); + logger.info('Patched Metro config successfully!'); +} else { + logger.info('Metro config already patched!'); +} diff --git a/test/react-native/rn.patch.xcode.js b/test/react-native/rn.patch.xcode.js index 54436b92e2..7571adacdd 100755 --- a/test/react-native/rn.patch.xcode.js +++ b/test/react-native/rn.patch.xcode.js @@ -25,39 +25,26 @@ let bundleScript; let bundleScriptRegex; let bundlePatchRegex; const symbolsScript = ` -export SENTRY_PROPERTIES=sentry.properties -../node_modules/@sentry/cli/bin/sentry-cli debug-files upload --force-foreground "$DWARF_DSYM_FOLDER_PATH" +/bin/sh ../node_modules/@sentry/react-native/scripts/sentry-xcode-debug-files.sh `; const symbolsPatchRegex = /sentry-cli\s+(upload-dsym|debug-files upload)/; if (semver.satisfies(args['rn-version'], `< ${newBundleScriptRNVersion}`)) { logger.info('Applying old bundle script patch'); bundleScript = ` -export SENTRY_PROPERTIES=sentry.properties -export EXTRA_PACKAGER_ARGS="--sourcemap-output $DERIVED_FILE_DIR/main.jsbundle.map" -set -e - export NODE_BINARY=node -../node_modules/@sentry/cli/bin/sentry-cli react-native xcode --force-foreground ../node_modules/react-native/scripts/react-native-xcode.sh - -/bin/sh ../node_modules/@sentry/react-native/scripts/collect-modules.sh +export SENTRY_CLI_EXTRA_ARGS="--force-foreground" +../node_modules/@sentry/react-native/scripts/sentry-xcode.sh ../node_modules/react-native/scripts/react-native-xcode.sh `; bundleScriptRegex = /(packager|scripts)\/react-native-xcode\.sh\b/; bundlePatchRegex = /sentry-cli\s+react-native[\s-]xcode/; - - } else if (semver.satisfies(args['rn-version'], `>= ${newBundleScriptRNVersion}`)) { logger.info('Applying new bundle script patch'); bundleScript = ` -export SENTRY_PROPERTIES=sentry.properties -export EXTRA_PACKAGER_ARGS="--sourcemap-output $DERIVED_FILE_DIR/main.jsbundle.map" -set -e - +export SENTRY_CLI_EXTRA_ARGS="--force-foreground" WITH_ENVIRONMENT="../node_modules/react-native/scripts/xcode/with-environment.sh" REACT_NATIVE_XCODE="../node_modules/react-native/scripts/react-native-xcode.sh" -/bin/sh -c "$WITH_ENVIRONMENT \\"../node_modules/@sentry/cli/bin/sentry-cli react-native xcode --force-foreground $REACT_NATIVE_XCODE\\"" - -/bin/sh ../node_modules/@sentry/react-native/scripts/collect-modules.sh +/bin/sh -c "$WITH_ENVIRONMENT \\"/bin/sh ../node_modules/@sentry/react-native/scripts/sentry-xcode.sh $REACT_NATIVE_XCODE\\"" `; bundleScriptRegex = /\/scripts\/react-native-xcode\.sh/i; bundlePatchRegex = /sentry-cli\s+react-native\s+xcode/i; diff --git a/test/tools/fixtures/bundleWithPrelude.js.fixture b/test/tools/fixtures/bundleWithPrelude.js.fixture new file mode 100644 index 0000000000..63b1939141 --- /dev/null +++ b/test/tools/fixtures/bundleWithPrelude.js.fixture @@ -0,0 +1,4 @@ +__mock_prelude__ +var _sentryDebugIds={},_sentryDebugIdIdentifier="";void 0===_sentryDebugIds&&(_sentryDebugIds={});try{var stack=(new Error).stack;stack&&(_sentryDebugIds[stack]="d2b7a793-6dd9-4802-9250-9cc0c54f8c23",_sentryDebugIdIdentifier="sentry-dbid-d2b7a793-6dd9-4802-9250-9cc0c54f8c23")}catch(e){} +__mock_index_js__ +//# debugId=d2b7a793-6dd9-4802-9250-9cc0c54f8c23 \ No newline at end of file diff --git a/test/tools/fixtures/bundleWithPrelude.js.fixture.map b/test/tools/fixtures/bundleWithPrelude.js.fixture.map new file mode 100644 index 0000000000..2f1555724a --- /dev/null +++ b/test/tools/fixtures/bundleWithPrelude.js.fixture.map @@ -0,0 +1 @@ +{"version":3,"sources":["__prelude__","__debugid__","index.js"],"sourcesContent":["__mock_prelude__","var _sentryDebugIds={},_sentryDebugIdIdentifier=\"\";void 0===_sentryDebugIds&&(_sentryDebugIds={});try{var stack=(new Error).stack;stack&&(_sentryDebugIds[stack]=\"d2b7a793-6dd9-4802-9250-9cc0c54f8c23\",_sentryDebugIdIdentifier=\"sentry-dbid-d2b7a793-6dd9-4802-9250-9cc0c54f8c23\")}catch(e){}","__mock_index_js__"],"names":[],"mappings":"","debug_id":"d2b7a793-6dd9-4802-9250-9cc0c54f8c23","debugId":"d2b7a793-6dd9-4802-9250-9cc0c54f8c23"} \ No newline at end of file diff --git a/test/tools/sentryMetroSerializer.test.ts b/test/tools/sentryMetroSerializer.test.ts new file mode 100644 index 0000000000..1d8920c0d2 --- /dev/null +++ b/test/tools/sentryMetroSerializer.test.ts @@ -0,0 +1,144 @@ +import * as fs from 'fs'; +import type { MixedOutput, Module } from 'metro'; +import CountingSet from 'metro/src/lib/CountingSet'; +import * as countLines from 'metro/src/lib/countLines'; +import { minify } from 'uglify-js'; + +import { createSentryMetroSerializer } from '../../src/js/tools/sentryMetroSerializer'; +import { type MetroSerializer, type VirtualJSOutput, createDebugIdSnippet } from '../../src/js/tools/utils'; + +describe('Sentry Metro Serializer', () => { + test('debug id minified code snippet is the same as in the original implementation', () => { + const original = fs.readFileSync(`${__dirname}/../../scripts/sentry-debugid-injection-snippet.js`, 'utf8'); + const minified = minify(original).code; + const snippet = createDebugIdSnippet('__SENTRY_DEBUG_ID__'); + expect(minified).toEqual(snippet); + }); + + test('generates bundle and source map with deterministic uuidv5 debug id', async () => { + const serializer = createSentryMetroSerializer(); + + const bundle = await serializer(...mockMinSerializerArgs()); + if (typeof bundle === 'string') { + fail('Expected bundle to be an object with a "code" property'); + } + + expect(bundle.code).toEqual( + 'var _sentryDebugIds={},_sentryDebugIdIdentifier="";void 0===_sentryDebugIds&&(_sentryDebugIds={});try{var stack=(new Error).stack;stack&&(_sentryDebugIds[stack]="901c00b1-71e1-40fc-b787-3fe0a7e23a92",_sentryDebugIdIdentifier="sentry-dbid-901c00b1-71e1-40fc-b787-3fe0a7e23a92")}catch(e){}\n//# debugId=901c00b1-71e1-40fc-b787-3fe0a7e23a92', + ); + expect(bundle.map).toEqual( + '{"version":3,"sources":["__debugid__"],"sourcesContent":["var _sentryDebugIds={},_sentryDebugIdIdentifier=\\"\\";void 0===_sentryDebugIds&&(_sentryDebugIds={});try{var stack=(new Error).stack;stack&&(_sentryDebugIds[stack]=\\"901c00b1-71e1-40fc-b787-3fe0a7e23a92\\",_sentryDebugIdIdentifier=\\"sentry-dbid-901c00b1-71e1-40fc-b787-3fe0a7e23a92\\")}catch(e){}"],"names":[],"mappings":"","debug_id":"901c00b1-71e1-40fc-b787-3fe0a7e23a92","debugId":"901c00b1-71e1-40fc-b787-3fe0a7e23a92"}', + ); + }); + + test('generated debug id is uuid v4 format', async () => { + const serializer = createSentryMetroSerializer(); + const bundle = await serializer(...mockMinSerializerArgs()); + const debugId = determineDebugIdFromBundleSource(typeof bundle === 'string' ? bundle : bundle.code); + expect(debugId).toEqual('901c00b1-71e1-40fc-b787-3fe0a7e23a92'); + }); + + test('adds debug id snipped after prelude module and before ', async () => { + const serializer = createSentryMetroSerializer(); + + const bundle = await serializer(...mockWithPreludeAndDepsSerializerArgs()); + if (typeof bundle === 'string') { + fail('Expected bundle to be an object with a "code" property'); + } + + expect(bundle.code).toEqual(fs.readFileSync(`${__dirname}/fixtures/bundleWithPrelude.js.fixture`, 'utf8')); + expect(bundle.map).toEqual(fs.readFileSync(`${__dirname}/fixtures/bundleWithPrelude.js.fixture.map`, 'utf8')); + }); +}); + +function mockMinSerializerArgs(): Parameters { + let modulesCounter = 0; + return [ + 'index.js', + [], + { + entryPoints: new Set(), + dependencies: new Map(), + transformOptions: { + hot: false, + dev: false, + minify: false, + type: 'script', + unstable_transformProfile: 'hermes-stable', + }, + }, + { + asyncRequireModulePath: 'asyncRequire', + createModuleId: (_filePath: string): number => modulesCounter++, + dev: false, + getRunModuleStatement: (_moduleId: string | number): string => '', + includeAsyncPaths: false, + modulesOnly: false, + processModuleFilter: (_module: Module) => true, + projectRoot: '/project/root', + runBeforeMainModule: [], + runModule: false, + serverRoot: '/server/root', + shouldAddToIgnoreList: (_module: Module) => false, + }, + ]; +} + +function mockWithPreludeAndDepsSerializerArgs(): Parameters { + const mockPreludeCode = '__mock_prelude__'; + const indexJsCode = '__mock_index_js__'; + const args = mockMinSerializerArgs(); + args[1] = [ + { + dependencies: new Map(), + getSource: () => Buffer.from(mockPreludeCode), + inverseDependencies: new CountingSet(), + path: '__prelude__', + output: [ + { + type: 'js/script/virtual', + data: { + code: mockPreludeCode, + lineCount: countLines(indexJsCode), + map: [], + }, + }, + ], + }, + ]; + + // @ts-expect-error - This is a mock + args[2].dependencies = [2]['dependencies']>new Map([ + [ + 'index.js', + >{ + dependencies: new Map(), + getSource: () => Buffer.from(indexJsCode), + inverseDependencies: new CountingSet(), + path: 'index.js', + output: [ + { + type: 'js/script/virtual', + data: { + code: indexJsCode, + lineCount: countLines(indexJsCode), + map: [], + }, + }, + ], + }, + ], + ]); + + return args; +} + +/** + * This function is on purpose not shared with the actual implementation. + */ +function determineDebugIdFromBundleSource(code: string): string | undefined { + const match = code.match( + /sentry-dbid-([0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12})/, + ); + return match?.[1]; +} diff --git a/tsconfig.build.json b/tsconfig.build.json index 4bbaba5ee1..ede2570695 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -2,7 +2,7 @@ "extends": "./node_modules/@sentry/typescript/tsconfig.json", "include": [ "src/js/*.ts", - "typings/*.d.ts" + "typings/react-native.d.ts" ], "exclude": [ "node_modules" diff --git a/tsconfig.build.tools.json b/tsconfig.build.tools.json index 30d5648e41..31b76b0ab7 100644 --- a/tsconfig.build.tools.json +++ b/tsconfig.build.tools.json @@ -1,7 +1,7 @@ { "extends": "./node_modules/@sentry/typescript/tsconfig.json", "include": [ - "src/js/tools/*.ts" + "src/js/tools/**/*.ts" ], "exclude": [ "node_modules" diff --git a/tsconfig.json b/tsconfig.json index f528b70582..34093603df 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig.build.json", "include": [ - "src/js/tools/*.ts", + "src/js/tools/**/*.ts", "src/js/*.ts", "src/js/*.tsx", "test/**/*.ts", diff --git a/yarn.lock b/yarn.lock index eb90711e84..991ac4c303 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2343,6 +2343,18 @@ resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397" integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw== +"@types/uglify-js@^3.17.2": + version "3.17.2" + resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.17.2.tgz#a2ba86fd524f6281a7655463338c546f845b29c3" + integrity sha512-9SjrHO54LINgC/6Ehr81NjAxAYvwEZqjUHLjJYvC4Nmr9jbLQCIZbWSvl4vXQkkmR1UAuaKDycau3O1kWGFyXQ== + dependencies: + source-map "^0.6.1" + +"@types/uuid@^9.0.4": + version "9.0.4" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.4.tgz#e884a59338da907bda8d2ed03e01c5c49d036f1c" + integrity sha512-zAuJWQflfx6dYJM62vna+Sn5aeSWhh3OB+wfUEACNcqUSc0AGc5JKl+ycL1vrH7frGTXhJchYjE1Hak8L819dA== + "@types/yargs-parser@*": version "15.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" @@ -5951,11 +5963,25 @@ metro-babel-transformer@0.76.7: hermes-parser "0.12.0" nullthrows "^1.1.1" +metro-babel-transformer@0.76.8: + version "0.76.8" + resolved "https://registry.yarnpkg.com/metro-babel-transformer/-/metro-babel-transformer-0.76.8.tgz#5efd1027353b36b73706164ef09c290dceac096a" + integrity sha512-Hh6PW34Ug/nShlBGxkwQJSgPGAzSJ9FwQXhUImkzdsDgVu6zj5bx258J8cJVSandjNoQ8nbaHK6CaHlnbZKbyA== + dependencies: + "@babel/core" "^7.20.0" + hermes-parser "0.12.0" + nullthrows "^1.1.1" + metro-cache-key@0.76.7: version "0.76.7" resolved "https://registry.yarnpkg.com/metro-cache-key/-/metro-cache-key-0.76.7.tgz#70913f43b92b313096673c37532edd07438cb325" integrity sha512-0pecoIzwsD/Whn/Qfa+SDMX2YyasV0ndbcgUFx7w1Ct2sLHClujdhQ4ik6mvQmsaOcnGkIyN0zcceMDjC2+BFQ== +metro-cache-key@0.76.8: + version "0.76.8" + resolved "https://registry.yarnpkg.com/metro-cache-key/-/metro-cache-key-0.76.8.tgz#8a0a5e991c06f56fcc584acadacb313c312bdc16" + integrity sha512-buKQ5xentPig9G6T37Ww/R/bC+/V1MA5xU/D8zjnhlelsrPG6w6LtHUS61ID3zZcMZqYaELWk5UIadIdDsaaLw== + metro-cache@0.76.7: version "0.76.7" resolved "https://registry.yarnpkg.com/metro-cache/-/metro-cache-0.76.7.tgz#e49e51423fa960df4eeff9760d131f03e003a9eb" @@ -5964,6 +5990,14 @@ metro-cache@0.76.7: metro-core "0.76.7" rimraf "^3.0.2" +metro-cache@0.76.8: + version "0.76.8" + resolved "https://registry.yarnpkg.com/metro-cache/-/metro-cache-0.76.8.tgz#296c1c189db2053b89735a8f33dbe82575f53661" + integrity sha512-QBJSJIVNH7Hc/Yo6br/U/qQDUpiUdRgZ2ZBJmvAbmAKp2XDzsapnMwK/3BGj8JNWJF7OLrqrYHsRsukSbUBpvQ== + dependencies: + metro-core "0.76.8" + rimraf "^3.0.2" + metro-config@0.76.7: version "0.76.7" resolved "https://registry.yarnpkg.com/metro-config/-/metro-config-0.76.7.tgz#f0fc171707523aa7d3a9311550872136880558c0" @@ -5977,6 +6011,19 @@ metro-config@0.76.7: metro-core "0.76.7" metro-runtime "0.76.7" +metro-config@0.76.8: + version "0.76.8" + resolved "https://registry.yarnpkg.com/metro-config/-/metro-config-0.76.8.tgz#20bd5397fcc6096f98d2a813a7cecb38b8af062d" + integrity sha512-SL1lfKB0qGHALcAk2zBqVgQZpazDYvYFGwCK1ikz0S6Y/CM2i2/HwuZN31kpX6z3mqjv/6KvlzaKoTb1otuSAA== + dependencies: + connect "^3.6.5" + cosmiconfig "^5.0.5" + jest-validate "^29.2.1" + metro "0.76.8" + metro-cache "0.76.8" + metro-core "0.76.8" + metro-runtime "0.76.8" + metro-core@0.76.7: version "0.76.7" resolved "https://registry.yarnpkg.com/metro-core/-/metro-core-0.76.7.tgz#5d2b8bac2cde801dc22666ad7be1336d1f021b61" @@ -5985,6 +6032,14 @@ metro-core@0.76.7: lodash.throttle "^4.1.1" metro-resolver "0.76.7" +metro-core@0.76.8: + version "0.76.8" + resolved "https://registry.yarnpkg.com/metro-core/-/metro-core-0.76.8.tgz#917c8157c63406cb223522835abb8e7c6291dcad" + integrity sha512-sl2QLFI3d1b1XUUGxwzw/KbaXXU/bvFYrSKz6Sg19AdYGWFyzsgZ1VISRIDf+HWm4R/TJXluhWMEkEtZuqi3qA== + dependencies: + lodash.throttle "^4.1.1" + metro-resolver "0.76.8" + metro-file-map@0.76.7: version "0.76.7" resolved "https://registry.yarnpkg.com/metro-file-map/-/metro-file-map-0.76.7.tgz#0f041a4f186ac672f0188180310609c8483ffe89" @@ -6005,6 +6060,26 @@ metro-file-map@0.76.7: optionalDependencies: fsevents "^2.3.2" +metro-file-map@0.76.8: + version "0.76.8" + resolved "https://registry.yarnpkg.com/metro-file-map/-/metro-file-map-0.76.8.tgz#a1db1185b6c316904ba6b53d628e5d1323991d79" + integrity sha512-A/xP1YNEVwO1SUV9/YYo6/Y1MmzhL4ZnVgcJC3VmHp/BYVOXVStzgVbWv2wILe56IIMkfXU+jpXrGKKYhFyHVw== + dependencies: + anymatch "^3.0.3" + debug "^2.2.0" + fb-watchman "^2.0.0" + graceful-fs "^4.2.4" + invariant "^2.2.4" + jest-regex-util "^27.0.6" + jest-util "^27.2.0" + jest-worker "^27.2.0" + micromatch "^4.0.4" + node-abort-controller "^3.1.1" + nullthrows "^1.1.1" + walker "^1.0.7" + optionalDependencies: + fsevents "^2.3.2" + metro-inspector-proxy@0.76.7: version "0.76.7" resolved "https://registry.yarnpkg.com/metro-inspector-proxy/-/metro-inspector-proxy-0.76.7.tgz#c067df25056e932002a72a4b45cf7b4b749f808e" @@ -6016,6 +6091,17 @@ metro-inspector-proxy@0.76.7: ws "^7.5.1" yargs "^17.6.2" +metro-inspector-proxy@0.76.8: + version "0.76.8" + resolved "https://registry.yarnpkg.com/metro-inspector-proxy/-/metro-inspector-proxy-0.76.8.tgz#6b8678a7461b0b42f913a7881cc9319b4d3cddff" + integrity sha512-Us5o5UEd4Smgn1+TfHX4LvVPoWVo9VsVMn4Ldbk0g5CQx3Gu0ygc/ei2AKPGTwsOZmKxJeACj7yMH2kgxQP/iw== + dependencies: + connect "^3.6.5" + debug "^2.2.0" + node-fetch "^2.2.0" + ws "^7.5.1" + yargs "^17.6.2" + metro-minify-terser@0.76.7: version "0.76.7" resolved "https://registry.yarnpkg.com/metro-minify-terser/-/metro-minify-terser-0.76.7.tgz#aefac8bb8b6b3a0fcb5ea0238623cf3e100893ff" @@ -6023,6 +6109,13 @@ metro-minify-terser@0.76.7: dependencies: terser "^5.15.0" +metro-minify-terser@0.76.8: + version "0.76.8" + resolved "https://registry.yarnpkg.com/metro-minify-terser/-/metro-minify-terser-0.76.8.tgz#915ab4d1419257fc6a0b9fa15827b83fe69814bf" + integrity sha512-Orbvg18qXHCrSj1KbaeSDVYRy/gkro2PC7Fy2tDSH1c9RB4aH8tuMOIXnKJE+1SXxBtjWmQ5Yirwkth2DyyEZA== + dependencies: + terser "^5.15.0" + metro-minify-uglify@0.76.7: version "0.76.7" resolved "https://registry.yarnpkg.com/metro-minify-uglify/-/metro-minify-uglify-0.76.7.tgz#3e0143786718dcaea4e28a724698d4f8ac199a43" @@ -6030,6 +6123,13 @@ metro-minify-uglify@0.76.7: dependencies: uglify-es "^3.1.9" +metro-minify-uglify@0.76.8: + version "0.76.8" + resolved "https://registry.yarnpkg.com/metro-minify-uglify/-/metro-minify-uglify-0.76.8.tgz#74745045ea2dd29f8783db483b2fce58385ba695" + integrity sha512-6l8/bEvtVaTSuhG1FqS0+Mc8lZ3Bl4RI8SeRIifVLC21eeSDp4CEBUWSGjpFyUDfi6R5dXzYaFnSgMNyfxADiQ== + dependencies: + uglify-es "^3.1.9" + metro-react-native-babel-preset@0.76.7: version "0.76.7" resolved "https://registry.yarnpkg.com/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.76.7.tgz#dfe15c040d0918147a8b0e9f530d558287acbb54" @@ -6075,6 +6175,51 @@ metro-react-native-babel-preset@0.76.7: babel-plugin-transform-flow-enums "^0.0.2" react-refresh "^0.4.0" +metro-react-native-babel-preset@0.76.8: + version "0.76.8" + resolved "https://registry.yarnpkg.com/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.76.8.tgz#7476efae14363cbdfeeec403b4f01d7348e6c048" + integrity sha512-Ptza08GgqzxEdK8apYsjTx2S8WDUlS2ilBlu9DR1CUcHmg4g3kOkFylZroogVAUKtpYQNYwAvdsjmrSdDNtiAg== + dependencies: + "@babel/core" "^7.20.0" + "@babel/plugin-proposal-async-generator-functions" "^7.0.0" + "@babel/plugin-proposal-class-properties" "^7.18.0" + "@babel/plugin-proposal-export-default-from" "^7.0.0" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.18.0" + "@babel/plugin-proposal-numeric-separator" "^7.0.0" + "@babel/plugin-proposal-object-rest-spread" "^7.20.0" + "@babel/plugin-proposal-optional-catch-binding" "^7.0.0" + "@babel/plugin-proposal-optional-chaining" "^7.20.0" + "@babel/plugin-syntax-dynamic-import" "^7.8.0" + "@babel/plugin-syntax-export-default-from" "^7.0.0" + "@babel/plugin-syntax-flow" "^7.18.0" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.0.0" + "@babel/plugin-syntax-optional-chaining" "^7.0.0" + "@babel/plugin-transform-arrow-functions" "^7.0.0" + "@babel/plugin-transform-async-to-generator" "^7.20.0" + "@babel/plugin-transform-block-scoping" "^7.0.0" + "@babel/plugin-transform-classes" "^7.0.0" + "@babel/plugin-transform-computed-properties" "^7.0.0" + "@babel/plugin-transform-destructuring" "^7.20.0" + "@babel/plugin-transform-flow-strip-types" "^7.20.0" + "@babel/plugin-transform-function-name" "^7.0.0" + "@babel/plugin-transform-literals" "^7.0.0" + "@babel/plugin-transform-modules-commonjs" "^7.0.0" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.0.0" + "@babel/plugin-transform-parameters" "^7.0.0" + "@babel/plugin-transform-react-display-name" "^7.0.0" + "@babel/plugin-transform-react-jsx" "^7.0.0" + "@babel/plugin-transform-react-jsx-self" "^7.0.0" + "@babel/plugin-transform-react-jsx-source" "^7.0.0" + "@babel/plugin-transform-runtime" "^7.0.0" + "@babel/plugin-transform-shorthand-properties" "^7.0.0" + "@babel/plugin-transform-spread" "^7.0.0" + "@babel/plugin-transform-sticky-regex" "^7.0.0" + "@babel/plugin-transform-typescript" "^7.5.0" + "@babel/plugin-transform-unicode-regex" "^7.0.0" + "@babel/template" "^7.0.0" + babel-plugin-transform-flow-enums "^0.0.2" + react-refresh "^0.4.0" + metro-react-native-babel-transformer@0.76.7: version "0.76.7" resolved "https://registry.yarnpkg.com/metro-react-native-babel-transformer/-/metro-react-native-babel-transformer-0.76.7.tgz#ccc7c25b49ee8a1860aafdbf48bfa5441d206f8f" @@ -6091,6 +6236,11 @@ metro-resolver@0.76.7: resolved "https://registry.yarnpkg.com/metro-resolver/-/metro-resolver-0.76.7.tgz#f00ebead64e451c060f30926ecbf4f797588df52" integrity sha512-pC0Wgq29HHIHrwz23xxiNgylhI8Rq1V01kQaJ9Kz11zWrIdlrH0ZdnJ7GC6qA0ErROG+cXmJ0rJb8/SW1Zp2IA== +metro-resolver@0.76.8: + version "0.76.8" + resolved "https://registry.yarnpkg.com/metro-resolver/-/metro-resolver-0.76.8.tgz#0862755b9b84e26853978322464fb37c6fdad76d" + integrity sha512-KccOqc10vrzS7ZhG2NSnL2dh3uVydarB7nOhjreQ7C4zyWuiW9XpLC4h47KtGQv3Rnv/NDLJYeDqaJ4/+140HQ== + metro-runtime@0.76.7: version "0.76.7" resolved "https://registry.yarnpkg.com/metro-runtime/-/metro-runtime-0.76.7.tgz#4d75f2dbbcd19a4f01e0d89494e140b0ba8247e4" @@ -6170,6 +6320,17 @@ metro-transform-plugins@0.76.7: "@babel/traverse" "^7.20.0" nullthrows "^1.1.1" +metro-transform-plugins@0.76.8: + version "0.76.8" + resolved "https://registry.yarnpkg.com/metro-transform-plugins/-/metro-transform-plugins-0.76.8.tgz#d77c28a6547a8e3b72250f740fcfbd7f5408f8ba" + integrity sha512-PlkGTQNqS51Bx4vuufSQCdSn2R2rt7korzngo+b5GCkeX5pjinPjnO2kNhQ8l+5bO0iUD/WZ9nsM2PGGKIkWFA== + dependencies: + "@babel/core" "^7.20.0" + "@babel/generator" "^7.20.0" + "@babel/template" "^7.0.0" + "@babel/traverse" "^7.20.0" + nullthrows "^1.1.1" + metro-transform-worker@0.76.7: version "0.76.7" resolved "https://registry.yarnpkg.com/metro-transform-worker/-/metro-transform-worker-0.76.7.tgz#b842d5a542f1806cca401633fc002559b3e3d668" @@ -6188,6 +6349,78 @@ metro-transform-worker@0.76.7: metro-transform-plugins "0.76.7" nullthrows "^1.1.1" +metro-transform-worker@0.76.8: + version "0.76.8" + resolved "https://registry.yarnpkg.com/metro-transform-worker/-/metro-transform-worker-0.76.8.tgz#b9012a196cee205170d0c899b8b175b9305acdea" + integrity sha512-mE1fxVAnJKmwwJyDtThildxxos9+DGs9+vTrx2ktSFMEVTtXS/bIv2W6hux1pqivqAfyJpTeACXHk5u2DgGvIQ== + dependencies: + "@babel/core" "^7.20.0" + "@babel/generator" "^7.20.0" + "@babel/parser" "^7.20.0" + "@babel/types" "^7.20.0" + babel-preset-fbjs "^3.4.0" + metro "0.76.8" + metro-babel-transformer "0.76.8" + metro-cache "0.76.8" + metro-cache-key "0.76.8" + metro-source-map "0.76.8" + metro-transform-plugins "0.76.8" + nullthrows "^1.1.1" + +metro@0.76, metro@0.76.8: + version "0.76.8" + resolved "https://registry.yarnpkg.com/metro/-/metro-0.76.8.tgz#ba526808b99977ca3f9ac5a7432fd02a340d13a6" + integrity sha512-oQA3gLzrrYv3qKtuWArMgHPbHu8odZOD9AoavrqSFllkPgOtmkBvNNDLCELqv5SjBfqjISNffypg+5UGG3y0pg== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/core" "^7.20.0" + "@babel/generator" "^7.20.0" + "@babel/parser" "^7.20.0" + "@babel/template" "^7.0.0" + "@babel/traverse" "^7.20.0" + "@babel/types" "^7.20.0" + accepts "^1.3.7" + async "^3.2.2" + chalk "^4.0.0" + ci-info "^2.0.0" + connect "^3.6.5" + debug "^2.2.0" + denodeify "^1.2.1" + error-stack-parser "^2.0.6" + graceful-fs "^4.2.4" + hermes-parser "0.12.0" + image-size "^1.0.2" + invariant "^2.2.4" + jest-worker "^27.2.0" + jsc-safe-url "^0.2.2" + lodash.throttle "^4.1.1" + metro-babel-transformer "0.76.8" + metro-cache "0.76.8" + metro-cache-key "0.76.8" + metro-config "0.76.8" + metro-core "0.76.8" + metro-file-map "0.76.8" + metro-inspector-proxy "0.76.8" + metro-minify-terser "0.76.8" + metro-minify-uglify "0.76.8" + metro-react-native-babel-preset "0.76.8" + metro-resolver "0.76.8" + metro-runtime "0.76.8" + metro-source-map "0.76.8" + metro-symbolicate "0.76.8" + metro-transform-plugins "0.76.8" + metro-transform-worker "0.76.8" + mime-types "^2.1.27" + node-fetch "^2.2.0" + nullthrows "^1.1.1" + rimraf "^3.0.2" + serialize-error "^2.1.0" + source-map "^0.5.6" + strip-ansi "^6.0.0" + throat "^5.0.0" + ws "^7.5.1" + yargs "^17.6.2" + metro@0.76.7: version "0.76.7" resolved "https://registry.yarnpkg.com/metro/-/metro-0.76.7.tgz#4885917ad28738c7d1e556630e0155f687336230" @@ -8164,6 +8397,11 @@ uglify-es@^3.1.9: commander "~2.13.0" source-map "~0.6.1" +uglify-js@^3.17.4: + version "3.17.4" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c" + integrity sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g== + unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" @@ -8289,6 +8527,11 @@ uuid@^7.0.3: resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== +uuid@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + v8-compile-cache@^2.0.3: version "2.1.1" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745"