From 7aba7005c9dcc958211d74dd3b29065afdf67872 Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Thu, 23 Jul 2026 05:31:54 -0700 Subject: [PATCH 1/2] Drop --version arg from bundle script wrapper (#57644) Summary: Remove the unused `--version` option from the standalone bundle script wrapper (unreferenced, separately available via RN CLI). Changelog: [Internal] Reviewed By: cortinico Differential Revision: D113389459 --- packages/react-native/scripts/bundle.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/react-native/scripts/bundle.js b/packages/react-native/scripts/bundle.js index 8feb593cf3a6..225c006b486c 100644 --- a/packages/react-native/scripts/bundle.js +++ b/packages/react-native/scripts/bundle.js @@ -13,19 +13,11 @@ const {bundleCommand: bc} = require('@react-native/community-cli-plugin'); const commander = require('commander'); const {execSync} = require('node:child_process'); -const {readFileSync} = require('node:fs'); -const path = require('node:path'); // Commander 12.0.0 changes from the global to named export // $FlowFixMe[signature-verification-failure] const program = commander.program ?? commander; -program.version( - JSON.parse( - readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8'), - ).version, -); - program .name(bc.name) .description(bc.description ?? '') From 4444e823a47b283708bf14c79fba894f3e353491 Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Thu, 23 Jul 2026 05:31:54 -0700 Subject: [PATCH 2/2] Remove commander dependency from main package (#57645) Summary: Simplify the `scripts/bundle.js` wrapper and remove the `commander` dependency from `packages/react-native`. This script is a lightweight wrapper around `npx react-native bundle`, with extra local config arg handling (for now, unchanged / remains located here). **Refactor only** with script behaviour unchanged. **Other changes** - Combined arg parsing is hoisted into `community-cli-plugin` (which does have `commander`) via `unstable_createBundleCommandParser`. - Drop `program` value export (also unused). **Notes** After this change, only `yargs` remains as a direct dependency `react-native` package (`packages/react-native/scripts/`) (a future cleanup). Changelog: [Internal] Reviewed By: cortinico Differential Revision: D113389458 --- packages/community-cli-plugin/package.json | 1 + .../src/commands/bundle/index.js | 51 +++++++- .../community-cli-plugin/src/index.flow.js | 5 +- packages/react-native/package.json | 1 - packages/react-native/scripts/bundle.js | 114 ++++++++++-------- 5 files changed, 118 insertions(+), 54 deletions(-) diff --git a/packages/community-cli-plugin/package.json b/packages/community-cli-plugin/package.json index 1bf9ab2c8aa4..c780f698a964 100644 --- a/packages/community-cli-plugin/package.json +++ b/packages/community-cli-plugin/package.json @@ -33,6 +33,7 @@ "dependencies": { "@react-native/asset-utils": "0.87.0-main", "@react-native/dev-middleware": "0.87.0-main", + "commander": "^12.0.0", "debug": "^4.4.0", "invariant": "^2.2.4", "metro": "^0.87.0", diff --git a/packages/community-cli-plugin/src/commands/bundle/index.js b/packages/community-cli-plugin/src/commands/bundle/index.js index 28073cd9fd99..7f82ef81d672 100644 --- a/packages/community-cli-plugin/src/commands/bundle/index.js +++ b/packages/community-cli-plugin/src/commands/bundle/index.js @@ -8,14 +8,22 @@ * @format */ -import type {Command} from '@react-native-community/cli-types'; +import type {Command as CommunityCommand} from '@react-native-community/cli-types'; import buildBundle from './buildBundle'; +import {Command} from 'commander'; import path from 'node:path'; export type {BundleCommandArgs} from './buildBundle'; -const bundleCommand: Command = { +type CommandOption = Readonly[number]>; + +type BundleCommandParser = { + parser: Command, + baseHelpInformation: string, +}; + +const bundleCommand: CommunityCommand = { name: 'bundle', description: 'Build the bundle for the provided JavaScript entry file.', func: buildBundle, @@ -123,4 +131,43 @@ const bundleCommand: Command = { ], }; +function addOptions( + command: Command, + options: ReadonlyArray, +): void { + for (const option of options) { + const description = option.description ?? ''; + const defaultValue = + typeof option.default === 'function' ? undefined : option.default; + + if (option.parse != null) { + command.option(option.name, description, option.parse, defaultValue); + } else if ( + typeof defaultValue === 'string' || + typeof defaultValue === 'boolean' || + Array.isArray(defaultValue) + ) { + command.option(option.name, description, defaultValue); + } else { + command.option(option.name, description); + } + } +} + +export function unstable_createBundleCommandParser( + additionalOptions: ReadonlyArray = [], +): BundleCommandParser { + const parser = new Command() + .name(bundleCommand.name) + .description(bundleCommand.description ?? '') + .helpOption('--help', 'Display help for command') + .allowUnknownOption(); + + addOptions(parser, bundleCommand.options ?? []); + const baseHelpInformation = parser.helpInformation(); + addOptions(parser, additionalOptions); + + return {parser, baseHelpInformation}; +} + export default bundleCommand; diff --git a/packages/community-cli-plugin/src/index.flow.js b/packages/community-cli-plugin/src/index.flow.js index 1f8177f03313..0b4dccbe5bda 100644 --- a/packages/community-cli-plugin/src/index.flow.js +++ b/packages/community-cli-plugin/src/index.flow.js @@ -8,7 +8,10 @@ * @format */ -export {default as bundleCommand} from './commands/bundle'; +export { + default as bundleCommand, + unstable_createBundleCommandParser, +} from './commands/bundle'; export {default as startCommand} from './commands/start'; export {unstable_buildBundleWithConfig} from './commands/bundle/buildBundle'; diff --git a/packages/react-native/package.json b/packages/react-native/package.json index 19cc2133e023..091d8770a40b 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -159,7 +159,6 @@ "ansi-regex": "^5.0.0", "babel-plugin-syntax-hermes-parser": "0.37.0", "base64-js": "^1.5.1", - "commander": "^12.0.0", "flow-enums-runtime": "^0.0.6", "hermes-compiler": "0.0.0", "invariant": "^2.2.4", diff --git a/packages/react-native/scripts/bundle.js b/packages/react-native/scripts/bundle.js index 225c006b486c..75783b06919f 100644 --- a/packages/react-native/scripts/bundle.js +++ b/packages/react-native/scripts/bundle.js @@ -10,65 +10,79 @@ 'use strict'; -const {bundleCommand: bc} = require('@react-native/community-cli-plugin'); -const commander = require('commander'); +const { + bundleCommand: bundleCommandBase, + unstable_createBundleCommandParser, +} = require('@react-native/community-cli-plugin'); const {execSync} = require('node:child_process'); -// Commander 12.0.0 changes from the global to named export -// $FlowFixMe[signature-verification-failure] -const program = commander.program ?? commander; +/*:: +type BundleOptions = { + configCmd?: string, + loadConfig?: string, + ... +}; +*/ -program - .name(bc.name) - .description(bc.description ?? '') - .option( - '--config-cmd ', - 'Command to generate a JSON project config', - 'npx react-native config', - ) - .option('--load-config ', 'JSON project config') - .option('--verbose', 'Additional logs', () => true, false) - .allowUnknownOption() - .action(async function handleAction() { - let config = null; - let options = program - .opts /*::<{ - configCmd?: string, - loadConfig?: string, - verbose: boolean, - ... - }>*/ - (); - if (options.loadConfig != null) { - config = JSON.parse( - options.loadConfig.replace(/^\W*'/, '').replace(/'\W*$/, ''), - ); - } else if (options.configCmd != null) { - config = JSON.parse( - execSync(options.configCmd.trim(), {encoding: 'utf8'}), - ); - } +const baseOptions = [ + { + name: '--config-cmd ', + description: 'Command to generate a JSON project config', + default: 'npx react-native config', + }, + { + name: '--load-config ', + description: 'JSON project config', + }, +]; - if (config == null) { - throw new Error('No config provided'); - } +const { + parser: bundleCommandParser, + baseHelpInformation: bundleCommandBaseHelp, +} = unstable_createBundleCommandParser(baseOptions); - await bc.func(program.args, config, options); - }); +function formatOptions( + options /*: ReadonlyArray> */, +) { + return options + .map(option => ` ${option.name.padEnd(35)} ${option.description ?? ''}`) + .join('\n'); +} + +function printHelp() { + console.log(`${bundleCommandBaseHelp} +Additional options: +${formatOptions(baseOptions)} +`); +} + +async function main(argv /*: ReadonlyArray */ = process.argv.slice(2)) { + if (argv.includes('--help')) { + printHelp(); + return; + } -if (bc.options != null) { - for (const o of bc.options) { - program.option( - o.name, - o.description ?? '', - o.parse ?? (value => value), - o.default, + bundleCommandParser.parse(argv, {from: 'user'}); + const options = bundleCommandParser + .opts /*::*/ + (); + + let config = null; + if (options.loadConfig != null) { + config = JSON.parse( + options.loadConfig.replace(/^\W*'/, '').replace(/'\W*$/, ''), ); + } else if (options.configCmd != null) { + config = JSON.parse(execSync(options.configCmd.trim(), {encoding: 'utf8'})); + } + + if (config == null) { + throw new Error('No config provided'); } + + await bundleCommandBase.func(bundleCommandParser.args, config, options); } if (require.main === module) { - program.parse(process.argv); + void main(); } - -module.exports = program;