diff --git a/packages/react-native/__tests__/cli-test.js b/packages/react-native/__tests__/cli-test.js new file mode 100644 index 00000000000..01b43a4c2b8 --- /dev/null +++ b/packages/react-native/__tests__/cli-test.js @@ -0,0 +1,95 @@ +/** + * 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. + * + * @format + * @noflow + */ + +'use strict'; + +const {spawnSync} = require('node:child_process'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); + +describe('react-native CLI compatibility', () => { + let projectDir; + + beforeEach(() => { + projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rn-cli-test-')); + }); + + afterEach(() => { + fs.rmSync(projectDir, {recursive: true, force: true}); + }); + + function runConfig(version) { + const cliDir = path.join( + projectDir, + 'node_modules', + '@react-native-community', + 'cli', + ); + fs.mkdirSync(cliDir, {recursive: true}); + fs.writeFileSync( + path.join(cliDir, 'package.json'), + JSON.stringify({version}), + ); + fs.writeFileSync( + path.join(cliDir, 'index.js'), + 'exports.run = () => console.log(JSON.stringify({platforms: {}}));', + ); + const reactNativeDir = path.join( + projectDir, + 'node_modules', + 'react-native', + ); + fs.mkdirSync(reactNativeDir, {recursive: true}); + for (const file of ['cli.js', 'package.json']) { + fs.copyFileSync( + path.join(__dirname, '..', file), + path.join(reactNativeDir, file), + ); + } + const {status, stdout, stderr} = spawnSync( + process.execPath, + [path.join(reactNativeDir, 'cli.js'), 'config'], + { + cwd: projectDir, + encoding: 'utf8', + env: { + ...process.env, + NODE_PATH: path.dirname( + path.dirname(require.resolve('semver/package.json')), + ), + }, + }, + ); + return {status, stdout, stderr}; + } + + it('refuses to run an incompatible CLI with an upgrade error', () => { + expect(runConfig('20.2.0')).toMatchInlineSnapshot(` +Object { + "status": 1, + "stderr": "Error: Refusing to use @react-native-community/cli, as the installed version is incompatible with this version of React Native. Please upgrade @react-native-community/cli dependencies to 21.x.x. +", + "stdout": "", +} +`); + }); + + it.each(['21.0.0-alpha.1', '21.0.0', '22.0.0-alpha.1'])( + 'runs compatible CLI %s without an error', + version => { + expect(runConfig(version)).toEqual({ + status: 0, + stdout: '{"platforms":{}}\n', + stderr: '', + }); + }, + ); +}); diff --git a/packages/react-native/cli.js b/packages/react-native/cli.js index dfc5847c9d3..609f647ff23 100755 --- a/packages/react-native/cli.js +++ b/packages/react-native/cli.js @@ -14,6 +14,10 @@ // $FlowFixMe[untyped-import] const {name} = require('./package.json'); const {styleText} = require('node:util'); +const {major} = require('semver'); + +const COMMUNITY_CLI_PACKAGE = '@react-native-community/cli'; +const MIN_SUPPORTED_CLI_MAJOR = 21; const deprecated = () => { throw new Error( @@ -26,7 +30,18 @@ function findCommunityCli(startDir /*: string */ = process.cwd()) { // `@react-native-community/cli` starting from the `react-native` directory. // Instead, we should use the project root, which we assume to be the cwd. const options /*: {paths?: Array, ...} */ = {paths: [startDir]}; - const rncli = require.resolve('@react-native-community/cli', options); + const rncli = require.resolve(COMMUNITY_CLI_PACKAGE, options); + const manifestPath = require.resolve( + `${COMMUNITY_CLI_PACKAGE}/package.json`, + options, + ); + // $FlowFixMe[unsupported-syntax] + const {version} = require(manifestPath); + if (major(version) < MIN_SUPPORTED_CLI_MAJOR) { + throw new Error( + `Refusing to use ${COMMUNITY_CLI_PACKAGE}, as the installed version is incompatible with this version of React Native. Please upgrade ${COMMUNITY_CLI_PACKAGE} dependencies to ${MIN_SUPPORTED_CLI_MAJOR}.x.x.`, + ); + } // $FlowFixMe[unsupported-syntax] return require(rncli); } @@ -70,9 +85,11 @@ function main() { } catch (e) { if (isMissingCliDependency(e)) { warnWithExplicitDependency(); - process.exit(1); + } else { + console.error(String(e)); } - throw e; + process.exitCode = 1; + return; } }