From da6a189a41d442301c315037656061d3880755ba Mon Sep 17 00:00:00 2001 From: Christoph Purrer Date: Mon, 26 Oct 2026 20:25:51 -0700 Subject: [PATCH] Report an actionable error when '@react-native/codegen' is not built Summary: When `react-native/codegen` is checked out from source but never built, `babel-plugin-codegen` failed with a bare `Cannot find module .../lib/...` deep inside Babel preset loading. Catch the failed `lib/` fallback and name the fix, keeping the original failure as `cause`. A missing install and a broken `lib/` build report separately. Adds `codegen-load-test.js` covering all three failures. Changelog: [General][Fixed] - Show an actionable error when `react-native/codegen` is not built Differential Revision: D127711037 --- .../__tests__/codegen-load-test.js | 76 +++++++++++++++++++ packages/babel-plugin-codegen/index.js | 45 ++++++++--- 2 files changed, 112 insertions(+), 9 deletions(-) create mode 100644 packages/babel-plugin-codegen/__tests__/codegen-load-test.js diff --git a/packages/babel-plugin-codegen/__tests__/codegen-load-test.js b/packages/babel-plugin-codegen/__tests__/codegen-load-test.js new file mode 100644 index 000000000000..f1325df3d892 --- /dev/null +++ b/packages/babel-plugin-codegen/__tests__/codegen-load-test.js @@ -0,0 +1,76 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +class ModuleNotFoundError extends Error { + code: string = 'MODULE_NOT_FOUND'; +} + +function missingLibError(): Error { + return new ModuleNotFoundError( + "Cannot find module '@react-native/codegen/lib/parsers/flow/parser'", + ); +} + +// Failure thrown by the virtual `lib/` mock; tests override it per case. +let mockLibLoadError: Error = missingLibError(); + +// `lib/` is gitignored build output and may not exist on disk, so it is +// mocked virtually for every test. This stays a top-level `jest.mock` +// because only it accepts the options argument under this repo's Flow libdef. +jest.mock( + '@react-native/codegen/lib/parsers/flow/parser', + () => { + throw mockLibLoadError; + }, + {virtual: true}, +); + +describe('codegen loading', () => { + beforeEach(() => { + mockLibLoadError = missingLibError(); + }); + + it('throws an actionable error when @react-native/codegen is not built', () => { + jest.isolateModules(() => { + jest.doMock('@react-native/codegen/src/parsers/flow/parser', () => { + throw new SyntaxError("Unexpected token '{'"); + }); + expect(() => { + // $FlowFixMe[untyped-import] `../index.js` is `@noflow`. + require('../index.js'); + }).toThrow(/never built.*yarn workspace @react-native\/codegen build/s); + }); + }); + + it('throws an actionable error when @react-native/codegen is not installed', () => { + jest.isolateModules(() => { + jest.doMock('@react-native/codegen/src/parsers/flow/parser', () => { + throw new ModuleNotFoundError('Cannot find module'); + }); + expect(() => { + // $FlowFixMe[untyped-import] `../index.js` is `@noflow`. + require('../index.js'); + }).toThrow(/could not be found/); + }); + }); + + it('throws an actionable error when the built lib is broken', () => { + jest.isolateModules(() => { + mockLibLoadError = new SyntaxError("Unexpected token '{'"); + jest.doMock('@react-native/codegen/src/parsers/flow/parser', () => { + throw new ModuleNotFoundError('Cannot find module'); + }); + expect(() => { + // $FlowFixMe[untyped-import] `../index.js` is `@noflow`. + require('../index.js'); + }).toThrow(/failed to load.*Rebuild/s); + }); + }); +}); diff --git a/packages/babel-plugin-codegen/index.js b/packages/babel-plugin-codegen/index.js index 2b6dbac1840e..b60e4e7f940f 100644 --- a/packages/babel-plugin-codegen/index.js +++ b/packages/babel-plugin-codegen/index.js @@ -15,22 +15,49 @@ let FlowParser, TypeScriptParser, RNCodegen; const {cheap: traverseCheap} = require('@babel/traverse').default; const {basename} = require('node:path'); +function getCodegenLoadError(srcError, libError) { + if (libError != null && libError.code !== 'MODULE_NOT_FOUND') { + return ( + 'Unable to load @react-native/codegen: its built `lib/` output failed ' + + 'to load, so NativeComponent specs cannot be transformed. Rebuild it, ' + + 'e.g. `yarn workspace @react-native/codegen build`, then retry.' + ); + } + if (srcError != null && srcError.code !== 'MODULE_NOT_FOUND') { + return ( + 'Unable to load @react-native/codegen: the package is checked out ' + + 'from source but was never built (its `lib/` output is missing), so ' + + 'NativeComponent specs cannot be transformed. Build it first, e.g. ' + + '`yarn workspace @react-native/codegen build`, then retry.' + ); + } + return ( + 'Unable to load @react-native/codegen: the package could not be found, ' + + 'so NativeComponent specs cannot be transformed. Install dependencies ' + + 'first, then retry.' + ); +} + try { FlowParser = require('@react-native/codegen/src/parsers/flow/parser').FlowParser; TypeScriptParser = require('@react-native/codegen/src/parsers/typescript/parser').TypeScriptParser; RNCodegen = require('@react-native/codegen/src/generators/RNCodegen'); -} catch (e) { - // Fallback to lib when source doesn't exit (e.g. when installed as a dev dependency) - FlowParser = - // $FlowFixMe[cannot-resolve-module] - require('@react-native/codegen/lib/parsers/flow/parser').FlowParser; - TypeScriptParser = +} catch (srcError) { + // Fallback to lib when source isn't usable (e.g. when installed as a dev dependency) + try { + FlowParser = + // $FlowFixMe[cannot-resolve-module] + require('@react-native/codegen/lib/parsers/flow/parser').FlowParser; + TypeScriptParser = + // $FlowFixMe[cannot-resolve-module] + require('@react-native/codegen/lib/parsers/typescript/parser').TypeScriptParser; // $FlowFixMe[cannot-resolve-module] - require('@react-native/codegen/lib/parsers/typescript/parser').TypeScriptParser; - // $FlowFixMe[cannot-resolve-module] - RNCodegen = require('@react-native/codegen/lib/generators/RNCodegen'); + RNCodegen = require('@react-native/codegen/lib/generators/RNCodegen'); + } catch (libError) { + throw new Error(getCodegenLoadError(srcError, libError), {cause: libError}); + } } const flowParser = new FlowParser();