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();