Move core-cli-utils to private/ as a reference implementation (#56922)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/56922

Move `react-native/core-cli-utils` from `packages/` to `private/`, stop publishing it to npm, and reframe it as a reference implementation of React Native CLI tooling.

The package has no known external consumers and is only used internally by `private/helloworld/` and `packages/rn-tester/`. Publishing it to npm creates a maintenance surface for a package that serves no external users.

See https://github.com/react-native-community/discussions-and-proposals/pull/1002.

#### Changes

- Move `packages/core-cli-utils/` to `private/core-cli-utils/`.
- Convert all source files to CJS (require/module.exports) with Flow comment syntax (/*:: */), eliminating the runtime Babel dependency.
 - Remove from the JS build pipeline (`scripts/build/config.js`).
 - Remove the defunct `patchCoreCLIUtilsPackageJSON()` runtime patching from both `helloworld/cli.js` and `rn-tester/cli.js`, and delete both `monorepo.js` files that contained it.
  - Add as `devDependency` to `rn-tester` and `helloworld`.
  - Skip `"*"` version deps in `_prepareHelloWorld()` so they aren't rewritten to the Verdaccio-published version.
  - Remove redundant desktop import ignore entry (`private/**` already covers it).
  - Rewrite README as reference implementation documentation.

Changelog:
[General][Breaking] - The `react-native/core-cli-utils` package is no longer published. It remains available in the React Native repo as a reference implementation.

Reviewed By: cortinico

Differential Revision: D105959855

fbshipit-source-id: 42e439a45273bdeca76029eff306cdf2451308e2
This commit is contained in:
Alex Hunt
2026-05-22 12:36:57 -07:00
committed by meta-codesync[bot]
parent 36bec56b0b
commit 9c3fddf9cd
26 changed files with 267 additions and 424 deletions
-55
View File
@@ -1,55 +0,0 @@
# @react-native/core-cli-utils
![npm package](https://img.shields.io/npm/v/@react-native/core-cli-utils?color=brightgreen&label=npm%20package)
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.
## Usage
```js
import { Command } from 'commander';
import cli from '@react-native/core-cli-utils';
import debug from 'debug';
const android = new Command('android');
const frameworkFindsAndroidSrcDir = "...";
const tasks = cli.clean.android(frameworkFindsAndroidSrcDir);
const log = debug('fancy-framework:android');
android
.command('clean')
.description(cli.clean.android)
.action(async () => {
const log = debug('fancy-framework:android:clean');
log(`🧹 let me clean your Android caches`);
// Add other caches your framework needs besides the normal React Native caches
// here.
for (const task of tasks) {
try {
log(`\t ${task.label}`);
// See: https://github.com/sindresorhus/execa#lines
const {stdout} = await task.action({ lines: true })
log(stdout.join('\n\tGradle: '));
} catch (e) {
log(`\t ⚠️ whoops: ${e.message}`);
}
}
});
```
And you'd be using it like this:
```bash
$ ./fancy-framework android clean
🧹 let me clean your Android caches
Gradle: // a bunch of gradle output
Gradle: ....
```
## Features
- `"@react-native/core-cli-utils/version.js"` contains the platform and tooling version requirements for react-native.
## Contributing
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).
-42
View File
@@ -1,42 +0,0 @@
{
"name": "@react-native/core-cli-utils",
"version": "0.87.0-main",
"description": "React Native CLI library for Frameworks to build on",
"license": "MIT",
"keywords": [
"cli-utils",
"react-native"
],
"homepage": "https://github.com/facebook/react-native/tree/HEAD/packages/core-cli-utils#readme",
"bugs": "https://github.com/facebook/react-native/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/facebook/react-native.git",
"directory": "packages/core-cli-utils"
},
"main": "./src/index.flow.js",
"exports": {
".": "./src/index.js",
"./package.json": "./package.json",
"./version.js": "./src/public/version.js"
},
"publishConfig": {
"main": "./dist/index.js",
"exports": {
".": "./dist/index.js",
"./package.json": "./package.json",
"./version.js": "./dist/public/version.js"
}
},
"files": [
"dist"
],
"scripts": {
"prepack": "node ../../scripts/build/prepack.js"
},
"dependencies": {},
"devDependencies": {},
"engines": {
"node": "^22.13.0 || ^24.3.0 || >= 26.0.0"
}
}
-25
View File
@@ -1,25 +0,0 @@
/**
* 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
*/
// @babel/register doesn't like export {foo} from './bar'; statements,
// so we have to jump through hoops here.
import {tasks as _android} from './private/android.js';
import {tasks as _app} from './private/app.js';
import {tasks as _apple} from './private/apple.js';
import {tasks as _clean} from './private/clean.js';
import * as _version from './public/version.js';
export const android = _android;
export const app = _app;
export const apple = _apple;
export const clean = _clean;
export const version = _version;
export type {Task} from './private/types';
-19
View File
@@ -1,19 +0,0 @@
/**
* 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
*/
/*::
export type * from './index.flow';
*/
if (process.env.BUILD_EXCLUDE_BABEL_REGISTER == null) {
require('../../../scripts/shared/babelRegister').registerForMonorepo();
}
module.exports = require('./index.flow');
@@ -1,19 +0,0 @@
/**
* 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
*/
/*::
export type * from './version.flow';
*/
if (process.env.BUILD_EXCLUDE_BABEL_REGISTER == null) {
require('../../../../scripts/shared/babelRegister').registerForMonorepo();
}
module.exports = require('./version.flow');
+1 -1
View File
@@ -18,7 +18,7 @@ import type {Command} from '@react-native-community/cli-types';
// depending on and injecting:
// - @react-native-community/cli-platform-android
// - @react-native-community/cli-platform-ios
// - @react-native/community-cli-plugin (via the @react-native/core-cli-utils package)
// - @react-native/community-cli-plugin
// - codegen command should be inhoused into @react-native-community/cli
//
// This is a temporary workaround.
-19
View File
@@ -12,30 +12,11 @@
import {Command} from 'commander';
*/
// eslint-disable-next-line @react-native/monorepo/sort-imports
const {patchCoreCLIUtilsPackageJSON} = require('./scripts/monorepo');
function injectCoreCLIUtilsRuntimePatch() {
patchCoreCLIUtilsPackageJSON(true);
const cleared = {
status: false,
};
['exit', 'SIGUSR1', 'SIGUSR2', 'uncaughtException'].forEach(event => {
if (cleared.status) {
return;
}
patchCoreCLIUtilsPackageJSON(false);
cleared.status = true;
});
}
if (process.env.BUILD_EXCLUDE_BABEL_REGISTER == null) {
// $FlowFixMe[cannot-resolve-module]
require('../../scripts/shared/babelRegister').registerForMonorepo();
}
injectCoreCLIUtilsRuntimePatch();
const program /*: Command */ = require('./cli.flow.js').default;
if (require.main === module) {
+1
View File
@@ -55,6 +55,7 @@
"@react-native-community/cli": "20.0.0",
"@react-native-community/cli-platform-android": "20.0.0",
"@react-native-community/cli-platform-ios": "20.0.0",
"@react-native/core-cli-utils": "*",
"commander": "^12.0.0",
"listr2": "^6.4.1",
"rxjs": "npm:@react-native-community/rxjs@6.5.4-custom"
-41
View File
@@ -1,41 +0,0 @@
/**
* 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
*/
// To be able to execute the cli as a yarn script, we have to strip our yarn types.
// This causes problems for some of our dependencies, because they live in Meta internals,
// Github and in NPM:
// - Github and Meta: dynamicly transpile our dependencies. They each have to register on the monorepo
// - NPM: `yarn run build`, and it should update the package.json's exports, main and files
function patchCoreCLIUtilsPackageJSON(patch /*: boolean */) {
const fs = require('fs');
const log = require('debug');
const pkg = JSON.parse(
fs.readFileSync('../core-cli-utils/package.json', 'utf8'),
);
const target = patch ? './src/monorepo.js' : './src/index.flow.js';
if (pkg.main === target) {
return;
}
pkg.main = target;
pkg.exports['.'] = target;
log(
`Patched: ${JSON.stringify(
{main: pkg.main, exports: pkg.exports},
null,
2,
)}`,
);
fs.writeFileSync(
'../core-cli-utils/package.json',
JSON.stringify(pkg, null, 2),
);
}
module.exports.patchCoreCLIUtilsPackageJSON = patchCoreCLIUtilsPackageJSON;
+18
View File
@@ -0,0 +1,18 @@
# @react-native/core-cli-utils
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.
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.
## Modules
- **`android`** — Gradle-based Android build tasks (assemble, build, install)
- **`apple`** — Xcode/CocoaPods-based iOS tasks (bootstrap, build, install)
- **`app`** — Metro bundler tasks (watch mode, bundle mode, Hermes bytecode compilation)
- **`clean`** — Cache-cleaning tasks (Android/Gradle, Metro, npm, Watchman, Yarn, CocoaPods)
- **`version`** — Semver version requirements for platform toolchains (Android NDK/SDK, Xcode, Node, etc.)
## Consumers
- [`private/helloworld/`](../helloworld/) — the primary consumer, using Android, iOS, and Metro modules
- [`packages/rn-tester/`](../../packages/rn-tester/) — uses iOS bootstrap for CocoaPods setup
+19
View File
@@ -0,0 +1,19 @@
{
"name": "@react-native/core-cli-utils",
"version": "0.0.0",
"description": "Reference implementation of React Native CLI tooling",
"license": "MIT",
"main": "./src/index.js",
"exports": {
".": "./src/index.js",
"./package.json": "./package.json",
"./version.js": "./src/public/version.js"
},
"dependencies": {
"metro-babel-register": "^0.84.3"
},
"devDependencies": {},
"engines": {
"node": "^22.13.0 || ^24.3.0 || >= 26.0.0"
}
}
+21
View File
@@ -0,0 +1,21 @@
/**
* 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
*/
/*::
export type {Task} from './private/types';
*/
const {tasks: android} = require('./private/android.js');
const {tasks: app} = require('./private/app.js');
const {tasks: apple} = require('./private/apple.js');
const {tasks: clean} = require('./private/clean.js');
const version = require('./public/version.js');
module.exports = {android, app, apple, clean, version};
@@ -8,12 +8,15 @@
* @format
*/
/*::
import type {Task} from './types';
import type {ExecaPromise} from 'execa';
*/
import {isWindows, task} from './utils';
import execa from 'execa';
const {isWindows, task} = require('./utils');
const execa = require('execa');
/*::
type AndroidBuildMode = 'Debug' | 'Release';
type Path = string;
@@ -27,12 +30,13 @@ type Config = {
newArchitecture?: boolean,
sdk?: Path,
};
*/
function gradle(
taskName: string,
args: Args,
options: {cwd: string, env?: {[k: string]: string | void}},
): ExecaPromise {
taskName /*: string */,
args /*: Args */,
options /*: {cwd: string, env?: {[k: string]: string | void}} */,
) /*: ExecaPromise */ {
const gradlew = isWindows ? 'gradlew.bat' : './gradlew';
return execa(gradlew, [taskName, ...args], {
cwd: options.cwd,
@@ -40,11 +44,11 @@ function gradle(
});
}
function androidSdkPath(sdk?: string): string {
function androidSdkPath(sdk /*: ?string */) /*: string */ {
return sdk ?? process.env.ANDROID_HOME ?? process.env.ANDROID_SDK ?? '';
}
function boolToStr(value: boolean): string {
function boolToStr(value /*: boolean */) /*: string */ {
return value ? 'true' : 'false';
}
@@ -53,9 +57,8 @@ const FIRST = 1;
//
// Android Tasks
//
export const tasks = (
config: Config,
): ({
/*::
type AndroidTasks = {
assemble: (...gradleArgs: Args) => {
run: Task<ExecaPromise>,
},
@@ -65,8 +68,11 @@ export const tasks = (
install: (...gradleArgs: Args) => {
run: Task<ExecaPromise>,
},
}) => ({
assemble: (...gradleArgs: Args) => ({
};
*/
const tasks = (config /*: Config */) /*: AndroidTasks */ => ({
assemble: (...gradleArgs /*: Args */) => ({
run: task(FIRST, 'Assemble Android App', () => {
const args = [];
if (config.hermes != null) {
@@ -82,7 +88,7 @@ export const tasks = (
});
}),
}),
build: (...gradleArgs: Args) => ({
build: (...gradleArgs /*: Args */) => ({
run: task(FIRST, 'Assembles and tests Android App', () => {
const args = [];
if (config.hermes != null) {
@@ -101,10 +107,11 @@ export const tasks = (
/**
* Useful extra gradle arguments:
*
*
* -PreactNativeDevServerPort=8081 sets the port for the installed app to point towards a Metro
* server on (for example) 8081.
*/
install: (...gradleArgs: Args) => ({
install: (...gradleArgs /*: Args */) => ({
run: task(FIRST, 'Installs the assembled Android App', () =>
gradle(`${config.name}:install${config.mode}`, gradleArgs, {
cwd: config.cwd,
@@ -118,3 +125,5 @@ export const tasks = (
// CLI's code:
// https://github.com/react-native-community/cli/blob/54d48a4e08a1aef334ae6168788e0157a666b4f5/packages/cli-platform-android/src/commands/runAndroid/index.ts#L272C1-L290C2
});
module.exports = {tasks};
@@ -8,17 +8,20 @@
* @format
*/
/*::
import type {Task} from './types';
import type {ExecaPromise} from 'execa';
*/
import {task} from './utils';
import debug from 'debug';
import execa from 'execa';
import fs from 'fs';
import path from 'path';
const {task} = require('./utils');
const debug = require('debug');
const execa = require('execa');
const fs = require('fs');
const path = require('path');
const log = debug('core-cli-utils');
/*::
type BundlerOptions = {
// Metro's config: https://metrobundler.dev/docs/configuration/
config?: string,
@@ -65,24 +68,37 @@ type BundlerBuild = {
type Bundler = BundlerWatch | BundlerBuild;
type Bundle = {
validate?: Task<void>,
javascript: Task<ExecaPromise>,
sourcemap?: Task<void>,
validateHermesc?: Task<ExecaPromise>,
convert?: Task<ExecaPromise>,
compose?: Task<ExecaPromise>,
};
*/
const FIRST = 1,
SECOND = 2,
THIRD = 3,
FOURTH = 4;
function getNodePackagePath(packageName: string): string {
function getNodePackagePath(packageName /*: string */) /*: string */ {
// $FlowFixMe[prop-missing] type definition is incomplete
return require.resolve(packageName, {cwd: [process.cwd(), ...module.paths]});
}
function metro(...args: ReadonlyArray<string>): ExecaPromise {
function metro(...args /*: ReadonlyArray<string> */) /*: ExecaPromise */ {
log(`🚇 metro ${args.join(' ')} `);
return execa('npx', ['--offline', 'metro', ...args]);
}
export const tasks = {
bundle: (options: BundlerOptions, ...args: ReadonlyArray<string>): Bundle => {
const steps: Bundle = {
const tasks = {
bundle: (
options /*: BundlerOptions */,
...args /*: ReadonlyArray<string> */
) /*: Bundle */ => {
const steps /*: Bundle */ = {
/* eslint-disable sort-keys */
validate: task(FIRST, 'Check if Metro is available', () => {
try {
@@ -103,29 +119,16 @@ export const tasks = {
},
};
type Bundle = {
validate?: Task<void>,
javascript: Task<ExecaPromise>,
sourcemap?: Task<void>,
validateHermesc?: Task<ExecaPromise>,
convert?: Task<ExecaPromise>,
compose?: Task<ExecaPromise>,
};
const bundleApp = (
options: BundlerOptions,
...metroArgs: ReadonlyArray<string>
options /*: BundlerOptions */,
...metroArgs /*: ReadonlyArray<string> */
) => {
if (options.outputJsBundle === options.outputBundle) {
throw new Error('outputJsBundle and outputBundle cannot be the same.');
}
// When using Hermes, Metro should generate the JS bundle to an intermediate file
// to then be converted to bytecode in the outputBundle. Otherwise just write to
// the outputBundle directly.
let output =
options.jsvm === 'hermes' ? options.outputJsBundle : options.outputBundle;
// TODO: Fix this by not using Metro CLI, which appends a .js extension
if (output === options.outputJsBundle && !output.endsWith('.js')) {
log(
`Appending .js to outputBundle (because metro cli does it if it's missing): ${output}`,
@@ -134,7 +137,7 @@ const bundleApp = (
}
const isSourceMaps = options.outputSourceMap != null;
const bundle: Bundle = {
const bundle /*: Bundle */ = {
javascript: task(SECOND, 'Metro generating an .jsbundle', () => {
const args = [
'--platform',
@@ -146,7 +149,6 @@ const bundleApp = (
output,
];
if (options.jsvm === 'hermes' && !options.dev) {
// Hermes doesn't require JS minification
args.push('--minify', 'false');
} else {
args.push('--minify', options.minify ? 'true' : 'false');
@@ -166,9 +168,9 @@ const bundleApp = (
throw new Error('If jsvm == "hermes", hermes config must be provided.');
}
const hermes: HermesConfig = options.hermes;
const hermes /*: HermesConfig */ = options.hermes;
const isHermesInstalled: boolean = fs.existsSync(hermes.path);
const isHermesInstalled /*: boolean */ = fs.existsSync(hermes.path);
if (!isHermesInstalled) {
throw new Error(
'Hermes Pod must be installed before bundling.\n' +
@@ -176,11 +178,8 @@ const bundleApp = (
);
}
const hermesc: string = path.join(hermes.path, hermes.hermesc);
const hermesc /*: string */ = path.join(hermes.path, hermes.hermesc);
/*
* Hermes only tasks:
*/
let composeSourceMaps;
if (isSourceMaps) {
bundle.sourcemap = task(
@@ -244,3 +243,5 @@ const bundleApp = (
return bundle;
};
module.exports = {tasks};
@@ -8,14 +8,17 @@
* @format
*/
/*::
import type {Task} from './types';
import type {ExecaPromise} from 'execa';
*/
import {assertDependencies, isOnPath, task} from './utils';
import execa from 'execa';
import fs from 'fs';
import path from 'path';
const {assertDependencies, isOnPath, task} = require('./utils');
const execa = require('execa');
const fs = require('fs');
const path = require('path');
/*::
type AppleBuildMode = 'Debug' | 'Release';
type AppleBuildOptions = {
@@ -50,10 +53,11 @@ type AppleOptions = {
// The environment variables to pass to the build command
env?: {[key: string]: string | void, ...},
};
*/
function checkPodfileInSyncWithManifest(
lockfilePath: string,
manifestLockfilePath: string,
lockfilePath /*: string */,
manifestLockfilePath /*: string */,
) {
try {
const expected = fs.readFileSync(lockfilePath, 'utf8');
@@ -63,8 +67,8 @@ function checkPodfileInSyncWithManifest(
'Please run: yarn bootstrap ios, Podfile.lock and Pods/Manifest.lock are out of sync',
);
}
} catch (e) {
throw new Error('Please run: yarn run bootstrap ios: ' + e.message);
} catch (e /*: unknown */) {
throw new Error('Please run: yarn run bootstrap ios: ' + String(e));
}
}
@@ -74,23 +78,36 @@ const FIRST = 1,
FOURTH = 4,
FIFTH = 5;
function getNodePackagePath(packageName: string): string {
function getNodePackagePath(packageName /*: string */) /*: string */ {
// $FlowFixMe[prop-missing] type definition is incomplete
return require.resolve(packageName, {cwd: [process.cwd(), ...module.paths]});
}
/*::
type BootstrapTasks = {
cleanupBuildFolder: Task<void>,
runCodegen: Task<void>,
validate: Task<void>,
installRubyGems: Task<ExecaPromise>,
installDependencies: Task<ExecaPromise>,
};
type BuildTasks = {
validate: Task<void>,
hasPodsInstalled: Task<void>,
build: Task<ExecaPromise>,
};
type InstallTasks = {
validate: Task<void>,
install: Task<ExecaPromise>,
};
*/
/* eslint sort-keys: "off" */
export const tasks = {
const tasks = {
// 1. Setup your environment for building the iOS apps
bootstrap: (
options: AppleBootstrapOption,
): {
cleanupBuildFolder: Task<void>,
runCodegen: Task<void>,
validate: Task<void>,
installRubyGems: Task<ExecaPromise>,
installDependencies: Task<ExecaPromise>,
} => ({
bootstrap: (options /*: AppleBootstrapOption */) /*: BootstrapTasks */ => ({
cleanupBuildFolder: task(FIRST, 'Cleanup build folder', () => {
execa.sync('rm', ['-rf', 'build'], {
cwd: options.cwd,
@@ -125,7 +142,7 @@ export const tasks = {
}),
),
installDependencies: task(FIFTH, 'Install CocoaPods dependencies', () => {
const env: {[string]: string | void} = {
const env /*: {[string]: string | void} */ = {
RCT_NEW_ARCH_ENABLED: options.newArchitecture ? '1' : '0',
USE_FRAMEWORKS: options.frameworks,
USE_HERMES: options.hermes ? '1' : '0',
@@ -143,13 +160,9 @@ export const tasks = {
// 2. Build the iOS app using a setup environment
build: (
options: AppleBuildOptions,
...args: ReadonlyArray<string>
): {
validate: Task<void>,
hasPodsInstalled: Task<void>,
build: Task<ExecaPromise>,
} => ({
options /*: AppleBuildOptions */,
...args /*: ReadonlyArray<string> */
) /*: BuildTasks */ => ({
validate: task(
FIRST,
"Check you've run xcode-select --install for xcodebuild",
@@ -165,8 +178,8 @@ export const tasks = {
/* eslint-disable-next-line no-bitwise */
fs.constants.F_OK | fs.constants.R_OK,
);
} catch (e) {
throw new Error('Please run: yarn run bootstrap ios: ' + e.message);
} catch (e /*: unknown */) {
throw new Error('Please run: yarn run bootstrap ios: ' + String(e));
}
}
checkPodfileInSyncWithManifest(
@@ -185,7 +198,6 @@ export const tasks = {
_args.push('-scheme', options.scheme);
}
if (options.destination != null) {
// The user doesn't want a generic target, they know better.
switch (options.destination) {
case 'simulator':
_args.push('-sdk', 'iphonesimulator');
@@ -204,9 +216,7 @@ export const tasks = {
// 3. Install the built app on a simulator or device
ios: {
install: (
options: AppleInstallApp,
): {validate: Task<void>, install: Task<ExecaPromise>} => ({
install: (options /*: AppleInstallApp */) /*: InstallTasks */ => ({
validate: task(
FIRST,
"Check you've run xcode-select --install for xcrun",
@@ -225,3 +235,5 @@ export const tasks = {
}),
},
};
module.exports = {tasks};
@@ -8,19 +8,27 @@
* @format
*/
/*::
import type {Task} from './types';
import type {ExecaPromise, Options as ExecaOptions} from 'execa';
*/
import {assertDependencies, isMacOS, isOnPath, isWindows, task} from './utils';
import execa from 'execa';
import {existsSync, readdirSync, rm} from 'fs';
import os from 'os';
import path from 'path';
const {
assertDependencies,
isMacOS,
isOnPath,
isWindows,
task,
} = require('./utils');
const execa = require('execa');
const {existsSync, readdirSync, rm} = require('fs');
const os = require('os');
const path = require('path');
const FIRST = 1,
SECOND = 2;
const rmrf = (pathname: string) => {
const rmrf = (pathname /*: string */) => {
if (!existsSync(pathname)) {
return;
}
@@ -31,13 +39,13 @@ const rmrf = (pathname: string) => {
* Removes the contents of a directory matching a given pattern, but keeps the directory.
* @private
*/
export function deleteDirectoryContents(
directory: string,
filePattern: RegExp,
): () => Promise<void> {
function deleteDirectoryContents(
directory /*: string */,
filePattern /*: RegExp */,
) /*: () => Promise<void> */ {
return async function deleteDirectoryContentsAction() {
const base = path.dirname(directory);
const files = readdirSync(base).filter((filename: string) =>
const files = readdirSync(base).filter((filename /*: string */) =>
filePattern.test(filename),
);
for (const filename of files) {
@@ -50,7 +58,7 @@ export function deleteDirectoryContents(
* Removes a directory recursively.
* @private
*/
export function deleteDirectory(directory: string): () => Promise<void> {
function deleteDirectory(directory /*: string */) /*: () => Promise<void> */ {
return async function cleanDirectoryAction() {
rmrf(directory);
};
@@ -60,14 +68,15 @@ export function deleteDirectory(directory: string): () => Promise<void> {
* Deletes the contents of the tmp directory matching a given pattern.
* @private
*/
export function deleteTmpDirectoryContents(
filepattern: RegExp,
): () => Promise<void> {
function deleteTmpDirectoryContents(
filepattern /*: RegExp */,
) /*: () => Promise<void> */ {
return deleteDirectoryContents(os.tmpdir(), filepattern);
}
const platformGradlew = isWindows ? 'gradlew.bat' : 'gradlew';
/*::
type CocoaPodsClean = {
clean: Task<ExecaPromise>,
};
@@ -104,14 +113,15 @@ type CleanTasks = {
yarn: (projectRootDir: string) => YarnClean,
cocoapods?: (projectRootDir: string) => CocoaPodsClean,
};
*/
// The tasks that cleanup various build artefacts.
/* eslint sort-keys: "off" */
export const tasks: CleanTasks = {
const tasks /*: CleanTasks */ = {
/**
* Cleans up the Android Gradle cache
*/
android: (androidSrcDir: ?string, opts?: ExecaOptions) => ({
android: (androidSrcDir /*: ?string */, opts /*: ?ExecaOptions */) => ({
validate: task(FIRST, 'Check gradlew is available', () => {
assertDependencies(isOnPath(platformGradlew, 'Gradle wrapper'));
}),
@@ -150,37 +160,43 @@ export const tasks: CleanTasks = {
/**
* Cleans up the `node_modules` folder and optionally garbage collects the npm cache.
*/
npm: (projectRootDir: string) => ({
npm: (projectRootDir /*: string */) => ({
node_modules: task(
FIRST,
'🧹 Clean node_modules',
deleteDirectory(path.join(projectRootDir, 'node_modules')),
),
verify_cache: task(SECOND, '🔬 Verify npm cache', (opts?: ExecaOptions) =>
execa('npm', ['cache', 'verify'], {cwd: projectRootDir, ...opts}),
verify_cache: task(
SECOND,
'🔬 Verify npm cache',
(opts /*: ?ExecaOptions */) =>
execa('npm', ['cache', 'verify'], {cwd: projectRootDir, ...opts}),
),
}),
/**
* Stops Watchman and clears its cache
*/
watchman: (projectRootDir: string) => ({
stop: task(FIRST, '✋ Stop Watchman', (opts?: ExecaOptions) =>
watchman: (projectRootDir /*: string */) => ({
stop: task(FIRST, '✋ Stop Watchman', (opts /*: ?ExecaOptions */) =>
execa(isWindows ? 'tskill' : 'killall', ['watchman'], {
cwd: projectRootDir,
...opts,
}),
),
cache: task(SECOND, '🧹 Delete Watchman cache', (opts?: ExecaOptions) =>
execa('watchman', ['watch-del-all'], {cwd: projectRootDir, ...opts}),
cache: task(
SECOND,
'🧹 Delete Watchman cache',
(opts /*: ?ExecaOptions */) =>
execa('watchman', ['watch-del-all'], {cwd: projectRootDir, ...opts}),
),
}),
/**
* Cleans up the Yarn cache
*/
yarn: (projectRootDir: string) => ({
clean: task(FIRST, '🧹 Clean Yarn cache', (opts?: ExecaOptions) =>
yarn: (projectRootDir /*: string */) => ({
clean: task(FIRST, '🧹 Clean Yarn cache', (opts /*: ?ExecaOptions */) =>
execa('yarn', ['cache', 'clean'], {cwd: projectRootDir, ...opts}),
),
}),
@@ -190,13 +206,22 @@ if (isMacOS) {
/**
* Cleans up the local and global CocoaPods cache
*/
tasks.cocoapods = (projectRootDir: string) => ({
// TODO: add project root
clean: task(FIRST, '🧹 Clean CocoaPods pod cache', (opts?: ExecaOptions) =>
execa('bundle', ['exec', 'pod', 'deintegrate'], {
cwd: projectRootDir,
...opts,
}),
tasks.cocoapods = (projectRootDir /*: string */) => ({
clean: task(
FIRST,
'🧹 Clean CocoaPods pod cache',
(opts /*: ?ExecaOptions */) =>
execa('bundle', ['exec', 'pod', 'deintegrate'], {
cwd: projectRootDir,
...opts,
}),
),
});
}
module.exports = {
deleteDirectory,
deleteDirectoryContents,
deleteTmpDirectoryContents,
tasks,
};
@@ -8,8 +8,10 @@
* @format
*/
/*::
export type Task<R = unknown> = {
order: number,
label: string,
action: () => R,
};
*/
@@ -8,16 +8,18 @@
* @format
*/
/*::
import type {Task} from './types';
*/
import execa from 'execa';
import os from 'os';
const execa = require('execa');
const os = require('os');
export function task<R>(
order: number,
label: string,
action: Task<R>['action'],
): Task<R> {
function task /*:: <R> */(
order /*: number */,
label /*: string */,
action /*: Task<R>['action'] */,
) /*: Task<R> */ {
return {
action,
label,
@@ -25,19 +27,24 @@ export function task<R>(
};
}
export const isWindows = os.platform() === 'win32';
export const isMacOS = os.platform() === 'darwin';
const isWindows = os.platform() === 'win32';
const isMacOS = os.platform() === 'darwin';
export const toPascalCase = (label: string): string =>
const toPascalCase = (label /*: string */) /*: string */ =>
label.length === 0 ? '' : label[0].toUpperCase() + label.slice(1);
/*::
type PathCheckResult = {
found: boolean,
dep: string,
description: string,
};
*/
export function isOnPath(dep: string, description: string): PathCheckResult {
function isOnPath(
dep /*: string */,
description /*: string */,
) /*: PathCheckResult */ {
const cmd = isWindows ? ['where', [dep]] : ['command', ['-v', dep]];
try {
return {
@@ -54,8 +61,8 @@ export function isOnPath(dep: string, description: string): PathCheckResult {
}
}
export function assertDependencies(
...deps: ReadonlyArray<ReturnType<typeof isOnPath>>
function assertDependencies(
...deps /*: ReadonlyArray<ReturnType<typeof isOnPath>> */
) {
for (const {found, dep, description} of deps) {
if (!found) {
@@ -63,3 +70,12 @@ export function assertDependencies(
}
}
}
module.exports = {
assertDependencies,
isMacOS,
isOnPath,
isWindows,
task,
toPascalCase,
};
@@ -26,17 +26,17 @@
//
// For react-native@0.75.0 you have to have a version of XCode >= 12
export const android = {
const android = {
ANDROID_NDK: '>= 23.x',
ANDROID_SDK: '>= 33.x',
};
export const apple = {
const apple = {
COCOAPODS: '>= 1.10.0',
XCODE: '>= 12.x',
};
export const common = {
const common = {
BUN: '>= 1.0.0',
JAVA: '>= 17 <= 20',
NODE_JS: '>= 18',
@@ -45,8 +45,10 @@ export const common = {
YARN: '>= 1.10.x',
};
export const all = {
const all = {
...apple,
...android,
...common,
};
module.exports = {all, android, apple, common};
-19
View File
@@ -12,30 +12,11 @@
import {Command} from 'commander';
*/
// eslint-disable-next-line @react-native/monorepo/sort-imports
const {patchCoreCLIUtilsPackageJSON} = require('./scripts/monorepo');
function injectCoreCLIUtilsRuntimePatch() {
patchCoreCLIUtilsPackageJSON(true);
const cleared = {
status: false,
};
['exit', 'SIGUSR1', 'SIGUSR2', 'uncaughtException'].forEach(event => {
if (cleared.status) {
return;
}
patchCoreCLIUtilsPackageJSON(false);
cleared.status = true;
});
}
if (process.env.BUILD_EXCLUDE_BABEL_REGISTER == null) {
// $FlowFixMe[cannot-resolve-module]
require('../../scripts/shared/babelRegister').registerForMonorepo();
}
injectCoreCLIUtilsRuntimePatch();
const program /*: Command */ = require('./cli.flow.js').default;
if (require.main === module) {
+1 -1
View File
@@ -20,7 +20,7 @@
"@babel/preset-env": "^7.25.3",
"@babel/runtime": "^7.25.0",
"@react-native/babel-preset": "0.87.0-main",
"@react-native/core-cli-utils": "0.87.0-main",
"@react-native/core-cli-utils": "*",
"@react-native/eslint-config": "0.87.0-main",
"@react-native/metro-config": "0.87.0-main",
"@react-native/typescript-config": "0.87.0-main",
-46
View File
@@ -1,46 +0,0 @@
/**
* 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
*/
// To be able to execute the cli as a yarn script, we have to strip our yarn types.
// This causes problems for some of our dependencies, because they live in Meta internals,
// Github and in NPM:
// - Github and Meta: dynamicly transpile our dependencies. They each have to register on the monorepo
// - NPM: `yarn run build`, and it should update the package.json's exports, main and files
function patchCoreCLIUtilsPackageJSON(patch /*: boolean */) {
const log = require('debug');
const fs = require('fs');
const path = require('path');
function repositoryPath(relativePath /*: string */) {
return path.join(__dirname, '..', '..', '..', relativePath);
}
const packageJsonPath = repositoryPath(
'packages/core-cli-utils/package.json',
);
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const target = patch ? './src/monorepo.js' : './src/index.flow.js';
if (pkg.main === target) {
return;
}
pkg.main = target;
pkg.exports['.'] = target;
log(
`Patched: ${JSON.stringify(
{main: pkg.main, exports: pkg.exports},
null,
2,
)}`,
);
fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2));
}
module.exports.patchCoreCLIUtilsPackageJSON = patchCoreCLIUtilsPackageJSON;
-4
View File
@@ -41,10 +41,6 @@ const buildConfig: BuildConfig = {
'community-cli-plugin': {
target: 'node',
},
'core-cli-utils': {
emitTypeScriptDefs: true,
target: 'node',
},
'debugger-shell': {
emitTypeScriptDefs: true,
target: 'node',
+8 -2
View File
@@ -219,12 +219,18 @@ function _prepareHelloWorld(
// and update the dependencies and devDependencies of packages scoped as @react-native
// to the version passed as parameter
for (const key of Object.keys(packageJson.dependencies)) {
if (key.startsWith('@react-native/')) {
if (
key.startsWith('@react-native/') &&
packageJson.dependencies[key] !== '*'
) {
packageJson.dependencies[key] = version;
}
}
for (const key of Object.keys(packageJson.devDependencies)) {
if (key.startsWith('@react-native/')) {
if (
key.startsWith('@react-native/') &&
packageJson.devDependencies[key] !== '*'
) {
packageJson.devDependencies[key] = version;
}
}