Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions packages/core-cli-utils/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# @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: ....
```

## 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).
16 changes: 16 additions & 0 deletions packages/core-cli-utils/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* 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
* @oncall react_native
*/

import {tasks as clean} from './private/clean.js';

export default {
clean,
};
30 changes: 30 additions & 0 deletions packages/core-cli-utils/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "@react-native/core-cli-utils",
"version": "0.74.0",
"description": "React Native CLI library for Frameworks to build on",
"main": "index.js",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/facebook/react-native.git",
"directory": "packages/core-cli-utils"
},
"exports": {
".": "./index.js",
"./package.json": "./package.json"
},
"homepage": "https://github.com/facebook/react-native/tree/HEAD/packages/core-cli-utils#readme",
"keywords": [
"cli-utils",
"react-native"
],
"bugs": "https://github.com/facebook/react-native/issues",
"engines": {
"node": ">=18"
},
"files": [
"dist"
],
"dependencies": {},
"devDependencies": {}
}
168 changes: 168 additions & 0 deletions packages/core-cli-utils/private/clean.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/**
* 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
* @oncall react_native
*/

import type {Task} from './types';
import type {Options as ExecaOptions} from 'execa';

import {isMacOS, isWindows, task} from './utils';
// TODO: Remove execa dependency.
import execa from 'execa';
import {existsSync, readdirSync, rm} from 'fs';
import os from 'os';
import path from 'path';

type CleanTasks = {
android: (androidSrcDir: ?string) => Task[],
metro: () => Task[],
npm: (projectRootDir: string, verifyCache?: boolean) => Task[],
bun: (projectRootDir: string) => Task[],
watchman: (projectRootDir: string) => Task[],
yarn: (projectRootDir: string) => Task[],
cocoapods?: (projectRootDir: string) => Task[],
};

const rmrf = (pathname: string) => {
if (!existsSync(pathname)) {
return;
}
rm(pathname, {maxRetries: 3, recursive: true, force: true});
};

export function cleanDir(
directory: string,
pattern: ?RegExp,
): () => Promise<void> {
return async function cleanDirectory() {
if (pattern != null) {
const base = path.dirname(directory);
const files = readdirSync(base).filter((filename: string) =>
pattern.test(filename),
);
for (const filename of files) {
rmrf(path.join(base, filename));
}
} else {
rmrf(directory);
}
};
}

export function cleanTmpDir(filepattern: RegExp): ReturnType<typeof cleanDir> {
return cleanDir(os.tmpdir(), filepattern);
}

// The tasks that cleanup various build artefacts.
export const tasks: CleanTasks = {
/**
* Cleans up the Android Gradle cache
*/
android: (androidSrcDir: ?string) => [
task('Clean Gradle cache', async function gradle(opts?: ExecaOptions) {
const gradlew = path.join(
androidSrcDir ?? 'android',
isWindows ? 'gradlew.bat' : 'gradlew',
);

if (!existsSync(gradlew)) {
return;
}
const script = path.basename(gradlew);
const cwd = path.dirname(gradlew);
await execa(isWindows ? script : `./${script}`, ['clean'], {
cwd,
...opts,
});
}),
],

/**
* Agressively cleans up all Metro caches.
*/
metro: () => [
task('Clean Metro cache', cleanTmpDir(/^metro-.+/)),
task('Clean Haste cache', cleanTmpDir(/^haste-map-.+/)),
task('Clean React Native cache', cleanTmpDir(/^react-.+/)),
],

/**
* Cleans up the `node_modules` folder and optionally garbage collects the npm cache.
*/
npm: (projectRootDir: string, verifyCache = false) => {
const _tasks = [
task(
'Remove node_modules',
cleanDir(path.join(projectRootDir, 'node_modules')),
),
];
if (verifyCache) {
_tasks.push(
task('Verify npm cache', (opts?: ExecaOptions) =>
execa('npm', ['cache', 'verify'], {cwd: projectRootDir, ...opts}),
),
);
}
return _tasks;
},

/**
* Cleans up the Bun cache.
*/
bun: (projectRootDir: string) => [
task('Clean Bun cache', (opts?: ExecaOptions) =>
execa('bun', ['pm', 'cache', 'rm'], {cwd: projectRootDir, ...opts}),
),
],

/**
* Stops Watchman and clears its cache
*/
watchman: (projectRootDir: string) => [
task('Stop Watchman', (opts?: ExecaOptions) =>
execa(isWindows ? 'tskill' : 'killall', ['watchman'], {
cwd: projectRootDir,
...opts,
}),
),
task('Delete Watchman cache', (opts?: ExecaOptions) =>
execa('watchman', ['watch-del-all'], {cwd: projectRootDir, ...opts}),
),
],

/**
* Cleans up the Yarn cache
*/
yarn: (projectRootDir: string) => [
task('Clean Yarn cache', (opts?: ExecaOptions) =>
execa('yarn', ['cache', 'clean'], {cwd: projectRootDir, ...opts}),
),
],
};

if (isMacOS) {
/**
* Cleans up the local and global CocoaPods cache
*/
tasks.cocoapods = (projectRootDir: string) => [
// TODO: add project root
task('Clean CocoaPods pod cache', function removePodCache(opts?: ExecaOptions) {
return execa('pod', ['cache', 'clean', '--all'], {
cwd: projectRootDir,
...opts,
});
}),
task('Remove installed CocoaPods', cleanDir('./ios/Pods')),
task('Remove CocoaPods spec cache', cleanDir('~/.cocoapods')),
];
}

//
// Internal CLI
//
15 changes: 15 additions & 0 deletions packages/core-cli-utils/private/types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* 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
* @oncall react_native
*/

export type Task = {
label: string,
action: () => Promise<mixed>,
};
24 changes: 24 additions & 0 deletions packages/core-cli-utils/private/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* 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
* @oncall react_native
*/

import type {Task} from './types';

import os from 'os';

export function task(label: string, action: Task['action']): Task {
return {
label,
action,
};
}

export const isWindows = os.platform() === 'win32';
export const isMacOS = os.platform() === 'darwin';