diff --git a/packages/core-cli-utils/README.md b/packages/core-cli-utils/README.md deleted file mode 100644 index 10ea4acc7570..000000000000 --- a/packages/core-cli-utils/README.md +++ /dev/null @@ -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). diff --git a/packages/core-cli-utils/package.json b/packages/core-cli-utils/package.json deleted file mode 100644 index 794add883bde..000000000000 --- a/packages/core-cli-utils/package.json +++ /dev/null @@ -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" - } -} diff --git a/packages/core-cli-utils/src/index.flow.js b/packages/core-cli-utils/src/index.flow.js deleted file mode 100644 index 966caa905129..000000000000 --- a/packages/core-cli-utils/src/index.flow.js +++ /dev/null @@ -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'; diff --git a/packages/core-cli-utils/src/index.js b/packages/core-cli-utils/src/index.js deleted file mode 100644 index 8d76ba451f1b..000000000000 --- a/packages/core-cli-utils/src/index.js +++ /dev/null @@ -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'); diff --git a/packages/core-cli-utils/src/public/version.js b/packages/core-cli-utils/src/public/version.js deleted file mode 100644 index a5aa0613fa52..000000000000 --- a/packages/core-cli-utils/src/public/version.js +++ /dev/null @@ -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'); diff --git a/packages/react-native/react-native.config.js b/packages/react-native/react-native.config.js index 452c5ffbf379..713290231165 100644 --- a/packages/react-native/react-native.config.js +++ b/packages/react-native/react-native.config.js @@ -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. diff --git a/packages/rn-tester/cli.js b/packages/rn-tester/cli.js index 72cfc6f7c139..34eadcc36e6f 100644 --- a/packages/rn-tester/cli.js +++ b/packages/rn-tester/cli.js @@ -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) { diff --git a/packages/rn-tester/package.json b/packages/rn-tester/package.json index db6034fbac64..26b5b7bd97cc 100644 --- a/packages/rn-tester/package.json +++ b/packages/rn-tester/package.json @@ -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" diff --git a/packages/rn-tester/scripts/monorepo.js b/packages/rn-tester/scripts/monorepo.js deleted file mode 100644 index 7c0d72c2824b..000000000000 --- a/packages/rn-tester/scripts/monorepo.js +++ /dev/null @@ -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; diff --git a/packages/core-cli-utils/.eslintrc.json b/private/core-cli-utils/.eslintrc.json similarity index 100% rename from packages/core-cli-utils/.eslintrc.json rename to private/core-cli-utils/.eslintrc.json diff --git a/packages/core-cli-utils/.gitignore b/private/core-cli-utils/.gitignore similarity index 100% rename from packages/core-cli-utils/.gitignore rename to private/core-cli-utils/.gitignore diff --git a/private/core-cli-utils/README.md b/private/core-cli-utils/README.md new file mode 100644 index 000000000000..545affa2aa5d --- /dev/null +++ b/private/core-cli-utils/README.md @@ -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 diff --git a/private/core-cli-utils/package.json b/private/core-cli-utils/package.json new file mode 100644 index 000000000000..725bf2650b5e --- /dev/null +++ b/private/core-cli-utils/package.json @@ -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" + } +} diff --git a/private/core-cli-utils/src/index.js b/private/core-cli-utils/src/index.js new file mode 100644 index 000000000000..3a963b26c27b --- /dev/null +++ b/private/core-cli-utils/src/index.js @@ -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}; diff --git a/packages/core-cli-utils/src/private/android.js b/private/core-cli-utils/src/private/android.js similarity index 83% rename from packages/core-cli-utils/src/private/android.js rename to private/core-cli-utils/src/private/android.js index c9ea7a7e59ed..2b520b49945a 100644 --- a/packages/core-cli-utils/src/private/android.js +++ b/private/core-cli-utils/src/private/android.js @@ -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, }, @@ -65,8 +68,11 @@ export const tasks = ( install: (...gradleArgs: Args) => { run: Task, }, -}) => ({ - 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}; diff --git a/packages/core-cli-utils/src/private/app.js b/private/core-cli-utils/src/private/app.js similarity index 85% rename from packages/core-cli-utils/src/private/app.js rename to private/core-cli-utils/src/private/app.js index 0491ae577cd6..afb792541432 100644 --- a/packages/core-cli-utils/src/private/app.js +++ b/private/core-cli-utils/src/private/app.js @@ -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, + javascript: Task, + sourcemap?: Task, + validateHermesc?: Task, + convert?: Task, + compose?: Task, +}; +*/ + 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): ExecaPromise { +function metro(...args /*: ReadonlyArray */) /*: ExecaPromise */ { log(`๐Ÿš‡ metro ${args.join(' ')} `); return execa('npx', ['--offline', 'metro', ...args]); } -export const tasks = { - bundle: (options: BundlerOptions, ...args: ReadonlyArray): Bundle => { - const steps: Bundle = { +const tasks = { + bundle: ( + options /*: BundlerOptions */, + ...args /*: ReadonlyArray */ + ) /*: 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, - javascript: Task, - sourcemap?: Task, - validateHermesc?: Task, - convert?: Task, - compose?: Task, -}; - const bundleApp = ( - options: BundlerOptions, - ...metroArgs: ReadonlyArray + options /*: BundlerOptions */, + ...metroArgs /*: ReadonlyArray */ ) => { 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}; diff --git a/packages/core-cli-utils/src/private/apple.js b/private/core-cli-utils/src/private/apple.js similarity index 81% rename from packages/core-cli-utils/src/private/apple.js rename to private/core-cli-utils/src/private/apple.js index 9bea66afbb12..a8a1d933b8d2 100644 --- a/packages/core-cli-utils/src/private/apple.js +++ b/private/core-cli-utils/src/private/apple.js @@ -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, + runCodegen: Task, + validate: Task, + installRubyGems: Task, + installDependencies: Task, +}; + +type BuildTasks = { + validate: Task, + hasPodsInstalled: Task, + build: Task, +}; + +type InstallTasks = { + validate: Task, + install: Task, +}; +*/ + /* eslint sort-keys: "off" */ -export const tasks = { +const tasks = { // 1. Setup your environment for building the iOS apps - bootstrap: ( - options: AppleBootstrapOption, - ): { - cleanupBuildFolder: Task, - runCodegen: Task, - validate: Task, - installRubyGems: Task, - installDependencies: Task, - } => ({ + 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 - ): { - validate: Task, - hasPodsInstalled: Task, - build: Task, - } => ({ + options /*: AppleBuildOptions */, + ...args /*: ReadonlyArray */ + ) /*: 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, install: Task} => ({ + 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}; diff --git a/packages/core-cli-utils/src/private/clean.js b/private/core-cli-utils/src/private/clean.js similarity index 67% rename from packages/core-cli-utils/src/private/clean.js rename to private/core-cli-utils/src/private/clean.js index aade0a1e2e1c..31019e75615a 100644 --- a/packages/core-cli-utils/src/private/clean.js +++ b/private/core-cli-utils/src/private/clean.js @@ -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 { +function deleteDirectoryContents( + directory /*: string */, + filePattern /*: RegExp */, +) /*: () => Promise */ { 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 { +function deleteDirectory(directory /*: string */) /*: () => Promise */ { return async function cleanDirectoryAction() { rmrf(directory); }; @@ -60,14 +68,15 @@ export function deleteDirectory(directory: string): () => Promise { * Deletes the contents of the tmp directory matching a given pattern. * @private */ -export function deleteTmpDirectoryContents( - filepattern: RegExp, -): () => Promise { +function deleteTmpDirectoryContents( + filepattern /*: RegExp */, +) /*: () => Promise */ { return deleteDirectoryContents(os.tmpdir(), filepattern); } const platformGradlew = isWindows ? 'gradlew.bat' : 'gradlew'; +/*:: type CocoaPodsClean = { clean: Task, }; @@ -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, +}; diff --git a/packages/core-cli-utils/src/private/types.js b/private/core-cli-utils/src/private/types.js similarity index 97% rename from packages/core-cli-utils/src/private/types.js rename to private/core-cli-utils/src/private/types.js index 2c2987e1d306..9a5e1fb6956b 100644 --- a/packages/core-cli-utils/src/private/types.js +++ b/private/core-cli-utils/src/private/types.js @@ -8,8 +8,10 @@ * @format */ +/*:: export type Task = { order: number, label: string, action: () => R, }; +*/ diff --git a/packages/core-cli-utils/src/private/utils.js b/private/core-cli-utils/src/private/utils.js similarity index 58% rename from packages/core-cli-utils/src/private/utils.js rename to private/core-cli-utils/src/private/utils.js index 78cadf5ebaae..6acc1fcf861c 100644 --- a/packages/core-cli-utils/src/private/utils.js +++ b/private/core-cli-utils/src/private/utils.js @@ -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( - order: number, - label: string, - action: Task['action'], -): Task { +function task /*:: */( + order /*: number */, + label /*: string */, + action /*: Task['action'] */, +) /*: Task */ { return { action, label, @@ -25,19 +27,24 @@ export function task( }; } -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> +function assertDependencies( + ...deps /*: ReadonlyArray> */ ) { for (const {found, dep, description} of deps) { if (!found) { @@ -63,3 +70,12 @@ export function assertDependencies( } } } + +module.exports = { + assertDependencies, + isMacOS, + isOnPath, + isWindows, + task, + toPascalCase, +}; diff --git a/packages/core-cli-utils/src/public/version.flow.js b/private/core-cli-utils/src/public/version.js similarity index 89% rename from packages/core-cli-utils/src/public/version.flow.js rename to private/core-cli-utils/src/public/version.js index 05a0bd98b389..fae43fbf5a21 100644 --- a/packages/core-cli-utils/src/public/version.flow.js +++ b/private/core-cli-utils/src/public/version.js @@ -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}; diff --git a/private/helloworld/cli.js b/private/helloworld/cli.js index 72cfc6f7c139..34eadcc36e6f 100644 --- a/private/helloworld/cli.js +++ b/private/helloworld/cli.js @@ -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) { diff --git a/private/helloworld/package.json b/private/helloworld/package.json index a37652822ec2..63cdc897f1ee 100644 --- a/private/helloworld/package.json +++ b/private/helloworld/package.json @@ -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", diff --git a/private/helloworld/scripts/monorepo.js b/private/helloworld/scripts/monorepo.js deleted file mode 100644 index bd3d88bc6f4e..000000000000 --- a/private/helloworld/scripts/monorepo.js +++ /dev/null @@ -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; diff --git a/scripts/build/config.js b/scripts/build/config.js index 499da73c399b..e4fd32091dfb 100644 --- a/scripts/build/config.js +++ b/scripts/build/config.js @@ -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', diff --git a/scripts/e2e/init-project-e2e.js b/scripts/e2e/init-project-e2e.js index af396bb34cf4..34468725f695 100644 --- a/scripts/e2e/init-project-e2e.js +++ b/scripts/e2e/init-project-e2e.js @@ -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; } }