diff --git a/packages/build-tools/src/buildErrors/__tests__/detectError.test.ts b/packages/build-tools/src/buildErrors/__tests__/detectError.test.ts index 50ee3672fc..f49e270318 100644 --- a/packages/build-tools/src/buildErrors/__tests__/detectError.test.ts +++ b/packages/build-tools/src/buildErrors/__tests__/detectError.test.ts @@ -309,4 +309,219 @@ Refer to "Xcode Logs" below for additional, more detailed logs.`); `Gradle build failed with unknown error. See logs for the "Run gradlew" phase for more information.` ); }); + + // --- Tests for new build error handlers (tracking codes) --- + + it('detects NPM_ERESOLVE tracking code', async () => { + const err = await resolveBuildPhaseErrorAsync( + new Error(), + [ + 'npm ERR! code ERESOLVE', + 'npm ERR! ERESOLVE could not resolve', + 'npm ERR! While resolving: react-native@0.71.0', + ], + { + job: { platform: Platform.ANDROID } as Job, + phase: BuildPhase.INSTALL_DEPENDENCIES, + env: {}, + }, + '/fake/path' + ); + expect(err.errorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR); + expect(err.trackingCode).toBe('NPM_ERESOLVE'); + }); + + it('detects METRO_UNABLE_TO_RESOLVE tracking code', async () => { + const err = await resolveBuildPhaseErrorAsync( + new Error(), + [ + 'error: Unable to resolve module ./src/missing from /home/expo/workingdir/build/index.js: ', + 'None of these files exist:', + ], + { + job: { platform: Platform.ANDROID } as Job, + phase: BuildPhase.PREBUILD, + env: {}, + }, + '/fake/path' + ); + expect(err.errorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR); + expect(err.trackingCode).toBe('METRO_UNABLE_TO_RESOLVE'); + }); + + it('detects METRO_UNABLE_TO_RESOLVE without phase restriction', async () => { + const err = await resolveBuildPhaseErrorAsync( + new Error(), + [ + 'error: Unable to resolve module @react-native/assets from /home/expo/workingdir/build/App.js', + ], + { + job: { platform: Platform.ANDROID } as Job, + phase: BuildPhase.EAGER_BUNDLE, + env: {}, + }, + '/fake/path' + ); + expect(err.errorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR); + expect(err.trackingCode).toBe('METRO_UNABLE_TO_RESOLVE'); + }); + + it('detects PNPM_ERROR tracking code', async () => { + const err = await resolveBuildPhaseErrorAsync( + new Error(), + [ + ' ERR_PNPM_PEER_DEP_ISSUES Unmet peer dependencies', + 'hint: If you want peer dependencies to be automatically installed, add "auto-install-peers=true" to an .npmrc file.', + ], + { + job: { platform: Platform.ANDROID } as Job, + phase: BuildPhase.INSTALL_DEPENDENCIES, + env: {}, + }, + '/fake/path' + ); + expect(err.errorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR); + expect(err.trackingCode).toBe('PNPM_ERROR'); + }); + + it('does not detect PNPM_ERROR outside INSTALL_DEPENDENCIES phase', async () => { + const err = await resolveBuildPhaseErrorAsync( + new Error(), + [' ERR_PNPM_PEER_DEP_ISSUES Unmet peer dependencies'], + { + job: { platform: Platform.ANDROID } as Job, + phase: BuildPhase.PREBUILD, + env: {}, + }, + '/fake/path' + ); + expect(err.errorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR); + expect(err.trackingCode).toBeUndefined(); + }); + + it('detects PREBUILD_DANGEROUS_MOD_ENOENT tracking code', async () => { + const err = await resolveBuildPhaseErrorAsync( + new Error(), + [ + "Error: [android.dangerous]: withAndroidDangerousBaseMod: ENOENT: no such file or directory, open './assets/splash.png'", + ], + { + job: { platform: Platform.ANDROID } as Job, + phase: BuildPhase.PREBUILD, + env: {}, + }, + '/fake/path' + ); + expect(err.errorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR); + expect(err.trackingCode).toBe('PREBUILD_DANGEROUS_MOD_ENOENT'); + }); + + it('detects PREBUILD_DANGEROUS_MOD_ENOENT for iOS', async () => { + const err = await resolveBuildPhaseErrorAsync( + new Error(), + [ + "Error: [ios.dangerous]: withIosDangerousBaseMod: ENOENT: no such file or directory, open './assets/fonts/CustomFont.ttf'", + ], + { + job: { platform: Platform.IOS } as Job, + phase: BuildPhase.PREBUILD, + env: {}, + }, + '/fake/path' + ); + expect(err.errorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR); + expect(err.trackingCode).toBe('PREBUILD_DANGEROUS_MOD_ENOENT'); + }); + + it('detects SYNTAX_ERROR tracking code', async () => { + const err = await resolveBuildPhaseErrorAsync( + new Error(), + ['SyntaxError: Unexpected token } in JSON at position 1234'], + { + job: { platform: Platform.ANDROID } as Job, + phase: BuildPhase.PREBUILD, + env: {}, + }, + '/fake/path' + ); + expect(err.errorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR); + expect(err.trackingCode).toBe('SYNTAX_ERROR'); + }); + + it('detects MONOREPO_PACKAGE_JSON_NOT_FOUND tracking code', async () => { + const err = await resolveBuildPhaseErrorAsync( + new Error(), + ['Error: package.json does not exist at /home/expo/workingdir/build/packages/app'], + { + job: { platform: Platform.ANDROID } as Job, + phase: BuildPhase.INSTALL_DEPENDENCIES, + env: {}, + }, + '/fake/path' + ); + expect(err.errorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR); + expect(err.trackingCode).toBe('MONOREPO_PACKAGE_JSON_NOT_FOUND'); + }); + + it('detects EXPO_CONFIG_ERROR tracking code', async () => { + const err = await resolveBuildPhaseErrorAsync( + new Error(), + ['ConfigError: Property "expo.ios.bundleIdentifier" in app.json is invalid.'], + { + job: { platform: Platform.IOS } as Job, + phase: BuildPhase.PREBUILD, + env: {}, + }, + '/fake/path' + ); + expect(err.errorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR); + expect(err.trackingCode).toBe('EXPO_CONFIG_ERROR'); + }); + + it('detects RUNTIME_VERSION_MISMATCH tracking code', async () => { + const err = await resolveBuildPhaseErrorAsync( + new Error(), + ['Error: runtimeVersion in Expo.plist policies must be set to a valid value.'], + { + job: { platform: Platform.IOS } as Job, + phase: BuildPhase.CONFIGURE_EXPO_UPDATES, + env: {}, + }, + '/fake/path' + ); + expect(err.errorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR); + expect(err.trackingCode).toBe('RUNTIME_VERSION_MISMATCH'); + }); + + it('does not detect RUNTIME_VERSION_MISMATCH outside CONFIGURE_EXPO_UPDATES phase', async () => { + const err = await resolveBuildPhaseErrorAsync( + new Error(), + ['Error: runtimeVersion in Expo.plist policies must be set to a valid value.'], + { + job: { platform: Platform.IOS } as Job, + phase: BuildPhase.PREBUILD, + env: {}, + }, + '/fake/path' + ); + expect(err.errorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR); + expect(err.trackingCode).toBeUndefined(); + }); + + it('detects CONFIG_PLUGIN_RESOLVE_ERROR tracking code', async () => { + const err = await resolveBuildPhaseErrorAsync( + new Error(), + [ + 'Error: Failed to resolve plugin for module "expo-camera" relative to "/home/expo/workingdir/build"', + ], + { + job: { platform: Platform.ANDROID } as Job, + phase: BuildPhase.PREBUILD, + env: {}, + }, + '/fake/path' + ); + expect(err.errorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR); + expect(err.trackingCode).toBe('CONFIG_PLUGIN_RESOLVE_ERROR'); + }); }); diff --git a/packages/build-tools/src/buildErrors/buildErrorHandlers.ts b/packages/build-tools/src/buildErrors/buildErrorHandlers.ts index 854defad6f..fc3579c175 100644 --- a/packages/build-tools/src/buildErrors/buildErrorHandlers.ts +++ b/packages/build-tools/src/buildErrors/buildErrorHandlers.ts @@ -361,4 +361,69 @@ export const buildErrorHandlers: ErrorHandler[] = [ 'fastlane: Bundle React Native code and images failed.' ), }, + // --- Specific patterns for reducing unknown_error bucket --- + { + phase: BuildPhase.INSTALL_DEPENDENCIES, + // npm ERR! ERESOLVE could not resolve + regexp: /npm ERR!.*ERESOLVE|ERESOLVE could not resolve/, + createError: () => new TrackedBuildError('NPM_ERESOLVE', 'npm: ERESOLVE could not resolve.'), + }, + { + phase: BuildPhase.INSTALL_DEPENDENCIES, + // ERR_PNPM_PEER_DEP_ISSUES or other pnpm errors + regexp: /ERR_PNPM_/, + createError: () => new TrackedBuildError('PNPM_ERROR', 'pnpm: error during install.'), + }, + { + // Can occur in PREBUILD, EAGER_BUNDLE, or other phases + // Unable to resolve module `./src/missing` from `index.js` + regexp: /Unable to resolve module/, + createError: () => + new TrackedBuildError('METRO_UNABLE_TO_RESOLVE', 'metro: Unable to resolve module.'), + }, + { + phase: BuildPhase.PREBUILD, + // Error: [android.dangerous]: withAndroidDangerousBaseMod: ENOENT: no such file or directory, open './assets/splash.png' + // Must come AFTER EXPO_CLI_MISSING_ICON which matches the icon-specific subset + regexp: /with(?:Android|Ios)DangerousBaseMod:.*ENOENT/, + createError: () => + new TrackedBuildError( + 'PREBUILD_DANGEROUS_MOD_ENOENT', + 'prebuild: ENOENT in dangerous base mod.' + ), + }, + { + // SyntaxError: Unexpected token ... + regexp: /SyntaxError:/, + createError: () => new TrackedBuildError('SYNTAX_ERROR', 'SyntaxError encountered.'), + }, + { + // package.json does not exist (common in monorepos with wrong working directory) + regexp: /package\.json does not exist/, + createError: () => + new TrackedBuildError('MONOREPO_PACKAGE_JSON_NOT_FOUND', 'package.json does not exist.'), + }, + { + // ConfigError: Property ... in app.json is invalid + regexp: /ConfigError:/, + createError: () => new TrackedBuildError('EXPO_CONFIG_ERROR', 'expo: ConfigError encountered.'), + }, + { + phase: BuildPhase.CONFIGURE_EXPO_UPDATES, + // runtimeVersion policies must be set ... + // runtime version is not equal ... + regexp: /runtimeVersion.*policies.*must.*set|runtime version.*not.*equal/i, + createError: () => + new TrackedBuildError('RUNTIME_VERSION_MISMATCH', 'expo-updates: runtime version mismatch.'), + }, + { + phase: BuildPhase.PREBUILD, + // Failed to resolve plugin for module "expo-camera" ... + regexp: /Failed to resolve plugin/, + createError: () => + new TrackedBuildError( + 'CONFIG_PLUGIN_RESOLVE_ERROR', + 'prebuild: Failed to resolve config plugin.' + ), + }, ];