@react-native/core-cli-utils 0.87.0-nightly-20260519-58cd1bf58 → 0.87.0-nightly-20260528-eaf770433

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintrc.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "ignorePatterns": ["dist/**"],
3
+ "rules": {
4
+ "sort-keys": 2
5
+ }
6
+ }
package/README.md CHANGED
@@ -1,55 +1,18 @@
1
1
  # @react-native/core-cli-utils
2
2
 
3
- ![npm package](https://img.shields.io/npm/v/@react-native/core-cli-utils?color=brightgreen&label=npm%20package)
3
+ A reference implementation of React Native CLI tooling. This package provides composable, ordered `Task` objects for common CLI operations including Android Gradle builds, iOS Xcode/CocoaPods workflows, Metro bundling with Hermes support, and cache cleaning.
4
4
 
5
- A collection of utilites to help Frameworks build their React Native CLI tooling. This is not intended to be used directly use users of React Native.
5
+ This is not published to npm. Framework authors can use this code as a starting point for their own CLI tooling, but should not depend on it as a versioned API.
6
6
 
7
- ## Usage
7
+ ## Modules
8
8
 
9
- ```js
10
- import { Command } from 'commander';
11
- import cli from '@react-native/core-cli-utils';
12
- import debug from 'debug';
9
+ - **`android`** — Gradle-based Android build tasks (assemble, build, install)
10
+ - **`apple`** Xcode/CocoaPods-based iOS tasks (bootstrap, build, install)
11
+ - **`app`** Metro bundler tasks (watch mode, bundle mode, Hermes bytecode compilation)
12
+ - **`clean`** Cache-cleaning tasks (Android/Gradle, Metro, npm, Watchman, Yarn, CocoaPods)
13
+ - **`version`** — Semver version requirements for platform toolchains (Android NDK/SDK, Xcode, Node, etc.)
13
14
 
14
- const android = new Command('android');
15
+ ## Consumers
15
16
 
16
- const frameworkFindsAndroidSrcDir = "...";
17
- const tasks = cli.clean.android(frameworkFindsAndroidSrcDir);
18
- const log = debug('fancy-framework:android');
19
-
20
- android
21
- .command('clean')
22
- .description(cli.clean.android)
23
- .action(async () => {
24
- const log = debug('fancy-framework:android:clean');
25
- log(`🧹 let me clean your Android caches`);
26
- // Add other caches your framework needs besides the normal React Native caches
27
- // here.
28
- for (const task of tasks) {
29
- try {
30
- log(`\t ${task.label}`);
31
- // See: https://github.com/sindresorhus/execa#lines
32
- const {stdout} = await task.action({ lines: true })
33
- log(stdout.join('\n\tGradle: '));
34
- } catch (e) {
35
- log(`\t ⚠️ whoops: ${e.message}`);
36
- }
37
- }
38
- });
39
- ```
40
-
41
- And you'd be using it like this:
42
-
43
- ```bash
44
- $ ./fancy-framework android clean
45
- 🧹 let me clean your Android caches
46
- Gradle: // a bunch of gradle output
47
- Gradle: ....
48
- ```
49
-
50
- ## Features
51
- - `"@react-native/core-cli-utils/version.js"` contains the platform and tooling version requirements for react-native.
52
-
53
- ## Contributing
54
-
55
- Changes to this package can be made locally and linked against your app. Please see the [Contributing guide](https://reactnative.dev/contributing/overview#contributing-code).
17
+ - [`private/helloworld/`](../helloworld/) the primary consumer, using Android, iOS, and Metro modules
18
+ - [`packages/rn-tester/`](../../packages/rn-tester/) uses iOS bootstrap for CocoaPods setup
package/package.json CHANGED
@@ -1,34 +1,19 @@
1
1
  {
2
2
  "name": "@react-native/core-cli-utils",
3
- "version": "0.87.0-nightly-20260519-58cd1bf58",
4
- "description": "React Native CLI library for Frameworks to build on",
3
+ "version": "0.87.0-nightly-20260528-eaf770433",
4
+ "description": "Reference implementation of React Native CLI tooling",
5
5
  "license": "MIT",
6
- "keywords": [
7
- "cli-utils",
8
- "react-native"
9
- ],
10
- "homepage": "https://github.com/facebook/react-native/tree/HEAD/packages/core-cli-utils#readme",
11
- "bugs": "https://github.com/facebook/react-native/issues",
12
- "repository": {
13
- "type": "git",
14
- "url": "git+https://github.com/facebook/react-native.git",
15
- "directory": "packages/core-cli-utils"
16
- },
17
- "main": "./dist/index.js",
6
+ "main": "./src/index.js",
18
7
  "exports": {
19
- ".": "./dist/index.js",
8
+ ".": "./src/index.js",
20
9
  "./package.json": "./package.json",
21
- "./version.js": "./dist/public/version.js"
10
+ "./version.js": "./src/public/version.js"
22
11
  },
23
- "files": [
24
- "dist"
25
- ],
26
- "scripts": {
27
- "prepack": "node ../../scripts/build/prepack.js"
12
+ "dependencies": {
13
+ "metro-babel-register": "^0.84.3"
28
14
  },
29
- "dependencies": {},
30
15
  "devDependencies": {},
31
16
  "engines": {
32
- "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
17
+ "node": "^22.13.0 || ^24.3.0 || >= 26.0.0"
33
18
  }
34
19
  }
package/src/index.js ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow strict-local
8
+ * @format
9
+ */
10
+
11
+ /*::
12
+ export type {Task} from './private/types';
13
+ */
14
+
15
+ const {tasks: android} = require('./private/android.js');
16
+ const {tasks: app} = require('./private/app.js');
17
+ const {tasks: apple} = require('./private/apple.js');
18
+ const {tasks: clean} = require('./private/clean.js');
19
+ const version = require('./public/version.js');
20
+
21
+ module.exports = {android, app, apple, clean, version};
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow strict-local
8
+ * @format
9
+ */
10
+
11
+ /*::
12
+ import type {Task} from './types';
13
+ import type {ExecaPromise} from 'execa';
14
+ */
15
+
16
+ const {isWindows, task} = require('./utils');
17
+ const execa = require('execa');
18
+
19
+ /*::
20
+ type AndroidBuildMode = 'Debug' | 'Release';
21
+
22
+ type Path = string;
23
+ type Args = ReadonlyArray<string>;
24
+
25
+ type Config = {
26
+ cwd: Path,
27
+ hermes?: boolean,
28
+ mode: AndroidBuildMode,
29
+ name: string,
30
+ newArchitecture?: boolean,
31
+ sdk?: Path,
32
+ };
33
+ */
34
+
35
+ function gradle(
36
+ taskName /*: string */,
37
+ args /*: Args */,
38
+ options /*: {cwd: string, env?: {[k: string]: string | void}} */,
39
+ ) /*: ExecaPromise */ {
40
+ const gradlew = isWindows ? 'gradlew.bat' : './gradlew';
41
+ return execa(gradlew, [taskName, ...args], {
42
+ cwd: options.cwd,
43
+ env: options.env,
44
+ });
45
+ }
46
+
47
+ function androidSdkPath(sdk /*: ?string */) /*: string */ {
48
+ return sdk ?? process.env.ANDROID_HOME ?? process.env.ANDROID_SDK ?? '';
49
+ }
50
+
51
+ function boolToStr(value /*: boolean */) /*: string */ {
52
+ return value ? 'true' : 'false';
53
+ }
54
+
55
+ const FIRST = 1;
56
+
57
+ //
58
+ // Android Tasks
59
+ //
60
+ /*::
61
+ type AndroidTasks = {
62
+ assemble: (...gradleArgs: Args) => {
63
+ run: Task<ExecaPromise>,
64
+ },
65
+ build: (...gradleArgs: Args) => {
66
+ run: Task<ExecaPromise>,
67
+ },
68
+ install: (...gradleArgs: Args) => {
69
+ run: Task<ExecaPromise>,
70
+ },
71
+ };
72
+ */
73
+
74
+ const tasks = (config /*: Config */) /*: AndroidTasks */ => ({
75
+ assemble: (...gradleArgs /*: Args */) => ({
76
+ run: task(FIRST, 'Assemble Android App', () => {
77
+ const args = [];
78
+ if (config.hermes != null) {
79
+ args.push(`-PhermesEnabled=${boolToStr(config.hermes)}`);
80
+ }
81
+ if (config.newArchitecture != null) {
82
+ args.push(`-PnewArchEnabled=${boolToStr(config.newArchitecture)}`);
83
+ }
84
+ args.push(...gradleArgs);
85
+ return gradle(`${config.name}:assemble${config.mode}`, gradleArgs, {
86
+ cwd: config.cwd,
87
+ env: {ANDROID_HOME: androidSdkPath(config.sdk)},
88
+ });
89
+ }),
90
+ }),
91
+ build: (...gradleArgs /*: Args */) => ({
92
+ run: task(FIRST, 'Assembles and tests Android App', () => {
93
+ const args = [];
94
+ if (config.hermes != null) {
95
+ args.push(`-PhermesEnabled=${boolToStr(config.hermes)}`);
96
+ }
97
+ if (config.newArchitecture != null) {
98
+ args.push(`-PnewArchEnabled=${boolToStr(config.newArchitecture)}`);
99
+ }
100
+ args.push(...gradleArgs);
101
+ return gradle(`${config.name}:bundle${config.mode}`, args, {
102
+ cwd: config.cwd,
103
+ env: {ANDROID_HOME: androidSdkPath(config.sdk)},
104
+ });
105
+ }),
106
+ }),
107
+ /**
108
+ * Useful extra gradle arguments:
109
+ *
110
+ *
111
+ * -PreactNativeDevServerPort=8081 sets the port for the installed app to point towards a Metro
112
+ * server on (for example) 8081.
113
+ */
114
+ install: (...gradleArgs /*: Args */) => ({
115
+ run: task(FIRST, 'Installs the assembled Android App', () =>
116
+ gradle(`${config.name}:install${config.mode}`, gradleArgs, {
117
+ cwd: config.cwd,
118
+ env: {ANDROID_HOME: androidSdkPath(config.sdk)},
119
+ }),
120
+ ),
121
+ }),
122
+
123
+ // We are not supporting launching the app and setting up the tunnel for metro <-> app, this is
124
+ // a framework concern. For an example of how one could do this, please look at the community
125
+ // CLI's code:
126
+ // https://github.com/react-native-community/cli/blob/54d48a4e08a1aef334ae6168788e0157a666b4f5/packages/cli-platform-android/src/commands/runAndroid/index.ts#L272C1-L290C2
127
+ });
128
+
129
+ module.exports = {tasks};
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow strict-local
8
+ * @format
9
+ */
10
+
11
+ /*::
12
+ import type {Task} from './types';
13
+ import type {ExecaPromise} from 'execa';
14
+ */
15
+
16
+ const {task} = require('./utils');
17
+ const debug = require('debug');
18
+ const execa = require('execa');
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ const log = debug('core-cli-utils');
23
+
24
+ /*::
25
+ type BundlerOptions = {
26
+ // Metro's config: https://metrobundler.dev/docs/configuration/
27
+ config?: string,
28
+ // Typically index.{ios,android}.js
29
+ entryFile: string,
30
+ +platform: 'ios' | 'android' | string,
31
+ dev: boolean,
32
+ // Metro built main bundle
33
+ outputJsBundle: string,
34
+ minify: boolean,
35
+ optimize: boolean,
36
+ // Generate a source map file
37
+ outputSourceMap: string,
38
+ // Where to pass the final bundle. Typically this is the App's resource
39
+ // folder, however this is app specific. React Native will need to know where
40
+ // this is to bootstrap your application. See:
41
+ // - Android: https://reactnative.dev/docs/integration-with-existing-apps?language=kotlin#creating-a-release-build-in-android-studio
42
+ // - iOS: https://reactnative.dev/docs/integration-with-existing-apps?language=swift#2-event-handler
43
+ outputBundle: string,
44
+ cwd: string,
45
+
46
+ jsvm: 'hermes' | 'jsc',
47
+ hermes?: HermesConfig,
48
+
49
+ ...Bundler,
50
+ };
51
+
52
+ type HermesConfig = {
53
+ // Path where hermes is is installed
54
+ // iOS: Pods/hermes-engine
55
+ path: string,
56
+ // iOS: <hermes.path>/destroot/bin/hermesc
57
+ hermesc: string,
58
+ };
59
+
60
+ type BundlerWatch = {
61
+ +mode: 'watch',
62
+ callback?: (metro: ExecaPromise) => void,
63
+ };
64
+
65
+ type BundlerBuild = {
66
+ +mode: 'bundle',
67
+ };
68
+
69
+ type Bundler = BundlerWatch | BundlerBuild;
70
+
71
+ type Bundle = {
72
+ validate?: Task<void>,
73
+ javascript: Task<ExecaPromise>,
74
+ sourcemap?: Task<void>,
75
+ validateHermesc?: Task<ExecaPromise>,
76
+ convert?: Task<ExecaPromise>,
77
+ compose?: Task<ExecaPromise>,
78
+ };
79
+ */
80
+
81
+ const FIRST = 1,
82
+ SECOND = 2,
83
+ THIRD = 3,
84
+ FOURTH = 4;
85
+
86
+ function getNodePackagePath(packageName /*: string */) /*: string */ {
87
+ // $FlowFixMe[prop-missing] type definition is incomplete
88
+ return require.resolve(packageName, {cwd: [process.cwd(), ...module.paths]});
89
+ }
90
+
91
+ function metro(...args /*: ReadonlyArray<string> */) /*: ExecaPromise */ {
92
+ log(`🚇 metro ${args.join(' ')} `);
93
+ return execa('npx', ['--offline', 'metro', ...args]);
94
+ }
95
+
96
+ const tasks = {
97
+ bundle: (
98
+ options /*: BundlerOptions */,
99
+ ...args /*: ReadonlyArray<string> */
100
+ ) /*: Bundle */ => {
101
+ const steps /*: Bundle */ = {
102
+ /* eslint-disable sort-keys */
103
+ validate: task(FIRST, 'Check if Metro is available', () => {
104
+ try {
105
+ require('metro');
106
+ } catch {
107
+ throw new Error('Metro is not available');
108
+ }
109
+ }),
110
+ javascript: task(SECOND, 'Metro watching for changes', () =>
111
+ metro('serve', ...args),
112
+ ),
113
+ };
114
+
115
+ return options.mode === 'bundle'
116
+ ? // $FlowFixMe[unsafe-object-assign]
117
+ Object.assign(steps, bundleApp(options, ...args))
118
+ : steps;
119
+ },
120
+ };
121
+
122
+ const bundleApp = (
123
+ options /*: BundlerOptions */,
124
+ ...metroArgs /*: ReadonlyArray<string> */
125
+ ) => {
126
+ if (options.outputJsBundle === options.outputBundle) {
127
+ throw new Error('outputJsBundle and outputBundle cannot be the same.');
128
+ }
129
+ let output =
130
+ options.jsvm === 'hermes' ? options.outputJsBundle : options.outputBundle;
131
+
132
+ if (output === options.outputJsBundle && !output.endsWith('.js')) {
133
+ log(
134
+ `Appending .js to outputBundle (because metro cli does it if it's missing): ${output}`,
135
+ );
136
+ output += '.js';
137
+ }
138
+
139
+ const isSourceMaps = options.outputSourceMap != null;
140
+ const bundle /*: Bundle */ = {
141
+ javascript: task(SECOND, 'Metro generating an .jsbundle', () => {
142
+ const args = [
143
+ '--platform',
144
+ options.platform,
145
+ '--dev',
146
+ options.dev ? 'true' : 'false',
147
+ '--reset-cache',
148
+ '--out',
149
+ output,
150
+ ];
151
+ if (options.jsvm === 'hermes' && !options.dev) {
152
+ args.push('--minify', 'false');
153
+ } else {
154
+ args.push('--minify', options.minify ? 'true' : 'false');
155
+ }
156
+ if (isSourceMaps) {
157
+ args.push('--source-map');
158
+ }
159
+ return metro('build', options.entryFile, ...args, ...metroArgs);
160
+ }),
161
+ };
162
+
163
+ if (options.jsvm === 'jsc') {
164
+ return bundle;
165
+ }
166
+
167
+ if (options.hermes?.path == null || options.hermes?.hermesc == null) {
168
+ throw new Error('If jsvm == "hermes", hermes config must be provided.');
169
+ }
170
+
171
+ const hermes /*: HermesConfig */ = options.hermes;
172
+
173
+ const isHermesInstalled /*: boolean */ = fs.existsSync(hermes.path);
174
+ if (!isHermesInstalled) {
175
+ throw new Error(
176
+ 'Hermes Pod must be installed before bundling.\n' +
177
+ 'Did you forget to bootstrap?',
178
+ );
179
+ }
180
+
181
+ const hermesc /*: string */ = path.join(hermes.path, hermes.hermesc);
182
+
183
+ let composeSourceMaps;
184
+ if (isSourceMaps) {
185
+ bundle.sourcemap = task(
186
+ FIRST,
187
+ 'Check if SourceMap script available',
188
+ () => {
189
+ composeSourceMaps = getNodePackagePath(
190
+ 'react-native/scripts/compose-source-maps.js',
191
+ );
192
+ },
193
+ );
194
+ }
195
+
196
+ bundle.validateHermesc = task(FIRST, 'Check if Hermesc is available', () =>
197
+ execa(hermesc, ['--version']),
198
+ );
199
+
200
+ bundle.convert = task(
201
+ THIRD,
202
+ 'Hermesc converting .jsbundle → bytecode',
203
+ () => {
204
+ const args = [
205
+ '-emit-binary',
206
+ '-max-diagnostic-width=80',
207
+ options.dev === true ? '-Og' : '-O',
208
+ ];
209
+ if (isSourceMaps) {
210
+ args.push('-output-source-map');
211
+ }
212
+ args.push(`-out=${options.outputBundle}`, output);
213
+ return execa(hermesc, args, {cwd: options.cwd});
214
+ },
215
+ );
216
+
217
+ bundle.compose = task(FOURTH, 'Compose Hermes and Metro source maps', () => {
218
+ if (composeSourceMaps == null) {
219
+ throw new Error(
220
+ 'Unable to find the compose-source-map.js script in react-native',
221
+ );
222
+ }
223
+ const metroSourceMap = output.replace(/(\.js)?$/, '.map');
224
+ const hermesSourceMap = options.outputBundle + '.map';
225
+ const compose = execa(
226
+ 'node',
227
+ [
228
+ composeSourceMaps,
229
+ metroSourceMap,
230
+ hermesSourceMap,
231
+ `-o ${options.outputSourceMap}`,
232
+ ],
233
+ {
234
+ cwd: options.cwd,
235
+ },
236
+ );
237
+ compose.finally(() => {
238
+ fs.rmSync(metroSourceMap, {force: true});
239
+ fs.rmSync(hermesSourceMap, {force: true});
240
+ });
241
+ return compose;
242
+ });
243
+
244
+ return bundle;
245
+ };
246
+
247
+ module.exports = {tasks};